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.
- iatoolkit/__init__.py +8 -34
- iatoolkit/base_company.py +14 -3
- iatoolkit/common/routes.py +83 -52
- iatoolkit/common/session_manager.py +0 -1
- iatoolkit/common/util.py +0 -27
- iatoolkit/iatoolkit.py +61 -46
- iatoolkit/infra/llm_client.py +7 -8
- iatoolkit/infra/openai_adapter.py +1 -1
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/repositories/database_manager.py +17 -2
- iatoolkit/repositories/models.py +31 -6
- iatoolkit/repositories/profile_repo.py +7 -2
- iatoolkit/services/auth_service.py +188 -0
- iatoolkit/services/branding_service.py +147 -0
- iatoolkit/services/dispatcher_service.py +10 -40
- iatoolkit/services/excel_service.py +15 -15
- iatoolkit/services/history_service.py +3 -12
- iatoolkit/services/jwt_service.py +15 -24
- iatoolkit/services/onboarding_service.py +43 -0
- iatoolkit/services/profile_service.py +97 -44
- iatoolkit/services/query_service.py +124 -81
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +67 -31
- iatoolkit/services/user_session_context_service.py +112 -54
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
- iatoolkit/static/js/chat_history_button.js +126 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +130 -220
- iatoolkit/static/js/chat_onboarding_button.js +97 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +52 -0
- iatoolkit/static/styles/chat_iatoolkit.css +329 -507
- iatoolkit/static/styles/chat_modal.css +95 -56
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/onboarding.css +169 -0
- iatoolkit/system_prompts/query_main.prompt +3 -12
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +40 -0
- iatoolkit/templates/base.html +8 -3
- iatoolkit/templates/change_password.html +54 -37
- iatoolkit/templates/chat.html +149 -66
- iatoolkit/templates/chat_modals.html +47 -18
- iatoolkit/templates/error.html +41 -8
- iatoolkit/templates/forgot_password.html +37 -24
- iatoolkit/templates/index.html +140 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +105 -0
- iatoolkit/templates/signup.html +64 -66
- iatoolkit/views/base_login_view.py +81 -0
- iatoolkit/views/change_password_view.py +23 -12
- iatoolkit/views/external_login_view.py +61 -28
- iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
- iatoolkit/views/forgot_password_view.py +23 -13
- iatoolkit/views/history_api_view.py +52 -0
- iatoolkit/views/home_view.py +58 -25
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +68 -0
- iatoolkit/views/llmquery_api_view.py +45 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +118 -34
- iatoolkit/views/logout_api_view.py +45 -0
- iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
- iatoolkit/views/signup_view.py +38 -29
- iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
- iatoolkit/views/verify_user_view.py +13 -8
- {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
- iatoolkit-0.63.4.dist-info/RECORD +113 -0
- {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
- iatoolkit/common/auth.py +0 -200
- iatoolkit/static/images/arrow_up.png +0 -0
- iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
- iatoolkit/static/images/logo_clinica.png +0 -0
- iatoolkit/static/images/logo_iatoolkit.png +0 -0
- iatoolkit/static/images/logo_maxxa.png +0 -0
- iatoolkit/static/images/logo_notaria.png +0 -0
- iatoolkit/static/images/logo_tarjeta.png +0 -0
- iatoolkit/static/images/logo_umayor.png +0 -0
- iatoolkit/static/images/upload.png +0 -0
- iatoolkit/static/js/chat_history.js +0 -117
- iatoolkit/templates/home.html +0 -201
- iatoolkit/templates/login.html +0 -43
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/chat_view.py +0 -51
- iatoolkit/views/download_file_view.py +0 -58
- iatoolkit/views/external_chat_login_view.py +0 -88
- iatoolkit/views/history_view.py +0 -57
- iatoolkit/views/llmquery_view.py +0 -65
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit-0.8.1.dist-info/RECORD +0 -175
- tests/__init__.py +0 -5
- tests/common/__init__.py +0 -0
- tests/common/test_auth.py +0 -279
- tests/common/test_routes.py +0 -42
- tests/common/test_session_manager.py +0 -59
- tests/common/test_util.py +0 -444
- tests/companies/__init__.py +0 -5
- tests/conftest.py +0 -36
- tests/infra/__init__.py +0 -5
- tests/infra/connectors/__init__.py +0 -5
- tests/infra/connectors/test_google_drive_connector.py +0 -107
- tests/infra/connectors/test_local_file_connector.py +0 -85
- tests/infra/connectors/test_s3_connector.py +0 -95
- tests/infra/test_call_service.py +0 -92
- tests/infra/test_database_manager.py +0 -59
- tests/infra/test_gemini_adapter.py +0 -137
- tests/infra/test_google_chat_app.py +0 -68
- tests/infra/test_llm_client.py +0 -165
- tests/infra/test_llm_proxy.py +0 -122
- tests/infra/test_mail_app.py +0 -94
- tests/infra/test_openai_adapter.py +0 -105
- tests/infra/test_redis_session_manager_service.py +0 -117
- tests/repositories/__init__.py +0 -5
- tests/repositories/test_database_manager.py +0 -87
- tests/repositories/test_document_repo.py +0 -76
- tests/repositories/test_llm_query_repo.py +0 -340
- tests/repositories/test_models.py +0 -38
- tests/repositories/test_profile_repo.py +0 -142
- tests/repositories/test_tasks_repo.py +0 -76
- tests/repositories/test_vs_repo.py +0 -107
- tests/services/__init__.py +0 -5
- tests/services/test_dispatcher_service.py +0 -274
- tests/services/test_document_service.py +0 -181
- tests/services/test_excel_service.py +0 -208
- tests/services/test_file_processor_service.py +0 -121
- tests/services/test_history_service.py +0 -164
- tests/services/test_jwt_service.py +0 -255
- tests/services/test_load_documents_service.py +0 -112
- tests/services/test_mail_service.py +0 -70
- tests/services/test_profile_service.py +0 -379
- tests/services/test_prompt_manager_service.py +0 -190
- tests/services/test_query_service.py +0 -243
- tests/services/test_search_service.py +0 -39
- tests/services/test_sql_service.py +0 -160
- tests/services/test_tasks_service.py +0 -252
- tests/services/test_user_feedback_service.py +0 -389
- tests/services/test_user_session_context_service.py +0 -132
- tests/views/__init__.py +0 -5
- tests/views/test_change_password_view.py +0 -191
- tests/views/test_chat_token_request_view.py +0 -188
- tests/views/test_chat_view.py +0 -98
- tests/views/test_download_file_view.py +0 -149
- tests/views/test_external_chat_login_view.py +0 -120
- tests/views/test_external_login_view.py +0 -102
- tests/views/test_file_store_view.py +0 -128
- tests/views/test_forgot_password_view.py +0 -142
- tests/views/test_history_view.py +0 -336
- tests/views/test_home_view.py +0 -61
- tests/views/test_llm_query_view.py +0 -154
- tests/views/test_login_view.py +0 -114
- tests/views/test_prompt_view.py +0 -111
- tests/views/test_signup_view.py +0 -140
- tests/views/test_tasks_review_view.py +0 -104
- tests/views/test_tasks_view.py +0 -130
- tests/views/test_user_feedback_view.py +0 -214
- tests/views/test_verify_user_view.py +0 -110
- {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,
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
iatoolkit/repositories/models.py
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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 {'
|
|
30
|
+
return {'message': 'Historial vacio actualmente', 'history': []}
|
|
40
31
|
|
|
41
32
|
history_list = [query.to_dict() for query in history]
|
|
42
33
|
|