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
34
frontend/app/__tests__/api-client.test.ts
Normal file
34
frontend/app/__tests__/api-client.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { wsUrl } from "@/app/utils/api-client";
|
||||
|
||||
describe("wsUrl", () => {
|
||||
it("should convert http to ws", () => {
|
||||
const url = wsUrl("test-run-id");
|
||||
expect(url).toBe("ws://localhost:8000/ws/council/test-run-id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("API client types", () => {
|
||||
it("should export runApi with expected methods", async () => {
|
||||
const { runApi } = await import("@/app/utils/api-client");
|
||||
expect(runApi.start).toBeDefined();
|
||||
expect(runApi.startFromBlueprint).toBeDefined();
|
||||
expect(runApi.status).toBeDefined();
|
||||
expect(runApi.approve).toBeDefined();
|
||||
expect(runApi.getState).toBeDefined();
|
||||
});
|
||||
|
||||
it("should export councilApi with expected methods", async () => {
|
||||
const { councilApi } = await import("@/app/utils/api-client");
|
||||
expect(councilApi.list).toBeDefined();
|
||||
expect(councilApi.get).toBeDefined();
|
||||
expect(councilApi.create).toBeDefined();
|
||||
expect(councilApi.update).toBeDefined();
|
||||
expect(councilApi.delete).toBeDefined();
|
||||
});
|
||||
|
||||
it("should export pdfApi with upload method", async () => {
|
||||
const { pdfApi } = await import("@/app/utils/api-client");
|
||||
expect(pdfApi.upload).toBeDefined();
|
||||
});
|
||||
});
|
||||
148
frontend/app/__tests__/blueprint-parser.test.ts
Normal file
148
frontend/app/__tests__/blueprint-parser.test.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { parseGraphToBlueprint, parseBlueprintToGraph } from "@/app/utils/blueprint-parser";
|
||||
import { Node, Edge } from "@xyflow/react";
|
||||
import { AgentNodeData, CouncilBlueprint } from "@/app/types/council";
|
||||
|
||||
describe("parseGraphToBlueprint", () => {
|
||||
it("should convert React Flow nodes and edges to blueprint format", () => {
|
||||
const nodes: Node<AgentNodeData>[] = [
|
||||
{
|
||||
id: "n1",
|
||||
type: "agentNode",
|
||||
position: { x: 100, y: 200 },
|
||||
data: {
|
||||
label: "Master Agent",
|
||||
systemPrompt: "You are the master writer.",
|
||||
model: "claude-3-5-sonnet",
|
||||
tools: { webSearch: true, pdfReader: false },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "n2",
|
||||
type: "agentNode",
|
||||
position: { x: 400, y: 200 },
|
||||
data: {
|
||||
label: "Critic Agent",
|
||||
systemPrompt: "You evaluate drafts.",
|
||||
model: "gpt-4o",
|
||||
tools: { webSearch: false, pdfReader: true },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: "e1",
|
||||
source: "n1",
|
||||
target: "n2",
|
||||
type: "default",
|
||||
data: { type: "linear" },
|
||||
},
|
||||
];
|
||||
|
||||
const blueprint = parseGraphToBlueprint(nodes, edges, "Test Council");
|
||||
|
||||
expect(blueprint.version).toBe(1);
|
||||
expect(blueprint.name).toBe("Test Council");
|
||||
expect(blueprint.nodes).toHaveLength(2);
|
||||
expect(blueprint.edges).toHaveLength(1);
|
||||
|
||||
expect(blueprint.nodes[0].label).toBe("Master Agent");
|
||||
expect(blueprint.nodes[0].tools.webSearch).toBe(true);
|
||||
expect(blueprint.nodes[1].model).toBe("gpt-4o");
|
||||
|
||||
expect(blueprint.edges[0].type).toBe("linear");
|
||||
expect(blueprint.edges[0].source).toBe("n1");
|
||||
expect(blueprint.edges[0].target).toBe("n2");
|
||||
});
|
||||
|
||||
it("should handle conditional edges with condition labels", () => {
|
||||
const nodes: Node<AgentNodeData>[] = [
|
||||
{
|
||||
id: "n1",
|
||||
type: "agentNode",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: "A",
|
||||
systemPrompt: "",
|
||||
model: "claude-3-5-sonnet",
|
||||
tools: { webSearch: false, pdfReader: false },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: "e1",
|
||||
source: "n1",
|
||||
target: "n2",
|
||||
type: "conditionalEdge",
|
||||
data: { type: "conditional", condition: "approve" },
|
||||
},
|
||||
];
|
||||
|
||||
const blueprint = parseGraphToBlueprint(nodes, edges, "Test");
|
||||
expect(blueprint.edges[0].type).toBe("conditional");
|
||||
expect(blueprint.edges[0].condition).toBe("approve");
|
||||
});
|
||||
|
||||
it("should preserve existing blueprint ID", () => {
|
||||
const blueprint = parseGraphToBlueprint([], [], "Test", "existing-id");
|
||||
expect(blueprint.id).toBe("existing-id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseBlueprintToGraph", () => {
|
||||
it("should convert blueprint back to React Flow format", () => {
|
||||
const blueprint: CouncilBlueprint = {
|
||||
version: 1,
|
||||
name: "Test",
|
||||
nodes: [
|
||||
{
|
||||
id: "n1",
|
||||
label: "Master",
|
||||
systemPrompt: "You are the master.",
|
||||
model: "claude-3-5-sonnet",
|
||||
tools: { webSearch: true, pdfReader: false },
|
||||
position: { x: 100, y: 200 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: "e1",
|
||||
source: "n1",
|
||||
target: "n2",
|
||||
type: "conditional",
|
||||
condition: "rework",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { nodes, edges } = parseBlueprintToGraph(blueprint);
|
||||
|
||||
expect(nodes).toHaveLength(1);
|
||||
expect(nodes[0].type).toBe("agentNode");
|
||||
expect(nodes[0].data.label).toBe("Master");
|
||||
expect(nodes[0].data.tools.webSearch).toBe(true);
|
||||
|
||||
expect(edges).toHaveLength(1);
|
||||
expect(edges[0].type).toBe("conditionalEdge");
|
||||
expect(edges[0].data?.condition).toBe("rework");
|
||||
expect(edges[0].animated).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle linear edges", () => {
|
||||
const blueprint: CouncilBlueprint = {
|
||||
version: 1,
|
||||
name: "Test",
|
||||
nodes: [],
|
||||
edges: [
|
||||
{ id: "e1", source: "a", target: "b", type: "linear" },
|
||||
],
|
||||
};
|
||||
|
||||
const { edges } = parseBlueprintToGraph(blueprint);
|
||||
expect(edges[0].type).toBe("default");
|
||||
expect(edges[0].animated).toBe(false);
|
||||
});
|
||||
});
|
||||
178
frontend/app/__tests__/council-store.test.ts
Normal file
178
frontend/app/__tests__/council-store.test.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useCouncilStore } from "@/app/store/council-store";
|
||||
|
||||
describe("CouncilStore", () => {
|
||||
beforeEach(() => {
|
||||
// Reset store state between tests
|
||||
useCouncilStore.setState({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
selectedNodeId: null,
|
||||
selectedEdgeId: null,
|
||||
councilName: "Mein Rat",
|
||||
activeRun: null,
|
||||
activeNodeId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should have default state", () => {
|
||||
const state = useCouncilStore.getState();
|
||||
expect(state.nodes).toEqual([]);
|
||||
expect(state.edges).toEqual([]);
|
||||
expect(state.selectedNodeId).toBeNull();
|
||||
expect(state.selectedEdgeId).toBeNull();
|
||||
expect(state.councilName).toBe("Mein Rat");
|
||||
});
|
||||
|
||||
it("should add an agent node", () => {
|
||||
const { addAgentNode } = useCouncilStore.getState();
|
||||
addAgentNode({ x: 100, y: 200 });
|
||||
|
||||
const { nodes } = useCouncilStore.getState();
|
||||
expect(nodes).toHaveLength(1);
|
||||
expect(nodes[0].position).toEqual({ x: 100, y: 200 });
|
||||
expect(nodes[0].type).toBe("agentNode");
|
||||
expect(nodes[0].data.label).toBe("Agent 1");
|
||||
expect(nodes[0].data.model).toBe("claude-3-5-sonnet");
|
||||
expect(nodes[0].data.tools).toEqual({ webSearch: false, pdfReader: false });
|
||||
});
|
||||
|
||||
it("should update node data", () => {
|
||||
const { addAgentNode } = useCouncilStore.getState();
|
||||
addAgentNode({ x: 0, y: 0 });
|
||||
|
||||
const { nodes, updateNodeData } = useCouncilStore.getState();
|
||||
const nodeId = nodes[0].id;
|
||||
|
||||
updateNodeData(nodeId, { label: "Master Agent", model: "gpt-4o" });
|
||||
|
||||
const updated = useCouncilStore.getState().nodes[0];
|
||||
expect(updated.data.label).toBe("Master Agent");
|
||||
expect(updated.data.model).toBe("gpt-4o");
|
||||
});
|
||||
|
||||
it("should select a node and deselect edge", () => {
|
||||
const { selectNode } = useCouncilStore.getState();
|
||||
selectNode("node-1");
|
||||
|
||||
const state = useCouncilStore.getState();
|
||||
expect(state.selectedNodeId).toBe("node-1");
|
||||
expect(state.selectedEdgeId).toBeNull();
|
||||
});
|
||||
|
||||
it("should select an edge and deselect node", () => {
|
||||
const { selectEdge, selectNode } = useCouncilStore.getState();
|
||||
selectNode("node-1");
|
||||
selectEdge("edge-1");
|
||||
|
||||
const state = useCouncilStore.getState();
|
||||
expect(state.selectedEdgeId).toBe("edge-1");
|
||||
expect(state.selectedNodeId).toBeNull();
|
||||
});
|
||||
|
||||
it("should update edge data to conditional", () => {
|
||||
useCouncilStore.setState({
|
||||
edges: [
|
||||
{
|
||||
id: "e1",
|
||||
source: "a",
|
||||
target: "b",
|
||||
type: "default",
|
||||
data: { type: "linear" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { updateEdgeData } = useCouncilStore.getState();
|
||||
updateEdgeData("e1", "conditional", "rework");
|
||||
|
||||
const { edges } = useCouncilStore.getState();
|
||||
expect(edges[0].type).toBe("conditionalEdge");
|
||||
expect(edges[0].data?.type).toBe("conditional");
|
||||
expect(edges[0].data?.condition).toBe("rework");
|
||||
expect(edges[0].animated).toBe(true);
|
||||
});
|
||||
|
||||
it("should update edge data back to linear", () => {
|
||||
useCouncilStore.setState({
|
||||
edges: [
|
||||
{
|
||||
id: "e1",
|
||||
source: "a",
|
||||
target: "b",
|
||||
type: "conditionalEdge",
|
||||
data: { type: "conditional", condition: "approve" },
|
||||
animated: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { updateEdgeData } = useCouncilStore.getState();
|
||||
updateEdgeData("e1", "linear");
|
||||
|
||||
const { edges } = useCouncilStore.getState();
|
||||
expect(edges[0].type).toBe("default");
|
||||
expect(edges[0].data?.type).toBe("linear");
|
||||
expect(edges[0].animated).toBe(false);
|
||||
});
|
||||
|
||||
it("should mark a node as active by name", () => {
|
||||
useCouncilStore.setState({
|
||||
nodes: [
|
||||
{
|
||||
id: "n1",
|
||||
type: "agentNode",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: "Master Agent",
|
||||
systemPrompt: "",
|
||||
model: "claude-3-5-sonnet" as const,
|
||||
tools: { webSearch: false, pdfReader: false },
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { markNodeActive } = useCouncilStore.getState();
|
||||
markNodeActive("Master Agent");
|
||||
|
||||
const { nodes, activeNodeId } = useCouncilStore.getState();
|
||||
expect(activeNodeId).toBe("n1");
|
||||
expect(nodes[0].data.isActive).toBe(true);
|
||||
});
|
||||
|
||||
it("should clear active node", () => {
|
||||
useCouncilStore.setState({
|
||||
activeNodeId: "n1",
|
||||
nodes: [
|
||||
{
|
||||
id: "n1",
|
||||
type: "agentNode",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: "Test",
|
||||
systemPrompt: "",
|
||||
model: "claude-3-5-sonnet" as const,
|
||||
tools: { webSearch: false, pdfReader: false },
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { clearActiveNode } = useCouncilStore.getState();
|
||||
clearActiveNode();
|
||||
|
||||
const { nodes, activeNodeId } = useCouncilStore.getState();
|
||||
expect(activeNodeId).toBeNull();
|
||||
expect(nodes[0].data.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it("should set council name", () => {
|
||||
const { setCouncilName } = useCouncilStore.getState();
|
||||
setCouncilName("Test Rat");
|
||||
|
||||
expect(useCouncilStore.getState().councilName).toBe("Test Rat");
|
||||
});
|
||||
});
|
||||
80
frontend/app/__tests__/types.test.ts
Normal file
80
frontend/app/__tests__/types.test.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import type {
|
||||
AgentNodeData,
|
||||
CouncilBlueprint,
|
||||
ExecutionMode,
|
||||
GodModeAction,
|
||||
GodModeState,
|
||||
RunStatus,
|
||||
WSEventType,
|
||||
WSMessage,
|
||||
} from "@/app/types/council";
|
||||
|
||||
describe("Council types", () => {
|
||||
it("should support all run statuses", () => {
|
||||
const statuses: RunStatus[] = ["pending", "running", "completed", "failed", "paused"];
|
||||
expect(statuses).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("should support execution modes", () => {
|
||||
const modes: ExecutionMode[] = ["auto-pilot", "god-mode"];
|
||||
expect(modes).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should support god mode actions", () => {
|
||||
const actions: GodModeAction[] = ["approve", "reject", "modify"];
|
||||
expect(actions).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should support all WS event types", () => {
|
||||
const events: WSEventType[] = [
|
||||
"connected",
|
||||
"node_active",
|
||||
"run_paused",
|
||||
"run_resumed",
|
||||
"run_complete",
|
||||
"run_failed",
|
||||
];
|
||||
expect(events).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("should enforce AgentNodeData structure", () => {
|
||||
const data: AgentNodeData = {
|
||||
label: "Test Agent",
|
||||
systemPrompt: "You are a test agent.",
|
||||
model: "claude-3-5-sonnet",
|
||||
tools: { webSearch: true, pdfReader: false },
|
||||
isActive: false,
|
||||
};
|
||||
expect(data.label).toBe("Test Agent");
|
||||
expect(data.tools.webSearch).toBe(true);
|
||||
});
|
||||
|
||||
it("should enforce GodModeState structure", () => {
|
||||
const state: GodModeState = {
|
||||
run_id: "test-run",
|
||||
paused: true,
|
||||
next_nodes: ["critic"],
|
||||
current_state: {
|
||||
current_draft: "Draft text",
|
||||
critic_score: 6.5,
|
||||
iteration_count: 2,
|
||||
},
|
||||
};
|
||||
expect(state.paused).toBe(true);
|
||||
expect(state.next_nodes).toContain("critic");
|
||||
expect(state.current_state.critic_score).toBe(6.5);
|
||||
});
|
||||
|
||||
it("should enforce WSMessage structure", () => {
|
||||
const msg: WSMessage = {
|
||||
event: "run_paused",
|
||||
run_id: "test",
|
||||
next_nodes: ["agent1"],
|
||||
current_draft: "Draft",
|
||||
critic_score: 7.0,
|
||||
};
|
||||
expect(msg.event).toBe("run_paused");
|
||||
expect(msg.next_nodes).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue