iatoolkit 0.3.9__py3-none-any.whl → 0.107.4__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 (150) hide show
  1. iatoolkit/__init__.py +27 -35
  2. iatoolkit/base_company.py +3 -35
  3. iatoolkit/cli_commands.py +18 -47
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +48 -0
  6. iatoolkit/common/interfaces/__init__.py +0 -0
  7. iatoolkit/common/interfaces/asset_storage.py +34 -0
  8. iatoolkit/common/interfaces/database_provider.py +39 -0
  9. iatoolkit/common/model_registry.py +159 -0
  10. iatoolkit/common/routes.py +138 -0
  11. iatoolkit/common/session_manager.py +26 -0
  12. iatoolkit/common/util.py +353 -0
  13. iatoolkit/company_registry.py +66 -29
  14. iatoolkit/core.py +514 -0
  15. iatoolkit/infra/__init__.py +5 -0
  16. iatoolkit/infra/brevo_mail_app.py +123 -0
  17. iatoolkit/infra/call_service.py +140 -0
  18. iatoolkit/infra/connectors/__init__.py +5 -0
  19. iatoolkit/infra/connectors/file_connector.py +17 -0
  20. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  21. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  22. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  23. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  24. iatoolkit/infra/connectors/s3_connector.py +33 -0
  25. iatoolkit/infra/google_chat_app.py +57 -0
  26. iatoolkit/infra/llm_providers/__init__.py +0 -0
  27. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  28. iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
  29. iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
  30. iatoolkit/infra/llm_proxy.py +268 -0
  31. iatoolkit/infra/llm_response.py +45 -0
  32. iatoolkit/infra/redis_session_manager.py +122 -0
  33. iatoolkit/locales/en.yaml +222 -0
  34. iatoolkit/locales/es.yaml +225 -0
  35. iatoolkit/repositories/__init__.py +5 -0
  36. iatoolkit/repositories/database_manager.py +187 -0
  37. iatoolkit/repositories/document_repo.py +33 -0
  38. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  39. iatoolkit/repositories/llm_query_repo.py +105 -0
  40. iatoolkit/repositories/models.py +279 -0
  41. iatoolkit/repositories/profile_repo.py +171 -0
  42. iatoolkit/repositories/vs_repo.py +150 -0
  43. iatoolkit/services/__init__.py +5 -0
  44. iatoolkit/services/auth_service.py +193 -0
  45. {services → iatoolkit/services}/benchmark_service.py +7 -7
  46. iatoolkit/services/branding_service.py +153 -0
  47. iatoolkit/services/company_context_service.py +214 -0
  48. iatoolkit/services/configuration_service.py +375 -0
  49. iatoolkit/services/dispatcher_service.py +134 -0
  50. {services → iatoolkit/services}/document_service.py +20 -8
  51. iatoolkit/services/embedding_service.py +148 -0
  52. iatoolkit/services/excel_service.py +156 -0
  53. {services → iatoolkit/services}/file_processor_service.py +36 -21
  54. iatoolkit/services/history_manager_service.py +208 -0
  55. iatoolkit/services/i18n_service.py +104 -0
  56. iatoolkit/services/jwt_service.py +80 -0
  57. iatoolkit/services/language_service.py +89 -0
  58. iatoolkit/services/license_service.py +82 -0
  59. iatoolkit/services/llm_client_service.py +438 -0
  60. iatoolkit/services/load_documents_service.py +174 -0
  61. iatoolkit/services/mail_service.py +213 -0
  62. {services → iatoolkit/services}/profile_service.py +200 -101
  63. iatoolkit/services/prompt_service.py +303 -0
  64. iatoolkit/services/query_service.py +467 -0
  65. iatoolkit/services/search_service.py +55 -0
  66. iatoolkit/services/sql_service.py +169 -0
  67. iatoolkit/services/tool_service.py +246 -0
  68. iatoolkit/services/user_feedback_service.py +117 -0
  69. iatoolkit/services/user_session_context_service.py +213 -0
  70. iatoolkit/static/images/fernando.jpeg +0 -0
  71. iatoolkit/static/images/iatoolkit_core.png +0 -0
  72. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  73. iatoolkit/static/js/chat_feedback_button.js +80 -0
  74. iatoolkit/static/js/chat_filepond.js +85 -0
  75. iatoolkit/static/js/chat_help_content.js +124 -0
  76. iatoolkit/static/js/chat_history_button.js +110 -0
  77. iatoolkit/static/js/chat_logout_button.js +36 -0
  78. iatoolkit/static/js/chat_main.js +401 -0
  79. iatoolkit/static/js/chat_model_selector.js +227 -0
  80. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  81. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  82. iatoolkit/static/js/chat_reload_button.js +38 -0
  83. iatoolkit/static/styles/chat_iatoolkit.css +559 -0
  84. iatoolkit/static/styles/chat_modal.css +133 -0
  85. iatoolkit/static/styles/chat_public.css +135 -0
  86. iatoolkit/static/styles/documents.css +598 -0
  87. iatoolkit/static/styles/landing_page.css +398 -0
  88. iatoolkit/static/styles/llm_output.css +148 -0
  89. iatoolkit/static/styles/onboarding.css +176 -0
  90. iatoolkit/system_prompts/__init__.py +0 -0
  91. iatoolkit/system_prompts/query_main.prompt +30 -23
  92. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  93. iatoolkit/templates/_company_header.html +45 -0
  94. iatoolkit/templates/_login_widget.html +42 -0
  95. iatoolkit/templates/base.html +78 -0
  96. iatoolkit/templates/change_password.html +66 -0
  97. iatoolkit/templates/chat.html +337 -0
  98. iatoolkit/templates/chat_modals.html +185 -0
  99. iatoolkit/templates/error.html +51 -0
  100. iatoolkit/templates/forgot_password.html +51 -0
  101. iatoolkit/templates/onboarding_shell.html +106 -0
  102. iatoolkit/templates/signup.html +79 -0
  103. iatoolkit/views/__init__.py +5 -0
  104. iatoolkit/views/base_login_view.py +96 -0
  105. iatoolkit/views/change_password_view.py +116 -0
  106. iatoolkit/views/chat_view.py +76 -0
  107. iatoolkit/views/embedding_api_view.py +65 -0
  108. iatoolkit/views/forgot_password_view.py +75 -0
  109. iatoolkit/views/help_content_api_view.py +54 -0
  110. iatoolkit/views/history_api_view.py +56 -0
  111. iatoolkit/views/home_view.py +63 -0
  112. iatoolkit/views/init_context_api_view.py +74 -0
  113. iatoolkit/views/llmquery_api_view.py +59 -0
  114. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  115. iatoolkit/views/load_document_api_view.py +65 -0
  116. iatoolkit/views/login_view.py +170 -0
  117. iatoolkit/views/logout_api_view.py +57 -0
  118. iatoolkit/views/profile_api_view.py +46 -0
  119. iatoolkit/views/prompt_api_view.py +37 -0
  120. iatoolkit/views/root_redirect_view.py +22 -0
  121. iatoolkit/views/signup_view.py +100 -0
  122. iatoolkit/views/static_page_view.py +27 -0
  123. iatoolkit/views/user_feedback_api_view.py +60 -0
  124. iatoolkit/views/users_api_view.py +33 -0
  125. iatoolkit/views/verify_user_view.py +60 -0
  126. iatoolkit-0.107.4.dist-info/METADATA +268 -0
  127. iatoolkit-0.107.4.dist-info/RECORD +132 -0
  128. iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
  129. iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  130. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
  131. iatoolkit/iatoolkit.py +0 -413
  132. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  133. iatoolkit-0.3.9.dist-info/METADATA +0 -252
  134. iatoolkit-0.3.9.dist-info/RECORD +0 -32
  135. services/__init__.py +0 -5
  136. services/api_service.py +0 -75
  137. services/dispatcher_service.py +0 -351
  138. services/excel_service.py +0 -98
  139. services/history_service.py +0 -45
  140. services/jwt_service.py +0 -91
  141. services/load_documents_service.py +0 -212
  142. services/mail_service.py +0 -62
  143. services/prompt_manager_service.py +0 -172
  144. services/query_service.py +0 -334
  145. services/search_service.py +0 -32
  146. services/sql_service.py +0 -42
  147. services/tasks_service.py +0 -188
  148. services/user_feedback_service.py +0 -67
  149. services/user_session_context_service.py +0 -85
  150. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,208 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+
7
+ import logging
8
+ import json
9
+ from typing import Dict, Any, Optional
10
+ from iatoolkit.services.user_session_context_service import UserSessionContextService
11
+ from iatoolkit.services.i18n_service import I18nService
12
+ from iatoolkit.services.llm_client_service import llmClient
13
+ from iatoolkit.repositories.models import Company
14
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
15
+ from iatoolkit.repositories.profile_repo import ProfileRepo
16
+ from injector import inject
17
+
18
+
19
+ class HistoryManagerService:
20
+ """
21
+ Manages conversation history for LLMs in a unified way.
22
+ Handles:
23
+ 1. Server-side history (e.g., OpenAI response_ids).
24
+ 2. Client-side history (e.g., Gemini message lists).
25
+ 3. Database persistence retrieval (full chat history).
26
+ """
27
+ TYPE_SERVER_SIDE = 'server_side' # For models like OpenAI
28
+ TYPE_CLIENT_SIDE = 'client_side' # For models like Gemini and Deepseek
29
+
30
+ GEMINI_MAX_TOKENS_CONTEXT_HISTORY = 200000
31
+
32
+
33
+ @inject
34
+ def __init__(self,
35
+ session_context: UserSessionContextService,
36
+ i18n: I18nService,
37
+ llm_query_repo: LLMQueryRepo,
38
+ profile_repo: ProfileRepo,
39
+ llm_client: Optional[llmClient] = None):
40
+ self.session_context = session_context
41
+ self.i18n = i18n
42
+ self.llm_query_repo = llm_query_repo
43
+ self.profile_repo = profile_repo
44
+ self.llm_client = llm_client
45
+
46
+ def initialize_context(self,
47
+ company_short_name: str,
48
+ user_identifier: str,
49
+ history_type: str,
50
+ prepared_context: str,
51
+ company: Company, model: str) -> Dict[str, Any]:
52
+ """
53
+ Initializes a new conversation history.
54
+ """
55
+ # 1. Clear existing history
56
+ self.session_context.clear_llm_history(company_short_name, user_identifier, model=model)
57
+
58
+ if history_type == self.TYPE_SERVER_SIDE:
59
+ # OpenAI: Send system prompt to API and store the resulting ID
60
+ response_id = self.llm_client.set_company_context(
61
+ company=company,
62
+ company_base_context=prepared_context,
63
+ model=model
64
+ )
65
+ self.session_context.save_last_response_id(company_short_name, user_identifier, response_id, model=model)
66
+ self.session_context.save_initial_response_id(company_short_name, user_identifier, response_id, model=model)
67
+ return {'response_id': response_id}
68
+
69
+ elif history_type == self.TYPE_CLIENT_SIDE:
70
+ # Gemini: Store system prompt as the first message in the list
71
+ context_history = [{"role": "user", "content": prepared_context}]
72
+ self.session_context.save_context_history(company_short_name, user_identifier, context_history, model=model)
73
+ return {}
74
+
75
+ return {}
76
+
77
+ def populate_request_params(self,
78
+ handle: Any,
79
+ user_turn_prompt: str,
80
+ ignore_history: bool = False) -> bool:
81
+ """
82
+ Populates the request_params within the HistoryHandle.
83
+ Returns True if a rebuild is needed, False otherwise.
84
+ """
85
+ model = getattr(handle, "model", None)
86
+
87
+ if handle.type == self.TYPE_SERVER_SIDE:
88
+ if ignore_history:
89
+ previous_response_id = self.session_context.get_initial_response_id(
90
+ handle.company_short_name,handle.user_identifier,model=model)
91
+ else:
92
+ previous_response_id = self.session_context.get_last_response_id(
93
+ handle.company_short_name,handle.user_identifier,model=model)
94
+
95
+
96
+ if not previous_response_id:
97
+ handle.request_params = {}
98
+ return True # Needs rebuild
99
+
100
+ handle.request_params = {'previous_response_id': previous_response_id}
101
+ return False
102
+
103
+ elif handle.type == self.TYPE_CLIENT_SIDE:
104
+ context_history = self.session_context.get_context_history(
105
+ handle.company_short_name,handle.user_identifier,model=model) or []
106
+
107
+ if not context_history:
108
+ handle.request_params = {}
109
+ return True # Needs rebuild
110
+
111
+ if ignore_history and len(context_history) > 1:
112
+ # Keep only system prompt
113
+ context_history = [context_history[0]]
114
+
115
+ # Append the current user turn to the context sent to the API
116
+ context_history.append({"role": "user", "content": user_turn_prompt})
117
+
118
+ self._trim_context_history(context_history)
119
+
120
+ handle.request_params = {'context_history': context_history}
121
+ return False
122
+
123
+ handle.request_params = {}
124
+ return False
125
+
126
+ def update_history(self,
127
+ history_handle: Any,
128
+ user_turn_prompt: str,
129
+ response: Dict[str, Any]):
130
+ """Saves or updates the history after a successful LLM call."""
131
+
132
+ # We access the type from the handle
133
+ history_type = history_handle.type
134
+ company_short_name = history_handle.company_short_name
135
+ user_identifier = history_handle.user_identifier
136
+ model = getattr(history_handle, "model", None)
137
+
138
+ if history_type == self.TYPE_SERVER_SIDE:
139
+ if "response_id" in response:
140
+ self.session_context.save_last_response_id(
141
+ company_short_name,
142
+ user_identifier,
143
+ response["response_id"],
144
+ model=model)
145
+
146
+ elif history_type == self.TYPE_CLIENT_SIDE:
147
+ # get the history for this company/user/model
148
+ context_history = self.session_context.get_context_history(
149
+ company_short_name,
150
+ user_identifier,
151
+ model=model)
152
+
153
+ # Ensure the user prompt is recorded if not already.
154
+ # We check content equality to handle the case where the previous message was
155
+ # also 'user' (e.g., System Prompt) but different content.
156
+ last_content = context_history[-1].get("content") if context_history else None
157
+
158
+ if last_content != user_turn_prompt:
159
+ context_history.append({"role": "user", "content": user_turn_prompt})
160
+
161
+ if response.get('answer'):
162
+ context_history.append({"role": "assistant", "content": response.get('answer', '')})
163
+
164
+ self.session_context.save_context_history(
165
+ company_short_name,
166
+ user_identifier,
167
+ context_history,
168
+ model=model)
169
+
170
+ def _trim_context_history(self, context_history: list):
171
+ """Internal helper to keep token usage within limits for client-side history."""
172
+ if not context_history or len(context_history) <= 1:
173
+ return
174
+ try:
175
+ total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
176
+ except Exception as e:
177
+ logging.error(f"Error counting tokens for history: {e}.")
178
+ return
179
+
180
+ while total_tokens > self.GEMINI_MAX_TOKENS_CONTEXT_HISTORY and len(context_history) > 1:
181
+ try:
182
+ # Remove the oldest message after system prompt
183
+ removed_message = context_history.pop(1)
184
+ removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
185
+ total_tokens -= removed_tokens
186
+ logging.warning(
187
+ f"History tokens exceed limit. Removed old message. New total: {total_tokens} tokens."
188
+ )
189
+ except IndexError:
190
+ break
191
+
192
+ # --- this is for the history popup in the chat page
193
+ def get_full_history(self, company_short_name: str, user_identifier: str) -> dict:
194
+ """Retrieves the full persisted history from the database."""
195
+ try:
196
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
197
+ if not company:
198
+ return {"error": self.i18n.t('errors.company_not_found', company_short_name=company_short_name)}
199
+
200
+ history = self.llm_query_repo.get_history(company, user_identifier)
201
+ if not history:
202
+ return {'message': 'empty history', 'history': []}
203
+
204
+ history_list = [query.to_dict() for query in history]
205
+ return {'message': 'history loaded ok', 'history': history_list}
206
+
207
+ except Exception as e:
208
+ return {'error': str(e)}
@@ -0,0 +1,104 @@
1
+ # iatoolkit/services/i18n_service.py
2
+ import os
3
+ import logging
4
+ from injector import inject, singleton
5
+ from iatoolkit.common.util import Utility
6
+ from iatoolkit.services.language_service import LanguageService
7
+
8
+ @singleton
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("Directory 'locales' not found.")
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"Error while loading the translation file {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
@@ -0,0 +1,80 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import jwt
7
+ import time
8
+ import logging
9
+ from injector import singleton, inject
10
+ from typing import Optional, Dict, Any
11
+ from flask import Flask
12
+
13
+
14
+ @singleton
15
+ class JWTService:
16
+ @inject
17
+ def __init__(self, app: Flask):
18
+ # Acceder a la configuración directamente desde app.config
19
+ try:
20
+ self.secret_key = app.config['IATOOLKIT_SECRET_KEY']
21
+ self.algorithm = app.config['JWT_ALGORITHM']
22
+ except KeyError as e:
23
+ logging.error(f"missing JWT configuration: {e}.")
24
+ raise RuntimeError(f"missing JWT configuration variables: {e}")
25
+
26
+ def generate_chat_jwt(self,
27
+ company_short_name: str,
28
+ user_identifier: str,
29
+ expires_delta_seconds: int) -> Optional[str]:
30
+ # generate a JWT for a chat session
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
+
36
+ payload = {
37
+ 'company_short_name': company_short_name,
38
+ 'user_identifier': user_identifier,
39
+ 'exp': time.time() + expires_delta_seconds,
40
+ 'iat': time.time(),
41
+ 'type': 'chat_session' # Identificador del tipo de token
42
+ }
43
+ token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
44
+ return token
45
+ except Exception as e:
46
+ logging.error(f"Error al generar JWT para {company_short_name}/{user_identifier}: {e}")
47
+ return None
48
+
49
+ def validate_chat_jwt(self, token: str) -> Optional[Dict[str, Any]]:
50
+ """
51
+ Valida un JWT de sesión de chat.
52
+ Retorna el payload decodificado si es válido y coincide con la empresa, o None.
53
+ """
54
+ if not token:
55
+ return None
56
+ try:
57
+ payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
58
+
59
+ # Validaciones adicionales
60
+ if payload.get('type') != 'chat_session':
61
+ logging.warning(f"Invalid JWT type '{payload.get('type')}'")
62
+ return None
63
+
64
+ # user_identifier debe estar presente
65
+ if not payload.get('user_identifier'):
66
+ logging.warning(f"missing user_identifier in JWT payload.")
67
+ return None
68
+
69
+ if not payload.get('company_short_name'):
70
+ logging.warning(f"missing company_short_name in JWT payload.")
71
+ return None
72
+
73
+ return payload
74
+
75
+ except jwt.InvalidTokenError as e:
76
+ logging.warning(f"Invalid JWT token:: {e}")
77
+ return None
78
+ except Exception as e:
79
+ logging.error(f"unexpected error during JWT validation: {e}")
80
+ return None
@@ -0,0 +1,89 @@
1
+ # iatoolkit/services/language_service.py
2
+
3
+ import logging
4
+ from injector import inject, singleton
5
+ from flask import g, request
6
+ from iatoolkit.repositories.profile_repo import ProfileRepo
7
+ from iatoolkit.services.configuration_service import ConfigurationService
8
+ from iatoolkit.common.session_manager import SessionManager
9
+
10
+ @singleton
11
+ class LanguageService:
12
+ """
13
+ Determines the correct language for the current request
14
+ based on a defined priority order (session, URL, etc.)
15
+ and caches it in the Flask 'g' object for the request's lifecycle.
16
+ """
17
+
18
+ FALLBACK_LANGUAGE = 'es'
19
+
20
+ @inject
21
+ def __init__(self,
22
+ config_service: ConfigurationService,
23
+ profile_repo: ProfileRepo):
24
+ self.config_service = config_service
25
+ self.profile_repo = profile_repo
26
+
27
+ def _get_company_short_name(self) -> str | None:
28
+ """
29
+ Gets the company_short_name from the current request context.
30
+ This handles different scenarios like web sessions, public URLs, and API calls.
31
+
32
+ Priority Order:
33
+ 1. Flask Session (for logged-in web users).
34
+ 2. URL rule variable (for public pages and API endpoints).
35
+ """
36
+ # 1. Check session for logged-in users
37
+ company_short_name = SessionManager.get('company_short_name')
38
+ if company_short_name:
39
+ return company_short_name
40
+
41
+ # 2. Check URL arguments (e.g., /<company_short_name>/login)
42
+ # This covers public pages and most API calls.
43
+ if request.view_args and 'company_short_name' in request.view_args:
44
+ return request.view_args['company_short_name']
45
+
46
+ return None
47
+
48
+ def get_current_language(self) -> str:
49
+ """
50
+ Determines and caches the language for the current request using a priority order:
51
+ 0. Query parameter '?lang=<code>' (highest priority; e.g., 'en', 'es').
52
+ 1. User's preference (from their profile).
53
+ 2. Company's default language.
54
+ 3. System-wide fallback language ('es').
55
+ """
56
+ if 'lang' in g:
57
+ return g.lang
58
+
59
+ try:
60
+ # Priority 0: Explicit query parameter (?lang=)
61
+ lang_arg = request.args.get('lang')
62
+ if lang_arg:
63
+ g.lang = lang_arg
64
+ return g.lang
65
+
66
+ # Priority 1: User's preferred language
67
+ user_identifier = SessionManager.get('user_identifier')
68
+ if user_identifier:
69
+ user = self.profile_repo.get_user_by_email(user_identifier)
70
+ if user and user.preferred_language:
71
+ logging.debug(f"Language determined by user preference: {user.preferred_language}")
72
+ g.lang = user.preferred_language
73
+ return g.lang
74
+
75
+ # Priority 2: Company's default language
76
+ company_short_name = self._get_company_short_name()
77
+ if company_short_name:
78
+ locale = self.config_service.get_configuration(company_short_name, 'locale')
79
+ if locale:
80
+ company_language = locale.split('_')[0]
81
+ g.lang = company_language
82
+ return g.lang
83
+ except Exception as e:
84
+ pass
85
+
86
+ # Priority 3: System-wide fallback
87
+ logging.debug(f"Language determined by system fallback: {self.FALLBACK_LANGUAGE}")
88
+ g.lang = self.FALLBACK_LANGUAGE
89
+ return g.lang
@@ -0,0 +1,82 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import jwt
7
+ import os
8
+ import logging
9
+ from pathlib import Path
10
+ from iatoolkit.common.exceptions import IAToolkitException
11
+ from injector import inject, singleton
12
+
13
+
14
+ @singleton
15
+ class LicenseService:
16
+ """
17
+ Manages system restrictions and features based on a license (JWT).
18
+ If no license or an invalid license is provided, Community Edition limits apply.
19
+ """
20
+ @inject
21
+ def __init__(self):
22
+ self.limits = self._load_limits()
23
+
24
+ def _load_limits(self):
25
+ # 1. Define default limits (Community Edition)
26
+ default_limits = {
27
+ "license_type": "Community Edition",
28
+ "plan": "Open Source (Community Edition)",
29
+ "max_companies": 1,
30
+ "max_tools": 3,
31
+ "features": {
32
+ "multi_tenant": False,
33
+ "rag_advanced": False,
34
+ }
35
+ }
36
+ return default_limits
37
+
38
+
39
+ # --- Information Getters ---
40
+ def get_license_type(self) -> str:
41
+ return self.limits.get("license_type", "Community Edition")
42
+
43
+ def get_plan_name(self) -> str:
44
+ return self.limits.get("plan", "Unknown")
45
+
46
+ def get_max_companies(self) -> int:
47
+ return self.limits.get("max_companies", 1)
48
+
49
+ def get_max_tools_per_company(self) -> int:
50
+ return self.limits.get("max_tools", 3)
51
+
52
+ def get_license_info(self) -> str:
53
+ return f"Plan: {self.get_plan_name()}, Companies: {self.get_max_companies()}, Tools: {self.get_max_tools_per_company()}"
54
+
55
+ # --- Restriction Validators ---
56
+
57
+ def validate_company_limit(self, current_count: int):
58
+ """Raises exception if the limit of active companies is exceeded."""
59
+ limit = self.get_max_companies()
60
+ # -1 means unlimited
61
+ if limit != -1 and current_count > limit:
62
+ raise IAToolkitException(
63
+ IAToolkitException.ErrorType.PERMISSION,
64
+ f"Company limit ({limit}) reached for plan '{self.get_plan_name()}'."
65
+ )
66
+
67
+
68
+ def validate_tool_config_limit(self, tools_config: list):
69
+ """Validates a configuration list before processing it."""
70
+ limit = self.get_max_tools_per_company()
71
+ if limit != -1 and len(tools_config) > limit:
72
+ raise IAToolkitException(
73
+ IAToolkitException.ErrorType.PERMISSION,
74
+ f"Configuration defines {len(tools_config)} tools, but limit is {limit}."
75
+ )
76
+
77
+ # --- Feature Gating Validators ---
78
+
79
+ def has_feature(self, feature_key: str) -> bool:
80
+ """Checks if a specific feature is enabled in the license."""
81
+ features = self.limits.get("features", {})
82
+ return features.get(feature_key, False)