iatoolkit 0.22.1__py3-none-any.whl → 0.50.2__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.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

Files changed (46) hide show
  1. iatoolkit/common/routes.py +31 -31
  2. iatoolkit/common/session_manager.py +0 -1
  3. iatoolkit/common/util.py +0 -21
  4. iatoolkit/iatoolkit.py +5 -18
  5. iatoolkit/infra/llm_client.py +3 -5
  6. iatoolkit/infra/redis_session_manager.py +48 -2
  7. iatoolkit/repositories/models.py +1 -2
  8. iatoolkit/services/auth_service.py +74 -0
  9. iatoolkit/services/dispatcher_service.py +12 -21
  10. iatoolkit/services/excel_service.py +15 -15
  11. iatoolkit/services/history_service.py +2 -11
  12. iatoolkit/services/profile_service.py +83 -25
  13. iatoolkit/services/query_service.py +132 -82
  14. iatoolkit/services/tasks_service.py +1 -1
  15. iatoolkit/services/user_feedback_service.py +3 -6
  16. iatoolkit/services/user_session_context_service.py +112 -54
  17. iatoolkit/static/js/chat_feedback.js +1 -1
  18. iatoolkit/static/js/chat_history.js +1 -5
  19. iatoolkit/static/js/chat_main.js +1 -1
  20. iatoolkit/static/styles/landing_page.css +62 -2
  21. iatoolkit/system_prompts/query_main.prompt +3 -12
  22. iatoolkit/templates/_login_widget.html +6 -8
  23. iatoolkit/templates/chat.html +78 -4
  24. iatoolkit/templates/error.html +1 -1
  25. iatoolkit/templates/index.html +38 -11
  26. iatoolkit/templates/{home.html → login_test.html} +11 -51
  27. iatoolkit/views/external_login_view.py +50 -111
  28. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +4 -4
  29. iatoolkit/views/history_api_view.py +52 -0
  30. iatoolkit/views/init_context_api_view.py +62 -0
  31. iatoolkit/views/llmquery_api_view.py +50 -0
  32. iatoolkit/views/llmquery_web_view.py +38 -0
  33. iatoolkit/views/{home_view.py → login_test_view.py} +2 -5
  34. iatoolkit/views/login_view.py +79 -56
  35. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +4 -4
  36. iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -19
  37. {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.2.dist-info}/METADATA +2 -2
  38. {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.2.dist-info}/RECORD +40 -41
  39. iatoolkit/common/auth.py +0 -200
  40. iatoolkit/templates/login.html +0 -43
  41. iatoolkit/views/download_file_view.py +0 -58
  42. iatoolkit/views/history_view.py +0 -57
  43. iatoolkit/views/init_context_view.py +0 -35
  44. iatoolkit/views/llmquery_view.py +0 -65
  45. {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.2.dist-info}/WHEEL +0 -0
  46. {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.2.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@
6
6
  from flask import render_template, redirect, flash, url_for,send_from_directory, current_app, abort
7
7
  from iatoolkit.common.session_manager import SessionManager
8
8
  from flask import jsonify
9
- from iatoolkit.views.history_view import HistoryView
9
+ from iatoolkit.views.history_api_view import HistoryApiView
10
10
  import os
11
11
 
12
12
 
@@ -20,36 +20,34 @@ def logout(company_short_name: str):
20
20
  def register_views(injector, app):
21
21
 
22
22
  from iatoolkit.views.index_view import IndexView
23
- from iatoolkit.views.init_context_view import InitContextView
24
- from iatoolkit.views.llmquery_view import LLMQueryView
23
+ from iatoolkit.views.init_context_api_view import InitContextApiView
24
+ from iatoolkit.views.llmquery_web_view import LLMQueryWebView
25
+ from iatoolkit.views.llmquery_api_view import LLMQueryApiView
25
26
  from iatoolkit.views.tasks_view import TaskView
26
27
  from iatoolkit.views.tasks_review_view import TaskReviewView
27
- from iatoolkit.views.home_view import HomeView
28
+ from iatoolkit.views.login_test_view import LoginTest
28
29
  from iatoolkit.views.login_view import LoginView, InitiateLoginView
29
- from iatoolkit.views.external_login_view import InitiateExternalChatView, ExternalChatLoginView
30
+ from iatoolkit.views.external_login_view import InitiateExternalChatView
30
31
  from iatoolkit.views.signup_view import SignupView
31
32
  from iatoolkit.views.verify_user_view import VerifyAccountView
32
33
  from iatoolkit.views.forgot_password_view import ForgotPasswordView
33
34
  from iatoolkit.views.change_password_view import ChangePasswordView
34
- from iatoolkit.views.file_store_view import FileStoreView
35
- from iatoolkit.views.user_feedback_view import UserFeedbackView
36
- from iatoolkit.views.prompt_view import PromptView
35
+ from iatoolkit.views.file_store_api_view import FileStoreApiView
36
+ from iatoolkit.views.user_feedback_api_view import UserFeedbackApiView
37
+ from iatoolkit.views.prompt_api_view import PromptApiView
37
38
  from iatoolkit.views.chat_token_request_view import ChatTokenRequestView
38
- from iatoolkit.views.download_file_view import DownloadFileView
39
39
 
40
40
  # iatoolkit home page
41
41
  app.add_url_rule('/<company_short_name>', view_func=IndexView.as_view('index'))
42
42
 
43
43
  # init (reset) the company context (with api-key)
44
- app.add_url_rule('/<company_short_name>/<external_user_id>/init-context',
45
- view_func=InitContextView.as_view('init-context'))
44
+ app.add_url_rule('/<company_short_name>/api/init_context_api',
45
+ view_func=InitContextApiView.as_view('init_context_api'))
46
46
 
47
47
  # this functions are for login external users (with api-key)
48
48
  # only the first one should be used from an external app
49
49
  app.add_url_rule('/<company_short_name>/initiate_external_chat',
50
50
  view_func=InitiateExternalChatView.as_view('initiate_external_chat'))
51
- app.add_url_rule('/<company_short_name>/external_login',
52
- view_func=ExternalChatLoginView.as_view('external_login'))
53
51
 
54
52
  # this endpoint is for requesting a chat token for external users
55
53
  app.add_url_rule('/auth/chat_token',
@@ -70,36 +68,27 @@ def register_views(injector, app):
70
68
 
71
69
  # main chat query, used by the JS in the browser (with credentials)
72
70
  # can be used also for executing iatoolkit prompts
73
- app.add_url_rule('/<company_short_name>/llm_query', view_func=LLMQueryView.as_view('llm_query'))
71
+ app.add_url_rule('/<company_short_name>/llm_query', view_func=LLMQueryWebView.as_view('llm_query_web'))
74
72
 
75
- # chat buttons are here
73
+ # this is the same function as above, but with api-key
74
+ app.add_url_rule('/<company_short_name>/api/llm_query', view_func=LLMQueryApiView.as_view('llm_query_api'))
75
+
76
+ # chat buttons are here on
76
77
 
77
78
  # open the promt directory
78
- app.add_url_rule('/<company_short_name>/prompts', view_func=PromptView.as_view('prompt'))
79
+ app.add_url_rule('/<company_short_name>/api/prompts', view_func=PromptApiView.as_view('prompt'))
79
80
 
80
81
  # feedback and history
81
- app.add_url_rule('/<company_short_name>/feedback', view_func=UserFeedbackView.as_view('feedback'))
82
- app.add_url_rule('/<company_short_name>/history', view_func=HistoryView.as_view('history'))
82
+ app.add_url_rule('/<company_short_name>/api/feedback', view_func=UserFeedbackApiView.as_view('feedback'))
83
+ app.add_url_rule('/<company_short_name>/api/history', view_func=HistoryApiView.as_view('history'))
83
84
 
84
85
  # tasks management endpoints: create task, and review answer
85
86
  app.add_url_rule('/tasks', view_func=TaskView.as_view('tasks'))
86
87
  app.add_url_rule('/tasks/review/<int:task_id>', view_func=TaskReviewView.as_view('tasks-review'))
87
88
 
88
89
  # this endpoint is for upload documents into the vector store (api-key)
89
- app.add_url_rule('/load', view_func=FileStoreView.as_view('load'))
90
-
91
- # login testing (old home page)
92
- app.add_url_rule('/login_testing', view_func=HomeView.as_view('home'))
93
-
94
- app.add_url_rule(
95
- '/about', # URL de la ruta
96
- view_func=lambda: render_template('about.html'))
97
-
98
- app.add_url_rule('/version', 'version',
99
- lambda: jsonify({"iatoolkit_version": current_app.config.get('VERSION', 'N/A')}))
90
+ app.add_url_rule('/api/load', view_func=FileStoreApiView.as_view('load_api'))
100
91
 
101
- app.add_url_rule('/<company_short_name>/<external_user_id>/download-file/<path:filename>',
102
- view_func=DownloadFileView.as_view('download-file'))
103
92
 
104
93
  @app.route('/download/<path:filename>')
105
94
  def download_file(filename):
@@ -122,6 +111,17 @@ def register_views(injector, app):
122
111
  except FileNotFoundError:
123
112
  abort(404)
124
113
 
114
+ # login testing (old home page)
115
+ app.add_url_rule('/login_test', view_func=LoginTest.as_view('login_test'))
116
+
117
+ app.add_url_rule(
118
+ '/about', # URL de la ruta
119
+ view_func=lambda: render_template('about.html'))
120
+
121
+ app.add_url_rule('/version', 'version',
122
+ lambda: jsonify({"iatoolkit_version": current_app.config.get('VERSION', 'N/A')}))
123
+
124
+
125
125
  # hacer que la raíz '/' vaya al home de iatoolkit
126
126
  @app.route('/')
127
127
  def root_redirect():
@@ -5,7 +5,6 @@
5
5
 
6
6
  from flask import session
7
7
 
8
-
9
8
  class SessionManager:
10
9
  @staticmethod
11
10
  def set(key, value):
iatoolkit/common/util.py CHANGED
@@ -9,7 +9,6 @@ from iatoolkit.common.exceptions import IAToolkitException
9
9
  from injector import inject
10
10
  import os
11
11
  from jinja2 import Environment, FileSystemLoader
12
- from iatoolkit.common.session_manager import SessionManager
13
12
  from datetime import datetime, date
14
13
  from decimal import Decimal
15
14
  import yaml
@@ -22,26 +21,6 @@ class Utility:
22
21
  def __init__(self):
23
22
  self.encryption_key = os.getenv('FERNET_KEY')
24
23
 
25
- @staticmethod
26
- def resolve_user_identifier(external_user_id: str = None, local_user_id: int = 0) -> tuple[str, bool]:
27
- """
28
- Resuelve un identificador único de usuario desde external_user_id o local_user_id.
29
-
30
- Lógica:
31
- - Si external_user_id existe y no está vacío: usar external_user_id
32
- - Si no, y local_user_id > 0: obtener email de la sesión actual y retornarlo como ID
33
- - Si ninguno: retornar string vacío
34
-
35
- """
36
- if external_user_id and external_user_id.strip():
37
- return external_user_id.strip(), False
38
- elif local_user_id and local_user_id > 0:
39
- # get the user information from the session
40
- user_data = SessionManager.get('user')
41
- if user_data:
42
- return user_data.get('email', ''), True
43
-
44
- return "", False
45
24
 
46
25
  def render_prompt_from_template(self,
47
26
  template_pathname: str,
iatoolkit/iatoolkit.py CHANGED
@@ -257,7 +257,6 @@ class IAToolkit:
257
257
  self._bind_repositories(binder)
258
258
  self._bind_services(binder)
259
259
  self._bind_infrastructure(binder)
260
- self._bind_views(binder)
261
260
 
262
261
  logging.info("✅ Dependencias configuradas correctamente")
263
262
 
@@ -309,34 +308,21 @@ class IAToolkit:
309
308
  binder.bind(Dispatcher, to=Dispatcher)
310
309
  binder.bind(BrandingService, to=BrandingService)
311
310
 
312
-
313
311
  def _bind_infrastructure(self, binder: Binder):
314
312
  from iatoolkit.infra.llm_client import llmClient
315
313
  from iatoolkit.infra.llm_proxy import LLMProxy
316
314
  from iatoolkit.infra.google_chat_app import GoogleChatApp
317
315
  from iatoolkit.infra.mail_app import MailApp
318
- from iatoolkit.common.auth import IAuthentication
316
+ from iatoolkit.services.auth_service import AuthService
319
317
  from iatoolkit.common.util import Utility
320
318
 
321
-
322
-
323
319
  binder.bind(LLMProxy, to=LLMProxy, scope=singleton)
324
320
  binder.bind(llmClient, to=llmClient, scope=singleton)
325
321
  binder.bind(GoogleChatApp, to=GoogleChatApp)
326
322
  binder.bind(MailApp, to=MailApp)
327
- binder.bind(IAuthentication, to=IAuthentication)
323
+ binder.bind(AuthService, to=AuthService)
328
324
  binder.bind(Utility, to=Utility)
329
325
 
330
- def _bind_views(self, binder: Binder):
331
- """Vincula las vistas después de que el injector ha sido creado"""
332
- from iatoolkit.views.llmquery_view import LLMQueryView
333
- from iatoolkit.views.home_view import HomeView
334
-
335
- binder.bind(HomeView, to=HomeView)
336
- binder.bind(LLMQueryView, to=LLMQueryView)
337
-
338
- logging.info("✅ Views configuradas correctamente")
339
-
340
326
  def _setup_additional_services(self):
341
327
  Bcrypt(self.app)
342
328
 
@@ -374,12 +360,13 @@ class IAToolkit:
374
360
  @self.app.context_processor
375
361
  def inject_globals():
376
362
  from iatoolkit.common.session_manager import SessionManager
363
+
377
364
  return {
378
365
  'url_for': url_for,
379
366
  'iatoolkit_version': self.version,
380
367
  'app_name': 'IAToolkit',
381
- 'user': SessionManager.get('user'),
382
- 'user_company': SessionManager.get('company_short_name'),
368
+ 'user_identifier': SessionManager.get('user_identifier'),
369
+ 'company_short_name': SessionManager.get('company_short_name'),
383
370
  'iatoolkit_base_url': os.environ.get('IATOOLKIT_BASE_URL', ''),
384
371
  }
385
372
 
@@ -266,16 +266,14 @@ class llmClient:
266
266
  def set_company_context(self,
267
267
  company: Company,
268
268
  company_base_context: str,
269
- model: str = None) -> str:
269
+ model) -> str:
270
270
 
271
- if model:
272
- self.model = model
273
- logging.info(f"initializing model '{self.model}' with company context: {self.count_tokens(company_base_context)} tokens...")
271
+ logging.info(f"initializing model '{model}' with company context: {self.count_tokens(company_base_context)} tokens...")
274
272
 
275
273
  llm_proxy = self.llm_proxy_factory.create_for_company(company)
276
274
  try:
277
275
  response = llm_proxy.create_response(
278
- model=self.model,
276
+ model=model,
279
277
  input=[{
280
278
  "role": "system",
281
279
  "content": company_base_context
@@ -37,9 +37,14 @@ class RedisSessionManager:
37
37
  return cls._client
38
38
 
39
39
  @classmethod
40
- def set(cls, key: str, value: str, ex: int = None):
40
+ def set(cls, key: str, value: str, **kwargs):
41
+ """
42
+ Método set flexible que pasa argumentos opcionales (como ex, nx)
43
+ directamente al cliente de redis.
44
+ """
41
45
  client = cls._get_client()
42
- result = client.set(key, value, ex=ex)
46
+ # Pasa todos los argumentos de palabra clave adicionales al cliente real
47
+ result = client.set(key, value, **kwargs)
43
48
  return result
44
49
 
45
50
  @classmethod
@@ -49,12 +54,53 @@ class RedisSessionManager:
49
54
  result = value if value is not None else default
50
55
  return result
51
56
 
57
+ @classmethod
58
+ def hset(cls, key: str, field: str, value: str):
59
+ """
60
+ Establece un campo en un Hash de Redis.
61
+ """
62
+ client = cls._get_client()
63
+ return client.hset(key, field, value)
64
+
65
+ @classmethod
66
+ def hget(cls, key: str, field: str):
67
+ """
68
+ Obtiene el valor de un campo de un Hash de Redis.
69
+ Devuelve None si la clave o el campo no existen.
70
+ """
71
+ client = cls._get_client()
72
+ return client.hget(key, field)
73
+
74
+ @classmethod
75
+ def hdel(cls, key: str, *fields):
76
+ """
77
+ Elimina uno o más campos de un Hash de Redis.
78
+ """
79
+ client = cls._get_client()
80
+ return client.hdel(key, *fields)
81
+
82
+ @classmethod
83
+ def pipeline(cls):
84
+ """
85
+ Inicia una transacción (pipeline) de Redis.
86
+ """
87
+ client = cls._get_client()
88
+ return client.pipeline()
89
+
90
+
52
91
  @classmethod
53
92
  def remove(cls, key: str):
54
93
  client = cls._get_client()
55
94
  result = client.delete(key)
56
95
  return result
57
96
 
97
+ @classmethod
98
+ def exists(cls, key: str) -> bool:
99
+ """Verifica si una clave existe en Redis."""
100
+ client = cls._get_client()
101
+ # El comando EXISTS de Redis devuelve un entero (0 o 1). Lo convertimos a booleano.
102
+ return bool(client.exists(key))
103
+
58
104
  @classmethod
59
105
  def set_json(cls, key: str, value: dict, ex: int = None):
60
106
  json_str = json.dumps(value)
@@ -265,8 +265,7 @@ class UserFeedback(Base):
265
265
  id = Column(Integer, primary_key=True)
266
266
  company_id = Column(Integer, ForeignKey('iat_companies.id',
267
267
  ondelete='CASCADE'), nullable=False)
268
- local_user_id = Column(Integer, default=0, nullable=True)
269
- external_user_id = Column(String(128), default='', nullable=True)
268
+ user_identifier = Column(String(128), default='', nullable=True)
270
269
  message = Column(Text, nullable=False)
271
270
  rating = Column(Integer, nullable=False)
272
271
  created_at = Column(DateTime, default=datetime.now)
@@ -0,0 +1,74 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask import request
7
+ from injector import inject
8
+ from iatoolkit.services.profile_service import ProfileService
9
+ from flask import request
10
+
11
+
12
+ class AuthService:
13
+ """
14
+ Centralized service for handling authentication for all incoming requests.
15
+ It determines the user's identity based on either a Flask session cookie or an API Key.
16
+ """
17
+
18
+ @inject
19
+ def __init__(self, profile_service: ProfileService):
20
+ """
21
+ Injects ProfileService to access session information and validate API keys.
22
+ """
23
+ self.profile_service = profile_service
24
+
25
+ def verify(self) -> dict:
26
+ """
27
+ Verifies the current request and identifies the user.
28
+
29
+ Returns a dictionary with:
30
+ - success: bool
31
+ - user_identifier: str (if successful)
32
+ - company_short_name: str (if successful)
33
+ - error_message: str (on failure)
34
+ - status_code: int (on failure)
35
+ """
36
+ # --- Priority 1: Check for a valid Flask web session ---
37
+ session_info = self.profile_service.get_current_session_info()
38
+ if session_info and session_info.get('user_identifier'):
39
+ # User is authenticated via a web session cookie.
40
+ return {
41
+ "success": True,
42
+ "company_short_name": session_info['company_short_name'],
43
+ "user_identifier": session_info['user_identifier']
44
+ }
45
+
46
+ # --- Priority 2: Check for a valid API Key in headers ---
47
+ api_key = None
48
+ auth = request.headers.get('Authorization', '')
49
+ if isinstance(auth, str) and auth.lower().startswith('bearer '):
50
+ api_key = auth.split(' ', 1)[1].strip()
51
+
52
+ if api_key:
53
+ api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
54
+ if not api_key_entry:
55
+ return {"success": False, "error": "Invalid or inactive API Key", "status_code": 401}
56
+
57
+ # obtain the company from the api_key_entry
58
+ company = api_key_entry.company
59
+
60
+ # For API calls, the external_user_id must be provided in the request.
61
+ user_identifier = ''
62
+ if request.is_json:
63
+ data = request.get_json() or {}
64
+ user_identifier = data.get('external_user_id', '')
65
+
66
+ return {
67
+ "success": True,
68
+ "company_short_name": company.short_name,
69
+ "user_identifier": user_identifier
70
+ }
71
+
72
+ # --- Failure: No valid credentials found ---
73
+ return {"success": False, "error": "Authentication required. No session cookie or API Key provided.",
74
+ "status_code": 401}
@@ -10,7 +10,6 @@ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
10
10
  from iatoolkit.repositories.models import Company, Function
11
11
  from iatoolkit.services.excel_service import ExcelService
12
12
  from iatoolkit.services.mail_service import MailService
13
- from iatoolkit.common.session_manager import SessionManager
14
13
  from iatoolkit.common.util import Utility
15
14
  from injector import inject
16
15
  import logging
@@ -171,29 +170,24 @@ class Dispatcher:
171
170
  tools.append(ai_tool)
172
171
  return tools
173
172
 
174
- def get_user_info(self, company_name: str, user_identifier: str, is_local_user: bool) -> dict:
173
+ def get_user_info(self, company_name: str, user_identifier: str) -> dict:
175
174
  if company_name not in self.company_instances:
176
175
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
177
176
  f"Empresa no configurada: {company_name}")
178
177
 
179
- raw_user_data = {}
180
- if is_local_user:
181
- # source 1: local user login into IAToolkit
182
- raw_user_data = SessionManager.get('user', {})
183
- else:
184
- # source 2: external company user
185
- company_instance = self.company_instances[company_name]
186
- try:
187
- raw_user_data = company_instance.get_user_info(user_identifier)
188
- except Exception as e:
189
- logging.exception(e)
190
- raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
191
- f"Error en get_user_info de {company_name}: {str(e)}") from e
178
+ # source 2: external company user
179
+ company_instance = self.company_instances[company_name]
180
+ try:
181
+ raw_user_data = company_instance.get_user_info(user_identifier)
182
+ except Exception as e:
183
+ logging.exception(e)
184
+ raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
185
+ f"Error en get_user_info de {company_name}: {str(e)}") from e
192
186
 
193
187
  # always normalize the data for consistent structure
194
- return self._normalize_user_data(raw_user_data, is_local_user)
188
+ return self._normalize_user_data(raw_user_data)
195
189
 
196
- def _normalize_user_data(self, raw_data: dict, is_local: bool) -> dict:
190
+ def _normalize_user_data(self, raw_data: dict) -> dict:
197
191
  """
198
192
  Asegura que los datos del usuario siempre tengan una estructura consistente.
199
193
  """
@@ -203,10 +197,7 @@ class Dispatcher:
203
197
  "username": raw_data.get("id", 0),
204
198
  "user_email": raw_data.get("email", ""),
205
199
  "user_fullname": raw_data.get("user_fullname", ""),
206
- "company_id": raw_data.get("company_id", 0),
207
- "company_name": raw_data.get("company", ""),
208
- "company_short_name": raw_data.get("company_short_name", ""),
209
- "is_local": is_local,
200
+ "is_local": False,
210
201
  "extras": raw_data.get("extras", {})
211
202
  }
212
203
 
@@ -23,21 +23,21 @@ class ExcelService:
23
23
 
24
24
  def excel_generator(self, **kwargs) -> str:
25
25
  """
26
- Genera un Excel a partir de una lista de diccionarios.
27
-
28
- Parámetros esperados en kwargs:
29
- - filename: str (nombre lógico a mostrar, ej. "reporte_clientes.xlsx") [obligatorio]
30
- - data: list[dict] (filas del excel) [obligatorio]
31
- - sheet_name: str = "hoja 1"
32
-
33
- Retorna:
34
- {
35
- "filename": "reporte.xlsx",
36
- "attachment_token": "8b7f8a66-...-c1c3.xlsx",
37
- "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
38
- "download_link": "/download/8b7f8a66-...-c1c3.xlsx"
39
- }
40
- """
26
+ Genera un Excel a partir de una lista de diccionarios.
27
+
28
+ Parámetros esperados en kwargs:
29
+ - filename: str (nombre lógico a mostrar, ej. "reporte_clientes.xlsx") [obligatorio]
30
+ - data: list[dict] (filas del excel) [obligatorio]
31
+ - sheet_name: str = "hoja 1"
32
+
33
+ Retorna:
34
+ {
35
+ "filename": "reporte.xlsx",
36
+ "attachment_token": "8b7f8a66-...-c1c3.xlsx",
37
+ "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
38
+ "download_link": "/download/8b7f8a66-...-c1c3.xlsx"
39
+ }
40
+ """
41
41
  try:
42
42
  # get the parameters
43
43
  fname = kwargs.get('filename')
@@ -5,29 +5,20 @@
5
5
 
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
-
9
8
  from iatoolkit.repositories.profile_repo import ProfileRepo
10
- from iatoolkit.common.util import Utility
11
9
 
12
10
 
13
11
  class HistoryService:
14
12
  @inject
15
13
  def __init__(self, llm_query_repo: LLMQueryRepo,
16
- profile_repo: ProfileRepo,
17
- util: Utility):
14
+ profile_repo: ProfileRepo):
18
15
  self.llm_query_repo = llm_query_repo
19
16
  self.profile_repo = profile_repo
20
- self.util = util
21
17
 
22
18
  def get_history(self,
23
19
  company_short_name: str,
24
- external_user_id: str = None,
25
- local_user_id: int = 0) -> dict:
20
+ user_identifier: str) -> dict:
26
21
  try:
27
- user_identifier, _ = self.util.resolve_user_identifier(external_user_id, local_user_id)
28
- if not user_identifier:
29
- return {'error': "No se pudo resolver el identificador del usuario"}
30
-
31
22
  # validate company
32
23
  company = self.profile_repo.get_company_by_short_name(company_short_name)
33
24
  if not company: