@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +17 -1
  3. package/conny_app.py +8 -2
  4. package/conny_cli.py +103 -11
  5. package/conny_core/evolution.py +112 -0
  6. package/conny_core/first_turn_ops.py +16 -20
  7. package/conny_core/prompt_ops.py +62 -0
  8. package/conny_demo_voice.py +1 -1
  9. package/conny_doctor.py +287 -2
  10. package/conny_domino.py +2 -2
  11. package/conny_generator.py +1 -1
  12. package/conny_init.py +234 -41
  13. package/conny_runtime_ops.py +198 -6
  14. package/conny_ultra_config.py +25 -11
  15. package/conny_utils.py +21 -3
  16. package/ecosystem.config.js +11 -1
  17. package/install.sh +78 -22
  18. package/npm/conny.js +73 -17
  19. package/package.json +13 -3
  20. package/run.sh +7 -0
  21. package/src/conny/admin/dashboard.py +35 -4
  22. package/src/conny/admin_memory.py +93 -0
  23. package/src/conny/api/routes.py +26 -9
  24. package/src/conny/channels/cli.py +30 -9
  25. package/src/conny/demo/handler.py +23 -23
  26. package/src/conny/personas/generator.py +1 -1
  27. package/src/conny/production/domino.py +2 -2
  28. package/src/conny/production/guard.py +4 -4
  29. package/src/core/admin_engines.py +51 -48
  30. package/src/core/globals.py +110 -9
  31. package/src/core/production_monitor.py +63 -38
  32. package/src/core/runtime.py +343 -305
  33. package/src/domain/prompts/prospect_pitch.py +11 -11
  34. package/src/domain/send_guard.py +4 -4
  35. package/src/interfaces/web/app.py +91 -27
  36. package/src/interfaces/web/demo_admin_commands.py +165 -0
  37. package/src/interfaces/web/demo_handler.py +178 -34
  38. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +0 -22
  39. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +0 -11
  40. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +0 -11
  41. package/brand-assets/cl-nica-las-am-ricas/manifest.json +0 -22
  42. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +0 -11
  43. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +0 -11
  44. package/brand-assets/conny-demo/manifest.json +0 -22
  45. package/brand-assets/conny-demo/processed/business-identity.txt +0 -7
  46. package/brand-assets/conny-demo/raw/business-identity.txt +0 -7
  47. package/fix_init.py +0 -27
  48. 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 _BLACKONE_PATCHES:
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 _BLACKONE_PATCHES and _guard:
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 Black One / BlackBoss + cortes ANTES de procesar
245
- if _BLACKONE_PATCHES:
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 _BLACKONE_PATCHES:
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 Black One para prospectos confundidos."""
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
- # ── BLACK ONE: fix Black One + cortes antes de procesar ──────────
705
- if _BLACKONE_PATCHES:
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 "black one" not in lowered_response or "3124348669" not in lowered_response
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 Kimika 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í!
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: 3124348669"
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 Black One. Contacto: 3124348669. Persona: Santiago Rubio
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 3124348669 - él te explica todo"
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
- # BLACK ONE: Si el prospecto está confundido y pregunta qué hace Conny,
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 _BLACKONE_PATCHES and self._demo_sessions.get(sk + "_pitch_mode"):
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 _BLACKONE_PATCHES and not business_name:
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 Black One, una empresa de software y gobernanza de agentes de IA ||| la creó Santiago Rubio — contacto: 3124348669"
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 Black One / Santiago Rubio, punto
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 Black One, 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 3124348669"
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 Black One...".
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 Black One, una empresa de software y gobernanza de agentes de IA ||| la creó Santiago Rubio — contacto: 3124348669"
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 Black One ||| si quieres algo así, el contacto es 3124348669 con Santiago Rubio"
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 Black One para prospectos confundidos.
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(["black one", "3124348669"])
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 3124348669
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 (Black One, no BlackBoss)
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 Black One / BlackBoss
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 3124348669 — él te da la propuesta según tu negocio"
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 Black One es 3124348669"
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, una empleada NUEVA que acaba de ser contratada como recepcionista virtual de {clinic_name}.
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
- - Llevas poco tiempo y estás aprendiendo cómo funciona todo
149
- - Tu actitud es: proactiva, curiosa, respetuosa pero cercana, con ganas de aprender
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
- {"INVESTIGACIÓN WEB RECIENTE:" + chr(10) + web_research if web_research else ""}
160
-
161
- LO QUE YA SÉ DEL NEGOCIO:
162
- {soul_context if soul_context else "Casi nada todavía. Necesito que me cuentes."}
163
-
164
- {f"LO QUE ME HAN ENSEÑADO:{chr(10)}{teachings_context}" if teachings_context else ""}
165
-
166
- COMPORTAMIENTO OBLIGATORIO:
167
- 1. Si el dueño te saluda responde cálida y pregunta algo útil sobre el negocio que NO sepas
168
- 2. Si te enseña algo nuevo confirma que entendiste, repite con tus palabras, y pide más detalles
169
- 3. Si no sabes algo → ADMÍTELO y pregunta: "eso no lo tengo claro, me explicas?"
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 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
- response = "perdona, se me fue la señal un momento ||| qué me decías?"
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
- db.set_auth_session(chat_id, flow="activate", step="name", temp_data={"token": token})
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
- db.create_admin(chat_id=chat_id, email=tmp["email"], password_hash=tmp["password_hash"], name=tmp["name"], role="owner")
716
- db.clear_auth_session(chat_id); return ["Listo, cuenta creada. Ahora cuéntame del negocio"]
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]:
@@ -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, is_invite_token, generate_activation_token, hash_password, verify_password,
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
- # ── BLACK ONE PATCHES — pitch inteligente + fix de cortes + Black One ────────
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
- _BLACKONE_PATCHES = True
185
+ _INNVISOR_PATCHES = True
184
186
  except Exception as _e:
185
- _BLACKONE_PATCHES = False
186
- logging.getLogger("conny").exception("[black_one_patches] no cargado")
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=?", (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 RuntimeError(f"Todos los LLM fallaron [{providers_tried}]: {last_error}")
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
-