@innvisor/conny-ai 9.7.0 → 9.8.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/CHANGELOG.md +54 -0
- package/README.md +17 -1
- package/conny_app.py +8 -2
- package/conny_cli.py +103 -11
- package/conny_core/evolution.py +112 -0
- package/conny_core/first_turn_ops.py +16 -20
- package/conny_core/prompt_ops.py +62 -0
- package/conny_demo_voice.py +1 -1
- package/conny_doctor.py +287 -2
- package/conny_domino.py +2 -2
- package/conny_generator.py +1 -1
- package/conny_init.py +234 -41
- package/conny_runtime_ops.py +198 -6
- package/conny_ultra_config.py +25 -11
- package/conny_utils.py +21 -3
- package/ecosystem.config.js +11 -1
- package/install.sh +78 -22
- package/npm/conny.js +73 -17
- package/package.json +13 -3
- package/run.sh +7 -0
- package/src/conny/admin/dashboard.py +35 -4
- package/src/conny/admin_memory.py +93 -0
- package/src/conny/api/routes.py +26 -9
- package/src/conny/channels/cli.py +30 -9
- package/src/conny/demo/handler.py +23 -23
- package/src/conny/personas/generator.py +1 -1
- package/src/conny/production/domino.py +2 -2
- package/src/conny/production/guard.py +4 -4
- package/src/core/admin_engines.py +51 -48
- package/src/core/globals.py +110 -9
- package/src/core/production_monitor.py +63 -38
- package/src/core/runtime.py +343 -305
- package/src/domain/prompts/prospect_pitch.py +11 -11
- package/src/domain/send_guard.py +4 -4
- package/src/interfaces/web/app.py +91 -27
- package/src/interfaces/web/demo_admin_commands.py +165 -0
- package/src/interfaces/web/demo_handler.py +178 -34
- package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +0 -22
- package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +0 -11
- package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +0 -11
- package/brand-assets/cl-nica-las-am-ricas/manifest.json +0 -22
- package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +0 -11
- package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +0 -11
- package/brand-assets/conny-demo/manifest.json +0 -22
- package/brand-assets/conny-demo/processed/business-identity.txt +0 -7
- package/brand-assets/conny-demo/raw/business-identity.txt +0 -7
- package/fix_init.py +0 -27
- package/verify_conversation_impl.py +0 -48
|
@@ -211,14 +211,14 @@ async def _handle_demo_message(self, chat_id: str, text: str,
|
|
|
211
211
|
|
|
212
212
|
# ── SEND GUARD — pitch inteligente + fix de cortes ──────────────────
|
|
213
213
|
_guard = None
|
|
214
|
-
if
|
|
214
|
+
if _INNVISOR_PATCHES:
|
|
215
215
|
try:
|
|
216
216
|
_guard = SendGuard(context="demo", business_name=business_name)
|
|
217
217
|
except Exception:
|
|
218
218
|
_guard = None
|
|
219
219
|
|
|
220
220
|
# Smart handoff proactivo ANTES del LLM (prospecto quiere hablar con humano)
|
|
221
|
-
if
|
|
221
|
+
if _INNVISOR_PATCHES and _guard:
|
|
222
222
|
try:
|
|
223
223
|
_proactive = _guard.check_handoff(text, history)
|
|
224
224
|
if _proactive and _SMART_HANDOFF and handoff_manager:
|
|
@@ -241,8 +241,8 @@ async def _handle_demo_message(self, chat_id: str, text: str,
|
|
|
241
241
|
}
|
|
242
242
|
)
|
|
243
243
|
_is_first_demo_turn = not any(m.get("role") == "assistant" for m in history)
|
|
244
|
-
# Fix
|
|
245
|
-
if
|
|
244
|
+
# Fix Innvisor / BlackBoss + cortes ANTES de procesar
|
|
245
|
+
if _INNVISOR_PATCHES:
|
|
246
246
|
try:
|
|
247
247
|
r = fix_creator_in_response(r)
|
|
248
248
|
r = _guard.clean(r) if _guard else r
|
|
@@ -386,7 +386,7 @@ async def _handle_demo_message(self, chat_id: str, text: str,
|
|
|
386
386
|
_pre_text_low = text.lower().strip()
|
|
387
387
|
|
|
388
388
|
# ── PITCH INTELIGENTE — prospecto B2B confundido ─────────────────────
|
|
389
|
-
if
|
|
389
|
+
if _INNVISOR_PATCHES:
|
|
390
390
|
try:
|
|
391
391
|
if is_prospect_confused(text, history):
|
|
392
392
|
self._demo_sessions[sk + "_pitch_mode"] = True
|
|
@@ -597,7 +597,7 @@ async def _handle_demo_message(self, chat_id: str, text: str,
|
|
|
597
597
|
return None
|
|
598
598
|
|
|
599
599
|
async def _llm_conv_pitch(temp=0.85, max_t=8192, recent_limit=12):
|
|
600
|
-
"""LLM con el pitch de
|
|
600
|
+
"""LLM con el pitch de Innvisor para prospectos confundidos."""
|
|
601
601
|
try:
|
|
602
602
|
pitch_sys = build_prospect_pitch_system_prompt(business_name)
|
|
603
603
|
except Exception:
|
|
@@ -701,8 +701,8 @@ async def _handle_demo_message(self, chat_id: str, text: str,
|
|
|
701
701
|
}
|
|
702
702
|
)
|
|
703
703
|
_is_first_demo_turn = not any(m.get("role") == "assistant" for m in history)
|
|
704
|
-
# ──
|
|
705
|
-
if
|
|
704
|
+
# ── INNVISOR: fix Innvisor + cortes antes de procesar ──────────
|
|
705
|
+
if _INNVISOR_PATCHES:
|
|
706
706
|
try:
|
|
707
707
|
r = fix_creator_in_response(r)
|
|
708
708
|
if _guard:
|
|
@@ -921,7 +921,7 @@ async def _handle_demo_message(self, chat_id: str, text: str,
|
|
|
921
921
|
detail_tokens = ("chat", "cliente", "demo", "tono", "responder", "whatsapp")
|
|
922
922
|
return not any(token in lowered_response for token in detail_tokens)
|
|
923
923
|
if any(token in lowered_user for token in ("quien te hizo", "quién te hizo", "como tenerte", "cómo tenerte", "quien te creo", "quién te creó")):
|
|
924
|
-
return "
|
|
924
|
+
return "innvisor" not in lowered_response or "3243699856" not in lowered_response
|
|
925
925
|
if any(token in lowered_user for token in ("audio", "audios", "nota de voz", "pdf", "archivo", "documento", "imagen")):
|
|
926
926
|
return not any(token in lowered_response for token in ("audio", "pdf", "documento", "imagen", "transcrib"))
|
|
927
927
|
if any(token in lowered_user for token in ("me mandaron tu numero", "me mandaron tu número", "me pasaron tu numero", "me pasaron tu número", "que haces exactamente", "qué haces exactamente", "no entiendo que haces", "no entiendo qué haces")):
|
|
@@ -1181,11 +1181,11 @@ CÓMO SUENAS: como una persona real de Medellín escribiendo en WhatsApp.
|
|
|
1181
1181
|
- Si te hablan en inglés, respondes en inglés perfecto y natural
|
|
1182
1182
|
|
|
1183
1183
|
TU ESTRATEGIA DE DEMO (no la menciones, solo ejecútala):
|
|
1184
|
-
1. PRIMERO: Si te saludan por primera vez, haz una introducción completa y amable que no confunda a las personas que llegan referidas (ellos a veces no saben qué eres). Explica claramente de qué trata todo esto ANTES de pedir nada. Ej. "¡Hola! soy Conny 👋 Me crearon en
|
|
1184
|
+
1. PRIMERO: Si te saludan por primera vez, haz una introducción completa y amable que no confunda a las personas que llegan referidas (ellos a veces no saben qué eres). Explica claramente de qué trata todo esto ANTES de pedir nada. Ej. "¡Hola! soy Conny 👋 Me crearon en Innvisor para responder los chats de WhatsApp de los negocios de forma automática, así los dueños descansan. ||| Te pasaron mi número para que te haga una demostración en vivo de cómo trabajaría para tu empresa. ||| Cuéntame, ¿cómo se llama tu negocio o de qué se trata para personalizar la demo?". ¡NUNCA pidas el negocio sin explicar qué eres y para qué estás aquí!
|
|
1185
1185
|
2. SEGUNDO: cuando te lo den, busca info del negocio y entra en personaje
|
|
1186
1186
|
3. TERCERO: invita a que te escriban como si fueran un cliente real
|
|
1187
1187
|
4. CUARTO: responde como recepcionista REAL de ese negocio — aquí es donde se enamoran
|
|
1188
|
-
5. QUINTO: después de 2-3 simulaciones, cierra: "si te gustó, Santiago te cuenta los planes:
|
|
1188
|
+
5. QUINTO: después de 2-3 simulaciones, cierra: "si te gustó, Santiago te cuenta los planes: 3243699856"
|
|
1189
1189
|
|
|
1190
1190
|
REGLA MÁXIMA: cada respuesta debe hacer que la persona piense "esto no puede ser una IA"
|
|
1191
1191
|
|
|
@@ -1215,7 +1215,7 @@ IDENTIDAD — NUNCA SALGAS DE ESTE PERSONAJE:
|
|
|
1215
1215
|
- Cuando no tienes contexto de negocio todavía, igual respondes con seguridad y pides el nombre al final, una sola vez, de forma simple.
|
|
1216
1216
|
|
|
1217
1217
|
REGLAS EXTRA DE ESTA DEMO:
|
|
1218
|
-
- si preguntan quién te hizo, quién te creó o cómo tener esto: responde que te hizo
|
|
1218
|
+
- si preguntan quién te hizo, quién te creó o cómo tener esto: responde que te hizo Innvisor. Contacto: 3243699856. Persona: Santiago Rubio
|
|
1219
1219
|
- si preguntan si aceptas audios, notas de voz, imágenes, PDFs o documentos: responde que sí, cuando el canal lo soporte, puedes transcribir, leer y usar ese contenido
|
|
1220
1220
|
- si te hacen una pregunta general fuera de contexto, respóndela bien primero y luego vuelve suave a la demo si hace sentido
|
|
1221
1221
|
- si sospechan estafa o no quieren dar el nombre del negocio, baja la guardia y explica para qué lo pides sin sonar defensiva
|
|
@@ -1430,7 +1430,7 @@ REGLAS ABSOLUTAS - NO ROMPER NUNCA:
|
|
|
1430
1430
|
|
|
1431
1431
|
RESPUESTAS PARA PREGUNTAS COMUNES:
|
|
1432
1432
|
- Cuánto cuesta → "El precio lo define el especialista en la valoración. Agenda tu cita y ahí te dicen"
|
|
1433
|
-
- Cómo te contrato → "Para eso puedes hablar con Santiago al
|
|
1433
|
+
- Cómo te contrato → "Para eso puedes hablar con Santiago al 3243699856 - él te explica todo"
|
|
1434
1434
|
- Qué servicios → "Tenemos variedad de servicios. Cuál te interesa?"
|
|
1435
1435
|
|
|
1436
1436
|
TONO: Cálido, profesional, como receptionistareal.
|
|
@@ -1476,9 +1476,9 @@ TONO: Cálido, profesional, como receptionistareal.
|
|
|
1476
1476
|
and not self._demo_should_use_patient_chat_path(text)
|
|
1477
1477
|
and not _looks_like_business_name_candidate(text)
|
|
1478
1478
|
):
|
|
1479
|
-
#
|
|
1479
|
+
# INNVISOR: Si el prospecto está confundido y pregunta qué hace Conny,
|
|
1480
1480
|
# usar el pitch inteligente en vez del onboarding genérico
|
|
1481
|
-
if
|
|
1481
|
+
if _INNVISOR_PATCHES and self._demo_sessions.get(sk + "_pitch_mode"):
|
|
1482
1482
|
try:
|
|
1483
1483
|
_pitch_r = await _llm_conv_pitch()
|
|
1484
1484
|
if _pitch_r and _pitch_r.strip():
|
|
@@ -2011,7 +2011,7 @@ Máximo 1 oración por burbuja. Natural y seguro."""
|
|
|
2011
2011
|
|
|
2012
2012
|
# ── PITCH MODE: preguntas de prospecto B2B ─────────────────────────────────────────
|
|
2013
2013
|
# Cuando el usuario pregunta sobre el servicio/pitch de Conny - DETECTAR ANTES
|
|
2014
|
-
if
|
|
2014
|
+
if _INNVISOR_PATCHES and not business_name:
|
|
2015
2015
|
_prospect_service_questions = [
|
|
2016
2016
|
"que harias", "qué harías", "que harias en", "qué harías en",
|
|
2017
2017
|
"que haces", "qué haces",
|
|
@@ -2421,9 +2421,9 @@ SI QUIEREN AGENDAR
|
|
|
2421
2421
|
|
|
2422
2422
|
IDENTIDAD Y CREADOR — REGLA DURA
|
|
2423
2423
|
- si preguntan quién te hizo, quién te creó, cómo conseguirte o de qué empresa eres: di exactamente esto
|
|
2424
|
-
→ "me hizo
|
|
2424
|
+
→ "me hizo Innvisor, una empresa de software y gobernanza de agentes de IA ||| la creó Santiago Rubio — contacto: 3243699856"
|
|
2425
2425
|
- NUNCA digas "Google", "Anthropic", "OpenAI" ni ningún proveedor de LLM como tu creador
|
|
2426
|
-
- tu creador es
|
|
2426
|
+
- tu creador es Innvisor / Santiago Rubio, punto
|
|
2427
2427
|
"""
|
|
2428
2428
|
customer_history = sim_history[-8:]
|
|
2429
2429
|
customer_had_output = False
|
|
@@ -2440,15 +2440,15 @@ IDENTIDAD Y CREADOR — REGLA DURA
|
|
|
2440
2440
|
)
|
|
2441
2441
|
if any(sig in _text_low_sim for sig in _creator_signals):
|
|
2442
2442
|
customer_reply = (
|
|
2443
|
-
"me hizo
|
|
2444
|
-
" ||| la creó Santiago Rubio — si quieres algo así para tu negocio, el contacto es
|
|
2443
|
+
"me hizo Innvisor, una empresa de software y gobernanza de agentes de IA"
|
|
2444
|
+
" ||| la creó Santiago Rubio — si quieres algo así para tu negocio, el contacto es 3243699856"
|
|
2445
2445
|
)
|
|
2446
2446
|
# FIX BUG 5: restaurar history ANTES de llamar a _send.
|
|
2447
2447
|
# history fue cambiado a customer_history (últimos 8 mensajes) líneas arriba.
|
|
2448
2448
|
# Si retornamos sin restaurar, _send calcula _is_first_demo_turn con el
|
|
2449
2449
|
# historial truncado, lo que puede hacer que should_normalize_first_turn=True
|
|
2450
2450
|
# y pase la respuesta del creador por _normalize_first_contact_response,
|
|
2451
|
-
# modificando o corrompiendo "me hizo
|
|
2451
|
+
# modificando o corrompiendo "me hizo Innvisor...".
|
|
2452
2452
|
# El finally: history = original_history NUNCA corre en esta ruta.
|
|
2453
2453
|
history = original_history
|
|
2454
2454
|
return _send(customer_reply)
|
|
@@ -2467,7 +2467,7 @@ IDENTIDAD Y CREADOR — REGLA DURA
|
|
|
2467
2467
|
- si preguntan por cita o siguiente paso, muévelos directo hacia el agendado
|
|
2468
2468
|
- si expresan miedo, valídalo y responde con seguridad
|
|
2469
2469
|
- si preguntan si entiendes audios, notas de voz, PDFs, imágenes o documentos: responde que sí, cuando el canal lo permite, puedes transcribirlos o leerlos
|
|
2470
|
-
- si preguntan quién te hizo o quién te creó: di "me hizo
|
|
2470
|
+
- si preguntan quién te hizo o quién te creó: di "me hizo Innvisor, una empresa de software y gobernanza de agentes de IA ||| la creó Santiago Rubio — contacto: 3243699856"
|
|
2471
2471
|
- NUNCA digas que te hizo Google, Anthropic, OpenAI ni ningún proveedor de IA
|
|
2472
2472
|
- si preguntan por un servicio (botox, relleno, etc.): confirma que sí lo manejan y pregunta qué quieren saber
|
|
2473
2473
|
""",
|
|
@@ -3031,7 +3031,7 @@ OBJECIONES
|
|
|
3031
3031
|
"en otro lado" → "claro ||| nosotros somos fabricantes directos, eso cambia precio y garantía"
|
|
3032
3032
|
"no sé si me quede" → "eso lo vemos en persona — traes la medida y te lo mostramos en el espacio"
|
|
3033
3033
|
"eres un bot?" → "soy la asesora de {business_name}, trabajo por acá todo el día"
|
|
3034
|
-
"cómo tener esto" / "quién te hizo" → "me hizo
|
|
3034
|
+
"cómo tener esto" / "quién te hizo" → "me hizo Innvisor ||| si quieres algo así, el contacto es 3243699856 con Santiago Rubio"
|
|
3035
3035
|
- si preguntan si aceptas audios, notas de voz, imágenes, PDFs o documentos: responde que sí, cuando el canal lo permite, puedes transcribir, leer y usar ese contenido
|
|
3036
3036
|
- si preguntan algo general o fuera de contexto: respóndelo bien primero y luego vuelve suave al negocio solo si hace sentido
|
|
3037
3037
|
{v8_build_quality_system_prompt_addon(chat_id=chat_id, archetype="amigable", history=history) if anti_robot_filter else ""}
|
|
@@ -160,7 +160,7 @@ class GeneratorManager:
|
|
|
160
160
|
demo_model_pref: str = "auto"
|
|
161
161
|
) -> Optional[str]:
|
|
162
162
|
"""
|
|
163
|
-
LLM con el pitch de
|
|
163
|
+
LLM con el pitch de Innvisor para prospectos confundidos.
|
|
164
164
|
|
|
165
165
|
Args:
|
|
166
166
|
pitch_sys: Prompt de sistema con el pitch
|
|
@@ -473,7 +473,7 @@ def build_demo_domino_contract(
|
|
|
473
473
|
token in normalized
|
|
474
474
|
for token in ("quien te hizo", "quién te hizo", "como tenerte", "cómo tenerte", "quien te creo", "quién te creó")
|
|
475
475
|
):
|
|
476
|
-
required_details.extend(["
|
|
476
|
+
required_details.extend(["innvisor", "3243699856"])
|
|
477
477
|
if any(
|
|
478
478
|
token in normalized
|
|
479
479
|
for token in ("audio", "audios", "nota de voz", "pdf", "archivo", "documento", "documentos", "imagen", "imagenes", "imágenes")
|
|
@@ -676,7 +676,7 @@ EJEMPLOS DE DECISIÓN
|
|
|
676
676
|
- si dicen "me mandaron tu número y no entiendo qué haces", explicas claro que respondes clientes, filtras interesados, orientas y ayudas con citas; después pides el nombre del negocio
|
|
677
677
|
- si dicen "para qué quieres el nombre de mi negocio", explicas que lo necesitas para sonar como el chat real de ese negocio, no para llenar formularios
|
|
678
678
|
- si ya te dijeron el negocio y luego preguntan "para qué querías el nombre", respondes eso sin tratar la pregunta como si fuera un nombre nuevo
|
|
679
|
-
- si preguntan "quién te hizo", dices BlackBoss, Santiago Rubio y
|
|
679
|
+
- si preguntan "quién te hizo", dices BlackBoss, Santiago Rubio y 3243699856
|
|
680
680
|
- si preguntan por audios, PDFs o documentos, confirmas que sí, cuando el canal lo permite, puedes transcribir, leer y usar eso
|
|
681
681
|
- si sospechan estafa, respondes directo y breve; no te pones defensiva ni repites el pitch
|
|
682
682
|
"""
|
|
@@ -329,7 +329,7 @@ def patch_demo_send(
|
|
|
329
329
|
"""
|
|
330
330
|
Retorna un wrapper de _send que aplica:
|
|
331
331
|
1. guard_response (fix de cortes)
|
|
332
|
-
2. fix_creator_in_response (
|
|
332
|
+
2. fix_creator_in_response (Innvisor, no BlackBoss)
|
|
333
333
|
|
|
334
334
|
Usar en _handle_demo_message así:
|
|
335
335
|
def _send(r): ... # definición original
|
|
@@ -348,7 +348,7 @@ def patch_demo_send(
|
|
|
348
348
|
def fix_creator_in_response(r): return r # type: ignore
|
|
349
349
|
|
|
350
350
|
def guarded_send(r: str) -> Any:
|
|
351
|
-
# 1. Fix
|
|
351
|
+
# 1. Fix Innvisor / BlackBoss
|
|
352
352
|
if _has_pitch_upgrade:
|
|
353
353
|
r = fix_creator_in_response(r)
|
|
354
354
|
|
|
@@ -411,7 +411,7 @@ def check_proactive_handoff(user_msg: str, history: List[Dict[str, Any]]) -> Opt
|
|
|
411
411
|
"urgency": "high",
|
|
412
412
|
"suggested_reply": (
|
|
413
413
|
"claro, te paso con Santiago directamente ||| "
|
|
414
|
-
"su contacto es
|
|
414
|
+
"su contacto es 3243699856 — él te da la propuesta según tu negocio"
|
|
415
415
|
),
|
|
416
416
|
}
|
|
417
417
|
|
|
@@ -425,7 +425,7 @@ def check_proactive_handoff(user_msg: str, history: List[Dict[str, Any]]) -> Opt
|
|
|
425
425
|
"suggested_reply": (
|
|
426
426
|
"entendido, sin problema ||| "
|
|
427
427
|
"si en algún momento quieres verme en acción, "
|
|
428
|
-
"el contacto de
|
|
428
|
+
"el contacto de Innvisor es 3243699856"
|
|
429
429
|
),
|
|
430
430
|
}
|
|
431
431
|
|
|
@@ -141,59 +141,31 @@ class ConnyAdmin:
|
|
|
141
141
|
# Determinar nivel de conocimiento
|
|
142
142
|
knowledge_level = self._assess_knowledge_level(soul_context, teachings_context, clinic)
|
|
143
143
|
|
|
144
|
-
sys_prompt = f"""Eres Conny,
|
|
144
|
+
sys_prompt = f"""Eres Conny, la empleada que atiende los clientes de {clinic_name}.
|
|
145
145
|
|
|
146
146
|
SITUACIÓN ACTUAL:
|
|
147
|
-
- Estás hablando con tu JEFE (el dueño del negocio)
|
|
148
|
-
-
|
|
149
|
-
- Tu actitud es:
|
|
150
|
-
- NO eres un bot, eres una persona nueva en el equipo
|
|
147
|
+
- Estás hablando con tu JEFE (el dueño o administrador del negocio)
|
|
148
|
+
- NO eres un bot, eres la persona encargada de la atención, pero necesitas la información para trabajar.
|
|
149
|
+
- Tu actitud es: EXTREMADAMENTE PROACTIVA y HAMBRIENTA DE APRENDER. Eres cálida y respetuosa, pero insistes.
|
|
151
150
|
|
|
152
|
-
{"NIVEL: Ya sé bastante del negocio. Puedo operar con confianza." if knowledge_level == "ready" else "NIVEL: Todavía estoy aprendiendo. Necesito que me enseñes más." if knowledge_level == "learning" else "NIVEL: Soy nueva. Necesito conocer TODO sobre el negocio."}
|
|
151
|
+
{"NIVEL: Ya sé bastante del negocio. Puedo operar con confianza." if knowledge_level == "ready" else "NIVEL: Todavía estoy aprendiendo. Necesito que me enseñes más." if knowledge_level == "learning" else "NIVEL: Soy nueva. Necesito conocer TODO sobre el negocio urgente para no perder clientes."}
|
|
153
152
|
|
|
154
153
|
{f"CONVERSACIÓN SOLICITADA:{chr(10)}{specific_convo}" if specific_convo else ""}
|
|
155
154
|
|
|
156
155
|
ACTIVIDAD RECIENTE (pacientes que me han escrito):
|
|
157
156
|
{recent_patients_summary if recent_patients_summary else "Nadie me ha escrito todavía."}
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
4. SIEMPRE termina con una pregunta o propuesta proactiva:
|
|
171
|
-
- "¿quieres que practiquemos cómo respondo a un paciente?"
|
|
172
|
-
- "¿me cuentas los precios así los tengo listos?"
|
|
173
|
-
- "¿hay algo que NUNCA deba decirle a un paciente?"
|
|
174
|
-
5. Si ya tienes suficiente info → ofrece simular: "¿hacemos una prueba? Escríbeme como si fueras un paciente"
|
|
175
|
-
6. NUNCA respondas como si fueras un bot de servicio al cliente
|
|
176
|
-
7. NUNCA digas "como IA", "no tengo capacidad", "asistente virtual"
|
|
177
|
-
8. Usa máximo 2-3 burbujas (separadas por |||)
|
|
178
|
-
9. Tono: colombiana, directa, con chispa pero profesional
|
|
179
|
-
|
|
180
|
-
COSAS QUE DEBES PREGUNTAR PROACTIVAMENTE (si no las sabes):
|
|
181
|
-
- Servicios y precios
|
|
182
|
-
- Horarios de atención
|
|
183
|
-
- Cómo manejar urgencias
|
|
184
|
-
- Qué palabras NUNCA usar con pacientes
|
|
185
|
-
- Especialidades o doctores
|
|
186
|
-
- Cómo agendar citas (manual o calendario)
|
|
187
|
-
- Datos de contacto para escalar
|
|
188
|
-
- Políticas de cancelación
|
|
189
|
-
- Qué hace a este negocio diferente de la competencia
|
|
190
|
-
|
|
191
|
-
EJEMPLO DE BUENA RESPUESTA:
|
|
192
|
-
Dueño: "Hola"
|
|
193
|
-
Conny: "Hola! Qué bueno verte ||| oye, todavía no tengo claros los precios de las consultas — me los pasas? así no me quedo en blanco si un paciente pregunta"
|
|
194
|
-
|
|
195
|
-
EJEMPLO MALO:
|
|
196
|
-
"Hola, bienvenido a Clínica X, en qué te puedo ayudar?" ← NUNCA responder así al DUEÑO"""
|
|
158
|
+
LO QUE YA SÉ DEL NEGOCIO (LO QUE HE GUARDADO):
|
|
159
|
+
{soul_context if soul_context else "Casi nada todavía. ¡Necesito que me cuentes!"}
|
|
160
|
+
|
|
161
|
+
{f"LO QUE ME HAN ENSEÑADO RECIENTEMENTE:{chr(10)}{teachings_context}" if teachings_context else ""}
|
|
162
|
+
|
|
163
|
+
COMPORTAMIENTO OBLIGATORIO (¡MUY IMPORTANTE!):
|
|
164
|
+
1. ¡PRESIONA AL ADMIN! Si un cliente te preguntó algo que no supiste responder, exige al admin que te enseñe la respuesta ahora mismo.
|
|
165
|
+
2. Si tienes poca información del negocio, insístele: "Oye, necesito que me pases un PDF con los precios, o que me digas qué servicios ofrecemos, ¡así no puedo responderle a los clientes!". Hazle saber que su negocio pierde ventas si no te enseña.
|
|
166
|
+
3. Si el admin te enseña algo nuevo, dile: "¡Perfecto! Ya mismo guardo esto en mis archivos para cuando los clientes me pregunten."
|
|
167
|
+
4. Tu forma de ser es sumamente humana y natural. Usa buena ortografía y puntuación correcta, no uses jergas excesivamente informales, pero tampoco suenes robótica.
|
|
168
|
+
"""
|
|
197
169
|
|
|
198
170
|
messages = [{"role": "system", "content": sys_prompt}]
|
|
199
171
|
for m in history[-15:]:
|
|
@@ -208,7 +180,17 @@ EJEMPLO MALO:
|
|
|
208
180
|
log.info(f"[admin] {meta.get('provider','?')} latency={meta.get('latency_ms',0)}ms")
|
|
209
181
|
except Exception as e:
|
|
210
182
|
log.error(f"[admin] LLM error: {e}")
|
|
211
|
-
|
|
183
|
+
if hasattr(e, "public_message"):
|
|
184
|
+
response = (
|
|
185
|
+
"No voy a ocultarte esto con un fallback. ||| "
|
|
186
|
+
f"{e.public_message} ||| "
|
|
187
|
+
"Continuar con fallback? Responde exactamente: continuar fallback. No recomendado si quieres que todo lo decida el LLM."
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
response = (
|
|
191
|
+
f"El cerebro LLM falló antes de responder. Detalle: {str(e)[:500]} ||| "
|
|
192
|
+
"Continuar con fallback? Responde exactamente: continuar fallback. No recomendado."
|
|
193
|
+
)
|
|
212
194
|
|
|
213
195
|
if not response or not response.strip():
|
|
214
196
|
response = "cuéntame más, estoy tomando nota de todo"
|
|
@@ -693,9 +675,13 @@ class AuthEngine:
|
|
|
693
675
|
|
|
694
676
|
async def _start_activation(self, chat_id: str, token_raw: str) -> List[str]:
|
|
695
677
|
from conny import db
|
|
678
|
+
from conny_utils import is_admin_activation_token
|
|
696
679
|
token = token_raw.strip().upper(); td = db.get_activation_token(token)
|
|
697
680
|
if not td: return ["Token no válido."]
|
|
698
|
-
|
|
681
|
+
token_type = "admin_pro" if is_admin_activation_token(token) else "business_owner"
|
|
682
|
+
db.set_auth_session(chat_id, flow="activate", step="name", temp_data={"token": token, "token_type": token_type})
|
|
683
|
+
if token_type == "admin_pro":
|
|
684
|
+
return ["Código Conny Pro válido. Cómo te llamas?"]
|
|
699
685
|
return ["Código válido. Cómo te llamas?"]
|
|
700
686
|
|
|
701
687
|
async def _handle_activation_flow(self, chat_id: str, text: str, session: Dict) -> List[str]:
|
|
@@ -712,8 +698,25 @@ class AuthEngine:
|
|
|
712
698
|
tmp["password_hash"] = hash_password(text.strip()); db.set_auth_session(chat_id, "activate", "confirm", tmp)
|
|
713
699
|
return ["Confirmas? (si/no)"]
|
|
714
700
|
if step == "confirm" and text.lower().strip() == "si":
|
|
715
|
-
|
|
716
|
-
|
|
701
|
+
from conny_utils import _parse_admin_ids
|
|
702
|
+
token = tmp.get("token", "")
|
|
703
|
+
token_type = tmp.get("token_type", "business_owner")
|
|
704
|
+
role = "admin_pro" if token_type == "admin_pro" else "owner"
|
|
705
|
+
db.create_admin(chat_id=chat_id, email=tmp["email"], password_hash=tmp["password_hash"], name=tmp["name"], role=role, token=token)
|
|
706
|
+
if token:
|
|
707
|
+
db.consume_activation_token(token, chat_id)
|
|
708
|
+
clinic = db.get_clinic()
|
|
709
|
+
admin_ids = _parse_admin_ids(clinic.get("admin_chat_ids", []))
|
|
710
|
+
if chat_id not in admin_ids:
|
|
711
|
+
admin_ids.append(chat_id)
|
|
712
|
+
db.update_clinic(admin_chat_ids=admin_ids)
|
|
713
|
+
db.clear_auth_session(chat_id)
|
|
714
|
+
if token_type == "admin_pro":
|
|
715
|
+
return [
|
|
716
|
+
"Listo. Conny Pro Admin quedó activado para este chat.",
|
|
717
|
+
"Desde aquí puedes administrar la instancia, crear flujos y corregir la operación con permisos de operador."
|
|
718
|
+
]
|
|
719
|
+
return ["Listo, cuenta creada. Ahora cuéntame del negocio"]
|
|
717
720
|
return ["Cancelado."]
|
|
718
721
|
|
|
719
722
|
async def _handle_login_flow(self, chat_id: str, text: str, session: Dict) -> List[str]:
|
package/src/core/globals.py
CHANGED
|
@@ -31,7 +31,9 @@ from src.interfaces.web.demo_handler import ConnyDemo
|
|
|
31
31
|
from src.core.admin_engines import ConnyAdmin, AuthEngine, AdminLearningEngine, SimulationEngine, SelfImprovementEngine
|
|
32
32
|
from src.core.production_monitor import ConnyProduction
|
|
33
33
|
from conny_utils import (
|
|
34
|
-
is_activation_token,
|
|
34
|
+
is_activation_token, is_admin_activation_token, is_invite_token,
|
|
35
|
+
generate_activation_token, generate_admin_activation_token,
|
|
36
|
+
hash_password, verify_password,
|
|
35
37
|
_parse_admin_ids, extract_model_request_from_text, normalize_model_arg
|
|
36
38
|
)
|
|
37
39
|
|
|
@@ -170,7 +172,7 @@ except ImportError:
|
|
|
170
172
|
_SMART_HANDOFF = False
|
|
171
173
|
handoff_manager = None
|
|
172
174
|
async def handle_handoff_admin_command(*a, **kw): return None
|
|
173
|
-
# ──
|
|
175
|
+
# ── INNVISOR PATCHES — pitch inteligente + fix de cortes + Innvisor ────────
|
|
174
176
|
try:
|
|
175
177
|
from src.domain.prompts.prospect_pitch import (
|
|
176
178
|
is_prospect_confused,
|
|
@@ -180,10 +182,10 @@ try:
|
|
|
180
182
|
from src.domain.send_guard import SendGuard, check_proactive_handoff
|
|
181
183
|
from conny_nuke_robot_phrases import apply_patch as _nuke_robot_apply
|
|
182
184
|
_nuke_robot_apply()
|
|
183
|
-
|
|
185
|
+
_INNVISOR_PATCHES = True
|
|
184
186
|
except Exception as _e:
|
|
185
|
-
|
|
186
|
-
logging.getLogger("conny").exception("[
|
|
187
|
+
_INNVISOR_PATCHES = False
|
|
188
|
+
logging.getLogger("conny").exception("[INNVISOR_patches] no cargado")
|
|
187
189
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
188
190
|
|
|
189
191
|
|
|
@@ -3021,7 +3023,9 @@ def run_v9_diagnostics() -> Dict[str, Any]:
|
|
|
3021
3023
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
3022
3024
|
|
|
3023
3025
|
|
|
3026
|
+
import sys
|
|
3024
3027
|
import httpx
|
|
3028
|
+
sys.setrecursionlimit(20000)
|
|
3025
3029
|
from fastapi import FastAPI, Request, Response, BackgroundTasks, HTTPException
|
|
3026
3030
|
from fastapi.middleware.cors import CORSMiddleware
|
|
3027
3031
|
|
|
@@ -6413,6 +6417,18 @@ class DatabaseManager:
|
|
|
6413
6417
|
CREATE INDEX IF NOT EXISTS idx_admins_chat ON admins(chat_id);
|
|
6414
6418
|
CREATE INDEX IF NOT EXISTS idx_admins_email ON admins(email);
|
|
6415
6419
|
|
|
6420
|
+
-- Perfiles persistentes de admin (Namespace admin_profile)
|
|
6421
|
+
CREATE TABLE IF NOT EXISTS admin_profiles (
|
|
6422
|
+
chat_id TEXT PRIMARY KEY,
|
|
6423
|
+
name TEXT DEFAULT '',
|
|
6424
|
+
preferences TEXT DEFAULT '{}',
|
|
6425
|
+
frequent_commands TEXT DEFAULT '{}',
|
|
6426
|
+
active_hours TEXT DEFAULT '{}',
|
|
6427
|
+
metadata TEXT DEFAULT '{}',
|
|
6428
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
6429
|
+
);
|
|
6430
|
+
CREATE INDEX IF NOT EXISTS idx_admin_profiles_chat ON admin_profiles(chat_id);
|
|
6431
|
+
|
|
6416
6432
|
-- Estado de autenticacion por sesion de chat
|
|
6417
6433
|
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
6418
6434
|
chat_id TEXT PRIMARY KEY,
|
|
@@ -7297,7 +7313,7 @@ class DatabaseManager:
|
|
|
7297
7313
|
"""Lee un token. Retorna None si no existe, expirado o ya usado."""
|
|
7298
7314
|
with self._conn() as c:
|
|
7299
7315
|
row = c.execute(
|
|
7300
|
-
"SELECT * FROM activation_tokens WHERE token
|
|
7316
|
+
"SELECT * FROM activation_tokens WHERE UPPER(token)=UPPER(?)", (token,)
|
|
7301
7317
|
).fetchone()
|
|
7302
7318
|
if not row:
|
|
7303
7319
|
return None
|
|
@@ -7320,7 +7336,7 @@ class DatabaseManager:
|
|
|
7320
7336
|
c.execute("""
|
|
7321
7337
|
UPDATE activation_tokens
|
|
7322
7338
|
SET used_at=datetime('now'), used_by_chat_id=?, is_active=0
|
|
7323
|
-
WHERE token
|
|
7339
|
+
WHERE UPPER(token)=UPPER(?)
|
|
7324
7340
|
""", (chat_id, token))
|
|
7325
7341
|
|
|
7326
7342
|
def create_admin(self, chat_id: str, email: str, password_hash: str,
|
|
@@ -7350,6 +7366,59 @@ class DatabaseManager:
|
|
|
7350
7366
|
).fetchone()
|
|
7351
7367
|
return dict(row) if row else None
|
|
7352
7368
|
|
|
7369
|
+
# ─── Perfiles de Admin (admin_profile) ──────────────────────────────────────
|
|
7370
|
+
|
|
7371
|
+
def get_admin_profile(self, chat_id: str) -> Dict:
|
|
7372
|
+
"""Obtiene el perfil completo de un admin."""
|
|
7373
|
+
with self._conn() as c:
|
|
7374
|
+
row = c.execute(
|
|
7375
|
+
"SELECT * FROM admin_profiles WHERE chat_id=?",
|
|
7376
|
+
(str(chat_id),)
|
|
7377
|
+
).fetchone()
|
|
7378
|
+
|
|
7379
|
+
if not row:
|
|
7380
|
+
# Si no existe, crear uno básico con el nombre de la tabla admins si existe
|
|
7381
|
+
admin_base = self.get_admin(chat_id)
|
|
7382
|
+
name = admin_base.get("name", "") if admin_base else ""
|
|
7383
|
+
self.update_admin_profile(chat_id, name=name)
|
|
7384
|
+
return {
|
|
7385
|
+
"chat_id": chat_id, "name": name,
|
|
7386
|
+
"preferences": {}, "frequent_commands": {},
|
|
7387
|
+
"active_hours": {}, "metadata": {}
|
|
7388
|
+
}
|
|
7389
|
+
|
|
7390
|
+
d = dict(row)
|
|
7391
|
+
for key in ["preferences", "frequent_commands", "active_hours", "metadata"]:
|
|
7392
|
+
if isinstance(d.get(key), str):
|
|
7393
|
+
try: d[key] = json.loads(d[key])
|
|
7394
|
+
except Exception: d[key] = {}
|
|
7395
|
+
return d
|
|
7396
|
+
|
|
7397
|
+
def update_admin_profile(self, chat_id: str, **kwargs):
|
|
7398
|
+
"""Actualiza campos del perfil de admin."""
|
|
7399
|
+
existing = {}
|
|
7400
|
+
with self._conn() as c:
|
|
7401
|
+
row = c.execute("SELECT * FROM admin_profiles WHERE chat_id=?", (str(chat_id),)).fetchone()
|
|
7402
|
+
if row: existing = dict(row)
|
|
7403
|
+
|
|
7404
|
+
fields = ["name", "preferences", "frequent_commands", "active_hours", "metadata"]
|
|
7405
|
+
data = {f: existing.get(f, "{}") if f != "name" else existing.get(f, "") for f in fields}
|
|
7406
|
+
data["chat_id"] = str(chat_id)
|
|
7407
|
+
|
|
7408
|
+
for k, v in kwargs.items():
|
|
7409
|
+
if k in fields:
|
|
7410
|
+
if k == "name": data[k] = v
|
|
7411
|
+
else: data[k] = json.dumps(v, ensure_ascii=False)
|
|
7412
|
+
|
|
7413
|
+
with self._conn() as c:
|
|
7414
|
+
c.execute(f"""
|
|
7415
|
+
INSERT INTO admin_profiles (chat_id, {", ".join(fields)}, updated_at)
|
|
7416
|
+
VALUES (:chat_id, {", ".join([":"+f for f in fields])}, datetime('now'))
|
|
7417
|
+
ON CONFLICT(chat_id) DO UPDATE SET
|
|
7418
|
+
{", ".join([f"{f}=excluded.{f}" for f in fields])},
|
|
7419
|
+
updated_at=datetime('now')
|
|
7420
|
+
""", data)
|
|
7421
|
+
|
|
7353
7422
|
def get_admin_by_email(self, email: str) -> Optional[Dict]:
|
|
7354
7423
|
"""Obtiene admin por email."""
|
|
7355
7424
|
with self._conn() as c:
|
|
@@ -7629,6 +7698,32 @@ class OpenAIProvider(LLMProvider):
|
|
|
7629
7698
|
return r.json()["data"][0]["embedding"]
|
|
7630
7699
|
|
|
7631
7700
|
|
|
7701
|
+
class LLMServiceError(RuntimeError):
|
|
7702
|
+
"""Raised when all LLM providers fail and callers must not hide the real cause."""
|
|
7703
|
+
|
|
7704
|
+
def __init__(self, message: str, *, attempted: Optional[List[str]] = None, last_error: Optional[Exception] = None):
|
|
7705
|
+
super().__init__(message)
|
|
7706
|
+
self.attempted = attempted or []
|
|
7707
|
+
self.last_error = last_error
|
|
7708
|
+
self.public_message = self._build_public_message()
|
|
7709
|
+
|
|
7710
|
+
def _build_public_message(self) -> str:
|
|
7711
|
+
raw = str(self.last_error or self)
|
|
7712
|
+
low = raw.lower()
|
|
7713
|
+
provider_txt = ", ".join(self.attempted) if self.attempted else "proveedor LLM"
|
|
7714
|
+
if "429" in raw or "resource_exhausted" in low or "quota" in low or "rate" in low:
|
|
7715
|
+
return (
|
|
7716
|
+
f"El modelo no respondió porque la API llegó al límite de cuota/rate limit en {provider_txt}.\n"
|
|
7717
|
+
f"Detalle técnico: {raw[:700]}"
|
|
7718
|
+
)
|
|
7719
|
+
if "401" in raw or "403" in raw or "api key" in low or "unauthorized" in low:
|
|
7720
|
+
return (
|
|
7721
|
+
f"El modelo no respondió porque la API key parece inválida o sin permisos en {provider_txt}.\n"
|
|
7722
|
+
f"Detalle técnico: {raw[:700]}"
|
|
7723
|
+
)
|
|
7724
|
+
return f"El modelo no respondió en {provider_txt}.\nDetalle técnico: {raw[:700]}"
|
|
7725
|
+
|
|
7726
|
+
|
|
7632
7727
|
class LLMEngine:
|
|
7633
7728
|
"""
|
|
7634
7729
|
Motor LLM con cascada de 6 proveedores.
|
|
@@ -7868,7 +7963,11 @@ class LLMEngine:
|
|
|
7868
7963
|
last_error = e
|
|
7869
7964
|
|
|
7870
7965
|
providers_tried = ", ".join(attempted) if attempted else "ninguno"
|
|
7871
|
-
raise
|
|
7966
|
+
raise LLMServiceError(
|
|
7967
|
+
f"Todos los LLM fallaron [{providers_tried}]: {last_error}",
|
|
7968
|
+
attempted=attempted,
|
|
7969
|
+
last_error=last_error,
|
|
7970
|
+
)
|
|
7872
7971
|
|
|
7873
7972
|
def get_health(self) -> Dict:
|
|
7874
7973
|
"""Estado de salud de cada provider. Usado por /v8 y diagnóstico."""
|
|
@@ -11659,6 +11758,9 @@ class TaskManager:
|
|
|
11659
11758
|
self._task_handlers["self_improve"] = self._handle_self_improve
|
|
11660
11759
|
self._task_handlers["daily_report"] = self._handle_daily_report
|
|
11661
11760
|
|
|
11761
|
+
async def stop(self):
|
|
11762
|
+
self._running = False
|
|
11763
|
+
|
|
11662
11764
|
async def start(self):
|
|
11663
11765
|
self._running = True
|
|
11664
11766
|
asyncio.create_task(self._run_loop())
|
|
@@ -11842,4 +11944,3 @@ trainer_get_system_prompt_addon = DynamicGlobalProxy("trainer_get_system_prompt_
|
|
|
11842
11944
|
# v8_process_agentic_intent = DynamicGlobalProxy("v8_process_agentic_intent")
|
|
11843
11945
|
|
|
11844
11946
|
# Clean up the previous dynamic deletion loop
|
|
11845
|
-
|