Skip to content

Agent chat for policy builder UI#7993

Merged
lucanovera merged 22 commits intomainfrom
ENG-2959-FE-Astralis-agent-chat-for-policy-building
Apr 23, 2026
Merged

Agent chat for policy builder UI#7993
lucanovera merged 22 commits intomainfrom
ENG-2959-FE-Astralis-agent-chat-for-policy-building

Conversation

@lucanovera
Copy link
Copy Markdown
Contributor

@lucanovera lucanovera commented Apr 22, 2026

Ticket ENG-2959

Description Of Changes

Adds an AI agent chat panel next to the Access Policy builder. When enabled, users describe a policy in natural language and the agent proposes YAML (and a set of controls) that re-hydrates into the canvas and code editor. First UI surface for the Astralis policy-building assistant.

Note: This is only the basic implementation of the chat UI. There is a followup ticket to improve the experience adding transitions and animations to the visual builder when an agent makes edits.

The chat UI is built on @ant-design/x and sits inside a resizable Splitter panel beside the existing Builder / Split / Code tabs. The panel is gated on the existing server-wide LLM capability flag — no new config key was introduced.

Along with the feature itself, this PR picks up two dependency-hygiene changes that were required to make @ant-design/x components inherit the app's antd theme. Both are also fixes for pre-existing dual antd module instance and dual cssinjs module instance pitfalls that fidesui has a comment about, but that weren't enforced end-to-end:

  • Bumped @ant-design/cssinjs from ^1.21.0^2.1.2 in admin-ui. The v1 pin was holding the top-level node_modules slot, forcing antd and @ant-design/x to each install their own nested v2 copy. Different module identities meant different StyleContext React contexts and broken theme inheritance. The three APIs used in _document.tsx (createCache, extractStyle, StyleProvider) are backward-compatible in v2, so no code change was needed there.
  • Aliased "antd""antd/lib" in both Turbopack and webpack. fidesui re-exports antd from "antd/lib" (CJS) deliberately (see the existing comment in fidesui/src/index.ts), but third-party packages like @ant-design/x import plain "antd", which the bundler resolves to the ESM build. That produced two ConfigContext identities and useToken() inside Bubble/Sender fell back to antd defaults (#1677ff). The alias ensures every "antd" import resolves to the single CJS instance.

Other notable details:

  • Feature gate reuses detection_discovery.llm_classifier_enabled, which is already exposed on /api/v1/config and already used by LlmModelSelector as the "LLM is available on this server" signal. Parity with existing LLM features (classifier, privacy assessment chat, IDP/website monitor).
  • @ant-design/x is now a fidesui dependency. Bubble, Sender, and BubbleItemType are re-exported from fidesui using the @ant-design/x/lib CJS path, mirroring the antd re-export pattern.
  • Agent responses with a new_policy_yaml flow through handleYamlProposed, which bumps syncKey so PolicyCanvasPanel re-hydrates from YAML and the controls state stays consistent with the parsed policy.
  • Replaced a stale-deps useMemo around the tab items with a plain array literal — the deps list was omitting controls, controlOptions, and syncKey with a disabled lint rule.
  • Scripted MSW handler (dev:mock) drives a deterministic 3-turn demo conversation against /api/v1/plus/llm/access-policy-chat so the UI is iterable without the backend.

Code Changes

  • clients/admin-ui/src/features/access-policies/AgentChatPanel.tsx — new panel using Bubble, Sender (via fidesui) and useMessage-based error surfacing.
  • clients/admin-ui/src/features/access-policies/AgentChatPanel.module.scss — panel layout styles.
  • clients/admin-ui/src/features/access-policies/agent-chat.slice.ts — RTK Query mutation for POST /api/v1/plus/llm/access-policy-chat, injected via baseApi.injectEndpoints.
  • clients/admin-ui/src/features/access-policies/AccessPolicyEditor.tsx
    • Gate on detection_discovery.llm_classifier_enabled and render the chat inside a <Splitter> when enabled.
    • Replace useMemo(tabItems, …) + eslint-disable with a plain array literal.
    • Code tab height: calc(100vh - 220px)100%.
    • Wire handleYamlProposedsetYamlValue + setSyncKey + setControls so agent-proposed YAML re-hydrates the canvas.
  • clients/admin-ui/src/features/access-policies/AccessPolicyEditor.module.scss — make the editor body fill available height.
  • clients/admin-ui/src/mocks/access-policies/agent-chat-handlers.ts — scripted 3-turn handler for /api/v1/plus/llm/access-policy-chat, keyed by chat_history_id.
  • clients/admin-ui/src/mocks/handlers.ts — register agentChatHandlers().
  • clients/admin-ui/package.json — remove @ant-design/x; bump @ant-design/cssinjs from ^1.21.0 to ^2.1.2.
  • clients/admin-ui/next.config.js — Turbopack + webpack alias forcing "antd""antd/lib".
  • clients/fidesui/package.json — add @ant-design/x: ^2.5.0.
  • clients/fidesui/src/index.ts — re-export Bubble, Sender, and BubbleItemType from @ant-design/x/lib.
  • clients/fidesui/src/icons/carbon.ts — export the Carbon Ai icon for the agent avatar.
  • clients/package-lock.json — regenerated; nested cssinjs copies collapsed to a single root v2.1.2, one antd module instance.
  • changelog/7993-astralis-agent-chat-policy-builder.yaml — "Added" entry.

Steps to Confirm

  1. Install deps and start the admin-ui dev server in mock mode:
    cd clients && npm install
    cd admin-ui && npm run dev:mock
    
  2. Log in at http://localhost:3000
  3. Because the panel is gated on detection_discovery.llm_classifier_enabled, enable that flag for your session. The simplest way is to set it in .fides/fides.toml on your local backend and restart it; alternatively, temporarily stub the flag to true in config-settings.slice.ts while manually testing.
  4. Navigate to Access policies → New policy (or edit an existing policy). You should see the builder canvas on the left and the "Policy builder agent" chat panel on the right inside a resizable Splitter.
  5. Drag the splitter handle; the canvas should resize smoothly. Collapse the right panel to confirm the canvas takes the full width.
  6. Open devtools and inspect a <Bubble> or the <Sender> input: --ant-color-primary should be #2b2e35 (Fides brand), not #1677ff (antd default). Compare with a regular <Button> in the header — values must match. This verifies the antd theme actually reaches @ant-design/x components.
  7. In the chat input, send any message (e.g. "Draft a policy blocking third-party advertising for customer data"). The MSW handler replies with a draft policy and populates the Code and Builder tabs.
  8. Send a second message to trigger the next scripted turn — the canvas should re-hydrate with the updated YAML (a consent "unless" clause should now appear).
  9. Send a third message and confirm the agent's final response arrives with no YAML change (existing policy stays intact).
  10. Switch between Builder, Code, and (dev-only) Split tabs:
    • ReactFlow viewport / node positions should survive tab switches.
    • The Code editor's YAML stays in sync with what the agent proposed.
    • The Code tab should fill the available height (no double scrollbar, no cut-off editor).
  11. Toggle llm_classifier_enabled off (or run against a backend that doesn't set it) and reload. The chat panel must not render; the editor falls back to the original full-width tabs layout.
  12. Run npm run build && npm run start once to confirm SSR style extraction still works with the cssinjs v2 bump. First paint in production mode should not show a flash of unstyled antd components.
  13. (Optional) Toggle macOS "Reduce motion" and confirm bubble entrance animations are suppressed — reduced motion now propagates into @ant-design/x components because they share the same antd/cssinjs instance as the rest of the app.

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Apr 22, 2026 8:09pm
fides-privacy-center Ignored Ignored Apr 22, 2026 8:09pm

Request Review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
See the Details below.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA bf1f799.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

  • clients/admin-ui/package.json
  • clients/fidesui/package.json
  • clients/package-lock.json

@lucanovera
Copy link
Copy Markdown
Contributor Author

/code-review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

Title Lines Statements Branches Functions
admin-ui Coverage: 8%
6.33% (2799/44163) 5.58% (1402/25082) 4.43% (579/13060)
fides-js Coverage: 78%
78.98% (1962/2484) 65.55% (1214/1852) 72.57% (336/463)
privacy-center Coverage: 88%
85.97% (331/385) 81.36% (179/220) 78.87% (56/71)

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #7993 — Agent chat for policy builder UI

This is a frontend-only PR adding an AgentChatPanel component with @ant-design/x to the access policy editor. The core feature approach is solid, but there are two bugs to fix before merging and two architectural issues worth addressing.


Bugs (must fix)

1. .panel missing flex container (AgentChatPanel.module.scss:4)
The .body { flex: 1 }, .header { flex-shrink: 0 }, and .footer { flex-shrink: 0 } rules are flex-child properties that have no effect without a flex container parent. .panel needs display: flex; flex-direction: column. Without it, the body won't fill available height and the layout will break.

2. Wrong RTKErrorResult cast (AgentChatPanel.tsx:105)
(error as RTKErrorResult).error passes the outer wrapper object to getErrorMessage. The correct pattern used elsewhere in the codebase is error as RTKErrorResult["error"]. As written, error toasts will show a bad or empty message.


Architecture issues

3. Nested XProvider (AgentChatPanel.tsx:156)
The app already has a root FidesUIProvider / ConfigProvider. A per-component <XProvider> creates a second CSS-in-JS token context that overrides rather than merges. If @ant-design/x needs a provider, it should be added once in fidesui's FidesUIProvider.

4. @ant-design/cssinjs version split (package.json:19)
@ant-design/x requires @ant-design/cssinjs ^2.x but the root dependency is ^1.x. npm resolves this with a private nested copy (node_modules/@ant-design/x/node_modules/@ant-design/cssinjs@2.1.2), so two different CSS-in-JS engine versions coexist. This can cause duplicate style injection, token hash mismatches, and SSR hydration errors that are hard to diagnose. Consider moving @ant-design/x into fidesui to share the same dependency resolution. Also note that @ant-design/x unconditionally depends on mermaid and react-syntax-highlighter — bundle impact should be verified.


Minor / suggestions

  • canvasPanel reuse in two tab slots (AccessPolicyEditor.tsx): Both Builder and Split tabs reference the same JSX element under a shared ReactFlowProvider, meaning two PolicyCanvasPanel instances are mounted simultaneously with shared node/edge state. Low risk since Split is dev-only, but worth a comment.
  • Feature flag coupling (AccessPolicyEditor.tsx): detection_discovery.llm_classifier_enabled is a data-discovery capability flag, not a policy-agent flag. Works fine for now but worth a follow-up when this feature matures to give operators finer-grained control.
  • MSW handlers use rest from msw v1 API (agent-chat-handlers.ts): Consistent with the rest of the mock layer — no action needed, just noting it for the eventual msw v2 migration.

Overall: the feature structure is clean and the API slice is minimal and well-typed. Fix the two bugs above and consider the XProvider / cssinjs issues before merging to avoid hard-to-debug styling regressions.

🔬 Codegraph: connected (47178 nodes)


💡 Write /code-review in a comment to re-run this review.

Comment thread clients/admin-ui/src/features/access-policies/AgentChatPanel.tsx Outdated
Comment thread clients/admin-ui/src/features/access-policies/AgentChatPanel.tsx Outdated
@lucanovera
Copy link
Copy Markdown
Contributor Author

/code-review

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Agent Chat for Access Policy Builder

Overall this is a clean, well-scoped addition. The architecture is sound — fidesui re-exports Bubble/Sender from @ant-design/x/lib (CJS) to maintain a single ConfigProvider/cssinjs context identity, the webpack/Turbopack antd$ alias enforces that at the bundler level, and AgentChatPanel itself is simple and readable. The RTK slice is minimal and correct. A few things to address before merge:

Must Fix

Feature flag reuse (AccessPolicyEditor.tsx:799-800)detection_discovery.llm_classifier_enabled gates a different feature (the discovery LLM classifier). Repurposing it couples two unrelated features to one config key. See inline comment.

handleYamlProposed state divergence (AccessPolicyEditor.tsx:852-859) — when the agent returns invalid YAML, controls is left at its previous value rather than being cleared, causing the canvas and code editor to disagree. Fix: setControls(parsed?.controls ?? []) unconditionally.

Shared SVG viewBox change (logomark-ethyca.svg) — the viewBox was changed from a 3:2 rectangle to a 1:1 square to fit the chat avatar. This is a shared asset; please verify all other consumers (nav header, etc.) still render correctly. A separate avatar-specific copy may be safer.

Suggestions

chatHistoryId not reset on external YAML changes (AgentChatPanel.tsx:56) — if the user manually edits the YAML outside the chat, the conversation thread continues referencing the old policy context. A "Clear conversation" button (resetting both messages and chatHistoryId) would address this.

isLoading in handleSend dep array (AgentChatPanel.tsx:116-120) — causes handleSend to be recreated on every request state change. The Sender's loading prop already prevents double-submission; isLoading can be removed from deps.

@ant-design/cssinjs major version bump (package.json:37) — confirm no visual/functional regressions in existing antd components after the 1.x → 2.x upgrade.

Nice to Have

  • No unit tests for AgentChatPanel or the state logic in handleYamlProposed/handleSend. The 3-turn scripted mock handler is a great foundation for integration tests.
  • The "Policy builder agent" header string is hardcoded — consider a named constant if it needs to be reused or localized.

🔬 Codegraph: connected (47276 nodes)


💡 Write /code-review in a comment to re-run this review.

Comment on lines +852 to +859
const handleYamlProposed = useCallback((newYaml: string) => {
setYamlValue(newYaml);
setSyncKey((k) => k + 1);
const parsed = parseYaml(newYaml);
if (parsed?.controls) {
setControls(parsed.controls);
}
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clients/admin-ui/src/features/access-policies/AccessPolicyEditor.tsx:852-859

If parseYaml(newYaml) returns null (invalid YAML from the agent), yamlValue and syncKey are still updated but controls is left at its previous value — state diverges silently. Compare with handleSave which already uses parsed?.controls ?? [].

Suggest:

const parsed = parseYaml(newYaml);
setControls(parsed?.controls ?? []);

Comment thread clients/admin-ui/public/images/logomark-ethyca.svg
}: AgentChatPanelProps) => {
const messageApi = useMessage();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [chatHistoryId, setChatHistoryId] = useState<string>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clients/admin-ui/src/features/access-policies/AgentChatPanel.tsx:56

chatHistoryId is never reset when currentYaml changes externally (e.g. the user manually edits the Code tab, imports a new policy, or resets the form). After an external YAML edit, the next chat turn will continue the existing server-side conversation thread, which was built around the old YAML context. The agent will receive the updated current_policy_yaml but may make references to earlier turns that no longer apply.

At minimum, consider adding a "Clear conversation" button that resets both messages and chatHistoryId, so users can start fresh after making manual edits.

Comment on lines +116 to +120
[
isLoading,
nextKey,
chatHistoryId,
currentYaml,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clients/admin-ui/src/features/access-policies/AgentChatPanel.tsx:116-120

isLoading in the dep array means handleSend is recreated every time a request starts or finishes, causing Sender's onSubmit prop to change on each state transition. The guard if (!trimmed || isLoading) return works regardless of when the callback was created, so isLoading is not needed as a dep here — the Sender's own loading prop already disables the submit button while in-flight.

Removing isLoading from the dep array reduces unnecessary renders.

Comment thread clients/admin-ui/package.json
Comment on lines +903 to +906
<div style={{ flex: "0 0 60%" }}>
<ReactFlowProvider>{canvasPanel}</ReactFlowProvider>
</div>
<div style={{ flex: "0 0 calc(40% - 8px)" }}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could probably go into a css module, but not critical.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, I'll move those to the modules file.

@lucanovera lucanovera marked this pull request as ready for review April 22, 2026 19:36
@lucanovera lucanovera requested a review from a team as a code owner April 22, 2026 19:36
@lucanovera lucanovera requested review from speaker-ender and removed request for a team and speaker-ender April 22, 2026 19:36
Comment on lines +18 to +28
// Force all imports of "antd" to resolve to the CJS build. fidesui uses
// "antd/lib" (CJS), but third-party packages like @ant-design/x import plain
// "antd", which the bundler resolves to ESM — producing two separate antd
// modules with their own ConfigProvider contexts. useToken() then reads
// unthemed defaults inside those components. The alias keeps everything on
// one antd instance.
turbopack: {
resolveAlias: {
antd: "antd/lib",
},
},
Copy link
Copy Markdown
Contributor

@kruulik kruulik Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense, but we have to be certain that everything using ant imports exactly the same way. ant and ant/lib will resolve to different themes and not respect any overrides.
Storybook and Chromatic can be a good place to check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is the issue that the resolveAlias solves. When I added the Ant Design X library, it was importing from "antd" and we were using "antd/lib", so we had that issue of the themes not matching. Adding the resolveAlias ensures the bundler will use "antd/lib" even if it sees an "antd" import.

The unfortunate part, is that fidesui is not a full library that compiles on it's own. That means this resolveAlias had to be added at the admin-ui level instead of fidesui. That's one more reason to make fidesui a standalone library in the future.

Copy link
Copy Markdown
Contributor

@kruulik kruulik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool!

@lucanovera lucanovera added this pull request to the merge queue Apr 23, 2026
Merged via the queue into main with commit cbdbb2a Apr 23, 2026
54 of 57 checks passed
@lucanovera lucanovera deleted the ENG-2959-FE-Astralis-agent-chat-for-policy-building branch April 23, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants