# TORQ SDR Bruno — Webhook

Agente SDR que atende WhatsApp inbound da TORQ Brasil via Linkia (GoHighLevel).
Framework ALMA, 24 campos custom `sdr__`, pipeline TESTE RAFA (Climb).

## Arquitetura de LLM (IMPORTANTE)

Bruno **NAO usa a API paga da Anthropic**. Ele roda em cima da **assinatura Claude Code
Max plan** do Renato, via CLI headless (`claude --print`).

- Binario: `/usr/bin/claude` (instalado com `npm i -g @anthropic-ai/claude-code`)
- Auth: `/home/mia/.claude/.credentials.json` (OAuth da assinatura)
- Chamada: `subprocess.run(["claude", "--print", "--output-format", "json", "--append-system-prompt", DOSSIE, "--model", "claude-sonnet-4-6"], input=USER_PROMPT)`
- Prompt caching: aplicado automaticamente pelo CLI no `--append-system-prompt`

Por que isso importa:
- Zero custo variavel por request (assinatura ja paga)
- Nunca vai dar "credit balance too low"
- Se precisar re-autenticar: `sudo -u mia claude login` (uma vez)

## Fluxo

```
Lead responde WhatsApp
      ↓
GHL webhook (automacao "cliente respondeu no WhatsApp")
      ↓
POST https://sdr-torq.agenciaclimb.com.br/webhook
      ↓
Bruno webhook (porta 8940)
      ↓ (debounce 6s, agrupa msgs)
Busca contato + custom fields + historico da conversa (GHL API)
      ↓
Claude Sonnet 4.6 via CLI headless (assinatura Max plan + prompt caching automatico)
      ↓
Parseia resposta: texto pro lead + META (campos, stage, notify)
      ↓
1) Envia texto pro lead (via GHL Conversations API, blocos [[BREAK]] com typing delay)
2) Atualiza custom fields sdr__ do contato
3) Move opportunity no pipeline TESTE RAFA (se stage mudou)
4) Notifica Renato via outbox Mia (se notificar_renato=true)
```

## URL publica

**Webhook:** `https://sdr-torq.agenciaclimb.com.br/webhook`
**Health:** `https://sdr-torq.agenciaclimb.com.br/health`

Porta interna: **8940** (definida via `PORT=8940` no unit systemd).

## Estrutura

```
sdr_bruno_webhook/
├── app.py                    # Flask webhook (porta 8940)
├── bruno_agent.py            # Bruno = persona ALMA + Claude CLI headless (assinatura)
├── ghl_client.py             # Wrappers GHL API (mensagens, contato, opportunities)
├── setup_custom_fields.py    # Script one-shot: cria os 24 campos sdr__ na Linkia
├── custom_fields.json        # Mapa {field_key: id} (gerado pelo setup)
├── torq_bruno.env            # Credenciais + config (Linkia token, pipeline IDs, model)
├── requirements.txt          # flask, requests, python-dotenv (sem anthropic!)
├── torq-sdr.service          # systemd unit (com HOME=/home/mia)
├── logs/                     # logs locais (rotacionado)
├── historico/                # backup conversas (JSON por contato)
└── tests/
    └── test_webhook.py       # 4 cenarios de teste local
```

## Setup (uma vez)

```bash
# 1. Garante que o CLI Claude Code esta instalado
which claude || npm i -g @anthropic-ai/claude-code

# 2. Autentica na assinatura (uma vez, como user mia)
sudo -u mia claude login

# 3. Cria venv (sem anthropic — nao precisa mais)
python3.12 -m venv /opt/mia/venvs/torq_sdr
/opt/mia/venvs/torq_sdr/bin/pip install -r requirements.txt

# 4. Cria os 24 campos custom sdr__ na Linkia Climb (uma vez)
/opt/mia/venvs/torq_sdr/bin/python setup_custom_fields.py

# 5. Instala systemd
sudo cp torq-sdr.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now torq-sdr.service

# 6. Verifica
sudo systemctl status torq-sdr.service
curl http://localhost:8940/health
```

## Config sensiveis do CLI no unit systemd

O `torq-sdr.service` DEVE ter:
```ini
Environment=HOME=/home/mia
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
```

Sem `HOME`, o CLI nao acha `~/.claude/.credentials.json` e falha na auth.
Sem `PATH`, `shutil.which("claude")` pode nao achar o binario dependendo do build do venv.

Se `ANTHROPIC_API_KEY` estiver setada no ambiente, o `bruno_agent.py` a REMOVE antes
de chamar o subprocess (pra forcar uso da assinatura).

## Payload esperado (GHL webhook)

```json
{
  "id": "MSG-abc123",
  "contactId": "ct_xyz789",
  "conversationId": "conv_def456",
  "type": "WhatsApp",
  "direction": "inbound",
  "body": "oi, queria saber sobre a bike",
  "firstName": "Marcos",
  "lastName": "Silva",
  "phone": "+5531988887777",
  "locationId": "ztCw0x5ghGgkA7kH3RQ2"
}
```

## Configurar no GHL/Linkia

1. Vai em **Automations** > cria workflow novo
2. Trigger: **Customer Replied** (filtrado por WhatsApp)
3. Action: **Webhook**
   - URL: `https://sdr-torq.agenciaclimb.com.br/webhook`
   - Method: POST
4. Ativa o workflow

## Reiniciar / logs

```bash
sudo systemctl status torq-sdr.service
sudo systemctl restart torq-sdr.service

tail -f /opt/mia/logs/sdr_bruno.log          # log da app
tail -f /opt/mia/logs/sdr_bruno_systemd.log  # stdout/stderr
sudo journalctl -u torq-sdr.service -f       # journal
```

## Testar localmente

Com o service rodando:
```bash
/opt/mia/venvs/torq_sdr/bin/python tests/test_webhook.py
```

Simula 4 msgs de um lead fake. O envio pro GHL vai falhar (contato nao existe),
mas o log tem que mostrar `claude_cli usage input=... cache_read=... output=...`
e o texto real do Bruno. Isso confirma que a assinatura ta autenticada e
funcional.

Pra testar so o agente (sem webhook nem GHL):
```bash
sudo -u mia HOME=/home/mia /opt/mia/venvs/torq_sdr/bin/python -c "
import sys; sys.path.insert(0, '/opt/mia/workspace/clientes/torq-brasil/sdr_bruno_webhook')
from bruno_agent import gerar_resposta
t, m = gerar_resposta('TEST', 'oi queria conhecer a bike', {'first_name':'Marcos','phone':'+553199'}, None, None)
print(t); print(m)
"
```

## Bloco META retornado pelo Bruno

Bruno adiciona no fim da resposta (quando ha atualizacoes):

```
Beleza Marcos, sabado 10h fechado. [[BREAK]] Me passa teu nome completo pro cadastro?

---SDR-META---
{"campos":{"sdr__nome_lead":"Marcos","sdr__cidade":"BH","sdr__uf":"MG","sdr__perfil":"b2c_urbano","sdr__modelo_interesse_principal":"whale","sdr__classificacao_lead":"quente","sdr__aceita_visita":"sim_bh"},"stage":"test_drive_agendado","notificar_renato":true}
---FIM---
```

O app.py separa o texto (envia pro lead) do META (aplica no GHL). O lead **nunca**
ve o bloco META — fica so nos logs e nas acoes internas.

## Prompt caching (via CLI)

O `--append-system-prompt` recebe o dossie ALMA completo (~52KB / ~13k tokens).
O CLI aplica cache automaticamente:
- 1a request: paga `cache_creation_input_tokens` (~30k tokens, ~$0.14)
- Requests seguintes em 5min: paga so `cache_read_input_tokens` a 0.1x (~$0.05 por resposta)
- Total pago via assinatura Max plan (nao ha cobranca variavel real)

Confira no log: `claude_cli usage input=X cache_read=Y cache_creation=Z output=W cost_usd=$`.

## Debounce

Se o lead manda 3 msgs consecutivas em 6s, o webhook agrupa tudo numa unica
chamada ao Bruno (evita spam de respostas). Ajustavel via `SDR_DEBOUNCE_SECONDS`.
Ajuda tambem a bufferizar durante latencia do CLI (10-20s por resposta).

## Stages do pipeline TESTE RAFA

| Bruno diz stage | Move pra                | ID GHL |
|-----------------|-------------------------|--------|
| `novo_lead`     | NOVO LEAD               | 6f0597e3-8390-4613-b49c-ff8a62a016e7 |
| `qualificado`   | QUALIFICADO             | 4608ef25-e9dc-46ed-835e-d299f7f4f3f3 |
| `test_drive_agendado` | TEST DRIVE AGENDADO | 94edcbaa-c6a6-483c-ba2a-01aa0af68739 |
| `proposta`      | PROPOSTA                | 9329b33e-8afc-4cf5-9f06-709959e97c58 |
| `perdido`       | PERDIDO                 | 5110cab1-0f2e-4b48-9c64-bfd545082eda |
| `ganhamos`      | GANHAMOS                | 52648474-b9f0-43b7-8bd3-3a84379f6b99 |
| `perdemos`      | PERDEMOS                | f3280b4b-bca1-4a6a-9c78-a0300b631ae3 |

Se a opportunity nao existir ainda, o webhook cria automaticamente no stage
NOVO LEAD e depois move pro stage novo.

## Modelo / latencia

- **Modelo:** `claude-sonnet-4-6` (via CLI --model)
- **Max tokens:** controlado pelo CLI (nao ha flag no headless, saida geralmente < 800 tokens)
- **Latencia tipica:** 10-20s por resposta (CLI + Sonnet)
- **Timeout do subprocess:** 120s (configuravel via `BRUNO_CLI_TIMEOUT`)

## Troubleshooting

**Sintoma:** log mostra `CLI retornou stdout vazio` ou `permission denied`.
**Causa:** auth do Claude Code expirada ou HOME errado.
**Fix:** `sudo -u mia claude login` e restart do service.

**Sintoma:** `CLI timeout apos 120s`.
**Causa:** Sonnet demorou demais (raro, adaptive thinking pesado).
**Fix:** aumentar `BRUNO_CLI_TIMEOUT` no env ou desativar thinking no futuro.

**Sintoma:** `CLI status error subtype=login_required`.
**Causa:** OAuth token da assinatura expirado.
**Fix:** `sudo -u mia claude login`.
