@innvisor/conny-ai 9.8.2 → 9.8.5

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.8.4 - 2026-06-02
4
+
5
+ - made bare `conny` route to the modern onboarding/chat surface instead of the legacy help screen
6
+ - kept `conny init` untouched while preserving the same banner and branding in the post-onboarding chat UI
7
+ - aligned the legacy Python CLI fallback so direct `conny_cli.py` launches follow the same start behavior
8
+
9
+ ## 9.8.3 - 2026-06-02
10
+
11
+ - made `conny` open the real chat interface after onboarding instead of the setup flow
12
+ - kept `conny init` unchanged and preserved its banner/design exactly
13
+ - added slash-command chat shortcuts with a Codex-style launcher header
14
+
3
15
  ## 9.8.2 - 2026-06-02
4
16
 
5
17
  - persisted the language selected in `conny init` so the rest of the CLI loads it automatically
package/conny_app.py CHANGED
@@ -282,10 +282,12 @@ def _sh(*a):
282
282
 
283
283
  def main():
284
284
  signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
285
- if first_run() and not (len(sys.argv)>1 and sys.argv[1] in ("help","--help","-h","-v","--version")):
286
- onboard()
287
285
  if len(sys.argv) <= 1:
288
- cmd_new()
286
+ if first_run():
287
+ onboard()
288
+ return
289
+ cmd_chat()
290
+ return
289
291
  else:
290
292
  route(sys.argv[1], " ".join(sys.argv[2:]))
291
293
 
package/conny_cli.py CHANGED
@@ -11845,6 +11845,9 @@ def main():
11845
11845
  if not workspace_is_configured() or not get_instances():
11846
11846
  cmd_init(args)
11847
11847
  return
11848
+ modern_entrypoint = Path(os.environ.get("CONNY_DIR", os.path.dirname(os.path.abspath(__file__)))) / "conny_app.py"
11849
+ subprocess.call([sys.executable, str(modern_entrypoint)])
11850
+ return
11848
11851
 
11849
11852
  # Help
11850
11853
  if args.help or cmd in ("help", "--help", "-h", ""):
package/conny_studio.py CHANGED
@@ -1,20 +1,32 @@
1
1
  #!/usr/bin/env python3
2
- """conny_studio.py — Interactive CLI session with live monitoring."""
2
+ """conny_studio.py — Interactive CLI chat with live monitoring."""
3
3
  import asyncio
4
4
  import json
5
5
  import os
6
6
  import sys
7
7
  import time
8
8
  import uuid
9
+ import subprocess
9
10
  from pathlib import Path
10
11
  from datetime import datetime
11
12
 
12
13
  import httpx
14
+ from rich.console import Console
13
15
 
14
16
  sys.path.insert(0, str(Path(__file__).parent))
17
+ from conny_design import LOGO_FULL, SEP, ICON_BRAND
15
18
  from conny_uncertainty import UncertaintyDetector
16
19
  from conny_voice import ConnyVoice
17
20
 
21
+ CONSOLE = Console()
22
+ VERSION = "9.8.2"
23
+ try:
24
+ package_path = Path(__file__).resolve().parent / "package.json"
25
+ if package_path.exists():
26
+ VERSION = json.loads(package_path.read_text(encoding="utf-8")).get("version", VERSION)
27
+ except Exception:
28
+ pass
29
+
18
30
  STUDIO_DIR = Path.home() / ".conny" / "studio" / "memory"
19
31
  API_URL = "http://localhost:8001/test"
20
32
 
@@ -69,11 +81,16 @@ class ConnyStudio:
69
81
  }, ensure_ascii=False) + "\n")
70
82
 
71
83
  async def handle_command(self, cmd: str) -> str:
84
+ if cmd in ("/help", "/menu", "/start"):
85
+ return (
86
+ "Comandos: /help /menu /clear /history /models /config /language "
87
+ "/export /reload-persona /fix-last"
88
+ )
72
89
  if cmd == "/clear":
73
90
  self.history = []
74
91
  self.chat_id = f"studio_{uuid.uuid4().hex[:8]}"
75
92
  return "Session cleared. New conversation started."
76
- elif cmd == "/show-memory":
93
+ elif cmd in ("/history", "/show-memory"):
77
94
  if not self.history:
78
95
  return "No turns in memory yet."
79
96
  lines = []
@@ -81,6 +98,14 @@ class ConnyStudio:
81
98
  role = "YOU" if h["role"] == "user" else "MEL"
82
99
  lines.append(f" [{role}] {h['content'][:80]}")
83
100
  return "\n".join(lines)
101
+ elif cmd == "/models":
102
+ return self._run_cli_command("modelo")
103
+ elif cmd == "/config":
104
+ return self._run_cli_command("config")
105
+ elif cmd == "/language":
106
+ return self._run_cli_command("language")
107
+ elif cmd in ("/new", "/init"):
108
+ return self._run_cli_command("init")
84
109
  elif cmd == "/show-failures":
85
110
  if not self.failures_file.exists():
86
111
  return "No failures detected this session."
@@ -101,13 +126,28 @@ class ConnyStudio:
101
126
  return "No previous turn to fix."
102
127
  return f"Unknown command: {cmd}"
103
128
 
