@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,447 @@
1
+ """
2
+ Módulo de generación LLM para Conny Ultra.
3
+
4
+ Contiene la lógica de generación de respuestas:
5
+ - _llm(): Generación básica
6
+ - _llm_conv_pitch(): Generación con pitch para prospectos confuse
7
+ - _llm_conv(): Generación conversacional con historial
8
+ - _demo_llm_conv_quality_chain(): Chain con validación y repair
9
+ - _demo_llm_quality_chain(): Chain de calidad con retries
10
+
11
+ Este módulo fue extraído de conny.py para reducir su tamaño y mejorar mantenibilidad.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import time
16
+ from typing import Any, Callable, Dict, List, Optional, Tuple
17
+
18
+ try:
19
+ from conny_config import Config
20
+ except ImportError:
21
+ class Config:
22
+ GROQ_API_KEY = ""
23
+ GEMINI_API_KEY = ""
24
+ GEMINI_API_KEY_2 = ""
25
+ GEMINI_API_KEY_3 = ""
26
+ OPENROUTER_API_KEY = ""
27
+
28
+
29
+ class GeneratorManager:
30
+ """
31
+ Gestor de generación LLM para Conny Ultra.
32
+
33
+ Proporciona métodos para:
34
+ - Generación básica de texto
35
+ - Generación conversacional con historial
36
+ - Generación con pitch especializado
37
+ - Chains de calidad con validación y retry
38
+ """
39
+
40
+ def __init__(self, llm_engine=None, generator=None):
41
+ self._llm_engine = llm_engine
42
+ self._generator = generator
43
+
44
+ def _get_demo_engine(self, demo_model_pref: str = "auto", bmodel_key: str = ""):
45
+ """
46
+ Devuelve el proveedor LLM según preferencia de sesión.
47
+
48
+ Formatos soportados:
49
+ auto → engine global
50
+ gemini → GeminiProvider con gemini-2.5-flash
51
+ gemini:gemini-2.5-pro → GeminiProvider con modelo específico
52
+ groq → GroqProvider con llama-3.3-70b-versatile
53
+ groq:llama-3.1-8b-instant → GroqProvider con modelo específico
54
+ openrouter → OpenRouterProvider
55
+ openrouter:anthropic/claude-sonnet-4 → OpenRouter con modelo específico
56
+ """
57
+ from llm_engine import (
58
+ GeminiProvider, GroqProvider, OpenRouterProvider
59
+ )
60
+
61
+ pref = demo_model_pref
62
+
63
+ if ":" in pref:
64
+ provider, model_name = pref.split(":", 1)
65
+ else:
66
+ provider, model_name = pref, None
67
+
68
+ if provider == "groq" and Config.GROQ_API_KEY:
69
+ eng = GroqProvider(Config.GROQ_API_KEY)
70
+ if model_name:
71
+ eng.MDLS = {"reasoning": model_name, "fast": model_name, "lite": model_name}
72
+ return eng
73
+
74
+ elif provider == "gemini":
75
+ key = Config.GEMINI_API_KEY or Config.GEMINI_API_KEY_2 or Config.GEMINI_API_KEY_3
76
+ if key:
77
+ eng = GeminiProvider(key, "gemini_demo")
78
+ if model_name:
79
+ eng.MDLS = {"reasoning": model_name, "fast": model_name, "lite": model_name}
80
+ else:
81
+ eng.MDLS = {
82
+ "reasoning": "gemini-2.5-flash",
83
+ "fast": "gemini-2.5-flash",
84
+ "lite": "gemini-2.5-flash-lite"
85
+ }
86
+ return eng
87
+ if Config.OPENROUTER_API_KEY:
88
+ eng = OpenRouterProvider(Config.OPENROUTER_API_KEY)
89
+ m = model_name or "google/gemini-2.5-flash"
90
+ eng.MDLS = {"reasoning": m, "fast": m, "lite": m}
91
+ return eng
92
+
93
+ elif provider == "openrouter" and Config.OPENROUTER_API_KEY:
94
+ eng = OpenRouterProvider(Config.OPENROUTER_API_KEY)
95
+ if model_name:
96
+ eng.MDLS = {"reasoning": model_name, "fast": model_name, "lite": model_name}
97
+ return eng
98
+
99
+ return self._llm_engine or (self._generator.llm if self._generator else None)
100
+
101
+ async def _llm(
102
+ self,
103
+ sys_p: str,
104
+ usr_p: str,
105
+ temp: float = 0.82,
106
+ max_t: int = 8192,
107
+ model_tier: str = "fast",
108
+ demo_model_pref: str = "auto"
109
+ ) -> Optional[str]:
110
+ """
111
+ Generación básica de texto con LLM.
112
+
113
+ Args:
114
+ sys_p: Prompt del sistema
115
+ usr_p: Prompt del usuario
116
+ temp: Temperatura de generación
117
+ max_t: Máximo de tokens de salida
118
+ model_tier: Tier del modelo (fast, reasoning, lite)
119
+ demo_model_pref: Preferencia de modelo demo
120
+
121
+ Returns:
122
+ Texto generado o None en caso de error
123
+ """
124
+ import logging
125
+ log = logging.getLogger("conny_generator")
126
+
127
+ msgs = [{"role": "system", "content": sys_p}, {"role": "user", "content": usr_p}]
128
+
129
+ try:
130
+ eng = self._get_demo_engine(demo_model_pref)
131
+ if not eng:
132
+ raise RuntimeError("LLM no init")
133
+
134
+ r, meta = await eng.complete(
135
+ msgs,
136
+ model_tier=model_tier,
137
+ temperature=temp,
138
+ max_tokens=max_t,
139
+ use_cache=False,
140
+ )
141
+
142
+ log.info(f"[demo] {meta.get('provider','?')} model={meta.get('model','?')[:30]}")
143
+
144
+ if self._generator:
145
+ return self._generator._postprocess(r, self._generator.PersonalityProfile())
146
+ return r
147
+
148
+ except Exception as e:
149
+ log.error(f"[demo] llm error: {e}")
150
+ return None
151
+
152
+ async def _llm_conv_pitch(
153
+ self,
154
+ pitch_sys: str,
155
+ history: List[Dict[str, Any]],
156
+ text: str,
157
+ temp: float = 0.85,
158
+ max_t: int = 8192,
159
+ recent_limit: int = 12,
160
+ demo_model_pref: str = "auto"
161
+ ) -> Optional[str]:
162
+ """
163
+ LLM con el pitch de Black One para prospectos confundidos.
164
+
165
+ Args:
166
+ pitch_sys: Prompt de sistema con el pitch
167
+ history: Historial de conversación
168
+ text: Mensaje actual
169
+ temp: Temperatura
170
+ max_t: Máximo de tokens
171
+ recent_limit: Límite de mensajes recientes a incluir
172
+ demo_model_pref: Preferencia de modelo
173
+
174
+ Returns:
175
+ Texto generado o None
176
+ """
177
+ import logging
178
+ log = logging.getLogger("conny_generator")
179
+
180
+ msgs = [{"role": "system", "content": pitch_sys}]
181
+ for m in history[-recent_limit:]:
182
+ msgs.append({"role": m["role"], "content": m["content"]})
183
+ msgs.append({"role": "user", "content": text})
184
+
185
+ try:
186
+ eng = self._get_demo_engine(demo_model_pref)
187
+ if not eng:
188
+ raise RuntimeError("LLM no init")
189
+
190
+ r, meta = await eng.complete(
191
+ msgs,
192
+ model_tier="fast",
193
+ temperature=temp,
194
+ max_tokens=max_t,
195
+ use_cache=False
196
+ )
197
+
198
+ log.info(f"[demo][pitch] {meta.get('provider','?')}")
199
+
200
+ if self._generator:
201
+ return self._generator._postprocess(r, self._generator.PersonalityProfile())
202
+ return r
203
+
204
+ except Exception as e:
205
+ log.error(f"[demo][pitch] error: {e}")
206
+ return None
207
+
208
+ async def _llm_conv(
209
+ self,
210
+ sys_p: str,
211
+ history: List[Dict[str, Any]],
212
+ text: str,
213
+ temp: float = 0.85,
214
+ max_t: int = 8192,
215
+ model_tier: str = "fast",
216
+ recent_limit: int = 12,
217
+ demo_model_pref: str = "auto"
218
+ ) -> Optional[str]:
219
+ """
220
+ Generación conversacional con historial.
221
+
222
+ Args:
223
+ sys_p: Prompt del sistema
224
+ history: Historial de conversación
225
+ text: Mensaje actual
226
+ temp: Temperatura
227
+ max_t: Máximo de tokens
228
+ model_tier: Tier del modelo
229
+ recent_limit: Límite de mensajes recientes
230
+ demo_model_pref: Preferencia de modelo
231
+
232
+ Returns:
233
+ Texto generado o None
234
+ """
235
+ import logging
236
+ log = logging.getLogger("conny_generator")
237
+
238
+ msgs = [{"role": "system", "content": sys_p}]
239
+ for m in history[-recent_limit:]:
240
+ msgs.append({"role": m["role"], "content": m["content"]})
241
+ msgs.append({"role": "user", "content": text})
242
+
243
+ try:
244
+ eng = self._get_demo_engine(demo_model_pref)
245
+ if not eng:
246
+ raise RuntimeError("LLM no init")
247
+
248
+ r, meta = await eng.complete(
249
+ msgs,
250
+ model_tier=model_tier,
251
+ temperature=temp,
252
+ max_tokens=max_t,
253
+ use_cache=False,
254
+ )
255
+
256
+ log.info(f"[demo] {meta.get('provider','?')} model={meta.get('model','?')[:30]}")
257
+
258
+ if self._generator:
259
+ return self._generator._postprocess(r, self._generator.PersonalityProfile())
260
+ return r
261
+
262
+ except Exception as e:
263
+ log.error(f"[demo] llm_conv error: {e}")
264
+ return None
265
+
266
+ async def _demo_llm_conv_quality_chain(
267
+ self,
268
+ system_prompt: str,
269
+ validator: Callable[[str], bool],
270
+ repair_instructions: str,
271
+ history: List[Dict[str, Any]],
272
+ text: str,
273
+ temp: float = 0.72,
274
+ max_t: int = 8192,
275
+ model_tier: str = "fast",
276
+ recent_limit: int = 8,
277
+ demo_model_pref: str = "auto"
278
+ ) -> Tuple[Optional[str], bool]:
279
+ """
280
+ Chain de calidad con validación y repair para conversaciones demo.
281
+
282
+ Intenta primero la generación normal, si falla la validación
283
+ reintenta con instrucciones de repair.
284
+
285
+ Args:
286
+ system_prompt: Prompt del sistema
287
+ validator: Función que valida la respuesta
288
+ repair_instructions: Instrucciones para repair si falla validación
289
+ history: Historial de conversación
290
+ text: Mensaje actual
291
+ temp: Temperatura
292
+ max_t: Máximo de tokens
293
+ model_tier: Tier del modelo
294
+ recent_limit: Límite de mensajes recientes
295
+ demo_model_pref: Preferencia de modelo
296
+
297
+ Returns:
298
+ Tuple (respuesta_valida o None, tuvo_output)
299
+ """
300
+ import logging
301
+ log = logging.getLogger("conny_generator")
302
+
303
+ _chain_start = time.time()
304
+ _CHAIN_TIMEOUT_S = 45 # Timeout entre intentos
305
+
306
+ attempts = [
307
+ (system_prompt, temp, max_t, model_tier, recent_limit),
308
+ (
309
+ system_prompt
310
+ + "\n\nREPARA LA RESPUESTA:\n"
311
+ + repair_instructions.strip()
312
+ + "\n- no repitas introducciones\n- no suenes a bot ni a guion de demo",
313
+ 0.58,
314
+ max_t,
315
+ "reasoning",
316
+ recent_limit,
317
+ ),
318
+ ]
319
+
320
+ had_output = False
321
+
322
+ for prompt_now, temp_now, max_now, tier_now, limit_now in attempts:
323
+ if time.time() - _chain_start > _CHAIN_TIMEOUT_S:
324
+ log.warning("[demo] conv_quality_chain abortada por timeout (%ds)", _CHAIN_TIMEOUT_S)
325
+ break
326
+
327
+ candidate = await self._llm_conv(
328
+ prompt_now,
329
+ history=history,
330
+ text=text,
331
+ temp=temp_now,
332
+ max_t=max_now,
333
+ model_tier=tier_now,
334
+ recent_limit=limit_now,
335
+ demo_model_pref=demo_model_pref,
336
+ )
337
+
338
+ if candidate and candidate.strip():
339
+ had_output = True
340
+ if validator(candidate):
341
+ return candidate, True
342
+
343
+ return None, had_output
344
+
345
+ async def _demo_llm_quality_chain(
346
+ self,
347
+ system_prompt: str,
348
+ validator: Callable[[str], bool],
349
+ repair_instructions: str,
350
+ user_message: str,
351
+ temp: float = 0.72,
352
+ max_t: int = 8192,
353
+ model_tier: str = "fast",
354
+ demo_model_pref: str = "auto"
355
+ ) -> Tuple[Optional[str], bool]:
356
+ """
357
+ Chain de calidad simple (sin historial) para generación demo.
358
+
359
+ Args:
360
+ system_prompt: Prompt del sistema
361
+ validator: Función que valida la respuesta
362
+ repair_instructions: Instrucciones para repair
363
+ user_message: Mensaje del usuario
364
+ temp: Temperatura
365
+ max_t: Máximo de tokens
366
+ model_tier: Tier del modelo
367
+ demo_model_pref: Preferencia de modelo
368
+
369
+ Returns:
370
+ Tuple (respuesta o None, tuvo_output)
371
+ """
372
+ import logging
373
+ log = logging.getLogger("conny_generator")
374
+
375
+ _chain_start = time.time()
376
+ _CHAIN_TIMEOUT_S = 45
377
+
378
+ attempts = [
379
+ (system_prompt, temp, max_t, model_tier),
380
+ (
381
+ system_prompt
382
+ + "\n\nREPARA LA RESPUESTA:\n"
383
+ + repair_instructions.strip()
384
+ + "\n- no repitas introducciones\n- no suenes a bot",
385
+ 0.58,
386
+ max_t,
387
+ "reasoning",
388
+ ),
389
+ ]
390
+
391
+ had_output = False
392
+
393
+ for prompt_now, temp_now, max_now, tier_now in attempts:
394
+ if time.time() - _chain_start > _CHAIN_TIMEOUT_S:
395
+ log.warning("[demo] quality_chain abortada por timeout")
396
+ break
397
+
398
+ candidate = await self._llm(
399
+ prompt_now,
400
+ user_message,
401
+ temp=temp_now,
402
+ max_t=max_now,
403
+ model_tier=tier_now,
404
+ demo_model_pref=demo_model_pref,
405
+ )
406
+
407
+ if candidate and candidate.strip():
408
+ had_output = True
409
+ if validator(candidate):
410
+ return candidate, True
411
+
412
+ return None, had_output
413
+
414
+
415
+ def estimate_tokens(text: str) -> int:
416
+ """
417
+ Estima el número de tokens en un texto.
418
+
419
+ Args:
420
+ text: Texto a estimar
421
+
422
+ Returns:
423
+ Estimación de tokens (approx 4 chars por token)
424
+ """
425
+ return len(text) // 4
426
+
427
+
428
+ def budget_tokens(
429
+ system_prompt: str,
430
+ max_t: int = 8192,
431
+ safety_margin: float = 0.9
432
+ ) -> Tuple[int, int]:
433
+ """
434
+ Calcula presupuesto de tokens para generación.
435
+
436
+ Args:
437
+ system_prompt: Prompt del sistema
438
+ max_t: Máximo de tokens disponibles
439
+ safety_margin: Margen de seguridad (0.0-1.0)
440
+
441
+ Returns:
442
+ Tuple (tokens_disponibles_output, tokens_reservados)
443
+ """
444
+ system_tokens = estimate_tokens(system_prompt)
445
+ available = int(max_t * safety_margin)
446
+ output_budget = max(available - system_tokens, 512)
447
+ return output_budget, system_tokens
File without changes