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.

Files changed (38) hide show
  1. sqlsaber/agents/__init__.py +2 -4
  2. sqlsaber/agents/base.py +18 -221
  3. sqlsaber/agents/mcp.py +2 -2
  4. sqlsaber/agents/pydantic_ai_agent.py +170 -0
  5. sqlsaber/cli/auth.py +146 -79
  6. sqlsaber/cli/commands.py +22 -7
  7. sqlsaber/cli/database.py +1 -1
  8. sqlsaber/cli/interactive.py +65 -30
  9. sqlsaber/cli/models.py +58 -29
  10. sqlsaber/cli/streaming.py +114 -77
  11. sqlsaber/config/api_keys.py +9 -11
  12. sqlsaber/config/providers.py +116 -0
  13. sqlsaber/config/settings.py +50 -30
  14. sqlsaber/database/connection.py +3 -3
  15. sqlsaber/mcp/mcp.py +43 -51
  16. sqlsaber/models/__init__.py +0 -3
  17. sqlsaber/tools/__init__.py +25 -0
  18. sqlsaber/tools/base.py +85 -0
  19. sqlsaber/tools/enums.py +21 -0
  20. sqlsaber/tools/instructions.py +251 -0
  21. sqlsaber/tools/registry.py +130 -0
  22. sqlsaber/tools/sql_tools.py +275 -0
  23. sqlsaber/tools/visualization_tools.py +144 -0
  24. {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/METADATA +20 -39
  25. sqlsaber-0.16.0.dist-info/RECORD +51 -0
  26. sqlsaber/agents/anthropic.py +0 -579
  27. sqlsaber/agents/streaming.py +0 -16
  28. sqlsaber/clients/__init__.py +0 -6
  29. sqlsaber/clients/anthropic.py +0 -285
  30. sqlsaber/clients/base.py +0 -31
  31. sqlsaber/clients/exceptions.py +0 -117
  32. sqlsaber/clients/models.py +0 -282
  33. sqlsaber/clients/streaming.py +0 -257
  34. sqlsaber/models/events.py +0 -28
  35. sqlsaber-0.14.0.dist-info/RECORD +0 -51
  36. {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/WHEEL +0 -0
  37. {sqlsaber-0.14.0.dist-info → sqlsaber-0.16.0.dist-info}/entry_points.txt +0 -0
  38. {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 questionary
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 method for SQLSaber."""
24
- console.print("\n[bold]SQLSaber Authentication Setup[/bold]\n")
25
-
26
- # Use questionary for selection
27
- auth_choice = questionary.select(
28
- "Choose your authentication method:",
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 auth_choice is None:
37
+ if provider is None:
44
38
  console.print("[yellow]Setup cancelled.[/yellow]")
45
39
  return
46
40
 
47
- # Handle auth method setup
48
- if auth_choice == AuthMethod.API_KEY:
49
- console.print("\nTo configure your API key, you can either:")
50
- console.print(" Set the ANTHROPIC_API_KEY environment variable")
51
- console.print(
52
- " Let SQLsaber prompt you for the key when needed (stored securely)"
53
- )
54
-
55
- config_manager.set_auth_method(auth_choice)
56
- console.print("\n[bold green]Authentication method saved![/bold green]")
57
-
58
- elif auth_choice == AuthMethod.CLAUDE_PRO:
59
- oauth_flow = AnthropicOAuthFlow()
60
- try:
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]Authentication setup complete![/bold green]"
56
+ "\n[bold green] Anthropic OAuth configured successfully![/bold green]"
66
57
  )
67
58
  else:
68
- console.print(
69
- "\n[yellow]OAuth authentication failed. Please try again.[/yellow]"
70
- )
71
- return
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
- if auth_method == AuthMethod.API_KEY:
93
- console.print("[green]✓ API Key authentication configured[/green]")
94
- console.print("Using Anthropic API key for authentication")
95
- elif auth_method == AuthMethod.CLAUDE_PRO:
96
- console.print("[green]✓ Claude Pro/Max subscription configured[/green]")
97
-
98
- # Check OAuth token status
99
- oauth_flow = AnthropicOAuthFlow()
100
- if oauth_flow.has_valid_authentication():
101
- console.print("OAuth token is valid and ready to use")
102
- else:
103
- console.print("[yellow]OAuth token missing or expired[/yellow]")
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 authentication configuration."""
109
- if not config_manager.has_auth_configured():
110
- console.print("[yellow]No authentication configuration to reset.[/yellow]")
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
- current_method = config_manager.get_auth_method()
114
- method_name = (
115
- "API Key" if current_method == AuthMethod.API_KEY else "Claude Pro/Max"
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 questionary.confirm(
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
- "Run [cyan]saber auth setup[/cyan] to configure authentication again."
152
+ f"[yellow]No stored credentials found for {provider}. Nothing to reset.[/yellow]"
134
153
  )
135
- else:
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.anthropic import AnthropicSQLAgent
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 DatabaseConnection
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 = AnthropicSQLAgent(db_conn, db_name)
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} {agent._get_database_type_name()}\n"
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
@@ -6,8 +6,8 @@ import sys
6
6
  from pathlib import Path
7
7
  from typing import Annotated
8
8
 
9
- import questionary
10
9
  import cyclopts
10
+ import questionary
11
11
  from rich.console import Console
12
12
  from rich.table import Table
13
13
 
@@ -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: BaseSQLAgent):
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 = getattr(self.agent, "database_name", None) or "Unknown"
35
- db_type = self.agent._get_database_type_name()
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
- f"[bold blue]Connected to:[/bold blue] {db_name} ({db_type})\n"
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
- "[dim]Press Esc-Enter or Meta-Enter to submit your query.[/dim]\n"
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
- # Use the schema manager directly which has built-in caching
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
- # Simply await the query task
110
- # Ctrl+C will be handled by the KeyboardInterrupt exception in run()
111
- await query_task
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
- await self.agent.clear_history()
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
- memory_id = self.agent.add_memory(memory_content)
158
- if memory_id:
159
- self.console.print(
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
- else:
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 (no database context)[/yellow]\n"
202
+ "[yellow]Could not add memory[/yellow]\n"
168
203
  )
169
204
  else:
170
205
  self.console.print(