diff --git a/.env.example b/.env.example index ccd1365..ebae699 100644 --- a/.env.example +++ b/.env.example @@ -1,51 +1,52 @@ # ============================================ -# CONFIGURACOES DO GOOGLE CLOUD +# VERTEX AI # ============================================ -GOOGLE_PROJECT_ID=id_do_seu_projeto -GOOGLE_LOCATION=loc_do_seu_projeto +GOOGLE_PROJECT_ID=seu_projeto +GOOGLE_LOCATION=us-central1 VERTEX_MODEL_NAME=gemini-2.5-pro # ============================================ -# CONFIGURACOES DO BANCO DE DADOS (MYSQL - TOOLS) +# AMBIENTE # ============================================ -# Banco de metadados de tools (pode ser o mesmo banco do mock) -DB_HOST=localhost +# Valores comuns: development, staging, production +ENVIRONMENT=development + +# Use apenas true ou false. +# Alias como "release" agora sao aceitos, mas evite depender disso. +DEBUG=false + +# ============================================ +# BANCO DE TOOLS (MYSQL) +# ============================================ + +DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=root -DB_PASSWORD=SUA_SENHA +DB_PASSWORD=sua_senha DB_NAME=orquestrador_mock +DB_CLOUD_SQL_CONNECTION_NAME= # ============================================ -# CONFIGURACOES DO BANCO DE DADOS MOCK (MYSQL - DADOS FICTICIOS) +# BANCO MOCK DE NEGOCIO (MYSQL) # ============================================ + MOCK_DB_HOST=127.0.0.1 MOCK_DB_PORT=3306 MOCK_DB_USER=root -MOCK_DB_PASSWORD=SUA_SENHA +MOCK_DB_PASSWORD=sua_senha MOCK_DB_NAME=orquestrador_mock +MOCK_DB_CLOUD_SQL_CONNECTION_NAME= + MOCK_SEED_ENABLED=true AUTO_SEED_TOOLS=true AUTO_SEED_MOCK=true # ============================================ -# CONFIGURACOES DE API - GOOGLE GENERATIVE AI (Gemini) +# TELEGRAM # ============================================ -# Descomente e informe a chave apenas se usar Gemini -# GOOGLE_API_KEY=sua-chave-api-aqui -# ============================================ -# AMBIENTE E DEBUG -# ============================================ -# Valores: development, staging, production -ENVIRONMENT=development -# DEBUG deve ser false em producao -DEBUG=true - -# ============================================ -# TELEGRAM (SERVICO SATELITE) -# ============================================ TELEGRAM_BOT_TOKEN= TELEGRAM_POLLING_TIMEOUT=30 TELEGRAM_REQUEST_TIMEOUT=45 @@ -53,11 +54,11 @@ TELEGRAM_REQUEST_TIMEOUT=45 # ============================================ # ESTADO CONVERSACIONAL # ============================================ -# Valores: memory, redis -CONVERSATION_STATE_BACKEND=memory + +# Em producao com Telegram, prefira redis. +CONVERSATION_STATE_BACKEND=redis CONVERSATION_STATE_TTL_MINUTES=60 -# Redis usado quando CONVERSATION_STATE_BACKEND=redis REDIS_URL=redis://127.0.0.1:6379/0 REDIS_KEY_PREFIX=orquestrador REDIS_SOCKET_TIMEOUT_SECONDS=5 diff --git a/DEPLOY_SERVIDOR.md b/DEPLOY_SERVIDOR.md index 0156df0..21bfa0a 100644 --- a/DEPLOY_SERVIDOR.md +++ b/DEPLOY_SERVIDOR.md @@ -1,15 +1,17 @@ -# Deploy no Servidor da Empresa (Debian/Ubuntu + systemd) +# Deploy do Telegram Satellite no Servidor -## 1) Fluxo correto com Git +Este guia considera o modelo operacional atual do projeto: +- MySQL para tools e base mock +- Redis para estado conversacional +- Vertex AI para o modelo +- Telegram como canal principal -Sim: primeiro voce faz commit e push da sua maquina local, depois no servidor faz pull. - -Exemplo: +## 1) Fluxo com Git ```bash -# local +# maquina local git add . -git commit -m "ajusta mysql/tools + telegram webhook" +git commit -m "ajustes no orquestrador" git push origin main # servidor @@ -17,7 +19,7 @@ cd /opt/orquestrador git pull origin main ``` -## 2) Preparacao no servidor +## 2) Preparacao do ambiente ```bash cd /opt/orquestrador @@ -27,14 +29,25 @@ pip install -U pip pip install -r requirements.txt ``` -## 3) Configurar .env.prod +## 3) Dependencias externas + +Garanta antes da subida: +- MySQL acessivel pelo host configurado +- Redis acessivel pelo host configurado +- credencial valida do Vertex AI +- token do bot do Telegram + +## 4) Configurar `.env.prod` -Crie/atualize o arquivo `.env.prod` no servidor: +Exemplo minimo: ```env GOOGLE_PROJECT_ID=seu-projeto GOOGLE_LOCATION=us-central1 -VERTEX_MODEL_NAME=gemini-2.5-flash +VERTEX_MODEL_NAME=gemini-2.5-pro + +ENVIRONMENT=production +DEBUG=false DB_HOST=127.0.0.1 DB_PORT=3306 @@ -52,30 +65,50 @@ AUTO_SEED_TOOLS=true AUTO_SEED_MOCK=true MOCK_SEED_ENABLED=true -# telegram (opcional) -# TELEGRAM_BOT_TOKEN=... -# TELEGRAM_WEBHOOK_SECRET=... +CONVERSATION_STATE_BACKEND=redis +CONVERSATION_STATE_TTL_MINUTES=60 +REDIS_URL=redis://127.0.0.1:6379/0 +REDIS_KEY_PREFIX=orquestrador +REDIS_SOCKET_TIMEOUT_SECONDS=5 + +TELEGRAM_BOT_TOKEN=seu_token_aqui +TELEGRAM_POLLING_TIMEOUT=30 +TELEGRAM_REQUEST_TIMEOUT=45 ``` -## 4) Credencial para Vertex AI +Observacoes: +- em producao, o satelite exige `CONVERSATION_STATE_BACKEND=redis`; +- use `DEBUG=false` ou `DEBUG=true`, sem valores livres como `release`. + +## 5) Credencial do Vertex AI -Defina um dos formatos abaixo no servidor: +Defina um dos formatos abaixo: - `GOOGLE_APPLICATION_CREDENTIALS=/opt/orquestrador/sa.json` -- ou Application Default Credentials (`gcloud auth application-default login`) +- ou Application Default Credentials -A service account deve ter permissao no Vertex AI (ex.: `roles/aiplatform.user`). +A service account precisa de permissao de uso do Vertex AI, como `roles/aiplatform.user`. -## 5) Configurar service do systemd +## 6) Bootstrap manual inicial -1. Copie o template e ajuste usuario/path: +Antes de ativar o servico, rode uma inicializacao manual para validar banco e seeds: + +```bash +cd /opt/orquestrador +source venv/bin/activate +python -m app.db.init_db +``` + +## 7) Configurar `systemd` + +Copie o template: ```bash sudo cp deploy/systemd/orquestrador.service.example /etc/systemd/system/orquestrador.service sudo nano /etc/systemd/system/orquestrador.service ``` -2. Recarregue e inicie: +Depois recarregue e inicie: ```bash sudo systemctl daemon-reload @@ -83,33 +116,27 @@ sudo systemctl enable --now orquestrador sudo systemctl status orquestrador ``` -3. Logs: +Logs: ```bash journalctl -u orquestrador -f ``` -## 6) Testes rapidos - -```bash -curl -s http://127.0.0.1:8080/openapi.json | head -``` - -```bash -curl -s -X POST http://127.0.0.1:8080/chat \ - -H "Content-Type: application/json" \ - -d '{"message":"Quero um sedan ate 50000"}' -``` +## 8) Validacao rapida -```bash -curl -s -X POST http://127.0.0.1:8080/mock/consultar-estoque \ - -H "Content-Type: application/json" \ - -d '{"preco_max":50000,"categoria":"sedan"}' -``` +Checklist minimo: +- o servico sobe sem excecao +- o Redis responde +- o bot aparece online no Telegram +- uma mensagem simples recebe resposta -## 7) Atualizacao em producao +Teste funcional: +1. Abra o bot no Telegram. +2. Envie uma mensagem como `Ola`. +3. Envie uma mensagem operacional como `Quero ver carros ate 50000 reais`. +4. Verifique os logs do servico se necessario. -Sempre que subir novas mudancas: +## 9) Atualizacao em producao ```bash cd /opt/orquestrador diff --git a/Dockerfile b/Dockerfile index d782eb3..7fbbfe4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,6 @@ WORKDIR /build RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ - libpq-dev \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . @@ -19,15 +18,9 @@ FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 -ENV PORT=8080 WORKDIR /app -# Install runtime dependencies only -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpq5 \ - && rm -rf /var/lib/apt/lists/* - # Copy Python dependencies from builder COPY --from=builder /root/.local /root/.local @@ -36,9 +29,5 @@ COPY app /app/app ENV PATH=/root/.local/bin:$PATH -EXPOSE 8080 - -#HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ -# CMD python -c "import httpx; httpx.get('http://localhost:8080/docs', timeout=5)" || exit 1 - -CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080}"] +# Sobe o bootstrap de banco e inicia o satelite do Telegram. +CMD ["sh", "-c", "python -m app.db.init_db && python -m app.integrations.telegram_satellite_service"] diff --git a/README.md b/README.md index 9614bb8..3eea69e 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,76 @@ # AI Orquestrador -Backend conversacional para concessionária, focado em orquestrar ferramentas de negócio com apoio de LLM. +Orquestrador conversacional para concessionaria, focado em combinar: +- interpretacao semantica via Vertex AI +- execucao deterministica de tools de negocio +- normalizacao tecnica na aplicacao +- atendimento pelo Telegram como canal principal -A proposta do sistema é: -- receber uma mensagem em linguagem natural -- deixar o modelo interpretar a intenção do usuário -- usar ferramentas do sistema quando precisar consultar dados ou executar ações -- devolver uma resposta objetiva ao usuário +## Proposta -Hoje o projeto já implementa esse fluxo, mas ainda inclui camadas de suporte ao modelo, como extração estruturada, memória temporária por usuário, coleta incremental de dados e fallback determinístico de resposta. +A ideia central do sistema e: +- o usuario conversa em linguagem natural +- o modelo entende intencao, contexto e proximo passo +- a aplicacao normaliza dados, protege regras e executa tools +- a resposta final volta para o usuario pelo canal de atendimento -## Visão Geral +Hoje o projeto esta mais proximo de um orquestrador conversacional do que de uma API tradicional. O FastAPI continua no repositorio como apoio legado/bootstrap, mas o fluxo operacional principal e o satelite do Telegram. -O domínio atual do projeto cobre dois fluxos principais: +## Estado Atual + +O dominio atual cobre dois grupos principais: - vendas -- revisão/pós-venda +- revisao e pos-venda -O sistema consegue: -- consultar estoque de veículos +Capacidades ja implementadas: +- consultar estoque de veiculos - validar cliente para compra -- avaliar veículo para troca -- criar pedido de compra +- avaliar veiculo para troca +- criar pedido +- listar pedidos - cancelar pedido -- agendar revisão -- listar revisões agendadas -- cancelar revisão -- remarcar revisão +- agendar revisao +- listar revisoes +- cancelar revisao +- remarcar revisao -Além do roteamento por tool, o orquestrador também trata cenários conversacionais como: -- mensagens com mais de um pedido na mesma entrada -- coleta de parâmetros em múltiplas mensagens -- reaproveitamento de contexto temporário -- confirmação antes de trocar de assunto -- sugestão de próximo horário quando uma revisão entra em conflito +Capacidades conversacionais ja tratadas: +- multiplos pedidos na mesma mensagem +- fila de pedidos +- troca de contexto entre dominios +- coleta incremental de campos +- reaproveitamento de contexto temporario +- confirmacao antes de mudar de assunto +- sugestao de horario alternativo em conflito de agenda -## Stack Atual +## Arquitetura Atual -| Componente | Tecnologia | Observação | +| Componente | Tecnologia | Papel atual | | --- | --- | --- | -| Backend | FastAPI | API principal | -| LLM | Google Vertex AI | Modelos Gemini | -| Banco de tools | MySQL | Metadados das ferramentas | -| Banco mock de negócio | MySQL | Veículos, clientes, usuários, pedidos e revisões | -| ORM | SQLAlchemy | Acesso aos dois bancos | -| Runtime | Python 3.11+ | Aplicação principal | -| Integração de canal | Telegram Bot API | Long polling via serviço satélite | -| Containerização | Docker | Ambiente local e deploy | - -## Como o Sistema Funciona - -Fluxo principal do `/chat`: - -1. O usuário envia uma mensagem. -2. O `OrquestradorService` inicializa ou atualiza o contexto temporário do usuário. -3. O sistema tenta separar múltiplos pedidos contidos na mesma mensagem. -4. O LLM ajuda a extrair intenções e entidades estruturadas. -5. Se houver fluxo em aberto, o orquestrador continua a coleta dos dados faltantes. -6. Quando necessário, o modelo escolhe uma tool. -7. A tool é executada pelos handlers do sistema. -8. O resultado é devolvido ao usuário, usando resposta do modelo ou fallback determinístico. +| LLM | Vertex AI / Gemini | interpretacao e apoio a decisao | +| Orquestracao | Python | coordena contexto, fluxo e tools | +| Metadados de tools | MySQL | catalogo das tools disponiveis | +| Dados mock de negocio | MySQL | veiculos, clientes, usuarios, pedidos e revisoes | +| Estado conversacional | Redis ou memoria | rascunhos, fila e contexto temporario | +| Canal | Telegram Bot API | atendimento principal via long polling | +| Containerizacao | Docker Compose | ambiente local e deploy simples | + +## Fluxo Principal + +Fluxo principal do atendimento pelo Telegram: + +1. O usuario envia uma mensagem ao bot. +2. O `TelegramSatelliteService` identifica ou cria o usuario interno. +3. O `OrquestradorService` recupera o contexto conversacional. +4. O `MessagePlanner` e o Vertex ajudam a estruturar a intencao do turno. +5. A aplicacao continua fluxos abertos ou decide a proxima tool. +6. A tool e executada com validacoes e normalizacoes deterministicas. +7. O sistema devolve a resposta ao usuario no Telegram. Importante: -- a memória de conversa atual é volátil e fica em memória do processo -- o sistema ainda não persiste histórico conversacional entre reinícios -- o `response_formatter` responde direto ao usuário; ele não devolve o resultado para o modelo +- o historico completo da conversa ainda nao e persistido como trilha audivel; +- o estado de trabalho pode ficar em memoria ou Redis; +- em producao, Redis e o backend recomendado para continuidade real. ## Estrutura do Projeto @@ -71,11 +78,6 @@ Importante: app/ main.py api/ - routes/ - chat.py - mock.py - tools.py - dependencies.py schemas.py core/ settings.py @@ -87,170 +89,117 @@ app/ mock_seed.py models/ tool.py + integrations/ + telegram_satellite_service.py + models/ + tool_model.py repositories/ tool_repository.py user_repository.py - integrations/ - telegram_satellite_service.py services/ ai/ llm_service.py + domain/ + credit_service.py + inventory_service.py + order_service.py + review_service.py flows/ order_flow.py review_flow.py orchestration/ - orquestrador_service.py - orchestrator_config.py + conversation_policy.py + conversation_state_repository.py conversation_state_store.py + entity_normalizer.py + message_planner.py + orquestrador_service.py prompt_builders.py + redis_state_repository.py response_formatter.py + tool_executor.py + turn_decision.py tools/ handlers.py tool_registry.py user/ + mock_customer_service.py user_service.py +tests/ + ... ``` -## Organização das Camadas - -### `services/orchestration` - -Núcleo do orquestrador: -- coordenação do fluxo conversacional -- controle de contexto ativo -- fila de pedidos -- construção de prompts -- fallback de resposta - -### `services/flows` - -Fluxos incrementais de negócio: -- criação e cancelamento de pedido -- agendamento, listagem, cancelamento e remarcação de revisão - -### `services/ai` - -Integração com Vertex AI e Gemini. +## Tools Disponiveis -### `services/tools` - -Registro das tools e implementação dos handlers que executam as ações reais. - -### `services/user` - -Serviços ligados à identificação e persistência de usuários de canal, como Telegram. - -## Ferramentas Disponíveis - -As tools padrão são semeadas a partir de [app/db/tool_seed.py](app/db/tool_seed.py): +As definicoes padrao ficam em [app/db/tool_seed.py](/d:/vitor/Pessoal/PJ/Orquestrador/app/db/tool_seed.py): | Tool | Finalidade | | --- | --- | -| `consultar_estoque` | Consulta veículos por preço, categoria e ordenação | -| `validar_cliente_venda` | Valida elegibilidade de compra por CPF e valor | -| `avaliar_veiculo_troca` | Estima valor de troca do veículo do cliente | -| `agendar_revisao` | Agenda revisão com cálculo de valor | -| `listar_agendamentos_revisao` | Lista revisões do usuário | -| `cancelar_agendamento_revisao` | Cancela uma revisão por protocolo | -| `editar_data_revisao` | Remarca revisão existente | -| `realizar_pedido` | Cria pedido de compra | -| `cancelar_pedido` | Cancela pedido existente | - -## Bancos e Seeds - -O projeto usa duas conexões distintas: - -### Banco de tools - -Responsável por armazenar: -- nome da tool -- descrição -- schema de parâmetros - -Arquivos principais: -- `app/db/database.py` -- `app/db/models/tool.py` -- `app/db/tool_seed.py` - -### Banco mock de negócio - -Responsável por armazenar dados fictícios de: -- veículos -- clientes -- usuários -- pedidos -- agendamentos de revisão - -Arquivos principais: -- `app/db/mock_database.py` -- `app/db/mock_models.py` -- `app/db/mock_seed.py` - -No startup, a aplicação tenta: -- criar as tabelas dos dois bancos -- popular tools -- popular dados mock -- fazer warmup do LLM +| `consultar_estoque` | consulta veiculos por preco, categoria e ordenacao | +| `validar_cliente_venda` | valida elegibilidade de compra por CPF e valor | +| `avaliar_veiculo_troca` | estima valor de troca do veiculo do cliente | +| `agendar_revisao` | agenda revisao com calculo de valor | +| `listar_agendamentos_revisao` | lista revisoes do usuario | +| `cancelar_agendamento_revisao` | cancela revisao por protocolo | +| `editar_data_revisao` | remarca revisao existente | +| `realizar_pedido` | cria pedido de compra | +| `listar_pedidos` | lista pedidos do usuario | +| `cancelar_pedido` | cancela pedido existente | +| `limpar_contexto_conversa` | zera contexto e fila | +| `continuar_proximo_pedido` | retoma o proximo item da fila | +| `descartar_pedidos_pendentes` | limpa apenas a fila pendente | +| `cancelar_fluxo_atual` | encerra apenas o fluxo atual | + +## Banco e Bootstrap + +O projeto usa duas conexoes MySQL: +- banco de tools +- banco mock de negocio + +O bootstrap atual cria tabelas e executa seed por meio de: +- [app/db/init_db.py](/d:/vitor/Pessoal/PJ/Orquestrador/app/db/init_db.py) + +Esse bootstrap e usado no container e pode ser executado manualmente antes do servico principal. + +## Execucao Local -## Endpoints - -### Chat - -- `POST /chat` - - entrada principal do orquestrador - -### Tools - -- `GET /tools/` -- `POST /tools/` -- `GET /tools/{tool_id}` -- `DELETE /tools/{tool_id}` - -### Mock - -Endpoints diretos para depuração e testes manuais dos handlers: - -- `POST /mock/consultar-estoque` -- `POST /mock/validar-cliente-venda` -- `POST /mock/avaliar-veiculo-troca` -- `POST /mock/agendar-revisao` -- `POST /mock/listar-agendamentos-revisao` -- `POST /mock/cancelar-agendamento-revisao` -- `POST /mock/editar-data-revisao` -- `POST /mock/realizar-pedido` -- `POST /mock/cancelar-pedido` +### Sem Docker -Documentação interativa: -- `GET /docs` +1. Configure as variaveis de ambiente com base em `.env.example`. +2. Inicialize banco e seed: -## Execução Local +```bash +python -m app.db.init_db +``` -### Sem Docker +3. Inicie o satelite do Telegram: ```bash -uvicorn app.main:app --reload +python -m app.integrations.telegram_satellite_service ``` ### Com Docker Compose +O compose atual sobe: +- `mysql` +- `redis` +- `telegram` + +Subida completa: + ```bash -docker-compose up +docker compose up --build ``` -Para testar estado conversacional em Redis com Docker Compose: +Somente infraestrutura: ```bash -docker-compose up redis app +docker compose up mysql redis ``` -Arquivos úteis: -- `TEST_CASES.md` -- `DEPLOY_SERVIDOR.md` -- `.env.example` - -## Variáveis de Ambiente +## Variaveis de Ambiente -Principais variáveis: +Principais variaveis: ### Vertex AI @@ -285,7 +234,7 @@ Principais variáveis: - `TELEGRAM_POLLING_TIMEOUT` - `TELEGRAM_REQUEST_TIMEOUT` -### Estado Conversacional +### Estado conversacional - `CONVERSATION_STATE_BACKEND` (`memory` ou `redis`) - `CONVERSATION_STATE_TTL_MINUTES` @@ -293,36 +242,50 @@ Principais variáveis: - `REDIS_KEY_PREFIX` - `REDIS_SOCKET_TIMEOUT_SECONDS` -## Telegram Satellite Service +### Ambiente -Existe um serviço satélite para atendimento via Telegram em long polling. +- `ENVIRONMENT` +- `DEBUG` -Arquivo principal: -- `app/integrations/telegram_satellite_service.py` +Observacao: +- para producao com Telegram, prefira `CONVERSATION_STATE_BACKEND=redis`; +- evite valores nao booleanos em `DEBUG`. -Execução: +## Docker + +O [Dockerfile](/d:/vitor/Pessoal/PJ/Orquestrador/Dockerfile) hoje sobe o servico principal do projeto: ```bash -python -m app.integrations.telegram_satellite_service +python -m app.db.init_db && python -m app.integrations.telegram_satellite_service +``` + +Isso deixa o container alinhado com o uso atual do sistema, sem assumir FastAPI como interface principal. + +## Testes + +Suite automatizada atual: + +```bash +python -m unittest discover -s tests -v ``` -Esse serviço: -- consome mensagens do Telegram -- cria ou recupera o usuário interno -- encaminha a mensagem para o `OrquestradorService` -- envia a resposta de volta ao chat - -## Estado Atual do Projeto - -O sistema já está além de um MVP simples de tool calling. Hoje ele combina: -- decisão assistida por modelo -- execução determinística de tools -- memória volátil por usuário -- fluxos multi-etapas -- múltiplos pedidos na mesma conversa - -Pontos que ainda permanecem em aberto: -- persistência real de histórico conversacional -- suíte automatizada de testes -- evolução da autonomia do modelo -- alinhamento contínuo entre prompts, fluxo e regras de negócio +Se o ambiente local tiver `DEBUG` com valor invalido, force antes da execucao: + +```bash +DEBUG=false python -m unittest discover -s tests -v +``` + +## Arquivos Uteis + +- [TEST_CASES.md](/d:/vitor/Pessoal/PJ/Orquestrador/TEST_CASES.md) +- [DEPLOY_SERVIDOR.md](/d:/vitor/Pessoal/PJ/Orquestrador/DEPLOY_SERVIDOR.md) +- [.env.example](/d:/vitor/Pessoal/PJ/Orquestrador/.env.example) + +## Proximos Passos Naturais + +Os proximos ganhos mais valiosos para o projeto sao: +- persistir trilha de conversa e decisoes +- desacoplar bootstrap de banco do startup da aplicacao +- aumentar observabilidade por turno e por tool +- reduzir o tamanho do `OrquestradorService` +- consolidar documentacao operacional Telegram-first diff --git a/TEST_CASES.md b/TEST_CASES.md index ff75d9b..8ea665a 100644 --- a/TEST_CASES.md +++ b/TEST_CASES.md @@ -6,34 +6,27 @@ Este documento foi reorganizado para validar a etapa atual do projeto: orquestra ### Setup +Opcao sem Docker: + ```bash -# Terminal 1 -uvicorn app.main:app --reload --log-level debug +python -m app.db.init_db +python -m app.integrations.telegram_satellite_service ``` -Opcional com Docker: +Opcao com Docker: ```bash -docker-compose up +docker compose up --build ``` -### Endpoint principal +### Canal principal -Todos os cenarios abaixo usam: - -```bash -curl -X POST http://localhost:8000/chat \ - -H "Content-Type: application/json" \ - -d '{ - "message": "SUA_MENSAGEM_AQUI", - "user_id": "qa-user-001" - }' -``` +Todos os cenarios abaixo devem ser executados enviando as mensagens diretamente ao bot no Telegram. Observacoes: -- Reaproveite o mesmo `user_id` dentro de cada cenario multi-turno. -- Troque o `user_id` entre cenarios para evitar contaminacao de contexto. -- Quando precisar inspecionar handlers diretamente, use os endpoints em `/mock/*`. +- Reaproveite a mesma conta do Telegram dentro de cada cenario multi-turno. +- Entre cenarios, limpe o contexto ou use outra conta para evitar contaminacao. +- O foco deste roteiro e validar comportamento conversacional, nao endpoints HTTP. ## Convencoes de Validacao @@ -490,23 +483,6 @@ Esperado: ## Apoio de Debug -### Swagger UI - -Acesse: - -```text -http://localhost:8000/docs -``` - -### Endpoints mock uteis - -- `POST /mock/agendar-revisao` -- `POST /mock/listar-agendamentos-revisao` -- `POST /mock/cancelar-agendamento-revisao` -- `POST /mock/editar-data-revisao` -- `POST /mock/realizar-pedido` -- `POST /mock/cancelar-pedido` - ### O que observar nos logs Com `--log-level debug`, acompanhe: diff --git a/app/core/settings.py b/app/core/settings.py index c3a3883..f9ecf8e 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -1,7 +1,13 @@ -from pydantic_settings import BaseSettings +from pydantic import field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + extra="ignore", + ) + google_project_id: str google_location: str = "us-central1" vertex_model_name: str = "gemini-2.5-pro" @@ -49,9 +55,23 @@ class Settings(BaseSettings): redis_key_prefix: str = "orquestrador" redis_socket_timeout_seconds: int = 5 - class Config: - env_file = ".env" - extra = "ignore" + @field_validator("debug", mode="before") + @classmethod + def parse_debug_aliases(cls, value): + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"debug", "development", "dev"}: + return True + if normalized in {"release", "production", "prod"}: + return False + return value + + @field_validator("environment", "conversation_state_backend", mode="before") + @classmethod + def normalize_text_settings(cls, value): + if isinstance(value, str): + return value.strip().lower() + return value settings = Settings() diff --git a/app/db/init_db.py b/app/db/init_db.py index 77fb1de..30b3bb7 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -3,6 +3,7 @@ Inicializacao de banco de dados. Cria tabelas e executa seed inicial em ambos os bancos. """ +from app.core.settings import settings from app.db.database import Base, engine from app.db.mock_database import MockBase, mock_engine from app.db.models import Tool @@ -14,24 +15,38 @@ from app.db.tool_seed import seed_tools def init_db(): """Cria tabelas e executa seed inicial em ambos os bancos.""" print("Inicializando bancos...") + failures: list[str] = [] try: print("Criando tabelas MySQL (tools)...") Base.metadata.create_all(bind=engine) - print("Populando tools iniciais...") - seed_tools() + if settings.auto_seed_tools: + print("Populando tools iniciais...") + seed_tools() + else: + print("Seed de tools desabilitada por configuracao.") print("MySQL tools OK.") except Exception as exc: print(f"Aviso: falha no MySQL (tools): {exc}") + failures.append(f"tools={exc}") try: print("Criando tabelas MySQL (dados ficticios)...") MockBase.metadata.create_all(bind=mock_engine) - print("Populando dados ficticios iniciais...") - seed_mock_data() + if settings.auto_seed_mock and settings.mock_seed_enabled: + print("Populando dados ficticios iniciais...") + seed_mock_data() + else: + print("Seed mock desabilitada por configuracao.") print("MySQL mock OK.") except Exception as exc: print(f"Aviso: falha no MySQL mock: {exc}") + failures.append(f"mock={exc}") + + if failures: + raise RuntimeError( + "Falha ao inicializar bancos do orquestrador: " + " | ".join(failures) + ) print("Bancos inicializados com sucesso!") diff --git a/app/main.py b/app/main.py index 5363601..2b31d93 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,6 @@ from fastapi import FastAPI -from app.core.settings import settings -from app.db.database import Base, engine -from app.db.mock_database import MockBase, mock_engine -from app.db.models import Tool -from app.db.mock_models import Customer, Order, ReviewSchedule, Vehicle +from app.db.init_db import init_db from app.services.ai.llm_service import LLMService app = FastAPI(title="AI Orquestrador") @@ -15,26 +11,8 @@ async def startup_event(): """ Inicializa o banco de dados e executa seeds automaticamente. """ - print("[Auto-Seed] Iniciando configuracao do banco...") - - # Tools (MySQL) e mock (MySQL) sobem de forma independente. - try: - Base.metadata.create_all(bind=engine) - if settings.auto_seed_tools: - from app.db.tool_seed import seed_tools - seed_tools() - print("[Auto-Seed] MySQL de tools inicializado.") - except Exception as e: - print(f"[Auto-Seed] Aviso: falha ao inicializar MySQL (tools): {e}") - - try: - MockBase.metadata.create_all(bind=mock_engine) - if settings.auto_seed_mock and settings.mock_seed_enabled: - from app.db.mock_seed import seed_mock_data - seed_mock_data() - print("[Auto-Seed] MySQL de mock inicializado.") - except Exception as e: - print(f"[Auto-Seed] Aviso: falha ao inicializar MySQL (mock): {e}") + print("[Startup] Iniciando bootstrap legado do app HTTP...") + init_db() try: await LLMService().warmup() @@ -42,4 +20,4 @@ async def startup_event(): except Exception as e: print(f"[Startup] Aviso: falha no warmup do LLM: {e}") - print("[Auto-Seed] Startup finalizado.") + print("[Startup] App HTTP legado inicializado.") diff --git a/deploy/systemd/orquestrador.service.example b/deploy/systemd/orquestrador.service.example index 1551728..b390ca9 100644 --- a/deploy/systemd/orquestrador.service.example +++ b/deploy/systemd/orquestrador.service.example @@ -1,5 +1,5 @@ [Unit] -Description=AI Orquestrador API (FastAPI/Uvicorn) +Description=AI Orquestrador Telegram Satellite After=network.target [Service] @@ -9,7 +9,7 @@ Group=vitor WorkingDirectory=/opt/orquestrador EnvironmentFile=/opt/orquestrador/.env.prod Environment=PATH=/opt/orquestrador/venv/bin -ExecStart=/opt/orquestrador/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8080 +ExecStart=/bin/sh -c '/opt/orquestrador/venv/bin/python -m app.db.init_db && /opt/orquestrador/venv/bin/python -m app.integrations.telegram_satellite_service' Restart=always RestartSec=5 diff --git a/docker-compose.yml b/docker-compose.yml index f9ab309..ef56082 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,20 @@ -version: "3.8" - services: - postgres: - image: postgres:15-alpine - container_name: orquestrador_postgres + mysql: + image: mysql:8.4 + container_name: orquestrador_mysql environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: orquestrador + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: orquestrador_mock ports: - - "5432:5432" + - "3306:3306" volumes: - - postgres_data:/var/lib/postgresql/data + - mysql_data:/var/lib/mysql healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD"] interval: 10s timeout: 5s - retries: 5 + retries: 10 + start_period: 20s redis: image: redis:7-alpine @@ -29,35 +27,42 @@ services: timeout: 5s retries: 5 - app: + telegram: build: . - container_name: orquestrador_app - ports: - - "8000:8080" + container_name: orquestrador_telegram environment: - GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID} + GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID:-local-dev} GOOGLE_LOCATION: ${GOOGLE_LOCATION:-us-central1} - DB_HOST: postgres - DB_PORT: 5432 - DB_USER: postgres - DB_PASSWORD: postgres - DB_NAME: orquestrador - MOCKAROO_API_KEY: ${MOCKAROO_API_KEY} - MOCKAROO_BASE_URL: ${MOCKAROO_BASE_URL:-https://my.api.mockaroo.com} - USE_MOCKAROO_WRITES: ${USE_MOCKAROO_WRITES:-false} + VERTEX_MODEL_NAME: ${VERTEX_MODEL_NAME:-gemini-2.5-pro} + ENVIRONMENT: ${ENVIRONMENT:-development} + DEBUG: ${ORQUESTRADOR_DEBUG:-false} + DB_HOST: mysql + DB_PORT: 3306 + DB_USER: root + DB_PASSWORD: root + DB_NAME: orquestrador_mock + MOCK_DB_HOST: mysql + MOCK_DB_PORT: 3306 + MOCK_DB_USER: root + MOCK_DB_PASSWORD: root + MOCK_DB_NAME: orquestrador_mock + AUTO_SEED_TOOLS: ${AUTO_SEED_TOOLS:-true} + AUTO_SEED_MOCK: ${AUTO_SEED_MOCK:-true} + MOCK_SEED_ENABLED: ${MOCK_SEED_ENABLED:-true} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} + TELEGRAM_POLLING_TIMEOUT: ${TELEGRAM_POLLING_TIMEOUT:-30} + TELEGRAM_REQUEST_TIMEOUT: ${TELEGRAM_REQUEST_TIMEOUT:-45} CONVERSATION_STATE_BACKEND: ${CONVERSATION_STATE_BACKEND:-redis} CONVERSATION_STATE_TTL_MINUTES: ${CONVERSATION_STATE_TTL_MINUTES:-60} REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX:-orquestrador} REDIS_SOCKET_TIMEOUT_SECONDS: ${REDIS_SOCKET_TIMEOUT_SECONDS:-5} depends_on: - postgres: + mysql: condition: service_healthy redis: condition: service_healthy - volumes: - - .:/app - command: uvicorn app.main:app --host 0.0.0.0 --port 8080 --reload + restart: unless-stopped volumes: - postgres_data: + mysql_data: diff --git a/tests/test_runtime_bootstrap.py b/tests/test_runtime_bootstrap.py new file mode 100644 index 0000000..2d988f8 --- /dev/null +++ b/tests/test_runtime_bootstrap.py @@ -0,0 +1,73 @@ +import unittest +from unittest.mock import patch + +from app.core.settings import Settings +from app.db import init_db as init_db_module + + +class SettingsParsingTests(unittest.TestCase): + def test_debug_accepts_release_alias(self): + settings = Settings( + google_project_id="test-project", + debug="release", + ) + + self.assertFalse(settings.debug) + + def test_normalizes_environment_and_backend_values(self): + settings = Settings( + google_project_id="test-project", + environment=" Production ", + conversation_state_backend=" Redis ", + ) + + self.assertEqual(settings.environment, "production") + self.assertEqual(settings.conversation_state_backend, "redis") + + +class InitDbBootstrapTests(unittest.TestCase): + @patch.object(init_db_module, "seed_tools") + @patch.object(init_db_module, "seed_mock_data") + @patch.object(init_db_module.MockBase.metadata, "create_all") + @patch.object(init_db_module.Base.metadata, "create_all") + def test_init_db_respects_seed_flags( + self, + tools_create_all, + mock_create_all, + seed_mock_data, + seed_tools, + ): + with patch.object(init_db_module.settings, "auto_seed_tools", False), patch.object( + init_db_module.settings, + "auto_seed_mock", + False, + ), patch.object(init_db_module.settings, "mock_seed_enabled", True): + init_db_module.init_db() + + tools_create_all.assert_called_once() + mock_create_all.assert_called_once() + seed_tools.assert_not_called() + seed_mock_data.assert_not_called() + + @patch.object(init_db_module, "seed_tools") + @patch.object(init_db_module, "seed_mock_data") + @patch.object(init_db_module.MockBase.metadata, "create_all") + @patch.object(init_db_module.Base.metadata, "create_all", side_effect=RuntimeError("tools db down")) + def test_init_db_raises_when_any_backend_fails( + self, + tools_create_all, + mock_create_all, + seed_mock_data, + seed_tools, + ): + with self.assertRaisesRegex(RuntimeError, "tools=tools db down"): + init_db_module.init_db() + + tools_create_all.assert_called_once() + mock_create_all.assert_called_once() + seed_mock_data.assert_called_once() + seed_tools.assert_not_called() + + +if __name__ == "__main__": + unittest.main()