mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Mémoire en notes Markdown + presets de configuration (repris d'AJEAN)
Les deux dernières idées d'AJEAN (github.com/nathaninline/jean). 1. Mémoire en notes Markdown ($DATA_DIR/MEMORY), écrites par l'agent via deux outils (memory_search / memory_save). Trois modes : off, ondemand (défaut) et always. Face au RAG vectoriel : aucun modèle d'embedding requis, notes lisibles et éditables à la main, et rien de mémorisé à l'insu — la recherche par mots-clés ne rappelle que du pertinent, là où le RAG ressortait une demande sans rapport. Le RAG disparaît de l'UI : un seul concept de mémoire. En mode Plan, seule la consultation est proposée, pas l'écriture. 2. Presets : jeux de réglages nommés, enregistrés et rappelés en un clic (sélecteur + « + Preset » dans l'en-tête des Réglages). Au passage, un vrai bug : ConfigPatch ne déclarait ni keep_alive, ni skills_enabled, ni ponytail — Pydantic ignore les champs non déclarés, donc ces trois réglages ne se sauvegardaient JAMAIS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
7 files changed
+463
-5
No files matched your search
@@ -293,6 +293,7 @@ export interface AgentConfig {
|
||||
embed_model: string;
|
||||
skills_enabled: boolean;
|
||||
ponytail: boolean;
|
||||
memory_mode: "off" | "ondemand" | "always";
|
||||
keep_alive: string;
|
||||
}
|
||||
|
||||
@@ -406,6 +407,44 @@ export async function getConfig(model?: string): Promise<{
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── Presets de configuration ─────────────────────────────────────────────
|
||||
export async function listPresets(): Promise<string[]> {
|
||||
const res = await fetch("/api/config/presets");
|
||||
return (await res.json()).presets;
|
||||
}
|
||||
|
||||
export async function savePreset(name: string, model?: string): Promise<string[]> {
|
||||
const query = model ? `?model=${encodeURIComponent(model)}` : "";
|
||||
const res = await fetch(`/api/config/presets${query}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!res.ok) throw await apiError(res, "preset refusé");
|
||||
return (await res.json()).presets;
|
||||
}
|
||||
|
||||
export async function applyPreset(
|
||||
name: string,
|
||||
model?: string
|
||||
): Promise<AgentConfig> {
|
||||
const query = model ? `?model=${encodeURIComponent(model)}` : "";
|
||||
const res = await fetch(`/api/config/presets/apply${query}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!res.ok) throw await apiError(res, "preset introuvable");
|
||||
return (await res.json()).config;
|
||||
}
|
||||
|
||||
export async function deletePreset(name: string): Promise<string[]> {
|
||||
const res = await fetch(`/api/config/presets?name=${encodeURIComponent(name)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
return (await res.json()).presets;
|
||||
}
|
||||
|
||||
export async function saveConfig(
|
||||
patch: Partial<AgentConfig>,
|
||||
model?: string
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useStore } from "../store/useStore";
|
||||
import {
|
||||
applyPreset,
|
||||
deleteModel,
|
||||
getBenchScores,
|
||||
getHardware,
|
||||
listMcp,
|
||||
listPresets,
|
||||
pullModel,
|
||||
runBench,
|
||||
savePreset,
|
||||
testMcp,
|
||||
updateMcp,
|
||||
} from "../api/client";
|
||||
@@ -125,6 +128,7 @@ export function SettingsView() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<PresetBar model={selectedModel} onApplied={refreshConfig} />
|
||||
<div className="flex items-center gap-1.5 text-[13px] text-ok">
|
||||
<span className="h-2 w-2 border-2 border-line bg-ok" />
|
||||
{status?.connected ? "Ollama connecté" : "Ollama déconnecté"}
|
||||
@@ -459,8 +463,6 @@ export function SettingsView() {
|
||||
"Méthodes expertes selon la tâche (débogage, web…)"] as const,
|
||||
["self_review", "Auto-critique",
|
||||
"Relit et révise la réponse — plus lent"] as const,
|
||||
["rag_enabled", "Mémoire entre sessions",
|
||||
"Rappelle d'AUTRES discussions ici. Chaque discussion garde toujours sa propre mémoire."] as const,
|
||||
]).map(([key, label, desc], i) => (
|
||||
<ToggleRow
|
||||
key={key}
|
||||
@@ -471,6 +473,29 @@ export function SettingsView() {
|
||||
onClick={() => set(key, !draft[key])}
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center gap-3 border-t-2 border-line-soft py-2">
|
||||
<span className="flex-1">
|
||||
<span className="block text-[14px] text-ink">Mémoire</span>
|
||||
<span className="block text-[12px] text-muted-2">
|
||||
Notes Markdown que l'agent écrit lui-même, relisibles
|
||||
dans <code>/data/MEMORY</code>
|
||||
</span>
|
||||
</span>
|
||||
<select
|
||||
value={draft.memory_mode}
|
||||
onChange={(e) =>
|
||||
set(
|
||||
"memory_mode",
|
||||
e.target.value as AgentConfig["memory_mode"]
|
||||
)
|
||||
}
|
||||
className="h-8 border-[3px] border-line bg-card px-2 text-[13px] text-ink"
|
||||
>
|
||||
<option value="off">Désactivée</option>
|
||||
<option value="ondemand">Sur demande</option>
|
||||
<option value="always">Automatique</option>
|
||||
</select>
|
||||
</div>
|
||||
</Advanced>
|
||||
<div className="mt-2 flex items-center gap-3 border-t-2 border-line-soft pt-3">
|
||||
<span className="flex-1">
|
||||
@@ -898,6 +923,75 @@ function Toggle({ on, onClick }: { on: boolean; onClick: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Presets : jeux de réglages nommés, enregistrés et rappelés en un clic. */
|
||||
function PresetBar({
|
||||
model,
|
||||
onApplied,
|
||||
}: {
|
||||
model: string;
|
||||
onApplied: () => void;
|
||||
}) {
|
||||
const [presets, setPresets] = useState<string[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
listPresets().then(setPresets).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const apply = async (name: string) => {
|
||||
if (!name) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await applyPreset(name, model || undefined);
|
||||
onApplied();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const create = async () => {
|
||||
const name = window.prompt("Nom du preset (réglages actuels) :");
|
||||
if (!name?.trim()) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
setPresets(await savePreset(name.trim(), model || undefined));
|
||||
} catch (e) {
|
||||
window.alert(e instanceof Error ? e.message : "preset refusé");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<select
|
||||
value=""
|
||||
disabled={busy || presets.length === 0}
|
||||
onChange={(e) => apply(e.target.value)}
|
||||
className="h-[34px] border-[3px] border-line bg-card px-2 text-[13px] text-ink disabled:opacity-40"
|
||||
title="Appliquer un preset"
|
||||
>
|
||||
<option value="">
|
||||
{presets.length ? "Preset…" : "Aucun preset"}
|
||||
</option>
|
||||
{presets.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={create}
|
||||
disabled={busy}
|
||||
className="h-[34px] border-[3px] border-line bg-card px-2.5 text-[13px] text-muted-2 disabled:opacity-40"
|
||||
title="Enregistrer les réglages actuels comme preset"
|
||||
>
|
||||
+ Preset
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Repli « Avancé » : sort les réglages rares du chemin principal.
|
||||
*
|
||||
* <details> natif : pas d'état à gérer, repliement mémorisé par le navigateur
|
||||
|
||||
Reference in new issue
Block a user