sqlsaber 0.13.0__tar.gz → 0.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlsaber might be problematic. Click here for more details.

Files changed (77) hide show
  1. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/.github/workflows/claude-code-review.yml +12 -13
  2. sqlsaber-0.14.0/.github/workflows/test.yml +33 -0
  3. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/CHANGELOG.md +21 -0
  4. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/PKG-INFO +1 -1
  5. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/pyproject.toml +1 -1
  6. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/anthropic.py +35 -7
  7. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/base.py +104 -1
  8. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/interactive.py +6 -2
  9. sqlsaber-0.14.0/src/sqlsaber/conversation/__init__.py +12 -0
  10. sqlsaber-0.14.0/src/sqlsaber/conversation/manager.py +224 -0
  11. sqlsaber-0.14.0/src/sqlsaber/conversation/models.py +120 -0
  12. sqlsaber-0.14.0/src/sqlsaber/conversation/storage.py +362 -0
  13. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/database/schema.py +2 -51
  14. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_cli/test_commands.py +23 -23
  15. sqlsaber-0.14.0/tests/test_conversation_storage.py +136 -0
  16. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/uv.lock +1 -1
  17. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/.github/workflows/claude.yml +0 -0
  18. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/.github/workflows/publish.yml +0 -0
  19. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/.gitignore +0 -0
  20. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/.python-version +0 -0
  21. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/AGENT.md +0 -0
  22. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/CLAUDE.md +0 -0
  23. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/LICENSE +0 -0
  24. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/README.md +0 -0
  25. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/pytest.ini +0 -0
  26. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/sqlsaber.svg +0 -0
  27. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/__init__.py +0 -0
  28. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/__main__.py +0 -0
  29. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/__init__.py +0 -0
  30. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/mcp.py +0 -0
  31. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/agents/streaming.py +0 -0
  32. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/__init__.py +0 -0
  33. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/auth.py +0 -0
  34. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/commands.py +0 -0
  35. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/completers.py +0 -0
  36. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/database.py +0 -0
  37. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/display.py +0 -0
  38. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/memory.py +0 -0
  39. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/models.py +0 -0
  40. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/cli/streaming.py +0 -0
  41. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/__init__.py +0 -0
  42. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/anthropic.py +0 -0
  43. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/base.py +0 -0
  44. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/exceptions.py +0 -0
  45. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/models.py +0 -0
  46. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/clients/streaming.py +0 -0
  47. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/config/__init__.py +0 -0
  48. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/config/api_keys.py +0 -0
  49. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/config/auth.py +0 -0
  50. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/config/database.py +0 -0
  51. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/config/oauth_flow.py +0 -0
  52. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/config/oauth_tokens.py +0 -0
  53. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/config/settings.py +0 -0
  54. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/database/__init__.py +0 -0
  55. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/database/connection.py +0 -0
  56. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/database/resolver.py +0 -0
  57. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/mcp/__init__.py +0 -0
  58. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/mcp/mcp.py +0 -0
  59. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/memory/__init__.py +0 -0
  60. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/memory/manager.py +0 -0
  61. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/memory/storage.py +0 -0
  62. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/models/__init__.py +0 -0
  63. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/models/events.py +0 -0
  64. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/src/sqlsaber/models/types.py +0 -0
  65. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/__init__.py +0 -0
  66. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/conftest.py +0 -0
  67. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_agents/test_anthropic_oauth.py +0 -0
  68. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_cli/__init__.py +0 -0
  69. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_clients/test_anthropic_client.py +0 -0
  70. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_clients/test_streaming.py +0 -0
  71. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_config/__init__.py +0 -0
  72. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_config/test_database.py +0 -0
  73. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_config/test_oauth.py +0 -0
  74. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_config/test_settings.py +0 -0
  75. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_database/__init__.py +0 -0
  76. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_database/test_connection.py +0 -0
  77. {sqlsaber-0.13.0 → sqlsaber-0.14.0}/tests/test_database_resolver.py +0 -0
