First commit

This commit is contained in:
2025-05-13 10:14:15 -03:00
commit 74b73abc15
24 changed files with 7489 additions and 0 deletions

BIN
source/faiss_index.bin Normal file

Binary file not shown.

View 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")

View 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}")

View 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;

View 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
View 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())

File diff suppressed because it is too large Load Diff

View 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
View 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

Binary file not shown.

42
source/script.sql Normal file
View 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
View 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")