llm.rb 5.2.0 → 5.3.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: 03ed8d289dc230fb6404f2fb3d1482401354f078b3502cd550949bcff48d97d2
4
- data.tar.gz: 8b54acc8723263b5bf8c2d0025452e1448dfc66953a2c0d0c24c13e4d7b3343b
3
+ metadata.gz: 39e1632fb63f83a65c5a146ea2a2f4178d0d99d26d2a347f36d360c09ea9845d
4
+ data.tar.gz: 04b2236d5cac243cc496b686d8d8a5097676e7bcd6973bacfa8d1f7e8d48e270
5
5
  SHA512:
6
- metadata.gz: b088838c5b1860e30413ba87e2c66dec393b3bff51e462e38af5bc1f13b746b7bdf5d103b67f949aa31a6bc6da280da3e170f876743f6286f8a5674f6cee42a6
7
- data.tar.gz: 769fecd327298f7b17b731f181d3091194cddeb758e1723a20a5c789f4b0298ce9a5f5244aa3d4a807b8d8260d541e1286443ca9d841a11fd666cb354a7f893b
6
+ metadata.gz: 1b4a68bd3b3e109a00f996f520296405ad6066b4f17c1e59da4077c2023c5fe0c95e770b9bd563a531748a16d79355f8db5fb2dcd66ac42673698ddcbea07704
7
+ data.tar.gz: 12713c07834164f3d13d01613126488cc19e937b8f127fcdbf839d8c75f2f6b77c6207e7e3e6f515d996e35aedb6d6e7333ba563a3f4578f4c33f454d41c6088
data/CHANGELOG.md CHANGED
@@ -2,8 +2,47 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ Changes since `v5.3.0`.
6
+
7
+ ## v5.3.0
8
+
9
+ Changes since `v5.2.1`.
10
+
11
+ This release deepens llm.rb's request-rewriting and tool-definition surface.
12
+ It adds transformer lifecycle hooks to `LLM::Stream` so UIs can surface work
13
+ like PII scrubbing before a request is sent, and it adds a more explicit
14
+ OmniAI-style tool DSL form with `parameter` plus separate `required`
15
+ declarations while keeping the older `param ... required: true` style working.
16
+
17
+ ### Change
18
+
19
+ * **Add transformer stream lifecycle hooks** <br>
20
+ Add `on_transform` and `on_transform_finish` to
21
+ `LLM::Stream` so UIs can surface request rewriting work such as PII
22
+ scrubbing before a request is sent to the model.
23
+
24
+ * **Add a separate `required` tool DSL form** <br>
25
+ Add `parameter` as an alias of `param` and support `required %i[...]`
26
+ as a separate declaration, inspired by OmniAI-style tools, while keeping
27
+ the existing `param ... required: true` form working too.
28
+
29
+ ## v5.2.1
30
+
5
31
  Changes since `v5.2.0`.
6
32
 
33
+ This release tightens the streamed queue fix from `v5.2.0` for concurrent
34
+ workloads. Request-local streams now stay bound long enough for `wait` to
35
+ drain queued work and then clear cleanly so later waits fall back to the
36
+ context's configured stream.
37
+
38
+ ### Fix
39
+
40
+ * **Reset request-local streams after `wait` drains queued work** <br>
41
+ Keep per-call `stream:` bindings alive through `LLM::Context#wait` so
42
+ queued streamed tool work still resolves correctly, then clear the
43
+ request-local stream after the wait completes to avoid leaking it into
44
+ later turns.
45
+
7
46
  ## v5.2.0
8
47
 
9
48
  Changes since `v5.1.0`.
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <p align="center">
5
5
  <a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
6
6
  <a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
7
- <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-5.2.0-green.svg?" alt="Version"></a>
7
+ <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-5.3.0-green.svg?" alt="Version"></a>
8
8
  </p>
9
9
 
10
10
  ## About
@@ -26,7 +26,7 @@ execution model instead of a pile of adapters.
26
26
 
