@danhachuel/leadmaster 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/backend/__pycache__/main.cpython-311.pyc +0 -0
  3. package/backend/main.py +50 -0
  4. package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
  6. package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
  7. package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
  8. package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
  9. package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
  10. package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
  11. package/backend/services/storage.py +44 -0
  12. package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
  13. package/backend/tests/__pycache__/test_agents_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  14. package/backend/tests/__pycache__/test_campaigns_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  15. package/backend/tests/__pycache__/test_cnpj_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  16. package/backend/tests/__pycache__/test_config.cpython-311-pytest-8.4.2.pyc +0 -0
  17. package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  18. package/backend/tests/__pycache__/test_instagram_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  19. package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
  20. package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  21. package/backend/tests/__pycache__/test_metrics.cpython-311-pytest-8.4.2.pyc +0 -0
  22. package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
  23. package/backend/tests/test_agents_routes.py +82 -0
  24. package/frontend/dist/assets/{index-ib1LXo9v.css → index-BBxljeDe.css} +1 -1
  25. package/frontend/dist/assets/index-ErHj9z-h.js +90 -0
  26. package/frontend/dist/index.html +2 -2
  27. package/package.json +1 -1
  28. package/frontend/dist/assets/index-CXuYUAoQ.js +0 -90
package/CHANGELOG.md CHANGED
@@ -37,6 +37,32 @@ normaliza para a forma canônica (ex.: `0.1.0`).
37
37
  falha se `process.stdin.resume();` desaparecer, se `.unref()` aparecer, se o
38
38
  ciclo de vida vinculado for removido ou se os handlers globais sumirem.
39
39
 
40
+ ## [0.1.20] — 2026-11-09
41
+
42
+ ### Adicionado
43
+
44
+ - **Módulo de Agentes de IA (controle e configuração):**
45
+ - Backend: `GET /api/agents` (status + prompt de cada agente, e flag
46
+ `llm_configurado`), `PUT /api/agents/{agent_id}/status` (ativo/inativo) e
47
+ `PUT /api/agents/{agent_id}/prompt` — persistência no novo arquivo
48
+ `storage/agents.json` (seed com prompts padrão em pt-BR).
49
+ - **Regra de bloqueio:** ativar o **Agente de Disparo** sem a chave do LLM
50
+ configurada retorna **400** com mensagem acionável ("Configure a API Key
51
+ do LLM em Ferramentas..."); desativar e o Atendimento são sempre
52
+ permitidos.
53
+ - Frontend: `frontend/src/pages/Agentes.tsx` reformulado em **dois cards
54
+ lado a lado** (Disparo / Atendimento): badge de status, **toggle grande**
55
+ (w-16 h-9 — Fitts), **pulso animado verde-esmeralda** e badge esmeralda
56
+ quando ativo (von Restorff + Zeigarnik), textarea do **Prompt do Sistema**
57
+ + botão "Salvar Prompt", toast específico ("Configure a API Key na aba
58
+ Ferramentas primeiro.") quando o bloqueio 400 ocorre, e **accordion** com
59
+ as configurações avançadas (modelo LLM/temperatura; modo texto/voz do
60
+ atendimento) — Lei de Hick.
61
+ - **8 novos testes pytest** (`backend/tests/test_agents_routes.py`): listagem,
62
+ bloqueio do disparo sem LLM (400), ativação do atendimento, ativação do
63
+ disparo com LLM, desativação, status inválido/agente desconhecido (400/404),
64
+ persistência do prompt e erros de prompt.
65
+
40
66
  ## [0.1.19] — 2026-11-09
41
67
 
42
68
  ### Adicionado
package/backend/main.py CHANGED
@@ -552,6 +552,56 @@ def buscar_cnpj(dados: CnpjSearchIn):
552
552
  raise HTTPException(502, str(e))
553
553
 
554
554
 
555
+ # ---------------------------------------------------------------------------
556
+ # Agentes de IA (status + prompt do sistema)
557
+ # ---------------------------------------------------------------------------
558
+
559
+ @app.get("/api/agents")
560
+ def listar_agentes():
561
+ """Status e prompt de cada agente (disparo / atendimento)."""
562
+ agentes = storage.get_agents()
563
+ llm_ok = llm.configurado()
564
+ return {
565
+ "llm_configurado": llm_ok,
566
+ "agentes": {
567
+ agent_id: {
568
+ "status": agentes.get(agent_id, {}).get("status", "inativo"),
569
+ "prompt": agentes.get(agent_id, {}).get("prompt", ""),
570
+ }
571
+ for agent_id in storage.AGENTE_IDS
572
+ },
573
+ }
574
+
575
+
576
+ @app.put("/api/agents/{agent_id}/status")
577
+ def alterar_status_agente(agent_id: str, dados: dict):
578
+ """Liga/desliga um agente. Ativar o de disparo exige LLM configurada."""
579
+ if agent_id not in storage.AGENTE_IDS:
580
+ raise HTTPException(404, f"Agente desconhecido: {agent_id}. Use: disparo ou atendimento.")
581
+ status = (dados.get("status") or "").strip().lower()
582
+ if status not in ("ativo", "inativo"):
583
+ raise HTTPException(400, 'Status inválido. Use: ativo ou inativo.')
584
+ if status == "ativo" and agent_id == "disparo" and not llm.configurado():
585
+ raise HTTPException(
586
+ 400,
587
+ "Configure a API Key do LLM em Ferramentas antes de ativar o Agente de Disparo.",
588
+ )
589
+ agente = storage.save_agent(agent_id, {"status": status})
590
+ return {"ok": True, "agent_id": agent_id, "status": agente["status"]}
591
+
592
+
593
+ @app.put("/api/agents/{agent_id}/prompt")
594
+ def salvar_prompt_agente(agent_id: str, dados: dict):
595
+ """Salva o prompt do sistema do agente em agents.json."""
596
+ if agent_id not in storage.AGENTE_IDS:
597
+ raise HTTPException(404, f"Agente desconhecido: {agent_id}. Use: disparo ou atendimento.")
598
+ prompt = dados.get("prompt")
599
+ if prompt is None:
600
+ raise HTTPException(400, "Informe o prompt do sistema.")
601
+ agente = storage.save_agent(agent_id, {"prompt": str(prompt)})
602
+ return {"ok": True, "agent_id": agent_id, "prompt": agente["prompt"]}
603
+
604
+
555
605
  # ---------------------------------------------------------------------------
556
606
  # Configuração e integrações
557
607
  # ---------------------------------------------------------------------------
@@ -20,6 +20,7 @@ LEADS_FILE = "leads.json"
20
20
  CAMPAIGNS_FILE = "campaigns.json"
21
21
  PIPELINE_FILE = "pipeline.json"
22
22
  CONFIG_FILE = "config.json"
23
+ AGENTS_FILE = "agents.json"
23
24
 
24
25
 
25
26
  # ---------------------------------------------------------------------------
@@ -352,6 +353,47 @@ def delete_campaign(campaign_id: str) -> bool:
352
353
  return True
353
354
 
354
355
 
356
+ # ---------------------------------------------------------------------------
357
+ # Agentes de IA (status e prompt do sistema)
358
+ # ---------------------------------------------------------------------------
359
+
360
+ DEFAULT_AGENTS = {
361
+ "disparo": {
362
+ "status": "inativo",
363
+ "prompt": (
364
+ "Você é um especialista em prospecção consultiva. Escreva mensagens curtas, "
365
+ "educadas e personalizadas, sempre citando a empresa do lead. Nunca use "
366
+ "linguagem agressiva ou spam."
367
+ ),
368
+ },
369
+ "atendimento": {
370
+ "status": "inativo",
371
+ "prompt": (
372
+ "Você é o assistente de atendimento do LeadMaster. Responda de forma clara e "
373
+ "objetiva em português do Brasil, encaminhe dúvidas comerciais para o "
374
+ "responsável quando necessário."
375
+ ),
376
+ },
377
+ }
378
+
379
+ AGENTE_IDS = ("disparo", "atendimento")
380
+
381
+
382
+ def get_agents() -> dict:
383
+ return _deep_merge(DEFAULT_AGENTS, _read(AGENTS_FILE, {}))
384
+
385
+
386
+ def save_agent(agent_id: str, dados: dict) -> dict:
387
+ """Atualiza campos de um agente (status/prompt) e persiste em agents.json."""
388
+ with LOCK:
389
+ agents = get_agents()
390
+ if agent_id not in agents:
391
+ agents[agent_id] = {"status": "inativo", "prompt": ""}
392
+ agents[agent_id].update({k: v for k, v in dados.items() if k in ("status", "prompt")})
393
+ _write(AGENTS_FILE, agents)
394
+ return agents[agent_id]
395
+
396
+
355
397
  # ---------------------------------------------------------------------------
356
398
  # Seeds (primeira execução)
357
399
  # ---------------------------------------------------------------------------
@@ -433,6 +475,8 @@ def ensure_seeds() -> None:
433
475
  _write(CAMPAIGNS_FILE, _seed_campaigns())
434
476
  if not (d / CONFIG_FILE).exists():
435
477
  _write(CONFIG_FILE, DEFAULT_CONFIG)
478
+ if not (d / AGENTS_FILE).exists():
479
+ _write(AGENTS_FILE, DEFAULT_AGENTS)
436
480
 
437
481
 
438
482
  def limpar_seeds_demo() -> None:
@@ -0,0 +1,82 @@
1
+ """Testes do controle dos Agentes de IA (backend/main.py).
2
+
3
+ GET /api/agents, PUT /api/agents/{id}/status (com bloqueio do disparo sem
4
+ LLM configurada) e PUT /api/agents/{id}/prompt. Home isolado em tmp.
5
+ """
6
+
7
+ import pytest
8
+ from fastapi.testclient import TestClient
9
+
10
+ import main
11
+
12
+
13
+ @pytest.fixture()
14
+ def client(tmp_path, monkeypatch):
15
+ monkeypatch.setenv("LEADMASTER_HOME", str(tmp_path))
16
+ with TestClient(main.app) as c:
17
+ yield c
18
+
19
+
20
+ def test_listar_agentes(client):
21
+ r = client.get("/api/agents")
22
+ assert r.status_code == 200
23
+ corpo = r.json()
24
+ assert "disparo" in corpo["agentes"] and "atendimento" in corpo["agentes"]
25
+ assert corpo["agentes"]["disparo"]["status"] == "inativo"
26
+ assert corpo["llm_configurado"] is False # seed sem chave de LLM
27
+
28
+
29
+ def test_ativar_disparo_sem_llm_bloqueia(client):
30
+ r = client.put("/api/agents/disparo/status", json={"status": "ativo"})
31
+ assert r.status_code == 400
32
+ assert "API Key" in r.json()["detail"]
33
+ # Continua inativo
34
+ estado = client.get("/api/agents").json()["agentes"]["disparo"]["status"]
35
+ assert estado == "inativo"
36
+
37
+
38
+ def test_ativar_atendimento_permitido_sem_llm(client):
39
+ r = client.put("/api/agents/atendimento/status", json={"status": "ativo"})
40
+ assert r.status_code == 200
41
+ assert r.json()["status"] == "ativo"
42
+ # Persistido em agents.json
43
+ estado = client.get("/api/agents").json()["agentes"]["atendimento"]["status"]
44
+ assert estado == "ativo"
45
+
46
+
47
+ def test_ativar_disparo_com_llm(client):
48
+ # Configura a chave do LLM e ativa o disparo.
49
+ client.put("/api/config", json={"llm": {"api_key": "sk-teste-123"}})
50
+ r = client.put("/api/agents/disparo/status", json={"status": "ativo"})
51
+ assert r.status_code == 200
52
+ assert r.json()["status"] == "ativo"
53
+
54
+
55
+ def test_desativar_agente(client):
56
+ client.put("/api/agents/atendimento/status", json={"status": "ativo"})
57
+ r = client.put("/api/agents/atendimento/status", json={"status": "inativo"})
58
+ assert r.status_code == 200
59
+ assert r.json()["status"] == "inativo"
60
+
61
+
62
+ def test_status_invalido_e_agente_desconhecido(client):
63
+ r = client.put("/api/agents/disparo/status", json={"status": "talvez"})
64
+ assert r.status_code == 400
65
+ r2 = client.put("/api/agents/inexistente/status", json={"status": "ativo"})
66
+ assert r2.status_code == 404
67
+
68
+
69
+ def test_salvar_prompt_persiste(client):
70
+ prompt = "Você é um agente de prospecção especialista em SaaS."
71
+ r = client.put("/api/agents/disparo/prompt", json={"prompt": prompt})
72
+ assert r.status_code == 200
73
+ assert r.json()["prompt"] == prompt
74
+ salvo = client.get("/api/agents").json()["agentes"]["disparo"]["prompt"]
75
+ assert salvo == prompt
76
+
77
+
78
+ def test_salvar_prompt_agente_desconhecido(client):
79
+ r = client.put("/api/agents/inexistente/prompt", json={"prompt": "x"})
80
+ assert r.status_code == 404
81
+ r2 = client.put("/api/agents/disparo/prompt", json={})
82
+ assert r2.status_code == 400