@@ -17,14 +17,14 @@ jobs:
17
17
  # github.event.pull_request.user.login == 'external-contributor' ||
18
18
  # github.event.pull_request.user.login == 'new-developer' ||
19
19
  # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20
-
20
+
21
21
  runs-on: ubuntu-latest
22
22
  permissions:
23
23
  contents: read
24
24
  pull-requests: read
25
25
  issues: read
26
26
  id-token: write
27
-
27
+
28
28
  steps:
29
29
  - name: Checkout repository
30
30
  uses: actions/checkout@v4
@@ -39,7 +39,7 @@ jobs:
39
39
 
40
40
  # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
41
41
  # model: "claude-opus-4-20250514"
42
-
42
+
43
43
  # Direct prompt for automated review (no @claude mention needed)
44
44
  direct_prompt: |
45
45
  Please review this pull request and provide feedback on:
@@ -48,12 +48,12 @@ jobs:
48
48
  - Performance considerations
49
49
  - Security concerns
50
50
  - Test coverage
51
-
51
+
52
52
  Be constructive and helpful in your feedback.
53
53
 
54
54
  # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
55
55
  # use_sticky_comment: true
56
-
56
+
57
57
  # Optional: Customize review based on file types
58
58
  # direct_prompt: |
59
59
  # Review this PR focusing on:
@@ -61,18 +61,17 @@ jobs:
61
61
  # - For API endpoints: Security, input validation, and error handling
62
62
  # - For React components: Performance, accessibility, and best practices
63
63
  # - For tests: Coverage, edge cases, and test quality
64
-
64
+
65
65
  # Optional: Different prompts for different authors
66
66
  # direct_prompt: |
67
- # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
67
+ # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
68
68
  # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
69
69
  # 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
70
-
70
+
71
71
  # Optional: Add specific tools for running tests or linting
72
72
  # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
73
-
74
- # Optional: Skip review for certain conditions
75
- # if: |
76
- # !contains(github.event.pull_request.title, '[skip-review]') &&
77
- # !contains(github.event.pull_request.title, '[WIP]')
78
73
 
74
+ # Optional: Skip review for certain conditions
75
+ if: |
76
+ !contains(github.event.pull_request.title, '[skip-review]') &&
77
+ !contains(github.event.pull_request.title, '[WIP]')
@@ -0,0 +1,33 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # Cancel active CI runs for a PR before starting another run
9
+ concurrency:
10
+ group: ${{ github.workflow}}-${{ github.ref }}
11
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v5
21
+ with:
22
+ enable-cache: true
23
+
24
+ - name: Set up Python
25
+ uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.12"
28
+
29
+ - name: Install dependencies
30
+ run: uv sync --locked --all-extras --dev
31
+
32
+ - name: Run tests
33
+ run: uv run python -m pytest
@@ -4,6 +4,27 @@ All notable changes to SQLSaber will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.14.0] - 2025-08-01
8
+
9
+ ### Added
10
+
11
+ - Local conversation storage between user and agent
12
+ - Store conversation history persistently
13
+ - Track messages with proper attribution
14
+ - Added automated test execution in CI
15
+ - New GitHub Actions workflow for running tests
16
+ - Updated code review workflow
17
+
18
+ ### Fixed
19
+
20
+ - Fixed CLI commands test suite (#11)
21
+
22
+ ### Changed
23
+
24
+ - Removed schema caching from SchemaManager
25
+ - Simplified schema introspection by removing cache logic
26
+ - Direct database queries for schema information
27
+
7
28
  ## [0.13.0] - 2025-07-26
8
29
 
9
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: SQLSaber - Agentic SQL assistant like Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlsaber"
3
- version = "0.13.0"
3
+ version = "0.14.0"
4
4
  description = "SQLSaber - Agentic SQL assistant like Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -450,6 +450,16 @@ Guidelines:
450
450
  self._last_query = None
451
451
 
452
452
  try:
453
+ # Ensure conversation is active for persistence
454
+ await self._ensure_conversation()
455
+
456
+ # Store user message in conversation history and persistence
457
+ if use_history:
458
+ self.conversation_history.append(
459
+ {"role": "user", "content": user_query}
460
+ )
461
+ await self._store_user_message(user_query)
462
+
453
463
  # Build messages with history if requested
454
464
  messages = []
455
465
  if use_history:
@@ -461,8 +471,9 @@ Guidelines:
461
471
  instructions = self._get_sql_assistant_instructions()
462
472
  messages.append(Message(MessageRole.USER, instructions))
463
473
 
464
- # Add current user message
465
- messages.append(Message(MessageRole.USER, user_query))
474
+ # Add current user message if not already in messages from history
475
+ if not use_history:
476
+ messages.append(Message(MessageRole.USER, user_query))
466
477
 
467
478
  # Create initial request and get response
468
479
  request = self._create_message_request(messages)
@@ -484,9 +495,12 @@ Guidelines:
484
495
  return
485
496
 
486
497
  # Add assistant's response to conversation
487
- collected_content.append(
488
- {"role": "assistant", "content": response.content}
489
- )
498
+ assistant_content = {"role": "assistant", "content": response.content}
499
+ collected_content.append(assistant_content)
500
+
501
+ # Store the assistant message immediately (not from collected_content)
502
+ if use_history:
503
+ await self._store_assistant_message(response.content)
490
504
 
491
505
  # Execute tools and get results
492
506
  tool_results = []
@@ -499,9 +513,19 @@ Guidelines:
499
513
  tool_results = event
500
514
 
501
515
  # Continue conversation with tool results
502
- collected_content.append({"role": "user", "content": tool_results})
516
+ tool_content = {"role": "user", "content": tool_results}
517
+ collected_content.append(tool_content)
518
+
519
+ # Store the tool message immediately and update history
503
520
  if use_history:
504
- self.conversation_history.extend(collected_content)
521
+ # Only add the NEW messages to history (not the accumulated ones)
522
+ # collected_content has [assistant1, tool1, assistant2, tool2, ...]
523
+ # We only want to add the last 2 items that were just added
524
+ new_messages_for_history = collected_content[
525
+ -2:
526
+ ] # Last assistant + tool pair
527
+ self.conversation_history.extend(new_messages_for_history)
528
+ await self._store_tool_message(tool_results)
505
529
 
506
530
  if cancellation_token is not None and cancellation_token.is_set():
507
531
  return
@@ -541,6 +565,10 @@ Guidelines:
541
565
  {"role": "assistant", "content": response.content}
542
566
  )
543
567
 
568
+ # Store final assistant message in persistence (only if not tool_use)
569
+ if response.stop_reason != "tool_use":
570
+ await self._store_assistant_message(response.content)
571
+
544
572
  except asyncio.CancelledError:
545
573
  return
546
574
  except Exception as e:
@@ -7,6 +7,7 @@ from typing import Any, AsyncIterator
7
7
 
8
8
  from uniplot import histogram, plot
9
9
 
