llm.rb 16.0.0.pre.4 → 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 +35 -0
- data/README.md +7 -1
- data/docs/deepdive/advanced/context.md +3 -1
- data/docs/deepdive/features/console.md +3 -3
- data/docs/deepdive/features/database.md +24 -19
- data/docs/deepdive/fundamentals/agents.md +3 -2
- data/docs/deepdive/fundamentals/stream.md +16 -6
- data/lib/llm/agent.rb +83 -7
- data/lib/llm/context/deserializer.rb +24 -1
- data/lib/llm/message.rb +7 -0
- data/lib/llm/prompt.rb +19 -8
- 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: 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
|
@@ -56,6 +56,29 @@
|
|
|
56
56
|
has not been saved, where `#messages` reads the column. The Sequel plugin
|
|
57
57
|
answers to `#messages!` too, where it is the same call as `#messages`.
|
|
58
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
|
+
|
|
59
82
|
### Console
|
|
60
83
|
|
|
61
84
|
* **console: erase with the backspace key on OpenBSD** <br>
|
|
@@ -142,6 +165,18 @@
|
|
|
142
165
|
once, when the whole turn had finished, so an interrupted turn persisted
|
|
143
166
|
nothing.
|
|
144
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
|
+
|
|
145
180
|
### Provider
|
|
146
181
|
|
|
147
182
|
* **deepseek: support image attachments in chat completions** <br>
|
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
|
|
@@ -424,7 +424,9 @@ The block receives the prompt object when it takes an argument, and
|
|
|
424
424
|
otherwise runs in the prompt's context:
|
|
425
425
|
[`LLM::Prompt#to_a`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html#to_a)
|
|
426
426
|
returns the messages in order, and two prompts are equal when their
|
|
427
|
-
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.
|
|
428
430
|
|
|
429
431
|
#### Why would I use it?
|
|
430
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
|
|
|
@@ -447,11 +455,8 @@ the plugin in the model class. The `data` column stores
|
|
|
447
455
|
the full agent state as JSON, same structure as ActiveRecord.
|
|
448
456
|
On first call, a fresh agent is created. On subsequent calls,
|
|
449
457
|
the stored state is restored and the conversation continues.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
or
|
|
453
|
-
[`LLM::Agent#ask`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#ask)
|
|
454
|
-
call persists the updated state.
|
|
458
|
+
The state is written after each completed request, as it is under
|
|
459
|
+
ActiveRecord.
|
|
455
460
|
|
|
456
461
|
The `data_column:` and `format:` options work identically to
|
|
457
462
|
ActiveRecord. For `:jsonb`, Sequel loads the `pg_json` extension
|
|
@@ -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
|
|
|
@@ -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.
|
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
|
|
@@ -802,27 +813,92 @@ module LLM
|
|
|
802
813
|
# @return [LLM::Prompt]
|
|
803
814
|
def apply_instructions(new_prompt)
|
|
804
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)
|
|
805
823
|
if LLM::Prompt === new_prompt
|
|
806
|
-
new_prompt.system(@instructions) if inject_instructions?(new_prompt)
|
|
824
|
+
new_prompt.system(@instructions, extra: INSTRUCTIONS) if inject_instructions?(new_prompt)
|
|
807
825
|
new_prompt
|
|
808
826
|
else
|
|
809
827
|
prompt do
|
|
810
|
-
_1.system(@instructions) if inject_instructions?
|
|
828
|
+
_1.system(@instructions, extra: INSTRUCTIONS) if inject_instructions?
|
|
811
829
|
_1.user(new_prompt)
|
|
812
830
|
end
|
|
813
831
|
end
|
|
814
832
|
end
|
|
815
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
|
+
|
|
816
890
|
##
|
|
817
891
|
# Returns true when agent instructions should be injected for the turn.
|
|
818
|
-
# Instructions are injected once unless a
|
|
819
|
-
# 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.
|
|
820
895
|
# @param [LLM::Prompt, nil] prompt
|
|
821
896
|
# @return [Boolean]
|
|
822
897
|
def inject_instructions?(prompt = nil)
|
|
823
|
-
|
|
898
|
+
role = @llm.system_role.to_s
|
|
899
|
+
return false if @ctx.messages.any? { _1.role.to_s == role }
|
|
824
900
|
return true if prompt.nil?
|
|
825
|
-
!prompt.to_a.any?
|
|
901
|
+
!prompt.to_a.any? { _1.role.to_s == role }
|
|
826
902
|
end
|
|
827
903
|
|
|
828
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/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/version.rb
CHANGED