@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.
- package/.env.example +68 -0
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
- package/brand-assets/Conny.web.logo.png +0 -0
- package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
- package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/conny-demo/manifest.json +22 -0
- package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
- package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
- package/brand-assets/conny-logo.png +0 -0
- package/brand-assets/web.background.png +0 -0
- package/brand_assets.py +323 -0
- package/conny +28 -0
- package/conny-chat.py +579 -0
- package/conny-omni.py +3843 -0
- package/conny.py +113 -0
- package/conny_agents/__init__.py +1 -0
- package/conny_agents/agenda.py +1 -0
- package/conny_agents/captacion.py +1 -0
- package/conny_agents/conocimiento.py +1 -0
- package/conny_agents/escalacion.py +1 -0
- package/conny_agents/objeciones.py +1 -0
- package/conny_agents/seguimiento.py +1 -0
- package/conny_app.py +287 -0
- package/conny_audio.py +350 -0
- package/conny_audio_learn.py +84 -0
- package/conny_brain_v10.py +804 -0
- package/conny_bridge.py +656 -0
- package/conny_calendar.py +169 -0
- package/conny_cli.py +11784 -0
- package/conny_cli_bb.py +437 -0
- package/conny_commands.py +243 -0
- package/conny_config.py +215 -0
- package/conny_core/__init__.py +3 -0
- package/conny_core/conversation_engine.py +446 -0
- package/conny_core/first_turn_ops.py +287 -0
- package/conny_core/persona_registry.py +157 -0
- package/conny_core/prompt_ops.py +561 -0
- package/conny_cron.py +72 -0
- package/conny_demo_v2.py +209 -0
- package/conny_demo_voice.py +134 -0
- package/conny_design.py +43 -0
- package/conny_doctor.py +319 -0
- package/conny_domino.py +696 -0
- package/conny_generator.py +447 -0
- package/conny_google_auth.py +159 -0
- package/conny_i18n.py +619 -0
- package/conny_init.py +509 -0
- package/conny_integrations/__init__.py +4 -0
- package/conny_integrations/llm.py +1 -0
- package/conny_integrations/vault.py +77 -0
- package/conny_integrations/whatsapp.py +1 -0
- package/conny_intelligence.py +65 -0
- package/conny_learning.py +154 -0
- package/conny_memory.py +243 -0
- package/conny_memory_engine.py +292 -0
- package/conny_nova_proxy.py +170 -0
- package/conny_nuke_robot_phrases.py +493 -0
- package/conny_pairing.py +253 -0
- package/conny_patch.py +291 -0
- package/conny_persona_cli.py +150 -0
- package/conny_router.py +308 -0
- package/conny_runtime_ops.py +271 -0
- package/conny_session.py +516 -0
- package/conny_skills/__init__.py +1 -0
- package/conny_skills/demo_mode.py +35 -0
- package/conny_skills/text_processing.py +1 -0
- package/conny_skills/tone_detection.py +1 -0
- package/conny_smart_features.py +333 -0
- package/conny_studio.py +161 -0
- package/conny_sync_fix.py +306 -0
- package/conny_tui.py +512 -0
- package/conny_tui_select.py +202 -0
- package/conny_ultra_config.py +411 -0
- package/conny_uncertainty.py +174 -0
- package/conny_utils.py +87 -0
- package/conny_voice.py +156 -0
- package/conny_voice_engine.py +124 -0
- package/conny_web_search.py +66 -0
- package/conny_weekly_report.py +85 -0
- package/conny_worm.py +88 -0
- package/core/__init__.py +25 -0
- package/ecosystem.config.js +24 -0
- package/fix_init.py +27 -0
- package/install.sh +78 -0
- package/knowledge_base.py +330 -0
- package/nova/rules/default.yaml +37 -0
- package/nova_bridge.py +509 -0
- package/npm/conny.js +471 -0
- package/package.json +102 -0
- package/personas/conny/base/default.yaml +35 -0
- package/personas/conny/base/estetica_whatsapp.yaml +36 -0
- package/requirements.txt +14 -0
- package/run.sh +47 -0
- package/search.py +465 -0
- package/smart_handoff.py +1150 -0
- package/src/__init__.py +0 -0
- package/src/conny/__init__.py +0 -0
- package/src/conny/admin/__init__.py +0 -0
- package/src/conny/admin/api.py +234 -0
- package/src/conny/admin/dashboard.py +772 -0
- package/src/conny/api/__init__.py +0 -0
- package/src/conny/api/routes.py +8851 -0
- package/src/conny/brain/__init__.py +15 -0
- package/src/conny/brain/engine.py +804 -0
- package/src/conny/brain/learning.py +154 -0
- package/src/conny/brain/memory.py +324 -0
- package/src/conny/brain/smart_features.py +333 -0
- package/src/conny/brain/uncertainty.py +167 -0
- package/src/conny/channels/__init__.py +0 -0
- package/src/conny/channels/audio.py +316 -0
- package/src/conny/channels/cli.py +11795 -0
- package/src/conny/channels/logo_art.py +11 -0
- package/src/conny/channels/voice.py +156 -0
- package/src/conny/core/__init__.py +0 -0
- package/src/conny/core/config.py +215 -0
- package/src/conny/core/cron.py +72 -0
- package/src/conny/core/messenger.py +563 -0
- package/src/conny/core/router.py +297 -0
- package/src/conny/core/session.py +312 -0
- package/src/conny/demo/__init__.py +0 -0
- package/src/conny/demo/handler.py +3110 -0
- package/src/conny/integrations/__init__.py +19 -0
- package/src/conny/integrations/calendar.py +169 -0
- package/src/conny/integrations/knowledge.py +312 -0
- package/src/conny/integrations/search.py +66 -0
- package/src/conny/personas/__init__.py +0 -0
- package/src/conny/personas/generator.py +447 -0
- package/src/conny/production/__init__.py +0 -0
- package/src/conny/production/domino.py +696 -0
- package/src/conny/production/guard.py +550 -0
- package/src/conny/production/handoff.py +1150 -0
- package/src/conny/production/monitor.py +353 -0
- package/src/conny/utils/__init__.py +2 -0
- package/src/conny/utils/helpers.py +75 -0
- package/src/conny/utils/i18n.py +619 -0
- package/src/core/admin_engines.py +772 -0
- package/src/core/globals.py +11845 -0
- package/src/core/orchestrator.py +273 -0
- package/src/core/production_monitor.py +353 -0
- package/src/core/runtime.py +5487 -0
- package/src/domain/onboarding_flow.py +230 -0
- package/src/domain/prompts/__init__.py +1 -0
- package/src/domain/prompts/prospect_pitch.py +282 -0
- package/src/domain/send_guard.py +636 -0
- package/src/domain/swarm/queen.py +96 -0
- package/src/infrastructure/llm_providers/engine.py +487 -0
- package/src/interfaces/mcp_server.py +73 -0
- package/src/interfaces/nova_bridge.py +58 -0
- package/src/interfaces/web/admin_api.py +1379 -0
- package/src/interfaces/web/app.py +9408 -0
- package/src/interfaces/web/demo_handler.py +3450 -0
- package/src/interfaces/web/static/generate_avatars.py +46 -0
- package/v7/__init__.py +46 -0
- package/v7/agents/__init__.py +46 -0
- package/v7/agents/agenda.py +77 -0
- package/v7/agents/base.py +216 -0
- package/v7/agents/captacion.py +60 -0
- package/v7/agents/conocimiento.py +69 -0
- package/v7/agents/escalacion.py +83 -0
- package/v7/agents/objeciones.py +109 -0
- package/v7/agents/seguimiento.py +71 -0
- package/v7/memory/__init__.py +46 -0
- package/v7/memory/patient_profile.py +200 -0
- package/v7/orchestrator.py +275 -0
- package/v7/postprocess.py +127 -0
- package/v7/router.py +239 -0
- 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
|
+
}
|