mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-08 03:08:21 +00:00
updated mastra and fixed merge conflitcs
This commit is contained in:
commit
eb3eb6da44
14 changed files with 768 additions and 51 deletions
25
.github/workflows/publish-ai-sdk.yml
vendored
25
.github/workflows/publish-ai-sdk.yml
vendored
|
|
@ -23,8 +23,12 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Upgrade npm for trusted publishing support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
|
|
@ -34,11 +38,24 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Check if version changed
|
||||
id: version-check
|
||||
run: |
|
||||
PACKAGE_NAME=$(jq -r '.name' package.json)
|
||||
LOCAL_VERSION=$(jq -r '.version' package.json)
|
||||
NPM_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "0.0.0")
|
||||
if [ "$LOCAL_VERSION" = "$NPM_VERSION" ]; then
|
||||
echo "Version $LOCAL_VERSION already published, skipping."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Publishing $LOCAL_VERSION (npm has $NPM_VERSION)"
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
if: steps.version-check.outputs.changed == 'true'
|
||||
run: bun run build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --access public --verbose
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
if: steps.version-check.outputs.changed == 'true'
|
||||
run: npm publish --access public --provenance
|
||||
|
|
|
|||
25
.github/workflows/publish-memory-graph.yml
vendored
25
.github/workflows/publish-memory-graph.yml
vendored
|
|
@ -23,8 +23,12 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Upgrade npm for trusted publishing support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
|
|
@ -34,11 +38,24 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Check if version changed
|
||||
id: version-check
|
||||
run: |
|
||||
PACKAGE_NAME=$(jq -r '.name' package.json)
|
||||
LOCAL_VERSION=$(jq -r '.version' package.json)
|
||||
NPM_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "0.0.0")
|
||||
if [ "$LOCAL_VERSION" = "$NPM_VERSION" ]; then
|
||||
echo "Version $LOCAL_VERSION already published, skipping."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Publishing $LOCAL_VERSION (npm has $NPM_VERSION)"
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
if: steps.version-check.outputs.changed == 'true'
|
||||
run: bun run build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --access public --verbose
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
if: steps.version-check.outputs.changed == 'true'
|
||||
run: npm publish --access public --provenance
|
||||
|
|
|
|||
25
.github/workflows/publish-tools.yml
vendored
25
.github/workflows/publish-tools.yml
vendored
|
|
@ -23,8 +23,12 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Upgrade npm for trusted publishing support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
|
|
@ -34,11 +38,24 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Check if version changed
|
||||
id: version-check
|
||||
run: |
|
||||
PACKAGE_NAME=$(jq -r '.name' package.json)
|
||||
LOCAL_VERSION=$(jq -r '.version' package.json)
|
||||
NPM_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "0.0.0")
|
||||
if [ "$LOCAL_VERSION" = "$NPM_VERSION" ]; then
|
||||
echo "Version $LOCAL_VERSION already published, skipping."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Publishing $LOCAL_VERSION (npm has $NPM_VERSION)"
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
if: steps.version-check.outputs.changed == 'true'
|
||||
run: bun run build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --access public --verbose
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
if: steps.version-check.outputs.changed == 'true'
|
||||
run: npm publish --access public --provenance
|
||||
|
|
|
|||
|
|
@ -98,8 +98,10 @@ sequenceDiagram
|
|||
|--------|------|---------|-------------|
|
||||
| `apiKey` | `string` | `SUPERMEMORY_API_KEY` env | Your Supermemory API key |
|
||||
| `baseUrl` | `string` | `https://api.supermemory.ai` | Custom API endpoint |
|
||||
| `mode` | `"profile" \| "query" \| "full"` | `"profile"` | Memory search mode |
|
||||
| `mode` | `"profile" \| "query" \| "full"` | `"profile"` | Memory retrieval mode |
|
||||
| `addMemory` | `"always" \| "never"` | `"always"` | Auto-save conversations |
|
||||
| `searchMode` | `"memories" \| "hybrid" \| "documents"` | `"memories"` | Search endpoint mode |
|
||||
| `searchLimit` | `number` | `10` | Max search results per endpoint |
|
||||
| `verbose` | `boolean` | `false` | Enable debug logging |
|
||||
| `promptTemplate` | `function` | - | Custom memory formatting |
|
||||
|
||||
|
|
@ -134,6 +136,55 @@ const agent = new Agent(withSupermemory(config, { containerTag: "user-123", cust
|
|||
|
||||
---
|
||||
|
||||
## Search Mode Options (RAG)
|
||||
|
||||
The `searchMode` option controls which search endpoints are used when `mode` is `"query"` or `"full"`:
|
||||
|
||||
**Memories (Default)** - Searches memory entries only. Low latency, best for conversational context:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(config, {
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "full",
|
||||
searchMode: "memories"
|
||||
}))
|
||||
```
|
||||
|
||||
**Documents** - Searches document chunks only. Best for RAG with uploaded documents:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(config, {
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "full",
|
||||
searchMode: "documents",
|
||||
searchLimit: 20 // Get more chunks for RAG
|
||||
}))
|
||||
```
|
||||
|
||||
**Hybrid** - Searches both memories AND document chunks in parallel. Comprehensive retrieval:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(config, {
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "full",
|
||||
searchMode: "hybrid",
|
||||
searchLimit: 10 // 10 from each endpoint
|
||||
}))
|
||||
```
|
||||
|
||||
### Search Mode Comparison
|
||||
|
||||
| Search Mode | Endpoint | Use Case |
|
||||
|-------------|----------|----------|
|
||||
| `memories` | `/v4/search` | Conversational memory, low latency |
|
||||
| `documents` | `/v3/search` | RAG with uploaded docs |
|
||||
| `hybrid` | Both | Comprehensive context |
|
||||
|
||||
---
|
||||
|
||||
## Saving Conversations
|
||||
|
||||
Enable automatic conversation saving with `addMemory: "always"`. The `customId` parameter groups messages into the same document:
|
||||
|
|
@ -404,6 +455,8 @@ interface SupermemoryMastraOptions {
|
|||
baseUrl?: string
|
||||
mode?: "profile" | "query" | "full"
|
||||
addMemory?: "always" | "never"
|
||||
searchMode?: "memories" | "hybrid" | "documents" // Search endpoint mode
|
||||
searchLimit?: number // Max search results per endpoint (default: 10)
|
||||
verbose?: boolean
|
||||
promptTemplate?: (data: MemoryPromptData) => string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "supermemory-cartesia"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "Supermemory integration for Cartesia Line - memory-enhanced voice agents"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
|
|
|||
|
|
@ -518,8 +518,10 @@ main()
|
|||
|
||||
#### Memory Search Modes
|
||||
|
||||
The `mode` option controls whether profile data is fetched:
|
||||
|
||||
- **`profile`** (default): Fetches user profile memories (static facts + dynamic context)
|
||||
- **`query`**: Searches memories based on the user's message
|
||||
- **`query`**: Searches memories based on the user's message (no profile data)
|
||||
- **`full`**: Combines both profile and query results
|
||||
|
||||
```typescript
|
||||
|
|
@ -545,6 +547,42 @@ const { input } = createSupermemoryProcessors({
|
|||
})
|
||||
```
|
||||
|
||||
#### Search Mode Options (RAG)
|
||||
|
||||
The `searchMode` option controls which search endpoints are used when `mode` is `"query"` or `"full"`:
|
||||
|
||||
- **`memories`** (default): Searches memory entries only - low latency, best for conversational context
|
||||
- **`documents`**: Searches document chunks only - best for RAG with uploaded documents
|
||||
- **`hybrid`**: Searches both memories AND document chunks in parallel - comprehensive retrieval
|
||||
|
||||
```typescript
|
||||
// Search only memories (default) - fast, conversational
|
||||
const { input } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "full",
|
||||
searchMode: "memories"
|
||||
})
|
||||
|
||||
// Search only document chunks - RAG use case
|
||||
const { input } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "full",
|
||||
searchMode: "documents",
|
||||
searchLimit: 20 // Get more chunks for RAG
|
||||
})
|
||||
|
||||
// Hybrid search - both memories and documents
|
||||
const { input } = createSupermemoryProcessors({
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "full",
|
||||
searchMode: "hybrid",
|
||||
searchLimit: 10 // 10 from each endpoint
|
||||
})
|
||||
```
|
||||
|
||||
#### Custom Prompt Templates
|
||||
|
||||
Customize how memories are formatted in the system prompt:
|
||||
|
|
@ -606,8 +644,10 @@ interface SupermemoryMastraOptions {
|
|||
customId: string // Required: Conversation ID to group messages
|
||||
apiKey?: string // Supermemory API key (or use SUPERMEMORY_API_KEY env var)
|
||||
baseUrl?: string // Custom API endpoint
|
||||
mode?: "profile" | "query" | "full" // Memory search mode (default: "profile")
|
||||
mode?: "profile" | "query" | "full" // Memory retrieval mode (default: "profile")
|
||||
addMemory?: "always" | "never" // Auto-save conversations (default: "always")
|
||||
searchMode?: "memories" | "hybrid" | "documents" // Search endpoint mode (default: "memories")
|
||||
searchLimit?: number // Max search results per endpoint (default: 10)
|
||||
verbose?: boolean // Enable debug logging (default: false)
|
||||
promptTemplate?: (data: MemoryPromptData) => string // Custom memory formatting
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@supermemory/tools",
|
||||
"type": "module",
|
||||
"version": "1.5.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Memory tools for AI SDK and OpenAI function calling with supermemory",
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
type Logger,
|
||||
type MemoryMode,
|
||||
type PromptTemplate,
|
||||
type SearchMode,
|
||||
} from "../shared"
|
||||
import {
|
||||
addConversation,
|
||||
|
|
@ -48,6 +49,8 @@ interface ProcessorContext {
|
|||
baseUrl: string
|
||||
mode: MemoryMode
|
||||
addMemory: "always" | "never"
|
||||
searchMode: SearchMode
|
||||
searchLimit: number
|
||||
logger: Logger
|
||||
promptTemplate?: PromptTemplate
|
||||
memoryCache: MemoryCache<string>
|
||||
|
|
@ -92,6 +95,8 @@ function createProcessorContext(
|
|||
baseUrl,
|
||||
mode: options.mode ?? "profile",
|
||||
addMemory: options.addMemory ?? "always",
|
||||
searchMode: options.searchMode ?? "memories",
|
||||
searchLimit: options.searchLimit ?? 10,
|
||||
logger,
|
||||
promptTemplate: options.promptTemplate,
|
||||
memoryCache: new MemoryCache<string>(),
|
||||
|
|
@ -204,6 +209,8 @@ export class SupermemoryInputProcessor implements Processor {
|
|||
apiKey: this.ctx.apiKey,
|
||||
logger: this.ctx.logger,
|
||||
promptTemplate: this.ctx.promptTemplate,
|
||||
searchMode: this.ctx.searchMode,
|
||||
searchLimit: this.ctx.searchLimit,
|
||||
})
|
||||
|
||||
if (memories) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
PromptTemplate,
|
||||
MemoryMode,
|
||||
AddMemoryMode,
|
||||
SearchMode,
|
||||
MemoryPromptData,
|
||||
SupermemoryBaseOptions,
|
||||
} from "../shared"
|
||||
|
|
@ -48,4 +49,10 @@ export interface SupermemoryMastraOptions extends SupermemoryBaseOptions {
|
|||
customId: string
|
||||
}
|
||||
|
||||
export type { PromptTemplate, MemoryMode, AddMemoryMode, MemoryPromptData }
|
||||
export type {
|
||||
PromptTemplate,
|
||||
MemoryMode,
|
||||
AddMemoryMode,
|
||||
SearchMode,
|
||||
MemoryPromptData,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export type {
|
|||
PromptTemplate,
|
||||
MemoryMode,
|
||||
AddMemoryMode,
|
||||
SearchMode,
|
||||
Logger,
|
||||
ProfileStructure,
|
||||
ProfileMarkdownData,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Supermemory from "supermemory"
|
||||
import { deduplicateMemories } from "../tools-shared"
|
||||
import type {
|
||||
Logger,
|
||||
|
|
@ -5,11 +6,13 @@ import type {
|
|||
MemoryPromptData,
|
||||
ProfileStructure,
|
||||
PromptTemplate,
|
||||
SearchMode,
|
||||
} from "./types"
|
||||
import {
|
||||
convertProfileToMarkdown,
|
||||
defaultPromptTemplate,
|
||||
} from "./prompt-builder"
|
||||
import { createSupermemoryClient } from "./context"
|
||||
|
||||
/**
|
||||
* Fetches profile and search results from the Supermemory API.
|
||||
|
|
@ -72,12 +75,148 @@ export interface BuildMemoriesTextOptions {
|
|||
apiKey: string
|
||||
logger: Logger
|
||||
promptTemplate?: PromptTemplate
|
||||
/** Search mode: "memories", "hybrid", or "documents" (default: "memories") */
|
||||
searchMode?: SearchMode
|
||||
/** Maximum number of search results (default: 10) */
|
||||
searchLimit?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Search result structure for unified handling.
|
||||
*/
|
||||
interface SearchResult {
|
||||
content: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for memories using the Supermemory SDK.
|
||||
*
|
||||
* @param client - Supermemory client instance
|
||||
* @param containerTag - Container tag for scoping
|
||||
* @param query - Search query text
|
||||
* @param limit - Maximum number of results
|
||||
* @returns Array of search results
|
||||
*/
|
||||
async function searchMemoriesSDK(
|
||||
client: Supermemory,
|
||||
containerTag: string,
|
||||
query: string,
|
||||
limit: number,
|
||||
): Promise<SearchResult[]> {
|
||||
const response = await client.search.memories({
|
||||
q: query,
|
||||
containerTag,
|
||||
limit,
|
||||
include: { chunks: false },
|
||||
})
|
||||
return (response.results || []).map((r) => ({
|
||||
content: r.memory || "",
|
||||
metadata: r.metadata ?? undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for document chunks using the Supermemory SDK.
|
||||
* Only includes chunks marked as relevant (isRelevant: true).
|
||||
*
|
||||
* @param client - Supermemory client instance
|
||||
* @param containerTag - Container tag for scoping
|
||||
* @param query - Search query text
|
||||
* @param limit - Maximum number of results
|
||||
* @returns Array of search results containing only relevant chunks
|
||||
*/
|
||||
async function searchDocumentsSDK(
|
||||
client: Supermemory,
|
||||
containerTag: string,
|
||||
query: string,
|
||||
limit: number,
|
||||
): Promise<SearchResult[]> {
|
||||
const response = await client.search.documents({
|
||||
q: query,
|
||||
containerTags: [containerTag],
|
||||
limit,
|
||||
})
|
||||
// Extract only relevant chunks from each document result
|
||||
const results: SearchResult[] = []
|
||||
for (const doc of response.results || []) {
|
||||
for (const chunk of doc.chunks || []) {
|
||||
// Only include chunks marked as relevant
|
||||
if (chunk.isRelevant) {
|
||||
results.push({
|
||||
content: chunk.content || "",
|
||||
metadata: doc.metadata ?? undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs search based on the specified search mode.
|
||||
*
|
||||
* @param client - Supermemory client instance
|
||||
* @param containerTag - Container tag for scoping
|
||||
* @param query - Search query text
|
||||
* @param searchMode - Search mode: "memories", "hybrid", or "documents"
|
||||
* @param limit - Maximum number of results per search type
|
||||
* @param logger - Logger instance
|
||||
* @returns Combined array of search results
|
||||
*/
|
||||
async function performSearch(
|
||||
client: Supermemory,
|
||||
containerTag: string,
|
||||
query: string,
|
||||
searchMode: SearchMode,
|
||||
limit: number,
|
||||
logger: Logger,
|
||||
): Promise<SearchResult[]> {
|
||||
logger.debug("Performing search", { searchMode, containerTag, limit })
|
||||
|
||||
switch (searchMode) {
|
||||
case "memories":
|
||||
return searchMemoriesSDK(client, containerTag, query, limit)
|
||||
|
||||
case "documents":
|
||||
return searchDocumentsSDK(client, containerTag, query, limit)
|
||||
|
||||
case "hybrid": {
|
||||
// Run both searches in parallel
|
||||
const [memoriesResults, documentsResults] = await Promise.all([
|
||||
searchMemoriesSDK(client, containerTag, query, limit),
|
||||
searchDocumentsSDK(client, containerTag, query, limit),
|
||||
])
|
||||
|
||||
logger.debug("Hybrid search completed", {
|
||||
memoriesCount: memoriesResults.length,
|
||||
documentsCount: documentsResults.length,
|
||||
})
|
||||
|
||||
// Combine results, memories first
|
||||
return [...memoriesResults, ...documentsResults]
|
||||
}
|
||||
|
||||
default:
|
||||
logger.warn(`Unknown search mode: ${searchMode}, defaulting to memories`)
|
||||
return searchMemoriesSDK(client, containerTag, query, limit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches memories from the API, deduplicates them, and formats them into
|
||||
* the final string to be injected into the system prompt.
|
||||
*
|
||||
* When searchMode is specified, uses the Supermemory SDK search endpoints:
|
||||
* - "memories": Uses search.memories() for memory entries
|
||||
* - "documents": Uses search.documents() for document chunks
|
||||
* - "hybrid": Uses both endpoints in parallel
|
||||
*
|
||||
* The mode option controls whether profile data is included:
|
||||
* - "profile": Only profile data (static/dynamic), no search
|
||||
* - "query": Only search results, no profile data
|
||||
* - "full": Both profile data and search results
|
||||
*
|
||||
* @param options - Configuration for building memories text
|
||||
* @returns The final formatted memories string ready for injection
|
||||
*/
|
||||
|
|
@ -92,48 +231,77 @@ export const buildMemoriesText = async (
|
|||
apiKey,
|
||||
logger,
|
||||
promptTemplate = defaultPromptTemplate,
|
||||
searchMode = "memories",
|
||||
searchLimit = 10,
|
||||
} = options
|
||||
|
||||
const memoriesResponse = await supermemoryProfileSearch(
|
||||
containerTag,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
)
|
||||
// Fetch profile data when mode includes profile (profile or full)
|
||||
let profileData: ProfileStructure | null = null
|
||||
if (mode !== "query") {
|
||||
profileData = await supermemoryProfileSearch(
|
||||
containerTag,
|
||||
mode === "profile" ? "" : queryText, // Only send query for full mode
|
||||
baseUrl,
|
||||
apiKey,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
|
||||
const memoryCountStatic = profileData.profile.static?.length || 0
|
||||
const memoryCountDynamic = profileData.profile.dynamic?.length || 0
|
||||
|
||||
logger.info("Memory search completed", {
|
||||
containerTag,
|
||||
memoryCountStatic,
|
||||
memoryCountDynamic,
|
||||
queryText:
|
||||
queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""),
|
||||
mode,
|
||||
})
|
||||
logger.info("Profile search completed", {
|
||||
containerTag,
|
||||
memoryCountStatic,
|
||||
memoryCountDynamic,
|
||||
mode,
|
||||
})
|
||||
}
|
||||
|
||||
// Perform SDK-based search when mode includes query (query or full)
|
||||
let searchResults: SearchResult[] = []
|
||||
if (mode !== "profile" && queryText) {
|
||||
const client = createSupermemoryClient({ apiKey, baseUrl })
|
||||
searchResults = await performSearch(
|
||||
client,
|
||||
containerTag,
|
||||
queryText,
|
||||
searchMode,
|
||||
searchLimit,
|
||||
logger,
|
||||
)
|
||||
|
||||
logger.info("Search completed", {
|
||||
containerTag,
|
||||
searchMode,
|
||||
searchLimit,
|
||||
resultCount: searchResults.length,
|
||||
queryText:
|
||||
queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""),
|
||||
})
|
||||
}
|
||||
|
||||
// Deduplicate profile memories
|
||||
const deduplicated = deduplicateMemories({
|
||||
static: memoriesResponse.profile.static,
|
||||
dynamic: memoriesResponse.profile.dynamic,
|
||||
searchResults: memoriesResponse.searchResults?.results,
|
||||
static: profileData?.profile.static,
|
||||
dynamic: profileData?.profile.dynamic,
|
||||
searchResults: searchResults.map((r) => ({ memory: r.content })),
|
||||
})
|
||||
|
||||
logger.debug("Memory deduplication completed", {
|
||||
static: {
|
||||
original: memoryCountStatic,
|
||||
original: profileData?.profile.static?.length || 0,
|
||||
deduplicated: deduplicated.static.length,
|
||||
},
|
||||
dynamic: {
|
||||
original: memoryCountDynamic,
|
||||
original: profileData?.profile.dynamic?.length || 0,
|
||||
deduplicated: deduplicated.dynamic.length,
|
||||
},
|
||||
searchResults: {
|
||||
original: memoriesResponse.searchResults?.results?.length,
|
||||
original: searchResults.length,
|
||||
deduplicated: deduplicated.searchResults?.length,
|
||||
},
|
||||
})
|
||||
|
||||
// Build user memories from profile (static + dynamic)
|
||||
const userMemories =
|
||||
mode !== "query"
|
||||
? convertProfileToMarkdown({
|
||||
|
|
@ -144,9 +312,11 @@ export const buildMemoriesText = async (
|
|||
searchResults: { results: [] },
|
||||
})
|
||||
: ""
|
||||
|
||||
// Build search results text
|
||||
const generalSearchMemories =
|
||||
mode !== "profile"
|
||||
? `Search results for user's recent message: \n${deduplicated.searchResults
|
||||
mode !== "profile" && deduplicated.searchResults.length > 0
|
||||
? `Search results for user's recent message:\n${deduplicated.searchResults
|
||||
.map((memory) => `- ${memory}`)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
|
|
@ -154,7 +324,10 @@ export const buildMemoriesText = async (
|
|||
const promptData: MemoryPromptData = {
|
||||
userMemories,
|
||||
generalSearchMemories,
|
||||
searchResults: memoriesResponse.searchResults?.results ?? [],
|
||||
searchResults: searchResults.map((r) => ({
|
||||
memory: r.content,
|
||||
metadata: r.metadata,
|
||||
})),
|
||||
}
|
||||
|
||||
const memories = promptTemplate(promptData)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@ export type MemoryMode = "profile" | "query" | "full"
|
|||
*/
|
||||
export type AddMemoryMode = "always" | "never"
|
||||
|
||||
/**
|
||||
* Search mode for memory retrieval:
|
||||
* - "memories": Search only memory entries (default)
|
||||
* - "hybrid": Search both memories AND document chunks (recommended for RAG)
|
||||
* - "documents": Search only document chunks
|
||||
*/
|
||||
export type SearchMode = "memories" | "hybrid" | "documents"
|
||||
|
||||
/**
|
||||
* Logger interface for consistent logging across integrations.
|
||||
*/
|
||||
|
|
@ -119,6 +127,10 @@ export interface SupermemoryBaseOptions {
|
|||
mode?: MemoryMode
|
||||
/** Memory persistence mode */
|
||||
addMemory?: AddMemoryMode
|
||||
/** Search mode for memory retrieval (default: "memories") */
|
||||
searchMode?: SearchMode
|
||||
/** Maximum number of search results to return (default: 10) */
|
||||
searchLimit?: number
|
||||
/** Enable detailed logging of memory search and injection */
|
||||
verbose?: boolean
|
||||
/** Custom function to format memory data into the system prompt */
|
||||
|
|
|
|||
|
|
@ -169,19 +169,19 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
}),
|
||||
)
|
||||
|
||||
// In query mode, profile API should NOT be called (only SDK search endpoints)
|
||||
const profileCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
expect(profileCalls.length).toBe(0)
|
||||
|
||||
const profileCall = profileCalls[0]
|
||||
if (profileCall?.[1]) {
|
||||
const body = JSON.parse(
|
||||
(profileCall[1] as RequestInit).body as string,
|
||||
)
|
||||
expect(body.q).toBe("What are my favorite programming languages?")
|
||||
}
|
||||
// SDK search endpoint should be called (search.memories uses /v4/search)
|
||||
const searchCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/search"),
|
||||
)
|
||||
expect(searchCalls.length).toBeGreaterThan(0)
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
|
|
|
|||
373
packages/tools/test/shared/memory-client.test.ts
Normal file
373
packages/tools/test/shared/memory-client.test.ts
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
/**
|
||||
* Unit tests for the buildMemoriesText function with searchMode support
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import {
|
||||
buildMemoriesText,
|
||||
type BuildMemoriesTextOptions,
|
||||
} from "../../src/shared/memory-client"
|
||||
import { createLogger } from "../../src/shared/logger"
|
||||
import "dotenv/config"
|
||||
|
||||
// Track mock instances
|
||||
let mockSearchDocuments: ReturnType<typeof vi.fn>
|
||||
let mockSearchMemories: ReturnType<typeof vi.fn>
|
||||
let supermemoryConstructorCalls: any[] = []
|
||||
|
||||
// Mock the Supermemory SDK
|
||||
vi.mock("supermemory", () => {
|
||||
return {
|
||||
default: vi.fn().mockImplementation((config: any) => {
|
||||
supermemoryConstructorCalls.push(config)
|
||||
return {
|
||||
search: {
|
||||
documents: mockSearchDocuments,
|
||||
memories: mockSearchMemories,
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe("buildMemoriesText with searchMode", () => {
|
||||
let originalFetch: typeof globalThis.fetch
|
||||
let fetchMock: ReturnType<typeof vi.fn>
|
||||
const logger = createLogger(false)
|
||||
|
||||
const createMockProfileResponse = (
|
||||
staticMemories: string[] = [],
|
||||
dynamicMemories: string[] = [],
|
||||
searchResults: string[] = [],
|
||||
) => ({
|
||||
profile: {
|
||||
static: staticMemories.map((memory) => ({ memory })),
|
||||
dynamic: dynamicMemories.map((memory) => ({ memory })),
|
||||
},
|
||||
searchResults: {
|
||||
results: searchResults.map((memory) => ({ memory })),
|
||||
},
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = globalThis.fetch
|
||||
fetchMock = vi.fn()
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch
|
||||
|
||||
// Reset mocks
|
||||
mockSearchDocuments = vi.fn().mockResolvedValue({ results: [] })
|
||||
mockSearchMemories = vi.fn().mockResolvedValue({ results: [] })
|
||||
supermemoryConstructorCalls = []
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
describe("searchMode: memories (default)", () => {
|
||||
it("should call profile API AND search.memories when searchMode is memories", async () => {
|
||||
// Mock profile API response
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockProfileResponse(["User profile fact"], [], []),
|
||||
),
|
||||
})
|
||||
|
||||
// Mock search.memories response
|
||||
mockSearchMemories.mockResolvedValue({
|
||||
results: [
|
||||
{ memory: "Memory about TypeScript" },
|
||||
{ memory: "Memory about React" },
|
||||
],
|
||||
})
|
||||
|
||||
const options: BuildMemoriesTextOptions = {
|
||||
containerTag: "test-user",
|
||||
queryText: "test query",
|
||||
mode: "full",
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
apiKey: "test-key",
|
||||
logger,
|
||||
searchMode: "memories",
|
||||
}
|
||||
|
||||
const result = await buildMemoriesText(options)
|
||||
|
||||
// Should call profile API (controlled by mode)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Should instantiate Supermemory SDK and call search.memories
|
||||
expect(supermemoryConstructorCalls.length).toBe(1)
|
||||
expect(mockSearchMemories).toHaveBeenCalledWith({
|
||||
q: "test query",
|
||||
containerTag: "test-user",
|
||||
limit: 10,
|
||||
include: { chunks: false },
|
||||
})
|
||||
|
||||
// Should NOT call search.documents
|
||||
expect(mockSearchDocuments).not.toHaveBeenCalled()
|
||||
|
||||
// Result should contain both profile AND search memories
|
||||
expect(result).toContain("User profile fact")
|
||||
expect(result).toContain("Memory about TypeScript")
|
||||
expect(result).toContain("Memory about React")
|
||||
})
|
||||
})
|
||||
|
||||
describe("searchMode: hybrid", () => {
|
||||
it("should call profile API AND both search.memories and search.documents", async () => {
|
||||
// Mock profile API response
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockProfileResponse(["User likes coding"], [], []),
|
||||
),
|
||||
})
|
||||
|
||||
// Mock search.memories
|
||||
mockSearchMemories.mockResolvedValue({
|
||||
results: [{ memory: "Memory about coding preferences" }],
|
||||
})
|
||||
|
||||
// Mock search.documents
|
||||
mockSearchDocuments.mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
documentId: "doc-1",
|
||||
chunks: [
|
||||
{
|
||||
content: "Document chunk about TypeScript best practices",
|
||||
isRelevant: true,
|
||||
},
|
||||
{ content: "Another chunk about React", isRelevant: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const options: BuildMemoriesTextOptions = {
|
||||
containerTag: "test-user",
|
||||
queryText: "TypeScript tips",
|
||||
mode: "full",
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
apiKey: "test-key",
|
||||
logger,
|
||||
searchMode: "hybrid",
|
||||
searchLimit: 5,
|
||||
}
|
||||
|
||||
const result = await buildMemoriesText(options)
|
||||
|
||||
// Should call profile API (controlled by mode)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Should instantiate Supermemory SDK
|
||||
expect(supermemoryConstructorCalls.length).toBe(1)
|
||||
expect(supermemoryConstructorCalls[0]).toEqual({
|
||||
apiKey: "test-key",
|
||||
})
|
||||
|
||||
// Should call BOTH search.memories and search.documents
|
||||
expect(mockSearchMemories).toHaveBeenCalledWith({
|
||||
q: "TypeScript tips",
|
||||
containerTag: "test-user",
|
||||
limit: 5,
|
||||
include: { chunks: false },
|
||||
})
|
||||
expect(mockSearchDocuments).toHaveBeenCalledWith({
|
||||
q: "TypeScript tips",
|
||||
containerTags: ["test-user"],
|
||||
limit: 5,
|
||||
})
|
||||
|
||||
// Result should contain profile, memories, and document chunks
|
||||
expect(result).toContain("User likes coding")
|
||||
expect(result).toContain("Memory about coding preferences")
|
||||
expect(result).toContain("Document chunk about TypeScript best practices")
|
||||
expect(result).toContain("Another chunk about React")
|
||||
})
|
||||
})
|
||||
|
||||
describe("searchMode: documents", () => {
|
||||
it("should call profile API and search.documents for chunks only", async () => {
|
||||
// Mock profile API
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockProfileResponse(["User profile fact"], [], []),
|
||||
),
|
||||
})
|
||||
|
||||
// Mock Supermemory search.documents
|
||||
mockSearchDocuments.mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
documentId: "doc-1",
|
||||
chunks: [
|
||||
{ content: "RAG chunk 1", isRelevant: true },
|
||||
{ content: "RAG chunk 2", isRelevant: true },
|
||||
{ content: "Irrelevant chunk", isRelevant: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const options: BuildMemoriesTextOptions = {
|
||||
containerTag: "test-user",
|
||||
queryText: "What's in my documents?",
|
||||
mode: "full",
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
apiKey: "test-key",
|
||||
logger,
|
||||
searchMode: "documents",
|
||||
searchLimit: 10,
|
||||
}
|
||||
|
||||
const result = await buildMemoriesText(options)
|
||||
|
||||
// Should call profile API
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Should call search.documents
|
||||
expect(mockSearchDocuments).toHaveBeenCalledWith({
|
||||
q: "What's in my documents?",
|
||||
containerTags: ["test-user"],
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
// Result should contain profile and relevant chunks only
|
||||
expect(result).toContain("User profile fact")
|
||||
expect(result).toContain("RAG chunk 1")
|
||||
expect(result).toContain("RAG chunk 2")
|
||||
// Irrelevant chunk should NOT be included
|
||||
expect(result).not.toContain("Irrelevant chunk")
|
||||
})
|
||||
})
|
||||
|
||||
describe("searchLimit option", () => {
|
||||
it("should use default searchLimit of 10", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse([], [], [])),
|
||||
})
|
||||
|
||||
mockSearchMemories.mockResolvedValue({ results: [] })
|
||||
mockSearchDocuments.mockResolvedValue({ results: [] })
|
||||
|
||||
const options: BuildMemoriesTextOptions = {
|
||||
containerTag: "test-user",
|
||||
queryText: "test",
|
||||
mode: "full",
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
apiKey: "test-key",
|
||||
logger,
|
||||
searchMode: "hybrid",
|
||||
// searchLimit not specified - should default to 10
|
||||
}
|
||||
|
||||
await buildMemoriesText(options)
|
||||
|
||||
expect(mockSearchMemories).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ limit: 10 }),
|
||||
)
|
||||
})
|
||||
|
||||
it("should use custom searchLimit when specified", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse([], [], [])),
|
||||
})
|
||||
|
||||
mockSearchDocuments.mockResolvedValue({ results: [] })
|
||||
|
||||
const options: BuildMemoriesTextOptions = {
|
||||
containerTag: "test-user",
|
||||
queryText: "test",
|
||||
mode: "full",
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
apiKey: "test-key",
|
||||
logger,
|
||||
searchMode: "documents",
|
||||
searchLimit: 25,
|
||||
}
|
||||
|
||||
await buildMemoriesText(options)
|
||||
|
||||
expect(mockSearchDocuments).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ limit: 25 }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should call profile API but not search APIs when queryText is empty", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(createMockProfileResponse(["Profile only"], [], [])),
|
||||
})
|
||||
|
||||
const options: BuildMemoriesTextOptions = {
|
||||
containerTag: "test-user",
|
||||
queryText: "", // Empty query
|
||||
mode: "profile",
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
apiKey: "test-key",
|
||||
logger,
|
||||
searchMode: "documents",
|
||||
}
|
||||
|
||||
const result = await buildMemoriesText(options)
|
||||
|
||||
// Should call profile API (mode controls this)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Should NOT instantiate Supermemory SDK when no query
|
||||
expect(supermemoryConstructorCalls.length).toBe(0)
|
||||
expect(mockSearchDocuments).not.toHaveBeenCalled()
|
||||
|
||||
// Should still return profile
|
||||
expect(result).toContain("Profile only")
|
||||
})
|
||||
|
||||
it("should handle custom baseUrl correctly", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse([], [], [])),
|
||||
})
|
||||
|
||||
mockSearchMemories.mockResolvedValue({ results: [] })
|
||||
|
||||
const options: BuildMemoriesTextOptions = {
|
||||
containerTag: "test-user",
|
||||
queryText: "test",
|
||||
mode: "full",
|
||||
baseUrl: "https://custom.api.example.com",
|
||||
apiKey: "test-key",
|
||||
logger,
|
||||
searchMode: "hybrid",
|
||||
}
|
||||
|
||||
await buildMemoriesText(options)
|
||||
|
||||
// Profile API should use custom base URL
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://custom.api.example.com/v4/profile",
|
||||
expect.any(Object),
|
||||
)
|
||||
|
||||
// Supermemory SDK should be configured with custom base URL
|
||||
expect(supermemoryConstructorCalls.length).toBe(1)
|
||||
expect(supermemoryConstructorCalls[0]).toEqual({
|
||||
apiKey: "test-key",
|
||||
baseURL: "https://custom.api.example.com",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue