mirror of
https://github.com/hoshikawa2/agent-ai-mcp-server.git
synced 2026-03-03 16:19:35 +00:00
First commit
This commit is contained in:
12
.idea/.gitignore
generated
vendored
Normal file
12
.idea/.gitignore
generated
vendored
Normal 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
9
.idea/agent-ai-mcp-server.iml
generated
Normal 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
7
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
6
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
634
README.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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"}
|
||||||
|
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
Digite:
|
||||||
|
|
||||||
|
{ "customer": "Cliente 43", "description": "Harry Poter", "location": "RJ"}
|
||||||
|
|
||||||
|
Desta vez, inserimos a localização, porém omitimos o preço unitário:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
BIN
images/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
BIN
images/img_1.png
Normal file
BIN
images/img_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
images/img_2.png
Normal file
BIN
images/img_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
images/img_3.png
Normal file
BIN
images/img_3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
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