iatoolkit 0.8.1__py3-none-any.whl → 0.63.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 (159) hide show
  1. iatoolkit/__init__.py +8 -34
  2. iatoolkit/base_company.py +14 -3
  3. iatoolkit/common/routes.py +83 -52
  4. iatoolkit/common/session_manager.py +0 -1
  5. iatoolkit/common/util.py +0 -27
  6. iatoolkit/iatoolkit.py +61 -46
  7. iatoolkit/infra/llm_client.py +7 -8
  8. iatoolkit/infra/openai_adapter.py +1 -1
  9. iatoolkit/infra/redis_session_manager.py +48 -2
  10. iatoolkit/repositories/database_manager.py +17 -2
  11. iatoolkit/repositories/models.py +31 -6
  12. iatoolkit/repositories/profile_repo.py +7 -2
  13. iatoolkit/services/auth_service.py +188 -0
  14. iatoolkit/services/branding_service.py +147 -0
  15. iatoolkit/services/dispatcher_service.py +10 -40
  16. iatoolkit/services/excel_service.py +15 -15
  17. iatoolkit/services/history_service.py +3 -12
  18. iatoolkit/services/jwt_service.py +15 -24
  19. iatoolkit/services/onboarding_service.py +43 -0
  20. iatoolkit/services/profile_service.py +97 -44
  21. iatoolkit/services/query_service.py +124 -81
  22. iatoolkit/services/tasks_service.py +1 -1
  23. iatoolkit/services/user_feedback_service.py +67 -31
  24. iatoolkit/services/user_session_context_service.py +112 -54
  25. iatoolkit/static/images/fernando.jpeg +0 -0
  26. iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
  27. iatoolkit/static/js/chat_history_button.js +126 -0
  28. iatoolkit/static/js/chat_logout_button.js +36 -0
  29. iatoolkit/static/js/chat_main.js +130 -220
  30. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  31. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  32. iatoolkit/static/js/chat_reload_button.js +52 -0
  33. iatoolkit/static/styles/chat_iatoolkit.css +329 -507
  34. iatoolkit/static/styles/chat_modal.css +95 -56
  35. iatoolkit/static/styles/landing_page.css +182 -0
  36. iatoolkit/static/styles/onboarding.css +169 -0
  37. iatoolkit/system_prompts/query_main.prompt +3 -12
  38. iatoolkit/templates/_company_header.html +20 -0
  39. iatoolkit/templates/_login_widget.html +40 -0
  40. iatoolkit/templates/base.html +8 -3
  41. iatoolkit/templates/change_password.html +54 -37
  42. iatoolkit/templates/chat.html +149 -66
  43. iatoolkit/templates/chat_modals.html +47 -18
  44. iatoolkit/templates/error.html +41 -8
  45. iatoolkit/templates/forgot_password.html +37 -24
  46. iatoolkit/templates/index.html +140 -0
  47. iatoolkit/templates/login_simulation.html +34 -0
  48. iatoolkit/templates/onboarding_shell.html +105 -0
  49. iatoolkit/templates/signup.html +64 -66
  50. iatoolkit/views/base_login_view.py +81 -0
  51. iatoolkit/views/change_password_view.py +23 -12
  52. iatoolkit/views/external_login_view.py +61 -28
  53. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
  54. iatoolkit/views/forgot_password_view.py +23 -13
  55. iatoolkit/views/history_api_view.py +52 -0
  56. iatoolkit/views/home_view.py +58 -25
  57. iatoolkit/views/index_view.py +14 -0
  58. iatoolkit/views/init_context_api_view.py +68 -0
  59. iatoolkit/views/llmquery_api_view.py +45 -0
  60. iatoolkit/views/login_simulation_view.py +81 -0
  61. iatoolkit/views/login_view.py +118 -34
  62. iatoolkit/views/logout_api_view.py +45 -0
  63. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
  64. iatoolkit/views/signup_view.py +38 -29
  65. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  66. iatoolkit/views/tasks_review_api_view.py +55 -0
  67. iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
  68. iatoolkit/views/verify_user_view.py +13 -8
  69. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
  70. iatoolkit-0.63.4.dist-info/RECORD +113 -0
  71. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
  72. iatoolkit/common/auth.py +0 -200
  73. iatoolkit/static/images/arrow_up.png +0 -0
  74. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  75. iatoolkit/static/images/logo_clinica.png +0 -0
  76. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  77. iatoolkit/static/images/logo_maxxa.png +0 -0
  78. iatoolkit/static/images/logo_notaria.png +0 -0
  79. iatoolkit/static/images/logo_tarjeta.png +0 -0
  80. iatoolkit/static/images/logo_umayor.png +0 -0
  81. iatoolkit/static/images/upload.png +0 -0
  82. iatoolkit/static/js/chat_history.js +0 -117
  83. iatoolkit/templates/home.html +0 -201
  84. iatoolkit/templates/login.html +0 -43
  85. iatoolkit/views/chat_token_request_view.py +0 -98
  86. iatoolkit/views/chat_view.py +0 -51
  87. iatoolkit/views/download_file_view.py +0 -58
  88. iatoolkit/views/external_chat_login_view.py +0 -88
  89. iatoolkit/views/history_view.py +0 -57
  90. iatoolkit/views/llmquery_view.py +0 -65
  91. iatoolkit/views/tasks_review_view.py +0 -83
  92. iatoolkit-0.8.1.dist-info/RECORD +0 -175
  93. tests/__init__.py +0 -5
  94. tests/common/__init__.py +0 -0
  95. tests/common/test_auth.py +0 -279
  96. tests/common/test_routes.py +0 -42
  97. tests/common/test_session_manager.py +0 -59
  98. tests/common/test_util.py +0 -444
  99. tests/companies/__init__.py +0 -5
  100. tests/conftest.py +0 -36
  101. tests/infra/__init__.py +0 -5
  102. tests/infra/connectors/__init__.py +0 -5
  103. tests/infra/connectors/test_google_drive_connector.py +0 -107
  104. tests/infra/connectors/test_local_file_connector.py +0 -85
  105. tests/infra/connectors/test_s3_connector.py +0 -95
  106. tests/infra/test_call_service.py +0 -92
  107. tests/infra/test_database_manager.py +0 -59
  108. tests/infra/test_gemini_adapter.py +0 -137
  109. tests/infra/test_google_chat_app.py +0 -68
  110. tests/infra/test_llm_client.py +0 -165
  111. tests/infra/test_llm_proxy.py +0 -122
  112. tests/infra/test_mail_app.py +0 -94
  113. tests/infra/test_openai_adapter.py +0 -105
  114. tests/infra/test_redis_session_manager_service.py +0 -117
  115. tests/repositories/__init__.py +0 -5
  116. tests/repositories/test_database_manager.py +0 -87
  117. tests/repositories/test_document_repo.py +0 -76
  118. tests/repositories/test_llm_query_repo.py +0 -340
  119. tests/repositories/test_models.py +0 -38
  120. tests/repositories/test_profile_repo.py +0 -142
  121. tests/repositories/test_tasks_repo.py +0 -76
  122. tests/repositories/test_vs_repo.py +0 -107
  123. tests/services/__init__.py +0 -5
  124. tests/services/test_dispatcher_service.py +0 -274
  125. tests/services/test_document_service.py +0 -181
  126. tests/services/test_excel_service.py +0 -208
  127. tests/services/test_file_processor_service.py +0 -121
  128. tests/services/test_history_service.py +0 -164
  129. tests/services/test_jwt_service.py +0 -255
  130. tests/services/test_load_documents_service.py +0 -112
  131. tests/services/test_mail_service.py +0 -70
  132. tests/services/test_profile_service.py +0 -379
  133. tests/services/test_prompt_manager_service.py +0 -190
  134. tests/services/test_query_service.py +0 -243
  135. tests/services/test_search_service.py +0 -39
  136. tests/services/test_sql_service.py +0 -160
  137. tests/services/test_tasks_service.py +0 -252
  138. tests/services/test_user_feedback_service.py +0 -389
  139. tests/services/test_user_session_context_service.py +0 -132
  140. tests/views/__init__.py +0 -5
  141. tests/views/test_change_password_view.py +0 -191
  142. tests/views/test_chat_token_request_view.py +0 -188
  143. tests/views/test_chat_view.py +0 -98
  144. tests/views/test_download_file_view.py +0 -149
  145. tests/views/test_external_chat_login_view.py +0 -120
  146. tests/views/test_external_login_view.py +0 -102
  147. tests/views/test_file_store_view.py +0 -128
  148. tests/views/test_forgot_password_view.py +0 -142
  149. tests/views/test_history_view.py +0 -336
  150. tests/views/test_home_view.py +0 -61
  151. tests/views/test_llm_query_view.py +0 -154
  152. tests/views/test_login_view.py +0 -114
  153. tests/views/test_prompt_view.py +0 -111
  154. tests/views/test_signup_view.py +0 -140
  155. tests/views/test_tasks_review_view.py +0 -104
  156. tests/views/test_tasks_view.py +0 -130
  157. tests/views/test_user_feedback_view.py +0 -214
  158. tests/views/test_verify_user_view.py +0 -110
  159. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/WHEEL +0 -0
