sqlsaber 0.16.1__py3-none-any.whl → 0.17.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/base.py +0 -108
- sqlsaber/cli/commands.py +33 -1
- sqlsaber/cli/display.py +0 -30
- sqlsaber/cli/interactive.py +76 -24
- sqlsaber/cli/streaming.py +0 -4
- sqlsaber/cli/threads.py +301 -0
- sqlsaber/database/schema.py +30 -2
- sqlsaber/threads/__init__.py +5 -0
- sqlsaber/threads/storage.py +303 -0
- sqlsaber/tools/__init__.py +0 -2
- sqlsaber/tools/base.py +0 -12
- sqlsaber/tools/enums.py +0 -2
- sqlsaber/tools/instructions.py +3 -23
- sqlsaber/tools/registry.py +0 -12
- {sqlsaber-0.16.1.dist-info → sqlsaber-0.17.0.dist-info}/METADATA +12 -3
- {sqlsaber-0.16.1.dist-info → sqlsaber-0.17.0.dist-info}/RECORD +19 -23
- sqlsaber/conversation/__init__.py +0 -12
- sqlsaber/conversation/manager.py +0 -224
- sqlsaber/conversation/models.py +0 -120
- sqlsaber/conversation/storage.py +0 -362
- sqlsaber/models/__init__.py +0 -10
- sqlsaber/models/types.py +0 -40
- sqlsaber/tools/visualization_tools.py +0 -144
- {sqlsaber-0.16.1.dist-info → sqlsaber-0.17.0.dist-info}/WHEEL +0 -0
- {sqlsaber-0.16.1.dist-info → sqlsaber-0.17.0.dist-info}/entry_points.txt +0 -0
- {sqlsaber-0.16.1.dist-info → sqlsaber-0.17.0.dist-info}/licenses/LICENSE +0 -0
sqlsaber/conversation/storage.py
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
"""SQLite storage implementation for conversation history."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import logging
|
|
6
|
-
import time
|
|
7
|
-
import uuid
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
import aiosqlite
|
|
12
|
-
import platformdirs
|
|
13
|
-
|
|
14
|
-
from .models import Conversation, ConversationMessage
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
# Database schema
|
|
19
|
-
SCHEMA_SQL = """
|
|
20
|
-
-- Conversations table
|
|
21
|
-
CREATE TABLE IF NOT EXISTS conversations (
|
|
22
|
-
id TEXT PRIMARY KEY,
|
|
23
|
-
database_name TEXT NOT NULL,
|
|
24
|
-
started_at REAL NOT NULL,
|
|
25
|
-
ended_at REAL
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
-- Messages table
|
|
29
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
30
|
-
id TEXT PRIMARY KEY,
|
|
31
|
-
conversation_id TEXT NOT NULL,
|
|
32
|
-
role TEXT NOT NULL,
|
|
33
|
-
content TEXT NOT NULL,
|
|
34
|
-
index_in_conv INTEGER NOT NULL,
|
|
35
|
-
created_at REAL NOT NULL,
|
|
36
|
-
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
-- Indices for performance
|
|
40
|
-
CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, index_in_conv);
|
|
41
|
-
CREATE INDEX IF NOT EXISTS idx_conv_dbname ON conversations(database_name);
|
|
42
|
-
|
|
43
|
-
-- Store schema version for future migrations
|
|
44
|
-
CREATE TABLE IF NOT EXISTS schema_version (
|
|
45
|
-
version INTEGER PRIMARY KEY
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class ConversationStorage:
|
|
53
|
-
"""Handles SQLite storage and retrieval of conversation history."""
|
|
54
|
-
|
|
55
|
-
_DB_VERSION = 1
|
|
56
|
-
|
|
57
|
-
def __init__(self):
|
|
58
|
-
"""Initialize conversation storage."""
|
|
59
|
-
self.db_path = (
|
|
60
|
-
Path(platformdirs.user_config_dir("sqlsaber")) / "conversations.db"
|
|
61
|
-
)
|
|
62
|
-
self._lock = asyncio.Lock()
|
|
63
|
-
self._initialized: bool = False
|
|
64
|
-
|
|
65
|
-
async def _init_db(self) -> None:
|
|
66
|
-
"""Initialize the database with schema if needed."""
|
|
67
|
-
if self._initialized:
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
# Ensure parent directory exists
|
|
72
|
-
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
-
|
|
74
|
-
async with aiosqlite.connect(self.db_path) as db:
|
|
75
|
-
await db.executescript(SCHEMA_SQL)
|
|
76
|
-
await db.commit()
|
|
77
|
-
|
|
78
|
-
self._initialized = True
|
|
79
|
-
logger.debug(f"Initialized conversation database at {self.db_path}")
|
|
80
|
-
|
|
81
|
-
except Exception as e:
|
|
82
|
-
logger.warning(f"Failed to initialize conversation database: {e}")
|
|
83
|
-
# Don't raise - let the system continue without persistence
|
|
84
|
-
|
|
85
|
-
async def create_conversation(self, database_name: str) -> str:
|
|
86
|
-
"""Create a new conversation record.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
database_name: Name of the database for this conversation
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
Conversation ID (UUID)
|
|
93
|
-
"""
|
|
94
|
-
await self._init_db()
|
|
95
|
-
|
|
96
|
-
conversation_id = str(uuid.uuid4())
|
|
97
|
-
started_at = time.time()
|
|
98
|
-
|
|
99
|
-
try:
|
|
100
|
-
async with self._lock, aiosqlite.connect(self.db_path) as db:
|
|
101
|
-
await db.execute(
|
|
102
|
-
"""
|
|
103
|
-
INSERT INTO conversations (id, database_name, started_at)
|
|
104
|
-
VALUES (?, ?, ?)
|
|
105
|
-
""",
|
|
106
|
-
(conversation_id, database_name, started_at),
|
|
107
|
-
)
|
|
108
|
-
await db.commit()
|
|
109
|
-
|
|
110
|
-
logger.debug(
|
|
111
|
-
f"Created conversation {conversation_id} for db {database_name}"
|
|
112
|
-
)
|
|
113
|
-
return conversation_id
|
|
114
|
-
|
|
115
|
-
except Exception as e:
|
|
116
|
-
logger.warning(f"Failed to create conversation: {e}")
|
|
117
|
-
return conversation_id # Return ID anyway to allow in-memory operation
|
|
118
|
-
|
|
119
|
-
async def add_message(
|
|
120
|
-
self,
|
|
121
|
-
conversation_id: str,
|
|
122
|
-
role: str,
|
|
123
|
-
content: str | list[dict[str, Any]] | dict[str, Any],
|
|
124
|
-
index_in_conv: int,
|
|
125
|
-
) -> str:
|
|
126
|
-
"""Add a message to a conversation.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
conversation_id: ID of the conversation
|
|
130
|
-
role: Message role (user, assistant, system, tool)
|
|
131
|
-
content: Message content (will be JSON-serialized)
|
|
132
|
-
index_in_conv: Sequential index of message in conversation
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
Message ID (UUID)
|
|
136
|
-
"""
|
|
137
|
-
await self._init_db()
|
|
138
|
-
|
|
139
|
-
message_id = str(uuid.uuid4())
|
|
140
|
-
created_at = time.time()
|
|
141
|
-
|
|
142
|
-
try:
|
|
143
|
-
async with self._lock, aiosqlite.connect(self.db_path) as db:
|
|
144
|
-
await db.execute(
|
|
145
|
-
"""
|
|
146
|
-
INSERT INTO messages (id, conversation_id, role, content, index_in_conv, created_at)
|
|
147
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
148
|
-
""",
|
|
149
|
-
(
|
|
150
|
-
message_id,
|
|
151
|
-
conversation_id,
|
|
152
|
-
role,
|
|
153
|
-
json.dumps(content),
|
|
154
|
-
index_in_conv,
|
|
155
|
-
created_at,
|
|
156
|
-
),
|
|
157
|
-
)
|
|
158
|
-
await db.commit()
|
|
159
|
-
|
|
160
|
-
logger.debug(
|
|
161
|
-
f"Added message {message_id} to conversation {conversation_id}"
|
|
162
|
-
)
|
|
163
|
-
return message_id
|
|
164
|
-
|
|
165
|
-
except Exception as e:
|
|
166
|
-
logger.warning(f"Failed to add message: {e}")
|
|
167
|
-
return message_id # Return ID anyway
|
|
168
|
-
|
|
169
|
-
async def end_conversation(self, conversation_id: str) -> bool:
|
|
170
|
-
"""Mark a conversation as ended.
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
conversation_id: ID of the conversation to end
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
True if successfully updated, False otherwise
|
|
177
|
-
"""
|
|
178
|
-
await self._init_db()
|
|
179
|
-
|
|
180
|
-
try:
|
|
181
|
-
async with self._lock, aiosqlite.connect(self.db_path) as db:
|
|
182
|
-
await db.execute(
|
|
183
|
-
"UPDATE conversations SET ended_at = ? WHERE id = ?",
|
|
184
|
-
(time.time(), conversation_id),
|
|
185
|
-
)
|
|
186
|
-
await db.commit()
|
|
187
|
-
|
|
188
|
-
logger.debug(f"Ended conversation {conversation_id}")
|
|
189
|
-
return True
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
logger.warning(f"Failed to end conversation: {e}")
|
|
193
|
-
return False
|
|
194
|
-
|
|
195
|
-
async def get_conversation(self, conversation_id: str) -> Conversation | None:
|
|
196
|
-
"""Get a conversation by ID.
|
|
197
|
-
|
|
198
|
-
Args:
|
|
199
|
-
conversation_id: ID of the conversation
|
|
200
|
-
|
|
201
|
-
Returns:
|
|
202
|
-
Conversation object or None if not found
|
|
203
|
-
"""
|
|
204
|
-
await self._init_db()
|
|
205
|
-
|
|
206
|
-
try:
|
|
207
|
-
async with aiosqlite.connect(self.db_path) as db:
|
|
208
|
-
async with db.execute(
|
|
209
|
-
"SELECT id, database_name, started_at, ended_at FROM conversations WHERE id = ?",
|
|
210
|
-
(conversation_id,),
|
|
211
|
-
) as cursor:
|
|
212
|
-
row = await cursor.fetchone()
|
|
213
|
-
if not row:
|
|
214
|
-
return None
|
|
215
|
-
|
|
216
|
-
return Conversation(
|
|
217
|
-
id=row[0],
|
|
218
|
-
database_name=row[1],
|
|
219
|
-
started_at=row[2],
|
|
220
|
-
ended_at=row[3],
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
except Exception as e:
|
|
224
|
-
logger.warning(f"Failed to get conversation {conversation_id}: {e}")
|
|
225
|
-
return None
|
|
226
|
-
|
|
227
|
-
async def get_conversation_messages(
|
|
228
|
-
self, conversation_id: str
|
|
229
|
-
) -> list[ConversationMessage]:
|
|
230
|
-
"""Get all messages for a conversation.
|
|
231
|
-
|
|
232
|
-
Args:
|
|
233
|
-
conversation_id: ID of the conversation
|
|
234
|
-
|
|
235
|
-
Returns:
|
|
236
|
-
List of ConversationMessage objects ordered by index
|
|
237
|
-
"""
|
|
238
|
-
await self._init_db()
|
|
239
|
-
|
|
240
|
-
try:
|
|
241
|
-
async with aiosqlite.connect(self.db_path) as db:
|
|
242
|
-
async with db.execute(
|
|
243
|
-
"""
|
|
244
|
-
SELECT id, conversation_id, role, content, index_in_conv, created_at
|
|
245
|
-
FROM messages
|
|
246
|
-
WHERE conversation_id = ?
|
|
247
|
-
ORDER BY index_in_conv
|
|
248
|
-
""",
|
|
249
|
-
(conversation_id,),
|
|
250
|
-
) as cursor:
|
|
251
|
-
messages = []
|
|
252
|
-
async for row in cursor:
|
|
253
|
-
message = ConversationMessage.from_storage_data(
|
|
254
|
-
id_=row[0],
|
|
255
|
-
conversation_id=row[1],
|
|
256
|
-
role=row[2],
|
|
257
|
-
content_json=row[3],
|
|
258
|
-
index_in_conv=row[4],
|
|
259
|
-
created_at=row[5],
|
|
260
|
-
)
|
|
261
|
-
messages.append(message)
|
|
262
|
-
|
|
263
|
-
return messages
|
|
264
|
-
|
|
265
|
-
except Exception as e:
|
|
266
|
-
logger.warning(
|
|
267
|
-
f"Failed to get messages for conversation {conversation_id}: {e}"
|
|
268
|
-
)
|
|
269
|
-
return []
|
|
270
|
-
|
|
271
|
-
async def list_conversations(
|
|
272
|
-
self, database_name: str | None = None, limit: int = 50
|
|
273
|
-
) -> list[Conversation]:
|
|
274
|
-
"""List conversations, optionally filtered by database.
|
|
275
|
-
|
|
276
|
-
Args:
|
|
277
|
-
database_name: Optional database name filter
|
|
278
|
-
limit: Maximum number of conversations to return
|
|
279
|
-
|
|
280
|
-
Returns:
|
|
281
|
-
List of Conversation objects ordered by start time (newest first)
|
|
282
|
-
"""
|
|
283
|
-
await self._init_db()
|
|
284
|
-
|
|
285
|
-
try:
|
|
286
|
-
query = """
|
|
287
|
-
SELECT id, database_name, started_at, ended_at
|
|
288
|
-
FROM conversations
|
|
289
|
-
"""
|
|
290
|
-
params = []
|
|
291
|
-
|
|
292
|
-
if database_name:
|
|
293
|
-
query += " WHERE database_name = ?"
|
|
294
|
-
params.append(database_name)
|
|
295
|
-
|
|
296
|
-
query += " ORDER BY started_at DESC LIMIT ?"
|
|
297
|
-
params.append(limit)
|
|
298
|
-
|
|
299
|
-
async with aiosqlite.connect(self.db_path) as db:
|
|
300
|
-
async with db.execute(query, params) as cursor:
|
|
301
|
-
conversations = []
|
|
302
|
-
async for row in cursor:
|
|
303
|
-
conversation = Conversation(
|
|
304
|
-
id=row[0],
|
|
305
|
-
database_name=row[1],
|
|
306
|
-
started_at=row[2],
|
|
307
|
-
ended_at=row[3],
|
|
308
|
-
)
|
|
309
|
-
conversations.append(conversation)
|
|
310
|
-
|
|
311
|
-
return conversations
|
|
312
|
-
|
|
313
|
-
except Exception as e:
|
|
314
|
-
logger.warning(f"Failed to list conversations: {e}")
|
|
315
|
-
return []
|
|
316
|
-
|
|
317
|
-
async def delete_conversation(self, conversation_id: str) -> bool:
|
|
318
|
-
"""Delete a conversation and all its messages.
|
|
319
|
-
|
|
320
|
-
Args:
|
|
321
|
-
conversation_id: ID of the conversation to delete
|
|
322
|
-
|
|
323
|
-
Returns:
|
|
324
|
-
True if successfully deleted, False otherwise
|
|
325
|
-
"""
|
|
326
|
-
await self._init_db()
|
|
327
|
-
|
|
328
|
-
try:
|
|
329
|
-
async with self._lock, aiosqlite.connect(self.db_path) as db:
|
|
330
|
-
# Delete conversation (messages will be deleted by CASCADE)
|
|
331
|
-
cursor = await db.execute(
|
|
332
|
-
"DELETE FROM conversations WHERE id = ?", (conversation_id,)
|
|
333
|
-
)
|
|
334
|
-
await db.commit()
|
|
335
|
-
|
|
336
|
-
deleted = cursor.rowcount > 0
|
|
337
|
-
if deleted:
|
|
338
|
-
logger.debug(f"Deleted conversation {conversation_id}")
|
|
339
|
-
return deleted
|
|
340
|
-
|
|
341
|
-
except Exception as e:
|
|
342
|
-
logger.warning(f"Failed to delete conversation {conversation_id}: {e}")
|
|
343
|
-
return False
|
|
344
|
-
|
|
345
|
-
async def get_database_names(self) -> list[str]:
|
|
346
|
-
"""Get list of all database names that have conversations.
|
|
347
|
-
|
|
348
|
-
Returns:
|
|
349
|
-
List of unique database names
|
|
350
|
-
"""
|
|
351
|
-
await self._init_db()
|
|
352
|
-
|
|
353
|
-
try:
|
|
354
|
-
async with aiosqlite.connect(self.db_path) as db:
|
|
355
|
-
async with db.execute(
|
|
356
|
-
"SELECT DISTINCT database_name FROM conversations ORDER BY database_name"
|
|
357
|
-
) as cursor:
|
|
358
|
-
return [row[0] async for row in cursor]
|
|
359
|
-
|
|
360
|
-
except Exception as e:
|
|
361
|
-
logger.warning(f"Failed to get database names: {e}")
|
|
362
|
-
return []
|
sqlsaber/models/__init__.py
DELETED
sqlsaber/models/types.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"""Type definitions for SQLSaber."""
|
|
2
|
-
|
|
3
|
-
from typing import Any, TypedDict
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ColumnInfo(TypedDict):
|
|
7
|
-
"""Type definition for column information."""
|
|
8
|
-
|
|
9
|
-
data_type: str
|
|
10
|
-
nullable: bool
|
|
11
|
-
default: str | None
|
|
12
|
-
max_length: int | None
|
|
13
|
-
precision: int | None
|
|
14
|
-
scale: int | None
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ForeignKeyInfo(TypedDict):
|
|
18
|
-
"""Type definition for foreign key information."""
|
|
19
|
-
|
|
20
|
-
column: str
|
|
21
|
-
references: dict[str, str] # {"table": "schema.table", "column": "column_name"}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class SchemaInfo(TypedDict):
|
|
25
|
-
"""Type definition for schema information."""
|
|
26
|
-
|
|
27
|
-
schema: str
|
|
28
|
-
name: str
|
|
29
|
-
type: str
|
|
30
|
-
columns: dict[str, ColumnInfo]
|
|
31
|
-
primary_keys: list[str]
|
|
32
|
-
foreign_keys: list[ForeignKeyInfo]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ToolDefinition(TypedDict):
|
|
36
|
-
"""Type definition for tool definition."""
|
|
37
|
-
|
|
38
|
-
name: str
|
|
39
|
-
description: str
|
|
40
|
-
input_schema: dict[str, Any]
|
|
@@ -1,144 +0,0 @@
|
|
|
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)}"})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|