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
8 changes: 8 additions & 0 deletions Examples/converse-stream/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
32 changes: 32 additions & 0 deletions Examples/converse-stream/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "ConverseStream",
platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)],
products: [
.executable(name: "ConverseStream", targets: ["ConverseStream"])
],
dependencies: [
// for production use, uncomment the following line
// .package(url: "https://github.com/build-on-aws/swift-bedrock-library.git", branch: "main"),

// for local development, use the following line
.package(name: "swift-bedrock-library", path: "../.."),

.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "ConverseStream",
dependencies: [
.product(name: "BedrockService", package: "swift-bedrock-library"),
.product(name: "Logging", package: "swift-log"),
]
)
]
)
91 changes: 91 additions & 0 deletions Examples/converse-stream/Sources/ConverseStream.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Bedrock Library open source project
//
// Copyright (c) 2025 Amazon.com, Inc. or its affiliates
// and the Swift Bedrock Library project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Bedrock Library project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import BedrockService
import Logging

@main
struct Main {
static func main() async throws {
do {
try await Main.converseStream()
} catch {
print("Error:\n\(error)")
}
}
static func converseStream() async throws {
var logger = Logger(label: "Converse")
logger.logLevel = .debug

let bedrock = try await BedrockService(
region: .useast1,
logger: logger
// uncomment if you use SSO with AWS Identity Center
// authentication: .sso
)

// select a model that supports the converse modality
// models must be enabled in your AWS account
let model: BedrockModel = .nova_lite

guard model.hasConverseModality() else {
throw MyError.incorrectModality("\(model.name) does not support converse")
}

// create a request
let builder = try ConverseRequestBuilder(with: model)
.withPrompt("Tell me about rainbows")

// send the request
let reply = try await bedrock.converseStream(with: builder)

// the reply gives access to two streams.
// 1. `stream` is a high-level stream that provides elements of the conversation :
// - messageStart: this is the start of a message, it contains the role (assistant or user)
// - text: this is a delta of the text content, it contains the partial text
// - reasoning: this is a delta of the reasoning content, it contains the partial reasoning text
// - toolUse: this is a buffered tool use response, it contains the tool name and id, and the input parameters
// - message complete: this includes the complete Message, ready to be added to the history and used for future requests
// - metaData: this is the metadata about the response, it contains statitics about the response, such as the number of tokens used and the latency
//
// 2. `sdkStream` is the low-level stream provided by the AWS SDK. Use it when you need low level access to the stream,
// such as when you want to handle the stream in a custom way or when you need to access the raw data.
// see : https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html#conversation-inference-call-response-converse-stream
for try await element in reply.stream {
// process the stream elements
switch element {
case .messageStart(let role):
logger.info("Message started with role: \(role)")
case .text(_, let text):
print(text, terminator: "")
case .reasoning(let index, let reasoning):
logger.info("Reasoning delta: \(reasoning)", metadata: ["index": "\(index)"])
case .toolUse(let index, let toolUse):
logger.info(
"Tool use: \(toolUse.name) with id: \(toolUse.id) and input: \(toolUse.input)",
metadata: ["index": "\(index)"]
)
case .messageComplete(_):
print("\n")
case .metaData(let metaData):
logger.info("Metadata: \(metaData)")
}
}
}

enum MyError: Error {
case incorrectModality(String)
}
}
7 changes: 6 additions & 1 deletion Examples/converse/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ let package = Package(
.executable(name: "Converse", targets: ["Converse"])
],
dependencies: [
.package(url: "https://github.com/build-on-aws/swift-bedrock-library.git", branch: "main"),
// for production use, uncomment the following line
// .package(url: "https://github.com/build-on-aws/swift-bedrock-library.git", branch: "main"),

// for local development, use the following line
.package(name: "swift-bedrock-library", path: "../.."),

.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion Examples/converse/Sources/Converse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct Main {
region: .useast1,
logger: logger
// uncomment if you use SSO with AWS Identity Center
// authentication: .sso
// authentication: .sso
)

// select a model that supports the converse modality
Expand Down
61 changes: 34 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,38 +125,45 @@ The stream will contain `ConverseStreamElement` object that can either be `conte
To create the next builder, with the same model and inference parameters, use the full message from the `.messageComplete`.

```swift
let model: BedrockModel = .nova_lite
let model: BedrockModel = .nova_lite

guard model.hasConverseModality() else {
throw MyError.incorrectModality("\(model.name) does not support converse")
}
guard model.hasConverseModality(.reasoning) else {
throw MyError.incorrectModality("\(model.name) does not support reasoning")
}
guard model.hasConverseModality() else {
throw MyError.incorrectModality("\(model.name) does not support converse")
}

var builder = try ConverseRequestBuilder(from: builder, with: reply)
.withPrompt("Tell me more about the birds in Paris")
// create a request
let builder = try ConverseRequestBuilder(with: model)
.withPrompt("Tell me about rainbows")

let stream = try await bedrock.converseStream(with: builder)
// send the request
let reply = try await bedrock.converseStream(with: builder)

for try await element in stream {
switch element {
case .contentSegment(let contentSegment):
switch contentSegment {
case .text(_, let text):
print(text, terminator: "")
default:
break
}
case .contentBlockComplete:
print("\n\n")
case .messageComplete(let message):
assistantMessage = message
}
}
// consume the stream of elements
for try await element in reply.stream {

builder = try ConverseRequestBuilder(from: builder, with: assistantMessage)
.withPrompt("And what about the rats?")
switch element {
case .messageStart(let role):
logger.info("Message started with role: \(role)")

case .text(_, let text):
print(text, terminator: "")

case .reasoning(let index, let reasoning):
logger.info("Reasoning delta: \(reasoning)", metadata: ["index": "\(index)"])

case .toolUse(let index, let toolUse):
logger.info(
"Tool use: \(toolUse.name) with id: \(toolUse.id) and input: \(toolUse.input)",
metadata: ["index": "\(index)"]
)

case .messageComplete(_):
print("\n")

case .metaData(let metaData):
logger.info("Metadata: \(metaData)")
}
}
```

### Vision
Expand Down
21 changes: 11 additions & 10 deletions Sources/Converse/BedrockService+ConverseStreaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ extension BedrockService {
/// BedrockLibraryError.invalidPrompt if the prompt is empty or too long
/// BedrockLibraryError.invalidModality for invalid modality from the selected model
/// BedrockLibraryError.invalidSDKResponse if the response body is missing
/// - Returns: A stream of ConverseResponseStreaming objects
/// - Returns: A ConverseReplyStream object that gives access to the high-level stream of ConverseStreamElements objects
/// or the low-level stream provided by the AWS SDK.
public func converseStream(
with model: BedrockModel,
conversation: [Message],
Expand All @@ -46,7 +47,7 @@ extension BedrockService {
tools: [Tool]? = nil,
enableReasoning: Bool? = false,
maxReasoningTokens: Int? = nil
) async throws -> AsyncThrowingStream<ConverseStreamElement, any Error> {
) async throws -> ConverseReplyStream {
do {
guard model.hasConverseStreamingModality() else {
throw BedrockLibraryError.invalidModality(
Expand Down Expand Up @@ -118,18 +119,18 @@ extension BedrockService {
// - message metadata
// see https://github.com/awslabs/aws-sdk-swift/blob/2697fb44f607b9c43ad0ce5ca79867d8d6c545c2/Sources/Services/AWSBedrockRuntime/Sources/AWSBedrockRuntime/Models.swift#L3478
// it will be the responsibility of the user to handle the stream and re-assemble the messages and content
// TODO: should we expose the SDK ConverseStreamOutput from the SDK ? or wrap it (what's the added value) ?

let reply = ConverseReplyStream(sdkStream)
let reply = try ConverseReplyStream(sdkStream)

// this time, a different stream is created from the previous one, this one has the following elements
// - content segment: this contains a ContentSegment, an enum which can be a .text(Int, String),
// the integer is the id for the content block that the content segment is a part of,
// the String is the part of text that is send from the model.
// - content block complete: this includes the id of the completed content block and the complete content block itself
// - messageStart: this is the start of a message, it contains the role (assistant or user)
// - text: this is a delta of the text content, it contains the partial text
// - reasoning: this is a delta of the reasoning content, it contains the partial reasoning text
// - toolUse: this is a buffered tool use response, it contains the tool name and id, and the input parameters
// - message complete: this includes the complete Message, ready to be added to the history and used for future requests
// - metaData: this is the metadata about the response, it contains statitics about the response, such as the number of tokens used and the latency

return reply.stream
return reply

} catch {
try handleCommonError(error, context: "invoking converse stream")
Expand All @@ -143,7 +144,7 @@ extension BedrockService {
/// - Returns: A stream of ConverseResponseStreaming objects
public func converseStream(
with builder: ConverseRequestBuilder
) async throws -> AsyncThrowingStream<ConverseStreamElement, any Error> {
) async throws -> ConverseReplyStream {
logger.trace("Conversing and streaming")
do {
var history = builder.history
Expand Down
44 changes: 43 additions & 1 deletion Sources/Converse/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,27 @@
import Foundation

public struct Message: Codable, CustomStringConvertible, Sendable {

// https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_MessageStopEvent.html
// end_turn | tool_use | max_tokens | stop_sequence | guardrail_intervened | content_filtered
public enum StopReason: Codable, Sendable {
case endTurn
case toolUse
case maxTokens
case stopSequence
case guardrailIntervened
case contentFiltered
}
public let role: Role
public let content: [Content]
public let stopReason: StopReason?

// MARK - initializers

public init(from role: Role, content: [Content]) {
public init(from role: Role, content: [Content], stopReason: StopReason? = nil) {
self.role = role
self.content = content
self.stopReason = stopReason
}

/// convenience initializer for message with only a user prompt
Expand Down Expand Up @@ -111,6 +124,15 @@ public struct Message: Codable, CustomStringConvertible, Sendable {
public func hasTextContent() -> Bool {
content.contains { $0.isText() }
}
public func textContent() -> String? {
let content = content.first(where: { $0.isText() })
if case .text(let text) = content {
return text
} else {
return nil
}
}

public func hasImageContent() -> Bool {
content.contains { $0.isImage() }
}
Expand All @@ -137,4 +159,24 @@ public struct Message: Codable, CustomStringConvertible, Sendable {
role: role.getSDKConversationRole()
)
}

public static func stopReason(fromSDK sdkStopReason: BedrockRuntimeClientTypes.StopReason?) -> StopReason? {
switch sdkStopReason {
case .endTurn:
return .endTurn
case .toolUse:
return .toolUse
case .maxTokens:
return .maxTokens
case .stopSequence:
return .stopSequence
case .guardrailIntervened:
return .guardrailIntervened
case .contentFiltered:
return .contentFiltered
default:
return nil
}
}

}
12 changes: 11 additions & 1 deletion Sources/Converse/Role.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
@preconcurrency import AWSBedrockRuntime
import Foundation

public struct Role: Codable, Sendable, Equatable {
public struct Role: Codable, Sendable, Equatable, CustomStringConvertible {
private enum RoleType: Codable, Sendable, Equatable {
case user
case assistant
Expand Down Expand Up @@ -71,9 +71,19 @@ public struct Role: Codable, Sendable, Equatable {
}
}
/// Returns the type of the role as a string.
public var description: String {
switch self.type {
case .user: return "user"
case .assistant: return "assistant"
}
}

// Equatable
public static func == (lhs: Role, rhs: Role) -> Bool {
lhs.type == rhs.type
}

// convenience static properties for common roles
private init(_ type: RoleType) {
self.type = type
}
Expand Down
Loading