mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
refactor(gui): Unify prompt analysis components for actor, intent decoder and Handoff
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { PromptAnalyzer } from "@/components/play/PromptAnalyzer";
|
||||
|
||||
interface HandoffModalProps {
|
||||
entry: SimSnapshot["log"][number];
|
||||
@@ -191,25 +192,40 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) {
|
||||
)}
|
||||
|
||||
{activeTab === "prompt" && entry.rawPrompt && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono mb-2">
|
||||
System Prompt
|
||||
</h4>
|
||||
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[250px] overflow-y-auto">
|
||||
{entry.rawPrompt.systemPrompt}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono mb-2">
|
||||
User Context (Candidates)
|
||||
</h4>
|
||||
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[350px] overflow-y-auto font-sans leading-relaxed font-mono">
|
||||
{entry.rawPrompt.userContext}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<PromptAnalyzer
|
||||
components={
|
||||
entry.rawPrompt.components &&
|
||||
entry.rawPrompt.components.length > 0
|
||||
? entry.rawPrompt.components
|
||||
: [
|
||||
{
|
||||
label: "System Prompt",
|
||||
type: "system",
|
||||
content: entry.rawPrompt.systemPrompt || "",
|
||||
},
|
||||
{
|
||||
label: "User Context",
|
||||
type: "world",
|
||||
content: entry.rawPrompt.userContext || "",
|
||||
},
|
||||
]
|
||||
}
|
||||
inputTokens={entry.usage?.inputTokens || 0}
|
||||
maxContext={
|
||||
entry.usage?.maxContext !== undefined
|
||||
? entry.usage.maxContext
|
||||
: 32768
|
||||
}
|
||||
modelName={entry.usage?.modelName}
|
||||
providerInstanceName={entry.usage?.providerInstanceName}
|
||||
outputLabel="LLM Output (Promoted Memory Chunks)"
|
||||
outputText={
|
||||
handoffResult
|
||||
? JSON.stringify(handoffResult, null, 2)
|
||||
: undefined
|
||||
}
|
||||
outputTokens={entry.usage?.outputTokens}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "output" && (
|
||||
|
||||
172
apps/gui/src/components/play/PromptAnalyzer.tsx
Normal file
172
apps/gui/src/components/play/PromptAnalyzer.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import type { PromptComponent } from "@/lib/simulation-types";
|
||||
|
||||
interface PromptAnalyzerProps {
|
||||
components: PromptComponent[];
|
||||
inputTokens: number;
|
||||
maxContext?: number;
|
||||
modelName?: string;
|
||||
providerInstanceName?: string;
|
||||
outputLabel?: string;
|
||||
outputText?: string;
|
||||
outputTokens?: number;
|
||||
}
|
||||
|
||||
export function PromptAnalyzer({
|
||||
components,
|
||||
inputTokens,
|
||||
maxContext = 32768,
|
||||
modelName,
|
||||
providerInstanceName,
|
||||
outputLabel = "LLM Output",
|
||||
outputText,
|
||||
outputTokens,
|
||||
}: PromptAnalyzerProps) {
|
||||
const totalLen = components.reduce((sum, s) => sum + s.content.length, 0);
|
||||
|
||||
if (totalLen === 0) {
|
||||
return (
|
||||
<div className="text-sm italic text-muted-foreground">
|
||||
No prompt context recorded.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sections = components.map((s) => {
|
||||
const pct = totalLen > 0 ? (s.content.length / totalLen) * 100 : 0;
|
||||
return {
|
||||
...s,
|
||||
pct,
|
||||
tokens: Math.round((s.content.length / totalLen) * inputTokens),
|
||||
};
|
||||
});
|
||||
|
||||
const usagePctOfContext =
|
||||
maxContext > 0 ? (inputTokens / maxContext) * 100 : 0;
|
||||
const isAbsolute = maxContext > 0 && usagePctOfContext >= 20;
|
||||
|
||||
const getColorClass = (type: string) => {
|
||||
switch (type) {
|
||||
case "system":
|
||||
return "bg-blue-500";
|
||||
case "world":
|
||||
return "bg-emerald-500";
|
||||
case "events":
|
||||
return "bg-purple-500";
|
||||
case "memories":
|
||||
return "bg-pink-500";
|
||||
case "input":
|
||||
return "bg-amber-500";
|
||||
default:
|
||||
return "bg-slate-500";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Provider Details */}
|
||||
{(providerInstanceName || modelName) && (
|
||||
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
|
||||
<strong>LLM Instance:</strong>{" "}
|
||||
<span>{providerInstanceName || "Default"}</span>
|
||||
{modelName && <span> ({modelName})</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Bar & Breakdown */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
|
||||
<span className="font-semibold">Input Prompt Breakdown</span>
|
||||
<span>
|
||||
Total Input Tokens: <strong>{inputTokens}</strong>
|
||||
{maxContext > 0 ? (
|
||||
<span>
|
||||
{" "}
|
||||
/ {maxContext} ({usagePctOfContext.toFixed(1)}% used)
|
||||
</span>
|
||||
) : (
|
||||
<span> (infinite context)</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Token Bar */}
|
||||
<div className="flex h-6 w-full rounded border overflow-hidden bg-muted shadow-inner mb-2">
|
||||
{sections.map((item, idx) => {
|
||||
const widthPct = isAbsolute
|
||||
? item.pct * (inputTokens / maxContext)
|
||||
: item.pct;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`h-full transition-all duration-300 ${getColorClass(item.type)}`}
|
||||
style={{ width: `${widthPct}%` }}
|
||||
title={`${item.label}: ${item.tokens} tokens (${item.pct.toFixed(1)}%)`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isAbsolute && (
|
||||
<div
|
||||
className="bg-white h-full"
|
||||
style={{ width: `${100 - usagePctOfContext}%` }}
|
||||
title={`Available: ${maxContext - inputTokens} tokens (${(100 - usagePctOfContext).toFixed(1)}% remaining)`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Accordion Components */}
|
||||
<Accordion type="multiple" className="w-full">
|
||||
{sections.map((item, idx) => {
|
||||
return (
|
||||
<AccordionItem key={idx} value={String(idx)}>
|
||||
<AccordionTrigger className="text-sm py-2.5 hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block w-2.5 h-2.5 rounded-sm ${getColorClass(item.type)}`}
|
||||
/>
|
||||
<span>{item.label}:</span>
|
||||
<span className="text-muted-foreground font-normal">
|
||||
<strong>{item.tokens}</strong> tokens (
|
||||
{item.pct.toFixed(0)}%)
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<pre className="m-0 p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[300px] overflow-y-auto">
|
||||
{item.content}
|
||||
</pre>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
{/* Output Section */}
|
||||
{outputText && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2 font-mono">
|
||||
<span className="font-semibold">{outputLabel}</span>
|
||||
{outputTokens !== undefined && (
|
||||
<span>
|
||||
Total Output Tokens: <strong>{outputTokens}</strong>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded border-2">
|
||||
<pre className="m-0 p-3 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground max-h-[250px] overflow-y-auto">
|
||||
{outputText}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,13 +9,8 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from "@/components/ui/accordion";
|
||||
import { PromptSwitcher } from "@/components/play/PromptSwitcher";
|
||||
import { PromptAnalyzer } from "@/components/play/PromptAnalyzer";
|
||||
|
||||
interface PromptModalProps {
|
||||
entry: SimSnapshot["log"][number];
|
||||
@@ -25,191 +20,39 @@ interface PromptModalProps {
|
||||
export function PromptModal({ entry, onClose }: PromptModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<"actor" | "decoder">("actor");
|
||||
|
||||
const parseActorPrompt = (
|
||||
systemPrompt: string,
|
||||
userContext: string,
|
||||
inputTokens: number,
|
||||
sectionsObj?: {
|
||||
worldInfo: string;
|
||||
memoryLedger: string;
|
||||
cognitiveBuffer: string;
|
||||
},
|
||||
) => {
|
||||
let worldStr = "";
|
||||
let recentStr = "";
|
||||
let ledgerStr = "";
|
||||
|
||||
if (sectionsObj) {
|
||||
worldStr = sectionsObj.worldInfo;
|
||||
recentStr = sectionsObj.cognitiveBuffer;
|
||||
ledgerStr = sectionsObj.memoryLedger;
|
||||
} else {
|
||||
const recentHeader = "=== COGNITIVE BUFFER ===";
|
||||
const ledgerHeader = "=== MEMORY LEDGER ===";
|
||||
|
||||
const recentIdx = userContext.indexOf(recentHeader);
|
||||
worldStr = userContext;
|
||||
|
||||
if (recentIdx !== -1) {
|
||||
worldStr = userContext.substring(0, recentIdx).trim();
|
||||
const rest = userContext.substring(recentIdx).trim();
|
||||
const ledgerIdx = rest.indexOf(ledgerHeader);
|
||||
if (ledgerIdx !== -1) {
|
||||
recentStr = rest.substring(0, ledgerIdx).trim();
|
||||
ledgerStr = rest.substring(ledgerIdx).trim();
|
||||
} else {
|
||||
recentStr = rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sections: { label: string; type: string; content: string }[] = [
|
||||
{ label: "System Prompt", type: "system", content: systemPrompt },
|
||||
{ label: "World Info", type: "world", content: worldStr },
|
||||
{
|
||||
label: "Cognitive Buffer",
|
||||
type: "events",
|
||||
content: recentStr || "(No cognitive buffer entries.)",
|
||||
},
|
||||
{
|
||||
label: "Memory Ledger",
|
||||
type: "memories",
|
||||
content: ledgerStr || "(No memory ledger entries.)",
|
||||
},
|
||||
];
|
||||
|
||||
const totalLen = sections.reduce((sum, s) => sum + s.content.length, 0);
|
||||
if (totalLen === 0) return null;
|
||||
|
||||
return sections.map((s) => {
|
||||
const pct = (s.content.length / totalLen) * 100;
|
||||
return {
|
||||
...s,
|
||||
pct,
|
||||
relativePct: pct,
|
||||
tokens: Math.round((s.content.length / totalLen) * inputTokens),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const parseDecoderPrompt = (
|
||||
systemPrompt: string,
|
||||
userContext: string,
|
||||
inputTokens: number,
|
||||
) => {
|
||||
const proseHeader = "=== NARRATIVE PROSE ===";
|
||||
const idx = userContext.indexOf(proseHeader);
|
||||
|
||||
let worldStr = userContext;
|
||||
let proseStr = "";
|
||||
|
||||
if (idx !== -1) {
|
||||
worldStr = userContext.substring(0, idx).trim();
|
||||
proseStr = userContext.substring(idx).trim();
|
||||
}
|
||||
|
||||
const sysLen = systemPrompt.length;
|
||||
const worldLen = worldStr.length;
|
||||
const proseLen = proseStr.length;
|
||||
const totalLen = sysLen + worldLen + proseLen;
|
||||
|
||||
if (totalLen === 0) return null;
|
||||
|
||||
const sysPct = (sysLen / totalLen) * 100;
|
||||
const worldPct = (worldLen / totalLen) * 100;
|
||||
const prosePct = (proseLen / totalLen) * 100;
|
||||
|
||||
const sysTokens = Math.round((sysLen / totalLen) * inputTokens);
|
||||
const worldTokens = Math.round((worldLen / totalLen) * inputTokens);
|
||||
const proseTokens = Math.max(0, inputTokens - sysTokens - worldTokens);
|
||||
|
||||
return [
|
||||
{
|
||||
label: "System Prompt",
|
||||
pct: sysPct,
|
||||
relativePct: sysPct,
|
||||
tokens: sysTokens,
|
||||
type: "system",
|
||||
content: systemPrompt,
|
||||
},
|
||||
{
|
||||
label: "Decoder Context",
|
||||
pct: worldPct,
|
||||
relativePct: worldPct,
|
||||
tokens: worldTokens,
|
||||
type: "world",
|
||||
content: worldStr,
|
||||
},
|
||||
{
|
||||
label: "Narrative Prose",
|
||||
pct: prosePct,
|
||||
relativePct: prosePct,
|
||||
tokens: proseTokens,
|
||||
type: "memories",
|
||||
content: proseStr,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const actorBreakdown =
|
||||
entry.rawPrompt && entry.usage
|
||||
? parseActorPrompt(
|
||||
entry.rawPrompt.systemPrompt,
|
||||
entry.rawPrompt.userContext,
|
||||
entry.usage.inputTokens,
|
||||
entry.rawPrompt.sections,
|
||||
)
|
||||
: null;
|
||||
const decoderBreakdown =
|
||||
entry.decoderPrompt && entry.decoderUsage
|
||||
? parseDecoderPrompt(
|
||||
entry.decoderPrompt.systemPrompt,
|
||||
entry.decoderPrompt.userContext,
|
||||
entry.decoderUsage.inputTokens,
|
||||
)
|
||||
: null;
|
||||
|
||||
const actorMaxContext =
|
||||
entry.usage?.maxContext !== undefined ? entry.usage.maxContext : 32768;
|
||||
const actorUsedTokens = entry.usage?.inputTokens || 0;
|
||||
const actorUsagePctOfContext =
|
||||
actorMaxContext > 0 ? (actorUsedTokens / actorMaxContext) * 100 : 0;
|
||||
const isActorAbsolute = actorMaxContext > 0 && actorUsagePctOfContext >= 20;
|
||||
|
||||
const scaledActorBreakdown = actorBreakdown
|
||||
? actorBreakdown.map((item) => ({
|
||||
...item,
|
||||
pct: isActorAbsolute
|
||||
? item.relativePct * (actorUsedTokens / actorMaxContext)
|
||||
: item.relativePct,
|
||||
}))
|
||||
: null;
|
||||
|
||||
const decoderMaxContext =
|
||||
entry.decoderUsage?.maxContext !== undefined
|
||||
? entry.decoderUsage.maxContext
|
||||
: 32768;
|
||||
const decoderUsedTokens = entry.decoderUsage?.inputTokens || 0;
|
||||
const decoderUsagePctOfContext =
|
||||
decoderMaxContext > 0 ? (decoderUsedTokens / decoderMaxContext) * 100 : 0;
|
||||
const isDecoderAbsolute =
|
||||
decoderMaxContext > 0 && decoderUsagePctOfContext >= 20;
|
||||
|
||||
const scaledDecoderBreakdown = decoderBreakdown
|
||||
? decoderBreakdown.map((item) => ({
|
||||
...item,
|
||||
pct: isDecoderAbsolute
|
||||
? item.relativePct * (decoderUsedTokens / decoderMaxContext)
|
||||
: item.relativePct,
|
||||
}))
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!entry.rawPrompt && entry.decoderPrompt) {
|
||||
setActiveTab("decoder");
|
||||
}
|
||||
}, [entry]);
|
||||
|
||||
// Helper to resolve components with a fallback if none exist (for backwards-compatibility)
|
||||
const getComponents = (
|
||||
promptBreakdown: any,
|
||||
defaultType: "world" | "input",
|
||||
) => {
|
||||
if (!promptBreakdown) return [];
|
||||
if (promptBreakdown.components && promptBreakdown.components.length > 0) {
|
||||
return promptBreakdown.components;
|
||||
}
|
||||
// Fallback: convert flat strings into components list
|
||||
return [
|
||||
{
|
||||
label: "System Prompt",
|
||||
type: "system" as const,
|
||||
content: promptBreakdown.systemPrompt || "",
|
||||
},
|
||||
{
|
||||
label: "User Context",
|
||||
type: defaultType,
|
||||
content: promptBreakdown.userContext || "",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const actorComponents = getComponents(entry.rawPrompt, "world");
|
||||
const decoderComponents = getComponents(entry.decoderPrompt, "input");
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-[750px] sm:max-w-[750px] h-[90vh] overflow-hidden flex flex-col p-0 gap-0">
|
||||
@@ -228,241 +71,37 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
|
||||
|
||||
<div className="overflow-y-auto flex-1 p-5">
|
||||
{activeTab === "actor" && entry.rawPrompt && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{entry.usage ? (
|
||||
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
|
||||
<strong>LLM Instance:</strong>{" "}
|
||||
<span>{entry.usage.providerInstanceName || "Default"}</span>
|
||||
{entry.usage.modelName && (
|
||||
<span> ({entry.usage.modelName})</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm italic text-muted-foreground">
|
||||
No LLM token usage (Player turn used fixed prose).
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scaledActorBreakdown && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
|
||||
<span className="font-semibold">
|
||||
Input Prompt Breakdown
|
||||
</span>
|
||||
<span>
|
||||
Total Input Tokens: <strong>{actorUsedTokens}</strong>
|
||||
{actorMaxContext > 0 ? (
|
||||
<span>
|
||||
{" "}
|
||||
/ {actorMaxContext} (
|
||||
{actorUsagePctOfContext.toFixed(1)}% used)
|
||||
</span>
|
||||
) : (
|
||||
<span> (infinite context)</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex h-6 w-full rounded border overflow-hidden bg-muted shadow-inner mb-2">
|
||||
{scaledActorBreakdown.map((item, idx) => {
|
||||
const displayPct =
|
||||
actorMaxContext > 0
|
||||
? (item.tokens / actorMaxContext) * 100
|
||||
: item.relativePct;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`h-full transition-all duration-300 ${
|
||||
item.type === "system"
|
||||
? "bg-blue-500"
|
||||
: item.type === "world"
|
||||
? "bg-emerald-500"
|
||||
: item.type === "memories"
|
||||
? "bg-purple-500"
|
||||
: "bg-amber-500"
|
||||
}`}
|
||||
style={{ width: `${item.pct}%` }}
|
||||
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isActorAbsolute && (
|
||||
<div
|
||||
className="bg-white h-full"
|
||||
style={{ width: `${100 - actorUsagePctOfContext}%` }}
|
||||
title={`Available: ${actorMaxContext - actorUsedTokens} tokens (${(100 - actorUsagePctOfContext).toFixed(1)}% remaining)`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Accordion type="multiple">
|
||||
{scaledActorBreakdown.map((item, idx) => {
|
||||
const displayPct =
|
||||
actorMaxContext > 0
|
||||
? (item.tokens / actorMaxContext) * 100
|
||||
: item.relativePct;
|
||||
return (
|
||||
<AccordionItem key={idx} value={String(idx)}>
|
||||
<AccordionTrigger className="text-sm">
|
||||
<span
|
||||
className={`inline-block w-2.5 h-2.5 rounded-sm mr-2 ${
|
||||
item.type === "system"
|
||||
? "bg-blue-500"
|
||||
: item.type === "world"
|
||||
? "bg-emerald-500"
|
||||
: item.type === "memories"
|
||||
? "bg-purple-500"
|
||||
: "bg-amber-500"
|
||||
}`}
|
||||
/>
|
||||
{item.label}: <strong>{item.tokens}</strong> tokens
|
||||
({displayPct.toFixed(0)}%)
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<pre className="m-0 p-2 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground">
|
||||
{item.content}
|
||||
</pre>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entry.usage && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2">
|
||||
<span className="font-semibold">LLM Output</span>
|
||||
<span>
|
||||
Total Output Tokens:{" "}
|
||||
<strong>{entry.usage.outputTokens}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded border-2">
|
||||
<pre className="m-0 p-2 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground">
|
||||
{entry.narrativeProse}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PromptAnalyzer
|
||||
components={actorComponents}
|
||||
inputTokens={entry.usage?.inputTokens || 0}
|
||||
maxContext={
|
||||
entry.usage?.maxContext !== undefined
|
||||
? entry.usage.maxContext
|
||||
: 32768
|
||||
}
|
||||
modelName={entry.usage?.modelName}
|
||||
providerInstanceName={entry.usage?.providerInstanceName}
|
||||
outputLabel="LLM Output (Narrative Prose)"
|
||||
outputText={entry.narrativeProse}
|
||||
outputTokens={entry.usage?.outputTokens}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "decoder" && entry.decoderPrompt && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{entry.decoderUsage && (
|
||||
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
|
||||
<strong>LLM Instance:</strong>{" "}
|
||||
<span>
|
||||
{entry.decoderUsage.providerInstanceName || "Default"}
|
||||
</span>
|
||||
{entry.decoderUsage.modelName && (
|
||||
<span> ({entry.decoderUsage.modelName})</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{scaledDecoderBreakdown && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
|
||||
<span className="font-semibold">
|
||||
Input Prompt Breakdown
|
||||
</span>
|
||||
<span>
|
||||
Total Input Tokens: <strong>{decoderUsedTokens}</strong>
|
||||
{decoderMaxContext > 0 ? (
|
||||
<span>
|
||||
{" "}
|
||||
/ {decoderMaxContext} (
|
||||
{decoderUsagePctOfContext.toFixed(1)}% used)
|
||||
</span>
|
||||
) : (
|
||||
<span> (infinite context)</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex h-6 w-full rounded border overflow-hidden bg-muted shadow-inner mb-2">
|
||||
{scaledDecoderBreakdown.map((item, idx) => {
|
||||
const displayPct =
|
||||
decoderMaxContext > 0
|
||||
? (item.tokens / decoderMaxContext) * 100
|
||||
: item.relativePct;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`h-full transition-all duration-300 ${
|
||||
item.type === "system"
|
||||
? "bg-blue-500"
|
||||
: item.type === "world"
|
||||
? "bg-emerald-500"
|
||||
: item.type === "memories"
|
||||
? "bg-purple-500"
|
||||
: "bg-amber-500"
|
||||
}`}
|
||||
style={{ width: `${item.pct}%` }}
|
||||
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isDecoderAbsolute && (
|
||||
<div
|
||||
className="bg-white h-full"
|
||||
style={{ width: `${100 - decoderUsagePctOfContext}%` }}
|
||||
title={`Available: ${decoderMaxContext - decoderUsedTokens} tokens (${(100 - decoderUsagePctOfContext).toFixed(1)}% remaining)`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Accordion type="multiple">
|
||||
{scaledDecoderBreakdown.map((item, idx) => {
|
||||
const displayPct =
|
||||
decoderMaxContext > 0
|
||||
? (item.tokens / decoderMaxContext) * 100
|
||||
: item.relativePct;
|
||||
return (
|
||||
<AccordionItem key={idx} value={String(idx)}>
|
||||
<AccordionTrigger className="text-sm">
|
||||
<span
|
||||
className={`inline-block w-2.5 h-2.5 rounded-sm mr-2 ${
|
||||
item.type === "system"
|
||||
? "bg-blue-500"
|
||||
: item.type === "world"
|
||||
? "bg-emerald-500"
|
||||
: item.type === "memories"
|
||||
? "bg-purple-500"
|
||||
: "bg-amber-500"
|
||||
}`}
|
||||
/>
|
||||
{item.label}: <strong>{item.tokens}</strong> tokens
|
||||
({displayPct.toFixed(0)}%)
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<pre className="m-0 p-2 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground">
|
||||
{item.content}
|
||||
</pre>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entry.decoderUsage && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2">
|
||||
<span className="font-semibold">LLM Output</span>
|
||||
<span>
|
||||
Total Output Tokens:{" "}
|
||||
<strong>{entry.decoderUsage.outputTokens}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded border-2">
|
||||
<pre className="m-0 p-2 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground">
|
||||
{JSON.stringify(entry.intents, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PromptAnalyzer
|
||||
components={decoderComponents}
|
||||
inputTokens={entry.decoderUsage?.inputTokens || 0}
|
||||
maxContext={
|
||||
entry.decoderUsage?.maxContext !== undefined
|
||||
? entry.decoderUsage.maxContext
|
||||
: 32768
|
||||
}
|
||||
modelName={entry.decoderUsage?.modelName}
|
||||
providerInstanceName={entry.decoderUsage?.providerInstanceName}
|
||||
outputLabel="LLM Output (Decoded Intent Sequence)"
|
||||
outputText={JSON.stringify(entry.intents, null, 2)}
|
||||
outputTokens={entry.decoderUsage?.outputTokens}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -8,6 +8,18 @@ export interface IntentInfo {
|
||||
minutesToAdvance?: number;
|
||||
}
|
||||
|
||||
export interface PromptComponent {
|
||||
label: string;
|
||||
type: "system" | "world" | "events" | "memories" | "input" | "other";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface PromptBreakdown {
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
components?: PromptComponent[];
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
turn: number;
|
||||
entityId: string;
|
||||
@@ -17,15 +29,7 @@ export interface LogEntry {
|
||||
timestamp: string;
|
||||
isHandoff?: boolean;
|
||||
handoffResult?: any;
|
||||
rawPrompt?: {
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
sections?: {
|
||||
worldInfo: string;
|
||||
memoryLedger: string;
|
||||
cognitiveBuffer: string;
|
||||
};
|
||||
};
|
||||
rawPrompt?: PromptBreakdown;
|
||||
usage?: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
@@ -34,10 +38,7 @@ export interface LogEntry {
|
||||
providerInstanceName?: string;
|
||||
maxContext?: number;
|
||||
};
|
||||
decoderPrompt?: {
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
};
|
||||
decoderPrompt?: PromptBreakdown;
|
||||
decoderUsage?: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
|
||||
@@ -45,6 +45,39 @@ export async function runHandoffResolution(session: SimSession): Promise<void> {
|
||||
];
|
||||
const info = session.entities.find((e) => e.id === entity.id);
|
||||
const entityName = info?.name || entity.id;
|
||||
let handoffPrompt = undefined;
|
||||
if (lastCall) {
|
||||
const header = "Cognitive Buffer Candidates for Handoff:";
|
||||
const userContext = lastCall.userContext;
|
||||
const idx = userContext.indexOf(header);
|
||||
|
||||
let contextStr = userContext;
|
||||
let candidatesStr = "";
|
||||
|
||||
if (idx !== -1) {
|
||||
contextStr = userContext.substring(0, idx).trim();
|
||||
candidatesStr = userContext.substring(idx).trim();
|
||||
}
|
||||
|
||||
handoffPrompt = {
|
||||
systemPrompt: lastCall.systemPrompt,
|
||||
userContext: lastCall.userContext,
|
||||
components: [
|
||||
{
|
||||
label: "System Prompt",
|
||||
type: "system",
|
||||
content: lastCall.systemPrompt,
|
||||
},
|
||||
{ label: "Entity Context", type: "world", content: contextStr },
|
||||
{
|
||||
label: "Cognitive Candidates",
|
||||
type: "input",
|
||||
content: candidatesStr,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
session.log.push({
|
||||
turn: session.turn,
|
||||
entityId: entity.id,
|
||||
@@ -53,12 +86,7 @@ export async function runHandoffResolution(session: SimSession): Promise<void> {
|
||||
intents: [],
|
||||
timestamp: worldState.clock.get().toISOString(),
|
||||
isHandoff: true,
|
||||
rawPrompt: lastCall
|
||||
? {
|
||||
systemPrompt: lastCall.systemPrompt,
|
||||
userContext: lastCall.userContext,
|
||||
}
|
||||
: undefined,
|
||||
rawPrompt: handoffPrompt,
|
||||
usage: lastCall?.usage,
|
||||
handoffResult: lastCall?.response,
|
||||
});
|
||||
|
||||
@@ -191,9 +191,30 @@ export async function processNpcTurn(
|
||||
session.decoderProvider.lastCalls[
|
||||
session.decoderProvider.lastCalls.length - 1
|
||||
];
|
||||
const proseHeader = "=== NARRATIVE PROSE ===";
|
||||
const userContext = decoderCall.userContext;
|
||||
const idx = userContext.indexOf(proseHeader);
|
||||
|
||||
let contextStr = userContext;
|
||||
let proseStr = "";
|
||||
|
||||
if (idx !== -1) {
|
||||
contextStr = userContext.substring(0, idx).trim();
|
||||
proseStr = userContext.substring(idx).trim();
|
||||
}
|
||||
|
||||
entry.decoderPrompt = {
|
||||
systemPrompt: decoderCall.systemPrompt,
|
||||
userContext: decoderCall.userContext,
|
||||
components: [
|
||||
{
|
||||
label: "System Prompt",
|
||||
type: "system",
|
||||
content: decoderCall.systemPrompt,
|
||||
},
|
||||
{ label: "Decoder Context", type: "world", content: contextStr },
|
||||
{ label: "Narrative Prose", type: "input", content: proseStr },
|
||||
],
|
||||
};
|
||||
entry.decoderUsage = decoderCall.usage;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
LedgerEntry,
|
||||
LedgerRepository,
|
||||
} from "@omnia/memory";
|
||||
import { hydrate } from "@omnia/voice";
|
||||
import { hydrate, PromptComponent } from "@omnia/voice";
|
||||
|
||||
/**
|
||||
* Zod schema for the structured response expected from the actor LLM.
|
||||
@@ -60,21 +60,24 @@ export class ActorPromptBuilder {
|
||||
/**
|
||||
* Assembles the system prompt and user context for a given entity.
|
||||
*/
|
||||
/**
|
||||
* Assembles the system prompt and user context for a given entity.
|
||||
*/
|
||||
build(
|
||||
worldState: WorldState,
|
||||
entity: Entity,
|
||||
): {
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
sections: {
|
||||
worldInfo: string;
|
||||
memoryLedger: string;
|
||||
cognitiveBuffer: string;
|
||||
};
|
||||
components: PromptComponent[];
|
||||
} {
|
||||
const systemPrompt = this.buildSystemPrompt();
|
||||
const { userContext, sections } = this.buildUserContext(worldState, entity);
|
||||
return { systemPrompt, userContext, sections };
|
||||
const { userContext, components } = this.buildUserContext(
|
||||
worldState,
|
||||
entity,
|
||||
systemPrompt,
|
||||
);
|
||||
return { systemPrompt, userContext, components };
|
||||
}
|
||||
|
||||
private buildSystemPrompt(): string {
|
||||
@@ -103,13 +106,10 @@ Guidelines:
|
||||
private buildUserContext(
|
||||
worldState: WorldState,
|
||||
entity: Entity,
|
||||
systemPrompt: string,
|
||||
): {
|
||||
userContext: string;
|
||||
sections: {
|
||||
worldInfo: string;
|
||||
memoryLedger: string;
|
||||
cognitiveBuffer: string;
|
||||
};
|
||||
components: PromptComponent[];
|
||||
} {
|
||||
const now = worldState.clock.get();
|
||||
|
||||
@@ -151,13 +151,28 @@ Guidelines:
|
||||
if (cognitiveBuffer) parts.push(cognitiveBuffer);
|
||||
const userContext = parts.join("\n\n");
|
||||
|
||||
const components: PromptComponent[] = [
|
||||
{ label: "System Prompt", type: "system", content: systemPrompt },
|
||||
{ label: "World Info", type: "world", content: worldInfo },
|
||||
];
|
||||
if (memoryLedger) {
|
||||
components.push({
|
||||
label: "Memory Ledger",
|
||||
type: "memories",
|
||||
content: memoryLedger,
|
||||
});
|
||||
}
|
||||
if (cognitiveBuffer) {
|
||||
components.push({
|
||||
label: "Cognitive Buffer",
|
||||
type: "events",
|
||||
content: cognitiveBuffer,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
userContext,
|
||||
sections: {
|
||||
worldInfo,
|
||||
memoryLedger,
|
||||
cognitiveBuffer,
|
||||
},
|
||||
components,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Entity, WorldState } from "@omnia/core";
|
||||
import { ILLMProvider } from "@omnia/llm";
|
||||
import { BufferEntry, BufferRepository, LedgerRepository } from "@omnia/memory";
|
||||
import { Intent, IntentDecoder, IntentSequence } from "@omnia/intent";
|
||||
import { PromptComponent } from "@omnia/voice";
|
||||
import {
|
||||
ActorPromptBuilder,
|
||||
ActorResponseSchema,
|
||||
@@ -56,11 +57,7 @@ export interface ActorTurnResult {
|
||||
intents: IntentSequence;
|
||||
systemPrompt?: string;
|
||||
userContext?: string;
|
||||
promptComponents?: {
|
||||
worldInfo: string;
|
||||
memoryLedger: string;
|
||||
cognitiveBuffer: string;
|
||||
};
|
||||
promptComponents?: PromptComponent[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +120,7 @@ export class ActorAgent {
|
||||
);
|
||||
}
|
||||
|
||||
const { systemPrompt, userContext, sections } = this.promptBuilder.build(
|
||||
const { systemPrompt, userContext, components } = this.promptBuilder.build(
|
||||
worldState,
|
||||
entity,
|
||||
);
|
||||
@@ -154,7 +151,7 @@ export class ActorAgent {
|
||||
intents,
|
||||
systemPrompt,
|
||||
userContext,
|
||||
promptComponents: sections,
|
||||
promptComponents: components,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
export * from "./dehydration.js";
|
||||
export * from "./hydration.js";
|
||||
export * from "./contractions.js";
|
||||
|
||||
export interface PromptComponent {
|
||||
label: string;
|
||||
type: "system" | "world" | "events" | "memories" | "input" | "other";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface PromptBreakdown {
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
components?: PromptComponent[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user