Skip to content

Commit

Permalink
Use data objects to encapsulate responses (#26)
Browse files Browse the repository at this point in the history
* Refactor client responses to use data objects

* Update README
  • Loading branch information
dickdavis committed Jun 25, 2024
1 parent f405326 commit 0639b0e
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 183 deletions.
109 changes: 69 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,58 @@ You can send a request to the Messages API.
Anthropic.messages.create(model: 'claude-2.1', max_tokens: 200, messages: [{role: 'user', content: 'Yo what up?'}])

# Output =>
# {
# id: "msg_013ePdwEkb4RMC1hCE61Hbm8",
# type: "message",
# role: "assistant",
# content: [{type: "text", text: "Hello! Not much up with me, just chatting. How about you?"}],
# model: "claude-2.1",
# stop_reason: "end_turn",
# stop_sequence: nil
# }
#<data Anthropic::Client::Response status="success", body={:id=>"msg_01UqHiw6oFLjMYiLV8hkXsrR", :type=>"message", :role=>"assistant", :model=>"claude-2.1", :content=>[{:type=>"text", :text=>"Hello! Not much up with me, just chatting with you. How's it going?"}], :stop_reason=>"end_turn", :stop_sequence=>nil, :usage=>{:input_tokens=>13, :output_tokens=>22}}>
```

Alternatively, you can stream the response:

```ruby
Anthropic.messages.create(model: 'claude-2.1', max_tokens: 200, messages: [{role: 'user', content: 'Yo what up?'}], stream: true) do |event|
options = {
model: 'claude-2.1',
max_tokens: 200,
messages: [{role: 'user', content: 'Yo what up?'}],
stream: true
}

Anthropic.messages.create(**options) do |event|
puts event
end

# Output =>
# { type: 'message_start', message: { id: 'msg_012pkeozZynwyNvSagwL7kMw', type: 'message', role: 'assistant', content: [], model: 'claude-2.1', stop_reason: nil, stop_sequence: nil } }
# { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }
# { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'Hello' } }
# { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '.' } }
# { type: 'content_block_stop', index: 0 }
# { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: nil } }
# { type: 'message_stop' }
#<data Anthropic::Client::Response status="success", body={:type=>"message_start", :message=>{:id=>"msg_01EsYcQkBJrHrtgpY5ZcLzvf", :type=>"message", :role=>"assistant", :model=>"claude-2.1", :content=>[], :stop_reason=>nil, :stop_sequence=>nil, :usage=>{:input_tokens=>13, :output_tokens=>1}}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_start", :index=>0, :content_block=>{:type=>"text", :text=>""}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>"Hello"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>"!"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" Not"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" much"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" up"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" with"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" me"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>","}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" I"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>"'m"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" an"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" AI"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" assistant"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" create"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>"d by"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>" An"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>"throp"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>"ic"}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_delta", :index=>0, :delta=>{:type=>"text_delta", :text=>"."}}>
#<data Anthropic::Client::Response status="success", body={:type=>"content_block_stop", :index=>0}>
#<data Anthropic::Client::Response status="success", body={:type=>"message_delta", :delta=>{:stop_reason=>"end_turn", :stop_sequence=>nil}, :usage=>{:output_tokens=>23}}>
#<data Anthropic::Client::Response status="success", body={:type=>"message_stop"}>

# Or, if you just want to print the text content:
Anthropic.messages.create(**options) do |event|
next unless event.body[:type] == 'content_block_delta'

print event.body[:delta][:text]
end

# Output =>
# Hello! Not much up with me, I'm an AI assistant created by Anthropic.
```

You can also experiment with the new tools beta by passing the `beta` name when calling the API. This will ensure each request includes the correct beta header and validate properly.
Expand Down Expand Up @@ -103,38 +129,41 @@ Anthropic.completions.create(
)

# Output =>
# {
# completion: "Hello! Not much going on with me, just chatting. How about you?",
# stop_reason: "stop_sequence",
# model: "claude-2.1",
# stop: "\n\nHuman:",
# log_id: "2496914137c520ec2b4ae8315864bcf3a4c6ce9f2e3c96e13be4c004587313ca"
# }
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_01Y9ptPR7xGHaH9rC3ffJExU", :completion=>" Hello! Not much going on here. How about you?", :stop_reason=>"stop_sequence", :model=>"claude-2.1", :stop=>"\n\nHuman:", :log_id=>"compl_01Y9ptPR7xGHaH9rC3ffJExU"}>
```

Alternatively, you can stream the response:

```ruby
Anthropic.completions.create(model: 'claude-2', max_tokens_to_sample: 200, prompt: 'Human: Yo what up?\n\nAssistant:', stream: true) do |event|
options = {
model: 'claude-2',
max_tokens_to_sample: 200,
prompt: 'Human: Yo what up?\n\nAssistant:',
stream: true
}

Anthropic.completions.create(**options) do |event|
puts event
end

# Output =>
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' Hello', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: '!', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' Not', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' much', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ',', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' just', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' chatting', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' with', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' people', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: '.', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' How', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' about', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: ' you', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: '?', stop_reason: nil, model: 'claude-2.1', stop: nil, log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
# { type: 'completion', id: 'compl_01G6cEfdZtLEEJVRzwUShiDY', completion: '', stop_reason: 'stop_sequence', model: 'claude-2.1', stop: "\n\nHuman:", log_id: 'compl_01G6cEfdZtLEEJVRzwUShiDY' }
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>" Hello", :stop_reason=>nil, :model=>"claude-2.1", :stop=>nil, :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>"!", :stop_reason=>nil, :model=>"claude-2.1", :stop=>nil, :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>" Not", :stop_reason=>nil, :model=>"claude-2.1", :stop=>nil, :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>" much", :stop_reason=>nil, :model=>"claude-2.1", :stop=>nil, :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>" going", :stop_reason=>nil, :model=>"claude-2.1", :stop=>nil, :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>" on", :stop_reason=>nil, :model=>"claude-2.1", :stop=>nil, :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>" here", :stop_reason=>nil, :model=>"claude-2.1", :stop=>nil, :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>".", :stop_reason=>nil, :model=>"claude-2.1", :stop=>nil, :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>
#<data Anthropic::Client::Response status="success", body={:type=>"completion", :id=>"compl_015AktggW7tcM4w11YpkuMbP", :completion=>"", :stop_reason=>"stop_sequence", :model=>"claude-2.1", :stop=>"\n\nHuman:", :log_id=>"compl_015AktggW7tcM4w11YpkuMbP"}>

# Or, if you just want to print the text content:
Anthropic.completions.create(**options) do |event|
print event.body[:completion]
end

# Output =>
# Hello! Not much, just chatting with you. How's it going?
```

## Installation
Expand Down
46 changes: 29 additions & 17 deletions lib/anthropic/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ module Anthropic
##
# Provides a client for sending HTTP requests.
class Client
Response = Data.define(:status, :body)

# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
def self.post(url, data, headers = {})
response = HTTPX.with(
Expand All @@ -16,29 +18,29 @@ def self.post(url, data, headers = {})
}.merge(headers)
).post(url, json: data)

response_body = JSON.parse(response.body, symbolize_names: true)
response_data = build_response(response.body)

case response.status
when 200
response_body
response_data
when 400
raise Anthropic::Errors::InvalidRequestError, response_body
raise Anthropic::Errors::InvalidRequestError, response_data
when 401
raise Anthropic::Errors::AuthenticationError, response_body
raise Anthropic::Errors::AuthenticationError, response_data
when 403
raise Anthropic::Errors::PermissionError, response_body
raise Anthropic::Errors::PermissionError, response_data
when 404
raise Anthropic::Errors::NotFoundError, response_body
raise Anthropic::Errors::NotFoundError, response_data
when 409
raise Anthropic::Errors::ConflictError, response_body
raise Anthropic::Errors::ConflictError, response_data
when 422
raise Anthropic::Errors::UnprocessableEntityError, response_body
raise Anthropic::Errors::UnprocessableEntityError, response_data
when 429
raise Anthropic::Errors::RateLimitError, response_body
raise Anthropic::Errors::RateLimitError, response_data
when 500
raise Anthropic::Errors::ApiError, response_body
raise Anthropic::Errors::ApiError, response_data
when 529
raise Anthropic::Errors::OverloadedError, response_body
raise Anthropic::Errors::OverloadedError, response_data
end
end

Expand All @@ -53,13 +55,13 @@ def self.post_as_stream(url, data, headers = {})
).post(url, json: data, stream: true)

response.each_line do |line|
event, data = line.split(/(\w+\b:\s)/)[1..2]
next unless event && data
type, event = line.split(/(\w+\b:\s)/)[1..2]

next unless type&.start_with?('data') && event

if event.start_with?('data')
formatted_data = JSON.parse(data, symbolize_names: true)
yield formatted_data unless %w[ping error].include?(formatted_data[:type])
end
response_data = build_response(event)

yield response_data unless %w[ping error].include?(response_data.body[:type])
end
rescue HTTPX::HTTPError => error
case error.response.status
Expand All @@ -84,5 +86,15 @@ def self.post_as_stream(url, data, headers = {})
end
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

class << self
private

def build_response(response)
response_hash = JSON.parse(response, symbolize_names: true)
status = response_hash[:type] == 'error' ? 'failure' : 'success'
Anthropic::Client::Response.new(status:, body: response_hash)
end
end
end
end
27 changes: 15 additions & 12 deletions spec/lib/anthropic/api/completions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,16 @@
let(:body) { 'data: {"bar":"foo"}' }
let(:events) { [] }

it 'raises an error if the API version is not supported' do
allow(Anthropic).to receive(:api_version).and_return('2023-06-02')
expect { call_method }.to raise_error(Anthropic::Errors::UnsupportedApiVersionError)
before do
stub_http_request(:post, 'https://api.anthropic.com/v1/complete').to_return(status: 200, body:)
end

it 'receives streamed events' do
stub_http_request(:post, 'https://api.anthropic.com/v1/complete').to_return(status: 200, body:)
call_method
expect(events).to eq([{ bar: 'foo' }])
aggregate_failures do
expect(events.map(&:status).uniq).to eq(%w[success])
expect(events.map(&:body)).to eq([{ bar: 'foo' }])
end
end
end

Expand All @@ -66,17 +67,19 @@
}
end

it 'raises an error if the API version is not supported' do
allow(Anthropic).to receive(:api_version).and_return('2023-06-02')
expect { call_method }.to raise_error(Anthropic::Errors::UnsupportedApiVersionError)
end

it 'returns the response from the API' do
before do
stub_http_request(:post, 'https://api.anthropic.com/v1/complete').and_return(
status: 200,
body: JSON.generate(response_body)
)
expect(call_method).to eq(response_body)
end

it 'returns the response from the API' do
response = call_method
aggregate_failures do
expect(response.status).to eq('success')
expect(response.body).to eq(response_body)
end
end
end
# rubocop:enable RSpec/NestedGroups
Expand Down
35 changes: 23 additions & 12 deletions spec/lib/anthropic/api/messages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
'data: {"type":"message_stop"}'
].join("\r\n")
end

let(:events) { [] }
let(:expected_events) do
[
Expand All @@ -107,15 +108,16 @@
]
end

it 'raises an error if the API version is not supported' do
allow(Anthropic).to receive(:api_version).and_return('2023-06-02')
expect { call_method }.to raise_error(Anthropic::Errors::UnsupportedApiVersionError)
before do
stub_http_request(:post, 'https://api.anthropic.com/v1/messages').to_return(status: 200, body:)
end

it 'receives streamed events' do
stub_http_request(:post, 'https://api.anthropic.com/v1/messages').to_return(status: 200, body:)
call_method
expect(events).to eq(expected_events)
aggregate_failures do
expect(events.map(&:status).uniq).to eq(['success'])
expect(events.map(&:body)).to eq(expected_events)
end
end
end

Expand All @@ -139,17 +141,26 @@
}
end

it 'raises an error if the API version is not supported' do
allow(Anthropic).to receive(:api_version).and_return('2023-06-02')
expect { call_method }.to raise_error(Anthropic::Errors::UnsupportedApiVersionError)
end

it 'returns the response from the API' do
before do
stub_http_request(:post, 'https://api.anthropic.com/v1/messages').and_return(
status: 200,
body: JSON.generate(response_body)
)
expect(call_method).to eq(response_body)
end

it 'returns the response from the API' do
response = call_method
aggregate_failures do
expect(response.status).to eq('success')
expect(response.body).to eq(response_body)
end
end

context 'with invalid API version configured' do
it 'raises an error' do
allow(Anthropic).to receive(:api_version).and_return('2023-06-02')
expect { call_method }.to raise_error(Anthropic::Errors::UnsupportedApiVersionError)
end
end
end
# rubocop:enable RSpec/NestedGroups
Expand Down
Loading

0 comments on commit 0639b0e

Please sign in to comment.