sqlsaber 0.13.0__py3-none-any.whl → 0.14.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 +35 -7
- sqlsaber/agents/base.py +104 -1
- 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-0.13.0.dist-info → sqlsaber-0.14.0.dist-info}/METADATA +1 -1
- {sqlsaber-0.13.0.dist-info → sqlsaber-0.14.0.dist-info}/RECORD +13 -9
- {sqlsaber-0.13.0.dist-info → sqlsaber-0.14.0.dist-info}/WHEEL +0 -0
- {sqlsaber-0.13.0.dist-info → sqlsaber-0.14.0.dist-info}/entry_points.txt +0 -0
- {sqlsaber-0.13.0.dist-info → sqlsaber-0.14.0.dist-info}/licenses/LICENSE +0 -0
sqlsaber/agents/anthropic.py
CHANGED
|
@@ -450,6 +450,16 @@ Guidelines:
|
|
|
450
450
|
self._last_query = None
|
|
451
451
|
|
|
452
452
|
try:
|
|
453
|
+
# Ensure conversation is active for persistence
|
|
454
|
+
await self._ensure_conversation()
|
|
455
|
+
|
|
456
|
+
# Store user message in conversation history and persistence
|
|
457
|
+
if use_history:
|
|
458
|
+
self.conversation_history.append(
|
|
459
|
+
{"role": "user", "content": user_query}
|
|
460
|
+
)
|
|
461
|
+
await self._store_user_message(user_query)
|
|
462
|
+
|
|
453
463
|
# Build messages with history if requested
|
|
454
464
|
messages = []
|
|
455
465
|
if use_history:
|
|
@@ -461,8 +471,9 @@ Guidelines:
|
|
|
461
471
|
instructions = self._get_sql_assistant_instructions()
|
|
462
472
|
messages.append(Message(MessageRole.USER, instructions))
|
|
463
473
|
|
|
464
|
-
# Add current user message
|
|
465
|
-
|
|
474
|
+
# Add current user message if not already in messages from history
|
|
475
|
+
if not use_history:
|
|
476
|
+
messages.append(Message(MessageRole.USER, user_query))
|
|
466
477
|
|
|
467
478
|
# Create initial request and get response
|
|
468
479
|
request = self._create_message_request(messages)
|
|
@@ -484,9 +495,12 @@ Guidelines:
|
|
|
484
495
|
return
|
|
485
496
|
|
|
486
497
|
# Add assistant's response to conversation
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
498
|
+
assistant_content = {"role": "assistant", "content": response.content}
|
|
499
|
+
collected_content.append(assistant_content)
|
|
500
|
+
|
|
501
|
+
# Store the assistant message immediately (not from collected_content)
|
|
502
|
+
if use_history:
|
|
503
|
+
await self._store_assistant_message(response.content)
|
|
490
504
|
|
|
491
505
|
# Execute tools and get results
|
|
492
506
|
tool_results = []
|
|
@@ -499,9 +513,19 @@ Guidelines:
|
|
|
499
513
|
tool_results = event
|
|
500
514
|
|
|
501
515
|
# Continue conversation with tool results
|
|
502
|
-
|
|
516
|
+
tool_content = {"role": "user", "content": tool_results}
|
|
517
|
+
collected_content.append(tool_content)
|
|
518
|
+
|
|
519
|
+
# Store the tool message immediately and update history
|
|
503
520
|
if use_history:
|
|
504
|
-
|
|
521
|
+
# Only add the NEW messages to history (not the accumulated ones)
|
|
522
|
+
# collected_content has [assistant1, tool1, assistant2, tool2, ...]
|
|
523
|
+
# We only want to add the last 2 items that were just added
|
|
524
|
+
new_messages_for_history = collected_content[
|
|
525
|
+
-2:
|
|
526
|
+
] # Last assistant + tool pair
|
|
527
|
+
self.conversation_history.extend(new_messages_for_history)
|
|
528
|
+
await self._store_tool_message(tool_results)
|
|
505
529
|
|
|
506
530
|
if cancellation_token is not None and cancellation_token.is_set():
|
|
507
531
|
return
|
|
@@ -541,6 +565,10 @@ Guidelines:
|
|
|
541
565
|
{"role": "assistant", "content": response.content}
|
|
542
566
|
)
|
|
543
567
|
|
|
568
|
+
# Store final assistant message in persistence (only if not tool_use)
|
|
569
|
+
if response.stop_reason != "tool_use":
|
|
570
|
+
await self._store_assistant_message(response.content)
|
|
571
|
+
|
|
544
572
|
except asyncio.CancelledError:
|
|
545
573
|
return
|
|
546
574
|
except Exception as e:
|
sqlsaber/agents/base.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Any, AsyncIterator
|
|
|
7
7
|
|
|
8
8
|
from uniplot import histogram, plot
|
|
9
9
|
|
|
10
|
+
from sqlsaber.conversation.manager import ConversationManager
|
|
10
11
|
from sqlsaber.database.connection import (
|
|
11
12
|
BaseDatabaseConnection,
|
|
12
13
|
CSVConnection,
|
|
@@ -26,6 +27,11 @@ class BaseSQLAgent(ABC):
|
|
|
26
27
|
self.schema_manager = SchemaManager(db_connection)
|
|
27
28
|
self.conversation_history: list[dict[str, Any]] = []
|
|
28
29
|
|
|
30
|
+
# Conversation persistence
|
|
31
|
+
self._conv_manager = ConversationManager()
|
|
32
|
+
self._conversation_id: str | None = None
|
|
33
|
+
self._msg_index: int = 0
|
|
34
|
+
|
|
29
35
|
@abstractmethod
|
|
30
36
|
async def query_stream(
|
|
31
37
|
self,
|
|
@@ -42,8 +48,12 @@ class BaseSQLAgent(ABC):
|
|
|
42
48
|
"""
|
|
43
49
|
pass
|
|
44
50
|
|
|
45
|
-
def clear_history(self):
|
|
51
|
+
async def clear_history(self):
|
|
46
52
|
"""Clear conversation history."""
|
|
53
|
+
# End current conversation in storage
|
|
54
|
+
await self._end_conversation()
|
|
55
|
+
|
|
56
|
+
# Clear in-memory history
|
|
47
57
|
self.conversation_history = []
|
|
48
58
|
|
|
49
59
|
def _get_database_type_name(self) -> str:
|
|
@@ -284,3 +294,96 @@ class BaseSQLAgent(ABC):
|
|
|
284
294
|
|
|
285
295
|
except Exception as e:
|
|
286
296
|
return json.dumps({"error": f"Error creating plot: {str(e)}"})
|
|
297
|
+
|
|
298
|
+
# Conversation persistence helpers
|
|
299
|
+
|
|
300
|
+
async def _ensure_conversation(self) -> None:
|
|
301
|
+
"""Ensure a conversation is active for storing messages."""
|
|
302
|
+
if self._conversation_id is None:
|
|
303
|
+
db_name = getattr(self, "database_name", "unknown")
|
|
304
|
+
self._conversation_id = await self._conv_manager.start_conversation(db_name)
|
|
305
|
+
self._msg_index = 0
|
|
306
|
+
|
|
307
|
+
async def _store_user_message(self, content: str | dict[str, Any]) -> None:
|
|
308
|
+
"""Store a user message in conversation history."""
|
|
309
|
+
if self._conversation_id is None:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
await self._conv_manager.add_user_message(
|
|
313
|
+
self._conversation_id, content, self._msg_index
|
|
314
|
+
)
|
|
315
|
+
self._msg_index += 1
|
|
316
|
+
|
|
317
|
+
async def _store_assistant_message(
|
|
318
|
+
self, content: list[dict[str, Any]] | dict[str, Any]
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Store an assistant message in conversation history."""
|
|
321
|
+
if self._conversation_id is None:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
await self._conv_manager.add_assistant_message(
|
|
325
|
+
self._conversation_id, content, self._msg_index
|
|
326
|
+
)
|
|
327
|
+
self._msg_index += 1
|
|
328
|
+
|
|
329
|
+
async def _store_tool_message(
|
|
330
|
+
self, content: list[dict[str, Any]] | dict[str, Any]
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Store a tool/system message in conversation history."""
|
|
333
|
+
if self._conversation_id is None:
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
await self._conv_manager.add_tool_message(
|
|
337
|
+
self._conversation_id, content, self._msg_index
|
|
338
|
+
)
|
|
339
|
+
self._msg_index += 1
|
|
340
|
+
|
|
341
|
+
async def _end_conversation(self) -> None:
|
|
342
|
+
"""End the current conversation."""
|
|
343
|
+
if self._conversation_id:
|
|
344
|
+
await self._conv_manager.end_conversation(self._conversation_id)
|
|
345
|
+
self._conversation_id = None
|
|
346
|
+
self._msg_index = 0
|
|
347
|
+
|
|
348
|
+
async def restore_conversation(self, conversation_id: str) -> bool:
|
|
349
|
+
"""Restore a conversation from storage to in-memory history.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
conversation_id: ID of the conversation to restore
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
True if successfully restored, False otherwise
|
|
356
|
+
"""
|
|
357
|
+
success = await self._conv_manager.restore_conversation_to_agent(
|
|
358
|
+
conversation_id, self.conversation_history
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if success:
|
|
362
|
+
# Set up for continuing this conversation
|
|
363
|
+
self._conversation_id = conversation_id
|
|
364
|
+
self._msg_index = len(self.conversation_history)
|
|
365
|
+
|
|
366
|
+
return success
|
|
367
|
+
|
|
368
|
+
async def list_conversations(self, limit: int = 50) -> list:
|
|
369
|
+
"""List conversations for this agent's database.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
limit: Maximum number of conversations to return
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
List of conversation data
|
|
376
|
+
"""
|
|
377
|
+
db_name = getattr(self, "database_name", None)
|
|
378
|
+
conversations = await self._conv_manager.list_conversations(db_name, limit)
|
|
379
|
+
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
"id": conv.id,
|
|
383
|
+
"database_name": conv.database_name,
|
|
384
|
+
"started_at": conv.formatted_start_time(),
|
|
385
|
+
"ended_at": conv.formatted_end_time(),
|
|
386
|
+
"duration": conv.duration_seconds(),
|
|
387
|
+
}
|
|
388
|
+
for conv in conversations
|
|
389
|
+
]
|
sqlsaber/cli/interactive.py
CHANGED
|
@@ -136,11 +136,15 @@ class InteractiveSession:
|
|
|
136
136
|
if not user_query:
|
|
137
137
|
continue
|
|
138
138
|
|
|
139
|
-
if
|
|
139
|
+
if (
|
|
140
|
+
user_query in ["/exit", "/quit"]
|
|
141
|
+
or user_query.startswith("/exit")
|
|
142
|
+
or user_query.startswith("/quit")
|
|
143
|
+
):
|
|
140
144
|
break
|
|
141
145
|
|
|
142
146
|
if user_query == "/clear":
|
|
143
|
-
self.agent.clear_history()
|
|
147
|
+
await self.agent.clear_history()
|
|
144
148
|
self.console.print("[green]Conversation history cleared.[/green]\n")
|
|
145
149
|
continue
|
|
146
150
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Conversation history storage for SQLSaber."""
|
|
2
|
+
|
|
3
|
+
from .manager import ConversationManager
|
|
4
|
+
from .models import Conversation, ConversationMessage
|
|
5
|
+
from .storage import ConversationStorage
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Conversation",
|
|
9
|
+
"ConversationMessage",
|
|
10
|
+
"ConversationStorage",
|
|
11
|
+
"ConversationManager",
|
|
12
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Manager for conversation storage operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .models import Conversation, ConversationMessage
|
|
8
|
+
from .storage import ConversationStorage
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConversationManager:
|
|
14
|
+
"""High-level manager for conversation storage operations."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize conversation manager."""
|
|
18
|
+
self._storage = ConversationStorage()
|
|
19
|
+
|
|
20
|
+
async def start_conversation(self, database_name: str) -> str:
|
|
21
|
+
"""Start a new conversation.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
database_name: Name of the database for this conversation
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Conversation ID
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
return await self._storage.create_conversation(database_name)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.warning(f"Failed to start conversation: {e}")
|
|
33
|
+
return str(uuid.uuid4())
|
|
34
|
+
|
|
35
|
+
async def add_user_message(
|
|
36
|
+
self, conversation_id: str, content: str | dict[str, Any], index: int
|
|
37
|
+
) -> str:
|
|
38
|
+
"""Add a user message to the conversation.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
conversation_id: ID of the conversation
|
|
42
|
+
content: Message content
|
|
43
|
+
index: Sequential index in conversation
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Message ID
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
return await self._storage.add_message(
|
|
50
|
+
conversation_id, "user", content, index
|
|
51
|
+
)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.warning(f"Failed to add user message: {e}")
|
|
54
|
+
return str(uuid.uuid4())
|
|
55
|
+
|
|
56
|
+
async def add_assistant_message(
|
|
57
|
+
self,
|
|
58
|
+
conversation_id: str,
|
|
59
|
+
content: list[dict[str, Any]] | dict[str, Any],
|
|
60
|
+
index: int,
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Add an assistant message to the conversation.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
conversation_id: ID of the conversation
|
|
66
|
+
content: Message content (typically ContentBlock list)
|
|
67
|
+
index: Sequential index in conversation
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Message ID
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
return await self._storage.add_message(
|
|
74
|
+
conversation_id, "assistant", content, index
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.warning(f"Failed to add assistant message: {e}")
|
|
78
|
+
return str(uuid.uuid4())
|
|
79
|
+
|
|
80
|
+
async def add_tool_message(
|
|
81
|
+
self,
|
|
82
|
+
conversation_id: str,
|
|
83
|
+
content: list[dict[str, Any]] | dict[str, Any],
|
|
84
|
+
index: int,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""Add a tool/system message to the conversation.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
conversation_id: ID of the conversation
|
|
90
|
+
content: Message content (typically tool results)
|
|
91
|
+
index: Sequential index in conversation
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Message ID
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
return await self._storage.add_message(
|
|
98
|
+
conversation_id, "tool", content, index
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.warning(f"Failed to add tool message: {e}")
|
|
102
|
+
return str(uuid.uuid4())
|
|
103
|
+
|
|
104
|
+
async def end_conversation(self, conversation_id: str) -> bool:
|
|
105
|
+
"""End a conversation.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
conversation_id: ID of the conversation to end
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if successfully ended, False otherwise
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
return await self._storage.end_conversation(conversation_id)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.warning(f"Failed to end conversation: {e}")
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
async def get_conversation(self, conversation_id: str) -> Conversation | None:
|
|
120
|
+
"""Get a conversation by ID.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
conversation_id: ID of the conversation
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Conversation object or None if not found
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
return await self._storage.get_conversation(conversation_id)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.warning(f"Failed to get conversation: {e}")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
async def get_conversation_messages(
|
|
135
|
+
self, conversation_id: str
|
|
136
|
+
) -> list[ConversationMessage]:
|
|
137
|
+
"""Get all messages for a conversation.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
conversation_id: ID of the conversation
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of messages ordered by index
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
return await self._storage.get_conversation_messages(conversation_id)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.warning(f"Failed to get conversation messages: {e}")
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
async def list_conversations(
|
|
152
|
+
self, database_name: str | None = None, limit: int = 50
|
|
153
|
+
) -> list[Conversation]:
|
|
154
|
+
"""List conversations.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
database_name: Optional database name filter
|
|
158
|
+
limit: Maximum number of conversations to return
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of conversations ordered by start time (newest first)
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
return await self._storage.list_conversations(database_name, limit)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.warning(f"Failed to list conversations: {e}")
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
async def delete_conversation(self, conversation_id: str) -> bool:
|
|
170
|
+
"""Delete a conversation.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
conversation_id: ID of the conversation to delete
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
True if successfully deleted, False otherwise
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
return await self._storage.delete_conversation(conversation_id)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.warning(f"Failed to delete conversation: {e}")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
async def get_database_names(self) -> list[str]:
|
|
185
|
+
"""Get list of database names with conversations.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of unique database names
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
return await self._storage.get_database_names()
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.warning(f"Failed to get database names: {e}")
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
async def restore_conversation_to_agent(
|
|
197
|
+
self, conversation_id: str, agent_history: list[dict[str, Any]]
|
|
198
|
+
) -> bool:
|
|
199
|
+
"""Restore a conversation's messages to an agent's in-memory history.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
conversation_id: ID of the conversation to restore
|
|
203
|
+
agent_history: Agent's conversation_history list to populate
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if successfully restored, False otherwise
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
messages = await self.get_conversation_messages(conversation_id)
|
|
210
|
+
|
|
211
|
+
# Clear existing history
|
|
212
|
+
agent_history.clear()
|
|
213
|
+
|
|
214
|
+
# Convert messages back to agent format
|
|
215
|
+
for msg in messages:
|
|
216
|
+
if msg.role in ("user", "assistant", "tool"):
|
|
217
|
+
agent_history.append({"role": msg.role, "content": msg.content})
|
|
218
|
+
|
|
219
|
+
logger.debug(f"Restored {len(messages)} messages to agent history")
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.warning(f"Failed to restore conversation to agent: {e}")
|
|
224
|
+
return False
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Data models for conversation storage."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Conversation:
|
|
11
|
+
"""Represents a conversation session."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
database_name: str
|
|
15
|
+
started_at: float
|
|
16
|
+
ended_at: float | None = None
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> dict[str, Any]:
|
|
19
|
+
"""Convert to dictionary for JSON serialization."""
|
|
20
|
+
return {
|
|
21
|
+
"id": self.id,
|
|
22
|
+
"database_name": self.database_name,
|
|
23
|
+
"started_at": self.started_at,
|
|
24
|
+
"ended_at": self.ended_at,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, data: dict[str, Any]) -> "Conversation":
|
|
29
|
+
"""Create from dictionary."""
|
|
30
|
+
return cls(
|
|
31
|
+
id=data["id"],
|
|
32
|
+
database_name=data["database_name"],
|
|
33
|
+
started_at=data["started_at"],
|
|
34
|
+
ended_at=data.get("ended_at"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def formatted_start_time(self) -> str:
|
|
38
|
+
"""Get human-readable start timestamp."""
|
|
39
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.started_at))
|
|
40
|
+
|
|
41
|
+
def formatted_end_time(self) -> str | None:
|
|
42
|
+
"""Get human-readable end timestamp."""
|
|
43
|
+
if self.ended_at is None:
|
|
44
|
+
return None
|
|
45
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.ended_at))
|
|
46
|
+
|
|
47
|
+
def duration_seconds(self) -> float | None:
|
|
48
|
+
"""Get conversation duration in seconds."""
|
|
49
|
+
if self.ended_at is None:
|
|
50
|
+
return None
|
|
51
|
+
return self.ended_at - self.started_at
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ConversationMessage:
|
|
56
|
+
"""Represents a single message in a conversation."""
|
|
57
|
+
|
|
58
|
+
id: str
|
|
59
|
+
conversation_id: str
|
|
60
|
+
role: str
|
|
61
|
+
content: dict[str, Any] | str
|
|
62
|
+
index_in_conv: int
|
|
63
|
+
created_at: float
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict[str, Any]:
|
|
66
|
+
"""Convert to dictionary for JSON serialization."""
|
|
67
|
+
return {
|
|
68
|
+
"id": self.id,
|
|
69
|
+
"conversation_id": self.conversation_id,
|
|
70
|
+
"role": self.role,
|
|
71
|
+
"content": self.content,
|
|
72
|
+
"index_in_conv": self.index_in_conv,
|
|
73
|
+
"created_at": self.created_at,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, data: dict[str, Any]) -> "ConversationMessage":
|
|
78
|
+
"""Create from dictionary."""
|
|
79
|
+
return cls(
|
|
80
|
+
id=data["id"],
|
|
81
|
+
conversation_id=data["conversation_id"],
|
|
82
|
+
role=data["role"],
|
|
83
|
+
content=data["content"],
|
|
84
|
+
index_in_conv=data["index_in_conv"],
|
|
85
|
+
created_at=data["created_at"],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def formatted_timestamp(self) -> str:
|
|
89
|
+
"""Get human-readable timestamp."""
|
|
90
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.created_at))
|
|
91
|
+
|
|
92
|
+
def content_json(self) -> str:
|
|
93
|
+
"""Get content as JSON string for storage."""
|
|
94
|
+
return json.dumps(self.content)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_storage_data(
|
|
98
|
+
cls,
|
|
99
|
+
id_: str,
|
|
100
|
+
conversation_id: str,
|
|
101
|
+
role: str,
|
|
102
|
+
content_json: str,
|
|
103
|
+
index_in_conv: int,
|
|
104
|
+
created_at: float,
|
|
105
|
+
) -> "ConversationMessage":
|
|
106
|
+
"""Create from SQLite storage data."""
|
|
107
|
+
try:
|
|
108
|
+
content = json.loads(content_json)
|
|
109
|
+
except json.JSONDecodeError:
|
|
110
|
+
# Fallback to string content for malformed JSON
|
|
111
|
+
content = content_json
|
|
112
|
+
|
|
113
|
+
return cls(
|
|
114
|
+
id=id_,
|
|
115
|
+
conversation_id=conversation_id,
|
|
116
|
+
role=role,
|
|
117
|
+
content=content,
|
|
118
|
+
index_in_conv=index_in_conv,
|
|
119
|
+
created_at=created_at,
|
|
120
|
+
)
|
|
@@ -0,0 +1,362 @@
|
|
|
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/database/schema.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Database schema introspection utilities."""
|
|
2
2
|
|
|
3
|
-
import time
|
|
4
3
|
from abc import ABC, abstractmethod
|
|
5
4
|
from typing import Any
|
|
6
5
|
|
|
@@ -532,12 +531,10 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
|
|
|
532
531
|
|
|
533
532
|
|
|
534
533
|
class SchemaManager:
|
|
535
|
-
"""Manages database schema introspection
|
|
534
|
+
"""Manages database schema introspection."""
|
|
536
535
|
|
|
537
|
-
def __init__(self, db_connection: BaseDatabaseConnection
|
|
536
|
+
def __init__(self, db_connection: BaseDatabaseConnection):
|
|
538
537
|
self.db = db_connection
|
|
539
|
-
self.cache_ttl = cache_ttl # Default 15 minutes
|
|
540
|
-
self._schema_cache: dict[str, tuple[float, dict[str, Any]]] = {}
|
|
541
538
|
|
|
542
539
|
# Select appropriate introspector based on connection type
|
|
543
540
|
if isinstance(db_connection, PostgreSQLConnection):
|
|
@@ -551,10 +548,6 @@ class SchemaManager:
|
|
|
551
548
|
f"Unsupported database connection type: {type(db_connection)}"
|
|
552
549
|
)
|
|
553
550
|
|
|
554
|
-
def clear_schema_cache(self):
|
|
555
|
-
"""Clear the schema cache."""
|
|
556
|
-
self._schema_cache.clear()
|
|
557
|
-
|
|
558
551
|
async def get_schema_info(
|
|
559
552
|
self, table_pattern: str | None = None
|
|
560
553
|
) -> dict[str, SchemaInfo]:
|
|
@@ -563,31 +556,6 @@ class SchemaManager:
|
|
|
563
556
|
Args:
|
|
564
557
|
table_pattern: Optional SQL LIKE pattern to filter tables (e.g., 'public.user%')
|
|
565
558
|
"""
|
|
566
|
-
# Check cache first
|
|
567
|
-
cache_key = f"schema:{table_pattern or 'all'}"
|
|
568
|
-
cached_data = self._get_cached_schema(cache_key)
|
|
569
|
-
if cached_data is not None:
|
|
570
|
-
return cached_data
|
|
571
|
-
|
|
572
|
-
# Fetch from database if not cached
|
|
573
|
-
schema_info = await self._fetch_schema_from_db(table_pattern)
|
|
574
|
-
|
|
575
|
-
# Cache the result
|
|
576
|
-
self._schema_cache[cache_key] = (time.time(), schema_info)
|
|
577
|
-
return schema_info
|
|
578
|
-
|
|
579
|
-
def _get_cached_schema(self, cache_key: str) -> dict[str, SchemaInfo] | None:
|
|
580
|
-
"""Get schema from cache if available and not expired."""
|
|
581
|
-
if cache_key in self._schema_cache:
|
|
582
|
-
cached_time, cached_data = self._schema_cache[cache_key]
|
|
583
|
-
if time.time() - cached_time < self.cache_ttl:
|
|
584
|
-
return cached_data
|
|
585
|
-
return None
|
|
586
|
-
|
|
587
|
-
async def _fetch_schema_from_db(
|
|
588
|
-
self, table_pattern: str | None
|
|
589
|
-
) -> dict[str, SchemaInfo]:
|
|
590
|
-
"""Fetch schema information from database."""
|
|
591
559
|
# Get all schema components
|
|
592
560
|
tables = await self.introspector.get_tables_info(self.db, table_pattern)
|
|
593
561
|
columns = await self.introspector.get_columns_info(self.db, tables)
|
|
@@ -672,13 +640,6 @@ class SchemaManager:
|
|
|
672
640
|
|
|
673
641
|
async def list_tables(self) -> dict[str, Any]:
|
|
674
642
|
"""Get a list of all tables with basic information."""
|
|
675
|
-
# Check cache first
|
|
676
|
-
cache_key = "list_tables"
|
|
677
|
-
cached_data = self._get_cached_tables(cache_key)
|
|
678
|
-
if cached_data is not None:
|
|
679
|
-
return cached_data
|
|
680
|
-
|
|
681
|
-
# Fetch from database if not cached
|
|
682
643
|
tables = await self.introspector.list_tables_info(self.db)
|
|
683
644
|
|
|
684
645
|
# Format the result
|
|
@@ -694,14 +655,4 @@ class SchemaManager:
|
|
|
694
655
|
}
|
|
695
656
|
)
|
|
696
657
|
|
|
697
|
-
# Cache the result
|
|
698
|
-
self._schema_cache[cache_key] = (time.time(), result)
|
|
699
658
|
return result
|
|
700
|
-
|
|
701
|
-
def _get_cached_tables(self, cache_key: str) -> dict[str, Any] | None:
|
|
702
|
-
"""Get table list from cache if available and not expired."""
|
|
703
|
-
if cache_key in self._schema_cache:
|
|
704
|
-
cached_time, cached_data = self._schema_cache[cache_key]
|
|
705
|
-
if time.time() - cached_time < self.cache_ttl:
|
|
706
|
-
return cached_data
|
|
707
|
-
return None
|
|
@@ -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=xH4xY1lq5idDgY8Vklgu5hGLoKLHWQMMfPgjvQjfG7I,23615
|
|
5
|
+
sqlsaber/agents/base.py,sha256=9A-iceb93eHrQHTdLoeSicP3ZqDHOFTx3Fe5OvHvUkg,14093
|
|
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,10 +28,14 @@ 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
40
|
sqlsaber/mcp/mcp.py,sha256=YH4crygqb5_Y94nsns6d-26FZCTlDPOh3tf-ghihzDM,4440
|
|
37
41
|
sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
|
|
@@ -40,8 +44,8 @@ sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,57
|
|
|
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-0.
|
|
44
|
-
sqlsaber-0.
|
|
45
|
-
sqlsaber-0.
|
|
46
|
-
sqlsaber-0.
|
|
47
|
-
sqlsaber-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|