mirror of
https://github.com/hoshikawa2/agent-ai-mcp-server.git
synced 2026-03-06 10:11:01 +00:00
adjustments
This commit is contained in:
131
README.md
131
README.md
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
## Introdução
|
## Introdução
|
||||||
|
|
||||||
|
|
||||||
|
Empresas que lidam com grandes volumes de produtos — como distribuidores, indústrias e redes de varejo — frequentemente enfrentam o desafio de identificar produtos com base em descrições textuais imprecisas, incompletas ou variadas. Em ambientes onde os dados são inseridos manualmente, erros de digitação, abreviações e nomes comerciais diferentes podem dificultar a identificação correta dos itens em sistemas como ERPs, CRMs e plataformas de e-commerce.
|
||||||
|
|
||||||
|
Neste cenário, é comum a necessidade de ferramentas que consigam:
|
||||||
|
|
||||||
|
• Interpretar descrições informais ou incorretas fornecidas por usuários;
|
||||||
|
|
||||||
|
• Sugerir os produtos mais semelhantes com base em similaridade semântica;
|
||||||
|
|
||||||
|
• Garantir um fallback com algoritmos tradicionais (como fuzzy matching), caso a busca semântica não encontre resultados relevantes;
|
||||||
|
|
||||||
|
• Ser integrável a APIs e fluxos automatizados de agentes inteligentes.
|
||||||
|
|
||||||
Neste tutorial, você aprenderá a criar um agente de IA especializado na resolução de inconsistências em **notas fiscais de devolução de clientes**. O agente é capaz de interagir com um **servidor MCP** que fornece ferramentas de busca vetorial e recuperação de notas fiscais, permitindo que o agente encontre automaticamente a **nota fiscal de saída original da empresa** com base em informações fornecidas pelo cliente.
|
Neste tutorial, você aprenderá a criar um agente de IA especializado na resolução de inconsistências em **notas fiscais de devolução de clientes**. O agente é capaz de interagir com um **servidor MCP** que fornece ferramentas de busca vetorial e recuperação de notas fiscais, permitindo que o agente encontre automaticamente a **nota fiscal de saída original da empresa** com base em informações fornecidas pelo cliente.
|
||||||
|
|
||||||
A comunicação entre o agente e o servidor ocorre via protocolo **MCP (Multi-Agent Communication Protocol)**, garantindo modularidade, escalabilidade e integração eficiente entre serviços.
|
A comunicação entre o agente e o servidor ocorre via protocolo **MCP (Multi-Agent Communication Protocol)**, garantindo modularidade, escalabilidade e integração eficiente entre serviços.
|
||||||
@@ -189,81 +202,97 @@ ORDER BY similaridade DESC;
|
|||||||
|
|
||||||
Nesta tarefa, vamos **complementar a busca avançada baseada em SQL** com uma nova abordagem baseada em **vetores semânticos**. Isso será especialmente útil para agentes de IA que usam embeddings (representações numéricas de frases) para comparar similaridade entre descrições de produtos — de forma mais flexível e inteligente que buscas por palavras ou fonética.
|
Nesta tarefa, vamos **complementar a busca avançada baseada em SQL** com uma nova abordagem baseada em **vetores semânticos**. Isso será especialmente útil para agentes de IA que usam embeddings (representações numéricas de frases) para comparar similaridade entre descrições de produtos — de forma mais flexível e inteligente que buscas por palavras ou fonética.
|
||||||
|
|
||||||
Para isso, será utilizado o script Python `process_vector_products.py`, que conecta ao banco Oracle, extrai os produtos da tabela `PRODUTOS`, transforma suas descrições em vetores (embeddings), e constrói um índice vetorial utilizando FAISS.
|
Para isso, será utilizado o script Python `process_vector_products.py`, que conecta ao banco Oracle, extrai os produtos da tabela `PRODUTOS`, transforma suas descrições em vetores (embeddings), e constrói um índice vetorial utilizando o próprio banco de dados Oracle.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### O Que o Script Faz?
|
### O Que o Script Faz?
|
||||||
|
|
||||||
- Extrair todos os produtos do banco Oracle.
|
1. **Leitura dos produtos** a partir da tabela `produtos` via `oracledb`;
|
||||||
- Gerar **embeddings** (vetores numéricos) a partir das descrições com um modelo pré-treinado.
|
2. **Geração dos embeddings** usando o modelo `all-MiniLM-L6-v2` do pacote `sentence-transformers`;
|
||||||
- Armazenar esses vetores em um **índice FAISS**, permitindo buscas rápidas por similaridade.
|
3. **Criação da tabela `embeddings_produtos`** para armazenar os vetores diretamente no Oracle;
|
||||||
- Salvar um **mapa de IDs** que relaciona os vetores aos produtos reais no banco de dados.
|
4. **Inserção ou atualização dos registros**, gravando o vetor como um BLOB binário (em formato `float32` serializado).
|
||||||
|
|
||||||
1. Consulta de Produtos no Banco
|
> **Nota:** Os embeddings são convertidos em bytes com `np.float32.tobytes()` para serem armazenados como BLOB. Para recuperar os vetores, utilize `np.frombuffer(blob, dtype=np.float32)`.
|
||||||
|
|
||||||
|
Esse formato permite que futuras buscas por similaridade sejam feitas diretamente via SQL ou carregando os vetores do banco para operações com `np.dot`, `cosine_similarity` ou integração com LLMs.
|
||||||
|
|
||||||
|
Este script realiza a geração de embeddings semânticos para produtos e grava esses vetores no banco de dados Oracle 23ai. A seguir, destacamos os pontos principais:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Configuração da Conexão com Oracle usando Wallet
|
||||||
|
|
||||||
|
O código utiliza a biblioteca `oracledb` em modo **thin** e configura o acesso seguro usando um **Oracle Wallet**.
|
||||||
|
|
||||||
|
```python
|
||||||
|
os.environ["TNS_ADMIN"] = WALLET_PATH
|
||||||
|
connection = oracledb.connect(
|
||||||
|
user=USERNAME,
|
||||||
|
password=PASSWORD,
|
||||||
|
dsn=DB_ALIAS,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Consulta à Tabela de Produtos
|
||||||
|
|
||||||
|
A tabela `produtos` contém os dados originais (ID, código e descrição). Essas descrições são usadas como base para gerar os vetores semânticos.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
cursor = connection.cursor()
|
|
||||||
cursor.execute("SELECT id, codigo, descricao FROM produtos")
|
cursor.execute("SELECT id, codigo, descricao FROM produtos")
|
||||||
rows = cursor.fetchall()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Lê todos os produtos e salva os dados em duas listas:
|
---
|
||||||
|
|
||||||
|
### 3. Geração de Embeddings com `sentence-transformers`
|
||||||
|
|
||||||
|
O modelo `all-MiniLM-L6-v2` é utilizado para transformar as descrições dos produtos em vetores numéricos de alta dimensão.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ids = [] # Metadados de produtos (id, código, descrição)
|
|
||||||
descricoes = [] # Apenas descrições (usadas para gerar embeddings)
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Geração de Embeddings com Sentence Transformers
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sentence_transformers import SentenceTransformer
|
|
||||||
model = SentenceTransformer('all-MiniLM-L6-v2')
|
model = SentenceTransformer('all-MiniLM-L6-v2')
|
||||||
```
|
|
||||||
|
|
||||||
4. Este modelo transforma descrições em vetores numéricos de 384 dimensões:
|
|
||||||
|
|
||||||
```python
|
|
||||||
embeddings = model.encode(descricoes, convert_to_numpy=True)
|
embeddings = model.encode(descricoes, convert_to_numpy=True)
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Criação do Índice Vetorial com FAISS
|
---
|
||||||
|
|
||||||
```python
|
### 4. Criação da Tabela de Embeddings (se não existir)
|
||||||
import faiss
|
|
||||||
dim = embeddings.shape[1]
|
A tabela `embeddings_produtos` é criada dinamicamente com os seguintes campos:
|
||||||
index = faiss.IndexFlatL2(dim)
|
|
||||||
index.add(embeddings)
|
- `id`: identificador do produto (chave primária)
|
||||||
|
- `codigo`: código do produto
|
||||||
|
- `descricao`: descrição original
|
||||||
|
- `vetor`: BLOB contendo o vetor serializado em `float32`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE embeddings_produtos (
|
||||||
|
id NUMBER PRIMARY KEY,
|
||||||
|
codigo VARCHAR2(100),
|
||||||
|
descricao VARCHAR2(4000),
|
||||||
|
vetor BLOB
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Salvando o Índice e o Mapeamento de Produtos
|
> Obs.: A criação usa `EXECUTE IMMEDIATE` dentro de um `BEGIN...EXCEPTION` para evitar erro se a tabela já existir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Inserção ou Atualização via `MERGE`
|
||||||
|
|
||||||
|
Para cada produto, o vetor é convertido em bytes (`float32`) e inserido ou atualizado na tabela `embeddings_produtos` usando um `MERGE INTO`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
faiss.write_index(index, "faiss_index.bin")
|
vetor_bytes = vetor.astype(np.float32).tobytes()
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Grava o índice FAISS no disco. Em paralelo, salva o dicionário de produtos (ID, código e descrição) em um arquivo .pkl:
|
```sql
|
||||||
|
MERGE INTO embeddings_produtos ...
|
||||||
```python
|
|
||||||
with open("produto_id_map.pkl", "wb") as f:
|
|
||||||
pickle.dump(ids, f)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1. Conexão com o Oracle Autonomous Database
|
---
|
||||||
|
|
||||||
Defina as variáveis de conexão, incluindo o uso do Oracle Wallet para autenticação segura:
|
|
||||||
|
|
||||||
WALLET_PATH = "/caminho/para/Wallet"
|
|
||||||
DB_ALIAS = "oradb23ai_high"
|
|
||||||
USERNAME = "admin"
|
|
||||||
PASSWORD = "..."
|
|
||||||
|
|
||||||
os.environ["TNS_ADMIN"] = WALLET_PATH
|
|
||||||
|
|
||||||
connection = oracledb.connect(...)
|
|
||||||
|
|
||||||
>A variável TNS_ADMIN aponta para o diretório com os arquivos de wallet (sqlnet.ora, tnsnames.ora, etc).
|
|
||||||
|
|
||||||
### Para Executar o Script
|
### Para Executar o Script
|
||||||
|
|
||||||
@@ -437,7 +466,7 @@ if __name__ == "__main__":
|
|||||||
Este módulo `product_search.py` implementa uma classe Python que permite buscar produtos semanticamente similares a partir de uma descrição textual, utilizando:
|
Este módulo `product_search.py` implementa uma classe Python que permite buscar produtos semanticamente similares a partir de uma descrição textual, utilizando:
|
||||||
|
|
||||||
- Embeddings da **OCI Generative AI**
|
- Embeddings da **OCI Generative AI**
|
||||||
- Índices vetoriais com **FAISS**
|
- Índices vetoriais com **Oracle Database 23ai**
|
||||||
- Comparações fuzzy com **RapidFuzz** como fallback
|
- Comparações fuzzy com **RapidFuzz** como fallback
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -483,7 +512,7 @@ llm = ChatOCIGenAI(
|
|||||||
### Usando o CLI
|
### Usando o CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
oci generative-ai model list --compartment-id <seu_compartment_id>
|
oci generative-ai model list --compartment-id <seu_compartment_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usando o Python SDK
|
### Usando o Python SDK
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import oracledb
|
import oracledb
|
||||||
import os
|
import os
|
||||||
from sentence_transformers import SentenceTransformer
|
from sentence_transformers import SentenceTransformer
|
||||||
import faiss
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pickle
|
|
||||||
|
|
||||||
# === CONFIGURAÇÃO ORACLE COM WALLET ===
|
# === CONFIGURAÇÃO ORACLE COM WALLET ===
|
||||||
WALLET_PATH = "/WALLET_PATH/Wallet_oradb23ai"
|
WALLET_PATH = "/WALLET_PATH/Wallet_oradb23ai"
|
||||||
@@ -14,7 +12,14 @@ PASSWORD = "Password"
|
|||||||
os.environ["TNS_ADMIN"] = WALLET_PATH
|
os.environ["TNS_ADMIN"] = WALLET_PATH
|
||||||
|
|
||||||
# === CONECTANDO USANDO oracledb (modo thin) ===
|
# === CONECTANDO USANDO oracledb (modo thin) ===
|
||||||
connection = oracledb.connect(user=USERNAME, password=PASSWORD, dsn=DB_ALIAS, config_dir=WALLET_PATH, wallet_location=WALLET_PATH, wallet_password=PASSWORD)
|
connection = oracledb.connect(
|
||||||
|
user=USERNAME,
|
||||||
|
password=PASSWORD,
|
||||||
|
dsn=DB_ALIAS,
|
||||||
|
config_dir=WALLET_PATH,
|
||||||
|
wallet_location=WALLET_PATH,
|
||||||
|
wallet_password=PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
@@ -26,22 +31,52 @@ ids = []
|
|||||||
descricoes = []
|
descricoes = []
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
ids.append({"id": row[0], "codigo": row[1], "descricao": row[2]})
|
ids.append((row[0], row[1], row[2]))
|
||||||
descricoes.append(row[2]) # Usado no embedding
|
descricoes.append(row[2])
|
||||||
|
|
||||||
# === GERAÇÃO DE EMBEDDINGS COM SENTENCE TRANSFORMERS ===
|
# === GERAÇÃO DOS EMBEDDINGS ===
|
||||||
model = SentenceTransformer('all-MiniLM-L6-v2')
|
model = SentenceTransformer('all-MiniLM-L6-v2')
|
||||||
embeddings = model.encode(descricoes, convert_to_numpy=True)
|
embeddings = model.encode(descricoes, convert_to_numpy=True)
|
||||||
|
|
||||||
# === CRIAÇÃO DO ÍNDICE FAISS ===
|
# === CRIAÇÃO DA TABELA DE EMBEDDINGS (caso não exista) ===
|
||||||
dim = embeddings.shape[1]
|
cursor.execute("""
|
||||||
index = faiss.IndexFlatL2(dim)
|
BEGIN
|
||||||
index.add(embeddings)
|
EXECUTE IMMEDIATE '
|
||||||
|
CREATE TABLE embeddings_produtos (
|
||||||
|
id NUMBER PRIMARY KEY,
|
||||||
|
codigo VARCHAR2(100),
|
||||||
|
descricao VARCHAR2(4000),
|
||||||
|
vetor BLOB
|
||||||
|
)';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLCODE != -955 THEN
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
""")
|
||||||
|
|
||||||
# === SALVANDO O ÍNDICE E O MAPA DE PRODUTOS ===
|
# === INSERÇÃO OU ATUALIZAÇÃO DOS DADOS ===
|
||||||
faiss.write_index(index, "faiss_index.bin")
|
for (id_, codigo, descricao), vetor in zip(ids, embeddings):
|
||||||
|
vetor_bytes = vetor.astype(np.float32).tobytes()
|
||||||
|
cursor.execute("""
|
||||||
|
MERGE INTO embeddings_produtos tgt
|
||||||
|
USING (SELECT :id AS id FROM dual) src
|
||||||
|
ON (tgt.id = src.id)
|
||||||
|
WHEN MATCHED THEN
|
||||||
|
UPDATE SET codigo = :codigo, descricao = :descricao, vetor = :vetor
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT (id, codigo, descricao, vetor)
|
||||||
|
VALUES (:id, :codigo, :descricao, :vetor)
|
||||||
|
""", {
|
||||||
|
"id": id_,
|
||||||
|
"codigo": codigo,
|
||||||
|
"descricao": descricao,
|
||||||
|
"vetor": vetor_bytes
|
||||||
|
})
|
||||||
|
|
||||||
with open("produto_id_map.pkl", "wb") as f:
|
connection.commit()
|
||||||
pickle.dump(ids, f)
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
print("✅ Vetores gerados e salvos com sucesso.")
|
print("✅ Vetores gravados com sucesso no banco Oracle.")
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
# product_search.py
|
import os
|
||||||
|
import oracledb
|
||||||
import faiss
|
|
||||||
import pickle
|
|
||||||
import difflib
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import difflib
|
||||||
from rapidfuzz import fuzz
|
from rapidfuzz import fuzz
|
||||||
from langchain_community.embeddings import OCIGenAIEmbeddings
|
from langchain_community.embeddings import OCIGenAIEmbeddings
|
||||||
|
|
||||||
@@ -11,19 +9,26 @@ from langchain_community.embeddings import OCIGenAIEmbeddings
|
|||||||
class BuscaProdutoSimilar:
|
class BuscaProdutoSimilar:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
faiss_index_path="faiss_index.bin",
|
|
||||||
id_map_path="produto_id_map.pkl",
|
|
||||||
top_k=5,
|
top_k=5,
|
||||||
distancia_minima=1.0,
|
distancia_minima=1.0,
|
||||||
model_id="cohere.embed-english-light-v3.0",
|
model_id="cohere.embed-english-light-v3.0",
|
||||||
service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com",
|
service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com",
|
||||||
compartment_id="ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
compartment_id="ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
auth_profile="DEFAULT"
|
auth_profile="DEFAULT",
|
||||||
|
wallet_path="/WALLET_PATH/Wallet_oradb23ai",
|
||||||
|
db_alias="oradb23ai_high",
|
||||||
|
username="USER",
|
||||||
|
password="Password"
|
||||||
):
|
):
|
||||||
print("📦 Carregando índice vetorial...")
|
os.environ["TNS_ADMIN"] = wallet_path
|
||||||
self.index = faiss.read_index(faiss_index_path)
|
self.conn = oracledb.connect(
|
||||||
with open(id_map_path, "rb") as f:
|
user=username,
|
||||||
self.id_map = pickle.load(f)
|
password=password,
|
||||||
|
dsn=db_alias,
|
||||||
|
config_dir=wallet_path,
|
||||||
|
wallet_location=wallet_path,
|
||||||
|
wallet_password=password
|
||||||
|
)
|
||||||
self.top_k = top_k
|
self.top_k = top_k
|
||||||
self.distancia_minima = distancia_minima
|
self.distancia_minima = distancia_minima
|
||||||
self.embedding = OCIGenAIEmbeddings(
|
self.embedding = OCIGenAIEmbeddings(
|
||||||
@@ -33,8 +38,27 @@ class BuscaProdutoSimilar:
|
|||||||
auth_profile=auth_profile
|
auth_profile=auth_profile
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print("📦 Carregando vetores do Oracle...")
|
||||||
|
self._carregar_embeddings()
|
||||||
|
|
||||||
|
def _carregar_embeddings(self):
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
cursor.execute("SELECT id, codigo, descricao, vetor FROM embeddings_produtos")
|
||||||
|
self.vetores = []
|
||||||
|
self.produtos = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
id_, codigo, descricao, blob = row
|
||||||
|
vetor = np.frombuffer(blob.read(), dtype=np.float32)
|
||||||
|
self.vetores.append(vetor)
|
||||||
|
self.produtos.append({
|
||||||
|
"id": id_,
|
||||||
|
"codigo": codigo,
|
||||||
|
"descricao": descricao
|
||||||
|
})
|
||||||
|
self.vetores = np.array(self.vetores)
|
||||||
|
|
||||||
def _corrigir_input(self, input_usuario):
|
def _corrigir_input(self, input_usuario):
|
||||||
descricoes = [p["descricao"] for p in self.id_map]
|
descricoes = [p["descricao"] for p in self.produtos]
|
||||||
sugestoes = difflib.get_close_matches(input_usuario, descricoes, n=1, cutoff=0.6)
|
sugestoes = difflib.get_close_matches(input_usuario, descricoes, n=1, cutoff=0.6)
|
||||||
return sugestoes[0] if sugestoes else input_usuario
|
return sugestoes[0] if sugestoes else input_usuario
|
||||||
|
|
||||||
@@ -50,12 +74,16 @@ class BuscaProdutoSimilar:
|
|||||||
}
|
}
|
||||||
|
|
||||||
consulta_emb = self.embedding.embed_query(descricao_corrigida)
|
consulta_emb = self.embedding.embed_query(descricao_corrigida)
|
||||||
consulta_emb = np.array([consulta_emb])
|
consulta_emb = np.array(consulta_emb)
|
||||||
distances, indices = self.index.search(consulta_emb, self.top_k)
|
|
||||||
|
|
||||||
for i, dist in zip(indices[0], distances[0]):
|
# Cálculo de distância euclidiana
|
||||||
|
dists = np.linalg.norm(self.vetores - consulta_emb, axis=1)
|
||||||
|
top_indices = np.argsort(dists)[:self.top_k]
|
||||||
|
|
||||||
|
for idx in top_indices:
|
||||||
|
dist = dists[idx]
|
||||||
if dist < self.distancia_minima:
|
if dist < self.distancia_minima:
|
||||||
match = self.id_map[i]
|
match = self.produtos[idx]
|
||||||
similaridade = 1 / (1 + dist)
|
similaridade = 1 / (1 + dist)
|
||||||
resultados["semanticos"].append({
|
resultados["semanticos"].append({
|
||||||
"id": match["id"],
|
"id": match["id"],
|
||||||
@@ -67,7 +95,7 @@ class BuscaProdutoSimilar:
|
|||||||
|
|
||||||
if not resultados["semanticos"]:
|
if not resultados["semanticos"]:
|
||||||
melhores_fuzz = []
|
melhores_fuzz = []
|
||||||
for produto in self.id_map:
|
for produto in self.produtos:
|
||||||
score = fuzz.token_sort_ratio(descricao_corrigida, produto["descricao"])
|
score = fuzz.token_sort_ratio(descricao_corrigida, produto["descricao"])
|
||||||
melhores_fuzz.append((produto, score))
|
melhores_fuzz.append((produto, score))
|
||||||
melhores_fuzz.sort(key=lambda x: x[1], reverse=True)
|
melhores_fuzz.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user