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,150 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from sqlalchemy import text
7
+ from injector import inject
8
+ from iatoolkit.common.exceptions import IAToolkitException
9
+ from iatoolkit.repositories.database_manager import DatabaseManager
10
+ from iatoolkit.services.embedding_service import EmbeddingService
11
+ from iatoolkit.repositories.models import Document, VSDoc, Company
12
+ import logging
13
+
14
+
15
+ class VSRepo:
16
+ @inject
17
+ def __init__(self,
18
+ db_manager: DatabaseManager,
19
+ embedding_service: EmbeddingService):
20
+ self.session = db_manager.get_session()
21
+ self.embedding_service = embedding_service
22
+
23
+
24
+ def add_document(self, company_short_name, vs_chunk_list: list[VSDoc]):
25
+ try:
26
+ for doc in vs_chunk_list:
27
+ # calculate the embedding for the text
28
+ doc.embedding = self.embedding_service.embed_text(company_short_name, doc.text)
29
+ self.session.add(doc)
30
+ self.session.commit()
31
+ except Exception as e:
32
+ logging.error(f"Error while inserting embedding chunk list: {str(e)}")
33
+ self.session.rollback()
34
+ raise IAToolkitException(IAToolkitException.ErrorType.VECTOR_STORE_ERROR,
35
+ f"Error while inserting embedding chunk list: {str(e)}")
36
+
37
+ def query(self,
38
+ company_short_name: str,
39
+ query_text: str,
40
+ n_results=5,
41
+ metadata_filter=None
42
+ ) -> list[Document]:
43
+ """
44
+ search documents similar to the query for a company
45
+
46
+ Args:
47
+ company_short_name: The company's unique short name.
48
+ query_text: query text
49
+ n_results: max number of results to return
50
+ metadata_filter: (e.g., {"document_type": "certificate"})
51
+
52
+ Returns:
53
+ list of documents matching the query and filters
54
+ """
55
+ # Generate the embedding with the query text for the specific company
56
+ try:
57
+ query_embedding = self.embedding_service.embed_text(company_short_name, query_text)
58
+ except Exception as e:
59
+ logging.error(f"error while creating text embedding: {str(e)}")
60
+ raise IAToolkitException(IAToolkitException.ErrorType.EMBEDDING_ERROR,
61
+ f"embedding error: {str(e)}")
62
+
63
+ sql_query, params = None, None
64
+ try:
65
+ # Get company ID from its short name for the SQL query
66
+ company = self.session.query(Company).filter(Company.short_name == company_short_name).one_or_none()
67
+ if not company:
68
+ raise IAToolkitException(IAToolkitException.ErrorType.VECTOR_STORE_ERROR,
69
+ f"Company with short name '{company_short_name}' not found.")
70
+
71
+ # build the SQL query
72
+ sql_query_parts = ["""
73
+ SELECT iat_documents.id, \
74
+ iat_documents.filename, \
75
+ iat_documents.content, \
76
+ iat_documents.content_b64, \
77
+ iat_documents.meta
78
+ FROM iat_vsdocs, \
79
+ iat_documents
80
+ WHERE iat_vsdocs.company_id = :company_id
81
+ AND iat_vsdocs.document_id = iat_documents.id \
82
+ """]
83
+
84
+ # query parameters
85
+ params = {
86
+ "company_id": company.id,
87
+ "query_embedding": query_embedding,
88
+ "n_results": n_results
89
+ }
90
+
91
+
92
+ # add metadata filter, if exists
93
+ if metadata_filter and isinstance(metadata_filter, dict):
94
+ for key, value in metadata_filter.items():
95
+ # Usar el operador ->> para extraer el valor del JSON como texto.
96
+ # La clave del JSON se interpola directamente.
97
+ # El valor se pasa como parámetro para evitar inyección SQL.
98
+ param_name = f"value_{key}_filter"
99
+ sql_query_parts.append(f" AND documents.meta->>'{key}' = :{param_name}")
100
+ params[param_name] = str(value) # parametros como string
101
+
102
+ # join all the query parts
103
+ sql_query = "".join(sql_query_parts)
104
+
105
+ # add sorting and limit of results
106
+ sql_query += " ORDER BY embedding <-> CAST(:query_embedding AS VECTOR) LIMIT :n_results"
107
+
108
+ logging.debug(f"Executing SQL query: {sql_query}")
109
+ logging.debug(f"With parameters: {params}")
110
+
111
+ # execute the query
112
+ result = self.session.execute(text(sql_query), params)
113
+
114
+ rows = result.fetchall()
115
+ vs_documents = []
116
+
117
+ for row in rows:
118
+ # create the document object with the data
119
+ meta_data = row[4] if len(row) > 4 and row[4] is not None else {}
120
+ doc = Document(
121
+ id=row[0],
122
+ company_id=company.id,
123
+ filename=row[1],
124
+ content=row[2],
125
+ content_b64=row[3],
126
+ meta=meta_data
127
+ )
128
+ vs_documents.append(doc)
129
+
130
+ return self.remove_duplicates_by_id(vs_documents)
131
+
132
+ except Exception as e:
133
+ logging.error(f"Error en la consulta de documentos: {str(e)}")
134
+ logging.error(f"Failed SQL: {sql_query}")
135
+ logging.error(f"Failed params: {params}")
136
+ raise IAToolkitException(IAToolkitException.ErrorType.VECTOR_STORE_ERROR,
137
+ f"Error en la consulta: {str(e)}")
138
+ finally:
139
+ self.session.close()
140
+
141
+ def remove_duplicates_by_id(self, objects):
142
+ unique_by_id = {}
143
+ result = []
144
+
145
+ for obj in objects:
146
+ if obj.id not in unique_by_id:
147
+ unique_by_id[obj.id] = True
148
+ result.append(obj)
149
+
150
+ return result
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
@@ -0,0 +1,193 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask import request
7
+ from injector import inject
8
+ from iatoolkit.services.profile_service import ProfileService
9
+ from iatoolkit.services.jwt_service import JWTService
10
+ from iatoolkit.services.i18n_service import I18nService
11
+ from iatoolkit.repositories.database_manager import DatabaseManager
12
+ from iatoolkit.repositories.models import AccessLog
13
+ from flask import request
14
+ import logging
15
+ import hashlib
16
+
17
+
18
+ class AuthService:
19
+ """
20
+ Centralized service for handling authentication for all incoming requests.
21
+ It determines the user's identity based on either a Flask session cookie or an API Key.
22
+ """
23
+
24
+ @inject
25
+ def __init__(self, profile_service: ProfileService,
26
+ jwt_service: JWTService,
27
+ db_manager: DatabaseManager,
28
+ i18n_service: I18nService
29
+ ):
30
+ self.profile_service = profile_service
31
+ self.jwt_service = jwt_service
32
+ self.db_manager = db_manager
33
+ self.i18n_service = i18n_service
34
+
35
+ def login_local_user(self, company_short_name: str, email: str, password: str) -> dict:
36
+ # try to autenticate a local user, register the event and return the result
37
+ auth_response = self.profile_service.login(
38
+ company_short_name=company_short_name,
39
+ email=email,
40
+ password=password,
41
+ )
42
+
43
+ if not auth_response.get('success'):
44
+ self.log_access(
45
+ company_short_name=company_short_name,
46
+ user_identifier=email,
47
+ auth_type='local',
48
+ outcome='failure',
49
+ reason_code='INVALID_CREDENTIALS',
50
+ )
51
+ else:
52
+ self.log_access(
53
+ company_short_name=company_short_name,
54
+ auth_type='local',
55
+ outcome='success',
56
+ user_identifier=auth_response.get('user_identifier')
57
+ )
58
+
59
+ return auth_response
60
+
61
+ def redeem_token_for_session(self, company_short_name: str, token: str) -> dict:
62
+ # redeem a token for a session, register the event and return the result
63
+ payload = self.jwt_service.validate_chat_jwt(token)
64
+
65
+ if not payload:
66
+ self.log_access(
67
+ company_short_name=company_short_name,
68
+ auth_type='redeem_token',
69
+ outcome='failure',
70
+ reason_code='JWT_INVALID'
71
+ )
72
+ return {'success': False, 'error': self.i18n_service.t('errors.auth.invalid_or_expired_token')}
73
+
74
+ # 2. if token is valid, extract the user_identifier
75
+ user_identifier = payload.get('user_identifier')
76
+ try:
77
+ # create the Flask session
78
+ self.profile_service.set_session_for_user(company_short_name, user_identifier)
79
+ self.log_access(
80
+ company_short_name=company_short_name,
81
+ auth_type='redeem_token',
82
+ outcome='success',
83
+ user_identifier=user_identifier
84
+ )
85
+ return {'success': True, 'user_identifier': user_identifier}
86
+ except Exception as e:
87
+ logging.error(f"error creeating session for Token of {user_identifier}: {e}")
88
+ self.log_access(
89
+ company_short_name=company_short_name,
90
+ auth_type='redeem_token',
91
+ outcome='failure',
92
+ reason_code='SESSION_CREATION_FAILED',
93
+ user_identifier=user_identifier
94
+ )
95
+ return {'success': False, 'error': self.i18n_service.t('errors.auth.session_creation_failed')}
96
+
97
+ def verify(self, anonymous: bool = False) -> dict:
98
+ """
99
+ Verifies the current request and identifies the user.
100
+ If anonymous is True the non-presence of use_identifier is ignored
101
+
102
+ Returns a dictionary with:
103
+ - success: bool
104
+ - user_identifier: str (if successful)
105
+ - company_short_name: str (if successful)
106
+ - error_message: str (on failure)
107
+ - status_code: int (on failure)
108
+ """
109
+ # --- Priority 1: Check for a valid Flask web session ---
110
+ session_info = self.profile_service.get_current_session_info()
111
+ if session_info and session_info.get('user_identifier'):
112
+ # User is authenticated via a web session cookie.
113
+ return {
114
+ "success": True,
115
+ "company_short_name": session_info['company_short_name'],
116
+ "user_identifier": session_info['user_identifier'],
117
+ }
118
+
119
+ # --- Priority 2: Check for a valid API Key in headers ---
120
+ api_key = None
121
+ auth = request.headers.get('Authorization', '')
122
+ if isinstance(auth, str) and auth.lower().startswith('bearer '):
123
+ api_key = auth.split(' ', 1)[1].strip()
124
+
125
+ if not api_key:
126
+ # --- Failure: No valid credentials found ---
127
+ logging.info(f"Authentication required. No session cookie or API Key provided.")
128
+ return {"success": False,
129
+ "error_message": self.i18n_service.t('errors.auth.authentication_required'),
130
+ "status_code": 401}
131
+
132
+ # check if the api-key is valid and active
133
+ api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
134
+ if not api_key_entry:
135
+ logging.error(f"Invalid or inactive IAToolkit API Key: {api_key}")
136
+ return {"success": False,
137
+ "error_message": self.i18n_service.t('errors.auth.invalid_api_key'),
138
+ "status_code": 402}
139
+
140
+ # get the company from the api_key_entry
141
+ company = api_key_entry.company
142
+
143
+ # For API calls, the external_user_id must be provided in the request.
144
+ data = request.get_json(silent=True) or {}
145
+ user_identifier = data.get('user_identifier', '')
146
+ if not anonymous and not user_identifier:
147
+ logging.info(f"No user_identifier provided for API call.")
148
+ return {"success": False,
149
+ "error_message": self.i18n_service.t('errors.auth.no_user_identifier_api'),
150
+ "status_code": 403}
151
+
152
+ return {
153
+ "success": True,
154
+ "company_short_name": company.short_name,
155
+ "user_identifier": user_identifier
156
+ }
157
+
158
+
159
+ def log_access(self,
160
+ company_short_name: str,
161
+ auth_type: str,
162
+ outcome: str,
163
+ user_identifier: str = None,
164
+ reason_code: str = None):
165
+ """
166
+ Registra un intento de acceso en la base de datos.
167
+ Es "best-effort" y no debe interrumpir el flujo de autenticación.
168
+ """
169
+ session = self.db_manager.scoped_session()
170
+ try:
171
+ # Capturar datos del contexto de la petición de Flask
172
+ source_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
173
+ path = request.path
174
+ ua = request.headers.get('User-Agent', '')
175
+ ua_hash = hashlib.sha256(ua.encode()).hexdigest()[:16] if ua else None
176
+
177
+ # Crear la entrada de log
178
+ log_entry = AccessLog(
179
+ company_short_name=company_short_name,
180
+ user_identifier=user_identifier,
181
+ auth_type=auth_type,
182
+ outcome=outcome,
183
+ reason_code=reason_code,
184
+ source_ip=source_ip,
185
+ user_agent_hash=ua_hash,
186
+ request_path=path,
187
+ )
188
+ session.add(log_entry)
189
+ session.commit()
190
+
191
+ except Exception as e:
192
+ logging.error(f"error writting to AccessLog: {e}", exc_info=False)
193
+ session.rollback()
@@ -1,15 +1,15 @@
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 pandas as pd
7
7
  import time
8
8
  import logging
9
9
  from injector import inject
10
- from services.query_service import QueryService
11
- from repositories.profile_repo import ProfileRepo
12
- from common.exceptions import IAToolkitException
10
+ from iatoolkit.services.query_service import QueryService
11
+ from iatoolkit.repositories.profile_repo import ProfileRepo
12
+ from iatoolkit.common.exceptions import IAToolkitException
13
13
 
14
14
 
15
15
  class BenchmarkService:
@@ -65,7 +65,7 @@ class BenchmarkService:
65
65
 
66
66
  company = self.profile_repo.get_company_by_short_name(company_short_name)
67
67
  if not company:
68
- raise IAToolkitException(IAToolkitException.ErrorType.CONFIG_ERROR, "Compañía 'maxxa' no encontrada.")
68
+ raise IAToolkitException(IAToolkitException.ErrorType.CONFIG_ERROR, f"Compañía {company_short_name} no encontrada.")
69
69
 
70
70
  total_rows = len(df)
71
71
  logging.info(f"Iniciando benchmark para {total_rows} casos de prueba desde el archivo: {file_path}")
@@ -0,0 +1,153 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.repositories.models import Company
7
+ from iatoolkit.services.configuration_service import ConfigurationService
8
+ from injector import inject
9
+
10
+
11
+ class BrandingService:
12
+ """
13
+ Branding configuration for IAToolkit
14
+ """
15
+ @inject
16
+ def __init__(self, config_service: ConfigurationService):
17
+ self.config_service = config_service
18
+ """
19
+ Define los estilos de branding por defecto para la aplicación.
20
+ """
21
+ self._default_branding = {
22
+ # --- Estilos del Encabezado Principal ---
23
+ "header_background_color": "#FFFFFF",
24
+ "header_text_color": "#6C757D",
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
35
+
36
+ # Estilos Globales de la Marca ---
37
+ "brand_primary_color": "#0d6efd", # Azul de Bootstrap por defecto
38
+ "brand_secondary_color": "#6c757d", # Gris de Bootstrap por defecto
39
+ "brand_text_on_primary": "#FFFFFF", # Texto blanco sobre color primario
40
+ "brand_text_on_secondary": "#FFFFFF", # Texto blanco sobre color secundario
41
+
42
+ # Estilos para Alertas de Error ---
43
+ "brand_danger_color": "#dc3545", # Rojo principal para alertas
44
+ "brand_danger_bg": "#f8d7da", # Fondo rojo pálido
45
+ "brand_danger_text": "#000000",
46
+ "brand_danger_border": "#f5c2c7", # Borde rojo intermedio
47
+
48
+ # Estilos para Alertas Informativas ---
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
52
+
53
+ # Estilos para el Asistente de Prompts ---
54
+ "prompt_assistant_bg": "#f8f9fa",
55
+ "prompt_assistant_border": "#dee2e6",
56
+ "prompt_assistant_button_bg": "#FFFFFF",
57
+ "prompt_assistant_button_text": "#495057",
58
+ "prompt_assistant_button_border": "#ced4da",
59
+ "prompt_assistant_dropdown_bg": "#f8f9fa",
60
+ "prompt_assistant_header_bg": "#e9ecef",
61
+ "prompt_assistant_header_text": "#495057",
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,
67
+
68
+ # Color para el botón de Enviar ---
69
+ "send_button_color": "#212529" # Gris oscuro/casi negro por defecto
70
+ }
71
+
72
+ def get_company_branding(self, company_short_name: str) -> dict:
73
+ """
74
+ Retorna los estilos de branding finales para una compañía,
75
+ fusionando los valores por defecto con los personalizados.
76
+ """
77
+ final_branding_values = self._default_branding.copy()
78
+ branding_data = self.config_service.get_configuration(company_short_name, 'branding')
79
+ final_branding_values.update(branding_data)
80
+
81
+
82
+ # Función para convertir HEX a RGB
83
+ def hex_to_rgb(hex_color):
84
+ hex_color = hex_color.lstrip('#')
85
+ return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
86
+
87
+ primary_rgb = hex_to_rgb(final_branding_values['brand_primary_color'])
88
+ secondary_rgb = hex_to_rgb(final_branding_values['brand_secondary_color'])
89
+
90
+ # --- CONSTRUCCIÓN DE ESTILOS Y VARIABLES CSS ---
91
+ primary_text_style = (
92
+ f"font-weight: {final_branding_values['primary_font_weight']}; "
93
+ f"font-size: {final_branding_values['primary_font_size']};"
94
+ )
95
+ secondary_text_style = (
96
+ f"font-weight: {final_branding_values['secondary_font_weight']}; "
97
+ f"font-size: {final_branding_values['secondary_font_size']};"
98
+ )
99
+ tertiary_text_style = (
100
+ f"font-weight: {final_branding_values['tertiary_font_weight']}; "
101
+ f"font-size: {final_branding_values['tertiary_font_size']}; "
102
+ f"opacity: {final_branding_values['tertiary_opacity']};"
103
+ )
104
+
105
+ # Generamos el bloque de variables CSS
106
+ css_variables = f"""
107
+ :root {{
108
+ --brand-primary-color: {final_branding_values['brand_primary_color']};
109
+ --brand-secondary-color: {final_branding_values['brand_secondary_color']};
110
+ --brand-header-bg: {final_branding_values['header_background_color']};
111
+ --brand-header-text: {final_branding_values['header_text_color']};
112
+ --brand-text-heading-color: {final_branding_values['brand_text_heading_color']};
113
+
114
+ --brand-primary-color-rgb: {', '.join(map(str, primary_rgb))};
115
+ --brand-secondary-color-rgb: {', '.join(map(str, secondary_rgb))};
116
+ --brand-text-on-primary: {final_branding_values['brand_text_on_primary']};
117
+ --brand-text-on-secondary: {final_branding_values['brand_text_on_secondary']};
118
+ --brand-modal-header-bg: {final_branding_values['header_background_color']};
119
+ --brand-modal-header-text: {final_branding_values['header_text_color']};
120
+ --brand-danger-color: {final_branding_values['brand_danger_color']};
121
+ --brand-danger-bg: {final_branding_values['brand_danger_bg']};
122
+ --brand-danger-text: {final_branding_values['brand_danger_text']};
123
+ --brand-danger-border: {final_branding_values['brand_danger_border']};
124
+ --brand-info-bg: {final_branding_values['brand_info_bg']};
125
+ --brand-info-text: {final_branding_values['brand_info_text'] or final_branding_values['brand_primary_color']};
126
+ --brand-info-border: {final_branding_values['brand_info_border']};
127
+ --brand-prompt-assistant-bg: {final_branding_values['prompt_assistant_bg']};
128
+ --brand-prompt-assistant-border: {final_branding_values['prompt_assistant_border']};
129
+ --brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color'] or final_branding_values['brand_primary_color']};
130
+ --brand-prompt-assistant-button-bg: {final_branding_values['prompt_assistant_button_bg']};
131
+ --brand-prompt-assistant-button-text: {final_branding_values['prompt_assistant_button_text']};
132
+ --brand-prompt-assistant-button-border: {final_branding_values['prompt_assistant_button_border']};
133
+ --brand-prompt-assistant-dropdown-bg: {final_branding_values['prompt_assistant_dropdown_bg']};
134
+ --brand-prompt-assistant-header-bg: {final_branding_values['prompt_assistant_header_bg']};
135
+ --brand-prompt-assistant-header-text: {final_branding_values['prompt_assistant_header_text']};
136
+ --brand-prompt-assistant-item-hover-bg: {final_branding_values['prompt_assistant_item_hover_bg'] or final_branding_values['brand_primary_color']};
137
+ --brand-prompt-assistant-item-hover-text: {final_branding_values['prompt_assistant_item_hover_text'] or final_branding_values['brand_text_on_primary']};
138
+
139
+ }}
140
+ """
141
+
142
+ # get the company name from configuration for the branding render
143
+ company_name = self.config_service.get_configuration(company_short_name, 'name')
144
+
145
+ return {
146
+ "name": company_name,
147
+ "primary_text_style": primary_text_style,
148
+ "secondary_text_style": secondary_text_style,
149
+ "tertiary_text_style": tertiary_text_style,
150
+ "header_text_color": final_branding_values['header_text_color'],
151
+ "css_variables": css_variables,
152
+ "send_button_color": final_branding_values['brand_primary_color']
153
+ }