Implement Phase 4: tools, God Mode, and missing features

Backend:
- Add Tavily web search tool wrapper (tools/web_search.py)
- Add PDF reader + ChromaDB vector store tool (tools/pdf_reader.py)
- Bind tools to LLM calls via .bind_tools() in dynamic_graph_builder
- Implement God Mode using LangGraph interrupt_before + MemorySaver
- Add approve/reject/modify API endpoints for God Mode
- Add PDF upload endpoint with ingestion pipeline
- Add persistent run history (CouncilRun model + run_service + API)
- Add Alembic migration for council_runs table
- Enhance WebSocket to emit run_paused and run_resumed events
- Add tests for tools, God Mode, and run history

Frontend:
- Add God Mode approval UI (GodModePanel component)
- Add Auto-Pilot / God Mode toggle in Konferenzzimmer
- Add functional PDF upload handler
- Add Conditional Edge editor (EdgeSettingsPanel component)
- Add edge click selection in ArchitectCanvas
- Update Zustand store with edge selection and update actions
- Update types for God Mode, execution modes, and WS events
- Update API client with God Mode, PDF upload, and blueprint run endpoints
- Update WebSocket hook for paused/resumed events
- Add Vitest config and frontend tests (store, parser, types, API)

https://claude.ai/code/session_017U6idFgaqnYTXzPxA7mxMv
This commit is contained in:
Claude 2026-02-21 10:53:12 +00:00
parent c6d0c4a636
commit 001649a364
No known key found for this signature in database
31 changed files with 2502 additions and 81 deletions

View file

