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:
parent
c6d0c4a636
commit
001649a364
31 changed files with 2502 additions and 81 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue