llm.rb 10.0.0 → 11.1.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 +158 -10
- data/README.md +145 -44
- 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 +15 -9
- data/lib/llm/active_record/acts_as_llm.rb +4 -4
- data/lib/llm/agent.rb +15 -9
- data/lib/llm/buffer.rb +1 -2
- data/lib/llm/context.rb +52 -5
- data/lib/llm/file.rb +7 -0
- data/lib/llm/function.rb +13 -5
- 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/builder.rb +1 -0
- data/lib/llm/object/kernel.rb +1 -1
- data/lib/llm/object.rb +9 -0
- data/lib/llm/provider.rb +2 -9
- data/lib/llm/response.rb +1 -1
- data/lib/llm/schema.rb +23 -5
- data/lib/llm/sequel/agent.rb +14 -9
- data/lib/llm/sequel/plugin.rb +8 -7
- data/lib/llm/skill.rb +44 -14
- data/lib/llm/tool.rb +57 -27
- data/lib/llm/tracer/telemetry.rb +3 -1
- 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 +44 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +23 -4
- data/llm.gemspec +16 -1
- metadata +26 -3
- data/lib/llm/mcp/transport/http/event_handler.rb +0 -68
data/lib/llm/function.rb
CHANGED
|
@@ -109,8 +109,16 @@ class LLM::Function
|
|
|
109
109
|
|
|
110
110
|
##
|
|
111
111
|
# Returns function arguments
|
|
112
|
-
# @return [Array, nil]
|
|
113
|
-
|
|
112
|
+
# @return [Hash, Array, LLM::Object, nil]
|
|
113
|
+
attr_reader :arguments
|
|
114
|
+
|
|
115
|
+
##
|
|
116
|
+
# Sets function arguments, wrapping them in an LLM::Object
|
|
117
|
+
# @param [Hash, LLM::Object] other
|
|
118
|
+
# @return [void]
|
|
119
|
+
def arguments=(other)
|
|
120
|
+
@arguments = LLM::Object.from(other)
|
|
121
|
+
end
|
|
114
122
|
|
|
115
123
|
##
|
|
116
124
|
# Returns a tracer, or nil
|
|
@@ -182,7 +190,7 @@ class LLM::Function
|
|
|
182
190
|
def define(klass = nil, &b)
|
|
183
191
|
@runner = klass || b
|
|
184
192
|
end
|
|
185
|
-
alias_method :
|
|
193
|
+
alias_method :def, :define
|
|
186
194
|
|
|
187
195
|
##
|
|
188
196
|
# Call the function
|
|
@@ -373,10 +381,10 @@ class LLM::Function
|
|
|
373
381
|
# Returns a Return object with either the function result or error information.
|
|
374
382
|
def call_function
|
|
375
383
|
runner = self.runner
|
|
376
|
-
kwargs =
|
|
384
|
+
kwargs = arguments.respond_to?(:to_h) ? arguments.to_h.transform_keys(&:to_sym) : arguments
|
|
377
385
|
Return.new(id, name, runner.call(**kwargs))
|
|
378
386
|
rescue => ex
|
|
379
|
-
Return.new(id, name,
|
|
387
|
+
Return.new(id, name, {error: true, type: ex.class.name, message: ex.message})
|
|
380
388
|
end
|
|
381
389
|
|
|
382
390
|
def call!
|
|
@@ -7,7 +7,7 @@ module LLM::MCP::Transport
|
|
|
7
7
|
# JSON-RPC messages with HTTP POST requests and buffers response
|
|
8
8
|
# messages for non-blocking reads.
|
|
9
9
|
class HTTP
|
|
10
|
-
|
|
10
|
+
include LLM::Transport::Utils
|
|
11
11
|
|
|
12
12
|
##
|
|
13
13
|
# @param [String] url
|
|
@@ -22,7 +22,7 @@ module LLM::MCP::Transport
|
|
|
22
22
|
def initialize(url:, headers: {}, timeout: nil, transport: nil)
|
|
23
23
|
@uri = URI.parse(url)
|
|
24
24
|
@headers = headers
|
|
25
|
-
@transport = resolve_transport(transport, timeout
|
|
25
|
+
@transport = resolve_transport(uri, transport, timeout)
|
|
26
26
|
@queue = []
|
|
27
27
|
@monitor = Monitor.new
|
|
28
28
|
@running = false
|
|
@@ -101,24 +101,11 @@ module LLM::MCP::Transport
|
|
|
101
101
|
res
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def resolve_transport(transport, timeout:)
|
|
105
|
-
return default_transport(timeout:) if transport.nil?
|
|
106
|
-
if Class === transport && transport <= LLM::Transport
|
|
107
|
-
return transport.new(host: uri.host, port: uri.port, timeout:, ssl: uri.scheme == "https")
|
|
108
|
-
end
|
|
109
|
-
transport
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def default_transport(timeout:)
|
|
113
|
-
LLM::Transport::HTTP.new(host: uri.host, port: uri.port, timeout:, ssl: uri.scheme == "https")
|
|
114
|
-
end
|
|
115
|
-
|
|
116
104
|
def read(res)
|
|
117
105
|
if res["content-type"].to_s.include?("text/event-stream")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
parser.free
|
|
106
|
+
decoder = LLM::Transport::StreamDecoder.new { enqueue(_1) }
|
|
107
|
+
res.read_body { decoder << _1 }
|
|
108
|
+
decoder.free
|
|
122
109
|
else
|
|
123
110
|
body = +""
|
|
124
111
|
res.read_body { body << _1 }
|
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/builder.rb
CHANGED
data/lib/llm/object/kernel.rb
CHANGED
data/lib/llm/object.rb
CHANGED
|
@@ -184,6 +184,15 @@ class LLM::Object < BasicObject
|
|
|
184
184
|
@h.slice(*args)
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
+
##
|
|
188
|
+
# @param [Hash, #to_h] other
|
|
189
|
+
# @return [Boolean]
|
|
190
|
+
def ==(other)
|
|
191
|
+
return false unless other.respond_to?(:to_h)
|
|
192
|
+
to_h == other.to_h || to_hash == other.to_h
|
|
193
|
+
end
|
|
194
|
+
alias_method :eql?, :==
|
|
195
|
+
|
|
187
196
|
private
|
|
188
197
|
|
|
189
198
|
def method_missing(m, *args, &b)
|
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
|
##
|
|
@@ -353,13 +353,6 @@ class LLM::Provider
|
|
|
353
353
|
"#{@base_path}#{suffix}"
|
|
354
354
|
end
|
|
355
355
|
|
|
356
|
-
def normalize_base_path(path)
|
|
357
|
-
path = path.to_s.strip
|
|
358
|
-
return "" if path.empty? || path == "/"
|
|
359
|
-
path = "/#{path}" unless path.start_with?("/")
|
|
360
|
-
path.sub(%r{/+\z}, "")
|
|
361
|
-
end
|
|
362
|
-
|
|
363
356
|
attr_reader :base_uri, :host, :port, :timeout, :ssl, :transport
|
|
364
357
|
|
|
365
358
|
##
|
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
|
@@ -20,14 +20,16 @@
|
|
|
20
20
|
#
|
|
21
21
|
# @example Ruby-style
|
|
22
22
|
# class Address < LLM::Schema
|
|
23
|
-
# property :street, String, "Street address"
|
|
23
|
+
# property :street, String, "Street address"
|
|
24
|
+
# required %i[street]
|
|
24
25
|
# end
|
|
25
26
|
#
|
|
26
27
|
# class Person < LLM::Schema
|
|
27
|
-
# property :name, String, "Person's name"
|
|
28
|
-
# property :age, Integer, "Person's age"
|
|
29
|
-
# property :hobbies, Array[String], "Person's hobbies"
|
|
30
|
-
# property :address, Address, "Person's address"
|
|
28
|
+
# property :name, String, "Person's name"
|
|
29
|
+
# property :age, Integer, "Person's age"
|
|
30
|
+
# property :hobbies, Array[String], "Person's hobbies"
|
|
31
|
+
# property :address, Address, "Person's address"
|
|
32
|
+
# required %i[name age hobbies address]
|
|
31
33
|
# end
|
|
32
34
|
class LLM::Schema
|
|
33
35
|
require_relative "schema/version"
|
|
@@ -74,6 +76,10 @@ class LLM::Schema
|
|
|
74
76
|
end
|
|
75
77
|
schema.array(item)
|
|
76
78
|
end
|
|
79
|
+
|
|
80
|
+
def fetch(properties, name)
|
|
81
|
+
properties[name] || properties.fetch(name.to_s)
|
|
82
|
+
end
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
##
|
|
@@ -103,6 +109,18 @@ class LLM::Schema
|
|
|
103
109
|
end
|
|
104
110
|
end
|
|
105
111
|
|
|
112
|
+
##
|
|
113
|
+
# Mark existing properties as required.
|
|
114
|
+
# @param names [Array<Symbol,String>]
|
|
115
|
+
# @return [LLM::Schema::Object]
|
|
116
|
+
def self.required(names)
|
|
117
|
+
lock do
|
|
118
|
+
object.tap do |schema|
|
|
119
|
+
[*names].each { Utils.fetch(schema.properties, _1).required }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
106
124
|
##
|
|
107
125
|
# @api private
|
|
108
126
|
# @return [LLM::Schema]
|
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)
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -75,11 +75,12 @@ module LLM::Sequel
|
|
|
75
75
|
##
|
|
76
76
|
# Persists the runtime state and usage columns back onto the record.
|
|
77
77
|
# @return [void]
|
|
78
|
-
def self.save(obj, ctx, options)
|
|
78
|
+
def self.save!(obj, ctx, options)
|
|
79
79
|
columns = self.columns(options)
|
|
80
80
|
payload = serialize_context(ctx, options[:format])
|
|
81
81
|
payload = wrap_json_payload(payload, options[:format])
|
|
82
|
-
obj
|
|
82
|
+
obj[columns[:data_column]] = payload
|
|
83
|
+
obj.save_changes(raise_on_failure: true)
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
##
|
|
@@ -159,16 +160,16 @@ module LLM::Sequel
|
|
|
159
160
|
# @return [LLM::Response]
|
|
160
161
|
def talk(...)
|
|
161
162
|
options = self.class.llm_plugin_options
|
|
162
|
-
ctx.talk(...).tap { Utils.save(self, ctx, options) }
|
|
163
|
+
ctx.talk(...).tap { Utils.save!(self, ctx, options) }
|
|
163
164
|
end
|
|
164
165
|
|
|
165
166
|
##
|
|
166
|
-
# Continues the stored context
|
|
167
|
-
# @see LLM::Context#
|
|
167
|
+
# Continues the stored context with new input and flushes it.
|
|
168
|
+
# @see LLM::Context#ask
|
|
168
169
|
# @return [LLM::Response]
|
|
169
|
-
def
|
|
170
|
+
def ask(...)
|
|
170
171
|
options = self.class.llm_plugin_options
|
|
171
|
-
ctx.
|
|
172
|
+
ctx.ask(...).tap { Utils.save!(self, ctx, options) }
|
|
172
173
|
end
|
|
173
174
|
|
|
174
175
|
##
|
data/lib/llm/skill.rb
CHANGED
|
@@ -56,6 +56,7 @@ module LLM
|
|
|
56
56
|
@instructions = ""
|
|
57
57
|
@frontmatter = LLM::Object.from({})
|
|
58
58
|
@tools = []
|
|
59
|
+
@inherit_tools = false
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
##
|
|
@@ -74,18 +75,8 @@ module LLM
|
|
|
74
75
|
# @param [LLM::Context] ctx
|
|
75
76
|
# @return [Hash]
|
|
76
77
|
def call(ctx)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
concurrency = params[:stream].extra[:concurrency] if LLM::Stream === params[:stream]
|
|
80
|
-
params[:concurrency] = concurrency if concurrency
|
|
81
|
-
agent = Class.new(LLM::Agent) do
|
|
82
|
-
instructions(instructions)
|
|
83
|
-
tools(*tools)
|
|
84
|
-
tracer(tracer)
|
|
85
|
-
end.new(ctx.llm, params)
|
|
86
|
-
agent.messages.concat(messages_for(ctx))
|
|
87
|
-
res = agent.talk("Solve the user's query.")
|
|
88
|
-
{content: res.content}
|
|
78
|
+
content = agent(ctx).talk("Solve the user's query.").content
|
|
79
|
+
{content:}
|
|
89
80
|
end
|
|
90
81
|
|
|
91
82
|
##
|
|
@@ -96,9 +87,10 @@ module LLM
|
|
|
96
87
|
def to_tool(ctx)
|
|
97
88
|
skill = self
|
|
98
89
|
Class.new(LLM::Tool) do
|
|
90
|
+
attr_accessor :tracer
|
|
91
|
+
|
|
99
92
|
name skill.name
|
|
100
93
|
description skill.description
|
|
101
|
-
attr_accessor :tracer
|
|
102
94
|
|
|
103
95
|
define_singleton_method(:skill?) do
|
|
104
96
|
true
|
|
@@ -110,6 +102,13 @@ module LLM
|
|
|
110
102
|
end
|
|
111
103
|
end
|
|
112
104
|
|
|
105
|
+
##
|
|
106
|
+
# Returns true when a skill should inherit tools from its parent
|
|
107
|
+
# @return [Boolean]
|
|
108
|
+
def inherit_tools?
|
|
109
|
+
@inherit_tools
|
|
110
|
+
end
|
|
111
|
+
|
|
113
112
|
private
|
|
114
113
|
|
|
115
114
|
def messages_for(ctx)
|
|
@@ -132,8 +131,39 @@ module LLM
|
|
|
132
131
|
@frontmatter = LLM::Object.from(YAML.safe_load(match[1]) || {})
|
|
133
132
|
@name = @frontmatter.name || @name
|
|
134
133
|
@description = @frontmatter.description || @description
|
|
135
|
-
@tools = [*@frontmatter.tools].map { LLM::Tool.find_by_name!(_1) }
|
|
136
134
|
@instructions = match[2]
|
|
135
|
+
@inherit_tools, @tools = parse_tools(@frontmatter.tools)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def parse_tools(tools)
|
|
139
|
+
case tools
|
|
140
|
+
when String
|
|
141
|
+
tools == "inherit" ? [true, []] : raise_invalid_error!(tools)
|
|
142
|
+
when Array
|
|
143
|
+
[false, [*@frontmatter.tools].map { LLM::Tool.find_by_name!(_1) }]
|
|
144
|
+
when NilClass
|
|
145
|
+
[false, []]
|
|
146
|
+
else
|
|
147
|
+
raise_invalid_error!(tools)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def raise_invalid_error!(tools)
|
|
152
|
+
raise LLM::Error, "invalid value for tools key: '#{tools}'"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def agent(ctx)
|
|
156
|
+
instructions, tools, tracer, inherit_tools = self.instructions, self.tools, ctx.llm.tracer, inherit_tools?
|
|
157
|
+
params = ctx.params.merge(mode: ctx.mode).reject { [:tools, :schema].include?(_1) }
|
|
158
|
+
concurrency = params[:stream].extra[:concurrency] if LLM::Stream === params[:stream]
|
|
159
|
+
params[:concurrency] = concurrency if concurrency
|
|
160
|
+
agent = Class.new(LLM::Agent) do
|
|
161
|
+
instructions(instructions)
|
|
162
|
+
tools(inherit_tools ? ctx.params[:tools] : tools)
|
|
163
|
+
tracer(tracer)
|
|
164
|
+
end.new(ctx.llm, params)
|
|
165
|
+
agent.messages.concat(messages_for(ctx))
|
|
166
|
+
agent
|
|
137
167
|
end
|
|
138
168
|
end
|
|
139
169
|
end
|
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/telemetry.rb
CHANGED
|
@@ -223,7 +223,9 @@ module LLM
|
|
|
223
223
|
require "opentelemetry/sdk" unless defined?(OpenTelemetry)
|
|
224
224
|
@exporter ||= OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new
|
|
225
225
|
processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(@exporter)
|
|
226
|
-
@tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new
|
|
226
|
+
@tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new(
|
|
227
|
+
sampler: OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON
|
|
228
|
+
)
|
|
227
229
|
@tracer_provider.add_span_processor(processor)
|
|
228
230
|
@tracer = @tracer_provider.tracer("llm.rb", LLM::VERSION)
|
|
229
231
|
end
|
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
|
|