iatoolkit 0.63.1__py3-none-any.whl → 0.67.0__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 (78) hide show
  1. iatoolkit/__init__.py +2 -0
  2. iatoolkit/base_company.py +1 -20
  3. iatoolkit/common/routes.py +11 -2
  4. iatoolkit/common/session_manager.py +2 -0
  5. iatoolkit/common/util.py +17 -0
  6. iatoolkit/company_registry.py +1 -2
  7. iatoolkit/iatoolkit.py +41 -5
  8. iatoolkit/locales/en.yaml +167 -0
  9. iatoolkit/locales/es.yaml +163 -0
  10. iatoolkit/repositories/database_manager.py +3 -3
  11. iatoolkit/repositories/document_repo.py +1 -1
  12. iatoolkit/repositories/models.py +2 -3
  13. iatoolkit/repositories/profile_repo.py +0 -4
  14. iatoolkit/services/auth_service.py +14 -9
  15. iatoolkit/services/branding_service.py +32 -22
  16. iatoolkit/services/configuration_service.py +140 -0
  17. iatoolkit/services/dispatcher_service.py +20 -18
  18. iatoolkit/services/document_service.py +5 -2
  19. iatoolkit/services/excel_service.py +15 -11
  20. iatoolkit/services/file_processor_service.py +4 -12
  21. iatoolkit/services/history_service.py +8 -7
  22. iatoolkit/services/i18n_service.py +104 -0
  23. iatoolkit/services/jwt_service.py +7 -9
  24. iatoolkit/services/language_service.py +79 -0
  25. iatoolkit/services/load_documents_service.py +4 -4
  26. iatoolkit/services/mail_service.py +9 -4
  27. iatoolkit/services/onboarding_service.py +10 -4
  28. iatoolkit/services/profile_service.py +58 -38
  29. iatoolkit/services/prompt_manager_service.py +20 -16
  30. iatoolkit/services/query_service.py +15 -14
  31. iatoolkit/services/sql_service.py +6 -2
  32. iatoolkit/services/user_feedback_service.py +16 -14
  33. iatoolkit/static/js/chat_feedback_button.js +57 -87
  34. iatoolkit/static/js/chat_help_content.js +124 -0
  35. iatoolkit/static/js/chat_history_button.js +48 -65
  36. iatoolkit/static/js/chat_main.js +27 -24
  37. iatoolkit/static/js/chat_reload_button.js +28 -45
  38. iatoolkit/static/styles/chat_iatoolkit.css +223 -315
  39. iatoolkit/static/styles/chat_modal.css +63 -97
  40. iatoolkit/static/styles/chat_public.css +107 -0
  41. iatoolkit/static/styles/landing_page.css +0 -1
  42. iatoolkit/templates/_company_header.html +6 -2
  43. iatoolkit/templates/_login_widget.html +42 -0
  44. iatoolkit/templates/base.html +34 -19
  45. iatoolkit/templates/change_password.html +22 -20
  46. iatoolkit/templates/chat.html +58 -27
  47. iatoolkit/templates/chat_modals.html +113 -74
  48. iatoolkit/templates/error.html +12 -13
  49. iatoolkit/templates/forgot_password.html +11 -7
  50. iatoolkit/templates/index.html +8 -3
  51. iatoolkit/templates/login_simulation.html +16 -5
  52. iatoolkit/templates/onboarding_shell.html +0 -1
  53. iatoolkit/templates/signup.html +14 -14
  54. iatoolkit/views/base_login_view.py +12 -1
  55. iatoolkit/views/change_password_view.py +49 -33
  56. iatoolkit/views/forgot_password_view.py +20 -19
  57. iatoolkit/views/help_content_api_view.py +54 -0
  58. iatoolkit/views/history_api_view.py +13 -9
  59. iatoolkit/views/home_view.py +30 -38
  60. iatoolkit/views/init_context_api_view.py +16 -11
  61. iatoolkit/views/llmquery_api_view.py +38 -26
  62. iatoolkit/views/login_simulation_view.py +14 -2
  63. iatoolkit/views/login_view.py +47 -35
  64. iatoolkit/views/logout_api_view.py +26 -22
  65. iatoolkit/views/profile_api_view.py +46 -0
  66. iatoolkit/views/prompt_api_view.py +6 -6
  67. iatoolkit/views/signup_view.py +26 -24
  68. iatoolkit/views/user_feedback_api_view.py +19 -18
  69. iatoolkit/views/verify_user_view.py +30 -29
  70. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/METADATA +40 -22
  71. iatoolkit-0.67.0.dist-info/RECORD +120 -0
  72. iatoolkit-0.67.0.dist-info/licenses/LICENSE +21 -0
  73. iatoolkit/static/styles/chat_info.css +0 -53
  74. iatoolkit/templates/header.html +0 -31
  75. iatoolkit/templates/test.html +0 -9
  76. iatoolkit-0.63.1.dist-info/RECORD +0 -112
  77. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/WHEEL +0 -0
  78. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
