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