@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_init.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""conny init — Infrastructure Provisioning Wizard v2."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
import secrets
|
|
11
|
+
import shutil
|
|
12
|
+
import urllib.request
|
|
13
|
+
import urllib.error
|
|
14
|
+
import subprocess
|
|
15
|
+
from deep_translator import GoogleTranslator
|
|
16
|
+
|
|
17
|
+
CURRENT_LANG = 'es'
|
|
18
|
+
|
|
19
|
+
def _t(text):
|
|
20
|
+
if CURRENT_LANG == 'es' or not text:
|
|
21
|
+
return text
|
|
22
|
+
try:
|
|
23
|
+
# Limpiar texto de colores si es necesario, o traducir directo
|
|
24
|
+
return GoogleTranslator(source='es', target=CURRENT_LANG).translate(text)
|
|
25
|
+
except:
|
|
26
|
+
return text
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
|
|
30
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
31
|
+
|
|
32
|
+
from conny_tui_select import select_menu as _orig_select_menu, confirm as _orig_confirm, text_input as _orig_text_input
|
|
33
|
+
|
|
34
|
+
def select_menu(options, title="", **kwargs):
|
|
35
|
+
if title: title = _t(title)
|
|
36
|
+
# also translate options if they are simple strings
|
|
37
|
+
opts = [_t(opt) for opt in options]
|
|
38
|
+
return _orig_select_menu(opts, title=title, **kwargs)
|
|
39
|
+
|
|
40
|
+
def confirm(text, default=True):
|
|
41
|
+
return _orig_confirm(_t(text), default=default)
|
|
42
|
+
|
|
43
|
+
def text_input(label, default="", required=True, is_password=False):
|
|
44
|
+
return _orig_text_input(_t(label), default=default, required=required, is_password=is_password)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
VERSION = "10.0.0"
|
|
48
|
+
try:
|
|
49
|
+
package_path = Path(__file__).parent / "package.json"
|
|
50
|
+
if package_path.exists():
|
|
51
|
+
VERSION = json.loads(package_path.read_text()).get("version", VERSION)
|
|
52
|
+
except: pass
|
|
53
|
+
|
|
54
|
+
# Colors - Brand Semantic
|
|
55
|
+
C_PRIMARY = "\033[38;2;139;92;246m" # Purple
|
|
56
|
+
C_SUCCESS = "\033[38;5;114m" # Green
|
|
57
|
+
C_WARNING = "\033[38;5;221m" # Yellow
|
|
58
|
+
C_ERROR = "\033[38;5;196m" # Red
|
|
59
|
+
C_MUTED = "\033[38;5;242m" # Gray
|
|
60
|
+
C_ACCENT = "\033[38;5;159m" # Cyan
|
|
61
|
+
B1 = "\033[1m"
|
|
62
|
+
R = "\033[0m"
|
|
63
|
+
|
|
64
|
+
INSTANCES_DIR = Path(os.environ.get("INSTANCES_DIR", str(Path.home() / ".conny-instances")))
|
|
65
|
+
CONNY_DIR = Path(os.environ.get("CONNY_DIR", os.path.dirname(os.path.abspath(__file__))))
|
|
66
|
+
|
|
67
|
+
SECTORS = [
|
|
68
|
+
("clinica", "Clínica / Centro médico"),
|
|
69
|
+
("estetica", "Estética / Belleza"),
|
|
70
|
+
("restaurante", "Restaurante / Bar"),
|
|
71
|
+
("salon", "Salón / Peluquería / Barbería"),
|
|
72
|
+
("inmobiliaria", "Inmobiliaria / Finca raíz"),
|
|
73
|
+
("gym", "Gimnasio / Fitness"),
|
|
74
|
+
("ecommerce", "E-commerce / Tienda online"),
|
|
75
|
+
("hotel", "Hotel / Hospedaje"),
|
|
76
|
+
("veterinaria", "Veterinaria"),
|
|
77
|
+
("educacion", "Educación / Academia"),
|
|
78
|
+
("otro", "Otro"),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
CHANNELS = [
|
|
82
|
+
("whatsapp", "WhatsApp (Baileys bridge)"),
|
|
83
|
+
("whatsapp_cloud", "WhatsApp Cloud API (Meta)"),
|
|
84
|
+
("telegram", "Telegram"),
|
|
85
|
+
("both", "WhatsApp + Telegram"),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
TONES = [
|
|
89
|
+
("colombian_warm", "Cálida — profesional pero cercana (Más usada)"),
|
|
90
|
+
("casual", "Casual — amigable y relajada"),
|
|
91
|
+
("formal", "Formal — respetuosa y directa"),
|
|
92
|
+
("luxury", "Luxury — sofisticada y exclusiva"),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
LANGUAGES = [
|
|
96
|
+
("es", "Español"),
|
|
97
|
+
("en", "English"),
|
|
98
|
+
("pt", "Português"),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
def clear():
|
|
102
|
+
# Animated fade out simulation
|
|
103
|
+
sys.stdout.write("\033[2J\033[H")
|
|
104
|
+
sys.stdout.flush()
|
|
105
|
+
|
|
106
|
+
def progress_bar(n, total):
|
|
107
|
+
percent = int((n / total) * 100)
|
|
108
|
+
filled = int((n / total) * 20)
|
|
109
|
+
bar = "█" * filled + "░" * (20 - filled)
|
|
110
|
+
return f"{C_PRIMARY}[{bar}] {percent}%{R} {C_MUTED}· Paso {n} de {total}{R}"
|
|
111
|
+
|
|
112
|
+
def step_header(n, total, title, context=""):
|
|
113
|
+
title = _t(title)
|
|
114
|
+
if context: context = _t(context)
|
|
115
|
+
print(f"\n {progress_bar(n, total)}")
|
|
116
|
+
print(f" {B1}{C_ACCENT}{title}{R}")
|
|
117
|
+
if context:
|
|
118
|
+
print(f" {C_MUTED}{context}{R}")
|
|
119
|
+
print(f" {C_MUTED}{'━' * 60}{R}\n")
|
|
120
|
+
|
|
121
|
+
def check_ollama():
|
|
122
|
+
try:
|
|
123
|
+
req = urllib.request.Request("http://localhost:11434/api/tags")
|
|
124
|
+
with urllib.request.urlopen(req, timeout=1.0) as response:
|
|
125
|
+
if response.status == 200:
|
|
126
|
+
data = json.loads(response.read().decode())
|
|
127
|
+
return [m["name"] for m in data.get("models", [])]
|
|
128
|
+
except:
|
|
129
|
+
pass
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def check_gpu():
|
|
133
|
+
try:
|
|
134
|
+
subprocess.check_output(["nvidia-smi"], stderr=subprocess.STDOUT)
|
|
135
|
+
return True
|
|
136
|
+
except:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def validate_api_key(provider, key):
|
|
140
|
+
# Dummy validation with spinner
|
|
141
|
+
sys.stdout.write(f" {C_MUTED}Validando API key...{R}")
|
|
142
|
+
sys.stdout.flush()
|
|
143
|
+
time.sleep(1.5)
|
|
144
|
+
sys.stdout.write(f"\r {C_SUCCESS}✅ API key validada correctamente.{R} \n")
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def run_wizard():
|
|
149
|
+
|
|
150
|
+
clear()
|
|
151
|
+
|
|
152
|
+
# Check dependencies silently
|
|
153
|
+
ollama_models = check_ollama()
|
|
154
|
+
has_gpu = check_gpu()
|
|
155
|
+
|
|
156
|
+
LOGO_ART_LINES = ["Conny."]
|
|
157
|
+
import subprocess
|
|
158
|
+
from pathlib import Path
|
|
159
|
+
import re
|
|
160
|
+
|
|
161
|
+
logo_path = Path(os.environ.get("CONNY_DIR", os.path.dirname(os.path.abspath(__file__)))) / "brand-assets" / "conny-logo.png"
|
|
162
|
+
if logo_path.exists():
|
|
163
|
+
try:
|
|
164
|
+
result = subprocess.run(
|
|
165
|
+
["chafa", "--symbols", "block", "-c", "256", "--size", "50x20", str(logo_path)],
|
|
166
|
+
capture_output=True, text=True, check=True
|
|
167
|
+
)
|
|
168
|
+
lines = result.stdout.splitlines()
|
|
169
|
+
|
|
170
|
+
def is_empty(line):
|
|
171
|
+
clean = re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', line)
|
|
172
|
+
return not clean.strip()
|
|
173
|
+
|
|
174
|
+
while lines and is_empty(lines[0]):
|
|
175
|
+
lines.pop(0)
|
|
176
|
+
|
|
177
|
+
while lines and is_empty(lines[-1]):
|
|
178
|
+
lines.pop()
|
|
179
|
+
|
|
180
|
+
if lines:
|
|
181
|
+
LOGO_ART_LINES = lines
|
|
182
|
+
except:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
if LOGO_ART_LINES == ["Conny."]:
|
|
186
|
+
try:
|
|
187
|
+
from src.conny.channels.logo_art import LOGO_ART_LINES
|
|
188
|
+
except ImportError:
|
|
189
|
+
try:
|
|
190
|
+
from conny.channels.logo_art import LOGO_ART_LINES
|
|
191
|
+
except ImportError:
|
|
192
|
+
LOGO_ART_LINES = ["Conny."]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Print the logo directly
|
|
197
|
+
for line in LOGO_ART_LINES:
|
|
198
|
+
print(f" {line}")
|
|
199
|
+
|
|
200
|
+
print()
|
|
201
|
+
print(f" {C_PRIMARY}{B1}Conny CLI {VERSION}{R} · {C_ACCENT}Autonomous Dynamic Receptionist{R} · {C_MUTED}⏱ Tiempo estimado: ~3 min{R}")
|
|
202
|
+
print(f" {C_MUTED}{'─' * 70}{R}")
|
|
203
|
+
print()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if not confirm("¿Comenzar configuración?"):
|
|
208
|
+
print(f"\n {C_MUTED}Operación cancelada.{R}\n")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
TOTAL_STEPS = 7
|
|
212
|
+
ctx = ""
|
|
213
|
+
|
|
214
|
+
# 0. Language (Optional pre-step)
|
|
215
|
+
clear()
|
|
216
|
+
print(f"\n {C_PRIMARY}🌐 {_t('Selección de idioma')}{R}")
|
|
217
|
+
i = select_menu([l[1] for l in LANGUAGES], title="Idioma preferido para Conny")
|
|
218
|
+
i = select_menu([l[1] for l in LANGUAGES], title="Idioma preferido para Conny")
|
|
219
|
+
lang = LANGUAGES[i][0]
|
|
220
|
+
global CURRENT_LANG
|
|
221
|
+
CURRENT_LANG = lang
|
|
222
|
+
|
|
223
|
+
# 1. Identity
|
|
224
|
+
clear()
|
|
225
|
+
default_name = Path.cwd().name.title()
|
|
226
|
+
step_header(1, TOTAL_STEPS, "Identidad", ctx)
|
|
227
|
+
name = text_input("Nombre del negocio", default=default_name)
|
|
228
|
+
instance_id = _slug(name)
|
|
229
|
+
ctx += f"Negocio: {name} | "
|
|
230
|
+
|
|
231
|
+
# 2. Domain
|
|
232
|
+
clear()
|
|
233
|
+
step_header(2, TOTAL_STEPS, "Dominio e Industria", ctx)
|
|
234
|
+
i = select_menu([s[1] for s in SECTORS], title="Sector Principal")
|
|
235
|
+
sector = SECTORS[i][0]
|
|
236
|
+
ctx += f"Sector: {sector} | "
|
|
237
|
+
|
|
238
|
+
# 3. Connectivity
|
|
239
|
+
clear()
|
|
240
|
+
step_header(3, TOTAL_STEPS, "Canales de Conectividad", ctx)
|
|
241
|
+
i = select_menu([c[1] for c in CHANNELS], title="Canal Principal de Acceso")
|
|
242
|
+
channel = CHANNELS[i][0]
|
|
243
|
+
|
|
244
|
+
# 4. Intelligence
|
|
245
|
+
clear()
|
|
246
|
+
step_header(4, TOTAL_STEPS, "Motor de Inteligencia", ctx)
|
|
247
|
+
|
|
248
|
+
# Level 1
|
|
249
|
+
llm_types = [
|
|
250
|
+
"☁️ Cloud (requiere API Key)",
|
|
251
|
+
"🏠 Local (Ollama · sin costo)",
|
|
252
|
+
"💚 Local GPU (NVIDIA NIM)",
|
|
253
|
+
"🔀 Manual / Custom endpoint"
|
|
254
|
+
]
|
|
255
|
+
i = select_menu(llm_types, title="Tipo de proveedor")
|
|
256
|
+
|
|
257
|
+
provider_id = ""
|
|
258
|
+
model_id = ""
|
|
259
|
+
is_local = False
|
|
260
|
+
|
|
261
|
+
if i == 0: # Cloud
|
|
262
|
+
providers = [
|
|
263
|
+
("anthropic", "🔴 Anthropic"),
|
|
264
|
+
("openai", "🟢 OpenAI"),
|
|
265
|
+
("gemini", "🔵 Google Gemini"),
|
|
266
|
+
("mistral", "🟠 Mistral AI"),
|
|
267
|
+
("groq", "⚡ Groq (Ultra rápido)"),
|
|
268
|
+
("xai", "✕ xAI (Grok)"),
|
|
269
|
+
("deepseek", "🐋 DeepSeek"),
|
|
270
|
+
("openrouter", "🔀 OpenRouter (Multi-modelo)"),
|
|
271
|
+
]
|
|
272
|
+
pi = select_menu([p[1] for p in providers], title="Proveedor Cloud")
|
|
273
|
+
provider_id = providers[pi][0]
|
|
274
|
+
|
|
275
|
+
# Models
|
|
276
|
+
models_map = {
|
|
277
|
+
"anthropic": ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"],
|
|
278
|
+
"openai": ["gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo"],
|
|
279
|
+
"gemini": ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-flash-lite"],
|
|
280
|
+
"mistral": ["mistral-large-latest", "mistral-small-latest"],
|
|
281
|
+
"groq": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
|
|
282
|
+
"xai": ["grok-2", "grok-2-mini"],
|
|
283
|
+
"deepseek": ["deepseek-chat", "deepseek-reasoner"],
|
|
284
|
+
"openrouter": ["anthropic/claude-3.5-sonnet", "openai/gpt-4o", "google/gemini-2.5-flash"],
|
|
285
|
+
}
|
|
286
|
+
model_opts = models_map.get(provider_id, []) + ["✏️ Escribir model ID manualmente"]
|
|
287
|
+
mi = select_menu(model_opts, title="Modelo Específico")
|
|
288
|
+
if mi == len(model_opts) - 1:
|
|
289
|
+
model_id = text_input("Model ID")
|
|
290
|
+
else:
|
|
291
|
+
model_id = model_opts[mi]
|
|
292
|
+
|
|
293
|
+
elif i == 1: # Ollama
|
|
294
|
+
is_local = True
|
|
295
|
+
provider_id = "ollama"
|
|
296
|
+
if ollama_models:
|
|
297
|
+
print(f" {C_SUCCESS}✅ Ollama detectado con {len(ollama_models)} modelos.{R}")
|
|
298
|
+
model_opts = ollama_models + ["✏️ Escribir model ID manualmente"]
|
|
299
|
+
else:
|
|
300
|
+
print(f" {C_WARNING}⚠️ Ollama no está corriendo o no tiene modelos.{R}")
|
|
301
|
+
model_opts = ["llama3.3:8b", "mistral:latest", "✏️ Escribir model ID manualmente"]
|
|
302
|
+
|
|
303
|
+
mi = select_menu(model_opts, title="Modelo de Ollama")
|
|
304
|
+
if mi == len(model_opts) - 1:
|
|
305
|
+
model_id = text_input("Model ID")
|
|
306
|
+
else:
|
|
307
|
+
model_id = model_opts[mi]
|
|
308
|
+
|
|
309
|
+
elif i == 2: # NVIDIA
|
|
310
|
+
is_local = True
|
|
311
|
+
provider_id = "nvidia_nim"
|
|
312
|
+
if not has_gpu:
|
|
313
|
+
print(f" {C_ERROR}❌ No se detectó GPU NVIDIA. Podría haber problemas.{R}")
|
|
314
|
+
model_opts = ["meta/llama3-70b-instruct", "mistralai/mixtral-8x22b-instruct", "✏️ Escribir model ID manualmente"]
|
|
315
|
+
mi = select_menu(model_opts, title="Modelo NIM")
|
|
316
|
+
if mi == len(model_opts) - 1:
|
|
317
|
+
model_id = text_input("Model ID")
|
|
318
|
+
else:
|
|
319
|
+
model_id = model_opts[mi]
|
|
320
|
+
|
|
321
|
+
else: # Manual
|
|
322
|
+
provider_id = "manual"
|
|
323
|
+
model_id = text_input("Model ID")
|
|
324
|
+
|
|
325
|
+
# 5. Personality
|
|
326
|
+
clear()
|
|
327
|
+
step_header(5, TOTAL_STEPS, "Humanización y Tono", ctx)
|
|
328
|
+
i = select_menu([t[1] for t in TONES], title="Perfil de Voz")
|
|
329
|
+
tone = TONES[i][0]
|
|
330
|
+
|
|
331
|
+
# 6. Credentials
|
|
332
|
+
clear()
|
|
333
|
+
step_header(6, TOTAL_STEPS, "Infraestructura y Secretos", ctx)
|
|
334
|
+
|
|
335
|
+
secrets_map = {}
|
|
336
|
+
|
|
337
|
+
if not is_local:
|
|
338
|
+
if provider_id == "anthropic":
|
|
339
|
+
key = text_input("Anthropic API Key", is_password=True)
|
|
340
|
+
secrets_map["ANTHROPIC_API_KEY"] = key
|
|
341
|
+
validate_api_key(provider_id, key)
|
|
342
|
+
elif provider_id == "openai":
|
|
343
|
+
key = text_input("OpenAI API Key", is_password=True)
|
|
344
|
+
secrets_map["OPENAI_API_KEY"] = key
|
|
345
|
+
validate_api_key(provider_id, key)
|
|
346
|
+
elif provider_id == "gemini":
|
|
347
|
+
key = text_input("Gemini API Key", is_password=True)
|
|
348
|
+
secrets_map["GEMINI_API_KEY"] = key
|
|
349
|
+
validate_api_key(provider_id, key)
|
|
350
|
+
elif provider_id == "groq":
|
|
351
|
+
key = text_input("Groq API Key", is_password=True)
|
|
352
|
+
secrets_map["GROQ_API_KEY"] = key
|
|
353
|
+
validate_api_key(provider_id, key)
|
|
354
|
+
elif provider_id == "openrouter":
|
|
355
|
+
key = text_input("OpenRouter API Key", is_password=True)
|
|
356
|
+
secrets_map["OPENROUTER_API_KEY"] = key
|
|
357
|
+
validate_api_key(provider_id, key)
|
|
358
|
+
else:
|
|
359
|
+
key = text_input(f"{provider_id.title()} API Key", is_password=True)
|
|
360
|
+
secrets_map[f"{provider_id.upper()}_API_KEY"] = key
|
|
361
|
+
validate_api_key(provider_id, key)
|
|
362
|
+
|
|
363
|
+
if provider_id == "manual":
|
|
364
|
+
secrets_map["CUSTOM_API_BASE"] = text_input("Endpoint URL (ej: http://localhost:8000/v1)")
|
|
365
|
+
|
|
366
|
+
# Channel Tokens
|
|
367
|
+
if channel in ["telegram", "both"]:
|
|
368
|
+
secrets_map["TELEGRAM_TOKEN"] = text_input("Telegram Bot Token", is_password=True)
|
|
369
|
+
|
|
370
|
+
if channel in ["whatsapp_cloud", "both"]:
|
|
371
|
+
secrets_map["WA_CLOUD_TOKEN"] = text_input("WA Cloud API Token", is_password=True)
|
|
372
|
+
secrets_map["WA_PHONE_NUMBER_ID"] = text_input("Phone Number ID")
|
|
373
|
+
|
|
374
|
+
# 7. Confirmation
|
|
375
|
+
clear()
|
|
376
|
+
step_header(7, TOTAL_STEPS, "Verificación Final")
|
|
377
|
+
|
|
378
|
+
print(f" ┌────────────────────────────────────────────────────────┐")
|
|
379
|
+
print(f" │ {C_PRIMARY}{B1}Resumen de Configuración{R} │")
|
|
380
|
+
print(f" ├────────────────────────────────────────────────────────┤")
|
|
381
|
+
print(f" │ {C_MUTED}Negocio:{R} {B1}{name.ljust(44)}{R}│")
|
|
382
|
+
print(f" │ {C_MUTED}Sector:{R} {sector.ljust(44)}│")
|
|
383
|
+
print(f" │ {C_MUTED}Canal:{R} {channel.ljust(44)}│")
|
|
384
|
+
print(f" │ {C_MUTED}Proveedor:{R} {provider_id.ljust(44)}│")
|
|
385
|
+
print(f" │ {C_MUTED}Modelo:{R} {model_id.ljust(44)}│")
|
|
386
|
+
print(f" │ {C_MUTED}Voz:{R} {tone.ljust(44)}│")
|
|
387
|
+
print(f" │ {C_MUTED}Secretos:{R} {str(len(secrets_map)).ljust(44)}│")
|
|
388
|
+
print(f" └────────────────────────────────────────────────────────┘\n")
|
|
389
|
+
|
|
390
|
+
if not confirm("¿Procesar e Implementar Infraestructura?"):
|
|
391
|
+
print(f"\n {C_MUTED}Cancelado.{R}\n")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
print(f"\n {C_PRIMARY}Creando recursos...{R}")
|
|
395
|
+
_create(name, instance_id, sector, channel, provider_id, model_id, tone, secrets_map, lang)
|
|
396
|
+
|
|
397
|
+
print(f"""
|
|
398
|
+
{C_SUCCESS}{B1}🚀 Infraestructura Desplegada Exitosamente{R}
|
|
399
|
+
|
|
400
|
+
{C_MUTED}Directorio:{R} {INSTANCES_DIR / instance_id}
|
|
401
|
+
{C_MUTED}Control:{R} {C_PRIMARY}conny status {instance_id}{R}
|
|
402
|
+
{C_MUTED}Check:{R} {C_PRIMARY}conny doctor{R}
|
|
403
|
+
{C_MUTED}Lanzar:{R} {C_PRIMARY}pm2 start {INSTANCES_DIR / instance_id}/run.sh --name conny-{instance_id}{R}
|
|
404
|
+
""")
|
|
405
|
+
|
|
406
|
+
# Post-onboarding commands
|
|
407
|
+
if is_local and provider_id == "ollama":
|
|
408
|
+
print(f" {C_WARNING}💡 Asegúrate de descargar el modelo: `ollama run {model_id}`{R}\n")
|
|
409
|
+
|
|
410
|
+
def _create(name, iid, sector, channel, provider, model_id, tone, secrets_map, lang):
|
|
411
|
+
idir = INSTANCES_DIR / iid
|
|
412
|
+
idir.mkdir(parents=True, exist_ok=True)
|
|
413
|
+
|
|
414
|
+
existing_ports = []
|
|
415
|
+
if INSTANCES_DIR.exists():
|
|
416
|
+
for d in INSTANCES_DIR.iterdir():
|
|
417
|
+
env = d / ".env"
|
|
418
|
+
if env.exists():
|
|
419
|
+
for line in env.read_text().splitlines():
|
|
420
|
+
if line.startswith("PORT="):
|
|
421
|
+
try:
|
|
422
|
+
parts = line.split("=")
|
|
423
|
+
if len(parts) > 1:
|
|
424
|
+
existing_ports.append(int(parts[1]))
|
|
425
|
+
except: pass
|
|
426
|
+
|
|
427
|
+
port = max(existing_ports + [8003]) + 1
|
|
428
|
+
webhook_secret = f"conny_{iid}_{secrets.token_hex(6)}"
|
|
429
|
+
|
|
430
|
+
env_lines = [
|
|
431
|
+
f"INSTANCE_ID={iid}",
|
|
432
|
+
f"PORT={port}",
|
|
433
|
+
"DEMO_MODE=false",
|
|
434
|
+
f"PLATFORM={channel}",
|
|
435
|
+
f"SECTOR={sector}",
|
|
436
|
+
f"BUSINESS_NAME=\"{name}\"",
|
|
437
|
+
f"WEBHOOK_SECRET={webhook_secret}",
|
|
438
|
+
f"LLM_PROVIDER={provider}",
|
|
439
|
+
f"LLM_MODEL={model_id}",
|
|
440
|
+
"DEBUG=false"
|
|
441
|
+
]
|
|
442
|
+
for k, v in secrets_map.items():
|
|
443
|
+
env_lines.append(f"{k}={v}")
|
|
444
|
+
|
|
445
|
+
(idir / ".env").write_text("\n".join(env_lines) + "\n")
|
|
446
|
+
|
|
447
|
+
# Generate System Prompt Base
|
|
448
|
+
sys_prompt = f"""Eres Conny, recepcionista autónoma de {name} (Sector: {sector}).
|
|
449
|
+
Tu tono de voz es {tone}. Responde por {channel}.
|
|
450
|
+
Idioma principal: {lang}
|
|
451
|
+
"""
|
|
452
|
+
(idir / "conny_system_prompt.txt").write_text(sys_prompt)
|
|
453
|
+
|
|
454
|
+
(idir / "personas").mkdir(exist_ok=True)
|
|
455
|
+
(idir / "personas" / "persona.yaml").write_text(f"""identity:
|
|
456
|
+
name: Conny
|
|
457
|
+
business: "{name}"
|
|
458
|
+
sector: {sector}
|
|
459
|
+
voice:
|
|
460
|
+
tone: {tone}
|
|
461
|
+
language: {lang}
|
|
462
|
+
llm:
|
|
463
|
+
provider: {provider}
|
|
464
|
+
model: {model_id}
|
|
465
|
+
""")
|
|
466
|
+
|
|
467
|
+
core = ["conny.py", "src/core/admin_engines.py", "src/core/production_monitor.py", "conny_config.py",
|
|
468
|
+
"conny_utils.py", "conny_commands.py", "conny_learning.py", "conny_voice.py",
|
|
469
|
+
"conny_uncertainty.py", "conny_memory_engine.py", "conny_admin_api.py",
|
|
470
|
+
"conny_cron.py", "conny_nova_proxy.py", "conny_smart_features.py",
|
|
471
|
+
"conny_web_search.py", "conny_google_auth.py", "run.sh", "requirements.txt",
|
|
472
|
+
"conny_design.py", "conny_i18n.py", "conny_cli_bb.py"]
|
|
473
|
+
|
|
474
|
+
for f in core:
|
|
475
|
+
src = CONNY_DIR / f
|
|
476
|
+
if src.exists(): shutil.copy2(src, idir / f)
|
|
477
|
+
|
|
478
|
+
for d in ["soul", "teachings", "memory_store", "knowledge_gaps", "integrations/vault", "logs"]:
|
|
479
|
+
(idir / d).mkdir(parents=True, exist_ok=True)
|
|
480
|
+
|
|
481
|
+
(idir / "run.sh").write_text(f"#!/bin/bash\ncd {idir}\nexec {sys.executable} conny.py\n")
|
|
482
|
+
os.chmod(idir / "run.sh", 0o755)
|
|
483
|
+
|
|
484
|
+
# State
|
|
485
|
+
state = {
|
|
486
|
+
"status": "configured",
|
|
487
|
+
"timestamp": datetime.now().isoformat(),
|
|
488
|
+
"instance": iid,
|
|
489
|
+
"provider": provider
|
|
490
|
+
}
|
|
491
|
+
(idir / "conny.state.json").write_text(json.dumps(state, indent=2))
|
|
492
|
+
|
|
493
|
+
def _slug(name):
|
|
494
|
+
s = name.lower().strip()
|
|
495
|
+
for a, b in [("á","a"),("é","e"),("í","i"),("ó","o"),("ú","u"),("ñ","n")]:
|
|
496
|
+
s = s.replace(a, b)
|
|
497
|
+
return re.sub(r'[^a-z0-9]+', '-', s).strip('-')[:40]
|
|
498
|
+
|
|
499
|
+
def main():
|
|
500
|
+
try:
|
|
501
|
+
if "--reset" in sys.argv:
|
|
502
|
+
print(f" {C_WARNING}Reset no implementado en mock v2.{R}")
|
|
503
|
+
return
|
|
504
|
+
run_wizard()
|
|
505
|
+
except KeyboardInterrupt:
|
|
506
|
+
print(f"\n\n {C_MUTED}Cancelado.{R}\n")
|
|
507
|
+
|
|
508
|
+
if __name__ == "__main__":
|
|
509
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""LLM integration scaffold."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""conny_integrations/vault.py — Central credential vault for all integrations."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json, logging, os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
log = logging.getLogger("conny.vault")
|
|
8
|
+
|
|
9
|
+
VAULT_DIR = Path("integrations/vault")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IntegrationVault:
|
|
13
|
+
"""
|
|
14
|
+
Per-instance credential manager. If API key exists -> integration active.
|
|
15
|
+
If not -> feature silently disabled. Zero errors shown to end user.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, instance_id: str):
|
|
19
|
+
self.instance_id = instance_id
|
|
20
|
+
self.active: Dict[str, Any] = {}
|
|
21
|
+
self._vault_dir = VAULT_DIR / instance_id
|
|
22
|
+
self._vault_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
self._load()
|
|
24
|
+
|
|
25
|
+
def _load(self):
|
|
26
|
+
"""Load credentials from manifest."""
|
|
27
|
+
manifest_path = self._vault_dir / "manifest.json"
|
|
28
|
+
if not manifest_path.exists():
|
|
29
|
+
return
|
|
30
|
+
try:
|
|
31
|
+
manifest = json.loads(manifest_path.read_text())
|
|
32
|
+
for service_name, config in manifest.items():
|
|
33
|
+
if config.get("enabled", True):
|
|
34
|
+
self.active[service_name] = config
|
|
35
|
+
log.info(f"[vault] {self.instance_id}: {service_name} active")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
log.warning(f"[vault] failed to load manifest for {self.instance_id}: {e}")
|
|
38
|
+
|
|
39
|
+
def get(self, service_name: str) -> Optional[Dict]:
|
|
40
|
+
"""Get integration config. Returns None if not active."""
|
|
41
|
+
return self.active.get(service_name)
|
|
42
|
+
|
|
43
|
+
def is_active(self, service_name: str) -> bool:
|
|
44
|
+
return service_name in self.active
|
|
45
|
+
|
|
46
|
+
async def add(self, service_name: str, credentials: Dict) -> bool:
|
|
47
|
+
"""Add or update an integration credential."""
|
|
48
|
+
self.active[service_name] = {**credentials, "enabled": True, "added_at": __import__("time").time()}
|
|
49
|
+
self._save_manifest()
|
|
50
|
+
log.info(f"[vault] added {service_name} for {self.instance_id}")
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
async def remove(self, service_name: str) -> bool:
|
|
54
|
+
"""Remove an integration."""
|
|
55
|
+
if service_name in self.active:
|
|
56
|
+
del self.active[service_name]
|
|
57
|
+
self._save_manifest()
|
|
58
|
+
log.info(f"[vault] removed {service_name} from {self.instance_id}")
|
|
59
|
+
return True
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def list_active(self) -> list:
|
|
63
|
+
"""List all active integrations."""
|
|
64
|
+
return list(self.active.keys())
|
|
65
|
+
|
|
66
|
+
def _save_manifest(self):
|
|
67
|
+
manifest_path = self._vault_dir / "manifest.json"
|
|
68
|
+
manifest_path.write_text(json.dumps(self.active, ensure_ascii=False, indent=2))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Cache of vaults per instance
|
|
72
|
+
_vaults: Dict[str, IntegrationVault] = {}
|
|
73
|
+
|
|
74
|
+
def get_vault(instance_id: str) -> IntegrationVault:
|
|
75
|
+
if instance_id not in _vaults:
|
|
76
|
+
_vaults[instance_id] = IntegrationVault(instance_id)
|
|
77
|
+
return _vaults[instance_id]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""WhatsApp integration scaffold."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger("conny.intelligence")
|
|
10
|
+
|
|
11
|
+
class IntentType(Enum):
|
|
12
|
+
GREETING = "saludo"
|
|
13
|
+
INFORMATION = "informacion"
|
|
14
|
+
BOOKING = "agendamiento"
|
|
15
|
+
PRICING = "precio"
|
|
16
|
+
COMPLAINT = "queja"
|
|
17
|
+
EMERGENCY = "emergencia"
|
|
18
|
+
OFF_TOPIC = "fuera_de_tema"
|
|
19
|
+
META = "pregunta_sobre_ia"
|
|
20
|
+
CLOSING = "cierre"
|
|
21
|
+
FOLLOW_UP = "seguimiento"
|
|
22
|
+
|
|
23
|
+
class SentimentType(Enum):
|
|
24
|
+
POSITIVE = "positivo"
|
|
25
|
+
NEUTRAL = "neutral"
|
|
26
|
+
NEGATIVE = "negativo"
|
|
27
|
+
ANXIOUS = "ansioso"
|
|
28
|
+
URGENT = "urgente"
|
|
29
|
+
|
|
30
|
+
class UrgencyLevel(Enum):
|
|
31
|
+
NONE = "ninguna"
|
|
32
|
+
LOW = "baja"
|
|
33
|
+
MEDIUM = "media"
|
|
34
|
+
HIGH = "alta"
|
|
35
|
+
CRITICAL = "critica"
|
|
36
|
+
|
|
37
|
+
class EmotionalMirrorEngine:
|
|
38
|
+
"""Refleja el estado emocional del cliente para crear empatía."""
|
|
39
|
+
def mirror(self, sentiment: SentimentType, text: str) -> str:
|
|
40
|
+
# Implementation...
|
|
41
|
+
return ""
|
|
42
|
+
|
|
43
|
+
class ClientPersonaDetector:
|
|
44
|
+
"""Detecta el arquetipo del cliente (Nervioso, Decidido, Informado, etc.)."""
|
|
45
|
+
def detect(self, text: str, history: List[Dict]) -> str:
|
|
46
|
+
# Implementation...
|
|
47
|
+
return "neutral"
|
|
48
|
+
|
|
49
|
+
class ResponseQualityPatcher:
|
|
50
|
+
"""Parches específicos de calidad basados en auditoría de respuestas reales."""
|
|
51
|
+
def patch(self, response: str) -> str:
|
|
52
|
+
# Implementation...
|
|
53
|
+
return response
|
|
54
|
+
|
|
55
|
+
class AntiRobotFilter:
|
|
56
|
+
"""Filtra TODOS los patrones de bot antes de enviar al cliente."""
|
|
57
|
+
def filter(self, text: str) -> str:
|
|
58
|
+
# Implementation...
|
|
59
|
+
return text
|
|
60
|
+
|
|
61
|
+
class HyperHumanEngine:
|
|
62
|
+
"""Motor de validación de humanidad."""
|
|
63
|
+
def validate(self, text: str) -> Tuple[bool, str]:
|
|
64
|
+
# Implementation...
|
|
65
|
+
return True, ""
|