10
+ from sqlsaber.conversation.manager import ConversationManager
10
11
  from sqlsaber.database.connection import (
11
12
  BaseDatabaseConnection,
12
13
  CSVConnection,
@@ -26,6 +27,11 @@ class BaseSQLAgent(ABC):
26
27
  self.schema_manager = SchemaManager(db_connection)
27
28
  self.conversation_history: list[dict[str, Any]] = []
28
29
 
30
+ # Conversation persistence
31
+ self._conv_manager = ConversationManager()
32
+ self._conversation_id: str | None = None
33
+ self._msg_index: int = 0
34
+
29
35
  @abstractmethod
30
36
  async def query_stream(
31
37
  self,
@@ -42,8 +48,12 @@ class BaseSQLAgent(ABC):
42
48
  """
43
49
  pass
44
50
 
45
- def clear_history(self):
51
+ async def clear_history(self):
46
52
  """Clear conversation history."""
53
+ # End current conversation in storage
54
+ await self._end_conversation()
55
+
56
+ # Clear in-memory history
47
57
  self.conversation_history = []
48
58
 
49
59
  def _get_database_type_name(self) -> str:
@@ -284,3 +294,96 @@ class BaseSQLAgent(ABC):
284
294
 
285
295
  except Exception as e:
286
296
  return json.dumps({"error": f"Error creating plot: {str(e)}"})
297
+
298
+ # Conversation persistence helpers
299
+
300
+ async def _ensure_conversation(self) -> None:
301
+ """Ensure a conversation is active for storing messages."""
302
+ if self._conversation_id is None:
303
+ db_name = getattr(self, "database_name", "unknown")
304
+ self._conversation_id = await self._conv_manager.start_conversation(db_name)
305
+ self._msg_index = 0
306
+
307
+ async def _store_user_message(self, content: str | dict[str, Any]) -> None:
308
+ """Store a user message in conversation history."""
309
+ if self._conversation_id is None:
310
+ return
311
+
312
+ await self._conv_manager.add_user_message(
313
+ self._conversation_id, content, self._msg_index
314
+ )
315
+ self._msg_index += 1
316
+
317
+ async def _store_assistant_message(
318
+ self, content: list[dict[str, Any]] | dict[str, Any]
319
+ ) -> None:
320
+ """Store an assistant message in conversation history."""
321
+ if self._conversation_id is None:
322
+ return
323
+
324
+ await self._conv_manager.add_assistant_message(
325
+ self._conversation_id, content, self._msg_index
326
+ )
327
+ self._msg_index += 1
328
+
329
+ async def _store_tool_message(
330
+ self, content: list[dict[str, Any]] | dict[str, Any]
331
+ ) -> None:
332
+ """Store a tool/system message in conversation history."""
333
+ if self._conversation_id is None:
334
+ return
335
+
336
+ await self._conv_manager.add_tool_message(
337
+ self._conversation_id, content, self._msg_index
338
+ )
339
+ self._msg_index += 1
340
+
341
+ async def _end_conversation(self) -> None:
342
+ """End the current conversation."""
343
+ if self._conversation_id:
344
+ await self._conv_manager.end_conversation(self._conversation_id)
345
+ self._conversation_id = None
346
+ self._msg_index = 0
347
+
348
+ async def restore_conversation(self, conversation_id: str) -> bool:
349
+ """Restore a conversation from storage to in-memory history.
350
+
351
+ Args:
352
+ conversation_id: ID of the conversation to restore
353
+
354
+ Returns:
355
+ True if successfully restored, False otherwise
356
+ """
357
+ success = await self._conv_manager.restore_conversation_to_agent(
358
+ conversation_id, self.conversation_history
359
+ )
360
+
361
+ if success:
362
+ # Set up for continuing this conversation
363
+ self._conversation_id = conversation_id
364
+ self._msg_index = len(self.conversation_history)
365
+
366
+ return success
367
+
368
+ async def list_conversations(self, limit: int = 50) -> list:
369
+ """List conversations for this agent's database.
370
+
371
+ Args:
372
+ limit: Maximum number of conversations to return
373
+
374
+ Returns:
375
+ List of conversation data
376
+ """
377
+ db_name = getattr(self, "database_name", None)
378
+ conversations = await self._conv_manager.list_conversations(db_name, limit)
379
+
380
+ return [
381
+ {
382
+ "id": conv.id,
383
+ "database_name": conv.database_name,
384
+ "started_at": conv.formatted_start_time(),
385
+ "ended_at": conv.formatted_end_time(),
386
+ "duration": conv.duration_seconds(),
387
+ }
388
+ for conv in conversations
389
+ ]
@@ -136,11 +136,15 @@ class InteractiveSession:
136
136
  if not user_query:
137
137
  continue
138
138
 
139
- if user_query in ["/exit", "/quit"]:
139
+ if (
140
+ user_query in ["/exit", "/quit"]
141
+ or user_query.startswith("/exit")
142
+ or user_query.startswith("/quit")
143
+ ):
140
144
  break
141
145
 
142
146
  if user_query == "/clear":
143
- self.agent.clear_history()
147
+ await self.agent.clear_history()
144
148
  self.console.print("[green]Conversation history cleared.[/green]\n")
145
149
  continue
146
150
 
@@ -0,0 +1,12 @@
1
+ """Conversation history storage for SQLSaber."""
2
+
3
+ from .manager import ConversationManager
4
+ from .models import Conversation, ConversationMessage
5
+ from .storage import ConversationStorage
6
+
7
+ __all__ = [
8
+ "Conversation",
9
+ "ConversationMessage",
10
+ "ConversationStorage",
11
+ "ConversationManager",
12
+ ]
@@ -0,0 +1,224 @@
1
+ """Manager for conversation storage operations."""
2
+
3
+ import logging
4
+ import uuid
5
+ from typing import Any
6
+
7
+ from .models import Conversation, ConversationMessage
8
+ from .storage import ConversationStorage
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class ConversationManager:
14
+ """High-level manager for conversation storage operations."""
15
+
16
+ def __init__(self):
17
+ """Initialize conversation manager."""
18
+ self._storage = ConversationStorage()
19
+
20
+ async def start_conversation(self, database_name: str) -> str:
21
+ """Start a new conversation.
22
+
23
+ Args:
24
+ database_name: Name of the database for this conversation
25
+
26
+ Returns:
27
+ Conversation ID
28
+ """
29
+ try:
30
+ return await self._storage.create_conversation(database_name)
31
+ except Exception as e:
32
+ logger.warning(f"Failed to start conversation: {e}")
33
+ return str(uuid.uuid4())
34
+
35
+ async def add_user_message(
36
+ self, conversation_id: str, content: str | dict[str, Any], index: int
37
+ ) -> str:
38
+ """Add a user message to the conversation.
39
+
40
+ Args:
41
+ conversation_id: ID of the conversation
42
+ content: Message content
43
+ index: Sequential index in conversation
44
+
45
+ Returns:
46
+ Message ID
47
+ """
48
+ try:
49
+ return await self._storage.add_message(
50
+ conversation_id, "user", content, index
51
+ )
52
+ except Exception as e:
53
+ logger.warning(f"Failed to add user message: {e}")
54
+ return str(uuid.uuid4())
55
+
56
+ async def add_assistant_message(
57
+ self,
58
+ conversation_id: str,
59
+ content: list[dict[str, Any]] | dict[str, Any],
60
+ index: int,
61
+ ) -> str:
62
+ """Add an assistant message to the conversation.
63
+
64
+ Args:
65
+ conversation_id: ID of the conversation
66
+ content: Message content (typically ContentBlock list)
67
+ index: Sequential index in conversation
68
+
69
+ Returns:
70
+ Message ID
71
+ """
72
+ try:
73
+ return await self._storage.add_message(
74
+ conversation_id, "assistant", content, index
75
+ )
76
+ except Exception as e:
77
+ logger.warning(f"Failed to add assistant message: {e}")
78
+ return str(uuid.uuid4())
79
+
80
+ async def add_tool_message(
81
+ self,
82
+ conversation_id: str,
83
+ content: list[dict[str, Any]] | dict[str, Any],
84
+ index: int,
85
+ ) -> str:
86
+ """Add a tool/system message to the conversation.
87
+
88
+ Args:
89
+ conversation_id: ID of the conversation
90
+ content: Message content (typically tool results)
91
+ index: Sequential index in conversation
92
+
93
+ Returns:
94
+ Message ID
95
+ """
96
+ try:
97
+ return await self._storage.add_message(
98
+ conversation_id, "tool", content, index
99
+ )
100
+ except Exception as e:
101
+ logger.warning(f"Failed to add tool message: {e}")
102
+ return str(uuid.uuid4())
103
+
104
+ async def end_conversation(self, conversation_id: str) -> bool:
105
+ """End a conversation.
106
+
107
+ Args:
108
+ conversation_id: ID of the conversation to end
109
+
110
+ Returns:
111
+ True if successfully ended, False otherwise
112
+ """
113
+ try:
114
+ return await self._storage.end_conversation(conversation_id)
115
+ except Exception as e:
116
+ logger.warning(f"Failed to end conversation: {e}")
117
+ return False
118
+
119
+ async def get_conversation(self, conversation_id: str) -> Conversation | None:
120
+ """Get a conversation by ID.
121
+
122
+ Args:
123
+ conversation_id: ID of the conversation
124
+
125
+ Returns:
126
+ Conversation object or None if not found
127
+ """
128
+ try:
129
+ return await self._storage.get_conversation(conversation_id)
130
+ except Exception as e:
131
+ logger.warning(f"Failed to get conversation: {e}")
132
+ return None
133
+
134
+ async def get_conversation_messages(
135
+ self, conversation_id: str
136
+ ) -> list[ConversationMessage]:
137
+ """Get all messages for a conversation.
138
+
139
+ Args:
140
+ conversation_id: ID of the conversation
141
+
142
+ Returns:
143
+ List of messages ordered by index
144
+ """
145
+ try:
146
+ return await self._storage.get_conversation_messages(conversation_id)
147
+ except Exception as e:
148
+ logger.warning(f"Failed to get conversation messages: {e}")
149
+ return []
150
+
151
+ async def list_conversations(
152
+ self, database_name: str | None = None, limit: int = 50
153
+ ) -> list[Conversation]:
154
+ """List conversations.
155
+
156
+ Args:
157
+ database_name: Optional database name filter
158
+ limit: Maximum number of conversations to return
159
+
160
+ Returns:
161
+ List of conversations ordered by start time (newest first)
162
+ """
163
+ try:
164
+ return await self._storage.list_conversations(database_name, limit)
165
+ except Exception as e:
166
+ logger.warning(f"Failed to list conversations: {e}")
167
+ return []
168
+
169
+ async def delete_conversation(self, conversation_id: str) -> bool:
170
+ """Delete a conversation.
171
+
172
+ Args:
173
+ conversation_id: ID of the conversation to delete
174
+
175
+ Returns:
176
+ True if successfully deleted, False otherwise
177
+ """
178
+ try:
179
+ return await self._storage.delete_conversation(conversation_id)
180
+ except Exception as e:
181
+ logger.warning(f"Failed to delete conversation: {e}")
182
+ return False
183
+
184
+ async def get_database_names(self) -> list[str]:
185
+ """Get list of database names with conversations.
186
+
187
+ Returns:
188
+ List of unique database names
189
+ """
190
+ try:
191
+ return await self._storage.get_database_names()
192
+ except Exception as e:
193
+ logger.warning(f"Failed to get database names: {e}")
194
+ return []
195
+
196
+ async def restore_conversation_to_agent(
197
+ self, conversation_id: str, agent_history: list[dict[str, Any]]
198
+ ) -> bool:
199
+ """Restore a conversation's messages to an agent's in-memory history.
200
+
201
+ Args:
202
+ conversation_id: ID of the conversation to restore
203
+ agent_history: Agent's conversation_history list to populate
204
+
205
+ Returns:
206
+ True if successfully restored, False otherwise
207
+ """
208
+ try:
209
+ messages = await self.get_conversation_messages(conversation_id)
210
+
211
+ # Clear existing history
212
+ agent_history.clear()
213
+
214
+ # Convert messages back to agent format
215
+ for msg in messages:
216
+ if msg.role in ("user", "assistant", "tool"):
217
+ agent_history.append({"role": msg.role, "content": msg.content})
218
+
219
+ logger.debug(f"Restored {len(messages)} messages to agent history")
220
+ return True
221
+
222
+ except Exception as e:
223
+ logger.warning(f"Failed to restore conversation to agent: {e}")
224
+ return False