diff --git a/packages/openai-sdk-python/src/supermemory_openai/__init__.py b/packages/openai-sdk-python/src/supermemory_openai/__init__.py index 3b42d4f9..1f7a98b9 100644 --- a/packages/openai-sdk-python/src/supermemory_openai/__init__.py +++ b/packages/openai-sdk-python/src/supermemory_openai/__init__.py @@ -22,6 +22,9 @@ from .middleware import ( SupermemoryOpenAIWrapper, ) +# Backward compatibility alias for renamed class +OpenAIMiddlewareOptions = SupermemoryOpenAIOptions + from .utils import ( Logger, create_logger, @@ -59,6 +62,7 @@ __all__ = [ # Middleware "with_supermemory", "SupermemoryOpenAIOptions", + "OpenAIMiddlewareOptions", # Backward compatibility alias "SupermemoryOpenAIWrapper", # Utils "Logger", diff --git a/packages/openai-sdk-python/test_integration.py b/packages/openai-sdk-python/test_integration.py index 9bf5880c..ac0a8420 100644 --- a/packages/openai-sdk-python/test_integration.py +++ b/packages/openai-sdk-python/test_integration.py @@ -11,7 +11,7 @@ import os from openai import AsyncOpenAI, OpenAI from supermemory_openai import ( with_supermemory, - OpenAIMiddlewareOptions, + SupermemoryOpenAIOptions, SupermemoryConfigurationError, SupermemoryAPIError, ) @@ -37,8 +37,9 @@ async def test_async_middleware(): # Wrap with Supermemory middleware openai_with_memory = with_supermemory( openai_client, - container_tag="test-user-123", - options=OpenAIMiddlewareOptions( + SupermemoryOpenAIOptions( + container_tag="test-user-123", + custom_id="test-conversation-async", mode="profile", verbose=True, add_memory="never" # Don't save test messages @@ -87,8 +88,9 @@ def test_sync_middleware(): # Wrap with Supermemory middleware openai_with_memory = with_supermemory( openai_client, - container_tag="test-user-sync-123", - options=OpenAIMiddlewareOptions( + SupermemoryOpenAIOptions( + container_tag="test-user-sync-123", + custom_id="test-conversation-sync", mode="profile", verbose=True ) @@ -122,17 +124,35 @@ def test_error_handling(): print("\nšŸ”„ Testing Error Handling...") try: - # Test with missing API key + # Test with missing API key (clear env var temporarily) + original_key = os.environ.pop("SUPERMEMORY_API_KEY", None) + openai_client = OpenAI(api_key="fake-key") # This should raise SupermemoryConfigurationError - with_supermemory(openai_client, "test-user") + with_supermemory( + openai_client, + SupermemoryOpenAIOptions( + container_tag="test-user", + custom_id="test-conv" + ) + ) + + # Restore key if it existed + if original_key: + os.environ["SUPERMEMORY_API_KEY"] = original_key print("āŒ Should have raised SupermemoryConfigurationError") except SupermemoryConfigurationError as e: + # Restore key if it existed + if original_key: + os.environ["SUPERMEMORY_API_KEY"] = original_key print(f"āœ… Correctly caught configuration error: {e}") except Exception as e: + # Restore key if it existed + if original_key: + os.environ["SUPERMEMORY_API_KEY"] = original_key print(f"āŒ Wrong exception type: {type(e).__name__}: {e}") @@ -156,8 +176,9 @@ def test_background_tasks(): # Wrap with memory storage enabled wrapped_client = with_supermemory( openai_client, - container_tag="test-background-tasks", - options=OpenAIMiddlewareOptions( + SupermemoryOpenAIOptions( + container_tag="test-background-tasks", + custom_id="test-bg-tasks-conv", add_memory="always", verbose=True ) diff --git a/packages/tools/src/shared/memory-client.ts b/packages/tools/src/shared/memory-client.ts index 97bbfa5e..05f6d0a8 100644 --- a/packages/tools/src/shared/memory-client.ts +++ b/packages/tools/src/shared/memory-client.ts @@ -241,11 +241,13 @@ export const buildMemoriesText = async ( } = options // Fetch profile data when mode includes profile (profile or full) + // Note: We never send queryText here - profile endpoint is only for static/dynamic memories. + // Query-based search is handled separately by the SDK search functions (performSearch). let profileData: ProfileStructure | null = null if (mode !== "query") { profileData = await supermemoryProfileSearch( containerTag, - mode === "profile" ? "" : queryText, // Only send query for full mode + "", // No query - profile is for static/dynamic only baseUrl, apiKey, ) diff --git a/packages/tools/test/openai/unit.test.ts b/packages/tools/test/openai/unit.test.ts index a8735578..57d657e1 100644 --- a/packages/tools/test/openai/unit.test.ts +++ b/packages/tools/test/openai/unit.test.ts @@ -4,7 +4,11 @@ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest" import { withSupermemory } from "../../src/openai" -import { createMockProfileResponse } from "../utils/supermemory-mocks" +import { + createMockProfileResponse, + createMockMemoriesSearchResponse, + createRoutedFetchMock, +} from "../utils/supermemory-mocks" // Create a mock OpenAI client const createMockOpenAIClient = () => { @@ -229,13 +233,14 @@ describe("Unit: OpenAI withSupermemory", () => { }) it("should search memories based on user message in query mode", async () => { - fetchMock.mockResolvedValue({ - ok: true, - json: () => - Promise.resolve( - createMockProfileResponse([], [], ["TypeScript is their favorite"]), - ), + // Use routed fetch mock to handle SDK search calls + const routedFetch = createRoutedFetchMock({ + memoriesSearchResponse: createMockMemoriesSearchResponse([ + "TypeScript is their favorite", + ]), }) + fetchMock = vi.fn(routedFetch) + globalThis.fetch = fetchMock as unknown as typeof fetch const mockClient = createMockOpenAIClient() const originalCreate = mockClient._mockCreate @@ -253,12 +258,14 @@ describe("Unit: OpenAI withSupermemory", () => { ], }) - // Verify fetch was called with query text + // Verify fetch was called (SDK search) expect(fetchMock.mock.calls.length).toBeGreaterThan(0) - const fetchCall = fetchMock.mock.calls[0] - const fetchBody = JSON.parse(fetchCall?.[1]?.body) - expect(fetchBody.q).toBe("What's my favorite programming language?") - expect(fetchBody.containerTag).toBe("user-123") + + // Find the /v4/search call + const searchCall = fetchMock.mock.calls.find((call: unknown[]) => + (call[0] as string)?.toString().includes("/v4/search"), + ) + expect(searchCall).toBeDefined() const calledMessages = originalCreate.mock.calls[0][0].messages expect(calledMessages[0].content).toContain( @@ -267,17 +274,18 @@ describe("Unit: OpenAI withSupermemory", () => { }) it("should combine profile and search in full mode", async () => { - fetchMock.mockResolvedValue({ - ok: true, - json: () => - Promise.resolve( - createMockProfileResponse( - ["Name: Alice"], - ["Likes coffee"], - ["Recently discussed Python"], - ), - ), + // Use routed fetch mock to handle both profile and SDK search calls + const routedFetch = createRoutedFetchMock({ + profileResponse: createMockProfileResponse( + ["Name: Alice"], + ["Likes coffee"], + ), + memoriesSearchResponse: createMockMemoriesSearchResponse([ + "Recently discussed Python", + ]), }) + fetchMock = vi.fn(routedFetch) + globalThis.fetch = fetchMock as unknown as typeof fetch const mockClient = createMockOpenAIClient() const originalCreate = mockClient._mockCreate @@ -285,6 +293,7 @@ describe("Unit: OpenAI withSupermemory", () => { containerTag: "user-123", customId: "conv-456", mode: "full", + addMemory: "never", }) await wrapped.chat.completions.create({ diff --git a/packages/tools/test/utils/supermemory-mocks.ts b/packages/tools/test/utils/supermemory-mocks.ts index f8eb31d9..f1b57c9e 100644 --- a/packages/tools/test/utils/supermemory-mocks.ts +++ b/packages/tools/test/utils/supermemory-mocks.ts @@ -15,3 +15,100 @@ export const createMockProfileResponse = ( results: searchResults.map((memory) => ({ memory })), }, }) + +/** + * Creates a mock response for the SDK search.memories() endpoint (/v4/search) + */ +export const createMockMemoriesSearchResponse = (memories: string[] = []) => ({ + results: memories.map((memory) => ({ memory })), +}) + +/** + * Creates a mock response for the SDK search.documents() endpoint (/v3/search) + */ +export const createMockDocumentsSearchResponse = ( + documents: Array<{ content: string; isRelevant?: boolean }> = [], +) => ({ + results: documents.map((doc, index) => ({ + id: `doc-${index}`, + chunks: [ + { + content: doc.content, + isRelevant: doc.isRelevant ?? true, + }, + ], + })), +}) + +/** + * Creates a mock fetch implementation that routes to different responses + * based on the URL endpoint. + */ +export const createRoutedFetchMock = (options: { + profileResponse?: ReturnType + memoriesSearchResponse?: ReturnType + documentsSearchResponse?: ReturnType + conversationResponse?: { id: string; conversationId: string; status: string } +}) => { + return (url: string | URL | Request, _init?: RequestInit) => { + const urlStr = typeof url === "string" ? url : url.toString() + + // Route based on endpoint + if (urlStr.includes("/v4/profile")) { + return Promise.resolve({ + ok: true, + headers: new Headers({ "content-type": "application/json" }), + json: () => + Promise.resolve( + options.profileResponse ?? createMockProfileResponse(), + ), + }) + } + + if (urlStr.includes("/v4/search")) { + return Promise.resolve({ + ok: true, + headers: new Headers({ "content-type": "application/json" }), + json: () => + Promise.resolve( + options.memoriesSearchResponse ?? + createMockMemoriesSearchResponse(), + ), + }) + } + + if (urlStr.includes("/v3/search")) { + return Promise.resolve({ + ok: true, + headers: new Headers({ "content-type": "application/json" }), + json: () => + Promise.resolve( + options.documentsSearchResponse ?? + createMockDocumentsSearchResponse(), + ), + }) + } + + if (urlStr.includes("/v4/conversations")) { + return Promise.resolve({ + ok: true, + headers: new Headers({ "content-type": "application/json" }), + json: () => + Promise.resolve( + options.conversationResponse ?? { + id: "mem-123", + conversationId: "conv-456", + status: "success", + }, + ), + }) + } + + // Default fallback + return Promise.resolve({ + ok: true, + headers: new Headers({ "content-type": "application/json" }), + json: () => Promise.resolve({}), + }) + } +}