sqlsaber 0.29.0__py3-none-any.whl → 0.30.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 sqlsaber might be problematic. Click here for more details.

@@ -7,7 +7,6 @@ from sqlsaber.database import BaseDatabaseConnection
7
7
  from sqlsaber.database.schema import SchemaManager
8
8
 
9
9
  from .base import Tool
10
- from .enums import ToolCategory, WorkflowPosition
11
10
  from .registry import register_tool
12
11
  from .sql_guard import add_limit, validate_read_only
13
12
 
@@ -26,11 +25,6 @@ class SQLTool(Tool):
26
25
  self.db = db_connection
27
26
  self.schema_manager = SchemaManager(db_connection)
28
27
 
29
- @property
30
- def category(self) -> ToolCategory:
31
- """SQL tools belong to the 'sql' category."""
32
- return ToolCategory.SQL
33
-
34
28
 
35
29
  @register_tool
36
30
  class ListTablesTool(SQLTool):
@@ -52,18 +46,6 @@ class ListTablesTool(SQLTool):
52
46
  "required": [],
53
47
  }
54
48
 
55
- def get_usage_instructions(self) -> str | None:
56
- """Return usage instructions for this tool."""
57
- return "ALWAYS start with 'list_tables' to see available tables and row counts. Use this first to discover available tables."
58
-
59
- def get_priority(self) -> int:
60
- """Return priority for tool ordering."""
61
- return 10 # High priority - should be used first
62
-
63
- def get_workflow_position(self) -> WorkflowPosition:
64
- """Return workflow position."""
65
- return WorkflowPosition.DISCOVERY
66
-
67
49
  async def execute(self, **kwargs) -> str:
68
50
  """List all tables in the database."""
69
51
  if not self.db or not self.schema_manager:
@@ -101,18 +83,6 @@ class IntrospectSchemaTool(SQLTool):
101
83
  "required": [],
102
84
  }
103
85
 
104
- def get_usage_instructions(self) -> str | None:
105
- """Return usage instructions for this tool."""
106
- return "Use 'introspect_schema' with a table_pattern to get details ONLY for relevant tables. Use table patterns like 'sample%' or '%experiment%' to filter related tables."
107
-
108
- def get_priority(self) -> int:
109
- """Return priority for tool ordering."""
110
- return 20 # Should come after list_tables
111
-
112
- def get_workflow_position(self) -> WorkflowPosition:
113
- """Return workflow position."""
114
- return WorkflowPosition.ANALYSIS
115
-
116
86
  async def execute(self, **kwargs) -> str:
117
87
  """Introspect database schema."""
118
88
  if not self.db or not self.schema_manager:
@@ -184,18 +154,6 @@ class ExecuteSQLTool(SQLTool):
184
154
  "required": ["query"],
185
155
  }
186
156
 
187
- def get_usage_instructions(self) -> str | None:
188
- """Return usage instructions for this tool."""
189
- return "Execute SQL queries safely with automatic LIMIT clauses for SELECT statements. Only SELECT queries are permitted for security."
190
-
191
- def get_priority(self) -> int:
192
- """Return priority for tool ordering."""
193
- return 30 # Should come after schema tools
194
-
195
- def get_workflow_position(self) -> WorkflowPosition:
196
- """Return workflow position."""
197
- return WorkflowPosition.EXECUTION
198
-
199
157
  async def execute(self, **kwargs) -> str:
200
158
  """Execute a SQL query."""
201
159
  if not self.db:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.29.0
3
+ Version: 0.30.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -9,7 +9,6 @@ Requires-Dist: aiosqlite>=0.21.0
9
9
  Requires-Dist: asyncpg>=0.30.0
10
10
  Requires-Dist: cyclopts>=3.22.1
11
11
  Requires-Dist: duckdb>=0.9.2
12
- Requires-Dist: fastmcp>=2.9.0
13
12
  Requires-Dist: httpx>=0.28.1
14
13
  Requires-Dist: keyring>=25.6.0
