diff --git a/.github/workflows/publish-agent-framework-python.yml b/.github/workflows/publish-agent-framework-python.yml new file mode 100644 index 00000000..bb46d3d3 --- /dev/null +++ b/.github/workflows/publish-agent-framework-python.yml @@ -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/ diff --git a/.github/workflows/publish-cartesia-sdk-python.yml b/.github/workflows/publish-cartesia-sdk-python.yml new file mode 100644 index 00000000..14155ac1 --- /dev/null +++ b/.github/workflows/publish-cartesia-sdk-python.yml @@ -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/ diff --git a/.github/workflows/publish-memory-graph.yml b/.github/workflows/publish-memory-graph.yml new file mode 100644 index 00000000..65b24e8d --- /dev/null +++ b/.github/workflows/publish-memory-graph.yml @@ -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 }} diff --git a/.github/workflows/publish-openai-sdk-python.yml b/.github/workflows/publish-openai-sdk-python.yml new file mode 100644 index 00000000..141477a8 --- /dev/null +++ b/.github/workflows/publish-openai-sdk-python.yml @@ -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/ diff --git a/.github/workflows/publish-pipecat-sdk-python.yml b/.github/workflows/publish-pipecat-sdk-python.yml new file mode 100644 index 00000000..a0152ce3 --- /dev/null +++ b/.github/workflows/publish-pipecat-sdk-python.yml @@ -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/ diff --git a/.github/workflows/publish-tools.yml b/.github/workflows/publish-tools.yml new file mode 100644 index 00000000..b8d271de --- /dev/null +++ b/.github/workflows/publish-tools.yml @@ -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 }} diff --git a/apps/docs/changelog/developer-platform.mdx b/apps/docs/changelog/developer-platform.mdx index 4f76925f..eef2ce83 100644 --- a/apps/docs/changelog/developer-platform.mdx +++ b/apps/docs/changelog/developer-platform.mdx @@ -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. diff --git a/apps/docs/changelog/overview.mdx b/apps/docs/changelog/overview.mdx index b27195c6..86dc0bc1 100644 --- a/apps/docs/changelog/overview.mdx +++ b/apps/docs/changelog/overview.mdx @@ -3,6 +3,14 @@ title: "Changelog" description: "New updates and improvements to Supermemory" --- + + +### 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. + + + ### Supermemory CLI diff --git a/apps/docs/connectors/google-drive.mdx b/apps/docs/connectors/google-drive.mdx index 94bc7809..9c3ac2a5 100644 --- a/apps/docs/connectors/google-drive.mdx +++ b/apps/docs/connectors/google-drive.mdx @@ -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. + + +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. + + ## 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" } }' ``` + +For **whole Drive** sync, include `"syncScope": "full"` in `metadata` on the same `POST /v3/connections/google-drive` request instead of `"selected"`. + + ### 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 diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 8a32c873..5731c945 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -161,6 +161,7 @@ "integrations/openai-agents-sdk", "integrations/agent-framework", "integrations/mastra", + "integrations/voltagent", "integrations/langchain", "integrations/crewai", "integrations/agno", diff --git a/apps/docs/integrations/cartesia.mdx b/apps/docs/integrations/cartesia.mdx new file mode 100644 index 00000000..5b790545 --- /dev/null +++ b/apps/docs/integrations/cartesia.mdx @@ -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) diff --git a/apps/docs/integrations/voltagent.mdx b/apps/docs/integrations/voltagent.mdx new file mode 100644 index 00000000..66f12826 --- /dev/null +++ b/apps/docs/integrations/voltagent.mdx @@ -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. + + + Check out the NPM page for more details + + +## 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?" }], +}) +``` + + + **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", + }) + ``` + + +## 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) | + diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index 67706a07..94a0bc74 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -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" diff --git a/apps/web/app/(app)/onboarding/welcome/layout.tsx b/apps/web/app/(app)/onboarding/welcome/layout.tsx index 2b889d38..8a685368 100644 --- a/apps/web/app/(app)/onboarding/welcome/layout.tsx +++ b/apps/web/app/(app)/onboarding/welcome/layout.tsx @@ -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) diff --git a/apps/web/app/(app)/onboarding/welcome/page.tsx b/apps/web/app/(app)/onboarding/welcome/page.tsx index e0df2445..ac3ea599 100644 --- a/apps/web/app/(app)/onboarding/welcome/page.tsx +++ b/apps/web/app/(app)/onboarding/welcome/page.tsx @@ -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} /> diff --git a/apps/web/components/initial-header.tsx b/apps/web/components/initial-header.tsx index 91edfa57..5d8aebc9 100644 --- a/apps/web/components/initial-header.tsx +++ b/apps/web/components/initial-header.tsx @@ -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 (
@@ -24,13 +40,24 @@ export function InitialHeader({
)}
- +
+ {showSkipOnboarding && !isLoading && ( + + )} + +
) } diff --git a/apps/web/components/onboarding/setup/header.tsx b/apps/web/components/onboarding/setup/header.tsx index 59dd4fbb..97c0cea7 100644 --- a/apps/web/components/onboarding/setup/header.tsx +++ b/apps/web/components/onboarding/setup/header.tsx @@ -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() { Setup - {user && ( - - )} +
+ {!isOrgLoading && ( + + )} + {user && } +
) } diff --git a/apps/web/components/onboarding/welcome/input-step.tsx b/apps/web/components/onboarding/welcome/input-step.tsx index 077d4944..7cb7fdb3 100644 --- a/apps/web/components/onboarding/welcome/input-step.tsx +++ b/apps/web/components/onboarding/welcome/input-step.tsx @@ -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 ( { - 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%)", }} />