bug fixes

This commit is contained in:
Sreeram Sreedhar 2026-04-19 10:31:05 -07:00
parent e8252b1048
commit 430410f65f
5 changed files with 165 additions and 32 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<typeof createMockProfileResponse>
memoriesSearchResponse?: ReturnType<typeof createMockMemoriesSearchResponse>
documentsSearchResponse?: ReturnType<typeof createMockDocumentsSearchResponse>
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({}),
})
}
}