mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-24 00:51:46 +00:00
Merge branch 'main' into 04-03-refactor_mastra_to_object-based_api_v2.0.0_
This commit is contained in:
commit
de31895f61
42 changed files with 4794 additions and 215 deletions
37
.github/workflows/publish-agent-framework-python.yml
vendored
Normal file
37
.github/workflows/publish-agent-framework-python.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Publish Agent Framework Python
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "packages/agent-framework-python/pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/agent-framework-python
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: pip install hatchling build
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: packages/agent-framework-python/dist/
|
||||
37
.github/workflows/publish-cartesia-sdk-python.yml
vendored
Normal file
37
.github/workflows/publish-cartesia-sdk-python.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Publish Cartesia SDK Python
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "packages/cartesia-sdk-python/pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/cartesia-sdk-python
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: pip install hatchling build
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: packages/cartesia-sdk-python/dist/
|
||||
44
.github/workflows/publish-memory-graph.yml
vendored
Normal file
44
.github/workflows/publish-memory-graph.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Publish Memory Graph
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "packages/memory-graph/package.json"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/memory-graph
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --access public --verbose
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
37
.github/workflows/publish-openai-sdk-python.yml
vendored
Normal file
37
.github/workflows/publish-openai-sdk-python.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Publish OpenAI SDK Python
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "packages/openai-sdk-python/pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/openai-sdk-python
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: pip install hatchling build
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: packages/openai-sdk-python/dist/
|
||||
37
.github/workflows/publish-pipecat-sdk-python.yml
vendored
Normal file
37
.github/workflows/publish-pipecat-sdk-python.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Publish Pipecat SDK Python
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "packages/pipecat-sdk-python/pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/pipecat-sdk-python
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: pip install hatchling build
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: packages/pipecat-sdk-python/dist/
|
||||
44
.github/workflows/publish-tools.yml
vendored
Normal file
44
.github/workflows/publish-tools.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Publish Tools
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "packages/tools/package.json"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/tools
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --access public --verbose
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
@ -6,6 +6,10 @@ description: "API updates, new endpoints, and SDK releases"
|
|||
|
||||
API updates, new endpoints, SDK releases, and developer-focused features.
|
||||
|
||||
## April 13, 2026
|
||||
|
||||
- **Google Drive scoped sync:** New connections default to a **hosted folder/file picker** after OAuth; only chosen items sync. Use `metadata.syncScope: "full"` to sync the whole Drive. Import jobs **skip** scoped connections until a selection exists.
|
||||
|
||||
## March 18, 2026
|
||||
|
||||
- **Supermemory CLI:** New command-line tool for managing memories, documents, profiles, tags, connectors, and API keys directly from the terminal.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@ title: "Changelog"
|
|||
description: "New updates and improvements to Supermemory"
|
||||
---
|
||||
|
||||
<Update label="April 13, 2026" tags={["Integrations", "API"]}>
|
||||
|
||||
### Google Drive: scoped sync by default
|
||||
|
||||
New Google Drive connections default to **folder and file** scope: after OAuth, users complete a hosted picker; only selected items sync. Set `metadata.syncScope` to `"full"` on connection creation to sync the entire Drive without the picker. Scoped connections without a saved selection are skipped by import jobs until setup is finished.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="March 18, 2026" tags={["API", "SDK", "Console", "CLI"]}>
|
||||
|
||||
### Supermemory CLI
|
||||
|
|
|
|||
|
|
@ -6,6 +6,18 @@ icon: "google-drive"
|
|||
|
||||
Connect Google Drive to sync documents into your Supermemory knowledge base with OAuth authentication and custom app support.
|
||||
|
||||
## Sync scope
|
||||
|
||||
**Default for new connections:** after OAuth, the user completes a **folder and file** picker (Google Docs, Sheets, Slides, and PDFs). Only items they select are synced and updated until they change the selection (for example from the Supermemory console).
|
||||
|
||||
**Whole Drive:** set `metadata.syncScope` to `"full"` when creating the connection so the entire Drive syncs without the picker.
|
||||
|
||||
**Explicit scoped mode:** set `metadata.syncScope` to `"selected"` for the picker flow, or rely on the default for new connects.
|
||||
|
||||
<Note>
|
||||
If you use scoped sync and the user has not finished the picker yet, **scheduled or manual import may skip that connection** until a selection is saved on the connection.
|
||||
</Note>
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### 1. Create Google Drive Connection
|
||||
|
|
@ -25,7 +37,8 @@ Connect Google Drive to sync documents into your Supermemory knowledge base with
|
|||
documentLimit: 3000,
|
||||
metadata: {
|
||||
source: 'google-drive',
|
||||
department: 'engineering'
|
||||
department: 'engineering',
|
||||
syncScope: 'selected'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -48,7 +61,8 @@ Connect Google Drive to sync documents into your Supermemory knowledge base with
|
|||
document_limit=3000,
|
||||
metadata={
|
||||
'source': 'google-drive',
|
||||
'department': 'engineering'
|
||||
'department': 'engineering',
|
||||
'syncScope': 'selected',
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -68,16 +82,21 @@ Connect Google Drive to sync documents into your Supermemory knowledge base with
|
|||
"documentLimit": 3000,
|
||||
"metadata": {
|
||||
"source": "google-drive",
|
||||
"department": "engineering"
|
||||
"department": "engineering",
|
||||
"syncScope": "selected"
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Note>
|
||||
For **whole Drive** sync, include `"syncScope": "full"` in `metadata` on the same `POST /v3/connections/google-drive` request instead of `"selected"`.
|
||||
</Note>
|
||||
|
||||
### 2. Handle OAuth Callback
|
||||
|
||||
After user grants permissions, Google redirects to your callback URL. The connection is automatically established.
|
||||
After the user grants permissions, Google redirects through Supermemory to finish the connection. With **scoped** sync (`syncScope` omitted or `"selected"`), the user is sent to Supermemory’s **hosted file and folder picker**; they must complete that step before imports run. With **`syncScope: "full"`**, Supermemory redirects to your `redirectUrl` (or returns connection details) **without** the picker. You can open the picker again later for an existing connection (Supermemory console, or `POST /v3/connections/{connectionId}/google-drive/hosted-picker` with an authenticated admin session).
|
||||
|
||||
### 3. Check Connection Status
|
||||
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@
|
|||
"integrations/openai-agents-sdk",
|
||||
"integrations/agent-framework",
|
||||
"integrations/mastra",
|
||||
"integrations/voltagent",
|
||||
"integrations/langchain",
|
||||
"integrations/crewai",
|
||||
"integrations/agno",
|
||||
|
|
|
|||
345
apps/docs/integrations/cartesia.mdx
Normal file
345
apps/docs/integrations/cartesia.mdx
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
---
|
||||
title: "Cartesia"
|
||||
sidebarTitle: "Cartesia (Voice)"
|
||||
description: "Integrate Supermemory with Cartesia for conversational memory in voice AI agents"
|
||||
icon: "/images/cartesia.svg"
|
||||
---
|
||||
|
||||
Supermemory integrates with [Cartesia](https://cartesia.ai/agents), providing long-term memory capabilities for voice AI agents. Your Cartesia applications will remember past conversations and provide personalized responses based on user history.
|
||||
|
||||
## Installation
|
||||
|
||||
To use Supermemory with Cartesia, install the required dependencies:
|
||||
|
||||
```bash
|
||||
pip install supermemory-cartesia
|
||||
```
|
||||
|
||||
Set up your API key as an environment variable:
|
||||
|
||||
```bash
|
||||
export SUPERMEMORY_API_KEY=your_supermemory_api_key
|
||||
```
|
||||
|
||||
You can obtain an API key from [console.supermemory.ai](https://console.supermemory.ai).
|
||||
|
||||
## Configuration
|
||||
|
||||
Supermemory integration is provided through the `SupermemoryCartesiaAgent` wrapper class:
|
||||
|
||||
```python
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent
|
||||
from line.llm_agent import LlmAgent, LlmConfig
|
||||
|
||||
# Create base LLM agent
|
||||
base_agent = LlmAgent(
|
||||
model="anthropic/claude-haiku-4-5-20251001",
|
||||
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
||||
config=LlmConfig(
|
||||
system_prompt="""You are a helpful voice assistant with memory.""",
|
||||
introduction="Hello! Great to talk with you again!",
|
||||
),
|
||||
)
|
||||
|
||||
# Wrap with Supermemory
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"),
|
||||
container_tag="user-123",
|
||||
custom_id="session-456", # Required: groups all messages in same document
|
||||
config=SupermemoryCartesiaAgent.MemoryConfig(
|
||||
mode="full", # "profile" | "query" | "full"
|
||||
search_limit=10, # Max memories to retrieve
|
||||
search_threshold=0.3, # Relevance threshold (0.0-1.0)
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Agent Wrapper Pattern
|
||||
|
||||
The `SupermemoryCartesiaAgent` wraps your existing `LlmAgent` to add memory capabilities:
|
||||
|
||||
```python
|
||||
from line.voice_agent_app import VoiceAgentApp
|
||||
|
||||
async def get_agent(env, call_request):
|
||||
# Extract container_tag from call metadata (typically user ID)
|
||||
container_tag = call_request.metadata.get("user_id", "default-user")
|
||||
|
||||
# Create base agent
|
||||
base_agent = LlmAgent(...)
|
||||
|
||||
# Wrap with memory
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
container_tag=container_tag,
|
||||
custom_id=call_request.call_id, # Required: groups all messages in same document
|
||||
)
|
||||
|
||||
return memory_agent
|
||||
|
||||
# Create voice agent app
|
||||
app = VoiceAgentApp(get_agent=get_agent)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
When integrated with Cartesia Line, Supermemory provides two key functionalities:
|
||||
|
||||
### 1. Memory Retrieval
|
||||
|
||||
When a `UserTurnEnded` event is detected, Supermemory retrieves relevant memories:
|
||||
|
||||
- **Static Profile**: Persistent facts about the user
|
||||
- **Dynamic Profile**: Recent context and preferences
|
||||
- **Search Results**: Semantically relevant past memories
|
||||
|
||||
### 2. Context Enhancement
|
||||
|
||||
Retrieved memories are formatted and injected into the agent's system prompt before processing, giving the model awareness of past conversations.
|
||||
|
||||
### 3. Background Storage
|
||||
|
||||
Conversations are automatically stored in Supermemory (non-blocking) for future retrieval.
|
||||
|
||||
## Memory Modes
|
||||
|
||||
| Mode | Static Profile | Dynamic Profile | Search Results | Use Case |
|
||||
| ----------- | -------------- | --------------- | -------------- | ------------------------------ |
|
||||
| `"profile"` | Yes | Yes | No | Personalization without search |
|
||||
| `"query"` | No | No | Yes | Finding relevant past context |
|
||||
| `"full"` | Yes | Yes | Yes | Complete memory (default) |
|
||||
|
||||
## Configuration Options
|
||||
|
||||
You can customize how memories are retrieved and used:
|
||||
|
||||
### MemoryConfig
|
||||
|
||||
```python
|
||||
SupermemoryCartesiaAgent.MemoryConfig(
|
||||
mode="full", # Memory mode (default: "full")
|
||||
search_limit=10, # Max memories to retrieve (default: 10)
|
||||
search_threshold=0.1, # Similarity threshold 0.0-1.0 (default: 0.1)
|
||||
system_prompt="Based on previous conversations:\n\n",
|
||||
)
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| ------------------ | ----- | -------------------------------------- | ---------------------------------------------------------- |
|
||||
| `search_limit` | int | 10 | Maximum number of memories to retrieve per query |
|
||||
| `search_threshold` | float | 0.1 | Minimum similarity threshold for memory retrieval |
|
||||
| `mode` | str | "full" | Memory retrieval mode: `"profile"`, `"query"`, or `"full"` |
|
||||
| `system_prompt` | str | "Based on previous conversations:\n\n" | Prefix text for memory context |
|
||||
|
||||
### Agent Parameters
|
||||
|
||||
```python
|
||||
SupermemoryCartesiaAgent(
|
||||
agent=base_agent, # Required: Cartesia Line LlmAgent
|
||||
container_tag="user-123", # Required: Primary container tag (e.g., user ID)
|
||||
custom_id="session-456", # Required: Groups all messages in same document
|
||||
add_memory="always", # Optional: "always" (default) or "never"
|
||||
container_tags=["org-acme", "prod"], # Optional: Additional tags
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"), # Optional: defaults to env var
|
||||
config=MemoryConfig(...), # Optional: memory configuration
|
||||
base_url=None, # Optional: custom API endpoint
|
||||
)
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------------- | ------------ | -------- | ------------------------------------------------------------------ |
|
||||
| `agent` | LlmAgent | **Yes** | The Cartesia Line agent to wrap |
|
||||
| `container_tag` | str | **Yes** | Primary container tag for memory scoping (e.g., user ID) |
|
||||
| `custom_id` | str | **Yes** | Groups all messages in the same document (e.g., call ID, conversation ID) |
|
||||
| `add_memory` | str | No | Memory persistence mode: "always" (default) or "never" |
|
||||
| `container_tags`| List[str] | No | Additional container tags for organization (e.g., ["org", "prod"]) |
|
||||
| `api_key` | str | No | Supermemory API key (or set `SUPERMEMORY_API_KEY` env var) |
|
||||
| `config` | MemoryConfig | No | Advanced configuration |
|
||||
| `base_url` | str | No | Custom API endpoint |
|
||||
|
||||
## Container Tags
|
||||
|
||||
Container tags allow you to organize memories across multiple dimensions:
|
||||
|
||||
```python
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
container_tag="user-alice", # Primary: user ID
|
||||
container_tags=["org-acme", "prod"], # Additional: organization, environment
|
||||
)
|
||||
```
|
||||
|
||||
Memories are stored with all tags:
|
||||
```json
|
||||
{
|
||||
"content": "User: What's the weather?\nAssistant: It's sunny today!",
|
||||
"container_tags": ["user-alice", "org-acme", "prod"],
|
||||
"metadata": { "platform": "cartesia" }
|
||||
}
|
||||
```
|
||||
|
||||
## Automatic Document Grouping
|
||||
|
||||
The SDK **automatically groups all messages from the same conversation** into a single Supermemory document using `custom_id`:
|
||||
|
||||
```python
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
container_tag="user-alice",
|
||||
custom_id=call_request.call_id, # Required: Groups all messages together
|
||||
)
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- The `custom_id` parameter groups all messages into the same Supermemory document
|
||||
- Typically you use the call ID or conversation ID from Cartesia
|
||||
- All messages from that conversation are appended to the same document
|
||||
- This ensures conversation continuity and proper memory generation
|
||||
|
||||
## Example: Basic Voice Agent with Memory
|
||||
|
||||
Here's a complete example of a Cartesia Line voice agent with Supermemory integration:
|
||||
|
||||
```python
|
||||
import os
|
||||
from line.llm_agent import LlmAgent, LlmConfig
|
||||
from line.voice_agent_app import VoiceAgentApp
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent
|
||||
|
||||
async def get_agent(env, call_request):
|
||||
# Extract container_tag from call metadata (typically user ID)
|
||||
container_tag = call_request.metadata.get("user_id", "default-user")
|
||||
|
||||
# Create base LLM agent
|
||||
base_agent = LlmAgent(
|
||||
model="anthropic/claude-haiku-4-5-20251001",
|
||||
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
||||
config=LlmConfig(
|
||||
system_prompt="""You are a helpful voice assistant with memory.""",
|
||||
introduction="Hello! Great to talk with you again!",
|
||||
),
|
||||
)
|
||||
|
||||
# Wrap with Supermemory
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"),
|
||||
container_tag=container_tag,
|
||||
custom_id=call_request.call_id, # Required: Groups all messages
|
||||
)
|
||||
|
||||
return memory_agent
|
||||
|
||||
# Create voice agent app
|
||||
app = VoiceAgentApp(get_agent=get_agent)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
## Example: Advanced Agent with Tools
|
||||
|
||||
Here's an example with custom tools and multi-tag support:
|
||||
|
||||
```python
|
||||
import os
|
||||
from line.llm_agent import LlmAgent, LlmConfig
|
||||
from line.tools import LoopbackTool
|
||||
from line.voice_agent_app import VoiceAgentApp
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent
|
||||
|
||||
# Define custom tool
|
||||
async def get_weather(location: str) -> str:
|
||||
return f"The weather in {location} is sunny, 72°F"
|
||||
|
||||
weather_tool = LoopbackTool(
|
||||
name="get_weather",
|
||||
description="Get current weather for a location",
|
||||
function=get_weather
|
||||
)
|
||||
|
||||
async def get_agent(env, call_request):
|
||||
container_tag = call_request.metadata.get("user_id", "default-user")
|
||||
org_id = call_request.metadata.get("org_id")
|
||||
|
||||
# Create LLM agent with tools
|
||||
base_agent = LlmAgent(
|
||||
model="gemini/gemini-2.5-flash-preview-09-2025",
|
||||
tools=[weather_tool],
|
||||
config=LlmConfig(
|
||||
system_prompt="You are a personal assistant with memory and tools.",
|
||||
introduction="Hi! How can I help you today?"
|
||||
)
|
||||
)
|
||||
|
||||
# Wrap with Supermemory
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"),
|
||||
container_tag=container_tag,
|
||||
custom_id=call_request.call_id, # Required: Groups all messages
|
||||
container_tags=[org_id] if org_id else None,
|
||||
config=SupermemoryCartesiaAgent.MemoryConfig(
|
||||
mode="full",
|
||||
search_limit=15,
|
||||
search_threshold=0.15,
|
||||
)
|
||||
)
|
||||
|
||||
return memory_agent
|
||||
|
||||
app = VoiceAgentApp(get_agent=get_agent)
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
To deploy to Cartesia Line, create a `main.py` file in your project root:
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add src to path for local imports
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||
|
||||
from line.llm_agent import LlmAgent, LlmConfig
|
||||
from line.voice_agent_app import VoiceAgentApp
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent
|
||||
|
||||
async def get_agent(env, call_request):
|
||||
"""Create a memory-enabled voice agent."""
|
||||
container_tag = call_request.metadata.get("user_id", "default-user")
|
||||
|
||||
base_agent = LlmAgent(
|
||||
model="anthropic/claude-haiku-4-5-20251001",
|
||||
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
||||
config=LlmConfig(
|
||||
system_prompt="""You are a helpful voice assistant with memory.
|
||||
You remember past conversations and can reference them naturally.
|
||||
Keep responses brief and conversational.""",
|
||||
introduction="Hello! Great to talk with you again!",
|
||||
),
|
||||
)
|
||||
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"),
|
||||
container_tag=container_tag,
|
||||
custom_id=call_request.call_id, # Required: Groups all messages
|
||||
)
|
||||
|
||||
return memory_agent
|
||||
|
||||
app = VoiceAgentApp(get_agent=get_agent)
|
||||
```
|
||||
|
||||
Then deploy with:
|
||||
|
||||
```bash
|
||||
cartesia deploy
|
||||
```
|
||||
|
||||
Make sure to set these environment variables in your Cartesia deployment:
|
||||
- `SUPERMEMORY_API_KEY` - Your Supermemory API key
|
||||
- `ANTHROPIC_API_KEY` - Your Anthropic API key (or the key for your chosen LLM provider)
|
||||
170
apps/docs/integrations/voltagent.mdx
Normal file
170
apps/docs/integrations/voltagent.mdx
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
---
|
||||
title: "VoltAgent"
|
||||
sidebarTitle: "VoltAgent"
|
||||
description: "Integrate Supermemory with VoltAgent for long-term memory in AI agents"
|
||||
icon: "bolt"
|
||||
---
|
||||
|
||||
Supermemory integrates with [VoltAgent](https://github.com/VoltAgent/voltagent), providing long-term memory capabilities for AI agents. Your VoltAgent applications will remember past conversations and provide personalized responses based on user history.
|
||||
|
||||
<Card title="@supermemory/tools on npm" icon="npm" href="https://www.npmjs.com/package/@supermemory/tools">
|
||||
Check out the NPM page for more details
|
||||
</Card>
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @supermemory/tools @voltagent/core
|
||||
```
|
||||
|
||||
Set up your API key as an environment variable:
|
||||
|
||||
```bash
|
||||
export SUPERMEMORY_API_KEY=your_supermemory_api_key
|
||||
```
|
||||
|
||||
You can obtain an API key from [console.supermemory.ai](https://console.supermemory.ai).
|
||||
|
||||
## Quick Start
|
||||
|
||||
Supermemory provides a `withSupermemory` wrapper that enhances any VoltAgent agent config with automatic memory retrieval and storage:
|
||||
|
||||
```typescript
|
||||
import { withSupermemory } from "@supermemory/tools/voltagent"
|
||||
import { Agent } from "@voltagent/core"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
// Create an agent with Supermemory memory capabilities
|
||||
const configWithMemory = withSupermemory({
|
||||
agentConfig: {
|
||||
name: "my-agent",
|
||||
instructions: "You are a helpful assistant.",
|
||||
model: openai("gpt-4o"),
|
||||
},
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
})
|
||||
|
||||
const agent = new Agent(configWithMemory)
|
||||
|
||||
// Memories are automatically injected and saved
|
||||
const result = await agent.generateText({
|
||||
messages: [{ role: "user", content: "What's my name?" }],
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Memory saving is enabled by default** in the VoltAgent integration. To disable it:
|
||||
|
||||
```typescript
|
||||
const configWithMemory = withSupermemory({
|
||||
agentConfig: {
|
||||
name: "my-agent",
|
||||
instructions: "You are a helpful assistant.",
|
||||
model: openai("gpt-4o"),
|
||||
},
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
addMemory: "never",
|
||||
})
|
||||
```
|
||||
</Note>
|
||||
|
||||
## How It Works
|
||||
|
||||
When integrated with VoltAgent, Supermemory hooks into two lifecycle events:
|
||||
|
||||
### 1. Memory Retrieval (onPrepareMessages)
|
||||
|
||||
Before each LLM call, Supermemory automatically:
|
||||
- Extracts the user's latest message
|
||||
- Searches for relevant memories scoped to the `containerTag`
|
||||
- Injects retrieved memories into the system prompt
|
||||
|
||||
### 2. Conversation Saving (onEnd)
|
||||
|
||||
After each agent response, the conversation is saved to Supermemory for future retrieval. This requires a `customId` to be set.
|
||||
|
||||
## Memory Modes
|
||||
|
||||
| Mode | Description | Use Case |
|
||||
| ----------- | ------------------------------------------- | ------------------------------ |
|
||||
| `"profile"` | Retrieves the user's complete profile | Personalization without search |
|
||||
| `"query"` | Searches memories based on the user's message | Finding relevant past context |
|
||||
| `"full"` | Combines profile AND query-based search | Complete memory (recommended) |
|
||||
|
||||
```typescript
|
||||
const configWithMemory = withSupermemory({
|
||||
agentConfig: {
|
||||
name: "my-agent",
|
||||
instructions: "You are a helpful assistant.",
|
||||
model: openai("gpt-4o"),
|
||||
},
|
||||
containerTag: "user-123",
|
||||
customId: "conversation-123",
|
||||
mode: "full",
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
```typescript
|
||||
const configWithMemory = withSupermemory({
|
||||
// Agent configuration
|
||||
agentConfig: {
|
||||
name: "my-agent",
|
||||
instructions: "You are a helpful assistant.",
|
||||
model: openai("gpt-4o"),
|
||||
},
|
||||
|
||||
// Required
|
||||
containerTag: "user-123", // User/project ID for scoping memories
|
||||
|
||||
// Memory behavior
|
||||
mode: "full", // "profile" | "query" | "full"
|
||||
addMemory: "always", // "always" | "never"
|
||||
customId: "conv-456", // Groups messages into a conversation
|
||||
|
||||
// Search tuning
|
||||
searchMode: "hybrid", // "memories" | "documents" | "hybrid"
|
||||
threshold: 0.1, // 0.0-1.0 (higher = more accurate)
|
||||
limit: 10, // Max results to return
|
||||
rerank: true, // Rerank for best relevance
|
||||
rewriteQuery: false, // AI-rewrite query (+400ms latency)
|
||||
|
||||
// Context
|
||||
entityContext: "This is John, a software engineer", // Guides memory extraction (max 1500 chars)
|
||||
metadata: { source: "voltagent" }, // Attached to saved conversations
|
||||
|
||||
// API
|
||||
apiKey: "sk-...", // Falls back to SUPERMEMORY_API_KEY env var
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
})
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| ----------------- | -------- | ------------ | -------------------------------------------------------- |
|
||||
| `agentConfig` | object | **required** | VoltAgent agent configuration object |
|
||||
| `containerTag` | string | **required** | User/project ID for scoping memories |
|
||||
| `mode` | string | `"profile"` | Memory retrieval mode |
|
||||
| `addMemory` | string | `"always"` | Whether to save conversations after each response |
|
||||
| `customId` | string | **required** | Custom ID to group messages into a conversation |
|
||||
| `searchMode` | string | — | `"memories"`, `"documents"`, or `"hybrid"` |
|
||||
| `threshold` | number | `0.1` | Similarity threshold (0 = more results, 1 = more accurate) |
|
||||
| `limit` | number | `10` | Maximum number of memory results |
|
||||
| `rerank` | boolean | `false` | Rerank results for relevance |
|
||||
| `rewriteQuery` | boolean | `false` | AI-rewrite query for better results (+400ms) |
|
||||
| `entityContext` | string | — | Context for memory extraction (max 1500 chars) |
|
||||
| `metadata` | object | — | Custom metadata attached to saved conversations |
|
||||
| `promptTemplate` | function | — | Custom function to format memory data into prompt |
|
||||
|
||||
## Search Modes
|
||||
|
||||
The `searchMode` option controls what type of results are searched:
|
||||
|
||||
| Mode | Description |
|
||||
| ------------- | ------------------------------------------------------ |
|
||||
| `"memories"` | Search only memory entries (atomic facts about the user) |
|
||||
| `"documents"` | Search only document chunks |
|
||||
| `"hybrid"` | Search both memories AND document chunks (recommended) |
|
||||
|
||||
|
|
@ -59,6 +59,7 @@ app.get("/", (c) => {
|
|||
// MCP clients use this to discover the authorization server
|
||||
app.get("/.well-known/oauth-protected-resource", (c) => {
|
||||
const apiUrl = c.env.API_URL || DEFAULT_API_URL
|
||||
|
||||
const host = c.req.header("x-forwarded-host") || c.req.header("host")
|
||||
const proto = c.req.header("x-forwarded-proto") || "https"
|
||||
const resourceUrl = host ? `${proto}://${host}` : "https://mcp.supermemory.ai"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useOnboardingContext, type MemoryFormData } from "../layout"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
export const WELCOME_STEPS = [
|
||||
|
|
@ -51,13 +52,17 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
|
|||
const searchParams = useSearchParams()
|
||||
const { name, setName, memoryFormData, setMemoryFormData } =
|
||||
useOnboardingContext()
|
||||
const { organizations } = useAuth()
|
||||
const hasOrgs = Array.isArray(organizations) && organizations.length > 0
|
||||
|
||||
const stepParam = searchParams.get("step")
|
||||
const currentStep: WelcomeStep = WELCOME_STEPS.includes(
|
||||
const resolvedStep: WelcomeStep = WELCOME_STEPS.includes(
|
||||
stepParam as WelcomeStep,
|
||||
)
|
||||
? (stepParam as WelcomeStep)
|
||||
: "input"
|
||||
const currentStep: WelcomeStep =
|
||||
resolvedStep === "input" && hasOrgs ? "greeting" : resolvedStep
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [showWelcomeContent, setShowWelcomeContent] = useState(false)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useRef } from "react"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { cn } from "@lib/utils"
|
||||
|
||||
|
|
@ -102,66 +103,70 @@ export default function WelcomePage() {
|
|||
} = useWelcomeContext()
|
||||
|
||||
const { refetchOrganizations, setActiveOrg } = useAuth()
|
||||
const submitLockRef = useRef(false)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
localStorage.setItem("username", name)
|
||||
if (name.trim()) {
|
||||
setIsSubmitting(true)
|
||||
const trimmed = name.trim()
|
||||
if (!trimmed) return
|
||||
if (submitLockRef.current) return
|
||||
submitLockRef.current = true
|
||||
localStorage.setItem("username", trimmed)
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
await authClient.updateUser({
|
||||
displayUsername: name.trim(),
|
||||
username: generateUsername(name.trim()),
|
||||
try {
|
||||
await authClient.updateUser({
|
||||
displayUsername: trimmed,
|
||||
username: generateUsername(trimmed),
|
||||
})
|
||||
|
||||
const refetchResult = await refetchOrganizations()
|
||||
const refetchData = (
|
||||
refetchResult as { data?: unknown[] | null | undefined }
|
||||
)?.data
|
||||
const existingOrgs = Array.isArray(refetchData) ? refetchData : []
|
||||
|
||||
if (existingOrgs.length > 0) {
|
||||
analytics.onboardingNameSubmitted({
|
||||
name_length: trimmed.length,
|
||||
})
|
||||
goToStep("greeting")
|
||||
return
|
||||
}
|
||||
|
||||
const refetchResult = await refetchOrganizations()
|
||||
const refetchData = (
|
||||
refetchResult as { data?: unknown[] | null | undefined }
|
||||
)?.data
|
||||
const existingOrgs = Array.isArray(refetchData) ? refetchData : []
|
||||
|
||||
if (existingOrgs.length > 0) {
|
||||
analytics.onboardingNameSubmitted({
|
||||
name_length: name.trim().length,
|
||||
})
|
||||
goToStep("greeting")
|
||||
return
|
||||
}
|
||||
|
||||
const uniqueSlug = generateOrgSlug(name.trim())
|
||||
const completedAt = new Date().toISOString()
|
||||
const newOrg = await authClient.organization.create({
|
||||
name: name.trim(),
|
||||
slug: uniqueSlug,
|
||||
metadata: {
|
||||
signupSource: "consumer",
|
||||
webOnboarding: {
|
||||
completedAt: null,
|
||||
steps: {
|
||||
welcomeInput: {
|
||||
startedAt: completedAt,
|
||||
completedAt,
|
||||
data: {},
|
||||
},
|
||||
const uniqueSlug = generateOrgSlug(trimmed)
|
||||
const completedAt = new Date().toISOString()
|
||||
const newOrg = await authClient.organization.create({
|
||||
name: trimmed,
|
||||
slug: uniqueSlug,
|
||||
metadata: {
|
||||
signupSource: "consumer",
|
||||
webOnboarding: {
|
||||
completedAt: null,
|
||||
steps: {
|
||||
welcomeInput: {
|
||||
startedAt: completedAt,
|
||||
completedAt,
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await setActiveOrg(newOrg.slug)
|
||||
await setActiveOrg(newOrg.slug)
|
||||
|
||||
analytics.onboardingNameSubmitted({ name_length: name.trim().length })
|
||||
goToStep("greeting")
|
||||
} catch (error) {
|
||||
console.error("Onboarding submit failed:", error)
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Could not set up your workspace. Please try again.",
|
||||
)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
analytics.onboardingNameSubmitted({ name_length: trimmed.length })
|
||||
goToStep("greeting")
|
||||
} catch (error) {
|
||||
console.error("Onboarding submit failed:", error)
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Could not set up your workspace. Please try again.",
|
||||
)
|
||||
} finally {
|
||||
submitLockRef.current = false
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +217,7 @@ export default function WelcomePage() {
|
|||
showUserSupermemory={
|
||||
currentStep === "features" || currentStep === "memories"
|
||||
}
|
||||
showSkipOnboarding={currentStep !== "input"}
|
||||
name={name}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,30 @@
|
|||
"use client"
|
||||
|
||||
import { Logo } from "@ui/assets/Logo"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
export function InitialHeader({
|
||||
showUserSupermemory,
|
||||
showSkipOnboarding,
|
||||
name,
|
||||
}: {
|
||||
showUserSupermemory?: boolean
|
||||
showSkipOnboarding?: boolean
|
||||
name?: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const { markOrgOnboarded, isLoading } = useOrgOnboarding()
|
||||
const userName = name ? `${name.split(" ")[0]}'s` : "My"
|
||||
|
||||
const handleSkip = () => {
|
||||
markOrgOnboarded()
|
||||
analytics.onboardingCompleted()
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex p-6 justify-between items-center">
|
||||
<div className="flex items-center z-10!">
|
||||
|
|
@ -24,13 +40,24 @@ export function InitialHeader({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="newDefault"
|
||||
className="rounded-2xl text-base gap-1 h-11! z-10!"
|
||||
size={"lg"}
|
||||
>
|
||||
Memory API <span className="text-xs mt-[4px]">↗</span>
|
||||
</Button>
|
||||
<div className="flex items-center gap-3 z-10!">
|
||||
{showSkipOnboarding && !isLoading && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSkip}
|
||||
className="text-sm text-white/40 hover:text-white/70 transition-colors cursor-pointer"
|
||||
>
|
||||
Skip Onboarding
|
||||
</button>
|
||||
)}
|
||||
<Button
|
||||
variant="newDefault"
|
||||
className="rounded-2xl text-base gap-1 h-11!"
|
||||
size="lg"
|
||||
>
|
||||
Memory API <span className="text-xs mt-[4px]">↗</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,20 @@ import { useRouter } from "next/navigation"
|
|||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
|
||||
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
export function SetupHeader() {
|
||||
const { user } = useAuth()
|
||||
const router = useRouter()
|
||||
const localStorageUsername = useLocalStorageUsername()
|
||||
const { markOrgOnboarded, isLoading: isOrgLoading } = useOrgOnboarding()
|
||||
|
||||
const handleSkip = () => {
|
||||
markOrgOnboarded()
|
||||
analytics.onboardingCompleted()
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
const displayName =
|
||||
user?.displayUsername || localStorageUsername || user?.name || ""
|
||||
|
|
@ -59,9 +68,21 @@ export function SetupHeader() {
|
|||
</span>
|
||||
<span className="text-white/50 font-medium shrink-0">Setup</span>
|
||||
</nav>
|
||||
{user && (
|
||||
<UserProfileMenu className="z-10" avatarClassName="border-border" />
|
||||
)}
|
||||
<div className="flex items-center gap-3 z-10">
|
||||
{!isOrgLoading && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSkip}
|
||||
className={cn(
|
||||
"text-sm text-white/40 hover:text-white/70 transition-colors cursor-pointer",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Skip Onboarding
|
||||
</button>
|
||||
)}
|
||||
{user && <UserProfileMenu avatarClassName="border-border" />}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { motion } from "motion/react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { LabeledInput } from "@ui/input/labeled-input"
|
||||
import { Button } from "@ui/components/button"
|
||||
|
||||
|
|
@ -17,7 +18,10 @@ export function InputStep({
|
|||
}: InputStepProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className="text-center min-w-[250px] flex flex-col"
|
||||
className={cn(
|
||||
"text-center min-w-[250px] flex flex-col",
|
||||
isSubmitting && "pointer-events-none",
|
||||
)}
|
||||
style={{ gap: "24px" }}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
|
|
@ -53,20 +57,27 @@ export function InputStep({
|
|||
className="w-full flex-1"
|
||||
inputProps={{
|
||||
defaultValue: name,
|
||||
disabled: isSubmitting,
|
||||
onKeyDown: (e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSubmit()
|
||||
}
|
||||
if (e.key !== "Enter") return
|
||||
e.preventDefault()
|
||||
if (isSubmitting) return
|
||||
handleSubmit()
|
||||
},
|
||||
className: "!text-white placeholder:!text-[#525966] !h-[40px] pl-4",
|
||||
}}
|
||||
onChange={(e) => setName((e.target as HTMLInputElement).value)}
|
||||
onChange={(e) => {
|
||||
if (isSubmitting) return
|
||||
setName((e.target as HTMLInputElement).value)
|
||||
}}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(0deg, rgba(91, 126, 245, 0.04) 0%, rgba(91, 126, 245, 0.04) 100%)",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
className={`rounded-[8px] w-8 h-8 p-2 absolute right-1 border-[0.5px] border-[#161F2C] hover:cursor-pointer hover:scale-[0.95] active:scale-[0.95] transition-transform ${
|
||||
isSubmitting ? "scale-[0.90]" : ""
|
||||
}`}
|
||||
|
|
|
|||
232
packages/cartesia-sdk-python/README.md
Normal file
232
packages/cartesia-sdk-python/README.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# Supermemory Cartesia SDK
|
||||
|
||||
Memory-enhanced voice agents with [Supermemory](https://supermemory.ai) and [Cartesia Line](https://cartesia.ai/agents).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install supermemory-cartesia
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import os
|
||||
from line.llm_agent import LlmAgent, LlmConfig
|
||||
from line.voice_agent_app import VoiceAgentApp
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent
|
||||
|
||||
async def get_agent(env, call_request):
|
||||
# Extract container_tag from call metadata (typically user ID)
|
||||
container_tag = call_request.metadata.get("user_id", "default-user")
|
||||
|
||||
# Create base LLM agent
|
||||
base_agent = LlmAgent(
|
||||
model="gemini/gemini-2.5-flash-preview-09-2025",
|
||||
config=LlmConfig(
|
||||
system_prompt="You are a helpful voice assistant with memory.",
|
||||
introduction="Hello! Great to talk with you again!"
|
||||
)
|
||||
)
|
||||
|
||||
# Wrap with Supermemory
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"),
|
||||
container_tag=container_tag,
|
||||
custom_id=call_request.call_id,
|
||||
)
|
||||
|
||||
return memory_agent
|
||||
|
||||
# Create voice agent app
|
||||
app = VoiceAgentApp(get_agent=get_agent)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------------- | ------------ | -------- | ------------------------------------------------------------------ |
|
||||
| `agent` | LlmAgent | **Yes** | The Cartesia Line agent to wrap |
|
||||
| `container_tag` | str | **Yes** | Primary container tag for memory scoping (e.g., user ID) |
|
||||
| `custom_id` | str | **Yes** | Custom ID for grouping conversation messages into a single document|
|
||||
| `add_memory` | Literal | No | Memory persistence mode: "always" (default) or "never" |
|
||||
| `container_tags`| List[str] | No | Additional container tags for organization (e.g., ["org", "prod"]) |
|
||||
| `api_key` | str | No | Supermemory API key (or set `SUPERMEMORY_API_KEY` env var) |
|
||||
| `config` | MemoryConfig | No | Advanced configuration |
|
||||
| `base_url` | str | No | Custom API endpoint |
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```python
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent
|
||||
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
container_tag="user-123",
|
||||
custom_id="conversation-456",
|
||||
add_memory="always", # "always" (default) or "never"
|
||||
container_tags=["org-acme", "prod"], # Optional: additional tags
|
||||
config=SupermemoryCartesiaAgent.MemoryConfig(
|
||||
search_limit=10, # Max memories to retrieve
|
||||
search_threshold=0.1, # Similarity threshold
|
||||
mode="full", # "profile", "query", or "full"
|
||||
system_prompt="Based on previous conversations, I recall:\n\n",
|
||||
),
|
||||
)
|
||||
|
||||
# Read-only mode - retrieve memories but don't save new ones
|
||||
read_only_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
container_tag="user-123",
|
||||
custom_id="conversation-456",
|
||||
add_memory="never", # Only retrieve, don't save
|
||||
)
|
||||
```
|
||||
|
||||
### Memory Modes
|
||||
|
||||
| Mode | Static Profile | Dynamic Profile | Search Results |
|
||||
| ----------- | -------------- | --------------- | -------------- |
|
||||
| `"profile"` | Yes | Yes | No |
|
||||
| `"query"` | No | No | Yes |
|
||||
| `"full"` | Yes | Yes | Yes |
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Intercepts events** - Listens for `UserTurnEnded` events from Cartesia Line
|
||||
2. **Retrieves memories** - Queries Supermemory `/v4/profile` API with user's message
|
||||
3. **Enriches context** - Adds memories to event history as system message
|
||||
4. **Stores messages** - Sends conversation to Supermemory (background, non-blocking)
|
||||
5. **Passes to agent** - Forwards enriched event to wrapped LlmAgent
|
||||
|
||||
### What Gets Stored
|
||||
|
||||
User and assistant messages are sent to Supermemory:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "User: What's the weather?\nAssistant: It's sunny today!",
|
||||
"container_tags": ["user-123", "org-acme", "prod"],
|
||||
"metadata": { "platform": "cartesia" }
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Cartesia Line uses an event-driven architecture:
|
||||
|
||||
```
|
||||
User Speaks (Audio)
|
||||
↓
|
||||
[Ink STT] → Automatic speech recognition
|
||||
↓
|
||||
UserTurnEnded Event {content: "user message", history: [...]}
|
||||
↓
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ SUPERMEMORY CARTESIA AGENT (Wrapper) │
|
||||
│ │
|
||||
│ process(env, event): │
|
||||
│ 1. Intercept UserTurnEnded │
|
||||
│ 2. Extract user message │
|
||||
│ 3. Query Supermemory API │
|
||||
│ 4. Enrich event.history with memories │
|
||||
│ 5. Pass to wrapped LlmAgent │
|
||||
│ 6. Store conversation (async background) │
|
||||
└──────────────────────────────────────────────┘
|
||||
↓
|
||||
AgentSendText Event {text: "response"}
|
||||
↓
|
||||
[Sonic TTS] → Ultra-fast speech synthesis
|
||||
↓
|
||||
Audio Output
|
||||
```
|
||||
|
||||
## Comparison with Pipecat SDK
|
||||
|
||||
| Aspect | Pipecat | Cartesia Line |
|
||||
| ----------------------- | ------------------------------ | ---------------------------- |
|
||||
| **Integration Pattern** | Extends `FrameProcessor` | Wrapper around `LlmAgent` |
|
||||
| **Event Handling** | `process_frame()` method | `process()` method |
|
||||
| **Events** | `LLMContextFrame`, `LLMMessagesFrame` | `UserTurnEnded`, `CallStarted` |
|
||||
| **Context Object** | `LLMContext.get_messages()` | `event.history` |
|
||||
| **Memory Injection** | Modify `context.add_message()` | Modify `event.history` |
|
||||
|
||||
## Full Example with Tools
|
||||
|
||||
```python
|
||||
import os
|
||||
from line.llm_agent import LlmAgent, LlmConfig
|
||||
from line.tools import LoopbackTool
|
||||
from line.voice_agent_app import VoiceAgentApp
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent
|
||||
|
||||
# Define custom tools
|
||||
async def get_weather(location: str) -> str:
|
||||
return f"The weather in {location} is sunny, 72°F"
|
||||
|
||||
weather_tool = LoopbackTool(
|
||||
name="get_weather",
|
||||
description="Get current weather for a location",
|
||||
function=get_weather
|
||||
)
|
||||
|
||||
async def get_agent(env, call_request):
|
||||
container_tag = call_request.metadata.get("user_id", "default-user")
|
||||
org_id = call_request.metadata.get("org_id")
|
||||
|
||||
# Create LLM agent with tools
|
||||
base_agent = LlmAgent(
|
||||
model="gemini/gemini-2.5-flash-preview-09-2025",
|
||||
tools=[weather_tool],
|
||||
config=LlmConfig(
|
||||
system_prompt="You are a personal assistant with memory and tools.",
|
||||
introduction="Hi! How can I help you today?"
|
||||
)
|
||||
)
|
||||
|
||||
# Wrap with Supermemory
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"),
|
||||
container_tag=container_tag,
|
||||
custom_id=call_request.call_id,
|
||||
container_tags=[org_id] if org_id else None,
|
||||
config=SupermemoryCartesiaAgent.MemoryConfig(
|
||||
mode="full",
|
||||
search_limit=15,
|
||||
search_threshold=0.15,
|
||||
)
|
||||
)
|
||||
|
||||
return memory_agent
|
||||
|
||||
app = VoiceAgentApp(get_agent=get_agent)
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/supermemoryai/supermemory
|
||||
cd supermemory/packages/cartesia-sdk-python
|
||||
|
||||
# Install in development mode
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Run tests
|
||||
pytest
|
||||
|
||||
# Format code
|
||||
black .
|
||||
isort .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
78
packages/cartesia-sdk-python/pyproject.toml
Normal file
78
packages/cartesia-sdk-python/pyproject.toml
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "supermemory-cartesia"
|
||||
version = "0.1.0"
|
||||
description = "Supermemory integration for Cartesia Line - memory-enhanced voice agents"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Supermemory", email = "support@supermemory.ai" }
|
||||
]
|
||||
keywords = [
|
||||
"supermemory",
|
||||
"cartesia",
|
||||
"line",
|
||||
"memory",
|
||||
"conversational-ai",
|
||||
"llm",
|
||||
"voice-ai",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
]
|
||||
dependencies = [
|
||||
"supermemory>=3.16.0",
|
||||
"cartesia-line>=0.2.0",
|
||||
"pydantic>=2.10.0",
|
||||
"loguru>=0.7.3",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.3.5",
|
||||
"pytest-asyncio>=0.24.0",
|
||||
"mypy>=1.14.1",
|
||||
"black>=24.8.0",
|
||||
"isort>=5.13.2",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://supermemory.ai"
|
||||
Documentation = "https://docs.supermemory.ai"
|
||||
Repository = "https://github.com/supermemoryai/supermemory"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/supermemory_cartesia"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"/src",
|
||||
"/tests",
|
||||
"/README.md",
|
||||
"/LICENSE",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py310"]
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 100
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
5
packages/cartesia-sdk-python/requirements.txt
Normal file
5
packages/cartesia-sdk-python/requirements.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Core dependencies for supermemory-cartesia
|
||||
supermemory>=3.16.0
|
||||
pydantic>=2.10.0
|
||||
loguru>=0.7.3
|
||||
cartesia-line>=0.2.0
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
"""Supermemory Cartesia SDK - Memory-enhanced voice agents with Cartesia Line.
|
||||
|
||||
This package provides seamless integration between Supermemory and Cartesia Line,
|
||||
enabling persistent memory and context enhancement for voice AI applications.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent, MemoryConfig
|
||||
from line.llm_agent import LlmAgent, LlmConfig
|
||||
|
||||
# Create base LLM agent
|
||||
base_agent = LlmAgent(
|
||||
model="gemini/gemini-2.5-flash-preview-09-2025",
|
||||
config=LlmConfig(
|
||||
system_prompt="You are a helpful assistant.",
|
||||
introduction="Hello!"
|
||||
)
|
||||
)
|
||||
|
||||
# Wrap with Supermemory
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"),
|
||||
container_tag="user-123",
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
from .agent import SupermemoryCartesiaAgent
|
||||
|
||||
# Export MemoryConfig as a top-level class for convenience
|
||||
MemoryConfig = SupermemoryCartesiaAgent.MemoryConfig
|
||||
|
||||
from .exceptions import (
|
||||
APIError,
|
||||
ConfigurationError,
|
||||
MemoryRetrievalError,
|
||||
MemoryStorageError,
|
||||
NetworkError,
|
||||
SupermemoryCartesiaError,
|
||||
)
|
||||
from .utils import (
|
||||
deduplicate_memories,
|
||||
format_memories_to_text,
|
||||
format_relative_time,
|
||||
get_last_user_message,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
# Main agent
|
||||
"SupermemoryCartesiaAgent",
|
||||
"MemoryConfig",
|
||||
# Exceptions
|
||||
"SupermemoryCartesiaError",
|
||||
"ConfigurationError",
|
||||
"MemoryRetrievalError",
|
||||
"MemoryStorageError",
|
||||
"APIError",
|
||||
"NetworkError",
|
||||
# Utilities
|
||||
"get_last_user_message",
|
||||
"deduplicate_memories",
|
||||
"format_memories_to_text",
|
||||
"format_relative_time",
|
||||
]
|
||||
446
packages/cartesia-sdk-python/src/supermemory_cartesia/agent.py
Normal file
446
packages/cartesia-sdk-python/src/supermemory_cartesia/agent.py
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
"""Supermemory Cartesia Line agent integration.
|
||||
|
||||
This module provides a memory-enhanced agent wrapper that integrates with
|
||||
Cartesia Line voice agents, adding persistent memory and context enrichment.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .exceptions import ConfigurationError, MemoryRetrievalError
|
||||
from .utils import deduplicate_memories, format_memories_to_text
|
||||
|
||||
try:
|
||||
import supermemory
|
||||
except ImportError:
|
||||
supermemory = None # type: ignore
|
||||
|
||||
try:
|
||||
from line.events import Event
|
||||
except ImportError:
|
||||
Event = Any # type: ignore
|
||||
|
||||
# XML tags for memory injection
|
||||
MEMORY_TAG_START = "<user_memories>"
|
||||
MEMORY_TAG_END = "</user_memories>"
|
||||
|
||||
|
||||
class SupermemoryCartesiaAgent:
|
||||
"""Memory-enhanced wrapper for Cartesia Line agents.
|
||||
|
||||
This wrapper intercepts UserTurnEnded events, retrieves relevant memories
|
||||
from Supermemory, and enriches the conversation history before passing to
|
||||
the wrapped agent.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from line.llm_agent import LlmAgent, LlmConfig
|
||||
from supermemory_cartesia import SupermemoryCartesiaAgent
|
||||
|
||||
base_agent = LlmAgent(
|
||||
model="anthropic/claude-haiku-4-5-20251001",
|
||||
config=LlmConfig(
|
||||
system_prompt="You are a helpful assistant.",
|
||||
introduction="Hello! How can I help you today?"
|
||||
)
|
||||
)
|
||||
|
||||
memory_agent = SupermemoryCartesiaAgent(
|
||||
agent=base_agent,
|
||||
api_key=os.getenv("SUPERMEMORY_API_KEY"),
|
||||
container_tag="user-123",
|
||||
custom_id="conversation-456",
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
class MemoryConfig(BaseModel):
|
||||
"""Configuration for memory retrieval.
|
||||
|
||||
Attributes:
|
||||
search_limit: Maximum memories to retrieve per query.
|
||||
search_threshold: Minimum similarity threshold (0.0-1.0).
|
||||
system_prompt: Prefix text for memory context.
|
||||
mode: "profile", "query", or "full".
|
||||
"""
|
||||
|
||||
search_limit: int = Field(default=10, ge=1)
|
||||
search_threshold: float = Field(default=0.1, ge=0.0, le=1.0)
|
||||
system_prompt: str = Field(default="Based on previous conversations:\n\n")
|
||||
mode: Literal["profile", "query", "full"] = Field(default="full")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
agent: Any,
|
||||
api_key: Optional[str] = None,
|
||||
container_tag: str,
|
||||
custom_id: str,
|
||||
add_memory: Literal["always", "never"] = "always",
|
||||
container_tags: Optional[List[str]] = None,
|
||||
config: Optional[MemoryConfig] = None,
|
||||
base_url: Optional[str] = None,
|
||||
):
|
||||
"""Initialize the Supermemory Cartesia agent wrapper.
|
||||
|
||||
Args:
|
||||
agent: The inner Cartesia Line agent to wrap.
|
||||
api_key: Supermemory API key (or SUPERMEMORY_API_KEY env var).
|
||||
container_tag: Primary container tag for memory scoping (e.g., user ID).
|
||||
custom_id: Required. Custom ID to store all conversation messages in the same document.
|
||||
Useful for grouping multi-turn conversations (e.g., call ID, conversation ID).
|
||||
add_memory: Memory persistence mode - "always" (default) or "never".
|
||||
container_tags: Optional list of additional container tags for
|
||||
organization/categorization (e.g., ["org-acme", "prod"]).
|
||||
config: Memory retrieval configuration.
|
||||
base_url: Optional custom Supermemory API URL.
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If API key, container_tag, or custom_id is missing.
|
||||
"""
|
||||
self.agent = agent
|
||||
self.container_tag = container_tag
|
||||
self.custom_id = custom_id
|
||||
self.add_memory = add_memory
|
||||
|
||||
# Build container tags list: primary tag first, then additional tags
|
||||
self.container_tags = [container_tag]
|
||||
if container_tags:
|
||||
self.container_tags.extend(container_tags)
|
||||
|
||||
self.config = config or SupermemoryCartesiaAgent.MemoryConfig()
|
||||
|
||||
self.api_key = api_key or os.getenv("SUPERMEMORY_API_KEY")
|
||||
if not self.api_key:
|
||||
raise ConfigurationError(
|
||||
"API key required. Set SUPERMEMORY_API_KEY or pass api_key."
|
||||
)
|
||||
|
||||
if not container_tag:
|
||||
raise ConfigurationError("container_tag is required")
|
||||
|
||||
if not custom_id or not custom_id.strip():
|
||||
raise ConfigurationError(
|
||||
"custom_id is required and must be a non-empty string. "
|
||||
"This ensures messages are grouped into the same document for a conversation."
|
||||
)
|
||||
|
||||
self._supermemory_client = None
|
||||
if supermemory is not None:
|
||||
try:
|
||||
self._supermemory_client = supermemory.AsyncSupermemory(
|
||||
api_key=self.api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
logger.info(f"[Supermemory] Initialized client for container_tag={container_tag}, all_tags={self.container_tags}")
|
||||
except Exception as e:
|
||||
logger.error(f"[Supermemory] Failed to initialize client: {e}")
|
||||
|
||||
self._messages_sent_count: int = 0
|
||||
self._last_query: Optional[str] = None
|
||||
self._background_tasks: set = set() # Track background tasks to prevent GC
|
||||
|
||||
async def _retrieve_memories(self, query: str) -> Dict[str, Any]:
|
||||
"""Retrieve memories from Supermemory."""
|
||||
if self._supermemory_client is None:
|
||||
raise MemoryRetrievalError("Supermemory client not initialized")
|
||||
|
||||
try:
|
||||
# Use primary container tag for profile retrieval
|
||||
kwargs: Dict[str, Any] = {"container_tag": self.container_tags[0]}
|
||||
|
||||
if self.config.mode != "profile" and query:
|
||||
kwargs["q"] = query
|
||||
kwargs["threshold"] = self.config.search_threshold
|
||||
kwargs["extra_body"] = {"limit": self.config.search_limit}
|
||||
|
||||
logger.info(f"[Supermemory] Retrieving memories for query: {query[:50]}...")
|
||||
|
||||
response = await asyncio.wait_for(
|
||||
self._supermemory_client.profile(**kwargs),
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
static_count = len(response.profile.static) if response.profile.static else 0
|
||||
dynamic_count = len(response.profile.dynamic) if response.profile.dynamic else 0
|
||||
search_count = len(response.search_results.results) if response.search_results and response.search_results.results else 0
|
||||
|
||||
logger.info(f"[Supermemory] Retrieved memories - static: {static_count}, dynamic: {dynamic_count}, search: {search_count}")
|
||||
|
||||
search_results = []
|
||||
if response.search_results and response.search_results.results:
|
||||
search_results = response.search_results.results
|
||||
|
||||
return {
|
||||
"profile": {
|
||||
"static": response.profile.static or [],
|
||||
"dynamic": response.profile.dynamic or [],
|
||||
},
|
||||
"search_results": search_results,
|
||||
}
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("[Supermemory] Profile API timed out after 10s")
|
||||
raise MemoryRetrievalError("Profile API timed out")
|
||||
except Exception as e:
|
||||
logger.error(f"[Supermemory] Error retrieving memories: {e}")
|
||||
raise MemoryRetrievalError("Failed to retrieve memories", e)
|
||||
|
||||
async def _store_messages(self, messages: List[Dict[str, Any]]) -> None:
|
||||
"""Store messages in Supermemory."""
|
||||
if self._supermemory_client is None or not messages or self.add_memory == "never":
|
||||
return
|
||||
|
||||
try:
|
||||
# Format as conversation transcript
|
||||
lines = []
|
||||
for msg in messages:
|
||||
role = msg.get("role", "")
|
||||
content = msg.get("content", "")
|
||||
if role == "user":
|
||||
lines.append(f"User: {content}")
|
||||
elif role == "assistant":
|
||||
lines.append(f"Assistant: {content}")
|
||||
|
||||
logger.info(f"[Supermemory] Storing {len(messages)} messages to containers={self.container_tags}")
|
||||
|
||||
# Build kwargs for add() call
|
||||
add_kwargs: Dict[str, Any] = {
|
||||
"content": "\n".join(lines),
|
||||
"container_tags": self.container_tags,
|
||||
"metadata": {"platform": "cartesia"},
|
||||
}
|
||||
|
||||
# Use custom_id for document grouping (required field)
|
||||
add_kwargs["custom_id"] = self.custom_id
|
||||
logger.info(f"[Supermemory] Using custom_id={self.custom_id} for document grouping")
|
||||
|
||||
await self._supermemory_client.add(**add_kwargs)
|
||||
|
||||
logger.info(f"[Supermemory] Successfully stored {len(messages)} messages")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Supermemory] Error storing messages: {e}")
|
||||
|
||||
def _build_memory_message(self, memories_data: Dict[str, Any]) -> Optional[str]:
|
||||
"""Build memory context from retrieved data."""
|
||||
profile = memories_data["profile"]
|
||||
deduplicated = deduplicate_memories(
|
||||
static=profile["static"],
|
||||
dynamic=profile["dynamic"],
|
||||
search_results=memories_data["search_results"],
|
||||
)
|
||||
|
||||
total = (
|
||||
len(deduplicated["static"])
|
||||
+ len(deduplicated["dynamic"])
|
||||
+ len(deduplicated["search_results"])
|
||||
)
|
||||
|
||||
if total == 0:
|
||||
return None
|
||||
|
||||
include_profile = self.config.mode in ("profile", "full")
|
||||
include_search = self.config.mode in ("query", "full")
|
||||
|
||||
memory_text = format_memories_to_text(
|
||||
deduplicated,
|
||||
system_prompt=self.config.system_prompt,
|
||||
include_static=include_profile,
|
||||
include_dynamic=include_profile,
|
||||
include_search=include_search,
|
||||
)
|
||||
|
||||
if not memory_text:
|
||||
return None
|
||||
|
||||
return f"{MEMORY_TAG_START}\n{memory_text}\n{MEMORY_TAG_END}"
|
||||
|
||||
def _extract_user_message(self, event: Any) -> Optional[str]:
|
||||
"""Extract user text from a UserTurnEnded event."""
|
||||
if not hasattr(event, 'content'):
|
||||
return None
|
||||
|
||||
content = event.content
|
||||
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
|
||||
if isinstance(content, list):
|
||||
texts = []
|
||||
for item in content:
|
||||
if hasattr(item, 'content') and isinstance(item.content, str):
|
||||
texts.append(item.content)
|
||||
elif isinstance(item, str):
|
||||
texts.append(item)
|
||||
return " ".join(texts) if texts else None
|
||||
|
||||
if hasattr(content, 'content'):
|
||||
return str(content.content)
|
||||
|
||||
return str(content)
|
||||
|
||||
def _extract_conversation_from_history(self, history: list) -> List[Dict[str, str]]:
|
||||
"""Extract messages from Cartesia event history."""
|
||||
messages = []
|
||||
seen = set()
|
||||
|
||||
for item in history:
|
||||
if isinstance(item, dict):
|
||||
if item.get("role") in ("user", "assistant"):
|
||||
content = item.get("content", "")
|
||||
if content and content not in seen:
|
||||
messages.append(item)
|
||||
seen.add(content)
|
||||
continue
|
||||
|
||||
event_type = getattr(item, 'type', None) or type(item).__name__
|
||||
|
||||
if event_type in ('user_turn_ended', 'UserTurnEnded'):
|
||||
nested = getattr(item, 'content', [])
|
||||
if isinstance(nested, list):
|
||||
for n in nested:
|
||||
if hasattr(n, 'content') and isinstance(n.content, str):
|
||||
if n.content not in seen:
|
||||
messages.append({"role": "user", "content": n.content})
|
||||
seen.add(n.content)
|
||||
|
||||
elif event_type in ('agent_turn_ended', 'AgentTurnEnded'):
|
||||
nested = getattr(item, 'content', [])
|
||||
if isinstance(nested, list):
|
||||
texts = [n.content for n in nested if hasattr(n, 'content') and isinstance(n.content, str)]
|
||||
if texts:
|
||||
content = " ".join(texts)
|
||||
if content not in seen:
|
||||
messages.append({"role": "assistant", "content": content})
|
||||
seen.add(content)
|
||||
|
||||
elif event_type in ('user_text_sent', 'UserTextSent'):
|
||||
content = getattr(item, 'content', '')
|
||||
if content and isinstance(content, str) and content not in seen:
|
||||
messages.append({"role": "user", "content": content})
|
||||
seen.add(content)
|
||||
|
||||
elif event_type in ('agent_text_sent', 'AgentTextSent'):
|
||||
content = getattr(item, 'content', '')
|
||||
if content and isinstance(content, str) and content not in seen:
|
||||
messages.append({"role": "assistant", "content": content})
|
||||
seen.add(content)
|
||||
|
||||
return messages
|
||||
|
||||
async def _enrich_event_with_memories(self, event: Any) -> tuple[Any, Optional[str]]:
|
||||
"""Enrich event by retrieving memories.
|
||||
|
||||
Returns:
|
||||
Tuple of (event, memory_context) - memory_context is None if no memories found.
|
||||
The event is returned unchanged; memory injection happens at the agent level.
|
||||
"""
|
||||
user_message = self._extract_user_message(event)
|
||||
|
||||
if not user_message:
|
||||
logger.warning("[Supermemory] Could not extract user message from event")
|
||||
return event, None
|
||||
|
||||
if user_message == self._last_query:
|
||||
return event, None
|
||||
|
||||
self._last_query = user_message
|
||||
logger.info(f"[Supermemory] Processing user message: {user_message[:50]}...")
|
||||
|
||||
try:
|
||||
memories_data = await self._retrieve_memories(user_message)
|
||||
memory_context = self._build_memory_message(memories_data)
|
||||
|
||||
if not memory_context:
|
||||
logger.info("[Supermemory] No memories found for context injection")
|
||||
return event, None
|
||||
|
||||
logger.info("[Supermemory] Retrieved memory context for injection")
|
||||
return event, memory_context
|
||||
|
||||
except MemoryRetrievalError as e:
|
||||
logger.warning(f"[Supermemory] Memory retrieval failed: {e}")
|
||||
return event, None
|
||||
except Exception as e:
|
||||
logger.error(f"[Supermemory] Error in memory enrichment: {e}")
|
||||
return event, None
|
||||
|
||||
async def process(self, env: Any, event: Event) -> AsyncGenerator[Event, None]:
|
||||
"""Process events with memory enrichment.
|
||||
|
||||
Args:
|
||||
env: Turn environment from Cartesia Line.
|
||||
event: Input event to process.
|
||||
|
||||
Yields:
|
||||
Output events from the wrapped agent.
|
||||
"""
|
||||
try:
|
||||
if type(event).__name__ == "UserTurnEnded":
|
||||
logger.info("[Supermemory] Processing UserTurnEnded event")
|
||||
event, memory_context = await self._enrich_event_with_memories(event)
|
||||
|
||||
# Clean up old memory context and inject new one if available
|
||||
if hasattr(self.agent, 'config'):
|
||||
original_prompt = getattr(self.agent.config, 'system_prompt', '')
|
||||
# Always remove old memory context if present to prevent stale data
|
||||
if MEMORY_TAG_START in original_prompt:
|
||||
original_prompt = re.sub(
|
||||
rf'{re.escape(MEMORY_TAG_START)}.*?{re.escape(MEMORY_TAG_END)}\s*',
|
||||
'',
|
||||
original_prompt,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
logger.debug("[Supermemory] Removed old memory context from system prompt")
|
||||
|
||||
# Inject new memory context if available
|
||||
if memory_context:
|
||||
self.agent.config.system_prompt = f"{memory_context}\n\n{original_prompt}"
|
||||
logger.info("[Supermemory] Injected new memory context into system prompt")
|
||||
else:
|
||||
# No new memories, but we cleaned up old ones
|
||||
self.agent.config.system_prompt = original_prompt
|
||||
logger.debug("[Supermemory] No new memories to inject, using clean prompt")
|
||||
|
||||
# Store conversation in background
|
||||
if hasattr(event, 'history') and event.history:
|
||||
messages = self._extract_conversation_from_history(event.history)
|
||||
unsent = messages[self._messages_sent_count:]
|
||||
if unsent:
|
||||
logger.info(f"[Supermemory] Queuing {len(unsent)} messages for storage")
|
||||
task = asyncio.create_task(self._store_messages(unsent))
|
||||
self._background_tasks.add(task)
|
||||
task.add_done_callback(self._background_tasks.discard)
|
||||
self._messages_sent_count = len(messages)
|
||||
else:
|
||||
# No history yet, store just the current user message
|
||||
user_content = self._extract_user_message(event)
|
||||
if user_content:
|
||||
logger.info(f"[Supermemory] No history, storing current user message: {user_content[:50]}...")
|
||||
task = asyncio.create_task(self._store_messages([{"role": "user", "content": user_content}]))
|
||||
self._background_tasks.add(task)
|
||||
task.add_done_callback(self._background_tasks.discard)
|
||||
self._messages_sent_count = 1 # CRITICAL: Increment counter to prevent duplicate storage
|
||||
|
||||
async for output in self.agent.process(env, event):
|
||||
yield output
|
||||
else:
|
||||
async for output in self.agent.process(env, event):
|
||||
yield output
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Supermemory] Error in process: {e}")
|
||||
async for output in self.agent.process(env, event):
|
||||
yield output
|
||||
|
||||
def reset_memory_tracking(self) -> None:
|
||||
"""Reset memory tracking for a new conversation."""
|
||||
self._messages_sent_count = 0
|
||||
self._last_query = None
|
||||
logger.info("[Supermemory] Reset memory tracking state")
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
"""Custom exceptions for Supermemory Cartesia integration."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class SupermemoryCartesiaError(Exception):
|
||||
"""Base exception for all Supermemory Cartesia errors."""
|
||||
|
||||
def __init__(self, message: str, original_error: Optional[Exception] = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.original_error = original_error
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.original_error:
|
||||
return f"{self.message}: {self.original_error}"
|
||||
return self.message
|
||||
|
||||
|
||||
class ConfigurationError(SupermemoryCartesiaError):
|
||||
"""Raised when there are configuration issues (e.g., missing API key, invalid params)."""
|
||||
|
||||
|
||||
class MemoryRetrievalError(SupermemoryCartesiaError):
|
||||
"""Raised when memory retrieval operations fail."""
|
||||
|
||||
|
||||
class MemoryStorageError(SupermemoryCartesiaError):
|
||||
"""Raised when memory storage operations fail."""
|
||||
|
||||
|
||||
class APIError(SupermemoryCartesiaError):
|
||||
"""Raised when Supermemory API requests fail."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: Optional[int] = None,
|
||||
response_text: Optional[str] = None,
|
||||
original_error: Optional[Exception] = None,
|
||||
):
|
||||
super().__init__(message, original_error)
|
||||
self.status_code = status_code
|
||||
self.response_text = response_text
|
||||
|
||||
def __str__(self) -> str:
|
||||
parts = [self.message]
|
||||
if self.status_code:
|
||||
parts.append(f"Status: {self.status_code}")
|
||||
if self.response_text:
|
||||
parts.append(f"Response: {self.response_text}")
|
||||
if self.original_error:
|
||||
parts.append(f"Cause: {self.original_error}")
|
||||
return " | ".join(parts)
|
||||
|
||||
|
||||
class NetworkError(SupermemoryCartesiaError):
|
||||
"""Raised when network operations fail."""
|
||||
134
packages/cartesia-sdk-python/src/supermemory_cartesia/utils.py
Normal file
134
packages/cartesia-sdk-python/src/supermemory_cartesia/utils.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
"""Utility functions for Supermemory Cartesia integration."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
|
||||
def get_last_user_message(messages: List[Dict[str, str]]) -> str | None:
|
||||
"""Extract the last user message content from a list of messages."""
|
||||
for msg in reversed(messages):
|
||||
if msg["role"] == "user":
|
||||
return msg["content"]
|
||||
return None
|
||||
|
||||
|
||||
def format_relative_time(iso_timestamp: str) -> str:
|
||||
"""Convert ISO timestamp to relative time string.
|
||||
|
||||
Format rules:
|
||||
- [just now] - within 30 minutes
|
||||
- [Xmins ago] - 30-60 minutes
|
||||
- [X hrs ago] - less than 1 day
|
||||
- [Xd ago] - less than 1 week
|
||||
- [X Jul] - more than 1 week, same year
|
||||
- [X Jul, 2023] - different year
|
||||
"""
|
||||
try:
|
||||
dt = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00"))
|
||||
now = datetime.now(timezone.utc)
|
||||
diff = now - dt
|
||||
|
||||
seconds = diff.total_seconds()
|
||||
minutes = seconds / 60
|
||||
hours = seconds / 3600
|
||||
days = seconds / 86400
|
||||
|
||||
if minutes < 30:
|
||||
return "just now"
|
||||
elif minutes < 60:
|
||||
return f"{int(minutes)}mins ago"
|
||||
elif hours < 24:
|
||||
return f"{int(hours)} hrs ago"
|
||||
elif days < 7:
|
||||
return f"{int(days)}d ago"
|
||||
elif dt.year == now.year:
|
||||
return f"{dt.day} {dt.strftime('%b')}"
|
||||
else:
|
||||
return f"{dt.day} {dt.strftime('%b')}, {dt.year}"
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def deduplicate_memories(
|
||||
static: List[str],
|
||||
dynamic: List[str],
|
||||
search_results: List[Dict[str, Any]],
|
||||
) -> Dict[str, Union[List[str], List[Dict[str, Any]]]]:
|
||||
"""Deduplicate memories. Priority: static > dynamic > search.
|
||||
|
||||
Args:
|
||||
static: List of static memory strings.
|
||||
dynamic: List of dynamic memory strings.
|
||||
search_results: List of search result dicts with 'memory' and 'updatedAt'.
|
||||
"""
|
||||
seen = set()
|
||||
|
||||
def unique_strings(memories: List[str]) -> List[str]:
|
||||
out = []
|
||||
for m in memories:
|
||||
if m not in seen:
|
||||
seen.add(m)
|
||||
out.append(m)
|
||||
return out
|
||||
|
||||
def unique_search(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
out = []
|
||||
for r in results:
|
||||
memory = r.get("memory", "")
|
||||
if memory and memory not in seen:
|
||||
seen.add(memory)
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
return {
|
||||
"static": unique_strings(static),
|
||||
"dynamic": unique_strings(dynamic),
|
||||
"search_results": unique_search(search_results),
|
||||
}
|
||||
|
||||
|
||||
def format_memories_to_text(
|
||||
memories: Dict[str, Union[List[str], List[Dict[str, Any]]]],
|
||||
system_prompt: str = "Based on previous conversations, I recall:\n\n",
|
||||
include_static: bool = True,
|
||||
include_dynamic: bool = True,
|
||||
include_search: bool = True,
|
||||
) -> str:
|
||||
"""Format deduplicated memories into a text string for injection.
|
||||
|
||||
Search results include temporal context (e.g., '3d ago') from updatedAt.
|
||||
"""
|
||||
sections = []
|
||||
|
||||
static = memories["static"]
|
||||
dynamic = memories["dynamic"]
|
||||
search_results = memories["search_results"]
|
||||
|
||||
if include_static and static:
|
||||
sections.append("## User Profile (Persistent)")
|
||||
sections.append("\n".join(f"- {item}" for item in static))
|
||||
|
||||
if include_dynamic and dynamic:
|
||||
sections.append("## Recent Context")
|
||||
sections.append("\n".join(f"- {item}" for item in dynamic))
|
||||
|
||||
if include_search and search_results:
|
||||
sections.append("## Relevant Memories")
|
||||
lines = []
|
||||
for item in search_results:
|
||||
if isinstance(item, dict):
|
||||
memory = item.get("memory", "")
|
||||
updated_at = item.get("updatedAt", "")
|
||||
time_str = format_relative_time(updated_at) if updated_at else ""
|
||||
if time_str:
|
||||
lines.append(f"- [{time_str}] {memory}")
|
||||
else:
|
||||
lines.append(f"- {memory}")
|
||||
else:
|
||||
lines.append(f"- {item}")
|
||||
sections.append("\n".join(lines))
|
||||
|
||||
if not sections:
|
||||
return ""
|
||||
|
||||
return f"{system_prompt}\n" + "\n\n".join(sections)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@supermemory/tools",
|
||||
"type": "module",
|
||||
"version": "2.0.0",
|
||||
"version": "1.5.0",
|
||||
"description": "Memory tools for AI SDK and OpenAI function calling with supermemory",
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
"devDependencies": {
|
||||
"@ai-sdk/provider": "^3.0.0",
|
||||
"@anthropic-ai/sdk": "^0.65.0",
|
||||
"@voltagent/core": "^2.6.12",
|
||||
"@mastra/core": "^1.0.0",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/bun": "^1.2.21",
|
||||
|
|
@ -31,7 +32,13 @@
|
|||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ai-sdk/provider": "^2.0.0 || ^3.0.0"
|
||||
"@ai-sdk/provider": "^2.0.0 || ^3.0.0",
|
||||
"@voltagent/core": "^2.6.12"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@voltagent/core": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
|
|
@ -45,6 +52,7 @@
|
|||
"./claude-memory": "./dist/claude-memory.js",
|
||||
"./mastra": "./dist/mastra.js",
|
||||
"./openai": "./dist/openai/index.js",
|
||||
"./voltagent": "./dist/voltagent/index.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export interface AddConversationParams {
|
|||
messages: ConversationMessage[]
|
||||
containerTags?: string[]
|
||||
metadata?: Record<string, string | number | boolean>
|
||||
entityContext?: string
|
||||
apiKey: string
|
||||
baseUrl?: string
|
||||
}
|
||||
|
|
@ -86,6 +87,7 @@ export async function addConversation(
|
|||
messages: params.messages,
|
||||
containerTags: params.containerTags,
|
||||
metadata: params.metadata,
|
||||
entityContext: params.entityContext,
|
||||
}),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export type { SupermemoryToolsConfig } from "./types"
|
||||
|
||||
export type { OpenAIMiddlewareOptions } from "./openai"
|
||||
|
||||
export type { SupermemoryVoltAgent } from "./voltagent"
|
||||
|
|
|
|||
|
|
@ -119,98 +119,113 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
promptTemplate: options?.promptTemplate,
|
||||
})
|
||||
|
||||
const wrappedModel = {
|
||||
...model,
|
||||
// Proxy keeps prototype/getter fields (e.g. provider, modelId) that `{ ...model }` drops.
|
||||
return new Proxy(model, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "doGenerate") {
|
||||
return async (params: LanguageModelCallOptions) => {
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(
|
||||
params,
|
||||
ctx,
|
||||
)
|
||||
|
||||
doGenerate: async (params: LanguageModelCallOptions) => {
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(params, ctx)
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
const result = await target.doGenerate(transformedParams as any)
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
const result = await model.doGenerate(transformedParams as any)
|
||||
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (ctx.addMemory === "always" && userMessage && userMessage.trim()) {
|
||||
const assistantResponseText = extractAssistantResponseText(
|
||||
result.content as unknown[],
|
||||
)
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
assistantResponseText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error generating response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
doStream: async (params: LanguageModelCallOptions) => {
|
||||
let generatedText = ""
|
||||
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(params, ctx)
|
||||
|
||||
const { stream, ...rest } = await model.doStream(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
transformedParams as any,
|
||||
)
|
||||
|
||||
const transformStream = new TransformStream<
|
||||
LanguageModelStreamPart,
|
||||
LanguageModelStreamPart
|
||||
>({
|
||||
transform(chunk, controller) {
|
||||
if (chunk.type === "text-delta") {
|
||||
generatedText += chunk.delta
|
||||
}
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
flush: async () => {
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
const assistantResponseText = extractAssistantResponseText(
|
||||
result.content as unknown[],
|
||||
)
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
generatedText,
|
||||
assistantResponseText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
stream: stream.pipeThrough(transformStream),
|
||||
...rest,
|
||||
return result
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error generating response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error streaming response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
} as T
|
||||
|
||||
return wrappedModel
|
||||
if (prop === "doStream") {
|
||||
return async (params: LanguageModelCallOptions) => {
|
||||
let generatedText = ""
|
||||
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(
|
||||
params,
|
||||
ctx,
|
||||
)
|
||||
|
||||
const { stream, ...rest } = await target.doStream(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
transformedParams as any,
|
||||
)
|
||||
|
||||
const transformStream = new TransformStream<
|
||||
LanguageModelStreamPart,
|
||||
LanguageModelStreamPart
|
||||
>({
|
||||
transform(chunk, controller) {
|
||||
if (chunk.type === "text-delta") {
|
||||
generatedText += chunk.delta
|
||||
}
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
flush: async () => {
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
generatedText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
stream: stream.pipeThrough(transformStream),
|
||||
...rest,
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error streaming response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.get(target, prop, receiver)
|
||||
},
|
||||
}) as T
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
|||
207
packages/tools/src/voltagent/hooks.ts
Normal file
207
packages/tools/src/voltagent/hooks.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* VoltAgent hooks for Supermemory integration.
|
||||
*
|
||||
* Provides onPrepareMessages and onEnd hooks that inject memories
|
||||
* and save conversations.
|
||||
*/
|
||||
|
||||
import type {
|
||||
VoltAgentHooks,
|
||||
HookPrepareMessagesArgs,
|
||||
HookEndArgs,
|
||||
VoltAgentMessage,
|
||||
SupermemoryVoltAgent,
|
||||
} from "./types"
|
||||
import {
|
||||
createSupermemoryContext,
|
||||
enhanceMessagesWithMemories,
|
||||
saveConversation,
|
||||
} from "./middleware"
|
||||
|
||||
/**
|
||||
* Creates Supermemory hooks for VoltAgent agents.
|
||||
*
|
||||
* These hooks intercept the agent lifecycle to inject memories
|
||||
* before LLM calls and save conversations after completion.
|
||||
*
|
||||
* @param containerTag - The container tag/user ID for scoping memories
|
||||
* @param options - Configuration options for memory behavior
|
||||
* @returns VoltAgent hooks object with onPrepareMessages and onEnd
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createSupermemoryHooks } from "@supermemory/tools/voltagent"
|
||||
*
|
||||
* const hooks = createSupermemoryHooks("user-123", {
|
||||
* mode: "full",
|
||||
* addMemory: "always",
|
||||
* customId: "conv-456",
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent({
|
||||
* name: "my-agent",
|
||||
* instructions: "You are a helpful assistant",
|
||||
* llm: new VercelAIProvider(),
|
||||
* model: openai("gpt-4o"),
|
||||
* hooks
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createSupermemoryHooks(
|
||||
containerTag: string,
|
||||
options: SupermemoryVoltAgent,
|
||||
): VoltAgentHooks {
|
||||
const ctx = createSupermemoryContext(containerTag, options)
|
||||
|
||||
return {
|
||||
onPrepareMessages: async (
|
||||
args: HookPrepareMessagesArgs,
|
||||
): Promise<{ messages: VoltAgentMessage[] }> => {
|
||||
try {
|
||||
// VoltAgent passes user messages in args.context.input.messages
|
||||
// and the prepared messages (system + conversation) in args.messages
|
||||
const contextInput = args.context?.input as
|
||||
| { messages?: VoltAgentMessage[] }
|
||||
| undefined
|
||||
const inputMessages = contextInput?.messages || []
|
||||
|
||||
ctx.logger.debug("onPrepareMessages called", {
|
||||
messageCount: args.messages.length,
|
||||
inputMessageCount: inputMessages.length,
|
||||
agentName: args.agent.name,
|
||||
})
|
||||
|
||||
const enhancedMessages = await enhanceMessagesWithMemories(
|
||||
inputMessages,
|
||||
ctx,
|
||||
args.messages,
|
||||
)
|
||||
|
||||
ctx.logger.debug("Messages enhanced with memories", {
|
||||
originalCount: args.messages.length,
|
||||
enhancedCount: enhancedMessages.length,
|
||||
})
|
||||
|
||||
return { messages: enhancedMessages }
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error in onPrepareMessages", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
return { messages: args.messages }
|
||||
}
|
||||
},
|
||||
|
||||
onEnd: async (args: HookEndArgs): Promise<void> => {
|
||||
try {
|
||||
ctx.logger.debug("onEnd called", {
|
||||
agentName: args.agent.name,
|
||||
hasContext: !!args.context,
|
||||
hasOutput: !!args.output,
|
||||
})
|
||||
|
||||
let messages: VoltAgentMessage[] = []
|
||||
|
||||
if (args.context?.input && args.output) {
|
||||
const inputData = args.context.input as
|
||||
| { messages?: VoltAgentMessage[] }
|
||||
| undefined
|
||||
const inputMessages = inputData?.messages || []
|
||||
|
||||
const outputData = args.output as
|
||||
| string
|
||||
| { text?: string; content?: string }
|
||||
| undefined
|
||||
const outputText =
|
||||
typeof outputData === "string"
|
||||
? outputData
|
||||
: outputData?.text || outputData?.content
|
||||
|
||||
if (inputMessages.length > 0 && outputText) {
|
||||
messages = [
|
||||
...inputMessages,
|
||||
{ role: "assistant" as const, content: outputText },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
ctx.logger.debug("No messages to save, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
saveConversation(messages, ctx).catch((error) => {
|
||||
ctx.logger.error("Background conversation save failed", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error in onEnd", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges Supermemory hooks with existing hooks from an agent config.
|
||||
* Preserves existing hooks and adds Supermemory hooks.
|
||||
*
|
||||
* @param existingHooks - Existing hooks from agent config (if any)
|
||||
* @param supermemoryHooks - Supermemory hooks to merge
|
||||
* @returns Merged hooks object
|
||||
*/
|
||||
export function mergeHooks(
|
||||
existingHooks: VoltAgentHooks | undefined,
|
||||
supermemoryHooks: VoltAgentHooks,
|
||||
): VoltAgentHooks {
|
||||
if (!existingHooks) {
|
||||
return supermemoryHooks
|
||||
}
|
||||
|
||||
const mergedHooks: VoltAgentHooks = { ...existingHooks }
|
||||
|
||||
if (existingHooks.onPrepareMessages && supermemoryHooks.onPrepareMessages) {
|
||||
const existingOnPrepareMessages = existingHooks.onPrepareMessages
|
||||
const supermemoryOnPrepareMessages = supermemoryHooks.onPrepareMessages
|
||||
|
||||
mergedHooks.onPrepareMessages = async (args) => {
|
||||
const resultAfterExisting = await existingOnPrepareMessages(args)
|
||||
const messagesAfterExisting =
|
||||
resultAfterExisting?.messages || args.messages
|
||||
|
||||
return await supermemoryOnPrepareMessages({
|
||||
...args,
|
||||
messages: messagesAfterExisting,
|
||||
})
|
||||
}
|
||||
} else if (supermemoryHooks.onPrepareMessages) {
|
||||
mergedHooks.onPrepareMessages = supermemoryHooks.onPrepareMessages
|
||||
}
|
||||
|
||||
if (existingHooks.onEnd && supermemoryHooks.onEnd) {
|
||||
const existingOnEnd = existingHooks.onEnd
|
||||
const supermemoryOnEnd = supermemoryHooks.onEnd
|
||||
|
||||
mergedHooks.onEnd = async (args) => {
|
||||
await supermemoryOnEnd(args)
|
||||
await existingOnEnd(args)
|
||||
}
|
||||
} else if (supermemoryHooks.onEnd) {
|
||||
mergedHooks.onEnd = supermemoryHooks.onEnd
|
||||
}
|
||||
|
||||
if (existingHooks.onStart && supermemoryHooks.onStart) {
|
||||
const existingOnStart = existingHooks.onStart
|
||||
const supermemoryOnStart = supermemoryHooks.onStart
|
||||
|
||||
mergedHooks.onStart = async (args) => {
|
||||
await existingOnStart(args)
|
||||
await supermemoryOnStart(args)
|
||||
}
|
||||
} else if (supermemoryHooks.onStart) {
|
||||
mergedHooks.onStart = supermemoryHooks.onStart
|
||||
}
|
||||
|
||||
return mergedHooks
|
||||
}
|
||||
179
packages/tools/src/voltagent/index.ts
Normal file
179
packages/tools/src/voltagent/index.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* VoltAgent integration for Supermemory.
|
||||
*
|
||||
* Provides a wrapper function that enhances VoltAgent agent configurations
|
||||
* with Supermemory hooks for automatic memory injection and storage.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { createSupermemoryHooks, mergeHooks } from "./hooks"
|
||||
import type { VoltAgentConfig, SupermemoryVoltAgent } from "./types"
|
||||
|
||||
/**
|
||||
* Configuration options for withSupermemory.
|
||||
*/
|
||||
interface WithSupermemoryOptions<T extends VoltAgentConfig>
|
||||
extends SupermemoryVoltAgent {
|
||||
/**
|
||||
* The VoltAgent agent configuration to enhance
|
||||
*/
|
||||
agentConfig: T
|
||||
|
||||
/**
|
||||
* Required. The container tag/user ID for scoping memories (e.g., "user-123")
|
||||
*/
|
||||
containerTag: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances a VoltAgent agent configuration with Supermemory memory capabilities.
|
||||
*
|
||||
* The function injects hooks that automatically:
|
||||
* - Retrieve relevant memories before LLM calls (via onPrepareMessages)
|
||||
* - Inject memories into the system prompt
|
||||
* - Optionally save conversations after completion (via onEnd)
|
||||
*
|
||||
* @param options - Configuration object containing agent config and Supermemory options
|
||||
* @param options.agentConfig - The VoltAgent agent configuration to enhance
|
||||
* @param options.containerTag - Required. The container tag/user ID for scoping memories (e.g., "user-123")
|
||||
* @param options.mode - Memory retrieval mode: "profile" (default), "query", or "full"
|
||||
* @param options.addMemory - Memory persistence: "always" (default for VoltAgent) or "never"
|
||||
* @param options.customId - Required. Custom ID to group messages into a single document
|
||||
* @param options.apiKey - Supermemory API key (falls back to SUPERMEMORY_API_KEY env var)
|
||||
* @param options.baseUrl - Custom Supermemory API base URL
|
||||
* @param options.promptTemplate - Custom function to format memory data into prompt
|
||||
* @param options.threshold - Search sensitivity: 0 (more results) to 1 (more accurate). Default: 0.1
|
||||
* @param options.limit - Maximum number of memory results to return. Default: 10
|
||||
* @param options.rerank - If true, rerank results for relevance. Default: false
|
||||
* @param options.rewriteQuery - If true, AI-rewrite query for better results (+400ms latency). Default: false
|
||||
* @param options.filters - Advanced AND/OR filters for search
|
||||
* @param options.include - Control what additional data to include (chunks, documents, etc.)
|
||||
* @param options.metadata - Optional metadata to attach to saved conversations
|
||||
* @param options.searchMode - Search mode: "memories" (atomic facts), "documents" (chunks), or "hybrid" (both)
|
||||
* @param options.entityContext - Context for memory extraction (max 1500 chars), guides how memories are understood
|
||||
* @returns Enhanced agent config with Supermemory hooks injected
|
||||
*
|
||||
* @example
|
||||
* Basic usage with profile memories:
|
||||
* ```typescript
|
||||
* import { withSupermemory } from "@supermemory/tools/voltagent"
|
||||
* import { Agent } from "@voltagent/core"
|
||||
* import { VercelAIProvider } from "@voltagent/vercel-ai"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const configWithMemory = withSupermemory({
|
||||
* agentConfig: {
|
||||
* name: "my-agent",
|
||||
* instructions: "You are a helpful assistant",
|
||||
* llm: new VercelAIProvider(),
|
||||
* model: openai("gpt-4o"),
|
||||
* },
|
||||
* containerTag: "user-123",
|
||||
* customId: "conversation-123"
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent(configWithMemory)
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Advanced usage with full memory mode and conversation saving:
|
||||
* ```typescript
|
||||
* const configWithMemory = withSupermemory({
|
||||
* agentConfig: {
|
||||
* name: "my-agent",
|
||||
* instructions: "You are a helpful assistant",
|
||||
* llm: new VercelAIProvider(),
|
||||
* model: openai("gpt-4o"),
|
||||
* },
|
||||
* containerTag: "user-123", // Required: user/project ID
|
||||
* mode: "full", // "profile" | "query" | "full"
|
||||
* addMemory: "always", // "always" | "never"
|
||||
* customId: "conv-456", // Group messages by conversation
|
||||
* threshold: 0.7, // 0.0-1.0 (higher = more accurate)
|
||||
* limit: 15, // Max results to return
|
||||
* rerank: true, // Rerank for best relevance
|
||||
* searchMode: "hybrid", // "memories" | "documents" | "hybrid"
|
||||
* entityContext: "This is John, a software engineer saving technical discussions",
|
||||
* metadata: { // Custom metadata
|
||||
* source: "voltagent",
|
||||
* version: "1.0"
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent(configWithMemory)
|
||||
*
|
||||
* // Use the agent - memories are automatically injected
|
||||
* const result = await agent.generateText({
|
||||
* messages: [{ role: "user", content: "What's my favorite programming language?" }]
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Custom prompt template:
|
||||
* ```typescript
|
||||
* const configWithMemory = withSupermemory({
|
||||
* agentConfig: {
|
||||
* name: "my-agent",
|
||||
* instructions: "...",
|
||||
* llm: new VercelAIProvider(),
|
||||
* model: openai("gpt-4o"),
|
||||
* },
|
||||
* containerTag: "user-123",
|
||||
* customId: "conversation-123",
|
||||
* mode: "full",
|
||||
* promptTemplate: (data) => `
|
||||
* <user_context>
|
||||
* ${data.userMemories}
|
||||
* ${data.generalSearchMemories}
|
||||
* </user_context>
|
||||
* `.trim()
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent(configWithMemory)
|
||||
* ```
|
||||
*
|
||||
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
|
||||
* @throws {Error} When Supermemory API request fails
|
||||
*/
|
||||
export function withSupermemory<T extends VoltAgentConfig>(
|
||||
options: WithSupermemoryOptions<T>,
|
||||
): T {
|
||||
const { agentConfig, containerTag, ...supermemoryOptions } = options
|
||||
|
||||
// Create Supermemory hooks (internally creates its own context, validates API key)
|
||||
const supermemoryHooks = createSupermemoryHooks(
|
||||
containerTag,
|
||||
supermemoryOptions,
|
||||
)
|
||||
|
||||
// Merge with existing hooks if present
|
||||
const mergedHooks = mergeHooks(agentConfig.hooks, supermemoryHooks)
|
||||
|
||||
// Return enhanced config with merged hooks
|
||||
return {
|
||||
...agentConfig,
|
||||
hooks: mergedHooks,
|
||||
}
|
||||
}
|
||||
|
||||
// Export types for consumers
|
||||
export type {
|
||||
SupermemoryVoltAgent,
|
||||
VoltAgentConfig,
|
||||
VoltAgentMessage,
|
||||
VoltAgentHooks,
|
||||
SearchFilters,
|
||||
IncludeOptions,
|
||||
PromptTemplate,
|
||||
MemoryMode,
|
||||
AddMemoryMode,
|
||||
MemoryPromptData,
|
||||
} from "./types"
|
||||
|
||||
export type { WithSupermemoryOptions }
|
||||
|
||||
// Note: WithSupermemoryOptions is exported above separately because it's generic
|
||||
|
||||
// Export hook creation utilities for advanced use cases
|
||||
export { createSupermemoryHooks } from "./hooks"
|
||||
481
packages/tools/src/voltagent/middleware.ts
Normal file
481
packages/tools/src/voltagent/middleware.ts
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
/**
|
||||
* Middleware utilities for VoltAgent integration with Supermemory.
|
||||
*
|
||||
* Provides memory retrieval, injection, and storage functionality.
|
||||
*/
|
||||
|
||||
import Supermemory from "supermemory"
|
||||
import {
|
||||
addConversation,
|
||||
type ConversationMessage,
|
||||
} from "../conversations-client"
|
||||
import {
|
||||
createLogger,
|
||||
normalizeBaseUrl,
|
||||
MemoryCache,
|
||||
buildMemoriesText,
|
||||
extractQueryText,
|
||||
type Logger,
|
||||
type MemoryMode,
|
||||
} from "../shared"
|
||||
import type { SupermemoryVoltAgent, VoltAgentMessage } from "./types"
|
||||
|
||||
/**
|
||||
* Context for Supermemory middleware operations.
|
||||
*/
|
||||
export interface SupermemoryMiddlewareContext {
|
||||
client: Supermemory
|
||||
logger: Logger
|
||||
containerTag: string
|
||||
customId: string
|
||||
mode: MemoryMode
|
||||
addMemory: "always" | "never"
|
||||
normalizedBaseUrl: string
|
||||
apiKey: string
|
||||
promptTemplate?: (data: {
|
||||
userMemories: string
|
||||
generalSearchMemories: string
|
||||
searchResults: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
}) => string
|
||||
/**
|
||||
* Per-turn memory cache. Stores the injected memories string for each
|
||||
* user turn (keyed by turnKey) to avoid redundant API calls.
|
||||
*/
|
||||
memoryCache: MemoryCache<string>
|
||||
// New search parameters
|
||||
threshold?: number
|
||||
limit?: number
|
||||
rerank?: boolean
|
||||
rewriteQuery?: boolean
|
||||
filters?: { OR: Array<unknown> } | { AND: Array<unknown> }
|
||||
include?: {
|
||||
chunks?: boolean
|
||||
documents?: boolean
|
||||
forgottenMemories?: boolean
|
||||
relatedMemories?: boolean
|
||||
summaries?: boolean
|
||||
}
|
||||
// Storage parameters
|
||||
metadata?: Record<string, string | number | boolean>
|
||||
searchMode?: "memories" | "documents" | "hybrid"
|
||||
entityContext?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Supermemory middleware context.
|
||||
*/
|
||||
export const createSupermemoryContext = (
|
||||
containerTag: string,
|
||||
options: SupermemoryVoltAgent,
|
||||
): SupermemoryMiddlewareContext => {
|
||||
const apiKey = options.apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"SUPERMEMORY_API_KEY is not set — provide it via `options.apiKey` or set `process.env.SUPERMEMORY_API_KEY`",
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
customId,
|
||||
mode = "profile",
|
||||
addMemory = "always", // VoltAgent default: save conversations by default for chat apps
|
||||
baseUrl,
|
||||
promptTemplate,
|
||||
threshold,
|
||||
limit,
|
||||
rerank,
|
||||
rewriteQuery,
|
||||
filters,
|
||||
include,
|
||||
metadata,
|
||||
searchMode,
|
||||
entityContext,
|
||||
} = options
|
||||
|
||||
// Runtime validation: customId is required
|
||||
if (!customId || typeof customId !== "string" || customId.trim() === "") {
|
||||
throw new Error(
|
||||
"customId is required and must be a non-empty string — provide it via `options.customId`",
|
||||
)
|
||||
}
|
||||
|
||||
const logger = createLogger(false) // VoltAgent SDK doesn't use verbose
|
||||
const normalizedBaseUrl = normalizeBaseUrl(baseUrl)
|
||||
|
||||
const client = new Supermemory({
|
||||
apiKey,
|
||||
...(normalizedBaseUrl !== "https://api.supermemory.ai"
|
||||
? { baseURL: normalizedBaseUrl }
|
||||
: {}),
|
||||
})
|
||||
|
||||
return {
|
||||
client,
|
||||
logger,
|
||||
containerTag,
|
||||
customId,
|
||||
mode,
|
||||
addMemory,
|
||||
normalizedBaseUrl,
|
||||
apiKey,
|
||||
promptTemplate,
|
||||
memoryCache: new MemoryCache<string>(),
|
||||
threshold,
|
||||
limit,
|
||||
rerank,
|
||||
rewriteQuery,
|
||||
filters,
|
||||
include,
|
||||
metadata,
|
||||
searchMode,
|
||||
entityContext,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cache key for the current turn based on context and user message.
|
||||
*/
|
||||
const makeTurnKey = (
|
||||
ctx: SupermemoryMiddlewareContext,
|
||||
userMessage: string,
|
||||
): string => {
|
||||
return MemoryCache.makeTurnKey(
|
||||
ctx.containerTag,
|
||||
ctx.customId,
|
||||
ctx.mode,
|
||||
userMessage,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is a new user turn (last message is from user).
|
||||
*/
|
||||
const isNewUserTurn = (messages: VoltAgentMessage[]): boolean => {
|
||||
const lastMessage = messages.at(-1)
|
||||
return lastMessage?.role === "user"
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the last user message text from messages array.
|
||||
*/
|
||||
const getLastUserMessage = (messages: VoltAgentMessage[]): string => {
|
||||
const lastUserMessage = messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((msg) => msg.role === "user")
|
||||
|
||||
if (!lastUserMessage) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const content = lastUserMessage.content
|
||||
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text || "")
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and injects memories into messages.
|
||||
* Returns enhanced messages with memories injected into system prompt.
|
||||
*
|
||||
* @param searchMessages - Messages to search for user input (VoltAgent's input messages)
|
||||
* @param ctx - Supermemory middleware context
|
||||
* @param systemMessages - System messages to inject memories into (VoltAgent's prepared messages)
|
||||
*/
|
||||
export const enhanceMessagesWithMemories = async (
|
||||
searchMessages: VoltAgentMessage[],
|
||||
ctx: SupermemoryMiddlewareContext,
|
||||
systemMessages?: VoltAgentMessage[],
|
||||
): Promise<VoltAgentMessage[]> => {
|
||||
const messagesToEnhance = systemMessages || searchMessages
|
||||
const messages = searchMessages
|
||||
|
||||
const userMessage = getLastUserMessage(messages)
|
||||
|
||||
if (ctx.mode !== "profile" && !userMessage) {
|
||||
ctx.logger.debug("No user message found, skipping memory search")
|
||||
return messagesToEnhance
|
||||
}
|
||||
|
||||
const turnKey = makeTurnKey(ctx, userMessage || "")
|
||||
const isNewTurn = isNewUserTurn(messages)
|
||||
|
||||
const cachedMemories = ctx.memoryCache.get(turnKey)
|
||||
if (!isNewTurn && cachedMemories) {
|
||||
ctx.logger.debug("Using cached memories", { turnKey })
|
||||
return injectMemoriesIntoMessages(
|
||||
messagesToEnhance,
|
||||
cachedMemories,
|
||||
ctx.logger,
|
||||
)
|
||||
}
|
||||
|
||||
ctx.logger.info("Starting memory search", {
|
||||
containerTag: ctx.containerTag,
|
||||
customId: ctx.customId,
|
||||
mode: ctx.mode,
|
||||
isNewTurn,
|
||||
})
|
||||
|
||||
const genericMessages = messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}))
|
||||
|
||||
const queryText = extractQueryText(genericMessages, ctx.mode)
|
||||
|
||||
const useAdvancedSearch =
|
||||
ctx.threshold !== undefined ||
|
||||
ctx.limit !== undefined ||
|
||||
ctx.rerank !== undefined ||
|
||||
ctx.rewriteQuery !== undefined ||
|
||||
ctx.filters !== undefined ||
|
||||
ctx.include !== undefined ||
|
||||
ctx.searchMode !== undefined
|
||||
|
||||
let memories: string
|
||||
|
||||
if (useAdvancedSearch && ctx.mode !== "profile") {
|
||||
ctx.logger.info("Using advanced search with custom parameters")
|
||||
|
||||
const searchParams: {
|
||||
q: string
|
||||
containerTag: string
|
||||
threshold?: number
|
||||
limit?: number
|
||||
rerank?: boolean
|
||||
rewriteQuery?: boolean
|
||||
filters?: { OR: Array<unknown> } | { AND: Array<unknown> }
|
||||
include?: {
|
||||
chunks?: boolean
|
||||
documents?: boolean
|
||||
forgottenMemories?: boolean
|
||||
relatedMemories?: boolean
|
||||
summaries?: boolean
|
||||
}
|
||||
searchMode?: "memories" | "documents" | "hybrid"
|
||||
} = {
|
||||
q: queryText,
|
||||
containerTag: ctx.containerTag,
|
||||
}
|
||||
|
||||
if (ctx.threshold !== undefined) searchParams.threshold = ctx.threshold
|
||||
if (ctx.limit !== undefined) searchParams.limit = ctx.limit
|
||||
if (ctx.rerank !== undefined) searchParams.rerank = ctx.rerank
|
||||
if (ctx.rewriteQuery !== undefined)
|
||||
searchParams.rewriteQuery = ctx.rewriteQuery
|
||||
if (ctx.filters !== undefined) searchParams.filters = ctx.filters
|
||||
if (ctx.include !== undefined) searchParams.include = ctx.include
|
||||
if (ctx.searchMode !== undefined) searchParams.searchMode = ctx.searchMode
|
||||
|
||||
const response = await ctx.client.search.memories(searchParams)
|
||||
|
||||
// Hybrid search returns both memory entries (`memory` field) and
|
||||
// document chunks (`chunk` field). Handle both.
|
||||
type SearchResult = {
|
||||
memory?: string
|
||||
chunk?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
const formattedMemories = response.results
|
||||
.map((result: SearchResult) => {
|
||||
const text = result.memory || result.chunk
|
||||
return text ? `- ${text}` : null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
|
||||
memories = ctx.promptTemplate
|
||||
? ctx.promptTemplate({
|
||||
userMemories: "",
|
||||
generalSearchMemories: formattedMemories,
|
||||
searchResults: response.results as Array<{
|
||||
memory: string
|
||||
metadata?: Record<string, unknown>
|
||||
}>,
|
||||
})
|
||||
: `The following are relevant memories and context about this user retrieved from previous interactions. Use these to personalize your response:\n\n${formattedMemories}`
|
||||
} else {
|
||||
memories = await buildMemoriesText({
|
||||
containerTag: ctx.containerTag,
|
||||
queryText,
|
||||
mode: ctx.mode,
|
||||
baseUrl: ctx.normalizedBaseUrl,
|
||||
apiKey: ctx.apiKey,
|
||||
logger: ctx.logger,
|
||||
promptTemplate: ctx.promptTemplate,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.memoryCache.set(turnKey, memories)
|
||||
ctx.logger.debug("Cached memories for turn", { turnKey })
|
||||
|
||||
return injectMemoriesIntoMessages(messagesToEnhance, memories, ctx.logger)
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects memories into messages by appending to existing system prompt
|
||||
* or creating a new one. Pure function - does not mutate the original messages.
|
||||
*
|
||||
* VoltAgent uses AI SDK v5's UIMessage format which requires `id` and `parts`
|
||||
* (not just `content`). We must conform to this format for messages to
|
||||
* actually reach the LLM.
|
||||
*/
|
||||
const injectMemoriesIntoMessages = (
|
||||
messages: VoltAgentMessage[],
|
||||
memories: string,
|
||||
logger: Logger,
|
||||
): VoltAgentMessage[] => {
|
||||
const systemMessageIndex = messages.findIndex((msg) => msg.role === "system")
|
||||
|
||||
if (systemMessageIndex !== -1) {
|
||||
logger.debug("Added memories to existing system message")
|
||||
const newMessages = [...messages]
|
||||
const systemMessage = newMessages[systemMessageIndex]
|
||||
if (!systemMessage) {
|
||||
return messages
|
||||
}
|
||||
|
||||
// Extract existing text from parts (UIMessage format) or content fallback
|
||||
const parts = (
|
||||
systemMessage as { parts?: Array<{ type: string; text?: string }> }
|
||||
).parts
|
||||
const existingContent = parts
|
||||
? parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text || "")
|
||||
.join("\n")
|
||||
: typeof systemMessage.content === "string"
|
||||
? systemMessage.content
|
||||
: ""
|
||||
|
||||
const newContent = `${existingContent}\n\n${memories}`
|
||||
|
||||
newMessages[systemMessageIndex] = {
|
||||
...systemMessage,
|
||||
content: newContent,
|
||||
// Update parts array to match - this is what the LLM actually reads
|
||||
parts: [{ type: "text", text: newContent }],
|
||||
} as VoltAgentMessage
|
||||
return newMessages
|
||||
}
|
||||
|
||||
logger.debug("Created system message with memories")
|
||||
return [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
role: "system" as const,
|
||||
content: memories,
|
||||
parts: [{ type: "text", text: memories }],
|
||||
} as VoltAgentMessage,
|
||||
...messages,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts VoltAgent messages to conversation format for storage.
|
||||
*/
|
||||
const convertToConversationMessages = (
|
||||
messages: VoltAgentMessage[],
|
||||
): ConversationMessage[] => {
|
||||
const conversationMessages: ConversationMessage[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "system") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
if (msg.content) {
|
||||
conversationMessages.push({
|
||||
role: msg.role as "user" | "assistant" | "tool",
|
||||
content: msg.content,
|
||||
})
|
||||
}
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
const contentParts = msg.content
|
||||
.map((c) => {
|
||||
if (c.type === "text" && c.text) {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: c.text,
|
||||
}
|
||||
}
|
||||
// Handle image URLs if present
|
||||
if (c.type === "image_url" && typeof c.image_url === "object") {
|
||||
const imageUrl = c.image_url as { url?: string }
|
||||
if (imageUrl.url) {
|
||||
return {
|
||||
type: "image_url" as const,
|
||||
image_url: { url: imageUrl.url },
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((part) => part !== null)
|
||||
|
||||
if (contentParts.length > 0) {
|
||||
conversationMessages.push({
|
||||
role: msg.role as "user" | "assistant" | "tool",
|
||||
content: contentParts,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conversationMessages
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves conversation to Supermemory (fire-and-forget).
|
||||
*/
|
||||
export const saveConversation = async (
|
||||
messages: VoltAgentMessage[],
|
||||
ctx: SupermemoryMiddlewareContext,
|
||||
): Promise<void> => {
|
||||
if (ctx.addMemory !== "always") {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const conversationMessages = convertToConversationMessages(messages)
|
||||
|
||||
if (conversationMessages.length === 0) {
|
||||
ctx.logger.debug("No messages to save")
|
||||
return
|
||||
}
|
||||
|
||||
const response = await addConversation({
|
||||
conversationId: ctx.customId,
|
||||
messages: conversationMessages,
|
||||
containerTags: [ctx.containerTag],
|
||||
metadata: ctx.metadata,
|
||||
entityContext: ctx.entityContext,
|
||||
apiKey: ctx.apiKey,
|
||||
baseUrl: ctx.normalizedBaseUrl,
|
||||
})
|
||||
|
||||
ctx.logger.info("Conversation saved successfully via /v4/conversations", {
|
||||
containerTag: ctx.containerTag,
|
||||
customId: ctx.customId,
|
||||
messageCount: conversationMessages.length,
|
||||
responseId: response.id,
|
||||
metadata: ctx.metadata,
|
||||
})
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error saving conversation", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
}
|
||||
}
|
||||
210
packages/tools/src/voltagent/types.ts
Normal file
210
packages/tools/src/voltagent/types.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* Type definitions for VoltAgent integration.
|
||||
*
|
||||
* VoltAgent uses hooks to intercept and modify agent behavior. We integrate
|
||||
* Supermemory by providing hooks that inject memories before LLM calls.
|
||||
*/
|
||||
|
||||
import type {
|
||||
PromptTemplate,
|
||||
MemoryMode,
|
||||
AddMemoryMode,
|
||||
MemoryPromptData,
|
||||
SupermemoryBaseOptions,
|
||||
} from "../shared"
|
||||
|
||||
/**
|
||||
* Configuration options for the Supermemory VoltAgent integration.
|
||||
* Extends base options with VoltAgent-specific settings.
|
||||
*/
|
||||
export interface SupermemoryVoltAgent
|
||||
extends Omit<SupermemoryBaseOptions, "verbose"> {
|
||||
/**
|
||||
* Custom ID to group messages into a single document.
|
||||
* Ensures related messages are added to the same document for that conversation.
|
||||
*/
|
||||
customId: string
|
||||
|
||||
/**
|
||||
* Threshold / sensitivity for memory selection. 0 is least sensitive (returns
|
||||
* most memories, more results), 1 is most sensitive (returns fewer memories,
|
||||
* more accurate results). Default: 0.1
|
||||
*/
|
||||
threshold?: number
|
||||
|
||||
/**
|
||||
* Maximum number of memory results to return. Default: 10
|
||||
*/
|
||||
limit?: number
|
||||
|
||||
/**
|
||||
* If true, rerank the results based on the query. This helps ensure the most
|
||||
* relevant results are returned. Default: false
|
||||
*/
|
||||
rerank?: boolean
|
||||
|
||||
/**
|
||||
* If true, rewrites the query to make it easier to find memories. This increases
|
||||
* latency by about 400ms. Default: false
|
||||
*/
|
||||
rewriteQuery?: boolean
|
||||
|
||||
/**
|
||||
* Advanced filters to apply to the search using AND/OR logic.
|
||||
* Example: { OR: [{ metadata: { type: "note" } }, { metadata: { type: "conversation" } }] }
|
||||
*/
|
||||
filters?: SearchFilters
|
||||
|
||||
/**
|
||||
* Control what additional data to include in search results
|
||||
*/
|
||||
include?: IncludeOptions
|
||||
|
||||
/**
|
||||
* Optional metadata to attach to saved documents/conversations.
|
||||
* Can include strings, numbers, or booleans.
|
||||
*/
|
||||
metadata?: Record<string, string | number | boolean>
|
||||
|
||||
/**
|
||||
* Search mode controlling what type of results to search.
|
||||
* - "memories": Search only memory entries (atomic facts)
|
||||
* - "documents": Search only document chunks
|
||||
* - "hybrid": Search both memories AND document chunks (recommended)
|
||||
*/
|
||||
searchMode?: "memories" | "documents" | "hybrid"
|
||||
|
||||
/**
|
||||
* Context for memory extraction when saving conversations.
|
||||
* Helps guide how memories are extracted and understood from content.
|
||||
* Max 1500 characters.
|
||||
* Example: "This is John, saving items in a personal knowledge management system"
|
||||
*/
|
||||
entityContext?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced search filters using AND/OR logic
|
||||
*/
|
||||
export type SearchFilters = { OR: Array<unknown> } | { AND: Array<unknown> }
|
||||
|
||||
/**
|
||||
* Options for including additional data in search results
|
||||
*/
|
||||
export interface IncludeOptions {
|
||||
/**
|
||||
* If true, fetch and return chunks from documents associated with found memories.
|
||||
* Performs vector search on chunks within those documents.
|
||||
*/
|
||||
chunks?: boolean
|
||||
|
||||
/**
|
||||
* If true, include full document information in results
|
||||
*/
|
||||
documents?: boolean
|
||||
|
||||
/**
|
||||
* If true, include forgotten memories in search results. Forgotten memories are
|
||||
* memories that have been explicitly forgotten or have passed their expiration date.
|
||||
*/
|
||||
forgottenMemories?: boolean
|
||||
|
||||
/**
|
||||
* If true, include related memories (parents/children in the memory graph)
|
||||
*/
|
||||
relatedMemories?: boolean
|
||||
|
||||
/**
|
||||
* If true, include document summaries in results
|
||||
*/
|
||||
summaries?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* VoltAgent message format (simplified to avoid direct dependency).
|
||||
* Compatible with VoltAgent's Message type.
|
||||
*/
|
||||
export interface VoltAgentMessage {
|
||||
role: "system" | "user" | "assistant" | "tool"
|
||||
content:
|
||||
| string
|
||||
| Array<{ type: string; text?: string; [key: string]: unknown }>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal VoltAgent AgentConfig interface representing properties we enhance.
|
||||
* This avoids a direct dependency on @voltagent/core while staying type-safe.
|
||||
*/
|
||||
export interface VoltAgentConfig {
|
||||
name: string
|
||||
instructions?: string
|
||||
model?: unknown
|
||||
llm?: unknown
|
||||
hooks?: VoltAgentHooks
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* VoltAgent hooks interface (simplified).
|
||||
* Hooks allow intercepting agent lifecycle events.
|
||||
*/
|
||||
export interface VoltAgentHooks {
|
||||
onStart?: (args: HookStartArgs) => void | Promise<void>
|
||||
onPrepareMessages?: (
|
||||
args: HookPrepareMessagesArgs,
|
||||
) =>
|
||||
| { messages?: VoltAgentMessage[] }
|
||||
| Promise<{ messages?: VoltAgentMessage[] }>
|
||||
onEnd?: (args: HookEndArgs) => void | Promise<void>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments passed to onStart hook.
|
||||
*/
|
||||
export interface HookStartArgs {
|
||||
agent: {
|
||||
name: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
context?: {
|
||||
messages?: VoltAgentMessage[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments passed to onPrepareMessages hook.
|
||||
*/
|
||||
export interface HookPrepareMessagesArgs {
|
||||
messages: VoltAgentMessage[]
|
||||
agent: {
|
||||
name: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
context?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments passed to onEnd hook.
|
||||
*/
|
||||
export interface HookEndArgs {
|
||||
agent: {
|
||||
name: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
context?: {
|
||||
input?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
output?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// Re-export shared types for convenience
|
||||
export type { PromptTemplate, MemoryMode, AddMemoryMode, MemoryPromptData }
|
||||
|
|
@ -1,27 +1,17 @@
|
|||
import { streamText, type ModelMessage } from "ai"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
import { withSupermemory } from "../../../../../src/vercel"
|
||||
import { gateway, streamText, type ModelMessage } from "ai"
|
||||
import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
||||
|
||||
const model = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
const model = withSupermemory(gateway("google/gemini-2.5-flash"), "user-1", {
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY ?? "",
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
conversationId: "chat-session",
|
||||
verbose: true,
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
})
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: ModelMessage[] } = await req.json()
|
||||
|
||||
// Commented out generateText implementation
|
||||
// const { response } = await generateText({
|
||||
// model,
|
||||
// system: "You are a helpful assistant.",
|
||||
// messages,
|
||||
// })
|
||||
// return Response.json({ messages: response.messages })
|
||||
|
||||
// New streaming implementation
|
||||
const result = await streamText({
|
||||
const result = streamText({
|
||||
model,
|
||||
system: "You are a helpful assistant.",
|
||||
messages,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { OpenAI } from "openai"
|
||||
//import { withSupermemory } from "@supermemory/tools/openai"
|
||||
import { withSupermemory } from "../../../../../src/openai"
|
||||
import { withSupermemory } from "@supermemory/tools/openai"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,51 @@
|
|||
import { convertToModelMessages, streamText, type UIMessage } from "ai"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
import { withSupermemory } from "../../../../../src/vercel"
|
||||
import { convertToModelMessages, gateway, streamText, type UIMessage } from "ai"
|
||||
import { withTracing } from "@posthog/ai"
|
||||
import { withSupermemory } from "../../../../../src/ai-sdk"
|
||||
import { PostHog } from "posthog-node"
|
||||
|
||||
const model = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
conversationId: "chat-session",
|
||||
verbose: true,
|
||||
const SUPERMEMORY_USER_ID = "user-1"
|
||||
|
||||
const gatewayModel = gateway("google/gemini-2.5-flash")
|
||||
|
||||
const supermemoryOptions = {
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY ?? "",
|
||||
mode: "full" as const,
|
||||
addMemory: "always" as const,
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL,
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json()
|
||||
|
||||
const posthogApiKey = process.env.POSTHOG_API_KEY
|
||||
const phClient = posthogApiKey
|
||||
? new PostHog(posthogApiKey, {
|
||||
host: process.env.POSTHOG_HOST ?? "https://us.i.posthog.com",
|
||||
})
|
||||
: null
|
||||
|
||||
const innerModel = phClient
|
||||
? withTracing(gatewayModel, phClient, {
|
||||
posthogDistinctId: SUPERMEMORY_USER_ID,
|
||||
posthogProperties: { route: "api/stream" },
|
||||
})
|
||||
: gatewayModel
|
||||
|
||||
const model = withSupermemory(
|
||||
innerModel,
|
||||
SUPERMEMORY_USER_ID,
|
||||
supermemoryOptions,
|
||||
)
|
||||
|
||||
const result = streamText({
|
||||
model,
|
||||
system: "You are a helpful assistant.",
|
||||
messages: convertToModelMessages(messages),
|
||||
messages: await convertToModelMessages(messages),
|
||||
onFinish: phClient
|
||||
? async () => {
|
||||
await phClient.shutdown()
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
return result.toUIMessageStreamResponse()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
"use client"
|
||||
|
||||
import { DefaultChatTransport } from "ai"
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
import { DefaultChatTransport } from "ai"
|
||||
import { useState } from "react"
|
||||
|
||||
export default function Page() {
|
||||
const [input, setInput] = useState("")
|
||||
|
||||
const { messages, sendMessage, status } = useChat({
|
||||
// @ts-expect-error - Type mismatch between ai and @ai-sdk/react versions
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/stream",
|
||||
}),
|
||||
|
|
|
|||
1130
packages/tools/test/chatapp/bun.lock
Normal file
1130
packages/tools/test/chatapp/bun.lock
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -9,23 +9,28 @@
|
|||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/react": "3.0.160",
|
||||
"@posthog/ai": "^7.3.0",
|
||||
"@supermemory/tools": "1.4.1",
|
||||
"ai": "6.0.158",
|
||||
"next": "16.0.0",
|
||||
"openai": "^4.104.0",
|
||||
"posthog-node": "^5.0.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"next": "16.0.0",
|
||||
"ai": "^4.0.0",
|
||||
"@ai-sdk/openai": "^1.0.0",
|
||||
"openai": "^4.104.0",
|
||||
"supermemory": "^1.0.0",
|
||||
"@supermemory/tools": "workspace:*"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.0"
|
||||
"eslint-config-next": "16.0.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"overrides": {
|
||||
"ai": "6.0.158"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,25 @@ describe("Unit: withSupermemory", () => {
|
|||
expect(wrappedModel).toBeDefined()
|
||||
expect(wrappedModel.specificationVersion).toBe("v2")
|
||||
})
|
||||
|
||||
it("should preserve provider, modelId, and spec when they live on the prototype", () => {
|
||||
process.env.SUPERMEMORY_API_KEY = "test-key"
|
||||
|
||||
const proto: LanguageModelV2 = {
|
||||
specificationVersion: "v2",
|
||||
provider: "gateway",
|
||||
modelId: "google/gemini-2.5-flash",
|
||||
supportedUrls: {},
|
||||
doGenerate: vi.fn(),
|
||||
doStream: vi.fn(),
|
||||
}
|
||||
const inner = Object.create(proto) as LanguageModelV2
|
||||
const wrappedModel = withSupermemory(inner, TEST_CONFIG.containerTag)
|
||||
|
||||
expect(wrappedModel.specificationVersion).toBe("v2")
|
||||
expect(wrappedModel.provider).toBe("gateway")
|
||||
expect(wrappedModel.modelId).toBe("google/gemini-2.5-flash")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Memory caching", () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export default defineConfig({
|
|||
"src/claude-memory.ts",
|
||||
"src/openai/index.ts",
|
||||
"src/mastra.ts",
|
||||
"src/voltagent/index.ts",
|
||||
],
|
||||
format: "esm",
|
||||
sourcemap: false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue