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 +4 -4
- data/CHANGELOG.md +7 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -1
- data/README.md +111 -3
- data/anthropic.gemspec +1 -0
- data/lib/anthropic/client.rb +58 -10
- data/lib/anthropic/http.rb +126 -22
- data/lib/anthropic/http_headers.rb +38 -0
- data/lib/anthropic/version.rb +1 -1
- data/lib/anthropic.rb +18 -2
- metadata +24 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef2ed78af9f188f394fce182554887c8a38c68864f05796f23ccd3e57cf38d40
|
4
|
+
data.tar.gz: 65495af068a5f373e6d77dbc0085846c75e1a77dab2139fe0a06bbc62678e3ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 [@
|
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
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
anthropic (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
|
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
|
data/lib/anthropic/client.rb
CHANGED
@@ -1,27 +1,75 @@
|
|
1
1
|
module Anthropic
|
2
2
|
class Client
|
3
|
-
|
3
|
+
include Anthropic::HTTP
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
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
|
|
data/lib/anthropic/http.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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] =
|
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
|
-
|
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
|
data/lib/anthropic/version.rb
CHANGED
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,
|
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.
|
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-
|
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.
|
117
|
+
rubygems_version: 3.5.11
|
97
118
|
signing_key:
|
98
119
|
specification_version: 4
|
99
120
|
summary: "Anthropic API + Ruby! \U0001F916\U0001F30C"
|