mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 13:38:11 +02:00
Phase 4 : boucle agentique, outils fichiers et aperçu live
Backend : - tools.py : read_file / write_file / list_dir confinés au workspace (anti-traversal) - agent.py : boucle de tool-calling itérative au-dessus d'Ollama, génère des événements (token, tool_call, tool_result, final) - routes/chat.py : pilote run_agent, relaie les événements en SSE, persiste la réponse finale + le récap des outils (meta) - routes/files.py : arborescence et contenu du workspace (confiné) - db.py : colonne meta sur messages (+ migration douce), list_messages_for_model Frontend : - ToolCard : carte d'appel d'outil dans le fil (statut en cours/terminé/échec) - streamChat : événements tool_call / tool_result - store : streamTools, fileTree, preview (ouverture auto d'un HTML écrit) - LeftPanel : arborescence réelle du workspace, clic -> aperçu - PreviewPanel : rendu HTML en iframe + onglet Code - ChatPanel : rendu des outils dans les bulles agent (streaming + persisté) Tests manuels : outils + confinement, boucle agent (mock Ollama), chat HTTP de bout en bout (SSE, persistance meta, fichiers écrits, confinement route). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
1 parent
48cbcaafaf
commit
b6bf4edf97
13 files changed
+677
-80
No files matched your search
@@ -5,12 +5,16 @@ import {
|
||||
getModels,
|
||||
getSession,
|
||||
getStatus,
|
||||
fileContent,
|
||||
listFiles,
|
||||
listSessions,
|
||||
streamChat,
|
||||
type FileNode,
|
||||
type Message,
|
||||
type OllamaModel,
|
||||
type OllamaStatus,
|
||||
type Session,
|
||||
type ToolCall,
|
||||
} from "../api/client";
|
||||
|
||||
interface LokiState {
|
||||
@@ -24,10 +28,17 @@ interface LokiState {
|
||||
messages: Message[];
|
||||
streaming: boolean;
|
||||
streamContent: string; // réponse de l'agent en cours de frappe
|
||||
streamTools: ToolCall[]; // appels d'outils de la réponse en cours
|
||||
|
||||
fileTree: FileNode[];
|
||||
previewPath: string | null;
|
||||
previewContent: string;
|
||||
|
||||
openPreview: (path: string) => Promise<void>;
|
||||
setSelectedModel: (name: string) => void;
|
||||
refreshStatus: () => Promise<void>;
|
||||
refreshModels: () => Promise<void>;
|
||||
refreshFiles: () => Promise<void>;
|
||||
|
||||
refreshSessions: () => Promise<void>;
|
||||
newSession: () => Promise<void>;
|
||||
@@ -47,9 +58,26 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
messages: [],
|
||||
streaming: false,
|
||||
streamContent: "",
|
||||
streamTools: [],
|
||||
fileTree: [],
|
||||
previewPath: null,
|
||||
previewContent: "",
|
||||
|
||||
openPreview: async (path) => {
|
||||
const content = await fileContent(path);
|
||||
set({ previewPath: path, previewContent: content });
|
||||
},
|
||||
|
||||
setSelectedModel: (name) => set({ selectedModel: name }),
|
||||
|
||||
refreshFiles: async () => {
|
||||
try {
|
||||
set({ fileTree: await listFiles() });
|
||||
} catch {
|
||||
/* workspace indisponible */
|
||||
}
|
||||
},
|
||||
|
||||
refreshStatus: async () => {
|
||||
try {
|
||||
const status = await getStatus();
|
||||
@@ -122,17 +150,42 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
messages: [...get().messages, userMsg],
|
||||
streaming: true,
|
||||
streamContent: "",
|
||||
streamTools: [],
|
||||
});
|
||||
|
||||
await streamChat(
|
||||
{ session_id: sid, content, model: get().selectedModel || undefined },
|
||||
{
|
||||
onToken: (t) => set({ streamContent: get().streamContent + t }),
|
||||
onToolCall: (call) =>
|
||||
set({ streamTools: [...get().streamTools, call] }),
|
||||
onToolResult: (call) => {
|
||||
// Met à jour le dernier outil correspondant (statut + résumé).
|
||||
const tools = [...get().streamTools];
|
||||
for (let i = tools.length - 1; i >= 0; i--) {
|
||||
if (tools[i].name === call.name && tools[i].status === "running") {
|
||||
tools[i] = { ...tools[i], ...call };
|
||||
break;
|
||||
}
|
||||
}
|
||||
set({ streamTools: tools });
|
||||
},
|
||||
onDone: async () => {
|
||||
set({ streaming: false, streamContent: "" });
|
||||
// Recharge depuis la base pour récupérer les ids/horodatages réels.
|
||||
// Repère un fichier HTML écrit pour l'afficher automatiquement.
|
||||
const writtenHtml = [...get().streamTools]
|
||||
.reverse()
|
||||
.find(
|
||||
(t) =>
|
||||
t.name === "write_file" &&
|
||||
typeof t.args?.path === "string" &&
|
||||
/\.html?$/.test(t.args.path as string)
|
||||
);
|
||||
set({ streaming: false, streamContent: "", streamTools: [] });
|
||||
// Recharge depuis la base + l'arborescence (fichiers créés par l'agent).
|
||||
if (get().currentSessionId === sid) await get().openSession(sid!);
|
||||
await get().refreshSessions();
|
||||
await get().refreshFiles();
|
||||
if (writtenHtml) await get().openPreview(writtenHtml.args.path as string);
|
||||
},
|
||||
onError: (msg) => {
|
||||
const errMsg: Message = {
|
||||
@@ -145,6 +198,7 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
set({
|
||||
streaming: false,
|
||||
streamContent: "",
|
||||
streamTools: [],
|
||||
messages: [...get().messages, errMsg],
|
||||
});
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user