129
+ def _run_cli_command(self, command: str) -> str:
130
+ cli = Path(__file__).resolve().parent / "conny_cli.py"
131
+ if not cli.exists():
132
+ return f"CLI no disponible para /{command}"
133
+ try:
134
+ print(f"\033[90m[system] launching: conny {command}\033[0m")
135
+ subprocess.run([sys.executable, str(cli), command], check=False)
136
+ return f"/{command} closed. Back in chat."
137
+ except Exception as exc:
138
+ return f"No pude abrir /{command}: {exc}"
139
+
104
140
  def print_header(self):
105
- print("\033[1;36m╔══════════════════════════════════════════════╗\033[0m")
106
- print("\033[1;36m║ CONNY STUDIO v1.0 ║\033[0m")
107
- print(f"\033[1;36m║ Instance: {self.instance_id:<33}║\033[0m")
108
- print(f"\033[1;36m║ Session: {self.session_id:<34}║\033[0m")
109
- print("\033[1;36m╚══════════════════════════════════════════════╝\033[0m")
110
- print("\033[90mCommands: /clear /show-memory /show-failures /fix-last /reload-persona /export-session\033[0m\n")
141
+ print()
142
+ CONSOLE.print(LOGO_FULL)
143
+ CONSOLE.print(f" {ICON_BRAND} v{VERSION} · chat real")
144
+ CONSOLE.print(SEP)
145
+ print(f" Instance: {self.instance_id}")
146
+ print(f" Session: {self.session_id}")
147
+ print(" Comandos: /help /menu /clear /history /models /config /language /export")
148
+ print(" Atajos: 1=models 2=config 3=language 4=help")
149
+ CONSOLE.print(SEP)
150
+ print()
111
151
 
112
152
  def print_scores(self, scores):
113
153
  conf = scores["confidence"]
@@ -122,14 +162,16 @@ class ConnyStudio:
122
162
  self.print_header()
123
163
  while True:
124
164
  try:
125
- user_input = input("\033[1;32m[YOU]\033[0m ")
165
+ user_input = input("\033[1;32m[YOU]\033[0m ").strip()
126
166
  except (EOFError, KeyboardInterrupt):
127
167
  print("\n\033[90mSession ended.\033[0m")
128
168
  break
129
- if not user_input.strip():
169
+ if not user_input:
130
170
  continue
171
+ if user_input in ("1", "2", "3", "4"):
172
+ user_input = { "1": "/models", "2": "/config", "3": "/language", "4": "/help" }[user_input]
131
173
  if user_input.startswith("/"):
132
- result = await self.handle_command(user_input.strip())
174
+ result = await self.handle_command(user_input)
133
175
  print(f"\033[1;33m[SYSTEM]\033[0m {result}")
134
176
  continue
135
177
  try:
package/npm/conny.js CHANGED
@@ -485,7 +485,7 @@ function execConny(argv) {
485
485
  }
486
486
 
487
487
  const args = process.argv.slice(2);
488
- const isHelp = args.length === 0 || args.includes("-h") || args.includes("--help") || args.includes("help");
488
+ const isHelp = args.includes("-h") || args.includes("--help") || args.includes("help");
489
489
  const isVersion = args.includes("-v") || args.includes("--version") || args.includes("version");
490
490
  const isBootstrapCheck = args.includes("--bootstrap-check");
491
491
  const isJson = args.includes("--json");
@@ -518,7 +518,7 @@ if (isHelp) {
518
518
  process.exit(0);
519
519
  }
520
520
 
521
- const launchArgs = args.length === 0 ? ["new"] : args;
521
+ const launchArgs = args;
522
522
 
