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.
- mindsdb/__about__.py +1 -1
- mindsdb/__main__.py +18 -4
- mindsdb/api/executor/command_executor.py +12 -2
- mindsdb/api/executor/data_types/response_type.py +1 -0
- mindsdb/api/executor/datahub/classes/tables_row.py +3 -10
- mindsdb/api/executor/datahub/datanodes/datanode.py +7 -2
- mindsdb/api/executor/datahub/datanodes/information_schema_datanode.py +44 -10
- mindsdb/api/executor/datahub/datanodes/integration_datanode.py +57 -38
- mindsdb/api/executor/datahub/datanodes/mindsdb_tables.py +2 -1
- mindsdb/api/executor/datahub/datanodes/project_datanode.py +39 -7
- mindsdb/api/executor/datahub/datanodes/system_tables.py +116 -109
- mindsdb/api/executor/planner/query_plan.py +1 -0
- mindsdb/api/executor/planner/query_planner.py +15 -1
- mindsdb/api/executor/planner/steps.py +8 -2
- mindsdb/api/executor/sql_query/sql_query.py +24 -8
- mindsdb/api/executor/sql_query/steps/apply_predictor_step.py +25 -8
- mindsdb/api/executor/sql_query/steps/fetch_dataframe_partition.py +4 -2
- mindsdb/api/executor/sql_query/steps/insert_step.py +2 -1
- mindsdb/api/executor/sql_query/steps/prepare_steps.py +2 -3
- mindsdb/api/http/namespaces/config.py +19 -11
- mindsdb/api/litellm/start.py +82 -0
- mindsdb/api/mysql/mysql_proxy/libs/constants/mysql.py +133 -0
- mindsdb/integrations/handlers/chromadb_handler/chromadb_handler.py +7 -2
- mindsdb/integrations/handlers/chromadb_handler/settings.py +1 -0
- mindsdb/integrations/handlers/mssql_handler/mssql_handler.py +13 -4
- mindsdb/integrations/handlers/mysql_handler/mysql_handler.py +14 -5
- mindsdb/integrations/handlers/openai_handler/helpers.py +3 -5
- mindsdb/integrations/handlers/openai_handler/openai_handler.py +20 -8
- mindsdb/integrations/handlers/oracle_handler/oracle_handler.py +14 -4
- mindsdb/integrations/handlers/pgvector_handler/pgvector_handler.py +34 -19
- mindsdb/integrations/handlers/postgres_handler/postgres_handler.py +21 -18
- mindsdb/integrations/handlers/snowflake_handler/snowflake_handler.py +14 -4
- mindsdb/integrations/handlers/togetherai_handler/__about__.py +9 -0
- mindsdb/integrations/handlers/togetherai_handler/__init__.py +20 -0
- mindsdb/integrations/handlers/togetherai_handler/creation_args.py +14 -0
- mindsdb/integrations/handlers/togetherai_handler/icon.svg +15 -0
- mindsdb/integrations/handlers/togetherai_handler/model_using_args.py +5 -0
- mindsdb/integrations/handlers/togetherai_handler/requirements.txt +2 -0
- mindsdb/integrations/handlers/togetherai_handler/settings.py +33 -0
- mindsdb/integrations/handlers/togetherai_handler/togetherai_handler.py +234 -0
- mindsdb/integrations/handlers/web_handler/urlcrawl_helpers.py +1 -1
- mindsdb/integrations/libs/response.py +80 -32
- mindsdb/integrations/utilities/handler_utils.py +4 -0
- mindsdb/integrations/utilities/rag/rerankers/base_reranker.py +360 -0
- mindsdb/integrations/utilities/rag/rerankers/reranker_compressor.py +8 -153
- mindsdb/interfaces/agents/litellm_server.py +345 -0
- mindsdb/interfaces/agents/mcp_client_agent.py +252 -0
- mindsdb/interfaces/agents/run_mcp_agent.py +205 -0
- mindsdb/interfaces/functions/controller.py +3 -2
- mindsdb/interfaces/knowledge_base/controller.py +106 -82
- mindsdb/interfaces/query_context/context_controller.py +55 -15
- mindsdb/interfaces/query_context/query_task.py +19 -0
- mindsdb/interfaces/skills/skill_tool.py +7 -1
- mindsdb/interfaces/skills/sql_agent.py +8 -3
- mindsdb/interfaces/storage/db.py +2 -2
- mindsdb/interfaces/tasks/task_monitor.py +5 -1
- mindsdb/interfaces/tasks/task_thread.py +6 -0
- mindsdb/migrations/versions/2025-04-22_53502b6d63bf_query_database.py +27 -0
- mindsdb/utilities/config.py +20 -2
- mindsdb/utilities/context.py +1 -0
- mindsdb/utilities/starters.py +7 -0
- {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/METADATA +226 -221
- {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/RECORD +67 -53
- {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/WHEEL +1 -1
- mindsdb/integrations/handlers/snowflake_handler/tests/test_snowflake_handler.py +0 -230
- /mindsdb/{integrations/handlers/snowflake_handler/tests → api/litellm}/__init__.py +0 -0
- {mindsdb-25.4.3.2.dist-info → mindsdb-25.4.5.0.dist-info}/licenses/LICENSE +0 -0
- {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)
|