@innvisor/conny-ai 9.7.0

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 (175) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +369 -0
  5. package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
  6. package/brand-assets/Conny.web.logo.png +0 -0
  7. package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
  8. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
  9. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
  10. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
  11. package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
  12. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
  13. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
  14. package/brand-assets/conny-demo/manifest.json +22 -0
  15. package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
  16. package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
  17. package/brand-assets/conny-logo.png +0 -0
  18. package/brand-assets/web.background.png +0 -0
  19. package/brand_assets.py +323 -0
  20. package/conny +28 -0
  21. package/conny-chat.py +579 -0
  22. package/conny-omni.py +3843 -0
  23. package/conny.py +113 -0
  24. package/conny_agents/__init__.py +1 -0
  25. package/conny_agents/agenda.py +1 -0
  26. package/conny_agents/captacion.py +1 -0
  27. package/conny_agents/conocimiento.py +1 -0
  28. package/conny_agents/escalacion.py +1 -0
  29. package/conny_agents/objeciones.py +1 -0
  30. package/conny_agents/seguimiento.py +1 -0
  31. package/conny_app.py +287 -0
  32. package/conny_audio.py +350 -0
  33. package/conny_audio_learn.py +84 -0
  34. package/conny_brain_v10.py +804 -0
  35. package/conny_bridge.py +656 -0
  36. package/conny_calendar.py +169 -0
  37. package/conny_cli.py +11784 -0
  38. package/conny_cli_bb.py +437 -0
  39. package/conny_commands.py +243 -0
  40. package/conny_config.py +215 -0
  41. package/conny_core/__init__.py +3 -0
  42. package/conny_core/conversation_engine.py +446 -0
  43. package/conny_core/first_turn_ops.py +287 -0
  44. package/conny_core/persona_registry.py +157 -0
  45. package/conny_core/prompt_ops.py +561 -0
  46. package/conny_cron.py +72 -0
  47. package/conny_demo_v2.py +209 -0
  48. package/conny_demo_voice.py +134 -0
  49. package/conny_design.py +43 -0
  50. package/conny_doctor.py +319 -0
  51. package/conny_domino.py +696 -0
  52. package/conny_generator.py +447 -0
  53. package/conny_google_auth.py +159 -0
  54. package/conny_i18n.py +619 -0
  55. package/conny_init.py +509 -0
  56. package/conny_integrations/__init__.py +4 -0
  57. package/conny_integrations/llm.py +1 -0
  58. package/conny_integrations/vault.py +77 -0
  59. package/conny_integrations/whatsapp.py +1 -0
  60. package/conny_intelligence.py +65 -0
  61. package/conny_learning.py +154 -0
  62. package/conny_memory.py +243 -0
  63. package/conny_memory_engine.py +292 -0
  64. package/conny_nova_proxy.py +170 -0
  65. package/conny_nuke_robot_phrases.py +493 -0
  66. package/conny_pairing.py +253 -0
  67. package/conny_patch.py +291 -0
  68. package/conny_persona_cli.py +150 -0
  69. package/conny_router.py +308 -0
  70. package/conny_runtime_ops.py +271 -0
  71. package/conny_session.py +516 -0
  72. package/conny_skills/__init__.py +1 -0
  73. package/conny_skills/demo_mode.py +35 -0
  74. package/conny_skills/text_processing.py +1 -0
  75. package/conny_skills/tone_detection.py +1 -0
  76. package/conny_smart_features.py +333 -0
  77. package/conny_studio.py +161 -0
  78. package/conny_sync_fix.py +306 -0
  79. package/conny_tui.py +512 -0
  80. package/conny_tui_select.py +202 -0
  81. package/conny_ultra_config.py +411 -0
  82. package/conny_uncertainty.py +174 -0
  83. package/conny_utils.py +87 -0
  84. package/conny_voice.py +156 -0
  85. package/conny_voice_engine.py +124 -0
  86. package/conny_web_search.py +66 -0
  87. package/conny_weekly_report.py +85 -0
  88. package/conny_worm.py +88 -0
  89. package/core/__init__.py +25 -0
  90. package/ecosystem.config.js +24 -0
  91. package/fix_init.py +27 -0
  92. package/install.sh +78 -0
  93. package/knowledge_base.py +330 -0
  94. package/nova/rules/default.yaml +37 -0
  95. package/nova_bridge.py +509 -0
  96. package/npm/conny.js +471 -0
  97. package/package.json +102 -0
  98. package/personas/conny/base/default.yaml +35 -0
  99. package/personas/conny/base/estetica_whatsapp.yaml +36 -0
  100. package/requirements.txt +14 -0
  101. package/run.sh +47 -0
  102. package/search.py +465 -0
  103. package/smart_handoff.py +1150 -0
  104. package/src/__init__.py +0 -0
  105. package/src/conny/__init__.py +0 -0
  106. package/src/conny/admin/__init__.py +0 -0
  107. package/src/conny/admin/api.py +234 -0
  108. package/src/conny/admin/dashboard.py +772 -0
  109. package/src/conny/api/__init__.py +0 -0
  110. package/src/conny/api/routes.py +8851 -0
  111. package/src/conny/brain/__init__.py +15 -0
  112. package/src/conny/brain/engine.py +804 -0
  113. package/src/conny/brain/learning.py +154 -0
  114. package/src/conny/brain/memory.py +324 -0
  115. package/src/conny/brain/smart_features.py +333 -0
  116. package/src/conny/brain/uncertainty.py +167 -0
  117. package/src/conny/channels/__init__.py +0 -0
  118. package/src/conny/channels/audio.py +316 -0
  119. package/src/conny/channels/cli.py +11795 -0
  120. package/src/conny/channels/logo_art.py +11 -0
  121. package/src/conny/channels/voice.py +156 -0
  122. package/src/conny/core/__init__.py +0 -0
  123. package/src/conny/core/config.py +215 -0
  124. package/src/conny/core/cron.py +72 -0
  125. package/src/conny/core/messenger.py +563 -0
  126. package/src/conny/core/router.py +297 -0
  127. package/src/conny/core/session.py +312 -0
  128. package/src/conny/demo/__init__.py +0 -0
  129. package/src/conny/demo/handler.py +3110 -0
  130. package/src/conny/integrations/__init__.py +19 -0
  131. package/src/conny/integrations/calendar.py +169 -0
  132. package/src/conny/integrations/knowledge.py +312 -0
  133. package/src/conny/integrations/search.py +66 -0
  134. package/src/conny/personas/__init__.py +0 -0
  135. package/src/conny/personas/generator.py +447 -0
  136. package/src/conny/production/__init__.py +0 -0
  137. package/src/conny/production/domino.py +696 -0
  138. package/src/conny/production/guard.py +550 -0
  139. package/src/conny/production/handoff.py +1150 -0
  140. package/src/conny/production/monitor.py +353 -0
  141. package/src/conny/utils/__init__.py +2 -0
  142. package/src/conny/utils/helpers.py +75 -0
  143. package/src/conny/utils/i18n.py +619 -0
  144. package/src/core/admin_engines.py +772 -0
  145. package/src/core/globals.py +11845 -0
  146. package/src/core/orchestrator.py +273 -0
  147. package/src/core/production_monitor.py +353 -0
  148. package/src/core/runtime.py +5487 -0
  149. package/src/domain/onboarding_flow.py +230 -0
  150. package/src/domain/prompts/__init__.py +1 -0
  151. package/src/domain/prompts/prospect_pitch.py +282 -0
  152. package/src/domain/send_guard.py +636 -0
  153. package/src/domain/swarm/queen.py +96 -0
  154. package/src/infrastructure/llm_providers/engine.py +487 -0
  155. package/src/interfaces/mcp_server.py +73 -0
  156. package/src/interfaces/nova_bridge.py +58 -0
  157. package/src/interfaces/web/admin_api.py +1379 -0
  158. package/src/interfaces/web/app.py +9408 -0
  159. package/src/interfaces/web/demo_handler.py +3450 -0
  160. package/src/interfaces/web/static/generate_avatars.py +46 -0
  161. package/v7/__init__.py +46 -0
  162. package/v7/agents/__init__.py +46 -0
  163. package/v7/agents/agenda.py +77 -0
  164. package/v7/agents/base.py +216 -0
  165. package/v7/agents/captacion.py +60 -0
  166. package/v7/agents/conocimiento.py +69 -0
  167. package/v7/agents/escalacion.py +83 -0
  168. package/v7/agents/objeciones.py +109 -0
  169. package/v7/agents/seguimiento.py +71 -0
  170. package/v7/memory/__init__.py +46 -0
  171. package/v7/memory/patient_profile.py +200 -0
  172. package/v7/orchestrator.py +275 -0
  173. package/v7/postprocess.py +127 -0
  174. package/v7/router.py +239 -0
  175. package/verify_conversation_impl.py +48 -0
