anthale 0.2.1 → 0.4.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.
@@ -0,0 +1,301 @@
1
+ import { Anthale } from '../client';
2
+ import { AnthaleError } from '../core/error';
3
+ import type { PolicyEnforceParams, PolicyEnforceResponse } from '../resources/organizations/policies';
4
+
5
+ /**
6
+ * Minimal Anthale client contract required by {@link PolicyEnforcer}.
7
+ *
8
+ * Pass your own client when you need shared configuration (timeouts, retries,
9
+ * telemetry), or let the enforcer construct one with `apiKey`.
10
+ */
11
+ export interface PolicyEnforcerClient {
12
+ organizations: {
13
+ policies: {
14
+ enforce: (
15
+ policyIdentifier: string,
16
+ body: PolicyEnforceParams,
17
+ ) => PromiseLike<PolicyEnforceResponse> | PolicyEnforceResponse;
18
+ };
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Options used to construct a {@link PolicyEnforcer}.
24
+ */
25
+ export interface PolicyEnforcerOptions {
26
+ /**
27
+ * Anthale policy identifier to enforce.
28
+ */
29
+ policyId: string;
30
+ /**
31
+ * Anthale API key used only when `client` is not provided.
32
+ */
33
+ apiKey?: string;
34
+ /**
35
+ * Optional pre-built Anthale client.
36
+ */
37
+ client?: PolicyEnforcerClient;
38
+ /**
39
+ * Metadata merged into every enforcement call.
40
+ */
41
+ metadata?: Record<string, unknown>;
42
+ }
43
+
44
+ /**
45
+ * Parameters for a single policy enforcement call.
46
+ */
47
+ export interface EnforceOptions {
48
+ /**
49
+ * Enforcement direction (`input` or `output`).
50
+ */
51
+ direction: PolicyEnforceParams['direction'];
52
+ /**
53
+ * Conversation messages to evaluate.
54
+ */
55
+ messages: PolicyEnforceParams['messages'];
56
+ /**
57
+ * Optional metadata merged over constructor metadata for this call.
58
+ */
59
+ metadata?: Record<string, unknown>;
60
+ }
61
+
62
+ /**
63
+ * Error raised when Anthale returns a blocking action.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { AnthalePolicyViolationError } from 'anthale/integrations/core';
68
+ *
69
+ * try {
70
+ * // ...
71
+ * } catch (error) {
72
+ * if (error instanceof AnthalePolicyViolationError) {
73
+ * console.error(error.enforcementIdentifier);
74
+ * }
75
+ * }
76
+ * ```
77
+ */
78
+ export class AnthalePolicyViolationError extends AnthaleError {
79
+ readonly enforcementIdentifier: string;
80
+
81
+ constructor(enforcementIdentifier: string) {
82
+ super(`Policy enforcement '${enforcementIdentifier}' was blocked due to a policy violation.`);
83
+ this.enforcementIdentifier = enforcementIdentifier;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Lightweight policy enforcer used by provider integrations.
89
+ *
90
+ * It always calls Anthale with `includeEvaluations: false` and throws
91
+ * {@link AnthalePolicyViolationError} when the resulting action is `block`.
92
+ */
93
+ export class PolicyEnforcer {
94
+ private readonly client: PolicyEnforcerClient;
95
+ private readonly policyId: string;
96
+ private readonly metadata: Record<string, unknown>;
97
+
98
+ /**
99
+ * Create a policy enforcer.
100
+ *
101
+ * @param options - Enforcer options.
102
+ */
103
+ constructor(options: PolicyEnforcerOptions) {
104
+ this.client = options.client ?? new Anthale({ apiKey: options.apiKey });
105
+ this.policyId = options.policyId;
106
+ this.metadata = options.metadata ?? {};
107
+ }
108
+
109
+ /**
110
+ * Enforce the configured Anthale policy on a message set.
111
+ *
112
+ * @param options - Enforcement options.
113
+ * @returns The raw Anthale enforcement response.
114
+ * @throws {AnthalePolicyViolationError} When Anthale action is `block`.
115
+ */
116
+ async enforce(options: EnforceOptions): Promise<PolicyEnforceResponse> {
117
+ const response = await this.client.organizations.policies.enforce(this.policyId, {
118
+ direction: options.direction,
119
+ messages: options.messages,
120
+ includeEvaluations: false,
121
+ metadata: { ...this.metadata, ...(options.metadata ?? {}) },
122
+ });
123
+
124
+ if (response.action === 'block') {
125
+ throw new AnthalePolicyViolationError(response.enforcerIdentifier);
126
+ }
127
+
128
+ return response;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Convenience factory for {@link PolicyEnforcer}.
134
+ */
135
+ export function buildPolicyEnforcer(options: PolicyEnforcerOptions): PolicyEnforcer {
136
+ return new PolicyEnforcer(options);
137
+ }
138
+
139
+ type MessageRole = PolicyEnforceParams.Message['role'];
140
+
141
+ const ROLE_MAP: Record<string, MessageRole> = {
142
+ system: 'system',
143
+ developer: 'system',
144
+ machine: 'system',
145
+ user: 'user',
146
+ human: 'user',
147
+ assistant: 'assistant',
148
+ ai: 'assistant',
149
+ tool: 'tool',
150
+ function: 'tool',
151
+ };
152
+
153
+ /**
154
+ * Normalize a role-like value to one of Anthale's supported roles.
155
+ *
156
+ * Unknown or non-string values fall back to `user`.
157
+ */
158
+ export function normalizeRole(value: unknown): MessageRole {
159
+ if (typeof value !== 'string') {
160
+ return 'user';
161
+ }
162
+
163
+ return ROLE_MAP[value.toLowerCase()] ?? 'user';
164
+ }
165
+
166
+ /**
167
+ * Safely stringify arbitrary values for message content.
168
+ */
169
+ export function stringify(value: unknown): string {
170
+ if (typeof value === 'string') {
171
+ return value;
172
+ }
173
+
174
+ if (value == null) {
175
+ return String(value);
176
+ }
177
+
178
+ if (typeof value === 'object') {
179
+ try {
180
+ return JSON.stringify(value);
181
+ } catch {
182
+ return String(value);
183
+ }
184
+ }
185
+
186
+ return String(value);
187
+ }
188
+
189
+ /**
190
+ * Normalize different message content shapes to a plain string.
191
+ *
192
+ * Handles raw strings, block arrays, and nested mappings commonly returned by
193
+ * OpenAI and LangChain payloads.
194
+ */
195
+ export function extractContent(raw: unknown): string {
196
+ if (raw == null) {
197
+ return '';
198
+ }
199
+
200
+ if (typeof raw === 'string') {
201
+ return raw;
202
+ }
203
+
204
+ if (Array.isArray(raw)) {
205
+ const parts = raw
206
+ .map((block) => {
207
+ if (typeof block === 'string') {
208
+ return block;
209
+ }
210
+
211
+ if (block && typeof block === 'object') {
212
+ const record = block as Record<string, unknown>;
213
+ return extractContent(
214
+ record['text'] ??
215
+ record['output_text'] ??
216
+ record['input_text'] ??
217
+ record['content'] ??
218
+ record['arguments'] ??
219
+ block,
220
+ );
221
+ }
222
+
223
+ return stringify(block);
224
+ })
225
+ .filter((part) => part.length > 0);
226
+
227
+ return parts.join('\n');
228
+ }
229
+
230
+ if (typeof raw === 'object') {
231
+ const record = raw as Record<string, unknown>;
232
+ return extractContent(
233
+ record['text'] ?? record['output_text'] ?? record['input_text'] ?? record['content'] ?? stringify(raw),
234
+ );
235
+ }
236
+
237
+ return stringify(raw);
238
+ }
239
+
240
+ /**
241
+ * Convert a role/content pair into an Anthale message.
242
+ *
243
+ * Empty/blank content returns `null`.
244
+ */
245
+ export function toAnthaleMessage(value: {
246
+ role: unknown;
247
+ content: unknown;
248
+ }): PolicyEnforceParams.Message | null {
249
+ const content = extractContent(value.content);
250
+ if (!content || content.trim() === '' || content.trim() === 'None') {
251
+ return null;
252
+ }
253
+
254
+ return {
255
+ role: normalizeRole(value.role),
256
+ content,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Recursively convert unknown payload values into Anthale messages.
262
+ *
263
+ * This function is intentionally permissive so integrations can normalize
264
+ * heterogeneous provider payloads with a single utility.
265
+ */
266
+ export function messagesFromValue(value: unknown, defaultRole: MessageRole): PolicyEnforceParams.Message[] {
267
+ if (value == null) {
268
+ return [];
269
+ }
270
+
271
+ if (Array.isArray(value)) {
272
+ const out: PolicyEnforceParams.Message[] = [];
273
+ for (const item of value) {
274
+ out.push(...messagesFromValue(item, defaultRole));
275
+ }
276
+
277
+ return out;
278
+ }
279
+
280
+ if (typeof value === 'object') {
281
+ const record = value as Record<string, unknown>;
282
+ const message = toAnthaleMessage({
283
+ role: record['role'] ?? defaultRole,
284
+ content:
285
+ record['content'] ??
286
+ record['text'] ??
287
+ record['input_text'] ??
288
+ record['output_text'] ??
289
+ record['arguments'],
290
+ });
291
+
292
+ return message == null ? [] : [message];
293
+ }
294
+
295
+ const content = stringify(value);
296
+ if (!content || content.trim() === '' || content.trim() === 'None') {
297
+ return [];
298
+ }
299
+
300
+ return [{ role: normalizeRole(defaultRole), content }];
301
+ }