iatoolkit 0.71.4__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 +19 -7
- iatoolkit/base_company.py +1 -71
- iatoolkit/cli_commands.py +9 -21
- iatoolkit/common/exceptions.py +2 -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 +53 -32
- iatoolkit/common/util.py +17 -12
- iatoolkit/company_registry.py +55 -14
- iatoolkit/{iatoolkit.py → core.py} +102 -72
- iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
- 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 +134 -4
- iatoolkit/locales/es.yaml +293 -162
- iatoolkit/repositories/database_manager.py +92 -22
- iatoolkit/repositories/document_repo.py +7 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +36 -22
- iatoolkit/repositories/models.py +86 -95
- iatoolkit/repositories/profile_repo.py +64 -13
- iatoolkit/repositories/vs_repo.py +31 -28
- iatoolkit/services/auth_service.py +1 -1
- iatoolkit/services/branding_service.py +1 -1
- iatoolkit/services/company_context_service.py +96 -39
- iatoolkit/services/configuration_service.py +329 -67
- iatoolkit/services/dispatcher_service.py +51 -227
- iatoolkit/services/document_service.py +10 -1
- iatoolkit/services/embedding_service.py +9 -6
- iatoolkit/services/excel_service.py +50 -2
- iatoolkit/services/file_processor_service.py +0 -5
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/jwt_service.py +1 -1
- iatoolkit/services/knowledge_base_service.py +412 -0
- iatoolkit/services/language_service.py +8 -2
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +42 -29
- iatoolkit/services/load_documents_service.py +18 -47
- iatoolkit/services/mail_service.py +171 -25
- iatoolkit/services/profile_service.py +69 -36
- iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +136 -25
- iatoolkit/services/query_service.py +229 -203
- iatoolkit/services/sql_service.py +116 -34
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +18 -6
- iatoolkit/services/user_session_context_service.py +121 -51
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +1 -1
- iatoolkit/static/js/chat_help_content.js +4 -4
- iatoolkit/static/js/chat_main.js +61 -9
- 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 +59 -3
- iatoolkit/static/styles/chat_public.css +28 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +223 -7
- iatoolkit/static/styles/llm_output.css +34 -1
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +28 -3
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +30 -5
- iatoolkit/templates/_login_widget.html +3 -3
- iatoolkit/templates/base.html +13 -0
- iatoolkit/templates/chat.html +45 -3
- iatoolkit/templates/forgot_password.html +3 -2
- iatoolkit/templates/onboarding_shell.html +1 -2
- iatoolkit/templates/signup.html +3 -0
- iatoolkit/views/base_login_view.py +8 -3
- iatoolkit/views/change_password_view.py +1 -1
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/forgot_password_view.py +9 -4
- iatoolkit/views/history_api_view.py +3 -3
- iatoolkit/views/home_view.py +4 -2
- iatoolkit/views/init_context_api_view.py +1 -1
- iatoolkit/views/llmquery_api_view.py +4 -3
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +15 -11
- iatoolkit/views/login_view.py +25 -8
- iatoolkit/views/logout_api_view.py +10 -2
- iatoolkit/views/prompt_api_view.py +1 -1
- iatoolkit/views/rag_api_view.py +216 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +12 -4
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +1 -1
- iatoolkit-1.4.2.dist-info/METADATA +268 -0
- iatoolkit-1.4.2.dist-info/RECORD +133 -0
- iatoolkit-1.4.2.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- iatoolkit/repositories/tasks_repo.py +0 -52
- iatoolkit/services/history_service.py +0 -37
- iatoolkit/services/search_service.py +0 -55
- iatoolkit/services/tasks_service.py +0 -188
- iatoolkit/templates/about.html +0 -13
- iatoolkit/templates/index.html +0 -145
- iatoolkit/templates/login_simulation.html +0 -45
- iatoolkit/views/external_login_view.py +0 -73
- iatoolkit/views/index_view.py +0 -14
- iatoolkit/views/login_simulation_view.py +0 -93
- iatoolkit/views/tasks_api_view.py +0 -72
- iatoolkit/views/tasks_review_api_view.py +0 -55
- iatoolkit-0.71.4.dist-info/METADATA +0 -276
- iatoolkit-0.71.4.dist-info/RECORD +0 -122
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/top_level.txt +0 -0
|
@@ -3,12 +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
|
|
10
|
+
from iatoolkit.common.util import Utility
|
|
11
11
|
from injector import inject, singleton
|
|
12
|
+
from typing import Callable
|
|
12
13
|
import json
|
|
13
14
|
import logging
|
|
14
15
|
|
|
@@ -27,61 +28,124 @@ class SqlService:
|
|
|
27
28
|
self.util = util
|
|
28
29
|
self.i18n_service = i18n_service
|
|
29
30
|
|
|
30
|
-
# Cache for database
|
|
31
|
-
|
|
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)
|
|
32
59
|
|
|
33
|
-
def register_database(self, db_name: str,
|
|
60
|
+
def register_database(self, company_short_name: str, db_name: str, config: dict):
|
|
34
61
|
"""
|
|
35
|
-
Creates and caches a
|
|
36
|
-
If a database with the same name is already registered, it does nothing.
|
|
62
|
+
Creates and caches a DatabaseProvider instance based on the configuration.
|
|
37
63
|
"""
|
|
38
|
-
|
|
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.")
|
|
39
73
|
return
|
|
40
74
|
|
|
41
|
-
|
|
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
|
|
42
85
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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]
|
|
46
91
|
|
|
47
|
-
def
|
|
92
|
+
def get_database_provider(self, company_short_name: str, db_name: str) -> DatabaseProvider:
|
|
48
93
|
"""
|
|
49
|
-
Retrieves a registered
|
|
94
|
+
Retrieves a registered DatabaseProvider instance using the composite key.
|
|
95
|
+
Replaces the old 'get_database_manager'.
|
|
50
96
|
"""
|
|
97
|
+
key = (company_short_name, db_name)
|
|
51
98
|
try:
|
|
52
|
-
return self._db_connections[
|
|
99
|
+
return self._db_connections[key]
|
|
53
100
|
except KeyError:
|
|
54
|
-
logging.error(
|
|
101
|
+
logging.error(
|
|
102
|
+
f"Attempted to access unregistered database: '{db_name}' for company '{company_short_name}'"
|
|
103
|
+
)
|
|
55
104
|
raise IAToolkitException(
|
|
56
105
|
IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
57
|
-
f"Database '{db_name}' is not registered
|
|
106
|
+
f"Database '{db_name}' is not registered for this company."
|
|
58
107
|
)
|
|
59
108
|
|
|
60
|
-
def exec_sql(self,
|
|
109
|
+
def exec_sql(self, company_short_name: str, **kwargs):
|
|
61
110
|
"""
|
|
62
|
-
Executes a raw SQL statement against a registered database
|
|
111
|
+
Executes a raw SQL statement against a registered database provider.
|
|
112
|
+
Delegates the actual execution details to the provider implementation.
|
|
63
113
|
"""
|
|
114
|
+
database_name = kwargs.get('database_key')
|
|
115
|
+
query = kwargs.get('query')
|
|
116
|
+
format = kwargs.get('format', 'json')
|
|
117
|
+
commit = kwargs.get('commit')
|
|
118
|
+
|
|
119
|
+
if not database_name:
|
|
120
|
+
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
121
|
+
'missing database_name in call to exec_sql')
|
|
122
|
+
|
|
64
123
|
try:
|
|
65
|
-
# 1. Get the
|
|
66
|
-
|
|
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)]
|
|
67
127
|
|
|
68
|
-
# 2.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
rows_context = [dict(zip(cols, row)) for row in result.fetchall()]
|
|
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)
|
|
72
131
|
|
|
73
|
-
#
|
|
74
|
-
|
|
132
|
+
# 3. Handle Formatting (Service layer responsibility)
|
|
133
|
+
if format == 'dict':
|
|
134
|
+
return result_data
|
|
135
|
+
|
|
136
|
+
# Serialize the result
|
|
137
|
+
return json.dumps(result_data, default=self.util.serialize)
|
|
75
138
|
|
|
76
|
-
return sql_result_json
|
|
77
139
|
except IAToolkitException:
|
|
78
|
-
# Re-raise exceptions from get_database_manager to preserve the specific error
|
|
79
140
|
raise
|
|
80
141
|
except Exception as e:
|
|
81
|
-
# Attempt
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
85
149
|
|
|
86
150
|
error_message = str(e)
|
|
87
151
|
if 'timed out' in str(e):
|
|
@@ -89,4 +153,22 @@ class SqlService:
|
|
|
89
153
|
|
|
90
154
|
logging.error(f"Error executing SQL statement: {error_message}")
|
|
91
155
|
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
92
|
-
error_message) from e
|
|
156
|
+
error_message) from e
|
|
157
|
+
|
|
158
|
+
def commit(self, company_short_name: str, database_name: str):
|
|
159
|
+
"""
|
|
160
|
+
Commits the current transaction for a registered database provider.
|
|
161
|
+
"""
|
|
162
|
+
provider = self.get_database_provider(company_short_name, database_name)
|
|
163
|
+
try:
|
|
164
|
+
provider.commit()
|
|
165
|
+
except Exception as e:
|
|
166
|
+
# Try rollback
|
|
167
|
+
try:
|
|
168
|
+
provider.rollback()
|
|
169
|
+
except:
|
|
170
|
+
pass
|
|
171
|
+
logging.error(f"Error while committing sql: '{str(e)}'")
|
|
172
|
+
raise IAToolkitException(
|
|
173
|
+
IAToolkitException.ErrorType.DATABASE_ERROR, str(e)
|
|
174
|
+
)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from injector import inject
|
|
7
|
+
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
8
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
|
+
from iatoolkit.repositories.models import Company, Tool
|
|
10
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
11
|
+
from iatoolkit.services.sql_service import SqlService
|
|
12
|
+
from iatoolkit.services.excel_service import ExcelService
|
|
13
|
+
from iatoolkit.services.mail_service import MailService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_SYSTEM_TOOLS = [
|
|
17
|
+
{
|
|
18
|
+
"function_name": "iat_generate_excel",
|
|
19
|
+
"description": "Generador de Excel."
|
|
20
|
+
"Genera un archivo Excel (.xlsx) a partir de una lista de diccionarios. "
|
|
21
|
+
"Cada diccionario representa una fila del archivo. "
|
|
22
|
+
"el archivo se guarda en directorio de descargas."
|
|
23
|
+
"retorna diccionario con filename, attachment_token (para enviar archivo por mail)"
|
|
24
|
+
"content_type y download_link",
|
|
25
|
+
"parameters": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"filename": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Nombre del archivo de salida (ejemplo: 'reporte.xlsx')",
|
|
31
|
+
"pattern": "^.+\\.xlsx?$"
|
|
32
|
+
},
|
|
33
|
+
"sheet_name": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Nombre de la hoja dentro del Excel",
|
|
36
|
+
"minLength": 1
|
|
37
|
+
},
|
|
38
|
+
"data": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"description": "Lista de diccionarios. Cada diccionario representa una fila.",
|
|
41
|
+
"minItems": 1,
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {},
|
|
45
|
+
"additionalProperties": {
|
|
46
|
+
"anyOf": [
|
|
47
|
+
{"type": "string"},
|
|
48
|
+
{"type": "number"},
|
|
49
|
+
{"type": "boolean"},
|
|
50
|
+
{"type": "null"},
|
|
51
|
+
{
|
|
52
|
+
"type": "string",
|
|
53
|
+
"format": "date"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"required": ["filename", "sheet_name", "data"]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
'function_name': "iat_send_email",
|
|
65
|
+
'description': "iatoolkit mail system. "
|
|
66
|
+
"envia mails cuando un usuario lo solicita.",
|
|
67
|
+
'parameters': {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"recipient": {"type": "string", "description": "email del destinatario"},
|
|
71
|
+
"subject": {"type": "string", "description": "asunto del email"},
|
|
72
|
+
"body": {"type": "string", "description": "HTML del email"},
|
|
73
|
+
"attachments": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"description": "Lista de archivos adjuntos codificados en base64",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"properties": {
|
|
79
|
+
"filename": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Nombre del archivo con su extensión (ej. informe.pdf)"
|
|
82
|
+
},
|
|
83
|
+
"content": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Contenido del archivo en b64."
|
|
86
|
+
},
|
|
87
|
+
"attachment_token": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "token para descargar el archivo."
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"required": ["filename", "content", "attachment_token"],
|
|
93
|
+
"additionalProperties": False
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"required": ["recipient", "subject", "body", "attachments"]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"function_name": "iat_sql_query",
|
|
102
|
+
"description": "Servicio SQL de IAToolkit: debes utilizar este servicio para todas las consultas SQL a bases de datos.",
|
|
103
|
+
"parameters": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"database_key": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "IMPORTANT: nombre de la base de datos a consultar."
|
|
109
|
+
},
|
|
110
|
+
"query": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"description": "string con la consulta en sql"
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"required": ["database_key", "query"]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ToolService:
|
|
122
|
+
@inject
|
|
123
|
+
def __init__(self,
|
|
124
|
+
llm_query_repo: LLMQueryRepo,
|
|
125
|
+
profile_repo: ProfileRepo,
|
|
126
|
+
sql_service: SqlService,
|
|
127
|
+
excel_service: ExcelService,
|
|
128
|
+
mail_service: MailService):
|
|
129
|
+
self.llm_query_repo = llm_query_repo
|
|
130
|
+
self.profile_repo = profile_repo
|
|
131
|
+
self.sql_service = sql_service
|
|
132
|
+
self.excel_service = excel_service
|
|
133
|
+
self.mail_service = mail_service
|
|
134
|
+
|
|
135
|
+
# execution mapper for system tools
|
|
136
|
+
self.system_handlers = {
|
|
137
|
+
"iat_generate_excel": self.excel_service.excel_generator,
|
|
138
|
+
"iat_send_email": self.mail_service.send_mail,
|
|
139
|
+
"iat_sql_query": self.sql_service.exec_sql
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
def register_system_tools(self):
|
|
143
|
+
"""Creates or updates system functions in the database."""
|
|
144
|
+
try:
|
|
145
|
+
# delete all system tools
|
|
146
|
+
self.llm_query_repo.delete_system_tools()
|
|
147
|
+
|
|
148
|
+
# create new system tools
|
|
149
|
+
for function in _SYSTEM_TOOLS:
|
|
150
|
+
new_tool = Tool(
|
|
151
|
+
company_id=None,
|
|
152
|
+
system_function=True,
|
|
153
|
+
name=function['function_name'],
|
|
154
|
+
description=function['description'],
|
|
155
|
+
parameters=function['parameters']
|
|
156
|
+
)
|
|
157
|
+
self.llm_query_repo.create_or_update_tool(new_tool)
|
|
158
|
+
|
|
159
|
+
self.llm_query_repo.commit()
|
|
160
|
+
except Exception as e:
|
|
161
|
+
self.llm_query_repo.rollback()
|
|
162
|
+
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))
|
|
163
|
+
|
|
164
|
+
def sync_company_tools(self, company_short_name: str, tools_config: list):
|
|
165
|
+
"""
|
|
166
|
+
Synchronizes tools from YAML config to Database (Create/Update/Delete strategy).
|
|
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
|
+
|
|
176
|
+
try:
|
|
177
|
+
# 1. Get existing tools map for later cleanup
|
|
178
|
+
existing_tools = {
|
|
179
|
+
f.name: f for f in self.llm_query_repo.get_company_tools(company)
|
|
180
|
+
}
|
|
181
|
+
defined_tool_names = set()
|
|
182
|
+
|
|
183
|
+
# 2. Sync (Create or Update) from Config
|
|
184
|
+
for tool_data in tools_config:
|
|
185
|
+
name = tool_data['function_name']
|
|
186
|
+
defined_tool_names.add(name)
|
|
187
|
+
|
|
188
|
+
# Construct the tool object with current config values
|
|
189
|
+
# We create a new transient object and let the repo merge it
|
|
190
|
+
tool_obj = Tool(
|
|
191
|
+
company_id=company.id,
|
|
192
|
+
name=name,
|
|
193
|
+
description=tool_data['description'],
|
|
194
|
+
parameters=tool_data['params'],
|
|
195
|
+
system_function=False
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Always call create_or_update. The repo handles checking for existence by name.
|
|
199
|
+
self.llm_query_repo.create_or_update_tool(tool_obj)
|
|
200
|
+
|
|
201
|
+
# 3. Cleanup: Delete tools present in DB but not in Config
|
|
202
|
+
for name, tool in existing_tools.items():
|
|
203
|
+
# Ensure we don't delete system functions or active tools accidentally if logic changes,
|
|
204
|
+
# though get_company_tools filters by company_id so system functions shouldn't be here usually.
|
|
205
|
+
if not tool.system_function and (name not in defined_tool_names):
|
|
206
|
+
self.llm_query_repo.delete_tool(tool)
|
|
207
|
+
|
|
208
|
+
self.llm_query_repo.commit()
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
self.llm_query_repo.rollback()
|
|
212
|
+
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_tools_for_llm(self, company: Company) -> list[dict]:
|
|
216
|
+
"""
|
|
217
|
+
Returns the list of tools (System + Company) formatted for the LLM (OpenAI Schema).
|
|
218
|
+
"""
|
|
219
|
+
tools = []
|
|
220
|
+
|
|
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
|
|
226
|
+
params = function.parameters.copy() if function.parameters else {}
|
|
227
|
+
params["additionalProperties"] = False
|
|
228
|
+
|
|
229
|
+
ai_tool = {
|
|
230
|
+
"type": "function",
|
|
231
|
+
"name": function.name,
|
|
232
|
+
"description": function.description,
|
|
233
|
+
"parameters": params,
|
|
234
|
+
"strict": True
|
|
235
|
+
}
|
|
236
|
+
if function.name == 'iat_sql_query':
|
|
237
|
+
params['properties']['database_key']['enum'] = self.sql_service.get_db_names(company.short_name)
|
|
238
|
+
|
|
239
|
+
tools.append(ai_tool)
|
|
240
|
+
return tools
|
|
241
|
+
|
|
242
|
+
def get_system_handler(self, function_name: str):
|
|
243
|
+
return self.system_handlers.get(function_name)
|
|
244
|
+
|
|
245
|
+
def is_system_tool(self, function_name: str) -> bool:
|
|
246
|
+
return function_name in self.system_handlers
|
|
@@ -8,7 +8,7 @@ from injector import inject
|
|
|
8
8
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
9
|
from iatoolkit.services.i18n_service import I18nService
|
|
10
10
|
from iatoolkit.infra.google_chat_app import GoogleChatApp
|
|
11
|
-
from iatoolkit.
|
|
11
|
+
from iatoolkit.services.mail_service import MailService
|
|
12
12
|
import logging
|
|
13
13
|
|
|
14
14
|
|
|
@@ -18,11 +18,11 @@ class UserFeedbackService:
|
|
|
18
18
|
profile_repo: ProfileRepo,
|
|
19
19
|
i18n_service: I18nService,
|
|
20
20
|
google_chat_app: GoogleChatApp,
|
|
21
|
-
|
|
21
|
+
mail_service: MailService):
|
|
22
22
|
self.profile_repo = profile_repo
|
|
23
23
|
self.i18n_service = i18n_service
|
|
24
24
|
self.google_chat_app = google_chat_app
|
|
25
|
-
self.
|
|
25
|
+
self.mail_service = mail_service
|
|
26
26
|
|
|
27
27
|
def _send_google_chat_notification(self, space_name: str, message_text: str):
|
|
28
28
|
"""Envía una notificación de feedback a un espacio de Google Chat."""
|
|
@@ -38,13 +38,21 @@ class UserFeedbackService:
|
|
|
38
38
|
except Exception as e:
|
|
39
39
|
logging.exception(f"error sending notification to Google Chat: {e}")
|
|
40
40
|
|
|
41
|
-
def _send_email_notification(self,
|
|
41
|
+
def _send_email_notification(self,
|
|
42
|
+
company_short_name: str,
|
|
43
|
+
destination_email: str,
|
|
44
|
+
company_name: str,
|
|
45
|
+
message_text: str):
|
|
42
46
|
"""Envía una notificación de feedback por correo electrónico."""
|
|
43
47
|
try:
|
|
44
48
|
subject = f"Nuevo Feedback de {company_name}"
|
|
45
49
|
# Convertir el texto plano a un HTML simple para mantener los saltos de línea
|
|
46
50
|
html_body = message_text.replace('\n', '<br>')
|
|
47
|
-
self.
|
|
51
|
+
self.mail_service.send_mail(
|
|
52
|
+
company_short_name=company_short_name,
|
|
53
|
+
to=destination_email,
|
|
54
|
+
subject=subject,
|
|
55
|
+
body=html_body)
|
|
48
56
|
except Exception as e:
|
|
49
57
|
logging.exception(f"error sending email de feedback: {e}")
|
|
50
58
|
|
|
@@ -65,7 +73,11 @@ class UserFeedbackService:
|
|
|
65
73
|
if channel == 'google_chat':
|
|
66
74
|
self._send_google_chat_notification(space_name=destination, message_text=message_text)
|
|
67
75
|
elif channel == 'email':
|
|
68
|
-
self._send_email_notification(
|
|
76
|
+
self._send_email_notification(
|
|
77
|
+
company_short_name=company.short_name,
|
|
78
|
+
destination_email=destination,
|
|
79
|
+
company_name=company.short_name,
|
|
80
|
+
message_text=message_text)
|
|
69
81
|
else:
|
|
70
82
|
logging.warning(f"unknown feedback channel: '{channel}' for company {company.short_name}.")
|
|
71
83
|
|