@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
package/conny_doctor.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""conny_doctor.py — health check + self-healing for Conny runtime."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from conny_runtime_ops import (
|
|
17
|
+
detect_tunnel_processes,
|
|
18
|
+
find_pm2_processes,
|
|
19
|
+
health_payload,
|
|
20
|
+
instance_runtime_info,
|
|
21
|
+
port_is_open,
|
|
22
|
+
python_candidates,
|
|
23
|
+
resolve_python,
|
|
24
|
+
telegram_webhook_info,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def color(text: str, code: str) -> str:
|
|
29
|
+
return f"\033[{code}m{text}\033[0m"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def green(text: str) -> str:
|
|
33
|
+
return color(text, "32")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def yellow(text: str) -> str:
|
|
37
|
+
return color(text, "33")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def red(text: str) -> str:
|
|
41
|
+
return color(text, "31")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def dim(text: str) -> str:
|
|
45
|
+
return color(text, "90")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def bold(text: str) -> str:
|
|
49
|
+
return color(text, "1")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HealthCheck:
|
|
53
|
+
def __init__(self, name: str, status: str, message: str = "", remedy: str = ""):
|
|
54
|
+
self.name = name
|
|
55
|
+
self.status = status
|
|
56
|
+
self.message = message
|
|
57
|
+
self.remedy = remedy
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> Dict[str, str]:
|
|
60
|
+
return {
|
|
61
|
+
"name": self.name,
|
|
62
|
+
"status": self.status,
|
|
63
|
+
"message": self.message,
|
|
64
|
+
"remedy": self.remedy,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
icon = {"ok": green("✓"), "warning": yellow("⚠"), "error": red("✗")}[self.status]
|
|
69
|
+
message = f" ({self.message})" if self.message else ""
|
|
70
|
+
return f" {icon} {self.name}{dim(message)}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ConnyDoctor:
|
|
74
|
+
def __init__(self, instance_id: str):
|
|
75
|
+
self.instance_id = instance_id or "base"
|
|
76
|
+
self.info = instance_runtime_info(self.instance_id)
|
|
77
|
+
self.checks: List[HealthCheck] = []
|
|
78
|
+
self.health: Dict[str, Any] = {}
|
|
79
|
+
self.webhook: Dict[str, Any] = {}
|
|
80
|
+
self.tunnels: List[Dict[str, Any]] = []
|
|
81
|
+
self.python: Optional[Dict[str, str]] = None
|
|
82
|
+
|
|
83
|
+
async def run_all_checks(self) -> List[HealthCheck]:
|
|
84
|
+
self.checks = []
|
|
85
|
+
self.health = health_payload(self.info["port"]) or {}
|
|
86
|
+
self.webhook = telegram_webhook_info(self.info["telegram_token"]) if self.info["platform"] == "telegram" else {}
|
|
87
|
+
self.tunnels = detect_tunnel_processes()
|
|
88
|
+
self.python = resolve_python(self.info["name"])
|
|
89
|
+
await asyncio.gather(
|
|
90
|
+
self._check_pm2(),
|
|
91
|
+
self._check_api_health(),
|
|
92
|
+
self._check_runtime_python(),
|
|
93
|
+
self._check_runtime_dependencies(),
|
|
94
|
+
self._check_tunnel_alignment(),
|
|
95
|
+
self._check_webhook(),
|
|
96
|
+
self._check_memory_files(),
|
|
97
|
+
)
|
|
98
|
+
return self.checks
|
|
99
|
+
|
|
100
|
+
async def _check_pm2(self) -> None:
|
|
101
|
+
processes = find_pm2_processes(self.info["name"])
|
|
102
|
+
if not processes:
|
|
103
|
+
self.checks.append(HealthCheck("PM2 instance", "error", "no registrada", f"pm2 start {self.info['root']}/run.sh --name {self.info['pm2_name']}"))
|
|
104
|
+
return
|
|
105
|
+
proc = processes[0]
|
|
106
|
+
status = proc.get("pm2_env", {}).get("status", "unknown")
|
|
107
|
+
if status == "online":
|
|
108
|
+
self.checks.append(HealthCheck("PM2 instance", "ok", f"{self.info['pm2_name']} online"))
|
|
109
|
+
else:
|
|
110
|
+
self.checks.append(HealthCheck("PM2 instance", "error", f"estado {status}", f"pm2 restart {self.info['pm2_name']}"))
|
|
111
|
+
|
|
112
|
+
async def _check_api_health(self) -> None:
|
|
113
|
+
if self.health and self.health.get("status") == "online":
|
|
114
|
+
self.checks.append(HealthCheck("API health", "ok", f"v{self.health.get('version', '?')}"))
|
|
115
|
+
return
|
|
116
|
+
if port_is_open(self.info["port"]):
|
|
117
|
+
self.checks.append(HealthCheck("API health", "warning", f"puerto {self.info['port']} abierto pero /health no responde"))
|
|
118
|
+
else:
|
|
119
|
+
self.checks.append(HealthCheck("API health", "error", f"sin respuesta en :{self.info['port']}", f"pm2 restart {self.info['pm2_name']}"))
|
|
120
|
+
|
|
121
|
+
async def _check_runtime_python(self) -> None:
|
|
122
|
+
if self.python:
|
|
123
|
+
self.checks.append(HealthCheck("Python runtime", "ok", self.python["path"]))
|
|
124
|
+
else:
|
|
125
|
+
self.checks.append(HealthCheck("Python runtime", "error", "no encontré intérprete válido", "conny config → Environment & Path Tuning"))
|
|
126
|
+
|
|
127
|
+
async def _check_runtime_dependencies(self) -> None:
|
|
128
|
+
if not self.python:
|
|
129
|
+
self.checks.append(HealthCheck("Runtime deps", "error", "sin python disponible"))
|
|
130
|
+
return
|
|
131
|
+
probe = subprocess.run(
|
|
132
|
+
[
|
|
133
|
+
self.python["path"],
|
|
134
|
+
"-c",
|
|
135
|
+
"import fastapi,httpx,dotenv;print('ok')",
|
|
136
|
+
],
|
|
137
|
+
capture_output=True,
|
|
138
|
+
text=True,
|
|
139
|
+
timeout=20,
|
|
140
|
+
check=False,
|
|
141
|
+
)
|
|
142
|
+
if probe.returncode == 0:
|
|
143
|
+
self.checks.append(HealthCheck("Runtime deps", "ok", "fastapi/httpx/dotenv presentes"))
|
|
144
|
+
else:
|
|
145
|
+
err = probe.stderr.strip().splitlines()[-1] if probe.stderr.strip() else "faltan dependencias"
|
|
146
|
+
self.checks.append(HealthCheck("Runtime deps", "error", err, "doctor --fix reinstala requirements"))
|
|
147
|
+
|
|
148
|
+
async def _check_tunnel_alignment(self) -> None:
|
|
149
|
+
if not self.tunnels:
|
|
150
|
+
self.checks.append(HealthCheck("Tunnel routing", "warning", "sin túneles detectados"))
|
|
151
|
+
return
|
|
152
|
+
target_port = self.info["port"]
|
|
153
|
+
matching = [t for t in self.tunnels if target_port in (t.get("ports") or [])]
|
|
154
|
+
if matching:
|
|
155
|
+
self.checks.append(HealthCheck("Tunnel routing", "ok", f"al menos un túnel apunta a :{target_port}"))
|
|
156
|
+
return
|
|
157
|
+
ports = sorted({p for tunnel in self.tunnels for p in tunnel.get("ports", [])})
|
|
158
|
+
detail = ", ".join(str(p) for p in ports) or "sin puertos parseados"
|
|
159
|
+
self.checks.append(HealthCheck("Tunnel routing", "error", f"túneles apuntan a [{detail}] y no a :{target_port}", "conny config → Network Management"))
|
|
160
|
+
|
|
161
|
+
async def _check_webhook(self) -> None:
|
|
162
|
+
if self.info["platform"] != "telegram":
|
|
163
|
+
self.checks.append(HealthCheck("Webhook", "warning", "plataforma no Telegram"))
|
|
164
|
+
return
|
|
165
|
+
base_url = self.info["base_url"]
|
|
166
|
+
secret = self.info["webhook_secret"]
|
|
167
|
+
token = self.info["telegram_token"]
|
|
168
|
+
if not base_url or not secret or not token:
|
|
169
|
+
self.checks.append(HealthCheck("Webhook", "warning", "faltan BASE_URL / TELEGRAM_TOKEN / WEBHOOK_SECRET"))
|
|
170
|
+
return
|
|
171
|
+
expected = f"{base_url.rstrip('/')}/webhook/{secret}"
|
|
172
|
+
current = self.webhook.get("url", "")
|
|
173
|
+
if current == expected:
|
|
174
|
+
self.checks.append(HealthCheck("Webhook", "ok", "registrado correctamente"))
|
|
175
|
+
elif current:
|
|
176
|
+
self.checks.append(HealthCheck("Webhook", "error", f"apunta a {current}", "conny config → Gateway & Webhooks"))
|
|
177
|
+
else:
|
|
178
|
+
self.checks.append(HealthCheck("Webhook", "error", "sin webhook registrado", "conny config → Gateway & Webhooks"))
|
|
179
|
+
|
|
180
|
+
async def _check_memory_files(self) -> None:
|
|
181
|
+
db_path = Path(self.info["env"].get("DB_PATH") or self.info["root"] / "conny_ultra.db")
|
|
182
|
+
wal_path = Path(str(db_path) + "-wal")
|
|
183
|
+
if db_path.exists():
|
|
184
|
+
msg = f"{db_path.name} presente"
|
|
185
|
+
if wal_path.exists() and wal_path.stat().st_size > 64 * 1024 * 1024:
|
|
186
|
+
self.checks.append(HealthCheck("Memory/DB", "warning", f"WAL grande ({wal_path.stat().st_size // (1024*1024)}MB)"))
|
|
187
|
+
else:
|
|
188
|
+
self.checks.append(HealthCheck("Memory/DB", "ok", msg))
|
|
189
|
+
else:
|
|
190
|
+
self.checks.append(HealthCheck("Memory/DB", "warning", "base de datos aún no creada"))
|
|
191
|
+
|
|
192
|
+
def print_report(self) -> None:
|
|
193
|
+
print()
|
|
194
|
+
print(bold(f"Conny Doctor — {self.instance_id}"))
|
|
195
|
+
print("─" * 60)
|
|
196
|
+
for check in self.checks:
|
|
197
|
+
print(str(check))
|
|
198
|
+
if check.remedy and check.status != "ok":
|
|
199
|
+
print(f" {dim('run:')} {check.remedy}")
|
|
200
|
+
print("─" * 60)
|
|
201
|
+
ok = sum(1 for c in self.checks if c.status == "ok")
|
|
202
|
+
warn = sum(1 for c in self.checks if c.status == "warning")
|
|
203
|
+
err = sum(1 for c in self.checks if c.status == "error")
|
|
204
|
+
print(f" {green(str(ok) + ' ok')} {yellow(str(warn) + ' warnings')} {red(str(err) + ' errors')}")
|
|
205
|
+
print()
|
|
206
|
+
|
|
207
|
+
async def auto_heal(self) -> List[str]:
|
|
208
|
+
actions: List[str] = []
|
|
209
|
+
if any(c.name == "Runtime deps" and c.status == "error" for c in self.checks):
|
|
210
|
+
if self.python:
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
[
|
|
213
|
+
self.python["path"],
|
|
214
|
+
"-m",
|
|
215
|
+
"pip",
|
|
216
|
+
"install",
|
|
217
|
+
"--disable-pip-version-check",
|
|
218
|
+
"-r",
|
|
219
|
+
str(Path(self.info["root"]) / "requirements.txt"),
|
|
220
|
+
],
|
|
221
|
+
capture_output=True,
|
|
222
|
+
text=True,
|
|
223
|
+
timeout=600,
|
|
224
|
+
check=False,
|
|
225
|
+
)
|
|
226
|
+
if result.returncode == 0:
|
|
227
|
+
actions.append("requirements reinstalados")
|
|
228
|
+
if any(c.name in {"PM2 instance", "API health"} and c.status == "error" for c in self.checks):
|
|
229
|
+
run_script = Path(self.info["root"]) / "run.sh"
|
|
230
|
+
if run_script.exists():
|
|
231
|
+
run_script.chmod(run_script.stat().st_mode | 0o111)
|
|
232
|
+
subprocess.run(["pm2", "delete", self.info["pm2_name"]], capture_output=True, check=False)
|
|
233
|
+
result = subprocess.run(
|
|
234
|
+
[
|
|
235
|
+
"pm2", "start", str(run_script),
|
|
236
|
+
"--name", self.info["pm2_name"],
|
|
237
|
+
"--cwd", str(self.info["root"]),
|
|
238
|
+
"--restart-delay", "3000",
|
|
239
|
+
"--max-restarts", "10",
|
|
240
|
+
"--log", str(Path(self.info["root"]) / "logs" / "conny.log"),
|
|
241
|
+
"--error", str(Path(self.info["root"]) / "logs" / "error.log"),
|
|
242
|
+
],
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
timeout=60,
|
|
246
|
+
check=False,
|
|
247
|
+
)
|
|
248
|
+
if result.returncode == 0:
|
|
249
|
+
actions.append(f"PM2 re-registrado para {self.info['pm2_name']}")
|
|
250
|
+
if any(c.name == "Webhook" and c.status == "error" for c in self.checks):
|
|
251
|
+
await self._auto_sync_webhook(actions)
|
|
252
|
+
return actions
|
|
253
|
+
|
|
254
|
+
async def _auto_sync_webhook(self, actions: List[str]) -> None:
|
|
255
|
+
base_url = self.info["base_url"]
|
|
256
|
+
secret = self.info["webhook_secret"]
|
|
257
|
+
token = self.info["telegram_token"]
|
|
258
|
+
if not base_url or not secret or not token:
|
|
259
|
+
return
|
|
260
|
+
target = f"{base_url.rstrip('/')}/webhook/{secret}"
|
|
261
|
+
try:
|
|
262
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
263
|
+
response = await client.post(
|
|
264
|
+
f"https://api.telegram.org/bot{token}/setWebhook",
|
|
265
|
+
json={"url": target},
|
|
266
|
+
)
|
|
267
|
+
payload = response.json()
|
|
268
|
+
if response.status_code == 200 and payload.get("ok"):
|
|
269
|
+
actions.append(f"Webhook resincronizado → {target}")
|
|
270
|
+
except Exception:
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
async def run_self_healing(self) -> List[str]:
|
|
274
|
+
await self.run_all_checks()
|
|
275
|
+
self.print_report()
|
|
276
|
+
actions = await self.auto_heal()
|
|
277
|
+
if actions:
|
|
278
|
+
print(bold("Auto-heal actions"))
|
|
279
|
+
for action in actions:
|
|
280
|
+
print(f" {green('→')} {action}")
|
|
281
|
+
print()
|
|
282
|
+
await asyncio.sleep(2)
|
|
283
|
+
await self.run_all_checks()
|
|
284
|
+
self.print_report()
|
|
285
|
+
return actions
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def main() -> None:
|
|
289
|
+
import argparse
|
|
290
|
+
|
|
291
|
+
parser = argparse.ArgumentParser(prog="conny doctor", description="Health check and self-heal for Conny instances")
|
|
292
|
+
parser.add_argument("instance", nargs="?", default="base", help="Instance name")
|
|
293
|
+
parser.add_argument("--fix", action="store_true", help="Intentar auto-reparación")
|
|
294
|
+
parser.add_argument("--json", action="store_true", help="Salida JSON")
|
|
295
|
+
args = parser.parse_args()
|
|
296
|
+
|
|
297
|
+
doctor = ConnyDoctor(args.instance)
|
|
298
|
+
await doctor.run_all_checks()
|
|
299
|
+
|
|
300
|
+
if args.fix:
|
|
301
|
+
actions = await doctor.auto_heal()
|
|
302
|
+
await asyncio.sleep(1)
|
|
303
|
+
await doctor.run_all_checks()
|
|
304
|
+
else:
|
|
305
|
+
actions = []
|
|
306
|
+
|
|
307
|
+
if args.json:
|
|
308
|
+
print(json.dumps({"checks": [c.to_dict() for c in doctor.checks], "actions": actions}, ensure_ascii=False, indent=2))
|
|
309
|
+
else:
|
|
310
|
+
doctor.print_report()
|
|
311
|
+
if actions:
|
|
312
|
+
print(bold("Auto-heal actions"))
|
|
313
|
+
for action in actions:
|
|
314
|
+
print(f" {green('→')} {action}")
|
|
315
|
+
print()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
asyncio.run(main())
|