@aichatkit/storage-adapter
Version:
Base storage adapter for Hypermode ChatKit
492 lines (360 loc) • 12.8 kB
Markdown
# @aichatkit/storage-adapter
Base storage adapter abstract class for Hypermode ChatKit with backend synchronization support and
rich response items.
## Installation
```bash
npm install @aichatkit/storage-adapter
```
## Usage
This package provides the base abstract class for implementing storage adapters. You typically won't
use this package directly, but rather create or use an implementation like
`@aichatkit/localstorage-adapter`.
```typescript
import { StorageAdapter } from "@aichatkit/storage-adapter"
import { Conversation, ChatResponseItem, Message } from "@aichatkit/types"
// Example: Create a custom adapter implementation
class CustomStorageAdapter extends StorageAdapter {
private conversations: Map<string, Conversation> = new Map()
private agentMapping: Map<string, string> = new Map()
async saveConversation(conversation: Conversation): Promise<void> {
this.conversations.set(conversation.id, { ...conversation })
}
async getConversation(id: string): Promise<Conversation | null> {
return this.conversations.get(id) || null
}
async getAllConversations(): Promise<Conversation[]> {
return Array.from(this.conversations.values())
}
async deleteConversation(id: string): Promise<boolean> {
const deleted = this.conversations.delete(id)
this.agentMapping.delete(id)
return deleted
}
async addItem(conversationId: string, item: ChatResponseItem): Promise<Conversation | null> {
const conversation = this.conversations.get(conversationId)
if (!conversation) return null
conversation.items.push(item)
await this.saveConversation(conversation)
return conversation
}
async getConversationItems(conversationId: string): Promise<ChatResponseItem[]> {
// Check if we have network callbacks for backend sync
const agentId = await this.getConversationAgent(conversationId)
if (agentId && this.callbacks?.getConversationItems) {
try {
const backendItems = await this.callbacks.getConversationItems(agentId)
// Update local storage with backend data
const conversation = await this.getConversation(conversationId)
if (conversation) {
conversation.items = backendItems
await this.saveConversation(conversation)
}
return backendItems
} catch (error) {
console.error("Backend sync failed, using local data:", error)
}
}
// Fall back to local data
const conversation = await this.getConversation(conversationId)
return conversation?.items || []
}
async clearConversationHistory(conversationId: string): Promise<void> {
const agentId = await this.getConversationAgent(conversationId)
// Clear on backend if possible
if (agentId && this.callbacks?.clearConversationHistory) {
try {
await this.callbacks.clearConversationHistory(agentId)
} catch (error) {
console.error("Failed to clear backend history:", error)
}
}
// Clear locally
const conversation = await this.getConversation(conversationId)
if (conversation) {
conversation.items = []
await this.saveConversation(conversation)
}
}
async setConversationAgent(conversationId: string, agentId: string): Promise<void> {
this.agentMapping.set(conversationId, agentId)
}
async getConversationAgent(conversationId: string): Promise<string | null> {
return this.agentMapping.get(conversationId) || null
}
}
```
## Abstract Methods
All storage adapters must implement these core methods:
### saveConversation(conversation: Conversation): Promise<void>
Saves a conversation with all its response items to storage.
```typescript
abstract saveConversation(conversation: Conversation): Promise<void>;
```
**Parameters**:
- `conversation`: The conversation object with items array containing messages, tool calls, cards,
etc.
**Example**:
```typescript
await adapter.saveConversation({
id: "conv-1",
title: "My Chat",
items: [
{
id: "msg_1",
type: "message",
content: "Hello",
role: "user",
},
{
id: "tool_1",
type: "tool_call",
toolCall: {
id: "call_1",
name: "get_weather",
arguments: { location: "NYC" },
status: "completed",
result: { temperature: "22°C" },
},
},
],
})
```
### getConversation(id: string): Promise<Conversation | null>
Retrieves a conversation from storage by ID with all its response items.
```typescript
abstract getConversation(id: string): Promise<Conversation | null>;
```
**Parameters**:
- `id`: ID of the conversation to retrieve
**Returns**: Promise resolving to the conversation with all item types or null if not found
### getAllConversations(): Promise<Conversation[]>
Retrieves all conversations from storage with their complete response items.
```typescript
abstract getAllConversations(): Promise<Conversation[]>;
```
**Returns**: Promise resolving to an array of all conversations with their items
### deleteConversation(id: string): Promise<boolean>
Deletes a conversation from storage.
```typescript
abstract deleteConversation(id: string): Promise<boolean>;
```
**Parameters**:
- `id`: ID of the conversation to delete
**Returns**: Promise resolving to true if successful, false otherwise
### addItem(conversationId: string, item: ChatResponseItem): Promise<Conversation | null>
Adds any type of response item to a conversation.
```typescript
abstract addItem(conversationId: string, item: ChatResponseItem): Promise<Conversation | null>;
```
**Parameters**:
- `conversationId`: ID of the conversation
- `item`: Response item to add (message, tool call, card, etc.)
**Returns**: Promise resolving to the updated conversation or null if not found
**Example**:
```typescript
// Add a message
await adapter.addItem("conv-1", {
id: "msg_2",
type: "message",
content: "How's the weather?",
role: "user",
})
// Add a tool call
await adapter.addItem("conv-1", {
id: "tool_2",
type: "tool_call",
toolCall: {
id: "call_2",
name: "get_weather",
arguments: { location: "NYC" },
status: "executing",
},
})
```
### addMessage(conversationId: string, message: Message): Promise<Conversation | null>
Convenience method for adding message items.
```typescript
async addMessage(conversationId: string, message: Message): Promise<Conversation | null>
```
This method converts a Message to a MessageItem and calls `addItem()`.
## Backend Synchronization Methods
These methods enable synchronization with backend agents:
### getConversationItems(conversationId: string): Promise<ChatResponseItem[]>
Gets all conversation items with backend synchronization.
```typescript
abstract getConversationItems(conversationId: string): Promise<ChatResponseItem[]>;
```
This method should:
1. Check if a backend agent exists for the conversation
2. Sync with backend if available to get all item types
3. Fall back to local storage if backend sync fails
### getConversationHistory(conversationId: string): Promise<Message[]>
Gets conversation history (messages only) with backend synchronization.
```typescript
async getConversationHistory(conversationId: string): Promise<Message[]>
```
This convenience method filters message items from all response items.
### clearConversationHistory(conversationId: string): Promise<void>
Clears conversation history both locally and on backend.
```typescript
abstract clearConversationHistory(conversationId: string): Promise<void>;
```
### setConversationAgent(conversationId: string, agentId: string): Promise<void>
Maps a conversation to a backend agent ID.
```typescript
abstract setConversationAgent(conversationId: string, agentId: string): Promise<void>;
```
### getConversationAgent(conversationId: string): Promise<string | null>
Gets the backend agent ID for a conversation.
```typescript
abstract getConversationAgent(conversationId: string): Promise<string | null>;
```
## Optional Methods
### initialize(config?: Record<string, any>): Promise<void>
Optional initialization method.
```typescript
async initialize(config?: Record<string, any>): Promise<void> {
return Promise.resolve();
}
```
### syncAllConversationsWithBackend(): Promise<void>
Syncs all conversations with backend (called on app load).
```typescript
async syncAllConversationsWithBackend?(): Promise<void> {
return Promise.resolve();
}
```
## Network Callbacks
Storage adapters can receive network callbacks for backend synchronization:
### StorageAdapterCallbacks Interface
```typescript
interface StorageAdapterCallbacks {
getConversationItems?: (agentId: string) => Promise<ChatResponseItem[]>
clearConversationHistory?: (agentId: string) => Promise<void>
}
```
### setNetworkCallbacks(callbacks: StorageAdapterCallbacks): void
Sets network callbacks for backend communication.
```typescript
import { NetworkAdapter } from "@aichatkit/network-adapter"
const networkAdapter = new SomeNetworkAdapter()
const storageAdapter = new SomeStorageAdapter()
// Connect storage with network for syncing
storageAdapter.setNetworkCallbacks({
getConversationItems: (agentId) => networkAdapter.getConversationItems(agentId),
clearConversationHistory: (agentId) => networkAdapter.clearConversationHistory(agentId),
})
```
## Data Types
### Conversation
```typescript
interface Conversation {
id: string
title: string
items: ChatResponseItem[] // Mixed array of messages, tool calls, cards, etc.
}
```
### ChatResponseItem
Union type supporting multiple item types:
```typescript
type ChatResponseItem = MessageItem | ToolCallItem | CardItem
interface MessageItem {
id: string | number
type: "message"
content: string
role: "user" | "assistant"
timestamp?: string
}
interface ToolCallItem {
id: string | number
type: "tool_call"
toolCall: {
id: string
name: string
arguments: Record<string, any>
status: "pending" | "executing" | "completed" | "error"
result?: any
error?: string
}
}
interface CardItem {
id: string | number
type: "card"
card: {
id: string
type: string
title?: string
content: Record<string, any>
actions?: CardAction[]
}
}
```
### Message
```typescript
interface Message {
id: string | number
content: string
role: "user" | "assistant"
timestamp?: string
}
```
## Agent-Conversation Mapping
Storage adapters maintain a mapping between conversations and backend agents:
```typescript
// When creating a new conversation with an agent
await storageAdapter.setConversationAgent("conv-1", "agent-123")
// When retrieving the agent for a conversation
const agentId = await storageAdapter.getConversationAgent("conv-1")
// When deleting a conversation, also remove the agent mapping
await storageAdapter.deleteConversation("conv-1") // Should also remove agent mapping
```
## Synchronization Flow
1. **App Initialization**: Call `syncAllConversationsWithBackend()` to get latest state
2. **Conversation Switch**: Sync specific conversation when user switches to it
3. **Item History**: Always try backend first, fall back to local storage
4. **Background Sync**: Periodically sync to keep data fresh
### Example Sync Implementation
```typescript
async getConversationItems(conversationId: string): Promise<ChatResponseItem[]> {
const agentId = await this.getConversationAgent(conversationId);
if (agentId && this.callbacks?.getConversationItems) {
try {
// Try to get latest from backend
const backendItems = await this.callbacks.getConversationItems(agentId);
// Update local storage with backend data
const conversation = await this.getConversation(conversationId);
if (conversation) {
conversation.items = backendItems;
await this.saveConversation(conversation);
}
return backendItems;
} catch (error) {
console.error('Backend sync failed:', error);
// Continue to local fallback
}
}
// Fall back to local storage
const conversation = await this.getConversation(conversationId);
return conversation?.items || [];
}
```
## Available Implementations
- **[@aichatkit/localstorage-adapter](../localstorage-adapter)**: Browser localStorage
implementation
- **Custom implementations**: Create your own for databases, cloud storage, etc.
## Testing
Mock storage adapters for testing:
```typescript
class MockStorageAdapter extends StorageAdapter {
private data: Map<string, Conversation> = new Map()
async saveConversation(conversation: Conversation): Promise<void> {
this.data.set(conversation.id, { ...conversation })
}
async getConversation(id: string): Promise<Conversation | null> {
return this.data.get(id) || null
}
// ... implement other methods
}
```
## License
[MIT](../../LICENSE) © Hypermode