Estrutura do projeto¶
Estrutura canônica de pastas e arquivos. Use isto como template ao criar um novo chatbot OTIMUS.
NOVOPROJETO/
├── .env.template
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── init.sql # schema do Postgres
├── requirements.txt
├── README.md
├── CLAUDE.md # guia específico para agentes Claude
│
├── config.py # env vars + logger
├── server.py # FastAPI — webhooks
├── workflow.py # LangGraph Functional API (entrypoint principal)
├── agent.py # ReAct agent + tools + system prompt
├── scheduling.py # cliente OTIMUS + regras de negócio
├── wts_client.py # cliente FlowChat/WTS Chat
├── processors.py # Gemini (imagem/PDF) + Whisper (áudio)
├── db.py # fila de mensagens em Postgres
└── tracking.py # telemetria dashboard
Por arquivo¶
config.py¶
Único lugar onde se lê os.getenv. Zero lógica.
import os, logging
# --- Obrigatórias ---
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
WTS_API_TOKEN = os.getenv("WTS_API_TOKEN", "")
DATABASE_URL = os.getenv("DATABASE_URL", "")
# --- OTIMUS ---
OTIMUS_API_URL = os.getenv("OTIMUS_API_URL", "")
OTIMUS_TOKEN = os.getenv("OTIMUS_TOKEN", "")
# --- WTS ---
WTS_BASE_URL = os.getenv("WTS_BASE_URL", "https://api.wts.chat")
WTS_CHANNEL_ID = os.getenv("WTS_CHANNEL_ID", "")
WTS_DEPARTMENT_HUMAN_ID = os.getenv("WTS_DEPARTMENT_HUMAN_ID", "")
WTS_DEPARTMENT_AI_ID = os.getenv("WTS_DEPARTMENT_AI_ID", "")
# --- Workflow ---
ALLOWED_INSTANCES = [i.strip() for i in os.getenv("ALLOWED_INSTANCE", "").split(",") if i.strip()]
ALLOWED_TEAM = os.getenv("ALLOWED_TEAM", "")
MESSAGE_DEBOUNCE_SECONDS = int(os.getenv("MESSAGE_DEBOUNCE_SECONDS", "5"))
CHAT_HISTORY_TABLE = os.getenv("CHAT_HISTORY_TABLE", "dados_cliente_projeto")
MESSAGE_QUEUE_TABLE = os.getenv("MESSAGE_QUEUE_TABLE", "n8n_fila_mensagens_projeto")
CONTEXT_WINDOW_LENGTH = int(os.getenv("CONTEXT_WINDOW_LENGTH", "50"))
# --- Mídia ---
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
# --- Logging ---
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger("<projeto>")
server.py¶
Thin layer sobre FastAPI. Apenas routing, validação grosseira de payload e thread spawn. Nenhuma lógica de negócio aqui.
workflow.py¶
@entrypoint do LangGraph Functional API. Orquestra:
- Fetch session do WTS (
get_session_by_id). - Filtros (platform, instance, team, status).
- Processamento de mídia (via
processors.py). - Debounce + anti-overlap (via
db.py). agent.invoke().- Split e envio da resposta (via
wts_client.py).
agent.py¶
- Instancia
llm = ChatOpenAI(model, api_key, temperature). - Conecta
PostgresSaver(fallbackMemorySaver). - Define tools com
@tool. - Define
SYSTEM_PROMPT(string gigante). - Constrói
agent = create_react_agent(llm, tools, prompt, checkpointer). - Expõe
run_agent(message, session_id, phone_number, contact_name).
scheduling.py (ou agendamento.py)¶
Núcleo da integração OTIMUS. Contém:
PROCEDIMENTOS/CONVENIOS/_DOCTOR_KEYWORDS— dados da clínica._otimus_headers()— headers autenticados.- Funções públicas:
consultar_horarios(),agendar_consulta(). - Funções privadas:
_chamar_api_horarios(),_verificar_disponibilidade(),_buscar_paciente_cpf(),_cadastrar_paciente(),_confirmar_agendamento(). - Helpers:
_normalize(),_only_digits(),_yyyymmdd_to_ddmmyyyy(),_validar_agenda_medico(),_filter_and_select_slots().
wts_client.py¶
Cliente FlowChat/WTS Chat. Funções públicas:
get_session_by_id(session_id)send_message(phone, text)— via telefonesend_message_in_session(session_id, text)— via sessãotransfer_session(session_id)add_tags(phone, tags)
processors.py¶
process_image(url, mime_type)→ texto (via Gemini)process_audio(url)→ texto (via Whisper)process_document(url, mime_type)→ texto (via Gemini, PDFs)
db.py¶
enqueue_message(phone, message_id, message)fetch_and_clear_queue(phone)→ lista de textosis_message_newer(phone, session_id)→ anti-overlap
tracking.py¶
log_session(session_id, phone, contact_name, content_type)log_token_usage(session_id, provider, model, prompt_tokens, completion_tokens)log_error(error_type, error_message, session_id, phone, stack_trace)log_tag(session_id, phone, tag_name)log_transfer(session_id)
init.sql¶
Schema do Postgres. Ver Database.
Dockerfile¶
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
docker-compose.yml (minimal)¶
services:
chatbot:
build: .
container_name: <projeto>-chatbot
restart: unless-stopped
env_file: .env
ports:
- "8000:8000"
depends_on:
- postgres
postgres:
image: postgres:16
container_name: <projeto>-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
volumes:
pgdata:
CLAUDE.md (por projeto)¶
Cada projeto deve ter um CLAUDE.md com só as informações específicas
dele — tudo que é padrão OTIMUS deve apontar para este OTIDOC. Ver exemplo em
CLINFETO (/home/DOCKER/CLINFETO/CLAUDE.md).