fix(sdk-playground): mirror SDK memory reconstruction

This commit is contained in:
ved015 2026-08-24 23:38:49 +05:30
parent 0fe2dabcee
commit 5d1f557b67
4 changed files with 175 additions and 149 deletions

View file

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

View file

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

View file

@ -3,7 +3,11 @@ import {
type MiddlewareRuntimeConfig,
normalizeMiddlewareConfig,
} from "./middleware-config"
import { dedupeProfileForMode } from "./memory-dedupe"
import {
type MemoryMode,
type MiddlewareFlavor,
reconstructSdkMemoryBlock,
} from "./memory-dedupe"
export interface MemoryDebugEntry {
type:
@ -92,34 +96,6 @@ function summarizeProfile(profile: ContainerContext["profile"]) {
}
}
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
@ -144,16 +120,18 @@ async function fetchProfileContext(
query?: string,
signal?: AbortSignal,
): Promise<ContainerContext["profile"]> {
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 ?? [],
@ -172,10 +150,7 @@ export async function fetchContainerContext(
if (!apiKey) throw new Error("Supermemory API key is required")
const client = getSupermemoryClient(apiKey)
const profile = dedupeProfileForMode(
query ? "full" : "profile",
await fetchProfileContext(client, containerTag, query),
)
const profile = await fetchProfileContext(client, containerTag, query)
const docsResponse = await client.post<{
documents?: unknown[]
@ -216,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<MiddlewareRuntimeConfig>,
aiSdkExtras?: {
middlewareConfig: Partial<MiddlewareRuntimeConfig> | undefined,
sdk: {
flavor: MiddlewareFlavor
includeToolCalls?: boolean
skipMemoryOnError?: boolean
},
@ -239,7 +215,12 @@ export async function buildMiddlewareMemoryDebug(
query,
signal,
)
const selectedProfile = dedupeProfileForMode(memoryMode, profile)
const reconstructed = reconstructSdkMemoryBlock(
memoryMode,
profile,
sdk.flavor,
)
const selectedProfile = reconstructed.profile
const summary = summarizeProfile(selectedProfile)
return [
@ -255,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,
@ -267,13 +248,14 @@ 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"
@ -286,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 }
: {}),
},
}

View file

@ -1,76 +1,75 @@
import type { ContainerContext } from "./context-api"
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"
type ProfileSlice = ContainerContext["profile"]
export type MemoryMode = "profile" | "query" | "full"
export type MiddlewareFlavor = "ai-sdk" | "openai"
/** Normalize a fact for exact comparison within retrieved context. */
export function normalizeFactKey(text: string): string {
return text
.trim()
.replace(/^\[recent\]\s*/i, "")
.replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, "")
.trim()
.replace(/\s+/g, " ")
.toLowerCase()
export interface MemoryProfileSlice {
static: unknown[]
dynamic: unknown[]
searchResults: unknown[]
}
function memoryText(item: unknown): string {
if (typeof item === "string") return item
if (item && typeof item === "object") {
const record = item as Record<string, unknown>
if (typeof record.memory === "string") return record.memory
if (typeof record.content === "string") return record.content
if (typeof record.chunk === "string") return record.chunk
export interface ReconstructedMemoryBlock {
profile: {
static: string[]
dynamic: string[]
searchResults: string[]
}
return ""
block: string
}
/**
* Deduplicate static dynamic search (same priority as @supermemory/tools middleware).
*/
export function dedupeProfileForMode(
mode: "profile" | "query" | "full",
profile: ProfileSlice,
): ProfileSlice {
const injectsProfile = mode !== "query"
const staticItems = injectsProfile ? profile.static : []
const dynamicItems = injectsProfile ? profile.dynamic : []
const searchItems = profile.searchResults
const seen = new Set<string>()
const staticOut: unknown[] = []
const dynamicOut: unknown[] = []
const searchOut: unknown[] = []
for (const item of staticItems) {
const text = memoryText(item).trim()
if (!text) continue
const key = normalizeFactKey(text)
if (!key || seen.has(key)) continue
seen.add(key)
staticOut.push(item)
/** 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,
}
for (const item of dynamicItems) {
const text = memoryText(item).trim()
if (!text) continue
const key = normalizeFactKey(text)
if (!key || seen.has(key)) continue
seen.add(key)
dynamicOut.push(item)
}
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")}`
: ""
for (const item of searchItems) {
const text = memoryText(item).trim()
if (!text) continue
const key = normalizeFactKey(text)
if (!key || seen.has(key)) continue
seen.add(key)
searchOut.push(item)
}
const memories =
flavor === "ai-sdk"
? defaultPromptTemplate({
userMemories,
generalSearchMemories,
searchResults: [],
})
: `${userMemories}\n${generalSearchMemories}`.trim()
return {
static: staticOut,
dynamic: dynamicOut,
searchResults: mode === "profile" ? [] : searchOut,
profile: visibleProfile,
block: wrapMemoryContext(memories),
}
}