llm.rb 12.5.1 → 13.0.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 +482 -0
- data/LICENSE +21 -93
- data/README.md +49 -159
- data/data/deepinfra.json +3 -0
- data/data/xai.json +1 -1
- data/lib/llm/a2a.rb +1 -1
- data/lib/llm/active_record/acts_as_agent.rb +32 -0
- data/lib/llm/active_record/acts_as_llm.rb +6 -6
- data/lib/llm/agent.rb +101 -26
- data/lib/llm/buffer.rb +85 -3
- data/lib/llm/compactor/null.rb +19 -0
- data/lib/llm/compactor/truncate.rb +80 -0
- data/lib/llm/compactor.rb +42 -124
- data/lib/llm/context.rb +33 -37
- data/lib/llm/contract.rb +4 -25
- data/lib/llm/function/array.rb +15 -14
- data/lib/llm/function/async/group.rb +54 -0
- data/lib/llm/function/async/reactor.rb +48 -0
- data/lib/llm/function/async/task.rb +83 -0
- data/lib/llm/function/fiber/group.rb +46 -0
- data/lib/llm/function/fiber/task.rb +62 -0
- data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
- data/lib/llm/function/fork/job.rb +4 -3
- data/lib/llm/function/fork/task.rb +20 -10
- data/lib/llm/function/group.rb +40 -0
- data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
- data/lib/llm/function/ractor/job.rb +19 -3
- data/lib/llm/function/ractor/mailbox.rb +9 -0
- data/lib/llm/function/ractor/task.rb +24 -15
- data/lib/llm/function/{call_group.rb → sequential/group.rb} +17 -8
- data/lib/llm/function/sequential/task.rb +49 -0
- data/lib/llm/function/task.rb +25 -37
- data/lib/llm/function/thread/group.rb +46 -0
- data/lib/llm/function/thread/task.rb +60 -0
- data/lib/llm/function/tracing.rb +2 -0
- data/lib/llm/function.rb +56 -64
- data/lib/llm/loop_guard.rb +1 -2
- data/lib/llm/mcp.rb +22 -0
- data/lib/llm/object.rb +2 -1
- data/lib/llm/provider.rb +6 -3
- data/lib/llm/providers/google.rb +2 -2
- data/lib/llm/repl/command.rb +35 -8
- data/lib/llm/repl/commands/compact.rb +33 -0
- data/lib/llm/repl/input.rb +80 -15
- data/lib/llm/repl/markdown/table.rb +76 -0
- data/lib/llm/repl/markdown.rb +31 -1
- data/lib/llm/repl/status.rb +1 -1
- data/lib/llm/repl/stream.rb +10 -3
- data/lib/llm/repl/transcript.rb +1 -1
- data/lib/llm/repl/walker.rb +46 -0
- data/lib/llm/repl.rb +18 -12
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +5 -0
- data/lib/llm/schema/object.rb +11 -5
- data/lib/llm/sequel/agent.rb +32 -0
- data/lib/llm/sequel/plugin.rb +6 -6
- data/lib/llm/stream.rb +24 -17
- data/lib/llm/tool/param.rb +12 -0
- data/lib/llm/tool.rb +20 -4
- data/lib/llm/tools/chdir.rb +0 -2
- data/lib/llm/tools/git.rb +8 -4
- data/lib/llm/tools/mkdir.rb +1 -1
- data/lib/llm/tools/pwd.rb +0 -2
- data/lib/llm/tools/read_file.rb +0 -2
- data/lib/llm/tools/rg.rb +8 -4
- data/lib/llm/tools/shell.rb +8 -4
- data/lib/llm/tools/utils.rb +31 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +25 -5
- data/llm.gemspec +3 -3
- data/resources/deepdive.md +693 -57
- metadata +24 -13
- data/lib/llm/function/call_task.rb +0 -46
- data/lib/llm/function/fiber_group.rb +0 -105
- data/lib/llm/function/task_group.rb +0 -97
- data/lib/llm/function/thread_group.rb +0 -102
data/lib/llm/repl/input.rb
CHANGED
|
@@ -12,16 +12,30 @@ class LLM::Repl
|
|
|
12
12
|
F: Curses::KEY_CTRL_F,
|
|
13
13
|
K: Curses::KEY_CTRL_K,
|
|
14
14
|
Y: Curses::KEY_CTRL_Y,
|
|
15
|
-
D: Curses::KEY_CTRL_D
|
|
15
|
+
D: Curses::KEY_CTRL_D,
|
|
16
|
+
P: Curses::KEY_CTRL_P,
|
|
17
|
+
N: Curses::KEY_CTRL_N
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
##
|
|
21
|
+
# This hash tracks how many times a given key
|
|
22
|
+
# was pressed repeatedly without being
|
|
23
|
+
# interrupted by another key. The previous key
|
|
24
|
+
# is reset to 0 when a different key is pressed.
|
|
25
|
+
REPEATS = {}
|
|
26
|
+
REPEATS.default = 0
|
|
27
|
+
|
|
18
28
|
UP = Curses::Key::UP
|
|
19
29
|
DOWN = Curses::Key::DOWN
|
|
20
30
|
LEFT = Curses::Key::LEFT
|
|
21
31
|
RIGHT = Curses::Key::RIGHT
|
|
32
|
+
PGUP = Curses::KEY_PPAGE
|
|
33
|
+
PGDOWN = Curses::KEY_NPAGE
|
|
34
|
+
|
|
35
|
+
TAB = 9
|
|
22
36
|
ESC = 27
|
|
23
|
-
ENTER =
|
|
24
|
-
BACKSPACE =
|
|
37
|
+
ENTER = 10
|
|
38
|
+
BACKSPACE = 127
|
|
25
39
|
|
|
26
40
|
##
|
|
27
41
|
# Threshold in seconds. If characters arrive faster than
|
|
@@ -35,21 +49,24 @@ class LLM::Repl
|
|
|
35
49
|
attr_reader :buffer
|
|
36
50
|
|
|
37
51
|
##
|
|
38
|
-
# @param [Boolean]
|
|
52
|
+
# @param [Boolean] value
|
|
39
53
|
# @return [void]
|
|
40
54
|
attr_writer :paste
|
|
41
55
|
|
|
42
56
|
##
|
|
43
|
-
# @param [LLM::
|
|
57
|
+
# @param [LLM::Repl] repl
|
|
44
58
|
# @return [LLM::Repl::Input]
|
|
45
|
-
def initialize(
|
|
46
|
-
@
|
|
47
|
-
@
|
|
59
|
+
def initialize(repl, options = {})
|
|
60
|
+
@name = repl.name
|
|
61
|
+
@agent = repl.agent
|
|
62
|
+
@provider = @agent.llm.name
|
|
48
63
|
@buffer = +""
|
|
49
64
|
@cursor = 0
|
|
50
65
|
@scroll = 0
|
|
51
66
|
@height = options.fetch(:height, 3)
|
|
52
67
|
@last_char_at = nil
|
|
68
|
+
@memory = @agent.messages.select(&:user?).map(&:content)
|
|
69
|
+
@walker = Walker.new(@memory)
|
|
53
70
|
@paste = false
|
|
54
71
|
end
|
|
55
72
|
|
|
@@ -59,8 +76,28 @@ class LLM::Repl
|
|
|
59
76
|
# @return [Symbol, nil]
|
|
60
77
|
def on_char(window, char, now)
|
|
61
78
|
is_paste = lambda { @last_char_at and (now - @last_char_at) < PASTE_THRESHOLD }
|
|
62
|
-
if
|
|
79
|
+
if char and @char != char
|
|
80
|
+
REPEATS[@char] = 0
|
|
81
|
+
end
|
|
82
|
+
if PGUP == char
|
|
83
|
+
(window.rows - 3).times { window.scroll_up }
|
|
84
|
+
:pageup
|
|
85
|
+
elsif PGDOWN == char
|
|
86
|
+
(window.rows - 3).times { window.scroll_down }
|
|
87
|
+
:pagedown
|
|
88
|
+
elsif TAB == char
|
|
89
|
+
autocomplete
|
|
90
|
+
:tab
|
|
91
|
+
elsif ESC == char
|
|
63
92
|
@agent.cancel!
|
|
93
|
+
elsif CTRL[:P] == char
|
|
94
|
+
@buffer = @walker.prev.dup
|
|
95
|
+
@cursor = @buffer.size
|
|
96
|
+
:ctrl_p
|
|
97
|
+
elsif CTRL[:N] == char
|
|
98
|
+
@buffer = @walker.next.dup
|
|
99
|
+
@cursor = @buffer.size
|
|
100
|
+
:ctrl_n
|
|
64
101
|
elsif CTRL[:D] == char
|
|
65
102
|
delete
|
|
66
103
|
:ctrl_d
|
|
@@ -85,14 +122,16 @@ class LLM::Repl
|
|
|
85
122
|
elsif char == RIGHT
|
|
86
123
|
move_right
|
|
87
124
|
:right
|
|
88
|
-
elsif BACKSPACE
|
|
125
|
+
elsif BACKSPACE == char
|
|
89
126
|
backspace
|
|
90
127
|
:backspace
|
|
91
|
-
elsif ENTER
|
|
128
|
+
elsif ENTER == char
|
|
92
129
|
if @paste = is_paste.()
|
|
93
130
|
insert("\n")
|
|
94
131
|
:char
|
|
95
132
|
else
|
|
133
|
+
@memory.push(@buffer.dup)
|
|
134
|
+
@walker.cursor = @memory.size
|
|
96
135
|
:submit
|
|
97
136
|
end
|
|
98
137
|
elsif char == UP
|
|
@@ -108,19 +147,23 @@ class LLM::Repl
|
|
|
108
147
|
nil
|
|
109
148
|
end
|
|
110
149
|
ensure
|
|
111
|
-
|
|
150
|
+
if char
|
|
151
|
+
REPEATS[char] += 1
|
|
152
|
+
@last_char_at = now
|
|
153
|
+
@char = char
|
|
154
|
+
end
|
|
112
155
|
end
|
|
113
156
|
|
|
114
157
|
##
|
|
115
158
|
# @return [String]
|
|
116
159
|
def to_s
|
|
117
|
-
"#{
|
|
160
|
+
"#{prompt}#{@buffer}"
|
|
118
161
|
end
|
|
119
162
|
|
|
120
163
|
##
|
|
121
164
|
# @return [Integer]
|
|
122
165
|
def cursor
|
|
123
|
-
prompt.
|
|
166
|
+
prompt.size + @cursor
|
|
124
167
|
end
|
|
125
168
|
|
|
126
169
|
##
|
|
@@ -182,6 +225,28 @@ class LLM::Repl
|
|
|
182
225
|
@cursor = [0, @cursor + 1].max
|
|
183
226
|
end
|
|
184
227
|
|
|
228
|
+
##
|
|
229
|
+
# @return [void]
|
|
230
|
+
def autocomplete
|
|
231
|
+
return unless @buffer[0] == "/"
|
|
232
|
+
##
|
|
233
|
+
# This method implements a simple autocomplete
|
|
234
|
+
# that supports cycling through all known
|
|
235
|
+
# commands. When given tab in quick succession,
|
|
236
|
+
# we cycle to the nearest neighbour for the last
|
|
237
|
+
# full match. However, it's not based on similarity,
|
|
238
|
+
# it's just the next element in the array.
|
|
239
|
+
keys = LLM::Command.registry.keys
|
|
240
|
+
candidates = LLM::Command.complete(@buffer)
|
|
241
|
+
if REPEATS[TAB] >= 1
|
|
242
|
+
candidate = keys[keys.index(candidates[0]) + 1] || keys[0]
|
|
243
|
+
else
|
|
244
|
+
candidate = candidates[0]
|
|
245
|
+
end
|
|
246
|
+
@buffer = "/#{candidate}"
|
|
247
|
+
@cursor = @buffer.size
|
|
248
|
+
end
|
|
249
|
+
|
|
185
250
|
##
|
|
186
251
|
# @return [void]
|
|
187
252
|
def kill
|
|
@@ -238,7 +303,7 @@ class LLM::Repl
|
|
|
238
303
|
end
|
|
239
304
|
|
|
240
305
|
def prompt
|
|
241
|
-
"#{@provider}> "
|
|
306
|
+
"#{@provider}(#{@name})> "
|
|
242
307
|
end
|
|
243
308
|
|
|
244
309
|
def insert(char)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl::Markdown
|
|
4
|
+
##
|
|
5
|
+
# Renders Kramdown `:table` nodes as aligned columns.
|
|
6
|
+
module Table
|
|
7
|
+
##
|
|
8
|
+
# Renders a table node by collecting all cells first to
|
|
9
|
+
# compute column widths, then emitting each row with
|
|
10
|
+
# padded text.
|
|
11
|
+
def walk_table(node, attrs)
|
|
12
|
+
rows = collect_rows(node, attrs)
|
|
13
|
+
return if rows.empty?
|
|
14
|
+
widths = column_widths(rows)
|
|
15
|
+
rows.each do |row|
|
|
16
|
+
emit("| ", attrs)
|
|
17
|
+
row.each_with_index do |chunks, i|
|
|
18
|
+
text = chunks.map { _1[:text] }.join
|
|
19
|
+
width = widths[i]
|
|
20
|
+
chunks.each { |c| emit(c[:text].ljust(width), c[:attrs]) }
|
|
21
|
+
emit(" | ", attrs) unless i == row.size - 1
|
|
22
|
+
end
|
|
23
|
+
emit(" |", attrs)
|
|
24
|
+
emit("\n", attrs)
|
|
25
|
+
end
|
|
26
|
+
emit("\n", attrs)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def collect_rows(node, attrs)
|
|
32
|
+
node.children.each_with_object([]) do |section, rows|
|
|
33
|
+
next unless [:thead, :tbody].include?(section.type)
|
|
34
|
+
section.children.each do |tr|
|
|
35
|
+
next unless tr.type == :tr
|
|
36
|
+
cells = tr.children.filter_map do |td|
|
|
37
|
+
next unless [:td, :th].include?(td.type)
|
|
38
|
+
collect_chunks(td, attrs)
|
|
39
|
+
end
|
|
40
|
+
rows << cells
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def collect_chunks(node, attrs)
|
|
46
|
+
[].tap do |chunks|
|
|
47
|
+
walk_collect(node, attrs, chunks)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def walk_collect(node, attrs, chunks)
|
|
52
|
+
case node.type
|
|
53
|
+
when :text
|
|
54
|
+
chunks << {text: node.value.to_s, attrs:}
|
|
55
|
+
when :strong
|
|
56
|
+
node.children.each { walk_collect(_1, Curses::A_BOLD, chunks) }
|
|
57
|
+
when :em
|
|
58
|
+
node.children.each { walk_collect(_1, Curses::A_UNDERLINE, chunks) }
|
|
59
|
+
when :codespan
|
|
60
|
+
chunks << {text: node.value, attrs: Curses::A_REVERSE}
|
|
61
|
+
when :a
|
|
62
|
+
node.children.each { walk_collect(_1, Curses::A_UNDERLINE, chunks) }
|
|
63
|
+
else
|
|
64
|
+
node.children.each { walk_collect(_1, attrs, chunks) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def column_widths(rows)
|
|
69
|
+
return [] if rows.empty?
|
|
70
|
+
cols = rows.first.size
|
|
71
|
+
(0...cols).map do |i|
|
|
72
|
+
rows.map { |r| r[i].map { _1[:text] }.join.length }.max
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/llm/repl/markdown.rb
CHANGED
|
@@ -8,11 +8,16 @@ class LLM::Repl
|
|
|
8
8
|
# of text, and also optional style information for
|
|
9
9
|
# that text (eg bold, underscore, ...)
|
|
10
10
|
class Markdown
|
|
11
|
+
require_relative "markdown/table"
|
|
12
|
+
include Table
|
|
13
|
+
|
|
11
14
|
##
|
|
12
15
|
# @param [String] text
|
|
16
|
+
# @param [Integer] width
|
|
13
17
|
# @return [LLM::Repl::Markdown]
|
|
14
|
-
def initialize(text)
|
|
18
|
+
def initialize(text, width)
|
|
15
19
|
@doc = Kramdown::Document.new(text)
|
|
20
|
+
@width = width
|
|
16
21
|
@ast = []
|
|
17
22
|
end
|
|
18
23
|
|
|
@@ -73,6 +78,31 @@ class LLM::Repl
|
|
|
73
78
|
emit("\n\n", attrs)
|
|
74
79
|
when :br
|
|
75
80
|
emit("\n", attrs)
|
|
81
|
+
when :ul, :ol
|
|
82
|
+
node.children.each { walk(_1, attrs) }
|
|
83
|
+
when :li
|
|
84
|
+
node.children.each { walk(_1, attrs) }
|
|
85
|
+
when :table
|
|
86
|
+
walk_table(node, attrs)
|
|
87
|
+
when :thead, :tbody
|
|
88
|
+
node.children.each { walk(_1, attrs) }
|
|
89
|
+
when :tr
|
|
90
|
+
emit("| ", attrs)
|
|
91
|
+
node.children.each { walk(_1, attrs) }
|
|
92
|
+
emit("\n", attrs)
|
|
93
|
+
when :td, :th
|
|
94
|
+
node.children.each { walk(_1, attrs) }
|
|
95
|
+
emit(" | ", attrs)
|
|
96
|
+
when :blockquote
|
|
97
|
+
emit("> ", attrs)
|
|
98
|
+
node.children.each { walk(_1, attrs) }
|
|
99
|
+
when :hr
|
|
100
|
+
emit("─" * @width, attrs)
|
|
101
|
+
emit("\n\n", attrs)
|
|
102
|
+
when :a
|
|
103
|
+
node.children.each { walk(_1, Curses::A_UNDERLINE) }
|
|
104
|
+
when :img
|
|
105
|
+
emit("[image: #{node.attr['alt']}]", attrs)
|
|
76
106
|
else
|
|
77
107
|
node.children.each { walk(_1, attrs) }
|
|
78
108
|
end
|
data/lib/llm/repl/status.rb
CHANGED
data/lib/llm/repl/stream.rb
CHANGED
|
@@ -60,9 +60,16 @@ class LLM::Repl
|
|
|
60
60
|
# @param [Integer] max
|
|
61
61
|
# @return [String]
|
|
62
62
|
def format_args(tool, max: 50)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
##
|
|
64
|
+
# 'tool.arguments' might be returned
|
|
65
|
+
# (by the model) in a different order
|
|
66
|
+
# than the tool definition - this code
|
|
67
|
+
# handles re-sorting.
|
|
68
|
+
args = tool.arguments
|
|
69
|
+
props = tool.params.properties.keys
|
|
70
|
+
props = props.sort_by { tool.params.properties[_1].index }
|
|
71
|
+
props = props.filter_map { args[_1] ? "#{_1}: #{format_value(args[_1])}" : nil }
|
|
72
|
+
result = props.join(", ")
|
|
66
73
|
result.size > max ? "#{result[0...max - 1]}…" : result
|
|
67
74
|
end
|
|
68
75
|
|
data/lib/llm/repl/transcript.rb
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# Walks through an array from the last element backward,
|
|
6
|
+
# one step at a time. {#next} moves forward toward the end.
|
|
7
|
+
# Both methods clamp at the array boundaries.
|
|
8
|
+
class Walker
|
|
9
|
+
##
|
|
10
|
+
# Sets the cursor position
|
|
11
|
+
attr_writer :cursor
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# @param [Array] items
|
|
15
|
+
def initialize(items)
|
|
16
|
+
@items = items
|
|
17
|
+
@cursor = items.size
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @return [Object, nil]
|
|
22
|
+
def next
|
|
23
|
+
if @items.empty?
|
|
24
|
+
nil
|
|
25
|
+
elsif @cursor >= @items.size - 1
|
|
26
|
+
@items[@cursor]
|
|
27
|
+
else
|
|
28
|
+
@cursor += 1
|
|
29
|
+
@items[@cursor]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @return [Object, nil]
|
|
35
|
+
def prev
|
|
36
|
+
if @items.empty?
|
|
37
|
+
nil
|
|
38
|
+
elsif @cursor <= 0
|
|
39
|
+
@items[@cursor]
|
|
40
|
+
else
|
|
41
|
+
@cursor -= 1
|
|
42
|
+
@items[@cursor]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/llm/repl.rb
CHANGED
|
@@ -23,9 +23,17 @@ module LLM
|
|
|
23
23
|
require_relative "repl/stream"
|
|
24
24
|
require_relative "repl/markdown"
|
|
25
25
|
require_relative "repl/command"
|
|
26
|
+
require_relative "repl/walker"
|
|
27
|
+
|
|
28
|
+
attr_reader :agent, :provider, :stream,
|
|
29
|
+
:status, :transcript, :input,
|
|
30
|
+
:window, :tools, :thread,
|
|
31
|
+
:name, :path
|
|
26
32
|
|
|
27
33
|
##
|
|
28
34
|
# @param [LLM::Agent] agent
|
|
35
|
+
# @param [String, nil] name
|
|
36
|
+
# The agent's name (optional)
|
|
29
37
|
# @param [String, nil] path
|
|
30
38
|
# The path where to maintain runtime state
|
|
31
39
|
# @param [Array<LLM::Tool>] tools
|
|
@@ -33,13 +41,14 @@ module LLM
|
|
|
33
41
|
# @param [Array<String>] skills
|
|
34
42
|
# Zero or more skills
|
|
35
43
|
# @return [LLM::Repl]
|
|
36
|
-
def initialize(agent:, tools: [], skills: [], path: nil)
|
|
37
|
-
@path
|
|
44
|
+
def initialize(agent:, name: nil, tools: [], skills: [], path: nil)
|
|
45
|
+
@path = path
|
|
46
|
+
@name = name || "agent"
|
|
38
47
|
@agent = configure(agent:, path:)
|
|
39
48
|
@provider = agent.llm.name
|
|
40
49
|
@status = Status.new(@agent)
|
|
41
50
|
@transcript = Transcript.new
|
|
42
|
-
@input = Input.new(
|
|
51
|
+
@input = Input.new(self, height: 3)
|
|
43
52
|
@window = Window.new(@status, @transcript, @input)
|
|
44
53
|
@thread = nil
|
|
45
54
|
@queue = Queue.new
|
|
@@ -150,7 +159,7 @@ module LLM
|
|
|
150
159
|
status.text = thinking_text
|
|
151
160
|
write("user: ", Curses::A_BOLD)
|
|
152
161
|
markdown(text)
|
|
153
|
-
write("\
|
|
162
|
+
write("\n#{name}: ", Curses::A_BOLD)
|
|
154
163
|
@thread = Thread.new do
|
|
155
164
|
@queue << [:start]
|
|
156
165
|
agent.talk(text, tools:, stream:)
|
|
@@ -181,12 +190,14 @@ module LLM
|
|
|
181
190
|
"command(#{command.name}): ",
|
|
182
191
|
"too few arguments: expected #{reqc} but got #{args.size}\n\n"]
|
|
183
192
|
else
|
|
193
|
+
parameters = parameters.sort_by(&:index).to_h
|
|
194
|
+
kwargs = {}
|
|
184
195
|
parameters.each_value do |parameter|
|
|
185
196
|
if args[parameter.index]
|
|
186
|
-
parameter.
|
|
197
|
+
kwargs[parameter.name] = args[parameter.index]
|
|
187
198
|
end
|
|
188
199
|
end
|
|
189
|
-
[:command, command.new(self),
|
|
200
|
+
[:command, command.new(self), kwargs]
|
|
190
201
|
end
|
|
191
202
|
else
|
|
192
203
|
[:input, text]
|
|
@@ -216,7 +227,7 @@ module LLM
|
|
|
216
227
|
@thread = nil
|
|
217
228
|
when :cancel
|
|
218
229
|
status.text = "Idle"
|
|
219
|
-
write("\n\
|
|
230
|
+
write("\n\n#{name}: ", Curses::A_BOLD)
|
|
220
231
|
write("request cancelled!")
|
|
221
232
|
write("\n\n")
|
|
222
233
|
transcript.finish
|
|
@@ -246,11 +257,6 @@ module LLM
|
|
|
246
257
|
File.readable?(path)
|
|
247
258
|
end
|
|
248
259
|
|
|
249
|
-
attr_reader :agent, :provider, :stream,
|
|
250
|
-
:status, :transcript, :input,
|
|
251
|
-
:window, :tools, :thread,
|
|
252
|
-
:path
|
|
253
|
-
|
|
254
260
|
File = ::File
|
|
255
261
|
private_constant :File
|
|
256
262
|
end
|
data/lib/llm/response.rb
CHANGED
|
@@ -10,12 +10,22 @@ module LLM
|
|
|
10
10
|
# handling can share one common surface without flattening away
|
|
11
11
|
# specialized behavior.
|
|
12
12
|
#
|
|
13
|
+
# @example Accessing response data
|
|
14
|
+
# res = agent.talk "Weather in Paris?", schema: Weather
|
|
15
|
+
# res.content # => raw text or structured data
|
|
16
|
+
# res.content! # => parsed JSON as LLM::Object
|
|
17
|
+
# res.usage # => token usage (input, output, cache, etc.)
|
|
18
|
+
# res.messages # => message history from this turn
|
|
19
|
+
#
|
|
13
20
|
# The normalized response keeps the transport response available
|
|
14
21
|
# through {#res}. When the default net/http transport is in use,
|
|
15
22
|
# {LLM::Transport::Response::HTTP
|
|
16
23
|
# LLM::Transport::Response::HTTP} keeps the
|
|
17
24
|
# original `Net::HTTPResponse` available through
|
|
18
25
|
# its own {LLM::Transport::Response::HTTP#res #res}.
|
|
26
|
+
#
|
|
27
|
+
# @see LLM::Context Responses are returned by Context#talk
|
|
28
|
+
# @see LLM::Agent Responses are returned by Agent#talk
|
|
19
29
|
class Response
|
|
20
30
|
require "json"
|
|
21
31
|
|
data/lib/llm/schema/leaf.rb
CHANGED
|
@@ -8,12 +8,17 @@ class LLM::Schema
|
|
|
8
8
|
# an example of how to create instances of {LLM::Schema::Leaf LLM::Schema::Leaf}
|
|
9
9
|
# through its subclasses.
|
|
10
10
|
class Leaf
|
|
11
|
+
##
|
|
12
|
+
# @return [Integer, nil]
|
|
13
|
+
attr_accessor :index
|
|
14
|
+
|
|
11
15
|
def initialize
|
|
12
16
|
@description = nil
|
|
13
17
|
@default = nil
|
|
14
18
|
@enum = nil
|
|
15
19
|
@required = nil
|
|
16
20
|
@const = nil
|
|
21
|
+
@index = nil
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
##
|
data/lib/llm/schema/object.rb
CHANGED
|
@@ -16,21 +16,22 @@ class LLM::Schema
|
|
|
16
16
|
# A hash of properties
|
|
17
17
|
# @return [LLM::Schema::Object]
|
|
18
18
|
def initialize(properties)
|
|
19
|
-
@properties =
|
|
19
|
+
@properties = LLM::Object.new
|
|
20
|
+
properties.each { set!(_1, _2) }
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
##
|
|
23
24
|
# Get a property
|
|
24
25
|
# @return [LLM::Schema::Leaf]
|
|
25
26
|
def [](key)
|
|
26
|
-
properties[key
|
|
27
|
+
properties[key]
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
##
|
|
30
31
|
# Set a property
|
|
31
32
|
# @return [void]
|
|
32
33
|
def []=(key, val)
|
|
33
|
-
|
|
34
|
+
set!(key, val)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
##
|
|
@@ -46,7 +47,7 @@ class LLM::Schema
|
|
|
46
47
|
# Returns self
|
|
47
48
|
def merge!(other)
|
|
48
49
|
raise TypeError, "expected #{self.class} but got #{other.class}" unless self.class === other
|
|
49
|
-
|
|
50
|
+
other.properties.each { |key, val| self[key] = val }
|
|
50
51
|
self
|
|
51
52
|
end
|
|
52
53
|
|
|
@@ -64,8 +65,13 @@ class LLM::Schema
|
|
|
64
65
|
|
|
65
66
|
private
|
|
66
67
|
|
|
68
|
+
def set!(key, val)
|
|
69
|
+
val.index = @properties.size
|
|
70
|
+
properties[key] = val
|
|
71
|
+
end
|
|
72
|
+
|
|
67
73
|
def required_items
|
|
68
|
-
@properties.filter_map {
|
|
74
|
+
@properties.filter_map { _2.required? ? _1 : nil }
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
77
|
end
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -32,9 +32,41 @@ module LLM::Sequel
|
|
|
32
32
|
@llm_agent_options || Agent::DEFAULTS
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
##
|
|
36
|
+
# @return [Class<LLM::Agent>]
|
|
35
37
|
def agent
|
|
36
38
|
@agent ||= Class.new(LLM::Agent)
|
|
37
39
|
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Bulk-assign class-level agent defaults.
|
|
43
|
+
#
|
|
44
|
+
# Each key is resolved by calling the corresponding class method on the
|
|
45
|
+
# internal agent subclass.
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# class Agent < Sequel::Model
|
|
49
|
+
# plugin :llm_agent
|
|
50
|
+
# set instructions: "You are a system administrator",
|
|
51
|
+
# model: "gpt-4.1-nano",
|
|
52
|
+
# tools: [Shell]
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# @param [Hash] properties
|
|
56
|
+
# @option properties [String] :instructions
|
|
57
|
+
# @option properties [String] :model
|
|
58
|
+
# @option properties [Array<LLM::Function>] :tools
|
|
59
|
+
# @option properties [Array<String>] :skills
|
|
60
|
+
# @option properties [#to_json] :schema
|
|
61
|
+
# @option properties [Symbol, Array<Symbol>] :concurrency
|
|
62
|
+
# @option properties [LLM::Tracer, Proc] :tracer
|
|
63
|
+
# @option properties [Object, Proc] :stream
|
|
64
|
+
# @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
|
|
65
|
+
# @raise [KeyError] when a property key does not match a class-level accessor
|
|
66
|
+
# @return [void]
|
|
67
|
+
def set(properties)
|
|
68
|
+
agent.set(properties)
|
|
69
|
+
end
|
|
38
70
|
end
|
|
39
71
|
|
|
40
72
|
module InstanceMethods
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -204,17 +204,17 @@ module LLM::Sequel
|
|
|
204
204
|
end
|
|
205
205
|
|
|
206
206
|
##
|
|
207
|
-
# @see LLM::Context#
|
|
207
|
+
# @see LLM::Context#pending_functions
|
|
208
208
|
# @return [Array<LLM::Function>]
|
|
209
|
-
def
|
|
210
|
-
ctx.
|
|
209
|
+
def pending_functions
|
|
210
|
+
ctx.pending_functions
|
|
211
211
|
end
|
|
212
212
|
|
|
213
213
|
##
|
|
214
|
-
# @see LLM::Context#
|
|
214
|
+
# @see LLM::Context#pending_functions?
|
|
215
215
|
# @return [Boolean]
|
|
216
|
-
def
|
|
217
|
-
ctx.
|
|
216
|
+
def pending_functions?
|
|
217
|
+
ctx.pending_functions?
|
|
218
218
|
end
|
|
219
219
|
|
|
220
220
|
##
|