Current section
9 Versions
Jump to
Current section
9 Versions
Compare versions
11
files changed
+639
additions
-107
deletions
| @@ -1,3 +1,17 @@ | |
| 1 | + ## 0.4.0 - 2024-03-13 |
| 2 | + |
| 3 | + ### Improved |
| 4 | + - Added tools handling. Now you can register tools that the AI can call, and these calls are automaticaly captured. |
| 5 | + |
| 6 | + ### Fixed |
| 7 | + - The way Messages.content was being generated |
| 8 | + |
| 9 | + ### Minor |
| 10 | + - Moved Response parsing to Request module. |
| 11 | + |
| 12 | + ### Breaking change |
| 13 | + - Replaced `Anthropic.add_image/2` with `Anthropic.add_user_image/2` |
| 14 | + |
| 1 15 | ## 0.3.0 - 2024-03-12 |
| 2 16 | |
| 3 17 | ### Improved |
| @@ -1,28 +1,23 @@ | |
| 1 1 | # Anthropic Elixir API Wrapper |
| 2 | - |
| 3 2 | This unofficial Elixir wrapper provides a convenient way to interact with the [Anthropic API](https://docs.anthropic.com/claude/reference/getting-started-with-the-api), specifically designed to work with the [Claude LLM model](https://docs.anthropic.com/claude/docs/intro-to-claude). It includes modules for handling configuration, preparing requests, sending them to the API, and processing responses. |
| 4 3 | |
| 5 4 | ## Features |
| 6 | - |
| 7 5 | - Easy setup and configuration. |
| 6 | + - Support for registering and invoking tools. |
| 8 7 | - Support for sending messages and receiving responses from the Claude LLM model. |
| 9 8 | - Error handling for both client and server-side issues. |
| 10 9 | - Customizable request parameters to tweak the behavior of the API. |
| 11 10 | |
| 12 | - ## To dos: |
| 13 | - |
| 14 | - - Add Streaming handling |
| 15 | - - Add tool description |
| 11 | + ## To-dos |
| 12 | + - Add streaming handling |
| 16 13 | |
| 17 14 | ## Installation |
| 18 | - |
| 19 | - The package can be installed |
| 20 | - by adding `anthropic` to your list of dependencies in `mix.exs`: |
| 15 | + The package can be installed by adding `anthropic` to your list of dependencies in `mix.exs`: |
| 21 16 | |
| 22 17 | ```elixir |
| 23 18 | def deps do |
| 24 19 | [ |
| 25 | - {:anthropic, "~> 0.1.0"} |
| 20 | + {:anthropic, "~> 0.4.0", hex: :anthropic_community} |
| 26 21 | ] |
| 27 22 | end |
| 28 23 | ``` |
| @@ -30,7 +25,7 @@ end | |
| 30 25 | ## Configuration |
| 31 26 | Add or create a config file to provide the `api_key` as in the example below. |
| 32 27 | |
| 33 | - ``` |
| 28 | + ```elixir |
| 34 29 | # config/config.exs |
| 35 30 | import Config |
| 36 31 | |
| @@ -39,17 +34,18 @@ config :anthropic, | |
| 39 34 | ``` |
| 40 35 | |
| 41 36 | ## Usage |
| 42 | - |
| 43 | - ``` |
| 37 | + ### Basic Conversation |
| 38 | + ```elixir |
| 44 39 | {:ok, response, request} = |
| 45 40 | Anthropic.new() |
| 46 41 | |> Anthropic.add_system_message("You are a helpful assistant") |
| 47 | - |> Anthropic.add_user_message("Explain me monads in computer science. Be concise.") |
| 42 | + |> Anthropic.add_user_message("Explain monads in computer science. Be concise.") |
| 48 43 | |> Anthropic.request_next_message() |
| 49 44 | ``` |
| 50 45 | |
| 51 | - Response will hold a map with the API response. |
| 52 | - ``` |
| 46 | + The `response` will hold a map with the API response. |
| 47 | + |
| 48 | + ```elixir |
| 53 49 | %{ |
| 54 50 | id: "msg_013Zva2CMHLNnXjNJJKqJ2EF", |
| 55 51 | content: [ |
| @@ -60,19 +56,55 @@ Response will hold a map with the API response. | |
| 60 56 | stop_reason: "end_turn", |
| 61 57 | stop_sequence: null, |
| 62 58 | type: "message", |
| 63 | - usage": { |
| 64 | - "input_tokens": 10, |
| 65 | - "output_tokens": 25 |
| 59 | + usage: %{ |
| 60 | + "input_tokens" => 10, |
| 61 | + "output_tokens" => 25 |
| 66 62 | } |
| 67 63 | } |
| 68 64 | ``` |
| 69 | - But the conversation can continue: |
| 70 65 | |
| 71 | - ``` |
| 66 | + The conversation can continue: |
| 67 | + |
| 68 | + ```elixir |
| 72 69 | request |
| 73 70 | |> Anthropic.add_user_message("Hold on right there! ELI5!") |
| 74 71 | |> Anthropic.request_next_message() |
| 75 | - |
| 76 72 | ``` |
| 77 73 | |
| 74 | + ### Registering and Invoking Tools |
| 75 | + You can register tools that the AI can use to perform specific tasks. Here's an example: |
| 78 76 | |
| 77 | + ```elixir |
| 78 | + defmodule MyApp.WeatherTool do |
| 79 | + @behaviour Anthropic.Tool.ToolBehaviour |
| 80 | + |
| 81 | + |
| 82 | + @impl true |
| 83 | + def description do |
| 84 | + """ |
| 85 | + Tool description to explain AI how and when to use it |
| 86 | + """ |
| 87 | + end |
| 88 | + |
| 89 | + @impl true |
| 90 | + def parameters do |
| 91 | + [ |
| 92 | + {:location, :string, "The city and state of the location you need the weather"} |
| 93 | + ] |
| 94 | + end |
| 95 | + |
| 96 | + @impl true |
| 97 | + def invoke(location: location) do |
| 98 | + # Implement the tool's functionality here |
| 99 | + # ... |
| 100 | + # and return a string |
| 101 | + end |
| 102 | + end |
| 103 | + |
| 104 | + {:ok, response, request} = |
| 105 | + Anthropic.new() |
| 106 | + |> Anthropic.register_tool(MyTool) |
| 107 | + |> Anthropic.add_user_message("Use the MyApp.WeatherTool to perform a task.") |
| 108 | + |> Anthropic.request_next_message() |
| 109 | + |> Anthropic.process_invocations() |
| 110 | + ``` |
| @@ -1,7 +1,7 @@ | |
| 1 1 | {<<"links">>, |
| 2 2 | [{<<"GitHub">>,<<"https://github.com/tubedude/anthropic-community">>}]}. |
| 3 3 | {<<"name">>,<<"anthropic_community">>}. |
| 4 | - {<<"version">>,<<"0.3.0">>}. |
| 4 | + {<<"version">>,<<"0.4.0">>}. |
| 5 5 | {<<"description">>,<<"Unofficial Anthropic API wrapper.">>}. |
| 6 6 | {<<"elixir">>,<<"~> 1.15">>}. |
| 7 7 | {<<"app">>,<<"anthropic">>}. |
| @@ -29,11 +29,13 @@ | |
| 29 29 | {<<"repository">>,<<"hexpm">>}]]}. |
| 30 30 | {<<"files">>, |
| 31 31 | [<<"lib">>,<<"lib/anthropic">>,<<"lib/anthropic/http_client">>, |
| 32 | - <<"lib/anthropic/http_client/utils.ex">>,<<"lib/anthropic/messages">>, |
| 32 | + <<"lib/anthropic/http_client/utils.ex">>,<<"lib/anthropic/tools">>, |
| 33 | + <<"lib/anthropic/tools/tool_behaviour.ex">>, |
| 34 | + <<"lib/anthropic/tools/utils.ex">>,<<"lib/anthropic/messages">>, |
| 33 35 | <<"lib/anthropic/messages/response.ex">>, |
| 34 36 | <<"lib/anthropic/messages/request.ex">>, |
| 35 37 | <<"lib/anthropic/messages/content">>, |
| 36 38 | <<"lib/anthropic/messages/content/image.ex">>,<<"lib/anthropic/config.ex">>, |
| 37 | - <<"lib/anthropic.ex">>,<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>, |
| 38 | - <<"CHANGELOG.md">>]}. |
| 39 | + <<"lib/anthropic/application.ex">>,<<"lib/anthropic.ex">>, |
| 40 | + <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>]}. |
| 39 41 | {<<"build_tools">>,[<<"mix">>]}. |
| @@ -1,25 +1,36 @@ | |
| 1 1 | defmodule Anthropic do |
| 2 2 | @moduledoc """ |
| 3 | - Provides an unofficial Elixir wrapper for the Anthropic API, facilitating access to the Claude LLM model. |
| 3 | + Provides an unofficial Elixir wrapper for the [Anthropic API](https://docs.anthropic.com/claude/reference/messages_post), facilitating access to the Claude LLM model. |
| 4 4 | This module handles configuration, request preparation, and communication with the API, offering an idiomatic Elixir interface to the Anthropic AI capabilities. |
| 5 5 | |
| 6 6 | ## Key Features |
| 7 | - |
| 7 | + - **Tool Invocations**: Allows registering and executing custom tool modules that implement the `Anthropic.Tools.ToolBehaviour`, enabling dynamic handling of function invocations from the assistant's responses. |
| 8 8 | - **Configuration Management**: Centralizes settings for the Anthropic API, such as model specifications, API keys, and request parameters, ensuring a consistent request configuration. |
| 9 9 | - **Message Handling**: Supports adding various types of messages to the request, including text and image content, enhancing interaction with the Anthropic AI. |
| 10 10 | - **Error Handling**: Implements comprehensive error handling for both request generation and response parsing, providing clear feedback on failures. |
| 11 11 | - **Telemetry Integration**: Integrates with Elixir's `:telemetry` library to emit events for key operations, enabling monitoring and observability. |
| 12 12 | |
| 13 | - ## Usage |
| 13 | + ## Configuration |
| 14 14 | |
| 15 | + Important to configure `api_key`. Please refer to `Antrhopic.Config` for more details. |
| 16 | + |
| 17 | + ## Usage |
| 15 18 | Start by configuring the API settings, then use the provided functions to add messages or images to your request. Finally, send the request to the Anthropic API and handle the response: |
| 16 19 | |
| 17 20 | ```elixir |
| 18 | - config = Anthropic.new(api_key: "your_api_key") |
| 21 | + config = Anthropic.new(max_tokens: 500) # This will take options that are included in the `Anthropic.Messages.Request` body, not the header. Setting `api_key` here won't work. |
| 19 22 | request = Anthropic.add_user_message(config, "Hello, Anthropic!") |
| 20 | - Anthropic.request_next_message(request) |
| 23 | + {:ok, response, updated_request} = Anthropic.request_next_message(request) |
| 21 24 | ``` |
| 22 25 | |
| 26 | + ## Error Handling |
| 27 | + The module returns errors in the format `{:error, reason}`, where `reason` can be: |
| 28 | + |
| 29 | + - A map containing error details from the Anthropic API, with an "error" key indicating the type of error. |
| 30 | + - A `Finch.Error` struct if there was an issue with the HTTP request. |
| 31 | + - A `Jason.DecodeError` struct if there was an issue decoding the JSON response. |
| 32 | + - An `:unknown_error` atom if an unexpected error occurred. |
| 33 | + |
| 23 34 | ## Telemetry |
| 24 35 | |
| 25 36 | This module emits several `:telemetry` events to help monitor its operations, which can be observed for logging, metrics, or operational insights. |
| @@ -27,26 +38,33 @@ defmodule Anthropic do | |
| 27 38 | ### Events |
| 28 39 | |
| 29 40 | - `[:anthropic, :request_next_message, :start]` - Emitted at the beginning of a request to the Anthropic API. |
| 41 | + - Measurements: `%{system_time: integer()}` |
| 42 | + - Metadata: `%{model: String.t(), max_tokens: integer()}` |
| 43 | + |
| 30 44 | - `[:anthropic, :request_next_message, :stop]` - Emitted after a request to the Anthropic API successfully completes. |
| 45 | + - Measurements: `%{duration: integer()}` |
| 46 | + - Metadata: `%{model: String.t(), max_tokens: integer(), input_tokens: integer(), output_tokens: integer()}` |
| 47 | + |
| 31 48 | - `[:anthropic, :request_next_message, :exception]` - Emitted if an exception occurs during a request to the Anthropic API. |
| 49 | + - Measurements: `%{duration: integer()}` |
| 50 | + - Metadata: `%{model: String.t(), max_tokens: integer(), kind: Exception.kind(), reason: term(), stacktrace: Exception.stacktrace()}` |
| 32 51 | |
| 33 52 | ### Metrics |
| 34 53 | |
| 35 | - Each telemetry event includes metadata with the following fields: |
| 54 | + Each telemetry event includes metadata with the following information: |
| 36 55 | |
| 37 | - - `:model` - The model specified in the request. |
| 38 | - - `:max_tokens` - The maximum number of tokens allowed in the response. |
| 56 | + - `:model` - The name of the model used for the request. |
| 57 | + - `:max_tokens` - The maximum number of tokens allowed in the generated response. |
| 58 | + - `:input_tokens` - The number of tokens in the input message (only available in the `:stop` event). |
| 59 | + - `:output_tokens` - The number of tokens in the generated response (only available in the `:stop` event). |
| 60 | + - `:kind` - The type of exception that occurred (only available in the `:exception` event). |
| 61 | + - `:reason` - The reason for the exception (only available in the `:exception` event). |
| 62 | + - `:stacktrace` - The stacktrace of the exception (only available in the `:exception` event). |
| 39 63 | |
| 40 | - In addition, the `:stop` event includes metrics on: |
| 41 | - |
| 42 | - - `:input_tokens` - The number of tokens in the request. |
| 43 | - - `:output_tokens` - The number of tokens in the API response. |
| 44 | - |
| 45 | - Errors are captured with their specific types, aiding in debugging and monitoring of the integration's health. |
| 64 | + By attaching to these events, you can monitor the performance and health of your Anthropic API integration, track usage metrics, and handle exceptions as needed. |
| 46 65 | """ |
| 47 66 | |
| 48 | - use Application |
| 49 | - |
| 67 | + alias Anthropic.Messages.Response |
| 50 68 | alias Anthropic.Messages.Request |
| 51 69 | alias Anthropic.Messages.Content.Image |
| 52 70 | alias Anthropic.Config |
| @@ -54,25 +72,13 @@ defmodule Anthropic do | |
| 54 72 | @type role :: :user | :assistant |
| 55 73 | @type message :: %{role: role, content: any()} |
| 56 74 | |
| 57 | - @doc false |
| 58 | - def start(_type, _args) do |
| 59 | - children = [ |
| 60 | - Config, |
| 61 | - {Finch, name: Anthropic.HTTPClient} |
| 62 | - ] |
| 63 | - |
| 64 | - opts = [strategy: :one_for_one, name: Anthropic.Supervisor] |
| 65 | - |
| 66 | - Supervisor.start_link(children, opts) |
| 67 | - end |
| 68 | - |
| 69 75 | @spec new(Anthropic.Config.config_options() | nil) :: Anthropic.Messages.Request.t() |
| 70 76 | @doc """ |
| 71 | - Initializes a new `Anthropic.Config` struct with the given options, merging them with the default configuration. |
| 77 | + Initializes a new `Anthropic.Messages.Request` struct with the given options, merging them with the default configuration `Anthropic.Config`. |
| 72 78 | |
| 73 79 | ## Parameters |
| 74 80 | |
| 75 | - - `opts`: (Optional) A keyword list of options to override the default configuration settings. |
| 81 | + - `opts`: (Optional) A keyword list of options to override the default configuration settings. Refer to `Anthropic.Messages.Request` for options that can be overridden. |
| 76 82 | |
| 77 83 | ## Returns |
| 78 84 | |
| @@ -92,7 +98,7 @@ defmodule Anthropic do | |
| 92 98 | |
| 93 99 | ## Parameters |
| 94 100 | |
| 95 | - - `request`: The current `Anthropic.Messages.Request` struct to which the system message will be added. |
| 101 | + - `request`: A `Anthropic.Messages.Request` struct to which the system message will be added. |
| 96 102 | - `message`: The system message to add, must be a binary string. |
| 97 103 | |
| 98 104 | ## Returns |
| @@ -114,7 +120,11 @@ defmodule Anthropic do | |
| 114 120 | "System message must be type String, got #{inspect(message, limit: 50, structs: false, width: 80)}" |
| 115 121 | ) |
| 116 122 | |
| 117 | - @spec add_message(Anthropic.Messages.Request.t(), role(), any()) :: any() |
| 123 | + @spec add_message( |
| 124 | + Anthropic.Messages.Request.t(), |
| 125 | + role(), |
| 126 | + String.t() | [String.t()] | Request.content_object() | [Request.content_object()] |
| 127 | + ) :: Anthropic.Messages.Request.t() |
| 118 128 | @doc """ |
| 119 129 | Adds a message to the request with a specified role. |
| 120 130 | |
| @@ -122,47 +132,87 @@ defmodule Anthropic do | |
| 122 132 | |
| 123 133 | - `request`: The `Anthropic.Messages.Request` struct to which the message will be added. |
| 124 134 | - `role`: The role of the message (e.g., `:user` or `:assistant`). |
| 125 | - - `message`: The content of the message, can be a binary string, or a list of binary strings that will be treated as different messages. |
| 135 | + - `message`: The content of the message, which can be one of the following: |
| 136 | + - A binary string representing a single text message, that will be converted to a content of type `text`. |
| 137 | + - A list of binary strings representing multiple text messages. |
| 138 | + - A content object representing a single message with a specific type (e.g., text or image). |
| 139 | + - A list of content objects representing multiple messages with specific types. |
| 126 140 | |
| 127 141 | ## Returns |
| 128 142 | |
| 129 | - - The updated `Anthropic.Messages.Request` struct with the new message added. |
| 130 | - """ |
| 131 | - def add_message(%Request{} = request, role, messages) when is_list(messages) do |
| 132 | - messages |
| 133 | - |> Enum.reduce(request, fn elem, acc -> add_message(acc, role, elem) end) |
| 134 | - end |
| 143 | + - The updated `Anthropic.Messages.Request` struct with the new message(s) added. |
| 135 144 | |
| 136 | - def add_message(%Request{messages: messages} = request, role, message) do |
| 137 | - messages = |
| 145 | + ## Examples |
| 146 | + |
| 147 | + ```elixir |
| 148 | + # Adding a single text message |
| 149 | + request = Anthropic.add_message(request, :user, "Hello!") |
| 150 | + |
| 151 | + # Adding multiple text messages |
| 152 | + request = Anthropic.add_message(request, :user, ["Hello!", "How are you?"]) |
| 153 | + |
| 154 | + # Adding a single content object |
| 155 | + content_object = %{type: "text", text: "Hello!"} |
| 156 | + request = Anthropic.add_message(request, :user, content_object) |
| 157 | + |
| 158 | + # Adding multiple content objects |
| 159 | + content_objects = [ |
| 160 | + %{type: "text", text: "Hello!"}, |
| 161 | + %{type: "image", source: %{data: "base64_encoded_data", type: "base64", media_type: "image/png"}} |
| 162 | + ] |
| 163 | + request = Anthropic.add_message(request, :user, content_objects) |
| 164 | + """ |
| 165 | + def add_message(%Request{messages: messages} = request, role, content) when is_list(content) do |
| 166 | + content_objects = |
| 167 | + content |
| 168 | + |> Enum.map(fn |
| 169 | + message when is_binary(message) -> %{type: "text", text: message} |
| 170 | + content_object -> content_object |
| 171 | + end) |
| 172 | + |
| 173 | + updated_messages = |
| 138 174 | messages |
| 139 175 | |> Enum.reverse() |
| 140 | - |> then(fn list -> [%{role: role, content: message} | list] end) |
| 176 | + |> then(fn list -> [%{role: role, content: content_objects} | list] end) |
| 141 177 | |> Enum.reverse() |
| 142 178 | |
| 143 | - %{request | messages: messages} |
| 179 | + %{request | messages: updated_messages} |
| 144 180 | end |
| 145 181 | |
| 146 | - @spec add_user_message(Anthropic.Messages.Request.t(), binary()) :: |
| 147 | - Anthropic.Messages.Request.t() |
| 182 | + def add_message(%Request{} = request, role, content_object) do |
| 183 | + add_message(request, role, [content_object]) |
| 184 | + end |
| 185 | + |
| 186 | + @spec add_user_message( |
| 187 | + Anthropic.Messages.Request.t(), |
| 188 | + Anthropic.Messages.Request.message() |
| 189 | + | list(Anthropic.Messages.Request.message()) |
| 190 | + | binary() |
| 191 | + | list(binary()) |
| 192 | + ) :: Anthropic.Messages.Request.t() |
| 148 193 | @doc """ |
| 149 | - Adds a user message to the request. |
| 194 | + A shorthand to add a user message to a request. |
| 150 195 | |
| 151 196 | ## Parameters |
| 152 197 | |
| 153 198 | - `request`: The `Anthropic.Messages.Request` struct to which the user message will be added. |
| 154 | - - `message`: The content of the user message, must be a binary string. |
| 199 | + - `message`: The content of the user message. |
| 155 200 | |
| 156 201 | ## Returns |
| 157 202 | |
| 158 203 | - The updated `Anthropic.Messages.Request` struct with the user message added. |
| 159 204 | """ |
| 160 | - def add_user_message(%Request{} = request, message) when is_binary(message) do |
| 205 | + def add_user_message(%Request{} = request, message) do |
| 161 206 | add_message(%Request{} = request, :user, message) |
| 162 207 | end |
| 163 208 | |
| 164 | - @spec add_assistant_message(Anthropic.Messages.Request.t(), binary()) :: |
| 165 | - Anthropic.Messages.Request.t() |
| 209 | + @spec add_assistant_message( |
| 210 | + Anthropic.Messages.Request.t(), |
| 211 | + Anthropic.Messages.Request.message() |
| 212 | + | list(Anthropic.Messages.Request.message()) |
| 213 | + | binary() |
| 214 | + | list(binary()) |
| 215 | + ) :: Anthropic.Messages.Request.t() |
| 166 216 | @doc """ |
| 167 217 | Adds a assistant message to the request. |
| 168 218 | |
| @@ -179,7 +229,7 @@ defmodule Anthropic do | |
| 179 229 | add_message(%Request{} = request, :assistant, message) |
| 180 230 | end |
| 181 231 | |
| 182 | - @spec add_image( |
| 232 | + @spec add_user_image( |
| 183 233 | Anthropic.Messages.Request.t(), |
| 184 234 | {Anthropic.Messages.Content.Image.input_type(), binary()} |
| 185 235 | ) :: Anthropic.Messages.Request.t() |
| @@ -199,13 +249,13 @@ defmodule Anthropic do | |
| 199 249 | |
| 200 250 | ## Examples |
| 201 251 | |
| 202 | - Anthropic.add_image(request, {:path, "/path/to/image.png"}) |
| 252 | + Anthropic.add_user_image(request, {:path, "/path/to/image.png"}) |
| 203 253 | # Adds an image from a local file path |
| 204 254 | |
| 205 | - Anthropic.add_image(request, {:binary, <<binary data>>}) |
| 255 | + Anthropic.add_user_image(request, {:binary, <<binary data>>}) |
| 206 256 | # Adds an image from binary data |
| 207 257 | |
| 208 | - Anthropic.add_image(request, {:base64, "base64 encoded image data"}) |
| 258 | + Anthropic.add_user_image(request, {:base64, "base64 encoded image data"}) |
| 209 259 | # Adds an image from a base64 encoded string |
| 210 260 | |
| 211 261 | ## Errors |
| @@ -213,12 +263,77 @@ defmodule Anthropic do | |
| 213 263 | - Returns `{:error, reason}` if the image processing fails, where `reason` is a descriptive error message. |
| 214 264 | """ |
| 215 265 | @doc since: "0.2.0" |
| 216 | - def add_image(%Request{} = request, {type, image_path}) do |
| 266 | + def add_user_image(%Request{} = request, {type, image_path}) do |
| 217 267 | {:ok, content} = Image.process_image(image_path, type) |
| 218 268 | add_message(request, :user, content) |
| 219 269 | end |
| 220 270 | |
| 221 | - @spec request_next_message(Anthropic.Messages.Request.t()) :: any() |
| 271 | + @spec register_tool(Anthropic.Messages.Request.t(), atom()) :: Anthropic.Messages.Request.t() |
| 272 | + @doc """ |
| 273 | + Registers a tool module with the given request. |
| 274 | + |
| 275 | + This function allows developers to register tools that implement the `Anthropic.Tools.ToolBehaviour`. |
| 276 | + Registered tools will be added to the system message automaticaly at the time of the request, and will be made |
| 277 | + available to the assistant. Once the assistant invokes a function call, it will be processed and the result will be returned |
| 278 | + back to the assistant. |
| 279 | + |
| 280 | + The `Anthropic.Tools.ToolBehaviour` requires the following callbacks to be implemented: |
| 281 | + |
| 282 | + - `description/0`: Returns a string describing the purpose and functionality of the tool. |
| 283 | + - `parameters/0`: Returns a list of parameter specifications, each represented as a tuple `{name, type, description}`. |
| 284 | + - `name`: An atom representing the name of the parameter. |
| 285 | + - `type`: The type of the parameter, which can be `:string`, `:float`, or `:integer`. |
| 286 | + - `description`: A string describing the parameter. |
| 287 | + - `invoke/1`: Accepts a Keyword list of arguments and returns the result of the tool invocation as a string. |
| 288 | + |
| 289 | + By implementing these callbacks, developers can create custom tools that can be dynamically invoked by the assistant during the conversation. |
| 290 | + |
| 291 | + ## Parameters |
| 292 | + |
| 293 | + - `request`: The `Anthropic.Messages.Request` struct that represents the current state of the conversation. |
| 294 | + - `tool_module`: The module that implements the `Anthropic.Tools.ToolBehaviour`, to be registered with the request. |
| 295 | + |
| 296 | + ## Returns |
| 297 | + |
| 298 | + - An updated `Anthropic.Messages.Request` struct with the tool module registered. |
| 299 | + |
| 300 | + ## Examples |
| 301 | + |
| 302 | + ```elixir |
| 303 | + defmodule MyCustomTool do |
| 304 | + @behaviour Anthropic.Tools.ToolBehaviour |
| 305 | + |
| 306 | + def description, do: "A custom tool for demonstration purposes" |
| 307 | + |
| 308 | + def parameters, do: [ |
| 309 | + {:name, :string, "The name of the person"}, |
| 310 | + {:age, :integer, "The age of the person"} |
| 311 | + ] |
| 312 | + |
| 313 | + def invoke([name, age]) do |
| 314 | + "Hello, \#{name}! You are \#{age} years old." |
| 315 | + end |
| 316 | + end |
| 317 | + |
| 318 | + request = Anthropic.register_tool(request, MyCustomTool) |
| 319 | + ``` |
| 320 | + """ |
| 321 | + @doc since: "0.4.0" |
| 322 | + def register_tool(%Request{} = request, tool_module) when is_atom(tool_module) do |
| 323 | + case {Code.ensure_loaded?(tool_module), Enum.member?(request.tools, tool_module)} do |
| 324 | + {true, true} -> |
| 325 | + request |
| 326 | + |
| 327 | + {true, false} -> |
| 328 | + %{request | tools: [tool_module | request.tools]} |
| 329 | + |
| 330 | + {false, _} -> |
| 331 | + raise ArgumentError, |
| 332 | + "Module #{tool_module} is not loaded. Please use module full name (MyApp.AnthropicTool)" |
| 333 | + end |
| 334 | + end |
| 335 | + |
| 336 | + @spec request_next_message(Anthropic.Messages.Request.t(), any()) :: any() |
| 222 337 | @doc """ |
| 223 338 | Sends the current request to the Anthropic API and awaits the next message in the conversation. |
| 224 339 | |
| @@ -246,14 +361,15 @@ defmodule Anthropic do | |
| 246 361 | |
| 247 362 | defp request_next_message_core(%Request{} = request, http_client_opts) do |
| 248 363 | Anthropic.Messages.Request.send_request(request, http_client_opts) |
| 249 | - |> Anthropic.Messages.Response.parse() |
| 250 364 | |> prepare_response(request) |
| 251 365 | |> wrap_to_telemetry() |
| 252 366 | end |
| 253 367 | |
| 254 368 | # Prepares the response from the API for successful requests, updating the request with the assistant's message. |
| 255 369 | defp prepare_response({:ok, response}, request) do |
| 256 | - updated_request = add_assistant_message(request, response.content) |
| 370 | + updated_request = |
| 371 | + request |
| 372 | + |> add_assistant_message(response.content) |
| 257 373 | |
| 258 374 | {:ok, response, updated_request} |
| 259 375 | end |
| @@ -289,4 +405,78 @@ defmodule Anthropic do | |
| 289 405 | defp wrap_to_telemetry({:error, _response, _updated_request} = result) do |
| 290 406 | {result, %{error: :unknown_error}} |
| 291 407 | end |
| 408 | + |
| 409 | + @spec process_invocations( |
| 410 | + {:ok, Anthropic.Messages.Response.t(), Anthropic.Messages.Request.t()} |
| 411 | + | {:error, any(), Anthropic.Messages.Request.t()} |
| 412 | + ) :: |
| 413 | + {:ok, Anthropic.Messages.Response.t(), Anthropic.Messages.Request.t()} |
| 414 | + | {:error, any(), Anthropic.Messages.Request.t()} |
| 415 | + @doc """ |
| 416 | + Processes the tool invocations present in the assistant's response. |
| 417 | + |
| 418 | + This function takes the result of `request_next_message/1` and checks if there are any tool invocations in the response. |
| 419 | + If invocations are found, it executes each one using the registered tools and appends the results to the conversation. |
| 420 | + It then sends the updated conversation back to the API for further processing. |
| 421 | + |
| 422 | + ## Parameters |
| 423 | + - `result`: The result of `request_next_message/1`, which can be either `{:ok, response, request}` or `{:error, response, request}`. |
| 424 | + - `request`: A `Anthropic.Messages.Request`. |
| 425 | + |
| 426 | + ## Returns |
| 427 | + - `{:ok, response, updated_request}`: If the invocations are processed successfully, where `response` is the original response |
| 428 | + from the API, and `updated_request` is the request struct updated with the invocation results. |
| 429 | + - `{:error, response, request}`: If an error occurs during the processing of invocations, where `response` contains the error |
| 430 | + details, and `request` is the original request struct. |
| 431 | + |
| 432 | + ## Raises |
| 433 | + - `ArgumentError`: If a tool invocation fails due to the specified tool not being registered or if there's an error during the |
| 434 | + execution of the tool. |
| 435 | + |
| 436 | + This function is responsible for handling the dynamic execution of tools based on the assistant's responses. It allows for a |
| 437 | + more interactive and extensible conversation flow by processing tool invocations and incorporating their results into the ongoing conversation. |
| 438 | + """ |
| 439 | + @doc since: "0.4.0" |
| 440 | + def process_invocations({:ok, %Response{} = response, %Request{} = request}) do |
| 441 | + case cycle_invocations(response, request) do |
| 442 | + {:ok, [], updated_request} -> |
| 443 | + {:ok, response, updated_request} |
| 444 | + |
| 445 | + {:ok, invocation_responses, updated_request} -> |
| 446 | + invocation_responses |
| 447 | + |> Enum.join("\n") |
| 448 | + |> then(&add_user_message(updated_request, &1)) |
| 449 | + |> request_next_message() |
| 450 | + end |
| 451 | + end |
| 452 | + |
| 453 | + def process_invocations({:error, _, _} = resp), do: resp |
| 454 | + |
| 455 | + defp cycle_invocations(%Response{invocations: []}, %Request{} = request) do |
| 456 | + {:ok, [], request} |
| 457 | + end |
| 458 | + |
| 459 | + defp cycle_invocations(%Response{invocations: invocations}, %Request{} = request) do |
| 460 | + {responses, updated_request} = |
| 461 | + Enum.map_reduce(invocations, request, fn {tool_name, args}, req -> |
| 462 | + case process_invocation(tool_name, args, req) do |
| 463 | + {:ok, result, updated_req} -> {result, updated_req} |
| 464 | + {:error, reason} -> raise ArgumentError, "Invocation error: #{reason}" |
| 465 | + end |
| 466 | + end) |
| 467 | + |
| 468 | + {:ok, responses, updated_request} |
| 469 | + end |
| 470 | + |
| 471 | + defp process_invocation(tool_name, args, %Request{} = request) do |
| 472 | + case Enum.find(request.tools, &(&1 == tool_name)) do |
| 473 | + nil -> |
| 474 | + {:error, "Tool #{tool_name} not found"} |
| 475 | + |
| 476 | + tool_module -> |
| 477 | + task = Anthropic.Tools.Utils.execute_async(tool_module, [args]) |
| 478 | + result = Anthropic.Tools.Utils.format_response(task, tool_name) |
| 479 | + {:ok, result, request} |
| 480 | + end |
| 481 | + end |
| 292 482 | end |
| @@ -0,0 +1,17 @@ | |
| 1 | + defmodule Anthropic.Application do |
| 2 | + @moduledoc false |
| 3 | + |
| 4 | + use Application |
| 5 | + |
| 6 | + @doc false |
| 7 | + def start(_type, _args) do |
| 8 | + children = [ |
| 9 | + Anthropic.Config, |
| 10 | + {Finch, name: Anthropic.HTTPClient} |
| 11 | + ] |
| 12 | + |
| 13 | + opts = [strategy: :one_for_one, name: Anthropic.Supervisor] |
| 14 | + |
| 15 | + Supervisor.start_link(children, opts) |
| 16 | + end |
| 17 | + end |
Loading more files…