Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions IntegrationTests/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ private var integrationTestTargets: [Target] {
"AWSSTS",
"AWSTranscribeStreaming",
"AWSCognitoIdentity",
"AWSBedrockRuntime",
].map { integrationTestTarget($0) }
return integrationTests + [.target(name: "AWSIntegrationTestUtils", path: "./AWSIntegrationTestUtils")]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import AWSBedrockRuntime
import AWSClientRuntime
import ClientRuntime
import Smithy
import XCTest

class InvokeModelWithBidirectionalStreamTest: XCTestCase {
private var client: BedrockRuntimeClient!
private let region = "us-east-1"
private var requestEvents: [String] = []
private let modelID: String = "amazon.nova-sonic-v1:0"

// The two end events required to close the stream successfully.
private let CONTENT_END_EVENT =
"""
{
"event": {
"contentEnd": {
"promptName": "126680f5-5859-4d15-ae70-488de4146484",
"contentName": "b3917935-2398-4889-94a8-e677f6c3e351"
}
}
}
"""
private let PROMPT_END_EVENT =
"""
{
"event": {
"promptEnd": {
"promptName": "126680f5-5859-4d15-ae70-488de4146484",
"contentName": "b3917935-2398-4889-94a8-e677f6c3e351"
}
}
}
"""

override func setUp() async throws {
let PREAMBLE_EVENTS: [String] = [
// Start Session.
"""
{
"event": {
"sessionStart": {
"inferenceConfiguration": {
"maxTokens": 10000,
"topP": 0.95,
"temperature": 0.9
}
}
}
}
""",
// Start Prompt.
"""
{
"event": {
"promptStart": {
"promptName": "126680f5-5859-4d15-ae70-488de4146484",
"textOutputConfiguration": {
"mediaType": "text/plain"
},
"audioOutputConfiguration": {
"mediaType": "audio/lpcm",
"sampleRateHertz": 24000,
"sampleSizeBits": 16,
"channelCount": 1,
"voiceId": "en_us_matthew",
"encoding": "base64",
"audioType": "SPEECH"
},
"toolUseOutputConfiguration": {
"mediaType": "application/json"
},
"toolConfiguration": {
"tools": []
}
}
}
}
""",
// Start Content.
"""
{
"event": {
"contentStart": {
"promptName": "126680f5-5859-4d15-ae70-488de4146484",
"contentName": "a6431ef2-e23c-4f8c-a552-3f308629d3c3",
"type": "TEXT",
"interactive": true,
"textInputConfiguration": {
"mediaType": "text/plain"
}
}
}
}
""",
// System Setup.
"""
{
"event": {
"textInput": {
"promptName": "126680f5-5859-4d15-ae70-488de4146484",
"contentName": "a6431ef2-e23c-4f8c-a552-3f308629d3c3",
"content": "You are a friend. The user and you will engage in a spoken dialog exchanging the transcripts of a natural real-time conversation. Keep your responses short, generally two or three sentences for chatty scenarios.",
"role": "SYSTEM"
}
}
}
""",
// End System Setup.
"""
{
"event": {
"contentEnd": {
"promptName": "126680f5-5859-4d15-ae70-488de4146484",
"contentName": "a6431ef2-e23c-4f8c-a552-3f308629d3c3"
}
}
}
""",
// Start Audio Stream.
"""
{
"event": {
"contentStart": {
"promptName": "126680f5-5859-4d15-ae70-488de4146484",
"contentName": "b3917935-2398-4889-94a8-e677f6c3e351",
"type": "AUDIO",
"interactive": true,
"audioInputConfiguration": {
"mediaType": "audio/lpcm",
"sampleRateHertz": 16000,
"sampleSizeBits": 16,
"channelCount": 1,
"audioType": "SPEECH",
"encoding": "base64"
}
}
}
}
"""
]

// Individual audio events.
// Contains "%@" which is a formatter string that will be replaced by
// the Base64-encoded audio content.
let AUDIO_EVENT =
"""
{
"event": {
"audioInput": {
"promptName": "126680f5-5859-4d15-ae70-488de4146484",
"contentName": "b3917935-2398-4889-94a8-e677f6c3e351",
"content": "%@",
"role": "USER"
}
}
}
"""

// First append preamble (setup) events.
requestEvents.append(contentsOf: PREAMBLE_EVENTS)

// Then, parse audio file into separate events and append them to list of events.
guard let audioURL = Bundle.module.url(forResource: "japan16k", withExtension: "raw") else {
throw ClientError.dataNotFound("Audio file not found.")
}
let audioData = try Data(contentsOf: audioURL)
let chunkSize = 1024
let totalSize = audioData.count
var offset = 0

while offset < totalSize {
let end = min(offset + chunkSize, totalSize)
let chunk = audioData.subdata(in: offset..<end)
let encodedChunk = chunk.base64EncodedString()
requestEvents.append(String(format: AUDIO_EVENT, encodedChunk))
offset += chunkSize
}
}

// Temporarily disabled for Linux which uses CRT HTTP client.
// Enabling in Linux is pending either service side fix or SDK side workaround for allowing
// END_STREAM HTTP2 frame with empty contents.
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
func testInvokeModelWithBidirectionalStream() async throws {
// Initialize input stream & continuation for the API input.
let inputStream: AsyncThrowingStream<BedrockRuntimeClientTypes.InvokeModelWithBidirectionalStreamInput, Swift.Error>
let continuation: AsyncThrowingStream<BedrockRuntimeClientTypes.InvokeModelWithBidirectionalStreamInput, Swift.Error>.Continuation
(inputStream, continuation) = AsyncThrowingStream.makeStream()

// Start up background task that feeds events to the stream.
Task {
for event in requestEvents {
let currentEvent = BedrockRuntimeClientTypes.InvokeModelWithBidirectionalStreamInput.chunk(
BedrockRuntimeClientTypes.BidirectionalInputPayloadPart(bytes: event.data(using: .utf8))
)
// Put 0.5 second delays to simulate real time events.
try await Task.sleep(nanoseconds: UInt64(500_000_000))
continuation.yield(currentEvent)
}
continuation.yield(BedrockRuntimeClientTypes.InvokeModelWithBidirectionalStreamInput.chunk(
BedrockRuntimeClientTypes.BidirectionalInputPayloadPart(bytes: CONTENT_END_EVENT.data(using: .utf8))
))
continuation.yield(BedrockRuntimeClientTypes.InvokeModelWithBidirectionalStreamInput.chunk(
BedrockRuntimeClientTypes.BidirectionalInputPayloadPart(bytes: PROMPT_END_EVENT.data(using: .utf8))
))
continuation.finish()
}

// Create BedrockRuntime client.
let bedrock = try BedrockRuntimeClient(region: region)

// Call the `invokeModelWithBidirectionalStream` API.
let resp = try await bedrock.invokeModelWithBidirectionalStream(input: InvokeModelWithBidirectionalStreamInput(
body: inputStream,
modelId: modelID
))

// Iterate on the returned output stream for output events.
for try await output in resp.body! {
switch output {
case .chunk(let output):
XCTAssertTrue(output.bytes != nil && !output.bytes!.isEmpty)
Comment thread
jbelkins marked this conversation as resolved.
case .sdkUnknown(let str):
throw ClientError.unknownError(str)
}
}
}
#endif
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@
"identifier" : "AWSEventBridgeIntegrationTests",
"name" : "AWSEventBridgeIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSBedrockRuntimeIntegrationTests",
"name" : "AWSBedrockRuntimeIntegrationTests"
}
}
],
"version" : 1
Expand Down
2 changes: 1 addition & 1 deletion scripts/integration-test-sdk.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Only include services needed for running integration tests
onlyIncludeModels=kinesis,s3,sso-admin,transcribe-streaming,sqs,mediaconvert,sts,cognito-identity,iam,ec2,ecs,cloudwatch-logs,s3-control,eventbridge,cloudfront,cloudfront-keyvaluestore,route-53,glacier,dynamodb
onlyIncludeModels=kinesis,s3,sso-admin,transcribe-streaming,sqs,mediaconvert,sts,cognito-identity,iam,ec2,ecs,cloudwatch-logs,s3-control,eventbridge,cloudfront,cloudfront-keyvaluestore,route-53,glacier,dynamodb,bedrock-runtime
Loading