Les 5 évolutions : plan, auto-critique, RAG, vérif HTML, benchmark

1. Plan-puis-exécute (enhance.make_plan) : les demandes complexes sont
   décomposées en 3-5 étapes (event SSE 'plan', carte PLAN dans le fil,
   meta.plan persisté) ; le plan guide l'agent et le moteur code.
2. Auto-critique « Qualité + » (enhance.self_review, toggle Intelligence) :
   critique éclair puis révision de la réponse (event 'revision').
3. Mémoire long-terme RAG (rag.py) : échanges vectorisés via /api/embed
   (modèle d'embedding auto-détecté), rappel cosinus top-3 inter-sessions
   injecté en contexte, indexation en arrière-plan, élagage à 2000 souvenirs.
4. Vérification HTML (tools.check_html) : références locales cassées et
   balises déséquilibrées ; branchée sur l'auto-vérification des outils ET
   sur le moteur code avec une passe d'auto-correction Aider.
5. Benchmark intégré (bench.py + /api/bench) : 5 épreuves notées /100
   (appel d'outil, code exécuté en sous-processus isolé, consignes, JSON,
   format), streaming SSE, scores stockés ; carte BENCHMARK dans l'UI.

Config : plan_mode / self_review / rag_enabled / embed_model + carte
Intelligence (3 toggles). Client SSE : events plan/revision ; PlanCard.

Tests : heuristique+parsing du plan, révision, index/rappel RAG (exclusion
de la session courante), html_check, bench 100/100 sur modèle simulé,
intégration chat HTTP (event plan + meta persisté), build front.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
Claude committed 2026-07-07 11:47:46 +00:00
1 parent cf1444f7c0
commit d9be1c4dda
15 files changed
+960 -24

No files matched your search

+80 -1
View File
@@ -89,7 +89,13 @@ export interface Message {
role: "user" | "assistant";
content: string;
model?: string;
meta?: { tools?: ToolCall[]; stats?: MessageStats; thinking?: string } | null;
meta?: {
tools?: ToolCall[];
stats?: MessageStats;
thinking?: string;
plan?: string[];
engine?: string;
} | null;
created_at: number;
}
@@ -105,6 +111,75 @@ export interface AgentConfig {
tools: Record<string, boolean>;
confirm_shell: boolean;
think: boolean;
code_model: string;
plan_mode: boolean;
self_review: boolean;
rag_enabled: boolean;
embed_model: string;
}
// ── Benchmark de modèles ─────────────────────────────────────────────────
export interface BenchDetail {
task: string;
score: number;
detail: string;
}
export interface BenchResult {
score: number;
details: BenchDetail[];
at: number;
}
export async function getBenchScores(): Promise<Record<string, BenchResult>> {
const res = await fetch("/api/bench");
return (await res.json()).scores;
}
/** Lance le benchmark d'un modèle en streamant la progression. */
export async function runBench(
model: string,
onProgress: (task: string, score: number | null, detail?: string) => void
): Promise<BenchResult | null> {
const res = await fetch("/api/bench", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model }),
});
if (!res.body) return null;
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let final: BenchResult | null = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split("\n\n");
buffer = events.pop() ?? "";
for (const block of events) {
let event = "";
let data = "";
for (const line of block.split("\n")) {
if (line.startsWith("event: ")) event = line.slice(7).trim();
else if (line.startsWith("data: ")) data += line.slice(6);
}
if (!data) continue;
try {
const payload = JSON.parse(data);
if (event === "task_start") onProgress(payload.task, null);
else if (event === "task_done")
onProgress(payload.task, payload.score, payload.detail);
else if (event === "done")
final = { score: payload.score, details: payload.details, at: Date.now() / 1000 };
} catch {
/* bloc partiel */
}
}
}
return final;
}
export async function runShell(
@@ -204,6 +279,8 @@ export async function streamChat(
onToolConfirm: (command: string) => void;
onStatus: (msg: string) => void;
onNotice: (msg: string) => void;
onPlan?: (steps: string[]) => void;
onRevision?: (content: string) => void;
onDone: (full: string) => void;
onError: (msg: string) => void;
onAbort?: () => void;
@@ -265,6 +342,8 @@ export async function streamChat(
try {
const payload = JSON.parse(dataLines.join("\n"));
if (event === "token") handlers.onToken(payload.content);
else if (event === "plan") handlers.onPlan?.(payload.steps);
else if (event === "revision") handlers.onRevision?.(payload.content);
else if (event === "thinking") handlers.onThinking(payload.content);
else if (event === "status") handlers.onStatus(payload.message);
else if (event === "notice") handlers.onNotice(payload.message);
+33 -2
View File
@@ -17,6 +17,7 @@ export function ChatPanel() {
streamStatus,
streamNotice,
streamTools,
streamPlan,
sendMessage,
currentSessionId,
config,
@@ -37,7 +38,7 @@ export function ChatPanel() {
// Auto-scroll vers le bas à chaque token / message.
useEffect(() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
}, [messages, streamContent, streamThinking, streamTools]);
}, [messages, streamContent, streamThinking, streamTools, streamPlan]);
const submit = () => {
if (!draft.trim() || streaming) return;
@@ -101,7 +102,7 @@ export function ChatPanel() {
role: "assistant",
content: streamContent,
model: selectedModel,
meta: { tools: streamTools },
meta: { tools: streamTools, plan: streamPlan },
created_at: Date.now() / 1000,
}}
pending
@@ -173,6 +174,33 @@ export function ChatPanel() {
);
}
/** Plan d'exécution affiché avant le travail de l'agent. */
function PlanCard({ steps }: { steps: string[] }) {
return (
<div
className="mb-[11px] overflow-hidden border-[3px] border-line bg-card shadow-hard-sm"
style={{ borderRadius: 7 }}
>
<div className="flex items-center gap-2 border-b-2 border-line-soft px-3 py-2">
<span className="font-pixel text-[9px] text-accent">PLAN</span>
<span className="text-[12px] text-muted-2">
{steps.length} étape{steps.length > 1 ? "s" : ""}
</span>
</div>
<ol className="m-0 list-none px-3 py-2">
{steps.map((s, i) => (
<li key={i} className="flex gap-2 py-[3px] text-[13px] text-ink-2">
<span className="flex h-[18px] w-[18px] flex-none items-center justify-center border-2 border-line bg-base text-[11px] text-ink">
{i + 1}
</span>
<span className="min-w-0">{s}</span>
</li>
))}
</ol>
</div>
);
}
function Bubble({
msg,
pending,
@@ -227,6 +255,9 @@ function Bubble({
text={thinking ?? msg.meta?.thinking ?? ""}
live={!!pending}
/>
{(msg.meta?.plan?.length ?? 0) > 0 && (
<PlanCard steps={msg.meta!.plan!} />
)}
{(msg.meta?.tools ?? []).map((t: ToolCall, i: number) => (
<ToolCard key={i} call={t} />
))}
+136 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useStore } from "../store/useStore";
import { deleteModel, pullModel } from "../api/client";
import type { AgentConfig } from "../api/client";
import { deleteModel, getBenchScores, pullModel, runBench } from "../api/client";
import type { AgentConfig, BenchResult } from "../api/client";
import { DownloadIcon, RefreshIcon } from "../components/Icon";
const TOOL_DESC: Record<string, string> = {
@@ -376,6 +376,38 @@ export function SettingsView() {
</div>
)}
</Card>
<Card>
<div className="mb-3.5 font-pixel text-[11px] text-ink">
INTELLIGENCE
</div>
{[
["plan_mode", "Plan-puis-exécute",
"Décompose les demandes complexes en étapes"] as const,
["self_review", "Auto-critique (Qualité +)",
"Relit et révise la réponse avant de la donner"] as const,
["rag_enabled", "Mémoire long-terme (RAG)",
"Se souvient des anciennes sessions (modèle d'embedding requis)"] as const,
].map(([key, label, desc], i) => (
<div
key={key}
className={`flex items-center gap-3 py-2 ${
i > 0 ? "border-t-2 border-line-soft" : ""
}`}
>
<span className="flex-1">
<span className="block text-[14px] text-ink">{label}</span>
<span className="block text-[12px] text-muted-2">{desc}</span>
</span>
<Toggle
on={Boolean(draft[key])}
onClick={() => set(key, !draft[key])}
/>
</div>
))}
</Card>
<BenchCard />
</div>
</div>
@@ -403,6 +435,108 @@ export function SettingsView() {
);
}
/** Benchmark : évalue le modèle sélectionné sur 5 mini-épreuves. */
function BenchCard() {
const selectedModel = useStore((s) => s.selectedModel);
const [scores, setScores] = useState<Record<string, BenchResult>>({});
const [running, setRunning] = useState(false);
const [progress, setProgress] = useState<
{ task: string; score: number | null; detail?: string }[]
>([]);
useEffect(() => {
getBenchScores().then(setScores).catch(() => {});
}, []);
const launch = async () => {
if (!selectedModel || running) return;
setRunning(true);
setProgress([]);
try {
const result = await runBench(selectedModel, (task, score, detail) => {
setProgress((p) => {
const rest = p.filter((x) => x.task !== task);
return [...rest, { task, score, detail }];
});
});
if (result) setScores((s) => ({ ...s, [selectedModel]: result }));
} finally {
setRunning(false);
}
};
const current = scores[selectedModel];
return (
<Card>
<div className="mb-3.5 flex items-center justify-between">
<div className="font-pixel text-[11px] text-ink">BENCHMARK</div>
<button
onClick={launch}
disabled={running || !selectedModel}
className="flex h-8 items-center gap-1.5 border-[3px] border-line bg-accent px-3 text-[13px] text-white shadow-accent-soft disabled:opacity-40"
style={{ borderRadius: 7 }}
>
{running ? "ÉVALUATION…" : "TESTER CE MODÈLE"}
</button>
</div>
<div className="mb-2 text-[13px] text-muted-2">
5 mini-épreuves (outils, code, consignes, JSON, format) — score /100
pour <b className="text-ink">{selectedModel || "—"}</b>.
</div>
{running && (
<div className="border-2 border-line bg-base px-3 py-2">
{progress.map((p) => (
<div key={p.task} className="flex items-center gap-2 py-[2px] text-[13px]">
<span className="flex-1 text-ink-2">{p.task}</span>
{p.score === null ? (
<span className="text-muted-3">…</span>
) : (
<span className={p.score >= 14 ? "text-ok" : p.score >= 8 ? "text-ink" : "text-warn"}>
{p.score}/20
</span>
)}
</div>
))}
</div>
)}
{!running && current && (
<div className="border-2 border-line bg-base px-3 py-2">
<div className="mb-1 flex items-center justify-between">
<span className="text-[13px] text-ink">Score</span>
<span
className={`font-pixel text-[13px] ${
current.score >= 70 ? "text-ok" : current.score >= 45 ? "text-ink" : "text-warn"
}`}
>
{current.score}/100
</span>
</div>
{current.details.map((d) => (
<div key={d.task} className="flex items-center gap-2 py-[2px] text-[12px]">
<span className="flex-1 text-muted-2">{d.task}</span>
<span className="text-muted" title={d.detail}>{d.score}/20</span>
</div>
))}
</div>
)}
{Object.keys(scores).length > 1 && (
<div className="mt-2 text-[12px] text-muted-2">
Autres :{" "}
{Object.entries(scores)
.filter(([m]) => m !== selectedModel)
.map(([m, r]) => `${m} (${r.score})`)
.join(" · ")}
</div>
)}
</Card>
);
}
function Card({
children,
className = "",
+5
View File
@@ -40,6 +40,7 @@ interface LokiState {
streamStatus: string;
streamNotice: string | null;
streamTools: ToolCall[]; // appels d'outils de la réponse en cours
streamPlan: string[]; // plan de la réponse en cours
fileTree: FileNode[];
previewPath: string | null;
@@ -88,6 +89,7 @@ export const useStore = create<LokiState>((set, get) => ({
streamStatus: "",
streamNotice: null,
streamTools: [],
streamPlan: [],
fileTree: [],
previewPath: null,
previewContent: "",
@@ -234,6 +236,7 @@ export const useStore = create<LokiState>((set, get) => ({
streamStatus: "",
streamNotice: null,
streamTools: [],
streamPlan: [],
pendingShell: null,
});
void get().refreshSessions();
@@ -305,6 +308,8 @@ export const useStore = create<LokiState>((set, get) => ({
onToken: (t) => set({ streamContent: get().streamContent + t }),
onThinking: (t) => set({ streamThinking: get().streamThinking + t }),
onStatus: (message) => set({ streamStatus: message }),
onPlan: (steps) => set({ streamPlan: steps }),
onRevision: (content) => set({ streamContent: content }),
onNotice: (message) => set({ streamNotice: message }),
onToolCall: (call) =>
set({ streamTools: [...get().streamTools, call] }),