@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.
- package/.env.example +68 -0
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
- package/brand-assets/Conny.web.logo.png +0 -0
- package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
- package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/conny-demo/manifest.json +22 -0
- package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
- package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
- package/brand-assets/conny-logo.png +0 -0
- package/brand-assets/web.background.png +0 -0
- package/brand_assets.py +323 -0
- package/conny +28 -0
- package/conny-chat.py +579 -0
- package/conny-omni.py +3843 -0
- package/conny.py +113 -0
- package/conny_agents/__init__.py +1 -0
- package/conny_agents/agenda.py +1 -0
- package/conny_agents/captacion.py +1 -0
- package/conny_agents/conocimiento.py +1 -0
- package/conny_agents/escalacion.py +1 -0
- package/conny_agents/objeciones.py +1 -0
- package/conny_agents/seguimiento.py +1 -0
- package/conny_app.py +287 -0
- package/conny_audio.py +350 -0
- package/conny_audio_learn.py +84 -0
- package/conny_brain_v10.py +804 -0
- package/conny_bridge.py +656 -0
- package/conny_calendar.py +169 -0
- package/conny_cli.py +11784 -0
- package/conny_cli_bb.py +437 -0
- package/conny_commands.py +243 -0
- package/conny_config.py +215 -0
- package/conny_core/__init__.py +3 -0
- package/conny_core/conversation_engine.py +446 -0
- package/conny_core/first_turn_ops.py +287 -0
- package/conny_core/persona_registry.py +157 -0
- package/conny_core/prompt_ops.py +561 -0
- package/conny_cron.py +72 -0
- package/conny_demo_v2.py +209 -0
- package/conny_demo_voice.py +134 -0
- package/conny_design.py +43 -0
- package/conny_doctor.py +319 -0
- package/conny_domino.py +696 -0
- package/conny_generator.py +447 -0
- package/conny_google_auth.py +159 -0
- package/conny_i18n.py +619 -0
- package/conny_init.py +509 -0
- package/conny_integrations/__init__.py +4 -0
- package/conny_integrations/llm.py +1 -0
- package/conny_integrations/vault.py +77 -0
- package/conny_integrations/whatsapp.py +1 -0
- package/conny_intelligence.py +65 -0
- package/conny_learning.py +154 -0
- package/conny_memory.py +243 -0
- package/conny_memory_engine.py +292 -0
- package/conny_nova_proxy.py +170 -0
- package/conny_nuke_robot_phrases.py +493 -0
- package/conny_pairing.py +253 -0
- package/conny_patch.py +291 -0
- package/conny_persona_cli.py +150 -0
- package/conny_router.py +308 -0
- package/conny_runtime_ops.py +271 -0
- package/conny_session.py +516 -0
- package/conny_skills/__init__.py +1 -0
- package/conny_skills/demo_mode.py +35 -0
- package/conny_skills/text_processing.py +1 -0
- package/conny_skills/tone_detection.py +1 -0
- package/conny_smart_features.py +333 -0
- package/conny_studio.py +161 -0
- package/conny_sync_fix.py +306 -0
- package/conny_tui.py +512 -0
- package/conny_tui_select.py +202 -0
- package/conny_ultra_config.py +411 -0
- package/conny_uncertainty.py +174 -0
- package/conny_utils.py +87 -0
- package/conny_voice.py +156 -0
- package/conny_voice_engine.py +124 -0
- package/conny_web_search.py +66 -0
- package/conny_weekly_report.py +85 -0
- package/conny_worm.py +88 -0
- package/core/__init__.py +25 -0
- package/ecosystem.config.js +24 -0
- package/fix_init.py +27 -0
- package/install.sh +78 -0
- package/knowledge_base.py +330 -0
- package/nova/rules/default.yaml +37 -0
- package/nova_bridge.py +509 -0
- package/npm/conny.js +471 -0
- package/package.json +102 -0
- package/personas/conny/base/default.yaml +35 -0
- package/personas/conny/base/estetica_whatsapp.yaml +36 -0
- package/requirements.txt +14 -0
- package/run.sh +47 -0
- package/search.py +465 -0
- package/smart_handoff.py +1150 -0
- package/src/__init__.py +0 -0
- package/src/conny/__init__.py +0 -0
- package/src/conny/admin/__init__.py +0 -0
- package/src/conny/admin/api.py +234 -0
- package/src/conny/admin/dashboard.py +772 -0
- package/src/conny/api/__init__.py +0 -0
- package/src/conny/api/routes.py +8851 -0
- package/src/conny/brain/__init__.py +15 -0
- package/src/conny/brain/engine.py +804 -0
- package/src/conny/brain/learning.py +154 -0
- package/src/conny/brain/memory.py +324 -0
- package/src/conny/brain/smart_features.py +333 -0
- package/src/conny/brain/uncertainty.py +167 -0
- package/src/conny/channels/__init__.py +0 -0
- package/src/conny/channels/audio.py +316 -0
- package/src/conny/channels/cli.py +11795 -0
- package/src/conny/channels/logo_art.py +11 -0
- package/src/conny/channels/voice.py +156 -0
- package/src/conny/core/__init__.py +0 -0
- package/src/conny/core/config.py +215 -0
- package/src/conny/core/cron.py +72 -0
- package/src/conny/core/messenger.py +563 -0
- package/src/conny/core/router.py +297 -0
- package/src/conny/core/session.py +312 -0
- package/src/conny/demo/__init__.py +0 -0
- package/src/conny/demo/handler.py +3110 -0
- package/src/conny/integrations/__init__.py +19 -0
- package/src/conny/integrations/calendar.py +169 -0
- package/src/conny/integrations/knowledge.py +312 -0
- package/src/conny/integrations/search.py +66 -0
- package/src/conny/personas/__init__.py +0 -0
- package/src/conny/personas/generator.py +447 -0
- package/src/conny/production/__init__.py +0 -0
- package/src/conny/production/domino.py +696 -0
- package/src/conny/production/guard.py +550 -0
- package/src/conny/production/handoff.py +1150 -0
- package/src/conny/production/monitor.py +353 -0
- package/src/conny/utils/__init__.py +2 -0
- package/src/conny/utils/helpers.py +75 -0
- package/src/conny/utils/i18n.py +619 -0
- package/src/core/admin_engines.py +772 -0
- package/src/core/globals.py +11845 -0
- package/src/core/orchestrator.py +273 -0
- package/src/core/production_monitor.py +353 -0
- package/src/core/runtime.py +5487 -0
- package/src/domain/onboarding_flow.py +230 -0
- package/src/domain/prompts/__init__.py +1 -0
- package/src/domain/prompts/prospect_pitch.py +282 -0
- package/src/domain/send_guard.py +636 -0
- package/src/domain/swarm/queen.py +96 -0
- package/src/infrastructure/llm_providers/engine.py +487 -0
- package/src/interfaces/mcp_server.py +73 -0
- package/src/interfaces/nova_bridge.py +58 -0
- package/src/interfaces/web/admin_api.py +1379 -0
- package/src/interfaces/web/app.py +9408 -0
- package/src/interfaces/web/demo_handler.py +3450 -0
- package/src/interfaces/web/static/generate_avatars.py +46 -0
- package/v7/__init__.py +46 -0
- package/v7/agents/__init__.py +46 -0
- package/v7/agents/agenda.py +77 -0
- package/v7/agents/base.py +216 -0
- package/v7/agents/captacion.py +60 -0
- package/v7/agents/conocimiento.py +69 -0
- package/v7/agents/escalacion.py +83 -0
- package/v7/agents/objeciones.py +109 -0
- package/v7/agents/seguimiento.py +71 -0
- package/v7/memory/__init__.py +46 -0
- package/v7/memory/patient_profile.py +200 -0
- package/v7/orchestrator.py +275 -0
- package/v7/postprocess.py +127 -0
- package/v7/router.py +239 -0
- 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
|
+
)
|