15
14
  Requires-Dist: platformdirs>=4.0.0
@@ -44,10 +43,7 @@ Ask your questions in natural language and `sqlsaber` will gather the right cont
44
43
  - [Resume Past Conversation](#resume-past-conversation)
45
44
  - [Database Selection](#database-selection)
46
45
  - [Examples](#examples)
47
- - [MCP Server Integration](#mcp-server-integration)
48
- - [Starting the MCP Server](#starting-the-mcp-server)
49
- - [Configuring MCP Clients](#configuring-mcp-clients)
50
- - [Available MCP Tools](#available-mcp-tools)
46
+
51
47
  - [How It Works](#how-it-works)
52
48
  - [Contributing](#contributing)
53
49
  - [License](#license)
@@ -60,7 +56,7 @@ Ask your questions in natural language and `sqlsaber` will gather the right cont
60
56
  - Interactive REPL mode
61
57
  - Conversation threads (store, display, and resume conversations)
62
58
  - Support for PostgreSQL, MySQL, SQLite, DuckDB, and CSVs
63
- - MCP (Model Context Protocol) server support
59
+
64
60
  - Extended thinking mode for select models (Anthropic, OpenAI, Google, Groq)
65
61
  - Beautiful formatted output
66
62
 
@@ -220,43 +216,6 @@ saber "show me orders with customer details for this week"
220
216
  saber "which products had the highest sales growth last quarter?"
221
217
  ```
222
218
 
223
- ## MCP Server Integration
224
-
225
- SQLSaber includes an MCP (Model Context Protocol) server that allows AI agents like Claude Code to directly leverage tools available in SQLSaber.
226
-
227
- ### Starting the MCP Server
228
-
229
- Run the MCP server using uvx:
230
-
231
- ```bash
232
- uvx --from sqlsaber saber-mcp
233
- ```
234
-
235
- ### Configuring MCP Clients
236
-
237
- #### Claude Code
238
-
239
- Add SQLSaber as an MCP server in Claude Code:
240
-
241
- ```bash
242
- claude mcp add sqlsaber -- uvx --from sqlsaber saber-mcp
243
- ```
244
-
245
- #### Other MCP Clients
246
-
247
- For other MCP clients, configure them to run the command: `uvx --from sqlsaber saber-mcp`
248
-
249
- ### Available MCP Tools
250
-
251
- Once connected, the MCP client will have access to these tools:
252
-
253
- - `get_databases()` - Lists all configured databases
254
- - `list_tables(database)` - Get all tables in a database with row counts
255
- - `introspect_schema(database, table_pattern?)` - Get detailed schema information
256
- - `execute_sql(database, query, limit?)` - Execute SQL queries (read-only)
257
-
258
- The MCP server uses your existing SQLSaber database configurations, so make sure to set up your databases using `saber db add` first.
259
-
260
219
  ## How It Works
261
220
 
262
221
  SQLsaber uses a multi-step agentic process to gather the right context and execute SQL queries to answer your questions:
@@ -0,0 +1,57 @@
1
+ sqlsaber/__init__.py,sha256=HjS8ULtP4MGpnTL7njVY45NKV9Fi4e_yeYuY-hyXWQc,73
2
+ sqlsaber/__main__.py,sha256=RIHxWeWh2QvLfah-2OkhI5IJxojWfy4fXpMnVEJYvxw,78
3
+ sqlsaber/agents/__init__.py,sha256=qYI6rLY4q5AbF47vXH5RVoM08-yQjymBSaePh4lFIW4,116
4
+ sqlsaber/agents/base.py,sha256=T05UsMZPwAlMhsJFpuuVI1RNDhdiwiEsgCWr9MbPoAU,2654
5
+ sqlsaber/agents/pydantic_ai_agent.py,sha256=cHHwQHJf9TqrBhItWJgnScL31lvyvKLcCBTSjRSwWug,12002
6
+ sqlsaber/application/__init__.py,sha256=KY_-d5nEdQyAwNOsK5r-f7Tb69c63XbuEkHPeLpJal8,84
7
+ sqlsaber/application/auth_setup.py,sha256=D94dyU9bOVfnNHLnnFJb5PaeWsKPTL21CiS_DLcY93A,5114
8
+ sqlsaber/application/db_setup.py,sha256=ZSgR9rJJVHttIjsbYQS9GEIyzkM09k5RLrVGdegrfYc,6859
9
+ sqlsaber/application/model_selection.py,sha256=fSC06MZNKinHDR-csMFVYYJFyK8MydKf6pStof74Jp0,3191
10
+ sqlsaber/application/prompts.py,sha256=4rMGcWpYJbNWPMzqVWseUMx0nwvXOkWS6GaTAJ5mhfc,3473
11
+ sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
12
+ sqlsaber/cli/auth.py,sha256=elUpw8gypHGlxbHx4a4_z4wFznx2vr6V1h8lqpeC6OQ,6121
13
+ sqlsaber/cli/commands.py,sha256=Pii_SlVKpNEtt57_QPQzwC1u-x6tA8kuG8yd43undWE,8628
14
+ sqlsaber/cli/completers.py,sha256=g-hLDq5fiBx7gg8Bte1Lq8GU-ZxCYVs4dcPsmHPIcK4,6574
15
+ sqlsaber/cli/database.py,sha256=Tqy8H5MnjsrmOSPcbA5Qy-u-IOYJCIXRJVhk0veLNDk,10726
16
+ sqlsaber/cli/display.py,sha256=WB5JCumhXadziDEX1EZHG3vN1Chol5FNAaTXHieqFK0,17892
17
+ sqlsaber/cli/interactive.py,sha256=PcY6mszImo_3PsqjjWmx_cOfj44OmKvD9ENOvGA-wjU,13715
18
+ sqlsaber/cli/memory.py,sha256=IKq09DUbqpvvtATsyDlpm7rDlGqWEhdUX9wgnR-oiq4,7850
19
+ sqlsaber/cli/models.py,sha256=nbn75gCnkRciGt4Q47yxa8wImiZcCkDdQZNVeehDim8,8530
20
+ sqlsaber/cli/onboarding.py,sha256=iBGT-W-OJFRvQoEpuHYyO1c9Mym5c97eIefRvxGHtTg,11265
21
+ sqlsaber/cli/streaming.py,sha256=eggj25ZlA-xKrAF726S29vfS2MHTFC5wTmgXLbS-RvM,6515
22
+ sqlsaber/cli/theme.py,sha256=hP0kmsMLCtqaT7b5wB1dk1hW1hV94oP4BHdz8S6887A,4243
23
+ sqlsaber/cli/threads.py,sha256=o9q9Hst1Wt7cxSyrpAtwG6pkUct6csgiAmN_0P_WO3k,13637
24
+ sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
25
+ sqlsaber/config/api_keys.py,sha256=9RyhD5Bntq8NMFRPiZZo8YEHACK9MPyFGp8dsmQZ1iI,3678
26
+ sqlsaber/config/auth.py,sha256=b5qB2h1doXyO9Bn8z0CcL8LAR2jF431gGXBGKLgTmtQ,2756
27
+ sqlsaber/config/database.py,sha256=Yec6_0wdzq-ADblMNnbgvouYCimYOY_DWHT9oweaISc,11449
28
+ sqlsaber/config/oauth_flow.py,sha256=P81lHhtICdhiQu8lNwyqn2m45FGEqCEzLgUQTLG5UW0,10343
29
+ sqlsaber/config/oauth_tokens.py,sha256=V4U8GAQHjTfgUcTzwjRVaIE7DeN0tF9OsSjiasHw7Uc,5970
30
+ sqlsaber/config/providers.py,sha256=JFjeJv1K5Q93zWSlWq3hAvgch1TlgoF0qFa0KJROkKY,2957
31
+ sqlsaber/config/settings.py,sha256=iB4CnGQ4hw8gxkaa9CVLB_JEy6Y9h9FQTAams5OCVyI,6421
32
+ sqlsaber/database/__init__.py,sha256=Gi9N_NOkD459WRWXDg3hSuGoBs3xWbMDRBvsTVmnGAg,2025
33
+ sqlsaber/database/base.py,sha256=oaipLxlvoylX6oJCITPAWWqRqv09hRELqqEBufsmFic,3703
34
+ sqlsaber/database/csv.py,sha256=41wuP40FaGPfj28HMiD0I69uG0JbUxArpoTLC3MG2uc,4464
35
+ sqlsaber/database/duckdb.py,sha256=8HNKdx208aFK_YtwGjLz6LTne0xEmNevD-f9dRWlrFg,11244
36
+ sqlsaber/database/mysql.py,sha256=wMzDQqq4GFbfEdqXtv_sCb4Qbr9GSWqYAvOLeo5UryY,14472
37
+ sqlsaber/database/postgresql.py,sha256=fuf2Wl29NKXvD3mqsR08PDleNQ1PG-fNvWSxT6HDh2M,13223
38
+ sqlsaber/database/resolver.py,sha256=wSCcn__aCqwIfpt_LCjtW2Zgb8RpG5PlmwwZHli1q_U,3628
39
+ sqlsaber/database/schema.py,sha256=CuV0ewoVaERe1gj_fJFJFWAP8aEPgepmn6X6B7bgkfQ,6962
40
+ sqlsaber/database/sqlite.py,sha256=iReEIiSpkhhS1VzITd79ZWqSL3fHMyfe3DRCDpM0DvE,9421
41
+ sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
42
+ sqlsaber/memory/manager.py,sha256=p3fybMVfH-E4ApT1ZRZUnQIWSk9dkfUPCyfkmA0HALs,2739
43
+ sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,5745
44
+ sqlsaber/theme/__init__.py,sha256=qCICX1Cg4B6yCbZ1UrerxglWxcqldRFVSRrSs73na_8,188
45
+ sqlsaber/theme/manager.py,sha256=TPourIKGU-UzHtImgexgtazpuDaFhqUYtVauMblgGAQ,6480
46
+ sqlsaber/threads/__init__.py,sha256=Hh3dIG1tuC8fXprREUpslCIgPYz8_6o7aRLx4yNeO48,139
47
+ sqlsaber/threads/storage.py,sha256=rsUdxT4CR52D7xtGir9UlsFnBMk11jZeflzDrk2q4ME,11183
48
+ sqlsaber/tools/__init__.py,sha256=O6eqkMk8mkhYDniQD1eYgAElOjiHz03I2bGARdgkDkk,421
49
+ sqlsaber/tools/base.py,sha256=NKEEooliPKTJj_Pomwte_wW0Xd9Z5kXNfVdCRfTppuw,883
50
+ sqlsaber/tools/registry.py,sha256=XmBzERq0LJXtg3BZ-r8cEyt8J54NUekgUlTJ_EdSYMk,2204
51
+ sqlsaber/tools/sql_guard.py,sha256=dTDwcZP-N4xPGzcr7MQtKUxKrlDzlc1irr9aH5a4wvk,6182
52
+ sqlsaber/tools/sql_tools.py,sha256=eo-NTxiXGHMopAjujvDDjmv9hf5bQNbiy3nTpxoJ_E8,7369
53
+ sqlsaber-0.30.0.dist-info/METADATA,sha256=8fYZ1qVQDu-oNhxPSTeLDwewRz1OMy3BK5iTF80nHaQ,5823
54
+ sqlsaber-0.30.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
55
+ sqlsaber-0.30.0.dist-info/entry_points.txt,sha256=tw1mB0fjlkXQiOsC0434X6nE-o1cFCuQwt2ZYHv_WAE,91
56
+ sqlsaber-0.30.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
57
+ sqlsaber-0.30.0.dist-info/RECORD,,
@@ -1,5 +1,3 @@
1
1
  [console_scripts]
2
2
  saber = sqlsaber.cli.commands:main
3
- saber-mcp = sqlsaber.mcp.mcp:main
4
3
  sqlsaber = sqlsaber.cli.commands:main
5
- sqlsaber-mcp = sqlsaber.mcp.mcp:main
sqlsaber/agents/mcp.py DELETED
@@ -1,21 +0,0 @@
1
- """Generic SQL agent implementation for MCP tools."""
2
-
3
- from typing import AsyncIterator
4
-
5
- from sqlsaber.agents.base import BaseSQLAgent
6
- from sqlsaber.database import BaseDatabaseConnection
7
-
8
-
9
- class MCPSQLAgent(BaseSQLAgent):
10
- """MCP SQL Agent for MCP tool operations without LLM-specific logic."""
11
-
12
- def __init__(self, db_connection: BaseDatabaseConnection):
13
- super().__init__(db_connection)
14
-
15
- async def query_stream(
16
- self, user_query: str, use_history: bool = True
17
- ) -> AsyncIterator:
18
- """Not implemented for generic agent as it's only used for tool operations."""
19
- raise NotImplementedError(
20
- "MCPSQLAgent does not support query streaming. Use specific agent implementations for conversation."
21
- )
sqlsaber/mcp/__init__.py DELETED
@@ -1,5 +0,0 @@
1
- """MCP (Model Context Protocol) server implementation for SQLSaber."""
2
-
3
- from .mcp import mcp
4
-
5
- __all__ = ["mcp"]
sqlsaber/mcp/mcp.py DELETED
@@ -1,129 +0,0 @@
1
- """FastMCP server implementation for SQLSaber."""
2
-
3
- import json
4
-
5
- from fastmcp import FastMCP
6
-
7
- from sqlsaber.agents.mcp import MCPSQLAgent
8
- from sqlsaber.config.database import DatabaseConfigManager
9
- from sqlsaber.database import DatabaseConnection
10
- from sqlsaber.tools import SQLTool, tool_registry
11
- from sqlsaber.tools.instructions import InstructionBuilder
12
-
13
- # Initialize the instruction builder
14
- instruction_builder = InstructionBuilder(tool_registry)
15
-
16
- # Generate dynamic instructions
17
- DYNAMIC_INSTRUCTIONS = instruction_builder.build_mcp_instructions()
18
-
19
- # Create the FastMCP server instance with dynamic instructions
20
- mcp = FastMCP(name="SQL Assistant", instructions=DYNAMIC_INSTRUCTIONS)
21
-
22
- # Initialize the database config manager
23
- config_manager = DatabaseConfigManager()
24
-
25
-
26
- async def _create_agent_for_database(database_name: str) -> MCPSQLAgent | None:
27
- """Create a MCPSQLAgent for the specified database."""
28
- try:
29
- # Look up configured database connection
30
- db_config = config_manager.get_database(database_name)
31
- if not db_config:
32
- return None
33
- connection_string = db_config.to_connection_string()
34
-
35
- # Create database connection
36
- db_conn = DatabaseConnection(connection_string)
37
-
38
- # Create and return the agent
39
- agent = MCPSQLAgent(db_conn)
40
- return agent
41
-
42
- except Exception:
43
- return None
44
-
45
-
46
- @mcp.tool
47
- def get_databases() -> dict:
48
- """List all configured databases with their types."""
49
- databases = []
50
- for db_config in config_manager.list_databases():
51
- databases.append(
52
- {
53
- "name": db_config.name,
54
- "type": db_config.type,
55
- "database": db_config.database,
56
- "host": db_config.host,
57
- "port": db_config.port,
58
- "is_default": db_config.name == config_manager.get_default_name(),
59
- }
60
- )
61
-
62
- return {"databases": databases, "count": len(databases)}
63
-
64
-
65
- async def _execute_with_connection(tool_name: str, database: str, **kwargs) -> str:
66
- """Execute a SQL tool with database connection management.
67
-
68
- Args:
69
- tool_name: Name of the tool to execute
70
- database: Database name to connect to
71
- **kwargs: Tool-specific parameters
72
-
73
- Returns:
74
- JSON string with the tool's output
75
- """
76
- try:
77
- agent = await _create_agent_for_database(database)
78
- if not agent:
79
- return json.dumps(
80
- {"error": f"Database '{database}' not found or could not connect"}
81
- )
82
-
83
- # Get the tool and set up connection
84
- tool = tool_registry.get_tool(tool_name)
85
- if isinstance(tool, SQLTool):
86
- tool.set_connection(agent.db)
87
-
88
- # Execute the tool
89
- result = await tool.execute(**kwargs)
90
- await agent.db.close()
91
- return result
92
-
93
- except Exception as e:
94
- return json.dumps({"error": f"Error in {tool_name}: {str(e)}"})
95
-
96
-
97
- # SQL Tool Wrappers with explicit signatures
98
-
99
-
100
- @mcp.tool
101
- async def list_tables(database: str) -> str:
102
- """Get a list of all tables in the database with row counts. Use this first to discover available tables."""
103
- return await _execute_with_connection("list_tables", database)
104
-
105
-
106
- @mcp.tool
107
- async def introspect_schema(database: str, table_pattern: str = None) -> str:
108
- """Introspect database schema to understand table structures."""
109
- kwargs = {}
110
- if table_pattern is not None:
111
- kwargs["table_pattern"] = table_pattern
112
- return await _execute_with_connection("introspect_schema", database, **kwargs)
113
-
114
-
115
- @mcp.tool
116
- async def execute_sql(database: str, query: str, limit: int = 100) -> str:
117
- """Execute a SQL query against the database."""
118
- return await _execute_with_connection(
119
- "execute_sql", database, query=query, limit=limit
120
- )
121
-
122
-
123
- def main():
124
- """Entry point for the MCP server console script."""
125
- mcp.run()
126
-
127
-
128
- if __name__ == "__main__":
129
- main()
sqlsaber/tools/enums.py DELETED
@@ -1,19 +0,0 @@
1
- """Enums for tool categories and workflow positions."""
2
-
3
- from enum import Enum
4
-
5
-
6
- class ToolCategory(Enum):
7
- """Tool categories for organizing and filtering tools."""
8
-
9
- GENERAL = "general"
10
- SQL = "sql"
11
-
12
-
13
- class WorkflowPosition(Enum):
14
- """Workflow positions for organizing tools by usage order."""
15
-
16
- DISCOVERY = "discovery"
17
- ANALYSIS = "analysis"
18
- EXECUTION = "execution"
19
- OTHER = "other"
@@ -1,231 +0,0 @@
1
- """Dynamic instruction builder for tools."""
2
-
3
- from .base import Tool
4
- from .enums import ToolCategory, WorkflowPosition
5
- from .registry import ToolRegistry
6
-
7
-
8
- class InstructionBuilder:
9
- """Builds dynamic instructions based on available tools."""
10
-
11
- def __init__(self, tool_registry: ToolRegistry):
12
- """Initialize with a tool registry."""
13
- self.registry = tool_registry
14
-
15
- def build_instructions(
16
- self,
17
- db_type: str = "database",
18
- category: str | ToolCategory | None = None,
19
- include_base_instructions: bool = True,
20
- ) -> str:
21
- """Build dynamic instructions from available tools.
22
-
23
- Args:
24
- db_type: Type of database (PostgreSQL, MySQL, SQLite, etc.)
25
- category: Optional category to filter tools by (string or ToolCategory enum)
26
- include_base_instructions: Whether to include base SQL assistant instructions
27
-
28
- Returns:
29
- Complete instruction string for LLM
30
- """
31
- # Get available tools
32
- tools = self.registry.get_all_tools(category)
33
-
34
- if not tools:
35
- return self._get_base_instructions(db_type)
36
-
37
- # Sort tools by priority and workflow position
38
- sorted_tools = self._sort_tools_by_workflow(tools)
39
-
40
- # Build instruction components
41
- instructions_parts = []
42
-
43
- if include_base_instructions:
44
- instructions_parts.append(self._get_base_instructions(db_type))
45
-
46
- # Add tool-specific workflow guidance
47
- workflow_instructions = self._build_workflow_instructions(sorted_tools)
48
- if workflow_instructions:
49
- instructions_parts.append(workflow_instructions)
50
-
51
- # Add tool descriptions and guidelines
52
- tool_guidelines = self._build_tool_guidelines(sorted_tools)
53
- if tool_guidelines:
54
- instructions_parts.append(tool_guidelines)
55
-
56
- # Add general guidelines
57
- general_guidelines = self._build_general_guidelines(sorted_tools)
58
- if general_guidelines:
59
- instructions_parts.append(general_guidelines)
60
-
61
- return "\n\n".join(instructions_parts)
62
-
63
- def _get_base_instructions(self, db_type: str) -> str:
64
- """Get base SQL assistant instructions."""
65
- return f"""You are also a helpful SQL assistant that helps users query their {db_type} database.
66
-
67
- Your responsibilities:
68
- 1. Understand user's natural language requests, think and convert them to SQL
69
- 2. Use the provided tools efficiently to explore database schema
70
- 3. Generate appropriate SQL queries
71
- 4. Execute queries safely - queries that modify the database are not allowed
72
- 5. Format and explain results clearly"""
73
-
74
- def _sort_tools_by_workflow(self, tools: list[Tool]) -> list[Tool]:
75
- """Sort tools by priority and workflow position."""
76
- # Define workflow position ordering
77
- position_order = {
78
- WorkflowPosition.DISCOVERY: 1,
79
- WorkflowPosition.ANALYSIS: 2,
80
- WorkflowPosition.EXECUTION: 3,
81
- WorkflowPosition.OTHER: 4,
82
- }
83
-
84
- return sorted(
85
- tools,
86
- key=lambda tool: (
87
- position_order.get(tool.get_workflow_position(), 4),
88
- tool.get_priority(),
89
- tool.name,
90
- ),
91
- )
92
-
93
- def _build_workflow_instructions(self, sorted_tools: list[Tool]) -> str:
94
- """Build workflow-based instructions."""
95
- # Group tools by workflow position
96
- workflow_groups = {}
97
- for tool in sorted_tools:
98
- position = tool.get_workflow_position()
99
- if position not in workflow_groups:
100
- workflow_groups[position] = []
101
- workflow_groups[position].append(tool)
102
-
103
- # Build workflow instructions
104
- instructions = ["IMPORTANT - Tool Usage Strategy:"]
105
- step = 1
106
-
107
- # Add discovery tools first
108
- if WorkflowPosition.DISCOVERY in workflow_groups:
109
- discovery_tools = workflow_groups[WorkflowPosition.DISCOVERY]
110
- for tool in discovery_tools:
111
- usage = tool.get_usage_instructions()
112
- if usage:
113
- instructions.append(f"{step}. {usage}")
114
- else:
115
- instructions.append(
116
- f"{step}. Use '{tool.name}' to {tool.description.lower()}"
117
- )
118
- step += 1
119
-
120
- # Add analysis tools
121
- if WorkflowPosition.ANALYSIS in workflow_groups:
122
- analysis_tools = workflow_groups[WorkflowPosition.ANALYSIS]
123
- for tool in analysis_tools:
124
- usage = tool.get_usage_instructions()
125
- if usage:
126
- instructions.append(f"{step}. {usage}")
127
- else:
128
- instructions.append(
129
- f"{step}. Use '{tool.name}' to {tool.description.lower()}"
130
- )
131
- step += 1
132
-
133
- # Add execution tools
134
- if WorkflowPosition.EXECUTION in workflow_groups:
135
- execution_tools = workflow_groups[WorkflowPosition.EXECUTION]
136
- for tool in execution_tools:
137
- usage = tool.get_usage_instructions()
138
- if usage:
139
- instructions.append(f"{step}. {usage}")
140
- else:
141
- instructions.append(
142
- f"{step}. Use '{tool.name}' to {tool.description.lower()}"
143
- )
144
- step += 1
145
-
146
- return "\n".join(instructions) if len(instructions) > 1 else ""
147
-
148
- def _build_tool_guidelines(self, sorted_tools: list[Tool]) -> str:
149
- """Build tool-specific guidelines."""
150
- guidelines = []
151
-
152
- for tool in sorted_tools:
153
- usage = tool.get_usage_instructions()
154
- if usage and not self._is_usage_in_workflow(usage):
155
- guidelines.append(f"- {tool.name}: {usage}")
156
-
157
- if guidelines:
158
- return "Tool-Specific Guidelines:\n" + "\n".join(guidelines)
159
- return ""
160
-
161
- def _build_general_guidelines(self, sorted_tools: list[Tool]) -> str:
162
- """Build general usage guidelines."""
163
- guidelines = [
164
- "Guidelines:",
165
- "- Use proper JOIN syntax and avoid cartesian products",
166
- "- Include appropriate WHERE clauses to limit results",
167
- "- Explain what the query does in simple terms",
168
- "- Handle errors gracefully and suggest fixes",
169
- "- Be security conscious - use parameterized queries when needed",
170
- ]
171
-
172
- # Add category-specific guidelines
173
- categories = {tool.category for tool in sorted_tools}
174
-
175
- if ToolCategory.SQL in categories:
176
- guidelines.extend(
177
- [
178
- "- Timestamp columns must be converted to text when you write queries",
179
- "- Use table patterns like 'sample%' or '%experiment%' to filter related tables",
180
- ]
181
- )
182
-
183
- return "\n".join(guidelines)
184
-
185
- def _is_usage_in_workflow(self, usage: str) -> bool:
186
- """Check if usage instruction is already covered in workflow section."""
187
- # Simple heuristic - if usage starts with workflow words, it's probably in workflow
188
- workflow_words = ["always start", "first", "use this", "begin with", "start by"]
189
- usage_lower = usage.lower()
190
- return any(word in usage_lower for word in workflow_words)
191
-
192
- def build_mcp_instructions(self) -> str:
193
- """Build instructions specifically for MCP server."""
194
- instructions = [
195
- "This server provides helpful resources and tools that will help you address users queries on their database.",
196
- "",
197
- ]
198
-
199
- # Add database discovery
200
- instructions.append("- Get all databases using `get_databases()`")
201
-
202
- # Add tool-specific instructions
203
- sql_tools = self.registry.get_all_tools(category=ToolCategory.SQL)
204
- sorted_tools = self._sort_tools_by_workflow(sql_tools)
205
-
206
- for tool in sorted_tools:
207
- instructions.append(f"- Call `{tool.name}()` to {tool.description.lower()}")
208
-
209
- # Add workflow guidelines
210
- instructions.extend(["", "Guidelines:"])
211
-
212
- workflow_instructions = self._build_workflow_instructions(sorted_tools)
213
- if workflow_instructions:
214
- # Extract just the numbered steps without the "IMPORTANT" header
215
- lines = workflow_instructions.split("\n")[1:] # Skip header
216
- for line in lines:
217
- if line.strip():
218
- # Convert numbered steps to bullet points
219
- if line.strip()[0].isdigit():
220
- instructions.append(f"- {line.strip()[3:]}") # Remove "X. "
221
-
222
- # Add general guidelines
223
- instructions.extend(
224
- [
225
- "- Use proper JOIN syntax and avoid cartesian products",
226
- "- Include appropriate WHERE clauses to limit results",
227
- "- Handle errors gracefully and suggest fixes",
228
- ]
229
- )
230
-
231
- return "\n".join(instructions)