llm.rb 9.0.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +182 -4
- data/README.md +194 -42
- data/data/anthropic.json +278 -258
- data/data/bedrock.json +1288 -1561
- data/data/deepseek.json +38 -38
- data/data/google.json +656 -579
- data/data/openai.json +860 -818
- data/data/xai.json +243 -552
- data/data/zai.json +168 -168
- data/lib/llm/a2a/card/capabilities.rb +41 -0
- data/lib/llm/a2a/card/interface.rb +34 -0
- data/lib/llm/a2a/card/provider.rb +27 -0
- data/lib/llm/a2a/card/skill.rb +68 -0
- data/lib/llm/a2a/card.rb +144 -0
- data/lib/llm/a2a/error.rb +49 -0
- data/lib/llm/a2a/notifications.rb +53 -0
- data/lib/llm/a2a/tasks.rb +55 -0
- data/lib/llm/a2a/transport/http.rb +131 -0
- data/lib/llm/a2a.rb +452 -0
- data/lib/llm/active_record/acts_as_agent.rb +20 -9
- data/lib/llm/active_record/acts_as_llm.rb +4 -4
- data/lib/llm/active_record.rb +1 -6
- data/lib/llm/agent.rb +96 -71
- data/lib/llm/buffer.rb +1 -2
- data/lib/llm/context.rb +77 -50
- data/lib/llm/file.rb +7 -0
- data/lib/llm/function/call_task.rb +46 -0
- data/lib/llm/function.rb +28 -2
- data/lib/llm/mcp/transport/http.rb +5 -18
- data/lib/llm/mcp/transport/stdio.rb +7 -0
- data/lib/llm/mcp.rb +20 -17
- data/lib/llm/message.rb +1 -1
- data/lib/llm/object/kernel.rb +1 -1
- data/lib/llm/provider.rb +9 -9
- data/lib/llm/providers/anthropic/stream_parser.rb +2 -2
- data/lib/llm/providers/bedrock/stream_parser.rb +2 -2
- data/lib/llm/providers/google/stream_parser.rb +2 -2
- data/lib/llm/providers/openai/responses/stream_parser.rb +2 -2
- data/lib/llm/providers/openai/stream_parser.rb +2 -2
- data/lib/llm/response.rb +1 -1
- data/lib/llm/schema.rb +11 -0
- data/lib/llm/sequel/agent.rb +19 -9
- data/lib/llm/sequel/plugin.rb +9 -13
- data/lib/llm/stream.rb +11 -36
- data/lib/llm/tool/param.rb +1 -8
- data/lib/llm/tool.rb +57 -27
- data/lib/llm/tracer.rb +1 -1
- data/lib/llm/transport/http.rb +1 -1
- data/lib/llm/transport/stream_decoder.rb +6 -3
- data/lib/llm/transport/utils.rb +35 -0
- data/lib/llm/transport.rb +1 -0
- data/lib/llm/utils.rb +73 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +24 -4
- data/llm.gemspec +16 -1
- metadata +29 -5
- data/lib/llm/bot.rb +0 -3
- data/lib/llm/mcp/transport/http/event_handler.rb +0 -68
data/lib/llm/mcp.rb
CHANGED
|
@@ -26,29 +26,23 @@ class LLM::MCP
|
|
|
26
26
|
|
|
27
27
|
##
|
|
28
28
|
# Builds an MCP client that uses the stdio transport.
|
|
29
|
-
# @param [LLM::Provider, nil] llm
|
|
30
|
-
# An instance of LLM::Provider. Optional.
|
|
31
29
|
# @param [Hash] stdio
|
|
32
30
|
# The stdio transport configuration
|
|
33
31
|
# @return [LLM::MCP]
|
|
34
|
-
def self.stdio(
|
|
35
|
-
new(
|
|
32
|
+
def self.stdio(**stdio)
|
|
33
|
+
new(stdio:)
|
|
36
34
|
end
|
|
37
35
|
|
|
38
36
|
##
|
|
39
37
|
# Builds an MCP client that uses the HTTP transport.
|
|
40
|
-
# @param [LLM::Provider, nil] llm
|
|
41
|
-
# An instance of LLM::Provider. Optional.
|
|
42
38
|
# @param [Hash] http
|
|
43
39
|
# The HTTP transport configuration
|
|
44
40
|
# @return [LLM::MCP]
|
|
45
|
-
def self.http(
|
|
46
|
-
new(
|
|
41
|
+
def self.http(**http)
|
|
42
|
+
new(http:)
|
|
47
43
|
end
|
|
48
44
|
|
|
49
45
|
##
|
|
50
|
-
# @param [LLM::Provider, nil] llm
|
|
51
|
-
# The provider to use for MCP transports that need one
|
|
52
46
|
# @param [Hash, nil] stdio The configuration for the stdio transport
|
|
53
47
|
# @option stdio [Array<String>] :argv
|
|
54
48
|
# The command to run for the MCP process
|
|
@@ -67,8 +61,7 @@ class LLM::MCP
|
|
|
67
61
|
# @param [Integer] timeout
|
|
68
62
|
# The maximum amount of time to wait when reading from an MCP process
|
|
69
63
|
# @return [LLM::MCP] A new MCP instance
|
|
70
|
-
def initialize(
|
|
71
|
-
@llm = llm
|
|
64
|
+
def initialize(stdio: nil, http: nil, timeout: 30)
|
|
72
65
|
@timeout = timeout
|
|
73
66
|
if stdio && http
|
|
74
67
|
raise ArgumentError, "stdio and http are mutually exclusive"
|
|
@@ -116,12 +109,13 @@ class LLM::MCP
|
|
|
116
109
|
ensure
|
|
117
110
|
stop
|
|
118
111
|
end
|
|
112
|
+
alias_method :session, :run
|
|
119
113
|
|
|
120
114
|
##
|
|
121
115
|
# Returns the tools provided by the MCP process.
|
|
122
116
|
# @return [Array<Class<LLM::Tool>>]
|
|
123
117
|
def tools
|
|
124
|
-
res = call(transport, "tools/list")
|
|
118
|
+
res = with_session { call(transport, "tools/list") }
|
|
125
119
|
res["tools"].map { LLM::Tool.mcp(self, _1) }
|
|
126
120
|
end
|
|
127
121
|
|
|
@@ -129,7 +123,7 @@ class LLM::MCP
|
|
|
129
123
|
# Returns the prompts provided by the MCP process.
|
|
130
124
|
# @return [Array<LLM::Object>]
|
|
131
125
|
def prompts
|
|
132
|
-
res = call(transport, "prompts/list")
|
|
126
|
+
res = with_session { call(transport, "prompts/list") }
|
|
133
127
|
LLM::Object.from(res["prompts"])
|
|
134
128
|
end
|
|
135
129
|
|
|
@@ -141,7 +135,7 @@ class LLM::MCP
|
|
|
141
135
|
def find_prompt(name:, arguments: nil)
|
|
142
136
|
params = {name:}
|
|
143
137
|
params[:arguments] = arguments if arguments
|
|
144
|
-
res = call(transport, "prompts/get", params)
|
|
138
|
+
res = with_session { call(transport, "prompts/get", params) }
|
|
145
139
|
res["messages"] = [*res["messages"]].map do |message|
|
|
146
140
|
LLM::Message.new(
|
|
147
141
|
message["role"],
|
|
@@ -159,13 +153,22 @@ class LLM::MCP
|
|
|
159
153
|
# @param [Hash] arguments The arguments to pass to the tool
|
|
160
154
|
# @return [Object] The result of the tool call
|
|
161
155
|
def call_tool(name, arguments = {})
|
|
162
|
-
res = call(transport, "tools/call", {name:, arguments:})
|
|
156
|
+
res = with_session { call(transport, "tools/call", {name:, arguments:}) }
|
|
163
157
|
adapt_tool_result(res)
|
|
164
158
|
end
|
|
165
159
|
|
|
166
160
|
private
|
|
167
161
|
|
|
168
|
-
attr_reader :
|
|
162
|
+
attr_reader :command, :transport, :timeout
|
|
163
|
+
|
|
164
|
+
def with_session
|
|
165
|
+
return yield if transport.running?
|
|
166
|
+
session_started = true
|
|
167
|
+
start
|
|
168
|
+
yield
|
|
169
|
+
ensure
|
|
170
|
+
stop if session_started
|
|
171
|
+
end
|
|
169
172
|
|
|
170
173
|
def adapt_content(content)
|
|
171
174
|
case content
|
data/lib/llm/message.rb
CHANGED
|
@@ -205,7 +205,7 @@ module LLM
|
|
|
205
205
|
# Returns a string representation of the message
|
|
206
206
|
# @return [String]
|
|
207
207
|
def inspect
|
|
208
|
-
"#<#{
|
|
208
|
+
"#<#{LLM::Utils.object_id(self)} " \
|
|
209
209
|
"tool_call=#{tool_calls.any?} role=#{role.inspect} " \
|
|
210
210
|
"content=#{content.inspect} reasoning_content=#{reasoning_content.inspect}>"
|
|
211
211
|
end
|
data/lib/llm/object/kernel.rb
CHANGED
data/lib/llm/provider.rb
CHANGED
|
@@ -32,7 +32,7 @@ class LLM::Provider
|
|
|
32
32
|
@port = port
|
|
33
33
|
@timeout = timeout
|
|
34
34
|
@ssl = ssl
|
|
35
|
-
@base_path = normalize_base_path(base_path)
|
|
35
|
+
@base_path = LLM::Utils.normalize_base_path(base_path)
|
|
36
36
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
37
37
|
@headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
|
|
38
38
|
@transport = resolve_transport(transport, persistent:)
|
|
@@ -44,7 +44,7 @@ class LLM::Provider
|
|
|
44
44
|
# @return [String]
|
|
45
45
|
# @note The secret key is redacted in inspect for security reasons
|
|
46
46
|
def inspect
|
|
47
|
-
"#<#{
|
|
47
|
+
"#<#{LLM::Utils.object_id(self)} @key=[REDACTED] @transport=#{transport.inspect} @tracer=#{tracer.inspect}>"
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
##
|
|
@@ -339,6 +339,13 @@ class LLM::Provider
|
|
|
339
339
|
LLM::Stream === stream || stream.respond_to?(:<<)
|
|
340
340
|
end
|
|
341
341
|
|
|
342
|
+
##
|
|
343
|
+
# @return [Boolean]
|
|
344
|
+
# Returns true when an API key is configured
|
|
345
|
+
def key?
|
|
346
|
+
@key != nil && @key.to_s.strip.size > 0
|
|
347
|
+
end
|
|
348
|
+
|
|
342
349
|
private
|
|
343
350
|
|
|
344
351
|
def path(suffix)
|
|
@@ -346,13 +353,6 @@ class LLM::Provider
|
|
|
346
353
|
"#{@base_path}#{suffix}"
|
|
347
354
|
end
|
|
348
355
|
|
|
349
|
-
def normalize_base_path(path)
|
|
350
|
-
path = path.to_s.strip
|
|
351
|
-
return "" if path.empty? || path == "/"
|
|
352
|
-
path = "/#{path}" unless path.start_with?("/")
|
|
353
|
-
path.sub(%r{/+\z}, "")
|
|
354
|
-
end
|
|
355
|
-
|
|
356
356
|
attr_reader :base_uri, :host, :port, :timeout, :ssl, :transport
|
|
357
357
|
|
|
358
358
|
##
|
|
@@ -105,14 +105,14 @@ class LLM::Anthropic
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def resolve_tool(tool)
|
|
108
|
-
registered = @stream.
|
|
108
|
+
registered = @stream.__find__(tool["name"])
|
|
109
109
|
fn = (registered || LLM::Function.new(tool["name"])).dup.tap do |fn|
|
|
110
110
|
fn.id = tool["id"]
|
|
111
111
|
fn.arguments = LLM::Anthropic.parse_tool_input(tool["input"])
|
|
112
112
|
fn.tracer = @stream.extra[:tracer]
|
|
113
113
|
fn.model = @stream.extra[:model]
|
|
114
114
|
end
|
|
115
|
-
[fn, (registered ? nil :
|
|
115
|
+
[fn, (registered ? nil : fn.unavailable)]
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
end
|
|
@@ -184,14 +184,14 @@ class LLM::Bedrock
|
|
|
184
184
|
|
|
185
185
|
def resolve_tool(tool)
|
|
186
186
|
payload = tool["toolUse"] || {}
|
|
187
|
-
registered = @stream.
|
|
187
|
+
registered = @stream.__find__(payload["name"])
|
|
188
188
|
fn = (registered || LLM::Function.new(payload["name"])).dup.tap do |f|
|
|
189
189
|
f.id = payload["toolUseId"]
|
|
190
190
|
f.arguments = payload["input"] || {}
|
|
191
191
|
f.tracer = @stream.extra[:tracer]
|
|
192
192
|
f.model = @stream.extra[:model]
|
|
193
193
|
end
|
|
194
|
-
[fn, registered ? nil :
|
|
194
|
+
[fn, registered ? nil : fn.unavailable]
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
def content
|
|
@@ -153,14 +153,14 @@ class LLM::Google
|
|
|
153
153
|
|
|
154
154
|
def resolve_tool(part, cindex, pindex)
|
|
155
155
|
call = part["functionCall"]
|
|
156
|
-
registered = @stream.
|
|
156
|
+
registered = @stream.__find__(call["name"])
|
|
157
157
|
fn = (registered || LLM::Function.new(call["name"])).dup.tap do |fn|
|
|
158
158
|
fn.id = LLM::Google.tool_id(part:, cindex:, pindex:)
|
|
159
159
|
fn.arguments = call["args"]
|
|
160
160
|
fn.tracer = @stream.extra[:tracer]
|
|
161
161
|
fn.model = @stream.extra[:model]
|
|
162
162
|
end
|
|
163
|
-
[fn, (registered ? nil :
|
|
163
|
+
[fn, (registered ? nil : fn.unavailable)]
|
|
164
164
|
end
|
|
165
165
|
end
|
|
166
166
|
end
|
|
@@ -269,14 +269,14 @@ class LLM::OpenAI
|
|
|
269
269
|
# @group Resolvers
|
|
270
270
|
|
|
271
271
|
def resolve_tool(tool, arguments)
|
|
272
|
-
registered = @stream.
|
|
272
|
+
registered = @stream.__find__(tool["name"])
|
|
273
273
|
fn = (registered || LLM::Function.new(tool["name"])).dup.tap do |fn|
|
|
274
274
|
fn.id = tool["call_id"]
|
|
275
275
|
fn.arguments = arguments
|
|
276
276
|
fn.tracer = @stream.extra[:tracer]
|
|
277
277
|
fn.model = @stream.extra[:model]
|
|
278
278
|
end
|
|
279
|
-
[fn, (registered ? nil :
|
|
279
|
+
[fn, (registered ? nil : fn.unavailable)]
|
|
280
280
|
end
|
|
281
281
|
|
|
282
282
|
def parse_arguments(arguments)
|
|
@@ -185,14 +185,14 @@ class LLM::OpenAI
|
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
def resolve_tool(tool, function, arguments)
|
|
188
|
-
registered = @stream.
|
|
188
|
+
registered = @stream.__find__(function["name"])
|
|
189
189
|
fn = (registered || LLM::Function.new(function["name"])).dup.tap do |fn|
|
|
190
190
|
fn.id = tool["id"]
|
|
191
191
|
fn.arguments = arguments
|
|
192
192
|
fn.tracer = @stream.extra[:tracer]
|
|
193
193
|
fn.model = @stream.extra[:model]
|
|
194
194
|
end
|
|
195
|
-
[fn, (registered ? nil :
|
|
195
|
+
[fn, (registered ? nil : fn.unavailable)]
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
def parse_arguments(arguments)
|
data/lib/llm/response.rb
CHANGED
|
@@ -46,7 +46,7 @@ module LLM
|
|
|
46
46
|
# Returns an inspection of the response object
|
|
47
47
|
# @return [String]
|
|
48
48
|
def inspect
|
|
49
|
-
"#<#{
|
|
49
|
+
"#<#{LLM::Utils.object_id(self)} @body=#{body.inspect} @res=#{@res.inspect}>"
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
##
|
data/lib/llm/schema.rb
CHANGED
|
@@ -56,6 +56,8 @@ class LLM::Schema
|
|
|
56
56
|
def resolve(schema, type)
|
|
57
57
|
if LLM::Schema::Leaf === type
|
|
58
58
|
type
|
|
59
|
+
elsif ::Array === type
|
|
60
|
+
resolve_array(schema, type)
|
|
59
61
|
elsif Class === type && type.respond_to?(:object)
|
|
60
62
|
type.object
|
|
61
63
|
else
|
|
@@ -63,6 +65,15 @@ class LLM::Schema
|
|
|
63
65
|
schema.public_send(target)
|
|
64
66
|
end
|
|
65
67
|
end
|
|
68
|
+
|
|
69
|
+
def resolve_array(schema, values)
|
|
70
|
+
item = if values.size == 1
|
|
71
|
+
resolve(schema, values[0])
|
|
72
|
+
else
|
|
73
|
+
schema.any_of(*values.map { resolve(schema, _1) })
|
|
74
|
+
end
|
|
75
|
+
schema.array(item)
|
|
76
|
+
end
|
|
66
77
|
end
|
|
67
78
|
|
|
68
79
|
##
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -33,19 +33,24 @@ module LLM::Sequel
|
|
|
33
33
|
@llm_agent_options || Agent::DEFAULTS
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def model(model = nil)
|
|
37
|
-
return agent.model if model.nil?
|
|
38
|
-
agent.model(model)
|
|
36
|
+
def model(model = nil, &block)
|
|
37
|
+
return agent.model if model.nil? && !block
|
|
38
|
+
agent.model(model, &block)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def tools(*tools)
|
|
42
|
-
return agent.tools if tools.empty?
|
|
43
|
-
agent.tools(*tools)
|
|
41
|
+
def tools(*tools, &block)
|
|
42
|
+
return agent.tools if tools.empty? && !block
|
|
43
|
+
agent.tools(*tools, &block)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def
|
|
47
|
-
return agent.
|
|
48
|
-
agent.
|
|
46
|
+
def skills(*skills, &block)
|
|
47
|
+
return agent.skills if skills.empty? && !block
|
|
48
|
+
agent.skills(*skills, &block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def schema(schema = nil, &block)
|
|
52
|
+
return agent.schema if schema.nil? && !block
|
|
53
|
+
agent.schema(schema, &block)
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def instructions(instructions = nil)
|
|
@@ -58,6 +63,11 @@ module LLM::Sequel
|
|
|
58
63
|
agent.concurrency(concurrency)
|
|
59
64
|
end
|
|
60
65
|
|
|
66
|
+
def confirm(*tool_names, &block)
|
|
67
|
+
return agent.confirm if tool_names.empty? && !block
|
|
68
|
+
agent.confirm(*tool_names, &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
61
71
|
def tracer(tracer = nil, &block)
|
|
62
72
|
return agent.tracer if tracer.nil? && !block
|
|
63
73
|
agent.tracer(tracer, &block)
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -30,12 +30,7 @@ module LLM::Sequel
|
|
|
30
30
|
# Resolves a single configured option against a model instance.
|
|
31
31
|
# @return [Object]
|
|
32
32
|
def self.resolve_option(obj, option)
|
|
33
|
-
|
|
34
|
-
when Proc then obj.instance_exec(&option)
|
|
35
|
-
when Symbol then obj.send(option)
|
|
36
|
-
when Hash then option.dup
|
|
37
|
-
else option
|
|
38
|
-
end
|
|
33
|
+
LLM::Utils.resolve_option(obj, option)
|
|
39
34
|
end
|
|
40
35
|
|
|
41
36
|
##
|
|
@@ -80,11 +75,12 @@ module LLM::Sequel
|
|
|
80
75
|
##
|
|
81
76
|
# Persists the runtime state and usage columns back onto the record.
|
|
82
77
|
# @return [void]
|
|
83
|
-
def self.save(obj, ctx, options)
|
|
78
|
+
def self.save!(obj, ctx, options)
|
|
84
79
|
columns = self.columns(options)
|
|
85
80
|
payload = serialize_context(ctx, options[:format])
|
|
86
81
|
payload = wrap_json_payload(payload, options[:format])
|
|
87
|
-
obj
|
|
82
|
+
obj[columns[:data_column]] = payload
|
|
83
|
+
obj.save_changes(raise_on_failure: true)
|
|
88
84
|
end
|
|
89
85
|
|
|
90
86
|
##
|
|
@@ -164,16 +160,16 @@ module LLM::Sequel
|
|
|
164
160
|
# @return [LLM::Response]
|
|
165
161
|
def talk(...)
|
|
166
162
|
options = self.class.llm_plugin_options
|
|
167
|
-
ctx.talk(...).tap { Utils.save(self, ctx, options) }
|
|
163
|
+
ctx.talk(...).tap { Utils.save!(self, ctx, options) }
|
|
168
164
|
end
|
|
169
165
|
|
|
170
166
|
##
|
|
171
|
-
# Continues the stored context
|
|
172
|
-
# @see LLM::Context#
|
|
167
|
+
# Continues the stored context with new input and flushes it.
|
|
168
|
+
# @see LLM::Context#ask
|
|
173
169
|
# @return [LLM::Response]
|
|
174
|
-
def
|
|
170
|
+
def ask(...)
|
|
175
171
|
options = self.class.llm_plugin_options
|
|
176
|
-
ctx.
|
|
172
|
+
ctx.ask(...).tap { Utils.save!(self, ctx, options) }
|
|
177
173
|
end
|
|
178
174
|
|
|
179
175
|
##
|
data/lib/llm/stream.rb
CHANGED
|
@@ -9,8 +9,7 @@ module LLM
|
|
|
9
9
|
# subclass that overrides the callbacks it needs. For basic streaming,
|
|
10
10
|
# llm.rb also accepts any object that implements `#<<`. {#queue} provides
|
|
11
11
|
# a small helper for collecting asynchronous tool work started from a
|
|
12
|
-
# callback
|
|
13
|
-
# streamed tool cannot be resolved.
|
|
12
|
+
# callback.
|
|
14
13
|
#
|
|
15
14
|
# @note The `on_*` callbacks run inline with the streaming parser. They
|
|
16
15
|
# therefore block streaming progress and should generally return as
|
|
@@ -150,48 +149,24 @@ module LLM
|
|
|
150
149
|
|
|
151
150
|
# @endgroup
|
|
152
151
|
|
|
153
|
-
# @group
|
|
154
|
-
|
|
155
|
-
##
|
|
156
|
-
# Returns a function return describing a streamed tool that could not
|
|
157
|
-
# be resolved.
|
|
158
|
-
# @note This is mainly useful as a fallback from {#on_tool_call}. It
|
|
159
|
-
# should be uncommon in normal use, since streamed tool callbacks only
|
|
160
|
-
# run for tools already defined in the context.
|
|
161
|
-
# @param [LLM::Function] tool
|
|
162
|
-
# @return [LLM::Function::Return]
|
|
163
|
-
def tool_not_found(tool)
|
|
164
|
-
LLM::Function::Return.new(tool.id, tool.name, {
|
|
165
|
-
error: true, type: LLM::NoSuchToolError.name, message: "tool not found"
|
|
166
|
-
})
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
##
|
|
170
|
-
# Returns the tool definitions available for the current streamed request.
|
|
171
|
-
# This prefers request-local tools attached to the stream and falls back
|
|
172
|
-
# to the current context defaults when present.
|
|
173
|
-
# @return [Array<LLM::Function, LLM::Tool>]
|
|
174
|
-
def tools
|
|
175
|
-
extra[:tools] || ctx&.params&.dig(:tools) || []
|
|
176
|
-
end
|
|
152
|
+
# @group Finders
|
|
177
153
|
|
|
178
154
|
##
|
|
179
155
|
# Resolves a streamed tool call against the current request tools first,
|
|
180
156
|
# then falls back to the global function registry.
|
|
181
157
|
# @param [String] name
|
|
182
158
|
# @return [LLM::Function, nil]
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
else
|
|
189
|
-
candidate.name
|
|
190
|
-
end
|
|
191
|
-
candidate_name.to_s == name.to_s
|
|
159
|
+
def __find__(name)
|
|
160
|
+
tools = extra[:tools] || ctx&.params&.dig(:tools) || []
|
|
161
|
+
tool = tools.find do
|
|
162
|
+
candidate = _1.respond_to?(:function) ? _1.function.name : _1.name
|
|
163
|
+
candidate.to_s == name.to_s
|
|
192
164
|
end
|
|
193
|
-
tool
|
|
165
|
+
if tool
|
|
166
|
+
tool.respond_to?(:function) ? tool.function : tool
|
|
167
|
+
else
|
|
194
168
|
LLM::Function.find_by_name(name)
|
|
169
|
+
end
|
|
195
170
|
end
|
|
196
171
|
|
|
197
172
|
# @endgroup
|
data/lib/llm/tool/param.rb
CHANGED
|
@@ -62,14 +62,7 @@ class LLM::Tool
|
|
|
62
62
|
extend self
|
|
63
63
|
|
|
64
64
|
def resolve(schema, type)
|
|
65
|
-
|
|
66
|
-
type
|
|
67
|
-
elsif Class === type && type.respond_to?(:object)
|
|
68
|
-
type.object
|
|
69
|
-
else
|
|
70
|
-
target = type.name.split("::").last.downcase
|
|
71
|
-
schema.public_send(target)
|
|
72
|
-
end
|
|
65
|
+
LLM::Schema::Utils.resolve(schema, type)
|
|
73
66
|
end
|
|
74
67
|
|
|
75
68
|
def setup(leaf, description, options)
|
data/lib/llm/tool.rb
CHANGED
|
@@ -40,31 +40,54 @@ class LLM::Tool
|
|
|
40
40
|
# @return [Class<LLM::Tool>]
|
|
41
41
|
# Returns a subclass of LLM::Tool
|
|
42
42
|
def self.mcp(mcp, tool)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
Class.new(LLM::Tool) do
|
|
44
|
+
name tool["name"]
|
|
45
|
+
description tool["description"]
|
|
46
|
+
params { tool["inputSchema"] || {type: "object", properties: {}} }
|
|
47
|
+
|
|
48
|
+
define_singleton_method(:inspect) do
|
|
49
|
+
"<#{LLM::Utils.object_id(self)} name=#{tool["name"]} (mcp)>"
|
|
50
|
+
end
|
|
51
|
+
singleton_class.alias_method :to_s, :inspect
|
|
52
|
+
|
|
53
|
+
define_singleton_method(:mcp?) do
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
define_method(:call) do |**args|
|
|
58
|
+
mcp.call_tool(tool["name"], args)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# @param [LLM::A2A] a2a
|
|
65
|
+
# The A2A client that will execute the tool call
|
|
66
|
+
# @param [LLM::A2A::Card::Skill]
|
|
67
|
+
# An A2A tool
|
|
68
|
+
# @return [Class<LLM::Tool>]
|
|
69
|
+
# Returns a subclass of LLM::Tool
|
|
70
|
+
def self.a2a(a2a, skill)
|
|
71
|
+
name = skill.name.gsub(" ", "-")
|
|
72
|
+
Class.new(LLM::Tool) do
|
|
73
|
+
name(name)
|
|
74
|
+
description(skill.description)
|
|
75
|
+
parameter :input, String, "The input string"
|
|
76
|
+
required %i[input]
|
|
77
|
+
|
|
78
|
+
define_singleton_method(:inspect) do
|
|
79
|
+
"<#{LLM::Utils.object_id(self)} name=#{name} (a2a)>"
|
|
80
|
+
end
|
|
81
|
+
singleton_class.alias_method :to_s, :inspect
|
|
82
|
+
|
|
83
|
+
define_singleton_method(:a2a?) do
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
define_method(:call) do |input:|
|
|
88
|
+
res = a2a.send_message(input)
|
|
89
|
+
{task: res}
|
|
62
90
|
end
|
|
63
|
-
@mcp = false
|
|
64
|
-
register(klass)
|
|
65
|
-
klass
|
|
66
|
-
ensure
|
|
67
|
-
@mcp = false
|
|
68
91
|
end
|
|
69
92
|
end
|
|
70
93
|
|
|
@@ -106,8 +129,8 @@ class LLM::Tool
|
|
|
106
129
|
def self.inherited(tool)
|
|
107
130
|
LLM.lock(:inherited) do
|
|
108
131
|
tool.instance_eval { @__monitor ||= Monitor.new }
|
|
109
|
-
tool.function.
|
|
110
|
-
LLM::Tool.register(tool) unless
|
|
132
|
+
tool.function.define(tool)
|
|
133
|
+
LLM::Tool.register(tool) unless tool.mcp? || tool.a2a?
|
|
111
134
|
end
|
|
112
135
|
end
|
|
113
136
|
|
|
@@ -172,11 +195,18 @@ class LLM::Tool
|
|
|
172
195
|
false
|
|
173
196
|
end
|
|
174
197
|
|
|
198
|
+
##
|
|
199
|
+
# Returns true if the tool is an A2A tool
|
|
200
|
+
# @return [Boolean]
|
|
201
|
+
def self.a2a?
|
|
202
|
+
false
|
|
203
|
+
end
|
|
204
|
+
|
|
175
205
|
##
|
|
176
206
|
# Returns a function bound to this tool instance.
|
|
177
207
|
# @return [LLM::Function]
|
|
178
208
|
def function
|
|
179
|
-
@function ||= self.class.function.dup.tap { _1.
|
|
209
|
+
@function ||= self.class.function.dup.tap { _1.define(self) }
|
|
180
210
|
end
|
|
181
211
|
|
|
182
212
|
##
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -128,7 +128,7 @@ module LLM
|
|
|
128
128
|
##
|
|
129
129
|
# @return [String]
|
|
130
130
|
def inspect
|
|
131
|
-
"#<#{
|
|
131
|
+
"#<#{LLM::Utils.object_id(self)} @provider=#{@llm.class} @tracer=#{@tracer.inspect}>"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
##
|
data/lib/llm/transport/http.rb
CHANGED
|
@@ -13,13 +13,15 @@ class LLM::Transport
|
|
|
13
13
|
attr_reader :parser
|
|
14
14
|
|
|
15
15
|
##
|
|
16
|
-
# @param [#parse!, #body] parser
|
|
16
|
+
# @param [#parse!, #body, nil] parser
|
|
17
|
+
# @yieldparam [Hash] chunk
|
|
17
18
|
# @return [LLM::Transport::StreamDecoder]
|
|
18
|
-
def initialize(parser)
|
|
19
|
+
def initialize(parser = nil, &on_chunk)
|
|
19
20
|
@buffer = +""
|
|
20
21
|
@cursor = 0
|
|
21
22
|
@data = []
|
|
22
23
|
@parser = parser
|
|
24
|
+
@on_chunk = on_chunk
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
##
|
|
@@ -78,7 +80,8 @@ class LLM::Transport
|
|
|
78
80
|
def decode!(payload)
|
|
79
81
|
return if payload.empty? || payload == "[DONE]"
|
|
80
82
|
chunk = LLM.json.load(payload)
|
|
81
|
-
|
|
83
|
+
return unless chunk
|
|
84
|
+
parser ? parser.parse!(chunk) : @on_chunk&.call(chunk)
|
|
82
85
|
rescue *LLM.json.parser_error
|
|
83
86
|
end
|
|
84
87
|
|