diff --git a/lib/ex_aws/bedrock/nova/content.ex b/lib/ex_aws/bedrock/nova/content.ex new file mode 100644 index 0000000..30db0b0 --- /dev/null +++ b/lib/ex_aws/bedrock/nova/content.ex @@ -0,0 +1,72 @@ +defmodule ExAws.Bedrock.Nova.Content do + @moduledoc """ + Constructor functions for dynamic content items (Text, Image, Video, Audio). + """ + + @doc "Text content item" + def text(string) when is_binary(string), do: %{text: string} + + @doc "Image content item" + def image(format, bytes_or_base64) when format in ~w(jpeg png gif webp) do + %{ + image: %{ + format: format, + source: %{bytes: bytes_or_base64} + } + } + end + + @doc "Video content item via bytes/base64" + def video_bytes(format, bytes_or_base64) + when format in ~w(mkv mov mp4 webm three_gp flv mpeg mpg wmv) do + %{ + video: %{ + format: format, + source: %{bytes: bytes_or_base64} + } + } + end + + @doc "Video content item via S3 Location" + def video_s3(format, uri, bucket_owner \\ nil) + when format in ~w(mkv mov mp4 webm three_gp flv mpeg mpg wmv) do + s3_loc = + then(%{uri: uri}, fn map -> + if bucket_owner, do: Map.put(map, :bucketOwner, bucket_owner), else: map + end) + + %{ + video: %{ + format: format, + source: %{s3Location: s3_loc} + } + } + end + + @doc "Audio content item via bytes/base64" + def audio_bytes(format, bytes_or_base64) + when format in ~w(mp3 opus wav aac flac mp4 ogg mkv) do + %{ + audio: %{ + format: format, + source: %{bytes: bytes_or_base64} + } + } + end + + @doc "Audio content item via S3 Location" + def audio_s3(format, uri, bucket_owner \\ nil) + when format in ~w(mp3 opus wav aac flac mp4 ogg mkv) do + s3_loc = + then(%{uri: uri}, fn map -> + if bucket_owner, do: Map.put(map, :bucketOwner, bucket_owner), else: map + end) + + %{ + audio: %{ + format: format, + source: %{s3Location: s3_loc} + } + } + end +end diff --git a/lib/ex_aws/bedrock/nova/inference_config.ex b/lib/ex_aws/bedrock/nova/inference_config.ex new file mode 100644 index 0000000..d8d69b8 --- /dev/null +++ b/lib/ex_aws/bedrock/nova/inference_config.ex @@ -0,0 +1,53 @@ +defmodule ExAws.Bedrock.Nova.InferenceConfig do + @moduledoc """ + Inference options for Nova text models. + """ + + alias ExAws.Bedrock.Nova.ReasoningConfig + + defstruct [ + :maxTokens, + :temperature, + :topP, + :topK, + :stopSequences, + :reasoningConfig + ] + + @type t :: %__MODULE__{ + maxTokens: pos_integer() | nil, + temperature: float() | nil, + topP: float() | nil, + topK: non_neg_integer() | nil, + stopSequences: list(String.t()) | nil, + reasoningConfig: ReasoningConfig.t() | nil + } + + def build(opts \\ []) when is_list(opts) do + reasoning = + case Keyword.get(opts, :reasoning_config) do + %ReasoningConfig{} = config -> config + opts when is_list(opts) -> ReasoningConfig.build(opts) + _ -> nil + end + + %__MODULE__{ + maxTokens: Keyword.get(opts, :max_tokens), + temperature: Keyword.get(opts, :temperature), + topP: Keyword.get(opts, :top_p), + topK: Keyword.get(opts, :top_k), + stopSequences: Keyword.get(opts, :stop_sequences), + reasoningConfig: reasoning + } + end +end + +defimpl Jason.Encoder, for: ExAws.Bedrock.Nova.InferenceConfig do + def encode(struct, opts) do + struct + |> Map.from_struct() + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + |> Map.new() + |> Jason.Encode.map(opts) + end +end diff --git a/lib/ex_aws/bedrock/nova/message.ex b/lib/ex_aws/bedrock/nova/message.ex new file mode 100644 index 0000000..01d9a44 --- /dev/null +++ b/lib/ex_aws/bedrock/nova/message.ex @@ -0,0 +1,40 @@ +defmodule ExAws.Bedrock.Nova.Message do + @moduledoc """ + Encapsulates user and assistant turns in the conversation. + """ + + alias ExAws.Bedrock.Nova.Content + + defstruct [:role, :content] + + @type role :: :user | :assistant + @type t :: %__MODULE__{ + role: String.t(), + content: list(map()) + } + + @doc "Constructs a user turn with text or multimodal content items." + def user(content) when is_binary(content), do: build("user", [Content.text(content)]) + def user(content) when is_list(content), do: build("user", content) + + @doc "Constructs an assistant turn (useful for prompt pre-filling)." + def assistant(content) when is_binary(content), do: build("assistant", [Content.text(content)]) + def assistant(content) when is_list(content), do: build("assistant", content) + + def build(role, content) when role in ["user", "assistant", :user, :assistant] do + %__MODULE__{ + role: to_string(role), + content: content + } + end +end + +defimpl Jason.Encoder, for: ExAws.Bedrock.Nova.Message do + def encode(struct, opts) do + struct + |> Map.from_struct() + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + |> Map.new() + |> Jason.Encode.map(opts) + end +end diff --git a/lib/ex_aws/bedrock/nova/reasoning_config.ex b/lib/ex_aws/bedrock/nova/reasoning_config.ex new file mode 100644 index 0000000..bf6030e --- /dev/null +++ b/lib/ex_aws/bedrock/nova/reasoning_config.ex @@ -0,0 +1,31 @@ +defmodule ExAws.Bedrock.Nova.ReasoningConfig do + @moduledoc """ + Reasoning configuration parameters for Nova reasoning models. + """ + + defstruct [:type, :maxReasoningEffort] + + @type type :: String.t() + @type max_effort :: String.t() + @type t :: %__MODULE__{ + type: type() | nil, + maxReasoningEffort: max_effort() | nil + } + + def build(opts) when is_list(opts) do + %__MODULE__{ + type: Keyword.get(opts, :type, "disabled"), + maxReasoningEffort: Keyword.get(opts, :max_reasoning_effort) + } + end +end + +defimpl Jason.Encoder, for: ExAws.Bedrock.Nova.ReasoningConfig do + def encode(struct, opts) do + struct + |> Map.from_struct() + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + |> Map.new() + |> Jason.Encode.map(opts) + end +end diff --git a/lib/ex_aws/bedrock/nova/text_model.ex b/lib/ex_aws/bedrock/nova/text_model.ex new file mode 100644 index 0000000..53b492f --- /dev/null +++ b/lib/ex_aws/bedrock/nova/text_model.ex @@ -0,0 +1,64 @@ +defmodule ExAws.Bedrock.Nova.TextModel do + @moduledoc """ + Top-level request payload structure for Amazon Nova models on Bedrock. + """ + + alias ExAws.Bedrock.Nova.{InferenceConfig, Message, ToolConfig} + + defstruct [ + :messages, + :system, + :inferenceConfig, + :toolConfig + ] + + @type t :: %__MODULE__{ + messages: list(Message.t()), + system: list(%{text: String.t()}) | nil, + inferenceConfig: InferenceConfig.t() | nil, + toolConfig: ToolConfig.t() | nil + } + + @doc """ + Build a `TextModel` struct. + + ## Options + * `:system` - String system prompt or list of system prompt maps. + * `:inference_config` - Keyword list or `%InferenceConfig{}`. + * `:tool_config` - Keyword list or `%ToolConfig{}`. + """ + def build(messages, opts \\ []) when is_list(messages) do + system = + case Keyword.get(opts, :system) do + nil -> nil + prompt when is_binary(prompt) -> [%{text: prompt}] + list when is_list(list) -> list + end + + inference_config = + case Keyword.get(opts, :inference_config) do + %InferenceConfig{} = config -> config + opts when is_list(opts) -> InferenceConfig.build(opts) + _ -> nil + end + + tool_config = Keyword.get(opts, :tool_config) + + %__MODULE__{ + messages: messages, + system: system, + inferenceConfig: inference_config, + toolConfig: tool_config + } + end +end + +defimpl Jason.Encoder, for: ExAws.Bedrock.Nova.TextModel do + def encode(struct, opts) do + struct + |> Map.from_struct() + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + |> Map.new() + |> Jason.Encode.map(opts) + end +end diff --git a/lib/ex_aws/bedrock/nova/tool_config.ex b/lib/ex_aws/bedrock/nova/tool_config.ex new file mode 100644 index 0000000..df750b0 --- /dev/null +++ b/lib/ex_aws/bedrock/nova/tool_config.ex @@ -0,0 +1,35 @@ +defmodule ExAws.Bedrock.Nova.ToolConfig do + @moduledoc """ + Tool specifications and tool choice settings. + """ + + defstruct [:tools, :toolChoice] + + @type tool_choice :: %{auto: %{}} | %{any: %{}} | %{tool: %{name: String.t()}} + @type t :: %__MODULE__{ + tools: list(map()), + toolChoice: tool_choice() | nil + } + + def build(tools, tool_choice \\ :auto) do + %__MODULE__{ + tools: tools, + toolChoice: format_tool_choice(tool_choice) + } + end + + defp format_tool_choice(:auto), do: %{auto: %{}} + defp format_tool_choice(:any), do: %{any: %{}} + defp format_tool_choice({:tool, name}) when is_binary(name), do: %{tool: %{name: name}} + defp format_tool_choice(map) when is_map(map), do: map +end + +defimpl Jason.Encoder, for: ExAws.Bedrock.Nova.ToolConfig do + def encode(struct, opts) do + struct + |> Map.from_struct() + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + |> Map.new() + |> Jason.Encode.map(opts) + end +end diff --git a/lib/ex_aws/bedrock/nova/tool_spec.ex b/lib/ex_aws/bedrock/nova/tool_spec.ex new file mode 100644 index 0000000..676f484 --- /dev/null +++ b/lib/ex_aws/bedrock/nova/tool_spec.ex @@ -0,0 +1,33 @@ +defmodule ExAws.Bedrock.Nova.ToolSpec do + @moduledoc """ + Defines an individual tool schema for tool use/function calling. + """ + + defstruct [:name, :description, :inputSchema] + + @type t :: %__MODULE__{ + name: String.t(), + description: String.t() | nil, + inputSchema: %{json: map()} + } + + def build(name, description, json_schema) do + %{ + toolSpec: %__MODULE__{ + name: name, + description: description, + inputSchema: %{json: json_schema} + } + } + end +end + +defimpl Jason.Encoder, for: ExAws.Bedrock.Nova.ToolSpec do + def encode(struct, opts) do + struct + |> Map.from_struct() + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + |> Map.new() + |> Jason.Encode.map(opts) + end +end diff --git a/test/ex_aws/bedrock_image_test.exs b/test/ex_aws/bedrock_image_test.exs index 9e30f60..542037f 100644 --- a/test/ex_aws/bedrock_image_test.exs +++ b/test/ex_aws/bedrock_image_test.exs @@ -57,7 +57,7 @@ defmodule ExAws.Bedrock.InvokeModelImageTest do end defp setup_image_model(_) do - [model_id: "amazon.titan-image-generator-v1"] + [model_id: "amazon.titan-image-generator-v2:0"] end defp setup_image_size(_) do diff --git a/test/ex_aws/bedrock_test.exs b/test/ex_aws/bedrock_test.exs index 184d929..d79c3ce 100644 --- a/test/ex_aws/bedrock_test.exs +++ b/test/ex_aws/bedrock_test.exs @@ -2,10 +2,11 @@ defmodule ExAws.BedrockTest do use ExUnit.Case, async: true import ExAws.Bedrock, only: [request: 1, request!: 1] alias ExAws.Bedrock + alias ExAws.Bedrock.Nova.{Message, TextModel} alias ExAws.Bedrock.Titan.TextModel alias ExAws.Operation.JSON - @model_id "amazon.titan-text-lite-v1" + @model_id "amazon.nova-micro-v1:0" @prompt "Hello, LLM!" describe "get_custom_model/1" do @@ -49,7 +50,7 @@ defmodule ExAws.BedrockTest do end test "path", %{request: request} do - assert %JSON{path: "/foundation-models/amazon.titan-text-lite-v1"} = request + assert %JSON{path: "/foundation-models/amazon.nova-micro-v1:0"} = request end test "service", %{request: request} do @@ -65,16 +66,18 @@ defmodule ExAws.BedrockTest do describe "invoke_model/2 text" do @tag :aws test "against AWS" do - inference_parameters = TextModel.build(@prompt, maxTokenCount: 32) - request = Bedrock.invoke_model(@model_id, inference_parameters) - assert {:ok, %{"results" => [%{"outputText" => _output} | _]}} = request(request) - end + # inference_parameters = TextModel.build(@prompt, maxTokenCount: 32) + inference_parameters = + TextModel.build( + [Message.user("Hello Amazon Nova!")], + system: "You are a helpful Elixir assistant.", + inference_config: [temperature: 0.5, max_tokens: 32] + ) - @tag :aws - test "against AWS!" do - inference_parameters = TextModel.build(@prompt, maxTokenCount: 32) request = Bedrock.invoke_model(@model_id, inference_parameters) - assert %{"results" => [%{"outputText" => _output} | _]} = request!(request) + + assert {:ok, %{"output" => %{"message" => %{"content" => [%{"text" => _}]}}}} = + request(request) end test "content type is JSON", %{request: request} do @@ -87,7 +90,7 @@ defmodule ExAws.BedrockTest do end test "path", %{request: request} do - assert %JSON{path: "/model/amazon.titan-text-lite-v1/invoke"} = request + assert %JSON{path: "/model/amazon.nova-micro-v1:0/invoke"} = request end test "service", %{request: request} do @@ -147,7 +150,7 @@ defmodule ExAws.BedrockTest do test "allow fine tuning" do request = Bedrock.list_foundation_models(by_customization_type: :FINE_TUNING) - assert %{"modelSummaries" => [%{"customizationsSupported" => ["FINE_TUNING"]} | _]} = + assert %{"modelSummaries" => [%{"customizationsSupported" => ["FINE_TUNING" | _]} | _]} = request!(request) end