llm.rb 12.4.0 → 12.5.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 +240 -0
- data/README.md +38 -8
- data/data/anthropic.json +249 -249
- data/data/bedrock.json +1964 -1964
- data/data/deepinfra.json +589 -589
- data/data/deepseek.json +31 -31
- data/data/google.json +332 -329
- data/data/mistral.json +381 -381
- data/data/openai.json +1133 -1133
- data/data/xai.json +138 -138
- data/data/zai.json +165 -165
- data/lib/llm/agent.rb +3 -1
- data/lib/llm/buffer.rb +15 -0
- data/lib/llm/context.rb +4 -5
- data/lib/llm/function.rb +2 -35
- data/lib/llm/message.rb +12 -2
- data/lib/llm/provider.rb +11 -1
- data/lib/llm/providers/anthropic/stream_parser.rb +3 -12
- data/lib/llm/providers/anthropic.rb +11 -0
- data/lib/llm/providers/bedrock/stream_parser.rb +4 -13
- data/lib/llm/providers/google/stream_parser.rb +3 -12
- data/lib/llm/providers/google.rb +7 -0
- data/lib/llm/providers/mistral.rb +11 -0
- data/lib/llm/providers/ollama/stream_parser.rb +4 -4
- data/lib/llm/providers/ollama.rb +11 -0
- data/lib/llm/providers/openai/responses/stream_parser.rb +4 -14
- data/lib/llm/providers/openai/responses.rb +10 -0
- data/lib/llm/providers/openai/stream_parser.rb +5 -15
- data/lib/llm/providers/openai.rb +11 -0
- data/lib/llm/repl/command.rb +121 -5
- data/lib/llm/repl/commands/exit.rb +4 -0
- data/lib/llm/repl/commands/help.rb +24 -0
- data/lib/llm/repl/input.rb +4 -1
- data/lib/llm/repl/stream.rb +3 -8
- data/lib/llm/repl.rb +58 -12
- data/lib/llm/stream.rb +2 -7
- data/lib/llm/version.rb +1 -1
- data/resources/deepdive.md +1 -1
- metadata +2 -1
data/lib/llm/message.rb
CHANGED
|
@@ -120,7 +120,9 @@ module LLM
|
|
|
120
120
|
# @return [Array<LLM::Function>]
|
|
121
121
|
def functions
|
|
122
122
|
@functions ||= tool_calls.filter_map do |fn|
|
|
123
|
-
function = available_tools.find { _1.name.to_s == fn
|
|
123
|
+
function = available_tools.find { _1.name.to_s == fn.name } ||
|
|
124
|
+
LLM::Function.find_by_name(fn.name) ||
|
|
125
|
+
function_missing(fn.name)
|
|
124
126
|
function = function.dup
|
|
125
127
|
function.tap { _1.id = fn.id }
|
|
126
128
|
function.tap { _1.arguments = fn.arguments }
|
|
@@ -222,10 +224,18 @@ module LLM
|
|
|
222
224
|
end
|
|
223
225
|
|
|
224
226
|
def available_tools
|
|
225
|
-
tools = extra.tools ||
|
|
227
|
+
tools = extra.tools || []
|
|
228
|
+
tools = nil if tools.empty?
|
|
229
|
+
tools = tools || response&.__tools__ || []
|
|
226
230
|
tools.map { _1.respond_to?(:function) ? _1.function : _1 }
|
|
227
231
|
end
|
|
228
232
|
|
|
233
|
+
def function_missing(name)
|
|
234
|
+
LLM.function(name) do |fn|
|
|
235
|
+
fn.define { raise LLM::NoSuchToolError, "tool not found" }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
229
239
|
def content_items
|
|
230
240
|
Array(content)
|
|
231
241
|
end
|
data/lib/llm/provider.rb
CHANGED
|
@@ -26,7 +26,7 @@ class LLM::Provider
|
|
|
26
26
|
# Requires the net-http-persistent gem.
|
|
27
27
|
# @param [LLM::Transport, Class, nil] transport
|
|
28
28
|
# Optional override with any {LLM::Transport} instance or subclass.
|
|
29
|
-
def initialize(key:, host:, port: 443, timeout:
|
|
29
|
+
def initialize(key:, host:, port: 443, timeout: 900, ssl: true, base_path: "", persistent: false, transport: nil)
|
|
30
30
|
@key = key
|
|
31
31
|
@host = host
|
|
32
32
|
@port = port
|
|
@@ -349,6 +349,16 @@ class LLM::Provider
|
|
|
349
349
|
@key != nil && @key.to_s.strip.size > 0
|
|
350
350
|
end
|
|
351
351
|
|
|
352
|
+
##
|
|
353
|
+
# Adapt a {LLM::Function} to the provider-specific tool schema.
|
|
354
|
+
# @abstract
|
|
355
|
+
# @param [LLM::Function] fn
|
|
356
|
+
# @raise [NotImplementedError]
|
|
357
|
+
# @return [Hash]
|
|
358
|
+
def adapt_function(fn)
|
|
359
|
+
raise NotImplementedError
|
|
360
|
+
end
|
|
361
|
+
|
|
352
362
|
private
|
|
353
363
|
|
|
354
364
|
def path(suffix, base_path: true)
|
|
@@ -10,15 +10,11 @@ class LLM::Anthropic
|
|
|
10
10
|
attr_reader :body
|
|
11
11
|
|
|
12
12
|
##
|
|
13
|
-
# @param [
|
|
14
|
-
# A stream sink that implements {#<<} or the {LLM::Stream} interface
|
|
13
|
+
# @param [LLM::Stream] stream
|
|
15
14
|
# @return [LLM::Anthropic::StreamParser]
|
|
16
15
|
def initialize(stream)
|
|
17
16
|
@body = {"role" => "assistant", "content" => []}
|
|
18
17
|
@stream = stream
|
|
19
|
-
@can_emit_content = stream.respond_to?(:on_content)
|
|
20
|
-
@can_emit_tool_call = stream.respond_to?(:on_tool_call)
|
|
21
|
-
@can_push_content = stream.respond_to?(:<<)
|
|
22
18
|
end
|
|
23
19
|
|
|
24
20
|
##
|
|
@@ -91,17 +87,12 @@ class LLM::Anthropic
|
|
|
91
87
|
end
|
|
92
88
|
|
|
93
89
|
def emit_content(value)
|
|
94
|
-
|
|
95
|
-
@stream.on_content(value)
|
|
96
|
-
elsif @can_push_content
|
|
97
|
-
@stream << value
|
|
98
|
-
end
|
|
90
|
+
@stream.on_content(value)
|
|
99
91
|
end
|
|
100
92
|
|
|
101
93
|
def emit_tool(tool)
|
|
102
|
-
return unless @can_emit_tool_call
|
|
103
94
|
function, error = resolve_tool(tool)
|
|
104
|
-
@stream.on_tool_call(function
|
|
95
|
+
error ? @stream.queue << error : @stream.on_tool_call(function)
|
|
105
96
|
end
|
|
106
97
|
|
|
107
98
|
def resolve_tool(tool)
|
|
@@ -81,6 +81,17 @@ module LLM
|
|
|
81
81
|
"assistant"
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
##
|
|
85
|
+
# @param [LLM::Function] fn
|
|
86
|
+
# @return [Hash]
|
|
87
|
+
def adapt_function(fn)
|
|
88
|
+
{
|
|
89
|
+
name: fn.name,
|
|
90
|
+
description: fn.description,
|
|
91
|
+
input_schema: fn.params || {type: "object", properties: {}}
|
|
92
|
+
}.compact
|
|
93
|
+
end
|
|
94
|
+
|
|
84
95
|
##
|
|
85
96
|
# Anthropic expects tool results to be sent as user messages
|
|
86
97
|
# containing `tool_result` content blocks rather than a distinct
|
|
@@ -24,15 +24,11 @@ class LLM::Bedrock
|
|
|
24
24
|
attr_reader :body
|
|
25
25
|
|
|
26
26
|
##
|
|
27
|
-
# @param [
|
|
27
|
+
# @param [LLM::Stream] stream
|
|
28
28
|
def initialize(stream)
|
|
29
29
|
@body = {"output" => {"message" => {"role" => "assistant", "content" => []}}}
|
|
30
30
|
@stream = stream
|
|
31
31
|
@text_markers = {}
|
|
32
|
-
@can_emit_content = stream.respond_to?(:on_content)
|
|
33
|
-
@can_emit_reasoning_content = stream.respond_to?(:on_reasoning_content)
|
|
34
|
-
@can_emit_tool_call = stream.respond_to?(:on_tool_call)
|
|
35
|
-
@can_push_content = stream.respond_to?(:<<)
|
|
36
32
|
end
|
|
37
33
|
|
|
38
34
|
##
|
|
@@ -165,21 +161,16 @@ class LLM::Bedrock
|
|
|
165
161
|
end
|
|
166
162
|
|
|
167
163
|
def emit_content(value)
|
|
168
|
-
|
|
169
|
-
@stream.on_content(value)
|
|
170
|
-
elsif @can_push_content
|
|
171
|
-
@stream << value
|
|
172
|
-
end
|
|
164
|
+
@stream.on_content(value)
|
|
173
165
|
end
|
|
174
166
|
|
|
175
167
|
def emit_reasoning_content(value)
|
|
176
|
-
@stream.on_reasoning_content(value)
|
|
168
|
+
@stream.on_reasoning_content(value)
|
|
177
169
|
end
|
|
178
170
|
|
|
179
171
|
def emit_tool(tool)
|
|
180
|
-
return unless @can_emit_tool_call
|
|
181
172
|
function, error = resolve_tool(tool)
|
|
182
|
-
@stream.on_tool_call(function
|
|
173
|
+
error ? @stream.queue << error : @stream.on_tool_call(function)
|
|
183
174
|
end
|
|
184
175
|
|
|
185
176
|
def resolve_tool(tool)
|
|
@@ -10,16 +10,12 @@ class LLM::Google
|
|
|
10
10
|
attr_reader :body
|
|
11
11
|
|
|
12
12
|
##
|
|
13
|
-
# @param [
|
|
14
|
-
# A stream sink that implements {#<<} or the {LLM::Stream} interface
|
|
13
|
+
# @param [LLM::Stream] stream
|
|
15
14
|
# @return [LLM::Google::StreamParser]
|
|
16
15
|
def initialize(stream)
|
|
17
16
|
@body = {"candidates" => []}
|
|
18
17
|
@stream = stream
|
|
19
18
|
@emits = {tools: []}
|
|
20
|
-
@can_emit_content = stream.respond_to?(:on_content)
|
|
21
|
-
@can_emit_tool_call = stream.respond_to?(:on_tool_call)
|
|
22
|
-
@can_push_content = stream.respond_to?(:<<)
|
|
23
19
|
end
|
|
24
20
|
|
|
25
21
|
##
|
|
@@ -129,21 +125,16 @@ class LLM::Google
|
|
|
129
125
|
end
|
|
130
126
|
|
|
131
127
|
def emit_content(value)
|
|
132
|
-
|
|
133
|
-
@stream.on_content(value)
|
|
134
|
-
elsif @can_push_content
|
|
135
|
-
@stream << value
|
|
136
|
-
end
|
|
128
|
+
@stream.on_content(value)
|
|
137
129
|
end
|
|
138
130
|
|
|
139
131
|
def emit_tool(pindex, cindex, part)
|
|
140
|
-
return unless @can_emit_tool_call
|
|
141
132
|
return unless complete_tool?(part)
|
|
142
133
|
key = [cindex, pindex]
|
|
143
134
|
return if @emits[:tools].include?(key)
|
|
144
135
|
function, error = resolve_tool(part, cindex, pindex)
|
|
145
136
|
@emits[:tools] << key
|
|
146
|
-
@stream.on_tool_call(function
|
|
137
|
+
error ? @stream.queue << error : @stream.on_tool_call(function)
|
|
147
138
|
end
|
|
148
139
|
|
|
149
140
|
def complete_tool?(part)
|
data/lib/llm/providers/google.rb
CHANGED
|
@@ -175,6 +175,13 @@ module LLM
|
|
|
175
175
|
"model"
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
+
##
|
|
179
|
+
# @param [LLM::Function] fn
|
|
180
|
+
# @return [Hash]
|
|
181
|
+
def adapt_function(fn)
|
|
182
|
+
{name: fn.name, description: fn.description, parameters: fn.params}.compact
|
|
183
|
+
end
|
|
184
|
+
|
|
178
185
|
private
|
|
179
186
|
|
|
180
187
|
def headers
|
|
@@ -119,6 +119,17 @@ module LLM
|
|
|
119
119
|
"mistral-large-latest"
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
##
|
|
123
|
+
# @param [LLM::Function] fn
|
|
124
|
+
# @return [Hash]
|
|
125
|
+
def adapt_function(fn)
|
|
126
|
+
params = fn.params || {type: "object", properties: {}}
|
|
127
|
+
{
|
|
128
|
+
type: "function",
|
|
129
|
+
function: {name: fn.name, description: fn.description, parameters: params}
|
|
130
|
+
}.compact
|
|
131
|
+
end
|
|
132
|
+
|
|
122
133
|
private
|
|
123
134
|
|
|
124
135
|
##
|
|
@@ -10,11 +10,11 @@ class LLM::Ollama
|
|
|
10
10
|
attr_reader :body
|
|
11
11
|
|
|
12
12
|
##
|
|
13
|
-
# @
|
|
13
|
+
# @param [LLM::Stream] stream
|
|
14
|
+
# @return [LLM::Ollama::StreamParser]
|
|
14
15
|
def initialize(stream)
|
|
15
16
|
@body = {}
|
|
16
17
|
@stream = stream
|
|
17
|
-
@can_push_content = stream.respond_to?(:<<)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
##
|
|
@@ -37,10 +37,10 @@ class LLM::Ollama
|
|
|
37
37
|
if key == "message"
|
|
38
38
|
if @body[key]
|
|
39
39
|
@body[key]["content"] << value["content"]
|
|
40
|
-
@stream
|
|
40
|
+
@stream.on_content(value["content"])
|
|
41
41
|
else
|
|
42
42
|
@body[key] = value
|
|
43
|
-
@stream
|
|
43
|
+
@stream.on_content(value["content"])
|
|
44
44
|
end
|
|
45
45
|
else
|
|
46
46
|
@body[key] = value
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -98,6 +98,17 @@ module LLM
|
|
|
98
98
|
"qwen3:latest"
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
##
|
|
102
|
+
# @param [LLM::Function] fn
|
|
103
|
+
# @return [Hash]
|
|
104
|
+
def adapt_function(fn)
|
|
105
|
+
params = fn.params || {type: "object", properties: {}}
|
|
106
|
+
{
|
|
107
|
+
type: "function", name: fn.name,
|
|
108
|
+
function: {name: fn.name, description: fn.description, parameters: params}
|
|
109
|
+
}.compact
|
|
110
|
+
end
|
|
111
|
+
|
|
101
112
|
private
|
|
102
113
|
|
|
103
114
|
def headers
|
|
@@ -12,17 +12,12 @@ class LLM::OpenAI
|
|
|
12
12
|
attr_reader :body
|
|
13
13
|
|
|
14
14
|
##
|
|
15
|
-
# @param [
|
|
16
|
-
# A stream sink that implements {#<<} or the {LLM::Stream} interface
|
|
15
|
+
# @param [LLM::Stream] stream
|
|
17
16
|
# @return [LLM::OpenAI::Responses::StreamParser]
|
|
18
17
|
def initialize(stream)
|
|
19
18
|
@body = {"output" => []}
|
|
20
19
|
@stream = stream
|
|
21
20
|
@emits = {tools: {}}
|
|
22
|
-
@can_emit_content = stream.respond_to?(:on_content)
|
|
23
|
-
@can_emit_reasoning_content = stream.respond_to?(:on_reasoning_content)
|
|
24
|
-
@can_emit_tool_call = stream.respond_to?(:on_tool_call)
|
|
25
|
-
@can_push_content = stream.respond_to?(:<<)
|
|
26
21
|
@cached_output_index = nil
|
|
27
22
|
@cached_output_item = nil
|
|
28
23
|
@cached_content_index = nil
|
|
@@ -240,26 +235,21 @@ class LLM::OpenAI
|
|
|
240
235
|
# @group Emitters
|
|
241
236
|
|
|
242
237
|
def emit_content(value)
|
|
243
|
-
|
|
244
|
-
@stream.on_content(value)
|
|
245
|
-
elsif @can_push_content
|
|
246
|
-
@stream << value
|
|
247
|
-
end
|
|
238
|
+
@stream.on_content(value)
|
|
248
239
|
end
|
|
249
240
|
|
|
250
241
|
def emit_reasoning_content(value)
|
|
251
|
-
@stream.on_reasoning_content(value)
|
|
242
|
+
@stream.on_reasoning_content(value)
|
|
252
243
|
end
|
|
253
244
|
|
|
254
245
|
def emit_tool(index, tool)
|
|
255
|
-
return unless @can_emit_tool_call
|
|
256
246
|
return if @emits[:tools][index]
|
|
257
247
|
return unless tool["call_id"] && tool["name"]
|
|
258
248
|
arguments = parse_arguments(tool["arguments"])
|
|
259
249
|
return unless arguments
|
|
260
250
|
function, error = resolve_tool(tool, arguments)
|
|
261
251
|
@emits[:tools][index] = true
|
|
262
|
-
@stream.on_tool_call(function
|
|
252
|
+
error ? @stream.queue << error : @stream.on_tool_call(function)
|
|
263
253
|
end
|
|
264
254
|
|
|
265
255
|
##
|
|
@@ -86,6 +86,16 @@ class LLM::OpenAI
|
|
|
86
86
|
res
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
##
|
|
90
|
+
# @param [LLM::Function] fn
|
|
91
|
+
# @return [Hash]
|
|
92
|
+
def adapt_function(fn)
|
|
93
|
+
{
|
|
94
|
+
type: "function", name: fn.name, description: fn.description,
|
|
95
|
+
parameters: (fn.params || {type: "object", properties: {}}).to_h.merge(additionalProperties: false), strict: false
|
|
96
|
+
}.compact
|
|
97
|
+
end
|
|
98
|
+
|
|
89
99
|
private
|
|
90
100
|
|
|
91
101
|
[:path, :headers, :execute, :transport, :resolve_tools].each do |m|
|
|
@@ -12,15 +12,12 @@ class LLM::OpenAI
|
|
|
12
12
|
attr_reader :body
|
|
13
13
|
|
|
14
14
|
##
|
|
15
|
-
# @
|
|
15
|
+
# @param [LLM::Stream] stream
|
|
16
|
+
# @return [LLM::OpenAI::StreamParser]
|
|
16
17
|
def initialize(stream)
|
|
17
18
|
@body = {}
|
|
18
19
|
@stream = stream
|
|
19
20
|
@emits = {tools: {}}
|
|
20
|
-
@can_emit_content = stream.respond_to?(:on_content)
|
|
21
|
-
@can_emit_reasoning_content = stream.respond_to?(:on_reasoning_content)
|
|
22
|
-
@can_emit_tool_call = stream.respond_to?(:on_tool_call)
|
|
23
|
-
@can_push_content = stream.respond_to?(:<<)
|
|
24
21
|
end
|
|
25
22
|
|
|
26
23
|
##
|
|
@@ -157,21 +154,14 @@ class LLM::OpenAI
|
|
|
157
154
|
end
|
|
158
155
|
|
|
159
156
|
def emit_content(value)
|
|
160
|
-
|
|
161
|
-
@stream.on_content(value)
|
|
162
|
-
elsif @can_push_content
|
|
163
|
-
@stream << value
|
|
164
|
-
end
|
|
157
|
+
@stream.on_content(value)
|
|
165
158
|
end
|
|
166
159
|
|
|
167
160
|
def emit_reasoning_content(value)
|
|
168
|
-
|
|
169
|
-
@stream.on_reasoning_content(value)
|
|
170
|
-
end
|
|
161
|
+
@stream.on_reasoning_content(value)
|
|
171
162
|
end
|
|
172
163
|
|
|
173
164
|
def emit_tool(tool, tindex)
|
|
174
|
-
return unless @can_emit_tool_call
|
|
175
165
|
return if @emits[:tools][tindex]
|
|
176
166
|
function = tool["function"]
|
|
177
167
|
return unless function && tool["id"] && function["name"]
|
|
@@ -180,7 +170,7 @@ class LLM::OpenAI
|
|
|
180
170
|
return unless arguments
|
|
181
171
|
function, error = resolve_tool(tool, function, arguments)
|
|
182
172
|
@emits[:tools][tindex] = true
|
|
183
|
-
@stream.on_tool_call(function
|
|
173
|
+
error ? @stream.queue << error : @stream.on_tool_call(function)
|
|
184
174
|
end
|
|
185
175
|
|
|
186
176
|
def resolve_tool(tool, function, arguments)
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -152,6 +152,17 @@ module LLM
|
|
|
152
152
|
"gpt-5.4-mini"
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
##
|
|
156
|
+
# @param [LLM::Function] fn
|
|
157
|
+
# @return [Hash]
|
|
158
|
+
def adapt_function(fn)
|
|
159
|
+
params = fn.params || {type: "object", properties: {}}
|
|
160
|
+
{
|
|
161
|
+
type: "function", name: fn.name,
|
|
162
|
+
function: {name: fn.name, description: fn.description, parameters: params}
|
|
163
|
+
}.compact
|
|
164
|
+
end
|
|
165
|
+
|
|
155
166
|
##
|
|
156
167
|
# @note
|
|
157
168
|
# This method includes certain tools that require configuration
|
data/lib/llm/repl/command.rb
CHANGED
|
@@ -7,9 +7,41 @@ class LLM::Repl
|
|
|
7
7
|
# description. This basic version does not implement parameters. A
|
|
8
8
|
# command is accessible via the `/` prefix: eg `/exit`.
|
|
9
9
|
class Command
|
|
10
|
+
UNDEFINED = Object.new
|
|
11
|
+
SINGLETON = self
|
|
12
|
+
private_constant :UNDEFINED, :SINGLETON
|
|
13
|
+
|
|
10
14
|
##
|
|
11
15
|
# @api private
|
|
12
|
-
|
|
16
|
+
Parameter = Struct.new(:name, :type, :description, :options, :index, :value) do
|
|
17
|
+
##
|
|
18
|
+
# @return [Boolean]
|
|
19
|
+
def required?
|
|
20
|
+
options[:required] == true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Mark the parameter as required
|
|
25
|
+
# @return [void]
|
|
26
|
+
def required!
|
|
27
|
+
options[:required] = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
def optional?
|
|
33
|
+
!required?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# Assign a parameter value - with type checks
|
|
38
|
+
# @param [String] other
|
|
39
|
+
# @return [void]
|
|
40
|
+
def value=(other)
|
|
41
|
+
raise TypeError, "#{other.class} is not a #{type}" unless type === other
|
|
42
|
+
self[:value] = other
|
|
43
|
+
end
|
|
44
|
+
end
|
|
13
45
|
|
|
14
46
|
##
|
|
15
47
|
# Find a command by a name, or by an input string.
|
|
@@ -26,15 +58,15 @@ class LLM::Repl
|
|
|
26
58
|
# @param [String] input
|
|
27
59
|
# @param [String] name
|
|
28
60
|
# @return [LLM::Repl::Command, nil]
|
|
29
|
-
def self.find_by(input:
|
|
30
|
-
if input
|
|
61
|
+
def self.find_by(input: UNDEFINED, name: UNDEFINED)
|
|
62
|
+
if input != UNDEFINED
|
|
31
63
|
return nil unless input[0] == "/"
|
|
32
64
|
n, = input.split(" ")
|
|
33
65
|
registry.find { n[1..] == _1.name }
|
|
34
|
-
elsif name
|
|
66
|
+
elsif name != UNDEFINED
|
|
35
67
|
registry.find { name == _1.name }
|
|
36
68
|
else
|
|
37
|
-
raise ArgumentError, "provide
|
|
69
|
+
raise ArgumentError, "provide either an input or a name"
|
|
38
70
|
end
|
|
39
71
|
end
|
|
40
72
|
|
|
@@ -45,6 +77,8 @@ class LLM::Repl
|
|
|
45
77
|
def self.inherited(command)
|
|
46
78
|
LLM.lock(:inherited) do
|
|
47
79
|
registry << command
|
|
80
|
+
command.instance_variable_set(:@parameters, {})
|
|
81
|
+
command.define_singleton_method(:inherited) { |command| SINGLETON.inherited(command) }
|
|
48
82
|
end
|
|
49
83
|
end
|
|
50
84
|
|
|
@@ -74,12 +108,94 @@ class LLM::Repl
|
|
|
74
108
|
@description = description
|
|
75
109
|
end
|
|
76
110
|
|
|
111
|
+
##
|
|
112
|
+
# @param [Symbol] name
|
|
113
|
+
# @param [Class] type
|
|
114
|
+
# @param [String] description
|
|
115
|
+
# @param [Hash] options
|
|
116
|
+
# @return [void]
|
|
117
|
+
def self.parameter(name, type, description, options = {})
|
|
118
|
+
@parameters[name] = Parameter.new(
|
|
119
|
+
name, type,
|
|
120
|
+
description, options,
|
|
121
|
+
@parameters.size, nil
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
##
|
|
126
|
+
# @return [Hash]
|
|
127
|
+
def self.parameters
|
|
128
|
+
@parameters
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
# @param [Array<Symbol>] names
|
|
133
|
+
# One or more required names
|
|
134
|
+
# @return [void]
|
|
135
|
+
def self.required(names)
|
|
136
|
+
names.each do |name|
|
|
137
|
+
if @parameters.key?(name)
|
|
138
|
+
@parameters[name].required!
|
|
139
|
+
else
|
|
140
|
+
raise LLM::Error, "'#{name}' is not a known parameter"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
##
|
|
146
|
+
# @param [LLM::Repl] repl
|
|
147
|
+
# @return [LLM::Repl::Command]
|
|
148
|
+
def initialize(repl)
|
|
149
|
+
@repl = repl
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
##
|
|
153
|
+
# Write a string to the transcript
|
|
154
|
+
# @param [String] str
|
|
155
|
+
# @return [void]
|
|
156
|
+
def write(str, who: "command(#{self.class.name}): ")
|
|
157
|
+
@repl.write(who, Curses::A_BOLD)
|
|
158
|
+
@repl.write(str)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
##
|
|
162
|
+
# Display a formatted help message for this command.
|
|
163
|
+
# Uses a single {#write} call to output the command name,
|
|
164
|
+
# description, and parameter details.
|
|
165
|
+
# @return [void]
|
|
166
|
+
def self.help
|
|
167
|
+
lines = []
|
|
168
|
+
lines << "Command: #{name}"
|
|
169
|
+
lines << "Description: #{description}"
|
|
170
|
+
unless parameters.empty?
|
|
171
|
+
lines << ""
|
|
172
|
+
lines << "Parameters:"
|
|
173
|
+
parameters.each_value do |param|
|
|
174
|
+
tag = param.required? ? "(required)" : "(optional)"
|
|
175
|
+
lines << " #{param.name} [#{param.type}] - #{param.description} #{tag}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
lines.join("\n")
|
|
179
|
+
end
|
|
180
|
+
|
|
77
181
|
##
|
|
78
182
|
# This method should be implemented by subclasses.
|
|
79
183
|
# @raise [NotImplementedError]
|
|
80
184
|
def call(...)
|
|
81
185
|
raise NotImplementedError, "#{self.class}#call is not implemented"
|
|
82
186
|
end
|
|
187
|
+
|
|
188
|
+
##
|
|
189
|
+
# @return [Hash<Symbol, Parameter>]
|
|
190
|
+
def parameters
|
|
191
|
+
self.class.parameters
|
|
192
|
+
end
|
|
193
|
+
|
|
83
194
|
require_relative "commands/exit"
|
|
195
|
+
require_relative "commands/help"
|
|
84
196
|
end
|
|
85
197
|
end
|
|
198
|
+
|
|
199
|
+
##
|
|
200
|
+
# Convenience constant
|
|
201
|
+
LLM::Command = LLM::Repl::Command
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
class Help < Command
|
|
5
|
+
name "help"
|
|
6
|
+
description "show help for a given command"
|
|
7
|
+
parameter :name, String, "The name of a command"
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# @param [String] name
|
|
11
|
+
# @return [void]
|
|
12
|
+
def call(name: nil)
|
|
13
|
+
if name.nil?
|
|
14
|
+
write("\n#{self.class.help}\n\n")
|
|
15
|
+
elsif command = LLM::Command.find_by(name:)
|
|
16
|
+
write("\n#{command.help}\n\n")
|
|
17
|
+
else
|
|
18
|
+
write "\nNo help for #{name} was found" \
|
|
19
|
+
"\nThat command doesn't exist." \
|
|
20
|
+
"\n\n"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/llm/repl/input.rb
CHANGED
|
@@ -19,6 +19,7 @@ class LLM::Repl
|
|
|
19
19
|
DOWN = Curses::Key::DOWN
|
|
20
20
|
LEFT = Curses::Key::LEFT
|
|
21
21
|
RIGHT = Curses::Key::RIGHT
|
|
22
|
+
ESC = 27
|
|
22
23
|
ENTER = [Curses::Key::ENTER, 10, 13]
|
|
23
24
|
BACKSPACE = [Curses::Key::BACKSPACE, 127]
|
|
24
25
|
|
|
@@ -58,7 +59,9 @@ class LLM::Repl
|
|
|
58
59
|
# @return [Symbol, nil]
|
|
59
60
|
def on_char(window, char, now)
|
|
60
61
|
is_paste = lambda { @last_char_at and (now - @last_char_at) < PASTE_THRESHOLD }
|
|
61
|
-
if
|
|
62
|
+
if ESC == char
|
|
63
|
+
@agent.cancel!
|
|
64
|
+
elsif CTRL[:D] == char
|
|
62
65
|
delete
|
|
63
66
|
:ctrl_d
|
|
64
67
|
elsif CTRL[:A] == char
|
data/lib/llm/repl/stream.rb
CHANGED
|
@@ -28,14 +28,9 @@ class LLM::Repl
|
|
|
28
28
|
|
|
29
29
|
##
|
|
30
30
|
# @param [LLM::Function] tool
|
|
31
|
-
# @param [LLM::Function::Return, nil] error
|
|
32
31
|
# @return [void]
|
|
33
|
-
def on_tool_call(tool
|
|
34
|
-
|
|
35
|
-
@_queue.push [:status, "tool not found: #{tool.name}"]
|
|
36
|
-
else
|
|
37
|
-
@_queue.push [:status, "#{tool.name}(#{format_args(tool)})"]
|
|
38
|
-
end
|
|
32
|
+
def on_tool_call(tool)
|
|
33
|
+
@_queue.push [:status, "#{tool.name}(#{format_args(tool)})"]
|
|
39
34
|
end
|
|
40
35
|
|
|
41
36
|
##
|
|
@@ -43,7 +38,7 @@ class LLM::Repl
|
|
|
43
38
|
# @param [LLM::Function::Return] result
|
|
44
39
|
# @return [void]
|
|
45
40
|
def on_tool_return(_tool, result)
|
|
46
|
-
@_queue.push [:status,
|
|
41
|
+
@_queue.push [:status, @repl.thinking_text]
|
|
47
42
|
end
|
|
48
43
|
|
|
49
44
|
##
|