|
| 1 | +/* |
| 2 | + * Copyright 2021 Collate |
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | + * you may not use this file except in compliance with the License. |
| 5 | + * You may obtain a copy of the License at |
| 6 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 7 | + * Unless required by applicable law or agreed to in writing, software |
| 8 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 9 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 10 | + * See the License for the specific language governing permissions and |
| 11 | + * limitations under the License. |
| 12 | + */ |
| 13 | + |
1 | 14 | package org.openmetadata.service.llm; |
2 | 15 |
|
3 | | -import com.fasterxml.jackson.databind.JsonNode; |
4 | | -import com.fasterxml.jackson.databind.ObjectMapper; |
5 | | -import com.fasterxml.jackson.databind.node.ArrayNode; |
6 | | -import com.fasterxml.jackson.databind.node.ObjectNode; |
7 | | -import java.io.IOException; |
8 | 16 | import java.time.Duration; |
9 | 17 | import lombok.extern.slf4j.Slf4j; |
10 | 18 | import org.openmetadata.schema.configuration.LLMBedrockConfig; |
11 | 19 | import org.openmetadata.schema.configuration.LLMConfiguration; |
12 | 20 | import org.openmetadata.schema.security.credentials.AWSBaseConfig; |
13 | 21 | import org.openmetadata.service.util.AwsCredentialsUtil; |
14 | 22 | import software.amazon.awssdk.awscore.exception.AwsServiceException; |
15 | | -import software.amazon.awssdk.core.SdkBytes; |
16 | 23 | import software.amazon.awssdk.core.exception.SdkClientException; |
17 | 24 | import software.amazon.awssdk.regions.Region; |
18 | 25 | import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; |
19 | | -import software.amazon.awssdk.services.bedrockruntime.model.InvokeModelRequest; |
20 | | -import software.amazon.awssdk.services.bedrockruntime.model.InvokeModelResponse; |
| 26 | +import software.amazon.awssdk.services.bedrockruntime.model.ContentBlock; |
| 27 | +import software.amazon.awssdk.services.bedrockruntime.model.ConversationRole; |
| 28 | +import software.amazon.awssdk.services.bedrockruntime.model.ConverseRequest; |
| 29 | +import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; |
| 30 | +import software.amazon.awssdk.services.bedrockruntime.model.InferenceConfiguration; |
| 31 | +import software.amazon.awssdk.services.bedrockruntime.model.Message; |
| 32 | +import software.amazon.awssdk.services.bedrockruntime.model.SystemContentBlock; |
| 33 | +import software.amazon.awssdk.services.bedrockruntime.model.TokenUsage; |
21 | 34 |
|
22 | | -/** AWS Bedrock chat-completion client (Anthropic messages API). Mirrors BedrockEmbeddingClient. */ |
| 35 | +/** AWS Bedrock chat-completion client (Converse API — works with all Bedrock model families). */ |
23 | 36 | @Slf4j |
24 | 37 | public final class BedrockCompletionClient extends LLMCompletionClient implements AutoCloseable { |
25 | | - private static final ObjectMapper MAPPER = new ObjectMapper(); |
26 | | - private static final String ANTHROPIC_VERSION = "bedrock-2023-05-31"; |
27 | 38 |
|
28 | 39 | private final BedrockRuntimeClient bedrockClient; |
29 | 40 | private final String modelId; |
@@ -61,59 +72,62 @@ protected CompletionResult doComplete( |
61 | 72 | int timeout = options.timeoutSecondsOr(this.timeoutSeconds); |
62 | 73 | CompletionResult result; |
63 | 74 | try { |
64 | | - InvokeModelRequest request = |
65 | | - InvokeModelRequest.builder() |
66 | | - .modelId(model) |
67 | | - .contentType("application/json") |
68 | | - .accept("application/json") |
69 | | - .body( |
70 | | - SdkBytes.fromUtf8String(buildRequestBody(systemPrompt, userPrompt, tokens, temp))) |
71 | | - .overrideConfiguration(c -> c.apiCallTimeout(Duration.ofSeconds(timeout))) |
72 | | - .build(); |
73 | | - InvokeModelResponse response = bedrockClient.invokeModel(request); |
74 | | - result = parseResult(response.body().asUtf8String()); |
| 75 | + ConverseResponse response = |
| 76 | + bedrockClient.converse( |
| 77 | + buildRequest(model, systemPrompt, userPrompt, tokens, temp, timeout)); |
| 78 | + result = toResult(response); |
75 | 79 | } catch (AwsServiceException | SdkClientException e) { |
76 | 80 | throw new LLMCompletionException("Bedrock completion failed", e); |
77 | 81 | } |
78 | 82 | return result; |
79 | 83 | } |
80 | 84 |
|
81 | | - static String buildRequestBody( |
82 | | - String systemPrompt, String userPrompt, int maxTokens, double temperature) { |
83 | | - String result; |
84 | | - try { |
85 | | - ObjectNode payload = MAPPER.createObjectNode(); |
86 | | - payload.put("anthropic_version", ANTHROPIC_VERSION); |
87 | | - payload.put("max_tokens", maxTokens); |
88 | | - payload.put("temperature", temperature); |
89 | | - payload.put("system", systemPrompt); |
90 | | - ArrayNode messages = payload.putArray("messages"); |
91 | | - messages.addObject().put("role", "user").put("content", userPrompt); |
92 | | - result = MAPPER.writeValueAsString(payload); |
93 | | - } catch (IOException e) { |
94 | | - throw new LLMCompletionException("Failed to build Bedrock request body", e); |
| 85 | + static ConverseRequest buildRequest( |
| 86 | + String model, |
| 87 | + String systemPrompt, |
| 88 | + String userPrompt, |
| 89 | + int maxTokens, |
| 90 | + double temperature, |
| 91 | + int timeoutSeconds) { |
| 92 | + return ConverseRequest.builder() |
| 93 | + .modelId(model) |
| 94 | + .system(SystemContentBlock.fromText(systemPrompt)) |
| 95 | + .messages( |
| 96 | + Message.builder() |
| 97 | + .role(ConversationRole.USER) |
| 98 | + .content(ContentBlock.fromText(userPrompt)) |
| 99 | + .build()) |
| 100 | + .inferenceConfig( |
| 101 | + InferenceConfiguration.builder() |
| 102 | + .maxTokens(maxTokens) |
| 103 | + .temperature((float) temperature) |
| 104 | + .build()) |
| 105 | + .overrideConfiguration(c -> c.apiCallTimeout(Duration.ofSeconds(timeoutSeconds))) |
| 106 | + .build(); |
| 107 | + } |
| 108 | + |
| 109 | + static CompletionResult toResult(ConverseResponse response) { |
| 110 | + String text = extractText(response); |
| 111 | + if (text.isEmpty()) { |
| 112 | + throw new LLMCompletionException("Invalid Bedrock response: no text content returned"); |
95 | 113 | } |
96 | | - return result; |
| 114 | + TokenUsage usage = response.usage(); |
| 115 | + int inputTokens = usage != null && usage.inputTokens() != null ? usage.inputTokens() : 0; |
| 116 | + int outputTokens = usage != null && usage.outputTokens() != null ? usage.outputTokens() : 0; |
| 117 | + return new CompletionResult(text, inputTokens, outputTokens); |
97 | 118 | } |
98 | 119 |
|
99 | | - static CompletionResult parseResult(String responseBody) { |
100 | | - CompletionResult result; |
101 | | - try { |
102 | | - JsonNode root = MAPPER.readTree(responseBody); |
103 | | - JsonNode content = root.get("content"); |
104 | | - if (content == null || !content.isArray() || content.isEmpty()) { |
105 | | - throw new LLMCompletionException("Invalid Bedrock response: no content returned"); |
| 120 | + private static String extractText(ConverseResponse response) { |
| 121 | + String text = ""; |
| 122 | + if (response.output() != null && response.output().message() != null) { |
| 123 | + for (ContentBlock block : response.output().message().content()) { |
| 124 | + if (block.text() != null) { |
| 125 | + text = block.text(); |
| 126 | + break; |
| 127 | + } |
106 | 128 | } |
107 | | - JsonNode usage = root.path("usage"); |
108 | | - result = |
109 | | - new CompletionResult( |
110 | | - content.get(0).get("text").asText(), |
111 | | - usage.path("input_tokens").asInt(0), |
112 | | - usage.path("output_tokens").asInt(0)); |
113 | | - } catch (IOException e) { |
114 | | - throw new LLMCompletionException("Failed to parse Bedrock response", e); |
115 | 129 | } |
116 | | - return result; |
| 130 | + return text; |
117 | 131 | } |
118 | 132 |
|
119 | 133 | @Override |
|
0 commit comments