llm.rb 13.1.0 → 15.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 +716 -1826
- data/README.md +668 -253
- data/bin/llm.rb +156 -56
- data/data/alibaba.json +1999 -0
- data/data/anthropic.json +195 -252
- data/data/bedrock.json +2189 -1854
- data/data/deepinfra.json +1312 -728
- data/data/deepseek.json +42 -39
- data/data/google.json +1024 -404
- data/data/mistral.json +481 -401
- data/data/moonshot.json +384 -0
- data/data/openai.json +985 -1354
- data/data/xai.json +220 -115
- data/data/zai.json +166 -166
- data/docs/deepdive/advanced/cancellation.md +74 -0
- data/docs/deepdive/advanced/compaction.md +85 -0
- data/docs/deepdive/advanced/context.md +280 -0
- data/docs/deepdive/advanced/guard.md +371 -0
- data/docs/deepdive/advanced/transformer.md +67 -0
- data/docs/deepdive/advanced/transports.md +45 -0
- data/docs/deepdive/features/builtin_tools.md +191 -0
- data/docs/deepdive/features/concurrency.md +110 -0
- data/docs/deepdive/features/database.md +449 -0
- data/docs/deepdive/features/embeddings.md +157 -0
- data/docs/deepdive/features/repl.md +141 -0
- data/docs/deepdive/fundamentals/agents.md +253 -0
- data/docs/deepdive/fundamentals/providers.md +159 -0
- data/docs/deepdive/fundamentals/schema.md +61 -0
- data/docs/deepdive/fundamentals/skills.md +111 -0
- data/docs/deepdive/fundamentals/stream.md +143 -0
- data/docs/deepdive/fundamentals/tools.md +329 -0
- data/docs/deepdive/media/audio.md +122 -0
- data/docs/deepdive/media/images.md +89 -0
- data/docs/deepdive/media/ocr.md +48 -0
- data/docs/deepdive/protocols/a2a.md +106 -0
- data/docs/deepdive/protocols/mcp.md +111 -0
- data/docs/deepdive/reference/cost.md +109 -0
- data/docs/deepdive/reference/model_registry.md +271 -0
- data/docs/deepdive/reference/object.md +108 -0
- data/docs/deepdive/reference/tracer.md +187 -0
- data/{resources → docs}/deepdive.md +37 -23
- data/lib/llm/a2a/transport/http.rb +1 -1
- data/lib/llm/active_record/acts_as_llm.rb +19 -5
- data/lib/llm/agent.rb +107 -17
- data/lib/llm/context.rb +164 -134
- data/lib/llm/cost.rb +114 -49
- data/lib/llm/error.rb +7 -8
- data/lib/llm/function/array.rb +1 -1
- data/lib/llm/function/async/task.rb +2 -0
- data/lib/llm/function/fiber/task.rb +2 -0
- data/lib/llm/function/fork/task.rb +16 -1
- data/lib/llm/function/ractor/task.rb +2 -0
- data/lib/llm/function/sequential/group.rb +20 -10
- data/lib/llm/function/sequential/task.rb +2 -9
- data/lib/llm/function/task.rb +4 -0
- data/lib/llm/function/thread/task.rb +2 -0
- data/lib/llm/function.rb +33 -4
- data/lib/llm/guard/loop.rb +89 -0
- data/lib/llm/guard/null.rb +19 -0
- data/lib/llm/guard.rb +61 -0
- data/lib/llm/message.rb +5 -4
- data/lib/llm/provider.rb +43 -0
- data/lib/llm/providers/alibaba/error_handler.rb +34 -0
- data/lib/llm/providers/alibaba/request_adapter.rb +13 -0
- data/lib/llm/providers/alibaba.rb +93 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
- data/lib/llm/providers/anthropic.rb +1 -9
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -0
- data/lib/llm/providers/bedrock.rb +9 -9
- data/lib/llm/providers/deepseek/request_adapter.rb +2 -33
- data/lib/llm/providers/google/stream_parser.rb +1 -0
- data/lib/llm/providers/google.rb +1 -9
- data/lib/llm/providers/moonshot.rb +76 -0
- data/lib/llm/providers/ollama.rb +1 -9
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
- data/lib/llm/providers/openai/responses.rb +6 -9
- data/lib/llm/providers/openai/schema.rb +37 -0
- data/lib/llm/providers/openai/stream_parser.rb +1 -0
- data/lib/llm/providers/openai.rb +4 -12
- data/lib/llm/registry/model.rb +186 -0
- data/lib/llm/registry.rb +45 -14
- data/lib/llm/repl/bar.rb +11 -12
- data/lib/llm/repl/buffer.rb +43 -16
- data/lib/llm/repl/color.rb +85 -0
- data/lib/llm/repl/command.rb +12 -0
- data/lib/llm/repl/commands/model.rb +39 -0
- data/lib/llm/repl/input/cache.rb +45 -0
- data/lib/llm/repl/input/char.rb +46 -0
- data/lib/llm/repl/input/row.rb +39 -0
- data/lib/llm/repl/input.rb +327 -79
- data/lib/llm/repl/markdown/table.rb +6 -2
- data/lib/llm/repl/markdown.rb +56 -6
- data/lib/llm/repl/node.rb +7 -0
- data/lib/llm/repl/status.rb +54 -5
- data/lib/llm/repl/stream.rb +16 -4
- data/lib/llm/repl/walker.rb +3 -2
- data/lib/llm/repl/window.rb +111 -11
- data/lib/llm/repl.rb +47 -21
- data/lib/llm/sequel/plugin.rb +19 -5
- data/lib/llm/skill.rb +21 -8
- data/lib/llm/stream.rb +35 -7
- data/lib/llm/tool.rb +27 -0
- data/lib/llm/tools/rg.rb +2 -1
- data/lib/llm/transformer/null.rb +21 -0
- data/lib/llm/transformer.rb +55 -0
- data/lib/llm/transport/curb.rb +23 -3
- data/lib/llm/usage.rb +155 -9
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +121 -29
- data/llm.gemspec +16 -10
- metadata +100 -13
- data/lib/llm/loop_guard.rb +0 -107
data/lib/llm/repl/node.rb
CHANGED
data/lib/llm/repl/status.rb
CHANGED
|
@@ -4,38 +4,87 @@ class LLM::Repl
|
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::Repl::Status LLM::Repl::Status} class stores
|
|
6
6
|
# the small status line shown at the top of the REPL.
|
|
7
|
+
#
|
|
8
|
+
# The status text can be a plain String, a single
|
|
9
|
+
# {LLM::Repl::Node LLM::Repl::Node} (text plus curses attributes),
|
|
10
|
+
# or an Array of either. Attributes on nodes are applied when the
|
|
11
|
+
# window draws the status line.
|
|
7
12
|
# @api private
|
|
8
13
|
class Status
|
|
9
14
|
##
|
|
10
15
|
# @param [LLM::Repl] repl
|
|
11
16
|
# @return [LLM::Repl::Status]
|
|
12
17
|
def initialize(repl)
|
|
18
|
+
@repl = repl
|
|
13
19
|
@agent = repl.agent
|
|
14
20
|
@provider = @agent.llm.name
|
|
15
|
-
@
|
|
21
|
+
@nodes = [Node.new("idle")]
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
##
|
|
19
25
|
# @return [String]
|
|
20
26
|
def context_bar
|
|
21
27
|
LLM::Repl::Bar.new(
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
##
|
|
29
|
+
# After compaction the used context is unknown until the next
|
|
30
|
+
# response, so the bar renders an unknown state instead of a
|
|
31
|
+
# stale percentage.
|
|
32
|
+
fraction: @agent.compacted? ? nil : @agent.context_usage
|
|
24
33
|
).to_s
|
|
25
34
|
end
|
|
26
35
|
|
|
27
36
|
##
|
|
28
37
|
# @return [String]
|
|
29
38
|
def cost
|
|
30
|
-
"$#{@agent.cost}"
|
|
39
|
+
"$#{@agent.cost} "
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Returns the current model in use.
|
|
44
|
+
# @return [LLM::Repl::Node]
|
|
45
|
+
def model
|
|
46
|
+
Node.new @repl.model.to_s, Curses::A_BOLD
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# Returns the current working directory.
|
|
51
|
+
# @return [LLM::Repl::Node]
|
|
52
|
+
def cwd
|
|
53
|
+
Node.new Dir.pwd.sub(Dir.home, "~"), Curses::A_BOLD
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Sets the status text.
|
|
58
|
+
# @param [String, LLM::Repl::Node, Array<String, LLM::Repl::Node>] value
|
|
59
|
+
# @return [Array<LLM::Repl::Node>]
|
|
60
|
+
def text=(value)
|
|
61
|
+
@nodes =
|
|
62
|
+
case value
|
|
63
|
+
when Node then [value]
|
|
64
|
+
when Array then value.map { |node| Node === node ? node : Node.new(node) }
|
|
65
|
+
else [Node.new(value)]
|
|
66
|
+
end
|
|
31
67
|
end
|
|
32
68
|
|
|
33
69
|
##
|
|
34
70
|
# @return [String]
|
|
35
|
-
|
|
71
|
+
def text
|
|
72
|
+
@nodes.map(&:text).join
|
|
73
|
+
end
|
|
36
74
|
|
|
37
75
|
##
|
|
38
76
|
# @return [String]
|
|
39
77
|
alias_method :to_s, :text
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# The nodes making up the status line.
|
|
81
|
+
# @return [Array<LLM::Repl::Node>]
|
|
82
|
+
def nodes
|
|
83
|
+
if @agent.compacted?
|
|
84
|
+
[Node.new("Context compacted")]
|
|
85
|
+
else
|
|
86
|
+
@nodes
|
|
87
|
+
end
|
|
88
|
+
end
|
|
40
89
|
end
|
|
41
90
|
end
|
data/lib/llm/repl/stream.rb
CHANGED
|
@@ -8,6 +8,10 @@ class LLM::Repl
|
|
|
8
8
|
# tool returns.
|
|
9
9
|
# @api private
|
|
10
10
|
class Stream < LLM::Stream
|
|
11
|
+
##
|
|
12
|
+
# @return [Hash<Symbol, LLM::Tool>]
|
|
13
|
+
attr_reader :tools
|
|
14
|
+
|
|
11
15
|
##
|
|
12
16
|
# @param [LLM::Repl] repl
|
|
13
17
|
# @return [LLM::Repl::Stream]
|
|
@@ -15,6 +19,7 @@ class LLM::Repl
|
|
|
15
19
|
@repl = repl
|
|
16
20
|
@_queue = queue
|
|
17
21
|
@buffer = +""
|
|
22
|
+
@tools = {}
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
##
|
|
@@ -30,15 +35,16 @@ class LLM::Repl
|
|
|
30
35
|
# @param [LLM::Function] tool
|
|
31
36
|
# @return [void]
|
|
32
37
|
def on_tool_call(tool)
|
|
33
|
-
@
|
|
38
|
+
@tools[tool.name] = tool
|
|
39
|
+
@_queue.push [:status, [" ", lambda, " • #{tool.name}(#{format_args(tool)})"]]
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
##
|
|
37
|
-
# @param [LLM::Function]
|
|
43
|
+
# @param [LLM::Function] tool
|
|
38
44
|
# @param [LLM::Function::Return] result
|
|
39
45
|
# @return [void]
|
|
40
|
-
def on_tool_return(
|
|
41
|
-
@
|
|
46
|
+
def on_tool_return(tool, result)
|
|
47
|
+
@tools.delete(tool.name)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
##
|
|
@@ -50,6 +56,12 @@ class LLM::Repl
|
|
|
50
56
|
|
|
51
57
|
private
|
|
52
58
|
|
|
59
|
+
##
|
|
60
|
+
# @return [LLM::Repl::Node]
|
|
61
|
+
def lambda
|
|
62
|
+
Node.new("λ", Curses::A_BOLD | Color.red)
|
|
63
|
+
end
|
|
64
|
+
|
|
53
65
|
##
|
|
54
66
|
# Formats tool arguments as compact key: value pairs
|
|
55
67
|
# suitable for the status line. Strings are quoted and
|
data/lib/llm/repl/walker.rb
CHANGED
|
@@ -18,12 +18,13 @@ class LLM::Repl
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
##
|
|
21
|
-
# @return [Object, nil]
|
|
21
|
+
# @return [Object, String, nil]
|
|
22
22
|
def next
|
|
23
23
|
if @items.empty?
|
|
24
24
|
nil
|
|
25
25
|
elsif @cursor >= @items.size - 1
|
|
26
|
-
@items
|
|
26
|
+
@cursor = @items.size
|
|
27
|
+
""
|
|
27
28
|
else
|
|
28
29
|
@cursor += 1
|
|
29
30
|
@items[@cursor]
|
data/lib/llm/repl/window.rb
CHANGED
|
@@ -6,6 +6,11 @@ class LLM::Repl
|
|
|
6
6
|
# curses screen for the REPL.
|
|
7
7
|
# @api private
|
|
8
8
|
class Window
|
|
9
|
+
##
|
|
10
|
+
# Rows reserved for the top chrome (the blue model/cwd row).
|
|
11
|
+
# @api private
|
|
12
|
+
TOP_ROWS = 1
|
|
13
|
+
|
|
9
14
|
##
|
|
10
15
|
# @return [LLM::Repl::Status]
|
|
11
16
|
attr_reader :status
|
|
@@ -23,6 +28,7 @@ class LLM::Repl
|
|
|
23
28
|
# A read-eval-print loop.
|
|
24
29
|
# @return [LLM::Repl::Window]
|
|
25
30
|
def initialize(repl)
|
|
31
|
+
@repl = repl
|
|
26
32
|
@status = repl.status
|
|
27
33
|
@buffer = repl.buffer
|
|
28
34
|
@input = repl.input
|
|
@@ -33,21 +39,25 @@ class LLM::Repl
|
|
|
33
39
|
# @return [void]
|
|
34
40
|
def open
|
|
35
41
|
Curses.init_screen
|
|
42
|
+
Color.enable
|
|
36
43
|
Curses.cbreak
|
|
37
44
|
Curses.noecho
|
|
38
45
|
Curses.stdscr.keypad(true)
|
|
39
46
|
Curses.stdscr.nodelay = true
|
|
47
|
+
hide_cursor
|
|
40
48
|
yield
|
|
41
49
|
ensure
|
|
50
|
+
show_cursor
|
|
42
51
|
Curses.close_screen
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
##
|
|
46
55
|
# @return [void]
|
|
47
56
|
def redraw
|
|
57
|
+
draw_meta(offset: Curses.lines)
|
|
48
58
|
draw_status(offset: input.height + 1)
|
|
49
59
|
draw_divider(offset: 5)
|
|
50
|
-
draw_buffer(offset:
|
|
60
|
+
draw_buffer(offset: TOP_ROWS)
|
|
51
61
|
draw_input
|
|
52
62
|
Curses.refresh
|
|
53
63
|
end
|
|
@@ -58,6 +68,20 @@ class LLM::Repl
|
|
|
58
68
|
[Curses.lines - (input.height + 4), 1].max
|
|
59
69
|
end
|
|
60
70
|
|
|
71
|
+
##
|
|
72
|
+
# @return [Integer]
|
|
73
|
+
def columns
|
|
74
|
+
Curses.cols
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# Redraws the window after it has been resized.
|
|
79
|
+
# @return [void]
|
|
80
|
+
def resize
|
|
81
|
+
Curses.clear
|
|
82
|
+
redraw
|
|
83
|
+
end
|
|
84
|
+
|
|
61
85
|
##
|
|
62
86
|
# @return [Object]
|
|
63
87
|
def getch
|
|
@@ -102,43 +126,91 @@ class LLM::Repl
|
|
|
102
126
|
|
|
103
127
|
private
|
|
104
128
|
|
|
129
|
+
##
|
|
130
|
+
# Draws the window's primary content
|
|
131
|
+
# @return [void]
|
|
105
132
|
def draw_buffer(offset:)
|
|
106
133
|
rows = buffer.visible(self.rows)
|
|
107
|
-
rows.each.with_index(
|
|
134
|
+
rows.each.with_index(TOP_ROWS) do |row, index|
|
|
108
135
|
Curses.setpos(index, 0)
|
|
109
136
|
Curses.clrtoeol
|
|
137
|
+
Curses.setpos(index, gutter)
|
|
138
|
+
width = 0
|
|
110
139
|
row.each do |chunk|
|
|
111
|
-
|
|
140
|
+
remaining = buffer.width - width
|
|
141
|
+
break if remaining <= 0
|
|
142
|
+
text, attrs = chunk.text.to_s, chunk.attrs
|
|
143
|
+
clipped = text[0, remaining]
|
|
112
144
|
Curses.attron(attrs) if attrs
|
|
113
|
-
Curses.addstr(
|
|
145
|
+
Curses.addstr(clipped)
|
|
114
146
|
Curses.attroff(attrs) if attrs
|
|
147
|
+
width += clipped.length
|
|
115
148
|
end
|
|
116
149
|
end
|
|
117
150
|
last_drawn = offset + rows.size
|
|
118
|
-
(last_drawn...
|
|
151
|
+
(last_drawn...(Curses.lines - 5)).each do |line|
|
|
119
152
|
Curses.setpos(line, 0)
|
|
120
153
|
Curses.clrtoeol
|
|
121
154
|
end
|
|
122
155
|
end
|
|
123
156
|
|
|
157
|
+
##
|
|
158
|
+
# Draws the status line
|
|
159
|
+
# @return [void]
|
|
124
160
|
def draw_status(offset:)
|
|
125
|
-
Curses.
|
|
161
|
+
y = Curses.lines - offset
|
|
162
|
+
Curses.setpos(y, 0)
|
|
126
163
|
Curses.clrtoeol
|
|
127
|
-
|
|
164
|
+
status.nodes.each { addnode(_1) }
|
|
128
165
|
context = status.context_bar
|
|
129
|
-
Curses.setpos(
|
|
166
|
+
Curses.setpos(y, [(columns - context.length) / 2, 0].max)
|
|
130
167
|
Curses.addstr(context)
|
|
131
168
|
cost = status.cost.to_s
|
|
132
|
-
Curses.setpos(
|
|
169
|
+
Curses.setpos(y, [columns - cost.length, 0].max)
|
|
133
170
|
Curses.addstr(cost)
|
|
134
171
|
end
|
|
135
172
|
|
|
173
|
+
##
|
|
174
|
+
# Draws the top chrome row (model left, cwd right) at the very top
|
|
175
|
+
# of the screen, with a blue background.
|
|
176
|
+
# @param [Integer] offset
|
|
177
|
+
# @return [void]
|
|
178
|
+
def draw_meta(offset:)
|
|
179
|
+
y = Curses.lines - offset
|
|
180
|
+
fill_row(y) do
|
|
181
|
+
addnode(status.cwd, status.cwd.text[0, columns])
|
|
182
|
+
Curses.setpos(y, [columns - status.model.text.size, 0].max)
|
|
183
|
+
addnode(status.model, status.model.text[0, columns])
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
##
|
|
188
|
+
# Fills an entire row with the status-bar background color, then
|
|
189
|
+
# draws the given content on top (which inherits the blue
|
|
190
|
+
# background), and resets the background afterwards.
|
|
191
|
+
# @param [Integer] y
|
|
192
|
+
# @yield Content to draw on the row
|
|
193
|
+
# @return [void]
|
|
194
|
+
def fill_row(y, &)
|
|
195
|
+
Curses.setpos(y, 0)
|
|
196
|
+
Curses.bkgdset(Color.statusbar)
|
|
197
|
+
Curses.clrtoeol
|
|
198
|
+
yield
|
|
199
|
+
Curses.bkgdset(0)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
##
|
|
203
|
+
# Draws a horiztonal line
|
|
204
|
+
# @return [void]
|
|
136
205
|
def draw_divider(offset:)
|
|
137
206
|
Curses.setpos(Curses.lines - offset, 0)
|
|
138
207
|
Curses.clrtoeol
|
|
139
208
|
Curses.addstr("─" * Curses.cols)
|
|
140
209
|
end
|
|
141
210
|
|
|
211
|
+
##
|
|
212
|
+
# Draws the input area
|
|
213
|
+
# @return [void]
|
|
142
214
|
def draw_input
|
|
143
215
|
rows = input.lines
|
|
144
216
|
(0...input.height).each do |idx|
|
|
@@ -148,12 +220,40 @@ class LLM::Repl
|
|
|
148
220
|
end
|
|
149
221
|
line, col = input.cursor_pos
|
|
150
222
|
Curses.setpos((Curses.lines - input.height) + line, col)
|
|
223
|
+
show_cursor
|
|
151
224
|
end
|
|
152
225
|
|
|
153
226
|
##
|
|
227
|
+
# Hides the cursor.
|
|
228
|
+
# @return [void]
|
|
229
|
+
def hide_cursor
|
|
230
|
+
Curses.curs_set(0)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
##
|
|
234
|
+
# Shows the cursor.
|
|
235
|
+
# @return [void]
|
|
236
|
+
def show_cursor
|
|
237
|
+
Curses.curs_set(1)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
##
|
|
241
|
+
# 20% offset that occupies the left margin and
|
|
242
|
+
# helps center {LLM::Repl::Buffer LLM::Repl::Buffer}.
|
|
154
243
|
# @return [Integer]
|
|
155
|
-
def
|
|
156
|
-
|
|
244
|
+
def gutter
|
|
245
|
+
(columns * 0.2).floor
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
##
|
|
249
|
+
# Adds a {LLM::Repl::Node} to the Curses window.
|
|
250
|
+
# @param [LLM::Repl::Node] node
|
|
251
|
+
# @param [String] text
|
|
252
|
+
# @return [void]
|
|
253
|
+
def addnode(node, text = node.text)
|
|
254
|
+
Curses.attron(node.attrs) if node.attrs
|
|
255
|
+
Curses.addstr(text)
|
|
256
|
+
Curses.attroff(node.attrs) if node.attrs
|
|
157
257
|
end
|
|
158
258
|
end
|
|
159
259
|
end
|
data/lib/llm/repl.rb
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
LLM.require "curses"
|
|
4
|
-
LLM.require "kramdown"
|
|
5
|
-
|
|
6
3
|
module LLM
|
|
7
4
|
##
|
|
8
5
|
# The {LLM::Repl LLM::Repl} class provides a small
|
|
@@ -15,6 +12,10 @@ module LLM
|
|
|
15
12
|
# task correctly, or for it to correct course after a
|
|
16
13
|
# mistake was made.
|
|
17
14
|
class Repl
|
|
15
|
+
LLM.require "curses", "~> 1.6"
|
|
16
|
+
LLM.require "kramdown", "~> 2.5"
|
|
17
|
+
|
|
18
|
+
require_relative "repl/color"
|
|
18
19
|
require_relative "repl/window"
|
|
19
20
|
require_relative "repl/status"
|
|
20
21
|
require_relative "repl/buffer"
|
|
@@ -29,7 +30,7 @@ module LLM
|
|
|
29
30
|
attr_reader :agent, :provider, :stream,
|
|
30
31
|
:status, :buffer, :input,
|
|
31
32
|
:window, :tools, :thread,
|
|
32
|
-
:name, :path
|
|
33
|
+
:name, :path
|
|
33
34
|
|
|
34
35
|
##
|
|
35
36
|
# @param [LLM::Agent] agent
|
|
@@ -43,14 +44,15 @@ module LLM
|
|
|
43
44
|
# Zero or more skills
|
|
44
45
|
# @return [LLM::Repl]
|
|
45
46
|
def initialize(agent:, name: nil, tools: [], skills: [], path: nil)
|
|
46
|
-
@width = 80
|
|
47
47
|
@path = path
|
|
48
48
|
@name = name || "agent"
|
|
49
|
+
@sender = "You"
|
|
49
50
|
@agent = configure(agent:, path:)
|
|
50
51
|
@provider = agent.llm.name
|
|
51
|
-
@
|
|
52
|
-
@buffer = Buffer.new(self)
|
|
52
|
+
@model = agent.model
|
|
53
53
|
@input = Input.new(self, height: 3)
|
|
54
|
+
@buffer = Buffer.new(self)
|
|
55
|
+
@status = Status.new(self)
|
|
54
56
|
@window = Window.new(self)
|
|
55
57
|
@thread = nil
|
|
56
58
|
@queue = Queue.new
|
|
@@ -112,7 +114,7 @@ module LLM
|
|
|
112
114
|
# @param [String] chars
|
|
113
115
|
# @return [Array<Node>]
|
|
114
116
|
def markdown(chars)
|
|
115
|
-
LLM::Repl::Markdown.new(chars, width).ast
|
|
117
|
+
LLM::Repl::Markdown.new(chars, buffer.width).ast
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
##
|
|
@@ -125,8 +127,34 @@ module LLM
|
|
|
125
127
|
|
|
126
128
|
##
|
|
127
129
|
# @return [String]
|
|
128
|
-
def
|
|
129
|
-
"
|
|
130
|
+
def think_text
|
|
131
|
+
" 🧠 • Thinking • Esc to cancel"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
##
|
|
135
|
+
# @return [String]
|
|
136
|
+
def connect_text
|
|
137
|
+
" 🌐 • Connecting • Esc to cancel"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
##
|
|
141
|
+
# Returns a label for the person who sent a message.
|
|
142
|
+
# @return [String]
|
|
143
|
+
def sender
|
|
144
|
+
@sender
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
##
|
|
148
|
+
# Returns the active model.
|
|
149
|
+
# @return [String]
|
|
150
|
+
def model
|
|
151
|
+
@model
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# Sets the active model.
|
|
156
|
+
def model=(other)
|
|
157
|
+
@model = other
|
|
130
158
|
end
|
|
131
159
|
|
|
132
160
|
private
|
|
@@ -138,11 +166,11 @@ module LLM
|
|
|
138
166
|
def tree(messages)
|
|
139
167
|
messages.flat_map do |message|
|
|
140
168
|
next if message.tool_call? || message.tool_return?
|
|
141
|
-
user = message.assistant? ? name :
|
|
169
|
+
user = message.assistant? ? name : sender
|
|
142
170
|
[
|
|
143
|
-
Node.new("#{user}
|
|
144
|
-
|
|
145
|
-
Node.new(
|
|
171
|
+
Node.new("#{user}:\n", Curses::A_BOLD | Color.blue),
|
|
172
|
+
*markdown(message.content),
|
|
173
|
+
Node.new("\n\n")
|
|
146
174
|
]
|
|
147
175
|
end.compact
|
|
148
176
|
end
|
|
@@ -180,17 +208,15 @@ module LLM
|
|
|
180
208
|
case on_text(text)
|
|
181
209
|
in [:command, Command => command, Hash => parameters]
|
|
182
210
|
command.call(**parameters)
|
|
183
|
-
write("\n")
|
|
184
211
|
in [:error, String => user, String => text]
|
|
185
212
|
write_message(user, text)
|
|
186
|
-
write("\n")
|
|
187
213
|
in [:input, String => text]
|
|
188
214
|
window.scroll_to_bottom
|
|
189
|
-
status.text =
|
|
190
|
-
write_message(
|
|
215
|
+
status.text = connect_text
|
|
216
|
+
write_message(sender, markdown(text))
|
|
191
217
|
@thread = Thread.new do
|
|
192
218
|
@queue << [:start]
|
|
193
|
-
agent.talk(text, tools:, stream:)
|
|
219
|
+
agent.talk(text, model:, tools:, stream:)
|
|
194
220
|
agent.save(path:) if save?
|
|
195
221
|
@queue << [:done]
|
|
196
222
|
rescue LLM::Interrupt => e
|
|
@@ -245,6 +271,7 @@ module LLM
|
|
|
245
271
|
buffer.open
|
|
246
272
|
stream.clear
|
|
247
273
|
when :stream
|
|
274
|
+
status.text = think_text if stream.tools.empty?
|
|
248
275
|
write_message name, markdown(value), method: :replace
|
|
249
276
|
when :status
|
|
250
277
|
self.status = value
|
|
@@ -252,13 +279,12 @@ module LLM
|
|
|
252
279
|
status.text = "idle"
|
|
253
280
|
buffer.close
|
|
254
281
|
@thread = nil
|
|
255
|
-
write("\n")
|
|
256
282
|
when :cancel
|
|
283
|
+
stream.tools.clear
|
|
257
284
|
buffer.close
|
|
258
285
|
status.text = "Idle"
|
|
259
286
|
write_message(name, "Request cancelled")
|
|
260
287
|
@thread = nil
|
|
261
|
-
write("\n")
|
|
262
288
|
when :error
|
|
263
289
|
status.text = "error"
|
|
264
290
|
write_message(name, "(#{value.class}): #{value.message}")
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -236,16 +236,30 @@ module LLM::Sequel
|
|
|
236
236
|
# @return [Integer]
|
|
237
237
|
def context_window
|
|
238
238
|
ctx.context_window
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
##
|
|
242
|
+
# Returns how many tokens have been used
|
|
243
|
+
# within the context window
|
|
244
|
+
# @return [Integer]
|
|
245
|
+
def context_used
|
|
246
|
+
ctx.context_used
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
##
|
|
250
|
+
# Returns context window usage as a Rational
|
|
251
|
+
# @return [Rational, nil]
|
|
252
|
+
def context_usage
|
|
253
|
+
ctx.context_usage
|
|
241
254
|
end
|
|
242
255
|
|
|
243
256
|
##
|
|
244
257
|
# Returns usage from the mapped usage columns.
|
|
245
|
-
# @return [LLM::
|
|
246
|
-
def
|
|
247
|
-
ctx.
|
|
258
|
+
# @return [LLM::Usage]
|
|
259
|
+
def token_usage
|
|
260
|
+
ctx.token_usage
|
|
248
261
|
end
|
|
262
|
+
alias_method :usage, :token_usage
|
|
249
263
|
|
|
250
264
|
##
|
|
251
265
|
# @see LLM::Context#interrupt!
|
data/lib/llm/skill.rb
CHANGED
|
@@ -80,19 +80,25 @@ module LLM
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
##
|
|
83
|
-
# Execute the skill by wrapping it in a small agent
|
|
84
|
-
# instructions. The context is bound
|
|
85
|
-
#
|
|
83
|
+
# Execute the skill by wrapping it in a small agent
|
|
84
|
+
# with the skill instructions. The context is bound
|
|
85
|
+
# explicitly by the caller so the nested agent can
|
|
86
|
+
# inherit context-level behavior such as streaming.
|
|
86
87
|
# @param [LLM::Context] ctx
|
|
87
88
|
# @return [Hash]
|
|
88
89
|
def call(ctx)
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
stream = ctx.stream
|
|
91
|
+
stream.on_skill_call(self)
|
|
92
|
+
agent = self.agent(ctx)
|
|
93
|
+
res = agent.talk("Solve the user's query.")
|
|
94
|
+
stream.on_skill_return(agent, self, res)
|
|
95
|
+
{content: res.content}
|
|
91
96
|
end
|
|
92
97
|
|
|
93
98
|
##
|
|
94
|
-
# Expose the skill as a normal LLM::Tool. The
|
|
95
|
-
# when the tool class
|
|
99
|
+
# Expose the skill as a normal LLM::Tool. The
|
|
100
|
+
# context is bound explicitly when the tool class
|
|
101
|
+
# is built.
|
|
96
102
|
# @param [LLM::Context] ctx
|
|
97
103
|
# @return [Class<LLM::Tool>]
|
|
98
104
|
def to_tool(ctx)
|
|
@@ -100,7 +106,14 @@ module LLM
|
|
|
100
106
|
Class.new(LLM::Tool) do
|
|
101
107
|
attr_accessor :tracer
|
|
102
108
|
|
|
103
|
-
|
|
109
|
+
##
|
|
110
|
+
# The skill's tool is named "<skill>-skill" so
|
|
111
|
+
# that it is unique from any tool the skill
|
|
112
|
+
# itself exposes (or any global tool). Otherwise
|
|
113
|
+
# a skill named `weather` that uses a `weather`
|
|
114
|
+
# tool would collide: both resolve to the same
|
|
115
|
+
# name.
|
|
116
|
+
name "#{skill.name}-skill"
|
|
104
117
|
description skill.description
|
|
105
118
|
|
|
106
119
|
define_singleton_method(:skill?) do
|
data/lib/llm/stream.rb
CHANGED
|
@@ -128,7 +128,10 @@ module LLM
|
|
|
128
128
|
##
|
|
129
129
|
# Called when a streamed tool call has been fully constructed.
|
|
130
130
|
# A stream implementation may start tool execution here by pushing
|
|
131
|
-
# `@queue << tool.task(:thread)` onto {#queue}
|
|
131
|
+
# `@queue << tool.task(:thread).tap(&:spawn)` onto {#queue} — `spawn`
|
|
132
|
+
# returns nil, so `tap` keeps the task while starting it. The task
|
|
133
|
+
# carries the context's guard, so it is checked before the tool runs —
|
|
134
|
+
# a blocked call yields its guard return without executing.
|
|
132
135
|
# @param [LLM::Function] tool
|
|
133
136
|
# The parsed tool call.
|
|
134
137
|
# @return [nil]
|
|
@@ -151,19 +154,17 @@ module LLM
|
|
|
151
154
|
|
|
152
155
|
##
|
|
153
156
|
# Called before a context transformer rewrites a prompt.
|
|
154
|
-
# @param [LLM::
|
|
155
|
-
# @param [#call] transformer
|
|
157
|
+
# @param [LLM::Transformer] transformer
|
|
156
158
|
# @return [nil]
|
|
157
|
-
def on_transform(
|
|
159
|
+
def on_transform(transformer)
|
|
158
160
|
nil
|
|
159
161
|
end
|
|
160
162
|
|
|
161
163
|
##
|
|
162
164
|
# Called after a context transformer finishes rewriting a prompt.
|
|
163
|
-
# @param [LLM::
|
|
164
|
-
# @param [#call] transformer
|
|
165
|
+
# @param [LLM::Transformer] transformer
|
|
165
166
|
# @return [nil]
|
|
166
|
-
def on_transform_finish(
|
|
167
|
+
def on_transform_finish(transformer)
|
|
167
168
|
nil
|
|
168
169
|
end
|
|
169
170
|
|
|
@@ -183,6 +184,33 @@ module LLM
|
|
|
183
184
|
nil
|
|
184
185
|
end
|
|
185
186
|
|
|
187
|
+
##
|
|
188
|
+
# Called when a request is rate limited and will be retried.
|
|
189
|
+
# @param [LLM::RateLimitError] error
|
|
190
|
+
# @return [nil]
|
|
191
|
+
def on_rate_limit(error)
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
##
|
|
196
|
+
# Called before a skill's subagent runs.
|
|
197
|
+
# @param [LLM::Skill] skill
|
|
198
|
+
# @return [nil]
|
|
199
|
+
def on_skill_call(skill)
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
##
|
|
204
|
+
# Called after a skill's subagent finishes.
|
|
205
|
+
# @param [LLM::Agent] agent
|
|
206
|
+
# The subagent that ran the skill
|
|
207
|
+
# @param [LLM::Skill] skill
|
|
208
|
+
# @param [LLM::Response, nil] result
|
|
209
|
+
# @return [nil]
|
|
210
|
+
def on_skill_return(agent, skill, result)
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
|
|
186
214
|
# @endgroup
|
|
187
215
|
|
|
188
216
|
# @group Finders
|