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 CHANGED
@@ -1,6 +1,6 @@
1
1
  __title__ = "MindsDB"
2
2
  __package_name__ = "mindsdb"
3
- __version__ = "25.7.1.0"
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['port'],
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('restart_on_failure', False),
451
- max_restart_count=http_api_config.get('max_restart_count', TrunkProcessData.max_restart_count),
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
- 'max_restart_interval_seconds', TrunkProcessData.max_restart_interval_seconds
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['port'],
432
+ port=mysql_api_config["port"],
460
433
  args=(config.cmd_args.verbose,),
461
- restart_on_failure=mysql_api_config.get('restart_on_failure', False),
462
- max_restart_count=mysql_api_config.get('max_restart_count', TrunkProcessData.max_restart_count),
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
- 'max_restart_interval_seconds', TrunkProcessData.max_restart_interval_seconds
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['api']['mongodb']['port'],
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['api']['postgres']['port'],
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('port', 47337),
464
+ port=mcp_api_config.get("port", 47337),
498
465
  args=(config.cmd_args.verbose,),
499
- need_to_run=mcp_api_config.get('need_to_run', False),
500
- restart_on_failure=mcp_api_config.get('restart_on_failure', False),
501
- max_restart_count=mcp_api_config.get('max_restart_count', TrunkProcessData.max_restart_count),
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
- 'max_restart_interval_seconds', TrunkProcessData.max_restart_interval_seconds
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('port', 8000),
476
+ port=litellm_api_config.get("port", 8000),
510
477
  args=(config.cmd_args.verbose,),
511
- restart_on_failure=litellm_api_config.get('restart_on_failure', False),
512
- max_restart_count=litellm_api_config.get('max_restart_count', TrunkProcessData.max_restart_count),
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
- 'max_restart_interval_seconds', TrunkProcessData.max_restart_interval_seconds
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('port', 8001),
487
+ port=a2a_api_config.get("port", 8001),
521
488
  args=(config.cmd_args.verbose,),
522
- need_to_run=a2a_api_config.get('enabled', False),
523
- restart_on_failure=a2a_api_config.get('restart_on_failure', True),
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
- 'max_restart_interval_seconds', TrunkProcessData.max_restart_interval_seconds
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, Iterator
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: List[dict], timeout: int = DEFAULT_STREAM_TIMEOUT) -> Iterator[Dict[str, Any]]:
122
- """Stream responses from the MindsDB agent using the direct API endpoint.
123
-
124
- Args:
125
- messages: List of message dictionaries, each containing 'question' and optionally 'answer'.
126
- Example: [{'question': 'what is the average rental price for a three bedroom?', 'answer': None}]
127
- timeout: Request timeout in seconds (default: 300)
128
-
129
- Returns:
130
- Iterator yielding chunks of the streaming response.
131
- """
132
- try:
133
- # Construct the URL for the streaming completions endpoint
134
- url = f"{self.base_url}/api/projects/{self.project_name}/agents/{self.agent_name}/completions/stream"
135
-
136
- # Log request for debugging
137
- logger.info(f"Sending streaming request to MindsDB agent: {self.agent_name}")
138
- logger.debug(f"Request messages: {json.dumps(messages)[:200]}...")
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
- # Use the streaming_invoke method to get real streaming responses
288
- streaming_response = self.streaming_invoke(formatted_messages)
289
-
290
- # Yield all chunks directly from the streaming response
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 {