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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0bca66b2bd8873cf39abb3be19dc99ca20d558e40ef3e9f475bf1f33faef6b6
4
- data.tar.gz: c73a2c5093e7e09557242919feb5a377f25b0fa8a11249a9f346673ad7d3a921
3
+ metadata.gz: 96698cb3af793b0bd83cae7635279cefbff24f86b11f59c9209edd76f76b757c
4
+ data.tar.gz: 389e4372ab3b4a2e90020e6e2e838b5a36516d5a5dd82a71243975dfe6f8f959
5
5
  SHA512:
6
- metadata.gz: 2a00191aaab47702a794f9fa86d782f21832be2a7ef309bd558aa482100d7c66ddbdf3320e89c80af2942c6e33295f10d387702130162fbac7cc98fd9b24c9a8
7
- data.tar.gz: a6709f6fd265af673da771f635f34c68e28e490405700c1a59b18253391dbbcae09ce677a4251994d898a851ec08dc598c5ff858e516e25b1206948f509abf67
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 a lightweight runtime for building capable AI systems in Ruby.
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 are just tools loaded from directories** <br>
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 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.
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 do
358
- model "gpt-5.4-mini"
359
- instructions "You are a concise support assistant."
360
- tools SearchDocs, Escalate
361
- concurrency :thread
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.31
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.075,
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.llm_agent_options
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 :llm_agent_options, instance_accessor: false, default: DEFAULTS unless respond_to?(:llm_agent_options)
83
- self.llm_agent_options = options.merge(usage_columns: usage_columns.freeze).freeze
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.llm_agent_options
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.llm_agent_options
109
- params = resolve_options(options[:context]).dup
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
- ctx.talk(...).tap { flush }
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
- ctx.respond(...).tap { flush }
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
- params = resolve_options(options[:context]).dup
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 only on the first request.
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 @ctx.messages.empty?
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 @ctx.messages.empty?
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(llm) }
360
+ [*skills].map { LLM::Skill.load(_1).to_tool(self) }
354
361
  end
355
362
  end
356
363
 
@@ -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
- params = resolve_options(options[:context]).dup
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
@@ -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
- ctx.talk(...).tap { flush }
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
- ctx.respond(...).tap { flush }
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
- params = resolve_options(options[:context]).dup
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 provider is bound explicitly by the caller.
69
- # @param [LLM::Provider] llm
70
- # @param [Hash] input
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(llm, **)
73
- instructions = self.instructions
74
- tools = self.tools
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 instructions
80
+ instructions(instructions)
77
81
  tools(*tools)
78
- end.new(llm)
79
- res = agent.talk(instructions)
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 provider is bound explicitly
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::Provider] llm
91
+ # @param [LLM::Context] ctx
87
92
  # @return [Class<LLM::Tool>]
88
- def to_tool(llm)
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 |**input|
95
- skill.call(llm, **input)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "4.21.0"
4
+ VERSION = "4.22.0"
5
5
  end
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.21.0
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,