Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/apollo-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@
"import": "./dist/canvas/constants.js",
"require": "./dist/canvas/constants.cjs"
},
"./canvas/guardrails": {
"types": "./dist/canvas/components/Guardrails/index.d.ts",
"import": "./dist/canvas/components/Guardrails/index.js",
"require": "./dist/canvas/components/Guardrails/index.cjs"
},
"./canvas/xyflow/*.css": "./dist/canvas/xyflow/*.css",
"./canvas/styles/*.css": "./dist/canvas/styles/*.css"
},
Expand Down Expand Up @@ -171,6 +176,7 @@
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/cache": "^11.14.0",
Expand Down Expand Up @@ -211,6 +217,7 @@
"@uipath/apollo-wind": "workspace:*",
"@xyflow/react": "12.8.2",
"@xyflow/system": "0.0.66",
"class-variance-authority": "^0.7.1",
"d3-hierarchy": "^3.1.2",
"d3-sankey": "^0.12.3",
"d3-scale": "^4.0.2",
Expand Down Expand Up @@ -267,6 +274,7 @@
"@types/d3-scale-chromatic": "^3.0.3",
"@types/d3-selection": "^3.0.11",
"@types/d3-zoom": "^3.0.8",
"@types/jest-axe": "^3.5.9",
"@types/lodash": "^4.17.21",
"@types/luxon": "^3.7.1",
"@types/mdast": "^4.0.4",
Expand All @@ -284,6 +292,7 @@
"esbuild": "^0.28.1",
"glob": "^13.0.0",
"happy-dom": "^20.0.0",
"jest-axe": "^10.0.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"typescript": "^5.9.3",
Expand Down
4 changes: 4 additions & 0 deletions packages/apollo-react/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export default defineConfig({
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/storybook-utils/**',
// Test-only, like `src/test/**`: fixtures are data for the suites, not API, and they
// reach for devDependencies (the dnd geometry helper needs `@testing-library/react`),
// so there is nothing to gain from publishing them.
'!./src/**/__fixtures__/**',
'!./src/test/**',
'!./src/icons/.cache',
'!./src/**/*.md',
Expand Down
514 changes: 514 additions & 0 deletions packages/apollo-react/src/canvas/components/Guardrails/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import type { GuardrailDefinitionWire } from '../definitions-wire';

/**
* Wire payloads shaped like what `GET /api/execution/guardrails/definitions` actually
* returns, trimmed to the fields the definitions layer reads. Kept close to the real
* payload (option lists, threshold bounds, BYO fields) so the enrichment tests exercise the
* same shapes the products see.
*/

/** The 25 PII entities the backend offers today. */
export const PII_ENTITY_OPTIONS: string[] = [
'Person',
'Address',
'Date',
'PhoneNumber',
'EugpsCoordinates',
'Email',
'CreditCardNumber',
'InternationalBankingAccountNumber',
'SwiftCode',
'ABARoutingNumber',
'USDriversLicenseNumber',
'UKDriversLicenseNumber',
'USIndividualTaxpayerIdentification',
'UKUniqueTaxpayerNumber',
'USBankAccountNumber',
'USSocialSecurityNumber',
'UsukPassportNumber',
'NOIdentityNumber',
'FINationalID',
'FIPassportNumber',
'SENationalID',
'DKPersonalIdentificationNumber',
'NLCitizensServiceNumber',
'URL',
'IPAddress',
];

export const PII_DETECTION_WIRE: GuardrailDefinitionWire = {
validator: 'pii_detection',
allowedScopes: ['Agent', 'Llm', 'Tool'],
status: 'Available',
parameters: [
{
id: 'entities',
type: 'enum-list',
required: true,
defaultValue: ['Email', 'CreditCardNumber'],
options: PII_ENTITY_OPTIONS,
},
{
// The backend omits the bounds here; enrichment supplies 0..1 step 0.1.
id: 'entityThresholds',
type: 'map-enum',
required: false,
defaultValue: { Email: 0.8, CreditCardNumber: 0.8 },
keySource: 'entities',
},
],
};

export const HARMFUL_CONTENT_WIRE: GuardrailDefinitionWire = {
validator: 'harmful_content',
allowedScopes: ['Llm', 'Tool'],
status: 'Available',
parameters: [
{
id: 'harmfulContentEntities',
type: 'enum-list',
required: true,
defaultValue: ['Hate', 'Violence'],
options: ['Hate', 'SelfHarm', 'Sexual', 'Violence'],
},
{
id: 'harmfulContentEntityThresholds',
type: 'map-enum',
required: false,
defaultValue: { Hate: 2, Violence: 2 },
keySource: 'harmfulContentEntities',
min: 0,
max: 6,
step: 2,
},
],
};

export const PROMPT_INJECTION_WIRE: GuardrailDefinitionWire = {
validator: 'prompt_injection',
allowedScopes: ['Llm'],
status: 'Available',
parameters: [
{
id: 'threshold',
type: 'number',
required: true,
defaultValue: 0.7,
min: 0,
max: 1,
step: 0.1,
},
],
};

export const USER_PROMPT_ATTACKS_WIRE: GuardrailDefinitionWire = {
validator: 'user_prompt_attacks',
allowedScopes: ['Llm'],
status: 'Available',
parameters: [],
};

export const INTELLECTUAL_PROPERTY_WIRE: GuardrailDefinitionWire = {
validator: 'intellectual_property',
allowedScopes: ['Llm', 'Tool'],
status: 'FeatureDisabled',
parameters: [
{
id: 'ipEntities',
type: 'enum-list',
required: true,
defaultValue: ['Text'],
options: ['Text', 'Code'],
},
],
};

export const LLM_AS_JUDGE_WIRE: GuardrailDefinitionWire = {
validator: 'llm_as_judge',
allowedScopes: ['Agent', 'Llm', 'Tool'],
status: 'Available',
parameters: [
{
id: 'guardrailText',
type: 'text',
required: true,
defaultValue: null,
maxLength: 4000,
},
{
id: 'model',
type: 'enum',
required: true,
defaultValue: null,
options: ['gpt-4o-mini-2024-07-18', 'gpt-4o-2024-11-20'],
},
{
// Agents' schema has no `defaultValue` for text-list at all; this mirrors that.
id: 'positiveExamples',
type: 'text-list',
required: false,
maxItems: 5,
maxLength: 1000,
},
{
id: 'negativeExamples',
type: 'text-list',
required: false,
defaultValue: null,
maxItems: 5,
maxLength: 1000,
},
{
id: 'threshold',
type: 'number',
required: true,
defaultValue: 4,
min: 0,
max: 6,
step: 2,
},
],
};

/** A bring-your-own guardrail: manifest copy only, no curated table entry applies. */
export const BYO_WIRE: GuardrailDefinitionWire = {
validator: 'pii_detection',
allowedScopes: ['Llm'],
status: 'Available',
displayName: 'Acme PII scan',
description: 'Runs the Acme detector over prompts and completions.',
byoValidatorName: 'acme-pii',
byoConnectorName: 'Acme AI Guardrails',
byoConnectorKey: 'acme',
byoGuardrailConnectionId: 'conn-1',
byoConfigurationId: 'cfg-1',
parameters: [
{
id: 'sensitivity',
type: 'enum',
required: true,
defaultValue: 'medium',
displayName: 'Sensitivity',
description: 'How aggressively to flag.',
options: ['low', 'medium', 'high'],
optionLabels: { low: 'Low', high: 'High' },
},
],
};

/** A UiPath validator the curated table has never heard of. */
export const UNCURATED_WIRE: GuardrailDefinitionWire = {
validator: 'topic_drift',
allowedScopes: ['Llm'],
status: 'Available',
parameters: [
{ id: 'maxDriftScore', type: 'number', required: true, defaultValue: 0.5 },
{
id: 'allowedTopics',
type: 'enum-list',
required: false,
defaultValue: [],
options: ['finance', 'legal'],
},
],
};

export const ALL_BUILT_IN_WIRE: GuardrailDefinitionWire[] = [
PII_DETECTION_WIRE,
PROMPT_INJECTION_WIRE,
HARMFUL_CONTENT_WIRE,
USER_PROMPT_ATTACKS_WIRE,
INTELLECTUAL_PROPERTY_WIRE,
LLM_AS_JUDGE_WIRE,
];

/**
* A raw payload as it arrives over the wire: unknown keys the backend added, one entry that
* fails validation, and the empty display strings Flow's schema normalizes away.
*/
export const RAW_PAYLOAD_WITH_NOISE: unknown = [
{
...PROMPT_INJECTION_WIRE,
// Fields a newer backend added that this package does not model.
executionStage: 'PreLlm',
internalRanking: 3,
displayName: '',
description: '',
},
{
// Missing `status`, so the whole definition is dropped.
validator: 'broken_validator',
allowedScopes: ['Llm'],
parameters: [],
},
USER_PROMPT_ATTACKS_WIRE,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Synthetic layout for the drag-and-drop suites.
*
* happy-dom runs no layout, so every element reports a zero rect. dnd-kit's collision
* detection then cannot tell two rows apart, and the keyboard sensor finds no candidate in the
* arrow direction, so a reorder silently does nothing. Laying the rendered rows out on a
* vertical grid is the minimum geometry a reorder needs; it is also all of it, which is why
* this is a fixture and not a mock of dnd-kit.
*/

import { act } from '@testing-library/react';

const ROW_HEIGHT = 48;
const ROW_WIDTH = 320;

function rectAt(top: number, height: number): DOMRect {
const rect = {
x: 0,
y: top,
top,
left: 0,
right: ROW_WIDTH,
bottom: top + height,
width: ROW_WIDTH,
height,
};
return { ...rect, toJSON: () => rect } as DOMRect;
}

function stubRect(element: Element, rect: DOMRect): void {
Object.defineProperty(element, 'getBoundingClientRect', {
configurable: true,
value: () => rect,
});
}

/**
* Stack every rendered row into a column, and give their container a rect that spans it (the
* `restrictToParentElement` modifier clamps the drag translation to that rect, and a zero-sized
* parent clamps it to nothing).
*/
export function layoutRowsVertically(
container: HTMLElement,
selector = '[data-slot="guardrail-list-row"]'
): void {
const rows = Array.from(container.querySelectorAll<HTMLElement>(selector));
rows.forEach((row, index) => stubRect(row, rectAt(index * ROW_HEIGHT, ROW_HEIGHT)));

const parents = new Set(
rows.map((row) => row.parentElement).filter((el): el is HTMLElement => el !== null)
);
for (const parent of parents) {
stubRect(parent, rectAt(0, rows.length * ROW_HEIGHT));
}
}

/**
* Let dnd-kit finish a step of a drag.
*
* It measures droppables and applies coordinate changes inside `requestAnimationFrame`, which
* the canvas test setup implements as `setTimeout(cb, 0)`: a macrotask, so it does not flush
* with the `act()` around a `fireEvent`. Await this between the keystrokes of a drag, or the
* next one is computed against the geometry of the step before.
*/
export async function flushDndFrame(): Promise<void> {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
}
Loading
Loading