From ecddce9944ed5601027a3cabf11f9a7ff762f7d0 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 17 Jul 2026 14:15:35 +0100 Subject: [PATCH] Remove agent builder from processor (#7063) # Description of Changes Remove agent builder from processor because we don't intend to implement it any time soon. --- .../public/locales/en-US/translation.toml | 85 ---- .../sources/AgentBuilderAction.test.tsx | 10 - .../components/sources/AgentBuilderAction.tsx | 7 - .../src/portal-saas/views/AgentBuilder.tsx | 10 - frontend/editor/src/portal/MOCKS.md | 1 - frontend/editor/src/portal/ViewRouter.tsx | 5 - frontend/editor/src/portal/api/agents.ts | 132 ----- .../editor/src/portal/api/demoData.test.ts | 10 +- .../AgentBuilderPanel.stories.tsx | 30 -- .../agent-builder/AgentBuilderPanel.tsx | 86 ---- .../agent-builder/AgentKpiStrip.stories.tsx | 23 - .../agent-builder/AgentKpiStrip.tsx | 57 --- .../agent-builder/AgentSelector.stories.tsx | 33 -- .../agent-builder/AgentSelector.tsx | 55 --- .../agent-builder/BootstrapDialog.stories.tsx | 30 -- .../agent-builder/BootstrapDialog.tsx | 71 --- .../agent-builder/EvalsPanel.stories.tsx | 36 -- .../components/agent-builder/EvalsPanel.tsx | 113 ----- .../agent-builder/ScenariosPanel.stories.tsx | 30 -- .../agent-builder/ScenariosPanel.tsx | 112 ----- .../agent-builder/ToolsPanel.stories.tsx | 35 -- .../components/agent-builder/ToolsPanel.tsx | 87 ---- .../agent-builder/VersionsPanel.stories.tsx | 35 -- .../agent-builder/VersionsPanel.tsx | 104 ---- .../editor/src/portal/components/icons.tsx | 12 - .../components/sources/AgentBuilderAction.tsx | 22 - .../src/portal/contexts/ViewContext.tsx | 3 - frontend/editor/src/portal/mocks/agents.ts | 399 --------------- .../src/portal/mocks/handlers/agents.ts | 12 - .../editor/src/portal/mocks/handlers/index.ts | 2 - .../editor/src/portal/views/AgentBuilder.css | 458 ------------------ .../editor/src/portal/views/AgentBuilder.tsx | 101 ---- .../editor/src/portal/views/Sources.test.tsx | 5 - frontend/editor/src/portal/views/Sources.tsx | 2 - 34 files changed, 5 insertions(+), 2208 deletions(-) delete mode 100644 frontend/editor/src/portal-saas/components/sources/AgentBuilderAction.test.tsx delete mode 100644 frontend/editor/src/portal-saas/components/sources/AgentBuilderAction.tsx delete mode 100644 frontend/editor/src/portal-saas/views/AgentBuilder.tsx delete mode 100644 frontend/editor/src/portal/api/agents.ts delete mode 100644 frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.stories.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.stories.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/AgentSelector.stories.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/AgentSelector.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/BootstrapDialog.stories.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/BootstrapDialog.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/EvalsPanel.stories.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/EvalsPanel.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/ScenariosPanel.stories.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/ScenariosPanel.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/ToolsPanel.stories.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/ToolsPanel.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/VersionsPanel.stories.tsx delete mode 100644 frontend/editor/src/portal/components/agent-builder/VersionsPanel.tsx delete mode 100644 frontend/editor/src/portal/components/sources/AgentBuilderAction.tsx delete mode 100644 frontend/editor/src/portal/mocks/agents.ts delete mode 100644 frontend/editor/src/portal/mocks/handlers/agents.ts delete mode 100644 frontend/editor/src/portal/views/AgentBuilder.css delete mode 100644 frontend/editor/src/portal/views/AgentBuilder.tsx diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index eada1744c4..9681a69f2f 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -6136,90 +6136,6 @@ free = "Editor plan" subscribed = "Processor plan" unlinked = "Not linked" -[portal.agentBuilder] -bootstrapFromDocument = "Bootstrap from document" -sectionsAriaLabel = "Agent builder sections" -selectorAriaLabel = "Agents" -subtitle = "Design, test and ship the AI agents that classify, extract from and route your documents. Define scenarios, fence tool access, run a golden set, and publish a version." -title = "Agent Builder" - -[portal.agentBuilder.bootstrap] -cancel = "Cancel" -dropzoneText = "Choose a sample document (PDF or image)" -lead = "Drop a sample document and we'll propose scenarios and an extraction schema you can refine. Nothing is published until you review it." -submit = "Bootstrap agent" -subtitle = "Seed a new agent from one representative file" -title = "Bootstrap from a document" - -[portal.agentBuilder.empty] -description = "Bootstrap an agent from a sample document to seed its scenarios and extraction schema, then refine and publish." -title = "No agents yet" - -[portal.agentBuilder.evals] -casesPassing = "Cases passing" -columnCase = "Eval case" -columnLatency = "Latency" -columnResult = "Result" -fail = "fail" -goldenSetPassRate = "Golden-set pass rate" -latencyMs = "{{ms}} ms" -notRun = "not run" -pass = "pass" -passRate = "Pass rate" -runEvals = "Run evals" - -[portal.agentBuilder.evals.empty] -description = "Evals turn your scenarios into a repeatable golden set. Upgrade to capture pass-rate over time and gate publishes on it." -title = "No golden set yet" - -[portal.agentBuilder.kpi] -acrossGoldenSets = "across golden sets" -activeAgents = "Active agents" -avgPassRate = "Avg eval pass rate" -fleetWide = "fleet-wide" -latestPublished = "Latest published" -scenarios = "Scenarios" -testCases = "test cases" -totalDescription_one = "{{count}} total" -totalDescription_other = "{{count}} total" - -[portal.agentBuilder.scenarios] -add = "Add" -addScenario = "Add scenario" -enable = "Enable" -expectationLabel = "Expected behaviour" -expectationPlaceholder = "What the agent should do" -inEval = "in eval" -mute = "Mute" -muted = "muted" -nameLabel = "Name" -namePlaceholder = "e.g. Compliance escalation" - -[portal.agentBuilder.status] -draft = "Draft" -published = "Published" - -[portal.agentBuilder.tabs] -evals = "Evals" -scenarios = "Scenarios" -tools = "Tools" -versions = "Versions" - -[portal.agentBuilder.tools] -broadAccess = "Broad access" -deniedHint = "Selected tools are blocked. Everything else stays callable." -deniedTools = "Denied tools" -governanceGate = "Tool governance is available on the Enterprise plan." -restricted = "Restricted" -restrictedAccess = "Restricted tool access" -restrictedDescription = "Allow every tool except the ones you deny below." - -[portal.agentBuilder.versions] -current = "current" -historyGate = "Full version history and rollback are available on the Enterprise plan." -publish = "Publish" -rollBack = "Roll back" - [portal.assistant] close = "Close assistant" error = "Couldn't reach the assistant." @@ -7988,7 +7904,6 @@ subtitle = "Reusable input connections that feed documents into Stirling. Config title = "Sources" [portal.sources.actions] -agentBuilder = "Agent Builder" connectSource = "Connect source" [portal.sources.builder] diff --git a/frontend/editor/src/portal-saas/components/sources/AgentBuilderAction.test.tsx b/frontend/editor/src/portal-saas/components/sources/AgentBuilderAction.test.tsx deleted file mode 100644 index 2dcafa1dcf..0000000000 --- a/frontend/editor/src/portal-saas/components/sources/AgentBuilderAction.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { render } from "@testing-library/react"; -import { AgentBuilderAction } from "@portal/components/sources/AgentBuilderAction"; - -describe("AgentBuilderAction (SaaS)", () => { - it("renders nothing — Agent Builder is hidden pre-release", () => { - const { container } = render(); - expect(container).toBeEmptyDOMElement(); - }); -}); diff --git a/frontend/editor/src/portal-saas/components/sources/AgentBuilderAction.tsx b/frontend/editor/src/portal-saas/components/sources/AgentBuilderAction.tsx deleted file mode 100644 index 0576cf45e9..0000000000 --- a/frontend/editor/src/portal-saas/components/sources/AgentBuilderAction.tsx +++ /dev/null @@ -1,7 +0,0 @@ -/** - * SaaS pre-release: Agent Builder isn't shipped yet, so its Sources-header entry - * point is hidden. - */ -export function AgentBuilderAction() { - return null; -} diff --git a/frontend/editor/src/portal-saas/views/AgentBuilder.tsx b/frontend/editor/src/portal-saas/views/AgentBuilder.tsx deleted file mode 100644 index e6d4dbb15b..0000000000 --- a/frontend/editor/src/portal-saas/views/AgentBuilder.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext"; - -/** - * SaaS pre-release: Agent Builder isn't shipped yet and its Sources entry point is - * hidden, so redirect any /agent-builder deep link back to Home. - */ -export function AgentBuilder() { - return ; -} diff --git a/frontend/editor/src/portal/MOCKS.md b/frontend/editor/src/portal/MOCKS.md index 84fb8d97cd..6d66b969ee 100644 --- a/frontend/editor/src/portal/MOCKS.md +++ b/frontend/editor/src/portal/MOCKS.md @@ -62,7 +62,6 @@ non-2xx). Views consume via `useAsync()` + `useSectionFlags()` (`hooks/useAsync. | Documents | `GET /v1/documents` | `tier` | `fetchDocuments` | `DocumentsResponse` | | Pipelines | `GET /v1/pipelines` · `POST /v1/pipelines/:id/promote-to-policy` | `tier` | `fetchPipelines` · `promoteToPolicy` | `PipelinesResponse` | | Policies | `GET/POST /api/v1/policies` · `GET/DELETE /api/v1/policies/{id}` · `POST /api/v1/policies/{id}/run` | — | `fetchPolicies` · `savePolicy` · `deletePolicy` · `runPolicy` | `PoliciesResponse` · `Policy` | -| Agent Builder | `GET /v1/agents` | `tier` | `fetchAgents` | `AgentsResponse` | | Sources | `GET /v1/sources` | `tier` | `fetchSources` | `SourcesResponse` | | Components | `GET /v1/components` | `tier` | `fetchComponents` | `ComponentsResponse` | | Infrastructure | `GET /v1/infrastructure/deployments` | `tier` | `fetchDeployments` | `DeploymentsResponse` | diff --git a/frontend/editor/src/portal/ViewRouter.tsx b/frontend/editor/src/portal/ViewRouter.tsx index dc580a3a2b..67b7c146a9 100644 --- a/frontend/editor/src/portal/ViewRouter.tsx +++ b/frontend/editor/src/portal/ViewRouter.tsx @@ -7,7 +7,6 @@ import { Pipelines } from "@portal/views/Pipelines"; import { PipelineBuilder } from "@portal/views/PipelineBuilder"; import { Sources } from "@portal/views/Sources"; import { SourceBuilder } from "@portal/views/SourceBuilder"; -import { AgentBuilder } from "@portal/views/AgentBuilder"; import { Policies } from "@portal/views/Policies"; import { EditorAdmin } from "@portal/views/EditorAdmin"; import { Infrastructure } from "@portal/views/Infrastructure"; @@ -51,10 +50,6 @@ export function ViewRouter() { path={`${rel(VIEW_PATHS.sources)}/:id`} element={} /> - } - /> } /> } /> } /> diff --git a/frontend/editor/src/portal/api/agents.ts b/frontend/editor/src/portal/api/agents.ts deleted file mode 100644 index 7a0cd49464..0000000000 --- a/frontend/editor/src/portal/api/agents.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { apiClient } from "@portal/api/http"; -import type { Tier } from "@portal/contexts/TierContext"; - -/* - * An "agent" here is an AI agent that classifies, extracts from, and routes - * documents. The builder is its lifecycle surface: scenarios (named test - * cases), tool-access governance, an eval / golden set, and version history. - */ - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Domain types */ -/* ──────────────────────────────────────────────────────────────────────── */ - -export type AgentStatus = "draft" | "published"; - -/** - * Tool-access posture. `broad` lets the agent call any tool it can reach; - * `restricted` is allow-by-default minus an explicit deny list (the governance - * mode enterprise tenants use to fence agents away from sensitive tools). - */ -export type ToolMode = "broad" | "restricted"; - -/** A named test case describing expected agent behaviour for a kind of input. */ -export interface Scenario { - id: string; - name: string; - /** What the agent is expected to do for this input. */ - expectation: string; - /** Whether this scenario is currently exercised by the eval run. */ - enabled: boolean; -} - -/** A single golden-set check with its last-run outcome. */ -export interface EvalCase { - id: string; - name: string; - /** Last observed pass/fail; null when the case has never been run. */ - passing: boolean | null; - /** Mean latency of the last run in milliseconds. */ - latencyMs: number; -} - -export interface AgentVersion { - /** Display label, e.g. "v3" or "v2-draft". */ - version: string; - status: AgentStatus; - /** ISO timestamp the version was created. */ - createdAt: string; - author: string; - /** One-line change summary. */ - note: string; -} - -export interface Agent { - id: string; - name: string; - /** One-line role description shown under the name in the selector. */ - role: string; - status: AgentStatus; - /** Current working version, e.g. "v3" or "v2-draft". */ - version: string; - model: string; - scenarios: Scenario[]; - toolMode: ToolMode; - /** Tools the agent may not call when `toolMode` is "restricted". */ - deniedTools: string[]; - /** Count of golden-set cases currently passing. */ - evalsPassing: number; - /** Total golden-set cases. */ - evalsTotal: number; - evalCases: EvalCase[]; - versions: AgentVersion[]; -} - -export interface AgentsSummary { - /** Agents in the "published" state. */ - activeAgents: number; - /** Total agents regardless of status. */ - totalAgents: number; - /** Mean eval pass-rate across all agents, 0..1. */ - avgPassRate: number; - /** Total scenarios across all agents. */ - totalScenarios: number; - /** Latest published version label across the fleet, e.g. "v3". */ - latestPublished: string; -} - -export interface AgentsResponse { - summary: AgentsSummary; - agents: Agent[]; -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Presentation metadata (chip tone per status). Product copy, client-side. */ -/* ──────────────────────────────────────────────────────────────────────── */ - -export const AGENT_STATUS_TONE: Record = { - published: "success", - draft: "neutral", -}; - -export const AGENT_STATUS_LABEL: Record = { - published: "portal.agentBuilder.status.published", - draft: "portal.agentBuilder.status.draft", -}; - -/** - * Catalogue of tools an agent can be granted or denied. Surfaced as the chip - * palette in restricted mode so the deny list is picked from a known set - * rather than free-typed. - */ -export const TOOL_CATALOGUE = [ - "extract.fields", - "classify.document", - "route.pipeline", - "lookup.crm", - "send.email", - "write.audit", - "read.pii", - "invoke.webhook", -] as const; - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Endpoints */ -/* ──────────────────────────────────────────────────────────────────────── */ - -/** GET /v1/agents?tier=… — fleet summary + every agent with its full builder state. */ -export async function fetchAgents(tier: Tier): Promise { - return apiClient.local.json( - `/v1/agents?tier=${encodeURIComponent(tier)}`, - ); -} diff --git a/frontend/editor/src/portal/api/demoData.test.ts b/frontend/editor/src/portal/api/demoData.test.ts index a978703203..7a9ab50e13 100644 --- a/frontend/editor/src/portal/api/demoData.test.ts +++ b/frontend/editor/src/portal/api/demoData.test.ts @@ -12,7 +12,7 @@ describe("portal demo data seam", () => { it("is inert until enabled", async () => { expect( await resolveDemoResponse( - new URL("/v1/agents?tier=pro", window.location.origin), + new URL("/v1/notifications", window.location.origin), {}, ), ).toBeUndefined(); @@ -22,12 +22,12 @@ describe("portal demo data seam", () => { it("answers from the fixture handlers while enabled", async () => { await enablePortalDemoData(); const res = await resolveDemoResponse( - new URL("/v1/agents?tier=pro", window.location.origin), + new URL("/v1/notifications", window.location.origin), {}, ); expect(res?.status).toBe(200); - const body = (await res?.json()) as { agents: unknown[] }; - expect(body.agents.length).toBeGreaterThan(0); + const body = (await res?.json()) as unknown[]; + expect(body.length).toBeGreaterThan(0); }); it("releases back to the network on disable", async () => { @@ -35,7 +35,7 @@ describe("portal demo data seam", () => { disablePortalDemoData(); expect( await resolveDemoResponse( - new URL("/v1/agents?tier=pro", window.location.origin), + new URL("/v1/notifications", window.location.origin), {}, ), ).toBeUndefined(); diff --git a/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.stories.tsx deleted file mode 100644 index 2b096b5952..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { agentsFor } from "@portal/mocks/agents"; -import { AgentBuilderPanel } from "@portal/components/agent-builder/AgentBuilderPanel"; - -const PRO = agentsFor("pro"); - -const meta: Meta = { - title: "Portal/AgentBuilder/AgentBuilderPanel", - component: AgentBuilderPanel, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -/** Published agent with enterprise governance unlocked. */ -export const Published: Story = { - args: { agent: PRO[0], governanceUnlocked: true }, -}; - -/** Draft agent with governance locked (pro/free posture). */ -export const DraftLocked: Story = { - args: { agent: PRO[2], governanceUnlocked: false }, -}; diff --git a/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.tsx b/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.tsx deleted file mode 100644 index a411f7f680..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/AgentBuilderPanel.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { StatusBadge, Tabs, type TabItem } from "@app/ui"; -import { - type Agent, - AGENT_STATUS_LABEL, - AGENT_STATUS_TONE, -} from "@portal/api/agents"; -import { ScenariosPanel } from "@portal/components/agent-builder/ScenariosPanel"; -import { ToolsPanel } from "@portal/components/agent-builder/ToolsPanel"; -import { EvalsPanel } from "@portal/components/agent-builder/EvalsPanel"; -import { VersionsPanel } from "@portal/components/agent-builder/VersionsPanel"; -import "@portal/views/AgentBuilder.css"; - -type BuilderTab = "scenarios" | "tools" | "evals" | "versions"; - -interface AgentBuilderPanelProps { - agent: Agent; - /** Enterprise unlocks restricted-tools governance and deep version history. */ - governanceUnlocked: boolean; -} - -/** The selected agent's builder: header + tabbed Scenarios / Tools / Evals / Versions. */ -export function AgentBuilderPanel({ - agent, - governanceUnlocked, -}: AgentBuilderPanelProps) { - const { t } = useTranslation(); - const [tab, setTab] = useState("scenarios"); - - const tabs: TabItem[] = [ - { - key: "scenarios", - label: t("portal.agentBuilder.tabs.scenarios"), - count: agent.scenarios.length, - }, - { key: "tools", label: t("portal.agentBuilder.tabs.tools") }, - { - key: "evals", - label: t("portal.agentBuilder.tabs.evals"), - count: agent.evalsTotal > 0 ? agent.evalsTotal : undefined, - }, - { - key: "versions", - label: t("portal.agentBuilder.tabs.versions"), - count: agent.versions.length, - }, - ]; - - return ( -
-
-
-

{agent.name}

- {agent.role} -
-
- - {t(AGENT_STATUS_LABEL[agent.status])} - - - {agent.version} - - {agent.model} -
-
- - - items={tabs} - activeKey={tab} - onChange={setTab} - variant="underline" - ariaLabel={t("portal.agentBuilder.sectionsAriaLabel")} - /> - - {tab === "scenarios" && } - {tab === "tools" && ( - - )} - {tab === "evals" && } - {tab === "versions" && ( - - )} -
- ); -} diff --git a/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.stories.tsx b/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.stories.tsx deleted file mode 100644 index 9ab047309e..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { buildAgentsResponse } from "@portal/mocks/agents"; -import { AgentKpiStrip } from "@portal/components/agent-builder/AgentKpiStrip"; - -const meta: Meta = { - title: "Portal/AgentBuilder/AgentKpiStrip", - component: AgentKpiStrip, - parameters: { layout: "padded" }, -}; -export default meta; -type Story = StoryObj; - -export const Pro: Story = { - args: { summary: buildAgentsResponse("pro").summary, loading: false }, -}; - -export const Enterprise: Story = { - args: { summary: buildAgentsResponse("enterprise").summary, loading: false }, -}; - -export const Loading: Story = { - args: { summary: null, loading: true }, -}; diff --git a/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.tsx b/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.tsx deleted file mode 100644 index 0d3b729920..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/AgentKpiStrip.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { MetricCard, MetricStrip } from "@app/ui"; -import type { AgentsSummary } from "@portal/api/agents"; - -interface AgentKpiStripProps { - summary: AgentsSummary | null; - loading: boolean; -} - -export function AgentKpiStrip({ summary, loading }: AgentKpiStripProps) { - const { t } = useTranslation(); - - /** - * KPI labels are product copy — they describe what each metric IS, not its - * value — so the strip's structure stays stable across loading / ready - * states; only values flow from the API. - */ - const kpiLabels = [ - t("portal.agentBuilder.kpi.activeAgents"), - t("portal.agentBuilder.kpi.avgPassRate"), - t("portal.agentBuilder.kpi.scenarios"), - t("portal.agentBuilder.kpi.latestPublished"), - ] as const; - - const values: (string | number)[] = summary - ? [ - summary.activeAgents, - `${Math.round(summary.avgPassRate * 100)}%`, - summary.totalScenarios, - summary.latestPublished, - ] - : ["—", "—", "—", "—"]; - - const descriptions: (string | undefined)[] = summary - ? [ - t("portal.agentBuilder.kpi.totalDescription", { - count: summary.totalAgents, - }), - t("portal.agentBuilder.kpi.acrossGoldenSets"), - t("portal.agentBuilder.kpi.testCases"), - t("portal.agentBuilder.kpi.fleetWide"), - ] - : []; - - return ( - - {kpiLabels.map((label, i) => ( - - ))} - - ); -} diff --git a/frontend/editor/src/portal/components/agent-builder/AgentSelector.stories.tsx b/frontend/editor/src/portal/components/agent-builder/AgentSelector.stories.tsx deleted file mode 100644 index 064bd55da1..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/AgentSelector.stories.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useState } from "react"; -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { agentsFor } from "@portal/mocks/agents"; -import { AgentSelector } from "@portal/components/agent-builder/AgentSelector"; - -const AGENTS = agentsFor("enterprise"); - -const meta: Meta = { - title: "Portal/AgentBuilder/AgentSelector", - component: AgentSelector, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { agents: AGENTS, selectedId: AGENTS[0].id, onSelect: () => {} }, -}; - -/** Clicking a row moves the selection — drives which builder is shown. */ -export const Interactive: Story = { - render: () => { - const [id, setId] = useState(AGENTS[0].id); - return ; - }, -}; diff --git a/frontend/editor/src/portal/components/agent-builder/AgentSelector.tsx b/frontend/editor/src/portal/components/agent-builder/AgentSelector.tsx deleted file mode 100644 index bb01bf914b..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/AgentSelector.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { - type Agent, - AGENT_STATUS_LABEL, - AGENT_STATUS_TONE, -} from "@portal/api/agents"; -import { Button, StatusBadge } from "@app/ui"; -import "@portal/views/AgentBuilder.css"; - -interface AgentSelectorProps { - agents: Agent[]; - selectedId: string | null; - onSelect: (id: string) => void; -} - -/** Left-rail list of agents; the selected row drives which builder is shown. */ -export function AgentSelector({ - agents, - selectedId, - onSelect, -}: AgentSelectorProps) { - const { t } = useTranslation(); - return ( - - ); -} diff --git a/frontend/editor/src/portal/components/agent-builder/BootstrapDialog.stories.tsx b/frontend/editor/src/portal/components/agent-builder/BootstrapDialog.stories.tsx deleted file mode 100644 index 086ac982dc..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/BootstrapDialog.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useState } from "react"; -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Button } from "@app/ui"; -import { BootstrapDialog } from "@portal/components/agent-builder/BootstrapDialog"; - -const meta: Meta = { - title: "Portal/AgentBuilder/BootstrapDialog", - component: BootstrapDialog, - parameters: { layout: "fullscreen" }, -}; -export default meta; -type Story = StoryObj; - -/** Open by default so the dialog is visible in the canvas. */ -export const Open: Story = { - args: { open: true, onClose: () => {} }, -}; - -/** Toggled from a trigger, mirroring how the view drives it. */ -export const Triggered: Story = { - render: () => { - const [open, setOpen] = useState(false); - return ( -
- - setOpen(false)} /> -
- ); - }, -}; diff --git a/frontend/editor/src/portal/components/agent-builder/BootstrapDialog.tsx b/frontend/editor/src/portal/components/agent-builder/BootstrapDialog.tsx deleted file mode 100644 index 27b076eb69..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/BootstrapDialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import FileUploadRounded from "@mui/icons-material/FileUploadRounded"; -import { Button, Modal } from "@app/ui"; -import "@portal/views/AgentBuilder.css"; - -interface BootstrapDialogProps { - open: boolean; - onClose: () => void; -} - -/** - * Seed a new agent from a sample document — the user drops one representative - * file and the backend proposes scenarios and an extraction schema. Demo stub: - * it captures the chosen file name locally and closes without provisioning. - */ -export function BootstrapDialog({ open, onClose }: BootstrapDialogProps) { - const { t } = useTranslation(); - const [fileName, setFileName] = useState(null); - - function close() { - onClose(); - setTimeout(() => setFileName(null), 200); - } - - function bootstrap() { - // TODO(backend): POST /v1/agents/bootstrap (multipart sample document) — - // infer a starter agent (scenarios + extraction schema) from the file. - close(); - } - - return ( - - - - - } - > -
-

- {t("portal.agentBuilder.bootstrap.lead")} -

- -
-
- ); -} diff --git a/frontend/editor/src/portal/components/agent-builder/EvalsPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/EvalsPanel.stories.tsx deleted file mode 100644 index 39d1676dd1..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/EvalsPanel.stories.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { agentsFor } from "@portal/mocks/agents"; -import { EvalsPanel } from "@portal/components/agent-builder/EvalsPanel"; - -const PRO = agentsFor("pro"); -const FREE = agentsFor("free"); - -const meta: Meta = { - title: "Portal/AgentBuilder/EvalsPanel", - component: EvalsPanel, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -/** High pass-rate agent — bar and pass-rate tile read green. */ -export const Passing: Story = { - args: { agent: PRO[0] }, -}; - -/** KYC draft sits below the green band, so the pass-rate tile warns. */ -export const BelowTarget: Story = { - args: { agent: PRO[2] }, -}; - -/** Free-tier agent has no golden set — the panel shows the upgrade gate. */ -export const NoGoldenSet: Story = { - args: { agent: FREE[0] }, -}; diff --git a/frontend/editor/src/portal/components/agent-builder/EvalsPanel.tsx b/frontend/editor/src/portal/components/agent-builder/EvalsPanel.tsx deleted file mode 100644 index 4654f10635..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/EvalsPanel.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { - Button, - EmptyState, - ProgressBar, - StatTile, - StatusBadge, - Table, - type TableColumn, -} from "@app/ui"; -import type { Agent, EvalCase } from "@portal/api/agents"; -import "@portal/views/AgentBuilder.css"; - -interface EvalsPanelProps { - agent: Agent; -} - -/** Golden-set pass-rate, the per-case results table, and a run affordance. */ -export function EvalsPanel({ agent }: EvalsPanelProps) { - const { t } = useTranslation(); - - const columns: TableColumn[] = [ - { - key: "name", - header: t("portal.agentBuilder.evals.columnCase"), - render: (c) => c.name, - }, - { - key: "result", - header: t("portal.agentBuilder.evals.columnResult"), - render: (c) => - c.passing === null ? ( - - {t("portal.agentBuilder.evals.notRun")} - - ) : ( - - {c.passing - ? t("portal.agentBuilder.evals.pass") - : t("portal.agentBuilder.evals.fail")} - - ), - }, - { - key: "latency", - header: t("portal.agentBuilder.evals.columnLatency"), - align: "right", - render: (c) => ( - - {t("portal.agentBuilder.evals.latencyMs", { ms: c.latencyMs })} - - ), - }, - ]; - - if (agent.evalsTotal === 0) { - return ( -
- -
- ); - } - - const rate = agent.evalsPassing / agent.evalsTotal; - - function runEvals() { - // TODO(backend): POST /v1/agents/{id}/evals/run — kick off a golden-set - // run, then poll for the updated case results. - } - - return ( -
-
-
- = 0.95 ? "success" : rate >= 0.8 ? "warning" : "danger"} - /> - -
- -
- -
-
- {t("portal.agentBuilder.evals.goldenSetPassRate")} - {Math.round(rate * 100)}% -
- = 0.95 ? "var(--color-green)" : "var(--color-amber)"} - label={t("portal.agentBuilder.evals.goldenSetPassRate")} - /> -
- - - columns={columns} - rows={agent.evalCases} - rowKey={(c) => c.id} - /> -
- ); -} diff --git a/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.stories.tsx deleted file mode 100644 index 8ae91e390a..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { agentsFor } from "@portal/mocks/agents"; -import { ScenariosPanel } from "@portal/components/agent-builder/ScenariosPanel"; - -const AGENTS = agentsFor("pro"); - -const meta: Meta = { - title: "Portal/AgentBuilder/ScenariosPanel", - component: ScenariosPanel, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -/** Contract Router ships three scenarios; the add-row stages a fourth locally. */ -export const Default: Story = { - args: { agent: AGENTS[0] }, -}; - -/** An agent with a muted scenario (excluded from the eval run). */ -export const WithMutedScenario: Story = { - args: { agent: AGENTS[1] }, -}; diff --git a/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.tsx b/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.tsx deleted file mode 100644 index d3a2822838..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/ScenariosPanel.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Button, Chip, FormField, Input, StatusBadge } from "@app/ui"; -import type { Agent, Scenario } from "@portal/api/agents"; -import "@portal/views/AgentBuilder.css"; - -interface ScenariosPanelProps { - agent: Agent; -} - -/** - * Named test cases describing expected behaviour. Edits live in local state — - * the surface is mock-driven, so adding a row only stages it client-side until - * the submit endpoint exists. - */ -export function ScenariosPanel({ agent }: ScenariosPanelProps) { - const { t } = useTranslation(); - // Seed from the agent and re-seed when the selection changes (key prop on the - // builder forces a remount, so a plain useState initialiser is enough). - const [scenarios, setScenarios] = useState(agent.scenarios); - const [name, setName] = useState(""); - const [expectation, setExpectation] = useState(""); - - const canAdd = name.trim() !== "" && expectation.trim() !== ""; - - function addScenario() { - if (!canAdd) return; - const next: Scenario = { - id: `sc-local-${Date.now()}`, - name: name.trim(), - expectation: expectation.trim(), - enabled: true, - }; - // TODO(backend): POST /v1/agents/{id}/scenarios { name, expectation } — - // persist the scenario, then replace the optimistic row with the response. - setScenarios((cur) => [...cur, next]); - setName(""); - setExpectation(""); - } - - function toggleEnabled(id: string) { - setScenarios((cur) => - cur.map((s) => (s.id === id ? { ...s, enabled: !s.enabled } : s)), - ); - } - - return ( -
-
    - {scenarios.map((s) => ( -
  • -
    -
    - {s.name} - - {s.enabled - ? t("portal.agentBuilder.scenarios.inEval") - : t("portal.agentBuilder.scenarios.muted")} - -
    - - {s.expectation} - -
    - -
  • - ))} -
- -
- - {t("portal.agentBuilder.scenarios.addScenario")} - -
- - setName(e.target.value)} - placeholder={t("portal.agentBuilder.scenarios.namePlaceholder")} - /> - - - setExpectation(e.target.value)} - placeholder={t( - "portal.agentBuilder.scenarios.expectationPlaceholder", - )} - /> - - -
-
-
- ); -} diff --git a/frontend/editor/src/portal/components/agent-builder/ToolsPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/ToolsPanel.stories.tsx deleted file mode 100644 index ed0de96ff9..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/ToolsPanel.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { agentsFor } from "@portal/mocks/agents"; -import { ToolsPanel } from "@portal/components/agent-builder/ToolsPanel"; - -const AGENTS = agentsFor("enterprise"); - -const meta: Meta = { - title: "Portal/AgentBuilder/ToolsPanel", - component: ToolsPanel, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -/** Restricted mode with a deny list — the enterprise governance posture. */ -export const Restricted: Story = { - args: { agent: AGENTS[0], governanceUnlocked: true }, -}; - -/** Broad access — every tool callable. */ -export const Broad: Story = { - args: { agent: AGENTS[1], governanceUnlocked: true }, -}; - -/** Governance locked: the toggle is disabled and explains the upgrade. */ -export const GovernanceLocked: Story = { - args: { agent: AGENTS[0], governanceUnlocked: false }, -}; diff --git a/frontend/editor/src/portal/components/agent-builder/ToolsPanel.tsx b/frontend/editor/src/portal/components/agent-builder/ToolsPanel.tsx deleted file mode 100644 index 6c0d3030ac..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/ToolsPanel.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Chip, ToggleSwitch } from "@app/ui"; -import { type Agent, type ToolMode, TOOL_CATALOGUE } from "@portal/api/agents"; -import "@portal/views/AgentBuilder.css"; - -interface ToolsPanelProps { - agent: Agent; - /** Restricted-tools governance is an enterprise capability. */ - governanceUnlocked: boolean; -} - -/** - * Tool-access posture. `broad` grants every tool; `restricted` is allow-by- - * default minus an explicit deny list, picked from the known tool catalogue. - */ -export function ToolsPanel({ agent, governanceUnlocked }: ToolsPanelProps) { - const { t } = useTranslation(); - const [mode, setMode] = useState(agent.toolMode); - const [denied, setDenied] = useState(agent.deniedTools); - - function setRestricted(on: boolean) { - // TODO(backend): PATCH /v1/agents/{id}/tools { mode, deniedTools } — - // persist the access posture. - setMode(on ? "restricted" : "broad"); - } - - function toggleDenied(tool: string) { - setDenied((cur) => - cur.includes(tool) ? cur.filter((t) => t !== tool) : [...cur, tool], - ); - } - - const restricted = mode === "restricted"; - - return ( -
-
- - - {restricted - ? t("portal.agentBuilder.tools.restricted") - : t("portal.agentBuilder.tools.broadAccess")} - -
- - {restricted && ( -
- - {t("portal.agentBuilder.tools.deniedTools")} - -

- {t("portal.agentBuilder.tools.deniedHint")} -

-
- {TOOL_CATALOGUE.map((tool) => { - const isDenied = denied.includes(tool); - return ( - toggleDenied(tool) : undefined - } - > - {isDenied ? "✕ " : ""} - {tool} - - ); - })} -
-
- )} -
- ); -} diff --git a/frontend/editor/src/portal/components/agent-builder/VersionsPanel.stories.tsx b/frontend/editor/src/portal/components/agent-builder/VersionsPanel.stories.tsx deleted file mode 100644 index d15fdac6e0..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/VersionsPanel.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { agentsFor } from "@portal/mocks/agents"; -import { VersionsPanel } from "@portal/components/agent-builder/VersionsPanel"; - -const PRO = agentsFor("pro"); - -const meta: Meta = { - title: "Portal/AgentBuilder/VersionsPanel", - component: VersionsPanel, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -/** Full history with rollback on prior published versions. */ -export const FullHistory: Story = { - args: { agent: PRO[0], historyUnlocked: true }, -}; - -/** A draft current version offers a publish action. */ -export const DraftAwaitingPublish: Story = { - args: { agent: PRO[2], historyUnlocked: true }, -}; - -/** History locked: only the current version shows, with an upgrade hint. */ -export const HistoryLocked: Story = { - args: { agent: PRO[0], historyUnlocked: false }, -}; diff --git a/frontend/editor/src/portal/components/agent-builder/VersionsPanel.tsx b/frontend/editor/src/portal/components/agent-builder/VersionsPanel.tsx deleted file mode 100644 index 3b63fe3045..0000000000 --- a/frontend/editor/src/portal/components/agent-builder/VersionsPanel.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Button, StatusBadge } from "@app/ui"; -import { - type Agent, - AGENT_STATUS_LABEL, - AGENT_STATUS_TONE, -} from "@portal/api/agents"; -import "@portal/views/AgentBuilder.css"; - -interface VersionsPanelProps { - agent: Agent; - /** Deep version history is an enterprise capability; lower tiers see the gate. */ - historyUnlocked: boolean; -} - -/** Render an ISO timestamp as a short, locale-stable date. */ -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }); -} - -/** Version history with publish / rollback actions per row. */ -export function VersionsPanel({ agent, historyUnlocked }: VersionsPanelProps) { - const { t } = useTranslation(); - // Without governance, only the current version is meaningful to show. - const versions = historyUnlocked - ? agent.versions - : agent.versions.slice(0, 1); - const publishedExists = agent.versions.some((v) => v.status === "published"); - - function publish(version: string) { - void version; - // TODO(backend): POST /v1/agents/{id}/versions/{v}/publish — promote the - // draft to published and demote the previous published version. - } - - function rollback(version: string) { - void version; - // TODO(backend): POST /v1/agents/{id}/versions/{v}/rollback — re-publish a - // prior version as the active one. - } - - return ( -
-
    - {versions.map((v) => { - const isCurrent = v.version === agent.version; - return ( -
  1. -
    -
    - - {v.version} - - - {t(AGENT_STATUS_LABEL[v.status])} - - {isCurrent && ( - - {t("portal.agentBuilder.versions.current")} - - )} -
    - {v.note} - - {formatDate(v.createdAt)} · {v.author} - -
    -
    - {v.status === "draft" && ( - - )} - {v.status === "published" && !isCurrent && ( - - )} -
    -
  2. - ); - })} -
- - {!historyUnlocked && publishedExists && ( -

- {t("portal.agentBuilder.versions.historyGate")} -

- )} -
- ); -} diff --git a/frontend/editor/src/portal/components/icons.tsx b/frontend/editor/src/portal/components/icons.tsx index ed8e6454ff..d60f86646e 100644 --- a/frontend/editor/src/portal/components/icons.tsx +++ b/frontend/editor/src/portal/components/icons.tsx @@ -221,18 +221,6 @@ export function PoliciesIcon(props: IconProps) { ); } -export function AgentBuilderIcon(props: IconProps) { - return ( - - - - - - - - ); -} - export function ProcurementIcon(props: IconProps) { return ( diff --git a/frontend/editor/src/portal/components/sources/AgentBuilderAction.tsx b/frontend/editor/src/portal/components/sources/AgentBuilderAction.tsx deleted file mode 100644 index 5e3885e5fc..0000000000 --- a/frontend/editor/src/portal/components/sources/AgentBuilderAction.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Button } from "@app/ui"; -import { useView } from "@portal/contexts/ViewContext"; -import { AgentBuilderIcon } from "@portal/components/icons"; - -/** - * The "Agent Builder" action in the Sources header. A flavor seam: the SaaS build - * shadows this with a no-op (Agent Builder isn't shipped there yet). - */ -export function AgentBuilderAction() { - const { t } = useTranslation(); - const { setActiveView } = useView(); - return ( - - ); -} diff --git a/frontend/editor/src/portal/contexts/ViewContext.tsx b/frontend/editor/src/portal/contexts/ViewContext.tsx index 3c9ebc9fa8..db3a64ee40 100644 --- a/frontend/editor/src/portal/contexts/ViewContext.tsx +++ b/frontend/editor/src/portal/contexts/ViewContext.tsx @@ -7,7 +7,6 @@ export type ViewId = | "editor" | "users" | "sources" - | "agent-builder" | "policies" | "pipelines" | "documents" @@ -22,7 +21,6 @@ export const VIEW_LABELS: Record = { editor: "Editor", users: "Users", sources: "Sources", - "agent-builder": "Agent Builder", policies: "Policies", pipelines: "Pipelines", documents: "Documents", @@ -38,7 +36,6 @@ export const VIEW_PATHS: Record = { editor: "/editor", users: "/users", sources: "/sources", - "agent-builder": "/agent-builder", policies: "/policies", pipelines: "/pipelines", documents: "/documents", diff --git a/frontend/editor/src/portal/mocks/agents.ts b/frontend/editor/src/portal/mocks/agents.ts deleted file mode 100644 index 6ff7b14910..0000000000 --- a/frontend/editor/src/portal/mocks/agents.ts +++ /dev/null @@ -1,399 +0,0 @@ -/** - * Agent Builder fixtures. Types live in api/agents.ts (the backend contract); - * this module only builds fake data for Storybook and tests. - * - * An "agent" here is an AI agent that classifies, extracts from, and routes - * documents. The builder is its lifecycle surface: scenarios (named test - * cases), tool-access governance, an eval / golden set, and version history. - */ - -import type { Tier } from "@portal/contexts/TierContext"; -import type { Agent, AgentsResponse, AgentsSummary } from "@portal/api/agents"; - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Fixture builders */ -/* ──────────────────────────────────────────────────────────────────────── */ - -const CONTRACT_ROUTER: Agent = { - id: "agent-contract-router", - name: "Contract Router", - role: "Classifies inbound contracts and routes to the right review queue", - status: "published", - version: "v3", - model: "claude-opus-4.1", - toolMode: "restricted", - deniedTools: ["send.email", "read.pii"], - evalsPassing: 23, - evalsTotal: 24, - scenarios: [ - { - id: "sc-cr-1", - name: "New customer file", - expectation: "Route to onboarding queue and tag as net-new account", - enabled: true, - }, - { - id: "sc-cr-2", - name: "Renewal with price change", - expectation: "Flag the delta and route to legal for redline review", - enabled: true, - }, - { - id: "sc-cr-3", - name: "Non-standard liability clause", - expectation: "Escalate to senior counsel rather than auto-approving", - enabled: true, - }, - ], - evalCases: [ - { - id: "ec-cr-1", - name: "MSA — standard terms", - passing: true, - latencyMs: 1840, - }, - { - id: "ec-cr-2", - name: "MSA — capped liability", - passing: true, - latencyMs: 2010, - }, - { - id: "ec-cr-3", - name: "Order form — multi-year", - passing: true, - latencyMs: 1620, - }, - { - id: "ec-cr-4", - name: "DPA — cross-border transfer", - passing: false, - latencyMs: 2440, - }, - ], - versions: [ - { - version: "v3", - status: "published", - createdAt: "2026-05-28T14:12:00Z", - author: "legal-ops@acme.com", - note: "Added cross-border DPA detection", - }, - { - version: "v2", - status: "published", - createdAt: "2026-04-09T09:30:00Z", - author: "legal-ops@acme.com", - note: "Tightened renewal price-change routing", - }, - { - version: "v1", - status: "published", - createdAt: "2026-02-15T16:45:00Z", - author: "you@acme.com", - note: "Initial publish", - }, - ], -}; - -const INVOICE_AGENT: Agent = { - id: "agent-invoice", - name: "Invoice Agent", - role: "Extracts line items and routes invoices into the AP pipeline", - status: "published", - version: "v5", - model: "claude-sonnet-4.5", - toolMode: "broad", - deniedTools: [], - evalsPassing: 31, - evalsTotal: 32, - scenarios: [ - { - id: "sc-inv-1", - name: "Multi-page invoice", - expectation: "Extract all line items across pages into one record", - enabled: true, - }, - { - id: "sc-inv-2", - name: "Foreign currency total", - expectation: "Preserve original currency and attach FX note", - enabled: true, - }, - { - id: "sc-inv-3", - name: "Missing PO number", - expectation: "Route to manual matching rather than auto-posting", - enabled: false, - }, - ], - evalCases: [ - { id: "ec-inv-1", name: "Single-page USD", passing: true, latencyMs: 980 }, - { id: "ec-inv-2", name: "Multi-page EUR", passing: true, latencyMs: 1320 }, - { - id: "ec-inv-3", - name: "Handwritten total", - passing: true, - latencyMs: 1510, - }, - { - id: "ec-inv-4", - name: "No PO reference", - passing: false, - latencyMs: 1100, - }, - ], - versions: [ - { - version: "v5", - status: "published", - createdAt: "2026-06-01T11:00:00Z", - author: "platform@acme.com", - note: "FX note extraction for foreign-currency invoices", - }, - { - version: "v4", - status: "published", - createdAt: "2026-05-10T08:20:00Z", - author: "platform@acme.com", - note: "Multi-page line-item stitching", - }, - ], -}; - -const KYC_AGENT: Agent = { - id: "agent-kyc", - name: "KYC Agent", - role: "Verifies identity documents and flags compliance gaps", - status: "draft", - version: "v2-draft", - model: "claude-sonnet-4.5", - toolMode: "restricted", - deniedTools: ["send.email", "invoke.webhook"], - evalsPassing: 14, - evalsTotal: 20, - scenarios: [ - { - id: "sc-kyc-1", - name: "KYC question", - expectation: "Answer from the verified document set, never invent facts", - enabled: true, - }, - { - id: "sc-kyc-2", - name: "Expired identity document", - expectation: "Mark verification as failed and request a fresh document", - enabled: true, - }, - { - id: "sc-kyc-3", - name: "Compliance escalation", - expectation: "Escalate sanctions-list hits to the compliance officer", - enabled: true, - }, - ], - evalCases: [ - { id: "ec-kyc-1", name: "Valid passport", passing: true, latencyMs: 2200 }, - { id: "ec-kyc-2", name: "Expired licence", passing: true, latencyMs: 2050 }, - { - id: "ec-kyc-3", - name: "Sanctions-list match", - passing: false, - latencyMs: 2600, - }, - { - id: "ec-kyc-4", - name: "Mismatched name fields", - passing: false, - latencyMs: 2310, - }, - ], - versions: [ - { - version: "v2-draft", - status: "draft", - createdAt: "2026-06-12T13:40:00Z", - author: "risk@acme.com", - note: "Sanctions-list escalation path (in review)", - }, - { - version: "v1", - status: "published", - createdAt: "2026-03-22T10:15:00Z", - author: "risk@acme.com", - note: "Initial publish", - }, - ], -}; - -/** - * Enterprise-only agent — governance-heavy and exercised against a larger - * golden set than the base tenants get, so it only appears for that tier. - */ -const COMPLIANCE_SWEEP: Agent = { - id: "agent-compliance-sweep", - name: "Compliance Sweep", - role: "Audits processed documents against retention and PII policy", - status: "published", - version: "v4", - model: "claude-opus-4.1", - toolMode: "restricted", - deniedTools: ["send.email", "route.pipeline", "invoke.webhook"], - evalsPassing: 58, - evalsTotal: 60, - scenarios: [ - { - id: "sc-cs-1", - name: "Over-retention detection", - expectation: "Flag documents held past their retention window", - enabled: true, - }, - { - id: "sc-cs-2", - name: "Unredacted PII", - expectation: "Quarantine and write an audit entry, never auto-delete", - enabled: true, - }, - ], - evalCases: [ - { - id: "ec-cs-1", - name: "Expired retention", - passing: true, - latencyMs: 3100, - }, - { id: "ec-cs-2", name: "SSN in free text", passing: true, latencyMs: 2900 }, - { - id: "ec-cs-3", - name: "Redacted re-share", - passing: true, - latencyMs: 2700, - }, - { - id: "ec-cs-4", - name: "Cross-region copy", - passing: false, - latencyMs: 3300, - }, - ], - versions: [ - { - version: "v4", - status: "published", - createdAt: "2026-06-03T15:05:00Z", - author: "compliance@acme.com", - note: "Cross-region copy detection", - }, - { - version: "v3", - status: "published", - createdAt: "2026-05-01T12:00:00Z", - author: "compliance@acme.com", - note: "Retention-window policy refresh", - }, - { - version: "v2", - status: "published", - createdAt: "2026-03-14T09:00:00Z", - author: "compliance@acme.com", - note: "PII quarantine workflow", - }, - { - version: "v1", - status: "published", - createdAt: "2026-01-20T08:00:00Z", - author: "you@acme.com", - note: "Initial publish", - }, - ], -}; - -/** - * On the free tier a single starter agent ships as a draft so the surface has - * something to render, but with an empty golden set and no version history — - * evals and version governance are paid capabilities. - */ -const STARTER_AGENT: Agent = { - id: "agent-starter", - name: "Starter Classifier", - role: "Sorts uploads into a handful of document types", - status: "draft", - version: "v1-draft", - model: "claude-haiku-4.5", - toolMode: "broad", - deniedTools: [], - evalsPassing: 0, - evalsTotal: 0, - scenarios: [ - { - id: "sc-st-1", - name: "Unknown document type", - expectation: "Fall back to the inbox queue instead of guessing", - enabled: true, - }, - ], - evalCases: [], - versions: [ - { - version: "v1-draft", - status: "draft", - createdAt: "2026-06-14T17:30:00Z", - author: "you@acme.com", - note: "Draft — not yet published", - }, - ], -}; - -const PRO_AGENTS: Agent[] = [CONTRACT_ROUTER, INVOICE_AGENT, KYC_AGENT]; - -/** - * Trim an agent to the free-tier shape: drop the eval golden set and collapse - * version history to the current version, mirroring the capability gating the - * KPI strip advertises. - */ -function toFreeShape(agent: Agent): Agent { - return { - ...agent, - status: "draft", - evalsPassing: 0, - evalsTotal: 0, - evalCases: [], - versions: agent.versions.slice(0, 1), - }; -} - -/** Agents for a given tier. */ -export function agentsFor(tier: Tier): Agent[] { - if (tier === "free") return [toFreeShape(STARTER_AGENT)]; - if (tier === "enterprise") return [...PRO_AGENTS, COMPLIANCE_SWEEP]; - return PRO_AGENTS; -} - -function summaryFor(agents: Agent[]): AgentsSummary { - const published = agents.filter((a) => a.status === "published"); - const totalScenarios = agents.reduce((n, a) => n + a.scenarios.length, 0); - - // Pass-rate is meaningless without a golden set, so average only over agents - // that actually have eval cases — free tenants land at 0. - const withEvals = agents.filter((a) => a.evalsTotal > 0); - const avgPassRate = - withEvals.length === 0 - ? 0 - : withEvals.reduce((sum, a) => sum + a.evalsPassing / a.evalsTotal, 0) / - withEvals.length; - - const latestPublished = published.length > 0 ? published[0].version : "—"; - - return { - activeAgents: published.length, - totalAgents: agents.length, - avgPassRate, - totalScenarios, - latestPublished, - }; -} - -export function buildAgentsResponse(tier: Tier): AgentsResponse { - const agents = agentsFor(tier); - return { summary: summaryFor(agents), agents }; -} diff --git a/frontend/editor/src/portal/mocks/handlers/agents.ts b/frontend/editor/src/portal/mocks/handlers/agents.ts deleted file mode 100644 index a5b054c57e..0000000000 --- a/frontend/editor/src/portal/mocks/handlers/agents.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { http, HttpResponse, delay } from "msw"; -import type { Tier } from "@portal/contexts/TierContext"; -import { buildAgentsResponse } from "@portal/mocks/agents"; - -export const agentsHandlers = [ - http.get("/v1/agents", async ({ request }) => { - await delay(120); - const url = new URL(request.url); - const tier = (url.searchParams.get("tier") ?? "pro") as Tier; - return HttpResponse.json(buildAgentsResponse(tier)); - }), -]; diff --git a/frontend/editor/src/portal/mocks/handlers/index.ts b/frontend/editor/src/portal/mocks/handlers/index.ts index 29d7415c61..8163c10e4d 100644 --- a/frontend/editor/src/portal/mocks/handlers/index.ts +++ b/frontend/editor/src/portal/mocks/handlers/index.ts @@ -10,7 +10,6 @@ import { procurementSaasHandlers } from "@portal/mocks/handlers/procurementSaas" import { docsHandlers } from "@portal/mocks/handlers/docs"; import { usersHandlers } from "@portal/mocks/handlers/users"; import { teamSaasHandlers } from "@portal/mocks/handlers/teamSaas"; -import { agentsHandlers } from "@portal/mocks/handlers/agents"; import { policiesHandlers } from "@portal/mocks/handlers/policies"; import { classificationHandlers } from "@portal/mocks/handlers/classification"; import { documentsHandlers } from "@portal/mocks/handlers/documents"; @@ -30,7 +29,6 @@ export const handlers = [ ...procurementSaasHandlers, ...usersHandlers, ...teamSaasHandlers, - ...agentsHandlers, ...policiesHandlers, ...classificationHandlers, ...documentsHandlers, diff --git a/frontend/editor/src/portal/views/AgentBuilder.css b/frontend/editor/src/portal/views/AgentBuilder.css deleted file mode 100644 index e4be384bf9..0000000000 --- a/frontend/editor/src/portal/views/AgentBuilder.css +++ /dev/null @@ -1,458 +0,0 @@ -.portal-agents { - display: flex; - flex-direction: column; - gap: 1.25rem; - padding: 1.5rem; - max-width: 84rem; - margin: 0 auto; -} - -/* Header */ -.portal-agents__head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; -} - -.portal-agents__title { - margin: 0; - font-size: 1.375rem; - font-weight: 650; - color: var(--color-text-1); -} - -.portal-agents__sub { - margin: 0.25rem 0 0; - font-size: 0.8125rem; - line-height: 1.5; - color: var(--color-text-3); - max-width: 46rem; -} - -/* Two-column layout: selector rail + builder */ -.portal-agents__layout { - display: grid; - grid-template-columns: 17rem 1fr; - gap: 1.25rem; - align-items: start; -} - -@media (max-width: 56rem) { - .portal-agents__layout { - grid-template-columns: 1fr; - } -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Selector rail */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-agents__selector { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.portal-agents__selector-item { - display: flex; - flex-direction: column; - gap: 0.5rem; - text-align: left; - padding: 0.75rem 0.875rem; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - cursor: pointer; - transition: - border-color var(--motion-fast), - background var(--motion-fast); -} - -.portal-agents__selector-item:hover { - background: var(--color-bg-hover); -} - -.portal-agents__selector-item.is-selected { - border-color: var(--color-blue); - background: var(--color-blue-light); - box-shadow: 0 0 0 1px var(--color-blue) inset; -} - -.portal-agents__selector-main { - display: flex; - flex-direction: column; - gap: 0.25rem; - min-width: 0; -} - -.portal-agents__selector-name { - font-size: 0.8125rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-agents__selector-role { - font-size: 0.6875rem; - line-height: 1.4; - color: var(--color-text-4); -} - -.portal-agents__selector-meta { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.portal-agents__selector-version, -.portal-agents__builder-version, -.portal-agents__builder-model, -.portal-agents__version-tag { - font-family: var(--font-mono); - font-size: 0.6875rem; - color: var(--color-text-3); -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Builder panel */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-agents__builder { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-sm); - padding: 1.25rem; - display: flex; - flex-direction: column; - gap: 1.125rem; -} - -.portal-agents__builder-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; -} - -.portal-agents__builder-title { - margin: 0; - font-size: 1rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-agents__builder-sub { - font-size: 0.75rem; - color: var(--color-text-4); -} - -.portal-agents__builder-meta { - display: flex; - align-items: center; - gap: 0.625rem; - flex-shrink: 0; -} - -.portal-agents__builder-model { - background: var(--color-bg-subtle); - border: 1px solid var(--color-border-light); - border-radius: var(--radius-md); - padding: 0.125rem 0.375rem; -} - -/* Shared panel body */ -.portal-agents__panel { - display: flex; - flex-direction: column; - gap: 1.125rem; -} - -.portal-agents__detail-section { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.portal-agents__detail-heading { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--color-text-5); - font-weight: 600; -} - -.portal-agents__chips { - display: flex; - flex-wrap: wrap; - gap: 0.375rem; -} - -.portal-agents__hint { - margin: 0; - font-size: 0.75rem; - color: var(--color-text-4); -} - -.portal-agents__muted { - font-size: 0.8125rem; - color: var(--color-text-4); -} - -.portal-agents__mono { - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--color-text-2); -} - -.portal-agents__stat-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 0.875rem; -} - -.portal-agents__stat-grid--two { - grid-template-columns: repeat(2, minmax(0, 14rem)); -} - -/* Bars (eval pass-rate) */ -.portal-agents__bar-row { - display: flex; - flex-direction: column; - gap: 0.4375rem; -} - -.portal-agents__bar-head { - display: flex; - align-items: baseline; - justify-content: space-between; - font-size: 0.75rem; - color: var(--color-text-4); -} - -.portal-agents__bar-head strong { - font-size: 0.8125rem; - color: var(--color-text-1); - font-family: var(--font-mono); -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Scenarios */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-agents__scenarios { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.portal-agents__scenario { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - padding: 0.75rem 0.875rem; - background: var(--color-bg-subtle); - border: 1px solid var(--color-border-light); - border-radius: var(--radius-md); -} - -.portal-agents__scenario-text { - display: flex; - flex-direction: column; - gap: 0.25rem; - min-width: 0; -} - -.portal-agents__scenario-head { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.portal-agents__scenario-head strong { - font-size: 0.8125rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-agents__scenario-expect { - font-size: 0.75rem; - line-height: 1.45; - color: var(--color-text-3); -} - -.portal-agents__scenario-add { - display: flex; - flex-direction: column; - gap: 0.625rem; - padding-top: 0.25rem; - border-top: 1px dashed var(--color-border-light); -} - -.portal-agents__scenario-form { - display: grid; - grid-template-columns: 1fr 1.5fr auto; - align-items: end; - gap: 0.625rem; -} - -@media (max-width: 48rem) { - .portal-agents__scenario-form { - grid-template-columns: 1fr; - } -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Tools */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-agents__tool-mode { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.875rem 1rem; - background: var(--color-bg-subtle); - border: 1px solid var(--color-border-light); - border-radius: var(--radius-md); -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Evals */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-agents__eval-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - flex-wrap: wrap; -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Versions */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-agents__versions { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.portal-agents__version { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - padding: 0.75rem 0.875rem; - background: var(--color-bg-subtle); - border: 1px solid var(--color-border-light); - border-radius: var(--radius-md); -} - -.portal-agents__version-main { - display: flex; - flex-direction: column; - gap: 0.25rem; - min-width: 0; -} - -.portal-agents__version-head { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.portal-agents__version-tag { - font-size: 0.8125rem; - color: var(--color-text-1); - font-weight: 600; -} - -.portal-agents__version-note { - font-size: 0.8125rem; - color: var(--color-text-2); -} - -.portal-agents__version-meta { - font-size: 0.6875rem; - color: var(--color-text-5); -} - -.portal-agents__version-actions { - flex-shrink: 0; -} - -/* ──────────────────────────────────────────────────────────────────────── */ -/* Bootstrap dialog */ -/* ──────────────────────────────────────────────────────────────────────── */ - -.portal-agents__bootstrap { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.portal-agents__bootstrap-lead { - margin: 0; - font-size: 0.875rem; - line-height: 1.55; - color: var(--color-text-2); -} - -.portal-agents__dropzone { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - padding: 1.75rem 1rem; - text-align: center; - background: var(--color-bg-subtle); - border: 1px dashed var(--color-border); - border-radius: var(--radius-lg); - cursor: pointer; - transition: - border-color var(--motion-fast), - background var(--motion-fast); -} - -.portal-agents__dropzone:hover { - border-color: var(--color-blue); - background: var(--color-blue-light); -} - -.portal-agents__dropzone-input { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -.portal-agents__dropzone-icon { - font-size: 1.5rem; - color: var(--color-text-3); -} - -.portal-agents__dropzone-text { - font-size: 0.8125rem; - color: var(--color-text-3); -} - -.portal-agents__dialog-footer { - display: flex; - justify-content: space-between; - gap: 0.5rem; - width: 100%; -} diff --git a/frontend/editor/src/portal/views/AgentBuilder.tsx b/frontend/editor/src/portal/views/AgentBuilder.tsx deleted file mode 100644 index 30a8f5e14e..0000000000 --- a/frontend/editor/src/portal/views/AgentBuilder.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import FileUploadRounded from "@mui/icons-material/FileUploadRounded"; -import { Button, EmptyState, Skeleton } from "@app/ui"; -import { useTier } from "@portal/contexts/TierContext"; -import { useAsync, useSectionFlags } from "@portal/hooks/useAsync"; -import { fetchAgents, type AgentsResponse } from "@portal/api/agents"; -import { AgentKpiStrip } from "@portal/components/agent-builder/AgentKpiStrip"; -import { AgentSelector } from "@portal/components/agent-builder/AgentSelector"; -import { AgentBuilderPanel } from "@portal/components/agent-builder/AgentBuilderPanel"; -import { BootstrapDialog } from "@portal/components/agent-builder/BootstrapDialog"; -import "@portal/views/AgentBuilder.css"; - -export function AgentBuilder() { - const { t } = useTranslation(); - const { tier } = useTier(); - const state = useAsync(() => fetchAgents(tier), [tier]); - const { data, loading } = state; - const { isLoading, isEmpty } = useSectionFlags(state); - - const [selectedId, setSelectedId] = useState(null); - const [bootstrapOpen, setBootstrapOpen] = useState(false); - - const agents = data?.agents ?? []; - // Default the selection to the first agent once data lands, but honour an - // explicit pick. Recomputed each render so a tier change re-points cleanly. - const selected = agents.find((a) => a.id === selectedId) ?? agents[0] ?? null; - - // Enterprise unlocks restricted-tools governance and deep version history. - const governanceUnlocked = tier === "enterprise"; - - return ( -
-
-
-

- {t("portal.agentBuilder.title")} -

-

- {t("portal.agentBuilder.subtitle")} -

-
- -
- - - - {isLoading && ( -
-
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
-
- -
-
- )} - - {isEmpty && ( - setBootstrapOpen(true)}> - {t("portal.agentBuilder.bootstrapFromDocument")} - - } - /> - )} - - {!isLoading && !isEmpty && selected && ( -
- - -
- )} - - setBootstrapOpen(false)} - /> -
- ); -} diff --git a/frontend/editor/src/portal/views/Sources.test.tsx b/frontend/editor/src/portal/views/Sources.test.tsx index a006cbe4a2..2bdda1b64b 100644 --- a/frontend/editor/src/portal/views/Sources.test.tsx +++ b/frontend/editor/src/portal/views/Sources.test.tsx @@ -31,11 +31,6 @@ vi.mock("@portal/api/integrations", () => ({ deleteIntegration: vi.fn(), })); -// The Agent Builder header action is a flavor seam; stub it to keep the test focused. -vi.mock("@portal/components/sources/AgentBuilderAction", () => ({ - AgentBuilderAction: () => null, -})); - const RESPONSE: SourcesResponse = { kpis: [ { value: 1, description: "" }, diff --git a/frontend/editor/src/portal/views/Sources.tsx b/frontend/editor/src/portal/views/Sources.tsx index fb0b14ccf5..385a0ab121 100644 --- a/frontend/editor/src/portal/views/Sources.tsx +++ b/frontend/editor/src/portal/views/Sources.tsx @@ -10,7 +10,6 @@ import { type SourceView, } from "@portal/api/sources"; import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext"; -import { AgentBuilderAction } from "@portal/components/sources/AgentBuilderAction"; import { KpiStrip } from "@portal/components/sources/KpiStrip"; import { SourcesTable } from "@portal/components/sources/SourcesTable"; import { ConnectionsTab } from "@portal/components/sources/ConnectionsTab"; @@ -55,7 +54,6 @@ export function Sources() { {activeTab === "sources" && (
-