@@ -37,9 +37,14 @@ class RedisSessionManager:
37
37
  return cls._client
38
38
 
39
39
  @classmethod
40
- def set(cls, key: str, value: str, ex: int = None):
40
+ def set(cls, key: str, value: str, **kwargs):
41
+ """
42
+ Método set flexible que pasa argumentos opcionales (como ex, nx)
43
+ directamente al cliente de redis.
44
+ """
41
45
  client = cls._get_client()
42
- result = client.set(key, value, ex=ex)
46
+ # Pasa todos los argumentos de palabra clave adicionales al cliente real
47
+ result = client.set(key, value, **kwargs)
43
48
  return result
44
49
 
45
50
  @classmethod
@@ -49,12 +54,53 @@ class RedisSessionManager:
49
54
  result = value if value is not None else default
50
55
  return result
51
56
 
57
+ @classmethod
58
+ def hset(cls, key: str, field: str, value: str):
59
+ """
60
+ Establece un campo en un Hash de Redis.
61
+ """
62
+ client = cls._get_client()
63
+ return client.hset(key, field, value)
64
+
65
+ @classmethod
66
+ def hget(cls, key: str, field: str):
67
+ """
68
+ Obtiene el valor de un campo de un Hash de Redis.
69
+ Devuelve None si la clave o el campo no existen.
70
+ """
71
+ client = cls._get_client()
72
+ return client.hget(key, field)
73
+
74
+ @classmethod
75
+ def hdel(cls, key: str, *fields):
76
+ """
77
+ Elimina uno o más campos de un Hash de Redis.
78
+ """
79
+ client = cls._get_client()
80
+ return client.hdel(key, *fields)
81
+
82
+ @classmethod
83
+ def pipeline(cls):
84
+ """
85
+ Inicia una transacción (pipeline) de Redis.
86
+ """
87
+ client = cls._get_client()
88
+ return client.pipeline()
89
+
90
+
52
91
  @classmethod
53
92
  def remove(cls, key: str):
54
93
  client = cls._get_client()
55
94
  result = client.delete(key)
56
95
  return result
57
96
 
97
+ @classmethod
98
+ def exists(cls, key: str) -> bool:
99
+ """Verifica si una clave existe en Redis."""
100
+ client = cls._get_client()
101
+ # El comando EXISTS de Redis devuelve un entero (0 o 1). Lo convertimos a booleano.
102
+ return bool(client.exists(key))
103
+
58
104
  @classmethod
59
105
  def set_json(cls, key: str, value: dict, ex: int = None):
60
106
  json_str = json.dumps(value)
@@ -21,8 +21,23 @@ class DatabaseManager:
21
21
  :param echo: Si True, habilita logs de SQL.
22
22
  """
23
23
  self.url = make_url(database_url)
24
- self._engine = create_engine(database_url, echo=False)
25
- self.SessionFactory = sessionmaker(bind=self._engine)
24
+ if database_url.startswith('sqlite'): # for tests
25
+ self._engine = create_engine(database_url, echo=False)
26
+ else:
27
+ self._engine = create_engine(
28
+ database_url,
29
+ echo=False,
30
+ pool_size=2, # per worker
31
+ max_overflow=3,
32
+ pool_timeout=30,
33
+ pool_recycle=1800,
34
+ pool_pre_ping=True,
35
+ future=True,
36
+ )
37
+ self.SessionFactory = sessionmaker(bind=self._engine,
38
+ autoflush=False,
39
+ autocommit=False,
40
+ expire_on_commit=False)
26
41
  self.scoped_session = scoped_session(self.SessionFactory)
27
42
 
28
43
  # REGISTRAR pgvector para cada nueva conexión solo en postgres
@@ -3,9 +3,10 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from sqlalchemy import Column, Integer, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
6
+ from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
7
7
  from sqlalchemy.orm import DeclarativeBase
8
8
  from sqlalchemy.orm import relationship, class_mapper, declarative_base
9
+ from sqlalchemy.sql import func
9
10
  from datetime import datetime
10
11
  from pgvector.sqlalchemy import Vector
11
12
  from enum import Enum as PyEnum
@@ -57,10 +58,11 @@ class Company(Base):
57
58
  openai_api_key = Column(String, nullable=True)
58
59
  gemini_api_key = Column(String, nullable=True)
59
60
 
60
- logo_file = Column(String(128), nullable=True, default='')
61
- parameters = Column(JSON, nullable=True, default={})
61
+ branding = Column(JSON, nullable=True)
62
+ onboarding_cards = Column(JSON, nullable=True)
63
+ parameters = Column(JSON, nullable=True)
62
64
  created_at = Column(DateTime, default=datetime.now)
63
- allow_jwt = Column(Boolean, default=False, nullable=True)
65
+ allow_jwt = Column(Boolean, default=True, nullable=True)
64
66
 
65
67
  documents = relationship("Document",
66
68
  back_populates="company",
@@ -264,8 +266,7 @@ class UserFeedback(Base):
264
266
  id = Column(Integer, primary_key=True)
265
267
  company_id = Column(Integer, ForeignKey('iat_companies.id',
266
268
  ondelete='CASCADE'), nullable=False)
267
- local_user_id = Column(Integer, default=0, nullable=True)
268
- external_user_id = Column(String(128), default='', nullable=True)
269
+ user_identifier = Column(String(128), default='', nullable=True)
269
270
  message = Column(Text, nullable=False)
270
271
  rating = Column(Integer, nullable=False)
271
272
  created_at = Column(DateTime, default=datetime.now)
@@ -307,3 +308,27 @@ class Prompt(Base):
307
308
 
308
309
  company = relationship("Company", back_populates="prompts")
309
310
  category = relationship("PromptCategory", back_populates="prompts")
311
+
312
+ class AccessLog(Base):
313
+ # Modelo ORM para registrar cada intento de acceso a la plataforma.
314
+ __tablename__ = 'iat_access_log'
315
+
316
+ id = Column(BigInteger, primary_key=True)
317
+
318
+ timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
319
+ company_short_name = Column(String(100), nullable=False, index=True)
320
+ user_identifier = Column(String(255), index=True)
321
+
322
+ # Cómo y el Resultado
323
+ auth_type = Column(String(20), nullable=False) # 'local', 'external_api', 'redeem_token', etc.
324
+ outcome = Column(String(10), nullable=False) # 'success' o 'failure'
325
+ reason_code = Column(String(50)) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
326
+
327
+ # Contexto de la Petición
328
+ source_ip = Column(String(45), nullable=False)
329
+ user_agent_hash = Column(String(16)) # Hash corto del User-Agent
330
+ request_path = Column(String(255), nullable=False)
331
+
332
+ def __repr__(self):
333
+ return (f"<AccessLog(id={self.id}, company='{self.company_short_name}', "
334
+ f"user='{self.user_identifier}', outcome='{self.outcome}')>")
@@ -72,9 +72,14 @@ class ProfileRepo:
72
72
  def create_company(self, new_company: Company):
73
73
  company = self.session.query(Company).filter_by(name=new_company.name).first()
74
74
  if company:
75
- company.parameters = new_company.parameters
76
- company.logo_file = new_company.logo_file
75
+ if company.parameters != new_company.parameters:
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
77
81
  else:
82
+ # Si la compañía no existe, la añade a la sesión.
78
83
  self.session.add(new_company)
79
84
  company = new_company
80
85
 
@@ -0,0 +1,188 @@
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.repositories.database_manager import DatabaseManager
11
+ from iatoolkit.repositories.models import AccessLog
12
+ from flask import request
13
+ import logging
14
+ import hashlib
15
+
16
+
17
+ class AuthService:
18
+ """
19
+ Centralized service for handling authentication for all incoming requests.
20
+ It determines the user's identity based on either a Flask session cookie or an API Key.
21
+ """
22
+
23
+ @inject
24
+ def __init__(self, profile_service: ProfileService,
25
+ jwt_service: JWTService,
26
+ db_manager: DatabaseManager
27
+ ):
28
+ self.profile_service = profile_service
29
+ self.jwt_service = jwt_service
30
+ self.db_manager = db_manager
31
+
32
+ def login_local_user(self, company_short_name: str, email: str, password: str) -> dict:
33
+ # try to autenticate a local user, register the event and return the result
34
+ auth_response = self.profile_service.login(
35
+ company_short_name=company_short_name,
36
+ email=email,
37
+ password=password
38
+ )
39
+
40
+ if not auth_response.get('success'):
41
+ self.log_access(
42
+ company_short_name=company_short_name,
43
+ user_identifier=email,
44
+ auth_type='local',
45
+ outcome='failure',
46
+ reason_code='INVALID_CREDENTIALS',
47
+ )
48
+ else:
49
+ self.log_access(
50
+ company_short_name=company_short_name,
51
+ auth_type='local',
52
+ outcome='success',
53
+ user_identifier=auth_response.get('user_identifier')
54
+ )
55
+
56
+ return auth_response
57
+
58
+ def redeem_token_for_session(self, company_short_name: str, token: str) -> dict:
59
+ # redeem a token for a session, register the event and return the result
60
+ payload = self.jwt_service.validate_chat_jwt(token)
61
+
62
+ if not payload:
63
+ self.log_access(
64
+ company_short_name=company_short_name,
65
+ auth_type='redeem_token',
66
+ outcome='failure',
67
+ reason_code='JWT_INVALID'
68
+ )
69
+ return {'success': False, 'error': 'Token inválido o expirado'}
70
+
71
+ # 2. if token is valid, extract the user_identifier
72
+ user_identifier = payload.get('user_identifier')
73
+ try:
74
+ # create the Flask session
75
+ self.profile_service.set_session_for_user(company_short_name, user_identifier)
76
+ self.log_access(
77
+ company_short_name=company_short_name,
78
+ auth_type='redeem_token',
79
+ outcome='success',
80
+ user_identifier=user_identifier
81
+ )
82
+ return {'success': True, 'user_identifier': user_identifier}
83
+ except Exception as e:
84
+ logging.error(f"Error al crear la sesión desde token para {user_identifier}: {e}")
85
+ self.log_access(
86
+ company_short_name=company_short_name,
87
+ auth_type='redeem_token',
88
+ outcome='failure',
89
+ reason_code='SESSION_CREATION_FAILED',
90
+ user_identifier=user_identifier
91
+ )
92
+ return {'success': False, 'error': 'No se pudo crear la sesión del usuario'}
93
+
94
+ def verify(self, anonymous: bool = False) -> dict:
95
+ """
96
+ Verifies the current request and identifies the user.
97
+ If anonymous is True the non-presence of use_identifier is ignored
98
+
99
+ Returns a dictionary with:
100
+ - success: bool
101
+ - user_identifier: str (if successful)
102
+ - company_short_name: str (if successful)
103
+ - error_message: str (on failure)
104
+ - status_code: int (on failure)
105
+ """
106
+ # --- Priority 1: Check for a valid Flask web session ---
107
+ session_info = self.profile_service.get_current_session_info()
108
+ if session_info and session_info.get('user_identifier'):
109
+ # User is authenticated via a web session cookie.
110
+ return {
111
+ "success": True,
112
+ "company_short_name": session_info['company_short_name'],
113
+ "user_identifier": session_info['user_identifier'],
114
+ }
115
+
116
+ # --- Priority 2: Check for a valid API Key in headers ---
117
+ api_key = None
118
+ auth = request.headers.get('Authorization', '')
119
+ if isinstance(auth, str) and auth.lower().startswith('bearer '):
120
+ api_key = auth.split(' ', 1)[1].strip()
121
+
122
+ if not api_key:
123
+ # --- Failure: No valid credentials found ---
124
+ logging.info(f"Authentication required. No session cookie or API Key provided.")
125
+ return {"success": False,
126
+ "error_message": "Authentication required. No session cookie or API Key provided.",
127
+ "status_code": 401}
128
+
129
+ # check if the api-key is valid and active
130
+ api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
131
+ if not api_key_entry:
132
+ logging.info(f"Invalid or inactive API Key {api_key}")
133
+ return {"success": False, "error_message": "Invalid or inactive API Key",
134
+ "status_code": 402}
135
+
136
+ # get the company from the api_key_entry
137
+ company = api_key_entry.company
138
+
139
+ # For API calls, the external_user_id must be provided in the request.
140
+ data = request.get_json(silent=True) or {}
141
+ user_identifier = data.get('user_identifier', '')
142
+ if not anonymous and not user_identifier:
143
+ logging.info(f"No user_identifier provided for API call.")
144
+ return {"success": False, "error_message": "No user_identifier provided for API call.",
145
+ "status_code": 403}
146
+
147
+ return {
148
+ "success": True,
149
+ "company_short_name": company.short_name,
150
+ "user_identifier": user_identifier
151
+ }
152
+
153
+
154
+ def log_access(self,
155
+ company_short_name: str,
156
+ auth_type: str,
157
+ outcome: str,
158
+ user_identifier: str = None,
159
+ reason_code: str = None):
160
+ """
161
+ Registra un intento de acceso en la base de datos.
162
+ Es "best-effort" y no debe interrumpir el flujo de autenticación.
163
+ """
164
+ session = self.db_manager.scoped_session()
165
+ try:
166
+ # Capturar datos del contexto de la petición de Flask
167
+ source_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
168
+ path = request.path
169
+ ua = request.headers.get('User-Agent', '')
170
+ ua_hash = hashlib.sha256(ua.encode()).hexdigest()[:16] if ua else None
171
+
172
+ # Crear la entrada de log
173
+ log_entry = AccessLog(
174
+ company_short_name=company_short_name,
175
+ user_identifier=user_identifier,
176
+ auth_type=auth_type,
177
+ outcome=outcome,
178
+ reason_code=reason_code,
179
+ source_ip=source_ip,
180
+ user_agent_hash=ua_hash,
181
+ request_path=path,
182
+ )
183
+ session.add(log_entry)
184
+ session.commit()
185
+
186
+ except Exception as e:
187
+ logging.error(f"Fallo al escribir en AccessLog: {e}", exc_info=False)
188
+ session.rollback()
@@ -0,0 +1,147 @@
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
+
8
+
9
+ class BrandingService:
10
+ """
11
+ Servicio centralizado que gestiona la configuración de branding.
12
+ """
13
+
14
+ def __init__(self):
15
+ """
16
+ Define los estilos de branding por defecto para la aplicación.
17
+ """
18
+ self._default_branding = {
19
+ # --- Estilos del Encabezado Principal ---
20
+ "header_background_color": "#FFFFFF",
21
+ "header_text_color": "#6C757D",
22
+ "primary_font_weight": "600",
23
+ "primary_font_size": "1.2rem",
24
+ "secondary_font_weight": "400",
25
+ "secondary_font_size": "0.9rem",
26
+ "tertiary_font_weight": "300",
27
+ "tertiary_font_size": "0.8rem",
28
+ "tertiary_opacity": "0.7",
29
+
30
+ # headings
31
+ "brand_text_heading_color": "#334155", # Gris pizarra por defecto
32
+
33
+ # Estilos Globales de la Marca ---
34
+ "brand_primary_color": "#0d6efd", # Azul de Bootstrap por defecto
35
+ "brand_secondary_color": "#6c757d", # Gris de Bootstrap por defecto
36
+ "brand_text_on_primary": "#FFFFFF", # Texto blanco sobre color primario
37
+ "brand_text_on_secondary": "#FFFFFF", # Texto blanco sobre color secundario
38
+
39
+ # Estilos para Alertas de Error ---
40
+ "brand_danger_color": "#dc3545", # Rojo principal para alertas
41
+ "brand_danger_bg": "#f8d7da", # Fondo rojo pálido
42
+ "brand_danger_text": "#842029", # Texto rojo oscuro
43
+ "brand_danger_border": "#f5c2c7", # Borde rojo intermedio
44
+
45
+ # Estilos para Alertas Informativas ---
46
+ "brand_info_bg": "#F0F4F8", # Un fondo de gris azulado muy pálido
47
+ "brand_info_text": "#0d6efd", # Texto en el color primario
48
+ "brand_info_border": "#D9E2EC", # Borde de gris azulado pálido
49
+
50
+ # Estilos para el Asistente de Prompts ---
51
+ "prompt_assistant_bg": "#f8f9fa",
52
+ "prompt_assistant_border": "#dee2e6",
53
+ "prompt_assistant_button_bg": "#FFFFFF",
54
+ "prompt_assistant_button_text": "#495057",
55
+ "prompt_assistant_button_border": "#ced4da",
56
+ "prompt_assistant_dropdown_bg": "#f8f9fa",
57
+ "prompt_assistant_header_bg": "#e9ecef",
58
+ "prompt_assistant_header_text": "#495057",
59
+
60
+ # this use the primary by default
61
+ "prompt_assistant_icon_color": None,
62
+ "prompt_assistant_item_hover_bg": None,
63
+ "prompt_assistant_item_hover_text": None,
64
+
65
+ # Color para el botón de Enviar ---
66
+ "send_button_color": "#212529" # Gris oscuro/casi negro por defecto
67
+ }
68
+
69
+ def get_company_branding(self, company: Company | None) -> dict:
70
+ """
71
+ Retorna los estilos de branding finales para una compañía,
72
+ fusionando los valores por defecto con los personalizados.
73
+ """
74
+ final_branding_values = self._default_branding.copy()
75
+
76
+ if company and company.branding:
77
+ final_branding_values.update(company.branding)
78
+
79
+ # Función para convertir HEX a RGB
80
+ def hex_to_rgb(hex_color):
81
+ hex_color = hex_color.lstrip('#')
82
+ return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
83
+
84
+ primary_rgb = hex_to_rgb(final_branding_values['brand_primary_color'])
85
+ secondary_rgb = hex_to_rgb(final_branding_values['brand_secondary_color'])
86
+
87
+ # --- CONSTRUCCIÓN DE ESTILOS Y VARIABLES CSS ---
88
+ primary_text_style = (
89
+ f"font-weight: {final_branding_values['primary_font_weight']}; "
90
+ f"font-size: {final_branding_values['primary_font_size']};"
91
+ )
92
+ secondary_text_style = (
93
+ f"font-weight: {final_branding_values['secondary_font_weight']}; "
94
+ f"font-size: {final_branding_values['secondary_font_size']};"
95
+ )
96
+ tertiary_text_style = (
97
+ f"font-weight: {final_branding_values['tertiary_font_weight']}; "
98
+ f"font-size: {final_branding_values['tertiary_font_size']}; "
99
+ f"opacity: {final_branding_values['tertiary_opacity']};"
100
+ )
101
+
102
+ # Generamos el bloque de variables CSS
103
+ css_variables = f"""
104
+ :root {{
105
+ --brand-primary-color: {final_branding_values['brand_primary_color']};
106
+ --brand-secondary-color: {final_branding_values['brand_secondary_color']};
107
+ --brand-header-bg: {final_branding_values['header_background_color']};
108
+ --brand-header-text: {final_branding_values['header_text_color']};
109
+ --brand-text-heading-color: {final_branding_values['brand_text_heading_color']};
110
+
111
+ --brand-primary-color-rgb: {', '.join(map(str, primary_rgb))};
112
+ --brand-secondary-color-rgb: {', '.join(map(str, secondary_rgb))};
113
+ --brand-text-on-primary: {final_branding_values['brand_text_on_primary']};
114
+ --brand-text-on-secondary: {final_branding_values['brand_text_on_secondary']};
115
+ --brand-modal-header-bg: {final_branding_values['header_background_color']};
116
+ --brand-modal-header-text: {final_branding_values['header_text_color']};
117
+ --brand-danger-color: {final_branding_values['brand_danger_color']};
118
+ --brand-danger-bg: {final_branding_values['brand_danger_bg']};
119
+ --brand-danger-text: {final_branding_values['brand_danger_text']};
120
+ --brand-danger-border: {final_branding_values['brand_danger_border']};
121
+ --brand-info-bg: {final_branding_values['brand_info_bg']};
122
+ --brand-info-text: {final_branding_values['brand_info_text'] or final_branding_values['brand_primary_color']};
123
+ --brand-info-border: {final_branding_values['brand_info_border']};
124
+ --brand-prompt-assistant-bg: {final_branding_values['prompt_assistant_bg']};
125
+ --brand-prompt-assistant-border: {final_branding_values['prompt_assistant_border']};
126
+ --brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color'] or final_branding_values['brand_primary_color']};
127
+ --brand-prompt-assistant-button-bg: {final_branding_values['prompt_assistant_button_bg']};
128
+ --brand-prompt-assistant-button-text: {final_branding_values['prompt_assistant_button_text']};
129
+ --brand-prompt-assistant-button-border: {final_branding_values['prompt_assistant_button_border']};
130
+ --brand-prompt-assistant-dropdown-bg: {final_branding_values['prompt_assistant_dropdown_bg']};
131
+ --brand-prompt-assistant-header-bg: {final_branding_values['prompt_assistant_header_bg']};
132
+ --brand-prompt-assistant-header-text: {final_branding_values['prompt_assistant_header_text']};
133
+ --brand-prompt-assistant-item-hover-bg: {final_branding_values['prompt_assistant_item_hover_bg'] or final_branding_values['brand_primary_color']};
134
+ --brand-prompt-assistant-item-hover-text: {final_branding_values['prompt_assistant_item_hover_text'] or final_branding_values['brand_text_on_primary']};
135
+
136
+ }}
137
+ """
138
+
139
+ return {
140
+ "name": company.name if company else "IAToolkit",
141
+ "primary_text_style": primary_text_style,
142
+ "secondary_text_style": secondary_text_style,
143
+ "tertiary_text_style": tertiary_text_style,
144
+ "header_text_color": final_branding_values['header_text_color'],
145
+ "css_variables": css_variables,
146
+ "send_button_color": final_branding_values['send_button_color']
147
+ }
@@ -10,7 +10,6 @@ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
10
10
  from iatoolkit.repositories.models import Company, Function
11
11
  from iatoolkit.services.excel_service import ExcelService
12
12
  from iatoolkit.services.mail_service import MailService
13
- from iatoolkit.common.session_manager import SessionManager
14
13
  from iatoolkit.common.util import Utility
15
14
  from injector import inject
16
15
  import logging
@@ -171,50 +170,21 @@ class Dispatcher:
171
170
  tools.append(ai_tool)
172
171
  return tools
173
172
 
174
- def get_user_info(self, company_name: str, user_identifier: str, is_local_user: bool) -> dict:
173
+ def get_user_info(self, company_name: str, user_identifier: str) -> dict:
175
174
  if company_name not in self.company_instances:
176
175
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
177
176
  f"Empresa no configurada: {company_name}")
178
177
 
179
- raw_user_data = {}
180
- if is_local_user:
181
- # source 1: local user login into IAToolkit
182
- raw_user_data = SessionManager.get('user', {})
183
- else:
184
- # source 2: external company user
185
- company_instance = self.company_instances[company_name]
186
- try:
187
- raw_user_data = company_instance.get_user_info(user_identifier)
188
- except Exception as e:
189
- logging.exception(e)
190
- raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
191
- f"Error en get_user_info de {company_name}: {str(e)}") from e
192
-
193
- # always normalize the data for consistent structure
194
- return self._normalize_user_data(raw_user_data, is_local_user)
195
-
196
- def _normalize_user_data(self, raw_data: dict, is_local: bool) -> dict:
197
- """
198
- Asegura que los datos del usuario siempre tengan una estructura consistente.
199
- """
200
- # default values
201
- normalized_user = {
202
- "id": raw_data.get("id", 0),
203
- "user_email": raw_data.get("email", ""),
204
- "user_fullname": raw_data.get("user_fullname", ""),
205
- "company_id": raw_data.get("company_id", 0),
206
- "company_name": raw_data.get("company", ""),
207
- "company_short_name": raw_data.get("company_short_name", ""),
208
- "is_local": is_local,
209
- "extras": raw_data.get("extras", {})
210
- }
211
-
212
- # get the extras from the raw data, if any
213
- extras = raw_data.get("extras", {})
214
- if isinstance(extras, dict):
215
- 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
216
186
 
217
- return normalized_user
187
+ return external_user_profile
218
188
 
219
189
  def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
220
190
  if company_name not in self.company_instances:
@@ -23,21 +23,21 @@ class ExcelService:
23
23
 
24
24
  def excel_generator(self, **kwargs) -> str:
25
25
  """
26
- Genera un Excel a partir de una lista de diccionarios.
27
-
28
- Parámetros esperados en kwargs:
29
- - filename: str (nombre lógico a mostrar, ej. "reporte_clientes.xlsx") [obligatorio]
30
- - data: list[dict] (filas del excel) [obligatorio]
31
- - sheet_name: str = "hoja 1"
32
-
33
- Retorna:
34
- {
35
- "filename": "reporte.xlsx",
36
- "attachment_token": "8b7f8a66-...-c1c3.xlsx",
37
- "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
38
- "download_link": "/download/8b7f8a66-...-c1c3.xlsx"
39
- }
40
- """
26
+ Genera un Excel a partir de una lista de diccionarios.
27
+
28
+ Parámetros esperados en kwargs:
29
+ - filename: str (nombre lógico a mostrar, ej. "reporte_clientes.xlsx") [obligatorio]
30
+ - data: list[dict] (filas del excel) [obligatorio]
31
+ - sheet_name: str = "hoja 1"
32
+
33
+ Retorna:
34
+ {
35
+ "filename": "reporte.xlsx",
36
+ "attachment_token": "8b7f8a66-...-c1c3.xlsx",
37
+ "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
38
+ "download_link": "/download/8b7f8a66-...-c1c3.xlsx"
39
+ }
40
+ """
41
41
  try:
42
42
  # get the parameters
43
43
  fname = kwargs.get('filename')
@@ -5,29 +5,20 @@
5
5
 
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
-
9
8
  from iatoolkit.repositories.profile_repo import ProfileRepo
10
- from iatoolkit.common.util import Utility
11
9
 
12
10
 
13
11
  class HistoryService:
14
12
  @inject
15
13
  def __init__(self, llm_query_repo: LLMQueryRepo,
16
- profile_repo: ProfileRepo,
17
- util: Utility):
14
+ profile_repo: ProfileRepo):
18
15
  self.llm_query_repo = llm_query_repo
19
16
  self.profile_repo = profile_repo
20
- self.util = util
21
17
 
22
18
  def get_history(self,
23
19
  company_short_name: str,
24
- external_user_id: str = None,
25
- local_user_id: int = 0) -> dict:
20
+ user_identifier: str) -> dict:
26
21
  try:
27
- user_identifier, _ = self.util.resolve_user_identifier(external_user_id, local_user_id)
28
- if not user_identifier:
29
- return {'error': "No se pudo resolver el identificador del usuario"}
30
-
31
22
  # validate company
32
23
  company = self.profile_repo.get_company_by_short_name(company_short_name)
33
24
  if not company:
@@ -36,7 +27,7 @@ class HistoryService:
36
27
  history = self.llm_query_repo.get_history(company, user_identifier)
37
28
 
38
29
  if not history:
39
- return {'error': 'No se pudo obtener el historial'}
30
+ return {'message': 'Historial vacio actualmente', 'history': []}
40
31
 
41
32
  history_list = [query.to_dict() for query in history]
42
33