@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,516 @@
1
+ """
2
+ Módulo de gestión de sesiones demo para Conny Ultra.
3
+
4
+ Contiene la lógica de sesiones demo incluyendo:
5
+ - Almacenamiento de estado de sesión (_demo_sessions)
6
+ - Generación de claves de sesión (bname_key, bctx_key, etc.)
7
+ - Métodos de gestión (get/set/clear)
8
+ - Limpieza de sesiones expiradas
9
+ - Detección de idioma del owner
10
+
11
+ Este módulo fue extraído de conny.py para reducir su tamaño y mejorar mantenibilidad.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import time
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+ try:
19
+ from conny_config import Config
20
+ except ImportError:
21
+ class Config:
22
+ DEMO_SESSION_TTL = 1800
23
+ DEMO_MODE = False
24
+ DEMO_BUSINESS_NAME = "tu negocio"
25
+ DEMO_SECTOR = "estetica"
26
+
27
+
28
+
29
+ class DatabaseBackedDict:
30
+ """
31
+ Un diccionario persistente respaldado por SQLite para compartir el estado
32
+ de sesiones demo entre múltiples procesos (PM2 cluster) de manera segura.
33
+ """
34
+ def __init__(self, db_path: str):
35
+ import os
36
+ import sqlite3
37
+ self.db_path = db_path
38
+ # Asegurar que el directorio de la base de datos existe
39
+ os.makedirs(os.path.dirname(os.path.abspath(self.db_path)), exist_ok=True)
40
+ # Crear la tabla de sesiones demo si no existe
41
+ with sqlite3.connect(self.db_path, timeout=30.0) as conn:
42
+ conn.execute("""
43
+ CREATE TABLE IF NOT EXISTS demo_sessions (
44
+ key TEXT PRIMARY KEY,
45
+ value TEXT
46
+ )
47
+ """)
48
+ conn.commit()
49
+
50
+ def _conn(self):
51
+ import sqlite3
52
+ conn = sqlite3.connect(self.db_path, timeout=30.0)
53
+ conn.row_factory = sqlite3.Row
54
+ return conn
55
+
56
+ def __getitem__(self, key: str) -> Any:
57
+ import json
58
+ with self._conn() as conn:
59
+ row = conn.execute("SELECT value FROM demo_sessions WHERE key = ?", (key,)).fetchone()
60
+ if row is None:
61
+ raise KeyError(key)
62
+ try:
63
+ return json.loads(row["value"])
64
+ except Exception:
65
+ raise KeyError(key)
66
+
67
+ def __setitem__(self, key: str, value: Any) -> None:
68
+ import json
69
+ with self._conn() as conn:
70
+ conn.execute(
71
+ "INSERT OR REPLACE INTO demo_sessions (key, value) VALUES (?, ?)",
72
+ (key, json.dumps(value, ensure_ascii=False))
73
+ )
74
+ conn.commit()
75
+
76
+ def __delitem__(self, key: str) -> None:
77
+ with self._conn() as conn:
78
+ row = conn.execute("SELECT 1 FROM demo_sessions WHERE key = ?", (key,)).fetchone()
79
+ if row is None:
80
+ raise KeyError(key)
81
+ conn.execute("DELETE FROM demo_sessions WHERE key = ?", (key,))
82
+ conn.commit()
83
+
84
+ def __contains__(self, key: str) -> bool:
85
+ with self._conn() as conn:
86
+ row = conn.execute("SELECT 1 FROM demo_sessions WHERE key = ?", (key,)).fetchone()
87
+ return row is not None
88
+
89
+ def get(self, key: str, default: Any = None) -> Any:
90
+ try:
91
+ return self[key]
92
+ except KeyError:
93
+ return default
94
+
95
+ def pop(self, key: str, default: Any = None) -> Any:
96
+ try:
97
+ val = self[key]
98
+ del self[key]
99
+ return val
100
+ except KeyError:
101
+ return default
102
+
103
+ def setdefault(self, key: str, default: Any = None) -> Any:
104
+ try:
105
+ return self[key]
106
+ except KeyError:
107
+ self[key] = default
108
+ return default
109
+
110
+ def items(self) -> List[Tuple[str, Any]]:
111
+ import json
112
+ with self._conn() as conn:
113
+ rows = conn.execute("SELECT key, value FROM demo_sessions").fetchall()
114
+ res = []
115
+ for r in rows:
116
+ try:
117
+ res.append((r["key"], json.loads(r["value"])))
118
+ except Exception:
119
+ pass
120
+ return res
121
+
122
+ def keys(self) -> List[str]:
123
+ with self._conn() as conn:
124
+ rows = conn.execute("SELECT key FROM demo_sessions").fetchall()
125
+ return [r["key"] for r in rows]
126
+
127
+ def values(self) -> List[Any]:
128
+ import json
129
+ with self._conn() as conn:
130
+ rows = conn.execute("SELECT value FROM demo_sessions").fetchall()
131
+ res = []
132
+ for r in rows:
133
+ try:
134
+ res.append(json.loads(r["value"]))
135
+ except Exception:
136
+ pass
137
+ return res
138
+
139
+ def __iter__(self):
140
+ return iter(self.keys())
141
+
142
+ def __len__(self) -> int:
143
+ with self._conn() as conn:
144
+ row = conn.execute("SELECT count(*) as cnt FROM demo_sessions").fetchone()
145
+ return row["cnt"]
146
+
147
+ def clear(self) -> None:
148
+ with self._conn() as conn:
149
+ conn.execute("DELETE FROM demo_sessions")
150
+ conn.commit()
151
+
152
+
153
+ class SessionManager:
154
+ """
155
+ Gestor de sesiones demo para Conny Ultra.
156
+
157
+ Maneja el estado de sesiones demo de usuarios, incluyendo:
158
+ - Nombre del negocio (bname_key)
159
+ - Contexto/descripción del negocio (bctx_key)
160
+ - Estado de búsqueda web (bfound_key, burl_key)
161
+ - Tracking de tricks/demo commands (btrick_key)
162
+ - Persona/archetype (bpersona_key)
163
+ - Tono detectado (btone_key)
164
+ - Modelo LLM preferido (bmodel_key)
165
+ - Idioma del owner (blang_key)
166
+ - Modo aprendizaje (blearn_key)
167
+ - Modo simulación (bsim_key)
168
+ """
169
+
170
+ def __init__(self, sessions_dict: Dict[str, float] = None, emoji_chats_off: set = None):
171
+ self._demo_sessions: Dict[str, float] = sessions_dict if sessions_dict is not None else {}
172
+ self._emoji_chats_off: set = emoji_chats_off if emoji_chats_off is not None else set()
173
+
174
+ def _get_ttl(self, chat_id: str = None) -> int:
175
+ ttl = Config.DEMO_SESSION_TTL
176
+ if chat_id:
177
+ sk = f"demo_{chat_id}"
178
+ if self._demo_sessions.get(sk + "_ttl"):
179
+ try:
180
+ return int(self._demo_sessions[sk + "_ttl"])
181
+ except Exception:
182
+ pass
183
+ try:
184
+ from src.core.globals import db
185
+ if db:
186
+ clinic = db.get_clinic()
187
+ if clinic and clinic.get("demo_session_ttl"):
188
+ return int(clinic.get("demo_session_ttl"))
189
+ db_ttl = db.recall("demo_session_ttl")
190
+ if db_ttl:
191
+ return int(db_ttl)
192
+ except Exception:
193
+ pass
194
+ return ttl
195
+
196
+ def is_demo_mode_active(self) -> bool:
197
+ """Verifica si hay sesiones demo activas."""
198
+ now = time.time()
199
+ return any(
200
+ k.endswith("_ts") and (now - v) < self._get_ttl(k.replace("_ts", "").replace("demo_", ""))
201
+ for k, v in self._demo_sessions.items()
202
+ )
203
+
204
+ def get_demo_session(self, chat_id: str) -> Dict[str, any]:
205
+ """
206
+ Obtiene todos los valores de sesión para un chat_id.
207
+
208
+ Args:
209
+ chat_id: Identificador del chat
210
+
211
+ Returns:
212
+ Dict con todas las claves de sesión del demo
213
+ """
214
+ sk = f"demo_{chat_id}"
215
+ keys = [
216
+ f"{sk}_name", f"{sk}_ctx", f"{sk}_found", f"{sk}_url",
217
+ f"{sk}_trick", f"{sk}_persona", f"{sk}_tone", f"{sk}_model",
218
+ f"{sk}_owner_lang", f"{sk}_learn", f"{sk}_sim_mode"
219
+ ]
220
+ return {k.replace(sk + "_", ""): self._demo_sessions.get(k) for k in keys}
221
+
222
+ def set_demo_session(self, chat_id: str, key: str, value: any) -> None:
223
+ """
224
+ Establece un valor en la sesión demo.
225
+
226
+ Args:
227
+ chat_id: Identificador del chat
228
+ key: Clave (sin prefijo demo_{chat_id}_)
229
+ value: Valor a almacenar
230
+ """
231
+ sk = f"demo_{chat_id}"
232
+ self._demo_sessions[f"{sk}_{key}"] = value
233
+ if key == "ts":
234
+ self._touch_session(chat_id)
235
+
236
+ def clear_demo_session(self, chat_id: str) -> None:
237
+ """
238
+ Limpia todos los datos de sesión para un chat_id.
239
+
240
+ Args:
241
+ chat_id: Identificador del chat
242
+ """
243
+ sk = f"demo_{chat_id}"
244
+ keys_to_delete = [k for k in list(self._demo_sessions) if k.startswith(sk + "_")]
245
+ for k in keys_to_delete:
246
+ del self._demo_sessions[k]
247
+
248
+ def _touch_session(self, chat_id: str) -> None:
249
+ """Actualiza el timestamp de la sesión."""
250
+ sk = f"demo_{chat_id}"
251
+ self._demo_sessions[f"{sk}_ts"] = time.time()
252
+
253
+ def cleanup_expired_sessions(self) -> int:
254
+ """
255
+ Limpia sesiones expiradas basándose en TTL.
256
+
257
+ Returns:
258
+ Número de sesiones limpiadas
259
+ """
260
+ now = time.time()
261
+ ttl = Config.DEMO_SESSION_TTL
262
+
263
+ expired_keys = []
264
+ for k, v in self._demo_sessions.items():
265
+ if k.endswith("_ts") and (now - v) > ttl * 2:
266
+ expired_keys.append(k)
267
+
268
+ for k in expired_keys:
269
+ chat_id = k.replace("_ts", "").replace("demo_", "")
270
+ self.clear_demo_session(chat_id)
271
+
272
+ return len(expired_keys)
273
+
274
+ def generate_session_keys(self, chat_id: str) -> Dict[str, str]:
275
+ """
276
+ Genera todas las claves de sesión para un chat_id.
277
+
278
+ Args:
279
+ chat_id: Identificador del chat
280
+
281
+ Returns:
282
+ Dict con todas las claves de sesión generadas
283
+ """
284
+ sk = f"demo_{chat_id}"
285
+ return {
286
+ "bname_key": sk + "_name",
287
+ "bctx_key": sk + "_ctx",
288
+ "bfound_key": sk + "_found",
289
+ "burl_key": sk + "_url",
290
+ "btrick_key": sk + "_trick",
291
+ "bpersona_key": sk + "_persona",
292
+ "btone_key": sk + "_tone",
293
+ "bmodel_key": sk + "_model",
294
+ "blang_key": sk + "_owner_lang",
295
+ "blearn_key": sk + "_learn",
296
+ "bsim_key": sk + "_sim_mode",
297
+ "sk": sk,
298
+ }
299
+
300
+ def get_session_value(self, chat_id: str, key: str, default: any = None) -> any:
301
+ """Obtiene un valor específico de la sesión."""
302
+ sk = f"demo_{chat_id}"
303
+ return self._demo_sessions.get(f"{sk}_{key}", default)
304
+
305
+ def set_session_value(self, chat_id: str, key: str, value: any) -> None:
306
+ """Establece un valor específico en la sesión."""
307
+ sk = f"demo_{chat_id}"
308
+ self._demo_sessions[f"{sk}_{key}"] = value
309
+ self._touch_session(chat_id)
310
+
311
+ def set_timestamp(self, chat_id: str) -> None:
312
+ """Actualiza el timestamp de la sesión sin setear un valor de clave."""
313
+ self._touch_session(chat_id)
314
+
315
+ def touch_and_cleanup(self, chat_id: str) -> Tuple[bool, List[str]]:
316
+ """
317
+ Toca el timestamp y limpia sesiones expiradas.
318
+ Retorna (is_new, keys_to_delete) donde keys_to_delete son las claves
319
+ que deben borrarse si is_new=True.
320
+ """
321
+ now = time.time()
322
+ ttl = self._get_ttl(chat_id)
323
+ sk = f"demo_{chat_id}"
324
+ last_seen = self._demo_sessions.get(sk + "_ts", 0)
325
+ is_new = (now - last_seen) > ttl
326
+ self._demo_sessions[sk + "_ts"] = now
327
+
328
+ # Limpiar sesiones expiradas in-place sin reasignar el diccionario
329
+ expired_keys = [
330
+ k for k, v in list(self._demo_sessions.items())
331
+ if k.endswith("_ts") and (now - v) >= ttl * 2
332
+ ]
333
+ for ek in expired_keys:
334
+ expired_chat_id = ek.replace("_ts", "").replace("demo_", "")
335
+ sk_expired = f"demo_{expired_chat_id}"
336
+ keys_to_del = [k for k in list(self._demo_sessions) if k.startswith(sk_expired)]
337
+ for kd in keys_to_del:
338
+ self._demo_sessions.pop(kd, None)
339
+
340
+ # Borrar la caché de sesión y la base de datos para la sesión expirada
341
+ try:
342
+ from conny_memory import get_memory
343
+ instance_id = "default"
344
+ from src.core.globals import db
345
+ if db:
346
+ remembered_slug = (db.recall("instance_slug") or "").strip()
347
+ if remembered_slug:
348
+ instance_id = remembered_slug.lower()
349
+ mem = get_memory(instance_id)
350
+ mem.delete_session_cache(expired_chat_id)
351
+ except Exception:
352
+ pass
353
+ try:
354
+ from src.core.globals import db
355
+ if db:
356
+ with db._conn() as c:
357
+ c.execute("DELETE FROM conversations WHERE chat_id=?", (expired_chat_id,))
358
+ except Exception:
359
+ pass
360
+
361
+ if is_new:
362
+ keys_to_delete = [k for k in list(self._demo_sessions)
363
+ if k.startswith(sk + "_") and not k.endswith("_ts")]
364
+ # Borrar la caché de sesión y la base de datos para este chat_id al iniciar una nueva sesión
365
+ try:
366
+ from conny_memory import get_memory
367
+ instance_id = "default"
368
+ from src.core.globals import db
369
+ if db:
370
+ remembered_slug = (db.recall("instance_slug") or "").strip()
371
+ if remembered_slug:
372
+ instance_id = remembered_slug.lower()
373
+ mem = get_memory(instance_id)
374
+ mem.delete_session_cache(chat_id)
375
+ except Exception:
376
+ pass
377
+ try:
378
+ from src.core.globals import db
379
+ if db:
380
+ with db._conn() as c:
381
+ c.execute("DELETE FROM conversations WHERE chat_id=?", (chat_id,))
382
+ except Exception:
383
+ pass
384
+ else:
385
+ keys_to_delete = []
386
+ return is_new, keys_to_delete
387
+
388
+ def is_session_new(self, chat_id: str) -> bool:
389
+ """Determina si la sesión es nueva (expiró el TTL)."""
390
+ sk = f"demo_{chat_id}"
391
+ now = time.time()
392
+ last_seen = self._demo_sessions.get(sk + "_ts", 0)
393
+ return (now - last_seen) > Config.DEMO_SESSION_TTL
394
+
395
+ def reset_session(self, chat_id: str) -> None:
396
+ """Resetea completamente la sesión (para expiración)."""
397
+ sk = f"demo_{chat_id}"
398
+ keys_del = [k for k in list(self._demo_sessions)
399
+ if k.startswith(sk + "_") and not k.endswith("_ts")]
400
+ for k in keys_del:
401
+ del self._demo_sessions[k]
402
+ self._touch_session(chat_id)
403
+
404
+
405
+ def _detect_demo_owner_language(raw_text: str, current_lang: str = "es") -> str:
406
+ """
407
+ Detecta el idioma del owner en modo demo basándose en señales explícitas.
408
+
409
+ Args:
410
+ raw_text: Mensaje original del usuario
411
+ current_lang: Idioma actual detectado
412
+
413
+ Returns:
414
+ Código de idioma ('es', 'en', 'pt')
415
+ """
416
+ try:
417
+ from conny_helpers import _normalize_conv_text
418
+ except ImportError:
419
+ def _normalize_conv_text(s):
420
+ return s.lower().strip() if s else ""
421
+
422
+ normalized = _normalize_conv_text(raw_text or "")
423
+ if not normalized:
424
+ return current_lang or "es"
425
+
426
+ explicit_en = (
427
+ "just english sorry", "sorry just english", "english sorry",
428
+ "english only", "speak english", "speak in english",
429
+ "i dont speak spanish", "i don t speak spanish",
430
+ "i dont talk spanish", "i don t talk spanish",
431
+ "no spanish", "only english", "what is this", "sorry what is this",
432
+ "i dont understand", "i don t understand",
433
+ "what did you say", "what did u say",
434
+ "thats not my business", "that s not my business",
435
+ "thats not us", "that s not us",
436
+ "wrong business", "wrong company",
437
+ )
438
+ explicit_pt = (
439
+ "só portugues", "so portugues", "falo portugues",
440
+ "nao falo espanhol", "não falo espanhol",
441
+ )
442
+
443
+ if any(token in normalized for token in explicit_en):
444
+ return "en"
445
+ if any(token in normalized for token in explicit_pt):
446
+ return "pt"
447
+
448
+ try:
449
+ from multilingual import MultilingualHandler
450
+ detected = MultilingualHandler().detect(raw_text)
451
+ except Exception:
452
+ detected = "es"
453
+
454
+ if current_lang in {"en", "pt"} and detected == "es" and len(normalized.split()) <= 6:
455
+ return current_lang
456
+
457
+ return detected if detected in {"es", "en", "pt"} else (current_lang or "es")
458
+
459
+
460
+ def _owner_confusion_or_language_signal(raw_text: str) -> bool:
461
+ """
462
+ Detecta señales de confusión del owner o cambio de idioma.
463
+
464
+ Args:
465
+ raw_text: Mensaje del usuario
466
+
467
+ Returns:
468
+ True si detecta señal de confusión/language switch
469
+ """
470
+ try:
471
+ from conny_helpers import _normalize_conv_text
472
+ except ImportError:
473
+ def _normalize_conv_text(s):
474
+ return s.lower().strip() if s else ""
475
+
476
+ normalized = _normalize_conv_text(raw_text or "")
477
+ if not normalized:
478
+ return False
479
+
480
+ signals = (
481
+ "just english sorry", "sorry just english", "english sorry",
482
+ "english only", "speak english", "speak in english", "only english",
483
+ "i dont speak spanish", "i don t speak spanish",
484
+ "i dont talk spanish", "i don t talk spanish",
485
+ "what is this", "sorry what is this",
486
+ "i dont understand", "i don t understand",
487
+ "what did you say", "what did u say",
488
+ "thats not my business", "that s not my business",
489
+ "that is not my business", "not my business",
490
+ "thats not us", "that s not us", "that is not us",
491
+ "wrong business", "wrong company", "wrong one", "not the right one",
492
+ "no hablo español", "no hablo espanol",
493
+ "solo ingles", "solo inglés",
494
+ )
495
+ return any(signal in normalized for signal in signals)
496
+
497
+
498
+ def _lang_text(es_text: str, en_text: str, pt_text: Optional[str] = None,
499
+ owner_lang: str = "es") -> str:
500
+ """
501
+ Retorna texto según el idioma del owner.
502
+
503
+ Args:
504
+ es_text: Texto en español
505
+ en_text: Texto en inglés
506
+ pt_text: Texto en portugués (opcional)
507
+ owner_lang: Idioma del owner
508
+
509
+ Returns:
510
+ Texto en el idioma apropiado
511
+ """
512
+ if owner_lang == "en":
513
+ return en_text
514
+ if owner_lang == "pt" and pt_text is not None:
515
+ return pt_text
516
+ return es_text
@@ -0,0 +1 @@
1
+ """Conny skills scaffold."""
@@ -0,0 +1,35 @@
1
+ """Demo mode multilingual support."""
2
+ from conny_i18n import get_i18n, SUPPORTED_LANGUAGES
3
+
4
+ _I18N = None
5
+ try:
6
+ _I18N = get_i18n()
7
+ except Exception:
8
+ pass
9
+
10
+
11
+ def demo_t(key: str) -> str:
12
+ if _I18N:
13
+ return _I18N.demo(key)
14
+ defaults = {
15
+ "enter_name": "Enter your name",
16
+ "enter_email": "Enter your email",
17
+ "enter_phone": "Enter your phone",
18
+ "ask_interest": "What are you interested in?",
19
+ "schedule_demo": "Schedule demo",
20
+ "demo_confirmed": "Demo confirmed! We'll contact you soon.",
21
+ "company_name": "Company name",
22
+ "company_size": "Company size",
23
+ }
24
+ return defaults.get(key, key)
25
+
26
+
27
+ def get_demo_greeting(lang: str = "es") -> str:
28
+ greetings = {
29
+ "es": "¡Hola! Te gustaría ver una demostración de Conny?",
30
+ "en": "Hello! Would you like to see a demo of Conny?",
31
+ "pt": "Olá! Você gostaria de ver uma demonstração da Conny?",
32
+ "fr": "Bonjour! Voulez-vous voir une démo de Conny?",
33
+ "de": "Hallo! Möchten Sie eine Demo von Conny sehen?",
34
+ }
35
+ return greetings.get(lang, greetings["es"])
@@ -0,0 +1 @@
1
+ """Text processing scaffold."""
@@ -0,0 +1 @@
1
+ """Tone detection scaffold."""