sqlsaber 0.30.2__py3-none-any.whl → 0.32.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/cli/auth.py +15 -1
- sqlsaber/cli/commands.py +74 -0
- sqlsaber/cli/database.py +39 -0
- sqlsaber/cli/interactive.py +8 -2
- sqlsaber/cli/memory.py +26 -0
- sqlsaber/cli/models.py +19 -0
- sqlsaber/cli/streaming.py +5 -0
- sqlsaber/cli/theme.py +8 -0
- sqlsaber/cli/threads.py +17 -0
- sqlsaber/config/logging.py +196 -0
- sqlsaber/config/oauth_flow.py +22 -10
- sqlsaber/config/oauth_tokens.py +15 -6
- sqlsaber/threads/storage.py +31 -17
- {sqlsaber-0.30.2.dist-info → sqlsaber-0.32.0.dist-info}/METADATA +2 -1
- {sqlsaber-0.30.2.dist-info → sqlsaber-0.32.0.dist-info}/RECORD +18 -17
- {sqlsaber-0.30.2.dist-info → sqlsaber-0.32.0.dist-info}/WHEEL +0 -0
- {sqlsaber-0.30.2.dist-info → sqlsaber-0.32.0.dist-info}/entry_points.txt +0 -0
- {sqlsaber-0.30.2.dist-info → sqlsaber-0.32.0.dist-info}/licenses/LICENSE +0 -0
sqlsaber/cli/auth.py
CHANGED
|
@@ -11,10 +11,12 @@ from sqlsaber.config.api_keys import APIKeyManager
|
|
|
11
11
|
from sqlsaber.config.auth import AuthConfigManager, AuthMethod
|
|
12
12
|
from sqlsaber.config.oauth_tokens import OAuthTokenManager
|
|
13
13
|
from sqlsaber.theme.manager import create_console
|
|
14
|
+
from sqlsaber.config.logging import get_logger
|
|
14
15
|
|
|
15
16
|
# Global instances for CLI commands
|
|
16
17
|
console = create_console()
|
|
17
18
|
config_manager = AuthConfigManager()
|
|
19
|
+
logger = get_logger(__name__)
|
|
18
20
|
|
|
19
21
|
# Create the authentication management CLI app
|
|
20
22
|
auth_app = cyclopts.App(
|
|
@@ -46,7 +48,9 @@ def setup():
|
|
|
46
48
|
)
|
|
47
49
|
return success, provider
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
logger.info("auth.setup.start")
|
|
52
|
+
success, provider = asyncio.run(run_setup())
|
|
53
|
+
logger.info("auth.setup.complete", success=bool(success), provider=str(provider))
|
|
50
54
|
|
|
51
55
|
if not success:
|
|
52
56
|
console.print("\n[warning]No authentication configured.[/warning]")
|
|
@@ -59,6 +63,7 @@ def setup():
|
|
|
59
63
|
@auth_app.command
|
|
60
64
|
def status():
|
|
61
65
|
"""Show current authentication configuration and provider key status."""
|
|
66
|
+
logger.info("auth.status.start")
|
|
62
67
|
auth_method = config_manager.get_auth_method()
|
|
63
68
|
|
|
64
69
|
console.print("\n[bold blue]Authentication Status[/bold blue]")
|
|
@@ -68,6 +73,7 @@ def status():
|
|
|
68
73
|
console.print(
|
|
69
74
|
"Run [primary]saber auth setup[/primary] to configure authentication."
|
|
70
75
|
)
|
|
76
|
+
logger.info("auth.status.none_configured")
|
|
71
77
|
return
|
|
72
78
|
|
|
73
79
|
# Show configured method summary
|
|
@@ -93,6 +99,7 @@ def status():
|
|
|
93
99
|
console.print(f"> {provider}: [green]configured[/green]")
|
|
94
100
|
else:
|
|
95
101
|
console.print(f"> {provider}: [warning]not configured[/warning]")
|
|
102
|
+
logger.info("auth.status.complete", method=str(auth_method))
|
|
96
103
|
|
|
97
104
|
|
|
98
105
|
@auth_app.command
|
|
@@ -108,6 +115,7 @@ def reset():
|
|
|
108
115
|
|
|
109
116
|
if provider is None:
|
|
110
117
|
console.print("[warning]Reset cancelled.[/warning]")
|
|
118
|
+
logger.info("auth.reset.cancelled_no_provider")
|
|
111
119
|
return
|
|
112
120
|
|
|
113
121
|
api_key_manager = APIKeyManager()
|
|
@@ -125,6 +133,7 @@ def reset():
|
|
|
125
133
|
console.print(
|
|
126
134
|
f"[warning]No stored credentials found for {provider}. Nothing to reset.[/warning]"
|
|
127
135
|
)
|
|
136
|
+
logger.info("auth.reset.nothing_to_reset", provider=provider)
|
|
128
137
|
return
|
|
129
138
|
|
|
130
139
|
# Build confirmation message
|
|
@@ -142,6 +151,7 @@ def reset():
|
|
|
142
151
|
|
|
143
152
|
if not confirmed:
|
|
144
153
|
console.print("Reset cancelled.")
|
|
154
|
+
logger.info("auth.reset.cancelled_confirm", provider=provider)
|
|
145
155
|
return
|
|
146
156
|
|
|
147
157
|
# Perform deletions
|
|
@@ -151,11 +161,13 @@ def reset():
|
|
|
151
161
|
try:
|
|
152
162
|
keyring.delete_password(service, provider)
|
|
153
163
|
console.print(f"Removed {provider} API key from keyring", style="green")
|
|
164
|
+
logger.info("auth.reset.api_key_removed", provider=provider)
|
|
154
165
|
except keyring.errors.PasswordDeleteError:
|
|
155
166
|
# Already absent; treat as success
|
|
156
167
|
pass
|
|
157
168
|
except Exception as e:
|
|
158
169
|
console.print(f"Warning: Could not remove API key: {e}", style="warning")
|
|
170
|
+
logger.warning("auth.reset.api_key_remove_failed", provider=provider, error=str(e))
|
|
159
171
|
|
|
160
172
|
# Optionally clear global auth method if removing Anthropic OAuth configuration
|
|
161
173
|
if provider == "anthropic" and oauth_present:
|
|
@@ -170,8 +182,10 @@ def reset():
|
|
|
170
182
|
config["auth_method"] = None
|
|
171
183
|
config_manager._save_config(config)
|
|
172
184
|
console.print("Global auth method unset.", style="green")
|
|
185
|
+
logger.info("auth.reset.global_method_unset")
|
|
173
186
|
|
|
174
187
|
console.print("\n[success]✓ Reset complete.[/success]")
|
|
188
|
+
logger.info("auth.reset.complete", provider=provider)
|
|
175
189
|
console.print(
|
|
176
190
|
"Environment variables are not modified by this command.", style="dim"
|
|
177
191
|
)
|
sqlsaber/cli/commands.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""CLI command definitions and handlers."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
from typing import Annotated
|
|
6
7
|
|
|
@@ -16,6 +17,7 @@ from sqlsaber.cli.threads import create_threads_app
|
|
|
16
17
|
|
|
17
18
|
# Lazy imports - only import what's needed for CLI parsing
|
|
18
19
|
from sqlsaber.config.database import DatabaseConfigManager
|
|
20
|
+
from sqlsaber.config.logging import get_logger, setup_logging
|
|
19
21
|
from sqlsaber.theme.manager import create_console
|
|
20
22
|
|
|
21
23
|
|
|
@@ -42,6 +44,52 @@ app.command(create_threads_app(), name="threads")
|
|
|
42
44
|
console = create_console()
|
|
43
45
|
config_manager = DatabaseConfigManager()
|
|
44
46
|
|
|
47
|
+
_MLFLOW_CONFIGURED = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _maybe_configure_mlflow(log) -> bool:
|
|
51
|
+
"""Enable mlflow autologging when environment variables are present."""
|
|
52
|
+
global _MLFLOW_CONFIGURED
|
|
53
|
+
if _MLFLOW_CONFIGURED:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
tracking_uri = os.getenv("MLFLOW_URI")
|
|
57
|
+
experiment = os.getenv("MLFLOW_EXP")
|
|
58
|
+
if not tracking_uri and not experiment:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
import mlflow
|
|
63
|
+
except ModuleNotFoundError:
|
|
64
|
+
log.warning(
|
|
65
|
+
"mlflow.setup.skipped",
|
|
66
|
+
reason="mlflow package not installed",
|
|
67
|
+
uri=tracking_uri,
|
|
68
|
+
experiment=experiment,
|
|
69
|
+
)
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
mlflow.pydantic_ai.autolog()
|
|
74
|
+
except Exception:
|
|
75
|
+
log.warning("mlflow.autolog.failed", exc_info=True)
|
|
76
|
+
try:
|
|
77
|
+
if tracking_uri:
|
|
78
|
+
mlflow.set_tracking_uri(tracking_uri)
|
|
79
|
+
if experiment:
|
|
80
|
+
mlflow.set_experiment(experiment)
|
|
81
|
+
except Exception:
|
|
82
|
+
log.warning("mlflow.setup.failed", exc_info=True)
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
_MLFLOW_CONFIGURED = True
|
|
86
|
+
log.info(
|
|
87
|
+
"mlflow.setup.enabled",
|
|
88
|
+
uri=tracking_uri,
|
|
89
|
+
experiment=experiment,
|
|
90
|
+
)
|
|
91
|
+
return True
|
|
92
|
+
|
|
45
93
|
|
|
46
94
|
@app.meta.default
|
|
47
95
|
def meta_handler(
|
|
@@ -111,6 +159,14 @@ def query(
|
|
|
111
159
|
"""
|
|
112
160
|
|
|
113
161
|
async def run_session():
|
|
162
|
+
log = get_logger(__name__)
|
|
163
|
+
log.info(
|
|
164
|
+
"cli.session.start",
|
|
165
|
+
argv=sys.argv[1:],
|
|
166
|
+
database=database,
|
|
167
|
+
has_query=query_text is not None,
|
|
168
|
+
thinking=thinking,
|
|
169
|
+
)
|
|
114
170
|
# Import heavy dependencies only when actually running a query
|
|
115
171
|
# This is only done to speed up startup time
|
|
116
172
|
from sqlsaber.agents import SQLSaberAgent
|
|
@@ -134,29 +190,39 @@ def query(
|
|
|
134
190
|
# Check if onboarding is needed (only for interactive mode or when no database is configured)
|
|
135
191
|
if needs_onboarding(database):
|
|
136
192
|
# Run onboarding flow
|
|
193
|
+
log.debug("cli.onboarding.start")
|
|
137
194
|
onboarding_success = await run_onboarding()
|
|
138
195
|
if not onboarding_success:
|
|
139
196
|
# User cancelled or onboarding failed
|
|
140
197
|
raise CLIError(
|
|
141
198
|
"Setup incomplete. Please configure your database and try again."
|
|
142
199
|
)
|
|
200
|
+
log.info("cli.onboarding.complete", success=True)
|
|
143
201
|
|
|
144
202
|
# Resolve database from CLI input
|
|
145
203
|
try:
|
|
146
204
|
resolved = resolve_database(database, config_manager)
|
|
147
205
|
connection_string = resolved.connection_string
|
|
148
206
|
db_name = resolved.name
|
|
207
|
+
log.info(
|
|
208
|
+
"db.resolve.success",
|
|
209
|
+
name=db_name,
|
|
210
|
+
)
|
|
149
211
|
except DatabaseResolutionError as e:
|
|
212
|
+
log.error("db.resolve.error", error=str(e))
|
|
150
213
|
raise CLIError(str(e))
|
|
151
214
|
|
|
152
215
|
# Create database connection
|
|
153
216
|
try:
|
|
154
217
|
db_conn = DatabaseConnection(connection_string)
|
|
218
|
+
log.info("db.connection.created", db_type=type(db_conn).__name__)
|
|
155
219
|
except Exception as e:
|
|
220
|
+
log.exception("db.connection.error", error=str(e))
|
|
156
221
|
raise CLIError(f"Error creating database connection: {e}")
|
|
157
222
|
|
|
158
223
|
# Create pydantic-ai agent instance with database name for memory context
|
|
159
224
|
sqlsaber_agent = SQLSaberAgent(db_conn, db_name, thinking_enabled=thinking)
|
|
225
|
+
_maybe_configure_mlflow(log)
|
|
160
226
|
|
|
161
227
|
try:
|
|
162
228
|
if actual_query:
|
|
@@ -166,9 +232,11 @@ def query(
|
|
|
166
232
|
console.print(
|
|
167
233
|
f"[primary]Connected to:[/primary] {db_name} ({db_type})\n"
|
|
168
234
|
)
|
|
235
|
+
log.info("query.execute.start", db_name=db_name, db_type=db_type)
|
|
169
236
|
run = await streaming_handler.execute_streaming_query(
|
|
170
237
|
actual_query, sqlsaber_agent
|
|
171
238
|
)
|
|
239
|
+
|
|
172
240
|
# Persist non-interactive run as a thread snapshot so it can be resumed later
|
|
173
241
|
try:
|
|
174
242
|
if run is not None:
|
|
@@ -187,8 +255,10 @@ def query(
|
|
|
187
255
|
console.print(
|
|
188
256
|
f"[dim]You can continue this thread using:[/dim] saber threads resume {thread_id}"
|
|
189
257
|
)
|
|
258
|
+
log.info("thread.save.success", thread_id=thread_id)
|
|
190
259
|
except Exception:
|
|
191
260
|
# best-effort persistence; don't fail the CLI on storage errors
|
|
261
|
+
log.warning("thread.save.failed", exc_info=True)
|
|
192
262
|
pass
|
|
193
263
|
finally:
|
|
194
264
|
await threads.prune_threads()
|
|
@@ -200,16 +270,20 @@ def query(
|
|
|
200
270
|
finally:
|
|
201
271
|
# Clean up
|
|
202
272
|
await db_conn.close()
|
|
273
|
+
log.info("db.connection.closed")
|
|
203
274
|
console.print("\n[success]Goodbye![/success]")
|
|
204
275
|
|
|
205
276
|
# Run the async function with proper error handling
|
|
206
277
|
try:
|
|
207
278
|
asyncio.run(run_session())
|
|
208
279
|
except CLIError as e:
|
|
280
|
+
get_logger(__name__).error("cli.error", error=str(e))
|
|
209
281
|
console.print(f"[error]Error:[/error] {e}")
|
|
210
282
|
sys.exit(e.exit_code)
|
|
211
283
|
|
|
212
284
|
|
|
213
285
|
def main():
|
|
214
286
|
"""Entry point for the CLI application."""
|
|
287
|
+
setup_logging()
|
|
288
|
+
get_logger(__name__).info("cli.start")
|
|
215
289
|
app()
|
sqlsaber/cli/database.py
CHANGED
|
@@ -11,11 +11,13 @@ import questionary
|
|
|
11
11
|
from rich.table import Table
|
|
12
12
|
|
|
13
13
|
from sqlsaber.config.database import DatabaseConfig, DatabaseConfigManager
|
|
14
|
+
from sqlsaber.config.logging import get_logger
|
|
14
15
|
from sqlsaber.theme.manager import create_console
|
|
15
16
|
|
|
16
17
|
# Global instances for CLI commands
|
|
17
18
|
console = create_console()
|
|
18
19
|
config_manager = DatabaseConfigManager()
|
|
20
|
+
logger = get_logger(__name__)
|
|
19
21
|
|
|
20
22
|
# Create the database management CLI app
|
|
21
23
|
db_app = cyclopts.App(
|
|
@@ -78,6 +80,13 @@ def add(
|
|
|
78
80
|
] = True,
|
|
79
81
|
):
|
|
80
82
|
"""Add a new database connection."""
|
|
83
|
+
logger.info(
|
|
84
|
+
"db.add.start",
|
|
85
|
+
name=name,
|
|
86
|
+
type=type,
|
|
87
|
+
interactive=bool(interactive),
|
|
88
|
+
has_password=False,
|
|
89
|
+
)
|
|
81
90
|
|
|
82
91
|
if interactive:
|
|
83
92
|
# Interactive mode - prompt for all required fields
|
|
@@ -96,6 +105,7 @@ def add(
|
|
|
96
105
|
|
|
97
106
|
if db_input is None:
|
|
98
107
|
console.print("[warning]Operation cancelled[/warning]")
|
|
108
|
+
logger.info("db.add.cancelled")
|
|
99
109
|
return
|
|
100
110
|
|
|
101
111
|
# Extract values from db_input
|
|
@@ -116,6 +126,7 @@ def add(
|
|
|
116
126
|
console.print(
|
|
117
127
|
"[bold error]Error:[/bold error] Database file path is required for SQLite"
|
|
118
128
|
)
|
|
129
|
+
logger.error("db.add.missing_path", db_type="sqlite")
|
|
119
130
|
sys.exit(1)
|
|
120
131
|
host = "localhost"
|
|
121
132
|
port = 0
|
|
@@ -126,6 +137,7 @@ def add(
|
|
|
126
137
|
console.print(
|
|
127
138
|
"[bold error]Error:[/bold error] Database file path is required for DuckDB"
|
|
128
139
|
)
|
|
140
|
+
logger.error("db.add.missing_path", db_type="duckdb")
|
|
129
141
|
sys.exit(1)
|
|
130
142
|
database = str(Path(database).expanduser().resolve())
|
|
131
143
|
host = "localhost"
|
|
@@ -137,6 +149,7 @@ def add(
|
|
|
137
149
|
console.print(
|
|
138
150
|
"[bold error]Error:[/bold error] Host, database, and username are required"
|
|
139
151
|
)
|
|
152
|
+
logger.error("db.add.missing_fields")
|
|
140
153
|
sys.exit(1)
|
|
141
154
|
|
|
142
155
|
if port is None:
|
|
@@ -173,12 +186,15 @@ def add(
|
|
|
173
186
|
# Add the configuration
|
|
174
187
|
config_manager.add_database(db_config, password if password else None)
|
|
175
188
|
console.print(f"[green]Successfully added database connection '{name}'[/green]")
|
|
189
|
+
logger.info("db.add.success", name=name, type=type)
|
|
176
190
|
|
|
177
191
|
# Set as default if it's the first one
|
|
178
192
|
if len(config_manager.list_databases()) == 1:
|
|
179
193
|
console.print(f"[blue]Set '{name}' as default database[/blue]")
|
|
194
|
+
logger.info("db.default.set", name=name)
|
|
180
195
|
|
|
181
196
|
except Exception as e:
|
|
197
|
+
logger.exception("db.add.error", name=name, error=str(e))
|
|
182
198
|
console.print(f"[bold error]Error adding database:[/bold error] {e}")
|
|
183
199
|
sys.exit(1)
|
|
184
200
|
|
|
@@ -186,12 +202,14 @@ def add(
|
|
|
186
202
|
@db_app.command
|
|
187
203
|
def list():
|
|
188
204
|
"""List all configured database connections."""
|
|
205
|
+
logger.info("db.list.start")
|
|
189
206
|
databases = config_manager.list_databases()
|
|
190
207
|
default_name = config_manager.get_default_name()
|
|
191
208
|
|
|
192
209
|
if not databases:
|
|
193
210
|
console.print("[warning]No database connections configured[/warning]")
|
|
194
211
|
console.print("Use 'sqlsaber db add <name>' to add a database connection")
|
|
212
|
+
logger.info("db.list.empty")
|
|
195
213
|
return
|
|
196
214
|
|
|
197
215
|
table = Table(title="Database Connections")
|
|
@@ -228,6 +246,7 @@ def list():
|
|
|
228
246
|
)
|
|
229
247
|
|
|
230
248
|
console.print(table)
|
|
249
|
+
logger.info("db.list.complete", count=len(databases))
|
|
231
250
|
|
|
232
251
|
|
|
233
252
|
@db_app.command
|
|
@@ -237,10 +256,12 @@ def remove(
|
|
|
237
256
|
],
|
|
238
257
|
):
|
|
239
258
|
"""Remove a database connection."""
|
|
259
|
+
logger.info("db.remove.start", name=name)
|
|
240
260
|
if not config_manager.get_database(name):
|
|
241
261
|
console.print(
|
|
242
262
|
f"[bold error]Error:[/bold error] Database connection '{name}' not found"
|
|
243
263
|
)
|
|
264
|
+
logger.error("db.remove.not_found", name=name)
|
|
244
265
|
sys.exit(1)
|
|
245
266
|
|
|
246
267
|
if questionary.confirm(
|
|
@@ -250,13 +271,16 @@ def remove(
|
|
|
250
271
|
console.print(
|
|
251
272
|
f"[green]Successfully removed database connection '{name}'[/green]"
|
|
252
273
|
)
|
|
274
|
+
logger.info("db.remove.success", name=name)
|
|
253
275
|
else:
|
|
254
276
|
console.print(
|
|
255
277
|
f"[bold error]Error:[/bold error] Failed to remove database connection '{name}'"
|
|
256
278
|
)
|
|
279
|
+
logger.error("db.remove.failed", name=name)
|
|
257
280
|
sys.exit(1)
|
|
258
281
|
else:
|
|
259
282
|
console.print("Operation cancelled")
|
|
283
|
+
logger.info("db.remove.cancelled", name=name)
|
|
260
284
|
|
|
261
285
|
|
|
262
286
|
@db_app.command
|
|
@@ -267,18 +291,22 @@ def set_default(
|
|
|
267
291
|
],
|
|
268
292
|
):
|
|
269
293
|
"""Set the default database connection."""
|
|
294
|
+
logger.info("db.default.start", name=name)
|
|
270
295
|
if not config_manager.get_database(name):
|
|
271
296
|
console.print(
|
|
272
297
|
f"[bold error]Error:[/bold error] Database connection '{name}' not found"
|
|
273
298
|
)
|
|
299
|
+
logger.error("db.default.not_found", name=name)
|
|
274
300
|
sys.exit(1)
|
|
275
301
|
|
|
276
302
|
if config_manager.set_default_database(name):
|
|
277
303
|
console.print(f"[green]Successfully set '{name}' as default database[/green]")
|
|
304
|
+
logger.info("db.default.success", name=name)
|
|
278
305
|
else:
|
|
279
306
|
console.print(
|
|
280
307
|
f"[bold error]Error:[/bold error] Failed to set '{name}' as default"
|
|
281
308
|
)
|
|
309
|
+
logger.error("db.default.failed", name=name)
|
|
282
310
|
sys.exit(1)
|
|
283
311
|
|
|
284
312
|
|
|
@@ -292,6 +320,7 @@ def test(
|
|
|
292
320
|
] = None,
|
|
293
321
|
):
|
|
294
322
|
"""Test a database connection."""
|
|
323
|
+
logger.info("db.test.start")
|
|
295
324
|
|
|
296
325
|
async def test_connection():
|
|
297
326
|
# Lazy import to keep CLI startup fast
|
|
@@ -303,6 +332,7 @@ def test(
|
|
|
303
332
|
console.print(
|
|
304
333
|
f"[bold error]Error:[/bold error] Database connection '{name}' not found"
|
|
305
334
|
)
|
|
335
|
+
logger.error("db.test.not_found", name=name)
|
|
306
336
|
sys.exit(1)
|
|
307
337
|
else:
|
|
308
338
|
db_config = config_manager.get_default_database()
|
|
@@ -313,6 +343,7 @@ def test(
|
|
|
313
343
|
console.print(
|
|
314
344
|
"Use 'sqlsaber db add <name>' to add a database connection"
|
|
315
345
|
)
|
|
346
|
+
logger.error("db.test.no_default")
|
|
316
347
|
sys.exit(1)
|
|
317
348
|
|
|
318
349
|
console.print(f"[blue]Testing connection to '{db_config.name}'...[/blue]")
|
|
@@ -328,8 +359,16 @@ def test(
|
|
|
328
359
|
console.print(
|
|
329
360
|
f"[green]✓ Connection to '{db_config.name}' successful[/green]"
|
|
330
361
|
)
|
|
362
|
+
logger.info("db.test.success", name=db_config.name)
|
|
331
363
|
|
|
332
364
|
except Exception as e:
|
|
365
|
+
logger.exception(
|
|
366
|
+
"db.test.failed",
|
|
367
|
+
name=(
|
|
368
|
+
db_config.name if "db_config" in locals() and db_config else name
|
|
369
|
+
),
|
|
370
|
+
error=str(e),
|
|
371
|
+
)
|
|
333
372
|
console.print(f"[bold error]✗ Connection failed:[/bold error] {e}")
|
|
334
373
|
sys.exit(1)
|
|
335
374
|
|
sqlsaber/cli/interactive.py
CHANGED
|
@@ -30,6 +30,7 @@ from sqlsaber.database import (
|
|
|
30
30
|
from sqlsaber.database.schema import SchemaManager
|
|
31
31
|
from sqlsaber.theme.manager import get_theme_manager
|
|
32
32
|
from sqlsaber.threads import ThreadStorage
|
|
33
|
+
from sqlsaber.config.logging import get_logger
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from sqlsaber.agents.pydantic_ai_agent import SQLSaberAgent
|
|
@@ -66,6 +67,7 @@ class InteractiveSession:
|
|
|
66
67
|
self._threads = ThreadStorage()
|
|
67
68
|
self._thread_id: str | None = initial_thread_id
|
|
68
69
|
self.first_message = not self._thread_id
|
|
70
|
+
self.log = get_logger(__name__)
|
|
69
71
|
|
|
70
72
|
def _history_path(self) -> Path:
|
|
71
73
|
"""Get the history file path, ensuring directory exists."""
|
|
@@ -240,6 +242,7 @@ class InteractiveSession:
|
|
|
240
242
|
|
|
241
243
|
async def _execute_query_with_cancellation(self, user_query: str):
|
|
242
244
|
"""Execute a query with cancellation support."""
|
|
245
|
+
self.log.info("interactive.query.start", database=self.database_name)
|
|
243
246
|
# Create cancellation token
|
|
244
247
|
self.cancellation_token = asyncio.Event()
|
|
245
248
|
|
|
@@ -276,16 +279,18 @@ class InteractiveSession:
|
|
|
276
279
|
model_name=self.sqlsaber_agent.agent.model.model_name,
|
|
277
280
|
)
|
|
278
281
|
self.first_message = False
|
|
279
|
-
except Exception:
|
|
280
|
-
|
|
282
|
+
except Exception as e:
|
|
283
|
+
self.log.warning("interactive.thread.save_failed", error=str(e))
|
|
281
284
|
finally:
|
|
282
285
|
await self._threads.prune_threads()
|
|
283
286
|
finally:
|
|
284
287
|
self.current_task = None
|
|
285
288
|
self.cancellation_token = None
|
|
289
|
+
self.log.info("interactive.query.end")
|
|
286
290
|
|
|
287
291
|
async def run(self):
|
|
288
292
|
"""Run the interactive session loop."""
|
|
293
|
+
self.log.info("interactive.start", database=self.database_name)
|
|
289
294
|
self.show_welcome_message()
|
|
290
295
|
await self.before_prompt_loop()
|
|
291
296
|
|
|
@@ -348,3 +353,4 @@ class InteractiveSession:
|
|
|
348
353
|
break
|
|
349
354
|
except Exception as exc:
|
|
350
355
|
self.console.print(f"[error]Error:[/error] {exc}")
|
|
356
|
+
self.log.exception("interactive.error", error=str(exc))
|
sqlsaber/cli/memory.py
CHANGED
|
@@ -8,6 +8,7 @@ import questionary
|
|
|
8
8
|
from rich.table import Table
|
|
9
9
|
|
|
10
10
|
from sqlsaber.config.database import DatabaseConfigManager
|
|
11
|
+
from sqlsaber.config.logging import get_logger
|
|
11
12
|
from sqlsaber.memory.manager import MemoryManager
|
|
12
13
|
from sqlsaber.theme.manager import create_console
|
|
13
14
|
|
|
@@ -15,6 +16,7 @@ from sqlsaber.theme.manager import create_console
|
|
|
15
16
|
console = create_console()
|
|
16
17
|
config_manager = DatabaseConfigManager()
|
|
17
18
|
memory_manager = MemoryManager()
|
|
19
|
+
logger = get_logger(__name__)
|
|
18
20
|
|
|
19
21
|
# Create the memory management CLI app
|
|
20
22
|
memory_app = cyclopts.App(
|
|
@@ -31,6 +33,7 @@ def _get_database_name(database: str | None = None) -> str:
|
|
|
31
33
|
console.print(
|
|
32
34
|
f"[bold error]Error:[/bold error] Database connection '{database}' not found."
|
|
33
35
|
)
|
|
36
|
+
logger.error("memory.db.not_found", database=database)
|
|
34
37
|
sys.exit(1)
|
|
35
38
|
return database
|
|
36
39
|
else:
|
|
@@ -40,6 +43,7 @@ def _get_database_name(database: str | None = None) -> str:
|
|
|
40
43
|
"[bold error]Error:[/bold error] No database connections configured."
|
|
41
44
|
)
|
|
42
45
|
console.print("Use 'sqlsaber db add <name>' to add a database connection.")
|
|
46
|
+
logger.error("memory.db.none_configured")
|
|
43
47
|
sys.exit(1)
|
|
44
48
|
return db_config.name
|
|
45
49
|
|
|
@@ -57,14 +61,17 @@ def add(
|
|
|
57
61
|
):
|
|
58
62
|
"""Add a new memory for the specified database."""
|
|
59
63
|
database_name = _get_database_name(database)
|
|
64
|
+
logger.info("memory.add.start", database=database_name)
|
|
60
65
|
|
|
61
66
|
try:
|
|
62
67
|
memory = memory_manager.add_memory(database_name, content)
|
|
63
68
|
console.print(f"[green]✓ Memory added for database '{database_name}'[/green]")
|
|
64
69
|
console.print(f"[dim]Memory ID:[/dim] {memory.id}")
|
|
65
70
|
console.print(f"[dim]Content:[/dim] {memory.content}")
|
|
71
|
+
logger.info("memory.add.success", database=database_name, id=memory.id)
|
|
66
72
|
except Exception as e:
|
|
67
73
|
console.print(f"[bold error]Error adding memory:[/bold error] {e}")
|
|
74
|
+
logger.exception("memory.add.error", database=database_name, error=str(e))
|
|
68
75
|
sys.exit(1)
|
|
69
76
|
|
|
70
77
|
|
|
@@ -80,6 +87,7 @@ def list(
|
|
|
80
87
|
):
|
|
81
88
|
"""List all memories for the specified database."""
|
|
82
89
|
database_name = _get_database_name(database)
|
|
90
|
+
logger.info("memory.list.start", database=database_name)
|
|
83
91
|
|
|
84
92
|
memories = memory_manager.get_memories(database_name)
|
|
85
93
|
|
|
@@ -88,6 +96,7 @@ def list(
|
|
|
88
96
|
f"[warning]No memories found for database '{database_name}'[/warning]"
|
|
89
97
|
)
|
|
90
98
|
console.print("Use 'sqlsaber memory add \"<content>\"' to add memories")
|
|
99
|
+
logger.info("memory.list.empty", database=database_name)
|
|
91
100
|
return
|
|
92
101
|
|
|
93
102
|
table = Table(title=f"Memories for Database: {database_name}")
|
|
@@ -105,6 +114,7 @@ def list(
|
|
|
105
114
|
|
|
106
115
|
console.print(table)
|
|
107
116
|
console.print(f"\n[dim]Total memories: {len(memories)}[/dim]")
|
|
117
|
+
logger.info("memory.list.complete", database=database_name, count=len(memories))
|
|
108
118
|
|
|
109
119
|
|
|
110
120
|
@memory_app.command
|
|
@@ -120,6 +130,7 @@ def show(
|
|
|
120
130
|
):
|
|
121
131
|
"""Show the full content of a specific memory."""
|
|
122
132
|
database_name = _get_database_name(database)
|
|
133
|
+
logger.info("memory.show.start", database=database_name, id=memory_id)
|
|
123
134
|
|
|
124
135
|
memory = memory_manager.get_memory_by_id(database_name, memory_id)
|
|
125
136
|
|
|
@@ -127,6 +138,7 @@ def show(
|
|
|
127
138
|
console.print(
|
|
128
139
|
f"[bold error]Error:[/bold error] Memory with ID '{memory_id}' not found for database '{database_name}'"
|
|
129
140
|
)
|
|
141
|
+
logger.error("memory.show.not_found", database=database_name, id=memory_id)
|
|
130
142
|
sys.exit(1)
|
|
131
143
|
|
|
132
144
|
console.print(f"[bold]Memory ID:[/bold] {memory.id}")
|
|
@@ -149,6 +161,7 @@ def remove(
|
|
|
149
161
|
):
|
|
150
162
|
"""Remove a specific memory by ID."""
|
|
151
163
|
database_name = _get_database_name(database)
|
|
164
|
+
logger.info("memory.remove.start", database=database_name, id=memory_id)
|
|
152
165
|
|
|
153
166
|
# First check if memory exists
|
|
154
167
|
memory = memory_manager.get_memory_by_id(database_name, memory_id)
|
|
@@ -156,6 +169,7 @@ def remove(
|
|
|
156
169
|
console.print(
|
|
157
170
|
f"[bold error]Error:[/bold error] Memory with ID '{memory_id}' not found for database '{database_name}'"
|
|
158
171
|
)
|
|
172
|
+
logger.error("memory.remove.not_found", database=database_name, id=memory_id)
|
|
159
173
|
sys.exit(1)
|
|
160
174
|
|
|
161
175
|
# Show memory content before removal
|
|
@@ -166,10 +180,12 @@ def remove(
|
|
|
166
180
|
console.print(
|
|
167
181
|
f"[green]✓ Memory removed from database '{database_name}'[/green]"
|
|
168
182
|
)
|
|
183
|
+
logger.info("memory.remove.success", database=database_name, id=memory_id)
|
|
169
184
|
else:
|
|
170
185
|
console.print(
|
|
171
186
|
f"[bold error]Error:[/bold error] Failed to remove memory '{memory_id}'"
|
|
172
187
|
)
|
|
188
|
+
logger.error("memory.remove.failed", database=database_name, id=memory_id)
|
|
173
189
|
sys.exit(1)
|
|
174
190
|
|
|
175
191
|
|
|
@@ -192,6 +208,7 @@ def clear(
|
|
|
192
208
|
):
|
|
193
209
|
"""Clear all memories for the specified database."""
|
|
194
210
|
database_name = _get_database_name(database)
|
|
211
|
+
logger.info("memory.clear.start", database=database_name, force=bool(force))
|
|
195
212
|
|
|
196
213
|
# Count memories first
|
|
197
214
|
memories_count = len(memory_manager.get_memories(database_name))
|
|
@@ -200,6 +217,7 @@ def clear(
|
|
|
200
217
|
console.print(
|
|
201
218
|
f"[warning]No memories to clear for database '{database_name}'[/warning]"
|
|
202
219
|
)
|
|
220
|
+
logger.info("memory.clear.nothing", database=database_name)
|
|
203
221
|
return
|
|
204
222
|
|
|
205
223
|
if not force:
|
|
@@ -210,12 +228,14 @@ def clear(
|
|
|
210
228
|
|
|
211
229
|
if not questionary.confirm("Are you sure you want to proceed?").ask():
|
|
212
230
|
console.print("Operation cancelled")
|
|
231
|
+
logger.info("memory.clear.cancelled", database=database_name)
|
|
213
232
|
return
|
|
214
233
|
|
|
215
234
|
cleared_count = memory_manager.clear_memories(database_name)
|
|
216
235
|
console.print(
|
|
217
236
|
f"[green]✓ Cleared {cleared_count} memories for database '{database_name}'[/green]"
|
|
218
237
|
)
|
|
238
|
+
logger.info("memory.clear.success", database=database_name, deleted=cleared_count)
|
|
219
239
|
|
|
220
240
|
|
|
221
241
|
@memory_app.command
|
|
@@ -230,6 +250,7 @@ def summary(
|
|
|
230
250
|
):
|
|
231
251
|
"""Show memory summary for the specified database."""
|
|
232
252
|
database_name = _get_database_name(database)
|
|
253
|
+
logger.info("memory.summary.start", database=database_name)
|
|
233
254
|
|
|
234
255
|
summary = memory_manager.get_memories_summary(database_name)
|
|
235
256
|
|
|
@@ -240,6 +261,11 @@ def summary(
|
|
|
240
261
|
console.print("\n[bold]Recent memories:[/bold]")
|
|
241
262
|
for memory in summary["memories"][-5:]: # Show last 5 memories
|
|
242
263
|
console.print(f"[dim]{memory['timestamp']}[/dim] - {memory['content']}")
|
|
264
|
+
logger.info(
|
|
265
|
+
"memory.summary.complete",
|
|
266
|
+
database=database_name,
|
|
267
|
+
total=summary["total_memories"],
|
|
268
|
+
)
|
|
243
269
|
|
|
244
270
|
|
|
245
271
|
def create_memory_app() -> cyclopts.App:
|