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 +4 -4
- data/CHANGELOG.md +14 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -1
- data/README.md +162 -33
- data/Rakefile +14 -1
- data/anthropic.gemspec +2 -1
- data/lib/anthropic/client.rb +61 -9
- 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 +25 -4
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,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.
|
8
|
+
## [0.3.0] - 2024-06-10
|
9
9
|
|
10
10
|
### Added
|
11
11
|
|
12
|
-
-
|
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
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
@@ -1,18 +1,14 @@
|
|
1
|
-
# Anthropic
|
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
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
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
|
-
###
|
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
|
-
|
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.
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
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
|
-
|
141
|
+
There are two special parameters, though, to do with... streaming. Keep reading to find out more.
|
111
142
|
|
112
|
-
|
143
|
+
#### JSON
|
113
144
|
|
114
|
-
|
145
|
+
If you want your output to be json, it is recommended to provide an additional message like this:
|
115
146
|
|
116
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
data/lib/anthropic/client.rb
CHANGED
@@ -1,23 +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)
|
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
|
|
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:
|
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.
|
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: []
|