llm.rb 12.5.1 → 13.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 +482 -0
- data/LICENSE +21 -93
- data/README.md +49 -159
- data/data/deepinfra.json +3 -0
- data/data/xai.json +1 -1
- data/lib/llm/a2a.rb +1 -1
- data/lib/llm/active_record/acts_as_agent.rb +32 -0
- data/lib/llm/active_record/acts_as_llm.rb +6 -6
- data/lib/llm/agent.rb +101 -26
- data/lib/llm/buffer.rb +85 -3
- data/lib/llm/compactor/null.rb +19 -0
- data/lib/llm/compactor/truncate.rb +80 -0
- data/lib/llm/compactor.rb +42 -124
- data/lib/llm/context.rb +33 -37
- data/lib/llm/contract.rb +4 -25
- data/lib/llm/function/array.rb +15 -14
- data/lib/llm/function/async/group.rb +54 -0
- data/lib/llm/function/async/reactor.rb +48 -0
- data/lib/llm/function/async/task.rb +83 -0
- data/lib/llm/function/fiber/group.rb +46 -0
- data/lib/llm/function/fiber/task.rb +62 -0
- data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
- data/lib/llm/function/fork/job.rb +4 -3
- data/lib/llm/function/fork/task.rb +20 -10
- data/lib/llm/function/group.rb +40 -0
- data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
- data/lib/llm/function/ractor/job.rb +19 -3
- data/lib/llm/function/ractor/mailbox.rb +9 -0
- data/lib/llm/function/ractor/task.rb +24 -15
- data/lib/llm/function/{call_group.rb → sequential/group.rb} +17 -8
- data/lib/llm/function/sequential/task.rb +49 -0
- data/lib/llm/function/task.rb +25 -37
- data/lib/llm/function/thread/group.rb +46 -0
- data/lib/llm/function/thread/task.rb +60 -0
- data/lib/llm/function/tracing.rb +2 -0
- data/lib/llm/function.rb +56 -64
- data/lib/llm/loop_guard.rb +1 -2
- data/lib/llm/mcp.rb +22 -0
- data/lib/llm/object.rb +2 -1
- data/lib/llm/provider.rb +6 -3
- data/lib/llm/providers/google.rb +2 -2
- data/lib/llm/repl/command.rb +35 -8
- data/lib/llm/repl/commands/compact.rb +33 -0
- data/lib/llm/repl/input.rb +80 -15
- data/lib/llm/repl/markdown/table.rb +76 -0
- data/lib/llm/repl/markdown.rb +31 -1
- data/lib/llm/repl/status.rb +1 -1
- data/lib/llm/repl/stream.rb +10 -3
- data/lib/llm/repl/transcript.rb +1 -1
- data/lib/llm/repl/walker.rb +46 -0
- data/lib/llm/repl.rb +18 -12
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +5 -0
- data/lib/llm/schema/object.rb +11 -5
- data/lib/llm/sequel/agent.rb +32 -0
- data/lib/llm/sequel/plugin.rb +6 -6
- data/lib/llm/stream.rb +24 -17
- data/lib/llm/tool/param.rb +12 -0
- data/lib/llm/tool.rb +20 -4
- data/lib/llm/tools/chdir.rb +0 -2
- data/lib/llm/tools/git.rb +8 -4
- data/lib/llm/tools/mkdir.rb +1 -1
- data/lib/llm/tools/pwd.rb +0 -2
- data/lib/llm/tools/read_file.rb +0 -2
- data/lib/llm/tools/rg.rb +8 -4
- data/lib/llm/tools/shell.rb +8 -4
- data/lib/llm/tools/utils.rb +31 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +25 -5
- data/llm.gemspec +3 -3
- data/resources/deepdive.md +693 -57
- metadata +24 -13
- data/lib/llm/function/call_task.rb +0 -46
- data/lib/llm/function/fiber_group.rb +0 -105
- data/lib/llm/function/task_group.rb +0 -97
- data/lib/llm/function/thread_group.rb +0 -102
data/README.md
CHANGED
|
@@ -24,7 +24,8 @@ optional dependencies that are opt-in.
|
|
|
24
24
|
The runtime supports OpenAI, OpenAI-compatible endpoints, Anthropic, Google
|
|
25
25
|
Gemini, Mistral, DeepSeek, DeepInfra, xAI, Z.ai, AWS Bedrock, Ollama, and llama.cpp.
|
|
26
26
|
It has first-class support for streaming, tool calls, MCP
|
|
27
|
-
and A2A, embeddings, vector stores
|
|
27
|
+
and A2A, embeddings, vector stores, OCR, context compaction,
|
|
28
|
+
and the RAG pattern.
|
|
28
29
|
|
|
29
30
|
There are multiple HTTP backends to choose from, tools can be run concurrently
|
|
30
31
|
or in parallel via threads, async tasks, fibers, ractors, and fork, and it is
|
|
@@ -35,6 +36,9 @@ so once you learn the fundamentals, everything else falls into place naturally.
|
|
|
35
36
|
you learn llm.rb, you will also be able to use <a href="https://r.uby.dev/mruby-llm">mruby-llm</a> and
|
|
36
37
|
<a href="https://r.uby.dev/wasm-llm">wasm-llm</a> because the API is pretty much identical.
|
|
37
38
|
|
|
39
|
+
For detailed explanations, configuration, and advanced patterns, see the
|
|
40
|
+
[deepdive.md](https://r.uby.dev/llm/deepdive/).
|
|
41
|
+
|
|
38
42
|
## Install
|
|
39
43
|
|
|
40
44
|
```bash
|
|
@@ -116,153 +120,52 @@ agent = LLM::Agent.new(llm, stream: MyStream.new)
|
|
|
116
120
|
agent.talk "Explain Ruby fibers."
|
|
117
121
|
```
|
|
118
122
|
|
|
119
|
-
#### LLM::
|
|
120
|
-
|
|
121
|
-
The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
|
|
122
|
-
method allows an agent to spawn a read-eval-print loop
|
|
123
|
-
that can be useful while developing or operating agents.
|
|
124
|
-
It can be used to debug tool calls, confirm an
|
|
125
|
-
agent has done what was expected, or improve an agent by
|
|
126
|
-
asking questions about what it has done up to that point.
|
|
127
|
-
|
|
128
|
-
This feature requires that the [curses](https://github.com/ruby/curses)
|
|
129
|
-
and [kramdown](https://github.com/gettalong/kramdown) libraries are
|
|
130
|
-
installed and available to require.
|
|
131
|
-
|
|
132
|
-
The TUI displays a status line with a context-usage bar and cost
|
|
133
|
-
counter, a scrollable transcript with markdown rendering, and a
|
|
134
|
-
multi-line input area. The UI stays responsive while the model
|
|
135
|
-
is generating a response.
|
|
136
|
-
|
|
137
|
-
##### REPL: Agent
|
|
138
|
-
|
|
139
|
-
A REPL session is started by calling `repl` on any agent
|
|
140
|
-
instance. The session inherits the agent's model, tools,
|
|
141
|
-
skills, and instructions.
|
|
123
|
+
#### LLM::Schema
|
|
142
124
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
agent = LLM::Agent.new(llm)
|
|
148
|
-
agent.repl
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
##### REPL: State
|
|
125
|
+
[`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html) subclasses produce typed, structured
|
|
126
|
+
output from any model call. Pass a schema to `LLM::Context#talk`,
|
|
127
|
+
`LLM::Agent#talk`, or `LLM::Provider#complete` to receive validated
|
|
128
|
+
JSON instead of free text. Schemas work alongside tools and streams.
|
|
152
129
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
130
|
+
[`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html) can define objects, arrays, enums, nested schemas,
|
|
131
|
+
and more. It is also used internally by [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) for parameter
|
|
132
|
+
definitions, so you already benefit from it when you declare tool
|
|
133
|
+
parameters.
|
|
156
134
|
|
|
157
135
|
```ruby
|
|
158
|
-
|
|
136
|
+
class Weather < LLM::Schema
|
|
137
|
+
property :city, String, "The city name"
|
|
138
|
+
property :temperature, Float, "Current temperature"
|
|
139
|
+
property :conditions, String, "Weather conditions"
|
|
140
|
+
required %i[city temperature conditions]
|
|
141
|
+
end
|
|
159
142
|
|
|
160
|
-
llm = LLM.
|
|
161
|
-
agent = LLM::Agent.new(llm)
|
|
162
|
-
agent.
|
|
143
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
144
|
+
agent = LLM::Agent.new(llm, schema: Weather)
|
|
145
|
+
res = agent.talk "Weather in Paris?"
|
|
146
|
+
res.content! # => {city: "Paris", temperature: 15.0, conditions: "Cloudy"}
|
|
163
147
|
```
|
|
164
148
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
The `tools` option lets you attach additional tools
|
|
168
|
-
for the duration of the session. This is in addition to
|
|
169
|
-
any tools that might already be associated with an agent.
|
|
170
|
-
|
|
171
|
-
A number of optional tools are distributed as part of
|
|
172
|
-
llm.rb. They power the agents that can be found in the
|
|
173
|
-
[agents/](agents/) directory.
|
|
174
|
-
|
|
175
|
-
```ruby
|
|
176
|
-
require "llm"
|
|
177
|
-
|
|
178
|
-
llm = LLM.deepseek(key: ENV["KEY"])
|
|
179
|
-
agent = LLM::Agent.new(llm)
|
|
180
|
-
agent.repl(tools: [Debugger])
|
|
181
|
-
```
|
|
149
|
+
#### LLM::REPL
|
|
182
150
|
|
|
183
|
-
The
|
|
184
|
-
|
|
151
|
+
The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
|
|
152
|
+
method drops you into a curses-based TUI for talking to an
|
|
153
|
+
agent interactively. The `path:` option saves and restores
|
|
154
|
+
runtime state across sessions. The `tools:` option attaches
|
|
155
|
+
extra tools for the duration of the session. It is like
|
|
156
|
+
`binding.pry` but for agents. For the full reference see the
|
|
157
|
+
[REPL section](https://r.uby.dev/llm/deepdive/#repl) in the
|
|
158
|
+
deepdive.
|
|
185
159
|
|
|
186
160
|
```ruby
|
|
187
161
|
require "llm"
|
|
188
162
|
require "llm/tools"
|
|
189
163
|
|
|
190
164
|
llm = LLM.deepseek(key: ENV["KEY"])
|
|
191
|
-
agent = LLM::Agent.new(llm)
|
|
192
|
-
agent.repl(tools: LLM::Tool.subclasses)
|
|
165
|
+
agent = LLM::Agent.new(llm, name: "my-agent")
|
|
166
|
+
agent.repl(path: "agent.json", tools: LLM::Tool.subclasses)
|
|
193
167
|
```
|
|
194
168
|
|
|
195
|
-
##### REPL: Skills
|
|
196
|
-
|
|
197
|
-
The `skills` option lets you load extra skill directories
|
|
198
|
-
without attaching them to an agent permanently.
|
|
199
|
-
|
|
200
|
-
```ruby
|
|
201
|
-
require "llm"
|
|
202
|
-
|
|
203
|
-
llm = LLM.deepseek(key: ENV["KEY"])
|
|
204
|
-
agent = LLM::Agent.new(llm)
|
|
205
|
-
agent.repl(skills: [__dir__])
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
##### REPL: Tracer
|
|
209
|
-
|
|
210
|
-
By default the tracer is disabled for the duration of the
|
|
211
|
-
session. Setting `tracer: true` configures the REPL to use
|
|
212
|
-
the tracer associated with an instance of
|
|
213
|
-
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
|
|
214
|
-
|
|
215
|
-
```ruby
|
|
216
|
-
require "llm"
|
|
217
|
-
|
|
218
|
-
llm = LLM.deepseek(key: ENV["KEY"])
|
|
219
|
-
tracer = LLM.logger(llm, path: "agent.log")
|
|
220
|
-
agent = LLM::Agent.new(llm, tracer:)
|
|
221
|
-
agent.repl(tracer: true, tools: [Debugger])
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
##### REPL: Commands
|
|
225
|
-
|
|
226
|
-
Commands are recognized by a `/` prefix and are backed by the
|
|
227
|
-
[`LLM::Repl::Command`](https://r.uby.dev/api-docs/llm.rb/LLM/Repl/Command.html)
|
|
228
|
-
class, which can be subclassed to add custom commands. Once you
|
|
229
|
-
create a subclass, it is automatically added to the repl. A command
|
|
230
|
-
can have zero or more parameters, and all parameters are presumed
|
|
231
|
-
to be a String (at least for now).
|
|
232
|
-
|
|
233
|
-
```ruby
|
|
234
|
-
require "llm"
|
|
235
|
-
require "llm/repl"
|
|
236
|
-
|
|
237
|
-
class Greeter < LLM::Command
|
|
238
|
-
name "greet"
|
|
239
|
-
description "Greets the given name"
|
|
240
|
-
parameter :name, String, "The person's name"
|
|
241
|
-
required %i[name]
|
|
242
|
-
|
|
243
|
-
def call(name:)
|
|
244
|
-
write("Welcome #{name}!\n")
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
##### REPL: Input
|
|
250
|
-
|
|
251
|
-
The input area supports several keyboard shortcuts:
|
|
252
|
-
|
|
253
|
-
| Key | Action |
|
|
254
|
-
|---|---|
|
|
255
|
-
| `Enter` | Submit the current prompt |
|
|
256
|
-
| `Ctrl+A` | Jump to the start of the line |
|
|
257
|
-
| `Ctrl+E` | Jump to the end of the line |
|
|
258
|
-
| `Ctrl+F` | Move the cursor forward |
|
|
259
|
-
| `Ctrl+K` | Erase from cursor to the end of the line |
|
|
260
|
-
| `Ctrl+Y` | Paste previously killed text |
|
|
261
|
-
| `Ctrl+D` | Delete the character at the cursor |
|
|
262
|
-
| `Left / Right` | Move the cursor |
|
|
263
|
-
| `Up / Down` | Scroll the transcript |
|
|
264
|
-
| `/exit` | Leave the REPL |
|
|
265
|
-
|
|
266
169
|
#### LLM::MCP
|
|
267
170
|
|
|
268
171
|
The Model Context Protocol (MCP) has first-class support
|
|
@@ -326,16 +229,20 @@ Document.create!(
|
|
|
326
229
|
|
|
327
230
|
#### Concurrency
|
|
328
231
|
|
|
329
|
-
The runtime supports
|
|
232
|
+
The runtime supports six different concurrency strategies that have
|
|
330
233
|
different attributes. The choice between all of them often depends
|
|
331
234
|
on the requirements of your application.
|
|
332
235
|
|
|
333
|
-
IO-bound tools are a good fit for the `:
|
|
236
|
+
IO-bound tools are a good fit for the `:async`, `:thread`,
|
|
334
237
|
and `:fiber` strategies while true parallelism can be achieved
|
|
335
238
|
with the `:fork` and `:ractor` strategies. The
|
|
336
|
-
`:
|
|
239
|
+
`:sequential` strategy runs tools one at a time and is the default.
|
|
240
|
+
The `:fork` strategy also provides a separate process that offers
|
|
337
241
|
isolation from its parent.
|
|
338
242
|
|
|
243
|
+
You can learn more about the llm.rb concurrency model in the
|
|
244
|
+
[deepdive.md](https://r.uby.dev/llm/deepdive/#concurrency).
|
|
245
|
+
|
|
339
246
|
```ruby
|
|
340
247
|
require "llm"
|
|
341
248
|
|
|
@@ -362,11 +269,11 @@ require "llm"
|
|
|
362
269
|
require "llm/active_record"
|
|
363
270
|
|
|
364
271
|
class Agent < ApplicationRecord
|
|
365
|
-
acts_as_agent
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
272
|
+
acts_as_agent
|
|
273
|
+
set name: "my-agent",
|
|
274
|
+
instructions: "solve the user's query",
|
|
275
|
+
model: "deepseek-v4-pro",
|
|
276
|
+
tools: [Research, FinalizeResearch, ActOnResearch]
|
|
370
277
|
|
|
371
278
|
private
|
|
372
279
|
|
|
@@ -482,22 +389,5 @@ and resources.
|
|
|
482
389
|
|
|
483
390
|
## License
|
|
484
391
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
Commercial production use requires a commercial license.
|
|
488
|
-
<br>
|
|
489
|
-
Each version converts to the [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
|
|
490
|
-
four years after its first public release.
|
|
491
|
-
<br>
|
|
492
|
-
Contact [robert@r.uby.dev](mailto:robert@r.uby.dev) for a commercial license.
|
|
493
|
-
|
|
494
|
-
### Waivers
|
|
495
|
-
|
|
496
|
-
Waivers are automatically granted for: <br>
|
|
497
|
-
|
|
498
|
-
* Personal use
|
|
499
|
-
* Students
|
|
500
|
-
* Teachers
|
|
501
|
-
* Evaluation, development, and testing
|
|
502
|
-
* Non-profits and charities
|
|
503
|
-
* Companies with less than or equal to 50 employees
|
|
392
|
+
This software is released under the terms of the MIT license. <br>
|
|
393
|
+
See [LICENSE](./LICENSE) for details.
|
data/data/deepinfra.json
CHANGED
|
@@ -503,6 +503,7 @@
|
|
|
503
503
|
"context": 131072,
|
|
504
504
|
"output": 131072
|
|
505
505
|
},
|
|
506
|
+
"status": "deprecated",
|
|
506
507
|
"cost": {
|
|
507
508
|
"input": 0.4,
|
|
508
509
|
"output": 0.4
|
|
@@ -537,6 +538,7 @@
|
|
|
537
538
|
"context": 262144,
|
|
538
539
|
"output": 65536
|
|
539
540
|
},
|
|
541
|
+
"status": "deprecated",
|
|
540
542
|
"cost": {
|
|
541
543
|
"input": 0.2,
|
|
542
544
|
"output": 0.8
|
|
@@ -832,6 +834,7 @@
|
|
|
832
834
|
"context": 196608,
|
|
833
835
|
"output": 131072
|
|
834
836
|
},
|
|
837
|
+
"status": "deprecated",
|
|
835
838
|
"cost": {
|
|
836
839
|
"input": 0.15,
|
|
837
840
|
"output": 1.15,
|
data/data/xai.json
CHANGED
data/lib/llm/a2a.rb
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
# a2a = LLM::A2A.rest(url: "https://agent.example.com")
|
|
28
28
|
# ctx = LLM::Context.new(llm, tools: a2a.skills)
|
|
29
29
|
# ctx.talk("Analyze this data using the remote agent.")
|
|
30
|
-
# ctx.talk(ctx.wait(:
|
|
30
|
+
# ctx.talk(ctx.wait(:sequential)) while ctx.pending_functions?
|
|
31
31
|
class LLM::A2A
|
|
32
32
|
require_relative "a2a/card"
|
|
33
33
|
require_relative "a2a/error"
|
|
@@ -17,9 +17,41 @@ module LLM::ActiveRecord
|
|
|
17
17
|
# class and forwarded to an internal agent subclass.
|
|
18
18
|
module ActsAsAgent
|
|
19
19
|
module ClassMethods
|
|
20
|
+
##
|
|
21
|
+
# @return [Class<LLM::Agent>]
|
|
20
22
|
def agent
|
|
21
23
|
@agent ||= Class.new(LLM::Agent)
|
|
22
24
|
end
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Bulk-assign class-level agent defaults.
|
|
28
|
+
#
|
|
29
|
+
# Each key is resolved by calling the corresponding class method on the
|
|
30
|
+
# internal agent subclass.
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# class Agent < ApplicationRecord
|
|
34
|
+
# acts_as_agent
|
|
35
|
+
# set instructions: "You are a system administrator",
|
|
36
|
+
# model: "gpt-4.1-nano",
|
|
37
|
+
# tools: [Shell]
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# @param [Hash] properties
|
|
41
|
+
# @option properties [String] :instructions
|
|
42
|
+
# @option properties [String] :model
|
|
43
|
+
# @option properties [Array<LLM::Function>] :tools
|
|
44
|
+
# @option properties [Array<String>] :skills
|
|
45
|
+
# @option properties [#to_json] :schema
|
|
46
|
+
# @option properties [Symbol, Array<Symbol>] :concurrency
|
|
47
|
+
# @option properties [LLM::Tracer, Proc] :tracer
|
|
48
|
+
# @option properties [Object, Proc] :stream
|
|
49
|
+
# @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
|
|
50
|
+
# @raise [KeyError] when a property key does not match a class-level accessor
|
|
51
|
+
# @return [void]
|
|
52
|
+
def set(properties)
|
|
53
|
+
agent.set(properties)
|
|
54
|
+
end
|
|
23
55
|
end
|
|
24
56
|
|
|
25
57
|
module Hooks
|
|
@@ -104,17 +104,17 @@ module LLM::ActiveRecord
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
##
|
|
107
|
-
# @see LLM::Context#
|
|
107
|
+
# @see LLM::Context#pending_functions
|
|
108
108
|
# @return [Array<LLM::Function>]
|
|
109
|
-
def
|
|
110
|
-
ctx.
|
|
109
|
+
def pending_functions
|
|
110
|
+
ctx.pending_functions
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
##
|
|
114
|
-
# @see LLM::Context#
|
|
114
|
+
# @see LLM::Context#pending_functions?
|
|
115
115
|
# @return [Boolean]
|
|
116
|
-
def
|
|
117
|
-
ctx.
|
|
116
|
+
def pending_functions?
|
|
117
|
+
ctx.pending_functions?
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
##
|
data/lib/llm/agent.rb
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module LLM
|
|
4
4
|
##
|
|
5
|
-
# {LLM::Agent LLM::Agent}
|
|
6
|
-
#
|
|
7
|
-
# tools, schema,
|
|
5
|
+
# {LLM::Agent LLM::Agent} is the recommended entry point for most
|
|
6
|
+
# use-cases. It provides a class-level DSL for defining reusable,
|
|
7
|
+
# preconfigured assistants with defaults for model, tools, schema,
|
|
8
|
+
# and instructions.
|
|
8
9
|
#
|
|
9
10
|
# It wraps the same stateful runtime surface as
|
|
10
11
|
# {LLM::Context LLM::Context}: message history, usage, persistence,
|
|
@@ -22,26 +23,91 @@ module LLM
|
|
|
22
23
|
# * The default tool attempt budget is `25`. After that, the agent sends
|
|
23
24
|
# advisory tool errors back through the model and keeps the loop in-band.
|
|
24
25
|
# Set `tool_attempts: nil` to disable that advisory behavior.
|
|
25
|
-
# * Tool loop execution can be configured with `concurrency :
|
|
26
|
-
# `:thread`, `:
|
|
26
|
+
# * Tool loop execution can be configured with `concurrency :sequential`,
|
|
27
|
+
# `:thread`, `:async`, `:fiber`, or `:ractor`.
|
|
27
28
|
#
|
|
28
|
-
# @example
|
|
29
|
+
# @example Subclass with defaults
|
|
29
30
|
# class SystemAdmin < LLM::Agent
|
|
30
|
-
# model "gpt-4.1-nano"
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
31
|
+
# set model: "gpt-4.1-nano",
|
|
32
|
+
# instructions: "You are a Linux system admin",
|
|
33
|
+
# tools: [Shell],
|
|
34
|
+
# schema: Result
|
|
34
35
|
# end
|
|
35
36
|
#
|
|
36
37
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
37
38
|
# agent = SystemAdmin.new(llm)
|
|
38
39
|
# agent.talk("Run 'date'")
|
|
40
|
+
#
|
|
41
|
+
# @example Direct instance
|
|
42
|
+
# llm = LLM.deepseek(key: ENV["KEY"])
|
|
43
|
+
# agent = LLM::Agent.new(llm, stream: $stdout)
|
|
44
|
+
# agent.talk "Hello world"
|
|
45
|
+
#
|
|
46
|
+
# @see LLM::Context The low-level runtime that Agent wraps
|
|
47
|
+
# @see LLM::Tool Tools that Agent can call on your behalf
|
|
48
|
+
# @see LLM::Stream Stream callbacks for model output
|
|
39
49
|
class Agent
|
|
50
|
+
##
|
|
51
|
+
# @api private
|
|
52
|
+
UNDEFINED = Object.new
|
|
53
|
+
private_constant :UNDEFINED
|
|
54
|
+
|
|
40
55
|
##
|
|
41
56
|
# Returns a provider
|
|
42
57
|
# @return [LLM::Provider]
|
|
43
58
|
attr_reader :llm
|
|
44
59
|
|
|
60
|
+
##
|
|
61
|
+
# Bulk-assign class-level agent defaults from a Hash.
|
|
62
|
+
#
|
|
63
|
+
# Each key is resolved by calling the corresponding class method on the
|
|
64
|
+
# agent subclass. An error is raised for unknown keys so that typos are
|
|
65
|
+
# caught early.
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# class AdminAgent < LLM::Agent
|
|
69
|
+
# set name: "admin",
|
|
70
|
+
# instructions: "You are a system administrator",
|
|
71
|
+
# model: "gpt-4.1-nano",
|
|
72
|
+
# tools: [Shell, ReadFile]
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
# @param [Hash] properties
|
|
76
|
+
# @option properties [String] :instructions
|
|
77
|
+
# @option properties [String] :model
|
|
78
|
+
# @option properties [Array<LLM::Function>] :tools
|
|
79
|
+
# @option properties [Array<String>] :skills
|
|
80
|
+
# @option properties [#to_json] :schema
|
|
81
|
+
# @option properties [Symbol, Array<Symbol>] :concurrency
|
|
82
|
+
# @option properties [LLM::Tracer, Proc] :tracer
|
|
83
|
+
# @option properties [Object, Proc] :stream
|
|
84
|
+
# @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
|
|
85
|
+
# @raise [KeyError] when a property key does not match a class-level accessor
|
|
86
|
+
# @return [void]
|
|
87
|
+
def self.set(properties)
|
|
88
|
+
properties.each do
|
|
89
|
+
if respond_to?(_1)
|
|
90
|
+
public_send(_1, _2)
|
|
91
|
+
else
|
|
92
|
+
raise KeyError, "key not found: #{_1}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# Set or get an agent's name
|
|
99
|
+
# @param [String] name
|
|
100
|
+
# The agent name
|
|
101
|
+
# @return [String]
|
|
102
|
+
# Return's the agents name
|
|
103
|
+
def self.name(name = UNDEFINED, &block)
|
|
104
|
+
if name.equal?(UNDEFINED)
|
|
105
|
+
@name || self.to_s.gsub(/(.)([A-Z])/, '\\1-\\2').downcase
|
|
106
|
+
else
|
|
107
|
+
@name = block || name
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
45
111
|
##
|
|
46
112
|
# Set or get the default model
|
|
47
113
|
# @param [String, nil] model
|
|
@@ -110,9 +176,9 @@ module LLM
|
|
|
110
176
|
#
|
|
111
177
|
# @param [Symbol, Array<Symbol>, nil] concurrency
|
|
112
178
|
# Controls how pending tool loops are executed:
|
|
113
|
-
# - `:
|
|
179
|
+
# - `:sequential`: sequential calls
|
|
114
180
|
# - `:thread`: concurrent threads
|
|
115
|
-
# - `:
|
|
181
|
+
# - `:async`: concurrent async tasks
|
|
116
182
|
# - `:fiber`: concurrent scheduler-backed fibers
|
|
117
183
|
# - `:fork`: forked child processes
|
|
118
184
|
# - `:ractor`: concurrent Ruby ractors for class-based tools; MCP tools are not supported,
|
|
@@ -211,8 +277,8 @@ module LLM
|
|
|
211
277
|
# @option params [Symbol, Array<Symbol>, nil] :concurrency Defaults to the agent class concurrency
|
|
212
278
|
def initialize(llm, params = {})
|
|
213
279
|
@llm = llm
|
|
214
|
-
fields = %i[model skills schema tracer stream tools concurrency instructions confirm]
|
|
215
|
-
fields_ivar = %i[tracer concurrency instructions confirm]
|
|
280
|
+
fields = %i[name model skills schema tracer stream tools concurrency instructions confirm]
|
|
281
|
+
fields_ivar = %i[name tracer concurrency instructions confirm]
|
|
216
282
|
fields.each do |field|
|
|
217
283
|
resolvable = params.key?(field) ? params.delete(field) : self.class.public_send(field)
|
|
218
284
|
resolve_symbol = !%i[concurrency].include?(field)
|
|
@@ -229,6 +295,13 @@ module LLM
|
|
|
229
295
|
@ctx = LLM::Context.new(llm, {guard: true}.merge(params))
|
|
230
296
|
end
|
|
231
297
|
|
|
298
|
+
##
|
|
299
|
+
# Returns the agent's name
|
|
300
|
+
# @return [String]
|
|
301
|
+
def name
|
|
302
|
+
@name
|
|
303
|
+
end
|
|
304
|
+
|
|
232
305
|
##
|
|
233
306
|
# Maintain a conversation via the chat completions API.
|
|
234
307
|
# This method immediately sends a request to the LLM and returns the response.
|
|
@@ -263,10 +336,9 @@ module LLM
|
|
|
263
336
|
|
|
264
337
|
##
|
|
265
338
|
# @return [Array<LLM::Function>]
|
|
266
|
-
def
|
|
267
|
-
@tracer ? @llm.with_tracer(@tracer) { @ctx.
|
|
339
|
+
def pending_functions
|
|
340
|
+
@tracer ? @llm.with_tracer(@tracer) { @ctx.pending_functions } : @ctx.pending_functions
|
|
268
341
|
end
|
|
269
|
-
alias_method :pending_functions, :functions
|
|
270
342
|
|
|
271
343
|
##
|
|
272
344
|
# @see LLM::Context#returns
|
|
@@ -399,6 +471,9 @@ module LLM
|
|
|
399
471
|
# By default this method disables the tracer for
|
|
400
472
|
# the duration of the repl session, and restores
|
|
401
473
|
# it afterwards.
|
|
474
|
+
# @param [String] name
|
|
475
|
+
# The agent's name.
|
|
476
|
+
# Defaults to {LLM::Agent#name}.
|
|
402
477
|
# @param [String] path
|
|
403
478
|
# The path to a file where runtime state is read
|
|
404
479
|
# from, and written to
|
|
@@ -410,7 +485,7 @@ module LLM
|
|
|
410
485
|
# When true, the tracer is kept alive during the
|
|
411
486
|
# repl session. Default is false.
|
|
412
487
|
# @return [void]
|
|
413
|
-
def repl(path: nil, tools: [], skills: [], tracer: false, trace: nil)
|
|
488
|
+
def repl(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
|
|
414
489
|
if trace != nil
|
|
415
490
|
warn "llm.rb: trace option is deprecated, use tracer instead"
|
|
416
491
|
tracer = trace
|
|
@@ -420,7 +495,7 @@ module LLM
|
|
|
420
495
|
self.tracer = nil
|
|
421
496
|
end
|
|
422
497
|
require_relative "repl" unless defined?(::LLM::Repl)
|
|
423
|
-
LLM::Repl.new(agent: self, path:, tools:, skills:).start
|
|
498
|
+
LLM::Repl.new(agent: self, name:, path:, tools:, skills:).start
|
|
424
499
|
ensure
|
|
425
500
|
if !tracer
|
|
426
501
|
self.tracer = previous
|
|
@@ -480,7 +555,7 @@ module LLM
|
|
|
480
555
|
# @param [Symbol, Array<Symbol>] strategy
|
|
481
556
|
# The execution strategy that would be used for the tool call.
|
|
482
557
|
# @return [LLM::Function::Return]
|
|
483
|
-
# Return either `fn.
|
|
558
|
+
# Return either `fn.task(strategy).wait` to approve execution or
|
|
484
559
|
# `fn.cancel(...)` to cancel the call.
|
|
485
560
|
def on_tool_confirmation(fn, strategy)
|
|
486
561
|
fn.cancel
|
|
@@ -518,12 +593,12 @@ module LLM
|
|
|
518
593
|
##
|
|
519
594
|
# @return [Array<LLM::Function::Return>]
|
|
520
595
|
def call_functions
|
|
521
|
-
strategy = concurrency || :
|
|
596
|
+
strategy = concurrency || :sequential
|
|
522
597
|
return wait(strategy) unless @confirm&.any?
|
|
523
|
-
confirmables = @ctx.
|
|
598
|
+
confirmables = @ctx.pending_functions.select { @confirm.include?(_1.name.to_s) }
|
|
524
599
|
results = confirmables.map { method(:on_tool_confirmation).call(_1, strategy) }
|
|
525
600
|
@ctx.method(:emit_tool_returns).call(confirmables, results)
|
|
526
|
-
if (@ctx.
|
|
601
|
+
if (@ctx.pending_functions - confirmables).any?
|
|
527
602
|
[*results, *wait(strategy, except: confirmables)]
|
|
528
603
|
else
|
|
529
604
|
results
|
|
@@ -541,13 +616,13 @@ module LLM
|
|
|
541
616
|
stream = params[:stream] || @ctx.params[:stream]
|
|
542
617
|
params[:stream] = LLM::Stream.try(stream, extra: {concurrency:})
|
|
543
618
|
res = talk.call(apply_instructions(prompt), params)
|
|
544
|
-
while @ctx.
|
|
619
|
+
while @ctx.pending_functions?
|
|
545
620
|
if max
|
|
546
621
|
max.times do
|
|
547
|
-
break unless @ctx.
|
|
622
|
+
break unless @ctx.pending_functions?
|
|
548
623
|
res = talk.call(call_functions, params)
|
|
549
624
|
end
|
|
550
|
-
res = talk.call(@ctx.
|
|
625
|
+
res = talk.call(@ctx.pending_functions.map(&:rate_limit), params) if @ctx.pending_functions?
|
|
551
626
|
else
|
|
552
627
|
res = talk.call(call_functions, params)
|
|
553
628
|
end
|