mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
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:
15 files changed
+960
-24
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
@@ -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] }),
|
||||
|
||||
Reference in new issue
Block a user