sqlsaber 0.13.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.
- sqlsaber/agents/anthropic.py +63 -123
- sqlsaber/agents/base.py +111 -210
- sqlsaber/cli/interactive.py +6 -2
- sqlsaber/conversation/__init__.py +12 -0
- sqlsaber/conversation/manager.py +224 -0
- sqlsaber/conversation/models.py +120 -0
- sqlsaber/conversation/storage.py +362 -0
- sqlsaber/database/schema.py +2 -51
- sqlsaber/mcp/mcp.py +43 -51
- sqlsaber/tools/__init__.py +25 -0
- sqlsaber/tools/base.py +83 -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.13.0.dist-info → sqlsaber-0.15.0.dist-info}/METADATA +1 -1
- {sqlsaber-0.13.0.dist-info → sqlsaber-0.15.0.dist-info}/RECORD +21 -10
- {sqlsaber-0.13.0.dist-info → sqlsaber-0.15.0.dist-info}/WHEEL +0 -0
- {sqlsaber-0.13.0.dist-info → sqlsaber-0.15.0.dist-info}/entry_points.txt +0 -0
- {sqlsaber-0.13.0.dist-info → sqlsaber-0.15.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Tool registry for managing available tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Type
|
|
4
|
+
|
|
5
|
+
from .base import Tool
|
|
6
|
+
from .enums import ToolCategory
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolRegistry:
|
|
10
|
+
"""Registry for managing and discovering tools."""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
"""Initialize the registry."""
|
|
14
|
+
self._tools: dict[str, Type[Tool]] = {}
|
|
15
|
+
self._instances: dict[str, Tool] = {}
|
|
16
|
+
|
|
17
|
+
def register(self, tool_class: Type[Tool]) -> None:
|
|
18
|
+
"""Register a tool class.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
tool_class: The tool class to register
|
|
22
|
+
"""
|
|
23
|
+
# Create a temporary instance to get the name
|
|
24
|
+
temp_instance = tool_class()
|
|
25
|
+
name = temp_instance.name
|
|
26
|
+
|
|
27
|
+
if name in self._tools:
|
|
28
|
+
raise ValueError(f"Tool '{name}' is already registered")
|
|
29
|
+
|
|
30
|
+
self._tools[name] = tool_class
|
|
31
|
+
|
|
32
|
+
def unregister(self, name: str) -> None:
|
|
33
|
+
"""Unregister a tool.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name: Name of the tool to unregister
|
|
37
|
+
"""
|
|
38
|
+
if name in self._tools:
|
|
39
|
+
del self._tools[name]
|
|
40
|
+
if name in self._instances:
|
|
41
|
+
del self._instances[name]
|
|
42
|
+
|
|
43
|
+
def get_tool(self, name: str) -> Tool:
|
|
44
|
+
"""Get a tool instance by name.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: Name of the tool
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Tool instance
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
KeyError: If tool is not found
|
|
54
|
+
"""
|
|
55
|
+
if name not in self._tools:
|
|
56
|
+
raise KeyError(f"Tool '{name}' not found in registry")
|
|
57
|
+
|
|
58
|
+
# Create instance if not already created (singleton pattern)
|
|
59
|
+
if name not in self._instances:
|
|
60
|
+
self._instances[name] = self._tools[name]()
|
|
61
|
+
|
|
62
|
+
return self._instances[name]
|
|
63
|
+
|
|
64
|
+
def list_tools(self, category: str | ToolCategory | None = None) -> list[str]:
|
|
65
|
+
"""List all registered tool names.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
category: Optional category to filter by (string or ToolCategory enum)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of tool names
|
|
72
|
+
"""
|
|
73
|
+
if category is None:
|
|
74
|
+
return list(self._tools.keys())
|
|
75
|
+
|
|
76
|
+
# Convert string to enum
|
|
77
|
+
if isinstance(category, str):
|
|
78
|
+
try:
|
|
79
|
+
category = ToolCategory(category)
|
|
80
|
+
except ValueError:
|
|
81
|
+
# If string doesn't match any enum, return empty list
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
# Filter by category
|
|
85
|
+
result = []
|
|
86
|
+
for name, tool_class in self._tools.items():
|
|
87
|
+
tool = self.get_tool(name)
|
|
88
|
+
if tool.category == category:
|
|
89
|
+
result.append(name)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
def get_all_tools(self, category: str | ToolCategory | None = None) -> list[Tool]:
|
|
93
|
+
"""Get all tool instances.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
category: Optional category to filter by (string or ToolCategory enum)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of tool instances
|
|
100
|
+
"""
|
|
101
|
+
names = self.list_tools(category)
|
|
102
|
+
return [self.get_tool(name) for name in names]
|
|
103
|
+
|
|
104
|
+
def get_tool_definitions(self, category: str | ToolCategory | None = None) -> list:
|
|
105
|
+
"""Get tool definitions for all tools.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
category: Optional category to filter by (string or ToolCategory enum)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of ToolDefinition objects
|
|
112
|
+
"""
|
|
113
|
+
tools = self.get_all_tools(category)
|
|
114
|
+
return [tool.to_definition() for tool in tools]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Global registry instance
|
|
118
|
+
tool_registry = ToolRegistry()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def register_tool(tool_class: Type[Tool]) -> Type[Tool]:
|
|
122
|
+
"""Decorator to register a tool class.
|
|
123
|
+
|
|
124
|
+
Usage:
|
|
125
|
+
@register_tool
|
|
126
|
+
class MyTool(Tool):
|
|
127
|
+
...
|
|
128
|
+
"""
|
|
129
|
+
tool_registry.register(tool_class)
|
|
130
|
+
return tool_class
|
|
@@ -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,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=
|
|
5
|
-
sqlsaber/agents/base.py,sha256=
|
|
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
|
|
@@ -11,7 +11,7 @@ sqlsaber/cli/commands.py,sha256=VX7pqQnf-85A9zkjXqzytVNeCG8KO0mB2TyIEzB4sh8,6241
|
|
|
11
11
|
sqlsaber/cli/completers.py,sha256=HsUPjaZweLSeYCWkAcgMl8FylQ1xjWBWYTEL_9F6xfU,6430
|
|
12
12
|
sqlsaber/cli/database.py,sha256=tJ8rqGrafZpg3VgDmSiq7eZoPscoGAW3XLTYGoQw8LE,12910
|
|
13
13
|
sqlsaber/cli/display.py,sha256=HtXwPe3VPUh2EJpyvpJVWyisCanu9O7w-rkqq7Y4UaY,9791
|
|
14
|
-
sqlsaber/cli/interactive.py,sha256=
|
|
14
|
+
sqlsaber/cli/interactive.py,sha256=7RjUMMPJ49RUuUGn-gP6vgiW6Ccvin9mzmRL-9p6eto,8171
|
|
15
15
|
sqlsaber/cli/memory.py,sha256=OufHFJFwV0_GGn7LvKRTJikkWhV1IwNIUDOxFPHXOaQ,7794
|
|
16
16
|
sqlsaber/cli/models.py,sha256=HByezaeKqj65GzB_FmWuugjkgTq2Pvab_mzNZnHxya0,7690
|
|
17
17
|
sqlsaber/cli/streaming.py,sha256=WfhFd5ntq2HStpJZwWJ0C5uyXKc3aU14eo8HdjzW1o0,3767
|
|
@@ -28,20 +28,31 @@ sqlsaber/config/database.py,sha256=c6q3l4EvoBch1ckYHA70hf6L7fSOY-sItnLCpvJiPrA,1
|
|
|
28
28
|
sqlsaber/config/oauth_flow.py,sha256=A3bSXaBLzuAfXV2ZPA94m9NV33c2MyL6M4ii9oEkswQ,10291
|
|
29
29
|
sqlsaber/config/oauth_tokens.py,sha256=C9z35hyx-PvSAYdC1LNf3rg9_wsEIY56hkEczelbad0,6015
|
|
30
30
|
sqlsaber/config/settings.py,sha256=gKhGlErzsBk39RoRSy1b8pb-bN2K7HIaPaBgbJDhY4M,4753
|
|
31
|
+
sqlsaber/conversation/__init__.py,sha256=xa-1gX6NsZpVGg_LDrsZAtDtsDo5FZc1SO8gwtm_IPk,302
|
|
32
|
+
sqlsaber/conversation/manager.py,sha256=LDfmKGIMvTzsL7S0aXGWw6Ve54CHIeTGLU4qwes2NgU,7046
|
|
33
|
+
sqlsaber/conversation/models.py,sha256=fq4wpIB2yxLCQtsXhdpDji4FpscG2ayrOBACrNvgF14,3510
|
|
34
|
+
sqlsaber/conversation/storage.py,sha256=phpGEnZjXVFTmV5PalCKZpiO9VFHubMMfWA9OJCDbwc,11626
|
|
31
35
|
sqlsaber/database/__init__.py,sha256=a_gtKRJnZVO8-fEZI7g3Z8YnGa6Nio-5Y50PgVp07ss,176
|
|
32
36
|
sqlsaber/database/connection.py,sha256=sZVGNMzMwiM11GrsLLPwR8A5ugzJ5O0TCdkrt0KVRuI,15123
|
|
33
37
|
sqlsaber/database/resolver.py,sha256=RPXF5EoKzvQDDLmPGNHYd2uG_oNICH8qvUjBp6iXmNY,3348
|
|
34
|
-
sqlsaber/database/schema.py,sha256=
|
|
38
|
+
sqlsaber/database/schema.py,sha256=OC93dnZkijCoVNqb6itSpQ2XsiZ85PjVUW-VZDwrPrk,25989
|
|
35
39
|
sqlsaber/mcp/__init__.py,sha256=COdWq7wauPBp5Ew8tfZItFzbcLDSEkHBJSMhxzy8C9c,112
|
|
36
|
-
sqlsaber/mcp/mcp.py,sha256=
|
|
40
|
+
sqlsaber/mcp/mcp.py,sha256=X12oCMZYAtgJ7MNuh5cqz8y3lALrOzkXWcfpuY0Ijxk,3950
|
|
37
41
|
sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
|
|
38
42
|
sqlsaber/memory/manager.py,sha256=p3fybMVfH-E4ApT1ZRZUnQIWSk9dkfUPCyfkmA0HALs,2739
|
|
39
43
|
sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,5745
|
|
40
44
|
sqlsaber/models/__init__.py,sha256=RJ7p3WtuSwwpFQ1Iw4_DHV2zzCtHqIzsjJzxv8kUjUE,287
|
|
41
45
|
sqlsaber/models/events.py,sha256=89SXKb5GGpH01yTr2kPEBhzp9xv35RFIYuFdAZSIPoE,721
|
|
42
46
|
sqlsaber/models/types.py,sha256=w-zk81V2dtveuteej36_o1fDK3So428j3P2rAejU62U,862
|
|
43
|
-
sqlsaber
|
|
44
|
-
sqlsaber
|
|
45
|
-
sqlsaber
|
|
46
|
-
sqlsaber
|
|
47
|
-
sqlsaber
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|