updated mastra and fixed merge conflitcs

This commit is contained in:
Sreeram Sreedhar 2026-04-18 14:07:11 -07:00
commit eb3eb6da44
14 changed files with 768 additions and 51 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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"

View file

@ -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
}

View file

@ -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",

View file

@ -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) {

View file

@ -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,
}

View file

@ -4,6 +4,7 @@ export type {
PromptTemplate,
MemoryMode,
AddMemoryMode,
SearchMode,
Logger,
ProfileStructure,
ProfileMarkdownData,

View file

@ -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)

View file

@ -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 */

View file

@ -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()
})

View 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",
})
})
})
})