llm.rb 16.0.0 → 17.0.1
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 +535 -2
- data/README.md +857 -501
- data/data/alibaba.json +2 -2
- data/data/anthropic.json +66 -3
- data/data/bedrock.json +781 -12
- data/data/deepinfra.json +157 -1
- data/data/google.json +48 -0
- data/data/mistral.json +326 -16
- data/data/moonshot.json +2 -1
- data/data/openai.json +13 -0
- data/data/openrouter.json +1078 -1235
- data/data/xai.json +4 -2
- data/docs/deepdive/advanced/cancellation.md +35 -7
- data/docs/deepdive/advanced/compaction.md +5 -5
- data/docs/deepdive/advanced/context.md +45 -24
- data/docs/deepdive/advanced/{guard.md → intercept.md} +87 -83
- data/docs/deepdive/features/builtin_tools.md +1 -1
- data/docs/deepdive/features/concurrency.md +15 -10
- data/docs/deepdive/features/database.md +19 -19
- data/docs/deepdive/features/embeddings.md +4 -4
- data/docs/deepdive/features/files.md +7 -5
- data/docs/deepdive/fundamentals/agents.md +13 -8
- data/docs/deepdive/fundamentals/providers.md +1 -1
- data/docs/deepdive/fundamentals/schema.md +2 -2
- data/docs/deepdive/fundamentals/stream.md +8 -8
- data/docs/deepdive/fundamentals/tools.md +14 -14
- data/docs/deepdive/media/ocr.md +1 -1
- data/docs/deepdive/protocols/a2a.md +3 -3
- data/docs/deepdive/protocols/mcp.md +116 -7
- data/docs/deepdive/reference/cost.md +1 -1
- data/docs/deepdive/reference/response.md +1 -1
- data/docs/deepdive/reference/tracer.md +90 -22
- data/docs/deepdive.md +1 -1
- data/lib/llm/agent/interrupt.rb +35 -46
- data/lib/llm/agent/registry.rb +106 -0
- data/lib/llm/agent.rb +242 -268
- data/lib/llm/context.rb +63 -55
- data/lib/llm/function/array.rb +2 -1
- data/lib/llm/function/async/group.rb +2 -2
- data/lib/llm/function/async/task.rb +25 -48
- data/lib/llm/function/fiber/group.rb +2 -2
- data/lib/llm/function/fiber/task.rb +16 -30
- data/lib/llm/function/fork/group.rb +2 -2
- data/lib/llm/function/fork/job.rb +8 -4
- data/lib/llm/function/fork/task.rb +25 -30
- data/lib/llm/function/group.rb +33 -5
- data/lib/llm/function/ractor/group.rb +2 -2
- data/lib/llm/function/ractor/job.rb +8 -6
- data/lib/llm/function/ractor/task.rb +32 -9
- data/lib/llm/function/sequential/group.rb +2 -1
- data/lib/llm/function/sequential/task.rb +8 -1
- data/lib/llm/function/task.rb +52 -4
- data/lib/llm/function/thread/group.rb +2 -2
- data/lib/llm/function/thread/task.rb +36 -10
- data/lib/llm/function/window.rb +16 -0
- data/lib/llm/function.rb +14 -43
- data/lib/llm/intercept/multi.rb +40 -0
- data/lib/llm/intercept/null.rb +18 -0
- data/lib/llm/intercept.rb +96 -0
- data/lib/llm/message.rb +1 -1
- data/lib/llm/provider.rb +16 -1
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -1
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -1
- data/lib/llm/providers/deepseek/files.rb +49 -0
- data/lib/llm/providers/deepseek/request_adapter/completion.rb +15 -13
- data/lib/llm/providers/deepseek.rb +16 -2
- data/lib/llm/providers/google/stream_parser.rb +1 -1
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -1
- data/lib/llm/providers/openai/stream_parser.rb +1 -1
- data/lib/llm/stream.rb +8 -7
- data/lib/llm/tool.rb +1 -1
- data/lib/llm/tools/utils.rb +1 -1
- data/lib/llm/tracer/logger.rb +42 -1
- data/lib/llm/tracer/multi.rb +144 -0
- data/lib/llm/tracer/null.rb +20 -0
- data/lib/llm/tracer/pretty_logger.rb +33 -2
- data/lib/llm/tracer/rescue.rb +110 -0
- data/lib/llm/tracer/telemetry.rb +69 -4
- data/lib/llm/tracer.rb +125 -48
- data/lib/llm/utils.rb +12 -2
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +36 -1
- data/llm.gemspec +9 -4
- metadata +16 -35
- data/lib/llm/function/tracing.rb +0 -21
- data/lib/llm/guard/null.rb +0 -19
- data/lib/llm/guard.rb +0 -55
data/README.md
CHANGED
|
@@ -18,16 +18,9 @@ llm.rb is a runtime for building agentic AI applications
|
|
|
18
18
|
on CRuby. It has zero runtime dependencies by default, supports
|
|
19
19
|
concurrent and parallel tool execution and has a single coherent API
|
|
20
20
|
that spans 14+ providers.
|
|
21
|
-
The README covers
|
|
22
|
-
[
|
|
23
|
-
|
|
24
|
-
[changelog](CHANGELOG.md).
|
|
25
|
-
|
|
26
|
-
[The r.uby.dev website](https://r.uby.dev) hosts
|
|
27
|
-
an agentic platform that provides users with
|
|
28
|
-
personalized agents who can access GitHub, and
|
|
29
|
-
other services. It is built with llm.rb. Check it
|
|
30
|
-
out if curious. **Still in early development.**
|
|
21
|
+
The README covers a lot of ground and the
|
|
22
|
+
[changelog](CHANGELOG.md)
|
|
23
|
+
tracks what has changed between releases.
|
|
31
24
|
|
|
32
25
|
## Install
|
|
33
26
|
|
|
@@ -46,101 +39,191 @@ The
|
|
|
46
39
|
class is the default high-level interface,
|
|
47
40
|
and it is recommended for most use-cases. It manages the tool loop
|
|
48
41
|
and provides configurable features on top of it. For example you can
|
|
49
|
-
manage the tool loop with a retry budget
|
|
50
|
-
|
|
42
|
+
manage the tool loop with a retry budget and a tool call budget -
|
|
43
|
+
alongside other features.
|
|
51
44
|
|
|
52
45
|
The runtime is designed to keep the tool loop alive and it will
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
model
|
|
56
|
-
|
|
57
|
-
A
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
rescue exceptions. When an exception is encountered in a tool it
|
|
47
|
+
is reported back to the model as an in-band error that allows the
|
|
48
|
+
model to change course or retry with different parameters.
|
|
49
|
+
|
|
50
|
+
A tool call requires a tool return (or response), and the lack of
|
|
51
|
+
one can corrupt the conversation and lead to API-level errors from
|
|
52
|
+
a provider. But sometimes it is unavoidable (for example, via an
|
|
53
|
+
interrupt or power loss) so the runtime automatically closes
|
|
54
|
+
tool calls that fall into that category by telling the model
|
|
55
|
+
the tool call(s) were cancelled.
|
|
56
|
+
|
|
57
|
+
##### helloworld.rb
|
|
58
|
+
|
|
59
|
+
A classic "hello world" example. <br> The environment variable
|
|
60
|
+
`$DEEPSEEK_API_KEY` is assumed to have been set and reference
|
|
61
|
+
a valid DeepSeek API key, otherwise you can provide a key explicitly
|
|
62
|
+
(eg: `LLM.deepseek(key: ENV["MYKEY"])`) and you could swap
|
|
63
|
+
deepseek for any of the other 13 providers as well
|
|
64
|
+
(eg: `llm = LLM.openai`):
|
|
66
65
|
|
|
67
66
|
```ruby
|
|
68
67
|
require "llm"
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
class GetTime < LLM::Tool
|
|
70
|
+
name "get-time"
|
|
71
|
+
description "returns the current time"
|
|
72
|
+
|
|
73
|
+
def call
|
|
74
|
+
{now: Time.now}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
llm = LLM.deepseek
|
|
79
|
+
tracer = LLM::Tracer.pretty(llm)
|
|
80
|
+
tools = [GetTime]
|
|
81
|
+
stream = StringIO.new
|
|
82
|
+
|
|
83
|
+
agent = LLM::Agent.new(llm, tools:, tracer:, stream:)
|
|
72
84
|
agent.talk "hello world"
|
|
85
|
+
agent.talk "what time is it?"
|
|
86
|
+
puts stream.string
|
|
73
87
|
```
|
|
88
|
+
|
|
89
|
+
#### Essentials
|
|
90
|
+
|
|
74
91
|
<details>
|
|
75
92
|
<summary>Stream</summary>
|
|
76
93
|
<br>
|
|
77
94
|
|
|
78
|
-
|
|
79
|
-
[`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
A stream can be a simple IO object (eg `$stdout`)
|
|
96
|
+
or it can be a subclass of [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html).
|
|
97
|
+
An IO object can receive content but it cannot
|
|
98
|
+
receive the other callbacks that are available
|
|
99
|
+
to subclasses of [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html).
|
|
100
|
+
It is generally a good idea to return as
|
|
101
|
+
quickly as possible from all of these methods,
|
|
102
|
+
but especially for the `on_content` and
|
|
103
|
+
`on_reasoning_content` methods: they run
|
|
104
|
+
inline with the request.
|
|
105
|
+
|
|
106
|
+
A subclass of [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
|
|
107
|
+
can implement callbacks that the runtime will call
|
|
108
|
+
throughout an agent's lifetime. All callbacks are
|
|
109
|
+
optional. The callbacks provide for content, reasoning,
|
|
110
|
+
tool calls, tool returns, steps in a turn, retries,
|
|
111
|
+
compaction and more:
|
|
84
112
|
|
|
85
113
|
```ruby
|
|
86
|
-
class
|
|
87
|
-
|
|
114
|
+
class Stream < LLM::Stream
|
|
115
|
+
##
|
|
116
|
+
# @param [String] content
|
|
117
|
+
# A chunk of text
|
|
88
118
|
def on_content(content)
|
|
89
119
|
print content
|
|
90
120
|
end
|
|
91
121
|
|
|
92
|
-
|
|
122
|
+
##
|
|
123
|
+
# @param [String] content
|
|
124
|
+
# A chunk of text
|
|
93
125
|
def on_reasoning_content(content)
|
|
94
126
|
warn content
|
|
95
127
|
end
|
|
96
128
|
|
|
97
|
-
|
|
129
|
+
##
|
|
130
|
+
# @param [LLM::Function] tool
|
|
131
|
+
# The tool being called
|
|
98
132
|
def on_tool_call(tool)
|
|
133
|
+
nil
|
|
99
134
|
end
|
|
100
135
|
|
|
101
|
-
|
|
136
|
+
##
|
|
137
|
+
# @param [LLM::Function] tool
|
|
138
|
+
# The tool that returned
|
|
139
|
+
# @param [LLM::Function::Return] result
|
|
140
|
+
# The return from the tool call
|
|
102
141
|
def on_tool_return(tool, result)
|
|
142
|
+
nil
|
|
103
143
|
end
|
|
104
144
|
|
|
105
|
-
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# Before a transformer rewrites an outgoing message.
|
|
145
|
+
##
|
|
146
|
+
# @note
|
|
147
|
+
# This method is called _before_ a transformer runs
|
|
148
|
+
# @param [LLM::Transformer] transformer
|
|
149
|
+
# A transformer
|
|
111
150
|
def on_transform(transformer)
|
|
151
|
+
nil
|
|
112
152
|
end
|
|
113
153
|
|
|
114
|
-
|
|
154
|
+
##
|
|
155
|
+
# @note
|
|
156
|
+
# This method is called _after_ a transformer runs
|
|
157
|
+
# @param [LLM::Transformer] transformer
|
|
158
|
+
# A transformer
|
|
115
159
|
def on_transform_finish(transformer)
|
|
160
|
+
nil
|
|
116
161
|
end
|
|
117
162
|
|
|
118
|
-
|
|
163
|
+
##
|
|
164
|
+
# @note
|
|
165
|
+
# This method is called _before_ a compactor runs
|
|
166
|
+
# @param [LLM::Compactor] compactor
|
|
167
|
+
# A compactor
|
|
119
168
|
def on_compaction(compactor)
|
|
169
|
+
nil
|
|
120
170
|
end
|
|
121
171
|
|
|
122
|
-
|
|
172
|
+
##
|
|
173
|
+
# @note
|
|
174
|
+
# This method is called _after_ a compactor runs
|
|
175
|
+
# @param [LLM::Compactor] compactor
|
|
176
|
+
# A compactor
|
|
123
177
|
def on_compaction_finish(compactor)
|
|
178
|
+
nil
|
|
124
179
|
end
|
|
125
180
|
|
|
126
|
-
|
|
127
|
-
|
|
181
|
+
##
|
|
182
|
+
# @note
|
|
183
|
+
# This method is called once per request
|
|
184
|
+
# in a turn (which can contain multiple
|
|
185
|
+
# requests)
|
|
186
|
+
# @param [LLM::Context] ctx
|
|
187
|
+
# The context
|
|
188
|
+
# @param [LLM::Response] res
|
|
189
|
+
# The response
|
|
190
|
+
def on_step(ctx, res)
|
|
191
|
+
nil
|
|
128
192
|
end
|
|
129
193
|
|
|
130
|
-
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
|
|
194
|
+
##
|
|
195
|
+
# @note
|
|
196
|
+
# This method is called when a request is
|
|
197
|
+
# rate limited and retried.
|
|
198
|
+
# @param [LLM::RateLimitError] error
|
|
199
|
+
# @param [Integer] attempt
|
|
200
|
+
def on_retry(error, attempt)
|
|
201
|
+
nil
|
|
135
202
|
end
|
|
136
203
|
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
##
|
|
205
|
+
# @note
|
|
206
|
+
# This method is called _before_ a skill runs
|
|
207
|
+
# @param [LLM::Skill] skill
|
|
208
|
+
# A skill
|
|
209
|
+
def on_skill_call(skill)
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
##
|
|
214
|
+
# @note
|
|
215
|
+
# This method is called _after_ a skill runs
|
|
216
|
+
# @param [LLM::Agent] agent
|
|
217
|
+
# The agent who ran the skill
|
|
218
|
+
# @param [LLM::Skill] skill
|
|
219
|
+
# @param [LLM::Response] res
|
|
220
|
+
def on_skill_return(agent, skill, res)
|
|
221
|
+
nil
|
|
139
222
|
end
|
|
140
223
|
end
|
|
141
224
|
|
|
142
|
-
llm = LLM.deepseek
|
|
143
|
-
agent = LLM::Agent.new(llm, stream:
|
|
225
|
+
llm = LLM.deepseek
|
|
226
|
+
agent = LLM::Agent.new(llm, stream: Stream.new)
|
|
144
227
|
agent.talk "Explain Ruby fibers."
|
|
145
228
|
```
|
|
146
229
|
</details>
|
|
@@ -148,17 +231,23 @@ agent.talk "Explain Ruby fibers."
|
|
|
148
231
|
<details><summary>Tools</summary>
|
|
149
232
|
<br>
|
|
150
233
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
The runtime
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
234
|
+
An agent requires one or more tools to be
|
|
235
|
+
able to interact with the "outside" world.
|
|
236
|
+
At a high-level a tool is how a model can
|
|
237
|
+
access your filesystem, search the internet,
|
|
238
|
+
post a comment on your behalf and anything
|
|
239
|
+
else that your own code could do.
|
|
240
|
+
|
|
241
|
+
The runtime represents a tool as a subclass
|
|
242
|
+
of [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
243
|
+
that provides a name, a description, an optional
|
|
244
|
+
set of parameters and a method that the runtime
|
|
245
|
+
will call on the model's behalf. A tool is implemented
|
|
246
|
+
on top of
|
|
247
|
+
an [`LLM::Function`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
|
|
248
|
+
object, and you might come across it in stream and
|
|
249
|
+
tracer callbacks. The model decides when and how a
|
|
250
|
+
tool is called:
|
|
162
251
|
|
|
163
252
|
```ruby
|
|
164
253
|
class ReadFile < LLM::Tool
|
|
@@ -172,255 +261,441 @@ class ReadFile < LLM::Tool
|
|
|
172
261
|
end
|
|
173
262
|
end
|
|
174
263
|
|
|
175
|
-
llm = LLM.deepseek
|
|
264
|
+
llm = LLM.deepseek
|
|
176
265
|
agent = LLM::Agent.new(llm, tools: [ReadFile], stream: $stdout)
|
|
177
266
|
agent.talk "summarize README.md"
|
|
178
267
|
```
|
|
179
268
|
</details>
|
|
180
|
-
<details>
|
|
181
|
-
<summary>Skills</summary>
|
|
269
|
+
<details><summary>Schema</summary>
|
|
182
270
|
<br>
|
|
183
271
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
272
|
+
The
|
|
273
|
+
[`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
|
|
274
|
+
class describes an object that has properties
|
|
275
|
+
of different types, and it can also reference
|
|
276
|
+
other objects, or schemas. The object describes
|
|
277
|
+
the shape of a JSON object that the model will
|
|
278
|
+
return in place of the usual free-form text that
|
|
279
|
+
a model replies with.
|
|
280
|
+
|
|
281
|
+
This feature is implemented with
|
|
282
|
+
[JSONSchema](https://json-schema.org/)
|
|
283
|
+
under the hood and a large portion
|
|
284
|
+
of the spec is supported: objects, enums,
|
|
285
|
+
arrays, nested objects, strings, integers,
|
|
286
|
+
and so on.
|
|
287
|
+
[`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
|
|
288
|
+
also provides the parameter definitions
|
|
289
|
+
available to
|
|
290
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
291
|
+
subclasses and the same range of types are
|
|
292
|
+
supported.
|
|
195
293
|
|
|
196
|
-
|
|
294
|
+
The
|
|
295
|
+
[`LLM::DeepSeek`](https://r.uby.dev/api-docs/llm.rb/LLM/DeepSeek.html)
|
|
296
|
+
provider does not support structured outputs
|
|
297
|
+
officially but the runtime implements structured
|
|
298
|
+
outputs on top of the `json_object` API and it
|
|
299
|
+
works the same as all other providers who do
|
|
300
|
+
support structured outputs:
|
|
197
301
|
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
302
|
+
```ruby
|
|
303
|
+
class Forecast < LLM::Schema
|
|
304
|
+
property :city, String, "The city name"
|
|
305
|
+
property :temperature, Number, "Current temperature"
|
|
306
|
+
property :conditions, String, "Weather conditions"
|
|
307
|
+
required %i[city temperature conditions]
|
|
308
|
+
end
|
|
204
309
|
|
|
205
|
-
|
|
206
|
-
|
|
310
|
+
llm = LLM.deepseek
|
|
311
|
+
agent = LLM::Agent.new(llm)
|
|
312
|
+
res = agent.talk "Weather in Paris?", schema: Forecast
|
|
313
|
+
res.content! # => {city: "Paris", temperature: 15.0, conditions: "Cloudy"}
|
|
207
314
|
```
|
|
315
|
+
</details>
|
|
316
|
+
<details>
|
|
317
|
+
<summary>Serialization</summary>
|
|
318
|
+
<br>
|
|
208
319
|
|
|
209
|
-
|
|
320
|
+
Both [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
|
|
321
|
+
and
|
|
322
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
323
|
+
can be serialized to JSON and written to disk.
|
|
324
|
+
This feature is what supports the ActiveRecord
|
|
325
|
+
and Sequel integrations too but rather than
|
|
326
|
+
store the agent directly on disk it is stored
|
|
327
|
+
in a database column instead.
|
|
328
|
+
|
|
329
|
+
An agent can be configured to read from and
|
|
330
|
+
write to a file automatically with the `path`
|
|
331
|
+
option. When the file already exists, the agent
|
|
332
|
+
is restored from the file and continues where
|
|
333
|
+
he left off. After each turn the agent flushes
|
|
334
|
+
its state to the file. The text file can be shared
|
|
335
|
+
like any other text file and it can be used to
|
|
336
|
+
restore the agent in another process or machine:
|
|
210
337
|
|
|
211
338
|
```ruby
|
|
212
339
|
require "llm"
|
|
213
340
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
agent
|
|
341
|
+
path = File.join(Dir.home, ".agents", "myagent.json")
|
|
342
|
+
llm = LLM.deepseek
|
|
343
|
+
agent = LLM::Agent.new(llm, path:)
|
|
344
|
+
agent.talk "remember my name is robert"
|
|
345
|
+
|
|
346
|
+
##
|
|
347
|
+
# Resume the conversation where the agent left off
|
|
348
|
+
agent = LLM::Agent.new(llm, path:)
|
|
349
|
+
agent.talk "what's my name?"
|
|
217
350
|
```
|
|
218
351
|
</details>
|
|
219
|
-
|
|
220
352
|
<details>
|
|
221
|
-
<summary>
|
|
353
|
+
<summary>Tracer</summary>
|
|
222
354
|
<br>
|
|
223
355
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
on the
|
|
356
|
+
An agent can have its own tracer that observes runtime
|
|
357
|
+
events throughout its lifetime. A tracer is local to an
|
|
358
|
+
agent and the tracer runs on the same thread as the agent.
|
|
359
|
+
There are three builtin tracers, and a fourth tracer that
|
|
360
|
+
lets you combine multiple tracers into a single tracer -
|
|
361
|
+
so you can log to a file, and export to OpenTelemetry
|
|
362
|
+
at the same time.
|
|
363
|
+
|
|
364
|
+
All tracers protect themselves and the agent being traced
|
|
365
|
+
from a crash through a rescue that dumps a tracer error
|
|
366
|
+
to standard error (`$stderr`). Beyond the default tracers,
|
|
367
|
+
it is not unusual to write your own. A tracer can let you
|
|
368
|
+
export runtime state to your own database, an external
|
|
369
|
+
service, or any other medium you might have in your stack.
|
|
370
|
+
A new tracer can be created through a subclass of
|
|
371
|
+
[`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
|
|
372
|
+
that implements the expected callback methods. This feature
|
|
373
|
+
also works for ActiveRecord and Sequel models.
|
|
374
|
+
|
|
375
|
+
The builtin tracers:
|
|
376
|
+
|
|
377
|
+
* [`LLM::Tracer.pretty`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#pretty_logger-class_method): human-readable single-line logs to stderr, ideal during development.
|
|
378
|
+
* [`LLM::Tracer.telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#telemetry-class_method):
|
|
379
|
+
exports spans via OTLP for OpenTelemetry in production.
|
|
380
|
+
* [`LLM::Tracer.logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#logger-class_method):
|
|
381
|
+
structured JSON to stdout or a file.
|
|
382
|
+
* [`LLM::Tracer.multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#multi-class_method):
|
|
383
|
+
fans out every callback to multiple tracers.
|
|
384
|
+
|
|
385
|
+
A simple example:
|
|
227
386
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
387
|
+
```ruby
|
|
388
|
+
llm = LLM.deepseek
|
|
389
|
+
tracer = LLM::Tracer.pretty(llm)
|
|
390
|
+
agent = LLM::Agent.new(llm, tracer:)
|
|
391
|
+
agent.talk "Hello"
|
|
392
|
+
```
|
|
234
393
|
|
|
235
|
-
A
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
394
|
+
A multi-tracer example:
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
llm = LLM.deepseek
|
|
398
|
+
tracer = LLM::Tracer.multi(
|
|
399
|
+
llm,
|
|
400
|
+
LLM::Tracer::Logger,
|
|
401
|
+
LLM::Tracer::Telemetry,
|
|
402
|
+
LLM::Tracer::Foobar.new(llm, {option1: "foo", option2: "bar"})
|
|
403
|
+
)
|
|
404
|
+
agent = LLM::Agent.new(llm, tracer:)
|
|
405
|
+
agent.talk "Hello"
|
|
406
|
+
```
|
|
240
407
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
408
|
+
</details>
|
|
409
|
+
|
|
410
|
+
<details><summary>MCP</summary>
|
|
411
|
+
<br>
|
|
412
|
+
|
|
413
|
+
The runtime can communicate with MCP servers via HTTP(S)
|
|
414
|
+
and stdio. The stdio transport spawns a local process that
|
|
415
|
+
uses its standard input and output streams for communication.
|
|
416
|
+
The runtime provides a full-featured MCP client and the tools
|
|
417
|
+
that an MCP server has announced are available as
|
|
418
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
419
|
+
subclasses that can be used like any other subclass
|
|
420
|
+
of
|
|
421
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html).
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
The first example uses the HTTP transport to connect to
|
|
425
|
+
GitHub's MCP server. The example uses a Personal Access
|
|
426
|
+
Token (get yours [here](https://github.com/settings/apps))
|
|
427
|
+
although it could also use a token that was obtained
|
|
428
|
+
through OAuth and belongs to a user other than you:
|
|
244
429
|
|
|
245
430
|
```ruby
|
|
246
431
|
require "llm"
|
|
247
|
-
require "llm/tools"
|
|
248
432
|
|
|
249
|
-
llm
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
433
|
+
llm = LLM.deepseek
|
|
434
|
+
mcp = LLM::MCP.http(
|
|
435
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
436
|
+
headers: {"Authorization" => "Bearer #{ENV['GITHUB_PAT']}"},
|
|
437
|
+
transport: :net_http_persistent
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
agent = LLM::Agent.new(llm, stream: $stdout, tools: mcp.tools)
|
|
441
|
+
agent.talk "Find me an interesting Ruby project on GitHub"
|
|
253
442
|
```
|
|
254
443
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
444
|
+
The second example focuses on the second transport.
|
|
445
|
+
It is recommended (but not required) to launch a
|
|
446
|
+
stdio MCP server with a session so that the process
|
|
447
|
+
is kept alive for as long as you use it, otherwise
|
|
448
|
+
the process can be launched multiple times and it
|
|
449
|
+
can add latency to your requests.
|
|
259
450
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
the same process.
|
|
265
|
-
|
|
266
|
-
The runtime raises
|
|
267
|
-
[`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
|
|
268
|
-
on the fiber making a request and on every active tool
|
|
269
|
-
regardless of the active concurrency strategy. Before that
|
|
270
|
-
happens the request socket is forcibly closed so the request
|
|
271
|
-
stops burning tokens. It works the same across all strategies
|
|
272
|
-
although there can be subtle differences that usually go unnoticed.
|
|
273
|
-
|
|
274
|
-
A tool can implement the `#on_interrupt` callback
|
|
275
|
-
to be notified when a tool call has been interrupted
|
|
276
|
-
and it can also rescue the `LLM::Interrupt` exception
|
|
277
|
-
to free resources or perform other actions before the
|
|
278
|
-
tool dies.
|
|
451
|
+
The example uses a local stdio MCP server from the
|
|
452
|
+
[sem](https://github.com/Ataraxy-Labs/sem)
|
|
453
|
+
project. <br> It works on top of git and parses your code
|
|
454
|
+
with tree-sitter:
|
|
279
455
|
|
|
280
456
|
```ruby
|
|
281
|
-
|
|
282
|
-
agent = LLM::Agent.new(llm)
|
|
283
|
-
Thread.new { sleep(1); agent.cancel! }
|
|
457
|
+
require "llm"
|
|
284
458
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
459
|
+
mcp = LLM::MCP.stdio(
|
|
460
|
+
argv: ["sem", "mcp"],
|
|
461
|
+
env: {'SEM_REPO' => Dir.getwd},
|
|
462
|
+
cwd: Dir.getwd,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
##
|
|
466
|
+
# 'mcp.session' is optional, but recommended.
|
|
467
|
+
# It can be dropped but then the process may
|
|
468
|
+
# be launched once per-request.
|
|
469
|
+
mcp.session do
|
|
470
|
+
llm = LLM.deepseek
|
|
471
|
+
tracer = LLM::Tracer.pretty(llm)
|
|
472
|
+
tools = mcp.tools.select { _1.name == "sem_log" }
|
|
473
|
+
stream = $stdout
|
|
474
|
+
agent = LLM::Agent.new(llm, tracer:, stream:, tools:)
|
|
475
|
+
agent.talk "summarize recent git activity"
|
|
289
476
|
end
|
|
290
477
|
```
|
|
291
478
|
</details>
|
|
479
|
+
|
|
292
480
|
<details>
|
|
293
|
-
<summary>Console
|
|
481
|
+
<summary>Console</summary>
|
|
294
482
|
<br>
|
|
295
483
|
|
|
296
484
|
The [LLM::Agent#console](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#console-instance_method)
|
|
297
485
|
method drops you into an interactive console that is built on
|
|
298
|
-
top of curses.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
486
|
+
top of (n)curses. The `llm.rb` executable packaged with the gem
|
|
487
|
+
is another way to access the console and ActiveRecord models who
|
|
488
|
+
have called `acts_as_agent` can access the console as well
|
|
489
|
+
(via `agent.console`).
|
|
490
|
+
|
|
491
|
+
A console for an ActiveRecord model does not write back to the
|
|
492
|
+
database. The `llm.rb` executable automatically associates a
|
|
493
|
+
session with the current working directory and it can be resumed
|
|
494
|
+
by calling `llm.rb` in the same directory at a later point.
|
|
495
|
+
|
|
496
|
+
The console is not intended to compete with Claude, Codex and
|
|
497
|
+
friends. It is much more limited, serves an entirely different
|
|
498
|
+
purpose and is more like a debugger for your agents. The
|
|
499
|
+
dependencies required by the console are not installed
|
|
500
|
+
by default, and the easiest way to grab them is via
|
|
501
|
+
`gem install llm-shell`.
|
|
303
502
|
|
|
304
503
|
##### Demo
|
|
305
504
|
|
|
306
505
|

|
|
307
506
|
|
|
507
|
+
</details>
|
|
508
|
+
<details><summary>Budgets</summary>
|
|
509
|
+
<br>
|
|
308
510
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
511
|
+
An agent has a retry budget, and a tool call
|
|
512
|
+
budget. The retry budget is enabled by default,
|
|
513
|
+
and it determines how many times we will retry
|
|
514
|
+
when a request raises an error. The errors
|
|
515
|
+
that are retried are returned by
|
|
516
|
+
[`LLM::Provider#retry_errors`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#retry_errors-instance_method)
|
|
517
|
+
and can vary by provider but typically cover
|
|
518
|
+
rate limits, and time outs. Between retries
|
|
519
|
+
we will sleep, and each retry sleeps a little
|
|
520
|
+
longer (`sleep 2.0 * attempts`) than the last.
|
|
521
|
+
|
|
522
|
+
The default retry budget is usually 5 but
|
|
523
|
+
it can also vary by provider and it is determined by the
|
|
524
|
+
[`LLM::Provider#retry_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#retry_budget-instance_method)
|
|
525
|
+
method. When the budget is spent but the an
|
|
526
|
+
exception continues to raise we will give up
|
|
527
|
+
and let the exception bubble up to the caller.
|
|
528
|
+
The budget can be disabled by setting
|
|
529
|
+
`retry_budget` to zero:
|
|
316
530
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
to run the console:
|
|
531
|
+
```ruby
|
|
532
|
+
require "llm"
|
|
320
533
|
|
|
321
|
-
|
|
534
|
+
llm = LLM.deepseek
|
|
535
|
+
agent = LLM::Agent.new(llm, retry_budget: 0)
|
|
536
|
+
```
|
|
322
537
|
|
|
323
|
-
|
|
538
|
+
The second budget describes how many tool calls
|
|
539
|
+
are allowed in a single turn, and it is disabled
|
|
540
|
+
by default. When the limit is reached or breached
|
|
541
|
+
the agent will reply with an in-band error that
|
|
542
|
+
tells the model it has spent its tool call budget,
|
|
543
|
+
and that it should stop requesting tool calls for
|
|
544
|
+
this turn. This exchange repeats until the model
|
|
545
|
+
stops requesting tool calls.
|
|
324
546
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
Ctrl+N.
|
|
547
|
+
The budget is restored after a turn completes and the
|
|
548
|
+
model will normally provide a response from the tool
|
|
549
|
+
calls made before the budget was spent:
|
|
329
550
|
|
|
330
551
|
```ruby
|
|
331
552
|
require "llm"
|
|
332
553
|
require "llm/tools"
|
|
333
554
|
|
|
334
|
-
llm
|
|
335
|
-
|
|
336
|
-
agent.
|
|
555
|
+
llm = LLM.deepseek
|
|
556
|
+
tools = LLM::Tool.subclasses
|
|
557
|
+
agent = LLM::Agent.new(llm, tools:, tool_budget: 25)
|
|
558
|
+
agent.talk "fix every FIXME in this repository"
|
|
337
559
|
```
|
|
560
|
+
</details>
|
|
561
|
+
<details>
|
|
562
|
+
<summary>Usage|Cost</summary>
|
|
563
|
+
<br>
|
|
338
564
|
|
|
339
|
-
|
|
565
|
+
An agent knows how many tokens it has spent,
|
|
566
|
+
and how much those tokens cost. The runtime
|
|
567
|
+
embeds the [models.dev](https://models.dev)
|
|
568
|
+
registry (only the parts it uses, and on-demand)
|
|
569
|
+
that gives it access to model information that
|
|
570
|
+
includes costs and limits.
|
|
571
|
+
|
|
572
|
+
The `token_usage` method sums the whole conversation as a
|
|
573
|
+
[LLM::Usage](https://r.uby.dev/api-docs/llm.rb/LLM/Usage.html)
|
|
574
|
+
object and [LLM::Cost](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html)
|
|
575
|
+
combines it with the model registry to determine how much money
|
|
576
|
+
has been spent. These numbers should not be taken as gospel though.
|
|
577
|
+
They're a best-guess estimate, and your provider's dashboard
|
|
578
|
+
might disagree:
|
|
340
579
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
provider from standard environment variables (`DEEPSEEK_API_KEY`,
|
|
344
|
-
`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.). Persistent sessions are
|
|
345
|
-
stored under `~/.llm.rb/` and restored automatically on your next visit.
|
|
580
|
+
```ruby
|
|
581
|
+
require "llm"
|
|
346
582
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
llm.rb -x 900 # read timeout of 15 minutes
|
|
354
|
-
llm.rb -t # temporary session, no persistence
|
|
355
|
-
llm.rb -v # print the version
|
|
356
|
-
llm.rb -h # print usage
|
|
583
|
+
llm = LLM.deepseek
|
|
584
|
+
agent = LLM::Agent.new(llm)
|
|
585
|
+
agent.talk "Hello"
|
|
586
|
+
|
|
587
|
+
agent.token_usage # => LLM::Usage for the whole conversation
|
|
588
|
+
agent.cost # => LLM::Cost, priced from the registry
|
|
357
589
|
```
|
|
590
|
+
|
|
591
|
+
The `context_*` methods can tell you about the model's context
|
|
592
|
+
window: its total capacity, and how much of it have we already
|
|
593
|
+
spent:
|
|
594
|
+
|
|
595
|
+
```ruby
|
|
596
|
+
agent.context_used # => tokens in the latest turn
|
|
597
|
+
agent.context_window # => the model's limit, or nil when unknown
|
|
598
|
+
agent.context_usage # => Rational, eg Rational(100, 10_000)
|
|
599
|
+
```
|
|
600
|
+
|
|
358
601
|
</details>
|
|
359
602
|
<details>
|
|
360
|
-
<summary>
|
|
603
|
+
<summary><code>class Agent < LLM::Agent</code></summary>
|
|
361
604
|
<br>
|
|
362
605
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
606
|
+
The examples so far have focused on
|
|
607
|
+
[LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
608
|
+
as an object but you can create a subclass that inherits from
|
|
609
|
+
[LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
610
|
+
too. Both support the same features and accept the same options
|
|
611
|
+
but a subclass can be configured with the `set` method, implement
|
|
612
|
+
its own methods, inherit from other modules and create multiple
|
|
613
|
+
instances of itself.
|
|
614
|
+
|
|
615
|
+
The
|
|
616
|
+
[`LLM::Agent.set`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#set-class_method)
|
|
617
|
+
method can be used to configure your agent, and
|
|
618
|
+
it accepts all the same options that an instance
|
|
619
|
+
of [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
620
|
+
accepts. An option can be resolved lazily via
|
|
621
|
+
a Proc or a Symbol that represents a method
|
|
622
|
+
on your class, and all options are optional -
|
|
623
|
+
you can set many or none, and your subclass
|
|
624
|
+
will still work. For example:
|
|
369
625
|
|
|
370
626
|
```ruby
|
|
371
627
|
require "llm"
|
|
628
|
+
require "llm/tools"
|
|
372
629
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
630
|
+
class Agent < LLM::Agent
|
|
631
|
+
set name: "sysadmin",
|
|
632
|
+
description: "performs system administration tasks",
|
|
633
|
+
instructions: proc { File.read("prompt.md") },
|
|
634
|
+
concurrency: :fork,
|
|
635
|
+
tools: :mytools
|
|
376
636
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
637
|
+
private
|
|
638
|
+
|
|
639
|
+
def mytools
|
|
640
|
+
[LLM::Tool::Exec]
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
llm = LLM.deepseek
|
|
645
|
+
agent = Agent.new(llm)
|
|
646
|
+
agent.talk "What's our uptime?", stream: $stdout
|
|
380
647
|
```
|
|
381
648
|
</details>
|
|
382
|
-
|
|
649
|
+
|
|
650
|
+
#### Database
|
|
651
|
+
|
|
652
|
+
<details><summary>ActiveRecord </summary>
|
|
383
653
|
<br>
|
|
384
654
|
|
|
385
|
-
|
|
655
|
+
Both
|
|
386
656
|
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) and
|
|
387
657
|
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
388
|
-
can be serialized to JSON and stored in a
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
658
|
+
can be serialized to JSON and stored in a database column. The `jsonb`
|
|
659
|
+
column type from PostgreSQL is recommended but it can also be stored as
|
|
660
|
+
a string on other databases. ActiveRecord support is optimized
|
|
661
|
+
for the `jsonb` column type and PostgreSQL.
|
|
662
|
+
|
|
663
|
+
The column captures everything an agent has done up to that point,
|
|
664
|
+
it includes tool calls and returns, exchanged messages, and other
|
|
665
|
+
metadata that carries runtime state. Each agent is an ActiveRecord
|
|
666
|
+
model that calls `acts_as_agent` and each row represents an instance
|
|
667
|
+
of that agent. It can be used with new and existing models alike.
|
|
668
|
+
|
|
669
|
+
The column should have the name `data` but this can be
|
|
670
|
+
changed when the `acts_as_agent` method is called (eg
|
|
671
|
+
`acts_as_agent(data_column: :my_column)`). The column
|
|
672
|
+
is updated after every request that an agent makes rather
|
|
673
|
+
than every turn, so an unexpected interrupt can be resumed
|
|
674
|
+
from from the last request and no progress (or spent tokens)
|
|
675
|
+
are lost:
|
|
395
676
|
|
|
396
677
|
```ruby
|
|
397
678
|
require "active_record"
|
|
398
679
|
require "llm"
|
|
399
680
|
require "llm/active_record"
|
|
400
681
|
|
|
401
|
-
|
|
402
|
-
# The Robert agent.
|
|
403
|
-
class Robert < ActiveRecord::Base
|
|
682
|
+
class Octocat < ActiveRecord::Base
|
|
404
683
|
acts_as_agent(format: :jsonb) do |agent|
|
|
405
|
-
agent.set name: "
|
|
406
|
-
description: "
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
tools: :tools,
|
|
411
|
-
concurrency: :async,
|
|
412
|
-
|
|
413
|
-
##
|
|
414
|
-
# The maximum number of tool calls per-turn.
|
|
415
|
-
tool_budget: 25,
|
|
416
|
-
|
|
417
|
-
##
|
|
418
|
-
# The default tracer that all agents have associated
|
|
419
|
-
# with them. The tracer exports a trace to a couple of
|
|
420
|
-
# SQL tables.
|
|
421
|
-
tracer: proc { Raven::Tracer::SQL.new(llm, agent: self) }
|
|
684
|
+
agent.set name: "octocat",
|
|
685
|
+
description: "performs actions on GitHub",
|
|
686
|
+
instructions: proc { File.read("prompt.md") },
|
|
687
|
+
tools: :toolset,
|
|
688
|
+
tracer: proc { SQLTracer.new(llm, agent: self) }
|
|
422
689
|
end
|
|
423
690
|
|
|
691
|
+
##
|
|
692
|
+
# @return [Array<LLM::Tool>]
|
|
693
|
+
def toolset
|
|
694
|
+
github.tools
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
private
|
|
698
|
+
|
|
424
699
|
##
|
|
425
700
|
# @return [LLM::MCP]
|
|
426
701
|
def github
|
|
@@ -432,32 +707,17 @@ class Robert < ActiveRecord::Base
|
|
|
432
707
|
end
|
|
433
708
|
|
|
434
709
|
##
|
|
435
|
-
# @
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def allowlist
|
|
443
|
-
%w[
|
|
444
|
-
get_commit
|
|
445
|
-
get_file_contents
|
|
446
|
-
list_branches
|
|
447
|
-
list_commits
|
|
448
|
-
search_code
|
|
449
|
-
search_commits
|
|
450
|
-
search_repositories
|
|
451
|
-
search_issues
|
|
452
|
-
pull_request_read
|
|
453
|
-
list_pull_requests
|
|
454
|
-
list_issues
|
|
455
|
-
issue_read
|
|
456
|
-
].freeze
|
|
710
|
+
# @note
|
|
711
|
+
# All models who call 'acts_as_agent' must implement
|
|
712
|
+
# this method. It returns the provider that the agent
|
|
713
|
+
# will use. It can be determined dynamically.
|
|
714
|
+
# @return [LLM::Provider]
|
|
715
|
+
def set_provider
|
|
716
|
+
LLM.deepseek
|
|
457
717
|
end
|
|
458
718
|
end
|
|
459
719
|
|
|
460
|
-
agent =
|
|
720
|
+
agent = Octocat.create!
|
|
461
721
|
|
|
462
722
|
##
|
|
463
723
|
# Every call to `talk` automatically persists
|
|
@@ -468,7 +728,7 @@ agent.talk "what's new on the llm.rb repository?"
|
|
|
468
728
|
# The conversation was persisted to database. A
|
|
469
729
|
# fresh instance restores it and continues where
|
|
470
730
|
# we left off
|
|
471
|
-
agent =
|
|
731
|
+
agent = Octocat.find(agent.id).talk "and what about roda?"
|
|
472
732
|
|
|
473
733
|
##
|
|
474
734
|
# Start an agent console.
|
|
@@ -477,8 +737,9 @@ agent = Robert.find(agent.id).talk "and what about roda-llm?"
|
|
|
477
737
|
agent.console
|
|
478
738
|
```
|
|
479
739
|
</details>
|
|
740
|
+
|
|
480
741
|
<details>
|
|
481
|
-
<summary>
|
|
742
|
+
<summary>SQL optimizations </summary>
|
|
482
743
|
<br>
|
|
483
744
|
|
|
484
745
|
In a database environment the runtime optimizes for
|
|
@@ -572,123 +833,238 @@ the whole column.
|
|
|
572
833
|
|
|
573
834
|
</details>
|
|
574
835
|
|
|
575
|
-
|
|
836
|
+
#### Concurrency
|
|
837
|
+
|
|
838
|
+
<details>
|
|
839
|
+
<summary>Strategies</summary>
|
|
576
840
|
<br>
|
|
577
841
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
842
|
+
There are six different concurrency strategies
|
|
843
|
+
to choose from. A tool can be spawned on its own
|
|
844
|
+
process, on its own ractor, on its own thread,
|
|
845
|
+
on a thread that manages an `Async::Reactor`, or
|
|
846
|
+
on its own fiber.
|
|
847
|
+
|
|
848
|
+
Each strategy is built around an abstraction that
|
|
849
|
+
makes them interchangeable - you can switch between
|
|
850
|
+
processes, threads, ractors, async fibers, or a ruby
|
|
851
|
+
fiber by changing a single line of code. **All** subclasses
|
|
852
|
+
of [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
853
|
+
are supported, so you can launch MCP tools concurrently
|
|
854
|
+
or in parallel as well.
|
|
855
|
+
|
|
856
|
+
The strategy that you choose depends on the workload
|
|
857
|
+
and the requirements of your application, as well as
|
|
858
|
+
what the strategy offers. The `:fork` and `:ractor`
|
|
859
|
+
strategies provide true parallelism, and the other
|
|
860
|
+
strategies - especially `:thread`, and `:async` -
|
|
861
|
+
can be great for IO-bound tools.
|
|
862
|
+
|
|
863
|
+
The following example configures an agent to use
|
|
864
|
+
all builtin tools (`LLM::Tool.subclasses`) and it
|
|
865
|
+
spawns each tool in its own process
|
|
866
|
+
(`concurrency: :fork`). It could be changed to use
|
|
867
|
+
async instead with `concurrency: :async`, or with
|
|
868
|
+
threads via `concurrency: :thread`, and so on. The
|
|
869
|
+
default concurrency strategy is `:sequential`, and
|
|
870
|
+
it runs tools sequentially on a single thread.
|
|
585
871
|
|
|
586
872
|
```ruby
|
|
587
873
|
require "llm"
|
|
874
|
+
require "llm/tools"
|
|
588
875
|
|
|
589
|
-
llm = LLM.deepseek
|
|
590
|
-
|
|
591
|
-
agent = LLM::Agent.new(llm,
|
|
592
|
-
agent.talk "
|
|
876
|
+
llm = LLM.deepseek
|
|
877
|
+
tools = LLM::Tool.subclasses
|
|
878
|
+
agent = LLM::Agent.new(llm, tools:, concurrency: :fork)
|
|
879
|
+
agent.talk "implement the feature described in todo.txt"
|
|
593
880
|
```
|
|
881
|
+
|
|
594
882
|
</details>
|
|
595
|
-
<details
|
|
883
|
+
<details>
|
|
884
|
+
<summary>Cancellation</summary>
|
|
596
885
|
<br>
|
|
597
886
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
[`LLM::
|
|
604
|
-
|
|
887
|
+
It is possible to interrupt a running agent who
|
|
888
|
+
is between requests or tool calls as long as the
|
|
889
|
+
cancel request is sent from another thread or fiber
|
|
890
|
+
running in the same process as the agent. A cancel
|
|
891
|
+
request can be sent with the
|
|
892
|
+
[`LLM::Agent#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#interrupt!-instance_method)
|
|
893
|
+
method.
|
|
894
|
+
|
|
895
|
+
An interrupted tool call is made aware of the interrupt
|
|
896
|
+
and it can both rescue `LLM::Interrupt` and/or implement
|
|
897
|
+
the `on_interrupt` callback on the tool class. The option to
|
|
898
|
+
cancel on [r.uby.dev](https://r.uby.dev) is built on top of
|
|
899
|
+
this feature, and it has a single background process
|
|
900
|
+
with 24 threads. Each thread can run an agent request that
|
|
901
|
+
can be interrupted via another thread in the same process.
|
|
902
|
+
|
|
903
|
+
It is solid and reliable but r.uby.dev had to meet this
|
|
904
|
+
feature halfway, so expect to build your own infrastructure
|
|
905
|
+
around it. See
|
|
906
|
+
[cancellation chapter](docs/deepdive/advanced/cancellation.md)
|
|
907
|
+
to learn more.
|
|
605
908
|
|
|
606
909
|
```ruby
|
|
607
|
-
|
|
910
|
+
class Search < LLM::Tool
|
|
911
|
+
name "search"
|
|
912
|
+
description "Search many files"
|
|
913
|
+
parameter :pattern, String, "The pattern to search for"
|
|
914
|
+
required %i[pattern]
|
|
608
915
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
916
|
+
##
|
|
917
|
+
# A raise is delivered here; `on_interrupt` is a notification, and it
|
|
918
|
+
# runs on every strategy - `:sequential` included.
|
|
919
|
+
def call(pattern:)
|
|
920
|
+
search(pattern)
|
|
921
|
+
rescue LLM::Interrupt
|
|
922
|
+
##
|
|
923
|
+
# A tool can return a value from here, and the turn carries on with
|
|
924
|
+
# it, or re-raise and the fiber that made the request is raised
|
|
925
|
+
# into as well.
|
|
926
|
+
cleanup
|
|
927
|
+
raise
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
##
|
|
931
|
+
# Told on the thread or fiber the call runs on, before the raise on
|
|
932
|
+
# `:fork` and `:ractor` and after the rescue above on the other three.
|
|
933
|
+
def on_interrupt
|
|
934
|
+
cleanup
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
private
|
|
938
|
+
|
|
939
|
+
def cleanup
|
|
940
|
+
# Release a file, a socket, or a lock here.
|
|
941
|
+
end
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
llm = LLM.deepseek
|
|
945
|
+
agent = LLM::Agent.new(llm, tools: [Search], concurrency: :async)
|
|
946
|
+
queue = Queue.new
|
|
947
|
+
Thread.new { [sleep(queue.pop), agent.interrupt!] }
|
|
948
|
+
|
|
949
|
+
begin
|
|
950
|
+
queue.push(3)
|
|
951
|
+
agent.talk "find every TODO in the repository", stream: $stdout
|
|
952
|
+
rescue LLM::Interrupt
|
|
953
|
+
puts
|
|
954
|
+
puts "!! cancelled !!"
|
|
955
|
+
end
|
|
613
956
|
```
|
|
614
957
|
</details>
|
|
615
|
-
|
|
616
|
-
<
|
|
958
|
+
<details>
|
|
959
|
+
<summary>Cancel by record ID</summary>
|
|
617
960
|
<br>
|
|
618
961
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
can define objects, arrays, enums, nested schemas,
|
|
630
|
-
and more. It is also used internally by
|
|
631
|
-
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) for parameter
|
|
632
|
-
definitions, so you already benefit from it when you declare tool
|
|
633
|
-
parameters.
|
|
962
|
+
A common deployment setup is to run your agents in a
|
|
963
|
+
background process that a web frontend can communicate
|
|
964
|
+
with (usually via a database). The background process
|
|
965
|
+
would have one thread per agent, and it could run as
|
|
966
|
+
many agents as it has threads. This is how the
|
|
967
|
+
[r.uby.dev](https://r.uby.dev) website is configured,
|
|
968
|
+
and it is the configuration that the
|
|
969
|
+
[`LLM.interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM.html#interrupt-class_method)
|
|
970
|
+
method is optimized for: a single process with each agent
|
|
971
|
+
running in its own thread.
|
|
634
972
|
|
|
635
973
|
The
|
|
636
|
-
[`LLM
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
974
|
+
[`LLM.interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM.html#interrupt-class_method)
|
|
975
|
+
method has access to a process-wide
|
|
976
|
+
registry that contains every active instance of
|
|
977
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
|
|
978
|
+
and that includes Sequel and ActiveRecord agents, too. An
|
|
979
|
+
agent enters the registry when it starts a turn, and it
|
|
980
|
+
exits the registry afterwards. The method returns true
|
|
981
|
+
when it sent an interrupt, and otherwise it returns false.
|
|
982
|
+
|
|
983
|
+
There is often a window between when an agent is queued
|
|
984
|
+
and when it runs, so a poll approach lets you eventually
|
|
985
|
+
interrupt the agent, or give up trying:
|
|
642
986
|
|
|
643
987
|
```ruby
|
|
644
|
-
class
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
988
|
+
class InterruptJob
|
|
989
|
+
def call(agent_id:)
|
|
990
|
+
LLM.interrupt(
|
|
991
|
+
id: agent_id,
|
|
992
|
+
attempts: 10,
|
|
993
|
+
interval: 0.1
|
|
994
|
+
)
|
|
995
|
+
end
|
|
649
996
|
end
|
|
650
|
-
|
|
651
|
-
llm = LLM.deepseek(key: ENV["KEY"])
|
|
652
|
-
agent = LLM::Agent.new(llm, schema: Weather)
|
|
653
|
-
res = agent.talk "Weather in Paris?"
|
|
654
|
-
res.content! # => {city: "Paris", temperature: 15.0, conditions: "Cloudy"}
|
|
655
997
|
```
|
|
656
998
|
</details>
|
|
657
|
-
|
|
999
|
+
|
|
1000
|
+
#### Hooks
|
|
1001
|
+
|
|
1002
|
+
<details><summary>Intercepts</summary>
|
|
658
1003
|
<br>
|
|
659
1004
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
[`LLM::
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
the
|
|
1005
|
+
When a model requests a tool call, the
|
|
1006
|
+
runtime represents it as a [`LLM::Function`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
|
|
1007
|
+
object that a subclass of [`LLM::Intercept`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html)
|
|
1008
|
+
can see _before_ it is called. An intercept
|
|
1009
|
+
can return a
|
|
1010
|
+
[`LLM::Function::Return`]((https://r.uby.dev/api-docs/llm.rb/LLM/Function.html))
|
|
1011
|
+
object on behalf of the tool call, and so the tool
|
|
1012
|
+
call is never made. Or it can return any other
|
|
1013
|
+
value to let the tool continue as normal.
|
|
1014
|
+
|
|
1015
|
+
Maybe best of all, an intercept can modify
|
|
1016
|
+
the arguments given to a [`LLM::Function`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
|
|
1017
|
+
in-place, before the tool call is made. This
|
|
1018
|
+
allows an intercept to modify what the model
|
|
1019
|
+
wanted to send to a function, and then let
|
|
1020
|
+
the function be called as normal.
|
|
1021
|
+
|
|
1022
|
+
This also works for MCP tools - so you might
|
|
1023
|
+
be able to imagine an intercept that wrapped
|
|
1024
|
+
a comment body at 80 columns, or parsed a
|
|
1025
|
+
markdown post into HTML, before the tool
|
|
1026
|
+
call is made.
|
|
1027
|
+
|
|
1028
|
+
A simple example:
|
|
676
1029
|
|
|
677
1030
|
```ruby
|
|
678
|
-
|
|
1031
|
+
require "llm"
|
|
1032
|
+
require "llm/tools"
|
|
1033
|
+
|
|
1034
|
+
class Sandbox < LLM::Intercept
|
|
679
1035
|
def call(function:)
|
|
680
1036
|
if function.name == "exec"
|
|
681
|
-
function.return(error: true, type: "
|
|
682
|
-
message: "exec is disabled")
|
|
1037
|
+
function.return(error: true, type: "permission_error", message: "exec is disabled")
|
|
683
1038
|
end
|
|
684
1039
|
end
|
|
685
1040
|
end
|
|
686
1041
|
|
|
687
|
-
llm
|
|
688
|
-
|
|
1042
|
+
llm = LLM.deepseek
|
|
1043
|
+
tools = [LLM::Tool::Exec, LLM::Tool::ReadFile]
|
|
1044
|
+
agent = LLM::Agent.new(llm, tools:, intercept: Sandbox)
|
|
1045
|
+
agent.talk "run 'rm -rf myapp/' right now", stream: $stdout
|
|
689
1046
|
```
|
|
690
|
-
</details>
|
|
691
1047
|
|
|
1048
|
+
A multi-intercept example. <br>
|
|
1049
|
+
|
|
1050
|
+
Each intercept is called in order they're given
|
|
1051
|
+
and the first one that returns an
|
|
1052
|
+
[`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html)
|
|
1053
|
+
stops the tool call from being made. This feature
|
|
1054
|
+
lets you combine multiple intercepts into a single
|
|
1055
|
+
intercept that calls out to all of them:
|
|
1056
|
+
|
|
1057
|
+
```ruby
|
|
1058
|
+
require "llm"
|
|
1059
|
+
|
|
1060
|
+
llm = LLM.deepseek
|
|
1061
|
+
mcp = LLM::MCP.stdio(argv: ["bundle", "exec", "slack-mcp"])
|
|
1062
|
+
intercept = LLM::Intercept.multi(llm, WrapComment, BlockBadLanguage)
|
|
1063
|
+
agent = LLM::Agent.new(llm, tools: [mcp.tools], intercept:)
|
|
1064
|
+
|
|
1065
|
+
agent.talk "Tell slack channel I'm AFK", stream: $stdout
|
|
1066
|
+
```
|
|
1067
|
+
</details>
|
|
692
1068
|
<details>
|
|
693
1069
|
<summary>Transformers</summary>
|
|
694
1070
|
<br>
|
|
@@ -706,7 +1082,7 @@ class RedactEmails < LLM::Transformer
|
|
|
706
1082
|
end
|
|
707
1083
|
end
|
|
708
1084
|
|
|
709
|
-
llm = LLM.deepseek
|
|
1085
|
+
llm = LLM.deepseek
|
|
710
1086
|
agent = LLM::Agent.new(llm, transformer: RedactEmails)
|
|
711
1087
|
agent.talk "Contact support@example.com for help"
|
|
712
1088
|
```
|
|
@@ -729,13 +1105,13 @@ never contains an orphaned result. It is also possible to subclass
|
|
|
729
1105
|
[`LLM::Compactor`](https://r.uby.dev/api-docs/llm.rb/LLM/Compactor.html)
|
|
730
1106
|
to implement your own compactor with its own logic. Streams can observe the
|
|
731
1107
|
process through the
|
|
732
|
-
[`LLM::Stream#on_compaction`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction)
|
|
1108
|
+
[`LLM::Stream#on_compaction`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction-instance_method)
|
|
733
1109
|
and
|
|
734
|
-
[`LLM::Stream#on_compaction_finish`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction_finish)
|
|
1110
|
+
[`LLM::Stream#on_compaction_finish`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction_finish-instance_method)
|
|
735
1111
|
callbacks.
|
|
736
1112
|
|
|
737
1113
|
```ruby
|
|
738
|
-
llm = LLM.deepseek
|
|
1114
|
+
llm = LLM.deepseek
|
|
739
1115
|
agent = LLM::Agent.new(
|
|
740
1116
|
llm,
|
|
741
1117
|
compactor: LLM::Compactor::Truncate,
|
|
@@ -745,119 +1121,66 @@ agent.talk "Hello"
|
|
|
745
1121
|
```
|
|
746
1122
|
</details>
|
|
747
1123
|
|
|
748
|
-
|
|
749
|
-
<summary>Automatic retries</summary>
|
|
750
|
-
<br>
|
|
1124
|
+
#### Everything else
|
|
751
1125
|
|
|
752
|
-
|
|
753
|
-
retry a 429 up to five times with a growing backoff before giving
|
|
754
|
-
up, so most request failures resolve on their own. Connection and
|
|
755
|
-
read timeouts are retried the same way. Set `retry_budget`
|
|
756
|
-
to change the number of retries, or `retry_budget: 0` to disable
|
|
757
|
-
them.
|
|
758
|
-
|
|
759
|
-
```ruby
|
|
760
|
-
require "llm"
|
|
761
|
-
|
|
762
|
-
llm = LLM.deepseek(key: ENV["KEY"])
|
|
763
|
-
agent = LLM::Agent.new(llm, retry_budget: 0)
|
|
764
|
-
agent.talk "Hello"
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
</details>
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
<details>
|
|
771
|
-
<summary>Usage and cost</summary>
|
|
1126
|
+
<details><summary>A2A</summary>
|
|
772
1127
|
<br>
|
|
773
1128
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
[LLM::
|
|
778
|
-
|
|
779
|
-
|
|
1129
|
+
The Agent 2 Agent (A2A) protocol has first-class support
|
|
1130
|
+
in llm.rb. The http and jsonrpc transports work out of the
|
|
1131
|
+
box. A2A skills are translated into subclasses of
|
|
1132
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) that can be
|
|
1133
|
+
used with
|
|
1134
|
+
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) or
|
|
1135
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
|
|
780
1136
|
|
|
781
1137
|
```ruby
|
|
782
1138
|
require "llm"
|
|
783
1139
|
|
|
784
|
-
llm
|
|
785
|
-
|
|
786
|
-
agent.
|
|
787
|
-
|
|
788
|
-
agent.token_usage # => LLM::Usage for the whole conversation
|
|
789
|
-
agent.cost # => LLM::Cost, priced from the registry
|
|
790
|
-
```
|
|
791
|
-
|
|
792
|
-
A `context_used` is one turn's worth - the live size of the most recent
|
|
793
|
-
assistant message - so it is what a context window is really being
|
|
794
|
-
spent on, and `context_usage` is that as a fraction of the window:
|
|
795
|
-
|
|
796
|
-
```ruby
|
|
797
|
-
agent.context_used # => tokens in the latest turn
|
|
798
|
-
agent.context_window # => the model's limit, or nil when unknown
|
|
799
|
-
agent.context_usage # => Rational, eg Rational(100, 10_000)
|
|
1140
|
+
llm = LLM.deepseek
|
|
1141
|
+
a2a = LLM::A2A.rest(url: "https://remote-agent.example.com")
|
|
1142
|
+
agent = LLM::Agent.new(llm, stream: $stdout, tools: a2a.skills)
|
|
1143
|
+
agent.talk "Run the skill"
|
|
800
1144
|
```
|
|
801
|
-
|
|
802
1145
|
</details>
|
|
803
1146
|
|
|
804
|
-
|
|
805
1147
|
<details>
|
|
806
|
-
<summary>
|
|
1148
|
+
<summary>Skills</summary>
|
|
807
1149
|
<br>
|
|
808
1150
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
factory method:
|
|
1151
|
+
A skill turns a markdown file into a callable tool. When the model
|
|
1152
|
+
calls it, the runtime spawns a subagent with the skill's instructions
|
|
1153
|
+
as its system prompt and the skill's own tool set. The subagent runs
|
|
1154
|
+
one turn and returns the result, then is discarded. Each call
|
|
1155
|
+
is fresh and stateless.
|
|
815
1156
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1157
|
+
A [LLM::Stream](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
|
|
1158
|
+
can be notified as a skill starts and when it returns. The `on_skill_return`
|
|
1159
|
+
callback hands back the subagent that ran the skill, so you can inspect
|
|
1160
|
+
its conversation, measure its usage, track costs or add a verification
|
|
1161
|
+
step (eg `subagent.talk("verify your work")`).
|
|
821
1162
|
|
|
822
|
-
|
|
823
|
-
of [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
|
|
824
|
-
that implements a number of callbacks that cover an agent's lifecycle.
|
|
825
|
-
The tracer feature provides visibility into what the runtime is doing,
|
|
826
|
-
and the tracer API lets other code hook into that feature.
|
|
1163
|
+
##### summary.md
|
|
827
1164
|
|
|
828
|
-
```
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1165
|
+
```markdown
|
|
1166
|
+
---
|
|
1167
|
+
name: summary
|
|
1168
|
+
description: Reads recent git history and writes a summary
|
|
1169
|
+
tools: all
|
|
1170
|
+
---
|
|
834
1171
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1172
|
+
Collect the recent git log, analyze each commit,
|
|
1173
|
+
and write a summary to summary.txt.
|
|
1174
|
+
```
|
|
838
1175
|
|
|
839
|
-
|
|
840
|
-
is a class-level DSL that accepts a Hash of properties. Each key resolves to a
|
|
841
|
-
corresponding class accessor: `name`, `description`, `model`, `tools`,
|
|
842
|
-
`instructions`, `schema`, `stream`, `tracer`, `concurrency`, `confirm`,
|
|
843
|
-
`path`, `skills`, `tool_budget`, and `retry_budget`. All options are
|
|
844
|
-
optional; zero or more can be set.
|
|
845
|
-
An error is raised for unknown keys so that typos are caught early.
|
|
1176
|
+
##### agent.rb
|
|
846
1177
|
|
|
847
1178
|
```ruby
|
|
848
1179
|
require "llm"
|
|
849
|
-
require "llm/tools"
|
|
850
1180
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
model: "deepseek-v4-pro",
|
|
855
|
-
tools: [LLM::Tool::Exec]
|
|
856
|
-
end
|
|
857
|
-
|
|
858
|
-
llm = LLM.deepseek(key: ENV["KEY"])
|
|
859
|
-
agent = Agent.new(llm)
|
|
860
|
-
agent.talk "Run 'date'"
|
|
1181
|
+
llm = LLM.deepseek
|
|
1182
|
+
agent = LLM::Agent.new(llm, skills: ["summary.md"])
|
|
1183
|
+
agent.talk "Summarize the last week of work"
|
|
861
1184
|
```
|
|
862
1185
|
</details>
|
|
863
1186
|
|
|
@@ -869,7 +1192,9 @@ Each provider is constructed with a class-level factory method on
|
|
|
869
1192
|
or
|
|
870
1193
|
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html). The
|
|
871
1194
|
same API drives every one of them, so switching providers is a one-line
|
|
872
|
-
change.
|
|
1195
|
+
change. One default differs: an OpenAI context speaks the Responses API
|
|
1196
|
+
(`mode: :responses`) unless it is built with `mode: :completions`, while
|
|
1197
|
+
every other provider defaults to chat completions.
|
|
873
1198
|
|
|
874
1199
|
#### What providers does llm.rb support?
|
|
875
1200
|
|
|
@@ -941,13 +1266,19 @@ llm = LLM.bedrock(
|
|
|
941
1266
|
</details>
|
|
942
1267
|
|
|
943
1268
|
<details>
|
|
944
|
-
<summary>
|
|
1269
|
+
<summary>Registry</summary>
|
|
945
1270
|
<br>
|
|
946
1271
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1272
|
+
The runtime ships with the [models.dev](https://models.dev)
|
|
1273
|
+
registry. The registry is a moving target and each new release
|
|
1274
|
+
of llm.rb includes an updated copy of it. The registry is loaded
|
|
1275
|
+
per-provider, on-demand and it provides information about the limits,
|
|
1276
|
+
costs, and capabilities of a model. The runtime uses the registry
|
|
1277
|
+
to implement methods like
|
|
1278
|
+
[LLM::Agent#cost](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#cost-instance_method)
|
|
1279
|
+
and the registry can be accessed through the
|
|
1280
|
+
[LLM::Provider#registry](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#registry-instance_method)
|
|
1281
|
+
method as well:
|
|
951
1282
|
|
|
952
1283
|
```ruby
|
|
953
1284
|
require "llm"
|
|
@@ -973,7 +1304,6 @@ libcurl. They share one interface, so switching is a one-word change.
|
|
|
973
1304
|
|
|
974
1305
|
```ruby
|
|
975
1306
|
llm = LLM.deepseek(
|
|
976
|
-
key: ENV["KEY"],
|
|
977
1307
|
transport: :net_http_persistent
|
|
978
1308
|
)
|
|
979
1309
|
```
|
|
@@ -987,13 +1317,16 @@ Providers accept two timeouts:
|
|
|
987
1317
|
|
|
988
1318
|
* `connect_timeout` - opening the connection. Defaults to 5 seconds.
|
|
989
1319
|
* `read_timeout` - waiting for a response on an idle connection.
|
|
990
|
-
Defaults to
|
|
1320
|
+
Defaults to 240 seconds (4 minutes).
|
|
1321
|
+
|
|
1322
|
+
A timeout is retriable, so it is often better to fail early and open a
|
|
1323
|
+
new session than to hold a connection open for a response that never
|
|
1324
|
+
comes. This is especially true for `read_timeout`. Both timeouts can
|
|
1325
|
+
be given as a custom value that is higher or lower than the default.
|
|
991
1326
|
|
|
992
|
-
|
|
993
|
-
local models. The legacy `timeout:` option remains as a shorthand for
|
|
994
|
-
`read_timeout`. Timeouts are retriable:
|
|
1327
|
+
A request that times out is retried automatically. The
|
|
995
1328
|
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
996
|
-
retries a timed out request up to its
|
|
1329
|
+
class retries a timed out request up to its
|
|
997
1330
|
[`retry_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#retry_budget-class_method)
|
|
998
1331
|
(five by default), so a dropped connection or a slow first token
|
|
999
1332
|
is often something we can recover from.
|
|
@@ -1001,7 +1334,7 @@ is often something we can recover from.
|
|
|
1001
1334
|
```ruby
|
|
1002
1335
|
llm = LLM.deepseek(
|
|
1003
1336
|
connect_timeout: 5, # opening the connection
|
|
1004
|
-
read_timeout: 600 #
|
|
1337
|
+
read_timeout: 600 # raise it when a model needs longer
|
|
1005
1338
|
)
|
|
1006
1339
|
```
|
|
1007
1340
|
</details>
|
|
@@ -1040,7 +1373,7 @@ that here.
|
|
|
1040
1373
|
```ruby
|
|
1041
1374
|
require "llm"
|
|
1042
1375
|
|
|
1043
|
-
llm = LLM.openai
|
|
1376
|
+
llm = LLM.openai
|
|
1044
1377
|
body = "llm.rb is Ruby's capable AI runtime."
|
|
1045
1378
|
embedding = llm.embed([body]).embeddings.first
|
|
1046
1379
|
|
|
@@ -1062,7 +1395,7 @@ the same across providers:
|
|
|
1062
1395
|
```ruby
|
|
1063
1396
|
require "llm"
|
|
1064
1397
|
|
|
1065
|
-
llm = LLM.openai
|
|
1398
|
+
llm = LLM.openai
|
|
1066
1399
|
res = llm.images.create(prompt: "a dog on a rocket to the moon")
|
|
1067
1400
|
IO.copy_stream res.images[0], "rocket.png"
|
|
1068
1401
|
```
|
|
@@ -1077,7 +1410,7 @@ to maintain a session across generations:
|
|
|
1077
1410
|
|
|
1078
1411
|
```ruby
|
|
1079
1412
|
require "llm"
|
|
1080
|
-
llm = LLM.deepseek
|
|
1413
|
+
llm = LLM.deepseek
|
|
1081
1414
|
|
|
1082
1415
|
##
|
|
1083
1416
|
# First generation
|
|
@@ -1091,8 +1424,74 @@ res = llm.images.create(prompt: "add a dog next to the rocket",
|
|
|
1091
1424
|
IO.copy_stream res.images[0], "rocket-with-dog.svg"
|
|
1092
1425
|
```
|
|
1093
1426
|
|
|
1427
|
+
## Docs
|
|
1428
|
+
|
|
1429
|
+
The [deepdive](docs/deepdive.md) is the reference for everything below
|
|
1430
|
+
the quick start. Each chapter is a topic of its own:
|
|
1431
|
+
|
|
1432
|
+
- **Fundamentals**:
|
|
1433
|
+
[Providers](docs/deepdive/fundamentals/providers.md),
|
|
1434
|
+
[Agents](docs/deepdive/fundamentals/agents.md),
|
|
1435
|
+
[Tools](docs/deepdive/fundamentals/tools.md),
|
|
1436
|
+
[Stream](docs/deepdive/fundamentals/stream.md),
|
|
1437
|
+
[Schema](docs/deepdive/fundamentals/schema.md),
|
|
1438
|
+
[Skills](docs/deepdive/fundamentals/skills.md)
|
|
1439
|
+
- **Features**:
|
|
1440
|
+
[Built-in tools](docs/deepdive/features/builtin_tools.md),
|
|
1441
|
+
[Concurrency](docs/deepdive/features/concurrency.md),
|
|
1442
|
+
[Embeddings](docs/deepdive/features/embeddings.md),
|
|
1443
|
+
[Files](docs/deepdive/features/files.md),
|
|
1444
|
+
[Moderations](docs/deepdive/features/moderations.md),
|
|
1445
|
+
[Responses](docs/deepdive/features/responses.md),
|
|
1446
|
+
[Database](docs/deepdive/features/database.md),
|
|
1447
|
+
[Console](docs/deepdive/features/console.md)
|
|
1448
|
+
- **Advanced**:
|
|
1449
|
+
[Context](docs/deepdive/advanced/context.md),
|
|
1450
|
+
[Compaction](docs/deepdive/advanced/compaction.md),
|
|
1451
|
+
[Cancellation](docs/deepdive/advanced/cancellation.md),
|
|
1452
|
+
[Intercept](docs/deepdive/advanced/intercept.md),
|
|
1453
|
+
[Transformer](docs/deepdive/advanced/transformer.md),
|
|
1454
|
+
[Transports](docs/deepdive/advanced/transports.md)
|
|
1455
|
+
- **Media**:
|
|
1456
|
+
[Images](docs/deepdive/media/images.md),
|
|
1457
|
+
[Audio](docs/deepdive/media/audio.md),
|
|
1458
|
+
[OCR](docs/deepdive/media/ocr.md)
|
|
1459
|
+
- **Protocols**:
|
|
1460
|
+
[MCP](docs/deepdive/protocols/mcp.md),
|
|
1461
|
+
[A2A](docs/deepdive/protocols/a2a.md)
|
|
1462
|
+
- **Reference**:
|
|
1463
|
+
[LLM::Response](docs/deepdive/reference/response.md),
|
|
1464
|
+
[LLM::Object](docs/deepdive/reference/object.md),
|
|
1465
|
+
[LLM::Cost](docs/deepdive/reference/cost.md),
|
|
1466
|
+
[Tracer](docs/deepdive/reference/tracer.md),
|
|
1467
|
+
[Model registry](docs/deepdive/reference/model_registry.md)
|
|
1468
|
+
|
|
1094
1469
|
## FAQ
|
|
1095
1470
|
|
|
1471
|
+
<details>
|
|
1472
|
+
<summary>Does it work on Windows?</summary>
|
|
1473
|
+
<br>
|
|
1474
|
+
I think so. Our Windows build is green on CI.
|
|
1475
|
+
That's the only signal I have to go by.
|
|
1476
|
+
|
|
1477
|
+
However support is limited compared to
|
|
1478
|
+
other platforms. The `fork` strategy is not
|
|
1479
|
+
available (Windows does not provide fork),
|
|
1480
|
+
and the `ractor` strategy has one test out
|
|
1481
|
+
of many that hangs - so it is assumed ractor
|
|
1482
|
+
support is not perfect.
|
|
1483
|
+
|
|
1484
|
+
The `curses` gem that the console requires has
|
|
1485
|
+
to be compiled from source, and this can be a
|
|
1486
|
+
challenge on Windows without a compiler toolchain.
|
|
1487
|
+
By default the console does not work but it might
|
|
1488
|
+
work if you are able to install the `curses` gem.
|
|
1489
|
+
|
|
1490
|
+
So it mostly works as long as you avoid the
|
|
1491
|
+
parts that don't work. Pull requests that
|
|
1492
|
+
improve Windows support are very welcome.
|
|
1493
|
+
</details>
|
|
1494
|
+
|
|
1096
1495
|
<details>
|
|
1097
1496
|
<summary>Where can I see llm.rb in action?</summary>
|
|
1098
1497
|
<br>
|
|
@@ -1115,42 +1514,8 @@ are running on your own hardware.
|
|
|
1115
1514
|
<summary>I have a limited budget. What should I do?</summary>
|
|
1116
1515
|
<br>
|
|
1117
1516
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
providers. This can be difficult though because
|
|
1121
|
-
a capable model requires hardware that can
|
|
1122
|
-
match it. If you have the ability to self-host,
|
|
1123
|
-
this would be my first option.
|
|
1124
|
-
|
|
1125
|
-
The second option is DeepSeek. <br>
|
|
1126
|
-
The deepseek-v4-flash model costs pennies to use. <br>
|
|
1127
|
-
And llm.rb has been optimized for deepseek. For example,
|
|
1128
|
-
DeepSeek does not have image generation capabilities
|
|
1129
|
-
but on the llm.rb runtime it does (vector graphics only,
|
|
1130
|
-
though).
|
|
1131
|
-
|
|
1132
|
-
The same is true for structured outputs. DeepSeek does
|
|
1133
|
-
not support structured outputs in the same way as OpenAI or
|
|
1134
|
-
Google, but the llm.rb runtime makes it appear as
|
|
1135
|
-
though it does, through the `json_object` response
|
|
1136
|
-
type.
|
|
1137
|
-
|
|
1138
|
-
If you're on a budget, DeepSeek is hard to beat.
|
|
1139
|
-
</details>
|
|
1140
|
-
<details>
|
|
1141
|
-
<summary>Sources other than GitHub?</summary>
|
|
1142
|
-
<br>
|
|
1143
|
-
<p>
|
|
1144
|
-
We are on the <a href="https://radicle.network">radicle.network</a> as well.
|
|
1145
|
-
<br>
|
|
1146
|
-
Every commit that lands on GitHub also lands on Radicle.
|
|
1147
|
-
<br>
|
|
1148
|
-
Our repository ID is z2PtfQ6dYwyYaW2aGrztG1sMyDmCE.
|
|
1149
|
-
<br>
|
|
1150
|
-
Browse on <a
|
|
1151
|
-
href="https://radicle.network/nodes/iris.radicle.network/z2PtfQ6dYwyYaW2aGrztG1sMyDmCE">the
|
|
1152
|
-
web</a>.
|
|
1153
|
-
</p>
|
|
1517
|
+
If you're on a budget, [DeepSeek](https://platform.deepseek.com)
|
|
1518
|
+
is hard to beat and it works really well with llm.rb.
|
|
1154
1519
|
</details>
|
|
1155
1520
|
|
|
1156
1521
|
<details>
|
|
@@ -1162,9 +1527,9 @@ years ago by
|
|
|
1162
1527
|
[@altruby](https://github.com/altruby) and
|
|
1163
1528
|
[@antaz](https://github.com/altruby). The primary
|
|
1164
1529
|
maintainer is [@altruby](https://github.com/altruby).
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1530
|
+
There have been multiple other contributors over the
|
|
1531
|
+
last three years and new contributors are always
|
|
1532
|
+
welcome.
|
|
1168
1533
|
</details>
|
|
1169
1534
|
|
|
1170
1535
|
<details>
|
|
@@ -1173,18 +1538,9 @@ always welcome.
|
|
|
1173
1538
|
|
|
1174
1539
|
It is battle tested daily.
|
|
1175
1540
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
website where multiple llm.rb agents are deployed. I'm
|
|
1180
|
-
aware of at least one production Rails deployment
|
|
1181
|
-
at a large-ish company.
|
|
1182
|
-
|
|
1183
|
-
And this git repository includes llm.rb agents that help me
|
|
1184
|
-
maintain the documentation and perform other repository
|
|
1185
|
-
maintainence. The feedback loop is constant. Outside of that
|
|
1186
|
-
there is a large test suite that covers live requests (recorded
|
|
1187
|
-
by VCR) and database interactions.
|
|
1541
|
+
It powers the [r.uby.dev](https://r.uby.dev)
|
|
1542
|
+
website and I'm aware of at least one production
|
|
1543
|
+
Rails deployment at a large-ish company.
|
|
1188
1544
|
</details>
|
|
1189
1545
|
|
|
1190
1546
|
## License
|