llm.rb 12.3.1 → 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 +395 -0
- data/README.md +103 -16
- data/data/anthropic.json +249 -249
- data/data/bedrock.json +2038 -1879
- data/data/deepinfra.json +591 -591
- data/data/deepseek.json +31 -31
- data/data/google.json +332 -329
- data/data/mistral.json +381 -381
- data/data/openai.json +1132 -1132
- data/data/xai.json +138 -138
- data/data/zai.json +165 -165
- data/lib/llm/agent.rb +12 -7
- data/lib/llm/buffer.rb +15 -0
- data/lib/llm/context/deserializer.rb +2 -5
- 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 +201 -0
- data/lib/llm/repl/commands/exit.rb +23 -0
- data/lib/llm/repl/commands/help.rb +24 -0
- data/lib/llm/repl/input.rb +118 -35
- data/lib/llm/repl/stream.rb +43 -8
- data/lib/llm/repl/transcript.rb +6 -0
- data/lib/llm/repl/window.rb +28 -6
- data/lib/llm/repl.rb +125 -27
- data/lib/llm/stream.rb +2 -7
- data/lib/llm/tools/ls.rb +30 -0
- data/lib/llm/tools/which.rb +38 -0
- data/lib/llm/version.rb +1 -1
- data/resources/deepdive.md +67 -1
- metadata +6 -1
data/lib/llm/function.rb
CHANGED
|
@@ -117,7 +117,7 @@ class LLM::Function
|
|
|
117
117
|
# @param [Hash, LLM::Object] other
|
|
118
118
|
# @return [void]
|
|
119
119
|
def arguments=(other)
|
|
120
|
-
@arguments = LLM::Object.from(other)
|
|
120
|
+
@arguments = other.nil? ? nil : LLM::Object.from(other)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
##
|
|
@@ -358,18 +358,7 @@ class LLM::Function
|
|
|
358
358
|
##
|
|
359
359
|
# @return [Hash]
|
|
360
360
|
def adapt(provider)
|
|
361
|
-
|
|
362
|
-
when "LLM::Google"
|
|
363
|
-
{name: @name, description: @description, parameters: @params}.compact
|
|
364
|
-
when "LLM::Anthropic"
|
|
365
|
-
{
|
|
366
|
-
name: @name,
|
|
367
|
-
description: @description,
|
|
368
|
-
input_schema: @params || {type: "object", properties: {}}
|
|
369
|
-
}.compact
|
|
370
|
-
else
|
|
371
|
-
format_openai(provider)
|
|
372
|
-
end
|
|
361
|
+
provider.adapt_function(self)
|
|
373
362
|
end
|
|
374
363
|
|
|
375
364
|
##
|
|
@@ -383,28 +372,6 @@ class LLM::Function
|
|
|
383
372
|
|
|
384
373
|
private
|
|
385
374
|
|
|
386
|
-
def format_openai(provider)
|
|
387
|
-
case provider.class.to_s
|
|
388
|
-
when "LLM::OpenAI::Responses"
|
|
389
|
-
{
|
|
390
|
-
type: "function", name: @name, description: @description,
|
|
391
|
-
parameters: (@params || {type: "object", properties: {}}).to_h.merge(additionalProperties: false), strict: false
|
|
392
|
-
}.compact
|
|
393
|
-
when "LLM::Mistral"
|
|
394
|
-
params = @params || {type: "object", properties: {}}
|
|
395
|
-
{
|
|
396
|
-
type: "function",
|
|
397
|
-
function: {name: @name, description: @description, parameters: params}
|
|
398
|
-
}.compact
|
|
399
|
-
else
|
|
400
|
-
params = @params || {type: "object", properties: {}}
|
|
401
|
-
{
|
|
402
|
-
type: "function", name: @name,
|
|
403
|
-
function: {name: @name, description: @description, parameters: params}
|
|
404
|
-
}.compact
|
|
405
|
-
end
|
|
406
|
-
end
|
|
407
|
-
|
|
408
375
|
##
|
|
409
376
|
# Internal method that calls the function and returns a Return object.
|
|
410
377
|
# Handles both class-based and proc-based runners, and rescues exceptions.
|
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
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Repl::Command LLM::Repl::Command} class is the superclass
|
|
6
|
+
# of all read-eval-print loop commands. A command has a name, and a
|
|
7
|
+
# description. This basic version does not implement parameters. A
|
|
8
|
+
# command is accessible via the `/` prefix: eg `/exit`.
|
|
9
|
+
class Command
|
|
10
|
+
UNDEFINED = Object.new
|
|
11
|
+
SINGLETON = self
|
|
12
|
+
private_constant :UNDEFINED, :SINGLETON
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# @api private
|
|
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
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# Find a command by a name, or by an input string.
|
|
48
|
+
# @example find by name
|
|
49
|
+
# LLM::Repl::Command.find_by(name: "exit")
|
|
50
|
+
# @example find by input string
|
|
51
|
+
# LLM::Repl::Command.find_by(input: "/exit")
|
|
52
|
+
# @note
|
|
53
|
+
# The input string must be prefixed with "/"
|
|
54
|
+
# or it won't be matched. The match is made
|
|
55
|
+
# against the string before the first space -
|
|
56
|
+
# so "/exit foo" will match the "exit" command
|
|
57
|
+
# but "/exitnow" will not.
|
|
58
|
+
# @param [String] input
|
|
59
|
+
# @param [String] name
|
|
60
|
+
# @return [LLM::Repl::Command, nil]
|
|
61
|
+
def self.find_by(input: UNDEFINED, name: UNDEFINED)
|
|
62
|
+
if input != UNDEFINED
|
|
63
|
+
return nil unless input[0] == "/"
|
|
64
|
+
n, = input.split(" ")
|
|
65
|
+
registry.find { n[1..] == _1.name }
|
|
66
|
+
elsif name != UNDEFINED
|
|
67
|
+
registry.find { name == _1.name }
|
|
68
|
+
else
|
|
69
|
+
raise ArgumentError, "provide either an input or a name"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# @param [LLM::Repl::Command] command
|
|
75
|
+
# A new subclass
|
|
76
|
+
# @return [void]
|
|
77
|
+
def self.inherited(command)
|
|
78
|
+
LLM.lock(:inherited) do
|
|
79
|
+
registry << command
|
|
80
|
+
command.instance_variable_set(:@parameters, {})
|
|
81
|
+
command.define_singleton_method(:inherited) { |command| SINGLETON.inherited(command) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# @return [Array<LLM::Repl::Command]
|
|
87
|
+
def self.registry
|
|
88
|
+
@registry ||= []
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# Set or get a command name.
|
|
93
|
+
# @param [String] name
|
|
94
|
+
# The command name.
|
|
95
|
+
# @return [String]
|
|
96
|
+
def self.name(name = UNDEFINED)
|
|
97
|
+
return @name if name == UNDEFINED
|
|
98
|
+
@name = name
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# Set or get a command description.
|
|
103
|
+
# @param [String] description
|
|
104
|
+
# The command description.
|
|
105
|
+
# @return [String]
|
|
106
|
+
def self.description(description = UNDEFINED)
|
|
107
|
+
return @description if description == UNDEFINED
|
|
108
|
+
@description = description
|
|
109
|
+
end
|
|
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
|
+
|
|
181
|
+
##
|
|
182
|
+
# This method should be implemented by subclasses.
|
|
183
|
+
# @raise [NotImplementedError]
|
|
184
|
+
def call(...)
|
|
185
|
+
raise NotImplementedError, "#{self.class}#call is not implemented"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
##
|
|
189
|
+
# @return [Hash<Symbol, Parameter>]
|
|
190
|
+
def parameters
|
|
191
|
+
self.class.parameters
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
require_relative "commands/exit"
|
|
195
|
+
require_relative "commands/help"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
##
|
|
200
|
+
# Convenience constant
|
|
201
|
+
LLM::Command = LLM::Repl::Command
|