anthropic 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7bf4e9a547533edb57fcaee7dd07c8c1a5c7d3f5291b539541b9b9b8c3b5e614
4
- data.tar.gz: e62081a581aa15d9b8dbd335fb6b989671eb733629b64992c847584e83100c4e
3
+ metadata.gz: ef2ed78af9f188f394fce182554887c8a38c68864f05796f23ccd3e57cf38d40
4
+ data.tar.gz: 65495af068a5f373e6d77dbc0085846c75e1a77dab2139fe0a06bbc62678e3ea
5
5
  SHA512:
6
- metadata.gz: dc148818d355a80050ae35c9b7a60d48dfa713826b9607d9add0ceb70b88da4b19f7b650dae1b5714ea41003fcec94cd4bc2fd226f999c3e4c3562d681a8100a
7
- data.tar.gz: 683dedfd00584546691bde56a569f2a043a0ba8b9806450e5baabcdbc0c0ff28960778c9cda600bea5b13a8024b01337e1f271f363508dd657424ad6da3b3fbd
6
+ metadata.gz: e508affc831d8136633d74ca73026fb5936d52bf1ffbb82541a8ead24c5e4dec640faef5d3747986d242bde4296efc6e5ceac6688c091641da13b74a27371819
7
+ data.tar.gz: 4fd4fd39c63c902c1a54d71fe884329eaca8d7d5a385beb6fdf3c2e3fabeb8cbf4f67a41014d2df43eef15707ef474ee892c7f954f46ca34f33c625577038b6f
data/CHANGELOG.md CHANGED
@@ -5,14 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.0.0] - 2023-07-12
8
+ ## [0.3.0] - 2024-06-10
9
9
 
10
10
  ### Added
11
11
 
12
- - Initialise repository.
12
+ - Add chat streaming! Thank you to the inimitable [@swombat](https://github.com/swombat) for adding this vital functionality!
13
+
14
+ ## [0.2.0] - 2024-04-25
15
+
16
+ ### Added
17
+
18
+ - Add new Messages endpoint - thanks [@svs](https://github.com/svs) for the PR, [@obie](https://github.com/obie) for the first pass, and many others for requesting and contributions!
13
19
 
14
20
  ## [0.1.0] - 2023-07-18
15
21
 
16
22
  ### Changed
17
23
 
18
24
  - Got the gem working with the API. MVP
25
+
26
+ ## [0.0.0] - 2023-07-12
27
+
28
+ ### Added
29
+
30
+ - Initialise repository.
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ gemspec
5
5
 
6
6
  gem "byebug", "~> 11.1.3"
7
7
  gem "dotenv", "~> 2.8.1"
8
+ gem "racc", "~> 1.7.3"
8
9
  gem "rake", "~> 13.0"
9
10
  gem "rspec", "~> 3.12"
10
11
  gem "rubocop", "~> 1.50.2"
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- anthropic (0.1.0)
4
+ anthropic (0.3.0)
5
+ event_stream_parser (>= 0.3.0, < 2.0.0)
5
6
  faraday (>= 1)
6
7
  faraday-multipart (>= 1)
7
8
 
@@ -16,6 +17,7 @@ GEM
16
17
  rexml
17
18
  diff-lcs (1.5.0)
18
19
  dotenv (2.8.1)
20
+ event_stream_parser (1.0.0)
19
21
  faraday (2.7.10)
20
22
  faraday-net_http (>= 2.0, < 3.1)
21
23
  ruby2_keywords (>= 0.0.4)
@@ -29,6 +31,7 @@ GEM
29
31
  parser (3.2.2.0)
30
32
  ast (~> 2.4.1)
31
33
  public_suffix (5.0.1)
34
+ racc (1.7.3)
32
35
  rainbow (3.1.1)
33
36
  rake (13.0.6)
34
37
  regexp_parser (2.8.0)
@@ -74,6 +77,7 @@ DEPENDENCIES
74
77
  anthropic!
75
78
  byebug (~> 11.1.3)
76
79
  dotenv (~> 2.8.1)
80
+ racc (~> 1.7.3)
77
81
  rake (~> 13.0)
78
82
  rspec (~> 3.12)
79
83
  rubocop (~> 1.50.2)
data/README.md CHANGED
@@ -1,18 +1,14 @@
1
- # Anthropic (WIP)
1
+ # Anthropic
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/anthropic.svg)](https://badge.fury.io/rb/anthropic)
4
4
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/alexrudall/anthropic/blob/main/LICENSE.txt)
5
5
  [![CircleCI Build Status](https://circleci.com/gh/alexrudall/anthropic.svg?style=shield)](https://circleci.com/gh/alexrudall/anthropic)
6
6
 
7
- Use the [Anthropic API](https://docs.anthropic.com/claude/reference/getting-started-with-the-api) with Ruby! 🌌❤️
7
+ Use the [Anthropic API](https://docs.anthropic.com/claude/reference/getting-started-with-the-api) with Ruby! 🤖🌌
8
8
 
9
- You can apply for access to the API [here](https://docs.anthropic.com/claude/docs/getting-access-to-claude).
9
+ You can get access to the API [here](https://docs.anthropic.com/claude/docs/getting-access-to-claude).
10
10
 
11
- [Ruby AI Builders Discord](https://discord.gg/k4Uc224xVD)
12
-
13
- [Rails AI Guides](https://railsai.com)
14
-
15
- Follow me on [Twitter](https://twitter.com/alexrudall) for more Ruby / AI content!
11
+ [🎮 Ruby AI Builders Discord](https://discord.gg/k4Uc224xVD) | [🐦 Twitter](https://twitter.com/alexrudall) | [🤖 OpenAI Gem](https://github.com/alexrudall/ruby-openai) | [🚂 Midjourney Gem](https://github.com/alexrudall/midjourney)
16
12
 
17
13
  ### Bundler
18
14
 
@@ -52,11 +48,15 @@ client = Anthropic::Client.new(access_token: "access_token_goes_here")
52
48
 
53
49
  ### With Config
54
50
 
55
- For a more robust setup, you can configure the gem with your API keys, for example in an `anthropic.rb` initializer file. Never hardcode secrets into your codebase - instead use something like [dotenv](https://github.com/motdotla/dotenv) to pass the keys safely into your environments.
51
+ For a more robust setup, you can configure the gem with your API keys, for example in an `anthropic.rb` initializer file. Never hardcode secrets into your codebase - instead use something like [dotenv](https://github.com/motdotla/dotenv) to pass the keys safely into your environments or rails credentials if you are using this in a rails project.
56
52
 
57
53
  ```ruby
58
54
  Anthropic.configure do |config|
59
- config.access_token = ENV.fetch("ANTHROPIC_API_KEY")
55
+ # With dotenv
56
+ config.access_token = ENV.fetch("ANTHROPIC_API_KEY")
57
+ # OR
58
+ # With Rails credentials
59
+ config.access_token = Rails.application.credentials.dig(:anthropic, :api_key)
60
60
  end
61
61
  ```
62
62
 
@@ -74,9 +74,9 @@ The default timeout for any request using this library is 120 seconds. You can c
74
74
 
75
75
  ```ruby
76
76
  client = Anthropic::Client.new(
77
- access_token: "access_token_goes_here",
78
- anthropic_version: "2023-01-01", # Optional
79
- request_timeout: 240 # Optional
77
+ access_token: "access_token_goes_here",
78
+ anthropic_version: "2023-01-01", # Optional
79
+ request_timeout: 240 # Optional
80
80
  )
81
81
  ```
82
82
 
@@ -84,40 +84,164 @@ You can also set these keys when configuring the gem:
84
84
 
85
85
  ```ruby
86
86
  Anthropic.configure do |config|
87
- config.access_token = ENV.fetch("ANTHROPIC_API_KEY")
88
- config.anthropic_version = "2023-01-01" # Optional
89
- config.request_timeout = 240 # Optional
87
+ config.access_token = ENV.fetch("ANTHROPIC_API_KEY")
88
+ config.anthropic_version = "2023-01-01" # Optional
89
+ config.request_timeout = 240 # Optional
90
90
  end
91
91
  ```
92
92
 
93
- ### Completions
93
+ ### Models
94
+
95
+ Available Models:
96
+
97
+ | Name | API Name |
98
+ | --------------- | ------------------------ |
99
+ | Claude 3 Opus | claude-3-opus-20240229 |
100
+ | Claude 3 Sonnet | claude-3-sonnet-20240229 |
101
+ | Claude 3 Haiku | claude-3-haiku-20240307 |
102
+
103
+ You can find the latest model names in the [Anthropic API documentation](https://docs.anthropic.com/claude/docs/models-overview#model-recommendations).
104
+
105
+ ### Messages
94
106
 
95
- Hit the Anthropic API for a completion:
107
+ ```
108
+ POST https://api.anthropic.com/v1/messages
109
+ ```
110
+
111
+ Send a sequence of messages (user or assistant) to the API and receive a message in response.
96
112
 
97
113
  ```ruby
98
- response = client.complete(
99
- parameters: {
100
- model: "claude-2",
101
- prompt: "How high is the sky?",
102
- max_tokens_to_sample: 5
103
- })
104
- puts response["completion"]
105
- # => " The sky has no definitive"
114
+ response = client.messages(
115
+ parameters: {
116
+ model: "claude-3-haiku-20240307", # claude-3-opus-20240229, claude-3-sonnet-20240229
117
+ system: "Respond only in Spanish.",
118
+ messages: [
119
+ {"role": "user", "content": "Hello, Claude!"}
120
+ ],
121
+ max_tokens: 1000
122
+ }
123
+ )
124
+ # => {
125
+ # => "id" => "msg_0123MiRVCgSG2PaQZwCGbgmV",
126
+ # => "type" => "message",
127
+ # => "role" => "assistant",
128
+ # => "content" => [{"type"=>"text", "text"=>"¡Hola! Es un gusto saludarte. ¿En qué puedo ayudarte hoy?"}],
129
+ # => "model" => "claude-3-haiku-20240307",
130
+ # => "stop_reason" => "end_turn",
131
+ # => "stop_sequence" => nil,
132
+ # => "usage" => {"input_tokens"=>17, "output_tokens"=>32}
133
+ # => }
106
134
  ```
107
135
 
108
- Note that all requests are prepended by this library with
136
+ #### Additional parameters
137
+
138
+ You can add other parameters to the parameters hash, like `temperature` and even `top_k` or `top_p`. They will just be passed to the Anthropic server. You
139
+ can read more about the supported parameters [here](https://docs.anthropic.com/claude/reference/messages_post).
109
140
 
110
- `\n\nHuman: `
141
+ There are two special parameters, though, to do with... streaming. Keep reading to find out more.
111
142
 
112
- and appended with
143
+ #### JSON
113
144
 
114
- `\n\nAssistant:`
145
+ If you want your output to be json, it is recommended to provide an additional message like this:
115
146
 
116
- so whatever prompt you pass will be sent to the API as
147
+ ```ruby
148
+ [{ role: "user", content: "Give me the heights of the 3 tallest mountains. Answer in the provided JSON format. Only include JSON." },
149
+ { role: "assistant", content: '[{"name": "Mountain Name", "height": "height in km"}]' }]
150
+ ```
151
+
152
+ Then Claude v3, even Haiku, might respond with:
153
+
154
+ ```ruby
155
+ [{"name"=>"Mount Everest", "height"=>"8.85 km"}, {"name"=>"K2", "height"=>"8.61 km"}, {"name"=>"Kangchenjunga", "height"=>"8.58 km"}]
156
+ ```
157
+
158
+ ### Streaming
159
+
160
+ There are two modes of streaming: raw and preprocessed. The default is raw. You can call it like this:
161
+
162
+ ```ruby
163
+ client.messages(
164
+ parameters: {
165
+ model: "claude-3-haiku-20240307",
166
+ messages: [{ role: "user", content: "How high is the sky?" }],
167
+ max_tokens: 50,
168
+ stream: Proc.new { |chunk| print chunk }
169
+ }
170
+ )
171
+ ```
117
172
 
118
- `\n\nHuman: How high is the sky?\n\nAssistant:`
173
+ This still returns a regular response at the end, but also gives you direct access to every single chunk returned by Anthropic as they come in. Even if you don't want to
174
+ use the streaming, you may find this useful to avoid timeouts, which can happen if you send Opus a large input context, and expect a long response... It has been known to take
175
+ several minutes to compile the full response - which is longer than our 120 second default timeout. But when streaming, the connection does not time out.
119
176
 
120
- This is a requirement of [the API](https://docs.anthropic.com/claude/reference/complete_post).
177
+ Here is an example of a stream you might get back:
178
+
179
+ ```ruby
180
+ {"type"=>"message_start", "message"=>{"id"=>"msg_01WMWvcZq5JEMLf6Jja4Bven", "type"=>"message", "role"=>"assistant", "model"=>"claude-3-haiku-20240307", "stop_sequence"=>nil, "usage"=>{"input_tokens"=>13, "output_tokens"=>1}, "content"=>[], "stop_reason"=>nil}}
181
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>"There"}}
182
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" is"}}
183
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" no"}}
184
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" single"}}
185
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" defin"}}
186
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>"itive"}}
187
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" \""}}
188
+ ...
189
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>"'s"}}
190
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" atmosphere"}}
191
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" extends"}}
192
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" up"}}
193
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" to"}}
194
+ {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" about"}}
195
+ {"type"=>"content_block_stop", "index"=>0}
196
+ {"type"=>"message_delta", "delta"=>{"stop_reason"=>"max_tokens", "stop_sequence"=>nil}, "usage"=>{"output_tokens"=>50}}
197
+ {"type"=>"message_stop"}
198
+ ```
199
+
200
+ Now, you may find this... somewhat less than practical. Surely, the vast majority of developers will not want to deal with so much
201
+ boilerplate json.
202
+
203
+ Luckily, you can ask the anthropic gem to preprocess things for you!
204
+
205
+ First, if you expect simple text output, you can receive it delta by delta:
206
+
207
+ ```ruby
208
+ client.messages(
209
+ parameters: {
210
+ model: "claude-3-haiku-20240307",
211
+ messages: [{ role: "user", content: "How high is the sky?" }],
212
+ max_tokens: 50,
213
+ stream: Proc.new { |incremental_response, delta| process_your(incremental_response, delta) },
214
+ preprocess_stream: :text
215
+ }
216
+ )
217
+ ```
218
+
219
+ The first block argument, `incremental_response`, accrues everything that's been returned so far, so you don't have to. If you just want the last bit,
220
+ then use the second, `delta` argument.
221
+
222
+ But what if you want to stream JSON?
223
+
224
+ Partial JSON is not very useful. But it is common enough to request a collection of JSON objects as a response, as in our earlier example of asking for the heights of the 3 tallest mountains.
225
+
226
+ If you ask it to, this gem will also do its best to sort this out for you:
227
+
228
+ ```ruby
229
+ client.messages(
230
+ parameters: {
231
+ model: "claude-3-haiku-20240307",
232
+ messages: [{ role: "user", content: "How high is the sky?" }],
233
+ max_tokens: 50,
234
+ stream: Proc.new { |json_object| process_your(json_object) },
235
+ preprocess_stream: :json
236
+ }
237
+ )
238
+ ```
239
+
240
+ Each time a `}` is reached in the stream, the preprocessor will take what it has in the preprocessing stack, pick out whatever's between the widest `{` and `}`, and try to parse it into a JSON object.
241
+ If it succeeds, it will pass you the json object, reset its preprocessing stack, and carry on.
242
+
243
+ If the parsing fails despite reaching a `}`, currently, it will catch the Error, log it to `$stdout`, ignore the malformed object, reset the preprocessing stack and carry on. This does mean that it is possible,
244
+ if the AI is sending some malformed JSON (which can happen, albeit rarely), that some objects will be lost.
121
245
 
122
246
  ## Development
123
247
 
@@ -125,6 +249,11 @@ After checking out the repo, run `bin/setup` to install dependencies. You can ru
125
249
 
126
250
  To install this gem onto your local machine, run `bundle exec rake install`.
127
251
 
252
+ To run all tests, execute the command `bundle exec rake`, which will also run the linter (Rubocop). This repository uses [VCR](https://github.com/vcr/vcr) to log API requests.
253
+
254
+ > [!WARNING]
255
+ > If you have an `ANTHROPIC_API_KEY` in your `ENV`, running the specs will use this to run the specs against the actual API, which will be slow and cost you money - 2 cents or more! Remove it from your environment with `unset` or similar if you just want to run the specs against the stored VCR responses.
256
+
128
257
  ### Warning
129
258
 
130
259
  If you have an `ANTHROPIC_API_KEY` in your `ENV`, running the specs will use this to run the specs against the actual API, which will be slow and cost you money - 2 cents or more! Remove it from your environment with `unset` or similar if you just want to run the specs against the stored VCR responses.
data/Rakefile CHANGED
@@ -1,6 +1,19 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
6
- task default: :spec
7
+ task :default do
8
+ Rake::Task["test"].invoke
9
+ Rake::Task["lint"].invoke
10
+ end
11
+
12
+ task :test do
13
+ Rake::Task["spec"].invoke
14
+ end
15
+
16
+ task :lint do
17
+ RuboCop::RakeTask.new(:rubocop)
18
+ Rake::Task["rubocop"].invoke
19
+ end
data/anthropic.gemspec CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Alex"]
7
7
  spec.email = ["alexrudall@users.noreply.github.com"]
8
8
 
9
- spec.summary = "Anthropic API + Ruby! 🌌❤️"
9
+ spec.summary = "Anthropic API + Ruby! 🤖🌌"
10
10
  spec.homepage = "https://github.com/alexrudall/anthropic"
11
11
  spec.license = "MIT"
12
12
  spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
26
  spec.require_paths = ["lib"]
27
27
 
28
+ spec.add_dependency "event_stream_parser", ">= 0.3.0", "< 2.0.0"
28
29
  spec.add_dependency "faraday", ">= 1"
29
30
  spec.add_dependency "faraday-multipart", ">= 1"
30
31
  end
@@ -1,23 +1,75 @@
1
1
  module Anthropic
2
2
  class Client
3
- extend Anthropic::HTTP
3
+ include Anthropic::HTTP
4
4
 
5
- def initialize(access_token: nil, organization_id: nil, uri_base: nil, request_timeout: nil,
6
- extra_headers: {})
7
- Anthropic.configuration.access_token = access_token if access_token
8
- Anthropic.configuration.organization_id = organization_id if organization_id
9
- Anthropic.configuration.uri_base = uri_base if uri_base
10
- Anthropic.configuration.request_timeout = request_timeout if request_timeout
11
- Anthropic.configuration.extra_headers = extra_headers
5
+ CONFIG_KEYS = %i[
6
+ access_token
7
+ anthropic_version
8
+ api_version
9
+ uri_base
10
+ request_timeout
11
+ extra_headers
12
+ ].freeze
13
+
14
+ def initialize(config = {}, &faraday_middleware)
15
+ CONFIG_KEYS.each do |key|
16
+ # Set instance variables like api_type & access_token. Fall back to global config
17
+ # if not present.
18
+ instance_variable_set(
19
+ "@#{key}",
20
+ config[key].nil? ? Anthropic.configuration.send(key) : config[key]
21
+ )
22
+ end
23
+ @faraday_middleware = faraday_middleware
12
24
  end
13
25
 
26
+ # @deprecated (but still works while Anthropic API responds to it)
14
27
  def complete(parameters: {})
15
28
  parameters[:prompt] = wrap_prompt(prompt: parameters[:prompt])
16
- Anthropic::Client.json_post(path: "/complete", parameters: parameters)
29
+ json_post(path: "/complete", parameters: parameters)
30
+ end
31
+
32
+ # Anthropic API Parameters as of 2024-05-07:
33
+ # @see https://docs.anthropic.com/claude/reference/messages_post
34
+ #
35
+ # @param [Hash] parameters
36
+ # @option parameters [Array] :messages - Required. An array of messages to send to the API. Each
37
+ # message should have a role and content. Single message example:
38
+ # +[{ role: "user", content: "Hello, Claude!" }]+
39
+ # @option parameters [String] :model - see https://docs.anthropic.com/claude/docs/models-overview
40
+ # @option parameters [Integer] :max_tokens - Required, must be less than 4096 - @see https://docs.anthropic.com/claude/docs/models-overview
41
+ # @option parameters [String] :system - Optional but recommended. @see https://docs.anthropic.com/claude/docs/system-prompts
42
+ # @option parameters [Float] :temperature - Optional, defaults to 1.0
43
+ # @option parameters [Proc] :stream - Optional, if present, must be a Proc that will receive the
44
+ # content fragments as they come in
45
+ # @option parameters [String] :preprocess_stream - If true, the streaming Proc will be pre-
46
+ # processed. Specifically, instead of being passed a raw Hash like:
47
+ # {"type"=>"content_block_delta", "index"=>0, "delta"=>{"type"=>"text_delta", "text"=>" of"}}
48
+ # the Proc will instead be passed something nicer. If +preprocess_stream+ is set to +"json"+
49
+ # or +:json+, then the Proc will only receive full json objects, one at a time.
50
+ # If +preprocess_stream+ is set to +"text"+ or +:text+ then the Proc will receive two
51
+ # arguments: the first will be the text accrued so far, and the second will be the delta
52
+ # just received in the current chunk.
53
+ #
54
+ # @returns [Hash] the response from the API (after the streaming is done, if streaming)
55
+ # @example:
56
+ # {
57
+ # "id" => "msg_013xVudG9xjSvLGwPKMeVXzG",
58
+ # "type" => "message",
59
+ # "role" => "assistant",
60
+ # "content" => [{"type" => "text", "text" => "The sky has no distinct"}],
61
+ # "model" => "claude-2.1",
62
+ # "stop_reason" => "max_tokens",
63
+ # "stop_sequence" => nil,
64
+ # "usage" => {"input_tokens" => 15, "output_tokens" => 5}
65
+ # }
66
+ def messages(parameters: {})
67
+ json_post(path: "/messages", parameters: parameters)
17
68
  end
18
69
 
19
70
  private
20
71
 
72
+ # Used only by @deprecated +complete+ method
21
73
  def wrap_prompt(prompt:, prefix: "\n\nHuman: ", suffix: "\n\nAssistant:")
22
74
  return if prompt.nil?
23
75
 
@@ -1,23 +1,40 @@
1
+ require "event_stream_parser"
2
+
3
+ require_relative "http_headers"
4
+
5
+ # rubocop:disable Metrics/ModuleLength
1
6
  module Anthropic
2
7
  module HTTP
8
+ include HTTPHeaders
9
+
10
+ # Unused?
3
11
  def get(path:)
4
12
  to_json(conn.get(uri(path: path)) do |req|
5
13
  req.headers = headers
6
14
  end&.body)
7
15
  end
8
16
 
17
+ # This is currently the workhorse for all API calls.
18
+ # rubocop:disable Metrics/MethodLength
9
19
  def json_post(path:, parameters:)
10
- to_json(conn.post(uri(path: path)) do |req|
20
+ str_resp = {}
21
+ response = conn.post(uri(path: path)) do |req|
11
22
  if parameters[:stream].is_a?(Proc)
12
- req.options.on_data = to_json_stream(user_proc: parameters[:stream])
23
+ req.options.on_data = to_json_stream(user_proc: parameters[:stream], response: str_resp,
24
+ preprocess: parameters[:preprocess_stream])
13
25
  parameters[:stream] = true # Necessary to tell Anthropic to stream.
26
+ parameters.delete(:preprocess_stream)
14
27
  end
15
28
 
16
29
  req.headers = headers
17
30
  req.body = parameters.to_json
18
- end&.body)
31
+ end
32
+
33
+ str_resp.empty? ? response.body : str_resp
19
34
  end
35
+ # rubocop:enable Metrics/MethodLength
20
36
 
37
+ # Unused?
21
38
  def multipart_post(path:, parameters: nil)
22
39
  to_json(conn(multipart: true).post(uri(path: path)) do |req|
23
40
  req.headers = headers.merge({ "Content-Type" => "multipart/form-data" })
@@ -25,6 +42,7 @@ module Anthropic
25
42
  end&.body)
26
43
  end
27
44
 
45
+ # Unused?
28
46
  def delete(path:)
29
47
  to_json(conn.delete(uri(path: path)) do |req|
30
48
  req.headers = headers
@@ -33,6 +51,7 @@ module Anthropic
33
51
 
34
52
  private
35
53
 
54
+ # Used only by unused methods?
36
55
  def to_json(string)
37
56
  return unless string
38
57
 
@@ -44,41 +63,119 @@ module Anthropic
44
63
 
45
64
  # Given a proc, returns an outer proc that can be used to iterate over a JSON stream of chunks.
46
65
  # For each chunk, the inner user_proc is called giving it the JSON object. The JSON object could
47
- # be a data object or an error object as described in the Anthropic API documentation.
48
- #
49
- # If the JSON object for a given data or error message is invalid, it is ignored.
66
+ # be a data object or an error object as described in the OpenAI API documentation.
50
67
  #
51
68
  # @param user_proc [Proc] The inner proc to call for each JSON object in the chunk.
52
69
  # @return [Proc] An outer proc that iterates over a raw stream, converting it to JSON.
53
- def to_json_stream(user_proc:)
54
- proc do |chunk, _|
55
- chunk.scan(/(?:data|error): (\{.*\})/i).flatten.each do |data|
56
- user_proc.call(JSON.parse(data))
57
- rescue JSON::ParserError
58
- # Ignore invalid JSON.
70
+ # rubocop:disable Metrics/MethodLength
71
+ def to_json_stream(user_proc:, response:, preprocess: nil)
72
+ parser = EventStreamParser::Parser.new
73
+ preprocess_stack = ""
74
+
75
+ proc do |chunk, _bytes, env|
76
+ handle_faraday_error(chunk, env)
77
+
78
+ parser.feed(chunk) do |type, data|
79
+ parsed_data = JSON.parse(data)
80
+
81
+ _handle_message_type(type, parsed_data, response) do |delta|
82
+ preprocess(preprocess, preprocess_stack, delta, user_proc) unless preprocess.nil?
83
+ end
84
+
85
+ user_proc.call(parsed_data) if preprocess.nil?
59
86
  end
60
87
  end
61
88
  end
89
+ # rubocop:enable Metrics/MethodLength
90
+
91
+ # rubocop:disable Metrics/MethodLength
92
+ def _handle_message_type(type, parsed_data, response, &block)
93
+ case type
94
+ when "message_start"
95
+ response.merge!(parsed_data["message"])
96
+ response["content"] = [{ "type" => "text", "text" => "" }]
97
+ when "message_delta"
98
+ response["usage"].merge!(parsed_data["usage"])
99
+ response.merge!(parsed_data["delta"])
100
+ when "content_block_delta"
101
+ delta = parsed_data["delta"]["text"]
102
+ response["content"][0]["text"].concat(delta)
103
+ block.yield delta
104
+ end
105
+ end
106
+ # rubocop:enable Metrics/MethodLength
107
+
108
+ # Decides whether to preprocess JSON or text and calls the appropriate method.
109
+ def preprocess(directive, stack, delta, user_proc)
110
+ stack.concat(delta)
111
+ case directive
112
+ when :json
113
+ preprocess_json(stack, delta, user_proc)
114
+ when :text
115
+ preprocess_text(stack, delta, user_proc)
116
+ else
117
+ raise Anthropic::Error,
118
+ "Invalid preprocess directive (valid: :text, :json): #{directive.inspect}"
119
+ end
120
+ end
121
+
122
+ # Just sends the incremental response (aka stack) and delta up to the user
123
+ def preprocess_text(stack, delta, user_proc)
124
+ user_proc.call(stack, delta)
125
+ end
126
+
127
+ # If the stack contains a +}+, uses a regexp to try to find a complete JSON object.
128
+ # If it finds one, it calls the user_proc with the JSON object. If it fails, catches and logs
129
+ # an exception but does not currently raise it, which means that if there is just one malformed
130
+ # JSON object (which does happen, albeit rarely), it will continue and process the other ones.
131
+ #
132
+ # TODO: Make the exception processing parametrisable (set logger? exit on error?)
133
+ def preprocess_json(stack, _delta, user_proc)
134
+ if stack.strip.include?("}")
135
+ matches = stack.match(/\{(?:[^{}]|\g<0>)*\}/)
136
+ user_proc.call(JSON.parse(matches[0]))
137
+ stack.clear
138
+ end
139
+ rescue StandardError => e
140
+ log(e)
141
+ ensure
142
+ stack.clear if stack.strip.include?("}")
143
+ end
144
+
145
+ def log(error)
146
+ logger = Logger.new($stdout)
147
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
148
+ "\033[31mAnthropic JSON Error (spotted in ruby-anthropic #{VERSION}): #{msg}\n\033[0m"
149
+ end
150
+ logger.error(error)
151
+ end
152
+
153
+ def handle_faraday_error(chunk, env)
154
+ return unless env&.status != 200
155
+
156
+ raise_error = Faraday::Response::RaiseError.new
157
+ raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
158
+ end
62
159
 
63
160
  def conn(multipart: false)
64
- Faraday.new do |f|
65
- f.options[:timeout] = Anthropic.configuration.request_timeout
161
+ connection = Faraday.new do |f|
162
+ f.options[:timeout] = @request_timeout
66
163
  f.request(:multipart) if multipart
164
+ f.use MiddlewareErrors if @log_errors
165
+ f.response :raise_error
166
+ f.response :json
67
167
  end
168
+
169
+ @faraday_middleware&.call(connection)
170
+
171
+ connection
68
172
  end
69
173
 
70
174
  def uri(path:)
71
175
  Anthropic.configuration.uri_base + Anthropic.configuration.api_version + path
72
176
  end
73
177
 
74
- def headers
75
- {
76
- "Content-Type" => "application/json",
77
- "x-api-key" => Anthropic.configuration.access_token,
78
- "Anthropic-Version" => Anthropic.configuration.anthropic_version
79
- }.merge(Anthropic.configuration.extra_headers)
80
- end
81
-
178
+ # Unused except by unused method
82
179
  def multipart_parameters(parameters)
83
180
  parameters&.transform_values do |value|
84
181
  next value unless value.is_a?(File)
@@ -89,5 +186,12 @@ module Anthropic
89
186
  Faraday::UploadIO.new(value, "", value.path)
90
187
  end
91
188
  end
189
+
190
+ def try_parse_json(maybe_json)
191
+ JSON.parse(maybe_json)
192
+ rescue JSON::ParserError
193
+ maybe_json
194
+ end
92
195
  end
93
196
  end
197
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,38 @@
1
+ module Anthropic
2
+ module HTTPHeaders
3
+ def add_headers(headers)
4
+ @extra_headers = extra_headers.merge(headers.transform_keys(&:to_s))
5
+ end
6
+
7
+ private
8
+
9
+ def headers
10
+ # TODO: Implement Amazon Bedrock headers
11
+ # if azure?
12
+ # azure_headers
13
+ # else
14
+ # openai_headers
15
+ # end.merge(extra_headers)
16
+ anthropic_headers.merge(extra_headers)
17
+ end
18
+
19
+ def anthropic_headers
20
+ {
21
+ "x-api-key" => @access_token,
22
+ "anthropic-version" => @anthropic_version,
23
+ "Content-Type" => "application/json"
24
+ }
25
+ end
26
+
27
+ # def azure_headers
28
+ # {
29
+ # "Content-Type" => "application/json",
30
+ # "api-key" => @access_token
31
+ # }
32
+ # end
33
+
34
+ def extra_headers
35
+ @extra_headers ||= {}
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  module Anthropic
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
data/lib/anthropic.rb CHANGED
@@ -9,9 +9,25 @@ module Anthropic
9
9
  class Error < StandardError; end
10
10
  class ConfigurationError < Error; end
11
11
 
12
+ class MiddlewareErrors < Faraday::Middleware
13
+ def call(env)
14
+ @app.call(env)
15
+ rescue Faraday::Error => e
16
+ raise e unless e.response.is_a?(Hash)
17
+
18
+ logger = Logger.new($stdout)
19
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
20
+ "\033[31mAnthropic HTTP Error (spotted in ruby-anthropic #{VERSION}): #{msg}\n\033[0m"
21
+ end
22
+ logger.error(e.response[:body])
23
+
24
+ raise e
25
+ end
26
+ end
27
+
12
28
  class Configuration
13
29
  attr_writer :access_token
14
- attr_accessor :anthropic_version, :api_version, :extra_headers, :organization_id,
30
+ attr_accessor :anthropic_version, :api_version, :extra_headers,
15
31
  :request_timeout, :uri_base
16
32
 
17
33
  DEFAULT_API_VERSION = "v1".freeze
@@ -23,9 +39,9 @@ module Anthropic
23
39
  @access_token = nil
24
40
  @api_version = DEFAULT_API_VERSION
25
41
  @anthropic_version = DEFAULT_ANTHROPIC_VERSION
26
- @organization_id = nil
27
42
  @uri_base = DEFAULT_URI_BASE
28
43
  @request_timeout = DEFAULT_REQUEST_TIMEOUT
44
+ @extra_headers = {}
29
45
  end
30
46
 
31
47
  def access_token
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anthropic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-07-18 00:00:00.000000000 Z
11
+ date: 2024-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: event_stream_parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.3.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
13
33
  - !ruby/object:Gem::Dependency
14
34
  name: faraday
15
35
  requirement: !ruby/object:Gem::Requirement
@@ -67,6 +87,7 @@ files:
67
87
  - lib/anthropic/client.rb
68
88
  - lib/anthropic/compatibility.rb
69
89
  - lib/anthropic/http.rb
90
+ - lib/anthropic/http_headers.rb
70
91
  - lib/anthropic/version.rb
71
92
  - lib/ruby/anthropic.rb
72
93
  - pull_request_template.md
@@ -93,8 +114,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
114
  - !ruby/object:Gem::Version
94
115
  version: '0'
95
116
  requirements: []
96
- rubygems_version: 3.4.12
117
+ rubygems_version: 3.5.11
97
118
  signing_key:
98
119
  specification_version: 4
99
- summary: "Anthropic API + Ruby! \U0001F30C❤️"
120
+ summary: "Anthropic API + Ruby! \U0001F916\U0001F30C"
100
121
  test_files: []