diff --git a/docs/src/content/docs/storage/dynamodb.mdx b/docs/src/content/docs/storage/dynamodb.mdx
index b3917bed..426a9677 100644
--- a/docs/src/content/docs/storage/dynamodb.mdx
+++ b/docs/src/content/docs/storage/dynamodb.mdx
@@ -17,6 +17,14 @@ DynamoDB storage provides a scalable and persistent solution for storing convers
- When long-term persistence of conversation history is required
- For applications that need to scale horizontally
+## Python Package
+
+If you haven't already installed the AWS-related dependencies, make sure to install them:
+
+```bash
+pip install "multi-agent-orchestrator[aws]"
+```
+
## Implementation
To use DynamoDB storage in your Multi-Agent Orchestrator:
diff --git a/docs/src/content/docs/storage/overview.md b/docs/src/content/docs/storage/overview.md
index 79211746..78c46264 100644
--- a/docs/src/content/docs/storage/overview.md
+++ b/docs/src/content/docs/storage/overview.md
@@ -21,19 +21,25 @@ The Multi-Agent Orchestrator System offers flexible storage options for maintain
- Provides persistent storage for production environments.
- Allows for scalable and durable conversation history storage.
-3. **Custom Storage Solutions**:
+3. **SQL Storage**:
+ - Offers persistent storage using SQLite or Turso databases.
+ - When you need local-first development with remote deployment options
+
+4. **Custom Storage Solutions**:
- The system allows for implementation of custom storage options to meet specific needs.
## Choosing the Right Storage Option
- Use In-Memory Storage for development, testing, or when persistence between application restarts is not necessary.
- Choose DynamoDB Storage for production environments where conversation history needs to be preserved long-term or across multiple instances of your application.
+- Consider SQL Storage for a balance between simplicity and scalability, supporting both local and remote databases.
- Implement a custom storage solution if you have specific requirements not met by the provided options.
## Next Steps
- Learn more about [In-Memory Storage](/multi-agent-orchestrator/storage/in-memory)
- Explore [DynamoDB Storage](/multi-agent-orchestrator/storage/dynamodb) for persistent storage
+- Explore [SQL Storage](/multi-agent-orchestrator/storage/sql) for persistent storage using SQLite or Turso.
- Discover how to [implement custom storage solutions](/multi-agent-orchestrator/storage/custom)
By leveraging these storage options, you can ensure that your Multi-Agent Orchestrator System maintains the necessary context for coherent and effective conversations across various use cases and deployment scenarios.
\ No newline at end of file
diff --git a/docs/src/content/docs/storage/sql.mdx b/docs/src/content/docs/storage/sql.mdx
new file mode 100644
index 00000000..68e316fe
--- /dev/null
+++ b/docs/src/content/docs/storage/sql.mdx
@@ -0,0 +1,144 @@
+---
+title: SQL Storage
+description: Using SQL databases (SQLite/Turso) for persistent conversation storage in the Multi-Agent Orchestrator System
+---
+
+SQL storage provides a flexible and reliable solution for storing conversation history in the Multi-Agent Orchestrator System. This implementation supports both local SQLite databases and remote Turso databases, making it suitable for various deployment scenarios from development to production.
+
+## Features
+
+- Persistent storage across application restarts
+- Support for both local and remote databases
+- Built-in connection pooling and retry mechanisms
+- Compatible with edge and serverless deployments
+- Transaction support for data consistency
+- Efficient indexing for quick data retrieval
+
+## When to Use SQL Storage
+
+- When you need a balance between simplicity and scalability
+- For applications requiring persistent storage without complex infrastructure
+- In both development and production environments
+- When working with edge or serverless deployments
+- When you need local-first development with remote deployment options
+
+## Python Package Installation
+
+To use SQL storage in your Python application, make sure to install them:
+
+```bash
+pip install "multi-agent-orchestrator[sql]"
+```
+
+This will install the `libsql-client` package required for SQL storage functionality.
+
+## Implementation
+
+To use SQL storage in your Multi-Agent Orchestrator:
+
+import { Tabs, TabItem } from '@astrojs/starlight/components';
+
+
+
+ ```typescript
+ import { SqlChatStorage, MultiAgentOrchestrator } from 'multi-agent-orchestrator';
+
+ // For local SQLite database
+ const localStorage = new SqlChatStorage('file:local.db');
+ await localStorage.waitForInitialization();
+
+ // For remote database
+ const remoteStorage = new SqlChatStorage(
+ 'libsql://your-database-url.example.com',
+ 'your-auth-token'
+ );
+ await remoteStorage.waitForInitialization();
+
+ const orchestrator = new MultiAgentOrchestrator({
+ storage: localStorage // or remoteStorage
+ });
+
+
+ // Close the database connections when done
+ await localStorage.close();
+ await remoteStorage.close();
+ ```
+
+
+ ```python
+ from multi_agent_orchestrator.storage import SqlChatStorage
+ from multi_agent_orchestrator.orchestrator import MultiAgentOrchestrator
+
+ # For local SQLite database
+ local_storage = SqlChatStorage('file:local.db')
+
+ # For remote Turso database
+ remote_storage = SqlChatStorage(
+ url='libsql://your-database-url.turso.io',
+ auth_token='your-auth-token'
+ )
+
+ orchestrator = MultiAgentOrchestrator(storage=local_storage) # or remote_storage
+ ```
+
+
+
+## Configuration
+
+### Local DB
+For local development, simply provide a file URL:
+```typescript
+const storage = new SqlChatStorage('file:local.db');
+```
+
+### Remote DB
+For production with Turso:
+1. Create a Turso database through their platform
+2. Obtain your database URL and authentication token
+3. Configure your storage:
+```typescript
+const storage = new SqlChatStorage(
+ 'libsql://your-database-url.com',
+ 'your-auth-token'
+);
+```
+
+## Database Schema
+
+The SQL storage implementation uses the following schema:
+
+```sql
+CREATE TABLE conversations (
+ user_id TEXT NOT NULL,
+ session_id TEXT NOT NULL,
+ agent_id TEXT NOT NULL,
+ message_index INTEGER NOT NULL,
+ role TEXT NOT NULL,
+ content TEXT NOT NULL,
+ timestamp INTEGER NOT NULL,
+ PRIMARY KEY (user_id, session_id, agent_id, message_index)
+);
+
+CREATE INDEX idx_conversations_lookup
+ON conversations(user_id, session_id, agent_id);
+```
+
+## Considerations
+
+- Automatic table and index creation on initialization
+- Built-in transaction support for data consistency
+- Efficient query performance through proper indexing
+- Support for message history size limits
+- Automatic JSON serialization/deserialization of message content
+
+## Best Practices
+
+- Use a single instance of SqlChatStorage throughout your application
+- Regularly backup your database
+- Implement data cleanup strategies for old conversations
+- Monitor database size and performance
+- Keep your authentication tokens secure
+- Implement proper access controls at the application level
+- Close the database connections when done
+
+SQL storage provides a robust and flexible solution for managing conversation history in the Multi-Agent Orchestrator System. It offers a good balance between simplicity and features, making it suitable for both development and production environments.
\ No newline at end of file
diff --git a/python/setup.cfg b/python/setup.cfg
index 8909a797..338d2c48 100644
--- a/python/setup.cfg
+++ b/python/setup.cfg
@@ -27,10 +27,13 @@ anthropic =
anthropic>=0.40.0
openai =
openai>=1.55.3
+sql =
+ libsql-client>=0.3.1
all =
anthropic==0.40.0
openai==1.55.3
boto3==1.35.0
+ libsql-client==0.3.1
[options.packages.find]
where = src
diff --git a/python/src/multi_agent_orchestrator/storage/__init__.py b/python/src/multi_agent_orchestrator/storage/__init__.py
index 51d3279d..8ddaaa78 100644
--- a/python/src/multi_agent_orchestrator/storage/__init__.py
+++ b/python/src/multi_agent_orchestrator/storage/__init__.py
@@ -5,6 +5,7 @@
from .in_memory_chat_storage import InMemoryChatStorage
_AWS_AVAILABLE = False
+_SQL_AVAILABLE = False
try:
from .dynamodb_chat_storage import DynamoDbChatStorage
@@ -12,6 +13,12 @@
except ImportError:
_AWS_AVAILABLE = False
+try:
+ from .sql_chat_storage import SqlChatStorage
+ _SQL_AVAILABLE = True
+except ImportError:
+ _SQL_AVAILABLE = False
+
__all__ = [
'ChatStorage',
'InMemoryChatStorage',
@@ -20,4 +27,9 @@
if _AWS_AVAILABLE:
__all__.extend([
'DynamoDbChatStorage'
+ ])
+
+if _SQL_AVAILABLE:
+ __all__.extend([
+ 'SqlChatStorage'
])
\ No newline at end of file
diff --git a/python/src/multi_agent_orchestrator/storage/sql_chat_storage.py b/python/src/multi_agent_orchestrator/storage/sql_chat_storage.py
new file mode 100644
index 00000000..b8274944
--- /dev/null
+++ b/python/src/multi_agent_orchestrator/storage/sql_chat_storage.py
@@ -0,0 +1,202 @@
+from typing import List, Dict, Optional, Union
+import time
+import json
+from libsql_client import Client, create_client
+from multi_agent_orchestrator.storage import ChatStorage
+from multi_agent_orchestrator.types import ConversationMessage, ParticipantRole, TimestampedMessage
+from multi_agent_orchestrator.utils import Logger
+
+class SqlChatStorage(ChatStorage):
+ """SQL-based chat storage implementation supporting both local SQLite and remote Turso databases."""
+
+ def __init__(
+ self,
+ url: str,
+ auth_token: Optional[str] = None
+ ):
+ """Initialize SQL storage.
+
+ Args:
+ url: Database URL (e.g., 'file:local.db' or 'libsql://your-db-url.com')
+ auth_token: Authentication token for remote databases (optional)
+ """
+ super().__init__()
+ self.client = create_client(
+ url=url,
+ auth_token=auth_token
+ )
+ self._initialize_database()
+
+ def _initialize_database(self) -> None:
+ """Create necessary tables and indexes if they don't exist."""
+ try:
+ # Create conversations table
+ self.client.execute("""
+ CREATE TABLE IF NOT EXISTS conversations (
+ user_id TEXT NOT NULL,
+ session_id TEXT NOT NULL,
+ agent_id TEXT NOT NULL,
+ message_index INTEGER NOT NULL,
+ role TEXT NOT NULL,
+ content TEXT NOT NULL,
+ timestamp INTEGER NOT NULL,
+ PRIMARY KEY (user_id, session_id, agent_id, message_index)
+ )
+ """)
+
+ # Create index for faster queries
+ self.client.execute("""
+ CREATE INDEX IF NOT EXISTS idx_conversations_lookup
+ ON conversations(user_id, session_id, agent_id)
+ """)
+ except Exception as error:
+ Logger.error(f"Error initializing database: {str(error)}")
+ raise error
+
+ async def save_chat_message(
+ self,
+ user_id: str,
+ session_id: str,
+ agent_id: str,
+ new_message: ConversationMessage,
+ max_history_size: Optional[int] = None
+ ) -> List[ConversationMessage]:
+ """Save a new chat message."""
+ try:
+ # Fetch existing conversation
+ existing_conversation = await self.fetch_chat(user_id, session_id, agent_id)
+
+ if self.is_consecutive_message(existing_conversation, new_message):
+ Logger.debug(f"> Consecutive {new_message.role} message detected for agent {agent_id}. Not saving.")
+ return existing_conversation
+
+ # Get next message index
+ result = self.client.execute("""
+ SELECT COALESCE(MAX(message_index) + 1, 0) as next_index
+ FROM conversations
+ WHERE user_id = ? AND session_id = ? AND agent_id = ?
+ """, [user_id, session_id, agent_id])
+
+ next_index = result.rows[0]['next_index']
+ timestamp = int(time.time() * 1000)
+ content = json.dumps(new_message.content)
+
+ # Begin transaction
+ with self.client.transaction():
+ # Insert new message
+ self.client.execute("""
+ INSERT INTO conversations (
+ user_id, session_id, agent_id, message_index,
+ role, content, timestamp
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
+ """, [
+ user_id, session_id, agent_id, next_index,
+ new_message.role, content, timestamp
+ ])
+
+ # Clean up old messages if max_history_size is set
+ if max_history_size is not None:
+ self.client.execute("""
+ DELETE FROM conversations
+ WHERE user_id = ?
+ AND session_id = ?
+ AND agent_id = ?
+ AND message_index <= (
+ SELECT MAX(message_index) - ?
+ FROM conversations
+ WHERE user_id = ?
+ AND session_id = ?
+ AND agent_id = ?
+ )
+ """, [
+ user_id, session_id, agent_id,
+ max_history_size,
+ user_id, session_id, agent_id
+ ])
+
+ # Return updated conversation
+ return await self.fetch_chat(user_id, session_id, agent_id, max_history_size)
+ except Exception as error:
+ Logger.error(f"Error saving message: {str(error)}")
+ raise error
+
+ async def fetch_chat(
+ self,
+ user_id: str,
+ session_id: str,
+ agent_id: str,
+ max_history_size: Optional[int] = None
+ ) -> List[ConversationMessage]:
+ """Fetch chat messages."""
+ try:
+ query = """
+ SELECT role, content, timestamp
+ FROM conversations
+ WHERE user_id = ? AND session_id = ? AND agent_id = ?
+ ORDER BY message_index {}
+ """.format('DESC LIMIT ?' if max_history_size else 'ASC')
+
+ params = [user_id, session_id, agent_id]
+ if max_history_size:
+ params.append(max_history_size)
+
+ result = self.client.execute(query, params)
+ messages = result.rows
+
+ # If we used LIMIT, reverse to maintain chronological order
+ if max_history_size:
+ messages.reverse()
+
+ return [
+ ConversationMessage(
+ role=msg['role'],
+ content=json.loads(msg['content'])
+ ) for msg in messages
+ ]
+ except Exception as error:
+ Logger.error(f"Error fetching chat: {str(error)}")
+ raise error
+
+ async def fetch_all_chats(
+ self,
+ user_id: str,
+ session_id: str
+ ) -> List[ConversationMessage]:
+ """Fetch all chat messages for a user and session."""
+ try:
+ result = self.client.execute("""
+ SELECT role, content, timestamp, agent_id
+ FROM conversations
+ WHERE user_id = ? AND session_id = ?
+ ORDER BY timestamp ASC
+ """, [user_id, session_id])
+
+ return [
+ ConversationMessage(
+ role=msg['role'],
+ content=self._format_content(
+ msg['role'],
+ json.loads(msg['content']),
+ msg['agent_id']
+ )
+ ) for msg in result.rows
+ ]
+ except Exception as error:
+ Logger.error(f"Error fetching all chats: {str(error)}")
+ raise error
+
+ def _format_content(
+ self,
+ role: str,
+ content: Union[List, str],
+ agent_id: str
+ ) -> List[Dict[str, str]]:
+ """Format message content with agent ID for assistant messages."""
+ if role == ParticipantRole.ASSISTANT.value:
+ text = content[0]['text'] if isinstance(content, list) else content
+ return [{'text': f"[{agent_id}] {text}"}]
+ return content if isinstance(content, list) else [{'text': content}]
+
+ def close(self) -> None:
+ """Close the database connection."""
+ self.client.close()
\ No newline at end of file
diff --git a/typescript/package-lock.json b/typescript/package-lock.json
index 494c1b35..5060785b 100644
--- a/typescript/package-lock.json
+++ b/typescript/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "multi-agent-orchestrator",
- "version": "0.1.1",
+ "version": "0.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-agent-orchestrator",
- "version": "0.1.1",
+ "version": "0.1.2",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.24.3",
@@ -18,8 +18,8 @@
"@aws-sdk/client-lex-runtime-v2": "^3.621.0",
"@aws-sdk/lib-dynamodb": "^3.621.0",
"@aws-sdk/util-dynamodb": "^3.621.0",
+ "@libsql/client": "^0.14.0",
"axios": "^1.7.2",
- "chai": "^5.1.2",
"eslint-config-prettier": "^9.1.0",
"natural": "^7.0.7",
"openai": "^4.52.7",
@@ -2947,6 +2947,168 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@libsql/client": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.14.0.tgz",
+ "integrity": "sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@libsql/core": "^0.14.0",
+ "@libsql/hrana-client": "^0.7.0",
+ "js-base64": "^3.7.5",
+ "libsql": "^0.4.4",
+ "promise-limit": "^2.7.0"
+ }
+ },
+ "node_modules/@libsql/core": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.14.0.tgz",
+ "integrity": "sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-base64": "^3.7.5"
+ }
+ },
+ "node_modules/@libsql/darwin-arm64": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.4.7.tgz",
+ "integrity": "sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@libsql/darwin-x64": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.4.7.tgz",
+ "integrity": "sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@libsql/hrana-client": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.7.0.tgz",
+ "integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@libsql/isomorphic-fetch": "^0.3.1",
+ "@libsql/isomorphic-ws": "^0.1.5",
+ "js-base64": "^3.7.5",
+ "node-fetch": "^3.3.2"
+ }
+ },
+ "node_modules/@libsql/hrana-client/node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
+ "node_modules/@libsql/isomorphic-fetch": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz",
+ "integrity": "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@libsql/isomorphic-ws": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz",
+ "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ws": "^8.5.4",
+ "ws": "^8.13.0"
+ }
+ },
+ "node_modules/@libsql/linux-arm64-gnu": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.7.tgz",
+ "integrity": "sha512-WlX2VYB5diM4kFfNaYcyhw5y+UJAI3xcMkEUJZPtRDEIu85SsSFrQ+gvoKfcVh76B//ztSeEX2wl9yrjF7BBCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/linux-arm64-musl": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.7.tgz",
+ "integrity": "sha512-6kK9xAArVRlTCpWeqnNMCoXW1pe7WITI378n4NpvU5EJ0Ok3aNTIC2nRPRjhro90QcnmLL1jPcrVwO4WD1U0xw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/linux-x64-gnu": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.7.tgz",
+ "integrity": "sha512-CMnNRCmlWQqqzlTw6NeaZXzLWI8bydaXDke63JTUCvu8R+fj/ENsLrVBtPDlxQ0wGsYdXGlrUCH8Qi9gJep0yQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/linux-x64-musl": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz",
+ "integrity": "sha512-nI6tpS1t6WzGAt1Kx1n1HsvtBbZ+jHn0m7ogNNT6pQHZQj7AFFTIMeDQw/i/Nt5H38np1GVRNsFe99eSIMs9XA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@libsql/win32-x64-msvc": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz",
+ "integrity": "sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@mongodb-js/saslprep": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
@@ -2956,6 +3118,12 @@
"sparse-bitfield": "^3.0.3"
}
},
+ "node_modules/@neon-rs/load": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz",
+ "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==",
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3859,6 +4027,15 @@
"@types/webidl-conversions": "*"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.5.13",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
+ "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/yargs": {
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
@@ -4271,14 +4448,6 @@
"node": ">=8"
}
},
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
@@ -4598,21 +4767,6 @@
}
]
},
- "node_modules/chai": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
- "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -4637,14 +4791,6 @@
"node": ">=10"
}
},
- "node_modules/check-error": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
- "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
- "engines": {
- "node": ">= 16"
- }
- },
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@@ -4790,6 +4936,15 @@
"node": ">= 8"
}
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
@@ -4825,14 +4980,6 @@
}
}
},
- "node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -5412,6 +5559,29 @@
"bser": "2.1.1"
}
},
+ "node_modules/fetch-blob": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "^1.0.0",
+ "web-streams-polyfill": "^3.0.3"
+ },
+ "engines": {
+ "node": "^12.20 || >= 14.13"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5553,6 +5723,18 @@
"node": ">= 14"
}
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -6705,6 +6887,12 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/js-base64": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
+ "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6821,6 +7009,44 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/libsql": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.4.7.tgz",
+ "integrity": "sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==",
+ "cpu": [
+ "x64",
+ "arm64",
+ "wasm32"
+ ],
+ "license": "MIT",
+ "os": [
+ "darwin",
+ "linux",
+ "win32"
+ ],
+ "dependencies": {
+ "@neon-rs/load": "^0.0.4",
+ "detect-libc": "2.0.2"
+ },
+ "optionalDependencies": {
+ "@libsql/darwin-arm64": "0.4.7",
+ "@libsql/darwin-x64": "0.4.7",
+ "@libsql/linux-arm64-gnu": "0.4.7",
+ "@libsql/linux-arm64-musl": "0.4.7",
+ "@libsql/linux-x64-gnu": "0.4.7",
+ "@libsql/linux-x64-musl": "0.4.7",
+ "@libsql/win32-x64-msvc": "0.4.7"
+ }
+ },
+ "node_modules/libsql/node_modules/detect-libc": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+ "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -6861,11 +7087,6 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
- "node_modules/loupe": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
- "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg=="
- },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -7498,14 +7719,6 @@
"node": ">=8"
}
},
- "node_modules/pathval": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
- "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
- "engines": {
- "node": ">= 14.16"
- }
- },
"node_modules/pg": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
@@ -7730,6 +7943,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/promise-limit": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
+ "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
+ "license": "ISC"
+ },
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -8682,6 +8901,27 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/typescript/package.json b/typescript/package.json
index 0b57005d..91c16c96 100644
--- a/typescript/package.json
+++ b/typescript/package.json
@@ -36,6 +36,7 @@
"@aws-sdk/client-lex-runtime-v2": "^3.621.0",
"@aws-sdk/lib-dynamodb": "^3.621.0",
"@aws-sdk/util-dynamodb": "^3.621.0",
+ "@libsql/client": "^0.14.0",
"axios": "^1.7.2",
"eslint-config-prettier": "^9.1.0",
"natural": "^7.0.7",
diff --git a/typescript/src/storage/sqlChatStorage.ts b/typescript/src/storage/sqlChatStorage.ts
new file mode 100644
index 00000000..59c3c93f
--- /dev/null
+++ b/typescript/src/storage/sqlChatStorage.ts
@@ -0,0 +1,205 @@
+import { Client, createClient } from '@libsql/client';
+import { ConversationMessage, ParticipantRole } from "../types";
+import { Logger } from "../utils/logger";
+import { ChatStorage } from "./chatStorage";
+
+export class SqlChatStorage extends ChatStorage {
+ private client: Client;
+ private initialized: Promise;
+
+ constructor(url: string, authToken?: string) {
+ super();
+ this.client = createClient({
+ url,
+ authToken,
+ });
+ this.initialized = this.initializeDatabase();
+ }
+
+ private async initializeDatabase() {
+ try {
+ // Create conversations table if it doesn't exist
+ await this.client.execute(/*sql*/`
+ CREATE TABLE IF NOT EXISTS conversations (
+ user_id TEXT NOT NULL,
+ session_id TEXT NOT NULL,
+ agent_id TEXT NOT NULL,
+ message_index INTEGER NOT NULL,
+ role TEXT NOT NULL,
+ content TEXT NOT NULL,
+ timestamp INTEGER NOT NULL,
+ PRIMARY KEY (user_id, session_id, agent_id, message_index)
+ );
+ `);
+
+ // Create index for faster queries
+ await this.client.execute(/*sql*/`
+ CREATE INDEX IF NOT EXISTS idx_conversations_lookup
+ ON conversations(user_id, session_id, agent_id);
+ `);
+ } catch (error) {
+ Logger.logger.error("Error initializing database:", error);
+ throw new Error("Database initialization error");
+ }
+ }
+
+ async saveChatMessage(
+ userId: string,
+ sessionId: string,
+ agentId: string,
+ newMessage: ConversationMessage,
+ maxHistorySize?: number
+ ): Promise {
+ try {
+ // Fetch existing conversation
+ const existingConversation = await this.fetchChat(userId, sessionId, agentId);
+
+ if (super.isConsecutiveMessage(existingConversation, newMessage)) {
+ Logger.logger.log(`> Consecutive ${newMessage.role} message detected for agent ${agentId}. Not saving.`);
+ return existingConversation;
+ }
+
+ // Begin transaction
+ await this.client.transaction().then(async (tx) => {
+ // Get the next message index
+ const nextIndexResult = await tx.execute({
+ sql: /*sql*/`
+ SELECT COALESCE(MAX(message_index) + 1, 0) as next_index
+ FROM conversations
+ WHERE user_id = ? AND session_id = ? AND agent_id = ?
+ `,
+ args: [userId, sessionId, agentId]
+ });
+
+ const nextIndex = nextIndexResult.rows[0].next_index as number;
+ const timestamp = Date.now();
+ const content = Array.isArray(newMessage.content)
+ ? JSON.stringify(newMessage.content)
+ : JSON.stringify([{ text: newMessage.content }]);
+
+ // Insert new message
+ await tx.execute({
+ sql: /*sql*/`
+ INSERT INTO conversations (
+ user_id, session_id, agent_id, message_index,
+ role, content, timestamp
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
+ `,
+ args: [
+ userId,
+ sessionId,
+ agentId,
+ nextIndex,
+ newMessage.role,
+ content,
+ timestamp
+ ]
+ });
+
+ // If maxHistorySize is set, cleanup old messages
+ if (maxHistorySize !== undefined) {
+ await tx.execute({
+ sql: /*sql*/`
+ DELETE FROM conversations
+ WHERE user_id = ?
+ AND session_id = ?
+ AND agent_id = ?
+ AND message_index <= (
+ SELECT MAX(message_index) - ?
+ FROM conversations
+ WHERE user_id = ?
+ AND session_id = ?
+ AND agent_id = ?
+ )
+ `,
+ args: [
+ userId, sessionId, agentId,
+ maxHistorySize,
+ userId, sessionId, agentId
+ ]
+ });
+ }
+ });
+
+ // Return updated conversation
+ return this.fetchChat(userId, sessionId, agentId, maxHistorySize);
+ } catch (error) {
+ Logger.logger.error("Error saving message:", error);
+ throw error;
+ }
+ }
+
+ async fetchChat(
+ userId: string,
+ sessionId: string,
+ agentId: string,
+ maxHistorySize?: number
+ ): Promise {
+ try {
+ const sql = /*sql*/`
+ SELECT role, content, timestamp
+ FROM conversations
+ WHERE user_id = ? AND session_id = ? AND agent_id = ?
+ ORDER BY message_index ${maxHistorySize ? 'DESC LIMIT ?' : 'ASC'}
+ `;
+
+ const args = maxHistorySize
+ ? [userId, sessionId, agentId, maxHistorySize]
+ : [userId, sessionId, agentId];
+
+ const result = await this.client.execute({ sql, args });
+ const messages = result.rows;
+
+ // If we used LIMIT, we need to reverse the results to maintain chronological order
+ if (maxHistorySize) messages.reverse();
+
+ return messages.map(msg => ({
+ role: msg.role as ParticipantRole,
+ content: JSON.parse(msg.content as string),
+ }));
+ } catch (error) {
+ Logger.logger.error("Error fetching chat:", error);
+ throw error;
+ }
+ }
+
+ async fetchAllChats(
+ userId: string,
+ sessionId: string
+ ): Promise {
+ try {
+ const result = await this.client.execute({
+ sql: /*sql*/`
+ SELECT role, content, timestamp, agent_id
+ FROM conversations
+ WHERE user_id = ? AND session_id = ?
+ ORDER BY timestamp ASC
+ `,
+ args: [userId, sessionId]
+ });
+
+ const messages = result.rows;
+
+ return messages.map(msg => ({
+ role: msg.role as ParticipantRole,
+ content: msg.role === ParticipantRole.ASSISTANT
+ ? [{ text: `[${msg.agent_id}] ${JSON.parse(msg.content as string)[0]?.text || ''}` }]
+ : JSON.parse(msg.content as string)
+ }));
+ } catch (error) {
+ Logger.logger.error("Error fetching all chats:", error);
+ throw error;
+ }
+ }
+
+ async waitForInitialization() {
+ if (this.client.closed) {
+ throw new Error("Database connection closed");
+ }
+ await this.initialized;
+ }
+
+ close() {
+ this.client.close();
+ }
+}