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
mindsdb/__about__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
__title__ = "MindsDB"
|
|
2
2
|
__package_name__ = "mindsdb"
|
|
3
|
-
__version__ = "25.7.
|
|
3
|
+
__version__ = "25.7.2.0"
|
|
4
4
|
__description__ = "MindsDB's AI SQL Server enables developers to build AI tools that need access to real-time data to perform their tasks"
|
|
5
5
|
__email__ = "jorge@mindsdb.com"
|
|
6
6
|
__author__ = "MindsDB Inc"
|
mindsdb/__main__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import gc
|
|
2
|
+
|
|
2
3
|
gc.disable()
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
@@ -105,9 +106,7 @@ class TrunkProcessData:
|
|
|
105
106
|
self._restarts_time.append(current_time_seconds)
|
|
106
107
|
if self.max_restart_interval_seconds > 0:
|
|
107
108
|
self._restarts_time = [
|
|
108
|
-
x
|
|
109
|
-
for x in self._restarts_time
|
|
110
|
-
if x >= (current_time_seconds - self.max_restart_interval_seconds)
|
|
109
|
+
x for x in self._restarts_time if x >= (current_time_seconds - self.max_restart_interval_seconds)
|
|
111
110
|
]
|
|
112
111
|
if len(self._restarts_time) > self.max_restart_count:
|
|
113
112
|
return False
|
|
@@ -124,16 +123,11 @@ class TrunkProcessData:
|
|
|
124
123
|
if config.is_cloud:
|
|
125
124
|
return False
|
|
126
125
|
if sys.platform in ("linux", "darwin"):
|
|
127
|
-
return
|
|
128
|
-
self.restart_on_failure
|
|
129
|
-
and self.process.exitcode == -signal.SIGKILL.value
|
|
130
|
-
)
|
|
126
|
+
return self.restart_on_failure and self.process.exitcode == -signal.SIGKILL.value
|
|
131
127
|
else:
|
|
132
128
|
if self.max_restart_count == 0:
|
|
133
129
|
# to prevent infinity restarts, max_restart_count should be > 0
|
|
134
|
-
logger.warning(
|
|
135
|
-
"In the current OS, it is not possible to use `max_restart_count=0`"
|
|
136
|
-
)
|
|
130
|
+
logger.warning("In the current OS, it is not possible to use `max_restart_count=0`")
|
|
137
131
|
return False
|
|
138
132
|
return self.restart_on_failure
|
|
139
133
|
|
|
@@ -174,24 +168,18 @@ def set_error_model_status_by_pids(unexisting_pids: List[int]):
|
|
|
174
168
|
db.session.query(db.Predictor)
|
|
175
169
|
.filter(
|
|
176
170
|
db.Predictor.deleted_at.is_(None),
|
|
177
|
-
db.Predictor.status.not_in(
|
|
178
|
-
[db.PREDICTOR_STATUS.COMPLETE, db.PREDICTOR_STATUS.ERROR]
|
|
179
|
-
),
|
|
171
|
+
db.Predictor.status.not_in([db.PREDICTOR_STATUS.COMPLETE, db.PREDICTOR_STATUS.ERROR]),
|
|
180
172
|
)
|
|
181
173
|
.all()
|
|
182
174
|
)
|
|
183
175
|
for predictor_record in predictor_records:
|
|
184
|
-
predictor_process_id = (predictor_record.training_metadata or {}).get(
|
|
185
|
-
"process_id"
|
|
186
|
-
)
|
|
176
|
+
predictor_process_id = (predictor_record.training_metadata or {}).get("process_id")
|
|
187
177
|
if predictor_process_id in unexisting_pids:
|
|
188
178
|
predictor_record.status = db.PREDICTOR_STATUS.ERROR
|
|
189
179
|
if isinstance(predictor_record.data, dict) is False:
|
|
190
180
|
predictor_record.data = {}
|
|
191
181
|
if "error" not in predictor_record.data:
|
|
192
|
-
predictor_record.data["error"] =
|
|
193
|
-
"The training process was terminated for unknown reasons"
|
|
194
|
-
)
|
|
182
|
+
predictor_record.data["error"] = "The training process was terminated for unknown reasons"
|
|
195
183
|
flag_modified(predictor_record, "data")
|
|
196
184
|
db.session.commit()
|
|
197
185
|
|
|
@@ -204,9 +192,7 @@ def set_error_model_status_for_unfinished():
|
|
|
204
192
|
db.session.query(db.Predictor)
|
|
205
193
|
.filter(
|
|
206
194
|
db.Predictor.deleted_at.is_(None),
|
|
207
|
-
db.Predictor.status.not_in(
|
|
208
|
-
[db.PREDICTOR_STATUS.COMPLETE, db.PREDICTOR_STATUS.ERROR]
|
|
209
|
-
),
|
|
195
|
+
db.Predictor.status.not_in([db.PREDICTOR_STATUS.COMPLETE, db.PREDICTOR_STATUS.ERROR]),
|
|
210
196
|
)
|
|
211
197
|
.all()
|
|
212
198
|
)
|
|
@@ -234,11 +220,7 @@ def create_permanent_integrations():
|
|
|
234
220
|
NOTE: this is intentional to avoid importing integration_controller
|
|
235
221
|
"""
|
|
236
222
|
integration_name = "files"
|
|
237
|
-
existing = (
|
|
238
|
-
db.session.query(db.Integration)
|
|
239
|
-
.filter_by(name=integration_name, company_id=None)
|
|
240
|
-
.first()
|
|
241
|
-
)
|
|
223
|
+
existing = db.session.query(db.Integration).filter_by(name=integration_name, company_id=None).first()
|
|
242
224
|
if existing is None:
|
|
243
225
|
integration_record = db.Integration(
|
|
244
226
|
name=integration_name,
|
|
@@ -250,9 +232,7 @@ def create_permanent_integrations():
|
|
|
250
232
|
try:
|
|
251
233
|
db.session.commit()
|
|
252
234
|
except Exception as e:
|
|
253
|
-
logger.error(
|
|
254
|
-
f"Failed to commit permanent integration {integration_name}: {e}"
|
|
255
|
-
)
|
|
235
|
+
logger.error(f"Failed to commit permanent integration {integration_name}: {e}")
|
|
256
236
|
db.session.rollback()
|
|
257
237
|
|
|
258
238
|
|
|
@@ -278,9 +258,7 @@ def validate_default_project() -> None:
|
|
|
278
258
|
func.lower(db.Project.name) == func.lower(new_default_project_name),
|
|
279
259
|
).first()
|
|
280
260
|
if existing_project is None:
|
|
281
|
-
logger.critical(
|
|
282
|
-
f"A project with the name '{new_default_project_name}' does not exist"
|
|
283
|
-
)
|
|
261
|
+
logger.critical(f"A project with the name '{new_default_project_name}' does not exist")
|
|
284
262
|
sys.exit(1)
|
|
285
263
|
|
|
286
264
|
existing_project.metadata_ = {"is_default": True}
|
|
@@ -293,9 +271,7 @@ def validate_default_project() -> None:
|
|
|
293
271
|
func.lower(db.Project.name) == func.lower(new_default_project_name),
|
|
294
272
|
).first()
|
|
295
273
|
if existing_project is not None:
|
|
296
|
-
logger.critical(
|
|
297
|
-
f"A project with the name '{new_default_project_name}' already exists"
|
|
298
|
-
)
|
|
274
|
+
logger.critical(f"A project with the name '{new_default_project_name}' already exists")
|
|
299
275
|
sys.exit(1)
|
|
300
276
|
current_default_project.name = new_default_project_name
|
|
301
277
|
db.session.commit()
|
|
@@ -317,9 +293,7 @@ def start_process(trunc_process_data: TrunkProcessData) -> None:
|
|
|
317
293
|
)
|
|
318
294
|
trunc_process_data.process.start()
|
|
319
295
|
except Exception as e:
|
|
320
|
-
logger.error(
|
|
321
|
-
f"Failed to start {trunc_process_data.name} API with exception {e}\n{traceback.format_exc()}"
|
|
322
|
-
)
|
|
296
|
+
logger.error(f"Failed to start {trunc_process_data.name} API with exception {e}\n{traceback.format_exc()}")
|
|
323
297
|
close_api_gracefully(trunc_processes_struct)
|
|
324
298
|
raise e
|
|
325
299
|
|
|
@@ -329,8 +303,7 @@ if __name__ == "__main__":
|
|
|
329
303
|
# warn if less than 1Gb of free RAM
|
|
330
304
|
if psutil.virtual_memory().available < (1 << 30):
|
|
331
305
|
logger.warning(
|
|
332
|
-
"The system is running low on memory. "
|
|
333
|
-
+ "This may impact the stability and performance of the program."
|
|
306
|
+
"The system is running low on memory. " + "This may impact the stability and performance of the program."
|
|
334
307
|
)
|
|
335
308
|
|
|
336
309
|
ctx.set_default()
|
|
@@ -440,94 +413,86 @@ if __name__ == "__main__":
|
|
|
440
413
|
mysql_api_config = config.get("api", {}).get("mysql", {})
|
|
441
414
|
mcp_api_config = config.get("api", {}).get("mcp", {})
|
|
442
415
|
litellm_api_config = config.get("api", {}).get("litellm", {})
|
|
443
|
-
a2a_api_config = config.get("a2a", {})
|
|
416
|
+
a2a_api_config = config.get("api", {}).get("a2a", {})
|
|
444
417
|
trunc_processes_struct = {
|
|
445
418
|
TrunkProcessEnum.HTTP: TrunkProcessData(
|
|
446
419
|
name=TrunkProcessEnum.HTTP.value,
|
|
447
420
|
entrypoint=start_http,
|
|
448
|
-
port=http_api_config[
|
|
421
|
+
port=http_api_config["port"],
|
|
449
422
|
args=(config.cmd_args.verbose, config.cmd_args.no_studio),
|
|
450
|
-
restart_on_failure=http_api_config.get(
|
|
451
|
-
max_restart_count=http_api_config.get(
|
|
423
|
+
restart_on_failure=http_api_config.get("restart_on_failure", False),
|
|
424
|
+
max_restart_count=http_api_config.get("max_restart_count", TrunkProcessData.max_restart_count),
|
|
452
425
|
max_restart_interval_seconds=http_api_config.get(
|
|
453
|
-
|
|
454
|
-
)
|
|
426
|
+
"max_restart_interval_seconds", TrunkProcessData.max_restart_interval_seconds
|
|
427
|
+
),
|
|
455
428
|
),
|
|
456
429
|
TrunkProcessEnum.MYSQL: TrunkProcessData(
|
|
457
430
|
name=TrunkProcessEnum.MYSQL.value,
|
|
458
431
|
entrypoint=start_mysql,
|
|
459
|
-
port=mysql_api_config[
|
|
432
|
+
port=mysql_api_config["port"],
|
|
460
433
|
args=(config.cmd_args.verbose,),
|
|
461
|
-
restart_on_failure=mysql_api_config.get(
|
|
462
|
-
max_restart_count=mysql_api_config.get(
|
|
434
|
+
restart_on_failure=mysql_api_config.get("restart_on_failure", False),
|
|
435
|
+
max_restart_count=mysql_api_config.get("max_restart_count", TrunkProcessData.max_restart_count),
|
|
463
436
|
max_restart_interval_seconds=mysql_api_config.get(
|
|
464
|
-
|
|
465
|
-
)
|
|
437
|
+
"max_restart_interval_seconds", TrunkProcessData.max_restart_interval_seconds
|
|
438
|
+
),
|
|
466
439
|
),
|
|
467
440
|
TrunkProcessEnum.MONGODB: TrunkProcessData(
|
|
468
441
|
name=TrunkProcessEnum.MONGODB.value,
|
|
469
442
|
entrypoint=start_mongo,
|
|
470
|
-
port=config[
|
|
471
|
-
args=(config.cmd_args.verbose,)
|
|
443
|
+
port=config["api"]["mongodb"]["port"],
|
|
444
|
+
args=(config.cmd_args.verbose,),
|
|
472
445
|
),
|
|
473
446
|
TrunkProcessEnum.POSTGRES: TrunkProcessData(
|
|
474
447
|
name=TrunkProcessEnum.POSTGRES.value,
|
|
475
448
|
entrypoint=start_postgres,
|
|
476
|
-
port=config[
|
|
477
|
-
args=(config.cmd_args.verbose,)
|
|
449
|
+
port=config["api"]["postgres"]["port"],
|
|
450
|
+
args=(config.cmd_args.verbose,),
|
|
478
451
|
),
|
|
479
452
|
TrunkProcessEnum.JOBS: TrunkProcessData(
|
|
480
|
-
name=TrunkProcessEnum.JOBS.value,
|
|
481
|
-
entrypoint=start_scheduler,
|
|
482
|
-
args=(config.cmd_args.verbose,)
|
|
453
|
+
name=TrunkProcessEnum.JOBS.value, entrypoint=start_scheduler, args=(config.cmd_args.verbose,)
|
|
483
454
|
),
|
|
484
455
|
TrunkProcessEnum.TASKS: TrunkProcessData(
|
|
485
|
-
name=TrunkProcessEnum.TASKS.value,
|
|
486
|
-
entrypoint=start_tasks,
|
|
487
|
-
args=(config.cmd_args.verbose,)
|
|
456
|
+
name=TrunkProcessEnum.TASKS.value, entrypoint=start_tasks, args=(config.cmd_args.verbose,)
|
|
488
457
|
),
|
|
489
458
|
TrunkProcessEnum.ML_TASK_QUEUE: TrunkProcessData(
|
|
490
|
-
name=TrunkProcessEnum.ML_TASK_QUEUE.value,
|
|
491
|
-
entrypoint=start_ml_task_queue,
|
|
492
|
-
args=(config.cmd_args.verbose,)
|
|
459
|
+
name=TrunkProcessEnum.ML_TASK_QUEUE.value, entrypoint=start_ml_task_queue, args=(config.cmd_args.verbose,)
|
|
493
460
|
),
|
|
494
461
|
TrunkProcessEnum.MCP: TrunkProcessData(
|
|
495
462
|
name=TrunkProcessEnum.MCP.value,
|
|
496
463
|
entrypoint=start_mcp,
|
|
497
|
-
port=mcp_api_config.get(
|
|
464
|
+
port=mcp_api_config.get("port", 47337),
|
|
498
465
|
args=(config.cmd_args.verbose,),
|
|
499
|
-
need_to_run=mcp_api_config.get(
|
|
500
|
-
restart_on_failure=mcp_api_config.get(
|
|
501
|
-
max_restart_count=mcp_api_config.get(
|
|
466
|
+
need_to_run=mcp_api_config.get("need_to_run", False),
|
|
467
|
+
restart_on_failure=mcp_api_config.get("restart_on_failure", False),
|
|
468
|
+
max_restart_count=mcp_api_config.get("max_restart_count", TrunkProcessData.max_restart_count),
|
|
502
469
|
max_restart_interval_seconds=mcp_api_config.get(
|
|
503
|
-
|
|
504
|
-
)
|
|
470
|
+
"max_restart_interval_seconds", TrunkProcessData.max_restart_interval_seconds
|
|
471
|
+
),
|
|
505
472
|
),
|
|
506
473
|
TrunkProcessEnum.LITELLM: TrunkProcessData(
|
|
507
474
|
name=TrunkProcessEnum.LITELLM.value,
|
|
508
475
|
entrypoint=start_litellm,
|
|
509
|
-
port=litellm_api_config.get(
|
|
476
|
+
port=litellm_api_config.get("port", 8000),
|
|
510
477
|
args=(config.cmd_args.verbose,),
|
|
511
|
-
restart_on_failure=litellm_api_config.get(
|
|
512
|
-
max_restart_count=litellm_api_config.get(
|
|
478
|
+
restart_on_failure=litellm_api_config.get("restart_on_failure", False),
|
|
479
|
+
max_restart_count=litellm_api_config.get("max_restart_count", TrunkProcessData.max_restart_count),
|
|
513
480
|
max_restart_interval_seconds=litellm_api_config.get(
|
|
514
|
-
|
|
515
|
-
)
|
|
481
|
+
"max_restart_interval_seconds", TrunkProcessData.max_restart_interval_seconds
|
|
482
|
+
),
|
|
516
483
|
),
|
|
517
484
|
TrunkProcessEnum.A2A: TrunkProcessData(
|
|
518
485
|
name=TrunkProcessEnum.A2A.value,
|
|
519
486
|
entrypoint=start_a2a,
|
|
520
|
-
port=a2a_api_config.get(
|
|
487
|
+
port=a2a_api_config.get("port", 8001),
|
|
521
488
|
args=(config.cmd_args.verbose,),
|
|
522
|
-
need_to_run=a2a_api_config.get(
|
|
523
|
-
restart_on_failure=a2a_api_config.get(
|
|
524
|
-
max_restart_count=a2a_api_config.get(
|
|
525
|
-
'max_restart_count', TrunkProcessData.max_restart_count
|
|
526
|
-
),
|
|
489
|
+
need_to_run=a2a_api_config.get("enabled", False),
|
|
490
|
+
restart_on_failure=a2a_api_config.get("restart_on_failure", True),
|
|
491
|
+
max_restart_count=a2a_api_config.get("max_restart_count", TrunkProcessData.max_restart_count),
|
|
527
492
|
max_restart_interval_seconds=a2a_api_config.get(
|
|
528
|
-
|
|
529
|
-
)
|
|
530
|
-
)
|
|
493
|
+
"max_restart_interval_seconds", TrunkProcessData.max_restart_interval_seconds
|
|
494
|
+
),
|
|
495
|
+
),
|
|
531
496
|
}
|
|
532
497
|
|
|
533
498
|
for api_enum in api_arr:
|
|
@@ -546,10 +511,7 @@ if __name__ == "__main__":
|
|
|
546
511
|
trunc_processes_struct[TrunkProcessEnum.ML_TASK_QUEUE].need_to_run = True
|
|
547
512
|
|
|
548
513
|
for trunc_process_data in trunc_processes_struct.values():
|
|
549
|
-
if
|
|
550
|
-
trunc_process_data.started is True
|
|
551
|
-
or trunc_process_data.need_to_run is False
|
|
552
|
-
):
|
|
514
|
+
if trunc_process_data.started is True or trunc_process_data.need_to_run is False:
|
|
553
515
|
continue
|
|
554
516
|
start_process(trunc_process_data)
|
|
555
517
|
|
|
@@ -572,8 +534,7 @@ if __name__ == "__main__":
|
|
|
572
534
|
trunc_process_data.port,
|
|
573
535
|
)
|
|
574
536
|
for trunc_process_data in trunc_processes_struct.values()
|
|
575
|
-
if trunc_process_data.port is not None
|
|
576
|
-
and trunc_process_data.need_to_run is True
|
|
537
|
+
if trunc_process_data.port is not None and trunc_process_data.need_to_run is True
|
|
577
538
|
]
|
|
578
539
|
for future in asyncio.as_completed(futures):
|
|
579
540
|
api_name, port, started = await future
|
|
@@ -596,9 +557,7 @@ if __name__ == "__main__":
|
|
|
596
557
|
finally:
|
|
597
558
|
if trunc_process_data.should_restart:
|
|
598
559
|
if trunc_process_data.request_restart_attempt():
|
|
599
|
-
logger.warning(
|
|
600
|
-
f"{trunc_process_data.name} API: stopped unexpectedly, restarting"
|
|
601
|
-
)
|
|
560
|
+
logger.warning(f"{trunc_process_data.name} API: stopped unexpectedly, restarting")
|
|
602
561
|
trunc_process_data.process = None
|
|
603
562
|
if trunc_process_data.name == TrunkProcessEnum.HTTP.value:
|
|
604
563
|
# do not open GUI on HTTP API restart
|
mindsdb/api/a2a/agent.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import Any, AsyncIterable, Dict, List
|
|
2
|
+
from typing import Any, AsyncIterable, Dict, List
|
|
3
3
|
import requests
|
|
4
4
|
import logging
|
|
5
|
-
|
|
5
|
+
import httpx
|
|
6
|
+
from mindsdb.api.a2a.utils import to_serializable
|
|
6
7
|
from mindsdb.api.a2a.constants import DEFAULT_STREAM_TIMEOUT
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
@@ -11,9 +12,6 @@ logger = logging.getLogger(__name__)
|
|
|
11
12
|
class MindsDBAgent:
|
|
12
13
|
"""An agent that communicates with MindsDB over HTTP following the A2A protocol."""
|
|
13
14
|
|
|
14
|
-
# Supported content-types according to A2A spec. We include both the
|
|
15
|
-
# mime-type form and the simple "text" token so that clients using either
|
|
16
|
-
# convention succeed.
|
|
17
15
|
SUPPORTED_CONTENT_TYPES = ["text", "text/plain", "application/json"]
|
|
18
16
|
|
|
19
17
|
def __init__(
|
|
@@ -35,31 +33,15 @@ class MindsDBAgent:
|
|
|
35
33
|
def invoke(self, query, session_id) -> Dict[str, Any]:
|
|
36
34
|
"""Send a query to the MindsDB agent using SQL API."""
|
|
37
35
|
try:
|
|
38
|
-
# Escape single quotes in the query for SQL
|
|
39
36
|
escaped_query = query.replace("'", "''")
|
|
40
|
-
|
|
41
|
-
# Build the SQL query to the agent
|
|
42
37
|
sql_query = f"SELECT * FROM {self.project_name}.{self.agent_name} WHERE question = '{escaped_query}'"
|
|
43
|
-
|
|
44
|
-
# Log request for debugging
|
|
45
38
|
logger.info(f"Sending SQL query to MindsDB: {sql_query[:100]}...")
|
|
46
|
-
|
|
47
|
-
# Send the request to MindsDB SQL API
|
|
48
39
|
response = requests.post(self.sql_url, json={"query": sql_query})
|
|
49
40
|
response.raise_for_status()
|
|
50
|
-
|
|
51
|
-
# Process the response
|
|
52
41
|
data = response.json()
|
|
53
|
-
|
|
54
|
-
# Log the response for debugging
|
|
55
42
|
logger.debug(f"Received response from MindsDB: {json.dumps(data)[:200]}...")
|
|
56
|
-
|
|
57
43
|
if "data" in data and len(data["data"]) > 0:
|
|
58
|
-
# The result should be in the first row
|
|
59
44
|
result_row = data["data"][0]
|
|
60
|
-
|
|
61
|
-
# Find the response column (might be 'response', 'answer', 'result', etc.)
|
|
62
|
-
# Try common column names or just return all content
|
|
63
45
|
for column in ["response", "result", "answer", "completion", "output"]:
|
|
64
46
|
if column in result_row:
|
|
65
47
|
content = result_row[column]
|
|
@@ -68,19 +50,9 @@ class MindsDBAgent:
|
|
|
68
50
|
"content": content,
|
|
69
51
|
"parts": [{"type": "text", "text": content}],
|
|
70
52
|
}
|
|
71
|
-
|
|
72
|
-
# If no specific column found, return the whole row as JSON
|
|
73
53
|
logger.info("No specific result column found, returning full row")
|
|
74
54
|
content = json.dumps(result_row, indent=2)
|
|
75
|
-
|
|
76
|
-
# Return structured data only if it is a dictionary (A2A `data` part
|
|
77
|
-
# must itself be a JSON object). In some cases MindsDB may return a
|
|
78
|
-
# list (for instance a list of rows or records). If that happens we
|
|
79
|
-
# downgrade it to plain-text to avoid schema-validation errors on the
|
|
80
|
-
# A2A side.
|
|
81
|
-
|
|
82
|
-
parts: List[dict] = [{"type": "text", "text": content}]
|
|
83
|
-
|
|
55
|
+
parts = [{"type": "text", "text": content}]
|
|
84
56
|
if isinstance(result_row, dict):
|
|
85
57
|
parts.append(
|
|
86
58
|
{
|
|
@@ -89,7 +61,6 @@ class MindsDBAgent:
|
|
|
89
61
|
"metadata": {"subtype": "json"},
|
|
90
62
|
}
|
|
91
63
|
)
|
|
92
|
-
|
|
93
64
|
return {
|
|
94
65
|
"content": content,
|
|
95
66
|
"parts": parts,
|
|
@@ -101,7 +72,6 @@ class MindsDBAgent:
|
|
|
101
72
|
"content": error_msg,
|
|
102
73
|
"parts": [{"type": "text", "text": error_msg}],
|
|
103
74
|
}
|
|
104
|
-
|
|
105
75
|
except requests.exceptions.RequestException as e:
|
|
106
76
|
error_msg = f"Error connecting to MindsDB: {str(e)}"
|
|
107
77
|
logger.error(error_msg)
|
|
@@ -109,7 +79,6 @@ class MindsDBAgent:
|
|
|
109
79
|
"content": error_msg,
|
|
110
80
|
"parts": [{"type": "text", "text": error_msg}],
|
|
111
81
|
}
|
|
112
|
-
|
|
113
82
|
except Exception as e:
|
|
114
83
|
error_msg = f"Error: {str(e)}"
|
|
115
84
|
logger.error(error_msg)
|
|
@@ -118,203 +87,58 @@ class MindsDBAgent:
|
|
|
118
87
|
"parts": [{"type": "text", "text": error_msg}],
|
|
119
88
|
}
|
|
120
89
|
|
|
121
|
-
def streaming_invoke(self, messages
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
# Send the request to MindsDB streaming API with timeout
|
|
141
|
-
stream = requests.post(url, json={"messages": messages}, stream=True, timeout=timeout)
|
|
142
|
-
stream.raise_for_status()
|
|
143
|
-
|
|
144
|
-
# Process the streaming response directly
|
|
145
|
-
for line in stream.iter_lines():
|
|
146
|
-
if line:
|
|
147
|
-
# Parse each non-empty line
|
|
148
|
-
try:
|
|
149
|
-
line = line.decode("utf-8")
|
|
150
|
-
if line.startswith("data: "):
|
|
151
|
-
# Extract the JSON data from the line that starts with 'data: '
|
|
152
|
-
data = line[6:] # Remove 'data: ' prefix
|
|
153
|
-
try:
|
|
154
|
-
chunk = json.loads(data)
|
|
155
|
-
# Pass through the chunk with minimal modifications
|
|
156
|
-
yield chunk
|
|
157
|
-
except json.JSONDecodeError as e:
|
|
158
|
-
logger.warning(f"Failed to parse JSON from line: {data}. Error: {str(e)}")
|
|
159
|
-
# Yield error information but continue processing
|
|
160
|
-
yield {
|
|
161
|
-
"error": f"JSON parse error: {str(e)}",
|
|
162
|
-
"data": data,
|
|
163
|
-
"is_task_complete": False,
|
|
164
|
-
"parts": [
|
|
165
|
-
{
|
|
166
|
-
"type": "text",
|
|
167
|
-
"text": f"Error parsing response: {str(e)}",
|
|
168
|
-
}
|
|
169
|
-
],
|
|
170
|
-
"metadata": {},
|
|
171
|
-
}
|
|
172
|
-
else:
|
|
173
|
-
# Log other lines for debugging
|
|
174
|
-
logger.debug(f"Received non-data line: {line}")
|
|
175
|
-
|
|
176
|
-
# If it looks like a raw text response (not SSE format), wrap it
|
|
177
|
-
if not line.startswith("event:") and not line.startswith(":"):
|
|
178
|
-
yield {"content": line, "is_task_complete": False}
|
|
179
|
-
except UnicodeDecodeError as e:
|
|
180
|
-
logger.warning(f"Failed to decode line: {str(e)}")
|
|
181
|
-
# Continue processing despite decode errors
|
|
182
|
-
|
|
183
|
-
except requests.exceptions.Timeout as e:
|
|
184
|
-
error_msg = f"Request timed out after {timeout} seconds: {str(e)}"
|
|
185
|
-
logger.error(error_msg)
|
|
186
|
-
yield {
|
|
187
|
-
"content": error_msg,
|
|
188
|
-
"parts": [{"type": "text", "text": error_msg}],
|
|
189
|
-
"is_task_complete": True,
|
|
190
|
-
"error": "timeout",
|
|
191
|
-
"metadata": {"error": True},
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
except requests.exceptions.ChunkedEncodingError as e:
|
|
195
|
-
error_msg = f"Stream was interrupted: {str(e)}"
|
|
196
|
-
logger.error(error_msg)
|
|
197
|
-
yield {
|
|
198
|
-
"content": error_msg,
|
|
199
|
-
"parts": [{"type": "text", "text": error_msg}],
|
|
200
|
-
"is_task_complete": True,
|
|
201
|
-
"error": "stream_interrupted",
|
|
202
|
-
"metadata": {"error": True},
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
except requests.exceptions.ConnectionError as e:
|
|
206
|
-
error_msg = f"Connection error: {str(e)}"
|
|
207
|
-
logger.error(error_msg)
|
|
208
|
-
yield {
|
|
209
|
-
"content": error_msg,
|
|
210
|
-
"parts": [{"type": "text", "text": error_msg}],
|
|
211
|
-
"is_task_complete": True,
|
|
212
|
-
"error": "connection_error",
|
|
213
|
-
"metadata": {"error": True},
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
except requests.exceptions.RequestException as e:
|
|
217
|
-
error_msg = f"Error connecting to MindsDB streaming API: {str(e)}"
|
|
218
|
-
logger.error(error_msg)
|
|
219
|
-
yield {
|
|
220
|
-
"content": error_msg,
|
|
221
|
-
"parts": [{"type": "text", "text": error_msg}],
|
|
222
|
-
"is_task_complete": True,
|
|
223
|
-
"error": "request_error",
|
|
224
|
-
"metadata": {"error": True},
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
except Exception as e:
|
|
228
|
-
error_msg = f"Error in streaming: {str(e)}"
|
|
229
|
-
logger.error(error_msg)
|
|
230
|
-
yield {
|
|
231
|
-
"content": error_msg,
|
|
232
|
-
"parts": [{"type": "text", "text": error_msg}],
|
|
233
|
-
"is_task_complete": True,
|
|
234
|
-
"error": "unknown_error",
|
|
235
|
-
"metadata": {"error": True},
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
# Send a final completion message
|
|
239
|
-
yield {"is_task_complete": True, "metadata": {"complete": True}}
|
|
90
|
+
async def streaming_invoke(self, messages, timeout=DEFAULT_STREAM_TIMEOUT):
|
|
91
|
+
url = f"{self.base_url}/api/projects/{self.project_name}/agents/{self.agent_name}/completions/stream"
|
|
92
|
+
logger.info(f"Sending streaming request to MindsDB agent: {self.agent_name}")
|
|
93
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
94
|
+
async with client.stream("POST", url, json={"messages": to_serializable(messages)}) as response:
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
async for line in response.aiter_lines():
|
|
97
|
+
if not line.strip():
|
|
98
|
+
continue
|
|
99
|
+
# Only process actual SSE data lines
|
|
100
|
+
if line.startswith("data:"):
|
|
101
|
+
payload = line[len("data:") :].strip()
|
|
102
|
+
try:
|
|
103
|
+
yield json.loads(payload)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f"Failed to parse SSE JSON payload: {e}; line: {payload}")
|
|
106
|
+
# Ignore comments or control lines
|
|
107
|
+
# Signal the end of the stream
|
|
108
|
+
yield {"is_task_complete": True}
|
|
240
109
|
|
|
241
110
|
async def stream(
|
|
242
111
|
self,
|
|
243
112
|
query: str,
|
|
244
113
|
session_id: str,
|
|
245
114
|
history: List[dict] | None = None,
|
|
115
|
+
timeout: int = DEFAULT_STREAM_TIMEOUT,
|
|
246
116
|
) -> AsyncIterable[Dict[str, Any]]:
|
|
247
|
-
"""Stream responses from the MindsDB agent (uses streaming API endpoint).
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
query: The current query to send to the agent.
|
|
251
|
-
session_id: Unique identifier for the conversation session.
|
|
252
|
-
history: Optional list of previous messages in the conversation.
|
|
253
|
-
|
|
254
|
-
Returns:
|
|
255
|
-
AsyncIterable yielding chunks of the streaming response.
|
|
256
|
-
"""
|
|
117
|
+
"""Stream responses from the MindsDB agent (uses streaming API endpoint)."""
|
|
257
118
|
try:
|
|
258
119
|
logger.info(f"Using streaming API for query: {query[:100]}...")
|
|
259
|
-
|
|
260
|
-
# Format history into the expected format
|
|
261
120
|
formatted_messages = []
|
|
262
121
|
if history:
|
|
263
122
|
for msg in history:
|
|
264
|
-
# Convert Message object to dict if needed
|
|
265
123
|
msg_dict = msg.dict() if hasattr(msg, "dict") else msg
|
|
266
124
|
role = msg_dict.get("role", "user")
|
|
267
|
-
|
|
268
|
-
# Extract text from parts
|
|
269
125
|
text = ""
|
|
270
126
|
for part in msg_dict.get("parts", []):
|
|
271
127
|
if part.get("type") == "text":
|
|
272
128
|
text = part.get("text", "")
|
|
273
129
|
break
|
|
274
|
-
|
|
275
130
|
if text:
|
|
276
131
|
if role == "user":
|
|
277
132
|
formatted_messages.append({"question": text, "answer": None})
|
|
278
133
|
elif role == "assistant" and formatted_messages:
|
|
279
|
-
# Add the answer to the last question
|
|
280
134
|
formatted_messages[-1]["answer"] = text
|
|
281
|
-
|
|
282
|
-
# Add the current query to the messages
|
|
283
135
|
formatted_messages.append({"question": query, "answer": None})
|
|
284
|
-
|
|
285
136
|
logger.debug(f"Formatted messages for agent: {formatted_messages}")
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
for chunk in streaming_response:
|
|
292
|
-
# Only add required fields if they don't exist
|
|
293
|
-
# This preserves the original structure as much as possible
|
|
294
|
-
if "is_task_complete" not in chunk:
|
|
295
|
-
chunk["is_task_complete"] = False
|
|
296
|
-
|
|
297
|
-
if "metadata" not in chunk:
|
|
298
|
-
chunk["metadata"] = {}
|
|
299
|
-
|
|
300
|
-
# Ensure parts exist, but try to preserve original content
|
|
301
|
-
if "parts" not in chunk:
|
|
302
|
-
# If content exists, create a part from it
|
|
303
|
-
if "content" in chunk:
|
|
304
|
-
chunk["parts"] = [{"type": "text", "text": chunk["content"]}]
|
|
305
|
-
# If output exists, create a part from it
|
|
306
|
-
elif "output" in chunk:
|
|
307
|
-
chunk["parts"] = [{"type": "text", "text": chunk["output"]}]
|
|
308
|
-
# If actions exist, create empty parts
|
|
309
|
-
elif "actions" in chunk or "steps" in chunk or "messages" in chunk:
|
|
310
|
-
# These chunks have their own format, just add empty parts
|
|
311
|
-
chunk["parts"] = []
|
|
312
|
-
else:
|
|
313
|
-
# Skip chunks with no content
|
|
314
|
-
continue
|
|
315
|
-
|
|
316
|
-
yield chunk
|
|
317
|
-
|
|
137
|
+
streaming_response = self.streaming_invoke(formatted_messages, timeout=timeout)
|
|
138
|
+
async for chunk in streaming_response:
|
|
139
|
+
content_value = chunk.get("text") or chunk.get("output") or json.dumps(chunk)
|
|
140
|
+
wrapped_chunk = {"is_task_complete": False, "content": content_value, "metadata": {}}
|
|
141
|
+
yield wrapped_chunk
|
|
318
142
|
except Exception as e:
|
|
319
143
|
logger.error(f"Error in streaming: {str(e)}")
|
|
320
144
|
yield {
|