anthropic 0.2.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: 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"