Repository navigation
feat(server-utils): Record Bedrock prompts, system instructions, and tool definitions #26249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
faabebf
feat(aws-serverless): Record Bedrock prompts, system instructions, an…
andreiborza b8764bf
Record Claude InvokeModel inputs with the Anthropic integration helpers
andreiborza da8c4f3
Use the shared GenAiMessagePart type for Bedrock message parts
andreiborza e5cd9cc
Shrink the Bedrock input recording code
andreiborza ef71932
Merge branch 'develop' into ab/bedrock-gen-ai-inputs
andreiborza d3e57bb
Test Bedrock input recording against the real AWS SDK
andreiborza 19f6112
Merge remote-tracking branch 'origin/ab/bedrock-gen-ai-inputs' into a…
andreiborza 3d8a788
Merge branch 'develop' into ab/bedrock-gen-ai-inputs
andreiborza 549da26
Correct the Converse content block JSDoc
andreiborza e187926
Merge remote-tracking branch 'origin/ab/bedrock-gen-ai-inputs' into a…
andreiborza e695ff9
Merge remote-tracking branch 'origin/develop' into ab/bedrock-gen-ai-…
andreiborza 9b802da
Merge branch 'develop' into ab/bedrock-gen-ai-inputs
andreiborza File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
dev-packages/node-integration-tests/suites/aws-serverless/bedrock/instrument-gen-ai.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import * as Sentry from '@sentry/aws-serverless'; | ||
| import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
|
||
| Sentry.init({ | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| release: '1.0', | ||
| tracesSampleRate: 1.0, | ||
| transport: loggingTransport, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
packages/server-utils/src/integrations/aws-sdk/services/bedrock-runtime-content.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| import { isObjectLike, stringify } from '@sentry/core'; | ||
| import { | ||
| GEN_AI_INPUT_MESSAGES, | ||
| GEN_AI_SYSTEM_INSTRUCTIONS, | ||
| GEN_AI_TOOL_DEFINITIONS, | ||
| } from '@sentry/conventions/attributes'; | ||
| import { getMessagesAttributes, messagesFromParams } from '../../../ai/anthropic-ai/utils'; | ||
| import type { GenAiMessagePart } from '../../../ai/types'; | ||
|
|
||
| /** | ||
| * Translate Converse `ContentBlock`s (also used by Amazon Nova `InvokeModel` bodies) into message parts. | ||
| * The conventions require a `type` on each part, and Bedrock's blocks (`{ text }`, `{ toolUse }`, ...) have none. | ||
| * | ||
| * Media is reported by modality only: inline media is raw bytes, which the conventions require to be | ||
| * dropped rather than recorded. Blocks without content worth recording (`cachePoint`, redacted | ||
| * reasoning, unknown members) are left out rather than serialized, so no binary payload slips through. | ||
| */ | ||
| function contentBlocksToParts(content: unknown): GenAiMessagePart[] { | ||
| return Array.isArray(content) ? content.flatMap(contentBlockToParts) : []; | ||
| } | ||
|
|
||
| function contentBlockToParts(block: unknown): GenAiMessagePart[] { | ||
| if (!isObjectLike(block)) { | ||
| return []; | ||
| } | ||
|
|
||
| if (typeof block.text === 'string') { | ||
| return [{ type: 'text', content: block.text }]; | ||
| } | ||
|
|
||
| const { toolUse, toolResult, reasoningContent, guardContent, citationsContent } = block; | ||
|
|
||
| if (isObjectLike(toolUse)) { | ||
| return [ | ||
| { | ||
| type: 'tool_call', | ||
| id: toolUse.toolUseId, | ||
| name: toolUse.name, | ||
| arguments: stringify(toolUse.input ?? {}, String), | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| if (isObjectLike(toolResult)) { | ||
| return [{ type: 'tool_call_response', id: toolResult.toolUseId, result: toolResultToString(toolResult.content) }]; | ||
| } | ||
|
|
||
| if (isObjectLike(reasoningContent)) { | ||
| const reasoningText = reasoningContent.reasoningText; | ||
| return isObjectLike(reasoningText) && typeof reasoningText.text === 'string' | ||
| ? [{ type: 'reasoning', content: reasoningText.text }] | ||
| : []; | ||
| } | ||
|
|
||
| if (isObjectLike(guardContent)) { | ||
| // `guardContent.text` is a `{ text }` block, `guardContent.image` an image block. | ||
| return contentBlockToParts(isObjectLike(guardContent.text) ? guardContent.text : guardContent); | ||
| } | ||
|
|
||
| if (isObjectLike(citationsContent)) { | ||
| return contentBlocksToParts(citationsContent.content); | ||
| } | ||
|
|
||
| for (const modality of ['image', 'video', 'audio', 'document']) { | ||
| const media = block[modality]; | ||
| if (isObjectLike(media)) { | ||
| // Only image formats (`png`, ...) map to a MIME type by prefix; `undefined` is left out of the JSON. | ||
| const format = modality === 'image' && typeof media.format === 'string' ? `image/${media.format}` : undefined; | ||
| return [{ type: 'blob', modality, mime_type: format }]; | ||
| } | ||
| } | ||
|
|
||
| return []; | ||
| } | ||
|
|
||
| function toolResultToString(content: unknown): string { | ||
| if (!Array.isArray(content)) { | ||
| return ''; | ||
| } | ||
|
|
||
| return content | ||
| .map(block => { | ||
| if (!isObjectLike(block)) { | ||
| return undefined; | ||
| } | ||
| if (typeof block.text === 'string') { | ||
| return block.text; | ||
| } | ||
| return block.json !== undefined ? stringify(block.json, String) : undefined; | ||
| }) | ||
| .filter((text): text is string => text !== undefined) | ||
| .join('\n'); | ||
| } | ||
|
|
||
| function converseMessagesToMessages(messages: unknown): Array<{ role: string; parts: GenAiMessagePart[] }> { | ||
| if (!Array.isArray(messages)) { | ||
| return []; | ||
| } | ||
|
|
||
| return messages | ||
| .filter(isObjectLike) | ||
| .map(message => ({ | ||
| role: typeof message.role === 'string' ? message.role : 'user', | ||
| parts: contentBlocksToParts(message.content), | ||
| })) | ||
| .filter(message => message.parts.length > 0); | ||
| } | ||
|
|
||
| /** | ||
| * Input attributes of a Converse-shaped request (`Converse`, `ConverseStream`, and Amazon Nova | ||
| * `InvokeModel` bodies, which use the same `messages`/`system`/`toolConfig` shape). | ||
| */ | ||
| export function getConverseInputAttributes(input: Record<string, unknown>): Record<string, unknown> { | ||
| const attributes: Record<string, unknown> = {}; | ||
|
|
||
| const systemInstructions = contentBlocksToParts(input.system); | ||
| if (systemInstructions.length) { | ||
| attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = JSON.stringify(systemInstructions); | ||
| } | ||
|
|
||
| const messages = converseMessagesToMessages(input.messages); | ||
| if (messages.length) { | ||
| attributes[GEN_AI_INPUT_MESSAGES] = JSON.stringify(messages); | ||
| } | ||
|
|
||
| const toolConfig = input.toolConfig; | ||
| if (isObjectLike(toolConfig) && Array.isArray(toolConfig.tools)) { | ||
| // `cachePoint` entries share the list with the `toolSpec`s; they are not tools. | ||
| const toolSpecs = toolConfig.tools | ||
| .map(tool => (isObjectLike(tool) ? tool.toolSpec : undefined)) | ||
| .filter(isObjectLike); | ||
| if (toolSpecs.length) { | ||
| attributes[GEN_AI_TOOL_DEFINITIONS] = JSON.stringify(toolSpecs); | ||
| } | ||
| } | ||
|
|
||
| return attributes; | ||
| } | ||
|
|
||
| /** | ||
| * Claude `InvokeModel` bodies are Anthropic Messages API requests, so they are recorded like the Anthropic | ||
| * integration records them. | ||
| */ | ||
| export function getClaudeInputAttributes(body: Record<string, unknown>): Record<string, unknown> { | ||
| const attributes = getMessagesAttributes(messagesFromParams(body)); | ||
| if (Array.isArray(body.tools)) { | ||
| attributes[GEN_AI_TOOL_DEFINITIONS] = JSON.stringify(body.tools); | ||
| } | ||
| return attributes; | ||
| } | ||
|
|
||
| /** Input attributes of the single-prompt `InvokeModel` bodies (Titan, Llama, Cohere, Mistral, ...). */ | ||
| export function getPromptInputAttributes(prompt: unknown): Record<string, unknown> { | ||
| if (typeof prompt !== 'string') { | ||
| return {}; | ||
| } | ||
| return { [GEN_AI_INPUT_MESSAGES]: JSON.stringify([{ role: 'user', parts: [{ type: 'text', content: prompt }] }]) }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this one handle inline media? So we don't attach raw base64 to
gen_ai.input.messages?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it doesn't, which is in line with the anthropic integration. A while ago we decided to walk back on truncation and stripping images out. This has since changed, but we need to align this on all integrations.
Webby and I are working on aligning integrations more in the future, but we are at the stage of writing clearer specs.