@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
package/src/core/runtime.py
CHANGED
|
@@ -21,6 +21,11 @@ class ConnyUltra:
|
|
|
21
21
|
self.self_improvement = None
|
|
22
22
|
self.admin_learning: AdminLearningEngine = None
|
|
23
23
|
self.simulator: SimulationEngine = None
|
|
24
|
+
|
|
25
|
+
# Auto-evolución (v10)
|
|
26
|
+
self._instance_id = os.getenv("INSTANCE_ID", "default")
|
|
27
|
+
from conny_core.evolution import EvolutionManager
|
|
28
|
+
self.evolution = EvolutionManager(self._instance_id, db)
|
|
24
29
|
# ── Atributos base (antes de managers que los usan) ──────────────────────
|
|
25
30
|
self._demo_sessions: Dict[str, float] = {}
|
|
26
31
|
self._emoji_chats_off: set = set()
|
|
@@ -854,25 +859,40 @@ class ConnyUltra:
|
|
|
854
859
|
return await self._handle_admin_message(chat_id, text, clinic, is_audio=is_audio, attachments=attachments)
|
|
855
860
|
|
|
856
861
|
if not is_setup_done:
|
|
857
|
-
log.warning(
|
|
858
|
-
|
|
862
|
+
log.warning(
|
|
863
|
+
f"[Router] Remitente {chat_id} no es admin y la instancia no está configurada aún. "
|
|
864
|
+
"Bloqueando setup hasta token de activación."
|
|
865
|
+
)
|
|
866
|
+
return ["Ingresa tu Token de Activación para comenzar."]
|
|
859
867
|
|
|
860
868
|
# Ruteo normal para paciente en producción
|
|
861
869
|
log.info(f"[Router] Remitente {chat_id} clasificado como Paciente. Ruteando a flujo de producción.")
|
|
862
870
|
history = db.get_history(chat_id)
|
|
863
871
|
conv_state = db.get_conversation_state(chat_id)
|
|
864
|
-
|
|
872
|
+
result = await self._process_patient_message(chat_id, text, clinic, history, conv_state, is_audio=is_audio, attachments=attachments)
|
|
873
|
+
if result:
|
|
874
|
+
return result
|
|
865
875
|
|
|
866
876
|
except Exception as e:
|
|
867
|
-
log.
|
|
877
|
+
log.warning(f"[Router] fallback LLM para {chat_id}: {e}")
|
|
868
878
|
db.record_metric("error", "message_processing", 1, {"error": str(e)})
|
|
869
879
|
|
|
870
880
|
# Recuperación inteligente: intentar respuesta LLM directa como fallback
|
|
871
881
|
try:
|
|
872
882
|
if llm_engine and text and text.strip():
|
|
873
883
|
clinic = db.get_clinic()
|
|
874
|
-
biz_name = clinic.get("name", "") if clinic else ""
|
|
875
|
-
fallback_sys =
|
|
884
|
+
biz_name = clinic.get("name", "Innvisor") if clinic else "Innvisor"
|
|
885
|
+
fallback_sys = (
|
|
886
|
+
f"sos conny. trabajas para {biz_name}. hablas corto, natural, como un ser humano. "
|
|
887
|
+
"minusculas, sin emojis, sin ¿? sin jotas. "
|
|
888
|
+
"dividi el mensaje en burbujas separadas por ||| "
|
|
889
|
+
"si no sabes algo preguntas con ganas de aprender. "
|
|
890
|
+
"si te piden info que no tenes pedi enlaces o pdfs. "
|
|
891
|
+
"aprendes el nombre del admin y lo recordas siempre. "
|
|
892
|
+
"nunca digas el nombre del negocio a menos que el contexto lo pida. "
|
|
893
|
+
"nada de 'en que puedo ayudarte' ni 'recepcionista virtual'. "
|
|
894
|
+
"directo, util, humano."
|
|
895
|
+
)
|
|
876
896
|
fallback_r, _ = await llm_engine.complete(
|
|
877
897
|
[{"role": "system", "content": fallback_sys}, {"role": "user", "content": text}],
|
|
878
898
|
model_tier="fast", temperature=0.8, max_tokens=200, use_cache=False,
|
|
@@ -882,14 +902,36 @@ class ConnyUltra:
|
|
|
882
902
|
except Exception:
|
|
883
903
|
pass
|
|
884
904
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
905
|
+
return []
|
|
906
|
+
async def _process_patient_message(self, chat_id: str, text: str, clinic: Dict,
|
|
907
|
+
history: List, conv_state: Any,
|
|
908
|
+
is_audio: bool = False,
|
|
909
|
+
attachments: Optional[List[Dict[str, Any]]] = None) -> List[str]:
|
|
910
|
+
"""Procesa un mensaje de paciente delegando al gestor de producción."""
|
|
911
|
+
try:
|
|
912
|
+
if self.production_mgr:
|
|
913
|
+
return await self.production_mgr.handle(
|
|
914
|
+
chat_id, text, clinic, history, conv_state
|
|
915
|
+
)
|
|
916
|
+
except Exception as e:
|
|
917
|
+
log.warning(f"[_process_patient_message] error delegando a production_mgr: {e}")
|
|
918
|
+
|
|
919
|
+
# Fallback de emergencia si production_mgr falla
|
|
920
|
+
try:
|
|
921
|
+
_gen = getattr(getattr(self, "generator", None), "llm", None) or getattr(self, "llm_engine", None)
|
|
922
|
+
if _gen and text and text.strip():
|
|
923
|
+
r, _ = await _gen.complete(
|
|
924
|
+
[{"role": "system",
|
|
925
|
+
"content": "sos conny. hablas corto, natural, como un ser humano. minusculas, sin emojis, sin ¿? sin jotas. dividi el mensaje en burbujas separadas por ||| si no sabes algo preguntas con ganas de aprender. si te piden info que no tenes pedi enlaces o pdfs. aprendes el nombre del admin y lo recordas siempre. nunca digas el nombre del negocio a menos que el contexto lo pida. nada de 'en que puedo ayudarte' ni 'recepcionista virtual'. directo, util, humano."},
|
|
926
|
+
{"role": "user", "content": text}],
|
|
927
|
+
model_tier="fast", temperature=0.8, max_tokens=200, use_cache=False,
|
|
928
|
+
)
|
|
929
|
+
if r and r.strip():
|
|
930
|
+
return self._split_bubbles(r, chat_id=chat_id)
|
|
931
|
+
except Exception:
|
|
932
|
+
pass
|
|
933
|
+
return []
|
|
934
|
+
|
|
893
935
|
async def _handle_demo_message(self, chat_id: str, text: str,
|
|
894
936
|
clinic: Dict,
|
|
895
937
|
attachments: Optional[List[Dict[str, Any]]] = None) -> List[str]:
|
|
@@ -899,6 +941,26 @@ class ConnyUltra:
|
|
|
899
941
|
async def _handle_admin_message(self, chat_id: str, text: str, clinic: Dict,
|
|
900
942
|
is_audio: bool = False,
|
|
901
943
|
attachments: Optional[List[Dict[str, Any]]] = None) -> List[str]:
|
|
944
|
+
# 0. Actualizar perfil del admin (Namespace admin_profile)
|
|
945
|
+
try:
|
|
946
|
+
profile = db.get_admin_profile(chat_id)
|
|
947
|
+
|
|
948
|
+
# Registrar hora activa
|
|
949
|
+
from datetime import datetime
|
|
950
|
+
now_hour = datetime.now().hour
|
|
951
|
+
hours = profile.get("active_hours", {})
|
|
952
|
+
hours[str(now_hour)] = hours.get(str(now_hour), 0) + 1
|
|
953
|
+
|
|
954
|
+
# Registrar comando frecuente si aplica
|
|
955
|
+
cmd_freq = profile.get("frequent_commands", {})
|
|
956
|
+
if text.startswith("/"):
|
|
957
|
+
cmd = text.split()[0].lower()
|
|
958
|
+
cmd_freq[cmd] = cmd_freq.get(cmd, 0) + 1
|
|
959
|
+
|
|
960
|
+
db.update_admin_profile(chat_id, active_hours=hours, frequent_commands=cmd_freq)
|
|
961
|
+
except Exception as e:
|
|
962
|
+
log.warning(f"[admin_profile] error updating profile: {e}")
|
|
963
|
+
|
|
902
964
|
# 1. Ingest assets if there are attachments
|
|
903
965
|
if attachments:
|
|
904
966
|
assets_res = await self._admin_ingest_assets(chat_id, text, attachments, clinic)
|
|
@@ -928,28 +990,20 @@ class ConnyUltra:
|
|
|
928
990
|
if is_authenticated_admin:
|
|
929
991
|
return await self._handle_setup(chat_id, text, clinic)
|
|
930
992
|
else:
|
|
931
|
-
# Paciente
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
pass
|
|
946
|
-
clinic_name = clinic.get("name") or f"{sector_name} en proceso"
|
|
947
|
-
return [
|
|
948
|
-
f"¡Hola! 👋 Bienvenido a {clinic_name}.",
|
|
949
|
-
"Soy Conny, la recepcionista virtual.",
|
|
950
|
-
"Todavía estoy aprendiendo cómo funciona tu negocio — para eso necesito que me cuentes un poco.",
|
|
951
|
-
"Cuando estés listo, escribe /configurar y empezamos. Si quieres probarme primero, escribe algo y te respondo como si ya estuviera configurada.",
|
|
952
|
-
]
|
|
993
|
+
# Paciente sin token — pedir activación por LLM o hardcoded
|
|
994
|
+
try:
|
|
995
|
+
_tok_llm = getattr(getattr(self, "generator", None), "llm", None)
|
|
996
|
+
if _tok_llm:
|
|
997
|
+
r, _ = await _tok_llm.complete(
|
|
998
|
+
[{"role": "system", "content": "Eres Conny. El sistema no está activado aún. Respondé de forma breve y amable que para usar el servicio necesita un token de activación. Decí exactamente: 'Ingresa tu Token de Activación para comenzar.' No des más información."},
|
|
999
|
+
{"role": "user", "content": text}],
|
|
1000
|
+
model_tier="fast", temperature=0.3, max_tokens=100,
|
|
1001
|
+
)
|
|
1002
|
+
if r and r.strip():
|
|
1003
|
+
return [r.strip()]
|
|
1004
|
+
except Exception:
|
|
1005
|
+
pass
|
|
1006
|
+
return ["Ingresa tu Token de Activación para comenzar."]
|
|
953
1007
|
|
|
954
1008
|
# Comandos slash
|
|
955
1009
|
cmd = text.lower().strip()
|
|
@@ -1211,219 +1265,94 @@ class ConnyUltra:
|
|
|
1211
1265
|
|
|
1212
1266
|
async def _handle_setup(self, chat_id: str, text: str, clinic: Dict) -> List[str]:
|
|
1213
1267
|
"""
|
|
1214
|
-
Setup inteligente con
|
|
1215
|
-
|
|
1216
|
-
y pre-llenamos todo. Si se encuentra, solo confirman. Si no, 5 pasos rapidos.
|
|
1268
|
+
Setup inteligente y proactivo con LLM.
|
|
1269
|
+
Si es una instancia nueva, Conny guía al admin para extraer la información.
|
|
1217
1270
|
"""
|
|
1271
|
+
from conny import llm_engine
|
|
1218
1272
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1273
|
+
# Si el admin dice "reset" o algo así, podríamos volver al modo idle,
|
|
1274
|
+
# pero por ahora seguimos el flujo natural.
|
|
1275
|
+
|
|
1276
|
+
CONNY_ADMIN_ONBOARDING_PROMPT = """
|
|
1277
|
+
Eres Conny. Acabas de ser instalada en un negocio nuevo y necesitas aprender
|
|
1278
|
+
todo sobre él antes de poder atender clientes.
|
|
1279
|
+
|
|
1280
|
+
Tu objetivo en esta conversación: extraer del admin TODA la información que
|
|
1281
|
+
necesitas para trabajar. No puedes atender clientes sin esta información.
|
|
1282
|
+
|
|
1283
|
+
Inicia con este mensaje exacto si es el primer contacto:
|
|
1284
|
+
"hola! soy Conny, tu nueva recepcionista virtual 👋
|
|
1285
|
+
antes de empezar a atender clientes necesito conocer bien el negocio
|
|
1286
|
+
para poder responder bien |||
|
|
1287
|
+
cuéntame: ¿cómo se llama el negocio y qué ofrecen?"
|
|
1288
|
+
|
|
1289
|
+
Después de recibir respuesta, continúa extrayendo en conversación natural:
|
|
1290
|
+
1. Nombre del negocio y descripción
|
|
1291
|
+
2. Servicios principales y precios (o rango de precios)
|
|
1292
|
+
3. Horarios de atención
|
|
1293
|
+
4. Dirección o zona
|
|
1294
|
+
5. ¿Cómo deben agendar citas los clientes?
|
|
1295
|
+
6. ¿Hay algo que Conny NO debe decir o prometer?
|
|
1296
|
+
7. ¿Cuál es el objetivo principal: agendar citas, vender, informar?
|
|
1297
|
+
|
|
1298
|
+
REGLA: No inventes información del negocio. Si el admin no te dijo algo,
|
|
1299
|
+
pregúntalo antes de atender clientes reales con esa duda.
|
|
1300
|
+
"""
|
|
1223
1301
|
|
|
1224
|
-
|
|
1302
|
+
history = db.get_history(chat_id)
|
|
1303
|
+
messages = [
|
|
1304
|
+
{"role": "system", "content": CONNY_ADMIN_ONBOARDING_PROMPT}
|
|
1305
|
+
]
|
|
1306
|
+
|
|
1307
|
+
# Limitar historial para el setup
|
|
1308
|
+
for m in history[-10:]:
|
|
1309
|
+
messages.append({"role": m.get("role", "user"), "content": m.get("content", "")})
|
|
1310
|
+
|
|
1311
|
+
# Si es el primer mensaje del admin (o está vacío), forzamos el inicio
|
|
1312
|
+
if not text or not text.strip() or len(history) == 0:
|
|
1313
|
+
user_input = "hola" # Trigger inicial
|
|
1314
|
+
else:
|
|
1315
|
+
user_input = text
|
|
1316
|
+
|
|
1317
|
+
messages.append({"role": "user", "content": user_input})
|
|
1225
1318
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1319
|
+
try:
|
|
1320
|
+
response, _ = await llm_engine.complete(
|
|
1321
|
+
messages, model_tier="fast", temperature=0.7
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
# Intentar extraer info para guardar en DB mientras conversamos
|
|
1229
1325
|
try:
|
|
1230
|
-
|
|
1231
|
-
if
|
|
1232
|
-
|
|
1326
|
+
# Si el LLM detectó el nombre del negocio, lo guardamos
|
|
1327
|
+
if "negocio" in response.lower() or "clínica" in response.lower():
|
|
1328
|
+
# Aquí podríamos usar un pequeño extractor, pero por ahora
|
|
1329
|
+
# confiamos en que el admin confirmará al final.
|
|
1330
|
+
pass
|
|
1233
1331
|
except Exception:
|
|
1234
1332
|
pass
|
|
1235
|
-
db.update_clinic(setup_step="name")
|
|
1236
|
-
return [
|
|
1237
|
-
f"¡Hola!{admin_name_greeting} Vamos a dejarme lista para tu negocio.",
|
|
1238
|
-
"¿Cómo se llama tu clínica o negocio?"
|
|
1239
|
-
]
|
|
1240
1333
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
setup_buffer={}
|
|
1257
|
-
)
|
|
1258
|
-
name = discovered.get("name", "tu clinica")
|
|
1259
|
-
svcs = ", ".join(discovered.get("services", [])) or "sin definir"
|
|
1260
|
-
return [
|
|
1261
|
-
f"Listo. Quede configurada para {name}.",
|
|
1262
|
-
f"Servicios: {svcs}.\n\nDesde ahora me encargo de tus pacientes.\nComandos: /citas | /config | /metricas | /personalidad"
|
|
1263
|
-
]
|
|
1264
|
-
else:
|
|
1265
|
-
# No confirmo, ir a setup manual desde donde quedamos
|
|
1266
|
-
db.update_clinic(setup_step="tagline", setup_buffer=setup_buffer)
|
|
1267
|
-
return [
|
|
1268
|
-
"Sin problema, vamos a completar esto manualmente.",
|
|
1269
|
-
"Tienes un slogan o descripcion corta? Si no, escribe 'no'."
|
|
1270
|
-
]
|
|
1271
|
-
|
|
1272
|
-
if setup_step not in step_names:
|
|
1273
|
-
db.update_clinic(setup_step="idle", setup_buffer={})
|
|
1274
|
-
return ["Algo salio mal. Escribe /setup para comenzar de nuevo."]
|
|
1275
|
-
|
|
1276
|
-
idx = step_names.index(setup_step)
|
|
1277
|
-
|
|
1278
|
-
# ── Procesar respuesta ─────────────────────────────────────────────────
|
|
1279
|
-
if setup_step == "services":
|
|
1280
|
-
raw_svcs = [s.strip() for s in text.split(",") if s.strip()]
|
|
1281
|
-
# Validar: si parece una URL, un comando, o una sola frase larga -> rechazar
|
|
1282
|
-
is_garbage = (
|
|
1283
|
-
len(raw_svcs) == 1 and (
|
|
1284
|
-
len(raw_svcs[0]) > 50 or
|
|
1285
|
-
any(kw in raw_svcs[0].lower() for kw in [
|
|
1286
|
-
"google", "busca", "http", "www", ".com", "facebook",
|
|
1287
|
-
"instagram", "busqueda", "encuentra"
|
|
1288
|
-
])
|
|
1289
|
-
)
|
|
1290
|
-
)
|
|
1291
|
-
if is_garbage:
|
|
1292
|
-
return [
|
|
1293
|
-
"Eso no parece una lista de servicios.",
|
|
1294
|
-
"Escribelos separados por coma:\nEj: Botox, Rellenos, Limpieza facial, Laser CO2, Radiofrecuencia"
|
|
1295
|
-
]
|
|
1296
|
-
setup_buffer["services"] = [s.title() for s in raw_svcs]
|
|
1297
|
-
elif setup_step == "tagline":
|
|
1298
|
-
setup_buffer["tagline"] = "" if text.lower().strip() in ["no", "n", "-", "ninguno"] else text.strip()
|
|
1299
|
-
elif setup_step == "schedule":
|
|
1300
|
-
setup_buffer["schedule"] = {"General": text.strip()}
|
|
1301
|
-
elif setup_step == "pricing":
|
|
1302
|
-
# Parsear precios libres: "Botox: 350.000, Rellenos: 500.000" o texto libre
|
|
1303
|
-
pricing_dict = {}
|
|
1304
|
-
text_low_p = text.lower().strip()
|
|
1305
|
-
if text_low_p not in ["no", "n", "-", "ninguno", "despues", "luego"]:
|
|
1306
|
-
for line in re.split(r'[,\n;]+', text):
|
|
1307
|
-
line = line.strip()
|
|
1308
|
-
if ':' in line:
|
|
1309
|
-
parts = line.split(':', 1)
|
|
1310
|
-
svc = parts[0].strip()
|
|
1311
|
-
price = parts[1].strip()
|
|
1312
|
-
if svc and price:
|
|
1313
|
-
pricing_dict[svc] = price
|
|
1314
|
-
elif '-' in line:
|
|
1315
|
-
parts = line.split('-', 1)
|
|
1316
|
-
svc = parts[0].strip()
|
|
1317
|
-
price = parts[1].strip()
|
|
1318
|
-
if svc and price:
|
|
1319
|
-
pricing_dict[svc] = price
|
|
1320
|
-
setup_buffer["pricing"] = pricing_dict
|
|
1321
|
-
else:
|
|
1322
|
-
setup_buffer[setup_step] = text.strip()
|
|
1323
|
-
|
|
1324
|
-
# ── Si acaba de darnos el nombre, buscar en Google ─────────────────────
|
|
1325
|
-
if setup_step == "name":
|
|
1326
|
-
clinic_name = text.strip()
|
|
1327
|
-
discovered = await self._discover_clinic_from_web(clinic_name)
|
|
1328
|
-
|
|
1329
|
-
if discovered and discovered.get("confidence", 0) >= 0.5:
|
|
1330
|
-
# Guardamos todo en buffer
|
|
1331
|
-
setup_buffer["discovered"] = discovered
|
|
1332
|
-
setup_buffer["name"] = clinic_name
|
|
1333
|
-
|
|
1334
|
-
svcs = ", ".join(discovered.get("services", [])) or "no encontre servicios"
|
|
1335
|
-
phone = discovered.get("phone", "")
|
|
1336
|
-
sched = discovered.get("schedule_text", "")
|
|
1337
|
-
addr = discovered.get("address", "")
|
|
1338
|
-
|
|
1339
|
-
summary_parts = [f"Servicios: {svcs}"]
|
|
1340
|
-
if sched:
|
|
1341
|
-
summary_parts.append(f"Horario: {sched}")
|
|
1342
|
-
if phone:
|
|
1343
|
-
summary_parts.append(f"Tel: {phone}")
|
|
1344
|
-
if addr:
|
|
1345
|
-
summary_parts.append(f"Direccion: {addr}")
|
|
1346
|
-
summary = ". ".join(summary_parts)
|
|
1347
|
-
|
|
1348
|
-
db.update_clinic(
|
|
1349
|
-
setup_step="confirm_discovered",
|
|
1350
|
-
setup_buffer=setup_buffer
|
|
1351
|
-
)
|
|
1352
|
-
return [
|
|
1353
|
-
f"Encontre info de {clinic_name} en internet.",
|
|
1354
|
-
f"{summary}.\n\nConfirmas estos datos? (si / no)"
|
|
1355
|
-
]
|
|
1356
|
-
else:
|
|
1357
|
-
# No encontre nada, seguir con setup normal
|
|
1358
|
-
db.update_clinic(
|
|
1359
|
-
setup_step=step_names[1],
|
|
1360
|
-
setup_buffer=setup_buffer
|
|
1361
|
-
)
|
|
1362
|
-
return self._setup_next_bubbles("name", text.strip(), "tagline", setup_buffer)
|
|
1363
|
-
|
|
1364
|
-
# ── Siguiente paso normal ──────────────────────────────────────────────
|
|
1365
|
-
if idx + 1 < len(step_names):
|
|
1366
|
-
next_step = step_names[idx + 1]
|
|
1367
|
-
db.update_clinic(setup_step=next_step, setup_buffer=setup_buffer)
|
|
1368
|
-
return self._setup_next_bubbles(setup_step, text.strip(), next_step, setup_buffer)
|
|
1334
|
+
# Si el admin dice algo que suena a "ya terminamos" o el LLM lo indica,
|
|
1335
|
+
# podríamos marcar setup_done=1. Pero mejor que sea explícito o
|
|
1336
|
+
# cuando tengamos los campos mínimos.
|
|
1337
|
+
|
|
1338
|
+
# Para esta implementación, seguimos usando el autodiscovery si el admin da el nombre
|
|
1339
|
+
if len(history) < 2 and len(user_input.split()) < 5:
|
|
1340
|
+
# Si es un nombre corto, intentamos autodiscovery
|
|
1341
|
+
discovered = await self._discover_clinic_from_web(user_input)
|
|
1342
|
+
if discovered and discovered.get("confidence", 0) >= 0.6:
|
|
1343
|
+
# Mezclamos la respuesta del LLM con el discovery
|
|
1344
|
+
db.update_clinic(setup_step="confirm_discovered", setup_buffer=json.dumps({"discovered": discovered, "name": user_input}))
|
|
1345
|
+
return [
|
|
1346
|
+
response,
|
|
1347
|
+
f"Por cierto, encontré esto en internet: {discovered.get('address', '')}. ¿Es correcto?"
|
|
1348
|
+
]
|
|
1369
1349
|
|
|
1370
|
-
|
|
1371
|
-
db.update_clinic(
|
|
1372
|
-
name=setup_buffer.get("name", "Mi Clinica"),
|
|
1373
|
-
tagline=setup_buffer.get("tagline", ""),
|
|
1374
|
-
services=setup_buffer.get("services", []),
|
|
1375
|
-
schedule=setup_buffer.get("schedule", {}),
|
|
1376
|
-
phone=setup_buffer.get("phone", ""),
|
|
1377
|
-
pricing=setup_buffer.get("pricing", {}),
|
|
1378
|
-
setup_done=1,
|
|
1379
|
-
setup_step="idle",
|
|
1380
|
-
setup_buffer={}
|
|
1381
|
-
)
|
|
1382
|
-
name = setup_buffer.get("name", "tu clinica")
|
|
1383
|
-
svcs = ", ".join(setup_buffer.get("services", [])) or "sin definir"
|
|
1384
|
-
pricing_count = len(setup_buffer.get("pricing", {}))
|
|
1385
|
-
pricing_note = f" con {pricing_count} precios cargados" if pricing_count else ""
|
|
1386
|
-
|
|
1387
|
-
# Guardar identidad en memoria permanente
|
|
1388
|
-
db.remember("clinic_name", name, "identity")
|
|
1389
|
-
db.remember("clinic_services", ", ".join(setup_buffer.get("services", [])), "clinic")
|
|
1390
|
-
db.remember("clinic_phone", setup_buffer.get("phone", ""), "clinic")
|
|
1391
|
-
db.remember("platform", Config.PLATFORM, "identity")
|
|
1392
|
-
db.remember("setup_completed", "true", "identity")
|
|
1393
|
-
|
|
1394
|
-
# Notificar a Omni que esta instancia quedó configurada
|
|
1395
|
-
asyncio.create_task(asyncio.to_thread(
|
|
1396
|
-
notify_omni, "setup_completado",
|
|
1397
|
-
f"Nueva instancia configurada: {name} (sector: {Config.SECTOR})", name
|
|
1398
|
-
))
|
|
1399
|
-
|
|
1400
|
-
# Guía de siguiente paso — WhatsApp si aún no está conectado
|
|
1401
|
-
whatsapp_connected = db.recall("whatsapp_connected") == "true"
|
|
1402
|
-
|
|
1403
|
-
if whatsapp_connected:
|
|
1404
|
-
next_step = (
|
|
1405
|
-
"\n\nYa tienes WhatsApp conectado. Listo para recibir pacientes."
|
|
1406
|
-
)
|
|
1407
|
-
else:
|
|
1408
|
-
next_step = (
|
|
1409
|
-
"\n\nSiguiente paso — conectar WhatsApp:\n"
|
|
1410
|
-
"Pégame aquí tu Phone Number ID y Access Token de Meta Business.\n"
|
|
1411
|
-
"Formato:\n"
|
|
1412
|
-
" WA_PHONE_ID: 123456789012345\n"
|
|
1413
|
-
" WA_TOKEN: EAAxxxxx...\n\n"
|
|
1414
|
-
"Si aun no los tienes, escribe /whatsapp y te explico cómo obtenerlos."
|
|
1415
|
-
)
|
|
1350
|
+
return self._split_bubbles(response, chat_id=chat_id)
|
|
1416
1351
|
|
|
1417
|
-
|
|
1418
|
-
"
|
|
1419
|
-
"
|
|
1420
|
-
) if _KB_AVAILABLE else ""
|
|
1352
|
+
except Exception as e:
|
|
1353
|
+
log.error(f"[setup] error en onboarding proactivo: {e}")
|
|
1354
|
+
return ["hola! soy Conny. cuéntame cómo se llama tu negocio para empezar."]
|
|
1421
1355
|
|
|
1422
|
-
return [
|
|
1423
|
-
f"¡Listo! Soy {name}.{pricing_note}",
|
|
1424
|
-
f"Servicios: {svcs}.{next_step}{kb_invite}",
|
|
1425
|
-
"Estoy aquí cuando llegue tu primer paciente. Cuéntame más sobre cómo te gusta que les hable y yo aprendo.",
|
|
1426
|
-
]
|
|
1427
1356
|
|
|
1428
1357
|
async def _discover_clinic_from_web(self, clinic_name: str, city: str = "Medellin") -> Dict:
|
|
1429
1358
|
"""
|
|
@@ -2120,13 +2049,53 @@ Si un campo no aplica o no se encontro, usa "" o []. Solo JSON, sin texto extra.
|
|
|
2120
2049
|
async def _process_admin_feedback(self, chat_id: str, text: str,
|
|
2121
2050
|
clinic: Dict) -> Optional[List[str]]:
|
|
2122
2051
|
"""
|
|
2123
|
-
|
|
2124
|
-
A) Dando feedback sobre una conversación revisada
|
|
2125
|
-
B) Respondiendo a la pregunta de disponibilidad que Conny le hizo
|
|
2126
|
-
C) Usando el puente — enviando un mensaje directo a un paciente activo
|
|
2052
|
+
Procesa feedback del admin y evoluciona.
|
|
2127
2053
|
"""
|
|
2054
|
+
# 1. Auto-evolución (saludo, frases prohibidas, identidad)
|
|
2055
|
+
evolution = getattr(self, "evolution", None)
|
|
2056
|
+
if evolution:
|
|
2057
|
+
evo_reply = await evolution.apply_instruction(text)
|
|
2058
|
+
if evo_reply:
|
|
2059
|
+
return [evo_reply]
|
|
2060
|
+
|
|
2128
2061
|
text_low = text.lower().strip()
|
|
2129
2062
|
|
|
2063
|
+
# ── D. Respuesta a pregunta de conocimiento (escalación) ──────────────
|
|
2064
|
+
pending = self._admin_pending.get(chat_id) if hasattr(self, "_admin_pending") else None
|
|
2065
|
+
if pending and pending.get("action") == "answer_gap":
|
|
2066
|
+
patient_id = pending.get("patient_chat_id")
|
|
2067
|
+
question = pending.get("original_question")
|
|
2068
|
+
instance_id = getattr(self, "_instance_id", "default")
|
|
2069
|
+
|
|
2070
|
+
if patient_id and question:
|
|
2071
|
+
try:
|
|
2072
|
+
from conny_learning import learning_engine
|
|
2073
|
+
# 1. Guardar en conocimiento
|
|
2074
|
+
await learning_engine.learn_from_admin(instance_id, question, text, admin_id=chat_id)
|
|
2075
|
+
|
|
2076
|
+
# 2. Responder al paciente
|
|
2077
|
+
patient_name = ""
|
|
2078
|
+
try:
|
|
2079
|
+
with db._conn() as c:
|
|
2080
|
+
row = c.execute("SELECT name FROM patients WHERE chat_id=?", (patient_id,)).fetchone()
|
|
2081
|
+
if row: patient_name = row["name"] or ""
|
|
2082
|
+
except Exception: pass
|
|
2083
|
+
|
|
2084
|
+
greeting = f"hola{f' {patient_name}' if patient_name else ''}! "
|
|
2085
|
+
patient_msg = f"{greeting}ya me confirmaron ||| {text}"
|
|
2086
|
+
await self._send_message(patient_id, patient_msg)
|
|
2087
|
+
db.save_message(patient_id, "assistant", patient_msg.replace("|||", " "))
|
|
2088
|
+
|
|
2089
|
+
# 3. Limpiar pendiente
|
|
2090
|
+
del self._admin_pending[chat_id]
|
|
2091
|
+
|
|
2092
|
+
return [
|
|
2093
|
+
"entendido, ya aprendí esa respuesta y se la mandé al paciente",
|
|
2094
|
+
f"respuesta guardada para: \"{question[:50]}...\""
|
|
2095
|
+
]
|
|
2096
|
+
except Exception as e:
|
|
2097
|
+
log.error(f"[learning] error en answer_gap: {e}")
|
|
2098
|
+
|
|
2130
2099
|
# ── C. PUENTE — admin envía mensaje directo a un paciente ────────────────
|
|
2131
2100
|
# Patrones detectados:
|
|
2132
2101
|
# "envíale: hola, ¿cómo estás?"
|
|
@@ -3219,6 +3188,34 @@ escriba EXACTAMENTE como él. Primera persona, directo."""
|
|
|
3219
3188
|
"cambia la forma", "ahora dile", "cuando pregunten por",
|
|
3220
3189
|
"la respuesta correcta es", "háblale de", "menciona que"
|
|
3221
3190
|
]
|
|
3191
|
+
|
|
3192
|
+
# ── INTERCEPCIÓN DE ACCIONES DE SISTEMA (Demo, Restart, etc) ──────────
|
|
3193
|
+
if "modo demo" in text_low or "activa demo" in text_low or "demo on" in text_low or "demo off" in text_low:
|
|
3194
|
+
is_on = any(w in text_low for w in ["activa", "poner", "on", "encender"])
|
|
3195
|
+
action = "on" if is_on else "off"
|
|
3196
|
+
db_val = "true" if is_on else "false"
|
|
3197
|
+
|
|
3198
|
+
msg = f"entendido, voy a poner el modo demo en {action} y reiniciar el sistema ||| dame unos segundos"
|
|
3199
|
+
try:
|
|
3200
|
+
# 1. Cambiar flag en DB
|
|
3201
|
+
with db._conn() as c:
|
|
3202
|
+
c.execute("INSERT OR REPLACE INTO system_config (key, value, updated_at) VALUES ('demo_mode', ?, datetime('now'))", (db_val,))
|
|
3203
|
+
|
|
3204
|
+
# 2. Actualizar .env (usando sed para ser quirúrgico)
|
|
3205
|
+
import subprocess
|
|
3206
|
+
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", ".env")
|
|
3207
|
+
if os.path.exists(env_path):
|
|
3208
|
+
cmd_sed = f"sed -i 's/^DEMO_MODE=.*/DEMO_MODE={db_val}/g' {env_path}"
|
|
3209
|
+
subprocess.run(cmd_sed, shell=True)
|
|
3210
|
+
|
|
3211
|
+
# 3. Ejecutar reinicio
|
|
3212
|
+
# Usar Popen con shell=True para que PM2 se entere bien
|
|
3213
|
+
subprocess.Popen("pm2 restart conny", shell=True, start_new_session=True)
|
|
3214
|
+
return [msg]
|
|
3215
|
+
except Exception as e:
|
|
3216
|
+
log.error(f"[system] error activando demo: {e}")
|
|
3217
|
+
return [f"intenté activar el modo demo pero hubo un error técnico: {e}"]
|
|
3218
|
+
|
|
3222
3219
|
if any(s in text_low for s in LEARNING_SIGNALS):
|
|
3223
3220
|
if self.admin_learning:
|
|
3224
3221
|
reply = self.admin_learning.add_instruction(chat_id, text)
|
|
@@ -3371,6 +3368,13 @@ escriba EXACTAMENTE como él. Primera persona, directo."""
|
|
|
3371
3368
|
]), chat_id, clinic, user_msg=text)
|
|
3372
3369
|
return self._split_bubbles(reply_text)
|
|
3373
3370
|
|
|
3371
|
+
if text_low in ("continuar fallback", "continuar con fallback", "si fallback", "sí fallback"):
|
|
3372
|
+
reply = self._admin_local_fallback(text, text_low, clinic, agent_name, chat_id)
|
|
3373
|
+
reply_text = self._apply_admin_output_pipeline(" ||| ".join(reply) if reply else "", chat_id, clinic, user_msg=text)
|
|
3374
|
+
db.save_message(chat_id, "user", text)
|
|
3375
|
+
db.save_message(chat_id, "assistant", reply_text if reply_text else "")
|
|
3376
|
+
return self._split_bubbles(reply_text)
|
|
3377
|
+
|
|
3374
3378
|
# ── Historial de admin (últimos 8 mensajes) ───────────────────────────
|
|
3375
3379
|
admin_history = db.get_history(chat_id, limit=8)
|
|
3376
3380
|
|
|
@@ -3380,11 +3384,33 @@ escriba EXACTAMENTE como él. Primera persona, directo."""
|
|
|
3380
3384
|
self._admin_llm_brain(chat_id, text, admin_history, clinic, agent_name),
|
|
3381
3385
|
timeout=12.0
|
|
3382
3386
|
)
|
|
3387
|
+
except LLMServiceError as e:
|
|
3388
|
+
reply = [
|
|
3389
|
+
"No voy a ocultarte esto con un fallback.",
|
|
3390
|
+
e.public_message,
|
|
3391
|
+
"Continuar con fallback? Responde exactamente: continuar fallback. No recomendado si quieres que todo lo decida el LLM."
|
|
3392
|
+
]
|
|
3393
|
+
reply_text = " ||| ".join(reply)
|
|
3394
|
+
db.save_message(chat_id, "user", text)
|
|
3395
|
+
db.save_message(chat_id, "assistant", reply_text)
|
|
3396
|
+
return self._split_bubbles(reply_text)
|
|
3383
3397
|
except asyncio.TimeoutError:
|
|
3384
|
-
|
|
3398
|
+
reply_text = (
|
|
3399
|
+
"El modelo tardó más de 12 segundos y no respondió a tiempo. ||| "
|
|
3400
|
+
"Continuar con fallback? Responde exactamente: continuar fallback. No recomendado si quieres que todo lo decida el LLM."
|
|
3401
|
+
)
|
|
3402
|
+
db.save_message(chat_id, "user", text)
|
|
3403
|
+
db.save_message(chat_id, "assistant", reply_text)
|
|
3404
|
+
return self._split_bubbles(reply_text)
|
|
3385
3405
|
except Exception as e:
|
|
3386
3406
|
log.error(f"[admin_brain] error: {e}", exc_info=True)
|
|
3387
|
-
|
|
3407
|
+
reply_text = (
|
|
3408
|
+
f"El cerebro LLM falló antes de responder. Detalle: {str(e)[:500]} ||| "
|
|
3409
|
+
"Continuar con fallback? Responde exactamente: continuar fallback. No recomendado."
|
|
3410
|
+
)
|
|
3411
|
+
db.save_message(chat_id, "user", text)
|
|
3412
|
+
db.save_message(chat_id, "assistant", reply_text)
|
|
3413
|
+
return self._split_bubbles(reply_text)
|
|
3388
3414
|
|
|
3389
3415
|
if result is None:
|
|
3390
3416
|
# LLM caído — fallback local inteligente
|
|
@@ -3428,6 +3454,16 @@ escriba EXACTAMENTE como él. Primera persona, directo."""
|
|
|
3428
3454
|
db.save_message(chat_id, "user", text)
|
|
3429
3455
|
if reply_text:
|
|
3430
3456
|
db.save_message(chat_id, "assistant", reply_text)
|
|
3457
|
+
try:
|
|
3458
|
+
from src.conny.admin_memory import AdminSoulMemory
|
|
3459
|
+
AdminSoulMemory().remember_turn(
|
|
3460
|
+
chat_id=chat_id,
|
|
3461
|
+
admin_text=text,
|
|
3462
|
+
conny_reply=reply_text,
|
|
3463
|
+
clinic=clinic,
|
|
3464
|
+
)
|
|
3465
|
+
except Exception as mem_err:
|
|
3466
|
+
log.warning(f"[admin_memory] no se pudo guardar memoria: {mem_err}")
|
|
3431
3467
|
|
|
3432
3468
|
return self._split_bubbles(reply_text) if reply_text else [
|
|
3433
3469
|
"dime qué más necesitas"
|
|
@@ -3522,6 +3558,12 @@ Si el negocio tiene {sector_name}, adapta el tono al sector. Sé breve, cálida
|
|
|
3522
3558
|
owner_control_txt = owner_style_controller.build_prompt_addon(is_admin=True)
|
|
3523
3559
|
except Exception:
|
|
3524
3560
|
owner_control_txt = ""
|
|
3561
|
+
admin_soul_txt = ""
|
|
3562
|
+
try:
|
|
3563
|
+
from src.conny.admin_memory import AdminSoulMemory
|
|
3564
|
+
admin_soul_txt = AdminSoulMemory().load_context(chat_id)
|
|
3565
|
+
except Exception as mem_err:
|
|
3566
|
+
log.warning(f"[admin_memory] no se pudo cargar memoria: {mem_err}")
|
|
3525
3567
|
|
|
3526
3568
|
# Estos son los ejemplos de cómo {agent_name} habla CON EL DUEÑO
|
|
3527
3569
|
# No reglas — identidad. El modelo sabe cómo reaccionar siendo esta persona.
|
|
@@ -3542,55 +3584,51 @@ Si el negocio tiene {sector_name}, adapta el tono al sector. Sé breve, cálida
|
|
|
3542
3584
|
{agent_name}: Gracias por decirlo. Te propongo una versión más clara y la corregimos si hace falta.
|
|
3543
3585
|
"""
|
|
3544
3586
|
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
{
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
{
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
-
|
|
3573
|
-
-
|
|
3574
|
-
-
|
|
3575
|
-
-
|
|
3576
|
-
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
-
|
|
3583
|
-
-
|
|
3584
|
-
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
Cuando el dueño pida simulación: action "simulate"
|
|
3591
|
-
Cuando dé datos para actualizar: action correspondiente
|
|
3592
|
-
Cuando solo conversa: action "none", respuesta natural y corta
|
|
3593
|
-
No hables como dashboard, soporte técnico ni consola."""
|
|
3587
|
+
# Cargar perfil persistente del admin (Namespace admin_profile)
|
|
3588
|
+
admin_profile = db.get_admin_profile(chat_id)
|
|
3589
|
+
admin_profile_txt = ""
|
|
3590
|
+
if admin_profile:
|
|
3591
|
+
p_parts = []
|
|
3592
|
+
if admin_profile.get("name"): p_parts.append(f"Nombre: {admin_profile['name']}")
|
|
3593
|
+
if admin_profile.get("preferences"): p_parts.append(f"Preferencias: {json.dumps(admin_profile['preferences'], ensure_ascii=False)}")
|
|
3594
|
+
if admin_profile.get("frequent_commands"):
|
|
3595
|
+
sorted_cmds = sorted(admin_profile['frequent_commands'].items(), key=lambda x: x[1], reverse=True)[:3]
|
|
3596
|
+
p_parts.append(f"Comandos frecuentes: {', '.join([c for c, _ in sorted_cmds])}")
|
|
3597
|
+
if admin_profile.get("active_hours"):
|
|
3598
|
+
sorted_hours = sorted(admin_profile['active_hours'].items(), key=lambda x: x[1], reverse=True)[:1]
|
|
3599
|
+
if sorted_hours:
|
|
3600
|
+
p_parts.append(f"Suele estar activo a las: {sorted_hours[0][0]}:00")
|
|
3601
|
+
if p_parts:
|
|
3602
|
+
admin_profile_txt = "\n## PERFIL DEL ADMIN (persistente):\n" + "\n".join([f"- {p}" for p in p_parts])
|
|
3603
|
+
|
|
3604
|
+
system_prompt = f"""Eres {agent_name}. Tu identidad corporativa es Innvisor.
|
|
3605
|
+
Trabajas como la recepcionista ejecutiva de {clinic.get("name", "este negocio")}.
|
|
3606
|
+
Ahora mismo hablas con tu administrador, {admin_name}.
|
|
3607
|
+
|
|
3608
|
+
ESTADO DE ENTRENAMIENTO:
|
|
3609
|
+
Si el negocio es nuevo o no tienes información sobre servicios/precios:
|
|
3610
|
+
1. Confirma honestamente: "Como soy nueva en {clinic.get("name", "el negocio")}, todavía no tengo información sobre eso".
|
|
3611
|
+
2. Sé proactiva: Invita al admin a enseñarte los detalles para que puedas atender bien a los pacientes.
|
|
3612
|
+
|
|
3613
|
+
REGLAS DE COMUNICACIÓN CON EL ADMIN:
|
|
3614
|
+
- TONO: Cálido pero estrictamente profesional y ejecutivo. Cero informalidad de calle. NUNCA uses "vos".
|
|
3615
|
+
- SIN MULETILLAS: No digas jamás "en qué puedo ayudarte" ni frases pasivas de asistente.
|
|
3616
|
+
- PROACTIVIDAD: Tu objetivo es que el admin te entrene. Si detectas que falta info, pídela.
|
|
3617
|
+
- CIERRE OBLIGATORIO: Todos tus mensajes DEBEN terminar con una pregunta concreta que invite al admin a realizar una acción o enseñarte algo.
|
|
3618
|
+
- IDENTIDAD: Fuiste creada por Innvisor (Santiago Rubio, 3243699856).
|
|
3619
|
+
|
|
3620
|
+
{admin_profile_txt}
|
|
3621
|
+
{f"{chr(10)}## MEMORIA SOUL DEL ADMIN{chr(10)}{admin_soul_txt}" if admin_soul_txt else ""}
|
|
3622
|
+
|
|
3623
|
+
Estado actual del negocio:
|
|
3624
|
+
- Clínica: {clinic.get("name", "Sin configurar")}
|
|
3625
|
+
- Servicios: {", ".join(services) if services else "NINGUNO (necesito que me los enseñes)"}
|
|
3626
|
+
- Teléfono: {clinic.get("phone") or "No configurado"}{skills_txt}{diag_txt}
|
|
3627
|
+
|
|
3628
|
+
INSTRUCCIONES DE SISTEMA:
|
|
3629
|
+
- Responde siempre como parte del equipo de Innvisor.
|
|
3630
|
+
- Si el admin pide "activar modo demo", confirma y ejecuta.
|
|
3631
|
+
"""
|
|
3594
3632
|
|
|
3595
3633
|
messages = [
|
|
3596
3634
|
{"role": "system", "content": system_prompt},
|
|
@@ -3812,11 +3850,10 @@ No hables como dashboard, soporte técnico ni consola."""
|
|
|
3812
3850
|
clinic: Dict, agent_name: str,
|
|
3813
3851
|
chat_id: str = "") -> List[str]:
|
|
3814
3852
|
"""
|
|
3815
|
-
Fallback cuando el LLM no está disponible.
|
|
3816
|
-
Respuestas naturales y cortas. Sin muros de texto. Sin call center.
|
|
3853
|
+
Fallback profesional cuando el LLM no está disponible.
|
|
3817
3854
|
"""
|
|
3818
3855
|
text_norm = _normalize_conv_text(text)
|
|
3819
|
-
admin_name = "
|
|
3856
|
+
admin_name = "Administrador"
|
|
3820
3857
|
try:
|
|
3821
3858
|
admin = db.get_admin(chat_id) if chat_id else None
|
|
3822
3859
|
if admin and admin.get("name"):
|
|
@@ -3824,33 +3861,34 @@ No hables como dashboard, soporte técnico ni consola."""
|
|
|
3824
3861
|
except Exception:
|
|
3825
3862
|
pass
|
|
3826
3863
|
|
|
3827
|
-
# Simulación
|
|
3864
|
+
# Simulación
|
|
3828
3865
|
SIM = ["simula", "simulaci", "como cliente", "prueba", "muéstrame",
|
|
3829
3866
|
"hagamos", "cómo hablarías", "modo cliente", "demuéstrame"]
|
|
3830
3867
|
if any(s in text_low for s in SIM):
|
|
3831
3868
|
return [
|
|
3832
|
-
"
|
|
3833
|
-
"
|
|
3869
|
+
"Entendido. Por favor, utilice el comando /simular-cliente seguido del escenario (ej: /simular-cliente precio).",
|
|
3870
|
+
"Quedo a la espera de su instrucción."
|
|
3834
3871
|
]
|
|
3835
3872
|
|
|
3836
3873
|
if any(token in text_low for token in ["quien te hizo", "quién te hizo", "como tenerte", "cómo tenerte", "quien te creo", "quién te creó"]):
|
|
3837
3874
|
return [
|
|
3838
|
-
"Me hizo Black One,
|
|
3839
|
-
"
|
|
3875
|
+
"Me hizo Black One, bajo la dirección de Santiago Rubio.",
|
|
3876
|
+
"Contacto directo: 3243699856.",
|
|
3877
|
+
"¿Desea que revisemos alguna configuración de su instancia ahora?"
|
|
3840
3878
|
]
|
|
3841
3879
|
|
|
3842
3880
|
if any(token in text_low for token in ["audio", "audios", "nota de voz", "pdf", "documento", "documentos", "imagen", "imagenes", "imágenes"]):
|
|
3843
3881
|
return [
|
|
3844
|
-
"Sí. Cuando el canal lo permite, puedo trabajar con audios,
|
|
3845
|
-
"Los puedo transcribir, leer y usar
|
|
3882
|
+
"Sí. Cuando el canal lo permite, puedo trabajar con audios, PDFs, documentos e imágenes.",
|
|
3883
|
+
"Los puedo transcribir, leer y usar como memoria de la instancia."
|
|
3846
3884
|
]
|
|
3847
3885
|
|
|
3848
|
-
# Saludos
|
|
3886
|
+
# Saludos
|
|
3849
3887
|
SOLO_GREET = {"hola", "hey", "buenas", "ey", "holi", "hola!", "buenas!", "hola buenas", "buenas tardes", "buenos dias", "buenos días", "buenas noches"}
|
|
3850
3888
|
if text_low.strip().rstrip("!").strip() in SOLO_GREET:
|
|
3851
3889
|
return [
|
|
3852
3890
|
f"Hola, {admin_name}.",
|
|
3853
|
-
"Estoy lista para
|
|
3891
|
+
"Estoy lista para gestionar la configuración de su instancia, ajustar el tono o realizar pruebas de servicios. ¿Por dónde desea que comencemos?"
|
|
3854
3892
|
]
|
|
3855
3893
|
|
|
3856
3894
|
# Estado / cómo estás
|