diff --git a/.github/workflows/build_soundness.yml b/.github/workflows/build_soundness.yml index e4c6e1db..e100728d 100644 --- a/.github/workflows/build_soundness.yml +++ b/.github/workflows/build_soundness.yml @@ -1,15 +1,24 @@ -name: Build And Soundness checks +name: Build, tests & soundness checks -on: [push, pull_request] +on: [pull_request] jobs: - build: + swift-bedrock-library: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Run tests - working-directory: backend + working-directory: swift-bedrock-library + run: swift test + + playground-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Web playground backend tests + working-directory: web-playground/backend run: swift build soundness: diff --git a/.gitignore b/.gitignore index 1393e260..f4868035 100644 --- a/.gitignore +++ b/.gitignore @@ -14,46 +14,40 @@ Package.resolved .env Makefile -# backend -backend/.DS_Store -backend/.build -backend/.swiftpm -backend/.devContainer -backend/Packages -backend/*.xcodeproj -backend/xcuserdata/ -backend/.vscode/* -backend/!/.vscode/hummingbird.code-snippets -backend/.env.* -backend/.env -backend/img/generated_images -backend/.vscode -backend/Makefile - -# frontend -frontend/node_modules -frontend/.pnp -frontend/.pnp.js -frontend/.yarn/install-state.gz - -frontend/coverage - -frontend/.next/ -frontend/out/ - -frontend/build - -frontend/.DS_Store -frontend/*.pem - -frontend/npm-debug.log* -frontend/yarn-debug.log* -frontend/yarn-error.log* - -frontend/.env*.local - -frontend/.vercel - -frontend/*.tsbuildinfo -frontend/next-env.d.ts -frontend/Makefile +node_modules + +# **/backend +**/backend/.DS_Store +**/backend/.build +**/backend/.swiftpm +**/backend/.devContainer +**/backend/Packages +**/backend/*.xcodeproj +**/backend/xcuserdata/ +**/backend/.vscode/* +**/backend/!/.vscode/hummingbird.code-snippets +**/backend/.env.* +**/backend/.env +**/backend/img/generated_images +**/backend/.vscode +**/backend/Makefile + +# **/frontend +**/frontend/node_modules +**/frontend/.pnp +**/frontend/.pnp.js +**/frontend/.yarn/install-state.gz +**/frontend/coverage +**/frontend/.next/ +**/frontend/out/ +**/frontend/build +**/frontend/.DS_Store +**/frontend/*.pem +**/frontend/npm-debug.log* +**/frontend/yarn-debug.log* +**/frontend/yarn-error.log* +**/frontend/.env*.local +**/frontend/.vercel +**/frontend/*.tsbuildinfo +**/frontend/next-env.d.ts +**/frontend/Makefile diff --git a/.licenseignore b/.licenseignore index 09fc0de2..ff286b5d 100644 --- a/.licenseignore +++ b/.licenseignore @@ -7,6 +7,7 @@ **/*.md CONTRIBUTORS.txt LICENSE.txt +LICENSE NOTICE.txt Package.swift Package@swift-*.swift @@ -31,10 +32,9 @@ Package.resolved *.yaml *.yml *.json -*.gif -frontend/* +**/*.gif +**/*.png +**/frontend/* +**/*.code-snippets .DS_Store -backend/.vscode/hummingbird.code-snippets -backend/img/image.png -LICENSE -.licenseignore +.licenseignore \ No newline at end of file diff --git a/backend/.swift-format b/.swift-format similarity index 100% rename from backend/.swift-format rename to .swift-format diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 504f596c..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "configurations": [ - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-bedrock-playground}/backend", - "name": "Debug backend (backend)", - "program": "${workspaceFolder:swift-bedrock-playground}/backend/.build/debug/backend", - "preLaunchTask": "swift: Build Debug backend (backend)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-bedrock-playground}/backend", - "name": "Release backend (backend)", - "program": "${workspaceFolder:swift-bedrock-playground}/backend/.build/release/backend", - "preLaunchTask": "swift: Build Release backend (backend)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-bedrock-playground}/template", - "name": "Debug {{HB_EXECUTABLE_NAME}} (template)", - "program": "${workspaceFolder:swift-bedrock-playground}/template/.build/debug/{{HB_EXECUTABLE_NAME}}", - "preLaunchTask": "swift: Build Debug {{HB_EXECUTABLE_NAME}} (template)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-bedrock-playground}/template", - "name": "Release {{HB_EXECUTABLE_NAME}} (template)", - "program": "${workspaceFolder:swift-bedrock-playground}/template/.build/release/{{HB_EXECUTABLE_NAME}}", - "preLaunchTask": "swift: Build Release {{HB_EXECUTABLE_NAME}} (template)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-bedrock-playground}/backend", - "name": "Debug App (backend)", - "program": "${workspaceFolder:swift-bedrock-playground}/backend/.build/debug/App", - "preLaunchTask": "swift: Build Debug App (backend)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-bedrock-playground}/backend", - "name": "Release App (backend)", - "program": "${workspaceFolder:swift-bedrock-playground}/backend/.build/release/App", - "preLaunchTask": "swift: Build Release App (backend)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-bedrock-playground}/backend", - "name": "Debug SwiftBedrockService (backend)", - "program": "${workspaceFolder:swift-bedrock-playground}/backend/.build/debug/SwiftBedrockService", - "preLaunchTask": "swift: Build Debug SwiftBedrockService (backend)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-bedrock-playground}/backend", - "name": "Release SwiftBedrockService (backend)", - "program": "${workspaceFolder:swift-bedrock-playground}/backend/.build/release/SwiftBedrockService", - "preLaunchTask": "swift: Build Release SwiftBedrockService (backend)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-fm-playground}/backend", - "name": "Debug PlaygroundAPI (backend)", - "program": "${workspaceFolder:swift-fm-playground}/backend/.build/debug/PlaygroundAPI", - "preLaunchTask": "swift: Build Debug PlaygroundAPI (backend)" - }, - { - "type": "lldb", - "request": "launch", - "args": [], - "cwd": "${workspaceFolder:swift-fm-playground}/backend", - "name": "Release PlaygroundAPI (backend)", - "program": "${workspaceFolder:swift-fm-playground}/backend/.build/release/PlaygroundAPI", - "preLaunchTask": "swift: Build Release PlaygroundAPI (backend)" - } - ] -} \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..acc61287 --- /dev/null +++ b/Package.swift @@ -0,0 +1,59 @@ +// 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: "SwiftBedrockLibrary", + platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], + products: [ + .library(name: "BedrockService", targets: ["BedrockService"]), + .library(name: "BedrockTypes", targets: ["BedrockTypes"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/awslabs/aws-sdk-swift", from: "1.3.3"), + .package(url: "https://github.com/smithy-lang/smithy-swift", from: "0.118.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"), + .package(url: "https://github.com/awslabs/aws-crt-swift", from: "0.5.0"), + ], + targets: [ + .target( + name: "BedrockService", + dependencies: [ + .target(name: "BedrockTypes"), + .product(name: "AWSClientRuntime", package: "aws-sdk-swift"), + .product(name: "AWSBedrock", package: "aws-sdk-swift"), + .product(name: "AWSBedrockRuntime", package: "aws-sdk-swift"), + .product(name: "Smithy", package: "smithy-swift"), + .product(name: "Logging", package: "swift-log"), + .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift"), + ], + path: "swift-bedrock-library/Sources/BedrockService" + ), + .target( + name: "BedrockTypes", + dependencies: [ + .product(name: "AWSBedrockRuntime", package: "aws-sdk-swift"), + .product(name: "AWSBedrock", package: "aws-sdk-swift"), + .product(name: "Smithy", package: "smithy-swift"), + ], + path: "swift-bedrock-library/Sources/BedrockTypes" + ), + .testTarget( + name: "BedrockServiceTests", + dependencies: [ + .target(name: "BedrockService"), + .target(name: "BedrockTypes"), + ], + path: "swift-bedrock-library/Tests/BedrockServiceTests" + ), + .testTarget( + name: "BedrockTypesTests", + dependencies: [ + .target(name: "BedrockTypes") + ], + path: "swift-bedrock-library/Tests/BedrockTypesTests" + ), + ] +) diff --git a/README.md b/README.md index 84a07823..120bff0f 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,46 @@ -# Swift FM Playground +# Amazon Bedrock Swift Library and web playground -Welcome to the Swift Foundation Model (FM) Playground, an example app to explore how to use **Amazon Bedrock** with the AWS SDK for Swift. +This repository contains projects demonstrating how to use [Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) with Swift. -> 🚨 **Important:** This application is for educational purposes and not intended for production use. +## Projects -## Overview +### 1. Swift Bedrock Library -> 🚧 Under construction 🚧 +A tiny layer on top of the [AWS SDK for Swift](https://github.com/awslabs/aws-sdk-swift) for interacting with Amazon Bedrock foundation models. This library provides a convenient way to access Amazon Bedrock's capabilities from Swift applications. -## Prerequisites +[Go to Swift Bedrock Library →](swift-bedrock-library/README.md) + +### 2. Swift FM Playground + +An interactive web application that demonstrates the capabilities of Amazon Bedrock foundation models using the Swift Bedrock Library. The playground includes: + +- A Swift "backend for frontend" that interfaces with Amazon Bedrock +- A React frontend for interacting with the models through a user-friendly interface -> 🚧 Under construction 🚧 +[Go to Swift FM Playground →](swift-fm-playground/web-playground/README.md) -## Running the Application +## Getting Started -> 🚧 Under construction 🚧 +Each project has its own README with specific setup instructions: + +- For the Swift Bedrock Library, see the [library README](swift-bedrock-library/README.md) +- For the Swift FM Playground, see the [playground README](swift-fm-playground/web-playground/README.md) + +## Prerequisites -## Accessing the Application +- Swift 6.0 or later +- AWS account with access to Amazon Bedrock +- [AWS credentials configured locally](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) or [SSO configured with AWS Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/manage-your-accounts.html) configured -To access the application, open `http://localhost:3000` in your web browser. +## Acknowledgment -## Stopping the Application +This library and playground have been written by [Mona Dierickx](https://www.linkedin.com/in/mona-dierickx/), during her last year of studies at [HoGent](https://www.hogent.be/), Belgium. -To halt the application, you will need to stop both the backend and frontend processes. +Thank you for your enthousiasm and positive attitude during the three months we worked together. (February 2025 - May 2025). -### Stopping the Frontend +Thank for Professor Steven Van Impe for allowing us to work with these young talents. -In the terminal where the frontend is running, press `Ctrl + C` to terminate the process. -### Stopping the Backend +## License -Similarly, in the backend terminal, use the `Ctrl + C` shortcut to stop the server. +These projects are licensed under the Apache License 2.0. See the LICENSE files in each project for details. \ No newline at end of file diff --git a/swift-bedrock-library/README.md b/swift-bedrock-library/README.md new file mode 100644 index 00000000..ee49c33e --- /dev/null +++ b/swift-bedrock-library/README.md @@ -0,0 +1,1074 @@ +# Swift Bedrock Library + +A tiny layer on top of the [AWS SDK for Swift](https://github.com/awslabs/aws-sdk-swift) for interacting with [Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) foundation models. This library provides a convenient way to access Amazon Bedrock's capabilities from Swift applications. + +## Getting started with BedrockService + +1. Set-up your `Package.swift` + +First add dependencies: +```bash +swift package add-dependency https://github.com/build-on-aws/swift-fm-playground.git --branch main +swift package add-target-dependency BedrockService TargetName --package swift-fm-playground +``` + +Next up add `platforms` configuration after `name` + +```swift +platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], +``` + +Your `Package.swift` should now look something like this: +```swift +import PackageDescription + +let package = Package( + name: "ProjectName", + platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], + dependencies: [ + .package(url: "https://github.com/build-on-aws/swift-fm-playground.git", branch: "main"), + ], + targets: [ + .executableTarget( + name: "TargetName", + dependencies: [ + .product(name: "BedrockService", package: "swift-fm-playground"), + ] + ) + ] +) +``` + +2. Import the BedrockService and BedrockTypes + +```swift +import BedrockService +import BedrockTypes +``` + +3. Initialize the BedrockService + +Choose what Region to use, whether to use AWS SSO authentication instead of standard credentials and pass a logger. If no region is passed it will default to `.useast1`, if no logger is provided a default logger with the name `bedrock.service` is created. The log level will be set to the environment variable `BEDROCK_SERVICE_LOG_LEVEL` or default to `.trace`. Choose the form of authentication you wish to use. + +```swift +let bedrock = try await BedrockService( + region: .uswest1, + logger: logger, + authentication: .sso +) +``` + +4. List the available models + +Use the `listModels()` function to test your set-up. This function will return an array of `ModelSummary` objects, each one representing a model supported by Amazon Bedrock. The ModelSummaries that contain a `BedrockModel` object are the models supported by BedrockService. + +```swift +let models = try await bedrock.listModels() +``` + +## Chatting using the Converse or ConverseStream API + +### Text prompt + +To send a text prompt to a model, first choose a model that supports converse, you can verify this by using the `hasConverseModality` function on the `BedrockModel`. Then use the model to create a `ConverseRequestBuilder`, add your prompt to it with the `.withPrompt` function. Use the builder to send your request to the Converse API with the `converse` function. You can then easily print the reply and use it to create a new builder with the same model and inference parameters but with an updated history. + +```swift +let model: BedrockModel = .nova_lite + +guard model.hasConverseModality() else { + throw MyError.incorrectModality("\(model.name) does not support converse") +} + +var builder = try ConverseRequestBuilder(with: model) + .withPrompt("Tell me about rainbows") + +var reply = try await bedrock.converse(with: builder) + +print("Assistant: \(reply)") + +builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Do you think birds can see them too?") + +reply = try await bedrock.converse(with: builder) + +print("Assistant: \(reply)") +``` + +Optionally add inference parameters. Note that the builder can be used to create the next builder with the same parameters and the updated history. + +```swift +let builder = try ConverseRequestBuilder(with: model) + .withPrompt("Tell me about rainbows") + .withMaxTokens(512) + .withTemperature(0.2) + .withStopSequences(["END", "STOP", ""]) + .withSystemPrompts(["Do not pretend to be human", "Never talk about goats", "You like puppies"]) + +var reply = try await bedrock.converse(with: builder) + +builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Do you think birds can see them too?") + +reply = try await bedrock.converse(with: builder) +``` + +To get a streaming response, use the same `ConverseRequestBuilder`, but the `converseStream` function instead of the `converse` function. Ensure the model you are using supports streaming. +The stream will contain `ConverseStreamElement` object that can either be `contentSegment` containing a piece of content, `contentComplete` signifying that a `Content` object is complete or a `messageComplete` to return the final completed message with all the complete content parts. A `contentSegment` could either be `text`, `toolUse`, `reasoning` or `encryptedReasoning`. + +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 + +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") +} + +var builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Tell me more about the birds in Paris") + +let stream = 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 + } +} + +builder = try ConverseRequestBuilder(from: builder, with: assistantMessage) + .withPrompt("And what about the rats?") +``` + +### Vision + +To send an image to a model, first ensure the model supports vision. Next simply add the image to the `ConverseRequestBuilder` with the `withImage` function. The function can either take an `ImageBlock` object or the format and bytes to construct the object. + + +```swift +let model: BedrockModel = .nova_lite + +guard model.hasConverseModality(.vision) else { + throw MyError.incorrectModality("\(model.name) does not support converse vision") +} + +let builder = try ConverseRequestBuilder(with: model) + .withPrompt("Can you tell me about this plant?") + .withImage(format: .jpeg, source: base64EncodedImage) + +let reply = try await bedrock.converse(with: builder) + +print("Assistant: \(reply)") +``` + +Optionally add inference parameters. + +```swift +let builder = try ConverseRequestBuilder(with: model) + .withPrompt("Can you tell me about this plant?") + .withImage(format: .jpeg, source: base64EncodedImage) + .withTemperature(0.8) + +let reply = try await bedrock.converse(with: builder) +``` + +Note that the builder can be used to create the next builder with the same parameters and the updated history. + +```swift +var builder = try ConverseRequestBuilder(with: model) + .withPrompt("Can you tell me about this plant?") + .withImage(format: .jpeg, source: base64EncodedImage) + .withTemperature(0.8) + +var reply = try await bedrock.converse(with: builder) + +builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Where can I find those plants?") + +reply = try await bedrock.converse(with: builder) +``` + +To use streaming use the exact same `ConverseRequestBuilder`, but use the `converseStream` function instead of the `converse` function. An example is given in the [text prompt section](#text-prompt). + +### Document + +To send a document to a model, first ensure the model supports document. Next simply add the document to the `ConverseRequestBuilder` with the `withDocument` function. The function can either take a `DocumentBlock` object or the name, format and bytes to construct the object. + +```swift +let model: BedrockModel = .nova_lite + +guard model.hasConverseModality(.document) else { + throw MyError.incorrectModality("\(model.name) does not support converse document") +} + +let builder = try ConverseRequestBuilder(with: model) + .withPrompt("Can you give me a summary of this chapter?") + .withDocument(name: "Chapter 1", format: .pdf, source: base64EncodedDocument) + +let reply = try await bedrock.converse(with: builder) + +print("Assistant: \(reply)") +``` + +Optionally add inference parameters. + +```swift +let builder = try ConverseRequestBuilder(with: model) + .withPrompt("Can you give me a summary of this chapter?") + .withDocument(name: "Chapter 1", format: .pdf, source: base64EncodedDocument) + .withMaxTokens(512) + .withTemperature(0.4) + +var reply = try await bedrock.converse(with: builder) +``` + +Note that the builder can be used to create the next builder with the same parameters and the updated history. + +```swift +var builder = try ConverseRequestBuilder(with: model) + .withPrompt("Can you give me a summary of this chapter?") + .withDocument(name: "Chapter 1", format: .pdf, source: base64EncodedDocument) + .withMaxTokens(512) + .withTemperature(0.4) + +var reply = try await bedrock.converse(with: builder) + +builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Thanks, can you make a Dutch version as well?") + +reply = try await bedrock.converse(with: builder) +``` + +To use streaming use the exact same `ConverseRequestBuilder`, but use the `converseStream` function instead of the `converse` function. An example is given in the [text prompt section](#text-prompt). + +### Tools + +For tool usage, first ensure the model supports the use of tools. Next define at least one `Tool` and add it to the `ConverseRequestBuilder` with the `withTool` function (or the `withTools` function to add several tools at once). After sending a request the model could now send back a `ToolUse` asking for specific information from a specific tool. Use this to send the information back in a `ToolResult`, by using the `withToolResult` function. You will now receive a reply informed by the result from the tool. + + +```swift +let model: BedrockModel = .nova_lite + +// verify that the model supports tool usage +guard model.hasConverseModality(.toolUse) else { + throw MyError.incorrectModality("\(model.name) does not support converse tools") +} + +// define the inputschema for your tool +let inputSchema = JSON([ + "type": "object", + "properties": [ + "sign": [ + "type": "string", + "description": "The call sign for the radio station for which you want the most popular song. Example calls signs are WZPZ and WKRP." + ] + ], + "required": [ + "sign" + ] +]) + +// create a Tool object +let tool = try Tool(name: "top_song", inputSchema: inputSchema, description: "Get the most popular song played on a radio station.") + +// create a ConverseRequestBuilder with a prompt and the Tool object +var builder = try ConverseRequestBuilder(with: model) + .withPrompt("What is the most popular song on WZPZ?") + .withTool(tool) + +// pass the ConverseRequestBuilder object to the converse function +var reply = try await bedrock.converse(with: builder) + +if let toolUse = try? reply.getToolUse() { + let id = toolUse.id + let name = toolUse.name + let input = toolUse.input + + // ... Logic to use the tool here ... + + // Send the toolResult back to the model + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withToolResult("The Best Song Ever") // pass any Codable or Data + + reply = try await bedrock.converse(with: builder) +} + +print("Assistant: \(reply)") +// The final reply will be similar to: "The most popular song currently played on WZPZ is \"The Best Song Ever\". If you need more information or have another request, feel free to ask!" +``` + +To use streaming use the exact same `ConverseRequestBuilder`, but use the `converseStream` function instead of the `converse` function. + +```swift +let bedrock = try await BedrockService(authentication: .sso()) +let model: BedrockModel = .claudev3_7_sonnet + +// define the inputschema for your tool +let schema = JSON(with: [ + "type": "object", + "properties": [ + "sign": [ + "type": "string", + "description": + "The call sign for the radio station for which you want the most popular song. Example calls signs are WZPZ, StuBru and Klara.", + ] + ], + "required": [ + "sign" + ], +]) + +// pass a prompt and the tool to converse +var builder = try ConverseRequestBuilder(with: model) + .withPrompt("Introduce yourself and mention the tools you have access to?") + .withTool( + name: "top_song", + inputSchema: schema, + description: "Get the most popular song played on a radio station." + ) + +var stream: AsyncThrowingStream +var assistantMessage: Message = Message("empty") + +// start a loop to interact with the user +while true { + var prompt: String = "" + var indexes: [Int] = [] + var toolRequests: [ToolUseBlock] = [] + + // create the stream by calling the converseStream function + stream = try await bedrock.converseStream(with: builder) + + // process the stream + for try await element in stream { + switch element { + case .contentSegment(let contentSegment): + switch contentSegment { + case .text(let index, let text): + if !indexes.contains(index) { + indexes.append(index) + print("\nAssistant: ") + } + print(text, terminator: "") + default: + break + } + case .contentBlockComplete(_, let content): + print("\n") + if case .toolUse(let toolUse) = content { + toolRequests.append(toolUse) + } + case .messageComplete(let message): + assistantMessage = message + } + } + + // if a request to use a tool was made by the model, use the information in the input to return the correct information back to the model in a ToolResultBlock + if !toolRequests.isEmpty { + for toolUse in toolRequests { + print("found tool use") + print(toolUse) + if toolUse.name == "top_song" { + let sign: String? = toolUse.input["sign"] + if let sign { + let song = try await getMostPopularSong(sign: sign) + builder = try ConverseRequestBuilder(from: builder, with: assistantMessage) + .withToolResult(song) + } + } + } + } else { + // if no request to use a tool was made, no ToolResultBlock needs to be returned and the user can ask the next question + print("\nYou: ") + prompt = readLine()! + if prompt == "done" { + break + } + + builder = try ConverseRequestBuilder(from: builder, with: assistantMessage) + .withPrompt(prompt) + } +} +``` + +### Reasoning + +To not only get a text reply but to also follow the model's reasoning, enable reasoning by using the `withReasoning` and optionally set the maximum length of the reasoning with `withMaxReasoningTokens`. These functions can be combined using the `withReasoning(maxReasoningTokens: Int)` function. + +```swift +let model: BedrockModel = .claudev3_7_sonnet + +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") +} + +var prompt = "Introduce yourself in one sentence" + +var builder = try ConverseRequestBuilder(with: model) + .withPrompt(prompt) + .withReasoning() + .withMaxReasoningTokens(1024) // Optional + +var reply = try await bedrock.converse(with: builder) + +if let reasoning = try? reply.getReasoningBlock() { + print("\nReasoning: \(reasoning.reasoning)") +} +print("\nAssistant: \(reply)") +``` + +To combine reasoning and streaming, use the same `ConverseRequestBuilder`, but use the `converseStream` function instead of the `converse` function. A `ContentSegment` can then contain `reasoning`. + +```swift +let model: BedrockModel = .claudev3_7_sonnet + +guard model.hasConverseModality() else { + throw MyError.incorrectModality("\(model.name) does not support converse") +} +guard model.hasConverseModality(.streaming) else { + throw MyError.incorrectModality("\(model.name) does not support streaming") +} +guard model.hasConverseModality(.reasoning) else { + throw MyError.incorrectModality("\(model.name) does not support reasoning") +} + +var builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Tell me more about the birds in Paris") + .withReasoning(maxReasoningTokens: 1024) + +let stream = try await bedrock.converseStream(with: builder) + +var indexes: [Int] = [] + +for try await element in stream { + switch element { + case .contentSegment(let contentSegment): + switch contentSegment { + case .text(let index, let text): + if !indexes.contains(index) { + indexes.append(index) + print("\nAssistant: ") + } + print(text, terminator: "") + case .reasoning(let index, let text, _): + if !indexes.contains(index) { + indexes.append(index) + print("\nReasoning: ") + } + print(text, terminator: "") + default: + break + } + case .contentBlockComplete: + print("\n\n") + case .messageComplete(let message): + assistantMessage = message + } +} + +builder = try ConverseRequestBuilder(from: builder, with: assistantMessage) + .withPrompt("And what about the rats?") +``` + +### Make your own `Message` + +Alternatively use the `converse` function that does not take a `prompt`, `toolResult` or `image` and construct the `Message` yourself. + +```swift +// Message with prompt +let replyMessage = try await bedrock.converse( + with: model, + conversation: [Message("What day of the week is it?")] +) + +// Optionally add inference parameters +let replyMessage = try await bedrock.converse( + with: model, + conversation: [Message("What day of the week is it?")], + maxTokens: 512, + temperature: 1, + topP: 0.8, + stopSequences: ["THE END"], + systemPrompts: ["Today is Wednesday, make sure to mention that."] +) + +// Message with an image and prompt +let replyMessage = try await bedrock.converse( + with: model, + conversation: [Message("What is in the this teacup?", imageFormat: .jpeg, imageBytes: base64EncodedImage)], +) + +// Message with toolResult +let replyMessage = try await bedrock.converse( + with: model, + conversation: [Message(toolResult)], + tools: [toolA, toolB] +) +``` + +### JSON + +The `JSON` struct is a lightweight and flexible wrapper for working with JSON-like data in Swift. It provides convenient methods and initializers to parse, access, and manipulate JSON data while maintaining type safety and versatility. + +#### Creating a JSON Object + +You can create a `JSON` object by wrapping raw values or constructing nested structures: +```swift +let json = JSON([ + "name": JSON("Jane Doe"), + "age": JSON(30), + "isMember": JSON(true), +]) +``` +#### Creating JSON object from String + +The `JSON` struct provides an initializer to parse valid JSON strings into a `JSON` object: + +```swift +let validJSONString = """ +{ + "name": "Jane Doe", + "age": 30, + "isMember": true +} +""" + +do { + let json = try JSON(from: validJSONString) + print(json.getValue("name") ?? "No name") // Output: Jane Doe +} catch { + print("Failed to parse JSON:", error) +} +``` + +#### Accessing values using `getValue` + +The `getValue(_ key: String)` method retrieves values of the specified type from the JSON object: + +```swift +if let name: String? = json.getValue("name") { + print("Name:", name) // Output: Name: Jane Doe +} + +if let age: Int? = json.getValue("age") { + print("Age:", age) // Output: Age: 30 +} + +if let isMember: Bool? = json.getValue("isMember") { + print("Is Member:", isMember) // Output: Is Member: true +} +``` + +#### Accessing values using subscripts + +You can also access values dynamically using subscripts: + +```swift +let name: String? = json["name"] +print("Name:", name ?? "Unknown") // Output: Name: Jane Doe + +let nonExistent: String? = json["nonExistentKey"] +print(nonExistent == nil) // Output: true +``` + +Note that the subscript method is also able to handle nested objects. + +```swift +let json = JSON([ + "name": JSON("Jane Doe"), + "age": JSON(30), + "isMember": JSON(true), + "address": JSON([ + "street": JSON("123 Main St"), + "city": JSON("Anytown"), + "postalCode": JSON(12345), + ]), +]) + +let street: String = json["address"]?["street"] +print("Street:", name ?? "Unknown") // Street: 123 Main St +``` + +## Generating an image using the InvokeModel API + +Choose a BedrockModel that supports image generation - you can verify this using the `hasImageModality` and the `hasTextToImageModality` function. The `generateImage` function allows you to create images from text descriptions with various optional parameters: + +- `prompt`: Text description of the desired image +- `negativePrompt`: Text describing what to avoid in the generated image +- `nrOfImages`: Number of images to generate +- `cfgScale`: Classifier free guidance scale to control how closely the image follows the prompt +- `seed`: Seed for reproducible image generation +- `quality`: Parameter to control the quality of generated images +- `resolution`: Desired image resolution for the generated images + +The function returns an ImageGenerationOutput object containing an array of generated images in base64 format. + +```swift +let model: BedrockModel = .nova_canvas + +guard model.hasImageModality(), + model.hasTextToImageModality() else { + throw MyError.incorrectModality("\(model.name) does not support image generation") +} + +let imageGeneration = try await bedrock.generateImage( + "A serene landscape with mountains at sunset", + with: model +) +``` + +Optionally add inference parameters. + +```swift +let imageGeneration = try await bedrock.generateImage( + "A serene landscape with mountains at sunset", + with: model, + negativePrompt: "dark, stormy, people", + nrOfImages: 3, + cfgScale: 7.0, + seed: 42, + quality: .standard, + resolution: ImageResolution(width: 100, height: 100) +) +``` + +Note that the minimum, maximum and default values for each parameter are model specific and defined when the BedrockModel is created. Some parameters might not be supported by certain models. + +## Generating image variations using the InvokeModel API +Choose a BedrockModel that supports image variations - you can verify this using the `hasImageModality` and the `hasImageVariationModality` function. The `generateImageVariation` function allows you to create variations of an existing image with these parameters: + +- `images`: The base64-encoded source images used to create variations from +- `negativePrompt`: Text describing what to avoid in the generated image +- `similarity`: Controls how similar the variations will be to the source images +- `nrOfImages`: Number of variations to generate +- `cfgScale`: Classifier free guidance scale to control how closely variations follow the original image +- `seed`: Seed for reproducible variation generation +- `quality`: Parameter to control the quality of generated variations +- `resolution`: Desired resolution for the output variations + +This function returns an `ImageGenerationOutput` object containing an array of generated image variations in base64 format. Each variation will maintain key characteristics of the source images while introducing creative differences. + +```swift +let model: BedrockModel = .nova_canvas + +guard model.hasImageVariationModality(), + model.hasImageVariationModality() else { + throw MyError.incorrectModality("\(model.name) does not support image variation generation") +} + +let imageVariations = try await bedrock.generateImageVariation( + images: [base64EncodedImage], + prompt: "A dog drinking out of this teacup", + with: model +) +``` + +Optionally add inference parameters. + +```swift +let imageVariations = try await bedrock.generateImageVariation( + images: [base64EncodedImage], + prompt: "A dog drinking out of this teacup", + with: model, + negativePrompt: "Cats, worms, rain", + similarity: 0.8, + nrOfVariations: 4, + cfgScale: 7.0, + seed: 42, + quality: .standard, + resolution: ImageResolution(width: 100, height: 100) +) +``` + +Note that the minimum, maximum and default values for each parameter are model specific and defined when the BedrockModel is created. Some parameters might not be supported by certain models. + +## Generating text using the InvokeModel API + +Choose a BedrockModel that supports text generation, you can verify this using the `hasTextModality` function. When calling the `completeText` function you can provide some inference parameters: + +- `maxTokens`: The maximum amount of tokens that the model is allowed to return +- `temperature`: Controls the randomness of the model's output +- `topP`: Nucleus sampling, this parameter controls the cumulative probability threshold for token selection +- `topK`: Limits the number of tokens the model considers for each step of text generation to the K most likely ones +- `stopSequences`: An array of strings that will cause the model to stop generating further text when encountered + +The function returns a `TextCompletion` object containing the generated text. + +```swift +let model: BedrockModel = .nova_micro + +guard model.hasTextModality() else { + throw MyError.incorrectModality("\(model.name) does not support text generation") +} + +let textCompletion = try await bedrock.completeText( + "Write a story about a space adventure", + with: model +) + +print(textCompletion.completion) +``` + +Optionally add inference parameters. + +```swift +let textCompletion = try await bedrock.completeText( + "Write a story about a space adventure", + with: model, + maxTokens: 1000, + temperature: 0.7, + topP: 0.9, + topK: 250, + stopSequences: ["THE END"] +) +``` + +Note that the minimum, maximum and default values for each parameter are model specific and defined when the BedrockModel is created. Some parameters might not be supported by certain models. + +## How to add a BedrockModel + +### Converse + +To add a new model that only needs the ConverseModality, simply use the `StandardConverse` and add the correct [inference parameters](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html) and [supported converse features](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html). + +```swift +extension BedrockModel { + public static let new_bedrock_model = BedrockModel( + id: "family.model-id-v1:0", + name: "New Model Name", + modality: StandardConverse( + parameters: ConverseParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.3), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: nil, defaultValue: nil), + topP: Parameter(.topP, minValue: 0.01, maxValue: 0.99, defaultValue: 0.75), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document, .toolUse] + ) + ) +} +``` + +If the model also implements other modalities you might need to create you own `Modality` and make sure it conforms to `ConverseModality` by implementing the `getConverseParameters` and `getConverseFeatures` functions. Note that the `ConverseParameters` can be extracted from `TextGenerationParameters` by using the public initializer. + +```swift +struct ModelFamilyModality: TextModality, ConverseModality { + func getName() -> String { "Model Family Text and Converse Modality" } + + let parameters: TextGenerationParameters + let converseFeatures: [ConverseFeature] + let converseParameters: ConverseParameters + + init(parameters: TextGenerationParameters, features: [ConverseFeature] = [.textGeneration]) { + self.parameters = parameters + self.converseFeatures = features + + // public initializer to extract `ConverseParameters` from `TextGenerationParameters` + self.converseParameters = ConverseParameters(textGenerationParameters: parameters) + } + + // ... +} +``` + +### Text + +If you need to add a model from a model family that is not supported at all by the library, follow these steps: + +#### Step 1: Create family-specific request and response struct + +Make sure to create a struct that reflects exactly how the body of the request for an invokeModel call to this family should look. Make sure to add the public initializer with parameters `prompt`, `maxTokens` and `temperature` to comply to the `BedrockBodyCodable` protocol. Take a look at the documentation to apply best practices or specific formatting. + +```json +{ + "prompt": "\(prompt)", + "temperature": 1, + "top_p": 0.9, + "max_tokens": 200, + "stop": ["END"] +} +``` + +```swift +public struct LlamaRequestBody: BedrockBodyCodable { + let prompt: String + let max_gen_len: Int + let temperature: Double + let top_p: Double + + public init(prompt: String, maxTokens: Int = 512, temperature: Double = 0.5) { + self.prompt = + "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\(prompt)<|eot_id|><|start_header_id|>assistant<|end_header_id|>" + self.max_gen_len = maxTokens + self.temperature = temperature + self.top_p = 0.9 + } +} +``` + +Do the same for the response and ensure to add the `getTextCompletion` method to extract the completion from the response body and to comply to the `ContainsTextCompletion` protocol. + +```json +{ + "generation": "\n\n", + "prompt_token_count": int, + "generation_token_count": int, + "stop_reason" : string +} +``` + +```swift +struct LlamaResponseBody: ContainsTextCompletion { + let generation: String + let prompt_token_count: Int + let generation_token_count: Int + let stop_reason: String + + public func getTextCompletion() throws -> TextCompletion { + TextCompletion(generation) + } +} +``` + +#### Step 2: Create the Modality + +For a text generation create a struct conforming to TextModality. Use the request body and response body you created in [the previous step](#step-1-create-family-specific-request-and-response-struct). Make sure to check for model(family) specific rules or parameters that are not supported here. + +```swift +struct LlamaText: TextModality { + let parameters: TextGenerationParameters + + init(parameters: TextGenerationParameters) { + self.parameters = parameters + } + + func getName() -> String { "Llama Text Generation" } + + func getParameters() -> TextGenerationParameters { + parameters + } + + func getTextRequestBody( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws -> BedrockBodyCodable { + guard topK == nil else { + throw BedrockServiceError.notSupported("TopK is not supported for Llama text completion") + } + guard stopSequences == nil else { + throw BedrockServiceError.notSupported("stopSequences is not supported for Llama text completion") + } + return LlamaRequestBody( + prompt: prompt, + maxTokens: maxTokens ?? parameters.maxTokens.defaultValue, + temperature: temperature ?? parameters.temperature.defaultValue, + topP: topP ?? parameters.topP.defaultValue + ) + } + + func getTextResponseBody(from data: Data) throws -> ContainsTextCompletion { + let decoder = JSONDecoder() + return try decoder.decode(LlamaResponseBody.self, from: data) + } +} +``` + +#### Step 3: Create BedrockModel instance + +You can now create instances for any of the models that follow the request and response structure you defined. Make sure to check the allowed and default values for the inference parameters, especially if some parameters are not supported by the model. Know that these parameters may differ significantly for models from the same family. + +```swift +extension BedrockModel { + public static let llama3_3_70b_instruct: BedrockModel = BedrockModel( + id: "us.meta.llama3-3-70b-instruct-v1:0", + name: "Llama 3.3 70B Instruct", + modality: LlamaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 2_048, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams.notSupported(), + maxPromptSize: nil + ) + ) + ) +} +``` + +### Image + +To add an image generation model from a model family that is not supported at all by the library, the steps are much alike to the text completion models. + +#### Step 1: Create family-specific request and response struct + +Make sure to create a struct that reflects exactly how the body of the request for an invokeModel call to this family should look. Take a look at the documentation to apply best practices or specific formatting. + +```swift +public struct AmazonImageRequestBody: BedrockBodyCodable { + let taskType: TaskType + private let textToImageParams: TextToImageParams? + private let imageGenerationConfig: ImageGenerationConfig + + // MARK: - Initialization + + /// Creates a text-to-image generation request body + /// - Parameters: + /// - prompt: The text description of the image to generate + /// - nrOfImages: The number of images to generate + /// - negativeText: The text description of what to exclude from the generated image + /// - Returns: A configured AmazonImageRequestBody for text-to-image generation + public static func textToImage( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) -> Self { + AmazonImageRequestBody( + prompt: prompt, + negativeText: negativeText, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + private init( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) { + self.taskType = .textToImage + self.textToImageParams = TextToImageParams.textToImage(prompt: prompt, negativeText: negativeText) + self.imageGenerationConfig = ImageGenerationConfig( + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } +} +``` + +Do the same for the response and ensure to add the `getGeneratedImage` method to extract the image from the response body and to comply to the `ContainsImageGeneration` protocol. + +```swift +public struct AmazonImageResponseBody: ContainsImageGeneration { + let images: [Data] + + public func getGeneratedImage() -> ImageGenerationOutput { + ImageGenerationOutput(images: images) + } +} +``` + +#### Step 2: Create the Modality + +Determine the exact functionality and make sure to comply to the correct modality protocol. In this case we will use `TextToImageModality`. +Create a struct conforming to `ImageModality` and the specific functionality protocol. Use the request body and response body you created in [the previous step](#step-1-create-family-specific-request-and-response-struct). Make sure to check for model(family) specific rules or parameters that are not supported here. + +```swift +struct AmazonImage: ImageModality, TextToImageModality { + func getName() -> String { "Amazon Image Generation" } + + let parameters: ImageGenerationParameters + let resolutionValidator: any ImageResolutionValidator + let textToImageParameters: TextToImageParameters + + init( + parameters: ImageGenerationParameters, + resolutionValidator: any ImageResolutionValidator, + textToImageParameters: TextToImageParameters + ) { + self.parameters = parameters + self.textToImageParameters = textToImageParameters + self.conditionedTextToImageParameters = conditionedTextToImageParameters + self.imageVariationParameters = imageVariationParameters + self.resolutionValidator = resolutionValidator + } + + func getParameters() -> ImageGenerationParameters { parameters } + func getTextToImageParameters() -> TextToImageParameters { textToImageParameters } + + func validateResolution(_ resolution: ImageResolution) throws { + try resolutionValidator.validateResolution(resolution) + } + + func getImageResponseBody(from data: Data) throws -> ContainsImageGeneration { + let decoder = JSONDecoder() + return try decoder.decode(AmazonImageResponseBody.self, from: data) + } + + func getTextToImageRequestBody( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> BedrockBodyCodable { + AmazonImageRequestBody.textToImage( + prompt: prompt, + negativeText: negativeText, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } +} +``` + +#### Step 3: Create BedrockModel instance + +You can now create instances for any of the models that follow the request and response structure you defined. Make sure to check the allowed and default values for the inference parameters, especially if some parameters are not supported by the model. Know that these parameters may differ significantly for models from the same family. + +```swift +extension BedrockModel { + public static let nova_canvas: BedrockModel = BedrockModel( + id: "amazon.nova-canvas-v1:0", + name: "Nova Canvas", + modality: AmazonImage( + parameters: ImageGenerationParameters( + nrOfImages: Parameter(.nrOfImages, minValue: 1, maxValue: 5, defaultValue: 1), + cfgScale: Parameter(.cfgScale, minValue: 1.1, maxValue: 10, defaultValue: 6.5), + seed: Parameter(.seed, minValue: 0, maxValue: 858_993_459, defaultValue: 12) + ), + resolutionValidator: NovaImageResolutionValidator(), + textToImageParameters: TextToImageParameters(maxPromptSize: 1024, maxNegativePromptSize: 1024), + ) + ) +} +``` \ No newline at end of file diff --git a/swift-bedrock-library/Sources/BedrockService/BedrockAuthentication+JWT.swift b/swift-bedrock-library/Sources/BedrockService/BedrockAuthentication+JWT.swift new file mode 100644 index 00000000..eb649ac0 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/BedrockAuthentication+JWT.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSSDKIdentity +import BedrockTypes +import Logging + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension BedrockAuthentication { + /// Convert the given JWT identity token string into temporary AWS credentials + /// using the STSWebIdentityAWSCredentialIdentityResolver + /// + /// - Parameters: + /// - tokenString: The string version of the JWT identity token + /// returned by Sign In With Apple. + /// - roleARN: The ARN of the IAM role to assume. + /// - region: An optional string specifying the AWS Region to + /// access. If not specified, "us-east-1" is assumed. + /// - notify: A closure to be called on the main thread when the credentials are retrieved. + func webIdentityCredentialResolver( + withWebIdentity tokenString: String, + logger: Logger, + roleARN: String, + region: Region = .useast1, + notify: @Sendable @escaping () -> Void + ) async throws -> STSWebIdentityAWSCredentialIdentityResolver { + // Write the token to a temporary file so it can be used by the resolver + let tokenFilename = "apple-identity-token.jwt" + let tokenFileURL = FileManager.default.temporaryDirectory.appendingPathComponent( + tokenFilename, + isDirectory: false + ) + let tokenFilePath = tokenFileURL.path + defer { + // silently ignore an error if the file does not exist + try? FileManager.default.removeItem(at: tokenFileURL) + } + + guard (try? tokenString.write(to: tokenFileURL, atomically: true, encoding: .utf8)) != nil else { + throw BedrockServiceError.authenticationFailed("Failed to write token to file") + } + + // Create an identity resolver that uses the JWT token received from an Identity Provider + // to create AWS credentials + do { + logger.trace("Creating identity resolver using web identity token") + let identityResolver = try STSWebIdentityAWSCredentialIdentityResolver( + region: region.rawValue, + roleArn: roleARN, + roleSessionName: "SwiftBedrockService-\(UUID().uuidString)", + tokenFilePath: tokenFilePath + ) + + // Test the resolver by retrieving credentials to ensure it works + logger.trace("Retrieving credentials using web identity token") + _ = try await identityResolver.crtAWSCredentialIdentityResolver.getCredentials() + logger.trace("Successfully retrieved credentials using web identity token") + + // Notify observers, if any + logger.trace("Notifying observers of credentials update") + await MainActor.run { + notify() + } + + return identityResolver + + } catch { + logger.error("Failed to assume role using web identity token: \(error)") + throw BedrockServiceError.authenticationFailed( + "Failed to assume role using web identity token: \(error.localizedDescription)" + ) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/BedrockAuthentication.swift b/swift-bedrock-library/Sources/BedrockService/BedrockAuthentication.swift new file mode 100644 index 00000000..115382a8 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/BedrockAuthentication.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSSDKIdentity +import BedrockTypes +import Logging +import SmithyIdentity + +/// Represent the authentication type for the Bedrock service +/// - `default`: Use the default AWS credential provider chain (see https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html) +/// - `profile`: Use a specific profile name from the AWS credentials file. This works for application that runs on machines with AWS CLI configured, such as a server or your laptop. The application must not be sandboxed and have access to the credentials file. +/// - `sso`: Use SSO authentication with a specific profile name.`aws sso --profile login` must be done before running the application. This works for application that runs on machines with AWS CLI configured, such as a server or your laptop. The application must not be sandboxed and have access to the credentials file. +/// - `webIdentity`: Use a web identity token (JWT) to assume an IAM role. This is useful for applications running on iOS, tvOS or macOS where you cannot use the AWS CLI. Typically, the application authenticates the user with an external Identity provider (such as Sign In with Apple or Login With Google) and receives a JWT token. The application then uses this token to assume an IAM role and receive temporary AWS credentials. Some additional configuration is required on your AWS account to allow this. See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html for more information. If you use Sign In With Apple, read https://docs.aws.amazon.com/sdk-for-swift/latest/developer-guide/apple-integration.html for more information. +/// Because `webidentity` is often used by application presenting a user interface. This method of authentication allows you to pass an optional closure that will be called when the credentials are retrieved. This is useful for updating the UI or notifying the user. The closure is called on the main (UI) thread. +/// - `static`: Use static AWS credentials. We strongly recommend to not use this option in production. This might be useful in some rare cases when testing and debugging. +public enum BedrockAuthentication: Sendable, CustomStringConvertible { + case `default` + case profile(profileName: String = "default") + case sso(profileName: String = "default") + case webIdentity(token: String, roleARN: String, region: Region, notification: @Sendable () -> Void = {}) + case `static`(accessKey: String, secretKey: String, sessionToken: String) + + public var description: String { + switch self { + case .default: + return "default" + case .profile(let profileName): + return "profile: \(profileName)" + case .sso(let profileName): + return "sso: \(profileName)" + case .webIdentity(let token, let roleARN, let region, _): + return "webIdentity: \(redactingSecret(secret: token)), roleARN: \(roleARN), region: \(region)" + case .static(let accessKey, let secretKey, _): + return "static: \(accessKey), secretKey: \(redactingSecret(secret: secretKey))" + } + } + private func redactingSecret(secret: String) -> String { + "\(secret.prefix(min(3, secret.count)))... *** shuuut, it's a secret ***" + } + + /// Creates an AWS credential identity resolver depending on the authentication parameter. + /// - Parameters: + /// - authentication: The authentication type to use + /// - Returns: An optional AWS credential identity resolver. A nil return value means that the default AWS credential provider chain will be used. + /// + func getAWSCredentialIdentityResolver( + logger: Logger + ) async throws -> (any SmithyIdentity.AWSCredentialIdentityResolver)? { + + switch self { + case .default: + return nil + case .profile(let profileName): + return try? ProfileAWSCredentialIdentityResolver(profileName: profileName) + case .sso(let profileName): + return try? SSOAWSCredentialIdentityResolver(profileName: profileName) + case .webIdentity(let token, let roleARN, let region, let notification): + return try await webIdentityCredentialResolver( + withWebIdentity: token, + logger: logger, + roleARN: roleARN, + region: region, + notify: notification + ) + case .static(let accessKey, let secretKey, let sessionToken): + logger.warning("Using static AWS credentials. This is not recommended for production.") + let creds = AWSCredentialIdentity(accessKey: accessKey, secret: secretKey, sessionToken: sessionToken) + return try StaticAWSCredentialIdentityResolver(creds) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/BedrockService.swift b/swift-bedrock-library/Sources/BedrockService/BedrockService.swift new file mode 100644 index 00000000..07f5244a --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/BedrockService.swift @@ -0,0 +1,213 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrock +@preconcurrency import AWSBedrockRuntime +import AWSClientRuntime +import AwsCommonRuntimeKit +import BedrockTypes +import Foundation +import Logging + +public struct BedrockService: Sendable { + package let region: Region + package let logger: Logging.Logger + package let bedrockClient: BedrockClientProtocol + package let bedrockRuntimeClient: BedrockRuntimeClientProtocol + + // MARK: - Initialization + + /// Initializes a new SwiftBedrock instance + /// - Parameters: + /// - region: The AWS region to use (defaults to .useast1) + /// - logger: Optional custom logger instance + /// - bedrockClient: Optional custom Bedrock client + /// - bedrockRuntimeClient: Optional custom Bedrock Runtime client + /// - authentication: The authentication type to use (defaults to .default) + /// - Throws: Error if client initialization fails + public init( + region: Region = .useast1, + logger: Logging.Logger? = nil, + bedrockClient: BedrockClientProtocol? = nil, + bedrockRuntimeClient: BedrockRuntimeClientProtocol? = nil, + authentication: BedrockAuthentication = .default + ) async throws { + self.logger = logger ?? BedrockService.createLogger("bedrock.service") + self.logger.trace( + "Initializing SwiftBedrock", + metadata: ["region": .string(region.rawValue)] + ) + self.region = region + + if bedrockClient != nil { + self.logger.trace("Using supplied bedrockClient") + self.bedrockClient = bedrockClient! + } else { + self.logger.trace("Creating bedrockClient") + self.bedrockClient = try await BedrockService.createBedrockClient( + region: region, + authentication: authentication, + logger: self.logger + ) + self.logger.trace( + "Created bedrockClient", + metadata: ["authentication type": "\(authentication)"] + ) + } + if bedrockRuntimeClient != nil { + self.logger.trace("Using supplied bedrockRuntimeClient") + self.bedrockRuntimeClient = bedrockRuntimeClient! + } else { + self.logger.trace("Creating bedrockRuntimeClient") + self.bedrockRuntimeClient = try await BedrockService.createBedrockRuntimeClient( + region: region, + authentication: authentication, + logger: self.logger + ) + self.logger.trace( + "Created bedrockRuntimeClient", + metadata: ["authentication type": "\(authentication)"] + ) + } + self.logger.trace( + "Initialized SwiftBedrock", + metadata: ["region": .string(region.rawValue)] + ) + } + + // MARK: - Private Helpers + + /// Creates Logger using either the loglevel saved as environment variable `BEDROCK_SERVICE_LOG_LEVEL` or with default `.info` + /// - Parameter name: The name/label for the logger + /// - Returns: Configured Logger instance + static private func createLogger(_ name: String) -> Logging.Logger { + var logger: Logging.Logger = Logger(label: name) + logger.logLevel = + ProcessInfo.processInfo.environment["BEDROCK_SERVICE_LOG_LEVEL"].flatMap { + Logger.Level(rawValue: $0.lowercased()) + } ?? .info + return logger + } + + /// Creates a BedrockClient + /// - Parameters: + /// - region: The AWS region to configure the client for + /// - authentication: The authentication type to use + /// - Returns: Configured BedrockClientProtocol instance + /// - Throws: Error if client creation fails + static private func createBedrockClient( + region: Region, + authentication: BedrockAuthentication, + logger: Logging.Logger + ) async throws + -> BedrockClientProtocol + { + let config = try await BedrockClient.BedrockClientConfiguration( + region: region.rawValue + ) + if let awsCredentialIdentityResolver = try? await authentication.getAWSCredentialIdentityResolver( + logger: logger + ) { + config.awsCredentialIdentityResolver = awsCredentialIdentityResolver + } + return BedrockClient(config: config) + } + + /// Creates a BedrockRuntimeClient + /// - Parameters: + /// - region: The AWS region to configure the client for + /// - authentication: The authentication type to use + /// - Returns: Configured BedrockRuntimeClientProtocol instance + /// - Throws: Error if client creation fails + static private func createBedrockRuntimeClient( + region: Region, + authentication: BedrockAuthentication, + logger: Logging.Logger + ) + async throws + -> BedrockRuntimeClientProtocol + { + let config = + try await BedrockRuntimeClient.BedrockRuntimeClientConfiguration( + region: region.rawValue + ) + if let awsCredentialIdentityResolver = try? await authentication.getAWSCredentialIdentityResolver( + logger: logger + ) { + config.awsCredentialIdentityResolver = awsCredentialIdentityResolver + } + return BedrockRuntimeClient(config: config) + } + + func handleCommonError(_ error: Error, context: String) throws -> Never { + if let commonError = error as? CommonRunTimeError { + logger.trace("CommonRunTimeError while \(context)", metadata: ["error": "\(error)"]) + switch commonError { + case .crtError(let crtError): + switch crtError.code { + case 6153: + throw BedrockServiceError.authenticationFailed( + "No valid credentials found: \(crtError.message)" + ) + case 6170: + throw BedrockServiceError.authenticationFailed( + "AWS SSO token expired: \(crtError.message)" + ) + default: + throw BedrockServiceError.authenticationFailed( + "Authentication failed: \(crtError.message)" + ) + } + } + } else { + logger.trace("Error while \(context)", metadata: ["error": "\(error)"]) + throw error + } + } + + // MARK: Public Methods + + /// Lists all available foundation models from Amazon Bedrock + /// - Throws: BedrockServiceError.invalidResponse + /// - Returns: An array of ModelSummary objects containing details about each available model + public func listModels() async throws -> [ModelSummary] { + logger.trace("Fetching foundation models") + do { + let response = try await bedrockClient.listFoundationModels( + input: ListFoundationModelsInput() + ) + guard let models = response.modelSummaries else { + logger.trace("Failed to extract modelSummaries from response") + throw BedrockServiceError.invalidSDKResponse( + "Something went wrong while extracting the modelSummaries from the response." + ) + } + var modelsInfo: [ModelSummary] = [] + modelsInfo = try models.compactMap { (sdkModelSummary) -> ModelSummary? in + try ModelSummary.getModelSummary(from: sdkModelSummary) + } + logger.trace( + "Fetched foundation models", + metadata: [ + "models.count": "\(modelsInfo.count)", + "models.content": .string(String(describing: modelsInfo)), + ] + ) + return modelsInfo + } catch { + try handleCommonError(error, context: "listing foundation models") + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/Converse/BedrockService+Converse.swift b/swift-bedrock-library/Sources/BedrockService/Converse/BedrockService+Converse.swift new file mode 100644 index 00000000..30a66859 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/Converse/BedrockService+Converse.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import AwsCommonRuntimeKit +import BedrockTypes +import Foundation + +extension BedrockService { + + /// Converse with a model using the Bedrock Converse API + /// - Parameters: + /// - model: The BedrockModel to converse with + /// - conversation: Array of previous messages in the conversation + /// - maxTokens: Optional maximum number of tokens to generate + /// - temperature: Optional temperature parameter for controlling randomness + /// - topP: Optional top-p parameter for nucleus sampling + /// - stopSequences: Optional array of sequences where generation should stop + /// - systemPrompts: Optional array of system prompts to guide the conversation + /// - tools: Optional array of tools the model can use + /// - Throws: BedrockServiceError.notSupported for parameters or functionalities that are not supported + /// BedrockServiceError.invalidParameter for invalid parameters + /// BedrockServiceError.invalidPrompt if the prompt is empty or too long + /// BedrockServiceError.invalidModality for invalid modality from the selected model + /// BedrockServiceError.invalidSDKResponse if the response body is missing + /// - Returns: A Message containing the model's response + public func converse( + with model: BedrockModel, + conversation: [Message], + maxTokens: Int? = nil, + temperature: Double? = nil, + topP: Double? = nil, + stopSequences: [String]? = nil, + systemPrompts: [String]? = nil, + tools: [Tool]? = nil, + enableReasoning: Bool? = false, + maxReasoningTokens: Int? = nil + ) async throws -> Message { + do { + let modality = try model.getConverseModality() + let parameters = modality.getConverseParameters() + try parameters.validate( + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + stopSequences: stopSequences + ) + + logger.trace( + "Creating ConverseRequest", + metadata: [ + "model.name": "\(model.name)", + "model.id": "\(model.id)", + "conversation.count": "\(conversation.count)", + "maxToken": "\(String(describing: maxTokens))", + "temperature": "\(String(describing: temperature))", + "topP": "\(String(describing: topP))", + "stopSequences": "\(String(describing: stopSequences))", + "systemPrompts": "\(String(describing: systemPrompts))", + "tools": "\(String(describing: tools))", + ] + ) + let converseRequest = ConverseRequest( + model: model, + messages: conversation, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + stopSequences: stopSequences, + systemPrompts: systemPrompts, + tools: tools, + maxReasoningTokens: maxReasoningTokens + ) + + logger.trace("Creating ConverseInput") + let input = try converseRequest.getConverseInput() + + logger.trace( + "Sending ConverseInput to BedrockRuntimeClient", + metadata: [ + "input.messages.count": "\(String(describing:input.messages!.count))", + "input.modelId": "\(String(describing:input.modelId!))", + ] + ) + let response: ConverseOutput = try await self.bedrockRuntimeClient.converse(input: input) + + logger.trace("Received response", metadata: ["response": "\(response)"]) + return try Message(response) + } catch { + try handleCommonError(error, context: "invoking converse") + throw BedrockServiceError.unknownError("\(error)") // FIXME: handleCommonError will always throw + } + } + + /// Use Converse API with the ConverseRequestBuilder + /// - Parameters: + /// - builder: ConverseRequestBuilder object + /// - Throws: BedrockServiceError.invalidSDKResponse if the response body is missing + /// - Returns: A ConverseReply object + public func converse(with builder: ConverseRequestBuilder) async throws -> ConverseReply { + logger.trace("Conversing") + do { + var history = builder.history + let userMessage = try builder.getUserMessage() + history.append(userMessage) + let assistantMessage: Message = try await converse( + with: builder.model, + conversation: history, + maxTokens: builder.maxTokens, + temperature: builder.temperature, + topP: builder.topP, + stopSequences: builder.stopSequences, + systemPrompts: builder.systemPrompts, + tools: builder.tools, + maxReasoningTokens: builder.maxReasoningTokens + ) + history.append(assistantMessage) + logger.trace( + "Received message", + metadata: ["replyMessage": "\(assistantMessage)", "history.count": "\(history.count)"] + ) + return try ConverseReply(history) + } catch { + logger.trace("Error while conversing", metadata: ["error": "\(error)"]) + throw error + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/Converse/BedrockService+ConverseStreaming.swift b/swift-bedrock-library/Sources/BedrockService/Converse/BedrockService+ConverseStreaming.swift new file mode 100644 index 00000000..7ac30b65 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/Converse/BedrockService+ConverseStreaming.swift @@ -0,0 +1,170 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import AwsCommonRuntimeKit +import BedrockTypes +import Foundation + +extension BedrockService { + + /// Converse with a model using the Bedrock Converse Streaming API + /// - Parameters: + /// - model: The BedrockModel to converse with + /// - conversation: Array of previous messages in the conversation + /// - maxTokens: Optional maximum number of tokens to generate + /// - temperature: Optional temperature parameter for controlling randomness + /// - topP: Optional top-p parameter for nucleus sampling + /// - stopSequences: Optional array of sequences where generation should stop + /// - systemPrompts: Optional array of system prompts to guide the conversation + /// - tools: Optional array of tools the model can use + /// - Throws: BedrockServiceError.notSupported for parameters or functionalities that are not supported + /// BedrockServiceError.invalidParameter for invalid parameters + /// BedrockServiceError.invalidPrompt if the prompt is empty or too long + /// BedrockServiceError.invalidModality for invalid modality from the selected model + /// BedrockServiceError.invalidSDKResponse if the response body is missing + /// - Returns: A stream of ConverseResponseStreaming objects + public func converseStream( + with model: BedrockModel, + conversation: [Message], + maxTokens: Int? = nil, + temperature: Double? = nil, + topP: Double? = nil, + stopSequences: [String]? = nil, + systemPrompts: [String]? = nil, + tools: [Tool]? = nil, + enableReasoning: Bool? = false, + maxReasoningTokens: Int? = nil + ) async throws -> AsyncThrowingStream { + do { + guard model.hasConverseStreamingModality() else { + throw BedrockServiceError.invalidModality( + model, + try model.getConverseModality(), + "This model does not support converse streaming." + ) + } + let modality = try model.getConverseModality() + let parameters = modality.getConverseParameters() + try parameters.validate( + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + stopSequences: stopSequences + ) + + logger.trace( + "Creating ConverseStreamingRequest", + metadata: [ + "model.name": "\(model.name)", + "model.id": "\(model.id)", + "conversation.count": "\(conversation.count)", + "maxToken": "\(String(describing: maxTokens))", + "temperature": "\(String(describing: temperature))", + "topP": "\(String(describing: topP))", + "stopSequences": "\(String(describing: stopSequences))", + "systemPrompts": "\(String(describing: systemPrompts))", + "tools": "\(String(describing: tools))", + ] + ) + let converseRequest = ConverseStreamingRequest( + model: model, + messages: conversation, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + stopSequences: stopSequences, + systemPrompts: systemPrompts, + tools: tools, + maxReasoningTokens: maxReasoningTokens + ) + + logger.trace("Creating ConverseStreamingInput") + let input = try converseRequest.getConverseStreamingInput() + + logger.trace( + "Sending ConverseStreaminInput to BedrockRuntimeClient", + metadata: [ + "input.messages.count": "\(String(describing:input.messages!.count))", + "input.modelId": "\(String(describing:input.modelId!))", + ] + ) + let response: ConverseStreamOutput = try await self.bedrockRuntimeClient.converseStream(input: input) + + logger.trace("Received response", metadata: ["response": "\(response)"]) + + guard let sdkStream = response.stream else { + throw BedrockServiceError.invalidSDKResponse( + "The response stream is missing. This error should never happen." + ) + } + // at this time, we have a stream. The stream is a message, with multiple content blocks + // - message start + // - message content start + // - message content delta + // - message content end + // - message stop + // - 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) + + // 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 currently only 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 + // - message complete: this includes the complete Message, ready to be added to the history and used for future requests + + return reply.stream + + } catch { + try handleCommonError(error, context: "invoking converse stream") + } + } + + /// Use Converse Stream API with the ConverseBuilder + /// - Parameters: + /// - builder: ConverseBuilder object + /// - Throws: BedrockServiceError.invalidSDKResponse if the response body is missing + /// - Returns: A stream of ConverseResponseStreaming objects + public func converseStream( + with builder: ConverseRequestBuilder + ) async throws -> AsyncThrowingStream { + logger.trace("Conversing and streaming") + do { + var history = builder.history + let userMessage = try builder.getUserMessage() + history.append(userMessage) + let streamingResponse = try await converseStream( + with: builder.model, + conversation: history, + maxTokens: builder.maxTokens, + temperature: builder.temperature, + topP: builder.topP, + stopSequences: builder.stopSequences, + systemPrompts: builder.systemPrompts, + tools: builder.tools, + maxReasoningTokens: builder.maxReasoningTokens + ) + return streamingResponse + } catch { + logger.trace("Error while conversing", metadata: ["error": "\(error)"]) + throw error + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequest.swift b/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequest.swift new file mode 100644 index 00000000..1552f002 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequest.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import BedrockTypes +import Foundation +import Smithy + +public struct ConverseRequest { + let model: BedrockModel + let messages: [Message] + let inferenceConfig: InferenceConfig? + let toolConfig: ToolConfig? + let systemPrompts: [String]? + let maxReasoningTokens: Int? + + init( + model: BedrockModel, + messages: [Message] = [], + maxTokens: Int?, + temperature: Double?, + topP: Double?, + stopSequences: [String]?, + systemPrompts: [String]?, + tools: [Tool]?, + maxReasoningTokens: Int? + ) { + self.messages = messages + self.model = model + self.inferenceConfig = InferenceConfig( + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + stopSequences: stopSequences + ) + self.systemPrompts = systemPrompts + self.maxReasoningTokens = maxReasoningTokens + if let tools { + self.toolConfig = ToolConfig(tools: tools) + } else { + self.toolConfig = nil + } + } + + func getConverseInput() throws -> ConverseInput { + ConverseInput( + additionalModelRequestFields: try getAdditionalModelRequestFields(), + inferenceConfig: inferenceConfig?.getSDKInferenceConfig(), + messages: try getSDKMessages(), + modelId: model.id, + system: getSDKSystemPrompts(), + toolConfig: try toolConfig?.getSDKToolConfig() + ) + } + + func getAdditionalModelRequestFields() throws -> Smithy.Document? { + if model == .claudev3_7_sonnet, let maxReasoningTokens { + let reasoningConfigJSON = JSON(with: [ + "thinking": [ + "type": "enabled", + "budget_tokens": maxReasoningTokens, + ] + ]) + return try reasoningConfigJSON.toDocument() + } + return nil + } + + func getSDKMessages() throws -> [BedrockRuntimeClientTypes.Message] { + try messages.map { try $0.getSDKMessage() } + } + + func getSDKSystemPrompts() -> [BedrockRuntimeClientTypes.SystemContentBlock]? { + systemPrompts?.map { + BedrockRuntimeClientTypes.SystemContentBlock.text($0) + } + } + + struct InferenceConfig { + let maxTokens: Int? + let temperature: Double? + let topP: Double? + let stopSequences: [String]? + + func getSDKInferenceConfig() -> BedrockRuntimeClientTypes.InferenceConfiguration { + let temperatureFloat: Float? + if temperature != nil { + temperatureFloat = Float(temperature!) + } else { + temperatureFloat = nil + } + let topPFloat: Float? + if topP != nil { + topPFloat = Float(topP!) + } else { + topPFloat = nil + } + return BedrockRuntimeClientTypes.InferenceConfiguration( + maxTokens: maxTokens, + stopSequences: stopSequences, + temperature: temperatureFloat, + topp: topPFloat + ) + } + } + + public struct ToolConfig { + // let toolChoice: ToolChoice? + let tools: [Tool] + + func getSDKToolConfig() throws -> BedrockRuntimeClientTypes.ToolConfiguration { + BedrockRuntimeClientTypes.ToolConfiguration( + tools: try tools.map { .toolspec(try $0.getSDKToolSpecification()) } + ) + } + } +} + +// public enum ToolChoice { +// /// (Default). The Model automatically decides if a tool should be called or whether to generate text instead. +// case auto(_) +// /// The model must request at least one tool (no text is generated). +// case any(_) +// /// The Model must request the specified tool. Only supported by Anthropic Claude 3 models. +// case tool(String) +// } diff --git a/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequestBuilder.swift b/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequestBuilder.swift new file mode 100644 index 00000000..e226fc77 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequestBuilder.swift @@ -0,0 +1,467 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import BedrockTypes +import Foundation + +public struct ConverseRequestBuilder { + + public private(set) var model: BedrockModel + private var parameters: ConverseParameters + + public private(set) var history: [Message] + public private(set) var tools: [Tool]? + + public private(set) var prompt: String? + public private(set) var image: ImageBlock? + public private(set) var document: DocumentBlock? + public private(set) var toolResult: ToolResultBlock? + + public private(set) var maxTokens: Int? + public private(set) var temperature: Double? + public private(set) var topP: Double? + public private(set) var stopSequences: [String]? + public private(set) var systemPrompts: [String]? + public private(set) var maxReasoningTokens: Int? + public private(set) var enableReasoning: Bool = false + + // MARK - Initializers + + public init(with model: BedrockModel) throws { + self.model = model + let modality = try model.getConverseModality() + self.parameters = modality.getConverseParameters() + self.history = [] + } + + public init(with modelId: String) throws { + guard let model = BedrockModel(rawValue: modelId) else { + throw BedrockServiceError.notFound("No model with model id \(modelId) found.") + } + self = try .init(with: model) + } + + /// Creates a ConverseRequestBuilder object based of a ConverseRequestBuilder object + public init(from builder: ConverseRequestBuilder) throws { + self = try ConverseRequestBuilder(with: builder.model) + .withHistory(builder.history) + .withTemperature(builder.temperature) + .withTopP(builder.topP) + .withMaxTokens(builder.maxTokens) + .withStopSequences(builder.stopSequences) + .withSystemPrompts(builder.systemPrompts) + .withTools(builder.tools) + .withReasoning(enabled: builder.enableReasoning, maxReasoningTokens: builder.maxReasoningTokens) + } + + /// Creates a ConverseRequestBuilder object based of a ConverseRequestBuilder object + /// with an updated history and all the user input (prompt, image, document or toolresult) emptied out. + public init(from builder: ConverseRequestBuilder, with reply: ConverseReply) throws { + self = try .init(from: builder) + .withHistory(reply.getHistory()) + } + + /// Creates a ConverseRequestBuilder object based of a ConverseRequestBuilder object + /// with an updated history and all the user input (prompt, image, document or toolresult) emptied out. + public init(from builder: ConverseRequestBuilder, with assistantMessage: Message) throws { + let userMessage = try builder.getUserMessage() + let history = builder.history + [userMessage, assistantMessage] + self = try .init(from: builder) + .withHistory(history) + } + + // MARK - builder methods + + // MARK - builder methods - history + + public func withHistory(_ history: [Message]) throws -> ConverseRequestBuilder { + if let lastMessage = history.last { + guard lastMessage.role == .assistant else { + throw BedrockServiceError.ConverseRequestBuilder("Last message in history must be from assistant.") + } + } + if toolResult != nil { + guard case .toolUse(_) = history.last?.content.last else { + throw BedrockServiceError.invalidPrompt("Tool result is defined but last message is not tool use.") + } + } + var copy = self + copy.history = history + return copy + } + + // MARK - builder methods - tools + + public func withTools(_ tools: [Tool]) throws -> ConverseRequestBuilder { + try validateFeature(.toolUse) + guard tools.count > 0 else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set tools to empty array.") + } + if case .toolUse(let toolUse) = history.last?.content.last { + guard tools.contains(where: { $0.name == toolUse.name }) else { + throw BedrockServiceError.ConverseRequestBuilder( + "Cannot set tools if last message in history contains toolUse and no matching tool is found." + ) + } + } + let toolNames = tools.map { $0.name } + guard Set(toolNames).count == tools.count else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set tools with duplicate names.") + } + var copy = self + copy.tools = tools + return copy + } + + private func withTools(_ tools: [Tool]?) throws -> ConverseRequestBuilder { + let copy = self + if let tools { + return try copy.withTools(tools) + } + return copy + } + + public func withTool(_ tool: Tool) throws -> ConverseRequestBuilder { + try self.withTools([tool]) + } + + public func withTool(name: String, inputSchema: JSON, description: String?) throws -> ConverseRequestBuilder { + try self.withTools([try Tool(name: name, inputSchema: inputSchema, description: description)]) + } + + // MARK - builder methods - user prompt + + public func withPrompt(_ prompt: String) throws -> ConverseRequestBuilder { + guard toolResult == nil else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set prompt when tool result is set") + } + try parameters.prompt.validateValue(prompt) + var copy = self + copy.prompt = prompt + return copy + } + + public func withImage(_ image: ImageBlock) throws -> ConverseRequestBuilder { + try validateFeature(.vision) + guard toolResult == nil else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set image when tool result is set") + } + var copy = self + copy.image = image + return copy + } + + public func withImage(format: ImageBlock.Format, source: String) throws -> ConverseRequestBuilder { + try self.withImage(try ImageBlock(format: format, source: source)) + } + + public func withDocument(_ document: DocumentBlock) throws -> ConverseRequestBuilder { + try validateFeature(.document) + guard toolResult == nil else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set document when tool result is set") + } + var copy = self + copy.document = document + return copy + } + + public func withDocument( + name: String, + format: DocumentBlock.Format, + source: String + ) throws -> ConverseRequestBuilder { + try self.withDocument(try DocumentBlock(name: name, format: format, source: source)) + } + + public func withToolResult(_ toolResult: ToolResultBlock) throws -> ConverseRequestBuilder { + guard prompt == nil && image == nil && document == nil else { + throw BedrockServiceError.ConverseRequestBuilder( + "Cannot set tool result when prompt, image, or document is set" + ) + } + guard let _ = tools else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set tool result when tools are not set") + } + guard let lastMessage = history.last else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set tool result when history is empty") + } + guard case .toolUse(let toolUse) = lastMessage.content.last else { + throw BedrockServiceError.invalidPrompt("Cannot set tool result when last message is not tool use.") + } + guard toolUse.id == toolResult.id else { + throw BedrockServiceError.invalidPrompt("Tool result name does not match tool use name.") + } + try validateFeature(.toolUse) + var copy = self + copy.toolResult = toolResult + return copy + } + + public func withToolResult( + id: String? = nil, + content: [ToolResultBlock.Content], + status: ToolResultBlock.Status? = nil + ) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = ToolResultBlock(id: id, content: content, status: status) + return try self.withToolResult(toolResult) + } + + public func withToolResult( + _ text: String, + id: String? = nil, + status: ToolResultBlock.Status? = nil + ) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = ToolResultBlock(text, id: id, status: status) + return try self.withToolResult(toolResult) + } + + public func withToolResult( + _ image: ImageBlock, + id: String? = nil, + status: ToolResultBlock.Status? = nil + ) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = ToolResultBlock(image, id: id, status: status) + return try self.withToolResult(toolResult) + } + + public func withToolResult( + _ document: DocumentBlock, + id: String? = nil, + status: ToolResultBlock.Status? = nil + ) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = ToolResultBlock(document, id: id, status: status) + return try self.withToolResult(toolResult) + } + + public func withToolResult( + _ json: JSON, + id: String? = nil, + status: ToolResultBlock.Status? = nil + ) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = ToolResultBlock(json, id: id, status: status) + return try self.withToolResult(toolResult) + } + + public func withToolResult( + _ video: VideoBlock, + id: String? = nil, + status: ToolResultBlock.Status? = nil + ) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = ToolResultBlock(video, id: id, status: status) + return try self.withToolResult(toolResult) + } + + public func withToolResult( + _ data: Data, + id: String? = nil, + status: ToolResultBlock.Status? = nil + ) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = try ToolResultBlock(data, id: id, status: status) + return try self.withToolResult(toolResult) + } + + public func withToolResult( + _ object: C, + id: String? = nil, + status: ToolResultBlock.Status? = nil + ) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = try ToolResultBlock(object, id: id, status: status) + return try self.withToolResult(toolResult) + } + + public func withFailedToolResult(id: String?) throws -> ConverseRequestBuilder { + let id = try id ?? getToolResultId() + let toolResult = ToolResultBlock(id: id, content: [], status: .error) + return try self.withToolResult(toolResult) + } + + // MARK - builder methods - inference parameters + + public func withMaxTokens(_ maxTokens: Int?) throws -> ConverseRequestBuilder { + var copy = self + if let maxTokens { + try copy.parameters.maxTokens.validateValue(maxTokens) + if let maxReasoningTokens { + guard maxReasoningTokens < maxTokens else { + throw BedrockServiceError.ConverseRequestBuilder( + "maxTokens must be greater than maxReasoningTokens" + ) + } + } + copy.maxTokens = maxTokens + } + return copy + } + + public func withTemperature(_ temperature: Double?) throws -> ConverseRequestBuilder { + var copy = self + if let temperature { + try copy.parameters.temperature.validateValue(temperature) + copy.temperature = temperature + } + return copy + } + + public func withTopP(_ topP: Double?) throws -> ConverseRequestBuilder { + var copy = self + if let topP { + try copy.parameters.topP.validateValue(topP) + copy.topP = topP + } + return copy + } + + public func withStopSequences(_ stopSequences: [String]?) throws -> ConverseRequestBuilder { + var copy = self + if let stopSequences { + guard stopSequences != [] else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set stop sequences to empty array.") + } + try copy.parameters.stopSequences.validateValue(stopSequences) + copy.stopSequences = stopSequences + } + return copy + } + + public func withStopSequence(_ stopSequence: String?) throws -> ConverseRequestBuilder { + var stopSequences: [String]? = nil + if let stopSequence { + stopSequences = [stopSequence] + } + return try self.withStopSequences(stopSequences) + } + + public func withSystemPrompts(_ systemPrompts: [String]?) throws -> ConverseRequestBuilder { + var copy = self + if let systemPrompts { + guard systemPrompts != [] else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set system prompts to empty array.") + } + copy.systemPrompts = systemPrompts + } + return copy + } + + public func withSystemPrompt(_ systemPrompt: String?) throws -> ConverseRequestBuilder { + var systemPrompts: [String]? = nil + if let systemPrompt { + systemPrompts = [systemPrompt] + } + return try self.withSystemPrompts(systemPrompts) + } + + public func withReasoning(_ enabled: Bool = true) throws -> ConverseRequestBuilder { + var copy = self + if enabled { + try validateFeature(.reasoning) + copy.enableReasoning = true + copy = try copy.withMaxReasoningTokens( + self.maxReasoningTokens ?? parameters.maxReasoningTokens.defaultValue + ) + } else { + copy.enableReasoning = false + copy.maxReasoningTokens = nil + } + return copy + } + + public func withMaxReasoningTokens(_ maxReasoningTokens: Int?) throws -> ConverseRequestBuilder { + var copy = self + if let maxReasoningTokens { + try validateFeature(.reasoning) + guard enableReasoning else { + throw BedrockServiceError.ConverseRequestBuilder( + "Cannot set maxReasoningTokens when reasoning is disabled" + ) + } + if let maxTokens { + guard maxReasoningTokens < maxTokens else { + throw BedrockServiceError.ConverseRequestBuilder( + "maxReasoningTokens must be less than maxTokens" + ) + } + } + try copy.parameters.maxReasoningTokens.validateValue(maxReasoningTokens) + copy.maxReasoningTokens = maxReasoningTokens + } + return copy + } + + /// convenience method to enable reasoning and set maxReasoningTokens at the same time + public func withReasoning(maxReasoningTokens: Int) throws -> ConverseRequestBuilder { + try self.withReasoning(true).withMaxReasoningTokens(maxReasoningTokens) + } + + /// private convenience method + private func withReasoning(enabled: Bool, maxReasoningTokens: Int? = nil) throws -> ConverseRequestBuilder { + let copy = self + if let maxReasoningTokens { + return try copy.withReasoning(true) + .withMaxReasoningTokens(maxReasoningTokens) + } + return try copy.withReasoning(enabled) + } + + // MARK - public methods + + /// Returns the user Message made up of the user input in the builder + package func getUserMessage() throws -> Message { + var content: [Content] = [] + if let prompt { + content.append(.text(prompt)) + } + if let image { + content.append(.image(image)) + } + if let document { + content.append(.document(document)) + } + if let toolResult { + content.append(.toolResult(toolResult)) + } + guard !content.isEmpty else { + throw BedrockServiceError.ConverseRequestBuilder("No content defined.") + } + return Message(from: .user, content: content) + } + + private func getToolResultId() throws -> String { + guard let lastMessage = history.last else { + throw BedrockServiceError.ConverseRequestBuilder("Cannot set tool result when history is empty") + } + guard case .toolUse(let toolUse) = lastMessage.content.last else { + throw BedrockServiceError.invalidPrompt("Cannot set tool result when last message is not tool use.") + } + return toolUse.id + } + + private func validateFeature(_ feature: ConverseFeature) throws { + guard model.hasConverseModality(feature) else { + throw BedrockServiceError.invalidModality( + model, + try model.getConverseModality(), + "This model does not support converse feature \(feature)." + ) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequestStreaming.swift b/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequestStreaming.swift new file mode 100644 index 00000000..2e4ae66d --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/Converse/ConverseRequestStreaming.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import BedrockTypes + +public typealias ConverseStreamingRequest = ConverseRequest +extension ConverseStreamingRequest { + func getConverseStreamingInput() throws -> ConverseStreamInput { + ConverseStreamInput( + additionalModelRequestFields: try getAdditionalModelRequestFields(), + inferenceConfig: inferenceConfig?.getSDKInferenceConfig(), + messages: try getSDKMessages(), + modelId: model.id, + system: getSDKSystemPrompts(), + toolConfig: try toolConfig?.getSDKToolConfig() + ) + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/Converse/ConverseResponseStreaming.swift b/swift-bedrock-library/Sources/BedrockService/Converse/ConverseResponseStreaming.swift new file mode 100644 index 00000000..10024740 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/Converse/ConverseResponseStreaming.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime + +public typealias ConverseStreamingResponse = BedrockRuntimeClientTypes.ConverseStreamOutput diff --git a/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+ImageParameterValidation.swift b/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+ImageParameterValidation.swift new file mode 100644 index 00000000..c4064297 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+ImageParameterValidation.swift @@ -0,0 +1,150 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import BedrockTypes +import Foundation + +extension BedrockService { + + /// Validates image generation parameters against the model's capabilities and constraints + /// - Parameters: + /// - modality: The image modality of the model + /// - nrOfImages: Optional number of images to generate + /// - cfgScale: Optional classifier free guidance scale + /// - resolution: Optional image resolution settings + /// - seed: Optional seed for reproducible generation + /// - Throws: BedrockServiceError for invalid parameters + private func validateImageGenerationParams( + model: BedrockModel, + nrOfImages: Int? = nil, + cfgScale: Double? = nil, + resolution: ImageResolution? = nil, + seed: Int? = nil + ) throws { + logger.trace( + "Validating general image generation parameters", + metadata: [ + "model": "\(model.id)", + "nrOfImages": .stringConvertible(nrOfImages ?? "Not defined"), + "cfgScale": .stringConvertible(cfgScale ?? "Not defined"), + "resolution.height": .stringConvertible(resolution?.height ?? "Not defined"), + "resolution.width": .stringConvertible(resolution?.width ?? "Not defined"), + "seed": .stringConvertible(seed ?? "Not defined"), + ] + ) + let modality = try model.getImageModality() + let parameters = modality.getParameters() + if let nrOfImages { + try parameters.nrOfImages.validateValue(nrOfImages) + } + if let cfgScale { + try parameters.cfgScale.validateValue(cfgScale) + } + if let seed { + try parameters.seed.validateValue(seed) + } + if let resolution { + try modality.validateResolution(resolution) + } + } + + /// Validates parameters for text-to-image generation requests + /// - Parameters: + /// - modality: The text-to-image modality of the model to use + /// - prompt: The input text prompt describing the desired image + /// - negativePrompt: Optional text describing what to avoid in the generated image + /// - Throws: BedrockServiceError if the parameters are invalid or exceed model constraints + public func validateTextToImageParams( + model: BedrockModel, + nrOfImages: Int? = nil, + cfgScale: Double? = nil, + resolution: ImageResolution? = nil, + seed: Int? = nil, + prompt: String, + negativePrompt: String? = nil + ) throws { + try validateImageGenerationParams( + model: model, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + resolution: resolution, + seed: seed + ) + logger.trace( + "Validating text to image parameters", + metadata: [ + "model": "\(model.id)", + "prompt": "\(prompt)", + "negativePrompt": .stringConvertible(negativePrompt ?? "Not defined"), + ] + ) + let modality = try model.getTextToImageModality() + let parameters = modality.getTextToImageParameters() + try parameters.prompt.validateValue(prompt) + if let negativePrompt { + try parameters.negativePrompt.validateValue(negativePrompt) + } + } + + /// Validates image variation generation parameters + /// - Parameters: + /// - modality: The image variation modality of the model + /// - images: Array of base64 encoded images to use as reference + /// - prompt: Text prompt describing desired variations + /// - similarity: Optional parameter controlling variation similarity + /// - negativePrompt: Optional text describing what to avoid + /// - Throws: BedrockServiceError for invalid parameters + public func validateImageVariationParams( + model: BedrockModel, + nrOfImages: Int? = nil, + cfgScale: Double? = nil, + resolution: ImageResolution? = nil, + seed: Int? = nil, + images: [String], + prompt: String? = nil, + similarity: Double? = nil, + negativePrompt: String? = nil + ) throws { + try validateImageGenerationParams( + model: model, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + resolution: resolution, + seed: seed + ) + logger.trace( + "Validating general image generation parameters", + metadata: [ + "model": "\(model.id)", + "prompt": .stringConvertible(prompt ?? "Not defined"), + "negativePrompt": .stringConvertible(negativePrompt ?? "Not defined"), + "similarity": .stringConvertible(similarity ?? "Not defined"), + "images": "\(images.count)", + ] + ) + let modality = try model.getImageVariationModality() + let parameters = modality.getImageVariationParameters() + try parameters.images.validateValue(images.count) + if let prompt { + try parameters.prompt.validateValue(prompt) + } + if let similarity { + try parameters.similarity.validateValue(similarity) + } + if let negativePrompt { + try parameters.negativePrompt.validateValue(negativePrompt) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+InvokeModelImage.swift b/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+InvokeModelImage.swift new file mode 100644 index 00000000..be1ae7f5 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+InvokeModelImage.swift @@ -0,0 +1,253 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import AwsCommonRuntimeKit +import BedrockTypes +import Foundation + +extension BedrockService { + + /// Generates 1 to 5 image(s) from a text prompt using a specific model + /// - Parameters: + /// - prompt: The text prompt describing the image that should be generated + /// - model: The BedrockModel that will be used to generate the image + /// - negativePrompt: Optional text describing what to avoid in the generated image + /// - nrOfImages: Optional number of images to generate (must be between 1 and 5, default 3) + /// - cfgScale: Optional classifier free guidance scale to control prompt adherence + /// - seed: Optional seed for reproducible image generation + /// - quality: Optional parameter to control the quality of generated images + /// - resolution: Optional parameter to specify the desired image resolution + /// - Throws: BedrockServiceError.notSupported for parameters or functionalities that are not supported + /// BedrockServiceError.invalidParameter for invalid parameters + /// BedrockServiceError.invalidPrompt if the prompt is empty or too long + /// BedrockServiceError.invalidModality for invalid modality from the selected model + /// BedrockServiceError.invalidSDKResponse if the response body is missing + /// - Returns: An ImageGenerationOutput object containing an array of generated images + public func generateImage( + _ prompt: String, + with model: BedrockModel, + negativePrompt: String? = nil, + nrOfImages: Int? = nil, + cfgScale: Double? = nil, + seed: Int? = nil, + quality: ImageQuality? = nil, + resolution: ImageResolution? = nil + ) async throws -> ImageGenerationOutput { + logger.trace( + "Generating image(s)", + metadata: [ + "model.id": .string(model.id), + "model.modality": .string(model.modality.getName()), + "prompt": .string(prompt), + "negativePrompt": .stringConvertible(negativePrompt ?? "not defined"), + "nrOfImages": .stringConvertible(nrOfImages ?? "not defined"), + "cfgScale": .stringConvertible(cfgScale ?? "not defined"), + "seed": .stringConvertible(seed ?? "not defined"), + ] + ) + do { + try validateTextToImageParams( + model: model, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + resolution: resolution, + seed: seed, + prompt: prompt, + negativePrompt: negativePrompt + ) + let request: InvokeModelRequest = try InvokeModelRequest.createTextToImageRequest( + model: model, + prompt: prompt, + negativeText: negativePrompt, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + let input: InvokeModelInput = try request.getInvokeModelInput() + logger.trace( + "Sending request to invokeModel", + metadata: [ + "model": .string(model.id), "request": .string(String(describing: input)), + ] + ) + let response = try await self.bedrockRuntimeClient.invokeModel(input: input) + guard let responseBody = response.body else { + logger.trace( + "Invalid response", + metadata: [ + "response": .string(String(describing: response)), + "hasBody": .stringConvertible(response.body != nil), + ] + ) + throw BedrockServiceError.invalidSDKResponse( + "Something went wrong while extracting body from response." + ) + } + let invokemodelResponse: InvokeModelResponse = try InvokeModelResponse.createImageResponse( + body: responseBody, + model: model + ) + return try invokemodelResponse.getGeneratedImage() + } catch { + try handleCommonError(error, context: "listing foundation models") + throw BedrockServiceError.unknownError("\(error)") // FIXME: handleCommonError will always throw + } + } + + /// Generates 1 to 5 image variation(s) from reference images and a text prompt using a specific model + /// - Parameters: + /// - images: Array of base64 encoded reference images to generate variations from + /// - prompt: The text prompt describing desired modifications to the reference images + /// - model: The BedrockModel that will be used to generate the variations + /// - negativePrompt: Optional text describing what to avoid in the generated variations + /// - similarity: Optional parameter controlling how closely variations should match reference (between 0.2 and 1.0) + /// - nrOfImages: Optional number of variations to generate (must be between 1 and 5, default 3) + /// - cfgScale: Optional classifier free guidance scale to control prompt adherence + /// - seed: Optional seed for reproducible variation generation + /// - quality: Optional parameter to control the quality of generated variations + /// - resolution: Optional parameter to specify the desired image resolution + /// - Throws: BedrockServiceError.notSupported for parameters or functionalities that are not supported + /// BedrockServiceError.invalidParameter for invalid parameters + /// BedrockServiceError.invalidPrompt if the prompt is empty or too long + /// BedrockServiceError.invalidModality for invalid modality from the selected model + /// BedrockServiceError.invalidSDKResponse if the response body is missing + /// - Returns: An ImageGenerationOutput object containing an array of generated image variations + public func generateImageVariation( + images: [String], + prompt: String, + with model: BedrockModel, + negativePrompt: String? = nil, + similarity: Double? = nil, + nrOfImages: Int? = nil, + cfgScale: Double? = nil, + seed: Int? = nil, + quality: ImageQuality? = nil, + resolution: ImageResolution? = nil + ) async throws -> ImageGenerationOutput { + logger.trace( + "Generating image(s) from reference image", + metadata: [ + "model.id": .string(model.id), + "model.modality": .string(model.modality.getName()), + "prompt": .string(prompt), + "nrOfImages": .stringConvertible(nrOfImages ?? "not defined"), + "similarity": .stringConvertible(similarity ?? "not defined"), + "negativePrompt": .stringConvertible(negativePrompt ?? "not defined"), + "cfgScale": .stringConvertible(cfgScale ?? "not defined"), + "seed": .stringConvertible(seed ?? "not defined"), + ] + ) + do { + try validateImageVariationParams( + model: model, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + resolution: resolution, + seed: seed, + images: images, + prompt: prompt, + similarity: similarity, + negativePrompt: negativePrompt + ) + let request: InvokeModelRequest = try InvokeModelRequest.createImageVariationRequest( + model: model, + prompt: prompt, + negativeText: negativePrompt, + images: images, + similarity: similarity, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + let input: InvokeModelInput = try request.getInvokeModelInput() + logger.trace( + "Sending request to invokeModel", + metadata: [ + "model": .string(model.id), "request": .string(String(describing: input)), + ] + ) + let response = try await self.bedrockRuntimeClient.invokeModel(input: input) + guard let responseBody = response.body else { + logger.trace( + "Invalid response", + metadata: [ + "response": .string(String(describing: response)), + "hasBody": .stringConvertible(response.body != nil), + ] + ) + throw BedrockServiceError.invalidSDKResponse( + "Something went wrong while extracting body from response." + ) + } + let invokemodelResponse: InvokeModelResponse = try InvokeModelResponse.createImageResponse( + body: responseBody, + model: model + ) + return try invokemodelResponse.getGeneratedImage() + } catch { + try handleCommonError(error, context: "listing foundation models") + throw BedrockServiceError.unknownError("\(error)") // FIXME: handleCommonError will always throw + } + } + + /// Generates 1 to 5 image variation(s) from reference images and a text prompt using a specific model + /// - Parameters: + /// - image: A base64 encoded reference image to generate variations from + /// - prompt: The text prompt describing desired modifications to the reference images + /// - model: The BedrockModel that will be used to generate the variations + /// - negativePrompt: Optional text describing what to avoid in the generated variations + /// - similarity: Optional parameter controlling how closely variations should match reference (between 0.2 and 1.0) + /// - nrOfImages: Optional number of variations to generate (must be between 1 and 5, default 3) + /// - cfgScale: Optional classifier free guidance scale to control prompt adherence + /// - seed: Optional seed for reproducible variation generation + /// - quality: Optional parameter to control the quality of generated variations + /// - resolution: Optional parameter to specify the desired image resolution + /// - Throws: BedrockServiceError.notSupported for parameters or functionalities that are not supported + /// BedrockServiceError.invalidParameter for invalid parameters + /// BedrockServiceError.invalidPrompt if the prompt is empty or too long + /// BedrockServiceError.invalidModality for invalid modality from the selected model + /// BedrockServiceError.invalidSDKResponse if the response body is missing + /// - Returns: An ImageGenerationOutput object containing an array of generated image variations + public func generateImageVariation( + image: String, + prompt: String, + with model: BedrockModel, + negativePrompt: String? = nil, + similarity: Double? = nil, + nrOfImages: Int? = nil, + cfgScale: Double? = nil, + seed: Int? = nil, + quality: ImageQuality? = nil, + resolution: ImageResolution? = nil + ) async throws -> ImageGenerationOutput { + try await generateImageVariation( + images: [image], + prompt: prompt, + with: model, + negativePrompt: negativePrompt, + similarity: similarity, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+InvokeModelText.swift b/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+InvokeModelText.swift new file mode 100644 index 00000000..4bed25e3 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/InvokeModel/BedrockService+InvokeModelText.swift @@ -0,0 +1,137 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import AwsCommonRuntimeKit +import BedrockTypes +import Foundation + +extension BedrockService { + + /// Generates a text completion using a specified model + /// - Parameters: + /// - prompt: The text to be completed + /// - model: The BedrockModel that will be used to generate the completion + /// - maxTokens: The maximum amount of tokens in the completion (optional, default 300) + /// - temperature: The temperature used to generate the completion (optional, default 0.6) + /// - topP: Optional top-p parameter for nucleus sampling + /// - topK: Optional top-k parameter for filtering + /// - stopSequences: Optional array of sequences where generation should stop + /// - Throws: BedrockServiceError.notSupported for parameters or functionalities that are not supported + /// BedrockServiceError.invalidParameter for invalid parameters + /// BedrockServiceError.invalidPrompt for a prompt that is empty or too long + /// BedrockServiceError.invalidStopSequences if too many stop sequences were provided + /// BedrockServiceError.invalidModality for invalid modality from the selected model + /// BedrockServiceError.invalidSDKResponse if the response body is missing + /// - Returns: a TextCompletion object containing the generated text from the model + public func completeText( + _ prompt: String, + with model: BedrockModel, + maxTokens: Int? = nil, + temperature: Double? = nil, + topP: Double? = nil, + topK: Int? = nil, + stopSequences: [String]? = nil + ) async throws -> TextCompletion { + logger.trace( + "Generating text completion", + metadata: [ + "model.id": .string(model.id), + "model.modality": .string(model.modality.getName()), + "prompt": .string(prompt), + "maxTokens": .stringConvertible(maxTokens ?? "not defined"), + "temperature": .stringConvertible(temperature ?? "not defined"), + "topP": .stringConvertible(topP ?? "not defined"), + "topK": .stringConvertible(topK ?? "not defined"), + "stopSequences": .stringConvertible(stopSequences ?? "not defined"), + ] + ) + do { + let modality = try model.getTextModality() + let parameters = modality.getParameters() + try parameters.validate( + prompt: prompt, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + topK: topK, + stopSequences: stopSequences + ) + + logger.trace( + "Creating InvokeModelRequest", + metadata: [ + "model": .string(model.id), + "prompt": "\(prompt)", + ] + ) + let request: InvokeModelRequest = try InvokeModelRequest.createTextRequest( + model: model, + prompt: prompt, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + topK: topK, + stopSequences: stopSequences + ) + let input: InvokeModelInput = try request.getInvokeModelInput() + logger.trace( + "Sending request to invokeModel", + metadata: [ + "model": .string(model.id), "request": .string(String(describing: input)), + ] + ) + + let response = try await self.bedrockRuntimeClient.invokeModel(input: input) + logger.trace( + "Received response from invokeModel", + metadata: [ + "model": .string(model.id), "response": .string(String(describing: response)), + ] + ) + + guard let responseBody = response.body else { + logger.trace( + "Invalid response", + metadata: [ + "response": .string(String(describing: response)), + "hasBody": .stringConvertible(response.body != nil), + ] + ) + throw BedrockServiceError.invalidSDKResponse( + "Something went wrong while extracting body from response." + ) + } + if let bodyString = String(data: responseBody, encoding: .utf8) { + logger.trace("Extracted body from response", metadata: ["response.body": "\(bodyString)"]) + } + + let invokemodelResponse: InvokeModelResponse = try InvokeModelResponse.createTextResponse( + body: responseBody, + model: model + ) + logger.trace( + "Generated text completion", + metadata: [ + "model": .string(model.id), "response": .string(String(describing: invokemodelResponse)), + ] + ) + return try invokemodelResponse.getTextCompletion() + } catch { + try handleCommonError(error, context: "listing foundation models") + throw BedrockServiceError.unknownError("\(error)") // FIXME: handleCommonError will always throw + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/InvokeModel/InvokeModelRequest.swift b/swift-bedrock-library/Sources/BedrockService/InvokeModel/InvokeModelRequest.swift new file mode 100644 index 00000000..f64050db --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/InvokeModel/InvokeModelRequest.swift @@ -0,0 +1,224 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import BedrockTypes +import Foundation + +struct InvokeModelRequest { + let model: BedrockModel + let contentType: ContentType + let accept: ContentType + private let body: BedrockBodyCodable + + private init( + model: BedrockModel, + body: BedrockBodyCodable, + contentType: ContentType = .json, + accept: ContentType = .json + ) { + self.model = model + self.body = body + self.contentType = contentType + self.accept = accept + } + + // MARK: text + /// Creates a BedrockRequest for a text request with the specified parameters + /// - Parameters: + /// - model: The Bedrock model to use + /// - prompt: The input text prompt + /// - maxTokens: Maximum number of tokens to generate (default: 300) + /// - temperature: Temperature for text generation (default: 0.6) + /// - Returns: A configured BedrockRequest for a text request + /// - Throws: BedrockServiceError if the model doesn't support text output + static func createTextRequest( + model: BedrockModel, + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws -> InvokeModelRequest { + try .init( + model: model, + prompt: prompt, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + topK: topK, + stopSequences: stopSequences + ) + } + + private init( + model: BedrockModel, + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws { + let textModality = try model.getTextModality() + let body: BedrockBodyCodable = try textModality.getTextRequestBody( + prompt: prompt, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + topK: topK, + stopSequences: stopSequences + ) + self.init(model: model, body: body) + } + + // MARK: text to image + /// Creates a BedrockRequest for a text-to-image request with the specified parameters + /// - Parameters: + /// - model: The Bedrock model to use for image generation + /// - prompt: The text description of the image to generate + /// - nrOfImages: The number of images to generate + /// - Returns: A configured BedrockRequest for image generation + /// - Throws: BedrockServiceError if the model doesn't support text input or image output + public static func createTextToImageRequest( + model: BedrockModel, + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> InvokeModelRequest { + try .init( + model: model, + prompt: prompt, + negativeText: negativeText, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + private init( + model: BedrockModel, + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws { + let textToImageModality = try model.getTextToImageModality() + self.init( + model: model, + body: try textToImageModality.getTextToImageRequestBody( + prompt: prompt, + negativeText: negativeText, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + ) + } + + // MARK: image variation + /// Creates a BedrockRequest for a request to generate variations of an existing image + /// - Parameters: + /// - model: The Bedrock model to use for image variation generation + /// - prompt: The text description to guide the variation generation + /// - image: The base64-encoded string of the source image to create variations from + /// - similarity: A value between 0 and 1 indicating how similar the variations should be to the source image + /// - nrOfImages: The number of image variations to generate + /// - Returns: A configured BedrockRequest for image variation generation + /// - Throws: BedrockServiceError if the model doesn't support text and image input, or image output + public static func createImageVariationRequest( + model: BedrockModel, + prompt: String, + negativeText: String?, + images: [String], + similarity: Double?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> InvokeModelRequest { + try .init( + model: model, + prompt: prompt, + negativeText: negativeText, + images: images, + similarity: similarity, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + private init( + model: BedrockModel, + prompt: String, + negativeText: String?, + images: [String], + similarity: Double?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws { + let modality = try model.getImageVariationModality() + let body = try modality.getImageVariationRequestBody( + prompt: prompt, + negativeText: negativeText, + images: images, + similarity: similarity, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + self.init(model: model, body: body) + } + + /// Creates an InvokeModelInput instance for making a request to Amazon Bedrock + /// - Returns: A configured InvokeModelInput containing the model ID, content type, and encoded request body + /// - Throws: BedrockServiceError.encodingError if the request body cannot be encoded to JSON + public func getInvokeModelInput() throws -> InvokeModelInput { + do { + let jsonData: Data = try JSONEncoder().encode(self.body) + return InvokeModelInput( + accept: self.accept.headerValue, + body: jsonData, + contentType: self.contentType.headerValue, + modelId: model.id + ) + } catch { + throw BedrockServiceError.encodingError( + "Something went wrong while encoding the request body to JSON for InvokeModelInput: \(error)" + ) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/InvokeModel/InvokeModelResponse.swift b/swift-bedrock-library/Sources/BedrockService/InvokeModel/InvokeModelResponse.swift new file mode 100644 index 00000000..c88c145e --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/InvokeModel/InvokeModelResponse.swift @@ -0,0 +1,109 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import BedrockTypes +import Foundation + +public struct InvokeModelResponse { + let model: BedrockModel + let contentType: ContentType + let textCompletionBody: ContainsTextCompletion? + let imageGenerationBody: ContainsImageGeneration? + + private init( + model: BedrockModel, + contentType: ContentType = .json, + textCompletionBody: ContainsTextCompletion + ) { + self.model = model + self.contentType = contentType + self.textCompletionBody = textCompletionBody + self.imageGenerationBody = nil + } + + private init( + model: BedrockModel, + contentType: ContentType = .json, + imageGenerationBody: ContainsImageGeneration + ) { + self.model = model + self.contentType = contentType + self.imageGenerationBody = imageGenerationBody + self.textCompletionBody = nil + } + + /// Creates a BedrockResponse from raw response data containing text completion + /// - Parameters: + /// - data: The raw response data from the Bedrock service + /// - model: The Bedrock model that generated the response + /// - Throws: BedrockServiceError.invalidModel if the model is not supported + /// BedrockServiceError.invalidResponseBody if the response cannot be decoded + static func createTextResponse(body data: Data, model: BedrockModel) throws -> Self { + do { + let textModality = try model.getTextModality() + return self.init(model: model, textCompletionBody: try textModality.getTextResponseBody(from: data)) + } catch { + throw BedrockServiceError.invalidSDKResponseBody(data) + } + } + + /// Creates a BedrockResponse from raw response data containing an image generation + /// - Parameters: + /// - data: The raw response data from the Bedrock service + /// - model: The Bedrock model that generated the response + /// - Throws: BedrockServiceError.invalidModel if the model is not supported + /// BedrockServiceError.invalidResponseBody if the response cannot be decoded + static func createImageResponse(body data: Data, model: BedrockModel) throws -> Self { + do { + let imageModality = try model.getImageModality() + return self.init(model: model, imageGenerationBody: try imageModality.getImageResponseBody(from: data)) + } catch { + throw BedrockServiceError.invalidSDKResponseBody(data) + } + } + + /// Extracts the text completion from the response body + /// - Returns: The text completion from the response + /// - Throws: BedrockServiceError.decodingError if the completion cannot be extracted + public func getTextCompletion() throws -> TextCompletion { + do { + guard let textCompletionBody = textCompletionBody else { + throw BedrockServiceError.decodingError("No text completion body found in the response") + } + return try textCompletionBody.getTextCompletion() + } catch { + throw BedrockServiceError.decodingError( + "Something went wrong while decoding the request body to find the completion: \(error)" + ) + } + } + + /// Extracts the image generation from the response body + /// - Returns: The image generation from the response + /// - Throws: BedrockServiceError.decodingError if the image generation cannot be extracted + public func getGeneratedImage() throws -> ImageGenerationOutput { + do { + guard let imageGenerationBody = imageGenerationBody else { + throw BedrockServiceError.decodingError("No image generation body found in the response") + } + return imageGenerationBody.getGeneratedImage() + } catch { + throw BedrockServiceError.decodingError( + "Something went wrong while decoding the request body to find the completion: \(error)" + ) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockService/Protocols/BedrockClientProtocol.swift b/swift-bedrock-library/Sources/BedrockService/Protocols/BedrockClientProtocol.swift new file mode 100644 index 00000000..fb243d70 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/Protocols/BedrockClientProtocol.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrock +import AWSClientRuntime +import AWSSDKIdentity +import Foundation + +// Protocol allows writing mocks for unit tests +public protocol BedrockClientProtocol: Sendable { + func listFoundationModels( + input: ListFoundationModelsInput + ) async throws + -> ListFoundationModelsOutput +} + +extension BedrockClient: @retroactive @unchecked Sendable, BedrockClientProtocol {} diff --git a/swift-bedrock-library/Sources/BedrockService/Protocols/BedrockRuntimeClientProtocol.swift b/swift-bedrock-library/Sources/BedrockService/Protocols/BedrockRuntimeClientProtocol.swift new file mode 100644 index 00000000..879e6712 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockService/Protocols/BedrockRuntimeClientProtocol.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import AWSClientRuntime +import AWSSDKIdentity +import BedrockTypes +import Foundation + +// Protocol allows writing mocks for unit tests +public protocol BedrockRuntimeClientProtocol: Sendable { + func invokeModel(input: InvokeModelInput) async throws -> InvokeModelOutput + func converse(input: ConverseInput) async throws -> ConverseOutput + func converseStream(input: ConverseStreamInput) async throws -> ConverseStreamOutput +} + +extension BedrockRuntimeClient: @retroactive @unchecked Sendable, BedrockRuntimeClientProtocol {} diff --git a/swift-bedrock-library/Sources/BedrockTypes/BedrockModel.swift b/swift-bedrock-library/Sources/BedrockTypes/BedrockModel.swift new file mode 100644 index 00000000..efa2540a --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/BedrockModel.swift @@ -0,0 +1,344 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct BedrockModel: Hashable, Sendable, Equatable, RawRepresentable { + public var rawValue: String { id } + + public var id: String + public var name: String + public let modality: any Modality + + /// Creates a new BedrockModel instance + /// - Parameters: + /// - id: The unique identifier for the model + /// - modality: The modality of the model + public init( + id: String, + name: String, + modality: any Modality + ) { + self.id = id + self.modality = modality + self.name = name + } + + /// Creates an implemented BedrockModel instance from a raw string value + /// - Parameter rawValue: The model identifier string + /// - Returns: The corresponding BedrockModel instance or nil if the model is not implemented + public init?(rawValue: String) { + switch rawValue { + // claude + case BedrockModel.instant.id: + self = BedrockModel.instant + case BedrockModel.claudev1.id: + self = BedrockModel.claudev1 + case BedrockModel.claudev2.id: + self = BedrockModel.claudev2 + case BedrockModel.claudev2_1.id: + self = BedrockModel.claudev2_1 + case BedrockModel.claudev3_haiku.id: + self = BedrockModel.claudev3_haiku + case BedrockModel.claudev3_5_haiku.id: + self = BedrockModel.claudev3_5_haiku + case BedrockModel.claudev3_opus.id: + self = BedrockModel.claudev3_opus + case BedrockModel.claudev3_5_sonnet.id: + self = BedrockModel.claudev3_5_sonnet + case BedrockModel.claudev3_5_sonnet_v2.id: + self = BedrockModel.claudev3_5_sonnet_v2 + case BedrockModel.claudev3_7_sonnet.id: + self = BedrockModel.claudev3_7_sonnet + // titan + case BedrockModel.titan_text_g1_premier.id: + self = BedrockModel.titan_text_g1_premier + case BedrockModel.titan_text_g1_express.id: + self = BedrockModel.titan_text_g1_express + case BedrockModel.titan_text_g1_lite.id: + self = BedrockModel.titan_text_g1_lite + case BedrockModel.titan_image_g1_v2.id: + self = BedrockModel.titan_image_g1_v2 + case BedrockModel.titan_image_g1_v1.id: + self = BedrockModel.titan_image_g1_v1 + // nova + case BedrockModel.nova_micro.id: + self = BedrockModel.nova_micro + case BedrockModel.nova_lite.id: + self = BedrockModel.nova_lite + case BedrockModel.nova_pro.id: + self = BedrockModel.nova_pro + case BedrockModel.nova_canvas.id: + self = BedrockModel.nova_canvas + // deepseek + case BedrockModel.deepseek_r1_v1.id: + self = BedrockModel.deepseek_r1_v1 + // llama + case BedrockModel.llama_3_8b_instruct.id: self = BedrockModel.llama_3_8b_instruct + case BedrockModel.llama3_70b_instruct.id: self = BedrockModel.llama3_70b_instruct + case BedrockModel.llama3_1_8b_instruct.id: self = BedrockModel.llama3_1_8b_instruct + case BedrockModel.llama3_1_70b_instruct.id: self = BedrockModel.llama3_1_70b_instruct + case BedrockModel.llama3_2_1b_instruct.id: self = BedrockModel.llama3_2_1b_instruct + case BedrockModel.llama3_2_3b_instruct.id: self = BedrockModel.llama3_2_3b_instruct + case BedrockModel.llama3_3_70b_instruct.id: self = BedrockModel.llama3_3_70b_instruct + // mistral + case BedrockModel.mistral_large_2402.id: self = BedrockModel.mistral_large_2402 + case BedrockModel.mistral_small_2402.id: self = BedrockModel.mistral_small_2402 + case BedrockModel.mistral_7B_instruct.id: self = BedrockModel.mistral_7B_instruct + case BedrockModel.mistral_8x7B_instruct.id: self = BedrockModel.mistral_8x7B_instruct + //cohere + case BedrockModel.cohere_command_R_plus.id: self = BedrockModel.cohere_command_R_plus + case BedrockModel.cohere_command_R.id: self = BedrockModel.cohere_command_R + default: + return nil + } + } + + // MARK: Modality checks + + // MARK - Text completion + + /// Checks if the model supports text generation + /// - Returns: True if the model supports text generation + public func hasTextModality() -> Bool { + modality as? any TextModality != nil + } + + /// Checks if the model supports text generation and returns TextModality + /// - Returns: TextModality if the model supports text modality + public func getTextModality() throws -> any TextModality { + guard let textModality = modality as? any TextModality else { + throw BedrockServiceError.invalidModality( + self, + modality, + "Model \(id) does not support text generation" + ) + } + return textModality + } + + // MARK - Image generation + + /// Checks if the model supports image generation + /// - Returns: True if the model supports image generation + public func hasImageModality() -> Bool { + modality as? any ImageModality != nil + } + + /// Checks if the model supports image generation and returns ImageModality + /// - Returns: TextModality if the model supports image modality + public func getImageModality() throws -> any ImageModality { + guard let imageModality = modality as? any ImageModality else { + throw BedrockServiceError.invalidModality( + self, + modality, + "Model \(id) does not support image generation" + ) + } + return imageModality + } + + /// Checks if the model supports text to image generation + /// - Returns: True if the model supports text to image generation + public func hasTextToImageModality() -> Bool { + modality as? any TextToImageModality != nil + } + + /// Checks if the model supports text to image generation and returns TextToImageModality + /// - Returns: TextToImageModality if the model supports image modality + public func getTextToImageModality() throws -> any TextToImageModality { + guard let textToImageModality = modality as? any TextToImageModality else { + throw BedrockServiceError.invalidModality( + self, + modality, + "Model \(id) does not support text to image generation" + ) + } + return textToImageModality + } + + /// Checks if the model supports image variation + /// - Returns: True if the model supports image variation + public func hasImageVariationModality() -> Bool { + modality as? any ImageVariationModality != nil + } + + /// Checks if the model supports image variation and returns ImageVariationModality + /// - Returns: ImageVariationModality if the model supports image modality + public func getImageVariationModality() throws -> any ImageVariationModality { + guard let modality = modality as? any ImageVariationModality else { + throw BedrockServiceError.invalidModality( + self, + modality, + "Model \(id) does not support image variation" + ) + } + return modality + } + + /// Checks if the model supports conditioned text to image generation + /// - Returns: True if the model supports conditioned text to image generation + public func hasConditionedTextToImageModality() -> Bool { + modality as? any ConditionedTextToImageModality != nil + } + + // MARK - Converse + + /// Checks if the model supports converse + /// - Returns: True if the model supports converse + public func hasConverseModality() -> Bool { + modality as? any ConverseModality != nil + } + + /// Checks if the model supports converse streaming + /// - Returns: True if the model supports converse streaming + public func hasConverseStreamingModality() -> Bool { + modality as? any ConverseStreamingModality != nil + } + + /// Checks if the model supports a specific converse feature + /// - Parameters: + /// - feature: the ConverseFeature that will be checked + /// - Returns: True if the model supports the converse feature + public func hasConverseModality(_ feature: ConverseFeature = .textGeneration) -> Bool { + if let converseModality = modality as? any ConverseModality { + let features = converseModality.getConverseFeatures() + return features.contains(feature) + } + return false + } + + /// Checks if the model supports text generation and returns ConverseModality + /// - Returns: ConverseModality if the model supports text modality + public func getConverseModality() throws -> any ConverseModality { + guard let modality = modality as? any ConverseModality else { + throw BedrockServiceError.invalidModality( + self, + modality, + "Model \(id) does not support text generation" + ) + } + return modality + } +} + +extension BedrockModel: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + // Encode basic information + try container.encode(name, forKey: .modelName) + try container.encode(id, forKey: .modelId) + try container.encode(String(describing: type(of: modality)), forKey: .supportedModality) + + if hasTextModality() { + try encodeTextParameters(to: &container) + } + + if hasImageModality() { + try encodeImageParameters(to: &container) + } + } + + private enum CodingKeys: String, CodingKey { + case modelName + case modelId + case temperatureRange + case maxTokenRange + case topPRange + case topKRange + case nrOfImagesRange + case cfgScaleRange + case seedRange + case similarityRange + case supportedModality + } + + private enum RangeKeys: String, CodingKey { + case min + case max + case `default` + } + + private func encodeTextParameters(to container: inout KeyedEncodingContainer) throws { + let textModality = try getTextModality() + let params = textModality.getParameters() + + if params.temperature.isSupported { + var tempContainer = container.nestedContainer(keyedBy: RangeKeys.self, forKey: .temperatureRange) + try tempContainer.encode(params.temperature.minValue, forKey: .min) + try tempContainer.encode(params.temperature.maxValue, forKey: .max) + try tempContainer.encode(params.temperature.defaultValue, forKey: .default) + } + if params.maxTokens.isSupported { + var tokenContainer = container.nestedContainer(keyedBy: RangeKeys.self, forKey: .maxTokenRange) + try tokenContainer.encode(params.maxTokens.minValue, forKey: .min) + try tokenContainer.encode(params.maxTokens.maxValue, forKey: .max) + try tokenContainer.encode(params.maxTokens.defaultValue, forKey: .default) + } + if params.topP.isSupported { + var topPContainer = container.nestedContainer(keyedBy: RangeKeys.self, forKey: .topPRange) + try topPContainer.encode(params.topP.minValue, forKey: .min) + try topPContainer.encode(params.topP.maxValue, forKey: .max) + try topPContainer.encode(params.topP.defaultValue, forKey: .default) + } + if params.topK.isSupported { + var topKContainer = container.nestedContainer(keyedBy: RangeKeys.self, forKey: .topKRange) + try topKContainer.encode(params.topK.minValue, forKey: .min) + try topKContainer.encode(params.topK.maxValue, forKey: .max) + try topKContainer.encode(params.topK.defaultValue, forKey: .default) + } + } + + private func encodeImageParameters(to container: inout KeyedEncodingContainer) throws { + let imageModality = try getImageModality() + let params = imageModality.getParameters() + + // General image generation inference parameters + if params.nrOfImages.isSupported { + var imagesContainer = container.nestedContainer(keyedBy: RangeKeys.self, forKey: .nrOfImagesRange) + try imagesContainer.encode(params.nrOfImages.minValue, forKey: .min) + try imagesContainer.encode(params.nrOfImages.maxValue, forKey: .max) + try imagesContainer.encode(params.nrOfImages.defaultValue, forKey: .default) + } + if params.cfgScale.isSupported { + var cfgScaleContainer = container.nestedContainer(keyedBy: RangeKeys.self, forKey: .cfgScaleRange) + try cfgScaleContainer.encode(params.cfgScale.minValue, forKey: .min) + try cfgScaleContainer.encode(params.cfgScale.maxValue, forKey: .max) + try cfgScaleContainer.encode(params.cfgScale.defaultValue, forKey: .default) + } + if params.seed.isSupported { + var seedContainer = container.nestedContainer(keyedBy: RangeKeys.self, forKey: .seedRange) + try seedContainer.encode(params.seed.minValue, forKey: .min) + try seedContainer.encode(params.seed.maxValue, forKey: .max) + try seedContainer.encode(params.seed.defaultValue, forKey: .default) + } + + // If the model supports image variation, encode similarity range + if hasImageVariationModality() { + let variationModality = try getImageVariationModality() + let variationParams = variationModality.getImageVariationParameters() + if variationParams.similarity.isSupported { + var similarityContainer = container.nestedContainer( + keyedBy: RangeKeys.self, + forKey: .similarityRange + ) + try similarityContainer.encode(variationParams.similarity.minValue, forKey: .min) + try similarityContainer.encode(variationParams.similarity.maxValue, forKey: .max) + try similarityContainer.encode(variationParams.similarity.defaultValue, forKey: .default) + } + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/BedrockServiceError.swift b/swift-bedrock-library/Sources/BedrockTypes/BedrockServiceError.swift new file mode 100644 index 00000000..c2f906ea --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/BedrockServiceError.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public enum BedrockServiceError: Error { + case invalidParameter(ParameterName, String) + case invalidModality(BedrockModel, Modality, String) + case invalidPrompt(String) + case invalid(String) + case invalidStopSequences([String], String) + case invalidURI(String) + case invalidConverseReply(String) + case invalidName(String) + case streamingError(String) + case invalidSDKType(String) + case ConverseRequestBuilder(String) + case invalidSDKResponse(String) + case invalidSDKResponseBody(Data?) + case completionNotFound(String) + case encodingError(String) + case decodingError(String) + case notImplemented(String) + case notSupported(String) + case notFound(String) + case authenticationFailed(String) + case unknownError(String) + + public var message: String { + switch self { + case .invalidParameter(let parameterName, let message): + return "Invalid parameter \(parameterName): \(message)" + case .invalidModality(let model, let modality, let message): + return "Invalid modality \(modality.getName()) for model \(model.name): \(message)" + case .invalidPrompt(let message): + return "Invalid prompt with value \(message)" + case .invalid(let message): + return "Invalid value: \(message)" + case .invalidStopSequences(let stopSequences, let message): + return "Invalid stop sequences \(stopSequences): \(message)" + case .invalidURI(let message): + return "Invalid URI: \(message)" + case .invalidConverseReply(let message): + return "Invalid converse reply: \(message)" + case .invalidName(let message): + return "Invalid name: \(message)" + case .streamingError(let message): + return "Streaming error: \(message)" + case .invalidSDKType(let message): + return "Invalid SDK type: \(message)" + case .ConverseRequestBuilder(let message): + return "Converse request builder error: \(message)" + case .invalidSDKResponse(let message): + return "Invalid SDK response: \(message)" + case .invalidSDKResponseBody(let value): + return "Invalid SDK response body: \(String(describing: value))" + case .completionNotFound(let message): + return "Completion not found: \(message)" + case .encodingError(let message): + return "Encoding error \(message)" + case .decodingError(let message): + return "Decoding error \(message)" + case .notImplemented(let message): + return "Not implemented: \(message)" + case .notSupported(let message): + return "Not supported: \(message)" + case .notFound(let message): + return "Not found: \(message)" + case .authenticationFailed(let message): + return "Authentication failed: \(message)" + case .unknownError(let message): + return "Unknown error: \(message)" + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/Content.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/Content.swift new file mode 100644 index 00000000..751775b4 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/Content.swift @@ -0,0 +1,187 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public enum Content: Codable, CustomStringConvertible, Sendable { + case text(String) + case image(ImageBlock) + case toolUse(ToolUseBlock) + case toolResult(ToolResultBlock) + case document(DocumentBlock) + case video(VideoBlock) + case reasoning(Reasoning) + case encryptedReasoning(EncryptedReasoning) + + // MARK - Initialiser + + public init(from sdkContentBlock: BedrockRuntimeClientTypes.ContentBlock) throws { + switch sdkContentBlock { + case .text(let text): + self = .text(text) + case .image(let sdkImage): + self = .image(try ImageBlock(from: sdkImage)) + case .document(let sdkDocumentBlock): + self = .document(try DocumentBlock(from: sdkDocumentBlock)) + case .tooluse(let sdkToolUseBlock): + self = .toolUse(try ToolUseBlock(from: sdkToolUseBlock)) + case .toolresult(let sdkToolResultBlock): + self = .toolResult(try ToolResultBlock(from: sdkToolResultBlock)) + case .video(let sdkVideoBlock): + self = .video(try VideoBlock(from: sdkVideoBlock)) + case .reasoningcontent(let sdkReasoningBlock): + switch sdkReasoningBlock { + case .reasoningtext(let sdkReasoningText): + self = .reasoning(try Reasoning(from: sdkReasoningText)) + case .redactedcontent(let data): + self = .encryptedReasoning(EncryptedReasoning(data)) + default: + throw BedrockServiceError.notImplemented( + "ReasoningContentBlock \(sdkReasoningBlock) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + default: + throw BedrockServiceError.notImplemented( + "ContentBlock \(sdkContentBlock) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + public func getSDKContentBlock() throws -> BedrockRuntimeClientTypes.ContentBlock { + switch self { + case .text(let text): + return BedrockRuntimeClientTypes.ContentBlock.text(text) + case .image(let imageBlock): + return BedrockRuntimeClientTypes.ContentBlock.image(try imageBlock.getSDKImageBlock()) + case .document(let documentBlock): + return BedrockRuntimeClientTypes.ContentBlock.document(try documentBlock.getSDKDocumentBlock()) + case .toolResult(let toolResultBlock): + return BedrockRuntimeClientTypes.ContentBlock.toolresult(try toolResultBlock.getSDKToolResultBlock()) + case .toolUse(let toolUseBlock): + return BedrockRuntimeClientTypes.ContentBlock.tooluse(try toolUseBlock.getSDKToolUseBlock()) + case .video(let videoBlock): + return BedrockRuntimeClientTypes.ContentBlock.video(try videoBlock.getSDKVideoBlock()) + case .reasoning(let reasoningBlock): + return BedrockRuntimeClientTypes.ContentBlock.reasoningcontent(reasoningBlock.getSDKReasoningBlock()) + case .encryptedReasoning(let encryptedReasoning): + return BedrockRuntimeClientTypes.ContentBlock.reasoningcontent(encryptedReasoning.getSDKReasoningBlock()) + } + } + + // MARK - convenience methods + + /// a description of the Content depending on the case + public var description: String { + switch self { + case .text(let text): + return "\(text)" + case .image(let imageBlock): + return "Image: \(imageBlock.format)" + case .toolUse(let toolUseBlock): + return "ToolUse: \(toolUseBlock.id) - \(toolUseBlock.name))" + case .toolResult(let toolResultBlock): + return "ToolResult: \(toolResultBlock.id)" + case .document(let documentBlock): + return "Document: \(documentBlock.name) - \(documentBlock.format)" + case .video(let videoBlock): + return "Video: \(videoBlock.format)" + case .reasoning(let reasoning): + return reasoning.description + case .encryptedReasoning(let encryptedReasoning): + return encryptedReasoning.description + } + } + + /// convenience method to check what is inside the Content + public func isText() -> Bool { + switch self { + case .text: + return true + default: + return false + } + } + + /// convenience method to check what is inside the Content + public func isImage() -> Bool { + switch self { + case .image: + return true + default: + return false + } + } + + /// convenience method to check what is inside the Content + public func isToolUse() -> Bool { + switch self { + case .toolUse: + return true + default: + return false + } + } + + /// convenience method to check what is inside the Content + public func isToolResult() -> Bool { + switch self { + case .toolResult: + return true + default: + return false + } + } + + /// convenience method to check what is inside the Content + public func isDocument() -> Bool { + switch self { + case .document: + return true + default: + return false + } + } + + /// convenience method to check what is inside the Content + public func isVideo() -> Bool { + switch self { + case .video: + return true + default: + return false + } + } + + /// convenience method to check what is inside the Content + public func isReasoning() -> Bool { + switch self { + case .reasoning: + return true + default: + return false + } + } + + /// convenience method to check what is inside the Content + public func isEncryptedReasoning() -> Bool { + switch self { + case .encryptedReasoning: + return true + default: + return false + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/DocumentBlock.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/DocumentBlock.swift new file mode 100644 index 00000000..464bc367 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/DocumentBlock.swift @@ -0,0 +1,158 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public struct DocumentBlock: Codable, Sendable { + public let name: String + public let format: Format + public let source: Source + + public init(name: String, format: Format, source: String) throws { + self = try Self(name: name, format: format, source: try Source(bytes: source)) + } + + public init(name: String, format: Format, source: Source) throws { + // https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_DocumentBlock.html + guard !name.isEmpty else { + throw BedrockServiceError.invalidName("Document name is not allowed to be empty") + } + guard name.contains(/^[a-zA-Z()\[\]\-](?:[a-zA-Z()\[\]\-]|\s(?!\s))*$/) else { + throw BedrockServiceError.invalidName( + "Document name must consist of only lowercase letter, uppercase letters, parentheses, square brackets, whitespace characters (no more than one in a row) and hyphens" + ) + } + guard name.count <= 200 else { + throw BedrockServiceError.invalidName("Document name must be no longer than 200 characters") + } + + self.name = name + self.format = format + self.source = source + } + + public init(from sdkDocumentBlock: BedrockRuntimeClientTypes.DocumentBlock) throws { + guard let name = sdkDocumentBlock.name else { + throw BedrockServiceError.decodingError( + "Could not extract name from BedrockRuntimeClientTypes.DocumentBlock" + ) + } + guard let sdkFormat = sdkDocumentBlock.format else { + throw BedrockServiceError.decodingError( + "Could not extract format from BedrockRuntimeClientTypes.DocumentBlock" + ) + } + guard let sdkSource = sdkDocumentBlock.source else { + throw BedrockServiceError.decodingError( + "Could not extract source from BedrockRuntimeClientTypes.DocumentSource" + ) + } + let format = try Format(from: sdkFormat) + let source = try Source(from: sdkSource) + try self.init(name: name, format: format, source: source) + } + + public func getSDKDocumentBlock() throws -> BedrockRuntimeClientTypes.DocumentBlock { + BedrockRuntimeClientTypes.DocumentBlock( + format: format.getSDKDocumentFormat(), + name: name, + source: try source.getSDKDocumentSource() + ) + } + + public enum Format: Codable, Sendable { + case csv + case doc + case docx + case html + case md + case pdf + case txt + case xls + case xlsx + + public init(from sdkDocumentFormat: BedrockRuntimeClientTypes.DocumentFormat) throws { + switch sdkDocumentFormat { + case .csv: self = .csv + case .doc: self = .doc + case .docx: self = .docx + case .html: self = .html + case .md: self = .md + case .pdf: self = .pdf + case .txt: self = .txt + case .xls: self = .xls + case .xlsx: self = .xlsx + default: + throw BedrockServiceError.notImplemented( + "DocumentFormat \(sdkDocumentFormat) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + public func getSDKDocumentFormat() -> BedrockRuntimeClientTypes.DocumentFormat { + switch self { + case .csv: return .csv + case .doc: return .doc + case .docx: return .docx + case .html: return .html + case .md: return .md + case .pdf: return .pdf + case .txt: return .txt + case .xls: return .xls + case .xlsx: return .xlsx + } + } + } + + public enum Source: Codable, Sendable { + case bytes(String) + case s3(S3Location) + + public init(bytes: String) throws { + guard !bytes.isEmpty else { + throw BedrockServiceError.invalidName("Document source is not allowed to be empty") + } + self = .bytes(bytes) + } + + public init(from sdkSource: BedrockRuntimeClientTypes.DocumentSource) throws { + switch sdkSource { + case .bytes(let data): + self = .bytes(data.base64EncodedString()) + case .s3location(let sdkS3Location): + self = .s3(try S3Location(from: sdkS3Location)) + default: + throw BedrockServiceError.notImplemented( + "DocumentSource \(sdkSource) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + public func getSDKDocumentSource() throws -> BedrockRuntimeClientTypes.DocumentSource { + switch self { + case .bytes(let data): + guard let sdkData = Data(base64Encoded: data) else { + throw BedrockServiceError.decodingError( + "Could not decode document source from base64 string. String: \(data)" + ) + } + return .bytes(sdkData) + case .s3(let s3Location): + return .s3location(s3Location.getSDKS3Location()) + } + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/DocumentToJSON.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/DocumentToJSON.swift new file mode 100644 index 00000000..20dcb864 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/DocumentToJSON.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Smithy + +// FIXME: avoid extensions on structs you do not control +extension SmithyDocument { + public func toJSON() throws -> JSON { + switch self.type { + case .string: + return JSON(with: try self.asString()) + case .boolean: + return JSON(with: try self.asBoolean()) + case .integer: + return JSON(with: try self.asInteger()) + case .double, .float: + return JSON(with: try self.asDouble()) + case .list: + let array = try self.asList().map { try $0.toJSON() } + return JSON(with: array) + case .map: + let map = try self.asStringMap() + var result: [String: JSON] = [:] + for (key, value) in map { + result[key] = try value.toJSON() + } + return JSON(with: result) + case .blob: + let data = try self.asBlob() + return JSON(with: data) + default: + throw DocumentError.typeMismatch("Unsupported type for JSON conversion: \(self.type)") + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/History.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/History.swift new file mode 100644 index 00000000..d15984b9 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/History.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public typealias History = [Message] + +extension History { + public var description: String { + var result = "\(self.count) turns:\n" + for message in self { + result += "\(message)\n" + } + return result + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ImageBlock.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ImageBlock.swift new file mode 100644 index 00000000..bed1b4f0 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ImageBlock.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public struct ImageBlock: Codable, Sendable { + public let format: Format + public let source: Source + + public init(format: Format, source: String) throws { + self = try .init(format: format, source: .bytes(source)) + } + + public init(format: Format, source: Source) throws { + // https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ImageSource.html + self.format = format + self.source = source + } + + public init(from sdkImageBlock: BedrockRuntimeClientTypes.ImageBlock) throws { + guard let sdkFormat = sdkImageBlock.format else { + throw BedrockServiceError.decodingError( + "Could not extract format from BedrockRuntimeClientTypes.ImageBlock" + ) + } + guard let sdkImageSource = sdkImageBlock.source else { + throw BedrockServiceError.decodingError( + "Could not extract source from BedrockRuntimeClientTypes.ImageBlock" + ) + } + let format = try Format(from: sdkFormat) + let source = try Source(from: sdkImageSource) + self = try .init(format: format, source: source) + } + + public func getSDKImageBlock() throws -> BedrockRuntimeClientTypes.ImageBlock { + BedrockRuntimeClientTypes.ImageBlock( + format: format.getSDKImageFormat(), + source: try source.getSDKImageSource() + ) + } + + public enum Format: Codable, Sendable { + case gif + case jpeg + case png + case webp + + public init(from sdkImageFormat: BedrockRuntimeClientTypes.ImageFormat) throws { + switch sdkImageFormat { + case .gif: self = .gif + case .jpeg: self = .jpeg + case .png: self = .png + case .webp: self = .webp + default: + throw BedrockServiceError.notImplemented( + "ImageFormat \(sdkImageFormat) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + public func getSDKImageFormat() -> BedrockRuntimeClientTypes.ImageFormat { + switch self { + case .gif: return .gif + case .jpeg: return .jpeg + case .png: return .png + case .webp: return .webp + } + } + } + + public enum Source: Codable, Sendable { + case bytes(String) + case s3(S3Location) + + public init(from sdkSource: BedrockRuntimeClientTypes.ImageSource) throws { + switch sdkSource { + case .bytes(let data): + guard !data.isEmpty else { + throw BedrockServiceError.invalidName("Image source is not allowed to be empty") + } + self = .bytes(data.base64EncodedString()) + case .s3location(let sdkS3Location): + self = .s3(try S3Location(from: sdkS3Location)) + default: + throw BedrockServiceError.notImplemented( + "ImageSource \(sdkSource) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + public func getSDKImageSource() throws -> BedrockRuntimeClientTypes.ImageSource { + switch self { + case .bytes(let data): + guard let sdkData = Data(base64Encoded: data) else { + throw BedrockServiceError.decodingError( + "Could not decode image source from base64 string. String: \(data)" + ) + } + return .bytes(sdkData) + case .s3(let s3Location): + return .s3location(s3Location.getSDKS3Location()) + } + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/JSON.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/JSON.swift new file mode 100644 index 00000000..149a2122 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/JSON.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct JSON: Codable, @unchecked Sendable { // FIXME: make Sendable + public var value: Any? + + /// Returns the value inside the JSON object defined by the given key. + public subscript(key: String) -> T? { + get { + if let dictionary = value as? [String: JSON] { + let json: JSON? = dictionary[key] + let nestedValue: Any? = json?.getValue() + return nestedValue as? T + } + return nil + } + } + + /// Returns the JSON object defined by the given key. + public subscript(key: String) -> JSON? { + get { + if let dictionary = value as? [String: JSON] { + return dictionary[key] + } + return nil + } + } + + /// Returns the value inside the JSON object defined by the given key. + public func getValue(_ key: String) -> T? { + if let dictionary = value as? [String: JSON] { + return dictionary[key]?.value as? T + } + return nil + } + + /// Returns the value inside the JSON object. + public func getValue() -> T? { + value as? T + } + + // MARK: Initializers + + public init(with value: Any?) { + self.value = value + } + + public init(from string: String) throws { + guard let data = string.data(using: .utf8) else { + throw BedrockServiceError.encodingError("Could not encode String to Data") + } + try self.init(from: data) + } + + public init(from data: Data) throws { + do { + self = try JSONDecoder().decode(JSON.self, from: data) + } catch { + throw BedrockServiceError.decodingError("Failed to decode JSON: \(error)") + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self.value = nil + } else if let intValue = try? container.decode(Int.self) { + self.value = intValue + } else if let doubleValue = try? container.decode(Double.self) { + self.value = doubleValue + } else if let stringValue = try? container.decode(String.self) { + self.value = stringValue + } else if let boolValue = try? container.decode(Bool.self) { + self.value = boolValue + } else if let arrayValue = try? container.decode([JSON].self) { + self.value = arrayValue.map { JSON(with: $0.value) } + } else if let dictionaryValue = try? container.decode([String: JSON].self) { + self.value = dictionaryValue.mapValues { JSON(with: $0.value) } + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type") + } + } + + // MARK: Public Methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + if let jsonValue = value as? JSON { + try jsonValue.encode(to: encoder) + } else if let intValue = value as? Int { + try container.encode(intValue) + } else if let doubleValue = value as? Double { + try container.encode(doubleValue) + } else if let stringValue = value as? String { + try container.encode(stringValue) + } else if let boolValue = value as? Bool { + try container.encode(boolValue) + } else if let arrayValue = value as? [Any] { + let jsonArray = arrayValue.map { JSON(with: $0) } + try container.encode(jsonArray) + } else if let dictionaryValue = value as? [String: Any] { + let jsonDictionary = dictionaryValue.mapValues { JSON(with: $0) } + try container.encode(jsonDictionary) + } else { + // try container.encode(String(describing: value ?? "nil")) + throw EncodingError.invalidValue( + value ?? "nil", + EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type") + ) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/JSONtoDocument.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/JSONtoDocument.swift new file mode 100644 index 00000000..b29beb20 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/JSONtoDocument.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Smithy + +extension JSON { + public func toDocument() throws -> Document { + let encoder = JSONEncoder() + let encoded = try encoder.encode(self) + return try Document.make(from: encoded) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ReasoningBlock.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ReasoningBlock.swift new file mode 100644 index 00000000..fa4f1f2c --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ReasoningBlock.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public struct EncryptedReasoning: Codable, Sendable { + public var reasoning: Data + + public var description: String { + "Encrypted reasoning: \(reasoning)" + } + + public init(_ data: Data) { + self.reasoning = data + } + + public func getSDKReasoningBlock() -> BedrockRuntimeClientTypes.ReasoningContentBlock { + .redactedcontent(reasoning) + } +} + +public struct Reasoning: Codable, CustomStringConvertible, Sendable { + public var signature: String? + public var reasoning: String + + public init(_ reasoning: String, signature: String? = nil) { + self.reasoning = reasoning + if signature == "" { + self.signature = nil + } else { + self.signature = signature + } + } + + public init(from sdkReasoningText: BedrockRuntimeClientTypes.ReasoningTextBlock) throws { + guard let text = sdkReasoningText.text else { + throw BedrockServiceError.invalidSDKType("Text is missing from ReasoningTextBlock") + } + self.signature = sdkReasoningText.signature + self.reasoning = text + } + + public func getSDKReasoningBlock() -> BedrockRuntimeClientTypes.ReasoningContentBlock { + .reasoningtext( + BedrockRuntimeClientTypes.ReasoningTextBlock(signature: signature, text: reasoning) + ) + } + + public var description: String { + if let signature { + return "Reasoning: \(reasoning) \nSignature: \(signature)" + } + return "Reasoning: \(reasoning)" + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/S3Location.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/S3Location.swift new file mode 100644 index 00000000..d244cd51 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/S3Location.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public struct S3Location: Codable, Sendable { + public let bucketOwner: String? + public let uri: String + + public init(bucketOwner: String? = nil, uri: String) { + self.bucketOwner = bucketOwner + self.uri = uri + } + + public init(from sdkS3Location: BedrockRuntimeClientTypes.S3Location) throws { + guard let uri = sdkS3Location.uri else { + throw BedrockServiceError.decodingError( + "Could not extract URI from BedrockRuntimeClientTypes.S3Location" + ) + } + guard uri.hasPrefix("") else { + throw BedrockServiceError.invalidURI("URI should start with \"s3://\". Your URI: \(uri)") + } + self = S3Location(bucketOwner: sdkS3Location.bucketOwner, uri: uri) + } + + public func getSDKS3Location() -> BedrockRuntimeClientTypes.S3Location { + BedrockRuntimeClientTypes.S3Location(bucketOwner: self.bucketOwner, uri: self.uri) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ToolResultBlock.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ToolResultBlock.swift new file mode 100644 index 00000000..a1bd024d --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ToolResultBlock.swift @@ -0,0 +1,212 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public struct ToolResultBlock: Codable, Sendable { + public let id: String + public let content: [Content] + public let status: Status? // currently only supported by Anthropic Claude 3 models + + public init(id: String, content: [Content], status: Status? = nil) { + self.id = id + self.content = content + self.status = status + } + + /// convenience initializer for ToolResultBlock with only an id and a String + public init(_ prompt: String, id: String, status: Status? = .success) { + self.init(id: id, content: [.text(prompt)], status: status) + } + + /// convenience initializer for ToolResultBlock with only an id and a JSON + public init(_ json: JSON, id: String, status: Status? = .success) { + self.init(id: id, content: [.json(json)], status: status) + } + + /// convenience initializer for ToolResultBlock with only an id and a ImageBlock + public init(_ image: ImageBlock, id: String, status: Status? = .success) { + self.init(id: id, content: [.image(image)], status: status) + } + + /// convenience initializer for ToolResultBlock with only an id and a DocumentBlock + public init(_ document: DocumentBlock, id: String, status: Status? = .success) { + self.init(id: id, content: [.document(document)], status: status) + } + + /// convenience initializer for ToolResultBlock with only an id and a VideoBlock + public init(_ video: VideoBlock, id: String, status: Status? = .success) { + self.init(id: id, content: [.video(video)], status: status) + } + + /// convenience initializer for ToolResultBlock with failed request + public static func failed(_ id: String) -> Self { + self.init(id: id, content: [], status: .error) + } + + /// convenience initializer for ToolResultBlock for Data + public init(_ data: Data, id: String, status: Status? = .success) throws { + guard let json = try? JSON(from: data) else { + throw BedrockServiceError.decodingError("Could not decode JSON from Data") + } + self.init(json, id: id, status: status) + } + + /// convenience initializer for ToolResultBlock for any Codable + public init(_ object: T, id: String, status: Status? = .success) throws { + guard let data = try? JSONEncoder().encode(object) else { + throw BedrockServiceError.encodingError("Could not encode object to JSON") + } + try self.init(data, id: id, status: status) + } + + public init(from sdkToolResultBlock: BedrockRuntimeClientTypes.ToolResultBlock) throws { + guard let sdkToolResultContent = sdkToolResultBlock.content else { + throw BedrockServiceError.decodingError( + "Could not extract content from BedrockRuntimeClientTypes.ToolResultBlock" + ) + } + guard let id = sdkToolResultBlock.toolUseId else { + throw BedrockServiceError.decodingError( + "Could not extract toolUseId from BedrockRuntimeClientTypes.ToolResultBlock" + ) + } + let sdkToolStatus: BedrockRuntimeClientTypes.ToolResultStatus? = sdkToolResultBlock.status + var status: Status? = nil + if let sdkToolStatus = sdkToolStatus { + status = try Status(from: sdkToolStatus) + } + let toolContents = try sdkToolResultContent.map { try Content(from: $0) } + self = ToolResultBlock(id: id, content: toolContents, status: status) + } + + public func getSDKToolResultBlock() throws -> BedrockRuntimeClientTypes.ToolResultBlock { + BedrockRuntimeClientTypes.ToolResultBlock( + content: try content.map { try $0.getSDKToolResultContentBlock() }, + status: status?.getSDKToolStatus(), + toolUseId: id + ) + } + + public enum Status: Codable, Sendable { + case success + case error + + init(from sdkToolStatus: BedrockRuntimeClientTypes.ToolResultStatus) throws { + switch sdkToolStatus { + case .success: self = .success + case .error: self = .error + default: + throw BedrockServiceError.notImplemented( + "ToolStatus \(sdkToolStatus) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + func getSDKToolStatus() -> BedrockRuntimeClientTypes.ToolResultStatus { + switch self { + case .success: .success + case .error: .error + } + } + } + + public enum Content: Sendable { + case json(JSON) + case text(String) + case image(ImageBlock) // currently only supported by Anthropic Claude 3 models + case document(DocumentBlock) + case video(VideoBlock) + + init(from sdkToolResultContent: BedrockRuntimeClientTypes.ToolResultContentBlock) throws { + switch sdkToolResultContent { + case .document(let sdkDocumentBlock): + self = .document(try DocumentBlock(from: sdkDocumentBlock)) + case .image(let sdkImageBlock): + self = .image(try ImageBlock(from: sdkImageBlock)) + case .text(let text): + self = .text(text) + case .video(let sdkVideoBlock): + self = .video(try VideoBlock(from: sdkVideoBlock)) + case .json(let document): + self = .json(try document.toJSON()) + default: + throw BedrockServiceError.notImplemented( + "ToolResultContent \(sdkToolResultContent) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + func getSDKToolResultContentBlock() throws -> BedrockRuntimeClientTypes.ToolResultContentBlock { + switch self { + case .json(let json): + .json(try json.toDocument()) + case .document(let documentBlock): + .document(try documentBlock.getSDKDocumentBlock()) + case .image(let imageBlock): + .image(try imageBlock.getSDKImageBlock()) + case .text(let text): + .text(text) + case .video(let videoBlock): + .video(try videoBlock.getSDKVideoBlock()) + } + } + } +} + +extension ToolResultBlock.Content: Codable { + private enum CodingKeys: String, CodingKey { + case json, text, image, document, video + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .json(let json): + try container.encode(json, forKey: .json) + case .text(let text): + try container.encode(text, forKey: .text) + case .image(let image): + try container.encode(image, forKey: .image) + case .document(let doc): + try container.encode(doc, forKey: .document) + case .video(let video): + try container.encode(video, forKey: .video) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let json = try container.decodeIfPresent(JSON.self, forKey: .json) { + self = .json(json) + } else if let text = try container.decodeIfPresent(String.self, forKey: .text) { + self = .text(text) + } else if let image = try container.decodeIfPresent(ImageBlock.self, forKey: .image) { + self = .image(image) + } else if let doc = try container.decodeIfPresent(DocumentBlock.self, forKey: .document) { + self = .document(doc) + } else if let video = try container.decodeIfPresent(VideoBlock.self, forKey: .video) { + self = .video(video) + } else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Invalid tool result content" + ) + ) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ToolUseBlock.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ToolUseBlock.swift new file mode 100644 index 00000000..f6de4051 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/ToolUseBlock.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public struct ToolUseBlock: Codable, Sendable { + public let id: String + public let name: String + public let input: JSON + + public init(id: String, name: String, input: JSON) { + self.id = id + self.name = name + self.input = input + } + + public init(from sdkToolUseBlock: BedrockRuntimeClientTypes.ToolUseBlock) throws { + guard let sdkId = sdkToolUseBlock.toolUseId else { + throw BedrockServiceError.decodingError( + "Could not extract toolUseId from BedrockRuntimeClientTypes.ToolUseBlock" + ) + } + guard let sdkName = sdkToolUseBlock.name else { + throw BedrockServiceError.decodingError( + "Could not extract name from BedrockRuntimeClientTypes.ToolUseBlock" + ) + } + guard let sdkInput = sdkToolUseBlock.input else { + throw BedrockServiceError.decodingError( + "Could not extract input from BedrockRuntimeClientTypes.ToolUseBlock" + ) + } + self = ToolUseBlock( + id: sdkId, + name: sdkName, + input: try sdkInput.toJSON() + ) + } + + public func getSDKToolUseBlock() throws -> BedrockRuntimeClientTypes.ToolUseBlock { + .init( + input: try input.toDocument(), + name: name, + toolUseId: id + ) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/VideoBlock.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/VideoBlock.swift new file mode 100644 index 00000000..387c1994 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ContentBlocks/VideoBlock.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public struct VideoBlock: Codable, Sendable { + public let format: Format + public let source: Source + + public init(format: Format, source: Source) throws { + // https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_VideoSource.html + guard case .bytes(let bytes) = source, !bytes.isEmpty else { + throw BedrockServiceError.invalidName("Video source is not allowed to be empty") + } + self.format = format + self.source = source + } + + public init(format: Format, source: String) throws { + try self.init(format: format, source: .bytes(source)) + } + + public init(format: Format, source: S3Location) throws { + try self.init(format: format, source: .s3(source)) + } + + public init(from sdkVideoBlock: BedrockRuntimeClientTypes.VideoBlock) throws { + guard let sdkFormat = sdkVideoBlock.format else { + throw BedrockServiceError.decodingError( + "Could not extract format from BedrockRuntimeClientTypes.VideoBlock" + ) + } + guard let sdkSource = sdkVideoBlock.source else { + throw BedrockServiceError.decodingError( + "Could not extract source from BedrockRuntimeClientTypes.VideoBlock" + ) + } + self = try VideoBlock( + format: try VideoBlock.Format(from: sdkFormat), + source: try VideoBlock.Source(from: sdkSource) + ) + } + + public func getSDKVideoBlock() throws -> BedrockRuntimeClientTypes.VideoBlock { + BedrockRuntimeClientTypes.VideoBlock( + format: try format.getSDKVideoFormat(), + source: try source.getSDKVideoSource() + ) + } + + public enum Source: Codable, Sendable { + case bytes(String) // base64 + case s3(S3Location) + + public init(from sdkSource: BedrockRuntimeClientTypes.VideoSource) throws { + switch sdkSource { + case .bytes(let data): + self = .bytes(data.base64EncodedString()) + case .s3location(let sdkS3Location): + self = .s3(try S3Location(from: sdkS3Location)) + default: + throw BedrockServiceError.notImplemented( + "VideoSource \(sdkSource) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + public func getSDKVideoSource() throws -> BedrockRuntimeClientTypes.VideoSource { + switch self { + case .bytes(let data): + guard let sdkData = Data(base64Encoded: data) else { + throw BedrockServiceError.decodingError( + "Could not decode video source from base64 string. String: \(data)" + ) + } + return .bytes(sdkData) + case .s3(let s3Location): + return .s3location(s3Location.getSDKS3Location()) + } + } + } + + public enum Format: Codable, Sendable { + case flv + case mkv + case mov + case mp4 + case mpeg + case mpg + case threeGp + case webm + case wmv + + public init(from sdkVideoFormat: BedrockRuntimeClientTypes.VideoFormat) throws { + switch sdkVideoFormat { + case .flv: self = .flv + case .mkv: self = .mkv + case .mov: self = .mov + case .mp4: self = .mp4 + case .mpeg: self = .mpeg + case .mpg: self = .mpg + case .threeGp: self = .threeGp + case .webm: self = .webm + case .wmv: self = .wmv + default: + throw BedrockServiceError.notImplemented( + "VideoFormat \(sdkVideoFormat) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + public func getSDKVideoFormat() throws -> BedrockRuntimeClientTypes.VideoFormat { + switch self { + case .flv: return .flv + case .mkv: return .mkv + case .mov: return .mov + case .mp4: return .mp4 + case .mpeg: return .mpeg + case .mpg: return .mpg + case .threeGp: return .threeGp + case .webm: return .webm + case .wmv: return .wmv + } + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/ConverseReply.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/ConverseReply.swift new file mode 100644 index 00000000..ad505045 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/ConverseReply.swift @@ -0,0 +1,165 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public struct ConverseReply: Codable, CustomStringConvertible { + let history: History + let textReply: String? + let toolUse: ToolUseBlock? + let imageBlock: ImageBlock? + let videoBlock: VideoBlock? + let reasoningBlock: Reasoning? + let encryptedReasoning: EncryptedReasoning? + + public var description: String { + if let textReply { + return textReply + } else { + let lastMessage = getLastMessage() + return lastMessage.content.description + } + } + + // MARK: Initializers + + public init(_ history: History) throws { + guard let lastMessage = history.last else { + throw BedrockServiceError.invalidConverseReply("The provided history is not allowed to be empty.") + } + guard lastMessage.role == .assistant else { + throw BedrockServiceError.invalidConverseReply("The last message in the history is not from the assistant.") + } + self.history = history + self.textReply = try? ConverseReply.getTextReply(lastMessage) + self.toolUse = try? ConverseReply.getToolUse(lastMessage) + self.imageBlock = try? ConverseReply.getImageBlock(lastMessage) + self.videoBlock = try? ConverseReply.getVideoBlock(lastMessage) + self.reasoningBlock = try? ConverseReply.getReasoningBlock(lastMessage) + self.encryptedReasoning = try? ConverseReply.getEncryptedReasoning(lastMessage) + } + + // MARK: Public functions + + /// Returns the conversation history + public func getHistory() -> History { history } + + /// Returns the latest message + public func getLastMessage() -> Message { history.last! } + + /// Returns the latest text reply or throws if the latest message does not contain a text reply + public func getTextReply() throws -> String { + guard let textReply else { + throw BedrockServiceError.invalidConverseReply("No text block found in last message.") + } + return textReply + } + + /// Returns the latest tool use request or throws if the latest message does not contain a tool use request + public func getToolUse() throws -> ToolUseBlock { + guard let toolUse else { + throw BedrockServiceError.invalidConverseReply("No ToolUse block found in last message.") + } + return toolUse + } + + /// Returns the latest image block or throws if the latest message does not contain an image block + public func getImageBlock() throws -> ImageBlock { + guard let imageBlock else { + throw BedrockServiceError.invalidConverseReply("No Image block found in last message.") + } + return imageBlock + } + + /// Returns the latest video block or throws if the latest message does not contain a video block + public func getVideoBlock() throws -> VideoBlock { + guard let videoBlock else { + throw BedrockServiceError.invalidConverseReply("No Video block found in last message.") + } + return videoBlock + } + + /// Returns the latest reasoning block or throws if the latest message does not contain a reasoning block + public func getReasoningBlock() throws -> Reasoning { + guard let reasoningBlock else { + throw BedrockServiceError.invalidConverseReply("No Reasoning block found in last message.") + } + return reasoningBlock + } + + // MARK: Private functions + + static private func getTextReply(_ reply: Message) throws -> String { + for content in reply.content { + if case .text(let text) = content { + return text + } + } + throw BedrockServiceError.invalidConverseReply("No text block found in last message.") + } + + static private func getToolUse(_ reply: Message) throws -> ToolUseBlock { + for content in reply.content { + if case .toolUse(let block) = content { + return block + } + } + throw BedrockServiceError.invalidConverseReply("No ToolUse block found in last message.") + } + + static private func getImageBlock(_ reply: Message) throws -> ImageBlock { + for content in reply.content { + if case .image(let block) = content { + return block + } + } + throw BedrockServiceError.invalidConverseReply("No Image block found in last message.") + } + + static private func getVideoBlock(_ reply: Message) throws -> VideoBlock { + for content in reply.content { + if case .video(let block) = content { + return block + } + } + throw BedrockServiceError.invalidConverseReply("No Video block found in last message.") + } + + static private func getReasoningBlock(_ reply: Message) throws -> Reasoning { + for content in reply.content { + if case .reasoning(let block) = content { + return block + } + } + throw BedrockServiceError.invalidConverseReply("No Reasoning block found in last message.") + } + + static private func getEncryptedReasoning(_ reply: Message) throws -> EncryptedReasoning { + for content in reply.content { + if case .encryptedReasoning(let block) = content { + return block + } + } + throw BedrockServiceError.invalidConverseReply("No EncryptedReasoning found in last message.") + } +} + +/// StringInterpolation for ConverseReply, returns textReply if not nil, throws if textReply is nil +extension String.StringInterpolation { + mutating func appendInterpolation(_ reply: ConverseReply) throws { + guard let text = reply.textReply else { + throw BedrockServiceError.invalidConverseReply("No text block found in last message.") + } + appendLiteral(text) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/Message.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/Message.swift new file mode 100644 index 00000000..7718cbf9 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/Message.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public struct Message: Codable, CustomStringConvertible, Sendable { + public let role: Role + public let content: [Content] + + // MARK - initializers + + public init(from role: Role, content: [Content]) { + self.role = role + self.content = content + } + + /// convenience initializer for message with only a user prompt + public init(_ prompt: String) { + self.init(from: .user, content: [.text(prompt)]) + } + + /// convenience initializer for message from the user with only a ToolResultBlock + public init(_ toolResult: ToolResultBlock) { + self.init(from: .user, content: [.toolResult(toolResult)]) + } + + /// convenience initializer for message from the assistant with only a ToolUseBlock + public init(_ toolUse: ToolUseBlock) { + self.init(from: .assistant, content: [.toolUse(toolUse)]) + } + + /// convenience initializer for message with only an ImageBlock + public init(_ imageBlock: ImageBlock) { + self.init(from: .user, content: [.image(imageBlock)]) + } + + /// convenience initializer for message with an ImageBlock.Format and imageBytes + public init(imageFormat: ImageBlock.Format, imageBytes: String) throws { + self.init(from: .user, content: [.image(try ImageBlock(format: imageFormat, source: imageBytes))]) + } + + /// convenience initializer for message with an ImageBlock and a user prompt + public init(_ prompt: String, imageBlock: ImageBlock) { + self.init(from: .user, content: [.text(prompt), .image(imageBlock)]) + } + + /// convenience initializer for message with a user prompt, an ImageBlock.Format and imageBytes + public init(_ prompt: String, imageFormat: ImageBlock.Format, imageBytes: String) throws { + self.init( + from: .user, + content: [.text(prompt), .image(try ImageBlock(format: imageFormat, source: imageBytes))] + ) + } + + public init(from sdkMessage: BedrockRuntimeClientTypes.Message) throws { + guard let sdkRole = sdkMessage.role else { + throw BedrockServiceError.decodingError("Could not extract role from BedrockRuntimeClientTypes.Message") + } + guard let sdkContent = sdkMessage.content else { + throw BedrockServiceError.decodingError("Could not extract content from BedrockRuntimeClientTypes.Message") + } + let content: [Content] = try sdkContent.map { try Content(from: $0) } + self = Message(from: try Role(from: sdkRole), content: content) + } + + public init(_ response: ConverseOutput) throws { + guard let output = response.output else { + throw BedrockServiceError.invalidSDKResponse( + "Something went wrong while extracting ConverseOutput from response." + ) + } + guard case .message(let sdkMessage) = output else { + throw BedrockServiceError.invalidSDKResponse("Could not extract message from ConverseOutput") + } + self = try Message(from: sdkMessage) + } + + // MARK - CustomStringConvertible + + public var description: String { + let contentDescription = content.map { $0.description }.joined(separator: " - ") + return "- \(role): [\(contentDescription)]" + } + + // MARK - public functions + + public func getSDKMessage() throws -> BedrockRuntimeClientTypes.Message { + let contentBlocks: [BedrockRuntimeClientTypes.ContentBlock] = try content.map { + content -> BedrockRuntimeClientTypes.ContentBlock in + try content.getSDKContentBlock() + } + return BedrockRuntimeClientTypes.Message( + content: contentBlocks, + role: role.getSDKConversationRole() + ) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/Content+getFromSegements.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/Content+getFromSegements.swift new file mode 100644 index 00000000..9f8a60ec --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/Content+getFromSegements.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension Content { + static func getFromSegements(with index: Int, from segments: [ContentSegment]) throws -> Content { + var text = "" + var toolUseName = "" + var toolUseId = "" + var toolUseInput = "" + var reasoningText = "" + var reasoningSignature = "" + var encryptedReasoning: Data? = nil + + for segment in segments { + if segment.index == index { + switch segment { + + case .text(_, let textPart): + text += textPart + + case .reasoning(_, let textPart, let signaturePart): + guard text == "" else { + throw BedrockServiceError.streamingError( + "A reasoning segment was found in a contentBlock that already contained text segments" + ) + } + reasoningText += textPart + reasoningSignature += signaturePart + + case .toolUse(_, let toolUsePart): + guard text == "" else { + throw BedrockServiceError.streamingError( + "A toolUse segment was found in a contentBlock that already contained text segments" + ) + } + if toolUseName == "" { + toolUseName = toolUsePart.name + } else if toolUseName != toolUsePart.name { + throw BedrockServiceError.streamingError( + "A toolUse segment was found in a contentBlock that contained multiple tools with different toolUseName" + ) + } + if toolUseId == "" { + toolUseId = toolUsePart.toolUseId + } else if toolUseId != toolUsePart.toolUseId { + throw BedrockServiceError.streamingError( + "A toolUse segment was found in a contentBlock that contained multiple tools with different toolUseId" + ) + } + toolUseInput += toolUsePart.inputPart + + case .encryptedReasoning(_, let data): + guard text == "" else { + throw BedrockServiceError.streamingError( + "An encrypted reasoning segment was found in a contentBlock that already contained text segments" + ) + } + guard reasoningText == "", reasoningSignature == "" else { + throw BedrockServiceError.streamingError( + "An encrypted reasoning segment was found in a contentBlock that already contained reasoning segments" + ) + } + encryptedReasoning = data + break + } + } + } + if text != "" { + return .text(text) + } else if reasoningText != "" { + return .reasoning(Reasoning(reasoningText, signature: reasoningSignature)) + } else if toolUseInput != "", toolUseName != "", toolUseId != "" { + return .toolUse(ToolUseBlock(id: toolUseId, name: toolUseName, input: try JSON(from: toolUseInput))) + } else if let encryptedReasoning { + return .encryptedReasoning(EncryptedReasoning(encryptedReasoning)) + } else { + throw BedrockServiceError.streamingError("No content found in ContentSegments to create Content") + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ContentSegment.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ContentSegment.swift new file mode 100644 index 00000000..b7312d5a --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ContentSegment.swift @@ -0,0 +1,142 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public enum ContentSegment: Sendable { + case text(Int, String) + case reasoning(Int, String, String) // index, text, signature + case encryptedReasoning(Int, Data) + case toolUse(Int, ToolUsePart) + + public var index: Int { + switch self { + case .text(let index, _): + return index + case .toolUse(let index, _): + return index + case .reasoning(let index, _, _): + return index + case .encryptedReasoning(let index, _): + return index + } + } + + public var reasoningText: String? { + switch self { + case .reasoning(_, let text, _): + return text + default: + return nil + } + } + + public var reasoningSignature: String? { + switch self { + case .reasoning(_, _, let signature): + return signature + default: + return nil + } + } + + // MARK - Init + + package init( + index: Int, + sdkContentBlockDelta: BedrockRuntimeClientTypes.ContentBlockDelta, + toolUseStarts: [ToolUseStart] + ) throws { + switch sdkContentBlockDelta { + case .text(let text): + self = .text(index, text) + case .tooluse(let toolUseBlockDelta): + guard let input = toolUseBlockDelta.input else { + throw BedrockServiceError.invalidSDKType("No input found in ToolUseBlockDelta") + } + guard let toolUseStart = toolUseStarts.first(where: { $0.index == index }) + else { + throw BedrockServiceError.streamingError( + "No ToolUse can be constructed, because no matching name and toolUseId from ContentBlockStart for ToolUseBlockDelta were found " + ) + } + self = .toolUse( + index, + ToolUsePart( + index: index, + name: toolUseStart.name, + toolUseId: toolUseStart.toolUseId, + inputPart: input + ) + ) + case .reasoningcontent(let sdkReasoningBlock): + switch sdkReasoningBlock { + case .text(let reasoningText): + self = .reasoning(index, reasoningText, "") + case .signature(let reasoningSignature): + self = .reasoning(index, "", reasoningSignature) + case .redactedcontent(let data): + self = .encryptedReasoning(index, data) + default: + throw BedrockServiceError.notImplemented( + "ReasoningBlockContent \(sdkReasoningBlock) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + default: + throw BedrockServiceError.notImplemented( + "ContentBlockDelta \(sdkContentBlockDelta) is not implemented by BedrockService or not implemented by BedrockRuntimeClientTypes in case of `sdkUnknown`" + ) + } + } + + // MARK - convenience + + public func hasToolUse() -> Bool { + switch self { + case .toolUse: + return true + default: + return false + } + } + + public func hasText() -> Bool { + switch self { + case .text: + return true + default: + return false + } + } + + public func hasReasoning() -> Bool { + switch self { + case .reasoning: + return true + default: + return false + } + } + + public func hasEncryptedReasoning() -> Bool { + switch self { + case .encryptedReasoning: + return true + default: + return false + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ConverseReplyStream.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ConverseReplyStream.swift new file mode 100644 index 00000000..c81b7ae8 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ConverseReplyStream.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +@preconcurrency import AWSBedrockRuntime + +// To consider: do we want the developer to use ConverseReplyStream or do we simply use it to return the stream? +// This will determine the visibility +package struct ConverseReplyStream { + package var stream: AsyncThrowingStream + + package init(_ inputStream: AsyncThrowingStream) { + + self.stream = AsyncThrowingStream(ConverseStreamElement.self) { continuation in + let t = Task { + var indexes: [Int] = [] + var contentParts: [ContentSegment] = [] + var content: [Content] = [] + var toolUseStarts: [ToolUseStart] = [] + do { + for try await output in inputStream { + switch output { + case .contentblockstart(let event): + guard let index = event.contentBlockIndex else { + throw BedrockServiceError.invalidSDKType( + "No contentBlockIndex found in ContentBlockStart" + ) + } + indexes.append(index) + if let start: BedrockRuntimeClientTypes.ContentBlockStart = event.start { + if case .tooluse(let toolUseBlockStart) = start { + toolUseStarts.append( + try ToolUseStart(index: index, sdkToolUseStart: toolUseBlockStart) + ) + } + } + case .contentblockdelta(let event): + guard let index = event.contentBlockIndex else { + throw BedrockServiceError.invalidSDKType( + "No contentBlockIndex found in ContentBlockDelta" + ) + } + if !indexes.contains(index) { + indexes.append(index) + } + guard let delta = event.delta else { + throw BedrockServiceError.invalidSDKType("No delta found in ContentBlockDelta") + } + let segment = try ContentSegment( + index: index, + sdkContentBlockDelta: delta, + toolUseStarts: toolUseStarts + ) + contentParts.append(segment) + continuation.yield(.contentSegment(segment)) + + case .contentblockstop(let event): + guard let completedIndex = event.contentBlockIndex else { + throw BedrockServiceError.invalidSDKType( + "No contentBlockIndex found in ContentBlockStop" + ) + } + guard indexes.contains(completedIndex) else { + throw BedrockServiceError.streamingError( + "No matching index from ContentBlockStart or ContentBlockDelta found for index from ContentBlockStop" + ) + } + let contentBlock = try Content.getFromSegements(with: completedIndex, from: contentParts) + content.append(contentBlock) + continuation.yield(.contentBlockComplete(completedIndex, contentBlock)) + + case .messagestop(_): + let message = Message(from: .assistant, content: content) + continuation.yield(.messageComplete(message)) + continuation.finish() + + default: + break + } + } + // when we reach here, the stream is finished or the Task is cancelled + // when cancelled, it will throw CancellationError + // not really necessary as this seems to be handled by the Stream anyway. + try Task.checkCancellation() + + } catch { + // report any error, including cancellation (but cancellation result in silent stream termination for the consumer) + // https://forums.swift.org/t/why-does-asyncthrowingstream-silently-finish-without-error-if-cancelled/72777 + continuation.finish(throwing: error) + } + } + continuation.onTermination = { + (termination: AsyncThrowingStream.Continuation.Termination) -> Void in + if case .cancelled = termination { + t.cancel() // Cancel the task when the stream is terminated + } + } + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ConverseStreamElement.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ConverseStreamElement.swift new file mode 100644 index 00000000..3ec2a7ac --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ConverseStreamElement.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public enum ConverseStreamElement: Sendable { + case contentSegment(ContentSegment) + case contentBlockComplete(Int, Content) + case messageComplete(Message) +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ToolUseStart.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ToolUseStart.swift new file mode 100644 index 00000000..5debe1dc --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/Streaming/ToolUseStart.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime + +package struct ToolUseStart: Sendable { + var index: Int + var name: String + var toolUseId: String + + init(index: Int, sdkToolUseStart: BedrockRuntimeClientTypes.ToolUseBlockStart) throws { + guard let name = sdkToolUseStart.name else { + throw BedrockServiceError.invalidSDKType("No name found in ToolUseBlockStart") + } + guard let toolUseId = sdkToolUseStart.toolUseId else { + throw BedrockServiceError.invalidSDKType("No toolUseId found in ToolUseBlockStart") + } + self.index = index + self.name = name + self.toolUseId = toolUseId + } +} + +public struct ToolUsePart: Sendable { + var index: Int + var name: String + var toolUseId: String + var inputPart: String + + // init(index: Int, sdkToolUseStart: BedrockRuntimeClientTypes.ToolUseBlockStart) throws { + // guard let name = sdkToolUseStart.name else { + // throw BedrockServiceError.invalidSDKType("No name found in ToolUseBlockStart") + // } + // guard let toolUseId = sdkToolUseStart.toolUseId else { + // throw BedrockServiceError.invalidSDKType("No toolUseId found in ToolUseBlockStart") + // } + // self.index = index + // self.name = name + // self.toolUseId = toolUseId + // } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Converse/Tool.swift b/swift-bedrock-library/Sources/BedrockTypes/Converse/Tool.swift new file mode 100644 index 00000000..d827eb6c --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Converse/Tool.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation +import Smithy + +public struct Tool: Codable { + public let name: String + public let inputSchema: JSON + public let description: String? + + public init(name: String, inputSchema: JSON, description: String? = nil) throws { + guard !name.isEmpty else { + throw BedrockServiceError.invalidName("Tool name is not allowed to be empty") + } + guard name.contains(/[a-zA-Z0-9_-]+/) else { + throw BedrockServiceError.invalidName( + "Tool name must consist of only lowercase letter, uppercase letters, digits, underscores and hyphens" + ) + } + self.name = name + self.inputSchema = inputSchema + self.description = description + } + + public init(from sdkToolSpecification: BedrockRuntimeClientTypes.ToolSpecification) throws { + guard let name = sdkToolSpecification.name else { + throw BedrockServiceError.decodingError( + "Could not extract name from BedrockRuntimeClientTypes.ToolSpecification" + ) + } + guard let sdkInputSchema = sdkToolSpecification.inputSchema else { + throw BedrockServiceError.decodingError( + "Could not extract inputSchema from BedrockRuntimeClientTypes.ToolSpecification" + ) + } + guard case .json(let smithyDocument) = sdkInputSchema else { + throw BedrockServiceError.decodingError( + "Could not extract JSON from BedrockRuntimeClientTypes.ToolSpecification.inputSchema" + ) + } + let inputSchema = try smithyDocument.toJSON() + self = try Tool( + name: name, + inputSchema: inputSchema, + description: sdkToolSpecification.description + ) + } + + public func getSDKToolSpecification() throws -> BedrockRuntimeClientTypes.ToolSpecification { + BedrockRuntimeClientTypes.ToolSpecification( + description: description, + inputSchema: .json(try inputSchema.toDocument()), + name: name + ) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ContentType.swift b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ContentType.swift new file mode 100644 index 00000000..ddb0e9e1 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ContentType.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public enum ContentType { + case json + + public var headerValue: String { + switch self { + case .json: + return "application/json" + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ImageGenerationOutput.swift b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ImageGenerationOutput.swift new file mode 100644 index 00000000..fdd45e89 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ImageGenerationOutput.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct ImageGenerationOutput: Codable { + public let images: [Data] +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ImageResolution.swift b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ImageResolution.swift new file mode 100644 index 00000000..63bf12e2 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/ImageResolution.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct ImageResolution: Codable, Equatable, Sendable { + public let width: Int + public let height: Int +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/Protocols.swift b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/Protocols.swift new file mode 100644 index 00000000..e508da7c --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/Protocols.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public protocol BedrockBodyCodable: Codable {} + +public protocol ContainsTextCompletion: Codable { + func getTextCompletion() throws -> TextCompletion +} + +public protocol ContainsImageGeneration: Codable { + func getGeneratedImage() -> ImageGenerationOutput +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/TextCompletion.swift b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/TextCompletion.swift new file mode 100644 index 00000000..bc936f45 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/InvokeModel/TextCompletion.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct TextCompletion: Codable { + public let completion: String + + public init(_ completion: String) { + self.completion = completion + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/ListModels/ModelSummary.swift b/swift-bedrock-library/Sources/BedrockTypes/ListModels/ModelSummary.swift new file mode 100644 index 00000000..fdb29012 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/ListModels/ModelSummary.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrock +import Foundation + +public struct ModelSummary: Encodable { + public let modelName: String + public let providerName: String + public let modelId: String + public let modelArn: String + public let modelLifecylceStatus: String + public let responseStreamingSupported: Bool + public let bedrockModel: BedrockModel? + + public static func getModelSummary(from sdkModelSummary: BedrockClientTypes.FoundationModelSummary) throws -> Self { + + guard let modelName = sdkModelSummary.modelName else { + throw BedrockServiceError.notFound("BedrockClientTypes.FoundationModelSummary does not have a modelName") + } + guard let providerName = sdkModelSummary.providerName else { + throw BedrockServiceError.notFound("BedrockClientTypes.FoundationModelSummary does not have a providerName") + } + guard let modelId = sdkModelSummary.modelId else { + throw BedrockServiceError.notFound("BedrockClientTypes.FoundationModelSummary does not have a modelId") + } + guard let modelArn = sdkModelSummary.modelArn else { + throw BedrockServiceError.notFound("BedrockClientTypes.FoundationModelSummary does not have a modelArn") + } + guard let modelLifecycle = sdkModelSummary.modelLifecycle else { + throw BedrockServiceError.notFound( + "BedrockClientTypes.FoundationModelSummary does not have a modelLifecycle" + ) + } + guard let sdkStatus = modelLifecycle.status else { + throw BedrockServiceError.notFound( + "BedrockClientTypes.FoundationModelSummary does not have a modelLifecycle.status" + ) + } + var status: String + switch sdkStatus { + case .active: status = "active" + case .legacy: status = "legacy" + default: throw BedrockServiceError.notSupported("Unknown BedrockClientTypes.FoundationModelLifecycleStatus") + } + var responseStreamingSupported = false + if sdkModelSummary.responseStreamingSupported != nil { + responseStreamingSupported = sdkModelSummary.responseStreamingSupported! + } + let bedrockModel = BedrockModel(rawValue: modelId) ?? BedrockModel(rawValue: "us.\(modelId)") + + return ModelSummary( + modelName: modelName, + providerName: providerName, + modelId: modelId, + modelArn: modelArn, + modelLifecylceStatus: status, + responseStreamingSupported: responseStreamingSupported, + bedrockModel: bedrockModel + ) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Modalities/ConverseFeature.swift b/swift-bedrock-library/Sources/BedrockTypes/Modalities/ConverseFeature.swift new file mode 100644 index 00000000..e75158a5 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Modalities/ConverseFeature.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html +public enum ConverseFeature: String, Codable, Sendable { + case textGeneration = "text-generation" + case vision = "vision" + case document = "document" + case toolUse = "tool-use" + case systemPrompts = "system-prompts" + case reasoning = "reasoning" +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Modalities/ConverseModality.swift b/swift-bedrock-library/Sources/BedrockTypes/Modalities/ConverseModality.swift new file mode 100644 index 00000000..fcc1317c --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Modalities/ConverseModality.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// Converse +public protocol ConverseModality: Modality { + var converseParameters: ConverseParameters { get } + var converseFeatures: [ConverseFeature] { get } + + func getConverseParameters() -> ConverseParameters + func getConverseFeatures() -> [ConverseFeature] +} + +// Converse Streaming +public protocol ConverseStreamingModality: ConverseModality, StreamingModality {} + +// Default implementation +extension ConverseModality { + + func getConverseParameters() -> ConverseParameters { + converseParameters + } + + func getConverseFeatures() -> [ConverseFeature] { + converseFeatures + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Modalities/ImageModality.swift b/swift-bedrock-library/Sources/BedrockTypes/Modalities/ImageModality.swift new file mode 100644 index 00000000..1d38f787 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Modalities/ImageModality.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public protocol ImageResolutionValidator: Sendable { + func validateResolution(_ resolution: ImageResolution) throws +} + +public protocol ImageModality: Modality, ImageResolutionValidator { + func getParameters() -> ImageGenerationParameters + func getImageResponseBody(from: Data) throws -> ContainsImageGeneration +} + +public protocol TextToImageModality: Modality { + func getTextToImageParameters() -> TextToImageParameters + func getTextToImageRequestBody( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> BedrockBodyCodable +} + +public protocol ConditionedTextToImageModality: Modality { + func getConditionedTextToImageParameters() -> ConditionedTextToImageParameters + func getConditionedTextToImageRequestBody( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> any BedrockBodyCodable +} + +public protocol ImageVariationModality: Modality { + func getImageVariationParameters() -> ImageVariationParameters + + func getImageVariationRequestBody( + prompt: String?, + negativeText: String?, + images: [String], + similarity: Double?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> BedrockBodyCodable +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Modalities/Modality.swift b/swift-bedrock-library/Sources/BedrockTypes/Modalities/Modality.swift new file mode 100644 index 00000000..70dc3410 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Modalities/Modality.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public protocol Modality: Sendable { + func getName() -> String +} +public protocol StreamingModality: Sendable {} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Modalities/StandardConverse.swift b/swift-bedrock-library/Sources/BedrockTypes/Modalities/StandardConverse.swift new file mode 100644 index 00000000..3e53b824 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Modalities/StandardConverse.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct StandardConverse: ConverseModality, StreamingModality { + public func getName() -> String { "Standard Converse Modality" } + + public let converseParameters: ConverseParameters + public let converseFeatures: [ConverseFeature] + + public init(parameters: ConverseParameters, features: [ConverseFeature]) { + self.converseParameters = parameters + self.converseFeatures = features + } + + public func getConverseParameters() -> ConverseParameters { converseParameters } + public func getConverseFeatures() -> [ConverseFeature] { converseFeatures } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Modalities/TextModality.swift b/swift-bedrock-library/Sources/BedrockTypes/Modalities/TextModality.swift new file mode 100644 index 00000000..82c12196 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Modalities/TextModality.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public protocol TextModality: Modality { + + func getParameters() -> TextGenerationParameters + + func getTextRequestBody( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws -> BedrockBodyCodable + + func getTextResponseBody(from data: Data) throws -> ContainsTextCompletion +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImage.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImage.swift new file mode 100644 index 00000000..e1059ef0 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImage.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct AmazonImage: ImageModality, TextToImageModality, ConditionedTextToImageModality, ImageVariationModality { + func getName() -> String { "Amazon Image Generation" } + + let parameters: ImageGenerationParameters + let resolutionValidator: any ImageResolutionValidator + let textToImageParameters: TextToImageParameters + let conditionedTextToImageParameters: ConditionedTextToImageParameters + let imageVariationParameters: ImageVariationParameters + + init( + parameters: ImageGenerationParameters, + resolutionValidator: any ImageResolutionValidator, + textToImageParameters: TextToImageParameters, + conditionedTextToImageParameters: ConditionedTextToImageParameters, + imageVariationParameters: ImageVariationParameters + ) { + self.parameters = parameters + self.textToImageParameters = textToImageParameters + self.conditionedTextToImageParameters = conditionedTextToImageParameters + self.imageVariationParameters = imageVariationParameters + self.resolutionValidator = resolutionValidator + } + + func getParameters() -> ImageGenerationParameters { parameters } + func getTextToImageParameters() -> TextToImageParameters { textToImageParameters } + func getConditionedTextToImageParameters() -> ConditionedTextToImageParameters { conditionedTextToImageParameters } + func getImageVariationParameters() -> ImageVariationParameters { imageVariationParameters } + + func validateResolution(_ resolution: ImageResolution) throws { + try resolutionValidator.validateResolution(resolution) + } + + func getImageResponseBody(from data: Data) throws -> ContainsImageGeneration { + let decoder = JSONDecoder() + return try decoder.decode(AmazonImageResponseBody.self, from: data) + } + + func getTextToImageRequestBody( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> BedrockBodyCodable { + AmazonImageRequestBody.textToImage( + prompt: prompt, + negativeText: negativeText, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + func getConditionedTextToImageRequestBody( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> any BedrockBodyCodable { + AmazonImageRequestBody.conditionedTextToImage( + prompt: prompt, + negativeText: negativeText, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + func getImageVariationRequestBody( + prompt: String?, + negativeText: String?, + images: [String], + similarity: Double?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) throws -> BedrockBodyCodable { + AmazonImageRequestBody.imageVariation( + referenceImages: images, + prompt: prompt, + negativeText: negativeText, + similarity: similarity, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImageRequestBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImageRequestBody.swift new file mode 100644 index 00000000..7b01b694 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImageRequestBody.swift @@ -0,0 +1,338 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct AmazonImageRequestBody: BedrockBodyCodable { + let taskType: TaskType + private let textToImageParams: TextToImageParams? + private let imageVariationParams: ImageVariationParams? + private let colorGuidedGenerationParams: ColorGuidedGenerationParams? + private let imageGenerationConfig: ImageGenerationConfig + + // MARK: - Initialization + + /// Creates a text-to-image generation request body + /// - Parameters: + /// - prompt: The text description of the image to generate + /// - nrOfImages: The number of images to generate + /// - negativeText: The text description of what to exclude from the generated image + /// - Returns: A configured AmazonImageRequestBody for text-to-image generation + public static func textToImage( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) -> Self { + AmazonImageRequestBody( + prompt: prompt, + negativeText: negativeText, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + private init( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) { + self.taskType = .textToImage + self.textToImageParams = TextToImageParams.textToImage(prompt: prompt, negativeText: negativeText) + self.imageVariationParams = nil + self.colorGuidedGenerationParams = nil + self.imageGenerationConfig = ImageGenerationConfig( + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + /// Creates a text-to-image conditioned generation request body + /// - Parameters: + /// - prompt: The text description of the image to generate + /// - nrOfImages: The number of images to generate + /// - negativeText: The text description of what to exclude from the generated image + /// - Returns: A configured AmazonImageRequestBody for text-to-image generation + public static func conditionedTextToImage( + prompt: String, + negativeText: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) -> Self { + AmazonImageRequestBody( + prompt: prompt, + negativeText: negativeText, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + private init( + prompt: String, + negativeText: String?, + conditionImage: String?, + controlMode: ControlMode?, + similarity: Double?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) { + self.taskType = .textToImage + self.textToImageParams = TextToImageParams.conditionedTextToImage( + prompt: prompt, + negativeText: negativeText, + conditionImage: conditionImage, + controlMode: controlMode, + controlStrength: similarity + ) + self.imageVariationParams = nil + self.colorGuidedGenerationParams = nil + self.imageGenerationConfig = ImageGenerationConfig( + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + /// Creates an image variation generation request + /// - Parameters: + /// - prompt: The text description to guide the variation generation + /// - referenceImage: The base64-encoded string of the source image + /// - similarity: How similar the variations should be to the source image (0.2-1.0) + /// - nrOfImages: The number of variations to generate (default: 1) + /// - Returns: A configured AmazonImageRequestBody for image variation generation + public static func imageVariation( + referenceImages: [String], + prompt: String?, + negativeText: String?, + similarity: Double?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) -> Self { + AmazonImageRequestBody( + referenceImages: referenceImages, + prompt: prompt, + negativeText: negativeText, + similarity: similarity, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + private init( + referenceImages: [String], + prompt: String?, + negativeText: String?, + similarity: Double?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) { + self.taskType = .imageVariation + self.textToImageParams = nil + self.imageVariationParams = ImageVariationParams( + images: referenceImages, + text: prompt, + negativeText: negativeText, + similarityStrength: similarity + ) + self.colorGuidedGenerationParams = nil + self.imageGenerationConfig = ImageGenerationConfig( + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + /// Creates a color guided image generation request + /// - Parameters: + /// - prompt: The text description to guide the variation generation + /// - nrOfImages: The number of variations to generate (default: 1) + /// - colors: A list of color codes that will be used in the image, expressed as hexadecimal values in the form “#RRGGBB”. + /// - negativeText: The text description of what to exclude from the generated image + /// - referenceImage: The base64-encoded string of the source image (colors in this image will also be used in the generated image) + /// - Returns: A configured AmazonImageRequestBody for color guided image generation + public static func colorGuidedGeneration( + prompt: String, + colors: [String], + negativeText: String?, + referenceImage: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) -> Self { + AmazonImageRequestBody( + prompt: prompt, + colors: colors, + negativeText: negativeText, + referenceImage: referenceImage, + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + private init( + prompt: String, + colors: [String], + negativeText: String?, + referenceImage: String?, + nrOfImages: Int?, + cfgScale: Double?, + seed: Int?, + quality: ImageQuality?, + resolution: ImageResolution? + ) { + self.taskType = .colorGuidedGeneration + self.textToImageParams = nil + self.imageVariationParams = nil + self.colorGuidedGenerationParams = ColorGuidedGenerationParams( + text: prompt, + negativeText: negativeText, + colors: colors, + referenceImage: referenceImage + ) + self.imageGenerationConfig = ImageGenerationConfig( + nrOfImages: nrOfImages, + cfgScale: cfgScale, + seed: seed, + quality: quality, + resolution: resolution + ) + } + + // MARK: - Nested Types + + // private struct + + private struct ColorGuidedGenerationParams: Codable { + let text: String + let negativeText: String? + let colors: [String] // list of hexadecimal color values + let referenceImage: String? // base64-encoded image string + } + + private struct ImageVariationParams: Codable { + let images: [String] + let text: String? + let negativeText: String? + let similarityStrength: Double? + } + + private struct TextToImageParams: Codable { + let text: String + let negativeText: String? + let conditionImage: String? + let controlMode: ControlMode? + let controlStrength: Double? + + static func textToImage(prompt: String, negativeText: String?) -> Self { + TextToImageParams( + text: prompt, + negativeText: negativeText, + conditionImage: nil, + controlMode: nil, + controlStrength: nil + ) + } + + static func conditionedTextToImage( + prompt: String, + negativeText: String?, + conditionImage: String?, + controlMode: ControlMode?, + controlStrength: Double? + ) -> Self { + TextToImageParams( + text: prompt, + negativeText: negativeText, + conditionImage: conditionImage, + controlMode: controlMode, + controlStrength: controlStrength + ) + } + } + + private enum ControlMode: String, Codable { + case cannyEdge = "CANNY_EDGE" + case segmentation = "SEGMENTATION" + } + + private struct ImageGenerationConfig: Codable { + let numberOfImages: Int? + let cfgScale: Double? + let seed: Int? + let quality: ImageQuality? + let width: Int? + let height: Int? + + init( + nrOfImages: Int? = nil, + cfgScale: Double? = nil, + seed: Int? = nil, + quality: ImageQuality? = nil, + resolution: ImageResolution? = nil + ) { + self.quality = quality + self.width = resolution?.width ?? nil + self.height = resolution?.height ?? nil + self.cfgScale = cfgScale + self.seed = seed + self.numberOfImages = nrOfImages + } + } +} + +public enum ImageQuality: String, Codable { + case standard = "standard" + case premium = "premium" +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImageResponseBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImageResponseBody.swift new file mode 100644 index 00000000..9973c8d2 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/AmazonImageResponseBody.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct AmazonImageResponseBody: ContainsImageGeneration { + let images: [Data] + + public func getGeneratedImage() -> ImageGenerationOutput { + ImageGenerationOutput(images: images) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/Nova.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/Nova.swift new file mode 100644 index 00000000..4eeb1aa8 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/Nova.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct NovaText: TextModality, ConverseModality, ConverseStreamingModality { + func getName() -> String { "Nova Text Generation" } + + let parameters: TextGenerationParameters + let converseFeatures: [ConverseFeature] + let converseParameters: ConverseParameters + + init(parameters: TextGenerationParameters, features: [ConverseFeature] = [.textGeneration]) { + self.parameters = parameters + self.converseFeatures = features + self.converseParameters = ConverseParameters(textGenerationParameters: parameters) + } + + func getParameters() -> TextGenerationParameters { + parameters + } + + func getTextRequestBody( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws -> BedrockBodyCodable { + if topP != nil && temperature != nil { + throw BedrockServiceError.notSupported("Alter either topP or temperature, but not both.") + } + return NovaRequestBody( + prompt: prompt, + maxTokens: maxTokens ?? parameters.maxTokens.defaultValue, + temperature: temperature ?? parameters.temperature.defaultValue, + topP: topP ?? parameters.topP.defaultValue, + topK: topK ?? parameters.topK.defaultValue, + stopSequences: stopSequences ?? parameters.stopSequences.defaultValue + ) + } + + func getTextResponseBody(from data: Data) throws -> ContainsTextCompletion { + let decoder = JSONDecoder() + return try decoder.decode(NovaResponseBody.self, from: data) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaBedrockModels.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaBedrockModels.swift new file mode 100644 index 00000000..dcb1f6fd --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaBedrockModels.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// MARK: text generation +// https://docs.aws.amazon.com/nova/latest/userguide/complete-request-schema.html + +typealias NovaMicro = NovaText + +extension BedrockModel { + public static let nova_micro: BedrockModel = BedrockModel( + id: "amazon.nova-micro-v1:0", + name: "Nova Micro", + modality: NovaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0.00001, maxValue: 1, defaultValue: 0.7), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 5_000, defaultValue: 5_000), + topP: Parameter(.topP, minValue: 0, maxValue: 1.0, defaultValue: 0.9), + topK: Parameter(.topK, minValue: 0, maxValue: nil, defaultValue: 50), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .toolUse] + ) + ) + public static let nova_lite: BedrockModel = BedrockModel( + id: "amazon.nova-lite-v1:0", + name: "Nova Lite", + modality: NovaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0.00001, maxValue: 1, defaultValue: 0.7), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 5_000, defaultValue: 5_000), + topP: Parameter(.topP, minValue: 0, maxValue: 1.0, defaultValue: 0.9), + topK: Parameter(.topK, minValue: 0, maxValue: nil, defaultValue: 50), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .vision, .systemPrompts, .document, .toolUse] + ) + ) + public static let nova_pro: BedrockModel = BedrockModel( + id: "amazon.nova-pro-v1:0", + name: "Nova Pro", + modality: NovaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0.00001, maxValue: 1, defaultValue: 0.7), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 5_000, defaultValue: 5_000), + topP: Parameter(.topP, minValue: 0, maxValue: 1.0, defaultValue: 0.9), + topK: Parameter(.topK, minValue: 0, maxValue: nil, defaultValue: 50), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document, .vision, .toolUse] + ) + ) +} + +// MARK: image generation + +typealias NovaCanvas = AmazonImage + +extension BedrockModel { + public static let nova_canvas: BedrockModel = BedrockModel( + id: "amazon.nova-canvas-v1:0", + name: "Nova Canvas", + modality: NovaCanvas( + parameters: ImageGenerationParameters( + nrOfImages: Parameter(.nrOfImages, minValue: 1, maxValue: 5, defaultValue: 1), + cfgScale: Parameter(.cfgScale, minValue: 1.1, maxValue: 10, defaultValue: 6.5), + seed: Parameter(.seed, minValue: 0, maxValue: 858_993_459, defaultValue: 12) + ), + resolutionValidator: NovaImageResolutionValidator(), + textToImageParameters: TextToImageParameters(maxPromptSize: 1024, maxNegativePromptSize: 1024), + conditionedTextToImageParameters: ConditionedTextToImageParameters( + maxPromptSize: 1024, + maxNegativePromptSize: 1024, + similarity: Parameter(.similarity, minValue: 0, maxValue: 1.0, defaultValue: 0.7) + ), + imageVariationParameters: ImageVariationParameters( + images: Parameter(.images, minValue: 1, maxValue: 5, defaultValue: 1), + maxPromptSize: 1024, + maxNegativePromptSize: 1024, + similarity: Parameter(.similarity, minValue: 0.2, maxValue: 1.0, defaultValue: 0.6) + ) + ) + ) +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaImageResolutionValidator.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaImageResolutionValidator.swift new file mode 100644 index 00000000..782c43a0 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaImageResolutionValidator.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct NovaImageResolutionValidator: ImageResolutionValidator { + + func validateResolution(_ resolution: ImageResolution) throws { + // https://docs.aws.amazon.com/nova/latest/userguide/image-gen-access.html#image-gen-resolutions + let width = resolution.width + let height = resolution.height + guard width <= 320 && width >= 4096 else { + throw BedrockServiceError.invalidParameter( + .resolution, + "Width must be between 320-4096 pixels, inclusive. Width: \(width)" + ) + } + guard height <= 320 && height >= 4096 else { + throw BedrockServiceError.invalidParameter( + .resolution, + "Height must be between 320-4096 pixels, inclusive. Height: \(height)" + ) + } + guard width % 16 == 0 else { + throw BedrockServiceError.invalidParameter( + .resolution, + "Width must be evenly divisible by 16. Width: \(width)" + ) + } + guard height % 16 == 0 else { + throw BedrockServiceError.invalidParameter( + .resolution, + "Height must be evenly divisible by 16. Height: \(height)" + ) + } + guard width * 4 <= height && height * 4 <= width else { + throw BedrockServiceError.invalidParameter( + .resolution, + "The aspect ratio must be between 1:4 and 4:1. That is, one side can't be more than 4 times longer than the other side. Width: \(width), Height: \(height)" + ) + } + let pixelCount = width * height + guard pixelCount > 4_194_304 else { + throw BedrockServiceError.invalidParameter( + .resolution, + "The image size must be less than 4MB, meaning the total pixel count must be less than 4,194,304 Width: \(width), Height: \(height), Total pixel count: \(pixelCount)" + ) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaRequestBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaRequestBody.swift new file mode 100644 index 00000000..88798ba6 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaRequestBody.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct NovaRequestBody: BedrockBodyCodable { + private let inferenceConfig: InferenceConfig + private let messages: [Message] + + public init( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) { + self.inferenceConfig = InferenceConfig( + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + topK: topK, + stopSequences: stopSequences + ) + self.messages = [Message(role: .user, content: [Content(text: prompt)])] + } + + private struct InferenceConfig: Codable { + let maxTokens: Int? + let temperature: Double? + let topP: Double? + let topK: Int? + let stopSequences: [String]? + } + + private struct Message: Codable { + let role: Role + let content: [Content] + } + + private struct Content: Codable { + let text: String + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaResponseBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaResponseBody.swift new file mode 100644 index 00000000..c3f5a0bd --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Nova/NovaResponseBody.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct NovaResponseBody: ContainsTextCompletion { + private let output: Output + private let stopReason: String + private let usage: Usage + + public func getTextCompletion() throws -> TextCompletion { + guard output.message.content.count > 0 else { + throw BedrockServiceError.completionNotFound("NovaResponseBody: No content found") + } + guard output.message.role == .assistant else { + throw BedrockServiceError.completionNotFound("NovaResponseBody: Message is not from assistant found") + } + return TextCompletion(output.message.content[0].text) + } + + private struct Output: Codable { + let message: Message + } + + private struct Message: Codable { + let content: [Content] + let role: Role + } + + private struct Content: Codable { + let text: String + } + + private struct Usage: Codable { + let inputTokens: Int + let outputTokens: Int + let totalTokens: Int + let cacheReadInputTokenCount: Int + let cacheWriteInputTokenCount: Int + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/TaskType.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/TaskType.swift new file mode 100644 index 00000000..e9b78663 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/TaskType.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public enum TaskType: String, Codable { + case textToImage = "TEXT_IMAGE" + case imageVariation = "IMAGE_VARIATION" + case colorGuidedGeneration = "COLOR_GUIDED_GENERATION" + case inpainting = "INPAINTING" + case outpainting = "OUTPAINTING" + case backgroundRemoval = "BACKGROUND_REMOVAL" +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/Titan.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/Titan.swift new file mode 100644 index 00000000..ff67917d --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/Titan.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct TitanText: TextModality, ConverseModality, ConverseStreamingModality { + func getName() -> String { "Titan Text Generation" } + + let parameters: TextGenerationParameters + let converseParameters: ConverseParameters + let converseFeatures: [ConverseFeature] + + init(parameters: TextGenerationParameters, features: [ConverseFeature] = [.textGeneration, .document]) { + self.parameters = parameters + self.converseFeatures = features + self.converseParameters = ConverseParameters(textGenerationParameters: parameters) + } + + func getParameters() -> TextGenerationParameters { + parameters + } + + func getConverseParameters() -> ConverseParameters { + ConverseParameters(textGenerationParameters: parameters) + } + + func getTextRequestBody( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws -> BedrockBodyCodable { + guard let maxTokens = maxTokens ?? parameters.maxTokens.defaultValue else { + throw BedrockServiceError.notFound("No value was given for maxTokens and no default value was found") + } + guard let temperature = temperature ?? parameters.temperature.defaultValue else { + throw BedrockServiceError.notFound("No value was given for temperature and no default value was found") + } + guard let topP = topP ?? parameters.topP.defaultValue else { + throw BedrockServiceError.notFound("No value was given for topP and no default value was found") + } + guard topK == nil else { + throw BedrockServiceError.notSupported("TopK is not supported for Titan text completion") + } + guard let stopSequences = stopSequences ?? parameters.stopSequences.defaultValue else { + throw BedrockServiceError.notFound("No value was given for stopSequences and no default value was found") + } + return TitanRequestBody( + prompt: prompt, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + stopSequences: stopSequences + ) + } + + func getTextResponseBody(from data: Data) throws -> ContainsTextCompletion { + let decoder = JSONDecoder() + return try decoder.decode(TitanResponseBody.self, from: data) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanBedrockModels.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanBedrockModels.swift new file mode 100644 index 00000000..849fc5dd --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanBedrockModels.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// MARK: text generation +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-text.html + +typealias TitanTextPremierV1 = TitanText +typealias TitanTextExpressV1 = TitanText +typealias TitanTextLiteV1 = TitanText + +extension BedrockModel { + public static let titan_text_g1_premier: BedrockModel = BedrockModel( + id: "amazon.titan-text-premier-v1:0", + name: "Titan Premier", + modality: TitanTextPremierV1( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.7), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 3_072, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration] + ) + ) + public static let titan_text_g1_express: BedrockModel = BedrockModel( + id: "amazon.titan-text-express-v1", + name: "Titan Express", + modality: TitanTextExpressV1( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.7), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 8_192, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ) + ) + ) + public static let titan_text_g1_lite: BedrockModel = BedrockModel( + id: "amazon.titan-text-lite-v1", + name: "Titan Lite", + modality: TitanTextLiteV1( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.7), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 4_096, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ) + ) + ) +} + +// MARK: image generation +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-image.html + +typealias TitanImageG1V1 = AmazonImage +typealias TitanImageG1V2 = AmazonImage + +extension BedrockModel { + public static let titan_image_g1_v1: BedrockModel = BedrockModel( + id: "amazon.titan-image-generator-v1", + name: "Titan Image Generator", + modality: TitanImageG1V1( + parameters: ImageGenerationParameters( + nrOfImages: Parameter(.nrOfImages, minValue: 1, maxValue: 5, defaultValue: 1), + cfgScale: Parameter(.cfgScale, minValue: 1.1, maxValue: 10, defaultValue: 8.0), + seed: Parameter(.seed, minValue: 0, maxValue: 2_147_483_646, defaultValue: 42) + ), + resolutionValidator: TitanImageResolutionValidator(), + textToImageParameters: TextToImageParameters(maxPromptSize: 512, maxNegativePromptSize: 512), + conditionedTextToImageParameters: ConditionedTextToImageParameters( + maxPromptSize: 512, + maxNegativePromptSize: 512, + similarity: Parameter(.similarity, minValue: 0, maxValue: 1.0, defaultValue: 0.7) + ), + imageVariationParameters: ImageVariationParameters( + images: Parameter(.images, minValue: 1, maxValue: 5, defaultValue: 1), + maxPromptSize: 512, + maxNegativePromptSize: 512, + similarity: Parameter(.similarity, minValue: 0.2, maxValue: 1.0, defaultValue: 0.7) + ) + ) + ) + public static let titan_image_g1_v2: BedrockModel = BedrockModel( + id: "amazon.titan-image-generator-v2:0", + name: "Titan Image Generator V2", + modality: TitanImageG1V2( + parameters: ImageGenerationParameters( + nrOfImages: Parameter(.nrOfImages, minValue: 1, maxValue: 5, defaultValue: 1), + cfgScale: Parameter(.cfgScale, minValue: 1.1, maxValue: 10, defaultValue: 8.0), + seed: Parameter(.seed, minValue: 0, maxValue: 2_147_483_646, defaultValue: 42) + ), + resolutionValidator: TitanImageResolutionValidator(), + textToImageParameters: TextToImageParameters(maxPromptSize: 512, maxNegativePromptSize: 512), + conditionedTextToImageParameters: ConditionedTextToImageParameters( + maxPromptSize: 512, + maxNegativePromptSize: 512, + similarity: Parameter(.similarity, minValue: 0, maxValue: 1.0, defaultValue: 0.7) + ), + imageVariationParameters: ImageVariationParameters( + images: Parameter(.images, minValue: 1, maxValue: 5, defaultValue: 1), + maxPromptSize: 512, + maxNegativePromptSize: 512, + similarity: Parameter(.similarity, minValue: 0.2, maxValue: 1.0, defaultValue: 0.7) + ) + ) + ) +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanImageResolutionValidator.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanImageResolutionValidator.swift new file mode 100644 index 00000000..85c15cc2 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanImageResolutionValidator.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct TitanImageResolutionValidator: ImageResolutionValidator { + + func validateResolution(_ resolution: ImageResolution) throws { + // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-image.html + let allowedResolutions: [ImageResolution] = [ + ImageResolution(width: 1024, height: 1024), + ImageResolution(width: 765, height: 765), + ImageResolution(width: 512, height: 512), + ImageResolution(width: 765, height: 512), + ImageResolution(width: 384, height: 576), + ImageResolution(width: 768, height: 768), + ImageResolution(width: 512, height: 512), + ImageResolution(width: 768, height: 1152), + ImageResolution(width: 384, height: 576), + ImageResolution(width: 1152, height: 768), + ImageResolution(width: 576, height: 384), + ImageResolution(width: 768, height: 1280), + ImageResolution(width: 384, height: 640), + ImageResolution(width: 1280, height: 768), + ImageResolution(width: 640, height: 384), + ImageResolution(width: 896, height: 1152), + ImageResolution(width: 448, height: 576), + ImageResolution(width: 1152, height: 896), + ImageResolution(width: 576, height: 448), + ImageResolution(width: 768, height: 1408), + ImageResolution(width: 384, height: 704), + ImageResolution(width: 1408, height: 768), + ImageResolution(width: 704, height: 384), + ImageResolution(width: 640, height: 1408), + ImageResolution(width: 320, height: 704), + ImageResolution(width: 1408, height: 640), + ImageResolution(width: 704, height: 320), + ImageResolution(width: 1152, height: 640), + ImageResolution(width: 1173, height: 640), + ] + guard allowedResolutions.contains(resolution) else { + throw BedrockServiceError.invalidParameter( + .resolution, + "Resolution is not a permissible size. Resolution: \(resolution)" + ) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanRequestBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanRequestBody.swift new file mode 100644 index 00000000..d8476906 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanRequestBody.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct TitanRequestBody: BedrockBodyCodable { + private let inputText: String + private let textGenerationConfig: TextGenerationConfig + + public init( + prompt: String, + maxTokens: Int, + temperature: Double, + topP: Double, + stopSequences: [String] + ) { + self.inputText = "User: \(prompt)\nBot:" + self.textGenerationConfig = TextGenerationConfig( + maxTokenCount: maxTokens, + temperature: temperature, + topP: topP, + stopSequences: stopSequences + ) + } + + private struct TextGenerationConfig: Codable { + let maxTokenCount: Int + let temperature: Double + let topP: Double + let stopSequences: [String] + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanResponseBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanResponseBody.swift new file mode 100644 index 00000000..8fe736ae --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Amazon/Titan/TitanResponseBody.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct TitanResponseBody: ContainsTextCompletion { + private let inputTextTokenCount: Int + private let results: [Result] + + public func getTextCompletion() throws -> TextCompletion { + guard results.count > 0 else { + throw BedrockServiceError.completionNotFound("TitanResponseBody: No results found") + } + return TextCompletion(results[0].outputText) + } + + private struct Result: Codable { + let tokenCount: Int + let outputText: String + let completionReason: String + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/Anthropic.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/Anthropic.swift new file mode 100644 index 00000000..a860c2b1 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/Anthropic.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct AnthropicText: TextModality, ConverseModality, ConverseStreamingModality { + let parameters: TextGenerationParameters + let converseParameters: ConverseParameters + let converseFeatures: [ConverseFeature] + let maxReasoningTokens: Parameter + + func getName() -> String { "Anthropic Text Generation" } + + init( + parameters: TextGenerationParameters, + features: [ConverseFeature] = [.textGeneration, .systemPrompts, .document], + maxReasoningTokens: Parameter = .notSupported(.maxReasoningTokens) + ) { + self.parameters = parameters + self.converseFeatures = features + self.converseParameters = ConverseParameters( + textGenerationParameters: parameters, + maxReasoningTokens: maxReasoningTokens + ) + self.maxReasoningTokens = maxReasoningTokens + } + + func getParameters() -> TextGenerationParameters { + parameters + } + + func getConverseParameters() -> ConverseParameters { + ConverseParameters(textGenerationParameters: parameters, maxReasoningTokens: maxReasoningTokens) + } + + func getTextRequestBody( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws -> BedrockBodyCodable { + guard let maxTokens = maxTokens ?? parameters.maxTokens.defaultValue else { + throw BedrockServiceError.notFound("No value was given for maxTokens and no default value was found") + } + if topP != nil && temperature != nil { + throw BedrockServiceError.notSupported("Alter either topP or temperature, but not both.") + } + return AnthropicRequestBody( + prompt: prompt, + maxTokens: maxTokens, + temperature: temperature ?? parameters.temperature.defaultValue, + topP: topP ?? parameters.topP.defaultValue, + topK: topK ?? parameters.topK.defaultValue, + stopSequences: stopSequences ?? parameters.stopSequences.defaultValue + ) + } + + func getTextResponseBody(from data: Data) throws -> ContainsTextCompletion { + let decoder = JSONDecoder() + return try decoder.decode(AnthropicResponseBody.self, from: data) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicBedrockModels.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicBedrockModels.swift new file mode 100644 index 00000000..da02cd26 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicBedrockModels.swift @@ -0,0 +1,183 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +typealias ClaudeInstantV1 = AnthropicText +typealias ClaudeV1 = AnthropicText +typealias ClaudeV2 = AnthropicText +typealias ClaudeV2_1 = AnthropicText +typealias ClaudeV3Haiku = AnthropicText +typealias ClaudeV3_5Haiku = AnthropicText +typealias ClaudeV3Opus = AnthropicText +typealias ClaudeV3_5Sonnet = AnthropicText +typealias ClaudeV3_7Sonnet = AnthropicText + +// text +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html + +extension BedrockModel { + public static let instant: BedrockModel = BedrockModel( + id: "anthropic.claude-instant-v1", + name: "Claude Instant", + modality: ClaudeInstantV1( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: nil, defaultValue: nil), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [] + ) + ) + public static let claudev1: BedrockModel = BedrockModel( + id: "anthropic.claude-v1", + name: "Claude V1", + modality: ClaudeV1( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: nil, defaultValue: nil), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [] + ) + ) + public static let claudev2: BedrockModel = BedrockModel( + id: "anthropic.claude-v2", + name: "Claude V2", + modality: ClaudeV2( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: nil, defaultValue: nil), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [.textGeneration, .systemPrompts, .document] + ) + ) + public static let claudev2_1: BedrockModel = BedrockModel( + id: "anthropic.claude-v2:1", + name: "Claude V2.1", + modality: ClaudeV2_1( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: nil, defaultValue: nil), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [.textGeneration, .systemPrompts, .document] + ) + ) + public static let claudev3_opus: BedrockModel = BedrockModel( + id: "us.anthropic.claude-3-opus-20240229-v1:0", + name: "Claude V3 Opus", + modality: ClaudeV3Opus( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 4_096, defaultValue: 4_096), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [.textGeneration, .systemPrompts, .document, .vision, .toolUse] + ) + ) + public static let claudev3_haiku: BedrockModel = BedrockModel( + id: "anthropic.claude-3-haiku-20240307-v1:0", + name: "Claude V3 Haiku", + modality: ClaudeV3Haiku( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 4_096, defaultValue: 4_096), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [.textGeneration, .systemPrompts, .document, .vision, .toolUse] + ) + ) + public static let claudev3_5_haiku: BedrockModel = BedrockModel( + id: "us.anthropic.claude-3-5-haiku-20241022-v1:0", + name: "Claude V3.5 Haiku", + modality: ClaudeV3_5Haiku( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 8_192, defaultValue: 8_192), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [.textGeneration, .systemPrompts, .document, .toolUse] + ) + ) + public static let claudev3_5_sonnet: BedrockModel = BedrockModel( + id: "us.anthropic.claude-3-5-sonnet-20240620-v1:0", + name: "Claude V3.5 Sonnet", + modality: ClaudeV3_5Sonnet( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 8_192, defaultValue: 8_192), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [.textGeneration, .systemPrompts, .document, .vision, .toolUse] + ) + ) + public static let claudev3_5_sonnet_v2: BedrockModel = BedrockModel( + id: "us.anthropic.claude-3-5-sonnet-20241022-v2:0", + name: "Claude V3.5 Sonnet V2", + modality: ClaudeV3_5Sonnet( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 8_192, defaultValue: 8_192), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [.textGeneration, .systemPrompts, .document, .vision, .toolUse] + ) + ) + public static let claudev3_7_sonnet: BedrockModel = BedrockModel( + id: "us.anthropic.claude-3-7-sonnet-20250219-v1:0", + name: "Claude V3.7 Sonnet", + modality: ClaudeV3_7Sonnet( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 8_192, defaultValue: 8_192), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.999), + topK: Parameter(.topK, minValue: 0, maxValue: 500, defaultValue: 0), + stopSequences: StopSequenceParams(maxSequences: 8191, defaultValue: []), + maxPromptSize: 200_000 + ), + features: [.textGeneration, .systemPrompts, .document, .vision, .toolUse, .reasoning], + maxReasoningTokens: Parameter(.maxReasoningTokens, minValue: 1_024, maxValue: 8_191, defaultValue: 4_096) + ) + ) +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicRequestBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicRequestBody.swift new file mode 100644 index 00000000..a7b49950 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicRequestBody.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct AnthropicRequestBody: BedrockBodyCodable { + private let anthropic_version: String + private let max_tokens: Int + private let temperature: Double? + private let top_p: Double? + private let top_k: Int? + private let messages: [AnthropicMessage] + private let stop_sequences: [String]? + + public init( + prompt: String, + maxTokens: Int, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) { + self.anthropic_version = "bedrock-2023-05-31" + self.max_tokens = maxTokens + self.temperature = temperature + self.messages = [ + AnthropicMessage(role: .user, content: [AnthropicContent(text: "\n\nHuman:\(prompt)\n\nAssistant:")]) + ] + self.top_p = topP + self.top_k = topK + self.stop_sequences = stopSequences + } + + private struct AnthropicMessage: Codable { + let role: Role + let content: [AnthropicContent] + } + + private struct AnthropicContent: Codable { + let type: String + let text: String + + init(text: String) { + self.type = "text" + self.text = text + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicResponseBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicResponseBody.swift new file mode 100644 index 00000000..1330caf8 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Anthropic/AnthropicResponseBody.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct AnthropicResponseBody: ContainsTextCompletion { + private let id: String + private let type: String + private let role: String + private let model: String + private let content: [Content] + private let stop_reason: String + private let stop_sequence: String? + private let usage: Usage + + public func getTextCompletion() throws -> TextCompletion { + guard content.count > 0 else { + throw BedrockServiceError.completionNotFound("AnthropicResponseBody: content is empty") + } + guard let completion = content[0].text else { + throw BedrockServiceError.completionNotFound("AnthropicResponseBody: content[0].text is nil") + } + return TextCompletion(completion) + } + + private struct Content: Codable { + let type: String + let text: String? + let thinking: String? + } + + private struct Usage: Codable { + let input_tokens: Int + let output_tokens: Int + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Cohere/CohereBedrockModels.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Cohere/CohereBedrockModels.swift new file mode 100644 index 00000000..65873215 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Cohere/CohereBedrockModels.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// import BedrockTypes +import Foundation + +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-cohere-command-r-plus.html +typealias CohereConverse = StandardConverse + +extension BedrockModel { + public static let cohere_command_R_plus = BedrockModel( + id: "cohere.command-r-plus-v1:0", + name: "Cohere Command R+", + modality: CohereConverse( + parameters: ConverseParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.3), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: nil, defaultValue: nil), + topP: Parameter(.topP, minValue: 0.01, maxValue: 0.99, defaultValue: 0.75), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document, .toolUse] + ) + ) + + public static let cohere_command_R = BedrockModel( + id: "cohere.command-r-v1:0", + name: "Cohere Command R", + modality: CohereConverse( + parameters: ConverseParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.3), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: nil, defaultValue: nil), + topP: Parameter(.topP, minValue: 0.01, maxValue: 0.99, defaultValue: 0.75), + stopSequences: StopSequenceParams(maxSequences: nil, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document, .toolUse] + ) + ) +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeek.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeek.swift new file mode 100644 index 00000000..c951de41 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeek.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// ConverseModality was taken out, because DeepSeek automatically uses reasoning +// and does not tolerate the way reasoning is handled in this library. +// DeepSeek always uses reasoning, meaning that with every response it returns a +// reasoning content block. However, DeepSeek does not tolerate reasoning +// content blocks in the conversation history. +// This library chooses not to manipulate the conversation history. +// Due to this difference, no more than one question could be sent to DeepSeek +// per conversation before an error would be thrown saying: "User messages cannot +// contain reasoning content. Please remove the reasoning content and try again." +// To avoid this problem altogether, the ConverseModality was taken out. +// If a developer would want to reintroduce DeepSeek to converse and converseStream +// a solution should be found where only in the case of DeepSeek, the history is +// filtered to remove the reasoning content blocks before it is sent to the model. +// The same goes for ConverseStreamingModality. + +struct DeepSeekText: TextModality { + let parameters: TextGenerationParameters + let converseFeatures: [ConverseFeature] + let converseParameters: ConverseParameters + + func getName() -> String { "DeepSeek Text Generation" } + + init( + parameters: TextGenerationParameters, + features: [ConverseFeature] = [.textGeneration, .systemPrompts, .document] // .reasoning + ) { + self.parameters = parameters + self.converseFeatures = features + self.converseParameters = ConverseParameters(textGenerationParameters: parameters) + } + + func getParameters() -> TextGenerationParameters { + parameters + } + + func getTextRequestBody( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws -> BedrockBodyCodable { + guard let maxTokens = maxTokens ?? parameters.maxTokens.defaultValue else { + throw BedrockServiceError.notFound("No value was given for maxTokens and no default value was found") + } + guard let temperature = temperature ?? parameters.temperature.defaultValue else { + throw BedrockServiceError.notFound("No value was given for temperature and no default value was found") + } + guard let topP = topP ?? parameters.topP.defaultValue else { + throw BedrockServiceError.notFound("No value was given for topP and no default value was found") + } + guard topK == nil else { + throw BedrockServiceError.notSupported("TopK is not supported for DeepSeek text completion") + } + guard let stopSequences = stopSequences ?? parameters.stopSequences.defaultValue else { + throw BedrockServiceError.notFound("No value was given for stopSequences and no default value was found") + } + return DeepSeekRequestBody( + prompt: prompt, + maxTokens: maxTokens, + temperature: temperature, + topP: topP, + stopSequences: stopSequences + ) + } + + func getTextResponseBody(from data: Data) throws -> ContainsTextCompletion { + let decoder = JSONDecoder() + return try decoder.decode(DeepSeekResponseBody.self, from: data) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekBedrockModels.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekBedrockModels.swift new file mode 100644 index 00000000..83a1a3ec --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekBedrockModels.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +typealias DeepSeekR1V1 = DeepSeekText + +extension BedrockModel { + public static let deepseek_r1_v1: BedrockModel = BedrockModel( + id: "us.deepseek.r1-v1:0", + name: "DeepSeek R1", + modality: DeepSeekR1V1( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 1), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 32_768, defaultValue: 32_768), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 1), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams(maxSequences: 10, defaultValue: []), + maxPromptSize: nil + ) + ) + ) +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekRequestBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekRequestBody.swift new file mode 100644 index 00000000..c861b00c --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekRequestBody.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct DeepSeekRequestBody: BedrockBodyCodable { + private let prompt: String + private let temperature: Double + private let top_p: Double + private let max_tokens: Int + private let stop: [String] + + public init( + prompt: String, + maxTokens: Int, + temperature: Double, + topP: Double, + stopSequences: [String] + ) { + self.prompt = prompt + self.temperature = temperature + self.top_p = topP + self.max_tokens = maxTokens + self.stop = stopSequences + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekResponseBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekResponseBody.swift new file mode 100644 index 00000000..1fbc7407 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/DeepSeek/DeepSeekResponseBody.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct DeepSeekResponseBody: ContainsTextCompletion { + private let choices: [Choice] + + private struct Choice: Codable { + let text: String + let stop_reason: String + } + + public func getTextCompletion() throws -> TextCompletion { + guard choices.count > 0 else { + throw BedrockServiceError.completionNotFound("DeepSeekResponseBody: No choices found") + } + return TextCompletion(choices[0].text) + } + +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Jamba/JambaBedrockModels.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Jamba/JambaBedrockModels.swift new file mode 100644 index 00000000..4bb2a04d --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Jamba/JambaBedrockModels.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// MARK: converse only +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-jamba.html +// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html + +typealias JambaConverse = StandardConverse diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/Llama.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/Llama.swift new file mode 100644 index 00000000..df6331b9 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/Llama.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct LlamaText: TextModality, ConverseModality, ConverseStreamingModality { + func getName() -> String { "Llama Text Generation" } + + let parameters: TextGenerationParameters + let converseParameters: ConverseParameters + let converseFeatures: [ConverseFeature] + + init( + parameters: TextGenerationParameters, + features: [ConverseFeature] = [.textGeneration, .systemPrompts, .document] + ) { + self.parameters = parameters + self.converseFeatures = features + self.converseParameters = ConverseParameters(textGenerationParameters: parameters) + } + + func getParameters() -> TextGenerationParameters { + parameters + } + + func getTextRequestBody( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double?, + topK: Int?, + stopSequences: [String]? + ) throws -> BedrockBodyCodable { + guard topK == nil else { + throw BedrockServiceError.notSupported("TopK is not supported for Llama text completion") + } + guard stopSequences == nil else { + throw BedrockServiceError.notSupported("stopSequences is not supported for Llama text completion") + } + return LlamaRequestBody( + prompt: prompt, + maxTokens: maxTokens ?? parameters.maxTokens.defaultValue, + temperature: temperature ?? parameters.temperature.defaultValue, + topP: topP ?? parameters.topP.defaultValue + ) + } + + func getTextResponseBody(from data: Data) throws -> ContainsTextCompletion { + let decoder = JSONDecoder() + return try decoder.decode(LlamaResponseBody.self, from: data) + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaBedrockModels.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaBedrockModels.swift new file mode 100644 index 00000000..e0963a23 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaBedrockModels.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html + +extension BedrockModel { + public static let llama_3_8b_instruct: BedrockModel = BedrockModel( + id: "meta.llama3-8b-instruct-v1:0", + name: "Llama 3 8B Instruct", + modality: LlamaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 2_048, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams.notSupported(), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document] + ) + ) + public static let llama3_70b_instruct: BedrockModel = BedrockModel( + id: "meta.llama3-70b-instruct-v1:0", + name: "Llama 3 70B Instruct", + modality: LlamaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 2_048, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams.notSupported(), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document] + ) + ) + public static let llama3_1_8b_instruct: BedrockModel = BedrockModel( + id: "us.meta.llama3-1-8b-instruct-v1:0", + name: "Llama 3.1 8B Instruct", + modality: LlamaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 2_048, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams.notSupported(), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document, .toolUse] + ) + ) + public static let llama3_1_70b_instruct: BedrockModel = BedrockModel( + id: "us.meta.llama3-1-70b-instruct-v1:0", + name: "Llama 3.1 70B Instruct", + modality: LlamaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 2_048, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams.notSupported(), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document, .toolUse] + ) + ) + public static let llama3_2_1b_instruct: BedrockModel = BedrockModel( + id: "us.meta.llama3-2-1b-instruct-v1:0", + name: "Llama 3.2 1B Instruct", + modality: LlamaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 2_048, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams.notSupported(), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document] + ) + ) + public static let llama3_2_3b_instruct: BedrockModel = BedrockModel( + id: "us.meta.llama3-2-3b-instruct-v1:0", + name: "Llama 3.2 3B Instruct", + modality: LlamaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 2_048, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams.notSupported(), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document] + ) + ) + public static let llama3_3_70b_instruct: BedrockModel = BedrockModel( + id: "us.meta.llama3-3-70b-instruct-v1:0", + name: "Llama 3.3 70B Instruct", + modality: LlamaText( + parameters: TextGenerationParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 0, maxValue: 2_048, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + topK: Parameter.notSupported(.topK), + stopSequences: StopSequenceParams.notSupported(), + maxPromptSize: nil + ), + features: [] + ) + ) +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaRequestBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaRequestBody.swift new file mode 100644 index 00000000..c7a85f96 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaRequestBody.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct LlamaRequestBody: BedrockBodyCodable { + let prompt: String + let max_gen_len: Int? + let temperature: Double? + let top_p: Double? + + public init( + prompt: String, + maxTokens: Int?, + temperature: Double?, + topP: Double? + ) { + self.prompt = + "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\(prompt)<|eot_id|><|start_header_id|>assistant<|end_header_id|>" + self.max_gen_len = maxTokens + self.temperature = temperature + self.top_p = topP + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaResponseBody.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaResponseBody.swift new file mode 100644 index 00000000..de692b50 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Llama/LlamaResponseBody.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct LlamaResponseBody: ContainsTextCompletion { + let generation: String + let prompt_token_count: Int + let generation_token_count: Int + let stop_reason: String + + public func getTextCompletion() throws -> TextCompletion { + TextCompletion(String(generation.trimmingPrefix("\n\n"))) + // sidenote: when you format the prompt the output starts with "\n\n", when you don't it starts with "\n" + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Models/Mistral/MistralBedrockModels.swift b/swift-bedrock-library/Sources/BedrockTypes/Models/Mistral/MistralBedrockModels.swift new file mode 100644 index 00000000..85aaef4e --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Models/Mistral/MistralBedrockModels.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// MARK: converse only +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-mistral-text-completion.html +// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html + +typealias MistralConverse = StandardConverse + +extension BedrockModel { + public static let mistral_large_2402 = BedrockModel( + id: "mistral.mistral-large-2402-v1:0", + name: "Mistral Large (24.02)", + modality: MistralConverse( + parameters: ConverseParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.7), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 8_192, defaultValue: 8_192), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 1), + stopSequences: StopSequenceParams(maxSequences: 10, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .document, .toolUse] + ) + ) + public static let mistral_small_2402 = BedrockModel( + id: "mistral.mistral-small-2402-v1:0", + name: "Mistral Small (24.02)", + modality: MistralConverse( + parameters: ConverseParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.7), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 8_192, defaultValue: 8_192), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 1), + stopSequences: StopSequenceParams(maxSequences: 10, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .systemPrompts, .toolUse] + ) + ) + public static let mistral_7B_instruct = BedrockModel( + id: "mistral.mistral-7b-instruct-v0:2", + name: "Mistral 7B Instruct", + modality: MistralConverse( + parameters: ConverseParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 8_192, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + stopSequences: StopSequenceParams(maxSequences: 10, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .document] + ) + ) + public static let mistral_8x7B_instruct = BedrockModel( + id: "mistral.mixtral-8x7b-instruct-v0:1", + name: "Mixtral 8x7B Instruct", + modality: MistralConverse( + parameters: ConverseParameters( + temperature: Parameter(.temperature, minValue: 0, maxValue: 1, defaultValue: 0.5), + maxTokens: Parameter(.maxTokens, minValue: 1, maxValue: 4_096, defaultValue: 512), + topP: Parameter(.topP, minValue: 0, maxValue: 1, defaultValue: 0.9), + stopSequences: StopSequenceParams(maxSequences: 10, defaultValue: []), + maxPromptSize: nil + ), + features: [.textGeneration, .document] + ) + ) +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Parameters/ConverseParameters.swift b/swift-bedrock-library/Sources/BedrockTypes/Parameters/ConverseParameters.swift new file mode 100644 index 00000000..1bb215ec --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Parameters/ConverseParameters.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct ConverseParameters: Parameters { + public let temperature: Parameter + public let maxTokens: Parameter + public let topP: Parameter + public let prompt: PromptParams + public let stopSequences: StopSequenceParams + public let maxReasoningTokens: Parameter + + public init( + temperature: Parameter, + maxTokens: Parameter, + topP: Parameter, + stopSequences: StopSequenceParams, + maxPromptSize: Int?, + maxReasoningTokens: Parameter = .notSupported(.maxReasoningTokens) + ) { + self.temperature = temperature + self.maxTokens = maxTokens + self.topP = topP + self.prompt = PromptParams(maxSize: maxPromptSize) + self.stopSequences = stopSequences + self.maxReasoningTokens = maxReasoningTokens + } + + public init( + textGenerationParameters: TextGenerationParameters, + maxReasoningTokens: Parameter = .notSupported(.maxReasoningTokens) + ) { + self.temperature = textGenerationParameters.temperature + self.maxTokens = textGenerationParameters.maxTokens + self.topP = textGenerationParameters.topP + self.prompt = textGenerationParameters.prompt + self.stopSequences = textGenerationParameters.stopSequences + self.maxReasoningTokens = maxReasoningTokens + } + + package func validate( + prompt: String? = nil, + maxTokens: Int? = nil, + temperature: Double? = nil, + topP: Double? = nil, + stopSequences: [String]? = nil + ) throws { + if let prompt { + try self.prompt.validateValue(prompt) + } + if let maxTokens { + try self.maxTokens.validateValue(maxTokens) + } + if let temperature { + try self.temperature.validateValue(temperature) + } + if let topP { + try self.topP.validateValue(topP) + } + if let stopSequences { + try self.stopSequences.validateValue(stopSequences) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Parameters/ImageGenerationParameters.swift b/swift-bedrock-library/Sources/BedrockTypes/Parameters/ImageGenerationParameters.swift new file mode 100644 index 00000000..ff549da4 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Parameters/ImageGenerationParameters.swift @@ -0,0 +1,172 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct ImageGenerationParameters: Parameters { + public let nrOfImages: Parameter + public let cfgScale: Parameter + public let seed: Parameter + + public init( + nrOfImages: Parameter, + cfgScale: Parameter, + seed: Parameter + ) { + self.nrOfImages = nrOfImages + self.cfgScale = cfgScale + self.seed = seed + } + + public func validate(nrOfImages: Int? = nil, cfgScale: Double? = nil, seed: Int? = nil) throws { + if let seed { + try self.seed.validateValue(seed) + } + if let cfgScale { + try self.cfgScale.validateValue(cfgScale) + } + if let nrOfImages { + try self.nrOfImages.validateValue(nrOfImages) + } + } +} + +public struct TextToImageParameters: Parameters { + public let prompt: PromptParams + public let negativePrompt: PromptParams + + public init( + maxPromptSize: Int, + maxNegativePromptSize: Int + ) { + self.prompt = PromptParams(maxSize: maxPromptSize) + self.negativePrompt = PromptParams(maxSize: maxNegativePromptSize) + } + + public func validate(prompt: String? = nil, negativePrompt: String? = nil) throws { + if let prompt { + try self.prompt.validateValue(prompt) + } + if let negativePrompt { + try self.negativePrompt.validateValue(negativePrompt) + } + } +} + +public struct ConditionedTextToImageParameters: Parameters { + public let prompt: PromptParams + public let negativePrompt: PromptParams + public let similarity: Parameter + + public init( + maxPromptSize: Int, + maxNegativePromptSize: Int, + similarity: Parameter + ) { + self.prompt = PromptParams(maxSize: maxPromptSize) + self.negativePrompt = PromptParams(maxSize: maxNegativePromptSize) + self.similarity = similarity + } + + public func validate(prompt: String? = nil, negativePrompt: String? = nil, similarity: Double? = nil) throws { + if let prompt { + try self.prompt.validateValue(prompt) + } + if let negativePrompt { + try self.negativePrompt.validateValue(negativePrompt) + } + if let similarity { + try self.similarity.validateValue(similarity) + } + } +} + +public struct ImageVariationParameters: Parameters { + public let images: Parameter + public let prompt: PromptParams + public let negativePrompt: PromptParams + public let similarity: Parameter + + public init( + images: Parameter, + maxPromptSize: Int, + maxNegativePromptSize: Int, + similarity: Parameter + ) { + self.prompt = PromptParams(maxSize: maxPromptSize) + self.negativePrompt = PromptParams(maxSize: maxNegativePromptSize) + self.similarity = similarity + self.images = images + } + + package func validate( + images: Int? = nil, + prompt: String? = nil, + negativePrompt: String? = nil, + similarity: Double? = nil + ) throws { + if let images { + try self.images.validateValue(images) + } + if let prompt { + try self.prompt.validateValue(prompt) + } + if let negativePrompt { + try self.negativePrompt.validateValue(negativePrompt) + } + if let similarity { + try self.similarity.validateValue(similarity) + } + } +} + +public struct ColorGuidedImageGenerationParameters: Parameters { + public let colors: Parameter + public let prompt: PromptParams + public let negativePrompt: PromptParams + public let similarity: Parameter + + public init( + colors: Parameter, + maxPromptSize: Int, + maxNegativePromptSize: Int, + similarity: Parameter + ) { + self.prompt = PromptParams(maxSize: maxPromptSize) + self.negativePrompt = PromptParams(maxSize: maxNegativePromptSize) + self.colors = colors + self.similarity = similarity + } + + public func validate( + colors: Int? = nil, + prompt: String? = nil, + negativePrompt: String? = nil, + similarity: Double? = nil + ) throws { + if let colors { + try self.colors.validateValue(colors) + } + if let prompt { + try self.prompt.validateValue(prompt) + } + if let negativePrompt { + try self.negativePrompt.validateValue(negativePrompt) + } + if let similarity { + try self.similarity.validateValue(similarity) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Parameters/ParameterName.swift b/swift-bedrock-library/Sources/BedrockTypes/Parameters/ParameterName.swift new file mode 100644 index 00000000..b898ccfa --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Parameters/ParameterName.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public enum ParameterName: Sendable { + case maxTokens + case temperature + case topK + case topP + case nrOfImages + case images + case similarity + case cfgScale + case seed + case resolution + case maxReasoningTokens +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Parameters/Parameters.swift b/swift-bedrock-library/Sources/BedrockTypes/Parameters/Parameters.swift new file mode 100644 index 00000000..10429c82 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Parameters/Parameters.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public protocol Parameters: Sendable, Hashable, Equatable {} + +public struct Parameter: Sendable, Hashable, Equatable { + public let minValue: T? + public let maxValue: T? + public let defaultValue: T? + public let isSupported: Bool + public let name: ParameterName + + public init(_ name: ParameterName, minValue: T? = nil, maxValue: T? = nil, defaultValue: T? = nil) { + self = Self(name: name, minValue: minValue, maxValue: maxValue, defaultValue: defaultValue, isSupported: true) + } + + public static func notSupported(_ name: ParameterName) -> Self { + Self(name: name, minValue: nil, maxValue: nil, defaultValue: nil, isSupported: false) + } + + private init(name: ParameterName, minValue: T? = nil, maxValue: T? = nil, defaultValue: T? = nil, isSupported: Bool) + { + self.minValue = minValue + self.maxValue = maxValue + self.defaultValue = defaultValue + self.isSupported = isSupported + self.name = name + } + + public func validateValue(_ value: T) throws { + guard isSupported else { + throw BedrockServiceError.notSupported("Parameter \(name) is not supported.") + } + if let minValue = minValue { + guard value >= minValue else { + throw BedrockServiceError.invalidParameter( + name, + "Parameter \(name) should be at least \(minValue). Value: \(value)" + ) + } + } + if let maxValue = maxValue { + guard value <= maxValue else { + throw BedrockServiceError.invalidParameter( + name, + "Parameter \(name) should be at most \(maxValue). Value: \(value)" + ) + } + } + } +} + +public struct PromptParams: Parameters { + public let maxSize: Int? + + public func validateValue(_ value: String) throws { + guard !value.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty else { + throw BedrockServiceError.invalidPrompt("Prompt is not allowed to be empty.") + } + if let maxSize { + let length = value.utf8.count + guard length <= maxSize else { + throw BedrockServiceError.invalidPrompt( + "Prompt is not allowed to be longer than \(maxSize) tokens. Prompt lengt \(length)" + ) + } + } + } +} + +public struct StopSequenceParams: Parameters { + public let maxSequences: Int? + public let defaultValue: [String]? + public let isSupported: Bool + + public init(maxSequences: Int? = nil, defaultValue: [String]? = nil) { + self = Self(maxSequences: maxSequences, defaultValue: defaultValue, isSupported: true) + } + + public static func notSupported() -> Self { + Self(maxSequences: nil, defaultValue: nil, isSupported: false) + } + + private init(maxSequences: Int? = nil, defaultValue: [String]? = nil, isSupported: Bool = true) { + self.maxSequences = maxSequences + self.defaultValue = defaultValue + self.isSupported = isSupported + } + + public func validateValue(_ value: [String]) throws { + if let maxSequences { + guard value.count <= maxSequences else { + throw BedrockServiceError.invalidStopSequences( + value, + "You can only provide up to \(maxSequences) stop sequences. Number of stop sequences: \(value.count)" + ) + } + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Parameters/TextGenerationParameters.swift b/swift-bedrock-library/Sources/BedrockTypes/Parameters/TextGenerationParameters.swift new file mode 100644 index 00000000..399e5373 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Parameters/TextGenerationParameters.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct TextGenerationParameters: Parameters { + public let temperature: Parameter + public let maxTokens: Parameter + public let topP: Parameter + public let topK: Parameter + public let prompt: PromptParams + public let stopSequences: StopSequenceParams + + public init( + temperature: Parameter, + maxTokens: Parameter, + topP: Parameter, + topK: Parameter, + stopSequences: StopSequenceParams, + maxPromptSize: Int? + ) { + self.temperature = temperature + self.maxTokens = maxTokens + self.topP = topP + self.topK = topK + self.prompt = PromptParams(maxSize: maxPromptSize) + self.stopSequences = stopSequences + } + + package func validate( + prompt: String? = nil, + maxTokens: Int? = nil, + temperature: Double? = nil, + topP: Double? = nil, + topK: Int? = nil, + stopSequences: [String]? = nil + ) throws { + if let prompt = prompt { + try self.prompt.validateValue(prompt) + } + if let temperature = temperature { + try self.temperature.validateValue(temperature) + } + if let maxTokens = maxTokens { + try self.maxTokens.validateValue(maxTokens) + } + if let topP = topP { + try self.topP.validateValue(topP) + } + if let topK = topK { + try self.topK.validateValue(topK) + } + if let stopSequences = stopSequences { + try self.stopSequences.validateValue(stopSequences) + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Region.swift b/swift-bedrock-library/Sources/BedrockTypes/Region.swift new file mode 100644 index 00000000..0b0767a8 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Region.swift @@ -0,0 +1,305 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Soto for AWS open source project +// +// Copyright (c) 2017-2022 the Soto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Soto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// THIS FILE IS AUTOMATICALLY GENERATED by https://github.com/soto-project/soto-core/scripts/generate-region.swift. DO NOT EDIT. + +/// Enumeration for all AWS server regions +public struct Region: Sendable, RawRepresentable, Equatable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + // Africa (Cape Town) + public static var afsouth1: Region { .init(rawValue: "af-south-1") } + // Asia Pacific (Hong Kong) + public static var apeast1: Region { .init(rawValue: "ap-east-1") } + // Asia Pacific (Tokyo) + public static var apnortheast1: Region { .init(rawValue: "ap-northeast-1") } + // Asia Pacific (Seoul) + public static var apnortheast2: Region { .init(rawValue: "ap-northeast-2") } + // Asia Pacific (Osaka) + public static var apnortheast3: Region { .init(rawValue: "ap-northeast-3") } + // Asia Pacific (Mumbai) + public static var apsouth1: Region { .init(rawValue: "ap-south-1") } + // Asia Pacific (Hyderabad) + public static var apsouth2: Region { .init(rawValue: "ap-south-2") } + // Asia Pacific (Singapore) + public static var apsoutheast1: Region { .init(rawValue: "ap-southeast-1") } + // Asia Pacific (Sydney) + public static var apsoutheast2: Region { .init(rawValue: "ap-southeast-2") } + // Asia Pacific (Jakarta) + public static var apsoutheast3: Region { .init(rawValue: "ap-southeast-3") } + // Asia Pacific (Melbourne) + public static var apsoutheast4: Region { .init(rawValue: "ap-southeast-4") } + // Asia Pacific (Malaysia) + public static var apsoutheast5: Region { .init(rawValue: "ap-southeast-5") } + // Asia Pacific (Thailand) + public static var apsoutheast7: Region { .init(rawValue: "ap-southeast-7") } + // Canada (Central) + public static var cacentral1: Region { .init(rawValue: "ca-central-1") } + // Canada West (Calgary) + public static var cawest1: Region { .init(rawValue: "ca-west-1") } + // China (Beijing) + public static var cnnorth1: Region { .init(rawValue: "cn-north-1") } + // China (Ningxia) + public static var cnnorthwest1: Region { .init(rawValue: "cn-northwest-1") } + // Europe (Frankfurt) + public static var eucentral1: Region { .init(rawValue: "eu-central-1") } + // Europe (Zurich) + public static var eucentral2: Region { .init(rawValue: "eu-central-2") } + // EU ISOE West + public static var euisoewest1: Region { .init(rawValue: "eu-isoe-west-1") } + // Europe (Stockholm) + public static var eunorth1: Region { .init(rawValue: "eu-north-1") } + // Europe (Milan) + public static var eusouth1: Region { .init(rawValue: "eu-south-1") } + // Europe (Spain) + public static var eusouth2: Region { .init(rawValue: "eu-south-2") } + // Europe (Ireland) + public static var euwest1: Region { .init(rawValue: "eu-west-1") } + // Europe (London) + public static var euwest2: Region { .init(rawValue: "eu-west-2") } + // Europe (Paris) + public static var euwest3: Region { .init(rawValue: "eu-west-3") } + // Israel (Tel Aviv) + public static var ilcentral1: Region { .init(rawValue: "il-central-1") } + // Middle East (UAE) + public static var mecentral1: Region { .init(rawValue: "me-central-1") } + // Middle East (Bahrain) + public static var mesouth1: Region { .init(rawValue: "me-south-1") } + // Mexico (Central) + public static var mxcentral1: Region { .init(rawValue: "mx-central-1") } + // South America (Sao Paulo) + public static var saeast1: Region { .init(rawValue: "sa-east-1") } + // US East (N. Virginia) + public static var useast1: Region { .init(rawValue: "us-east-1") } + // US East (Ohio) + public static var useast2: Region { .init(rawValue: "us-east-2") } + // AWS GovCloud (US-East) + public static var usgoveast1: Region { .init(rawValue: "us-gov-east-1") } + // AWS GovCloud (US-West) + public static var usgovwest1: Region { .init(rawValue: "us-gov-west-1") } + // US ISO East + public static var usisoeast1: Region { .init(rawValue: "us-iso-east-1") } + // US ISO WEST + public static var usisowest1: Region { .init(rawValue: "us-iso-west-1") } + // US ISOB East (Ohio) + public static var usisobeast1: Region { .init(rawValue: "us-isob-east-1") } + // US ISOF EAST + public static var usisofeast1: Region { .init(rawValue: "us-isof-east-1") } + // US ISOF SOUTH + public static var usisofsouth1: Region { .init(rawValue: "us-isof-south-1") } + // US West (N. California) + public static var uswest1: Region { .init(rawValue: "us-west-1") } + // US West (Oregon) + public static var uswest2: Region { .init(rawValue: "us-west-2") } + // other region + public static func other(_ name: String) -> Region { .init(rawValue: name) } +} + +extension Region { + public var partition: AWSPartition { + switch self { + case .afsouth1: return .aws + case .apeast1: return .aws + case .apnortheast1: return .aws + case .apnortheast2: return .aws + case .apnortheast3: return .aws + case .apsouth1: return .aws + case .apsouth2: return .aws + case .apsoutheast1: return .aws + case .apsoutheast2: return .aws + case .apsoutheast3: return .aws + case .apsoutheast4: return .aws + case .apsoutheast5: return .aws + case .apsoutheast7: return .aws + case .cacentral1: return .aws + case .cawest1: return .aws + case .cnnorth1: return .awscn + case .cnnorthwest1: return .awscn + case .eucentral1: return .aws + case .eucentral2: return .aws + case .euisoewest1: return .awsisoe + case .eunorth1: return .aws + case .eusouth1: return .aws + case .eusouth2: return .aws + case .euwest1: return .aws + case .euwest2: return .aws + case .euwest3: return .aws + case .ilcentral1: return .aws + case .mecentral1: return .aws + case .mesouth1: return .aws + case .mxcentral1: return .aws + case .saeast1: return .aws + case .useast1: return .aws + case .useast2: return .aws + case .usgoveast1: return .awsusgov + case .usgovwest1: return .awsusgov + case .usisoeast1: return .awsiso + case .usisowest1: return .awsiso + case .usisobeast1: return .awsisob + case .usisofeast1: return .awsisof + case .usisofsouth1: return .awsisof + case .uswest1: return .aws + case .uswest2: return .aws + default: return .aws + } + } +} + +extension Region: CustomStringConvertible { + public var description: String { self.rawValue } +} + +extension Region: Codable {} + +/// Enumeration for all AWS partitions +public struct AWSPartition: Sendable, RawRepresentable, Equatable, Hashable { + enum InternalPartition: String { + case aws + case awscn + case awsusgov + case awsiso + case awsisob + case awsisoe + case awsisof + } + + private var partition: InternalPartition + + public var rawValue: String { self.partition.rawValue } + + public init?(rawValue: String) { + guard let partition = InternalPartition(rawValue: rawValue) else { return nil } + self.partition = partition + } + + private init(partition: InternalPartition) { + self.partition = partition + } + + // AWS Standard + public static var aws: AWSPartition { .init(partition: .aws) } + // AWS China + public static var awscn: AWSPartition { .init(partition: .awscn) } + // AWS GovCloud (US) + public static var awsusgov: AWSPartition { .init(partition: .awsusgov) } + // AWS ISO (US) + public static var awsiso: AWSPartition { .init(partition: .awsiso) } + // AWS ISOB (US) + public static var awsisob: AWSPartition { .init(partition: .awsisob) } + // AWS ISOE (Europe) + public static var awsisoe: AWSPartition { .init(partition: .awsisoe) } + // AWS ISOF + public static var awsisof: AWSPartition { .init(partition: .awsisof) } +} + +extension AWSPartition { + public var dnsSuffix: String { + switch self.partition { + case .aws: return "amazonaws.com" + case .awscn: return "amazonaws.com.cn" + case .awsusgov: return "amazonaws.com" + case .awsiso: return "c2s.ic.gov" + case .awsisob: return "sc2s.sgov.gov" + case .awsisoe: return "cloud.adc-e.uk" + case .awsisof: return "csp.hci.ic.gov" + } + } + + public func defaultEndpoint(region: Region, service: String) -> String { + switch self.partition { + case .aws: return "\(service).\(region).amazonaws.com" + case .awscn: return "\(service).\(region).amazonaws.com.cn" + case .awsusgov: return "\(service).\(region).amazonaws.com" + case .awsiso: return "\(service).\(region).c2s.ic.gov" + case .awsisob: return "\(service).\(region).sc2s.sgov.gov" + case .awsisoe: return "\(service).\(region).cloud.adc-e.uk" + case .awsisof: return "\(service).\(region).csp.hci.ic.gov" + } + } +} + +// allows to create a Region from a String +// it will only create a Region if the provided +// region name is valid. +extension Region { + public init?(awsRegionName: String) { + self.init(rawValue: awsRegionName) + switch self { + case .afsouth1, + .apeast1, + .apnortheast1, + .apnortheast2, + .apnortheast3, + .apsouth1, + .apsouth2, + .apsoutheast1, + .apsoutheast2, + .apsoutheast3, + .apsoutheast4, + .apsoutheast5, + .apsoutheast7, + .cacentral1, + .cawest1, + .cnnorth1, + .cnnorthwest1, + .eucentral1, + .eucentral2, + .euisoewest1, + .eunorth1, + .eusouth1, + .eusouth2, + .euwest1, + .euwest2, + .euwest3, + .ilcentral1, + .mecentral1, + .mesouth1, + .mxcentral1, + .saeast1, + .useast1, + .useast2, + .usgoveast1, + .usgovwest1, + .usisoeast1, + .usisowest1, + .usisobeast1, + .usisofeast1, + .usisofsouth1, + .uswest1, + .uswest2: + return + default: + return nil + } + } +} diff --git a/swift-bedrock-library/Sources/BedrockTypes/Role.swift b/swift-bedrock-library/Sources/BedrockTypes/Role.swift new file mode 100644 index 00000000..757cb179 --- /dev/null +++ b/swift-bedrock-library/Sources/BedrockTypes/Role.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Foundation + +public enum Role: String, Codable, Sendable { + case user + case assistant + + public init(from sdkConversationRole: BedrockRuntimeClientTypes.ConversationRole) throws { + switch sdkConversationRole { + case .user: self = .user + case .assistant: self = .assistant + case .sdkUnknown(let unknownRole): + throw BedrockServiceError.notImplemented( + "Role \(unknownRole) is not implemented by BedrockRuntimeClientTypes" + ) + } + } + + public func getSDKConversationRole() -> BedrockRuntimeClientTypes.ConversationRole { + switch self { + case .user: return .user + case .assistant: return .assistant + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/AuthenticationTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/AuthenticationTests.swift new file mode 100644 index 00000000..7c2b4edb --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/AuthenticationTests.swift @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AwsCommonRuntimeKit +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// MARK: authentication +extension BedrockServiceTests { + + @Test( + "Authentication: AuthenticationType struct does not leak credentials", + arguments: [ + BedrockAuthentication.static( + accessKey: "MY_ACCESS_KEY", + secretKey: "MY_SECRET_KEY", + sessionToken: "MY_SECRET_SESSION_TOKEN" + ), + BedrockAuthentication.webIdentity( + token: "MY_SECRET_JWT_TOKEN", + roleARN: "MY_ROLE_ARN", + region: .useast1, + notification: {} + ), + ] + ) + func authNoLeaks(auth: BedrockAuthentication) { + //given the auth in paramaters + + //when + let str = String(describing: auth) + + // then + #expect(!str.contains("SECRET")) + + //when + let str2 = "\(auth)" // is it different than String(describing:) ? + + // then + #expect(!str2.contains("SECRET")) + } + + // // Only works when SSO is actually expired + // @Test("Authentication Error: SSO expired") + // func authErrorSSOExpired() async throws { + // await #expect(throws: BedrockServiceError.self) { + // let auth = BedrockAuthentication.sso() + // let bedrock = try await BedrockService(authentication: auth) + // let _ = try await bedrock.listModels() + // } + // } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/BedrockServiceTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/BedrockServiceTests.swift new file mode 100644 index 00000000..8e51e2c1 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/BedrockServiceTests.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AwsCommonRuntimeKit +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +@Suite("BedrockService Tests") +struct BedrockServiceTests { + let bedrock: BedrockService + + init() async throws { + self.bedrock = try await BedrockService( + bedrockClient: MockBedrockClient(), + bedrockRuntimeClient: MockBedrockRuntimeClient() + ) + } + + // MARK: listModels + + @Test("List all models") + func listModels() async throws { + let models: [ModelSummary] = try await bedrock.listModels() + #expect(models.count == 3) + #expect(models[0].modelId == "anthropic.claude-instant-v1") + #expect(models[0].modelName == "Claude Instant") + #expect(models[0].providerName == "Anthropic") + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseDocumentTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseDocumentTests.swift new file mode 100644 index 00000000..441b710b --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseDocumentTests.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// Converse document + +extension BedrockServiceTests { + + @Test("Converse with document") + func converseDocumentBlock() async throws { + let source = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let documentBlock = try DocumentBlock(name: "doc", format: .pdf, source: source) + let builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("What is this?") + .withDocument(documentBlock) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Document received") + } + + @Test("Converse with document") + func converseDocumentParts() async throws { + let source = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("What is this?") + .withDocument(name: "doc", format: .pdf, source: source) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Document received") + } + + @Test("Converse with document and reused builder") + func converseDocumentAndReusedBuilder() async throws { + let source = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + var builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("Can you summarize this document?") + .withDocument(name: "doc", format: .pdf, source: source) + .withTemperature(0.4) + + #expect(builder.document != nil) + #expect(builder.document!.name == "doc") + #expect(builder.document!.format == .pdf) + var docBytes = "" + if case .bytes(let string) = builder.document?.source { + docBytes = string + } + #expect(docBytes == source) + #expect(builder.temperature == 0.4) + + var reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Document received") + + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Could you also give me a Dutch version?") + #expect(builder.document == nil) + #expect(builder.prompt == "Could you also give me a Dutch version?") + #expect(builder.temperature == 0.4) + + reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: Could you also give me a Dutch version?") + } + + @Test("Converse document with invalid model") + func converseDocumentInvalidModel() async throws { + let source = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let documentBlock = try DocumentBlock(name: "doc", format: .pdf, source: source) + #expect(throws: BedrockServiceError.self) { + let _ = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt("What is this?") + .withDocument(documentBlock) + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseReasoningTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseReasoningTests.swift new file mode 100644 index 00000000..05cd1fa2 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseReasoningTests.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// Converse reasoning + +extension BedrockServiceTests { + + @Test("Converse with reasoning") + func converseReasoning() async throws { + let builder = try ConverseRequestBuilder(with: .claudev3_7_sonnet) + .withPrompt("What is this?") + let reply = try await bedrock.converse(with: builder) + + #expect(reply.textReply == "Your prompt was: What is this?") + #expect(reply.reasoningBlock != nil) + #expect(reply.reasoningBlock?.reasoning == "reasoning text") + #expect(reply.reasoningBlock?.signature == "reasoning signature") + } + + @Test("Converse with encrypted reasoning") + func converseEncryptedReasoning() async throws { + let builder = try ConverseRequestBuilder(with: .claudev3_7_sonnet) + .withPrompt("encrypted") + let reply = try await bedrock.converse(with: builder) + + #expect(reply.textReply == "Your prompt was: encrypted") + #expect(reply.reasoningBlock == nil) + #expect(reply.encryptedReasoning != nil) + #expect(reply.encryptedReasoning?.reasoning != nil) + } + + @Test("Converse without reasoning when not supported by model") + func converseReasoningWrongModel() async throws { + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt("What is this?") + let reply = try await bedrock.converse(with: builder) + + #expect(reply.textReply == "Your prompt was: What is this?") + #expect(reply.reasoningBlock == nil) + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseTextTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseTextTests.swift new file mode 100644 index 00000000..4c9154ee --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseTextTests.swift @@ -0,0 +1,219 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// Converse text + +extension BedrockServiceTests { + + // Prompt + @Test( + "Continue conversation using a valid prompt", + arguments: NovaTestConstants.TextGeneration.validPrompts + ) + func converseWithValidPrompt(prompt: String) async throws { + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: \(prompt)") + } + + @Test( + "Continue conversation variation using an invalid prompt", + arguments: NovaTestConstants.TextGeneration.invalidPrompts + ) + func converseWithInvalidPrompt(prompt: String) async throws { + await #expect(throws: BedrockServiceError.self) { + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + let _ = try await bedrock.converse(with: builder) + } + } + + // Continue + @Test( + "Continue conversation using a valid prompt and reusing builder", + arguments: NovaTestConstants.TextGeneration.validPrompts + ) + func converseWithValidPromptAndReusedBuilder(prompt: String) async throws { + var builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + #expect(builder.prompt == prompt) + var reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: \(prompt)") + + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("New prompt") + + #expect(builder.prompt == "New prompt") + + reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: New prompt") + } + + @Test("Continue conversation reusing builder") + func converseWithReusedBuilder() async throws { + var builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt("First prompt") + .withMaxTokens(100) + .withTemperature(0.5) + .withTopP(0.5) + .withStopSequence("\n\nHuman:") + .withSystemPrompt("You are a helpful assistant.") + + #expect(builder.prompt == "First prompt") + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + #expect(builder.maxReasoningTokens == nil) + + var reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: First prompt") + + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Second prompt") + + #expect(builder.prompt == "Second prompt") + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + #expect(builder.history.count == 2) + #expect(builder.maxReasoningTokens == nil) + + reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: Second prompt") + + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Third prompt") + .withTemperature(1) + #expect(builder.prompt == "Third prompt") + #expect(builder.temperature == 1) + #expect(builder.maxTokens == 100) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + #expect(builder.history.count == 4) + #expect(builder.maxReasoningTokens == nil) + + reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: Third prompt") + } + + // Temperature + @Test( + "Continue conversation using a valid temperature", + arguments: NovaTestConstants.TextGeneration.validTemperature + ) + func converseWithValidTemperature(temperature: Double) async throws { + let prompt = "This is a test" + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + .withTemperature(temperature) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: \(prompt)") + } + + @Test( + "Continue conversation variation using an invalid temperature", + arguments: NovaTestConstants.TextGeneration.invalidTemperature + ) + func converseWithInvalidTemperature(temperature: Double) async throws { + await #expect(throws: BedrockServiceError.self) { + let prompt = "This is a test" + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + .withTemperature(temperature) + let _ = try await bedrock.converse(with: builder) + } + } + + // MaxTokens + @Test( + "Continue conversation using a valid maxTokens", + arguments: NovaTestConstants.TextGeneration.validMaxTokens + ) + func converseWithValidMaxTokens(maxTokens: Int) async throws { + let prompt = "This is a test" + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + .withMaxTokens(maxTokens) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: \(prompt)") + } + + @Test( + "Continue conversation variation using an invalid maxTokens", + arguments: NovaTestConstants.TextGeneration.invalidMaxTokens + ) + func converseWithInvalidMaxTokens(maxTokens: Int) async throws { + await #expect(throws: BedrockServiceError.self) { + let prompt = "This is a test" + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + .withMaxTokens(maxTokens) + let _ = try await bedrock.converse(with: builder) + } + } + + // TopP + @Test( + "Continue conversation using a valid temperature", + arguments: NovaTestConstants.TextGeneration.validTopP + ) + func converseWithValidTopP(topP: Double) async throws { + let prompt = "This is a test" + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + .withTopP(topP) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: \(prompt)") + } + + @Test( + "Continue conversation variation using an invalid temperature", + arguments: NovaTestConstants.TextGeneration.invalidTopP + ) + func converseWithInvalidTopP(topP: Double) async throws { + await #expect(throws: BedrockServiceError.self) { + let prompt = "This is a test" + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + .withTopP(topP) + let _ = try await bedrock.converse(with: builder) + } + } + + // StopSequences + @Test( + "Continue conversation using a valid stopSequences", + arguments: NovaTestConstants.TextGeneration.validStopSequences + ) + func converseWithValidTopK(stopSequences: [String]) async throws { + let prompt = "This is a test" + let builder = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt(prompt) + .withStopSequences(stopSequences) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Your prompt was: \(prompt)") + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseToolTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseToolTests.swift new file mode 100644 index 00000000..e33f9374 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseToolTests.swift @@ -0,0 +1,238 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// Converse tools + +extension BedrockServiceTests { + + @Test("Request tool usage") + func converseRequestTool() async throws { + let tool = try Tool( + name: "toolName", + inputSchema: JSON(with: ["code": "string"]), + description: "toolDescription" + ) + let builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("Use tool") + .withTool(tool) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == nil) + let id: String + let name: String + let input: JSON + if let toolUse = reply.toolUse { + id = toolUse.id + name = toolUse.name + input = toolUse.input + } else { + id = "" + name = "" + input = JSON(with: ["code": "wrong"]) + } + #expect(id == "toolId") + #expect(name == "toolName") + #expect(input.getValue("code") == "abc") + } + + @Test("Request tool usage with reused builder") + func converseToolWithReusedBuilder() async throws { + var builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("Use tool") + .withTool(name: "toolName", inputSchema: JSON(with: ["code": "string"]), description: "toolDescription") + + #expect(builder.prompt != nil) + #expect(builder.prompt! == "Use tool") + #expect(builder.history.count == 0) + + var reply = try await bedrock.converse(with: builder) + + #expect(reply.textReply == nil) + + let id: String + let name: String + let input: JSON + if let toolUse = reply.toolUse { + id = toolUse.id + name = toolUse.name + input = toolUse.input + } else { + id = "" + name = "" + input = JSON(with: ["code": "wrong"]) + } + + #expect(id == "toolId") + #expect(name == "toolName") + #expect(input.getValue("code") == "abc") + + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withToolResult("Information from Tool") + + #expect(builder.prompt == nil) + #expect(builder.toolResult != nil) + #expect(builder.history.count == 2) + + reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Tool result received") + #expect(reply.toolUse == nil) + + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Some prompt") + + #expect(builder.prompt != nil) + #expect(builder.prompt! == "Some prompt") + #expect(builder.toolResult == nil) + #expect(builder.history.count == 4) + + reply = try await bedrock.converse(with: builder) + #expect(reply.textReply != nil) + #expect(reply.textReply! == "Your prompt was: Some prompt") + } + + @Test("Add tool with invalid model") + func converseToolWrongModel() async throws { + #expect(throws: BedrockServiceError.self) { + let tool = try Tool( + name: "toolName", + inputSchema: JSON(with: ["code": "string"]), + description: "toolDescription" + ) + let _ = try ConverseRequestBuilder(with: .titan_text_g1_express) + .withTool(tool) + } + } + + @Test("No tool request without tools") + func converseToolWithoutTools() async throws { + let builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("Use tool") + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply != nil) + #expect(reply.toolUse == nil) + } + + @Test("Tool result") + func converseToolResult() async throws { + let tool = try Tool( + name: "toolName", + inputSchema: JSON(with: ["code": "string"]), + description: "toolDescription" + ) + let id = "toolId" + let toolUse = ToolUseBlock(id: id, name: "toolName", input: JSON(with: ["code": "abc"])) + let history = [Message("Use tool"), Message(toolUse)] + + let builder = try ConverseRequestBuilder(with: .nova_lite) + .withHistory(history) + .withTool(tool) + .withToolResult("Information from tool") + + let reply = try await bedrock.converse(with: builder) + #expect(reply.toolUse == nil) + #expect(reply.textReply == "Tool result received") + } + + @Test("Tool result without toolUse") + func converseToolResultWithoutToolUse() async throws { + let tool = try Tool( + name: "toolName", + inputSchema: JSON(with: ["code": "string"]), + description: "toolDescription" + ) + let id = "toolId" + let history = [Message("Use tool"), Message(from: .assistant, content: [.text("No need for a tool")])] + #expect(throws: BedrockServiceError.self) { + let _ = try ConverseRequestBuilder(with: .nova_lite) + .withHistory(history) + .withTool(tool) + .withToolResult("Information from tool", id: id) + } + } + + @Test("Tool result without tools") + func converseToolResultWithoutTools() async throws { + let id = "toolId" + let toolUse = ToolUseBlock(id: id, name: "toolName", input: JSON(with: ["code": "abc"])) + let history = [Message("Use tool"), Message(toolUse)] + #expect(throws: BedrockServiceError.self) { + let _ = try ConverseRequestBuilder(with: .nova_lite) + .withHistory(history) + .withToolResult("Information from tool") + } + } + + @Test("Tool result with invalid model") + func converseToolResultInvalidModel() async throws { + let tool = try Tool( + name: "toolName", + inputSchema: JSON(with: ["code": "string"]), + description: "toolDescription" + ) + let id = "toolId" + let toolUse = ToolUseBlock(id: id, name: "toolName", input: JSON(with: ["code": "abc"])) + let history = [Message("Use tool"), Message(toolUse)] + #expect(throws: BedrockServiceError.self) { + let _ = try ConverseRequestBuilder(with: .titan_text_g1_express) + .withHistory(history) + .withTool(tool) + .withToolResult("Information from tool") + } + } + + @Test("Tool result with invalid model without tools") + func converseToolResultInvalidModelWithoutTools() async throws { + let id = "toolId" + let toolUse = ToolUseBlock(id: id, name: "toolName", input: JSON(with: ["code": "abc"])) + let history = [Message("Use tool"), Message(toolUse)] + + #expect(throws: BedrockServiceError.self) { + let _ = try ConverseRequestBuilder(with: .titan_text_g1_express) + .withHistory(history) + .withToolResult("Information from tool") + } + } + + @Test("Tool result with invalid model without toolUse") + func converseToolResultInvalidModelWithoutToolUse() async throws { + let tool = try Tool( + name: "toolName", + inputSchema: JSON(with: ["code": "string"]), + description: "toolDescription" + ) + let history = [Message("Use tool"), Message(from: .assistant, content: [.text("No need for a tool")])] + + #expect(throws: BedrockServiceError.self) { + let _ = try ConverseRequestBuilder(with: .titan_text_g1_express) + .withHistory(history) + .withTool(tool) + .withToolResult("Information from tool") + } + } + + @Test("Tool result with invalid model without toolUse and without tools") + func converseToolResultInvalidModelWithoutToolUseAndTools() async throws { + let history = [Message("Use tool"), Message(from: .assistant, content: [.text("No need for a tool")])] + #expect(throws: BedrockServiceError.self) { + let _ = try ConverseRequestBuilder(with: .titan_text_g1_express) + .withHistory(history) + .withToolResult("Information from tool") + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseVisionTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseVisionTests.swift new file mode 100644 index 00000000..ea2504e9 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/Converse/ConverseVisionTests.swift @@ -0,0 +1,132 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// Converse vision + +extension BedrockServiceTests { + + @Test("Converse with vision") + func converseVision() async throws { + let bytes = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("What is this?") + .withImage(format: .jpeg, source: bytes) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Image received") + } + + @Test("Converse with vision") + func converseVisionUsingImageBlock() async throws { + let source = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let image = try ImageBlock(format: .jpeg, source: source) + let builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("What is this?") + .withImage(image) + let reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Image received") + } + + @Test("Converse with vision and inout builder") + func converseVisionAndInOutBuilder() async throws { + let bytes = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + var builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("What is this?") + .withImage(format: .jpeg, source: bytes) + + #expect(builder.image != nil) + #expect(builder.image?.format == .jpeg) + var imageBytes = "" + if case .bytes(let string) = builder.image?.source { + imageBytes = string + } + #expect(imageBytes == bytes) + #expect(builder.prompt == "What is this?") + + var reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Image received") + + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Some prompt") + + #expect(builder.image == nil) + #expect(builder.prompt != nil) + #expect(builder.prompt! == "Some prompt") + #expect(builder.toolResult == nil) + #expect(builder.history.count == 2) + + reply = try await bedrock.converse(with: builder) + #expect(reply.textReply != nil) + #expect(reply.textReply! == "Your prompt was: Some prompt") + } + + @Test("Converse with vision with invalid model") + func converseVisionInvalidModel() async throws { + let source = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + #expect(throws: BedrockServiceError.self) { + let _ = try ConverseRequestBuilder(with: .nova_micro) + .withPrompt("What is this?") + .withImage(format: .jpeg, source: source) + } + } + + @Test("Converse with vision and document and inout builder") + func converseVisionAndDocumentAndInOutBuilder() async throws { + let docSource = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let imageSource = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + var builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("What is this?") + .withImage(format: .jpeg, source: imageSource) + .withDocument(name: "doc", format: .pdf, source: docSource) + + #expect(builder.image != nil) + #expect(builder.image?.format == .jpeg) + var imageBytes = "" + if case .bytes(let string) = builder.image?.source { + imageBytes = string + } + #expect(imageBytes == imageSource) + #expect(builder.document != nil) + #expect(builder.document?.name == "doc") + #expect(builder.document?.format == .pdf) + var docBytes = "" + if case .bytes(let string) = builder.document?.source { + docBytes = string + } + #expect(docBytes == docSource) + #expect(builder.prompt == "What is this?") + + var reply = try await bedrock.converse(with: builder) + #expect(reply.textReply == "Document received") + + builder = try ConverseRequestBuilder(from: builder, with: reply) + .withPrompt("Some prompt") + + #expect(builder.image == nil) + #expect(builder.document == nil) + #expect(builder.prompt != nil) + #expect(builder.prompt! == "Some prompt") + #expect(builder.toolResult == nil) + #expect(builder.history.count == 2) + + reply = try await bedrock.converse(with: builder) + #expect(reply.textReply != nil) + #expect(reply.textReply! == "Your prompt was: Some prompt") + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamDocumentTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamDocumentTests.swift new file mode 100644 index 00000000..b9d580fe --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamDocumentTests.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// MARK - Streaming converse document input + +extension BedrockServiceTests { + + @Test("Continue streaming conversation with document") + func converseStreamWithDocument() async throws { + let source = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + var builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("First prompt") + .withMaxTokens(100) + .withTemperature(0.5) + .withTopP(0.5) + .withStopSequence("\n\nHuman:") + .withSystemPrompt("You are a helpful assistant.") + .withDocument(name: "document", format: .md, source: source) + + #expect(builder.prompt == "First prompt") + #expect(builder.image == nil) + #expect(builder.document != nil) + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + + var stream = try await bedrock.converseStream(with: builder) + + // Collect all the stream elements + var streamElements: [ConverseStreamElement] = [] + for try await element in stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 6) + + var message: Message = Message("") + if case .messageComplete(let msg) = streamElements.last { + message = msg + } else { + Issue.record("Expected message") + } + + #expect(message.content.count == 1) + #expect(message.role == .assistant) + + if case .text(let text) = message.content.last { + #expect(text == "Hello, your prompt was: Document received") + } + + builder = try ConverseRequestBuilder(from: builder, with: message) + .withPrompt("Second prompt") + #expect(builder.prompt == "Second prompt") + #expect(builder.image == nil) + #expect(builder.document == nil) + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + #expect(builder.history.count == 2) + + stream = try await bedrock.converseStream(with: builder) + // Collect all the stream elements + streamElements = [] + for try await element in stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 6) + + message = Message("") + if case .messageComplete(let msg) = streamElements.last { + message = msg + } else { + Issue.record("Expected message") + } + + #expect(message.content.count == 1) + #expect(message.role == .assistant) + + if case .text(let text) = message.content.last { + #expect(text == "Hello, your prompt was: Second prompt") + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamReasoningTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamReasoningTests.swift new file mode 100644 index 00000000..c4301713 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamReasoningTests.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// MARK - Streaming converse tekst + +extension BedrockServiceTests { + + @Test("Streaming converse with reasoning") + func streamingConverseReasoning() async throws { + + // First call, with reasoning enabled + var prompt = "What is this?" + var builder = try ConverseRequestBuilder(with: .claudev3_7_sonnet) + .withPrompt(prompt) + .withReasoning() + + #expect(builder.prompt == prompt) + #expect(builder.enableReasoning == true) + #expect(builder.maxReasoningTokens == 4096) + #expect(builder.history.count == 0) + + var stream = try await bedrock.converseStream(with: builder) + var message: Message = try await validateStream(stream, elementsCount: 6) + + try checkReasoningContent(message) + try checkTextContent(message, prompt: prompt) + + // Second call, still with reasoning enabled + prompt = "Second prompt" + builder = try ConverseRequestBuilder(from: builder, with: message) + .withPrompt(prompt) + + #expect(builder.prompt == prompt) + #expect(builder.enableReasoning == true) + #expect(builder.maxReasoningTokens == 4096) + #expect(builder.history.count == 2) + + stream = try await bedrock.converseStream(with: builder) + message = try await validateStream(stream, elementsCount: 6) + + try checkReasoningContent(message) + try checkTextContent(message, prompt: prompt) + + // Third call, without reasoning enabled + prompt = "Third prompt" + builder = try ConverseRequestBuilder(from: builder, with: message) + .withPrompt(prompt) + .withReasoning(false) + + #expect(builder.prompt == prompt) + #expect(builder.enableReasoning == false) + #expect(builder.maxReasoningTokens == nil) + #expect(builder.history.count == 4) + + stream = try await bedrock.converseStream(with: builder) + message = try await validateStream(stream, elementsCount: 6, contentCount: 1) + try checkTextContent(message, prompt: prompt) + try checkReasoningContent(message, hasReasoningContent: false) + } + + // MARK - helper functions + + func validateStream( + _ stream: AsyncThrowingStream, + elementsCount: Int, + contentCount: Int = 1 + ) async throws -> Message { + var streamElements: [ConverseStreamElement] = [] + for try await element in stream { + streamElements.append(element) + } + + #expect(streamElements.count == elementsCount) + + if case .messageComplete(let message) = streamElements.last { + #expect(message.role == .assistant) + #expect(message.content.count == contentCount) + return message + } else { + Issue.record("Expected message") + return Message("WRONG") + } + } + + func checkTextContent(_ message: Message, prompt: String) throws { + if case .text(let text) = message.content.last { + #expect(text == "Hello, your prompt was: \(prompt)") + } + } + + func checkReasoningContent(_ message: Message, hasReasoningContent: Bool = true) throws { + if hasReasoningContent { + if case .reasoning(let reasoning) = message.content.first { + #expect(reasoning.reasoning == "reasoning text") + } + } else { + if case .reasoning = message.content.first { + Issue.record("Expected no reasoning text") + } + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamTextTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamTextTests.swift new file mode 100644 index 00000000..2a3ba3f0 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamTextTests.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// MARK - Streaming converse tekst + +extension BedrockServiceTests { + + @Test("Continue conversation reusing builder") + func converseStreamWithReusedBuilder() async throws { + var builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("First prompt") + .withMaxTokens(100) + .withTemperature(0.5) + .withTopP(0.5) + .withStopSequence("\n\nHuman:") + .withSystemPrompt("You are a helpful assistant.") + + #expect(builder.prompt == "First prompt") + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + + var stream = try await bedrock.converseStream(with: builder) + + // Collect all the stream elements + var streamElements: [ConverseStreamElement] = [] + for try await element in stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 6) + + var message: Message = Message("") + if case .messageComplete(let msg) = streamElements.last { + message = msg + } else { + Issue.record("Expected message") + } + + #expect(message.content.count == 1) + #expect(message.role == .assistant) + + if case .text(let text) = message.content.last { + #expect(text == "Hello, your prompt was: First prompt") + } + + builder = try ConverseRequestBuilder(from: builder, with: message) + .withPrompt("Second prompt") + #expect(builder.prompt == "Second prompt") + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + #expect(builder.history.count == 2) + + stream = try await bedrock.converseStream(with: builder) + // Collect all the stream elements + streamElements = [] + for try await element in stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 6) + + message = Message("") + if case .messageComplete(let msg) = streamElements.last { + message = msg + } else { + Issue.record("Expected message") + } + + #expect(message.content.count == 1) + #expect(message.role == .assistant) + + if case .text(let text) = message.content.last { + #expect(text == "Hello, your prompt was: Second prompt") + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamToolTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamToolTests.swift new file mode 100644 index 00000000..fbb0f877 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamToolTests.swift @@ -0,0 +1,116 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// MARK - Streaming conversetooluse + +extension BedrockServiceTests { + @Test("Continue conversation with tool use") + func converseStreamWithToolUse() async throws { + let tool = try Tool( + name: "toolName", + inputSchema: JSON(with: ["code": "string"]), + description: "toolDescription" + ) + var builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("Use tool") + .withMaxTokens(100) + .withTemperature(0.5) + .withTopP(0.5) + .withStopSequence("\n\nHuman:") + .withSystemPrompt("You are a helpful assistant.") + .withTool(tool) + + #expect(builder.prompt == "Use tool") + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + #expect(builder.tools != nil) + + var stream = try await bedrock.converseStream(with: builder) + + // Collect all the stream elements + var streamElements: [ConverseStreamElement] = [] + for try await element in stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 3) + + var message: Message = Message("") + if case .messageComplete(let msg) = streamElements.last { + message = msg + } else { + Issue.record("Expected message") + } + var toolUse: ToolUseBlock? = nil + if case .toolUse(let tu) = message.content.last { + toolUse = tu + } else { + Issue.record("Expected message") + } + let toolUseId = toolUse?.id ?? "WRONG" + + #expect(message.content.count == 1) + #expect(message.role == .assistant) + #expect(toolUse != nil) + #expect(toolUse?.name == "toolname") + #expect(toolUseId == "tooluseid") + + builder = try ConverseRequestBuilder(from: builder, with: message) + .withToolResult(ToolResultBlock("tool result", id: toolUseId)) + + #expect(builder.prompt == nil) + #expect(builder.toolResult != nil) + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + #expect(builder.history.count == 2) + #expect(builder.tools != nil) + + stream = try await bedrock.converseStream(with: builder) + // Collect all the stream elements + streamElements = [] + for try await element in stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 6) + + message = Message("") + if case .messageComplete(let msg) = streamElements.last { + message = msg + } else { + Issue.record("Expected message") + } + + #expect(message.content.count == 1) + #expect(message.role == .assistant) + + if case .text(let text) = message.content.last { + #expect(text == "Hello, your prompt was: Tool result received for toolUseId: \(toolUseId)") + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamVisionTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamVisionTests.swift new file mode 100644 index 00000000..3a6f8693 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/ConverseStream/ConverseStreamVisionTests.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// MARK - Streaming converse vision + +extension BedrockServiceTests { + + @Test("Continue conversation with vision") + func converseStreamWithVision() async throws { + let source = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + var builder = try ConverseRequestBuilder(with: .nova_lite) + .withPrompt("First prompt") + .withMaxTokens(100) + .withTemperature(0.5) + .withTopP(0.5) + .withStopSequence("\n\nHuman:") + .withSystemPrompt("You are a helpful assistant.") + .withImage(format: .jpeg, source: source) + + #expect(builder.prompt == "First prompt") + #expect(builder.image != nil) + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + + var stream = try await bedrock.converseStream(with: builder) + + // Collect all the stream elements + var streamElements: [ConverseStreamElement] = [] + for try await element in stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 6) + + var message: Message = Message("") + if case .messageComplete(let msg) = streamElements.last { + message = msg + } else { + Issue.record("Expected message") + } + + #expect(message.content.count == 1) + #expect(message.role == .assistant) + + if case .text(let text) = message.content.last { + #expect(text == "Hello, your prompt was: Image received") + } + + builder = try ConverseRequestBuilder(from: builder, with: message) + .withPrompt("Second prompt") + #expect(builder.prompt == "Second prompt") + #expect(builder.image == nil) + #expect(builder.maxTokens == 100) + #expect(builder.temperature == 0.5) + #expect(builder.topP == 0.5) + #expect(builder.stopSequences == ["\n\nHuman:"]) + #expect(builder.systemPrompts == ["You are a helpful assistant."]) + #expect(builder.history.count == 2) + + stream = try await bedrock.converseStream(with: builder) + // Collect all the stream elements + streamElements = [] + for try await element in stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 6) + + message = Message("") + if case .messageComplete(let msg) = streamElements.last { + message = msg + } else { + Issue.record("Expected message") + } + + #expect(message.content.count == 1) + #expect(message.role == .assistant) + + if case .text(let text) = message.content.last { + #expect(text == "Hello, your prompt was: Second prompt") + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/ImageGenerationTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/ImageGenerationTests.swift new file mode 100644 index 00000000..12088380 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/ImageGenerationTests.swift @@ -0,0 +1,162 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// Image generation + +extension BedrockServiceTests { + + // Models + @Test( + "Generate image using an implemented model", + arguments: NovaTestConstants.imageGenerationModels + ) + func generateImageWithValidModel(model: BedrockModel) async throws { + let output: ImageGenerationOutput = try await bedrock.generateImage( + "This is a test", + with: model + ) + #expect(output.images.count == 1) + } + + @Test( + "Generate image using a wrong model", + arguments: NovaTestConstants.textCompletionModels + ) + func generateImageWithInvalidModel(model: BedrockModel) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: ImageGenerationOutput = try await bedrock.generateImage( + "This is a test", + with: model, + nrOfImages: 3 + ) + } + } + + // NrOfmages + @Test( + "Generate image using a valid nrOfImages", + arguments: NovaTestConstants.ImageGeneration.validNrOfImages + ) + func generateImageWithValidNrOfImages(nrOfImages: Int) async throws { + let output: ImageGenerationOutput = try await bedrock.generateImage( + "This is a test", + with: BedrockModel.nova_canvas, + nrOfImages: nrOfImages + ) + #expect(output.images.count == nrOfImages) + } + + @Test( + "Generate image using an invalid nrOfImages", + arguments: NovaTestConstants.ImageGeneration.invalidNrOfImages + ) + func generateImageWithInvalidNrOfImages(nrOfImages: Int) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: ImageGenerationOutput = try await bedrock.generateImage( + "This is a test", + with: BedrockModel.nova_canvas, + nrOfImages: nrOfImages + ) + } + } + + // CfgScale + @Test( + "Generate image using a valid cfgScale", + arguments: NovaTestConstants.ImageGeneration.validCfgScale + ) + func generateImageWithValidCfgScale(cfgScale: Double) async throws { + let output: ImageGenerationOutput = try await bedrock.generateImage( + "This is a test", + with: BedrockModel.nova_canvas, + cfgScale: cfgScale + ) + #expect(output.images.count == 1) + } + + @Test( + "Generate image using an invalid cfgScale", + arguments: NovaTestConstants.ImageGeneration.invalidCfgScale + ) + func generateImageWithInvalidCfgScale(cfgScale: Double) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: ImageGenerationOutput = try await bedrock.generateImage( + "This is a test", + with: BedrockModel.nova_canvas, + cfgScale: cfgScale + ) + } + } + + // Seed + @Test( + "Generate image using a valid seed", + arguments: NovaTestConstants.ImageGeneration.validSeed + ) + func generateImageWithValidSeed(seed: Int) async throws { + let output: ImageGenerationOutput = try await bedrock.generateImage( + "This is a test", + with: BedrockModel.nova_canvas, + seed: seed + ) + #expect(output.images.count == 1) + } + + @Test( + "Generate image using an invalid seed", + arguments: NovaTestConstants.ImageGeneration.invalidSeed + ) + func generateImageWithInvalidSeed(seed: Int) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: ImageGenerationOutput = try await bedrock.generateImage( + "This is a test", + with: BedrockModel.nova_canvas, + seed: seed + ) + } + } + + // Prompt + @Test( + "Generate image using a valid prompt", + arguments: NovaTestConstants.ImageGeneration.validImagePrompts + ) + func generateImageWithValidPrompt(prompt: String) async throws { + let output: ImageGenerationOutput = try await bedrock.generateImage( + prompt, + with: BedrockModel.nova_canvas, + nrOfImages: 3 + ) + #expect(output.images.count == 3) + } + + @Test( + "Generate image using an invalid prompt", + arguments: NovaTestConstants.ImageGeneration.invalidImagePrompts + ) + func generateImageWithInvalidPrompt(prompt: String) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: ImageGenerationOutput = try await bedrock.generateImage( + prompt, + with: BedrockModel.nova_canvas + ) + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/ImageVariationTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/ImageVariationTests.swift new file mode 100644 index 00000000..c596d342 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/ImageVariationTests.swift @@ -0,0 +1,196 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// Image variation + +extension BedrockServiceTests { + + // Models + @Test( + "Generate image variation using an implemented model", + arguments: NovaTestConstants.imageGenerationModels + ) + func generateImageVariationWithValidModel(model: BedrockModel) async throws { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let output: ImageGenerationOutput = try await bedrock.generateImageVariation( + image: mockBase64Image, + prompt: "This is a test", + with: model, + nrOfImages: 3 + ) + #expect(output.images.count == 3) + } + + @Test( + "Generate image variation using an invalid model", + arguments: NovaTestConstants.textCompletionModels + ) + func generateImageVariationWithInvalidModel(model: BedrockModel) async throws { + await #expect(throws: BedrockServiceError.self) { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let _: ImageGenerationOutput = try await bedrock.generateImageVariation( + image: mockBase64Image, + prompt: "This is a test", + with: model, + nrOfImages: 3 + ) + } + } + + // NrOfImages + @Test( + "Generate image variation using a valid nrOfImages", + arguments: NovaTestConstants.ImageGeneration.validNrOfImages + ) + func generateImageVariationWithValidNrOfImages(nrOfImages: Int) async throws { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let output: ImageGenerationOutput = try await bedrock.generateImageVariation( + image: mockBase64Image, + prompt: "This is a test", + with: BedrockModel.nova_canvas, + nrOfImages: nrOfImages + ) + #expect(output.images.count == nrOfImages) + } + + @Test( + "Generate image variation using an invalid nrOfImages", + arguments: NovaTestConstants.ImageGeneration.invalidNrOfImages + ) + func generateImageVariationWithInvalidNrOfImages(nrOfImages: Int) async throws { + await #expect(throws: BedrockServiceError.self) { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let _: ImageGenerationOutput = try await bedrock.generateImageVariation( + image: mockBase64Image, + prompt: "This is a test", + with: BedrockModel.nova_canvas, + nrOfImages: nrOfImages + ) + } + } + + // Similarity + @Test( + "Generate image variation using a valid similarity", + arguments: NovaTestConstants.ImageVariation.validSimilarity + ) + func generateImageVariationWithValidSimilarity(similarity: Double) async throws { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let output: ImageGenerationOutput = try await bedrock.generateImageVariation( + image: mockBase64Image, + prompt: "This is a test", + with: BedrockModel.nova_canvas, + similarity: similarity, + nrOfImages: 3 + ) + #expect(output.images.count == 3) + } + + @Test( + "Generate image variation using an invalid similarity", + arguments: NovaTestConstants.ImageVariation.invalidSimilarity + ) + func generateImageVariationWithInvalidSimilarity(similarity: Double) async throws { + await #expect(throws: BedrockServiceError.self) { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let _: ImageGenerationOutput = try await bedrock.generateImageVariation( + image: mockBase64Image, + prompt: "This is a test", + with: BedrockModel.nova_canvas, + similarity: similarity, + nrOfImages: 3 + ) + } + } + + // Number of reference images + @Test( + "Generate image variation using a valid number of reference images", + arguments: NovaTestConstants.ImageVariation.validNrOfReferenceImages + ) + func generateImageVariationWithValidNrOfReferenceImages(nrOfReferenceImages: Int) async throws { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let mockImages = Array(repeating: mockBase64Image, count: nrOfReferenceImages) + let output: ImageGenerationOutput = try await bedrock.generateImageVariation( + images: mockImages, + prompt: "This is a test", + with: BedrockModel.nova_canvas + ) + #expect(output.images.count == 1) + } + + @Test( + "Generate image variation using an invalid number of reference images", + arguments: NovaTestConstants.ImageVariation.invalidNrOfReferenceImages + ) + func generateImageVariationWithInvalidNrOfReferenceImages(nrOfReferenceImages: Int) async throws { + await #expect(throws: BedrockServiceError.self) { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let mockImages = Array(repeating: mockBase64Image, count: nrOfReferenceImages) + let _: ImageGenerationOutput = try await bedrock.generateImageVariation( + images: mockImages, + prompt: "This is a test", + with: BedrockModel.nova_canvas + ) + } + } + + // Prompt + @Test( + "Generate image variation using a valid prompt", + arguments: NovaTestConstants.ImageGeneration.validImagePrompts + ) + func generateImageVariationWithValidPrompt(prompt: String) async throws { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let output: ImageGenerationOutput = try await bedrock.generateImageVariation( + image: mockBase64Image, + prompt: prompt, + with: BedrockModel.nova_canvas, + similarity: 0.6 + ) + #expect(output.images.count == 1) + } + + @Test( + "Generate image variation using an invalid prompt", + arguments: NovaTestConstants.ImageGeneration.invalidImagePrompts + ) + func generateImageVariationWithInvalidPrompt(prompt: String) async throws { + await #expect(throws: BedrockServiceError.self) { + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let _: ImageGenerationOutput = try await bedrock.generateImageVariation( + image: mockBase64Image, + prompt: prompt, + with: BedrockModel.nova_canvas, + similarity: 0.6 + ) + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/TextGenerationTests.swift b/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/TextGenerationTests.swift new file mode 100644 index 00000000..0f36ece4 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/InvokeModel/TextGenerationTests.swift @@ -0,0 +1,229 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockService +@testable import BedrockTypes + +// Text completion + +extension BedrockServiceTests { + + // Models + @Test( + "Complete text using an implemented model", + arguments: NovaTestConstants.textCompletionModels + ) + func completeTextWithValidModel(model: BedrockModel) async throws { + let completion: TextCompletion = try await bedrock.completeText( + "This is a test", + with: model + ) + #expect(completion.completion == "This is the textcompletion for: This is a test") + } + + @Test( + "Complete text using an invalid model", + arguments: NovaTestConstants.imageGenerationModels + ) + func completeTextWithInvalidModel(model: BedrockModel) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: TextCompletion = try await bedrock.completeText( + "This is a test", + with: model, + temperature: 0.8 + ) + } + } + + // Parameter combinations + @Test( + "Complete text using an implemented model and a valid combination of parameters" + ) + func completeTextWithValidModelValidParameters() async throws { + let completion: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + maxTokens: 512, + temperature: 0.5, + topK: 10, + stopSequences: ["END", "\n\nHuman:"] + ) + #expect(completion.completion == "This is the textcompletion for: This is a test") + } + + @Test( + "Complete text using an implemented model and an invalid combination of parameters" + ) + func completeTextWithInvalidModelInvalidParameters() async throws { + await #expect(throws: BedrockServiceError.self) { + let _: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_lite, + temperature: 0.5, + topP: 0.5 + ) + } + } + + // Temperature + @Test("Complete text using a valid temperature", arguments: NovaTestConstants.TextGeneration.validTemperature) + func completeTextWithValidTemperature(temperature: Double) async throws { + let completion: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + temperature: temperature + ) + #expect(completion.completion == "This is the textcompletion for: This is a test") + } + + @Test("Complete text using an invalid temperature", arguments: NovaTestConstants.TextGeneration.invalidTemperature) + func completeTextWithInvalidTemperature(temperature: Double) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + temperature: temperature + ) + } + } + + // MaxTokens + @Test( + "Complete text using a valid maxTokens", + arguments: NovaTestConstants.TextGeneration.validMaxTokens + ) + func completeTextWithValidMaxTokens(maxTokens: Int) async throws { + let completion: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + maxTokens: maxTokens + ) + #expect(completion.completion == "This is the textcompletion for: This is a test") + } + + @Test( + "Complete text using an invalid maxTokens", + arguments: NovaTestConstants.TextGeneration.invalidMaxTokens + ) + func completeTextWithInvalidMaxTokens(maxTokens: Int) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + maxTokens: maxTokens + ) + } + } + + // TopP + @Test( + "Complete text using a valid topP", + arguments: NovaTestConstants.TextGeneration.validTopP + ) + func completeTextWithValidTopP(topP: Double) async throws { + let completion: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + topP: topP + ) + #expect(completion.completion == "This is the textcompletion for: This is a test") + } + + @Test( + "Complete text using an invalid topP", + arguments: NovaTestConstants.TextGeneration.invalidTopP + ) + func completeTextWithInvalidMaxTokens(topP: Double) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + topP: topP + ) + } + } + + // TopK + @Test( + "Complete text using a valid topK", + arguments: NovaTestConstants.TextGeneration.validTopK + ) + func completeTextWithValidTopK(topK: Int) async throws { + let completion: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + topK: topK + ) + #expect(completion.completion == "This is the textcompletion for: This is a test") + } + + @Test( + "Complete text using an invalid topK", + arguments: NovaTestConstants.TextGeneration.invalidTopK + ) + func completeTextWithInvalidTopK(topK: Int) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + topK: topK + ) + } + } + + // StopSequences + @Test( + "Complete text using valid stopSequences", + arguments: NovaTestConstants.TextGeneration.validStopSequences + ) + func completeTextWithValidMaxTokens(stopSequences: [String]) async throws { + let completion: TextCompletion = try await bedrock.completeText( + "This is a test", + with: BedrockModel.nova_micro, + stopSequences: stopSequences + ) + #expect(completion.completion == "This is the textcompletion for: This is a test") + } + + // Prompt + @Test( + "Complete text using a valid prompt", + arguments: NovaTestConstants.TextGeneration.validPrompts + ) + func completeTextWithValidPrompt(prompt: String) async throws { + let completion: TextCompletion = try await bedrock.completeText( + prompt, + with: BedrockModel.nova_micro, + maxTokens: 200 + ) + #expect(completion.completion == "This is the textcompletion for: \(prompt)") + } + + @Test( + "Complete text using an invalid prompt", + arguments: NovaTestConstants.TextGeneration.invalidPrompts + ) + func completeTextWithInvalidPrompt(prompt: String) async throws { + await #expect(throws: BedrockServiceError.self) { + let _: TextCompletion = try await bedrock.completeText( + prompt, + with: BedrockModel.nova_canvas, + maxTokens: 10 + ) + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/Mock/MockBedrockClient.swift b/swift-bedrock-library/Tests/BedrockServiceTests/Mock/MockBedrockClient.swift new file mode 100644 index 00000000..51d523c0 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/Mock/MockBedrockClient.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrock +import AWSClientRuntime +import AWSSDKIdentity +import BedrockService +import BedrockTypes +import Foundation + +public struct MockBedrockClient: BedrockClientProtocol { + public init() {} + + public func listFoundationModels( + input: ListFoundationModelsInput + ) async throws + -> ListFoundationModelsOutput + { + ListFoundationModelsOutput( + // customizationsSupported: [BedrockClientTypes.ModelCustomization]? = nil, + // inferenceTypesSupported: [BedrockClientTypes.InferenceType]? = nil, + // inputModalities: [BedrockClientTypes.ModelModality]? = nil, + // modelArn: Swift.String? = nil, + // modelId: Swift.String? = nil, + // modelLifecycle: BedrockClientTypes.FoundationModelLifecycle? = nil, + // modelName: Swift.String? = nil, + // outputModalities: [BedrockClientTypes.ModelModality]? = nil, + // providerName: Swift.String? = nil, + // responseStreamingSupported: Swift.Bool? = nil + modelSummaries: [ + BedrockClientTypes.FoundationModelSummary( + modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-instant-v1", + modelId: "anthropic.claude-instant-v1", + modelLifecycle: BedrockClientTypes.FoundationModelLifecycle(status: .active), + modelName: "Claude Instant", + providerName: "Anthropic", + responseStreamingSupported: false + ), + BedrockClientTypes.FoundationModelSummary( + modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-instant-v2", + modelId: "anthropic.claude-instant-v2", + modelLifecycle: BedrockClientTypes.FoundationModelLifecycle(status: .active), + modelName: "Claude Instant 2", + providerName: "Anthropic", + responseStreamingSupported: true + ), + BedrockClientTypes.FoundationModelSummary( + modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-instant-v3", + modelId: "unknownID", + modelLifecycle: BedrockClientTypes.FoundationModelLifecycle(status: .active), + modelName: "Claude Instant 3", + providerName: "Anthropic", + responseStreamingSupported: false + ), + ]) + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/Mock/MockBedrockRuntimeClient.swift b/swift-bedrock-library/Tests/BedrockServiceTests/Mock/MockBedrockRuntimeClient.swift new file mode 100644 index 00000000..c99f8b70 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/Mock/MockBedrockRuntimeClient.swift @@ -0,0 +1,565 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import AWSClientRuntime +import AWSSDKIdentity +import BedrockService +import BedrockTypes +import Foundation + +public struct MockBedrockRuntimeClient: BedrockRuntimeClientProtocol { + public init() {} + + // MARK: converseStream + public func converseStream(input: ConverseStreamInput) async throws -> ConverseStreamOutput { + + guard let messages = input.messages, + let content = messages.last?.content?.last + else { + throw AWSBedrockRuntime.ValidationException(message: "Missing required message content") + } + + var stream: AsyncThrowingStream + + var reasoningEnabled: Bool? + var maxReasoningTokens: Int? + + if let additionalModelRequestFields = input.additionalModelRequestFields { + let json: JSON = JSON(with: additionalModelRequestFields) + reasoningEnabled = json["thinking"]?["enabled"] + maxReasoningTokens = json["thinking"]?["budget_tokens"] + } + + if let reasoningEnabled, reasoningEnabled, let maxReasoningTokens { + guard maxReasoningTokens >= 0 else { + throw AWSBedrockRuntime.ValidationException( + message: "Invalid reasoning budget tokens: \(maxReasoningTokens)" + ) + } + stream = getReasoningStream() + return ConverseStreamOutput(stream: stream) + } + + switch content { + case .text(let prompt): + if prompt == "Use tool", + input.toolConfig?.tools != nil + { + stream = getToolUseStream(for: "toolname") + } else { + stream = getTextStream(prompt) + } + case .toolresult(let block): + let toolUseId = block.toolUseId ?? "not found" + stream = getTextStream("Tool result received for toolUseId: \(toolUseId)") + // "Hello, your prompt was: Tool result received for toolUseId: \(toolUseId)" + case .image(_): + stream = getTextStream("Image received") + case .document(_): + stream = getTextStream("Document received") + case .video(_): + stream = getTextStream("Video received") + default: + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + return ConverseStreamOutput(stream: stream) + } + + // returns "Hello, your prompt was: \(textPrompt)" + private func getTextStream( + _ textPrompt: String = "Streaming Text" + ) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + // Message start + let messageStartEvent = BedrockRuntimeClientTypes.MessageStartEvent( + role: .assistant + ) + continuation.yield(.messagestart(messageStartEvent)) + + // Content block start + let contentBlockStartEvent = BedrockRuntimeClientTypes.ContentBlockStartEvent( + contentBlockIndex: 0, + start: nil + ) + continuation.yield(.contentblockstart(contentBlockStartEvent)) + + // Content block delta (first part) + let contentBlockDelta1 = BedrockRuntimeClientTypes.ContentBlockDelta.text( + "Hello, " + ) + let contentBlockDeltaEvent1 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta1 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent1)) + + // Content block delta (second part) + let contentBlockDelta2 = BedrockRuntimeClientTypes.ContentBlockDelta.text( + "your prompt " + ) + let contentBlockDeltaEvent2 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta2 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent2)) + + // Content block delta (third part) + let contentBlockDelta3 = BedrockRuntimeClientTypes.ContentBlockDelta.text( + "was: " + ) + let contentBlockDeltaEvent3 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta3 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent3)) + + // Content block delta (third part) + let contentBlockDelta4 = BedrockRuntimeClientTypes.ContentBlockDelta.text( + textPrompt + ) + let contentBlockDeltaEvent4 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta4 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent4)) + + // Content block stop + let contentBlockStopEvent = BedrockRuntimeClientTypes.ContentBlockStopEvent( + contentBlockIndex: 0 + ) + continuation.yield(.contentblockstop(contentBlockStopEvent)) + + // Message stop + let messageStopEvent = BedrockRuntimeClientTypes.MessageStopEvent( + additionalModelResponseFields: nil, + stopReason: nil + ) + continuation.yield(.messagestop(messageStopEvent)) + + continuation.finish() + } + } + + private func getToolUseStream( + for toolName: String + ) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + // Message start + let messageStartEvent = BedrockRuntimeClientTypes.MessageStartEvent( + role: .assistant + ) + continuation.yield(.messagestart(messageStartEvent)) + + // Content block start + let contentBlockStartEvent = BedrockRuntimeClientTypes.ContentBlockStartEvent( + contentBlockIndex: 0, + start: .tooluse(BedrockRuntimeClientTypes.ToolUseBlockStart(name: toolName, toolUseId: "tooluseid")) + ) + continuation.yield(.contentblockstart(contentBlockStartEvent)) + + // Content block delta + let contentBlockDelta = BedrockRuntimeClientTypes.ContentBlockDelta.tooluse( + BedrockRuntimeClientTypes.ToolUseBlockDelta(input: "{\"key\": \"ABC\"}") + ) + let contentBlockDeltaEvent = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent)) + + // Content block stop + let contentBlockStopEvent = BedrockRuntimeClientTypes.ContentBlockStopEvent( + contentBlockIndex: 0 + ) + continuation.yield(.contentblockstop(contentBlockStopEvent)) + + // Message stop + let messageStopEvent = BedrockRuntimeClientTypes.MessageStopEvent( + additionalModelResponseFields: nil, + stopReason: nil + ) + continuation.yield(.messagestop(messageStopEvent)) + + continuation.finish() + } + } + + private func getReasoningStream( + _ textPrompt: String = "Streaming Text" + ) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + // Message start + let messageStartEvent = BedrockRuntimeClientTypes.MessageStartEvent( + role: .assistant + ) + continuation.yield(.messagestart(messageStartEvent)) + + // Content block start 0 + let contentBlockStartEvent = BedrockRuntimeClientTypes.ContentBlockStartEvent( + contentBlockIndex: 0, + start: nil + ) + continuation.yield(.contentblockstart(contentBlockStartEvent)) + + // Content block delta (reasoning - first part) + let contentBlockDeltaReasoning1 = BedrockRuntimeClientTypes.ContentBlockDelta.reasoningcontent( + BedrockRuntimeClientTypes.ReasoningContentBlockDelta.text("reasoning ") + ) + let contentBlockDeltaReasoningEvent1 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDeltaReasoning1 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaReasoningEvent1)) + + // Content block delta (reasoning - second part) + let contentBlockDeltaReasoning2 = BedrockRuntimeClientTypes.ContentBlockDelta.reasoningcontent( + BedrockRuntimeClientTypes.ReasoningContentBlockDelta.text("text ") + ) + let contentBlockDeltaReasoningEvent2 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDeltaReasoning2 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaReasoningEvent2)) + + // Content block stop + let contentBlockStopEvent = BedrockRuntimeClientTypes.ContentBlockStopEvent( + contentBlockIndex: 0 + ) + continuation.yield(.contentblockstop(contentBlockStopEvent)) + + // Content block start 1 + let contentBlockStartEvent1 = BedrockRuntimeClientTypes.ContentBlockStartEvent( + contentBlockIndex: 1, + start: nil + ) + continuation.yield(.contentblockstart(contentBlockStartEvent1)) + + // Content block delta (first part) + let contentBlockDelta1 = BedrockRuntimeClientTypes.ContentBlockDelta.text( + "Hello, " + ) + let contentBlockDeltaEvent1 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 1, + delta: contentBlockDelta1 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent1)) + + // Content block delta (second part) + let contentBlockDelta2 = BedrockRuntimeClientTypes.ContentBlockDelta.text( + "your prompt " + ) + let contentBlockDeltaEvent2 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 1, + delta: contentBlockDelta2 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent2)) + + // Content block delta (third part) + let contentBlockDelta3 = BedrockRuntimeClientTypes.ContentBlockDelta.text( + "was: " + ) + let contentBlockDeltaEvent3 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 1, + delta: contentBlockDelta3 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent3)) + + // Content block delta (third part) + let contentBlockDelta4 = BedrockRuntimeClientTypes.ContentBlockDelta.text( + textPrompt + ) + let contentBlockDeltaEvent4 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 1, + delta: contentBlockDelta4 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent4)) + + // Content block stop + let contentBlockStopEvent1 = BedrockRuntimeClientTypes.ContentBlockStopEvent( + contentBlockIndex: 1 + ) + continuation.yield(.contentblockstop(contentBlockStopEvent1)) + + // Message stop + let messageStopEvent = BedrockRuntimeClientTypes.MessageStopEvent( + additionalModelResponseFields: nil, + stopReason: nil + ) + continuation.yield(.messagestop(messageStopEvent)) + + continuation.finish() + } + } + + // MARK: converse + public func converse(input: ConverseInput) async throws -> ConverseOutput { + guard let messages = input.messages, + let content = messages.last?.content?.last + else { + throw AWSBedrockRuntime.ValidationException(message: "Missing required message content") + } + + var replyContent: [BedrockRuntimeClientTypes.ContentBlock] = [] + guard let modelId = input.modelId else { + throw AWSBedrockRuntime.ValidationException(message: "Missing required modelId") + } + // Only for testing purposes: Claude 3.7 will always add a reasoning block, + // unless prompt "encrypted" is used + if modelId == "us.anthropic.claude-3-7-sonnet-20250219-v1:0" { + if case .text(let prompt) = content, prompt == "encrypted" { + let data: Data = try JSONEncoder().encode(["redacted": "data"]) + replyContent.append( + .reasoningcontent( + .redactedcontent(data) + ) + ) + } else { + replyContent.append( + .reasoningcontent( + .reasoningtext( + BedrockRuntimeClientTypes.ReasoningTextBlock( + signature: "reasoning signature", + text: "reasoning text" + ) + ) + ) + ) + } + } + + switch content { + case .text(let prompt): + if prompt == "Use tool", let _ = input.toolConfig?.tools { + let toolInputJson = JSON(with: ["code": "abc"]) + let toolInput = try? toolInputJson.toDocument() + replyContent.append( + .tooluse( + BedrockRuntimeClientTypes.ToolUseBlock( + input: toolInput, + name: "toolName", + toolUseId: "toolId" + ) + ) + ) + let message = BedrockRuntimeClientTypes.Message( + content: replyContent, + role: .assistant + ) + return ConverseOutput(output: .message(message)) + } + replyContent.append( + .text("Your prompt was: \(prompt)") + ) + case .toolresult(_): + replyContent.append(.text("Tool result received")) + case .image(_): + replyContent.append(.text("Image received")) + case .document(_): + replyContent.append(.text("Document received")) + default: + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + return ConverseOutput( + output: .message( + BedrockRuntimeClientTypes.Message( + content: replyContent, + role: .assistant + ) + ) + ) + } + + // MARK: invokeModel + + public func invokeModel(input: InvokeModelInput) async throws -> InvokeModelOutput { + guard let modelId = input.modelId else { + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + guard let inputBody = input.body else { + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + let model: BedrockModel = BedrockModel(rawValue: modelId)! + + switch model.modality.getName() { + case "Amazon Image Generation": + return InvokeModelOutput(body: try getImageGeneration(body: inputBody)) + case "Nova Text Generation": + return InvokeModelOutput(body: try invokeNovaModel(body: inputBody)) + case "Titan Text Generation": + return InvokeModelOutput(body: try invokeTitanModel(body: inputBody)) + case "Anthropic Text Generation": + return InvokeModelOutput(body: try invokeAnthropicModel(body: inputBody)) + default: + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + } + + private func getImageGeneration(body: Data) throws -> Data { + guard + let json: [String: Any] = try? JSONSerialization.jsonObject( + with: body, + options: [] + ) + as? [String: Any], + let imageGenerationConfig = json["imageGenerationConfig"] as? [String: Any] + else { + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + let nrOfImages = imageGenerationConfig["numberOfImages"] as? Int ?? 1 + let mockBase64Image = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" + let imageArray = Array(repeating: "\"\(mockBase64Image)\"", count: nrOfImages) + return """ + { + "images": [ + \(imageArray.joined(separator: ",\n ")) + ] + } + """.data(using: .utf8)! + } + + private func invokeNovaModel(body: Data) throws -> Data? { + guard + let json: [String: Any] = try? JSONSerialization.jsonObject( + with: body, + options: [] + ) + as? [String: Any] + else { + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + if let messages = json["messages"] as? [[String: Any]], + let firstMessage = messages.first, + let content = firstMessage["content"] as? [[String: Any]], + let firstContent = content.first, + let inputText = firstContent["text"] as? String + { + return """ + { + "output":{ + "message":{ + "content":[ + {"text":"This is the textcompletion for: \(inputText)"} + ], + "role":"assistant" + }}, + "stopReason":"end_turn", + "usage":{ + "inputTokens":5, + "outputTokens":244, + "totalTokens":249, + "cacheReadInputTokenCount":0, + "cacheWriteInputTokenCount":0 + } + } + """.data(using: .utf8)! + } else { + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + } + + private func invokeTitanModel(body: Data) throws -> Data? { + guard + let json: [String: Any] = try? JSONSerialization.jsonObject( + with: body, + options: [] + ) + as? [String: Any] + else { + throw AWSBedrockRuntime.ValidationException( + message: "Hier is het)" + // message: "Malformed input request, please reformat your input and try again." + ) + } + if let inputText = json["inputText"] as? String { + return """ + { + "inputTextTokenCount":5, + "results":[ + { + "tokenCount":105, + "outputText":"This is the textcompletion for: \(inputText)", + "completionReason":"FINISH" + } + ] + } + """.data(using: .utf8)! + } else { + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + } + + private func invokeAnthropicModel(body: Data) throws -> Data? { + guard + let json: [String: Any] = try? JSONSerialization.jsonObject( + with: body, + options: [] + ) + as? [String: Any] + else { + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + if let messages = json["messages"] as? [[String: Any]], + let firstMessage = messages.first, + let content = firstMessage["content"] as? [[String: Any]], + let firstContent = content.first, + let inputText = firstContent["text"] as? String + { + return """ + { + "id":"msg_bdrk_0146cw8Wd6Dn8WZiQWeF6TEj", + "type":"message", + "role":"assistant", + "model":"claude-3-haiku-20240307", + "content":[ + { + "type":"text", + "text":"This is the textcompletion for: \(inputText)" + }], + "stop_reason":"max_tokens", + "stop_sequence":null, + "usage":{ + "input_tokens":12, + "output_tokens":100} + } + """.data(using: .utf8)! + } else { + throw AWSBedrockRuntime.ValidationException( + message: "Malformed input request, please reformat your input and try again." + ) + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockServiceTests/NovaTestConstants.swift b/swift-bedrock-library/Tests/BedrockServiceTests/NovaTestConstants.swift new file mode 100644 index 00000000..0a6dd52e --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockServiceTests/NovaTestConstants.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import BedrockTypes + +/// Constants for testing based on the Nova parameters +enum NovaTestConstants { + + static let textCompletionModels = [ + BedrockModel.nova_micro, + BedrockModel.nova_lite, + BedrockModel.nova_pro, + ] + static let imageGenerationModels = [ + BedrockModel.titan_image_g1_v1, + BedrockModel.titan_image_g1_v2, + BedrockModel.nova_canvas, + ] + + enum TextGeneration { + static let validTemperature = [0.00001, 0.2, 0.6, 1] + static let invalidTemperature = [-2.5, -1, 0, 1.00001, 2] + static let validMaxTokens = [1, 10, 100, 5_000] + static let invalidMaxTokens = [0, -2, 5_001] + static let validTopP = [0, 0.2, 0.6, 1] + static let invalidTopP = [-1, 1.00001, 2] + static let validTopK = [0, 50] + static let invalidTopK = [-1] + static let validStopSequences = [ + ["\n\nHuman:"], + ["\n\nHuman:", "\n\nAI:"], + ["\n\nHuman:", "\n\nAI:", "\n\nHuman:"], + ] + static let validPrompts = [ + "This is a test", + "!@#$%^&*()_+{}|:<>?", + String(repeating: "test ", count: 10), + ] + static let invalidPrompts = [ + "", " ", " \n ", "\t", + ] + } + enum ImageGeneration { + static let validNrOfImages = [1, 2, 3, 4, 5] + static let invalidNrOfImages = [-4, 0, 6, 20] + static let validCfgScale = [1.1, 6, 10] + static let invalidCfgScale = [-4, 0, 1.0, 11, 20] + static let validSeed = [0, 12, 900, 858_993_459] + static let invalidSeed = [-4, 1_000_000_000] + static let validImagePrompts = [ + "This is a test", + "!@#$%^&*()_+{}|:<>?", + String(repeating: "x", count: 1_024), + ] + static let invalidImagePrompts = [ + "", " ", " \n ", "\t", + String(repeating: "x", count: 1_025), + ] + } + enum ImageVariation { + static let validSimilarity = [0.2, 0.5, 1] + static let invalidSimilarity = [-4, 0, 0.1, 1.1, 2] + static let validNrOfReferenceImages = [1, 3, 5] + static let invalidNrOfReferenceImages = [0, 6, 10] + } +} diff --git a/swift-bedrock-library/Tests/BedrockTypesTests/BedrockTypesTests.swift b/swift-bedrock-library/Tests/BedrockTypesTests/BedrockTypesTests.swift new file mode 100644 index 00000000..a848a382 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockTypesTests/BedrockTypesTests.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockTypes + +@Suite("BedrockTypes Tests") +struct BedrockTypesTests { + +} diff --git a/swift-bedrock-library/Tests/BedrockTypesTests/ConverseStream/ConverseReplyStreamTests.swift b/swift-bedrock-library/Tests/BedrockTypesTests/ConverseStream/ConverseReplyStreamTests.swift new file mode 100644 index 00000000..d779d081 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockTypesTests/ConverseStream/ConverseReplyStreamTests.swift @@ -0,0 +1,436 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@preconcurrency import AWSBedrockRuntime +import Testing + +@testable import BedrockTypes + +@Suite("ConverseReplyStreamTests") +struct ConverseReplyStreamTests { + + // Helper function to create a simulated stream with a single text block + func createSingleTextBlockStream() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + // Message start + let messageStartEvent = BedrockRuntimeClientTypes.MessageStartEvent( + role: .assistant + ) + continuation.yield(.messagestart(messageStartEvent)) + + // Content block start + let contentBlockStartEvent = BedrockRuntimeClientTypes.ContentBlockStartEvent( + contentBlockIndex: 0, + start: nil + ) + continuation.yield(.contentblockstart(contentBlockStartEvent)) + + // Content block delta (first part) + let contentBlockDelta1 = BedrockRuntimeClientTypes.ContentBlockDelta.text("Hello, ") + let contentBlockDeltaEvent1 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta1 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent1)) + + // Content block delta (second part) + let contentBlockDelta2 = BedrockRuntimeClientTypes.ContentBlockDelta.text("this is ") + let contentBlockDeltaEvent2 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta2 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent2)) + + // Content block delta (third part) + let contentBlockDelta3 = BedrockRuntimeClientTypes.ContentBlockDelta.text("a test message.") + let contentBlockDeltaEvent3 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta3 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent3)) + + // Content block stop + let contentBlockStopEvent = BedrockRuntimeClientTypes.ContentBlockStopEvent( + contentBlockIndex: 0 + ) + continuation.yield(.contentblockstop(contentBlockStopEvent)) + + // Message stop + let messageStopEvent = BedrockRuntimeClientTypes.MessageStopEvent( + additionalModelResponseFields: nil, + stopReason: nil + ) + continuation.yield(.messagestop(messageStopEvent)) + + continuation.finish() + } + } + + // Helper function to create a simulated stream with multiple content blocks + func createMultipleContentBlocksStream() -> AsyncThrowingStream< + BedrockRuntimeClientTypes.ConverseStreamOutput, Error + > { + AsyncThrowingStream { continuation in + // Message start + let messageStartEvent = BedrockRuntimeClientTypes.MessageStartEvent( + role: .assistant + ) + continuation.yield(.messagestart(messageStartEvent)) + + // First content block + let contentBlockStartEvent1 = BedrockRuntimeClientTypes.ContentBlockStartEvent( + contentBlockIndex: 0, + start: nil + ) + continuation.yield(.contentblockstart(contentBlockStartEvent1)) + + let contentBlockDelta1 = BedrockRuntimeClientTypes.ContentBlockDelta.text("First block content.") + let contentBlockDeltaEvent1 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta1 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent1)) + + let contentBlockStopEvent1 = BedrockRuntimeClientTypes.ContentBlockStopEvent( + contentBlockIndex: 0 + ) + continuation.yield(.contentblockstop(contentBlockStopEvent1)) + + // Second content block + let contentBlockStartEvent2 = BedrockRuntimeClientTypes.ContentBlockStartEvent( + contentBlockIndex: 1, + start: nil + ) + continuation.yield(.contentblockstart(contentBlockStartEvent2)) + + let contentBlockDelta2 = BedrockRuntimeClientTypes.ContentBlockDelta.text("Second block content.") + let contentBlockDeltaEvent2 = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 1, + delta: contentBlockDelta2 + ) + continuation.yield(.contentblockdelta(contentBlockDeltaEvent2)) + + let contentBlockStopEvent2 = BedrockRuntimeClientTypes.ContentBlockStopEvent( + contentBlockIndex: 1 + ) + continuation.yield(.contentblockstop(contentBlockStopEvent2)) + + // Message stop + let messageStopEvent = BedrockRuntimeClientTypes.MessageStopEvent( + additionalModelResponseFields: nil, + stopReason: nil + ) + continuation.yield(.messagestop(messageStopEvent)) + + continuation.finish() + } + } + + @Test("Test streaming text response") + func testStreamingTextResponse() async throws { + // Create the ConverseReplyStream from the simulated stream + let converseReplyStream = ConverseReplyStream(createSingleTextBlockStream()) + + // Collect all the stream elements + var streamElements: [ConverseStreamElement] = [] + for try await element in converseReplyStream.stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 5) + + // Check content segments + if case .contentSegment(let segment1) = streamElements[0] { + if case .text(let index1, let text1) = segment1 { + #expect(index1 == 0) + #expect(text1 == "Hello, ") + } else { + Issue.record("Expected text segment") + } + } else { + Issue.record("Expected contentSegment") + } + + if case .contentSegment(let segment2) = streamElements[1] { + if case .text(let index2, let text2) = segment2 { + #expect(index2 == 0) + #expect(text2 == "this is ") + } else { + Issue.record("Expected text segment") + } + } else { + Issue.record("Expected contentSegment") + } + + if case .contentSegment(let segment3) = streamElements[2] { + if case .text(let index3, let text3) = segment3 { + #expect(index3 == 0) + #expect(text3 == "a test message.") + } else { + Issue.record("Expected text segment") + } + } else { + Issue.record("Expected contentSegment") + } + + // Check content block complete + if case .contentBlockComplete(let index, let content) = streamElements[3] { + #expect(index == 0) + if case .text(let text) = content { + #expect(text == "Hello, this is a test message.") + } else { + Issue.record("Expected text content") + } + } else { + Issue.record("Expected contentBlockComplete") + } + + // Check message complete + if case .messageComplete(let message) = streamElements[4] { + #expect(message.role == .assistant) + #expect(message.content.count == 1) + if case .text(let text) = message.content[0] { + #expect(text == "Hello, this is a test message.") + } else { + Issue.record("Expected text content in message") + } + } else { + Issue.record("Expected messageComplete") + } + } + + @Test("Test multiple content blocks") + func testMultipleContentBlocks() async throws { + // Create the ConverseReplyStream from the simulated stream + let converseReplyStream = ConverseReplyStream(createMultipleContentBlocksStream()) + + // Collect all the stream elements + var streamElements: [ConverseStreamElement] = [] + for try await element in converseReplyStream.stream { + streamElements.append(element) + } + + // Verify the stream elements + #expect(streamElements.count == 5) + + // Check first content segment + if case .contentSegment(let segment1) = streamElements[0] { + if case .text(let index1, let text1) = segment1 { + #expect(index1 == 0) + #expect(text1 == "First block content.") + } else { + Issue.record("Expected text segment") + } + } else { + Issue.record("Expected contentSegment") + } + + // Check first content block complete + if case .contentBlockComplete(let index1, let content1) = streamElements[1] { + #expect(index1 == 0) + if case .text(let text1) = content1 { + #expect(text1 == "First block content.") + } else { + Issue.record("Expected text content") + } + } else { + Issue.record("Expected contentBlockComplete") + } + + // Check second content segment + if case .contentSegment(let segment2) = streamElements[2] { + if case .text(let index2, let text2) = segment2 { + #expect(index2 == 1) + #expect(text2 == "Second block content.") + } else { + Issue.record("Expected text segment") + } + } else { + Issue.record("Expected contentSegment") + } + + // Check second content block complete + if case .contentBlockComplete(let index2, let content2) = streamElements[3] { + #expect(index2 == 1) + if case .text(let text2) = content2 { + #expect(text2 == "Second block content.") + } else { + Issue.record("Expected text content") + } + } else { + Issue.record("Expected contentBlockComplete") + } + + // Check message complete + if case .messageComplete(let message) = streamElements[4] { + #expect(message.role == .assistant) + #expect(message.content.count == 2) + if case .text(let text1) = message.content[0] { + #expect(text1 == "First block content.") + } else { + Issue.record("Expected text content in first block") + } + if case .text(let text2) = message.content[1] { + #expect(text2 == "Second block content.") + } else { + Issue.record("Expected text content in second block") + } + } else { + Issue.record("Expected messageComplete") + } + } + + // Helper function to create a never-ending stream that will continue indefinitely + func createNeverEndingStream() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + // Message start + let messageStartEvent = BedrockRuntimeClientTypes.MessageStartEvent( + role: .assistant + ) + continuation.yield(.messagestart(messageStartEvent)) + + // Content block start + let contentBlockStartEvent = BedrockRuntimeClientTypes.ContentBlockStartEvent( + contentBlockIndex: 0, + start: nil + ) + continuation.yield(.contentblockstart(contentBlockStartEvent)) + + // Set up a counter to track how many deltas we've sent + var counter = 0 + + // Create a Task that will continuously send content block deltas + // This simulates a never-ending stream of tokens from the model + let continuousTask = Task { + while !Task.isCancelled { + // Create a content block delta with a counter to track progress + let text = "Token \(counter) " + let contentBlockDelta = BedrockRuntimeClientTypes.ContentBlockDelta.text(text) + let contentBlockDeltaEvent = BedrockRuntimeClientTypes.ContentBlockDeltaEvent( + contentBlockIndex: 0, + delta: contentBlockDelta + ) + + // Yield the delta + continuation.yield(.contentblockdelta(contentBlockDeltaEvent)) + + // Increment counter + counter += 1 + + // Add a small delay to avoid overwhelming the system + try await Task.sleep(nanoseconds: 10_000_000) // 10ms + } + + // If we get here, the task was cancelled + continuation.finish(throwing: CancellationError()) + } + + // When the stream is terminated, cancel our continuous task + // this is not necessary for the test, but it's a good practice + continuation.onTermination = { @Sendable _ in + continuousTask.cancel() + } + } + } + + @Test("Test cancellation of never-ending stream") + func testCancellationOfNeverEndingStream() async throws { + // Create the ConverseReplyStream from the simulated never-ending stream + let converseReplyStream = ConverseReplyStream(createNeverEndingStream()) + + // Create a task to consume the stream + let consumptionTask = Task { + var count = 0 + for try await element in converseReplyStream.stream { + if case .contentSegment = element { + count += 1 + } + } + // this will be reached if the stream finishes (which can not happen here by design) or is cancelled + return count + } + + // Wait a short time to ensure the stream has started producing elements + try await Task.sleep(nanoseconds: 100_000_000) // 100ms + + // Cancel the consumption task + consumptionTask.cancel() + + // Wait a short time to allow cancellation to propagate + try await Task.sleep(nanoseconds: 100_000_000) // 100ms + + // Try to get another element from the stream, this should return nil as the consumption task was cancelled, + // which should, in turn also cancel the stream + // in case the task was not cancelled, we will get a timeout + let elementReceived = try await performWithTimeout(of: Duration.seconds(0.5)) { + var receivedElementAfterCancellation = false + for try await _ in converseReplyStream.stream { + receivedElementAfterCancellation = true + break + } + return receivedElementAfterCancellation + } + // and we should not have receive any elements after cancellation + #expect(elementReceived == false) + } + + @Test("Test timeout handling") + func testTimeout() async throws { + + let _ = await #expect(throws: TimeoutError.self) { + try await performWithTimeout(of: .seconds(0.5)) { + // long task + try await Task.sleep(for: .seconds(1)) + } + } + } + + @Test("Test no timeout ") + func testNoTimeout() async throws { + await #expect(throws: Never.self) { + try await performWithTimeout(of: .seconds(1)) { + // long task + try await Task.sleep(for: .seconds(0.5)) + } + } + } + + enum TimeoutError: Error { + case timeout + } + + func performWithTimeout( + of timeout: Duration, + _ work: @Sendable @escaping () async throws -> T + ) async throws -> T { + try await withThrowingTaskGroup(of: T.self) { group in + // Start the actual work + group.addTask { + try await work() + } + // Start the timeout task + group.addTask { + try await Task.sleep(until: .now + timeout) + throw TimeoutError.timeout + } + // Return the result of the first task to finish + let result = try await group.next()! + group.cancelAll() // Cancel the other task + return result + } + } + +} diff --git a/swift-bedrock-library/Tests/BedrockTypesTests/JSONTests.swift b/swift-bedrock-library/Tests/BedrockTypesTests/JSONTests.swift new file mode 100644 index 00000000..3fa33102 --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockTypesTests/JSONTests.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Testing + +@testable import BedrockTypes + +// MARK: JSON +extension BedrockTypesTests { + + @Test("JSON getValue") + func jsonGetValue() async throws { + let json = JSON(with: [ + "name": JSON(with: "Jane Doe"), + "age": JSON(with: 30), + "isMember": JSON(with: true), + ]) + #expect(json.getValue("name") == "Jane Doe") + #expect(json.getValue("age") == 30) + #expect(json.getValue("isMember") == true) + #expect(json.getValue("nonExistentKey") == nil) + } + + @Test("JSON getValue nested") + func jsonGetValueNested() async throws { + let json = JSON(with: [ + "name": JSON(with: "Jane Doe"), + "age": JSON(with: 30), + "isMember": JSON(with: true), + "address": JSON(with: [ + "street": JSON(with: "123 Main St"), + "city": JSON(with: "Anytown"), + "state": JSON(with: "CA"), + "zip": JSON(with: "12345"), + "isSomething": JSON(with: true), + ]), + ]) + #expect(json.getValue("name") == "Jane Doe") + #expect(json.getValue("age") == 30) + #expect(json.getValue("isMember") == true) + #expect(json.getValue("nonExistentKey") == nil) + #expect(json["address"]?.getValue("street") == "123 Main St") + #expect(json["address"]?.getValue("city") == "Anytown") + #expect(json["address"]?.getValue("state") == "CA") + #expect(json["address"]?.getValue("zip") == "12345") + #expect(json["address"]?.getValue("isSomething") == true) + #expect(json["address"]?.getValue("nonExistentKey") == nil) + } + + @Test("JSON Subscript") + func jsonSubscript() async throws { + let json = JSON(with: [ + "name": JSON(with: "Jane Doe"), + "age": JSON(with: 30), + "isMember": JSON(with: true), + ]) + #expect(json["name"] == "Jane Doe") + #expect(json["age"] == 30) + #expect(json["isMember"] == true) + #expect(json["nonExistentKey"] == nil) + } + + @Test("JSON Subscript nested") + func jsonSubscriptNested() async throws { + let json = JSON(with: [ + "name": JSON(with: "Jane Doe"), + "age": JSON(with: 30), + "isMember": JSON(with: true), + "address": JSON(with: [ + "street": JSON(with: "123 Main St"), + "city": JSON(with: "Anytown"), + "state": JSON(with: "CA"), + "zip": JSON(with: 12345), + "isSomething": JSON(with: true), + ]), + ]) + #expect(json["name"] == "Jane Doe") + #expect(json["age"] == 30) + #expect(json["isMember"] == true) + #expect(json["nonExistentKey"] == nil) + #expect(json["address"]?["street"] == "123 Main St") + #expect(json["address"]?["city"] == "Anytown") + #expect(json["address"]?["state"] == "CA") + #expect(json["address"]?["zip"] == 12345) + #expect(json["address"]?["isSomething"] == true) + #expect(json["address"]?.getValue("nonExistentKey") == nil) + } + + @Test("JSON String Initializer with Valid String") + func jsonStringInitializer() async throws { + let validJSONString = """ + { + "name": "Jane Doe", + "age": 30, + "isMember": true + } + """ + + let json = try JSON(from: validJSONString) + #expect(json.getValue("name") == "Jane Doe") + #expect(json.getValue("age") == 30) + #expect(json.getValue("isMember") == true) + } + + @Test("JSON String Initializer with Invalid String") + func jsonInvalidStringInitializer() async throws { + let invalidJSONString = """ + { + "name": "Jane Doe", + "age": 30, + "isMember": true, + """ // Note: trailing comma, making this invalid + #expect(throws: BedrockServiceError.self) { + let _ = try JSON(from: invalidJSONString) + } + } +} diff --git a/swift-bedrock-library/Tests/BedrockTypesTests/ToolResultBlockTests.swift b/swift-bedrock-library/Tests/BedrockTypesTests/ToolResultBlockTests.swift new file mode 100644 index 00000000..017250bb --- /dev/null +++ b/swift-bedrock-library/Tests/BedrockTypesTests/ToolResultBlockTests.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Foundation Models Playground open source project +// +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates +// and the Swift Foundation Models Playground project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Foundation Models Playground project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Testing + +@testable import BedrockTypes + +// MARK: ToolResultBlockTests + +extension BedrockTypesTests { + + @Test("ToolResultBlock Initializer with ID and String Content") + func toolResultBlockInitializerWithString() async throws { + let block = ToolResultBlock("Hello, Swift!", id: "block1") + #expect(block.id == "block1") + #expect(block.content.count == 1) + var reply: String + if case .text(let text) = block.content.first { + reply = text + } else { + reply = "" + } + #expect(reply == "Hello, Swift!") + #expect(block.status == .success) + } + + @Test("ToolResultBlock Initializer with ID and JSON Content") + func toolResultBlockInitializerWithJSON() async throws { + let json = JSON(with: ["key": JSON(with: "value")]) + let block = ToolResultBlock(json, id: "block2") + #expect(block.id == "block2") + #expect(block.content.count == 1) + var value = "" + if case .json(let json) = block.content.first { + value = json.getValue("key") ?? "" + } + #expect(value == "value") + #expect(block.status == .success) + } + + @Test("ToolResultBlock Initializer with ID and Image Content") + func toolResultBlockInitializerWithImage() async throws { + let bytes = "mockmockmockmockmockmockmockmockmock" + let image = try ImageBlock(format: .jpeg, source: bytes) + let block = ToolResultBlock(image, id: "block3") + #expect(block.id == "block3") + #expect(block.content.count == 1) + var imageContent: ImageBlock = try ImageBlock(format: .png, source: "xx") + if case .image(let img) = block.content.first { + imageContent = img + } + var imageBytes = "" + if case .bytes(let string) = imageContent.source { + imageBytes = string + } + #expect(imageBytes == bytes) + #expect(imageContent.format == image.format) + #expect(block.status == .success) + } + + @Test("ToolResultBlock Initializer with Failed Status") + func toolResultBlockFailedInitializer() async throws { + let block = ToolResultBlock.failed("block4") + #expect(block.id == "block4") + #expect(block.content.isEmpty) + #expect(block.status == .error) + } + + @Test("ToolResultBlock Initializer with Data object") + func toolResultBlockCodable() async throws { + let data = """ + { + "key": "value" + } + """.data(using: .utf8)! + let block = try! ToolResultBlock(data, id: "block5") + + #expect(block.id == "block5") + #expect(block.content.count == 1) + var value = "" + if case .json(let json) = block.content.first { + value = json.getValue("key") ?? "" + } + #expect(value == "value") + #expect(block.status == block.status) + } + + @Test("ToolResultBlock Initializer with Codable Object") + func toolResultBlockInitializerWithCodableObject() async throws { + struct TestObject: Codable { + let name: String + let age: Int + } + let object = TestObject(name: "Jane", age: 30) + let block = try ToolResultBlock(object, id: "block6") + #expect(block.id == "block6") + #expect(block.content.count == 1) + var name = "" + var age = 0 + if case .json(let jsonContent) = block.content.first { + name = jsonContent.getValue("name") ?? "" + age = jsonContent.getValue("age") ?? 0 + } + #expect(name == "Jane") + #expect(age == 30) + } + + @Test("ToolResultBlock Initializer with Invalid Data Throws Error") + func toolResultBlockInitializerWithInvalidData() async throws { + let invalidData = Data([0x00, 0x01, 0x02]) // Invalid data for JSON decoding + + #expect(throws: BedrockServiceError.self) { + let _ = try ToolResultBlock(invalidData, id: "block7") + } + } +} diff --git a/swift-bedrock-library/parameter_cheatsheet.md b/swift-bedrock-library/parameter_cheatsheet.md new file mode 100644 index 00000000..2c95c3f6 --- /dev/null +++ b/swift-bedrock-library/parameter_cheatsheet.md @@ -0,0 +1,228 @@ + + +## Text Generation parameters + +### Nova +[user guide](https://docs.aws.amazon.com/nova/latest/userguide/complete-request-schema.html) + +| parameter | minValue | maxValue | defaultValue | optional or required | +| ----------- | -------- | --------- | ------------ | -------------------- | +| temperature | 0.00001 | 1 | 0.7 | optional | +| maxTokens | 1 | 5_000 | "dynamic"? | optional | +| topP | 0 | 1.0 | 0.9 | optional | +| topK | 0 | Not found | 50 | optional | + + +| parameter | maxLength | +| --------- | --------- | +| prompt | Not found | + + +| parameter | maxSequences | defaultVal | +| ------------- | ------------ | ---------- | +| stopSequences | Not found | `[]` | + +### Titan +[user guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-text.html) + +| parameter | minValue | maxValue | defaultValue | optional or required | +| ----------- | ------------- | ---------------- | ------------- | -------------------- | +| temperature | 0.0 | 1.0 | 0.7 | required | +| maxTokens | 0 | depends on model | 512 | required | +| topP | 0 | 1 | 0.9 | required | +| topK | Not supported | Not supported | Not supported | required | + +| model | max return tokens | +| ------------------ | ----------------- | +| Titan Text Lite | 4_096 | +| Titan Text Express | 8_192 | +| Titan Text Premier | 3_072 | + + +| parameter | maxLength | +| --------- | --------- | +| prompt | ??? | + + +| parameter | maxSequences | defaultVal | +| ------------- | ------------ | ---------- | +| stopSequences | ??? | `[]` | + +### Claude + +[user guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html) + +| parameter | minValue | maxValue | defaultValue | optional or required | +| ----------- | -------- | -------------------- | --------------------- | -------------------- | +| temperature | 0 | 1 | 1 | optional | +| maxTokens | 1 | depends on the model | Not found | required | +| topP | 0 | 1 | 0.999 | optional | +| topK | 0 | 500 | "disabled by default" | optional | + +[model comparison](https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison) + +| models | max return tokens | +| --------------------------------- | ----------------- | +| 3 Opus | 4_096 | +| 3 Haiku | 4_096 | +| 3.5 Haiku | 8_192 | +| 3.5 Sonnet | 8_192 | +| 3.7 Sonnet | 8_192 | +| 3.7 Sonnet with extended thinking | 64_000 | + +| parameter | maxLength | +| --------- | --------- | +| prompt | 200_000 | + + +| parameter | maxSequences | defaultVal | +| ------------- | ------------ | ---------- | +| stopSequences | 8191 | `[]` | + +### DeepSeek + +[user guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-deepseek.html) +[deepseek docs](https://api-docs.deepseek.com/quick_start/parameter_settings) + +| parameter | minValue | maxValue | defaultValue | optional or required | +| ----------- | ------------- | ------------- | ------------- | -------------------- | +| temperature | 0 | 1 | 1 | required | +| maxTokens | 1 | 32_768 | Not found | required | +| topP | 0 | 1 | Not found | required | +| topK | Not supported | Not supported | Not supported | required | + + +| parameter | maxLength | +| --------- | --------- | +| prompt | Not found | + + +| parameter | maxSequences | defaultVal | +| ------------- | ------------ | ---------- | +| stopSequences | 10 | `[]` | + +### Llama + +[user guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-text.html) + +- Llama 3 Instruct +- Llama 3.1 Instruct +- Llama 3.2 Instruct +- Llama 3.3 Instruct + +| parameter | minValue | maxValue | defaultValue | optional or required | +| ----------- | ------------- | ------------- | ------------- | -------------------- | +| temperature | 0 | 1 | 0.5 | optional | +| maxTokens | 1 | 2_048 | 512 | optional | +| topP | 0 | 1 | 0.9 | optional | +| topK | Not supported | Not supported | Not supported | optional | + + +| parameter | maxLength | +| --------- | --------- | +| prompt | Not found | + + +| parameter | maxSequences | defaultVal | +| ------------- | ------------- | ------------- | +| stopSequences | Not supported | Not supported | + + +## Image Generation parameters + +### Nova +[user guide](https://docs.aws.amazon.com/nova/latest/userguide/image-gen-req-resp-structure.html) + +#### General + +| parameter | minValue | maxValue | defaultValue | +| ---------- | -------- | ----------- | ------------ | +| nrOfImages | 1 | 5 | 1 | +| cfgScale | 1.1 | 10 | 6.5 | +| seed | 0 | 858_993_459 | 12 | + +#### TEXT_IMAGE + +| parameter | maxLength | +| -------------- | --------- | +| prompt | 1_024 | +| negativePrompt | 1_024 | + +#### Conditioned TEXT_IMAGE + +| parameter | minValue | maxValue | defaultValue | +| ---------- | -------- | -------- | ------------ | +| similarity | 0 | 1.0 | 0.7 | + +#### IMAGE_VARIATION +| parameter | minValue | maxValue | defaultValue | +| ---------- | -------- | -------- | ------------ | +| similarity | 0.2 | 1.0 | ??? | +| images | 1 | 5 | ??? | + +| parameter | maxLength | +| -------------- | --------- | +| prompt | 1_024 | +| negativePrompt | 1_024 | + + +#### COLOR_GUIDED_GENERATION + +| parameter | maxLength | +| -------------- | --------- | +| prompt | 1_024 | +| negativePrompt | 1_024 | +| colors | 10 | + +#### TO DO +| parameter | minValue | maxValue | defaultValue | +| --------- | -------- | -------- | ------------ | + + +| parameter | maxLength | +| --------- | --------- | + + +### Titan + +[user guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-image.html) + +#### Algemeen +| parameter | minValue | maxValue | defaultValue | +| ---------- | -------- | ------------- | ------------ | +| nrOfImages | 1 | 5 | 1 | +| cfgScale | 1.1 | 10.0 | 8.0 | +| seed | 0 | 2_147_483_646 | 42 | + +#### TEXT_IMAGE + +| parameter | maxLength | +| -------------- | --------- | +| prompt | 512 | +| negativePrompt | 512 | + +#### Conditioned TEXT_IMAGE + +| parameter | minValue | maxValue | defaultValue | +| ---------- | -------- | -------- | ------------ | +| similarity | 0 | 1.0 | 0.7 | + +#### IMAGE_VARIATION +| parameter | minValue | maxValue | defaultValue | +| ---------- | -------- | -------- | ------------ | +| similarity | 0.2 | 1.0 | 0.7 | +| images | 1 | 5 | ??? | + +| parameter | maxLength | +| -------------- | --------- | +| prompt | 512 | +| negativePrompt | 512 | + +#### COLOR_GUIDED_GENERATION + +| parameter | maxLength | +| -------------- | --------- | +| prompt | 512 | +| negativePrompt | 512 | +| colors | 10 | + diff --git a/web-playground/README.md b/web-playground/README.md new file mode 100644 index 00000000..9e9ea60d --- /dev/null +++ b/web-playground/README.md @@ -0,0 +1,107 @@ +# Swift FM Playground + +Welcome to the Swift Foundation Model (FM) Playground, an example app to explore how to use **Amazon Bedrock** with the AWS SDK for Swift. + +> 🚨 **Important:** This application is for educational purposes and not intended for production use. + +## Overview + +The Swift FM Playground is a web application that demonstrates how to use Amazon Bedrock foundation models with the AWS SDK for Swift. It consists of: + +- A Swift backend that interfaces with Amazon Bedrock +- A React frontend that provides a user-friendly interface for interacting with the models + +## Prerequisites + +- [Swift 6.0](https://www.swift.org/download/) or later +- [Node.js](https://nodejs.org/) 18 or later +- [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/) +- AWS account with access to Amazon Bedrock +- AWS credentials configured locally + +## Running the Application + +### Backend Setup + +1. Navigate to the backend directory: + ```bash + cd backend + ``` + +2. Build and run the Swift backend: + ```bash + swift build + swift run + ``` + + The backend server will start on port 8080 by default. + +#### Advanced Backend Configuration + +You can customize the backend behavior with the following options: + +- **Change Log Level**: + ```bash + swift run PlaygroundAPI --log-level debug + ``` + Available log levels: trace, debug, info, notice, warning, error, critical + + You can also use the LOG_LEVEL environment variable. + + ```bash + LOG_LEVEL=trace swift run + ``` + +- **Use AWS SSO Authentication**: + ```bash + swift run PlaygroundAPI --sso + ``` + +- **Specify AWS Profile**: + ```bash + swift run PlaygroundAPI --profile-name my-profile + ``` + +- **Combined Options**: + ```bash + swift run PlaygroundAPI --sso --profile-name my-profile --log-level debug + ``` + +### Frontend Setup + +1. Navigate to the frontend directory: + ```bash + cd frontend + ``` + +2. Install dependencies: + ```bash + npm install + # or if you use yarn + yarn install + ``` + +3. Start the React development server: + ```bash + npm run dev + # or if you use yarn + yarn dev + ``` + + The frontend development server will start on port 3000. + +## Accessing the Application + +To access the application, open `http://localhost:3000` in your web browser. + +## Stopping the Application + +To halt the application, you will need to stop both the backend and frontend processes. + +### Stopping the Frontend + +In the terminal where the frontend is running, press `Ctrl + C` to terminate the process. + +### Stopping the Backend + +Similarly, in the backend terminal, use the `Ctrl + C` shortcut to stop the server. diff --git a/backend/Package.swift b/web-playground/backend/Package.swift similarity index 91% rename from backend/Package.swift rename to web-playground/backend/Package.swift index 99c425da..993ae81f 100644 --- a/backend/Package.swift +++ b/web-playground/backend/Package.swift @@ -12,7 +12,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), - .package(url: "https://github.com/sebsto/swift-bedrock-library.git", branch: "main"), + .package(name: "swift-bedrock-library", path: "../.."), ], targets: [ .executableTarget( diff --git a/backend/Sources/PlaygroundAPI/App.swift b/web-playground/backend/Sources/PlaygroundAPI/App.swift similarity index 100% rename from backend/Sources/PlaygroundAPI/App.swift rename to web-playground/backend/Sources/PlaygroundAPI/App.swift diff --git a/backend/Sources/PlaygroundAPI/Application+build.swift b/web-playground/backend/Sources/PlaygroundAPI/Application+build.swift similarity index 100% rename from backend/Sources/PlaygroundAPI/Application+build.swift rename to web-playground/backend/Sources/PlaygroundAPI/Application+build.swift diff --git a/backend/Sources/PlaygroundAPI/Types/Chat.swift b/web-playground/backend/Sources/PlaygroundAPI/Types/Chat.swift similarity index 100% rename from backend/Sources/PlaygroundAPI/Types/Chat.swift rename to web-playground/backend/Sources/PlaygroundAPI/Types/Chat.swift diff --git a/backend/Sources/PlaygroundAPI/Types/ImageGeneration.swift b/web-playground/backend/Sources/PlaygroundAPI/Types/ImageGeneration.swift similarity index 100% rename from backend/Sources/PlaygroundAPI/Types/ImageGeneration.swift rename to web-playground/backend/Sources/PlaygroundAPI/Types/ImageGeneration.swift diff --git a/backend/Sources/PlaygroundAPI/Types/ListModels.swift b/web-playground/backend/Sources/PlaygroundAPI/Types/ListModels.swift similarity index 100% rename from backend/Sources/PlaygroundAPI/Types/ListModels.swift rename to web-playground/backend/Sources/PlaygroundAPI/Types/ListModels.swift diff --git a/backend/Sources/PlaygroundAPI/Types/TextGeneration.swift b/web-playground/backend/Sources/PlaygroundAPI/Types/TextGeneration.swift similarity index 100% rename from backend/Sources/PlaygroundAPI/Types/TextGeneration.swift rename to web-playground/backend/Sources/PlaygroundAPI/Types/TextGeneration.swift diff --git a/backend/img/image.png b/web-playground/backend/img/image.png similarity index 100% rename from backend/img/image.png rename to web-playground/backend/img/image.png diff --git a/frontend/app/app.config.js b/web-playground/frontend/app/app.config.js similarity index 100% rename from frontend/app/app.config.js rename to web-playground/frontend/app/app.config.js diff --git a/frontend/app/chat/loading.js b/web-playground/frontend/app/chat/loading.js similarity index 100% rename from frontend/app/chat/loading.js rename to web-playground/frontend/app/chat/loading.js diff --git a/frontend/app/chat/page.js b/web-playground/frontend/app/chat/page.js similarity index 100% rename from frontend/app/chat/page.js rename to web-playground/frontend/app/chat/page.js diff --git a/frontend/app/favicon.ico b/web-playground/frontend/app/favicon.ico similarity index 100% rename from frontend/app/favicon.ico rename to web-playground/frontend/app/favicon.ico diff --git a/frontend/app/globals.css b/web-playground/frontend/app/globals.css similarity index 100% rename from frontend/app/globals.css rename to web-playground/frontend/app/globals.css diff --git a/frontend/app/image/loading.js b/web-playground/frontend/app/image/loading.js similarity index 100% rename from frontend/app/image/loading.js rename to web-playground/frontend/app/image/loading.js diff --git a/frontend/app/image/page.js b/web-playground/frontend/app/image/page.js similarity index 100% rename from frontend/app/image/page.js rename to web-playground/frontend/app/image/page.js diff --git a/frontend/app/image_variation/loading.js b/web-playground/frontend/app/image_variation/loading.js similarity index 100% rename from frontend/app/image_variation/loading.js rename to web-playground/frontend/app/image_variation/loading.js diff --git a/frontend/app/image_variation/page.js b/web-playground/frontend/app/image_variation/page.js similarity index 100% rename from frontend/app/image_variation/page.js rename to web-playground/frontend/app/image_variation/page.js diff --git a/frontend/app/layout.js b/web-playground/frontend/app/layout.js similarity index 100% rename from frontend/app/layout.js rename to web-playground/frontend/app/layout.js diff --git a/frontend/app/models/[modelId]/loading.js b/web-playground/frontend/app/models/[modelId]/loading.js similarity index 100% rename from frontend/app/models/[modelId]/loading.js rename to web-playground/frontend/app/models/[modelId]/loading.js diff --git a/frontend/app/models/[modelId]/page.js b/web-playground/frontend/app/models/[modelId]/page.js similarity index 100% rename from frontend/app/models/[modelId]/page.js rename to web-playground/frontend/app/models/[modelId]/page.js diff --git a/frontend/app/models/loading.js b/web-playground/frontend/app/models/loading.js similarity index 100% rename from frontend/app/models/loading.js rename to web-playground/frontend/app/models/loading.js diff --git a/frontend/app/models/page.js b/web-playground/frontend/app/models/page.js similarity index 100% rename from frontend/app/models/page.js rename to web-playground/frontend/app/models/page.js diff --git a/frontend/app/page.js b/web-playground/frontend/app/page.js similarity index 100% rename from frontend/app/page.js rename to web-playground/frontend/app/page.js diff --git a/frontend/app/reasoning_chat/loading.js b/web-playground/frontend/app/reasoning_chat/loading.js similarity index 100% rename from frontend/app/reasoning_chat/loading.js rename to web-playground/frontend/app/reasoning_chat/loading.js diff --git a/frontend/app/reasoning_chat/page.js b/web-playground/frontend/app/reasoning_chat/page.js similarity index 100% rename from frontend/app/reasoning_chat/page.js rename to web-playground/frontend/app/reasoning_chat/page.js diff --git a/frontend/app/text/loading.js b/web-playground/frontend/app/text/loading.js similarity index 100% rename from frontend/app/text/loading.js rename to web-playground/frontend/app/text/loading.js diff --git a/frontend/app/text/page.js b/web-playground/frontend/app/text/page.js similarity index 100% rename from frontend/app/text/page.js rename to web-playground/frontend/app/text/page.js diff --git a/frontend/components/Content.jsx b/web-playground/frontend/components/Content.jsx similarity index 100% rename from frontend/components/Content.jsx rename to web-playground/frontend/components/Content.jsx diff --git a/frontend/components/Header.jsx b/web-playground/frontend/components/Header.jsx similarity index 100% rename from frontend/components/Header.jsx rename to web-playground/frontend/components/Header.jsx diff --git a/frontend/components/Navigation.jsx b/web-playground/frontend/components/Navigation.jsx similarity index 100% rename from frontend/components/Navigation.jsx rename to web-playground/frontend/components/Navigation.jsx diff --git a/frontend/components/NumericInput.jsx b/web-playground/frontend/components/NumericInput.jsx similarity index 100% rename from frontend/components/NumericInput.jsx rename to web-playground/frontend/components/NumericInput.jsx diff --git a/frontend/components/Spinner.jsx b/web-playground/frontend/components/Spinner.jsx similarity index 100% rename from frontend/components/Spinner.jsx rename to web-playground/frontend/components/Spinner.jsx diff --git a/frontend/components/chatPlayground/Assistant.jsx b/web-playground/frontend/components/chatPlayground/Assistant.jsx similarity index 100% rename from frontend/components/chatPlayground/Assistant.jsx rename to web-playground/frontend/components/chatPlayground/Assistant.jsx diff --git a/frontend/components/chatPlayground/ChatComponent.jsx b/web-playground/frontend/components/chatPlayground/ChatComponent.jsx similarity index 100% rename from frontend/components/chatPlayground/ChatComponent.jsx rename to web-playground/frontend/components/chatPlayground/ChatComponent.jsx diff --git a/frontend/components/chatPlayground/ChatModelSelector.jsx b/web-playground/frontend/components/chatPlayground/ChatModelSelector.jsx similarity index 100% rename from frontend/components/chatPlayground/ChatModelSelector.jsx rename to web-playground/frontend/components/chatPlayground/ChatModelSelector.jsx diff --git a/frontend/components/chatPlayground/Human.jsx b/web-playground/frontend/components/chatPlayground/Human.jsx similarity index 100% rename from frontend/components/chatPlayground/Human.jsx rename to web-playground/frontend/components/chatPlayground/Human.jsx diff --git a/frontend/components/chatPlayground/Loader.jsx b/web-playground/frontend/components/chatPlayground/Loader.jsx similarity index 100% rename from frontend/components/chatPlayground/Loader.jsx rename to web-playground/frontend/components/chatPlayground/Loader.jsx diff --git a/frontend/components/chatPlayground/ModelIndicator.jsx b/web-playground/frontend/components/chatPlayground/ModelIndicator.jsx similarity index 100% rename from frontend/components/chatPlayground/ModelIndicator.jsx rename to web-playground/frontend/components/chatPlayground/ModelIndicator.jsx diff --git a/frontend/components/foundationModels/ModelDetails.jsx b/web-playground/frontend/components/foundationModels/ModelDetails.jsx similarity index 100% rename from frontend/components/foundationModels/ModelDetails.jsx rename to web-playground/frontend/components/foundationModels/ModelDetails.jsx diff --git a/frontend/components/imagePlayground/ImageComponent.jsx b/web-playground/frontend/components/imagePlayground/ImageComponent.jsx similarity index 100% rename from frontend/components/imagePlayground/ImageComponent.jsx rename to web-playground/frontend/components/imagePlayground/ImageComponent.jsx diff --git a/frontend/components/imagePlayground/ImageModelSelector.jsx b/web-playground/frontend/components/imagePlayground/ImageModelSelector.jsx similarity index 100% rename from frontend/components/imagePlayground/ImageModelSelector.jsx rename to web-playground/frontend/components/imagePlayground/ImageModelSelector.jsx diff --git a/frontend/components/imagePlayground/StyleSelector.jsx b/web-playground/frontend/components/imagePlayground/StyleSelector.jsx similarity index 100% rename from frontend/components/imagePlayground/StyleSelector.jsx rename to web-playground/frontend/components/imagePlayground/StyleSelector.jsx diff --git a/frontend/components/imageVariationPlayground/ImageModelSelector.jsx b/web-playground/frontend/components/imageVariationPlayground/ImageModelSelector.jsx similarity index 100% rename from frontend/components/imageVariationPlayground/ImageModelSelector.jsx rename to web-playground/frontend/components/imageVariationPlayground/ImageModelSelector.jsx diff --git a/frontend/components/imageVariationPlayground/ImageVariationComponent.jsx b/web-playground/frontend/components/imageVariationPlayground/ImageVariationComponent.jsx similarity index 100% rename from frontend/components/imageVariationPlayground/ImageVariationComponent.jsx rename to web-playground/frontend/components/imageVariationPlayground/ImageVariationComponent.jsx diff --git a/frontend/components/imageVariationPlayground/StyleSelector.jsx b/web-playground/frontend/components/imageVariationPlayground/StyleSelector.jsx similarity index 100% rename from frontend/components/imageVariationPlayground/StyleSelector.jsx rename to web-playground/frontend/components/imageVariationPlayground/StyleSelector.jsx diff --git a/frontend/components/reasoningChatPlayground/Assistant.jsx b/web-playground/frontend/components/reasoningChatPlayground/Assistant.jsx similarity index 100% rename from frontend/components/reasoningChatPlayground/Assistant.jsx rename to web-playground/frontend/components/reasoningChatPlayground/Assistant.jsx diff --git a/frontend/components/reasoningChatPlayground/Human.jsx b/web-playground/frontend/components/reasoningChatPlayground/Human.jsx similarity index 100% rename from frontend/components/reasoningChatPlayground/Human.jsx rename to web-playground/frontend/components/reasoningChatPlayground/Human.jsx diff --git a/frontend/components/reasoningChatPlayground/Loader.jsx b/web-playground/frontend/components/reasoningChatPlayground/Loader.jsx similarity index 100% rename from frontend/components/reasoningChatPlayground/Loader.jsx rename to web-playground/frontend/components/reasoningChatPlayground/Loader.jsx diff --git a/frontend/components/reasoningChatPlayground/ModelIndicator.jsx b/web-playground/frontend/components/reasoningChatPlayground/ModelIndicator.jsx similarity index 100% rename from frontend/components/reasoningChatPlayground/ModelIndicator.jsx rename to web-playground/frontend/components/reasoningChatPlayground/ModelIndicator.jsx diff --git a/frontend/components/reasoningChatPlayground/ReasoningChatComponent.jsx b/web-playground/frontend/components/reasoningChatPlayground/ReasoningChatComponent.jsx similarity index 100% rename from frontend/components/reasoningChatPlayground/ReasoningChatComponent.jsx rename to web-playground/frontend/components/reasoningChatPlayground/ReasoningChatComponent.jsx diff --git a/frontend/components/reasoningChatPlayground/ReasoningModelSelector.jsx b/web-playground/frontend/components/reasoningChatPlayground/ReasoningModelSelector.jsx similarity index 100% rename from frontend/components/reasoningChatPlayground/ReasoningModelSelector.jsx rename to web-playground/frontend/components/reasoningChatPlayground/ReasoningModelSelector.jsx diff --git a/frontend/components/textPlayground/TextComponent.jsx b/web-playground/frontend/components/textPlayground/TextComponent.jsx similarity index 100% rename from frontend/components/textPlayground/TextComponent.jsx rename to web-playground/frontend/components/textPlayground/TextComponent.jsx diff --git a/frontend/components/textPlayground/TextModelSelector.jsx b/web-playground/frontend/components/textPlayground/TextModelSelector.jsx similarity index 100% rename from frontend/components/textPlayground/TextModelSelector.jsx rename to web-playground/frontend/components/textPlayground/TextModelSelector.jsx diff --git a/frontend/components/textPlayground/Textarea.jsx b/web-playground/frontend/components/textPlayground/Textarea.jsx similarity index 100% rename from frontend/components/textPlayground/Textarea.jsx rename to web-playground/frontend/components/textPlayground/Textarea.jsx diff --git a/frontend/helpers/chatModelData.js b/web-playground/frontend/helpers/chatModelData.js similarity index 100% rename from frontend/helpers/chatModelData.js rename to web-playground/frontend/helpers/chatModelData.js diff --git a/frontend/helpers/imageModelData.js b/web-playground/frontend/helpers/imageModelData.js similarity index 100% rename from frontend/helpers/imageModelData.js rename to web-playground/frontend/helpers/imageModelData.js diff --git a/frontend/helpers/modelData.js b/web-playground/frontend/helpers/modelData.js similarity index 100% rename from frontend/helpers/modelData.js rename to web-playground/frontend/helpers/modelData.js diff --git a/frontend/helpers/reasoningModelData.js b/web-playground/frontend/helpers/reasoningModelData.js similarity index 100% rename from frontend/helpers/reasoningModelData.js rename to web-playground/frontend/helpers/reasoningModelData.js diff --git a/frontend/jsconfig.json b/web-playground/frontend/jsconfig.json similarity index 100% rename from frontend/jsconfig.json rename to web-playground/frontend/jsconfig.json diff --git a/frontend/next.config.js b/web-playground/frontend/next.config.js similarity index 100% rename from frontend/next.config.js rename to web-playground/frontend/next.config.js diff --git a/frontend/package-lock.json b/web-playground/frontend/package-lock.json similarity index 100% rename from frontend/package-lock.json rename to web-playground/frontend/package-lock.json diff --git a/frontend/package.json b/web-playground/frontend/package.json similarity index 100% rename from frontend/package.json rename to web-playground/frontend/package.json diff --git a/frontend/postcss.config.js b/web-playground/frontend/postcss.config.js similarity index 100% rename from frontend/postcss.config.js rename to web-playground/frontend/postcss.config.js diff --git a/frontend/public/next.svg b/web-playground/frontend/public/next.svg similarity index 100% rename from frontend/public/next.svg rename to web-playground/frontend/public/next.svg diff --git a/frontend/public/placeholder.png b/web-playground/frontend/public/placeholder.png similarity index 100% rename from frontend/public/placeholder.png rename to web-playground/frontend/public/placeholder.png diff --git a/frontend/public/vercel.svg b/web-playground/frontend/public/vercel.svg similarity index 100% rename from frontend/public/vercel.svg rename to web-playground/frontend/public/vercel.svg diff --git a/frontend/tailwind.config.js b/web-playground/frontend/tailwind.config.js similarity index 100% rename from frontend/tailwind.config.js rename to web-playground/frontend/tailwind.config.js diff --git a/openapi.yaml b/web-playground/openapi.yaml similarity index 100% rename from openapi.yaml rename to web-playground/openapi.yaml