llm.rb 5.2.1 → 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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +65 -15
- data/lib/llm/context.rb +4 -0
- data/lib/llm/function.rb +27 -0
- data/lib/llm/stream.rb +19 -1
- data/lib/llm/tool/param.rb +19 -1
- data/lib/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39e1632fb63f83a65c5a146ea2a2f4178d0d99d26d2a347f36d360c09ea9845d
|
|
4
|
+
data.tar.gz: 04b2236d5cac243cc496b686d8d8a5097676e7bcd6973bacfa8d1f7e8d48e270
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b4a68bd3b3e109a00f996f520296405ad6066b4f17c1e59da4077c2023c5fe0c95e770b9bd563a531748a16d79355f8db5fb2dcd66ac42673698ddcbea07704
|
|
7
|
+
data.tar.gz: 12713c07834164f3d13d01613126488cc19e937b8f127fcdbf839d8c75f2f6b77c6207e7e3e6f515d996e35aedb6d6e7333ba563a3f4578f4c33f454d41c6088
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
Changes since `v5.3.0`.
|
|
6
|
+
|
|
7
|
+
## v5.3.0
|
|
8
|
+
|
|
5
9
|
Changes since `v5.2.1`.
|
|
6
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
|
+
|
|
7
29
|
## v5.2.1
|
|
8
30
|
|
|
9
31
|
Changes since `v5.2.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.
|
|
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
|
|
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`, `
|
|
228
|
-
and `on_compaction_finish`, which
|
|
229
|
-
|
|
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
|
-
[](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
|
@@ -489,7 +489,11 @@ module LLM
|
|
|
489
489
|
|
|
490
490
|
def transform(prompt, params)
|
|
491
491
|
return [prompt, params] unless transformer
|
|
492
|
+
stream = params[:stream]
|
|
493
|
+
stream.on_transform(self, transformer) if LLM::Stream === stream
|
|
492
494
|
transformer.call(self, prompt, params)
|
|
495
|
+
ensure
|
|
496
|
+
stream.on_transform_finish(self, transformer) if LLM::Stream === stream
|
|
493
497
|
end
|
|
494
498
|
|
|
495
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
|
data/lib/llm/tool/param.rb
CHANGED
|
@@ -11,7 +11,8 @@ class LLM::Tool
|
|
|
11
11
|
# class Greeter < LLM::Tool
|
|
12
12
|
# name "greeter"
|
|
13
13
|
# description "Greets the user"
|
|
14
|
-
#
|
|
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