@innvisor/conny-ai 9.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +369 -0
  5. package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
  6. package/brand-assets/Conny.web.logo.png +0 -0
  7. package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
  8. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
  9. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
  10. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
  11. package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
  12. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
  13. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
  14. package/brand-assets/conny-demo/manifest.json +22 -0
  15. package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
  16. package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
  17. package/brand-assets/conny-logo.png +0 -0
  18. package/brand-assets/web.background.png +0 -0
  19. package/brand_assets.py +323 -0
  20. package/conny +28 -0
  21. package/conny-chat.py +579 -0
  22. package/conny-omni.py +3843 -0
  23. package/conny.py +113 -0
  24. package/conny_agents/__init__.py +1 -0
  25. package/conny_agents/agenda.py +1 -0
  26. package/conny_agents/captacion.py +1 -0
  27. package/conny_agents/conocimiento.py +1 -0
  28. package/conny_agents/escalacion.py +1 -0
  29. package/conny_agents/objeciones.py +1 -0
  30. package/conny_agents/seguimiento.py +1 -0
  31. package/conny_app.py +287 -0
  32. package/conny_audio.py +350 -0
  33. package/conny_audio_learn.py +84 -0
  34. package/conny_brain_v10.py +804 -0
  35. package/conny_bridge.py +656 -0
  36. package/conny_calendar.py +169 -0
  37. package/conny_cli.py +11784 -0
  38. package/conny_cli_bb.py +437 -0
  39. package/conny_commands.py +243 -0
  40. package/conny_config.py +215 -0
  41. package/conny_core/__init__.py +3 -0
  42. package/conny_core/conversation_engine.py +446 -0
  43. package/conny_core/first_turn_ops.py +287 -0
  44. package/conny_core/persona_registry.py +157 -0
  45. package/conny_core/prompt_ops.py +561 -0
  46. package/conny_cron.py +72 -0
  47. package/conny_demo_v2.py +209 -0
  48. package/conny_demo_voice.py +134 -0
  49. package/conny_design.py +43 -0
  50. package/conny_doctor.py +319 -0
  51. package/conny_domino.py +696 -0
  52. package/conny_generator.py +447 -0
  53. package/conny_google_auth.py +159 -0
  54. package/conny_i18n.py +619 -0
  55. package/conny_init.py +509 -0
  56. package/conny_integrations/__init__.py +4 -0
  57. package/conny_integrations/llm.py +1 -0
  58. package/conny_integrations/vault.py +77 -0
  59. package/conny_integrations/whatsapp.py +1 -0
  60. package/conny_intelligence.py +65 -0
  61. package/conny_learning.py +154 -0
  62. package/conny_memory.py +243 -0
  63. package/conny_memory_engine.py +292 -0
  64. package/conny_nova_proxy.py +170 -0
  65. package/conny_nuke_robot_phrases.py +493 -0
  66. package/conny_pairing.py +253 -0
  67. package/conny_patch.py +291 -0
  68. package/conny_persona_cli.py +150 -0
  69. package/conny_router.py +308 -0
  70. package/conny_runtime_ops.py +271 -0
  71. package/conny_session.py +516 -0
  72. package/conny_skills/__init__.py +1 -0
  73. package/conny_skills/demo_mode.py +35 -0
  74. package/conny_skills/text_processing.py +1 -0
  75. package/conny_skills/tone_detection.py +1 -0
  76. package/conny_smart_features.py +333 -0
  77. package/conny_studio.py +161 -0
  78. package/conny_sync_fix.py +306 -0
  79. package/conny_tui.py +512 -0
  80. package/conny_tui_select.py +202 -0
  81. package/conny_ultra_config.py +411 -0
  82. package/conny_uncertainty.py +174 -0
  83. package/conny_utils.py +87 -0
  84. package/conny_voice.py +156 -0
  85. package/conny_voice_engine.py +124 -0
  86. package/conny_web_search.py +66 -0
  87. package/conny_weekly_report.py +85 -0
  88. package/conny_worm.py +88 -0
  89. package/core/__init__.py +25 -0
  90. package/ecosystem.config.js +24 -0
  91. package/fix_init.py +27 -0
  92. package/install.sh +78 -0
  93. package/knowledge_base.py +330 -0
  94. package/nova/rules/default.yaml +37 -0
  95. package/nova_bridge.py +509 -0
  96. package/npm/conny.js +471 -0
  97. package/package.json +102 -0
  98. package/personas/conny/base/default.yaml +35 -0
  99. package/personas/conny/base/estetica_whatsapp.yaml +36 -0
  100. package/requirements.txt +14 -0
  101. package/run.sh +47 -0
  102. package/search.py +465 -0
  103. package/smart_handoff.py +1150 -0
  104. package/src/__init__.py +0 -0
  105. package/src/conny/__init__.py +0 -0
  106. package/src/conny/admin/__init__.py +0 -0
  107. package/src/conny/admin/api.py +234 -0
  108. package/src/conny/admin/dashboard.py +772 -0
  109. package/src/conny/api/__init__.py +0 -0
  110. package/src/conny/api/routes.py +8851 -0
  111. package/src/conny/brain/__init__.py +15 -0
  112. package/src/conny/brain/engine.py +804 -0
  113. package/src/conny/brain/learning.py +154 -0
  114. package/src/conny/brain/memory.py +324 -0
  115. package/src/conny/brain/smart_features.py +333 -0
  116. package/src/conny/brain/uncertainty.py +167 -0
  117. package/src/conny/channels/__init__.py +0 -0
  118. package/src/conny/channels/audio.py +316 -0
  119. package/src/conny/channels/cli.py +11795 -0
  120. package/src/conny/channels/logo_art.py +11 -0
  121. package/src/conny/channels/voice.py +156 -0
  122. package/src/conny/core/__init__.py +0 -0
  123. package/src/conny/core/config.py +215 -0
  124. package/src/conny/core/cron.py +72 -0
  125. package/src/conny/core/messenger.py +563 -0
  126. package/src/conny/core/router.py +297 -0
  127. package/src/conny/core/session.py +312 -0
  128. package/src/conny/demo/__init__.py +0 -0
  129. package/src/conny/demo/handler.py +3110 -0
  130. package/src/conny/integrations/__init__.py +19 -0
  131. package/src/conny/integrations/calendar.py +169 -0
  132. package/src/conny/integrations/knowledge.py +312 -0
  133. package/src/conny/integrations/search.py +66 -0
  134. package/src/conny/personas/__init__.py +0 -0
  135. package/src/conny/personas/generator.py +447 -0
  136. package/src/conny/production/__init__.py +0 -0
  137. package/src/conny/production/domino.py +696 -0
  138. package/src/conny/production/guard.py +550 -0
  139. package/src/conny/production/handoff.py +1150 -0
  140. package/src/conny/production/monitor.py +353 -0
  141. package/src/conny/utils/__init__.py +2 -0
  142. package/src/conny/utils/helpers.py +75 -0
  143. package/src/conny/utils/i18n.py +619 -0
  144. package/src/core/admin_engines.py +772 -0
  145. package/src/core/globals.py +11845 -0
  146. package/src/core/orchestrator.py +273 -0
  147. package/src/core/production_monitor.py +353 -0
  148. package/src/core/runtime.py +5487 -0
  149. package/src/domain/onboarding_flow.py +230 -0
  150. package/src/domain/prompts/__init__.py +1 -0
  151. package/src/domain/prompts/prospect_pitch.py +282 -0
  152. package/src/domain/send_guard.py +636 -0
  153. package/src/domain/swarm/queen.py +96 -0
  154. package/src/infrastructure/llm_providers/engine.py +487 -0
  155. package/src/interfaces/mcp_server.py +73 -0
  156. package/src/interfaces/nova_bridge.py +58 -0
  157. package/src/interfaces/web/admin_api.py +1379 -0
  158. package/src/interfaces/web/app.py +9408 -0
  159. package/src/interfaces/web/demo_handler.py +3450 -0
  160. package/src/interfaces/web/static/generate_avatars.py +46 -0
  161. package/v7/__init__.py +46 -0
  162. package/v7/agents/__init__.py +46 -0
  163. package/v7/agents/agenda.py +77 -0
  164. package/v7/agents/base.py +216 -0
  165. package/v7/agents/captacion.py +60 -0
  166. package/v7/agents/conocimiento.py +69 -0
  167. package/v7/agents/escalacion.py +83 -0
  168. package/v7/agents/objeciones.py +109 -0
  169. package/v7/agents/seguimiento.py +71 -0
  170. package/v7/memory/__init__.py +46 -0
  171. package/v7/memory/patient_profile.py +200 -0
  172. package/v7/orchestrator.py +275 -0
  173. package/v7/postprocess.py +127 -0
  174. package/v7/router.py +239 -0
  175. package/verify_conversation_impl.py +48 -0
