MindsDB 25.7.1.0__py3-none-any.whl → 25.7.2.0__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 MindsDB might be problematic. Click here for more details.
- mindsdb/__about__.py +1 -1
- mindsdb/__main__.py +53 -94
- mindsdb/api/a2a/agent.py +30 -206
- mindsdb/api/a2a/common/server/server.py +26 -27
- mindsdb/api/a2a/task_manager.py +93 -227
- mindsdb/api/a2a/utils.py +21 -0
- mindsdb/api/executor/utilities/sql.py +97 -21
- mindsdb/api/http/namespaces/agents.py +126 -201
- mindsdb/api/http/namespaces/config.py +12 -1
- mindsdb/integrations/handlers/pgvector_handler/pgvector_handler.py +94 -1
- mindsdb/integrations/handlers/salesforce_handler/salesforce_handler.py +3 -2
- mindsdb/integrations/handlers/salesforce_handler/salesforce_tables.py +1 -1
- mindsdb/integrations/libs/keyword_search_base.py +41 -0
- mindsdb/integrations/libs/vectordatabase_handler.py +35 -14
- mindsdb/integrations/utilities/sql_utils.py +11 -0
- mindsdb/interfaces/database/projects.py +1 -3
- mindsdb/interfaces/functions/controller.py +54 -64
- mindsdb/interfaces/functions/to_markdown.py +47 -14
- mindsdb/interfaces/knowledge_base/controller.py +127 -35
- mindsdb/interfaces/knowledge_base/evaluate.py +2 -2
- mindsdb/utilities/config.py +46 -39
- mindsdb/utilities/exception.py +11 -0
- {mindsdb-25.7.1.0.dist-info → mindsdb-25.7.2.0.dist-info}/METADATA +244 -244
- {mindsdb-25.7.1.0.dist-info → mindsdb-25.7.2.0.dist-info}/RECORD +27 -25
- {mindsdb-25.7.1.0.dist-info → mindsdb-25.7.2.0.dist-info}/WHEEL +0 -0
- {mindsdb-25.7.1.0.dist-info → mindsdb-25.7.2.0.dist-info}/licenses/LICENSE +0 -0
- {mindsdb-25.7.1.0.dist-info → mindsdb-25.7.2.0.dist-info}/top_level.txt +0 -0
|
@@ -135,36 +135,35 @@ class A2AServer:
|
|
|
135
135
|
|
|
136
136
|
def _create_response(self, result: Any) -> JSONResponse | EventSourceResponse:
|
|
137
137
|
if isinstance(result, AsyncIterable):
|
|
138
|
-
|
|
139
|
-
async def event_generator(result)
|
|
138
|
+
# Step 2: Yield actual serialized event as JSON, with timing logs
|
|
139
|
+
async def event_generator(result):
|
|
140
140
|
async for item in result:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
},
|
|
163
|
-
# Explicitly set media_type
|
|
164
|
-
media_type="text/event-stream",
|
|
165
|
-
)
|
|
141
|
+
t0 = time.time()
|
|
142
|
+
logger.debug(f"[A2AServer] STEP2 serializing item at {t0}: {str(item)[:120]}")
|
|
143
|
+
try:
|
|
144
|
+
if hasattr(item, "model_dump_json"):
|
|
145
|
+
data = item.model_dump_json(exclude_none=True)
|
|
146
|
+
else:
|
|
147
|
+
data = json.dumps(item)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"Serialization error in SSE stream: {e}")
|
|
150
|
+
data = json.dumps({"error": f"Serialization error: {str(e)}"})
|
|
151
|
+
yield {"data": data}
|
|
152
|
+
|
|
153
|
+
# Add robust SSE headers for compatibility
|
|
154
|
+
sse_headers = {
|
|
155
|
+
"Content-Type": "text/event-stream",
|
|
156
|
+
"Cache-Control": "no-cache, no-transform",
|
|
157
|
+
"X-Accel-Buffering": "no",
|
|
158
|
+
"Connection": "keep-alive",
|
|
159
|
+
"Transfer-Encoding": "chunked",
|
|
160
|
+
}
|
|
161
|
+
return EventSourceResponse(event_generator(result), headers=sse_headers)
|
|
166
162
|
elif isinstance(result, JSONRPCResponse):
|
|
167
163
|
return JSONResponse(result.model_dump(exclude_none=True))
|
|
164
|
+
elif isinstance(result, dict):
|
|
165
|
+
logger.warning("Falling back to JSONResponse for result type: dict")
|
|
166
|
+
return JSONResponse(result)
|
|
168
167
|
else:
|
|
169
168
|
logger.error(f"Unexpected result type: {type(result)}")
|
|
170
169
|
raise ValueError(f"Unexpected result type: {type(result)}")
|
mindsdb/api/a2a/task_manager.py
CHANGED
|
@@ -18,14 +18,30 @@ from mindsdb.api.a2a.common.types import (
|
|
|
18
18
|
)
|
|
19
19
|
from mindsdb.api.a2a.common.server.task_manager import InMemoryTaskManager
|
|
20
20
|
from mindsdb.api.a2a.agent import MindsDBAgent
|
|
21
|
+
from mindsdb.api.a2a.utils import to_serializable
|
|
21
22
|
|
|
22
23
|
from typing import Union
|
|
23
24
|
import logging
|
|
24
25
|
import asyncio
|
|
26
|
+
import time
|
|
25
27
|
|
|
26
28
|
logger = logging.getLogger(__name__)
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
def to_question_format(messages):
|
|
32
|
+
"""Convert A2A messages to a list of {"question": ...} dicts for agent compatibility."""
|
|
33
|
+
out = []
|
|
34
|
+
for msg in messages:
|
|
35
|
+
if "question" in msg:
|
|
36
|
+
out.append(msg)
|
|
37
|
+
elif "parts" in msg and isinstance(msg["parts"], list):
|
|
38
|
+
for part in msg["parts"]:
|
|
39
|
+
part_dict = to_serializable(part)
|
|
40
|
+
if part_dict.get("type") == "text" and "text" in part_dict:
|
|
41
|
+
out.append({"question": part_dict["text"]})
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
29
45
|
class AgentTaskManager(InMemoryTaskManager):
|
|
30
46
|
def __init__(
|
|
31
47
|
self,
|
|
@@ -67,10 +83,13 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
67
83
|
logger.info(f"Task created/updated with history length: {len(task.history) if task.history else 0}")
|
|
68
84
|
except Exception as e:
|
|
69
85
|
logger.error(f"Error creating task: {str(e)}")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
error_result = to_serializable(
|
|
87
|
+
{
|
|
88
|
+
"id": request.id,
|
|
89
|
+
"error": to_serializable(InternalError(message=f"Error creating task: {str(e)}")),
|
|
90
|
+
}
|
|
73
91
|
)
|
|
92
|
+
yield error_result
|
|
74
93
|
return # Early return from generator
|
|
75
94
|
|
|
76
95
|
agent = self._create_agent(agent_name)
|
|
@@ -123,239 +142,81 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
123
142
|
await self._update_store(task_send_params.id, task_status, [artifact])
|
|
124
143
|
|
|
125
144
|
# Yield the artifact update
|
|
126
|
-
yield
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
yield to_serializable(
|
|
146
|
+
SendTaskStreamingResponse(
|
|
147
|
+
id=request.id,
|
|
148
|
+
result=to_serializable(TaskArtifactUpdateEvent(id=task_send_params.id, artifact=artifact)),
|
|
149
|
+
)
|
|
129
150
|
)
|
|
130
151
|
|
|
131
152
|
# Yield the final status update
|
|
132
|
-
yield
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
yield to_serializable(
|
|
154
|
+
SendTaskStreamingResponse(
|
|
155
|
+
id=request.id,
|
|
156
|
+
result=to_serializable(
|
|
157
|
+
TaskStatusUpdateEvent(
|
|
158
|
+
id=task_send_params.id,
|
|
159
|
+
status=to_serializable(TaskStatus(state=task_status.state)),
|
|
160
|
+
final=True,
|
|
161
|
+
)
|
|
162
|
+
),
|
|
163
|
+
)
|
|
139
164
|
)
|
|
140
165
|
return
|
|
141
166
|
|
|
142
167
|
except Exception as e:
|
|
143
168
|
logger.error(f"Error invoking agent: {e}")
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
169
|
+
error_result = to_serializable(
|
|
170
|
+
{
|
|
171
|
+
"id": request.id,
|
|
172
|
+
"error": to_serializable(
|
|
173
|
+
JSONRPCResponse(
|
|
174
|
+
id=request.id,
|
|
175
|
+
error=to_serializable(InternalError(message=f"Error invoking agent: {str(e)}")),
|
|
176
|
+
)
|
|
177
|
+
),
|
|
178
|
+
}
|
|
147
179
|
)
|
|
180
|
+
yield error_result
|
|
148
181
|
return
|
|
149
182
|
|
|
150
183
|
# If streaming is enabled (default), use the streaming implementation
|
|
151
184
|
try:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
for action in item.get("actions", []):
|
|
170
|
-
if "log" in action:
|
|
171
|
-
# Use "text" type for all parts, but add a thought_type in metadata
|
|
172
|
-
parts.append(
|
|
173
|
-
{
|
|
174
|
-
"type": "text",
|
|
175
|
-
"text": action["log"],
|
|
176
|
-
"metadata": {"thought_type": "thought"},
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
if "tool_input" in action:
|
|
180
|
-
# Include SQL queries
|
|
181
|
-
tool_input = action.get("tool_input", "")
|
|
182
|
-
if "$START$" in tool_input and "$STOP$" in tool_input:
|
|
183
|
-
sql = tool_input.replace("$START$", "").replace("$STOP$", "")
|
|
184
|
-
parts.append(
|
|
185
|
-
{
|
|
186
|
-
"type": "text",
|
|
187
|
-
"text": sql,
|
|
188
|
-
"metadata": {"thought_type": "sql"},
|
|
189
|
-
}
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
elif "steps" in item:
|
|
193
|
-
# Extract observations from steps
|
|
194
|
-
thought_dict["type"] = "observation"
|
|
195
|
-
thought_dict["steps"] = item["steps"]
|
|
196
|
-
|
|
197
|
-
for step in item.get("steps", []):
|
|
198
|
-
if "observation" in step:
|
|
199
|
-
parts.append(
|
|
200
|
-
{
|
|
201
|
-
"type": "text",
|
|
202
|
-
"text": step["observation"],
|
|
203
|
-
"metadata": {"thought_type": "observation"},
|
|
204
|
-
}
|
|
205
|
-
)
|
|
206
|
-
if "action" in step and "log" in step["action"]:
|
|
207
|
-
parts.append(
|
|
208
|
-
{
|
|
209
|
-
"type": "text",
|
|
210
|
-
"text": step["action"]["log"],
|
|
211
|
-
"metadata": {"thought_type": "thought"},
|
|
212
|
-
}
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
elif "output" in item:
|
|
216
|
-
# Final answer
|
|
217
|
-
thought_dict["type"] = "answer"
|
|
218
|
-
thought_dict["output"] = item["output"]
|
|
219
|
-
parts.append({"type": "text", "text": item["output"]})
|
|
220
|
-
|
|
221
|
-
elif "parts" in item and item["parts"]:
|
|
222
|
-
# Use existing parts, but ensure they have valid types
|
|
223
|
-
for part in item["parts"]:
|
|
224
|
-
if part.get("type") in ["text", "file", "data"]:
|
|
225
|
-
# Valid type, use as is
|
|
226
|
-
parts.append(part)
|
|
227
|
-
else:
|
|
228
|
-
# Invalid type, convert to text
|
|
229
|
-
text_content = part.get("text", "")
|
|
230
|
-
if not text_content and "content" in part:
|
|
231
|
-
text_content = part["content"]
|
|
232
|
-
|
|
233
|
-
new_part = {"type": "text", "text": text_content}
|
|
234
|
-
|
|
235
|
-
# Preserve metadata if it exists
|
|
236
|
-
if "metadata" in part:
|
|
237
|
-
new_part["metadata"] = part["metadata"]
|
|
238
|
-
else:
|
|
239
|
-
new_part["metadata"] = {"thought_type": part.get("type", "text")}
|
|
240
|
-
|
|
241
|
-
parts.append(new_part)
|
|
242
|
-
|
|
243
|
-
# Try to determine the type from parts for the thought dictionary
|
|
244
|
-
for part in item["parts"]:
|
|
245
|
-
if part.get("type") == "text" and part.get("text", "").startswith("$START$"):
|
|
246
|
-
thought_dict["type"] = "sql"
|
|
247
|
-
thought_dict["query"] = part.get("text")
|
|
248
|
-
else:
|
|
249
|
-
thought_dict["type"] = "text"
|
|
250
|
-
|
|
251
|
-
elif "content" in item:
|
|
252
|
-
# Simple content
|
|
253
|
-
thought_dict["type"] = "text"
|
|
254
|
-
thought_dict["content"] = item["content"]
|
|
255
|
-
parts.append({"type": "text", "text": item["content"]})
|
|
256
|
-
|
|
257
|
-
elif "messages" in item:
|
|
258
|
-
# Extract content from messages
|
|
259
|
-
thought_dict["type"] = "message"
|
|
260
|
-
thought_dict["messages"] = item["messages"]
|
|
261
|
-
|
|
262
|
-
for message in item.get("messages", []):
|
|
263
|
-
if "content" in message:
|
|
264
|
-
parts.append(
|
|
265
|
-
{
|
|
266
|
-
"type": "text",
|
|
267
|
-
"text": message["content"],
|
|
268
|
-
"metadata": {"thought_type": "message"},
|
|
269
|
-
}
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
# Skip if we have no parts to send
|
|
273
|
-
if not parts:
|
|
274
|
-
continue
|
|
275
|
-
|
|
276
|
-
# Process each part individually to ensure true streaming
|
|
277
|
-
for part in parts:
|
|
278
|
-
# Generate a unique key for this part to avoid duplicates
|
|
279
|
-
part_key = str(part)
|
|
280
|
-
if part_key in seen_chunks:
|
|
281
|
-
continue
|
|
282
|
-
seen_chunks.add(part_key)
|
|
283
|
-
|
|
284
|
-
# Ensure metadata exists
|
|
285
|
-
metadata = item.get("metadata", {})
|
|
286
|
-
|
|
287
|
-
# Add the thought dictionary to metadata for frontend parsing
|
|
288
|
-
if thought_dict:
|
|
289
|
-
metadata["thought_process"] = thought_dict
|
|
290
|
-
|
|
291
|
-
# Handle error field if present
|
|
292
|
-
if "error" in item and not is_task_complete:
|
|
293
|
-
logger.warning(f"Error in streaming response: {item['error']}")
|
|
294
|
-
# Mark as complete if there's an error
|
|
295
|
-
is_task_complete = True
|
|
296
|
-
|
|
297
|
-
if not is_task_complete:
|
|
298
|
-
# Create a message with just this part and send it immediately
|
|
299
|
-
task_state = TaskState.WORKING
|
|
300
|
-
message = Message(role="agent", parts=[part], metadata=metadata)
|
|
301
|
-
task_status = TaskStatus(state=task_state, message=message)
|
|
302
|
-
await self._update_store(task_send_params.id, task_status, [])
|
|
303
|
-
task_update_event = TaskStatusUpdateEvent(
|
|
304
|
-
id=task_send_params.id,
|
|
305
|
-
status=task_status,
|
|
306
|
-
final=False,
|
|
307
|
-
)
|
|
308
|
-
yield SendTaskStreamingResponse(id=request.id, result=task_update_event)
|
|
309
|
-
|
|
310
|
-
# If this is the final chunk, send a completion message
|
|
311
|
-
if is_task_complete:
|
|
312
|
-
task_state = TaskState.COMPLETED
|
|
313
|
-
artifact = Artifact(parts=parts, index=0, append=False)
|
|
314
|
-
task_status = TaskStatus(state=task_state)
|
|
315
|
-
yield SendTaskStreamingResponse(
|
|
316
|
-
id=request.id,
|
|
317
|
-
result=TaskArtifactUpdateEvent(id=task_send_params.id, artifact=artifact),
|
|
318
|
-
)
|
|
319
|
-
await self._update_store(task_send_params.id, task_status, [artifact])
|
|
320
|
-
yield SendTaskStreamingResponse(
|
|
321
|
-
id=request.id,
|
|
322
|
-
result=TaskStatusUpdateEvent(
|
|
323
|
-
id=task_send_params.id,
|
|
324
|
-
status=TaskStatus(
|
|
325
|
-
state=task_status.state,
|
|
326
|
-
),
|
|
327
|
-
final=True,
|
|
328
|
-
),
|
|
329
|
-
)
|
|
330
|
-
|
|
185
|
+
logger.debug(f"[TaskManager] Entering agent.stream() at {time.time()}")
|
|
186
|
+
# Transform to agent-compatible format
|
|
187
|
+
agent_messages = to_question_format(
|
|
188
|
+
[
|
|
189
|
+
{
|
|
190
|
+
"role": task_send_params.message.role,
|
|
191
|
+
"parts": task_send_params.message.parts,
|
|
192
|
+
"metadata": task_send_params.message.metadata,
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
)
|
|
196
|
+
async for item in agent.streaming_invoke(agent_messages, timeout=60):
|
|
197
|
+
# Clean up: Remove verbose debug logs, keep only errors and essential info
|
|
198
|
+
if isinstance(item, dict) and "artifact" in item and "parts" in item["artifact"]:
|
|
199
|
+
item["artifact"]["parts"] = [to_serializable(p) for p in item["artifact"]["parts"]]
|
|
200
|
+
yield to_serializable(item)
|
|
331
201
|
except Exception as e:
|
|
332
202
|
logger.error(f"An error occurred while streaming the response: {e}")
|
|
333
203
|
error_text = f"An error occurred while streaming the response: {str(e)}"
|
|
204
|
+
# Ensure all parts are plain dicts
|
|
334
205
|
parts = [{"type": "text", "text": error_text}]
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
yield SendTaskStreamingResponse(
|
|
350
|
-
id=request.id,
|
|
351
|
-
result=TaskStatusUpdateEvent(
|
|
352
|
-
id=task_send_params.id,
|
|
353
|
-
status=TaskStatus(
|
|
354
|
-
state=task_status.state,
|
|
355
|
-
),
|
|
356
|
-
final=True,
|
|
357
|
-
),
|
|
358
|
-
)
|
|
206
|
+
parts = [to_serializable(part) for part in parts]
|
|
207
|
+
artifact = {
|
|
208
|
+
"parts": parts,
|
|
209
|
+
"index": 0,
|
|
210
|
+
"append": False,
|
|
211
|
+
}
|
|
212
|
+
error_result = {
|
|
213
|
+
"id": request.id,
|
|
214
|
+
"error": {
|
|
215
|
+
"id": task_send_params.id,
|
|
216
|
+
"artifact": artifact,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
yield error_result
|
|
359
220
|
|
|
360
221
|
async def upsert_task(self, task_send_params: TaskSendParams) -> Task:
|
|
361
222
|
"""Create or update a task in the task store.
|
|
@@ -472,21 +333,26 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
472
333
|
) -> AsyncIterable[SendTaskStreamingResponse]:
|
|
473
334
|
error = self._validate_request(request)
|
|
474
335
|
if error:
|
|
475
|
-
|
|
476
|
-
yield SendTaskStreamingResponse(id=request.id, error=error.error)
|
|
336
|
+
logger.info(f"[TaskManager] Yielding error at {time.time()} for invalid request: {error}")
|
|
337
|
+
yield to_serializable(SendTaskStreamingResponse(id=request.id, error=to_serializable(error.error)))
|
|
477
338
|
return
|
|
478
339
|
|
|
479
340
|
# We can't await an async generator directly, so we need to use it as is
|
|
480
341
|
try:
|
|
342
|
+
logger.debug(f"[TaskManager] Entering streaming path at {time.time()}")
|
|
481
343
|
async for response in self._stream_generator(request):
|
|
344
|
+
logger.debug(f"[TaskManager] Yielding streaming response at {time.time()} with: {str(response)[:120]}")
|
|
482
345
|
yield response
|
|
483
346
|
except Exception as e:
|
|
484
347
|
# If an error occurs, yield an error response
|
|
485
348
|
logger.error(f"Error in on_send_task_subscribe: {str(e)}")
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
349
|
+
error_result = to_serializable(
|
|
350
|
+
{
|
|
351
|
+
"id": request.id,
|
|
352
|
+
"error": to_serializable(InternalError(message=f"Error processing streaming request: {str(e)}")),
|
|
353
|
+
}
|
|
489
354
|
)
|
|
355
|
+
yield error_result
|
|
490
356
|
|
|
491
357
|
async def _update_store(self, task_id: str, status: TaskStatus, artifacts: list[Artifact]) -> Task:
|
|
492
358
|
async with self.lock:
|
|
@@ -579,7 +445,7 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
579
445
|
# Just create a minimal response to acknowledge the request
|
|
580
446
|
task_state = TaskState.WORKING
|
|
581
447
|
task = await self._update_store(task_send_params.id, TaskStatus(state=task_state), [])
|
|
582
|
-
return SendTaskResponse(id=request.id, result=task)
|
|
448
|
+
return to_serializable(SendTaskResponse(id=request.id, result=task))
|
|
583
449
|
else:
|
|
584
450
|
# For non-streaming mode, collect all chunks into a single response
|
|
585
451
|
async for chunk in stream_gen:
|
|
@@ -607,7 +473,7 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
607
473
|
),
|
|
608
474
|
[Artifact(parts=all_parts)],
|
|
609
475
|
)
|
|
610
|
-
return SendTaskResponse(id=request.id, result=task)
|
|
476
|
+
return to_serializable(SendTaskResponse(id=request.id, result=task))
|
|
611
477
|
except Exception as e:
|
|
612
478
|
logger.error(f"Error invoking agent: {e}")
|
|
613
479
|
result_text = f"Error invoking agent: {e}"
|
|
@@ -619,4 +485,4 @@ class AgentTaskManager(InMemoryTaskManager):
|
|
|
619
485
|
TaskStatus(state=task_state, message=Message(role="agent", parts=parts)),
|
|
620
486
|
[Artifact(parts=parts)],
|
|
621
487
|
)
|
|
622
|
-
return SendTaskResponse(id=request.id, result=task)
|
|
488
|
+
return to_serializable(SendTaskResponse(id=request.id, result=task))
|
mindsdb/api/a2a/utils.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
def to_serializable(obj):
|
|
2
|
+
# Primitives
|
|
3
|
+
if isinstance(obj, (str, int, float, bool, type(None))):
|
|
4
|
+
return obj
|
|
5
|
+
# Pydantic v2
|
|
6
|
+
if hasattr(obj, "model_dump"):
|
|
7
|
+
return to_serializable(obj.model_dump(exclude_none=True))
|
|
8
|
+
# Pydantic v1
|
|
9
|
+
if hasattr(obj, "dict"):
|
|
10
|
+
return to_serializable(obj.dict(exclude_none=True))
|
|
11
|
+
# Custom classes with __dict__
|
|
12
|
+
if hasattr(obj, "__dict__"):
|
|
13
|
+
return {k: to_serializable(v) for k, v in vars(obj).items() if not k.startswith("_")}
|
|
14
|
+
# Dicts
|
|
15
|
+
if isinstance(obj, dict):
|
|
16
|
+
return {k: to_serializable(v) for k, v in obj.items()}
|
|
17
|
+
# Lists, Tuples, Sets
|
|
18
|
+
if isinstance(obj, (list, tuple, set)):
|
|
19
|
+
return [to_serializable(v) for v in obj]
|
|
20
|
+
# Fallback: string
|
|
21
|
+
return str(obj)
|
|
@@ -6,13 +6,14 @@ from duckdb import InvalidInputException
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
8
|
from mindsdb_sql_parser import parse_sql
|
|
9
|
-
from mindsdb.utilities.render.sqlalchemy_render import SqlalchemyRender
|
|
10
|
-
from mindsdb.integrations.utilities.query_traversal import query_traversal
|
|
11
9
|
from mindsdb_sql_parser.ast import ASTNode, Select, Identifier, Function, Constant
|
|
12
|
-
from mindsdb.utilities.functions import resolve_table_identifier, resolve_model_identifier
|
|
13
10
|
|
|
11
|
+
from mindsdb.integrations.utilities.query_traversal import query_traversal
|
|
14
12
|
from mindsdb.utilities import log
|
|
13
|
+
from mindsdb.utilities.exception import format_db_error_message
|
|
14
|
+
from mindsdb.utilities.functions import resolve_table_identifier, resolve_model_identifier
|
|
15
15
|
from mindsdb.utilities.json_encoder import CustomJSONEncoder
|
|
16
|
+
from mindsdb.utilities.render.sqlalchemy_render import SqlalchemyRender
|
|
16
17
|
|
|
17
18
|
logger = log.getLogger(__name__)
|
|
18
19
|
|
|
@@ -64,29 +65,85 @@ def query_df_with_type_infer_fallback(query_str: str, dataframes: dict, user_fun
|
|
|
64
65
|
pandas.columns
|
|
65
66
|
"""
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
user_functions
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
try:
|
|
69
|
+
with duckdb.connect(database=":memory:") as con:
|
|
70
|
+
if user_functions:
|
|
71
|
+
user_functions.register(con)
|
|
72
|
+
|
|
73
|
+
for name, value in dataframes.items():
|
|
74
|
+
con.register(name, value)
|
|
75
|
+
|
|
76
|
+
exception = None
|
|
77
|
+
for sample_size in [1000, 10000, 1000000]:
|
|
78
|
+
try:
|
|
79
|
+
con.execute(f"set global pandas_analyze_sample={sample_size};")
|
|
80
|
+
result_df = con.execute(query_str).fetchdf()
|
|
81
|
+
except InvalidInputException as e:
|
|
82
|
+
exception = e
|
|
83
|
+
else:
|
|
84
|
+
break
|
|
81
85
|
else:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
raise exception
|
|
87
|
+
description = con.description
|
|
88
|
+
except Exception as e:
|
|
89
|
+
raise Exception(
|
|
90
|
+
format_db_error_message(db_type="DuckDB", db_error_msg=str(e), failed_query=query_str, is_external=False)
|
|
91
|
+
) from e
|
|
86
92
|
|
|
87
93
|
return result_df, description
|
|
88
94
|
|
|
89
95
|
|
|
96
|
+
_duckdb_functions_and_kw_list = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_duckdb_functions_and_kw_list() -> list[str] | None:
|
|
100
|
+
"""Returns a list of all functions and keywords supported by DuckDB.
|
|
101
|
+
The list is merge of:
|
|
102
|
+
- list of duckdb's functions: 'select * from duckdb_functions()' or 'pragma functions'
|
|
103
|
+
- ist of keywords, because of some functions are just sintax-sugar
|
|
104
|
+
and not present in the duckdb_functions (like 'if()').
|
|
105
|
+
- hardcoded list of window_functions, because there are no way to get if from duckdb,
|
|
106
|
+
and they are not present in the duckdb_functions()
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
list[str] | None: List of supported functions and keywords, or None if unable to retrieve the list.
|
|
110
|
+
"""
|
|
111
|
+
global _duckdb_functions_and_kw_list
|
|
112
|
+
window_functions_list = [
|
|
113
|
+
"cume_dist",
|
|
114
|
+
"dense_rank",
|
|
115
|
+
"first_value",
|
|
116
|
+
"lag",
|
|
117
|
+
"last_value",
|
|
118
|
+
"lead",
|
|
119
|
+
"nth_value",
|
|
120
|
+
"ntile",
|
|
121
|
+
"percent_rank",
|
|
122
|
+
"rank_dense",
|
|
123
|
+
"rank",
|
|
124
|
+
"row_number",
|
|
125
|
+
]
|
|
126
|
+
if _duckdb_functions_and_kw_list is None:
|
|
127
|
+
try:
|
|
128
|
+
df, _ = query_df_with_type_infer_fallback(
|
|
129
|
+
"""
|
|
130
|
+
select distinct name
|
|
131
|
+
from (
|
|
132
|
+
select function_name as name from duckdb_functions()
|
|
133
|
+
union all
|
|
134
|
+
select keyword_name as name from duckdb_keywords()
|
|
135
|
+
) ta;
|
|
136
|
+
""",
|
|
137
|
+
dataframes={},
|
|
138
|
+
)
|
|
139
|
+
df.columns = [name.lower() for name in df.columns]
|
|
140
|
+
_duckdb_functions_and_kw_list = df["name"].drop_duplicates().str.lower().to_list() + window_functions_list
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning(f"Unable to get DuckDB functions list: {e}")
|
|
143
|
+
|
|
144
|
+
return _duckdb_functions_and_kw_list
|
|
145
|
+
|
|
146
|
+
|
|
90
147
|
def query_df(df, query, session=None):
|
|
91
148
|
"""Perform simple query ('select' from one table, without subqueries and joins) on DataFrame.
|
|
92
149
|
|
|
@@ -100,8 +157,10 @@ def query_df(df, query, session=None):
|
|
|
100
157
|
|
|
101
158
|
if isinstance(query, str):
|
|
102
159
|
query_ast = parse_sql(query)
|
|
160
|
+
query_str = query
|
|
103
161
|
else:
|
|
104
162
|
query_ast = copy.deepcopy(query)
|
|
163
|
+
query_str = str(query)
|
|
105
164
|
|
|
106
165
|
if isinstance(query_ast, Select) is False or isinstance(query_ast.from_table, Identifier) is False:
|
|
107
166
|
raise Exception("Only 'SELECT from TABLE' statements supported for internal query")
|
|
@@ -125,6 +184,7 @@ def query_df(df, query, session=None):
|
|
|
125
184
|
return node
|
|
126
185
|
if isinstance(node, Function):
|
|
127
186
|
fnc_name = node.op.lower()
|
|
187
|
+
|
|
128
188
|
if fnc_name == "database" and len(node.args) == 0:
|
|
129
189
|
if session is not None:
|
|
130
190
|
cur_db = session.database
|
|
@@ -142,6 +202,22 @@ def query_df(df, query, session=None):
|
|
|
142
202
|
if user_functions is not None:
|
|
143
203
|
user_functions.check_function(node)
|
|
144
204
|
|
|
205
|
+
duckdb_functions_and_kw_list = get_duckdb_functions_and_kw_list() or []
|
|
206
|
+
custom_functions_list = [] if user_functions is None else list(user_functions.functions.keys())
|
|
207
|
+
all_functions_list = duckdb_functions_and_kw_list + custom_functions_list
|
|
208
|
+
if len(all_functions_list) > 0 and fnc_name not in all_functions_list:
|
|
209
|
+
raise Exception(
|
|
210
|
+
format_db_error_message(
|
|
211
|
+
db_type="DuckDB",
|
|
212
|
+
db_error_msg=(
|
|
213
|
+
f"Unknown function: '{fnc_name}'. This function is not recognized during internal query processing.\n"
|
|
214
|
+
"Please use DuckDB-supported functions instead."
|
|
215
|
+
),
|
|
216
|
+
failed_query=query_str,
|
|
217
|
+
is_external=False,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
145
221
|
query_traversal(query_ast, adapt_query)
|
|
146
222
|
|
|
147
223
|
# convert json columns
|