llm.rb 11.3.1 → 12.1.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 +354 -2
- data/LICENSE +93 -17
- data/README.md +226 -616
- data/data/anthropic.json +322 -426
- data/data/bedrock.json +2634 -1144
- data/data/deepinfra.json +1513 -0
- data/data/deepseek.json +57 -28
- data/data/google.json +411 -771
- data/data/openai.json +1104 -771
- data/data/xai.json +141 -292
- data/data/zai.json +263 -141
- data/lib/llm/active_record/acts_as_agent.rb +3 -41
- data/lib/llm/active_record/acts_as_llm.rb +18 -0
- data/lib/llm/active_record.rb +3 -3
- data/lib/llm/agent.rb +25 -1
- data/lib/llm/context.rb +17 -5
- data/lib/llm/contract/completion.rb +2 -2
- data/lib/llm/json_adapter.rb +29 -3
- data/lib/llm/provider.rb +3 -3
- data/lib/llm/providers/deepinfra/audio.rb +66 -0
- data/lib/llm/providers/deepinfra/images.rb +90 -0
- data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
- data/lib/llm/providers/deepinfra.rb +100 -0
- data/lib/llm/providers/deepseek/images.rb +109 -0
- data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
- data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
- data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
- data/lib/llm/providers/deepseek.rb +4 -2
- data/lib/llm/providers/google/request_adapter.rb +22 -5
- data/lib/llm/providers/google.rb +4 -4
- data/lib/llm/providers/llamacpp.rb +5 -5
- data/lib/llm/providers/openai/audio.rb +6 -2
- data/lib/llm/providers/openai/images.rb +9 -50
- data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
- data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
- data/lib/llm/providers/openai/responses.rb +1 -0
- data/lib/llm/providers/openai/stream_parser.rb +5 -6
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/providers/xai/images.rb +49 -26
- data/lib/llm/providers/xai.rb +2 -2
- data/lib/llm/repl/input.rb +64 -0
- data/lib/llm/repl/status.rb +30 -0
- data/lib/llm/repl/stream.rb +46 -0
- data/lib/llm/repl/transcript.rb +61 -0
- data/lib/llm/repl/window.rb +107 -0
- data/lib/llm/repl.rb +78 -0
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +7 -1
- data/lib/llm/schema/renderer.rb +121 -0
- data/lib/llm/schema.rb +30 -0
- data/lib/llm/sequel/agent.rb +2 -43
- data/lib/llm/sequel/plugin.rb +25 -7
- data/lib/llm/tools/chdir.rb +23 -0
- data/lib/llm/tools/git.rb +41 -0
- data/lib/llm/tools/mkdir.rb +32 -0
- data/lib/llm/tools/pwd.rb +20 -0
- data/lib/llm/tools/read_file.rb +40 -0
- data/lib/llm/tools/rg.rb +46 -0
- data/lib/llm/tools/shell.rb +48 -0
- data/lib/llm/tools/swap_text.rb +25 -0
- data/lib/llm/tools/write_file.rb +24 -0
- data/lib/llm/tools.rb +5 -0
- data/lib/llm/tracer/telemetry.rb +4 -6
- data/lib/llm/tracer.rb +9 -21
- data/lib/llm/transport/execution.rb +16 -1
- data/lib/llm/transport/net_http_adapter.rb +1 -1
- data/lib/llm/uridata.rb +16 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +9 -0
- data/llm.gemspec +5 -18
- data/resources/deepdive.md +829 -263
- metadata +31 -18
- data/lib/llm/tracer/langsmith.rb +0 -144
data/resources/deepdive.md
CHANGED
|
@@ -12,421 +12,987 @@
|
|
|
12
12
|
|
|
13
13
|
> A [r.uby.dev](https://r.uby.dev) project.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Welcome
|
|
16
|
+
|
|
17
|
+
Welcome to the llm.rb deepdive. You are reading this document
|
|
18
|
+
in the markdown format. An optimized version exists
|
|
19
|
+
at [https://r.uby.dev/llm/deepdive](https://r.uby.dev/llm/deepdive)
|
|
20
|
+
and it is both easier to read and navigate.
|
|
21
|
+
|
|
22
|
+
This document is a continuation of the [homepage documentation](https://r.uby.dev/llm).
|
|
23
|
+
It assumes you are familiar with the basics already, and focuses on
|
|
24
|
+
features that didn't make it into the homepage documentation.
|
|
25
|
+
|
|
26
|
+
## Table of contents
|
|
27
|
+
|
|
28
|
+
- [Agents](#agents)
|
|
29
|
+
- [As a subclass](#as-a-subclass)
|
|
30
|
+
- [As an object](#as-an-object)
|
|
31
|
+
- [Skills](#skills)
|
|
32
|
+
- [SKILL.md](#skillmd)
|
|
33
|
+
- [Run it](#run-it)
|
|
34
|
+
- [MCP](#mcp)
|
|
35
|
+
- [stdio](#stdio)
|
|
36
|
+
- [http](#http)
|
|
37
|
+
- [A2A](#a2a)
|
|
38
|
+
- [rest](#rest)
|
|
39
|
+
- [jsonrpc](#jsonrpc)
|
|
40
|
+
- [Transports](#transports)
|
|
41
|
+
- [net/http](#nethttp)
|
|
42
|
+
- [net/http/persistent](#nethttppersistent)
|
|
43
|
+
- [curb](#curb)
|
|
44
|
+
- [Stream](#stream)
|
|
45
|
+
- [IO-like object](#io-like-object)
|
|
46
|
+
- [LLM::Stream](#llmstream)
|
|
47
|
+
- [ORM](#orm)
|
|
48
|
+
- [ActiveRecord](#activerecord)
|
|
49
|
+
- [Sequel](#sequel)
|
|
50
|
+
- [Schema](#schema)
|
|
51
|
+
- [Estimation](#estimation)
|
|
52
|
+
- [Cancellation](#cancellation)
|
|
53
|
+
- [Cancel a request](#cancel-a-request)
|
|
54
|
+
- [Tracer](#tracer)
|
|
55
|
+
- [Provider-wide tracer](#provider-wide-tracer)
|
|
56
|
+
- [Agent-local tracer](#agent-local-tracer)
|
|
57
|
+
- [REPL](#repl)
|
|
58
|
+
- [LLM::Agent](#llmagent)
|
|
59
|
+
- [Images](#images)
|
|
60
|
+
- [Generation](#generation)
|
|
61
|
+
- [Edits](#edits)
|
|
62
|
+
- [Audio](#audio)
|
|
63
|
+
- [text-to-speech](#text-to-speech)
|
|
64
|
+
- [speech-to-text](#speech-to-text)
|
|
65
|
+
- [translation](#translation)
|
|
66
|
+
|
|
67
|
+
## Agents
|
|
68
|
+
|
|
69
|
+
An agent is represented by the
|
|
70
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
71
|
+
class, and it is built on top of
|
|
72
|
+
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) -
|
|
73
|
+
the heart of the runtime. An agent manages the tool loop automatically,
|
|
74
|
+
implements a tool loop guard for misbehaving models, and
|
|
75
|
+
it can use five different concurrency strategies to execute
|
|
76
|
+
tools.
|
|
77
|
+
|
|
78
|
+
An agent can be a subclass of
|
|
79
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
|
|
80
|
+
or a direct
|
|
81
|
+
instance of it. The subclass approach is useful when you
|
|
82
|
+
want reusable agents that can attach behavior (as methods)
|
|
83
|
+
to their own class.
|
|
84
|
+
|
|
85
|
+
#### As a subclass
|
|
86
|
+
|
|
87
|
+
A subclass of
|
|
88
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
89
|
+
can define its model, tools,
|
|
90
|
+
and other attributes at the class-level. All of these
|
|
91
|
+
attributes are optional, and they act as defaults that
|
|
92
|
+
can be overriden on the instance level.
|
|
93
|
+
|
|
94
|
+
The example uses the `:fork` concurrency model. It has
|
|
95
|
+
two primary benefits: tools are run in parallel, and in
|
|
96
|
+
a separate process with a separate memory address space.
|
|
97
|
+
|
|
98
|
+
The example purposefully demonstrates how the attributes
|
|
99
|
+
can be lazily defined with a block, or a Symbol that is
|
|
100
|
+
evaluated as an instance method on the subclass. It is
|
|
101
|
+
not strictly neccessary, though, and the example would
|
|
102
|
+
be simpler without it.
|
|
16
103
|
|
|
17
|
-
|
|
18
|
-
|
|
104
|
+
```ruby
|
|
105
|
+
class Agent < LLM::Agent
|
|
106
|
+
model "deepseek-v4-pro"
|
|
107
|
+
tools { [DoResearch, FinalizeResearch, ActOnResearch] }
|
|
108
|
+
stream { $stdout }
|
|
109
|
+
tracer :set_tracer
|
|
110
|
+
concurrency :fork
|
|
111
|
+
|
|
112
|
+
def research!
|
|
113
|
+
talk "start the research"
|
|
114
|
+
end
|
|
19
115
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def set_tracer
|
|
119
|
+
LLM::Tracer::Logger.new(llm, io: $stderr)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
123
|
+
agent = Agent.new(llm).tap(&:research!)
|
|
124
|
+
agent.talk "How did the research go?"
|
|
125
|
+
```
|
|
24
126
|
|
|
25
|
-
|
|
26
|
-
DeepSeek, xAI, Z.ai, AWS Bedrock, Ollama, and llama.cpp. ActiveRecord and
|
|
27
|
-
Sequel support are built in, along with concurrent tool execution through
|
|
28
|
-
threads, tasks, fibers, ractors, and fork.
|
|
127
|
+
#### As an object
|
|
29
128
|
|
|
30
|
-
|
|
129
|
+
The more direct, and sometimes more convienent approach, is to
|
|
130
|
+
create an instance of
|
|
131
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
132
|
+
directly. The same attributes can be provided as the
|
|
133
|
+
second argument given to
|
|
134
|
+
[`LLM::Agent.new`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
|
|
135
|
+
and the same lazy evaluation rules apply. This approach can be
|
|
136
|
+
great for prototyping quickly, and you can always turn to a
|
|
137
|
+
subclass later if that makes more sense.
|
|
31
138
|
|
|
32
|
-
```
|
|
33
|
-
|
|
139
|
+
```ruby
|
|
140
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
141
|
+
agent = LLM::Agent.new(llm, stream: $stdout)
|
|
142
|
+
agent.talk "Hello, fellow agent"
|
|
34
143
|
```
|
|
35
144
|
|
|
36
|
-
|
|
145
|
+
[Back to top](#table-of-contents)
|
|
37
146
|
|
|
38
|
-
|
|
147
|
+
## Tools
|
|
39
148
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
149
|
+
A tool extends the capabilities of a model. <br>
|
|
150
|
+
A tool is a subclass of
|
|
151
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
152
|
+
that has a name,
|
|
153
|
+
a description, and an optional set of typed parameters.
|
|
154
|
+
|
|
155
|
+
A tool also has a method associated with it, and when the
|
|
156
|
+
model calls a tool it will do so through this method –
|
|
157
|
+
alongside any parameters the tool might have defined.
|
|
158
|
+
|
|
159
|
+
In other words, a tool provides a way for a model to
|
|
160
|
+
call a method you have written, and it returns a value
|
|
161
|
+
to the model that is considered the tool's response.
|
|
162
|
+
The model then proceeds to process the tool's response,
|
|
163
|
+
and then might generate its own response, or perhaps call
|
|
164
|
+
another tool.
|
|
165
|
+
|
|
166
|
+
#### LLM::Tool
|
|
167
|
+
|
|
168
|
+
A tool can be defined by subclassing
|
|
169
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
170
|
+
with
|
|
171
|
+
a name, description, and optional set of parameters. The
|
|
172
|
+
tool name, and description should be informative so the
|
|
173
|
+
model can understand what the tool does and how it can
|
|
174
|
+
serve a user's query.
|
|
44
175
|
|
|
45
176
|
```ruby
|
|
46
177
|
require "llm"
|
|
178
|
+
require "shellwords"
|
|
179
|
+
|
|
180
|
+
class Shell < LLM::Shell
|
|
181
|
+
name "shell"
|
|
182
|
+
description "execute a shell command"
|
|
183
|
+
parameter :name, String, "the command's name"
|
|
184
|
+
parameter :arguments, Array[String], "One or more arguments"
|
|
185
|
+
required %i[name]
|
|
186
|
+
defaults arguments: []
|
|
187
|
+
|
|
188
|
+
def call(name:, arguments:)
|
|
189
|
+
out = `#{name.shellscape} #{arguments.map(&:shellescape).join(" ")}`
|
|
190
|
+
{ok: $?.success?, out:}
|
|
191
|
+
end
|
|
192
|
+
end
|
|
47
193
|
|
|
48
|
-
llm = LLM.
|
|
49
|
-
agent = LLM::Agent.new(llm, stream: $stdout)
|
|
50
|
-
agent.talk "
|
|
194
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
195
|
+
agent = LLM::Agent.new(llm, tools: [Shell], stream: $stdout)
|
|
196
|
+
agent.talk "What files are in the current working directory?"
|
|
51
197
|
```
|
|
52
198
|
|
|
53
|
-
####
|
|
199
|
+
#### Errors
|
|
200
|
+
|
|
201
|
+
Exceptions that might be raised by a tool are automatically
|
|
202
|
+
rescued and returned to the model as a structured error.
|
|
203
|
+
Otherwise – the conversation's history could be left
|
|
204
|
+
in an invalid state.
|
|
54
205
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
206
|
+
That's because a tool call must complete with a tool response,
|
|
207
|
+
that's the only valid response a model expects, so even in the
|
|
208
|
+
case of an error, something must be returned that communicates
|
|
209
|
+
what happened.
|
|
59
210
|
|
|
60
211
|
```ruby
|
|
61
|
-
|
|
212
|
+
class Error < LLM::Tool
|
|
213
|
+
name "error"
|
|
214
|
+
description "demo how errors are handled"
|
|
215
|
+
|
|
216
|
+
##
|
|
217
|
+
# Returns
|
|
218
|
+
# {error: true, kind: "RuntimeError", message: "boom"}
|
|
219
|
+
def call
|
|
220
|
+
raise "boom"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
62
224
|
|
|
63
|
-
|
|
64
|
-
agent = LLM::Agent.new(llm, stream: $stdout)
|
|
225
|
+
## Skills
|
|
65
226
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
227
|
+
The skill concept is borrowed from tools like Claude and
|
|
228
|
+
Codex, but llm.rb gives it a runtime of its own. A skill
|
|
229
|
+
is a directory with a `SKILL.md` file. That file contains
|
|
230
|
+
frontmatter where the skill's name, description, and tools
|
|
231
|
+
can be declared.
|
|
232
|
+
|
|
233
|
+
#### SKILL.md
|
|
234
|
+
|
|
235
|
+
The `SKILL.md` file can look like this. When a skill runs,
|
|
236
|
+
the runtime spawns a subagent with its own context window
|
|
237
|
+
and message history. Some context is inherited from the
|
|
238
|
+
parent agent, though.
|
|
239
|
+
|
|
240
|
+
By default the subagent can only access the tools declared
|
|
241
|
+
by the skill. The `inherit` directive lets it inherit the
|
|
242
|
+
parent agent's tools instead, including A2A and MCP tools.
|
|
243
|
+
|
|
244
|
+
```markdown
|
|
245
|
+
---
|
|
246
|
+
name: git-skill
|
|
247
|
+
description: reads my git history and writes a summary
|
|
248
|
+
tools: ['git-log', 'git-show', 'write-file']
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Task
|
|
252
|
+
|
|
253
|
+
Collect a log of recent history.
|
|
254
|
+
Analyze each commit.
|
|
255
|
+
Write a summary to summary.txt
|
|
71
256
|
```
|
|
72
257
|
|
|
73
|
-
####
|
|
258
|
+
#### Run it
|
|
74
259
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
260
|
+
Given the skill above, llm.rb only needs the path to the
|
|
261
|
+
directory that contains `SKILL.md`. Under the hood, a skill
|
|
262
|
+
is represented as a tool the model can call. That means
|
|
263
|
+
a skill can be called whenever it satisfies the user's
|
|
264
|
+
request – in the same way that a regular tool can.
|
|
265
|
+
|
|
266
|
+
This feature also works with both the ActiveRecord, and
|
|
267
|
+
Sequel integrations.
|
|
80
268
|
|
|
81
269
|
```ruby
|
|
82
270
|
require "llm"
|
|
83
271
|
|
|
84
|
-
llm = LLM.
|
|
85
|
-
|
|
86
|
-
|
|
272
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
273
|
+
agent = LLM::Agent.new(llm, skills: [__dir__])
|
|
274
|
+
agent.talk "run the git skill"
|
|
87
275
|
```
|
|
88
276
|
|
|
89
|
-
|
|
277
|
+
[Back to top](#table-of-contents)
|
|
278
|
+
|
|
279
|
+
## MCP
|
|
280
|
+
|
|
281
|
+
#### stdio
|
|
282
|
+
|
|
283
|
+
The stdio transport connects to an MCP server that is launched as a
|
|
284
|
+
separate process, and both its standard input and standard output
|
|
285
|
+
streams are used for communication. It is recommended but not
|
|
286
|
+
required to execute commands for a stdio transport over a
|
|
287
|
+
persistent session via the
|
|
288
|
+
[`LLM::MCP#session`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html#session-instance_method)
|
|
289
|
+
method – otherwise
|
|
290
|
+
you could end up launching the same process multiple times.
|
|
90
291
|
|
|
91
292
|
```ruby
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
293
|
+
require "llm"
|
|
294
|
+
|
|
295
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
296
|
+
mcp = LLM::MCP.stdio(argv: ["npx", "-y", "@forgejo/mcp-server"])
|
|
297
|
+
agent = LLM::Agent.new(llm)
|
|
298
|
+
|
|
299
|
+
mcp.session do
|
|
300
|
+
agent.talk "What's happening on forgejo?", tools: mcp.tools
|
|
301
|
+
end
|
|
95
302
|
```
|
|
96
303
|
|
|
97
|
-
|
|
98
|
-
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
|
|
99
|
-
It does the same thing but manages the loop for you.
|
|
304
|
+
#### http
|
|
100
305
|
|
|
101
|
-
|
|
306
|
+
The http transport connects to an MCP server over HTTP, and unlike
|
|
307
|
+
the stdio transport, the MCP server does not have to be running
|
|
308
|
+
locally. Popular services like GitHub provide their own MCP server
|
|
309
|
+
over HTTP, and it is one of the most capable MCP servers I have
|
|
310
|
+
used.
|
|
311
|
+
|
|
312
|
+
Unlike the stdio transport,
|
|
313
|
+
[`LLM::MCP#session`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html#session-instance_method)
|
|
314
|
+
carries little benefit for the http transport and it can be
|
|
315
|
+
omitted. It is recommended to consider the `net_http_persistent`
|
|
316
|
+
transport for MCP interactions that run over HTTP, otherwise
|
|
317
|
+
you could end up tearing down and setting up the same connection
|
|
318
|
+
multiple times.
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
require "llm"
|
|
322
|
+
|
|
323
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
324
|
+
mcp = LLM::MCP.http(
|
|
325
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
326
|
+
headers: {
|
|
327
|
+
"Authorization" => "Bearer #{ENV.fetch('GITHUB_PAT')}"
|
|
328
|
+
},
|
|
329
|
+
transport: :net_http_persistent
|
|
330
|
+
)
|
|
331
|
+
agent = LLM::Agent.new(llm)
|
|
332
|
+
agent.talk "What's happening on GitHub?", tools: mcp.tools
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
[Back to top](#table-of-contents)
|
|
336
|
+
|
|
337
|
+
## A2A
|
|
102
338
|
|
|
103
|
-
####
|
|
339
|
+
#### rest
|
|
104
340
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
341
|
+
The rest transport communicates with other agents via A2A
|
|
342
|
+
endpoints that speak both HTTP and JSON. The skills advertised
|
|
343
|
+
by an agent become subclasses of
|
|
344
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
345
|
+
that can be used by both
|
|
346
|
+
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html),
|
|
347
|
+
and [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
348
|
+
– similar to how MCP tools become subclasses of
|
|
349
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html).
|
|
109
350
|
|
|
110
351
|
```ruby
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def call(path:)
|
|
118
|
-
{contents: File.read(path)}
|
|
119
|
-
end
|
|
120
|
-
end
|
|
352
|
+
require "llm"
|
|
353
|
+
|
|
354
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
355
|
+
a2a = LLM::A2A.rest(url: "https://agent.example.com")
|
|
356
|
+
agent = LLM::Agent.new(llm, tools: a2a.skills)
|
|
357
|
+
agent.talk "What's happening, fellow agent?"
|
|
121
358
|
```
|
|
122
359
|
|
|
123
|
-
|
|
360
|
+
#### jsonrpc
|
|
361
|
+
|
|
362
|
+
The jsonrpc transport communicates with other agents via HTTP
|
|
363
|
+
and a protocol known as jsonrpc. Sometimes an agent will
|
|
364
|
+
implement both, or just one of each. An agent's card, which
|
|
365
|
+
is represented by an instance of
|
|
366
|
+
[`LLM::A2A::Card`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A/Card.html),
|
|
367
|
+
can be
|
|
368
|
+
used to discover available transports via the
|
|
369
|
+
[`LLM::A2A::Card#interfaces`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A/Card.html#interfaces-instance_method)
|
|
370
|
+
method.
|
|
124
371
|
|
|
125
372
|
```ruby
|
|
126
|
-
|
|
127
|
-
|
|
373
|
+
require "llm"
|
|
374
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
375
|
+
a2a = LLM::A2A.jsonrpc(url: "https://agent.example.com")
|
|
376
|
+
agent = LLM::Agent.new(llm, tools: a2a.skills)
|
|
377
|
+
agent.talk "What's happening, fellow agent?"
|
|
128
378
|
```
|
|
129
379
|
|
|
130
|
-
[
|
|
131
|
-
|
|
132
|
-
|
|
380
|
+
[Back to top](#table-of-contents)
|
|
381
|
+
|
|
382
|
+
## Transports
|
|
133
383
|
|
|
134
|
-
|
|
384
|
+
The [`LLM::Provider`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html),
|
|
385
|
+
[`LLM::MCP`](https://r.uby.dev/api-docs/llm.rb/LLM/MCP.html), and
|
|
386
|
+
[`LLM::A2A`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A.html) classes
|
|
387
|
+
all accept a `transport` option that decides which library
|
|
388
|
+
will be used for HTTP communication. There are three options out
|
|
389
|
+
of the box:
|
|
390
|
+
[`net-http`](https://github.com/ruby/net-http),
|
|
391
|
+
[`net-http-persistent`](https://github.com/drbrain/net-http-persistent),
|
|
392
|
+
and [`curb`](https://github.com/taf2/curb).
|
|
135
393
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
394
|
+
#### net/http
|
|
395
|
+
|
|
396
|
+
The [`net/http`](https://github.com/ruby/net-http) transport is represented by the symbol `:net_http`. <br>
|
|
397
|
+
It is the default transport.
|
|
140
398
|
|
|
141
399
|
```ruby
|
|
142
|
-
|
|
143
|
-
model "gpt-5.4-mini"
|
|
144
|
-
tools ReadFile
|
|
145
|
-
concurrency :thread
|
|
146
|
-
end
|
|
400
|
+
require "llm"
|
|
147
401
|
|
|
148
|
-
llm = LLM.
|
|
149
|
-
|
|
150
|
-
|
|
402
|
+
llm = LLM.deepseek(key: "...", transport: :net_http)
|
|
403
|
+
mcp = LLM::MCP.http(url: "...", transport: :net_http)
|
|
404
|
+
a2a = LLM::A2A.rest(url: "...", transport: :net_http)
|
|
151
405
|
```
|
|
152
406
|
|
|
153
|
-
|
|
407
|
+
#### net/http/persistent
|
|
154
408
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
[`
|
|
159
|
-
<br>
|
|
160
|
-
The model will return data that matches your schema instead of free text.
|
|
409
|
+
The [`net/http/persistent`](https://github.com/drbrain/net-http-persistent) transport is represented by the symbol `:net_http_persistent`. <br>
|
|
410
|
+
It maintains a connection pool so the cost of tearing down and
|
|
411
|
+
setting up a connection repeatedly is kept low, and it is built
|
|
412
|
+
on top of [`net/http`](https://github.com/ruby/net-http).
|
|
161
413
|
|
|
162
414
|
```ruby
|
|
163
|
-
|
|
164
|
-
property :category, Enum["performance", "security", "outage"]
|
|
165
|
-
property :summary, String, "Short summary"
|
|
166
|
-
property :services, Array[String], "Impacted services"
|
|
167
|
-
required %i[category summary services]
|
|
168
|
-
end
|
|
415
|
+
require "llm"
|
|
169
416
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
417
|
+
llm = LLM.deepseek(key: "...", transport: :net_http_persistent)
|
|
418
|
+
mcp = LLM::MCP.http(url: "...", transport: :net_http_persistent)
|
|
419
|
+
a2a = LLM::A2A.rest(url: "...", transport: :net_http_persistent)
|
|
173
420
|
```
|
|
174
421
|
|
|
175
|
-
|
|
422
|
+
#### curb
|
|
423
|
+
|
|
424
|
+
The [`curb`](https://github.com/taf2/curb) transport is represented by the symbol `:curb`. <br>
|
|
425
|
+
It provides bindings for libcurl – a widely used, highly portable
|
|
426
|
+
and feature-rich HTTP library written in C.
|
|
176
427
|
|
|
177
428
|
```ruby
|
|
178
|
-
|
|
179
|
-
category: LLM::Schema.new.string.enum("bug", "feature").required,
|
|
180
|
-
summary: LLM::Schema.new.string.required
|
|
181
|
-
)
|
|
429
|
+
require "llm"
|
|
182
430
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
431
|
+
llm = LLM.deepseek(key: "...", transport: :curb)
|
|
432
|
+
mcp = LLM::MCP.http(url: "...", transport: :curb)
|
|
433
|
+
a2a = LLM::A2A.rest(url: "...", transport: :curb)
|
|
186
434
|
```
|
|
187
435
|
|
|
188
|
-
|
|
436
|
+
[Back to top](#table-of-contents)
|
|
437
|
+
|
|
438
|
+
## Stream
|
|
189
439
|
|
|
190
|
-
####
|
|
440
|
+
#### IO-like object
|
|
191
441
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
442
|
+
Any object that implements the `#<<` method can receive
|
|
443
|
+
chunks from a stream. That includes objects like `$stdout`.
|
|
444
|
+
This form of streaming is simple and limited. It is the
|
|
445
|
+
equivalent of
|
|
446
|
+
[`LLM::Stream#on_content`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_content-instance_method),
|
|
447
|
+
and doesn't include
|
|
448
|
+
any of the other
|
|
449
|
+
[`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
|
|
450
|
+
hooks.
|
|
197
451
|
|
|
198
452
|
```ruby
|
|
199
|
-
|
|
453
|
+
require "llm"
|
|
454
|
+
|
|
455
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
456
|
+
agent = LLM::Agent.new(llm, stream: $stdout)
|
|
457
|
+
agent.talk "hello world"
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
#### LLM::Stream
|
|
461
|
+
|
|
462
|
+
The [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
|
|
463
|
+
class provides many hooks that a subclass
|
|
464
|
+
can implement. They range from being notified when a tool call
|
|
465
|
+
starts to when a tool call finishes, or when a conversation is
|
|
466
|
+
due to be compacted because the context window exceeded a defined
|
|
467
|
+
limit. All these callbacks support a responsive user interface
|
|
468
|
+
where the user is always aware of what is happening behind the
|
|
469
|
+
scenes.
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
class Stream < LLM::Stream
|
|
200
473
|
def on_content(content)
|
|
201
|
-
|
|
474
|
+
puts content
|
|
202
475
|
end
|
|
203
476
|
|
|
204
477
|
def on_reasoning_content(content)
|
|
205
|
-
|
|
478
|
+
puts content
|
|
206
479
|
end
|
|
207
|
-
end
|
|
208
480
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
481
|
+
def on_tool_call(tool, error)
|
|
482
|
+
# this callback can be used to either log a tool call,
|
|
483
|
+
# or execute a tool call during a stream.
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def on_tool_return(tool, result)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def on_compaction(ctx, compactor)
|
|
490
|
+
# this callback is called *before* a compact happens
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def on_compaction_finish(ctx, compactor)
|
|
494
|
+
# this callback is called *after* a compact happens
|
|
495
|
+
end
|
|
496
|
+
end
|
|
212
497
|
```
|
|
213
498
|
|
|
214
|
-
|
|
499
|
+
[Back to top](#table-of-contents)
|
|
215
500
|
|
|
216
|
-
|
|
501
|
+
## Serialization
|
|
217
502
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
on
|
|
503
|
+
The [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
|
|
504
|
+
class can be serialized to JSON and stored in a string or on disk.
|
|
505
|
+
That is powerful because a context contains runtime state that can
|
|
506
|
+
be restored later, in a different process or even on a different
|
|
507
|
+
machine. And because an agent is implemented on top of
|
|
508
|
+
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
|
|
509
|
+
this feature works for [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
|
|
510
|
+
too.
|
|
223
511
|
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
name: release
|
|
227
|
-
description: Prepare a release
|
|
228
|
-
tools: ["search-docs", "git"]
|
|
229
|
-
---
|
|
512
|
+
#### Save to disk
|
|
230
513
|
|
|
231
|
-
|
|
514
|
+
The runtime can serialize its state to a string, a text file, or
|
|
515
|
+
a database column. The option that fits best depends on your application
|
|
516
|
+
and environment. Web applications might be more interested in the [ORM](#orm)
|
|
517
|
+
feature, which is built on top of the serialization feature.
|
|
232
518
|
|
|
233
|
-
|
|
519
|
+
```ruby
|
|
520
|
+
##
|
|
521
|
+
# Create a provider
|
|
522
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
523
|
+
|
|
524
|
+
##
|
|
525
|
+
# Save agent
|
|
526
|
+
agent1 = LLM::Agent.new(llm)
|
|
527
|
+
agent1.talk "remember my name is robert"
|
|
528
|
+
agent1.save(path: "agent.json")
|
|
529
|
+
|
|
530
|
+
##
|
|
531
|
+
# Restore agent
|
|
532
|
+
agent2 = LLM::Agent.new(llm, stream: $stdout)
|
|
533
|
+
agent2.restore(path: "agent.json")
|
|
534
|
+
agent2.talk "what's my name?"
|
|
234
535
|
```
|
|
235
536
|
|
|
537
|
+
## ORM
|
|
538
|
+
|
|
539
|
+
Both ActiveRecord, and Sequel have first-class support on the
|
|
540
|
+
llm.rb runtime. In both cases an ActiveRecord or Sequel model
|
|
541
|
+
can be turned into a model that has the same capabilities as
|
|
542
|
+
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html),
|
|
543
|
+
or [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
|
|
544
|
+
|
|
545
|
+
The main difference is that the runtime persists directly into
|
|
546
|
+
the database with no requirements beyond a single column on a
|
|
547
|
+
single row. That means it is usually trivial to turn an existing
|
|
548
|
+
model into an AI-aware model.
|
|
549
|
+
|
|
550
|
+
#### ActiveRecord
|
|
551
|
+
|
|
552
|
+
The ActiveRecord interface for
|
|
553
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
|
|
554
|
+
is
|
|
555
|
+
[`acts_as_agent`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord/ActsAsAgent.html).
|
|
556
|
+
It yields an instance of
|
|
557
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
|
|
558
|
+
and that can be used
|
|
559
|
+
to configure the agent (eg which model, instructions, skills,
|
|
560
|
+
tools, etc).
|
|
561
|
+
|
|
562
|
+
An interesting option is the `format` option, by default it
|
|
563
|
+
defaults to `:string` but it can also be changed to `:json`
|
|
564
|
+
or `:jsonb` depending on the configuration and type of underlying
|
|
565
|
+
column. The JSONB column type is recommended.
|
|
566
|
+
|
|
236
567
|
```ruby
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
end
|
|
568
|
+
require "active_record"
|
|
569
|
+
require "llm"
|
|
570
|
+
require "llm/active_record"
|
|
241
571
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
572
|
+
class Agent < ApplicationRecord
|
|
573
|
+
acts_as_agent(format: :jsonb) do |agent|
|
|
574
|
+
agent.model "deepseek-v4-pro"
|
|
575
|
+
agent.instructions "solve the user's query"
|
|
576
|
+
agent.tools [Research, FinalizeResearch, ActOnResearch]
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
private
|
|
245
580
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
581
|
+
##
|
|
582
|
+
# By convention, this method defines the provider
|
|
583
|
+
# for a model. If neccessary, it can be renamed and
|
|
584
|
+
# configured via `provider: :your_method` instead.
|
|
585
|
+
def set_provider
|
|
586
|
+
LLM.deepseek(key: ENV["KEY"])
|
|
587
|
+
end
|
|
249
588
|
|
|
250
|
-
##
|
|
589
|
+
##
|
|
590
|
+
# By convention, this method should return what is
|
|
591
|
+
# given as the second argument to `LLM::Context` or
|
|
592
|
+
# `LLM::Agent`.
|
|
593
|
+
#
|
|
594
|
+
# Often, there is no need to set it, so it can be left
|
|
595
|
+
# undefined or it can be reassigned in the same way as
|
|
596
|
+
# `set_provider`. For example: `context: :your_method`
|
|
597
|
+
def set_context
|
|
598
|
+
{}
|
|
599
|
+
end
|
|
600
|
+
end
|
|
251
601
|
|
|
252
|
-
|
|
602
|
+
agent = Agent.create!
|
|
603
|
+
agent.talk "perform research"
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
#### Sequel
|
|
253
607
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
that speaks the Model Context Protocol.
|
|
608
|
+
The following is a Sequel equivalent to the ActiveRecord example,
|
|
609
|
+
but to keep it interesting and informative, this example also
|
|
610
|
+
configures a per-model tracer that logs to `$stdout`. Works the
|
|
611
|
+
same for ActiveRecord.
|
|
259
612
|
|
|
260
613
|
```ruby
|
|
614
|
+
require "sequel"
|
|
261
615
|
require "llm"
|
|
616
|
+
require "llm/sequel/plugin"
|
|
617
|
+
|
|
618
|
+
class Agent < Sequel::Model
|
|
619
|
+
plugin(:agent, format: :jsonb) do |agent|
|
|
620
|
+
agent.model "deepseek-v4-pro"
|
|
621
|
+
agent.instructions "solve the user's query"
|
|
622
|
+
agent.tools [Research, FinalizeResearch, ActOnResearch]
|
|
623
|
+
agent.tracer { LLM::Tracer::Logger.new(llm, io: $stdout) }
|
|
624
|
+
end
|
|
262
625
|
|
|
263
|
-
|
|
264
|
-
mcp = LLM::MCP.stdio(argv: ["ruby", "server.rb"])
|
|
626
|
+
private
|
|
265
627
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
628
|
+
def set_provider
|
|
629
|
+
LLM.deepseek(key: ENV["KEY"])
|
|
630
|
+
end
|
|
269
631
|
end
|
|
632
|
+
|
|
633
|
+
agent = Agent.create
|
|
634
|
+
agent.talk "perform research"
|
|
270
635
|
```
|
|
271
636
|
|
|
272
|
-
|
|
637
|
+
[Back to top](#table-of-contents)
|
|
638
|
+
|
|
639
|
+
## Schema
|
|
640
|
+
|
|
641
|
+
The [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
|
|
642
|
+
class can be subclassed to describe
|
|
643
|
+
the shape of a JSON object or objects that you expect
|
|
644
|
+
the model to respond with.
|
|
645
|
+
|
|
646
|
+
It can be useful for a wide range of use cases but the
|
|
647
|
+
most popular might be classification, data extraction,
|
|
648
|
+
and transferring structured data between different software
|
|
649
|
+
rather than blobs of text that a machine cannot easily parse
|
|
650
|
+
in a structured way.
|
|
651
|
+
|
|
652
|
+
#### Estimation
|
|
653
|
+
|
|
654
|
+
The following example asks the model to estimate the age
|
|
655
|
+
of a person in a photo. The model provides a structured response
|
|
656
|
+
that's represented by an instance of
|
|
657
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html).
|
|
273
658
|
|
|
274
|
-
|
|
275
|
-
|
|
659
|
+
The object returned by
|
|
660
|
+
[`LLM::Response#content!`](https://r.uby.dev/api-docs/llm.rb/LLM/Contract/Completion.html#content!-instance_method)
|
|
661
|
+
has methods that can access the age, confidence, and comments
|
|
662
|
+
properties.
|
|
663
|
+
This approach can also work for extracting data or an analysis
|
|
664
|
+
from a PDF, and other file types.
|
|
276
665
|
|
|
277
666
|
```ruby
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
667
|
+
require "llm"
|
|
668
|
+
require "pp"
|
|
669
|
+
|
|
670
|
+
class Estimation < LLM::Schema
|
|
671
|
+
property :age, Integer, "The estimated age of the person"
|
|
672
|
+
property :confidence, Number, "Your confidence in the estimate"
|
|
673
|
+
property :applicable, Boolean, "True when the photo contains a person"
|
|
674
|
+
property :comments, String, "Any additional comments or input"
|
|
675
|
+
required %i[age confidence applicable comments]
|
|
676
|
+
end
|
|
282
677
|
|
|
283
|
-
|
|
284
|
-
agent
|
|
678
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
679
|
+
agent = LLM::Agent.new(llm, schema: Estimation)
|
|
680
|
+
res = agent.ask "Given this photo, provide an age estimate", with: "photo.jpg"
|
|
681
|
+
|
|
682
|
+
##
|
|
683
|
+
# Coerces the model's response from a JSON string
|
|
684
|
+
# to an instance of LLM::Object.
|
|
685
|
+
estimate = res.content!
|
|
686
|
+
|
|
687
|
+
##
|
|
688
|
+
# Let's print the estimate
|
|
689
|
+
if estimate.applicable
|
|
690
|
+
print "The person is approx ", estimate.age.to_s, " years old", "\n"
|
|
691
|
+
print "I have a confidence rating of ", estimate.confidence.to_s, "\n"
|
|
692
|
+
else
|
|
693
|
+
print "This photo is not applicable:", "\n"
|
|
694
|
+
print estimate.comments
|
|
695
|
+
end
|
|
285
696
|
```
|
|
286
697
|
|
|
287
|
-
|
|
698
|
+
[Back to top](#table-of-contents)
|
|
699
|
+
|
|
700
|
+
## Cancellation
|
|
288
701
|
|
|
289
|
-
####
|
|
702
|
+
#### Cancel a request
|
|
290
703
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
704
|
+
A common scenario when communicating with a model is to
|
|
705
|
+
want to cancel the request mid-stream. This could be done
|
|
706
|
+
for a number of different reasons, most often because the
|
|
707
|
+
user made a mistake, or the model is making a mistake and
|
|
708
|
+
the user wants to cancel the action.
|
|
294
709
|
|
|
295
|
-
|
|
710
|
+
The runtime has built-in support for cancellation. So for
|
|
711
|
+
example it is possible to cancel a request on the main
|
|
712
|
+
thread from a secondary thread. A number of things happen
|
|
713
|
+
when a request is cancelled. First the request is cancelled
|
|
714
|
+
at the transport level, and each transport handles it a little
|
|
715
|
+
differently. The net effect in every case is that the connection
|
|
716
|
+
is closed.
|
|
296
717
|
|
|
297
|
-
|
|
718
|
+
The runtime then notifies the rest of the system. so for example,
|
|
719
|
+
if a tool was running, it will receive the `on_interrupt` / `on_cancel`
|
|
720
|
+
callback that lets the tool do any necessary cleanup, or execute its own
|
|
721
|
+
cancellation plan. Tools that were pending (not yet run but requetsed to
|
|
722
|
+
run) are cancelled through
|
|
723
|
+
[`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel-instance_method).
|
|
298
724
|
|
|
299
725
|
```ruby
|
|
300
726
|
require "llm"
|
|
301
727
|
|
|
302
|
-
llm = LLM.
|
|
728
|
+
llm = LLM.deepseek(key: ENV["DEEPSEEK_SECRET"])
|
|
303
729
|
agent = LLM::Agent.new(llm)
|
|
304
|
-
|
|
730
|
+
queue = Queue.new
|
|
305
731
|
|
|
306
|
-
|
|
307
|
-
|
|
732
|
+
Thread.new do
|
|
733
|
+
queue.push(nil)
|
|
734
|
+
sleep(2)
|
|
735
|
+
agent.cancel!
|
|
736
|
+
end
|
|
308
737
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
738
|
+
begin
|
|
739
|
+
queue.pop
|
|
740
|
+
agent.talk "write me a very long poem", stream: $stdout
|
|
741
|
+
rescue LLM::Interrupt
|
|
742
|
+
puts "request cancelled!"
|
|
743
|
+
end
|
|
313
744
|
```
|
|
314
745
|
|
|
315
|
-
|
|
746
|
+
[Back to top](#table-of-contents)
|
|
747
|
+
|
|
748
|
+
## Tracer
|
|
316
749
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
750
|
+
The runtime can be observed by subclasses of
|
|
751
|
+
[`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html). <br>
|
|
752
|
+
The default tracers include a tracer that can write to standard
|
|
753
|
+
output
|
|
754
|
+
([`LLM::Tracer::Logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Logger.html)),
|
|
755
|
+
and a generic OpenTelemetry tracer that can export spans via OTLP
|
|
756
|
+
([`LLM::Tracer::Telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Telemetry.html)).
|
|
757
|
+
|
|
758
|
+
llm.rb has numerous hooks implemented throughout the runtime that
|
|
759
|
+
[`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
|
|
760
|
+
subclasses can hook into, and the tracer is
|
|
761
|
+
purposefully designed to be extensible. The scope of a trace
|
|
762
|
+
can vary from an individual agent (an instance of
|
|
763
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)),
|
|
764
|
+
or for every request a provider makes (an indirect instance of
|
|
765
|
+
[`LLM::Provider`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html)).
|
|
766
|
+
|
|
767
|
+
#### Provider-wide tracer
|
|
768
|
+
|
|
769
|
+
The following two examples demonstrate provider-wide tracers that
|
|
770
|
+
cover every request made for a single provider.
|
|
322
771
|
|
|
323
772
|
```ruby
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
773
|
+
##
|
|
774
|
+
# Provider-wide tracer
|
|
775
|
+
# Writes to $stdout
|
|
776
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
777
|
+
llm.tracer = LLM::Tracer::Logger.new(llm, io: $stdout)
|
|
327
778
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
779
|
+
##
|
|
780
|
+
# Provider-wide tracer
|
|
781
|
+
# Writes to deepseek.log
|
|
782
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
783
|
+
llm.tracer = LLM::Tracer::Logger.new(llm, path: "deepseek.log")
|
|
784
|
+
```
|
|
334
785
|
|
|
335
|
-
|
|
786
|
+
#### Agent-local tracer
|
|
336
787
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
end
|
|
788
|
+
The next two examples demonstrate a tracer that is local
|
|
789
|
+
to an agent.
|
|
340
790
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
791
|
+
```ruby
|
|
792
|
+
##
|
|
793
|
+
# Agent-local
|
|
794
|
+
# Writes to $stdout
|
|
795
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
796
|
+
agent = LLM::Agent.new(llm, tracer: LLM::Tracer::Logger.new(llm, io: $stdout))
|
|
797
|
+
|
|
798
|
+
##
|
|
799
|
+
# Agent-local
|
|
800
|
+
# Writes to deepseek-agent.log
|
|
801
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
802
|
+
agent = LLM::Agent.new(llm, tracer: LLM::Tracer::Logger.new(llm, path: "deepseek-agent.log"))
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
[Back to top](#table-of-contents)
|
|
806
|
+
|
|
807
|
+
## REPL
|
|
808
|
+
|
|
809
|
+
During the development and operation of agents it can often
|
|
810
|
+
be helpful to drop into a read-eval-print loop. This gives
|
|
811
|
+
you a simple way to confirm the work was successful, inspect
|
|
812
|
+
anything that went wrong (for example, tool call errors), and
|
|
813
|
+
keep talking to the same agent so you can learn about its
|
|
814
|
+
decision-making process and make improvements for future runs.
|
|
815
|
+
It can also be used to ask the agent to correct any errors
|
|
816
|
+
that might have made.
|
|
817
|
+
|
|
818
|
+
##### LLM::Agent
|
|
345
819
|
|
|
346
|
-
|
|
347
|
-
|
|
820
|
+
The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
|
|
821
|
+
method requires the [curses](https://github.com/ruby/curses)
|
|
822
|
+
gem to be installed and available for require. It is an
|
|
823
|
+
optional dependency that only becomes required when you
|
|
824
|
+
call the [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
|
|
825
|
+
method.
|
|
826
|
+
|
|
827
|
+
```ruby
|
|
828
|
+
require "llm"
|
|
829
|
+
|
|
830
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
831
|
+
agent = LLM::Agent.new(llm)
|
|
832
|
+
agent.repl
|
|
348
833
|
```
|
|
349
834
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) with the
|
|
354
|
-
same persistence contract.
|
|
835
|
+
[Back to top](#table-of-contents)
|
|
836
|
+
|
|
837
|
+
## Images
|
|
355
838
|
|
|
356
|
-
|
|
839
|
+
The OpenAI, Google, xAI, DeepInfra, and DeepSeek providers have
|
|
840
|
+
builtin image generation capabilities. OpenAI, xAI, and DeepInfra
|
|
841
|
+
also support image edits. Google only supports image generation.
|
|
842
|
+
DeepSeek supports generation and edits too, but only through SVG
|
|
843
|
+
output rather than raster image models.
|
|
357
844
|
|
|
358
|
-
####
|
|
845
|
+
#### Generation
|
|
359
846
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
847
|
+
The [`LLM::Provider#images`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#images-instance_method)
|
|
848
|
+
method returns an Image
|
|
849
|
+
object that a subset of providers implement. At the
|
|
850
|
+
moment Google, xAI, OpenAI, DeepInfra, and DeepSeek have image
|
|
851
|
+
generation capabilities. DeepSeek is the odd one out: it generates
|
|
852
|
+
SVG documents rather than raster images.
|
|
364
853
|
|
|
365
854
|
```ruby
|
|
855
|
+
require "llm"
|
|
856
|
+
|
|
857
|
+
##
|
|
858
|
+
# Store dogrocket.png
|
|
366
859
|
llm = LLM.openai(key: ENV["KEY"])
|
|
367
|
-
res = llm.
|
|
368
|
-
|
|
369
|
-
puts res.embeddings.first.size
|
|
860
|
+
res = llm.images.create(prompt: "a dog on a rocket to the moon")
|
|
861
|
+
IO.copy_stream res.images[0], "dogrocket.png"
|
|
370
862
|
```
|
|
371
863
|
|
|
372
|
-
|
|
864
|
+
The API is the same across providers. <br>
|
|
865
|
+
For example – xAI:
|
|
373
866
|
|
|
374
867
|
```ruby
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
res = llm.
|
|
382
|
-
res.
|
|
868
|
+
require "llm"
|
|
869
|
+
|
|
870
|
+
##
|
|
871
|
+
# Store dogrocket.png
|
|
872
|
+
# Same API as OpenAI
|
|
873
|
+
llm = LLM.xai(key: ENV["KEY"])
|
|
874
|
+
res = llm.images.create(prompt: "a dog on a rocket to the moon")
|
|
875
|
+
IO.copy_stream res.images[0], "dogrocket.png"
|
|
383
876
|
```
|
|
384
877
|
|
|
385
|
-
|
|
878
|
+
#### Edits
|
|
386
879
|
|
|
387
|
-
|
|
880
|
+
OpenAI, xAI, and DeepInfra have the same interface for image edits. <br>
|
|
881
|
+
DeepSeek also supports edits, but only for SVG files. <br>
|
|
882
|
+
Google does not have edit image support. <br>
|
|
388
883
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
884
|
+
```ruby
|
|
885
|
+
require "llm"
|
|
886
|
+
|
|
887
|
+
##
|
|
888
|
+
# Edit self.jpg and add a mustache
|
|
889
|
+
# Save to mustache.png
|
|
890
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
891
|
+
res = llm.images.edit(prompt: "add a mustache", image: "self.jpg")
|
|
892
|
+
IO.copy_stream res.images[0], "mustache.png"
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
#### DeepSeek
|
|
896
|
+
|
|
897
|
+
The DeepSeek provider does not provide an image generation model
|
|
898
|
+
but it is possible to ask a text-to-text model to produce
|
|
899
|
+
vector graphics (SVGs), and in that limited sense, it can become
|
|
900
|
+
a capable text-to-image model.
|
|
393
901
|
|
|
394
902
|
```ruby
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
903
|
+
require "llm"
|
|
904
|
+
|
|
905
|
+
##
|
|
906
|
+
# Edit rocket.svg and change its color
|
|
907
|
+
# Save to rocket-edited.svg
|
|
908
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
909
|
+
res = llm.images.edit(prompt: "make the rocket red", image: "rocket.svg")
|
|
910
|
+
IO.copy_stream res.images[0], "rocket-edited.svg"
|
|
400
911
|
```
|
|
401
912
|
|
|
402
|
-
|
|
403
|
-
|
|
913
|
+
An interesting property of the DeepSeek implementation is that
|
|
914
|
+
it can maintain a session that can perform multiple image generations
|
|
915
|
+
or edits rather than just one-shot generations.
|
|
916
|
+
|
|
917
|
+
It's possible because under the hood
|
|
918
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
|
|
919
|
+
is attached to the
|
|
920
|
+
[`LLM::Response`](https://r.uby.dev/api-docs/llm.rb/LLM/Response.html)
|
|
921
|
+
object that is returned to the caller. So the response includes an
|
|
922
|
+
`agent` method, and it can be carried across multiple generations.
|
|
923
|
+
It is specific to this endpoint though. It works like this:
|
|
404
924
|
|
|
405
925
|
```ruby
|
|
406
|
-
|
|
407
|
-
|
|
926
|
+
require "llm"
|
|
927
|
+
|
|
928
|
+
llm = LLM.deepseek(key: ENV["DEEPSEEK_SECRET"])
|
|
929
|
+
agent = nil
|
|
930
|
+
loop do
|
|
931
|
+
print "> "
|
|
932
|
+
prompt = $stdin.gets
|
|
933
|
+
res = llm.images.create(prompt:, agent:)
|
|
934
|
+
agent = res.agent
|
|
935
|
+
IO.copy_stream res.images[0], "image.svg"
|
|
936
|
+
print "ok: saved image.svg", "\n"
|
|
937
|
+
end
|
|
408
938
|
```
|
|
409
939
|
|
|
410
|
-
|
|
940
|
+
[Back to top](#table-of-contents)
|
|
411
941
|
|
|
412
|
-
|
|
942
|
+
## Audio
|
|
413
943
|
|
|
414
|
-
|
|
944
|
+
The audio interface defined by llm.rb describes three methods,
|
|
945
|
+
although not every provider implements all of them. Generally
|
|
946
|
+
speaking the audio interface is for text-to-speech, and
|
|
947
|
+
speech-to-text models.
|
|
948
|
+
|
|
949
|
+
The following providers have audio support:
|
|
950
|
+
|
|
951
|
+
* OpenAI - full support
|
|
952
|
+
* Google - partial support
|
|
953
|
+
* DeepInfra - partial support
|
|
954
|
+
|
|
955
|
+
#### text-to-speech
|
|
956
|
+
|
|
957
|
+
The `create_speech` method generates an audio clip based
|
|
958
|
+
on the given input. This method returns a
|
|
959
|
+
[`LLM::URIData`](https://r.uby.dev/api-docs/llm.rb/LLM/URIData.html)
|
|
960
|
+
object. OpenAI, and DeepInfra support this method.
|
|
415
961
|
|
|
416
962
|
```ruby
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
963
|
+
require "llm"
|
|
964
|
+
|
|
965
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
966
|
+
res = llm.audio.create_speech(input: "Hello world")
|
|
967
|
+
IO.copy_stream res.audio.decoded, "helloworld.mp3"
|
|
420
968
|
```
|
|
421
969
|
|
|
422
|
-
|
|
970
|
+
#### speech-to-text
|
|
423
971
|
|
|
424
|
-
|
|
972
|
+
The `create_transcription` method transcribes a given
|
|
973
|
+
audio clip as text. OpenAI, Google and DeepInfra support
|
|
974
|
+
this method.
|
|
975
|
+
|
|
976
|
+
```ruby
|
|
977
|
+
require "llm"
|
|
978
|
+
|
|
979
|
+
llm = LLM.google(key: ENV["KEY"])
|
|
980
|
+
res = llm.audio.create_transcription(file: "helloworld.mp3")
|
|
981
|
+
res.text # => "Hello world"
|
|
982
|
+
```
|
|
425
983
|
|
|
426
|
-
|
|
427
|
-
|
|
984
|
+
#### translation
|
|
985
|
+
|
|
986
|
+
The `create_translation` method translates a given audio
|
|
987
|
+
clip, then transcribes it as text. OpenAI, and Google
|
|
988
|
+
support this method.
|
|
989
|
+
|
|
990
|
+
```ruby
|
|
991
|
+
require "llm"
|
|
992
|
+
|
|
993
|
+
llm = LLM.google(key: ENV["KEY"])
|
|
994
|
+
res = llm.audio.create_translation(file: "bomdia.mp3")
|
|
995
|
+
res.text # => "Good day"
|
|
996
|
+
```
|
|
428
997
|
|
|
429
|
-
|
|
430
|
-
|---|---|---|
|
|
431
|
-
| [matz](https://r.uby.dev/matz/) | `ssh matz@r.uby.dev` | [mruby-llm](https://r.uby.dev/mruby-llm/) |
|
|
432
|
-
| [robert](https://4.4bsd.dev/robert) | `ssh robert@4.4bsd.dev` | [mruby-llm](https://r.uby.dev/mruby-llm/) |
|
|
998
|
+
[Back to top](#table-of-contents)
|