llm.rb 0.8.0 → 0.9.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/README.md +62 -48
- data/lib/llm/{chat → bot}/builder.rb +1 -1
- data/lib/llm/bot/conversable.rb +31 -0
- data/lib/llm/{chat → bot}/prompt/completion.rb +14 -4
- data/lib/llm/{chat → bot}/prompt/respond.rb +16 -5
- data/lib/llm/{chat.rb → bot.rb} +48 -66
- data/lib/llm/error.rb +22 -22
- data/lib/llm/event_handler.rb +44 -0
- data/lib/llm/eventstream/event.rb +69 -0
- data/lib/llm/eventstream/parser.rb +88 -0
- data/lib/llm/eventstream.rb +8 -0
- data/lib/llm/function.rb +9 -12
- data/lib/llm/object/builder.rb +8 -9
- data/lib/llm/object/kernel.rb +1 -1
- data/lib/llm/object.rb +7 -1
- data/lib/llm/provider.rb +61 -26
- data/lib/llm/providers/anthropic/error_handler.rb +3 -3
- data/lib/llm/providers/anthropic/models.rb +3 -7
- data/lib/llm/providers/anthropic/response_parser/completion_parser.rb +3 -3
- data/lib/llm/providers/anthropic/response_parser.rb +1 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +66 -0
- data/lib/llm/providers/anthropic.rb +9 -4
- data/lib/llm/providers/gemini/error_handler.rb +4 -4
- data/lib/llm/providers/gemini/files.rb +12 -15
- data/lib/llm/providers/gemini/images.rb +4 -8
- data/lib/llm/providers/gemini/models.rb +3 -7
- data/lib/llm/providers/gemini/stream_parser.rb +69 -0
- data/lib/llm/providers/gemini.rb +19 -11
- data/lib/llm/providers/ollama/error_handler.rb +3 -3
- data/lib/llm/providers/ollama/format/completion_format.rb +1 -1
- data/lib/llm/providers/ollama/models.rb +3 -7
- data/lib/llm/providers/ollama/stream_parser.rb +44 -0
- data/lib/llm/providers/ollama.rb +13 -6
- data/lib/llm/providers/openai/audio.rb +5 -9
- data/lib/llm/providers/openai/error_handler.rb +3 -3
- data/lib/llm/providers/openai/files.rb +12 -15
- data/lib/llm/providers/openai/images.rb +8 -11
- data/lib/llm/providers/openai/models.rb +3 -7
- data/lib/llm/providers/openai/moderations.rb +3 -7
- data/lib/llm/providers/openai/response_parser/completion_parser.rb +3 -3
- data/lib/llm/providers/openai/response_parser.rb +3 -0
- data/lib/llm/providers/openai/responses.rb +10 -12
- data/lib/llm/providers/openai/stream_parser.rb +77 -0
- data/lib/llm/providers/openai.rb +11 -7
- data/lib/llm/providers/voyageai/error_handler.rb +3 -3
- data/lib/llm/providers/voyageai.rb +1 -1
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +4 -2
- data/llm.gemspec +1 -1
- metadata +30 -25
- data/lib/llm/chat/conversable.rb +0 -53
- /data/lib/{json → llm/json}/schema/array.rb +0 -0
- /data/lib/{json → llm/json}/schema/boolean.rb +0 -0
- /data/lib/{json → llm/json}/schema/integer.rb +0 -0
- /data/lib/{json → llm/json}/schema/leaf.rb +0 -0
- /data/lib/{json → llm/json}/schema/null.rb +0 -0
- /data/lib/{json → llm/json}/schema/number.rb +0 -0
- /data/lib/{json → llm/json}/schema/object.rb +0 -0
- /data/lib/{json → llm/json}/schema/string.rb +0 -0
- /data/lib/{json → llm/json}/schema/version.rb +0 -0
- /data/lib/{json → llm/json}/schema.rb +0 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::EventStream
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
class Event
|
7
|
+
FIELD_REGEXP = /[^:]+/
|
8
|
+
VALUE_REGEXP = /(?<=: ).+/
|
9
|
+
|
10
|
+
##
|
11
|
+
# Returns the field name
|
12
|
+
# @return [Symbol]
|
13
|
+
attr_reader :field
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns the field value
|
17
|
+
# @return [String]
|
18
|
+
attr_reader :value
|
19
|
+
|
20
|
+
##
|
21
|
+
# Returns the full chunk
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :chunk
|
24
|
+
|
25
|
+
##
|
26
|
+
# @param [String] chunk
|
27
|
+
# @return [LLM::EventStream::Event]
|
28
|
+
def initialize(chunk)
|
29
|
+
@field = chunk[FIELD_REGEXP]
|
30
|
+
@value = chunk[VALUE_REGEXP]
|
31
|
+
@chunk = chunk
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Returns true when the event represents an "id" chunk
|
36
|
+
# @return [Boolean]
|
37
|
+
def id?
|
38
|
+
@field == "id"
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Returns true when the event represents a "data" chunk
|
43
|
+
# @return [Boolean]
|
44
|
+
def data?
|
45
|
+
@field == "data"
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Returns true when the event represents an "event" chunk
|
50
|
+
# @return [Boolean]
|
51
|
+
def event?
|
52
|
+
@field == "event"
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Returns true when the event represents a "retry" chunk
|
57
|
+
# @return [Boolean]
|
58
|
+
def retry?
|
59
|
+
@field == "retry"
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Returns true when a chunk represents the end of the stream
|
64
|
+
# @return [Boolean]
|
65
|
+
def end?
|
66
|
+
@value == "[DONE]"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM::EventStream
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
class Parser
|
7
|
+
##
|
8
|
+
# @return [LLM::EventStream::Parser]
|
9
|
+
def initialize
|
10
|
+
@buffer = StringIO.new
|
11
|
+
@events = Hash.new { |h, k| h[k] = [] }
|
12
|
+
@offset = 0
|
13
|
+
@visitors = []
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Register a visitor
|
18
|
+
# @param [#on_data] visitor
|
19
|
+
# @return [void]
|
20
|
+
def register(visitor)
|
21
|
+
@visitors << visitor
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Subscribe to an event
|
26
|
+
# @param [Symbol] evtname
|
27
|
+
# @param [Proc] block
|
28
|
+
# @return [void]
|
29
|
+
def on(evtname, &block)
|
30
|
+
@events[evtname.to_s] << block
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Append an event to the internal buffer
|
35
|
+
# @return [void]
|
36
|
+
def <<(event)
|
37
|
+
io = StringIO.new(event)
|
38
|
+
IO.copy_stream io, @buffer
|
39
|
+
each_line { parse!(_1) }
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Returns the internal buffer
|
44
|
+
# @return [String]
|
45
|
+
def body
|
46
|
+
@buffer.string
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Free the internal buffer
|
51
|
+
# @return [void]
|
52
|
+
def free
|
53
|
+
@buffer.truncate(0)
|
54
|
+
@buffer.rewind
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def parse!(event)
|
60
|
+
event = Event.new(event)
|
61
|
+
dispatch(event)
|
62
|
+
end
|
63
|
+
|
64
|
+
def dispatch(event)
|
65
|
+
@visitors.each { dispatch_visitor(_1, event) }
|
66
|
+
@events[event.field].each { _1.call(event) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def dispatch_visitor(visitor, event)
|
70
|
+
method = "on_#{event.field}"
|
71
|
+
if visitor.respond_to?(method)
|
72
|
+
visitor.public_send(method, event)
|
73
|
+
elsif visitor.respond_to?("on_chunk")
|
74
|
+
visitor.on_chunk(event)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def each_line
|
79
|
+
string.each_line.with_index do
|
80
|
+
next if _2 < @offset
|
81
|
+
yield(_1)
|
82
|
+
@offset += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def string = @buffer.string
|
87
|
+
end
|
88
|
+
end
|
data/lib/llm/function.rb
CHANGED
@@ -1,32 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
##
|
4
|
-
# The {LLM::Function LLM::Function} class represents a
|
5
|
-
# be called by an LLM.
|
6
|
-
# or a Class-based function.
|
4
|
+
# The {LLM::Function LLM::Function} class represents a
|
5
|
+
# local function that can be called by an LLM.
|
7
6
|
#
|
8
|
-
# @example
|
9
|
-
# # Proc-based
|
7
|
+
# @example example #1
|
10
8
|
# LLM.function(:system) do |fn|
|
11
|
-
# fn.description "Runs system commands
|
9
|
+
# fn.description "Runs system commands"
|
12
10
|
# fn.params do |schema|
|
13
11
|
# schema.object(command: schema.string.required)
|
14
12
|
# end
|
15
13
|
# fn.define do |params|
|
16
|
-
# Kernel.system(params.command)
|
14
|
+
# {success: Kernel.system(params.command)}
|
17
15
|
# end
|
18
16
|
# end
|
19
17
|
#
|
20
|
-
# @example
|
21
|
-
# # Class-based
|
18
|
+
# @example example #2
|
22
19
|
# class System
|
23
20
|
# def call(params)
|
24
|
-
# Kernel.system(params.command)
|
21
|
+
# {success: Kernel.system(params.command)}
|
25
22
|
# end
|
26
23
|
# end
|
27
24
|
#
|
28
25
|
# LLM.function(:system) do |fn|
|
29
|
-
# fn.description "Runs system commands
|
26
|
+
# fn.description "Runs system commands"
|
30
27
|
# fn.params do |schema|
|
31
28
|
# schema.object(command: schema.string.required)
|
32
29
|
# end
|
@@ -99,7 +96,7 @@ class LLM::Function
|
|
99
96
|
# Returns a value that communicates that the function call was cancelled
|
100
97
|
# @example
|
101
98
|
# llm = LLM.openai(key: ENV["KEY"])
|
102
|
-
# bot = LLM::
|
99
|
+
# bot = LLM::Bot.new(llm, tools: [fn1, fn2])
|
103
100
|
# bot.chat "I want to run the functions"
|
104
101
|
# bot.chat bot.functions.map(&:cancel)
|
105
102
|
# @return [LLM::Function::Return]
|
data/lib/llm/object/builder.rb
CHANGED
@@ -9,7 +9,7 @@ class LLM::Object
|
|
9
9
|
# obj = LLM::Object.from_hash(person: {name: 'John'})
|
10
10
|
# obj.person.name # => 'John'
|
11
11
|
# obj.person.class # => LLM::Object
|
12
|
-
# @param [Hash, Array] obj
|
12
|
+
# @param [Hash, LLM::Object, Array] obj
|
13
13
|
# A Hash object
|
14
14
|
# @return [LLM::Object]
|
15
15
|
# An LLM::Object object initialized by visiting `obj` with recursion
|
@@ -19,20 +19,19 @@ class LLM::Object
|
|
19
19
|
when Array then obj.map { |v| from_hash(v) }
|
20
20
|
else
|
21
21
|
visited = {}
|
22
|
-
obj.each { visited[_1] =
|
22
|
+
obj.each { visited[_1] = visit(_2) }
|
23
23
|
new(visited)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
28
28
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
else
|
35
|
-
value
|
29
|
+
def visit(value)
|
30
|
+
case value
|
31
|
+
when self then from_hash(value.to_h)
|
32
|
+
when Hash then from_hash(value)
|
33
|
+
when Array then value.map { |v| visit(v) }
|
34
|
+
else value
|
36
35
|
end
|
37
36
|
end
|
38
37
|
end
|
data/lib/llm/object/kernel.rb
CHANGED
data/lib/llm/object.rb
CHANGED
@@ -17,7 +17,7 @@ class LLM::Object < BasicObject
|
|
17
17
|
##
|
18
18
|
# @param [Hash] h
|
19
19
|
# @return [LLM::Object]
|
20
|
-
def initialize(h)
|
20
|
+
def initialize(h = {})
|
21
21
|
@h = h.transform_keys(&:to_sym) || h
|
22
22
|
end
|
23
23
|
|
@@ -51,6 +51,12 @@ class LLM::Object < BasicObject
|
|
51
51
|
to_h.to_json(...)
|
52
52
|
end
|
53
53
|
|
54
|
+
##
|
55
|
+
# @return [Boolean]
|
56
|
+
def empty?
|
57
|
+
@h.empty?
|
58
|
+
end
|
59
|
+
|
54
60
|
##
|
55
61
|
# @return [Hash]
|
56
62
|
def to_h
|
data/lib/llm/provider.rb
CHANGED
@@ -21,10 +21,9 @@ class LLM::Provider
|
|
21
21
|
# Whether to use SSL for the connection
|
22
22
|
def initialize(key:, host:, port: 443, timeout: 60, ssl: true)
|
23
23
|
@key = key
|
24
|
-
@
|
25
|
-
|
26
|
-
|
27
|
-
end
|
24
|
+
@client = Net::HTTP.new(host, port)
|
25
|
+
@client.use_ssl = ssl
|
26
|
+
@client.read_timeout = timeout
|
28
27
|
end
|
29
28
|
|
30
29
|
##
|
@@ -78,55 +77,55 @@ class LLM::Provider
|
|
78
77
|
# Starts a new lazy chat powered by the chat completions API
|
79
78
|
# @note
|
80
79
|
# This method creates a lazy version of a
|
81
|
-
# {LLM::
|
80
|
+
# {LLM::Bot LLM::Bot} object.
|
82
81
|
# @param prompt (see LLM::Provider#complete)
|
83
82
|
# @param params (see LLM::Provider#complete)
|
84
|
-
# @return [LLM::
|
83
|
+
# @return [LLM::Bot]
|
85
84
|
def chat(prompt, params = {})
|
86
85
|
role = params.delete(:role)
|
87
|
-
LLM::
|
86
|
+
LLM::Bot.new(self, params).chat(prompt, role:)
|
88
87
|
end
|
89
88
|
|
90
89
|
##
|
91
90
|
# Starts a new chat powered by the chat completions API
|
92
91
|
# @note
|
93
92
|
# This method creates a non-lazy version of a
|
94
|
-
# {LLM::
|
93
|
+
# {LLM::Bot LLM::Bot} object.
|
95
94
|
# @param prompt (see LLM::Provider#complete)
|
96
95
|
# @param params (see LLM::Provider#complete)
|
97
96
|
# @raise (see LLM::Provider#complete)
|
98
|
-
# @return [LLM::
|
97
|
+
# @return [LLM::Bot]
|
99
98
|
def chat!(prompt, params = {})
|
100
99
|
role = params.delete(:role)
|
101
|
-
LLM::
|
100
|
+
LLM::Bot.new(self, params).chat(prompt, role:)
|
102
101
|
end
|
103
102
|
|
104
103
|
##
|
105
104
|
# Starts a new lazy chat powered by the responses API
|
106
105
|
# @note
|
107
106
|
# This method creates a lazy variant of a
|
108
|
-
# {LLM::
|
107
|
+
# {LLM::Bot LLM::Bot} object.
|
109
108
|
# @param prompt (see LLM::Provider#complete)
|
110
109
|
# @param params (see LLM::Provider#complete)
|
111
110
|
# @raise (see LLM::Provider#complete)
|
112
|
-
# @return [LLM::
|
111
|
+
# @return [LLM::Bot]
|
113
112
|
def respond(prompt, params = {})
|
114
113
|
role = params.delete(:role)
|
115
|
-
LLM::
|
114
|
+
LLM::Bot.new(self, params).respond(prompt, role:)
|
116
115
|
end
|
117
116
|
|
118
117
|
##
|
119
118
|
# Starts a new chat powered by the responses API
|
120
119
|
# @note
|
121
120
|
# This method creates a non-lazy variant of a
|
122
|
-
# {LLM::
|
121
|
+
# {LLM::Bot LLM::Bot} object.
|
123
122
|
# @param prompt (see LLM::Provider#complete)
|
124
123
|
# @param params (see LLM::Provider#complete)
|
125
124
|
# @raise (see LLM::Provider#complete)
|
126
|
-
# @return [LLM::
|
125
|
+
# @return [LLM::Bot]
|
127
126
|
def respond!(prompt, params = {})
|
128
127
|
role = params.delete(:role)
|
129
|
-
LLM::
|
128
|
+
LLM::Bot.new(self, params).respond(prompt, role:)
|
130
129
|
end
|
131
130
|
|
132
131
|
##
|
@@ -194,10 +193,7 @@ class LLM::Provider
|
|
194
193
|
# Returns an object that can generate a JSON schema
|
195
194
|
# @return [JSON::Schema]
|
196
195
|
def schema
|
197
|
-
@schema ||=
|
198
|
-
require_relative "../json/schema"
|
199
|
-
JSON::Schema.new
|
200
|
-
end
|
196
|
+
@schema ||= JSON::Schema.new
|
201
197
|
end
|
202
198
|
|
203
199
|
##
|
@@ -216,6 +212,8 @@ class LLM::Provider
|
|
216
212
|
|
217
213
|
private
|
218
214
|
|
215
|
+
attr_reader :client
|
216
|
+
|
219
217
|
##
|
220
218
|
# The headers to include with a request
|
221
219
|
# @raise [NotImplementedError]
|
@@ -243,10 +241,21 @@ class LLM::Provider
|
|
243
241
|
end
|
244
242
|
|
245
243
|
##
|
246
|
-
#
|
247
|
-
|
248
|
-
|
249
|
-
|
244
|
+
# @return [Class]
|
245
|
+
def event_handler
|
246
|
+
LLM::EventHandler
|
247
|
+
end
|
248
|
+
|
249
|
+
##
|
250
|
+
# @return [Class]
|
251
|
+
# Returns the provider-specific Server-Side Events (SSE) parser
|
252
|
+
def stream_parser
|
253
|
+
raise NotImplementedError
|
254
|
+
end
|
255
|
+
|
256
|
+
##
|
257
|
+
# Executes a HTTP request
|
258
|
+
# @param [Net::HTTPRequest] request
|
250
259
|
# The request to send
|
251
260
|
# @param [Proc] b
|
252
261
|
# A block to yield the response to (optional)
|
@@ -260,8 +269,34 @@ class LLM::Provider
|
|
260
269
|
# When any other unsuccessful status code is returned
|
261
270
|
# @raise [SystemCallError]
|
262
271
|
# When there is a network error at the operating system level
|
263
|
-
|
264
|
-
|
272
|
+
# @return [Net::HTTPResponse]
|
273
|
+
def execute(request:, stream: nil, &b)
|
274
|
+
res = if stream
|
275
|
+
client.request(request) do |res|
|
276
|
+
handler = event_handler.new stream_parser.new(stream)
|
277
|
+
parser = LLM::EventStream::Parser.new
|
278
|
+
parser.register(handler)
|
279
|
+
res.read_body(parser)
|
280
|
+
# If the handler body is empty, it means the
|
281
|
+
# response was most likely not streamed or
|
282
|
+
# parsing has failed. In that case, we fallback
|
283
|
+
# on the original response body.
|
284
|
+
res.body = handler.body.empty? ? parser.body.dup : handler.body
|
285
|
+
ensure
|
286
|
+
parser&.free
|
287
|
+
end
|
288
|
+
else
|
289
|
+
client.request(request, &b)
|
290
|
+
end
|
291
|
+
handle_response(res)
|
292
|
+
end
|
293
|
+
|
294
|
+
##
|
295
|
+
# Handles the response from a request
|
296
|
+
# @param [Net::HTTPResponse] res
|
297
|
+
# The response to handle
|
298
|
+
# @return [Net::HTTPResponse]
|
299
|
+
def handle_response(res)
|
265
300
|
case res
|
266
301
|
when Net::HTTPOK then res
|
267
302
|
else error_handler.new(res).raise_error!
|
@@ -23,11 +23,11 @@ class LLM::Anthropic
|
|
23
23
|
def raise_error!
|
24
24
|
case res
|
25
25
|
when Net::HTTPUnauthorized
|
26
|
-
raise LLM::
|
26
|
+
raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
|
27
27
|
when Net::HTTPTooManyRequests
|
28
|
-
raise LLM::
|
28
|
+
raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
|
29
29
|
else
|
30
|
-
raise LLM::
|
30
|
+
raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -40,7 +40,7 @@ class LLM::Anthropic
|
|
40
40
|
def all(**params)
|
41
41
|
query = URI.encode_www_form(params)
|
42
42
|
req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
|
43
|
-
res = request
|
43
|
+
res = execute(request: req)
|
44
44
|
LLM::Response::ModelList.new(res).tap { |modellist|
|
45
45
|
models = modellist.body["data"].map do |model|
|
46
46
|
LLM::Model.from_hash(model).tap { _1.provider = @provider }
|
@@ -51,12 +51,8 @@ class LLM::Anthropic
|
|
51
51
|
|
52
52
|
private
|
53
53
|
|
54
|
-
|
55
|
-
@provider.
|
56
|
-
end
|
57
|
-
|
58
|
-
[:headers, :request].each do |m|
|
59
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
54
|
+
[:headers, :execute].each do |m|
|
55
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
60
56
|
end
|
61
57
|
end
|
62
58
|
end
|
@@ -41,9 +41,9 @@ module LLM::Anthropic::ResponseParser
|
|
41
41
|
def body = @body
|
42
42
|
def role = body.role
|
43
43
|
def model = body.model
|
44
|
-
def prompt_tokens = body.usage
|
45
|
-
def completion_tokens = body.usage
|
46
|
-
def total_tokens = body.usage
|
44
|
+
def prompt_tokens = body.usage&.input_tokens
|
45
|
+
def completion_tokens = body.usage&.output_tokens
|
46
|
+
def total_tokens = body.usage&.total_tokens
|
47
47
|
def parts = body.content
|
48
48
|
def texts = parts.select { _1["type"] == "text" }
|
49
49
|
def tools = parts.select { _1["type"] == "tool_use" }
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Anthropic
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
class StreamParser
|
7
|
+
##
|
8
|
+
# Returns the fully constructed response body
|
9
|
+
# @return [LLM::Object]
|
10
|
+
attr_reader :body
|
11
|
+
|
12
|
+
##
|
13
|
+
# @param [#<<] io An IO-like object
|
14
|
+
# @return [LLM::Anthropic::StreamParser]
|
15
|
+
def initialize(io)
|
16
|
+
@body = LLM::Object.new(role: "assistant", content: [])
|
17
|
+
@io = io
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# @param [Hash] chunk
|
22
|
+
# @return [LLM::Anthropic::StreamParser]
|
23
|
+
def parse!(chunk)
|
24
|
+
tap { merge!(chunk) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def merge!(chunk)
|
30
|
+
if chunk["type"] == "message_start"
|
31
|
+
merge_message!(chunk["message"])
|
32
|
+
elsif chunk["type"] == "content_block_start"
|
33
|
+
@body["content"][chunk["index"]] = chunk["content_block"]
|
34
|
+
elsif chunk["type"] == "content_block_delta"
|
35
|
+
if chunk["delta"]["type"] == "text_delta"
|
36
|
+
@body.content[chunk["index"]]["text"] << chunk["delta"]["text"]
|
37
|
+
@io << chunk["delta"]["text"] if @io.respond_to?(:<<)
|
38
|
+
elsif chunk["delta"]["type"] == "input_json_delta"
|
39
|
+
content = @body.content[chunk["index"]]
|
40
|
+
if Hash === content["input"]
|
41
|
+
content["input"] = chunk["delta"]["partial_json"]
|
42
|
+
else
|
43
|
+
content["input"] << chunk["delta"]["partial_json"]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
elsif chunk["type"] == "message_delta"
|
47
|
+
merge_message!(chunk["delta"])
|
48
|
+
elsif chunk["type"] == "content_block_stop"
|
49
|
+
content = @body.content[chunk["index"]]
|
50
|
+
if content["input"]
|
51
|
+
content["input"] = JSON.parse(content["input"])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def merge_message!(message)
|
57
|
+
message.each do |key, value|
|
58
|
+
@body[key] = if value.respond_to?(:each_pair)
|
59
|
+
merge_message!(value)
|
60
|
+
else
|
61
|
+
value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -5,10 +5,10 @@ module LLM
|
|
5
5
|
# The Anthropic class implements a provider for
|
6
6
|
# [Anthropic](https://www.anthropic.com)
|
7
7
|
class Anthropic < Provider
|
8
|
+
require_relative "anthropic/format"
|
8
9
|
require_relative "anthropic/error_handler"
|
10
|
+
require_relative "anthropic/stream_parser"
|
9
11
|
require_relative "anthropic/response_parser"
|
10
|
-
require_relative "anthropic/response_parser/completion_parser"
|
11
|
-
require_relative "anthropic/format"
|
12
12
|
require_relative "anthropic/models"
|
13
13
|
include Format
|
14
14
|
|
@@ -50,12 +50,13 @@ module LLM
|
|
50
50
|
def complete(prompt, params = {})
|
51
51
|
params = {role: :user, model: default_model, max_tokens: 1024}.merge!(params)
|
52
52
|
params = [params, format_tools(params)].inject({}, &:merge!).compact
|
53
|
-
role = params.delete(:role)
|
53
|
+
role, stream = params.delete(:role), params.delete(:stream)
|
54
|
+
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
54
55
|
req = Net::HTTP::Post.new("/v1/messages", headers)
|
55
56
|
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
56
57
|
body = JSON.dump({messages: [format(messages)].flatten}.merge!(params))
|
57
58
|
set_body_stream(req, StringIO.new(body))
|
58
|
-
res = request
|
59
|
+
res = execute(request: req, stream:)
|
59
60
|
Response::Completion.new(res).extend(response_parser)
|
60
61
|
end
|
61
62
|
|
@@ -95,6 +96,10 @@ module LLM
|
|
95
96
|
LLM::Anthropic::ResponseParser
|
96
97
|
end
|
97
98
|
|
99
|
+
def stream_parser
|
100
|
+
LLM::Anthropic::StreamParser
|
101
|
+
end
|
102
|
+
|
98
103
|
def error_handler
|
99
104
|
LLM::Anthropic::ErrorHandler
|
100
105
|
end
|
@@ -25,14 +25,14 @@ class LLM::Gemini
|
|
25
25
|
when Net::HTTPBadRequest
|
26
26
|
reason = body.dig("error", "details", 0, "reason")
|
27
27
|
if reason == "API_KEY_INVALID"
|
28
|
-
raise LLM::
|
28
|
+
raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
|
29
29
|
else
|
30
|
-
raise LLM::
|
30
|
+
raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
|
31
31
|
end
|
32
32
|
when Net::HTTPTooManyRequests
|
33
|
-
raise LLM::
|
33
|
+
raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
|
34
34
|
else
|
35
|
-
raise LLM::
|
35
|
+
raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|