@innvisor/conny-ai 9.7.0 → 9.8.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/CHANGELOG.md +54 -0
- package/README.md +17 -1
- package/conny_app.py +8 -2
- package/conny_cli.py +103 -11
- package/conny_core/evolution.py +112 -0
- package/conny_core/first_turn_ops.py +16 -20
- package/conny_core/prompt_ops.py +62 -0
- package/conny_demo_voice.py +1 -1
- package/conny_doctor.py +287 -2
- package/conny_domino.py +2 -2
- package/conny_generator.py +1 -1
- package/conny_init.py +234 -41
- package/conny_runtime_ops.py +198 -6
- package/conny_ultra_config.py +25 -11
- package/conny_utils.py +21 -3
- package/ecosystem.config.js +11 -1
- package/install.sh +78 -22
- package/npm/conny.js +73 -17
- package/package.json +13 -3
- package/run.sh +7 -0
- package/src/conny/admin/dashboard.py +35 -4
- package/src/conny/admin_memory.py +93 -0
- package/src/conny/api/routes.py +26 -9
- package/src/conny/channels/cli.py +30 -9
- package/src/conny/demo/handler.py +23 -23
- package/src/conny/personas/generator.py +1 -1
- package/src/conny/production/domino.py +2 -2
- package/src/conny/production/guard.py +4 -4
- package/src/core/admin_engines.py +51 -48
- package/src/core/globals.py +110 -9
- package/src/core/production_monitor.py +63 -38
- package/src/core/runtime.py +343 -305
- package/src/domain/prompts/prospect_pitch.py +11 -11
- package/src/domain/send_guard.py +4 -4
- package/src/interfaces/web/app.py +91 -27
- package/src/interfaces/web/demo_admin_commands.py +165 -0
- package/src/interfaces/web/demo_handler.py +178 -34
- package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +0 -22
- package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +0 -11
- package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +0 -11
- package/brand-assets/cl-nica-las-am-ricas/manifest.json +0 -22
- package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +0 -11
- package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +0 -11
- package/brand-assets/conny-demo/manifest.json +0 -22
- package/brand-assets/conny-demo/processed/business-identity.txt +0 -7
- package/brand-assets/conny-demo/raw/business-identity.txt +0 -7
- package/fix_init.py +0 -27
- package/verify_conversation_impl.py +0 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.8.0 - 2026-06-02
|
|
4
|
+
|
|
5
|
+
- blocked first-contact setup for unknown chats until a valid activation token is provided
|
|
6
|
+
- added `conny token --admin` with `ADMN-` Conny Pro Admin tokens, offline SQLite generation and web/API developer access
|
|
7
|
+
- fixed token casing and case-insensitive token lookup so generated tokens validate reliably
|
|
8
|
+
- made `/api/activate`, web registration and developer registration accept Conny Pro Admin tokens safely
|
|
9
|
+
- added admin Soul memory folders under `soul/admins/<chat_id>` for persistent operator context and learned business/ops facts
|
|
10
|
+
- introduced typed LLM service errors so quota/API-key failures are shown to admins before any fallback
|
|
11
|
+
- changed admin fallback into an explicit opt-in flow via `continuar fallback`
|
|
12
|
+
- cleaned npm packaging to exclude patch/fix scripts and private per-business brand asset folders
|
|
13
|
+
- fixed first contact greeting regression (`Holaal`) and restored admin capability fallback for audios, PDFs and documents
|
|
14
|
+
|
|
15
|
+
## 9.7.6 - 2026-05-26
|
|
16
|
+
|
|
17
|
+
- fixed demo LLM resolution through the Conny facade so tests and runtime use the configured engine instead of a stale runtime module
|
|
18
|
+
- removed the fake “internet dropped” demo fallback and replaced it with contextual owner onboarding when every model response is empty or unusable
|
|
19
|
+
- preserved continuity in owner demo flows so Conny does not repeat “soy una IA” after capability context was already explained
|
|
20
|
+
- verified Python, Node/npm and shell entrypoints for Linux/macOS/PowerShell-compatible npm usage
|
|
21
|
+
|
|
22
|
+
## 9.7.5 - 2026-05-26
|
|
23
|
+
|
|
24
|
+
- unified `conny init`, `conny config` and `conny doctor` around the active instance `.env`
|
|
25
|
+
- mirrored provisioned instance secrets and URLs into the base runtime `.env` after setup and config edits
|
|
26
|
+
- replaced brittle `.env` text replacement with parser-based updates that handle quoted, unquoted and `pending` values
|
|
27
|
+
- added dashboard exposure setup to `conny init` for localhost, LAN/IP and custom public URLs
|
|
28
|
+
|
|
29
|
+
## 9.7.4 - 2026-05-26
|
|
30
|
+
|
|
31
|
+
- fixed existing installs with a stale/incomplete Python runtime by making bootstrap health check required imports before every command
|
|
32
|
+
- added `conny --bootstrap-check` and wired the shell installer to run it before reporting success
|
|
33
|
+
- ensured missing `rich`/CLI dependencies are repaired during install instead of surfacing later at `conny init`
|
|
34
|
+
|
|
35
|
+
## 9.7.3 - 2026-05-26
|
|
36
|
+
|
|
37
|
+
- fixed Termux/proot installs where `conny init` could start without `rich` installed
|
|
38
|
+
- made the npm bootstrap install critical CLI dependencies first and fail loudly if they are missing
|
|
39
|
+
- kept heavy production dependencies best-effort so packages like `scikit-learn` cannot leave the CLI half-installed
|
|
40
|
+
- expanded runtime health checks to include `rich`, `deep_translator`, `questionary`, `fastapi`, `httpx`, `dotenv` and `pydantic`
|
|
41
|
+
|
|
42
|
+
## 9.7.2 - 2026-05-26
|
|
43
|
+
|
|
44
|
+
- removed the obsolete boxed `CONNY ULTRA CONFIG v9.7.0` layout from `conny config`
|
|
45
|
+
- added a Gateway/Webhook step to `conny init` with automatic `localhost.run` tunneling or manual `BASE_URL`
|
|
46
|
+
- fixed tunnel routing to target the active instance `PORT` instead of stale defaults like `8002`
|
|
47
|
+
- made webhook sync reload the real `.env` and restart PM2 with `--update-env` before calling Telegram `setWebhook`
|
|
48
|
+
- unified the default instances path under `~/.conny/instances`
|
|
49
|
+
|
|
50
|
+
## 9.7.1 - 2026-05-26
|
|
51
|
+
|
|
52
|
+
- fixed GitHub/npm bootstrap for fresh installs where `conny init` still tried to run from `/home/ubuntu/conny`
|
|
53
|
+
- removed the hardcoded working directory in `conny_app.py`; subcommands now run from the installed `CONNY_DIR`
|
|
54
|
+
- bumped the package version so existing `~/.conny/repo` installs resync automatically on reinstall
|
|
55
|
+
- kept the public package name as `conny-ai` for GitHub installs and npm compatibility
|
|
56
|
+
|
|
3
57
|
## 9.7.0 - 2026-05-26
|
|
4
58
|
|
|
5
59
|
- corregido el arranque PM2 para usar `run.sh` y selección dinámica de Python, evitando `--interpreter python3` duro y rutas rotas de venv
|
package/README.md
CHANGED
|
@@ -123,6 +123,22 @@ It's a **white-label AI receptionist platform** that lets you sell branded conve
|
|
|
123
123
|
|
|
124
124
|
### Step 1 — Install Conny
|
|
125
125
|
|
|
126
|
+
Recommended GitHub installer (always tracks the current `main` branch):
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
curl -fsSL https://raw.githubusercontent.com/sxrubyo/conny/latest/install.sh | bash
|
|
130
|
+
conny --version
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Direct GitHub install with npm:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm install -g github:sxrubyo/conny#main
|
|
137
|
+
conny --version
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Registry install:
|
|
141
|
+
|
|
126
142
|
```bash
|
|
127
143
|
npm install -g conny-ai
|
|
128
144
|
conny --version
|
|
@@ -242,7 +258,7 @@ backups/ # Local backups
|
|
|
242
258
|
## 🎮 CLI Reference
|
|
243
259
|
|
|
244
260
|
```bash
|
|
245
|
-
curl -fsSL https://raw.githubusercontent.com/sxrubyo/conny/
|
|
261
|
+
curl -fsSL https://raw.githubusercontent.com/sxrubyo/conny/latest/install.sh | bash
|
|
246
262
|
conny --version # Check version
|
|
247
263
|
|
|
248
264
|
conny sync --list # List all client instances
|
package/conny_app.py
CHANGED
|
@@ -43,6 +43,8 @@ TAGLINES = [
|
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
BOOT_FILE = Path.home() / ".conny" / ".boot_shown"
|
|
46
|
+
APP_DIR = Path(os.environ.get("CONNY_DIR", str(Path(__file__).resolve().parent))).resolve()
|
|
47
|
+
INSTANCES_DIR = Path(os.environ.get("INSTANCES_DIR", str(Path.home() / ".conny" / "instances"))).resolve()
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
# ─── Boot ─────────────────────────────────────────────────────────────────────
|
|
@@ -156,7 +158,7 @@ def cmd_status(args=""):
|
|
|
156
158
|
con.print(); con.print(Padding(t,(0,2))); con.print()
|
|
157
159
|
|
|
158
160
|
def cmd_list(args=""):
|
|
159
|
-
idir =
|
|
161
|
+
idir = INSTANCES_DIR
|
|
160
162
|
if not idir.exists(): con.print(" [dim]no instances[/dim]"); return
|
|
161
163
|
t = Table(box=box.SIMPLE, border_style=COLORS["primary"], show_edge=False, padding=(0,1))
|
|
162
164
|
t.add_column("", width=3); t.add_column("name", style="bold")
|
|
@@ -264,7 +266,11 @@ def _uptime(ms):
|
|
|
264
266
|
return f"{int(s/86400)}d"
|
|
265
267
|
|
|
266
268
|
def _py(*a):
|
|
267
|
-
try:
|
|
269
|
+
try:
|
|
270
|
+
env = os.environ.copy()
|
|
271
|
+
env.setdefault("CONNY_DIR", str(APP_DIR))
|
|
272
|
+
env.setdefault("INSTANCES_DIR", str(INSTANCES_DIR))
|
|
273
|
+
subprocess.run([sys.executable]+[str(x) for x in a],cwd=str(APP_DIR),env=env)
|
|
268
274
|
except Exception as e: con.print(f" [err]{e}[/err]")
|
|
269
275
|
|
|
270
276
|
def _sh(*a):
|
package/conny_cli.py
CHANGED
|
@@ -250,7 +250,7 @@ def build_pm2_start_command(instance_dir: str, pm2_name: str, log_dir: str) -> L
|
|
|
250
250
|
]
|
|
251
251
|
|
|
252
252
|
|
|
253
|
-
VERSION = "9.
|
|
253
|
+
VERSION = "9.8.0"
|
|
254
254
|
CONNY_HOME = os.getenv("CONNY_HOME", str(Path.home() / ".conny"))
|
|
255
255
|
CONNY_DIR = os.getenv("CONNY_DIR", str(Path(__file__).resolve().parent))
|
|
256
256
|
INSTANCES_DIR = _default_instances_dir()
|
|
@@ -876,7 +876,7 @@ def prompt(label, default="", secret=False):
|
|
|
876
876
|
d = f" [{default}]" if default else ""
|
|
877
877
|
try:
|
|
878
878
|
import getpass
|
|
879
|
-
v =
|
|
879
|
+
v = input(f"\n {q(C.P2, '▸')} {label}{q(C.G3, d)} ") if secret else input(f"\n {q(C.P2, '▸')} {label}{q(C.G3, d)} ")
|
|
880
880
|
except (EOFError, KeyboardInterrupt):
|
|
881
881
|
print()
|
|
882
882
|
return default
|
|
@@ -6296,11 +6296,15 @@ def cmd_token(args):
|
|
|
6296
6296
|
|
|
6297
6297
|
Uso:
|
|
6298
6298
|
conny token → genera para la instancia base
|
|
6299
|
+
conny token --admin → genera token Conny Pro Admin
|
|
6299
6300
|
conny token 1 → genera para la instancia 1
|
|
6300
6301
|
conny token "Clinica Demo" → genera con ese nombre
|
|
6301
6302
|
conny token all → genera para todas las instancias
|
|
6302
6303
|
"""
|
|
6303
6304
|
name = getattr(args, 'name', '') or getattr(args, 'subcommand', '') or ''
|
|
6305
|
+
admin_mode = bool(getattr(args, "admin", False)) or name.lower() in ("--admin", "admin", "pro")
|
|
6306
|
+
if name.lower() in ("--admin", "admin", "pro"):
|
|
6307
|
+
name = ""
|
|
6304
6308
|
|
|
6305
6309
|
def _gen_token(inst, label_override=""):
|
|
6306
6310
|
ev = dict(load_env(f"{inst.dir}/.env"))
|
|
@@ -6310,11 +6314,67 @@ def cmd_token(args):
|
|
|
6310
6314
|
return
|
|
6311
6315
|
label = label_override or inst.label
|
|
6312
6316
|
base_url = f"http://localhost:{inst.port}"
|
|
6317
|
+
payload = {"clinic_label": label}
|
|
6318
|
+
if admin_mode:
|
|
6319
|
+
payload["token_type"] = "admin_pro"
|
|
6320
|
+
payload["admin"] = True
|
|
6321
|
+
|
|
6322
|
+
def _print_token(token, expires, offline=False):
|
|
6323
|
+
kind = "Conny Pro Admin" if admin_mode else "activación"
|
|
6324
|
+
suffix = " offline" if offline else ""
|
|
6325
|
+
ok(f"Token {kind}{suffix} generado para: {q(C.YLW, label)}")
|
|
6326
|
+
print(f"\n {q(C.YLW, token, bold=True)}\n")
|
|
6327
|
+
info(f"Expira: {expires}")
|
|
6328
|
+
if admin_mode:
|
|
6329
|
+
info("Activa el modo operador: el admin lo escribe en WhatsApp/Telegram para tomar control de la instancia.")
|
|
6330
|
+
else:
|
|
6331
|
+
info("Envialo al admin — lo escribe en el chat de WhatsApp/Telegram")
|
|
6332
|
+
|
|
6333
|
+
def _gen_offline(reason: str):
|
|
6334
|
+
import secrets as _secrets
|
|
6335
|
+
import re as _re
|
|
6336
|
+
db_path = inst.db_path
|
|
6337
|
+
if not db_path:
|
|
6338
|
+
fail(f"No se pudo generar: {reason}")
|
|
6339
|
+
return
|
|
6340
|
+
label_clean = _re.sub(r'[^a-zA-Z0-9]', '', label.lower())[:10].upper() or ("ADMIN" if admin_mode else "GENERIC")
|
|
6341
|
+
entropy = _secrets.token_hex(18 if admin_mode else 16).upper()
|
|
6342
|
+
token = f"{'ADMN' if admin_mode else 'ACTV'}-{label_clean}-{entropy}"
|
|
6343
|
+
expires_at = (datetime.now() + timedelta(hours=72)).isoformat()
|
|
6344
|
+
stored_label = f"ADMIN_PRO:{label}" if admin_mode else label
|
|
6345
|
+
try:
|
|
6346
|
+
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
6347
|
+
conn = sqlite3.connect(db_path)
|
|
6348
|
+
cur = conn.cursor()
|
|
6349
|
+
cur.execute("""
|
|
6350
|
+
CREATE TABLE IF NOT EXISTS activation_tokens (
|
|
6351
|
+
token TEXT PRIMARY KEY,
|
|
6352
|
+
clinic_label TEXT NOT NULL,
|
|
6353
|
+
expires_at TEXT NOT NULL,
|
|
6354
|
+
used_at TEXT,
|
|
6355
|
+
used_by_chat_id TEXT,
|
|
6356
|
+
is_active INTEGER DEFAULT 1,
|
|
6357
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
6358
|
+
)
|
|
6359
|
+
""")
|
|
6360
|
+
cur.execute("""
|
|
6361
|
+
INSERT INTO activation_tokens
|
|
6362
|
+
(token, clinic_label, expires_at, created_at)
|
|
6363
|
+
VALUES (?, ?, ?, ?)
|
|
6364
|
+
""", (token, stored_label, expires_at, datetime.now().isoformat()))
|
|
6365
|
+
conn.commit()
|
|
6366
|
+
conn.close()
|
|
6367
|
+
warn(f"API local no disponible ({reason}); usé generación offline segura.")
|
|
6368
|
+
_print_token(token, expires_at[:16], offline=True)
|
|
6369
|
+
except Exception as offline_error:
|
|
6370
|
+
fail(f"No se pudo generar: {reason}")
|
|
6371
|
+
fail(f"Fallback offline falló: {offline_error}")
|
|
6372
|
+
|
|
6313
6373
|
try:
|
|
6314
6374
|
if _HTTPX:
|
|
6315
6375
|
r = _httpx.post(
|
|
6316
6376
|
f"{base_url}/api/tokens/create",
|
|
6317
|
-
json=
|
|
6377
|
+
json=payload,
|
|
6318
6378
|
headers={"X-Master-Key": master_key},
|
|
6319
6379
|
timeout=10
|
|
6320
6380
|
)
|
|
@@ -6323,7 +6383,7 @@ def cmd_token(args):
|
|
|
6323
6383
|
import urllib.request as _ur
|
|
6324
6384
|
req = _ur.Request(
|
|
6325
6385
|
f"{base_url}/api/tokens/create",
|
|
6326
|
-
data=json.dumps(
|
|
6386
|
+
data=json.dumps(payload).encode(),
|
|
6327
6387
|
headers={"Content-Type": "application/json", "X-Master-Key": master_key},
|
|
6328
6388
|
method="POST"
|
|
6329
6389
|
)
|
|
@@ -6331,15 +6391,11 @@ def cmd_token(args):
|
|
|
6331
6391
|
token = data.get("token", "")
|
|
6332
6392
|
expires = data.get("expires_at", "")[:16] if data.get("expires_at") else "72h"
|
|
6333
6393
|
if token:
|
|
6334
|
-
|
|
6335
|
-
print(f"\n {q(C.YLW, token, bold=True)}\n")
|
|
6336
|
-
info(f"Expira: {expires}")
|
|
6337
|
-
info("Envialo al admin — lo escribe en el chat de WhatsApp/Telegram")
|
|
6394
|
+
_print_token(token, expires)
|
|
6338
6395
|
else:
|
|
6339
|
-
|
|
6396
|
+
_gen_offline(f"respuesta API sin token: {data}")
|
|
6340
6397
|
except Exception as e:
|
|
6341
|
-
|
|
6342
|
-
info(f"¿Está corriendo? conny restart {inst.name}")
|
|
6398
|
+
_gen_offline(str(e))
|
|
6343
6399
|
|
|
6344
6400
|
# Generar para todas
|
|
6345
6401
|
if name.lower() in ("all", "todas", "todos"):
|
|
@@ -7126,6 +7182,40 @@ def cmd_sync_web(args):
|
|
|
7126
7182
|
ok(f"Instancia {inst.label} vinculada y sincronizada correctamente.")
|
|
7127
7183
|
info("Puedes gestionarla desde el portal web usando tu token.")
|
|
7128
7184
|
|
|
7185
|
+
|
|
7186
|
+
|
|
7187
|
+
def cmd_dev_account(args):
|
|
7188
|
+
"""Crea una cuenta de desarrollador para la UI web."""
|
|
7189
|
+
print("\n══ Cuenta de Desarrollador (Web Dev Console) ══")
|
|
7190
|
+
import os, sqlite3, secrets, hashlib
|
|
7191
|
+
import getpass
|
|
7192
|
+
|
|
7193
|
+
db_path = "/home/ubuntu/conny/conny_ultra.db"
|
|
7194
|
+
if not os.path.exists(db_path):
|
|
7195
|
+
db_path = "/home/ubuntu/conny/conny.db"
|
|
7196
|
+
|
|
7197
|
+
email = input(f" {q(C.CYN, 'Email del Dev:')} ").strip().lower()
|
|
7198
|
+
if not email: return
|
|
7199
|
+
password = getpass.getpass(f" {q(C.CYN, 'Contraseña:')} ").strip()
|
|
7200
|
+
if not password: return
|
|
7201
|
+
|
|
7202
|
+
try:
|
|
7203
|
+
salt = secrets.token_hex(16)
|
|
7204
|
+
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), 260_000).hex()
|
|
7205
|
+
hashed = f"{salt}:{key}"
|
|
7206
|
+
|
|
7207
|
+
conn = sqlite3.connect(db_path)
|
|
7208
|
+
c = conn.cursor()
|
|
7209
|
+
c.execute("CREATE TABLE IF NOT EXISTS dev_accounts (email TEXT PRIMARY KEY, password_hash TEXT)")
|
|
7210
|
+
c.execute("INSERT OR REPLACE INTO dev_accounts (email, password_hash) VALUES (?, ?)", (email, hashed))
|
|
7211
|
+
conn.commit()
|
|
7212
|
+
conn.close()
|
|
7213
|
+
ok(f"Cuenta '{email}' creada con éxito.")
|
|
7214
|
+
info("Ya puedes entrar en la web mediante el botón 'API Access / Conny Dev'.")
|
|
7215
|
+
except Exception as e:
|
|
7216
|
+
fail(f"Error al crear cuenta localmente: {e}")
|
|
7217
|
+
|
|
7218
|
+
|
|
7129
7219
|
ROUTES = {
|
|
7130
7220
|
|
|
7131
7221
|
"auth": cmd_auth,
|
|
@@ -7134,6 +7224,7 @@ ROUTES = {
|
|
|
7134
7224
|
"portal": cmd_portal,
|
|
7135
7225
|
"web": cmd_portal,
|
|
7136
7226
|
"sync-web": cmd_sync_web,
|
|
7227
|
+
"dev-account": cmd_dev_account, "dev": cmd_dev_account,
|
|
7137
7228
|
# Principal
|
|
7138
7229
|
"init": cmd_init,
|
|
7139
7230
|
"new": cmd_new, "nuevo": cmd_new, "crear": cmd_new,
|
|
@@ -11699,6 +11790,7 @@ def main():
|
|
|
11699
11790
|
parser.add_argument("--quiet", "-q", action="store_true")
|
|
11700
11791
|
parser.add_argument("--json", "-j", action="store_true", help="Output JSON")
|
|
11701
11792
|
parser.add_argument("--auto", "-y", action="store_true", help="Auto-confirm")
|
|
11793
|
+
parser.add_argument("--admin", action="store_true", help="Genera token Conny Pro Admin")
|
|
11702
11794
|
parser.add_argument("--dry-run", "-n", action="store_true", dest="dry_run",
|
|
11703
11795
|
help="Preview sin ejecutar (sync)")
|
|
11704
11796
|
parser.add_argument("--no-restart", action="store_true", dest="no_restart",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger("conny.evolution")
|
|
11
|
+
|
|
12
|
+
class EvolutionManager:
|
|
13
|
+
"""
|
|
14
|
+
Maneja la auto-evolución de Conny.
|
|
15
|
+
Escribe en SOUL.md e IDENTITY.md y persiste cambios en DB.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, instance_id: str, db, instance_path: Optional[Union[str, Path]] = None):
|
|
19
|
+
self.instance_id = instance_id
|
|
20
|
+
self.db = db
|
|
21
|
+
# Intentar determinar la ruta de la instancia
|
|
22
|
+
if instance_path:
|
|
23
|
+
self.base_path = Path(instance_path)
|
|
24
|
+
else:
|
|
25
|
+
self.base_path = Path(f"/home/ubuntu/conny-instances/{instance_id}")
|
|
26
|
+
if not self.base_path.exists():
|
|
27
|
+
self.base_path = Path(".") # Fallback
|
|
28
|
+
|
|
29
|
+
self.soul_path = self.base_path / "soul" / "SOUL.md"
|
|
30
|
+
self.identity_path = self.base_path / "identity" / "IDENTITY.md"
|
|
31
|
+
|
|
32
|
+
# Asegurar que los directorios existen
|
|
33
|
+
self.soul_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
self.identity_path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
async def apply_instruction(self, text: str) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Analiza un mensaje del admin y aplica cambios si detecta instrucciones de evolución.
|
|
39
|
+
"""
|
|
40
|
+
text_low = text.lower().strip()
|
|
41
|
+
|
|
42
|
+
# 1. Cambio de Saludo
|
|
43
|
+
saludo_match = re.search(r"(?:cambia|pon|usa|setea)\s+(?:tu|el)\s+saludo\s+(?:a|por|como)\s+[\"']?(.+?)[\"']?$", text, re.IGNORECASE)
|
|
44
|
+
if saludo_match:
|
|
45
|
+
new_greeting = saludo_match.group(1).strip()
|
|
46
|
+
# Si el saludo tiene |||, lo dividimos en dos burbujas
|
|
47
|
+
if "|||" in new_greeting:
|
|
48
|
+
g1, g2 = [p.strip() for p in new_greeting.split("|||", 1)]
|
|
49
|
+
self.db.update_clinic(custom_greeting_1=g1, custom_greeting_2=g2)
|
|
50
|
+
else:
|
|
51
|
+
self.db.update_clinic(custom_greeting_1=new_greeting)
|
|
52
|
+
|
|
53
|
+
await self._update_soul(f"El admin pidió cambiar el saludo a: {new_greeting}")
|
|
54
|
+
return f"entendido, ya actualicé mi saludo a: {new_greeting}"
|
|
55
|
+
|
|
56
|
+
# 2. Frases prohibidas
|
|
57
|
+
forbidden_match = re.search(r"(?:no\s+uses|no\s+digas|deja\s+de\s+usar|prohibido\s+decir)\s+(?:la\s+frase|la\s+palabra|el\s+termino)?\s*[\"']?(.+?)[\"']?$", text, re.IGNORECASE)
|
|
58
|
+
if forbidden_match:
|
|
59
|
+
phrase = forbidden_match.group(1).strip()
|
|
60
|
+
# Guardar en business_rules
|
|
61
|
+
clinic = self.db.get_clinic()
|
|
62
|
+
rules = clinic.get("business_rules", {})
|
|
63
|
+
if isinstance(rules, str): rules = json.loads(rules)
|
|
64
|
+
|
|
65
|
+
forbidden = rules.get("forbidden_phrases", [])
|
|
66
|
+
if phrase not in forbidden:
|
|
67
|
+
forbidden.append(phrase)
|
|
68
|
+
rules["forbidden_phrases"] = forbidden
|
|
69
|
+
|
|
70
|
+
self.db.update_clinic(business_rules=json.dumps(rules, ensure_ascii=False))
|
|
71
|
+
await self._update_soul(f"REGLA CRÍTICA: No usar jamás la frase o palabra: '{phrase}'")
|
|
72
|
+
return f"anotado. no volveré a decir '{phrase}' nunca más"
|
|
73
|
+
|
|
74
|
+
# 3. Datos del Admin
|
|
75
|
+
if "me llamo" in text_low or "mi nombre es" in text_low:
|
|
76
|
+
name_match = re.search(r"(?:me llamo|soy|mi nombre es)\s+([A-ZÁÉÍÓÚ][a-záéíóú]+(?:\s+[A-ZÁÉÍÓÚ][a-záéíóú]+)?)", text)
|
|
77
|
+
if name_match:
|
|
78
|
+
admin_name = name_match.group(1)
|
|
79
|
+
await self._update_identity(f"Nombre del Administrador: {admin_name}")
|
|
80
|
+
# El profile ya se actualiza en runtime.py, pero esto refuerza los archivos .md
|
|
81
|
+
return f"mucho gusto {admin_name}, ya guardé tu nombre en mi identidad operativa"
|
|
82
|
+
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
async def _update_soul(self, observation: str):
|
|
86
|
+
"""Añade una observación al archivo SOUL.md."""
|
|
87
|
+
try:
|
|
88
|
+
content = ""
|
|
89
|
+
if self.soul_path.exists():
|
|
90
|
+
content = self.soul_path.read_text(encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
new_entry = f"\n- [{datetime.now().strftime('%Y-%m-%d %H:%M')}] {observation}"
|
|
93
|
+
if "# EVOLUCIÓN" not in content:
|
|
94
|
+
content += "\n\n# EVOLUCIÓN\n"
|
|
95
|
+
|
|
96
|
+
content += new_entry
|
|
97
|
+
self.soul_path.write_text(content, encoding="utf-8")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
log.error(f"Error updating SOUL.md: {e}")
|
|
100
|
+
|
|
101
|
+
async def _update_identity(self, line: str):
|
|
102
|
+
"""Actualiza o añade una línea al archivo IDENTITY.md."""
|
|
103
|
+
try:
|
|
104
|
+
content = ""
|
|
105
|
+
if self.identity_path.exists():
|
|
106
|
+
content = self.identity_path.read_text(encoding="utf-8")
|
|
107
|
+
|
|
108
|
+
if line not in content:
|
|
109
|
+
content += f"\n- {line}"
|
|
110
|
+
self.identity_path.write_text(content, encoding="utf-8")
|
|
111
|
+
except Exception as e:
|
|
112
|
+
log.error(f"Error updating IDENTITY.md: {e}")
|
|
@@ -48,29 +48,25 @@ def _strip_leading_greeting(text: str) -> str:
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _first_contact_intro(clinic: Dict[str, Any], agent_name: str = "Conny") -> str:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
# ── Override del Admin ──────────────────────────────────────────────
|
|
52
|
+
manual = clinic.get("custom_greeting_1")
|
|
53
|
+
if manual:
|
|
54
|
+
return manual
|
|
55
|
+
|
|
56
|
+
clinic_name = str(clinic.get("name") or "").strip()
|
|
57
|
+
if clinic_name:
|
|
58
|
+
return f"Hola, soy {agent_name} de {clinic_name}"
|
|
59
|
+
return f"Hola, soy {agent_name}"
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
def _first_contact_welcome_line(clinic: Dict[str, Any], user_msg: str) -> str:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
opening = "Hola, buenas noches"
|
|
66
|
-
elif "buenas" in normalized:
|
|
67
|
-
opening = "Hola, buenas"
|
|
68
|
-
else:
|
|
69
|
-
opening = "Hola"
|
|
70
|
-
|
|
71
|
-
if clinic_name:
|
|
72
|
-
return f"{opening}! Bienvenido a {clinic_name}."
|
|
73
|
-
return f"{opening}! Cómo estás?"
|
|
63
|
+
# ── Override del Admin ──────────────────────────────────────────────
|
|
64
|
+
manual = clinic.get("custom_greeting_2")
|
|
65
|
+
if manual:
|
|
66
|
+
return manual
|
|
67
|
+
|
|
68
|
+
# ── Saludo por Defecto (Upgrade) ───────────────────────────────────
|
|
69
|
+
return "Gracias por contactarte con nosotros como te puedo ayudar??"
|
|
74
70
|
|
|
75
71
|
|
|
76
72
|
def _first_contact_identity_line(clinic: Dict[str, Any], agent_name: str = "Conny") -> str:
|
package/conny_core/prompt_ops.py
CHANGED
|
@@ -34,6 +34,68 @@ def truncate_block(text: str, max_chars: int) -> str:
|
|
|
34
34
|
return f"{clipped}..."
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def build_business_context(clinic: dict, db, instance_path: str = None) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Construye el contexto del negocio para el system prompt.
|
|
40
|
+
Lee de DB + archivos de knowledge si existen.
|
|
41
|
+
"""
|
|
42
|
+
parts = []
|
|
43
|
+
|
|
44
|
+
name = clinic.get("name", "")
|
|
45
|
+
if name:
|
|
46
|
+
parts.append(f"Negocio: {name}")
|
|
47
|
+
|
|
48
|
+
services = clinic.get("services", "")
|
|
49
|
+
if isinstance(services, list):
|
|
50
|
+
services = ", ".join(services)
|
|
51
|
+
if services:
|
|
52
|
+
parts.append(f"Servicios: {services}")
|
|
53
|
+
|
|
54
|
+
prices = clinic.get("prices", "") or clinic.get("pricing", "")
|
|
55
|
+
if isinstance(prices, dict):
|
|
56
|
+
prices = ", ".join(f"{k}: {v}" for k, v in prices.items())
|
|
57
|
+
if prices:
|
|
58
|
+
parts.append(f"Precios: {prices}")
|
|
59
|
+
|
|
60
|
+
schedule = clinic.get("schedule", "") or clinic.get("hours", "")
|
|
61
|
+
if isinstance(schedule, dict):
|
|
62
|
+
schedule = ", ".join(f"{k}: {v}" for k, v in schedule.items())
|
|
63
|
+
if schedule:
|
|
64
|
+
parts.append(f"Horarios: {schedule}")
|
|
65
|
+
|
|
66
|
+
address = clinic.get("address", "") or clinic.get("location", "")
|
|
67
|
+
if address:
|
|
68
|
+
parts.append(f"Ubicación: {address}")
|
|
69
|
+
|
|
70
|
+
# Leer archivos de knowledge si existen
|
|
71
|
+
if instance_path:
|
|
72
|
+
import os
|
|
73
|
+
from pathlib import Path
|
|
74
|
+
# Intentar varias rutas comunes para knowledge
|
|
75
|
+
paths_to_try = [
|
|
76
|
+
Path(instance_path) / "knowledge",
|
|
77
|
+
Path(f"instances/{instance_path}/knowledge"),
|
|
78
|
+
Path(f"/home/ubuntu/conny/instances/{instance_path}/knowledge"),
|
|
79
|
+
Path(f"knowledge/{instance_path}")
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for knowledge_dir in paths_to_try:
|
|
83
|
+
if knowledge_dir.exists():
|
|
84
|
+
for md_file in sorted(knowledge_dir.glob("*.md"))[:5]: # max 5 archivos
|
|
85
|
+
try:
|
|
86
|
+
content = md_file.read_text(encoding="utf-8").strip()
|
|
87
|
+
if content:
|
|
88
|
+
parts.append(f"\n--- {md_file.stem} ---\n{content[:1000]}")
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
break # Si encontramos uno que existe, paramos
|
|
92
|
+
|
|
93
|
+
if not parts:
|
|
94
|
+
return "El negocio no ha completado su configuración todavía."
|
|
95
|
+
|
|
96
|
+
return "\n".join(parts)
|
|
97
|
+
|
|
98
|
+
|
|
37
99
|
def build_compact_examples(examples: str, max_chars: int = 650, max_lines: int = 16) -> str:
|
|
38
100
|
if not examples:
|
|
39
101
|
return ""
|
package/conny_demo_voice.py
CHANGED
|
@@ -129,6 +129,6 @@ def get_closing_response() -> str:
|
|
|
129
129
|
"""When prospect wants to buy, close with Santiago's contact."""
|
|
130
130
|
return (
|
|
131
131
|
"me alegra que te haya gustado la demo! |||"
|
|
132
|
-
" para activarlo en tu negocio, Santiago te explica todo:
|
|
132
|
+
" para activarlo en tu negocio, Santiago te explica todo: 3243699856 |||"
|
|
133
133
|
" la activación es rápida, en menos de 5 minutos ya estoy respondiendo tu WhatsApp"
|
|
134
134
|
)
|