llm.rb 13.0.0 → 14.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 +505 -14
- data/README.md +484 -50
- data/bin/llm.rb +148 -0
- data/data/anthropic.json +206 -263
- data/data/bedrock.json +2138 -1860
- data/data/deepinfra.json +1003 -624
- data/data/deepseek.json +38 -34
- data/data/google.json +1079 -371
- data/data/mistral.json +448 -368
- data/data/moonshot.json +384 -0
- data/data/openai.json +974 -1343
- data/data/xai.json +154 -126
- data/data/zai.json +191 -191
- data/lib/llm/agent.rb +123 -20
- data/lib/llm/context.rb +71 -88
- data/lib/llm/cost.rb +23 -17
- data/lib/llm/error.rb +0 -8
- data/lib/llm/function/array.rb +3 -3
- 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 +2 -0
- data/lib/llm/function/ractor/task.rb +2 -0
- data/lib/llm/function/sequential/group.rb +4 -1
- data/lib/llm/function/sequential/task.rb +1 -1
- data/lib/llm/function/task.rb +4 -0
- data/lib/llm/function/thread/task.rb +2 -0
- data/lib/llm/function.rb +33 -6
- 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/provider.rb +36 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
- data/lib/llm/providers/anthropic.rb +2 -9
- data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -0
- data/lib/llm/providers/bedrock.rb +1 -8
- data/lib/llm/providers/google/stream_parser.rb +1 -0
- data/lib/llm/providers/google.rb +1 -8
- data/lib/llm/providers/mistral.rb +1 -1
- data/lib/llm/providers/moonshot.rb +76 -0
- data/lib/llm/providers/ollama.rb +2 -9
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
- data/lib/llm/providers/openai/responses.rb +7 -9
- data/lib/llm/providers/openai/stream_parser.rb +1 -0
- data/lib/llm/providers/openai.rb +4 -11
- data/lib/llm/repl/bar.rb +4 -3
- data/lib/llm/repl/{transcript.rb → buffer.rb} +69 -29
- data/lib/llm/repl/color.rb +78 -0
- data/lib/llm/repl/command.rb +12 -5
- data/lib/llm/repl/commands/compact.rb +2 -2
- data/lib/llm/repl/commands/help.rb +3 -5
- data/lib/llm/repl/input/char.rb +46 -0
- data/lib/llm/repl/input/row.rb +39 -0
- data/lib/llm/repl/input.rb +251 -66
- data/lib/llm/repl/markdown/table.rb +11 -3
- data/lib/llm/repl/markdown.rb +34 -8
- data/lib/llm/repl/node.rb +37 -0
- data/lib/llm/repl/status.rb +42 -7
- data/lib/llm/repl/stream.rb +18 -6
- data/lib/llm/repl/walker.rb +3 -2
- data/lib/llm/repl/window.rb +54 -35
- data/lib/llm/repl.rb +74 -32
- data/lib/llm/skill.rb +20 -4
- data/lib/llm/stream.rb +8 -7
- data/lib/llm/tool.rb +29 -0
- data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
- data/lib/llm/tools/git.rb +3 -0
- data/lib/llm/tools/mkdir.rb +3 -0
- data/lib/llm/tools/rg.rb +3 -0
- data/lib/llm/tools/ruby.rb +46 -0
- data/lib/llm/tools/shell.rb +3 -0
- data/lib/llm/tracer/pretty_logger.rb +127 -0
- data/lib/llm/tracer.rb +1 -0
- data/lib/llm/transformer/null.rb +21 -0
- data/lib/llm/transformer.rb +55 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +12 -2
- data/llm.gemspec +9 -2
- data/resources/deepdive/advanced/cancellation.md +74 -0
- data/resources/deepdive/advanced/compaction.md +83 -0
- data/resources/deepdive/advanced/context.md +267 -0
- data/resources/deepdive/advanced/guard.md +371 -0
- data/resources/deepdive/advanced/tracer.md +180 -0
- data/resources/deepdive/advanced/transformer.md +67 -0
- data/resources/deepdive/advanced/transports.md +45 -0
- data/resources/deepdive/everything_else/audio.md +122 -0
- data/resources/deepdive/everything_else/cost.md +99 -0
- data/resources/deepdive/everything_else/images.md +89 -0
- data/resources/deepdive/everything_else/object.md +108 -0
- data/resources/deepdive/everything_else/ocr.md +48 -0
- data/resources/deepdive/fundamentals/agents.md +202 -0
- data/resources/deepdive/fundamentals/builtin_tools.md +191 -0
- data/resources/deepdive/fundamentals/concurrency.md +104 -0
- data/resources/deepdive/fundamentals/database.md +449 -0
- data/resources/deepdive/fundamentals/embeddings.md +157 -0
- data/resources/deepdive/fundamentals/repl.md +87 -0
- data/resources/deepdive/fundamentals/schema.md +61 -0
- data/resources/deepdive/fundamentals/skills.md +106 -0
- data/resources/deepdive/fundamentals/stream.md +110 -0
- data/resources/deepdive/fundamentals/tools.md +265 -0
- data/resources/deepdive/protocols/a2a.md +106 -0
- data/resources/deepdive/protocols/mcp.md +111 -0
- data/resources/deepdive.md +58 -1792
- metadata +51 -7
- data/lib/llm/loop_guard.rb +0 -107
data/lib/llm/repl/status.rb
CHANGED
|
@@ -4,22 +4,31 @@ 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
|
-
# @param [LLM::
|
|
15
|
+
# @param [LLM::Repl] repl
|
|
11
16
|
# @return [LLM::Repl::Status]
|
|
12
|
-
def initialize(
|
|
13
|
-
@agent = agent
|
|
14
|
-
@provider = agent.llm.name
|
|
15
|
-
@
|
|
17
|
+
def initialize(repl)
|
|
18
|
+
@agent = repl.agent
|
|
19
|
+
@provider = @agent.llm.name
|
|
20
|
+
@nodes = [Node.new("idle")]
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
##
|
|
19
24
|
# @return [String]
|
|
20
25
|
def context_bar
|
|
21
26
|
LLM::Repl::Bar.new(
|
|
22
|
-
|
|
27
|
+
##
|
|
28
|
+
# After compaction the used context is unknown until the next
|
|
29
|
+
# response, so the bar renders an unknown state instead of a
|
|
30
|
+
# stale percentage.
|
|
31
|
+
used: @agent.compacted? ? nil : @agent.usage.total_tokens,
|
|
23
32
|
total: @agent.context_window
|
|
24
33
|
).to_s
|
|
25
34
|
end
|
|
@@ -30,12 +39,38 @@ class LLM::Repl
|
|
|
30
39
|
"$#{@agent.cost}"
|
|
31
40
|
end
|
|
32
41
|
|
|
42
|
+
##
|
|
43
|
+
# Sets the status text.
|
|
44
|
+
# @param [String, LLM::Repl::Node, Array<String, LLM::Repl::Node>] value
|
|
45
|
+
# @return [Array<LLM::Repl::Node>]
|
|
46
|
+
def text=(value)
|
|
47
|
+
@nodes =
|
|
48
|
+
case value
|
|
49
|
+
when Node then [value]
|
|
50
|
+
when Array then value.map { |node| Node === node ? node : Node.new(node) }
|
|
51
|
+
else [Node.new(value)]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
33
55
|
##
|
|
34
56
|
# @return [String]
|
|
35
|
-
|
|
57
|
+
def text
|
|
58
|
+
@nodes.map(&:text).join
|
|
59
|
+
end
|
|
36
60
|
|
|
37
61
|
##
|
|
38
62
|
# @return [String]
|
|
39
63
|
alias_method :to_s, :text
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# The nodes making up the status line.
|
|
67
|
+
# @return [Array<LLM::Repl::Node>]
|
|
68
|
+
def nodes
|
|
69
|
+
if @agent.compacted?
|
|
70
|
+
[Node.new("Context compacted")]
|
|
71
|
+
else
|
|
72
|
+
@nodes
|
|
73
|
+
end
|
|
74
|
+
end
|
|
40
75
|
end
|
|
41
76
|
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,26 +35,33 @@ 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
|
##
|
|
45
|
-
#
|
|
51
|
+
# Clear the accumulated buffer
|
|
46
52
|
# @return [void]
|
|
47
|
-
def
|
|
53
|
+
def clear
|
|
48
54
|
@buffer.clear
|
|
49
55
|
end
|
|
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
|
@@ -11,22 +11,22 @@ class LLM::Repl
|
|
|
11
11
|
attr_reader :status
|
|
12
12
|
|
|
13
13
|
##
|
|
14
|
-
# @return [LLM::Repl::
|
|
15
|
-
attr_reader :
|
|
14
|
+
# @return [LLM::Repl::Buffer]
|
|
15
|
+
attr_reader :buffer
|
|
16
16
|
|
|
17
17
|
##
|
|
18
18
|
# @return [LLM::Repl::Input]
|
|
19
19
|
attr_reader :input
|
|
20
20
|
|
|
21
21
|
##
|
|
22
|
-
# @param [LLM::Repl
|
|
23
|
-
#
|
|
24
|
-
# @param [LLM::Repl::Input] input
|
|
22
|
+
# @param [LLM::Repl] repl
|
|
23
|
+
# A read-eval-print loop.
|
|
25
24
|
# @return [LLM::Repl::Window]
|
|
26
|
-
def initialize(
|
|
27
|
-
@
|
|
28
|
-
@
|
|
29
|
-
@
|
|
25
|
+
def initialize(repl)
|
|
26
|
+
@repl = repl
|
|
27
|
+
@status = repl.status
|
|
28
|
+
@buffer = repl.buffer
|
|
29
|
+
@input = repl.input
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
##
|
|
@@ -34,6 +34,7 @@ class LLM::Repl
|
|
|
34
34
|
# @return [void]
|
|
35
35
|
def open
|
|
36
36
|
Curses.init_screen
|
|
37
|
+
Color.enable
|
|
37
38
|
Curses.cbreak
|
|
38
39
|
Curses.noecho
|
|
39
40
|
Curses.stdscr.keypad(true)
|
|
@@ -48,7 +49,7 @@ class LLM::Repl
|
|
|
48
49
|
def redraw
|
|
49
50
|
draw_status(offset: input.height + 1)
|
|
50
51
|
draw_divider(offset: 5)
|
|
51
|
-
|
|
52
|
+
draw_buffer(offset: 0)
|
|
52
53
|
draw_input
|
|
53
54
|
Curses.refresh
|
|
54
55
|
end
|
|
@@ -59,6 +60,12 @@ class LLM::Repl
|
|
|
59
60
|
[Curses.lines - (input.height + 4), 1].max
|
|
60
61
|
end
|
|
61
62
|
|
|
63
|
+
##
|
|
64
|
+
# @return [Integer]
|
|
65
|
+
def columns
|
|
66
|
+
Curses.cols
|
|
67
|
+
end
|
|
68
|
+
|
|
62
69
|
##
|
|
63
70
|
# @return [Object]
|
|
64
71
|
def getch
|
|
@@ -86,27 +93,56 @@ class LLM::Repl
|
|
|
86
93
|
##
|
|
87
94
|
# @return [void]
|
|
88
95
|
def scroll_up
|
|
89
|
-
|
|
96
|
+
buffer.scroll_up(rows)
|
|
90
97
|
end
|
|
91
98
|
|
|
92
99
|
##
|
|
93
100
|
# @return [void]
|
|
94
101
|
def scroll_down
|
|
95
|
-
|
|
102
|
+
buffer.scroll_down
|
|
96
103
|
end
|
|
97
104
|
|
|
98
105
|
##
|
|
99
106
|
# @return [void]
|
|
100
107
|
def scroll_to_bottom
|
|
101
|
-
|
|
108
|
+
buffer.scroll_to_bottom
|
|
102
109
|
end
|
|
103
110
|
|
|
104
111
|
private
|
|
105
112
|
|
|
113
|
+
def draw_buffer(offset:)
|
|
114
|
+
rows = buffer.visible(self.rows)
|
|
115
|
+
rows.each.with_index(offset) do |row, index|
|
|
116
|
+
Curses.setpos(index, 0)
|
|
117
|
+
Curses.clrtoeol
|
|
118
|
+
Curses.setpos(index, gutter)
|
|
119
|
+
width = 0
|
|
120
|
+
row.each do |chunk|
|
|
121
|
+
remaining = buffer.width - width
|
|
122
|
+
break if remaining <= 0
|
|
123
|
+
text, attrs = chunk.text.to_s, chunk.attrs
|
|
124
|
+
clipped = text[0, remaining]
|
|
125
|
+
Curses.attron(attrs) if attrs
|
|
126
|
+
Curses.addstr(clipped)
|
|
127
|
+
Curses.attroff(attrs) if attrs
|
|
128
|
+
width += clipped.length
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
last_drawn = offset + rows.size
|
|
132
|
+
(last_drawn...self.rows).each do |line|
|
|
133
|
+
Curses.setpos(line, 0)
|
|
134
|
+
Curses.clrtoeol
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
106
138
|
def draw_status(offset:)
|
|
107
139
|
Curses.setpos(Curses.lines - offset, 0)
|
|
108
140
|
Curses.clrtoeol
|
|
109
|
-
|
|
141
|
+
status.nodes.each do |node|
|
|
142
|
+
Curses.attron(node.attrs) if node.attrs
|
|
143
|
+
Curses.addstr(node.text)
|
|
144
|
+
Curses.attroff(node.attrs) if node.attrs
|
|
145
|
+
end
|
|
110
146
|
context = status.context_bar
|
|
111
147
|
Curses.setpos(Curses.lines - offset, [(columns - context.length) / 2, 0].max)
|
|
112
148
|
Curses.addstr(context)
|
|
@@ -132,29 +168,12 @@ class LLM::Repl
|
|
|
132
168
|
Curses.setpos((Curses.lines - input.height) + line, col)
|
|
133
169
|
end
|
|
134
170
|
|
|
135
|
-
def draw_transcript(offset:)
|
|
136
|
-
rows = transcript.visible(self.rows)
|
|
137
|
-
rows.each.with_index(offset) do |row, index|
|
|
138
|
-
Curses.setpos(index, 0)
|
|
139
|
-
Curses.clrtoeol
|
|
140
|
-
row.each do |chunk|
|
|
141
|
-
text, attrs = chunk.values_at(:text, :attrs)
|
|
142
|
-
Curses.attron(attrs) if attrs
|
|
143
|
-
Curses.addstr(text)
|
|
144
|
-
Curses.attroff(attrs) if attrs
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
last_drawn = offset + rows.size
|
|
148
|
-
(last_drawn...self.rows).each do |line|
|
|
149
|
-
Curses.setpos(line, 0)
|
|
150
|
-
Curses.clrtoeol
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
171
|
##
|
|
172
|
+
# 20% offset that occupies the left margin and
|
|
173
|
+
# helps center {LLM::Repl::Buffer LLM::Repl::Buffer}.
|
|
155
174
|
# @return [Integer]
|
|
156
|
-
def
|
|
157
|
-
|
|
175
|
+
def gutter
|
|
176
|
+
(columns * 0.2).floor
|
|
158
177
|
end
|
|
159
178
|
end
|
|
160
179
|
end
|
data/lib/llm/repl.rb
CHANGED
|
@@ -15,18 +15,20 @@ module LLM
|
|
|
15
15
|
# task correctly, or for it to correct course after a
|
|
16
16
|
# mistake was made.
|
|
17
17
|
class Repl
|
|
18
|
+
require_relative "repl/color"
|
|
18
19
|
require_relative "repl/window"
|
|
19
20
|
require_relative "repl/status"
|
|
20
|
-
require_relative "repl/
|
|
21
|
+
require_relative "repl/buffer"
|
|
21
22
|
require_relative "repl/input"
|
|
22
23
|
require_relative "repl/bar"
|
|
23
24
|
require_relative "repl/stream"
|
|
25
|
+
require_relative "repl/node"
|
|
24
26
|
require_relative "repl/markdown"
|
|
25
27
|
require_relative "repl/command"
|
|
26
28
|
require_relative "repl/walker"
|
|
27
29
|
|
|
28
30
|
attr_reader :agent, :provider, :stream,
|
|
29
|
-
:status, :
|
|
31
|
+
:status, :buffer, :input,
|
|
30
32
|
:window, :tools, :thread,
|
|
31
33
|
:name, :path
|
|
32
34
|
|
|
@@ -44,12 +46,13 @@ module LLM
|
|
|
44
46
|
def initialize(agent:, name: nil, tools: [], skills: [], path: nil)
|
|
45
47
|
@path = path
|
|
46
48
|
@name = name || "agent"
|
|
49
|
+
@sender = "You"
|
|
47
50
|
@agent = configure(agent:, path:)
|
|
48
51
|
@provider = agent.llm.name
|
|
49
|
-
@status = Status.new(@agent)
|
|
50
|
-
@transcript = Transcript.new
|
|
51
52
|
@input = Input.new(self, height: 3)
|
|
52
|
-
@
|
|
53
|
+
@buffer = Buffer.new(self)
|
|
54
|
+
@status = Status.new(self)
|
|
55
|
+
@window = Window.new(self)
|
|
53
56
|
@thread = nil
|
|
54
57
|
@queue = Queue.new
|
|
55
58
|
@stream = Stream.new(self, @queue)
|
|
@@ -70,6 +73,7 @@ module LLM
|
|
|
70
73
|
def start
|
|
71
74
|
window.open do
|
|
72
75
|
catch(:exit) do
|
|
76
|
+
write tree(agent.messages)
|
|
73
77
|
loop do
|
|
74
78
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
75
79
|
case input.on_char(window, input.paste? ? window.read_paste : window.getch, now)
|
|
@@ -86,22 +90,32 @@ module LLM
|
|
|
86
90
|
end
|
|
87
91
|
|
|
88
92
|
##
|
|
89
|
-
# @param [String] chars
|
|
93
|
+
# @param [String, Array] chars
|
|
90
94
|
# @param [Object] attrs
|
|
95
|
+
# @param [Symbol] method (:append, :replace)
|
|
91
96
|
# @return [void]
|
|
92
|
-
def write(chars, attrs = nil)
|
|
93
|
-
|
|
97
|
+
def write(chars, attrs = nil, method: :append)
|
|
98
|
+
buffer.write(chars, attrs, method:)
|
|
94
99
|
window.redraw
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
##
|
|
98
|
-
# @param [String]
|
|
103
|
+
# @param [String] user
|
|
104
|
+
# @param [String] content
|
|
99
105
|
# @return [void]
|
|
100
|
-
def
|
|
101
|
-
|
|
106
|
+
def write_message(user, content, method: :append)
|
|
107
|
+
buffer.write_message(user, content, method:)
|
|
102
108
|
window.redraw
|
|
103
109
|
end
|
|
104
110
|
|
|
111
|
+
##
|
|
112
|
+
# Returns an AST
|
|
113
|
+
# @param [String] chars
|
|
114
|
+
# @return [Array<Node>]
|
|
115
|
+
def markdown(chars)
|
|
116
|
+
LLM::Repl::Markdown.new(chars, buffer.width).ast
|
|
117
|
+
end
|
|
118
|
+
|
|
105
119
|
##
|
|
106
120
|
# @param [String] value
|
|
107
121
|
# @return [void]
|
|
@@ -112,12 +126,41 @@ module LLM
|
|
|
112
126
|
|
|
113
127
|
##
|
|
114
128
|
# @return [String]
|
|
115
|
-
def
|
|
116
|
-
"
|
|
129
|
+
def think_text
|
|
130
|
+
" 🧠 • Thinking • Esc to cancel"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
##
|
|
134
|
+
# @return [String]
|
|
135
|
+
def connect_text
|
|
136
|
+
" 🌐 • Connecting • Esc to cancel"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
##
|
|
140
|
+
# Returns a label for the person who sent a message.
|
|
141
|
+
# @return [String]
|
|
142
|
+
def sender
|
|
143
|
+
@sender
|
|
117
144
|
end
|
|
118
145
|
|
|
119
146
|
private
|
|
120
147
|
|
|
148
|
+
##
|
|
149
|
+
# @param [LLM::Buffer] messages
|
|
150
|
+
# A message buffer
|
|
151
|
+
# @return [Array<Node>]
|
|
152
|
+
def tree(messages)
|
|
153
|
+
messages.flat_map do |message|
|
|
154
|
+
next if message.tool_call? || message.tool_return?
|
|
155
|
+
user = message.assistant? ? name : sender
|
|
156
|
+
[
|
|
157
|
+
Node.new("#{user}:\n", Curses::A_BOLD | Color.blue),
|
|
158
|
+
*markdown(message.content),
|
|
159
|
+
Node.new("\n\n")
|
|
160
|
+
]
|
|
161
|
+
end.compact
|
|
162
|
+
end
|
|
163
|
+
|
|
121
164
|
##
|
|
122
165
|
# @param [LLM::Agent] agent
|
|
123
166
|
# An agent
|
|
@@ -151,15 +194,14 @@ module LLM
|
|
|
151
194
|
case on_text(text)
|
|
152
195
|
in [:command, Command => command, Hash => parameters]
|
|
153
196
|
command.call(**parameters)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
197
|
+
write("\n")
|
|
198
|
+
in [:error, String => user, String => text]
|
|
199
|
+
write_message(user, text)
|
|
200
|
+
write("\n")
|
|
157
201
|
in [:input, String => text]
|
|
158
202
|
window.scroll_to_bottom
|
|
159
|
-
status.text =
|
|
160
|
-
|
|
161
|
-
markdown(text)
|
|
162
|
-
write("\n#{name}: ", Curses::A_BOLD)
|
|
203
|
+
status.text = connect_text
|
|
204
|
+
write_message(sender, markdown(text))
|
|
163
205
|
@thread = Thread.new do
|
|
164
206
|
@queue << [:start]
|
|
165
207
|
agent.talk(text, tools:, stream:)
|
|
@@ -214,29 +256,29 @@ module LLM
|
|
|
214
256
|
type, value = @queue.pop(true)
|
|
215
257
|
case type
|
|
216
258
|
when :start
|
|
217
|
-
|
|
218
|
-
stream.
|
|
259
|
+
buffer.open
|
|
260
|
+
stream.clear
|
|
219
261
|
when :stream
|
|
220
|
-
|
|
262
|
+
status.text = think_text if stream.tools.empty?
|
|
263
|
+
write_message name, markdown(value), method: :replace
|
|
221
264
|
when :status
|
|
222
265
|
self.status = value
|
|
223
266
|
when :done
|
|
224
267
|
status.text = "idle"
|
|
225
|
-
|
|
226
|
-
transcript.finish
|
|
268
|
+
buffer.close
|
|
227
269
|
@thread = nil
|
|
270
|
+
write("\n")
|
|
228
271
|
when :cancel
|
|
272
|
+
stream.tools.clear
|
|
273
|
+
buffer.close
|
|
229
274
|
status.text = "Idle"
|
|
230
|
-
|
|
231
|
-
write("request cancelled!")
|
|
232
|
-
write("\n\n")
|
|
233
|
-
transcript.finish
|
|
275
|
+
write_message(name, "Request cancelled")
|
|
234
276
|
@thread = nil
|
|
277
|
+
write("\n")
|
|
235
278
|
when :error
|
|
236
|
-
# Do this better
|
|
237
279
|
status.text = "error"
|
|
238
|
-
|
|
239
|
-
|
|
280
|
+
write_message(name, "(#{value.class}): #{value.message}")
|
|
281
|
+
buffer.close
|
|
240
282
|
@thread = nil
|
|
241
283
|
end
|
|
242
284
|
end
|
data/lib/llm/skill.rb
CHANGED
|
@@ -7,6 +7,11 @@ module LLM
|
|
|
7
7
|
# Skills can expose themselves as normal {LLM::Tool LLM::Tool} classes through
|
|
8
8
|
# {#to_tool}. This keeps skills on the same execution path as local tools.
|
|
9
9
|
class Skill
|
|
10
|
+
##
|
|
11
|
+
# @api private
|
|
12
|
+
File = ::File
|
|
13
|
+
private_constant :File
|
|
14
|
+
|
|
10
15
|
##
|
|
11
16
|
# Load a skill from a directory.
|
|
12
17
|
# @param [String, Pathname] path
|
|
@@ -63,8 +68,14 @@ module LLM
|
|
|
63
68
|
# Load and parse the skill.
|
|
64
69
|
# @return [LLM::Skill]
|
|
65
70
|
def load!
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
if File.file?(@path)
|
|
72
|
+
parse(File.read(@path))
|
|
73
|
+
elsif File.directory?(@path)
|
|
74
|
+
path = File.join(@path, "SKILL.md")
|
|
75
|
+
parse(File.read(path))
|
|
76
|
+
else
|
|
77
|
+
raise LLM::Error, "'#{@path}' is neither a file or directory"
|
|
78
|
+
end
|
|
68
79
|
self
|
|
69
80
|
end
|
|
70
81
|
|
|
@@ -103,7 +114,8 @@ module LLM
|
|
|
103
114
|
end
|
|
104
115
|
|
|
105
116
|
##
|
|
106
|
-
# Returns true when a skill should inherit tools
|
|
117
|
+
# Returns true when a skill should inherit tools
|
|
118
|
+
# from its parent
|
|
107
119
|
# @return [Boolean]
|
|
108
120
|
def inherit_tools?
|
|
109
121
|
@inherit_tools
|
|
@@ -138,7 +150,11 @@ module LLM
|
|
|
138
150
|
def parse_tools(tools)
|
|
139
151
|
case tools
|
|
140
152
|
when String
|
|
141
|
-
|
|
153
|
+
case tools
|
|
154
|
+
when "inherit" then [true, []]
|
|
155
|
+
when "all", "*" then [false, LLM::Tool.registry]
|
|
156
|
+
else raise_invalid_error!(tools)
|
|
157
|
+
end
|
|
142
158
|
when Array
|
|
143
159
|
[false, [*@frontmatter.tools].map { LLM::Tool.find_by_name!(_1) }]
|
|
144
160
|
when NilClass
|
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
|
|
data/lib/llm/tool.rb
CHANGED
|
@@ -150,6 +150,35 @@ class LLM::Tool
|
|
|
150
150
|
end
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
+
##
|
|
154
|
+
# Assign multiple tool properties at once.
|
|
155
|
+
# @example
|
|
156
|
+
# class Tool < LLM::Tool
|
|
157
|
+
# set name: "my-tool",
|
|
158
|
+
# description: "my tool does this and that",
|
|
159
|
+
# parameters: [
|
|
160
|
+
# [:name, String , "this and that"],
|
|
161
|
+
# [:age , Integer, "this and that"]
|
|
162
|
+
# ],
|
|
163
|
+
# required: %i[name],
|
|
164
|
+
# defaults: {age: 42}
|
|
165
|
+
# end
|
|
166
|
+
# @param [Hash] properties
|
|
167
|
+
# @return [void]
|
|
168
|
+
def self.set(properties)
|
|
169
|
+
properties.each do
|
|
170
|
+
if _1.to_s == "parameters"
|
|
171
|
+
_2.each { |attributes| parameter(*attributes) }
|
|
172
|
+
else
|
|
173
|
+
if respond_to?(_1)
|
|
174
|
+
public_send(_1, _2)
|
|
175
|
+
else
|
|
176
|
+
raise KeyError, "key not found: #{_1}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
153
182
|
##
|
|
154
183
|
# Returns (or sets) the tool name
|
|
155
184
|
# @param [String, nil] name The tool name
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
class LLM::Tool
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::Tool::
|
|
5
|
+
# The {LLM::Tool::EditFile LLM::Tool::EditFile} class
|
|
6
6
|
# implements a tool that can substitute one piece of
|
|
7
7
|
# text for another piece of text in a given file.
|
|
8
|
-
class
|
|
9
|
-
name "
|
|
8
|
+
class EditFile < self
|
|
9
|
+
name "edit-file"
|
|
10
10
|
description "Replace an exact snippet in a file"
|
|
11
11
|
parameter :path, String, "Path to file"
|
|
12
12
|
parameter :before, String, "Exact text to replace"
|
data/lib/llm/tools/git.rb
CHANGED
data/lib/llm/tools/mkdir.rb
CHANGED
data/lib/llm/tools/rg.rb
CHANGED