You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(apollo-react): shared support for the classifier guardrail [AL-644] - #1410
Shared support for the Agents classifier guardrail (Jev Classifier, UiPath/Agents#6449). Read generically, its definition renders unusably: a raw multi-select of 26 question names, a bare text list, a threshold row per question and a free-text vendor box. This ports the behaviour of Agents frontend-sw's ClassifierQuestionPicker, ClassifierCustomQuestionsEditor, ClassifierThresholdSlider, api/hooks/classifier-questions.ts and the classifier branches of ApolloToolGuardrailPolicyBuilder's renderParameter into the family, on apollo-wind and Tailwind (no MUI, Emotion or Ap*), so frontend-sw can delete its MUI copies and Flow gets the same screen.
Wire contract unchanged, no new $parameterType.
Commits
Curated copy (130cc547): classifier in definitions-copy.ts with Agents' English verbatim (single-host rule, like sentiment's 51f559e5), plus the 7 category headings as a new optional GuardrailValidatorCopy.categoryLabels (guardrails.definitions.classifier.category.<id>). Agents baseline, SINGLE_HOST_VALIDATORS.classifier, id pattern and parity flatten extended, CLASSIFIER_WIRE from OutOfTheBoxGuardrailDefinitions.cs.
Builder fix (a8f2cfb3): GuardrailBuilder saved 0 for every untuned classifier question. On save, syncMapEnumParameters fills keys missing from a map-enum with the per-key default, then min. The classifier's map defaults to {} with min 0, and stores only tuned questions (a missing key runs at the backend's 0.5). So "pick two questions, tune one, save" saved the other at 0 and flagged every payload; reopening such a guardrail did the same. Agents' flag-on path has this today. A map listed in overrideParameterIds is now pruned but not filled (new leaveUnsetIds option); a probed override keeps the old fill.
Question catalog (80fdeb6b): useClassifierQuestions(ctx | null, { questions, enabled, copy }) over GET /api/execution/guardrails/classifier-questions. Same context and three host shapes as useGuardrailDefinitions, never throws, private zod, hand-written wire type. The two hooks now share one private request core, moved out of useGuardrailDefinitions unchanged (its suite passes as it was).
Editors (9a54ae8b): ClassifierQuestionPicker (grouped, searchable, collapsible, select-all with counts, Strictness slider for yes/no, severity-tier select for score, one onChange(selected, thresholds)) and ClassifierCustomQuestionsEditor, about 30 guardrails.classifier.* ids with labels overrides, and the pure sidecar helpers plus hasClassifierQuestions.
Builder seam (b912d8d7): useClassifierGuardrailParameters claims selectedQuestions, questionThresholds, questions and modelVendor, leaves model to the host, and returns a validateParameters save gate for "no questions". README section and a Classifier builder story.
Empty parameter rows (d6c81c12): a parameter that renders nothing (an override returning null, such as questionThresholds and modelVendor, or a threshold map with no key selected) kept its grid row, doubling the gap around it. GuardrailValidatorForm now hides an empty field cell. Found in Storybook: the model field sat 32px below the custom questions instead of 16px.
Review-stage commits
Trim (df5506ef): +4363 down to +2979 lines.
The catalog parser joins definitions-parse.ts (zod stays in one file); the wire type joins definitions-wire.ts; enrichment folds into the hook.
Both editors take the parameter's definition and reuse ParameterLabel.
The level, tier and sanitize helpers and the pure parse and enrich functions are no longer exported.
The catalog fixture keeps 10 of the 26 questions; tests keep one case per behaviour; stories and the README section are about half as long.
Severity display (6fe47dcf, Copilot): a stored threshold between two levels shows the first level that reaches it, not the nearest.
Threshold-map errors (8f20762e, Copilot): questionThresholds shows its own error (for example a saved value above 1). GuardrailValidatorForm now clears the error of every defined parameter an override rewrites, so a corrected host error stops holding Save.
Custom-question errors (23b0e23d, Copilot): each question is described by the editor's error.
abc5c16c: a test matcher in commit 9 had lost its d.
Narrow panels (1d7b9b29, from Flow at 260 to 420px): the slider track shrinks and its Strictness label and percentage sit above it; Strict, Balanced and Lenient mark the start, middle and end; the severity select wraps under its label; both rows start at the checkbox edge; each custom question is a card with a "Question {position}" title and the delete button in its header. Checked in Storybook at 280px and 400px: no horizontal overflow, the delete button stays visible, and the legend labels keep at least 32px between them.
The host's model picker keeps writing modelVendor with model. Declare overrideParameterIds: that is what turns on the threshold fix. Agents passes none today, so it relies on the probe and would keep zero-filling.
Behaviour notes for review
Chrome English is Agents' verbatim, em dashes included ("Counts as yes (flags) — optional"), so frontend-sw's screens read the same after the swap. The no-em-dash rule applies to story copy, which has none.
The picker's rows highlight with accent, which in the future themes equals the checkbox border, so an unchecked box vanished under the pointer; a hovered row's checkbox takes muted-foreground.
Deviations from the MUI originals, all deliberate:
A question's checkbox is named by its visible question text, not "Select question pii_contact_information".
Each category has one toggle button instead of a clickable heading plus an icon button.
The slider shows its percentage next to the track rather than on hover.
English only. The l10n sync fills the other 12 catalogs.
Test plan
Full apollo-react on the pushed branch: 4063 passed, 33 failed. 31 are the known environment failures in 8 files (Node localStorage, JsonTree.import timeout, the chat suites), the same as on a clean tree; the other 2 were commit 9's tests with a broken matcher, fixed in abc5c16c (the seam suite is 6/6 after it, and fails without commit 9's GuardrailValidatorForm change).
tsc 0, biome clean (only the builder's pre-existing hostErrors warnings), rslib build clean with no zod in the folder's .d.ts (before the trim; the trim moves zod back into one file).
Storybook (future dark): the Classifier builder story end to end; parameter spacing even; checkboxes stay visible on row hover.
CI, visual regression (new stories).
Hosts: Agents and Flow adoption PRs (see Host adoption).
Adds the classifier to the canonical definitions copy: its name and
description, labels for selectedQuestions, questions and model, and
tooltips for the first two. The wording is Agents' own from
the merged UiPath/Agents#6449, so its legacy screens and the shared
ones read the same. Flow has no classifier yet, so it joins sentiment
as a single-host validator in the parity suite.
The question catalog groups its questions by category, and those seven
headings are copy too. They ride on the validator's entry as a new
optional categoryLabels map, ids guardrails.definitions.classifier.
category.<groupId>; enrichment ignores them, since no parameter carries
them. English only; the l10n sync fills the other catalogs.
CLASSIFIER_WIRE is the definition the backend sends, and the parser
keeps it whole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… unset [AL-644]
At save time GuardrailBuilder prunes every map-enum to the keys its
keySource selects and fills the keys it lacks (stored value, per-key
default, then min), so the saved map matches MapEnumField's rows.
The classifier renders its threshold map itself and stores a threshold
only for a question the user tuned; a question with no entry runs at
the backend's 0.5. Its map defaults to {} with min 0, so picking two
questions, tuning one and saving stored 0 for the other, which flags
every payload. Reopening a guardrail saved that way did the same.
A map listed in overrideParameterIds is now pruned but not filled,
through a new leaveUnsetIds option on syncMapEnumParameters. A probed
override is still filled: only the form knows which ids the probe
claimed, which is one more reason to declare them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useClassifierQuestions(ctx | null, { questions, enabled, copy }) reads
GET /api/execution/guardrails/classifier-questions, the predefined
questions grouped by category as the validator runs them, and returns
the categories with their headings resolved from the curated copy.
It takes the same context and the same three host shapes as
useGuardrailDefinitions: a context to fetch, null plus a catalog the
host already holds, null alone to stay idle. Both hooks now share one
private request core, moved out of useGuardrailDefinitions unchanged;
that hook's suite passes as it was and pins the shared behaviour.
The parser never throws. zod stays private to it, the wire types are
hand-written and pinned to the schema both ways, and a malformed
question is dropped and reported while its siblings stay.
parseClassifierQuestions and enrichClassifierQuestions are exported
for callers outside React.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ditor [AL-644]
ClassifierQuestionPicker renders the classifier's predefined questions
grouped by category: searchable by question text, each category
collapsible with a selected count and a select-all. A selected yes/no
question takes a Strictness slider (0..1, step 0.1, Strict, Balanced
and Lenient marks); a selected score question takes the severity tier
it flags from, stored as level / (levels - 1). A question the user
never tunes stores nothing and runs at the backend's 0.5. Both values
come back in one onChange(selected, thresholds), so a question leaving
the selection takes its threshold with it.
ClassifierCustomQuestionsEditor edits the author's own questions, each
with its Strictness and optional "counts as yes" / "counts as no"
definitions. Rows live in local state so a blank row can be typed
into, and only rows with a question persist.
Both are apollo-wind and Tailwind only, resolve nothing, and take their
chrome from lingui guardrails.classifier.* ids with labels overrides,
in Agents' English from UiPath/Agents#6449. The pure helpers ship with
them: read, write and sanitize for the position-keyed sidecars,
hasClassifierQuestions for the save gate, and the level mapping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… builder [AL-644]
useClassifierGuardrailParameters({ definition, categories }) is the one
seam both hosts compose into GuardrailBuilder. It claims
selectedQuestions, questionThresholds, questions and modelVendor, and
returns undefined for the rest, so model falls through to the host's
own discovery picker:
- selectedQuestions renders ClassifierQuestionPicker, which writes both
lists in one onParametersChange; questionThresholds renders nothing
- questions renders ClassifierCustomQuestionsEditor, which writes the
list and its position-keyed sidecars together
- modelVendor renders nothing until the builder flags it with a model
chosen, then asks for the model again
- validateParameters is the save gate for the rule the builder cannot
know, at least one question across both lists, with its message on
the picker until either editor changes
Nothing is claimed for another validator or a bring-your-own one. The
editors mount through the existing renderParameter seam, so this adds
no renderer next to MetadataForm. Documented in the README, with a
Classifier builder story composing the seam the way the hosts will.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nothing [AL-644]
Every parameter owns a cell in GuardrailValidatorForm's grid, and a
field that renders nothing kept its row: an override returning null,
such as the classifier's questionThresholds and modelVendor, or a
threshold map with no key selected. Each empty row added a second gap,
so the classifier's model field sat twice as far from its neighbours.
An empty field cell is now hidden, which removes its row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.
Package
Coverage
New-line coverage
Packed (gzip)
Unpacked
vs main
@uipath/apollo-core
—
—
42.02 MB
50.17 MB
±0
@uipath/apollo-react
51.7%
87.4% (285/326)
8.01 MB
31.60 MB
+22.6 KB
@uipath/apollo-ui-icons
—
—
2.86 MB
6.97 MB
±0
@uipath/apollo-vertex
—
—
—
—
—
@uipath/apollo-wind
—
—
785.2 KB
4.55 MB
−9 B
@uipath/ap-chat
85.8%
—
40.37 MB
45.58 MB
+1.2 KB
"Coverage" is each package's own coverage.include scope (e.g. apollo-core instruments only scripts/). "Packed"/"Unpacked" come from npm pack --dry-run and only cover built packages — "—" means not measured this run (package not affected / not built). "vs main" is the packed (gzipped) delta against the last successful main build (the package-sizes artifact from the Release workflow); "—" there means no main baseline was available this run. The baseline is main's latest build, not this PR's exact merge-base, so it includes any drift since the branch diverged. Packages with no vitest config are omitted.
…tests [AL-644]
The two tests added with the threshold-error fix looked for the
Intellectual property toggle with a pattern that had lost its \d, so
they failed before reaching the code under test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a host supplies a modelVendor error for a nonempty vendor, this replaces its actionable message with “This model has no vendor.” A disallowed-vendor error, for example, becomes a misleading missing-value error while still blocking Save. Use the specialized message only when the vendor is blank; otherwise preserve ctx.error.
Custom thresholds use questionThreshold.{i} sidecars, which are absent from the definitions checked by the builder's range validation. A saved questionThreshold.0 of 1.4 therefore passes this gate and is saved unchanged, even after editing the question text. The equivalent predefined-question threshold blocks Save. Validate custom thresholds as finite numbers within 0..1 and show an editor error before allowing Save. Add a regression test for reopening and saving an invalid custom threshold.
…AL-644]
Flow's builder panel runs about 260 to 420px wide, and three things
broke there; Agents hits them too.
- The threshold slider's track had a 220px minimum and pushed the row
past the panel. The track now shrinks, and the Strictness label and
percentage sit above it, so the track and its legend get the full
row: beside them, a 280px panel left 89px of track for 125px of
labels.
- Strict, Balanced and Lenient sat at 0.3, 0.5 and 0.8 and collided on
a short track. They now mark its start, middle and end.
- The severity row kept a 160px select beside a label that would not
shrink. It wraps the select under the label when there is no room,
and both the severity and strictness rows start at the checkbox edge.
- Consecutive custom questions ran together. Each is now a card like a
custom guardrail's rule card: a "Question {position}" title (a new
id, English only) and the delete button in its header, the question
full width below, no indent on its refinements.
The same fixes as flow-workbench b8af0a65f and a0e5bfe43 make to Flow's
legacy copies, plus the stacked slider label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ually flags [AL-644]
The severity select rounded a stored threshold to the nearest level, so
a value between two levels could show a lower tier than the one that
flags. On a four-level ladder 0.75 showed High, which scores 2/3; only
Critical reaches 0.75. It now shows the first level that reaches the
threshold. Values the picker writes sit on a level and read the same.
Raised by Copilot on #1410.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs [AL-644]
The classifier's questionThresholds slot rendered nothing, so an error
on it held Save with no visible reason: a saved threshold above 1, or
an error the host reported. It now shows the error, named by the
parameter's label, where the map's field sits.
A host error on the map also outlived its correction. The picker writes
the map together with selectedQuestions through onParametersChange,
and GuardrailValidatorForm cleared only the error of the parameter
that rendered the override. It now clears every defined parameter the
replacement changes, which also covers any other override that writes
more than one parameter.
Raised by Copilot on #1410.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AL-644]
The custom-questions editor marked each question aria-invalid but never
pointed it at the message, so a screen reader landing on a question
later heard that it was invalid but not why. Each question is now
described by the editor's error, as the predefined-question picker's
group already is.
Raised by Copilot on #1410.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Shared support for the Agents
classifierguardrail (Jev Classifier, UiPath/Agents#6449). Read generically, its definition renders unusably: a raw multi-select of 26 question names, a bare text list, a threshold row per question and a free-text vendor box. This ports the behaviour of Agentsfrontend-sw'sClassifierQuestionPicker,ClassifierCustomQuestionsEditor,ClassifierThresholdSlider,api/hooks/classifier-questions.tsand the classifier branches ofApolloToolGuardrailPolicyBuilder'srenderParameterinto the family, on apollo-wind and Tailwind (no MUI, Emotion orAp*), sofrontend-swcan delete its MUI copies and Flow gets the same screen.Wire contract unchanged, no new
$parameterType.Commits
130cc547):classifierindefinitions-copy.tswith Agents' English verbatim (single-host rule, like sentiment's51f559e5), plus the 7 category headings as a new optionalGuardrailValidatorCopy.categoryLabels(guardrails.definitions.classifier.category.<id>). Agents baseline,SINGLE_HOST_VALIDATORS.classifier, id pattern and parity flatten extended,CLASSIFIER_WIREfromOutOfTheBoxGuardrailDefinitions.cs.a8f2cfb3):GuardrailBuildersaved 0 for every untuned classifier question. On save,syncMapEnumParametersfills keys missing from amap-enumwith the per-key default, thenmin. The classifier's map defaults to{}withmin0, and stores only tuned questions (a missing key runs at the backend's 0.5). So "pick two questions, tune one, save" saved the other at 0 and flagged every payload; reopening such a guardrail did the same. Agents' flag-on path has this today. A map listed inoverrideParameterIdsis now pruned but not filled (newleaveUnsetIdsoption); a probed override keeps the old fill.80fdeb6b):useClassifierQuestions(ctx | null, { questions, enabled, copy })overGET /api/execution/guardrails/classifier-questions. Same context and three host shapes asuseGuardrailDefinitions, never throws, private zod, hand-written wire type. The two hooks now share one private request core, moved out ofuseGuardrailDefinitionsunchanged (its suite passes as it was).9a54ae8b):ClassifierQuestionPicker(grouped, searchable, collapsible, select-all with counts, Strictness slider for yes/no, severity-tier select for score, oneonChange(selected, thresholds)) andClassifierCustomQuestionsEditor, about 30guardrails.classifier.*ids withlabelsoverrides, and the pure sidecar helpers plushasClassifierQuestions.b912d8d7):useClassifierGuardrailParametersclaimsselectedQuestions,questionThresholds,questionsandmodelVendor, leavesmodelto the host, and returns avalidateParameterssave gate for "no questions". README section and aClassifierbuilder story.d6c81c12): a parameter that renders nothing (an override returningnull, such asquestionThresholdsandmodelVendor, or a threshold map with no key selected) kept its grid row, doubling the gap around it.GuardrailValidatorFormnow hides an empty field cell. Found in Storybook: the model field sat 32px below the custom questions instead of 16px.Review-stage commits
df5506ef): +4363 down to +2979 lines.definitions-parse.ts(zod stays in one file); the wire type joinsdefinitions-wire.ts; enrichment folds into the hook.ParameterLabel.6fe47dcf, Copilot): a stored threshold between two levels shows the first level that reaches it, not the nearest.8f20762e, Copilot):questionThresholdsshows its own error (for example a saved value above 1).GuardrailValidatorFormnow clears the error of every defined parameter an override rewrites, so a corrected host error stops holding Save.23b0e23d, Copilot): each question is described by the editor's error.abc5c16c: a test matcher in commit 9 had lost itsd.1d7b9b29, from Flow at 260 to 420px): the slider track shrinks and its Strictness label and percentage sit above it; Strict, Balanced and Lenient mark the start, middle and end; the severity select wraps under its label; both rows start at the checkbox edge; each custom question is a card with a "Question {position}" title and the delete button in its header. Checked in Storybook at 280px and 400px: no horizontal overflow, the delete button stays visible, and the legend labels keep at least 32px between them.Host adoption (both products)
The host's model picker keeps writing
modelVendorwithmodel. DeclareoverrideParameterIds: that is what turns on the threshold fix. Agents passes none today, so it relies on the probe and would keep zero-filling.Behaviour notes for review
frontend-sw's screens read the same after the swap. The no-em-dash rule applies to story copy, which has none.accent, which in the future themes equals the checkbox border, so an unchecked box vanished under the pointer; a hovered row's checkbox takesmuted-foreground.Test plan
localStorage,JsonTree.importtimeout, the chat suites), the same as on a clean tree; the other 2 were commit 9's tests with a broken matcher, fixed inabc5c16c(the seam suite is 6/6 after it, and fails without commit 9'sGuardrailValidatorFormchange).tsc0, biome clean (only the builder's pre-existinghostErrorswarnings),rslib buildclean with no zod in the folder's.d.ts(before the trim; the trim moves zod back into one file).Classifierbuilder story end to end; parameter spacing even; checkboxes stay visible on row hover.🤖 Generated with Claude Code