llm.rb 12.6.0 → 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 +387 -0
- data/LICENSE +21 -93
- data/README.md +46 -155
- 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_llm.rb +6 -6
- data/lib/llm/agent.rb +62 -23
- 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 +31 -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 +2 -2
- data/lib/llm/function/fork/task.rb +19 -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 +9 -3
- data/lib/llm/function/ractor/mailbox.rb +2 -0
- data/lib/llm/function/ractor/task.rb +23 -15
- data/lib/llm/function/{call_group.rb → sequential/group.rb} +12 -8
- data/lib/llm/function/sequential/task.rb +49 -0
- data/lib/llm/function/task.rb +25 -48
- data/lib/llm/function/thread/group.rb +46 -0
- data/lib/llm/function/thread/task.rb +60 -0
- data/lib/llm/function.rb +54 -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/plugin.rb +6 -6
- data/lib/llm/stream.rb +24 -17
- 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 +645 -58
- 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.
|
|
123
|
+
#### LLM::Schema
|
|
136
124
|
|
|
137
|
-
|
|
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.
|
|
138
129
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
142
134
|
|
|
143
135
|
```ruby
|
|
144
|
-
|
|
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
|
|
145
142
|
|
|
146
|
-
llm = LLM.
|
|
147
|
-
agent = LLM::Agent.new(llm)
|
|
148
|
-
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"}
|
|
149
147
|
```
|
|
150
148
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
The `path:` option accepts a file path where runtime state
|
|
154
|
-
is read from and written to. This lets you resume a
|
|
155
|
-
conversation across REPL sessions.
|
|
156
|
-
|
|
157
|
-
```ruby
|
|
158
|
-
require "llm"
|
|
159
|
-
|
|
160
|
-
llm = LLM.deepseek(key: ENV["KEY"])
|
|
161
|
-
agent = LLM::Agent.new(llm)
|
|
162
|
-
agent.repl(path: "session.json")
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
##### REPL: Tools
|
|
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)
|
|
193
|
-
```
|
|
194
|
-
|
|
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])
|
|
165
|
+
agent = LLM::Agent.new(llm, name: "my-agent")
|
|
166
|
+
agent.repl(path: "agent.json", tools: LLM::Tool.subclasses)
|
|
222
167
|
```
|
|
223
168
|
|
|
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
|
|
|
@@ -363,7 +270,8 @@ require "llm/active_record"
|
|
|
363
270
|
|
|
364
271
|
class Agent < ApplicationRecord
|
|
365
272
|
acts_as_agent
|
|
366
|
-
set
|
|
273
|
+
set name: "my-agent",
|
|
274
|
+
instructions: "solve the user's query",
|
|
367
275
|
model: "deepseek-v4-pro",
|
|
368
276
|
tools: [Research, FinalizeResearch, ActOnResearch]
|
|
369
277
|
|
|
@@ -481,22 +389,5 @@ and resources.
|
|
|
481
389
|
|
|
482
390
|
## License
|
|
483
391
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
Commercial production use requires a commercial license.
|
|
487
|
-
<br>
|
|
488
|
-
Each version converts to the [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
|
|
489
|
-
four years after its first public release.
|
|
490
|
-
<br>
|
|
491
|
-
Contact [robert@r.uby.dev](mailto:robert@r.uby.dev) for a commercial license.
|
|
492
|
-
|
|
493
|
-
### Waivers
|
|
494
|
-
|
|
495
|
-
Waivers are automatically granted for: <br>
|
|
496
|
-
|
|
497
|
-
* Personal use
|
|
498
|
-
* Students
|
|
499
|
-
* Teachers
|
|
500
|
-
* Evaluation, development, and testing
|
|
501
|
-
* Non-profits and charities
|
|
502
|
-
* 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"
|
|
@@ -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,10 +23,10 @@ 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
31
|
# set model: "gpt-4.1-nano",
|
|
31
32
|
# instructions: "You are a Linux system admin",
|
|
@@ -36,7 +37,21 @@ module LLM
|
|
|
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]
|
|
@@ -51,7 +66,8 @@ module LLM
|
|
|
51
66
|
#
|
|
52
67
|
# @example
|
|
53
68
|
# class AdminAgent < LLM::Agent
|
|
54
|
-
# set
|
|
69
|
+
# set name: "admin",
|
|
70
|
+
# instructions: "You are a system administrator",
|
|
55
71
|
# model: "gpt-4.1-nano",
|
|
56
72
|
# tools: [Shell, ReadFile]
|
|
57
73
|
# end
|
|
@@ -78,6 +94,20 @@ module LLM
|
|
|
78
94
|
end
|
|
79
95
|
end
|
|
80
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
|
+
|
|
81
111
|
##
|
|
82
112
|
# Set or get the default model
|
|
83
113
|
# @param [String, nil] model
|
|
@@ -146,9 +176,9 @@ module LLM
|
|
|
146
176
|
#
|
|
147
177
|
# @param [Symbol, Array<Symbol>, nil] concurrency
|
|
148
178
|
# Controls how pending tool loops are executed:
|
|
149
|
-
# - `:
|
|
179
|
+
# - `:sequential`: sequential calls
|
|
150
180
|
# - `:thread`: concurrent threads
|
|
151
|
-
# - `:
|
|
181
|
+
# - `:async`: concurrent async tasks
|
|
152
182
|
# - `:fiber`: concurrent scheduler-backed fibers
|
|
153
183
|
# - `:fork`: forked child processes
|
|
154
184
|
# - `:ractor`: concurrent Ruby ractors for class-based tools; MCP tools are not supported,
|
|
@@ -247,8 +277,8 @@ module LLM
|
|
|
247
277
|
# @option params [Symbol, Array<Symbol>, nil] :concurrency Defaults to the agent class concurrency
|
|
248
278
|
def initialize(llm, params = {})
|
|
249
279
|
@llm = llm
|
|
250
|
-
fields = %i[model skills schema tracer stream tools concurrency instructions confirm]
|
|
251
|
-
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]
|
|
252
282
|
fields.each do |field|
|
|
253
283
|
resolvable = params.key?(field) ? params.delete(field) : self.class.public_send(field)
|
|
254
284
|
resolve_symbol = !%i[concurrency].include?(field)
|
|
@@ -265,6 +295,13 @@ module LLM
|
|
|
265
295
|
@ctx = LLM::Context.new(llm, {guard: true}.merge(params))
|
|
266
296
|
end
|
|
267
297
|
|
|
298
|
+
##
|
|
299
|
+
# Returns the agent's name
|
|
300
|
+
# @return [String]
|
|
301
|
+
def name
|
|
302
|
+
@name
|
|
303
|
+
end
|
|
304
|
+
|
|
268
305
|
##
|
|
269
306
|
# Maintain a conversation via the chat completions API.
|
|
270
307
|
# This method immediately sends a request to the LLM and returns the response.
|
|
@@ -299,10 +336,9 @@ module LLM
|
|
|
299
336
|
|
|
300
337
|
##
|
|
301
338
|
# @return [Array<LLM::Function>]
|
|
302
|
-
def
|
|
303
|
-
@tracer ? @llm.with_tracer(@tracer) { @ctx.
|
|
339
|
+
def pending_functions
|
|
340
|
+
@tracer ? @llm.with_tracer(@tracer) { @ctx.pending_functions } : @ctx.pending_functions
|
|
304
341
|
end
|
|
305
|
-
alias_method :pending_functions, :functions
|
|
306
342
|
|
|
307
343
|
##
|
|
308
344
|
# @see LLM::Context#returns
|
|
@@ -435,6 +471,9 @@ module LLM
|
|
|
435
471
|
# By default this method disables the tracer for
|
|
436
472
|
# the duration of the repl session, and restores
|
|
437
473
|
# it afterwards.
|
|
474
|
+
# @param [String] name
|
|
475
|
+
# The agent's name.
|
|
476
|
+
# Defaults to {LLM::Agent#name}.
|
|
438
477
|
# @param [String] path
|
|
439
478
|
# The path to a file where runtime state is read
|
|
440
479
|
# from, and written to
|
|
@@ -446,7 +485,7 @@ module LLM
|
|
|
446
485
|
# When true, the tracer is kept alive during the
|
|
447
486
|
# repl session. Default is false.
|
|
448
487
|
# @return [void]
|
|
449
|
-
def repl(path: nil, tools: [], skills: [], tracer: false, trace: nil)
|
|
488
|
+
def repl(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
|
|
450
489
|
if trace != nil
|
|
451
490
|
warn "llm.rb: trace option is deprecated, use tracer instead"
|
|
452
491
|
tracer = trace
|
|
@@ -456,7 +495,7 @@ module LLM
|
|
|
456
495
|
self.tracer = nil
|
|
457
496
|
end
|
|
458
497
|
require_relative "repl" unless defined?(::LLM::Repl)
|
|
459
|
-
LLM::Repl.new(agent: self, path:, tools:, skills:).start
|
|
498
|
+
LLM::Repl.new(agent: self, name:, path:, tools:, skills:).start
|
|
460
499
|
ensure
|
|
461
500
|
if !tracer
|
|
462
501
|
self.tracer = previous
|
|
@@ -516,7 +555,7 @@ module LLM
|
|
|
516
555
|
# @param [Symbol, Array<Symbol>] strategy
|
|
517
556
|
# The execution strategy that would be used for the tool call.
|
|
518
557
|
# @return [LLM::Function::Return]
|
|
519
|
-
# Return either `fn.
|
|
558
|
+
# Return either `fn.task(strategy).wait` to approve execution or
|
|
520
559
|
# `fn.cancel(...)` to cancel the call.
|
|
521
560
|
def on_tool_confirmation(fn, strategy)
|
|
522
561
|
fn.cancel
|
|
@@ -554,12 +593,12 @@ module LLM
|
|
|
554
593
|
##
|
|
555
594
|
# @return [Array<LLM::Function::Return>]
|
|
556
595
|
def call_functions
|
|
557
|
-
strategy = concurrency || :
|
|
596
|
+
strategy = concurrency || :sequential
|
|
558
597
|
return wait(strategy) unless @confirm&.any?
|
|
559
|
-
confirmables = @ctx.
|
|
598
|
+
confirmables = @ctx.pending_functions.select { @confirm.include?(_1.name.to_s) }
|
|
560
599
|
results = confirmables.map { method(:on_tool_confirmation).call(_1, strategy) }
|
|
561
600
|
@ctx.method(:emit_tool_returns).call(confirmables, results)
|
|
562
|
-
if (@ctx.
|
|
601
|
+
if (@ctx.pending_functions - confirmables).any?
|
|
563
602
|
[*results, *wait(strategy, except: confirmables)]
|
|
564
603
|
else
|
|
565
604
|
results
|
|
@@ -577,13 +616,13 @@ module LLM
|
|
|
577
616
|
stream = params[:stream] || @ctx.params[:stream]
|
|
578
617
|
params[:stream] = LLM::Stream.try(stream, extra: {concurrency:})
|
|
579
618
|
res = talk.call(apply_instructions(prompt), params)
|
|
580
|
-
while @ctx.
|
|
619
|
+
while @ctx.pending_functions?
|
|
581
620
|
if max
|
|
582
621
|
max.times do
|
|
583
|
-
break unless @ctx.
|
|
622
|
+
break unless @ctx.pending_functions?
|
|
584
623
|
res = talk.call(call_functions, params)
|
|
585
624
|
end
|
|
586
|
-
res = talk.call(@ctx.
|
|
625
|
+
res = talk.call(@ctx.pending_functions.map(&:rate_limit), params) if @ctx.pending_functions?
|
|
587
626
|
else
|
|
588
627
|
res = talk.call(call_functions, params)
|
|
589
628
|
end
|
data/lib/llm/buffer.rb
CHANGED
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
module LLM
|
|
4
4
|
##
|
|
5
5
|
# {LLM::Buffer LLM::Buffer} provides an Enumerable object that
|
|
6
|
-
# tracks messages in a conversation thread.
|
|
6
|
+
# tracks messages in a conversation thread. Access it through
|
|
7
|
+
# {LLM::Context#messages}.
|
|
8
|
+
#
|
|
9
|
+
# @example Working with message history
|
|
10
|
+
# ctx.messages.last # => most recent message
|
|
11
|
+
# ctx.messages.first # => oldest message
|
|
12
|
+
# ctx.messages.select! { |m| m.assistant? }
|
|
13
|
+
# ctx.messages.reverse # => reversed copy
|
|
14
|
+
# ctx.messages.reject! { |m| m.compaction? }
|
|
15
|
+
#
|
|
16
|
+
# @see LLM::Message Individual messages in the buffer
|
|
17
|
+
# @see LLM::Context Where the buffer lives (ctx.messages)
|
|
7
18
|
class Buffer
|
|
8
19
|
include Enumerable
|
|
9
20
|
|
|
21
|
+
UNDEFINED = Object.new
|
|
22
|
+
private_constant :UNDEFINED
|
|
23
|
+
|
|
10
24
|
##
|
|
11
25
|
# @param [LLM::Provider] provider
|
|
12
26
|
# @return [LLM::Buffer]
|
|
@@ -65,8 +79,69 @@ module LLM
|
|
|
65
79
|
# @param [Integer, nil] n
|
|
66
80
|
# The number of messages to return
|
|
67
81
|
# @return [LLM::Message, Array<LLM::Message>, nil]
|
|
68
|
-
def last(n =
|
|
69
|
-
n.
|
|
82
|
+
def last(n = UNDEFINED)
|
|
83
|
+
n.equal?(UNDEFINED) ? @messages.last : @messages.last(n)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# Returns the first message(s) in the buffer
|
|
88
|
+
# @param [Integer, nil] n
|
|
89
|
+
# The number of messages to return
|
|
90
|
+
# @return [LLM::Message, Array<LLM::Message>, nil]
|
|
91
|
+
def first(n = UNDEFINED)
|
|
92
|
+
n.equal?(UNDEFINED) ? @messages.first : @messages.first(n)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# Removes messages matching the block in-place.
|
|
97
|
+
# @yield [LLM::Message]
|
|
98
|
+
# @return [LLM::Buffer]
|
|
99
|
+
def reject!(&)
|
|
100
|
+
@messages.reject!(&)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
alias_method :delete_if, :reject!
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Keeps messages matching the block in-place.
|
|
107
|
+
# @yield [LLM::Message]
|
|
108
|
+
# @return [LLM::Buffer]
|
|
109
|
+
def select!(&)
|
|
110
|
+
@messages.select!(&)
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
# Removes and returns the first message.
|
|
116
|
+
# @return [LLM::Message, nil]
|
|
117
|
+
def shift
|
|
118
|
+
@messages.shift
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Removes all messages.
|
|
123
|
+
# @return [LLM::Buffer]
|
|
124
|
+
def clear
|
|
125
|
+
@messages.clear
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Returns all elements after the first n.
|
|
131
|
+
# @param [Integer] n
|
|
132
|
+
# The number of messages to skip
|
|
133
|
+
# @return [Array<LLM::Message>]
|
|
134
|
+
def drop(n)
|
|
135
|
+
@messages.drop(n)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
##
|
|
139
|
+
# Returns the first n elements without removing them.
|
|
140
|
+
# @param [Integer] n
|
|
141
|
+
# The number of messages to return
|
|
142
|
+
# @return [Array<LLM::Message>]
|
|
143
|
+
def take(n)
|
|
144
|
+
@messages.take(n)
|
|
70
145
|
end
|
|
71
146
|
|
|
72
147
|
##
|
|
@@ -103,6 +178,13 @@ module LLM
|
|
|
103
178
|
@messages[index]
|
|
104
179
|
end
|
|
105
180
|
|
|
181
|
+
##
|
|
182
|
+
# Returns a reversed copy of the internal array.
|
|
183
|
+
# @return [Array]
|
|
184
|
+
def reverse
|
|
185
|
+
@messages.reverse
|
|
186
|
+
end
|
|
187
|
+
|
|
106
188
|
##
|
|
107
189
|
# @return [String]
|
|
108
190
|
def to_json(...)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Compactor
|
|
4
|
+
##
|
|
5
|
+
# An {LLM::Compactor::Null LLM::Compactor::Null} is a compactor that
|
|
6
|
+
# does nothing. It is used as the default when no compactor strategy
|
|
7
|
+
# is configured.
|
|
8
|
+
#
|
|
9
|
+
# All methods return nil and produce no side effects.
|
|
10
|
+
class Null < self
|
|
11
|
+
##
|
|
12
|
+
# @param [Hash] opts
|
|
13
|
+
# Ignored
|
|
14
|
+
# @return [nil]
|
|
15
|
+
def call(**opts)
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|