@framers/agentos-ext-topicality 0.1.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/LICENSE +23 -0
- package/dist/TopicDriftTracker.d.ts +152 -0
- package/dist/TopicDriftTracker.d.ts.map +1 -0
- package/dist/TopicDriftTracker.js +265 -0
- package/dist/TopicDriftTracker.js.map +1 -0
- package/dist/TopicEmbeddingIndex.d.ts +160 -0
- package/dist/TopicEmbeddingIndex.d.ts.map +1 -0
- package/dist/TopicEmbeddingIndex.js +291 -0
- package/dist/TopicEmbeddingIndex.js.map +1 -0
- package/dist/TopicalityGuardrail.d.ts +196 -0
- package/dist/TopicalityGuardrail.d.ts.map +1 -0
- package/dist/TopicalityGuardrail.js +426 -0
- package/dist/TopicalityGuardrail.js.map +1 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +259 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/CheckTopicTool.d.ts +148 -0
- package/dist/tools/CheckTopicTool.d.ts.map +1 -0
- package/dist/tools/CheckTopicTool.js +202 -0
- package/dist/tools/CheckTopicTool.js.map +1 -0
- package/dist/types.d.ts +358 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +215 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -0
- package/src/TopicDriftTracker.ts +307 -0
- package/src/TopicEmbeddingIndex.ts +346 -0
- package/src/TopicalityGuardrail.ts +521 -0
- package/src/index.ts +302 -0
- package/src/tools/CheckTopicTool.ts +296 -0
- package/src/types.ts +565 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview TopicEmbeddingIndex — semantic similarity lookup for topic guardrails.
|
|
3
|
+
*
|
|
4
|
+
* This module implements a lightweight in-memory embedding index that:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Builds** per-topic centroid embeddings from descriptions + examples.
|
|
7
|
+
* 2. **Matches** an arbitrary embedding or text string against all topic centroids
|
|
8
|
+
* using cosine similarity.
|
|
9
|
+
* 3. **Answers** boolean on-topic queries at a configurable similarity threshold.
|
|
10
|
+
*
|
|
11
|
+
* ### How centroids are built
|
|
12
|
+
* For each {@link TopicDescriptor} the index concatenates:
|
|
13
|
+
* ```
|
|
14
|
+
* texts = [descriptor.description, ...descriptor.examples]
|
|
15
|
+
* ```
|
|
16
|
+
* All topics are embedded in a single batch call to `embeddingFn` to minimise
|
|
17
|
+
* round-trips. The centroid for a topic is the component-wise average (mean)
|
|
18
|
+
* of all its embedding vectors.
|
|
19
|
+
*
|
|
20
|
+
* ### Similarity scoring
|
|
21
|
+
* Raw cosine similarity can be negative when vectors point in opposite directions.
|
|
22
|
+
* `matchByVector` clamps scores to `Math.max(0, similarity)` so that all
|
|
23
|
+
* {@link TopicMatch} values represent non-negative relevance scores.
|
|
24
|
+
*
|
|
25
|
+
* @module topicality/TopicEmbeddingIndex
|
|
26
|
+
*/
|
|
27
|
+
import { cosineSimilarity } from '@framers/agentos/core/utils/text-utils';
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// TopicEmbeddingIndex
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/**
|
|
32
|
+
* Semantic embedding index for topicality guardrail matching.
|
|
33
|
+
*
|
|
34
|
+
* The index is intentionally **lazy** — it holds no embeddings until
|
|
35
|
+
* {@link build} is called. This makes instantiation cheap and lets the
|
|
36
|
+
* caller defer the (potentially expensive) batch embedding call until the
|
|
37
|
+
* agent's first message.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const index = new TopicEmbeddingIndex(async (texts) => {
|
|
42
|
+
* const res = await openai.embeddings.create({ model: 'text-embedding-3-small', input: texts });
|
|
43
|
+
* return res.data.map(d => d.embedding);
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* await index.build(TOPIC_PRESETS.customerSupport);
|
|
47
|
+
*
|
|
48
|
+
* const matches = await index.match('How do I cancel my subscription?');
|
|
49
|
+
* // → [{ topicId: 'billing', topicName: 'Billing & Payments', similarity: 0.82 }, ...]
|
|
50
|
+
*
|
|
51
|
+
* const onTopic = await index.isOnTopic('Tell me a joke', 0.35);
|
|
52
|
+
* // → false (a joke doesn't match any customer-support topic)
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export class TopicEmbeddingIndex {
|
|
56
|
+
/**
|
|
57
|
+
* Caller-supplied batch embedding function.
|
|
58
|
+
* Invoked once during {@link build} with all topic texts concatenated.
|
|
59
|
+
*/
|
|
60
|
+
embeddingFn;
|
|
61
|
+
/**
|
|
62
|
+
* Internal store mapping `topicId → TopicEntry`.
|
|
63
|
+
* Populated by {@link build}; empty until then.
|
|
64
|
+
*/
|
|
65
|
+
entries = new Map();
|
|
66
|
+
/** Whether {@link build} has been called and completed successfully. */
|
|
67
|
+
built = false;
|
|
68
|
+
// -------------------------------------------------------------------------
|
|
69
|
+
// Constructor
|
|
70
|
+
// -------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* Creates a new `TopicEmbeddingIndex`.
|
|
73
|
+
*
|
|
74
|
+
* @param embeddingFn - Async function that converts an array of text strings
|
|
75
|
+
* into corresponding numeric embedding vectors. All returned vectors must
|
|
76
|
+
* share the same dimensionality. The function is called exactly **once**
|
|
77
|
+
* per {@link build} invocation with all texts for all topics batched
|
|
78
|
+
* together.
|
|
79
|
+
*/
|
|
80
|
+
constructor(embeddingFn) {
|
|
81
|
+
this.embeddingFn = embeddingFn;
|
|
82
|
+
}
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
// Public API — build
|
|
85
|
+
// -------------------------------------------------------------------------
|
|
86
|
+
/**
|
|
87
|
+
* Embeds all topic descriptions and examples, computes per-topic centroid
|
|
88
|
+
* embeddings, and stores them in the internal index.
|
|
89
|
+
*
|
|
90
|
+
* Calling `build()` a second time replaces the existing index entirely,
|
|
91
|
+
* allowing hot-reloading of topic configurations without recreating the
|
|
92
|
+
* instance.
|
|
93
|
+
*
|
|
94
|
+
* ### Centroid computation
|
|
95
|
+
* For each topic we collect `[description, ...examples]` as a list of
|
|
96
|
+
* strings, embed them all in one batch, then average the resulting vectors
|
|
97
|
+
* component-wise to produce a single representative centroid.
|
|
98
|
+
*
|
|
99
|
+
* All topics are embedded in a **single batch call** to minimise latency.
|
|
100
|
+
*
|
|
101
|
+
* @param topics - Array of {@link TopicDescriptor} objects to index.
|
|
102
|
+
* An empty array is valid — the index will simply return no matches.
|
|
103
|
+
* @returns A promise that resolves once all embeddings are computed and
|
|
104
|
+
* stored. Rejects if `embeddingFn` throws or returns vectors of
|
|
105
|
+
* mismatched length.
|
|
106
|
+
*/
|
|
107
|
+
async build(topics) {
|
|
108
|
+
// Reset state before (re)building so a failed build leaves the index empty
|
|
109
|
+
// rather than in a partial state.
|
|
110
|
+
this.entries.clear();
|
|
111
|
+
this.built = false;
|
|
112
|
+
if (topics.length === 0) {
|
|
113
|
+
// Nothing to embed — mark as built so isBuilt returns true.
|
|
114
|
+
this.built = true;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Collect, per topic, the list of texts to embed and the range of
|
|
118
|
+
// indices they will occupy in the flat batch array.
|
|
119
|
+
//
|
|
120
|
+
// Layout: [topic0_desc, topic0_ex0, topic0_ex1, …, topic1_desc, …]
|
|
121
|
+
const allTexts = [];
|
|
122
|
+
const topicRanges = [];
|
|
123
|
+
for (const topic of topics) {
|
|
124
|
+
const start = allTexts.length;
|
|
125
|
+
// Always include the description as the first text for this topic.
|
|
126
|
+
allTexts.push(topic.description);
|
|
127
|
+
// Then all examples (may be empty — the centroid will just be the description).
|
|
128
|
+
for (const example of topic.examples) {
|
|
129
|
+
allTexts.push(example);
|
|
130
|
+
}
|
|
131
|
+
const end = allTexts.length; // exclusive
|
|
132
|
+
topicRanges.push({ topic, start, end });
|
|
133
|
+
}
|
|
134
|
+
// Single batch embedding call — one round-trip regardless of how many
|
|
135
|
+
// topics or examples are configured.
|
|
136
|
+
const allEmbeddings = await this.embeddingFn(allTexts);
|
|
137
|
+
// Validate that the embedding function returned the right number of vectors.
|
|
138
|
+
if (allEmbeddings.length !== allTexts.length) {
|
|
139
|
+
throw new Error(`TopicEmbeddingIndex.build: embeddingFn returned ${allEmbeddings.length} vectors ` +
|
|
140
|
+
`but ${allTexts.length} texts were provided.`);
|
|
141
|
+
}
|
|
142
|
+
// Compute centroid for each topic from its slice of the batch result.
|
|
143
|
+
for (const { topic, start, end } of topicRanges) {
|
|
144
|
+
const slice = allEmbeddings.slice(start, end);
|
|
145
|
+
const centroid = computeCentroid(slice);
|
|
146
|
+
this.entries.set(topic.id, { descriptor: topic, centroid });
|
|
147
|
+
}
|
|
148
|
+
this.built = true;
|
|
149
|
+
}
|
|
150
|
+
// -------------------------------------------------------------------------
|
|
151
|
+
// Public API — matchByVector
|
|
152
|
+
// -------------------------------------------------------------------------
|
|
153
|
+
/**
|
|
154
|
+
* Computes similarity between a pre-computed embedding vector and all topic
|
|
155
|
+
* centroids **without** making any additional embedding calls.
|
|
156
|
+
*
|
|
157
|
+
* This is the hot path invoked by {@link TopicDriftTracker}, which maintains
|
|
158
|
+
* its own running embedding and never needs to re-embed.
|
|
159
|
+
*
|
|
160
|
+
* Results are clamped to `[0, 1]` (negative cosine → 0) and sorted
|
|
161
|
+
* descending by similarity.
|
|
162
|
+
*
|
|
163
|
+
* @param embedding - A numeric vector with the same dimensionality as the
|
|
164
|
+
* centroids produced during {@link build}.
|
|
165
|
+
* @returns Array of {@link TopicMatch} objects sorted by similarity
|
|
166
|
+
* descending. Returns an empty array if the index was not yet built or
|
|
167
|
+
* contains no topics.
|
|
168
|
+
*/
|
|
169
|
+
matchByVector(embedding) {
|
|
170
|
+
if (!this.built || this.entries.size === 0) {
|
|
171
|
+
// Return empty rather than throwing — callers can treat no match as off-topic.
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const matches = [];
|
|
175
|
+
for (const [topicId, entry] of this.entries) {
|
|
176
|
+
const raw = cosineSimilarity(embedding, entry.centroid);
|
|
177
|
+
// Clamp to [0, 1] — negative similarity means "opposite direction" which
|
|
178
|
+
// is no more useful than "unrelated" for topic matching.
|
|
179
|
+
const similarity = Math.max(0, raw);
|
|
180
|
+
matches.push({
|
|
181
|
+
topicId,
|
|
182
|
+
topicName: entry.descriptor.name,
|
|
183
|
+
similarity,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Sort descending so the best match is first.
|
|
187
|
+
matches.sort((a, b) => b.similarity - a.similarity);
|
|
188
|
+
return matches;
|
|
189
|
+
}
|
|
190
|
+
// -------------------------------------------------------------------------
|
|
191
|
+
// Public API — match
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
/**
|
|
194
|
+
* Embeds `text` and returns similarity scores against all topic centroids.
|
|
195
|
+
*
|
|
196
|
+
* This is a convenience wrapper that handles the embedding step. If you
|
|
197
|
+
* already have an embedding (e.g. from the drift tracker's running vector)
|
|
198
|
+
* prefer {@link matchByVector} to avoid a redundant embedding call.
|
|
199
|
+
*
|
|
200
|
+
* @param text - The user message or assistant output to evaluate.
|
|
201
|
+
* @returns A promise resolving to {@link TopicMatch}[] sorted descending.
|
|
202
|
+
*/
|
|
203
|
+
async match(text) {
|
|
204
|
+
// Embed the single query text.
|
|
205
|
+
const [embedding] = await this.embeddingFn([text]);
|
|
206
|
+
return this.matchByVector(embedding);
|
|
207
|
+
}
|
|
208
|
+
// -------------------------------------------------------------------------
|
|
209
|
+
// Public API — isOnTopicByVector
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
/**
|
|
212
|
+
* Returns `true` if the given embedding vector scores above `threshold`
|
|
213
|
+
* against **at least one** topic in the index.
|
|
214
|
+
*
|
|
215
|
+
* Uses {@link matchByVector} internally so no additional embedding call is
|
|
216
|
+
* made.
|
|
217
|
+
*
|
|
218
|
+
* @param embedding - Pre-computed numeric vector.
|
|
219
|
+
* @param threshold - Minimum similarity (in `[0, 1]`) for a topic to count
|
|
220
|
+
* as a match.
|
|
221
|
+
* @returns `true` if any topic centroid has similarity > threshold; otherwise `false`.
|
|
222
|
+
*/
|
|
223
|
+
isOnTopicByVector(embedding, threshold) {
|
|
224
|
+
const matches = this.matchByVector(embedding);
|
|
225
|
+
// The list is sorted descending, so we only need to check the first entry.
|
|
226
|
+
return matches.length > 0 && matches[0].similarity > threshold;
|
|
227
|
+
}
|
|
228
|
+
// -------------------------------------------------------------------------
|
|
229
|
+
// Public API — isOnTopic
|
|
230
|
+
// -------------------------------------------------------------------------
|
|
231
|
+
/**
|
|
232
|
+
* Embeds `text` and returns `true` if it scores above `threshold` against
|
|
233
|
+
* at least one allowed topic.
|
|
234
|
+
*
|
|
235
|
+
* @param text - The text to evaluate.
|
|
236
|
+
* @param threshold - Minimum cosine similarity for the text to be considered on-topic.
|
|
237
|
+
* @returns A promise resolving to `true` if on-topic, `false` otherwise.
|
|
238
|
+
*/
|
|
239
|
+
async isOnTopic(text, threshold) {
|
|
240
|
+
const [embedding] = await this.embeddingFn([text]);
|
|
241
|
+
return this.isOnTopicByVector(embedding, threshold);
|
|
242
|
+
}
|
|
243
|
+
// -------------------------------------------------------------------------
|
|
244
|
+
// Getter
|
|
245
|
+
// -------------------------------------------------------------------------
|
|
246
|
+
/**
|
|
247
|
+
* Whether {@link build} has been called and completed successfully.
|
|
248
|
+
*
|
|
249
|
+
* Use this to guard against calling {@link match} or {@link matchByVector}
|
|
250
|
+
* before the index is ready.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* if (!index.isBuilt) await index.build(topics);
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
get isBuilt() {
|
|
258
|
+
return this.built;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Internal helpers
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
/**
|
|
265
|
+
* Computes the component-wise average (centroid) of an array of embedding
|
|
266
|
+
* vectors.
|
|
267
|
+
*
|
|
268
|
+
* All input vectors are assumed to have the same dimensionality. If the
|
|
269
|
+
* input array is empty an empty array is returned (safe no-op).
|
|
270
|
+
*
|
|
271
|
+
* @param vectors - One or more numeric vectors of equal length.
|
|
272
|
+
* @returns A single vector whose i-th element is the mean of i-th elements
|
|
273
|
+
* across all input vectors.
|
|
274
|
+
*
|
|
275
|
+
* @internal
|
|
276
|
+
*/
|
|
277
|
+
function computeCentroid(vectors) {
|
|
278
|
+
if (vectors.length === 0)
|
|
279
|
+
return [];
|
|
280
|
+
const dim = vectors[0].length;
|
|
281
|
+
// Initialise accumulator to all zeros.
|
|
282
|
+
const sum = new Array(dim).fill(0);
|
|
283
|
+
for (const vec of vectors) {
|
|
284
|
+
for (let i = 0; i < dim; i++) {
|
|
285
|
+
sum[i] += vec[i];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Divide each component by the number of vectors to get the mean.
|
|
289
|
+
return sum.map((v) => v / vectors.length);
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=TopicEmbeddingIndex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopicEmbeddingIndex.js","sourceRoot":"","sources":["../src/TopicEmbeddingIndex.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview IGuardrailService implementation for topicality enforcement.
|
|
3
|
+
*
|
|
4
|
+
* `TopicalityGuardrail` evaluates user input (and optionally agent output)
|
|
5
|
+
* against configured allowed and forbidden topic sets using semantic
|
|
6
|
+
* embedding similarity. It enforces three independent policy checks:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Forbidden topics** — Messages that score above `forbiddenThreshold`
|
|
9
|
+
* against any forbidden topic are blocked (or flagged).
|
|
10
|
+
* 2. **Off-topic detection** — Messages that score below `allowedThreshold`
|
|
11
|
+
* against *all* allowed topics are flagged (or blocked/redirected).
|
|
12
|
+
* 3. **Session drift** — An EMA-based tracker flags sustained drift away
|
|
13
|
+
* from allowed topics across consecutive messages.
|
|
14
|
+
*
|
|
15
|
+
* ### Lazy initialisation
|
|
16
|
+
* Embedding indices are built on the **first evaluation call**, not at
|
|
17
|
+
* construction time. This keeps instantiation cheap and defers the
|
|
18
|
+
* potentially expensive batch embedding call until the agent actually
|
|
19
|
+
* receives its first message.
|
|
20
|
+
*
|
|
21
|
+
* ### Fail-open semantics
|
|
22
|
+
* All evaluation methods wrap their logic in try/catch. If the embedding
|
|
23
|
+
* function throws, or any other unexpected error occurs, the guardrail
|
|
24
|
+
* logs a warning and returns `null` (pass) to avoid blocking legitimate
|
|
25
|
+
* traffic due to infrastructure failures.
|
|
26
|
+
*
|
|
27
|
+
* @module topicality/TopicalityGuardrail
|
|
28
|
+
*/
|
|
29
|
+
import type { GuardrailConfig, GuardrailEvaluationResult, GuardrailInputPayload, GuardrailOutputPayload, IGuardrailService } from '@framers/agentos';
|
|
30
|
+
import type { ISharedServiceRegistry } from '@framers/agentos';
|
|
31
|
+
import type { TopicalityPackOptions } from './types';
|
|
32
|
+
/**
|
|
33
|
+
* Guardrail that enforces topicality constraints via semantic embeddings.
|
|
34
|
+
*
|
|
35
|
+
* Implements {@link IGuardrailService} with Phase 2 (parallel) semantics:
|
|
36
|
+
* `evaluateStreamingChunks: false` and `canSanitize: false`. The guardrail
|
|
37
|
+
* never modifies content — it only blocks or flags.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const guardrail = new TopicalityGuardrail(registry, {
|
|
42
|
+
* allowedTopics: TOPIC_PRESETS.customerSupport,
|
|
43
|
+
* forbiddenTopics: TOPIC_PRESETS.commonUnsafe,
|
|
44
|
+
* forbiddenAction: 'block',
|
|
45
|
+
* offTopicAction: 'flag',
|
|
46
|
+
* }, embeddingFn);
|
|
47
|
+
*
|
|
48
|
+
* const result = await guardrail.evaluateInput(payload);
|
|
49
|
+
* if (result?.action === GuardrailAction.BLOCK) {
|
|
50
|
+
* // Reject the message
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare class TopicalityGuardrail implements IGuardrailService {
|
|
55
|
+
/**
|
|
56
|
+
* Guardrail pipeline configuration.
|
|
57
|
+
*
|
|
58
|
+
* - `evaluateStreamingChunks: false` — topicality evaluation requires
|
|
59
|
+
* complete text, not partial deltas.
|
|
60
|
+
* - `canSanitize: false` — this guardrail only blocks or flags; it never
|
|
61
|
+
* modifies content, so it runs in Phase 2 (parallel) of the pipeline.
|
|
62
|
+
*/
|
|
63
|
+
readonly config: GuardrailConfig;
|
|
64
|
+
/** Shared service registry provided by the extension manager. */
|
|
65
|
+
private readonly services;
|
|
66
|
+
/** Resolved pack options with caller overrides. */
|
|
67
|
+
private readonly options;
|
|
68
|
+
/** Caller-supplied or registry-backed embedding function. */
|
|
69
|
+
private readonly embeddingFn;
|
|
70
|
+
/**
|
|
71
|
+
* Embedding index for allowed topics. Lazily built on the first
|
|
72
|
+
* evaluation call. `null` until built or if no allowed topics are
|
|
73
|
+
* configured.
|
|
74
|
+
*/
|
|
75
|
+
private allowedIndex;
|
|
76
|
+
/**
|
|
77
|
+
* Embedding index for forbidden topics. Lazily built on the first
|
|
78
|
+
* evaluation call. `null` until built or if no forbidden topics are
|
|
79
|
+
* configured.
|
|
80
|
+
*/
|
|
81
|
+
private forbiddenIndex;
|
|
82
|
+
/**
|
|
83
|
+
* Session-level EMA drift tracker. Only instantiated when
|
|
84
|
+
* `enableDriftDetection` is `true` (default). `null` otherwise.
|
|
85
|
+
*/
|
|
86
|
+
private driftTracker;
|
|
87
|
+
/**
|
|
88
|
+
* Which side of the conversation to evaluate.
|
|
89
|
+
* - `'input'` — only user messages
|
|
90
|
+
* - `'output'` — only agent responses
|
|
91
|
+
* - `'both'` — both directions
|
|
92
|
+
*/
|
|
93
|
+
private readonly scope;
|
|
94
|
+
/**
|
|
95
|
+
* Minimum similarity to any allowed topic for the message to be
|
|
96
|
+
* considered on-topic.
|
|
97
|
+
*/
|
|
98
|
+
private readonly allowedThreshold;
|
|
99
|
+
/**
|
|
100
|
+
* Similarity above which a forbidden topic match triggers action.
|
|
101
|
+
*/
|
|
102
|
+
private readonly forbiddenThreshold;
|
|
103
|
+
/**
|
|
104
|
+
* Whether the lazy initialisation of embedding indices has been
|
|
105
|
+
* performed. Prevents redundant build calls.
|
|
106
|
+
*/
|
|
107
|
+
private indicesBuilt;
|
|
108
|
+
/**
|
|
109
|
+
* Creates a new `TopicalityGuardrail`.
|
|
110
|
+
*
|
|
111
|
+
* @param services - Shared service registry for heavyweight resource sharing.
|
|
112
|
+
* @param options - Pack-level configuration (topics, thresholds, actions).
|
|
113
|
+
* @param embeddingFn - Optional explicit embedding function. When omitted,
|
|
114
|
+
* the guardrail falls back to requesting an EmbeddingManager from the
|
|
115
|
+
* shared service registry at evaluation time.
|
|
116
|
+
*/
|
|
117
|
+
constructor(services: ISharedServiceRegistry, options: TopicalityPackOptions, embeddingFn?: (texts: string[]) => Promise<number[][]>);
|
|
118
|
+
/**
|
|
119
|
+
* Clears any session-level drift-tracking state held by this guardrail.
|
|
120
|
+
*
|
|
121
|
+
* Called by the topicality pack's `onDeactivate` hook so long-lived agents
|
|
122
|
+
* do not retain per-session EMA state after the pack is removed or the
|
|
123
|
+
* agent shuts down.
|
|
124
|
+
*/
|
|
125
|
+
clearSessionState(): void;
|
|
126
|
+
/**
|
|
127
|
+
* Evaluates a user input message against configured topic constraints.
|
|
128
|
+
*
|
|
129
|
+
* When `scope` is `'output'`, this method immediately returns `null`
|
|
130
|
+
* because input evaluation is disabled.
|
|
131
|
+
*
|
|
132
|
+
* @param payload - The input payload containing the user message text and
|
|
133
|
+
* session context.
|
|
134
|
+
* @returns A guardrail evaluation result (BLOCK or FLAG), or `null` if
|
|
135
|
+
* the message passes all topic checks. Returns `null` on any error
|
|
136
|
+
* (fail-open).
|
|
137
|
+
*/
|
|
138
|
+
evaluateInput(payload: GuardrailInputPayload): Promise<GuardrailEvaluationResult | null>;
|
|
139
|
+
/**
|
|
140
|
+
* Evaluates an agent output chunk against configured topic constraints.
|
|
141
|
+
*
|
|
142
|
+
* When `scope` is `'input'`, this method immediately returns `null`
|
|
143
|
+
* because output evaluation is disabled.
|
|
144
|
+
*
|
|
145
|
+
* For output evaluation, the guardrail extracts text from the response
|
|
146
|
+
* chunk's `finalResponseText` field (since `evaluateStreamingChunks` is
|
|
147
|
+
* `false`, only FINAL_RESPONSE chunks are seen).
|
|
148
|
+
*
|
|
149
|
+
* @param payload - The output payload containing the response chunk and
|
|
150
|
+
* session context.
|
|
151
|
+
* @returns A guardrail evaluation result (BLOCK or FLAG), or `null` if
|
|
152
|
+
* the output passes all topic checks. Returns `null` on any error
|
|
153
|
+
* (fail-open).
|
|
154
|
+
*/
|
|
155
|
+
evaluateOutput(payload: GuardrailOutputPayload): Promise<GuardrailEvaluationResult | null>;
|
|
156
|
+
/**
|
|
157
|
+
* Runs the three-stage topicality evaluation pipeline on a pre-computed
|
|
158
|
+
* embedding vector.
|
|
159
|
+
*
|
|
160
|
+
* Evaluation order:
|
|
161
|
+
* 1. Forbidden topic check (highest priority — immediate block/flag)
|
|
162
|
+
* 2. Off-topic check against allowed topics
|
|
163
|
+
* 3. Session drift check (only if drift detection is enabled and allowed
|
|
164
|
+
* topics are configured)
|
|
165
|
+
*
|
|
166
|
+
* @param embedding - Pre-computed embedding vector for the text.
|
|
167
|
+
* @param sessionId - Session identifier for drift tracking.
|
|
168
|
+
* @returns A {@link GuardrailEvaluationResult} if any check triggers, or
|
|
169
|
+
* `null` if all checks pass.
|
|
170
|
+
*
|
|
171
|
+
* @internal
|
|
172
|
+
*/
|
|
173
|
+
private evaluateEmbedding;
|
|
174
|
+
/**
|
|
175
|
+
* Ensures that the allowed and forbidden embedding indices have been built.
|
|
176
|
+
*
|
|
177
|
+
* Called once before the first evaluation. Subsequent calls are no-ops
|
|
178
|
+
* (guarded by the `indicesBuilt` flag).
|
|
179
|
+
*
|
|
180
|
+
* @internal
|
|
181
|
+
*/
|
|
182
|
+
private ensureIndicesBuilt;
|
|
183
|
+
/**
|
|
184
|
+
* Creates an embedding function that retrieves an EmbeddingManager from
|
|
185
|
+
* the shared service registry at call time.
|
|
186
|
+
*
|
|
187
|
+
* This fallback is used when no explicit `embeddingFn` is provided to
|
|
188
|
+
* the constructor. It throws if the EmbeddingManager service is not
|
|
189
|
+
* available in the registry.
|
|
190
|
+
*
|
|
191
|
+
* @returns An async embedding function.
|
|
192
|
+
* @internal
|
|
193
|
+
*/
|
|
194
|
+
private createRegistryEmbeddingFn;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=TopicalityGuardrail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopicalityGuardrail.d.ts","sourceRoot":"","sources":["../src/TopicalityGuardrail.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,EACtB,iBAAiB,EAClB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AA+BrD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,mBAAoB,YAAW,iBAAiB;IAK3D;;;;;;;OAOG;IACH,SAAgB,MAAM,EAAE,eAAe,CAGrC;IAMF,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAElD,mDAAmD;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAEhD,6DAA6D;IAC7D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2C;IAEvE;;;;OAIG;IACH,OAAO,CAAC,YAAY,CAAoC;IAExD;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAoC;IAE1D;;;OAGG;IACH,OAAO,CAAC,YAAY,CAAkC;IAEtD;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IAEpD;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAE5C;;;OAGG;IACH,OAAO,CAAC,YAAY,CAAS;IAM7B;;;;;;;;OAQG;gBAED,QAAQ,EAAE,sBAAsB,EAChC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IAsBxD;;;;;;OAMG;IACH,iBAAiB,IAAI,IAAI;IAQzB;;;;;;;;;;;OAWG;IACG,aAAa,CACjB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC;IAoC5C;;;;;;;;;;;;;;;OAeG;IACG,cAAc,CAClB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC;IAyC5C;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,iBAAiB;IAkHzB;;;;;;;OAOG;YACW,kBAAkB;IAwBhC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,yBAAyB;CAiBlC"}
|