mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
fix(sdk-playground): mirror SDK memory reconstruction
This commit is contained in:
parent
0fe2dabcee
commit
5d1f557b67
4 changed files with 175 additions and 149 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue