diff --git a/README.md b/README.md index d9ca9ad..ce9c5f2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,19 @@ ## 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. 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. -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? -- Extrair todos os produtos do banco Oracle. -- Gerar **embeddings** (vetores numéricos) a partir das descrições com um modelo pré-treinado. -- Armazenar esses vetores em um **índice FAISS**, permitindo buscas rápidas por similaridade. -- Salvar um **mapa de IDs** que relaciona os vetores aos produtos reais no banco de dados. +1. **Leitura dos produtos** a partir da tabela `produtos` via `oracledb`; +2. **Geração dos embeddings** usando o modelo `all-MiniLM-L6-v2` do pacote `sentence-transformers`; +3. **Criação da tabela `embeddings_produtos`** para armazenar os vetores diretamente no Oracle; +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 -cursor = connection.cursor() 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 -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') -``` - -4. Este modelo transforma descrições em vetores numéricos de 384 dimensões: - -```python embeddings = model.encode(descricoes, convert_to_numpy=True) ``` -5. Criação do Índice Vetorial com FAISS +--- -```python -import faiss -dim = embeddings.shape[1] -index = faiss.IndexFlatL2(dim) -index.add(embeddings) +### 4. Criação da Tabela de Embeddings (se não existir) + +A tabela `embeddings_produtos` é criada dinamicamente com os seguintes campos: + +- `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 -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: - -```python -with open("produto_id_map.pkl", "wb") as f: - pickle.dump(ids, f) +```sql +MERGE INTO embeddings_produtos ... ``` -### 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 @@ -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: - Embeddings da **OCI Generative AI** -- Índices vetoriais com **FAISS** +- Índices vetoriais com **Oracle Database 23ai** - Comparações fuzzy com **RapidFuzz** como fallback --- @@ -483,7 +512,7 @@ llm = ChatOCIGenAI( ### Usando o CLI ```bash -oci generative-ai model list --compartment-id + oci generative-ai model list --compartment-id ``` ### Usando o Python SDK diff --git a/source/process_vector_products.py b/source/process_vector_products.py index 6a96c62..850d329 100644 --- a/source/process_vector_products.py +++ b/source/process_vector_products.py @@ -1,9 +1,7 @@ import oracledb import os from sentence_transformers import SentenceTransformer -import faiss import numpy as np -import pickle # === CONFIGURAÇÃO ORACLE COM WALLET === WALLET_PATH = "/WALLET_PATH/Wallet_oradb23ai" @@ -14,7 +12,14 @@ PASSWORD = "Password" os.environ["TNS_ADMIN"] = WALLET_PATH # === 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() @@ -26,22 +31,52 @@ ids = [] descricoes = [] for row in rows: - ids.append({"id": row[0], "codigo": row[1], "descricao": row[2]}) - descricoes.append(row[2]) # Usado no embedding + ids.append((row[0], row[1], row[2])) + descricoes.append(row[2]) -# === GERAÇÃO DE EMBEDDINGS COM SENTENCE TRANSFORMERS === +# === GERAÇÃO DOS EMBEDDINGS === model = SentenceTransformer('all-MiniLM-L6-v2') embeddings = model.encode(descricoes, convert_to_numpy=True) -# === CRIAÇÃO DO ÍNDICE FAISS === -dim = embeddings.shape[1] -index = faiss.IndexFlatL2(dim) -index.add(embeddings) +# === CRIAÇÃO DA TABELA DE EMBEDDINGS (caso não exista) === +cursor.execute(""" + BEGIN + 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 === -faiss.write_index(index, "faiss_index.bin") +# === INSERÇÃO OU ATUALIZAÇÃO DOS DADOS === +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: - pickle.dump(ids, f) +connection.commit() +cursor.close() +connection.close() -print("✅ Vetores gerados e salvos com sucesso.") \ No newline at end of file +print("✅ Vetores gravados com sucesso no banco Oracle.") \ No newline at end of file diff --git a/source/product_search.py b/source/product_search.py index ef5ddae..cd8aa5f 100644 --- a/source/product_search.py +++ b/source/product_search.py @@ -1,9 +1,7 @@ -# product_search.py - -import faiss -import pickle -import difflib +import os +import oracledb import numpy as np +import difflib from rapidfuzz import fuzz from langchain_community.embeddings import OCIGenAIEmbeddings @@ -11,19 +9,26 @@ from langchain_community.embeddings import OCIGenAIEmbeddings class BuscaProdutoSimilar: def __init__( self, - faiss_index_path="faiss_index.bin", - id_map_path="produto_id_map.pkl", top_k=5, distancia_minima=1.0, model_id="cohere.embed-english-light-v3.0", service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", - compartment_id="ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - auth_profile="DEFAULT" + compartment_id="ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + auth_profile="DEFAULT", + wallet_path="/WALLET_PATH/Wallet_oradb23ai", + db_alias="oradb23ai_high", + username="USER", + password="Password" ): - print("📦 Carregando índice vetorial...") - self.index = faiss.read_index(faiss_index_path) - with open(id_map_path, "rb") as f: - self.id_map = pickle.load(f) + os.environ["TNS_ADMIN"] = wallet_path + self.conn = oracledb.connect( + user=username, + password=password, + dsn=db_alias, + config_dir=wallet_path, + wallet_location=wallet_path, + wallet_password=password + ) self.top_k = top_k self.distancia_minima = distancia_minima self.embedding = OCIGenAIEmbeddings( @@ -33,8 +38,27 @@ class BuscaProdutoSimilar: 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): - 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) return sugestoes[0] if sugestoes else input_usuario @@ -50,12 +74,16 @@ class BuscaProdutoSimilar: } consulta_emb = self.embedding.embed_query(descricao_corrigida) - consulta_emb = np.array([consulta_emb]) - distances, indices = self.index.search(consulta_emb, self.top_k) + consulta_emb = np.array(consulta_emb) - 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: - match = self.id_map[i] + match = self.produtos[idx] similaridade = 1 / (1 + dist) resultados["semanticos"].append({ "id": match["id"], @@ -67,7 +95,7 @@ class BuscaProdutoSimilar: if not resultados["semanticos"]: melhores_fuzz = [] - for produto in self.id_map: + for produto in self.produtos: score = fuzz.token_sort_ratio(descricao_corrigida, produto["descricao"]) melhores_fuzz.append((produto, score)) melhores_fuzz.sort(key=lambda x: x[1], reverse=True)