Compare commits

...
1 Commits
Author SHA1 Message Date
Nicolò Boschi cb44a15122 fix(ui): reflections based on don't show up all contents 2026-01-26 18:43:21 +01:00
3 changed files with 120 additions and 142 deletions
-48
View File
@@ -554,18 +554,6 @@ class ReflectLLMCall(BaseModel):
duration_ms: int = Field(description="Execution time in milliseconds")
class ReflectMentalModel(BaseModel):
"""A mental model accessed during reflect."""
id: str = Field(description="Mental model ID")
name: str = Field(description="Mental model name")
type: str = Field(description="Mental model type: entity, concept, event")
subtype: str = Field(description="Mental model subtype: structural, emergent, learned, directive")
observations: list[str] | None = Field(
default=None, description="Observations for directive mental models (subtype='directive')"
)
class ReflectBasedOn(BaseModel):
"""Evidence the response is based on: memories and mental models."""
@@ -577,10 +565,6 @@ class ReflectTrace(BaseModel):
tool_calls: list[ReflectToolCall] = Field(default_factory=list, description="Tool calls made during reflection")
llm_calls: list[ReflectLLMCall] = Field(default_factory=list, description="LLM calls made during reflection")
mental_models: list[ReflectMentalModel] = Field(
default_factory=list,
description="Mental models used during reflection (includes directives with subtype='directive')",
)
class ReflectResponse(BaseModel):
@@ -604,14 +588,6 @@ class ReflectResponse(BaseModel):
"trace": {
"tool_calls": [{"tool": "recall", "input": {"query": "AI"}, "duration_ms": 150}],
"llm_calls": [{"scope": "agent_1", "duration_ms": 1200}],
"mental_models": [
{
"id": "mm-1",
"name": "AI Technology",
"type": "concept",
"subtype": "structural",
}
],
},
}
}
@@ -1894,33 +1870,9 @@ def _register_routes(app: FastAPI):
for tc in core_result.tool_trace
]
llm_calls = [ReflectLLMCall(scope=lc.scope, duration_ms=lc.duration_ms) for lc in core_result.llm_trace]
# Build map of directive observations by id
directive_observations = {d.id: d.rules for d in core_result.directives_applied}
# Build mental models from tool trace (get_mental_model outputs)
trace_mental_models: list[ReflectMentalModel] = []
seen_model_ids: set[str] = set()
for tc in core_result.tool_trace:
if tc.tool == "get_mental_model" and tc.output.get("found") and "model" in tc.output:
model = tc.output["model"]
model_id = model.get("id")
if model_id and model_id not in seen_model_ids:
seen_model_ids.add(model_id)
model_subtype = model.get("subtype", "structural")
trace_mental_models.append(
ReflectMentalModel(
id=model_id,
name=model.get("name", ""),
type=model.get("type", "concept"),
subtype=model_subtype,
observations=directive_observations.get(model_id)
if model_subtype == "directive"
else None,
)
)
trace_result = ReflectTrace(
tool_calls=tool_calls,
llm_calls=llm_calls,
mental_models=trace_mental_models,
)
return ReflectResponse(
@@ -613,10 +613,6 @@ class MemoryEngine(MemoryEngineInterface):
]
for fact_type, facts in reflect_result.based_on.items()
},
# Extract mental models from based_on["mental-models"] for easy UI access
"mental_models": [
{"id": str(fact.id), "text": fact.text} for fact in reflect_result.based_on.get("mental-models", [])
],
}
# Update the reflection with the generated content and reflect_response
@@ -686,10 +682,6 @@ class MemoryEngine(MemoryEngineInterface):
]
for fact_type, facts in reflect_result.based_on.items()
},
# Extract mental models from based_on["mental-models"] for easy UI access
"mental_models": [
{"id": str(fact.id), "text": fact.text} for fact in reflect_result.based_on.get("mental-models", [])
],
}
# Update the reflection with the generated content and reflect_response
@@ -3722,14 +3714,13 @@ class MemoryEngine(MemoryEngineInterface):
continue # Skip models not actually used by the agent
seen_model_ids.add(model_id)
# Add to based_on as MemoryFact with type "mental-models"
model_name = model.get("name", "")
model_summary = model.get("summary") or model.get("description", "")
# Mental models have a "text" field containing the consolidated knowledge
based_on["mental-models"].append(
MemoryFact(
id=model_id,
text=f"{model_name}: {model_summary}",
text=model.get("text", ""),
fact_type="mental-models",
context=f"{model.get('type', 'concept')} ({model.get('subtype', 'structural')})",
context=None,
occurred_start=None,
occurred_end=None,
)
@@ -3744,14 +3735,13 @@ class MemoryEngine(MemoryEngineInterface):
continue # Skip models not actually used by the agent
seen_model_ids.add(model_id)
# Add to based_on as MemoryFact with type "mental-models"
model_name = model.get("name", "")
model_summary = model.get("summary") or model.get("description", "")
# Mental models have a "text" field containing the consolidated knowledge
based_on["mental-models"].append(
MemoryFact(
id=model_id,
text=f"{model_name}: {model_summary}",
text=model.get("text", ""),
fact_type="mental-models",
context=f"{model.get('type', 'concept')} ({model.get('subtype', 'structural')})",
context=None,
occurred_start=None,
occurred_end=None,
)
@@ -5021,7 +5011,6 @@ class MemoryEngine(MemoryEngineInterface):
]
for fact_type, facts in reflect_result.based_on.items()
},
"mental_models": [], # Mental models are included in based_on["mental-models"]
}
# Update the reflection with new content and reflect_response
@@ -33,6 +33,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Plus,
Sparkles,
@@ -57,7 +58,6 @@ interface ReflectResponseBasedOnFact {
interface ReflectResponse {
text: string;
based_on: Record<string, ReflectResponseBasedOnFact[]>;
mental_models?: Array<{ id: string; text: string }>;
}
interface Reflection {
@@ -616,14 +616,12 @@ function ReflectionDetailPanel({
})}`;
};
// Extract all memories from based_on
const basedOnFacts = reflection.reflect_response?.based_on
? Object.entries(reflection.reflect_response.based_on).flatMap(([factType, facts]) =>
facts.map((fact) => ({ ...fact, factType }))
)
: [];
const mentalModels = reflection.reflect_response?.mental_models || [];
// Extract facts by type from based_on
const basedOn = reflection.reflect_response?.based_on || {};
const worldFacts = basedOn["world"] || [];
const experienceFacts = basedOn["experience"] || [];
const mentalModels = basedOn["mental-models"] || [];
const totalFacts = worldFacts.length + experienceFacts.length + mentalModels.length;
return (
<div className="fixed right-0 top-0 h-screen w-1/2 bg-card border-l shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
@@ -703,80 +701,119 @@ function ReflectionDetailPanel({
</div>
</div>
{/* Based On Facts Section */}
{basedOnFacts.length > 0 && (
{/* Based On Section with Tabs */}
{totalFacts > 0 ? (
<div className="border-t border-border pt-5">
<div className="text-xs font-bold text-muted-foreground uppercase mb-3">
Based On ({basedOnFacts.length} {basedOnFacts.length === 1 ? "fact" : "facts"})
Based On ({totalFacts} {totalFacts === 1 ? "item" : "items"})
</div>
<div className="space-y-3">
{basedOnFacts.map((fact, i) => (
<div
key={fact.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
>
<div className="flex items-start justify-between gap-2 mb-2">
<span
className={`px-2 py-0.5 rounded text-xs font-medium ${
fact.factType === "world"
? "bg-blue-500/10 text-blue-600 dark:text-blue-400"
: fact.factType === "experience"
? "bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-purple-500/10 text-purple-600 dark:text-purple-400"
}`}
>
{fact.factType}
</span>
<Button
variant="outline"
size="sm"
className="h-6 text-xs"
onClick={() => setViewMemoryId(fact.id)}
>
View
</Button>
</div>
<p className="text-sm text-foreground leading-relaxed">{fact.text}</p>
</div>
))}
</div>
</div>
)}
<Tabs
defaultValue={
worldFacts.length > 0
? "world"
: experienceFacts.length > 0
? "experience"
: "mental-models"
}
>
<TabsList className="mb-4">
<TabsTrigger value="world" disabled={worldFacts.length === 0}>
World
<span className="ml-1.5 px-1.5 py-0.5 rounded-full text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400">
{worldFacts.length}
</span>
</TabsTrigger>
<TabsTrigger value="experience" disabled={experienceFacts.length === 0}>
Experience
<span className="ml-1.5 px-1.5 py-0.5 rounded-full text-xs bg-green-500/10 text-green-600 dark:text-green-400">
{experienceFacts.length}
</span>
</TabsTrigger>
<TabsTrigger value="mental-models" disabled={mentalModels.length === 0}>
Mental Models
<span className="ml-1.5 px-1.5 py-0.5 rounded-full text-xs bg-amber-500/10 text-amber-600 dark:text-amber-400">
{mentalModels.length}
</span>
</TabsTrigger>
</TabsList>
{/* Mental Models Used Section */}
{mentalModels.length > 0 && (
<div className="border-t border-border pt-5">
<div className="text-xs font-bold text-muted-foreground uppercase mb-3">
Mental Models Used ({mentalModels.length})
</div>
<div className="space-y-3">
{mentalModels.map((model, i) => (
<div
key={model.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
>
<div className="flex items-start justify-between gap-2 mb-2">
<span className="px-2 py-0.5 rounded text-xs font-medium bg-amber-500/10 text-amber-600 dark:text-amber-400">
mental_model
</span>
<Button
variant="outline"
size="sm"
className="h-6 text-xs"
onClick={() => setViewMemoryId(model.id)}
<TabsContent value="world">
<div className="space-y-3">
{worldFacts.map((fact, i) => (
<div
key={fact.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
>
View
</Button>
</div>
<p className="text-sm text-foreground leading-relaxed">{model.text}</p>
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-foreground leading-relaxed flex-1">
{fact.text}
</p>
<Button
variant="outline"
size="sm"
className="h-6 text-xs shrink-0"
onClick={() => setViewMemoryId(fact.id)}
>
View
</Button>
</div>
</div>
))}
</div>
))}
</div>
</div>
)}
</TabsContent>
{/* No based_on data yet */}
{!reflection.reflect_response && (
<TabsContent value="experience">
<div className="space-y-3">
{experienceFacts.map((fact, i) => (
<div
key={fact.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-foreground leading-relaxed flex-1">
{fact.text}
</p>
<Button
variant="outline"
size="sm"
className="h-6 text-xs shrink-0"
onClick={() => setViewMemoryId(fact.id)}
>
View
</Button>
</div>
</div>
))}
</div>
</TabsContent>
<TabsContent value="mental-models">
<div className="space-y-3">
{mentalModels.map((model, i) => (
<div
key={model.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-foreground leading-relaxed flex-1">
{model.text}
</p>
<Button
variant="outline"
size="sm"
className="h-6 text-xs shrink-0"
onClick={() => setViewMemoryId(model.id)}
>
View
</Button>
</div>
</div>
))}
</div>
</TabsContent>
</Tabs>
</div>
) : !reflection.reflect_response ? (
<div className="border-t border-border pt-5">
<div className="text-xs font-bold text-muted-foreground uppercase mb-3">Based On</div>
<p className="text-sm text-muted-foreground">
@@ -784,7 +821,7 @@ function ReflectionDetailPanel({
tracking.
</p>
</div>
)}
) : null}
{reflection.tags && reflection.tags.length > 0 && (
<div>