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
|
|
@ -8,6 +8,7 @@ import {
|
|||
MiniMap,
|
||||
BackgroundVariant,
|
||||
useReactFlow,
|
||||
Edge,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ export function ArchitectCanvas() {
|
|||
const onConnect = useCouncilStore((s) => s.onConnect);
|
||||
const addAgentNode = useCouncilStore((s) => s.addAgentNode);
|
||||
const selectNode = useCouncilStore((s) => s.selectNode);
|
||||
const selectEdge = useCouncilStore((s) => s.selectEdge);
|
||||
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
|
||||
|
|
@ -53,7 +55,15 @@ export function ArchitectCanvas() {
|
|||
|
||||
const onPaneClick = useCallback(() => {
|
||||
selectNode(null);
|
||||
}, [selectNode]);
|
||||
selectEdge(null);
|
||||
}, [selectNode, selectEdge]);
|
||||
|
||||
const onEdgeClick = useCallback(
|
||||
(_event: React.MouseEvent, edge: Edge) => {
|
||||
selectEdge(edge.id);
|
||||
},
|
||||
[selectEdge]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex-1 h-full">
|
||||
|
|
@ -66,6 +76,7 @@ export function ArchitectCanvas() {
|
|||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onPaneClick={onPaneClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
nodeTypes={NODE_TYPES}
|
||||
edgeTypes={EDGE_TYPES}
|
||||
fitView
|
||||
|
|
|
|||
146
frontend/app/components/panels/EdgeSettingsPanel.tsx
Normal file
146
frontend/app/components/panels/EdgeSettingsPanel.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { X, ArrowRight } from "lucide-react";
|
||||
import { EdgeType } from "@/app/types/council";
|
||||
import { useCouncilStore } from "@/app/store/council-store";
|
||||
|
||||
// Right-side panel shown when a canvas edge is selected
|
||||
export function EdgeSettingsPanel() {
|
||||
const selectedEdgeId = useCouncilStore((s) => s.selectedEdgeId);
|
||||
const edges = useCouncilStore((s) => s.edges);
|
||||
const nodes = useCouncilStore((s) => s.nodes);
|
||||
const updateEdgeData = useCouncilStore((s) => s.updateEdgeData);
|
||||
const selectEdge = useCouncilStore((s) => s.selectEdge);
|
||||
|
||||
const edge = edges.find((e) => e.id === selectedEdgeId);
|
||||
|
||||
const [edgeType, setEdgeType] = useState<EdgeType>("linear");
|
||||
const [condition, setCondition] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (edge) {
|
||||
setEdgeType((edge.data?.type as EdgeType) ?? "linear");
|
||||
setCondition((edge.data?.condition as string) ?? "");
|
||||
}
|
||||
}, [selectedEdgeId, edge]);
|
||||
|
||||
if (!selectedEdgeId || !edge) return null;
|
||||
|
||||
const sourceNode = nodes.find((n) => n.id === edge.source);
|
||||
const targetNode = nodes.find((n) => n.id === edge.target);
|
||||
|
||||
const handleTypeChange = (newType: EdgeType) => {
|
||||
setEdgeType(newType);
|
||||
updateEdgeData(selectedEdgeId, newType, newType === "conditional" ? condition : undefined);
|
||||
};
|
||||
|
||||
const handleConditionChange = (value: string) => {
|
||||
setCondition(value);
|
||||
updateEdgeData(selectedEdgeId, edgeType, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="w-72 flex-shrink-0 bg-white border-l border-slate-200 p-4 flex flex-col gap-4 overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowRight size={16} className="text-indigo-600" />
|
||||
<h2 className="font-semibold text-slate-800 text-sm flex-1">
|
||||
Kanten-Einstellungen
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => selectEdge(null)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Connection info */}
|
||||
<div className="rounded-lg bg-slate-50 p-3 text-xs text-slate-600 space-y-1">
|
||||
<p>
|
||||
<strong>Von:</strong>{" "}
|
||||
{sourceNode ? (sourceNode.data as { label: string }).label : edge.source}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Nach:</strong>{" "}
|
||||
{targetNode ? (targetNode.data as { label: string }).label : edge.target}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Edge type */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-slate-500">Typ</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleTypeChange("linear")}
|
||||
className={[
|
||||
"flex-1 text-sm px-3 py-2 rounded-lg border transition-colors",
|
||||
edgeType === "linear"
|
||||
? "bg-slate-100 border-slate-400 text-slate-800 font-medium"
|
||||
: "bg-white border-slate-200 text-slate-500 hover:border-slate-300",
|
||||
].join(" ")}
|
||||
>
|
||||
Linear
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTypeChange("conditional")}
|
||||
className={[
|
||||
"flex-1 text-sm px-3 py-2 rounded-lg border transition-colors",
|
||||
edgeType === "conditional"
|
||||
? "bg-indigo-50 border-indigo-400 text-indigo-800 font-medium"
|
||||
: "bg-white border-slate-200 text-slate-500 hover:border-slate-300",
|
||||
].join(" ")}
|
||||
>
|
||||
Bedingt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Condition value (only for conditional edges) */}
|
||||
{edgeType === "conditional" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-slate-500">
|
||||
Bedingung (Routing-Wert)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={condition}
|
||||
onChange={(e) => handleConditionChange(e.target.value)}
|
||||
placeholder='z.B. "rework" oder "approve"'
|
||||
className="rounded-lg border border-slate-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-300"
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Dieser Wert wird mit <code className="bg-slate-100 px-1 rounded">route_decision</code> im
|
||||
State verglichen, um den Pfad zu bestimmen.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preset conditions */}
|
||||
{edgeType === "conditional" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-slate-500">
|
||||
Schnellauswahl
|
||||
</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{["approve", "rework", "done", "escalate"].map((preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
onClick={() => handleConditionChange(preset)}
|
||||
className={[
|
||||
"text-xs px-2 py-1 rounded-full border transition-colors",
|
||||
condition === preset
|
||||
? "bg-indigo-600 text-white border-indigo-600"
|
||||
: "bg-white text-slate-600 border-slate-200 hover:border-indigo-300",
|
||||
].join(" ")}
|
||||
>
|
||||
{preset}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
113
frontend/app/components/panels/GodModePanel.tsx
Normal file
113
frontend/app/components/panels/GodModePanel.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, X, Pencil, Shield } from "lucide-react";
|
||||
import { GodModeAction } from "@/app/types/council";
|
||||
import { PauseInfo } from "@/app/hooks/useCouncilWebSocket";
|
||||
|
||||
interface Props {
|
||||
pauseInfo: PauseInfo;
|
||||
onAction: (action: GodModeAction, modifiedDraft?: string) => void;
|
||||
isResuming: boolean;
|
||||
}
|
||||
|
||||
// God Mode approval panel — shown when the graph pauses at a node
|
||||
export function GodModePanel({ pauseInfo, onAction, isResuming }: Props) {
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editedDraft, setEditedDraft] = useState(pauseInfo.current_draft);
|
||||
|
||||
const handleModify = () => {
|
||||
if (editMode) {
|
||||
onAction("modify", editedDraft);
|
||||
setEditMode(false);
|
||||
} else {
|
||||
setEditedDraft(pauseInfo.current_draft);
|
||||
setEditMode(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border-2 border-amber-300 bg-amber-50 p-4 space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield size={18} className="text-amber-600" />
|
||||
<h3 className="font-semibold text-sm text-amber-800">
|
||||
God Mode — Freigabe erforderlich
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Info about which node is next */}
|
||||
<div className="text-xs text-amber-700 space-y-1">
|
||||
<p>
|
||||
<strong>Nächster Agent:</strong>{" "}
|
||||
{pauseInfo.next_nodes.join(", ") || "—"}
|
||||
</p>
|
||||
{pauseInfo.iteration_count != null && (
|
||||
<p>
|
||||
<strong>Iteration:</strong> {pauseInfo.iteration_count}
|
||||
</p>
|
||||
)}
|
||||
{pauseInfo.critic_score != null && (
|
||||
<p>
|
||||
<strong>Bewertung:</strong> {pauseInfo.critic_score}/10
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Current draft preview / editor */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-amber-700">
|
||||
Aktueller Entwurf
|
||||
</label>
|
||||
{editMode ? (
|
||||
<textarea
|
||||
value={editedDraft}
|
||||
onChange={(e) => setEditedDraft(e.target.value)}
|
||||
rows={8}
|
||||
className="rounded-lg border border-amber-300 bg-white px-3 py-2 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-lg bg-white border border-amber-200 p-3 text-sm text-slate-700 whitespace-pre-wrap leading-relaxed max-h-48 overflow-y-auto">
|
||||
{pauseInfo.current_draft || (
|
||||
<span className="italic text-slate-400">Kein Entwurf vorhanden</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => onAction("approve")}
|
||||
disabled={isResuming}
|
||||
className="flex items-center gap-1.5 text-sm text-white bg-green-600 px-3 py-1.5 rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Check size={14} />
|
||||
Genehmigen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleModify}
|
||||
disabled={isResuming}
|
||||
className="flex items-center gap-1.5 text-sm text-white bg-blue-600 px-3 py-1.5 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
{editMode ? "Änderung senden" : "Ändern"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction("reject")}
|
||||
disabled={isResuming}
|
||||
className="flex items-center gap-1.5 text-sm text-white bg-red-500 px-3 py-1.5 rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<X size={14} />
|
||||
Ablehnen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isResuming && (
|
||||
<p className="text-xs text-amber-600 animate-pulse">
|
||||
Wird fortgesetzt…
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue