@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,11 @@
1
+ LOGO_ART_LINES = [
2
+ ' ',
3
+ ' \x1b[48;2;251;6;114m \x1b[0m\x1b[38;2;250;4;114m\x1b[48;2;255;4;133m▀\x1b[0m\x1b[38;2;251;4;135m\x1b[48;2;231;3;140m▀\x1b[0m\x1b[38;2;226;3;139m\x1b[48;2;233;4;168m▀\x1b[0m\x1b[48;2;202;3;162m \x1b[0m ',
4
+ ' \x1b[48;2;250;4;119m \x1b[0m\x1b[48;2;253;3;123m \x1b[0m\x1b[48;2;252;5;115m \x1b[0m\x1b[48;2;253;5;119m \x1b[0m \x1b[38;2;239;4;127m\x1b[48;2;220;3;137m▀\x1b[0m\x1b[38;2;247;4;146m\x1b[48;2;229;3;158m▀\x1b[0m\x1b[38;2;213;3;153m\x1b[48;2;198;3;162m▀\x1b[0m\x1b[38;2;211;4;178m\x1b[48;2;192;4;182m▀\x1b[0m\x1b[38;2;184;4;170m\x1b[48;2;172;3;175m▀\x1b[0m \x1b[48;2;175;3;183m \x1b[0m\x1b[48;2;165;4;189m \x1b[0m\x1b[48;2;150;6;199m \x1b[0m\x1b[48;2;134;5;198m \x1b[0m',
5
+ ' \x1b[38;2;250;3;118m\x1b[48;2;250;6;117m▀\x1b[0m\x1b[38;2;255;4;117m\x1b[48;2;255;4;124m▀\x1b[0m\x1b[38;2;251;4;109m\x1b[48;2;241;3;122m▀\x1b[0m\x1b[38;2;255;3;137m\x1b[48;2;226;3;138m▀\x1b[0m\x1b[38;2;231;3;142m\x1b[48;2;231;3;164m▀\x1b[0m\x1b[48;2;200;4;159m \x1b[0m\x1b[38;2;200;3;161m\x1b[48;2;184;7;169m▀\x1b[0m\x1b[38;2;206;4;194m\x1b[48;2;196;5;198m▀\x1b[0m\x1b[38;2;170;4;181m\x1b[48;2;160;5;182m▀\x1b[0m\x1b[48;2;165;4;181m \x1b[0m\x1b[38;2;172;3;182m\x1b[48;2;172;5;203m▀\x1b[0m\x1b[38;2;177;5;204m\x1b[48;2;141;6;195m▀\x1b[0m\x1b[38;2;146;6;196m\x1b[48;2;129;7;204m▀\x1b[0m\x1b[38;2;133;7;203m\x1b[48;2;124;11;227m▀\x1b[0m\x1b[38;2;125;9;215m\x1b[48;2;106;11;211m▀\x1b[0m',
6
+ ' \x1b[38;2;244;5;122m▀\x1b[0m\x1b[38;2;235;4;139m▀\x1b[0m\x1b[38;2;230;3;161m\x1b[48;2;208;6;161m▀\x1b[0m\x1b[38;2;221;3;180m\x1b[48;2;190;6;164m▀\x1b[0m\x1b[38;2;192;4;165m▀\x1b[0m \x1b[38;2;156;6;187m▀\x1b[0m\x1b[38;2;156;5;217m\x1b[48;2;131;8;198m▀\x1b[0m\x1b[38;2;136;7;218m\x1b[48;2;124;10;208m▀\x1b[0m\x1b[38;2;118;9;213m▀\x1b[0m\x1b[38;2;108;10;215m▀\x1b[0m ',
7
+ ' \x1b[48;2;226;3;145m \x1b[0m\x1b[48;2;216;3;154m \x1b[0m\x1b[38;2;212;2;160m\x1b[48;2;213;3;173m▀\x1b[0m\x1b[38;2;193;5;166m\x1b[48;2;201;4;188m▀\x1b[0m\x1b[48;2;170;3;177m \x1b[0m \x1b[48;2;127;6;200m \x1b[0m\x1b[38;2;128;8;203m\x1b[48;2;135;8;225m▀\x1b[0m\x1b[38;2;120;9;215m\x1b[48;2;116;12;228m▀\x1b[0m\x1b[48;2;93;14;222m \x1b[0m\x1b[48;2;73;18;228m \x1b[0m ',
8
+ ' \x1b[38;2;223;2;137m\x1b[48;2;213;3;147m▀\x1b[0m\x1b[38;2;237;3;163m\x1b[48;2;205;3;162m▀\x1b[0m\x1b[38;2;199;3;159m\x1b[48;2;187;3;167m▀\x1b[0m\x1b[38;2;184;3;168m\x1b[48;2;190;4;194m▀\x1b[0m\x1b[38;2;185;4;194m\x1b[48;2;166;4;183m▀\x1b[0m\x1b[38;2;163;4;179m▀\x1b[0m\x1b[38;2;155;5;185m\x1b[48;2;154;4;188m▀\x1b[0m\x1b[38;2;165;7;216m\x1b[48;2;157;6;219m▀\x1b[0m\x1b[38;2;133;7;194m\x1b[48;2;125;7;203m▀\x1b[0m\x1b[38;2;121;8;203m▀\x1b[0m\x1b[38;2;120;11;227m\x1b[48;2;95;12;215m▀\x1b[0m\x1b[38;2;91;13;214m\x1b[48;2;87;17;242m▀\x1b[0m\x1b[38;2;78;16;222m\x1b[48;2;64;19;229m▀\x1b[0m\x1b[38;2;67;22;249m\x1b[48;2;48;24;237m▀\x1b[0m\x1b[38;2;49;21;234m\x1b[48;2;35;27;248m▀\x1b[0m',
9
+ ' \x1b[38;2;201;4;156m▀\x1b[0m\x1b[38;2;198;3;171m▀\x1b[0m\x1b[38;2;180;4;177m▀\x1b[0m\x1b[38;2;171;5;183m▀\x1b[0m \x1b[38;2;152;3;186m\x1b[48;2;141;4;190m▀\x1b[0m\x1b[38;2;150;5;200m\x1b[48;2;142;6;210m▀\x1b[0m\x1b[38;2;125;7;199m\x1b[48;2;114;9;207m▀\x1b[0m\x1b[38;2;115;11;218m\x1b[48;2;103;13;227m▀\x1b[0m\x1b[38;2;102;10;212m\x1b[48;2;88;13;215m▀\x1b[0m \x1b[38;2;71;19;229m▀\x1b[0m\x1b[38;2;52;22;238m▀\x1b[0m\x1b[38;2;34;27;246m▀\x1b[0m\x1b[38;2;25;29;243m▀\x1b[0m',
10
+ ' \x1b[38;2;129;7;197m▀\x1b[0m\x1b[38;2;127;8;217m\x1b[48;2;105;9;207m▀\x1b[0m\x1b[38;2;101;11;211m\x1b[48;2;96;14;231m▀\x1b[0m\x1b[38;2;90;16;233m\x1b[48;2;75;16;218m▀\x1b[0m\x1b[38;2;74;17;217m▀\x1b[0m ',
11
+ ]
@@ -0,0 +1,156 @@
1
+ """Humanization engine — post-processes LLM responses to eliminate robot patterns."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import random
6
+ import logging
7
+ from typing import Optional, Dict, List
8
+
9
+ log = logging.getLogger("conny.voice")
10
+
11
+ # Patterns that MUST be removed/replaced from any response
12
+ ROBOT_PATTERNS = [
13
+ (re.compile(r"como (IA|inteligencia artificial|asistente virtual|bot)", re.I), ""),
14
+ (re.compile(r"no (tengo|tenemos) (la )?capacidad", re.I), "no manejo eso ahora mismo"),
15
+ (re.compile(r"está fuera de mi (alcance|capacidades)", re.I), "eso tendría que verificarlo"),
16
+ (re.compile(r"no (puedo|podemos) (procesar|entender)", re.I), "no tengo esa información ahora"),
17
+ (re.compile(r"^(Hola|Buenos días|Buenas tardes)[,!]?\s*(Soy|Me llamo)\s*Conny", re.I), ""),
18
+ (re.compile(r"¡?Por supuesto[,!]?\s*", re.I), ""),
19
+ (re.compile(r"¡?Claro que sí[,!]?\s*", re.I), ""),
20
+ (re.compile(r"¡?Con gusto[,!]?\s*", re.I), ""),
21
+ (re.compile(r"¡+", re.I), ""), # Remove excessive exclamation
22
+ (re.compile(r"^\s*¡\s*", re.I), ""),
23
+ (re.compile(r",?\s*tu\s+asistente\s+virtual[.,]?\s*", re.I), ". "),
24
+ (re.compile(r"(?:como|en mi rol de) asistente virtual", re.I), ""),
25
+ (re.compile(r"(?:soy\s+)?(?:tu\s+)?asistente\s+virtual", re.I), ""),
26
+ (re.compile(r"estoy aquí para (servirte|atenderte)", re.I), ""),
27
+ (re.compile(r"no dudes en", re.I), ""),
28
+ (re.compile(r"estaré encantad[ao] de", re.I), ""),
29
+ (re.compile(r"será un placer", re.I), ""),
30
+ ]
31
+
32
+ # Natural fillers by sector
33
+ SECTOR_FILLERS = {
34
+ "estetica": ["con mucho gusto", "perfecto", "listo", "dale"],
35
+ "salud": ["con gusto", "perfecto", "entendido", "listo"],
36
+ "restaurante": ["dale", "perfecto", "listo", "va"],
37
+ "retail": ["dale", "listo", "perfecto", "genial"],
38
+ "default": ["con gusto", "perfecto", "listo", "entendido"],
39
+ }
40
+
41
+ # Thinking block to prepend to system prompts
42
+ THINKING_BLOCK_ES = """INSTRUCCIÓN INTERNA (no mostrar al usuario):
43
+ Antes de responder, piensa en silencio:
44
+ 1. ¿Qué quiere realmente este usuario? (intención real)
45
+ 2. ¿Qué información tengo disponible para responderle?
46
+ 3. ¿Hay alguna ambigüedad? ¿Necesito pedir aclaración?
47
+ 4. ¿Cómo lo diría una recepcionista colombiana real, cálida, natural?
48
+ 5. ¿Mi respuesta suena robótica? Si sí, reescríbela.
49
+
50
+ SOLO después de pensar, escribe la respuesta al usuario.
51
+ NUNCA menciones este proceso. NUNCA uses frases como "como IA" o "no tengo la capacidad"."""
52
+
53
+ THINKING_BLOCK_EN = """INTERNAL INSTRUCTION (do not show to user):
54
+ Before responding, think silently:
55
+ 1. What does this user actually want?
56
+ 2. What information do I have to answer them?
57
+ 3. Is there any ambiguity? Should I ask for clarification?
58
+ 4. How would a real, warm receptionist say this?
59
+ 5. Does my response sound robotic? If yes, rewrite it.
60
+
61
+ ONLY after thinking, write the response to the user.
62
+ NEVER mention this process."""
63
+
64
+
65
+ class ConnyVoice:
66
+ """Post-processes LLM output to sound human."""
67
+
68
+ def __init__(self, sector: str = "default", tone: str = "warm"):
69
+ self.sector = sector
70
+ self.tone = tone
71
+ self._fillers = SECTOR_FILLERS.get(sector, SECTOR_FILLERS["default"])
72
+
73
+ def humanize(self, text: str, persona_override: Optional[Dict] = None) -> str:
74
+ """Main post-processing pipeline."""
75
+ if not text or not text.strip():
76
+ return text
77
+
78
+ result = text
79
+
80
+ # 1. Remove robot patterns
81
+ for pattern, replacement in ROBOT_PATTERNS:
82
+ result = pattern.sub(replacement, result)
83
+
84
+ # 2. Clean up whitespace artifacts
85
+ result = re.sub(r'\s{2,}', ' ', result)
86
+ result = re.sub(r'\n{3,}', '\n\n', result)
87
+ result = result.strip()
88
+
89
+ # 3. Limit exclamation marks (max 1 per response)
90
+ excl_count = result.count('!')
91
+ if excl_count > 1:
92
+ # Keep only the first one
93
+ parts = result.split('!')
94
+ result = parts[0] + '!' + ''.join(parts[1:])
95
+
96
+ # 4. Response must NOT start with bot name
97
+ result = re.sub(r'^Conny[,:.]?\s*', '', result, flags=re.I)
98
+
99
+ # 5. Capitalize first letter
100
+ if result and result[0].islower():
101
+ result = result[0].upper() + result[1:]
102
+
103
+ # 6. Apply persona overrides if present
104
+ if persona_override:
105
+ forbidden = persona_override.get("forbidden_topics", [])
106
+ for topic in forbidden:
107
+ if topic.lower() in result.lower():
108
+ result = re.sub(re.escape(topic), "[tema no disponible]", result, flags=re.I)
109
+
110
+ return result
111
+
112
+ def inject_thinking_block(self, system_prompt: str, lang: str = "es", thinking_budget: int = 0) -> str:
113
+ """Prepend chain-of-thought instruction to system prompt."""
114
+ block = THINKING_BLOCK_ES if lang == "es" else THINKING_BLOCK_EN
115
+ return block + "\n\n" + system_prompt
116
+
117
+ def split_long_response(self, text: str, max_chars: int = 300) -> List[str]:
118
+ """Split long responses into multiple WhatsApp-style messages."""
119
+ if len(text) <= max_chars:
120
+ return [text]
121
+
122
+ # Split on sentence boundaries
123
+ sentences = re.split(r'(?<=[.!?])\s+', text)
124
+ bubbles = []
125
+ current = ""
126
+
127
+ for sentence in sentences:
128
+ if len(current) + len(sentence) + 1 > max_chars and current:
129
+ bubbles.append(current.strip())
130
+ current = sentence
131
+ else:
132
+ current = (current + " " + sentence).strip() if current else sentence
133
+
134
+ if current:
135
+ bubbles.append(current.strip())
136
+
137
+ return bubbles if bubbles else [text]
138
+
139
+ def check_robot_patterns(self, text: str) -> List[str]:
140
+ """Return list of robot patterns found (for testing/monitoring)."""
141
+ found = []
142
+ for pattern, _ in ROBOT_PATTERNS:
143
+ if pattern.search(text):
144
+ found.append(pattern.pattern)
145
+ return found
146
+
147
+ @staticmethod
148
+ def get_thinking_params(provider: str, thinking_budget: int = 0) -> Dict:
149
+ """Get provider-specific thinking parameters."""
150
+ if provider == "claude" and thinking_budget > 0:
151
+ return {"thinking": {"type": "enabled", "budget_tokens": thinking_budget}}
152
+ return {}
153
+
154
+
155
+ # Default instance
156
+ voice = ConnyVoice()
File without changes
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ conny_config.py — All constants, templates and string literals from conny.py
4
+ Extracted for Phase 3 split.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Dict, List, Set, Tuple
12
+
13
+ _CONNY_HOME = os.getenv("CONNY_HOME", str(Path.home() / ".conny"))
14
+
15
+
16
+ class Config:
17
+ """Configuración centralizada con validación."""
18
+
19
+ TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "")
20
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
21
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
22
+ GEMINI_API_KEY_2 = os.getenv("GEMINI_API_KEY_2", "")
23
+ GEMINI_API_KEY_3 = os.getenv("GEMINI_API_KEY_3", "")
24
+ GEMINI_API_KEY_4 = os.getenv("GEMINI_API_KEY_4", "")
25
+ GEMINI_API_KEY_5 = os.getenv("GEMINI_API_KEY_5", "")
26
+ GEMINI_API_KEY_6 = os.getenv("GEMINI_API_KEY_6", "")
27
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
28
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
29
+ ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
30
+ BRAVE_API_KEY = os.getenv("BRAVE_API_KEY", "")
31
+ APIFY_API_KEY = os.getenv("APIFY_API_KEY", "")
32
+ SERP_API_KEY = os.getenv("SERP_API_KEY", "")
33
+ CALENDLY_LINK = os.getenv("CALENDLY_LINK", "")
34
+ GCAL_ACCESS_TOKEN = os.getenv("GCAL_ACCESS_TOKEN", "")
35
+ GCAL_REFRESH_TOKEN = os.getenv("GCAL_REFRESH_TOKEN", "")
36
+ GCAL_CLIENT_ID = os.getenv("GCAL_CLIENT_ID", "")
37
+ GCAL_CLIENT_SECRET = os.getenv("GCAL_CLIENT_SECRET", "")
38
+ GCAL_CALENDAR_ID = os.getenv("GCAL_CALENDAR_ID", "primary")
39
+ META_APP_ID = os.getenv("META_APP_ID", "")
40
+ META_APP_SECRET = os.getenv("META_APP_SECRET", "")
41
+ NOVA_URL = os.getenv("NOVA_URL", "http://localhost:9003")
42
+ NOVA_TOKEN = os.getenv("NOVA_TOKEN", "")
43
+ NOVA_API_KEY = os.getenv("NOVA_API_KEY", "")
44
+ NOVA_ENABLED = os.getenv("NOVA_ENABLED", "false").lower() == "true"
45
+ WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "conny_ultra_5")
46
+ BASE_URL = os.getenv("BASE_URL", "")
47
+ TELEGRAM_SHARED = os.getenv("TELEGRAM_SHARED", "false").lower() == "true"
48
+ TELEGRAM_SHARED_ROUTER = os.getenv("TELEGRAM_SHARED_ROUTER", "false").lower() == "true"
49
+ TELEGRAM_SHARED_SECRET = os.getenv("TELEGRAM_SHARED_SECRET", "conny_shared_telegram")
50
+ TELEGRAM_DEFAULT_INSTANCE = os.getenv("TELEGRAM_DEFAULT_INSTANCE", "").strip()
51
+ TELEGRAM_SHARED_ROUTES_PATH = os.getenv(
52
+ "TELEGRAM_SHARED_ROUTES_PATH",
53
+ str(Path(_CONNY_HOME) / "shared_telegram_routes.json"),
54
+ )
55
+ TELEGRAM_SHARED_INSTANCES_DIR = os.getenv(
56
+ "TELEGRAM_SHARED_INSTANCES_DIR",
57
+ str(Path(_CONNY_HOME) / "instances"),
58
+ )
59
+ PLATFORM = os.getenv("PLATFORM", "telegram")
60
+ WHATSAPP_BRIDGE_URL = os.getenv("WHATSAPP_BRIDGE_URL", "http://localhost:3000")
61
+ SECTOR = os.getenv("SECTOR", "otro")
62
+ WA_PHONE_ID = os.getenv("WA_PHONE_ID", "")
63
+ WA_ACCESS_TOKEN = os.getenv("WA_ACCESS_TOKEN", "")
64
+ WA_VERIFY_TOKEN = os.getenv("WA_VERIFY_TOKEN", "")
65
+ EVOLUTION_URL = os.getenv("EVOLUTION_URL", "")
66
+ EVOLUTION_API_KEY = os.getenv("EVOLUTION_API_KEY", "")
67
+ EVOLUTION_INSTANCE = os.getenv("EVOLUTION_INSTANCE", "conny")
68
+ MASTER_API_KEY = os.getenv("MASTER_API_KEY", "")
69
+ N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL", "")
70
+ TOKEN_EXPIRY_HOURS = int(os.getenv("TOKEN_EXPIRY_HOURS", "72"))
71
+ DEMO_MODE = os.getenv("DEMO_MODE", "false").lower() == "true"
72
+ DEMO_BUSINESS_NAME = os.getenv("DEMO_BUSINESS_NAME", "tu negocio")
73
+ DEMO_SECTOR = os.getenv("DEMO_SECTOR", "estetica")
74
+ DEMO_SESSION_TTL = int(os.getenv("DEMO_SESSION_TTL", "1800"))
75
+ GREETING_ONLY_IDLE_SECONDS = int(os.getenv("GREETING_ONLY_IDLE_SECONDS", "300"))
76
+ V8_ACTIVE_MODEL_REASONING = os.getenv("V8_ACTIVE_MODEL_REASONING", "")
77
+ V8_ACTIVE_MODEL_FAST = os.getenv("V8_ACTIVE_MODEL_FAST", "")
78
+ V8_ACTIVE_MODEL_LITE = os.getenv("V8_ACTIVE_MODEL_LITE", "")
79
+ V8_QUALITY_THRESHOLD = float(os.getenv("V8_QUALITY_THRESHOLD", "0.72"))
80
+ V8_MAX_RETRIES = int(os.getenv("V8_MAX_RETRIES", "3"))
81
+ CONNY_COMPACT_PROMPT = os.getenv("CONNY_COMPACT_PROMPT", "true").lower() in ("1", "true", "yes", "on")
82
+ CONNY_CONTEXT_RECENT_MESSAGES = int(os.getenv("CONNY_CONTEXT_RECENT_MESSAGES", "12"))
83
+ CONNY_CORE_ENABLED = os.getenv("CONNY_CORE_ENABLED", "true").lower() in ("1", "true", "yes", "on")
84
+ CONNY_CORE_PERSONAS_DIR = os.getenv(
85
+ "CONNY_CORE_PERSONAS_DIR",
86
+ str(Path(__file__).resolve().parent / "personas" / "conny" / "base"),
87
+ )
88
+ V8_FILTER_LEVEL = int(os.getenv("V8_FILTER_LEVEL", "2"))
89
+ DB_PATH = os.getenv("DB_PATH", "/home/ubuntu/conny/conny_ultra.db")
90
+ VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", "/home/ubuntu/conny/vectors.db")
91
+ LLM_MODELS = {
92
+ "reasoning": os.getenv("LLM_REASONING", "google/gemini-2.5-pro"),
93
+ "fast": os.getenv("LLM_FAST", "google/gemini-2.5-flash"),
94
+ "lite": os.getenv("LLM_LITE", "google/gemini-2.5-flash-lite"),
95
+ "embedding": os.getenv("LLM_EMBEDDING", "openai/text-embedding-3-small"),
96
+ }
97
+ WHISPER_MODEL = os.getenv("WHISPER_MODEL", "openai/whisper-large-v3")
98
+ BUFFER_WAIT_MIN = int(os.getenv("BUFFER_WAIT_MIN", "25"))
99
+ BUFFER_WAIT_MAX = int(os.getenv("BUFFER_WAIT_MAX", "45"))
100
+ BUBBLE_PAUSE_MIN = float(os.getenv("BUBBLE_PAUSE_MIN", "1.2"))
101
+ BUBBLE_PAUSE_MAX = float(os.getenv("BUBBLE_PAUSE_MAX", "3.0"))
102
+ BRAND_ASSETS_BASE_DIR = os.getenv(
103
+ "BRAND_ASSETS_BASE_DIR",
104
+ "/home/ubuntu/conny/brand-assets",
105
+ )
106
+ SELF_IMPROVE_INTERVAL = int(os.getenv("SELF_IMPROVE_INTERVAL", "3600"))
107
+ LEARNING_RATE = float(os.getenv("LEARNING_RATE", "0.1"))
108
+ MAX_CONTEXT_MESSAGES = int(os.getenv("MAX_CONTEXT", "50"))
109
+ MAX_MEMORY_ITEMS = int(os.getenv("MAX_MEMORY", "1000"))
110
+
111
+ @classmethod
112
+ def validate(cls) -> List[str]:
113
+ errors = []
114
+ if not cls.TELEGRAM_TOKEN:
115
+ errors.append("TELEGRAM_TOKEN requerido")
116
+ if not cls.OPENROUTER_API_KEY and not cls.GEMINI_API_KEY:
117
+ errors.append("Se requiere al menos OPENROUTER_API_KEY o GEMINI_API_KEY")
118
+ return errors
119
+
120
+
121
+ DEMO_COMMANDS: Dict[str, str] = {
122
+ "/formal":"/formal", "/amigable":"/amigable", "/luxury":"/luxury",
123
+ "/directa":"/directa", "/energica":"/energica", "/empatica":"/empatica",
124
+ "/experta":"/experta", "/juvenil":"/juvenil",
125
+ "/objecion":"/objecion", "/cita":"/cita", "/stats":"/stats",
126
+ "/prueba":"/prueba", "/cierre":"/cierre", "/bot":"/bot",
127
+ "/memoria":"/memoria", "/2am":"/2am", "/competencia":"/competencia",
128
+ "/precio":"/precio", "/siguiente":"/siguiente",
129
+ }
130
+
131
+ DEMO_TRICKS_ORDER: List[Tuple[str, str]] = [
132
+ ("/objecion", "ver cómo manejo objeciones en vivo"),
133
+ ("/cita", "ver cómo agendo una cita completa"),
134
+ ("/luxury", "activar personalidad premium"),
135
+ ("/empatica", "cambiar a modo empático y de escucha"),
136
+ ("/stats", "ver el impacto en números reales"),
137
+ ("/prueba", "lanzarme el mensaje más difícil que tengas"),
138
+ ("/cierre", "ver cómo cierro una venta"),
139
+ ("/directa", "activar modo al grano sin rodeos"),
140
+ ("/menu", "ver modo bot con emojis y menú numerado"),
141
+ ("/2am", "verme responder a las 2 de la madrugada"),
142
+ ("/memoria", "ver qué recuerdo de esta conversación"),
143
+ ("/experta", "cambiar a modo técnico y preciso"),
144
+ ("/modelo", "cambiar el modelo de IA que me impulsa"),
145
+ ]
146
+
147
+ DEMO_CMD_ALIASES: Dict[str, str] = {
148
+ "formal":"formal","amigable":"amigable","luxury":"luxury","lujo":"luxury",
149
+ "directa":"directa","energica":"energica","enérgica":"energica",
150
+ "empatica":"empatica","empática":"empatica","experta":"experta",
151
+ "juvenil":"juvenil","joven":"juvenil","profesional":"formal","objecion":"objecion","objeción":"objecion",
152
+ "cita":"cita","agendar":"cita","stats":"stats","estadisticas":"stats",
153
+ "prueba":"prueba","reto":"prueba","cierre":"cierre","bot":"bot",
154
+ "memoria":"memoria","recuerdas":"memoria","2am":"2am","de noche":"2am",
155
+ "competencia":"competencia","precio":"precio","caro":"precio",
156
+ "siguiente":"siguiente","que mas":"siguiente","qué más":"siguiente",
157
+ "menu":"menu_bot","menú":"menu_bot","modo bot":"menu_bot","bot menu":"menu_bot",
158
+ "list":"list","lista":"list","comandos":"list","ayuda":"list","help":"list",
159
+ "qué puedes hacer":"list","que puedes hacer":"list",
160
+ "emojis":"emojis_on","con emojis":"emojis_on","activa emojis":"emojis_on",
161
+ "sin emojis":"emojis_off","quita emojis":"emojis_off","desactiva emojis":"emojis_off",
162
+ "modelo":"modelo","model":"modelo","cambiar modelo":"modelo",
163
+ }
164
+
165
+ MODEL_CATALOG: Dict[str, Tuple[str, str, str]] = {
166
+ "claude-opus": ("anthropic/claude-opus-4", "reasoning", "Más inteligente. Más caro."),
167
+ "claude-sonnet": ("anthropic/claude-sonnet-4", "reasoning", "Balance inteligencia/costo."),
168
+ "claude-haiku": ("anthropic/claude-haiku-3-5", "fast", "Rapidísimo y económico."),
169
+ "gemini-pro": ("google/gemini-2.5-pro", "reasoning", "Google Pro."),
170
+ "gemini-flash": ("google/gemini-2.5-flash", "fast", "Velocidad + calidad."),
171
+ "gemini-lite": ("google/gemini-2.5-flash-lite", "lite", "El más económico."),
172
+ "llama-70b": ("meta-llama/llama-3.3-70b-instruct","fast", "Open source, excelente español."),
173
+ "llama-8b": ("meta-llama/llama-3.1-8b-instruct", "lite", "Ultrarrápido, básico."),
174
+ "gpt4o": ("openai/gpt-4o", "reasoning", "OpenAI flagship."),
175
+ "gpt4o-mini": ("openai/gpt-4o-mini", "fast", "OpenAI económico."),
176
+ "mistral-large": ("mistralai/mistral-large", "reasoning", "Europeo, buen español."),
177
+ "mistral-small": ("mistralai/mistral-small", "fast", "Rápido y asequible."),
178
+ }
179
+
180
+ INTERNAL_PHRASES_TO_BLOCK: List[str] = [
181
+ "todavía no tengo este chat enlazado",
182
+ "ya recibí tu mensaje",
183
+ "no tengo este chat",
184
+ "[error",
185
+ "[internal",
186
+ "{",
187
+ ]
188
+
189
+ AUDIO_ERROR_MSG = "Recibí tu audio pero no lo pude procesar. ¿Puedes escribirlo?"
190
+ JSON_ERROR_MSG = "Entendido, déjame verificar eso."
191
+ UNKNOWN_CMD_MSG = "Comando no reconocido. Escribe /help para ver los disponibles."
192
+
193
+ DEMO_HELP_FULL = (
194
+ "esto es lo que puedo mostrarte 👇\n\n"
195
+ "🎭 Personalidades: /formal · /amigable · /luxury · /directa · /empatica · /experta · /juvenil\n\n"
196
+ "💬 Situaciones reales:\n"
197
+ "objecion — cliente difícil\n"
198
+ "cita — agendamiento completo\n"
199
+ "cierre — técnica de cierre\n"
200
+ "competencia — ya fui a otro lado\n"
201
+ "precio — está muy caro\n"
202
+ "prueba — mándame el más difícil\n"
203
+ "bot — soy un bot?\n"
204
+ "2am — respuesta a las 2am\n\n"
205
+ "📊 Demo y datos:\n"
206
+ "stats — impacto en números\n"
207
+ "memoria — qué recuerdo de ti\n"
208
+ "menu — modo bot con emojis\n\n"
209
+ "⚙️ Ajustes:\n"
210
+ "usa emojis / sin emojis\n"
211
+ "siguiente — próximo truco\n"
212
+ "/modelo — cambiar el modelo\n"
213
+ "reset — empezar de nuevo\n\n"
214
+ "escribe sin slash para activar"
215
+ )
@@ -0,0 +1,72 @@
1
+ """
2
+ conny_cron.py — Scheduled tasks for Conny (memory consolidation, cleanup).
3
+ """
4
+ from __future__ import annotations
5
+ import logging, asyncio
6
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
7
+ from apscheduler.triggers.cron import CronTrigger
8
+
9
+ log = logging.getLogger("conny.cron")
10
+
11
+ _scheduler: AsyncIOScheduler = None
12
+
13
+
14
+ def init_scheduler(memory_engine=None, instance_ids: list = None):
15
+ """Initialize the cron scheduler. Call during app startup."""
16
+ global _scheduler
17
+ if _scheduler:
18
+ return _scheduler
19
+
20
+ _scheduler = AsyncIOScheduler()
21
+
22
+ if memory_engine and instance_ids:
23
+ for iid in instance_ids:
24
+ _scheduler.add_job(
25
+ _run_consolidation,
26
+ CronTrigger(day_of_week="sun", hour=3, minute=0),
27
+ args=[memory_engine, iid],
28
+ id=f"consolidation_{iid}",
29
+ replace_existing=True,
30
+ )
31
+ # Weekly report every Monday at 9am
32
+ _scheduler.add_job(
33
+ _send_weekly_report,
34
+ CronTrigger(day_of_week="mon", hour=9, minute=0),
35
+ args=[iid],
36
+ id=f"weekly_report_{iid}",
37
+ replace_existing=True,
38
+ )
39
+ log.info(f"[cron] consolidation (Sun 3am) + weekly report (Mon 9am) scheduled for {iid}")
40
+
41
+ _scheduler.start()
42
+ log.info("[cron] scheduler started")
43
+ return _scheduler
44
+
45
+
46
+ async def _run_consolidation(memory_engine, instance_id: str):
47
+ """Run weekly memory consolidation for an instance."""
48
+ try:
49
+ await memory_engine.weekly_consolidation(instance_id)
50
+ log.info(f"[cron] consolidation complete: {instance_id}")
51
+ except Exception as e:
52
+ log.error(f"[cron] consolidation failed for {instance_id}: {e}")
53
+
54
+
55
+ async def _send_weekly_report(instance_id: str):
56
+ """Send weekly report to admin."""
57
+ try:
58
+ from conny_weekly_report import generate_weekly_report
59
+ report = await generate_weekly_report(instance_id)
60
+ log.info(f"[cron] weekly report generated for {instance_id}")
61
+ # TODO: wire send_fn when admin_jid is available in cron context
62
+ except Exception as e:
63
+ log.error(f"[cron] weekly report failed: {e}")
64
+
65
+
66
+ def shutdown_scheduler():
67
+ """Shutdown the scheduler gracefully."""
68
+ global _scheduler
69
+ if _scheduler:
70
+ _scheduler.shutdown(wait=False)
71
+ _scheduler = None
72
+ log.info("[cron] scheduler stopped")