diff --git a/biome.json b/biome.json index 0959d573..9519c53e 100644 --- a/biome.json +++ b/biome.json @@ -1,75 +1,105 @@ { "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "assist": { + "actions": { + "source": { + "organizeImports": "on", + "useSortedAttributes": "on", + "useSortedKeys": "off" + } + }, + "enabled": true + }, "files": { "includes": [ - "**/*.js", - "**/*.jsx", - "**/*.ts", - "**/*.tsx", - "**/*.json", - "**/*.jsonc", + "**", "!**/node_modules", + "!**/.next/", + "!**/.contentlayer", + "!**/.vercel", + "!**/.react-router", + "!**/.wrangler", + "!**/package.json", + "!**/worker-configuration.d.ts", + "!**/.turbo/", + "!**/.vercel", "!**/dist", + "!**/.astro", "!**/build", - "!**/.next", - "!**/.turbo", - "!**/*.d.ts" + "!**/.alchemy", + "!**/.build", + "!**/.open-next", + "!**/*.astro" ] }, "formatter": { "enabled": true, - "formatWithErrors": false, - "indentStyle": "tab", - "indentWidth": 2, - "lineEnding": "lf", - "lineWidth": 80, - "attributePosition": "auto", - "includes": ["**", "!**/*.md"] - }, - "assist": { "actions": { "source": { "organizeImports": "on" } } }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "a11y": { - "recommended": true - }, - "complexity": { - "recommended": true - }, - "correctness": { - "recommended": true - }, - "performance": { - "recommended": true - }, - "security": { - "recommended": true - }, - "style": { - "recommended": true - }, - "suspicious": { - "recommended": true - } - } + "indentStyle": "tab" }, "javascript": { "formatter": { "quoteStyle": "double", - "jsxQuoteStyle": "double", - "trailingCommas": "all", - "semicolons": "asNeeded", - "arrowParentheses": "always", - "bracketSpacing": true, - "bracketSameLine": false, - "quoteProperties": "asNeeded", - "attributePosition": "auto" + "semicolons": "asNeeded" } }, - "json": { - "formatter": { - "trailingCommas": "none" + "linter": { + "domains": { + "project": "none" + }, + "enabled": true, + "rules": { + "a11y": { + "useValidAnchor": "warn" + }, + "correctness": { + "noUnusedVariables": { + "level": "warn", + "options": { + "ignoreRestSiblings": true + } + }, + "useExhaustiveDependencies": "warn", + "noUnusedImports": "warn" + }, + "recommended": true, + "style": { + "noDefaultExport": "off", + "noInferrableTypes": "error", + "noNonNullAssertion": "warn", + "noParameterAssign": "error", + "noUnusedTemplateLiteral": "error", + "noUselessElse": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useNamingConvention": { + "level": "off", + "options": { + "strictCase": false + } + }, + "useNumberNamespace": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error" + } } + }, + "overrides": [ + { + "includes": ["**/*.astro"], + "linter": { + "rules": { + "correctness": { + "noUnusedVariables": "off", + "noUnusedImports": "off" + } + } + } + } + ], + "vcs": { + "clientKind": "git", + "enabled": true, + "useIgnoreFile": true } } diff --git a/bun.lockb b/bun.lockb index 44008365..2143da85 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index ca4b4df9..369a3f32 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "zod-openapi": "^4.2.4" }, "devDependencies": { - "@biomejs/biome": "^2.1.3", + "@biomejs/biome": "^2.2.0", "@total-typescript/tsconfig": "^1.0.4", "@types/pg": "^8.15.4", "drizzle-kit": "^0.31.4", diff --git a/packages/ai-sdk/README.md b/packages/ai-sdk/README.md new file mode 100644 index 00000000..da7fa8ab --- /dev/null +++ b/packages/ai-sdk/README.md @@ -0,0 +1,313 @@ +# supermemory AI SDK Utilities + +Vercel AI SDK utilities for supermemory + +## Installation + +```bash +npm install @supermemory/ai-sdk +# or +bun add @supermemory/ai-sdk +# or +pnpm add @supermemory/ai-sdk +# or +yarn add @supermemory/ai-sdk +``` + +## Features + +Choose **one** of the following approaches (they cannot be used together): + +- **Infinite Chat Provider**: Connect to various LLM providers with unlimited context support +- **Memory Tools**: Search, add, and fetch memories from supermemory using AI agents + +## Infinite Chat Provider + +The infinite chat provider allows you to connect to various LLM providers with supermemory's context management. + +```typescript +import { createSupermemoryInfiniteChat } from '@supermemory/ai-sdk' +import { generateText } from 'ai' + +// Using a named provider +const supermemory = createSupermemoryInfiniteChat('your-supermemory-api-key', { + providerName: 'openai', + providerApiKey: 'your-openai-api-key', + headers: { + // Optional additional headers + } +}) + +// Using a custom provider URL +const supermemory = createSupermemoryInfiniteChat('your-supermemory-api-key', { + providerUrl: 'https://your-custom-provider.com/v1/chat/completions', + providerApiKey: 'your-provider-api-key', + headers: { + // Optional additional headers + } +}) + +const result = await generateText({ + model: supermemory('gpt-4-turbo'), + messages: [ + { role: 'user', content: 'Hello, how are you?' } + ] +}) +``` + +### Complete Infinite Chat Example + +```typescript +import { createSupermemoryInfiniteChat } from '@supermemory/ai-sdk' +import { generateText } from 'ai' + +const supermemoryApiKey = process.env.SUPERMEMORY_API_KEY! +const openaiApiKey = process.env.OPENAI_API_KEY! + +// Initialize infinite chat provider +const supermemory = createSupermemoryInfiniteChat(supermemoryApiKey, { + providerName: 'openai', + providerApiKey: openaiApiKey, + headers: {} +}) + +async function chat(userMessage: string) { + const result = await generateText({ + model: supermemory('gpt-4-turbo'), + messages: [ + { + role: 'system', + content: 'You are a helpful assistant with unlimited context.' + }, + { + role: 'user', + content: userMessage + } + ] + // No tools - infinite chat handles context automatically + }) + + return result.text +} +``` + +### Configuration + +```typescript +// Option 1: Use a named provider +interface ConfigWithProviderName { + providerName: 'openai' | 'anthropic' | 'openrouter' | 'deepinfra' | 'groq' | 'google' | 'cloudflare' + providerApiKey: string + headers?: Record +} + +// Option 2: Use a custom provider URL +interface ConfigWithProviderUrl { + providerUrl: string + providerApiKey: string + headers?: Record +} +``` + +## Memory Tools + +supermemory tools allow AI agents to interact with user memories for enhanced context and personalization. + +```typescript +import { supermemoryTools } from '@supermemory/ai-sdk' +import { generateText } from 'ai' + +const result = await generateText({ + model: openai('gpt-4-turbo'), + messages: [ + { role: 'user', content: 'What do you remember about my preferences?' } + ], + tools: { + ...supermemoryTools('your-supermemory-api-key', { + // Optional: specify a base URL for self-hosted instances + baseUrl: 'https://api.supermemory.com', + + // Use either projectId OR containerTags, not both + projectId: 'your-project-id', + // OR + containerTags: ['tag1', 'tag2'] +}), +// Your other tools go here + } +}) +``` + +### Complete Memory Tools Example + +```typescript +import { supermemoryTools } from '@supermemory/ai-sdk' +import { generateText } from 'ai' +import { openai } from '@ai-sdk/openai' + +const supermemoryApiKey = process.env.SUPERMEMORY_API_KEY! + +async function chatWithTools(userMessage: string) { + const result = await generateText({ + model: openai('gpt-4-turbo'), // Use standard provider + messages: [ + { + role: 'system', + content: 'You are a helpful assistant with access to user memories.' + }, + { + role: 'user', + content: userMessage + } + ], + tools: { + ...supermemoryTools(supermemoryApiKey, { + projectId: 'my-project' + }) + }, + maxToolRoundtrips: 5 + }) + + return result.text +} +``` + +### Configuration + +```typescript +interface SupermemoryConfig { + // Optional: Base URL for API calls (default: https://api.supermemory.com) + baseUrl?: string + + // Container tags for organizing memories (cannot be used with projectId) + containerTags?: string[] + + // Project ID for scoping memories (cannot be used with containerTags) + projectId?: string +} +``` + +### Self-Hosted supermemory + +If you're running a self-hosted supermemory instance: + +```typescript +const tools = supermemoryTools('your-api-key', { + baseUrl: 'https://your-supermemory-instance.com', + containerTags: ['production', 'user-memories'] +}) +``` + +### Available Tools + +##### Search Memories + +Search through user memories using semantic matching. + +```typescript +const searchResult = await tools.searchMemories.execute({ + informationToGet: 'user preferences about coffee' +}) +``` + +##### Add Memory + +Add new memories to the user's memory store. + +```typescript +const addResult = await tools.addMemory.execute({ + memory: 'User prefers dark roast coffee in the morning' +}) +``` + +##### Fetch Memory + +Retrieve a specific memory by its ID. + +```typescript +const fetchResult = await tools.fetchMemory.execute({ + memoryId: 'memory-id-123' +}) +``` + +### Using Individual Tools + +For more flexibility, you can import and use individual tools: + +```typescript +import { + searchMemoriesTool, + addMemoryTool, + fetchMemoryTool +} from '@supermemory/ai-sdk' + +const searchTool = searchMemoriesTool('your-api-key', { + projectId: 'your-project-id' +}) + +// Use only the search tool +const result = await generateText({ + model: openai('gpt-4-turbo'), + messages: [...], + tools: { + searchMemories: searchTool + } +}) +``` + +### Error Handling + +All tool executions return a result object with a `success` field: + +```typescript +const result = await tools.searchMemories.execute({ + informationToGet: 'user preferences' +}) + +if (result.success) { + console.log('Found memories:', result.results) + console.log('Total count:', result.count) +} else { + console.error('Error searching memories:', result.error) +} +``` + +## Development + +### Running Tests + +```bash +# Run all tests +bun test + +# Run tests in watch mode +bun test --watch +``` + +#### Environment Variables for Tests + +All tests require API keys to run. Copy `.env.example` to `.env` and set the required values: + +```bash +cp .env.example .env +``` + +**Required:** +- `SUPERMEMORY_API_KEY`: Your Supermemory API key +- `PROVIDER_API_KEY`: Your AI provider API key (OpenAI, Anthropic, etc.) +- `OPENAI_API_KEY`: Your OpenAI API key for tool integration tests + +**Optional:** +- `SUPERMEMORY_BASE_URL`: Custom Supermemory base URL (defaults to `https://api.supermemory.ai`) +- `PROVIDER_NAME`: Provider name (defaults to `openai`) - one of: `openai`, `anthropic`, `openrouter`, `deepinfra`, `groq`, `google`, `cloudflare` +- `PROVIDER_URL`: Custom provider URL (use instead of `PROVIDER_NAME`) +- `MODEL_NAME`: Model to use in tests (defaults to `gpt-3.5-turbo`) + +Tests will fail if required API keys are not provided. + +## License + +MIT + +## Support + +Email our [24/7 Founder/CEO/Support Executive](dhravya@supermemory.com) \ No newline at end of file diff --git a/packages/ai-sdk/package.json b/packages/ai-sdk/package.json new file mode 100644 index 00000000..88b6fcee --- /dev/null +++ b/packages/ai-sdk/package.json @@ -0,0 +1,26 @@ +{ + "name": "@supermemory/ai-sdk", + "version": "0.0.1", + "main": "dist/index.mjs", + "types": "dist/index.d.mts", + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "check-types": "tsc --noEmit", + "test": "vitest", + "test:watch": "vitest --watch" + }, + "dependencies": { + "@ai-sdk/openai": "^2.0.15", + "ai": "^5.0.17", + "supermemory": "^3.0.0-alpha.22", + "zod": "^4.0.17" + }, + "devDependencies": { + "@total-typescript/tsconfig": "^1.0.4", + "@types/bun": "^1.2.20", + "tsdown": "^0.14.1", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + } +} diff --git a/packages/ai-sdk/src/index.ts b/packages/ai-sdk/src/index.ts new file mode 100644 index 00000000..b6962318 --- /dev/null +++ b/packages/ai-sdk/src/index.ts @@ -0,0 +1,2 @@ +export * from "./infinite-chat" +export * from "./tools" diff --git a/packages/ai-sdk/src/infinite-chat.test.ts b/packages/ai-sdk/src/infinite-chat.test.ts new file mode 100644 index 00000000..92562b2e --- /dev/null +++ b/packages/ai-sdk/src/infinite-chat.test.ts @@ -0,0 +1,365 @@ +import { generateText } from "ai" +import { describe, expect, it } from "vitest" +import z from "zod" +import { + createSupermemoryInfiniteChat, + type SupermemoryInfiniteChatConfig, +} from "./infinite-chat" + +import "dotenv/config" + +const providers = z.enum([ + "openai", + "anthropic", + "openrouter", + "deepinfra", + "groq", + "google", + "cloudflare", +] satisfies SupermemoryInfiniteChatConfig["providerName"][]) + +describe("createSupermemoryInfiniteChat", () => { + // Required API keys - tests will fail if not provided + const testApiKey = process.env.SUPERMEMORY_API_KEY + const testProviderApiKey = process.env.PROVIDER_API_KEY + + if (!testApiKey) { + throw new Error( + "SUPERMEMORY_API_KEY environment variable is required for tests", + ) + } + if (!testProviderApiKey) { + throw new Error( + "PROVIDER_API_KEY environment variable is required for tests", + ) + } + + // Optional configuration with defaults + const testProviderName = providers.parse( + process.env.PROVIDER_NAME ?? "openai", + ) + const testProviderUrl = process.env.PROVIDER_URL + const testModelName = process.env.MODEL_NAME || "gpt-5-mini" + const testHeaders = { "custom-header": "test-value" } + + // Validate provider configuration - either name OR URL, not both + if (testProviderUrl && process.env.PROVIDER_NAME) { + throw new Error( + "Cannot specify both PROVIDER_NAME and PROVIDER_URL - use one or the other", + ) + } + + // Test prompts and inputs + const testPrompts = [ + "Hello, how are you?", + "What is 2 + 2?", + "Write a short poem about AI", + "Explain quantum computing in simple terms", + "What can you help me with today?", + ] + + const testMessages = [ + [{ role: "user" as const, content: "Hello!" }], + [ + { role: "system" as const, content: "You are a helpful assistant." }, + { role: "user" as const, content: "What is AI?" }, + ], + [ + { role: "user" as const, content: "Tell me a joke" }, + { + role: "assistant" as const, + content: + "Why don't scientists trust atoms? Because they make up everything!", + }, + { role: "user" as const, content: "Tell me another one" }, + ], + ] + + describe("client creation", () => { + it("should create client with configured provider", () => { + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: testHeaders, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: testHeaders, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + expect(client).toBeDefined() + expect(typeof client).toBe("function") + }) + + it("should create client with openai provider configuration", () => { + const config: SupermemoryInfiniteChatConfig = { + providerName: "openai", + providerApiKey: testProviderApiKey, + headers: testHeaders, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + expect(client).toBeDefined() + expect(typeof client).toBe("function") + }) + + it("should create client with anthropic provider configuration", () => { + const config: SupermemoryInfiniteChatConfig = { + providerName: "anthropic", + providerApiKey: testProviderApiKey, + headers: testHeaders, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + expect(client).toBeDefined() + expect(typeof client).toBe("function") + }) + + it("should create client with custom provider URL", () => { + const customUrl = "https://custom-provider.com/v1/chat" + const config: SupermemoryInfiniteChatConfig = { + providerUrl: customUrl, + providerApiKey: testProviderApiKey, + headers: testHeaders, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + expect(client).toBeDefined() + expect(typeof client).toBe("function") + }) + }) + + describe("AI SDK integration", () => { + it("should generate text with simple prompt", async () => { + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: {}, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: {}, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + console.log(client(testModelName)) + + const result = await generateText({ + model: client(testModelName), + prompt: testPrompts[0], // "Hello, how are you?" + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(typeof result.text).toBe("string") + expect(result.text.length).toBeGreaterThan(0) + }) + + it("should generate text with messages array", async () => { + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: {}, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: {}, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + const result = await generateText({ + model: client(testModelName), + messages: testMessages[1], // System + user messages + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(typeof result.text).toBe("string") + expect(result.text.length).toBeGreaterThan(0) + }) + + it("should handle conversation history", async () => { + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: {}, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: {}, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + const result = await generateText({ + model: client(testModelName), + messages: testMessages[2], // Multi-turn conversation + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(typeof result.text).toBe("string") + expect(result.text.length).toBeGreaterThan(0) + }) + + it("should work with different prompt variations", async () => { + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: {}, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: {}, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + // Test multiple prompts + for (const prompt of testPrompts.slice(0, 3)) { + const result = await generateText({ + model: client(testModelName), + prompt, + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(typeof result.text).toBe("string") + expect(result.text.length).toBeGreaterThan(0) + } + }) + + it("should work with configured and alternate models", async () => { + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: {}, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: {}, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + const modelsToTest = [testModelName] + // Add alternate model for OpenAI + if (testProviderName === "openai" && !testProviderUrl) { + modelsToTest.push("gpt-4o-mini") + } + + for (const modelName of modelsToTest) { + const result = await generateText({ + model: client(modelName), + prompt: "Say hello in one word", + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(typeof result.text).toBe("string") + } + }) + + it("should work with custom headers", async () => { + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: { + "x-custom-header": "test-value", + }, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: { + "x-custom-header": "test-value", + }, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + const result = await generateText({ + model: client(testModelName), + prompt: "Hello", + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(typeof result.text).toBe("string") + }) + }) + + describe("configuration validation", () => { + it("should handle empty headers object", () => { + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: {}, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: {}, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + expect(client).toBeDefined() + }) + + it("should handle configuration with custom headers", () => { + const customHeaders = { + authorization: "Bearer custom-token", + "x-custom": "custom-value", + } + const config: SupermemoryInfiniteChatConfig = testProviderUrl + ? { + providerUrl: testProviderUrl, + providerApiKey: testProviderApiKey, + headers: customHeaders, + } + : { + providerName: testProviderName, + providerApiKey: testProviderApiKey, + headers: customHeaders, + } + + const client = createSupermemoryInfiniteChat(testApiKey, config) + + expect(client).toBeDefined() + }) + + it("should handle different API keys", () => { + const config: SupermemoryInfiniteChatConfig = { + providerName: "openai", + providerApiKey: "different-provider-key", + headers: {}, + } + + const client = createSupermemoryInfiniteChat("different-sm-key", config) + + expect(client).toBeDefined() + }) + }) +}) diff --git a/packages/ai-sdk/src/infinite-chat.ts b/packages/ai-sdk/src/infinite-chat.ts new file mode 100644 index 00000000..d8b7c80a --- /dev/null +++ b/packages/ai-sdk/src/infinite-chat.ts @@ -0,0 +1,49 @@ +import { createOpenAI } from "@ai-sdk/openai" + +interface SupermemoryInfiniteChatConfigBase { + providerApiKey: string + headers: Record +} + +interface SupermemoryInfiniteChatConfigWithProviderName + extends SupermemoryInfiniteChatConfigBase { + providerName: keyof typeof providerMap + providerUrl?: never +} + +interface SupermemoryInfiniteChatConfigWithProviderUrl + extends SupermemoryInfiniteChatConfigBase { + providerUrl: string + providerName?: never +} + +export type SupermemoryInfiniteChatConfig = + | SupermemoryInfiniteChatConfigWithProviderName + | SupermemoryInfiniteChatConfigWithProviderUrl + +type SupermemoryApiKey = string + +const providerMap = { + openai: "https://api.openai.com/v1", + anthropic: "https://api.anthropic.com/v1", + openrouter: "https://openrouter.ai/api/v1", + deepinfra: "https://api.deepinfra.com/v1/openai", + groq: "https://api.groq.com/openai/v1", + google: "https://generativelanguage.googleapis.com/v1beta/openai", + cloudflare: "https://gateway.ai.cloudflare.com/v1/*/unlimited-context/openai", +} as const + +export const createSupermemoryInfiniteChat = ( + apiKey: SupermemoryApiKey, + config: SupermemoryInfiniteChatConfig, +) => + createOpenAI({ + apiKey: config.providerApiKey, + baseURL: config.providerName + ? providerMap[config.providerName] + : config.providerUrl, + headers: { + "x-api-key": apiKey, + ...config.headers, + }, + }).chat diff --git a/packages/ai-sdk/src/tools.test.ts b/packages/ai-sdk/src/tools.test.ts new file mode 100644 index 00000000..556a8511 --- /dev/null +++ b/packages/ai-sdk/src/tools.test.ts @@ -0,0 +1,185 @@ +import { createOpenAI } from "@ai-sdk/openai" +import { generateText } from "ai" +import { describe, expect, it } from "vitest" +import { type SupermemoryToolsConfig, supermemoryTools } from "./tools" + +import "dotenv/config" + +describe("supermemoryTools", () => { + // Required API keys - tests will fail if not provided + const testApiKey = process.env.SUPERMEMORY_API_KEY + const testOpenAIKey = process.env.OPENAI_API_KEY + + if (!testApiKey) { + throw new Error( + "SUPERMEMORY_API_KEY environment variable is required for tests", + ) + } + if (!testOpenAIKey) { + throw new Error("OPENAI_API_KEY environment variable is required for tests") + } + + // Optional configuration with defaults + const testBaseUrl = process.env.SUPERMEMORY_BASE_URL ?? undefined + const testModelName = process.env.MODEL_NAME || "gpt-5-mini" + + const testPrompts = [ + "What do you remember about my preferences?", + "Help me plan my day based on what you know about me", + "What are my current projects?", + "Remind me of my interests and hobbies", + "What should I focus on today?", + ] + + describe("client initialization", () => { + it("should create tools with default configuration", () => { + const config: SupermemoryToolsConfig = {} + const tools = supermemoryTools(testApiKey, config) + + expect(tools).toBeDefined() + expect(tools.searchMemories).toBeDefined() + expect(tools.addMemory).toBeDefined() + expect(tools.fetchMemory).toBeDefined() + }) + + it("should create tools with custom baseUrl", () => { + const config: SupermemoryToolsConfig = { + baseUrl: testBaseUrl, + } + const tools = supermemoryTools(testApiKey, config) + + expect(tools).toBeDefined() + expect(tools.searchMemories).toBeDefined() + expect(tools.addMemory).toBeDefined() + expect(tools.fetchMemory).toBeDefined() + }) + + it("should create tools with projectId configuration", () => { + const config: SupermemoryToolsConfig = { + projectId: "test-project-123", + } + const tools = supermemoryTools(testApiKey, config) + + expect(tools).toBeDefined() + expect(tools.searchMemories).toBeDefined() + expect(tools.addMemory).toBeDefined() + expect(tools.fetchMemory).toBeDefined() + }) + + it("should create tools with custom container tags", () => { + const config: SupermemoryToolsConfig = { + containerTags: ["custom-tag-1", "custom-tag-2"], + } + const tools = supermemoryTools(testApiKey, config) + + expect(tools).toBeDefined() + expect(tools.searchMemories).toBeDefined() + expect(tools.addMemory).toBeDefined() + expect(tools.fetchMemory).toBeDefined() + }) + }) + + describe("AI SDK integration", () => { + it("should work with AI SDK generateText", async () => { + const openai = createOpenAI({ + apiKey: testOpenAIKey, + }) + + const result = await generateText({ + model: openai(testModelName), + messages: [ + { + role: "system", + content: + "You are a helpful assistant with access to user memories. Use the search tool when the user asks about preferences or past information.", + }, + { + role: "user", + content: testPrompts[0]!, + }, + ], + tools: { + ...supermemoryTools(testApiKey, { + projectId: "test-ai-integration", + baseUrl: testBaseUrl, + }), + }, + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(typeof result.text).toBe("string") + }) + + it("should use tools when prompted", async () => { + const openai = createOpenAI({ + apiKey: testOpenAIKey, + }) + + const tools = supermemoryTools(testApiKey, { + projectId: "test-tool-usage", + baseUrl: testBaseUrl, + }) + + const result = await generateText({ + model: openai(testModelName), + messages: [ + { + role: "system", + content: + "You are a helpful assistant. When the user asks you to remember something, use the addMemory tool.", + }, + { + role: "user", + content: "Please remember that I prefer dark roast coffee", + }, + ], + tools: { + addMemory: tools.addMemory, + }, + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(result.toolCalls).toBeDefined() + + if (result.toolCalls && result.toolCalls.length > 0) { + const addMemoryCall = result.toolCalls.find( + (call) => call.toolName === "addMemory", + ) + expect(addMemoryCall).toBeDefined() + } + }) + + it("should handle multiple tool types", async () => { + const openai = createOpenAI({ + apiKey: testOpenAIKey, + }) + + const result = await generateText({ + model: openai(testModelName), + messages: [ + { + role: "system", + content: + "You are a helpful assistant with access to user memories. Use search when asked about preferences, and addMemory when told to remember something.", + }, + { + role: "user", + content: + "Search for my preferences and then remember that I also like green tea", + }, + ], + tools: { + ...supermemoryTools(testApiKey, { + containerTags: ["test-multi-tools"], + }), + }, + }) + + expect(result).toBeDefined() + expect(result.text).toBeDefined() + expect(typeof result.text).toBe("string") + }) + }) +}) diff --git a/packages/ai-sdk/src/tools.ts b/packages/ai-sdk/src/tools.ts new file mode 100644 index 00000000..7d99e2c7 --- /dev/null +++ b/packages/ai-sdk/src/tools.ts @@ -0,0 +1,147 @@ +import { tool } from "ai" +import Supermemory from "supermemory" +import { z } from "zod" + +/** + * Supermemory configuration + * Only one of `projectId` or `containerTags` can be provided. + */ +export interface SupermemoryToolsConfig { + baseUrl?: string + containerTags?: string[] + projectId?: string +} + +/** + * Create Supermemory tools for AI SDK + */ +export function supermemoryTools( + apiKey: string, + config: SupermemoryToolsConfig, +) { + const client = new Supermemory({ + apiKey, + ...(config.baseUrl && { baseURL: config.baseUrl }), + }) + + const containerTags = config.projectId + ? [`sm_project_${config.projectId}`] + : (config.containerTags ?? ["sm_project_default"]) + + const searchMemories = tool({ + description: + "Search user memories and patterns. Run when explicitly asked or when context about user's past choices would be helpful. Uses semantic matching to find relevant details across related experiences.", + inputSchema: z.object({ + informationToGet: z + .string() + .describe("Terms to search for in the user's memories"), + }), + execute: async ({ informationToGet }) => { + try { + const response = await client.search.execute({ + q: informationToGet, + containerTags, + limit: 10, + chunkThreshold: 0.6, + }) + + return { + success: true, + results: response.results, + count: response.results?.length || 0, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } + }, + }) + + const addMemory = tool({ + description: + "Add a new memory to the user's memories. Run when explicitly asked or when the user mentions any information generalizable beyond the context of the current conversation.", + inputSchema: z.object({ + memory: z + .string() + .describe( + "The text content of the memory to add. This should be a single sentence or a short paragraph.", + ), + }), + execute: async ({ memory }) => { + try { + const metadata: Record = {} + + const response = await client.memories.add({ + content: memory, + containerTags, + ...(Object.keys(metadata).length > 0 && { metadata }), + }) + + return { + success: true, + memory: response, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } + }, + }) + + const fetchMemory = tool({ + description: "Fetch a specific memory by ID to get its full details", + inputSchema: z.object({ + memoryId: z.string().describe("The ID of the memory to fetch"), + }), + execute: async ({ memoryId }) => { + try { + const response = await client.memories.get(memoryId) + + return { + success: true, + memory: response, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } + }, + }) + + return { + searchMemories, + addMemory, + fetchMemory, + } +} + +// Export individual tool creators for more flexibility +export const searchMemoriesTool = ( + apiKey: string, + config: SupermemoryToolsConfig, +) => { + const { searchMemories } = supermemoryTools(apiKey, config) + return searchMemories +} + +export const addMemoryTool = ( + apiKey: string, + config: SupermemoryToolsConfig, +) => { + const { addMemory } = supermemoryTools(apiKey, config) + return addMemory +} + +export const fetchMemoryTool = ( + apiKey: string, + config: SupermemoryToolsConfig, +) => { + const { fetchMemory } = supermemoryTools(apiKey, config) + return fetchMemory +} diff --git a/packages/ai-sdk/tsconfig.json b/packages/ai-sdk/tsconfig.json new file mode 100644 index 00000000..1bfa9593 --- /dev/null +++ b/packages/ai-sdk/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@total-typescript/tsconfig/bundler/dom/library-monorepo", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"], + "@lib/*": ["../lib/*"] + } + } +} diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md deleted file mode 100644 index 8b42d901..00000000 --- a/packages/eslint-config/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `@turbo/eslint-config` - -Collection of internal eslint configurations. diff --git a/packages/eslint-config/base.js b/packages/eslint-config/base.js deleted file mode 100644 index ca42eee9..00000000 --- a/packages/eslint-config/base.js +++ /dev/null @@ -1,32 +0,0 @@ -import js from "@eslint/js" -import eslintConfigPrettier from "eslint-config-prettier" -import onlyWarn from "eslint-plugin-only-warn" -import turboPlugin from "eslint-plugin-turbo" -import tseslint from "typescript-eslint" - -/** - * A shared ESLint configuration for the repository. - * - * @type {import("eslint").Linter.Config[]} - * */ -export const config = [ - js.configs.recommended, - eslintConfigPrettier, - ...tseslint.configs.recommended, - { - plugins: { - turbo: turboPlugin, - }, - rules: { - "turbo/no-undeclared-env-vars": "warn", - }, - }, - { - plugins: { - onlyWarn, - }, - }, - { - ignores: ["dist/**"], - }, -] diff --git a/packages/eslint-config/next.js b/packages/eslint-config/next.js deleted file mode 100644 index 74ef8ef3..00000000 --- a/packages/eslint-config/next.js +++ /dev/null @@ -1,49 +0,0 @@ -import js from "@eslint/js" -import pluginNext from "@next/eslint-plugin-next" -import eslintConfigPrettier from "eslint-config-prettier" -import pluginReact from "eslint-plugin-react" -import pluginReactHooks from "eslint-plugin-react-hooks" -import globals from "globals" -import tseslint from "typescript-eslint" -import { config as baseConfig } from "./base.js" - -/** - * A custom ESLint configuration for libraries that use Next.js. - * - * @type {import("eslint").Linter.Config[]} - * */ -export const nextJsConfig = [ - ...baseConfig, - js.configs.recommended, - eslintConfigPrettier, - ...tseslint.configs.recommended, - { - ...pluginReact.configs.flat.recommended, - languageOptions: { - ...pluginReact.configs.flat.recommended.languageOptions, - globals: { - ...globals.serviceworker, - }, - }, - }, - { - plugins: { - "@next/next": pluginNext, - }, - rules: { - ...pluginNext.configs.recommended.rules, - ...pluginNext.configs["core-web-vitals"].rules, - }, - }, - { - plugins: { - "react-hooks": pluginReactHooks, - }, - settings: { react: { version: "detect" } }, - rules: { - ...pluginReactHooks.configs.recommended.rules, - // React scope no longer necessary with new JSX transform. - "react/react-in-jsx-scope": "off", - }, - }, -] diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json deleted file mode 100644 index 9f420e9c..00000000 --- a/packages/eslint-config/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@repo/eslint-config", - "version": "0.0.0", - "type": "module", - "private": true, - "exports": { - "./base": "./base.js", - "./next-js": "./next.js", - "./react-internal": "./react-internal.js" - }, - "devDependencies": { - "@eslint/js": "^9.33.0", - "@next/eslint-plugin-next": "^15.4.2", - "eslint": "^9.33.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-only-warn": "^1.1.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-turbo": "^2.5.0", - "globals": "^16.3.0", - "typescript": "^5.9.2", - "typescript-eslint": "^8.39.0" - } -} diff --git a/packages/eslint-config/react-internal.js b/packages/eslint-config/react-internal.js deleted file mode 100644 index b179949e..00000000 --- a/packages/eslint-config/react-internal.js +++ /dev/null @@ -1,39 +0,0 @@ -import js from "@eslint/js" -import eslintConfigPrettier from "eslint-config-prettier" -import pluginReact from "eslint-plugin-react" -import pluginReactHooks from "eslint-plugin-react-hooks" -import globals from "globals" -import tseslint from "typescript-eslint" -import { config as baseConfig } from "./base.js" - -/** - * A custom ESLint configuration for libraries that use React. - * - * @type {import("eslint").Linter.Config[]} */ -export const config = [ - ...baseConfig, - js.configs.recommended, - eslintConfigPrettier, - ...tseslint.configs.recommended, - pluginReact.configs.flat.recommended, - { - languageOptions: { - ...pluginReact.configs.flat.recommended.languageOptions, - globals: { - ...globals.serviceworker, - ...globals.browser, - }, - }, - }, - { - plugins: { - "react-hooks": pluginReactHooks, - }, - settings: { react: { version: "detect" } }, - rules: { - ...pluginReactHooks.configs.recommended.rules, - // React scope no longer necessary with new JSX transform. - "react/react-in-jsx-scope": "off", - }, - }, -] diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json deleted file mode 100644 index 0756a8cd..00000000 --- a/packages/typescript-config/base.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "esModuleInterop": true, - "incremental": false, - "isolatedModules": true, - "lib": ["es2022", "DOM", "DOM.Iterable"], - "module": "NodeNext", - "moduleDetection": "force", - "moduleResolution": "NodeNext", - "noUncheckedIndexedAccess": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "target": "ES2022" - } -} diff --git a/packages/typescript-config/nextjs.json b/packages/typescript-config/nextjs.json deleted file mode 100644 index 20317a2a..00000000 --- a/packages/typescript-config/nextjs.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "plugins": [{ "name": "next" }], - "module": "ESNext", - "moduleResolution": "Bundler", - "allowJs": true, - "jsx": "preserve", - "noEmit": true - } -} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json deleted file mode 100644 index a9d4b226..00000000 --- a/packages/typescript-config/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@repo/typescript-config", - "version": "0.0.0", - "private": true, - "license": "MIT", - "publishConfig": { - "access": "public" - } -} diff --git a/packages/typescript-config/react-library.json b/packages/typescript-config/react-library.json deleted file mode 100644 index 44957d69..00000000 --- a/packages/typescript-config/react-library.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "jsx": "react-jsx" - } -}