iatoolkit 0.91.1__py3-none-any.whl → 1.4.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.
- iatoolkit/__init__.py +6 -4
- iatoolkit/base_company.py +0 -16
- iatoolkit/cli_commands.py +3 -14
- iatoolkit/common/exceptions.py +1 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +38 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +42 -5
- iatoolkit/common/util.py +11 -12
- iatoolkit/company_registry.py +5 -0
- iatoolkit/core.py +51 -20
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
- iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
- iatoolkit/infra/llm_proxy.py +235 -134
- iatoolkit/infra/llm_response.py +5 -0
- iatoolkit/locales/en.yaml +124 -2
- iatoolkit/locales/es.yaml +122 -0
- iatoolkit/repositories/database_manager.py +44 -19
- iatoolkit/repositories/document_repo.py +7 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +2 -0
- iatoolkit/repositories/models.py +72 -79
- iatoolkit/repositories/profile_repo.py +59 -3
- iatoolkit/repositories/vs_repo.py +22 -24
- iatoolkit/services/company_context_service.py +88 -39
- iatoolkit/services/configuration_service.py +157 -68
- iatoolkit/services/dispatcher_service.py +21 -3
- iatoolkit/services/file_processor_service.py +0 -5
- iatoolkit/services/history_manager_service.py +43 -24
- iatoolkit/services/knowledge_base_service.py +412 -0
- iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
- iatoolkit/services/load_documents_service.py +18 -47
- iatoolkit/services/profile_service.py +32 -4
- iatoolkit/services/prompt_service.py +32 -30
- iatoolkit/services/query_service.py +51 -26
- iatoolkit/services/sql_service.py +105 -74
- iatoolkit/services/tool_service.py +26 -11
- iatoolkit/services/user_session_context_service.py +115 -63
- iatoolkit/static/js/chat_main.js +44 -4
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +1 -1
- iatoolkit/static/js/chat_reload_button.js +4 -1
- iatoolkit/static/styles/chat_iatoolkit.css +58 -2
- iatoolkit/static/styles/llm_output.css +34 -1
- iatoolkit/system_prompts/query_main.prompt +26 -2
- iatoolkit/templates/base.html +13 -0
- iatoolkit/templates/chat.html +44 -2
- iatoolkit/templates/onboarding_shell.html +0 -1
- iatoolkit/views/base_login_view.py +7 -2
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +14 -10
- iatoolkit/views/login_view.py +8 -3
- iatoolkit/views/rag_api_view.py +216 -0
- iatoolkit/views/users_api_view.py +33 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/METADATA +4 -4
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/RECORD +64 -56
- iatoolkit/repositories/tasks_repo.py +0 -52
- iatoolkit/services/search_service.py +0 -55
- iatoolkit/services/tasks_service.py +0 -188
- iatoolkit/views/tasks_api_view.py +0 -72
- iatoolkit/views/tasks_review_api_view.py +0 -55
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/top_level.txt +0 -0
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
#
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
+
from iatoolkit.common.interfaces.database_provider import DatabaseProvider
|
|
6
7
|
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
7
|
-
from iatoolkit.common.util import Utility
|
|
8
8
|
from iatoolkit.services.i18n_service import I18nService
|
|
9
9
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
10
|
-
from
|
|
11
|
-
from sqlalchemy.exc import SQLAlchemyError
|
|
10
|
+
from iatoolkit.common.util import Utility
|
|
12
11
|
from injector import inject, singleton
|
|
12
|
+
from typing import Callable
|
|
13
13
|
import json
|
|
14
14
|
import logging
|
|
15
15
|
|
|
@@ -28,91 +28,124 @@ class SqlService:
|
|
|
28
28
|
self.util = util
|
|
29
29
|
self.i18n_service = i18n_service
|
|
30
30
|
|
|
31
|
-
# Cache for database
|
|
32
|
-
|
|
31
|
+
# Cache for database providers. Key is tuple: (company_short_name, db_name)
|
|
32
|
+
# Value is the abstract interface DatabaseProvider
|
|
33
|
+
self._db_connections: dict[tuple[str, str], DatabaseProvider] = {}
|
|
34
|
+
|
|
35
|
+
# cache for database schemas. Key is tuple: (company_short_name, db_name)
|
|
36
|
+
self._db_schemas: dict[tuple[str, str], str] = {}
|
|
37
|
+
|
|
38
|
+
# Registry of factory functions.
|
|
39
|
+
# Format: {'connection_type': function(config_dict) -> DatabaseProvider}
|
|
40
|
+
self._provider_factories: dict[str, Callable[[dict], DatabaseProvider]] = {}
|
|
41
|
+
|
|
42
|
+
# Register the default 'direct' strategy (SQLAlchemy)
|
|
43
|
+
self.register_provider_factory('direct', self._create_direct_connection)
|
|
44
|
+
|
|
45
|
+
def register_provider_factory(self, connection_type: str, factory: Callable[[dict], DatabaseProvider]):
|
|
46
|
+
"""
|
|
47
|
+
Allows plugins (Enterprise) to register new connection types.
|
|
48
|
+
"""
|
|
49
|
+
self._provider_factories[connection_type] = factory
|
|
50
|
+
|
|
51
|
+
def _create_direct_connection(self, config: dict) -> DatabaseProvider:
|
|
52
|
+
"""Default factory for standard SQLAlchemy connections."""
|
|
53
|
+
uri = config.get('db_uri') or config.get('DATABASE_URI')
|
|
54
|
+
schema = config.get('schema')
|
|
55
|
+
if not uri:
|
|
56
|
+
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
57
|
+
"Missing db_uri for direct connection")
|
|
58
|
+
return DatabaseManager(uri, schema=schema, register_pgvector=False)
|
|
33
59
|
|
|
34
|
-
def register_database(self,
|
|
60
|
+
def register_database(self, company_short_name: str, db_name: str, config: dict):
|
|
35
61
|
"""
|
|
36
|
-
Creates and caches a
|
|
37
|
-
If a database with the same name is already registered, it does nothing.
|
|
62
|
+
Creates and caches a DatabaseProvider instance based on the configuration.
|
|
38
63
|
"""
|
|
39
|
-
|
|
64
|
+
key = (company_short_name, db_name)
|
|
65
|
+
|
|
66
|
+
# Determine connection type (default to 'direct')
|
|
67
|
+
conn_type = config.get('connection_type', 'direct')
|
|
68
|
+
logging.info(f"Registering DB '{db_name}' ({conn_type}) for company '{company_short_name}'")
|
|
69
|
+
|
|
70
|
+
factory = self._provider_factories.get(conn_type)
|
|
71
|
+
if not factory:
|
|
72
|
+
logging.error(f"Unknown connection type '{conn_type}' for DB '{db_name}'. Skipping.")
|
|
40
73
|
return
|
|
41
74
|
|
|
42
|
-
|
|
75
|
+
try:
|
|
76
|
+
# Create the provider using the appropriate factory
|
|
77
|
+
provider_instance = factory(config)
|
|
78
|
+
self._db_connections[key] = provider_instance
|
|
79
|
+
|
|
80
|
+
# save the db_schema
|
|
81
|
+
self._db_schemas[key] = config.get('schema', 'public')
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logging.error(f"Failed to register DB '{db_name}': {e}")
|
|
84
|
+
# We don't raise here to allow other DBs to load if one fails
|
|
43
85
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
86
|
+
def get_db_names(self, company_short_name: str) -> list[str]:
|
|
87
|
+
"""
|
|
88
|
+
Returns list of logical database names available ONLY for the specified company.
|
|
89
|
+
"""
|
|
90
|
+
return [db for (co, db) in self._db_connections.keys() if co == company_short_name]
|
|
47
91
|
|
|
48
|
-
def
|
|
92
|
+
def get_database_provider(self, company_short_name: str, db_name: str) -> DatabaseProvider:
|
|
49
93
|
"""
|
|
50
|
-
Retrieves a registered
|
|
94
|
+
Retrieves a registered DatabaseProvider instance using the composite key.
|
|
95
|
+
Replaces the old 'get_database_manager'.
|
|
51
96
|
"""
|
|
97
|
+
key = (company_short_name, db_name)
|
|
52
98
|
try:
|
|
53
|
-
return self._db_connections[
|
|
99
|
+
return self._db_connections[key]
|
|
54
100
|
except KeyError:
|
|
55
|
-
logging.error(
|
|
101
|
+
logging.error(
|
|
102
|
+
f"Attempted to access unregistered database: '{db_name}' for company '{company_short_name}'"
|
|
103
|
+
)
|
|
56
104
|
raise IAToolkitException(
|
|
57
105
|
IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
58
|
-
f"Database '{db_name}' is not registered
|
|
106
|
+
f"Database '{db_name}' is not registered for this company."
|
|
59
107
|
)
|
|
60
108
|
|
|
61
|
-
def exec_sql(self, company_short_name: str,
|
|
62
|
-
database: str,
|
|
63
|
-
query: str,
|
|
64
|
-
format: str = 'json',
|
|
65
|
-
commit: bool = False):
|
|
109
|
+
def exec_sql(self, company_short_name: str, **kwargs):
|
|
66
110
|
"""
|
|
67
|
-
Executes a raw SQL statement against a registered database.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
company_short_name: The company identifier (for logging/context).
|
|
71
|
-
database: The logical name of the database to query.
|
|
72
|
-
query: The SQL statement to execute.
|
|
73
|
-
format: The output format ('json' or 'dict'). Only relevant for SELECT queries.
|
|
74
|
-
commit: Whether to commit the transaction immediately after execution.
|
|
75
|
-
Use True for INSERT/UPDATE/DELETE statements.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
- A JSON string or list of dicts for SELECT queries.
|
|
79
|
-
- A dictionary {'rowcount': N} for non-returning statements (INSERT/UPDATE) if not using RETURNING.
|
|
111
|
+
Executes a raw SQL statement against a registered database provider.
|
|
112
|
+
Delegates the actual execution details to the provider implementation.
|
|
80
113
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# 2. Execute the SQL statement
|
|
87
|
-
result = session.execute(text(query))
|
|
114
|
+
database_name = kwargs.get('database_key')
|
|
115
|
+
query = kwargs.get('query')
|
|
116
|
+
format = kwargs.get('format', 'json')
|
|
117
|
+
commit = kwargs.get('commit')
|
|
88
118
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
if not database_name:
|
|
120
|
+
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
121
|
+
'missing database_name in call to exec_sql')
|
|
92
122
|
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
rows_context = [dict(zip(cols, row)) for row in result.fetchall()]
|
|
123
|
+
try:
|
|
124
|
+
# 1. Get the abstract provider (could be Direct or Bridge)
|
|
125
|
+
provider = self.get_database_provider(company_short_name, database_name)
|
|
126
|
+
db_schema = self._db_schemas[(company_short_name, database_name)]
|
|
98
127
|
|
|
99
|
-
|
|
100
|
-
|
|
128
|
+
# 2. Delegate execution
|
|
129
|
+
# The provider returns a clean List[Dict] or Dict result
|
|
130
|
+
result_data = provider.execute_query(query=query, commit=commit)
|
|
101
131
|
|
|
102
|
-
|
|
103
|
-
|
|
132
|
+
# 3. Handle Formatting (Service layer responsibility)
|
|
133
|
+
if format == 'dict':
|
|
134
|
+
return result_data
|
|
104
135
|
|
|
105
|
-
#
|
|
106
|
-
return
|
|
136
|
+
# Serialize the result
|
|
137
|
+
return json.dumps(result_data, default=self.util.serialize)
|
|
107
138
|
|
|
108
139
|
except IAToolkitException:
|
|
109
|
-
# Re-raise exceptions from get_database_manager to preserve the specific error
|
|
110
140
|
raise
|
|
111
141
|
except Exception as e:
|
|
112
|
-
# Attempt
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
# Attempt rollback if supported/needed
|
|
143
|
+
try:
|
|
144
|
+
provider = self.get_database_provider(company_short_name, database_name)
|
|
145
|
+
if provider:
|
|
146
|
+
provider.rollback()
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
116
149
|
|
|
117
150
|
error_message = str(e)
|
|
118
151
|
if 'timed out' in str(e):
|
|
@@ -122,22 +155,20 @@ class SqlService:
|
|
|
122
155
|
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
123
156
|
error_message) from e
|
|
124
157
|
|
|
125
|
-
def commit(self,
|
|
158
|
+
def commit(self, company_short_name: str, database_name: str):
|
|
126
159
|
"""
|
|
127
|
-
Commits the current transaction for a registered database.
|
|
128
|
-
Useful when multiple exec_sql calls are part of a single transaction.
|
|
160
|
+
Commits the current transaction for a registered database provider.
|
|
129
161
|
"""
|
|
130
|
-
|
|
131
|
-
# Get the database manager from the cache
|
|
132
|
-
db_manager = self.get_database_manager(database)
|
|
162
|
+
provider = self.get_database_provider(company_short_name, database_name)
|
|
133
163
|
try:
|
|
134
|
-
|
|
135
|
-
except SQLAlchemyError as db_error:
|
|
136
|
-
db_manager.get_session().rollback()
|
|
137
|
-
logging.error(f"Error de base de datos: {str(db_error)}")
|
|
138
|
-
raise db_error
|
|
164
|
+
provider.commit()
|
|
139
165
|
except Exception as e:
|
|
140
|
-
|
|
166
|
+
# Try rollback
|
|
167
|
+
try:
|
|
168
|
+
provider.rollback()
|
|
169
|
+
except:
|
|
170
|
+
pass
|
|
171
|
+
logging.error(f"Error while committing sql: '{str(e)}'")
|
|
141
172
|
raise IAToolkitException(
|
|
142
173
|
IAToolkitException.ErrorType.DATABASE_ERROR, str(e)
|
|
143
174
|
)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
from injector import inject
|
|
7
7
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
8
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
8
9
|
from iatoolkit.repositories.models import Company, Tool
|
|
9
10
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
10
11
|
from iatoolkit.services.sql_service import SqlService
|
|
@@ -98,20 +99,20 @@ _SYSTEM_TOOLS = [
|
|
|
98
99
|
},
|
|
99
100
|
{
|
|
100
101
|
"function_name": "iat_sql_query",
|
|
101
|
-
"description": "Servicio SQL de IAToolkit: debes utilizar este servicio para todas las consultas a
|
|
102
|
+
"description": "Servicio SQL de IAToolkit: debes utilizar este servicio para todas las consultas SQL a bases de datos.",
|
|
102
103
|
"parameters": {
|
|
103
104
|
"type": "object",
|
|
104
105
|
"properties": {
|
|
105
|
-
"
|
|
106
|
+
"database_key": {
|
|
106
107
|
"type": "string",
|
|
107
|
-
"description": "nombre de la base de datos a consultar
|
|
108
|
+
"description": "IMPORTANT: nombre de la base de datos a consultar."
|
|
108
109
|
},
|
|
109
110
|
"query": {
|
|
110
111
|
"type": "string",
|
|
111
112
|
"description": "string con la consulta en sql"
|
|
112
113
|
},
|
|
113
114
|
},
|
|
114
|
-
"required": ["
|
|
115
|
+
"required": ["database_key", "query"]
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
]
|
|
@@ -121,10 +122,12 @@ class ToolService:
|
|
|
121
122
|
@inject
|
|
122
123
|
def __init__(self,
|
|
123
124
|
llm_query_repo: LLMQueryRepo,
|
|
125
|
+
profile_repo: ProfileRepo,
|
|
124
126
|
sql_service: SqlService,
|
|
125
127
|
excel_service: ExcelService,
|
|
126
128
|
mail_service: MailService):
|
|
127
129
|
self.llm_query_repo = llm_query_repo
|
|
130
|
+
self.profile_repo = profile_repo
|
|
128
131
|
self.sql_service = sql_service
|
|
129
132
|
self.excel_service = excel_service
|
|
130
133
|
self.mail_service = mail_service
|
|
@@ -158,14 +161,22 @@ class ToolService:
|
|
|
158
161
|
self.llm_query_repo.rollback()
|
|
159
162
|
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))
|
|
160
163
|
|
|
161
|
-
def sync_company_tools(self,
|
|
164
|
+
def sync_company_tools(self, company_short_name: str, tools_config: list):
|
|
162
165
|
"""
|
|
163
166
|
Synchronizes tools from YAML config to Database (Create/Update/Delete strategy).
|
|
164
167
|
"""
|
|
168
|
+
if not tools_config:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
172
|
+
if not company:
|
|
173
|
+
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
174
|
+
f'Company {company_short_name} not found')
|
|
175
|
+
|
|
165
176
|
try:
|
|
166
177
|
# 1. Get existing tools map for later cleanup
|
|
167
178
|
existing_tools = {
|
|
168
|
-
f.name: f for f in self.llm_query_repo.get_company_tools(
|
|
179
|
+
f.name: f for f in self.llm_query_repo.get_company_tools(company)
|
|
169
180
|
}
|
|
170
181
|
defined_tool_names = set()
|
|
171
182
|
|
|
@@ -177,7 +188,7 @@ class ToolService:
|
|
|
177
188
|
# Construct the tool object with current config values
|
|
178
189
|
# We create a new transient object and let the repo merge it
|
|
179
190
|
tool_obj = Tool(
|
|
180
|
-
company_id=
|
|
191
|
+
company_id=company.id,
|
|
181
192
|
name=name,
|
|
182
193
|
description=tool_data['description'],
|
|
183
194
|
parameters=tool_data['params'],
|
|
@@ -206,11 +217,12 @@ class ToolService:
|
|
|
206
217
|
Returns the list of tools (System + Company) formatted for the LLM (OpenAI Schema).
|
|
207
218
|
"""
|
|
208
219
|
tools = []
|
|
209
|
-
# Obtiene tanto las de la empresa como las del sistema (la query del repo debería soportar esto con OR)
|
|
210
|
-
functions = self.llm_query_repo.get_company_tools(company)
|
|
211
220
|
|
|
212
|
-
for
|
|
213
|
-
|
|
221
|
+
# get all the tools for the company and system
|
|
222
|
+
company_tools = self.llm_query_repo.get_company_tools(company)
|
|
223
|
+
|
|
224
|
+
for function in company_tools:
|
|
225
|
+
# clone for no modify the SQLAlchemy session object
|
|
214
226
|
params = function.parameters.copy() if function.parameters else {}
|
|
215
227
|
params["additionalProperties"] = False
|
|
216
228
|
|
|
@@ -221,6 +233,9 @@ class ToolService:
|
|
|
221
233
|
"parameters": params,
|
|
222
234
|
"strict": True
|
|
223
235
|
}
|
|
236
|
+
if function.name == 'iat_sql_query':
|
|
237
|
+
params['properties']['database_key']['enum'] = self.sql_service.get_db_names(company.short_name)
|
|
238
|
+
|
|
224
239
|
tools.append(ai_tool)
|
|
225
240
|
return tools
|
|
226
241
|
|
|
@@ -15,73 +15,101 @@ class UserSessionContextService:
|
|
|
15
15
|
Esto mejora la atomicidad y la eficiencia.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
def _get_session_key(self, company_short_name: str, user_identifier: str) -> Optional[str]:
|
|
18
|
+
def _get_session_key(self, company_short_name: str, user_identifier: str, model: str = None) -> Optional[str]:
|
|
19
19
|
"""Devuelve la clave única de Redis para el Hash de sesión del usuario."""
|
|
20
20
|
user_identifier = (user_identifier or "").strip()
|
|
21
21
|
if not company_short_name or not user_identifier:
|
|
22
22
|
return None
|
|
23
|
-
return f"session:{company_short_name}/{user_identifier}"
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
model_key = "" if not model else f"-{model}"
|
|
25
|
+
return f"session:{company_short_name}/{user_identifier}{model_key}"
|
|
26
|
+
|
|
27
|
+
def clear_all_context(self, company_short_name: str, user_identifier: str, model: str = None):
|
|
28
|
+
"""Clears LLM-related context for a user (history and response IDs), preserving profile_data."""
|
|
29
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
28
30
|
if session_key:
|
|
29
|
-
# RedisSessionManager.remove(session_key)
|
|
30
31
|
# 'profile_data' should not be deleted
|
|
31
|
-
RedisSessionManager.hdel(session_key,
|
|
32
|
-
RedisSessionManager.hdel(session_key,
|
|
33
|
-
RedisSessionManager.hdel(session_key,
|
|
32
|
+
RedisSessionManager.hdel(session_key, "context_version")
|
|
33
|
+
RedisSessionManager.hdel(session_key, "context_history")
|
|
34
|
+
RedisSessionManager.hdel(session_key, "last_response_id")
|
|
34
35
|
|
|
35
|
-
def clear_llm_history(self, company_short_name: str, user_identifier: str):
|
|
36
|
-
"""
|
|
37
|
-
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
36
|
+
def clear_llm_history(self, company_short_name: str, user_identifier: str, model: str = None):
|
|
37
|
+
"""Clears only LLM history fields (last_response_id and context_history)."""
|
|
38
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
38
39
|
if session_key:
|
|
39
|
-
RedisSessionManager.hdel(session_key,
|
|
40
|
+
RedisSessionManager.hdel(session_key, "last_response_id", "context_history")
|
|
40
41
|
|
|
41
|
-
def get_last_response_id(self, company_short_name: str, user_identifier: str) -> Optional[str]:
|
|
42
|
-
|
|
42
|
+
def get_last_response_id(self, company_short_name: str, user_identifier: str, model: str = None) -> Optional[str]:
|
|
43
|
+
"""Returns the last LLM response ID for this user/model combination."""
|
|
44
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
43
45
|
if not session_key:
|
|
44
46
|
return None
|
|
45
|
-
return RedisSessionManager.hget(session_key,
|
|
46
|
-
|
|
47
|
-
def save_last_response_id(self,
|
|
48
|
-
|
|
47
|
+
return RedisSessionManager.hget(session_key, "last_response_id")
|
|
48
|
+
|
|
49
|
+
def save_last_response_id(self,
|
|
50
|
+
company_short_name: str,
|
|
51
|
+
user_identifier: str,
|
|
52
|
+
response_id: str,
|
|
53
|
+
model: str = None,
|
|
54
|
+
):
|
|
55
|
+
"""Persists the last LLM response ID for this user/model combination."""
|
|
56
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
49
57
|
if session_key:
|
|
50
|
-
RedisSessionManager.hset(session_key,
|
|
58
|
+
RedisSessionManager.hset(session_key, "last_response_id", response_id)
|
|
51
59
|
|
|
52
|
-
def get_initial_response_id(self,
|
|
60
|
+
def get_initial_response_id(self,
|
|
61
|
+
company_short_name: str,
|
|
62
|
+
user_identifier: str,
|
|
63
|
+
model: str = None,
|
|
64
|
+
) -> Optional[str]:
|
|
53
65
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
Returns the initial LLM response ID for this user/model combination.
|
|
67
|
+
This ID represents the state right after the context was set on the LLM.
|
|
56
68
|
"""
|
|
57
|
-
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
69
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
58
70
|
if not session_key:
|
|
59
71
|
return None
|
|
60
|
-
return RedisSessionManager.hget(session_key,
|
|
61
|
-
|
|
62
|
-
def save_initial_response_id(self,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
return RedisSessionManager.hget(session_key, "initial_response_id")
|
|
73
|
+
|
|
74
|
+
def save_initial_response_id(self,
|
|
75
|
+
company_short_name: str,
|
|
76
|
+
user_identifier: str,
|
|
77
|
+
response_id: str,
|
|
78
|
+
model: str = None,
|
|
79
|
+
):
|
|
80
|
+
"""Persists the initial LLM response ID for this user/model combination."""
|
|
81
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
67
82
|
if session_key:
|
|
68
|
-
RedisSessionManager.hset(session_key,
|
|
69
|
-
|
|
70
|
-
def save_context_history(
|
|
71
|
-
|
|
83
|
+
RedisSessionManager.hset(session_key, "initial_response_id", response_id)
|
|
84
|
+
|
|
85
|
+
def save_context_history(
|
|
86
|
+
self,
|
|
87
|
+
company_short_name: str,
|
|
88
|
+
user_identifier: str,
|
|
89
|
+
context_history: List[Dict],
|
|
90
|
+
model: str = None,
|
|
91
|
+
):
|
|
92
|
+
"""Serializes and stores the context history for this user/model combination."""
|
|
93
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
72
94
|
if session_key:
|
|
73
95
|
try:
|
|
74
96
|
history_json = json.dumps(context_history)
|
|
75
|
-
RedisSessionManager.hset(session_key,
|
|
97
|
+
RedisSessionManager.hset(session_key, "context_history", history_json)
|
|
76
98
|
except (TypeError, ValueError) as e:
|
|
77
|
-
logging.error(f"Error
|
|
78
|
-
|
|
79
|
-
def get_context_history(
|
|
80
|
-
|
|
99
|
+
logging.error(f"Error serializing context_history for {session_key}: {e}")
|
|
100
|
+
|
|
101
|
+
def get_context_history(
|
|
102
|
+
self,
|
|
103
|
+
company_short_name: str,
|
|
104
|
+
user_identifier: str,
|
|
105
|
+
model: str = None,
|
|
106
|
+
) -> Optional[List[Dict]]:
|
|
107
|
+
"""Reads and deserializes the context history for this user/model combination."""
|
|
108
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
81
109
|
if not session_key:
|
|
82
|
-
return
|
|
110
|
+
return []
|
|
83
111
|
|
|
84
|
-
history_json = RedisSessionManager.hget(session_key,
|
|
112
|
+
history_json = RedisSessionManager.hget(session_key, "context_history")
|
|
85
113
|
if not history_json:
|
|
86
114
|
return []
|
|
87
115
|
|
|
@@ -113,37 +141,61 @@ class UserSessionContextService:
|
|
|
113
141
|
except json.JSONDecodeError:
|
|
114
142
|
return {}
|
|
115
143
|
|
|
116
|
-
def save_context_version(self,
|
|
117
|
-
|
|
144
|
+
def save_context_version(self,
|
|
145
|
+
company_short_name: str,
|
|
146
|
+
user_identifier: str,
|
|
147
|
+
version: str,
|
|
148
|
+
model: str = None,
|
|
149
|
+
):
|
|
150
|
+
"""Saves the context version for this user/model combination."""
|
|
151
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
118
152
|
if session_key:
|
|
119
|
-
RedisSessionManager.hset(session_key,
|
|
120
|
-
|
|
121
|
-
def get_context_version(self,
|
|
122
|
-
|
|
153
|
+
RedisSessionManager.hset(session_key, "context_version", version)
|
|
154
|
+
|
|
155
|
+
def get_context_version(self,
|
|
156
|
+
company_short_name: str,
|
|
157
|
+
user_identifier: str,
|
|
158
|
+
model: str = None,
|
|
159
|
+
) -> Optional[str]:
|
|
160
|
+
"""Returns the context version for this user/model combination."""
|
|
161
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
123
162
|
if not session_key:
|
|
124
163
|
return None
|
|
125
|
-
return RedisSessionManager.hget(session_key,
|
|
126
|
-
|
|
127
|
-
def save_prepared_context(self,
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
return RedisSessionManager.hget(session_key, "context_version")
|
|
165
|
+
|
|
166
|
+
def save_prepared_context(self,
|
|
167
|
+
company_short_name: str,
|
|
168
|
+
user_identifier: str,
|
|
169
|
+
context: str,
|
|
170
|
+
version: str,
|
|
171
|
+
model: str = None,
|
|
172
|
+
):
|
|
173
|
+
"""Stores a pre-rendered system context and its version, ready to be sent to the LLM."""
|
|
174
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
130
175
|
if session_key:
|
|
131
|
-
RedisSessionManager.hset(session_key,
|
|
132
|
-
RedisSessionManager.hset(session_key,
|
|
133
|
-
|
|
134
|
-
def get_and_clear_prepared_context(self,
|
|
135
|
-
|
|
136
|
-
|
|
176
|
+
RedisSessionManager.hset(session_key, "prepared_context", context)
|
|
177
|
+
RedisSessionManager.hset(session_key, "prepared_context_version", version)
|
|
178
|
+
|
|
179
|
+
def get_and_clear_prepared_context(self,
|
|
180
|
+
company_short_name: str,
|
|
181
|
+
user_identifier: str,
|
|
182
|
+
model: str = None,
|
|
183
|
+
) -> tuple:
|
|
184
|
+
"""
|
|
185
|
+
Atomically retrieves the prepared context and its version and then deletes them
|
|
186
|
+
to guarantee they are consumed only once.
|
|
187
|
+
"""
|
|
188
|
+
session_key = self._get_session_key(company_short_name, user_identifier, model=model)
|
|
137
189
|
if not session_key:
|
|
138
190
|
return None, None
|
|
139
191
|
|
|
140
192
|
pipe = RedisSessionManager.pipeline()
|
|
141
|
-
pipe.hget(session_key,
|
|
142
|
-
pipe.hget(session_key,
|
|
143
|
-
pipe.hdel(session_key,
|
|
193
|
+
pipe.hget(session_key, "prepared_context")
|
|
194
|
+
pipe.hget(session_key, "prepared_context_version")
|
|
195
|
+
pipe.hdel(session_key, "prepared_context", "prepared_context_version")
|
|
144
196
|
results = pipe.execute()
|
|
145
197
|
|
|
146
|
-
# results[0]
|
|
198
|
+
# results[0] is the context, results[1] is the version
|
|
147
199
|
return (results[0], results[1]) if results else (None, None)
|
|
148
200
|
|
|
149
201
|
# --- Métodos de Bloqueo ---
|
iatoolkit/static/js/chat_main.js
CHANGED
|
@@ -110,13 +110,48 @@ const handleChatMessage = async function () {
|
|
|
110
110
|
prompt_name: promptName,
|
|
111
111
|
client_data: clientData,
|
|
112
112
|
files: filesBase64.map(f => ({ filename: f.name, content: f.base64 })),
|
|
113
|
-
user_identifier: window.user_identifier
|
|
113
|
+
user_identifier: window.user_identifier,
|
|
114
|
+
model: (window.currentLlmModel || window.defaultLlmModel || '')
|
|
115
|
+
|
|
114
116
|
};
|
|
115
117
|
|
|
116
118
|
const responseData = await callToolkit("/api/llm_query", data, "POST");
|
|
117
119
|
if (responseData && responseData.answer) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
// CAMBIO: contenedor principal para la respuesta del bot
|
|
121
|
+
const botMessageContainer = $('<div>').addClass('bot-message-container');
|
|
122
|
+
|
|
123
|
+
// 1. Si hay reasoning_content, agregar el acordeón colapsable
|
|
124
|
+
if (responseData.reasoning_content) {
|
|
125
|
+
const uniqueId = 'reasoning-' + Date.now(); // ID único para el collapse
|
|
126
|
+
|
|
127
|
+
const reasoningBlock = $(`
|
|
128
|
+
<div class="reasoning-block">
|
|
129
|
+
<button class="reasoning-toggle btn btn-sm btn-link text-decoration-none p-0"
|
|
130
|
+
type="button"
|
|
131
|
+
data-bs-toggle="collapse"
|
|
132
|
+
data-bs-target="#${uniqueId}"
|
|
133
|
+
aria-expanded="false"
|
|
134
|
+
aria-controls="${uniqueId}">
|
|
135
|
+
<i class="bi bi-lightbulb me-1"></i> ${t_js('show_reasoning')}
|
|
136
|
+
</button>
|
|
137
|
+
|
|
138
|
+
<div class="collapse mt-2" id="${uniqueId}">
|
|
139
|
+
<div class="reasoning-card">
|
|
140
|
+
${responseData.reasoning_content}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
`);
|
|
145
|
+
botMessageContainer.append(reasoningBlock);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 2. Agregar la respuesta final
|
|
149
|
+
const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
|
|
150
|
+
botMessageContainer.append(answerSection);
|
|
151
|
+
|
|
152
|
+
// 3. Mostrar el contenedor completo
|
|
153
|
+
displayBotMessage(botMessageContainer);
|
|
154
|
+
|
|
120
155
|
}
|
|
121
156
|
} catch (error) {
|
|
122
157
|
if (error.name === 'AbortError') {
|
|
@@ -207,7 +242,12 @@ const toggleSendStopButtons = function (showStop) {
|
|
|
207
242
|
* @returns {Promise<object|null>} The response data or null on error.
|
|
208
243
|
*/
|
|
209
244
|
const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
|
|
210
|
-
|
|
245
|
+
// normalize the url for avoiding double //
|
|
246
|
+
const base = (window.iatoolkit_base_url || '').replace(/\/+$/, '');
|
|
247
|
+
const company = (window.companyShortName || '').replace(/^\/+|\/+$/g, '');
|
|
248
|
+
const path = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
|
249
|
+
const url = `${base}/${company}${path}`;
|
|
250
|
+
|
|
211
251
|
|
|
212
252
|
abortController = new AbortController();
|
|
213
253
|
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|