MindsDB 25.4.3.1__py3-none-any.whl → 25.4.4.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/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/project_datanode.py +39 -7
- mindsdb/api/executor/datahub/datanodes/system_tables.py +116 -109
- mindsdb/api/executor/planner/query_planner.py +10 -1
- mindsdb/api/executor/planner/steps.py +8 -2
- mindsdb/api/executor/sql_query/steps/apply_predictor_step.py +5 -5
- mindsdb/api/executor/sql_query/steps/fetch_dataframe_partition.py +1 -1
- mindsdb/api/executor/sql_query/steps/insert_step.py +2 -1
- mindsdb/api/executor/sql_query/steps/prepare_steps.py +2 -3
- 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/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/web_handler/urlcrawl_helpers.py +1 -1
- mindsdb/integrations/libs/response.py +80 -32
- mindsdb/integrations/utilities/rag/rerankers/reranker_compressor.py +208 -13
- 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/knowledge_base/controller.py +17 -7
- mindsdb/interfaces/skills/skill_tool.py +7 -1
- mindsdb/interfaces/skills/sql_agent.py +8 -3
- mindsdb/utilities/config.py +8 -1
- mindsdb/utilities/starters.py +7 -0
- {mindsdb-25.4.3.1.dist-info → mindsdb-25.4.4.0.dist-info}/METADATA +232 -230
- {mindsdb-25.4.3.1.dist-info → mindsdb-25.4.4.0.dist-info}/RECORD +42 -39
- {mindsdb-25.4.3.1.dist-info → mindsdb-25.4.4.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.1.dist-info → mindsdb-25.4.4.0.dist-info}/licenses/LICENSE +0 -0
- {mindsdb-25.4.3.1.dist-info → mindsdb-25.4.4.0.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
from contextlib import AsyncExitStack
|
|
6
|
+
|
|
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.mcp_client_agent import create_mcp_agent
|
|
12
|
+
|
|
13
|
+
logger = log.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def run_conversation(agent_wrapper, messages: List[Dict[str, str]], stream: bool = False):
|
|
17
|
+
"""Run a conversation with the agent and print responses"""
|
|
18
|
+
try:
|
|
19
|
+
if stream:
|
|
20
|
+
logger.info("Streaming response:")
|
|
21
|
+
async for chunk in agent_wrapper.acompletion_stream(messages):
|
|
22
|
+
content = chunk["choices"][0]["delta"].get("content", "")
|
|
23
|
+
if content:
|
|
24
|
+
# We still need to print content for streaming display
|
|
25
|
+
# but we'll log it as debug as well
|
|
26
|
+
logger.debug(f"Stream content: {content}")
|
|
27
|
+
sys.stdout.write(content)
|
|
28
|
+
sys.stdout.flush()
|
|
29
|
+
logger.debug("End of stream")
|
|
30
|
+
sys.stdout.write("\n\n")
|
|
31
|
+
sys.stdout.flush()
|
|
32
|
+
else:
|
|
33
|
+
logger.info("Getting response...")
|
|
34
|
+
response = await agent_wrapper.acompletion(messages)
|
|
35
|
+
content = response["choices"][0]["message"]["content"]
|
|
36
|
+
logger.info(f"Response: {content}")
|
|
37
|
+
# We still need to display the response to the user
|
|
38
|
+
sys.stdout.write(f"{content}\n")
|
|
39
|
+
sys.stdout.flush()
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Error during agent conversation: {str(e)}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def execute_direct_query(query):
|
|
45
|
+
"""Execute a direct SQL query using MCP"""
|
|
46
|
+
logger.info(f"Executing direct SQL query: {query}")
|
|
47
|
+
|
|
48
|
+
# Set up MCP client to connect to the running server
|
|
49
|
+
async with AsyncExitStack() as stack:
|
|
50
|
+
# Connect to MCP server
|
|
51
|
+
server_params = StdioServerParameters(
|
|
52
|
+
command="python",
|
|
53
|
+
args=["-m", "mindsdb", "--api=mcp"],
|
|
54
|
+
env=None
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
stdio_transport = await stack.enter_async_context(stdio_client(server_params))
|
|
59
|
+
stdio, write = stdio_transport
|
|
60
|
+
session = await stack.enter_async_context(ClientSession(stdio, write))
|
|
61
|
+
|
|
62
|
+
await session.initialize()
|
|
63
|
+
|
|
64
|
+
# List available tools
|
|
65
|
+
tools_response = await session.list_tools()
|
|
66
|
+
tool_names = [tool.name for tool in tools_response.tools]
|
|
67
|
+
logger.info(f"Available tools: {tool_names}")
|
|
68
|
+
|
|
69
|
+
# Find query tool
|
|
70
|
+
query_tool = None
|
|
71
|
+
for tool in tools_response.tools:
|
|
72
|
+
if tool.name == "query":
|
|
73
|
+
query_tool = tool
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
if not query_tool:
|
|
77
|
+
logger.error("No 'query' tool found in MCP server")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Execute query
|
|
81
|
+
result = await session.call_tool("query", {"query": query})
|
|
82
|
+
logger.info(f"Query result: {result.content}")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Error executing query: {str(e)}")
|
|
85
|
+
logger.info("Make sure the MindsDB server is running with MCP enabled: python -m mindsdb --api=mysql,mcp,http")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def main():
|
|
89
|
+
parser = argparse.ArgumentParser(description="Run an agent as an MCP client")
|
|
90
|
+
parser.add_argument("--agent", type=str, help="Name of the agent to use")
|
|
91
|
+
parser.add_argument("--project", type=str, default="mindsdb", help="Project containing the agent")
|
|
92
|
+
parser.add_argument("--host", type=str, default="127.0.0.1", help="MCP server host")
|
|
93
|
+
parser.add_argument("--port", type=int, default=47337, help="MCP server port")
|
|
94
|
+
parser.add_argument("--query", type=str, help="Query to send to the agent")
|
|
95
|
+
parser.add_argument("--stream", action="store_true", help="Stream the response")
|
|
96
|
+
parser.add_argument("--execute-direct", type=str, help="Execute a direct SQL query via MCP (for testing)")
|
|
97
|
+
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Initialize database connection
|
|
102
|
+
from mindsdb.interfaces.storage import db
|
|
103
|
+
db.init()
|
|
104
|
+
|
|
105
|
+
# Direct SQL execution mode (for testing MCP connection)
|
|
106
|
+
if args.execute_direct:
|
|
107
|
+
await execute_direct_query(args.execute_direct)
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
# Make sure agent name is provided
|
|
111
|
+
if not args.agent:
|
|
112
|
+
parser.error("the --agent argument is required unless --execute-direct is used")
|
|
113
|
+
|
|
114
|
+
# Create the agent
|
|
115
|
+
logger.info(f"Creating MCP client agent for '{args.agent}' in project '{args.project}'")
|
|
116
|
+
logger.info(f"Connecting to MCP server at {args.host}:{args.port}")
|
|
117
|
+
logger.info("Make sure MindsDB server is running with MCP enabled: python -m mindsdb --api=mysql,mcp,http")
|
|
118
|
+
|
|
119
|
+
agent_wrapper = create_mcp_agent(
|
|
120
|
+
agent_name=args.agent,
|
|
121
|
+
project_name=args.project,
|
|
122
|
+
mcp_host=args.host,
|
|
123
|
+
mcp_port=args.port
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Run an example query if provided
|
|
127
|
+
if args.query:
|
|
128
|
+
messages = [{"role": "user", "content": args.query}]
|
|
129
|
+
await run_conversation(agent_wrapper, messages, args.stream)
|
|
130
|
+
else:
|
|
131
|
+
# Interactive mode
|
|
132
|
+
logger.info("Entering interactive mode. Type 'exit' to quit.")
|
|
133
|
+
logger.info("Available commands: exit/quit, clear, sql:")
|
|
134
|
+
|
|
135
|
+
# We still need to show these instructions to the user
|
|
136
|
+
sys.stdout.write("\nEntering interactive mode. Type 'exit' to quit.\n")
|
|
137
|
+
sys.stdout.write("\nAvailable commands:\n")
|
|
138
|
+
sys.stdout.write(" exit, quit - Exit the program\n")
|
|
139
|
+
sys.stdout.write(" clear - Clear conversation history\n")
|
|
140
|
+
sys.stdout.write(" sql: <query> - Execute a direct SQL query via MCP\n")
|
|
141
|
+
sys.stdout.flush()
|
|
142
|
+
|
|
143
|
+
messages = []
|
|
144
|
+
|
|
145
|
+
while True:
|
|
146
|
+
# We need to keep input for user interaction
|
|
147
|
+
user_input = input("\nYou: ")
|
|
148
|
+
|
|
149
|
+
# Check for special commands
|
|
150
|
+
if user_input.lower() in ["exit", "quit"]:
|
|
151
|
+
logger.info("Exiting interactive mode")
|
|
152
|
+
break
|
|
153
|
+
elif user_input.lower() == "clear":
|
|
154
|
+
messages = []
|
|
155
|
+
logger.info("Conversation history cleared")
|
|
156
|
+
sys.stdout.write("Conversation history cleared\n")
|
|
157
|
+
sys.stdout.flush()
|
|
158
|
+
continue
|
|
159
|
+
elif user_input.lower().startswith("sql:"):
|
|
160
|
+
# Direct SQL execution using the agent's session
|
|
161
|
+
sql_query = user_input[4:].strip()
|
|
162
|
+
logger.info(f"Executing SQL: {sql_query}")
|
|
163
|
+
try:
|
|
164
|
+
# Use the tool from the agent
|
|
165
|
+
if hasattr(agent_wrapper.agent, "session") and agent_wrapper.agent.session:
|
|
166
|
+
result = await agent_wrapper.agent.session.call_tool("query", {"query": sql_query})
|
|
167
|
+
logger.info(f"SQL result: {result.content}")
|
|
168
|
+
# We need to show the result to the user
|
|
169
|
+
sys.stdout.write(f"Result: {result.content}\n")
|
|
170
|
+
sys.stdout.flush()
|
|
171
|
+
else:
|
|
172
|
+
logger.error("No active MCP session")
|
|
173
|
+
sys.stdout.write("Error: No active MCP session\n")
|
|
174
|
+
sys.stdout.flush()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"SQL Error: {str(e)}")
|
|
177
|
+
sys.stdout.write(f"SQL Error: {str(e)}\n")
|
|
178
|
+
sys.stdout.flush()
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
messages.append({"role": "user", "content": user_input})
|
|
182
|
+
await run_conversation(agent_wrapper, messages, args.stream)
|
|
183
|
+
|
|
184
|
+
# Add assistant's response to the conversation history
|
|
185
|
+
if not args.stream:
|
|
186
|
+
response = await agent_wrapper.acompletion(messages)
|
|
187
|
+
messages.append({
|
|
188
|
+
"role": "assistant",
|
|
189
|
+
"content": response["choices"][0]["message"]["content"]
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
# Clean up resources
|
|
193
|
+
logger.info("Cleaning up resources")
|
|
194
|
+
await agent_wrapper.cleanup()
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(f"Error running MCP agent: {str(e)}")
|
|
198
|
+
logger.info("Make sure the MindsDB server is running with MCP enabled: python -m mindsdb --api=mysql,mcp,http")
|
|
199
|
+
return 1
|
|
200
|
+
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
sys.exit(asyncio.run(main()))
|
|
@@ -65,6 +65,11 @@ def get_embedding_model_from_params(embedding_model_params: dict):
|
|
|
65
65
|
if provider == 'azure_openai':
|
|
66
66
|
# Azure OpenAI expects the api_key to be passed as 'openai_api_key'.
|
|
67
67
|
params_copy['openai_api_key'] = api_key
|
|
68
|
+
params_copy['azure_endpoint'] = params_copy.pop('base_url')
|
|
69
|
+
if 'chunk_size' not in params_copy:
|
|
70
|
+
params_copy['chunk_size'] = 2048
|
|
71
|
+
if 'api_version' in params_copy:
|
|
72
|
+
params_copy['openai_api_version'] = params_copy['api_version']
|
|
68
73
|
else:
|
|
69
74
|
params_copy[f"{provider}_api_key"] = api_key
|
|
70
75
|
params_copy.pop('api_key', None)
|
|
@@ -78,11 +83,10 @@ def get_reranking_model_from_params(reranking_model_params: dict):
|
|
|
78
83
|
Create reranking model from parameters.
|
|
79
84
|
"""
|
|
80
85
|
params_copy = copy.deepcopy(reranking_model_params)
|
|
81
|
-
provider = params_copy.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
params_copy.pop('api_key', None)
|
|
86
|
+
provider = params_copy.get('provider', "openai").lower()
|
|
87
|
+
|
|
88
|
+
if "api_key" not in params_copy:
|
|
89
|
+
params_copy["api_key"] = get_api_key(provider, params_copy, strict=False)
|
|
86
90
|
params_copy['model'] = params_copy.pop('model_name', None)
|
|
87
91
|
|
|
88
92
|
return LLMReranker(**params_copy)
|
|
@@ -424,11 +428,12 @@ class KnowledgeBaseTable:
|
|
|
424
428
|
db_handler = self.get_vector_db()
|
|
425
429
|
db_handler.delete(self._kb.vector_database_table)
|
|
426
430
|
|
|
427
|
-
def insert(self, df: pd.DataFrame):
|
|
431
|
+
def insert(self, df: pd.DataFrame, params: dict = None):
|
|
428
432
|
"""Insert dataframe to KB table.
|
|
429
433
|
|
|
430
434
|
Args:
|
|
431
435
|
df: DataFrame to insert
|
|
436
|
+
params: User parameters of insert
|
|
432
437
|
"""
|
|
433
438
|
if df.empty:
|
|
434
439
|
return
|
|
@@ -497,7 +502,12 @@ class KnowledgeBaseTable:
|
|
|
497
502
|
df_emb = self._df_to_embeddings(df)
|
|
498
503
|
df = pd.concat([df, df_emb], axis=1)
|
|
499
504
|
db_handler = self.get_vector_db()
|
|
500
|
-
|
|
505
|
+
|
|
506
|
+
if params is not None and params.get('kb_no_upsert', False):
|
|
507
|
+
# speed up inserting by disable checking existing records
|
|
508
|
+
db_handler.insert(self._kb.vector_database_table, df)
|
|
509
|
+
else:
|
|
510
|
+
db_handler.do_upsert(self._kb.vector_database_table, df)
|
|
501
511
|
|
|
502
512
|
def _adapt_column_names(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
503
513
|
'''
|
|
@@ -243,7 +243,13 @@ class SkillToolController:
|
|
|
243
243
|
kb_table = session_controller.kb_controller.get_table(knowledge_base_name, skill.project_id)
|
|
244
244
|
|
|
245
245
|
res = kb_table.select_query(query)
|
|
246
|
-
|
|
246
|
+
# Handle both chunk_content and content column names
|
|
247
|
+
if hasattr(res, 'chunk_content'):
|
|
248
|
+
return '\n'.join(res.chunk_content)
|
|
249
|
+
elif hasattr(res, 'content'):
|
|
250
|
+
return '\n'.join(res.content)
|
|
251
|
+
else:
|
|
252
|
+
return "No content or chunk_content found in knowledge base response"
|
|
247
253
|
|
|
248
254
|
return _answer_question
|
|
249
255
|
|
|
@@ -12,6 +12,7 @@ from mindsdb_sql_parser.ast import Select, Show, Describe, Explain, Identifier
|
|
|
12
12
|
from mindsdb.utilities import log
|
|
13
13
|
from mindsdb.utilities.context import context as ctx
|
|
14
14
|
from mindsdb.integrations.utilities.query_traversal import query_traversal
|
|
15
|
+
from mindsdb.integrations.libs.response import INF_SCHEMA_COLUMNS_NAMES
|
|
15
16
|
|
|
16
17
|
logger = log.getLogger(__name__)
|
|
17
18
|
|
|
@@ -275,9 +276,13 @@ class SQLAgent:
|
|
|
275
276
|
dn = self._command_executor.session.datahub.get(integration)
|
|
276
277
|
|
|
277
278
|
fields, dtypes = [], []
|
|
278
|
-
for
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
for df in dn.get_table_columns_df(table_name, schema_name):
|
|
280
|
+
df_records = df.to_dict(orient='records')
|
|
281
|
+
fields.append(df_records[INF_SCHEMA_COLUMNS_NAMES.COLUMN_NAME])
|
|
282
|
+
if df_records[INF_SCHEMA_COLUMNS_NAMES.MYSQL_DATA_TYPE] is not None:
|
|
283
|
+
dtypes.append(df_records[INF_SCHEMA_COLUMNS_NAMES.MYSQL_DATA_TYPE].value)
|
|
284
|
+
else:
|
|
285
|
+
dtypes.append(df_records[INF_SCHEMA_COLUMNS_NAMES.DATA_TYPE])
|
|
281
286
|
|
|
282
287
|
info = f'Table named `{table_str}`:\n'
|
|
283
288
|
info += f"\nSample with first {self._sample_rows_in_table_info} rows from table {table_str} in CSV format (dialect is 'excel'):\n"
|
mindsdb/utilities/config.py
CHANGED
|
@@ -209,6 +209,10 @@ class Config:
|
|
|
209
209
|
"restart_on_failure": True,
|
|
210
210
|
"max_restart_count": 1,
|
|
211
211
|
"max_restart_interval_seconds": 60
|
|
212
|
+
},
|
|
213
|
+
"litellm": {
|
|
214
|
+
"host": "0.0.0.0", # API server binds to all interfaces by default
|
|
215
|
+
"port": "8000"
|
|
212
216
|
}
|
|
213
217
|
},
|
|
214
218
|
"cache": {
|
|
@@ -378,7 +382,8 @@ class Config:
|
|
|
378
382
|
):
|
|
379
383
|
self._cmd_args = argparse.Namespace(
|
|
380
384
|
api=None, config=None, install_handlers=None, verbose=False,
|
|
381
|
-
no_studio=False, version=False, ml_task_queue_consumer=None
|
|
385
|
+
no_studio=False, version=False, ml_task_queue_consumer=None,
|
|
386
|
+
agent=None, project=None
|
|
382
387
|
)
|
|
383
388
|
return
|
|
384
389
|
|
|
@@ -390,6 +395,8 @@ class Config:
|
|
|
390
395
|
parser.add_argument('--no_studio', action='store_true')
|
|
391
396
|
parser.add_argument('-v', '--version', action='store_true')
|
|
392
397
|
parser.add_argument('--ml_task_queue_consumer', action='store_true', default=None)
|
|
398
|
+
parser.add_argument('--agent', type=str, default=None, help='Name of the agent to use with litellm APIs')
|
|
399
|
+
parser.add_argument('--project', type=str, default=None, help='Project containing the agent (default: mindsdb)')
|
|
393
400
|
self._cmd_args = parser.parse_args()
|
|
394
401
|
|
|
395
402
|
def fetch_auto_config(self) -> bool:
|
mindsdb/utilities/starters.py
CHANGED
|
@@ -38,3 +38,10 @@ def start_mcp(*args, **kwargs):
|
|
|
38
38
|
from mindsdb.api.mcp.start import start
|
|
39
39
|
|
|
40
40
|
start(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def start_litellm(*args, **kwargs):
|
|
44
|
+
"""Start the LiteLLM server"""
|
|
45
|
+
from mindsdb.api.litellm.start import start
|
|
46
|
+
|
|
47
|
+
start(*args, **kwargs)
|