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

12
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,12 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Zeppelin ignored files
/ZeppelinRemoteNotebooks/

9
.idea/agent-ai-mcp-server.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ScalaCodeStyleSettings>
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
</ScalaCodeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

6
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/agent-ai-mcp-server.iml" filepath="$PROJECT_DIR$/.idea/agent-ai-mcp-server.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

634
README.md Normal file
View File

@@ -0,0 +1,634 @@
# Construa um Agente de IA com Servidor MCP para Resolução de Notas Fiscais
## Introdução
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.
Este agente utiliza como base um modelo de linguagem da Oracle Cloud Generative AI, integrando-se com ferramentas declaradas dinamicamente e gerenciadas por um servidor MCP.
---
## Pré-requisitos
Antes de iniciar, certifique-se de ter os seguintes itens:
- Python 3.10 ou superior instalado
- Acesso a uma conta Oracle Cloud com o serviço OCI Generative AI habilitado
- Biblioteca [`langchain`](https://python.langchain.com) instalada e configurada
- Acesso ao modelo `cohere.command-r-08-2024` via OCI Generative AI
- Bibliotecas auxiliares instaladas:
- `langgraph`
- `langchain_mcp_adapters`
- `phoenix` (para observabilidade com OpenTelemetry)
- `opentelemetry-sdk`, `opentelemetry-exporter-otlp`
- Um servidor MCP funcional com as ferramentas:
- `resolve_ean`
- `buscar_produto_vetorizado`
- `buscar_notas_por_criterios`
- Arquivo `server_nf_items.py` configurado para ser executado como servidor MCP simulando um ERP
---
## Objetivos
Ao final deste tutorial, você será capaz de:
- Configurar um agente de IA com LangGraph e LangChain para trabalhar com prompts estruturados
- Integrar este agente a um servidor MCP via protocolo `stdio`
- Utilizar ferramentas remotas registradas no servidor para:
- Realizar buscas vetoriais a partir de descrições de produtos
- Identificar o código EAN mais provável de um item
- Buscar notas fiscais originais com base em critérios como cliente, estado e preço
- Monitorar a execução do agente em tempo real usando o **Phoenix** e **OpenTelemetry**
- Simular uma resolução real de problema com base em um JSON de entrada como:
```json
{
"customer": "Cliente 43",
"description": "Harry Poter",
"price": 139.55,
"location": "RJ"
}
### 🧩 Tarefa 1: Criar um Banco de Dados Oracle Autonomous Database 23ai (Always Free)
Nesta etapa, você aprenderá como provisionar um banco de dados Oracle Autonomous Database 23ai na modalidade Always Free. Essa versão oferece um ambiente totalmente gerenciado, ideal para desenvolvimento, testes e aprendizado, sem custos adicionais.
Antes de iniciar, certifique-se de:
- Possuir uma conta na Oracle Cloud Infrastructure (OCI). Se ainda não tiver, você pode se registrar gratuitamente em [oracle.com/cloud/free](https://www.oracle.com/cloud/free/).
- Ter acesso ao Oracle Cloud Console para gerenciar seus recursos na nuvem.
### Etapas para Criar o Banco de Dados
1. **Acesse o Oracle Cloud Console**:
- Navegue até [Oracle Cloud Console](https://cloud.oracle.com/) e faça login com suas credenciais.
2. **Inicie o Provisionamento do Autonomous Database**:
- No menu de navegação, selecione **"Oracle Database"** e, em seguida, **"Autonomous Database"**.
- Clique em **"Criar Instância do Autonomous Database"**.
3. **Configure os Detalhes da Instância**:
- **Nome do Banco de Dados**: Escolha um nome identificador para sua instância.
- **Tipo de Carga de Trabalho**: Selecione entre *Data Warehouse* ou *Transaction Processing*, conforme suas necessidades.
- **Compartimento**: Escolha o compartimento apropriado para organizar seus recursos.
4. **Selecione a Opção Always Free**:
- Certifique-se de marcar a opção **"Always Free"** para garantir que a instância seja provisionada na modalidade gratuita.
5. **Defina as Credenciais de Acesso**:
- Crie uma senha segura para o usuário ADMIN, que será utilizada para acessar o banco de dados.
6. **Finalize o Provisionamento**:
- Revise as configurações e clique em **"Criar Autonomous Database"**.
- Aguarde alguns minutos até que a instância seja provisionada e esteja disponível para uso.
### Tarefa 2: Executar o Script de Criação de Tabelas no Autonomous Database
Agora que o Oracle Autonomous Database 23ai foi provisionado com sucesso, o próximo passo é preparar o banco de dados para o nosso caso de uso. Vamos executar um script SQL (`script.sql`) que cria três tabelas essenciais para o cenário de reconciliação de notas fiscais com agentes de IA:
- `PRODUTOS`
- `NOTA_FISCAL`
- `ITEM_NOTA_FISCAL`
### Etapas para Executar o Script
1. **Acesse o Autonomous Database**:
- No [Oracle Cloud Console](https://cloud.oracle.com/), vá até **"Oracle Database" > "Autonomous Database"**.
- Clique sobre o nome da instância recém-criada.
2. **Abra a SQL Console**:
- No painel da instância, clique em **"Database Actions"**.
- Em seguida, clique em **"SQL"** para abrir o SQL Console no navegador.
3. **Copie e Cole o Script SQL**:
- Abra o arquivo `script.sql` localmente e copie todo o conteúdo.
- Cole no editor do SQL Console.
4. **Execute o Script**:
- Clique em **"Run"** ou pressione `Ctrl+Enter` para executar.
- Aguarde a confirmação de que os comandos foram executados com sucesso.
5. **Valide as Tabelas Criadas**:
- Você pode usar os seguintes comandos para verificar se as tabelas foram criadas:
```sql
SELECT table_name FROM user_tables;
```
### Tarefa 3: Inserir Dados de Exemplo nas Tabelas
Com as tabelas criadas no Autonomous Database, agora é hora de inserir dados fictícios que simularão um cenário real para a aplicação de agentes de IA. Utilizaremos dois scripts SQL:
- `insert_produtos_livros.sql` insere uma lista de livros como produtos, com seus respectivos EANs e descrições.
- `notas_fiscais_mock.sql` insere registros de notas fiscais de saída simuladas, associadas a clientes, produtos e preços.
Esses dados serão usados pelos agentes de IA para resolver inconsistências em notas de devolução.
### Etapas para Executar os Scripts
1. **Acesse o SQL Console**:
- No Oracle Cloud Console, vá até sua instância do Autonomous Database.
- Acesse **Database Actions > SQL**.
2. **Execute o Script de Produtos**:
- Abra o conteúdo do arquivo `insert_produtos_livros.sql` e cole no editor SQL.
- Clique em **"Run"** ou pressione `Ctrl+Enter`.
3. **Execute o Script de Notas Fiscais**:
- Agora abra o conteúdo do arquivo `notas_fiscais_mock.sql` e cole no editor.
- Execute da mesma forma.
4. **Validar os Dados Inseridos**:
- Você pode verificar os dados com comandos como:
```sql
SELECT * FROM PRODUTOS;
SELECT * FROM NOTA_FISCAL;
SELECT * FROM ITEM_NOTA_FISCAL;
```
## Tarefa 4: Criar e Compilar a Função de Busca Avançada no Banco de Dados
O próximo passo é criar uma função PL/SQL chamada `fn_busca_avancada`, que realiza buscas inteligentes por palavras-chave em descrições de produtos. Essa função será utilizada pelos agentes de IA como parte da ferramenta `resolve_ean`, permitindo encontrar o código EAN mais próximo com base na descrição fornecida por um cliente na nota de devolução.
### O Que a Função Faz?
A função `fn_busca_avancada` realiza:
1. **Tokenização** dos termos informados (ex: `"harry poter pedra"` vira `["harry", "poter", "pedra"]`).
2. **Busca direta** nas descrições (`LIKE '%termo%'`) → +3 pontos.
3. **Busca fonética** com `SOUNDEX` → +2 pontos.
4. **Busca por escrita similar** com `UTL_MATCH.EDIT_DISTANCE <= 2` → +1 ponto.
5. Soma a pontuação para cada produto e retorna aqueles com score > 0.
6. Retorna os produtos como objetos do tipo `produto_resultado`, contendo:
- `codigo` (EAN),
- `descricao` do produto,
- `similaridade` (pontuação da busca).
### Etapas de Execução
1. **Copie e cole o script completo no SQL Console do Autonomous Database.**
- Isso inclui:
- Criação da tabela `produtos` (se ainda não foi feita).
- Criação de índice de texto.
- Tipos `produto_resultado` e `produto_resultado_tab`.
- A função `fn_busca_avancada`.
- Testes opcionais.
2. **Execute o script completo.** O resultado deverá ser `Function created` e `Type created`.
3. **Teste a função com descrições simuladas:**
```sql
SELECT *
FROM TABLE(fn_busca_avancada('harry poter pedra'))
ORDER BY similaridade DESC;
```
## Tarefa 5: Vetorizar os Produtos para Busca Semântica com IA
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.
---
### 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. Consulta de Produtos no Banco
```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:
```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)
```
6. Salvando o Índice e o Mapeamento de Produtos
```python
faiss.write_index(index, "faiss_index.bin")
```
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)
```
### 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
Lembre-se de que é necessário o **Oracle Wallet** baixado e configurado.
Execute no terminal:
```python
python process_vector_products.py
```
Pronto! Os produtos da base de dados estão vetorizados.
### Por que Isso é Importante?
Busca vetorial é altamente eficaz para encontrar produtos mesmo quando a descrição é subjetiva, imprecisa ou está em linguagem natural.
## Entendendo o Código: Agente LLM com Servidor MCP
Este projeto é composto por **3 componentes principais**:
1. **Agente ReAct com LangGraph + LLM da OCI** (Arquivo **main.py**)
2. **Servidor MCP com Ferramentas para Resolução de Notas Fiscais** (Arquivo **server_nf_items.py**)
3. **Busca de Produtos Similares com OCI Generative AI e FAISS** (Arquivo **product_search.py**)
Abaixo detalhamos a funcionalidade de cada componente e destacamos os trechos mais importantes do código.
---
### 1. Agente ReAct com LangGraph + LLM da OCI
Este componente executa a aplicação principal, onde o usuário interage com o agente baseado em LLM (Large Language Model) da Oracle Cloud. Ele se comunica com o servidor MCP por meio de um protocolo stdio.
### Principais funcionalidades:
* **Configuração de Telemetria com Phoenix e OpenTelemetry**
```python
px.launch_app()
...
trace.set_tracer_provider(provider)
```
* **Criação do modelo LLM usando `ChatOCIGenAI`**:
```python
llm = ChatOCIGenAI(
model_id="cohere.command-r-08-2024",
...
)
```
* **Definição do prompt orientado à tarefa de reconciliação de notas fiscais**:
```python
prompt = ChatPromptTemplate.from_messages([
("system", """Você é um agente responsável por resolver inconsistências em notas fiscais..."""),
("placeholder", "{messages}")
])
```
* **Execução do servidor MCP local via stdio**
```python
server_params = StdioServerParameters(
command="python",
args=["server_nf_items.py"],
)
```
* **Loop principal de interação com o usuário:**
```python
while True:
query = input("You: ")
...
result = await agent_executor.ainvoke({"messages": memory_state.messages})
```
* **Integração com ferramentas expostas pelo servidor MCP:**
```python
agent_executor = create_react_agent(
model=llm,
tools=tools,
prompt=prompt,
)
```
### Prompt
O prompt é fundamental para estabelecer o processo e as regras de funcionamento para o Agente de IA.
![img_3.png](images/img_3.png)
---
### 2. Servidor MCP com Ferramentas de Resolução
Este servidor responde às chamadas do agente, fornecendo ferramentas que acessam um banco de dados Oracle com informações de produtos e notas fiscais.
### Principais funcionalidades:
* **Inicialização do servidor MCP com o nome `InvoiceItemResolver`**:
```python
mcp = FastMCP("InvoiceItemResolver")
```
* **Conexão com o banco Oracle via Oracle Wallet:**
```python
connection = oracledb.connect(
user=USERNAME,
password=PASSWORD,
dsn=DB_ALIAS,
wallet_location=WALLET_PATH,
...
)
```
* **Implementação das ferramentas MCP**:
#### `buscar_produto_vetorizado`
Busca produtos similares com embeddings:
```python
@mcp.tool()
def buscar_produto_vetorizado(descricao: str) -> dict:
return buscador.buscar_produtos_similares(descricao)
```
#### `resolve_ean`
Resolve um EAN com base em similaridade da descrição:
```python
@mcp.tool()
def resolve_ean(description: str) -> dict:
result = executar_busca_ean(description)
...
return {"ean": result[0]["codigo"], ...}
```
#### `buscar_notas_por_criterios`
Busca notas fiscais de saída com base em múltiplos filtros:
```python
@mcp.tool()
def buscar_notas_por_criterios(cliente: str = None, estado: str = None, preco: float = None, ean: str = None, ...):
query = """
SELECT nf.numero_nf, ...
FROM nota_fiscal nf
JOIN item_nota_fiscal inf ON nf.numero_nf = inf.numero_nf
WHERE 1=1
...
"""
```
* **Execução do servidor em modo `stdio`:**
```python
if __name__ == "__main__":
mcp.run(transport="stdio")
```
### 3. Busca de Produtos Similares com OCI Generative AI e FAISS
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**
- Comparações fuzzy com **RapidFuzz** como fallback
---
## Tarefa 5: Configurando o Modelo e Embeddings no Agente MCP
Vamos configurar o modelo de linguagem e os embeddings usados pelo agente conversacional com base no protocolo MCP, utilizando os serviços da Oracle Cloud Infrastructure (OCI) Generative AI.
---
### 1. Configurando o Modelo de Linguagem (LLM)
O modelo de linguagem é responsável por interpretar mensagens, gerar respostas e atuar como cérebro principal do agente.
### Configure no arquivo main.py
```python
from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI
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..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
auth_profile="DEFAULT",
model_kwargs={"temperature": 0.1, "top_p": 0.75, "max_tokens": 2000}
)
```
### Parâmetros
| Parâmetro | Descrição |
|------------------|-----------|
| `model_id` | ID do modelo Generative AI, ex: `cohere.command-r-08-2024` |
| `service_endpoint` | Endpoint regional do serviço Generative AI |
| `compartment_id` | OCID do compartimento OCI |
| `auth_profile` | Nome do perfil configurado no arquivo `~/.oci/config` |
| `model_kwargs` | Temperatura, top-p e tamanho da resposta |
### Como Listar os Modelos Disponíveis
### Usando o CLI
```bash
oci generative-ai model list --compartment-id <seu_compartment_id>
```
### Usando o Python SDK
```python
from oci.generative_ai import GenerativeAiClient
from oci.config import from_file
config = from_file(profile_name="DEFAULT")
client = GenerativeAiClient(config)
models = client.list_models(compartment_id=config["compartment_id"])
for model in models.data:
print(model.display_name, model.model_id)
```
---
### 2. Configurando Embeddings para Busca Semântica
A busca por produtos similares ou informações contextuais depende de embeddings vetoriais.
### Exemplo de uso no agente
```python
@mcp.tool()
def buscar_produto_vetorizado(descricao: str) -> dict:
return buscador.buscar_produtos_similares(descricao)
```
Altere os parametros (Arquivo **product_search.py**) conforme a orientação abaixo:
```python
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..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
auth_profile="DEFAULT"
```
### Parâmetros Explicados
| Parâmetro | Descrição |
| ------------------ | ------------------------------------------------------------------------- |
| `faiss_index_path` | Caminho do arquivo `.bin` com o índice vetorial FAISS. |
| `id_map_path` | Arquivo `.pkl` com lista de produtos e descrições. |
| `top_k` | Número de sugestões retornadas. |
| `distancia_minima` | Distância máxima para considerar resultado relevante. |
| `model_id` | ID do modelo de embedding na OCI (ex: `cohere.embed-english-light-v3.0`). |
| `service_endpoint` | Endpoint regional da OCI Generative AI. |
| `compartment_id` | OCID do compartimento. |
| `auth_profile` | Nome do perfil no arquivo `~/.oci/config`. |
### 3. Configurando o Servidor MCP
Assim como feito anteriormente na execução do código **process_vector_products.py**, será necessária a configuração do **Oracle Wallet** para o banco de dados **23ai**.
Modifique os parâmetros conforme suas configurações:
```python
import os
# Configurações Oracle Wallet
WALLET_PATH = "/caminho/para/Wallet"
DB_ALIAS = "oradb23ai_high"
USERNAME = "admin"
PASSWORD = "..."
# Define a variável de ambiente necessária para o cliente Oracle
os.environ["TNS_ADMIN"] = WALLET_PATH
```
---
Com isso, o modelo LLM e os embeddings estarão prontos para serem usados pelo agente MCP com LangGraph e LangChain.
## Tarefa 6: Testar a busca pela descrição de Produto e Nota Fiscal
Executar o arquivo **main.py** conforme abaixo:
```python
python main.py
```
Ao aparecer o prompt **You:**, digite:
{ "customer": "Cliente 43", "description": "Harry Poter", "price": 139.55, "location": "RJ"}
![img.png](images/img.png)
Perceba que foram executados os serviços:
buscar_produto_vetorizado
resolve_ean
buscar_notas_por_criterios
Agora digite:
{ "customer": "Cliente 43", "description": "Harry Poter", "price": 139.54}
Verá que não houve registro de Nota Fiscal encontrado. Isto ocorre porque a localização é fundamental para encontrar uma NF.
![img_1.png](images/img_1.png)
Digite:
{ "customer": "Cliente 43", "description": "Harry Poter", "location": "RJ"}
Desta vez, inserimos a localização, porém omitimos o preço unitário:
![img_2.png](images/img_2.png)
E mesmo assim foi encontrada a NF. Isto porque o preço não é fundamental porém ajuda a fechar mais o cerco para se ter mais assertividiade.
## Conclusão
Com esses dois componentes integrados, o sistema permite que um agente baseado em LLM da Oracle:
* Utilize ferramentas hospedadas remotamente via MCP
* Faça buscas inteligentes por produtos e EANs
* Localize notas fiscais de saída correspondentes
* Registre tudo em observabilidade via Phoenix + OpenTelemetry
Este design modular permite reusabilidade e fácil evolução do sistema para outros domínios além de notas fiscais.
## Referências
- [Introdução ao Oracle Autonomous Database](https://www.oracle.com/autonomous-database/get-started/)
- [Documentação do Oracle Database 23ai](https://docs.oracle.com/en/database/oracle/oracle-database/23/)
- [Blog da Oracle sobre o Autonomous Database 23ai Always Free](https://blogs.oracle.com/datawarehousing/post/23ai-autonomous-database-free)
- [Develop a Simple AI Agent Tool using Oracle Cloud Infrastructure Generative AI and REST APIs](https://docs.oracle.com/en/learn/oci-agent-ai/)
## Acknowledgments
- **Author** - Cristiano Hoshikawa (Oracle LAD A-Team Solution Engineer)

BIN
images/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
images/img_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
images/img_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
images/img_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

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