iatoolkit 0.22.1__py3-none-any.whl → 0.50.2__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/common/routes.py +31 -31
- iatoolkit/common/session_manager.py +0 -1
- iatoolkit/common/util.py +0 -21
- iatoolkit/iatoolkit.py +5 -18
- iatoolkit/infra/llm_client.py +3 -5
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/repositories/models.py +1 -2
- iatoolkit/services/auth_service.py +74 -0
- iatoolkit/services/dispatcher_service.py +12 -21
- iatoolkit/services/excel_service.py +15 -15
- iatoolkit/services/history_service.py +2 -11
- iatoolkit/services/profile_service.py +83 -25
- iatoolkit/services/query_service.py +132 -82
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +3 -6
- iatoolkit/services/user_session_context_service.py +112 -54
- iatoolkit/static/js/chat_feedback.js +1 -1
- iatoolkit/static/js/chat_history.js +1 -5
- iatoolkit/static/js/chat_main.js +1 -1
- iatoolkit/static/styles/landing_page.css +62 -2
- iatoolkit/system_prompts/query_main.prompt +3 -12
- iatoolkit/templates/_login_widget.html +6 -8
- iatoolkit/templates/chat.html +78 -4
- iatoolkit/templates/error.html +1 -1
- iatoolkit/templates/index.html +38 -11
- iatoolkit/templates/{home.html → login_test.html} +11 -51
- iatoolkit/views/external_login_view.py +50 -111
- iatoolkit/views/{file_store_view.py → file_store_api_view.py} +4 -4
- iatoolkit/views/history_api_view.py +52 -0
- iatoolkit/views/init_context_api_view.py +62 -0
- iatoolkit/views/llmquery_api_view.py +50 -0
- iatoolkit/views/llmquery_web_view.py +38 -0
- iatoolkit/views/{home_view.py → login_test_view.py} +2 -5
- iatoolkit/views/login_view.py +79 -56
- iatoolkit/views/{prompt_view.py → prompt_api_view.py} +4 -4
- iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -19
- {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.2.dist-info}/METADATA +2 -2
- {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.2.dist-info}/RECORD +40 -41
- iatoolkit/common/auth.py +0 -200
- iatoolkit/templates/login.html +0 -43
- iatoolkit/views/download_file_view.py +0 -58
- iatoolkit/views/history_view.py +0 -57
- iatoolkit/views/init_context_view.py +0 -35
- iatoolkit/views/llmquery_view.py +0 -65
- {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.22.1.dist-info → iatoolkit-0.50.2.dist-info}/top_level.txt +0 -0
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
|
|
6
6
|
from injector import inject
|
|
7
7
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
8
|
+
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
8
9
|
from iatoolkit.repositories.models import User, Company, ApiKey
|
|
9
10
|
from flask_bcrypt import check_password_hash
|
|
10
11
|
from iatoolkit.common.session_manager import SessionManager
|
|
12
|
+
from iatoolkit.services.user_session_context_service import UserSessionContextService
|
|
11
13
|
from flask_bcrypt import Bcrypt
|
|
12
14
|
from iatoolkit.infra.mail_app import MailApp
|
|
13
15
|
import random
|
|
14
|
-
import logging
|
|
15
16
|
import re
|
|
16
17
|
import secrets
|
|
17
18
|
import string
|
|
18
|
-
from
|
|
19
|
-
from iatoolkit.services.user_session_context_service import UserSessionContextService
|
|
19
|
+
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ProfileService:
|
|
@@ -24,8 +24,10 @@ class ProfileService:
|
|
|
24
24
|
def __init__(self,
|
|
25
25
|
profile_repo: ProfileRepo,
|
|
26
26
|
session_context_service: UserSessionContextService,
|
|
27
|
+
dispatcher: Dispatcher,
|
|
27
28
|
mail_app: MailApp):
|
|
28
29
|
self.profile_repo = profile_repo
|
|
30
|
+
self.dispatcher = dispatcher
|
|
29
31
|
self.session_context = session_context_service
|
|
30
32
|
self.mail_app = mail_app
|
|
31
33
|
self.bcrypt = Bcrypt()
|
|
@@ -42,7 +44,7 @@ class ProfileService:
|
|
|
42
44
|
if not check_password_hash(user.password, password):
|
|
43
45
|
return {'success': False, "message": "Contraseña inválida"}
|
|
44
46
|
|
|
45
|
-
company = self.get_company_by_short_name(company_short_name)
|
|
47
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
46
48
|
if not company:
|
|
47
49
|
return {'success': False, "message": "Empresa no encontrada"}
|
|
48
50
|
|
|
@@ -54,34 +56,87 @@ class ProfileService:
|
|
|
54
56
|
return {'success': False,
|
|
55
57
|
"message": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
|
|
56
58
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
# 1. Build the local user profile dictionary here.
|
|
60
|
+
# the user_profile variables are used on the LLM templates also (see in query_main.prompt)
|
|
61
|
+
user_identifier = user.email # no longer de ID
|
|
62
|
+
user_profile = {
|
|
63
|
+
"id": user_identifier,
|
|
64
|
+
"user_email": user.email,
|
|
65
|
+
"user_fullname": f'{user.first_name} {user.last_name}',
|
|
66
|
+
"user_is_local": True,
|
|
67
|
+
"extras": {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# 2. Call the session creation helper with the pre-built profile.
|
|
71
|
+
# user_identifier = str(user.id)
|
|
72
|
+
self.create_web_session(company, user_identifier, user_profile)
|
|
73
|
+
return {'success': True, "user_identifier": user_identifier, "message": "Login exitoso"}
|
|
61
74
|
except Exception as e:
|
|
62
75
|
return {'success': False, "message": str(e)}
|
|
63
76
|
|
|
77
|
+
def create_external_user_session(self, company: Company, external_user_id: str):
|
|
78
|
+
"""
|
|
79
|
+
Public method for views to create a web session for an external user.
|
|
80
|
+
"""
|
|
81
|
+
# 1. Fetch the profile from the external system via Dispatcher.
|
|
82
|
+
user_profile = self.dispatcher.get_user_info(
|
|
83
|
+
company_name=company.short_name,
|
|
84
|
+
user_identifier=external_user_id
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# 2. Call the session creation helper with external_user_id as user_identifier
|
|
88
|
+
self.create_web_session(
|
|
89
|
+
company=company,
|
|
90
|
+
user_identifier=external_user_id,
|
|
91
|
+
user_profile=user_profile)
|
|
92
|
+
|
|
93
|
+
def create_web_session(self, company: Company, user_identifier: str, user_profile: dict):
|
|
94
|
+
"""
|
|
95
|
+
Private helper: Takes a pre-built profile, saves it to Redis, and sets the Flask cookie.
|
|
96
|
+
"""
|
|
97
|
+
user_profile['company_short_name'] = company.short_name
|
|
98
|
+
user_profile['user_identifier'] = user_identifier
|
|
99
|
+
user_profile['user_id'] = user_identifier
|
|
100
|
+
user_profile['company_id'] = company.id
|
|
101
|
+
user_profile['company'] = company.name
|
|
102
|
+
|
|
103
|
+
# user_profile['last_activity'] = datetime.now(timezone.utc).timestamp()
|
|
104
|
+
self.session_context.save_profile_data(company.short_name, user_identifier, user_profile)
|
|
64
105
|
|
|
65
|
-
|
|
66
|
-
SessionManager.set('
|
|
67
|
-
SessionManager.set('company_id', company.id)
|
|
106
|
+
# save this values into Flask session cookie
|
|
107
|
+
SessionManager.set('user_identifier', user_identifier)
|
|
68
108
|
SessionManager.set('company_short_name', company.short_name)
|
|
69
109
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
110
|
+
def get_current_session_info(self) -> dict:
|
|
111
|
+
"""
|
|
112
|
+
Gets the current web user's profile from the unified session.
|
|
113
|
+
This is the standard way to access user data for web requests.
|
|
114
|
+
"""
|
|
115
|
+
# 1. Get identifiers from the simple Flask session cookie.
|
|
116
|
+
user_identifier = SessionManager.get('user_identifier')
|
|
117
|
+
company_short_name = SessionManager.get('company_short_name')
|
|
118
|
+
|
|
119
|
+
if not user_identifier or not company_short_name:
|
|
120
|
+
# No authenticated web user.
|
|
121
|
+
return {}
|
|
122
|
+
|
|
123
|
+
# 2. Use the identifiers to fetch the full, authoritative profile from Redis.
|
|
124
|
+
profile = self.session_context.get_profile_data(company_short_name, user_identifier)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"user_identifier": user_identifier,
|
|
128
|
+
"company_short_name": company_short_name,
|
|
129
|
+
"profile": profile
|
|
80
130
|
}
|
|
81
|
-
SessionManager.set('user', user_data)
|
|
82
131
|
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
|
|
133
|
+
"""
|
|
134
|
+
Fetches a user profile directly by their identifier, bypassing the Flask session.
|
|
135
|
+
This is ideal for API-side checks.
|
|
136
|
+
"""
|
|
137
|
+
if not company_short_name or not user_identifier:
|
|
138
|
+
return {}
|
|
139
|
+
return self.session_context.get_profile_data(company_short_name, user_identifier)
|
|
85
140
|
|
|
86
141
|
|
|
87
142
|
def signup(self,
|
|
@@ -95,7 +150,7 @@ class ProfileService:
|
|
|
95
150
|
try:
|
|
96
151
|
|
|
97
152
|
# get company info
|
|
98
|
-
company = self.get_company_by_short_name(company_short_name)
|
|
153
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
99
154
|
if not company:
|
|
100
155
|
return {"error": f"la empresa {company_short_name} no existe"}
|
|
101
156
|
|
|
@@ -240,6 +295,9 @@ class ProfileService:
|
|
|
240
295
|
def get_company_by_short_name(self, short_name: str) -> Company:
|
|
241
296
|
return self.profile_repo.get_company_by_short_name(short_name)
|
|
242
297
|
|
|
298
|
+
def get_active_api_key_entry(self, api_key_value: str) -> ApiKey | None:
|
|
299
|
+
return self.profile_repo.get_active_api_key_entry(api_key_value)
|
|
300
|
+
|
|
243
301
|
def new_api_key(self, company_short_name: str):
|
|
244
302
|
company = self.get_company_by_short_name(company_short_name)
|
|
245
303
|
if not company:
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from iatoolkit.infra.llm_client import llmClient
|
|
7
|
+
from iatoolkit.services.profile_service import ProfileService
|
|
7
8
|
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
8
9
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
10
|
from iatoolkit.services.document_service import DocumentService
|
|
10
11
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
11
|
-
|
|
12
12
|
from iatoolkit.repositories.models import Task
|
|
13
13
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
14
14
|
from iatoolkit.services.prompt_manager_service import PromptService
|
|
@@ -21,6 +21,7 @@ import logging
|
|
|
21
21
|
from typing import Optional
|
|
22
22
|
import json
|
|
23
23
|
import time
|
|
24
|
+
import hashlib
|
|
24
25
|
import os
|
|
25
26
|
|
|
26
27
|
|
|
@@ -30,6 +31,7 @@ class QueryService:
|
|
|
30
31
|
@inject
|
|
31
32
|
def __init__(self,
|
|
32
33
|
llm_client: llmClient,
|
|
34
|
+
profile_service: ProfileService,
|
|
33
35
|
document_service: DocumentService,
|
|
34
36
|
document_repo: DocumentRepo,
|
|
35
37
|
llmquery_repo: LLMQueryRepo,
|
|
@@ -39,6 +41,7 @@ class QueryService:
|
|
|
39
41
|
dispatcher: Dispatcher,
|
|
40
42
|
session_context: UserSessionContextService
|
|
41
43
|
):
|
|
44
|
+
self.profile_service = profile_service
|
|
42
45
|
self.document_service = document_service
|
|
43
46
|
self.document_repo = document_repo
|
|
44
47
|
self.llmquery_repo = llmquery_repo
|
|
@@ -55,107 +58,133 @@ class QueryService:
|
|
|
55
58
|
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
56
59
|
"La variable de entorno 'LLM_MODEL' no está configurada.")
|
|
57
60
|
|
|
61
|
+
def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
|
|
62
|
+
# this method read the user/company context from the database and renders the system prompt
|
|
63
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
64
|
+
if not company:
|
|
65
|
+
return None, None
|
|
66
|
+
|
|
67
|
+
# Get the user profile from the single source of truth.
|
|
68
|
+
user_profile = self.profile_service.get_profile_by_identifier(company_short_name, user_identifier)
|
|
69
|
+
if not user_profile:
|
|
70
|
+
# This might happen if a session exists for a user that was deleted.
|
|
71
|
+
return None, None
|
|
72
|
+
|
|
73
|
+
# render the iatoolkit main system prompt with the company/user information
|
|
74
|
+
system_prompt_template = self.prompt_service.get_system_prompt()
|
|
75
|
+
rendered_system_prompt = self.util.render_prompt_from_string(
|
|
76
|
+
template_string=system_prompt_template,
|
|
77
|
+
question=None,
|
|
78
|
+
client_data=user_profile,
|
|
79
|
+
company=company,
|
|
80
|
+
service_list=self.dispatcher.get_company_services(company)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# get the company context: schemas, database models, .md files
|
|
84
|
+
company_specific_context = self.dispatcher.get_company_context(company_name=company_short_name)
|
|
85
|
+
|
|
86
|
+
# merge context: company + user
|
|
87
|
+
final_system_context = f"{company_specific_context}\n{rendered_system_prompt}"
|
|
88
|
+
|
|
89
|
+
return final_system_context, user_profile
|
|
90
|
+
|
|
91
|
+
def prepare_context(self, company_short_name: str, user_identifier: str) -> dict:
|
|
92
|
+
# prepare the context and decide if it needs to be rebuilt
|
|
93
|
+
# save the generated context in the session context for later use
|
|
94
|
+
if not user_identifier:
|
|
95
|
+
return {'rebuild_needed': True, 'error': 'Invalid user identifier'}
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
local_user_id: int = 0,
|
|
63
|
-
model: str = ''):
|
|
64
|
-
start_time = time.time()
|
|
65
|
-
if not model:
|
|
66
|
-
model = self.model
|
|
97
|
+
# create the company/user context and compute its version
|
|
98
|
+
final_system_context, user_profile = self._build_context_and_profile(
|
|
99
|
+
company_short_name, user_identifier)
|
|
67
100
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_USER,
|
|
72
|
-
"No se pudo resolver el identificador del usuario")
|
|
101
|
+
# save the user information in the session context
|
|
102
|
+
# it's needed for the jinja predefined prompts (filtering)
|
|
103
|
+
self.session_context.save_profile_data(company_short_name, user_identifier, user_profile)
|
|
73
104
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
77
|
-
f"Empresa no encontrada: {company_short_name}")
|
|
105
|
+
# calculate the context version
|
|
106
|
+
current_version = self._compute_context_version_from_string(final_system_context)
|
|
78
107
|
|
|
79
|
-
logging.info(f"Inicializando contexto para {company_short_name}/{user_identifier} con modelo {model} ...")
|
|
80
108
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
user_identifier=user_identifier
|
|
85
|
-
)
|
|
109
|
+
prev_version = self.session_context.get_context_version(company_short_name, user_identifier)
|
|
110
|
+
except Exception:
|
|
111
|
+
prev_version = None
|
|
86
112
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
user_profile = self.dispatcher.get_user_info(
|
|
90
|
-
company_name=company_short_name,
|
|
91
|
-
user_identifier=user_identifier,
|
|
92
|
-
is_local_user=is_local_user
|
|
93
|
-
)
|
|
113
|
+
rebuild_is_needed = not (prev_version and prev_version == current_version and
|
|
114
|
+
self._has_valid_cached_context(company_short_name, user_identifier))
|
|
94
115
|
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
if rebuild_is_needed:
|
|
117
|
+
logging.info(
|
|
118
|
+
f"Se necesita reconstrucción de contexto para {company_short_name}/{user_identifier}. Preparando...")
|
|
97
119
|
|
|
98
|
-
# save the user information in the session context
|
|
99
|
-
# it's needed for the jinja predefined prompts (filtering)
|
|
100
|
-
self.session_context.save_user_session_data(company_short_name, user_identifier, user_profile)
|
|
101
120
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
# Guardar el contexto preparado y su versión para que `finalize_context_rebuild` los use.
|
|
122
|
+
self.session_context.save_prepared_context(company_short_name, user_identifier, final_system_context,
|
|
123
|
+
current_version)
|
|
124
|
+
|
|
125
|
+
return {'rebuild_needed': rebuild_is_needed}
|
|
126
|
+
|
|
127
|
+
def finalize_context_rebuild(self, company_short_name: str, user_identifier: str, model: str = ''):
|
|
128
|
+
|
|
129
|
+
# end the initilization, if there is a prepare context send it to llm
|
|
130
|
+
if not model:
|
|
131
|
+
model = self.model
|
|
111
132
|
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
# --- Lógica de Bloqueo ---
|
|
134
|
+
lock_key = f"lock:context:{company_short_name}/{user_identifier}"
|
|
135
|
+
if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
|
|
136
|
+
logging.warning(
|
|
137
|
+
f"Intento de reconstruir contexto para {user_identifier} mientras ya estaba en progreso. Se omite.")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
start_time = time.time()
|
|
142
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
143
|
+
|
|
144
|
+
# get the prepared context and version from the session cache
|
|
145
|
+
prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name,
|
|
146
|
+
user_identifier)
|
|
147
|
+
if not prepared_context:
|
|
148
|
+
logging.info(
|
|
149
|
+
f"No se requiere reconstrucción de contexto para {company_short_name}/{user_identifier}. Finalización rápida.")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
logging.info(f"Enviando contexto al LLM para {company_short_name}/{user_identifier}...")
|
|
114
153
|
|
|
115
|
-
#
|
|
116
|
-
|
|
154
|
+
# Limpiar solo el historial de chat y el ID de respuesta anterior
|
|
155
|
+
self.session_context.clear_llm_history(company_short_name, user_identifier)
|
|
117
156
|
|
|
118
157
|
if self.util.is_gemini_model(model):
|
|
119
|
-
|
|
120
|
-
context_history = [{"role": "user", "content": final_system_context}]
|
|
158
|
+
context_history = [{"role": "user", "content": prepared_context}]
|
|
121
159
|
self.session_context.save_context_history(company_short_name, user_identifier, context_history)
|
|
122
|
-
logging.info(f"Contexto inicial para Gemini guardado en sesión")
|
|
123
|
-
return "gemini-context-initialized"
|
|
124
160
|
|
|
125
161
|
elif self.util.is_openai_model(model):
|
|
126
|
-
|
|
127
|
-
# 6. set the company/user context as the initial context for the LLM
|
|
128
162
|
response_id = self.llm_client.set_company_context(
|
|
129
|
-
company=company,
|
|
130
|
-
company_base_context=final_system_context,
|
|
131
|
-
model=model
|
|
163
|
+
company=company, company_base_context=prepared_context, model=model
|
|
132
164
|
)
|
|
133
|
-
|
|
134
|
-
# 7. save response_id in the session context
|
|
135
165
|
self.session_context.save_last_response_id(company_short_name, user_identifier, response_id)
|
|
136
166
|
|
|
137
|
-
|
|
138
|
-
|
|
167
|
+
if version_to_save:
|
|
168
|
+
self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
|
|
139
169
|
|
|
170
|
+
logging.info(
|
|
171
|
+
f"Contexto de {company_short_name}/{user_identifier} establecido en {int(time.time() - start_time)} seg.")
|
|
140
172
|
except Exception as e:
|
|
141
|
-
logging.exception(f"Error
|
|
173
|
+
logging.exception(f"Error en finalize_context_rebuild para {company_short_name}: {e}")
|
|
142
174
|
raise e
|
|
175
|
+
finally:
|
|
176
|
+
# --- Liberar el Bloqueo ---
|
|
177
|
+
self.session_context.release_lock(lock_key)
|
|
143
178
|
|
|
144
179
|
def llm_query(self,
|
|
145
180
|
company_short_name: str,
|
|
146
|
-
|
|
147
|
-
local_user_id: int = 0,
|
|
181
|
+
user_identifier: str,
|
|
148
182
|
task: Optional[Task] = None,
|
|
149
183
|
prompt_name: str = None,
|
|
150
184
|
question: str = '',
|
|
151
185
|
client_data: dict = {},
|
|
152
186
|
files: list = []) -> dict:
|
|
153
187
|
try:
|
|
154
|
-
user_identifier, is_local_user = self.util.resolve_user_identifier(external_user_id, local_user_id)
|
|
155
|
-
if not user_identifier:
|
|
156
|
-
return {"error": True,
|
|
157
|
-
"error_message": "No se pudo identificar al usuario"}
|
|
158
|
-
|
|
159
188
|
company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
|
|
160
189
|
if not company:
|
|
161
190
|
return {"error": True,
|
|
@@ -173,23 +202,19 @@ class QueryService:
|
|
|
173
202
|
# get user context
|
|
174
203
|
previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
|
|
175
204
|
if not previous_response_id:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return {'error': True,
|
|
180
|
-
"error_message": f"FATAL: No se encontró 'previous_response_id' para '{company.short_name}/{user_identifier}'. La conversación no puede continuar."
|
|
181
|
-
}
|
|
205
|
+
return {'error': True,
|
|
206
|
+
"error_message": f"FATAL: No se encontró 'previous_response_id' para '{company.short_name}/{user_identifier}'. La conversación no puede continuar."
|
|
207
|
+
}
|
|
182
208
|
elif self.util.is_gemini_model(self.model):
|
|
183
209
|
# check the length of the context_history and remove old messages
|
|
184
210
|
self._trim_context_history(context_history)
|
|
185
211
|
|
|
186
|
-
# get the user data from the session context
|
|
187
|
-
|
|
212
|
+
# get the user profile data from the session context
|
|
213
|
+
user_profile = self.profile_service.get_profile_by_identifier(company.short_name, user_identifier)
|
|
188
214
|
|
|
189
|
-
#
|
|
190
|
-
final_client_data = (
|
|
215
|
+
# combine client_data with user_profile
|
|
216
|
+
final_client_data = (user_profile or {}).copy()
|
|
191
217
|
final_client_data.update(client_data)
|
|
192
|
-
final_client_data['user_id'] = user_identifier
|
|
193
218
|
|
|
194
219
|
# Load attached files into the context
|
|
195
220
|
files_context = self.load_files_for_context(files)
|
|
@@ -207,7 +232,7 @@ class QueryService:
|
|
|
207
232
|
template_string=prompt_content,
|
|
208
233
|
question=question,
|
|
209
234
|
client_data=final_client_data,
|
|
210
|
-
|
|
235
|
+
user_identifier=user_identifier,
|
|
211
236
|
company=company,
|
|
212
237
|
)
|
|
213
238
|
|
|
@@ -254,6 +279,31 @@ class QueryService:
|
|
|
254
279
|
logging.exception(e)
|
|
255
280
|
return {'error': True, "error_message": f"{str(e)}"}
|
|
256
281
|
|
|
282
|
+
def _compute_context_version_from_string(self, final_system_context: str) -> str:
|
|
283
|
+
# returns a hash of the context string
|
|
284
|
+
try:
|
|
285
|
+
return hashlib.sha256(final_system_context.encode("utf-8")).hexdigest()
|
|
286
|
+
except Exception:
|
|
287
|
+
return "unknown"
|
|
288
|
+
|
|
289
|
+
def _has_valid_cached_context(self, company_short_name: str, user_identifier: str) -> bool:
|
|
290
|
+
"""
|
|
291
|
+
Verifica si existe un estado de contexto reutilizable en sesión.
|
|
292
|
+
- OpenAI: last_response_id presente.
|
|
293
|
+
- Gemini: context_history con al menos 1 mensaje.
|
|
294
|
+
"""
|
|
295
|
+
try:
|
|
296
|
+
if self.util.is_openai_model(self.model):
|
|
297
|
+
prev_id = self.session_context.get_last_response_id(company_short_name, user_identifier)
|
|
298
|
+
return bool(prev_id)
|
|
299
|
+
if self.util.is_gemini_model(self.model):
|
|
300
|
+
history = self.session_context.get_context_history(company_short_name, user_identifier) or []
|
|
301
|
+
return len(history) >= 1
|
|
302
|
+
return False
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logging.warning(f"Error verificando caché de contexto: {e}")
|
|
305
|
+
return False
|
|
306
|
+
|
|
257
307
|
def load_files_for_context(self, files: list) -> str:
|
|
258
308
|
"""
|
|
259
309
|
Processes a list of attached files, decodes their content,
|
|
@@ -101,7 +101,7 @@ class TaskService:
|
|
|
101
101
|
# call the IA
|
|
102
102
|
response = self.query_service.llm_query(
|
|
103
103
|
task=task,
|
|
104
|
-
|
|
104
|
+
user_identifier='task-monitor',
|
|
105
105
|
company_short_name=task.company.short_name,
|
|
106
106
|
prompt_name=task.task_type.name,
|
|
107
107
|
client_data=task.client_data,
|
|
@@ -19,8 +19,7 @@ class UserFeedbackService:
|
|
|
19
19
|
def new_feedback(self,
|
|
20
20
|
company_short_name: str,
|
|
21
21
|
message: str,
|
|
22
|
-
|
|
23
|
-
local_user_id: int = 0,
|
|
22
|
+
user_identifier: str,
|
|
24
23
|
space: str = None,
|
|
25
24
|
type: str = None,
|
|
26
25
|
rating: int = None) -> dict:
|
|
@@ -31,7 +30,7 @@ class UserFeedbackService:
|
|
|
31
30
|
return {'error': f'No existe la empresa: {company_short_name}'}
|
|
32
31
|
|
|
33
32
|
# send notification to Google Chat
|
|
34
|
-
chat_message = f"*Nuevo feedback de {company_short_name}*:\n*Usuario:* {
|
|
33
|
+
chat_message = f"*Nuevo feedback de {company_short_name}*:\n*Usuario:* {user_identifier}\n*Mensaje:* {message}\n*Calificación:* {rating}"
|
|
35
34
|
|
|
36
35
|
# TO DO: get the space and type from the input data
|
|
37
36
|
chat_data = {
|
|
@@ -45,7 +44,6 @@ class UserFeedbackService:
|
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
chat_result = self.google_chat_app.send_message(message_data=chat_data)
|
|
48
|
-
|
|
49
47
|
if not chat_result.get('success'):
|
|
50
48
|
logging.warning(f"Error al enviar notificación a Google Chat: {chat_result.get('message')}")
|
|
51
49
|
|
|
@@ -53,8 +51,7 @@ class UserFeedbackService:
|
|
|
53
51
|
new_feedback = UserFeedback(
|
|
54
52
|
company_id=company.id,
|
|
55
53
|
message=message,
|
|
56
|
-
|
|
57
|
-
external_user_id=external_user_id,
|
|
54
|
+
user_identifier=user_identifier,
|
|
58
55
|
rating=rating
|
|
59
56
|
)
|
|
60
57
|
new_feedback = self.profile_repo.save_feedback(new_feedback)
|