27
27
  Want to see some code? Jump to [the examples](#examples) section. <br>
28
28
  Want to see an agentic framework built on top of llm.rb? Check out [general-intelligence-systems/brute](https://github.com/general-intelligence-systems/brute). <br>
29
- Want a taste of what llm.rb can build? See [the screencast](#screencast).
29
+ Want to see a self-hosted LLM environment built on llm.rb? Check out [Relay](https://github.com/llmrb/relay).
30
30
 
31
31
  ## Architecture
32
32
 
@@ -193,11 +193,22 @@ Transformers let llm.rb rewrite outgoing prompts and params before a request
193
193
  is sent to the provider. They also live on
194
194
  [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html), but
195
195
  they solve a different problem from guards: instead of blocking execution,
196
- they can normalize or scrub what gets sent.
196
+ they can normalize or scrub what gets sent. When a stream is present, that
197
+ lifecycle is also exposed through
198
+ [`LLM::Stream`](https://0x1eef.github.io/x/llm.rb/LLM/Stream.html) with
199
+ `on_transform` and `on_transform_finish`.
197
200
 
198
201
  That makes them a good fit for things like PII scrubbing, prompt
199
202
  normalization, or request-level param injection. A transformer just needs to
200
- implement `call(ctx, prompt, params)` and return `[prompt, params]`.
203
+ implement `call(ctx, prompt, params)` and return `[prompt, params]`. That
204
+ means a transformer can scrub plain text prompts, but it can also scrub
205
+ [`LLM::Function::Return`](https://0x1eef.github.io/x/llm.rb/LLM/Function/Return.html)
206
+ values. In other words, you can intercept a tool call's return value and
207
+ modify it before sending it back to the LLM.
208
+
209
+ That is also a useful UI hook. A stream can surface messages like
210
+ `Anonymizing your data...` before a scrubber runs and `Data anonymized.`
211
+ after it finishes.
201
212
 
202
213
  ```ruby
203
214
  class ScrubPII
@@ -212,22 +223,45 @@ class ScrubPII
212
223
  def scrub(prompt)
213
224
  case prompt
214
225
  when String then prompt.gsub(EMAIL, "[REDACTED_EMAIL]")
226
+ when Array then prompt.map { scrub(_1) }
227
+ when LLM::Function::Return then on_tool_return(prompt)
215
228
  else prompt
216
229
  end
217
230
  end
231
+
232
+ def on_tool_return(result)
233
+ value = case result.name
234
+ when "lookup-customer" then scrub_value(result.value)
235
+ else result.value
236
+ end
237
+ LLM::Function::Return.new(result.id, result.name, value)
238
+ end
239
+
240
+ def scrub_value(value)
241
+ case value
242
+ when String then value.gsub(EMAIL, "[REDACTED_EMAIL]")
243
+ when Array then value.map { scrub_value(_1) }
244
+ when Hash then value.transform_values { scrub_value(_1) }
245
+ else value
246
+ end
247
+ end
218
248
  end
219
249
 
220
250
  ctx = LLM::Context.new(llm)
221
251
  ctx.transformer = ScrubPII.new
222
252
  ```
223
253
 
254
+ When a stream is present, that transformer lifecycle is also exposed through
255
+ `on_transform` and `on_transform_finish` on
256
+ [`LLM::Stream`](https://0x1eef.github.io/x/llm.rb/LLM/Stream.html).
257
+
224
258
  #### LLM::Stream
225
259
 
226
260
  `LLM::Stream` is not just for printing tokens. It supports `on_content`,
227
- `on_reasoning_content`, `on_tool_call`, `on_tool_return`, `on_compaction`,
228
- and `on_compaction_finish`, which means visible output, reasoning output, tool
229
- execution, and context compaction can all be driven through the same
230
- execution path.
261
+ `on_reasoning_content`, `on_tool_call`, `on_tool_return`, `on_transform`,
262
+ `on_transform_finish`, `on_compaction`, and `on_compaction_finish`, which
263
+ means visible output, reasoning output, request rewriting, tool execution,
264
+ and context compaction can all be driven through the same execution path.
231
265
 
232
266
  ```ruby
233
267
  class Stream < LLM::Stream
@@ -477,6 +511,29 @@ loop do
477
511
  end
478
512
  ```
479
513
 
514
+ #### Multimodal: Local Files
515
+
516
+ In llm.rb, a prompt can be a string, an [`LLM::Prompt`](https://0x1eef.github.io/x/llm.rb/LLM/Prompt.html), or an array.
517
+ When you use an array, each element can be plain text or a tagged object such as
518
+ [`ctx.image_url(...)`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#image_url-instance_method),
519
+ [`ctx.local_file(...)`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#local_file-instance_method),
520
+ or [`ctx.remote_file(...)`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#remote_file-instance_method).
521
+ Those tagged objects carry the metadata the provider adapter needs to turn one
522
+ Ruby prompt into the provider-specific multimodal request schema.
523
+
524
+ `ctx.local_file(path)` tags a local path as a `:local_file` object around
525
+ `LLM.File(path)`. If the model understands that file type, you can include it
526
+ directly in the prompt array instead of uploading it first through a provider
527
+ Files API:
528
+
529
+ ```ruby
530
+ require "llm"
531
+
532
+ llm = LLM.openai(key: ENV["KEY"])
533
+ ctx = LLM::Context.new(llm)
534
+ ctx.talk ["Summarize this document.", ctx.local_file("README.md")]
535
+ ```
536
+
480
537
  #### Agent
481
538
 
482
539
  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.
@@ -738,13 +795,6 @@ mcp.run do
738
795
  end
739
796
  ```
740
797
 
741
- ## Screencast
742
-
743
- This screencast was built on an older version of llm.rb, but it still shows
744
- how capable the runtime can be in a real application:
745
-
746
- [![Watch the llm.rb screencast](https://img.youtube.com/vi/Jb7LNUYlCf4/maxresdefault.jpg)](https://www.youtube.com/watch?v=x1K4wMeO_QA)
747
-
748
798
  ## Resources
749
799
 
750
800
  - [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) and
data/lib/llm/context.rb CHANGED
@@ -305,6 +305,7 @@ module LLM
305
305
  end
306
306
  ensure
307
307
  @queue = nil
308
+ @stream = nil
308
309
  end
309
310
 
310
311
  ##
@@ -488,7 +489,11 @@ module LLM
488
489
 
489
490
  def transform(prompt, params)
490
491
  return [prompt, params] unless transformer
492
+ stream = params[:stream]
493
+ stream.on_transform(self, transformer) if LLM::Stream === stream
491
494
  transformer.call(self, prompt, params)
495
+ ensure
496
+ stream.on_transform_finish(self, transformer) if LLM::Stream === stream
492
497
  end
493
498
 
494
499
  def guarded_return_for(function, warning)
data/lib/llm/function.rb CHANGED
@@ -42,6 +42,33 @@ class LLM::Function
42
42
  extend LLM::Function::Registry
43
43
  prepend LLM::Function::Tracing
44
44
 
45
+ ##
46
+ # {LLM::Function::Return LLM::Function::Return} represents the result of a
47
+ # tool call.
48
+ #
49
+ # In llm.rb, tool execution is not complete until the requested function is
50
+ # answered with a return object and that return is sent back through the
51
+ # context. This is the object that closes that loop.
52
+ #
53
+ # The return carries:
54
+ # - the tool call ID
55
+ # - the tool name
56
+ # - the tool's return value
57
+ #
58
+ # That value is usually a `Hash`, but it can be any JSON-like structure your
59
+ # tool returns. `LLM::Function#call` produces one automatically, and
60
+ # `LLM::Function#cancel` produces one that represents a cancelled tool call.
61
+ #
62
+ # You can also construct one directly when you need to intercept, scrub, or
63
+ # synthesize a tool return before sending it back to the model.
64
+ #
65
+ # @example Returning a normal tool result
66
+ # ret = LLM::Function::Return.new("call_1", "weather", {forecast: "sunny"})
67
+ # ctx.talk(ret)
68
+ #
69
+ # @example Returning a tool result after rewriting its payload
70
+ # value = ret.value.merge(email: "[REDACTED_EMAIL]")
71
+ # ctx.talk(LLM::Function::Return.new(ret.id, ret.name, value))
45
72
  Return = Struct.new(:id, :name, :value) do
46
73
  ##
47
74
  # Returns true when the return value represents an error.
data/lib/llm/stream.rb CHANGED
@@ -19,7 +19,7 @@ module LLM
19
19
  # The most common callback is {#on_content}, which also maps to {#<<}.
20
20
  # Providers may also call {#on_reasoning_content} and {#on_tool_call} when
21
21
  # that data is available. Runtime features such as context compaction may
22
- # also emit lifecycle callbacks like {#on_compaction}.
22
+ # also emit lifecycle callbacks like {#on_transform} or {#on_compaction}.
23
23
  class Stream
24
24
  require_relative "stream/queue"
25
25
 
@@ -112,6 +112,24 @@ module LLM
112
112
  nil
113
113
  end
114
114
 
115
+ ##
116
+ # Called before a context transformer rewrites a prompt.
117
+ # @param [LLM::Context] ctx
118
+ # @param [#call] transformer
119
+ # @return [nil]
120
+ def on_transform(ctx, transformer)
121
+ nil
122
+ end
123
+
124
+ ##
125
+ # Called after a context transformer finishes rewriting a prompt.
126
+ # @param [LLM::Context] ctx
127
+ # @param [#call] transformer
128
+ # @return [nil]
129
+ def on_transform_finish(ctx, transformer)
130
+ nil
131
+ end
132
+
115
133
  ##
116
134
  # Called before a context compaction starts.
117
135
  # @param [LLM::Context] ctx
@@ -11,7 +11,8 @@ class LLM::Tool
11
11
  # class Greeter < LLM::Tool
12
12
  # name "greeter"
13
13
  # description "Greets the user"
14
- # param :name, String, "The user's name", required: true
14
+ # parameter :name, String, "The user's name"
15
+ # required %i[name]
15
16
  #
16
17
  # def call(name:)
17
18
  # puts "Hello, #{name}!"
@@ -41,6 +42,19 @@ class LLM::Tool
41
42
  end
42
43
  end
43
44
  end
45
+ alias_method :parameter, :param
46
+
47
+ ##
48
+ # Mark existing parameters as required.
49
+ # @param names [Array<Symbol,String>]
50
+ # @return [LLM::Schema::Object]
51
+ def required(names)
52
+ lock do
53
+ function.params.tap do |schema|
54
+ [*names].each { Utils.fetch(schema.properties, _1).required }
55
+ end
56
+ end
57
+ end
44
58
 
45
59
  ##
46
60
  # @api private
@@ -68,6 +82,10 @@ class LLM::Tool
68
82
  leaf.enum(*enum) if enum
69
83
  leaf
70
84
  end
85
+
86
+ def fetch(properties, name)
87
+ properties[name] || properties.fetch(name.to_s)
88
+ end
71
89
  end
72
90
  end
73
91
  end
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "5.2.0"
4
+ VERSION = "5.3.0"
5
5
  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: 5.2.0
4
+ version: 5.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri