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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +158 -10
  3. data/README.md +145 -44
  4. data/lib/llm/a2a/card/capabilities.rb +41 -0
  5. data/lib/llm/a2a/card/interface.rb +34 -0
  6. data/lib/llm/a2a/card/provider.rb +27 -0
  7. data/lib/llm/a2a/card/skill.rb +68 -0
  8. data/lib/llm/a2a/card.rb +144 -0
  9. data/lib/llm/a2a/error.rb +49 -0
  10. data/lib/llm/a2a/notifications.rb +53 -0
  11. data/lib/llm/a2a/tasks.rb +55 -0
  12. data/lib/llm/a2a/transport/http.rb +131 -0
  13. data/lib/llm/a2a.rb +452 -0
  14. data/lib/llm/active_record/acts_as_agent.rb +15 -9
  15. data/lib/llm/active_record/acts_as_llm.rb +4 -4
  16. data/lib/llm/agent.rb +15 -9
  17. data/lib/llm/buffer.rb +1 -2
  18. data/lib/llm/context.rb +52 -5
  19. data/lib/llm/file.rb +7 -0
  20. data/lib/llm/function.rb +13 -5
  21. data/lib/llm/mcp/transport/http.rb +5 -18
  22. data/lib/llm/mcp/transport/stdio.rb +7 -0
  23. data/lib/llm/mcp.rb +20 -17
  24. data/lib/llm/message.rb +1 -1
  25. data/lib/llm/object/builder.rb +1 -0
  26. data/lib/llm/object/kernel.rb +1 -1
  27. data/lib/llm/object.rb +9 -0
  28. data/lib/llm/provider.rb +2 -9
  29. data/lib/llm/response.rb +1 -1
  30. data/lib/llm/schema.rb +23 -5
  31. data/lib/llm/sequel/agent.rb +14 -9
  32. data/lib/llm/sequel/plugin.rb +8 -7
  33. data/lib/llm/skill.rb +44 -14
  34. data/lib/llm/tool.rb +57 -27
  35. data/lib/llm/tracer/telemetry.rb +3 -1
  36. data/lib/llm/tracer.rb +1 -1
  37. data/lib/llm/transport/http.rb +1 -1
  38. data/lib/llm/transport/stream_decoder.rb +6 -3
  39. data/lib/llm/transport/utils.rb +35 -0
  40. data/lib/llm/transport.rb +1 -0
  41. data/lib/llm/utils.rb +44 -0
  42. data/lib/llm/version.rb +1 -1
  43. data/lib/llm.rb +23 -4
  44. data/llm.gemspec +16 -1
  45. metadata +26 -3
  46. 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
- attr_accessor :arguments
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 :register, :define
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 = Hash === arguments ? arguments.transform_keys(&:to_sym) : arguments
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, {error: true, type: ex.class.name, message: ex.message})
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
- require_relative "http/event_handler"
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
- parser = LLM::EventStream::Parser.new
119
- parser.register EventHandler.new { enqueue(_1) }
120
- res.read_body { parser << _1 }
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 }
@@ -78,6 +78,13 @@ module LLM::MCP::Transport
78
78
  command.wait
79
79
  end
80
80
 
81
+ ##
82
+ # @return [Boolean]
83
+ # Returns true when the MCP server connection is alive
84
+ def running?
85
+ command.alive?
86
+ end
87
+
81
88
  private
82
89
 
83
90
  attr_reader :command, :stdin, :stdout, :stderr
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(llm = nil, **stdio)
35
- new(llm, stdio:)
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(llm = nil, **http)
46
- new(llm, http:)
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(llm = nil, stdio: nil, http: nil, timeout: 30)
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 :llm, :command, :transport, :timeout
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
- "#<#{self.class.name}:0x#{object_id.to_s(16)} " \
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
@@ -17,6 +17,7 @@ class LLM::Object
17
17
  case obj
18
18
  when self then from(obj.to_h)
19
19
  when Array then obj.map { |v| from(v) }
20
+ when String then obj
20
21
  else
21
22
  visited = {}
22
23
  obj.each { visited[_1] = visit(_2) }
@@ -48,7 +48,7 @@ class LLM::Object
48
48
  end
49
49
 
50
50
  def inspect
51
- "#<#{self.class}:0x#{object_id.to_s(16)} properties=#{to_h.inspect}>"
51
+ "#<#{LLM::Utils.object_id(self)} properties=#{to_h.inspect}>"
52
52
  end
53
53
  alias_method :to_s, :inspect
54
54
 
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
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @transport=#{transport.inspect} @tracer=#{tracer.inspect}>"
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
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @body=#{body.inspect} @res=#{@res.inspect}>"
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", required: true
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", required: true
28
- # property :age, Integer, "Person's age", required: true
29
- # property :hobbies, Array[String], "Person's hobbies", required: true
30
- # property :address, Address, "Person's address", required: true
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]
@@ -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 schema(schema = nil)
47
- return agent.schema if schema.nil?
48
- agent.schema(schema)
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)
@@ -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.update(columns[:data_column] => payload)
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 through the Responses API and flushes it.
167
- # @see LLM::Context#respond
167
+ # Continues the stored context with new input and flushes it.
168
+ # @see LLM::Context#ask
168
169
  # @return [LLM::Response]
169
- def respond(...)
170
+ def ask(...)
170
171
  options = self.class.llm_plugin_options
171
- ctx.respond(...).tap { Utils.save(self, ctx, options) }
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
- instructions, tools, tracer = self.instructions, self.tools, ctx.llm.tracer
78
- params = ctx.params.merge(mode: ctx.mode).reject { [:tools, :schema].include?(_1) }
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
- lock do
44
- @mcp = true
45
- klass = Class.new(LLM::Tool) do
46
- name tool["name"]
47
- description tool["description"]
48
- params { tool["inputSchema"] || {type: "object", properties: {}} }
49
-
50
- define_singleton_method(:inspect) do
51
- "<LLM::Tool:0x#{object_id.to_s(16)} name=#{tool["name"]} (mcp)>"
52
- end
53
- singleton_class.alias_method :to_s, :inspect
54
-
55
- define_singleton_method(:mcp?) do
56
- true
57
- end
58
-
59
- define_method(:call) do |**args|
60
- mcp.call_tool(tool["name"], args)
61
- end
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.register(tool)
110
- LLM::Tool.register(tool) unless lock { @mcp }
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.register(self) }
209
+ @function ||= self.class.function.dup.tap { _1.define(self) }
180
210
  end
181
211
 
182
212
  ##
@@ -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
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @provider=#{@llm.class} @tracer=#{@tracer.inspect}>"
131
+ "#<#{LLM::Utils.object_id(self)} @provider=#{@llm.class} @tracer=#{@tracer.inspect}>"
132
132
  end
133
133
 
134
134
  ##
@@ -83,7 +83,7 @@ class LLM::Transport
83
83
  ##
84
84
  # @return [String]
85
85
  def inspect
86
- "#<#{self.class.name}:0x#{object_id.to_s(16)}>"
86
+ "#<#{LLM::Utils.object_id(self)}>"
87
87
  end
88
88
 
89
89
  private
@@ -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
- parser.parse!(chunk) if chunk
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