llm.rb 4.20.1 → 4.21.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 +47 -0
- data/README.md +175 -27
- data/lib/llm/agent.rb +24 -8
- data/lib/llm/context.rb +13 -3
- data/lib/llm/providers/google/response_adapter/completion.rb +6 -0
- data/lib/llm/sequel/agent.rb +107 -0
- data/lib/llm/skill.rb +116 -0
- data/lib/llm/stream/queue.rb +36 -6
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +1 -0
- data/lib/sequel/plugins/agent.rb +8 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f0bca66b2bd8873cf39abb3be19dc99ca20d558e40ef3e9f475bf1f33faef6b6
|
|
4
|
+
data.tar.gz: c73a2c5093e7e09557242919feb5a377f25b0fa8a11249a9f346673ad7d3a921
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a00191aaab47702a794f9fa86d782f21832be2a7ef309bd558aa482100d7c66ddbdf3320e89c80af2942c6e33295f10d387702130162fbac7cc98fd9b24c9a8
|
|
7
|
+
data.tar.gz: a6709f6fd265af673da771f635f34c68e28e490405700c1a59b18253391dbbcae09ce677a4251994d898a851ec08dc598c5ff858e516e25b1206948f509abf67
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
Changes since `v4.21.0`.
|
|
6
|
+
|
|
7
|
+
## v4.21.0
|
|
8
|
+
|
|
9
|
+
Changes since `v4.20.2`.
|
|
10
|
+
|
|
11
|
+
This release expands higher-level composition in llm.rb. It adds Sequel agent
|
|
12
|
+
persistence through `plugin :agent` and introduces directory-backed skills
|
|
13
|
+
that load from `SKILL.md`, resolve named tools, and plug directly into
|
|
14
|
+
`LLM::Context` and `LLM::Agent`.
|
|
15
|
+
|
|
16
|
+
### Change
|
|
17
|
+
|
|
18
|
+
* **Add `plugin :agent` for Sequel models** <br>
|
|
19
|
+
Add Sequel support for `plugin :agent`, similar to ActiveRecord's
|
|
20
|
+
`acts_as_agent`, so models can wrap `LLM::Agent` with built-in
|
|
21
|
+
persistence.
|
|
22
|
+
|
|
23
|
+
* **Load directory-backed skills through `LLM::Context` and `LLM::Agent`** <br>
|
|
24
|
+
Add `skills:` to `LLM::Context` and `skills ...` to `LLM::Agent` so
|
|
25
|
+
directories with `SKILL.md` can be loaded, resolved into tools, and run
|
|
26
|
+
through the normal llm.rb tool path.
|
|
27
|
+
|
|
28
|
+
## v4.20.2
|
|
29
|
+
|
|
5
30
|
Changes since `v4.20.1`.
|
|
6
31
|
|
|
32
|
+
This patch release improves runtime behavior around interruption and mixed
|
|
33
|
+
concurrency waits. It also rounds out response API uniformity for Google
|
|
34
|
+
completion responses.
|
|
35
|
+
|
|
36
|
+
### Fix
|
|
37
|
+
|
|
38
|
+
* **Expose Google completion response IDs through `.id`** <br>
|
|
39
|
+
Add `LLM::Response#id` support to Google completion responses so tracer
|
|
40
|
+
and caller code can rely on the same API used by other providers.
|
|
41
|
+
|
|
42
|
+
* **Track interrupt ownership on the active request** <br>
|
|
43
|
+
Bind `LLM::Context` interruption to the fiber running `talk` or `respond`
|
|
44
|
+
so `interrupt!` works correctly when requests are started outside the
|
|
45
|
+
context's initialization fiber.
|
|
46
|
+
|
|
47
|
+
### Change
|
|
48
|
+
|
|
49
|
+
* **Allow mixed concurrency strategies in `wait(...)`** <br>
|
|
50
|
+
Let `LLM::Context#wait`, `LLM::Stream#wait`, and `LLM::Agent.concurrency`
|
|
51
|
+
accept arrays such as `[:thread, :ractor]` so mixed tool sets can wait on
|
|
52
|
+
more than one concurrency strategy.
|
|
53
|
+
|
|
7
54
|
## v4.20.1
|
|
8
55
|
|
|
9
56
|
Changes since `v4.20.0`.
|
data/README.md
CHANGED
|
@@ -4,26 +4,33 @@
|
|
|
4
4
|
<p align="center">
|
|
5
5
|
<a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
|
|
6
6
|
<a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
|
|
7
|
-
<a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.
|
|
7
|
+
<a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.21.0-green.svg?" alt="Version"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
## About
|
|
11
11
|
|
|
12
12
|
llm.rb is a lightweight runtime for building capable AI systems in Ruby.
|
|
13
|
+
<br>
|
|
14
|
+
|
|
15
|
+
It is also the most capable AI Ruby runtime that exists _today_, and that claim is
|
|
16
|
+
backed up by research. Maybe it won't always be true, and that would be good news too -
|
|
17
|
+
because it would mean the Ruby ecosystem is getting stronger.
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
contexts, agents, tools, MCP servers, streaming, schemas, files, and
|
|
16
|
-
state, so real systems can be built out of one coherent execution
|
|
17
|
-
of a pile of adapters.
|
|
19
|
+
llm.rb is not just an API wrapper: it gives you one runtime for providers,
|
|
20
|
+
contexts, agents, tools, skills, MCP servers, streaming, schemas, files, and
|
|
21
|
+
persisted state, so real systems can be built out of one coherent execution
|
|
22
|
+
model instead of a pile of adapters.
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
llm.rb is designed for Ruby, and although it works great in Rails, it is not tightly
|
|
25
|
+
coupled to it. It runs on the standard library by default (zero dependencies),
|
|
26
|
+
loads optional pieces only when needed, includes built-in ActiveRecord support through
|
|
21
27
|
`acts_as_llm` and `acts_as_agent`, includes built-in Sequel support through
|
|
22
|
-
`plugin :llm`, and is designed for engineers who want control over
|
|
28
|
+
`plugin :llm` and `plugin :agent`, and is designed for engineers who want control over
|
|
23
29
|
long-lived, tool-capable, stateful AI workflows instead of just
|
|
24
30
|
request/response helpers.
|
|
25
31
|
|
|
26
|
-
Want to see some code? Jump to [the examples](#examples) section.
|
|
32
|
+
Want to see some code? Jump to [the examples](#examples) section. <br>
|
|
33
|
+
Want a taste of what llm.rb can build? See [the screencast](#screencast).
|
|
27
34
|
|
|
28
35
|
## Architecture
|
|
29
36
|
|
|
@@ -100,13 +107,18 @@ same context object.
|
|
|
100
107
|
integration stack.
|
|
101
108
|
- **ActiveRecord and Sequel persistence are built in** <br>
|
|
102
109
|
llm.rb includes built-in ActiveRecord support through `acts_as_llm` and
|
|
103
|
-
`acts_as_agent`, plus built-in Sequel support through `plugin :llm
|
|
110
|
+
`acts_as_agent`, plus built-in Sequel support through `plugin :llm` and
|
|
111
|
+
`plugin :agent`.
|
|
104
112
|
Use `acts_as_llm` when you want to wrap `LLM::Context`, `acts_as_agent`
|
|
105
|
-
when you want to wrap `LLM::Agent`,
|
|
106
|
-
|
|
107
|
-
integrations support `provider:` and `context:` hooks,
|
|
108
|
-
:string` for text columns or `format: :jsonb` for native
|
|
109
|
-
storage when ORM JSON typecasting support is enabled.
|
|
113
|
+
when you want to wrap `LLM::Agent`, `plugin :llm` when you want a
|
|
114
|
+
`LLM::Context` on a Sequel model, or `plugin :agent` when you want an
|
|
115
|
+
`LLM::Agent`. These integrations support `provider:` and `context:` hooks,
|
|
116
|
+
plus `format: :string` for text columns or `format: :jsonb` for native
|
|
117
|
+
PostgreSQL JSON storage when ORM JSON typecasting support is enabled.
|
|
118
|
+
- **ORM models can become persistent agents** <br>
|
|
119
|
+
Turn an ActiveRecord or Sequel model into an agent-capable model with
|
|
120
|
+
built-in persistence, stored on the same table, with `jsonb` support when
|
|
121
|
+
your ORM and database support native JSON columns.
|
|
110
122
|
- **Persistent HTTP pooling is shared process-wide** <br>
|
|
111
123
|
When enabled, separate
|
|
112
124
|
[`LLM::Provider`](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html)
|
|
@@ -125,6 +137,11 @@ same context object.
|
|
|
125
137
|
- **Tools are explicit** <br>
|
|
126
138
|
Run local tools, provider-native tools, and MCP tools through the same path
|
|
127
139
|
with fewer special cases.
|
|
140
|
+
- **Skills are just tools loaded from directories** <br>
|
|
141
|
+
Point llm.rb at directories with a `SKILL.md`, resolve named tools through
|
|
142
|
+
the registry, and run those skills through `LLM::Context` or `LLM::Agent`
|
|
143
|
+
without creating a second execution model. If you are familiar with skills
|
|
144
|
+
in Claude or Codex, llm.rb supports the same general idea.
|
|
128
145
|
- **Providers are normalized, not flattened** <br>
|
|
129
146
|
Share one API surface across providers without losing access to provider-
|
|
130
147
|
specific capabilities where they matter.
|
|
@@ -164,6 +181,7 @@ same context object.
|
|
|
164
181
|
- **Run Tools While Streaming** — overlap model output with tool latency
|
|
165
182
|
- **Concurrent Execution** — threads, async tasks, and fibers
|
|
166
183
|
- **Agents** — reusable assistants with tool auto-execution
|
|
184
|
+
- **Skills** — directory-backed capabilities loaded from `SKILL.md`
|
|
167
185
|
- **Structured Outputs** — JSON Schema-based responses
|
|
168
186
|
- **Responses API** — stateful response workflows where providers support them
|
|
169
187
|
- **MCP Support** — stdio and HTTP MCP clients with prompt and tool support
|
|
@@ -186,9 +204,9 @@ gem install llm.rb
|
|
|
186
204
|
|
|
187
205
|
## Examples
|
|
188
206
|
|
|
189
|
-
|
|
207
|
+
#### REPL
|
|
190
208
|
|
|
191
|
-
This example uses [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) directly for an interactive REPL. <br> See the [deepdive](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) for more examples.
|
|
209
|
+
This example uses [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) directly for an interactive REPL. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
192
210
|
|
|
193
211
|
```ruby
|
|
194
212
|
require "llm"
|
|
@@ -203,9 +221,91 @@ loop do
|
|
|
203
221
|
end
|
|
204
222
|
```
|
|
205
223
|
|
|
206
|
-
|
|
224
|
+
#### Streaming
|
|
225
|
+
|
|
226
|
+
This example uses [`LLM::Stream`](https://0x1eef.github.io/x/llm.rb/LLM/Stream.html) directly so visible output and tool execution can happen together. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
require "llm"
|
|
230
|
+
|
|
231
|
+
class Stream < LLM::Stream
|
|
232
|
+
def on_content(content)
|
|
233
|
+
$stdout << content
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def on_tool_call(tool, error)
|
|
237
|
+
return queue << error if error
|
|
238
|
+
$stdout << "\nRunning tool #{tool.name}...\n"
|
|
239
|
+
queue << tool.spawn(:thread)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def on_tool_return(tool, result)
|
|
243
|
+
if result.error?
|
|
244
|
+
$stdout << "Tool #{tool.name} failed\n"
|
|
245
|
+
else
|
|
246
|
+
$stdout << "Finished tool #{tool.name}\n"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
252
|
+
ctx = LLM::Context.new(llm, stream: Stream.new, tools: [System])
|
|
253
|
+
|
|
254
|
+
ctx.talk("Run `date` and `uname -a`.")
|
|
255
|
+
ctx.talk(ctx.wait(:thread)) while ctx.functions.any?
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### Reasoning
|
|
259
|
+
|
|
260
|
+
This example uses [`LLM::Stream`](https://0x1eef.github.io/x/llm.rb/LLM/Stream.html) with the OpenAI Responses API so reasoning output is streamed separately from visible assistant output. See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
require "llm"
|
|
264
|
+
|
|
265
|
+
class Stream < LLM::Stream
|
|
266
|
+
def on_content(content)
|
|
267
|
+
$stdout << content
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def on_reasoning_content(content)
|
|
271
|
+
$stderr << content
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
276
|
+
ctx = LLM::Context.new(
|
|
277
|
+
llm,
|
|
278
|
+
model: "gpt-5.4-mini",
|
|
279
|
+
mode: :responses,
|
|
280
|
+
reasoning: {effort: "medium"},
|
|
281
|
+
stream: Stream.new
|
|
282
|
+
)
|
|
283
|
+
ctx.talk("Solve 17 * 19 and show your work.")
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### Request Cancellation
|
|
207
287
|
|
|
208
|
-
|
|
288
|
+
Need to cancel a stream? llm.rb has you covered through [`LLM::Context#interrupt!`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#interrupt-21-instance_method). <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
require "llm"
|
|
292
|
+
require "io/console"
|
|
293
|
+
|
|
294
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
295
|
+
ctx = LLM::Context.new(llm, stream: $stdout)
|
|
296
|
+
|
|
297
|
+
worker = Thread.new do
|
|
298
|
+
ctx.talk("Write a very long essay about network protocols.")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
STDIN.getch
|
|
302
|
+
ctx.interrupt!
|
|
303
|
+
worker.join
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Sequel (ORM)
|
|
307
|
+
|
|
308
|
+
The `plugin :llm` integration wraps [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) on a `Sequel::Model` and keeps tool execution explicit. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
209
309
|
|
|
210
310
|
```ruby
|
|
211
311
|
require "llm"
|
|
@@ -222,10 +322,10 @@ ctx.talk("Remember that my favorite language is Ruby")
|
|
|
222
322
|
puts ctx.talk("What is my favorite language?").content
|
|
223
323
|
```
|
|
224
324
|
|
|
225
|
-
|
|
325
|
+
#### ActiveRecord (ORM): acts_as_llm
|
|
226
326
|
|
|
227
327
|
The `acts_as_llm` method wraps [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) and
|
|
228
|
-
provides full control over tool execution. <br> See the [deepdive](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) for more examples.
|
|
328
|
+
provides full control over tool execution. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
229
329
|
|
|
230
330
|
```ruby
|
|
231
331
|
require "llm"
|
|
@@ -242,10 +342,10 @@ ctx.talk("Remember that my favorite language is Ruby")
|
|
|
242
342
|
puts ctx.talk("What is my favorite language?").content
|
|
243
343
|
```
|
|
244
344
|
|
|
245
|
-
|
|
345
|
+
#### ActiveRecord (ORM): acts_as_agent
|
|
246
346
|
|
|
247
347
|
The `acts_as_agent` method wraps [`LLM::Agent`](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html) and
|
|
248
|
-
manages tool execution for you. <br> See the [deepdive](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) for more examples.
|
|
348
|
+
manages tool execution for you. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
249
349
|
|
|
250
350
|
```ruby
|
|
251
351
|
require "llm"
|
|
@@ -272,9 +372,9 @@ ticket = Ticket.create!(provider: "openai", model: "gpt-5.4-mini")
|
|
|
272
372
|
puts ticket.talk("How do I rotate my API key?").content
|
|
273
373
|
```
|
|
274
374
|
|
|
275
|
-
|
|
375
|
+
#### Agent
|
|
276
376
|
|
|
277
|
-
This example uses [`LLM::Agent`](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html) directly and lets the agent manage tool execution. <br> See the [deepdive](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) for more examples.
|
|
377
|
+
This example uses [`LLM::Agent`](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html) directly and lets the agent manage tool execution. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
278
378
|
|
|
279
379
|
```ruby
|
|
280
380
|
require "llm"
|
|
@@ -291,10 +391,58 @@ agent = ShellAgent.new(llm)
|
|
|
291
391
|
puts agent.talk("What time is it on this system?").content
|
|
292
392
|
```
|
|
293
393
|
|
|
394
|
+
#### Skills
|
|
395
|
+
|
|
396
|
+
This example uses [`LLM::Agent`](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html) with directory-backed skills so `SKILL.md` capabilities run through the normal tool path. If you have used skills in Claude or Codex, this is the same kind of building block. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
require "llm"
|
|
400
|
+
|
|
401
|
+
class Agent < LLM::Agent
|
|
402
|
+
model "gpt-5.4-mini"
|
|
403
|
+
instructions "You are a concise release assistant."
|
|
404
|
+
skills "./skills/release", "./skills/review"
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
408
|
+
puts Agent.new(llm).talk("Use the review skill.").content
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
#### MCP
|
|
412
|
+
|
|
413
|
+
This example uses [`LLM::MCP`](https://0x1eef.github.io/x/llm.rb/LLM/MCP.html) over HTTP so remote GitHub MCP tools run through the same `LLM::Context` tool path as local tools. See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
require "llm"
|
|
417
|
+
require "net/http/persistent"
|
|
418
|
+
|
|
419
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
420
|
+
mcp = LLM::MCP.http(
|
|
421
|
+
url: "https://api.githubcopilot.com/mcp/",
|
|
422
|
+
headers: {"Authorization" => "Bearer #{ENV.fetch("GITHUB_PAT")}"}
|
|
423
|
+
).persistent
|
|
424
|
+
|
|
425
|
+
begin
|
|
426
|
+
mcp.start
|
|
427
|
+
ctx = LLM::Context.new(llm, stream: $stdout, tools: mcp.tools)
|
|
428
|
+
ctx.talk("Pull information about my GitHub account.")
|
|
429
|
+
ctx.talk(ctx.call(:functions)) while ctx.functions.any?
|
|
430
|
+
ensure
|
|
431
|
+
mcp.stop
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## Screencast
|
|
436
|
+
|
|
437
|
+
This screencast was built on an older version of llm.rb, but it still shows
|
|
438
|
+
how capable the runtime can be in a real application:
|
|
439
|
+
|
|
440
|
+
[](https://www.youtube.com/watch?v=x1K4wMeO_QA)
|
|
441
|
+
|
|
294
442
|
## Resources
|
|
295
443
|
|
|
296
|
-
- [deepdive](https://0x1eef.github.io/x/llm.rb/file.deepdive.html)
|
|
297
|
-
examples guide.
|
|
444
|
+
- [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) and
|
|
445
|
+
[deepdive (markdown)](resources/deepdive.md) are the examples guide.
|
|
298
446
|
- [relay](https://github.com/llmrb/relay) shows a real application built on
|
|
299
447
|
top of llm.rb.
|
|
300
448
|
- [doc site](https://0x1eef.github.io/x/llm.rb?rebuild=1) has the API docs.
|
data/lib/llm/agent.rb
CHANGED
|
@@ -17,7 +17,8 @@ module LLM
|
|
|
17
17
|
# * Instructions are injected only on the first request.
|
|
18
18
|
# * An agent automatically executes tool loops (unlike {LLM::Context LLM::Context}).
|
|
19
19
|
# * Tool loop execution can be configured with `concurrency :call`,
|
|
20
|
-
# `:thread`, `:task`, `:fiber`, or
|
|
20
|
+
# `:thread`, `:task`, `:fiber`, `:ractor`, or a list of queued task
|
|
21
|
+
# types such as `[:thread, :ractor]`.
|
|
21
22
|
#
|
|
22
23
|
# @example
|
|
23
24
|
# class SystemAdmin < LLM::Agent
|
|
@@ -58,6 +59,17 @@ module LLM
|
|
|
58
59
|
@tools = tools.flatten
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
##
|
|
63
|
+
# Set or get the default skills
|
|
64
|
+
# @param [Array<String>, nil] skills
|
|
65
|
+
# One or more skill directories
|
|
66
|
+
# @return [Array<String>, nil]
|
|
67
|
+
# Returns the current skills when no argument is provided
|
|
68
|
+
def self.skills(*skills)
|
|
69
|
+
return @skills if skills.empty?
|
|
70
|
+
@skills = skills.flatten
|
|
71
|
+
end
|
|
72
|
+
|
|
61
73
|
##
|
|
62
74
|
# Set or get the default schema
|
|
63
75
|
# @param [#to_json, nil] schema
|
|
@@ -83,7 +95,7 @@ module LLM
|
|
|
83
95
|
##
|
|
84
96
|
# Set or get the tool execution concurrency.
|
|
85
97
|
#
|
|
86
|
-
# @param [Symbol, nil] concurrency
|
|
98
|
+
# @param [Symbol, Array<Symbol>, nil] concurrency
|
|
87
99
|
# Controls how pending tool loops are executed:
|
|
88
100
|
# - `:call`: sequential calls
|
|
89
101
|
# - `:thread`: concurrent threads
|
|
@@ -91,7 +103,10 @@ module LLM
|
|
|
91
103
|
# - `:fiber`: concurrent raw fibers
|
|
92
104
|
# - `:ractor`: concurrent Ruby ractors for class-based tools; MCP tools are not supported,
|
|
93
105
|
# and this mode is especially useful for CPU-bound tool work
|
|
94
|
-
#
|
|
106
|
+
# - `[:thread, :ractor]`: the possible concurrency strategies to wait on, in the
|
|
107
|
+
# given order. This is useful for mixed tool sets or when work may have been
|
|
108
|
+
# spawned with more than one concurrency strategy.
|
|
109
|
+
# @return [Symbol, Array<Symbol>, nil]
|
|
95
110
|
def self.concurrency(concurrency = nil)
|
|
96
111
|
return @concurrency if concurrency.nil?
|
|
97
112
|
@concurrency = concurrency
|
|
@@ -106,10 +121,11 @@ module LLM
|
|
|
106
121
|
# not only those listed here.
|
|
107
122
|
# @option params [String] :model Defaults to the provider's default model
|
|
108
123
|
# @option params [Array<LLM::Function>, nil] :tools Defaults to nil
|
|
124
|
+
# @option params [Array<String>, nil] :skills Defaults to nil
|
|
109
125
|
# @option params [#to_json, nil] :schema Defaults to nil
|
|
110
|
-
# @option params [Symbol, nil] :concurrency Defaults to the agent class concurrency
|
|
126
|
+
# @option params [Symbol, Array<Symbol>, nil] :concurrency Defaults to the agent class concurrency
|
|
111
127
|
def initialize(llm, params = {})
|
|
112
|
-
defaults = {model: self.class.model, tools: self.class.tools, schema: self.class.schema}.compact
|
|
128
|
+
defaults = {model: self.class.model, tools: self.class.tools, skills: self.class.skills, schema: self.class.schema}.compact
|
|
113
129
|
@concurrency = params.delete(:concurrency) || self.class.concurrency
|
|
114
130
|
@llm = llm
|
|
115
131
|
@ctx = LLM::Context.new(llm, defaults.merge(params))
|
|
@@ -270,7 +286,7 @@ module LLM
|
|
|
270
286
|
|
|
271
287
|
##
|
|
272
288
|
# Returns the configured tool execution concurrency.
|
|
273
|
-
# @return [Symbol, nil]
|
|
289
|
+
# @return [Symbol, Array<Symbol>, nil]
|
|
274
290
|
def concurrency
|
|
275
291
|
@concurrency
|
|
276
292
|
end
|
|
@@ -348,8 +364,8 @@ module LLM
|
|
|
348
364
|
def call_functions
|
|
349
365
|
case concurrency || :call
|
|
350
366
|
when :call then call(:functions)
|
|
351
|
-
when :thread, :task, :fiber, :ractor then wait(concurrency)
|
|
352
|
-
else raise ArgumentError, "Unknown concurrency: #{concurrency.inspect}. Expected :call, :thread, :task, :fiber, or
|
|
367
|
+
when :thread, :task, :fiber, :ractor, Array then wait(concurrency)
|
|
368
|
+
else raise ArgumentError, "Unknown concurrency: #{concurrency.inspect}. Expected :call, :thread, :task, :fiber, :ractor, or an array of queued task types"
|
|
353
369
|
end
|
|
354
370
|
end
|
|
355
371
|
end
|
data/lib/llm/context.rb
CHANGED
|
@@ -64,12 +64,14 @@ module LLM
|
|
|
64
64
|
# @option params [Symbol] :mode Defaults to :completions
|
|
65
65
|
# @option params [String] :model Defaults to the provider's default model
|
|
66
66
|
# @option params [Array<LLM::Function>, nil] :tools Defaults to nil
|
|
67
|
+
# @option params [Array<String>, nil] :skills Defaults to nil
|
|
67
68
|
def initialize(llm, params = {})
|
|
68
69
|
@llm = llm
|
|
69
70
|
@mode = params.delete(:mode) || :completions
|
|
71
|
+
tools = [*params.delete(:tools), *load_skills(params.delete(:skills))]
|
|
70
72
|
@params = {model: llm.default_model, schema: nil}.compact.merge!(params)
|
|
73
|
+
@params[:tools] = tools unless tools.empty?
|
|
71
74
|
@messages = LLM::Buffer.new(llm)
|
|
72
|
-
@owner = Fiber.current
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
##
|
|
@@ -86,6 +88,7 @@ module LLM
|
|
|
86
88
|
# puts res.messages[0].content
|
|
87
89
|
def talk(prompt, params = {})
|
|
88
90
|
return respond(prompt, params) if mode == :responses
|
|
91
|
+
@owner = Fiber.current
|
|
89
92
|
params = params.merge(messages: @messages.to_a)
|
|
90
93
|
params = @params.merge(params)
|
|
91
94
|
bind!(params[:stream], params[:model])
|
|
@@ -112,6 +115,7 @@ module LLM
|
|
|
112
115
|
# res = ctx.respond("What is the capital of France?")
|
|
113
116
|
# puts res.output_text
|
|
114
117
|
def respond(prompt, params = {})
|
|
118
|
+
@owner = Fiber.current
|
|
115
119
|
params = @params.merge(params)
|
|
116
120
|
bind!(params[:stream], params[:model])
|
|
117
121
|
res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
|
|
@@ -182,8 +186,10 @@ module LLM
|
|
|
182
186
|
# exposes a non-empty queue. Otherwise it falls back to waiting on
|
|
183
187
|
# the context's pending functions directly.
|
|
184
188
|
#
|
|
185
|
-
# @param [Symbol] strategy
|
|
186
|
-
# The concurrency strategy to use
|
|
189
|
+
# @param [Symbol, Array<Symbol>] strategy
|
|
190
|
+
# The concurrency strategy to use, or the possible concurrency strategies to
|
|
191
|
+
# wait on. For example, `[:thread, :ractor]` waits for any queued thread or
|
|
192
|
+
# ractor work, in that order.
|
|
187
193
|
# @return [Array<LLM::Function::Return>]
|
|
188
194
|
def wait(strategy)
|
|
189
195
|
stream = @params[:stream]
|
|
@@ -342,6 +348,10 @@ module LLM
|
|
|
342
348
|
stream.extra[:tracer] = tracer
|
|
343
349
|
stream.extra[:model] = model
|
|
344
350
|
end
|
|
351
|
+
|
|
352
|
+
def load_skills(skills)
|
|
353
|
+
[*skills].map { LLM::Skill.load(_1).to_tool(llm) }
|
|
354
|
+
end
|
|
345
355
|
end
|
|
346
356
|
|
|
347
357
|
# Backward-compatible alias
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Sequel
|
|
4
|
+
##
|
|
5
|
+
# Sequel plugin for persisting {LLM::Agent LLM::Agent} state.
|
|
6
|
+
#
|
|
7
|
+
# This wrapper reuses the same record-backed runtime surface as
|
|
8
|
+
# {LLM::Sequel::Plugin}, but builds an {LLM::Agent LLM::Agent} instead of an
|
|
9
|
+
# {LLM::Context LLM::Context}. Agent defaults such as model, tools, schema,
|
|
10
|
+
# instructions, and concurrency are configured on the model class and
|
|
11
|
+
# forwarded to an internal agent subclass.
|
|
12
|
+
module Agent
|
|
13
|
+
EMPTY_HASH = LLM::Sequel::Plugin::EMPTY_HASH
|
|
14
|
+
DEFAULT_USAGE_COLUMNS = LLM::Sequel::Plugin::DEFAULT_USAGE_COLUMNS
|
|
15
|
+
DEFAULTS = LLM::Sequel::Plugin::DEFAULTS
|
|
16
|
+
|
|
17
|
+
def self.apply(model, **)
|
|
18
|
+
model.extend ClassMethods
|
|
19
|
+
model.include LLM::Sequel::Plugin::InstanceMethods
|
|
20
|
+
model.include InstanceMethods
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.configure(model, options = EMPTY_HASH, &block)
|
|
24
|
+
options = DEFAULTS.merge(options)
|
|
25
|
+
usage_columns = DEFAULT_USAGE_COLUMNS.merge(options[:usage_columns] || EMPTY_HASH)
|
|
26
|
+
model.instance_variable_set(
|
|
27
|
+
:@llm_agent_options,
|
|
28
|
+
options.merge(usage_columns: usage_columns.freeze).freeze
|
|
29
|
+
)
|
|
30
|
+
model.instance_exec(&block) if block
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module ClassMethods
|
|
34
|
+
def llm_plugin_options
|
|
35
|
+
@llm_agent_options || Agent::DEFAULTS
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def model(model = nil)
|
|
39
|
+
return agent.model if model.nil?
|
|
40
|
+
agent.model(model)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tools(*tools)
|
|
44
|
+
return agent.tools if tools.empty?
|
|
45
|
+
agent.tools(*tools)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def schema(schema = nil)
|
|
49
|
+
return agent.schema if schema.nil?
|
|
50
|
+
agent.schema(schema)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def instructions(instructions = nil)
|
|
54
|
+
return agent.instructions if instructions.nil?
|
|
55
|
+
agent.instructions(instructions)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def concurrency(concurrency = nil)
|
|
59
|
+
return agent.concurrency if concurrency.nil?
|
|
60
|
+
agent.concurrency(concurrency)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def agent
|
|
64
|
+
@agent ||= Class.new(LLM::Agent)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module InstanceMethods
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def ctx
|
|
72
|
+
@ctx ||= begin
|
|
73
|
+
options = self.class.llm_plugin_options
|
|
74
|
+
params = resolve_options(options[:context]).dup
|
|
75
|
+
params[:model] ||= self[columns[:model_column]]
|
|
76
|
+
ctx = self.class.agent.new(llm, params.compact)
|
|
77
|
+
data = self[columns[:data_column]]
|
|
78
|
+
if data.nil? || data == ""
|
|
79
|
+
ctx
|
|
80
|
+
else
|
|
81
|
+
case options[:format]
|
|
82
|
+
when :string then ctx.restore(string: data)
|
|
83
|
+
when :json, :jsonb then ctx.restore(data:)
|
|
84
|
+
else raise ArgumentError, "Unknown format: #{options[:format].inspect}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def resolve_option(option)
|
|
91
|
+
case option
|
|
92
|
+
when Proc then instance_exec(&option)
|
|
93
|
+
when Symbol then send(option)
|
|
94
|
+
when Hash then option.dup
|
|
95
|
+
else option
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resolve_options(option)
|
|
100
|
+
case option
|
|
101
|
+
when Proc, Symbol, Hash then resolve_option(option)
|
|
102
|
+
else Agent::EMPTY_HASH.dup
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/llm/skill.rb
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Skill LLM::Skill} represents a directory-backed packaged capability.
|
|
6
|
+
# A skill directory must contain a `SKILL.md` file with YAML frontmatter.
|
|
7
|
+
# Skills can expose themselves as normal {LLM::Tool LLM::Tool} classes through
|
|
8
|
+
# {#to_tool}. This keeps skills on the same execution path as local tools.
|
|
9
|
+
class Skill
|
|
10
|
+
##
|
|
11
|
+
# Load a skill from a directory.
|
|
12
|
+
# @param [String, Pathname] path
|
|
13
|
+
# @return [LLM::Skill]
|
|
14
|
+
def self.load(path)
|
|
15
|
+
new(path).tap(&:load!)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Returns the skill directory.
|
|
20
|
+
# @return [String]
|
|
21
|
+
attr_reader :path
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Returns the skill name.
|
|
25
|
+
# @return [String]
|
|
26
|
+
attr_reader :name
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Returns the skill description.
|
|
30
|
+
# @return [String]
|
|
31
|
+
attr_reader :description
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Returns the skill instructions.
|
|
35
|
+
# @return [String]
|
|
36
|
+
attr_reader :instructions
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Returns the skill frontmatter.
|
|
40
|
+
# @return [LLM::Object]
|
|
41
|
+
attr_reader :frontmatter
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Returns the skill tools.
|
|
45
|
+
# @return [Array<Class<LLM::Tool>>]
|
|
46
|
+
attr_reader :tools
|
|
47
|
+
|
|
48
|
+
def initialize(path)
|
|
49
|
+
@path = path.to_s
|
|
50
|
+
@name = ::File.basename(@path)
|
|
51
|
+
@description = "Skill: #{@name}"
|
|
52
|
+
@instructions = ""
|
|
53
|
+
@frontmatter = LLM::Object.from({})
|
|
54
|
+
@tools = []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Load and parse the skill.
|
|
59
|
+
# @return [LLM::Skill]
|
|
60
|
+
def load!
|
|
61
|
+
path = ::File.join(@path, "SKILL.md")
|
|
62
|
+
parse(::File.read(path))
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Execute the skill by wrapping it in a small agent with the skill
|
|
68
|
+
# instructions. The provider is bound explicitly by the caller.
|
|
69
|
+
# @param [LLM::Provider] llm
|
|
70
|
+
# @param [Hash] input
|
|
71
|
+
# @return [Hash]
|
|
72
|
+
def call(llm, **)
|
|
73
|
+
instructions = self.instructions
|
|
74
|
+
tools = self.tools
|
|
75
|
+
agent = Class.new(LLM::Agent) do
|
|
76
|
+
instructions instructions
|
|
77
|
+
tools(*tools)
|
|
78
|
+
end.new(llm)
|
|
79
|
+
res = agent.talk(instructions)
|
|
80
|
+
{content: res.content}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Expose the skill as a normal LLM::Tool. The provider is bound explicitly
|
|
85
|
+
# when the tool class is built.
|
|
86
|
+
# @param [LLM::Provider] llm
|
|
87
|
+
# @return [Class<LLM::Tool>]
|
|
88
|
+
def to_tool(llm)
|
|
89
|
+
skill = self
|
|
90
|
+
Class.new(LLM::Tool) do
|
|
91
|
+
name skill.name
|
|
92
|
+
description skill.description
|
|
93
|
+
|
|
94
|
+
define_method(:call) do |**input|
|
|
95
|
+
skill.call(llm, **input)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def parse(content)
|
|
103
|
+
match = content.match(/\A---\s*\n(.*?)\n---\s*\n?(.*)\z/m)
|
|
104
|
+
unless match
|
|
105
|
+
@instructions = content
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
require "yaml" unless defined?(::YAML)
|
|
109
|
+
@frontmatter = LLM::Object.from(YAML.safe_load(match[1]) || {})
|
|
110
|
+
@name = @frontmatter.name || @name
|
|
111
|
+
@description = @frontmatter.description || @description
|
|
112
|
+
@tools = [*@frontmatter.tools].map { LLM::Tool.find_by_name!(_1) }
|
|
113
|
+
@instructions = match[2]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/llm/stream/queue.rb
CHANGED
|
@@ -33,27 +33,57 @@ class LLM::Stream
|
|
|
33
33
|
|
|
34
34
|
##
|
|
35
35
|
# Waits for queued work to finish and returns function results.
|
|
36
|
-
# @param [Symbol] strategy
|
|
37
|
-
# Controls concurrency strategy
|
|
36
|
+
# @param [Symbol, Array<Symbol>] strategy
|
|
37
|
+
# Controls concurrency strategy, or lists the possible concurrency strategies
|
|
38
|
+
# to wait on:
|
|
38
39
|
# - `:thread`: Use threads
|
|
39
40
|
# - `:task`: Use async tasks (requires async gem)
|
|
40
41
|
# - `:fiber`: Use raw fibers
|
|
41
42
|
# - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
|
|
43
|
+
# - `[:thread, :ractor]`: Wait for any queued thread or ractor work, in the
|
|
44
|
+
# given order. This is useful when different tools were spawned with
|
|
45
|
+
# different concurrency strategies.
|
|
42
46
|
# @return [Array<LLM::Function::Return>]
|
|
43
47
|
def wait(strategy)
|
|
44
48
|
returns, tasks = @items.shift(@items.length).partition { LLM::Function::Return === _1 }
|
|
45
|
-
results =
|
|
49
|
+
results = wait_tasks(tasks, strategy)
|
|
50
|
+
returns.concat fire_hooks(tasks, results)
|
|
51
|
+
end
|
|
52
|
+
alias_method :value, :wait
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def wait_tasks(tasks, strategy)
|
|
57
|
+
strategies = Array(strategy)
|
|
58
|
+
return wait_group(tasks, strategies.first) unless strategies.length > 1
|
|
59
|
+
grouped = strategies.to_h { [_1, []] }
|
|
60
|
+
tasks.each do |task|
|
|
61
|
+
grouped[task_strategy(task)] << task
|
|
62
|
+
end
|
|
63
|
+
strategies.flat_map do |name|
|
|
64
|
+
selected = grouped.fetch(name)
|
|
65
|
+
selected.empty? ? [] : wait_group(selected, name)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def wait_group(tasks, strategy)
|
|
70
|
+
case strategy
|
|
46
71
|
when :thread then LLM::Function::ThreadGroup.new(tasks).wait
|
|
47
72
|
when :task then LLM::Function::TaskGroup.new(tasks).wait
|
|
48
73
|
when :fiber then LLM::Function::FiberGroup.new(tasks).wait
|
|
49
74
|
when :ractor then LLM::Function::Ractor::Group.new(tasks).wait
|
|
50
75
|
else raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, or :ractor"
|
|
51
76
|
end
|
|
52
|
-
returns.concat fire_hooks(tasks, results)
|
|
53
77
|
end
|
|
54
|
-
alias_method :value, :wait
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
def task_strategy(task)
|
|
80
|
+
case task.task
|
|
81
|
+
when Thread then :thread
|
|
82
|
+
when Fiber then :fiber
|
|
83
|
+
when LLM::Function::Ractor::Task then :ractor
|
|
84
|
+
else :task
|
|
85
|
+
end
|
|
86
|
+
end
|
|
57
87
|
|
|
58
88
|
def fire_hooks(tasks, results)
|
|
59
89
|
results.each_with_index do |result, idx|
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antar Azri
|
|
@@ -371,9 +371,11 @@ files:
|
|
|
371
371
|
- lib/llm/schema/parser.rb
|
|
372
372
|
- lib/llm/schema/string.rb
|
|
373
373
|
- lib/llm/schema/version.rb
|
|
374
|
+
- lib/llm/sequel/agent.rb
|
|
374
375
|
- lib/llm/sequel/plugin.rb
|
|
375
376
|
- lib/llm/server_tool.rb
|
|
376
377
|
- lib/llm/session.rb
|
|
378
|
+
- lib/llm/skill.rb
|
|
377
379
|
- lib/llm/stream.rb
|
|
378
380
|
- lib/llm/stream/queue.rb
|
|
379
381
|
- lib/llm/tool.rb
|
|
@@ -386,6 +388,7 @@ files:
|
|
|
386
388
|
- lib/llm/usage.rb
|
|
387
389
|
- lib/llm/utils.rb
|
|
388
390
|
- lib/llm/version.rb
|
|
391
|
+
- lib/sequel/plugins/agent.rb
|
|
389
392
|
- lib/sequel/plugins/llm.rb
|
|
390
393
|
- llm.gemspec
|
|
391
394
|
homepage: https://github.com/llmrb/llm.rb
|