iatoolkit 0.4.2__py3-none-any.whl → 0.66.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.
Files changed (123) hide show
  1. iatoolkit/__init__.py +13 -35
  2. iatoolkit/base_company.py +74 -8
  3. iatoolkit/cli_commands.py +15 -23
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +46 -0
  6. iatoolkit/common/routes.py +141 -0
  7. iatoolkit/common/session_manager.py +24 -0
  8. iatoolkit/common/util.py +348 -0
  9. iatoolkit/company_registry.py +7 -8
  10. iatoolkit/iatoolkit.py +169 -96
  11. iatoolkit/infra/__init__.py +5 -0
  12. iatoolkit/infra/call_service.py +140 -0
  13. iatoolkit/infra/connectors/__init__.py +5 -0
  14. iatoolkit/infra/connectors/file_connector.py +17 -0
  15. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  16. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  17. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  18. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  19. iatoolkit/infra/connectors/s3_connector.py +33 -0
  20. iatoolkit/infra/gemini_adapter.py +356 -0
  21. iatoolkit/infra/google_chat_app.py +57 -0
  22. iatoolkit/infra/llm_client.py +429 -0
  23. iatoolkit/infra/llm_proxy.py +139 -0
  24. iatoolkit/infra/llm_response.py +40 -0
  25. iatoolkit/infra/mail_app.py +145 -0
  26. iatoolkit/infra/openai_adapter.py +90 -0
  27. iatoolkit/infra/redis_session_manager.py +122 -0
  28. iatoolkit/locales/en.yaml +144 -0
  29. iatoolkit/locales/es.yaml +140 -0
  30. iatoolkit/repositories/__init__.py +5 -0
  31. iatoolkit/repositories/database_manager.py +110 -0
  32. iatoolkit/repositories/document_repo.py +33 -0
  33. iatoolkit/repositories/llm_query_repo.py +91 -0
  34. iatoolkit/repositories/models.py +336 -0
  35. iatoolkit/repositories/profile_repo.py +123 -0
  36. iatoolkit/repositories/tasks_repo.py +52 -0
  37. iatoolkit/repositories/vs_repo.py +139 -0
  38. iatoolkit/services/__init__.py +5 -0
  39. iatoolkit/services/auth_service.py +193 -0
  40. {services → iatoolkit/services}/benchmark_service.py +6 -6
  41. iatoolkit/services/branding_service.py +149 -0
  42. {services → iatoolkit/services}/dispatcher_service.py +39 -99
  43. {services → iatoolkit/services}/document_service.py +5 -5
  44. {services → iatoolkit/services}/excel_service.py +27 -21
  45. {services → iatoolkit/services}/file_processor_service.py +5 -5
  46. iatoolkit/services/help_content_service.py +30 -0
  47. {services → iatoolkit/services}/history_service.py +8 -16
  48. iatoolkit/services/i18n_service.py +104 -0
  49. {services → iatoolkit/services}/jwt_service.py +18 -27
  50. iatoolkit/services/language_service.py +77 -0
  51. {services → iatoolkit/services}/load_documents_service.py +19 -14
  52. {services → iatoolkit/services}/mail_service.py +5 -5
  53. iatoolkit/services/onboarding_service.py +43 -0
  54. {services → iatoolkit/services}/profile_service.py +155 -89
  55. {services → iatoolkit/services}/prompt_manager_service.py +26 -11
  56. {services → iatoolkit/services}/query_service.py +142 -104
  57. {services → iatoolkit/services}/search_service.py +21 -5
  58. {services → iatoolkit/services}/sql_service.py +24 -6
  59. {services → iatoolkit/services}/tasks_service.py +10 -10
  60. iatoolkit/services/user_feedback_service.py +103 -0
  61. iatoolkit/services/user_session_context_service.py +143 -0
  62. iatoolkit/static/images/fernando.jpeg +0 -0
  63. iatoolkit/static/js/chat_feedback_button.js +80 -0
  64. iatoolkit/static/js/chat_filepond.js +85 -0
  65. iatoolkit/static/js/chat_help_content.js +124 -0
  66. iatoolkit/static/js/chat_history_button.js +112 -0
  67. iatoolkit/static/js/chat_logout_button.js +36 -0
  68. iatoolkit/static/js/chat_main.js +364 -0
  69. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  70. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  71. iatoolkit/static/js/chat_reload_button.js +35 -0
  72. iatoolkit/static/styles/chat_iatoolkit.css +592 -0
  73. iatoolkit/static/styles/chat_modal.css +169 -0
  74. iatoolkit/static/styles/chat_public.css +107 -0
  75. iatoolkit/static/styles/landing_page.css +182 -0
  76. iatoolkit/static/styles/llm_output.css +115 -0
  77. iatoolkit/static/styles/onboarding.css +169 -0
  78. iatoolkit/system_prompts/query_main.prompt +5 -15
  79. iatoolkit/templates/_company_header.html +20 -0
  80. iatoolkit/templates/_login_widget.html +42 -0
  81. iatoolkit/templates/about.html +13 -0
  82. iatoolkit/templates/base.html +65 -0
  83. iatoolkit/templates/change_password.html +66 -0
  84. iatoolkit/templates/chat.html +287 -0
  85. iatoolkit/templates/chat_modals.html +181 -0
  86. iatoolkit/templates/error.html +51 -0
  87. iatoolkit/templates/forgot_password.html +50 -0
  88. iatoolkit/templates/index.html +145 -0
  89. iatoolkit/templates/login_simulation.html +34 -0
  90. iatoolkit/templates/onboarding_shell.html +104 -0
  91. iatoolkit/templates/signup.html +76 -0
  92. iatoolkit/views/__init__.py +5 -0
  93. iatoolkit/views/base_login_view.py +92 -0
  94. iatoolkit/views/change_password_view.py +117 -0
  95. iatoolkit/views/external_login_view.py +73 -0
  96. iatoolkit/views/file_store_api_view.py +65 -0
  97. iatoolkit/views/forgot_password_view.py +72 -0
  98. iatoolkit/views/help_content_api_view.py +54 -0
  99. iatoolkit/views/history_api_view.py +56 -0
  100. iatoolkit/views/home_view.py +61 -0
  101. iatoolkit/views/index_view.py +14 -0
  102. iatoolkit/views/init_context_api_view.py +73 -0
  103. iatoolkit/views/llmquery_api_view.py +57 -0
  104. iatoolkit/views/login_simulation_view.py +81 -0
  105. iatoolkit/views/login_view.py +153 -0
  106. iatoolkit/views/logout_api_view.py +49 -0
  107. iatoolkit/views/profile_api_view.py +46 -0
  108. iatoolkit/views/prompt_api_view.py +37 -0
  109. iatoolkit/views/signup_view.py +94 -0
  110. iatoolkit/views/tasks_api_view.py +72 -0
  111. iatoolkit/views/tasks_review_api_view.py +55 -0
  112. iatoolkit/views/user_feedback_api_view.py +60 -0
  113. iatoolkit/views/verify_user_view.py +62 -0
  114. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
  115. iatoolkit-0.66.2.dist-info/RECORD +119 -0
  116. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
  117. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  118. iatoolkit-0.4.2.dist-info/RECORD +0 -32
  119. services/__init__.py +0 -5
  120. services/api_service.py +0 -75
  121. services/user_feedback_service.py +0 -67
  122. services/user_session_context_service.py +0 -85
  123. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
