ruby-anthropic 0.4.2

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.
@@ -0,0 +1,200 @@
1
+ require "event_stream_parser"
2
+
3
+ require_relative "http_headers"
4
+
5
+ # rubocop:disable Metrics/ModuleLength
6
+ module Anthropic
7
+ module HTTP
8
+ include HTTPHeaders
9
+
10
+ def get(path:, parameters: nil)
11
+ response = conn.get(uri(path: path), parameters) do |req|
12
+ req.headers = headers
13
+ end
14
+ response&.body
15
+ end
16
+
17
+ # rubocop:disable Metrics/MethodLength
18
+ # rubocop:disable Metrics/AbcSize
19
+ def json_post(path:, parameters: {})
20
+ str_resp = {}
21
+ response = conn.post(uri(path: path)) do |req|
22
+ if parameters.respond_to?(:key?) && parameters[:stream].is_a?(Proc)
23
+ req.options.on_data = to_json_stream(user_proc: parameters[:stream], response: str_resp,
24
+ preprocess: parameters[:preprocess_stream])
25
+ parameters[:stream] = true # Necessary to tell Anthropic to stream.
26
+ parameters.delete(:preprocess_stream)
27
+ end
28
+
29
+ req.headers = headers
30
+ req.body = parameters.to_json
31
+ end
32
+
33
+ str_resp.empty? ? response.body : str_resp
34
+ end
35
+ # rubocop:enable Metrics/MethodLength
36
+ # rubocop:enable Metrics/AbcSize
37
+
38
+ # Unused - leave in until v1 since someone might be using it.
39
+ def multipart_post(path:, parameters: nil)
40
+ to_json(conn(multipart: true).post(uri(path: path)) do |req|
41
+ req.headers = headers.merge({ "Content-Type" => "multipart/form-data" })
42
+ req.body = multipart_parameters(parameters)
43
+ end&.body)
44
+ end
45
+
46
+ # Unused - leave in until v1 since someone might be using it.
47
+ def delete(path:)
48
+ to_json(conn.delete(uri(path: path)) do |req|
49
+ req.headers = headers
50
+ end&.body)
51
+ end
52
+
53
+ private
54
+
55
+ # Used only in unused methods - leave in until v1 since someone might be using it.
56
+ def to_json(string)
57
+ return unless string
58
+
59
+ JSON.parse(string)
60
+ rescue JSON::ParserError
61
+ # Convert a multiline string of JSON objects to a JSON array.
62
+ JSON.parse(string.gsub("}\n{", "},{").prepend("[").concat("]"))
63
+ end
64
+
65
+ # Given a proc, returns an outer proc that can be used to iterate over a JSON stream of chunks.
66
+ # For each chunk, the inner user_proc is called giving it the JSON object. The JSON object could
67
+ # be a data object or an error object as described in the Anthropic API documentation.
68
+ #
69
+ # @param user_proc [Proc] The inner proc to call for each JSON object in the chunk.
70
+ # @return [Proc] An outer proc that iterates over a raw stream, converting it to JSON.
71
+ # rubocop:disable Metrics/MethodLength
72
+ def to_json_stream(user_proc:, response:, preprocess: nil)
73
+ parser = EventStreamParser::Parser.new
74
+ preprocess_stack = ""
75
+
76
+ proc do |chunk, _bytes, env|
77
+ # Versions of Faraday < 2.5.0 do not include an `env` argument,
78
+ # so we have to assume they are successful
79
+ handle_faraday_error(chunk, env) if env
80
+
81
+ parser.feed(chunk) do |type, data|
82
+ parsed_data = JSON.parse(data)
83
+
84
+ _handle_message_type(type, parsed_data, response) do |delta|
85
+ preprocess(preprocess, preprocess_stack, delta, user_proc) unless preprocess.nil?
86
+ end
87
+
88
+ user_proc.call(parsed_data) if preprocess.nil?
89
+ end
90
+ end
91
+ end
92
+ # rubocop:enable Metrics/MethodLength
93
+
94
+ # rubocop:disable Metrics/MethodLength
95
+ def _handle_message_type(type, parsed_data, response, &block)
96
+ case type
97
+ when "message_start"
98
+ response.merge!(parsed_data["message"])
99
+ response["content"] = [{ "type" => "text", "text" => "" }]
100
+ when "message_delta"
101
+ response["usage"].merge!(parsed_data["usage"])
102
+ response.merge!(parsed_data["delta"])
103
+ when "content_block_delta"
104
+ delta = parsed_data["delta"]["text"]
105
+ response["content"][0]["text"].concat(delta) if delta
106
+ block.yield delta
107
+ end
108
+ end
109
+ # rubocop:enable Metrics/MethodLength
110
+
111
+ # Decides whether to preprocess JSON or text and calls the appropriate method.
112
+ def preprocess(directive, stack, delta, user_proc)
113
+ stack.concat(delta) if delta # Alters the stack.
114
+ case directive
115
+ when :json
116
+ preprocess_json(stack, delta, user_proc)
117
+ when :text
118
+ preprocess_text(stack, delta, user_proc)
119
+ else
120
+ raise Anthropic::Error,
121
+ "Invalid preprocess directive (valid: :text, :json): #{directive.inspect}"
122
+ end
123
+ end
124
+
125
+ # Just sends the incremental response (aka stack) and delta up to the user
126
+ def preprocess_text(stack, delta, user_proc)
127
+ user_proc.call(stack, delta)
128
+ end
129
+
130
+ # If the stack contains a +}+, uses a regexp to try to find a complete JSON object.
131
+ # If it finds one, it calls the user_proc with the JSON object. If it fails, catches and logs
132
+ # an exception but does not currently raise it, which means that if there is just one malformed
133
+ # JSON object (which does happen, albeit rarely), it will continue and process the other ones.
134
+ #
135
+ # TODO: Make the exception processing parametrisable (set logger? exit on error?)
136
+ def preprocess_json(stack, _delta, user_proc)
137
+ if stack.strip.include?("}")
138
+ matches = stack.match(/\{(?:[^{}]|\g<0>)*\}/)
139
+ user_proc.call(JSON.parse(matches[0]))
140
+ stack.clear
141
+ end
142
+ rescue StandardError => e
143
+ log(e)
144
+ ensure
145
+ stack.clear if stack.strip.include?("}")
146
+ end
147
+
148
+ def log(error)
149
+ logger = Logger.new($stdout)
150
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
151
+ "\033[31mAnthropic Error (spotted in anthropic #{VERSION}): #{msg}\n\033[0m"
152
+ end
153
+ logger.error(error)
154
+ end
155
+
156
+ def handle_faraday_error(chunk, env)
157
+ return unless env&.status != 200
158
+
159
+ raise_error = Faraday::Response::RaiseError.new
160
+ raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
161
+ end
162
+
163
+ def conn(multipart: false)
164
+ connection = Faraday.new do |f|
165
+ f.options[:timeout] = @request_timeout
166
+ f.request(:multipart) if multipart
167
+ f.use MiddlewareErrors if @log_errors
168
+ f.response :raise_error
169
+ f.response :json
170
+ end
171
+
172
+ @faraday_middleware&.call(connection)
173
+
174
+ connection
175
+ end
176
+
177
+ def uri(path:)
178
+ Anthropic.configuration.uri_base + Anthropic.configuration.api_version + path
179
+ end
180
+
181
+ # Unused except by unused method
182
+ def multipart_parameters(parameters)
183
+ parameters&.transform_values do |value|
184
+ next value unless value.is_a?(File)
185
+
186
+ # Doesn't seem like Anthropic needs mime_type yet, so not worth
187
+ # the library to figure this out. Hence the empty string
188
+ # as the second argument.
189
+ Faraday::UploadIO.new(value, "", value.path)
190
+ end
191
+ end
192
+
193
+ def try_parse_json(maybe_json)
194
+ JSON.parse(maybe_json)
195
+ rescue JSON::ParserError
196
+ maybe_json
197
+ end
198
+ end
199
+ end
200
+ # 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
+ # anthropic_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
@@ -0,0 +1,112 @@
1
+ module Anthropic
2
+ module Messages
3
+ class Batches
4
+ BETA_VERSION = "message-batches-2024-09-24".freeze
5
+
6
+ def initialize(client)
7
+ @client = client.beta(BETA_VERSION)
8
+ end
9
+
10
+ # Anthropic API Parameters as of 2024-10-09:
11
+ # @see https://docs.anthropic.com/en/api/creating-message-batches
12
+ #
13
+ # @param [Array] :parameters - List of requests for prompt completion.
14
+ # Each is an individual request to create a Message.
15
+ # Parameters are an array of hashes, each with the following keys:
16
+ # - :custom_id (required): Developer-provided ID created for each request in a Message Batch.
17
+ # Useful for matching results to requests, as results may be given out of request order.
18
+ # Must be unique for each request within the Message Batch.
19
+ # - :params (required): Messages API creation parameters for the individual request.
20
+ # See the Messages API reference for full documentation on available parameters.
21
+ #
22
+ # @returns [Hash] the response from the API (after the streaming is done, if streaming)
23
+ # @example:
24
+ #
25
+ # > messages.batches.create(parameters: [message1, message2])
26
+ # {
27
+ # "id"=>"msgbatch_01668jySCZeCpMLsxFcroNnN",
28
+ # "type"=>"message_batch",
29
+ # "processing_status"=>"in_progress",
30
+ # "request_counts"=>{
31
+ # "processing"=>2,
32
+ # "succeeded"=>0,
33
+ # "errored"=>0,
34
+ # "canceled"=>0,
35
+ # "expired"=>0
36
+ # },
37
+ # "ended_at"=>nil,
38
+ # "created_at"=>"2024-10-09T20:18:11.480471+00:00",
39
+ # "expires_at"=>"2024-10-10T20:18:11.480471+00:00",
40
+ # "cancel_initiated_at"=>nil,
41
+ # "results_url"=>nil
42
+ # }
43
+ def create(parameters: {})
44
+ @client.json_post(
45
+ path: "/messages/batches",
46
+ parameters: parameters
47
+ )
48
+ end
49
+
50
+ # Anthropic API Parameters as of 2024-10-09:
51
+ # @see https://docs.anthropic.com/en/api/creating-message-batches
52
+ #
53
+ # @param [String] :id - ID of the Message Batch.
54
+ #
55
+ # @returns [Hash] the same response shape as #create method.
56
+ def get(id:)
57
+ @client.get(path: "/messages/batches/#{id}")
58
+ end
59
+
60
+ # Anthropic API Parameters as of 2024-10-09:
61
+ # @see https://docs.anthropic.com/en/api/creating-message-batches
62
+ #
63
+ # @param [String] :id - ID of the Message Batch.
64
+ #
65
+ # Returns the results of a Message Batch as a .jsonl file.
66
+ # Each line in the file is a JSON object containing the result of a
67
+ # single request in the Message Batch.
68
+ # Results are not guaranteed to be in the same order as requests.
69
+ # Use the custom_id field to match results to requests.
70
+ def results(id:)
71
+ @client.get(path: "/messages/batches/#{id}/results")
72
+ end
73
+
74
+ # Anthropic API Parameters as of 2024-10-09:
75
+ # @see https://docs.anthropic.com/en/api/listing-message-batches
76
+ #
77
+ # List all Message Batches within a Workspace. Most recently created batches returned first.
78
+ # @param [String] :before_id - ID of the object to use as a cursor for pagination.
79
+ # When provided, returns the page of results immediately before this object.
80
+ #
81
+ # @param [String] :after_id - ID of the object to use as a cursor for pagination.
82
+ # When provided, returns the page of results immediately after this object.
83
+ #
84
+ # @param [Integer] :limit - Number of items to return per page.
85
+ # Defaults to 20. Ranges from 1 to 100.
86
+ #
87
+ # @returns [Hash] the response from the API
88
+ def list(parameters: {})
89
+ @client.get(path: "/messages/batches", parameters: parameters)
90
+ end
91
+
92
+ # Anthropic API Parameters as of 2024-10-09:
93
+ # @see https://docs.anthropic.com/en/api/creating-message-batches
94
+ #
95
+ # @param [String] :id - ID of the Message Batch.
96
+ #
97
+ # @returns [Hash] the response from the API
98
+ #
99
+ # Cancel a Message Batch.
100
+ # Batches may be canceled any time before processing ends. Once cancellation is initiated,
101
+ # the batch enters a canceling state, at which time the system may complete any in-progress,
102
+ # non-interruptible requests before finalizing cancellation.
103
+ #
104
+ # The number of canceled requests is specified in request_counts.
105
+ # To determine which requests were canceled, check the individual results within the batch.
106
+ # Note that cancellation may not cancel any requests if they were non-interruptible.
107
+ def cancel(id:)
108
+ @client.json_post(path: "/messages/batches/#{id}/cancel")
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,13 @@
1
+ module Anthropic
2
+ module Messages
3
+ class Client
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def batches
9
+ @batches ||= Batches.new(@client)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Anthropic
2
+ VERSION = "0.4.2".freeze
3
+ end
data/lib/anthropic.rb ADDED
@@ -0,0 +1,72 @@
1
+ require "faraday"
2
+ require "faraday/multipart"
3
+
4
+ require_relative "anthropic/http"
5
+ require_relative "anthropic/client"
6
+ require_relative "anthropic/version"
7
+
8
+ module Anthropic
9
+ class Error < StandardError; end
10
+ class ConfigurationError < Error; end
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 anthropic #{VERSION}): #{msg}\n\033[0m"
21
+ end
22
+ logger.error(e.response[:body])
23
+
24
+ raise e
25
+ end
26
+ end
27
+
28
+ class Configuration
29
+ attr_writer :access_token
30
+ attr_accessor :anthropic_version,
31
+ :api_version,
32
+ :extra_headers,
33
+ :log_errors,
34
+ :request_timeout,
35
+ :uri_base
36
+
37
+ DEFAULT_API_VERSION = "v1".freeze
38
+ DEFAULT_ANTHROPIC_VERSION = "2023-06-01".freeze
39
+ DEFAULT_LOG_ERRORS = false
40
+ DEFAULT_URI_BASE = "https://api.anthropic.com/".freeze
41
+ DEFAULT_REQUEST_TIMEOUT = 120
42
+
43
+ def initialize
44
+ @access_token = nil
45
+ @api_version = DEFAULT_API_VERSION
46
+ @anthropic_version = DEFAULT_ANTHROPIC_VERSION
47
+ @log_errors = DEFAULT_LOG_ERRORS
48
+ @uri_base = DEFAULT_URI_BASE
49
+ @request_timeout = DEFAULT_REQUEST_TIMEOUT
50
+ @extra_headers = {}
51
+ end
52
+
53
+ def access_token
54
+ return @access_token if @access_token
55
+
56
+ error_text = "Anthropic access token missing! See https://github.com/alexrudall/ruby-anthropic#usage"
57
+ raise ConfigurationError, error_text
58
+ end
59
+ end
60
+
61
+ class << self
62
+ attr_writer :configuration
63
+ end
64
+
65
+ def self.configuration
66
+ @configuration ||= Anthropic::Configuration.new
67
+ end
68
+
69
+ def self.configure
70
+ yield(configuration)
71
+ end
72
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "../anthropic"
2
+ require_relative "../anthropic/compatibility"
@@ -0,0 +1,5 @@
1
+ ## All Submissions:
2
+
3
+ * [ ] Have you followed the guidelines in our [Contributing document](../blob/main/CONTRIBUTING.md)?
4
+ * [ ] Have you checked to ensure there aren't other open [Pull Requests](../pulls) for the same update/change?
5
+ * [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-anthropic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.2
5
+ platform: ruby
6
+ authors:
7
+ - Alex
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-04-10 00:00:00.000000000 Z
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
33
+ - !ruby/object:Gem::Dependency
34
+ name: faraday
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: faraday-multipart
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '1'
61
+ description:
62
+ email:
63
+ - alexrudall@users.noreply.github.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - ".circleci/config.yml"
69
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
70
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
71
+ - ".github/dependabot.yml"
72
+ - ".gitignore"
73
+ - ".rspec"
74
+ - ".rubocop.yml"
75
+ - CHANGELOG.md
76
+ - CODE_OF_CONDUCT.md
77
+ - CONTRIBUTING.md
78
+ - Gemfile
79
+ - Gemfile.lock
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - anthropic.gemspec
84
+ - bin/console
85
+ - bin/setup
86
+ - lib/anthropic.rb
87
+ - lib/anthropic/client.rb
88
+ - lib/anthropic/compatibility.rb
89
+ - lib/anthropic/http.rb
90
+ - lib/anthropic/http_headers.rb
91
+ - lib/anthropic/messages/batches.rb
92
+ - lib/anthropic/messages/client.rb
93
+ - lib/anthropic/version.rb
94
+ - lib/ruby/anthropic.rb
95
+ - pull_request_template.md
96
+ homepage: https://github.com/alexrudall/ruby-anthropic
97
+ licenses:
98
+ - MIT
99
+ metadata:
100
+ homepage_uri: https://github.com/alexrudall/ruby-anthropic
101
+ source_code_uri: https://github.com/alexrudall/ruby-anthropic
102
+ changelog_uri: https://github.com/alexrudall/ruby-anthropic/blob/main/CHANGELOG.md
103
+ rubygems_mfa_required: 'true'
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 2.6.0
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubygems_version: 3.5.11
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: "Anthropic API + Ruby! \U0001F916\U0001F30C"
123
+ test_files: []