diff --git a/apps/sdk-playground/python/server.py b/apps/sdk-playground/python/server.py index caf81378f..842e3d7e7 100644 --- a/apps/sdk-playground/python/server.py +++ b/apps/sdk-playground/python/server.py @@ -343,6 +343,8 @@ async def fetch_profile_context( container_tag: str, sm_key: str, query: Optional[str] = None, + *, + include: Optional[list[str]] = None, ) -> dict[str, list[Any]]: from supermemory import AsyncSupermemory @@ -351,10 +353,12 @@ async def fetch_profile_context( base_url=supermemory_base_url(), timeout=HTTP_TIMEOUT_SECONDS, ) - profile_response = await client.profile( - container_tag=container_tag, - **({"q": query} if query else {}), - ) + request: dict[str, Any] = {"container_tag": container_tag} + if query: + request["q"] = query + if include is not None: + request["include"] = include + profile_response = await client.profile(**request) return extract_profile_context(profile_response) @@ -530,6 +534,49 @@ async def fetch_container_context( } +def reconstruct_python_sdk_memory_block( + memory_mode: str, + profile: dict[str, Any], +) -> tuple[dict[str, list[str]], str]: + from supermemory_openai import convert_profile_to_markdown, deduplicate_memories + from supermemory_openai.utils import wrap_memory_context + + deduplicated = deduplicate_memories( + static=profile.get("static", []) if memory_mode != "query" else [], + dynamic=profile.get("dynamic", []) if memory_mode != "query" else [], + search_results=profile.get("searchResults", []), + ) + visible_profile = { + "static": deduplicated.static, + "dynamic": deduplicated.dynamic, + "searchResults": ( + [] if memory_mode == "profile" else deduplicated.search_results + ), + } + + profile_data = "" + if memory_mode != "query": + profile_data = convert_profile_to_markdown( + { + "profile": { + "static": visible_profile["static"], + "dynamic": visible_profile["dynamic"], + }, + "searchResults": {"results": []}, + } + ) + + search_results_memories = "" + if memory_mode != "profile" and visible_profile["searchResults"]: + search_results_memories = ( + "Search results for user's recent message: \n" + + "\n".join(f"- {memory}" for memory in visible_profile["searchResults"]) + ) + + memories = f"{profile_data}\n{search_results_memories}".strip() + return visible_profile, wrap_memory_context(memories) + + def build_middleware_memory_debug( container_tag: str, conversation_id: str, @@ -549,39 +596,20 @@ def build_middleware_memory_debug( } ) else: - profile = context["profile"] - preview_lines = [ - f"[memory mode: {memory_mode}]", - "[post-response snapshot; not the exact middleware prompt]", - ] - if context.get("query"): - preview_lines.append(f"[query: {context['query']}]") - - selected_sections: list[tuple[str, list[Any]]] = [] - if memory_mode in ("profile", "full"): - selected_sections.extend( - ( - ("Static", profile.get("static", [])), - ("Dynamic", profile.get("dynamic", [])), - ) - ) - if memory_mode in ("query", "full"): - selected_sections.append( - ("Search results", profile.get("searchResults", [])) - ) - - for label, items in selected_sections: - if items: - preview_lines.append(f"{label}:") - for item in items[:8]: - preview_lines.append(f"- {display_context_item(item)}") + raw_profile = context["profile"] + profile, memory_block = reconstruct_python_sdk_memory_block( + memory_mode, + raw_profile, + ) debug.extend( ( { "type": "profile_fetch", - "label": "Post-response profile snapshot", + "label": "Post-response context reconstruction", "detail": { + "authoritativeMiddlewareCapture": False, + "timing": "after model response", "endpoint": "POST /v4/profile", "containerTag": container_tag, "customId": conversation_id, @@ -594,8 +622,19 @@ def build_middleware_memory_debug( }, { "type": "context_preview", - "label": "Post-response context preview", - "preview": "\n".join(preview_lines), + "label": ( + "Reconstructed SDK-owned memory block " + "(not middleware capture)" + ), + "preview": memory_block, + "detail": { + "totalFacts": ( + len(profile.get("static", [])) + + len(profile.get("dynamic", [])) + + len(profile.get("searchResults", [])) + ), + "fullLength": len(memory_block), + }, }, ) ) @@ -632,7 +671,12 @@ async def fetch_context_for_debug( ) -> tuple[Optional[dict[str, Any]], Optional[str]]: try: async with asyncio.timeout(CONTEXT_DEBUG_TIMEOUT_SECONDS): - profile = await fetch_profile_context(container_tag, sm_key, query) + profile = await fetch_profile_context( + container_tag, + sm_key, + query, + include=["static", "dynamic"], + ) return ( { "containerTag": container_tag, diff --git a/apps/sdk-playground/src/lib/chat-handlers.ts b/apps/sdk-playground/src/lib/chat-handlers.ts index 824d3d88a..60fffdd19 100644 --- a/apps/sdk-playground/src/lib/chat-handlers.ts +++ b/apps/sdk-playground/src/lib/chat-handlers.ts @@ -424,10 +424,11 @@ export async function runTypeScriptChat( middlewareConfig, request.sdkId === "ts-ai-sdk-middleware" ? { + flavor: "ai-sdk", includeToolCalls: middlewareConfig.includeToolCalls, skipMemoryOnError: middlewareConfig.skipMemoryOnError, } - : undefined, + : { flavor: "openai" }, keys.supermemoryApiKey, signal, ), diff --git a/apps/sdk-playground/src/lib/context-api.ts b/apps/sdk-playground/src/lib/context-api.ts index 88cf1a50a..9c201a113 100644 --- a/apps/sdk-playground/src/lib/context-api.ts +++ b/apps/sdk-playground/src/lib/context-api.ts @@ -3,6 +3,11 @@ import { type MiddlewareRuntimeConfig, normalizeMiddlewareConfig, } from "./middleware-config" +import { + type MemoryMode, + type MiddlewareFlavor, + reconstructSdkMemoryBlock, +} from "./memory-dedupe" export interface MemoryDebugEntry { type: @@ -91,45 +96,6 @@ function summarizeProfile(profile: ContainerContext["profile"]) { } } -function selectProfileForMode( - profile: ContainerContext["profile"], - mode: "profile" | "query" | "full", -): ContainerContext["profile"] { - return { - static: mode === "query" ? [] : profile.static, - dynamic: mode === "query" ? [] : profile.dynamic, - searchResults: mode === "profile" ? [] : profile.searchResults, - } -} - -function buildContextPreview( - profile: ContainerContext["profile"], - mode: "profile" | "query" | "full", - query?: string, -): string { - const lines: string[] = [`[memory mode: ${mode}]`] - if (query) lines.push(`[query: ${query}]`) - if (mode !== "query" && profile.static.length) { - lines.push("Static:") - for (const item of profile.static.slice(0, 8)) { - lines.push(`- ${memoryText(item)}`) - } - } - if (mode !== "query" && profile.dynamic.length) { - lines.push("Dynamic:") - for (const item of profile.dynamic.slice(0, 8)) { - lines.push(`- ${memoryText(item)}`) - } - } - if (mode !== "profile" && profile.searchResults.length) { - lines.push("Search results:") - for (const item of profile.searchResults.slice(0, 8)) { - lines.push(`- ${memoryText(item)}`) - } - } - return lines.join("\n") -} - function normalizeSearchResults(searchResults: unknown): unknown[] { if (!searchResults) return [] if (Array.isArray(searchResults)) return searchResults @@ -154,16 +120,18 @@ async function fetchProfileContext( query?: string, signal?: AbortSignal, ): Promise { - const profileResponse = await client.profile( - { + const profileResponse = await client.post<{ + profile?: { static?: unknown[]; dynamic?: unknown[] } + searchResults?: unknown + }>("/v4/profile", { + body: { containerTag, + include: ["static", "dynamic"], ...(query ? { q: query } : {}), }, - { signal }, - ) - const profileRaw = profileResponse.profile as - | { static?: unknown[]; dynamic?: unknown[] } - | undefined + ...(signal ? { signal } : {}), + }) + const profileRaw = profileResponse.profile return { static: profileRaw?.static ?? [], @@ -223,10 +191,11 @@ export async function fetchContainerContext( export async function buildMiddlewareMemoryDebug( containerTag: string, conversationId: string, - memoryMode: "profile" | "query" | "full", + memoryMode: MemoryMode, lastUserMessage: string, - middlewareConfig?: Partial, - aiSdkExtras?: { + middlewareConfig: Partial | undefined, + sdk: { + flavor: MiddlewareFlavor includeToolCalls?: boolean skipMemoryOnError?: boolean }, @@ -246,7 +215,12 @@ export async function buildMiddlewareMemoryDebug( query, signal, ) - const selectedProfile = selectProfileForMode(profile, memoryMode) + const reconstructed = reconstructSdkMemoryBlock( + memoryMode, + profile, + sdk.flavor, + ) + const selectedProfile = reconstructed.profile const summary = summarizeProfile(selectedProfile) return [ @@ -262,11 +236,11 @@ export async function buildMiddlewareMemoryDebug( memoryMode, addMemory: config.addMemory, verbose: config.verbose, - ...(aiSdkExtras?.includeToolCalls !== undefined - ? { includeToolCalls: aiSdkExtras.includeToolCalls } + ...(sdk.includeToolCalls !== undefined + ? { includeToolCalls: sdk.includeToolCalls } : {}), - ...(aiSdkExtras?.skipMemoryOnError !== undefined - ? { skipMemoryOnError: aiSdkExtras.skipMemoryOnError } + ...(sdk.skipMemoryOnError !== undefined + ? { skipMemoryOnError: sdk.skipMemoryOnError } : {}), query: query ?? null, ...summary, @@ -274,8 +248,15 @@ export async function buildMiddlewareMemoryDebug( }, { type: "context_preview", - label: "Reconstructed context preview (not middleware capture)", - preview: buildContextPreview(selectedProfile, memoryMode, query), + label: "Reconstructed SDK-owned memory block (not middleware capture)", + preview: reconstructed.block, + detail: { + totalFacts: + summary.staticCount + + summary.dynamicCount + + summary.searchResultCount, + fullLength: reconstructed.block.length, + }, }, config.addMemory === "always" ? { @@ -287,8 +268,8 @@ export async function buildMiddlewareMemoryDebug( customId: conversationId, addMemory: config.addMemory, verbose: config.verbose, - ...(aiSdkExtras?.includeToolCalls !== undefined - ? { includeToolCalls: aiSdkExtras.includeToolCalls } + ...(sdk.includeToolCalls !== undefined + ? { includeToolCalls: sdk.includeToolCalls } : {}), }, } diff --git a/apps/sdk-playground/src/lib/memory-dedupe.ts b/apps/sdk-playground/src/lib/memory-dedupe.ts new file mode 100644 index 000000000..0a641291f --- /dev/null +++ b/apps/sdk-playground/src/lib/memory-dedupe.ts @@ -0,0 +1,75 @@ +import { + deduplicateMemoriesForMode, + type ProfileWithMemories, +} from "../../../../packages/tools/src/tools-shared" +import { wrapMemoryContext } from "../../../../packages/tools/src/shared/memory-context" +import { + convertProfileToMarkdown, + defaultPromptTemplate, +} from "../../../../packages/tools/src/shared/prompt-builder" + +export type MemoryMode = "profile" | "query" | "full" +export type MiddlewareFlavor = "ai-sdk" | "openai" + +export interface MemoryProfileSlice { + static: unknown[] + dynamic: unknown[] + searchResults: unknown[] +} + +export interface ReconstructedMemoryBlock { + profile: { + static: string[] + dynamic: string[] + searchResults: string[] + } + block: string +} + +/** Reconstruct the exact SDK-owned block from a post-response profile snapshot. */ +export function reconstructSdkMemoryBlock( + mode: MemoryMode, + profile: MemoryProfileSlice, + flavor: MiddlewareFlavor, +): ReconstructedMemoryBlock { + const deduplicated = deduplicateMemoriesForMode( + mode, + profile as ProfileWithMemories, + ) + const visibleProfile = { + static: deduplicated.static, + dynamic: deduplicated.dynamic, + searchResults: mode === "profile" ? [] : deduplicated.searchResults, + } + + const userMemories = + mode === "query" + ? "" + : convertProfileToMarkdown({ + profile: { + static: visibleProfile.static, + dynamic: visibleProfile.dynamic, + }, + searchResults: { results: [] }, + }) + const generalSearchMemories = + mode !== "profile" && visibleProfile.searchResults.length > 0 + ? `Search results for user's recent message: \n${visibleProfile.searchResults + .map((memory) => `- ${memory}`) + .join("\n")}` + : "" + + const memories = + flavor === "ai-sdk" + ? defaultPromptTemplate({ + userMemories, + generalSearchMemories, + searchResults: [], + }) + : `${userMemories}\n${generalSearchMemories}`.trim() + + return { + profile: visibleProfile, + block: wrapMemoryContext(memories), + } +}