-
Notifications
You must be signed in to change notification settings - Fork 403
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
feat: Added support for Claude 3+ Chat API in Bedrock #2870
Changes from all commits
12c5e31
d41cfab
831e5ca
b6b59d0
cf3a875
340fecb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ | |
|
||
'use strict' | ||
|
||
const { stringifyClaudeChunkedMessage } = require('./utils') | ||
|
||
/** | ||
* Parses an AWS invoke command instance into a re-usable entity. | ||
*/ | ||
|
@@ -68,37 +70,34 @@ | |
/** | ||
* The question posed to the LLM. | ||
* | ||
* @returns {string|string[]|undefined} | ||
* @returns {object[]} The array of context messages passed to the LLM (or a single user prompt for legacy "non-chat" models) | ||
*/ | ||
get prompt() { | ||
let result | ||
if (this.isTitan() === true || this.isTitanEmbed() === true) { | ||
result = this.#body.inputText | ||
return [ | ||
{ | ||
role: 'user', | ||
content: this.#body.inputText | ||
} | ||
] | ||
} else if (this.isCohereEmbed() === true) { | ||
result = this.#body.texts.join(' ') | ||
return [ | ||
{ | ||
role: 'user', | ||
content: this.#body.texts.join(' ') | ||
} | ||
] | ||
} else if ( | ||
this.isClaude() === true || | ||
this.isClaudeTextCompletionApi() === true || | ||
this.isAi21() === true || | ||
this.isCohere() === true || | ||
this.isLlama() === true | ||
) { | ||
result = this.#body.prompt | ||
} else if (this.isClaude3() === true) { | ||
const collected = [] | ||
for (const message of this.#body?.messages) { | ||
if (message?.role === 'assistant') { | ||
continue | ||
} | ||
if (typeof message?.content === 'string') { | ||
collected.push(message?.content) | ||
continue | ||
} | ||
const mappedMsgObj = message?.content.map((msgContent) => msgContent.text) | ||
collected.push(mappedMsgObj) | ||
} | ||
result = collected.join(' ') | ||
return [{ role: 'user', content: this.#body.prompt }] | ||
} else if (this.isClaudeMessagesApi() === true) { | ||
return normalizeClaude3Messages(this.#body?.messages) | ||
} | ||
return result | ||
return [] | ||
} | ||
|
||
/** | ||
|
@@ -151,6 +150,41 @@ | |
isTitanEmbed() { | ||
return this.#modelId.startsWith('amazon.titan-embed') | ||
} | ||
|
||
isClaudeMessagesApi() { | ||
return (this.isClaude3() === true || this.isClaude() === true) && 'messages' in this.#body | ||
} | ||
|
||
isClaudeTextCompletionApi() { | ||
return this.isClaude() === true && 'prompt' in this.#body | ||
} | ||
} | ||
|
||
/** | ||
* Claude v3 requests in Bedrock can have two different "chat" flavors. This function normalizes them into a consistent | ||
* format per the AIM agent spec | ||
* | ||
* @param messages - The raw array of messages passed to the invoke API | ||
* @returns {number|undefined} - The normalized messages | ||
*/ | ||
function normalizeClaude3Messages(messages) { | ||
const result = [] | ||
for (const message of messages ?? []) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm going to approve this but we can follow up on the missing coverage here: |
||
if (message == null) { | ||
continue | ||
} | ||
if (typeof message.content === 'string') { | ||
// Messages can be specified with plain string content | ||
result.push({ role: message.role, content: message.content }) | ||
} else if (Array.isArray(message.content)) { | ||
// Or in a "chunked" format for multi-modal support | ||
result.push({ | ||
role: message.role, | ||
content: stringifyClaudeChunkedMessage(message.content) | ||
}) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
module.exports = BedrockCommand |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ | |
|
||
'use strict' | ||
|
||
const { stringifyClaudeChunkedMessage } = require('./utils') | ||
|
||
/** | ||
* @typedef {object} AwsBedrockMiddlewareResponse | ||
* @property {object} response Has a `body` property that is an IncomingMessage, | ||
|
@@ -63,7 +65,7 @@ class BedrockResponse { | |
// Streamed response | ||
this.#completions = body.completions | ||
} else { | ||
this.#completions = body?.content?.map((c) => c.text) | ||
this.#completions = [stringifyClaudeChunkedMessage(body?.content)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i see a versioned test but not a unit test for this |
||
} | ||
this.#id = body.id | ||
} else if (cmd.isCohere() === true) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/* | ||
* Copyright 2024 New Relic Corporation. All rights reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
|
||
/** | ||
* | ||
* @param {object[]} chunks - The "chunks" that make up a single conceptual message. In a multi-modal scenario, a single message | ||
* might have a number of different-typed chunks interspersed | ||
* @returns {string} - A stringified version of the message. We make a best-effort effort attempt to represent non-text chunks. In the future | ||
* we may want to extend the agent to support these non-text chunks in a richer way. Placeholders are represented in an XML-like format but | ||
* are NOT intended to be parsed as valid XML | ||
*/ | ||
function stringifyClaudeChunkedMessage(chunks) { | ||
const stringifiedChunks = chunks.map((msgContent) => { | ||
switch (msgContent.type) { | ||
case 'text': | ||
return msgContent.text | ||
case 'image': | ||
return '<image>' | ||
case 'tool_use': | ||
return `<tool_use>${msgContent.name}</tool_use>` | ||
case 'tool_result': | ||
return `<tool_result>${msgContent.content}</tool_result>` | ||
default: | ||
return '<unknown_chunk>' | ||
} | ||
}) | ||
return stringifiedChunks.join('\n\n') | ||
} | ||
|
||
module.exports = { | ||
stringifyClaudeChunkedMessage | ||
} |
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.
I need to do a bit more research on this but I think some of the Bedrock embedding models allow you to make a single invoke call that generates several embeddings (see this Cohere blog for an example). So I think it might be correct to allow unrolling one embedding command to several embedding events. Currently all the embedding models in the command class produce a single prompt but I'm wondering if the Cohere one is also incorrectly squashing messages in some cases.
I might try to treat that as a separate PR / task if you're alright with that though
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.
If that's the case, how do you want to handle an error? I assume we still want one error attached to the transaction? I opted to only attach the embedding info if there's one event to keep the current behavior but I'm not sure that's correct
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.
I don't know this enough, but seems ok for now