iatoolkit 0.3.9__py3-none-any.whl → 0.107.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 +27 -35
- iatoolkit/base_company.py +3 -35
- iatoolkit/cli_commands.py +18 -47
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +48 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +39 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +138 -0
- iatoolkit/common/session_manager.py +26 -0
- iatoolkit/common/util.py +353 -0
- iatoolkit/company_registry.py +66 -29
- iatoolkit/core.py +514 -0
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/brevo_mail_app.py +123 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
- iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
- iatoolkit/infra/llm_proxy.py +268 -0
- iatoolkit/infra/llm_response.py +45 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +222 -0
- iatoolkit/locales/es.yaml +225 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +187 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +105 -0
- iatoolkit/repositories/models.py +279 -0
- iatoolkit/repositories/profile_repo.py +171 -0
- iatoolkit/repositories/vs_repo.py +150 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +7 -7
- iatoolkit/services/branding_service.py +153 -0
- iatoolkit/services/company_context_service.py +214 -0
- iatoolkit/services/configuration_service.py +375 -0
- iatoolkit/services/dispatcher_service.py +134 -0
- {services → iatoolkit/services}/document_service.py +20 -8
- iatoolkit/services/embedding_service.py +148 -0
- iatoolkit/services/excel_service.py +156 -0
- {services → iatoolkit/services}/file_processor_service.py +36 -21
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +80 -0
- iatoolkit/services/language_service.py +89 -0
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/llm_client_service.py +438 -0
- iatoolkit/services/load_documents_service.py +174 -0
- iatoolkit/services/mail_service.py +213 -0
- {services → iatoolkit/services}/profile_service.py +200 -101
- iatoolkit/services/prompt_service.py +303 -0
- iatoolkit/services/query_service.py +467 -0
- iatoolkit/services/search_service.py +55 -0
- iatoolkit/services/sql_service.py +169 -0
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +117 -0
- iatoolkit/services/user_session_context_service.py +213 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +401 -0
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +103 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +38 -0
- iatoolkit/static/styles/chat_iatoolkit.css +559 -0
- iatoolkit/static/styles/chat_modal.css +133 -0
- iatoolkit/static/styles/chat_public.css +135 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +398 -0
- iatoolkit/static/styles/llm_output.css +148 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +30 -23
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +45 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +78 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +337 -0
- iatoolkit/templates/chat_modals.html +185 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +51 -0
- iatoolkit/templates/onboarding_shell.html +106 -0
- iatoolkit/templates/signup.html +79 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +96 -0
- iatoolkit/views/change_password_view.py +116 -0
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +75 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +63 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +59 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +65 -0
- iatoolkit/views/login_view.py +170 -0
- iatoolkit/views/logout_api_view.py +57 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +100 -0
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +60 -0
- iatoolkit-0.107.4.dist-info/METADATA +268 -0
- iatoolkit-0.107.4.dist-info/RECORD +132 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
- iatoolkit/iatoolkit.py +0 -413
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.3.9.dist-info/METADATA +0 -252
- iatoolkit-0.3.9.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/dispatcher_service.py +0 -351
- services/excel_service.py +0 -98
- services/history_service.py +0 -45
- services/jwt_service.py +0 -91
- services/load_documents_service.py +0 -212
- services/mail_service.py +0 -62
- services/prompt_manager_service.py +0 -172
- services/query_service.py +0 -334
- services/search_service.py +0 -32
- services/sql_service.py +0 -42
- services/tasks_service.py +0 -188
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# deepseek_adapter.py
|
|
2
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
3
|
+
# Product: IAToolkit
|
|
4
|
+
#
|
|
5
|
+
# IAToolkit is open source software.
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, List, Optional, Any
|
|
9
|
+
|
|
10
|
+
from iatoolkit.infra.llm_response import LLMResponse, ToolCall, Usage
|
|
11
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
class DeepseekAdapter:
|
|
15
|
+
"""
|
|
16
|
+
Adapter for DeepSeek using the OpenAI-compatible Chat Completions API.
|
|
17
|
+
It translates IAToolkit's common request/response format into
|
|
18
|
+
DeepSeek chat.completions calls.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, deepseek_client):
|
|
22
|
+
# deepseek_client is an OpenAI client configured with base_url="https://api.deepseek.com"
|
|
23
|
+
self.client = deepseek_client
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
# Public entry point
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def create_response(self, model: str, input: List[Dict], **kwargs) -> LLMResponse:
|
|
30
|
+
"""
|
|
31
|
+
Entry point called by LLMProxy.
|
|
32
|
+
|
|
33
|
+
:param model: DeepSeek model name (e.g. "deepseek-chat").
|
|
34
|
+
:param input: Common IAToolkit input list. It may contain:
|
|
35
|
+
- normal messages: {"role": "...", "content": "..."}
|
|
36
|
+
- function outputs: {"type": "function_call_output",
|
|
37
|
+
"call_id": "...", "output": "..."}
|
|
38
|
+
:param kwargs: extra options (tools, tool_choice, context_history, etc.).
|
|
39
|
+
"""
|
|
40
|
+
tools = kwargs.get("tools") or []
|
|
41
|
+
tool_choice = kwargs.get("tool_choice", "auto")
|
|
42
|
+
context_history = kwargs.get("context_history") or []
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# 1) Build messages from history (if any)
|
|
46
|
+
messages: List[Dict[str, Any]] = []
|
|
47
|
+
if context_history:
|
|
48
|
+
history_messages = self._build_messages_from_input(context_history)
|
|
49
|
+
messages.extend(history_messages)
|
|
50
|
+
|
|
51
|
+
# 2) Append current turn messages
|
|
52
|
+
current_messages = self._build_messages_from_input(input)
|
|
53
|
+
messages.extend(current_messages)
|
|
54
|
+
|
|
55
|
+
# Detect if this input already contains function_call_output items.
|
|
56
|
+
# That means we are in the "second phase" after executing tools.
|
|
57
|
+
has_function_outputs = any(
|
|
58
|
+
item.get("type") == "function_call_output" for item in input
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 3) Build the tools payload
|
|
62
|
+
tools_payload = self._build_tools_payload(tools)
|
|
63
|
+
|
|
64
|
+
# If we already have function_call_output messages and the caller did not force
|
|
65
|
+
# a specific tool_choice (e.g. "required" for SQL retry), we disable tools and
|
|
66
|
+
# tool_choice to avoid infinite tool-calling loops (especially with iat_sql_query).
|
|
67
|
+
if has_function_outputs and tool_choice == "auto":
|
|
68
|
+
logging.debug(
|
|
69
|
+
"[DeepseekAdapter] Detected function_call_output in input; "
|
|
70
|
+
"disabling tools and tool_choice to avoid tool loop."
|
|
71
|
+
)
|
|
72
|
+
tools_payload = None
|
|
73
|
+
tool_choice = None
|
|
74
|
+
|
|
75
|
+
logging.debug(f"[DeepseekAdapter] messages={messages}")
|
|
76
|
+
logging.debug(f"[DeepseekAdapter] tools={tools_payload}, tool_choice={tool_choice}")
|
|
77
|
+
|
|
78
|
+
# Build kwargs for API call, skipping empty parameters
|
|
79
|
+
call_kwargs: Dict[str, Any] = {
|
|
80
|
+
"model": model,
|
|
81
|
+
"messages": messages,
|
|
82
|
+
}
|
|
83
|
+
if tools_payload:
|
|
84
|
+
call_kwargs["tools"] = tools_payload
|
|
85
|
+
if tool_choice:
|
|
86
|
+
call_kwargs["tool_choice"] = tool_choice
|
|
87
|
+
|
|
88
|
+
logging.debug(f"[DeepseekAdapter] Calling DeepSeek chat.completions API...: {json.dumps(messages, indent=2)}")
|
|
89
|
+
response = self.client.chat.completions.create(**call_kwargs)
|
|
90
|
+
|
|
91
|
+
return self._map_deepseek_chat_response(response)
|
|
92
|
+
|
|
93
|
+
except IAToolkitException:
|
|
94
|
+
# Re-raise IAToolkit exceptions as is
|
|
95
|
+
raise
|
|
96
|
+
except Exception as ex:
|
|
97
|
+
logging.exception("Unexpected error calling DeepSeek")
|
|
98
|
+
raise IAToolkitException(
|
|
99
|
+
IAToolkitException.ErrorType.LLM_ERROR,
|
|
100
|
+
f"DeepSeek error: {ex}"
|
|
101
|
+
) from ex
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# Helpers to build the request
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def _build_messages_from_input(self, input_items: List[Dict]) -> List[Dict]:
|
|
108
|
+
"""
|
|
109
|
+
Transform IAToolkit 'input' items into ChatCompletion 'messages'.
|
|
110
|
+
|
|
111
|
+
We handle:
|
|
112
|
+
- Standard messages with 'role' and 'content'.
|
|
113
|
+
- function_call_output items by converting them into assistant messages
|
|
114
|
+
containing the tool result, so the model can use them to answer.
|
|
115
|
+
"""
|
|
116
|
+
messages: List[Dict[str, Any]] = []
|
|
117
|
+
|
|
118
|
+
for item in input_items:
|
|
119
|
+
# Tool call outputs are mapped to assistant messages with the tool result.
|
|
120
|
+
if item.get("type") == "function_call_output":
|
|
121
|
+
output = item.get("output", "")
|
|
122
|
+
if not output:
|
|
123
|
+
logging.warning(
|
|
124
|
+
"[DeepseekAdapter] function_call_output item without 'output': %s",
|
|
125
|
+
item
|
|
126
|
+
)
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
messages.append(
|
|
130
|
+
{
|
|
131
|
+
"role": "user",
|
|
132
|
+
"content": f"Tool result:\n{output}",
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
role = item.get("role")
|
|
138
|
+
content = item.get("content")
|
|
139
|
+
|
|
140
|
+
# Skip tool-role messages completely for DeepSeek
|
|
141
|
+
if role == "tool":
|
|
142
|
+
logging.warning(f"[DeepseekAdapter] Skipping tool-role message: {item}")
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if not role:
|
|
146
|
+
logging.warning(f"[DeepseekAdapter] Skipping message without role: {item}")
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
messages.append({"role": role, "content": content})
|
|
150
|
+
|
|
151
|
+
return messages
|
|
152
|
+
|
|
153
|
+
def _build_tools_payload(self, tools: List[Dict]) -> Optional[List[Dict]]:
|
|
154
|
+
"""
|
|
155
|
+
Transform IAToolkit tool definitions into DeepSeek/OpenAI chat tools format.
|
|
156
|
+
|
|
157
|
+
Expected internal tool format:
|
|
158
|
+
{
|
|
159
|
+
"type": "function",
|
|
160
|
+
"name": ...,
|
|
161
|
+
"description": ...,
|
|
162
|
+
"parameters": {...},
|
|
163
|
+
"strict": True/False
|
|
164
|
+
}
|
|
165
|
+
Or already in OpenAI tools format with "function" key.
|
|
166
|
+
"""
|
|
167
|
+
if not tools:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
tools_payload: List[Dict[str, Any]] = []
|
|
171
|
+
|
|
172
|
+
for tool in tools:
|
|
173
|
+
# If it's already in OpenAI 'function' format, reuse it
|
|
174
|
+
if "function" in tool:
|
|
175
|
+
func_def = tool["function"]
|
|
176
|
+
else:
|
|
177
|
+
# Build function definition from flattened structure
|
|
178
|
+
func_def = {
|
|
179
|
+
"name": tool.get("name"),
|
|
180
|
+
"description": tool.get("description", ""),
|
|
181
|
+
"parameters": tool.get("parameters", {}) or {},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Ensure parameters is a dict
|
|
185
|
+
if "parameters" in func_def and not isinstance(func_def["parameters"], dict):
|
|
186
|
+
logging.warning(
|
|
187
|
+
"Tool parameters must be a dict; got %s",
|
|
188
|
+
type(func_def["parameters"])
|
|
189
|
+
)
|
|
190
|
+
func_def["parameters"] = {}
|
|
191
|
+
|
|
192
|
+
ds_tool: Dict[str, Any] = {
|
|
193
|
+
"type": tool.get("type", "function"),
|
|
194
|
+
"function": func_def,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if tool.get("strict") is True:
|
|
198
|
+
ds_tool["strict"] = True
|
|
199
|
+
|
|
200
|
+
tools_payload.append(ds_tool)
|
|
201
|
+
|
|
202
|
+
return tools_payload or None
|
|
203
|
+
|
|
204
|
+
# ------------------------------------------------------------------
|
|
205
|
+
# Mapping DeepSeek response -> LLMResponse
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def _map_deepseek_chat_response(self, response: Any) -> LLMResponse:
|
|
209
|
+
"""
|
|
210
|
+
Map DeepSeek Chat Completion response to our common LLMResponse.
|
|
211
|
+
Handles both plain assistant messages and tool_calls.
|
|
212
|
+
"""
|
|
213
|
+
# We only look at the first choice
|
|
214
|
+
if not response.choices:
|
|
215
|
+
raise IAToolkitException(
|
|
216
|
+
IAToolkitException.ErrorType.LLM_ERROR,
|
|
217
|
+
"DeepSeek response has no choices."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
choice = response.choices[0]
|
|
221
|
+
message = choice.message
|
|
222
|
+
|
|
223
|
+
# Usage mapping
|
|
224
|
+
usage = Usage(
|
|
225
|
+
input_tokens=getattr(getattr(response, "usage", None), "prompt_tokens", 0) or 0,
|
|
226
|
+
output_tokens=getattr(getattr(response, "usage", None), "completion_tokens", 0) or 0,
|
|
227
|
+
total_tokens=getattr(getattr(response, "usage", None), "total_tokens", 0) or 0,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Capture reasoning content (specific to deepseek-reasoner)
|
|
231
|
+
reasoning_content = getattr(message, "reasoning_content", "") or ""
|
|
232
|
+
|
|
233
|
+
# If the model produced tool calls, fills this list
|
|
234
|
+
tool_calls_out: List[ToolCall] = []
|
|
235
|
+
|
|
236
|
+
tool_calls = getattr(message, "tool_calls", None) or []
|
|
237
|
+
if not tool_calls:
|
|
238
|
+
# No tool calls: standard assistant message
|
|
239
|
+
output_text = getattr(message, "content", "") or ""
|
|
240
|
+
status = "completed"
|
|
241
|
+
|
|
242
|
+
else:
|
|
243
|
+
logging.debug(f"[DeepSeek] RAW tool_calls: {tool_calls}")
|
|
244
|
+
|
|
245
|
+
for tc in tool_calls:
|
|
246
|
+
func = getattr(tc, "function", None)
|
|
247
|
+
if not func:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
name = getattr(func, "name", "")
|
|
251
|
+
arguments = getattr(func, "arguments", "") or "{}"
|
|
252
|
+
|
|
253
|
+
# DeepSeek/OpenAI return arguments as JSON string
|
|
254
|
+
logging.debug(
|
|
255
|
+
f"[DeepSeek] ToolCall generated -> id={getattr(tc, 'id', '')} "
|
|
256
|
+
f"name={name} arguments_raw={arguments}"
|
|
257
|
+
)
|
|
258
|
+
tool_calls_out.append(
|
|
259
|
+
ToolCall(
|
|
260
|
+
call_id=getattr(tc, "id", ""),
|
|
261
|
+
type="function_call",
|
|
262
|
+
name=name,
|
|
263
|
+
arguments=arguments,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
status = "tool_calls"
|
|
268
|
+
output_text = "" # caller will inspect tool_calls in .output
|
|
269
|
+
|
|
270
|
+
return LLMResponse(
|
|
271
|
+
id=getattr(response, "id", "deepseek-unknown"),
|
|
272
|
+
model=getattr(response, "model", "deepseek-unknown"),
|
|
273
|
+
status=status,
|
|
274
|
+
output_text=output_text,
|
|
275
|
+
output=tool_calls_out,
|
|
276
|
+
usage=usage,
|
|
277
|
+
reasoning_content=reasoning_content
|
|
278
|
+
)
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.infra.llm_response import LLMResponse, ToolCall, Usage
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
from google.generativeai.types import HarmCategory, HarmBlockThreshold
|
|
9
|
+
from google.protobuf.json_format import MessageToDict
|
|
10
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
11
|
+
import logging
|
|
12
|
+
import json
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
class GeminiAdapter:
|
|
16
|
+
|
|
17
|
+
def __init__(self, gemini_client):
|
|
18
|
+
self.client = gemini_client
|
|
19
|
+
|
|
20
|
+
# security configuration - allow content that might be blocked by default
|
|
21
|
+
self.safety_settings = {
|
|
22
|
+
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
|
|
23
|
+
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
|
|
24
|
+
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
|
|
25
|
+
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def create_response(self,
|
|
29
|
+
model: str,
|
|
30
|
+
input: List[Dict],
|
|
31
|
+
previous_response_id: Optional[str] = None,
|
|
32
|
+
context_history: Optional[List[Dict]] = None,
|
|
33
|
+
tools: Optional[List[Dict]] = None,
|
|
34
|
+
text: Optional[Dict] = None,
|
|
35
|
+
reasoning: Optional[Dict] = None,
|
|
36
|
+
tool_choice: str = "auto",
|
|
37
|
+
) -> LLMResponse:
|
|
38
|
+
try:
|
|
39
|
+
# init the model with the configured client
|
|
40
|
+
gemini_model = self.client.GenerativeModel(
|
|
41
|
+
model_name=self._map_model_name(model),
|
|
42
|
+
safety_settings=self.safety_settings
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# prepare the content for gemini
|
|
46
|
+
if context_history:
|
|
47
|
+
# concat the history with the current input
|
|
48
|
+
contents = self._prepare_gemini_contents(context_history + input)
|
|
49
|
+
else:
|
|
50
|
+
contents = self._prepare_gemini_contents(input)
|
|
51
|
+
|
|
52
|
+
# prepare tools
|
|
53
|
+
gemini_tools = self._prepare_gemini_tools(tools) if tools else None
|
|
54
|
+
|
|
55
|
+
# config generation
|
|
56
|
+
generation_config = self._prepare_generation_config(text, tool_choice)
|
|
57
|
+
|
|
58
|
+
# call gemini
|
|
59
|
+
if gemini_tools:
|
|
60
|
+
# with tools
|
|
61
|
+
response = gemini_model.generate_content(
|
|
62
|
+
contents,
|
|
63
|
+
tools=gemini_tools,
|
|
64
|
+
generation_config=generation_config
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
# without tools
|
|
68
|
+
response = gemini_model.generate_content(
|
|
69
|
+
contents,
|
|
70
|
+
generation_config=generation_config
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# map the answer to a common structure
|
|
74
|
+
llm_response = self._map_gemini_response(response, model)
|
|
75
|
+
|
|
76
|
+
# add the model answer to the history
|
|
77
|
+
if context_history and llm_response.output_text:
|
|
78
|
+
context_history.append(
|
|
79
|
+
{
|
|
80
|
+
'role': 'assistant',
|
|
81
|
+
'context': llm_response.output_text
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return llm_response
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
error_message = f"Error calling Gemini API: {str(e)}"
|
|
89
|
+
logging.error(error_message)
|
|
90
|
+
|
|
91
|
+
# handle gemini specific errors
|
|
92
|
+
if "quota" in str(e).lower():
|
|
93
|
+
error_message = "Se ha excedido la cuota de la API de Gemini"
|
|
94
|
+
elif "blocked" in str(e).lower():
|
|
95
|
+
error_message = "El contenido fue bloqueado por las políticas de seguridad de Gemini"
|
|
96
|
+
elif "token" in str(e).lower():
|
|
97
|
+
error_message = "Tu consulta supera el límite de contexto de Gemini"
|
|
98
|
+
|
|
99
|
+
raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
|
|
100
|
+
|
|
101
|
+
def _map_model_name(self, model: str) -> str:
|
|
102
|
+
model_mapping = {
|
|
103
|
+
"gemini-pro": "gemini-2.5-pro",
|
|
104
|
+
"gemini": "gemini-2.5-pro",
|
|
105
|
+
"gemini-1.5": "gemini-2.5-pro",
|
|
106
|
+
"gemini-flash": "gemini-1.5-flash",
|
|
107
|
+
"gemini-2.0": "gemini-2.0-flash-exp"
|
|
108
|
+
}
|
|
109
|
+
return model_mapping.get(model.lower(), model)
|
|
110
|
+
|
|
111
|
+
def _prepare_gemini_contents(self, input: List[Dict]) -> List[Dict]:
|
|
112
|
+
# convert input messages to Gemini format
|
|
113
|
+
gemini_contents = []
|
|
114
|
+
|
|
115
|
+
for message in input:
|
|
116
|
+
if message.get("role") == "system":
|
|
117
|
+
gemini_contents.append({
|
|
118
|
+
"role": "user",
|
|
119
|
+
"parts": [{"text": f"[INSTRUCCIONES DEL SISTEMA]\n{message.get('content', '')}"}]
|
|
120
|
+
})
|
|
121
|
+
elif message.get("role") == "user":
|
|
122
|
+
gemini_contents.append({
|
|
123
|
+
"role": "user",
|
|
124
|
+
"parts": [{"text": message.get("content", "")}]
|
|
125
|
+
})
|
|
126
|
+
elif message.get("type") == "function_call_output":
|
|
127
|
+
gemini_contents.append({
|
|
128
|
+
"role": "function",
|
|
129
|
+
"parts": [{
|
|
130
|
+
"function_response": {
|
|
131
|
+
"name": "tool_result",
|
|
132
|
+
"response": {"output": message.get("output", "")}
|
|
133
|
+
}
|
|
134
|
+
}]
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return gemini_contents
|
|
138
|
+
|
|
139
|
+
def _prepare_gemini_tools(self, tools: List[Dict]) -> List[Dict]:
|
|
140
|
+
# convert tools to Gemini format
|
|
141
|
+
if not tools:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
function_declarations = []
|
|
145
|
+
for i, tool in enumerate(tools):
|
|
146
|
+
# Verificar estructura básica
|
|
147
|
+
tool_type = tool.get("type")
|
|
148
|
+
|
|
149
|
+
if tool_type != "function":
|
|
150
|
+
logging.warning(f"Herramienta {i} no es de tipo 'function': {tool_type}")
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Extraer datos de la herramienta (estructura plana)
|
|
154
|
+
function_name = tool.get("name")
|
|
155
|
+
function_description = tool.get("description", "")
|
|
156
|
+
function_parameters = tool.get("parameters", {})
|
|
157
|
+
|
|
158
|
+
# Verificar si el nombre existe y no está vacío
|
|
159
|
+
if not function_name or not isinstance(function_name, str) or not function_name.strip():
|
|
160
|
+
logging.error(f"PROBLEMA: Herramienta {i} sin nombre válido")
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Preparar la declaración de función para Gemini
|
|
164
|
+
gemini_function = {
|
|
165
|
+
"name": function_name,
|
|
166
|
+
"description": function_description,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Agregar parámetros si existen y limpiar campos específicos de OpenAI
|
|
170
|
+
if function_parameters:
|
|
171
|
+
clean_parameters = self._clean_openai_specific_fields(function_parameters)
|
|
172
|
+
gemini_function["parameters"] = clean_parameters
|
|
173
|
+
|
|
174
|
+
function_declarations.append(gemini_function)
|
|
175
|
+
|
|
176
|
+
if function_declarations:
|
|
177
|
+
final_tools = [{
|
|
178
|
+
"function_declarations": function_declarations
|
|
179
|
+
}]
|
|
180
|
+
|
|
181
|
+
# Log de la estructura final para debug
|
|
182
|
+
# logging.info("Estructura final de herramientas para Gemini:")
|
|
183
|
+
# logging.info(f"{json.dumps(final_tools, indent=2)}")
|
|
184
|
+
|
|
185
|
+
return final_tools
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _clean_openai_specific_fields(self, parameters: Dict) -> Dict:
|
|
191
|
+
"""Limpiar campos específicos de OpenAI que Gemini no entiende"""
|
|
192
|
+
clean_params = {}
|
|
193
|
+
|
|
194
|
+
# Campos permitidos por Gemini según su Schema protobuf
|
|
195
|
+
# Estos son los únicos campos que Gemini acepta en sus esquemas
|
|
196
|
+
allowed_fields = {
|
|
197
|
+
"type", # Tipo de datos: string, number, object, array, boolean
|
|
198
|
+
"properties", # Para objetos: define las propiedades
|
|
199
|
+
"required", # Array de propiedades requeridas
|
|
200
|
+
"items", # Para arrays: define el tipo de elementos
|
|
201
|
+
"description", # Descripción del campo
|
|
202
|
+
"enum", # Lista de valores permitidos
|
|
203
|
+
# Gemini NO soporta estos campos comunes de JSON Schema:
|
|
204
|
+
# "pattern", "format", "minimum", "maximum", "minItems", "maxItems",
|
|
205
|
+
# "minLength", "maxLength", "additionalProperties", "strict"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for key, value in parameters.items():
|
|
209
|
+
if key in allowed_fields:
|
|
210
|
+
if key == "properties" and isinstance(value, dict):
|
|
211
|
+
# Limpiar recursivamente las propiedades
|
|
212
|
+
clean_props = {}
|
|
213
|
+
for prop_name, prop_def in value.items():
|
|
214
|
+
if isinstance(prop_def, dict):
|
|
215
|
+
clean_props[prop_name] = self._clean_openai_specific_fields(prop_def)
|
|
216
|
+
else:
|
|
217
|
+
clean_props[prop_name] = prop_def
|
|
218
|
+
clean_params[key] = clean_props
|
|
219
|
+
elif key == "items" and isinstance(value, dict):
|
|
220
|
+
# Limpiar recursivamente los items de array
|
|
221
|
+
clean_params[key] = self._clean_openai_specific_fields(value)
|
|
222
|
+
else:
|
|
223
|
+
clean_params[key] = value
|
|
224
|
+
else:
|
|
225
|
+
logging.debug(f"Campo '{key}' removido (no soportado por Gemini)")
|
|
226
|
+
|
|
227
|
+
return clean_params
|
|
228
|
+
|
|
229
|
+
def _prepare_generation_config(self, text: Optional[Dict], tool_choice: str) -> Dict:
|
|
230
|
+
"""Preparar configuración de generación para Gemini"""
|
|
231
|
+
config = {"candidate_count": 1}
|
|
232
|
+
|
|
233
|
+
if text:
|
|
234
|
+
if "temperature" in text:
|
|
235
|
+
config["temperature"] = float(text["temperature"])
|
|
236
|
+
if "max_tokens" in text:
|
|
237
|
+
config["max_output_tokens"] = int(text["max_tokens"])
|
|
238
|
+
if "top_p" in text:
|
|
239
|
+
config["top_p"] = float(text["top_p"])
|
|
240
|
+
|
|
241
|
+
return config
|
|
242
|
+
|
|
243
|
+
def _map_gemini_response(self, gemini_response, model: str) -> LLMResponse:
|
|
244
|
+
"""Mapear respuesta de Gemini a estructura común"""
|
|
245
|
+
response_id = str(uuid.uuid4())
|
|
246
|
+
output_text = ""
|
|
247
|
+
tool_calls = []
|
|
248
|
+
|
|
249
|
+
if gemini_response.candidates and len(gemini_response.candidates) > 0:
|
|
250
|
+
candidate = gemini_response.candidates[0]
|
|
251
|
+
|
|
252
|
+
for part in candidate.content.parts:
|
|
253
|
+
if hasattr(part, 'text') and part.text:
|
|
254
|
+
output_text += part.text
|
|
255
|
+
elif hasattr(part, 'function_call') and part.function_call:
|
|
256
|
+
func_call = part.function_call
|
|
257
|
+
tool_calls.append(ToolCall(
|
|
258
|
+
call_id=f"call_{uuid.uuid4().hex[:8]}",
|
|
259
|
+
type="function_call",
|
|
260
|
+
name=func_call.name,
|
|
261
|
+
arguments=json.dumps(MessageToDict(func_call._pb).get('args', {}))
|
|
262
|
+
))
|
|
263
|
+
|
|
264
|
+
# Determinar status
|
|
265
|
+
status = "completed"
|
|
266
|
+
if gemini_response.candidates:
|
|
267
|
+
candidate = gemini_response.candidates[0]
|
|
268
|
+
if hasattr(candidate, 'finish_reason'):
|
|
269
|
+
# Manejar finish_reason tanto como objeto con .name como entero/enum directo
|
|
270
|
+
finish_reason = candidate.finish_reason
|
|
271
|
+
|
|
272
|
+
# Si finish_reason tiene un atributo .name, usarlo
|
|
273
|
+
if hasattr(finish_reason, 'name'):
|
|
274
|
+
finish_reason_name = finish_reason.name
|
|
275
|
+
else:
|
|
276
|
+
# Si es un entero o enum directo, convertirlo a string
|
|
277
|
+
finish_reason_name = str(finish_reason)
|
|
278
|
+
|
|
279
|
+
if finish_reason_name in ["SAFETY", "RECITATION", "3", "4"]: # Agregar valores numéricos también
|
|
280
|
+
status = "blocked"
|
|
281
|
+
elif finish_reason_name in ["MAX_TOKENS", "LENGTH", "2"]: # Agregar valores numéricos también
|
|
282
|
+
status = "length_exceeded"
|
|
283
|
+
|
|
284
|
+
# Calcular usage de tokens
|
|
285
|
+
usage = self._extract_usage_metadata(gemini_response)
|
|
286
|
+
|
|
287
|
+
# Estimación básica si no hay datos de usage
|
|
288
|
+
if usage.total_tokens == 0:
|
|
289
|
+
estimated_output_tokens = len(output_text) // 4
|
|
290
|
+
usage = Usage(
|
|
291
|
+
input_tokens=0,
|
|
292
|
+
output_tokens=estimated_output_tokens,
|
|
293
|
+
total_tokens=estimated_output_tokens
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return LLMResponse(
|
|
297
|
+
id=response_id,
|
|
298
|
+
model=model,
|
|
299
|
+
status=status,
|
|
300
|
+
output_text=output_text,
|
|
301
|
+
output=tool_calls,
|
|
302
|
+
usage=usage
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def _extract_usage_metadata(self, gemini_response) -> Usage:
|
|
306
|
+
"""Extraer información de uso de tokens de manera segura"""
|
|
307
|
+
input_tokens = 0
|
|
308
|
+
output_tokens = 0
|
|
309
|
+
total_tokens = 0
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# Verificar si existe usage_metadata
|
|
313
|
+
if hasattr(gemini_response, 'usage_metadata') and gemini_response.usage_metadata:
|
|
314
|
+
usage_metadata = gemini_response.usage_metadata
|
|
315
|
+
|
|
316
|
+
# Acceder a los atributos directamente, no con .get()
|
|
317
|
+
if hasattr(usage_metadata, 'prompt_token_count'):
|
|
318
|
+
input_tokens = usage_metadata.prompt_token_count
|
|
319
|
+
if hasattr(usage_metadata, 'candidates_token_count'):
|
|
320
|
+
output_tokens = usage_metadata.candidates_token_count
|
|
321
|
+
if hasattr(usage_metadata, 'total_token_count'):
|
|
322
|
+
total_tokens = usage_metadata.total_token_count
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logging.warning(f"No se pudo extraer usage_metadata de Gemini: {e}")
|
|
326
|
+
|
|
327
|
+
# Si no hay datos de usage o son cero, hacer estimación básica
|
|
328
|
+
if total_tokens == 0 and output_tokens == 0:
|
|
329
|
+
# Obtener texto de salida para estimación
|
|
330
|
+
output_text = ""
|
|
331
|
+
if (hasattr(gemini_response, 'candidates') and
|
|
332
|
+
gemini_response.candidates and
|
|
333
|
+
len(gemini_response.candidates) > 0):
|
|
334
|
+
|
|
335
|
+
candidate = gemini_response.candidates[0]
|
|
336
|
+
if hasattr(candidate, 'content') and hasattr(candidate.content, 'parts'):
|
|
337
|
+
for part in candidate.content.parts:
|
|
338
|
+
if hasattr(part, 'text') and part.text:
|
|
339
|
+
output_text += part.text
|
|
340
|
+
|
|
341
|
+
# Estimación básica (4 caracteres por token aproximadamente)
|
|
342
|
+
estimated_output_tokens = len(output_text) // 4 if output_text else 0
|
|
343
|
+
output_tokens = estimated_output_tokens
|
|
344
|
+
total_tokens = estimated_output_tokens
|
|
345
|
+
|
|
346
|
+
return Usage(
|
|
347
|
+
input_tokens=input_tokens,
|
|
348
|
+
output_tokens=output_tokens,
|
|
349
|
+
total_tokens=total_tokens
|
|
350
|
+
)
|