@@ -74,10 +74,6 @@ class ProfileRepo:
74
74
  if company:
75
75
  if company.parameters != new_company.parameters:
76
76
  company.parameters = new_company.parameters
77
- if company.branding != new_company.branding:
78
- company.branding = new_company.branding
79
- if company.onboarding_cards != new_company.onboarding_cards:
80
- company.onboarding_cards = new_company.onboarding_cards
81
77
  else:
82
78
  # Si la compañía no existe, la añade a la sesión.
83
79
  self.session.add(new_company)
@@ -7,6 +7,7 @@ from flask import request
7
7
  from injector import inject
8
8
  from iatoolkit.services.profile_service import ProfileService
9
9
  from iatoolkit.services.jwt_service import JWTService
10
+ from iatoolkit.services.i18n_service import I18nService
10
11
  from iatoolkit.repositories.database_manager import DatabaseManager
11
12
  from iatoolkit.repositories.models import AccessLog
12
13
  from flask import request
@@ -23,18 +24,20 @@ class AuthService:
23
24
  @inject
24
25
  def __init__(self, profile_service: ProfileService,
25
26
  jwt_service: JWTService,
26
- db_manager: DatabaseManager
27
+ db_manager: DatabaseManager,
28
+ i18n_service: I18nService
27
29
  ):
28
30
  self.profile_service = profile_service
29
31
  self.jwt_service = jwt_service
30
32
  self.db_manager = db_manager
33
+ self.i18n_service = i18n_service
31
34
 
32
35
  def login_local_user(self, company_short_name: str, email: str, password: str) -> dict:
33
36
  # try to autenticate a local user, register the event and return the result
34
37
  auth_response = self.profile_service.login(
35
38
  company_short_name=company_short_name,
36
39
  email=email,
37
- password=password
40
+ password=password,
38
41
  )
39
42
 
40
43
  if not auth_response.get('success'):
@@ -66,7 +69,7 @@ class AuthService:
66
69
  outcome='failure',
67
70
  reason_code='JWT_INVALID'
68
71
  )
69
- return {'success': False, 'error': 'Token inválido o expirado'}
72
+ return {'success': False, 'error': self.i18n_service.t('errors.auth.invalid_or_expired_token')}
70
73
 
71
74
  # 2. if token is valid, extract the user_identifier
72
75
  user_identifier = payload.get('user_identifier')
@@ -81,7 +84,7 @@ class AuthService:
81
84
  )
82
85
  return {'success': True, 'user_identifier': user_identifier}
83
86
  except Exception as e:
84
- logging.error(f"Error al crear la sesión desde token para {user_identifier}: {e}")
87
+ logging.error(f"error creeating session for Token of {user_identifier}: {e}")
85
88
  self.log_access(
86
89
  company_short_name=company_short_name,
87
90
  auth_type='redeem_token',
@@ -89,7 +92,7 @@ class AuthService:
89
92
  reason_code='SESSION_CREATION_FAILED',
90
93
  user_identifier=user_identifier
91
94
  )
92
- return {'success': False, 'error': 'No se pudo crear la sesión del usuario'}
95
+ return {'success': False, 'error': self.i18n_service.t('errors.auth.session_creation_failed')}
93
96
 
94
97
  def verify(self, anonymous: bool = False) -> dict:
