Adds docs
This commit is contained in:
294
docs/api.md
Normal file
294
docs/api.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# Documentação da API
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
http://<host>:8000
|
||||
```
|
||||
|
||||
## Autenticação
|
||||
|
||||
Todos os endpoints protegidos requerem o header:
|
||||
|
||||
```
|
||||
X-API-Key: <sua-chave-de-api>
|
||||
```
|
||||
|
||||
Respostas de erro de autenticação:
|
||||
|
||||
```json
|
||||
HTTP 403 Forbidden
|
||||
{ "detail": "Invalid API key" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Verifica se o serviço está ativo.
|
||||
|
||||
**Autenticação:** Não requerida
|
||||
|
||||
**Resposta:**
|
||||
```json
|
||||
HTTP 200 OK
|
||||
{ "status": "healthy" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /rules`
|
||||
|
||||
Lista os códigos de procedimento que possuem regras de autorização configuradas.
|
||||
|
||||
**Autenticação:** Não requerida
|
||||
|
||||
**Resposta:**
|
||||
```json
|
||||
HTTP 200 OK
|
||||
{
|
||||
"codes": [
|
||||
"40808130",
|
||||
"40808122",
|
||||
"31303293",
|
||||
"31005470",
|
||||
"41501012",
|
||||
"40901254",
|
||||
"40901262",
|
||||
"..."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /process`
|
||||
|
||||
Processa uma lista de guias de autorização médica. Para cada guia, extrai texto dos documentos anexados e avalia cada serviço solicitado usando o agente de IA.
|
||||
|
||||
**Autenticação:** Requerida (`X-API-Key`)
|
||||
|
||||
**Content-Type:** `application/json`
|
||||
|
||||
#### Corpo da Requisição
|
||||
|
||||
```json
|
||||
{
|
||||
"operadora": {
|
||||
"organizacaoId": "string",
|
||||
"processId": "string"
|
||||
},
|
||||
"guias": [ <lista de objetos guia> ]
|
||||
}
|
||||
```
|
||||
|
||||
#### Estrutura de uma Guia
|
||||
|
||||
```json
|
||||
{
|
||||
"atendimento": {
|
||||
"codigoGuia": "GUIA-2025-0001",
|
||||
"nomeAtendente": "Maria da Silva",
|
||||
"codigoContratante": "UNIMED-SP",
|
||||
"nomeContratante": "Unimed São Paulo",
|
||||
"codigoPlanoContratado": "PLANO-OURO",
|
||||
"planoRegulamentado": "true",
|
||||
"carteirinhaBeneficiario": "1234567890",
|
||||
"nomeBeneficiario": "João Pereira",
|
||||
"dataNascimentoBeneficiario": "1980-05-20",
|
||||
"idadeBeneficiario": 45,
|
||||
"generoBeneficiario": "M"
|
||||
},
|
||||
"guia": {
|
||||
"codigoGuiaLocal": "GUIA-2025-0001",
|
||||
"tipoGuia": "SP/SADT",
|
||||
"nomePrestadorSolicitante": "Dr. Fulano",
|
||||
"nomePrestadorExecutante": "Hospital X",
|
||||
"caraterAtendimento": "eletivo",
|
||||
"tipoAtendimento": "ambulatorial",
|
||||
"codigoCid": "S72.0",
|
||||
"indicacaoClinica": "Paciente com fratura de fêmur."
|
||||
},
|
||||
"servicos": [
|
||||
{
|
||||
"sequenciaServico": 1,
|
||||
"codigoServico": "40808130",
|
||||
"descricaoServico": "Densitometria óssea",
|
||||
"tipoServico": "EXAME",
|
||||
"quantidadeSolicitada": 1,
|
||||
"servicoCobertoPlano": "true",
|
||||
"dataSolicitacao": "2025-11-17T10:15:00"
|
||||
}
|
||||
],
|
||||
"historico": {
|
||||
"quantidadeHistoricoProcedimento365dias": 1,
|
||||
"quantidadeHistoricoProcedimentoTotal": 1
|
||||
},
|
||||
"anexos": [
|
||||
{
|
||||
"codigoGuia": "GUIA-2025-0001",
|
||||
"tipoAnexo": "LAUDO",
|
||||
"descricao": "Laudo do exame.",
|
||||
"nomeArquivo": "laudo.pdf",
|
||||
"idAnexo": "IMG-000001",
|
||||
"mimeType": "application/pdf",
|
||||
"URLAnexo": "s3://meu-bucket/documentos/laudo.pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Campos Importantes
|
||||
|
||||
| Campo | Tipo | Descrição |
|
||||
|--------------------------------|--------|------------------------------------------------------------|
|
||||
| `atendimento.nomeBeneficiario` | string | Nome do paciente (verificado nos documentos pelo agente) |
|
||||
| `servicos[].codigoServico` | string | Código TUSS do procedimento solicitado |
|
||||
| `anexos[].URLAnexo` | string | URI S3 do documento (`s3://bucket/chave`) |
|
||||
| `guia.indicacaoClinica` | string | Indicação clínica usada pelo agente na decisão |
|
||||
|
||||
#### Resposta de Sucesso
|
||||
|
||||
```json
|
||||
HTTP 200 OK
|
||||
{
|
||||
"status": "success",
|
||||
"operadora": {
|
||||
"organizacaoId": "asasasabb-1234-5678-aabb-123456abcdef",
|
||||
"processId": "ihjas1212rjaklpastash"
|
||||
},
|
||||
"guias": [
|
||||
{
|
||||
"atendimento": { ... },
|
||||
"guia": { ... },
|
||||
"servicos": [ ... ],
|
||||
"historico": { ... },
|
||||
"anexos": [
|
||||
{
|
||||
"nomeArquivo": "laudo.pdf",
|
||||
"URLAnexo": "s3://meu-bucket/documentos/laudo.pdf",
|
||||
"textoExtraido": "João Pereira\nDensitometria Óssea\nResultado: ...",
|
||||
"pageCount": 2,
|
||||
"tempoExtracaoSegundos": 3.14
|
||||
}
|
||||
],
|
||||
"avaliacaoAgente": [
|
||||
{
|
||||
"codigoServico": "40808130",
|
||||
"resultado": "Aprovado",
|
||||
"agentOutput": "{\"status\": \"Aprovado\", \"criterio\": \"Mulher acima de 45 anos\", \"documentos\": [{\"nome\": \"laudo.pdf\", \"pertence_ao_paciente\": true}]}",
|
||||
"input_tokens": 1523,
|
||||
"output_tokens": 87,
|
||||
"tempoAgentSegundos": 4.32
|
||||
}
|
||||
],
|
||||
"tempoProcessamento": {
|
||||
"extracaoSegundos": 3.14,
|
||||
"agentSegundos": 4.32,
|
||||
"totalSegundos": 7.86
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Campos da Avaliação (`avaliacaoAgente`)
|
||||
|
||||
| Campo | Tipo | Valores possíveis | Descrição |
|
||||
|------------------------|--------|------------------------------------------|---------------------------------------------------|
|
||||
| `codigoServico` | string | — | Código original conforme enviado na requisição |
|
||||
| `resultado` | string | `"Aprovado"`, `"Reprovado"`, `"SKIPPED"` | Decisão final do agente |
|
||||
| `agentOutput` | string | JSON string | Resposta completa do agente (status + critério + documentos) |
|
||||
| `input_tokens` | int | — | Total de tokens de entrada consumidos |
|
||||
| `output_tokens` | int | — | Total de tokens de saída gerados |
|
||||
| `tempoAgentSegundos` | float | — | Tempo de execução do agente em segundos |
|
||||
|
||||
#### Resultado `"SKIPPED"`
|
||||
|
||||
Retornado quando o código de procedimento não possui regras configuradas no `rules.yaml`:
|
||||
|
||||
```json
|
||||
{
|
||||
"codigoServico": "99999999",
|
||||
"resultado": "SKIPPED",
|
||||
"motivo": "Codigo '99999999' nao encontrado nas regras",
|
||||
"agentOutput": "",
|
||||
"tempoAgentSegundos": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### Erro em Guia Individual
|
||||
|
||||
Se uma guia falhar completamente, ela é substituída por um objeto de erro na lista (as demais guias continuam sendo processadas):
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "mensagem de erro",
|
||||
"guia": "GUIA-2025-0001"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemplo Completo
|
||||
|
||||
### Requisição
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/process \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: sua-chave-aqui" \
|
||||
-d '{
|
||||
"operadora": {
|
||||
"organizacaoId": "org-123",
|
||||
"processId": "proc-456"
|
||||
},
|
||||
"guias": [
|
||||
{
|
||||
"atendimento": {
|
||||
"nomeBeneficiario": "Ana Paula Costa",
|
||||
"idadeBeneficiario": 52,
|
||||
"generoBeneficiario": "F",
|
||||
"carteirinhaBeneficiario": "9876543210"
|
||||
},
|
||||
"guia": {
|
||||
"codigoGuiaLocal": "GUIA-2025-0042",
|
||||
"indicacaoClinica": "Rastreamento de osteoporose pós-menopausa.",
|
||||
"codigoCid": "M81.0"
|
||||
},
|
||||
"servicos": [
|
||||
{
|
||||
"codigoServico": "40808130",
|
||||
"descricaoServico": "Densitometria óssea",
|
||||
"quantidadeSolicitada": 1
|
||||
}
|
||||
],
|
||||
"historico": {},
|
||||
"anexos": [
|
||||
{
|
||||
"nomeArquivo": "pedido_medico.pdf",
|
||||
"URLAnexo": "s3://meu-bucket/guias/pedido_medico.pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
## Formato de Saída no S3
|
||||
|
||||
Após cada chamada ao `/process`, os resultados são salvos automaticamente:
|
||||
|
||||
```
|
||||
s3://<OUTPUT_BUCKET>/<API_VERSION>/<codigoGuiaLocal>_<YYYYMMDD_HHMMSS>.json
|
||||
```
|
||||
|
||||
Exemplo:
|
||||
```
|
||||
s3://upflux-doc-analyzer/v1/GUIA-2025-0042_20251117_143022.json
|
||||
```
|
||||
|
||||
O conteúdo é o mesmo objeto da guia processada (JSON completo, incluindo texto extraído e avaliações).
|
||||
170
docs/langfuse.md
Normal file
170
docs/langfuse.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Observabilidade com Langfuse
|
||||
|
||||
## O que é o Langfuse
|
||||
|
||||
O [Langfuse](https://langfuse.com) é uma plataforma de observabilidade para aplicações LLM. No doc-processor ele é usado para rastrear cada chamada ao agente de IA, permitindo:
|
||||
|
||||
- Visualizar o histórico completo de mensagens de cada execução (System, Human, AI, Tool calls).
|
||||
- Monitorar consumo de tokens (entrada e saída) por execução e por procedimento.
|
||||
- Medir latência das chamadas ao modelo.
|
||||
- Inspecionar as respostas brutas do agente para depuração.
|
||||
- Auditar decisões do sistema ao longo do tempo.
|
||||
|
||||
---
|
||||
|
||||
## Como está integrado no código
|
||||
|
||||
### Inicialização (`utils/langgraph_agent.py`)
|
||||
|
||||
O cliente Langfuse é criado **uma única vez** na importação do módulo, usando as credenciais do Secrets Manager:
|
||||
|
||||
```python
|
||||
from langfuse import Langfuse
|
||||
from langfuse.langchain import CallbackHandler
|
||||
|
||||
langfuse = Langfuse(
|
||||
secret_key=SECRETS["LANGFUSE-SECRET-KEY"],
|
||||
public_key=SECRETS["LANGFUSE-PUBLIC-KEY"],
|
||||
host=LANGFUSE_HOST, # padrão: https://cloud.langfuse.com
|
||||
)
|
||||
```
|
||||
|
||||
### Rastreamento de execuções (`run_agent`)
|
||||
|
||||
Para cada chamada ao agente, um `CallbackHandler` do Langfuse é instanciado e passado como callback ao LangGraph. Isso faz com que todas as mensagens trocadas durante a execução sejam enviadas automaticamente ao Langfuse:
|
||||
|
||||
```python
|
||||
langfuse_handler = CallbackHandler()
|
||||
config = {"callbacks": [langfuse_handler]}
|
||||
final_state = await agent.ainvoke(initial_state, config=config)
|
||||
```
|
||||
|
||||
O `CallbackHandler` captura automaticamente:
|
||||
- O conteúdo do System Prompt e da mensagem Human enviados ao modelo.
|
||||
- Cada resposta do LLM (incluindo tool calls intermediárias).
|
||||
- As respostas das ferramentas (`ToolMessage`).
|
||||
- Tokens usados em cada step.
|
||||
|
||||
### Flush assíncrono
|
||||
|
||||
Após cada execução do agente, o buffer do Langfuse é enviado ao servidor de forma assíncrona para garantir que nenhum trace seja perdido mesmo que o processo seja encerrado logo após:
|
||||
|
||||
```python
|
||||
await asyncio.to_thread(langfuse.flush)
|
||||
```
|
||||
|
||||
> **Por que `to_thread`?** O método `flush()` do Langfuse é bloqueante. Executá-lo em uma thread separada evita que ele bloqueie o event loop do FastAPI enquanto aguarda a resposta da rede.
|
||||
|
||||
---
|
||||
|
||||
## Contagem de Tokens
|
||||
|
||||
Além do rastreamento automático pelo Langfuse, o código também acumula os tokens manualmente a partir dos metadados das mensagens AI para retorná-los na resposta da API:
|
||||
|
||||
```python
|
||||
input_tokens = 0
|
||||
output_tokens = 0
|
||||
for msg in final_state["messages"]:
|
||||
usage = getattr(msg, "usage_metadata", None)
|
||||
if usage:
|
||||
input_tokens += usage.get("input_tokens", 0)
|
||||
output_tokens += usage.get("output_tokens", 0)
|
||||
```
|
||||
|
||||
Os tokens são somados de **todas** as mensagens AI geradas durante a execução, incluindo chamadas intermediárias quando o agente usa a ferramenta `check`. Esses valores são retornados em cada `avaliacaoAgente`:
|
||||
|
||||
```json
|
||||
{
|
||||
"codigoServico": "40808130",
|
||||
"resultado": "Aprovado",
|
||||
"input_tokens": 1523,
|
||||
"output_tokens": 87
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Criando uma Organização e um Projeto no Langfuse
|
||||
|
||||
### 1. Criar conta e organização
|
||||
|
||||
1. Acesse o ip do Ec2 via http na porta 3000, após liberar no security group para o ip desejado, e clique em **Sign Up**.
|
||||
2. Após o login, o Langfuse pede para criar ou entrar em uma **organização**. A organização agrupa todos os projetos e membros da equipe.
|
||||
- Clique em **Create new organization**, preencha o nome e confirme.
|
||||
|
||||
### 2. Criar um projeto
|
||||
|
||||
Dentro da organização:
|
||||
|
||||
1. Crie um novo projeto.
|
||||
2. Nos passos iniciais tem um botão para criar as Api keys, copie os valores de Secret e Public.
|
||||
|
||||
### 3. Apontar o sistema para o novo projeto
|
||||
|
||||
Atualize os segredos no AWS Secrets Manager (`doc-analyzer`) com os novos valores:
|
||||
|
||||
Basta ir no Secrets manager, escolher o segredo doc-analyzer, ir em retrieve secret value e edit. Atualize com os novos valores.
|
||||
|
||||
Após atualizar, talvez seja nescessário reiniciar o container/serviço para que ele carregue as novas credenciais:
|
||||
|
||||
```bash
|
||||
# ECS — forçar novo deploy
|
||||
aws ecs update-service \
|
||||
--cluster <nome-do-cluster> \
|
||||
--service <nome-do-servico> \
|
||||
--force-new-deployment
|
||||
```
|
||||
|
||||
Nas próximas execuções, os traces aparecerão no novo projeto dentro da nova organização.
|
||||
|
||||
---
|
||||
|
||||
## Configuração
|
||||
|
||||
### Variável de Ambiente
|
||||
|
||||
| Variável | Padrão | Descrição |
|
||||
|------------------|------------------------------|------------------------------------|
|
||||
| `LANGFUSE_HOST` | `http://13.59.214.47:3000/` | Endereço do servidor Langfuse. Pode ser uma instância self-hosted. |
|
||||
|
||||
### Segredos (AWS Secrets Manager — `doc-analyzer`)
|
||||
|
||||
| Chave | Descrição |
|
||||
|-----------------------|------------------------------------|
|
||||
| `LANGFUSE-SECRET-KEY` | Chave secreta do projeto Langfuse |
|
||||
| `LANGFUSE-PUBLIC-KEY` | Chave pública do projeto Langfuse |
|
||||
|
||||
As chaves são obtidas no painel do Langfuse em **Settings → API Keys**.
|
||||
|
||||
### Self-hosted
|
||||
|
||||
Para usar uma instância própria do Langfuse (ex: rodando internamente), basta configurar (variável de ambiente do ecs):
|
||||
|
||||
```bash
|
||||
LANGFUSE_HOST=http://13.59.214.47:3000/
|
||||
```
|
||||
|
||||
E atualizar as chaves correspondentes no Secrets Manager.
|
||||
|
||||
---
|
||||
|
||||
## O que visualizar no painel
|
||||
|
||||
Cada chamada ao `POST /process` gera uma ou mais **traces** no Langfuse (uma por serviço avaliado). Em cada trace é possível ver:
|
||||
|
||||
1. **Input**: JSON da guia + conteúdo OCR dos documentos enviados ao agente.
|
||||
2. **System Prompt**: Critérios de auto-aprovação e documentação mínima do procedimento avaliado.
|
||||
3. **Steps intermediários**: Se o agente chamou a ferramenta `check`, aparece como um step separado com o conteúdo retornado.
|
||||
4. **Output**: JSON final com `status`, `criterio` e lista de `documentos`.
|
||||
5. **Tokens e latência**: Custo estimado e tempo de resposta do modelo.
|
||||
|
||||
---
|
||||
|
||||
## Depuração
|
||||
|
||||
Se as traces não aparecerem no painel:
|
||||
|
||||
1. Verifique se `LANGFUSE-SECRET-KEY` e `LANGFUSE-PUBLIC-KEY` estão corretos no Secrets Manager.
|
||||
2. Confirme que `LANGFUSE_HOST` aponta para o servidor correto.
|
||||
3. Verifique os logs do container — erros de conexão com o Langfuse são silenciosos por padrão (o `flush` falha sem derrubar o serviço).
|
||||
4. Confirme que o container tem acesso de rede ao host do Langfuse.
|
||||
342
docs/modulos.md
Normal file
342
docs/modulos.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# Documentação dos Módulos
|
||||
|
||||
## 1. `app.py` — Aplicação Principal
|
||||
|
||||
Ponto de entrada da API REST. Define os endpoints FastAPI, autentica as requisições e orquestra o processamento das guias.
|
||||
|
||||
### Autenticação
|
||||
|
||||
Toda requisição ao endpoint `/process` deve incluir o header:
|
||||
|
||||
```
|
||||
X-API-Key: <chave configurada no Secrets Manager>
|
||||
```
|
||||
|
||||
Se a chave estiver ausente ou incorreta, a API retorna `HTTP 403 Forbidden`.
|
||||
|
||||
### Função `process_guia(guia: dict) → dict`
|
||||
|
||||
Processa uma única guia de autorização do início ao fim.
|
||||
|
||||
**Etapas:**
|
||||
|
||||
1. Itera sobre todos os anexos da guia (`guia.anexos`).
|
||||
2. Para cada anexo, extrai o texto via OCR (em paralelo, usando `asyncio.gather`).
|
||||
3. Concatena todos os textos extraídos em uma única string (`file_content`).
|
||||
4. Avalia cada serviço médico (`guia.servicos`) em paralelo, chamando o agente de IA.
|
||||
5. Adiciona os tempos de processamento ao objeto da guia.
|
||||
|
||||
**Campos adicionados à guia:**
|
||||
|
||||
| Campo | Tipo | Descrição |
|
||||
|------------------------------------------|---------|---------------------------------------------------|
|
||||
| `anexo.textoExtraido` | string | Texto extraído via OCR do arquivo |
|
||||
| `anexo.pageCount` | int | Número de páginas extraídas |
|
||||
| `anexo.tempoExtracaoSegundos` | float | Tempo de extração do anexo em segundos |
|
||||
| `anexo.error` | string | Mensagem de erro (apenas se falhou) |
|
||||
| `guia.avaliacaoAgente` | list | Lista de avaliações dos serviços |
|
||||
| `guia.tempoProcessamento.extracaoSegundos` | float | Tempo total de extração de todos os anexos |
|
||||
| `guia.tempoProcessamento.agentSegundos` | float | Tempo total do agente para todos os serviços |
|
||||
| `guia.tempoProcessamento.totalSegundos` | float | Tempo total de processamento da guia |
|
||||
|
||||
### Função `_extract_anexo(anexo_idx, anexo)`
|
||||
|
||||
Função interna de `process_guia`. Processa um único anexo:
|
||||
|
||||
- Lê o campo `urlAnexo` ou `URLAnexo` para obter a URI S3.
|
||||
- Se a URI for inválida ou não começar com `s3://`, define `textoExtraido = ""` e retorna `None`.
|
||||
- Chama `extract_text_from_s3_document` para baixar e executar OCR.
|
||||
- Em caso de erro, armazena a mensagem em `anexo.error` e retorna texto vazio.
|
||||
- Retorna a string formatada `"--- <nomeArquivo> ---\n<texto>"`.
|
||||
|
||||
### Modelo de Requisição `ProcessRequest`
|
||||
|
||||
```python
|
||||
class ProcessRequest(BaseModel):
|
||||
operadora: dict # Metadados da operadora (passado diretamente na resposta)
|
||||
guias: list[dict] # Lista de guias a serem avaliadas
|
||||
```
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Método | Rota | Autenticação | Descrição |
|
||||
|--------|------------|--------------|---------------------------------------------------|
|
||||
| POST | `/process` | X-API-Key | Processa todas as guias da requisição |
|
||||
| GET | `/health` | Não | Verifica se o serviço está ativo |
|
||||
| GET | `/rules` | Não | Lista os códigos de procedimento com regras ativas|
|
||||
|
||||
---
|
||||
|
||||
## 2. `services/document_extractor.py` — Extração de Documentos
|
||||
|
||||
Responsável por baixar arquivos do S3 e extrair texto via AWS Textract.
|
||||
|
||||
### Clientes AWS
|
||||
|
||||
Dois clientes boto3 são criados no início do módulo:
|
||||
|
||||
- `_s3_input`: cliente S3 autenticado com as credenciais do Secrets Manager (para acesso ao bucket de entrada de documentos).
|
||||
- `_textract`: cliente Textract usando as credenciais de ambiente padrão.
|
||||
|
||||
### Função `parse_s3_uri(s3_uri: str) → tuple[str, str]`
|
||||
|
||||
Converte uma URI S3 no formato `s3://bucket/chave` em uma tupla `(bucket, chave)`.
|
||||
|
||||
**Erros:**
|
||||
- Lança `ValueError` se o scheme não for `s3://`.
|
||||
- Lança `ValueError` se o bucket ou a chave estiverem ausentes.
|
||||
|
||||
**Exemplos:**
|
||||
```python
|
||||
parse_s3_uri("s3://meu-bucket/pasta/arquivo.pdf")
|
||||
# → ("meu-bucket", "pasta/arquivo.pdf")
|
||||
```
|
||||
|
||||
### Função `extract_text_from_s3_document(bucket: str, key: str) → tuple[str, int]`
|
||||
|
||||
Função assíncrona principal do módulo. Baixa o arquivo e extrai o texto conforme o tipo:
|
||||
|
||||
| Extensão | Comportamento |
|
||||
|-----------------------|-------------------------------------------------------------------|
|
||||
| `.png`, `.jpg`, `.jpeg` | Envia os bytes diretamente ao Textract. Retorna `(texto, 1)`. |
|
||||
| `.pdf` | Separa o PDF em páginas individuais. Cada página é enviada ao Textract separadamente (em paralelo). Retorna `(texto_completo, num_paginas)`. |
|
||||
| Outros | Retorna `("", 0)` sem processar. |
|
||||
|
||||
> **Nota:** Operações bloqueantes do boto3 são executadas em threads separadas via `asyncio.to_thread` para não bloquear o event loop.
|
||||
|
||||
### Funções Auxiliares
|
||||
|
||||
#### `_split_pdf_pages(pdf_bytes: bytes) → list[bytes]`
|
||||
|
||||
Usa PyPDF2 para ler o PDF e separar cada página em um arquivo PDF individual (em bytes). Necessário porque o Textract aceita no máximo uma página por chamada via bytes.
|
||||
|
||||
#### `_textract_detect_bytes(file_bytes: bytes) → str`
|
||||
|
||||
Chama `textract.detect_document_text` com os bytes do arquivo e retorna o texto extraído.
|
||||
|
||||
#### `_extract_text_from_textract_response(response: dict) → str`
|
||||
|
||||
Filtra os blocos da resposta do Textract, mantendo apenas blocos do tipo `LINE`, e os concatena em uma string com quebras de linha.
|
||||
|
||||
---
|
||||
|
||||
## 3. `services/authorization.py` — Avaliação de Serviços
|
||||
|
||||
Avalia se um serviço médico deve ser aprovado ou reprovado, consultando o agente de IA.
|
||||
|
||||
### Função `evaluate_servico(servico, guia, file_content) → dict`
|
||||
|
||||
**Parâmetros:**
|
||||
|
||||
| Parâmetro | Tipo | Descrição |
|
||||
|----------------|--------|--------------------------------------------------------|
|
||||
| `servico` | dict | Objeto do serviço médico com `codigoServico` e outros campos |
|
||||
| `guia` | dict | Guia completa (atendimento, guia, histórico) |
|
||||
| `file_content` | str | Texto OCR concatenado de todos os anexos |
|
||||
|
||||
**Fluxo:**
|
||||
|
||||
1. Extrai o `codigoServico` e mantém apenas os dígitos numéricos.
|
||||
2. Verifica se o código está presente no dicionário `RULES` (carregado do `rules.yaml`).
|
||||
- Se não estiver: retorna `resultado = "SKIPPED"` imediatamente.
|
||||
3. Monta um payload JSON com `atendimento`, `guia`, `servico` e `historico`.
|
||||
4. Chama `run_agent(query, code, file_content)` de forma assíncrona.
|
||||
5. Tenta parsear a resposta JSON do agente para extrair o campo `status`.
|
||||
6. Suporta respostas encapsuladas em blocos de código markdown (` ```json ... ``` `).
|
||||
|
||||
**Resposta:**
|
||||
|
||||
```json
|
||||
{
|
||||
"codigoServico": "40808130",
|
||||
"resultado": "Aprovado",
|
||||
"agentOutput": "{ \"status\": \"Aprovado\", ... }",
|
||||
"input_tokens": 1523,
|
||||
"output_tokens": 87,
|
||||
"tempoAgentSegundos": 4.32
|
||||
}
|
||||
```
|
||||
|
||||
**Resultado possível:**
|
||||
|
||||
| Valor | Condição |
|
||||
|-------------|-------------------------------------------------------|
|
||||
| `"Aprovado"` | Agente retornou `status: Aprovado` |
|
||||
| `"Reprovado"` | Agente retornou `status: Reprovado` ou erro no parse |
|
||||
| `"SKIPPED"` | Código de serviço não possui regras configuradas |
|
||||
|
||||
---
|
||||
|
||||
## 4. `services/result_store.py` — Persistência de Resultados
|
||||
|
||||
Salva os resultados do processamento no S3 de saída.
|
||||
|
||||
### Função `save_results(results: list[dict]) → None`
|
||||
|
||||
Persiste cada guia processada como um arquivo JSON separado no S3.
|
||||
|
||||
**Localização no S3:**
|
||||
|
||||
```
|
||||
s3://<OUTPUT_BUCKET>/<API_VERSION>/<codigoGuiaLocal>_<YYYYMMDD_HHMMSS>.json
|
||||
```
|
||||
|
||||
Exemplo:
|
||||
```
|
||||
s3://upflux-doc-analyzer/v1/GUIA-2025-0001_20251117_103000.json
|
||||
```
|
||||
|
||||
**Comportamento:**
|
||||
- Todas as guias são salvas em paralelo com `asyncio.gather`.
|
||||
- Erros são silenciados (`except Exception: pass`) para não impedir a resposta da API.
|
||||
- Usa o cliente S3 padrão de ambiente (sem credenciais explícitas).
|
||||
|
||||
---
|
||||
|
||||
## 5. `utils/langgraph_agent.py` — Agente de IA (LangGraph)
|
||||
|
||||
Núcleo de decisão do sistema. Define o fluxo do agente ReAct usando LangGraph e executa a avaliação via AWS Bedrock (Claude).
|
||||
|
||||
### Carregamento das Regras
|
||||
|
||||
Ao importar o módulo, o arquivo `rules.yaml` é lido e dois dicionários são populados:
|
||||
|
||||
- `RULES`: mapeamento de código de procedimento → critérios de auto-aprovação (texto livre).
|
||||
- `MIN_DOC`: mapeamento de código de procedimento → documentação mínima exigida (texto livre).
|
||||
|
||||
### `AgentState` (TypedDict)
|
||||
|
||||
Estado compartilhado entre os nós do grafo LangGraph:
|
||||
|
||||
```python
|
||||
class AgentState(TypedDict):
|
||||
messages: Annotated[list, add_messages]
|
||||
```
|
||||
|
||||
O campo `messages` acumula todas as mensagens trocadas (System, Human, AI, Tool) ao longo da execução.
|
||||
|
||||
### Função `create_agent(file_content: str) → CompiledGraph`
|
||||
|
||||
Cria e compila o grafo LangGraph para uma execução específica.
|
||||
|
||||
**Ferramenta disponível:**
|
||||
|
||||
- `check(expression: str) → str`: Retorna o conteúdo OCR dos documentos. O agente pode chamar essa ferramenta quando o JSON da guia não for suficiente para a decisão.
|
||||
|
||||
**Nós do grafo:**
|
||||
|
||||
| Nó | Função |
|
||||
|-----------|------------------------------------------------------------------|
|
||||
| `agent` | Chama o LLM (Bedrock Claude) com o estado atual das mensagens |
|
||||
| `tools` | Executa as ferramentas solicitadas pelo LLM |
|
||||
|
||||
**Fluxo condicional:**
|
||||
|
||||
```
|
||||
[entrada] → agent → (tem tool_calls?) → Sim → tools → agent → ...
|
||||
→ Não → END
|
||||
```
|
||||
|
||||
### Função `run_agent(query, code, file_content) → dict`
|
||||
|
||||
Executa o agente para um serviço específico.
|
||||
|
||||
**System Prompt:** Instruções em inglês que descrevem:
|
||||
- O papel do agente (aprovador/reprovador de procedimentos médicos).
|
||||
- Os critérios de auto-aprovação do código (extraídos de `RULES[code]`).
|
||||
- A documentação mínima exigida (extraída de `MIN_DOC[code]`).
|
||||
- A lógica de decisão (auto-aprovação tem prioridade sobre documentação mínima).
|
||||
- O formato obrigatório de resposta (JSON puro, sem markdown):
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "Aprovado" ou "Reprovado",
|
||||
"criterio": "descrição breve do critério atendido ou motivo da negação",
|
||||
"documentos": [
|
||||
{ "nome": "nome do documento", "pertence_ao_paciente": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Human Message:** JSON da guia + conteúdo OCR dos documentos dentro da tag `<documentos_anexados>`.
|
||||
|
||||
**Observabilidade:** Cada execução é rastreada no Langfuse via `CallbackHandler`.
|
||||
|
||||
**Retorno:**
|
||||
|
||||
```python
|
||||
{
|
||||
"response": "<JSON string da resposta do agente>",
|
||||
"input_tokens": 1523,
|
||||
"output_tokens": 87
|
||||
}
|
||||
```
|
||||
|
||||
Os tokens são somados de todas as mensagens AI geradas durante a execução (incluindo chamadas intermediárias de ferramentas).
|
||||
|
||||
---
|
||||
|
||||
## 6. `utils/config.py` — Configurações
|
||||
|
||||
Carrega variáveis de ambiente com valores padrão:
|
||||
|
||||
```python
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "us-east-2")
|
||||
BEDROCK_MODEL_ARN = os.environ["BEDROCK_MODEL_ARN"] # obrigatória
|
||||
OUTPUT_BUCKET = os.environ.get("OUTPUT_BUCKET", "upflux-doc-analyzer")
|
||||
API_VERSION = os.environ.get("API_VERSION", "v1")
|
||||
LANGFUSE_HOST = os.environ.get("LANGFUSE_HOST", "https://cloud.langfuse.com")
|
||||
```
|
||||
|
||||
`BEDROCK_MODEL_ARN` é obrigatória; se ausente, o serviço falha na inicialização com `KeyError`.
|
||||
|
||||
---
|
||||
|
||||
## 7. `utils/secrets_manager.py` — Gerenciamento de Segredos
|
||||
|
||||
Carrega os segredos do AWS Secrets Manager uma única vez na inicialização:
|
||||
|
||||
```python
|
||||
_client = boto3.client("secretsmanager", region_name="us-east-2")
|
||||
_response = _client.get_secret_value(SecretId="doc-analyzer")
|
||||
SECRETS: dict = json.loads(_response["SecretString"])
|
||||
```
|
||||
|
||||
O dicionário `SECRETS` é importado pelos módulos que precisam de credenciais (`app.py`, `document_extractor.py`, `langgraph_agent.py`).
|
||||
|
||||
**Chaves esperadas em `SECRETS`:**
|
||||
|
||||
| Chave | Usado em |
|
||||
|------------------------|-----------------------------------|
|
||||
| `API-KEY` | `app.py` (autenticação da API) |
|
||||
| `AWS_ACCESS_KEY` | `document_extractor.py` (S3) |
|
||||
| `AWS_SECRET_KEY` | `document_extractor.py` (S3) |
|
||||
| `LANGFUSE-SECRET-KEY` | `langgraph_agent.py` (Langfuse) |
|
||||
| `LANGFUSE-PUBLIC-KEY` | `langgraph_agent.py` (Langfuse) |
|
||||
|
||||
---
|
||||
|
||||
## 8. `utils/rules.yaml` — Regras de Autorização
|
||||
|
||||
Arquivo YAML com dois blocos principais:
|
||||
|
||||
### `rules` — Critérios de Auto-Aprovação
|
||||
|
||||
Cada entrada é um código de procedimento TUSS mapeado para um texto descrevendo as condições que, se atendidas, permitem aprovação imediata sem auditoria médica.
|
||||
|
||||
**Exemplos de códigos:**
|
||||
|
||||
| Código | Procedimento |
|
||||
|--------------|-------------------------------------------|
|
||||
| `40808130` | Densitometria óssea (osteoporose) |
|
||||
| `31303293` | DIU Hormonal (Mirena/Kyleena) |
|
||||
| `31005470` | Colecistectomia (cálculo biliar/colelitíase) |
|
||||
| `40901254` | Ultrassom obstétrico 1º trimestre |
|
||||
| `40901262` | Ultrassom obstétrico morfológico |
|
||||
| `41501012` | Cirurgia de catarata |
|
||||
| `20103190` | Reabilitação do assoalho pélvico |
|
||||
| `40304906` | Eco-doppler venoso |
|
||||
|
||||
### `min_doc` — Documentação Mínima
|
||||
|
||||
Para cada código, descreve quais documentos são necessários quando os critérios de auto-aprovação não são atendidos. O agente verifica a presença dessas informações nos anexos OCR.
|
||||
115
docs/visao-geral.md
Normal file
115
docs/visao-geral.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Doc-Processor — Visão Geral do Sistema
|
||||
|
||||
## O que é
|
||||
|
||||
O **Doc-Processor** é um serviço de análise automatizada de guias de autorização médica. Ele recebe solicitações de procedimentos médicos (guias), extrai o texto dos documentos anexados via OCR, e utiliza um agente de inteligência artificial (LLM) para decidir se cada serviço solicitado deve ser **Aprovado** ou **Reprovado**, com base em regras de negócio configuradas para cada código de procedimento.
|
||||
|
||||
O sistema foi desenvolvido para operar no contexto de operadoras de saúde (ex: Unimed), reduzindo o trabalho manual dos auditores médicos.
|
||||
|
||||
---
|
||||
|
||||
## Fluxo de Processamento
|
||||
|
||||
```
|
||||
POST /process
|
||||
│
|
||||
├─► Para cada guia (em paralelo):
|
||||
│ │
|
||||
│ ├─► Para cada anexo (em paralelo):
|
||||
│ │ ├─► Baixa o arquivo do S3
|
||||
│ │ ├─► OCR via AWS Textract
|
||||
│ │ └─► Armazena texto extraído na guia
|
||||
│ │
|
||||
│ └─► Para cada serviço (em paralelo):
|
||||
│ ├─► Verifica se o código existe nas regras
|
||||
│ ├─► Monta o payload JSON (atendimento + guia + serviço + histórico)
|
||||
│ ├─► Chama o agente LangGraph com o texto dos documentos
|
||||
│ └─► Retorna Aprovado / Reprovado / SKIPPED
|
||||
│
|
||||
├─► Salva todos os resultados no S3 (bucket de saída)
|
||||
└─► Retorna resposta HTTP com as avaliações
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisão do Agente
|
||||
|
||||
O agente segue a seguinte lógica de decisão para cada serviço:
|
||||
|
||||
1. **Critérios de auto-aprovação** (`rules.yaml › rules`): Se qualquer um desses critérios estiver presente na guia ou nos documentos, o procedimento é aprovado imediatamente.
|
||||
2. **Documentação mínima** (`rules.yaml › min_doc`): Se nenhum critério de auto-aprovação for atendido, o agente verifica se a documentação mínima exigida está presente. Se estiver (mesmo que em tipo diferente de documento), o procedimento é aprovado.
|
||||
3. **Reprovação**: Se nem os critérios de auto-aprovação nem a documentação mínima estiverem presentes, o procedimento é reprovado.
|
||||
|
||||
O agente também verifica, em cada documento, se o nome do beneficiário da guia está presente.
|
||||
|
||||
---
|
||||
|
||||
## Tecnologias Utilizadas
|
||||
|
||||
| Tecnologia | Função |
|
||||
|-------------------|-----------------------------------------------------|
|
||||
| FastAPI | Framework web / API REST |
|
||||
| LangGraph | Orquestração do agente de IA (fluxo ReAct) |
|
||||
| LangChain AWS | Integração com AWS Bedrock (LLM Claude/Anthropic) |
|
||||
| AWS Bedrock | Modelo de linguagem (LLM) para decisão |
|
||||
| AWS Textract | OCR de PDFs e imagens |
|
||||
| AWS S3 | Armazenamento de documentos de entrada e resultados |
|
||||
| AWS Secrets Manager | Gerenciamento seguro de credenciais |
|
||||
| Langfuse | Observabilidade e rastreamento de chamadas ao LLM |
|
||||
| PyPDF2 | Separação de páginas de PDFs para OCR por página |
|
||||
| Pydantic | Validação do corpo da requisição |
|
||||
| Docker | Containerização do serviço |
|
||||
|
||||
---
|
||||
|
||||
## Estrutura de Diretórios
|
||||
|
||||
```
|
||||
code/
|
||||
├── app.py # Ponto de entrada da API FastAPI
|
||||
├── dockerfile # Imagem Docker da aplicação
|
||||
├── Makefile # Automação de build e deploy
|
||||
├── requirements.txt # Dependências Python
|
||||
│
|
||||
├── services/
|
||||
│ ├── authorization.py # Avalia cada serviço via agente de IA
|
||||
│ ├── document_extractor.py # Extrai texto de documentos no S3 via Textract
|
||||
│ └── result_store.py # Persiste resultados no S3
|
||||
│
|
||||
├── utils/
|
||||
│ ├── config.py # Variáveis de ambiente / configurações
|
||||
│ ├── langgraph_agent.py # Definição e execução do agente LangGraph
|
||||
│ ├── rules.yaml # Regras de autorização por código de procedimento
|
||||
│ └── secrets_manager.py # Carrega segredos do AWS Secrets Manager
|
||||
│
|
||||
└── document/ # Documentação do sistema (esta pasta)
|
||||
├── visao-geral.md
|
||||
├── modulos.md
|
||||
└── api.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuração
|
||||
|
||||
### Variáveis de Ambiente
|
||||
|
||||
| Variável | Obrigatória | Padrão | Descrição |
|
||||
|----------------------|-------------|-------------------------------|----------------------------------------------|
|
||||
| `AWS_REGION` | Não | `us-east-2` | Região AWS usada para todos os serviços |
|
||||
| `BEDROCK_MODEL_ARN` | **Sim** | — | ARN do modelo Claude no AWS Bedrock |
|
||||
| `OUTPUT_BUCKET` | Não | `upflux-doc-analyzer` | Bucket S3 onde os resultados são salvos |
|
||||
| `API_VERSION` | Não | `v1` | Prefixo de versão usado na chave do S3 |
|
||||
| `LANGFUSE_HOST` | Não | `https://cloud.langfuse.com` | Host do servidor Langfuse (observabilidade) |
|
||||
|
||||
### Segredos (AWS Secrets Manager — Secret ID: `doc-analyzer`)
|
||||
|
||||
| Chave | Descrição |
|
||||
|------------------------|----------------------------------------------------|
|
||||
| `API-KEY` | Chave de autenticação da API (`X-API-Key`) |
|
||||
| `AWS_ACCESS_KEY` | Access Key para acesso ao S3 de entrada |
|
||||
| `AWS_SECRET_KEY` | Secret Key para acesso ao S3 de entrada |
|
||||
| `LANGFUSE-SECRET-KEY` | Chave secreta do Langfuse |
|
||||
| `LANGFUSE-PUBLIC-KEY` | Chave pública do Langfuse |
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user