@innvisor/conny-ai 9.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +68 -0
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
- package/brand-assets/Conny.web.logo.png +0 -0
- package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
- package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
- package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
- package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
- package/brand-assets/conny-demo/manifest.json +22 -0
- package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
- package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
- package/brand-assets/conny-logo.png +0 -0
- package/brand-assets/web.background.png +0 -0
- package/brand_assets.py +323 -0
- package/conny +28 -0
- package/conny-chat.py +579 -0
- package/conny-omni.py +3843 -0
- package/conny.py +113 -0
- package/conny_agents/__init__.py +1 -0
- package/conny_agents/agenda.py +1 -0
- package/conny_agents/captacion.py +1 -0
- package/conny_agents/conocimiento.py +1 -0
- package/conny_agents/escalacion.py +1 -0
- package/conny_agents/objeciones.py +1 -0
- package/conny_agents/seguimiento.py +1 -0
- package/conny_app.py +287 -0
- package/conny_audio.py +350 -0
- package/conny_audio_learn.py +84 -0
- package/conny_brain_v10.py +804 -0
- package/conny_bridge.py +656 -0
- package/conny_calendar.py +169 -0
- package/conny_cli.py +11784 -0
- package/conny_cli_bb.py +437 -0
- package/conny_commands.py +243 -0
- package/conny_config.py +215 -0
- package/conny_core/__init__.py +3 -0
- package/conny_core/conversation_engine.py +446 -0
- package/conny_core/first_turn_ops.py +287 -0
- package/conny_core/persona_registry.py +157 -0
- package/conny_core/prompt_ops.py +561 -0
- package/conny_cron.py +72 -0
- package/conny_demo_v2.py +209 -0
- package/conny_demo_voice.py +134 -0
- package/conny_design.py +43 -0
- package/conny_doctor.py +319 -0
- package/conny_domino.py +696 -0
- package/conny_generator.py +447 -0
- package/conny_google_auth.py +159 -0
- package/conny_i18n.py +619 -0
- package/conny_init.py +509 -0
- package/conny_integrations/__init__.py +4 -0
- package/conny_integrations/llm.py +1 -0
- package/conny_integrations/vault.py +77 -0
- package/conny_integrations/whatsapp.py +1 -0
- package/conny_intelligence.py +65 -0
- package/conny_learning.py +154 -0
- package/conny_memory.py +243 -0
- package/conny_memory_engine.py +292 -0
- package/conny_nova_proxy.py +170 -0
- package/conny_nuke_robot_phrases.py +493 -0
- package/conny_pairing.py +253 -0
- package/conny_patch.py +291 -0
- package/conny_persona_cli.py +150 -0
- package/conny_router.py +308 -0
- package/conny_runtime_ops.py +271 -0
- package/conny_session.py +516 -0
- package/conny_skills/__init__.py +1 -0
- package/conny_skills/demo_mode.py +35 -0
- package/conny_skills/text_processing.py +1 -0
- package/conny_skills/tone_detection.py +1 -0
- package/conny_smart_features.py +333 -0
- package/conny_studio.py +161 -0
- package/conny_sync_fix.py +306 -0
- package/conny_tui.py +512 -0
- package/conny_tui_select.py +202 -0
- package/conny_ultra_config.py +411 -0
- package/conny_uncertainty.py +174 -0
- package/conny_utils.py +87 -0
- package/conny_voice.py +156 -0
- package/conny_voice_engine.py +124 -0
- package/conny_web_search.py +66 -0
- package/conny_weekly_report.py +85 -0
- package/conny_worm.py +88 -0
- package/core/__init__.py +25 -0
- package/ecosystem.config.js +24 -0
- package/fix_init.py +27 -0
- package/install.sh +78 -0
- package/knowledge_base.py +330 -0
- package/nova/rules/default.yaml +37 -0
- package/nova_bridge.py +509 -0
- package/npm/conny.js +471 -0
- package/package.json +102 -0
- package/personas/conny/base/default.yaml +35 -0
- package/personas/conny/base/estetica_whatsapp.yaml +36 -0
- package/requirements.txt +14 -0
- package/run.sh +47 -0
- package/search.py +465 -0
- package/smart_handoff.py +1150 -0
- package/src/__init__.py +0 -0
- package/src/conny/__init__.py +0 -0
- package/src/conny/admin/__init__.py +0 -0
- package/src/conny/admin/api.py +234 -0
- package/src/conny/admin/dashboard.py +772 -0
- package/src/conny/api/__init__.py +0 -0
- package/src/conny/api/routes.py +8851 -0
- package/src/conny/brain/__init__.py +15 -0
- package/src/conny/brain/engine.py +804 -0
- package/src/conny/brain/learning.py +154 -0
- package/src/conny/brain/memory.py +324 -0
- package/src/conny/brain/smart_features.py +333 -0
- package/src/conny/brain/uncertainty.py +167 -0
- package/src/conny/channels/__init__.py +0 -0
- package/src/conny/channels/audio.py +316 -0
- package/src/conny/channels/cli.py +11795 -0
- package/src/conny/channels/logo_art.py +11 -0
- package/src/conny/channels/voice.py +156 -0
- package/src/conny/core/__init__.py +0 -0
- package/src/conny/core/config.py +215 -0
- package/src/conny/core/cron.py +72 -0
- package/src/conny/core/messenger.py +563 -0
- package/src/conny/core/router.py +297 -0
- package/src/conny/core/session.py +312 -0
- package/src/conny/demo/__init__.py +0 -0
- package/src/conny/demo/handler.py +3110 -0
- package/src/conny/integrations/__init__.py +19 -0
- package/src/conny/integrations/calendar.py +169 -0
- package/src/conny/integrations/knowledge.py +312 -0
- package/src/conny/integrations/search.py +66 -0
- package/src/conny/personas/__init__.py +0 -0
- package/src/conny/personas/generator.py +447 -0
- package/src/conny/production/__init__.py +0 -0
- package/src/conny/production/domino.py +696 -0
- package/src/conny/production/guard.py +550 -0
- package/src/conny/production/handoff.py +1150 -0
- package/src/conny/production/monitor.py +353 -0
- package/src/conny/utils/__init__.py +2 -0
- package/src/conny/utils/helpers.py +75 -0
- package/src/conny/utils/i18n.py +619 -0
- package/src/core/admin_engines.py +772 -0
- package/src/core/globals.py +11845 -0
- package/src/core/orchestrator.py +273 -0
- package/src/core/production_monitor.py +353 -0
- package/src/core/runtime.py +5487 -0
- package/src/domain/onboarding_flow.py +230 -0
- package/src/domain/prompts/__init__.py +1 -0
- package/src/domain/prompts/prospect_pitch.py +282 -0
- package/src/domain/send_guard.py +636 -0
- package/src/domain/swarm/queen.py +96 -0
- package/src/infrastructure/llm_providers/engine.py +487 -0
- package/src/interfaces/mcp_server.py +73 -0
- package/src/interfaces/nova_bridge.py +58 -0
- package/src/interfaces/web/admin_api.py +1379 -0
- package/src/interfaces/web/app.py +9408 -0
- package/src/interfaces/web/demo_handler.py +3450 -0
- package/src/interfaces/web/static/generate_avatars.py +46 -0
- package/v7/__init__.py +46 -0
- package/v7/agents/__init__.py +46 -0
- package/v7/agents/agenda.py +77 -0
- package/v7/agents/base.py +216 -0
- package/v7/agents/captacion.py +60 -0
- package/v7/agents/conocimiento.py +69 -0
- package/v7/agents/escalacion.py +83 -0
- package/v7/agents/objeciones.py +109 -0
- package/v7/agents/seguimiento.py +71 -0
- package/v7/memory/__init__.py +46 -0
- package/v7/memory/patient_profile.py +200 -0
- package/v7/orchestrator.py +275 -0
- package/v7/postprocess.py +127 -0
- package/v7/router.py +239 -0
- package/verify_conversation_impl.py +48 -0
|
@@ -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)
|