95
98
  """
@@ -123,14 +126,15 @@ class AuthService:
123
126
  # --- Failure: No valid credentials found ---
124
127
  logging.info(f"Authentication required. No session cookie or API Key provided.")
125
128
  return {"success": False,
126
- "error_message": "Authentication required. No session cookie or API Key provided.",
129
+ "error_message": self.i18n_service.t('errors.auth.authentication_required'),
127
130
  "status_code": 401}
128
131
 
129
132
  # check if the api-key is valid and active
130
133
  api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
131
134
  if not api_key_entry:
132
135
  logging.info(f"Invalid or inactive API Key {api_key}")
133
- return {"success": False, "error_message": "Invalid or inactive API Key",
136
+ return {"success": False,
137
+ "error_message": self.i18n_service.t('errors.auth.invalid_api_key'),
134
138
  "status_code": 402}
135
139
 
136
140
  # get the company from the api_key_entry
@@ -141,7 +145,8 @@ class AuthService:
141
145
  user_identifier = data.get('user_identifier', '')
142
146
  if not anonymous and not user_identifier:
143
147
  logging.info(f"No user_identifier provided for API call.")
144
- return {"success": False, "error_message": "No user_identifier provided for API call.",
148
+ return {"success": False,
149
+ "error_message": self.i18n_service.t('errors.auth.no_user_identifier_api'),
145
150
  "status_code": 403}
146
151
 
147
152
  return {
@@ -184,5 +189,5 @@ class AuthService:
184
189
  session.commit()
185
190
 
186
191
  except Exception as e:
187
- logging.error(f"Fallo al escribir en AccessLog: {e}", exc_info=False)
192
+ logging.error(f"error writting to AccessLog: {e}", exc_info=False)
188
193
  session.rollback()
@@ -4,14 +4,17 @@
4
4
  # IAToolkit is open source software.
5
5
 
6
6
  from iatoolkit.repositories.models import Company
7
+ from iatoolkit.services.configuration_service import ConfigurationService
8
+ from injector import inject
7
9
 
8
10
 
9
11
  class BrandingService:
10
12
  """
11
- Servicio centralizado que gestiona la configuración de branding.
13
+ Branding configuration for IAToolkit
12
14
  """
13
-
14
- def __init__(self):
15
+ @inject
16
+ def __init__(self, config_service: ConfigurationService):
17
+ self.config_service = config_service
15
18
  """
16
19
  Define los estilos de branding por defecto para la aplicación.
17
20
  """
@@ -19,13 +22,16 @@ class BrandingService:
19
22
  # --- Estilos del Encabezado Principal ---
20
23
  "header_background_color": "#FFFFFF",
21
24
  "header_text_color": "#6C757D",
22
- "primary_font_weight": "bold",
23
- "primary_font_size": "1rem",
24
- "secondary_font_weight": "600",
25
- "secondary_font_size": "0.875rem",
26
- "tertiary_font_weight": "normal",
27
- "tertiary_font_size": "0.75rem",
28
- "tertiary_opacity": "0.8",
25
+ "primary_font_weight": "600",
26
+ "primary_font_size": "1.2rem",
27
+ "secondary_font_weight": "400",
28
+ "secondary_font_size": "0.9rem",
29
+ "tertiary_font_weight": "300",
30
+ "tertiary_font_size": "0.8rem",
31
+ "tertiary_opacity": "0.7",
32
+
33
+ # headings
34
+ "brand_text_heading_color": "#334155", # Gris pizarra por defecto
29
35
 
30
36
  # Estilos Globales de la Marca ---
31
37
  "brand_primary_color": "#0d6efd", # Azul de Bootstrap por defecto
@@ -40,25 +46,27 @@ class BrandingService:
40
46
  "brand_danger_border": "#f5c2c7", # Borde rojo intermedio
41
47
 
42
48
  # Estilos para Alertas Informativas ---
43
- "brand_info_bg": "#cff4fc", # Fondo celeste pálido
44
- "brand_info_text": "#055160", # Texto azul oscuro
45
- "brand_info_border": "#b6effb",
49
+ "brand_info_bg": "#F0F4F8", # Un fondo de gris azulado muy pálido
50
+ "brand_info_text": "#0d6efd", # Texto en el color primario
51
+ "brand_info_border": "#D9E2EC", # Borde de gris azulado pálido
46
52
 
47
53
  # Estilos para el Asistente de Prompts ---
48
54
  "prompt_assistant_bg": "#f8f9fa",
49
55
  "prompt_assistant_border": "#dee2e6",
50
- "prompt_assistant_icon_color": "#6c757d",
51
56
  "prompt_assistant_button_bg": "#FFFFFF",
52
57
  "prompt_assistant_button_text": "#495057",
53
58
  "prompt_assistant_button_border": "#ced4da",
54
59
  "prompt_assistant_dropdown_bg": "#f8f9fa",
55
60
  "prompt_assistant_header_bg": "#e9ecef",
56
61
  "prompt_assistant_header_text": "#495057",
57
- "prompt_assistant_item_hover_bg": None, # Usará el primario por defecto
58
- "prompt_assistant_item_hover_text": None, # Usará el texto sobre primario
62
+
63
+ # this use the primary by default
64
+ "prompt_assistant_icon_color": None,
65
+ "prompt_assistant_item_hover_bg": None,
66
+ "prompt_assistant_item_hover_text": None,
59
67
 
60
68
  # Color para el botón de Enviar ---
61
- "send_button_color": "#212529" # Gris oscuro/casi negro por defecto
69
+ "send_button_color": "#212529" # Gris oscuro/casi negro por defecto
62
70
  }
63
71
 
64
72
  def get_company_branding(self, company: Company | None) -> dict:
@@ -68,8 +76,9 @@ class BrandingService:
68
76
  """
69
77
  final_branding_values = self._default_branding.copy()
70
78
 
71
- if company and company.branding:
72
- final_branding_values.update(company.branding)
79
+ if company:
80
+ branding_data = self.config_service.get_company_content(company.short_name, 'branding')
81
+ final_branding_values.update(branding_data)
73
82
 
74
83
  # Función para convertir HEX a RGB
75
84
  def hex_to_rgb(hex_color):
@@ -101,6 +110,7 @@ class BrandingService:
101
110
  --brand-secondary-color: {final_branding_values['brand_secondary_color']};
102
111
  --brand-header-bg: {final_branding_values['header_background_color']};
103
112
  --brand-header-text: {final_branding_values['header_text_color']};
113
+ --brand-text-heading-color: {final_branding_values['brand_text_heading_color']};
104
114
 
105
115
  --brand-primary-color-rgb: {', '.join(map(str, primary_rgb))};
106
116
  --brand-secondary-color-rgb: {', '.join(map(str, secondary_rgb))};
@@ -113,11 +123,11 @@ class BrandingService:
113
123
  --brand-danger-text: {final_branding_values['brand_danger_text']};
114
124
  --brand-danger-border: {final_branding_values['brand_danger_border']};
115
125
  --brand-info-bg: {final_branding_values['brand_info_bg']};
116
- --brand-info-text: {final_branding_values['brand_info_text']};
126
+ --brand-info-text: {final_branding_values['brand_info_text'] or final_branding_values['brand_primary_color']};
117
127
  --brand-info-border: {final_branding_values['brand_info_border']};
118
128
  --brand-prompt-assistant-bg: {final_branding_values['prompt_assistant_bg']};
119
129
  --brand-prompt-assistant-border: {final_branding_values['prompt_assistant_border']};
120
- --brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color']};
130
+ --brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color'] or final_branding_values['brand_primary_color']};
121
131
  --brand-prompt-assistant-button-bg: {final_branding_values['prompt_assistant_button_bg']};
122
132
  --brand-prompt-assistant-button-text: {final_branding_values['prompt_assistant_button_text']};
123
133
  --brand-prompt-assistant-button-border: {final_branding_values['prompt_assistant_button_border']};
@@ -137,5 +147,5 @@ class BrandingService:
137
147
  "tertiary_text_style": tertiary_text_style,
138
148
  "header_text_color": final_branding_values['header_text_color'],
139
149
  "css_variables": css_variables,
140
- "send_button_color": final_branding_values['send_button_color']
150
+ "send_button_color": final_branding_values['brand_primary_color']
141
151
  }
@@ -0,0 +1,140 @@
1
+ # iatoolkit/services/configuration_service.py
2
+ # Copyright (c) 2024 Fernando Libedinsky
3
+ # Product: IAToolkit
4
+
5
+ from pathlib import Path
6
+ from iatoolkit import BaseCompany
7
+ from iatoolkit.repositories.profile_repo import ProfileRepo
8
+ from iatoolkit.repositories.models import Company
9
+ from iatoolkit.common.util import Utility
10
+ from injector import inject
11
+ import logging
12
+
13
+ class ConfigurationService:
14
+ """
15
+ Orchestrates the configuration of a Company by reading its YAML files
16
+ and using the BaseCompany's protected methods to register settings.
17
+ """
18
+
19
+ @inject
20
+ def __init__(self,
21
+ profile_repo: ProfileRepo,
22
+ utility: Utility):
23
+ self.profile_repo = profile_repo
24
+ self.utility = utility
25
+ self._loaded_configs = {} # cache for store loaded configurations
26
+
27
+ def get_company_content(self, company_short_name: str, content_key: str) -> dict | list | None:
28
+ """
29
+ Public method to provide a specific section of a company's configuration.
30
+ It uses a cache to avoid reading files from disk on every call.
31
+ """
32
+ self._ensure_config_loaded(company_short_name)
33
+ return self._loaded_configs[company_short_name].get(content_key)
34
+
35
+ def register_company(self, company_short_name: str, company_instance: BaseCompany):
36
+ """
37
+ Main entry point for configuring a company instance.
38
+ This method is invoked by the dispatcher for each registered company.
39
+ """
40
+ logging.info(f"⚙️ Starting configuration for company '{company_short_name}'...")
41
+
42
+ # 1. identify the instance with his name and load info from database
43
+ company_instance.id = company_short_name
44
+ company_instance.company = self.profile_repo.get_company_by_short_name(company_short_name)
45
+
46
+ # 2. Load the main configuration file and supplementary content files
47
+ config = self._load_and_merge_configs(company_short_name)
48
+
49
+ # 3. Register core company details and get the database object
50
+ company_db_object = self._register_core_details(company_instance, config)
51
+
52
+ # 4. Register tools (functions)
53
+ self._register_tools(company_instance, config.get('tools', []))
54
+
55
+ # 5. Register prompt categories and prompts
56
+ self._register_prompts(company_instance, config)
57
+
58
+ # 6. Link the persisted Company object back to the running instance
59
+ company_instance.company = company_db_object
60
+
61
+ logging.info(f"✅ Company '{company_short_name}' configured successfully.")
62
+
63
+ def _ensure_config_loaded(self, company_short_name: str):
64
+ """
65
+ Checks if the configuration for a company is in the cache.
66
+ If not, it loads it from files and stores it.
67
+ """
68
+ if company_short_name not in self._loaded_configs:
69
+ self._loaded_configs[company_short_name] = self._load_and_merge_configs(company_short_name)
70
+
71
+ def _load_and_merge_configs(self, company_short_name: str) -> dict:
72
+ """
73
+ Loads the main company.yaml and merges data from supplementary files
74
+ specified in the 'content_files' section.
75
+ """
76
+ config_dir = Path("companies") / company_short_name / "config"
77
+ main_config_path = config_dir / "company.yaml"
78
+
79
+ if not main_config_path.exists():
80
+ raise FileNotFoundError(f"Main configuration file not found: {main_config_path}")
81
+
82
+ config = self.utility.load_schema_from_yaml(main_config_path)
83
+
84
+ # Load and merge supplementary content files (e.g., onboarding_cards)
85
+ for key, file_path in config.get('help_files', {}).items():
86
+ supplementary_path = config_dir / file_path
87
+ if supplementary_path.exists():
88
+ config[key] = self.utility.load_schema_from_yaml(supplementary_path)
89
+ else:
90
+ logging.warning(f"⚠️ Warning: Content file not found: {supplementary_path}")
91
+ config[key] = None # Ensure the key exists but is empty
92
+
93
+ return config
94
+
95
+ def _register_core_details(self, company_instance: BaseCompany, config: dict) -> Company:
96
+ """Calls _create_company with data from the merged YAML config."""
97
+ return company_instance._create_company(
98
+ name=config['name'],
99
+ short_name=config['id'],
100
+ parameters=config.get('parameters', {})
101
+ )
102
+
103
+ def _register_tools(self, company_instance: BaseCompany, tools_config: list):
104
+ """Calls _create_function for each tool defined in the YAML."""
105
+ for tool in tools_config:
106
+ company_instance._create_function(
107
+ function_name=tool['function_name'],
108
+ description=tool['description'],
109
+ params=tool['params']
110
+ )
111
+
112
+ def _register_prompts(self, company_instance: BaseCompany, config: dict):
113
+ """
114
+ Creates prompt categories first, then creates each prompt and assigns
115
+ it to its respective category.
116
+ """
117
+ prompts_config = config.get('prompts', [])
118
+ categories_config = config.get('prompt_categories', [])
119
+
120
+ created_categories = {}
121
+ for i, category_name in enumerate(categories_config):
122
+ category_obj = company_instance._create_prompt_category(name=category_name, order=i + 1)
123
+ created_categories[category_name] = category_obj
124
+
125
+ for prompt_data in prompts_config:
126
+ category_name = prompt_data.get('category')
127
+ if not category_name or category_name not in created_categories:
128
+ logging.info(f"⚠️ Warning: Prompt '{prompt_data['name']}' has an invalid or missing category. Skipping.")
129
+ continue
130
+
131
+ category_obj = created_categories[category_name]
132
+
133
+ company_instance._create_prompt(
134
+ prompt_name=prompt_data['name'],
135
+ description=prompt_data['description'],
136
+ order=prompt_data['order'],
137
+ category=category_obj,
138
+ active=prompt_data.get('active', True),
139
+ custom_fields=prompt_data.get('custom_fields', [])
140
+ )
@@ -6,7 +6,7 @@
6
6
  from iatoolkit.common.exceptions import IAToolkitException
7
7
  from iatoolkit.services.prompt_manager_service import PromptService
8
8
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
9
-
9
+ from iatoolkit.services.configuration_service import ConfigurationService
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
@@ -19,11 +19,13 @@ import os
19
19
  class Dispatcher:
20
20
  @inject
21
21
  def __init__(self,
22
+ config_service: ConfigurationService,
22
23
  prompt_service: PromptService,
23
24
  llmquery_repo: LLMQueryRepo,
24
25
  util: Utility,
25
26
  excel_service: ExcelService,
26
27
  mail_service: MailService):
28
+ self.config_service = config_service
27
29
  self.prompt_service = prompt_service
28
30
  self.llmquery_repo = llmquery_repo
29
31
  self.util = util
@@ -55,14 +57,18 @@ class Dispatcher:
55
57
  self._company_instances = self.company_registry.get_all_company_instances()
56
58
  return self._company_instances
57
59
 
58
- def start_execution(self):
60
+ def load_company_configs(self):
59
61
  # initialize the system functions and prompts
60
62
  self.setup_iatoolkit_system()
61
63
 
62
- """Runs the startup logic for all registered companies."""
63
- for company in self.company_instances.values():
64
- company.register_company()
65
- company.start_execution()
64
+ """Loads the configuration of every company"""
65
+ for company_name, company_instance in self.company_instances.items():
66
+ try:
67
+ # register the company configuration
68
+ self.config_service.register_company(company_name, company_instance)
69
+ except Exception as e:
70
+ logging.error(f"❌ Failed to register configuration for '{company_name}': {e}")
71
+ continue
66
72
 
67
73
  return True
68
74
 
@@ -90,9 +96,6 @@ class Dispatcher:
90
96
  )
91
97
  i += 1
92
98
 
93
- # register in the database every company class
94
- for company in self.company_instances.values():
95
- company.register_company()
96
99
 
97
100
  def dispatch(self, company_name: str, action: str, **kwargs) -> dict:
98
101
  company_key = company_name.lower()
@@ -149,7 +152,7 @@ class Dispatcher:
149
152
  except Exception as e:
150
153
  logging.exception(e)
151
154
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
152
- f"Error en get_company_context de {company_name}: {str(e)}") from e
155
+ f"Error getting company context of: {company_name}: {str(e)}") from e
153
156
 
154
157
  def get_company_services(self, company: Company) -> list[dict]:
155
158
  # create the syntax with openai response syntax, for the company function list
@@ -173,7 +176,7 @@ class Dispatcher:
173
176
  def get_user_info(self, company_name: str, user_identifier: str) -> dict:
174
177
  if company_name not in self.company_instances:
175
178
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
176
- f"Empresa no configurada: {company_name}")
179
+ f"company not configured: {company_name}")
177
180
 
178
181
  # source 2: external company user
