diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..5f3cf01 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,294 @@ +# Documentação da API + +## Base URL + +``` +http://:8000 +``` + +## Autenticação + +Todos os endpoints protegidos requerem o header: + +``` +X-API-Key: +``` + +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": [ ] +} +``` + +#### 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:////_.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). diff --git a/docs/langfuse.md b/docs/langfuse.md new file mode 100644 index 0000000..062070a --- /dev/null +++ b/docs/langfuse.md @@ -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 \ + --service \ + --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. diff --git a/docs/modulos.md b/docs/modulos.md new file mode 100644 index 0000000..89f0e87 --- /dev/null +++ b/docs/modulos.md @@ -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: +``` + +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 `"--- ---\n"`. + +### 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:////_.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 ``. + +**Observabilidade:** Cada execução é rastreada no Langfuse via `CallbackHandler`. + +**Retorno:** + +```python +{ + "response": "", + "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. diff --git a/docs/visao-geral.md b/docs/visao-geral.md new file mode 100644 index 0000000..8ed5ff9 --- /dev/null +++ b/docs/visao-geral.md @@ -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 | + +---