From 520d7771919a159b1a9859a5b722e689b2efd742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 6 Jun 2025 23:24:21 +0200 Subject: [PATCH 1/3] add a text chat example --- .github/workflows/pull_request.yml | 2 +- Examples/text_chat/.gitignore | 8 ++ Examples/text_chat/Package.swift | 30 +++++++ Examples/text_chat/Sources/TextChat.swift | 101 ++++++++++++++++++++++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 Examples/text_chat/.gitignore create mode 100644 Examples/text_chat/Package.swift create mode 100644 Examples/text_chat/Sources/TextChat.swift diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4fedf67c..b82e2dd6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -35,7 +35,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'converse', 'converse-stream' ]" + examples: "[ 'converse', 'converse-stream', 'text_chat' ]" swift-6-language-mode: name: Swift 6 Language Mode diff --git a/Examples/text_chat/.gitignore b/Examples/text_chat/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/text_chat/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/text_chat/Package.swift b/Examples/text_chat/Package.swift new file mode 100644 index 00000000..bd4bc767 --- /dev/null +++ b/Examples/text_chat/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TextChat", + platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], + products: [ + .executable(name: "TextChat", targets: ["TextChat"]), + ], + 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: [ + .executableTarget( + name: "TextChat", + dependencies: [ + .product(name: "BedrockService", package: "swift-bedrock-library"), + .product(name: "Logging", package: "swift-log"), + ] + ) + ] +) diff --git a/Examples/text_chat/Sources/TextChat.swift b/Examples/text_chat/Sources/TextChat.swift new file mode 100644 index 00000000..137e3ad5 --- /dev/null +++ b/Examples/text_chat/Sources/TextChat.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// 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 TextChat { + static func main() async throws { + do { + try await TextChat.run() + } catch { + print("Error:\n\(error)") + } + } + static func run() async throws { + var logger = Logger(label: "TextChat") + 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 = .claudev3_7_sonnet + + guard model.hasConverseModality() else { + throw MyError.incorrectModality("\(model.name) does not support converse") + } + + // a reusable var to build the requests + var request: ConverseRequestBuilder? = nil + + // we keep track of the history of the conversation + var history: [Message] = [] + + // while the user doesn't type "exit" or "quit" + while true { + + print("\nYou: ", terminator: "") + let prompt: String = readLine() ?? "" + guard prompt.isEmpty == false else { continue } + if ["exit", "quit"].contains(prompt.lowercased()) { + break + } + + print("\nAssistant: ", terminator: "") + + if request == nil { + // create a new request + request = try ConverseRequestBuilder(with: model) + .withPrompt(prompt) + } else { + // append the new prompt to the existing request + // ConverseRequestBuilder is stateles, it doesn't keep track of the history + request = try ConverseRequestBuilder(from: request!) + .withHistory(history) + .withPrompt(prompt) + } + + // keep track of the history of the conversation + history.append(Message(prompt)) + + // send the request + let reply = try await bedrock.converseStream(with: request!) + + for try await element in reply.stream { + // process the stream elements + switch element { + case .text(_, let text): + print(text, terminator: "") + case .messageComplete(let message): + print("\n") + history.append(message) + default: + break + } + } + } + } + + enum MyError: Error { + case incorrectModality(String) + } +} From d01c975b618995aeef992967df989a7d207f0c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 7 Jun 2025 10:29:24 +0200 Subject: [PATCH 2/3] swift format --- Examples/text_chat/Package.swift | 2 +- Examples/text_chat/Sources/TextChat.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/text_chat/Package.swift b/Examples/text_chat/Package.swift index bd4bc767..94f1e165 100644 --- a/Examples/text_chat/Package.swift +++ b/Examples/text_chat/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "TextChat", platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], products: [ - .executable(name: "TextChat", targets: ["TextChat"]), + .executable(name: "TextChat", targets: ["TextChat"]) ], dependencies: [ // for production use, uncomment the following line diff --git a/Examples/text_chat/Sources/TextChat.swift b/Examples/text_chat/Sources/TextChat.swift index 137e3ad5..7561b2b0 100644 --- a/Examples/text_chat/Sources/TextChat.swift +++ b/Examples/text_chat/Sources/TextChat.swift @@ -32,8 +32,8 @@ struct TextChat { let bedrock = try await BedrockService( region: .useast1, logger: logger - // uncomment if you use SSO with AWS Identity Center - // authentication: .sso + // uncomment if you use SSO with AWS Identity Center + // authentication: .sso ) // select a model that supports the converse modality @@ -61,7 +61,7 @@ struct TextChat { } print("\nAssistant: ", terminator: "") - + if request == nil { // create a new request request = try ConverseRequestBuilder(with: model) From 0cbf009328e84e2e4f095922235b8780e42fb252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 7 Jun 2025 10:45:13 +0200 Subject: [PATCH 3/3] fix typo and add comments --- Examples/text_chat/Sources/TextChat.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Examples/text_chat/Sources/TextChat.swift b/Examples/text_chat/Sources/TextChat.swift index 7561b2b0..4df9d029 100644 --- a/Examples/text_chat/Sources/TextChat.swift +++ b/Examples/text_chat/Sources/TextChat.swift @@ -68,7 +68,8 @@ struct TextChat { .withPrompt(prompt) } else { // append the new prompt to the existing request - // ConverseRequestBuilder is stateles, it doesn't keep track of the history + // ConverseRequestBuilder is stateless, it doesn't keep track of the history + // thanks to the `if` above, we're sure `request` is not nil request = try ConverseRequestBuilder(from: request!) .withHistory(history) .withPrompt(prompt) @@ -77,7 +78,7 @@ struct TextChat { // keep track of the history of the conversation history.append(Message(prompt)) - // send the request + // send the request. We are sure `request` is not nil let reply = try await bedrock.converseStream(with: request!) for try await element in reply.stream {