@@ -0,0 +1,437 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import ast
5
+ import json
6
+ import os
7
+ import shutil
8
+ import sqlite3
9
+ import subprocess
10
+ import tempfile
11
+ from dataclasses import dataclass
12
+ from functools import lru_cache
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Dict, Optional
15
+
16
+
17
+ @dataclass
18
+ class BBContext:
19
+ conny_dir: str
20
+ colors: Any
21
+ print_logo: Callable[..., None]
22
+ section: Callable[..., None]
23
+ q: Callable[..., str]
24
+ kv: Callable[..., None]
25
+ info: Callable[..., None]
26
+ nl: Callable[..., None]
27
+ fail: Callable[..., None]
28
+ warn: Callable[..., None]
29
+ ok: Callable[..., None]
30
+ prompt: Callable[..., str]
31
+ confirm: Callable[..., bool]
32
+ select: Callable[..., int]
33
+ spinner_cls: Any
34
+ pick_instance: Callable[..., Any]
35
+ health: Callable[..., Any]
36
+ v8_api: Callable[..., Dict[str, Any]]
37
+ handler_chat: Callable[..., None]
38
+ handler_doctor: Callable[..., None]
39
+ handler_sync: Callable[..., None]
40
+ handler_guide: Callable[..., None]
41
+ handler_new: Callable[..., None]
42
+ handler_init: Callable[..., None]
43
+ handler_status: Callable[..., None]
44
+ handler_health: Callable[..., None]
45
+ handler_modelo: Callable[..., None]
46
+ handler_trainer_skills: Callable[..., None]
47
+ handler_trainer_control: Callable[..., None]
48
+ handler_bb_config: Callable[..., None]
49
+
50
+
51
+ def bb_persona_defaults() -> Dict[str, Any]:
52
+ return {
53
+ "name": "Conny",
54
+ "role": "recepcionista IA",
55
+ "archetype": "amigable",
56
+ "tone": "natural",
57
+ "tone_instruction": "",
58
+ "formality_level": 0.35,
59
+ "warmth_level": 0.80,
60
+ "humor_level": 0.10,
61
+ "verbosity": 0.35,
62
+ "greetings": [],
63
+ "closings": [],
64
+ "affirmations": [],
65
+ "forbidden_words": [],
66
+ "custom_phrases": [],
67
+ }
68
+
69
+
70
+ @lru_cache(maxsize=4)
71
+ def _bb_personality_catalog_for_path(conny_dir: str) -> Dict[str, Dict[str, Any]]:
72
+ conny_path = Path(conny_dir) / "conny.py"
73
+ fallback = {
74
+ "amigable": {
75
+ "desc": "Cercana y natural. La opción segura por defecto.",
76
+ "formality": 0.35,
77
+ "warmth": 0.80,
78
+ "humor": 0.15,
79
+ "verbosity": 0.35,
80
+ "greetings": ["hola", "buenas"],
81
+ "affirmations": ["claro", "listo"],
82
+ "closings": ["cualquier cosa me escribes"],
83
+ "forbidden": ["estimado", "cordialmente"],
84
+ "tone_instruction": "",
85
+ }
86
+ }
87
+ try:
88
+ module = ast.parse(conny_path.read_text(encoding="utf-8"))
89
+ for node in module.body:
90
+ if isinstance(node, ast.Assign):
91
+ for target in node.targets:
92
+ if isinstance(target, ast.Name) and target.id == "PERSONALITY_ARCHETYPES":
93
+ data = ast.literal_eval(node.value)
94
+ if isinstance(data, dict) and data:
95
+ return data
96
+ except Exception:
97
+ pass
98
+ return fallback
99
+
100
+
101
+ def bb_personality_catalog(ctx: BBContext) -> Dict[str, Dict[str, Any]]:
102
+ return _bb_personality_catalog_for_path(ctx.conny_dir)
103
+
104
+
105
+ def bb_read_persona_db(inst: Any) -> Dict[str, Any]:
106
+ db_path = Path(inst.db_path)
107
+ if not db_path.exists():
108
+ return {}
109
+ try:
110
+ conn = sqlite3.connect(str(db_path))
111
+ row = conn.execute("SELECT persona_config FROM clinic WHERE id=1").fetchone()
112
+ if not row:
113
+ row = conn.execute("SELECT persona_config FROM clinic LIMIT 1").fetchone()
114
+ conn.close()
115
+ except Exception:
116
+ return {}
117
+ if not row or not row[0]:
118
+ return {}
119
+ raw = row[0]
120
+ if isinstance(raw, dict):
121
+ return dict(raw)
122
+ if isinstance(raw, str):
123
+ try:
124
+ parsed = json.loads(raw)
125
+ return parsed if isinstance(parsed, dict) else {}
126
+ except Exception:
127
+ return {}
128
+ return {}
129
+
130
+
131
+ def bb_write_persona_db(ctx: BBContext, inst: Any, persona: Dict[str, Any]) -> bool:
132
+ db_path = Path(inst.db_path)
133
+ if not db_path.exists():
134
+ ctx.fail(f"No encontré la base de datos de {inst.label}: {db_path}")
135
+ return False
136
+ try:
137
+ conn = sqlite3.connect(str(db_path))
138
+ row = conn.execute("SELECT id FROM clinic WHERE id=1").fetchone()
139
+ payload = json.dumps(persona, ensure_ascii=False)
140
+ if row:
141
+ conn.execute("UPDATE clinic SET persona_config=? WHERE id=1", (payload,))
142
+ else:
143
+ any_row = conn.execute("SELECT id FROM clinic LIMIT 1").fetchone()
144
+ if any_row:
145
+ conn.execute("UPDATE clinic SET persona_config=? WHERE id=?", (payload, any_row[0]))
146
+ else:
147
+ conn.execute("INSERT INTO clinic (id, persona_config) VALUES (1, ?)", (payload,))
148
+ conn.commit()
149
+ conn.close()
150
+ return True
151
+ except Exception as exc:
152
+ ctx.fail(f"No pude guardar la personalidad en SQLite: {exc}")
153
+ return False
154
+
155
+
156
+ def bb_load_persona(inst: Any) -> Dict[str, Any]:
157
+ persona = bb_persona_defaults()
158
+ persona.update(bb_read_persona_db(inst))
159
+ return persona
160
+
161
+
162
+ def bb_apply_persona(
163
+ ctx: BBContext,
164
+ inst: Any,
165
+ updates: Dict[str, Any],
166
+ spinner_label: str = "Actualizando agente...",
167
+ ) -> bool:
168
+ current = bb_load_persona(inst)
169
+ current.update({key: value for key, value in updates.items() if value is not None})
170
+
171
+ if ctx.health(inst.port):
172
+ with ctx.spinner_cls(spinner_label) as spinner:
173
+ response = ctx.v8_api(inst, "/personality", method="PATCH", payload=updates, timeout=12)
174
+ if response.get("ok"):
175
+ spinner.finish("Agente actualizado")
176
+ return True
177
+ spinner.finish(f"API no respondió: {response.get('error', 'sin respuesta')}", ok=False)
178
+ ctx.warn("La instancia no aceptó el cambio por API; guardando directamente en la base local.")
179
+
180
+ if bb_write_persona_db(ctx, inst, current):
181
+ ctx.ok("Agente actualizado en la base local")
182
+ return True
183
+ return False
184
+
185
+
186
+ def bb_prompt_block(ctx: BBContext, label: str, initial: str = "") -> str:
187
+ editor = os.getenv("EDITOR") or (
188
+ "notepad" if os.name == "nt" else ("nano" if shutil.which("nano") else "vi" if shutil.which("vi") else "")
189
+ )
190
+ if editor and shutil.which(editor):
191
+ if ctx.confirm(f"¿Abrir {editor} para editar {label.lower()}?", default=True):
192
+ fd, tmp_path = tempfile.mkstemp(prefix="conny-bb-", suffix=".txt")
193
+ os.close(fd)
194
+ temp_file = Path(tmp_path)
195
+ temp_file.write_text((initial or "").strip() + "\n", encoding="utf-8")
196
+ subprocess.run([editor, str(temp_file)], check=False)
197
+ try:
198
+ value = temp_file.read_text(encoding="utf-8").strip()
199
+ finally:
200
+ temp_file.unlink(missing_ok=True)
201
+ return value or initial
202
+ return ctx.prompt(label, default=initial)
203
+
204
+
205
+ def bb_score_prompt(ctx: BBContext, label: str, current: Any, default: float) -> float:
206
+ raw_default = current if current not in (None, "") else default
207
+ raw = ctx.prompt(label, default=str(raw_default))
208
+ try:
209
+ value = float(raw)
210
+ except Exception:
211
+ ctx.warn("Valor inválido; mantengo el actual.")
212
+ return float(raw_default)
213
+ return max(0.0, min(1.0, value))
214
+
215
+
216
+ def bb_show_agent_summary(ctx: BBContext, inst: Any, persona: Optional[Dict[str, Any]] = None) -> None:
217
+ persona = persona or bb_load_persona(inst)
218
+ catalog = bb_personality_catalog(ctx)
219
+ archetype = persona.get("archetype", "amigable")
220
+ archetype_info = catalog.get(archetype, {})
221
+ ctx.section(f"Black Boss Config — {inst.label}", "Agente, prompt y personalidad")
222
+ ctx.kv("Agente", persona.get("name", "Conny"))
223
+ ctx.kv("Rol", persona.get("role", "recepcionista IA"))
224
+ ctx.kv("Arquetipo", f"{archetype} · {archetype_info.get('desc', 'sin descripción')}")
225
+ ctx.kv("Formalidad", f"{float(persona.get('formality_level', 0.35)):.2f}")
226
+ ctx.kv("Calidez", f"{float(persona.get('warmth_level', 0.80)):.2f}")
227
+ ctx.kv("Humor", f"{float(persona.get('humor_level', 0.10)):.2f}")
228
+ ctx.kv("Detalle", f"{float(persona.get('verbosity', 0.35)):.2f}")
229
+ tone_instruction = (persona.get("tone_instruction", "") or "").strip()
230
+ if tone_instruction:
231
+ ctx.info("Prompt maestro:")
232
+ print(f" {ctx.q(ctx.colors.G1, tone_instruction[:160] + ('…' if len(tone_instruction) > 160 else ''))}")
233
+ else:
234
+ ctx.info("Prompt maestro: usando el del arquetipo activo")
235
+ ctx.nl()
236
+
237
+
238
+ def bb_pick_archetype(ctx: BBContext, current_id: str) -> Optional[str]:
239
+ catalog = bb_personality_catalog(ctx)
240
+ keys = list(catalog.keys())
241
+ labels = [key.replace("_", " ").title() for key in keys]
242
+ descs = [catalog[key].get("desc", "") for key in keys]
243
+ idx = ctx.select(labels, descs=descs, title=f"Elige la personalidad base (actual: {current_id})")
244
+ return keys[idx] if 0 <= idx < len(keys) else None
245
+
246
+
247
+ def bb_forward_inst(handler: Callable[..., None], inst: Any, *, name: str = "", subcommand: str = "") -> None:
248
+ forwarded = argparse.Namespace(name=name, subcommand=subcommand or inst.name, command="")
249
+ handler(forwarded)
250
+
251
+
252
+ def cmd_bb(ctx: BBContext, args: Any) -> None:
253
+ action = (getattr(args, "subcommand", "") or "").strip().lower()
254
+ target_name = getattr(args, "name", "")
255
+
256
+ if not action:
257
+ ctx.print_logo(compact=True)
258
+ ctx.section("Black Boss", "Capa operativa rápida para Conny")
259
+ shortcuts = [
260
+ ("conny bb config [n]", "Crear y ajustar el agente de una instancia"),
261
+ ("conny bb chat [n]", "Entrar al chat operativo"),
262
+ ("conny bb doctor", "Diagnóstico completo"),
263
+ ("conny bb sync", "Clonar runtime exacto a todas las instancias"),
264
+ ("conny bb new", "Crear nueva instancia"),
265
+ ("conny bb guide", "Abrir guía operativa"),
266
+ ]
267
+ for cmd_text, desc in shortcuts:
268
+ print(f" {ctx.q(ctx.colors.CYN, cmd_text):<30} {ctx.q(ctx.colors.G1, desc)}")
269
+ ctx.nl()
270
+ ctx.info("Usa el patrón: conny bb <acción> [instancia]")
271
+ return
272
+
273
+ bb_routes = {
274
+ "config": ctx.handler_bb_config,
275
+ "chat": ctx.handler_chat,
276
+ "doctor": ctx.handler_doctor,
277
+ "sync": ctx.handler_sync,
278
+ "guide": ctx.handler_guide,
279
+ "guia": ctx.handler_guide,
280
+ "new": ctx.handler_new,
281
+ "crear": ctx.handler_new,
282
+ "init": ctx.handler_init,
283
+ "start": ctx.handler_init,
284
+ "status": ctx.handler_status,
285
+ "health": ctx.handler_health,
286
+ }
287
+ handler = bb_routes.get(action)
288
+ if not handler:
289
+ ctx.fail(f"Acción BB desconocida: '{action}'")
290
+ ctx.info("Prueba con: conny bb config | chat | doctor | sync | new | guide")
291
+ return
292
+
293
+ forwarded = argparse.Namespace(**vars(args))
294
+ forwarded.command = action
295
+ forwarded.subcommand = ""
296
+ forwarded.name = target_name
297
+ handler(forwarded)
298
+
299
+
300
+ def cmd_bb_config(ctx: BBContext, args: Any) -> None:
301
+ inst = ctx.pick_instance(args, "¿Cuál agente Conny quieres ajustar?")
302
+ if not inst:
303
+ return
304
+
305
+ while True:
306
+ ctx.print_logo(compact=True, sector=inst.sector)
307
+ persona = bb_load_persona(inst)
308
+ bb_show_agent_summary(ctx, inst, persona)
309
+
310
+ options = [
311
+ "Crear / renombrar agente",
312
+ "Elegir personalidad base",
313
+ "Editar prompt maestro",
314
+ "Ajustar tono fino",
315
+ "Cambiar modelo LLM",
316
+ "Activar / desactivar skills",
317
+ "Control duro y frases prohibidas",
318
+ "Enseñarle una instrucción nueva",
319
+ "Ver resumen otra vez",
320
+ "Salir",
321
+ ]
322
+ descs = [
323
+ "Nombre visible, rol y perfil del agente.",
324
+ "Aplica un arquetipo base listo para usar.",
325
+ "Edita la instrucción central del agente.",
326
+ "Formalidad, calidez, humor y nivel de detalle.",
327
+ "Abre el catálogo de modelos para esta instancia.",
328
+ "Gestiona skills del agente en caliente.",
329
+ "Ajusta frases prohibidas, saludo y estilo duro.",
330
+ "Le enseña un patrón nuevo sin tocar código.",
331
+ "Recarga la configuración actual.",
332
+ "Volver a la terminal.",
333
+ ]
334
+ choice = ctx.select(options, descs=descs, title="¿Qué quieres cambiar en este agente?")
335
+
336
+ if choice == 0:
337
+ new_name = ctx.prompt("Nombre visible del agente", default=persona.get("name", "Conny"))
338
+ new_role = ctx.prompt("Rol del agente", default=persona.get("role", "recepcionista IA"))
339
+ bb_apply_persona(
340
+ ctx,
341
+ inst,
342
+ {"name": new_name, "role": new_role},
343
+ spinner_label="Actualizando identidad del agente...",
344
+ )
345
+ elif choice == 1:
346
+ archetype_id = bb_pick_archetype(ctx, persona.get("archetype", "amigable"))
347
+ if not archetype_id:
348
+ continue
349
+ data = bb_personality_catalog(ctx).get(archetype_id, {})
350
+ bb_apply_persona(
351
+ ctx,
352
+ inst,
353
+ {
354
+ "archetype": archetype_id,
355
+ "tone": data.get("desc", persona.get("tone", "natural")),
356
+ "tone_instruction": data.get("tone_instruction", ""),
357
+ "formality_level": data.get("formality", persona.get("formality_level", 0.35)),
358
+ "warmth_level": data.get("warmth", persona.get("warmth_level", 0.80)),
359
+ "humor_level": data.get("humor", persona.get("humor_level", 0.10)),
360
+ "verbosity": data.get("verbosity", persona.get("verbosity", 0.35)),
361
+ "greetings": data.get("greetings", []),
362
+ "affirmations": data.get("affirmations", []),
363
+ "closings": data.get("closings", []),
364
+ "forbidden_words": data.get("forbidden", []),
365
+ },
366
+ spinner_label="Aplicando personalidad base...",
367
+ )
368
+ elif choice == 2:
369
+ new_prompt = bb_prompt_block(ctx, "Prompt maestro", initial=persona.get("tone_instruction", ""))
370
+ if new_prompt.strip():
371
+ bb_apply_persona(
372
+ ctx,
373
+ inst,
374
+ {"tone_instruction": new_prompt.strip()},
375
+ spinner_label="Guardando prompt maestro...",
376
+ )
377
+ elif choice == 3:
378
+ tone = ctx.prompt("Descripción corta del tono", default=persona.get("tone", "natural"))
379
+ formality = bb_score_prompt(ctx, "Formalidad (0.0 a 1.0)", persona.get("formality_level"), 0.35)
380
+ warmth = bb_score_prompt(ctx, "Calidez (0.0 a 1.0)", persona.get("warmth_level"), 0.80)
381
+ humor = bb_score_prompt(ctx, "Humor (0.0 a 1.0)", persona.get("humor_level"), 0.10)
382
+ verbosity = bb_score_prompt(ctx, "Nivel de detalle (0.0 a 1.0)", persona.get("verbosity"), 0.35)
383
+ bb_apply_persona(
384
+ ctx,
385
+ inst,
386
+ {
387
+ "tone": tone,
388
+ "formality_level": formality,
389
+ "warmth_level": warmth,
390
+ "humor_level": humor,
391
+ "verbosity": verbosity,
392
+ },
393
+ spinner_label="Ajustando tono del agente...",
394
+ )
395
+ elif choice == 4:
396
+ bb_forward_inst(ctx.handler_modelo, inst, name=inst.name, subcommand=inst.name)
397
+ elif choice == 5:
398
+ bb_forward_inst(ctx.handler_trainer_skills, inst, name="", subcommand=inst.name)
399
+ elif choice == 6:
400
+ bb_forward_inst(ctx.handler_trainer_control, inst, name=inst.name, subcommand=inst.name)
401
+ elif choice == 7:
402
+ instruction = ctx.prompt("¿Qué quieres que aprenda este agente?", default="")
403
+ if instruction.strip():
404
+ with ctx.spinner_cls("Enseñando al agente...") as spinner:
405
+ response = ctx.v8_api(
406
+ inst,
407
+ "/trainer/prompt/evolve",
408
+ method="POST",
409
+ payload={"instruction": instruction.strip(), "admin_chat_id": "cli"},
410
+ timeout=20,
411
+ )
412
+ spinner.finish("Aprendido" if response.get("ok") else "Error", ok=bool(response.get("ok")))
413
+ if response.get("ok"):
414
+ ctx.ok(response.get("description", "Instrucción procesada"))
415
+ else:
416
+ ctx.fail(f"Error: {response.get('error', 'sin respuesta')}")
417
+ elif choice == 8:
418
+ continue
419
+ else:
420
+ break
421
+ ctx.nl()
422
+
423
+
424
+ __all__ = [
425
+ "BBContext",
426
+ "bb_apply_persona",
427
+ "bb_load_persona",
428
+ "bb_persona_defaults",
429
+ "bb_personality_catalog",
430
+ "bb_prompt_block",
431
+ "bb_read_persona_db",
432
+ "bb_score_prompt",
433
+ "bb_show_agent_summary",
434
+ "bb_write_persona_db",
435
+ "cmd_bb",
436
+ "cmd_bb_config",
437
+ ]
@@ -0,0 +1,243 @@
1
+ """conny_commands.py — Slash command system for users and admins."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ import re
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ log = logging.getLogger("conny.commands")
12
+
13
+ USER_COMMANDS = {
14
+ "/cita": "Ver o gestionar tu cita",
15
+ "/horarios": "Ver horarios del negocio",
16
+ "/servicios": "Ver lista de servicios",
17
+ "/precios": "Consultar tarifas",
18
+ "/ubicacion": "Dirección y cómo llegar",
19
+ "/ayuda": "Ver comandos disponibles",
20
+ "/hablar": "Hablar con un humano",
21
+ }
22
+
23
+ ADMIN_COMMANDS = {
24
+ "/status": "Estado de la instancia",
25
+ "/gaps": "Preguntas sin responder",
26
+ "/persona": "Cambiar personalidad",
27
+ "/modelo": "Cambiar modelo LLM",
28
+ "/pausa": "Pausar Conny",
29
+ "/reanudar": "Reanudar Conny",
30
+ "/stats": "Estadísticas",
31
+ "/test": "Probar respuesta",
32
+ "/reload": "Recargar configuración",
33
+ "/aprender": "Enseñar respuesta",
34
+ "/personalidad": "Cambio rápido de personalidad",
35
+ "/broadcast": "Mensaje masivo",
36
+ "/blacklist": "Bloquear número",
37
+ }
38
+
39
+
40
+ class CommandHandler:
41
+ """Handles slash commands from both users and admins."""
42
+
43
+ def __init__(self, instance_id: str = "default"):
44
+ self.instance_id = instance_id
45
+ self._paused = False
46
+
47
+ def is_command(self, text: str) -> bool:
48
+ return text.strip().startswith("/")
49
+
50
+ def is_paused(self) -> bool:
51
+ return self._paused
52
+
53
+ async def handle(self, chat_id: str, text: str, is_admin: bool = False,
54
+ clinic: Optional[Dict] = None, db=None) -> Optional[List[str]]:
55
+ text = text.strip()
56
+ if not text.startswith("/"):
57
+ return None
58
+
59
+ parts = text.split(maxsplit=1)
60
+ cmd = parts[0].lower()
61
+ args_str = parts[1] if len(parts) > 1 else ""
62
+
63
+ if is_admin:
64
+ result = await self._handle_admin(cmd, args_str, chat_id, clinic, db)
65
+ if result:
66
+ return result
67
+
68
+ return await self._handle_user(cmd, args_str, chat_id, clinic, db)
69
+
70
+ async def _handle_user(self, cmd: str, args: str, chat_id: str,
71
+ clinic: Optional[Dict], db) -> Optional[List[str]]:
72
+ if cmd in ("/ayuda", "/help"):
73
+ lines = [
74
+ "Comandos disponibles:",
75
+ " /help — ver esta lista",
76
+ " /personalidad — ver y cambiar personalidades",
77
+ " /reset — empezar de cero con otro negocio",
78
+ " /modelo — cambiar modelo de IA",
79
+ "",
80
+ "También puedes escribir sin /:",
81
+ " 'formal' 'luxury' 'casual' — cambiar tono",
82
+ " 'reset' — reiniciar demo",
83
+ " 'stats' — ver estadísticas",
84
+ "",
85
+ "Quieres probarme? Dime el nombre de tu negocio y escríbeme como si fueras un cliente",
86
+ ]
87
+ return ["\n".join(lines)]
88
+
89
+ if cmd == "/personalidad":
90
+ return [
91
+ "Personalidades disponibles:",
92
+ " formal — profesional, usted, sin jerga\n"
93
+ " amigable — cercana, tutea, cálida\n"
94
+ " luxury — sofisticada, exclusiva, elegante\n"
95
+ " directa — concisa, sin rodeos\n"
96
+ " juvenil — fresca, emojis, moderna\n"
97
+ " experta — técnica, confiable, precisa",
98
+ "Escribe el nombre de la personalidad para verla en acción"
99
+ ]
100
+
101
+ if cmd == "/horarios":
102
+ hours = (clinic or {}).get("schedule", "No configurado")
103
+ if isinstance(hours, dict):
104
+ hours = "\n".join(f" {k}: {v}" for k, v in hours.items())
105
+ return [f"Nuestro horario:\n{hours}"]
106
+
107
+ if cmd == "/servicios":
108
+ services = (clinic or {}).get("services", [])
109
+ if isinstance(services, str):
110
+ services = [s.strip() for s in services.split(",")]
111
+ if services:
112
+ return ["Nuestros servicios:\n" + "\n".join(f" • {s}" for s in services)]
113
+ return ["Servicios no configurados aún."]
114
+
115
+ if cmd in ("/ubicacion", "/ubicación"):
116
+ loc = (clinic or {}).get("location", "") or (clinic or {}).get("address", "")
117
+ return [f"Nos encuentras en:\n{loc}"] if loc else ["Ubicación no configurada."]
118
+
119
+ if cmd == "/cita":
120
+ return ["Para agendar una cita, dime:\n• Tu nombre\n• Servicio\n• Fecha y hora preferida"]
121
+
122
+ if cmd == "/hablar":
123
+ return ["Te paso con alguien del equipo. Un momento."]
124
+
125
+ if cmd == "/precios":
126
+ return ["Los precios dependen del servicio. ¿Cuál te interesa?"]
127
+
128
+ return None
129
+
130
+ async def _handle_admin(self, cmd: str, args: str, chat_id: str,
131
+ clinic: Optional[Dict], db) -> Optional[List[str]]:
132
+ if cmd in ("/ayuda", "/help"):
133
+ lines = ["🔧 Comandos Admin:"]
134
+ for c, desc in ADMIN_COMMANDS.items():
135
+ lines.append(f" {c} — {desc}")
136
+ lines.append("\n📋 También puedes:")
137
+ lines.append(" 'conectar calendario' — vincular Google Calendar")
138
+ lines.append(" 'investiga X' — buscar en internet")
139
+ lines.append(" 'modo luxury/formal/casual' — cambiar tono")
140
+ lines.append(" Enviar archivos TXT/PDF/JSON — los leo y aprendo")
141
+ return ["\n".join(lines)]
142
+
143
+ if cmd == "/status":
144
+ return [f"Instancia: {self.instance_id}\nEstado: {'pausada' if self._paused else 'activa'}"]
145
+
146
+ if cmd == "/pausa":
147
+ self._paused = True
148
+ return ["⏸ Conny pausada. Mensajes no serán respondidos hasta /reanudar"]
149
+
150
+ if cmd == "/reanudar":
151
+ self._paused = False
152
+ return ["▶ Conny activa de nuevo."]
153
+
154
+ if cmd == "/gaps":
155
+ gaps_dir = Path("knowledge_gaps")
156
+ if not gaps_dir.exists():
157
+ return ["No hay gaps registrados."]
158
+ from datetime import datetime
159
+ today = datetime.now().strftime("%Y-%m-%d")
160
+ today_file = gaps_dir / f"{today}.jsonl"
161
+ if not today_file.exists():
162
+ return ["No hay gaps de hoy."]
163
+ gaps = []
164
+ for line in open(today_file):
165
+ g = json.loads(line)
166
+ gaps.append(f"• {g['user_msg'][:80]} (conf: {g['confidence']:.0%})")
167
+ return [f"Knowledge gaps ({len(gaps)}):\n" + "\n".join(gaps[:10])]
168
+
169
+ if cmd in ("/personalidad", "/persona"):
170
+ if not args:
171
+ return [
172
+ "Personalidades disponibles:\n"
173
+ " formal — profesional, usted, sin jerga\n"
174
+ " amigable — cercana, tutea, cálida\n"
175
+ " luxury — sofisticada, exclusiva, elegante\n"
176
+ " casual — relajada, como amiga\n"
177
+ " directa — concisa, sin rodeos\n"
178
+ " juvenil — fresca, emojis, moderna\n"
179
+ " experta — técnica, confiable, precisa\n\n"
180
+ "Para cambiar: /personalidad tono=luxury\n"
181
+ "O escribe: 'modo luxury' / 'modo formal' / 'modo casual'"
182
+ ]
183
+ updates = {}
184
+ for pair in re.findall(r'(\w+)=("[^"]+"|[^\s]+)', args):
185
+ key, val = pair
186
+ updates[key] = val.strip('"')
187
+ if updates:
188
+ override_path = Path(f"personas/{self.instance_id}/runtime_override.json")
189
+ override_path.parent.mkdir(parents=True, exist_ok=True)
190
+ existing = json.loads(override_path.read_text()) if override_path.exists() else {}
191
+ existing.update(updates)
192
+ existing["updated_at"] = time.time()
193
+ override_path.write_text(json.dumps(existing, ensure_ascii=False, indent=2))
194
+ return [f"✅ Personalidad actualizada: {', '.join(f'{k}={v}' for k,v in updates.items())}"]
195
+ return ["No se detectaron cambios."]
196
+
197
+ if cmd == "/aprender":
198
+ if "→" in args or "->" in args:
199
+ sep = "→" if "→" in args else "->"
200
+ question, answer = args.split(sep, 1)
201
+ question = question.strip().strip('"')
202
+ answer = answer.strip().strip('"')
203
+ teachings_dir = Path("teachings")
204
+ teachings_dir.mkdir(exist_ok=True)
205
+ with open(teachings_dir / f"{self.instance_id}.jsonl", "a") as f:
206
+ f.write(json.dumps({"ts": time.time(), "question": question, "answer": answer}, ensure_ascii=False) + "\n")
207
+
208
+ # Auto-update clinic DB with structured data
209
+ q_low = question.lower()
210
+ try:
211
+ if db and any(w in q_low for w in ["horario", "hora", "atienden", "abrimos"]):
212
+ db.update_clinic(schedule=json.dumps({"general": answer}))
213
+ elif db and any(w in q_low for w in ["precio", "cuesta", "vale", "cobran"]):
214
+ db.update_clinic(pricing=answer)
215
+ elif db and any(w in q_low for w in ["servicio", "ofrecen", "hacen"]):
216
+ db.update_clinic(services=[s.strip() for s in answer.split(",")])
217
+ elif db and any(w in q_low for w in ["telefono", "número", "celular", "llamar"]):
218
+ db.update_clinic(phone=answer)
219
+ except Exception:
220
+ pass
221
+
222
+ return [f"listo, ya me lo sé: '{question[:40]}' → '{answer[:40]}'"]
223
+ return ["Uso: /aprender pregunta → respuesta"]
224
+
225
+ if cmd == "/modelo":
226
+ return [f"Modelo: {args or 'auto'}"] if args else ["Uso: /modelo gemini-2.5-flash"]
227
+
228
+ if cmd == "/reload":
229
+ return ["✅ Configuración recargada."]
230
+
231
+ if cmd == "/stats":
232
+ return [f"Estadísticas {self.instance_id}: (pendiente integración)"]
233
+
234
+ return None
235
+
236
+
237
+ _handlers: Dict[str, CommandHandler] = {}
238
+
239
+
240
+ def get_command_handler(instance_id: str = "default") -> CommandHandler:
241
+ if instance_id not in _handlers:
242
+ _handlers[instance_id] = CommandHandler(instance_id)
243
+ return _handlers[instance_id]