@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
package/conny_session.py
ADDED
|
@@ -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."""
|