@directive-run/claude-plugin 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +6 -0
- package/LICENSE +26 -0
- package/README.md +113 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/skills/README.md +9 -0
- package/skills/building-ai-agents/SKILL.md +414 -0
- package/skills/building-ai-agents/ai-adapters.md +257 -0
- package/skills/building-ai-agents/ai-agents-streaming.md +307 -0
- package/skills/building-ai-agents/ai-communication.md +304 -0
- package/skills/building-ai-agents/api-skeleton.md +5 -0
- package/skills/building-ai-agents/examples.md +849 -0
- package/skills/building-ai-orchestrators/SKILL.md +401 -0
- package/skills/building-ai-orchestrators/ai-multi-agent.md +375 -0
- package/skills/building-ai-orchestrators/ai-orchestrator.md +466 -0
- package/skills/building-ai-orchestrators/ai-tasks.md +268 -0
- package/skills/building-ai-orchestrators/api-skeleton.md +5 -0
- package/skills/building-ai-orchestrators/examples.md +1609 -0
- package/skills/building-directive-systems/SKILL.md +499 -0
- package/skills/building-directive-systems/api-skeleton.md +5 -0
- package/skills/building-directive-systems/examples.md +4149 -0
- package/skills/building-directive-systems/multi-module.md +327 -0
- package/skills/building-directive-systems/plugins.md +361 -0
- package/skills/building-directive-systems/react-adapter.md +376 -0
- package/skills/building-directive-systems/system-api.md +463 -0
- package/skills/getting-started-with-directive/SKILL.md +198 -0
- package/skills/getting-started-with-directive/api-skeleton.md +5 -0
- package/skills/getting-started-with-directive/core-patterns.md +240 -0
- package/skills/getting-started-with-directive/examples.md +87 -0
- package/skills/getting-started-with-directive/sitemap.md +202 -0
- package/skills/hardening-ai-systems/SKILL.md +377 -0
- package/skills/hardening-ai-systems/ai-budget-resilience.md +408 -0
- package/skills/hardening-ai-systems/ai-guardrails-memory.md +370 -0
- package/skills/hardening-ai-systems/ai-security.md +400 -0
- package/skills/hardening-ai-systems/api-skeleton.md +5 -0
- package/skills/hardening-ai-systems/examples.md +628 -0
- package/skills/migrating-to-directive/SKILL.md +249 -0
- package/skills/migrating-to-directive/anti-patterns.md +382 -0
- package/skills/migrating-to-directive/api-skeleton.md +5 -0
- package/skills/migrating-to-directive/core-patterns.md +240 -0
- package/skills/migrating-to-directive/examples.md +511 -0
- package/skills/migrating-to-directive/schema-types.md +273 -0
- package/skills/reviewing-directive-code/SKILL.md +232 -0
- package/skills/reviewing-directive-code/anti-patterns.md +382 -0
- package/skills/reviewing-directive-code/api-skeleton.md +5 -0
- package/skills/reviewing-directive-code/core-patterns.md +240 -0
- package/skills/reviewing-directive-code/examples.md +463 -0
- package/skills/reviewing-directive-code/naming.md +321 -0
- package/skills/scaffolding-directive-modules/SKILL.md +218 -0
- package/skills/scaffolding-directive-modules/api-skeleton.md +5 -0
- package/skills/scaffolding-directive-modules/core-patterns.md +240 -0
- package/skills/scaffolding-directive-modules/examples.md +1386 -0
- package/skills/scaffolding-directive-modules/naming.md +321 -0
- package/skills/scaffolding-directive-modules/schema-types.md +273 -0
- package/skills/testing-ai-systems/SKILL.md +398 -0
- package/skills/testing-ai-systems/ai-debug-observability.md +329 -0
- package/skills/testing-ai-systems/ai-mcp-rag.md +293 -0
- package/skills/testing-ai-systems/ai-testing-evals.md +485 -0
- package/skills/testing-ai-systems/api-skeleton.md +5 -0
- package/skills/testing-ai-systems/examples.md +1261 -0
- package/skills/testing-directive-code/SKILL.md +479 -0
- package/skills/testing-directive-code/api-skeleton.md +5 -0
- package/skills/testing-directive-code/examples.md +156 -0
- package/skills/testing-directive-code/history.md +351 -0
- package/skills/testing-directive-code/testing.md +412 -0
- package/skills/writing-directive-constraints/SKILL.md +486 -0
- package/skills/writing-directive-constraints/api-skeleton.md +5 -0
- package/skills/writing-directive-constraints/constraints.md +274 -0
- package/skills/writing-directive-constraints/error-boundaries.md +330 -0
- package/skills/writing-directive-constraints/examples.md +1361 -0
- package/skills/writing-directive-constraints/resolvers.md +367 -0
- package/skills/writing-directive-modules/SKILL.md +342 -0
- package/skills/writing-directive-modules/anti-patterns.md +382 -0
- package/skills/writing-directive-modules/api-skeleton.md +5 -0
- package/skills/writing-directive-modules/core-patterns.md +240 -0
- package/skills/writing-directive-modules/examples.md +1639 -0
- package/skills/writing-directive-modules/naming.md +321 -0
- package/skills/writing-directive-modules/schema-types.md +273 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
> Auto-generated from extracted examples. Do not edit manually.
|
|
4
|
+
|
|
5
|
+
## ai-guardrails
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Example: ai-guardrails
|
|
9
|
+
// Source: examples/ai-guardrails/src/module.ts
|
|
10
|
+
// Pure module file — no DOM wiring
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AI Safety Shield — Directive Module
|
|
14
|
+
*
|
|
15
|
+
* Types, schema, module definition, timeline, analysis functions,
|
|
16
|
+
* and system creation for prompt injection & PII detection guardrails.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type InjectionDetectionResult,
|
|
21
|
+
type PIIDetectionResult,
|
|
22
|
+
detectAndRedactPII,
|
|
23
|
+
detectPII,
|
|
24
|
+
detectPromptInjection,
|
|
25
|
+
} from "@directive-run/ai";
|
|
26
|
+
import {
|
|
27
|
+
type ModuleSchema,
|
|
28
|
+
createModule,
|
|
29
|
+
createSystem,
|
|
30
|
+
t,
|
|
31
|
+
} from "@directive-run/core";
|
|
32
|
+
import { devtoolsPlugin, emitDevToolsEvent } from "@directive-run/core/plugins";
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Types
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export interface ChatMessage {
|
|
39
|
+
id: string;
|
|
40
|
+
text: string;
|
|
41
|
+
blocked: boolean;
|
|
42
|
+
redactedText: string;
|
|
43
|
+
injectionResult: InjectionDetectionResult | null;
|
|
44
|
+
piiResult: PIIDetectionResult | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TimelineEntry {
|
|
48
|
+
time: number;
|
|
49
|
+
event: string;
|
|
50
|
+
detail: string;
|
|
51
|
+
type: "pass" | "injection" | "pii" | "compliance" | "info";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ComplianceMode = "standard" | "gdpr" | "hipaa";
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Timeline
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
export const timeline: TimelineEntry[] = [];
|
|
61
|
+
|
|
62
|
+
export function addTimeline(
|
|
63
|
+
event: string,
|
|
64
|
+
detail: string,
|
|
65
|
+
type: TimelineEntry["type"],
|
|
66
|
+
) {
|
|
67
|
+
timeline.unshift({ time: Date.now(), event, detail, type });
|
|
68
|
+
if (timeline.length > 50) {
|
|
69
|
+
timeline.length = 50;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Schema
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
export const schema = {
|
|
78
|
+
facts: {
|
|
79
|
+
messages: t.array<ChatMessage>(),
|
|
80
|
+
complianceMode: t.string<ComplianceMode>(),
|
|
81
|
+
redactionEnabled: t.boolean(),
|
|
82
|
+
blockedCount: t.number(),
|
|
83
|
+
injectionAttempts: t.number(),
|
|
84
|
+
piiDetections: t.number(),
|
|
85
|
+
complianceBlocks: t.number(),
|
|
86
|
+
},
|
|
87
|
+
derivations: {
|
|
88
|
+
messageCount: t.number(),
|
|
89
|
+
blockRate: t.string(),
|
|
90
|
+
piiTypeCounts: t.string(),
|
|
91
|
+
},
|
|
92
|
+
events: {
|
|
93
|
+
setComplianceMode: { value: t.string() },
|
|
94
|
+
toggleRedaction: {},
|
|
95
|
+
clearHistory: {},
|
|
96
|
+
},
|
|
97
|
+
requirements: {},
|
|
98
|
+
} satisfies ModuleSchema;
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Module
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
const guardrailModule = createModule("guardrails", {
|
|
105
|
+
schema,
|
|
106
|
+
|
|
107
|
+
init: (facts) => {
|
|
108
|
+
facts.messages = [];
|
|
109
|
+
facts.complianceMode = "standard";
|
|
110
|
+
facts.redactionEnabled = true;
|
|
111
|
+
facts.blockedCount = 0;
|
|
112
|
+
facts.injectionAttempts = 0;
|
|
113
|
+
facts.piiDetections = 0;
|
|
114
|
+
facts.complianceBlocks = 0;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
derive: {
|
|
118
|
+
messageCount: (facts) => facts.messages.length,
|
|
119
|
+
blockRate: (facts) => {
|
|
120
|
+
if (facts.messages.length === 0) {
|
|
121
|
+
return "0%";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const blocked = (facts.messages as ChatMessage[]).filter(
|
|
125
|
+
(m) => m.blocked,
|
|
126
|
+
).length;
|
|
127
|
+
|
|
128
|
+
return `${Math.round((blocked / facts.messages.length) * 100)}%`;
|
|
129
|
+
},
|
|
130
|
+
piiTypeCounts: (facts) => {
|
|
131
|
+
const counts: Record<string, number> = {};
|
|
132
|
+
for (const msg of facts.messages as ChatMessage[]) {
|
|
133
|
+
if (msg.piiResult?.detected) {
|
|
134
|
+
for (const item of msg.piiResult.items) {
|
|
135
|
+
counts[item.type] = (counts[item.type] ?? 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
Object.entries(counts)
|
|
142
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
143
|
+
.join(", ") || "none"
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
events: {
|
|
149
|
+
setComplianceMode: (facts, { value }) => {
|
|
150
|
+
facts.complianceMode = value as ComplianceMode;
|
|
151
|
+
},
|
|
152
|
+
toggleRedaction: (facts) => {
|
|
153
|
+
facts.redactionEnabled = !facts.redactionEnabled;
|
|
154
|
+
},
|
|
155
|
+
clearHistory: (facts) => {
|
|
156
|
+
facts.messages = [];
|
|
157
|
+
facts.blockedCount = 0;
|
|
158
|
+
facts.injectionAttempts = 0;
|
|
159
|
+
facts.piiDetections = 0;
|
|
160
|
+
facts.complianceBlocks = 0;
|
|
161
|
+
timeline.length = 0;
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// System
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
export const system = createSystem({
|
|
171
|
+
module: guardrailModule,
|
|
172
|
+
trace: true,
|
|
173
|
+
plugins: [devtoolsPlugin({ name: "ai-guardrails" })],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Analysis Functions
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Compliance gate: blocks the message when the active mode forbids the kinds
|
|
182
|
+
* of PII detected. Emits its own devtools event and returns whether it blocked.
|
|
183
|
+
*/
|
|
184
|
+
function runComplianceCheck(
|
|
185
|
+
piiResult: PIIDetectionResult,
|
|
186
|
+
textLength: number,
|
|
187
|
+
): boolean {
|
|
188
|
+
const mode = system.facts.complianceMode as ComplianceMode;
|
|
189
|
+
let blocked = false;
|
|
190
|
+
|
|
191
|
+
if (mode !== "standard" && piiResult.detected) {
|
|
192
|
+
// HIPAA Safe Harbor (45 CFR 164.514) enumerates 18 identifier classes —
|
|
193
|
+
// names, geographic data, dates, contact info, account/certificate/license
|
|
194
|
+
// numbers, IP addresses, and more. That is effectively every PII type this
|
|
195
|
+
// detector emits, so under HIPAA *any* detected PII is PHI and blocks.
|
|
196
|
+
const hasPHI = piiResult.detected;
|
|
197
|
+
const hasContactInfo = piiResult.items.some(
|
|
198
|
+
(i) =>
|
|
199
|
+
i.type === "email" ||
|
|
200
|
+
i.type === "phone" ||
|
|
201
|
+
i.type === "name" ||
|
|
202
|
+
i.type === "date_of_birth" ||
|
|
203
|
+
i.type === "address",
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (mode === "hipaa" && hasPHI) {
|
|
207
|
+
blocked = true;
|
|
208
|
+
system.facts.complianceBlocks =
|
|
209
|
+
(system.facts.complianceBlocks as number) + 1;
|
|
210
|
+
addTimeline("compliance", "HIPAA: PHI detected", "compliance");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (mode === "gdpr" && hasContactInfo) {
|
|
214
|
+
blocked = true;
|
|
215
|
+
system.facts.complianceBlocks =
|
|
216
|
+
(system.facts.complianceBlocks as number) + 1;
|
|
217
|
+
addTimeline("compliance", "GDPR: personal data detected", "compliance");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
emitDevToolsEvent({
|
|
222
|
+
type: "guardrail_check",
|
|
223
|
+
guardrailName: `compliance-${mode}`,
|
|
224
|
+
guardrailType: "input",
|
|
225
|
+
passed: !blocked || !piiResult.detected,
|
|
226
|
+
inputLength: textLength,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return blocked;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* When redaction is on, never persist raw PII into reactive facts: strip the
|
|
234
|
+
* plaintext `value` from each detected item before it reaches facts.
|
|
235
|
+
*/
|
|
236
|
+
function toSafePiiResult(
|
|
237
|
+
piiResult: PIIDetectionResult,
|
|
238
|
+
redactionEnabled: boolean,
|
|
239
|
+
): PIIDetectionResult | null {
|
|
240
|
+
if (!piiResult.detected) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!redactionEnabled) {
|
|
245
|
+
return piiResult;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...piiResult,
|
|
250
|
+
items: piiResult.items.map((item) => ({
|
|
251
|
+
...item,
|
|
252
|
+
value: "[redacted]",
|
|
253
|
+
})),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function analyzeMessage(text: string): Promise<ChatMessage> {
|
|
258
|
+
const id = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
259
|
+
let blocked = false;
|
|
260
|
+
|
|
261
|
+
// 1. Prompt injection detection
|
|
262
|
+
const injectionResult = detectPromptInjection(text);
|
|
263
|
+
if (injectionResult.detected) {
|
|
264
|
+
blocked = true;
|
|
265
|
+
system.facts.injectionAttempts =
|
|
266
|
+
(system.facts.injectionAttempts as number) + 1;
|
|
267
|
+
for (const p of injectionResult.patterns) {
|
|
268
|
+
addTimeline("injection", `${p.name} (${p.severity})`, "injection");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
emitDevToolsEvent({
|
|
273
|
+
type: "guardrail_check",
|
|
274
|
+
guardrailName: "prompt-injection",
|
|
275
|
+
guardrailType: "input",
|
|
276
|
+
passed: !injectionResult.detected,
|
|
277
|
+
inputLength: text.length,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// 2. PII detection. detectPII is detection-only; detectAndRedactPII also
|
|
281
|
+
// populates `redactedText` so we never have to mutate a returned result.
|
|
282
|
+
const redactionEnabled = system.facts.redactionEnabled;
|
|
283
|
+
const piiResult = redactionEnabled
|
|
284
|
+
? await detectAndRedactPII(text, { style: "typed" })
|
|
285
|
+
: await detectPII(text);
|
|
286
|
+
if (piiResult.detected) {
|
|
287
|
+
system.facts.piiDetections = (system.facts.piiDetections as number) + 1;
|
|
288
|
+
for (const item of piiResult.items) {
|
|
289
|
+
addTimeline("pii", `${item.type} found`, "pii");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
emitDevToolsEvent({
|
|
294
|
+
type: "guardrail_check",
|
|
295
|
+
guardrailName: "pii-detection",
|
|
296
|
+
guardrailType: "input",
|
|
297
|
+
passed: !piiResult.detected,
|
|
298
|
+
inputLength: text.length,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// 3. Compliance check
|
|
302
|
+
if (runComplianceCheck(piiResult, text.length)) {
|
|
303
|
+
blocked = true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (blocked) {
|
|
307
|
+
system.facts.blockedCount = (system.facts.blockedCount as number) + 1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!blocked && !piiResult.detected) {
|
|
311
|
+
addTimeline("pass", "message passed all checks", "pass");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const redactedText = piiResult.redactedText ?? text;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
id,
|
|
318
|
+
text: redactionEnabled ? redactedText : text,
|
|
319
|
+
blocked,
|
|
320
|
+
redactedText,
|
|
321
|
+
injectionResult: injectionResult.detected ? injectionResult : null,
|
|
322
|
+
piiResult: toSafePiiResult(piiResult, redactionEnabled),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## topic-guard
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// Example: topic-guard
|
|
331
|
+
// Source: examples/topic-guard/src/topic-guard.ts
|
|
332
|
+
// Pure module file — no DOM wiring
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Topic Guard — Directive Module
|
|
336
|
+
*
|
|
337
|
+
* Demonstrates input guardrails for AI agents. Messages are checked against
|
|
338
|
+
* configurable guardrails before reaching the mock agent. Blocked messages
|
|
339
|
+
* are rejected with an explanation; allowed messages get a mock response.
|
|
340
|
+
*/
|
|
341
|
+
|
|
342
|
+
import { type ModuleSchema, createModule, t } from "@directive-run/core";
|
|
343
|
+
import {
|
|
344
|
+
type GuardrailResult,
|
|
345
|
+
checkKeywordGuardrail,
|
|
346
|
+
checkTopicClassifier,
|
|
347
|
+
getMockAgentResponse,
|
|
348
|
+
} from "./mock-guardrails.js";
|
|
349
|
+
|
|
350
|
+
// ============================================================================
|
|
351
|
+
// Types
|
|
352
|
+
// ============================================================================
|
|
353
|
+
|
|
354
|
+
export interface ChatMessage {
|
|
355
|
+
role: "user" | "agent" | "system";
|
|
356
|
+
text: string;
|
|
357
|
+
blocked: boolean;
|
|
358
|
+
guardrail?: string;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface GuardrailLogEntry {
|
|
362
|
+
timestamp: number;
|
|
363
|
+
input: string;
|
|
364
|
+
result: GuardrailResult;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ============================================================================
|
|
368
|
+
// Schema
|
|
369
|
+
// ============================================================================
|
|
370
|
+
|
|
371
|
+
export const topicGuardSchema = {
|
|
372
|
+
facts: {
|
|
373
|
+
input: t.string(),
|
|
374
|
+
messages: t.array<ChatMessage>(),
|
|
375
|
+
isProcessing: t.boolean(),
|
|
376
|
+
lastGuardrailResult: t.object<GuardrailResult | null>(),
|
|
377
|
+
guardrailLog: t.array<GuardrailLogEntry>(),
|
|
378
|
+
allowedTopics: t.array<string>(),
|
|
379
|
+
},
|
|
380
|
+
derivations: {
|
|
381
|
+
messageCount: t.number(),
|
|
382
|
+
blockedCount: t.number(),
|
|
383
|
+
allowedCount: t.number(),
|
|
384
|
+
blockRate: t.string(),
|
|
385
|
+
canSend: t.boolean(),
|
|
386
|
+
lastMessageBlocked: t.boolean(),
|
|
387
|
+
},
|
|
388
|
+
events: {
|
|
389
|
+
send: {},
|
|
390
|
+
clear: {},
|
|
391
|
+
setInput: { value: t.string() },
|
|
392
|
+
toggleTopic: { topic: t.string() },
|
|
393
|
+
},
|
|
394
|
+
requirements: {
|
|
395
|
+
BLOCK_MESSAGE: {
|
|
396
|
+
reason: t.string(),
|
|
397
|
+
guardrailName: t.string(),
|
|
398
|
+
},
|
|
399
|
+
ALLOW_MESSAGE: {},
|
|
400
|
+
},
|
|
401
|
+
} satisfies ModuleSchema;
|
|
402
|
+
|
|
403
|
+
// ============================================================================
|
|
404
|
+
// Module
|
|
405
|
+
// ============================================================================
|
|
406
|
+
|
|
407
|
+
export const topicGuardModule = createModule("topic-guard", {
|
|
408
|
+
schema: topicGuardSchema,
|
|
409
|
+
|
|
410
|
+
init: (facts) => {
|
|
411
|
+
facts.input = "";
|
|
412
|
+
facts.messages = [];
|
|
413
|
+
facts.isProcessing = false;
|
|
414
|
+
facts.lastGuardrailResult = null;
|
|
415
|
+
facts.guardrailLog = [];
|
|
416
|
+
facts.allowedTopics = ["product", "billing", "support", "technical"];
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// Derivations
|
|
421
|
+
// ============================================================================
|
|
422
|
+
|
|
423
|
+
derive: {
|
|
424
|
+
messageCount: (facts) => {
|
|
425
|
+
return facts.messages.filter((m) => m.role === "user").length;
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
blockedCount: (facts) => {
|
|
429
|
+
return facts.messages.filter((m) => m.role === "user" && m.blocked)
|
|
430
|
+
.length;
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
allowedCount: (facts) => {
|
|
434
|
+
return facts.messages.filter((m) => m.role === "user" && !m.blocked)
|
|
435
|
+
.length;
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
blockRate: (_facts, derived) => {
|
|
439
|
+
const total = derived.messageCount;
|
|
440
|
+
if (total === 0) {
|
|
441
|
+
return "0%";
|
|
442
|
+
}
|
|
443
|
+
const blocked = derived.blockedCount;
|
|
444
|
+
const rate = Math.round((blocked / total) * 100);
|
|
445
|
+
|
|
446
|
+
return `${rate}%`;
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
canSend: (facts) => {
|
|
450
|
+
return facts.input.trim().length > 0 && !facts.isProcessing;
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
lastMessageBlocked: (facts) => {
|
|
454
|
+
const msgs = facts.messages;
|
|
455
|
+
if (msgs.length === 0) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return msgs[msgs.length - 1].blocked;
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// Events
|
|
465
|
+
// ============================================================================
|
|
466
|
+
|
|
467
|
+
events: {
|
|
468
|
+
send: (facts) => {
|
|
469
|
+
const text = facts.input.trim();
|
|
470
|
+
if (text.length === 0 || facts.isProcessing) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Add user message
|
|
475
|
+
const messages = [...facts.messages];
|
|
476
|
+
messages.push({ role: "user", text, blocked: false });
|
|
477
|
+
facts.messages = messages;
|
|
478
|
+
|
|
479
|
+
// Run guardrails
|
|
480
|
+
const keywordResult = checkKeywordGuardrail(text);
|
|
481
|
+
if (keywordResult.blocked) {
|
|
482
|
+
facts.lastGuardrailResult = keywordResult;
|
|
483
|
+
facts.isProcessing = true;
|
|
484
|
+
facts.input = "";
|
|
485
|
+
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const classifierResult = checkTopicClassifier(text, facts.allowedTopics);
|
|
490
|
+
facts.lastGuardrailResult = classifierResult;
|
|
491
|
+
facts.isProcessing = true;
|
|
492
|
+
facts.input = "";
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
clear: (facts) => {
|
|
496
|
+
facts.messages = [];
|
|
497
|
+
facts.guardrailLog = [];
|
|
498
|
+
facts.lastGuardrailResult = null;
|
|
499
|
+
facts.isProcessing = false;
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
setInput: (facts, { value }) => {
|
|
503
|
+
facts.input = value;
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
toggleTopic: (facts, { topic }) => {
|
|
507
|
+
const topics = [...facts.allowedTopics];
|
|
508
|
+
const idx = topics.indexOf(topic);
|
|
509
|
+
if (idx >= 0) {
|
|
510
|
+
topics.splice(idx, 1);
|
|
511
|
+
} else {
|
|
512
|
+
topics.push(topic);
|
|
513
|
+
}
|
|
514
|
+
facts.allowedTopics = topics;
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
// ============================================================================
|
|
519
|
+
// Constraints
|
|
520
|
+
// ============================================================================
|
|
521
|
+
|
|
522
|
+
constraints: {
|
|
523
|
+
offTopicDetected: {
|
|
524
|
+
priority: 100,
|
|
525
|
+
when: (facts) => {
|
|
526
|
+
return (
|
|
527
|
+
facts.lastGuardrailResult?.blocked === true && facts.isProcessing
|
|
528
|
+
);
|
|
529
|
+
},
|
|
530
|
+
require: (facts) => {
|
|
531
|
+
const result = facts.lastGuardrailResult!;
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
type: "BLOCK_MESSAGE",
|
|
535
|
+
reason: result.reason,
|
|
536
|
+
guardrailName: result.guardrailName,
|
|
537
|
+
};
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
onTopicConfirmed: {
|
|
542
|
+
priority: 90,
|
|
543
|
+
when: (facts) => {
|
|
544
|
+
return (
|
|
545
|
+
facts.lastGuardrailResult?.blocked === false && facts.isProcessing
|
|
546
|
+
);
|
|
547
|
+
},
|
|
548
|
+
require: () => ({
|
|
549
|
+
type: "ALLOW_MESSAGE",
|
|
550
|
+
}),
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
// ============================================================================
|
|
555
|
+
// Resolvers
|
|
556
|
+
// ============================================================================
|
|
557
|
+
|
|
558
|
+
resolvers: {
|
|
559
|
+
blockMessage: {
|
|
560
|
+
requirement: "BLOCK_MESSAGE",
|
|
561
|
+
resolve: async (req, context) => {
|
|
562
|
+
const messages = [...context.facts.messages];
|
|
563
|
+
// Mark the last user message as blocked
|
|
564
|
+
const lastUserIdx = messages.length - 1;
|
|
565
|
+
if (lastUserIdx >= 0) {
|
|
566
|
+
messages[lastUserIdx] = {
|
|
567
|
+
...messages[lastUserIdx],
|
|
568
|
+
blocked: true,
|
|
569
|
+
guardrail: req.guardrailName,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// Add system rejection message
|
|
573
|
+
messages.push({
|
|
574
|
+
role: "system",
|
|
575
|
+
text: "I can only help with product-related questions.",
|
|
576
|
+
blocked: true,
|
|
577
|
+
guardrail: req.guardrailName,
|
|
578
|
+
});
|
|
579
|
+
context.facts.messages = messages;
|
|
580
|
+
context.facts.isProcessing = false;
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
allowMessage: {
|
|
585
|
+
requirement: "ALLOW_MESSAGE",
|
|
586
|
+
resolve: async (_req, context) => {
|
|
587
|
+
const messages = [...context.facts.messages];
|
|
588
|
+
const lastUserMsg = messages.filter((m) => m.role === "user").pop();
|
|
589
|
+
const responseText = getMockAgentResponse(lastUserMsg?.text ?? "");
|
|
590
|
+
messages.push({
|
|
591
|
+
role: "agent",
|
|
592
|
+
text: responseText,
|
|
593
|
+
blocked: false,
|
|
594
|
+
});
|
|
595
|
+
context.facts.messages = messages;
|
|
596
|
+
context.facts.isProcessing = false;
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
// ============================================================================
|
|
602
|
+
// Effects
|
|
603
|
+
// ============================================================================
|
|
604
|
+
|
|
605
|
+
effects: {
|
|
606
|
+
logGuardrailResult: {
|
|
607
|
+
deps: ["lastGuardrailResult"],
|
|
608
|
+
run: (facts) => {
|
|
609
|
+
const result = facts.lastGuardrailResult;
|
|
610
|
+
if (!result) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const lastUserMsg = [...facts.messages]
|
|
615
|
+
.reverse()
|
|
616
|
+
.find((m) => m.role === "user");
|
|
617
|
+
const log = [...facts.guardrailLog];
|
|
618
|
+
log.push({
|
|
619
|
+
timestamp: Date.now(),
|
|
620
|
+
input: lastUserMsg?.text ?? "",
|
|
621
|
+
result,
|
|
622
|
+
});
|
|
623
|
+
facts.guardrailLog = log;
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
```
|