@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_demo_v2.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""conny_demo_v2.py — Ultra-realistic demo with Colombian personas and memory."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import random
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
API_URL = "http://localhost:8001/test"
|
|
17
|
+
MASTER_KEY = os.getenv("MASTER_API_KEY", "conny_master_2026_santiago")
|
|
18
|
+
|
|
19
|
+
DEMO_PERSONAS = {
|
|
20
|
+
"dona_carmen": {
|
|
21
|
+
"profile": "Mujer 65 años, Medellín, no tecnológica, formal",
|
|
22
|
+
"typing_wpm": 20,
|
|
23
|
+
"typos": True,
|
|
24
|
+
"messages": [
|
|
25
|
+
"buenas tardes quiero saber si hay cita para el medico",
|
|
26
|
+
"es que me duele el pecho hace dias",
|
|
27
|
+
"cuanto vale la consulta",
|
|
28
|
+
"y el doctor atiende mañana",
|
|
29
|
+
"no pues gracias mija que Dios la bendiga",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
"juan_ejecutivo": {
|
|
33
|
+
"profile": "Hombre 35 años, ocupado, directo, WhatsApp",
|
|
34
|
+
"typing_wpm": 60,
|
|
35
|
+
"typos": False,
|
|
36
|
+
"messages": [
|
|
37
|
+
"Hola! Cita cardio para mañana si hay?",
|
|
38
|
+
"Cuánto tarda la espera normalmente",
|
|
39
|
+
"Ok perfecto, quedamos así entonces 👍",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
"mama_preocupada": {
|
|
43
|
+
"profile": "Mamá 40 años, hijo enfermo, ansiosa",
|
|
44
|
+
"typing_wpm": 45,
|
|
45
|
+
"typos": False,
|
|
46
|
+
"messages": [
|
|
47
|
+
"Hola urgente mi hijo de 8 años tiene fiebre alta",
|
|
48
|
+
"Me pueden atender hoy??",
|
|
49
|
+
"Tiene 39.5 grados y lleva 2 días así",
|
|
50
|
+
"A qué hora hay pediatra",
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
"joven_curioso": {
|
|
54
|
+
"profile": "Hombre 22 años, primera vez, informal",
|
|
55
|
+
"typing_wpm": 70,
|
|
56
|
+
"typos": True,
|
|
57
|
+
"messages": [
|
|
58
|
+
"ey hola q tal",
|
|
59
|
+
"quiero saber si hacen limpieza facial",
|
|
60
|
+
"y eso duele? jaja",
|
|
61
|
+
"bueno listo agendame pa cuando haya",
|
|
62
|
+
"vale gracias bro",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
"abuela_tecnologica": {
|
|
66
|
+
"profile": "Mujer 72 años, nietos le enseñaron WhatsApp, muy educada",
|
|
67
|
+
"typing_wpm": 15,
|
|
68
|
+
"typos": True,
|
|
69
|
+
"messages": [
|
|
70
|
+
"Buenos dias señorita",
|
|
71
|
+
"Disculpe la molestia quisiera saber si puedo sacar una cita",
|
|
72
|
+
"Es para un control de tension arterial",
|
|
73
|
+
"Mi nombre es Rosa Elena Martinez",
|
|
74
|
+
"Muchas gracias señorita muy amable Dios la bendiga",
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
TYPO_MAP = {
|
|
80
|
+
"que": "q", "para": "pa", "por favor": "porfa", "está": "esta",
|
|
81
|
+
"también": "tambien", "después": "despues", "cuánto": "cuanto",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def inject_typos(text: str, rate: float = 0.05) -> str:
|
|
86
|
+
"""Inject realistic typos based on rate."""
|
|
87
|
+
words = text.split()
|
|
88
|
+
result = []
|
|
89
|
+
for w in words:
|
|
90
|
+
if random.random() < rate and len(w) > 3:
|
|
91
|
+
# Swap two adjacent chars
|
|
92
|
+
i = random.randint(0, len(w) - 2)
|
|
93
|
+
w = w[:i] + w[i + 1] + w[i] + w[i + 2:]
|
|
94
|
+
result.append(w)
|
|
95
|
+
return " ".join(result)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def simulate_typing_delay(text: str, wpm: int = 35) -> float:
|
|
99
|
+
"""Calculate realistic typing delay."""
|
|
100
|
+
chars_per_second = (wpm * 5) / 60
|
|
101
|
+
delay = len(text) / chars_per_second
|
|
102
|
+
return min(delay, 6.0)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def color(text, code):
|
|
106
|
+
return f"\033[{code}m{text}\033[0m"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def send_to_conny(message: str, chat_id: str) -> Dict:
|
|
110
|
+
"""Send message to Conny API."""
|
|
111
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
112
|
+
r = await client.post(
|
|
113
|
+
API_URL,
|
|
114
|
+
json={"message": message, "chat_id": chat_id},
|
|
115
|
+
headers={"X-Master-Key": MASTER_KEY, "Content-Type": "application/json"},
|
|
116
|
+
)
|
|
117
|
+
return r.json()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def run_demo(persona_key: str = "dona_carmen", instance_id: str = "default"):
|
|
121
|
+
"""Run a full multi-turn demo with a persona."""
|
|
122
|
+
persona = DEMO_PERSONAS.get(persona_key)
|
|
123
|
+
if not persona:
|
|
124
|
+
print(f"Persona '{persona_key}' not found. Available: {list(DEMO_PERSONAS.keys())}")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
chat_id = f"demo_{persona_key}_{int(time.time())}"
|
|
128
|
+
|
|
129
|
+
print()
|
|
130
|
+
print(color("╔══════════════════════════════════════════════╗", "1;36"))
|
|
131
|
+
print(color("║ CONNY DEMO v2.0 — Simulación Realista ║", "1;36"))
|
|
132
|
+
print(color("╚══════════════════════════════════════════════╝", "1;36"))
|
|
133
|
+
print(f" Persona: {color(persona_key, '1')} — {persona['profile']}")
|
|
134
|
+
print(f" Instance: {instance_id}")
|
|
135
|
+
print(f" Velocidad: {persona['typing_wpm']} WPM | Typos: {'sí' if persona['typos'] else 'no'}")
|
|
136
|
+
print("─" * 50)
|
|
137
|
+
print()
|
|
138
|
+
|
|
139
|
+
for i, msg in enumerate(persona["messages"]):
|
|
140
|
+
# Simulate typing delay
|
|
141
|
+
delay = simulate_typing_delay(msg, persona["typing_wpm"])
|
|
142
|
+
if i > 0:
|
|
143
|
+
# Thinking time before next message
|
|
144
|
+
await asyncio.sleep(random.uniform(1.5, 4.0))
|
|
145
|
+
|
|
146
|
+
# Apply typos if persona has them
|
|
147
|
+
display_msg = msg
|
|
148
|
+
if persona["typos"] and random.random() < 0.3:
|
|
149
|
+
display_msg = inject_typos(msg, rate=0.08)
|
|
150
|
+
|
|
151
|
+
# Show user typing indicator
|
|
152
|
+
print(f" {color('...typing...', '90')}", end="\r")
|
|
153
|
+
await asyncio.sleep(min(delay, 3.0))
|
|
154
|
+
print(f" {color(f'[{persona_key.upper()}]', '1;33')} {display_msg}")
|
|
155
|
+
|
|
156
|
+
# Get Conny's response
|
|
157
|
+
try:
|
|
158
|
+
result = await send_to_conny(display_msg, chat_id)
|
|
159
|
+
bubbles = result.get("bubbles", [result.get("response", "error")])
|
|
160
|
+
# Simulate Conny "thinking"
|
|
161
|
+
await asyncio.sleep(random.uniform(1.0, 2.5))
|
|
162
|
+
for bubble in bubbles:
|
|
163
|
+
print(f" {color('[CONNY]', '1;35')} {bubble}")
|
|
164
|
+
if len(bubbles) > 1:
|
|
165
|
+
await asyncio.sleep(random.uniform(0.5, 1.5))
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f" {color('[ERROR]', '31')} {e}")
|
|
168
|
+
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
print("─" * 50)
|
|
172
|
+
print(color(" Demo completada.", "90"))
|
|
173
|
+
print(f" {len(persona['messages'])} turnos | Chat ID: {chat_id}")
|
|
174
|
+
print()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def run_all_demos(instance_id: str = "default"):
|
|
178
|
+
"""Run all demo personas sequentially."""
|
|
179
|
+
for persona_key in DEMO_PERSONAS:
|
|
180
|
+
await run_demo(persona_key, instance_id)
|
|
181
|
+
print("\n" + "═" * 50 + "\n")
|
|
182
|
+
await asyncio.sleep(2)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def main():
|
|
186
|
+
import argparse
|
|
187
|
+
|
|
188
|
+
parser = argparse.ArgumentParser(description="Conny Demo v2 — Realistic Colombian personas")
|
|
189
|
+
parser.add_argument("--persona", "-p", default="dona_carmen",
|
|
190
|
+
choices=list(DEMO_PERSONAS.keys()) + ["all"],
|
|
191
|
+
help="Which persona to simulate")
|
|
192
|
+
parser.add_argument("--instance", "-i", default="default", help="Instance ID")
|
|
193
|
+
parser.add_argument("--list", "-l", action="store_true", help="List available personas")
|
|
194
|
+
args = parser.parse_args()
|
|
195
|
+
|
|
196
|
+
if args.list:
|
|
197
|
+
print("Available demo personas:")
|
|
198
|
+
for key, p in DEMO_PERSONAS.items():
|
|
199
|
+
print(f" {key:20s} — {p['profile']}")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
if args.persona == "all":
|
|
203
|
+
asyncio.run(run_all_demos(args.instance))
|
|
204
|
+
else:
|
|
205
|
+
asyncio.run(run_demo(args.persona, args.instance))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""conny_demo_voice.py — Voice cascade with ElevenLabs for demo mode."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("conny.demo_voice")
|
|
13
|
+
|
|
14
|
+
# Cascade of API keys — if first fails, try second
|
|
15
|
+
API_KEYS = [
|
|
16
|
+
os.getenv("ELEVENLABS_API_KEY", "sk_e200c93fe4787b44dae55fdfe4e938d1f07ebbd2b0a67bb7"),
|
|
17
|
+
os.getenv("ELEVENLABS_API_KEY_2", "sk_199e6c73c2adbdeedd746de08fbb3ec7e9067259c7210c50"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Female voices ranked by how natural/warm they sound in Spanish
|
|
21
|
+
# All free tier, tested for WhatsApp voice note quality
|
|
22
|
+
VOICE_CASCADE: List[Tuple[str, str]] = [
|
|
23
|
+
("cgSgspJ2msm6clMCkdW9", "Jessica"), # Playful, Bright, Warm — young (best for Colombian)
|
|
24
|
+
("EXAVITQu4vr4xnSDxMaL", "Sarah"), # Mature, Confident — young (backup)
|
|
25
|
+
("FGY2WhTYpPnrIDTdsKH5", "Laura"), # Enthusiast, quirky — young (energetic)
|
|
26
|
+
("hpp4J3VqNfWAUOO0d1Us", "Bella"), # Professional, Warm — middle_aged (formal fallback)
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Default: Jessica (most natural for Colombian Spanish)
|
|
30
|
+
DEFAULT_VOICE = VOICE_CASCADE[0][0]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def generate_demo_audio(text: str, voice_idx: int = 0) -> Optional[str]:
|
|
34
|
+
"""Generate audio with cascade: try key1 → key2, voice1 → voice2 → voice3."""
|
|
35
|
+
if not any(API_KEYS):
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
# Try each API key
|
|
39
|
+
for key in API_KEYS:
|
|
40
|
+
if not key:
|
|
41
|
+
continue
|
|
42
|
+
# Try voices in order
|
|
43
|
+
for vid, vname in VOICE_CASCADE[voice_idx:voice_idx + 2]:
|
|
44
|
+
result = await _try_generate(text, key, vid)
|
|
45
|
+
if result:
|
|
46
|
+
log.info(f"[demo_voice] generated with {vname} ({vid[:8]}...)")
|
|
47
|
+
return result
|
|
48
|
+
log.debug(f"[demo_voice] {vname} failed with key ...{key[-6:]}, trying next")
|
|
49
|
+
|
|
50
|
+
log.warning("[demo_voice] all keys/voices exhausted")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def _try_generate(text: str, api_key: str, voice_id: str) -> Optional[str]:
|
|
55
|
+
"""Single attempt to generate audio."""
|
|
56
|
+
try:
|
|
57
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
58
|
+
r = await client.post(
|
|
59
|
+
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
|
|
60
|
+
headers={
|
|
61
|
+
"xi-api-key": api_key,
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
},
|
|
64
|
+
json={
|
|
65
|
+
"text": text,
|
|
66
|
+
"model_id": "eleven_flash_v2_5",
|
|
67
|
+
"voice_settings": {
|
|
68
|
+
"stability": 0.35, # Lower = more natural variation
|
|
69
|
+
"similarity_boost": 0.70,
|
|
70
|
+
"style": 0.25, # Subtle expressiveness
|
|
71
|
+
"use_speaker_boost": True,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
if r.status_code == 200 and len(r.content) > 1000:
|
|
76
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
|
|
77
|
+
tmp.write(r.content)
|
|
78
|
+
tmp.close()
|
|
79
|
+
return tmp.name
|
|
80
|
+
elif r.status_code == 401:
|
|
81
|
+
log.debug(f"[demo_voice] key unauthorized")
|
|
82
|
+
return None
|
|
83
|
+
elif r.status_code == 429:
|
|
84
|
+
log.debug(f"[demo_voice] rate limited")
|
|
85
|
+
return None
|
|
86
|
+
else:
|
|
87
|
+
log.debug(f"[demo_voice] HTTP {r.status_code}: {r.text[:100]}")
|
|
88
|
+
return None
|
|
89
|
+
except Exception as e:
|
|
90
|
+
log.debug(f"[demo_voice] error: {e}")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def should_send_voice_in_demo(text: str, turn_number: int, has_business_name: bool) -> bool:
|
|
95
|
+
"""Decide when to send voice in demo — strategic moments only."""
|
|
96
|
+
# First greeting: ALWAYS voice (immediate wow)
|
|
97
|
+
if turn_number <= 1:
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
# After getting business name and entering character
|
|
101
|
+
if has_business_name and turn_number <= 4:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
# Patient simulation responses (showing the product)
|
|
105
|
+
if turn_number >= 3 and len(text) < 150:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
# Never spam voice — max every 3 turns
|
|
109
|
+
if turn_number > 5 and turn_number % 3 != 0:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Conversion hooks for demo closing
|
|
116
|
+
DEMO_CLOSING_TRIGGERS = [
|
|
117
|
+
"como activo", "cómo activo", "como lo contrato", "cómo contrato",
|
|
118
|
+
"cuanto cuesta", "cuánto cuesta", "precio", "planes",
|
|
119
|
+
"me interesa", "lo quiero", "activar", "contratar",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def detect_buying_intent(text: str) -> bool:
|
|
124
|
+
"""Detect if prospect wants to buy."""
|
|
125
|
+
return any(t in text.lower() for t in DEMO_CLOSING_TRIGGERS)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_closing_response() -> str:
|
|
129
|
+
"""When prospect wants to buy, close with Santiago's contact."""
|
|
130
|
+
return (
|
|
131
|
+
"me alegra que te haya gustado la demo! |||"
|
|
132
|
+
" para activarlo en tu negocio, Santiago te explica todo: 3124348669 |||"
|
|
133
|
+
" la activación es rápida, en menos de 5 minutos ya estoy respondiendo tu WhatsApp"
|
|
134
|
+
)
|
package/conny_design.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""conny_design.py — Single source of truth for all visual elements."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
COLORS = {
|
|
5
|
+
"primary": "#b48ead", # Lavender/Purple
|
|
6
|
+
"secondary": "#81a1c1", # Soft Blue
|
|
7
|
+
"success": "#a3be8c", # Sage Green
|
|
8
|
+
"warning": "#ebcb8b", # Muted Yellow
|
|
9
|
+
"error": "#bf616a", # Soft Red
|
|
10
|
+
"dim": "#4c566a", # Slate Grey
|
|
11
|
+
"text": "#d8dee9", # Snow Storm White
|
|
12
|
+
"accent": "#88c0d0", # Frost Cyan
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
LOGO_FULL = """\
|
|
16
|
+
[#b48ead] ██████╗ ██████╗ ███╗ ██╗███╗ ██╗██╗ ██╗[/#b48ead]
|
|
17
|
+
[#a690b8]██╔════╝ ██╔═══██╗████╗ ██║████╗ ██║╚██╗ ██╔╝[/#a690b8]
|
|
18
|
+
[#9992c3]██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║ ╚████╔╝ [/#9992c3]
|
|
19
|
+
[#8b93ce]██║ ██║ ██║██║╚██╗██║██║╚██╗██║ ╚██╔╝ [/#8b93ce]
|
|
20
|
+
[#7d95d8]╚██████╗ ╚██████╔╝██║ ╚████║██║ ╚████║ ██║ [/#7d95d8]
|
|
21
|
+
[#6f97e3] ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝ ╚═╝ [/#6f97e3]"""
|
|
22
|
+
|
|
23
|
+
WORM_RESTING = """\
|
|
24
|
+
[#b48ead]◆[/#b48ead][#4c566a]█▓▒░[/#4c566a]╮
|
|
25
|
+
╰─╯"""
|
|
26
|
+
|
|
27
|
+
WORM_INLINE = "[#b48ead]◆[/#b48ead][dim]▓▒░[/dim]"
|
|
28
|
+
|
|
29
|
+
SEP = "[dim]─────────────────────────────────────────────────────[/dim]"
|
|
30
|
+
|
|
31
|
+
# Status icons - using standard circles and marks for universal compatibility
|
|
32
|
+
ICON_ONLINE = "[#a3be8c]●[/#a3be8c]"
|
|
33
|
+
ICON_OFFLINE = "[#bf616a]○[/#bf616a]"
|
|
34
|
+
ICON_WARN = "[#ebcb8b]![/#ebcb8b]"
|
|
35
|
+
ICON_OK = "[#a3be8c]✓[/#a3be8c]"
|
|
36
|
+
ICON_ERR = "[#bf616a]✕[/#bf616a]"
|
|
37
|
+
ICON_BRAND = "[#b48ead]✦[/#b48ead]"
|
|
38
|
+
|
|
39
|
+
# Category icons - professional and clean
|
|
40
|
+
ICON_CORE = "■"
|
|
41
|
+
ICON_BOT = "⬢"
|
|
42
|
+
ICON_INT = "▲"
|
|
43
|
+
ICON_OPS = "◆"
|