@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.
Files changed (175) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +369 -0
  5. package/brand-assets/A_dark_luxury_web_background_202605210700.jpeg +0 -0
  6. package/brand-assets/Conny.web.logo.png +0 -0
  7. package/brand-assets/Logo_Conny_Petalo_Claro.png +0 -0
  8. package/brand-assets/cl-nica-de-las-am-ricas/manifest.json +22 -0
  9. package/brand-assets/cl-nica-de-las-am-ricas/processed/business-identity.txt +11 -0
  10. package/brand-assets/cl-nica-de-las-am-ricas/raw/business-identity.txt +11 -0
  11. package/brand-assets/cl-nica-las-am-ricas/manifest.json +22 -0
  12. package/brand-assets/cl-nica-las-am-ricas/processed/business-identity.txt +11 -0
  13. package/brand-assets/cl-nica-las-am-ricas/raw/business-identity.txt +11 -0
  14. package/brand-assets/conny-demo/manifest.json +22 -0
  15. package/brand-assets/conny-demo/processed/business-identity.txt +7 -0
  16. package/brand-assets/conny-demo/raw/business-identity.txt +7 -0
  17. package/brand-assets/conny-logo.png +0 -0
  18. package/brand-assets/web.background.png +0 -0
  19. package/brand_assets.py +323 -0
  20. package/conny +28 -0
  21. package/conny-chat.py +579 -0
  22. package/conny-omni.py +3843 -0
  23. package/conny.py +113 -0
  24. package/conny_agents/__init__.py +1 -0
  25. package/conny_agents/agenda.py +1 -0
  26. package/conny_agents/captacion.py +1 -0
  27. package/conny_agents/conocimiento.py +1 -0
  28. package/conny_agents/escalacion.py +1 -0
  29. package/conny_agents/objeciones.py +1 -0
  30. package/conny_agents/seguimiento.py +1 -0
  31. package/conny_app.py +287 -0
  32. package/conny_audio.py +350 -0
  33. package/conny_audio_learn.py +84 -0
  34. package/conny_brain_v10.py +804 -0
  35. package/conny_bridge.py +656 -0
  36. package/conny_calendar.py +169 -0
  37. package/conny_cli.py +11784 -0
  38. package/conny_cli_bb.py +437 -0
  39. package/conny_commands.py +243 -0
  40. package/conny_config.py +215 -0
  41. package/conny_core/__init__.py +3 -0
  42. package/conny_core/conversation_engine.py +446 -0
  43. package/conny_core/first_turn_ops.py +287 -0
  44. package/conny_core/persona_registry.py +157 -0
  45. package/conny_core/prompt_ops.py +561 -0
  46. package/conny_cron.py +72 -0
  47. package/conny_demo_v2.py +209 -0
  48. package/conny_demo_voice.py +134 -0
  49. package/conny_design.py +43 -0
  50. package/conny_doctor.py +319 -0
  51. package/conny_domino.py +696 -0
  52. package/conny_generator.py +447 -0
  53. package/conny_google_auth.py +159 -0
  54. package/conny_i18n.py +619 -0
  55. package/conny_init.py +509 -0
  56. package/conny_integrations/__init__.py +4 -0
  57. package/conny_integrations/llm.py +1 -0
  58. package/conny_integrations/vault.py +77 -0
  59. package/conny_integrations/whatsapp.py +1 -0
  60. package/conny_intelligence.py +65 -0
  61. package/conny_learning.py +154 -0
  62. package/conny_memory.py +243 -0
  63. package/conny_memory_engine.py +292 -0
  64. package/conny_nova_proxy.py +170 -0
  65. package/conny_nuke_robot_phrases.py +493 -0
  66. package/conny_pairing.py +253 -0
  67. package/conny_patch.py +291 -0
  68. package/conny_persona_cli.py +150 -0
  69. package/conny_router.py +308 -0
  70. package/conny_runtime_ops.py +271 -0
  71. package/conny_session.py +516 -0
  72. package/conny_skills/__init__.py +1 -0
  73. package/conny_skills/demo_mode.py +35 -0
  74. package/conny_skills/text_processing.py +1 -0
  75. package/conny_skills/tone_detection.py +1 -0
  76. package/conny_smart_features.py +333 -0
  77. package/conny_studio.py +161 -0
  78. package/conny_sync_fix.py +306 -0
  79. package/conny_tui.py +512 -0
  80. package/conny_tui_select.py +202 -0
  81. package/conny_ultra_config.py +411 -0
  82. package/conny_uncertainty.py +174 -0
  83. package/conny_utils.py +87 -0
  84. package/conny_voice.py +156 -0
  85. package/conny_voice_engine.py +124 -0
  86. package/conny_web_search.py +66 -0
  87. package/conny_weekly_report.py +85 -0
  88. package/conny_worm.py +88 -0
  89. package/core/__init__.py +25 -0
  90. package/ecosystem.config.js +24 -0
  91. package/fix_init.py +27 -0
  92. package/install.sh +78 -0
  93. package/knowledge_base.py +330 -0
  94. package/nova/rules/default.yaml +37 -0
  95. package/nova_bridge.py +509 -0
  96. package/npm/conny.js +471 -0
  97. package/package.json +102 -0
  98. package/personas/conny/base/default.yaml +35 -0
  99. package/personas/conny/base/estetica_whatsapp.yaml +36 -0
  100. package/requirements.txt +14 -0
  101. package/run.sh +47 -0
  102. package/search.py +465 -0
  103. package/smart_handoff.py +1150 -0
  104. package/src/__init__.py +0 -0
  105. package/src/conny/__init__.py +0 -0
  106. package/src/conny/admin/__init__.py +0 -0
  107. package/src/conny/admin/api.py +234 -0
  108. package/src/conny/admin/dashboard.py +772 -0
  109. package/src/conny/api/__init__.py +0 -0
  110. package/src/conny/api/routes.py +8851 -0
  111. package/src/conny/brain/__init__.py +15 -0
  112. package/src/conny/brain/engine.py +804 -0
  113. package/src/conny/brain/learning.py +154 -0
  114. package/src/conny/brain/memory.py +324 -0
  115. package/src/conny/brain/smart_features.py +333 -0
  116. package/src/conny/brain/uncertainty.py +167 -0
  117. package/src/conny/channels/__init__.py +0 -0
  118. package/src/conny/channels/audio.py +316 -0
  119. package/src/conny/channels/cli.py +11795 -0
  120. package/src/conny/channels/logo_art.py +11 -0
  121. package/src/conny/channels/voice.py +156 -0
  122. package/src/conny/core/__init__.py +0 -0
  123. package/src/conny/core/config.py +215 -0
  124. package/src/conny/core/cron.py +72 -0
  125. package/src/conny/core/messenger.py +563 -0
  126. package/src/conny/core/router.py +297 -0
  127. package/src/conny/core/session.py +312 -0
  128. package/src/conny/demo/__init__.py +0 -0
  129. package/src/conny/demo/handler.py +3110 -0
  130. package/src/conny/integrations/__init__.py +19 -0
  131. package/src/conny/integrations/calendar.py +169 -0
  132. package/src/conny/integrations/knowledge.py +312 -0
  133. package/src/conny/integrations/search.py +66 -0
  134. package/src/conny/personas/__init__.py +0 -0
  135. package/src/conny/personas/generator.py +447 -0
  136. package/src/conny/production/__init__.py +0 -0
  137. package/src/conny/production/domino.py +696 -0
  138. package/src/conny/production/guard.py +550 -0
  139. package/src/conny/production/handoff.py +1150 -0
  140. package/src/conny/production/monitor.py +353 -0
  141. package/src/conny/utils/__init__.py +2 -0
  142. package/src/conny/utils/helpers.py +75 -0
  143. package/src/conny/utils/i18n.py +619 -0
  144. package/src/core/admin_engines.py +772 -0
  145. package/src/core/globals.py +11845 -0
  146. package/src/core/orchestrator.py +273 -0
  147. package/src/core/production_monitor.py +353 -0
  148. package/src/core/runtime.py +5487 -0
  149. package/src/domain/onboarding_flow.py +230 -0
  150. package/src/domain/prompts/__init__.py +1 -0
  151. package/src/domain/prompts/prospect_pitch.py +282 -0
  152. package/src/domain/send_guard.py +636 -0
  153. package/src/domain/swarm/queen.py +96 -0
  154. package/src/infrastructure/llm_providers/engine.py +487 -0
  155. package/src/interfaces/mcp_server.py +73 -0
  156. package/src/interfaces/nova_bridge.py +58 -0
  157. package/src/interfaces/web/admin_api.py +1379 -0
  158. package/src/interfaces/web/app.py +9408 -0
  159. package/src/interfaces/web/demo_handler.py +3450 -0
  160. package/src/interfaces/web/static/generate_avatars.py +46 -0
  161. package/v7/__init__.py +46 -0
  162. package/v7/agents/__init__.py +46 -0
  163. package/v7/agents/agenda.py +77 -0
  164. package/v7/agents/base.py +216 -0
  165. package/v7/agents/captacion.py +60 -0
  166. package/v7/agents/conocimiento.py +69 -0
  167. package/v7/agents/escalacion.py +83 -0
  168. package/v7/agents/objeciones.py +109 -0
  169. package/v7/agents/seguimiento.py +71 -0
  170. package/v7/memory/__init__.py +46 -0
  171. package/v7/memory/patient_profile.py +200 -0
  172. package/v7/orchestrator.py +275 -0
  173. package/v7/postprocess.py +127 -0
  174. package/v7/router.py +239 -0
  175. package/verify_conversation_impl.py +48 -0
@@ -0,0 +1,316 @@
1
+ """Audio transcription handler with multi-provider fallback (Gemini, Groq, OpenRouter)."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import logging
6
+ import os
7
+ import tempfile
8
+ from typing import Optional, Tuple
9
+
10
+ import httpx
11
+
12
+ # TODO: replace with `from ..core.config import Config` once core.config exists
13
+ from conny_config import Config
14
+
15
+ log = logging.getLogger("conny.audio")
16
+
17
+
18
+ class AudioHandler:
19
+ """
20
+ Audio transcription handler with multiple providers:
21
+ - Gemini 2.0 Flash (primary, best context comprehension)
22
+ - Groq Whisper (fallback 1)
23
+ - OpenRouter Whisper (fallback 2)
24
+ """
25
+
26
+ def __init__(self):
27
+ self._audio_cache: dict = {}
28
+
29
+ def _audio_suffix(self, mime: str) -> str:
30
+ """Map mime type to file extension."""
31
+ mapping = {
32
+ "audio/ogg": ".ogg",
33
+ "audio/oga": ".ogg",
34
+ "audio/opus": ".ogg",
35
+ "audio/mp3": ".mp3",
36
+ "audio/mpeg": ".mp3",
37
+ "audio/wav": ".wav",
38
+ "audio/x-wav": ".wav",
39
+ "audio/mp4": ".m4a",
40
+ "audio/x-m4a": ".m4a",
41
+ "audio/webm": ".webm",
42
+ }
43
+ return mapping.get((mime or "").lower(), ".ogg")
44
+
45
+ async def transcribe_audio(
46
+ self,
47
+ file_id: str,
48
+ platform: str = "telegram",
49
+ wa_media_id: str = None,
50
+ ) -> str:
51
+ """
52
+ Transcribe audio via Gemini 2.0 Flash (native) with Whisper fallback.
53
+
54
+ Args:
55
+ file_id: Audio file ID
56
+ platform: Source platform (telegram, whatsapp, whatsapp_cloud)
57
+ wa_media_id: WhatsApp Cloud media ID (if applicable)
58
+
59
+ Returns:
60
+ Transcribed text or error message
61
+ """
62
+ audio_bytes, mime_type = None, "audio/ogg"
63
+
64
+ try:
65
+ async with httpx.AsyncClient(timeout=30.0) as client:
66
+ if platform == "telegram":
67
+ r = await client.get(
68
+ f"https://api.telegram.org/bot{Config.TELEGRAM_TOKEN}/getFile",
69
+ params={"file_id": file_id},
70
+ )
71
+ fp = r.json()["result"]["file_path"]
72
+ ext = fp.rsplit(".", 1)[-1].lower() if "." in fp else "ogg"
73
+ mime_type = {
74
+ "ogg": "audio/ogg", "mp3": "audio/mp3", "wav": "audio/wav",
75
+ "m4a": "audio/mp4", "oga": "audio/ogg", "opus": "audio/ogg",
76
+ }.get(ext, "audio/ogg")
77
+ ar = await client.get(
78
+ f"https://api.telegram.org/file/bot{Config.TELEGRAM_TOKEN}/{fp}"
79
+ )
80
+ audio_bytes = ar.content
81
+
82
+ elif platform == "whatsapp_cloud" and wa_media_id:
83
+ mr = await client.get(
84
+ f"https://graph.facebook.com/v20.0/{wa_media_id}",
85
+ headers={"Authorization": f"Bearer {Config.WA_ACCESS_TOKEN}"},
86
+ )
87
+ url = mr.json().get("url", "")
88
+ if url:
89
+ dl = await client.get(
90
+ url,
91
+ headers={"Authorization": f"Bearer {Config.WA_ACCESS_TOKEN}"},
92
+ )
93
+ audio_bytes, mime_type = (
94
+ dl.content,
95
+ mr.json().get("mime_type", "audio/ogg"),
96
+ )
97
+
98
+ # WhatsApp Bridge (Baileys) — audio base64 inline
99
+ if platform == "whatsapp" and file_id.startswith("wa_b64:"):
100
+ try:
101
+ _, mime_part, b64_data = file_id.split(":", 2)
102
+ mime_type = mime_part or "audio/ogg"
103
+ audio_bytes = base64.b64decode(b64_data)
104
+ except Exception:
105
+ return "[no pude escuchar, puedes escribirlo?]"
106
+
107
+ if not audio_bytes:
108
+ return "[no pude escuchar, puedes escribirlo?]"
109
+
110
+ # Try transcription with Gemini 2.0 Flash
111
+ result = await self._transcribe_gemini(audio_bytes, mime_type)
112
+ if result:
113
+ return result
114
+
115
+ # Fallback 1: Groq Whisper
116
+ result = await self._transcribe_groq_whisper(audio_bytes, mime_type)
117
+ if result:
118
+ return result
119
+
120
+ # Fallback 2: OpenRouter Whisper
121
+ result = await self._transcribe_openrouter_whisper(audio_bytes, mime_type)
122
+ if result:
123
+ return result
124
+
125
+ return "[no se pudo transcribir el audio]"
126
+
127
+ except Exception as e:
128
+ log.error(f"[audio] Error: {e}", exc_info=True)
129
+ return "[no pude escuchar, puedes escribirlo?]"
130
+
131
+ async def _transcribe_gemini(self, audio_bytes: bytes, mime_type: str) -> Optional[str]:
132
+ """Transcribe using Gemini 2.0 Flash."""
133
+ effective_mime = "audio/ogg" if mime_type in ("audio/oga", "audio/opus") else mime_type
134
+
135
+ gemini_keys = Config.GEMINI_API_KEYS or [
136
+ k for k in [
137
+ Config.GEMINI_API_KEY,
138
+ Config.GEMINI_API_KEY_2,
139
+ Config.GEMINI_API_KEY_3,
140
+ Config.GEMINI_API_KEY_4,
141
+ Config.GEMINI_API_KEY_5,
142
+ Config.GEMINI_API_KEY_6,
143
+ ] if k
144
+ ]
145
+
146
+ for gkey in gemini_keys:
147
+ try:
148
+ b64 = base64.b64encode(audio_bytes).decode()
149
+ payload = {
150
+ "contents": [{
151
+ "parts": [
152
+ {"inline_data": {"mime_type": effective_mime, "data": b64}},
153
+ {
154
+ "text": "Transcribe este mensaje de voz en español exactamente "
155
+ "como se dice. Devuelve SOLO el texto transcrito, sin "
156
+ "comillas ni comentarios. Mantén el tono coloquial tal como se habla."
157
+ },
158
+ ]
159
+ }],
160
+ "generationConfig": {"temperature": 0.0, "maxOutputTokens": 500},
161
+ }
162
+
163
+ async with httpx.AsyncClient(timeout=25.0) as client:
164
+ resp = await client.post(
165
+ f"https://generativelanguage.googleapis.com/v1beta/models/"
166
+ f"gemini-2.5-flash:generateContent?key={gkey}",
167
+ json=payload,
168
+ )
169
+
170
+ if resp.status_code == 200:
171
+ parts = resp.json().get("candidates", [{}])[0].get("content", {}).get("parts", [{}])
172
+ t = parts[0].get("text", "").strip() if parts else ""
173
+ if t and len(t) > 2:
174
+ log.info(f"[audio] Gemini OK: {t[:80]}")
175
+ return t
176
+ elif resp.status_code in (408, 429, 500, 502, 503, 504):
177
+ continue # rotate key
178
+ else:
179
+ log.warning(f"[audio] Gemini {resp.status_code}: {resp.text[:120]}")
180
+ continue
181
+
182
+ except Exception as eg:
183
+ log.warning(f"[audio] Gemini error: {eg}")
184
+ continue
185
+
186
+ return None
187
+
188
+ async def _transcribe_groq_whisper(
189
+ self,
190
+ audio_bytes: bytes,
191
+ mime_type: str,
192
+ ) -> Optional[str]:
193
+ """Transcribe using Groq Whisper (fallback 1)."""
194
+ if not Config.GROQ_API_KEY:
195
+ return None
196
+
197
+ tmp_path = None
198
+ try:
199
+ suffix = self._audio_suffix(mime_type)
200
+ filename = f"audio{suffix}"
201
+
202
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
203
+ tmp.write(audio_bytes)
204
+ tmp_path = tmp.name
205
+
206
+ async with httpx.AsyncClient(timeout=60.0) as client:
207
+ with open(tmp_path, "rb") as f:
208
+ resp = await client.post(
209
+ "https://api.groq.com/openai/v1/audio/transcriptions",
210
+ headers={"Authorization": f"Bearer {Config.GROQ_API_KEY}"},
211
+ files={"file": (filename, f, mime_type)},
212
+ data={
213
+ "model": "whisper-large-v3-turbo",
214
+ "language": "es",
215
+ "response_format": "json",
216
+ "temperature": "0",
217
+ "prompt": "Transcribe este audio en español tal como se dice, sin comentarios adicionales.",
218
+ },
219
+ )
220
+
221
+ if tmp_path and os.path.exists(tmp_path):
222
+ os.unlink(tmp_path)
223
+
224
+ if resp.status_code == 200:
225
+ payload = resp.json()
226
+ t = (payload.get("text") or "").strip()
227
+ if t:
228
+ log.info(f"[audio] Groq Whisper OK: {t[:80]}")
229
+ return t
230
+ else:
231
+ log.warning(f"[audio] Groq Whisper {resp.status_code}: {resp.text[:160]}")
232
+
233
+ except Exception as eg:
234
+ log.warning(f"[audio] Groq Whisper error: {eg}")
235
+ if tmp_path and os.path.exists(tmp_path):
236
+ try:
237
+ os.unlink(tmp_path)
238
+ except Exception:
239
+ pass
240
+
241
+ return None
242
+
243
+ async def _transcribe_openrouter_whisper(
244
+ self,
245
+ audio_bytes: bytes,
246
+ mime_type: str,
247
+ ) -> Optional[str]:
248
+ """Transcribe using OpenRouter Whisper (fallback 2)."""
249
+ if not Config.OPENROUTER_API_KEY:
250
+ return None
251
+
252
+ tmp_path = None
253
+ try:
254
+ suffix = self._audio_suffix(mime_type)
255
+ filename = f"audio{suffix}"
256
+
257
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
258
+ tmp.write(audio_bytes)
259
+ tmp_path = tmp.name
260
+
261
+ async with httpx.AsyncClient(timeout=60.0) as client:
262
+ with open(tmp_path, "rb") as f:
263
+ resp = await client.post(
264
+ "https://openrouter.ai/api/v1/audio/transcriptions",
265
+ headers={"Authorization": f"Bearer {Config.OPENROUTER_API_KEY}"},
266
+ files={"file": (filename, f, mime_type)},
267
+ data={
268
+ "model": getattr(Config, "WHISPER_MODEL", "openai/whisper-large-v3"),
269
+ "language": "es",
270
+ },
271
+ )
272
+
273
+ if tmp_path and os.path.exists(tmp_path):
274
+ os.unlink(tmp_path)
275
+
276
+ if resp.status_code == 200:
277
+ t = resp.json().get("text", "").strip()
278
+ if t:
279
+ log.info(f"[audio] Whisper OK: {t[:80]}")
280
+ return t
281
+ else:
282
+ log.warning(f"[audio] OpenRouter Whisper {resp.status_code}: {resp.text[:160]}")
283
+
284
+ except Exception as ew:
285
+ log.warning(f"[audio] Whisper error: {ew}")
286
+ if tmp_path and os.path.exists(tmp_path):
287
+ try:
288
+ os.unlink(tmp_path)
289
+ except Exception:
290
+ pass
291
+
292
+ return None
293
+
294
+ def clear_cache(self) -> None:
295
+ """Clear the audio cache."""
296
+ self._audio_cache.clear()
297
+
298
+
299
+ async def transcribe_audio(
300
+ file_id: str,
301
+ platform: str = "telegram",
302
+ wa_media_id: str = None,
303
+ ) -> str:
304
+ """
305
+ Convenience function for audio transcription.
306
+
307
+ Args:
308
+ file_id: Audio file ID
309
+ platform: Source platform
310
+ wa_media_id: WhatsApp Cloud media ID
311
+
312
+ Returns:
313
+ Transcribed text or error message
314
+ """
315
+ handler = AudioHandler()
316
+ return await handler.transcribe_audio(file_id, platform, wa_media_id)