iatoolkit 1.9.0__py3-none-any.whl → 1.15.3__py3-none-any.whl

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 (38) hide show
  1. iatoolkit/__init__.py +1 -1
  2. iatoolkit/common/routes.py +1 -1
  3. iatoolkit/common/util.py +8 -123
  4. iatoolkit/core.py +1 -0
  5. iatoolkit/infra/connectors/file_connector.py +10 -2
  6. iatoolkit/infra/connectors/google_drive_connector.py +3 -0
  7. iatoolkit/infra/connectors/local_file_connector.py +3 -0
  8. iatoolkit/infra/connectors/s3_connector.py +24 -1
  9. iatoolkit/infra/llm_providers/deepseek_adapter.py +17 -1
  10. iatoolkit/infra/llm_providers/gemini_adapter.py +117 -18
  11. iatoolkit/infra/llm_providers/openai_adapter.py +175 -18
  12. iatoolkit/infra/llm_response.py +13 -0
  13. iatoolkit/locales/en.yaml +47 -2
  14. iatoolkit/locales/es.yaml +45 -1
  15. iatoolkit/repositories/llm_query_repo.py +44 -33
  16. iatoolkit/services/company_context_service.py +294 -133
  17. iatoolkit/services/dispatcher_service.py +1 -1
  18. iatoolkit/services/knowledge_base_service.py +26 -4
  19. iatoolkit/services/llm_client_service.py +58 -2
  20. iatoolkit/services/prompt_service.py +236 -330
  21. iatoolkit/services/query_service.py +37 -18
  22. iatoolkit/services/storage_service.py +92 -0
  23. iatoolkit/static/js/chat_filepond.js +188 -63
  24. iatoolkit/static/js/chat_main.js +105 -52
  25. iatoolkit/static/styles/chat_iatoolkit.css +96 -0
  26. iatoolkit/system_prompts/query_main.prompt +24 -41
  27. iatoolkit/templates/chat.html +15 -6
  28. iatoolkit/views/base_login_view.py +1 -1
  29. iatoolkit/views/categories_api_view.py +43 -3
  30. iatoolkit/views/chat_view.py +1 -1
  31. iatoolkit/views/login_view.py +1 -1
  32. iatoolkit/views/prompt_api_view.py +1 -1
  33. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/METADATA +1 -1
  34. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/RECORD +38 -37
  35. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/WHEEL +0 -0
  36. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE +0 -0
  37. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  38. {iatoolkit-1.9.0.dist-info → iatoolkit-1.15.3.dist-info}/top_level.txt +0 -0
@@ -21,6 +21,7 @@ import re
21
21
  import tiktoken
22
22
  from typing import Dict, Optional, List
23
23
  from iatoolkit.services.dispatcher_service import Dispatcher
24
+ from iatoolkit.services.storage_service import StorageService
24
25
 
25
26
  CONTEXT_ERROR_MESSAGE = 'Tu consulta supera el límite de contexto, utiliza el boton de recarga de contexto.'
26
27
 
@@ -33,11 +34,13 @@ class llmClient:
33
34
  llmquery_repo: LLMQueryRepo,
34
35
  llm_proxy: LLMProxy,
35
36
  model_registry: ModelRegistry,
37
+ storage_service: StorageService,
36
38
  util: Utility
37
39
  ):
38
40
  self.llmquery_repo = llmquery_repo
39
41
  self.llm_proxy = llm_proxy
40
42
  self.model_registry = model_registry
43
+ self.storage_service = storage_service
41
44
  self.util = util
42
45
  self._dispatcher = None # Cache for the lazy-loaded dispatcher
43
46
 
@@ -69,8 +72,10 @@ class llmClient:
69
72
  text: dict,
70
73
  model: str,
71
74
  context_history: Optional[List[Dict]] = None,
75
+ images: list = None,
72
76
  ) -> dict:
73
77
 
78
+ images = images or []
74
79
  f_calls = [] # keep track of the function calls executed by the LLM
75
80
  f_call_time = 0
76
81
  response = None
@@ -84,7 +89,7 @@ class llmClient:
84
89
 
85
90
  try:
86
91
  start_time = time.time()
87
- logging.info(f"calling llm model '{model}' with {self.count_tokens(context, context_history)} tokens...")
92
+ logging.info(f"calling llm model '{model}' with {self.count_tokens(context, context_history)} tokens...and {len(images)} images...")
88
93
 
89
94
  # this is the first call to the LLM on the iteration
90
95
  try:
@@ -102,6 +107,7 @@ class llmClient:
102
107
  tools=tools,
103
108
  text=text_payload,
104
109
  reasoning=reasoning,
110
+ images=images,
105
111
  )
106
112
  stats = self.get_stats(response)
107
113
 
@@ -163,7 +169,7 @@ class llmClient:
163
169
  error_message = f"Dispatch error en {function_name} con args {args} -******- {str(e)}"
164
170
  raise IAToolkitException(IAToolkitException.ErrorType.CALL_ERROR, error_message)
165
171
 
166
- # add the return value into the list of messages
172
+ # add the return value into the list of messages
167
173
  input_messages.append({
168
174
  "type": "function_call_output",
169
175
  "call_id": tool_call.call_id,
@@ -198,9 +204,14 @@ class llmClient:
198
204
  tool_choice=tool_choice_value,
199
205
  tools=tools,
200
206
  text=text_payload,
207
+ images=images,
201
208
  )
202
209
  stats_fcall = self.add_stats(stats_fcall, self.get_stats(response))
203
210
 
211
+ # --- IMAGE PROCESSING ---
212
+ # before save or respond, upload the images to S3 and clean content_parts
213
+ self._process_generated_images(response, company.short_name)
214
+
204
215
  # save the statistices
205
216
  stats['response_time']=int(time.time() - start_time)
206
217
  stats['sql_retry_count'] = sql_retry_count
@@ -239,6 +250,7 @@ class llmClient:
239
250
  'query_id': query.id,
240
251
  'model': model,
241
252
  'reasoning_content': final_reasoning,
253
+ 'content_parts': response.content_parts
242
254
  }
243
255
  except SQLAlchemyError as db_error:
244
256
  # rollback
@@ -295,6 +307,50 @@ class llmClient:
295
307
 
296
308
  return response.id
297
309
 
310
+ def _process_generated_images(self, response, company_short_name: str):
311
+ """
312
+ Traverse content_parts, detect images in Base64, upload to S3 and update content_parts.
313
+ """
314
+ if not response.content_parts:
315
+ return
316
+
317
+ for part in response.content_parts:
318
+ if part.get('type') == 'image':
319
+ source = part.get('source', {})
320
+ if source.get('type') in ['base64', 'url']:
321
+ try:
322
+ if source.get('type') == 'url':
323
+ url = source.get('url')
324
+ storage_key = None
325
+ else:
326
+ # upload image to S3
327
+ result = self.storage_service.store_generated_image(
328
+ company_short_name,
329
+ source.get('data'),
330
+ source.get('media_type', 'image/png')
331
+ )
332
+ url = result['url']
333
+ storage_key = result['storage_key']
334
+
335
+ # Update content_part: Now it's a remote reference, not base64 anymore.
336
+ # We keep 'url' for the frontend to display it itself, and storage_key for internal reference.
337
+ part['source'] = {
338
+ 'type': 'url',
339
+ 'url': url,
340
+ 'storage_key': storage_key,
341
+ 'media_type': source.get('media_type')
342
+ }
343
+
344
+ # clean data
345
+ logging.info(f"Imagen procesada y subida: {url}")
346
+
347
+ except Exception as e:
348
+ logging.error(f"Fallo al subir imagen generada: {e}")
349
+
350
+ # Fallback: keep the base64 and signal the error
351
+ part['error'] = "Failed to upload image"
352
+
353
+
298
354
  def decode_response(self, response) -> dict:
299
355
  message = response.output_text
300
356
  decoded_response = {