@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,202 @@
|
|
|
1
|
+
"""conny_tui_select.py — Arrow-key interactive selection menu (Nova-style)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import getpass
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import shutil
|
|
8
|
+
import select as _select
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
IS_WINDOWS = sys.platform.startswith("win")
|
|
12
|
+
if not IS_WINDOWS:
|
|
13
|
+
import termios
|
|
14
|
+
import tty
|
|
15
|
+
else:
|
|
16
|
+
import msvcrt
|
|
17
|
+
|
|
18
|
+
# Brand colors
|
|
19
|
+
PURPLE = "\033[38;5;141m"
|
|
20
|
+
MAGENTA = "\033[38;5;213m"
|
|
21
|
+
CYAN = "\033[38;5;159m"
|
|
22
|
+
DIM = "\033[38;5;242m"
|
|
23
|
+
BOLD = "\033[1m"
|
|
24
|
+
RESET = "\033[0m"
|
|
25
|
+
GREEN = "\033[38;5;114m"
|
|
26
|
+
WHITE = "\033[38;5;231m"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_tty() -> bool:
|
|
30
|
+
try:
|
|
31
|
+
return bool(sys.stdin.isatty()) and bool(sys.stdout.isatty())
|
|
32
|
+
except Exception:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _term_width() -> int:
|
|
37
|
+
return shutil.get_terminal_size((80, 24)).columns
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def select_menu(
|
|
41
|
+
options: List[str],
|
|
42
|
+
*,
|
|
43
|
+
title: str = "",
|
|
44
|
+
descriptions: Optional[List[str]] = None,
|
|
45
|
+
default: int = 0,
|
|
46
|
+
) -> int:
|
|
47
|
+
"""Interactive menu with arrow key navigation. Returns selected index."""
|
|
48
|
+
if not options:
|
|
49
|
+
return 0
|
|
50
|
+
if not is_tty():
|
|
51
|
+
return _fallback(options, title=title, default=default)
|
|
52
|
+
|
|
53
|
+
descriptions = descriptions or []
|
|
54
|
+
current = max(0, min(default, len(options) - 1))
|
|
55
|
+
line_count = 0
|
|
56
|
+
|
|
57
|
+
def draw(first=False):
|
|
58
|
+
nonlocal line_count
|
|
59
|
+
out = []
|
|
60
|
+
if not first and line_count:
|
|
61
|
+
out.append(f"\033[{line_count}F")
|
|
62
|
+
for _ in range(line_count):
|
|
63
|
+
out.append("\033[2K\033[1E")
|
|
64
|
+
out.append(f"\033[{line_count}F")
|
|
65
|
+
|
|
66
|
+
lc = 0
|
|
67
|
+
if title:
|
|
68
|
+
out.append(f"\n {CYAN}{BOLD}{title}{RESET}\n")
|
|
69
|
+
lc += 2
|
|
70
|
+
|
|
71
|
+
for i, opt in enumerate(options):
|
|
72
|
+
if i == current:
|
|
73
|
+
out.append(f" {MAGENTA}▶ {WHITE}{BOLD}{opt}{RESET}\n")
|
|
74
|
+
else:
|
|
75
|
+
out.append(f" {DIM}{opt}{RESET}\n")
|
|
76
|
+
lc += 1
|
|
77
|
+
if i < len(descriptions) and descriptions[i]:
|
|
78
|
+
out.append(f" {DIM}{descriptions[i][:60]}{RESET}\n")
|
|
79
|
+
lc += 1
|
|
80
|
+
|
|
81
|
+
out.append(f"\n {DIM}↑/↓ select · Enter confirm{RESET}\n")
|
|
82
|
+
lc += 2
|
|
83
|
+
line_count = lc
|
|
84
|
+
sys.stdout.write("".join(out))
|
|
85
|
+
sys.stdout.flush()
|
|
86
|
+
|
|
87
|
+
def read_key_unix():
|
|
88
|
+
fd = sys.stdin.fileno()
|
|
89
|
+
old = termios.tcgetattr(fd)
|
|
90
|
+
tty.setraw(fd)
|
|
91
|
+
try:
|
|
92
|
+
ch = os.read(fd, 1)
|
|
93
|
+
if ch in (b"\r", b"\n"):
|
|
94
|
+
termios.tcflush(fd, termios.TCIFLUSH)
|
|
95
|
+
return "ENTER"
|
|
96
|
+
if ch == b"\x03":
|
|
97
|
+
return "CTRL_C"
|
|
98
|
+
if ch == b"\x1b":
|
|
99
|
+
ready, _, _ = _select.select([fd], [], [], 0.05)
|
|
100
|
+
if not ready:
|
|
101
|
+
return "ESC"
|
|
102
|
+
ch2 = os.read(fd, 1)
|
|
103
|
+
if ch2 == b"[":
|
|
104
|
+
ready2, _, _ = _select.select([fd], [], [], 0.05)
|
|
105
|
+
if not ready2:
|
|
106
|
+
return "["
|
|
107
|
+
ch3 = os.read(fd, 1)
|
|
108
|
+
if ch3 == b"A": return "UP"
|
|
109
|
+
if ch3 == b"B": return "DOWN"
|
|
110
|
+
return ""
|
|
111
|
+
return ch.decode(errors="ignore")
|
|
112
|
+
finally:
|
|
113
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
114
|
+
|
|
115
|
+
def read_key_win():
|
|
116
|
+
while True:
|
|
117
|
+
if msvcrt.kbhit():
|
|
118
|
+
ch = msvcrt.getch()
|
|
119
|
+
if ch in (b"\r", b"\n"):
|
|
120
|
+
return "ENTER"
|
|
121
|
+
if ch == b"\x03":
|
|
122
|
+
return "CTRL_C"
|
|
123
|
+
if ch in (b"\x00", b"\xe0"):
|
|
124
|
+
ch2 = msvcrt.getch()
|
|
125
|
+
if ch2 == b"H": return "UP"
|
|
126
|
+
if ch2 == b"P": return "DOWN"
|
|
127
|
+
return ch.decode(errors="ignore")
|
|
128
|
+
|
|
129
|
+
read_key = read_key_win if IS_WINDOWS else read_key_unix
|
|
130
|
+
|
|
131
|
+
draw(first=True)
|
|
132
|
+
while True:
|
|
133
|
+
key = read_key()
|
|
134
|
+
if key == "ENTER":
|
|
135
|
+
sys.stdout.write("\n")
|
|
136
|
+
return current
|
|
137
|
+
if key == "CTRL_C":
|
|
138
|
+
sys.stdout.write("\n")
|
|
139
|
+
raise KeyboardInterrupt
|
|
140
|
+
if key == "UP" and current > 0:
|
|
141
|
+
current -= 1; draw()
|
|
142
|
+
elif key == "DOWN" and current < len(options) - 1:
|
|
143
|
+
current += 1; draw()
|
|
144
|
+
elif key.isdigit():
|
|
145
|
+
idx = int(key) - 1
|
|
146
|
+
if 0 <= idx < len(options):
|
|
147
|
+
current = idx; draw()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _fallback(options, title="", default=0):
|
|
151
|
+
if title:
|
|
152
|
+
print(f"\n {title}")
|
|
153
|
+
for i, opt in enumerate(options, 1):
|
|
154
|
+
marker = "▸" if i - 1 == default else " "
|
|
155
|
+
print(f" {marker} {i}. {opt}")
|
|
156
|
+
try:
|
|
157
|
+
ans = input("\n → ")
|
|
158
|
+
except (EOFError, KeyboardInterrupt):
|
|
159
|
+
return default
|
|
160
|
+
if ans.isdigit() and 0 < int(ans) <= len(options):
|
|
161
|
+
return int(ans) - 1
|
|
162
|
+
return default
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def confirm(text: str, default: bool = True) -> bool:
|
|
166
|
+
"""Y/N as arrow-key selector (never disappears)."""
|
|
167
|
+
if not is_tty():
|
|
168
|
+
try:
|
|
169
|
+
ans = input(f" {text} (y/n) ")
|
|
170
|
+
return ans.lower() in ("y", "yes", "s", "si", "sí", "")
|
|
171
|
+
except: return default
|
|
172
|
+
|
|
173
|
+
options = ["Yes", "No"] if default else ["No", "Yes"]
|
|
174
|
+
idx = select_menu(options, title=f" {text}", default=0)
|
|
175
|
+
if default:
|
|
176
|
+
return idx == 0
|
|
177
|
+
else:
|
|
178
|
+
return idx == 1
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def text_input(label: str, default: str = "", required: bool = True, is_password: bool = False) -> str:
|
|
182
|
+
"""Text input that stays visible."""
|
|
183
|
+
suffix = f" {DIM}(default: {default}){RESET}" if default and not is_password else ""
|
|
184
|
+
while True:
|
|
185
|
+
try:
|
|
186
|
+
sys.stdout.write(f" {MAGENTA}▶{RESET} {WHITE}{BOLD}{label}{RESET}{suffix}: ")
|
|
187
|
+
sys.stdout.flush()
|
|
188
|
+
if is_password:
|
|
189
|
+
val = getpass.getpass(prompt="")
|
|
190
|
+
else:
|
|
191
|
+
val = sys.stdin.readline().strip()
|
|
192
|
+
except (EOFError, KeyboardInterrupt):
|
|
193
|
+
print()
|
|
194
|
+
return default
|
|
195
|
+
if val:
|
|
196
|
+
return val
|
|
197
|
+
if default:
|
|
198
|
+
return default
|
|
199
|
+
if not required:
|
|
200
|
+
return ""
|
|
201
|
+
sys.stdout.write(f" {DIM}(required){RESET}\n")
|
|
202
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CONNY ULTRA CONFIG v9.7.0 — interactive runtime control panel."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Tuple
|
|
12
|
+
|
|
13
|
+
from conny_runtime_ops import (
|
|
14
|
+
detect_tunnel_processes,
|
|
15
|
+
find_pm2_processes,
|
|
16
|
+
health_payload,
|
|
17
|
+
instance_runtime_info,
|
|
18
|
+
port_is_open,
|
|
19
|
+
python_candidates,
|
|
20
|
+
resolve_python,
|
|
21
|
+
rewrite_tunnel_command_port,
|
|
22
|
+
telegram_webhook_info,
|
|
23
|
+
write_env_value,
|
|
24
|
+
)
|
|
25
|
+
from conny_tui_select import (
|
|
26
|
+
BOLD,
|
|
27
|
+
CYAN,
|
|
28
|
+
DIM,
|
|
29
|
+
GREEN,
|
|
30
|
+
MAGENTA,
|
|
31
|
+
RESET,
|
|
32
|
+
WHITE,
|
|
33
|
+
confirm,
|
|
34
|
+
select_menu,
|
|
35
|
+
text_input,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def clear_screen() -> None:
|
|
40
|
+
print("\033[H\033[J", end="")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def wait_for_enter() -> None:
|
|
44
|
+
input(f"\n{DIM}[Presiona Enter para continuar]{RESET}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _mask(value: str) -> str:
|
|
48
|
+
value = str(value or "").strip()
|
|
49
|
+
if not value:
|
|
50
|
+
return "vacía"
|
|
51
|
+
if len(value) <= 8:
|
|
52
|
+
return "*" * len(value)
|
|
53
|
+
return value[:4] + "…" + value[-4:]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _load_state(instance_name: str) -> Dict[str, Any]:
|
|
57
|
+
info = instance_runtime_info(instance_name)
|
|
58
|
+
health = health_payload(info["port"]) or {}
|
|
59
|
+
pm2_rows = find_pm2_processes(info["name"])
|
|
60
|
+
python = resolve_python(info["name"])
|
|
61
|
+
webhook = telegram_webhook_info(info["telegram_token"]) if info["platform"] == "telegram" else {}
|
|
62
|
+
tunnels = detect_tunnel_processes()
|
|
63
|
+
return {
|
|
64
|
+
"info": info,
|
|
65
|
+
"health": health,
|
|
66
|
+
"pm2": pm2_rows,
|
|
67
|
+
"python": python,
|
|
68
|
+
"webhook": webhook,
|
|
69
|
+
"tunnels": tunnels,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _render_header(instance_name: str, subtitle: str) -> None:
|
|
74
|
+
clear_screen()
|
|
75
|
+
print(f"┌{'─'*68}┐")
|
|
76
|
+
print(f"│{BOLD} CONNY ULTRA CONFIG v9.7.0 {RESET}│")
|
|
77
|
+
print(f"├{'─'*68}┤")
|
|
78
|
+
print(f"│ {WHITE}{BOLD}Instancia:{RESET} {instance_name or 'base':<57}│")
|
|
79
|
+
print(f"│ {CYAN}{subtitle:<66}{RESET}│")
|
|
80
|
+
print(f"└{'─'*68}┘")
|
|
81
|
+
print()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _provider_env_keys() -> List[Tuple[str, str]]:
|
|
85
|
+
return [
|
|
86
|
+
("GEMINI_API_KEY", "Gemini 1"),
|
|
87
|
+
("GEMINI_API_KEY_2", "Gemini 2"),
|
|
88
|
+
("GEMINI_API_KEY_3", "Gemini 3"),
|
|
89
|
+
("GEMINI_API_KEY_4", "Gemini 4"),
|
|
90
|
+
("GEMINI_API_KEY_5", "Gemini 5"),
|
|
91
|
+
("GEMINI_API_KEY_6", "Gemini 6"),
|
|
92
|
+
("GEMINI_API_KEY_7", "Gemini 7"),
|
|
93
|
+
("OPENAI_API_KEY", "OpenAI"),
|
|
94
|
+
("OPENROUTER_API_KEY", "OpenRouter"),
|
|
95
|
+
("GROQ_API_KEY", "Groq"),
|
|
96
|
+
("ANTHROPIC_API_KEY", "Anthropic"),
|
|
97
|
+
("BRAVE_API_KEY", "Brave Search"),
|
|
98
|
+
("APIFY_API_KEY", "Apify"),
|
|
99
|
+
("SERP_API_KEY", "SerpAPI"),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _test_provider(provider_key: str, secret: str) -> Tuple[bool, str]:
|
|
104
|
+
provider_key = provider_key.upper()
|
|
105
|
+
secret = str(secret or "").strip()
|
|
106
|
+
if not secret:
|
|
107
|
+
return False, "sin API key"
|
|
108
|
+
try:
|
|
109
|
+
import httpx
|
|
110
|
+
except Exception:
|
|
111
|
+
return False, "httpx no disponible"
|
|
112
|
+
try:
|
|
113
|
+
with httpx.Client(timeout=12.0) as client:
|
|
114
|
+
if provider_key.startswith("GEMINI"):
|
|
115
|
+
response = client.post(
|
|
116
|
+
f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={secret}",
|
|
117
|
+
json={"contents": [{"parts": [{"text": "ping"}]}]},
|
|
118
|
+
)
|
|
119
|
+
elif provider_key == "OPENAI_API_KEY":
|
|
120
|
+
response = client.get(
|
|
121
|
+
"https://api.openai.com/v1/models",
|
|
122
|
+
headers={"Authorization": f"Bearer {secret}"},
|
|
123
|
+
)
|
|
124
|
+
elif provider_key == "OPENROUTER_API_KEY":
|
|
125
|
+
response = client.get(
|
|
126
|
+
"https://openrouter.ai/api/v1/models",
|
|
127
|
+
headers={"Authorization": f"Bearer {secret}"},
|
|
128
|
+
)
|
|
129
|
+
elif provider_key == "GROQ_API_KEY":
|
|
130
|
+
response = client.get(
|
|
131
|
+
"https://api.groq.com/openai/v1/models",
|
|
132
|
+
headers={"Authorization": f"Bearer {secret}"},
|
|
133
|
+
)
|
|
134
|
+
elif provider_key == "ANTHROPIC_API_KEY":
|
|
135
|
+
response = client.get(
|
|
136
|
+
"https://api.anthropic.com/v1/models",
|
|
137
|
+
headers={"x-api-key": secret, "anthropic-version": "2023-06-01"},
|
|
138
|
+
)
|
|
139
|
+
elif provider_key == "BRAVE_API_KEY":
|
|
140
|
+
response = client.get(
|
|
141
|
+
"https://api.search.brave.com/res/v1/web/search",
|
|
142
|
+
params={"q": "Conny AI"},
|
|
143
|
+
headers={"X-Subscription-Token": secret},
|
|
144
|
+
)
|
|
145
|
+
elif provider_key == "APIFY_API_KEY":
|
|
146
|
+
response = client.get(
|
|
147
|
+
"https://api.apify.com/v2/users/me",
|
|
148
|
+
params={"token": secret},
|
|
149
|
+
)
|
|
150
|
+
elif provider_key == "SERP_API_KEY":
|
|
151
|
+
response = client.get(
|
|
152
|
+
"https://serpapi.com/account",
|
|
153
|
+
params={"api_key": secret},
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
return False, "proveedor no soportado"
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
return False, str(exc)[:90]
|
|
159
|
+
if response.status_code < 300:
|
|
160
|
+
return True, f"HTTP {response.status_code}"
|
|
161
|
+
return False, f"HTTP {response.status_code}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _sync_telegram_webhook(state: Dict[str, Any]) -> Tuple[bool, str]:
|
|
165
|
+
info = state["info"]
|
|
166
|
+
token = info["telegram_token"]
|
|
167
|
+
base_url = info["base_url"]
|
|
168
|
+
secret = info["webhook_secret"]
|
|
169
|
+
if not token or not base_url or not secret:
|
|
170
|
+
return False, "faltan TELEGRAM_TOKEN, BASE_URL o WEBHOOK_SECRET"
|
|
171
|
+
target_url = f"{base_url.rstrip('/')}/webhook/{secret}"
|
|
172
|
+
try:
|
|
173
|
+
import httpx
|
|
174
|
+
with httpx.Client(timeout=10.0) as client:
|
|
175
|
+
response = client.post(
|
|
176
|
+
f"https://api.telegram.org/bot{token}/setWebhook",
|
|
177
|
+
json={"url": target_url},
|
|
178
|
+
)
|
|
179
|
+
payload = response.json()
|
|
180
|
+
if response.status_code == 200 and payload.get("ok"):
|
|
181
|
+
return True, target_url
|
|
182
|
+
return False, payload.get("description", f"HTTP {response.status_code}")
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
return False, str(exc)[:90]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _retarget_tunnels(target_port: int, tunnels: List[Dict[str, Any]]) -> Tuple[bool, str]:
|
|
188
|
+
changed = 0
|
|
189
|
+
for tunnel in tunnels:
|
|
190
|
+
current_cmd = str(tunnel.get("command", "")).strip()
|
|
191
|
+
new_cmd = rewrite_tunnel_command_port(current_cmd, target_port)
|
|
192
|
+
if not current_cmd or new_cmd == current_cmd:
|
|
193
|
+
continue
|
|
194
|
+
try:
|
|
195
|
+
subprocess.run(["kill", str(tunnel["pid"])], capture_output=True, check=False)
|
|
196
|
+
subprocess.Popen(
|
|
197
|
+
["bash", "-lc", f"nohup {new_cmd} >/tmp/conny-tunnel-{tunnel['pid']}.log 2>&1 &"],
|
|
198
|
+
stdout=subprocess.DEVNULL,
|
|
199
|
+
stderr=subprocess.DEVNULL,
|
|
200
|
+
)
|
|
201
|
+
changed += 1
|
|
202
|
+
except Exception:
|
|
203
|
+
continue
|
|
204
|
+
if changed:
|
|
205
|
+
return True, f"{changed} túnel(es) reorientados a :{target_port}"
|
|
206
|
+
return False, "no encontré túneles compatibles para reorientar"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def module_network(instance_name: str) -> None:
|
|
210
|
+
state = _load_state(instance_name)
|
|
211
|
+
info = state["info"]
|
|
212
|
+
pm2_rows = state["pm2"]
|
|
213
|
+
tunnels = state["tunnels"]
|
|
214
|
+
port = info["port"]
|
|
215
|
+
_render_header(instance_name or "base", "NETWORK MANAGEMENT")
|
|
216
|
+
pm2_status = pm2_rows[0].get("pm2_env", {}).get("status", "offline") if pm2_rows else "not registered"
|
|
217
|
+
print(f"{GREEN}Puerto local esperado:{RESET} {port}")
|
|
218
|
+
print(f"{GREEN}Escucha en localhost:{RESET} {'sí' if port_is_open(port) else 'no'}")
|
|
219
|
+
print(f"{GREEN}Proceso PM2:{RESET} {info['pm2_name']} ({pm2_status})")
|
|
220
|
+
print(f"{GREEN}Túneles detectados:{RESET}")
|
|
221
|
+
if tunnels:
|
|
222
|
+
for tunnel in tunnels:
|
|
223
|
+
ports = ", ".join(str(p) for p in tunnel.get("ports", [])) or "sin puerto parseado"
|
|
224
|
+
print(f" {DIM}pid={tunnel['pid']} ports={ports} :: {tunnel['command'][:100]}{RESET}")
|
|
225
|
+
else:
|
|
226
|
+
print(f" {DIM}ninguno detectado{RESET}")
|
|
227
|
+
|
|
228
|
+
options = [
|
|
229
|
+
"Cambiar puerto de la instancia",
|
|
230
|
+
"Reorientar túneles al puerto actual",
|
|
231
|
+
"Volver",
|
|
232
|
+
]
|
|
233
|
+
choice = select_menu(options, title="Acción de red")
|
|
234
|
+
if choice == 0:
|
|
235
|
+
new_port = text_input("Nuevo puerto", default=str(port))
|
|
236
|
+
if new_port.isdigit():
|
|
237
|
+
write_env_value(info["env_path"], "PORT", new_port)
|
|
238
|
+
meta_path = Path(info["root"]) / "instance.json"
|
|
239
|
+
if meta_path.exists():
|
|
240
|
+
try:
|
|
241
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
242
|
+
meta["port"] = int(new_port)
|
|
243
|
+
meta_path.write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
print(f"\n{GREEN}✓ Puerto actualizado a {new_port}. Reinicia la instancia para aplicarlo.{RESET}")
|
|
247
|
+
elif choice == 1:
|
|
248
|
+
ok, msg = _retarget_tunnels(port, tunnels)
|
|
249
|
+
tone = GREEN if ok else MAGENTA
|
|
250
|
+
print(f"\n{tone}{msg}{RESET}")
|
|
251
|
+
wait_for_enter()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def module_models(instance_name: str) -> None:
|
|
255
|
+
state = _load_state(instance_name)
|
|
256
|
+
info = state["info"]
|
|
257
|
+
env = info["env"]
|
|
258
|
+
_render_header(instance_name or "base", "MODELS & LLM PROVIDERS")
|
|
259
|
+
print(f"{GREEN}Modelos activos:{RESET}")
|
|
260
|
+
print(f" reasoning: {env.get('LLM_REASONING', 'google/gemini-2.5-pro')}")
|
|
261
|
+
print(f" fast: {env.get('LLM_FAST', 'google/gemini-2.5-flash')}")
|
|
262
|
+
print(f" lite: {env.get('LLM_LITE', 'google/gemini-2.5-flash-lite')}")
|
|
263
|
+
print()
|
|
264
|
+
for key, label in _provider_env_keys():
|
|
265
|
+
print(f" {label:<14} {DIM}{_mask(env.get(key, ''))}{RESET}")
|
|
266
|
+
print()
|
|
267
|
+
|
|
268
|
+
options = [
|
|
269
|
+
"Editar API key",
|
|
270
|
+
"Probar API key",
|
|
271
|
+
"Cambiar modelo por tier",
|
|
272
|
+
"Volver",
|
|
273
|
+
]
|
|
274
|
+
choice = select_menu(options, title="Acción de modelos")
|
|
275
|
+
if choice == 0:
|
|
276
|
+
idx = select_menu([label for _, label in _provider_env_keys()], title="¿Cuál key quieres editar?")
|
|
277
|
+
key, label = _provider_env_keys()[idx]
|
|
278
|
+
current = env.get(key, "")
|
|
279
|
+
new_value = text_input(f"{label} API key", default=current, is_password=bool(current), required=False)
|
|
280
|
+
write_env_value(info["env_path"], key, new_value)
|
|
281
|
+
print(f"\n{GREEN}✓ {label} actualizada{RESET}")
|
|
282
|
+
elif choice == 1:
|
|
283
|
+
idx = select_menu([label for _, label in _provider_env_keys()], title="¿Cuál key quieres probar?")
|
|
284
|
+
key, label = _provider_env_keys()[idx]
|
|
285
|
+
ok, msg = _test_provider(key, env.get(key, ""))
|
|
286
|
+
print(f"\n{GREEN if ok else MAGENTA}{label}: {msg}{RESET}")
|
|
287
|
+
elif choice == 2:
|
|
288
|
+
tier = select_menu(["LLM_REASONING", "LLM_FAST", "LLM_LITE"], title="Tier")
|
|
289
|
+
keys = ["LLM_REASONING", "LLM_FAST", "LLM_LITE"]
|
|
290
|
+
selected = keys[tier]
|
|
291
|
+
current = env.get(selected, "")
|
|
292
|
+
new_value = text_input(f"{selected}", default=current or "google/gemini-2.5-flash")
|
|
293
|
+
write_env_value(info["env_path"], selected, new_value)
|
|
294
|
+
print(f"\n{GREEN}✓ {selected} actualizado{RESET}")
|
|
295
|
+
wait_for_enter()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def module_gateway(instance_name: str) -> None:
|
|
299
|
+
state = _load_state(instance_name)
|
|
300
|
+
info = state["info"]
|
|
301
|
+
webhook = state["webhook"]
|
|
302
|
+
_render_header(instance_name or "base", "GATEWAY & WEBHOOKS")
|
|
303
|
+
expected = f"{info['base_url'].rstrip('/')}/webhook/{info['webhook_secret']}" if info["base_url"] and info["webhook_secret"] else "incompleto"
|
|
304
|
+
print(f"{GREEN}BASE_URL:{RESET} {info['base_url'] or 'vacío'}")
|
|
305
|
+
print(f"{GREEN}Webhook esperado:{RESET} {expected}")
|
|
306
|
+
print(f"{GREEN}Webhook Telegram actual:{RESET} {webhook.get('url', 'sin registrar')}")
|
|
307
|
+
print(f"{GREEN}Pendientes:{RESET} {webhook.get('pending_update_count', 0)}")
|
|
308
|
+
if webhook.get("last_error_message"):
|
|
309
|
+
print(f"{MAGENTA}Último error:{RESET} {webhook['last_error_message']}")
|
|
310
|
+
print()
|
|
311
|
+
choice = select_menu(
|
|
312
|
+
["Auto-sincronizar webhook", "Editar BASE_URL", "Volver"],
|
|
313
|
+
title="Acción de gateway",
|
|
314
|
+
)
|
|
315
|
+
if choice == 0:
|
|
316
|
+
ok, msg = _sync_telegram_webhook(state)
|
|
317
|
+
print(f"\n{GREEN if ok else MAGENTA}{msg}{RESET}")
|
|
318
|
+
elif choice == 1:
|
|
319
|
+
new_url = text_input("Nueva BASE_URL", default=info["base_url"], required=False)
|
|
320
|
+
write_env_value(info["env_path"], "BASE_URL", new_url)
|
|
321
|
+
print(f"\n{GREEN}✓ BASE_URL actualizada{RESET}")
|
|
322
|
+
wait_for_enter()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def module_environment(instance_name: str) -> None:
|
|
326
|
+
state = _load_state(instance_name)
|
|
327
|
+
info = state["info"]
|
|
328
|
+
_render_header(instance_name or "base", "ENVIRONMENT & PATH TUNING")
|
|
329
|
+
print(f"{GREEN}Intérprete activo detectado:{RESET} {state['python']['path'] if state['python'] else 'ninguno'}")
|
|
330
|
+
print(f"{GREEN}Candidatos:{RESET}")
|
|
331
|
+
candidates = python_candidates(info["name"])
|
|
332
|
+
for candidate in candidates:
|
|
333
|
+
marker = "✓" if candidate["exists"] else "·"
|
|
334
|
+
print(f" {marker} {candidate['source']:<18} {DIM}{candidate['path']}{RESET}")
|
|
335
|
+
print()
|
|
336
|
+
choice = select_menu(
|
|
337
|
+
["Fijar intérprete manual para esta instancia", "Verificar run.sh", "Volver"],
|
|
338
|
+
title="Acción de entorno",
|
|
339
|
+
)
|
|
340
|
+
if choice == 0:
|
|
341
|
+
valid = [c for c in candidates if c["exists"]]
|
|
342
|
+
if not valid:
|
|
343
|
+
print(f"\n{MAGENTA}No encontré candidatos válidos.{RESET}")
|
|
344
|
+
else:
|
|
345
|
+
idx = select_menu([f"{c['source']} :: {c['path']}" for c in valid], title="Selecciona intérprete")
|
|
346
|
+
selected = valid[idx]
|
|
347
|
+
write_env_value(info["env_path"], "CONNY_PYTHON_BIN", selected["path"])
|
|
348
|
+
print(f"\n{GREEN}✓ CONNY_PYTHON_BIN fijado a {selected['path']}{RESET}")
|
|
349
|
+
elif choice == 1:
|
|
350
|
+
run_path = Path(info["root"]) / "run.sh"
|
|
351
|
+
print(f"\n{GREEN}{run_path}{RESET}")
|
|
352
|
+
print(run_path.read_text(encoding="utf-8", errors="replace")[:2000] if run_path.exists() else "run.sh no existe")
|
|
353
|
+
wait_for_enter()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def module_doctor(instance_name: str) -> None:
|
|
357
|
+
_render_header(instance_name or "base", "ADVANCED SYSTEM DOCTOR")
|
|
358
|
+
print(f"{MAGENTA}Iniciando Self-Healing...{RESET}\n")
|
|
359
|
+
try:
|
|
360
|
+
import conny_doctor
|
|
361
|
+
doctor = conny_doctor.ConnyDoctor(instance_name or "base")
|
|
362
|
+
asyncio.run(doctor.run_self_healing())
|
|
363
|
+
except Exception as exc:
|
|
364
|
+
print(f"{MAGENTA}Error ejecutando doctor: {exc}{RESET}")
|
|
365
|
+
wait_for_enter()
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def run_ultra_config(instance_name: str = "") -> None:
|
|
369
|
+
active = (instance_name or "base").strip()
|
|
370
|
+
while True:
|
|
371
|
+
_render_header(active, "CONTROL TOTAL DEL USUARIO")
|
|
372
|
+
state = _load_state(active)
|
|
373
|
+
port = state["info"]["port"]
|
|
374
|
+
health = "online" if state["health"].get("status") == "online" else "offline"
|
|
375
|
+
print(f"{GREEN}Estado rápido:{RESET} puerto :{port} · health {health} · pm2 {state['info']['pm2_name']}")
|
|
376
|
+
print()
|
|
377
|
+
options = [
|
|
378
|
+
"NETWORK MANAGEMENT",
|
|
379
|
+
"MODELS & LLM PROVIDERS",
|
|
380
|
+
"GATEWAY & WEBHOOKS",
|
|
381
|
+
"ENVIRONMENT & PATH TUNING",
|
|
382
|
+
"ADVANCED SYSTEM DOCTOR",
|
|
383
|
+
"SALIR",
|
|
384
|
+
]
|
|
385
|
+
descs = [
|
|
386
|
+
"Puertos locales, PM2 y túneles públicos",
|
|
387
|
+
"API keys, pruebas y tiering de modelos",
|
|
388
|
+
"Webhook esperado, URL activa y resincronización",
|
|
389
|
+
"Intérprete Python, overrides y run.sh",
|
|
390
|
+
"Autorreparación de runtime, deps y procesos",
|
|
391
|
+
"Cerrar configuración",
|
|
392
|
+
]
|
|
393
|
+
choice = select_menu(options, title="Conny config", descriptions=descs)
|
|
394
|
+
if choice == 0:
|
|
395
|
+
module_network(active)
|
|
396
|
+
elif choice == 1:
|
|
397
|
+
module_models(active)
|
|
398
|
+
elif choice == 2:
|
|
399
|
+
module_gateway(active)
|
|
400
|
+
elif choice == 3:
|
|
401
|
+
module_environment(active)
|
|
402
|
+
elif choice == 4:
|
|
403
|
+
module_doctor(active)
|
|
404
|
+
else:
|
|
405
|
+
clear_screen()
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
if __name__ == "__main__":
|
|
410
|
+
selected = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
411
|
+
run_ultra_config(selected)
|