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..94f1e165 --- /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..4df9d029 --- /dev/null +++ b/Examples/text_chat/Sources/TextChat.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// 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 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) + } + + // keep track of the history of the conversation + history.append(Message(prompt)) + + // 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 { + // 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) + } +}