llm.rb 10.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 +116 -10
- data/README.md +116 -32
- 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 +31 -5
- data/lib/llm/file.rb +7 -0
- data/lib/llm/function.rb +1 -1
- 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 +2 -9
- data/lib/llm/response.rb +1 -1
- data/lib/llm/sequel/agent.rb +14 -9
- data/lib/llm/sequel/plugin.rb +8 -7
- 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 +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
|
@@ -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/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
|
##
|
|
@@ -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/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/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
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Transport
|
|
4
|
+
##
|
|
5
|
+
# Shared utility methods for HTTP-backed transports.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module Utils
|
|
9
|
+
extend self
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def resolve_transport(uri, transport, timeout)
|
|
13
|
+
return default_transport(uri, timeout) if transport.nil?
|
|
14
|
+
if Class === transport && transport <= LLM::Transport
|
|
15
|
+
transport.new(
|
|
16
|
+
host: uri.host,
|
|
17
|
+
port: uri.port,
|
|
18
|
+
timeout:,
|
|
19
|
+
ssl: uri.scheme == "https"
|
|
20
|
+
)
|
|
21
|
+
else
|
|
22
|
+
transport
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def default_transport(uri, timeout)
|
|
27
|
+
LLM::Transport::HTTP.new(
|
|
28
|
+
host: uri.host,
|
|
29
|
+
port: uri.port,
|
|
30
|
+
timeout:,
|
|
31
|
+
ssl: uri.scheme == "https"
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/llm/transport.rb
CHANGED
|
@@ -27,6 +27,7 @@ module LLM
|
|
|
27
27
|
# transport-specific classes.
|
|
28
28
|
class Transport
|
|
29
29
|
require_relative "transport/response"
|
|
30
|
+
require_relative "transport/utils"
|
|
30
31
|
require_relative "transport/stream_decoder"
|
|
31
32
|
require_relative "transport/http"
|
|
32
33
|
require_relative "transport/persistent_http"
|
data/lib/llm/utils.rb
CHANGED
|
@@ -25,5 +25,49 @@ module LLM
|
|
|
25
25
|
else option
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Normalizes an HTTP API base path.
|
|
31
|
+
#
|
|
32
|
+
# Blank paths normalize to an empty string. Non-empty paths are
|
|
33
|
+
# prefixed with a leading slash and stripped of trailing slashes.
|
|
34
|
+
#
|
|
35
|
+
# @param [String, nil] path
|
|
36
|
+
# @return [String]
|
|
37
|
+
def normalize_base_path(path)
|
|
38
|
+
path = path.to_s.strip
|
|
39
|
+
return "" if path.empty? || path == "/"
|
|
40
|
+
path = "/#{path}" unless path.start_with?("/")
|
|
41
|
+
path.sub(%r{/+\z}, "")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Returns the Ruby module or class name for an object.
|
|
46
|
+
#
|
|
47
|
+
# This bypasses overridden `#name` implementations by binding
|
|
48
|
+
# {Module#name} directly.
|
|
49
|
+
#
|
|
50
|
+
# @param [Module] obj
|
|
51
|
+
# @return [String, nil]
|
|
52
|
+
def name_of(obj)
|
|
53
|
+
::Module.instance_method(:name).bind(obj).call
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Renders the class-and-object-id portion of an inspect string.
|
|
58
|
+
#
|
|
59
|
+
# This returns strings like `LLM::Tool:0x1234abcd`, which can be
|
|
60
|
+
# embedded into custom inspect output.
|
|
61
|
+
#
|
|
62
|
+
# @param [Object] obj
|
|
63
|
+
# @return [String]
|
|
64
|
+
def object_id(obj)
|
|
65
|
+
klass = if Class === obj
|
|
66
|
+
name_of(obj) || name_of(obj.superclass) || obj.class.name
|
|
67
|
+
else
|
|
68
|
+
obj.class.name || obj.class.to_s
|
|
69
|
+
end
|
|
70
|
+
"#{klass}:0x#{obj.object_id.to_s(16)}"
|
|
71
|
+
end
|
|
28
72
|
end
|
|
29
73
|
end
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module LLM
|
|
4
4
|
require "stringio"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
require_relative "llm/json_adapter"
|
|
6
7
|
require_relative "llm/tracer"
|
|
7
8
|
require_relative "llm/error"
|
|
@@ -35,6 +36,7 @@ module LLM
|
|
|
35
36
|
require_relative "llm/skill"
|
|
36
37
|
require_relative "llm/server_tool"
|
|
37
38
|
require_relative "llm/mcp"
|
|
39
|
+
require_relative "llm/a2a"
|
|
38
40
|
|
|
39
41
|
##
|
|
40
42
|
# Thread-safe monitors for different contexts
|
|
@@ -179,8 +181,6 @@ module LLM
|
|
|
179
181
|
end
|
|
180
182
|
|
|
181
183
|
##
|
|
182
|
-
# @param [LLM::Provider, nil] llm
|
|
183
|
-
# The provider to use for MCP transports that need one
|
|
184
184
|
# @param [Hash, nil] stdio
|
|
185
185
|
# @option stdio [Array<String>] :argv
|
|
186
186
|
# The command to run for the MCP process
|
|
@@ -189,8 +189,27 @@ module LLM
|
|
|
189
189
|
# @option stdio [String, nil] :cwd
|
|
190
190
|
# The working directory for the MCP process
|
|
191
191
|
# @return [LLM::MCP]
|
|
192
|
-
def mcp(
|
|
193
|
-
LLM::MCP.new(
|
|
192
|
+
def mcp(**)
|
|
193
|
+
LLM::MCP.new(**)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
##
|
|
197
|
+
# Creates a new A2A client connected to a remote agent.
|
|
198
|
+
#
|
|
199
|
+
# @param [Hash, nil] http
|
|
200
|
+
# @option http [String] :url
|
|
201
|
+
# The base URL of the A2A agent (e.g., "https://agent.example.com")
|
|
202
|
+
# @option http [Hash<String, String>] :headers
|
|
203
|
+
# Extra HTTP headers (e.g., Authorization)
|
|
204
|
+
# @option http [Integer, nil] :timeout
|
|
205
|
+
# Request timeout in seconds
|
|
206
|
+
# @option http [LLM::Transport, Class, nil] :transport
|
|
207
|
+
# Optional transport override
|
|
208
|
+
# @param [Symbol] binding
|
|
209
|
+
# The protocol binding to use. One of `:rest` or `:jsonrpc`
|
|
210
|
+
# @return [LLM::A2A]
|
|
211
|
+
def a2a(http:, binding: :rest)
|
|
212
|
+
LLM::A2A.http(**http, binding:)
|
|
194
213
|
end
|
|
195
214
|
|
|
196
215
|
##
|
data/llm.gemspec
CHANGED
|
@@ -9,7 +9,22 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ["azantar@proton.me", "0x1eef@hardenedbsd.org"]
|
|
10
10
|
|
|
11
11
|
spec.summary = "Ruby's most capable AI runtime"
|
|
12
|
-
spec.description =
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
llm.rb is Ruby's most capable AI runtime.
|
|
14
|
+
|
|
15
|
+
It runs on Ruby's standard library by default. loads optional pieces
|
|
16
|
+
only when needed, and offers a single runtime for providers, agents,
|
|
17
|
+
tools, skills, MCP, A2A (Agent2Agent), RAG (vector stores & embeddings),
|
|
18
|
+
streaming, files, and persisted state. As a bonus, llm.rb is also available
|
|
19
|
+
for mruby.
|
|
20
|
+
|
|
21
|
+
It supports OpenAI, OpenAI-compatible endpoints, Anthropic, Google
|
|
22
|
+
Gemini, DeepSeek, xAI, Z.ai, AWS Bedrock, Ollama, and llama.cpp. It
|
|
23
|
+
also includes built-in ActiveRecord and Sequel support, plus concurrent
|
|
24
|
+
tool execution through threads, tasks (via async gem), fibers, ractors,
|
|
25
|
+
and fork (via xchan.rb gem).
|
|
26
|
+
DESC
|
|
27
|
+
|
|
13
28
|
spec.license = "0BSD"
|
|
14
29
|
spec.required_ruby_version = ">= 3.3.0"
|
|
15
30
|
|