@wmaurer/effect-ai-amazon-bedrock 0.3.0 → 0.4.1
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/CHANGELOG.md +21 -0
- package/README.md +99 -9
- package/dist/AmazonBedrockClient.d.ts +2 -2
- package/dist/AmazonBedrockClient.d.ts.map +1 -1
- package/dist/AmazonBedrockLanguageModel.d.ts +188 -25
- package/dist/AmazonBedrockLanguageModel.d.ts.map +1 -1
- package/dist/AmazonBedrockLanguageModel.js +223 -25
- package/dist/AmazonBedrockLanguageModel.js.map +1 -1
- package/dist/AmazonBedrockSchema.d.ts +216 -4
- package/dist/AmazonBedrockSchema.d.ts.map +1 -1
- package/dist/AmazonBedrockSchema.js +175 -9
- package/dist/AmazonBedrockSchema.js.map +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +27 -18
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/guardrails.d.ts +15 -0
- package/dist/internal/guardrails.d.ts.map +1 -0
- package/dist/internal/guardrails.js +77 -0
- package/dist/internal/guardrails.js.map +1 -0
- package/package.json +1 -1
- package/src/AmazonBedrockClient.ts +2 -2
- package/src/AmazonBedrockLanguageModel.ts +409 -46
- package/src/AmazonBedrockSchema.ts +193 -9
- package/src/internal/errors.ts +35 -19
- package/src/internal/guardrails.ts +91 -0
|
@@ -377,6 +377,79 @@ export class CitationsContentBlock extends Schema.Class<CitationsContentBlock>(
|
|
|
377
377
|
citations: Schema.optional(Schema.Array(Citation))
|
|
378
378
|
}) {}
|
|
379
379
|
|
|
380
|
+
/**
|
|
381
|
+
* The role a guarded text block plays in a contextual grounding check.
|
|
382
|
+
*
|
|
383
|
+
* **Details**
|
|
384
|
+
*
|
|
385
|
+
* `grounding_source` marks the documents an answer must be grounded in,
|
|
386
|
+
* `query` marks the question, and `guard_content` marks text the guardrail
|
|
387
|
+
* evaluates without a grounding role.
|
|
388
|
+
*
|
|
389
|
+
* @stability unstable
|
|
390
|
+
* @category schemas
|
|
391
|
+
* @since 0.4.0
|
|
392
|
+
*/
|
|
393
|
+
export const GuardrailConverseContentQualifier = Schema.Literals(["grounding_source", "query", "guard_content"])
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Text that the guardrail evaluates.
|
|
397
|
+
*
|
|
398
|
+
* **Details**
|
|
399
|
+
*
|
|
400
|
+
* `qualifiers` uses `optionalKey` for the same reason as
|
|
401
|
+
* `CachePointBlock.ttl`: the block is built from `Prompt` provider options,
|
|
402
|
+
* whose values must satisfy `Schema.Json`.
|
|
403
|
+
*
|
|
404
|
+
* @stability unstable
|
|
405
|
+
* @category schemas
|
|
406
|
+
* @since 0.4.0
|
|
407
|
+
*/
|
|
408
|
+
export class GuardrailConverseTextBlock extends Schema.Class<GuardrailConverseTextBlock>(
|
|
409
|
+
makeIdentifier("GuardrailConverseTextBlock")
|
|
410
|
+
)({
|
|
411
|
+
text: Schema.String,
|
|
412
|
+
qualifiers: Schema.optionalKey(Schema.Array(GuardrailConverseContentQualifier))
|
|
413
|
+
}) {}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* An image that the guardrail evaluates.
|
|
417
|
+
*
|
|
418
|
+
* **Details**
|
|
419
|
+
*
|
|
420
|
+
* The Smithy model allows only png and jpeg, and only `bytes` as the source.
|
|
421
|
+
* `bytes` is a Smithy blob, which the JSON protocol carries as a base64
|
|
422
|
+
* string.
|
|
423
|
+
*
|
|
424
|
+
* @stability unstable
|
|
425
|
+
* @category schemas
|
|
426
|
+
* @since 0.4.0
|
|
427
|
+
*/
|
|
428
|
+
export class GuardrailConverseImageBlock extends Schema.Class<GuardrailConverseImageBlock>(
|
|
429
|
+
makeIdentifier("GuardrailConverseImageBlock")
|
|
430
|
+
)({
|
|
431
|
+
format: Schema.Literals(["png", "jpeg"]),
|
|
432
|
+
source: Schema.Struct({ bytes: Schema.String })
|
|
433
|
+
}) {}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Content that the guardrail evaluates, in a message or in the system prompt.
|
|
437
|
+
*
|
|
438
|
+
* **Details**
|
|
439
|
+
*
|
|
440
|
+
* AWS models `GuardrailConverseContentBlock` as a UNION, so both members are
|
|
441
|
+
* optional here. Once a request holds one `guardContent` block, the guardrail
|
|
442
|
+
* evaluates only the guarded blocks. The model still reads every block.
|
|
443
|
+
*
|
|
444
|
+
* @stability unstable
|
|
445
|
+
* @category schemas
|
|
446
|
+
* @since 0.4.0
|
|
447
|
+
*/
|
|
448
|
+
export const GuardrailConverseContentBlock = Schema.Struct({
|
|
449
|
+
text: Schema.optional(GuardrailConverseTextBlock),
|
|
450
|
+
image: Schema.optional(GuardrailConverseImageBlock)
|
|
451
|
+
})
|
|
452
|
+
|
|
380
453
|
/**
|
|
381
454
|
* A text content block within a Converse message.
|
|
382
455
|
*
|
|
@@ -401,7 +474,8 @@ export const ContentBlock = Schema.Struct({
|
|
|
401
474
|
image: Schema.optional(ImageBlock),
|
|
402
475
|
document: Schema.optional(DocumentBlock),
|
|
403
476
|
cachePoint: Schema.optional(CachePointBlock),
|
|
404
|
-
citationsContent: Schema.optional(CitationsContentBlock)
|
|
477
|
+
citationsContent: Schema.optional(CitationsContentBlock),
|
|
478
|
+
guardContent: Schema.optional(GuardrailConverseContentBlock)
|
|
405
479
|
})
|
|
406
480
|
|
|
407
481
|
/**
|
|
@@ -421,8 +495,9 @@ export class Message extends Schema.Class<Message>(makeIdentifier("Message"))({
|
|
|
421
495
|
*
|
|
422
496
|
* **Details**
|
|
423
497
|
*
|
|
424
|
-
* AWS models `SystemContentBlock` as a UNION, so
|
|
425
|
-
* block carries
|
|
498
|
+
* AWS models `SystemContentBlock` as a UNION, so every member is optional; a
|
|
499
|
+
* block carries the system text, guarded system text, or a cache point. The
|
|
500
|
+
* guardrail evaluates the system prompt only where it is guarded.
|
|
426
501
|
*
|
|
427
502
|
* @stability unstable
|
|
428
503
|
* @category schemas
|
|
@@ -430,7 +505,8 @@ export class Message extends Schema.Class<Message>(makeIdentifier("Message"))({
|
|
|
430
505
|
*/
|
|
431
506
|
export const SystemContentBlock = Schema.Struct({
|
|
432
507
|
text: Schema.optional(Schema.String),
|
|
433
|
-
cachePoint: Schema.optional(CachePointBlock)
|
|
508
|
+
cachePoint: Schema.optional(CachePointBlock),
|
|
509
|
+
guardContent: Schema.optional(GuardrailConverseContentBlock)
|
|
434
510
|
})
|
|
435
511
|
|
|
436
512
|
/**
|
|
@@ -578,7 +654,80 @@ export const OutputConfig = Schema.Struct({
|
|
|
578
654
|
})
|
|
579
655
|
|
|
580
656
|
/**
|
|
581
|
-
*
|
|
657
|
+
* Whether Bedrock returns the guardrail trace.
|
|
658
|
+
*
|
|
659
|
+
* **Details**
|
|
660
|
+
*
|
|
661
|
+
* Bedrock's default is `disabled`. The trace holds the text that a policy
|
|
662
|
+
* matched, unmasked. `enabled_full` is in the Smithy model, and AWS does not
|
|
663
|
+
* document it.
|
|
664
|
+
*
|
|
665
|
+
* @stability unstable
|
|
666
|
+
* @category schemas
|
|
667
|
+
* @since 0.4.0
|
|
668
|
+
*/
|
|
669
|
+
export const GuardrailTrace = Schema.Literals(["enabled", "disabled", "enabled_full"])
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* How ConverseStream applies a guardrail to the streamed output.
|
|
673
|
+
*
|
|
674
|
+
* **Details**
|
|
675
|
+
*
|
|
676
|
+
* `sync`, Bedrock's default, holds back each chunk until the guardrail has
|
|
677
|
+
* evaluated it. `async` sends chunks at once, so text that the guardrail
|
|
678
|
+
* later blocks can reach the caller, and masking does not apply.
|
|
679
|
+
*
|
|
680
|
+
* @stability unstable
|
|
681
|
+
* @category schemas
|
|
682
|
+
* @since 0.4.0
|
|
683
|
+
*/
|
|
684
|
+
export const GuardrailStreamProcessingMode = Schema.Literals(["sync", "async"])
|
|
685
|
+
|
|
686
|
+
// Smithy gives the identifier and the version `default: ""`, and Bedrock cannot
|
|
687
|
+
// apply a guardrail without them, so both are required here.
|
|
688
|
+
const guardrailFields = {
|
|
689
|
+
guardrailIdentifier: Schema.String,
|
|
690
|
+
guardrailVersion: Schema.String,
|
|
691
|
+
trace: Schema.optionalKey(GuardrailTrace)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* The guardrail of a Converse request.
|
|
696
|
+
*
|
|
697
|
+
* **Details**
|
|
698
|
+
*
|
|
699
|
+
* `guardrailIdentifier` is the guardrail's ID or ARN, and `guardrailVersion`
|
|
700
|
+
* is a version number or `DRAFT`. Converse has no `streamProcessingMode`, so
|
|
701
|
+
* the member is typed `never`: a ConverseStream request does not fit Converse
|
|
702
|
+
* until the mode is removed.
|
|
703
|
+
*
|
|
704
|
+
* @stability unstable
|
|
705
|
+
* @category schemas
|
|
706
|
+
* @since 0.4.0
|
|
707
|
+
*/
|
|
708
|
+
export class GuardrailConfiguration extends Schema.Class<GuardrailConfiguration>(
|
|
709
|
+
makeIdentifier("GuardrailConfiguration")
|
|
710
|
+
)({
|
|
711
|
+
...guardrailFields,
|
|
712
|
+
streamProcessingMode: Schema.optionalKey(Schema.Never)
|
|
713
|
+
}) {}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* The guardrail of a ConverseStream request.
|
|
717
|
+
*
|
|
718
|
+
* @stability unstable
|
|
719
|
+
* @category schemas
|
|
720
|
+
* @since 0.4.0
|
|
721
|
+
*/
|
|
722
|
+
export class GuardrailStreamConfiguration extends Schema.Class<GuardrailStreamConfiguration>(
|
|
723
|
+
makeIdentifier("GuardrailStreamConfiguration")
|
|
724
|
+
)({
|
|
725
|
+
...guardrailFields,
|
|
726
|
+
streamProcessingMode: Schema.optionalKey(GuardrailStreamProcessingMode)
|
|
727
|
+
}) {}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* The request payload for the Converse operation.
|
|
582
731
|
*
|
|
583
732
|
* **Details**
|
|
584
733
|
*
|
|
@@ -602,9 +751,30 @@ export class ConverseRequest extends Schema.Class<ConverseRequest>(makeIdentifie
|
|
|
602
751
|
toolConfig: Schema.optionalKey(ToolConfiguration),
|
|
603
752
|
inferenceConfig: Schema.optional(InferenceConfiguration),
|
|
604
753
|
outputConfig: Schema.optionalKey(OutputConfig),
|
|
605
|
-
additionalModelRequestFields: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
|
|
754
|
+
additionalModelRequestFields: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
755
|
+
guardrailConfig: Schema.optional(GuardrailConfiguration)
|
|
606
756
|
}) {}
|
|
607
757
|
|
|
758
|
+
/**
|
|
759
|
+
* The request payload for the ConverseStream operation.
|
|
760
|
+
*
|
|
761
|
+
* **Details**
|
|
762
|
+
*
|
|
763
|
+
* The members of `ConverseRequest`, with the guardrail of a stream, which
|
|
764
|
+
* adds `streamProcessingMode`. That member is the only difference between the
|
|
765
|
+
* two operations.
|
|
766
|
+
*
|
|
767
|
+
* @stability unstable
|
|
768
|
+
* @category schemas
|
|
769
|
+
* @since 0.4.0
|
|
770
|
+
*/
|
|
771
|
+
export class ConverseStreamRequest
|
|
772
|
+
extends Schema.Class<ConverseStreamRequest>(makeIdentifier("ConverseStreamRequest"))({
|
|
773
|
+
...ConverseRequest.fields,
|
|
774
|
+
guardrailConfig: Schema.optional(GuardrailStreamConfiguration)
|
|
775
|
+
})
|
|
776
|
+
{}
|
|
777
|
+
|
|
608
778
|
// =============================================================================
|
|
609
779
|
// Response
|
|
610
780
|
// =============================================================================
|
|
@@ -634,6 +804,12 @@ export class ConverseMetrics extends Schema.Class<ConverseMetrics>(makeIdentifie
|
|
|
634
804
|
/**
|
|
635
805
|
* The response from a successful Converse call.
|
|
636
806
|
*
|
|
807
|
+
* **Details**
|
|
808
|
+
*
|
|
809
|
+
* `trace` is kept as the JSON Bedrock sent. Its Smithy tree has about 90
|
|
810
|
+
* shapes that AWS still extends, and a struct would drop every member added
|
|
811
|
+
* later.
|
|
812
|
+
*
|
|
637
813
|
* @stability unstable
|
|
638
814
|
* @category schemas
|
|
639
815
|
* @since 0.1.0
|
|
@@ -642,7 +818,8 @@ export class ConverseResponse extends Schema.Class<ConverseResponse>(makeIdentif
|
|
|
642
818
|
output: ConverseOutput,
|
|
643
819
|
usage: TokenUsage,
|
|
644
820
|
stopReason: StopReason,
|
|
645
|
-
metrics: Schema.optional(ConverseMetrics)
|
|
821
|
+
metrics: Schema.optional(ConverseMetrics),
|
|
822
|
+
trace: Schema.optional(Schema.MutableJson)
|
|
646
823
|
}) {}
|
|
647
824
|
|
|
648
825
|
// =============================================================================
|
|
@@ -823,7 +1000,13 @@ export class MessageStopEvent extends Schema.Class<MessageStopEvent>(
|
|
|
823
1000
|
}) {}
|
|
824
1001
|
|
|
825
1002
|
/**
|
|
826
|
-
* The trailing metadata event for a Converse stream, carrying usage
|
|
1003
|
+
* The trailing metadata event for a Converse stream, carrying usage, metrics,
|
|
1004
|
+
* and the trace.
|
|
1005
|
+
*
|
|
1006
|
+
* **Details**
|
|
1007
|
+
*
|
|
1008
|
+
* This is the only stream event that carries the trace. It is kept as JSON,
|
|
1009
|
+
* as in `ConverseResponse`.
|
|
827
1010
|
*
|
|
828
1011
|
* @stability unstable
|
|
829
1012
|
* @category schemas
|
|
@@ -833,7 +1016,8 @@ export class ConverseStreamMetadataEvent extends Schema.Class<ConverseStreamMeta
|
|
|
833
1016
|
makeIdentifier("ConverseStreamMetadataEvent")
|
|
834
1017
|
)({
|
|
835
1018
|
usage: TokenUsage,
|
|
836
|
-
metrics: Schema.optional(ConverseMetrics)
|
|
1019
|
+
metrics: Schema.optional(ConverseMetrics),
|
|
1020
|
+
trace: Schema.optional(Schema.MutableJson)
|
|
837
1021
|
}) {}
|
|
838
1022
|
|
|
839
1023
|
const ExceptionBody = Schema.Struct({
|
package/src/internal/errors.ts
CHANGED
|
@@ -114,10 +114,10 @@ export const mapCredentialProviderError =
|
|
|
114
114
|
// with the exception type carried in the `x-amzn-errortype` header — NOT
|
|
115
115
|
// Anthropic's nested `{ error: { type, message } }`. Schema-guarded so a
|
|
116
116
|
// non-conforming body safely yields `undefined`.
|
|
117
|
-
const BedrockErrorBody = Schema.Struct({
|
|
117
|
+
const BedrockErrorBody = Schema.fromJsonString(Schema.Struct({
|
|
118
118
|
message: Schema.optional(Schema.String),
|
|
119
119
|
Message: Schema.optional(Schema.String)
|
|
120
|
-
})
|
|
120
|
+
}))
|
|
121
121
|
|
|
122
122
|
/** @internal */
|
|
123
123
|
const mapStatusCodeError = Effect.fnUntraced(function*(
|
|
@@ -137,16 +137,7 @@ const mapStatusCodeError = Effect.fnUntraced(function*(
|
|
|
137
137
|
|
|
138
138
|
// Promote the provider's error message to the structured reason (else 5xx
|
|
139
139
|
// shows "Server error" and 4xx shows a bare "HTTP <status>").
|
|
140
|
-
|
|
141
|
-
// @effect-diagnostics effect/tryCatchInEffectGen:off
|
|
142
|
-
if (Predicate.isNotUndefined(body)) {
|
|
143
|
-
try {
|
|
144
|
-
json = JSON.parse(body)
|
|
145
|
-
} catch {
|
|
146
|
-
json = undefined
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const decoded = Schema.decodeUnknownOption(BedrockErrorBody)(json)
|
|
140
|
+
const decoded = Schema.decodeUnknownOption(BedrockErrorBody)(body)
|
|
150
141
|
const message = Option.isSome(decoded)
|
|
151
142
|
? decoded.value.message ?? decoded.value.Message
|
|
152
143
|
: undefined
|
|
@@ -220,9 +211,23 @@ const buildInvalidRequestDescription = (params: {
|
|
|
220
211
|
parts.push(`Response: ${truncated}`)
|
|
221
212
|
}
|
|
222
213
|
|
|
214
|
+
if (params.message && dataRetentionRejection.test(params.message)) {
|
|
215
|
+
parts.push(dataRetentionHint)
|
|
216
|
+
}
|
|
217
|
+
|
|
223
218
|
return parts.join(" ")
|
|
224
219
|
}
|
|
225
220
|
|
|
221
|
+
// Converse has no request field for the data retention mode, so the AWS message
|
|
222
|
+
// alone leaves the reader looking for one. Only an account-level setting in each
|
|
223
|
+
// region, which the control plane sets, makes such a model callable.
|
|
224
|
+
const dataRetentionRejection = /data retention mode '[^']*' is not available for this model/
|
|
225
|
+
|
|
226
|
+
const dataRetentionHint = "The account's data retention mode in this region does not allow this model." +
|
|
227
|
+
" Claude Fable 5 and 5.1 need `aws_review`." +
|
|
228
|
+
" Set the mode with the Bedrock PutAccountDataRetention operation:" +
|
|
229
|
+
" https://docs.aws.amazon.com/bedrock/latest/userguide/data-retention.html"
|
|
230
|
+
|
|
226
231
|
/**
|
|
227
232
|
* AWS sends the exception shape as `Name:http://internal.amazon.com/coral/...`.
|
|
228
233
|
* Only the part before the colon identifies it.
|
|
@@ -238,7 +243,7 @@ const extractErrorType = (header: string | undefined): string | undefined =>
|
|
|
238
243
|
* expired `aws login` credentials are the common case. The exception shape in
|
|
239
244
|
* `x-amzn-errortype` is what actually distinguishes them.
|
|
240
245
|
*/
|
|
241
|
-
const authenticationKinds: Record<string,
|
|
246
|
+
const authenticationKinds: Record<string, AiError.AuthenticationError["kind"]> = {
|
|
242
247
|
AccessDeniedException: "InsufficientPermissions",
|
|
243
248
|
ExpiredTokenException: "ExpiredKey",
|
|
244
249
|
IncompleteSignatureException: "InvalidKey",
|
|
@@ -254,6 +259,17 @@ export const mapStatusCodeToReason = ({ errorType, http, message, status }: {
|
|
|
254
259
|
readonly errorType?: string | undefined
|
|
255
260
|
readonly http: typeof AiError.HttpContext.Type
|
|
256
261
|
}): AiError.AiErrorReason => {
|
|
262
|
+
// AWS sends this with a 400, but the request is well formed. The account has
|
|
263
|
+
// used up a quota, which a short retry delay will not restore.
|
|
264
|
+
if (errorType === "ServiceQuotaExceededException") {
|
|
265
|
+
return new AiError.QuotaExhaustedError({ http })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The exception shape is often the only clue to the cause, as when a 400
|
|
269
|
+
// arrives with the body `{}`.
|
|
270
|
+
const describe = (text: string | undefined): string | undefined =>
|
|
271
|
+
Predicate.isUndefined(errorType) ? text : Predicate.isUndefined(text) ? errorType : `${errorType}: ${text}`
|
|
272
|
+
|
|
257
273
|
const invalidRequestDescription = buildInvalidRequestDescription({
|
|
258
274
|
status,
|
|
259
275
|
message,
|
|
@@ -267,7 +283,7 @@ export const mapStatusCodeToReason = ({ errorType, http, message, status }: {
|
|
|
267
283
|
case 404:
|
|
268
284
|
case 422:
|
|
269
285
|
return new AiError.InvalidRequestError({
|
|
270
|
-
description: invalidRequestDescription,
|
|
286
|
+
description: describe(invalidRequestDescription),
|
|
271
287
|
http
|
|
272
288
|
})
|
|
273
289
|
case 401:
|
|
@@ -276,19 +292,19 @@ export const mapStatusCodeToReason = ({ errorType, http, message, status }: {
|
|
|
276
292
|
kind: (Predicate.isNotUndefined(errorType) ? authenticationKinds[errorType] : undefined)
|
|
277
293
|
// No header, or a shape we do not know: fall back to what the status implies.
|
|
278
294
|
?? (status === 401 ? "InvalidKey" : "InsufficientPermissions"),
|
|
279
|
-
description: message,
|
|
295
|
+
description: describe(message),
|
|
280
296
|
http
|
|
281
297
|
})
|
|
282
298
|
case 408:
|
|
283
299
|
// ModelTimeoutException: the request took too long to process.
|
|
284
300
|
return new AiError.InternalProviderError({
|
|
285
|
-
description: message ?? "Model timeout",
|
|
301
|
+
description: describe(message) ?? "Model timeout",
|
|
286
302
|
http
|
|
287
303
|
})
|
|
288
304
|
case 424:
|
|
289
305
|
// ModelErrorException: the model returned an error response.
|
|
290
306
|
return new AiError.InternalProviderError({
|
|
291
|
-
description: message ?? "Model error",
|
|
307
|
+
description: describe(message) ?? "Model error",
|
|
292
308
|
http
|
|
293
309
|
})
|
|
294
310
|
case 429:
|
|
@@ -298,12 +314,12 @@ export const mapStatusCodeToReason = ({ errorType, http, message, status }: {
|
|
|
298
314
|
default:
|
|
299
315
|
if (status >= 500) {
|
|
300
316
|
return new AiError.InternalProviderError({
|
|
301
|
-
description: message ?? "Server error",
|
|
317
|
+
description: describe(message) ?? "Server error",
|
|
302
318
|
http
|
|
303
319
|
})
|
|
304
320
|
}
|
|
305
321
|
return new AiError.UnknownError({
|
|
306
|
-
description: message,
|
|
322
|
+
description: describe(message),
|
|
307
323
|
http
|
|
308
324
|
})
|
|
309
325
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type * as Schema from "effect/Schema"
|
|
2
|
+
import type { FinishMetadata, GuardrailAction, GuardrailOutcome } from "../AmazonBedrockLanguageModel.ts"
|
|
3
|
+
|
|
4
|
+
const strength: Record<GuardrailAction, number> = { none: 0, masked: 1, blocked: 2 }
|
|
5
|
+
|
|
6
|
+
const stronger = (a: GuardrailAction, b: GuardrailAction): GuardrailAction => strength[b] > strength[a] ? b : a
|
|
7
|
+
|
|
8
|
+
const isObject = (value: unknown): value is Schema.MutableJsonObject =>
|
|
9
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
10
|
+
|
|
11
|
+
const actionOf = (value: string): GuardrailAction =>
|
|
12
|
+
value === "BLOCKED" ? "blocked" : value === "ANONYMIZED" ? "masked" : "none"
|
|
13
|
+
|
|
14
|
+
const strongestAction = (value: Schema.MutableJson): GuardrailAction => {
|
|
15
|
+
let action: GuardrailAction = "none"
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
for (const item of value) {
|
|
18
|
+
action = stronger(action, strongestAction(item))
|
|
19
|
+
}
|
|
20
|
+
} else if (isObject(value)) {
|
|
21
|
+
for (const [key, member] of Object.entries(value)) {
|
|
22
|
+
action = stronger(
|
|
23
|
+
action,
|
|
24
|
+
key === "action" && typeof member === "string" ? actionOf(member) : strongestAction(member)
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return action
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const assessSide = (assessments: ReadonlyArray<Schema.MutableJson>, policies: Array<string>): GuardrailAction => {
|
|
32
|
+
let side: GuardrailAction = "none"
|
|
33
|
+
for (const assessment of assessments) {
|
|
34
|
+
if (!isObject(assessment)) {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
for (const [policy, member] of Object.entries(assessment)) {
|
|
38
|
+
const action = strongestAction(member)
|
|
39
|
+
if (action !== "none" && !policies.includes(policy)) {
|
|
40
|
+
policies.push(policy)
|
|
41
|
+
}
|
|
42
|
+
side = stronger(side, action)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return side
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @internal */
|
|
49
|
+
export const guardrailOutcome = (trace: Schema.MutableJson | undefined): GuardrailOutcome | null => {
|
|
50
|
+
const guardrail = isObject(trace) ? trace.guardrail : undefined
|
|
51
|
+
if (!isObject(guardrail)) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
const inputAssessment = guardrail.inputAssessment
|
|
55
|
+
const outputAssessments = guardrail.outputAssessments
|
|
56
|
+
const policies: Array<string> = []
|
|
57
|
+
const input = assessSide(isObject(inputAssessment) ? Object.values(inputAssessment) : [], policies)
|
|
58
|
+
const output = assessSide(
|
|
59
|
+
isObject(outputAssessments)
|
|
60
|
+
? Object.values(outputAssessments).flatMap((chunks) => Array.isArray(chunks) ? chunks : [])
|
|
61
|
+
: [],
|
|
62
|
+
policies
|
|
63
|
+
)
|
|
64
|
+
return { input, output, policies }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @internal */
|
|
68
|
+
export const finishMetadata = (stopReason: string | null, trace: Schema.MutableJson | undefined): FinishMetadata => ({
|
|
69
|
+
stopReason,
|
|
70
|
+
guardrail: guardrailOutcome(trace),
|
|
71
|
+
trace: trace ?? null
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const isJson = (text: string): boolean => {
|
|
75
|
+
try {
|
|
76
|
+
JSON.parse(text)
|
|
77
|
+
return true
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Bedrock stops with `guardrail_intervened` both when a guardrail blocks,
|
|
85
|
+
* replacing the text with the guardrail's blocked message, and when it masks,
|
|
86
|
+
* leaving the object JSON.
|
|
87
|
+
*
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export const blocksObject = (stopReason: string, text: string): boolean =>
|
|
91
|
+
stopReason === "guardrail_intervened" && !isJson(text)
|