llm.rb 15.0.3 → 15.2.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 +433 -3
- data/README.md +188 -71
- data/bin/llm.rb +50 -7
- data/data/alibaba.json +912 -823
- data/data/anthropic.json +234 -187
- data/data/bedrock.json +3702 -2058
- data/data/deepinfra.json +1288 -951
- data/data/deepseek.json +87 -53
- data/data/google.json +670 -670
- data/data/mistral.json +501 -460
- data/data/moonshot.json +43 -248
- data/data/openai.json +1008 -914
- data/data/openrouter.json +14417 -0
- data/data/xai.json +213 -201
- data/data/zai.json +242 -149
- data/docs/deepdive/advanced/compaction.md +5 -5
- data/docs/deepdive/advanced/context.md +8 -6
- data/docs/deepdive/advanced/guard.md +2 -2
- data/docs/deepdive/features/builtin_tools.md +93 -22
- data/docs/deepdive/features/{repl.md → console.md} +28 -28
- data/docs/deepdive/features/database.md +3 -3
- data/docs/deepdive/fundamentals/agents.md +13 -12
- data/docs/deepdive/fundamentals/providers.md +91 -6
- data/docs/deepdive/fundamentals/skills.md +14 -6
- data/docs/deepdive/fundamentals/stream.md +4 -4
- data/docs/deepdive/fundamentals/tools.md +63 -31
- data/docs/deepdive/reference/cost.md +2 -2
- data/docs/deepdive/reference/model_registry.md +2 -2
- data/docs/deepdive/reference/tracer.md +15 -13
- data/docs/deepdive.md +2 -2
- data/lib/llm/active_record/acts_as_agent.rb +9 -5
- data/lib/llm/agent.rb +40 -15
- data/lib/llm/{repl → console}/bar.rb +3 -3
- data/lib/llm/{repl → console}/buffer.rb +24 -9
- data/lib/llm/{repl → console}/color.rb +2 -2
- data/lib/llm/{repl → console}/command.rb +12 -12
- data/lib/llm/{repl → console}/commands/exit.rb +4 -4
- data/lib/llm/{repl → console}/commands/help.rb +1 -1
- data/lib/llm/{repl/commands/compact.rb → console/commands/keep.rb} +11 -9
- data/lib/llm/{repl → console}/commands/model.rb +2 -2
- data/lib/llm/{repl → console}/input/cache.rb +2 -2
- data/lib/llm/{repl → console}/input/char.rb +2 -2
- data/lib/llm/{repl → console}/input/row.rb +1 -1
- data/lib/llm/{repl → console}/input.rb +18 -10
- data/lib/llm/console/markdown/parser.rb +78 -0
- data/lib/llm/{repl → console}/markdown/table.rb +8 -5
- data/lib/llm/{repl → console}/markdown.rb +13 -30
- data/lib/llm/console/node.rb +69 -0
- data/lib/llm/{repl → console}/status.rb +11 -11
- data/lib/llm/{repl → console}/stream.rb +36 -9
- data/lib/llm/{repl → console}/walker.rb +1 -1
- data/lib/llm/{repl → console}/window.rb +17 -17
- data/lib/llm/{repl.rb → console.rb} +39 -19
- data/lib/llm/context/deserializer.rb +2 -1
- data/lib/llm/context.rb +29 -13
- data/lib/llm/cost.rb +13 -0
- data/lib/llm/function/async/reactor.rb +20 -1
- data/lib/llm/function/fork/task.rb +14 -10
- data/lib/llm/function.rb +1 -1
- data/lib/llm/json_adapter.rb +40 -28
- data/lib/llm/message.rb +7 -0
- data/lib/llm/provider.rb +31 -10
- data/lib/llm/providers/alibaba.rb +1 -1
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/models.rb +2 -2
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/deepseek.rb +1 -1
- data/lib/llm/providers/google.rb +1 -1
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +2 -1
- data/lib/llm/providers/openai.rb +2 -1
- data/lib/llm/providers/openrouter.rb +87 -0
- data/lib/llm/schema/leaf.rb +34 -2
- data/lib/llm/schema.rb +4 -2
- data/lib/llm/sequel/agent.rb +9 -5
- data/lib/llm/skill.rb +7 -1
- data/lib/llm/stream.rb +8 -3
- data/lib/llm/tool/param.rb +5 -1
- data/lib/llm/tool.rb +5 -0
- data/lib/llm/tools/bundle.rb +53 -0
- data/lib/llm/tools/edit-file.rb +7 -2
- data/lib/llm/tools/exec.rb +78 -0
- data/lib/llm/tools/git.rb +27 -26
- data/lib/llm/tools/mkdir.rb +12 -19
- data/lib/llm/tools/read_file.rb +69 -9
- data/lib/llm/tools/rg.rb +20 -24
- data/lib/llm/tools/ruby.rb +17 -25
- data/lib/llm/tools/utils.rb +75 -2
- data/lib/llm/tools/write_file.rb +4 -1
- data/lib/llm/tracer/logger.rb +2 -2
- data/lib/llm/tracer/pretty_logger.rb +4 -4
- data/lib/llm/tracer/telemetry.rb +2 -2
- data/lib/llm/tracer.rb +33 -0
- data/lib/llm/transport/curb.rb +5 -3
- data/lib/llm/transport/http.rb +5 -2
- data/lib/llm/transport/persistent_http.rb +6 -4
- data/lib/llm/transport/utils.rb +8 -6
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +18 -12
- data/llm.gemspec +8 -8
- metadata +80 -37
- data/lib/llm/repl/node.rb +0 -44
- data/lib/llm/tools/shell.rb +0 -55
|
@@ -32,8 +32,8 @@ Tools that spawn subprocesses can include
|
|
|
32
32
|
[`LLM::Tool::Utils`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Utils.html)
|
|
33
33
|
to get shared
|
|
34
34
|
[`wait(command:, timeout:)`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Utils.html#wait-instance_method)
|
|
35
|
-
and `now` help. The built-in `
|
|
36
|
-
use it to kill a command that exceeds its `timeout`.
|
|
35
|
+
and `now` help. The built-in `Exec`, `Git`, `Rg`, `Mkdir`, and `Ruby`
|
|
36
|
+
tools use it to kill a command that exceeds its `timeout`.
|
|
37
37
|
|
|
38
38
|
If
|
|
39
39
|
[`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call)
|
|
@@ -44,22 +44,34 @@ also handle errors yourself inside
|
|
|
44
44
|
by rescuing and returning a domain-specific error hash.
|
|
45
45
|
|
|
46
46
|
```ruby
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
require "llm/tools/utils"
|
|
48
|
+
|
|
49
|
+
class Exec < LLM::Tool
|
|
50
|
+
include Utils
|
|
51
|
+
|
|
52
|
+
name "exec"
|
|
53
|
+
description "run a command without a shell"
|
|
54
|
+
parameter :name, String, "the command's name"
|
|
55
|
+
parameter :arguments, Array[String], "command args"
|
|
56
|
+
required %i[name]
|
|
57
|
+
defaults arguments: [], timeout: 60, max_bytes: :max_bytes
|
|
54
58
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
def self.max_bytes(bytes = nil)
|
|
60
|
+
bytes ? (@max_bytes = bytes) : (@max_bytes || 75_000)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def call(name:, arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
|
|
64
|
+
command = spawn(name:, arguments:, max_bytes:)
|
|
65
|
+
wait(command:, timeout:)
|
|
66
|
+
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
67
|
+
rescue LLM::Interrupt
|
|
68
|
+
command.kill! if command&.running?
|
|
69
|
+
raise
|
|
58
70
|
end
|
|
59
71
|
end
|
|
60
72
|
|
|
61
73
|
llm = LLM.deepseek(key: ENV["KEY"])
|
|
62
|
-
agent = LLM::Agent.new(llm, tools: [
|
|
74
|
+
agent = LLM::Agent.new(llm, tools: [Exec], stream: $stdout)
|
|
63
75
|
agent.talk "What files are in the current working directory?"
|
|
64
76
|
```
|
|
65
77
|
|
|
@@ -165,17 +177,26 @@ message: "boom"}` to the model. You can also rescue inside
|
|
|
165
177
|
and return your own error shape that gives the model more context.
|
|
166
178
|
|
|
167
179
|
```ruby
|
|
168
|
-
|
|
169
|
-
set name: "shell",
|
|
170
|
-
description: "run a shell command",
|
|
171
|
-
parameters: [
|
|
172
|
-
[:name, String, "the command name", {required: true}],
|
|
173
|
-
[:arguments, Array[String], "command args", {default: []}]
|
|
174
|
-
]
|
|
180
|
+
require "llm/tools/utils"
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
class Exec < LLM::Tool
|
|
183
|
+
include Utils
|
|
184
|
+
|
|
185
|
+
name "exec"
|
|
186
|
+
description "run a command without a shell"
|
|
187
|
+
parameter :name, String, "the command name"
|
|
188
|
+
parameter :arguments, Array[String], "command args"
|
|
189
|
+
required %i[name]
|
|
190
|
+
defaults arguments: [], timeout: 60, max_bytes: :max_bytes
|
|
191
|
+
|
|
192
|
+
def self.max_bytes(bytes = nil)
|
|
193
|
+
bytes ? (@max_bytes = bytes) : (@max_bytes || 75_000)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def call(name:, arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
|
|
197
|
+
command = spawn(name:, arguments:, max_bytes:)
|
|
198
|
+
wait(command:, timeout:)
|
|
199
|
+
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
179
200
|
rescue Errno::ENOENT
|
|
180
201
|
{ok: false, error: "command not found: #{name}"}
|
|
181
202
|
end
|
|
@@ -215,17 +236,28 @@ with a Hash. The keys match the individual method names. The
|
|
|
215
236
|
individual `parameter` method does.
|
|
216
237
|
|
|
217
238
|
```ruby
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
require "llm/tools/utils"
|
|
240
|
+
|
|
241
|
+
class Exec < LLM::Tool
|
|
242
|
+
include Utils
|
|
243
|
+
|
|
244
|
+
set name: "exec",
|
|
245
|
+
description: "run a command without a shell",
|
|
221
246
|
parameters: [
|
|
222
247
|
[:name, String, "the command's name", {required: true}],
|
|
223
|
-
[:arguments, Array[String], "
|
|
248
|
+
[:arguments, Array[String], "command args", {default: []}],
|
|
249
|
+
[:timeout, Integer, "timeout in seconds", {default: 60}],
|
|
250
|
+
[:max_bytes, Integer, "max bytes to emit", {default: 75_000}]
|
|
224
251
|
]
|
|
225
252
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
253
|
+
def self.max_bytes(bytes = nil)
|
|
254
|
+
bytes ? (@max_bytes = bytes) : (@max_bytes || 75_000)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def call(name:, arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
|
|
258
|
+
command = spawn(name:, arguments:, max_bytes:)
|
|
259
|
+
wait(command:, timeout:)
|
|
260
|
+
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
229
261
|
end
|
|
230
262
|
end
|
|
231
263
|
```
|
|
@@ -298,7 +330,7 @@ decide to retry or continue with the results it has.
|
|
|
298
330
|
|
|
299
331
|
#### Overview
|
|
300
332
|
|
|
301
|
-
llm.rb ships with
|
|
333
|
+
llm.rb ships with thirteen ready-made tools that cover filesystem,
|
|
302
334
|
search, and shell operations. Load them all with
|
|
303
335
|
`require "llm/tools"`. Each tool is documented in the
|
|
304
336
|
[built-in tools catalog](builtin_tools.md).
|
|
@@ -18,7 +18,7 @@ When you want to know what a conversation cost so far, call
|
|
|
18
18
|
[`LLM::Context#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#cost-instance_method)
|
|
19
19
|
(or
|
|
20
20
|
[`LLM::Agent#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#cost-instance_method))
|
|
21
|
-
and read the breakdown. The
|
|
21
|
+
and read the breakdown. The console shows this live in its status bar
|
|
22
22
|
after every turn:
|
|
23
23
|
|
|
24
24
|
```ruby
|
|
@@ -102,7 +102,7 @@ returns the total in a compact, human-friendly format, rounded to
|
|
|
102
102
|
two decimals (`"0.01"`). Components that were not used are `0`, so
|
|
103
103
|
you never need to guard against `nil` when aggregating.
|
|
104
104
|
|
|
105
|
-
The
|
|
105
|
+
The console renders context usage as a proportion, not a cost.
|
|
106
106
|
[`LLM::Context#context_usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#context_usage-instance_method)
|
|
107
107
|
returns a `Rational` of the tokens used over the context window
|
|
108
108
|
(for example `Rational(100, 10_000)`), or `nil` when the window is
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
The model registry is a catalog of every model each provider offers,
|
|
8
8
|
shipped with llm.rb under `data/<provider>.json`. The data is sourced
|
|
9
9
|
from [models.dev](https://models.dev) and powers cost estimation,
|
|
10
|
-
context-window limits, and the `/model` auto-complete in the
|
|
10
|
+
context-window limits, and the `/model` auto-complete in the console.
|
|
11
11
|
|
|
12
12
|
[`LLM::Registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html)
|
|
13
13
|
exposes that catalog, and
|
|
@@ -210,7 +210,7 @@ require "llm"
|
|
|
210
210
|
reg = LLM::Registry.for(:openai)
|
|
211
211
|
list = reg.models.sort
|
|
212
212
|
list.first.id # => "text-embedding-3-small"
|
|
213
|
-
list.last.id # => "
|
|
213
|
+
list.last.id # => "gpt-image-1-mini" (unpriced sorts last)
|
|
214
214
|
```
|
|
215
215
|
|
|
216
216
|
#### Why would I use it?
|
|
@@ -28,7 +28,7 @@ provider or an agent:
|
|
|
28
28
|
|
|
29
29
|
```ruby
|
|
30
30
|
llm = LLM.deepseek(key: ENV["KEY"])
|
|
31
|
-
llm.tracer = LLM::Tracer
|
|
31
|
+
llm.tracer = LLM::Tracer.pretty_logger(llm)
|
|
32
32
|
agent = LLM::Agent.new(llm)
|
|
33
33
|
agent.talk "Hello"
|
|
34
34
|
```
|
|
@@ -59,12 +59,14 @@ request a provider makes. Three built-in tracers are available:
|
|
|
59
59
|
[`LLM::Tracer::Telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Telemetry.html)
|
|
60
60
|
(OpenTelemetry).
|
|
61
61
|
|
|
62
|
-
For a shorter way to build
|
|
63
|
-
[`LLM::Tracer
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
For a shorter way to build the built-in tracers, use the
|
|
63
|
+
[`LLM::Tracer.logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#logger-class_method),
|
|
64
|
+
[`LLM::Tracer.pretty_logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#pretty_logger-class_method),
|
|
65
|
+
and
|
|
66
|
+
[`LLM::Tracer.telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#telemetry-class_method)
|
|
67
|
+
convenience methods. Each takes a provider and forwards its options to
|
|
68
|
+
the matching tracer, so `LLM::Tracer.pretty_logger(llm, io: $stdout)`
|
|
69
|
+
forwards `io:` to the pretty logger.
|
|
68
70
|
|
|
69
71
|
### Provider
|
|
70
72
|
|
|
@@ -86,7 +88,7 @@ consistent observability without configuring each agent individually.
|
|
|
86
88
|
|
|
87
89
|
```ruby
|
|
88
90
|
llm = LLM.deepseek(key: ENV["KEY"])
|
|
89
|
-
llm.tracer = LLM::Tracer
|
|
91
|
+
llm.tracer = LLM::Tracer.logger(llm, io: $stdout)
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
#### Why would I use it?
|
|
@@ -118,7 +120,7 @@ same provider unaffected.
|
|
|
118
120
|
|
|
119
121
|
```ruby
|
|
120
122
|
llm = LLM.deepseek(key: ENV["KEY"])
|
|
121
|
-
agent = LLM::Agent.new(llm, tracer: LLM::Tracer
|
|
123
|
+
agent = LLM::Agent.new(llm, tracer: LLM::Tracer.logger(llm, io: $stdout))
|
|
122
124
|
```
|
|
123
125
|
|
|
124
126
|
#### Why would I use it?
|
|
@@ -153,21 +155,21 @@ accepts an `io:` option to redirect output.
|
|
|
153
155
|
|
|
154
156
|
```ruby
|
|
155
157
|
llm = LLM.deepseek(key: ENV["KEY"])
|
|
156
|
-
llm.tracer = LLM::Tracer
|
|
158
|
+
llm.tracer = LLM::Tracer.pretty_logger(llm)
|
|
157
159
|
```
|
|
158
160
|
|
|
159
161
|
##### Agent-local
|
|
160
162
|
|
|
161
163
|
```ruby
|
|
162
164
|
llm = LLM.deepseek(key: ENV["KEY"])
|
|
163
|
-
agent = LLM::Agent.new(llm, tracer: LLM::Tracer
|
|
165
|
+
agent = LLM::Agent.new(llm, tracer: LLM::Tracer.pretty_logger(llm))
|
|
164
166
|
```
|
|
165
167
|
|
|
166
168
|
##### Custom output
|
|
167
169
|
|
|
168
170
|
```ruby
|
|
169
|
-
tracer = LLM::Tracer
|
|
170
|
-
tracer = LLM::Tracer
|
|
171
|
+
tracer = LLM::Tracer.pretty_logger(llm, io: $stdout)
|
|
172
|
+
tracer = LLM::Tracer.pretty_logger(llm, path: "trace.log")
|
|
171
173
|
```
|
|
172
174
|
|
|
173
175
|
#### Why would I use it?
|
data/docs/deepdive.md
CHANGED
|
@@ -37,7 +37,7 @@ code, `#### Why would I use it?` explains the use case, and
|
|
|
37
37
|
#### Why would I use it?
|
|
38
38
|
|
|
39
39
|
The deepdive documents everything there is to know about llm.rb.
|
|
40
|
-
It includes the
|
|
40
|
+
It includes the fundamentals, advanced patterns, configuration
|
|
41
41
|
options, ORM support, protocol support, and edge cases. It is
|
|
42
42
|
useful when you need to go beyond the basics.
|
|
43
43
|
|
|
@@ -69,7 +69,7 @@ is the best place to start if you are new to llm.rb.
|
|
|
69
69
|
- [Concurrency](deepdive/features/concurrency.md)
|
|
70
70
|
- [Embeddings](deepdive/features/embeddings.md)
|
|
71
71
|
- [Database](deepdive/features/database.md)
|
|
72
|
-
- [
|
|
72
|
+
- [Console](deepdive/features/console.md)
|
|
73
73
|
|
|
74
74
|
## Advanced
|
|
75
75
|
|
|
@@ -20,7 +20,10 @@ module LLM::ActiveRecord
|
|
|
20
20
|
##
|
|
21
21
|
# @return [Class<LLM::Agent>]
|
|
22
22
|
def agent
|
|
23
|
-
@agent
|
|
23
|
+
return @agent if defined?(@agent)
|
|
24
|
+
@agent = Class.new(LLM::Agent)
|
|
25
|
+
@agent.name(self)
|
|
26
|
+
@agent
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
##
|
|
@@ -112,11 +115,12 @@ module LLM::ActiveRecord
|
|
|
112
115
|
# This method does not persist to the database,
|
|
113
116
|
# but it can inspect and alter runtime state in
|
|
114
117
|
# a way that is temporary.
|
|
115
|
-
# @param (see LLM::Agent#
|
|
116
|
-
# @return (see LLM::Agent#
|
|
117
|
-
def
|
|
118
|
-
ctx.
|
|
118
|
+
# @param (see LLM::Agent#console)
|
|
119
|
+
# @return (see LLM::Agent#console)
|
|
120
|
+
def console(**params)
|
|
121
|
+
ctx.console(**params)
|
|
119
122
|
end
|
|
123
|
+
alias_method :repl, :console
|
|
120
124
|
|
|
121
125
|
private
|
|
122
126
|
|
data/lib/llm/agent.rb
CHANGED
|
@@ -128,7 +128,7 @@ module LLM
|
|
|
128
128
|
# Set or get an agent's name
|
|
129
129
|
# @note
|
|
130
130
|
# This method serves as a self-documenting string
|
|
131
|
-
# and it is used by {LLM::
|
|
131
|
+
# and it is used by {LLM::Console LLM::Console}. It is
|
|
132
132
|
# optional but recommended.
|
|
133
133
|
# @param [String] name
|
|
134
134
|
# The agent name
|
|
@@ -143,7 +143,12 @@ module LLM
|
|
|
143
143
|
@name
|
|
144
144
|
end
|
|
145
145
|
else
|
|
146
|
-
|
|
146
|
+
if Class === name
|
|
147
|
+
name = name.to_s.split("::").last
|
|
148
|
+
@name = name.gsub(CASE_PATTERN, "-").downcase
|
|
149
|
+
else
|
|
150
|
+
@name = block || name
|
|
151
|
+
end
|
|
147
152
|
end
|
|
148
153
|
end
|
|
149
154
|
|
|
@@ -292,7 +297,7 @@ module LLM
|
|
|
292
297
|
#
|
|
293
298
|
# @example
|
|
294
299
|
# class Agent < LLM::Agent
|
|
295
|
-
# tracer { LLM::Tracer
|
|
300
|
+
# tracer { LLM::Tracer.logger(llm, io: $stdout) }
|
|
296
301
|
# end
|
|
297
302
|
#
|
|
298
303
|
# @param [LLM::Tracer, Proc, nil] tracer
|
|
@@ -372,7 +377,7 @@ module LLM
|
|
|
372
377
|
# @return [Integer, nil]
|
|
373
378
|
def self.retry_budget(budget = UNDEFINED)
|
|
374
379
|
if budget.equal?(UNDEFINED)
|
|
375
|
-
@retry_budget.nil? ?
|
|
380
|
+
@retry_budget.nil? ? UNDEFINED : @retry_budget
|
|
376
381
|
else
|
|
377
382
|
@retry_budget = budget
|
|
378
383
|
end
|
|
@@ -404,7 +409,11 @@ module LLM
|
|
|
404
409
|
@llm = llm
|
|
405
410
|
fields, fields_ivar = FIELDS, IVARS
|
|
406
411
|
fields.each do |field|
|
|
407
|
-
resolvable = params.key?(field)
|
|
412
|
+
resolvable = if params.key?(field)
|
|
413
|
+
params.delete(field)
|
|
414
|
+
else
|
|
415
|
+
(self.class.respond_to?(field) ? self.class.public_send(field) : nil)
|
|
416
|
+
end
|
|
408
417
|
resolve_symbol = !%i[concurrency].include?(field)
|
|
409
418
|
resolved = resolvable != nil ? resolve_option(self, resolvable, resolve_symbol:) : resolvable
|
|
410
419
|
resolved = [*resolved].map(&:to_s) if field == :confirm && resolved
|
|
@@ -416,6 +425,13 @@ module LLM
|
|
|
416
425
|
instance_variable_set(:"@#{field}", resolved)
|
|
417
426
|
end
|
|
418
427
|
end
|
|
428
|
+
##
|
|
429
|
+
# Alibaba (token plan) will frequently issue rate
|
|
430
|
+
# limits or time outs that it recovers from. The
|
|
431
|
+
# higher retry count is to account for scenarios
|
|
432
|
+
# where it takes longer than expected to recover.
|
|
433
|
+
retry_budget = llm.name == :alibaba ? 8 : 5
|
|
434
|
+
params[:retry_budget] = retry_budget if params[:retry_budget].equal?(UNDEFINED)
|
|
419
435
|
@ctx = LLM::Context.new(llm, {guard: LLM::Guard::Loop}.merge(params))
|
|
420
436
|
@path and File.readable?(@path) ? @ctx.restore(path:) : nil
|
|
421
437
|
end
|
|
@@ -486,6 +502,12 @@ module LLM
|
|
|
486
502
|
@ctx.messages
|
|
487
503
|
end
|
|
488
504
|
|
|
505
|
+
##
|
|
506
|
+
# @return [Integer]
|
|
507
|
+
def retry_budget
|
|
508
|
+
@ctx.retry_budget
|
|
509
|
+
end
|
|
510
|
+
|
|
489
511
|
##
|
|
490
512
|
# @return [Array<LLM::Function>]
|
|
491
513
|
def pending_functions
|
|
@@ -644,14 +666,16 @@ module LLM
|
|
|
644
666
|
end
|
|
645
667
|
|
|
646
668
|
##
|
|
647
|
-
# Start
|
|
669
|
+
# Start an agent console that can interact
|
|
648
670
|
# with the agent and its current state. This
|
|
649
|
-
# method requires the
|
|
650
|
-
# and available to require
|
|
671
|
+
# method requires the following gems to be
|
|
672
|
+
# installed and available to require:
|
|
673
|
+
# 'unicode-display_width', 'kramdown', 'curses',
|
|
674
|
+
# 'test-cmd.rb', and 'xchan.rb'.
|
|
651
675
|
#
|
|
652
676
|
# @note
|
|
653
677
|
# By default this method disables the tracer for
|
|
654
|
-
# the duration of the
|
|
678
|
+
# the duration of the console session, and restores
|
|
655
679
|
# it afterwards.
|
|
656
680
|
# @param [String] name
|
|
657
681
|
# The agent's name.
|
|
@@ -660,14 +684,14 @@ module LLM
|
|
|
660
684
|
# The path to a file where runtime state is read
|
|
661
685
|
# from, and written to
|
|
662
686
|
# @param [Array<LLM::Tool>] tools
|
|
663
|
-
# Extra tools to attach for the
|
|
687
|
+
# Extra tools to attach for the console session
|
|
664
688
|
# @param [Array<String>] skills
|
|
665
|
-
# Extra skills to attach for the
|
|
689
|
+
# Extra skills to attach for the console session
|
|
666
690
|
# @param [Boolean] tracer
|
|
667
691
|
# When true, the tracer is kept alive during the
|
|
668
|
-
#
|
|
692
|
+
# console session. Default is false.
|
|
669
693
|
# @return [void]
|
|
670
|
-
def
|
|
694
|
+
def console(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
|
|
671
695
|
if trace != nil
|
|
672
696
|
warn "llm.rb: trace option is deprecated, use tracer instead"
|
|
673
697
|
tracer = trace
|
|
@@ -676,13 +700,14 @@ module LLM
|
|
|
676
700
|
previous = self.tracer
|
|
677
701
|
self.tracer = nil
|
|
678
702
|
end
|
|
679
|
-
require_relative "
|
|
680
|
-
LLM::
|
|
703
|
+
require_relative "console" unless defined?(::LLM::Console)
|
|
704
|
+
LLM::Console.new(agent: self, name:, path:, tools:, skills:).start
|
|
681
705
|
ensure
|
|
682
706
|
if !tracer
|
|
683
707
|
self.tracer = previous
|
|
684
708
|
end
|
|
685
709
|
end
|
|
710
|
+
alias_method :repl, :console
|
|
686
711
|
|
|
687
712
|
##
|
|
688
713
|
# @see LLM::Context#params
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class LLM::
|
|
3
|
+
class LLM::Console
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
5
|
+
# The {LLM::Console::Bar LLM::Console::Bar} class renders a
|
|
6
6
|
# small progress bar for the REPL. It is used to show
|
|
7
7
|
# the remaining size of the model's context window in
|
|
8
8
|
# a compact form near the input line.
|
|
@@ -21,7 +21,7 @@ class LLM::Repl
|
|
|
21
21
|
# The fraction of the context window used, or nil when unknown
|
|
22
22
|
# (eg after compaction).
|
|
23
23
|
# @param [Integer] width
|
|
24
|
-
# @return [LLM::
|
|
24
|
+
# @return [LLM::Console::Bar]
|
|
25
25
|
def initialize(fraction:, width: 10)
|
|
26
26
|
@width = width
|
|
27
27
|
@label, @filled = remainder(fraction)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class LLM::
|
|
3
|
+
class LLM::Console
|
|
4
4
|
##
|
|
5
5
|
# This class maintains conversation state that includes
|
|
6
6
|
# the conversation itself, and metadata associated with
|
|
@@ -16,9 +16,9 @@ class LLM::Repl
|
|
|
16
16
|
# row by overwriting its contents repeatedly.
|
|
17
17
|
class Buffer
|
|
18
18
|
##
|
|
19
|
-
# @param [LLM::
|
|
20
|
-
# An instance of {LLM::
|
|
21
|
-
# @return [LLM::
|
|
19
|
+
# @param [LLM::Console] console
|
|
20
|
+
# An instance of {LLM::Console LLM::Console}.
|
|
21
|
+
# @return [LLM::Console::Buffer]
|
|
22
22
|
def initialize(repl)
|
|
23
23
|
@repl = repl
|
|
24
24
|
@window = repl.window
|
|
@@ -147,15 +147,15 @@ class LLM::Repl
|
|
|
147
147
|
if token == "\n"
|
|
148
148
|
rows << []
|
|
149
149
|
elsif token.match?(/\s/)
|
|
150
|
-
if sum(rows.last) + token
|
|
150
|
+
if sum(rows.last) + Node.width(token) <= width
|
|
151
151
|
rows.last << Node.new(token, attrs)
|
|
152
152
|
end
|
|
153
|
-
elsif sum(rows.last) + token
|
|
153
|
+
elsif sum(rows.last) + Node.width(token) > width
|
|
154
154
|
if sum(rows.last) == 0
|
|
155
155
|
##
|
|
156
156
|
# A single word wider than the row: break it
|
|
157
|
-
# every width
|
|
158
|
-
token
|
|
157
|
+
# every width columns.
|
|
158
|
+
slices(token, width).each do |piece|
|
|
159
159
|
rows.last << Node.new(piece, attrs)
|
|
160
160
|
if sum(rows.last) >= width
|
|
161
161
|
rows << []
|
|
@@ -190,7 +190,22 @@ class LLM::Repl
|
|
|
190
190
|
##
|
|
191
191
|
# @api private
|
|
192
192
|
def sum(row)
|
|
193
|
-
row.sum { _1
|
|
193
|
+
row.sum { _1.size }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
##
|
|
197
|
+
# Splits a single word into pieces that each fit within the
|
|
198
|
+
# buffer's width, measured in display columns.
|
|
199
|
+
# @api private
|
|
200
|
+
def slices(token, width)
|
|
201
|
+
pieces = []
|
|
202
|
+
remaining = token
|
|
203
|
+
until remaining.empty?
|
|
204
|
+
piece = Node.slice(remaining, width)
|
|
205
|
+
pieces << piece
|
|
206
|
+
remaining = remaining[piece.length..] || +""
|
|
207
|
+
end
|
|
208
|
+
pieces
|
|
194
209
|
end
|
|
195
210
|
end
|
|
196
211
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class LLM::
|
|
3
|
+
class LLM::Console
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
5
|
+
# The {LLM::Console::Color LLM::Console::Color} module returns
|
|
6
6
|
# bitmasks that are understood as colors by the Curses
|
|
7
7
|
# library. They can be bitwise OR'ed with other attributes,
|
|
8
8
|
# such as Curses::A_BOLD.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class LLM::
|
|
3
|
+
class LLM::Console
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
5
|
+
# The {LLM::Console::Command LLM::Console::Command} class is the superclass
|
|
6
6
|
# of all read-eval-print loop commands. A command has a name, and a
|
|
7
7
|
# description. This basic version does not implement parameters. A
|
|
8
8
|
# command is accessible via the `/` prefix: eg `/exit`.
|
|
@@ -57,9 +57,9 @@ class LLM::Repl
|
|
|
57
57
|
##
|
|
58
58
|
# Find a command by a name, or by an input string.
|
|
59
59
|
# @example find by name
|
|
60
|
-
# LLM::
|
|
60
|
+
# LLM::Console::Command.find_by(name: "exit")
|
|
61
61
|
# @example find by input string
|
|
62
|
-
# LLM::
|
|
62
|
+
# LLM::Console::Command.find_by(input: "/exit")
|
|
63
63
|
# @note
|
|
64
64
|
# The input string must be prefixed with "/"
|
|
65
65
|
# or it won't be matched. The match is made
|
|
@@ -68,7 +68,7 @@ class LLM::Repl
|
|
|
68
68
|
# but "/exitnow" will not.
|
|
69
69
|
# @param [String] input
|
|
70
70
|
# @param [String] name
|
|
71
|
-
# @return [LLM::
|
|
71
|
+
# @return [LLM::Console::Command, nil]
|
|
72
72
|
def self.find_by(input: UNDEFINED, name: UNDEFINED)
|
|
73
73
|
if input != UNDEFINED
|
|
74
74
|
return nil unless input[0] == "/"
|
|
@@ -82,7 +82,7 @@ class LLM::Repl
|
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
##
|
|
85
|
-
# @param [LLM::
|
|
85
|
+
# @param [LLM::Console::Command] outer
|
|
86
86
|
# A new subclass
|
|
87
87
|
# @return [void]
|
|
88
88
|
def self.inherited(outer)
|
|
@@ -99,7 +99,7 @@ class LLM::Repl
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
##
|
|
102
|
-
# @return [Array<LLM::
|
|
102
|
+
# @return [Array<LLM::Console::Command]
|
|
103
103
|
def self.registry
|
|
104
104
|
@registry.transform_keys(&:name)
|
|
105
105
|
end
|
|
@@ -160,7 +160,7 @@ class LLM::Repl
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
##
|
|
163
|
-
# @return [LLM::
|
|
163
|
+
# @return [LLM::Console]
|
|
164
164
|
attr_reader :repl
|
|
165
165
|
|
|
166
166
|
##
|
|
@@ -168,8 +168,8 @@ class LLM::Repl
|
|
|
168
168
|
attr_reader :agent
|
|
169
169
|
|
|
170
170
|
##
|
|
171
|
-
# @param [LLM::
|
|
172
|
-
# @return [LLM::
|
|
171
|
+
# @param [LLM::Console] console
|
|
172
|
+
# @return [LLM::Console::Command]
|
|
173
173
|
def initialize(repl)
|
|
174
174
|
@repl = repl
|
|
175
175
|
@agent = repl.agent
|
|
@@ -235,7 +235,7 @@ class LLM::Repl
|
|
|
235
235
|
self.class.parameters
|
|
236
236
|
end
|
|
237
237
|
|
|
238
|
-
require_relative "commands/
|
|
238
|
+
require_relative "commands/keep"
|
|
239
239
|
require_relative "commands/exit"
|
|
240
240
|
require_relative "commands/help"
|
|
241
241
|
require_relative "commands/model"
|
|
@@ -244,4 +244,4 @@ end
|
|
|
244
244
|
|
|
245
245
|
##
|
|
246
246
|
# Convenience constant
|
|
247
|
-
LLM::Command = LLM::
|
|
247
|
+
LLM::Command = LLM::Console::Command
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class LLM::
|
|
3
|
+
class LLM::Console
|
|
4
4
|
##
|
|
5
|
-
# The 'exit' command exits the
|
|
6
|
-
# by throwing. The {LLM::
|
|
5
|
+
# The 'exit' command exits the interactive console
|
|
6
|
+
# by throwing. The {LLM::Console LLM::Console} class covers
|
|
7
7
|
# the loop with a catch that gracefully recovers and
|
|
8
8
|
# exits the loop.
|
|
9
9
|
class Command::Exit < Command
|
|
10
10
|
name "exit"
|
|
11
|
-
description "exits the
|
|
11
|
+
description "exits the console"
|
|
12
12
|
|
|
13
13
|
##
|
|
14
14
|
# @return [void]
|