MindsDB 25.4.3.2__py3-none-any.whl → 25.4.5.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.

Files changed (68) hide show
  1. mindsdb/__about__.py +1 -1
  2. mindsdb/__main__.py +18 -4
  3. mindsdb/api/executor/command_executor.py +12 -2
  4. mindsdb/api/executor/data_types/response_type.py +1 -0
  5. mindsdb/api/executor/datahub/classes/tables_row.py +3 -10
  6. mindsdb/api/executor/datahub/datanodes/datanode.py +7 -2
  7. mindsdb/api/executor/datahub/datanodes/information_schema_datanode.py +44 -10
  8. mindsdb/api/executor/datahub/datanodes/integration_datanode.py +57 -38
  9. mindsdb/api/executor/datahub/datanodes/mindsdb_tables.py +2 -1
  10. mindsdb/api/executor/datahub/datanodes/project_datanode.py +39 -7
  11. mindsdb/api/executor/datahub/datanodes/system_tables.py +116 -109
  12. mindsdb/api/executor/planner/query_plan.py +1 -0
  13. mindsdb/api/executor/planner/query_planner.py +15 -1
  14. mindsdb/api/executor/planner/steps.py +8 -2
  15. mindsdb/api/executor/sql_query/sql_query.py +24 -8
  16. mindsdb/api/executor/sql_query/steps/apply_predictor_step.py +25 -8
  17. mindsdb/api/executor/sql_query/steps/fetch_dataframe_partition.py +4 -2
  18. mindsdb/api/executor/sql_query/steps/insert_step.py +2 -1
  19. mindsdb/api/executor/sql_query/steps/prepare_steps.py +2 -3
  20. mindsdb/api/http/namespaces/config.py +19 -11
  21. mindsdb/api/litellm/start.py +82 -0
  22. mindsdb/api/mysql/mysql_proxy/libs/constants/mysql.py +133 -0
  23. mindsdb/integrations/handlers/chromadb_handler/chromadb_handler.py +7 -2
  24. mindsdb/integrations/handlers/chromadb_handler/settings.py +1 -0
  25. mindsdb/integrations/handlers/mssql_handler/mssql_handler.py +13 -4
  26. mindsdb/integrations/handlers/mysql_handler/mysql_handler.py +14 -5
  27. mindsdb/integrations/handlers/openai_handler/helpers.py +3 -5
  28. mindsdb/integrations/handlers/openai_handler/openai_handler.py +20 -8
  29. mindsdb/integrations/handlers/oracle_handler/oracle_handler.py +14 -4
  30. mindsdb/integrations/handlers/pgvector_handler/pgvector_handler.py +34 -19
  31. mindsdb/integrations/handlers/postgres_handler/postgres_handler.py +21 -18
  32. mindsdb/integrations/handlers/snowflake_handler/snowflake_handler.py +14 -4
  33. mindsdb/integrations/handlers/togetherai_handler/__about__.py +9 -0
  34. mindsdb/integrations/handlers/togetherai_handler/__init__.py +20 -0
  35. mindsdb/integrations/handlers/togetherai_handler/creation_args.py +14 -0
  36. mindsdb/integrations/handlers/togetherai_handler/icon.svg +15 -0
  37. mindsdb/integrations/handlers/togetherai_handler/model_using_args.py +5 -0
  38. mindsdb/integrations/handlers/togetherai_handler/requirements.txt +2 -0
  39. mindsdb/integrations/handlers/togetherai_handler/settings.py +33 -0
  40. mindsdb/integrations/handlers/togetherai_handler/togetherai_handler.py +234 -0
  41. mindsdb/integrations/handlers/web_handler/urlcrawl_helpers.py +1 -1
  42. mindsdb/integrations/libs/response.py +80 -32
  43. mindsdb/integrations/utilities/handler_utils.py +4 -0
  44. mindsdb/integrations/utilities/rag/rerankers/base_reranker.py +360 -0
  45. mindsdb/integrations/utilities/rag/rerankers/reranker_compressor.py +8 -153
  46. mindsdb/interfaces/agents/litellm_server.py +345 -0
  47. mindsdb/interfaces/agents/mcp_client_agent.py +252 -0
  48. mindsdb/interfaces/agents/run_mcp_agent.py +205 -0
  49. mindsdb/interfaces/functions/controller.py +3 -2
  50. mindsdb/interfaces/knowledge_base/controller.py +106 -82
  51. mindsdb/interfaces/query_context/context_controller.py +55 -15
  52. mindsdb/interfaces/query_context/query_task.py +19 -0
  53. mindsdb/interfaces/skills/skill_tool.py +7 -1
  54. mindsdb/interfaces/skills/sql_agent.py +8 -3
  55. mindsdb/interfaces/storage/db.py +2 -2
  56. mindsdb/interfaces/tasks/task_monitor.py +5 -1
  57. mindsdb/interfaces/tasks/task_thread.py +6 -0
  58. mindsdb/migrations/versions/2025-04-22_53502b6d63bf_query_database.py +27 -0
  59. mindsdb/utilities/config.py +20 -2
  60. mindsdb/utilities/context.py +1 -0
  61. mindsdb/utilities/starters.py +7 -0
  62. {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/METADATA +226 -221
  63. {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/RECORD +67 -53
  64. {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/WHEEL +1 -1
  65. mindsdb/integrations/handlers/snowflake_handler/tests/test_snowflake_handler.py +0 -230
  66. /mindsdb/{integrations/handlers/snowflake_handler/tests → api/litellm}/__init__.py +0 -0
  67. {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/licenses/LICENSE +0 -0
  68. {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,345 @@
1
+ import asyncio
2
+ import argparse
3
+ import json
4
+
5
+ from typing import List, Dict, Optional
6
+ from contextlib import AsyncExitStack
7
+
8
+ import uvicorn
9
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
10
+ from fastapi.responses import StreamingResponse
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from pydantic import BaseModel, Field
13
+ from mcp import ClientSession, StdioServerParameters
14
+ from mcp.client.stdio import stdio_client
15
+
16
+ from mindsdb.utilities import log
17
+ from mindsdb.interfaces.agents.mcp_client_agent import create_mcp_agent
18
+
19
+ logger = log.getLogger(__name__)
20
+
21
+ app = FastAPI(title="MindsDB MCP Agent LiteLLM API")
22
+
23
+ # Configure CORS
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"],
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ # Store agent wrapper as a global variable
33
+ agent_wrapper = None
34
+ # MCP session for direct SQL queries
35
+ mcp_session = None
36
+ exit_stack = AsyncExitStack()
37
+
38
+
39
+ class ChatMessage(BaseModel):
40
+ role: str
41
+ content: str
42
+
43
+
44
+ class ChatCompletionRequest(BaseModel):
45
+ model: str
46
+ messages: List[ChatMessage]
47
+ stream: bool = False
48
+ temperature: Optional[float] = None
49
+ max_tokens: Optional[int] = None
50
+
51
+
52
+ class ChatCompletionChoice(BaseModel):
53
+ index: int = 0
54
+ message: Optional[Dict[str, str]] = None
55
+ delta: Optional[Dict[str, str]] = None
56
+ finish_reason: Optional[str] = "stop"
57
+
58
+
59
+ class ChatCompletionResponse(BaseModel):
60
+ id: str = "mcp-agent-response"
61
+ object: str = "chat.completion"
62
+ created: int = 0
63
+ model: str
64
+ choices: List[ChatCompletionChoice]
65
+ usage: Dict[str, int] = Field(default_factory=lambda: {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0})
66
+
67
+
68
+ class DirectSQLRequest(BaseModel):
69
+ query: str
70
+
71
+
72
+ @app.post("/v1/chat/completions")
73
+ async def chat_completions(request: ChatCompletionRequest):
74
+ global agent_wrapper
75
+
76
+ if agent_wrapper is None:
77
+ raise HTTPException(status_code=500, detail="Agent not initialized. Make sure MindsDB server is running with MCP enabled: python -m mindsdb --api=mysql,mcp,http")
78
+
79
+ try:
80
+ # Convert request to messages format
81
+ messages = [
82
+ {"role": msg.role, "content": msg.content}
83
+ for msg in request.messages
84
+ ]
85
+
86
+ if request.stream:
87
+ # Return a streaming response
88
+ async def generate():
89
+ try:
90
+ async for chunk in agent_wrapper.acompletion_stream(messages, model=request.model):
91
+ yield f"data: {json.dumps(chunk)}\n\n"
92
+ yield "data: [DONE]\n\n"
93
+ except Exception as e:
94
+ logger.error(f"Streaming error: {str(e)}")
95
+ yield "data: {{'error': 'Streaming failed due to an internal error.'}}\n\n"
96
+ return StreamingResponse(generate(), media_type="text/event-stream")
97
+ else:
98
+ # Return a regular response
99
+ response = await agent_wrapper.acompletion(messages)
100
+
101
+ # Ensure the content is a string
102
+ content = response["choices"][0]["message"].get("content", "")
103
+ if not isinstance(content, str):
104
+ content = str(content)
105
+
106
+ # Transform to proper OpenAI format
107
+ return ChatCompletionResponse(
108
+ model=request.model,
109
+ choices=[
110
+ ChatCompletionChoice(
111
+ message={"role": "assistant", "content": content}
112
+ )
113
+ ]
114
+ )
115
+
116
+ except Exception as e:
117
+ logger.error(f"Error in chat completion: {str(e)}")
118
+ raise HTTPException(status_code=500, detail=str(e))
119
+
120
+
121
+ @app.post("/direct-sql")
122
+ async def direct_sql(request: DirectSQLRequest, background_tasks: BackgroundTasks):
123
+ """Execute a direct SQL query via MCP (for testing)"""
124
+ global agent_wrapper, mcp_session
125
+
126
+ if agent_wrapper is None and mcp_session is None:
127
+ raise HTTPException(status_code=500, detail="No MCP session available. Make sure MindsDB server is running with MCP enabled.")
128
+
129
+ try:
130
+ # First try to use the agent's session if available
131
+ if hasattr(agent_wrapper.agent, "session") and agent_wrapper.agent.session:
132
+ session = agent_wrapper.agent.session
133
+ result = await session.call_tool("query", {"query": request.query})
134
+ return {"result": result.content}
135
+ # If agent session not available, use the direct session
136
+ elif mcp_session:
137
+ result = await mcp_session.call_tool("query", {"query": request.query})
138
+ return {"result": result.content}
139
+ else:
140
+ raise HTTPException(status_code=500, detail="No MCP session available")
141
+
142
+ except Exception as e:
143
+ logger.error(f"Error executing direct SQL: {str(e)}")
144
+ raise HTTPException(status_code=500, detail=str(e))
145
+
146
+
147
+ @app.get("/v1/models")
148
+ async def list_models():
149
+ """List available models - always returns the single model we're using"""
150
+ global agent_wrapper
151
+
152
+ if agent_wrapper is None:
153
+ return {
154
+ "object": "list",
155
+ "data": [
156
+ {
157
+ "id": "mcp-agent",
158
+ "object": "model",
159
+ "created": 0,
160
+ "owned_by": "mindsdb"
161
+ }
162
+ ]
163
+ }
164
+
165
+ # Return the actual model name if available
166
+ model_name = agent_wrapper.agent.args.get("model_name", "mcp-agent")
167
+
168
+ return {
169
+ "object": "list",
170
+ "data": [
171
+ {
172
+ "id": model_name,
173
+ "object": "model",
174
+ "created": 0,
175
+ "owned_by": "mindsdb"
176
+ }
177
+ ]
178
+ }
179
+
180
+
181
+ @app.get("/health")
182
+ async def health_check():
183
+ """Health check endpoint"""
184
+ global agent_wrapper
185
+
186
+ health_status = {
187
+ "status": "ok",
188
+ "agent_initialized": agent_wrapper is not None,
189
+ }
190
+
191
+ if agent_wrapper is not None:
192
+ health_status["mcp_connected"] = hasattr(agent_wrapper.agent, "session") and agent_wrapper.agent.session is not None
193
+ health_status["agent_name"] = agent_wrapper.agent.agent.name
194
+ health_status["model_name"] = agent_wrapper.agent.args.get("model_name", "unknown")
195
+
196
+ return health_status
197
+
198
+
199
+ @app.get("/test-mcp-connection")
200
+ async def test_mcp_connection():
201
+ """Test the connection to the MCP server"""
202
+ global mcp_session, exit_stack
203
+
204
+ try:
205
+ # If we already have a session, test it
206
+ if mcp_session:
207
+ try:
208
+ tools_response = await mcp_session.list_tools()
209
+ return {
210
+ "status": "ok",
211
+ "message": "Successfully connected to MCP server",
212
+ "tools": [tool.name for tool in tools_response.tools]
213
+ }
214
+ except Exception:
215
+ # If error, close existing session and create a new one
216
+ await exit_stack.aclose()
217
+ mcp_session = None
218
+
219
+ # Create a new MCP session - connect to running server
220
+ server_params = StdioServerParameters(
221
+ command="python",
222
+ args=["-m", "mindsdb", "--api=mcp"],
223
+ env=None
224
+ )
225
+
226
+ stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params))
227
+ stdio, write = stdio_transport
228
+ session = await exit_stack.enter_async_context(ClientSession(stdio, write))
229
+
230
+ await session.initialize()
231
+
232
+ # Save the session for future use
233
+ mcp_session = session
234
+
235
+ # Get available tools
236
+ tools_response = await session.list_tools()
237
+
238
+ return {
239
+ "status": "ok",
240
+ "message": "Successfully connected to MCP server",
241
+ "tools": [tool.name for tool in tools_response.tools]
242
+ }
243
+ except Exception as e:
244
+ logger.error(f"Error connecting to MCP server: {str(e)}")
245
+ error_detail = f"Error connecting to MCP server: {str(e)}. Make sure MindsDB server is running with MCP enabled: python -m mindsdb --api=mysql,mcp,http"
246
+ raise HTTPException(status_code=500, detail=error_detail)
247
+
248
+
249
+ async def init_agent(agent_name: str, project_name: str, mcp_host: str, mcp_port: int):
250
+ """Initialize the agent"""
251
+ global agent_wrapper
252
+
253
+ try:
254
+ logger.info(f"Initializing MCP agent '{agent_name}' in project '{project_name}'")
255
+ logger.info(f"Connecting to MCP server at {mcp_host}:{mcp_port}")
256
+ logger.info("Make sure MindsDB server is running with MCP enabled: python -m mindsdb --api=mysql,mcp,http")
257
+
258
+ agent_wrapper = create_mcp_agent(
259
+ agent_name=agent_name,
260
+ project_name=project_name,
261
+ mcp_host=mcp_host,
262
+ mcp_port=mcp_port
263
+ )
264
+
265
+ logger.info("Agent initialized successfully")
266
+ return True
267
+ except Exception as e:
268
+ logger.error(f"Failed to initialize agent: {str(e)}")
269
+ return False
270
+
271
+
272
+ @app.on_event("shutdown")
273
+ async def shutdown_event():
274
+ """Clean up resources on server shutdown"""
275
+ global agent_wrapper, exit_stack
276
+
277
+ if agent_wrapper:
278
+ await agent_wrapper.cleanup()
279
+
280
+ await exit_stack.aclose()
281
+
282
+
283
+ async def run_server_async(
284
+ agent_name: str,
285
+ project_name: str = "mindsdb",
286
+ mcp_host: str = "127.0.0.1",
287
+ mcp_port: int = 47337,
288
+ host: str = "0.0.0.0",
289
+ port: int = 8000
290
+ ):
291
+ """Run the FastAPI server"""
292
+ # Initialize the agent
293
+ success = await init_agent(agent_name, project_name, mcp_host, mcp_port)
294
+ if not success:
295
+ logger.error("Failed to initialize agent. Make sure MindsDB server is running with MCP enabled.")
296
+ return 1
297
+
298
+ return 0
299
+
300
+
301
+ def run_server(
302
+ agent_name: str,
303
+ project_name: str = "mindsdb",
304
+ mcp_host: str = "127.0.0.1",
305
+ mcp_port: int = 47337,
306
+ host: str = "0.0.0.0",
307
+ port: int = 8000
308
+ ):
309
+ """Run the FastAPI server"""
310
+ logger.info("Make sure MindsDB server is running with MCP enabled: python -m mindsdb --api=mysql,mcp,http")
311
+ # Initialize database
312
+ from mindsdb.interfaces.storage import db
313
+ db.init()
314
+
315
+ # Run initialization in the event loop
316
+ loop = asyncio.new_event_loop()
317
+ asyncio.set_event_loop(loop)
318
+ result = loop.run_until_complete(run_server_async(agent_name, project_name, mcp_host, mcp_port))
319
+ if result != 0:
320
+ return result
321
+ # Run the server
322
+ logger.info(f"Starting server on {host}:{port}")
323
+ uvicorn.run(app, host=host, port=port)
324
+ return 0
325
+
326
+
327
+ if __name__ == "__main__":
328
+ parser = argparse.ArgumentParser(description="Run a LiteLLM-compatible API server for MCP agent")
329
+ parser.add_argument("--agent", type=str, required=True, help="Name of the agent to use")
330
+ parser.add_argument("--project", type=str, default="mindsdb", help="Project containing the agent")
331
+ parser.add_argument("--mcp-host", type=str, default="127.0.0.1", help="MCP server host")
332
+ parser.add_argument("--mcp-port", type=int, default=47337, help="MCP server port")
333
+ parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind the server to")
334
+ parser.add_argument("--port", type=int, default=8000, help="Port to run the server on")
335
+
336
+ args = parser.parse_args()
337
+
338
+ run_server(
339
+ agent_name=args.agent,
340
+ project_name=args.project,
341
+ mcp_host=args.mcp_host,
342
+ mcp_port=args.mcp_port,
343
+ host=args.host,
344
+ port=args.port
345
+ )
@@ -0,0 +1,252 @@
1
+ import json
2
+ import asyncio
3
+ from typing import Dict, List, Any, Iterator, ClassVar
4
+ from contextlib import AsyncExitStack
5
+
6
+ import pandas as pd
7
+ from mcp import ClientSession, StdioServerParameters
8
+ from mcp.client.stdio import stdio_client
9
+
10
+ from mindsdb.utilities import log
11
+ from mindsdb.interfaces.agents.langchain_agent import LangchainAgent
12
+ from mindsdb.interfaces.storage import db
13
+ from langchain_core.tools import BaseTool
14
+
15
+ logger = log.getLogger(__name__)
16
+
17
+
18
+ class MCPQueryTool(BaseTool):
19
+ """Tool that executes queries via MCP server"""
20
+
21
+ name: ClassVar[str] = "mcp_query"
22
+ description: ClassVar[str] = "Execute SQL queries against the MindsDB server via MCP protocol"
23
+
24
+ def __init__(self, session: ClientSession):
25
+ super().__init__()
26
+ self.session = session
27
+
28
+ async def _arun(self, query: str) -> str:
29
+ """Execute a query via MCP asynchronously"""
30
+ try:
31
+ logger.info(f"Executing MCP query: {query}")
32
+ # Find the appropriate tool for SQL queries
33
+ tools_response = await self.session.list_tools()
34
+ query_tool = None
35
+
36
+ for tool in tools_response.tools:
37
+ if tool.name == "query":
38
+ query_tool = tool
39
+ break
40
+
41
+ if not query_tool:
42
+ return "Error: No 'query' tool found in the MCP server"
43
+
44
+ # Call the query tool
45
+ result = await self.session.call_tool("query", {"query": query})
46
+
47
+ # Process the results
48
+ if isinstance(result.content, dict) and "data" in result.content and "column_names" in result.content:
49
+ # Create a DataFrame from the results
50
+ df = pd.DataFrame(result.content["data"], columns=result.content["column_names"])
51
+ return df.to_string()
52
+
53
+ # Return raw result for other types
54
+ return f"Query executed successfully: {json.dumps(result.content)}"
55
+
56
+ except Exception as e:
57
+ logger.error(f"Error executing MCP query: {str(e)}")
58
+ return f"Error executing query: {str(e)}"
59
+
60
+ def _run(self, query: str) -> str:
61
+ """Synchronous wrapper for async query function"""
62
+ loop = asyncio.get_event_loop()
63
+ return loop.run_until_complete(self._arun(query))
64
+
65
+
66
+ class MCPLangchainAgent(LangchainAgent):
67
+ """Extension of LangchainAgent that delegates to MCP server"""
68
+
69
+ def __init__(self, agent: db.Agents, model: dict = None, mcp_host: str = "127.0.0.1", mcp_port: int = 47337):
70
+ super().__init__(agent, model)
71
+ self.mcp_host = mcp_host
72
+ self.mcp_port = mcp_port
73
+ self.exit_stack = AsyncExitStack()
74
+ self.session = None
75
+ self.stdio = None
76
+ self.write = None
77
+
78
+ async def connect_to_mcp(self):
79
+ """Connect to the MCP server using stdio transport"""
80
+ if self.session is None:
81
+ logger.info(f"Connecting to MCP server at {self.mcp_host}:{self.mcp_port}")
82
+ try:
83
+ # For connecting to an already running MCP server
84
+ # Set up server parameters to connect to existing process
85
+ server_params = StdioServerParameters(
86
+ command="python",
87
+ args=["-m", "mindsdb", "--api=mcp"],
88
+ env={"MCP_HOST": self.mcp_host, "MCP_PORT": str(self.mcp_port)}
89
+ )
90
+
91
+ logger.info(f"Connecting to MCP server at {self.mcp_host}:{self.mcp_port}")
92
+
93
+ # Connect to the server
94
+ stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
95
+ self.stdio, self.write = stdio_transport
96
+ self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
97
+
98
+ await self.session.initialize()
99
+
100
+ # Test the connection by listing tools
101
+ tools_response = await self.session.list_tools()
102
+ logger.info(f"Successfully connected to MCP server. Available tools: {[tool.name for tool in tools_response.tools]}")
103
+
104
+ except Exception as e:
105
+ logger.error(f"Failed to connect to MCP server: {str(e)}")
106
+ raise ConnectionError(f"Failed to connect to MCP server: {str(e)}")
107
+
108
+ def _langchain_tools_from_skills(self, llm):
109
+ """Override to add MCP query tool along with other tools"""
110
+ # Get tools from parent implementation
111
+ tools = super()._langchain_tools_from_skills(llm)
112
+
113
+ # Initialize MCP connection
114
+ try:
115
+ # Using the event loop directly instead of asyncio.run()
116
+ loop = asyncio.get_event_loop()
117
+ if self.session is None:
118
+ loop.run_until_complete(self.connect_to_mcp())
119
+
120
+ # Add MCP query tool if session is established
121
+ if self.session:
122
+ tools.append(MCPQueryTool(self.session))
123
+ logger.info("Added MCP query tool to agent tools")
124
+ except Exception as e:
125
+ logger.error(f"Failed to add MCP query tool: {str(e)}")
126
+
127
+ return tools
128
+
129
+ def get_completion(self, messages, stream: bool = False):
130
+ """Override to ensure MCP connection is established before getting completion"""
131
+ try:
132
+ # Ensure connection to MCP is established
133
+ if self.session is None:
134
+ # Using the event loop directly instead of asyncio.run()
135
+ loop = asyncio.get_event_loop()
136
+ loop.run_until_complete(self.connect_to_mcp())
137
+ except Exception as e:
138
+ logger.error(f"Failed to connect to MCP server: {str(e)}")
139
+
140
+ # Call parent implementation to get completion
141
+ response = super().get_completion(messages, stream)
142
+
143
+ # Ensure response is a string (not a DataFrame)
144
+ if hasattr(response, 'to_string'): # It's a DataFrame
145
+ return response.to_string()
146
+
147
+ return response
148
+
149
+ async def cleanup(self):
150
+ """Clean up resources"""
151
+ if self.exit_stack:
152
+ await self.exit_stack.aclose()
153
+ self.session = None
154
+ self.stdio = None
155
+ self.write = None
156
+
157
+
158
+ class LiteLLMAgentWrapper:
159
+ """Wrapper for MCPLangchainAgent that provides LiteLLM-compatible interface"""
160
+
161
+ def __init__(self, agent: MCPLangchainAgent):
162
+ self.agent = agent
163
+
164
+ async def acompletion(self, messages: List[Dict[str, str]], **kwargs) -> Dict[str, Any]:
165
+ """Async completion interface compatible with LiteLLM"""
166
+ # Convert messages to format expected by agent
167
+ formatted_messages = [
168
+ {
169
+ "question": msg["content"] if msg["role"] == "user" else "",
170
+ "answer": msg["content"] if msg["role"] == "assistant" else ""
171
+ }
172
+ for msg in messages
173
+ ]
174
+
175
+ # Get completion from agent
176
+ response = self.agent.get_completion(formatted_messages)
177
+
178
+ # Ensure response is a string
179
+ if not isinstance(response, str):
180
+ if hasattr(response, 'to_string'): # It's a DataFrame
181
+ response = response.to_string()
182
+ else:
183
+ response = str(response)
184
+
185
+ # Format response in LiteLLM expected format
186
+ return {
187
+ "choices": [
188
+ {
189
+ "message": {
190
+ "role": "assistant",
191
+ "content": response
192
+ }
193
+ }
194
+ ],
195
+ "model": self.agent.args["model_name"],
196
+ "object": "chat.completion"
197
+ }
198
+
199
+ async def acompletion_stream(self, messages: List[Dict[str, str]], **kwargs) -> Iterator[Dict[str, Any]]:
200
+ """Async streaming completion interface compatible with LiteLLM"""
201
+ # Convert messages to format expected by agent
202
+ formatted_messages = [
203
+ {
204
+ "question": msg["content"] if msg["role"] == "user" else "",
205
+ "answer": msg["content"] if msg["role"] == "assistant" else ""
206
+ }
207
+ for msg in messages
208
+ ]
209
+
210
+ # Stream completion from agent
211
+ model_name = kwargs.get("model", self.agent.args.get("model_name", "mcp-agent"))
212
+ try:
213
+ # Handle synchronous generator from _get_completion_stream
214
+ for chunk in self.agent._get_completion_stream(formatted_messages):
215
+ content = chunk.get("output", "")
216
+ if content and isinstance(content, str):
217
+ yield {
218
+ "choices": [{"delta": {"role": "assistant", "content": content}}],
219
+ "model": model_name,
220
+ "object": "chat.completion.chunk"
221
+ }
222
+ # Allow async context switch
223
+ await asyncio.sleep(0)
224
+ except Exception as e:
225
+ logger.error(f"Streaming error: {str(e)}")
226
+ raise
227
+
228
+ async def cleanup(self):
229
+ """Clean up resources"""
230
+ await self.agent.cleanup()
231
+
232
+
233
+ def create_mcp_agent(agent_name: str, project_name: str, mcp_host: str = "127.0.0.1", mcp_port: int = 47337) -> LiteLLMAgentWrapper:
234
+ """Create an MCP agent and wrap it for LiteLLM compatibility"""
235
+ from mindsdb.interfaces.agents.agents_controller import AgentsController
236
+ from mindsdb.interfaces.storage import db
237
+
238
+ # Initialize database
239
+ db.init()
240
+
241
+ # Get the agent from database
242
+ agent_controller = AgentsController()
243
+ agent_db = agent_controller.get_agent(agent_name, project_name)
244
+
245
+ if agent_db is None:
246
+ raise ValueError(f"Agent {agent_name} not found in project {project_name}")
247
+
248
+ # Create MCP agent
249
+ mcp_agent = MCPLangchainAgent(agent_db, mcp_host=mcp_host, mcp_port=mcp_port)
250
+
251
+ # Wrap for LiteLLM compatibility
252
+ return LiteLLMAgentWrapper(mcp_agent)