sqlsaber 0.14.0__py3-none-any.whl → 0.16.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/__init__.py +2 -4
- sqlsaber/agents/base.py +18 -221
- sqlsaber/agents/mcp.py +2 -2
- sqlsaber/agents/pydantic_ai_agent.py +170 -0
- sqlsaber/cli/auth.py +146 -79
- sqlsaber/cli/commands.py +22 -7
- sqlsaber/cli/database.py +1 -1
- sqlsaber/cli/interactive.py +65 -30
- sqlsaber/cli/models.py +58 -29
- sqlsaber/cli/streaming.py +114 -77
- sqlsaber/config/api_keys.py +9 -11
- sqlsaber/config/providers.py +116 -0
- sqlsaber/config/settings.py +50 -30
- sqlsaber/database/connection.py +3 -3
- sqlsaber/mcp/mcp.py +43 -51
- sqlsaber/models/__init__.py +0 -3
- sqlsaber/tools/__init__.py +25 -0
- sqlsaber/tools/base.py +85 -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.14.0.dist-info → sqlsaber-0.16.0.dist-info}/METADATA +20 -39
- sqlsaber-0.16.0.dist-info/RECORD +51 -0
- sqlsaber/agents/anthropic.py +0 -579
- sqlsaber/agents/streaming.py +0 -16
- sqlsaber/clients/__init__.py +0 -6
- sqlsaber/clients/anthropic.py +0 -285
- sqlsaber/clients/base.py +0 -31
- sqlsaber/clients/exceptions.py +0 -117
- sqlsaber/clients/models.py +0 -282
- sqlsaber/clients/streaming.py +0 -257
- sqlsaber/models/events.py +0 -28
- sqlsaber-0.14.0.dist-info/RECORD +0 -51
- {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/WHEEL +0 -0
- {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/entry_points.txt +0 -0
- {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/licenses/LICENSE +0 -0
sqlsaber/cli/auth.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"""Authentication CLI commands."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import os
|
|
4
|
+
|
|
4
5
|
import cyclopts
|
|
6
|
+
import keyring
|
|
7
|
+
import questionary
|
|
5
8
|
from rich.console import Console
|
|
6
9
|
|
|
10
|
+
from sqlsaber.config import providers
|
|
11
|
+
from sqlsaber.config.api_keys import APIKeyManager
|
|
7
12
|
from sqlsaber.config.auth import AuthConfigManager, AuthMethod
|
|
8
13
|
from sqlsaber.config.oauth_flow import AnthropicOAuthFlow
|
|
14
|
+
from sqlsaber.config.oauth_tokens import OAuthTokenManager
|
|
9
15
|
|
|
10
16
|
# Global instances for CLI commands
|
|
11
17
|
console = Console()
|
|
@@ -20,59 +26,59 @@ auth_app = cyclopts.App(
|
|
|
20
26
|
|
|
21
27
|
@auth_app.command
|
|
22
28
|
def setup():
|
|
23
|
-
"""Configure authentication
|
|
24
|
-
console.print("\n[bold]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
choices=[
|
|
30
|
-
questionary.Choice(
|
|
31
|
-
title="Anthropic API Key",
|
|
32
|
-
value=AuthMethod.API_KEY,
|
|
33
|
-
description="You can create one by visiting https://console.anthropic.com",
|
|
34
|
-
),
|
|
35
|
-
questionary.Choice(
|
|
36
|
-
title="Claude Pro or Max Subscription",
|
|
37
|
-
value=AuthMethod.CLAUDE_PRO,
|
|
38
|
-
description="This does not require creating an API Key, but requires a subscription at https://claude.ai",
|
|
39
|
-
),
|
|
40
|
-
],
|
|
29
|
+
"""Configure authentication for SQLsaber (API keys and Anthropic OAuth)."""
|
|
30
|
+
console.print("\n[bold]SQLsaber Authentication Setup[/bold]\n")
|
|
31
|
+
|
|
32
|
+
provider = questionary.select(
|
|
33
|
+
"Select provider to configure:",
|
|
34
|
+
choices=providers.all_keys(),
|
|
41
35
|
).ask()
|
|
42
36
|
|
|
43
|
-
if
|
|
37
|
+
if provider is None:
|
|
44
38
|
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
45
39
|
return
|
|
46
40
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
success = oauth_flow.authenticate()
|
|
62
|
-
if success:
|
|
63
|
-
config_manager.set_auth_method(auth_choice)
|
|
41
|
+
if provider == "anthropic":
|
|
42
|
+
# Let user choose API key or OAuth
|
|
43
|
+
method_choice = questionary.select(
|
|
44
|
+
"Select Anthropic authentication method:",
|
|
45
|
+
choices=[
|
|
46
|
+
{"name": "API key", "value": AuthMethod.API_KEY},
|
|
47
|
+
{"name": "Claude Pro/Max (OAuth)", "value": AuthMethod.CLAUDE_PRO},
|
|
48
|
+
],
|
|
49
|
+
).ask()
|
|
50
|
+
|
|
51
|
+
if method_choice == AuthMethod.CLAUDE_PRO:
|
|
52
|
+
flow = AnthropicOAuthFlow()
|
|
53
|
+
if flow.authenticate():
|
|
54
|
+
config_manager.set_auth_method(AuthMethod.CLAUDE_PRO)
|
|
64
55
|
console.print(
|
|
65
|
-
"\n[bold green]
|
|
56
|
+
"\n[bold green]✓ Anthropic OAuth configured successfully![/bold green]"
|
|
66
57
|
)
|
|
67
58
|
else:
|
|
68
|
-
console.print(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
except Exception as e:
|
|
73
|
-
console.print(f"\n[red]Authentication setup failed: {str(e)}[/red]")
|
|
59
|
+
console.print("\n[red]✗ Anthropic OAuth setup failed.[/red]")
|
|
60
|
+
console.print(
|
|
61
|
+
"You can change this anytime by running [cyan]saber auth setup[/cyan] again."
|
|
62
|
+
)
|
|
74
63
|
return
|
|
75
64
|
|
|
65
|
+
# API key flow (all providers + Anthropic when selected above)
|
|
66
|
+
api_key_manager = APIKeyManager()
|
|
67
|
+
env_var = api_key_manager._get_env_var_name(provider)
|
|
68
|
+
console.print("\nTo configure your API key, you can either:")
|
|
69
|
+
console.print(f"• Set the {env_var} environment variable")
|
|
70
|
+
console.print("• Let SQLsaber prompt you for the key when needed (stored securely)")
|
|
71
|
+
|
|
72
|
+
# Fetch/store key (cascades env -> keyring -> prompt)
|
|
73
|
+
api_key = api_key_manager.get_api_key(provider)
|
|
74
|
+
if api_key:
|
|
75
|
+
config_manager.set_auth_method(AuthMethod.API_KEY)
|
|
76
|
+
console.print(
|
|
77
|
+
f"\n[bold green]✓ {provider.title()} API key configured successfully![/bold green]"
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
console.print("\n[yellow]No API key configured.[/yellow]")
|
|
81
|
+
|
|
76
82
|
console.print(
|
|
77
83
|
"You can change this anytime by running [cyan]saber auth setup[/cyan] again."
|
|
78
84
|
)
|
|
@@ -80,7 +86,7 @@ def setup():
|
|
|
80
86
|
|
|
81
87
|
@auth_app.command
|
|
82
88
|
def status():
|
|
83
|
-
"""Show current authentication configuration."""
|
|
89
|
+
"""Show current authentication configuration and provider key status."""
|
|
84
90
|
auth_method = config_manager.get_auth_method()
|
|
85
91
|
|
|
86
92
|
console.print("\n[bold blue]Authentication Status[/bold blue]")
|
|
@@ -88,52 +94,113 @@ def status():
|
|
|
88
94
|
if auth_method is None:
|
|
89
95
|
console.print("[yellow]No authentication method configured[/yellow]")
|
|
90
96
|
console.print("Run [cyan]saber auth setup[/cyan] to configure authentication.")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Show configured method summary
|
|
100
|
+
if auth_method == AuthMethod.CLAUDE_PRO:
|
|
101
|
+
console.print("[green]✓ Anthropic Claude Pro/Max (OAuth) configured[/green]\n")
|
|
91
102
|
else:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
console.print("[green]✓ API Key authentication configured[/green]\n")
|
|
104
|
+
|
|
105
|
+
# Show per-provider status without prompting
|
|
106
|
+
api_key_manager = APIKeyManager()
|
|
107
|
+
for provider in providers.all_keys():
|
|
108
|
+
if provider == "anthropic":
|
|
109
|
+
# Include OAuth status
|
|
110
|
+
if OAuthTokenManager().has_oauth_token("anthropic"):
|
|
111
|
+
console.print("> anthropic (oauth): [green]configured[/green]")
|
|
112
|
+
env_var = api_key_manager._get_env_var_name(provider)
|
|
113
|
+
service = api_key_manager._get_service_name(provider)
|
|
114
|
+
from_env = bool(os.getenv(env_var))
|
|
115
|
+
from_keyring = bool(keyring.get_password(service, provider))
|
|
116
|
+
if from_env:
|
|
117
|
+
console.print(f"> {provider}: configured via {env_var}")
|
|
118
|
+
elif from_keyring:
|
|
119
|
+
console.print(f"> {provider}: [green]configured[/green]")
|
|
120
|
+
else:
|
|
121
|
+
console.print(f"> {provider}: [yellow]not configured[/yellow]")
|
|
104
122
|
|
|
105
123
|
|
|
106
124
|
@auth_app.command
|
|
107
125
|
def reset():
|
|
108
|
-
"""Reset
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
"""Reset credentials for a selected provider (API key and/or OAuth)."""
|
|
127
|
+
console.print("\n[bold]SQLsaber Authentication Reset[/bold]\n")
|
|
128
|
+
|
|
129
|
+
# Choose provider to reset (mirrors setup)
|
|
130
|
+
provider = questionary.select(
|
|
131
|
+
"Select provider to reset:",
|
|
132
|
+
choices=providers.all_keys(),
|
|
133
|
+
).ask()
|
|
134
|
+
|
|
135
|
+
if provider is None:
|
|
136
|
+
console.print("[yellow]Reset cancelled.[/yellow]")
|
|
111
137
|
return
|
|
112
138
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
api_key_manager = APIKeyManager()
|
|
140
|
+
service = api_key_manager._get_service_name(provider)
|
|
141
|
+
|
|
142
|
+
# Determine what exists in keyring
|
|
143
|
+
api_key_present = bool(keyring.get_password(service, provider))
|
|
144
|
+
oauth_present = (
|
|
145
|
+
OAuthTokenManager().has_oauth_token("anthropic")
|
|
146
|
+
if provider == "anthropic"
|
|
147
|
+
else False
|
|
116
148
|
)
|
|
117
149
|
|
|
118
|
-
if
|
|
119
|
-
f"Are you sure you want to reset the current authentication method ({method_name})?",
|
|
120
|
-
default=False,
|
|
121
|
-
).ask():
|
|
122
|
-
# If Claude Pro, also remove OAuth tokens
|
|
123
|
-
if current_method == AuthMethod.CLAUDE_PRO:
|
|
124
|
-
oauth_flow = AnthropicOAuthFlow()
|
|
125
|
-
oauth_flow.remove_authentication()
|
|
126
|
-
|
|
127
|
-
# Clear the auth config by setting it to None
|
|
128
|
-
config = config_manager._load_config()
|
|
129
|
-
config["auth_method"] = None
|
|
130
|
-
config_manager._save_config(config)
|
|
131
|
-
console.print("[green]Authentication configuration reset.[/green]")
|
|
150
|
+
if not api_key_present and not oauth_present:
|
|
132
151
|
console.print(
|
|
133
|
-
"
|
|
152
|
+
f"[yellow]No stored credentials found for {provider}. Nothing to reset.[/yellow]"
|
|
134
153
|
)
|
|
135
|
-
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Build confirmation message
|
|
157
|
+
to_remove: list[str] = []
|
|
158
|
+
if oauth_present:
|
|
159
|
+
to_remove.append("Anthropic OAuth token")
|
|
160
|
+
if api_key_present:
|
|
161
|
+
to_remove.append(f"{provider.title()} API key")
|
|
162
|
+
|
|
163
|
+
summary = ", ".join(to_remove)
|
|
164
|
+
confirmed = questionary.confirm(
|
|
165
|
+
f"Remove the following for {provider}: {summary}?",
|
|
166
|
+
default=False,
|
|
167
|
+
).ask()
|
|
168
|
+
|
|
169
|
+
if not confirmed:
|
|
136
170
|
console.print("Reset cancelled.")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Perform deletions
|
|
174
|
+
if oauth_present:
|
|
175
|
+
OAuthTokenManager().remove_oauth_token("anthropic")
|
|
176
|
+
if api_key_present:
|
|
177
|
+
try:
|
|
178
|
+
keyring.delete_password(service, provider)
|
|
179
|
+
console.print(f"Removed {provider} API key from keyring", style="green")
|
|
180
|
+
except keyring.errors.PasswordDeleteError:
|
|
181
|
+
# Already absent; treat as success
|
|
182
|
+
pass
|
|
183
|
+
except Exception as e:
|
|
184
|
+
console.print(f"Warning: Could not remove API key: {e}", style="yellow")
|
|
185
|
+
|
|
186
|
+
# Optionally clear global auth method if removing Anthropic OAuth configuration
|
|
187
|
+
if provider == "anthropic" and oauth_present:
|
|
188
|
+
current_method = config_manager.get_auth_method()
|
|
189
|
+
if current_method == AuthMethod.CLAUDE_PRO:
|
|
190
|
+
also_clear = questionary.confirm(
|
|
191
|
+
"Anthropic OAuth was removed. Also unset the global auth method?",
|
|
192
|
+
default=False,
|
|
193
|
+
).ask()
|
|
194
|
+
if also_clear:
|
|
195
|
+
config = config_manager._load_config()
|
|
196
|
+
config["auth_method"] = None
|
|
197
|
+
config_manager._save_config(config)
|
|
198
|
+
console.print("Global auth method unset.", style="green")
|
|
199
|
+
|
|
200
|
+
console.print("\n[bold green]✓ Reset complete.[/bold green]")
|
|
201
|
+
console.print(
|
|
202
|
+
"Environment variables are not modified by this command.", style="dim"
|
|
203
|
+
)
|
|
137
204
|
|
|
138
205
|
|
|
139
206
|
def create_auth_app() -> cyclopts.App:
|
sqlsaber/cli/commands.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import Annotated
|
|
|
7
7
|
import cyclopts
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
10
|
-
from sqlsaber.agents
|
|
10
|
+
from sqlsaber.agents import build_sqlsaber_agent
|
|
11
11
|
from sqlsaber.cli.auth import create_auth_app
|
|
12
12
|
from sqlsaber.cli.database import create_db_app
|
|
13
13
|
from sqlsaber.cli.interactive import InteractiveSession
|
|
@@ -15,7 +15,13 @@ from sqlsaber.cli.memory import create_memory_app
|
|
|
15
15
|
from sqlsaber.cli.models import create_models_app
|
|
16
16
|
from sqlsaber.cli.streaming import StreamingQueryHandler
|
|
17
17
|
from sqlsaber.config.database import DatabaseConfigManager
|
|
18
|
-
from sqlsaber.database.connection import
|
|
18
|
+
from sqlsaber.database.connection import (
|
|
19
|
+
CSVConnection,
|
|
20
|
+
DatabaseConnection,
|
|
21
|
+
MySQLConnection,
|
|
22
|
+
PostgreSQLConnection,
|
|
23
|
+
SQLiteConnection,
|
|
24
|
+
)
|
|
19
25
|
from sqlsaber.database.resolver import DatabaseResolutionError, resolve_database
|
|
20
26
|
|
|
21
27
|
|
|
@@ -123,25 +129,34 @@ def query(
|
|
|
123
129
|
except Exception as e:
|
|
124
130
|
raise CLIError(f"Error creating database connection: {e}")
|
|
125
131
|
|
|
126
|
-
# Create agent instance with database name for memory context
|
|
127
|
-
agent =
|
|
132
|
+
# Create pydantic-ai agent instance with database name for memory context
|
|
133
|
+
agent = build_sqlsaber_agent(db_conn, db_name)
|
|
128
134
|
|
|
129
135
|
try:
|
|
130
136
|
if actual_query:
|
|
131
137
|
# Single query mode with streaming
|
|
132
138
|
streaming_handler = StreamingQueryHandler(console)
|
|
139
|
+
# Compute DB type for the greeting line
|
|
140
|
+
db_type = (
|
|
141
|
+
"PostgreSQL"
|
|
142
|
+
if isinstance(db_conn, PostgreSQLConnection)
|
|
143
|
+
else "MySQL"
|
|
144
|
+
if isinstance(db_conn, MySQLConnection)
|
|
145
|
+
else "SQLite"
|
|
146
|
+
if isinstance(db_conn, (SQLiteConnection, CSVConnection))
|
|
147
|
+
else "database"
|
|
148
|
+
)
|
|
133
149
|
console.print(
|
|
134
|
-
f"[bold blue]Connected to:[/bold blue] {db_name} {
|
|
150
|
+
f"[bold blue]Connected to:[/bold blue] {db_name} ({db_type})\n"
|
|
135
151
|
)
|
|
136
152
|
await streaming_handler.execute_streaming_query(actual_query, agent)
|
|
137
153
|
else:
|
|
138
154
|
# Interactive mode
|
|
139
|
-
session = InteractiveSession(console, agent)
|
|
155
|
+
session = InteractiveSession(console, agent, db_conn, db_name)
|
|
140
156
|
await session.run()
|
|
141
157
|
|
|
142
158
|
finally:
|
|
143
159
|
# Clean up
|
|
144
|
-
await agent.close() # Close the agent's HTTP client
|
|
145
160
|
await db_conn.close()
|
|
146
161
|
console.print("\n[green]Goodbye![/green]")
|
|
147
162
|
|
sqlsaber/cli/database.py
CHANGED
sqlsaber/cli/interactive.py
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
|
|
5
5
|
import questionary
|
|
6
|
+
from pydantic_ai import Agent
|
|
6
7
|
from rich.console import Console
|
|
7
8
|
from rich.panel import Panel
|
|
8
9
|
|
|
9
|
-
from sqlsaber.agents.base import BaseSQLAgent
|
|
10
10
|
from sqlsaber.cli.completers import (
|
|
11
11
|
CompositeCompleter,
|
|
12
12
|
SlashCommandCompleter,
|
|
@@ -14,25 +14,44 @@ from sqlsaber.cli.completers import (
|
|
|
14
14
|
)
|
|
15
15
|
from sqlsaber.cli.display import DisplayManager
|
|
16
16
|
from sqlsaber.cli.streaming import StreamingQueryHandler
|
|
17
|
+
from sqlsaber.database.schema import SchemaManager
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class InteractiveSession:
|
|
20
21
|
"""Manages interactive CLI sessions."""
|
|
21
22
|
|
|
22
|
-
def __init__(self, console: Console, agent:
|
|
23
|
+
def __init__(self, console: Console, agent: Agent, db_conn, database_name: str):
|
|
23
24
|
self.console = console
|
|
24
25
|
self.agent = agent
|
|
26
|
+
self.db_conn = db_conn
|
|
27
|
+
self.database_name = database_name
|
|
25
28
|
self.display = DisplayManager(console)
|
|
26
29
|
self.streaming_handler = StreamingQueryHandler(console)
|
|
27
30
|
self.current_task: asyncio.Task | None = None
|
|
28
31
|
self.cancellation_token: asyncio.Event | None = None
|
|
29
32
|
self.table_completer = TableNameCompleter()
|
|
33
|
+
self.message_history: list | None = []
|
|
30
34
|
|
|
31
35
|
def show_welcome_message(self):
|
|
32
36
|
"""Display welcome message for interactive mode."""
|
|
33
37
|
# Show database information
|
|
34
|
-
db_name =
|
|
35
|
-
|
|
38
|
+
db_name = self.database_name or "Unknown"
|
|
39
|
+
from sqlsaber.database.connection import (
|
|
40
|
+
CSVConnection,
|
|
41
|
+
MySQLConnection,
|
|
42
|
+
PostgreSQLConnection,
|
|
43
|
+
SQLiteConnection,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
db_type = (
|
|
47
|
+
"PostgreSQL"
|
|
48
|
+
if isinstance(self.db_conn, PostgreSQLConnection)
|
|
49
|
+
else "MySQL"
|
|
50
|
+
if isinstance(self.db_conn, MySQLConnection)
|
|
51
|
+
else "SQLite"
|
|
52
|
+
if isinstance(self.db_conn, (SQLiteConnection, CSVConnection))
|
|
53
|
+
else "database"
|
|
54
|
+
)
|
|
36
55
|
|
|
37
56
|
self.console.print(
|
|
38
57
|
Panel.fit(
|
|
@@ -44,26 +63,27 @@ class InteractiveSession:
|
|
|
44
63
|
███████ ██████ ███████ ███████ ██ ██ ██████ ███████ ██ ██
|
|
45
64
|
▀▀
|
|
46
65
|
"""
|
|
47
|
-
"\n\n"
|
|
48
|
-
"[dim]Use '/clear' to reset conversation, '/exit' or '/quit' to leave.[/dim]\n\n"
|
|
49
|
-
"[dim]Start a message with '#' to add something to agent's memory for this database.[/dim]\n\n"
|
|
50
|
-
"[dim]Type '@' to get table name completions.[/dim]",
|
|
51
|
-
border_style="green",
|
|
52
66
|
)
|
|
53
67
|
)
|
|
54
68
|
self.console.print(
|
|
55
|
-
|
|
69
|
+
"\n",
|
|
70
|
+
"[dim] ≥ Use '/clear' to reset conversation",
|
|
71
|
+
"[dim] ≥ Use '/exit' or '/quit' to leave[/dim]",
|
|
72
|
+
"[dim] ≥ Use 'Ctrl+C' to interrupt and return to prompt\n\n",
|
|
73
|
+
"[dim] ≥ Start message with '#' to add something to agent's memory for this database",
|
|
74
|
+
"[dim] ≥ Type '@' to get table name completions",
|
|
75
|
+
"[dim] ≥ Press 'Esc-Enter' or 'Meta-Enter' to submit your question",
|
|
76
|
+
sep="\n",
|
|
56
77
|
)
|
|
78
|
+
|
|
57
79
|
self.console.print(
|
|
58
|
-
"[
|
|
59
|
-
"[dim]Press Ctrl+C during query execution to interrupt and return to prompt.[/dim]\n"
|
|
80
|
+
f"[bold blue]\n\nConnected to:[/bold blue] {db_name} ({db_type})\n"
|
|
60
81
|
)
|
|
61
82
|
|
|
62
83
|
async def _update_table_cache(self):
|
|
63
84
|
"""Update the table completer cache with fresh data."""
|
|
64
85
|
try:
|
|
65
|
-
|
|
66
|
-
tables_data = await self.agent.schema_manager.list_tables()
|
|
86
|
+
tables_data = await SchemaManager(self.db_conn).list_tables()
|
|
67
87
|
|
|
68
88
|
# Parse the table information
|
|
69
89
|
table_list = []
|
|
@@ -100,16 +120,20 @@ class InteractiveSession:
|
|
|
100
120
|
# Create the query task
|
|
101
121
|
query_task = asyncio.create_task(
|
|
102
122
|
self.streaming_handler.execute_streaming_query(
|
|
103
|
-
user_query, self.agent, self.cancellation_token
|
|
123
|
+
user_query, self.agent, self.cancellation_token, self.message_history
|
|
104
124
|
)
|
|
105
125
|
)
|
|
106
126
|
self.current_task = query_task
|
|
107
127
|
|
|
108
128
|
try:
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
run_result = await query_task
|
|
130
|
+
# Persist message history from this run using pydantic-ai API
|
|
131
|
+
if run_result is not None:
|
|
132
|
+
try:
|
|
133
|
+
# Use all_messages() so the system prompt and all prior turns are preserved
|
|
134
|
+
self.message_history = run_result.all_messages()
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
113
137
|
finally:
|
|
114
138
|
self.current_task = None
|
|
115
139
|
self.cancellation_token = None
|
|
@@ -144,7 +168,8 @@ class InteractiveSession:
|
|
|
144
168
|
break
|
|
145
169
|
|
|
146
170
|
if user_query == "/clear":
|
|
147
|
-
|
|
171
|
+
# Reset local history (pydantic-ai call will receive empty history on next run)
|
|
172
|
+
self.message_history = []
|
|
148
173
|
self.console.print("[green]Conversation history cleared.[/green]\n")
|
|
149
174
|
continue
|
|
150
175
|
|
|
@@ -153,18 +178,28 @@ class InteractiveSession:
|
|
|
153
178
|
if memory_text.startswith("#"):
|
|
154
179
|
memory_content = memory_text[1:].strip() # Remove # and trim
|
|
155
180
|
if memory_content:
|
|
156
|
-
# Add memory
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
f"[green]✓ Memory added:[/green] {memory_content}"
|
|
161
|
-
)
|
|
162
|
-
self.console.print(
|
|
163
|
-
f"[dim]Memory ID: {memory_id}[/dim]\n"
|
|
181
|
+
# Add memory via the agent's memory manager
|
|
182
|
+
try:
|
|
183
|
+
mm = getattr(
|
|
184
|
+
self.agent, "_sqlsaber_memory_manager", None
|
|
164
185
|
)
|
|
165
|
-
|
|
186
|
+
if mm and self.database_name:
|
|
187
|
+
memory = mm.add_memory(
|
|
188
|
+
self.database_name, memory_content
|
|
189
|
+
)
|
|
190
|
+
self.console.print(
|
|
191
|
+
f"[green]✓ Memory added:[/green] {memory_content}"
|
|
192
|
+
)
|
|
193
|
+
self.console.print(
|
|
194
|
+
f"[dim]Memory ID: {memory.id}[/dim]\n"
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
self.console.print(
|
|
198
|
+
"[yellow]Could not add memory (no database context)[/yellow]\n"
|
|
199
|
+
)
|
|
200
|
+
except Exception:
|
|
166
201
|
self.console.print(
|
|
167
|
-
"[yellow]Could not add memory
|
|
202
|
+
"[yellow]Could not add memory[/yellow]\n"
|
|
168
203
|
)
|
|
169
204
|
else:
|
|
170
205
|
self.console.print(
|