523
523
  if (!execConny(launchArgs)) {
524
524
  fail(`No pude iniciar Conny desde ${connyHome}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@innvisor/conny-ai",
3
- "version": "9.8.2",
3
+ "version": "9.8.5",
4
4
  "description": "Open-source CLI and runtime for building Conny AI receptionist agents on WhatsApp and Telegram.",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Rubio",
@@ -11785,6 +11785,9 @@ def main():
11785
11785
  if not workspace_is_configured() or not get_instances():
11786
11786
  cmd_init(args)
11787
11787
  return
11788
+ modern_entrypoint = Path(os.environ.get("CONNY_DIR", os.path.dirname(os.path.abspath(__file__)))) / "conny_app.py"
11789
+ subprocess.call([sys.executable, str(modern_entrypoint)])
11790
+ return
11788
11791
 
11789
11792
  # Help
11790
11793
  if args.help or cmd in ("help", "--help", "-h", ""):
@@ -261,17 +261,8 @@ async def _handle_demo_message(self, chat_id: str, text: str,
261
261
  )
262
262
  _save("assistant", r)
263
263
  bubbles = self._split_bubbles(r, chat_id=chat_id, archetype=_demo_archetype)
264
- if should_normalize_first_turn and len(bubbles) == 1:
265
- _text_norm = _normalize_conv_text(text or "")
266
- _greeting_tokens = (
267
- "hola", "buenas", "buenas tardes", "buenos dias", "buenos días",
268
- "buenas noches", "hey", "holi", "hi", "hello",
269
- "good morning", "good afternoon", "good evening",
270
- )
271
- if any(_text_norm == token or _text_norm.startswith(token + " ") for token in _greeting_tokens):
272
- lowered_bubble = _normalize_conv_text(bubbles[0] or "")
273
- if not any(token in lowered_bubble for token in ("cuentame", "cuéntame", "revisar", "ayudo", "ayudar")):
274
- bubbles.append(_lang_text("cuéntame qué te gustaría revisar", "what would you like to check?"))
264
+ # The LLM owns the follow-up. Do not append generic CTA bubbles here:
265
+ # that made good model answers sound like fallback.
275
266
  tone = self._demo_sessions.get(btone_key, "GENERAL")
276
267
  if tone in ("SALUD PREMIUM", "PREMIUM"):
277
268
  bubbles = [b[0].upper() + b[1:] if b else b for b in bubbles]
@@ -665,6 +656,7 @@ async def _handle_demo_message(self, chat_id: str, text: str,
665
656
  ),
666
657
  ]
667
658
  had_output = False
659
+ last_candidate = None
668
660
  for prompt_now, temp_now, max_now, tier_now, limit_now in attempts:
669
661
  # No lanzar repair si ya pasó demasiado tiempo desde que llegó el mensaje
670
662
  if time.time() - _chain_start > _CHAIN_TIMEOUT_S:
@@ -679,9 +671,12 @@ async def _handle_demo_message(self, chat_id: str, text: str,
679
671
  )
680
672
  if candidate and candidate.strip():
681
673
  had_output = True
674
+ last_candidate = candidate
682
675
  if not validator(candidate):
683
676
  return candidate, True
684
- return None, had_output
677
+ if had_output and last_candidate:
678
+ return last_candidate, True
679
+ return None, False
685
680
 
686
681
  def _save(role, msg):
687
682
  if db:
@@ -723,20 +718,7 @@ async def _handle_demo_message(self, chat_id: str, text: str,
723
718
  )
724
719
  _save("assistant", r)
725
720
  bubbles = self._split_bubbles(r, chat_id=chat_id, archetype=_demo_archetype)
726
- # FIX BUG 4: Si es el primer turno, el usuario saludó, y la respuesta tiene
727
- # solo 1 burbuja sin pregunta de seguimiento → agregar burbuja de apertura.
728
- # Esta lógica existía en la primera definición de _send (línea 13772) pero
729
- # se perdió cuando se redefinió _send aquí en la misma función.
730
- if should_normalize_first_turn and len(bubbles) == 1:
731
- _text_norm = _normalize_conv_text(text or "")
732
- _greeting_tokens = (
733
- "hola", "buenas", "buenas tardes", "buenos dias", "buenos días",
734
- "buenas noches", "hey", "holi",
735
- )
736
- if any(_text_norm == token or _text_norm.startswith(token + " ") for token in _greeting_tokens):
737
- lowered_bubble = _normalize_conv_text(bubbles[0] or "")
738
- if not any(token in lowered_bubble for token in ("cuentame", "cuéntame", "revisar", "ayudo", "ayudar")):
739
- bubbles.append("cuéntame qué te gustaría revisar")
721
+ # The LLM owns the follow-up. Do not append generic CTA bubbles here.
740
722
  # Para premium/salud premium: restaurar mayúscula inicial
741
723
  tone = self._demo_sessions.get(btone_key, "GENERAL")
742
724
  if tone in ("SALUD PREMIUM", "PREMIUM"):
@@ -1089,6 +1071,7 @@ async def _handle_demo_message(self, chat_id: str, text: str,
1089
1071
  ),
1090
1072
  ]
1091
1073
  had_output = False
1074
+ last_candidate = None
1092
1075
  for prompt_now, temp_now, max_now, tier_now in attempts:
1093
1076
  candidate = await _llm(
1094
1077
  prompt_now,
@@ -1099,9 +1082,12 @@ async def _handle_demo_message(self, chat_id: str, text: str,
1099
1082
  )
1100
1083
  if candidate and candidate.strip():
1101
1084
  had_output = True
1085
+ last_candidate = candidate
1102
1086
  if not validator(candidate):
1103
1087
  return candidate, True
1104
- return None, had_output
1088
+ if had_output and last_candidate:
1089
+ return last_candidate, True
1090
+ return None, False
1105
1091
 
1106
1092
  def _demo_owner_last_resort(
1107
1093
  user_text: str,
@@ -1806,63 +1792,7 @@ Máximo 1 oración por burbuja. Natural y seguro."""
1806
1792
  max_t=220,
1807
1793
  )
1808
1794
  if not r:
1809
- if found:
1810
- r = _lang_text(
1811
- f"ya tengo {nombre} ||| ya me ubiqué con cómo tendría que sonar esto ||| Escríbeme como si fueras un cliente y te respondo",
1812
- f"I’ve got {nombre} now ||| I already know how this chat should sound ||| text me like a real client and I’ll reply in context",
1813
- f"já tenho {nombre} ||| já entendi como esse chat precisa soar ||| me escreve como um cliente real e eu respondo em contexto",
1814
- )
1815
- else:
1816
- # v12: no info → opciones naturales, sin exponer estado interno
1817
- if _owner_is_english():
1818
- _no_info_opts = [
1819
- f"got it, {nombre} ||| tell me what the business does and I’ll shape the demo around that",
1820
- f"okay, {nombre} ||| I’m not finding solid public info yet, so tell me what you offer and I’ll ground it from there",
1821
- f"I’ve got the name now ||| give me a quick picture of the business and I’ll keep going",
1822
- ]
1823
- elif _owner_is_portuguese():
1824
- _no_info_opts = [
1825
- f"perfeito, {nombre} ||| me conta com o que o negócio trabalha e eu monto a demo nisso",
1826
- f"ok, {nombre} ||| ainda não achei informação pública forte, então me conta o que vocês oferecem e eu ajusto a demo",
1827
- f"já tenho o nome ||| me dá um resumo rápido do negócio e eu sigo daqui",
1828
- ]
1829
- else:
1830
- _no_info_opts = [
1831
- f"ya anoté {nombre} ||| cuéntame a qué se dedican y te muestro cómo respondería",
1832
- f"listo, {nombre} ||| no los encuentro en Google todavía — cuéntame qué hacen y arrancamos",
1833
- f"ya los tengo ||| igual puedo hacer la demo — escríbeme un poco de qué trata el negocio",
1834
- ]
1835
- r = _r.choice(_no_info_opts)
1836
-
1837
- # ── Burbuja extra: confirmación del link ─────────────────────────
1838
- # Solo si encontramos info real (no cuando usamos el fallback de Google search)
1839
- import urllib.parse as _up
1840
- is_fallback_url = biz_url.startswith("https://www.google.com/search") or biz_url.startswith("https://www.google.com/maps/search")
1841
- if biz_url and found and not is_fallback_url:
1842
- # Natural: manda el link con texto corto, sin pregunta directa
1843
- if _owner_is_english():
1844
- _link_intros = [
1845
- "I found this for you",
1846
- "this looks like your business",
1847
- "I found you here",
1848
- "this is what I found for the business",
1849
- ]
1850
- elif _owner_is_portuguese():
1851
- _link_intros = [
1852
- "achei isso de vocês",
1853
- "encontrei vocês por aqui",
1854
- "isso parece ser de vocês",
1855
- "foi isso que eu achei do negócio",
1856
- ]
1857
- else:
1858
- _link_intros = [
1859
- "mira, encontré esto de ustedes",
1860
- "los encontré por acá",
1861
- "esto es de ustedes",
1862
- "vi esto de su negocio",
1863
- ]
1864
- r = r.rstrip() + f" ||| {_r.choice(_link_intros)} ||| {biz_url}"
1865
-
1795
+ return _send("⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
1866
1796
  return _send(r)
1867
1797
 
1868
1798
  # ── Confirmación positiva del link: "sí ese es / correcto / sí" ───────────
@@ -2155,7 +2085,7 @@ Natural, sin punto al final, sin ¿¡, en minúscula.""",
2155
2085
  f"perfecto, ya leí el documento de {business_name}"
2156
2086
  f" ||| ya sé de qué se tratan — probemos, Escríbeme algo como cliente"
2157
2087
  )
2158
- return _send(r or fallback)
2088
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2159
2089
  else:
2160
2090
  # Doc llegó pero no pudimos extraer texto (imagen, binario raro)
2161
2091
  return _send(
@@ -2244,7 +2174,7 @@ Natural, sin punto al final, sin ¿¡, en minúscula.""",
2244
2174
  f"listo, ya entendí bien lo que hace {business_name} ||| "
2245
2175
  f"arrancamos? Escríbeme algo como cliente a ver qué pasa"
2246
2176
  )
2247
- return _send(r or fallback)
2177
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2248
2178
 
2249
2179
  elif not _has_what:
2250
2180
  # Falta: qué hacen
@@ -2255,7 +2185,7 @@ Todavía no sabes exactamente qué servicios o productos ofrecen.
2255
2185
  Haz UNA pregunta natural para entenderlo. Muy corta. Sin punto al final. En minúscula. Sin ¿ ni ¡.""",
2256
2186
  "preguntando qué hacen", max_t=80
2257
2187
  )
2258
- return _send(r or "y a qué se dedican exactamente")
2188
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2259
2189
 
2260
2190
  elif not _has_where:
2261
2191
  # Falta: dónde están
@@ -2267,7 +2197,7 @@ Haz UNA pregunta natural para saberlo. Muy corta. Sin punto al final. En minúsc
2267
2197
  Ejemplo: "y dónde están ubicados?" o "en qué ciudad o barrio están" """,
2268
2198
  "preguntando ubicación", max_t=80
2269
2199
  )
2270
- return _send(r or "¿y dónde están ubicados?")
2200
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2271
2201
 
2272
2202
  else:
2273
2203
  # Seguir aprendiendo con una pregunta más
@@ -2278,7 +2208,7 @@ Haz UNA pregunta más para entender mejor al negocio (horario, qué los diferenc
2278
2208
  Muy corta. Sin punto al final. En minúscula. Sin ¿ ni ¡.""",
2279
2209
  "pregunta adicional", max_t=80
2280
2210
  )
2281
- return _send(r or "¿y cuál es su horario de atención?")
2211
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2282
2212
 
2283
2213
  # ── INTERCEPTOR: preguntas meta (soy bot? eres real? eres IA?) ─────────
2284
2214
  # Deben responderse ANTES del flujo normal — sin buscar en web ni confundirse
@@ -2427,6 +2357,7 @@ IDENTIDAD Y CREADOR — REGLA DURA
2427
2357
  """
2428
2358
  customer_history = sim_history[-8:]
2429
2359
  customer_had_output = False
2360
+ last_candidate = None
2430
2361
  customer_reply = None
2431
2362
  original_history = history
2432
2363
  history = customer_history
@@ -2574,7 +2505,7 @@ Maneja en 2 burbujas (|||). REGLAS ESTRICTAS:
2574
2505
 
2575
2506
  Ejemplo del tono que quiero:
2576
2507
  "sí, hay de todo en el mercado ||| qué presupuesto tienes más o menos, para ver qué te muestro" """, "maneja la objeción")
2577
- return _send((r or f"sí, hay de todo en el mercado ||| qué presupuesto tienes más o menos, para ver qué te muestro") + _next_trick())
2508
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2578
2509
 
2579
2510
  if detected_cmd == "/cita":
2580
2511
  r = await _llm(f"""Eres Conny, asesora de {business_name}. Un cliente acaba de decir que quiere ir o comprar.
@@ -2590,7 +2521,7 @@ Flujo sugerido:
2590
2521
  Sin punto al final. Sin ¿¡. Máximo 1-2 oraciones por burbuja.
2591
2522
  Ejemplo del tono: "qué producto te interesa llevar ||| esta semana puedo el miércoles o el viernes — cuál te queda" """,
2592
2523
  "quiero comprar / quiero ir", max_t=350)
2593
- return _send((r or f"qué te interesa llevar ||| esta semana tengo el miércoles o el viernes, cuál te queda mejor") + _next_trick())
2524
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2594
2525
 
2595
2526
  if detected_cmd == "/stats":
2596
2527
  return _send(f"el 78% de los clientes no vuelven si no les responden en menos de 5 minutos ||| una cita perdida en {business_name} vale entre $80k y $500k según el servicio ||| Conny responde en menos de 3 segundos, 24/7, sin días libres ni mal humor" + _next_trick())
@@ -2601,7 +2532,7 @@ Ejemplo del tono: "qué producto te interesa llevar ||| esta semana puedo el mi
2601
2532
  if detected_cmd == "/cierre":
2602
2533
  r = await _llm(f"""Eres Conny de {business_name}. Un cliente lleva 3 mensajes dudando.
2603
2534
  Haz el cierre en 2 burbujas (|||). Directo, con urgencia real. Sin presión forzada. Sin punto al final.""", "no sé, lo pienso")
2604
- return _send((r or f"claro, sin afán ||| igual te separo un espacio esta semana si decides que no, lo cancelas. te queda bien el jueves") + _next_trick())
2535
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2605
2536
 
2606
2537
  if detected_cmd == "/list":
2607
2538
  lista = (
@@ -2673,7 +2604,7 @@ Haz el cierre en 2 burbujas (|||). Directo, con urgencia real. Sin presión forz
2673
2604
  r = await _llm(f"""El usuario ha dicho: "{hist_text[:300]}"
2674
2605
  Extrae datos mencionados (nombre, interés, servicio). Demuestra en 2 burbujas (|||) que los recuerdas.
2675
2606
  Si no hay datos: "todavía no me has dado tu nombre — pero cuando lo hagas, lo recuerdo para siempre". Sin punto al final.""", "qué recuerdas")
2676
- return _send(r or "todo lo que me dices lo guardo ||| nombre, servicio de interés, objeciones todo queda")
2607
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2677
2608
 
2678
2609
  if detected_cmd == "/2am":
2679
2610
  return _send(f"son las 2 de la madrugada y estoy aquí ||| tu recepcionista está durmiendo — yo no. nunca" + _next_trick())
@@ -2681,12 +2612,12 @@ Si no hay datos: "todavía no me has dado tu nombre — pero cuando lo hagas, lo
2681
2612
  if detected_cmd == "/competencia":
2682
2613
  r = await _llm(f"""Eres Conny de {business_name}. Un cliente dice: "ya fui a otra parte y no me gustó."
2683
2614
  Responde en 2 burbujas (|||). Sin atacar a la competencia. Natural. Sin punto al final.""", "ya fui a otro lado")
2684
- return _send((r or f"ay qué pena ||| qué fue lo que no te gustó acá antes de tocar nada hacemos valoración para asegurarnos del resultado") + _next_trick())
2615
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2685
2616
 
2686
2617
  if detected_cmd == "/precio":
2687
2618
  r = await _llm(f"""Eres Conny de {business_name}. Un cliente dice: "está muy caro."
2688
2619
  Maneja en 2 burbujas (|||). Enfócate en valor. Cierra hacia valoración con día concreto. Sin punto al final.""", "está muy caro")
2689
- return _send((r or f"sí, vale lo que vale ||| los resultados duran, en la valoración gratis te dicen el número exacto. cuándo puedes") + _next_trick())
2620
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2690
2621
 
2691
2622
  if detected_cmd == "/menu_bot":
2692
2623
  # Modo bot — IVR con emojis, ideal para negocios que prefieren menú estructurado
@@ -3055,7 +2986,7 @@ OBJECIONES
3055
2986
  recent_limit=8,
3056
2987
  )
3057
2988
  if not r:
3058
- r = _demo_customer_last_resort(text)
2989
+ r = "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente."
3059
2990
  # Solo revelar truco si la respuesta tiene contenido real (>60 chars)
3060
2991
  # y no termina en pregunta (no interrumpir el flujo de la conversación)
3061
2992
  if _should_reveal_trick and r and len(r.replace("|||","").strip()) > 60:
@@ -3107,4 +3038,3 @@ OBJECIONES
3107
3038
  return _send(r)
3108
3039
 
3109
3040
 
3110
-
@@ -447,17 +447,8 @@ async def handle_demo_message(
447
447
  )
448
448
  _save("assistant", r)
449
449
  bubbles = self._split_bubbles(r, chat_id=chat_id, archetype=_demo_archetype)
450
- if should_normalize_first_turn and len(bubbles) == 1:
451
- _text_norm = _normalize_conv_text(text or "")
452
- _greeting_tokens = (
453
- "hola", "buenas", "buenas tardes", "buenos dias", "buenos días",
454
- "buenas noches", "hey", "holi", "hi", "hello",
455
- "good morning", "good afternoon", "good evening",
456
- )
457
- if any(_text_norm == token or _text_norm.startswith(token + " ") for token in _greeting_tokens):
458
- lowered_bubble = _normalize_conv_text(bubbles[0] or "")
459
- if not any(token in lowered_bubble for token in ("cuentame", "cuéntame", "revisar", "ayudo", "ayudar")):
460
- bubbles.append(_lang_text("cuéntame qué te gustaría revisar", "what would you like to check?"))
450
+ # The LLM owns the follow-up. Do not append generic CTA bubbles here:
451
+ # that made good model answers sound like fallback.
461
452
  tone = self._demo_sessions.get(btone_key, "GENERAL")
462
453
  if tone in ("SALUD PREMIUM", "PREMIUM"):
463
454
  bubbles = [b[0].upper() + b[1:] if b else b for b in bubbles]
@@ -978,11 +969,10 @@ async def handle_demo_message(
978
969
  last_candidate = candidate
979
970
  if not validator(candidate):
980
971
  return candidate, True
981
- if had_output and last_candidate and not looks_fragmented_reply(last_candidate):
982
- candidate_norm = _normalize_conv_text(last_candidate)
983
- if len(candidate_norm.split()) >= 5 and not validator(last_candidate):
984
- return last_candidate, True
985
- return None, had_output
972
+ # BUG FIX ARCHITECTURE: si el modelo generó algo, así no pase validaciones, se devuelve. NO FALLBACK.
973
+ if had_output and last_candidate:
974
+ return last_candidate, True
975
+ return None, False
986
976
 
987
977
  def _save(role, msg):
988
978
  if db:
@@ -1024,20 +1014,7 @@ async def handle_demo_message(
1024
1014
  )
1025
1015
  _save("assistant", r)
1026
1016
  bubbles = self._split_bubbles(r, chat_id=chat_id, archetype=_demo_archetype)
1027
- # FIX BUG 4: Si es el primer turno, el usuario saludó, y la respuesta tiene
1028
- # solo 1 burbuja sin pregunta de seguimiento → agregar burbuja de apertura.
1029
- # Esta lógica existía en la primera definición de _send (línea 13772) pero
1030
- # se perdió cuando se redefinió _send aquí en la misma función.
1031
- if should_normalize_first_turn and len(bubbles) == 1:
1032
- _text_norm = _normalize_conv_text(text or "")
1033
- _greeting_tokens = (
1034
- "hola", "buenas", "buenas tardes", "buenos dias", "buenos días",
1035
- "buenas noches", "hey", "holi",
1036
- )
1037
- if any(_text_norm == token or _text_norm.startswith(token + " ") for token in _greeting_tokens):
1038
- lowered_bubble = _normalize_conv_text(bubbles[0] or "")
1039
- if not any(token in lowered_bubble for token in ("cuentame", "cuéntame", "revisar", "ayudo", "ayudar")):
1040
- bubbles.append("cuéntame qué te gustaría revisar")
1017
+ # The LLM owns the follow-up. Do not append generic CTA bubbles here.
1041
1018
  # Para premium/salud premium: restaurar mayúscula inicial
1042
1019
  tone = self._demo_sessions.get(btone_key, "GENERAL")
1043
1020
  if tone in ("SALUD PREMIUM", "PREMIUM"):
@@ -1336,10 +1313,12 @@ async def handle_demo_message(
1336
1313
  )
1337
1314
  if candidate and candidate.strip():
1338
1315
  had_output = True
1316
+ last_candidate = candidate
1339
1317
  if not validator(candidate):
1340
1318
  return candidate, True
1341
- return None, had_output
1342
-
1319
+ if had_output and last_candidate:
1320
+ return last_candidate, True
1321
+ return None, False
1343
1322
 
1344
1323
  def _demo_owner_last_resort(
1345
1324
  user_text: str,
@@ -2181,63 +2160,7 @@ Máximo 1 oración por burbuja. Natural y seguro."""
2181
2160
  max_t=220,
2182
2161
  )
2183
2162
  if not r:
2184
- if found:
2185
- r = _lang_text(
2186
- f"ya tengo {nombre} ||| ya me ubiqué con cómo tendría que sonar esto ||| Escríbeme como si fueras un cliente y te respondo",
2187
- f"I’ve got {nombre} now ||| I already know how this chat should sound ||| text me like a real client and I’ll reply in context",
2188
- f"já tenho {nombre} ||| já entendi como esse chat precisa soar ||| me escreve como um cliente real e eu respondo em contexto",
2189
- )
2190
- else:
2191
- # v12: no info → opciones naturales, sin exponer estado interno
2192
- if _owner_is_english():
2193
- _no_info_opts = [
2194
- f"got it, {nombre} ||| tell me what the business does and I’ll shape the demo around that",
2195
- f"okay, {nombre} ||| I’m not finding solid public info yet, so tell me what you offer and I’ll ground it from there",
2196
- f"I’ve got the name now ||| give me a quick picture of the business and I’ll keep going",
2197
- ]
2198
- elif _owner_is_portuguese():
2199
- _no_info_opts = [
2200
- f"perfeito, {nombre} ||| me conta com o que o negócio trabalha e eu monto a demo nisso",
2201
- f"ok, {nombre} ||| ainda não achei informação pública forte, então me conta o que vocês oferecem e eu ajusto a demo",
2202
- f"já tenho o nome ||| me dá um resumo rápido do negócio e eu sigo daqui",
2203
- ]
2204
- else:
2205
- _no_info_opts = [
2206
- f"ya anoté {nombre} ||| cuéntame a qué se dedican y te muestro cómo respondería",
2207
- f"listo, {nombre} ||| no los encuentro en Google todavía — cuéntame qué hacen y arrancamos",
2208
- f"ya los tengo ||| igual puedo hacer la demo — escríbeme un poco de qué trata el negocio",
2209
- ]
2210
- r = _r.choice(_no_info_opts)
2211
-
2212
- # ── Burbuja extra: confirmación del link ─────────────────────────
2213
- # Solo si encontramos info real (no cuando usamos el fallback de Google search)
2214
- import urllib.parse as _up
2215
- is_fallback_url = biz_url.startswith("https://www.google.com/search") or biz_url.startswith("https://www.google.com/maps/search")
2216
- if biz_url and found and not is_fallback_url:
2217
- # Natural: manda el link con texto corto, sin pregunta directa
2218
- if _owner_is_english():
2219
- _link_intros = [
2220
- "I found this for you",
2221
- "this looks like your business",
2222
- "I found you here",
2223
- "this is what I found for the business",
2224
- ]
2225
- elif _owner_is_portuguese():
2226
- _link_intros = [
2227
- "achei isso de vocês",
2228
- "encontrei vocês por aqui",
2229
- "isso parece ser de vocês",
2230
- "foi isso que eu achei do negócio",
2231
- ]
2232
- else:
2233
- _link_intros = [
2234
- "mira, encontré esto de ustedes",
2235
- "los encontré por acá",
2236
- "esto es de ustedes",
2237
- "vi esto de su negocio",
2238
- ]
2239
- r = r.rstrip() + f" ||| {_r.choice(_link_intros)} ||| {biz_url}"
2240
-
2163
+ return _send("⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2241
2164
  return _send(r)
2242
2165
 
2243
2166
  # ── Confirmación positiva del link: "sí ese es / correcto / sí" ───────────
@@ -2553,7 +2476,7 @@ Natural, sin punto al final, sin ¿¡, en minúscula.""",
2553
2476
  f"perfecto, ya leí el documento de {business_name}"
2554
2477
  f" ||| ya sé de qué se tratan — probemos, Escríbeme algo como cliente"
2555
2478
  )
2556
- return _send(r or fallback)
2479
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2557
2480
  else:
2558
2481
  # Doc llegó pero no pudimos extraer texto (imagen, binario raro)
2559
2482
  return _send(
@@ -2714,7 +2637,7 @@ Natural, sin punto al final, sin ¿¡, en minúscula.""",
2714
2637
  f"listo, ya entendí bien lo que hace {business_name} ||| "
2715
2638
  f"arrancamos? Escríbeme algo como cliente a ver qué pasa"
2716
2639
  )
2717
- return _send(r or fallback)
2640
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2718
2641
 
2719
2642
  elif not _has_what:
2720
2643
  # Falta: qué hacen
@@ -2725,7 +2648,7 @@ Todavía no sabes exactamente qué servicios o productos ofrecen.
2725
2648
  Haz UNA pregunta natural para entenderlo. Muy corta. Sin punto al final. En minúscula. Sin ¿ ni ¡.""",
2726
2649
  "preguntando qué hacen", max_t=80
2727
2650
  )
2728
- return _send(r or "y a qué se dedican exactamente")
2651
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2729
2652
 
2730
2653
  elif not _has_where:
2731
2654
  # Falta: dónde están
@@ -2737,7 +2660,7 @@ Haz UNA pregunta natural para saberlo. Muy corta. Sin punto al final. En minúsc
2737
2660
  Ejemplo: "y dónde están ubicados?" o "en qué ciudad o barrio están" """,
2738
2661
  "preguntando ubicación", max_t=80
2739
2662
  )
2740
- return _send(r or "¿y dónde están ubicados?")
2663
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2741
2664
 
2742
2665
  else:
2743
2666
  # Seguir aprendiendo con una pregunta más
@@ -2748,7 +2671,7 @@ Haz UNA pregunta más para entender mejor al negocio (horario, qué los diferenc
2748
2671
  Muy corta. Sin punto al final. En minúscula. Sin ¿ ni ¡.""",
2749
2672
  "pregunta adicional", max_t=80
2750
2673
  )
2751
- return _send(r or "¿y cuál es su horario de atención?")
2674
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
2752
2675
 
2753
2676
  # ── INTERCEPTOR: preguntas meta (soy bot? eres real? eres IA?) ─────────
2754
2677
  # Deben responderse ANTES del flujo normal — sin buscar en web ni confundirse
@@ -3047,7 +2970,7 @@ Maneja en 2 burbujas (|||). REGLAS ESTRICTAS:
3047
2970
 
3048
2971
  Ejemplo del tono que quiero:
3049
2972
  "sí, hay de todo en el mercado ||| qué presupuesto tienes más o menos, para ver qué te muestro" """, "maneja la objeción")
3050
- return _send((r or f"sí, hay de todo en el mercado ||| qué presupuesto tienes más o menos, para ver qué te muestro") + _next_trick())
2973
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
3051
2974
 
3052
2975
  if detected_cmd == "/cita":
3053
2976
  r = await _llm(f"""Eres Conny, asesora de {business_name}. Un cliente acaba de decir que quiere ir o comprar.
@@ -3063,7 +2986,7 @@ Flujo sugerido:
3063
2986
  Sin punto al final. Sin ¿¡. Máximo 1-2 oraciones por burbuja.
3064
2987
  Ejemplo del tono: "qué producto te interesa llevar ||| esta semana puedo el miércoles o el viernes — cuál te queda" """,
3065
2988
  "quiero comprar / quiero ir", max_t=350)
3066
- return _send((r or f"qué te interesa llevar ||| esta semana tengo el miércoles o el viernes, cuál te queda mejor") + _next_trick())
2989
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
3067
2990
 
3068
2991
  if detected_cmd == "/stats":
3069
2992
  return _send(f"el 78% de los clientes no vuelven si no les responden en menos de 5 minutos ||| una cita perdida en {business_name} vale entre $80k y $500k según el servicio ||| Conny responde en menos de 3 segundos, 24/7, sin días libres ni mal humor" + _next_trick())
@@ -3074,7 +2997,7 @@ Ejemplo del tono: "qué producto te interesa llevar ||| esta semana puedo el mi
3074
2997
  if detected_cmd == "/cierre":
3075
2998
  r = await _llm(f"""Eres Conny de {business_name}. Un cliente lleva 3 mensajes dudando.
3076
2999
  Haz el cierre en 2 burbujas (|||). Directo, con urgencia real. Sin presión forzada. Sin punto al final.""", "no sé, lo pienso")
3077
- return _send((r or f"claro, sin afán ||| igual te separo un espacio esta semana si decides que no, lo cancelas. te queda bien el jueves") + _next_trick())
3000
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
3078
3001
 
3079
3002
  if detected_cmd == "/list":
3080
3003
  lista = (
@@ -3146,7 +3069,7 @@ Haz el cierre en 2 burbujas (|||). Directo, con urgencia real. Sin presión forz
3146
3069
  r = await _llm(f"""El usuario ha dicho: "{hist_text[:300]}"
3147
3070
  Extrae datos mencionados (nombre, interés, servicio). Demuestra en 2 burbujas (|||) que los recuerdas.
3148
3071
  Si no hay datos: "todavía no me has dado tu nombre — pero cuando lo hagas, lo recuerdo para siempre". Sin punto al final.""", "qué recuerdas")
3149
- return _send(r or "todo lo que me dices lo guardo ||| nombre, servicio de interés, objeciones todo queda")
3072
+ return _send(r if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
3150
3073
 
3151
3074
  if detected_cmd == "/2am":
3152
3075
  return _send(f"son las 2 de la madrugada y estoy aquí ||| tu recepcionista está durmiendo — yo no. nunca" + _next_trick())
@@ -3154,12 +3077,12 @@ Si no hay datos: "todavía no me has dado tu nombre — pero cuando lo hagas, lo
3154
3077
  if detected_cmd == "/competencia":
3155
3078
  r = await _llm(f"""Eres Conny de {business_name}. Un cliente dice: "ya fui a otra parte y no me gustó."
3156
3079
  Responde en 2 burbujas (|||). Sin atacar a la competencia. Natural. Sin punto al final.""", "ya fui a otro lado")
3157
- return _send((r or f"ay qué pena ||| qué fue lo que no te gustó acá antes de tocar nada hacemos valoración para asegurarnos del resultado") + _next_trick())
3080
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
3158
3081
 
3159
3082
  if detected_cmd == "/precio":
3160
3083
  r = await _llm(f"""Eres Conny de {business_name}. Un cliente dice: "está muy caro."
3161
3084
  Maneja en 2 burbujas (|||). Enfócate en valor. Cierra hacia valoración con día concreto. Sin punto al final.""", "está muy caro")
3162
- return _send((r or f"sí, vale lo que vale ||| los resultados duran, en la valoración gratis te dicen el número exacto. cuándo puedes") + _next_trick())
3085
+ return _send((r + _next_trick()) if r else "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente.")
3163
3086
 
3164
3087
  if detected_cmd == "/menu_bot":
3165
3088
  # Modo bot — IVR con emojis, ideal para negocios que prefieren menú estructurado
@@ -3528,7 +3451,7 @@ OBJECIONES
3528
3451
  recent_limit=8,
3529
3452
  )
3530
3453
  if not r:
3531
- r = _demo_customer_last_resort(text)
3454
+ r = "⚠️ Fallo del modelo LLM. No obtuve respuesta. Por favor, envía tu mensaje nuevamente."
3532
3455
  # Solo revelar truco si la respuesta tiene contenido real (>60 chars)
3533
3456
  # y no termina en pregunta (no interrumpir el flujo de la conversación)
3534
3457
  if _should_reveal_trick and r and len(r.replace("|||","").strip()) > 60: