@innvisor/conny-ai 9.7.0 → 9.8.2

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 (50) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +17 -1
  3. package/conny_app.py +9 -3
  4. package/conny_cli.py +103 -11
  5. package/conny_core/evolution.py +112 -0
  6. package/conny_core/first_turn_ops.py +16 -20
  7. package/conny_core/prompt_ops.py +62 -0
  8. package/conny_demo_voice.py +1 -1
  9. package/conny_doctor.py +287 -2
  10. package/conny_domino.py +2 -2
  11. package/conny_generator.py +1 -1
  12. package/conny_i18n.py +81 -2
  13. package/conny_init.py +254 -41
  14. package/conny_runtime_ops.py +198 -6
  15. package/conny_tui.py +7 -0
  16. package/conny_ultra_config.py +25 -11
  17. package/conny_utils.py +21 -3
  18. package/ecosystem.config.js +11 -1
  19. package/install.sh +78 -22
  20. package/npm/conny.js +75 -21
  21. package/package.json +12 -2
  22. package/run.sh +7 -0
  23. package/src/conny/admin/dashboard.py +35 -4
  24. package/src/conny/admin_memory.py +93 -0
  25. package/src/conny/api/routes.py +26 -9
  26. package/src/conny/channels/cli.py +30 -9
  27. package/src/conny/demo/handler.py +23 -23
  28. package/src/conny/personas/generator.py +1 -1
  29. package/src/conny/production/domino.py +2 -2
  30. package/src/conny/production/guard.py +4 -4
  31. package/src/core/admin_engines.py +51 -48
  32. package/src/core/globals.py +110 -9
  33. package/src/core/production_monitor.py +63 -38
  34. package/src/core/runtime.py +343 -305
  35. package/src/domain/prompts/prospect_pitch.py +11 -11
  36. package/src/domain/send_guard.py +4 -4
  37. package/src/interfaces/web/app.py +91 -27
  38. package/src/interfaces/web/demo_admin_commands.py +165 -0
  39. package/src/interfaces/web/demo_handler.py +178 -34
  40. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +0 -22
  41. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +0 -11
  42. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +0 -11
  43. package/brand-assets/cl-nica-las-am-ricas/manifest.json +0 -22
  44. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +0 -11
  45. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +0 -11
  46. package/brand-assets/conny-demo/manifest.json +0 -22
  47. package/brand-assets/conny-demo/processed/business-identity.txt +0 -7
  48. package/brand-assets/conny-demo/raw/business-identity.txt +0 -7
  49. package/fix_init.py +0 -27
  50. package/verify_conversation_impl.py +0 -48
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.8.2 - 2026-06-02
4
+
5
+ - persisted the language selected in `conny init` so the rest of the CLI loads it automatically
6
+ - made `conny_i18n` read the saved workspace language on startup
7
+ - kept the `conny init` banner unchanged while aligning other launch paths to the same brand
8
+
9
+ ## 9.8.1 - 2026-06-02
10
+
11
+ - made `conny` open the guided setup flow by default instead of the old help banner
12
+ - removed the default launcher banner from the primary `conny` path
13
+ - aligned the TUI version display with `package.json`
14
+
15
+ ## 9.8.0 - 2026-06-02
16
+
17
+ - blocked first-contact setup for unknown chats until a valid activation token is provided
18
+ - added `conny token --admin` with `ADMN-` Conny Pro Admin tokens, offline SQLite generation and web/API developer access
19
+ - fixed token casing and case-insensitive token lookup so generated tokens validate reliably
20
+ - made `/api/activate`, web registration and developer registration accept Conny Pro Admin tokens safely
21
+ - added admin Soul memory folders under `soul/admins/<chat_id>` for persistent operator context and learned business/ops facts
22
+ - introduced typed LLM service errors so quota/API-key failures are shown to admins before any fallback
23
+ - changed admin fallback into an explicit opt-in flow via `continuar fallback`
24
+ - cleaned npm packaging to exclude patch/fix scripts and private per-business brand asset folders
25
+ - fixed first contact greeting regression (`Holaal`) and restored admin capability fallback for audios, PDFs and documents
26
+
27
+ ## 9.7.6 - 2026-05-26
28
+
29
+ - fixed demo LLM resolution through the Conny facade so tests and runtime use the configured engine instead of a stale runtime module
30
+ - removed the fake “internet dropped” demo fallback and replaced it with contextual owner onboarding when every model response is empty or unusable
31
+ - preserved continuity in owner demo flows so Conny does not repeat “soy una IA” after capability context was already explained
32
+ - verified Python, Node/npm and shell entrypoints for Linux/macOS/PowerShell-compatible npm usage
33
+
34
+ ## 9.7.5 - 2026-05-26
35
+
36
+ - unified `conny init`, `conny config` and `conny doctor` around the active instance `.env`
37
+ - mirrored provisioned instance secrets and URLs into the base runtime `.env` after setup and config edits
38
+ - replaced brittle `.env` text replacement with parser-based updates that handle quoted, unquoted and `pending` values
39
+ - added dashboard exposure setup to `conny init` for localhost, LAN/IP and custom public URLs
40
+
41
+ ## 9.7.4 - 2026-05-26
42
+
43
+ - fixed existing installs with a stale/incomplete Python runtime by making bootstrap health check required imports before every command
44
+ - added `conny --bootstrap-check` and wired the shell installer to run it before reporting success
45
+ - ensured missing `rich`/CLI dependencies are repaired during install instead of surfacing later at `conny init`
46
+
47
+ ## 9.7.3 - 2026-05-26
48
+
49
+ - fixed Termux/proot installs where `conny init` could start without `rich` installed
50
+ - made the npm bootstrap install critical CLI dependencies first and fail loudly if they are missing
51
+ - kept heavy production dependencies best-effort so packages like `scikit-learn` cannot leave the CLI half-installed
52
+ - expanded runtime health checks to include `rich`, `deep_translator`, `questionary`, `fastapi`, `httpx`, `dotenv` and `pydantic`
53
+
54
+ ## 9.7.2 - 2026-05-26
55
+
56
+ - removed the obsolete boxed `CONNY ULTRA CONFIG v9.7.0` layout from `conny config`
57
+ - added a Gateway/Webhook step to `conny init` with automatic `localhost.run` tunneling or manual `BASE_URL`
58
+ - fixed tunnel routing to target the active instance `PORT` instead of stale defaults like `8002`
59
+ - made webhook sync reload the real `.env` and restart PM2 with `--update-env` before calling Telegram `setWebhook`
60
+ - unified the default instances path under `~/.conny/instances`
61
+
62
+ ## 9.7.1 - 2026-05-26
63
+
64
+ - fixed GitHub/npm bootstrap for fresh installs where `conny init` still tried to run from `/home/ubuntu/conny`
65
+ - removed the hardcoded working directory in `conny_app.py`; subcommands now run from the installed `CONNY_DIR`
66
+ - bumped the package version so existing `~/.conny/repo` installs resync automatically on reinstall
67
+ - kept the public package name as `conny-ai` for GitHub installs and npm compatibility
68
+
3
69
  ## 9.7.0 - 2026-05-26
