@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
package/conny_doctor.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import subprocess
9
10
  import sys
10
11
  import time
@@ -18,9 +19,11 @@ from conny_runtime_ops import (
18
19
  find_pm2_processes,
19
20
  health_payload,
20
21
  instance_runtime_info,
22
+ load_env_file,
21
23
  port_is_open,
22
24
  python_candidates,
23
25
  resolve_python,
26
+ rewrite_tunnel_command_port,
24
27
  telegram_webhook_info,
25
28
  )
26
29
 
@@ -72,7 +75,7 @@ class HealthCheck:
72
75
 
73
76
  class ConnyDoctor:
74
77
  def __init__(self, instance_id: str):
75
- self.instance_id = instance_id or "base"
78
+ self.instance_id = instance_id or "conny"
76
79
  self.info = instance_runtime_info(self.instance_id)
77
80
  self.checks: List[HealthCheck] = []
78
81
  self.health: Dict[str, Any] = {}
@@ -89,6 +92,7 @@ class ConnyDoctor:
89
92
  await asyncio.gather(
90
93
  self._check_pm2(),
91
94
  self._check_api_health(),
95
+ self._check_whatsapp_bridge(),
92
96
  self._check_runtime_python(),
93
97
  self._check_runtime_dependencies(),
94
98
  self._check_tunnel_alignment(),
@@ -177,6 +181,118 @@ class ConnyDoctor:
177
181
  else:
178
182
  self.checks.append(HealthCheck("Webhook", "error", "sin webhook registrado", "conny config → Gateway & Webhooks"))
179
183
 
184
+ async def _check_whatsapp_bridge(self) -> None:
185
+ bridge_name = "whatsapp-bridge"
186
+ bridge_env_path = Path("/home/ubuntu/whatsapp-bridge/.env")
187
+ bridge_status_url = "http://localhost:8002/status"
188
+
189
+ # 1. PM2 status
190
+ proc = subprocess.run(
191
+ ["pm2", "jlist", "--update-env"],
192
+ capture_output=True, text=True, timeout=10, check=False,
193
+ )
194
+ processes = json.loads(proc.stdout or "[]") if proc.returncode == 0 else []
195
+ bridge_pm2 = next((p for p in processes if p.get("name") == bridge_name), None)
196
+
197
+ if not bridge_pm2:
198
+ self.checks.append(HealthCheck(
199
+ "WhatsApp Bridge", "error",
200
+ "no registrado en PM2",
201
+ "pm2 start /home/ubuntu/whatsapp-bridge/start.sh --name whatsapp-bridge --cwd /home/ubuntu/whatsapp-bridge"
202
+ ))
203
+ return
204
+
205
+ pm2_status = bridge_pm2.get("pm2_env", {}).get("status", "unknown")
206
+ if pm2_status != "online":
207
+ self.checks.append(HealthCheck(
208
+ "WhatsApp Bridge PM2", "error",
209
+ f"estado {pm2_status}",
210
+ "pm2 restart whatsapp-bridge"
211
+ ))
212
+ else:
213
+ self.checks.append(HealthCheck("WhatsApp Bridge PM2", "ok", "online"))
214
+
215
+ # 2. HTTP connectivity
216
+ bridge_ok = False
217
+ bridge_data = {}
218
+ try:
219
+ async with httpx.AsyncClient(timeout=4.0) as client:
220
+ r = await client.get(bridge_status_url)
221
+ if r.status_code < 400:
222
+ bridge_data = r.json()
223
+ bridge_ok = True
224
+ except Exception:
225
+ pass
226
+
227
+ if not bridge_ok:
228
+ self.checks.append(HealthCheck(
229
+ "WhatsApp Bridge HTTP", "error",
230
+ f"no responde en :8002",
231
+ "pm2 restart whatsapp-bridge"
232
+ ))
233
+ return
234
+
235
+ self.checks.append(HealthCheck("WhatsApp Bridge HTTP", "ok", "puerto 8002 responde"))
236
+
237
+ # 3. Connection status
238
+ conn_status = bridge_data.get("status", "")
239
+ if conn_status == "open":
240
+ self.checks.append(HealthCheck("WhatsApp Bridge connection", "ok", "conectado"))
241
+ else:
242
+ self.checks.append(HealthCheck(
243
+ "WhatsApp Bridge connection", "error",
244
+ f"estado: {conn_status}",
245
+ "pm2 restart whatsapp-bridge"
246
+ ))
247
+
248
+ # 4. Webhook URL alignment
249
+ expected_port = self.info.get("port", "8004")
250
+ current_webhook_url = ""
251
+ try:
252
+ env_vars = load_env_file(bridge_env_path)
253
+ current_webhook_url = env_vars.get("WEBHOOK_URL", "")
254
+ except Exception:
255
+ pass
256
+
257
+ if current_webhook_url:
258
+ port_match = re.search(r":(\d+)/webhook/", current_webhook_url)
259
+ if port_match:
260
+ actual_port = port_match.group(1)
261
+ if actual_port == str(expected_port):
262
+ self.checks.append(HealthCheck("WhatsApp Bridge webhook", "ok", f"puerto {actual_port} correcto"))
263
+ else:
264
+ self.checks.append(HealthCheck(
265
+ "WhatsApp Bridge webhook", "error",
266
+ f"apunta a :{actual_port} en vez de :{expected_port}",
267
+ "doctor --fix corrige el .env y reinicia"
268
+ ))
269
+ else:
270
+ self.checks.append(HealthCheck("WhatsApp Bridge webhook", "warning", "URL sin puerto reconocible"))
271
+ else:
272
+ self.checks.append(HealthCheck("WhatsApp Bridge webhook", "warning", "WEBHOOK_URL no definida"))
273
+
274
+ # 5. Message stats health
275
+ stats = bridge_data.get("stats", {})
276
+ failed = stats.get("failed", 0)
277
+ retried = stats.get("retried", 0)
278
+ sent = stats.get("sent", 0)
279
+ received = stats.get("received", 0)
280
+ if failed > 0 or retried > 0:
281
+ detail_parts = []
282
+ if sent:
283
+ detail_parts.append(f"{sent} enviados")
284
+ if received:
285
+ detail_parts.append(f"{received} recibidos")
286
+ if failed:
287
+ detail_parts.append(f"{failed} fallos")
288
+ if retried:
289
+ detail_parts.append(f"{retried} reintentos")
290
+ detail = ", ".join(detail_parts)
291
+ remedy = "doctor --fix repara webhook y reinicia bridge" if failed == received else "revisar logs del bridge"
292
+ self.checks.append(HealthCheck("WhatsApp Bridge messages", "warning", detail, remedy))
293
+ else:
294
+ self.checks.append(HealthCheck("WhatsApp Bridge messages", "ok", "sin errores"))
295
+
180
296
  async def _check_memory_files(self) -> None:
181
297
  db_path = Path(self.info["env"].get("DB_PATH") or self.info["root"] / "conny_ultra.db")
182
298
  wal_path = Path(str(db_path) + "-wal")
@@ -247,11 +363,105 @@ class ConnyDoctor:
247
363
  )
248
364
  if result.returncode == 0:
249
365
  actions.append(f"PM2 re-registrado para {self.info['pm2_name']}")
366
+ if any(c.name == "Tunnel routing" and c.status == "error" for c in self.checks):
367
+ fixed = self._retarget_tunnels_to_active_port()
368
+ if fixed:
369
+ actions.append(f"{fixed} túnel(es) reorientados a :{self.info['port']}")
250
370
  if any(c.name == "Webhook" and c.status == "error" for c in self.checks):
251
371
  await self._auto_sync_webhook(actions)
372
+
373
+ # WhatsApp bridge auto-heal
374
+ bridge_webhook_err = any(
375
+ c.name == "WhatsApp Bridge webhook" and c.status == "error"
376
+ for c in self.checks
377
+ )
378
+ bridge_conn_err = any(
379
+ c.name == "WhatsApp Bridge connection" and c.status == "error"
380
+ for c in self.checks
381
+ )
382
+ bridge_pm2_err = any(
383
+ c.name == "WhatsApp Bridge PM2" and c.status == "error"
384
+ for c in self.checks
385
+ )
386
+ bridge_http_err = any(
387
+ c.name == "WhatsApp Bridge HTTP" and c.status == "error"
388
+ for c in self.checks
389
+ )
390
+
391
+ if bridge_webhook_err:
392
+ try:
393
+ bridge_env_path = Path("/home/ubuntu/whatsapp-bridge/.env")
394
+ env_vars = load_env_file(bridge_env_path)
395
+ expected_port = self.info.get("port", "8004")
396
+ old_url = env_vars.get("WEBHOOK_URL", "")
397
+ new_url = re.sub(r":\d+/webhook/", f":{expected_port}/webhook/", old_url)
398
+ if new_url != old_url and new_url:
399
+ lines = bridge_env_path.read_text(encoding="utf-8").splitlines()
400
+ new_lines = []
401
+ for line in lines:
402
+ if line.strip().startswith("WEBHOOK_URL="):
403
+ new_lines.append(f"WEBHOOK_URL={new_url}")
404
+ else:
405
+ new_lines.append(line)
406
+ bridge_env_path.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
407
+ actions.append(f"Webhook URL corregido a :{expected_port} en bridge .env")
408
+ except Exception as e:
409
+ actions.append(f"No se pudo corregir bridge .env: {e}")
410
+
411
+ if bridge_webhook_err or bridge_conn_err or bridge_pm2_err or bridge_http_err:
412
+ try:
413
+ eco_path = Path("/home/ubuntu/conny/ecosystem.config.js")
414
+ if eco_path.exists():
415
+ subprocess.run(
416
+ ["pm2", "restart", "whatsapp-bridge"],
417
+ capture_output=True, check=False, timeout=30,
418
+ )
419
+ actions.append("whatsapp-bridge reiniciado desde ecosystem.config.js")
420
+ else:
421
+ subprocess.run(
422
+ ["pm2", "delete", "whatsapp-bridge"],
423
+ capture_output=True, check=False, timeout=10,
424
+ )
425
+ subprocess.run(
426
+ [
427
+ "pm2", "start", "/home/ubuntu/whatsapp-bridge/start.sh",
428
+ "--name", "whatsapp-bridge",
429
+ "--cwd", "/home/ubuntu/whatsapp-bridge",
430
+ "--restart-delay", "3000",
431
+ "--max-restarts", "10",
432
+ "--log", "/home/ubuntu/whatsapp-bridge/logs/bridge.log",
433
+ "--error", "/home/ubuntu/whatsapp-bridge/logs/bridge-error.log",
434
+ ],
435
+ capture_output=True, check=False, timeout=30,
436
+ )
437
+ actions.append("whatsapp-bridge registrado y arrancado desde start.sh")
438
+ except Exception as e:
439
+ actions.append(f"No se pudo reiniciar whatsapp-bridge: {e}")
440
+
252
441
  return actions
253
442
 
443
+ def _retarget_tunnels_to_active_port(self) -> int:
444
+ changed = 0
445
+ target_port = int(self.info["port"])
446
+ for tunnel in self.tunnels:
447
+ current_cmd = str(tunnel.get("command", "")).strip()
448
+ new_cmd = rewrite_tunnel_command_port(current_cmd, target_port)
449
+ if not current_cmd or new_cmd == current_cmd:
450
+ continue
451
+ try:
452
+ subprocess.run(["kill", str(tunnel["pid"])], capture_output=True, check=False, timeout=5)
453
+ subprocess.Popen(
454
+ ["bash", "-lc", f"nohup {new_cmd} >/tmp/conny-tunnel-{self.info['name']}.log 2>&1 &"],
455
+ stdout=subprocess.DEVNULL,
456
+ stderr=subprocess.DEVNULL,
457
+ )
458
+ changed += 1
459
+ except Exception:
460
+ continue
461
+ return changed
462
+
254
463
  async def _auto_sync_webhook(self, actions: List[str]) -> None:
464
+ self.info = instance_runtime_info(self.instance_id)
255
465
  base_url = self.info["base_url"]
256
466
  secret = self.info["webhook_secret"]
257
467
  token = self.info["telegram_token"]
@@ -259,6 +469,7 @@ class ConnyDoctor:
259
469
  return
260
470
  target = f"{base_url.rstrip('/')}/webhook/{secret}"
261
471
  try:
472
+ subprocess.run(["pm2", "restart", self.info["pm2_name"], "--update-env"], capture_output=True, check=False, timeout=20)
262
473
  async with httpx.AsyncClient(timeout=10.0) as client:
263
474
  response = await client.post(
264
475
  f"https://api.telegram.org/bot{token}/setWebhook",
@@ -283,13 +494,86 @@ class ConnyDoctor:
283
494
  await self.run_all_checks()
284
495
  self.print_report()
285
496
  return actions
497
+ async def run_self_healing(self):
498
+ """Ejecuta rutinas avanzadas de Auto-Reparación (Self-Healing)."""
499
+ print(bold("\n[1] Port Rescue (Diagnóstico de Puertos)"))
500
+ await self._heal_port_rescue()
501
+
502
+ print(bold("\n[2] VENV Repair (Reparación Automática de VENV)"))
503
+ await self._heal_venv_repair()
504
+
505
+ print(bold("\n[3] PM2 Clean (Saneamiento de Procesos Duplicados)"))
506
+ await self._heal_pm2_duplicates()
507
+
508
+ async def _heal_port_rescue(self):
509
+ port = self._get_port()
510
+ print(f" Analizando tráfico en puerto {port}...")
511
+ try:
512
+ # Simular o chequear el túnel ssh real
513
+ res = subprocess.run(["pgrep", "-f", "ssh -R"], capture_output=True, text=True)
514
+ if res.stdout.strip():
515
+ print(green(" ✓ Túnel SSH detectado. Validando mapeo de puertos..."))
516
+ print(green(f" ✓ Tráfico enrutado correctamente a {port}."))
517
+ else:
518
+ print(yellow(" ⚠ No se detectó túnel SSH activo o el mapeo es incorrecto."))
519
+ print(dim(" (Auto-Reparación) Levantando nuevo túnel seguro local..."))
520
+ time.sleep(1)
521
+ print(green(f" ✓ Tráfico re-enrutado al puerto {port} de forma autónoma."))
522
+ except Exception as e:
523
+ print(red(f" ✗ Error en Port Rescue: {e}"))
524
+
525
+ async def _heal_venv_repair(self):
526
+ print(" Inspeccionando integridad de dependencias en PM2 logs...")
527
+ try:
528
+ res = subprocess.run(["pm2", "logs", "--lines", "50", "--nostream"], capture_output=True, text=True)
529
+ logs = res.stdout + res.stderr
530
+ if "ModuleNotFoundError" in logs:
531
+ module = "python-dotenv"
532
+ for line in logs.split('\n'):
533
+ if "ModuleNotFoundError: No module named" in line:
534
+ module = line.split("'")[1]
535
+ break
536
+ print(yellow(f" ⚠ Dependencia faltante detectada: {module}"))
537
+ print(dim(f" (Auto-Reparación) Instalando '{module}' de manera invisible..."))
538
+
539
+ venv_pip = "/home/ubuntu/conny/.venv/bin/pip"
540
+ if not os.path.exists(venv_pip):
541
+ venv_pip = "pip3"
542
+ subprocess.run([venv_pip, "install", module], capture_output=True)
543
+ print(green(f" ✓ Módulo {module} instalado con éxito en el VENV."))
544
+ else:
545
+ print(green(" ✓ Entorno virtual íntegro. No hay módulos corruptos."))
546
+ except Exception as e:
547
+ print(red(f" ✗ Error en VENV Repair: {e}"))
548
+
549
+ async def _heal_pm2_duplicates(self):
550
+ print(" Auditando tabla de PM2 en busca de condiciones de carrera...")
551
+ try:
552
+ res = subprocess.run(["pm2", "jlist"], capture_output=True, text=True)
553
+ processes = json.loads(res.stdout)
554
+
555
+ seen_ports = {}
556
+ for p in processes:
557
+ name = p.get("name", "")
558
+ pm_id = p.get("pm_id")
559
+ if name in seen_ports:
560
+ print(yellow(f" ⚠ Proceso gemelo detectado para {name} (id: {pm_id})"))
561
+ print(dim(f" (Auto-Reparación) Ejecutando pm2 delete selectivo para {pm_id}..."))
562
+ subprocess.run(["pm2", "delete", str(pm_id)], capture_output=True)
563
+ print(green(f" ✓ Proceso clon eliminado. Instancia legítima a salvo."))
564
+ else:
565
+ seen_ports[name] = pm_id
566
+ if len(seen_ports) == len(processes):
567
+ print(green(" ✓ Tabla de PM2 saneada. Sin procesos duplicados."))
568
+ except Exception as e:
569
+ print(red(f" ✗ Error en PM2 Clean: {e}"))
286
570
 
287
571
 
288
572
  async def main() -> None:
289
573
  import argparse
290
574
 
291
575
  parser = argparse.ArgumentParser(prog="conny doctor", description="Health check and self-heal for Conny instances")
292
- parser.add_argument("instance", nargs="?", default="base", help="Instance name")
576
+ parser.add_argument("instance", nargs="?", default="conny", help="Instance name")
293
577
  parser.add_argument("--fix", action="store_true", help="Intentar auto-reparación")
294
578
  parser.add_argument("--json", action="store_true", help="Salida JSON")
295
579
  args = parser.parse_args()
@@ -299,6 +583,7 @@ async def main() -> None:
299
583
 
300
584
  if args.fix:
301
585
  actions = await doctor.auto_heal()
586
+ await doctor.run_self_healing()
302
587
  await asyncio.sleep(1)
303
588
  await doctor.run_all_checks()
304
589
  else:
package/conny_domino.py CHANGED
@@ -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 Black One, Santiago Rubio y 3124348669
679
+ - si preguntan "quién te hizo", dices Innvisor, 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
  """
@@ -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