llm.rb 4.21.0 → 4.22.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 +49 -0
- data/README.md +230 -58
- data/data/anthropic.json +35 -2
- data/data/google.json +7 -2
- data/data/openai.json +0 -30
- data/lib/llm/active_record/acts_as_agent.rb +11 -64
- data/lib/llm/active_record/acts_as_llm.rb +81 -61
- data/lib/llm/agent.rb +15 -3
- data/lib/llm/context.rb +8 -1
- data/lib/llm/sequel/agent.rb +4 -17
- data/lib/llm/sequel/plugin.rb +82 -60
- data/lib/llm/skill.rb +29 -14
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +3 -0
- metadata +43 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96698cb3af793b0bd83cae7635279cefbff24f86b11f59c9209edd76f76b757c
|
|
4
|
+
data.tar.gz: 389e4372ab3b4a2e90020e6e2e838b5a36516d5a5dd82a71243975dfe6f8f959
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6bd4fa02802333bbb925db2e513913bd1669e8a4d7c85d8cb76b88399e9b0e84bfd5ddf922c7816a2afd0c0d76d6a9f8c873702c789665dfe3205ada01d34203
|
|
7
|
+
data.tar.gz: 0d579386ead2158a4e7ad4991ff0c025758ac51624947d07e5d112779d46cb36bcabdd492ac20bbabc981b3e75e25300d04ba8b86808e4825b5c66e2186e52ae
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
Changes since `v4.22.0`.
|
|
6
|
+
|
|
7
|
+
## v4.22.0
|
|
8
|
+
|
|
5
9
|
Changes since `v4.21.0`.
|
|
6
10
|
|
|
11
|
+
This release deepens the runtime shape of llm.rb. It reduces helper-method
|
|
12
|
+
surface on persisted ORM models, expands real ORM coverage, and makes skills
|
|
13
|
+
behave more like bounded sub-agents with inherited recent context and proper
|
|
14
|
+
instruction injection.
|
|
15
|
+
|
|
16
|
+
### Change
|
|
17
|
+
|
|
18
|
+
* **Reduce ActiveRecord wrapper model surface** <br>
|
|
19
|
+
Move helper methods such as option resolution, column mapping,
|
|
20
|
+
serialization, and persistence into `Utils` for the ActiveRecord
|
|
21
|
+
wrappers so wrapped models include fewer internal helper methods.
|
|
22
|
+
|
|
23
|
+
* **Reduce Sequel wrapper model surface** <br>
|
|
24
|
+
Move helper methods such as option resolution, column mapping,
|
|
25
|
+
serialization, and persistence into `Utils` for the Sequel wrappers
|
|
26
|
+
so wrapped models include fewer internal helper methods.
|
|
27
|
+
|
|
28
|
+
* **Expand ORM integration coverage** <br>
|
|
29
|
+
Add broader ActiveRecord and Sequel coverage for persisted context and
|
|
30
|
+
agent wrappers, including real SQLite-backed records and cassette-backed
|
|
31
|
+
OpenAI persistence paths.
|
|
32
|
+
|
|
33
|
+
* **Make skills inherit recent parent context** <br>
|
|
34
|
+
Run `LLM::Skill` with a curated slice of recent parent user and assistant
|
|
35
|
+
messages, prefixed with `Recent context:`, so skills behave more like
|
|
36
|
+
task-scoped sub-agents instead of instruction-only helpers.
|
|
37
|
+
|
|
38
|
+
### Fix
|
|
39
|
+
|
|
40
|
+
* **Fix Sequel `plugin :agent` load order** <br>
|
|
41
|
+
Require the shared Sequel plugin support from `LLM::Sequel::Agent` so
|
|
42
|
+
`plugin :agent` can load independently without raising
|
|
43
|
+
`uninitialized constant LLM::Sequel::Plugin`.
|
|
44
|
+
|
|
45
|
+
* **Make skill execution inherit parent context request settings** <br>
|
|
46
|
+
Run `LLM::Skill` through a parent `LLM::Context` instead of a bare
|
|
47
|
+
provider so nested skill agents inherit context-level settings such as
|
|
48
|
+
`mode: :responses`, `store: false`, streaming, and other request defaults,
|
|
49
|
+
while still keeping skill-local tools and avoiding parent schemas.
|
|
50
|
+
|
|
51
|
+
* **Keep agent instructions when history is preseeded** <br>
|
|
52
|
+
Inject `LLM::Agent` instructions once unless a system message is already
|
|
53
|
+
present, so agents and nested skills still get their instructions when
|
|
54
|
+
they start with inherited non-system context.
|
|
55
|
+
|
|
7
56
|
## v4.21.0
|
|
8
57
|
|
|
9
58
|
Changes since `v4.20.2`.
|
data/README.md
CHANGED
|
@@ -9,18 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
## About
|
|
11
11
|
|
|
12
|
-
llm.rb is
|
|
12
|
+
llm.rb is the most capable runtime for building AI systems in Ruby.
|
|
13
13
|
<br>
|
|
14
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.
|
|
18
|
-
|
|
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.
|
|
23
|
-
|
|
24
15
|
llm.rb is designed for Ruby, and although it works great in Rails, it is not tightly
|
|
25
16
|
coupled to it. It runs on the standard library by default (zero dependencies),
|
|
26
17
|
loads optional pieces only when needed, includes built-in ActiveRecord support through
|
|
@@ -29,6 +20,10 @@ loads optional pieces only when needed, includes built-in ActiveRecord support t
|
|
|
29
20
|
long-lived, tool-capable, stateful AI workflows instead of just
|
|
30
21
|
request/response helpers.
|
|
31
22
|
|
|
23
|
+
It provides one runtime for providers, agents, tools, skills, MCP servers, streaming,
|
|
24
|
+
schemas, files, and persisted state, so real systems can be built out of one coherent
|
|
25
|
+
execution model instead of a pile of adapters.
|
|
26
|
+
|
|
32
27
|
Want to see some code? Jump to [the examples](#examples) section. <br>
|
|
33
28
|
Want a taste of what llm.rb can build? See [the screencast](#screencast).
|
|
34
29
|
|
|
@@ -53,6 +48,175 @@ It holds:
|
|
|
53
48
|
Instead of switching abstractions for each feature, everything builds on the
|
|
54
49
|
same context object.
|
|
55
50
|
|
|
51
|
+
## Standout features
|
|
52
|
+
|
|
53
|
+
The following list is **not exhaustive**, but it covers a lot of ground.
|
|
54
|
+
|
|
55
|
+
#### Skills
|
|
56
|
+
|
|
57
|
+
Skills are reusable, directory-backed capabilities loaded from `SKILL.md`.
|
|
58
|
+
They run through the same runtime as tools, agents, and MCP. They do not
|
|
59
|
+
require a second orchestration layer or a parallel abstraction. If you've
|
|
60
|
+
used Claude or Codex, you know the general idea of skills, and llm.rb
|
|
61
|
+
supports that same concept with the same execution model as the rest of the
|
|
62
|
+
system.
|
|
63
|
+
|
|
64
|
+
In llm.rb, a skill has frontmatter and instructions. The frontmatter can
|
|
65
|
+
define `name`, `description`, and `tools`. The `tools` entries are tool names,
|
|
66
|
+
and each name must resolve to a subclass of
|
|
67
|
+
[`LLM::Tool`](https://0x1eef.github.io/x/llm.rb/LLM/Tool.html) that is already
|
|
68
|
+
loaded in the runtime.
|
|
69
|
+
|
|
70
|
+
If you want Claude/Codex-like skills that can drive scripts or shell
|
|
71
|
+
commands, you would typically pair the skill with a tool that can execute
|
|
72
|
+
system commands.
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
---
|
|
76
|
+
name: release
|
|
77
|
+
description: Prepare a release
|
|
78
|
+
tools:
|
|
79
|
+
- search_docs
|
|
80
|
+
- git
|
|
81
|
+
---
|
|
82
|
+
Review the release state, summarize what changed, and prepare the release.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class Agent < LLM::Agent
|
|
87
|
+
model "gpt-5.4-mini"
|
|
88
|
+
skills "./skills/release"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
92
|
+
Agent.new(llm, stream: $stdout).talk("Let's prepare the release!")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### ORM
|
|
96
|
+
|
|
97
|
+
Any ActiveRecord model or Sequel model can become an agent-capable model,
|
|
98
|
+
including existing business and domain models, without forcing you into a
|
|
99
|
+
separate agent table or a second persistence layer.
|
|
100
|
+
|
|
101
|
+
`acts_as_agent` extends a model with agent capabilities: the same runtime
|
|
102
|
+
surface as [`LLM::Agent`](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html),
|
|
103
|
+
because it actually wraps an `LLM::Agent`, plus persistence through a text,
|
|
104
|
+
JSON, or JSONB-backed column on the same table.
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
class Ticket < ApplicationRecord
|
|
109
|
+
acts_as_agent provider: :set_provider
|
|
110
|
+
model "gpt-5.4-mini"
|
|
111
|
+
instructions "You are a support assistant."
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def set_provider
|
|
116
|
+
{ key: ENV["#{provider.upcase}_SECRET"], persistent: true }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Agentic Patterns
|
|
122
|
+
|
|
123
|
+
llm.rb is especially strong when you want to build agentic systems in a Ruby
|
|
124
|
+
way. Agents can be ordinary application models with state, associations,
|
|
125
|
+
tools, skills, and persistence, which makes it much easier to build systems
|
|
126
|
+
where users have their own specialized agents instead of treating agents as
|
|
127
|
+
something outside the app.
|
|
128
|
+
|
|
129
|
+
That pattern works so well in llm.rb because
|
|
130
|
+
[`LLM::Agent`](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html),
|
|
131
|
+
`acts_as_agent`, `plugin :agent`, skills, tools, and persisted runtime state
|
|
132
|
+
all fit the same execution model. The runtime stays small enough that the
|
|
133
|
+
main design work becomes application design, not orchestration glue.
|
|
134
|
+
|
|
135
|
+
For a concrete example, see
|
|
136
|
+
[How to build a platform of agents](https://0x1eef.github.io/posts/how-to-build-a-platform-of-agents).
|
|
137
|
+
|
|
138
|
+
#### Persistence
|
|
139
|
+
|
|
140
|
+
The same runtime can be serialized to disk, restored later, persisted in JSON
|
|
141
|
+
or JSONB-backed ORM columns, resumed across process boundaries, or shared
|
|
142
|
+
across long-lived workflows.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
ctx = LLM::Context.new(llm)
|
|
146
|
+
ctx.talk("Remember that my favorite language is Ruby.")
|
|
147
|
+
ctx.save(path: "context.json")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### LLM::Stream
|
|
151
|
+
|
|
152
|
+
`LLM::Stream` is not just for printing tokens. It supports `on_content`,
|
|
153
|
+
`on_reasoning_content`, `on_tool_call`, and `on_tool_return`, which means
|
|
154
|
+
visible output, reasoning output, and tool execution can all be driven through
|
|
155
|
+
the same execution path.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
class Stream < LLM::Stream
|
|
159
|
+
def on_tool_call(tool, error)
|
|
160
|
+
queue << tool.spawn(:thread)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def on_tool_return(tool, result)
|
|
164
|
+
puts(result.value)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Concurrency
|
|
170
|
+
|
|
171
|
+
Tool execution can run sequentially with `:call` or concurrently through
|
|
172
|
+
`:thread`, `:task`, `:fiber`, and experimental `:ractor`, without rewriting
|
|
173
|
+
your tool layer.
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
class Agent < LLM::Agent
|
|
177
|
+
model "gpt-5.4-mini"
|
|
178
|
+
tools FetchWeather, FetchNews, FetchStock
|
|
179
|
+
concurrency :thread
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### MCP
|
|
184
|
+
|
|
185
|
+
Remote MCP tools and prompts are not bolted on as a separate integration
|
|
186
|
+
stack. They adapt into the same tool and prompt path used by local tools,
|
|
187
|
+
skills, contexts, and agents.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
begin
|
|
191
|
+
mcp = LLM::MCP.http(url: "https://api.githubcopilot.com/mcp/").persistent
|
|
192
|
+
mcp.start
|
|
193
|
+
ctx = LLM::Context.new(llm, tools: mcp.tools)
|
|
194
|
+
ensure
|
|
195
|
+
mcp.stop
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Cancellation
|
|
200
|
+
|
|
201
|
+
Cancellation is one of the harder problems to get right, and while llm.rb
|
|
202
|
+
makes it possible, it still requires careful engineering to use effectively.
|
|
203
|
+
The point though is that it is possible to stop in-flight provider work cleanly
|
|
204
|
+
through the same runtime, and the model used by llm.rb is directly inspired by
|
|
205
|
+
Go's context package. In fact, llm.rb is heavily inspired by Go but with a Ruby
|
|
206
|
+
twist.
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
ctx = LLM::Context.new(llm, stream: $stdout)
|
|
210
|
+
worker = Thread.new do
|
|
211
|
+
ctx.talk("Write a very long essay about network protocols.")
|
|
212
|
+
rescue LLM::Interrupt
|
|
213
|
+
puts "Request was interrupted!"
|
|
214
|
+
end
|
|
215
|
+
STDIN.getch
|
|
216
|
+
ctx.interrupt!
|
|
217
|
+
worker.join
|
|
218
|
+
```
|
|
219
|
+
|
|
56
220
|
## Differentiators
|
|
57
221
|
|
|
58
222
|
### Execution Model
|
|
@@ -137,11 +301,11 @@ same context object.
|
|
|
137
301
|
- **Tools are explicit** <br>
|
|
138
302
|
Run local tools, provider-native tools, and MCP tools through the same path
|
|
139
303
|
with fewer special cases.
|
|
140
|
-
- **Skills
|
|
304
|
+
- **Skills become bounded runtime capabilities** <br>
|
|
141
305
|
Point llm.rb at directories with a `SKILL.md`, resolve named tools through
|
|
142
|
-
the registry, and
|
|
143
|
-
|
|
144
|
-
|
|
306
|
+
the registry, and adapt each skill into its own callable capability through
|
|
307
|
+
the normal runtime. Unlike a generic skill-discovery tool, each skill runs
|
|
308
|
+
with its own bounded tool subset and behaves like a task-scoped sub-agent.
|
|
145
309
|
- **Providers are normalized, not flattened** <br>
|
|
146
310
|
Share one API surface across providers without losing access to provider-
|
|
147
311
|
specific capabilities where they matter.
|
|
@@ -173,24 +337,31 @@ same context object.
|
|
|
173
337
|
|
|
174
338
|
## Capabilities
|
|
175
339
|
|
|
340
|
+
Execution:
|
|
176
341
|
- **Chat & Contexts** — stateless and stateful interactions with persistence
|
|
177
342
|
- **Context Serialization** — save and restore state across processes or time
|
|
178
343
|
- **Streaming** — visible output, reasoning output, tool-call events
|
|
179
344
|
- **Request Interruption** — stop in-flight provider work cleanly
|
|
345
|
+
- **Concurrent Execution** — threads, async tasks, and fibers
|
|
346
|
+
|
|
347
|
+
Runtime Building Blocks:
|
|
180
348
|
- **Tool Calling** — class-based tools and closure-based functions
|
|
181
349
|
- **Run Tools While Streaming** — overlap model output with tool latency
|
|
182
|
-
- **Concurrent Execution** — threads, async tasks, and fibers
|
|
183
350
|
- **Agents** — reusable assistants with tool auto-execution
|
|
184
351
|
- **Skills** — directory-backed capabilities loaded from `SKILL.md`
|
|
352
|
+
- **MCP Support** — stdio and HTTP MCP clients with prompt and tool support
|
|
353
|
+
|
|
354
|
+
Data and Structure:
|
|
185
355
|
- **Structured Outputs** — JSON Schema-based responses
|
|
186
356
|
- **Responses API** — stateful response workflows where providers support them
|
|
187
|
-
- **MCP Support** — stdio and HTTP MCP clients with prompt and tool support
|
|
188
357
|
- **Multimodal Inputs** — text, images, audio, documents, URLs
|
|
189
358
|
- **Audio** — speech generation, transcription, translation
|
|
190
359
|
- **Images** — generation and editing
|
|
191
360
|
- **Files API** — upload and reference files in prompts
|
|
192
361
|
- **Embeddings** — vector generation for search and RAG
|
|
193
362
|
- **Vector Stores** — retrieval workflows
|
|
363
|
+
|
|
364
|
+
Operations:
|
|
194
365
|
- **Cost Tracking** — local cost estimation without extra API calls
|
|
195
366
|
- **Observability** — tracing, logging, telemetry
|
|
196
367
|
- **Model Registry** — local metadata for capabilities, limits, pricing
|
|
@@ -221,6 +392,44 @@ loop do
|
|
|
221
392
|
end
|
|
222
393
|
```
|
|
223
394
|
|
|
395
|
+
#### Agent
|
|
396
|
+
|
|
397
|
+
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.
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
require "llm"
|
|
401
|
+
|
|
402
|
+
class ShellAgent < LLM::Agent
|
|
403
|
+
model "gpt-5.4-mini"
|
|
404
|
+
instructions "You are a Linux system assistant."
|
|
405
|
+
tools Shell
|
|
406
|
+
concurrency :thread
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
410
|
+
agent = ShellAgent.new(llm)
|
|
411
|
+
puts agent.talk("What time is it on this system?").content
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### Skills
|
|
415
|
+
|
|
416
|
+
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. In llm.rb, a skill is exposed as a tool in the runtime. When that tool is called, it spawns a sub-agent with relevant context plus the instructions and tool subset declared in its own `SKILL.md`. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
417
|
+
|
|
418
|
+
Each skill runs only with the tools declared in its own frontmatter.
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
require "llm"
|
|
422
|
+
|
|
423
|
+
class Agent < LLM::Agent
|
|
424
|
+
model "gpt-5.4-mini"
|
|
425
|
+
instructions "You are a concise release assistant."
|
|
426
|
+
skills "./skills/release", "./skills/review"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
430
|
+
puts Agent.new(llm).talk("Use the review skill.").content
|
|
431
|
+
```
|
|
432
|
+
|
|
224
433
|
#### Streaming
|
|
225
434
|
|
|
226
435
|
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.
|
|
@@ -354,12 +563,11 @@ require "active_record"
|
|
|
354
563
|
require "llm/active_record"
|
|
355
564
|
|
|
356
565
|
class Ticket < ApplicationRecord
|
|
357
|
-
acts_as_agent provider: :set_provider
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
end
|
|
566
|
+
acts_as_agent provider: :set_provider
|
|
567
|
+
model "gpt-5.4-mini"
|
|
568
|
+
instructions "You are a concise support assistant."
|
|
569
|
+
tools SearchDocs, Escalate
|
|
570
|
+
concurrency :thread
|
|
363
571
|
|
|
364
572
|
private
|
|
365
573
|
|
|
@@ -372,42 +580,6 @@ ticket = Ticket.create!(provider: "openai", model: "gpt-5.4-mini")
|
|
|
372
580
|
puts ticket.talk("How do I rotate my API key?").content
|
|
373
581
|
```
|
|
374
582
|
|
|
375
|
-
#### Agent
|
|
376
|
-
|
|
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.
|
|
378
|
-
|
|
379
|
-
```ruby
|
|
380
|
-
require "llm"
|
|
381
|
-
|
|
382
|
-
class ShellAgent < LLM::Agent
|
|
383
|
-
model "gpt-5.4-mini"
|
|
384
|
-
instructions "You are a Linux system assistant."
|
|
385
|
-
tools Shell
|
|
386
|
-
concurrency :thread
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
llm = LLM.openai(key: ENV["KEY"])
|
|
390
|
-
agent = ShellAgent.new(llm)
|
|
391
|
-
puts agent.talk("What time is it on this system?").content
|
|
392
|
-
```
|
|
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
583
|
#### MCP
|
|
412
584
|
|
|
413
585
|
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.
|
data/data/anthropic.json
CHANGED
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"reasoning": true,
|
|
214
214
|
"tool_call": true,
|
|
215
215
|
"temperature": true,
|
|
216
|
-
"knowledge": "2025-08",
|
|
216
|
+
"knowledge": "2025-08-31",
|
|
217
217
|
"release_date": "2026-02-17",
|
|
218
218
|
"last_updated": "2026-03-13",
|
|
219
219
|
"modalities": {
|
|
@@ -271,6 +271,39 @@
|
|
|
271
271
|
"output": 32000
|
|
272
272
|
}
|
|
273
273
|
},
|
|
274
|
+
"claude-opus-4-7": {
|
|
275
|
+
"id": "claude-opus-4-7",
|
|
276
|
+
"name": "Claude Opus 4.7",
|
|
277
|
+
"family": "claude-opus",
|
|
278
|
+
"attachment": true,
|
|
279
|
+
"reasoning": true,
|
|
280
|
+
"tool_call": true,
|
|
281
|
+
"temperature": false,
|
|
282
|
+
"knowledge": "2026-01-31",
|
|
283
|
+
"release_date": "2026-04-16",
|
|
284
|
+
"last_updated": "2026-04-16",
|
|
285
|
+
"modalities": {
|
|
286
|
+
"input": [
|
|
287
|
+
"text",
|
|
288
|
+
"image",
|
|
289
|
+
"pdf"
|
|
290
|
+
],
|
|
291
|
+
"output": [
|
|
292
|
+
"text"
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
"open_weights": false,
|
|
296
|
+
"cost": {
|
|
297
|
+
"input": 5,
|
|
298
|
+
"output": 25,
|
|
299
|
+
"cache_read": 0.5,
|
|
300
|
+
"cache_write": 6.25
|
|
301
|
+
},
|
|
302
|
+
"limit": {
|
|
303
|
+
"context": 1000000,
|
|
304
|
+
"output": 128000
|
|
305
|
+
}
|
|
306
|
+
},
|
|
274
307
|
"claude-3-haiku-20240307": {
|
|
275
308
|
"id": "claude-3-haiku-20240307",
|
|
276
309
|
"name": "Claude Haiku 3",
|
|
@@ -609,7 +642,7 @@
|
|
|
609
642
|
"reasoning": true,
|
|
610
643
|
"tool_call": true,
|
|
611
644
|
"temperature": true,
|
|
612
|
-
"knowledge": "2025-05",
|
|
645
|
+
"knowledge": "2025-05-31",
|
|
613
646
|
"release_date": "2026-02-05",
|
|
614
647
|
"last_updated": "2026-03-13",
|
|
615
648
|
"modalities": {
|
data/data/google.json
CHANGED
|
@@ -594,7 +594,12 @@
|
|
|
594
594
|
"cost": {
|
|
595
595
|
"input": 1.25,
|
|
596
596
|
"output": 10,
|
|
597
|
-
"cache_read": 0.
|
|
597
|
+
"cache_read": 0.125,
|
|
598
|
+
"context_over_200k": {
|
|
599
|
+
"input": 2.5,
|
|
600
|
+
"output": 15,
|
|
601
|
+
"cache_read": 0.25
|
|
602
|
+
}
|
|
598
603
|
},
|
|
599
604
|
"limit": {
|
|
600
605
|
"context": 1048576,
|
|
@@ -824,7 +829,7 @@
|
|
|
824
829
|
"cost": {
|
|
825
830
|
"input": 0.3,
|
|
826
831
|
"output": 2.5,
|
|
827
|
-
"cache_read": 0.
|
|
832
|
+
"cache_read": 0.03,
|
|
828
833
|
"input_audio": 1
|
|
829
834
|
},
|
|
830
835
|
"limit": {
|
data/data/openai.json
CHANGED
|
@@ -1066,36 +1066,6 @@
|
|
|
1066
1066
|
"output": 100000
|
|
1067
1067
|
}
|
|
1068
1068
|
},
|
|
1069
|
-
"codex-mini-latest": {
|
|
1070
|
-
"id": "codex-mini-latest",
|
|
1071
|
-
"name": "Codex Mini",
|
|
1072
|
-
"family": "gpt-codex-mini",
|
|
1073
|
-
"attachment": true,
|
|
1074
|
-
"reasoning": true,
|
|
1075
|
-
"tool_call": true,
|
|
1076
|
-
"temperature": false,
|
|
1077
|
-
"knowledge": "2024-04",
|
|
1078
|
-
"release_date": "2025-05-16",
|
|
1079
|
-
"last_updated": "2025-05-16",
|
|
1080
|
-
"modalities": {
|
|
1081
|
-
"input": [
|
|
1082
|
-
"text"
|
|
1083
|
-
],
|
|
1084
|
-
"output": [
|
|
1085
|
-
"text"
|
|
1086
|
-
]
|
|
1087
|
-
},
|
|
1088
|
-
"open_weights": false,
|
|
1089
|
-
"cost": {
|
|
1090
|
-
"input": 1.5,
|
|
1091
|
-
"output": 6,
|
|
1092
|
-
"cache_read": 0.375
|
|
1093
|
-
},
|
|
1094
|
-
"limit": {
|
|
1095
|
-
"context": 200000,
|
|
1096
|
-
"output": 100000
|
|
1097
|
-
}
|
|
1098
|
-
},
|
|
1099
1069
|
"gpt-4": {
|
|
1100
1070
|
"id": "gpt-4",
|
|
1101
1071
|
"name": "GPT-4",
|
|
@@ -13,6 +13,7 @@ module LLM::ActiveRecord
|
|
|
13
13
|
EMPTY_HASH = LLM::ActiveRecord::ActsAsLLM::EMPTY_HASH
|
|
14
14
|
DEFAULT_USAGE_COLUMNS = LLM::ActiveRecord::ActsAsLLM::DEFAULT_USAGE_COLUMNS
|
|
15
15
|
DEFAULTS = LLM::ActiveRecord::ActsAsLLM::DEFAULTS
|
|
16
|
+
Utils = LLM::ActiveRecord::ActsAsLLM::Utils
|
|
16
17
|
|
|
17
18
|
module ClassMethods
|
|
18
19
|
def model(model = nil)
|
|
@@ -52,7 +53,7 @@ module LLM::ActiveRecord
|
|
|
52
53
|
# @param [Class] model
|
|
53
54
|
# @return [void]
|
|
54
55
|
def self.extended(model)
|
|
55
|
-
options = model.
|
|
56
|
+
options = model.llm_plugin_options
|
|
56
57
|
model.validates options[:provider_column], options[:model_column], presence: true
|
|
57
58
|
model.include LLM::ActiveRecord::ActsAsLLM::InstanceMethods unless model.ancestors.include?(LLM::ActiveRecord::ActsAsLLM::InstanceMethods)
|
|
58
59
|
model.include InstanceMethods unless model.ancestors.include?(InstanceMethods)
|
|
@@ -79,8 +80,8 @@ module LLM::ActiveRecord
|
|
|
79
80
|
def acts_as_agent(options = EMPTY_HASH, &block)
|
|
80
81
|
options = DEFAULTS.merge(options)
|
|
81
82
|
usage_columns = DEFAULT_USAGE_COLUMNS.merge(options[:usage_columns] || EMPTY_HASH)
|
|
82
|
-
class_attribute :
|
|
83
|
-
self.
|
|
83
|
+
class_attribute :llm_plugin_options, instance_accessor: false, default: DEFAULTS unless respond_to?(:llm_plugin_options)
|
|
84
|
+
self.llm_plugin_options = options.merge(usage_columns: usage_columns.freeze).freeze
|
|
84
85
|
extend Hooks
|
|
85
86
|
class_exec(&block) if block
|
|
86
87
|
end
|
|
@@ -90,12 +91,13 @@ module LLM::ActiveRecord
|
|
|
90
91
|
# Returns the resolved provider instance for this record.
|
|
91
92
|
# @return [LLM::Provider]
|
|
92
93
|
def llm
|
|
93
|
-
options = self.class.
|
|
94
|
+
options = self.class.llm_plugin_options
|
|
95
|
+
columns = Utils.columns(options)
|
|
94
96
|
provider = self[columns[:provider_column]]
|
|
95
|
-
kwargs = resolve_options(options[:provider])
|
|
97
|
+
kwargs = Utils.resolve_options(self, options[:provider], ActsAsAgent::EMPTY_HASH)
|
|
96
98
|
return @llm if @llm
|
|
97
99
|
@llm = LLM.method(provider).call(**kwargs)
|
|
98
|
-
@llm.tracer = resolve_option(options[:tracer]) if options[:tracer]
|
|
100
|
+
@llm.tracer = Utils.resolve_option(self, options[:tracer]) if options[:tracer]
|
|
99
101
|
@llm
|
|
100
102
|
end
|
|
101
103
|
|
|
@@ -105,8 +107,9 @@ module LLM::ActiveRecord
|
|
|
105
107
|
# @return [LLM::Agent]
|
|
106
108
|
def ctx
|
|
107
109
|
@ctx ||= begin
|
|
108
|
-
options = self.class.
|
|
109
|
-
|
|
110
|
+
options = self.class.llm_plugin_options
|
|
111
|
+
columns = Utils.columns(options)
|
|
112
|
+
params = Utils.resolve_options(self, options[:context], ActsAsAgent::EMPTY_HASH).dup
|
|
110
113
|
params[:model] ||= self[columns[:model_column]]
|
|
111
114
|
ctx = self.class.agent.new(llm, params.compact)
|
|
112
115
|
data = self[columns[:data_column]]
|
|
@@ -121,62 +124,6 @@ module LLM::ActiveRecord
|
|
|
121
124
|
end
|
|
122
125
|
end
|
|
123
126
|
end
|
|
124
|
-
|
|
125
|
-
##
|
|
126
|
-
# @return [void]
|
|
127
|
-
def flush
|
|
128
|
-
attrs = {
|
|
129
|
-
columns[:data_column] => serialize_context(self.class.llm_agent_options[:format]),
|
|
130
|
-
columns[:input_tokens] => ctx.usage.input_tokens,
|
|
131
|
-
columns[:output_tokens] => ctx.usage.output_tokens,
|
|
132
|
-
columns[:total_tokens] => ctx.usage.total_tokens
|
|
133
|
-
}
|
|
134
|
-
assign_attributes(attrs)
|
|
135
|
-
save!
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
##
|
|
139
|
-
# @return [Hash]
|
|
140
|
-
def resolve_option(option)
|
|
141
|
-
case option
|
|
142
|
-
when Proc then instance_exec(&option)
|
|
143
|
-
when Symbol then send(option)
|
|
144
|
-
when Hash then option.dup
|
|
145
|
-
else option
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
##
|
|
150
|
-
# @return [Hash]
|
|
151
|
-
def resolve_options(option)
|
|
152
|
-
case option
|
|
153
|
-
when Proc, Symbol, Hash then resolve_option(option)
|
|
154
|
-
else ActsAsAgent::EMPTY_HASH.dup
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def serialize_context(format)
|
|
159
|
-
case format
|
|
160
|
-
when :string then ctx.to_json
|
|
161
|
-
when :json, :jsonb then ctx.to_h
|
|
162
|
-
else raise ArgumentError, "Unknown format: #{format.inspect}"
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def columns
|
|
167
|
-
@columns ||= begin
|
|
168
|
-
options = self.class.llm_agent_options
|
|
169
|
-
usage_columns = options[:usage_columns]
|
|
170
|
-
{
|
|
171
|
-
provider_column: options[:provider_column],
|
|
172
|
-
model_column: options[:model_column],
|
|
173
|
-
data_column: options[:data_column],
|
|
174
|
-
input_tokens: usage_columns[:input_tokens],
|
|
175
|
-
output_tokens: usage_columns[:output_tokens],
|
|
176
|
-
total_tokens: usage_columns[:total_tokens]
|
|
177
|
-
}.freeze
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
127
|
end
|
|
181
128
|
end
|
|
182
129
|
end
|
|
@@ -33,6 +33,77 @@ module LLM::ActiveRecord
|
|
|
33
33
|
context: EMPTY_HASH
|
|
34
34
|
}.freeze
|
|
35
35
|
|
|
36
|
+
##
|
|
37
|
+
# Shared helper methods for the ORM wrapper.
|
|
38
|
+
#
|
|
39
|
+
# These utilities keep persistence plumbing out of the wrapped model's
|
|
40
|
+
# method namespace so the injected surface stays focused on the runtime
|
|
41
|
+
# API itself.
|
|
42
|
+
# @api private
|
|
43
|
+
module Utils
|
|
44
|
+
##
|
|
45
|
+
# Resolves a single configured option against a model instance.
|
|
46
|
+
# @return [Object]
|
|
47
|
+
def self.resolve_option(obj, option)
|
|
48
|
+
case option
|
|
49
|
+
when Proc then obj.instance_exec(&option)
|
|
50
|
+
when Symbol then obj.send(option)
|
|
51
|
+
when Hash then option.dup
|
|
52
|
+
else option
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Resolves hash-like wrapper options against a model instance.
|
|
58
|
+
# @return [Hash]
|
|
59
|
+
def self.resolve_options(obj, option, empty_hash)
|
|
60
|
+
case option
|
|
61
|
+
when Proc, Symbol, Hash then resolve_option(obj, option)
|
|
62
|
+
else empty_hash.dup
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Serializes the runtime into the configured storage format.
|
|
68
|
+
# @return [String, Hash]
|
|
69
|
+
def self.serialize_context(ctx, format)
|
|
70
|
+
case format
|
|
71
|
+
when :string then ctx.to_json
|
|
72
|
+
when :json, :jsonb then ctx.to_h
|
|
73
|
+
else raise ArgumentError, "Unknown format: #{format.inspect}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# Maps wrapper options onto the record's storage columns.
|
|
79
|
+
# @return [Hash]
|
|
80
|
+
def self.columns(options)
|
|
81
|
+
usage_columns = options[:usage_columns]
|
|
82
|
+
{
|
|
83
|
+
provider_column: options[:provider_column],
|
|
84
|
+
model_column: options[:model_column],
|
|
85
|
+
data_column: options[:data_column],
|
|
86
|
+
input_tokens: usage_columns[:input_tokens],
|
|
87
|
+
output_tokens: usage_columns[:output_tokens],
|
|
88
|
+
total_tokens: usage_columns[:total_tokens]
|
|
89
|
+
}.freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Persists the runtime state and usage columns back onto the record.
|
|
94
|
+
# @return [void]
|
|
95
|
+
def self.save(obj, ctx, options)
|
|
96
|
+
columns = self.columns(options)
|
|
97
|
+
obj.assign_attributes(
|
|
98
|
+
columns[:data_column] => serialize_context(ctx, options[:format]),
|
|
99
|
+
columns[:input_tokens] => ctx.usage.input_tokens,
|
|
100
|
+
columns[:output_tokens] => ctx.usage.output_tokens,
|
|
101
|
+
columns[:total_tokens] => ctx.usage.total_tokens
|
|
102
|
+
)
|
|
103
|
+
obj.save!
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
36
107
|
module Hooks
|
|
37
108
|
##
|
|
38
109
|
# Called when hooks are extended onto an ActiveRecord model.
|
|
@@ -72,7 +143,8 @@ module LLM::ActiveRecord
|
|
|
72
143
|
# @see LLM::Context#talk
|
|
73
144
|
# @return [LLM::Response]
|
|
74
145
|
def talk(...)
|
|
75
|
-
|
|
146
|
+
options = self.class.llm_plugin_options
|
|
147
|
+
ctx.talk(...).tap { Utils.save(self, ctx, options) }
|
|
76
148
|
end
|
|
77
149
|
|
|
78
150
|
##
|
|
@@ -80,7 +152,8 @@ module LLM::ActiveRecord
|
|
|
80
152
|
# @see LLM::Context#respond
|
|
81
153
|
# @return [LLM::Response]
|
|
82
154
|
def respond(...)
|
|
83
|
-
|
|
155
|
+
options = self.class.llm_plugin_options
|
|
156
|
+
ctx.respond(...).tap { Utils.save(self, ctx, options) }
|
|
84
157
|
end
|
|
85
158
|
|
|
86
159
|
##
|
|
@@ -155,6 +228,7 @@ module LLM::ActiveRecord
|
|
|
155
228
|
# Returns usage from the mapped usage columns.
|
|
156
229
|
# @return [LLM::Object]
|
|
157
230
|
def usage
|
|
231
|
+
columns = Utils.columns(self.class.llm_plugin_options)
|
|
158
232
|
LLM::Object.from(
|
|
159
233
|
input_tokens: self[columns[:input_tokens]] || 0,
|
|
160
234
|
output_tokens: self[columns[:output_tokens]] || 0,
|
|
@@ -211,11 +285,12 @@ module LLM::ActiveRecord
|
|
|
211
285
|
# @return [LLM::Provider]
|
|
212
286
|
def llm
|
|
213
287
|
options = self.class.llm_plugin_options
|
|
288
|
+
columns = Utils.columns(options)
|
|
214
289
|
provider = self[columns[:provider_column]]
|
|
215
|
-
kwargs = resolve_options(options[:provider])
|
|
290
|
+
kwargs = Utils.resolve_options(self, options[:provider], ActsAsLLM::EMPTY_HASH)
|
|
216
291
|
return @llm if @llm
|
|
217
292
|
@llm = LLM.method(provider).call(**kwargs)
|
|
218
|
-
@llm.tracer = resolve_option(options[:tracer]) if options[:tracer]
|
|
293
|
+
@llm.tracer = Utils.resolve_option(self, options[:tracer]) if options[:tracer]
|
|
219
294
|
@llm
|
|
220
295
|
end
|
|
221
296
|
|
|
@@ -226,7 +301,8 @@ module LLM::ActiveRecord
|
|
|
226
301
|
def ctx
|
|
227
302
|
@ctx ||= begin
|
|
228
303
|
options = self.class.llm_plugin_options
|
|
229
|
-
|
|
304
|
+
columns = Utils.columns(options)
|
|
305
|
+
params = Utils.resolve_options(self, options[:context], ActsAsLLM::EMPTY_HASH).dup
|
|
230
306
|
params[:model] ||= self[columns[:model_column]]
|
|
231
307
|
ctx = LLM::Context.new(llm, params.compact)
|
|
232
308
|
data = self[columns[:data_column]]
|
|
@@ -241,62 +317,6 @@ module LLM::ActiveRecord
|
|
|
241
317
|
end
|
|
242
318
|
end
|
|
243
319
|
end
|
|
244
|
-
|
|
245
|
-
##
|
|
246
|
-
# @return [void]
|
|
247
|
-
def flush
|
|
248
|
-
attrs = {
|
|
249
|
-
columns[:data_column] => serialize_context(self.class.llm_plugin_options[:format]),
|
|
250
|
-
columns[:input_tokens] => ctx.usage.input_tokens,
|
|
251
|
-
columns[:output_tokens] => ctx.usage.output_tokens,
|
|
252
|
-
columns[:total_tokens] => ctx.usage.total_tokens
|
|
253
|
-
}
|
|
254
|
-
assign_attributes(attrs)
|
|
255
|
-
save!
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
##
|
|
259
|
-
# @return [Hash]
|
|
260
|
-
def resolve_option(option)
|
|
261
|
-
case option
|
|
262
|
-
when Proc then instance_exec(&option)
|
|
263
|
-
when Symbol then send(option)
|
|
264
|
-
when Hash then option.dup
|
|
265
|
-
else option
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
##
|
|
270
|
-
# @return [Hash]
|
|
271
|
-
def resolve_options(option)
|
|
272
|
-
case option
|
|
273
|
-
when Proc, Symbol, Hash then resolve_option(option)
|
|
274
|
-
else ActsAsLLM::EMPTY_HASH.dup
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def serialize_context(format)
|
|
279
|
-
case format
|
|
280
|
-
when :string then ctx.to_json
|
|
281
|
-
when :json, :jsonb then ctx.to_h
|
|
282
|
-
else raise ArgumentError, "Unknown format: #{format.inspect}"
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def columns
|
|
287
|
-
@columns ||= begin
|
|
288
|
-
options = self.class.llm_plugin_options
|
|
289
|
-
usage_columns = options[:usage_columns]
|
|
290
|
-
{
|
|
291
|
-
provider_column: options[:provider_column],
|
|
292
|
-
model_column: options[:model_column],
|
|
293
|
-
data_column: options[:data_column],
|
|
294
|
-
input_tokens: usage_columns[:input_tokens],
|
|
295
|
-
output_tokens: usage_columns[:output_tokens],
|
|
296
|
-
total_tokens: usage_columns[:total_tokens]
|
|
297
|
-
}.freeze
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
320
|
end
|
|
301
321
|
end
|
|
302
322
|
end
|
data/lib/llm/agent.rb
CHANGED
|
@@ -14,7 +14,7 @@ module LLM
|
|
|
14
14
|
# `respond`, instead of leaving tool loops to the caller.
|
|
15
15
|
#
|
|
16
16
|
# **Notes:**
|
|
17
|
-
# * Instructions are injected
|
|
17
|
+
# * Instructions are injected once unless a system message is already present.
|
|
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
20
|
# `:thread`, `:task`, `:fiber`, `:ractor`, or a list of queued task
|
|
@@ -349,16 +349,28 @@ module LLM
|
|
|
349
349
|
instr = self.class.instructions
|
|
350
350
|
return new_prompt unless instr
|
|
351
351
|
if LLM::Prompt === new_prompt
|
|
352
|
-
new_prompt.system(instr) if
|
|
352
|
+
new_prompt.system(instr) if inject_instructions?(new_prompt)
|
|
353
353
|
new_prompt
|
|
354
354
|
else
|
|
355
355
|
prompt do
|
|
356
|
-
_1.system(instr) if
|
|
356
|
+
_1.system(instr) if inject_instructions?
|
|
357
357
|
_1.user(new_prompt)
|
|
358
358
|
end
|
|
359
359
|
end
|
|
360
360
|
end
|
|
361
361
|
|
|
362
|
+
##
|
|
363
|
+
# Returns true when agent instructions should be injected for the turn.
|
|
364
|
+
# Instructions are injected once unless a system message is already
|
|
365
|
+
# present in the existing context or the prompt being sent.
|
|
366
|
+
# @param [LLM::Prompt, nil] prompt
|
|
367
|
+
# @return [Boolean]
|
|
368
|
+
def inject_instructions?(prompt = nil)
|
|
369
|
+
return false if @ctx.messages.any?(&:system?)
|
|
370
|
+
return true if prompt.nil?
|
|
371
|
+
!prompt.to_a.any?(&:system?)
|
|
372
|
+
end
|
|
373
|
+
|
|
362
374
|
##
|
|
363
375
|
# @return [Array<LLM::Function::Return>]
|
|
364
376
|
def call_functions
|
data/lib/llm/context.rb
CHANGED
|
@@ -54,6 +54,13 @@ module LLM
|
|
|
54
54
|
# @return [Symbol]
|
|
55
55
|
attr_reader :mode
|
|
56
56
|
|
|
57
|
+
##
|
|
58
|
+
# Returns the default params for this context
|
|
59
|
+
# @return [Hash]
|
|
60
|
+
def params
|
|
61
|
+
@params.dup
|
|
62
|
+
end
|
|
63
|
+
|
|
57
64
|
##
|
|
58
65
|
# @param [LLM::Provider] llm
|
|
59
66
|
# A provider
|
|
@@ -350,7 +357,7 @@ module LLM
|
|
|
350
357
|
end
|
|
351
358
|
|
|
352
359
|
def load_skills(skills)
|
|
353
|
-
[*skills].map { LLM::Skill.load(_1).to_tool(
|
|
360
|
+
[*skills].map { LLM::Skill.load(_1).to_tool(self) }
|
|
354
361
|
end
|
|
355
362
|
end
|
|
356
363
|
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -10,9 +10,11 @@ module LLM::Sequel
|
|
|
10
10
|
# instructions, and concurrency are configured on the model class and
|
|
11
11
|
# forwarded to an internal agent subclass.
|
|
12
12
|
module Agent
|
|
13
|
+
require_relative "plugin"
|
|
13
14
|
EMPTY_HASH = LLM::Sequel::Plugin::EMPTY_HASH
|
|
14
15
|
DEFAULT_USAGE_COLUMNS = LLM::Sequel::Plugin::DEFAULT_USAGE_COLUMNS
|
|
15
16
|
DEFAULTS = LLM::Sequel::Plugin::DEFAULTS
|
|
17
|
+
Utils = LLM::Sequel::Plugin::Utils
|
|
16
18
|
|
|
17
19
|
def self.apply(model, **)
|
|
18
20
|
model.extend ClassMethods
|
|
@@ -71,7 +73,8 @@ module LLM::Sequel
|
|
|
71
73
|
def ctx
|
|
72
74
|
@ctx ||= begin
|
|
73
75
|
options = self.class.llm_plugin_options
|
|
74
|
-
|
|
76
|
+
columns = Agent::Utils.columns(options)
|
|
77
|
+
params = Agent::Utils.resolve_options(self, options[:context], Agent::EMPTY_HASH).dup
|
|
75
78
|
params[:model] ||= self[columns[:model_column]]
|
|
76
79
|
ctx = self.class.agent.new(llm, params.compact)
|
|
77
80
|
data = self[columns[:data_column]]
|
|
@@ -86,22 +89,6 @@ module LLM::Sequel
|
|
|
86
89
|
end
|
|
87
90
|
end
|
|
88
91
|
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
92
|
end
|
|
106
93
|
end
|
|
107
94
|
end
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -22,6 +22,76 @@ module LLM::Sequel
|
|
|
22
22
|
output_tokens: :output_tokens,
|
|
23
23
|
total_tokens: :total_tokens
|
|
24
24
|
}.freeze
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Shared helper methods for the ORM wrapper.
|
|
28
|
+
#
|
|
29
|
+
# These utilities keep persistence plumbing out of the wrapped model's
|
|
30
|
+
# method namespace so the injected surface stays focused on the runtime
|
|
31
|
+
# API itself.
|
|
32
|
+
# @api private
|
|
33
|
+
module Utils
|
|
34
|
+
##
|
|
35
|
+
# Resolves a single configured option against a model instance.
|
|
36
|
+
# @return [Object]
|
|
37
|
+
def self.resolve_option(obj, option)
|
|
38
|
+
case option
|
|
39
|
+
when Proc then obj.instance_exec(&option)
|
|
40
|
+
when Symbol then obj.send(option)
|
|
41
|
+
when Hash then option.dup
|
|
42
|
+
else option
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# Resolves hash-like wrapper options against a model instance.
|
|
48
|
+
# @return [Hash]
|
|
49
|
+
def self.resolve_options(obj, option, empty_hash)
|
|
50
|
+
case option
|
|
51
|
+
when Proc, Symbol, Hash then resolve_option(obj, option)
|
|
52
|
+
else empty_hash.dup
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Serializes the runtime into the configured storage format.
|
|
58
|
+
# @return [String, Hash]
|
|
59
|
+
def self.serialize_context(ctx, format)
|
|
60
|
+
case format
|
|
61
|
+
when :string then ctx.to_json
|
|
62
|
+
when :json, :jsonb then ctx.to_h
|
|
63
|
+
else raise ArgumentError, "Unknown format: #{format.inspect}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# Maps wrapper options onto the record's storage columns.
|
|
69
|
+
# @return [Hash]
|
|
70
|
+
def self.columns(options)
|
|
71
|
+
usage_columns = options[:usage_columns]
|
|
72
|
+
{
|
|
73
|
+
provider_column: options[:provider_column],
|
|
74
|
+
model_column: options[:model_column],
|
|
75
|
+
data_column: options[:data_column],
|
|
76
|
+
input_tokens: usage_columns[:input_tokens],
|
|
77
|
+
output_tokens: usage_columns[:output_tokens],
|
|
78
|
+
total_tokens: usage_columns[:total_tokens]
|
|
79
|
+
}.freeze
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# Persists the runtime state and usage columns back onto the record.
|
|
84
|
+
# @return [void]
|
|
85
|
+
def self.save(obj, ctx, options)
|
|
86
|
+
columns = self.columns(options)
|
|
87
|
+
obj.update(
|
|
88
|
+
columns[:data_column] => serialize_context(ctx, options[:format]),
|
|
89
|
+
columns[:input_tokens] => ctx.usage.input_tokens,
|
|
90
|
+
columns[:output_tokens] => ctx.usage.output_tokens,
|
|
91
|
+
columns[:total_tokens] => ctx.usage.total_tokens
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
25
95
|
DEFAULTS = {
|
|
26
96
|
provider_column: :provider,
|
|
27
97
|
model_column: :model,
|
|
@@ -84,12 +154,15 @@ module LLM::Sequel
|
|
|
84
154
|
end
|
|
85
155
|
|
|
86
156
|
module Plugin::InstanceMethods
|
|
157
|
+
Utils = Plugin::Utils
|
|
158
|
+
|
|
87
159
|
##
|
|
88
160
|
# Continues the stored context with new input and flushes it.
|
|
89
161
|
# @see LLM::Context#talk
|
|
90
162
|
# @return [LLM::Response]
|
|
91
163
|
def talk(...)
|
|
92
|
-
|
|
164
|
+
options = self.class.llm_plugin_options
|
|
165
|
+
ctx.talk(...).tap { Utils.save(self, ctx, options) }
|
|
93
166
|
end
|
|
94
167
|
|
|
95
168
|
##
|
|
@@ -97,7 +170,8 @@ module LLM::Sequel
|
|
|
97
170
|
# @see LLM::Context#respond
|
|
98
171
|
# @return [LLM::Response]
|
|
99
172
|
def respond(...)
|
|
100
|
-
|
|
173
|
+
options = self.class.llm_plugin_options
|
|
174
|
+
ctx.respond(...).tap { Utils.save(self, ctx, options) }
|
|
101
175
|
end
|
|
102
176
|
|
|
103
177
|
##
|
|
@@ -173,6 +247,7 @@ module LLM::Sequel
|
|
|
173
247
|
# Returns usage from the mapped usage columns.
|
|
174
248
|
# @return [LLM::Object]
|
|
175
249
|
def usage
|
|
250
|
+
columns = Utils.columns(self.class.llm_plugin_options)
|
|
176
251
|
LLM::Object.from(
|
|
177
252
|
input_tokens: self[columns[:input_tokens]] || 0,
|
|
178
253
|
output_tokens: self[columns[:output_tokens]] || 0,
|
|
@@ -229,11 +304,12 @@ module LLM::Sequel
|
|
|
229
304
|
# @return [LLM::Provider]
|
|
230
305
|
def llm
|
|
231
306
|
options = self.class.llm_plugin_options
|
|
307
|
+
columns = Utils.columns(options)
|
|
232
308
|
provider = self[columns[:provider_column]]
|
|
233
|
-
kwargs = resolve_options(options[:provider])
|
|
309
|
+
kwargs = Utils.resolve_options(self, options[:provider], Plugin::EMPTY_HASH)
|
|
234
310
|
return @llm if @llm
|
|
235
311
|
@llm = LLM.method(provider).call(**kwargs)
|
|
236
|
-
@llm.tracer = resolve_option(options[:tracer]) if options[:tracer]
|
|
312
|
+
@llm.tracer = Utils.resolve_option(self, options[:tracer]) if options[:tracer]
|
|
237
313
|
@llm
|
|
238
314
|
end
|
|
239
315
|
|
|
@@ -244,7 +320,8 @@ module LLM::Sequel
|
|
|
244
320
|
def ctx
|
|
245
321
|
@ctx ||= begin
|
|
246
322
|
options = self.class.llm_plugin_options
|
|
247
|
-
|
|
323
|
+
columns = Utils.columns(options)
|
|
324
|
+
params = Utils.resolve_options(self, options[:context], Plugin::EMPTY_HASH).dup
|
|
248
325
|
params[:model] ||= self[columns[:model_column]]
|
|
249
326
|
ctx = LLM::Context.new(llm, params.compact)
|
|
250
327
|
data = self[columns[:data_column]]
|
|
@@ -259,60 +336,5 @@ module LLM::Sequel
|
|
|
259
336
|
end
|
|
260
337
|
end
|
|
261
338
|
end
|
|
262
|
-
|
|
263
|
-
##
|
|
264
|
-
# @return [void]
|
|
265
|
-
def flush
|
|
266
|
-
options = self.class.llm_plugin_options
|
|
267
|
-
update({
|
|
268
|
-
columns[:data_column] => serialize_context(options[:format]),
|
|
269
|
-
columns[:input_tokens] => ctx.usage.input_tokens,
|
|
270
|
-
columns[:output_tokens] => ctx.usage.output_tokens,
|
|
271
|
-
columns[:total_tokens] => ctx.usage.total_tokens
|
|
272
|
-
})
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
##
|
|
276
|
-
# @return [Hash]
|
|
277
|
-
def resolve_option(option)
|
|
278
|
-
case option
|
|
279
|
-
when Proc then instance_exec(&option)
|
|
280
|
-
when Symbol then send(option)
|
|
281
|
-
when Hash then option.dup
|
|
282
|
-
else option
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
##
|
|
287
|
-
# @return [Hash]
|
|
288
|
-
def resolve_options(option)
|
|
289
|
-
case option
|
|
290
|
-
when Proc, Symbol, Hash then resolve_option(option)
|
|
291
|
-
else Plugin::EMPTY_HASH.dup
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def serialize_context(format)
|
|
296
|
-
case format
|
|
297
|
-
when :string then ctx.to_json
|
|
298
|
-
when :json, :jsonb then ctx.to_h
|
|
299
|
-
else raise ArgumentError, "Unknown format: #{format.inspect}"
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def columns
|
|
304
|
-
@columns ||= begin
|
|
305
|
-
options = self.class.llm_plugin_options
|
|
306
|
-
usage_columns = options[:usage_columns]
|
|
307
|
-
{
|
|
308
|
-
provider_column: options[:provider_column],
|
|
309
|
-
model_column: options[:model_column],
|
|
310
|
-
data_column: options[:data_column],
|
|
311
|
-
input_tokens: usage_columns[:input_tokens],
|
|
312
|
-
output_tokens: usage_columns[:output_tokens],
|
|
313
|
-
total_tokens: usage_columns[:total_tokens]
|
|
314
|
-
}.freeze
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
339
|
end
|
|
318
340
|
end
|
data/lib/llm/skill.rb
CHANGED
|
@@ -45,6 +45,10 @@ module LLM
|
|
|
45
45
|
# @return [Array<Class<LLM::Tool>>]
|
|
46
46
|
attr_reader :tools
|
|
47
47
|
|
|
48
|
+
##
|
|
49
|
+
# @param [String] path
|
|
50
|
+
# The path to a directory
|
|
51
|
+
# @return [LLM::Skill]
|
|
48
52
|
def initialize(path)
|
|
49
53
|
@path = path.to_s
|
|
50
54
|
@name = ::File.basename(@path)
|
|
@@ -65,40 +69,51 @@ module LLM
|
|
|
65
69
|
|
|
66
70
|
##
|
|
67
71
|
# Execute the skill by wrapping it in a small agent with the skill
|
|
68
|
-
# instructions. The
|
|
69
|
-
#
|
|
70
|
-
# @param [
|
|
72
|
+
# instructions. The context is bound explicitly by the caller so the
|
|
73
|
+
# nested agent can inherit context-level behavior such as streaming.
|
|
74
|
+
# @param [LLM::Context] ctx
|
|
71
75
|
# @return [Hash]
|
|
72
|
-
def call(
|
|
73
|
-
instructions = self.instructions
|
|
74
|
-
|
|
76
|
+
def call(ctx)
|
|
77
|
+
instructions, tools = self.instructions, self.tools
|
|
78
|
+
params = ctx.params.merge(mode: ctx.mode).reject { [:tools, :schema].include?(_1) }
|
|
75
79
|
agent = Class.new(LLM::Agent) do
|
|
76
|
-
instructions
|
|
80
|
+
instructions(instructions)
|
|
77
81
|
tools(*tools)
|
|
78
|
-
end.new(llm)
|
|
79
|
-
|
|
82
|
+
end.new(ctx.llm, params)
|
|
83
|
+
agent.messages.concat(messages_for(ctx))
|
|
84
|
+
res = agent.talk("Solve the user's query.")
|
|
80
85
|
{content: res.content}
|
|
81
86
|
end
|
|
82
87
|
|
|
83
88
|
##
|
|
84
|
-
# Expose the skill as a normal LLM::Tool. The
|
|
89
|
+
# Expose the skill as a normal LLM::Tool. The context is bound explicitly
|
|
85
90
|
# when the tool class is built.
|
|
86
|
-
# @param [LLM::
|
|
91
|
+
# @param [LLM::Context] ctx
|
|
87
92
|
# @return [Class<LLM::Tool>]
|
|
88
|
-
def to_tool(
|
|
93
|
+
def to_tool(ctx)
|
|
89
94
|
skill = self
|
|
90
95
|
Class.new(LLM::Tool) do
|
|
91
96
|
name skill.name
|
|
92
97
|
description skill.description
|
|
93
98
|
|
|
94
|
-
define_method(:call) do
|
|
95
|
-
skill.call(
|
|
99
|
+
define_method(:call) do
|
|
100
|
+
skill.call(ctx)
|
|
96
101
|
end
|
|
97
102
|
end
|
|
98
103
|
end
|
|
99
104
|
|
|
100
105
|
private
|
|
101
106
|
|
|
107
|
+
def messages_for(ctx)
|
|
108
|
+
messages = ctx.messages
|
|
109
|
+
.to_a
|
|
110
|
+
.select { _1.user? || _1.assistant? }
|
|
111
|
+
.reject { _1.tool_call? || _1.tool_return? }
|
|
112
|
+
.last(8)
|
|
113
|
+
return messages if messages.empty?
|
|
114
|
+
[LLM::Message.new(:user, "Recent context:"), *messages]
|
|
115
|
+
end
|
|
116
|
+
|
|
102
117
|
def parse(content)
|
|
103
118
|
match = content.match(/\A---\s*\n(.*?)\n---\s*\n?(.*)\z/m)
|
|
104
119
|
unless match
|
data/lib/llm/version.rb
CHANGED
data/llm.gemspec
CHANGED
|
@@ -54,4 +54,7 @@ Gem::Specification.new do |spec|
|
|
|
54
54
|
spec.add_development_dependency "net-http-persistent", "~> 4.0"
|
|
55
55
|
spec.add_development_dependency "opentelemetry-sdk", "~> 1.10"
|
|
56
56
|
spec.add_development_dependency "logger", "~> 1.7"
|
|
57
|
+
spec.add_development_dependency "activerecord", "~> 8.0"
|
|
58
|
+
spec.add_development_dependency "sequel", "~> 5.0"
|
|
59
|
+
spec.add_development_dependency "sqlite3", "~> 2.0"
|
|
57
60
|
end
|
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.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antar Azri
|
|
@@ -194,6 +194,48 @@ dependencies:
|
|
|
194
194
|
- - "~>"
|
|
195
195
|
- !ruby/object:Gem::Version
|
|
196
196
|
version: '1.7'
|
|
197
|
+
- !ruby/object:Gem::Dependency
|
|
198
|
+
name: activerecord
|
|
199
|
+
requirement: !ruby/object:Gem::Requirement
|
|
200
|
+
requirements:
|
|
201
|
+
- - "~>"
|
|
202
|
+
- !ruby/object:Gem::Version
|
|
203
|
+
version: '8.0'
|
|
204
|
+
type: :development
|
|
205
|
+
prerelease: false
|
|
206
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
207
|
+
requirements:
|
|
208
|
+
- - "~>"
|
|
209
|
+
- !ruby/object:Gem::Version
|
|
210
|
+
version: '8.0'
|
|
211
|
+
- !ruby/object:Gem::Dependency
|
|
212
|
+
name: sequel
|
|
213
|
+
requirement: !ruby/object:Gem::Requirement
|
|
214
|
+
requirements:
|
|
215
|
+
- - "~>"
|
|
216
|
+
- !ruby/object:Gem::Version
|
|
217
|
+
version: '5.0'
|
|
218
|
+
type: :development
|
|
219
|
+
prerelease: false
|
|
220
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
221
|
+
requirements:
|
|
222
|
+
- - "~>"
|
|
223
|
+
- !ruby/object:Gem::Version
|
|
224
|
+
version: '5.0'
|
|
225
|
+
- !ruby/object:Gem::Dependency
|
|
226
|
+
name: sqlite3
|
|
227
|
+
requirement: !ruby/object:Gem::Requirement
|
|
228
|
+
requirements:
|
|
229
|
+
- - "~>"
|
|
230
|
+
- !ruby/object:Gem::Version
|
|
231
|
+
version: '2.0'
|
|
232
|
+
type: :development
|
|
233
|
+
prerelease: false
|
|
234
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
235
|
+
requirements:
|
|
236
|
+
- - "~>"
|
|
237
|
+
- !ruby/object:Gem::Version
|
|
238
|
+
version: '2.0'
|
|
197
239
|
description: |
|
|
198
240
|
llm.rb is a lightweight runtime for building capable AI systems in Ruby.
|
|
199
241
|
It is not just an API wrapper. llm.rb gives you one runtime for providers,
|