4
70
 
5
71
  - 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/main/install.sh | bash
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 = Path("/home/ubuntu/conny-instances")
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: subprocess.run([sys.executable]+[str(x) for x in a],cwd="/home/ubuntu/conny")
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):
@@ -279,7 +285,7 @@ def main():
279
285
  if first_run() and not (len(sys.argv)>1 and sys.argv[1] in ("help","--help","-h","-v","--version")):
280
286
  onboard()
281
287
  if len(sys.argv) <= 1:
282
- cmd_help()
288
+ cmd_new()
283
289
  else:
284
290
  route(sys.argv[1], " ".join(sys.argv[2:]))
285
291
 
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.7.0"
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 = getpass.getpass(f"\n {q(C.P2, '▸')} {label}{q(C.G3, d)} ") if secret else input(f"\n {q(C.P2, '▸')} {label}{q(C.G3, d)} ")
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={"clinic_label": label},
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({"clinic_label": label}).encode(),
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
- ok(f"Token generado para: {q(C.YLW, label)}")
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
- fail(f"Error: {data}")
6396
+ _gen_offline(f"respuesta API sin token: {data}")
6340
6397
  except Exception as e:
6341
- fail(f"No se pudo generar: {e}")
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
- clinic_name = (clinic.get("name") or "").strip()
52
- if clinic_name:
53
- return f"Hola! Soy {agent_name} de {clinic_name}."
54
- return f"Hola! Soy {agent_name}."
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
- clinic_name = (clinic.get("name") or "").strip()
59
- normalized = _normalize_conv_text(user_msg or "")
60
- if "buenas tardes" in normalized:
61
- opening = "Hola, buenas tardes"
62
- elif "buenos dias" in normalized:
63
- opening = "Hola, buenos días"
64
- elif "buenas noches" in normalized:
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:
@@ -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 ""
@@ -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: 3124348669 |||"
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
  )