@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,696 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+ import re
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ OPENCLAW_WORKSPACE = Path("/home/ubuntu/.openclaw/workspace")
10
+ DOMINO_FALLBACK_TRIGGERS = ("empty", "exception", "below_threshold")
11
+
12
+ _DOMINO_QUALITY_THRESHOLDS = {
13
+ "enter-demo": 0.48,
14
+ "clarify-demo": 0.54,
15
+ "bind-business": 0.58,
16
+ "reset-demo": 0.42,
17
+ "re-ground": 0.56,
18
+ "simulate": 0.60,
19
+ }
20
+
21
+ _DOMINO_FORBIDDEN_MARKERS = (
22
+ "soy una ia",
23
+ "soy un bot",
24
+ "asistente virtual",
25
+ "recepcionista virtual",
26
+ "aqui lo que hago es",
27
+ "aquí lo que hago es",
28
+ "me doy cuenta de que",
29
+ "no se cual es el negocio",
30
+ "no sé cuál es el negocio",
31
+ "de manera efectiva",
32
+ "quiero asegurarme",
33
+ )
34
+
35
+
36
+ def _normalize(text: str) -> str:
37
+ normalized = (text or "").strip().lower()
38
+ normalized = normalized.replace("0", "o")
39
+ normalized = normalized.replace("¡", "").replace("¿", "")
40
+ normalized = re.sub(r"[!?.,;:]+", " ", normalized)
41
+ normalized = re.sub(r"\s+", " ", normalized)
42
+ return normalized.strip()
43
+
44
+
45
+ def _compact_markdown(text: str, *, max_chars: int = 900) -> str:
46
+ if not text:
47
+ return ""
48
+ pieces: List[str] = []
49
+ size = 0
50
+ for raw in text.splitlines():
51
+ line = raw.strip()
52
+ if not line or line in {"---", "EOF"}:
53
+ continue
54
+ if line.startswith("#"):
55
+ continue
56
+ line = re.sub(r"[*_`>]+", "", line).strip()
57
+ if not line:
58
+ continue
59
+ pieces.append(line)
60
+ size += len(line) + 1
61
+ if size >= max_chars:
62
+ break
63
+ return " ".join(pieces).strip()
64
+
65
+
66
+ def _read_workspace_file(name: str, *, max_chars: int = 900) -> str:
67
+ path = OPENCLAW_WORKSPACE / name
68
+ if not path.exists():
69
+ return ""
70
+ try:
71
+ return _compact_markdown(path.read_text(encoding="utf-8"), max_chars=max_chars)
72
+ except Exception:
73
+ return ""
74
+
75
+
76
+ def _sanitize_memory_markdown(text: str) -> str:
77
+ raw = (text or "").strip()
78
+ lowered = raw.lower()
79
+ poison_markers = (
80
+ "run your session startup sequence",
81
+ "read the required files before responding",
82
+ "do not mention internal steps",
83
+ "conversation info (untrusted metadata)",
84
+ "sender (untrusted metadata)",
85
+ "current time:",
86
+ "session key",
87
+ "session id",
88
+ "source: telegram",
89
+ "new session started",
90
+ "message_id",
91
+ "sender_id",
92
+ )
93
+ if any(marker in lowered for marker in poison_markers):
94
+ return ""
95
+ return _compact_markdown(raw, max_chars=700)
96
+
97
+
98
+ def _soul_excerpt(text: str) -> str:
99
+ compact = _compact_markdown(text or "", max_chars=900)
100
+ if not compact:
101
+ return ""
102
+ preferred_markers = (
103
+ "be genuinely helpful",
104
+ "have opinions",
105
+ "be resourceful before asking",
106
+ "earn trust through competence",
107
+ "not a corporate drone",
108
+ "not a sycophant",
109
+ )
110
+ pieces: List[str] = []
111
+ for sentence in re.split(r"(?<=[.!?])\s+", compact):
112
+ lowered = sentence.lower()
113
+ if any(marker in lowered for marker in preferred_markers):
114
+ pieces.append(sentence.strip())
115
+ if pieces:
116
+ return " ".join(pieces)[:520].strip()
117
+ return compact[:420].strip()
118
+
119
+
120
+ @lru_cache(maxsize=1)
121
+ def load_domino_sources() -> Dict[str, str]:
122
+ latest_memory = ""
123
+ memory_dir = OPENCLAW_WORKSPACE / "memory"
124
+ if memory_dir.exists():
125
+ latest_files = sorted(memory_dir.glob("*.md"))
126
+ if latest_files:
127
+ latest_memory = _sanitize_memory_markdown(
128
+ latest_files[-1].read_text(encoding="utf-8")
129
+ )
130
+ return {
131
+ "soul": _read_workspace_file("SOUL.md", max_chars=900),
132
+ "identity": _read_workspace_file("IDENTITY.md", max_chars=400),
133
+ "user": _read_workspace_file("USER.md", max_chars=500),
134
+ "memory": latest_memory,
135
+ }
136
+
137
+
138
+ def _history_block(history: List[Dict[str, Any]], limit: int = 6) -> str:
139
+ rows: List[str] = []
140
+ for item in history[-limit:]:
141
+ role = "dueño" if item.get("role") == "user" else "conny"
142
+ content = str(item.get("content") or "").replace("|||", " | ").strip()
143
+ if content:
144
+ rows.append(f"{role}: {content}")
145
+ return "\n".join(rows) if rows else "sin historial"
146
+
147
+
148
+ def _business_memory_block(business_name: str, business_ctx: str, found_online: bool) -> str:
149
+ if not business_name:
150
+ return "Aún no sabes cómo se llama el negocio."
151
+ parts = [f"Ya sabes que el negocio es: {business_name}."]
152
+ cleaned_ctx = (business_ctx or "").strip()
153
+ if found_online and cleaned_ctx:
154
+ compact = re.sub(r"\s+", " ", cleaned_ctx)[:600].strip()
155
+ parts.append(f"Contexto útil encontrado: {compact}")
156
+ elif cleaned_ctx:
157
+ compact = re.sub(r"\s+", " ", cleaned_ctx)[:280].strip()
158
+ parts.append(f"Contexto débil todavía: {compact}")
159
+ else:
160
+ parts.append("Todavía no hay contexto externo fiable; no inventes nada.")
161
+ return " ".join(parts)
162
+
163
+
164
+ def _assistant_repeated_business_prompt(history: List[Dict[str, Any]]) -> bool:
165
+ ask_markers = (
166
+ "nombre del negocio",
167
+ "como se llama tu negocio",
168
+ "cómo se llama tu negocio",
169
+ "pasame el nombre",
170
+ "pásame el nombre",
171
+ "dime el nombre de tu negocio",
172
+ )
173
+ asks = 0
174
+ for item in history[-6:]:
175
+ if item.get("role") != "assistant":
176
+ continue
177
+ normalized = _normalize(str(item.get("content") or ""))
178
+ if any(marker in normalized for marker in ask_markers):
179
+ asks += 1
180
+ return asks >= 2
181
+
182
+
183
+ def demo_opening_tone_issues(text: str) -> List[str]:
184
+ normalized = _normalize(text)
185
+ issues: List[str] = []
186
+ abstract_markers = (
187
+ "de manera efectiva",
188
+ "de manera mas efectiva",
189
+ "de manera más precisa",
190
+ "de manera precisa",
191
+ "de manera mas precisa",
192
+ "de la mejor manera",
193
+ "relevante",
194
+ "relevantes",
195
+ "útiles para ti",
196
+ "utiles para ti",
197
+ "tipo de empresa",
198
+ "quiero asegurarme",
199
+ "asegurarme de que",
200
+ "me permitirá",
201
+ "me permitira",
202
+ "entender mejor el contexto",
203
+ "entender mejor cómo",
204
+ "entender mejor como",
205
+ "idea más clara",
206
+ "idea mas clara",
207
+ "mejor atención",
208
+ "mejor atencion",
209
+ "personalizada",
210
+ )
211
+ consultive_markers = (
212
+ "gestion de tus mensajes",
213
+ "gestión de tus mensajes",
214
+ "puedo apoyarte",
215
+ "puedo ayudarte",
216
+ "estoy para ayudarte",
217
+ "responder a las consultas",
218
+ "responder consultas",
219
+ "con el que estoy trabajando",
220
+ "con el que estoy interactuando",
221
+ "interactuando",
222
+ "proceder con el trabajo",
223
+ "para darte una mejor",
224
+ "asi podre",
225
+ "así podré",
226
+ "empezar a trabajar",
227
+ "optimizar",
228
+ "areas de mejora",
229
+ "áreas de mejora",
230
+ "procesos",
231
+ "soluciones",
232
+ )
233
+ scripted_markers = (
234
+ "que bueno tenerte por aca",
235
+ "qué bueno tenerte por acá",
236
+ "retomamos desde donde lo dejamos",
237
+ "te ubico rapido",
238
+ "te ubico rápido",
239
+ "seguimos con la demo",
240
+ )
241
+ if any(marker in normalized for marker in abstract_markers):
242
+ issues.append("tono abstracto o marketinero")
243
+ if any(marker in normalized for marker in consultive_markers):
244
+ issues.append("tono consultivo o de onboarding")
245
+ if any(marker in normalized for marker in scripted_markers):
246
+ issues.append("continuidad prefabricada o libreto heredado")
247
+ return issues
248
+
249
+
250
+ def _is_greeting(normalized: str) -> bool:
251
+ return normalized in {
252
+ "hola",
253
+ "hola buenas",
254
+ "hola conny",
255
+ "buenas",
256
+ "buenas tardes",
257
+ "buenos dias",
258
+ "buenas noches",
259
+ "hey",
260
+ "holi",
261
+ "que mas",
262
+ "que tal",
263
+ "hola otra vez",
264
+ }
265
+
266
+
267
+ def _is_confused(normalized: str) -> bool:
268
+ markers = (
269
+ "a que te refieres",
270
+ "que quieres decir",
271
+ "no te entiendo",
272
+ "no entiendo",
273
+ "explícamelo",
274
+ "explicamelo",
275
+ "para que",
276
+ "para qué",
277
+ "como asi",
278
+ "cómo así",
279
+ "hablame claro",
280
+ "háblame claro",
281
+ "bajalo a tierra",
282
+ "bájalo a tierra",
283
+ "en donde quedamos",
284
+ "donde quedamos",
285
+ "me ubicas",
286
+ "me ubicas rapido",
287
+ "me ubicas rápido",
288
+ "que sigue",
289
+ "qué sigue",
290
+ "como arrancamos",
291
+ "cómo arrancamos",
292
+ "no te sigo",
293
+ "no sigo",
294
+ "perdona",
295
+ "perdón",
296
+ "puedes explicar",
297
+ "explicame",
298
+ "explícame",
299
+ )
300
+ if any(marker in normalized for marker in markers):
301
+ return True
302
+ # Mensajes ultra-cortos que son solo confusión: "que?", "qué?", "?", "???", "ok y?"
303
+ stripped = normalized.strip("? !")
304
+ if stripped in {"que", "qué", "ok", "ok y", "y", "y eso", "eso", "como"}:
305
+ return True
306
+ return False
307
+
308
+
309
+ def _is_identity_or_meta_probe(normalized: str) -> bool:
310
+ markers = (
311
+ "que eres",
312
+ "qué eres",
313
+ "quien eres",
314
+ "quién eres",
315
+ "como funcionas",
316
+ "cómo funcionas",
317
+ "que haces",
318
+ "qué haces",
319
+ "quiero probarte",
320
+ "quiero una demo",
321
+ "quiero demo",
322
+ "tengo un negocio",
323
+ "tengo una empresa",
324
+ "como trabajas",
325
+ "cómo trabajas",
326
+ "lo llevas tu sola",
327
+ "lo llevas tú sola",
328
+ "recuerdas lo que te digo",
329
+ "como recuerdas",
330
+ "cómo recuerdas",
331
+ "para que necesitas",
332
+ "para qué necesitas",
333
+ "en que quedamos",
334
+ "en qué quedamos",
335
+ "me mandaron tu numero",
336
+ "me mandaron tu número",
337
+ "me pasaron tu numero",
338
+ "me pasaron tu número",
339
+ "que haces exactamente",
340
+ "qué haces exactamente",
341
+ "no entiendo que haces",
342
+ "no entiendo qué haces",
343
+ "no entiendo para que",
344
+ "no entiendo para qué",
345
+ )
346
+ return any(marker in normalized for marker in markers)
347
+
348
+
349
+ def _is_reset_request(normalized: str) -> bool:
350
+ markers = (
351
+ "empezar de nuevo",
352
+ "volver a empezar",
353
+ "reset",
354
+ "reiniciar",
355
+ "ese no es mi negocio",
356
+ "no es mi negocio",
357
+ "cambiar negocio",
358
+ "cambia negocio",
359
+ "otro negocio",
360
+ )
361
+ return any(marker in normalized for marker in markers)
362
+
363
+
364
+ def _is_business_submission(normalized: str) -> bool:
365
+ markers = (
366
+ "mi negocio se llama",
367
+ "nuestro negocio se llama",
368
+ "mi empresa se llama",
369
+ "nuestra empresa se llama",
370
+ "el nombre de mi negocio es",
371
+ "el nombre del negocio es",
372
+ "la clinica se llama",
373
+ "la clínica se llama",
374
+ "se llama ",
375
+ "negocio es ",
376
+ "empresa es ",
377
+ )
378
+ return any(marker in normalized for marker in markers)
379
+
380
+
381
+ def should_route_demo_to_domino(
382
+ *,
383
+ user_text: str,
384
+ business_name: str,
385
+ history: Optional[List[Dict[str, Any]]] = None,
386
+ ) -> bool:
387
+ # En demo, toda la conversación debe pasar por la misma capa de identidad
388
+ # para no rebotar entre prompts legacy y respuestas de onboarding.
389
+ return True
390
+
391
+
392
+ def _domino_stage(
393
+ *,
394
+ normalized: str,
395
+ business_name: str,
396
+ explain_name: bool,
397
+ force_stage: Optional[str] = None,
398
+ history: Optional[List[Dict[str, Any]]] = None,
399
+ ) -> Dict[str, str]:
400
+ if force_stage == "reset-demo":
401
+ return {
402
+ "stage": "reset-demo",
403
+ "objective": "reiniciar la demo sin sonar a reset mecánico y volver a conseguir el nombre del negocio",
404
+ "action": "confirma que arrancan de cero y pide de nuevo solo el nombre del negocio, sin formularios ni branding",
405
+ }
406
+ if force_stage == "bind-business":
407
+ return {
408
+ "stage": "bind-business",
409
+ "objective": "mostrar que ya aterrizaste el negocio y mover al dueño a una simulación real",
410
+ "action": "reacciona como alguien que acaba de ubicarse en el negocio, usa el contexto encontrado solo si es fiable y empuja a una prueba real de cliente",
411
+ }
412
+ if not business_name:
413
+ repeated_business_prompt = _assistant_repeated_business_prompt(list(history or []))
414
+ if explain_name or _is_confused(normalized) or (
415
+ repeated_business_prompt and _is_identity_or_meta_probe(normalized)
416
+ ):
417
+ return {
418
+ "stage": "clarify-demo",
419
+ "objective": "explicar para qué necesitas el nombre del negocio y conseguirlo sin sonar a formulario",
420
+ "action": "baja la idea a tierra, explica tu función dentro del chat y pide una sola pieza de contexto: el nombre del negocio",
421
+ }
422
+ return {
423
+ "stage": "enter-demo",
424
+ "objective": "ubicar a la persona en la demo y conseguir el nombre del negocio con naturalidad",
425
+ "action": "explica desde adentro del trabajo qué harías aquí y pide el nombre del negocio para aterrizar la prueba",
426
+ }
427
+ if _is_reset_request(normalized):
428
+ return {
429
+ "stage": "reset-demo",
430
+ "objective": "arrancar de cero sin arrastrar negocio previo ni sonar a sistema",
431
+ "action": "di que arrancan otra vez y pide el nombre del negocio de forma directa y limpia",
432
+ }
433
+ if _is_business_submission(normalized):
434
+ return {
435
+ "stage": "bind-business",
436
+ "objective": "activar el contexto del negocio recién entregado y llevar la demo a simulación real",
437
+ "action": "deja claro que ya te ubicastes con ese negocio y pide que te hablen como cliente real",
438
+ }
439
+ if _is_greeting(normalized) or _is_confused(normalized) or _is_identity_or_meta_probe(normalized):
440
+ return {
441
+ "stage": "re-ground",
442
+ "objective": "retomar desde el negocio ya conocido y llevar al dueño a una simulación real",
443
+ "action": "no vuelvas a presentarte; si pregunta por qué querías el nombre o qué haces, responde eso primero y luego empuja a que te hablen como cliente real",
444
+ }
445
+ return {
446
+ "stage": "simulate",
447
+ "objective": "responder como si ya llevaras el WhatsApp del negocio real",
448
+ "action": "responde con criterio operativo, cuida el contexto y haz avanzar la conversación sin inventar ni volver meta la demo",
449
+ }
450
+
451
+
452
+ def build_demo_domino_contract(
453
+ *,
454
+ user_text: str,
455
+ business_name: str,
456
+ explain_name: bool = False,
457
+ force_stage: Optional[str] = None,
458
+ history: Optional[List[Dict[str, Any]]] = None,
459
+ ) -> Dict[str, Any]:
460
+ normalized = _normalize(user_text)
461
+ stage_info = _domino_stage(
462
+ normalized=normalized,
463
+ business_name=business_name,
464
+ explain_name=explain_name,
465
+ force_stage=force_stage,
466
+ history=list(history or []),
467
+ )
468
+
469
+ required_details: List[str] = []
470
+ if any(token in normalized for token in ("para que", "para qué", "por que", "por qué")):
471
+ required_details.extend(["chat", "negocio", "responder"])
472
+ if any(
473
+ token in normalized
474
+ for token in ("quien te hizo", "quién te hizo", "como tenerte", "cómo tenerte", "quien te creo", "quién te creó")
475
+ ):
476
+ required_details.extend(["black one", "3124348669"])
477
+ if any(
478
+ token in normalized
479
+ for token in ("audio", "audios", "nota de voz", "pdf", "archivo", "documento", "documentos", "imagen", "imagenes", "imágenes")
480
+ ):
481
+ required_details.extend(["audio", "pdf", "imagen"])
482
+ if any(
483
+ token in normalized
484
+ for token in (
485
+ "me mandaron tu numero",
486
+ "me mandaron tu número",
487
+ "me pasaron tu numero",
488
+ "me pasaron tu número",
489
+ "que haces",
490
+ "qué haces",
491
+ "no entiendo que haces",
492
+ "no entiendo qué haces",
493
+ )
494
+ ):
495
+ required_details.extend(["clientes", "citas", "responder"])
496
+ if stage_info["stage"] == "bind-business":
497
+ required_details.append("cliente")
498
+ if business_name and stage_info["stage"] in {"re-ground", "simulate"}:
499
+ required_details.append("negocio_actual")
500
+
501
+ ordered_required_details = list(dict.fromkeys(required_details))
502
+ should_reask_business_name = (
503
+ not business_name
504
+ and stage_info["stage"] in {"enter-demo", "clarify-demo", "reset-demo"}
505
+ )
506
+
507
+ return {
508
+ "decision_priority": "llm_first",
509
+ "fallback_triggers": list(DOMINO_FALLBACK_TRIGGERS),
510
+ "quality_threshold": _DOMINO_QUALITY_THRESHOLDS.get(stage_info["stage"], 0.50),
511
+ "repair_before_fallback": True,
512
+ "stage": stage_info["stage"],
513
+ "should_reask_business_name": should_reask_business_name,
514
+ "required_details": ordered_required_details,
515
+ "forbidden_markers": list(_DOMINO_FORBIDDEN_MARKERS),
516
+ }
517
+
518
+
519
+ def build_demo_domino_payload(
520
+ *,
521
+ user_text: str,
522
+ history: Optional[List[Dict[str, Any]]],
523
+ business_name: str,
524
+ business_ctx: str,
525
+ found_online: bool,
526
+ explain_name: bool = False,
527
+ force_stage: Optional[str] = None,
528
+ ) -> Dict[str, Any]:
529
+ normalized = _normalize(user_text)
530
+ stage_info = _domino_stage(
531
+ normalized=normalized,
532
+ business_name=business_name,
533
+ explain_name=explain_name,
534
+ force_stage=force_stage,
535
+ history=list(history or []),
536
+ )
537
+ contract = build_demo_domino_contract(
538
+ user_text=user_text,
539
+ business_name=business_name,
540
+ explain_name=explain_name,
541
+ force_stage=force_stage,
542
+ history=history,
543
+ )
544
+ sources = load_domino_sources()
545
+ history_block = _history_block(list(history or []))
546
+ business_memory = _business_memory_block(business_name, business_ctx, found_online)
547
+
548
+ soul = (
549
+ "Sé útil de verdad, no performativa. Ten criterio. Primero entiende y luego responde. "
550
+ "No rellenes ni suenes a pitch."
551
+ )
552
+ soul_seed = _soul_excerpt(sources["soul"])
553
+ if soul_seed:
554
+ soul += f" Base OpenClaw: {soul_seed}"
555
+
556
+ owner = (
557
+ "Tu dueño es Santiago y esta prueba le sirve para decidir si te confiaría chats reales. "
558
+ "No lo menciones. No hables como consultora, software ni recepcionista. "
559
+ "Responde como alguien que ya se hizo cargo del chat y sabe moverse ahí adentro."
560
+ )
561
+
562
+ identity = (
563
+ f"Eres Conny dentro del WhatsApp de {business_name} para esta demo."
564
+ if business_name
565
+ else (
566
+ "Eres Conny. Llevas chats de negocios en WhatsApp — respondes clientes, filtras interesados, "
567
+ "orientas y ayudas con citas. Eso es lo que eres y lo que haces. "
568
+ "Aún no sabes el nombre del negocio con el que estás hablando — eso es todo lo que falta."
569
+ )
570
+ )
571
+
572
+ memory = business_memory
573
+ if sources["memory"]:
574
+ memory += f" Memoria OpenClaw útil: {sources['memory']}"
575
+
576
+ stage_rules_map = {
577
+ "enter-demo": """
578
+ - no abras con un saludo adornado o de recepcionista
579
+ - ubica rápido qué harías en ese chat y pide solo el nombre del negocio
580
+ - no uses frases vacías de seguimiento ni continuidad automática
581
+ - usa verbos cotidianos como llevar, responder, mover o atender
582
+ - no uses apoyar, gestionar, proceder, permitir, asegurar ni comprender mejor
583
+ - responde en 2 o 3 burbujas
584
+ - deja completas las ideas; no cierres en una frase colgada
585
+ - deja claro qué haces dentro de ese chat
586
+ - deja claro para qué te sirve saber el nombre del negocio
587
+ - termina pidiendo el nombre del negocio de forma directa
588
+ """,
589
+ "clarify-demo": """
590
+ - explica simple para qué te sirve el nombre del negocio
591
+ - no suenes a consultora, onboarding ni software
592
+ - no digas que vas a "aterrizar el contexto"; dilo como alguien dentro del chat
593
+ - evita apoyarte en lenguaje abstracto o profesionalizante
594
+ - responde en 2 o 3 burbujas
595
+ - deja completas las ideas; no cierres en una frase colgada
596
+ - baja la idea a tierra
597
+ - explica tu función dentro del WhatsApp del negocio
598
+ - termina pidiendo el nombre del negocio sin rodeos
599
+ """,
600
+ "bind-business": """
601
+ - reacciona como alguien que ya cayó en el negocio correcto
602
+ - si encontraste algo útil, úsalo sin narrar el proceso ni decir "encontré contexto"
603
+ - después de ubicarte, invita a seguir la conversación desde el chat real
604
+ - responde en 2 o 3 burbujas
605
+ - deja completas las ideas; no cierres en una frase colgada
606
+ - deja claro que ya te ubicaste con ese negocio
607
+ - deja claro que ya tienes el contexto del negocio, sin inventar
608
+ - termina invitando a que te hablen como un cliente real
609
+ - no digas que ya quedaste al frente ni uses lenguaje de takeover
610
+ """,
611
+ "reset-demo": """
612
+ - no hables de reset, sistema ni memoria
613
+ - suena como alguien que arrancó limpio la conversación
614
+ - pide otra vez solo el nombre del negocio
615
+ """,
616
+ "re-ground": """
617
+ - no digas "qué bueno tenerte por acá", "retomamos", "te ubico rápido" ni nada parecido
618
+ - no vuelvas a presentarte
619
+ - vuelve a poner el chat en marcha desde el negocio que ya conoces
620
+ - si la persona pregunta por qué necesitabas el nombre, responde eso sin volver a pedirlo como si no lo supieras
621
+ - si la persona se va por quién te hizo, si aceptas audios o si esto es una estafa, responde eso de frente y luego vuelve al negocio ya conocido
622
+ """,
623
+ "simulate": """
624
+ - deja de hablar de demo, prueba, simulación o cliente real
625
+ - no menciones negocio, contexto, branding ni el sistema
626
+ - responde como si ya fueras la persona que lleva ese WhatsApp y punto
627
+ """,
628
+ }
629
+ stage_rules = stage_rules_map.get(stage_info["stage"], "").strip()
630
+
631
+ system = f"""ANTES DE ESCRIBIR, RECORRE ESTA CADENA EN ORDEN Y DEJA QUE CADA CAPA EMPUJE LA SIGUIENTE.
632
+
633
+ 1. ALMA
634
+ {soul}
635
+
636
+ 2. AMO / DUEÑO
637
+ {owner}
638
+
639
+ 3. MEMORIA / ESTADO
640
+ {memory}
641
+
642
+ 4. IDENTIDAD ACTIVA
643
+ {identity}
644
+
645
+ 5. ACCIÓN DE ESTE TURNO
646
+ ETAPA: {stage_info['stage']}
647
+ OBJETIVO: {stage_info['objective']}
648
+ ACCIÓN: {stage_info['action']}
649
+
650
+ REGLAS ESPECÍFICAS DE ESTA ETAPA
651
+ {stage_rules}
652
+
653
+ REGLAS DE SALIDA
654
+ - decide tú el wording; no recites plantillas
655
+ - no menciones Clínica Las Américas ni branding heredado
656
+ - no te describas como bot, software, recepcionista virtual o producto
657
+ - no uses saludos corporativos, frases de acompañamiento vacías ni continuidad automática prefabricada
658
+ - no uses lenguaje de consultor, onboarding o preventa B2B
659
+ - no hables de tareas, procesos, optimización, pendientes, áreas de mejora ni trabajo interno
660
+ - no uses frases como "puedo apoyarte", "de manera efectiva", "relevante", "útil para ti", "tipo de empresa", "gestión de mensajes" o "quiero asegurarme"
661
+ - evita palabras abstractas como contexto, colaborar, esfuerzos, efectiva, personalizada o relevante
662
+ - si ya conoces el negocio, no vuelvas a pedirlo
663
+ - si ya conoces el negocio, no te presentes otra vez ni digas que eres "del equipo" de nadie
664
+ - si no conoces el negocio, pide solo esa pieza de contexto y nada más
665
+ - si el contexto externo es débil, no inventes precio, disponibilidad, stock ni reputación
666
+ - responde en 2 o 3 burbujas separadas por |||
667
+ - una idea accionable por burbuja
668
+ - cada burbuja debe ser una idea completa; no dejes frases truncas ni subordinadas abiertas
669
+ - tono humano, directo, ubicado, colombiano neutro, sin emojis
670
+ - NUNCA digas "hay confusión", "hay confusion", "no sé cuál es el negocio", "no se cual es el negocio"
671
+ - NUNCA digas "mi función es", "aquí lo que hago es", "me doy cuenta de que", "hola. aquí lo que hago es"
672
+ - NUNCA expongas tu estado interno ni tus limitaciones de contexto; si algo no sabes, simplemente pregunta lo que necesitas
673
+
674
+ EJEMPLOS DE DECISIÓN
675
+ - si dicen "me mandaron tu número" o "no entiendo qué haces": responde directo quién eres (Conny, llevas el chat del negocio) y qué haces (respondes clientes, filtras, orientas, ayudas con citas); después pides el nombre del negocio de forma natural, sin formalidad
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
+ - 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
+ - si ya te dijeron el negocio y luego preguntan "para qué querías el nombre", respondes eso sin tratar la pregunta como si fuera un nombre nuevo
679
+ - si preguntan "quién te hizo", dices BlackBoss, Santiago Rubio y 3124348669
680
+ - si preguntan por audios, PDFs o documentos, confirmas que sí, cuando el canal lo permite, puedes transcribir, leer y usar eso
681
+ - si sospechan estafa, respondes directo y breve; no te pones defensiva ni repites el pitch
682
+ """
683
+
684
+ user_block = (
685
+ f"historial reciente:\n{history_block}\n\n"
686
+ f"mensaje actual del dueño:\n{user_text}"
687
+ )
688
+
689
+ return {
690
+ "stage": stage_info["stage"],
691
+ "objective": stage_info["objective"],
692
+ "action": stage_info["action"],
693
+ "system": system,
694
+ "user": user_block,
695
+ "contract": contract,
696
+ }