sqlsaber 0.15.0__py3-none-any.whl → 0.16.1__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 +1 -2
- sqlsaber/agents/mcp.py +2 -2
- sqlsaber/agents/pydantic_ai_agent.py +170 -0
- sqlsaber/cli/auth.py +146 -79
- sqlsaber/cli/commands.py +23 -8
- 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/models/__init__.py +0 -3
- sqlsaber/tools/base.py +7 -5
- {sqlsaber-0.15.0.dist-info → sqlsaber-0.16.1.dist-info}/METADATA +20 -39
- {sqlsaber-0.15.0.dist-info → sqlsaber-0.16.1.dist-info}/RECORD +21 -28
- sqlsaber/agents/anthropic.py +0 -491
- 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.15.0.dist-info → sqlsaber-0.16.1.dist-info}/WHEEL +0 -0
- {sqlsaber-0.15.0.dist-info → sqlsaber-0.16.1.dist-info}/entry_points.txt +0 -0
- {sqlsaber-0.15.0.dist-info → sqlsaber-0.16.1.dist-info}/licenses/LICENSE +0 -0
sqlsaber/agents/__init__.py
CHANGED
sqlsaber/agents/base.py
CHANGED
|
@@ -14,7 +14,6 @@ from sqlsaber.database.connection import (
|
|
|
14
14
|
SQLiteConnection,
|
|
15
15
|
)
|
|
16
16
|
from sqlsaber.database.schema import SchemaManager
|
|
17
|
-
from sqlsaber.models.events import StreamEvent
|
|
18
17
|
from sqlsaber.tools import SQLTool, tool_registry
|
|
19
18
|
|
|
20
19
|
|
|
@@ -40,7 +39,7 @@ class BaseSQLAgent(ABC):
|
|
|
40
39
|
user_query: str,
|
|
41
40
|
use_history: bool = True,
|
|
42
41
|
cancellation_token: asyncio.Event | None = None,
|
|
43
|
-
) -> AsyncIterator
|
|
42
|
+
) -> AsyncIterator:
|
|
44
43
|
"""Process a user query and stream responses.
|
|
45
44
|
|
|
46
45
|
Args:
|
sqlsaber/agents/mcp.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Generic SQL agent implementation for MCP tools."""
|
|
2
2
|
|
|
3
3
|
from typing import AsyncIterator
|
|
4
|
+
|
|
4
5
|
from sqlsaber.agents.base import BaseSQLAgent
|
|
5
6
|
from sqlsaber.database.connection import BaseDatabaseConnection
|
|
6
|
-
from sqlsaber.models.events import StreamEvent
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class MCPSQLAgent(BaseSQLAgent):
|
|
@@ -14,7 +14,7 @@ class MCPSQLAgent(BaseSQLAgent):
|
|
|
14
14
|
|
|
15
15
|
async def query_stream(
|
|
16
16
|
self, user_query: str, use_history: bool = True
|
|
17
|
-
) -> AsyncIterator
|
|
17
|
+
) -> AsyncIterator:
|
|
18
18
|
"""Not implemented for generic agent as it's only used for tool operations."""
|
|
19
19
|
raise NotImplementedError(
|
|
20
20
|
"MCPSQLAgent does not support query streaming. Use specific agent implementations for conversation."
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Pydantic-AI Agent for SQLSaber.
|
|
2
|
+
|
|
3
|
+
This replaces the custom AnthropicSQLAgent and uses pydantic-ai's Agent,
|
|
4
|
+
function tools, and streaming event types directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic_ai import Agent, RunContext
|
|
9
|
+
from pydantic_ai.models.anthropic import AnthropicModel
|
|
10
|
+
from pydantic_ai.models.google import GoogleModel
|
|
11
|
+
from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
12
|
+
from pydantic_ai.providers.google import GoogleProvider
|
|
13
|
+
|
|
14
|
+
from sqlsaber.config import providers
|
|
15
|
+
from sqlsaber.config.settings import Config
|
|
16
|
+
from sqlsaber.database.connection import (
|
|
17
|
+
BaseDatabaseConnection,
|
|
18
|
+
CSVConnection,
|
|
19
|
+
MySQLConnection,
|
|
20
|
+
PostgreSQLConnection,
|
|
21
|
+
SQLiteConnection,
|
|
22
|
+
)
|
|
23
|
+
from sqlsaber.memory.manager import MemoryManager
|
|
24
|
+
from sqlsaber.tools.instructions import InstructionBuilder
|
|
25
|
+
from sqlsaber.tools.registry import tool_registry
|
|
26
|
+
from sqlsaber.tools.sql_tools import SQLTool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_sqlsaber_agent(
|
|
30
|
+
db_connection: BaseDatabaseConnection,
|
|
31
|
+
database_name: str | None,
|
|
32
|
+
) -> Agent:
|
|
33
|
+
"""Create and configure a pydantic-ai Agent for SQLSaber.
|
|
34
|
+
|
|
35
|
+
- Registers function tools that delegate to the existing tool registry
|
|
36
|
+
- Attaches dynamic system prompt built from InstructionBuilder + MemoryManager
|
|
37
|
+
- Ensures SQL tools have the active DB connection
|
|
38
|
+
"""
|
|
39
|
+
# Ensure SQL tools receive the active connection
|
|
40
|
+
for tool_name in tool_registry.list_tools(category="sql"):
|
|
41
|
+
tool = tool_registry.get_tool(tool_name)
|
|
42
|
+
if isinstance(tool, SQLTool):
|
|
43
|
+
tool.set_connection(db_connection)
|
|
44
|
+
|
|
45
|
+
cfg = Config()
|
|
46
|
+
# Ensure provider env var is hydrated from keyring for current provider (Config.validate handles it)
|
|
47
|
+
cfg.validate()
|
|
48
|
+
|
|
49
|
+
# Build model/agent. For some providers (e.g., google), construct provider model explicitly to
|
|
50
|
+
# allow arbitrary model IDs even if not in pydantic-ai's KnownModelName.
|
|
51
|
+
model_name_only = (
|
|
52
|
+
cfg.model_name.split(":", 1)[1] if ":" in cfg.model_name else cfg.model_name
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
provider = providers.provider_from_model(cfg.model_name) or ""
|
|
56
|
+
if provider == "google":
|
|
57
|
+
model_obj = GoogleModel(
|
|
58
|
+
model_name_only, provider=GoogleProvider(api_key=cfg.api_key)
|
|
59
|
+
)
|
|
60
|
+
agent = Agent(model_obj, name="sqlsaber")
|
|
61
|
+
elif provider == "anthropic" and bool(getattr(cfg, "oauth_token", None)):
|
|
62
|
+
# Build custom httpx client to inject OAuth headers for Anthropic
|
|
63
|
+
async def add_oauth_headers(request: httpx.Request) -> None: # type: ignore[override]
|
|
64
|
+
# Remove API-key header if present and add OAuth headers
|
|
65
|
+
if "x-api-key" in request.headers:
|
|
66
|
+
del request.headers["x-api-key"]
|
|
67
|
+
request.headers.update(
|
|
68
|
+
{
|
|
69
|
+
"Authorization": f"Bearer {cfg.oauth_token}",
|
|
70
|
+
"anthropic-version": "2023-06-01",
|
|
71
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
72
|
+
"User-Agent": "ClaudeCode/1.0 (Anthropic Claude Code CLI)",
|
|
73
|
+
"X-Client-Name": "claude-code",
|
|
74
|
+
"X-Client-Version": "1.0.0",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
http_client = httpx.AsyncClient(event_hooks={"request": [add_oauth_headers]})
|
|
79
|
+
provider_obj = AnthropicProvider(api_key="placeholder", http_client=http_client)
|
|
80
|
+
model_obj = AnthropicModel(model_name_only, provider=provider_obj)
|
|
81
|
+
agent = Agent(model_obj, name="sqlsaber")
|
|
82
|
+
else:
|
|
83
|
+
agent = Agent(cfg.model_name, name="sqlsaber")
|
|
84
|
+
|
|
85
|
+
# Memory + dynamic system prompt
|
|
86
|
+
memory_manager = MemoryManager()
|
|
87
|
+
instruction_builder = InstructionBuilder(tool_registry)
|
|
88
|
+
|
|
89
|
+
is_oauth = provider == "anthropic" and bool(getattr(cfg, "oauth_token", None))
|
|
90
|
+
|
|
91
|
+
if not is_oauth:
|
|
92
|
+
|
|
93
|
+
@agent.system_prompt(dynamic=True)
|
|
94
|
+
async def sqlsaber_system_prompt(ctx: RunContext) -> str:
|
|
95
|
+
db_type = _get_database_type_name(db_connection)
|
|
96
|
+
instructions = instruction_builder.build_instructions(db_type=db_type)
|
|
97
|
+
|
|
98
|
+
# Add memory context if available
|
|
99
|
+
if database_name:
|
|
100
|
+
mem = memory_manager.format_memories_for_prompt(database_name)
|
|
101
|
+
else:
|
|
102
|
+
mem = ""
|
|
103
|
+
|
|
104
|
+
parts = [p for p in (instructions, mem) if p and p.strip()]
|
|
105
|
+
return "\n\n".join(parts) if parts else ""
|
|
106
|
+
else:
|
|
107
|
+
|
|
108
|
+
@agent.system_prompt(dynamic=True)
|
|
109
|
+
async def sqlsaber_system_prompt(ctx: RunContext) -> str:
|
|
110
|
+
# Minimal system prompt in OAuth mode to match Claude Code identity
|
|
111
|
+
return "You are Claude Code, Anthropic's official CLI for Claude."
|
|
112
|
+
|
|
113
|
+
# Expose helpers and context on agent instance
|
|
114
|
+
agent._sqlsaber_memory_manager = memory_manager # type: ignore[attr-defined]
|
|
115
|
+
agent._sqlsaber_database_name = database_name # type: ignore[attr-defined]
|
|
116
|
+
agent._sqlsaber_instruction_builder = instruction_builder # type: ignore[attr-defined]
|
|
117
|
+
agent._sqlsaber_db_type = _get_database_type_name(db_connection) # type: ignore[attr-defined]
|
|
118
|
+
agent._sqlsaber_is_oauth = is_oauth # type: ignore[attr-defined]
|
|
119
|
+
|
|
120
|
+
# Tool wrappers that invoke the registered tools
|
|
121
|
+
@agent.tool(name="list_tables")
|
|
122
|
+
async def list_tables(ctx: RunContext) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Get a list of all tables in the database with row counts.
|
|
125
|
+
Use this first to discover available tables.
|
|
126
|
+
"""
|
|
127
|
+
tool = tool_registry.get_tool("list_tables")
|
|
128
|
+
return await tool.execute()
|
|
129
|
+
|
|
130
|
+
@agent.tool(name="introspect_schema")
|
|
131
|
+
async def introspect_schema(
|
|
132
|
+
ctx: RunContext, table_pattern: str | None = None
|
|
133
|
+
) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Introspect database schema to understand table structures.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
table_pattern: Optional pattern to filter tables (e.g., 'public.users', 'user%', '%order%')
|
|
139
|
+
"""
|
|
140
|
+
tool = tool_registry.get_tool("introspect_schema")
|
|
141
|
+
return await tool.execute(table_pattern=table_pattern)
|
|
142
|
+
|
|
143
|
+
@agent.tool(name="execute_sql")
|
|
144
|
+
async def execute_sql(ctx: RunContext, query: str, limit: int | None = 100) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Execute a SQL query and return the results.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
query: SQL query to execute
|
|
150
|
+
limit: Maximum number of rows to return (default: 100)
|
|
151
|
+
"""
|
|
152
|
+
tool = tool_registry.get_tool("execute_sql")
|
|
153
|
+
return await tool.execute(query=query, limit=limit)
|
|
154
|
+
|
|
155
|
+
return agent
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_database_type_name(db: BaseDatabaseConnection) -> str:
|
|
159
|
+
"""Get the human-readable database type name (mirrors BaseSQLAgent)."""
|
|
160
|
+
|
|
161
|
+
if isinstance(db, PostgreSQLConnection):
|
|
162
|
+
return "PostgreSQL"
|
|
163
|
+
elif isinstance(db, MySQLConnection):
|
|
164
|
+
return "MySQL"
|
|
165
|
+
elif isinstance(db, SQLiteConnection):
|
|
166
|
+
return "SQLite"
|
|
167
|
+
elif isinstance(db, CSVConnection):
|
|
168
|
+
return "SQLite"
|
|
169
|
+
else:
|
|
170
|
+
return "database"
|
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
|
|
|
@@ -29,7 +35,7 @@ class CLIError(Exception):
|
|
|
29
35
|
|
|
30
36
|
app = cyclopts.App(
|
|
31
37
|
name="sqlsaber",
|
|
32
|
-
help="
|
|
38
|
+
help="SQLsaber - Open-source agentic SQL assistant for your database",
|
|
33
39
|
)
|
|
34
40
|
|
|
35
41
|
|
|
@@ -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
|
|