From cbd9b4242394a996b68b5671053e3e2501558c04 Mon Sep 17 00:00:00 2001 From: lebrunel <124721263+lebrunel@users.noreply.github.com> Date: Wed, 13 Mar 2024 16:46:23 +0000 Subject: [PATCH] updated readme. fixes to docs --- README.md | 113 ++++++++++++++++++++++++++++-- lib/anthropix.ex | 123 ++++++++++++++++++++++++++++++--- lib/anthropix/agent.ex | 4 +- lib/anthropix/function_call.ex | 2 +- lib/anthropix/tool.ex | 2 +- lib/anthropix/xml.ex | 8 ++- mix.exs | 5 +- mix.lock | 3 - 8 files changed, 232 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ec7bca3..36b694a 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,13 @@ ![License](https://img.shields.io/github/license/lebrunel/anthropix?color=informational) ![Build Status](https://img.shields.io/github/actions/workflow/status/lebrunel/anthropix/elixir.yml?branch=main) -An up-to-date and fully-featured Elixir client library for [Anthropic's REST API](https://docs.anthropic.com/claude/reference/getting-started-with-the-api). +Anthropix is an open-source Elixir client for the Anthropic API, providing a simple and convenient way to integrate Claude, Anthropic's powerful language model, into your applications. -- ✅ API client fully implementing the Anthropic API +- ✅ API client fully implementing the [Anthropic API](https://docs.anthropic.com/claude/reference/getting-started-with-the-api) - 🛜 Streaming API requests - Stream to an Enumerable - Or stream messages to any Elixir process -- 🔜 Advanced and flexible function calling workflow - -This library is currently a WIP. Check back in a week or two, by which point it should be bangin! +- 🕶️ Powerful yet painless function calling with **Agents** ## Installation @@ -21,14 +19,115 @@ The package can be installed by adding `anthropix` to your list of dependencies ```elixir def deps do [ - {:anthropix, "0.0.1"} + {:anthropix, "#{@version}"} ] end ``` ## Quickstart -TODO... +For more examples, refer to the [Anthropix documentation](https://hexdocs.pm/anthropix). + +### Initiate a client. + +See `Anthropix.init/2`. + +```elixir +client = Anthropix.init(api_key) +``` + +### Chat with Claude + +See `Anthropix.chat/2`. + +```elixir +messages = [ + %{role: "system", content: "You are a helpful assistant."}, + %{role: "user", content: "Why is the sky blue?"}, + %{role: "assistant", content: "Due to rayleigh scattering."}, + %{role: "user", content: "How is that different than mie scattering?"}, +] + +Anthropix.chat(client, [ + model: "claude-3-opus-20240229", + messages: messages, +]) +# {:ok, %{"content" => [%{ +# "type" => "text", +# "text" => "Mie scattering affects all wavelengths similarly, while Rayleigh favors shorter ones." +# }], ...}} +``` + +### Streaming + +A streaming request can be initiated by setting the `:stream` option. + +When `:stream` is true a lazy `t:Enumerable.t/0` is returned which can be used with any `Stream` functions. + +```elixir +{:ok, stream} = Anthropix.chat(client, [ + model: "claude-3-opus-20240229", + messages: messages, + stream: true, +]) +# {:ok, #Function<52.53678557/2 in Stream.resource/3>} + +stream +|> Stream.each(&update_ui_with_chunk/1) +|> Stream.run() +# :ok +``` + +Because the above approach builds the `t:Enumerable.t/0` by calling `receive`, using this approach inside GenServer callbacks may cause the GenServer to misbehave. Setting the `:stream` option to a `t:pid/0` returns a `t:Task.t/0` which will send messages to the specified process. + +## Function calling + +Chatting with Claude is nice and all, but when it comes to function calling, Anthropix has a trick up its sleeve. Meet `Anthropix.Agent`. + +The Agent module abstracts away all the rough bits of implementing [Anthropic style function calling](https://docs.anthropic.com/claude/docs/functions-external-tools), leaving a delightfully simple API that opens the doors to powerful and advanced agent workflows. + +```elixir +ticker_tool = %Anthropix.Tool.new([ + name: "get_ticker_symbol", + description: "Gets the stock ticker symbol for a company searched by name. Returns str: The ticker symbol for the company stock. Raises TickerNotFound: if no matching ticker symbol is found.", + params: [ + %{name: "company_name", description: "The name of the company.", type: "string"} + ], + function: &MyStocks.get_ticker/1 +]) + +price_tool = %Anthropix.Tool.new([ + name: "get_current_stock_price", + description: "Gets the current stock price for a company. Returns float: The current stock price. Raises ValueError: if the input symbol is invalid/unknown.", + params: [ + %{name: "symbol", description: "The stock symbol of the company to get the price for.", type: "string"} + ], + function: &MyStocks.get_price/1 +]) + +agent = Anthropix.Agent.init( + Anthropix.init(api_key), + [ticker_tool, price_tool] +) + +Anthropix.Agent.chat(agent, [ + model: "claude-3-sonnet-20240229", + system: "Answer like Snoop Dogg.", + messages: [ + %{role: "user", content: "What is the current stock price of General Motors?"} + ] +]) +%{ + result: %{ + "content" => [%{ + "type" => "text", + "text" => "*snaps fingers* Damn shawty, General Motors' stock is sittin' pretty at $39.21 per share right now. Dat's a fly price for them big ballers investin' in one of Detroit's finest auto makers, ya heard? *puts hands up like car doors* If ya askin' Snoop, dat stock could be rollin' on some dubs fo' sho'. Just don't get caught slippin' when them prices dippin', ya dig?" + }] + } +} +``` + +For a more detailed walkthrough, refer to the `Anthropix.Agent` documentation. # License diff --git a/lib/anthropix.ex b/lib/anthropix.ex index ffbcac6..12b10bb 100644 --- a/lib/anthropix.ex +++ b/lib/anthropix.ex @@ -3,17 +3,15 @@ defmodule Anthropix do @moduledoc """ ![License](https://img.shields.io/github/license/lebrunel/anthropix?color=informational) - An up-to-date and fully-featured Elixir client library for - [Anthropic's REST API](https://docs.anthropic.com/claude/reference/getting-started-with-the-api). + Anthropix is an open-source Elixir client for the Anthropic API, providing a + simple and convenient way to integrate Claude, Anthropic's powerful language + model, into your applications. - - ✅ API client fully implementing the Anthropic API + - ✅ API client fully implementing the [Anthropic API](https://docs.anthropic.com/claude/reference/getting-started-with-the-api) - 🛜 Streaming API requests - Stream to an Enumerable - Or stream messages to any Elixir process - - 🧩 Advanced and flexible function calling workflow - - This library is currently a WIP. Check back in a week or two, by which point - it should be bangin! + - 🕶️ Powerful yet painless function calling with **Agents** ## Installation @@ -30,7 +28,116 @@ defmodule Anthropix do ## Quickstart - TODO... + For more examples, refer to the [Anthropix documentation](https://hexdocs.pm/anthropix). + + ### Initiate a client. + + See `Anthropix.init/2`. + + ```elixir + iex> client = Anthropix.init(api_key) + ``` + + ### Chat with Claude + + See `Anthropix.chat/2`. + + ```elixir + iex> messages = [ + ...> %{role: "system", content: "You are a helpful assistant."}, + ...> %{role: "user", content: "Why is the sky blue?"}, + ...> %{role: "assistant", content: "Due to rayleigh scattering."}, + ...> %{role: "user", content: "How is that different than mie scattering?"}, + ...> ] + + iex> Anthropix.chat(client, [ + ...> model: "claude-3-opus-20240229", + ...> messages: messages, + ...> ]) + {:ok, %{"content" => [%{ + "type" => "text", + "text" => "Mie scattering affects all wavelengths similarly, while Rayleigh favors shorter ones." + }], ...}} + ``` + + ### Streaming + + A streaming request can be initiated by setting the `:stream` option. + + When `:stream` is true a lazy `t:Enumerable.t/0` is returned which can be used + with any `Stream` functions. + + ```elixir + iex> {:ok, stream} = Anthropix.chat(client, [ + ...> model: "claude-3-opus-20240229", + ...> messages: messages, + ...> stream: true, + ...> ]) + {:ok, #Function<52.53678557/2 in Stream.resource/3>} + + iex> stream + ...> |> Stream.each(&update_ui_with_chunk/1) + ...> |> Stream.run() + :ok + ``` + + Because the above approach builds the `t:Enumerable.t/0` by calling `receive`, + using this approach inside GenServer callbacks may cause the GenServer to + misbehave. Setting the `:stream` option to a `t:pid/0` returns a `t:Task.t/0` + which will send messages to the specified process. + + ## Function calling + + Chatting with Claude is nice and all, but when it comes to function calling, + Anthropix has a trick up its sleeve. Meet `Anthropix.Agent`. + + The Agent module abstracts away all the rough bits of implementing + [Anthropic style function calling](https://docs.anthropic.com/claude/docs/functions-external-tools), + leaving a delightfully simple API that opens the doors to powerful and + advanced agent workflows. + + ```elixir + iex> ticker_tool = %Anthropix.Tool.new([ + ...> name: "get_ticker_symbol", + ...> description: "Gets the stock ticker symbol for a company searched by name. Returns str: The ticker symbol for the company stock. Raises TickerNotFound: if no matching ticker symbol is found.", + ...> params: [ + ...> %{name: "company_name", description: "The name of the company.", type: "string"} + ...> ], + ...> function: &MyStocks.get_ticker/1 + ...> ]) + + iex> price_tool = %Anthropix.Tool.new([ + ...> name: "get_current_stock_price", + ...> description: "Gets the current stock price for a company. Returns float: The current stock price. Raises ValueError: if the input symbol is invalid/unknown.", + ...> params: [ + ...> %{name: "symbol", description: "The stock symbol of the company to get the price for.", type: "string"} + ...> ], + ...> function: &MyStocks.get_price/1 + ...> ]) + + iex> agent = Anthropix.Agent.init( + ...> Anthropix.init(api_key), + ...> [ticker_tool, price_tool] + ...> ) + + iex> Anthropix.Agent.chat(agent, [ + ...> model: "claude-3-sonnet-20240229", + ...> system: "Answer like Snoop Dogg.", + ...> messages: [ + ...> %{role: "user", content: "What is the current stock price of General Motors?"} + ...> ] + ...> ]) + %{ + result: %{ + "content" => [%{ + "type" => "text", + "text" => "*snaps fingers* Damn shawty, General Motors' stock is sittin' pretty at $39.21 per share right now. Dat's a fly price for them big ballers investin' in one of Detroit's finest auto makers, ya heard? *puts hands up like car doors* If ya askin' Snoop, dat stock could be rollin' on some dubs fo' sho'. Just don't get caught slippin' when them prices dippin', ya dig?" + }] + } + } + ``` + + For a more detailed walkthrough, refer to the `Anthropix.Agent` documentation. """ use Anthropix.Schemas alias Anthropix.{APIError, Tool, XML} diff --git a/lib/anthropix/agent.ex b/lib/anthropix/agent.ex index 6912b9d..a9ef43f 100644 --- a/lib/anthropix/agent.ex +++ b/lib/anthropix/agent.ex @@ -1,6 +1,6 @@ defmodule Anthropix.Agent do @moduledoc """ - The `Agent` module makes function calling with Claude a breeze! + The `Anthropix.Agent` module makes function calling with Claude a breeze! Whilst it's possible to manually implement function calling using `Anthropix.chat/2`, this module provides an interface on top that automates @@ -15,7 +15,7 @@ defmodule Anthropix.Agent do automatically, send the result back to Claude, iterating as many times as is necessary before ultimately a final result is returned. - `chat/2` returns a `t:t()` struct, which contains a list of all + `chat/2` returns a `t:t/0` struct, which contains a list of all messages, a sum of all usage statistics, as well as the final response. ## Example diff --git a/lib/anthropix/function_call.ex b/lib/anthropix/function_call.ex index 01e4d9e..7678d00 100644 --- a/lib/anthropix/function_call.ex +++ b/lib/anthropix/function_call.ex @@ -1,6 +1,6 @@ defmodule Anthropix.FunctionCall do @moduledoc """ - The `FunctionCall` module is used to capture function calls from Claud's + The `Anthropix.FunctionCall` module is used to capture function calls from Claud's responses and, match the function call to an existing `t:Anthropix.Tool.t/0`, and then invoke the function. diff --git a/lib/anthropix/tool.ex b/lib/anthropix/tool.ex index 076f6bc..96f5098 100644 --- a/lib/anthropix/tool.ex +++ b/lib/anthropix/tool.ex @@ -1,6 +1,6 @@ defmodule Anthropix.Tool do @moduledoc """ - The `Tool` module allows you to let Claude call functions in your application. + The `Anthropix.Tool` module allows you to let Claude call functions in your application. The `t:Anthropix.Tool.t/0` struct wraps around any function in your application - referenced functions, anonymous functions, or MFA style diff --git a/lib/anthropix/xml.ex b/lib/anthropix/xml.ex index 826e700..db3f0ca 100644 --- a/lib/anthropix/xml.ex +++ b/lib/anthropix/xml.ex @@ -9,9 +9,11 @@ defmodule Anthropix.XML do @allowed_roots [:tools, :function_results] @doc """ - Encodes the given data into the specified message type. Supports encoding - a list of `t:Tool.t()` structs into a `:tools` message, or a list of - `t:FunctionCall.t()` structs into a `:function_results` message. + Encodes the given data into the specified message type. + + Supports encoding a list of `t:Anthropix.Tool.t/0` structs into a `:tools` + message, or a list of `t:Anthropix.FunctionCall.t/0` structs into a + `:function_results` message. ## Examples diff --git a/mix.exs b/mix.exs index 9076cb2..64863af 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule Anthropix.MixProject do [ app: :anthropix, name: "Anthropix", - description: "todo", + description: "Anthropix is an open-source Elixir client with interacting with Claude, Anthropic's powerful language model.", source_url: "https://github.com/lebrunel/anthropix", version: "0.0.1", elixir: "~> 1.13", @@ -36,11 +36,10 @@ defmodule Anthropix.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:bandit, "~> 1.2", only: :test}, {:ex_doc, "~> 0.31", only: :dev, runtime: false}, {:jason, "~> 1.4"}, {:nimble_options, "~> 1.1"}, - {:plug, "~> 1.15"}, + {:plug, "~> 1.15", only: :test}, {:req, "~> 0.4"}, {:saxy, "~> 1.5"}, ] diff --git a/mix.lock b/mix.lock index 33c3d51..90cd13c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,4 @@ %{ - "bandit": {:hex, :bandit, "1.3.0", "6a4e8d7c9ea721edd02c389e2cc867890cd96f83116e71ddf1ccbdd80661550c", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "bda37d6c614d74778a5dc43b8bcdc3245cd30619eab0342f58042f968f2165da"}, "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, @@ -20,6 +19,4 @@ "req": {:hex, :req, "0.4.13", "6fde45b78e606e2a46fc3a7e4a74828c220cd0b16951f4321c1214f955402d72", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "e01a596b74272de799bc57191883e5d4d3875be63f0480223edc5a0086bfe31b"}, "saxy": {:hex, :saxy, "1.5.0", "0141127f2d042856f135fb2d94e0beecda7a2306f47546dbc6411fc5b07e28bf", [:mix], [], "hexpm", "ea7bb6328fbd1f2aceffa3ec6090bfb18c85aadf0f8e5030905e84235861cf89"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, - "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, }