mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-08 03:08:21 +00:00
mastra integration (#717)
adds withSupermemory wrapper and input/output processors for
mastra agents:
- input processor fetches and injects memories into system prompt
before llm calls
- output processor saves conversations to supermemory after
responses
- supports profile, query, and full memory search modes
- includes custom prompt templates and requestcontext support
const agent = new Agent(withSupermemory(
{ id: "my-assistant", model: openai("gpt-4o"), instructions:
"..." },
"user-123",
{ mode: "full", addMemory: "always", threadId: "conv-456" }
))
includes docs as well
this pr also reworks how the tools package works into shared modules
This commit is contained in:
parent
b7e6eb65b9
commit
9553434c9a
38 changed files with 3702 additions and 428 deletions
|
|
@ -153,6 +153,7 @@
|
|||
"integrations/supermemory-sdk",
|
||||
"integrations/ai-sdk",
|
||||
"integrations/openai",
|
||||
"integrations/mastra",
|
||||
"integrations/langchain",
|
||||
"integrations/memory-graph",
|
||||
"integrations/claude-memory",
|
||||
|
|
|
|||
3
apps/docs/images/mastra-icon.svg
Normal file
3
apps/docs/images/mastra-icon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 34 21">
|
||||
<path fill="currentColor" d="M4.5 11.7a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9M10.4 0a4.5 4.5 0 0 1 4.4 5.5c-.3 1.4-.6 3 .2 4.2l1.3 1.8.3.2q.2 0 .3-.2l1.3-1.9c.8-1.1.5-2.7.2-4a4.5 4.5 0 1 1 8.8 0c-.3 1.3-.6 2.8 0 4l1.3 2a4.5 4.5 0 1 1-4.3 3.5c.3-1.3.6-2.8 0-4l-1.2-2h-.2L21.5 11c-.8 1.2-.5 2.8-.2 4.2a4.5 4.5 0 1 1-8.8.2q.5-2-.4-3.8l-.9-1.3q-.9-1.1-2.4-1.6A4.5 4.5 0 0 1 10.4 0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 459 B |
450
apps/docs/integrations/mastra.mdx
Normal file
450
apps/docs/integrations/mastra.mdx
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
---
|
||||
title: "Mastra"
|
||||
sidebarTitle: "Mastra"
|
||||
description: "Add persistent memory to Mastra AI agents with Supermemory processors"
|
||||
icon: "/images/mastra-icon.svg"
|
||||
---
|
||||
|
||||
Integrate Supermemory with [Mastra](https://mastra.ai) to give your AI agents persistent memory. Use the `withSupermemory` wrapper for zero-config setup or processors for fine-grained control.
|
||||
|
||||
<Card title="@supermemory/tools on npm" icon="npm" href="https://www.npmjs.com/package/@supermemory/tools">
|
||||
Check out the NPM page for more details
|
||||
</Card>
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @supermemory/tools @mastra/core
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Wrap your agent config with `withSupermemory` to add memory capabilities:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { withSupermemory } from "@supermemory/tools/mastra"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
// Create agent with memory-enhanced config
|
||||
const agent = new Agent(withSupermemory(
|
||||
{
|
||||
id: "my-assistant",
|
||||
name: "My Assistant",
|
||||
model: openai("gpt-4o"),
|
||||
instructions: "You are a helpful assistant.",
|
||||
},
|
||||
"user-123", // containerTag - scopes memories to this user
|
||||
{
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
}
|
||||
))
|
||||
|
||||
const response = await agent.generate("What do you know about me?")
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Memory saving is disabled by default.** The wrapper only retrieves existing memories. To automatically save conversations:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(
|
||||
{ id: "my-assistant", model: openai("gpt-4o"), ... },
|
||||
"user-123",
|
||||
{
|
||||
addMemory: "always",
|
||||
threadId: "conv-456" // Required for conversation grouping
|
||||
}
|
||||
))
|
||||
```
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
The Mastra integration uses Mastra's native [Processor](https://mastra.ai/docs/agents/processors) interface:
|
||||
|
||||
1. **Input Processor** - Fetches relevant memories from Supermemory and injects them into the system prompt before the LLM call
|
||||
2. **Output Processor** - Optionally saves the conversation to Supermemory after generation completes
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Agent
|
||||
participant InputProcessor
|
||||
participant LLM
|
||||
participant OutputProcessor
|
||||
participant Supermemory
|
||||
|
||||
User->>Agent: Send message
|
||||
Agent->>InputProcessor: Process input
|
||||
InputProcessor->>Supermemory: Fetch memories
|
||||
Supermemory-->>InputProcessor: Return memories
|
||||
InputProcessor->>Agent: Inject into system prompt
|
||||
Agent->>LLM: Generate response
|
||||
LLM-->>Agent: Return response
|
||||
Agent->>OutputProcessor: Process output
|
||||
OutputProcessor->>Supermemory: Save conversation (if enabled)
|
||||
Agent-->>User: Return response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `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 |
|
||||
| `addMemory` | `"always" \| "never"` | `"never"` | Auto-save conversations |
|
||||
| `threadId` | `string` | - | Conversation ID for grouping messages |
|
||||
| `verbose` | `boolean` | `false` | Enable debug logging |
|
||||
| `promptTemplate` | `function` | - | Custom memory formatting |
|
||||
|
||||
---
|
||||
|
||||
## Memory Search Modes
|
||||
|
||||
**Profile Mode (Default)** - Retrieves the user's complete profile without query-based filtering:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(config, "user-123", { mode: "profile" }))
|
||||
```
|
||||
|
||||
**Query Mode** - Searches memories based on the user's message:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(config, "user-123", { mode: "query" }))
|
||||
```
|
||||
|
||||
**Full Mode** - Combines profile AND query-based search for maximum context:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(config, "user-123", { mode: "full" }))
|
||||
|
||||
### Mode Comparison
|
||||
|
||||
| Mode | Description | Use Case |
|
||||
|------|-------------|----------|
|
||||
| `profile` | Static + dynamic user facts | General personalization |
|
||||
| `query` | Semantic search on user message | Specific Q&A |
|
||||
| `full` | Both profile and search | Chatbots, assistants |
|
||||
|
||||
---
|
||||
|
||||
## Saving Conversations
|
||||
|
||||
Enable automatic conversation saving with `addMemory: "always"`. A `threadId` is required to group messages:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(
|
||||
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
|
||||
"user-123",
|
||||
{
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
}
|
||||
))
|
||||
|
||||
// All messages in this conversation are saved
|
||||
await agent.generate("I prefer TypeScript over JavaScript")
|
||||
await agent.generate("My favorite framework is Next.js")
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Without a `threadId`, the output processor will log a warning and skip saving. Always provide a `threadId` when using `addMemory: "always"`.
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
## Custom Prompt Templates
|
||||
|
||||
Customize how memories are formatted and injected:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { withSupermemory } from "@supermemory/tools/mastra"
|
||||
import type { MemoryPromptData } from "@supermemory/tools/mastra"
|
||||
|
||||
const claudePrompt = (data: MemoryPromptData) => `
|
||||
<context>
|
||||
<user_profile>
|
||||
${data.userMemories}
|
||||
</user_profile>
|
||||
<relevant_memories>
|
||||
${data.generalSearchMemories}
|
||||
</relevant_memories>
|
||||
</context>
|
||||
`.trim()
|
||||
|
||||
const agent = new Agent(withSupermemory(
|
||||
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
|
||||
"user-123",
|
||||
{
|
||||
mode: "full",
|
||||
promptTemplate: claudePrompt,
|
||||
}
|
||||
))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Direct Processor Usage
|
||||
|
||||
For advanced use cases, use processors directly instead of the wrapper:
|
||||
|
||||
### Input Processor Only
|
||||
|
||||
Inject memories without saving conversations:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { createSupermemoryProcessor } from "@supermemory/tools/mastra"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const agent = new Agent({
|
||||
id: "my-assistant",
|
||||
name: "My Assistant",
|
||||
model: openai("gpt-4o"),
|
||||
inputProcessors: [
|
||||
createSupermemoryProcessor("user-123", {
|
||||
mode: "full",
|
||||
verbose: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Output Processor Only
|
||||
|
||||
Save conversations without memory injection:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { createSupermemoryOutputProcessor } from "@supermemory/tools/mastra"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const agent = new Agent({
|
||||
id: "my-assistant",
|
||||
name: "My Assistant",
|
||||
model: openai("gpt-4o"),
|
||||
outputProcessors: [
|
||||
createSupermemoryOutputProcessor("user-123", {
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Both Processors
|
||||
|
||||
Use the factory function for shared configuration:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const { input, output } = createSupermemoryProcessors("user-123", {
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
verbose: true,
|
||||
})
|
||||
|
||||
const agent = new Agent({
|
||||
id: "my-assistant",
|
||||
name: "My Assistant",
|
||||
model: openai("gpt-4o"),
|
||||
inputProcessors: [input],
|
||||
outputProcessors: [output],
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using RequestContext
|
||||
|
||||
Mastra's `RequestContext` can provide `threadId` dynamically:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { RequestContext, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context"
|
||||
import { withSupermemory } from "@supermemory/tools/mastra"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const agent = new Agent(withSupermemory(
|
||||
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
|
||||
"user-123",
|
||||
{
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
// threadId not set - will use RequestContext
|
||||
}
|
||||
))
|
||||
|
||||
// Set threadId dynamically via RequestContext
|
||||
const ctx = new RequestContext()
|
||||
ctx.set(MASTRA_THREAD_ID_KEY, "dynamic-thread-id")
|
||||
|
||||
await agent.generate("Hello!", { requestContext: ctx })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verbose Logging
|
||||
|
||||
Enable detailed logging for debugging:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent(withSupermemory(
|
||||
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
|
||||
"user-123",
|
||||
{ verbose: true }
|
||||
))
|
||||
|
||||
// Console output:
|
||||
// [supermemory] Starting memory search { containerTag: "user-123", mode: "profile" }
|
||||
// [supermemory] Found 5 memories
|
||||
// [supermemory] Injected memories into system prompt { length: 1523 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Working with Existing Processors
|
||||
|
||||
The wrapper correctly merges with existing processors in the config:
|
||||
|
||||
```typescript
|
||||
// Supermemory processors are merged correctly:
|
||||
// - Input: [supermemory, myLogging] (supermemory runs first)
|
||||
// - Output: [myAnalytics, supermemory] (supermemory runs last)
|
||||
const agent = new Agent(withSupermemory(
|
||||
{
|
||||
id: "my-assistant",
|
||||
model: openai("gpt-4o"),
|
||||
inputProcessors: [myLoggingProcessor],
|
||||
outputProcessors: [myAnalyticsProcessor],
|
||||
},
|
||||
"user-123"
|
||||
))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### `withSupermemory`
|
||||
|
||||
Enhances a Mastra agent config with memory capabilities.
|
||||
|
||||
```typescript
|
||||
function withSupermemory<T extends AgentConfig>(
|
||||
config: T,
|
||||
containerTag: string,
|
||||
options?: SupermemoryMastraOptions
|
||||
): T
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `config` - The Mastra agent configuration object
|
||||
- `containerTag` - User/container ID for scoping memories
|
||||
- `options` - Configuration options
|
||||
|
||||
**Returns:** Enhanced config with Supermemory processors injected
|
||||
|
||||
### `createSupermemoryProcessor`
|
||||
|
||||
Creates an input processor for memory injection.
|
||||
|
||||
```typescript
|
||||
function createSupermemoryProcessor(
|
||||
containerTag: string,
|
||||
options?: SupermemoryMastraOptions
|
||||
): SupermemoryInputProcessor
|
||||
```
|
||||
|
||||
### `createSupermemoryOutputProcessor`
|
||||
|
||||
Creates an output processor for conversation saving.
|
||||
|
||||
```typescript
|
||||
function createSupermemoryOutputProcessor(
|
||||
containerTag: string,
|
||||
options?: SupermemoryMastraOptions
|
||||
): SupermemoryOutputProcessor
|
||||
```
|
||||
|
||||
### `createSupermemoryProcessors`
|
||||
|
||||
Creates both processors with shared configuration.
|
||||
|
||||
```typescript
|
||||
function createSupermemoryProcessors(
|
||||
containerTag: string,
|
||||
options?: SupermemoryMastraOptions
|
||||
): {
|
||||
input: SupermemoryInputProcessor
|
||||
output: SupermemoryOutputProcessor
|
||||
}
|
||||
```
|
||||
|
||||
### `SupermemoryMastraOptions`
|
||||
|
||||
```typescript
|
||||
interface SupermemoryMastraOptions {
|
||||
apiKey?: string
|
||||
baseUrl?: string
|
||||
mode?: "profile" | "query" | "full"
|
||||
addMemory?: "always" | "never"
|
||||
threadId?: string
|
||||
verbose?: boolean
|
||||
promptTemplate?: (data: MemoryPromptData) => string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
SUPERMEMORY_API_KEY=your_supermemory_key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
Processors gracefully handle errors without breaking the agent:
|
||||
|
||||
- **API errors** - Logged and skipped; agent continues without memories
|
||||
- **Missing API key** - Throws immediately with helpful error message
|
||||
- **Missing threadId** - Warns in console; skips saving
|
||||
|
||||
```typescript
|
||||
// Missing API key throws immediately
|
||||
const agent = new Agent(withSupermemory(
|
||||
{ id: "my-assistant", model: openai("gpt-4o"), instructions: "..." },
|
||||
"user-123",
|
||||
{ apiKey: undefined } // Will check SUPERMEMORY_API_KEY env
|
||||
))
|
||||
// Error: SUPERMEMORY_API_KEY is not set
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Vercel AI SDK" icon="triangle" href="/integrations/ai-sdk">
|
||||
Use with Vercel AI SDK for streamlined development
|
||||
</Card>
|
||||
|
||||
<Card title="User Profiles" icon="user" href="/user-profiles">
|
||||
Learn about user profile management
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
|
@ -15,7 +15,7 @@ const conversation = [
|
|||
// Get user profile + relevant memories for context
|
||||
const profile = await client.profile({
|
||||
containerTag: USER_ID,
|
||||
q: conversation.at(-1)!.content,
|
||||
q: conversation.at(-1)?.content,
|
||||
})
|
||||
|
||||
const context = `Static profile:
|
||||
|
|
@ -25,10 +25,10 @@ Dynamic profile:
|
|||
${profile.profile.dynamic.join("\n")}
|
||||
|
||||
Relevant memories:
|
||||
${profile.searchResults?.results.map((r) => r["content"]).join("\n")}`
|
||||
${profile.searchResults?.results.map((r) => r.content).join("\n")}`
|
||||
|
||||
// Build messages with memory-enriched context
|
||||
const messages = [
|
||||
const _messages = [
|
||||
{ role: "system", content: `User context:\n${context}` },
|
||||
...conversation,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -400,7 +400,10 @@ export function Header({
|
|||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
<FeedbackModal isOpen={isFeedbackOpen} onClose={() => setIsFeedbackOpen(false)} />
|
||||
<FeedbackModal
|
||||
isOpen={isFeedbackOpen}
|
||||
onClose={() => setIsFeedbackOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
|
|||
"correct" | "incorrect" | null
|
||||
>(null)
|
||||
const [isConfirmed, setIsConfirmed] = useState(false)
|
||||
const [processingByUrl, setProcessingByUrl] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
)
|
||||
const [processingByUrl, setProcessingByUrl] = useState<
|
||||
Record<string, boolean>
|
||||
>({})
|
||||
const displayedMemoriesRef = useRef<Set<string>>(new Set())
|
||||
const contextInjectedRef = useRef(false)
|
||||
const draftsBuiltRef = useRef(false)
|
||||
|
|
|
|||
|
|
@ -36,11 +36,14 @@ export function NewOnboardingModal() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
setOpen(false)
|
||||
}
|
||||
}}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Experience the new onboarding</DialogTitle>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ interface AuthContextType {
|
|||
user: SessionData["user"] | null
|
||||
org: Organization | null
|
||||
setActiveOrg: (orgSlug: string) => Promise<void>
|
||||
updateOrgMetadata: (
|
||||
partial: Record<string, unknown>,
|
||||
) => void
|
||||
updateOrgMetadata: (partial: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
|
@ -39,21 +37,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
setOrg(activeOrg)
|
||||
}
|
||||
|
||||
const updateOrgMetadata = useCallback(
|
||||
(partial: Record<string, unknown>) => {
|
||||
setOrg((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
metadata: {
|
||||
...prev.metadata,
|
||||
...partial,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
const updateOrgMetadata = useCallback((partial: Record<string, unknown>) => {
|
||||
setOrg((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
metadata: {
|
||||
...prev.metadata,
|
||||
...partial,
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: ignoring the setActiveOrg dependency
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# @supermemory/tools
|
||||
|
||||
Memory tools for AI SDK and OpenAI function calling with supermemory
|
||||
Memory tools for AI SDK, OpenAI, and Mastra with supermemory
|
||||
|
||||
This package provides supermemory tools for both AI SDK and OpenAI function calling through dedicated submodule exports, each with function-based architectures optimized for their respective use cases.
|
||||
This package provides supermemory tools for AI SDK, OpenAI, and Mastra through dedicated submodule exports, each with function-based architectures optimized for their respective use cases.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -12,9 +12,10 @@ npm install @supermemory/tools
|
|||
|
||||
## Usage
|
||||
|
||||
The package provides two submodule imports:
|
||||
The package provides three submodule imports:
|
||||
- `@supermemory/tools/ai-sdk` - For use with the AI SDK framework (includes `withSupermemory` middleware)
|
||||
- `@supermemory/tools/openai` - For use with OpenAI SDK (includes `withSupermemory` middleware and function calling tools)
|
||||
- `@supermemory/tools/mastra` - For use with Mastra AI agents (includes `withSupermemory` wrapper and processors)
|
||||
|
||||
### AI SDK Usage
|
||||
|
||||
|
|
@ -405,6 +406,194 @@ const addResult = await tools.addMemory({
|
|||
})
|
||||
```
|
||||
|
||||
### Mastra Usage
|
||||
|
||||
Add persistent memory to [Mastra](https://mastra.ai) AI agents. The integration provides processors that:
|
||||
- **Input Processor**: Fetches relevant memories and injects them into the system prompt before LLM calls
|
||||
- **Output Processor**: Optionally saves conversations to Supermemory after responses
|
||||
|
||||
#### Quick Start with `withSupermemory` Wrapper
|
||||
|
||||
The simplest way to add memory to a Mastra agent - wrap your config before creating the Agent:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { withSupermemory } from "@supermemory/tools/mastra"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
// Create agent with memory-enhanced config
|
||||
const agent = new Agent(withSupermemory(
|
||||
{
|
||||
id: "my-assistant",
|
||||
name: "My Assistant",
|
||||
model: openai("gpt-4o"),
|
||||
instructions: "You are a helpful assistant.",
|
||||
},
|
||||
"user-123", // containerTag - scopes memories to this user
|
||||
{
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
}
|
||||
))
|
||||
|
||||
const response = await agent.generate("What do you know about me?")
|
||||
console.log(response.text)
|
||||
```
|
||||
|
||||
#### Direct Processor Usage
|
||||
|
||||
For fine-grained control, use processors directly:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const { input, output } = createSupermemoryProcessors("user-123", {
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
verbose: true, // Enable logging
|
||||
})
|
||||
|
||||
const agent = new Agent({
|
||||
id: "my-assistant",
|
||||
name: "My Assistant",
|
||||
model: openai("gpt-4o"),
|
||||
instructions: "You are a helpful assistant with memory.",
|
||||
inputProcessors: [input],
|
||||
outputProcessors: [output],
|
||||
})
|
||||
|
||||
const response = await agent.generate("What's my favorite programming language?")
|
||||
```
|
||||
|
||||
#### Complete Example
|
||||
|
||||
Here's a full example showing a multi-turn conversation with memory:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
async function main() {
|
||||
const userId = "user-alex-123"
|
||||
const threadId = `thread-${Date.now()}`
|
||||
|
||||
const { input, output } = createSupermemoryProcessors(userId, {
|
||||
mode: "profile", // Fetch user profile memories
|
||||
addMemory: "always", // Save all conversations
|
||||
threadId,
|
||||
verbose: true,
|
||||
})
|
||||
|
||||
const agent = new Agent({
|
||||
id: "memory-assistant",
|
||||
name: "Memory Assistant",
|
||||
instructions: `You are a helpful assistant with memory.
|
||||
Use the memories provided to personalize your responses.`,
|
||||
model: openai("gpt-4o-mini"),
|
||||
inputProcessors: [input],
|
||||
outputProcessors: [output],
|
||||
})
|
||||
|
||||
// First conversation - introduce yourself
|
||||
console.log("User: Hi! I'm Alex, a TypeScript developer.")
|
||||
const r1 = await agent.generate("Hi! I'm Alex, a TypeScript developer.")
|
||||
console.log("Assistant:", r1.text)
|
||||
|
||||
// Second conversation - the agent should remember
|
||||
console.log("\nUser: What do you know about me?")
|
||||
const r2 = await agent.generate("What do you know about me?")
|
||||
console.log("Assistant:", r2.text)
|
||||
}
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
#### Memory Search Modes
|
||||
|
||||
- **`profile`** (default): Fetches user profile memories (static facts + dynamic context)
|
||||
- **`query`**: Searches memories based on the user's message
|
||||
- **`full`**: Combines both profile and query results
|
||||
|
||||
```typescript
|
||||
// Profile mode - good for general personalization
|
||||
const { input } = createSupermemoryProcessors("user-123", { mode: "profile" })
|
||||
|
||||
// Query mode - good for specific lookups
|
||||
const { input } = createSupermemoryProcessors("user-123", { mode: "query" })
|
||||
|
||||
// Full mode - comprehensive context
|
||||
const { input } = createSupermemoryProcessors("user-123", { mode: "full" })
|
||||
```
|
||||
|
||||
#### Custom Prompt Templates
|
||||
|
||||
Customize how memories are formatted in the system prompt:
|
||||
|
||||
```typescript
|
||||
import { createSupermemoryProcessors, type MemoryPromptData } from "@supermemory/tools/mastra"
|
||||
|
||||
const customTemplate = (data: MemoryPromptData) => `
|
||||
<user_context>
|
||||
${data.userMemories}
|
||||
${data.generalSearchMemories}
|
||||
</user_context>
|
||||
`.trim()
|
||||
|
||||
const { input, output } = createSupermemoryProcessors("user-123", {
|
||||
mode: "full",
|
||||
promptTemplate: customTemplate,
|
||||
})
|
||||
```
|
||||
|
||||
#### Using RequestContext for Dynamic Thread IDs
|
||||
|
||||
Instead of hardcoding `threadId`, use Mastra's RequestContext for dynamic values:
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@mastra/core/agent"
|
||||
import { RequestContext, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context"
|
||||
import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
|
||||
|
||||
const { input, output } = createSupermemoryProcessors("user-123", {
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
// threadId not set here - will be read from RequestContext
|
||||
})
|
||||
|
||||
const agent = new Agent({
|
||||
id: "my-assistant",
|
||||
name: "My Assistant",
|
||||
model: openai("gpt-4o"),
|
||||
inputProcessors: [input],
|
||||
outputProcessors: [output],
|
||||
})
|
||||
|
||||
// Set threadId dynamically per request
|
||||
const ctx = new RequestContext()
|
||||
ctx.set(MASTRA_THREAD_ID_KEY, "dynamic-thread-123")
|
||||
|
||||
const response = await agent.generate("Hello!", { requestContext: ctx })
|
||||
```
|
||||
|
||||
#### Mastra Configuration Options
|
||||
|
||||
```typescript
|
||||
interface SupermemoryMastraOptions {
|
||||
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")
|
||||
addMemory?: "always" | "never" // Auto-save conversations (default: "never")
|
||||
threadId?: string // Conversation ID for grouping messages
|
||||
verbose?: boolean // Enable debug logging (default: false)
|
||||
promptTemplate?: (data: MemoryPromptData) => string // Custom memory formatting
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Both modules accept the same configuration interface:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@supermemory/tools",
|
||||
"type": "module",
|
||||
"version": "1.3.68",
|
||||
"version": "1.4.00",
|
||||
"description": "Memory tools for AI SDK and OpenAI function calling with supermemory",
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
|
|
@ -20,13 +20,14 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/provider": "^3.0.0",
|
||||
"@anthropic-ai/sdk": "^0.65.0",
|
||||
"@mastra/core": "^1.0.0",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/bun": "^1.2.21",
|
||||
"dotenv": "^16.6.1",
|
||||
"tsdown": "^0.14.2",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4",
|
||||
"@anthropic-ai/sdk": "^0.65.0"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ai-sdk/provider": "^2.0.0 || ^3.0.0"
|
||||
|
|
@ -41,6 +42,7 @@
|
|||
".": "./dist/index.js",
|
||||
"./ai-sdk": "./dist/ai-sdk.js",
|
||||
"./claude-memory": "./dist/claude-memory.js",
|
||||
"./mastra": "./dist/mastra.js",
|
||||
"./openai": "./dist/openai/index.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
PARAMETER_DESCRIPTIONS,
|
||||
TOOL_DESCRIPTIONS,
|
||||
getContainerTags,
|
||||
} from "./shared"
|
||||
} from "./tools-shared"
|
||||
import type { SupermemoryToolsConfig } from "./types"
|
||||
|
||||
// Export individual tool creators
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Supermemory from "supermemory"
|
||||
import { getContainerTags } from "./shared"
|
||||
import { getContainerTags } from "./tools-shared"
|
||||
import type { SupermemoryToolsConfig } from "./types"
|
||||
|
||||
// Claude Memory Tool Types
|
||||
|
|
@ -57,15 +57,6 @@ export class ClaudeMemoryTool {
|
|||
.replace(/\./g, "_") // Replace . with _
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert customId back to file path
|
||||
* Note: This is lossy since we can't distinguish _ from . or /
|
||||
* We rely on metadata.file_path for accurate path reconstruction
|
||||
*/
|
||||
private customIdToPath(customId: string): string {
|
||||
return "/" + customId.replace(/_/g, "/")
|
||||
}
|
||||
|
||||
constructor(apiKey: string, config?: ClaudeMemoryConfig) {
|
||||
this.client = new Supermemory({
|
||||
apiKey,
|
||||
|
|
@ -182,7 +173,7 @@ export class ClaudeMemoryTool {
|
|||
// If path ends with / or is exactly /memories, it's a directory listing request
|
||||
if (path.endsWith("/") || path === "/memories") {
|
||||
// Normalize path to end with /
|
||||
const dirPath = path.endsWith("/") ? path : path + "/"
|
||||
const dirPath = path.endsWith("/") ? path : `${path}/`
|
||||
return await this.listDirectory(dirPath)
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +218,7 @@ export class ClaudeMemoryTool {
|
|||
const slashIndex = relativePath.indexOf("/")
|
||||
if (slashIndex > 0) {
|
||||
// It's a subdirectory
|
||||
dirs.add(relativePath.substring(0, slashIndex) + "/")
|
||||
dirs.add(`${relativePath.substring(0, slashIndex)}/`)
|
||||
} else if (relativePath !== "") {
|
||||
// It's a file in this directory
|
||||
files.push(relativePath)
|
||||
|
|
@ -335,7 +326,7 @@ export class ClaudeMemoryTool {
|
|||
try {
|
||||
const normalizedId = this.normalizePathToCustomId(filePath)
|
||||
|
||||
const response = await this.client.add({
|
||||
const _response = await this.client.add({
|
||||
content: fileText,
|
||||
customId: normalizedId,
|
||||
containerTags: this.containerTags,
|
||||
|
|
@ -394,7 +385,7 @@ export class ClaudeMemoryTool {
|
|||
|
||||
// Update the document
|
||||
const normalizedId = this.normalizePathToCustomId(filePath)
|
||||
const updateResponse = await this.client.add({
|
||||
const _updateResponse = await this.client.add({
|
||||
content: newContent,
|
||||
customId: normalizedId,
|
||||
containerTags: this.containerTags,
|
||||
|
|
|
|||
1
packages/tools/src/mastra.ts
Normal file
1
packages/tools/src/mastra.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./mastra/index"
|
||||
28
packages/tools/src/mastra/index.ts
Normal file
28
packages/tools/src/mastra/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
export { withSupermemory } from "./wrapper"
|
||||
|
||||
export {
|
||||
SupermemoryInputProcessor,
|
||||
SupermemoryOutputProcessor,
|
||||
createSupermemoryProcessor,
|
||||
createSupermemoryOutputProcessor,
|
||||
createSupermemoryProcessors,
|
||||
} from "./processor"
|
||||
|
||||
export type {
|
||||
SupermemoryMastraOptions,
|
||||
Processor,
|
||||
ProcessInputArgs,
|
||||
ProcessInputResult,
|
||||
ProcessOutputResultArgs,
|
||||
ProcessorMessageResult,
|
||||
MastraDBMessage,
|
||||
MastraMessageContentV2,
|
||||
MessageList,
|
||||
RequestContext,
|
||||
InputProcessor,
|
||||
OutputProcessor,
|
||||
PromptTemplate,
|
||||
MemoryMode,
|
||||
AddMemoryMode,
|
||||
MemoryPromptData,
|
||||
} from "./types"
|
||||
431
packages/tools/src/mastra/processor.ts
Normal file
431
packages/tools/src/mastra/processor.ts
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
/**
|
||||
* Mastra Processors for Supermemory Integration
|
||||
*
|
||||
* This module provides input and output processors for Mastra agents that enable:
|
||||
* - Memory injection: Fetches relevant user memories before LLM calls
|
||||
* - Conversation saving: Persists conversations to Supermemory after responses
|
||||
*
|
||||
* Processors integrate with Mastra's processor pipeline:
|
||||
* - InputProcessor runs before the LLM call, injecting memories into system messages
|
||||
* - OutputProcessor runs after the LLM responds, optionally saving the conversation
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
createLogger,
|
||||
normalizeBaseUrl,
|
||||
validateApiKey,
|
||||
MemoryCache,
|
||||
buildMemoriesText,
|
||||
extractQueryText,
|
||||
type Logger,
|
||||
type MemoryMode,
|
||||
type PromptTemplate,
|
||||
} from "../shared"
|
||||
import {
|
||||
addConversation,
|
||||
type ConversationMessage,
|
||||
} from "../conversations-client"
|
||||
import { MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context"
|
||||
import type {
|
||||
SupermemoryMastraOptions,
|
||||
Processor,
|
||||
ProcessInputArgs,
|
||||
ProcessInputResult,
|
||||
ProcessOutputResultArgs,
|
||||
MastraDBMessage,
|
||||
RequestContext,
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
* Internal context shared between input and output processors.
|
||||
*/
|
||||
interface ProcessorContext {
|
||||
containerTag: string
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
mode: MemoryMode
|
||||
addMemory: "always" | "never"
|
||||
threadId?: string
|
||||
logger: Logger
|
||||
promptTemplate?: PromptTemplate
|
||||
memoryCache: MemoryCache<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the shared processor context from options.
|
||||
*/
|
||||
function createProcessorContext(
|
||||
containerTag: string,
|
||||
options: SupermemoryMastraOptions = {},
|
||||
): ProcessorContext {
|
||||
const apiKey = validateApiKey(options.apiKey)
|
||||
const baseUrl = normalizeBaseUrl(options.baseUrl)
|
||||
const logger = createLogger(options.verbose ?? false)
|
||||
|
||||
return {
|
||||
containerTag,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
mode: options.mode ?? "profile",
|
||||
addMemory: options.addMemory ?? "never",
|
||||
threadId: options.threadId,
|
||||
logger,
|
||||
promptTemplate: options.promptTemplate,
|
||||
memoryCache: new MemoryCache<string>(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the effective threadId from options or RequestContext.
|
||||
*/
|
||||
function getEffectiveThreadId(
|
||||
ctx: ProcessorContext,
|
||||
requestContext?: RequestContext,
|
||||
): string | undefined {
|
||||
if (ctx.threadId) {
|
||||
return ctx.threadId
|
||||
}
|
||||
if (requestContext) {
|
||||
return requestContext.get(MASTRA_THREAD_ID_KEY) as string | undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Input processor that injects memories into the system prompt before LLM calls.
|
||||
*
|
||||
* This processor runs once at the start of agent execution (processInput).
|
||||
* It fetches relevant memories from Supermemory based on the user's message
|
||||
* and injects them into the system messages.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent } from "@mastra/core/agent"
|
||||
* import { SupermemoryInputProcessor } from "@supermemory/tools/mastra"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const agent = new Agent({
|
||||
* id: "my-agent",
|
||||
* name: "My Agent",
|
||||
* model: openai("gpt-4o"),
|
||||
* inputProcessors: [
|
||||
* new SupermemoryInputProcessor("user-123", {
|
||||
* mode: "full",
|
||||
* verbose: true,
|
||||
* }),
|
||||
* ],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class SupermemoryInputProcessor implements Processor {
|
||||
readonly id = "supermemory-input"
|
||||
readonly name = "Supermemory Memory Injection"
|
||||
|
||||
private ctx: ProcessorContext
|
||||
|
||||
constructor(containerTag: string, options: SupermemoryMastraOptions = {}) {
|
||||
this.ctx = createProcessorContext(containerTag, options)
|
||||
}
|
||||
|
||||
async processInput(args: ProcessInputArgs): Promise<ProcessInputResult> {
|
||||
const { messages, messageList, requestContext } = args
|
||||
|
||||
try {
|
||||
const queryText = extractQueryText(
|
||||
messages as unknown as Array<{
|
||||
role: string
|
||||
content: string | Array<{ type: string; text?: string }>
|
||||
}>,
|
||||
this.ctx.mode,
|
||||
)
|
||||
|
||||
if (this.ctx.mode !== "profile" && !queryText) {
|
||||
this.ctx.logger.debug("No user message found, skipping memory search")
|
||||
return messageList
|
||||
}
|
||||
|
||||
const effectiveThreadId = getEffectiveThreadId(this.ctx, requestContext)
|
||||
const turnKey = MemoryCache.makeTurnKey(
|
||||
this.ctx.containerTag,
|
||||
effectiveThreadId,
|
||||
this.ctx.mode,
|
||||
queryText || "",
|
||||
)
|
||||
|
||||
const cachedMemories = this.ctx.memoryCache.get(turnKey)
|
||||
if (cachedMemories) {
|
||||
this.ctx.logger.debug("Using cached memories", { turnKey })
|
||||
messageList.addSystem(cachedMemories, "supermemory")
|
||||
return messageList
|
||||
}
|
||||
|
||||
this.ctx.logger.info("Starting memory search", {
|
||||
containerTag: this.ctx.containerTag,
|
||||
threadId: effectiveThreadId,
|
||||
mode: this.ctx.mode,
|
||||
})
|
||||
|
||||
const memories = await buildMemoriesText({
|
||||
containerTag: this.ctx.containerTag,
|
||||
queryText: queryText || "",
|
||||
mode: this.ctx.mode,
|
||||
baseUrl: this.ctx.baseUrl,
|
||||
apiKey: this.ctx.apiKey,
|
||||
logger: this.ctx.logger,
|
||||
promptTemplate: this.ctx.promptTemplate,
|
||||
})
|
||||
|
||||
if (memories) {
|
||||
this.ctx.memoryCache.set(turnKey, memories)
|
||||
messageList.addSystem(memories, "supermemory")
|
||||
this.ctx.logger.debug("Injected memories into system prompt", {
|
||||
length: memories.length,
|
||||
})
|
||||
}
|
||||
|
||||
return messageList
|
||||
} catch (error) {
|
||||
this.ctx.logger.error("Error fetching memories", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
return messageList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Output processor that saves conversations to Supermemory after generation completes.
|
||||
*
|
||||
* This processor runs once after generation completes (processOutputResult).
|
||||
* When addMemory is set to "always", it saves the conversation to Supermemory
|
||||
* using the /v4/conversations API for thread-based storage.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent } from "@mastra/core/agent"
|
||||
* import { SupermemoryOutputProcessor } from "@supermemory/tools/mastra"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const agent = new Agent({
|
||||
* id: "my-agent",
|
||||
* name: "My Agent",
|
||||
* model: openai("gpt-4o"),
|
||||
* outputProcessors: [
|
||||
* new SupermemoryOutputProcessor("user-123", {
|
||||
* addMemory: "always",
|
||||
* threadId: "conv-456",
|
||||
* }),
|
||||
* ],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class SupermemoryOutputProcessor implements Processor {
|
||||
readonly id = "supermemory-output"
|
||||
readonly name = "Supermemory Conversation Save"
|
||||
|
||||
private ctx: ProcessorContext
|
||||
|
||||
constructor(containerTag: string, options: SupermemoryMastraOptions = {}) {
|
||||
this.ctx = createProcessorContext(containerTag, options)
|
||||
}
|
||||
|
||||
async processOutputResult(
|
||||
args: ProcessOutputResultArgs,
|
||||
): Promise<MastraDBMessage[]> {
|
||||
const { messages, messageList, requestContext } = args
|
||||
|
||||
if (this.ctx.addMemory !== "always") {
|
||||
return messages
|
||||
}
|
||||
|
||||
const effectiveThreadId = getEffectiveThreadId(this.ctx, requestContext)
|
||||
if (!effectiveThreadId) {
|
||||
this.ctx.logger.warn(
|
||||
"No threadId provided for conversation save. Provide via options.threadId or RequestContext.",
|
||||
)
|
||||
return messages
|
||||
}
|
||||
|
||||
try {
|
||||
const conversationMessages = this.convertToConversationMessages(messages)
|
||||
|
||||
if (conversationMessages.length === 0) {
|
||||
this.ctx.logger.debug("No messages to save")
|
||||
return messages
|
||||
}
|
||||
|
||||
const response = await addConversation({
|
||||
conversationId: effectiveThreadId,
|
||||
messages: conversationMessages,
|
||||
containerTags: [this.ctx.containerTag],
|
||||
apiKey: this.ctx.apiKey,
|
||||
baseUrl: this.ctx.baseUrl,
|
||||
})
|
||||
|
||||
this.ctx.logger.info("Conversation saved successfully", {
|
||||
containerTag: this.ctx.containerTag,
|
||||
conversationId: effectiveThreadId,
|
||||
messageCount: conversationMessages.length,
|
||||
responseId: response.id,
|
||||
})
|
||||
} catch (error) {
|
||||
this.ctx.logger.error("Error saving conversation", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
private convertToConversationMessages(
|
||||
messages: MastraDBMessage[],
|
||||
): ConversationMessage[] {
|
||||
const result: ConversationMessage[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "system") {
|
||||
continue
|
||||
}
|
||||
|
||||
const role = msg.role as "user" | "assistant"
|
||||
const content = msg.content
|
||||
|
||||
if (content.content && typeof content.content === "string") {
|
||||
result.push({ role, content: content.content })
|
||||
continue
|
||||
}
|
||||
|
||||
if (content.parts && Array.isArray(content.parts)) {
|
||||
const textParts = content.parts
|
||||
.filter(
|
||||
(part): part is { type: "text"; text: string } =>
|
||||
part.type === "text" &&
|
||||
"text" in part &&
|
||||
typeof part.text === "string",
|
||||
)
|
||||
.map((part) => ({
|
||||
type: "text" as const,
|
||||
text: part.text,
|
||||
}))
|
||||
|
||||
if (textParts.length > 0) {
|
||||
result.push({ role, content: textParts })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Supermemory input processor for memory injection.
|
||||
*
|
||||
* @param containerTag - The container tag/user ID for scoping memories
|
||||
* @param options - Configuration options
|
||||
* @returns Configured SupermemoryInputProcessor instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent } from "@mastra/core/agent"
|
||||
* import { createSupermemoryProcessor } from "@supermemory/tools/mastra"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const processor = createSupermemoryProcessor("user-123", {
|
||||
* mode: "full",
|
||||
* verbose: true,
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent({
|
||||
* id: "my-agent",
|
||||
* name: "My Agent",
|
||||
* model: openai("gpt-4o"),
|
||||
* inputProcessors: [processor],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createSupermemoryProcessor(
|
||||
containerTag: string,
|
||||
options: SupermemoryMastraOptions = {},
|
||||
): SupermemoryInputProcessor {
|
||||
return new SupermemoryInputProcessor(containerTag, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Supermemory output processor for saving conversations.
|
||||
*
|
||||
* @param containerTag - The container tag/user ID for scoping memories
|
||||
* @param options - Configuration options
|
||||
* @returns Configured SupermemoryOutputProcessor instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent } from "@mastra/core/agent"
|
||||
* import { createSupermemoryOutputProcessor } from "@supermemory/tools/mastra"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const processor = createSupermemoryOutputProcessor("user-123", {
|
||||
* addMemory: "always",
|
||||
* threadId: "conv-456",
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent({
|
||||
* id: "my-agent",
|
||||
* name: "My Agent",
|
||||
* model: openai("gpt-4o"),
|
||||
* outputProcessors: [processor],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createSupermemoryOutputProcessor(
|
||||
containerTag: string,
|
||||
options: SupermemoryMastraOptions = {},
|
||||
): SupermemoryOutputProcessor {
|
||||
return new SupermemoryOutputProcessor(containerTag, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates both input and output processors with shared configuration.
|
||||
*
|
||||
* Use this when you want both memory injection and conversation saving
|
||||
* with consistent settings across both processors.
|
||||
*
|
||||
* @param containerTag - The container tag/user ID for scoping memories
|
||||
* @param options - Configuration options shared by both processors
|
||||
* @returns Object containing both input and output processors
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent } from "@mastra/core/agent"
|
||||
* import { createSupermemoryProcessors } from "@supermemory/tools/mastra"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const { input, output } = createSupermemoryProcessors("user-123", {
|
||||
* mode: "full",
|
||||
* addMemory: "always",
|
||||
* threadId: "conv-456",
|
||||
* })
|
||||
*
|
||||
* const agent = new Agent({
|
||||
* id: "my-agent",
|
||||
* name: "My Agent",
|
||||
* model: openai("gpt-4o"),
|
||||
* inputProcessors: [input],
|
||||
* outputProcessors: [output],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createSupermemoryProcessors(
|
||||
containerTag: string,
|
||||
options: SupermemoryMastraOptions = {},
|
||||
): {
|
||||
input: SupermemoryInputProcessor
|
||||
output: SupermemoryOutputProcessor
|
||||
} {
|
||||
return {
|
||||
input: new SupermemoryInputProcessor(containerTag, options),
|
||||
output: new SupermemoryOutputProcessor(containerTag, options),
|
||||
}
|
||||
}
|
||||
47
packages/tools/src/mastra/types.ts
Normal file
47
packages/tools/src/mastra/types.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Type definitions for Mastra integration.
|
||||
*
|
||||
* We import core types directly from @mastra/core to ensure perfect compatibility.
|
||||
* Custom types are only defined where @mastra/core doesn't export what we need.
|
||||
*/
|
||||
|
||||
import type {
|
||||
PromptTemplate,
|
||||
MemoryMode,
|
||||
AddMemoryMode,
|
||||
MemoryPromptData,
|
||||
SupermemoryBaseOptions,
|
||||
} from "../shared"
|
||||
|
||||
// Re-export Mastra core types for consumers
|
||||
export type {
|
||||
Processor,
|
||||
ProcessInputArgs,
|
||||
ProcessInputResult,
|
||||
ProcessOutputResultArgs,
|
||||
ProcessorMessageResult,
|
||||
InputProcessor,
|
||||
OutputProcessor,
|
||||
} from "@mastra/core/processors"
|
||||
|
||||
export type {
|
||||
MastraDBMessage,
|
||||
MastraMessageContentV2,
|
||||
MessageList,
|
||||
} from "@mastra/core/agent"
|
||||
|
||||
export type { RequestContext } from "@mastra/core/request-context"
|
||||
|
||||
/**
|
||||
* Configuration options for the Supermemory Mastra processor.
|
||||
* Extends base options with Mastra-specific settings.
|
||||
*/
|
||||
export interface SupermemoryMastraOptions extends SupermemoryBaseOptions {
|
||||
/**
|
||||
* When using the output processor, set this to enable automatic conversation saving.
|
||||
* The threadId is used to group messages into a single conversation.
|
||||
*/
|
||||
threadId?: string
|
||||
}
|
||||
|
||||
export type { PromptTemplate, MemoryMode, AddMemoryMode, MemoryPromptData }
|
||||
100
packages/tools/src/mastra/wrapper.ts
Normal file
100
packages/tools/src/mastra/wrapper.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* Wrapper utilities for enhancing Mastra agent configurations with Supermemory.
|
||||
*
|
||||
* Since Mastra Agent instances have private properties that can't be modified
|
||||
* after construction, we provide utilities that work with agent configs.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { validateApiKey } from "../shared"
|
||||
import {
|
||||
SupermemoryInputProcessor,
|
||||
SupermemoryOutputProcessor,
|
||||
} from "./processor"
|
||||
import type { SupermemoryMastraOptions, Processor } from "./types"
|
||||
|
||||
/**
|
||||
* Minimal AgentConfig interface representing the properties we need to enhance.
|
||||
* This avoids a direct dependency on @mastra/core while staying type-safe.
|
||||
*/
|
||||
interface AgentConfig {
|
||||
id: string
|
||||
name?: string
|
||||
inputProcessors?: Processor[]
|
||||
outputProcessors?: Processor[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances a Mastra agent configuration with Supermemory memory capabilities.
|
||||
*
|
||||
* This function takes an agent config object and returns a new config with
|
||||
* Supermemory processors injected. Use this before creating your Agent instance.
|
||||
*
|
||||
* The enhanced config includes:
|
||||
* - Input processor: Fetches relevant memories before LLM calls
|
||||
* - Output processor: Optionally saves conversations after responses
|
||||
*
|
||||
* @param config - The Mastra agent configuration to enhance
|
||||
* @param containerTag - The container tag/user ID for scoping memories
|
||||
* @param options - Configuration options for memory behavior
|
||||
* @returns Enhanced agent config with Supermemory processors injected
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent } from "@mastra/core/agent"
|
||||
* import { withSupermemory } from "@supermemory/tools/mastra"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
*
|
||||
* const config = withSupermemory(
|
||||
* {
|
||||
* id: "my-agent",
|
||||
* name: "My Agent",
|
||||
* model: openai("gpt-4o"),
|
||||
* instructions: "You are a helpful assistant.",
|
||||
* },
|
||||
* "user-123",
|
||||
* {
|
||||
* mode: "full",
|
||||
* addMemory: "always",
|
||||
* threadId: "conv-456",
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* const agent = new Agent(config)
|
||||
* ```
|
||||
*
|
||||
* @throws {Error} When neither `options.apiKey` nor `process.env.SUPERMEMORY_API_KEY` are set
|
||||
*/
|
||||
export function withSupermemory<T extends AgentConfig>(
|
||||
config: T,
|
||||
containerTag: string,
|
||||
options: SupermemoryMastraOptions = {},
|
||||
): T {
|
||||
validateApiKey(options.apiKey)
|
||||
|
||||
const inputProcessor = new SupermemoryInputProcessor(containerTag, options)
|
||||
const outputProcessor = new SupermemoryOutputProcessor(containerTag, options)
|
||||
|
||||
const existingInputProcessors = config.inputProcessors ?? []
|
||||
const existingOutputProcessors = config.outputProcessors ?? []
|
||||
|
||||
// Supermemory input processor runs first (before other processors)
|
||||
const mergedInputProcessors: Processor[] = [
|
||||
inputProcessor,
|
||||
...existingInputProcessors,
|
||||
]
|
||||
|
||||
// Supermemory output processor runs last (after other processors)
|
||||
const mergedOutputProcessors: Processor[] = [
|
||||
...existingOutputProcessors,
|
||||
outputProcessor,
|
||||
]
|
||||
|
||||
return {
|
||||
...config,
|
||||
inputProcessors: mergedInputProcessors,
|
||||
outputProcessors: mergedOutputProcessors,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type OpenAI from "openai"
|
||||
import Supermemory from "supermemory"
|
||||
import { addConversation } from "../conversations-client"
|
||||
import { deduplicateMemories } from "../shared"
|
||||
import { deduplicateMemories } from "../tools-shared"
|
||||
import { createLogger, type Logger } from "../vercel/logger"
|
||||
import { convertProfileToMarkdown } from "../vercel/util"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
PARAMETER_DESCRIPTIONS,
|
||||
TOOL_DESCRIPTIONS,
|
||||
getContainerTags,
|
||||
} from "../shared"
|
||||
} from "../tools-shared"
|
||||
import type { SupermemoryToolsConfig } from "../types"
|
||||
|
||||
/**
|
||||
|
|
|
|||
79
packages/tools/src/shared/cache.ts
Normal file
79
packages/tools/src/shared/cache.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import type { MemoryMode } from "./types"
|
||||
|
||||
/**
|
||||
* Generic memory cache for storing per-turn memories to avoid redundant API calls.
|
||||
* Used to cache memory retrieval results during tool-call loops within the same turn.
|
||||
*/
|
||||
export class MemoryCache<T = string> {
|
||||
private cache: Map<string, T> = new Map()
|
||||
|
||||
/**
|
||||
* Generates a cache key for the current turn based on context parameters.
|
||||
* Normalizes the message by trimming and collapsing whitespace.
|
||||
*
|
||||
* @param containerTag - The container tag/user ID
|
||||
* @param threadId - Optional thread/conversation ID
|
||||
* @param mode - The memory retrieval mode
|
||||
* @param message - The user message content
|
||||
* @returns A unique cache key for this turn
|
||||
*/
|
||||
static makeTurnKey(
|
||||
containerTag: string,
|
||||
threadId: string | undefined,
|
||||
mode: MemoryMode,
|
||||
message: string,
|
||||
): string {
|
||||
const normalizedMessage = message.trim().replace(/\s+/g, " ")
|
||||
return `${containerTag}:${threadId || ""}:${mode}:${normalizedMessage}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a cached value by key.
|
||||
*
|
||||
* @param key - The cache key
|
||||
* @returns The cached value or undefined if not found
|
||||
*/
|
||||
get(key: string): T | undefined {
|
||||
return this.cache.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a value in the cache.
|
||||
*
|
||||
* @param key - The cache key
|
||||
* @param value - The value to cache
|
||||
*/
|
||||
set(key: string, value: T): void {
|
||||
this.cache.set(key, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a key exists in the cache.
|
||||
*
|
||||
* @param key - The cache key
|
||||
* @returns True if the key exists
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
return this.cache.has(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all cached values.
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of cached items.
|
||||
*/
|
||||
get size(): number {
|
||||
return this.cache.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to create a turn cache key.
|
||||
* @see MemoryCache.makeTurnKey
|
||||
*/
|
||||
export const makeTurnKey = MemoryCache.makeTurnKey
|
||||
61
packages/tools/src/shared/context.ts
Normal file
61
packages/tools/src/shared/context.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Supermemory from "supermemory"
|
||||
|
||||
/**
|
||||
* Normalizes a base URL by removing trailing slashes.
|
||||
*
|
||||
* @param url - Optional base URL to normalize
|
||||
* @returns Normalized URL without trailing slash, or default API URL
|
||||
*/
|
||||
export const normalizeBaseUrl = (url?: string): string => {
|
||||
const defaultUrl = "https://api.supermemory.ai"
|
||||
if (!url) return defaultUrl
|
||||
return url.endsWith("/") ? url.slice(0, -1) : url
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a Supermemory client.
|
||||
*/
|
||||
export interface CreateSupermemoryClientOptions {
|
||||
/** Supermemory API key */
|
||||
apiKey: string
|
||||
/** Optional custom base URL */
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configured Supermemory client instance.
|
||||
*
|
||||
* @param options - Client configuration options
|
||||
* @returns Configured Supermemory client
|
||||
*/
|
||||
export function createSupermemoryClient(
|
||||
options: CreateSupermemoryClientOptions,
|
||||
): Supermemory {
|
||||
const normalizedBaseUrl = normalizeBaseUrl(options.baseUrl)
|
||||
|
||||
return new Supermemory({
|
||||
apiKey: options.apiKey,
|
||||
...(normalizedBaseUrl !== "https://api.supermemory.ai"
|
||||
? { baseURL: normalizedBaseUrl }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that an API key is provided either via options or environment variable.
|
||||
*
|
||||
* @param apiKey - Optional API key from options
|
||||
* @returns The validated API key
|
||||
* @throws Error if no API key is available
|
||||
*/
|
||||
export function validateApiKey(apiKey?: string): string {
|
||||
const providedApiKey = apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
if (!providedApiKey) {
|
||||
throw new Error(
|
||||
"SUPERMEMORY_API_KEY is not set — provide it via `options.apiKey` or set `process.env.SUPERMEMORY_API_KEY`",
|
||||
)
|
||||
}
|
||||
|
||||
return providedApiKey
|
||||
}
|
||||
42
packages/tools/src/shared/index.ts
Normal file
42
packages/tools/src/shared/index.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Types
|
||||
export type {
|
||||
MemoryPromptData,
|
||||
PromptTemplate,
|
||||
MemoryMode,
|
||||
AddMemoryMode,
|
||||
Logger,
|
||||
ProfileStructure,
|
||||
ProfileMarkdownData,
|
||||
SupermemoryBaseOptions,
|
||||
} from "./types"
|
||||
|
||||
// Logger
|
||||
export { createLogger } from "./logger"
|
||||
|
||||
// Prompt builder
|
||||
export {
|
||||
defaultPromptTemplate,
|
||||
convertProfileToMarkdown,
|
||||
formatMemoriesForPrompt,
|
||||
} from "./prompt-builder"
|
||||
|
||||
// Cache
|
||||
export { MemoryCache, makeTurnKey } from "./cache"
|
||||
|
||||
// Context
|
||||
export {
|
||||
normalizeBaseUrl,
|
||||
createSupermemoryClient,
|
||||
validateApiKey,
|
||||
type CreateSupermemoryClientOptions,
|
||||
} from "./context"
|
||||
|
||||
// Memory client
|
||||
export {
|
||||
supermemoryProfileSearch,
|
||||
buildMemoriesText,
|
||||
extractQueryText,
|
||||
getLastUserMessageText,
|
||||
type BuildMemoriesTextOptions,
|
||||
type GenericMessage,
|
||||
} from "./memory-client"
|
||||
45
packages/tools/src/shared/logger.ts
Normal file
45
packages/tools/src/shared/logger.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { Logger } from "./types"
|
||||
|
||||
/**
|
||||
* Creates a logger instance that outputs to console when verbose mode is enabled.
|
||||
*
|
||||
* @param verbose - When true, logs are written to console; when false, logs are silently ignored
|
||||
* @returns Logger instance with debug, info, warn, and error methods
|
||||
*/
|
||||
export const createLogger = (verbose: boolean): Logger => {
|
||||
if (!verbose) {
|
||||
return {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
debug: (message: string, data?: unknown) => {
|
||||
console.log(
|
||||
`[supermemory] ${message}`,
|
||||
data ? JSON.stringify(data, null, 2) : "",
|
||||
)
|
||||
},
|
||||
info: (message: string, data?: unknown) => {
|
||||
console.log(
|
||||
`[supermemory] ${message}`,
|
||||
data ? JSON.stringify(data, null, 2) : "",
|
||||
)
|
||||
},
|
||||
warn: (message: string, data?: unknown) => {
|
||||
console.warn(
|
||||
`[supermemory] ${message}`,
|
||||
data ? JSON.stringify(data, null, 2) : "",
|
||||
)
|
||||
},
|
||||
error: (message: string, data?: unknown) => {
|
||||
console.error(
|
||||
`[supermemory] ${message}`,
|
||||
data ? JSON.stringify(data, null, 2) : "",
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
284
packages/tools/src/shared/memory-client.ts
Normal file
284
packages/tools/src/shared/memory-client.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import { deduplicateMemories } from "../tools-shared"
|
||||
import type {
|
||||
Logger,
|
||||
MemoryMode,
|
||||
MemoryPromptData,
|
||||
ProfileStructure,
|
||||
PromptTemplate,
|
||||
} from "./types"
|
||||
import {
|
||||
convertProfileToMarkdown,
|
||||
defaultPromptTemplate,
|
||||
} from "./prompt-builder"
|
||||
|
||||
/**
|
||||
* Fetches profile and search results from the Supermemory API.
|
||||
*
|
||||
* @param containerTag - The container tag/user ID for scoping memories
|
||||
* @param queryText - Optional query text for semantic search
|
||||
* @param baseUrl - The API base URL
|
||||
* @param apiKey - The API key for authentication
|
||||
* @returns The profile structure with static, dynamic, and search results
|
||||
*/
|
||||
export const supermemoryProfileSearch = async (
|
||||
containerTag: string,
|
||||
queryText: string,
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
): Promise<ProfileStructure> => {
|
||||
const payload = queryText
|
||||
? JSON.stringify({
|
||||
q: queryText,
|
||||
containerTag: containerTag,
|
||||
})
|
||||
: JSON.stringify({
|
||||
containerTag: containerTag,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v4/profile`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: payload,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "Unknown error")
|
||||
throw new Error(
|
||||
`Supermemory profile search failed: ${response.status} ${response.statusText}. ${errorText}`,
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error(`Supermemory API request failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for building memories text.
|
||||
*/
|
||||
export interface BuildMemoriesTextOptions {
|
||||
containerTag: string
|
||||
queryText: string
|
||||
mode: MemoryMode
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
logger: Logger
|
||||
promptTemplate?: PromptTemplate
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches memories from the API, deduplicates them, and formats them into
|
||||
* the final string to be injected into the system prompt.
|
||||
*
|
||||
* @param options - Configuration for building memories text
|
||||
* @returns The final formatted memories string ready for injection
|
||||
*/
|
||||
export const buildMemoriesText = async (
|
||||
options: BuildMemoriesTextOptions,
|
||||
): Promise<string> => {
|
||||
const {
|
||||
containerTag,
|
||||
queryText,
|
||||
mode,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
logger,
|
||||
promptTemplate = defaultPromptTemplate,
|
||||
} = options
|
||||
|
||||
const memoriesResponse = await supermemoryProfileSearch(
|
||||
containerTag,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
|
||||
|
||||
logger.info("Memory search completed", {
|
||||
containerTag,
|
||||
memoryCountStatic,
|
||||
memoryCountDynamic,
|
||||
queryText:
|
||||
queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""),
|
||||
mode,
|
||||
})
|
||||
|
||||
const deduplicated = deduplicateMemories({
|
||||
static: memoriesResponse.profile.static,
|
||||
dynamic: memoriesResponse.profile.dynamic,
|
||||
searchResults: memoriesResponse.searchResults?.results,
|
||||
})
|
||||
|
||||
logger.debug("Memory deduplication completed", {
|
||||
static: {
|
||||
original: memoryCountStatic,
|
||||
deduplicated: deduplicated.static.length,
|
||||
},
|
||||
dynamic: {
|
||||
original: memoryCountDynamic,
|
||||
deduplicated: deduplicated.dynamic.length,
|
||||
},
|
||||
searchResults: {
|
||||
original: memoriesResponse.searchResults?.results?.length,
|
||||
deduplicated: deduplicated.searchResults?.length,
|
||||
},
|
||||
})
|
||||
|
||||
const userMemories =
|
||||
mode !== "query"
|
||||
? convertProfileToMarkdown({
|
||||
profile: {
|
||||
static: deduplicated.static,
|
||||
dynamic: deduplicated.dynamic,
|
||||
},
|
||||
searchResults: { results: [] },
|
||||
})
|
||||
: ""
|
||||
const generalSearchMemories =
|
||||
mode !== "profile"
|
||||
? `Search results for user's recent message: \n${deduplicated.searchResults
|
||||
.map((memory) => `- ${memory}`)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
|
||||
const promptData: MemoryPromptData = {
|
||||
userMemories,
|
||||
generalSearchMemories,
|
||||
}
|
||||
|
||||
const memories = promptTemplate(promptData)
|
||||
if (memories) {
|
||||
logger.debug("Memory content preview", {
|
||||
content: memories,
|
||||
fullLength: memories.length,
|
||||
})
|
||||
}
|
||||
|
||||
return memories
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic interface for a message with role and content.
|
||||
* Framework-agnostic to support both Vercel AI SDK and Mastra.
|
||||
*/
|
||||
export interface GenericMessage {
|
||||
role: string
|
||||
content: string | Array<{ type: string; text?: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the query text from messages based on mode.
|
||||
* For "profile" mode, returns empty string (no query needed).
|
||||
* For "query" or "full" mode, extracts the last user message text.
|
||||
*
|
||||
* This is a framework-agnostic version that works with any message array.
|
||||
*
|
||||
* @param messages - Array of messages with role and content
|
||||
* @param mode - The memory retrieval mode
|
||||
* @returns The query text for memory search
|
||||
*/
|
||||
export const extractQueryText = (
|
||||
messages: GenericMessage[],
|
||||
mode: MemoryMode,
|
||||
): string => {
|
||||
if (mode === "profile") {
|
||||
return ""
|
||||
}
|
||||
|
||||
const userMessage = messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((msg) => msg.role === "user")
|
||||
|
||||
const content = userMessage?.content
|
||||
if (!content) return ""
|
||||
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text || "")
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
const objContent = content as unknown as {
|
||||
content?: string
|
||||
parts?: Array<{ type: string; text?: string }>
|
||||
}
|
||||
if (typeof objContent === "object" && objContent !== null) {
|
||||
if ("content" in objContent && typeof objContent.content === "string") {
|
||||
return objContent.content
|
||||
}
|
||||
if ("parts" in objContent && Array.isArray(objContent.parts)) {
|
||||
return objContent.parts
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text || "")
|
||||
.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts text content from the last user message in a message array.
|
||||
*
|
||||
* @param messages - Array of messages with role and content
|
||||
* @returns The last user message text, or undefined if not found
|
||||
*/
|
||||
export const getLastUserMessageText = (
|
||||
messages: GenericMessage[],
|
||||
): string | undefined => {
|
||||
const lastUserMessage = messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((msg) => msg.role === "user")
|
||||
|
||||
if (!lastUserMessage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const content = lastUserMessage.content
|
||||
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text || "")
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
const objContent = content as unknown as {
|
||||
content?: string
|
||||
parts?: Array<{ type: string; text?: string }>
|
||||
}
|
||||
if (typeof objContent === "object" && objContent !== null) {
|
||||
if ("content" in objContent && typeof objContent.content === "string") {
|
||||
return objContent.content
|
||||
}
|
||||
if ("parts" in objContent && Array.isArray(objContent.parts)) {
|
||||
return objContent.parts
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text || "")
|
||||
.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
49
packages/tools/src/shared/prompt-builder.ts
Normal file
49
packages/tools/src/shared/prompt-builder.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type {
|
||||
MemoryPromptData,
|
||||
PromptTemplate,
|
||||
ProfileMarkdownData,
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
* Default prompt template that formats memories in the original "User Supermemories" format.
|
||||
*/
|
||||
export const defaultPromptTemplate: PromptTemplate = (data) =>
|
||||
`User Supermemories: \n${data.userMemories}\n${data.generalSearchMemories}`.trim()
|
||||
|
||||
/**
|
||||
* Convert profile data to markdown format with sections for static and dynamic memories.
|
||||
*
|
||||
* @param data Profile data with string arrays for static and dynamic memories
|
||||
* @returns Markdown string with profile sections
|
||||
*/
|
||||
export function convertProfileToMarkdown(data: ProfileMarkdownData): string {
|
||||
const sections: string[] = []
|
||||
|
||||
if (data.profile.static && data.profile.static.length > 0) {
|
||||
sections.push("## Static Profile")
|
||||
sections.push(data.profile.static.map((item) => `- ${item}`).join("\n"))
|
||||
}
|
||||
|
||||
if (data.profile.dynamic && data.profile.dynamic.length > 0) {
|
||||
sections.push("## Dynamic Profile")
|
||||
sections.push(data.profile.dynamic.map((item) => `- ${item}`).join("\n"))
|
||||
}
|
||||
|
||||
return sections.join("\n\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats memories into the final prompt string using the provided template.
|
||||
*
|
||||
* @param data - The memory prompt data containing userMemories and generalSearchMemories
|
||||
* @param template - Optional custom template function (defaults to defaultPromptTemplate)
|
||||
* @returns The formatted memories string ready for prompt injection
|
||||
*/
|
||||
export function formatMemoriesForPrompt(
|
||||
data: MemoryPromptData,
|
||||
template: PromptTemplate = defaultPromptTemplate,
|
||||
): string {
|
||||
return template(data)
|
||||
}
|
||||
|
||||
export type { MemoryPromptData, PromptTemplate }
|
||||
119
packages/tools/src/shared/types.ts
Normal file
119
packages/tools/src/shared/types.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Data provided to the prompt template function for customizing memory injection.
|
||||
*/
|
||||
export interface MemoryPromptData {
|
||||
/**
|
||||
* Pre-formatted markdown combining static and dynamic profile memories.
|
||||
* Contains core user facts (name, preferences, goals) and recent context (projects, interests).
|
||||
*/
|
||||
userMemories: string
|
||||
/**
|
||||
* Pre-formatted search results text for the current query.
|
||||
* Contains memories retrieved based on semantic similarity to the conversation.
|
||||
* Empty string if mode is "profile" only.
|
||||
*/
|
||||
generalSearchMemories: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Function type for customizing the memory prompt injection.
|
||||
* Return the full string to be injected into the system prompt.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const promptTemplate: PromptTemplate = (data) => `
|
||||
* <user_memories>
|
||||
* Here is some information about your past conversations:
|
||||
* ${data.userMemories}
|
||||
* ${data.generalSearchMemories}
|
||||
* </user_memories>
|
||||
* `.trim()
|
||||
* ```
|
||||
*/
|
||||
export type PromptTemplate = (data: MemoryPromptData) => string
|
||||
|
||||
/**
|
||||
* Memory retrieval mode:
|
||||
* - "profile": Retrieves user profile memories (static + dynamic) without query filtering
|
||||
* - "query": Searches memories based on semantic similarity to the user's message
|
||||
* - "full": Combines both profile and query-based results
|
||||
*/
|
||||
export type MemoryMode = "profile" | "query" | "full"
|
||||
|
||||
/**
|
||||
* Memory persistence mode:
|
||||
* - "always": Automatically save conversations as memories
|
||||
* - "never": Only retrieve memories, don't store new ones
|
||||
*/
|
||||
export type AddMemoryMode = "always" | "never"
|
||||
|
||||
/**
|
||||
* Logger interface for consistent logging across integrations.
|
||||
*/
|
||||
export interface Logger {
|
||||
debug: (message: string, data?: unknown) => void
|
||||
info: (message: string, data?: unknown) => void
|
||||
warn: (message: string, data?: unknown) => void
|
||||
error: (message: string, data?: unknown) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Response structure from the Supermemory profile API.
|
||||
*/
|
||||
export interface ProfileStructure {
|
||||
profile: {
|
||||
/**
|
||||
* Core, stable facts about the user that rarely change.
|
||||
* Examples: name, profession, long-term preferences, goals.
|
||||
*/
|
||||
static?: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
/**
|
||||
* Recently learned or frequently updated information about the user.
|
||||
* Examples: current projects, recent interests, ongoing topics.
|
||||
*/
|
||||
dynamic?: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
}
|
||||
searchResults: {
|
||||
/**
|
||||
* Memories retrieved based on semantic similarity to the current query.
|
||||
* Most relevant to the immediate conversation context.
|
||||
*/
|
||||
results: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified profile data for markdown conversion.
|
||||
*/
|
||||
export interface ProfileMarkdownData {
|
||||
profile: {
|
||||
/** Core, stable user facts (name, preferences, goals) */
|
||||
static?: string[]
|
||||
/** Recently learned or updated information (current projects, interests) */
|
||||
dynamic?: string[]
|
||||
}
|
||||
searchResults: {
|
||||
/** Query-relevant memories based on semantic similarity */
|
||||
results: Array<{ memory: string }>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base options shared across all integrations for Supermemory configuration.
|
||||
*/
|
||||
export interface SupermemoryBaseOptions {
|
||||
/** Supermemory API key (falls back to SUPERMEMORY_API_KEY env var) */
|
||||
apiKey?: string
|
||||
/** Custom Supermemory API base URL */
|
||||
baseUrl?: string
|
||||
/** Optional conversation/thread ID to group messages for contextual memory generation */
|
||||
threadId?: string
|
||||
/** Memory retrieval mode */
|
||||
mode?: MemoryMode
|
||||
/** Memory persistence mode */
|
||||
addMemory?: AddMemoryMode
|
||||
/** Enable detailed logging of memory search and injection */
|
||||
verbose?: boolean
|
||||
/** Custom function to format memory data into the system prompt */
|
||||
promptTemplate?: PromptTemplate
|
||||
}
|
||||
|
|
@ -55,12 +55,13 @@ export interface MemoryItem {
|
|||
}
|
||||
|
||||
/**
|
||||
* Profile data structure containing memory items from different sources
|
||||
* Profile data structure containing memory items from different sources.
|
||||
* API may return either MemoryItem objects or plain strings.
|
||||
*/
|
||||
export interface ProfileWithMemories {
|
||||
static?: Array<MemoryItem>
|
||||
dynamic?: Array<MemoryItem>
|
||||
searchResults?: Array<MemoryItem>
|
||||
static?: Array<MemoryItem | string>
|
||||
dynamic?: Array<MemoryItem | string>
|
||||
searchResults?: Array<MemoryItem | string>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -101,8 +102,14 @@ export function deduplicateMemories(
|
|||
const dynamicItems = data.dynamic ?? []
|
||||
const searchItems = data.searchResults ?? []
|
||||
|
||||
const getMemoryString = (item: MemoryItem): string | null => {
|
||||
if (!item || typeof item.memory !== "string") return null
|
||||
const getMemoryString = (item: MemoryItem | string): string | null => {
|
||||
if (!item) return null
|
||||
// Handle both string format (from API) and object format
|
||||
if (typeof item === "string") {
|
||||
const trimmed = item.trim()
|
||||
return trimmed.length > 0 ? trimmed : null
|
||||
}
|
||||
if (typeof item.memory !== "string") return null
|
||||
const trimmed = item.memory.trim()
|
||||
return trimmed.length > 0 ? trimmed : null
|
||||
}
|
||||
|
|
@ -110,7 +117,7 @@ export function deduplicateMemories(
|
|||
const staticMemories: string[] = []
|
||||
const seenMemories = new Set<string>()
|
||||
|
||||
for (const item of staticItems) {
|
||||
for (const item of staticItems as Array<MemoryItem | string>) {
|
||||
const memory = getMemoryString(item)
|
||||
if (memory !== null) {
|
||||
staticMemories.push(memory)
|
||||
|
|
@ -120,7 +127,7 @@ export function deduplicateMemories(
|
|||
|
||||
const dynamicMemories: string[] = []
|
||||
|
||||
for (const item of dynamicItems) {
|
||||
for (const item of dynamicItems as Array<MemoryItem | string>) {
|
||||
const memory = getMemoryString(item)
|
||||
if (memory !== null && !seenMemories.has(memory)) {
|
||||
dynamicMemories.push(memory)
|
||||
|
|
@ -130,7 +137,7 @@ export function deduplicateMemories(
|
|||
|
||||
const searchMemories: string[] = []
|
||||
|
||||
for (const item of searchItems) {
|
||||
for (const item of searchItems as Array<MemoryItem | string>) {
|
||||
const memory = getMemoryString(item)
|
||||
if (memory !== null && !seenMemories.has(memory)) {
|
||||
searchMemories.push(memory)
|
||||
|
|
@ -165,16 +165,16 @@ describe("@supermemory/tools", () => {
|
|||
(d) => d.function.name === "searchMemories",
|
||||
)
|
||||
expect(searchTool).toBeDefined()
|
||||
expect(searchTool!.type).toBe("function")
|
||||
expect(searchTool!.function.parameters?.required).toContain(
|
||||
expect(searchTool?.type).toBe("function")
|
||||
expect(searchTool?.function.parameters?.required).toContain(
|
||||
"informationToGet",
|
||||
)
|
||||
|
||||
// Check addMemory
|
||||
const addTool = definitions.find((d) => d.function.name === "addMemory")
|
||||
expect(addTool).toBeDefined()
|
||||
expect(addTool!.type).toBe("function")
|
||||
expect(addTool!.function.parameters?.required).toContain("memory")
|
||||
expect(addTool?.type).toBe("function")
|
||||
expect(addTool?.function.parameters?.required).toContain("memory")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,44 +1,2 @@
|
|||
export interface Logger {
|
||||
debug: (message: string, data?: unknown) => void
|
||||
info: (message: string, data?: unknown) => void
|
||||
warn: (message: string, data?: unknown) => void
|
||||
error: (message: string, data?: unknown) => void
|
||||
}
|
||||
|
||||
export const createLogger = (verbose: boolean): Logger => {
|
||||
if (!verbose) {
|
||||
return {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
debug: (message: string, data?: unknown) => {
|
||||
console.log(
|
||||
`[supermemory] ${message}`,
|
||||
data ? JSON.stringify(data, null, 2) : "",
|
||||
)
|
||||
},
|
||||
info: (message: string, data?: unknown) => {
|
||||
console.log(
|
||||
`[supermemory] ${message}`,
|
||||
data ? JSON.stringify(data, null, 2) : "",
|
||||
)
|
||||
},
|
||||
warn: (message: string, data?: unknown) => {
|
||||
console.warn(
|
||||
`[supermemory] ${message}`,
|
||||
data ? JSON.stringify(data, null, 2) : "",
|
||||
)
|
||||
},
|
||||
error: (message: string, data?: unknown) => {
|
||||
console.error(
|
||||
`[supermemory] ${message}`,
|
||||
data ? JSON.stringify(data, null, 2) : "",
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
// Re-export logger from shared module for backward compatibility
|
||||
export { createLogger, type Logger } from "../shared"
|
||||
|
|
|
|||
|
|
@ -1,202 +1,50 @@
|
|||
import { deduplicateMemories } from "../shared"
|
||||
import type { Logger } from "./logger"
|
||||
import {
|
||||
type LanguageModelCallOptions,
|
||||
convertProfileToMarkdown,
|
||||
type ProfileStructure,
|
||||
} from "./util"
|
||||
// Re-export shared types and functions
|
||||
export {
|
||||
type MemoryPromptData,
|
||||
type PromptTemplate,
|
||||
defaultPromptTemplate,
|
||||
normalizeBaseUrl,
|
||||
buildMemoriesText,
|
||||
type BuildMemoriesTextOptions,
|
||||
} from "../shared"
|
||||
|
||||
import type { Logger } from "../shared"
|
||||
import type { LanguageModelCallOptions } from "./util"
|
||||
|
||||
/**
|
||||
* Data provided to the prompt template function for customizing memory injection.
|
||||
*/
|
||||
export interface MemoryPromptData {
|
||||
/**
|
||||
* Pre-formatted markdown combining static and dynamic profile memories.
|
||||
* Contains core user facts (name, preferences, goals) and recent context (projects, interests).
|
||||
*/
|
||||
userMemories: string
|
||||
/**
|
||||
* Pre-formatted search results text for the current query.
|
||||
* Contains memories retrieved based on semantic similarity to the conversation.
|
||||
* Empty string if mode is "profile" only.
|
||||
*/
|
||||
generalSearchMemories: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Function type for customizing the memory prompt injection.
|
||||
* Return the full string to be injected into the system prompt.
|
||||
* Extracts the query text from params based on mode.
|
||||
* For "profile" mode, returns empty string (no query needed).
|
||||
* For "query" or "full" mode, extracts the last user message text.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const promptTemplate: PromptTemplate = (data) => `
|
||||
* <user_memories>
|
||||
* Here is some information about your past conversations:
|
||||
* ${data.userMemories}
|
||||
* ${data.generalSearchMemories}
|
||||
* </user_memories>
|
||||
* `.trim()
|
||||
* ```
|
||||
* @param params - The language model call options
|
||||
* @param mode - The memory retrieval mode
|
||||
* @returns The query text for memory search
|
||||
*/
|
||||
export type PromptTemplate = (data: MemoryPromptData) => string
|
||||
|
||||
/**
|
||||
* Default prompt template that replicates the original behavior.
|
||||
*/
|
||||
export const defaultPromptTemplate: PromptTemplate = (data) =>
|
||||
`User Supermemories: \n${data.userMemories}\n${data.generalSearchMemories}`.trim()
|
||||
|
||||
export const normalizeBaseUrl = (url?: string): string => {
|
||||
const defaultUrl = "https://api.supermemory.ai"
|
||||
if (!url) return defaultUrl
|
||||
return url.endsWith("/") ? url.slice(0, -1) : url
|
||||
}
|
||||
|
||||
const supermemoryProfileSearch = async (
|
||||
containerTag: string,
|
||||
queryText: string,
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
): Promise<ProfileStructure> => {
|
||||
const payload = queryText
|
||||
? JSON.stringify({
|
||||
q: queryText,
|
||||
containerTag: containerTag,
|
||||
})
|
||||
: JSON.stringify({
|
||||
containerTag: containerTag,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v4/profile`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: payload,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "Unknown error")
|
||||
throw new Error(
|
||||
`Supermemory profile search failed: ${response.status} ${response.statusText}. ${errorText}`,
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error(`Supermemory API request failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for building memories text.
|
||||
*/
|
||||
export interface BuildMemoriesTextOptions {
|
||||
containerTag: string
|
||||
queryText: string
|
||||
mode: "profile" | "query" | "full"
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
logger: Logger
|
||||
promptTemplate?: PromptTemplate
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches memories from the API, deduplicates them, and formats them into
|
||||
* the final string to be injected into the system prompt.
|
||||
*
|
||||
* @param options - Configuration for building memories text
|
||||
* @returns The final formatted memories string ready for injection
|
||||
*/
|
||||
export const buildMemoriesText = async (
|
||||
options: BuildMemoriesTextOptions,
|
||||
): Promise<string> => {
|
||||
const {
|
||||
containerTag,
|
||||
queryText,
|
||||
mode,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
logger,
|
||||
promptTemplate = defaultPromptTemplate,
|
||||
} = options
|
||||
|
||||
const memoriesResponse = await supermemoryProfileSearch(
|
||||
containerTag,
|
||||
queryText,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
|
||||
|
||||
logger.info("Memory search completed", {
|
||||
containerTag,
|
||||
memoryCountStatic,
|
||||
memoryCountDynamic,
|
||||
queryText:
|
||||
queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""),
|
||||
mode,
|
||||
})
|
||||
|
||||
const deduplicated = deduplicateMemories({
|
||||
static: memoriesResponse.profile.static,
|
||||
dynamic: memoriesResponse.profile.dynamic,
|
||||
searchResults: memoriesResponse.searchResults?.results,
|
||||
})
|
||||
|
||||
logger.debug("Memory deduplication completed", {
|
||||
static: {
|
||||
original: memoryCountStatic,
|
||||
deduplicated: deduplicated.static.length,
|
||||
},
|
||||
dynamic: {
|
||||
original: memoryCountDynamic,
|
||||
deduplicated: deduplicated.dynamic.length,
|
||||
},
|
||||
searchResults: {
|
||||
original: memoriesResponse.searchResults?.results?.length,
|
||||
deduplicated: deduplicated.searchResults?.length,
|
||||
},
|
||||
})
|
||||
|
||||
const userMemories =
|
||||
mode !== "query"
|
||||
? convertProfileToMarkdown({
|
||||
profile: {
|
||||
static: deduplicated.static,
|
||||
dynamic: deduplicated.dynamic,
|
||||
},
|
||||
searchResults: { results: [] },
|
||||
})
|
||||
: ""
|
||||
const generalSearchMemories =
|
||||
mode !== "profile"
|
||||
? `Search results for user's recent message: \n${deduplicated.searchResults
|
||||
.map((memory) => `- ${memory}`)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
|
||||
const promptData: MemoryPromptData = {
|
||||
userMemories,
|
||||
generalSearchMemories,
|
||||
export const extractQueryText = (
|
||||
params: LanguageModelCallOptions,
|
||||
mode: "profile" | "query" | "full",
|
||||
): string => {
|
||||
if (mode === "profile") {
|
||||
return ""
|
||||
}
|
||||
|
||||
const memories = promptTemplate(promptData)
|
||||
if (memories) {
|
||||
logger.debug("Memory content preview", {
|
||||
content: memories,
|
||||
fullLength: memories.length,
|
||||
})
|
||||
const userMessage = params.prompt
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((prompt: { role: string }) => prompt.role === "user")
|
||||
|
||||
const content = userMessage?.content
|
||||
if (!content) return ""
|
||||
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
|
||||
return memories
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
return (content as any[])
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text || "")
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -239,42 +87,6 @@ export const injectMemoriesIntoParams = (
|
|||
return { ...params, prompt: newPrompt } as LanguageModelCallOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the query text from params based on mode.
|
||||
* For "profile" mode, returns empty string (no query needed).
|
||||
* For "query" or "full" mode, extracts the last user message text.
|
||||
*
|
||||
* @param params - The language model call options
|
||||
* @param mode - The memory retrieval mode
|
||||
* @returns The query text for memory search
|
||||
*/
|
||||
export const extractQueryText = (
|
||||
params: LanguageModelCallOptions,
|
||||
mode: "profile" | "query" | "full",
|
||||
): string => {
|
||||
if (mode === "profile") {
|
||||
return ""
|
||||
}
|
||||
|
||||
const userMessage = params.prompt
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((prompt: { role: string }) => prompt.role === "user")
|
||||
|
||||
const content = userMessage?.content
|
||||
if (!content) return ""
|
||||
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
return (content as any[])
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text || "")
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds memories to the system prompt by fetching from API and injecting.
|
||||
* This is the original combined function, now implemented via helpers.
|
||||
|
|
@ -288,8 +100,13 @@ export const addSystemPrompt = async (
|
|||
mode: "profile" | "query" | "full",
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
promptTemplate: PromptTemplate = defaultPromptTemplate,
|
||||
promptTemplate?: (data: {
|
||||
userMemories: string
|
||||
generalSearchMemories: string
|
||||
}) => string,
|
||||
): Promise<LanguageModelCallOptions> => {
|
||||
const { buildMemoriesText } = await import("../shared")
|
||||
|
||||
const queryText = extractQueryText(params, mode)
|
||||
|
||||
const memories = await buildMemoriesText({
|
||||
|
|
|
|||
|
|
@ -3,19 +3,21 @@ import {
|
|||
addConversation,
|
||||
type ConversationMessage,
|
||||
} from "../conversations-client"
|
||||
import { createLogger, type Logger } from "./logger"
|
||||
import {
|
||||
createLogger,
|
||||
normalizeBaseUrl,
|
||||
MemoryCache,
|
||||
buildMemoriesText,
|
||||
type Logger,
|
||||
type PromptTemplate,
|
||||
type MemoryMode,
|
||||
} from "../shared"
|
||||
import {
|
||||
type LanguageModelCallOptions,
|
||||
getLastUserMessage,
|
||||
filterOutSupermemories,
|
||||
} from "./util"
|
||||
import {
|
||||
buildMemoriesText,
|
||||
extractQueryText,
|
||||
injectMemoriesIntoParams,
|
||||
normalizeBaseUrl,
|
||||
type PromptTemplate,
|
||||
} from "./memory-prompt"
|
||||
import { extractQueryText, injectMemoriesIntoParams } from "./memory-prompt"
|
||||
|
||||
const getConversationContent = (params: LanguageModelCallOptions) => {
|
||||
return params.prompt
|
||||
|
|
@ -175,7 +177,7 @@ interface SupermemoryMiddlewareOptions {
|
|||
* - "query": Searches memories based on semantic similarity to the user's message
|
||||
* - "full": Combines both profile and query-based results
|
||||
*/
|
||||
mode?: "profile" | "query" | "full"
|
||||
mode?: MemoryMode
|
||||
/**
|
||||
* Memory persistence mode:
|
||||
* - "always": Automatically save conversations as memories
|
||||
|
|
@ -188,26 +190,21 @@ interface SupermemoryMiddlewareOptions {
|
|||
promptTemplate?: PromptTemplate
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached memories string for a user turn.
|
||||
*/
|
||||
type MemoryCache = string
|
||||
|
||||
interface SupermemoryMiddlewareContext {
|
||||
client: Supermemory
|
||||
logger: Logger
|
||||
containerTag: string
|
||||
conversationId?: string
|
||||
mode: "profile" | "query" | "full"
|
||||
mode: MemoryMode
|
||||
addMemory: "always" | "never"
|
||||
normalizedBaseUrl: string
|
||||
apiKey: string
|
||||
promptTemplate?: PromptTemplate
|
||||
/**
|
||||
* Per-turn memory cache map. Stores the injected memories string for each
|
||||
* Per-turn memory cache. Stores the injected memories string for each
|
||||
* user turn (keyed by turnKey) to avoid redundant API calls during tool-call
|
||||
*/
|
||||
memoryCache: Map<string, MemoryCache>
|
||||
memoryCache: MemoryCache<string>
|
||||
}
|
||||
|
||||
export const createSupermemoryContext = (
|
||||
|
|
@ -244,20 +241,24 @@ export const createSupermemoryContext = (
|
|||
normalizedBaseUrl,
|
||||
apiKey,
|
||||
promptTemplate,
|
||||
memoryCache: new Map<string, MemoryCache>(),
|
||||
memoryCache: new MemoryCache<string>(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cache key for the current turn based on context and user message.
|
||||
* Normalizes the user message by trimming and collapsing whitespace.
|
||||
* Uses the shared MemoryCache.makeTurnKey implementation.
|
||||
*/
|
||||
const makeTurnKey = (
|
||||
ctx: SupermemoryMiddlewareContext,
|
||||
userMessage: string,
|
||||
): string => {
|
||||
const normalizedMessage = userMessage.trim().replace(/\s+/g, " ")
|
||||
return `${ctx.containerTag}:${ctx.conversationId || ""}:${ctx.mode}:${normalizedMessage}`
|
||||
return MemoryCache.makeTurnKey(
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
ctx.mode,
|
||||
userMessage,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ import type {
|
|||
LanguageModelV3StreamPart,
|
||||
} from "@ai-sdk/provider"
|
||||
|
||||
// Re-export shared types for backward compatibility
|
||||
export type {
|
||||
ProfileStructure,
|
||||
ProfileMarkdownData,
|
||||
} from "../shared"
|
||||
|
||||
// Union types for dual SDK version support (V2 = SDK 5, V3 = SDK 6)
|
||||
export type LanguageModel = LanguageModelV2 | LanguageModelV3
|
||||
export type LanguageModelCallOptions =
|
||||
|
|
@ -21,47 +27,6 @@ export type LanguageModelStreamPart =
|
|||
| LanguageModelV2StreamPart
|
||||
| LanguageModelV3StreamPart
|
||||
|
||||
/**
|
||||
* Response structure from the Supermemory profile API.
|
||||
*/
|
||||
export interface ProfileStructure {
|
||||
profile: {
|
||||
/**
|
||||
* Core, stable facts about the user that rarely change.
|
||||
* Examples: name, profession, long-term preferences, goals.
|
||||
*/
|
||||
static?: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
/**
|
||||
* Recently learned or frequently updated information about the user.
|
||||
* Examples: current projects, recent interests, ongoing topics.
|
||||
*/
|
||||
dynamic?: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
}
|
||||
searchResults: {
|
||||
/**
|
||||
* Memories retrieved based on semantic similarity to the current query.
|
||||
* Most relevant to the immediate conversation context.
|
||||
*/
|
||||
results: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified profile data for markdown conversion.
|
||||
*/
|
||||
export interface ProfileMarkdownData {
|
||||
profile: {
|
||||
/** Core, stable user facts (name, preferences, goals) */
|
||||
static?: string[]
|
||||
/** Recently learned or updated information (current projects, interests) */
|
||||
dynamic?: string[]
|
||||
}
|
||||
searchResults: {
|
||||
/** Query-relevant memories based on semantic similarity */
|
||||
results: Array<{ memory: string }>
|
||||
}
|
||||
}
|
||||
|
||||
export type OutputContentItem =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "reasoning"; text: string }
|
||||
|
|
@ -79,26 +44,8 @@ export type OutputContentItem =
|
|||
title: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert profile data to markdown format
|
||||
* @param data Profile data with string arrays for static and dynamic memories
|
||||
* @returns Markdown string with profile sections
|
||||
*/
|
||||
export function convertProfileToMarkdown(data: ProfileMarkdownData): string {
|
||||
const sections: string[] = []
|
||||
|
||||
if (data.profile.static && data.profile.static.length > 0) {
|
||||
sections.push("## Static Profile")
|
||||
sections.push(data.profile.static.map((item) => `- ${item}`).join("\n"))
|
||||
}
|
||||
|
||||
if (data.profile.dynamic && data.profile.dynamic.length > 0) {
|
||||
sections.push("## Dynamic Profile")
|
||||
sections.push(data.profile.dynamic.map((item) => `- ${item}`).join("\n"))
|
||||
}
|
||||
|
||||
return sections.join("\n\n")
|
||||
}
|
||||
// Re-export convertProfileToMarkdown from shared for backward compatibility
|
||||
export { convertProfileToMarkdown } from "../shared"
|
||||
|
||||
export const getLastUserMessage = (
|
||||
params: LanguageModelCallOptions,
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ async function chatWithMemoryTool() {
|
|||
if (memoryResult.content) {
|
||||
console.log(
|
||||
"📄 Content preview:",
|
||||
memoryResult.content.substring(0, 100) + "...",
|
||||
`${memoryResult.content.substring(0, 100)}...`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -256,7 +256,7 @@ async function testMemoryOperations() {
|
|||
} else if (result.content) {
|
||||
console.log(
|
||||
"📄 Result:",
|
||||
result.content.substring(0, 150) + "... (truncated)",
|
||||
`${result.content.substring(0, 150)}... (truncated)`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -275,7 +275,7 @@ async function testMemoryOperations() {
|
|||
// Run the examples
|
||||
async function main() {
|
||||
await testMemoryOperations()
|
||||
console.log("\n" + "=".repeat(70) + "\n")
|
||||
console.log(`\n${"=".repeat(70)}\n`)
|
||||
await chatWithMemoryTool()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -314,14 +314,14 @@ export async function runAllExamples() {
|
|||
|
||||
try {
|
||||
await directFetchExample()
|
||||
console.log("\\n" + "=".repeat(70) + "\\n")
|
||||
console.log(`\\n${"=".repeat(70)}\\n`)
|
||||
await anthropicSdkExample()
|
||||
|
||||
console.log("\\n" + "=".repeat(70))
|
||||
console.log(`\\n${"=".repeat(70)}`)
|
||||
console.log("📋 Real Anthropic SDK Integration Template:")
|
||||
console.log(anthropicIntegrationTemplate)
|
||||
|
||||
console.log("\\n" + "=".repeat(70))
|
||||
console.log(`\\n${"=".repeat(70)}`)
|
||||
console.log("🔧 cURL Examples for Direct API Testing:")
|
||||
console.log(curlExamples)
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ export async function runRealExamples() {
|
|||
// Test with the actual tool call first
|
||||
await testWithRealToolCall()
|
||||
|
||||
console.log("\\n" + "=".repeat(70) + "\\n")
|
||||
console.log(`\\n${"=".repeat(70)}\\n`)
|
||||
|
||||
// Show web integration example
|
||||
console.log("🌐 Web Framework Integration Example:")
|
||||
|
|
@ -334,7 +334,7 @@ export async function runRealExamples() {
|
|||
|
||||
// Only run full API example if both keys are present
|
||||
if (process.env.ANTHROPIC_API_KEY && process.env.SUPERMEMORY_API_KEY) {
|
||||
console.log("\\n" + "=".repeat(70) + "\\n")
|
||||
console.log(`\\n${"=".repeat(70)}\\n`)
|
||||
await realClaudeMemoryExample()
|
||||
} else {
|
||||
console.log(
|
||||
|
|
|
|||
646
packages/tools/test/mastra/integration.test.ts
Normal file
646
packages/tools/test/mastra/integration.test.ts
Normal file
|
|
@ -0,0 +1,646 @@
|
|||
/**
|
||||
* Integration tests for the Mastra integration
|
||||
* Tests processors and wrapper with real Supermemory API calls
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import {
|
||||
RequestContext,
|
||||
MASTRA_THREAD_ID_KEY,
|
||||
} from "@mastra/core/request-context"
|
||||
import {
|
||||
SupermemoryInputProcessor,
|
||||
SupermemoryOutputProcessor,
|
||||
createSupermemoryProcessors,
|
||||
withSupermemory,
|
||||
} from "../../src/mastra"
|
||||
import type {
|
||||
ProcessInputArgs,
|
||||
ProcessOutputResultArgs,
|
||||
MessageList,
|
||||
MastraDBMessage,
|
||||
MastraMessageContentV2,
|
||||
Processor,
|
||||
} from "../../src/mastra"
|
||||
|
||||
interface MockAgentConfig {
|
||||
id: string
|
||||
name?: string
|
||||
model?: string
|
||||
inputProcessors?: Processor[]
|
||||
outputProcessors?: Processor[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
import "dotenv/config"
|
||||
|
||||
const INTEGRATION_CONFIG = {
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY || "",
|
||||
baseUrl: process.env.SUPERMEMORY_BASE_URL || "https://api.supermemory.ai",
|
||||
containerTag: "integration-test-mastra",
|
||||
}
|
||||
|
||||
const shouldRunIntegration = !!process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
/**
|
||||
* Helper to create MastraMessageContentV2 from text
|
||||
*/
|
||||
function createMessageContent(text: string): MastraMessageContentV2 {
|
||||
return {
|
||||
format: 2,
|
||||
parts: [{ type: "text", text }],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a MastraDBMessage
|
||||
*/
|
||||
function createMessage(
|
||||
role: "user" | "assistant" | "system",
|
||||
text: string,
|
||||
): MastraDBMessage {
|
||||
return {
|
||||
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
role,
|
||||
content: createMessageContent(text),
|
||||
createdAt: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock MessageList that captures calls for assertion.
|
||||
*/
|
||||
const createIntegrationMessageList = (): MessageList & {
|
||||
calls: { method: string; args: unknown[] }[]
|
||||
getSystemContent: () => string | undefined
|
||||
} => {
|
||||
const calls: { method: string; args: unknown[] }[] = []
|
||||
return {
|
||||
calls,
|
||||
addSystem: vi.fn((content: string, id?: string) => {
|
||||
calls.push({ method: "addSystem", args: [content, id] })
|
||||
}),
|
||||
addUser: vi.fn((content: string) => {
|
||||
calls.push({ method: "addUser", args: [content] })
|
||||
}),
|
||||
addAssistant: vi.fn((content: string) => {
|
||||
calls.push({ method: "addAssistant", args: [content] })
|
||||
}),
|
||||
getSystemContent: () => {
|
||||
const systemCall = calls.find((c) => c.method === "addSystem")
|
||||
return systemCall?.args[0] as string | undefined
|
||||
},
|
||||
} as unknown as MessageList & {
|
||||
calls: { method: string; args: unknown[] }[]
|
||||
getSystemContent: () => string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(!shouldRunIntegration)(
|
||||
"Integration: Mastra processors with real API",
|
||||
() => {
|
||||
describe("SupermemoryInputProcessor", () => {
|
||||
it("should fetch real memories and inject into messageList", async () => {
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createIntegrationMessageList()
|
||||
const messages: MastraDBMessage[] = [
|
||||
createMessage("user", "Hello, what do you know about me?"),
|
||||
]
|
||||
|
||||
const args: ProcessInputArgs = {
|
||||
messages,
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
expect(messageList.addSystem).toHaveBeenCalled()
|
||||
const systemContent = messageList.getSystemContent()
|
||||
expect(typeof systemContent).toBe("string")
|
||||
})
|
||||
|
||||
it("should use query mode with user message as search query", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "query",
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createIntegrationMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [
|
||||
createMessage(
|
||||
"user",
|
||||
"What are my favorite programming languages?",
|
||||
),
|
||||
],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
const profileCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(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?")
|
||||
}
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should use full mode with both profile and query", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "full",
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createIntegrationMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Full mode test query")],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
const profileCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
|
||||
const profileCall = profileCalls[0]
|
||||
if (profileCall?.[1]) {
|
||||
const body = JSON.parse(
|
||||
(profileCall[1] as RequestInit).body as string,
|
||||
)
|
||||
expect(body.q).toBe("Full mode test query")
|
||||
}
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should cache memories for repeated calls with same message", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const messages: MastraDBMessage[] = [
|
||||
createMessage("user", "Cache test message"),
|
||||
]
|
||||
|
||||
const args1: ProcessInputArgs = {
|
||||
messages,
|
||||
systemMessages: [],
|
||||
messageList: createIntegrationMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args1)
|
||||
const callsAfterFirst = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
).length
|
||||
|
||||
const args2: ProcessInputArgs = {
|
||||
messages,
|
||||
systemMessages: [],
|
||||
messageList: createIntegrationMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args2)
|
||||
const callsAfterSecond = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
).length
|
||||
|
||||
expect(callsAfterSecond).toBe(callsAfterFirst)
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should use custom promptTemplate for memory formatting", async () => {
|
||||
const customTemplate = (data: {
|
||||
userMemories: string
|
||||
generalSearchMemories: string
|
||||
}) => `<mastra-memories>${data.userMemories}</mastra-memories>`
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
promptTemplate: customTemplate,
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createIntegrationMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Custom template test")],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
const systemContent = messageList.getSystemContent()
|
||||
expect(systemContent).toMatch(/<mastra-memories>.*<\/mastra-memories>/s)
|
||||
})
|
||||
})
|
||||
|
||||
describe("SupermemoryOutputProcessor", () => {
|
||||
it("should save conversation when addMemory is always", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const threadId = `test-mastra-${Date.now()}`
|
||||
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
addMemory: "always",
|
||||
threadId,
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "Hello from Mastra integration test"),
|
||||
createMessage("assistant", "Hi! I'm responding to the test."),
|
||||
],
|
||||
messageList: createIntegrationMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
const conversationCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" &&
|
||||
call[0].includes("/v4/conversations"),
|
||||
)
|
||||
expect(conversationCalls.length).toBeGreaterThan(0)
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should not save when addMemory is never", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
addMemory: "never",
|
||||
threadId: "test-thread",
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "This should not be saved"),
|
||||
createMessage("assistant", "Agreed"),
|
||||
],
|
||||
messageList: createIntegrationMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
const conversationCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" &&
|
||||
call[0].includes("/v4/conversations"),
|
||||
)
|
||||
expect(conversationCalls.length).toBe(0)
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should use threadId from RequestContext when not in options", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
addMemory: "always",
|
||||
},
|
||||
)
|
||||
|
||||
const contextThreadId = `context-thread-${Date.now()}`
|
||||
const requestContext = new RequestContext()
|
||||
requestContext.set(MASTRA_THREAD_ID_KEY, contextThreadId)
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "Test with RequestContext threadId"),
|
||||
createMessage("assistant", "Got it!"),
|
||||
],
|
||||
messageList: createIntegrationMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
requestContext,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
const conversationCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" &&
|
||||
call[0].includes("/v4/conversations"),
|
||||
)
|
||||
expect(conversationCalls.length).toBeGreaterThan(0)
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("createSupermemoryProcessors", () => {
|
||||
it("should create working input and output processors", async () => {
|
||||
const { input, output } = createSupermemoryProcessors(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
threadId: `processors-test-${Date.now()}`,
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createIntegrationMessageList()
|
||||
const inputArgs: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Test processors factory")],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await input.processInput(inputArgs)
|
||||
expect(messageList.addSystem).toHaveBeenCalled()
|
||||
|
||||
const outputArgs: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "Test processors factory"),
|
||||
createMessage("assistant", "Response"),
|
||||
],
|
||||
messageList: createIntegrationMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await output.processOutputResult(outputArgs)
|
||||
})
|
||||
})
|
||||
|
||||
describe("withSupermemory wrapper", () => {
|
||||
it("should enhance config with working processors", async () => {
|
||||
const config: MockAgentConfig = {
|
||||
id: "test-mastra-agent",
|
||||
name: "Test Mastra Agent",
|
||||
model: "gpt-4o",
|
||||
}
|
||||
|
||||
const enhanced = withSupermemory(
|
||||
config,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
threadId: `wrapper-test-${Date.now()}`,
|
||||
},
|
||||
)
|
||||
|
||||
expect(enhanced.id).toBe("test-mastra-agent")
|
||||
expect(enhanced.name).toBe("Test Mastra Agent")
|
||||
expect(enhanced.model).toBe("gpt-4o")
|
||||
expect(enhanced.inputProcessors).toHaveLength(1)
|
||||
expect(enhanced.outputProcessors).toHaveLength(1)
|
||||
|
||||
const inputProcessor = enhanced.inputProcessors?.[0]
|
||||
expect(inputProcessor?.id).toBe("supermemory-input")
|
||||
|
||||
if (inputProcessor?.processInput) {
|
||||
const messageList = createIntegrationMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Wrapper test")],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await inputProcessor.processInput(args)
|
||||
expect(messageList.addSystem).toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
|
||||
it("should merge with existing processors correctly", async () => {
|
||||
const existingInputProcessor = {
|
||||
id: "existing-input",
|
||||
name: "Existing Input Processor",
|
||||
processInput: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
const existingOutputProcessor = {
|
||||
id: "existing-output",
|
||||
name: "Existing Output Processor",
|
||||
processOutputResult: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
const config: MockAgentConfig = {
|
||||
id: "agent-with-processors",
|
||||
name: "Agent With Processors",
|
||||
inputProcessors: [existingInputProcessor],
|
||||
outputProcessors: [existingOutputProcessor],
|
||||
}
|
||||
|
||||
const enhanced = withSupermemory(
|
||||
config,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
expect(enhanced.inputProcessors).toHaveLength(2)
|
||||
expect(enhanced.outputProcessors).toHaveLength(2)
|
||||
|
||||
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
|
||||
expect(enhanced.inputProcessors?.[1]?.id).toBe("existing-input")
|
||||
|
||||
expect(enhanced.outputProcessors?.[0]?.id).toBe("existing-output")
|
||||
expect(enhanced.outputProcessors?.[1]?.id).toBe("supermemory-output")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Options", () => {
|
||||
it("verbose mode should not break functionality", async () => {
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
verbose: true,
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createIntegrationMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Verbose mode test")],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
expect(messageList.addSystem).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("custom baseUrl should be used for API calls", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Base URL test")],
|
||||
systemMessages: [],
|
||||
messageList: createIntegrationMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
const profileCalls = fetchSpy.mock.calls.filter(
|
||||
(call) =>
|
||||
typeof call[0] === "string" && call[0].includes("/v4/profile"),
|
||||
)
|
||||
expect(profileCalls.length).toBeGreaterThan(0)
|
||||
|
||||
const url = profileCalls[0]?.[0] as string
|
||||
expect(url.startsWith(INTEGRATION_CONFIG.baseUrl)).toBe(true)
|
||||
|
||||
fetchSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Error handling", () => {
|
||||
it("should handle invalid API key gracefully", async () => {
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: "invalid-api-key-12345",
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createIntegrationMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Invalid key test")],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
const result = await processor.processInput(args)
|
||||
expect(result).toBe(messageList)
|
||||
expect(messageList.addSystem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("output processor should handle save errors gracefully", async () => {
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: "invalid-api-key-12345",
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
addMemory: "always",
|
||||
threadId: "error-test",
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "Error test"),
|
||||
createMessage("assistant", "Response"),
|
||||
],
|
||||
messageList: createIntegrationMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await expect(processor.processOutputResult(args)).resolves.toBeDefined()
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
974
packages/tools/test/mastra/unit.test.ts
Normal file
974
packages/tools/test/mastra/unit.test.ts
Normal file
|
|
@ -0,0 +1,974 @@
|
|||
/**
|
||||
* Unit tests for the Mastra integration
|
||||
* Tests processors, wrapper, and factory functions
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"
|
||||
import {
|
||||
RequestContext,
|
||||
MASTRA_THREAD_ID_KEY,
|
||||
} from "@mastra/core/request-context"
|
||||
import {
|
||||
SupermemoryInputProcessor,
|
||||
SupermemoryOutputProcessor,
|
||||
createSupermemoryProcessor,
|
||||
createSupermemoryOutputProcessor,
|
||||
createSupermemoryProcessors,
|
||||
withSupermemory,
|
||||
} from "../../src/mastra"
|
||||
import type {
|
||||
ProcessInputArgs,
|
||||
ProcessOutputResultArgs,
|
||||
MessageList,
|
||||
MastraDBMessage,
|
||||
MastraMessageContentV2,
|
||||
Processor,
|
||||
} from "../../src/mastra"
|
||||
|
||||
const TEST_CONFIG = {
|
||||
apiKey: "test-api-key",
|
||||
baseUrl: "https://api.supermemory.ai",
|
||||
containerTag: "test-mastra-user",
|
||||
}
|
||||
|
||||
interface MockAgentConfig {
|
||||
id: string
|
||||
name?: string
|
||||
model?: string
|
||||
customProp?: string
|
||||
inputProcessors?: Processor[]
|
||||
outputProcessors?: Processor[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create MastraMessageContentV2 from text
|
||||
*/
|
||||
function createMessageContent(text: string): MastraMessageContentV2 {
|
||||
return {
|
||||
format: 2,
|
||||
parts: [{ type: "text", text }],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a MastraDBMessage
|
||||
*/
|
||||
function createMessage(
|
||||
role: "user" | "assistant" | "system",
|
||||
text: string,
|
||||
): MastraDBMessage {
|
||||
return {
|
||||
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
role,
|
||||
content: createMessageContent(text),
|
||||
createdAt: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
const createMockMessageList = (): MessageList & {
|
||||
calls: { method: string; args: unknown[] }[]
|
||||
} => {
|
||||
const calls: { method: string; args: unknown[] }[] = []
|
||||
return {
|
||||
calls,
|
||||
addSystem: vi.fn((content: string, _id?: string) => {
|
||||
calls.push({ method: "addSystem", args: [content, _id] })
|
||||
}),
|
||||
addUser: vi.fn((content: string) => {
|
||||
calls.push({ method: "addUser", args: [content] })
|
||||
}),
|
||||
addAssistant: vi.fn((content: string) => {
|
||||
calls.push({ method: "addAssistant", args: [content] })
|
||||
}),
|
||||
} as unknown as MessageList & { calls: { method: string; args: unknown[] }[] }
|
||||
}
|
||||
|
||||
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 })),
|
||||
},
|
||||
})
|
||||
|
||||
const createMockConversationResponse = () => ({
|
||||
id: "mem-123",
|
||||
conversationId: "conv-456",
|
||||
status: "created",
|
||||
})
|
||||
|
||||
describe("SupermemoryInputProcessor", () => {
|
||||
let originalEnv: string | undefined
|
||||
let originalFetch: typeof globalThis.fetch
|
||||
let fetchMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.SUPERMEMORY_API_KEY
|
||||
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
|
||||
originalFetch = globalThis.fetch
|
||||
fetchMock = vi.fn()
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv) {
|
||||
process.env.SUPERMEMORY_API_KEY = originalEnv
|
||||
} else {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
}
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create processor with default options", () => {
|
||||
const processor = new SupermemoryInputProcessor(TEST_CONFIG.containerTag)
|
||||
expect(processor.id).toBe("supermemory-input")
|
||||
expect(processor.name).toBe("Supermemory Memory Injection")
|
||||
})
|
||||
|
||||
it("should throw error if API key is not set", () => {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
expect(() => {
|
||||
new SupermemoryInputProcessor(TEST_CONFIG.containerTag)
|
||||
}).toThrow("SUPERMEMORY_API_KEY is not set")
|
||||
})
|
||||
|
||||
it("should accept API key via options", () => {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: "custom-key",
|
||||
},
|
||||
)
|
||||
expect(processor.id).toBe("supermemory-input")
|
||||
})
|
||||
})
|
||||
|
||||
describe("processInput", () => {
|
||||
it("should inject memories into messageList", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockProfileResponse(
|
||||
["User likes TypeScript"],
|
||||
["Recent interest in AI"],
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createMockMessageList()
|
||||
const messages: MastraDBMessage[] = [createMessage("user", "Hello")]
|
||||
|
||||
const args: ProcessInputArgs = {
|
||||
messages,
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
expect(messageList.addSystem).toHaveBeenCalled()
|
||||
const systemCall = messageList.calls.find((c) => c.method === "addSystem")
|
||||
expect(systemCall).toBeDefined()
|
||||
expect(systemCall?.args[0]).toContain("TypeScript")
|
||||
expect(systemCall?.args[1]).toBe("supermemory")
|
||||
})
|
||||
|
||||
it("should use cached memories on second call with same message", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(createMockProfileResponse(["Cached memory"])),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const messages: MastraDBMessage[] = [createMessage("user", "Hello")]
|
||||
|
||||
const args1: ProcessInputArgs = {
|
||||
messages,
|
||||
systemMessages: [],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args1)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
const args2: ProcessInputArgs = {
|
||||
messages,
|
||||
systemMessages: [],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args2)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should refetch memories for different user message", async () => {
|
||||
let callCount = 0
|
||||
fetchMock.mockImplementation(() => {
|
||||
callCount++
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockProfileResponse([`Memory from call ${callCount}`]),
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "query",
|
||||
},
|
||||
)
|
||||
|
||||
const args1: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "First message")],
|
||||
systemMessages: [],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args1)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
const args2: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Different message")],
|
||||
systemMessages: [],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args2)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("should return messageList in query mode when no user message", async () => {
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "query",
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createMockMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
const result = await processor.processInput(args)
|
||||
|
||||
expect(result).toBe(messageList)
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
expect(messageList.addSystem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle API errors gracefully", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
text: () => Promise.resolve("Server error"),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const messageList = createMockMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Hello")],
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
const result = await processor.processInput(args)
|
||||
|
||||
expect(result).toBe(messageList)
|
||||
expect(messageList.addSystem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should use threadId from options", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
threadId: "thread-123",
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Hello")],
|
||||
systemMessages: [],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should use threadId from requestContext when not in options", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
|
||||
const requestContext = new RequestContext()
|
||||
requestContext.set(MASTRA_THREAD_ID_KEY, "ctx-thread-456")
|
||||
|
||||
const args: ProcessInputArgs = {
|
||||
messages: [createMessage("user", "Hello")],
|
||||
systemMessages: [],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
requestContext,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle messages with array content parts", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockProfileResponse(["Memory"])),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryInputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
mode: "query",
|
||||
},
|
||||
)
|
||||
|
||||
const messages: MastraDBMessage[] = [
|
||||
{
|
||||
id: "msg-1",
|
||||
role: "user",
|
||||
content: {
|
||||
format: 2,
|
||||
parts: [
|
||||
{ type: "text", text: "Hello " },
|
||||
{ type: "text", text: "World" },
|
||||
],
|
||||
},
|
||||
createdAt: new Date(),
|
||||
},
|
||||
]
|
||||
|
||||
const messageList = createMockMessageList()
|
||||
const args: ProcessInputArgs = {
|
||||
messages,
|
||||
systemMessages: [],
|
||||
messageList,
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processInput(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("SupermemoryOutputProcessor", () => {
|
||||
let originalEnv: string | undefined
|
||||
let originalFetch: typeof globalThis.fetch
|
||||
let fetchMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.SUPERMEMORY_API_KEY
|
||||
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
|
||||
originalFetch = globalThis.fetch
|
||||
fetchMock = vi.fn()
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv) {
|
||||
process.env.SUPERMEMORY_API_KEY = originalEnv
|
||||
} else {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
}
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create processor with default options", () => {
|
||||
const processor = new SupermemoryOutputProcessor(TEST_CONFIG.containerTag)
|
||||
expect(processor.id).toBe("supermemory-output")
|
||||
expect(processor.name).toBe("Supermemory Conversation Save")
|
||||
})
|
||||
})
|
||||
|
||||
describe("processOutputResult", () => {
|
||||
it("should save conversation when addMemory is always", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
},
|
||||
)
|
||||
|
||||
const messages: MastraDBMessage[] = [
|
||||
createMessage("user", "Hello"),
|
||||
createMessage("assistant", "Hi there!"),
|
||||
]
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages,
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/v4/conversations"),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: expect.objectContaining({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${TEST_CONFIG.apiKey}`,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
expect(callBody.conversationId).toBe("conv-456")
|
||||
expect(callBody.messages).toHaveLength(2)
|
||||
expect(callBody.containerTags).toContain(TEST_CONFIG.containerTag)
|
||||
})
|
||||
|
||||
it("should not save conversation when addMemory is never", async () => {
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "never",
|
||||
threadId: "conv-456",
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "Hello"),
|
||||
createMessage("assistant", "Hi!"),
|
||||
],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not save when no threadId provided", async () => {
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "Hello"),
|
||||
createMessage("assistant", "Hi!"),
|
||||
],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should use threadId from requestContext", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
},
|
||||
)
|
||||
|
||||
const requestContext = new RequestContext()
|
||||
requestContext.set(MASTRA_THREAD_ID_KEY, "ctx-thread-789")
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "Hello"),
|
||||
createMessage("assistant", "Hi!"),
|
||||
],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
requestContext,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
expect(callBody.conversationId).toBe("ctx-thread-789")
|
||||
})
|
||||
|
||||
it("should skip system messages when saving", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
},
|
||||
)
|
||||
|
||||
const messages: MastraDBMessage[] = [
|
||||
createMessage("system", "You are a helpful assistant"),
|
||||
createMessage("user", "Hello"),
|
||||
createMessage("assistant", "Hi there!"),
|
||||
]
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages,
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
expect(callBody.messages).toHaveLength(2)
|
||||
expect(
|
||||
callBody.messages.every((m: { role: string }) => m.role !== "system"),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle messages with array content parts", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createMockConversationResponse()),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
},
|
||||
)
|
||||
|
||||
const messages: MastraDBMessage[] = [
|
||||
{
|
||||
id: "msg-1",
|
||||
role: "user",
|
||||
content: {
|
||||
format: 2,
|
||||
parts: [
|
||||
{ type: "text", text: "Hello" },
|
||||
{ type: "text", text: " World" },
|
||||
],
|
||||
},
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: "msg-2",
|
||||
role: "assistant",
|
||||
content: {
|
||||
format: 2,
|
||||
parts: [{ type: "text", text: "Hi!" }],
|
||||
},
|
||||
createdAt: new Date(),
|
||||
},
|
||||
]
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages,
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
const callBody = JSON.parse(
|
||||
(fetchMock.mock.calls[0]?.[1] as { body: string }).body,
|
||||
)
|
||||
expect(callBody.messages).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should handle save errors gracefully", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
text: () => Promise.resolve("Server error"),
|
||||
})
|
||||
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [
|
||||
createMessage("user", "Hello"),
|
||||
createMessage("assistant", "Hi!"),
|
||||
],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
// Should not throw
|
||||
await expect(processor.processOutputResult(args)).resolves.toBeDefined()
|
||||
})
|
||||
|
||||
it("should not save when no messages to save", async () => {
|
||||
const processor = new SupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
addMemory: "always",
|
||||
threadId: "conv-456",
|
||||
},
|
||||
)
|
||||
|
||||
const args: ProcessOutputResultArgs = {
|
||||
messages: [],
|
||||
messageList: createMockMessageList(),
|
||||
abort: vi.fn() as never,
|
||||
retryCount: 0,
|
||||
}
|
||||
|
||||
await processor.processOutputResult(args)
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Factory functions", () => {
|
||||
let originalEnv: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.SUPERMEMORY_API_KEY
|
||||
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv) {
|
||||
process.env.SUPERMEMORY_API_KEY = originalEnv
|
||||
} else {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
}
|
||||
})
|
||||
|
||||
describe("createSupermemoryProcessor", () => {
|
||||
it("should create input processor", () => {
|
||||
const processor = createSupermemoryProcessor(TEST_CONFIG.containerTag)
|
||||
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
|
||||
expect(processor.id).toBe("supermemory-input")
|
||||
})
|
||||
|
||||
it("should pass options to processor", () => {
|
||||
const processor = createSupermemoryProcessor(TEST_CONFIG.containerTag, {
|
||||
apiKey: "custom-key",
|
||||
mode: "full",
|
||||
})
|
||||
expect(processor).toBeInstanceOf(SupermemoryInputProcessor)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createSupermemoryOutputProcessor", () => {
|
||||
it("should create output processor", () => {
|
||||
const processor = createSupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
)
|
||||
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
|
||||
expect(processor.id).toBe("supermemory-output")
|
||||
})
|
||||
|
||||
it("should pass options to processor", () => {
|
||||
const processor = createSupermemoryOutputProcessor(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: "custom-key",
|
||||
addMemory: "always",
|
||||
threadId: "conv-123",
|
||||
},
|
||||
)
|
||||
expect(processor).toBeInstanceOf(SupermemoryOutputProcessor)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createSupermemoryProcessors", () => {
|
||||
it("should create both input and output processors", () => {
|
||||
const { input, output } = createSupermemoryProcessors(
|
||||
TEST_CONFIG.containerTag,
|
||||
)
|
||||
expect(input).toBeInstanceOf(SupermemoryInputProcessor)
|
||||
expect(output).toBeInstanceOf(SupermemoryOutputProcessor)
|
||||
})
|
||||
|
||||
it("should share options between processors", () => {
|
||||
const { input, output } = createSupermemoryProcessors(
|
||||
TEST_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: "custom-key",
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
threadId: "conv-123",
|
||||
},
|
||||
)
|
||||
expect(input.id).toBe("supermemory-input")
|
||||
expect(output.id).toBe("supermemory-output")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("withSupermemory", () => {
|
||||
let originalEnv: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.SUPERMEMORY_API_KEY
|
||||
process.env.SUPERMEMORY_API_KEY = TEST_CONFIG.apiKey
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv) {
|
||||
process.env.SUPERMEMORY_API_KEY = originalEnv
|
||||
} else {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
}
|
||||
})
|
||||
|
||||
describe("API key validation", () => {
|
||||
it("should throw error if API key is not set", () => {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
||||
|
||||
expect(() => {
|
||||
withSupermemory(config, TEST_CONFIG.containerTag)
|
||||
}).toThrow("SUPERMEMORY_API_KEY is not set")
|
||||
})
|
||||
|
||||
it("should accept API key via options", () => {
|
||||
delete process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
||||
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag, {
|
||||
apiKey: "custom-key",
|
||||
})
|
||||
|
||||
expect(enhanced).toBeDefined()
|
||||
expect(enhanced.inputProcessors).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("processor injection", () => {
|
||||
it("should inject input and output processors", () => {
|
||||
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
||||
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
|
||||
|
||||
expect(enhanced.inputProcessors).toHaveLength(1)
|
||||
expect(enhanced.outputProcessors).toHaveLength(1)
|
||||
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
|
||||
expect(enhanced.outputProcessors?.[0]?.id).toBe("supermemory-output")
|
||||
})
|
||||
|
||||
it("should preserve original config properties", () => {
|
||||
const config: MockAgentConfig = {
|
||||
id: "test-agent",
|
||||
name: "Test Agent",
|
||||
model: "gpt-4",
|
||||
customProp: "value",
|
||||
}
|
||||
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
|
||||
|
||||
expect(enhanced.id).toBe("test-agent")
|
||||
expect(enhanced.name).toBe("Test Agent")
|
||||
expect(enhanced.model).toBe("gpt-4")
|
||||
expect(enhanced.customProp).toBe("value")
|
||||
})
|
||||
|
||||
it("should prepend input processor to existing processors", () => {
|
||||
const existingInputProcessor: Processor = {
|
||||
id: "existing-input",
|
||||
name: "Existing Input",
|
||||
}
|
||||
const config: MockAgentConfig = {
|
||||
id: "test-agent",
|
||||
name: "Test Agent",
|
||||
inputProcessors: [existingInputProcessor],
|
||||
}
|
||||
|
||||
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
|
||||
|
||||
expect(enhanced.inputProcessors).toHaveLength(2)
|
||||
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
|
||||
expect(enhanced.inputProcessors?.[1]?.id).toBe("existing-input")
|
||||
})
|
||||
|
||||
it("should append output processor to existing processors", () => {
|
||||
const existingOutputProcessor: Processor = {
|
||||
id: "existing-output",
|
||||
name: "Existing Output",
|
||||
}
|
||||
const config: MockAgentConfig = {
|
||||
id: "test-agent",
|
||||
name: "Test Agent",
|
||||
outputProcessors: [existingOutputProcessor],
|
||||
}
|
||||
|
||||
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
|
||||
|
||||
expect(enhanced.outputProcessors).toHaveLength(2)
|
||||
expect(enhanced.outputProcessors?.[0]?.id).toBe("existing-output")
|
||||
expect(enhanced.outputProcessors?.[1]?.id).toBe("supermemory-output")
|
||||
})
|
||||
|
||||
it("should handle configs with both existing input and output processors", () => {
|
||||
const existingInput: Processor = { id: "existing-input" }
|
||||
const existingOutput: Processor = { id: "existing-output" }
|
||||
const config: MockAgentConfig = {
|
||||
id: "test-agent",
|
||||
name: "Test Agent",
|
||||
inputProcessors: [existingInput],
|
||||
outputProcessors: [existingOutput],
|
||||
}
|
||||
|
||||
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag)
|
||||
|
||||
expect(enhanced.inputProcessors).toHaveLength(2)
|
||||
expect(enhanced.outputProcessors).toHaveLength(2)
|
||||
expect(enhanced.inputProcessors?.[0]?.id).toBe("supermemory-input")
|
||||
expect(enhanced.inputProcessors?.[1]?.id).toBe("existing-input")
|
||||
expect(enhanced.outputProcessors?.[0]?.id).toBe("existing-output")
|
||||
expect(enhanced.outputProcessors?.[1]?.id).toBe("supermemory-output")
|
||||
})
|
||||
})
|
||||
|
||||
describe("options passthrough", () => {
|
||||
it("should pass options to processors", () => {
|
||||
const config: MockAgentConfig = { id: "test-agent", name: "Test Agent" }
|
||||
const enhanced = withSupermemory(config, TEST_CONFIG.containerTag, {
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
threadId: "conv-123",
|
||||
verbose: true,
|
||||
})
|
||||
|
||||
expect(enhanced.inputProcessors).toHaveLength(1)
|
||||
expect(enhanced.outputProcessors).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -6,6 +6,7 @@ export default defineConfig({
|
|||
"src/ai-sdk.ts",
|
||||
"src/claude-memory.ts",
|
||||
"src/openai/index.ts",
|
||||
"src/mastra.ts",
|
||||
],
|
||||
format: "esm",
|
||||
sourcemap: false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue