@innvisor/conny-ai 9.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +369 -0
  5. package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
  6. package/brand-assets/Conny.web.logo.png +0 -0
  7. package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
  8. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
  9. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
  10. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
  11. package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
  12. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
  13. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
  14. package/brand-assets/conny-demo/manifest.json +22 -0
  15. package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
  16. package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
  17. package/brand-assets/conny-logo.png +0 -0
  18. package/brand-assets/web.background.png +0 -0
  19. package/brand_assets.py +323 -0
  20. package/conny +28 -0
  21. package/conny-chat.py +579 -0
  22. package/conny-omni.py +3843 -0
  23. package/conny.py +113 -0
  24. package/conny_agents/__init__.py +1 -0
  25. package/conny_agents/agenda.py +1 -0
  26. package/conny_agents/captacion.py +1 -0
  27. package/conny_agents/conocimiento.py +1 -0
  28. package/conny_agents/escalacion.py +1 -0
  29. package/conny_agents/objeciones.py +1 -0
  30. package/conny_agents/seguimiento.py +1 -0
  31. package/conny_app.py +287 -0
  32. package/conny_audio.py +350 -0
  33. package/conny_audio_learn.py +84 -0
  34. package/conny_brain_v10.py +804 -0
  35. package/conny_bridge.py +656 -0
  36. package/conny_calendar.py +169 -0
  37. package/conny_cli.py +11784 -0
  38. package/conny_cli_bb.py +437 -0
  39. package/conny_commands.py +243 -0
  40. package/conny_config.py +215 -0
  41. package/conny_core/__init__.py +3 -0
  42. package/conny_core/conversation_engine.py +446 -0
  43. package/conny_core/first_turn_ops.py +287 -0
  44. package/conny_core/persona_registry.py +157 -0
  45. package/conny_core/prompt_ops.py +561 -0
  46. package/conny_cron.py +72 -0
  47. package/conny_demo_v2.py +209 -0
  48. package/conny_demo_voice.py +134 -0
  49. package/conny_design.py +43 -0
  50. package/conny_doctor.py +319 -0
  51. package/conny_domino.py +696 -0
  52. package/conny_generator.py +447 -0
  53. package/conny_google_auth.py +159 -0
  54. package/conny_i18n.py +619 -0
  55. package/conny_init.py +509 -0
  56. package/conny_integrations/__init__.py +4 -0
  57. package/conny_integrations/llm.py +1 -0
  58. package/conny_integrations/vault.py +77 -0
  59. package/conny_integrations/whatsapp.py +1 -0
  60. package/conny_intelligence.py +65 -0
  61. package/conny_learning.py +154 -0
  62. package/conny_memory.py +243 -0
  63. package/conny_memory_engine.py +292 -0
  64. package/conny_nova_proxy.py +170 -0
  65. package/conny_nuke_robot_phrases.py +493 -0
  66. package/conny_pairing.py +253 -0
  67. package/conny_patch.py +291 -0
  68. package/conny_persona_cli.py +150 -0
  69. package/conny_router.py +308 -0
  70. package/conny_runtime_ops.py +271 -0
  71. package/conny_session.py +516 -0
  72. package/conny_skills/__init__.py +1 -0
  73. package/conny_skills/demo_mode.py +35 -0
  74. package/conny_skills/text_processing.py +1 -0
  75. package/conny_skills/tone_detection.py +1 -0
  76. package/conny_smart_features.py +333 -0
  77. package/conny_studio.py +161 -0
  78. package/conny_sync_fix.py +306 -0
  79. package/conny_tui.py +512 -0
  80. package/conny_tui_select.py +202 -0
  81. package/conny_ultra_config.py +411 -0
  82. package/conny_uncertainty.py +174 -0
  83. package/conny_utils.py +87 -0
  84. package/conny_voice.py +156 -0
  85. package/conny_voice_engine.py +124 -0
  86. package/conny_web_search.py +66 -0
  87. package/conny_weekly_report.py +85 -0
  88. package/conny_worm.py +88 -0
  89. package/core/__init__.py +25 -0
  90. package/ecosystem.config.js +24 -0
  91. package/fix_init.py +27 -0
  92. package/install.sh +78 -0
  93. package/knowledge_base.py +330 -0
  94. package/nova/rules/default.yaml +37 -0
  95. package/nova_bridge.py +509 -0
  96. package/npm/conny.js +471 -0
  97. package/package.json +102 -0
  98. package/personas/conny/base/default.yaml +35 -0
  99. package/personas/conny/base/estetica_whatsapp.yaml +36 -0
  100. package/requirements.txt +14 -0
  101. package/run.sh +47 -0
  102. package/search.py +465 -0
  103. package/smart_handoff.py +1150 -0
  104. package/src/__init__.py +0 -0
  105. package/src/conny/__init__.py +0 -0
  106. package/src/conny/admin/__init__.py +0 -0
  107. package/src/conny/admin/api.py +234 -0
  108. package/src/conny/admin/dashboard.py +772 -0
  109. package/src/conny/api/__init__.py +0 -0
  110. package/src/conny/api/routes.py +8851 -0
  111. package/src/conny/brain/__init__.py +15 -0
  112. package/src/conny/brain/engine.py +804 -0
  113. package/src/conny/brain/learning.py +154 -0
  114. package/src/conny/brain/memory.py +324 -0
  115. package/src/conny/brain/smart_features.py +333 -0
  116. package/src/conny/brain/uncertainty.py +167 -0
  117. package/src/conny/channels/__init__.py +0 -0
  118. package/src/conny/channels/audio.py +316 -0
  119. package/src/conny/channels/cli.py +11795 -0
  120. package/src/conny/channels/logo_art.py +11 -0
  121. package/src/conny/channels/voice.py +156 -0
  122. package/src/conny/core/__init__.py +0 -0
  123. package/src/conny/core/config.py +215 -0
  124. package/src/conny/core/cron.py +72 -0
  125. package/src/conny/core/messenger.py +563 -0
  126. package/src/conny/core/router.py +297 -0
  127. package/src/conny/core/session.py +312 -0
  128. package/src/conny/demo/__init__.py +0 -0
  129. package/src/conny/demo/handler.py +3110 -0
  130. package/src/conny/integrations/__init__.py +19 -0
  131. package/src/conny/integrations/calendar.py +169 -0
  132. package/src/conny/integrations/knowledge.py +312 -0
  133. package/src/conny/integrations/search.py +66 -0
  134. package/src/conny/personas/__init__.py +0 -0
  135. package/src/conny/personas/generator.py +447 -0
  136. package/src/conny/production/__init__.py +0 -0
  137. package/src/conny/production/domino.py +696 -0
  138. package/src/conny/production/guard.py +550 -0
  139. package/src/conny/production/handoff.py +1150 -0
  140. package/src/conny/production/monitor.py +353 -0
  141. package/src/conny/utils/__init__.py +2 -0
  142. package/src/conny/utils/helpers.py +75 -0
  143. package/src/conny/utils/i18n.py +619 -0
  144. package/src/core/admin_engines.py +772 -0
  145. package/src/core/globals.py +11845 -0
  146. package/src/core/orchestrator.py +273 -0
  147. package/src/core/production_monitor.py +353 -0
  148. package/src/core/runtime.py +5487 -0
  149. package/src/domain/onboarding_flow.py +230 -0
  150. package/src/domain/prompts/__init__.py +1 -0
  151. package/src/domain/prompts/prospect_pitch.py +282 -0
  152. package/src/domain/send_guard.py +636 -0
  153. package/src/domain/swarm/queen.py +96 -0
  154. package/src/infrastructure/llm_providers/engine.py +487 -0
  155. package/src/interfaces/mcp_server.py +73 -0
  156. package/src/interfaces/nova_bridge.py +58 -0
  157. package/src/interfaces/web/admin_api.py +1379 -0
  158. package/src/interfaces/web/app.py +9408 -0
  159. package/src/interfaces/web/demo_handler.py +3450 -0
  160. package/src/interfaces/web/static/generate_avatars.py +46 -0
  161. package/v7/__init__.py +46 -0
  162. package/v7/agents/__init__.py +46 -0
  163. package/v7/agents/agenda.py +77 -0
  164. package/v7/agents/base.py +216 -0
  165. package/v7/agents/captacion.py +60 -0
  166. package/v7/agents/conocimiento.py +69 -0
  167. package/v7/agents/escalacion.py +83 -0
  168. package/v7/agents/objeciones.py +109 -0
  169. package/v7/agents/seguimiento.py +71 -0
  170. package/v7/memory/__init__.py +46 -0
  171. package/v7/memory/patient_profile.py +200 -0
  172. package/v7/orchestrator.py +275 -0
  173. package/v7/postprocess.py +127 -0
  174. package/v7/router.py +239 -0
  175. package/verify_conversation_impl.py +48 -0
@@ -0,0 +1,170 @@
1
+ """
2
+ conny_nova_proxy.py — Transparent LLM proxy through Nova governance.
3
+ Wraps ALL outbound LLM calls so Nova can validate responses before delivery.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import time
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ log = logging.getLogger("conny.nova_proxy")
12
+
13
+
14
+ class NovaLLMProxy:
15
+ """
16
+ Transparent proxy that wraps LLM engine calls.
17
+ Applies Nova governance AFTER LLM response is generated, BEFORE delivery.
18
+
19
+ Usage:
20
+ proxy = NovaLLMProxy(llm_engine, nova_guard)
21
+ response, meta = await proxy.complete(messages, **kwargs)
22
+ """
23
+
24
+ def __init__(self, llm_engine, nova_guard=None, voice_engine=None,
25
+ uncertainty_detector=None, memory_engine=None):
26
+ self._llm = llm_engine
27
+ self._guard = nova_guard
28
+ self._voice = voice_engine
29
+ self._uncertainty = uncertainty_detector
30
+ self._memory = memory_engine
31
+ self._call_count = 0
32
+ self._blocked_count = 0
33
+
34
+ async def complete(
35
+ self,
36
+ messages: List[Dict[str, str]],
37
+ *,
38
+ model_tier: str = "fast",
39
+ temperature: float = 0.7,
40
+ max_tokens: int = 2048,
41
+ use_cache: bool = True,
42
+ instance_id: str = "",
43
+ chat_id: str = "",
44
+ inject_memory: bool = True,
45
+ inject_thinking: bool = True,
46
+ apply_voice: bool = True,
47
+ **kwargs,
48
+ ) -> Tuple[str, Dict[str, Any]]:
49
+ """
50
+ Full pipeline: memory recall -> thinking injection -> LLM call ->
51
+ Nova validation -> uncertainty check -> voice humanization.
52
+ """
53
+ self._call_count += 1
54
+ meta = {"proxy": True, "instance_id": instance_id}
55
+ start = time.time()
56
+
57
+ working_messages = list(messages)
58
+
59
+ # 1. Memory injection (prepend relevant context)
60
+ if inject_memory and self._memory and instance_id and chat_id:
61
+ try:
62
+ user_msg = ""
63
+ for m in reversed(working_messages):
64
+ if m.get("role") == "user":
65
+ user_msg = m["content"]
66
+ break
67
+ if user_msg:
68
+ memories = await self._memory.recall_context(instance_id, user_msg, top_k=3)
69
+ if memories:
70
+ memory_text = "\n".join(
71
+ f"- [{m['ts'][:10]}] {' | '.join(msg['content'][:100] for msg in m['messages'][-2:])}"
72
+ for m in memories[:3]
73
+ )
74
+ memory_block = f"\nMEMORIA RELEVANTE (conversaciones anteriores):\n{memory_text}\n"
75
+ if working_messages and working_messages[0]["role"] == "system":
76
+ working_messages[0]["content"] += memory_block
77
+ else:
78
+ working_messages.insert(0, {"role": "system", "content": memory_block})
79
+ meta["memory_injected"] = len(memories)
80
+ except Exception as e:
81
+ log.debug(f"[nova_proxy] memory injection skipped: {e}")
82
+
83
+ # 2. Thinking block injection
84
+ if inject_thinking and self._voice:
85
+ try:
86
+ if working_messages and working_messages[0]["role"] == "system":
87
+ working_messages[0]["content"] = self._voice.inject_thinking_block(
88
+ working_messages[0]["content"]
89
+ )
90
+ meta["thinking_injected"] = True
91
+ except Exception:
92
+ pass
93
+
94
+ # 3. LLM call
95
+ if not self._llm:
96
+ return "", {"error": "no_llm_engine"}
97
+
98
+ response, llm_meta = await self._llm.complete(
99
+ working_messages,
100
+ model_tier=model_tier,
101
+ temperature=temperature,
102
+ max_tokens=max_tokens,
103
+ use_cache=use_cache,
104
+ **kwargs,
105
+ )
106
+ meta.update(llm_meta)
107
+
108
+ # 4. Nova governance check (validate response before delivery)
109
+ if self._guard and response:
110
+ try:
111
+ ok, reason, ledger_id = await self._guard.should_send(
112
+ response[:500],
113
+ patient_chat_id=chat_id,
114
+ context=instance_id,
115
+ )
116
+ meta["nova_verdict"] = "approved" if ok else "blocked"
117
+ meta["nova_reason"] = reason
118
+ if ledger_id:
119
+ meta["nova_ledger_id"] = ledger_id
120
+ if not ok:
121
+ self._blocked_count += 1
122
+ log.info(f"[nova_proxy] response BLOCKED: {reason}")
123
+ response = self._generate_safe_fallback(reason)
124
+ meta["nova_fallback"] = True
125
+ except Exception as e:
126
+ log.debug(f"[nova_proxy] guard check skipped: {e}")
127
+ meta["nova_verdict"] = "skipped"
128
+
129
+ # 5. Uncertainty detection
130
+ if self._uncertainty and response:
131
+ try:
132
+ user_msg = ""
133
+ for m in reversed(messages):
134
+ if m.get("role") == "user":
135
+ user_msg = m["content"]
136
+ break
137
+ confidence = self._uncertainty.confidence_score(response, user_msg, messages)
138
+ meta["confidence"] = confidence
139
+ if confidence < self._uncertainty.threshold:
140
+ meta["low_confidence"] = True
141
+ await self._uncertainty.log_gap(
142
+ instance_id, user_msg, response, confidence, chat_id
143
+ )
144
+ except Exception as e:
145
+ log.debug(f"[nova_proxy] uncertainty check skipped: {e}")
146
+
147
+ # 6. Voice humanization
148
+ if apply_voice and self._voice and response:
149
+ try:
150
+ response = self._voice.humanize(response)
151
+ meta["voice_applied"] = True
152
+ except Exception:
153
+ pass
154
+
155
+ meta["total_ms"] = int((time.time() - start) * 1000)
156
+ return response, meta
157
+
158
+ def _generate_safe_fallback(self, reason: str) -> str:
159
+ """Generate a safe response when Nova blocks the original."""
160
+ if "medical" in reason.lower() or "diagnos" in reason.lower():
161
+ return "Para esa consulta específica te recomiendo hablar directamente con el especialista. Quieres que te ayude a agendar?"
162
+ if "price" in reason.lower() or "precio" in reason.lower():
163
+ return "Los precios dependen de la valoración personalizada. Te agendo una cita para que te den toda la información?"
164
+ return "Déjame verificar eso y te confirmo. Mientras tanto, hay algo más en lo que te pueda ayudar?"
165
+
166
+ def get_stats(self) -> Dict[str, int]:
167
+ return {
168
+ "total_calls": self._call_count,
169
+ "blocked": self._blocked_count,
170
+ }
@@ -0,0 +1,493 @@
1
+ """
2
+ conny_nuke_robot_phrases.py
3
+ ════════════════════════════════════════════════════════════════════════════════
4
+ ELIMINA LA LISTA _robot_phrases DE _postprocess — Patch de runtime v1.0
5
+ ════════════════════════════════════════════════════════════════════════════════
6
+
7
+ POR QUÉ ESTO CORTA LAS RESPUESTAS:
8
+ _postprocess() tiene una lista de 60+ frases que se eliminan por string
9
+ replace() sobre la respuesta ya generada. El problema:
10
+
11
+ LLM genera → "Listo, ahora soy más amigable ||| En qué más puedo ayudarte"
12
+ robot_filter borra → "En qué más puedo ayudarte"
13
+ queda → "Listo, ahora soy más amigable |||"
14
+ _split_bubbles filtra burbuja vacía → ["Listo, ahora soy más amigable"]
15
+ (o en casos peores, el LLM genera la segunda burbuja primero y se corta antes)
16
+
17
+ Otro caso:
18
+ LLM genera → "Entendido, volvemos ||| cuéntame"
19
+ pero si hay lag y el stream se parte → el LLM solo entregó "Entendido, vol"
20
+ sin que ningún filtro lo haya causado (eso es el stream cortado, no el filtro)
21
+
22
+ El filtro de frases fue correcto en V7 cuando el prompt no controlaba bien
23
+ el output. En V11 el system prompt ya le dice al LLM exactamente qué NO
24
+ decir antes de generarlo — el filtro postproceso es redundante y peligroso.
25
+
26
+ QUÉ HACE ESTE PATCH:
27
+ 1. Vacía _robot_phrases en runtime (no toca el archivo fuente)
28
+ 2. Solo conserva las frases que NUNCA son parte de una oración legítima
29
+ y que SÍ delatan bot aunque el prompt mejore (ver SAFE_TO_KEEP)
30
+ 3. Aplica el mismo vaciado en FORBIDDEN_HARD de AntiRobotFilter
31
+ para las frases que el filtro agresivo podría borrar
32
+
33
+ CÓMO USAR — al inicio de conny.py (después de los imports opcionales):
34
+ try:
35
+ from conny_nuke_robot_phrases import apply_patch
36
+ apply_patch()
37
+ except Exception as e:
38
+ log.warning(f"[nuke_robot] patch no aplicado: {e}")
39
+
40
+ O directamente en la función _postprocess:
41
+ # Reemplazar el bloque completo:
42
+ # _robot_phrases = [...]
43
+ # for phrase in _robot_phrases: ...
44
+ # Por:
45
+ # pass # filtro eliminado — el prompt lo maneja
46
+ ════════════════════════════════════════════════════════════════════════════════
47
+ """
48
+ from __future__ import annotations
49
+ from functools import lru_cache
50
+ import logging
51
+ import re
52
+ import sys
53
+ from typing import Any, Iterable, Tuple
54
+
55
+ log = logging.getLogger("conny.nuke_robot")
56
+
57
+ # ════════════════════════════════════════════════════════════════════════════════
58
+ # FRASES QUE SÍ SE CONSERVAN — nunca son parte de conversación legítima
59
+ # y no causan cortes porque son frases completas autónomas (al inicio o final)
60
+ # ════════════════════════════════════════════════════════════════════════════════
61
+ SAFE_TO_KEEP = {
62
+ # Delatan explícitamente que es un sistema automatizado
63
+ "como modelo de lenguaje",
64
+ "como inteligencia artificial",
65
+ "como ia,",
66
+ "soy tu asistente virtual",
67
+ "mis capacidades incluyen",
68
+ "mis limitaciones son",
69
+ # Cierres formales de email que nunca aparecen en WhatsApp real
70
+ "saludos cordiales,",
71
+ "atentamente,",
72
+ "afectuosamente,",
73
+ "sin más por el momento,",
74
+ }
75
+
76
+ EDGE_SEPARATOR_CHARS = ",;:.!?¡¿-–—"
77
+ LEADING_TRIM_CHARS = ",;:.!?-–—"
78
+ TRAILING_TRIM_CHARS = ",;:-–—"
79
+ _INTERNAL_SPACE_RE = re.compile(r"\s+")
80
+
81
+
82
+ def _normalize_phrase(phrase: str) -> str:
83
+ return phrase.strip().strip(EDGE_SEPARATOR_CHARS).strip()
84
+
85
+
86
+ def _canonical_phrases(phrases: Iterable[str]) -> Tuple[str, ...]:
87
+ seen = set()
88
+ ordered = []
89
+ for phrase in phrases:
90
+ normalized = _normalize_phrase(phrase)
91
+ if not normalized or normalized in seen:
92
+ continue
93
+ seen.add(normalized)
94
+ ordered.append(normalized)
95
+ return tuple(sorted(ordered))
96
+
97
+
98
+ EDGE_ONLY_ROBOT_PHRASES = _canonical_phrases((*SAFE_TO_KEEP, "como ia"))
99
+
100
+
101
+ @lru_cache(maxsize=None)
102
+ def _compile_phrase_pattern(phrase: str) -> re.Pattern[str]:
103
+ normalized = _normalize_phrase(phrase)
104
+ parts = [re.escape(part) for part in normalized.split()]
105
+ pattern = r"\s+".join(parts)
106
+ return re.compile(rf"(?<!\w){pattern}(?!\w)", re.IGNORECASE)
107
+
108
+
109
+ def _clean_edge_fragment(text: str, *, trim_leading: bool, trim_trailing: bool) -> str:
110
+ if trim_leading:
111
+ text = re.sub(rf"^[\s{re.escape(LEADING_TRIM_CHARS)}]+", "", text)
112
+ if trim_trailing:
113
+ text = re.sub(rf"[\s{re.escape(TRAILING_TRIM_CHARS)}]+$", "", text)
114
+ text = _INTERNAL_SPACE_RE.sub(" ", text).strip()
115
+ return text
116
+
117
+
118
+ def _capitalize_first_alpha(text: str) -> str:
119
+ for index, char in enumerate(text):
120
+ if char.isalpha():
121
+ return text[:index] + char.upper() + text[index + 1 :]
122
+ return text
123
+
124
+
125
+ def _strip_phrase_from_bubble(bubble: str, phrases: Tuple[str, ...]) -> Tuple[str, bool]:
126
+ cleaned = bubble.strip()
127
+ changed = False
128
+
129
+ while cleaned:
130
+ removed = False
131
+ for phrase in phrases:
132
+ match = _compile_phrase_pattern(phrase).search(cleaned)
133
+ if not match:
134
+ continue
135
+
136
+ before = cleaned[:match.start()].rstrip()
137
+ after = cleaned[match.end():].lstrip()
138
+ at_start = not before and (not after or after[0] in EDGE_SEPARATOR_CHARS)
139
+ at_end = not after and (not before or before[-1] in EDGE_SEPARATOR_CHARS)
140
+
141
+ if at_start:
142
+ cleaned = _capitalize_first_alpha(
143
+ _clean_edge_fragment(after, trim_leading=True, trim_trailing=False)
144
+ )
145
+ changed = True
146
+ removed = True
147
+ break
148
+
149
+ if at_end:
150
+ cleaned = _clean_edge_fragment(before, trim_leading=False, trim_trailing=True)
151
+ changed = True
152
+ removed = True
153
+ break
154
+
155
+ if not removed:
156
+ break
157
+
158
+ return cleaned, changed
159
+
160
+
161
+ def strip_robot_phrases(text: str, phrases: Iterable[str] | None = None) -> str:
162
+ """
163
+ Remueve solo frases robóticas completas en borde de burbuja o de texto.
164
+ Nunca corta frases mid-sentence ni vocabulario normal.
165
+ """
166
+ if not text:
167
+ return text
168
+
169
+ active_phrases = _canonical_phrases(phrases or EDGE_ONLY_ROBOT_PHRASES)
170
+ bubbles = [bubble.strip() for bubble in re.split(r"\s*\|\|\|\s*", text) if bubble.strip()]
171
+ cleaned_bubbles = []
172
+
173
+ for bubble in bubbles:
174
+ cleaned, _ = _strip_phrase_from_bubble(bubble, active_phrases)
175
+ if cleaned:
176
+ cleaned_bubbles.append(cleaned)
177
+
178
+ return " ||| ".join(cleaned_bubbles)
179
+
180
+ # ════════════════════════════════════════════════════════════════════════════════
181
+ # TODAS LAS FRASES ORIGINALES QUE SE ELIMINAN
182
+ # (para referencia y para el patch de archivos)
183
+ # ════════════════════════════════════════════════════════════════════════════════
184
+ ORIGINAL_ROBOT_PHRASES_LINES = """ "Con mucho gusto", "con mucho gusto",
185
+ "Encantada de conocerte", "encantada de conocerte",
186
+ "Encantado de conocerte", "encantado de conocerte",
187
+ "Es un placer atenderte", "es un placer atenderte",
188
+ "Fue un placer", "fue un placer",
189
+ "En qué más le puedo servir", "en qué más le puedo servir",
190
+ "En qué más puedo ayudarte", "en qué más puedo ayudarte",
191
+ "Estoy aquí para ayudarte", "estoy aquí para ayudarte",
192
+ "Por supuesto,", "por supuesto,",
193
+ "¡Por supuesto!", "¡por supuesto!",
194
+ "Definitivamente", "definitivamente",
195
+ "Absolutamente", "absolutamente",
196
+ " — ", " —",
197
+ # Relleno colombiano
198
+ "Claro que sí,", "claro que sí,",
199
+ "Claro que si,", "claro que si,",
200
+ "Con gusto te ayudo", "con gusto te ayudo",
201
+ "Con gusto te cuento", "con gusto te cuento",
202
+ "Me alegra que preguntes", "me alegra que preguntes",
203
+ "Perfecto, entiendo", "perfecto, entiendo",
204
+ "Te cuento que", "te cuento que",
205
+ "Lo que pasa es que", "lo que pasa es que",
206
+ "En ese sentido,", "en ese sentido,",
207
+ "De hecho,", "de hecho,",
208
+ "Con todo gusto", "con todo gusto",
209
+ "Claro, con gusto", "claro, con gusto",
210
+ # Frases IA/chatbot que delatan que es un bot
211
+ "Como asistente virtual", "como asistente virtual",
212
+ "No tengo emociones", "no tengo emociones",
213
+ "No te preocupes", "no te preocupes",
214
+ "Mi programación", "mi programación",
215
+ "He procesado tu consulta", "he procesado tu consulta",
216
+ "Tu solicitud ha sido", "tu solicitud ha sido",
217
+ "Espero haber sido de ayuda", "espero haber sido de ayuda",
218
+ "No dudes en preguntar", "no dudes en preguntar",
219
+ "Estoy a tu disposición", "estoy a tu disposición",
220
+ "Quedo a tu disposición", "quedo a tu disposición",
221
+ "Cualquier consulta adicional", "cualquier consulta adicional",
222
+ "Para mayor información", "para mayor información",
223
+ "En qué te puedo ayudar", "en qué te puedo ayudar",
224
+ "En qué puedo ayudarte", "en qué puedo ayudarte",
225
+ "Cómo puedo ayudarte", "cómo puedo ayudarte",
226
+ "Hola, en qué te puedo ayudar", "hola, en qué te puedo ayudar",
227
+ "Hola, en qué puedo ayudarte", "hola, en qué puedo ayudarte",
228
+ "Cuéntame cómo puedo ayudarte", "cuéntame cómo puedo ayudarte",
229
+ "Espero tu respuesta", "espero tu respuesta",
230
+ "Sin más por el momento", "sin más por el momento",
231
+ "Saludos cordiales", "saludos cordiales",
232
+ "Atentamente", "atentamente",
233
+ "Afectuosamente", "afectuosamente",
234
+ # Muletillas de relleno formal
235
+ "En primer lugar,", "en primer lugar,",
236
+ "En segundo lugar,", "en segundo lugar,",
237
+ "Por otro lado,", "por otro lado,",
238
+ "Adicionalmente,", "adicionalmente,",
239
+ "Asimismo,", "asimismo,",
240
+ "No obstante,", "no obstante,",
241
+ "Sin embargo,", "sin embargo,",
242
+ "Cabe mencionar", "cabe mencionar",
243
+ "Cabe destacar", "cabe destacar",
244
+ "Es importante mencionar", "es importante mencionar",
245
+ "Es importante destacar", "es importante destacar",
246
+ "Quiero informarte", "quiero informarte",
247
+ "Me complace informarte", "me complace informarte",
248
+ "Nos complace", "nos complace","""
249
+
250
+ # ════════════════════════════════════════════════════════════════════════════════
251
+ # PATCH DE ARCHIVO — modifica conny.py directamente (uso offline)
252
+ # ════════════════════════════════════════════════════════════════════════════════
253
+
254
+ OLD_BLOCK = ''' # Eliminar frases robóticas que se cuelan pese al prompt
255
+ _robot_phrases = [
256
+ "Con mucho gusto", "con mucho gusto",
257
+ "Encantada de conocerte", "encantada de conocerte",
258
+ "Encantado de conocerte", "encantado de conocerte",
259
+ "Es un placer atenderte", "es un placer atenderte",
260
+ "Fue un placer", "fue un placer",
261
+ "En qué más le puedo servir", "en qué más le puedo servir",
262
+ "En qué más puedo ayudarte", "en qué más puedo ayudarte",
263
+ "Estoy aquí para ayudarte", "estoy aquí para ayudarte",
264
+ "Por supuesto,", "por supuesto,",
265
+ "¡Por supuesto!", "¡por supuesto!",
266
+ "Definitivamente", "definitivamente",
267
+ "Absolutamente", "absolutamente",
268
+ " — ", " —",
269
+ # Relleno colombiano
270
+ "Claro que sí,", "claro que sí,",
271
+ "Claro que si,", "claro que si,",
272
+ "Con gusto te ayudo", "con gusto te ayudo",
273
+ "Con gusto te cuento", "con gusto te cuento",
274
+ "Me alegra que preguntes", "me alegra que preguntes",
275
+ "Perfecto, entiendo", "perfecto, entiendo",
276
+ "Te cuento que", "te cuento que",
277
+ "Lo que pasa es que", "lo que pasa es que",
278
+ "En ese sentido,", "en ese sentido,",
279
+ "De hecho,", "de hecho,",
280
+ "Con todo gusto", "con todo gusto",
281
+ "Claro, con gusto", "claro, con gusto",
282
+ # Frases IA/chatbot que delatan que es un bot
283
+ "Como asistente virtual", "como asistente virtual",
284
+ "No tengo emociones", "no tengo emociones",
285
+ "No te preocupes", "no te preocupes",
286
+ "Mi programación", "mi programación",
287
+ "He procesado tu consulta", "he procesado tu consulta",
288
+ "Tu solicitud ha sido", "tu solicitud ha sido",
289
+ "Espero haber sido de ayuda", "espero haber sido de ayuda",
290
+ "No dudes en preguntar", "no dudes en preguntar",
291
+ "Estoy a tu disposición", "estoy a tu disposición",
292
+ "Quedo a tu disposición", "quedo a tu disposición",
293
+ "Cualquier consulta adicional", "cualquier consulta adicional",
294
+ "Para mayor información", "para mayor información",
295
+ "En qué te puedo ayudar", "en qué te puedo ayudar",
296
+ "En qué puedo ayudarte", "en qué puedo ayudarte",
297
+ "Cómo puedo ayudarte", "cómo puedo ayudarte",
298
+ "Hola, en qué te puedo ayudar", "hola, en qué te puedo ayudar",
299
+ "Hola, en qué puedo ayudarte", "hola, en qué puedo ayudarte",
300
+ "Cuéntame cómo puedo ayudarte", "cuéntame cómo puedo ayudarte",
301
+ "Espero tu respuesta", "espero tu respuesta",
302
+ "Sin más por el momento", "sin más por el momento",
303
+ "Saludos cordiales", "saludos cordiales",
304
+ "Atentamente", "atentamente",
305
+ "Afectuosamente", "afectuosamente",
306
+ # Muletillas de relleno formal
307
+ "En primer lugar,", "en primer lugar,",
308
+ "En segundo lugar,", "en segundo lugar,",
309
+ "Por otro lado,", "por otro lado,",
310
+ "Adicionalmente,", "adicionalmente,",
311
+ "Asimismo,", "asimismo,",
312
+ "No obstante,", "no obstante,",
313
+ "Sin embargo,", "sin embargo,",
314
+ "Cabe mencionar", "cabe mencionar",
315
+ "Cabe destacar", "cabe destacar",
316
+ "Es importante mencionar", "es importante mencionar",
317
+ "Es importante destacar", "es importante destacar",
318
+ "Quiero informarte", "quiero informarte",
319
+ "Me complace informarte", "me complace informarte",
320
+ "Nos complace", "nos complace",
321
+ ]
322
+ for phrase in _robot_phrases:
323
+ if phrase in response:
324
+ # Eliminar la frase y limpiar espacios dobles
325
+ response = response.replace(phrase, "").strip()
326
+ response = re.sub(r\'\\s+\', \' \', response).strip()
327
+ response = re.sub(r\'^\\s*,\\s*\', \'\', response) # quitar coma inicial'''
328
+
329
+ NEW_BLOCK = ''' # PATCH: filtro de frases eliminado.
330
+ # El system prompt (V11 PROMPT-FIRST) le indica al LLM qué NO decir
331
+ # ANTES de generarlo. El reemplazo postproceso causaba cortes de respuesta
332
+ # porque borraba invitaciones de cierre dejando burbujas incompletas.
333
+ # Solo se conservan las señales que delatan explícitamente "soy una IA"
334
+ # y se limpian de forma quirúrgica: regex + bordes de burbuja/texto.
335
+ _robot_phrases_minimal = [
336
+ "como modelo de lenguaje",
337
+ "como inteligencia artificial",
338
+ "mis capacidades incluyen",
339
+ ]
340
+ _robot_edge_chars = ",;:.!?¡¿-–—"
341
+ for phrase in _robot_phrases_minimal:
342
+ _phrase_pattern = r"(?<!\\\\w)" + r"\\\\s+".join(re.escape(part) for part in phrase.split()) + r"(?!\\\\w)"
343
+ _match = re.search(_phrase_pattern, response, re.IGNORECASE)
344
+ if not _match:
345
+ continue
346
+ _before = response[:_match.start()].rstrip()
347
+ _after = response[_match.end():].lstrip()
348
+ _at_start = not _before and (not _after or _after[:1] in _robot_edge_chars)
349
+ _at_end = not _after and (not _before or _before[-1:] in _robot_edge_chars)
350
+ if not (_at_start or _at_end):
351
+ continue
352
+ if _at_start:
353
+ response = re.sub(rf"^[\\\\s{re.escape(_robot_edge_chars)}]+", "", _after)
354
+ else:
355
+ response = re.sub(rf"[\\\\s{re.escape(_robot_edge_chars)}]+$", "", _before)
356
+ response = re.sub(r\'\\\\s+\', \' \', response).strip()'''
357
+
358
+
359
+ def patch_file(filepath: str) -> bool:
360
+ """
361
+ Aplica el patch directamente al archivo conny.py.
362
+ Reemplaza el bloque _robot_phrases completo por la versión mínima.
363
+
364
+ Usar offline antes de deployar:
365
+ python3 -c "from conny_nuke_robot_phrases import patch_file; patch_file('conny.py')"
366
+ """
367
+ try:
368
+ with open(filepath, "r", encoding="utf-8") as f:
369
+ content = f.read()
370
+
371
+ if OLD_BLOCK not in content:
372
+ log.warning(f"[nuke_robot] bloque _robot_phrases no encontrado en {filepath}")
373
+ log.warning("[nuke_robot] puede que ya haya sido parchado o la versión es diferente")
374
+ return False
375
+
376
+ new_content = content.replace(OLD_BLOCK, NEW_BLOCK)
377
+
378
+ with open(filepath, "w", encoding="utf-8") as f:
379
+ f.write(new_content)
380
+
381
+ log.info(f"[nuke_robot] ✅ patch aplicado a {filepath}")
382
+ return True
383
+
384
+ except Exception as e:
385
+ log.error(f"[nuke_robot] error en patch_file: {e}")
386
+ return False
387
+
388
+
389
+ # ════════════════════════════════════════════════════════════════════════════════
390
+ # PATCH DE RUNTIME — parchea la clase en memoria sin tocar el archivo
391
+ # ════════════════════════════════════════════════════════════════════════════════
392
+
393
+ def apply_patch() -> bool:
394
+ """
395
+ Parchea _postprocess en runtime buscando la clase en todos los módulos
396
+ cargados. No toca el archivo fuente.
397
+
398
+ Llama esto al inicio de conny.py:
399
+ from conny_nuke_robot_phrases import apply_patch
400
+ apply_patch()
401
+ """
402
+ patched = 0
403
+
404
+ def make_safe_postprocess(original_fn):
405
+ def safe_postprocess(self_inner, response: str, personality: Any) -> str:
406
+ result = original_fn(self_inner, response, personality)
407
+ if not isinstance(result, str):
408
+ return result
409
+
410
+ cleaned = strip_robot_phrases(result)
411
+ if cleaned.strip():
412
+ result = cleaned
413
+
414
+ if response and isinstance(response, str):
415
+ orig_words = len(response.split())
416
+ result_words = len(result.split()) if result else 0
417
+ if orig_words > 5 and result_words < orig_words * 0.4:
418
+ fallback = strip_robot_phrases(response)
419
+ log.warning(
420
+ f"[nuke_robot] _postprocess recortó demasiado "
421
+ f"({orig_words}→{result_words} words), devolviendo scrub quirúrgico"
422
+ )
423
+ return fallback or response
424
+ return result or response
425
+
426
+ return safe_postprocess
427
+
428
+ def make_safe_remove_forbidden_exact():
429
+ def safe_remove_forbidden_exact(self_inner, text: str) -> str:
430
+ phrases = getattr(self_inner, "FORBIDDEN_HARD", EDGE_ONLY_ROBOT_PHRASES)
431
+ return strip_robot_phrases(text, phrases)
432
+
433
+ return safe_remove_forbidden_exact
434
+
435
+ for mod_name, mod in list(sys.modules.items()):
436
+ if mod is None or mod_name.startswith("typing"):
437
+ continue
438
+ for attr_name in dir(mod):
439
+ try:
440
+ obj = getattr(mod, attr_name, None)
441
+ if obj is None or not isinstance(obj, type):
442
+ continue
443
+
444
+ if hasattr(obj, "_postprocess") and not getattr(obj, "_nuke_robot_postprocess_patched", False):
445
+ obj._postprocess = make_safe_postprocess(obj._postprocess)
446
+ obj._nuke_robot_postprocess_patched = True
447
+ log.info(f"[nuke_robot] runtime patch en {mod_name}.{attr_name}._postprocess ✓")
448
+ patched += 1
449
+
450
+ has_antirobot_contract = hasattr(obj, "FORBIDDEN_HARD") and hasattr(obj, "_remove_forbidden_exact")
451
+ if has_antirobot_contract and not getattr(obj, "_nuke_robot_exact_patched", False):
452
+ obj._remove_forbidden_exact = make_safe_remove_forbidden_exact()
453
+ obj._nuke_robot_exact_patched = True
454
+ log.info(f"[nuke_robot] runtime patch en {mod_name}.{attr_name}._remove_forbidden_exact ✓")
455
+ patched += 1
456
+
457
+ except Exception as exc:
458
+ log.debug(f"[nuke_robot] se omitió {mod_name}.{attr_name}: {exc}")
459
+ continue
460
+
461
+ if patched == 0:
462
+ log.warning("[nuke_robot] ninguna clase parcheada — aplicar patch_file() en su lugar")
463
+ return patched > 0
464
+
465
+
466
+ # ════════════════════════════════════════════════════════════════════════════════
467
+ # USO DIRECTO: aplicar sobre conny.py localmente
468
+ # ════════════════════════════════════════════════════════════════════════════════
469
+
470
+ if __name__ == "__main__":
471
+ import sys as _sys
472
+ import shutil as _shutil
473
+ from pathlib import Path
474
+
475
+ target = Path(_sys.argv[1]) if len(_sys.argv) > 1 else Path("conny.py")
476
+
477
+ if not target.exists():
478
+ print(f"❌ No se encontró {target}")
479
+ _sys.exit(1)
480
+
481
+ # Backup automático
482
+ backup = target.with_suffix(".py.bak_robot_phrases")
483
+ _shutil.copy2(target, backup)
484
+ print(f"📦 Backup guardado en {backup}")
485
+
486
+ success = patch_file(str(target))
487
+ if success:
488
+ print(f"✅ Patch aplicado a {target}")
489
+ print(" El filtro _robot_phrases fue reemplazado por la versión mínima.")
490
+ print(" Reinicia Conny para que tome efecto.")
491
+ else:
492
+ print(f"❌ El patch no se pudo aplicar. Revisa el log.")
493
+ print(" El backup está en:", backup)