@ -1,12 +1,14 @@
"use client";
import { useState, useCallback } from "react";
import { useState, useCallback, useRef } from "react";
import { ReactFlowProvider } from "@xyflow/react";
import { Play, Square, Upload } from "lucide-react";
import { Play, Square, Upload, Shield, Zap } from "lucide-react";
import { ArchitectCanvas } from "@/app/components/ArchitectCanvas";
import { useCouncilWebSocket } from "@/app/hooks/useCouncilWebSocket";
import { GodModePanel } from "@/app/components/panels/GodModePanel";
import { useCouncilWebSocket, PauseInfo } from "@/app/hooks/useCouncilWebSocket";
import { useCouncilStore } from "@/app/store/council-store";
import { runApi } from "@/app/utils/api-client";
import { runApi, pdfApi } from "@/app/utils/api-client";
import { ExecutionMode, GodModeAction } from "@/app/types/council";
export default function KonferenzzimmerPage() {
const [topic, setTopic] = useState("");
@ -14,6 +16,10 @@ export default function KonferenzzimmerPage() {
const [result, setResult] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isRunning, setIsRunning] = useState(false);
const [executionMode, setExecutionMode] = useState<ExecutionMode>("auto-pilot");
const [pauseInfo, setPauseInfo] = useState<PauseInfo | null>(null);
const [isResuming, setIsResuming] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const setActiveRun = useCouncilStore((s) => s.setActiveRun);
const clearActiveNode = useCouncilStore((s) => s.clearActiveNode);
@ -22,24 +28,38 @@ export default function KonferenzzimmerPage() {
setResult(res);
setIsRunning(false);
setRunId(null);
setPauseInfo(null);
}, []);
const onError = useCallback((err: string) => {
setError(err);
setIsRunning(false);
setRunId(null);
setPauseInfo(null);
}, []);
useCouncilWebSocket({ run_id: runId, onComplete, onError });
const onPaused = useCallback((info: PauseInfo) => {
setPauseInfo(info);
setIsResuming(false);
}, []);
const onResumed = useCallback(() => {
setPauseInfo(null);
setIsResuming(false);
}, []);
useCouncilWebSocket({ run_id: runId, onComplete, onError, onPaused, onResumed });
const handleStart = async () => {
if (!topic.trim()) return;
setResult(null);
setError(null);
setIsRunning(true);
setPauseInfo(null);
clearActiveNode();
try {
const run = await runApi.start(topic);
const godMode = executionMode === "god-mode";
const run = await runApi.start(topic, godMode);
setActiveRun(run);
setRunId(run.run_id);
} catch (e) {
@ -53,6 +73,44 @@ export default function KonferenzzimmerPage() {
setIsRunning(false);
clearActiveNode();
setActiveRun(null);
setPauseInfo(null);
};
const handleGodModeAction = async (action: GodModeAction, modifiedDraft?: string) => {
if (!runId) return;
setIsResuming(true);
try {
const modified_state = modifiedDraft ? { current_draft: modifiedDraft } : undefined;
await runApi.approve(runId, action, modified_state);
if (action === "reject") {
setError("Vom Benutzer im God Mode abgelehnt.");
setIsRunning(false);
setRunId(null);
setPauseInfo(null);
}
} catch (e) {
setError("Fehler bei God Mode Aktion: " + (e as Error).message);
setIsResuming(false);
}
};
const handlePdfUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const res = await pdfApi.upload(file);
setTopic((prev) =>
prev
? `${prev}\n\n[PDF hochgeladen: ${res.filename}${res.chunks_ingested} Abschnitte]`
: `[PDF hochgeladen: ${res.filename}${res.chunks_ingested} Abschnitte]`
);
} catch (e) {
setError("PDF-Upload fehlgeschlagen: " + (e as Error).message);
}
// Reset the input
if (fileInputRef.current) fileInputRef.current.value = "";
};
return (
@ -66,10 +124,51 @@ export default function KonferenzzimmerPage() {
rows={1}
className="flex-1 rounded-lg border border-slate-200 px-3 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-indigo-300"
/>
<button className="flex items-center gap-1.5 text-sm text-slate-600 border border-slate-200 px-3 py-1.5 rounded-lg hover:bg-slate-50 transition-colors">
{/* PDF upload */}
<input
ref={fileInputRef}
type="file"
accept=".pdf"
onChange={handlePdfUpload}
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-1.5 text-sm text-slate-600 border border-slate-200 px-3 py-1.5 rounded-lg hover:bg-slate-50 transition-colors"
>
<Upload size={14} />
PDF
</button>
{/* Execution mode toggle */}
<button
onClick={() =>
setExecutionMode((m) => (m === "auto-pilot" ? "god-mode" : "auto-pilot"))
}
disabled={isRunning}
className={[
"flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg transition-colors border",
executionMode === "god-mode"
? "bg-amber-50 text-amber-700 border-amber-300 hover:bg-amber-100"
: "bg-slate-50 text-slate-600 border-slate-200 hover:bg-slate-100",
isRunning ? "opacity-50 cursor-not-allowed" : "",
].join(" ")}
title={
executionMode === "god-mode"
? "God Mode: Pause vor jedem Agenten"
: "Auto-Pilot: Automatischer Durchlauf"
}
>
{executionMode === "god-mode" ? (
<Shield size={14} />
) : (
<Zap size={14} />
)}
{executionMode === "god-mode" ? "God Mode" : "Auto-Pilot"}
</button>
{/* Start / Stop */}
{!isRunning ? (
<button
onClick={handleStart}
@ -95,11 +194,16 @@ export default function KonferenzzimmerPage() {
<ReactFlowProvider>
<div className="flex-1 h-full relative">
<ArchitectCanvas />
{isRunning && (
{isRunning && !pauseInfo && (
<div className="absolute top-3 left-1/2 -translate-x-1/2 bg-indigo-600 text-white text-xs px-4 py-1.5 rounded-full shadow-lg animate-pulse pointer-events-none">
Rat läuft
</div>
)}
{pauseInfo && (
<div className="absolute top-3 left-1/2 -translate-x-1/2 bg-amber-500 text-white text-xs px-4 py-1.5 rounded-full shadow-lg pointer-events-none">
Pausiert Freigabe erforderlich
</div>
)}
</div>
</ReactFlowProvider>
@ -108,7 +212,16 @@ export default function KonferenzzimmerPage() {
<div className="px-4 py-3 border-b border-slate-100">
<h2 className="text-sm font-semibold text-slate-700">Ergebnis</h2>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* God Mode approval panel */}
{pauseInfo && (
<GodModePanel
pauseInfo={pauseInfo}
onAction={handleGodModeAction}
isResuming={isResuming}
/>
)}
{error && (
<div className="rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-700">
{error}
@ -124,7 +237,7 @@ export default function KonferenzzimmerPage() {
Noch kein Ergebnis. Starte den Rat mit einem Thema.
</p>
)}
{isRunning && !result && (
{isRunning && !result && !pauseInfo && (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div