179
182
  company_instance = self.company_instances[company_name]
@@ -182,14 +185,14 @@ class Dispatcher:
182
185
  except Exception as e:
183
186
  logging.exception(e)
184
187
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
185
- f"Error en get_user_info de {company_name}: {str(e)}") from e
188
+ f"Error in get_user_info: {company_name}: {str(e)}") from e
186
189
 
187
190
  return external_user_profile
188
191
 
189
192
  def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
190
193
  if company_name not in self.company_instances:
191
194
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
192
- f"Empresa no configurada: {company_name}")
195
+ f"company not configured: {company_name}")
193
196
 
194
197
  company_instance = self.company_instances[company_name]
195
198
  try:
@@ -197,7 +200,7 @@ class Dispatcher:
197
200
  except Exception as e:
198
201
  logging.exception(e)
199
202
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
200
- f"Error en get_metadata_from_filename de {company_name}: {str(e)}") from e
203
+ f"Error in get_metadata_from_filename: {company_name}: {str(e)}") from e
201
204
 
202
205
  def get_company_instance(self, company_name: str):
203
206
  """Returns the instance for a given company name."""
@@ -207,12 +210,11 @@ class Dispatcher:
207
210
 
208
211
  # iatoolkit system prompts
209
212
  _SYSTEM_PROMPT = [
210
- {'name': 'query_main', 'description':'main prompt de iatoolkit'},
211
- {'name': 'format_styles', 'description':'formatos y estilos de salida'},
212
- {'name': 'sql_rules', 'description':'instrucciones para generar sql'}
213
+ {'name': 'query_main', 'description':'iatoolkit main prompt'},
214
+ {'name': 'format_styles', 'description':'output format styles'},
215
+ {'name': 'sql_rules', 'description':'instructions for SQL queries'}
213
216
  ]
214
217
 
215
-
216
218
  # iatoolkit function calls
217
219
  _FUNCTION_LIST = [
218
220
  {
@@ -11,10 +11,13 @@ import os
11
11
  import pytesseract
12
12
  from injector import inject
13
13
  from iatoolkit.common.exceptions import IAToolkitException
14
+ from iatoolkit.services.i18n_service import I18nService
14
15
 
15
16
  class DocumentService:
16
17
  @inject
17
- def __init__(self):
18
+ def __init__(self, i18n_service: I18nService):
19
+ self.i18n_service = i18n_service
20
+
18
21
  # max number of pages to load
19
22
  self.max_doc_pages = int(os.getenv("MAX_DOC_PAGES", "200"))
20
23
 
@@ -29,7 +32,7 @@ class DocumentService:
29
32
  file_content = file_content.decode('utf-8')
30
33
  except UnicodeDecodeError:
31
34
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_FORMAT_ERROR,
32
- "El archivo no es texto o la codificación no es UTF-8")
35
+ self.i18n_service.t('errors.services.no_text_file'))
33
36
 
34
37
  return file_content
35
38
  elif filename.lower().endswith('.pdf'):
@@ -8,6 +8,7 @@ import pandas as pd
8
8
  from uuid import uuid4
9
9
  from pathlib import Path
10
10
  from iatoolkit.common.exceptions import IAToolkitException
11
+ from iatoolkit.services.i18n_service import I18nService
11
12
  from injector import inject
12
13
  import os
13
14
  import logging
@@ -18,8 +19,11 @@ EXCEL_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
18
19
 
19
20
  class ExcelService:
20
21
  @inject
21
- def __init__(self,util: Utility):
22
+ def __init__(self,
23
+ util: Utility,
24
+ i18n_service: I18nService):
22
25
  self.util = util
26
+ self.i18n_service = i18n_service
23
27
 
24
28
  def excel_generator(self, **kwargs) -> str:
25
29
  """
@@ -42,11 +46,11 @@ class ExcelService:
42
46
  # get the parameters
43
47
  fname = kwargs.get('filename')
44
48
  if not fname:
45
- return 'falta el nombre del archivo de salida'
49
+ return self.i18n_service.t('errors.services.no_output_file')
46
50
 
47
51
  data = kwargs.get('data')
48
52
  if not data or not isinstance(data, list):
49
- return 'faltan los datos o no es una lista de diccionarios'
53
+ return self.i18n_service.t('errors.services.no_data_for_excel')
50
54
 
51
55
  sheet_name = kwargs.get('sheet_name', 'hoja 1')
52
56
 
@@ -58,7 +62,7 @@ class ExcelService:
58
62
 
59
63
  # 4. check that download directory is configured
60
64
  if 'IATOOLKIT_DOWNLOAD_DIR' not in current_app.config:
61
- return 'no esta configurado el directorio temporal para guardar excels'
65
+ return self.i18n_service.t('errors.services.no_download_directory')
62
66
 
63
67
  download_dir = current_app.config['IATOOLKIT_DOWNLOAD_DIR']
64
68
  filepath = Path(download_dir) / token
@@ -77,28 +81,28 @@ class ExcelService:
77
81
 
78
82
  except Exception as e:
79
83
  raise IAToolkitException(IAToolkitException.ErrorType.CALL_ERROR,
80
- 'error generating excel file') from e
84
+ self.i18n_service.t('errors.services.cannot_create_excel')) from e
81
85
 
82
86
  def validate_file_access(self, filename):
83
87
  try:
84
88
  if not filename:
85
- return jsonify({"error": "Nombre de archivo inválido"})
89
+ return jsonify({"error": self.i18n_service.t('errors.services.invalid_filename')})
86
90
  # Prevent path traversal attacks
87
91
  if '..' in filename or filename.startswith('/') or '\\' in filename:
88
- return jsonify({"error": "Nombre de archivo inválido"})
92
+ return jsonify({"error": self.i18n_service.t('errors.services.invalid_filename')})
89
93
 
90
94
  temp_dir = os.path.join(current_app.root_path, 'static', 'temp')
91
95
  file_path = os.path.join(temp_dir, filename)
92
96
 
93
97
  if not os.path.exists(file_path):
94
- return jsonify({"error": "Archivo no encontrado"})
98
+ return jsonify({"error": self.i18n_service.t('errors.services.file_not_exist')})
95
99
 
96
100
  if not os.path.isfile(file_path):
97
- return jsonify({"error": "La ruta no corresponde a un archivo"})
101
+ return jsonify({"error": self.i18n_service.t('errors.services.path_is_not_a_file')})
98
102
 
99
103
  return None
100
104
 
101
105
  except Exception as e:
102
- error_msg = f"Error validando acceso al archivo {filename}: {str(e)}"
106
+ error_msg = f"File validation error {filename}: {str(e)}"
103
107
  logging.error(error_msg)
104
- return jsonify({"error": "Error validando archivo"})
108
+ return jsonify({"error": self.i18n_service.t('errors.services.file_validation_error')})
@@ -52,27 +52,19 @@ class FileProcessor:
52
52
  logger: Optional[logging.Logger] = None):
53
53
  self.connector = connector
54
54
  self.config = config
55
- self.logger = logger or self._setup_logger()
56
55
  self.processed_files = 0
57
56
 
58
- def _setup_logger(self):
59
- logging.basicConfig(
60
- filename=self.config.log_file,
61
- level=logging.INFO,
62
- format='%(asctime)s - %(levelname)s - %(message)s'
63
- )
64
- return logging.getLogger(__name__)
65
57
 
66
58
  def process_files(self):
67
59
  # Fetches files from the connector, filters them, and processes them.
68
60
  try:
69
61
  files = self.connector.list_files()
70
62
  except Exception as e:
71
- self.logger.error(f"Error fetching files: {e}")
63
+ logging.error(f"Error fetching files: {e}")
72
64
  return False
73
65
 
74
66
  if self.config.echo:
75
- print(f'cargando un total de {len(files)} archivos')
67
+ print(f'loading {len(files)} files')
76
68
 
77
69
  for file_info in files:
78
70
  file_path = file_info['path']
@@ -95,10 +87,10 @@ class FileProcessor:
95
87
  context=self.config.context)
96
88
  self.processed_files += 1
97
89
 
98
- self.logger.info(f"Successfully processed file: {file_path}")
90
+ logging.info(f"Successfully processed file: {file_path}")
99
91
 
100
92
  except Exception as e:
101
- self.logger.error(f"Error processing {file_path}: {e}")
93
+ logging.error(f"Error processing {file_path}: {e}")
102
94
  if not self.config.continue_on_error:
103
95
  raise e
104
96