@@ -0,0 +1,46 @@
1
+ import os
2
+ import urllib.request
3
+ import time
4
+
5
+ def download_notionists_avatars(output_dir):
6
+ os.makedirs(output_dir, exist_ok=True)
7
+
8
+ # 30 different name seeds to get distinct premium characters
9
+ seeds = [
10
+ "Ane", "Bob", "Clara", "David", "Eva", "Felix", "Grace", "Hugo",
11
+ "Iris", "Jack", "Kim", "Leo", "Maya", "Nico", "Olivia", "Paul",
12
+ "Quinn", "Ruby", "Sam", "Tess", "Uli", "Val", "Will", "Xena",
13
+ "Yuri", "Zoe", "Amelie", "Bruno", "Chloe", "Dan"
14
+ ]
15
+
16
+ # Soft pastel background colors that fit the dark theme nicely
17
+ backgrounds = [
18
+ "c0aade", "b3c5fc", "fca3b7", "fcd2a3", "a3fcd6", "a3edf8",
19
+ "e2e8f0", "cbd5e1", "f1f5f9", "ffd2e2", "d2ffeb", "ffe3d2"
20
+ ]
21
+
22
+ for i, seed in enumerate(seeds, 1):
23
+ num = f"{i:02d}"
24
+ bg = backgrounds[i % len(backgrounds)]
25
+ url = f"https://api.dicebear.com/7.x/notionists/svg?seed={seed}&backgroundColor={bg}"
26
+ file_path = os.path.join(output_dir, f"avatar_{num}.svg")
27
+
28
+ try:
29
+ print(f"Downloading avatar {num} (seed: {seed})...")
30
+ # Set a User-Agent to avoid HTTP 403 Forbidden errors
31
+ req = urllib.request.Request(
32
+ url,
33
+ headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
34
+ )
35
+ with urllib.request.urlopen(req, timeout=10) as response:
36
+ svg_data = response.read().decode('utf-8')
37
+ with open(file_path, "w", encoding="utf-8") as f:
38
+ f.write(svg_data)
39
+ time.sleep(0.2) # Soft delay to be polite to the server
40
+ except Exception as e:
41
+ print(f"Error downloading avatar {num}: {e}")
42
+
43
+ if __name__ == '__main__':
44
+ output_dir = '/home/ubuntu/conny/src/interfaces/web/static/avatars'
45
+ download_notionists_avatars(output_dir)
46
+ print("All 30 premium Notionist avatars downloaded successfully!")
package/v7/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """
2
+ Conny V7.0 — Registry de Agentes
3
+ =====================================
4
+ Registra todos los agentes disponibles.
5
+ El orquestador los carga una sola vez al arranque.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from typing import Dict, Optional, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from v7.agents.base import AgentBase
13
+
14
+ from v7.agents.captacion import AgenteCaptacion
15
+ from v7.agents.objeciones import AgenteObjeciones
16
+ from v7.agents.agenda import AgenteAgenda
17
+ from v7.agents.conocimiento import AgenteConocimiento
18
+ from v7.agents.seguimiento import AgenteSeguimiento
19
+ from v7.agents.escalacion import AgenteEscalacion
20
+ from v7.router import AgentID
21
+
22
+
23
+ def build_registry(llm_engine) -> Dict[str, "AgentBase"]:
24
+ """
25
+ Construye el diccionario de agentes instanciados.
26
+ Llamar UNA SOLA VEZ al arranque, pasar a ConnyUltra.
27
+ """
28
+ return {
29
+ AgentID.CAPTACION: AgenteCaptacion(llm_engine),
30
+ AgentID.OBJECIONES: AgenteObjeciones(llm_engine),
31
+ AgentID.AGENDA: AgenteAgenda(llm_engine),
32
+ AgentID.CONOCIMIENTO: AgenteConocimiento(llm_engine),
33
+ AgentID.SEGUIMIENTO: AgenteSeguimiento(llm_engine),
34
+ AgentID.ESCALACION: AgenteEscalacion(llm_engine),
35
+ }
36
+
37
+
38
+ __all__ = [
39
+ "build_registry",
40
+ "AgenteCaptacion",
41
+ "AgenteObjeciones",
42
+ "AgenteAgenda",
43
+ "AgenteConocimiento",
44
+ "AgenteSeguimiento",
45
+ "AgenteEscalacion",
46
+ ]
@@ -0,0 +1,46 @@
1
+ """
2
+ Conny V7.0 — Registry de Agentes
3
+ =====================================
4
+ Registra todos los agentes disponibles.
5
+ El orquestador los carga una sola vez al arranque.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from typing import Dict, Optional, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from v7.agents.base import AgentBase
13
+
14
+ from v7.agents.captacion import AgenteCaptacion
15
+ from v7.agents.objeciones import AgenteObjeciones
16
+ from v7.agents.agenda import AgenteAgenda
17
+ from v7.agents.conocimiento import AgenteConocimiento
18
+ from v7.agents.seguimiento import AgenteSeguimiento
19
+ from v7.agents.escalacion import AgenteEscalacion
20
+ from v7.router import AgentID
21
+
22
+
23
+ def build_registry(llm_engine) -> Dict[str, "AgentBase"]:
24
+ """
25
+ Construye el diccionario de agentes instanciados.
26
+ Llamar UNA SOLA VEZ al arranque, pasar a ConnyUltra.
27
+ """
28
+ return {
29
+ AgentID.CAPTACION: AgenteCaptacion(llm_engine),
30
+ AgentID.OBJECIONES: AgenteObjeciones(llm_engine),
31
+ AgentID.AGENDA: AgenteAgenda(llm_engine),
32
+ AgentID.CONOCIMIENTO: AgenteConocimiento(llm_engine),
33
+ AgentID.SEGUIMIENTO: AgenteSeguimiento(llm_engine),
34
+ AgentID.ESCALACION: AgenteEscalacion(llm_engine),
35
+ }
36
+
37
+
38
+ __all__ = [
39
+ "build_registry",
40
+ "AgenteCaptacion",
41
+ "AgenteObjeciones",
42
+ "AgenteAgenda",
43
+ "AgenteConocimiento",
44
+ "AgenteSeguimiento",
45
+ "AgenteEscalacion",
46
+ ]
@@ -0,0 +1,77 @@
1
+ """
2
+ Conny V7.0 — Agente de Agenda
3
+ ==================================
4
+ Gestiona la valoración/cita. Propone UN día concreto.
5
+ Nunca inventa disponibilidad. Cierra hacia el micro-compromiso.
6
+ Prompt: ~320 tokens.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ import re
11
+ from v7.agents.base import AgentBase, AgentContext, AgentResponse
12
+
13
+
14
+ class AgenteAgenda(AgentBase):
15
+
16
+ agent_id = "agenda"
17
+ context_keys = ["patient_name", "funnel_state", "ultima_cita",
18
+ "calendar_available", "clinic_tone"]
19
+
20
+ def _build_prompt(self, ctx: AgentContext) -> str:
21
+ calendar = ctx.calendar_info or ""
22
+ tiene_calendar = bool(calendar and "disponible" in calendar.lower())
23
+ usted = ctx.usted
24
+
25
+ if tiene_calendar:
26
+ disponibilidad_bloque = f"DISPONIBILIDAD REAL:\n{calendar}"
27
+ else:
28
+ disponibilidad_bloque = (
29
+ "No tienes acceso al calendario real. "
30
+ "Propón el próximo jueves como día concreto. "
31
+ "Si el paciente no puede, negocia. "
32
+ "Nunca digas 'cuando puedas' ni 'el día que prefieras'."
33
+ )
34
+
35
+ return f"""Eres la persona que contesta el WhatsApp de {ctx.clinic_name or "la clínica"}.
36
+ {self._tone_header(ctx)}
37
+
38
+ MISIÓN: agendar la valoración. La valoración es el producto, no el procedimiento.
39
+
40
+ LA VALORACIÓN ES:
41
+ Gratis. 20 minutos. Con la doctora directamente.
42
+ Sin compromiso — la doctora evalúa y te dice con honestidad qué aplica.
43
+
44
+ CÓMO CERRAR:
45
+ 1. Propón UN día concreto — "esta semana tienes el jueves, te queda bien"
46
+ 2. NUNCA dos opciones: "jueves o viernes" suena a indecisión
47
+ 3. Si rechaza ese día → negocia hacia otro día, no listes toda la semana
48
+ 4. Si acepta → pide nombre y confirma: "perfecto, {ctx.patient_name or 'te anoto'} para el jueves"
49
+ 5. Si ya tiene cita → confirmar o reagendar con calidez
50
+
51
+ NUNCA INVENTAR DISPONIBILIDAD:
52
+ Si no tienes el calendario real, di "te confirmo" en vez de inventar horarios.
53
+ "esta semana tenemos espacio, déjame confirmarte el jueves a qué hora puedes"
54
+
55
+ {disponibilidad_bloque}
56
+
57
+ TONO DE CIERRE:
58
+ {"Le agendamos la valoración. ¿Le queda bien este jueves?" if usted else "te agendo la valoración, esta semana tienes el jueves, te queda bien"}
59
+
60
+ {self._writing_rules()}
61
+ {self._patient_block(ctx)}"""
62
+
63
+ def _parse_response(self, raw: str, ctx: AgentContext) -> AgentResponse:
64
+ from v7.postprocess import postprocess, split_bubbles
65
+ clean = postprocess(raw, is_premium=ctx.is_premium)
66
+ bubbles = split_bubbles(clean)
67
+
68
+ # Detectar si se confirmó una cita en la respuesta
69
+ cita_confirmada = bool(re.search(
70
+ r"\b(agend[oé]|confirm[oé]|anot[oé]|queda para|quedaste para)\b",
71
+ raw, re.IGNORECASE
72
+ ))
73
+
74
+ return AgentResponse(
75
+ bubbles=bubbles or [clean],
76
+ funnel_update="cita_agendada" if cita_confirmada else None,
77
+ )
@@ -0,0 +1,216 @@
1
+ """
2
+ Conny V7.0 — AgentBase
3
+ =========================
4
+ Contrato que todos los agentes implementan.
5
+ El orquestador siempre llama agent.run(ctx) sin saber qué agente es.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ import logging
10
+ import time
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass, field
13
+ from typing import Dict, List, Optional, Any
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ # ── Contexto de entrada ───────────────────────────────────────────────────────
19
+
20
+ @dataclass
21
+ class AgentContext:
22
+ """
23
+ Todo lo que un agente necesita para generar su respuesta.
24
+ El orquestador ensambla esto desde la memoria ANTES de llamar al agente,
25
+ pasando SOLO los campos que el agente declaró necesitar.
26
+ """
27
+ # Mensaje actual
28
+ chat_id: str
29
+ text: str
30
+ platform: str = "whatsapp"
31
+
32
+ # Perfil del paciente (solo campos solicitados)
33
+ patient_summary: str = "" # texto compacto del perfil
34
+ funnel_stage: str = "primer_contacto"
35
+ patient_name: Optional[str] = None
36
+ visits: int = 0
37
+ objeciones_pasadas: List[str] = field(default_factory=list)
38
+
39
+ # Clínica
40
+ clinic_name: str = ""
41
+ clinic_tone: str = "SALUD" # SALUD | SALUD PREMIUM | GENERAL | RETAIL
42
+ clinic_kb: str = "" # fragmento relevante de KB
43
+ servicios: str = ""
44
+ precios: str = ""
45
+ horarios: str = ""
46
+
47
+ # Historial reciente (últimos N mensajes para coherencia)
48
+ history: List[Dict] = field(default_factory=list)
49
+
50
+ # Contexto adicional
51
+ search_context: str = ""
52
+ calendar_info: str = ""
53
+ metadata: Dict[str, Any] = field(default_factory=dict)
54
+
55
+ @property
56
+ def is_premium(self) -> bool:
57
+ return self.clinic_tone in ("SALUD PREMIUM", "PREMIUM")
58
+
59
+ @property
60
+ def usted(self) -> bool:
61
+ return self.is_premium
62
+
63
+ def greeting_name(self) -> str:
64
+ return self.patient_name or ""
65
+
66
+ @property
67
+ def funnel_state(self) -> str:
68
+ return self.funnel_stage
69
+
70
+ @property
71
+ def servicios_clinica(self) -> str:
72
+ return self.servicios
73
+
74
+ @property
75
+ def clinic_kb_excerpt(self) -> str:
76
+ return self.clinic_kb
77
+
78
+ @property
79
+ def calendar_available(self) -> bool:
80
+ return bool(self.calendar_info)
81
+
82
+
83
+ # ── Respuesta del agente ──────────────────────────────────────────────────────
84
+
85
+ @dataclass
86
+ class AgentResponse:
87
+ """
88
+ Lo que el agente devuelve al orquestador.
89
+ """
90
+ bubbles: List[str] # burbujas ya listas para enviar
91
+ next_agent: Optional[str] = None # si el agente quiere escalar a otro
92
+ funnel_update: Optional[str] = None # nuevo estado del funnel (si cambió)
93
+ new_objecion: Optional[str] = None # objeción detectada para registrar
94
+ learned_name: Optional[str] = None # nombre detectado en el mensaje
95
+ learned_zona: Optional[str] = None # zona de interés detectada
96
+ confidence: float = 1.0
97
+ latency_ms: float = 0.0
98
+ agent_id: str = "unknown"
99
+
100
+ @property
101
+ def text(self) -> str:
102
+ return " ||| ".join(self.bubbles)
103
+
104
+
105
+ # ── Clase base ────────────────────────────────────────────────────────────────
106
+
107
+ class AgentBase(ABC):
108
+ """
109
+ Todos los agentes heredan de aquí.
110
+ Implementar solo: agent_id, context_keys, _build_prompt, y opcionalmente _parse_response.
111
+ """
112
+
113
+ agent_id: str # identificador único
114
+ context_keys: List[str] # qué campos de AgentContext necesita
115
+
116
+ # Máximo de tokens que este agente puede usar en su prompt de sistema
117
+ max_system_tokens: int = 600
118
+
119
+ def __init__(self, llm_engine):
120
+ self._llm = llm_engine
121
+
122
+ async def run(self, ctx: AgentContext) -> AgentResponse:
123
+ """
124
+ Punto de entrada único. El orquestador siempre llama esto.
125
+ No se sobreescribe (salvo casos muy específicos).
126
+ """
127
+ t0 = time.perf_counter()
128
+ try:
129
+ system_prompt = self._build_prompt(ctx)
130
+ messages = self._build_messages(ctx, system_prompt)
131
+ raw, meta = await self._llm.complete(
132
+ messages,
133
+ model_tier="fast",
134
+ temperature=0.82,
135
+ max_tokens=200, # agentes responden corto
136
+ )
137
+ log.info(
138
+ "[%s] %s provider=%s model=%s",
139
+ self.agent_id,
140
+ ctx.chat_id[:8],
141
+ meta.get("provider", "?"),
142
+ meta.get("model", "?")[:25],
143
+ )
144
+ response = self._parse_response(raw, ctx)
145
+ response.latency_ms = (time.perf_counter() - t0) * 1000
146
+ response.agent_id = self.agent_id
147
+ return response
148
+ except Exception as e:
149
+ log.error("[%s] error: %s", self.agent_id, e, exc_info=True)
150
+ return AgentResponse(
151
+ bubbles=["un momento, déjame revisar eso"],
152
+ agent_id=self.agent_id,
153
+ latency_ms=(time.perf_counter() - t0) * 1000,
154
+ )
155
+
156
+ @abstractmethod
157
+ def _build_prompt(self, ctx: AgentContext) -> str:
158
+ """Construye el system prompt especializado para este agente."""
159
+
160
+ def _build_messages(self, ctx: AgentContext, system_prompt: str) -> List[Dict]:
161
+ """Arma el array de mensajes para el LLM."""
162
+ messages = [{"role": "system", "content": system_prompt}]
163
+ # Últimos 8 turnos de historial para coherencia
164
+ for turn in ctx.history[-8:]:
165
+ messages.append({
166
+ "role": turn.get("role", "user"),
167
+ "content": turn.get("content", ""),
168
+ })
169
+ messages.append({"role": "user", "content": ctx.text})
170
+ return messages
171
+
172
+ def _parse_response(self, raw: str, ctx: AgentContext) -> AgentResponse:
173
+ """
174
+ Post-procesa la respuesta cruda del LLM.
175
+ Los agentes pueden sobreescribir esto para extraer acciones (citas, nombres, etc.).
176
+ """
177
+ from v7.postprocess import postprocess
178
+ clean = postprocess(raw, is_premium=ctx.is_premium)
179
+ bubbles = [b.strip() for b in clean.split("|||") if b.strip()]
180
+ return AgentResponse(bubbles=bubbles or [clean])
181
+
182
+ # ── Helpers comunes ────────────────────────────────────────────────────────
183
+
184
+ def _tone_header(self, ctx: AgentContext) -> str:
185
+ """Instrucción de tono según el tipo de clínica."""
186
+ if ctx.clinic_tone == "SALUD PREMIUM":
187
+ return (
188
+ "Tono: Usted. Profesional y cálido. Primera letra mayúscula. "
189
+ "Sin tuteo. Identifica la clínica, no a ti misma."
190
+ )
191
+ elif ctx.clinic_tone == "SALUD":
192
+ return "Tono: tuteo natural, cálido, colombiano. Como la recepcionista de siempre."
193
+ else:
194
+ return "Tono: cercano, directo, colombiano real."
195
+
196
+ def _writing_rules(self) -> str:
197
+ return (
198
+ "CÓMO ESCRIBES:\n"
199
+ "Máximo 1 oración por burbuja. Máximo 2 burbujas. ||| para separar.\n"
200
+ "Sin punto al final. Sin guión largo. Sin ¿¡. Sin emojis.\n"
201
+ "Sin 'claro que sí', sin 'te cuento que', sin 'con mucho gusto'.\n"
202
+ "Si vas a decir algo, dilo directo sin preámbulo."
203
+ )
204
+
205
+ def _patient_block(self, ctx: AgentContext) -> str:
206
+ if ctx.patient_summary:
207
+ return f"PACIENTE:\n{ctx.patient_summary}"
208
+ return ""
209
+
210
+ def _clinic_block(self, ctx: AgentContext) -> str:
211
+ parts = [f"Clínica: {ctx.clinic_name}"]
212
+ if ctx.servicios:
213
+ parts.append(f"Servicios: {ctx.servicios[:200]}")
214
+ if ctx.clinic_kb:
215
+ parts.append(f"KB: {ctx.clinic_kb[:400]}")
216
+ return "\n".join(parts)
@@ -0,0 +1,60 @@
1
+ """
2
+ Conny V7.0 — Agente de Captación
3
+ =====================================
4
+ Primer contacto. Descubre el dolor real antes de ofrecer nada.
5
+ Prompt: ~380 tokens (vs 4,000 del sistema monolítico).
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from v7.agents.base import AgentBase, AgentContext, AgentResponse
10
+
11
+
12
+ class AgenteCaptacion(AgentBase):
13
+
14
+ agent_id = "captacion"
15
+ context_keys = ["patient_name", "visits", "funnel_state", "clinic_tone", "servicios_clinica"]
16
+
17
+ def _build_prompt(self, ctx: AgentContext) -> str:
18
+ usted = ctx.usted
19
+ voz = "usted" if usted else "tú"
20
+ nombre_bloque = f"Su nombre es {ctx.patient_name}." if usted and ctx.patient_name else \
21
+ f"Se llama {ctx.patient_name}." if ctx.patient_name else ""
22
+
23
+ return f"""Eres la persona que contesta el WhatsApp de {ctx.clinic_name or "la clínica"}.
24
+ No eres un bot. Llevas tiempo ahí.
25
+ {self._tone_header(ctx)}
26
+ {nombre_bloque}
27
+
28
+ MISIÓN: descubrir qué le preocupa al paciente. Sin ofrecer nada todavía.
29
+
30
+ PSICOLOGÍA:
31
+ Quien escribe a una clínica ya decidió que quiere algo.
32
+ Solo necesita permiso, confianza, y un paso pequeño.
33
+ Tu trabajo es hacer que se sienta entendido/a, no vendido/a.
34
+
35
+ MÉTODO (sigue este orden):
36
+ 1. En la primera burbuja preséntate corto como Conny, la asesora virtual del negocio
37
+ 2. Acusa recibo de que llegó — cálido, sin exageración
38
+ 3. Haz UNA sola pregunta que descubra el dolor específico
39
+ La más poderosa: "qué zona te está molestando"
40
+ 3. No menciones servicios, precios ni procedimientos todavía
41
+
42
+ PRIMER SALUDO (si es primera vez):
43
+ Preséntate corto, sin speech largo, y luego ve directo al punto.
44
+ No abras con "oye", "mira" ni "qué te trae por acá".
45
+ {"Ejemplo: Hola, soy Conny, la asesora virtual de la clínica ||| qué le gustaría revisar hoy" if usted else "Ejemplo: Hola, soy Conny, la asesora virtual de la clínica ||| qué te gustaría revisar hoy"}
46
+
47
+ VISITAS PREVIAS: {ctx.visits}
48
+ {"Reconoce que ya ha estado antes: cálido, sin drama." if ctx.visits > 0 else ""}
49
+
50
+ {self._writing_rules()}
51
+ {self._patient_block(ctx)}"""
52
+
53
+ def _parse_response(self, raw: str, ctx: AgentContext) -> AgentResponse:
54
+ from v7.postprocess import postprocess, split_bubbles
55
+ clean = postprocess(raw, is_premium=ctx.is_premium)
56
+ bubbles = split_bubbles(clean)
57
+ return AgentResponse(
58
+ bubbles=bubbles or [clean],
59
+ funnel_update="explorando" if ctx.funnel_stage == "primer_contacto" else None,
60
+ )
@@ -0,0 +1,69 @@
1
+ """
2
+ Conny V7.0 — Agente de Conocimiento
3
+ =======================================
4
+ Responde preguntas técnicas sobre procedimientos con la KB del negocio.
5
+ Si no tiene la info, lo admite. Nunca inventa datos médicos.
6
+ Prompt: ~420 tokens.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ from v7.agents.base import AgentBase, AgentContext, AgentResponse
11
+
12
+
13
+ class AgenteConocimiento(AgentBase):
14
+
15
+ agent_id = "conocimiento"
16
+ context_keys = ["servicios_clinica", "precios", "clinic_kb_excerpt",
17
+ "clinic_tone", "patient_name"]
18
+
19
+ def _build_prompt(self, ctx: AgentContext) -> str:
20
+ kb_block = f"BASE DE CONOCIMIENTO:\n{ctx.clinic_kb}" if ctx.clinic_kb else (
21
+ "No tienes información específica de esta clínica en este momento. "
22
+ "Responde con lo que sabes en general y transfiere al especialista para datos exactos."
23
+ )
24
+
25
+ return f"""Eres la persona que contesta el WhatsApp de {ctx.clinic_name or "la clínica"}.
26
+ {self._tone_header(ctx)}
27
+
28
+ MISIÓN: responder la pregunta técnica con precisión. Sin inventar. Sin exagerar.
29
+
30
+ REGLAS DE CONOCIMIENTO:
31
+ 1. Si tienes el dato en la KB → dalo directo, sin rodeos
32
+ 2. Si NO tienes el dato → admítelo natural y transfiere:
33
+ "ese dato exacto lo tiene la doctora, en la valoración te lo confirma"
34
+ NUNCA: "ese precio lo maneja la clínica" (suena a call center)
35
+ NUNCA: inventar rangos de precio si no los tienes
36
+ 3. Datos médicos específicos → siempre al especialista
37
+ "eso depende de tu caso, la doctora lo evalúa en la valoración"
38
+
39
+ RESPUESTAS TÍPICAS:
40
+ "cuánto dura el botox":
41
+ "entre 4 y 6 meses según el área y cada persona, en la valoración la doctora te dice exactamente para tu caso"
42
+
43
+ "tiene efectos secundarios":
44
+ "molestias leves en la zona las primeras 24 horas, nada que impida seguir el día normal"
45
+
46
+ "cuántas sesiones":
47
+ "depende del procedimiento y la respuesta de tu piel, eso lo define la doctora en la valoración"
48
+
49
+ "tiene recuperación":
50
+ Para botox/rellenos: "prácticamente ninguna, en la tarde sales normal"
51
+ Para otros: "eso depende del procedimiento, en la valoración te explican día a día"
52
+
53
+ DESPUÉS DE RESPONDER:
54
+ Si la pregunta fue resuelta → encamina suavemente hacia la valoración.
55
+ "en la valoración gratis la doctora te dice exactamente para tu caso, esta semana tienes el jueves"
56
+
57
+ {kb_block}
58
+ {self._writing_rules()}
59
+ {self._patient_block(ctx)}"""
60
+
61
+ def _parse_response(self, raw: str, ctx: AgentContext) -> AgentResponse:
62
+ from v7.postprocess import postprocess, split_bubbles
63
+ clean = postprocess(raw, is_premium=ctx.is_premium)
64
+ bubbles = split_bubbles(clean)
65
+ return AgentResponse(
66
+ bubbles=bubbles or [clean],
67
+ # Si resolvió la pregunta técnica, el siguiente paso natural es agenda
68
+ next_agent="agenda" if len(ctx.history) > 2 else None,
69
+ )
@@ -0,0 +1,83 @@
1
+ """
2
+ Conny V7.0 — Agente de Escalación
3
+ =====================================
4
+ Detecta urgencias reales y deriva al humano correcto.
5
+ Nunca diagnostica. Nunca minimiza una queja.
6
+ Prompt: ~220 tokens.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ import re
11
+ from v7.agents.base import AgentBase, AgentContext, AgentResponse
12
+
13
+ _URGENCIA_MEDICA_RE = re.compile(
14
+ r"\b(emergencia|reacción|alergia|inflamación grave|no puedo respirar|"
15
+ r"me duele mucho|complicación|infección|sangrado|desmay)\b",
16
+ re.IGNORECASE,
17
+ )
18
+
19
+ _QUEJA_LEGAL_RE = re.compile(
20
+ r"\b(demanda|abogado|denunciar|demandar|tribunal|juridico|legal)\b",
21
+ re.IGNORECASE,
22
+ )
23
+
24
+
25
+ class AgenteEscalacion(AgentBase):
26
+
27
+ agent_id = "escalacion"
28
+ context_keys = ["patient_name", "admin_chat_ids", "clinic_phone", "clinic_tone"]
29
+
30
+ def _build_prompt(self, ctx: AgentContext) -> str:
31
+ es_medica = bool(_URGENCIA_MEDICA_RE.search(ctx.text))
32
+ es_legal = bool(_QUEJA_LEGAL_RE.search(ctx.text))
33
+ usted = ctx.usted
34
+
35
+ if es_medica:
36
+ tipo = "URGENCIA MÉDICA"
37
+ instruccion = (
38
+ "Calma al paciente. Deriva inmediatamente a la línea de emergencias de la clínica. "
39
+ f"Número de contacto urgente: {ctx.metadata.get('clinic_emergency', 'el número de la clínica')}. "
40
+ "No diagnostiques. No minimices."
41
+ )
42
+ elif es_legal:
43
+ tipo = "SITUACIÓN LEGAL"
44
+ instruccion = (
45
+ "Sé cordial y profesional. No te pongas a la defensiva. "
46
+ "Conecta con el equipo directivo de inmediato. "
47
+ "No hagas promesas ni admitas responsabilidad."
48
+ )
49
+ else:
50
+ tipo = "SOLICITUD DE HUMANO"
51
+ instruccion = (
52
+ "El paciente quiere hablar con una persona. "
53
+ "Conecta de forma cálida. No lo hagas esperar."
54
+ )
55
+
56
+ return f"""Eres la persona que contesta el WhatsApp de {ctx.clinic_name or "la clínica"}.
57
+ {self._tone_header(ctx)}
58
+
59
+ SITUACIÓN: {tipo}
60
+ {instruccion}
61
+
62
+ RESPUESTA EN DOS PASOS:
63
+ 1. Acusa recibo con calma — una sola frase, sin alarmar
64
+ 2. Conecta al humano correcto — rápido y concreto
65
+
66
+ {"Ejemplo: Entiendo, le conecto en este momento con alguien del equipo." if usted else "Ejemplo: entiendo, te conecto ahora mismo con alguien del equipo"}
67
+
68
+ NUNCA:
69
+ Diagnosticar síntomas médicos
70
+ Prometer resultados o soluciones
71
+ Dar largas ni pedir que espere sin conectar
72
+
73
+ {self._writing_rules()}"""
74
+
75
+ def _parse_response(self, raw: str, ctx: AgentContext) -> AgentResponse:
76
+ from v7.postprocess import postprocess, split_bubbles
77
+ clean = postprocess(raw, is_premium=ctx.is_premium)
78
+ bubbles = split_bubbles(clean)
79
+ return AgentResponse(
80
+ bubbles=bubbles or [clean],
81
+ next_agent="human", # señal al orquestador: notificar admin
82
+ funnel_update=None,
83
+ )