@@ -1,19 +1,17 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
5
-
6
- from common.exceptions import IAToolkitException
7
- from services.prompt_manager_service import PromptService
8
- from services.api_service import ApiService
9
- from repositories.llm_query_repo import LLMQueryRepo
10
- from repositories.models import Company, Function
11
- from services.excel_service import ExcelService
12
- from services.mail_service import MailService
13
- from common.session_manager import SessionManager
14
- from common.util import Utility
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.common.exceptions import IAToolkitException
7
+ from iatoolkit.services.prompt_manager_service import PromptService
8
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
9
+
10
+ from iatoolkit.repositories.models import Company, Function
11
+ from iatoolkit.services.excel_service import ExcelService
12
+ from iatoolkit.services.mail_service import MailService
13
+ from iatoolkit.common.util import Utility
15
14
  from injector import inject
16
- from typing import Dict
17
15
  import logging
18
16
  import os
19
17
 
@@ -24,12 +22,10 @@ class Dispatcher:
24
22
  prompt_service: PromptService,
25
23
  llmquery_repo: LLMQueryRepo,
26
24
  util: Utility,
27
- api_service: ApiService,
28
25
  excel_service: ExcelService,
29
26
  mail_service: MailService):
30
27
  self.prompt_service = prompt_service
31
28
  self.llmquery_repo = llmquery_repo
