mirror of
https://github.com/hoshikawa2/agent-ai-mcp-server.git
synced 2026-03-06 02:10:36 +00:00
First commit
This commit is contained in:
BIN
source/faiss_index.bin
Normal file
BIN
source/faiss_index.bin
Normal file
Binary file not shown.
72
source/find_products_by_similarity.py
Normal file
72
source/find_products_by_similarity.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import faiss
|
||||
import pickle
|
||||
import difflib
|
||||
from rapidfuzz import fuzz
|
||||
from langchain_community.embeddings import OCIGenAIEmbeddings
|
||||
import numpy as np
|
||||
|
||||
# === CONFIGURAÇÕES ===
|
||||
FAISS_INDEX_PATH = "faiss_index.bin"
|
||||
ID_MAP_PATH = "produto_id_map.pkl"
|
||||
TOP_K = 5
|
||||
DISTANCIA_MINIMA = 1.0
|
||||
|
||||
# === EMBEDDING COM OCI GEN AI ===
|
||||
embedding = OCIGenAIEmbeddings(
|
||||
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"
|
||||
)
|
||||
|
||||
# === CARREGA O ÍNDICE VETORIAL ===
|
||||
print("📦 Carregando índice vetorial...")
|
||||
index = faiss.read_index(FAISS_INDEX_PATH)
|
||||
with open(ID_MAP_PATH, "rb") as f:
|
||||
id_map = pickle.load(f)
|
||||
|
||||
# === CORREÇÃO AUTOMÁTICA DO INPUT ===
|
||||
def corrigir_input_mais_proximo(input_usuario, descricoes_existentes):
|
||||
sugestoes = difflib.get_close_matches(input_usuario, descricoes_existentes, n=1, cutoff=0.6)
|
||||
return sugestoes[0] if sugestoes else input_usuario
|
||||
|
||||
descricao_input = input("Digite a descrição do produto a buscar: ").strip()
|
||||
descricoes = [p["descricao"] for p in id_map]
|
||||
descricao_corrigida = corrigir_input_mais_proximo(descricao_input, descricoes)
|
||||
|
||||
if descricao_corrigida != descricao_input:
|
||||
print(f"🧠 Consulta sugerida: {descricao_corrigida}")
|
||||
else:
|
||||
print(f"✅ Consulta original mantida: {descricao_input}")
|
||||
|
||||
# === GERA EMBEDDING COM OCI ===
|
||||
consulta_emb = embedding.embed_query(descricao_corrigida)
|
||||
consulta_emb = np.array([consulta_emb]) # FAISS espera um array 2D
|
||||
|
||||
# === BUSCA NO FAISS ===
|
||||
distances, indices = index.search(consulta_emb, TOP_K)
|
||||
bons_resultados = [d for d in distances[0] if d < DISTANCIA_MINIMA]
|
||||
|
||||
# === EXIBE RESULTADOS VETORIAIS ===
|
||||
if bons_resultados:
|
||||
print("\n🔍 Resultados semânticos similares:")
|
||||
for i, dist in zip(indices[0], distances[0]):
|
||||
if dist >= DISTANCIA_MINIMA:
|
||||
continue
|
||||
match = id_map[i]
|
||||
similaridade = 1 / (1 + dist)
|
||||
print(f"ID: {match['id']} | Código: {match['codigo']} | Produto: {match['descricao']}")
|
||||
print(f"↳ Similaridade: {similaridade:.2%} | Distância: {dist:.4f}\n")
|
||||
else:
|
||||
# === FALLBACK FUZZY ===
|
||||
print("\n⚠️ Nenhum resultado vetorial relevante. Buscando por similaridade textual (fuzzy)...\n")
|
||||
melhores_fuzz = []
|
||||
for produto in id_map:
|
||||
score = fuzz.token_sort_ratio(descricao_corrigida, produto["descricao"])
|
||||
melhores_fuzz.append((produto, score))
|
||||
|
||||
melhores_fuzz.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for produto, score in melhores_fuzz[:TOP_K]:
|
||||
print(f"ID: {produto['id']} | Código: {produto['codigo']} | Produto: {produto['descricao']}")
|
||||
print(f"↳ Similaridade (fuzzy): {score:.2f}%\n")
|
||||
24
source/find_products_by_vector.py
Normal file
24
source/find_products_by_vector.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from sentence_transformers import SentenceTransformer
|
||||
import faiss
|
||||
import pickle
|
||||
|
||||
# === CARREGAR MODELO E ÍNDICE ===
|
||||
model = SentenceTransformer('all-MiniLM-L6-v2')
|
||||
index = faiss.read_index("faiss_index.bin")
|
||||
|
||||
with open("produto_id_map.pkl", "rb") as f:
|
||||
id_map = pickle.load(f)
|
||||
|
||||
# === CONSULTA DO USUÁRIO ===
|
||||
descricao_input = "harry potter especial"
|
||||
consulta_emb = model.encode([descricao_input], convert_to_numpy=True)
|
||||
|
||||
# === BUSCAR PRODUTOS MAIS SIMILARES ===
|
||||
k = 5
|
||||
distances, indices = index.search(consulta_emb, k)
|
||||
|
||||
# === EXIBIR RESULTADOS ===
|
||||
print("\n🔍 Resultados similares:")
|
||||
for i, dist in zip(indices[0], distances[0]):
|
||||
match = id_map[i]
|
||||
print(f"ID: {match['id']} | Código: {match['codigo']} | Produto: {match['descricao']} | Distância: {dist:.2f}")
|
||||
124
source/fn_busca_fonetica_produtos.sql
Normal file
124
source/fn_busca_fonetica_produtos.sql
Normal file
@@ -0,0 +1,124 @@
|
||||
CREATE TABLE produtos (
|
||||
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
codigo VARCHAR2(50),
|
||||
descricao VARCHAR2(4000)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_texto_descricao ON produtos(descricao)
|
||||
INDEXTYPE IS CTXSYS.CONTEXT;
|
||||
|
||||
|
||||
-- DROP TYPES (se existirem)
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE 'DROP TYPE produto_resultado_tab';
|
||||
EXECUTE IMMEDIATE 'DROP TYPE produto_resultado';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN NULL;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Criação de um tipo de tabela para retorno da função
|
||||
CREATE OR REPLACE TYPE produto_resultado AS OBJECT (
|
||||
codigo VARCHAR2(50),
|
||||
descricao VARCHAR2(4000),
|
||||
similaridade NUMBER
|
||||
);
|
||||
|
||||
CREATE OR REPLACE TYPE produto_resultado_tab AS TABLE OF produto_resultado;
|
||||
/
|
||||
|
||||
-- Função que faz busca por palavras aproximadas por fonética ou distância
|
||||
-- Criação de um tipo de tabela para retorno da função
|
||||
CREATE OR REPLACE TYPE produto_resultado AS OBJECT (
|
||||
codigo VARCHAR2(50),
|
||||
descricao VARCHAR2(4000),
|
||||
similaridade NUMBER
|
||||
);
|
||||
|
||||
CREATE OR REPLACE TYPE produto_resultado_tab AS TABLE OF produto_resultado;
|
||||
/
|
||||
|
||||
-- Função de busca fonética e por palavras-chave
|
||||
CREATE OR REPLACE FUNCTION fn_busca_avancada(p_termos IN VARCHAR2)
|
||||
RETURN produto_resultado_tab PIPELINED
|
||||
AS
|
||||
v_termos SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST();
|
||||
v_token VARCHAR2(1000);
|
||||
v_descricao VARCHAR2(4000);
|
||||
v_score NUMBER;
|
||||
v_dummy NUMBER;
|
||||
BEGIN
|
||||
-- Dividir os termos da busca
|
||||
FOR i IN 1..REGEXP_COUNT(p_termos, '\S+') LOOP
|
||||
v_termos.EXTEND;
|
||||
v_termos(i) := LOWER(REGEXP_SUBSTR(p_termos, '\S+', 1, i));
|
||||
END LOOP;
|
||||
|
||||
-- Loop pelos produtos
|
||||
FOR prod IN (SELECT codigo, descricao FROM produtos) LOOP
|
||||
v_descricao := LOWER(prod.descricao);
|
||||
v_score := 0;
|
||||
|
||||
-- Avaliar cada termo da busca
|
||||
FOR i IN 1..v_termos.COUNT LOOP
|
||||
v_token := v_termos(i);
|
||||
|
||||
-- 3 pontos se encontrar diretamente
|
||||
IF v_descricao LIKE '%' || v_token || '%' THEN
|
||||
v_score := v_score + 3;
|
||||
ELSE
|
||||
-- 2 pontos se foneticamente similar
|
||||
BEGIN
|
||||
SELECT 1 INTO v_dummy FROM dual
|
||||
WHERE SOUNDEX(v_token) IN (
|
||||
SELECT SOUNDEX(REGEXP_SUBSTR(v_descricao, '\w+', 1, LEVEL))
|
||||
FROM dual
|
||||
CONNECT BY LEVEL <= REGEXP_COUNT(v_descricao, '\w+')
|
||||
);
|
||||
v_score := v_score + 2;
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN NULL;
|
||||
END;
|
||||
|
||||
-- 1 ponto se similar por escrita
|
||||
BEGIN
|
||||
SELECT 1 INTO v_dummy FROM dual
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM (
|
||||
SELECT REGEXP_SUBSTR(v_descricao, '\w+', 1, LEVEL) AS palavra
|
||||
FROM dual
|
||||
CONNECT BY LEVEL <= REGEXP_COUNT(v_descricao, '\w+')
|
||||
)
|
||||
WHERE UTL_MATCH.EDIT_DISTANCE(palavra, v_token) <= 2
|
||||
);
|
||||
v_score := v_score + 1;
|
||||
EXCEPTION
|
||||
WHEN NO_DATA_FOUND THEN NULL;
|
||||
END;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- Só retorna se houver ao menos algum match
|
||||
IF v_score > 0 THEN
|
||||
PIPE ROW(produto_resultado(prod.codigo, prod.descricao, v_score));
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
RETURN;
|
||||
END;
|
||||
/
|
||||
|
||||
-- Grant para execução, se necessário:
|
||||
GRANT EXECUTE ON fn_busca_fonetica_por_palavra TO PUBLIC;
|
||||
|
||||
|
||||
-- Testes
|
||||
SELECT *
|
||||
FROM TABLE(fn_busca_avancada('harry poter pedra'))
|
||||
ORDER BY similaridade DESC;
|
||||
|
||||
SELECT * FROM TABLE(fn_busca_fonetica_por_palavra('velho mar'))
|
||||
ORDER BY similaridade DESC;
|
||||
|
||||
|
||||
100
source/inserts_produtos_livros.sql
Normal file
100
source/inserts_produtos_livros.sql
Normal file
@@ -0,0 +1,100 @@
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1001', '1984 - Edição Comentada - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1002', 'O Senhor dos Anéis - Nova Ortografia - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1003', 'O Velho e o Mar - Edição Comentada - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1004', 'O Velho e o Mar (Capa Dura) - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1005', 'O Velho e o Mar - Coleção Clássicos - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1006', 'It: A Coisa - Stephen King');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1007', 'It: A Coisa - Ilustrado - Stephen King');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1008', 'Assassinato no Expresso Oriente (Capa Dura) - Agatha Christie');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1009', '1984 - Edição Comentada - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1010', 'Dom Casmurro - Edição Comentada - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1011', 'Dom Casmurro - Nova Ortografia - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1012', 'O Velho e o Mar - Coleção Clássicos - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1013', 'O Velho e o Mar (Inglês) - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1014', 'Ensaio sobre a Cegueira (Capa Dura) - José Saramago');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1015', 'Dom Casmurro - Coleção Clássicos - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1016', '1984 (Capa Dura) - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1017', 'Cem Anos de Solidão - Edição Especial - Gabriel García Márquez');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1018', 'Assassinato no Expresso Oriente (Inglês) - Agatha Christie');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1019', 'A Hora da Estrela (Inglês) - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1020', 'Harry Potter e a Pedra Filosofal - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1021', '1984 (Inglês) - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1022', 'Harry Potter e a Pedra Filosofal - Edição Especial - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1023', 'O Senhor dos Anéis - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1024', 'A Hora da Estrela (Capa Dura) - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1025', '1984 - Edição Especial - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1026', 'Dom Casmurro (Inglês) - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1027', 'Cem Anos de Solidão (Capa Dura) - Gabriel García Márquez');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1028', 'It: A Coisa (Português) - Stephen King');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1029', 'A Hora da Estrela (Inglês) - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1030', 'O Senhor dos Anéis (Português) - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1031', 'Cem Anos de Solidão - Nova Ortografia - Gabriel García Márquez');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1032', 'O Velho e o Mar - Edição Comentada - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1033', 'O Senhor dos Anéis - Nova Ortografia - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1034', 'O Velho e o Mar - Volume Único - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1035', '1984 (Português) - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1036', 'It: A Coisa - Edição Especial - Stephen King');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1037', '1984 (Inglês) - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1038', 'Ensaio sobre a Cegueira (Capa Dura) - José Saramago');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1039', 'A Hora da Estrela (Português) - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1040', 'A Hora da Estrela (Português) - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1041', 'Ensaio sobre a Cegueira - Coleção Clássicos - José Saramago');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1042', '1984 (Capa Dura) - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1043', 'O Velho e o Mar - Coleção Clássicos - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1044', 'Ensaio sobre a Cegueira (Português) - José Saramago');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1045', 'Assassinato no Expresso Oriente - Agatha Christie');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1046', 'Assassinato no Expresso Oriente - Agatha Christie');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1047', 'Cem Anos de Solidão - Ilustrado - Gabriel García Márquez');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1048', 'A Hora da Estrela - Edição Especial - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1049', 'Ensaio sobre a Cegueira - Edição Especial - José Saramago');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1050', 'Harry Potter e a Pedra Filosofal - Ilustrado - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1051', '1984 - Volume Único - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1052', 'O Velho e o Mar (Inglês) - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1053', 'Harry Potter e a Pedra Filosofal (Capa Dura) - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1054', 'It: A Coisa (Português) - Stephen King');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1055', '1984 - Volume Único - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1056', 'O Senhor dos Anéis - Edição Especial - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1057', 'A Hora da Estrela - Nova Ortografia - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1058', 'A Hora da Estrela - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1059', 'O Senhor dos Anéis - Nova Ortografia - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1060', 'Cem Anos de Solidão - Nova Ortografia - Gabriel García Márquez');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1061', 'A Hora da Estrela (Inglês) - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1062', 'Dom Casmurro - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1063', 'Harry Potter e a Pedra Filosofal - Coleção Clássicos - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1064', 'Dom Casmurro (Português) - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1065', '1984 - Edição Comentada - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1066', 'It: A Coisa - Ilustrado - Stephen King');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1067', 'Dom Casmurro - Ilustrado - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1068', 'Assassinato no Expresso Oriente - Ilustrado - Agatha Christie');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1069', 'It: A Coisa - Ilustrado - Stephen King');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1070', 'O Velho e o Mar - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1071', '1984 (Inglês) - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1072', 'Dom Casmurro - Edição Especial - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1073', 'O Senhor dos Anéis - Coleção Clássicos - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1074', 'A Hora da Estrela - Edição Comentada - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1075', 'Ensaio sobre a Cegueira - Nova Ortografia - José Saramago');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1076', 'O Velho e o Mar (Português) - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1077', 'O Velho e o Mar - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1078', 'A Hora da Estrela - Coleção Clássicos - Clarice Lispector');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1079', 'Dom Casmurro - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1080', 'Ensaio sobre a Cegueira - Coleção Clássicos - José Saramago');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1081', 'Harry Potter e a Pedra Filosofal - Edição Comentada - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1082', '1984 - Volume Único - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1083', 'Dom Casmurro - Edição Comentada - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1084', 'Dom Casmurro - Edição Comentada - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1085', 'Assassinato no Expresso Oriente (Inglês) - Agatha Christie');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1086', 'Dom Casmurro - Coleção Clássicos - Machado de Assis');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1087', 'Assassinato no Expresso Oriente (Português) - Agatha Christie');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1088', 'Ensaio sobre a Cegueira - Nova Ortografia - José Saramago');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1089', 'O Senhor dos Anéis - Ilustrado - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1090', '1984 - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1091', 'Cem Anos de Solidão (Inglês) - Gabriel García Márquez');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1092', 'O Senhor dos Anéis (Inglês) - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1093', '1984 - Edição Comentada - George Orwell');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1094', 'Harry Potter e a Pedra Filosofal - Coleção Clássicos - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1095', 'Harry Potter e a Pedra Filosofal - Edição Comentada - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1096', 'Harry Potter e a Pedra Filosofal - J.K. Rowling');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1097', 'O Senhor dos Anéis - Volume Único - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1098', 'O Senhor dos Anéis - J.R.R. Tolkien');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1099', 'O Velho e o Mar - Edição Comentada - Ernest Hemingway');
|
||||
INSERT INTO produtos (codigo, descricao) VALUES ('LIV1100', 'Dom Casmurro - Coleção Clássicos - Machado de Assis');
|
||||
171
source/main.py
Normal file
171
source/main.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import asyncio
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
from langchain_mcp_adapters.tools import load_mcp_tools
|
||||
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langchain_core.runnables import Runnable
|
||||
from langchain_core.messages import HumanMessage, AIMessage
|
||||
|
||||
import phoenix as px
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
|
||||
# 1. Inicia o Phoenix (ele abre o servidor OTLP na porta 6006)
|
||||
px.launch_app()
|
||||
|
||||
# 2. Configura o OpenTelemetry
|
||||
resource = Resource(attributes={"service.name": "ollama_oraclegenai_trace"})
|
||||
provider = TracerProvider(resource=resource)
|
||||
trace.set_tracer_provider(provider)
|
||||
|
||||
# 3. Configura o exportador para mandar spans para o Phoenix
|
||||
otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:6006/v1/traces")
|
||||
span_processor = BatchSpanProcessor(otlp_exporter)
|
||||
provider.add_span_processor(span_processor)
|
||||
|
||||
# 4. Cria o tracer
|
||||
tracer = trace.get_tracer(__name__)
|
||||
|
||||
class MemoryState:
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
|
||||
# Define the language model
|
||||
llm = ChatOCIGenAI(
|
||||
model_id="cohere.command-r-08-2024",
|
||||
service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com",
|
||||
compartment_id="ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
auth_profile="DEFAULT",
|
||||
model_kwargs={"temperature": 0.1, "top_p": 0.75, "max_tokens": 2000}
|
||||
)
|
||||
|
||||
# Prompt
|
||||
prompt = ChatPromptTemplate.from_messages([
|
||||
("system", """Você é um agente responsável por resolver inconsistências em notas fiscais de devolução de clientes.
|
||||
Seu objetivo é encontrar a nota fiscal de **saída original da empresa**,
|
||||
com base nas informações da **nota de devolução do cliente**.
|
||||
|
||||
### Importante:
|
||||
1. Use o servidor `InvoiceItemResolver` para todas as consultas.
|
||||
2. Primeiro, utilize a ferramenta de **busca vetorial ou fuzzy** para encontrar o **EAN mais provável**,
|
||||
a partir da descrição fornecida pelo cliente. O atributo codigo vindo do resultado da lista de busca vetorial
|
||||
pode ser entendida como EAN.
|
||||
- Ferramentas: `buscar_produto_vetorizado` ou `resolve_ean`
|
||||
- Retorne o EAN mais provável com sua descrição e grau de similaridade.
|
||||
Use resolve_ean para obter o EAN mais provável. Se retornar um dicionário com erro, interrompa a operação.
|
||||
3. Só após encontrar um EAN válido, use a ferramenta `buscar_notas_por_criterios` para procurar a nota fiscal de saída
|
||||
original.
|
||||
- Use o EAN junto com cliente, preço e local (estado) para fazer a busca.
|
||||
|
||||
### Exemplo de entrada:
|
||||
```json
|
||||
{{
|
||||
"customer": "Cliente 43",
|
||||
"description": "Harry Poter",
|
||||
"price": 139.55,
|
||||
"location": "RJ"
|
||||
}}
|
||||
Se encontrar uma nota fiscal de saída correspondente, retorne:
|
||||
• número da nota,
|
||||
• cliente,
|
||||
• estado,
|
||||
• EAN,
|
||||
• descrição do produto,
|
||||
• preço unitário.
|
||||
|
||||
Se não encontrar nenhuma correspondência, responda exatamente:
|
||||
“EAN não encontrado com os critérios fornecidos.”
|
||||
"""),
|
||||
("placeholder", "{messages}")
|
||||
])
|
||||
|
||||
# Local MCP Server Parameters
|
||||
server_params = StdioServerParameters(
|
||||
command="python",
|
||||
args=["server_nf_items.py"],
|
||||
)
|
||||
|
||||
# Run the client with the MCP server
|
||||
async def main():
|
||||
async with stdio_client(server_params) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
|
||||
tools = await load_mcp_tools(session)
|
||||
if not tools:
|
||||
print("❌ No MCP tools were loaded. Please check if the server is running.")
|
||||
return
|
||||
|
||||
print("🛠️ Loaded tools:", [t.name for t in tools])
|
||||
|
||||
# Creating the LangGraph agent with in-memory state
|
||||
memory_state = MemoryState()
|
||||
|
||||
agent_executor = create_react_agent(
|
||||
model=llm,
|
||||
tools=tools,
|
||||
prompt=prompt,
|
||||
)
|
||||
|
||||
print("🤖 READY")
|
||||
while True:
|
||||
query = input("You: ")
|
||||
if query.lower() in ["quit", "exit"]:
|
||||
break
|
||||
if not query.strip():
|
||||
continue
|
||||
|
||||
memory_state.messages.append(HumanMessage(content=query))
|
||||
try:
|
||||
result = await agent_executor.ainvoke({"messages": memory_state.messages})
|
||||
new_messages = result.get("messages", [])
|
||||
|
||||
# Exibe a ferramenta sendo chamada (pode ser mais específico dependendo da lógica)
|
||||
for tool in tools:
|
||||
print(f"🛠️ Executing tool: {tool.name}")
|
||||
|
||||
# Store new messages
|
||||
memory_state.messages.extend(new_messages)
|
||||
|
||||
print("Assist:", new_messages[-1].content)
|
||||
|
||||
# Quando você chama o prompt.format_messages()
|
||||
formatted_messages = prompt.format_messages()
|
||||
|
||||
# Convertendo cada mensagem em string
|
||||
formatted_messages_str = "\n".join([str(msg) for msg in formatted_messages])
|
||||
with tracer.start_as_current_span("Server NF Items") as span:
|
||||
# Anexa o prompt e resposta como atributos no trace
|
||||
span.set_attribute("llm.prompt", formatted_messages_str)
|
||||
span.set_attribute("llm.response", new_messages[-1].content)
|
||||
span.set_attribute("llm.model", "ocigenai")
|
||||
|
||||
# Ferramentas usadas (se possível)
|
||||
executed_tools = []
|
||||
if "intermediate_steps" in result:
|
||||
for step in result["intermediate_steps"]:
|
||||
tool_call = step.get("tool_input") or step.get("action")
|
||||
if tool_call:
|
||||
tool_name = tool_call.get("tool") or step.get("tool")
|
||||
if tool_name:
|
||||
executed_tools.append(tool_name)
|
||||
|
||||
if not executed_tools:
|
||||
executed_tools = [t.name for t in tools] # fallback
|
||||
|
||||
span.set_attribute("llm.executed_tools", ", ".join(executed_tools))
|
||||
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
|
||||
# Run the agent with asyncio
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
6003
source/notas_fiscais_mock.sql
Normal file
6003
source/notas_fiscais_mock.sql
Normal file
File diff suppressed because it is too large
Load Diff
47
source/process_vector_products.py
Normal file
47
source/process_vector_products.py
Normal file
@@ -0,0 +1,47 @@
|
||||
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"
|
||||
DB_ALIAS = "oradb23ai_high"
|
||||
USERNAME = "USER"
|
||||
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)
|
||||
|
||||
cursor = connection.cursor()
|
||||
|
||||
# === CONSULTA A TABELA DE PRODUTOS ===
|
||||
cursor.execute("SELECT id, codigo, descricao FROM produtos")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
ids = []
|
||||
descricoes = []
|
||||
|
||||
for row in rows:
|
||||
ids.append({"id": row[0], "codigo": row[1], "descricao": row[2]})
|
||||
descricoes.append(row[2]) # Usado no embedding
|
||||
|
||||
# === GERAÇÃO DE EMBEDDINGS COM SENTENCE TRANSFORMERS ===
|
||||
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)
|
||||
|
||||
# === SALVANDO O ÍNDICE E O MAPA DE PRODUTOS ===
|
||||
faiss.write_index(index, "faiss_index.bin")
|
||||
|
||||
with open("produto_id_map.pkl", "wb") as f:
|
||||
pickle.dump(ids, f)
|
||||
|
||||
print("✅ Vetores gerados e salvos com sucesso.")
|
||||
83
source/product_search.py
Normal file
83
source/product_search.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# product_search.py
|
||||
|
||||
import faiss
|
||||
import pickle
|
||||
import difflib
|
||||
import numpy as np
|
||||
from rapidfuzz import fuzz
|
||||
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"
|
||||
):
|
||||
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)
|
||||
self.top_k = top_k
|
||||
self.distancia_minima = distancia_minima
|
||||
self.embedding = OCIGenAIEmbeddings(
|
||||
model_id=model_id,
|
||||
service_endpoint=service_endpoint,
|
||||
compartment_id=compartment_id,
|
||||
auth_profile=auth_profile
|
||||
)
|
||||
|
||||
def _corrigir_input(self, input_usuario):
|
||||
descricoes = [p["descricao"] for p in self.id_map]
|
||||
sugestoes = difflib.get_close_matches(input_usuario, descricoes, n=1, cutoff=0.6)
|
||||
return sugestoes[0] if sugestoes else input_usuario
|
||||
|
||||
def buscar_produtos_similares(self, descricao_input):
|
||||
descricao_input = descricao_input.strip()
|
||||
descricao_corrigida = self._corrigir_input(descricao_input)
|
||||
|
||||
resultados = {
|
||||
"consulta_original": descricao_input,
|
||||
"consulta_utilizada": descricao_corrigida,
|
||||
"semanticos": [],
|
||||
"fallback_fuzzy": []
|
||||
}
|
||||
|
||||
consulta_emb = self.embedding.embed_query(descricao_corrigida)
|
||||
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]):
|
||||
if dist < self.distancia_minima:
|
||||
match = self.id_map[i]
|
||||
similaridade = 1 / (1 + dist)
|
||||
resultados["semanticos"].append({
|
||||
"id": match["id"],
|
||||
"codigo": match["codigo"],
|
||||
"descricao": match["descricao"],
|
||||
"similaridade": round(similaridade * 100, 2),
|
||||
"distancia": round(dist, 4)
|
||||
})
|
||||
|
||||
if not resultados["semanticos"]:
|
||||
melhores_fuzz = []
|
||||
for produto in self.id_map:
|
||||
score = fuzz.token_sort_ratio(descricao_corrigida, produto["descricao"])
|
||||
melhores_fuzz.append((produto, score))
|
||||
melhores_fuzz.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for produto, score in melhores_fuzz[:self.top_k]:
|
||||
resultados["fallback_fuzzy"].append({
|
||||
"id": produto["id"],
|
||||
"codigo": produto["codigo"],
|
||||
"descricao": produto["descricao"],
|
||||
"score_fuzzy": round(score, 2)
|
||||
})
|
||||
|
||||
return resultados
|
||||
BIN
source/produto_id_map.pkl
Normal file
BIN
source/produto_id_map.pkl
Normal file
Binary file not shown.
42
source/script.sql
Normal file
42
source/script.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
CREATE TABLE produtos (
|
||||
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
codigo VARCHAR2(50),
|
||||
descricao VARCHAR2(4000)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_texto_descricao ON produtos(descricao)
|
||||
INDEXTYPE IS CTXSYS.CONTEXT;
|
||||
|
||||
-- Tabela principal: NOTA_FISCAL
|
||||
CREATE TABLE NOTA_FISCAL (
|
||||
NUMERO_NF VARCHAR2(20) PRIMARY KEY,
|
||||
CODIGO_CLIENTE VARCHAR2(20) NOT NULL,
|
||||
NOME_CLIENTE VARCHAR2(100),
|
||||
VALOR_TOTAL NUMBER(15, 2),
|
||||
DATA_SAIDA DATE,
|
||||
CIDADE VARCHAR2(100),
|
||||
ESTADO VARCHAR2(2) -- Ex: SP, RJ, MG
|
||||
);
|
||||
|
||||
-- Tabela de itens: ITEM_NOTA_FISCAL
|
||||
CREATE TABLE ITEM_NOTA_FISCAL (
|
||||
NUMERO_NF VARCHAR2(20) NOT NULL,
|
||||
NUMERO_ITEM NUMBER(5) NOT NULL,
|
||||
CODIGO_EAN VARCHAR2(20),
|
||||
DESCRICAO_PRODUTO VARCHAR2(200),
|
||||
VALOR_UNITARIO NUMBER(12, 4),
|
||||
QUANTIDADE NUMBER(10, 2),
|
||||
VALOR_TOTAL NUMBER(15, 2),
|
||||
VALOR_IMPOSTOS NUMBER(15, 2),
|
||||
|
||||
-- Chave primária composta
|
||||
CONSTRAINT PK_ITEM_NOTA PRIMARY KEY (NUMERO_NF, NUMERO_ITEM),
|
||||
|
||||
-- Chave estrangeira para NOTA_FISCAL
|
||||
CONSTRAINT FK_ITEM_NOTA_FISCAL FOREIGN KEY (NUMERO_NF)
|
||||
REFERENCES NOTA_FISCAL (NUMERO_NF)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Índice para acelerar busca por código de produto
|
||||
CREATE INDEX IDX_ITEM_EAN ON ITEM_NOTA_FISCAL (CODIGO_EAN);
|
||||
136
source/server_nf_items.py
Normal file
136
source/server_nf_items.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import oracledb
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from product_search import BuscaProdutoSimilar
|
||||
|
||||
buscador = BuscaProdutoSimilar()
|
||||
|
||||
mcp = FastMCP("InvoiceItemResolver")
|
||||
# Configurações Oracle Wallet
|
||||
WALLET_PATH = "/WALLET_PATH/Wallet_oradb23ai" # Altere conforme seu ambiente
|
||||
DB_ALIAS = "oradb23ai_high" # Alias definido no tnsnames.ora
|
||||
USERNAME = "USER" # Usuário do banco
|
||||
PASSWORD = "Password" # Senha do usuário
|
||||
os.environ["TNS_ADMIN"] = WALLET_PATH
|
||||
|
||||
|
||||
def executar_busca(query: str, params: dict = {}):
|
||||
try:
|
||||
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.execute(query, params)
|
||||
results = cursor.fetchall()
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return results
|
||||
except Exception as e:
|
||||
print(f"[ERRO] Consulta falhou: {e}")
|
||||
return []
|
||||
|
||||
def executar_busca_ean(termos_busca):
|
||||
results = []
|
||||
|
||||
try:
|
||||
connection = oracledb.connect(user=USERNAME, password=PASSWORD, dsn=DB_ALIAS, config_dir=WALLET_PATH, wallet_location=WALLET_PATH, wallet_password=PASSWORD)
|
||||
cursor = connection.cursor()
|
||||
|
||||
query = """
|
||||
SELECT * FROM TABLE(fn_busca_avancada(:1))
|
||||
ORDER BY similaridade DESC \
|
||||
"""
|
||||
cursor.execute(query, [termos_busca])
|
||||
|
||||
for row in cursor:
|
||||
results.append({
|
||||
"codigo": row[0],
|
||||
"descricao": row[1],
|
||||
"similaridade": row[2]
|
||||
})
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
except Exception as e:
|
||||
return {"erro": str(e)}, 500
|
||||
|
||||
return results
|
||||
# --------------------- FERRAMENTAS MCP ---------------------
|
||||
@mcp.tool()
|
||||
def buscar_produto_vetorizado(descricao: str) -> dict:
|
||||
"""Busca produto por descrição usando embeddings"""
|
||||
return buscador.buscar_produtos_similares(descricao)
|
||||
|
||||
@mcp.tool()
|
||||
def resolve_ean(description: str) -> dict:
|
||||
"""
|
||||
Resolve o código EAN do produto a partir da descrição
|
||||
"""
|
||||
|
||||
result = executar_busca_ean(description)
|
||||
|
||||
if isinstance(result, list) and result:
|
||||
return {
|
||||
"ean": result[0]["codigo"],
|
||||
"descricao": result[0]["descricao"],
|
||||
"similaridade": result[0]["similaridade"]
|
||||
}
|
||||
else:
|
||||
return {"erro": "EAN não encontrado com os critérios fornecidos."}
|
||||
|
||||
@mcp.tool()
|
||||
def buscar_notas_por_criterios(cliente: str = None, estado: str = None, preco: float = None, ean: str = None, margem: float = 0.05) -> list:
|
||||
"""
|
||||
Busca notas fiscais de saída com base em cliente, estado, EAN e preço aproximado.
|
||||
Permite que um ou mais campos sejam omitidos.
|
||||
Enquanto não houver um EAN estabelecido, nao adianta usar este servico.
|
||||
"""
|
||||
print("buscar_notas_por_criterios")
|
||||
|
||||
query = """
|
||||
SELECT nf.numero_nf, nf.nome_cliente, nf.estado, nf.data_saida,
|
||||
inf.numero_item, inf.codigo_ean, inf.descricao_produto, inf.valor_unitario
|
||||
FROM nota_fiscal nf
|
||||
JOIN item_nota_fiscal inf ON nf.numero_nf = inf.numero_nf
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = {}
|
||||
|
||||
if cliente:
|
||||
query += " AND LOWER(nf.nome_cliente) LIKE LOWER(:cliente)"
|
||||
params["cliente"] = f"%{cliente}%"
|
||||
if estado:
|
||||
query += " AND LOWER(nf.estado) = LOWER(:estado)"
|
||||
params["estado"] = estado
|
||||
if ean:
|
||||
query += " AND inf.codigo_ean = :ean"
|
||||
params["ean"] = ean
|
||||
if preco is not None:
|
||||
query += " AND inf.valor_unitario BETWEEN :preco_min AND :preco_max"
|
||||
params["preco_min"] = preco * (1 - margem)
|
||||
params["preco_max"] = preco * (1 + margem)
|
||||
|
||||
# Executa a consulta com os parâmetros nomeados
|
||||
result = executar_busca(query, params)
|
||||
|
||||
return [
|
||||
dict(zip(
|
||||
["numero_nota", "nome_cliente", "estado", "data_saida", "numero_item", "codigo_ean", "descricao_produto", "valor_unitario"],
|
||||
row
|
||||
))
|
||||
for row in result
|
||||
]
|
||||
|
||||
|
||||
# --------------------- EXECUÇÃO MCP ---------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Inicia o servidor MCP
|
||||
mcp.run(transport="stdio")
|
||||
Reference in New Issue
Block a user