sqlsaber 0.14.0__py3-none-any.whl → 0.16.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.
- sqlsaber/agents/__init__.py +2 -4
- sqlsaber/agents/base.py +18 -221
- sqlsaber/agents/mcp.py +2 -2
- sqlsaber/agents/pydantic_ai_agent.py +170 -0
- sqlsaber/cli/auth.py +146 -79
- sqlsaber/cli/commands.py +22 -7
- sqlsaber/cli/database.py +1 -1
- sqlsaber/cli/interactive.py +65 -30
- sqlsaber/cli/models.py +58 -29
- sqlsaber/cli/streaming.py +114 -77
- sqlsaber/config/api_keys.py +9 -11
- sqlsaber/config/providers.py +116 -0
- sqlsaber/config/settings.py +50 -30
- sqlsaber/database/connection.py +3 -3
- sqlsaber/mcp/mcp.py +43 -51
- sqlsaber/models/__init__.py +0 -3
- sqlsaber/tools/__init__.py +25 -0
- sqlsaber/tools/base.py +85 -0
- sqlsaber/tools/enums.py +21 -0
- sqlsaber/tools/instructions.py +251 -0
- sqlsaber/tools/registry.py +130 -0
- sqlsaber/tools/sql_tools.py +275 -0
- sqlsaber/tools/visualization_tools.py +144 -0
- {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/METADATA +20 -39
- sqlsaber-0.16.0.dist-info/RECORD +51 -0
- sqlsaber/agents/anthropic.py +0 -579
- sqlsaber/agents/streaming.py +0 -16
- sqlsaber/clients/__init__.py +0 -6
- sqlsaber/clients/anthropic.py +0 -285
- sqlsaber/clients/base.py +0 -31
- sqlsaber/clients/exceptions.py +0 -117
- sqlsaber/clients/models.py +0 -282
- sqlsaber/clients/streaming.py +0 -257
- sqlsaber/models/events.py +0 -28
- sqlsaber-0.14.0.dist-info/RECORD +0 -51
- {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/WHEEL +0 -0
- {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/entry_points.txt +0 -0
- {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""SQL-related tools for database operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sqlsaber.database.connection import BaseDatabaseConnection
|
|
7
|
+
from sqlsaber.database.schema import SchemaManager
|
|
8
|
+
|
|
9
|
+
from .base import Tool
|
|
10
|
+
from .enums import ToolCategory, WorkflowPosition
|
|
11
|
+
from .registry import register_tool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SQLTool(Tool):
|
|
15
|
+
"""Base class for SQL tools that need database access."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, db_connection: BaseDatabaseConnection | None = None):
|
|
18
|
+
"""Initialize with optional database connection."""
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.db = db_connection
|
|
21
|
+
self.schema_manager = SchemaManager(db_connection) if db_connection else None
|
|
22
|
+
|
|
23
|
+
def set_connection(self, db_connection: BaseDatabaseConnection) -> None:
|
|
24
|
+
"""Set the database connection after initialization."""
|
|
25
|
+
self.db = db_connection
|
|
26
|
+
self.schema_manager = SchemaManager(db_connection)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def category(self) -> ToolCategory:
|
|
30
|
+
"""SQL tools belong to the 'sql' category."""
|
|
31
|
+
return ToolCategory.SQL
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@register_tool
|
|
35
|
+
class ListTablesTool(SQLTool):
|
|
36
|
+
"""Tool for listing database tables."""
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def name(self) -> str:
|
|
40
|
+
return "list_tables"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def description(self) -> str:
|
|
44
|
+
return "Get a list of all tables in the database with row counts. Use this first to discover available tables."
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def input_schema(self) -> dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {},
|
|
51
|
+
"required": [],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def get_usage_instructions(self) -> str | None:
|
|
55
|
+
"""Return usage instructions for this tool."""
|
|
56
|
+
return "ALWAYS start with 'list_tables' to see available tables and row counts. Use this first to discover available tables."
|
|
57
|
+
|
|
58
|
+
def get_priority(self) -> int:
|
|
59
|
+
"""Return priority for tool ordering."""
|
|
60
|
+
return 10 # High priority - should be used first
|
|
61
|
+
|
|
62
|
+
def get_workflow_position(self) -> WorkflowPosition:
|
|
63
|
+
"""Return workflow position."""
|
|
64
|
+
return WorkflowPosition.DISCOVERY
|
|
65
|
+
|
|
66
|
+
async def execute(self, **kwargs) -> str:
|
|
67
|
+
"""List all tables in the database."""
|
|
68
|
+
if not self.db or not self.schema_manager:
|
|
69
|
+
return json.dumps({"error": "No database connection available"})
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
tables_info = await self.schema_manager.list_tables()
|
|
73
|
+
return json.dumps(tables_info)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return json.dumps({"error": f"Error listing tables: {str(e)}"})
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@register_tool
|
|
79
|
+
class IntrospectSchemaTool(SQLTool):
|
|
80
|
+
"""Tool for introspecting database schema."""
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def name(self) -> str:
|
|
84
|
+
return "introspect_schema"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def description(self) -> str:
|
|
88
|
+
return "Introspect database schema to understand table structures."
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def input_schema(self) -> dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"table_pattern": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Optional pattern to filter tables (e.g., 'public.users', 'user%', '%order%')",
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"required": [],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def get_usage_instructions(self) -> str | None:
|
|
104
|
+
"""Return usage instructions for this tool."""
|
|
105
|
+
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."
|
|
106
|
+
|
|
107
|
+
def get_priority(self) -> int:
|
|
108
|
+
"""Return priority for tool ordering."""
|
|
109
|
+
return 20 # Should come after list_tables
|
|
110
|
+
|
|
111
|
+
def get_workflow_position(self) -> WorkflowPosition:
|
|
112
|
+
"""Return workflow position."""
|
|
113
|
+
return WorkflowPosition.ANALYSIS
|
|
114
|
+
|
|
115
|
+
async def execute(self, **kwargs) -> str:
|
|
116
|
+
"""Introspect database schema."""
|
|
117
|
+
if not self.db or not self.schema_manager:
|
|
118
|
+
return json.dumps({"error": "No database connection available"})
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
table_pattern = kwargs.get("table_pattern")
|
|
122
|
+
schema_info = await self.schema_manager.get_schema_info(table_pattern)
|
|
123
|
+
|
|
124
|
+
# Format the schema information
|
|
125
|
+
formatted_info = {}
|
|
126
|
+
for table_name, table_info in schema_info.items():
|
|
127
|
+
formatted_info[table_name] = {
|
|
128
|
+
"columns": {
|
|
129
|
+
col_name: {
|
|
130
|
+
"type": col_info["data_type"],
|
|
131
|
+
"nullable": col_info["nullable"],
|
|
132
|
+
"default": col_info["default"],
|
|
133
|
+
}
|
|
134
|
+
for col_name, col_info in table_info["columns"].items()
|
|
135
|
+
},
|
|
136
|
+
"primary_keys": table_info["primary_keys"],
|
|
137
|
+
"foreign_keys": [
|
|
138
|
+
f"{fk['column']} -> {fk['references']['table']}.{fk['references']['column']}"
|
|
139
|
+
for fk in table_info["foreign_keys"]
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return json.dumps(formatted_info)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return json.dumps({"error": f"Error introspecting schema: {str(e)}"})
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@register_tool
|
|
149
|
+
class ExecuteSQLTool(SQLTool):
|
|
150
|
+
"""Tool for executing SQL queries."""
|
|
151
|
+
|
|
152
|
+
DEFAULT_LIMIT = 100
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def name(self) -> str:
|
|
156
|
+
return "execute_sql"
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def description(self) -> str:
|
|
160
|
+
return "Execute a SQL query against the database."
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def input_schema(self) -> dict[str, Any]:
|
|
164
|
+
return {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"properties": {
|
|
167
|
+
"query": {
|
|
168
|
+
"type": "string",
|
|
169
|
+
"description": "SQL query to execute",
|
|
170
|
+
},
|
|
171
|
+
"limit": {
|
|
172
|
+
"type": "integer",
|
|
173
|
+
"description": f"Maximum number of rows to return (default: {self.DEFAULT_LIMIT})",
|
|
174
|
+
"default": self.DEFAULT_LIMIT,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
"required": ["query"],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def get_usage_instructions(self) -> str | None:
|
|
181
|
+
"""Return usage instructions for this tool."""
|
|
182
|
+
return "Execute SQL queries safely with automatic LIMIT clauses for SELECT statements. Only SELECT queries are permitted for security."
|
|
183
|
+
|
|
184
|
+
def get_priority(self) -> int:
|
|
185
|
+
"""Return priority for tool ordering."""
|
|
186
|
+
return 30 # Should come after schema tools
|
|
187
|
+
|
|
188
|
+
def get_workflow_position(self) -> WorkflowPosition:
|
|
189
|
+
"""Return workflow position."""
|
|
190
|
+
return WorkflowPosition.EXECUTION
|
|
191
|
+
|
|
192
|
+
async def execute(self, **kwargs) -> str:
|
|
193
|
+
"""Execute a SQL query."""
|
|
194
|
+
if not self.db:
|
|
195
|
+
return json.dumps({"error": "No database connection available"})
|
|
196
|
+
|
|
197
|
+
query = kwargs.get("query")
|
|
198
|
+
if not query:
|
|
199
|
+
return json.dumps({"error": "No query provided"})
|
|
200
|
+
|
|
201
|
+
limit = kwargs.get("limit", self.DEFAULT_LIMIT)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# Security check - only allow SELECT queries unless write is enabled
|
|
205
|
+
write_error = self._validate_write_operation(query)
|
|
206
|
+
if write_error:
|
|
207
|
+
return json.dumps({"error": write_error})
|
|
208
|
+
|
|
209
|
+
# Add LIMIT if not present and it's a SELECT query
|
|
210
|
+
query = self._add_limit_to_query(query, limit)
|
|
211
|
+
|
|
212
|
+
# Execute the query
|
|
213
|
+
results = await self.db.execute_query(query)
|
|
214
|
+
|
|
215
|
+
# Format results
|
|
216
|
+
actual_limit = limit if limit is not None else len(results)
|
|
217
|
+
|
|
218
|
+
return json.dumps(
|
|
219
|
+
{
|
|
220
|
+
"success": True,
|
|
221
|
+
"row_count": len(results),
|
|
222
|
+
"results": results[:actual_limit],
|
|
223
|
+
"truncated": len(results) > actual_limit,
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
error_msg = str(e)
|
|
229
|
+
|
|
230
|
+
# Provide helpful error messages
|
|
231
|
+
suggestions = []
|
|
232
|
+
if "column" in error_msg.lower() and "does not exist" in error_msg.lower():
|
|
233
|
+
suggestions.append(
|
|
234
|
+
"Check column names using the schema introspection tool"
|
|
235
|
+
)
|
|
236
|
+
elif "table" in error_msg.lower() and "does not exist" in error_msg.lower():
|
|
237
|
+
suggestions.append(
|
|
238
|
+
"Check table names using the schema introspection tool"
|
|
239
|
+
)
|
|
240
|
+
elif "syntax error" in error_msg.lower():
|
|
241
|
+
suggestions.append(
|
|
242
|
+
"Review SQL syntax, especially JOIN conditions and WHERE clauses"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return json.dumps({"error": error_msg, "suggestions": suggestions})
|
|
246
|
+
|
|
247
|
+
def _validate_write_operation(self, query: str) -> str | None:
|
|
248
|
+
"""Validate if a write operation is allowed."""
|
|
249
|
+
query_upper = query.strip().upper()
|
|
250
|
+
|
|
251
|
+
# Check for write operations
|
|
252
|
+
write_keywords = [
|
|
253
|
+
"INSERT",
|
|
254
|
+
"UPDATE",
|
|
255
|
+
"DELETE",
|
|
256
|
+
"DROP",
|
|
257
|
+
"CREATE",
|
|
258
|
+
"ALTER",
|
|
259
|
+
"TRUNCATE",
|
|
260
|
+
]
|
|
261
|
+
is_write_query = any(query_upper.startswith(kw) for kw in write_keywords)
|
|
262
|
+
|
|
263
|
+
if is_write_query:
|
|
264
|
+
return (
|
|
265
|
+
"Write operations are not allowed. Only SELECT queries are permitted."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def _add_limit_to_query(self, query: str, limit: int = 100) -> str:
|
|
271
|
+
"""Add LIMIT clause to SELECT queries if not present."""
|
|
272
|
+
query_upper = query.strip().upper()
|
|
273
|
+
if query_upper.startswith("SELECT") and "LIMIT" not in query_upper:
|
|
274
|
+
return f"{query.rstrip(';')} LIMIT {limit};"
|
|
275
|
+
return query
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Visualization tools for data plotting."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from uniplot import histogram, plot
|
|
7
|
+
|
|
8
|
+
from .base import Tool
|
|
9
|
+
from .enums import ToolCategory, WorkflowPosition
|
|
10
|
+
from .registry import register_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@register_tool
|
|
14
|
+
class PlotDataTool(Tool):
|
|
15
|
+
"""Tool for creating terminal plots using uniplot."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "plot_data"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def description(self) -> str:
|
|
23
|
+
return "Create a plot of query results."
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def input_schema(self) -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"y_values": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": {"type": ["number", "null"]},
|
|
33
|
+
"description": "Y-axis data points (required)",
|
|
34
|
+
},
|
|
35
|
+
"x_values": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {"type": ["number", "null"]},
|
|
38
|
+
"description": "X-axis data points (optional, will use indices if not provided)",
|
|
39
|
+
},
|
|
40
|
+
"plot_type": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"enum": ["line", "scatter", "histogram"],
|
|
43
|
+
"description": "Type of plot to create (default: line)",
|
|
44
|
+
"default": "line",
|
|
45
|
+
},
|
|
46
|
+
"title": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Title for the plot",
|
|
49
|
+
},
|
|
50
|
+
"x_label": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Label for X-axis",
|
|
53
|
+
},
|
|
54
|
+
"y_label": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Label for Y-axis",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
"required": ["y_values"],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def category(self) -> ToolCategory:
|
|
64
|
+
return ToolCategory.VISUALIZATION
|
|
65
|
+
|
|
66
|
+
def get_usage_instructions(self) -> str | None:
|
|
67
|
+
"""Return usage instructions for this tool."""
|
|
68
|
+
return "Create terminal plots from query results when visualization would enhance understanding of the data."
|
|
69
|
+
|
|
70
|
+
def get_priority(self) -> int:
|
|
71
|
+
"""Return priority for tool ordering."""
|
|
72
|
+
return 40 # Should come after SQL execution
|
|
73
|
+
|
|
74
|
+
def get_workflow_position(self) -> WorkflowPosition:
|
|
75
|
+
"""Return workflow position."""
|
|
76
|
+
return WorkflowPosition.VISUALIZATION
|
|
77
|
+
|
|
78
|
+
async def execute(self, **kwargs) -> str:
|
|
79
|
+
"""Create a terminal plot."""
|
|
80
|
+
y_values = kwargs.get("y_values", [])
|
|
81
|
+
x_values = kwargs.get("x_values")
|
|
82
|
+
plot_type = kwargs.get("plot_type", "line")
|
|
83
|
+
title = kwargs.get("title")
|
|
84
|
+
x_label = kwargs.get("x_label")
|
|
85
|
+
y_label = kwargs.get("y_label")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Validate inputs
|
|
89
|
+
if not y_values:
|
|
90
|
+
return json.dumps({"error": "No data provided for plotting"})
|
|
91
|
+
|
|
92
|
+
# Convert to floats if needed
|
|
93
|
+
try:
|
|
94
|
+
y_values = [float(v) if v is not None else None for v in y_values]
|
|
95
|
+
if x_values:
|
|
96
|
+
x_values = [float(v) if v is not None else None for v in x_values]
|
|
97
|
+
except (ValueError, TypeError) as e:
|
|
98
|
+
return json.dumps({"error": f"Invalid data format: {str(e)}"})
|
|
99
|
+
|
|
100
|
+
# Create the plot
|
|
101
|
+
if plot_type == "histogram":
|
|
102
|
+
# For histogram, we only need y_values
|
|
103
|
+
histogram(
|
|
104
|
+
y_values,
|
|
105
|
+
title=title,
|
|
106
|
+
bins=min(20, len(set(y_values))), # Adaptive bin count
|
|
107
|
+
)
|
|
108
|
+
plot_info = {
|
|
109
|
+
"type": "histogram",
|
|
110
|
+
"data_points": len(y_values),
|
|
111
|
+
"title": title or "Histogram",
|
|
112
|
+
}
|
|
113
|
+
elif plot_type in ["line", "scatter"]:
|
|
114
|
+
# For line/scatter plots
|
|
115
|
+
plot_kwargs = {
|
|
116
|
+
"ys": y_values,
|
|
117
|
+
"title": title,
|
|
118
|
+
"lines": plot_type == "line",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if x_values:
|
|
122
|
+
plot_kwargs["xs"] = x_values
|
|
123
|
+
if x_label:
|
|
124
|
+
plot_kwargs["x_unit"] = x_label
|
|
125
|
+
if y_label:
|
|
126
|
+
plot_kwargs["y_unit"] = y_label
|
|
127
|
+
|
|
128
|
+
plot(**plot_kwargs)
|
|
129
|
+
|
|
130
|
+
plot_info = {
|
|
131
|
+
"type": plot_type,
|
|
132
|
+
"data_points": len(y_values),
|
|
133
|
+
"title": title or f"{plot_type.capitalize()} Plot",
|
|
134
|
+
"has_x_values": x_values is not None,
|
|
135
|
+
}
|
|
136
|
+
else:
|
|
137
|
+
return json.dumps({"error": f"Unsupported plot type: {plot_type}"})
|
|
138
|
+
|
|
139
|
+
return json.dumps(
|
|
140
|
+
{"success": True, "plot_rendered": True, "plot_info": plot_info}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return json.dumps({"error": f"Error creating plot: {str(e)}"})
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlsaber
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.16.0
|
|
4
|
+
Summary: SQLsaber - Open-source agentic SQL assistant
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Requires-Dist: aiomysql>=0.2.0
|
|
8
8
|
Requires-Dist: aiosqlite>=0.21.0
|
|
9
|
-
Requires-Dist: anthropic>=0.54.0
|
|
10
9
|
Requires-Dist: asyncpg>=0.30.0
|
|
11
10
|
Requires-Dist: cyclopts>=3.22.1
|
|
12
11
|
Requires-Dist: fastmcp>=2.9.0
|
|
@@ -14,27 +13,21 @@ Requires-Dist: httpx>=0.28.1
|
|
|
14
13
|
Requires-Dist: keyring>=25.6.0
|
|
15
14
|
Requires-Dist: pandas>=2.0.0
|
|
16
15
|
Requires-Dist: platformdirs>=4.0.0
|
|
16
|
+
Requires-Dist: pydantic-ai
|
|
17
17
|
Requires-Dist: questionary>=2.1.0
|
|
18
18
|
Requires-Dist: rich>=13.7.0
|
|
19
19
|
Requires-Dist: uniplot>=0.21.2
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
|
|
22
|
-
#
|
|
22
|
+
# SQLsaber
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
███████ ██████ ██ ███████ █████ ██████ ███████ ██████
|
|
26
|
-
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
27
|
-
███████ ██ ██ ██ ███████ ███████ ██████ █████ ██████
|
|
28
|
-
██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
29
|
-
███████ ██████ ███████ ███████ ██ ██ ██████ ███████ ██ ██
|
|
30
|
-
▀▀
|
|
31
|
-
```
|
|
24
|
+
> SQLsaber is an open-source agentic SQL assistant. Think Claude Code but for SQL.
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+

|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
Stop fighting your database.
|
|
36
29
|
|
|
37
|
-
Ask your questions in natural language and
|
|
30
|
+
Ask your questions in natural language and `sqlsaber` will gather the right context automatically and answer your query by writing SQL and analyzing the results.
|
|
38
31
|
|
|
39
32
|
## Table of Contents
|
|
40
33
|
|
|
@@ -59,14 +52,13 @@ Ask your questions in natural language and it will gather the right context auto
|
|
|
59
52
|
|
|
60
53
|
## Features
|
|
61
54
|
|
|
62
|
-
- Natural language to SQL conversion
|
|
63
55
|
- 🔍 Automatic database schema introspection
|
|
64
56
|
- 🛡️ Safe query execution (read-only by default)
|
|
65
57
|
- 🧠 Memory management
|
|
66
58
|
- 💬 Interactive REPL mode
|
|
67
|
-
- 🎨 Beautiful formatted output with syntax highlighting
|
|
68
59
|
- 🗄️ Support for PostgreSQL, SQLite, and MySQL
|
|
69
60
|
- 🔌 MCP (Model Context Protocol) server support
|
|
61
|
+
- 🎨 Beautiful formatted output
|
|
70
62
|
|
|
71
63
|
## Installation
|
|
72
64
|
|
|
@@ -165,34 +157,21 @@ saber -d mydb
|
|
|
165
157
|
|
|
166
158
|
# Single query with specific database
|
|
167
159
|
saber -d mydb "count all orders"
|
|
160
|
+
|
|
161
|
+
# You can also pass a connection string
|
|
162
|
+
saber -d "postgresql://user:password@localhost:5432/mydb" "count all orders"
|
|
168
163
|
```
|
|
169
164
|
|
|
170
165
|
## Examples
|
|
171
166
|
|
|
172
167
|
```bash
|
|
173
|
-
#
|
|
174
|
-
saber
|
|
175
|
-
|
|
176
|
-
# Count records
|
|
177
|
-
saber "how many active users do we have?"
|
|
168
|
+
# Start interactive mode
|
|
169
|
+
saber
|
|
178
170
|
|
|
179
|
-
#
|
|
171
|
+
# Non-interactive mode
|
|
180
172
|
saber "show me orders with customer details for this week"
|
|
181
173
|
|
|
182
|
-
# Aggregations
|
|
183
|
-
saber "what's the total revenue by product category?"
|
|
184
|
-
|
|
185
|
-
# Date filtering
|
|
186
|
-
saber "list users who haven't logged in for 30 days"
|
|
187
|
-
|
|
188
|
-
# Data exploration
|
|
189
|
-
saber "show me the distribution of customer ages"
|
|
190
|
-
|
|
191
|
-
# Business analytics
|
|
192
174
|
saber "which products had the highest sales growth last quarter?"
|
|
193
|
-
|
|
194
|
-
# Start interactive mode
|
|
195
|
-
saber
|
|
196
175
|
```
|
|
197
176
|
|
|
198
177
|
## MCP Server Integration
|
|
@@ -214,7 +193,7 @@ uvx --from sqlsaber saber-mcp
|
|
|
214
193
|
Add SQLSaber as an MCP server in Claude Code:
|
|
215
194
|
|
|
216
195
|
```bash
|
|
217
|
-
claude mcp add -- uvx --from sqlsaber saber-mcp
|
|
196
|
+
claude mcp add sqlsaber -- uvx --from sqlsaber saber-mcp
|
|
218
197
|
```
|
|
219
198
|
|
|
220
199
|
#### Other MCP Clients
|
|
@@ -234,7 +213,7 @@ The MCP server uses your existing SQLSaber database configurations, so make sure
|
|
|
234
213
|
|
|
235
214
|
## How It Works
|
|
236
215
|
|
|
237
|
-
|
|
216
|
+
SQLsaber uses a multi-step agentic process to gather the right context and execute SQL queries to answer your questions:
|
|
238
217
|
|
|
239
218
|

|
|
240
219
|
|
|
@@ -255,7 +234,9 @@ SQLSaber uses a multi-step process to gather the right context, provide it to th
|
|
|
255
234
|
|
|
256
235
|
## Contributing
|
|
257
236
|
|
|
258
|
-
|
|
237
|
+
If you like the project, starring the repo is a great way to show your support!
|
|
238
|
+
|
|
239
|
+
Other contributions are welcome! Please feel free to open an issue to discuss your ideas or report bugs.
|
|
259
240
|
|
|
260
241
|
## License
|
|
261
242
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
sqlsaber/__init__.py,sha256=HjS8ULtP4MGpnTL7njVY45NKV9Fi4e_yeYuY-hyXWQc,73
|
|
2
|
+
sqlsaber/__main__.py,sha256=RIHxWeWh2QvLfah-2OkhI5IJxojWfy4fXpMnVEJYvxw,78
|
|
3
|
+
sqlsaber/agents/__init__.py,sha256=i_MI2eWMQaVzGikKU71FPCmSQxNDKq36Imq1PrYoIPU,130
|
|
4
|
+
sqlsaber/agents/base.py,sha256=2JX7UKW7tbqxPaK4aQLpXIwT3uszdBQHdSsOyML2U7I,6353
|
|
5
|
+
sqlsaber/agents/mcp.py,sha256=GcJTx7YDYH6aaxIADEIxSgcWAdWakUx395JIzVnf17U,768
|
|
6
|
+
sqlsaber/agents/pydantic_ai_agent.py,sha256=dGdsgyxCZvfK-v-MH8KimKOr-xb2aSfSWY8CMcOUCT8,6795
|
|
7
|
+
sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
|
|
8
|
+
sqlsaber/cli/auth.py,sha256=jTsRgbmlGPlASSuIKmdjjwfqtKvjfKd_cTYxX0-QqaQ,7400
|
|
9
|
+
sqlsaber/cli/commands.py,sha256=caWyMqOyCJVM0gpC4M_7DMuI7fzsf4Kcm_BJxOQrkM0,6718
|
|
10
|
+
sqlsaber/cli/completers.py,sha256=HsUPjaZweLSeYCWkAcgMl8FylQ1xjWBWYTEL_9F6xfU,6430
|
|
11
|
+
sqlsaber/cli/database.py,sha256=atwg3l8acQ3YTDuhq7vNrBN6tpOv0syz6V62KTF-Bh8,12910
|
|
12
|
+
sqlsaber/cli/display.py,sha256=HtXwPe3VPUh2EJpyvpJVWyisCanu9O7w-rkqq7Y4UaY,9791
|
|
13
|
+
sqlsaber/cli/interactive.py,sha256=SNOV_MaqJt2NQr6H8GbbWzMyuGx2NLFEXetjKtFAefw,9635
|
|
14
|
+
sqlsaber/cli/memory.py,sha256=OufHFJFwV0_GGn7LvKRTJikkWhV1IwNIUDOxFPHXOaQ,7794
|
|
15
|
+
sqlsaber/cli/models.py,sha256=ZewtwGQwhd9b-yxBAPKePolvI1qQG-EkmeWAGMqtWNQ,8986
|
|
16
|
+
sqlsaber/cli/streaming.py,sha256=3KXDu8_IJDkt-q6HoO7LYtLXjliNwWy_pOrgV8BQ6Bg,5453
|
|
17
|
+
sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
|
|
18
|
+
sqlsaber/config/api_keys.py,sha256=RqWQCko1tY7sES7YOlexgBH5Hd5ne_kGXHdBDNqcV2U,3649
|
|
19
|
+
sqlsaber/config/auth.py,sha256=b5qB2h1doXyO9Bn8z0CcL8LAR2jF431gGXBGKLgTmtQ,2756
|
|
20
|
+
sqlsaber/config/database.py,sha256=c6q3l4EvoBch1ckYHA70hf6L7fSOY-sItnLCpvJiPrA,11357
|
|
21
|
+
sqlsaber/config/oauth_flow.py,sha256=A3bSXaBLzuAfXV2ZPA94m9NV33c2MyL6M4ii9oEkswQ,10291
|
|
22
|
+
sqlsaber/config/oauth_tokens.py,sha256=C9z35hyx-PvSAYdC1LNf3rg9_wsEIY56hkEczelbad0,6015
|
|
23
|
+
sqlsaber/config/providers.py,sha256=JFjeJv1K5Q93zWSlWq3hAvgch1TlgoF0qFa0KJROkKY,2957
|
|
24
|
+
sqlsaber/config/settings.py,sha256=vgb_RXaM-7DgbxYDmWNw1cSyMqwys4j3qNCvM4bljwI,5586
|
|
25
|
+
sqlsaber/conversation/__init__.py,sha256=xa-1gX6NsZpVGg_LDrsZAtDtsDo5FZc1SO8gwtm_IPk,302
|
|
26
|
+
sqlsaber/conversation/manager.py,sha256=LDfmKGIMvTzsL7S0aXGWw6Ve54CHIeTGLU4qwes2NgU,7046
|
|
27
|
+
sqlsaber/conversation/models.py,sha256=fq4wpIB2yxLCQtsXhdpDji4FpscG2ayrOBACrNvgF14,3510
|
|
28
|
+
sqlsaber/conversation/storage.py,sha256=phpGEnZjXVFTmV5PalCKZpiO9VFHubMMfWA9OJCDbwc,11626
|
|
29
|
+
sqlsaber/database/__init__.py,sha256=a_gtKRJnZVO8-fEZI7g3Z8YnGa6Nio-5Y50PgVp07ss,176
|
|
30
|
+
sqlsaber/database/connection.py,sha256=sJtIIe0GVbo-1Py9-j66UxJoY1aKL9gqk68jkDL-Kvk,15123
|
|
31
|
+
sqlsaber/database/resolver.py,sha256=RPXF5EoKzvQDDLmPGNHYd2uG_oNICH8qvUjBp6iXmNY,3348
|
|
32
|
+
sqlsaber/database/schema.py,sha256=OC93dnZkijCoVNqb6itSpQ2XsiZ85PjVUW-VZDwrPrk,25989
|
|
33
|
+
sqlsaber/mcp/__init__.py,sha256=COdWq7wauPBp5Ew8tfZItFzbcLDSEkHBJSMhxzy8C9c,112
|
|
34
|
+
sqlsaber/mcp/mcp.py,sha256=X12oCMZYAtgJ7MNuh5cqz8y3lALrOzkXWcfpuY0Ijxk,3950
|
|
35
|
+
sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
|
|
36
|
+
sqlsaber/memory/manager.py,sha256=p3fybMVfH-E4ApT1ZRZUnQIWSk9dkfUPCyfkmA0HALs,2739
|
|
37
|
+
sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,5745
|
|
38
|
+
sqlsaber/models/__init__.py,sha256=b3KVyLN7Y7p2rT1-TlIExpkO26bTG04-VCFwDmBO6i0,204
|
|
39
|
+
sqlsaber/models/types.py,sha256=w-zk81V2dtveuteej36_o1fDK3So428j3P2rAejU62U,862
|
|
40
|
+
sqlsaber/tools/__init__.py,sha256=a-JNOhHsC7WEVhTsQY_IHckaPOmswoM3Q85YOd2iC_E,652
|
|
41
|
+
sqlsaber/tools/base.py,sha256=XLaHrAzZc4OUiOsKBvEfZCtWKDBd5WqgNAnmRufnOp4,2174
|
|
42
|
+
sqlsaber/tools/enums.py,sha256=TnlvEOpkGtuzEzg_JBdSb_S38jg4INhtxw4qZ20kU_E,483
|
|
43
|
+
sqlsaber/tools/instructions.py,sha256=nnItVvJBtN-FLB3-PSR6Y23ix6AiOB5hNX3r4TtYFKw,9869
|
|
44
|
+
sqlsaber/tools/registry.py,sha256=decVRNf50JPNT4i-OHZIL2B8cmoOMDaE4y0Av6q6v0I,3619
|
|
45
|
+
sqlsaber/tools/sql_tools.py,sha256=hM6tKqW5MDhFUt6MesoqhTUqIpq_5baIIDoN1MjDCXY,9647
|
|
46
|
+
sqlsaber/tools/visualization_tools.py,sha256=059Pe3aOZvgpqT9487Ydv2PhY7T1pVmfALPTvfqPisI,4973
|
|
47
|
+
sqlsaber-0.16.0.dist-info/METADATA,sha256=wa2OqUBvJu-KGPMsH0vhULYksY9sDcn-nRJihroMzA8,5966
|
|
48
|
+
sqlsaber-0.16.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
49
|
+
sqlsaber-0.16.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
|
|
50
|
+
sqlsaber-0.16.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
51
|
+
sqlsaber-0.16.0.dist-info/RECORD,,
|