32
- self.api_service = api_service
33
29
  self.util = util
34
30
  self.excel_service = excel_service
35
31
  self.mail_service = mail_service
@@ -39,16 +35,9 @@ class Dispatcher:
39
35
  self._company_registry = None
40
36
  self._company_instances = None
41
37
 
42
- # load into the dispatcher the configured companies
43
- self.initialize_companies()
44
-
45
- # run the statrtup logic for all companies
46
- self.start_execution()
47
-
48
38
  self.tool_handlers = {
49
39
  "iat_generate_excel": self.excel_service.excel_generator,
50
40
  "iat_send_email": self.mail_service.send_mail,
51
- "iat_api_call": self.api_service.call_api
52
41
  }
53
42
 
54
43
  @property
@@ -66,30 +55,18 @@ class Dispatcher:
66
55
  self._company_instances = self.company_registry.get_all_company_instances()
67
56
  return self._company_instances
68
57
 
69
- def initialize_companies(self):
70
- from iatoolkit import current_iatoolkit
71
- """
72
- Initializes and instantiates all registered company classes.
73
- This method should be called *after* the main injector is fully configured
74
- and the company registry is populated.
75
- """
76
- if self.company_registry.get_all_company_instances(): # Check if already instantiated
77
- return
78
-
79
- # ✅ NOW it is safe to get the injector and instantiate companies.
80
- injector = current_iatoolkit().get_injector()
81
- self.company_registry.instantiate_companies(injector)
82
-
83
-
84
58
  def start_execution(self):
59
+ # initialize the system functions and prompts
60
+ self.setup_iatoolkit_system()
61
+
85
62
  """Runs the startup logic for all registered companies."""
86
- for company_name, company_instance in self.company_instances.items():
87
- logging.info(f'Starting execution for company: {company_name}')
88
- company_instance.start_execution()
63
+ for company in self.company_instances.values():
64
+ company.register_company()
65
+ company.start_execution()
89
66
 
90
67
  return True
91
68
 
92
- def setup_all_companies(self):
69
+ def setup_iatoolkit_system(self):
93
70
  # create system functions
94
71
  for function in self.system_functions:
95
72
  self.llmquery_repo.create_or_update_function(
@@ -102,16 +79,16 @@ class Dispatcher:
102
79
  )
103
80
  )
104
81
 
105
- # create the system prompts
106
- i = 1
107
- for prompt in self.system_prompts:
108
- self.prompt_service.create_prompt(
109
- prompt_name=prompt['name'],
110
- description=prompt['description'],
111
- order=1,
112
- is_system_prompt=True,
113
- )
114
- i += 1
82
+ # create the system prompts
83
+ i = 1
84
+ for prompt in self.system_prompts:
85
+ self.prompt_service.create_prompt(
86
+ prompt_name=prompt['name'],
87
+ description=prompt['description'],
88
+ order=1,
89
+ is_system_prompt=True,
90
+ )
91
+ i += 1
115
92
 
116
93
  # register in the database every company class
117
94
  for company in self.company_instances.values():
@@ -193,51 +170,21 @@ class Dispatcher:
193
170
  tools.append(ai_tool)
194
171
  return tools
195
172
 
196
- 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:
197
174
  if company_name not in self.company_instances:
198
175
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
199
176
  f"Empresa no configurada: {company_name}")
200
177
 
201
- raw_user_data = {}
202
- if is_local_user:
203
- # source 1: local user login into IAToolkit
204
- raw_user_data = SessionManager.get('user', {})
205
- else:
206
- # source 2: external company user
207
- company_instance = self.company_instances[company_name]
208
- try:
209
- raw_user_data = company_instance.get_user_info(user_identifier)
210
- except Exception as e:
211
- logging.exception(e)
212
- raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
213
- f"Error en get_user_info de {company_name}: {str(e)}") from e
214
-
215
- # always normalize the data for consistent structure
216
- return self._normalize_user_data(raw_user_data, is_local_user)
217
-
218
- def _normalize_user_data(self, raw_data: dict, is_local: bool) -> dict:
219
- """
220
- Asegura que los datos del usuario siempre tengan una estructura consistente.
221
- """
222
- # Valores por defecto para un perfil robusto
223
- normalized_user = {
224
- "id": raw_data.get("id", 0),
225
- "user_email": raw_data.get("email", ""),
226
- "user_fullname": raw_data.get("user_fullname", ""),
227
- "super_user": raw_data.get("super_user", False),
228
- "company_id": raw_data.get("company_id", 0),
229
- "company_name": raw_data.get("company", ""),
230
- "company_short_name": raw_data.get("company_short_name", ""),
231
- "is_local": is_local,
232
- "extras": raw_data.get("extras", {})
233
- }
234
-
235
- # get the extras from the raw data, if any
236
- extras = raw_data.get("extras", {})
237
- if isinstance(extras, dict):
238
- normalized_user.update(extras)
178
+ # source 2: external company user
179
+ company_instance = self.company_instances[company_name]
180
+ try:
181
+ external_user_profile = 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
239
186
 
240
- return normalized_user
187
+ return external_user_profile
241
188
 
242
189
  def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
243
190
  if company_name not in self.company_instances:
@@ -256,13 +203,6 @@ class Dispatcher:
256
203
  """Returns the instance for a given company name."""
257
204
  return self.company_instances.get(company_name)
258
205
 
259
- def get_registered_companies(self) -> dict:
260
- """Gets all registered companies (for debugging/admin purposes)"""
261
- return {
262
- "registered_classes": list(self.company_registry.get_registered_companies().keys()),
263
- "instantiated": list(self.company_instances.keys()),
264
- "count": len(self.company_instances)
265
- }
266
206
 
267
207
 
268
208
  # iatoolkit system prompts
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
5
 
6
6
  from docx import Document
7
7
  import fitz # PyMuPDF
@@ -10,7 +10,7 @@ import io
10
10
  import os
11
11
  import pytesseract
12
12
  from injector import inject
13
- from common.exceptions import IAToolkitException
13
+ from iatoolkit.common.exceptions import IAToolkitException
14
14
 
15
15
  class DocumentService:
16
16
  @inject
@@ -22,7 +22,7 @@ class DocumentService:
22
22
  try:
23
23
  if filename.lower().endswith('.docx'):
24
24
  return self.read_docx(file_content)
25
- elif filename.lower().endswith('.txt'):
25
+ elif filename.lower().endswith('.txt') or filename.lower().endswith('.md'):
26
26
  if isinstance(file_content, bytes):
27
27
  try:
28
28
  # decode using UTF-8
@@ -1,13 +1,13 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
5
 
6
- from common.util import Utility
6
+ from iatoolkit.common.util import Utility
7
7
  import pandas as pd
8
8
  from uuid import uuid4
9
9
  from pathlib import Path
10
- from common.exceptions import IAToolkitException
10
+ from iatoolkit.common.exceptions import IAToolkitException
11
11
  from injector import inject
12
12
  import os
13
13
  import logging
@@ -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')
@@ -55,7 +55,13 @@ class ExcelService:
55
55
 
56
56
  # 3. create temporary name
57
57
  token = f"{uuid4()}.xlsx"
58
- filepath = Path("static/temp") / token
58
+
59
+ # 4. check that download directory is configured
60
+ if 'IATOOLKIT_DOWNLOAD_DIR' not in current_app.config:
61
+ return 'no esta configurado el directorio temporal para guardar excels'
62
+
63
+ download_dir = current_app.config['IATOOLKIT_DOWNLOAD_DIR']
64
+ filepath = Path(download_dir) / token
59
65
  filepath.parent.mkdir(parents=True, exist_ok=True)
60
66
 
61
67
  # 4. save excel file in temporary directory
@@ -1,13 +1,13 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
5
 
6
- from infra.connectors.file_connector import FileConnector
6
+ from iatoolkit.infra.connectors.file_connector import FileConnector
7
7
  import logging
8
8
  import os
9
9
  from typing import Optional, Callable, Dict
10
- from repositories.models import Company
10
+ from iatoolkit.repositories.models import Company
11
11
 
12
12
 
13
13
  class FileProcessorConfig:
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.common.util import Utility
7
+ from iatoolkit.common.exceptions import IAToolkitException
8
+ import os
9
+ from injector import inject
10
+ import logging
11
+
12
+
13
+ class HelpContentService:
14
+ @inject
15
+ def __init__(self, util: Utility):
16
+ self.util = util
17
+
18
+ def get_content(self, company_short_name: str | None) -> dict:
19
+ filepath = f'companies/{company_short_name}/help_content.yaml'
20
+ if not os.path.exists(filepath):
21
+ return {}
22
+
23
+ # read the file
24
+ try:
25
+ help_content = self.util.load_schema_from_yaml(filepath)
26
+ return help_content
27
+ except Exception as e:
28
+ logging.exception(e)
29
+ raise IAToolkitException(IAToolkitException.ErrorType.CONFIG_ERROR,
30
+ f"Error obteniendo help de {company_short_name}: {str(e)}") from e
@@ -1,32 +1,24 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
5
 
