mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-23 23:44:13 +00:00
feat(sdk-playground): reflect SDK-owned memory block in debug view
Update the playground to display the current deduplicated <supermemory> replacement block produced by the SDK middleware instead of a browser-side seen-facts delta. Add memory-dedupe helper and ignore local tsbuildinfo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
42f308b224
commit
ed15364eb3
4 changed files with 105 additions and 12 deletions
1
apps/sdk-playground/.gitignore
vendored
1
apps/sdk-playground/.gitignore
vendored
|
|
@ -3,3 +3,4 @@
|
|||
.env.local
|
||||
node_modules
|
||||
python/.venv
|
||||
*.tsbuildinfo
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ export default function AgentPlaygroundPage() {
|
|||
)
|
||||
const keysReady =
|
||||
supermemoryApiKey.trim().length > 0 && openaiApiKey.trim().length > 0
|
||||
const chatReady =
|
||||
keysReady || (hasSupermemoryKey && hasOpenAiKey)
|
||||
|
||||
const [sdkId, setSdkId] = useState("ts-ai-sdk-middleware")
|
||||
const [containerTag, setContainerTag] = useState("sdk-playground")
|
||||
|
|
@ -99,7 +101,7 @@ export default function AgentPlaygroundPage() {
|
|||
}, [refreshMeta])
|
||||
|
||||
const send = async () => {
|
||||
if (!input.trim() || loading || !selectedSdk?.available || !keysReady) return
|
||||
if (!input.trim() || loading || !selectedSdk?.available || !chatReady) return
|
||||
|
||||
const userMessage: UserOrAssistantMessage = {
|
||||
kind: "user",
|
||||
|
|
@ -484,9 +486,9 @@ export default function AgentPlaygroundPage() {
|
|||
send()
|
||||
}
|
||||
}}
|
||||
disabled={loading || !input.trim() || !selectedSdk?.available || !keysReady}
|
||||
disabled={loading || !selectedSdk?.available || !chatReady}
|
||||
placeholder={
|
||||
keysReady
|
||||
chatReady
|
||||
? "Message the agent…"
|
||||
: "Enter API keys above to chat…"
|
||||
}
|
||||
|
|
@ -495,7 +497,12 @@ export default function AgentPlaygroundPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={send}
|
||||
disabled={loading || !input.trim() || !selectedSdk?.available || !keysReady}
|
||||
disabled={
|
||||
loading ||
|
||||
!input.trim() ||
|
||||
!selectedSdk?.available ||
|
||||
!chatReady
|
||||
}
|
||||
className="rounded-lg bg-emerald-600 px-5 py-3 text-sm font-medium text-white hover:bg-emerald-500 disabled:opacity-40"
|
||||
>
|
||||
Send
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
type MiddlewareRuntimeConfig,
|
||||
normalizeMiddlewareConfig,
|
||||
} from "./middleware-config"
|
||||
import { dedupeProfileForMode } from "./memory-dedupe"
|
||||
|
||||
export interface MemoryDebugEntry {
|
||||
type: "profile_fetch" | "context_preview" | "conversation_saved" | "manual_profile"
|
||||
|
|
@ -143,11 +144,14 @@ export async function fetchContainerContext(
|
|||
| { static?: unknown[]; dynamic?: unknown[] }
|
||||
| undefined
|
||||
|
||||
const profile = {
|
||||
static: profileRaw?.static ?? [],
|
||||
dynamic: profileRaw?.dynamic ?? [],
|
||||
searchResults: normalizeSearchResults(profileResponse.searchResults),
|
||||
}
|
||||
const profile = dedupeProfileForMode(
|
||||
query ? "full" : "profile",
|
||||
{
|
||||
static: profileRaw?.static ?? [],
|
||||
dynamic: profileRaw?.dynamic ?? [],
|
||||
searchResults: normalizeSearchResults(profileResponse.searchResults),
|
||||
},
|
||||
)
|
||||
|
||||
const docsResponse = await client.post<{
|
||||
documents?: unknown[]
|
||||
|
|
@ -204,7 +208,8 @@ export async function buildMiddlewareMemoryDebug(
|
|||
query,
|
||||
supermemoryApiKey,
|
||||
)
|
||||
const summary = summarizeProfile(context.profile)
|
||||
const dedupedProfile = dedupeProfileForMode(memoryMode, context.profile)
|
||||
const summary = summarizeProfile(dedupedProfile)
|
||||
|
||||
const trace: MemoryDebugEntry[] = [
|
||||
{
|
||||
|
|
@ -229,8 +234,14 @@ export async function buildMiddlewareMemoryDebug(
|
|||
},
|
||||
{
|
||||
type: "context_preview",
|
||||
label: "Context injected into prompt",
|
||||
preview: buildContextPreview(context.profile, memoryMode, query),
|
||||
label: "Current SDK memory block (replaces the prior block)",
|
||||
preview: buildContextPreview(dedupedProfile, memoryMode, query),
|
||||
detail: {
|
||||
totalFacts:
|
||||
summary.staticCount +
|
||||
summary.dynamicCount +
|
||||
summary.searchResultCount,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "conversation_saved",
|
||||
|
|
|
|||
74
apps/sdk-playground/src/lib/memory-dedupe.ts
Normal file
74
apps/sdk-playground/src/lib/memory-dedupe.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import type { ContainerContext } from "./context-api"
|
||||
|
||||
type ProfileSlice = ContainerContext["profile"]
|
||||
|
||||
/** Normalize a fact for exact comparison within retrieved context. */
|
||||
export function normalizeFactKey(text: string): string {
|
||||
return text
|
||||
.replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, " ")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return JSON.stringify(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
staticOut.push(item)
|
||||
}
|
||||
|
||||
for (const item of dynamicItems) {
|
||||
const text = memoryText(item).trim()
|
||||
if (!text) continue
|
||||
const key = normalizeFactKey(text)
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
dynamicOut.push(item)
|
||||
}
|
||||
|
||||
for (const item of searchItems) {
|
||||
const text = memoryText(item).trim()
|
||||
if (!text) continue
|
||||
const key = normalizeFactKey(text)
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
searchOut.push(item)
|
||||
}
|
||||
|
||||
return {
|
||||
static: staticOut,
|
||||
dynamic: dynamicOut,
|
||||
searchResults: mode === "profile" ? [] : searchOut,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue