anthropic 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5591b52e376e9a5915a3011d9919d188aaa441fa65f6a8d665b160c5529b0564
4
- data.tar.gz: e251bf3dd61569807e722f6fe116498fd6801e07fc2333c2f2f40c03e61b5b9f
3
+ metadata.gz: ef2ed78af9f188f394fce182554887c8a38c68864f05796f23ccd3e57cf38d40
4
+ data.tar.gz: 65495af068a5f373e6d77dbc0085846c75e1a77dab2139fe0a06bbc62678e3ea
5
5
  SHA512:
6
- metadata.gz: 6beac221f3595b6599e37175190a59fc20681aa679d7746f060447850aab0b4ca7362ef5ce99a458a9ce1a5f6acfc5a6491ca5bdb7147efadf14cca068576d08
7
- data.tar.gz: 39189b30389c23c56956b32135587513f3ae7f2927e323262333c82392b5c24239c3a94486a68b2bd569856fe34334a19eb8a3ad7a0f0d99ef9bd5ec16283580
6
+ metadata.gz: e508affc831d8136633d74ca73026fb5936d52bf1ffbb82541a8ead24c5e4dec640faef5d3747986d242bde4296efc6e5ceac6688c091641da13b74a27371819
7
+ data.tar.gz: 4fd4fd39c63c902c1a54d71fe884329eaca8d7d5a385beb6fdf3c2e3fabeb8cbf4f67a41014d2df43eef15707ef474ee892c7f954f46ca34f33c625577038b6f
data/CHANGELOG.md CHANGED
@@ -5,11 +5,17 @@ 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.3.0] - 2024-06-10
9
+
10
+ ### Added
11
+
12
+ - Add chat streaming! Thank you to the inimitable [@swombat](https://github.com/swombat) for adding this vital functionality!
13
+
8
14
  ## [0.2.0] - 2024-04-25
9
15
 
10
16
  ### Added
11
17
 
12
- - Add new Messages endpoint - thanks [@deepakmahakale](https://github.com/deepakmahakale) for the PR, [@obie](https://github.com/obie) for the first pass, and many others for requesting and contributions!
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
 
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.2.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
@@ -6,9 +6,7 @@
6
6
 
7
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).
10
-
11
- 🚢 Need someone to ship critical Rails features for you, fast? I'm taking on a few new clients at an experimental crazy low price, check it out: [railsai.com](https://railsai.com?utm_source=anthropic&utm_medium=readme&utm_id=26072023)
9
+ You can get access to the API [here](https://docs.anthropic.com/claude/docs/getting-access-to-claude).
12
10
 
13
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)
14
12
 
@@ -135,6 +133,116 @@ response = client.messages(
135
133
  # => }
136
134
  ```
137
135
 
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).
140
+
141
+ There are two special parameters, though, to do with... streaming. Keep reading to find out more.
142
+
143
+ #### JSON
144
+
145
+ If you want your output to be json, it is recommended to provide an additional message like this:
146
+
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
+ ```
172
+
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.
176
+
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.
245
+
138
246
  ## Development
139
247
 
140
248
  After checking out the repo, run `bin/setup` to install dependencies. You can run `bin/console` for an interactive prompt that will allow you to experiment.
data/anthropic.gemspec CHANGED
@@ -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,27 +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)
17
30
  end
18
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
+ # }
19
66
  def messages(parameters: {})
20
- Anthropic::Client.json_post(path: "/messages", parameters: parameters)
67
+ json_post(path: "/messages", parameters: parameters)
21
68
  end
22
69
 
23
70
  private
24
71
 
72
+ # Used only by @deprecated +complete+ method
25
73
  def wrap_prompt(prompt:, prefix: "\n\nHuman: ", suffix: "\n\nAssistant:")
26
74
  return if prompt.nil?
27
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.2.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.2.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: 2024-04-25 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,7 +114,7 @@ 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.22
117
+ rubygems_version: 3.5.11
97
118
  signing_key:
98
119
  specification_version: 4
99
120
  summary: "Anthropic API + Ruby! \U0001F916\U0001F30C"