llm.rb 13.1.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 +320 -0
- data/README.md +340 -31
- data/bin/llm.rb +36 -12
- 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 +47 -14
- 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/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 +32 -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/provider.rb +36 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
- data/lib/llm/providers/anthropic.rb +1 -8
- 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/moonshot.rb +76 -0
- data/lib/llm/providers/ollama.rb +1 -8
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
- data/lib/llm/providers/openai/responses.rb +6 -8
- data/lib/llm/providers/openai/stream_parser.rb +1 -0
- data/lib/llm/providers/openai.rb +3 -10
- data/lib/llm/repl/bar.rb +4 -3
- data/lib/llm/repl/buffer.rb +42 -15
- data/lib/llm/repl/color.rb +78 -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 +251 -66
- data/lib/llm/repl/markdown/table.rb +6 -2
- data/lib/llm/repl/markdown.rb +31 -5
- data/lib/llm/repl/status.rb +38 -3
- data/lib/llm/repl/stream.rb +16 -4
- data/lib/llm/repl/walker.rb +3 -2
- data/lib/llm/repl/window.rb +25 -5
- data/lib/llm/repl.rb +29 -13
- data/lib/llm/stream.rb +8 -7
- data/lib/llm/tool.rb +29 -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 +1 -0
- 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 +7 -1
- metadata +36 -3
- data/lib/llm/loop_guard.rb +0 -107
data/lib/llm/repl/markdown.rb
CHANGED
|
@@ -11,6 +11,26 @@ class LLM::Repl
|
|
|
11
11
|
require_relative "markdown/table"
|
|
12
12
|
include Table
|
|
13
13
|
|
|
14
|
+
##
|
|
15
|
+
# Kramdown goes a bit beyond a standard markdown
|
|
16
|
+
# parser by representing certain characters or
|
|
17
|
+
# character sequences as distinct node types that are
|
|
18
|
+
# represented by `:typographic_sym`, and `:smart_quote`.
|
|
19
|
+
#
|
|
20
|
+
# The node's value maps back to one of the keys in
|
|
21
|
+
# this Hash, and the values are unicode characters
|
|
22
|
+
# that provide a visual representation of the node.
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
25
|
+
SYMBOLS = {
|
|
26
|
+
hellip: "…",
|
|
27
|
+
ndash: "–", mdash: "—",
|
|
28
|
+
laquo: "«", raquo: "»",
|
|
29
|
+
laquo_space: "« ", raquo_space: "» ",
|
|
30
|
+
lsquo: "‘", rsquo: "’",
|
|
31
|
+
ldquo: "“", rdquo: "”"
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
##
|
|
15
35
|
# @param [String] text
|
|
16
36
|
# @param [Integer] width
|
|
@@ -65,17 +85,19 @@ class LLM::Repl
|
|
|
65
85
|
emit("\n\n", attrs)
|
|
66
86
|
when :header
|
|
67
87
|
emit("\n", attrs)
|
|
68
|
-
node.children.each { walk(_1, Curses::A_BOLD) }
|
|
88
|
+
node.children.each { walk(_1, Curses::A_BOLD | Color.white) }
|
|
69
89
|
emit("\n", attrs)
|
|
70
90
|
when :strong
|
|
71
|
-
node.children.each { walk(_1, Curses::A_BOLD) }
|
|
91
|
+
node.children.each { walk(_1, Curses::A_BOLD | Color.white) }
|
|
72
92
|
when :em
|
|
73
93
|
node.children.each { walk(_1, Curses::A_UNDERLINE) }
|
|
74
94
|
when :codespan
|
|
75
|
-
emit(node.value,
|
|
95
|
+
emit(node.value, Color.green)
|
|
76
96
|
when :codeblock
|
|
77
|
-
emit(node.value,
|
|
97
|
+
emit(node.value, Color.green)
|
|
78
98
|
emit("\n\n", attrs)
|
|
99
|
+
when :typographic_sym, :smart_quote
|
|
100
|
+
emit(symbol(node), attrs)
|
|
79
101
|
when :br
|
|
80
102
|
emit("\n", attrs)
|
|
81
103
|
when :ul, :ol
|
|
@@ -100,7 +122,7 @@ class LLM::Repl
|
|
|
100
122
|
emit("─" * @width, attrs)
|
|
101
123
|
emit("\n\n", attrs)
|
|
102
124
|
when :a
|
|
103
|
-
node.children.each { walk(_1, Curses::A_UNDERLINE) }
|
|
125
|
+
node.children.each { walk(_1, Curses::A_UNDERLINE | Color.green) }
|
|
104
126
|
when :img
|
|
105
127
|
emit("[image: #{node.attr["alt"]}]", attrs)
|
|
106
128
|
else
|
|
@@ -111,5 +133,9 @@ class LLM::Repl
|
|
|
111
133
|
def emit(text, attrs)
|
|
112
134
|
@ast.push(Node.new(text.to_s, attrs))
|
|
113
135
|
end
|
|
136
|
+
|
|
137
|
+
def symbol(node)
|
|
138
|
+
SYMBOLS[node.value]
|
|
139
|
+
end
|
|
114
140
|
end
|
|
115
141
|
end
|
data/lib/llm/repl/status.rb
CHANGED
|
@@ -4,6 +4,11 @@ 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
|
##
|
|
@@ -12,14 +17,18 @@ class LLM::Repl
|
|
|
12
17
|
def initialize(repl)
|
|
13
18
|
@agent = repl.agent
|
|
14
19
|
@provider = @agent.llm.name
|
|
15
|
-
@
|
|
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,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
|
@@ -23,6 +23,7 @@ class LLM::Repl
|
|
|
23
23
|
# A read-eval-print loop.
|
|
24
24
|
# @return [LLM::Repl::Window]
|
|
25
25
|
def initialize(repl)
|
|
26
|
+
@repl = repl
|
|
26
27
|
@status = repl.status
|
|
27
28
|
@buffer = repl.buffer
|
|
28
29
|
@input = repl.input
|
|
@@ -33,6 +34,7 @@ class LLM::Repl
|
|
|
33
34
|
# @return [void]
|
|
34
35
|
def open
|
|
35
36
|
Curses.init_screen
|
|
37
|
+
Color.enable
|
|
36
38
|
Curses.cbreak
|
|
37
39
|
Curses.noecho
|
|
38
40
|
Curses.stdscr.keypad(true)
|
|
@@ -58,6 +60,12 @@ class LLM::Repl
|
|
|
58
60
|
[Curses.lines - (input.height + 4), 1].max
|
|
59
61
|
end
|
|
60
62
|
|
|
63
|
+
##
|
|
64
|
+
# @return [Integer]
|
|
65
|
+
def columns
|
|
66
|
+
Curses.cols
|
|
67
|
+
end
|
|
68
|
+
|
|
61
69
|
##
|
|
62
70
|
# @return [Object]
|
|
63
71
|
def getch
|
|
@@ -107,11 +115,17 @@ class LLM::Repl
|
|
|
107
115
|
rows.each.with_index(offset) do |row, index|
|
|
108
116
|
Curses.setpos(index, 0)
|
|
109
117
|
Curses.clrtoeol
|
|
118
|
+
Curses.setpos(index, gutter)
|
|
119
|
+
width = 0
|
|
110
120
|
row.each do |chunk|
|
|
111
|
-
|
|
121
|
+
remaining = buffer.width - width
|
|
122
|
+
break if remaining <= 0
|
|
123
|
+
text, attrs = chunk.text.to_s, chunk.attrs
|
|
124
|
+
clipped = text[0, remaining]
|
|
112
125
|
Curses.attron(attrs) if attrs
|
|
113
|
-
Curses.addstr(
|
|
126
|
+
Curses.addstr(clipped)
|
|
114
127
|
Curses.attroff(attrs) if attrs
|
|
128
|
+
width += clipped.length
|
|
115
129
|
end
|
|
116
130
|
end
|
|
117
131
|
last_drawn = offset + rows.size
|
|
@@ -124,7 +138,11 @@ class LLM::Repl
|
|
|
124
138
|
def draw_status(offset:)
|
|
125
139
|
Curses.setpos(Curses.lines - offset, 0)
|
|
126
140
|
Curses.clrtoeol
|
|
127
|
-
|
|
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
|
|
128
146
|
context = status.context_bar
|
|
129
147
|
Curses.setpos(Curses.lines - offset, [(columns - context.length) / 2, 0].max)
|
|
130
148
|
Curses.addstr(context)
|
|
@@ -151,9 +169,11 @@ class LLM::Repl
|
|
|
151
169
|
end
|
|
152
170
|
|
|
153
171
|
##
|
|
172
|
+
# 20% offset that occupies the left margin and
|
|
173
|
+
# helps center {LLM::Repl::Buffer LLM::Repl::Buffer}.
|
|
154
174
|
# @return [Integer]
|
|
155
|
-
def
|
|
156
|
-
|
|
175
|
+
def gutter
|
|
176
|
+
(columns * 0.2).floor
|
|
157
177
|
end
|
|
158
178
|
end
|
|
159
179
|
end
|
data/lib/llm/repl.rb
CHANGED
|
@@ -15,6 +15,7 @@ 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
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,14 @@ 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
|
-
@status = Status.new(self)
|
|
52
|
-
@buffer = Buffer.new(self)
|
|
53
52
|
@input = Input.new(self, height: 3)
|
|
53
|
+
@buffer = Buffer.new(self)
|
|
54
|
+
@status = Status.new(self)
|
|
54
55
|
@window = Window.new(self)
|
|
55
56
|
@thread = nil
|
|
56
57
|
@queue = Queue.new
|
|
@@ -112,7 +113,7 @@ module LLM
|
|
|
112
113
|
# @param [String] chars
|
|
113
114
|
# @return [Array<Node>]
|
|
114
115
|
def markdown(chars)
|
|
115
|
-
LLM::Repl::Markdown.new(chars, width).ast
|
|
116
|
+
LLM::Repl::Markdown.new(chars, buffer.width).ast
|
|
116
117
|
end
|
|
117
118
|
|
|
118
119
|
##
|
|
@@ -125,8 +126,21 @@ module LLM
|
|
|
125
126
|
|
|
126
127
|
##
|
|
127
128
|
# @return [String]
|
|
128
|
-
def
|
|
129
|
-
"
|
|
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
|
|
130
144
|
end
|
|
131
145
|
|
|
132
146
|
private
|
|
@@ -138,11 +152,11 @@ module LLM
|
|
|
138
152
|
def tree(messages)
|
|
139
153
|
messages.flat_map do |message|
|
|
140
154
|
next if message.tool_call? || message.tool_return?
|
|
141
|
-
user = message.assistant? ? name :
|
|
155
|
+
user = message.assistant? ? name : sender
|
|
142
156
|
[
|
|
143
|
-
Node.new("#{user}
|
|
144
|
-
|
|
145
|
-
Node.new(
|
|
157
|
+
Node.new("#{user}:\n", Curses::A_BOLD | Color.blue),
|
|
158
|
+
*markdown(message.content),
|
|
159
|
+
Node.new("\n\n")
|
|
146
160
|
]
|
|
147
161
|
end.compact
|
|
148
162
|
end
|
|
@@ -186,8 +200,8 @@ module LLM
|
|
|
186
200
|
write("\n")
|
|
187
201
|
in [:input, String => text]
|
|
188
202
|
window.scroll_to_bottom
|
|
189
|
-
status.text =
|
|
190
|
-
write_message(
|
|
203
|
+
status.text = connect_text
|
|
204
|
+
write_message(sender, markdown(text))
|
|
191
205
|
@thread = Thread.new do
|
|
192
206
|
@queue << [:start]
|
|
193
207
|
agent.talk(text, tools:, stream:)
|
|
@@ -245,6 +259,7 @@ module LLM
|
|
|
245
259
|
buffer.open
|
|
246
260
|
stream.clear
|
|
247
261
|
when :stream
|
|
262
|
+
status.text = think_text if stream.tools.empty?
|
|
248
263
|
write_message name, markdown(value), method: :replace
|
|
249
264
|
when :status
|
|
250
265
|
self.status = value
|
|
@@ -254,6 +269,7 @@ module LLM
|
|
|
254
269
|
@thread = nil
|
|
255
270
|
write("\n")
|
|
256
271
|
when :cancel
|
|
272
|
+
stream.tools.clear
|
|
257
273
|
buffer.close
|
|
258
274
|
status.text = "Idle"
|
|
259
275
|
write_message(name, "Request cancelled")
|
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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Transformer
|
|
4
|
+
##
|
|
5
|
+
# An {LLM::Transformer::Null LLM::Transformer::Null} is a
|
|
6
|
+
# transformer that does nothing. It is used as the default when
|
|
7
|
+
# no transformer strategy is configured.
|
|
8
|
+
#
|
|
9
|
+
# It returns the given message unchanged.
|
|
10
|
+
class Null < self
|
|
11
|
+
##
|
|
12
|
+
# @param [LLM::Message] message
|
|
13
|
+
# The message to transform
|
|
14
|
+
# @param [Hash] opts
|
|
15
|
+
# Ignored
|
|
16
|
+
# @return [LLM::Message]
|
|
17
|
+
def call(message:, **opts)
|
|
18
|
+
message
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Transformer LLM::Transformer} is the superclass for
|
|
6
|
+
# message transformers in llm.rb.
|
|
7
|
+
#
|
|
8
|
+
# A transformer is bound to a context and rewrites a single
|
|
9
|
+
# message before it is sent to the provider. Each subclass
|
|
10
|
+
# implements a different transformation: it takes a message in
|
|
11
|
+
# {#call} and returns a message. {LLM::Transformer::Null} is a
|
|
12
|
+
# no-op (the default).
|
|
13
|
+
#
|
|
14
|
+
# A transformer may mutate the message in place or return a new
|
|
15
|
+
# one. Either way, the returned message is what gets sent.
|
|
16
|
+
class Transformer
|
|
17
|
+
require_relative "transformer/null"
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @return [LLM::Context]
|
|
21
|
+
attr_reader :ctx
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @param ctx [LLM::Context, LLM::Agent]
|
|
25
|
+
# @return [LLM::Transformer]
|
|
26
|
+
def initialize(ctx)
|
|
27
|
+
@ctx = LLM::Agent === ctx ? ctx.instance_variable_get(:@ctx) : ctx
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @abstract
|
|
32
|
+
# @param [LLM::Message] message
|
|
33
|
+
# The message to transform
|
|
34
|
+
# @param [Hash] opts
|
|
35
|
+
# Per-call options
|
|
36
|
+
# @return [LLM::Message]
|
|
37
|
+
def call(message:, **opts)
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @return [LLM::Stream]
|
|
45
|
+
def stream
|
|
46
|
+
@ctx.params[:stream]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# @return [LLM::Buffer]
|
|
51
|
+
def messages
|
|
52
|
+
@ctx.messages
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
#
|
|
8
8
|
# @example The three-step workflow
|
|
9
9
|
# require "llm"
|
|
10
|
-
# llm = LLM.deepseek(key: ENV["KEY"])
|
|
10
|
+
# llm = LLM.deepseek(key: ENV["KEY"]) # 1. pick a provider
|
|
11
11
|
# agent = LLM::Agent.new(llm, stream: $stdout) # 2. create an agent
|
|
12
12
|
# agent.talk "Hello world" # 3. talk to it
|
|
13
13
|
#
|
|
@@ -17,6 +17,7 @@ module LLM
|
|
|
17
17
|
require "stringio"
|
|
18
18
|
require "securerandom"
|
|
19
19
|
require_relative "llm/compactor"
|
|
20
|
+
require_relative "llm/transformer"
|
|
20
21
|
require_relative "llm/json_adapter"
|
|
21
22
|
require_relative "llm/tracer"
|
|
22
23
|
require_relative "llm/error"
|
|
@@ -40,7 +41,7 @@ module LLM
|
|
|
40
41
|
require_relative "llm/stream"
|
|
41
42
|
require_relative "llm/provider"
|
|
42
43
|
require_relative "llm/context"
|
|
43
|
-
require_relative "llm/
|
|
44
|
+
require_relative "llm/guard"
|
|
44
45
|
require_relative "llm/agent"
|
|
45
46
|
require_relative "llm/buffer"
|
|
46
47
|
require_relative "llm/function"
|
|
@@ -218,6 +219,15 @@ module LLM
|
|
|
218
219
|
LLM::ZAI.new(**)
|
|
219
220
|
end
|
|
220
221
|
|
|
222
|
+
##
|
|
223
|
+
# @param key (see LLM::Moonshot#initialize)
|
|
224
|
+
# @param host (see LLM::Moonshot#initialize)
|
|
225
|
+
# @return (see LLM::Moonshot#initialize)
|
|
226
|
+
def moonshot(**)
|
|
227
|
+
lock(:require) { require_relative "llm/providers/moonshot" unless defined?(LLM::Moonshot) }
|
|
228
|
+
LLM::Moonshot.new(**)
|
|
229
|
+
end
|
|
230
|
+
|
|
221
231
|
##
|
|
222
232
|
# @param [Hash] opts
|
|
223
233
|
# MCP client options
|
data/llm.gemspec
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
## Cancellation
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
Cancellation lets you abort a model request mid-stream and interrupt
|
|
9
|
+
any tools that are currently executing. The user changes their mind.
|
|
10
|
+
The model goes off course. A tool hangs. In all three cases,
|
|
11
|
+
cancellation stops the work and reclaims the tokens.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
When you want to cancel an active request or tool call, call
|
|
16
|
+
[`LLM::Agent#cancel!`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#cancel!)
|
|
17
|
+
or
|
|
18
|
+
[`LLM::Context#cancel!`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#cancel!)
|
|
19
|
+
from any thread. Two things
|
|
20
|
+
happen at once:
|
|
21
|
+
|
|
22
|
+
[`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
|
|
23
|
+
is raised on the thread where
|
|
24
|
+
[`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk)
|
|
25
|
+
or
|
|
26
|
+
[`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
|
|
27
|
+
is running, so the caller
|
|
28
|
+
can rescue it and know the request was cancelled.
|
|
29
|
+
|
|
30
|
+
At the same time,
|
|
31
|
+
[`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
|
|
32
|
+
is raised on every active tool.
|
|
33
|
+
A tool running in a thread gets it on that thread. A tool in a
|
|
34
|
+
fiber gets it on that fiber. A tool in a forked process gets it
|
|
35
|
+
via a message over the control channel. Pending tools (not yet
|
|
36
|
+
started) are cancelled through
|
|
37
|
+
[`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel)
|
|
38
|
+
without ever being executed.
|
|
39
|
+
|
|
40
|
+
The transport layer also cancels the in-flight HTTP request.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require "llm"
|
|
44
|
+
|
|
45
|
+
llm = LLM.deepseek(key: ENV["DEEPSEEK_SECRET"])
|
|
46
|
+
agent = LLM::Agent.new(llm)
|
|
47
|
+
queue = Queue.new
|
|
48
|
+
|
|
49
|
+
Thread.new do
|
|
50
|
+
queue.push(nil)
|
|
51
|
+
sleep(2)
|
|
52
|
+
agent.cancel!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
queue.pop
|
|
57
|
+
agent.talk "write me a very long poem", stream: $stdout
|
|
58
|
+
rescue LLM::Interrupt
|
|
59
|
+
puts "request cancelled!"
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Why would I use it?
|
|
64
|
+
|
|
65
|
+
Cancellation prevents wasted time and tokens when the model goes
|
|
66
|
+
off course, the user changes their mind, or a tool hangs. A forked
|
|
67
|
+
tool that enters an infinite loop would run forever without it.
|
|
68
|
+
|
|
69
|
+
#### Notes
|
|
70
|
+
|
|
71
|
+
The `:ractor` strategy delivers the interrupt through ractor
|
|
72
|
+
message passing. The `:fork` strategy delivers it via a message
|
|
73
|
+
over the xchan control channel. All other strategies raise the
|
|
74
|
+
exception directly on the executing thread or fiber.
|