sqlsaber 0.14.0__py3-none-any.whl → 0.15.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.

@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.14.0
3
+ Version: 0.15.0
4
4
  Summary: SQLSaber - Agentic SQL assistant like Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,8 +1,8 @@
1
1
  sqlsaber/__init__.py,sha256=HjS8ULtP4MGpnTL7njVY45NKV9Fi4e_yeYuY-hyXWQc,73
2
2
  sqlsaber/__main__.py,sha256=RIHxWeWh2QvLfah-2OkhI5IJxojWfy4fXpMnVEJYvxw,78
3
3
  sqlsaber/agents/__init__.py,sha256=LWeSeEUE4BhkyAYFF3TE-fx8TtLud3oyEtyB8ojFJgo,167
4
- sqlsaber/agents/anthropic.py,sha256=xH4xY1lq5idDgY8Vklgu5hGLoKLHWQMMfPgjvQjfG7I,23615
5
- sqlsaber/agents/base.py,sha256=9A-iceb93eHrQHTdLoeSicP3ZqDHOFTx3Fe5OvHvUkg,14093
4
+ sqlsaber/agents/anthropic.py,sha256=OSrf_2fancbGH04ckouxChNyKj1H54wt_fWGP0hECII,19564
5
+ sqlsaber/agents/base.py,sha256=jOchhaEQWiW__koy2WF4e_YvvsF5l_WYru_q0PkBF7g,6413
6
6
  sqlsaber/agents/mcp.py,sha256=FKtXgDrPZ2-xqUYCw2baI5JzrWekXaC5fjkYW1_Mg50,827
7
7
  sqlsaber/agents/streaming.py,sha256=LaSeMTlxuJFRArJVqDly5-_KgcePiCCKPKfMxfB4oGs,521
8
8
  sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
@@ -37,15 +37,22 @@ sqlsaber/database/connection.py,sha256=sZVGNMzMwiM11GrsLLPwR8A5ugzJ5O0TCdkrt0KVR
37
37
  sqlsaber/database/resolver.py,sha256=RPXF5EoKzvQDDLmPGNHYd2uG_oNICH8qvUjBp6iXmNY,3348
38
38
  sqlsaber/database/schema.py,sha256=OC93dnZkijCoVNqb6itSpQ2XsiZ85PjVUW-VZDwrPrk,25989
39
39
  sqlsaber/mcp/__init__.py,sha256=COdWq7wauPBp5Ew8tfZItFzbcLDSEkHBJSMhxzy8C9c,112
40
- sqlsaber/mcp/mcp.py,sha256=YH4crygqb5_Y94nsns6d-26FZCTlDPOh3tf-ghihzDM,4440
40
+ sqlsaber/mcp/mcp.py,sha256=X12oCMZYAtgJ7MNuh5cqz8y3lALrOzkXWcfpuY0Ijxk,3950
41
41
  sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
42
42
  sqlsaber/memory/manager.py,sha256=p3fybMVfH-E4ApT1ZRZUnQIWSk9dkfUPCyfkmA0HALs,2739
43
43
  sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,5745
44
44
  sqlsaber/models/__init__.py,sha256=RJ7p3WtuSwwpFQ1Iw4_DHV2zzCtHqIzsjJzxv8kUjUE,287
45
45
  sqlsaber/models/events.py,sha256=89SXKb5GGpH01yTr2kPEBhzp9xv35RFIYuFdAZSIPoE,721
46
46
  sqlsaber/models/types.py,sha256=w-zk81V2dtveuteej36_o1fDK3So428j3P2rAejU62U,862
47
- sqlsaber-0.14.0.dist-info/METADATA,sha256=xAj3wcH-3OJWud9grNykoiHhuVp2LIdMV5bbxNUQOEk,6877
48
- sqlsaber-0.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
49
- sqlsaber-0.14.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
50
- sqlsaber-0.14.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
51
- sqlsaber-0.14.0.dist-info/RECORD,,
47
+ sqlsaber/tools/__init__.py,sha256=a-JNOhHsC7WEVhTsQY_IHckaPOmswoM3Q85YOd2iC_E,652
48
+ sqlsaber/tools/base.py,sha256=0pCC2OMZc4uyxv-ErRHuXbu_9MeVXPzSyGIyix6Hvw4,2085
49
+ sqlsaber/tools/enums.py,sha256=TnlvEOpkGtuzEzg_JBdSb_S38jg4INhtxw4qZ20kU_E,483
50
+ sqlsaber/tools/instructions.py,sha256=nnItVvJBtN-FLB3-PSR6Y23ix6AiOB5hNX3r4TtYFKw,9869
51
+ sqlsaber/tools/registry.py,sha256=decVRNf50JPNT4i-OHZIL2B8cmoOMDaE4y0Av6q6v0I,3619
52
+ sqlsaber/tools/sql_tools.py,sha256=hM6tKqW5MDhFUt6MesoqhTUqIpq_5baIIDoN1MjDCXY,9647
53
+ sqlsaber/tools/visualization_tools.py,sha256=059Pe3aOZvgpqT9487Ydv2PhY7T1pVmfALPTvfqPisI,4973
54
+ sqlsaber-0.15.0.dist-info/METADATA,sha256=XEF-xwN39iDs9Gd_uSdeHxyE50i429ol5iU2HSnkdZc,6877
55
+ sqlsaber-0.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
+ sqlsaber-0.15.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
57
+ sqlsaber-0.15.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
58
+ sqlsaber-0.15.0.dist-info/RECORD,,