6
6
  from injector import inject
7
- from repositories.llm_query_repo import LLMQueryRepo
8
- from repositories.profile_repo import ProfileRepo
9
- from common.util import Utility
7
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
+ from iatoolkit.repositories.profile_repo import ProfileRepo
10
9
 
11
10
 
12
11
  class HistoryService:
13
12
  @inject
14
13
  def __init__(self, llm_query_repo: LLMQueryRepo,
15
- profile_repo: ProfileRepo,
16
- util: Utility):
14
+ profile_repo: ProfileRepo):
17
15
  self.llm_query_repo = llm_query_repo
18
16
  self.profile_repo = profile_repo
19
- self.util = util
20
17
 
21
18
  def get_history(self,
22
19
  company_short_name: str,
23
- external_user_id: str = None,
24
- local_user_id: int = 0) -> dict:
20
+ user_identifier: str) -> dict:
25
21
  try:
26
- user_identifier, _ = self.util.resolve_user_identifier(external_user_id, local_user_id)
27
- if not user_identifier:
28
- return {'error': "No se pudo resolver el identificador del usuario"}
29
-
30
22
  # validate company
31
23
  company = self.profile_repo.get_company_by_short_name(company_short_name)
32
24
  if not company:
@@ -35,7 +27,7 @@ class HistoryService:
35
27
  history = self.llm_query_repo.get_history(company, user_identifier)
36
28
 
37
29
  if not history:
38
- return {'error': 'No se pudo obtener el historial'}
30
+ return {'message': 'Historial vacio actualmente', 'history': []}
39
31
 
40
32
  history_list = [query.to_dict() for query in history]
41
33
 
@@ -0,0 +1,104 @@
1
+ # iatoolkit/services/i18n_service.py
2
+ import os
3
+ import logging
4
+ from injector import inject
5
+ from iatoolkit.common.util import Utility
6
+ from iatoolkit.services.language_service import LanguageService
7
+
8
+
9
+ class I18nService:
10
+ """
11
+ Servicio centralizado para manejar la internacionalización (i18n).
12
+ Carga todas las traducciones desde archivos YAML en memoria al iniciar.
13
+ """
14
+ FALLBACK_LANGUAGE = 'es'
15
+
16
+ @inject
17
+ def __init__(self, util: Utility, language_service: LanguageService):
18
+ self.util = util
19
+ self.language_service = language_service
20
+
21
+ self.translations = {}
22
+ self._load_translations()
23
+
24
+ def _load_translations(self):
25
+ """
26
+ Carga todos los archivos .yaml del directorio 'locales' en memoria.
27
+ """
28
+ locales_dir = os.path.join(os.path.dirname(__file__), '..', 'locales')
29
+ if not os.path.exists(locales_dir):
30
+ logging.error("El directorio 'locales' no fue encontrado.")
31
+ return
32
+
33
+ for filename in os.listdir(locales_dir):
34
+ if filename.endswith('.yaml'):
35
+ lang_code = filename.split('.')[0]
36
+ filepath = os.path.join(locales_dir, filename)
37
+ try:
38
+ self.translations[lang_code] = self.util.load_schema_from_yaml(filepath)
39
+ except Exception as e:
40
+ logging.error(f"Fallo al cargar el archivo de traducción {filepath}: {e}")
41
+
42
+ def _get_nested_key(self, lang: str, key: str):
43
+ """
44
+ Obtiene un valor de un diccionario anidado usando una clave con puntos.
45
+ """
46
+ data = self.translations.get(lang, {})
47
+ keys = key.split('.')
48
+ for k in keys:
49
+ if isinstance(data, dict) and k in data:
50
+ data = data[k]
51
+ else:
52
+ return None
53
+ return data
54
+
55
+ def get_translation_block(self, key: str, lang: str = None) -> dict:
56
+ """
57
+ Gets a whole dictionary block from the translations.
58
+ Useful for passing a set of translations to JavaScript.
59
+ """
60
+ if lang is None:
61
+ lang = self.language_service.get_current_language()
62
+
63
+ # 1. Try to get the block in the requested language
64
+ block = self._get_nested_key(lang, key)
65
+
66
+ # 2. If not found, try the fallback language
67
+ if not isinstance(block, dict):
68
+ block = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
69
+
70
+ return block if isinstance(block, dict) else {}
71
+
72
+ def t(self, key: str, lang: str = None, **kwargs) -> str:
73
+ """
74
+ Gets the translation for a given key.
75
+ If 'lang' is provided, it's used. Otherwise, it's determined automatically.
76
+ """
77
+ # If no specific language is requested, determine it from the current context.
78
+ if lang is None:
79
+ lang = self.language_service.get_current_language()
80
+
81
+ # 1. Attempt to get the translation in the requested language
82
+ message = self._get_nested_key(lang, key)
83
+
84
+ # 2. If not found, try the fallback language
85
+ if message is None and lang != self.FALLBACK_LANGUAGE:
86
+ logging.warning(
87
+ f"Translation key '{key}' not found for language '{lang}'. Attempting fallback to '{self.FALLBACK_LANGUAGE}'.")
88
+ message = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
89
+
90
+ # 3. If still not found, return the key itself as a last resort
91
+ if message is None:
92
+ logging.error(
93
+ f"Translation key '{key}' not found, even in fallback '{self.FALLBACK_LANGUAGE}'.")
94
+ return key
95
+
96
+ # 4. If variables are provided, format the message
97
+ if kwargs:
98
+ try:
99
+ return message.format(**kwargs)
100
+ except KeyError as e:
101
+ logging.error(f"Error formatting key '{key}': missing variable {e} in arguments.")
102
+ return message
103
+
104
+ return message
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
5
 
