llm.rb 16.0.0.pre.3 → 16.0.0.pre.5
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 +75 -0
- data/README.md +7 -1
- data/docs/deepdive/advanced/context.md +7 -4
- data/docs/deepdive/features/console.md +3 -3
- data/docs/deepdive/features/database.md +29 -23
- data/docs/deepdive/features/embeddings.md +2 -0
- data/docs/deepdive/features/files.md +52 -0
- data/docs/deepdive/features/moderations.md +48 -0
- data/docs/deepdive/features/responses.md +54 -0
- data/docs/deepdive/fundamentals/agents.md +11 -7
- data/docs/deepdive/fundamentals/providers.md +37 -0
- data/docs/deepdive/fundamentals/schema.md +50 -0
- data/docs/deepdive/fundamentals/stream.md +16 -6
- data/docs/deepdive/fundamentals/tools.md +6 -0
- data/docs/deepdive/reference/object.md +6 -0
- data/docs/deepdive/reference/response.md +55 -0
- data/docs/deepdive.md +4 -0
- data/lib/llm/active_record/acts_as_llm.rb +24 -7
- data/lib/llm/agent.rb +87 -8
- data/lib/llm/context/deserializer.rb +24 -1
- data/lib/llm/context.rb +1 -0
- data/lib/llm/message.rb +7 -0
- data/lib/llm/prompt.rb +19 -8
- data/lib/llm/sequel/plugin.rb +19 -6
- data/lib/llm/step.rb +59 -0
- data/lib/llm/stream.rb +36 -4
- data/lib/llm/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5558a0514d6bd6b9648f743a308ce8e6dbb3802b8709edc6caf45d7c1ab3de8
|
|
4
|
+
data.tar.gz: a5d3178f0c79e152b8032cce974be69edd1e52cc629658034a750183ccb41ea8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a0288112d140d3e0a3e34dc35e570ceb25ad62fd7f3c33a2942cb111432992b7194e18fb406bf55a25a945aff0e89ce055c292a1de50c00b66c801027b95c52
|
|
7
|
+
data.tar.gz: 454a1c74a59401689c88947714f741ada1d39f6ce95013bf8bdd6fab76690adafd3a4be8af02e9eace2a71ae26c3f4daf3a1751fdf326549f722da888cbcb309
|
data/CHANGELOG.md
CHANGED
|
@@ -46,6 +46,39 @@
|
|
|
46
46
|
[`LLM::Message`](https://r.uby.dev/api-docs/llm.rb/LLM/Message.html) objects.
|
|
47
47
|
Every other format, and Sequel, still loads the runtime.
|
|
48
48
|
|
|
49
|
+
### ActiveRecord
|
|
50
|
+
|
|
51
|
+
* **activerecord: add `messages!` for the runtime's own messages** <br>
|
|
52
|
+
[`LLM::ActiveRecord#messages!`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord.html#messages!-instance_method)
|
|
53
|
+
returns the messages the runtime holds whatever the storage format is, so a
|
|
54
|
+
jsonb record can still reach them now that its `#messages` answers with the
|
|
55
|
+
view over the column. It reads what the context holds, including state that
|
|
56
|
+
has not been saved, where `#messages` reads the column. The Sequel plugin
|
|
57
|
+
answers to `#messages!` too, where it is the same call as `#messages`.
|
|
58
|
+
|
|
59
|
+
### Agent
|
|
60
|
+
|
|
61
|
+
* **agent: keep the instructions it injected in step** <br>
|
|
62
|
+
[`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html) now brings
|
|
63
|
+
the instructions it injected up to date before a request goes out, so a
|
|
64
|
+
conversation restored from saved state runs on the agent's current
|
|
65
|
+
instructions rather than on the ones it was saved with. The message is
|
|
66
|
+
found by a mark the agent puts on it, so a message the caller composed is
|
|
67
|
+
never replaced, and the mark is written by
|
|
68
|
+
[`LLM::Message#to_h`](https://r.uby.dev/api-docs/llm.rb/LLM/Message.html#to_h-instance_method)
|
|
69
|
+
and read back on a restore, which is the conversation the refresh exists
|
|
70
|
+
for. Before this, the instructions were injected once and the message
|
|
71
|
+
stayed as it was for the life of the conversation.
|
|
72
|
+
|
|
73
|
+
* **agent: identify its own instructions by the mark, not by the role** <br>
|
|
74
|
+
Fix a bug where the agent decided whether it had already injected its
|
|
75
|
+
instructions by looking for a message with the `system` role. Google gives
|
|
76
|
+
instructions the `user` role, so the agent did not recognise the message it
|
|
77
|
+
had already injected and added another copy of the instructions on every
|
|
78
|
+
turn. It now asks the provider which role instructions are given, and
|
|
79
|
+
identifies the message it wrote by its mark, because on Google the message
|
|
80
|
+
a caller seeds a conversation with is a user message too.
|
|
81
|
+
|
|
49
82
|
### Console
|
|
50
83
|
|
|
51
84
|
* **console: erase with the backspace key on OpenBSD** <br>
|
|
@@ -115,6 +148,35 @@
|
|
|
115
148
|
`respond_to?` with false for an instance method. A hook that raises becomes
|
|
116
149
|
what the caller sees in place of the call's result.
|
|
117
150
|
|
|
151
|
+
### ORM
|
|
152
|
+
|
|
153
|
+
* **orm: save a conversation after each request, not once per turn** <br>
|
|
154
|
+
[`LLM::Step`](https://r.uby.dev/api-docs/llm.rb/LLM/Step.html) is prepended
|
|
155
|
+
onto every stream by
|
|
156
|
+
[`LLM::Stream.try`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#try-class_method),
|
|
157
|
+
so a conversation bound to a record is written down as it goes and a turn
|
|
158
|
+
that is interrupted can be continued rather than started over. It saves
|
|
159
|
+
through
|
|
160
|
+
[`LLM::Context#record`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#record-instance_method),
|
|
161
|
+
an ActiveRecord model through `LLM::ActiveRecord::Utils.save!` and a Sequel
|
|
162
|
+
model through `LLM::Sequel::Plugin::Utils.save!`, and the write is best
|
|
163
|
+
effort, so a failure is swallowed rather than raised inside the request that
|
|
164
|
+
has just completed. Before this, `talk` and `ask` on both wrappers saved
|
|
165
|
+
once, when the whole turn had finished, so an interrupted turn persisted
|
|
166
|
+
nothing.
|
|
167
|
+
|
|
168
|
+
### Prompt
|
|
169
|
+
|
|
170
|
+
* **prompt: let a message carry fields a caller attaches to it** <br>
|
|
171
|
+
[`LLM::Prompt#system`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html#system-instance_method),
|
|
172
|
+
[`#user`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html#user-instance_method),
|
|
173
|
+
[`#developer`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html#developer-instance_method),
|
|
174
|
+
and [`#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html#talk-instance_method),
|
|
175
|
+
along with its `#chat` alias, now take an `extra:` keyword, so a message a
|
|
176
|
+
caller composes can carry fields of its own. The runtime reads those fields
|
|
177
|
+
back rather than inferring provenance from a role, which is how an agent
|
|
178
|
+
tells the instructions it injected from a message the caller wrote.
|
|
179
|
+
|
|
118
180
|
### Provider
|
|
119
181
|
|
|
120
182
|
* **deepseek: support image attachments in chat completions** <br>
|
|
@@ -155,6 +217,19 @@
|
|
|
155
217
|
discarded a false default, and `required: false` marks the parameter optional
|
|
156
218
|
rather than being ignored.
|
|
157
219
|
|
|
220
|
+
### Stream
|
|
221
|
+
|
|
222
|
+
* **stream: add `on_step`, called when a request completes** <br>
|
|
223
|
+
[`LLM::Stream#on_step`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_step-instance_method)
|
|
224
|
+
is a new callback, emitted from
|
|
225
|
+
[`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
|
|
226
|
+
once the prompt and the response are in the conversation. It marks the
|
|
227
|
+
boundary between one request and the next, which is the point a stream can
|
|
228
|
+
checkpoint the conversation at, because a provider will accept it again
|
|
229
|
+
there. It fires once per successful request, so a request retried after a
|
|
230
|
+
rate limit or a timeout calls it when it lands and not once per attempt, and
|
|
231
|
+
it fires even when the stream is disabled.
|
|
232
|
+
|
|
158
233
|
## v15.5.0
|
|
159
234
|
|
|
160
235
|
Changes since `v15.4.1`.
|
data/README.md
CHANGED
|
@@ -78,7 +78,8 @@ agent.talk "hello world"
|
|
|
78
78
|
Streams can be simple IO objects or subclasses of
|
|
79
79
|
[`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
|
|
80
80
|
with structured callbacks for content,
|
|
81
|
-
reasoning, tool calls, tool returns,
|
|
81
|
+
reasoning, tool calls, tool returns, the boundary a completed
|
|
82
|
+
request marks, and compaction.
|
|
82
83
|
Streams can also observe message transformers, which rewrite
|
|
83
84
|
outgoing messages before they reach the provider.
|
|
84
85
|
|
|
@@ -102,6 +103,11 @@ class MyStream < LLM::Stream
|
|
|
102
103
|
def on_tool_return(tool, result)
|
|
103
104
|
end
|
|
104
105
|
|
|
106
|
+
# A request has completed: the response is in the conversation, and
|
|
107
|
+
# any tools it asked for run after this.
|
|
108
|
+
def on_step(ctx, res)
|
|
109
|
+
end
|
|
110
|
+
|
|
105
111
|
# Before a transformer rewrites an outgoing message.
|
|
106
112
|
def on_transform(transformer)
|
|
107
113
|
end
|
|
@@ -81,9 +81,10 @@ Pass `retry_budget:` to retry a request that was rate limited
|
|
|
81
81
|
Each retry sleeps a growing interval (2s, 4s, 6s, ...) and notifies
|
|
82
82
|
the stream through
|
|
83
83
|
[`LLM::Stream#on_retry`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_retry-instance_method)
|
|
84
|
-
before trying again.
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
before trying again. The budget lives on the provider, where it
|
|
85
|
+
defaults to 5 (8 on Alibaba, which rate limits more often), and an
|
|
86
|
+
agent that sets none of its own takes the provider's, so most users
|
|
87
|
+
never touch this directly.
|
|
87
88
|
|
|
88
89
|
### Identity
|
|
89
90
|
|
|
@@ -423,7 +424,9 @@ The block receives the prompt object when it takes an argument, and
|
|
|
423
424
|
otherwise runs in the prompt's context:
|
|
424
425
|
[`LLM::Prompt#to_a`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html#to_a)
|
|
425
426
|
returns the messages in order, and two prompts are equal when their
|
|
426
|
-
messages match.
|
|
427
|
+
messages match. Each of `system`, `user`, `developer`, and `talk`
|
|
428
|
+
accepts an `extra:` hash, which is attached to the message it builds,
|
|
429
|
+
so a caller can carry a field of its own alongside the content.
|
|
427
430
|
|
|
428
431
|
#### Why would I use it?
|
|
429
432
|
|
|
@@ -66,9 +66,9 @@ should use the `console` names.
|
|
|
66
66
|
When `bin/llm.rb` starts, it looks for an `AGENTS.md` file in the
|
|
67
67
|
current working directory. If one exists, its contents become the
|
|
68
68
|
agent's instructions for the session, so a project can describe how it
|
|
69
|
-
wants the agent to work without passing flags. The
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
wants the agent to work without passing flags. The agent brings its
|
|
70
|
+
instructions up to date before each request, so a resumed session runs
|
|
71
|
+
on the current file rather than the one it was saved with.
|
|
72
72
|
|
|
73
73
|
The user-message label is exposed through
|
|
74
74
|
[`LLM::Console#sender`](https://r.uby.dev/api-docs/llm.rb/LLM/Console.html#sender-instance_method),
|
|
@@ -34,13 +34,23 @@ and
|
|
|
34
34
|
for serialization and
|
|
35
35
|
[`LLM::Context#restore`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#restore)
|
|
36
36
|
for deserialization. Save writes the current state, restore loads
|
|
37
|
-
it back and picks up where the conversation left off.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[`LLM::
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
it back and picks up where the conversation left off.
|
|
38
|
+
|
|
39
|
+
The ORM wrappers write on a different boundary than the file does, and
|
|
40
|
+
the two are worth telling apart.
|
|
41
|
+
[`LLM::Step`](https://r.uby.dev/api-docs/llm.rb/LLM/Step.html) is
|
|
42
|
+
prepended onto the stream, and it saves the conversation through the
|
|
43
|
+
record the context is bound to each time
|
|
44
|
+
[`LLM::Stream#on_step`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_step-instance_method)
|
|
45
|
+
fires - after a response is in the conversation and before the tools it
|
|
46
|
+
asked for run. A turn that runs tools before it answers is therefore
|
|
47
|
+
saved more than once, and an application that writes it down can
|
|
48
|
+
continue an interrupted turn instead of redoing it.
|
|
49
|
+
|
|
50
|
+
A `path:` has no record behind it to write to, so that one saves once
|
|
51
|
+
per turn. The difference is what each is for: the column is a
|
|
52
|
+
checkpoint after every completed request, and the file is a session at
|
|
53
|
+
the end of one.
|
|
44
54
|
|
|
45
55
|
#### Why would I use it?
|
|
46
56
|
|
|
@@ -242,12 +252,10 @@ first call, a fresh agent is created and the conversation starts
|
|
|
242
252
|
from scratch.
|
|
243
253
|
|
|
244
254
|
On subsequent calls, the stored state is restored and the
|
|
245
|
-
conversation continues.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
call persists the
|
|
250
|
-
updated state back to the column automatically.
|
|
255
|
+
conversation continues. The column is written after each completed
|
|
256
|
+
request rather than at the end of a turn, so a turn that runs tools
|
|
257
|
+
before it answers is saved more than once - and a turn that is
|
|
258
|
+
interrupted can be resumed from its last completed request.
|
|
251
259
|
|
|
252
260
|
The `data_column:` option lets you use a different column name.
|
|
253
261
|
The `format:` option controls the storage type. Use `:string` for
|
|
@@ -327,7 +335,7 @@ agent.talk "perform research"
|
|
|
327
335
|
#### Why would I use it?
|
|
328
336
|
|
|
329
337
|
The ActiveRecord wrapper integrates with your existing models.
|
|
330
|
-
Agent state is automatically persisted after each
|
|
338
|
+
Agent state is automatically persisted after each completed request,
|
|
331
339
|
using the same `create!`, `save!`, and query methods you already
|
|
332
340
|
use.
|
|
333
341
|
|
|
@@ -419,11 +427,12 @@ records rather than
|
|
|
419
427
|
[`LLM::Message`](https://r.uby.dev/api-docs/llm.rb/LLM/Message.html)
|
|
420
428
|
objects, until
|
|
421
429
|
[`#unwrap!`](https://r.uby.dev/api-docs/llm.rb/LLM/ActiveRecord/Message.html#unwrap!-instance_method)
|
|
422
|
-
turns them back.
|
|
430
|
+
turns them back. `#messages!` is the runtime's own list, whatever the
|
|
431
|
+
format is, so a jsonb record reaches it in one call.
|
|
423
432
|
|
|
424
|
-
Every other format
|
|
425
|
-
|
|
426
|
-
|
|
433
|
+
Every other format answers `#messages` with the runtime's messages, as
|
|
434
|
+
it always did, and that is the default. Sequel has no equivalent to any
|
|
435
|
+
of this: its plugin persists the same state, and there is no view to
|
|
427
436
|
read it back in the database.
|
|
428
437
|
|
|
429
438
|
### Sequel
|
|
@@ -446,11 +455,8 @@ the plugin in the model class. The `data` column stores
|
|
|
446
455
|
the full agent state as JSON, same structure as ActiveRecord.
|
|
447
456
|
On first call, a fresh agent is created. On subsequent calls,
|
|
448
457
|
the stored state is restored and the conversation continues.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
or
|
|
452
|
-
[`LLM::Agent#ask`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#ask)
|
|
453
|
-
call persists the updated state.
|
|
458
|
+
The state is written after each completed request, as it is under
|
|
459
|
+
ActiveRecord.
|
|
454
460
|
|
|
455
461
|
The `data_column:` and `format:` options work identically to
|
|
456
462
|
ActiveRecord. For `:jsonb`, Sequel loads the `pg_json` extension
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
## Files
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
Some providers keep files you upload, and let a later request refer to
|
|
9
|
+
one by id instead of resending its bytes. The Files API is how a file
|
|
10
|
+
is uploaded, listed, fetched, downloaded, and deleted, and a file id is
|
|
11
|
+
then passed to an endpoint that reads it.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
Call the file methods on a provider that implements them. `create`
|
|
16
|
+
uploads a file and returns its record, `get` reads a record, `download`
|
|
17
|
+
returns the bytes, `all` lists the files, and `delete` removes one:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "llm"
|
|
21
|
+
|
|
22
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
23
|
+
ctx = LLM::Context.new(llm)
|
|
24
|
+
|
|
25
|
+
file = llm.files.create(file: "/books/goodread.pdf")
|
|
26
|
+
ctx.talk ["Tell me about this PDF", file]
|
|
27
|
+
|
|
28
|
+
llm.files.all # every uploaded file
|
|
29
|
+
llm.files.get(file: file.id) # one file's record
|
|
30
|
+
llm.files.download(file: file.id) # its bytes, under #file
|
|
31
|
+
llm.files.delete(file: file.id)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`create` takes a path, an `LLM::File`, or a `File` object, and a
|
|
35
|
+
`purpose:` that tells the provider what the file is for. An uploaded
|
|
36
|
+
file carries its id, so it can be passed to a turn directly, as above,
|
|
37
|
+
and a response from `download` exposes the bytes through `#file`.
|
|
38
|
+
|
|
39
|
+
#### Why would I use it?
|
|
40
|
+
|
|
41
|
+
A file that a provider stores can be reused across requests without
|
|
42
|
+
being uploaded each time. That keeps a large document or an image out
|
|
43
|
+
of the request body, and it is how OpenAI's vector stores, and the
|
|
44
|
+
assistants that read them, get their content.
|
|
45
|
+
|
|
46
|
+
#### Notes
|
|
47
|
+
|
|
48
|
+
OpenAI, Anthropic, and Google implement the Files API; a provider that
|
|
49
|
+
does not raises `NotImplementedError`. `download` is OpenAI-only. A
|
|
50
|
+
file id belongs to the provider that minted it: DeepSeek, for example,
|
|
51
|
+
has no Files API and raises `LLM::PromptError` when it is handed a file
|
|
52
|
+
id from another provider.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
## Moderations
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
A moderation classifies text or an image against a set of harm
|
|
9
|
+
categories, so an application can decide what to do with it before
|
|
10
|
+
showing it, storing it, or sending it to a model. OpenAI hosts the
|
|
11
|
+
moderation endpoint.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
Call `llm.moderations` with the content to check. The response lists a
|
|
16
|
+
moderation per input, and each one reports whether it was flagged, the
|
|
17
|
+
categories it matched, and their scores:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "llm"
|
|
21
|
+
|
|
22
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
23
|
+
|
|
24
|
+
res = llm.moderations.create(input: "I hate you")
|
|
25
|
+
mod = res.moderations[0]
|
|
26
|
+
|
|
27
|
+
mod.flagged? # => true
|
|
28
|
+
mod.categories # the categories that matched
|
|
29
|
+
mod.scores # a score per category
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`input` accepts a string or a URL, so an image URL is classified the
|
|
33
|
+
same way text is.
|
|
34
|
+
|
|
35
|
+
#### Why would I use it?
|
|
36
|
+
|
|
37
|
+
A moderation is a cheap check that runs before an expensive or public
|
|
38
|
+
step. Screen user-generated content before publishing it, gate a
|
|
39
|
+
request before it reaches a model, or keep a record of what was
|
|
40
|
+
filtered and why.
|
|
41
|
+
|
|
42
|
+
#### Notes
|
|
43
|
+
|
|
44
|
+
Only OpenAI implements moderations; another provider raises
|
|
45
|
+
`NotImplementedError`. The default model is `omni-moderation-latest`,
|
|
46
|
+
and it classifies both text and images. The scores are model
|
|
47
|
+
estimates, so a threshold is a product decision rather than a fixed
|
|
48
|
+
value.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
## Responses
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
OpenAI's Responses API gives every reply an id, and a later call can
|
|
9
|
+
continue from one by passing `previous_response_id:`. When the provider
|
|
10
|
+
is asked to store the reply, it keeps the history, so a client can hand
|
|
11
|
+
the thread back instead of resending it.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
Create a response with `llm.responses.create`, then continue it by
|
|
16
|
+
passing `previous_response_id:` the id of the last one. A response can
|
|
17
|
+
be read back with `get` and removed with `delete`:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "llm"
|
|
21
|
+
|
|
22
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
23
|
+
|
|
24
|
+
first = llm.responses.create "Your task is to answer the user's questions",
|
|
25
|
+
role: :developer
|
|
26
|
+
second = llm.responses.create "5 + 5 = X ?",
|
|
27
|
+
role: :user,
|
|
28
|
+
previous_response_id: first.id
|
|
29
|
+
|
|
30
|
+
puts second.content
|
|
31
|
+
|
|
32
|
+
llm.responses.get(second) # read it back
|
|
33
|
+
llm.responses.delete(second) # remove it
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`create` accepts the same prompt shapes as a completion (a string or a
|
|
37
|
+
list), plus `role:`, `model:`, `tools:`, and a `stream:` target. It
|
|
38
|
+
returns an `LLM::Response` like any other call.
|
|
39
|
+
|
|
40
|
+
#### Why would I use it?
|
|
41
|
+
|
|
42
|
+
Server-side state means the conversation lives with the provider
|
|
43
|
+
instead of in the request. Passing one id continues a thread without
|
|
44
|
+
rebuilding and resending the messages, which keeps requests small and
|
|
45
|
+
lets a client that lost its local history pick the thread back up.
|
|
46
|
+
|
|
47
|
+
#### Notes
|
|
48
|
+
|
|
49
|
+
Only OpenAI implements the Responses API. A context in `mode:
|
|
50
|
+
:responses` uses this endpoint and defaults to `store: false`, so the
|
|
51
|
+
conversation is not kept on the provider and the context rebuilds the
|
|
52
|
+
input itself; set `store: true` to keep it there and have the next turn
|
|
53
|
+
continue with `previous_response_id`. A plain `llm.responses.create`
|
|
54
|
+
call continues only when `previous_response_id:` is given.
|
|
@@ -21,8 +21,9 @@ the model responds, and if it requests tools the agent
|
|
|
21
21
|
executes them automatically and feeds the results back. The tool
|
|
22
22
|
loop can be bounded with
|
|
23
23
|
[`LLM::Agent.tool_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#tool_budget-class_method)
|
|
24
|
-
(see the Tool budget section). Instructions are injected once
|
|
25
|
-
|
|
24
|
+
(see the Tool budget section). Instructions are injected once and
|
|
25
|
+
brought up to date from the agent before each request, unless the
|
|
26
|
+
prompt already carries a system message of the caller's own.
|
|
26
27
|
|
|
27
28
|
#### Why would I use it?
|
|
28
29
|
|
|
@@ -245,9 +246,12 @@ of hanging.
|
|
|
245
246
|
|
|
246
247
|
The retry budget applies to rate-limited requests
|
|
247
248
|
(`LLM::RateLimitError`) and timeouts (`Timeout::Error`, covering
|
|
248
|
-
`Net::OpenTimeout` and `Net::ReadTimeout`)
|
|
249
|
-
retried. The budget
|
|
249
|
+
`Net::OpenTimeout` and `Net::ReadTimeout`); other errors are never
|
|
250
|
+
retried. The budget lives on the provider, where it defaults to five
|
|
251
|
+
(eight on Alibaba, which rate limits more often), and an agent that
|
|
252
|
+
sets none of its own takes the provider's. A raw
|
|
250
253
|
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
|
|
251
|
-
defaults to zero (`retry_budget: 0`)
|
|
252
|
-
content streams, so retrying the same
|
|
253
|
-
`retry_budget: 0` to disable retries on
|
|
254
|
+
defaults to zero (`retry_budget: 0`), so retries are opt-in there. A
|
|
255
|
+
429 is refused before any content streams, so retrying the same
|
|
256
|
+
request loses nothing. Pass `retry_budget: 0` to disable retries on
|
|
257
|
+
an agent.
|
|
@@ -92,6 +92,43 @@ missing model or registry raises `LLM::NoSuchModelError` or
|
|
|
92
92
|
`LLM::NoSuchRegistryError`, which the runtime rescues to default
|
|
93
93
|
gracefully (for example, an unknown context window reads as `nil`).
|
|
94
94
|
|
|
95
|
+
### Model listings
|
|
96
|
+
|
|
97
|
+
#### Overview
|
|
98
|
+
|
|
99
|
+
The registry is the catalog llm.rb ships. A provider can also list the
|
|
100
|
+
models it serves right now, which shows a model added since your copy
|
|
101
|
+
of `data/` was written, or one available to your account alone.
|
|
102
|
+
|
|
103
|
+
#### How it works
|
|
104
|
+
|
|
105
|
+
A provider with a models endpoint exposes it through `llm.models`, and
|
|
106
|
+
`all` returns the list:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
require "llm"
|
|
110
|
+
|
|
111
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
112
|
+
llm.models.all.each { puts _1.id }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
OpenAI, Anthropic, Google, Bedrock, and Ollama implement it. Bedrock
|
|
116
|
+
lists the foundation models in the configured region and accepts
|
|
117
|
+
`byProvider:` to narrow the list; Ollama lists the models pulled into
|
|
118
|
+
the local daemon.
|
|
119
|
+
|
|
120
|
+
#### Why would I use it?
|
|
121
|
+
|
|
122
|
+
A shipped catalog goes stale between releases. Asking the provider
|
|
123
|
+
keeps a model picker current and reveals models the catalog does not
|
|
124
|
+
know about.
|
|
125
|
+
|
|
126
|
+
#### Notes
|
|
127
|
+
|
|
128
|
+
Not every provider implements a models endpoint; calling `models` on
|
|
129
|
+
one that does not raises `NotImplementedError`. Unlike the registry,
|
|
130
|
+
the call needs credentials.
|
|
131
|
+
|
|
95
132
|
### Request headers
|
|
96
133
|
|
|
97
134
|
#### Overview
|
|
@@ -178,3 +178,53 @@ DSL builds, so the result is used the same way.
|
|
|
178
178
|
An unsupported `type`, an unresolvable `$ref`, or a `$ref` that does
|
|
179
179
|
not start with `#/` raises `TypeError`. When a schema omits `type`,
|
|
180
180
|
the parser infers it from `const`, `enum`, or `default`.
|
|
181
|
+
|
|
182
|
+
### Deferred types
|
|
183
|
+
|
|
184
|
+
#### Overview
|
|
185
|
+
|
|
186
|
+
A property or parameter type can be given as a block, in which case the
|
|
187
|
+
type is worked out when the schema is serialized rather than when the
|
|
188
|
+
class that declares it is defined. That is how a type that only exists
|
|
189
|
+
at runtime, such as the values a store holds right now, is described.
|
|
190
|
+
|
|
191
|
+
#### How it works
|
|
192
|
+
|
|
193
|
+
Pass a block in place of the type. It is called each time the schema is
|
|
194
|
+
serialized, and whatever it returns is resolved in turn, so it can
|
|
195
|
+
return a leaf, a class, or an array of either:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
class Article < LLM::Schema
|
|
199
|
+
property :kind, proc { LLM::Schema::Enum[*Article.kinds] }, "The article kind"
|
|
200
|
+
required %i[kind]
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The same works for a tool parameter:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
class Publish < LLM::Tool
|
|
208
|
+
name "publish"
|
|
209
|
+
parameter :kind, proc { LLM::Schema::Enum[*Article.kinds] }, "The article kind"
|
|
210
|
+
required %i[kind]
|
|
211
|
+
|
|
212
|
+
def call(kind:)
|
|
213
|
+
Article.publish(kind:)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Why would I use it?
|
|
219
|
+
|
|
220
|
+
A schema is a class-level definition, but the choices it offers are
|
|
221
|
+
sometimes only known when a request is made. A deferred type lets the
|
|
222
|
+
schema be declared once and still send the values that are current at
|
|
223
|
+
the time of the request.
|
|
224
|
+
|
|
225
|
+
#### Notes
|
|
226
|
+
|
|
227
|
+
The block is called on every serialization, so it runs once per
|
|
228
|
+
request and sees the latest values. Everything a leaf accepts, such as
|
|
229
|
+
`required`, `default`, and `enum`, is applied to the resolved leaf
|
|
230
|
+
after the block returns.
|
|
@@ -67,6 +67,17 @@ receives tokens as they arrive.
|
|
|
67
67
|
fires when the model requests a tool.
|
|
68
68
|
[`LLM::Stream#on_tool_return`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_tool_return)
|
|
69
69
|
fires when the tool completes.
|
|
70
|
+
[`LLM::Stream#on_step`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_step-instance_method)
|
|
71
|
+
fires when a request completes, which is the boundary between one
|
|
72
|
+
request and the next: the response is in the conversation, and a
|
|
73
|
+
turn that asked for tools runs them after this while a turn that is
|
|
74
|
+
finished ends here. That boundary is where a conversation is whole,
|
|
75
|
+
and the runtime's
|
|
76
|
+
[`LLM::Step`](https://r.uby.dev/api-docs/llm.rb/LLM/Step.html) is
|
|
77
|
+
written against it - it is prepended onto a stream, and saves the
|
|
78
|
+
conversation through the record the context is bound to, so a turn
|
|
79
|
+
that is interrupted can be continued from its last completed request
|
|
80
|
+
rather than started over.
|
|
70
81
|
[`LLM::Stream#on_retry`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_retry)
|
|
71
82
|
fires each time a failed request is retried. Compaction hooks
|
|
72
83
|
let you show progress or log what was trimmed. Skill hooks bracket a
|
|
@@ -96,6 +107,11 @@ class MyStream < LLM::Stream
|
|
|
96
107
|
def on_tool_return(tool, result)
|
|
97
108
|
end
|
|
98
109
|
|
|
110
|
+
# A request has completed: the response is in the conversation, and
|
|
111
|
+
# any tools it asked for run after this.
|
|
112
|
+
def on_step(ctx, res)
|
|
113
|
+
end
|
|
114
|
+
|
|
99
115
|
# Before a transformer rewrites an outgoing message.
|
|
100
116
|
def on_transform(transformer)
|
|
101
117
|
end
|
|
@@ -135,9 +151,3 @@ agent.talk "Explain Ruby fibers."
|
|
|
135
151
|
A stream subclass gives you visibility into more than just content chunks. React to tool
|
|
136
152
|
calls as they happen, show compaction progress, or integrate with
|
|
137
153
|
an existing observability stack.
|
|
138
|
-
|
|
139
|
-
#### Notes
|
|
140
|
-
|
|
141
|
-
The IO-like form is equivalent to
|
|
142
|
-
[`LLM::Stream#on_content`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_content)
|
|
143
|
-
and does not include the other hooks.
|
|
@@ -435,6 +435,12 @@ needs most. See the
|
|
|
435
435
|
The tools that spawn subprocesses use the optional `test-cmd.rb`
|
|
436
436
|
gem for process management and interrupt handling.
|
|
437
437
|
|
|
438
|
+
`LLM::Tool.subclasses` lists the direct subclasses of
|
|
439
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html), so a
|
|
440
|
+
tool defined under an intermediate base class is not in the list.
|
|
441
|
+
[`LLM::Tool.registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#registry-class_method)
|
|
442
|
+
lists every tool that has a name, wherever it sits in the hierarchy.
|
|
443
|
+
|
|
438
444
|
### Interrupts
|
|
439
445
|
|
|
440
446
|
#### Overview
|
|
@@ -61,6 +61,12 @@ is defined explicitly and serializes through the configured JSON
|
|
|
61
61
|
adapter via
|
|
62
62
|
[`LLM.json.dump`](https://r.uby.dev/api-docs/llm.rb/LLM.html#json-class_method).
|
|
63
63
|
|
|
64
|
+
The adapter is `LLM::JSONAdapter::JSON`, the standard library, by
|
|
65
|
+
default. Set `LLM.json = :oj` or `LLM.json = :yajl` to use a faster
|
|
66
|
+
backend when the matching gem is present; every adapter normalizes
|
|
67
|
+
strings to valid UTF-8 before it serializes, so the choice affects
|
|
68
|
+
speed rather than behavior.
|
|
69
|
+
|
|
64
70
|
### Reading and writing
|
|
65
71
|
|
|
66
72
|
#### Overview
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
## LLM::Response
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
Every call returns an
|
|
9
|
+
[`LLM::Response`](https://r.uby.dev/api-docs/llm.rb/LLM/Response.html).
|
|
10
|
+
It is one normalized shape that a provider, an endpoint, or a context
|
|
11
|
+
extends with the accessors that response needs, so chat completions,
|
|
12
|
+
embeddings, images, and OCR all share a common surface without losing
|
|
13
|
+
their own.
|
|
14
|
+
|
|
15
|
+
#### How it works
|
|
16
|
+
|
|
17
|
+
`LLM::Response` carries the transport response and a little common
|
|
18
|
+
state: `#res` (the underlying HTTP response), `#body` (the parsed body,
|
|
19
|
+
or a raw string), `#ok?`, `#id`, and `#file?`. A name the base class
|
|
20
|
+
does not define falls through to the body, so a provider field such as
|
|
21
|
+
`content` is reachable directly.
|
|
22
|
+
|
|
23
|
+
A completion response also includes
|
|
24
|
+
[`LLM::Contract::Completion`](https://r.uby.dev/api-docs/llm.rb/LLM/Contract/Completion.html),
|
|
25
|
+
which adds the pieces a chat reply needs:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
res = agent.talk "Weather in Paris?", schema: Weather
|
|
29
|
+
|
|
30
|
+
res.content # the raw text, or the structured reply
|
|
31
|
+
res.content! # the reply parsed as an LLM::Object
|
|
32
|
+
res.messages # the messages this turn produced
|
|
33
|
+
res.usage # an LLM::Usage (input, output, reasoning, cache, ...)
|
|
34
|
+
res.reasoning_content # the model's reasoning, when the provider exposes it
|
|
35
|
+
res.model # the model that answered
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### Why would I use it?
|
|
39
|
+
|
|
40
|
+
One shape means one set of methods to learn: the same `content!`,
|
|
41
|
+
`usage`, and `messages` work whatever the provider. When an endpoint
|
|
42
|
+
returns something shaped differently, it adds an accessor rather than
|
|
43
|
+
changing the contract.
|
|
44
|
+
|
|
45
|
+
#### Notes
|
|
46
|
+
|
|
47
|
+
`#usage` returns an
|
|
48
|
+
[`LLM::Usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Usage.html), so
|
|
49
|
+
token counts are read the same way everywhere; the individual token
|
|
50
|
+
readers (`input_tokens`, `output_tokens`, `cache_read_tokens`, and so
|
|
51
|
+
on) live on the completion contract. Endpoint-specific accessors such
|
|
52
|
+
as `images`, `pages`, `audio`, or `embeddings` come from the adapter
|
|
53
|
+
that extends the response, so they are present only on responses that
|
|
54
|
+
carry that data. `#content!` parses the content as JSON, which is the
|
|
55
|
+
read to use with a schema.
|
data/docs/deepdive.md
CHANGED
|
@@ -68,6 +68,9 @@ is the best place to start if you are new to llm.rb.
|
|
|
68
68
|
- [Built-in tools](deepdive/features/builtin_tools.md)
|
|
69
69
|
- [Concurrency](deepdive/features/concurrency.md)
|
|
70
70
|
- [Embeddings](deepdive/features/embeddings.md)
|
|
71
|
+
- [Files](deepdive/features/files.md)
|
|
72
|
+
- [Moderations](deepdive/features/moderations.md)
|
|
73
|
+
- [Responses](deepdive/features/responses.md)
|
|
71
74
|
- [Database](deepdive/features/database.md)
|
|
72
75
|
- [Console](deepdive/features/console.md)
|
|
73
76
|
|
|
@@ -88,6 +91,7 @@ is the best place to start if you are new to llm.rb.
|
|
|
88
91
|
|
|
89
92
|
## Reference
|
|
90
93
|
|
|
94
|
+
- [LLM::Response](deepdive/reference/response.md)
|
|
91
95
|
- [LLM::Object](deepdive/reference/object.md)
|
|
92
96
|
- [LLM::Cost](deepdive/reference/cost.md)
|
|
93
97
|
- [Tracer](deepdive/reference/tracer.md)
|
|
@@ -56,21 +56,23 @@ module LLM::ActiveRecord
|
|
|
56
56
|
|
|
57
57
|
module InstanceMethods
|
|
58
58
|
##
|
|
59
|
-
# Continues the stored context with new input
|
|
59
|
+
# Continues the stored context with new input.
|
|
60
|
+
#
|
|
61
|
+
# The conversation is saved by {LLM::Step LLM::Step} at each request.
|
|
60
62
|
# @see LLM::Context#talk
|
|
61
63
|
# @return [LLM::Response]
|
|
62
64
|
def talk(...)
|
|
63
|
-
|
|
64
|
-
ctx.talk(...).tap { Utils.save!(self, ctx, options) }
|
|
65
|
+
ctx.talk(...)
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
##
|
|
68
|
-
# Continues the stored context with new input
|
|
69
|
+
# Continues the stored context with new input.
|
|
70
|
+
#
|
|
71
|
+
# The conversation is saved by {LLM::Step LLM::Step} at each request.
|
|
69
72
|
# @see LLM::Context#ask
|
|
70
73
|
# @return [LLM::Response]
|
|
71
74
|
def ask(...)
|
|
72
|
-
|
|
73
|
-
ctx.ask(...).tap { Utils.save!(self, ctx, options) }
|
|
75
|
+
ctx.ask(...)
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
##
|
|
@@ -101,7 +103,8 @@ module LLM::ActiveRecord
|
|
|
101
103
|
# unordered - conversation order is `position`, not `id` - and its rows
|
|
102
104
|
# are {LLM::ActiveRecord::Message} records until
|
|
103
105
|
# {LLM::ActiveRecord::Message#unwrap!} turns them back into
|
|
104
|
-
# {LLM::Message} objects.
|
|
106
|
+
# {LLM::Message} objects. {#messages!} is the runtime's own collection,
|
|
107
|
+
# for a caller who wants that instead.
|
|
105
108
|
# @see LLM::Context#messages
|
|
106
109
|
# @return [ActiveRecord::Relation, LLM::Buffer]
|
|
107
110
|
def messages
|
|
@@ -113,6 +116,20 @@ module LLM::ActiveRecord
|
|
|
113
116
|
end
|
|
114
117
|
end
|
|
115
118
|
|
|
119
|
+
##
|
|
120
|
+
# Returns the messages the runtime holds, whatever the storage format is.
|
|
121
|
+
#
|
|
122
|
+
# This is what {#messages} answers with for every format but jsonb, and
|
|
123
|
+
# it is the way back to it for a jsonb record. The difference between
|
|
124
|
+
# the two is what each reads: this reads what the context holds,
|
|
125
|
+
# including state that has not been saved, where {#messages} reads the
|
|
126
|
+
# column.
|
|
127
|
+
# @see LLM::Context#messages
|
|
128
|
+
# @return [LLM::Buffer]
|
|
129
|
+
def messages!
|
|
130
|
+
ctx.messages
|
|
131
|
+
end
|
|
132
|
+
|
|
116
133
|
##
|
|
117
134
|
# @note The bang keeps the ActiveRecord and Sequel wrappers aligned.
|
|
118
135
|
# @see LLM::Context#model
|
data/lib/llm/agent.rb
CHANGED
|
@@ -15,7 +15,10 @@ module LLM
|
|
|
15
15
|
# instead of leaving tool loops to the caller.
|
|
16
16
|
#
|
|
17
17
|
# **Notes:**
|
|
18
|
-
# * Instructions are injected once
|
|
18
|
+
# * Instructions are injected once, and kept in step after that. A
|
|
19
|
+
# restored conversation carries the instructions that were current
|
|
20
|
+
# when it started, and the message is brought up to date before the
|
|
21
|
+
# next request goes out.
|
|
19
22
|
# * An agent automatically executes tool loops (unlike {LLM::Context LLM::Context}).
|
|
20
23
|
# * The tool loop can be bounded with `tool_budget`. Once the budget is
|
|
21
24
|
# spent, no further tool calls are run for that turn: the agent sends an
|
|
@@ -53,6 +56,14 @@ module LLM
|
|
|
53
56
|
UNDEFINED = Object.new
|
|
54
57
|
private_constant :UNDEFINED
|
|
55
58
|
|
|
59
|
+
##
|
|
60
|
+
# Marks the message an agent injects as its own, so that a later
|
|
61
|
+
# turn can tell it from a message the caller composed. The role
|
|
62
|
+
# cannot do that: it is true of whatever the caller put there.
|
|
63
|
+
# @api private
|
|
64
|
+
INSTRUCTIONS = {instructions: true}.freeze
|
|
65
|
+
private_constant :INSTRUCTIONS
|
|
66
|
+
|
|
56
67
|
##
|
|
57
68
|
# Ugh :)
|
|
58
69
|
# @api private
|
|
@@ -441,7 +452,10 @@ module LLM
|
|
|
441
452
|
# knows its own API, and how often it recovers from a
|
|
442
453
|
# rate limit rather than failing outright.
|
|
443
454
|
params[:retry_budget] = llm.retry_budget if params[:retry_budget].equal?(UNDEFINED)
|
|
444
|
-
|
|
455
|
+
##
|
|
456
|
+
# The context is bound to the same record the agent holds, so code
|
|
457
|
+
# written against a context alone can find it through `ctx.record`.
|
|
458
|
+
@ctx = LLM::Context.new(llm, params.merge(record: @record).compact)
|
|
445
459
|
@path and File.readable?(@path) ? @ctx.restore(path:) : nil
|
|
446
460
|
end
|
|
447
461
|
|
|
@@ -799,27 +813,92 @@ module LLM
|
|
|
799
813
|
# @return [LLM::Prompt]
|
|
800
814
|
def apply_instructions(new_prompt)
|
|
801
815
|
return new_prompt unless @instructions
|
|
816
|
+
##
|
|
817
|
+
# A prompt that carries a system message of its own is the
|
|
818
|
+
# caller's, and the instructions already in the conversation are
|
|
819
|
+
# left as they are. The context is deliberately not consulted: a
|
|
820
|
+
# restored context always holds the message this exists to bring
|
|
821
|
+
# up to date, which is what `inject_instructions?` tests for.
|
|
822
|
+
refresh_instructions! unless carries_system_message?(new_prompt)
|
|
802
823
|
if LLM::Prompt === new_prompt
|
|
803
|
-
new_prompt.system(@instructions) if inject_instructions?(new_prompt)
|
|
824
|
+
new_prompt.system(@instructions, extra: INSTRUCTIONS) if inject_instructions?(new_prompt)
|
|
804
825
|
new_prompt
|
|
805
826
|
else
|
|
806
827
|
prompt do
|
|
807
|
-
_1.system(@instructions) if inject_instructions?
|
|
828
|
+
_1.system(@instructions, extra: INSTRUCTIONS) if inject_instructions?
|
|
808
829
|
_1.user(new_prompt)
|
|
809
830
|
end
|
|
810
831
|
end
|
|
811
832
|
end
|
|
812
833
|
|
|
834
|
+
##
|
|
835
|
+
# Brings the instructions stored in the conversation up to date.
|
|
836
|
+
#
|
|
837
|
+
# The message is identified by the mark the agent put on it when it
|
|
838
|
+
# injected it, and not by its role or its position. A role is true
|
|
839
|
+
# of whatever the caller put there - on a provider whose system role
|
|
840
|
+
# is `:user`, the first message of a conversation the caller seeded
|
|
841
|
+
# is a user message too - so a rule written on either of those
|
|
842
|
+
# replaces a message that was never the agent's to replace.
|
|
843
|
+
#
|
|
844
|
+
# A message with no mark is left alone, which is the safe direction:
|
|
845
|
+
# a conversation written before the mark existed keeps its stale
|
|
846
|
+
# instructions rather than losing a message. Stale is recoverable.
|
|
847
|
+
#
|
|
848
|
+
# An agent with no instructions returns first. The caller reaches
|
|
849
|
+
# this through `apply_instructions`, which already returns early for
|
|
850
|
+
# the same reason, and the guard is repeated here so that the method
|
|
851
|
+
# cannot replace a message with nil however it is reached.
|
|
852
|
+
#
|
|
853
|
+
# Comparing content rather than the message is deliberate.
|
|
854
|
+
# {LLM::Message#==} compares everything a message carries apart from
|
|
855
|
+
# its id, which drags fields that have nothing to do with
|
|
856
|
+
# instructions into the comparison. The mark is carried onto the
|
|
857
|
+
# replacement so that the turn after this one can still find it, and
|
|
858
|
+
# so is the role the message already had: it is the provider's, and
|
|
859
|
+
# rebuilding it here would undo the reason the replacement is safe.
|
|
860
|
+
# @api private
|
|
861
|
+
# @return [void]
|
|
862
|
+
def refresh_instructions!
|
|
863
|
+
return unless @instructions
|
|
864
|
+
message = @ctx.messages.first
|
|
865
|
+
return unless message&.extra&.key?(:instructions)
|
|
866
|
+
return if message.content == @instructions
|
|
867
|
+
@ctx.messages.replace(
|
|
868
|
+
[LLM::Message.new(message.role, @instructions, message.extra), *@ctx.messages.drop(1)]
|
|
869
|
+
)
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
##
|
|
873
|
+
# Returns true when a prompt carries a system message of its own.
|
|
874
|
+
#
|
|
875
|
+
# This is the half of `inject_instructions?` that asks about the
|
|
876
|
+
# prompt rather than about the context, and it is the one a refresh
|
|
877
|
+
# needs. The context half says "do not inject again", which is not
|
|
878
|
+
# the same question as "whose instructions are these".
|
|
879
|
+
#
|
|
880
|
+
# The role asked about is the provider's: a prompt's `system` is
|
|
881
|
+
# built with the role the provider gives instructions, which is not
|
|
882
|
+
# the string "system" everywhere.
|
|
883
|
+
# @param [LLM::Prompt, Object] prompt
|
|
884
|
+
# @return [Boolean]
|
|
885
|
+
def carries_system_message?(prompt)
|
|
886
|
+
role = @llm.system_role.to_s
|
|
887
|
+
LLM::Prompt === prompt && prompt.to_a.any? { _1.role.to_s == role }
|
|
888
|
+
end
|
|
889
|
+
|
|
813
890
|
##
|
|
814
891
|
# Returns true when agent instructions should be injected for the turn.
|
|
815
|
-
# Instructions are injected once unless a
|
|
816
|
-
# present in the existing context or the prompt
|
|
892
|
+
# Instructions are injected once unless a message with the provider's
|
|
893
|
+
# system role is already present in the existing context or the prompt
|
|
894
|
+
# being sent.
|
|
817
895
|
# @param [LLM::Prompt, nil] prompt
|
|
818
896
|
# @return [Boolean]
|
|
819
897
|
def inject_instructions?(prompt = nil)
|
|
820
|
-
|
|
898
|
+
role = @llm.system_role.to_s
|
|
899
|
+
return false if @ctx.messages.any? { _1.role.to_s == role }
|
|
821
900
|
return true if prompt.nil?
|
|
822
|
-
!prompt.to_a.any?
|
|
901
|
+
!prompt.to_a.any? { _1.role.to_s == role }
|
|
823
902
|
end
|
|
824
903
|
|
|
825
904
|
##
|
|
@@ -40,6 +40,19 @@ class LLM::Context
|
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
+
##
|
|
44
|
+
# Rebuilds one message from a payload.
|
|
45
|
+
#
|
|
46
|
+
# The fields that describe a message are named here rather than
|
|
47
|
+
# taken from everything the payload holds, so a field that
|
|
48
|
+
# {LLM::Message#to_h} writes has to be read here as well - and one
|
|
49
|
+
# that is not named is dropped on the way in, whatever the payload
|
|
50
|
+
# carries. `instructions` marks the message an agent injected as its
|
|
51
|
+
# own, and the agent looks for that mark rather than for a role or a
|
|
52
|
+
# position when it refreshes the instructions in a restored
|
|
53
|
+
# conversation.
|
|
54
|
+
# @param [Hash] payload
|
|
55
|
+
# @return [LLM::Message]
|
|
43
56
|
def deserialize_message(payload)
|
|
44
57
|
tool_calls = deserialize_tool_calls(payload["tools"])
|
|
45
58
|
returns = deserialize_returns(payload["content"]) if returns.nil?
|
|
@@ -47,8 +60,18 @@ class LLM::Context
|
|
|
47
60
|
usage = payload["usage"]
|
|
48
61
|
reasoning_content = payload["reasoning_content"]
|
|
49
62
|
compaction = payload["compaction"]
|
|
63
|
+
instructions = payload["instructions"]
|
|
50
64
|
id = payload["id"]
|
|
51
|
-
extra = {
|
|
65
|
+
extra = {
|
|
66
|
+
tool_calls:,
|
|
67
|
+
original_tool_calls:,
|
|
68
|
+
tools: @params[:tools],
|
|
69
|
+
usage:,
|
|
70
|
+
reasoning_content:,
|
|
71
|
+
compaction:,
|
|
72
|
+
instructions:,
|
|
73
|
+
id:
|
|
74
|
+
}.compact
|
|
52
75
|
content = returns.nil? ? deserialize_content(payload["content"]) : returns
|
|
53
76
|
LLM::Message.new(payload["role"], content, extra)
|
|
54
77
|
end
|
data/lib/llm/context.rb
CHANGED
data/lib/llm/message.rb
CHANGED
|
@@ -46,6 +46,12 @@ module LLM
|
|
|
46
46
|
|
|
47
47
|
##
|
|
48
48
|
# Returns a Hash representation of the message.
|
|
49
|
+
#
|
|
50
|
+
# This is the whole of a message as far as storage is concerned:
|
|
51
|
+
# {LLM::Context#to_h} maps its messages through here, and restore
|
|
52
|
+
# reads them back from the same list. A field that is not named
|
|
53
|
+
# here does not survive a save, so a field the runtime relies on
|
|
54
|
+
# after a restore has to be named.
|
|
49
55
|
# @return [Hash]
|
|
50
56
|
def to_h
|
|
51
57
|
{
|
|
@@ -54,6 +60,7 @@ module LLM
|
|
|
54
60
|
content:,
|
|
55
61
|
reasoning_content:,
|
|
56
62
|
compaction: extra.compaction,
|
|
63
|
+
instructions: extra.instructions,
|
|
57
64
|
tools: extra.tool_calls&.map { LLM::Object === _1 ? _1.to_h : _1 },
|
|
58
65
|
usage:,
|
|
59
66
|
original_tool_calls: extra.original_tool_calls
|
data/lib/llm/prompt.rb
CHANGED
|
@@ -40,40 +40,51 @@ class LLM::Prompt
|
|
|
40
40
|
# The message
|
|
41
41
|
# @param [Symbol] role
|
|
42
42
|
# The role (eg user, system)
|
|
43
|
+
# @param [Hash, nil] extra
|
|
44
|
+
# Fields carried alongside the message. A caller that composes a
|
|
45
|
+
# message of its own can mark it here, and the runtime reads the
|
|
46
|
+
# mark back rather than inferring it from the role.
|
|
43
47
|
# @return [void]
|
|
44
|
-
def talk(content, role: @provider.user_role)
|
|
48
|
+
def talk(content, role: @provider.user_role, extra: nil)
|
|
45
49
|
role = case role.to_sym
|
|
46
50
|
when :system then @provider.system_role
|
|
47
51
|
when :user then @provider.user_role
|
|
48
52
|
when :developer then @provider.developer_role
|
|
49
53
|
else role
|
|
50
54
|
end
|
|
51
|
-
|
|
55
|
+
message = extra ? LLM::Message.new(role, content, extra) : LLM::Message.new(role, content)
|
|
56
|
+
@buffer << message
|
|
52
57
|
end
|
|
53
58
|
alias_method :chat, :talk
|
|
54
59
|
|
|
55
60
|
##
|
|
56
61
|
# @param [String] content
|
|
57
62
|
# The message content
|
|
63
|
+
# @param [Hash, nil] extra
|
|
64
|
+
# Fields carried alongside the message
|
|
58
65
|
# @return [void]
|
|
59
|
-
def user(content)
|
|
60
|
-
talk(content, role: @provider.user_role)
|
|
66
|
+
def user(content, extra: nil)
|
|
67
|
+
talk(content, role: @provider.user_role, extra:)
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
##
|
|
64
71
|
# @param [String] content
|
|
65
72
|
# The message content
|
|
73
|
+
# @param [Hash, nil] extra
|
|
74
|
+
# Fields carried alongside the message
|
|
66
75
|
# @return [void]
|
|
67
|
-
def system(content)
|
|
68
|
-
talk(content, role: @provider.system_role)
|
|
76
|
+
def system(content, extra: nil)
|
|
77
|
+
talk(content, role: @provider.system_role, extra:)
|
|
69
78
|
end
|
|
70
79
|
|
|
71
80
|
##
|
|
72
81
|
# @param [String] content
|
|
73
82
|
# The message content
|
|
83
|
+
# @param [Hash, nil] extra
|
|
84
|
+
# Fields carried alongside the message
|
|
74
85
|
# @return [void]
|
|
75
|
-
def developer(content)
|
|
76
|
-
talk(content, role: @provider.developer_role)
|
|
86
|
+
def developer(content, extra: nil)
|
|
87
|
+
talk(content, role: @provider.developer_role, extra:)
|
|
77
88
|
end
|
|
78
89
|
|
|
79
90
|
##
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -155,21 +155,23 @@ module LLM::Sequel
|
|
|
155
155
|
Utils = Plugin::Utils
|
|
156
156
|
|
|
157
157
|
##
|
|
158
|
-
# Continues the stored context with new input
|
|
158
|
+
# Continues the stored context with new input.
|
|
159
|
+
#
|
|
160
|
+
# The conversation is saved by {LLM::Step LLM::Step} at each request.
|
|
159
161
|
# @see LLM::Context#talk
|
|
160
162
|
# @return [LLM::Response]
|
|
161
163
|
def talk(...)
|
|
162
|
-
|
|
163
|
-
ctx.talk(...).tap { Utils.save!(self, ctx, options) }
|
|
164
|
+
ctx.talk(...)
|
|
164
165
|
end
|
|
165
166
|
|
|
166
167
|
##
|
|
167
|
-
# Continues the stored context with new input
|
|
168
|
+
# Continues the stored context with new input.
|
|
169
|
+
#
|
|
170
|
+
# The conversation is saved by {LLM::Step LLM::Step} at each request.
|
|
168
171
|
# @see LLM::Context#ask
|
|
169
172
|
# @return [LLM::Response]
|
|
170
173
|
def ask(...)
|
|
171
|
-
|
|
172
|
-
ctx.ask(...).tap { Utils.save!(self, ctx, options) }
|
|
174
|
+
ctx.ask(...)
|
|
173
175
|
end
|
|
174
176
|
|
|
175
177
|
##
|
|
@@ -199,6 +201,17 @@ module LLM::Sequel
|
|
|
199
201
|
ctx.messages
|
|
200
202
|
end
|
|
201
203
|
|
|
204
|
+
##
|
|
205
|
+
# @note The same call {#messages} makes here, and here for the sake of the
|
|
206
|
+
# name: on {LLM::ActiveRecord} the two differ for a jsonb record, and a
|
|
207
|
+
# wrapper that answered to a name the other one does not is the next
|
|
208
|
+
# reader's surprise.
|
|
209
|
+
# @see LLM::Context#messages
|
|
210
|
+
# @return [LLM::Buffer]
|
|
211
|
+
def messages!
|
|
212
|
+
ctx.messages
|
|
213
|
+
end
|
|
214
|
+
|
|
202
215
|
##
|
|
203
216
|
# @note The bang is used because Sequel reserves `model` for the
|
|
204
217
|
# underlying model class on instances.
|
data/lib/llm/step.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# Writes the conversation down when a request completes.
|
|
6
|
+
#
|
|
7
|
+
# {LLM::Step LLM::Step} is prepended onto a stream's singleton class by
|
|
8
|
+
# {LLM::Stream.try}, so a conversation bound to a record is saved as it goes:
|
|
9
|
+
# a turn that is interrupted - by a crash, by a deploy, or by a provider that
|
|
10
|
+
# refuses - can be continued rather than started over.
|
|
11
|
+
#
|
|
12
|
+
# It saves through the record the context is bound to ({LLM::Context#record}),
|
|
13
|
+
# and it knows both the ORMs llm.rb speaks: an ActiveRecord model is saved
|
|
14
|
+
# with {LLM::ActiveRecord::Utils.save!} and a Sequel model with
|
|
15
|
+
# {LLM::Sequel::Plugin::Utils.save!}. A context with no record - which is
|
|
16
|
+
# every context that is not backed by a model - has nothing to save, and
|
|
17
|
+
# neither has a record that carries neither plugin, so the step is passed
|
|
18
|
+
# along untouched.
|
|
19
|
+
#
|
|
20
|
+
# All of this is one method on purpose. The module is put onto an object the
|
|
21
|
+
# caller owns, and a helper would be one more name on that object for as long
|
|
22
|
+
# as it lives.
|
|
23
|
+
#
|
|
24
|
+
# The write is best effort: a failure is swallowed rather than raised, because
|
|
25
|
+
# this runs inside the request that has just completed, and a stale checkpoint
|
|
26
|
+
# is a smaller failure than a turn that ends because the database was busy.
|
|
27
|
+
# Only the write is covered, so a stream's own `on_step` is reached whether or
|
|
28
|
+
# not the write lands - a step that a tracer hears about does not depend on a
|
|
29
|
+
# database having been free.
|
|
30
|
+
#
|
|
31
|
+
# @see LLM::Stream#on_step
|
|
32
|
+
# @see LLM::ActiveRecord::Utils.save!
|
|
33
|
+
module Step
|
|
34
|
+
##
|
|
35
|
+
# Saves the conversation the context holds, then passes the step along.
|
|
36
|
+
#
|
|
37
|
+
# @param [LLM::Context] ctx
|
|
38
|
+
# The context the request belongs to
|
|
39
|
+
# @param [LLM::Response] res
|
|
40
|
+
# The response for the request that completed
|
|
41
|
+
# @return [nil]
|
|
42
|
+
def on_step(ctx, res)
|
|
43
|
+
record = ctx.record
|
|
44
|
+
if record and record.class.respond_to?(:llm_plugin_options)
|
|
45
|
+
options = record.class.llm_plugin_options
|
|
46
|
+
begin
|
|
47
|
+
if defined?(::ActiveRecord::Base) and ::ActiveRecord::Base === record
|
|
48
|
+
LLM::ActiveRecord::Utils.save!(record, ctx, options)
|
|
49
|
+
elsif defined?(::Sequel::Model) and ::Sequel::Model === record
|
|
50
|
+
LLM::Sequel::Plugin::Utils.save!(record, ctx, options)
|
|
51
|
+
end
|
|
52
|
+
rescue
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/llm/stream.rb
CHANGED
|
@@ -37,10 +37,12 @@ module LLM
|
|
|
37
37
|
#
|
|
38
38
|
# @see LLM::Agent Where streams are typically attached
|
|
39
39
|
# @see LLM::Context Where streams are bound per-turn
|
|
40
|
+
# @see LLM::Step An extension that saves the conversation at each step
|
|
40
41
|
class Stream
|
|
41
42
|
require_relative "stream/queue"
|
|
42
43
|
require_relative "stream/io"
|
|
43
44
|
require_relative "stream/disabled"
|
|
45
|
+
require_relative "step"
|
|
44
46
|
|
|
45
47
|
##
|
|
46
48
|
# This method will try to convert its argument into
|
|
@@ -51,18 +53,22 @@ module LLM
|
|
|
51
53
|
# objects, IO objects who implement `#<<`, true, false,
|
|
52
54
|
# and nil. Anything else raises a TypeError.
|
|
53
55
|
#
|
|
56
|
+
# The stream it answers with is ready to be told about a step:
|
|
57
|
+
# {LLM::Step LLM::Step} is prepended onto it here, once, rather than by
|
|
58
|
+
# every caller that means to save a conversation.
|
|
59
|
+
#
|
|
54
60
|
# @raise [TypeError]
|
|
55
61
|
# @param [LLM::Stream, #<<, Boolean, NilClass] obj
|
|
56
62
|
# @return [LLM::Stream]
|
|
57
63
|
def self.try(obj, extra: {})
|
|
58
64
|
if LLM::Stream === obj
|
|
59
|
-
obj.tap { _1.extra.merge!(extra) }
|
|
65
|
+
obj.tap { _1.extra.merge!(extra) }.tap { _1.singleton_class.prepend(LLM::Step) }
|
|
60
66
|
elsif obj.respond_to?(:<<)
|
|
61
|
-
LLM::Stream::IO.new(obj).tap { _1.extra.merge!(extra) }
|
|
67
|
+
LLM::Stream::IO.new(obj).tap { _1.extra.merge!(extra) }.tap { _1.singleton_class.prepend(LLM::Step) }
|
|
62
68
|
elsif obj == true
|
|
63
|
-
LLM::Stream.new.tap { _1.extra.merge!(extra) }
|
|
69
|
+
LLM::Stream.new.tap { _1.extra.merge!(extra) }.tap { _1.singleton_class.prepend(LLM::Step) }
|
|
64
70
|
elsif obj.nil? || obj == false
|
|
65
|
-
LLM::Stream::Disabled.new.tap { _1.extra.merge!(extra) }
|
|
71
|
+
LLM::Stream::Disabled.new.tap { _1.extra.merge!(extra) }.tap { _1.singleton_class.prepend(LLM::Step) }
|
|
66
72
|
else
|
|
67
73
|
raise TypeError, "invalid stream object"
|
|
68
74
|
end
|
|
@@ -152,6 +158,32 @@ module LLM
|
|
|
152
158
|
nil
|
|
153
159
|
end
|
|
154
160
|
|
|
161
|
+
##
|
|
162
|
+
# Called when a request in a turn has completed.
|
|
163
|
+
#
|
|
164
|
+
# This is the boundary between one request and the next: the response
|
|
165
|
+
# is in the context's message history, and a turn that asked for tools
|
|
166
|
+
# runs them after this while a turn that is finished ends here.
|
|
167
|
+
#
|
|
168
|
+
# It is emitted once per successful request, so a request that is
|
|
169
|
+
# retried after a rate limit or a timeout calls this when it lands and
|
|
170
|
+
# not once per attempt. It is also emitted when the stream is disabled:
|
|
171
|
+
# a disabled stream has no output to report, but it is still told that
|
|
172
|
+
# a step ended.
|
|
173
|
+
#
|
|
174
|
+
# The context is passed with the response so that a stream does not have
|
|
175
|
+
# to reach for it: {LLM::Step LLM::Step}, which saves the conversation the
|
|
176
|
+
# context holds, is written against this callback alone.
|
|
177
|
+
#
|
|
178
|
+
# @param [LLM::Context] ctx
|
|
179
|
+
# The context the request belongs to
|
|
180
|
+
# @param [LLM::Response] res
|
|
181
|
+
# The response for the request that completed
|
|
182
|
+
# @return [nil]
|
|
183
|
+
def on_step(ctx, res)
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
155
187
|
##
|
|
156
188
|
# Called before a context transformer rewrites a prompt.
|
|
157
189
|
# @param [LLM::Transformer] transformer
|
data/lib/llm/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.0.0.pre.
|
|
4
|
+
version: 16.0.0.pre.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robert Gleeson
|
|
@@ -400,6 +400,9 @@ files:
|
|
|
400
400
|
- docs/deepdive/features/console.md
|
|
401
401
|
- docs/deepdive/features/database.md
|
|
402
402
|
- docs/deepdive/features/embeddings.md
|
|
403
|
+
- docs/deepdive/features/files.md
|
|
404
|
+
- docs/deepdive/features/moderations.md
|
|
405
|
+
- docs/deepdive/features/responses.md
|
|
403
406
|
- docs/deepdive/fundamentals/agents.md
|
|
404
407
|
- docs/deepdive/fundamentals/providers.md
|
|
405
408
|
- docs/deepdive/fundamentals/schema.md
|
|
@@ -414,6 +417,7 @@ files:
|
|
|
414
417
|
- docs/deepdive/reference/cost.md
|
|
415
418
|
- docs/deepdive/reference/model_registry.md
|
|
416
419
|
- docs/deepdive/reference/object.md
|
|
420
|
+
- docs/deepdive/reference/response.md
|
|
417
421
|
- docs/deepdive/reference/tracer.md
|
|
418
422
|
- lib/llm.rb
|
|
419
423
|
- lib/llm/a2a.rb
|
|
@@ -642,6 +646,7 @@ files:
|
|
|
642
646
|
- lib/llm/server_tool.rb
|
|
643
647
|
- lib/llm/session.rb
|
|
644
648
|
- lib/llm/skill.rb
|
|
649
|
+
- lib/llm/step.rb
|
|
645
650
|
- lib/llm/stream.rb
|
|
646
651
|
- lib/llm/stream/disabled.rb
|
|
647
652
|
- lib/llm/stream/io.rb
|