sqlsaber 0.12.0__tar.gz → 0.14.0__tar.gz
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-0.12.0 → sqlsaber-0.14.0}/.github/workflows/claude-code-review.yml +12 -13
- sqlsaber-0.14.0/.github/workflows/test.yml +33 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/CHANGELOG.md +36 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/PKG-INFO +1 -1
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/pyproject.toml +1 -1
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/anthropic.py +35 -7
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/base.py +104 -1
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/commands.py +14 -36
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/interactive.py +6 -2
- sqlsaber-0.14.0/src/sqlsaber/conversation/__init__.py +12 -0
- sqlsaber-0.14.0/src/sqlsaber/conversation/manager.py +224 -0
- sqlsaber-0.14.0/src/sqlsaber/conversation/models.py +120 -0
- sqlsaber-0.14.0/src/sqlsaber/conversation/storage.py +362 -0
- sqlsaber-0.14.0/src/sqlsaber/database/resolver.py +96 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/database/schema.py +2 -51
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_cli/test_commands.py +23 -23
- sqlsaber-0.14.0/tests/test_conversation_storage.py +136 -0
- sqlsaber-0.14.0/tests/test_database_resolver.py +126 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/uv.lock +1 -1
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/.github/workflows/claude.yml +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/.github/workflows/publish.yml +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/.gitignore +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/.python-version +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/AGENT.md +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/CLAUDE.md +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/LICENSE +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/README.md +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/pytest.ini +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/sqlsaber.svg +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/__main__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/mcp.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/streaming.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/auth.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/completers.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/database.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/display.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/memory.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/models.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/streaming.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/anthropic.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/base.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/exceptions.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/models.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/streaming.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/config/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/config/api_keys.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/config/auth.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/config/database.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/config/oauth_flow.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/config/oauth_tokens.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/config/settings.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/database/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/database/connection.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/mcp/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/mcp/mcp.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/memory/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/memory/manager.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/memory/storage.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/models/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/models/events.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/src/sqlsaber/models/types.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/conftest.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_agents/test_anthropic_oauth.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_cli/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_clients/test_anthropic_client.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_clients/test_streaming.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_config/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_config/test_database.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_config/test_oauth.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_config/test_settings.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_database/__init__.py +0 -0
- {sqlsaber-0.12.0 → sqlsaber-0.14.0}/tests/test_database/test_connection.py +0 -0
|
@@ -17,14 +17,14 @@ jobs:
|
|
|
17
17
|
# github.event.pull_request.user.login == 'external-contributor' ||
|
|
18
18
|
# github.event.pull_request.user.login == 'new-developer' ||
|
|
19
19
|
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
runs-on: ubuntu-latest
|
|
22
22
|
permissions:
|
|
23
23
|
contents: read
|
|
24
24
|
pull-requests: read
|
|
25
25
|
issues: read
|
|
26
26
|
id-token: write
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
steps:
|
|
29
29
|
- name: Checkout repository
|
|
30
30
|
uses: actions/checkout@v4
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
|
|
40
40
|
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
|
41
41
|
# model: "claude-opus-4-20250514"
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
# Direct prompt for automated review (no @claude mention needed)
|
|
44
44
|
direct_prompt: |
|
|
45
45
|
Please review this pull request and provide feedback on:
|
|
@@ -48,12 +48,12 @@ jobs:
|
|
|
48
48
|
- Performance considerations
|
|
49
49
|
- Security concerns
|
|
50
50
|
- Test coverage
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
Be constructive and helpful in your feedback.
|
|
53
53
|
|
|
54
54
|
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
|
|
55
55
|
# use_sticky_comment: true
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
# Optional: Customize review based on file types
|
|
58
58
|
# direct_prompt: |
|
|
59
59
|
# Review this PR focusing on:
|
|
@@ -61,18 +61,17 @@ jobs:
|
|
|
61
61
|
# - For API endpoints: Security, input validation, and error handling
|
|
62
62
|
# - For React components: Performance, accessibility, and best practices
|
|
63
63
|
# - For tests: Coverage, edge cases, and test quality
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
# Optional: Different prompts for different authors
|
|
66
66
|
# direct_prompt: |
|
|
67
|
-
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
|
|
67
|
+
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
|
|
68
68
|
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
|
|
69
69
|
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
# Optional: Add specific tools for running tests or linting
|
|
72
72
|
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
|
|
73
|
-
|
|
74
|
-
# Optional: Skip review for certain conditions
|
|
75
|
-
# if: |
|
|
76
|
-
# !contains(github.event.pull_request.title, '[skip-review]') &&
|
|
77
|
-
# !contains(github.event.pull_request.title, '[WIP]')
|
|
78
73
|
|
|
74
|
+
# Optional: Skip review for certain conditions
|
|
75
|
+
if: |
|
|
76
|
+
!contains(github.event.pull_request.title, '[skip-review]') &&
|
|
77
|
+
!contains(github.event.pull_request.title, '[WIP]')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
# Cancel active CI runs for a PR before starting another run
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow}}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v5
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.12"
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: uv sync --locked --all-extras --dev
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: uv run python -m pytest
|
|
@@ -4,6 +4,42 @@ All notable changes to SQLSaber will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.14.0] - 2025-08-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Local conversation storage between user and agent
|
|
12
|
+
- Store conversation history persistently
|
|
13
|
+
- Track messages with proper attribution
|
|
14
|
+
- Added automated test execution in CI
|
|
15
|
+
- New GitHub Actions workflow for running tests
|
|
16
|
+
- Updated code review workflow
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed CLI commands test suite (#11)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Removed schema caching from SchemaManager
|
|
25
|
+
- Simplified schema introspection by removing cache logic
|
|
26
|
+
- Direct database queries for schema information
|
|
27
|
+
|
|
28
|
+
## [0.13.0] - 2025-07-26
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Database resolver abstraction for unified connection handling
|
|
33
|
+
- Extended `-d` flag to accept PostgreSQL and MySQL connection strings (e.g., `postgresql://user:pass@host:5432/db`)
|
|
34
|
+
- Support for direct connection strings alongside existing file path and configured database support
|
|
35
|
+
- Examples: `saber -d "postgresql://user:pass@host:5432/db" "show users"`
|
|
36
|
+
|
|
37
|
+
## [0.12.0] - 2025-07-23
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- Add support for ad-hoc SQLite files via `--database`/`-d` flag
|
|
42
|
+
|
|
7
43
|
## [0.11.0] - 2025-07-09
|
|
8
44
|
|
|
9
45
|
### 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:
|
|
@@ -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
|
+
]
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
from typing import Annotated
|
|
7
6
|
|
|
8
7
|
import cyclopts
|
|
@@ -17,6 +16,7 @@ from sqlsaber.cli.models import create_models_app
|
|
|
17
16
|
from sqlsaber.cli.streaming import StreamingQueryHandler
|
|
18
17
|
from sqlsaber.config.database import DatabaseConfigManager
|
|
19
18
|
from sqlsaber.database.connection import DatabaseConnection
|
|
19
|
+
from sqlsaber.database.resolver import DatabaseResolutionError, resolve_database
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class CLIError(Exception):
|
|
@@ -43,7 +43,7 @@ def meta_handler(
|
|
|
43
43
|
str | None,
|
|
44
44
|
cyclopts.Parameter(
|
|
45
45
|
["--database", "-d"],
|
|
46
|
-
help="Database connection name
|
|
46
|
+
help="Database connection name, file path (CSV/SQLite), or connection string (postgresql://, mysql://) (uses default if not specified)",
|
|
47
47
|
),
|
|
48
48
|
] = None,
|
|
49
49
|
):
|
|
@@ -56,6 +56,8 @@ def meta_handler(
|
|
|
56
56
|
saber -d mydb "show me users" # Run a query with specific database
|
|
57
57
|
saber -d data.csv "show me users" # Run a query with ad-hoc CSV file
|
|
58
58
|
saber -d data.db "show me users" # Run a query with ad-hoc SQLite file
|
|
59
|
+
saber -d "postgresql://user:pass@host:5432/db" "show users" # PostgreSQL connection string
|
|
60
|
+
saber -d "mysql://user:pass@host:3306/db" "show users" # MySQL connection string
|
|
59
61
|
echo "show me all users" | saber # Read query from stdin
|
|
60
62
|
cat query.txt | saber # Read query from file via stdin
|
|
61
63
|
"""
|
|
@@ -75,7 +77,7 @@ def query(
|
|
|
75
77
|
str | None,
|
|
76
78
|
cyclopts.Parameter(
|
|
77
79
|
["--database", "-d"],
|
|
78
|
-
help="Database connection name
|
|
80
|
+
help="Database connection name, file path (CSV/SQLite), or connection string (postgresql://, mysql://) (uses default if not specified)",
|
|
79
81
|
),
|
|
80
82
|
] = None,
|
|
81
83
|
):
|
|
@@ -92,6 +94,8 @@ def query(
|
|
|
92
94
|
saber "show me all users" # Run a single query
|
|
93
95
|
saber -d data.csv "show users" # Run a query with ad-hoc CSV file
|
|
94
96
|
saber -d data.db "show users" # Run a query with ad-hoc SQLite file
|
|
97
|
+
saber -d "postgresql://user:pass@host:5432/db" "show users" # PostgreSQL connection string
|
|
98
|
+
saber -d "mysql://user:pass@host:3306/db" "show users" # MySQL connection string
|
|
95
99
|
echo "show me all users" | saber # Read query from stdin
|
|
96
100
|
"""
|
|
97
101
|
|
|
@@ -105,39 +109,13 @@ def query(
|
|
|
105
109
|
# If stdin was empty, fall back to interactive mode
|
|
106
110
|
actual_query = None
|
|
107
111
|
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
connection_string = f"csv:///{csv_path}"
|
|
116
|
-
db_name = csv_path.stem
|
|
117
|
-
# Check if this is a direct SQLite file path
|
|
118
|
-
elif database.endswith((".db", ".sqlite", ".sqlite3")):
|
|
119
|
-
sqlite_path = Path(database).expanduser().resolve()
|
|
120
|
-
if not sqlite_path.exists():
|
|
121
|
-
raise CLIError(f"SQLite file '{database}' not found.")
|
|
122
|
-
connection_string = f"sqlite:///{sqlite_path}"
|
|
123
|
-
db_name = sqlite_path.stem
|
|
124
|
-
else:
|
|
125
|
-
# Look up configured database connection
|
|
126
|
-
db_config = config_manager.get_database(database)
|
|
127
|
-
if not db_config:
|
|
128
|
-
raise CLIError(
|
|
129
|
-
f"Database connection '{database}' not found. Use 'sqlsaber db list' to see available connections."
|
|
130
|
-
)
|
|
131
|
-
connection_string = db_config.to_connection_string()
|
|
132
|
-
db_name = db_config.name
|
|
133
|
-
else:
|
|
134
|
-
db_config = config_manager.get_default_database()
|
|
135
|
-
if not db_config:
|
|
136
|
-
raise CLIError(
|
|
137
|
-
"No database connections configured. Use 'sqlsaber db add <name>' to add a database connection."
|
|
138
|
-
)
|
|
139
|
-
connection_string = db_config.to_connection_string()
|
|
140
|
-
db_name = db_config.name
|
|
112
|
+
# Resolve database from CLI input
|
|
113
|
+
try:
|
|
114
|
+
resolved = resolve_database(database, config_manager)
|
|
115
|
+
connection_string = resolved.connection_string
|
|
116
|
+
db_name = resolved.name
|
|
117
|
+
except DatabaseResolutionError as e:
|
|
118
|
+
raise CLIError(str(e))
|
|
141
119
|
|
|
142
120
|
# Create database connection
|
|
143
121
|
try:
|
|
@@ -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
|
+
]
|