6
6
  import jwt
7
7
  import time
@@ -24,16 +24,18 @@ class JWTService:
24
24
  raise RuntimeError(f"Configuración JWT esencial faltante: {e}")
25
25
 
26
26
  def generate_chat_jwt(self,
27
- company_id: int,
28
27
  company_short_name: str,
29
- external_user_id: str,
28
+ user_identifier: str,
30
29
  expires_delta_seconds: int) -> Optional[str]:
31
30
  # generate a JWT for a chat session
32
31
  try:
32
+ if not company_short_name or not user_identifier:
33
+ logging.error(f"Missing token ID: {company_short_name}/{user_identifier}")
34
+ return None
35
+
33
36
  payload = {
34
- 'company_id': company_id,
35
37
  'company_short_name': company_short_name,
36
- 'external_user_id': external_user_id,
38
+ 'user_identifier': user_identifier,
37
39
  'exp': time.time() + expires_delta_seconds,
38
40
  'iat': time.time(),
39
41
  'type': 'chat_session' # Identificador del tipo de token
@@ -41,10 +43,10 @@ class JWTService:
41
43
  token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
42
44
  return token
43
45
  except Exception as e:
44
- logging.error(f"Error al generar JWT para company {company_id}, user {external_user_id}: {e}")
46
+ logging.error(f"Error al generar JWT para {company_short_name}/{user_identifier}: {e}")
45
47
  return None
46
48
 
47
- def validate_chat_jwt(self, token: str, expected_company_short_name: str) -> Optional[Dict[str, Any]]:
49
+ def validate_chat_jwt(self, token: str) -> Optional[Dict[str, Any]]:
48
50
  """
49
51
  Valida un JWT de sesión de chat.
50
52
  Retorna el payload decodificado si es válido y coincide con la empresa, o None.
@@ -59,33 +61,22 @@ class JWTService:
59
61
  logging.warning(f"Validación JWT fallida: tipo incorrecto '{payload.get('type')}'")
60
62
  return None
61
63
 
62
- if payload.get('company_short_name') != expected_company_short_name:
63
- logging.warning(
64
- f"Validación JWT fallida: company_short_name no coincide. "
65
- f"Esperado: {expected_company_short_name}, Obtenido: {payload.get('company_short_name')}"
66
- )
64
+ # user_identifier debe estar presente
65
+ if not payload.get('user_identifier'):
66
+ logging.warning(f"Validación JWT fallida: user_identifier ausente o vacío.")
67
67
  return None
68
68
 
69
- # external_user_id debe estar presente
70
- if 'external_user_id' not in payload or not payload['external_user_id']:
71
- logging.warning(f"Validación JWT fallida: external_user_id ausente o vacío.")
72
- return None
73
-
74
- # company_id debe estar presente
75
- if 'company_id' not in payload or not isinstance(payload['company_id'], int):
76
- logging.warning(f"Validación JWT fallida: company_id ausente o tipo incorrecto.")
69
+ if not payload.get('company_short_name'):
70
+ logging.warning(f"Validación JWT fallida: company_short_name ausente.")
77
71
  return None
78
72
 
79
73
  logging.debug(
80
74
  f"JWT validado exitosamente para company: {payload.get('company_short_name')}, user: {payload.get('external_user_id')}")
81
75
  return payload
82
76
 
83
- except jwt.ExpiredSignatureError:
84
- logging.info(f"Validación JWT fallida: token expirado para {expected_company_short_name}")
85
- return None
86
77
  except jwt.InvalidTokenError as e:
87
- logging.warning(f"Validación JWT fallida: token inválido para {expected_company_short_name}. Error: {e}")
78
+ logging.warning(f"Validación JWT fallida: token inválido . Error: {e}")
88
79
  return None
89
80
  except Exception as e:
90
- logging.error(f"Error inesperado durante validación de JWT para {expected_company_short_name}: {e}")
81
+ logging.error(f"Error inesperado durante validación de JWT : {e}")
91
82
  return None
@@ -0,0 +1,77 @@
1
+ # iatoolkit/services/language_service.py
2
+
3
+ import logging
4
+ from injector import inject
5
+ from flask import g, request
6
+ from iatoolkit.repositories.profile_repo import ProfileRepo
7
+ from iatoolkit.common.session_manager import SessionManager
8
+
9
+
10
+ class LanguageService:
11
+ """
12
+ Determines the correct language for the current request
13
+ based on a defined priority order (session, URL, etc.)
14
+ and caches it in the Flask 'g' object for the request's lifecycle.
15
+ """
16
+
17
+ @inject
18
+ def __init__(self, profile_repo: ProfileRepo):
19
+ self.profile_repo = profile_repo
20
+
21
+ def _get_company_short_name(self) -> str | None:
22
+ """
23
+ Gets the company_short_name from the current request context.
24
+ This handles different scenarios like web sessions, public URLs, and API calls.
25
+
26
+ Priority Order:
27
+ 1. Flask Session (for logged-in web users).
28
+ 2. URL rule variable (for public pages and API endpoints).
29
+ """
30
+ # 1. Check session for logged-in users
31
+ company_short_name = SessionManager.get('company_short_name')
32
+ if company_short_name:
33
+ return company_short_name
34
+
35
+ # 2. Check URL arguments (e.g., /<company_short_name>/login)
36
+ # This covers public pages and most API calls.
37
+ if request.view_args and 'company_short_name' in request.view_args:
38
+ return request.view_args['company_short_name']
39
+
40
+ return None
41
+
42
+ def get_current_language(self) -> str:
43
+ """
44
+ Determines and caches the language for the current request using a priority order:
45
+ 1. User's preference (from their profile).
46
+ 2. Company's default language.
47
+ 3. System-wide fallback language ('es').
48
+ """
49
+ if 'lang' in g:
50
+ return g.lang
51
+
52
+ from iatoolkit.services.i18n_service import I18nService
53
+ lang = I18nService.FALLBACK_LANGUAGE
54
+
55
+ try:
56
+ company_short_name = self._get_company_short_name()
57
+ if company_short_name:
58
+ # Prioridad 1: Preferencia del Usuario
59
+ user_identifier = SessionManager.get('user_identifier')
60
+ if user_identifier:
61
+ # Usamos el repositorio para obtener el objeto User
62
+ user = self.profile_repo.get_user_by_email(
63
+ user_identifier) # Asumiendo que el email es el identificador
64
+ if user and user.preferred_language:
65
+ g.lang = user.preferred_language
66
+ return g.lang
67
+
68
+ # Prioridad 2: Idioma por defecto de la Compañía (si no se encontró preferencia de usuario)
69
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
70
+ if company and company.default_language:
71
+ lang = company.default_language
72
+ except Exception as e:
73
+ logging.debug(f"Could not determine language, falling back to default. Reason: {e}")
74
+ pass
75
+
76
+ g.lang = lang
77
+ return lang