llm.rb 13.0.0 → 13.1.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 +185 -14
- data/README.md +155 -30
- data/bin/llm.rb +124 -0
- data/lib/llm/agent.rb +78 -8
- data/lib/llm/function/array.rb +3 -3
- data/lib/llm/function.rb +1 -2
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
- data/lib/llm/providers/mistral.rb +1 -1
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +1 -1
- data/lib/llm/providers/openai.rb +1 -1
- data/lib/llm/repl/{transcript.rb → buffer.rb} +34 -21
- 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/markdown/table.rb +7 -3
- data/lib/llm/repl/markdown.rb +3 -3
- data/lib/llm/repl/node.rb +37 -0
- data/lib/llm/repl/status.rb +4 -4
- data/lib/llm/repl/stream.rb +2 -2
- data/lib/llm/repl/window.rb +31 -32
- data/lib/llm/repl.rb +56 -30
- data/lib/llm/skill.rb +20 -4
- 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/version.rb +1 -1
- data/llm.gemspec +8 -2
- data/resources/deepdive.md +45 -1785
- metadata +16 -5
data/bin/llm.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "llm"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# utils
|
|
8
|
+
|
|
9
|
+
def keys
|
|
10
|
+
@keys ||= Dir[File.join(__dir__, "..", "lib", "llm", "providers", "*")]
|
|
11
|
+
.select { File.file?(_1) }
|
|
12
|
+
.map { File.basename(_1, ".rb") }
|
|
13
|
+
.sort_by { _1 == "deepseek" ? 0 : 1 }
|
|
14
|
+
.map { "#{_1.upcase}_API_KEY" } - ["BEDROCK_API_KEY"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def help
|
|
18
|
+
prog = File.basename($PROGRAM_NAME)
|
|
19
|
+
warn ""
|
|
20
|
+
warn "Usage: #{prog} [options]"
|
|
21
|
+
warn ""
|
|
22
|
+
warn "Options:"
|
|
23
|
+
warn " -p PROVIDER Choose a provider"
|
|
24
|
+
warn " -t Temporary session that doesn't persist to disk"
|
|
25
|
+
warn " -h Show this help"
|
|
26
|
+
warn ""
|
|
27
|
+
warn "Examples:"
|
|
28
|
+
warn " #{prog} # auto-detect provider from $PROVIDER_API_KEY"
|
|
29
|
+
warn " #{prog} -p openai # use OpenAI"
|
|
30
|
+
warn " #{prog} -h # this help"
|
|
31
|
+
warn ""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def loaderror(ex)
|
|
35
|
+
gem, = ex.message.split(" is an optional runtime dependency")
|
|
36
|
+
warn ""
|
|
37
|
+
warn " ── llm.rb ──────────────────────────────────────────────"
|
|
38
|
+
warn ""
|
|
39
|
+
warn " ✖ Missing dependency: #{gem}"
|
|
40
|
+
warn ""
|
|
41
|
+
warn " The repl needs this gem, but it's not installed."
|
|
42
|
+
warn ""
|
|
43
|
+
warn " Fix: gem install #{gem}"
|
|
44
|
+
warn " Or: bundle add #{gem}"
|
|
45
|
+
warn ""
|
|
46
|
+
warn " Tip: If you don't need the repl, you can use the"
|
|
47
|
+
warn " library directly with: require \"llm\""
|
|
48
|
+
warn ""
|
|
49
|
+
warn " ───────────────────────────────────────────────────────"
|
|
50
|
+
warn ""
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# main
|
|
55
|
+
|
|
56
|
+
def main(argv)
|
|
57
|
+
##
|
|
58
|
+
# Make sure the dependencies are satisified first
|
|
59
|
+
begin
|
|
60
|
+
require "llm/tools"
|
|
61
|
+
require "llm/repl"
|
|
62
|
+
rescue LLM::LoadError => ex
|
|
63
|
+
loaderror(ex)
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# C-Style option parser
|
|
69
|
+
# No external dep
|
|
70
|
+
while option = argv.shift
|
|
71
|
+
case option
|
|
72
|
+
when '-h'
|
|
73
|
+
help
|
|
74
|
+
exit 0
|
|
75
|
+
when '-t'
|
|
76
|
+
temp = true
|
|
77
|
+
when '-p'
|
|
78
|
+
provider = argv.shift
|
|
79
|
+
else
|
|
80
|
+
warn "llm.rb: unknown option #{option}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# Setup the home directory
|
|
86
|
+
# But only if the `-t` switch has not been provided
|
|
87
|
+
if temp.nil?
|
|
88
|
+
home = File.join(Dir.home, ".llm.rb")
|
|
89
|
+
FileUtils.mkdir_p(File.join(home, Dir.getwd))
|
|
90
|
+
session = File.join(home, Dir.getwd, "session.json")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# No provider has been given.
|
|
95
|
+
# Try to infer one.
|
|
96
|
+
if provider.nil?
|
|
97
|
+
key = keys.find { ENV[_1] }
|
|
98
|
+
if key.nil?
|
|
99
|
+
warn "llm.rb: provide a provider with the -p switch"
|
|
100
|
+
exit 1
|
|
101
|
+
else
|
|
102
|
+
provider, = key.split("_")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# We're ready to start the REPL
|
|
108
|
+
# This should always succeed unless -p gave garbage
|
|
109
|
+
provider = provider.downcase
|
|
110
|
+
if LLM.respond_to?(provider)
|
|
111
|
+
key ||= "#{provider.upcase}_API_KEY"
|
|
112
|
+
if ENV[key].nil? || ENV[key].to_s.empty?
|
|
113
|
+
warn "llm.rb: set #{key} to use #{provider}"
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
llm = LLM.method(provider).call(key: ENV[key])
|
|
117
|
+
agent = LLM::Agent.new(llm, path: temp ? nil : session, tools: LLM::Tool.subclasses)
|
|
118
|
+
agent.repl
|
|
119
|
+
else
|
|
120
|
+
warn "llm.rb: #{provider} was not recognized"
|
|
121
|
+
exit 1
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
main(ARGV)
|
data/lib/llm/agent.rb
CHANGED
|
@@ -11,8 +11,8 @@ module LLM
|
|
|
11
11
|
# {LLM::Context LLM::Context}: message history, usage, persistence,
|
|
12
12
|
# streaming parameters, and provider-backed requests still flow through
|
|
13
13
|
# an underlying context. The defining behavior of an agent is that it
|
|
14
|
-
# automatically resolves pending tool calls for you during `talk
|
|
15
|
-
#
|
|
14
|
+
# automatically resolves pending tool calls for you during `talk`,
|
|
15
|
+
# instead of leaving tool loops to the caller.
|
|
16
16
|
#
|
|
17
17
|
# **Notes:**
|
|
18
18
|
# * Instructions are injected once unless a system message is already present.
|
|
@@ -24,7 +24,7 @@ module LLM
|
|
|
24
24
|
# advisory tool errors back through the model and keeps the loop in-band.
|
|
25
25
|
# Set `tool_attempts: nil` to disable that advisory behavior.
|
|
26
26
|
# * Tool loop execution can be configured with `concurrency :sequential`,
|
|
27
|
-
# `:thread`, `:async`, `:fiber`, or `:ractor`.
|
|
27
|
+
# `:thread`, `:async`, `:fiber`, `:fork`, or `:ractor`.
|
|
28
28
|
#
|
|
29
29
|
# @example Subclass with defaults
|
|
30
30
|
# class SystemAdmin < LLM::Agent
|
|
@@ -52,6 +52,16 @@ module LLM
|
|
|
52
52
|
UNDEFINED = Object.new
|
|
53
53
|
private_constant :UNDEFINED
|
|
54
54
|
|
|
55
|
+
##
|
|
56
|
+
# @api private
|
|
57
|
+
CASE_PATTERN = /(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/
|
|
58
|
+
private_constant :CASE_PATTERN
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# @api private
|
|
62
|
+
File = ::File
|
|
63
|
+
private_constant :File
|
|
64
|
+
|
|
55
65
|
##
|
|
56
66
|
# Returns a provider
|
|
57
67
|
# @return [LLM::Provider]
|
|
@@ -96,18 +106,44 @@ module LLM
|
|
|
96
106
|
|
|
97
107
|
##
|
|
98
108
|
# Set or get an agent's name
|
|
109
|
+
# @note
|
|
110
|
+
# This method serves as a self-documenting string
|
|
111
|
+
# and it is used by {LLM::Repl LLM::Repl}. It is
|
|
112
|
+
# optional but recommended.
|
|
99
113
|
# @param [String] name
|
|
100
114
|
# The agent name
|
|
101
115
|
# @return [String]
|
|
102
116
|
# Return's the agents name
|
|
103
117
|
def self.name(name = UNDEFINED, &block)
|
|
104
118
|
if name.equal?(UNDEFINED)
|
|
105
|
-
@name
|
|
119
|
+
if @name.nil?
|
|
120
|
+
name = to_s.split("::").last
|
|
121
|
+
@name = name.gsub(CASE_PATTERN, "-").downcase
|
|
122
|
+
else
|
|
123
|
+
@name
|
|
124
|
+
end
|
|
106
125
|
else
|
|
107
126
|
@name = block || name
|
|
108
127
|
end
|
|
109
128
|
end
|
|
110
129
|
|
|
130
|
+
##
|
|
131
|
+
# Set or get an agent's description
|
|
132
|
+
# @note
|
|
133
|
+
# This method serves as a self-documenting string.
|
|
134
|
+
# It is optional but recommended.
|
|
135
|
+
# @param [String] desc
|
|
136
|
+
# The agent's description
|
|
137
|
+
# @return [String, nil]
|
|
138
|
+
# Returns the agent's description
|
|
139
|
+
def self.description(desc = UNDEFINED, &block)
|
|
140
|
+
if desc.equal?(UNDEFINED)
|
|
141
|
+
@desc
|
|
142
|
+
else
|
|
143
|
+
@desc = block || desc
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
111
147
|
##
|
|
112
148
|
# Set or get the default model
|
|
113
149
|
# @param [String, nil] model
|
|
@@ -261,6 +297,20 @@ module LLM
|
|
|
261
297
|
end
|
|
262
298
|
end
|
|
263
299
|
|
|
300
|
+
##
|
|
301
|
+
# Set the file path where an agent's memory
|
|
302
|
+
# can be restored from, and written to.
|
|
303
|
+
# @param [String] path
|
|
304
|
+
# The path to a file
|
|
305
|
+
# @return [String, nil]
|
|
306
|
+
def self.path(path = UNDEFINED, &block)
|
|
307
|
+
if path.equal?(UNDEFINED)
|
|
308
|
+
@path
|
|
309
|
+
else
|
|
310
|
+
@path = path || block
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
264
314
|
##
|
|
265
315
|
# @param [LLM::Provider] llm
|
|
266
316
|
# A provider
|
|
@@ -277,8 +327,8 @@ module LLM
|
|
|
277
327
|
# @option params [Symbol, Array<Symbol>, nil] :concurrency Defaults to the agent class concurrency
|
|
278
328
|
def initialize(llm, params = {})
|
|
279
329
|
@llm = llm
|
|
280
|
-
fields = %i[name model skills schema tracer stream tools concurrency instructions confirm]
|
|
281
|
-
fields_ivar = %i[name tracer concurrency instructions confirm]
|
|
330
|
+
fields = %i[name description path model skills schema tracer stream tools concurrency instructions confirm]
|
|
331
|
+
fields_ivar = %i[name description path tracer concurrency instructions confirm]
|
|
282
332
|
fields.each do |field|
|
|
283
333
|
resolvable = params.key?(field) ? params.delete(field) : self.class.public_send(field)
|
|
284
334
|
resolve_symbol = !%i[concurrency].include?(field)
|
|
@@ -293,6 +343,7 @@ module LLM
|
|
|
293
343
|
end
|
|
294
344
|
end
|
|
295
345
|
@ctx = LLM::Context.new(llm, {guard: true}.merge(params))
|
|
346
|
+
@path and File.readable?(@path) ? @ctx.restore(path:) : nil
|
|
296
347
|
end
|
|
297
348
|
|
|
298
349
|
##
|
|
@@ -302,6 +353,21 @@ module LLM
|
|
|
302
353
|
@name
|
|
303
354
|
end
|
|
304
355
|
|
|
356
|
+
##
|
|
357
|
+
# Returns a file path where an agent's memory is
|
|
358
|
+
# restored from, and written to after each turn.
|
|
359
|
+
# @return [String, nil]
|
|
360
|
+
def path
|
|
361
|
+
@path
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
##
|
|
365
|
+
# Returns the agent's description
|
|
366
|
+
# @return [String, nil]
|
|
367
|
+
def description
|
|
368
|
+
@description
|
|
369
|
+
end
|
|
370
|
+
|
|
305
371
|
##
|
|
306
372
|
# Maintain a conversation via the chat completions API.
|
|
307
373
|
# This method immediately sends a request to the LLM and returns the response.
|
|
@@ -319,13 +385,17 @@ module LLM
|
|
|
319
385
|
# response = agent.talk("Hello, what is your name?")
|
|
320
386
|
# puts response.choices[0].content
|
|
321
387
|
def talk(prompt, params = {})
|
|
322
|
-
run_loop(prompt, params, :talk)
|
|
388
|
+
res = run_loop(prompt, params, :talk)
|
|
389
|
+
path ? @ctx.save(path:) : nil
|
|
390
|
+
res
|
|
323
391
|
end
|
|
324
392
|
|
|
325
393
|
##
|
|
326
394
|
# @see LLM::Context#ask
|
|
327
395
|
def ask(prompt, params = {})
|
|
328
|
-
run_loop(prompt, params, :ask)
|
|
396
|
+
res = run_loop(prompt, params, :ask)
|
|
397
|
+
path ? @ctx.save(path:) : nil
|
|
398
|
+
res
|
|
329
399
|
end
|
|
330
400
|
|
|
331
401
|
##
|
data/lib/llm/function/array.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
class LLM::Function
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::Function::Array} module extends the array
|
|
6
|
-
# returned by {LLM::Context#
|
|
6
|
+
# returned by {LLM::Context#pending_functions} with methods
|
|
7
7
|
# that can call all pending functions sequentially or
|
|
8
8
|
# concurrently. The return values can be reported back
|
|
9
9
|
# to the LLM on the next turn.
|
|
@@ -57,9 +57,9 @@ class LLM::Function
|
|
|
57
57
|
#
|
|
58
58
|
# @param [Symbol] strategy
|
|
59
59
|
# Controls concurrency strategy:
|
|
60
|
-
# - `:
|
|
60
|
+
# - `:sequential`: Call functions sequentially without spawning
|
|
61
61
|
# - `:thread`: Use threads
|
|
62
|
-
# - `:
|
|
62
|
+
# - `:async`: Use async tasks (requires async gem)
|
|
63
63
|
# - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
|
|
64
64
|
# - `:fork`: Use forked child processes
|
|
65
65
|
# - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
|
data/lib/llm/function.rb
CHANGED
|
@@ -204,7 +204,7 @@ class LLM::Function
|
|
|
204
204
|
@params = params
|
|
205
205
|
end
|
|
206
206
|
else
|
|
207
|
-
@params
|
|
207
|
+
@params || LLM::Schema::Object.new({})
|
|
208
208
|
end
|
|
209
209
|
end
|
|
210
210
|
|
|
@@ -227,7 +227,6 @@ class LLM::Function
|
|
|
227
227
|
@called = true
|
|
228
228
|
end
|
|
229
229
|
|
|
230
|
-
|
|
231
230
|
##
|
|
232
231
|
# Returns a function as a {LLM::Function::Task LLM::Function::Task}.
|
|
233
232
|
#
|
|
@@ -123,7 +123,7 @@ module LLM
|
|
|
123
123
|
# @param [LLM::Function] fn
|
|
124
124
|
# @return [Hash]
|
|
125
125
|
def adapt_function(fn)
|
|
126
|
-
params = fn.params
|
|
126
|
+
params = fn.params.to_h
|
|
127
127
|
{
|
|
128
128
|
type: "function",
|
|
129
129
|
function: {name: fn.name, description: fn.description, parameters: params}
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -102,7 +102,7 @@ module LLM
|
|
|
102
102
|
# @param [LLM::Function] fn
|
|
103
103
|
# @return [Hash]
|
|
104
104
|
def adapt_function(fn)
|
|
105
|
-
params = fn.params
|
|
105
|
+
params = fn.params.to_h
|
|
106
106
|
{
|
|
107
107
|
type: "function", name: fn.name,
|
|
108
108
|
function: {name: fn.name, description: fn.description, parameters: params}
|
|
@@ -92,7 +92,7 @@ class LLM::OpenAI
|
|
|
92
92
|
def adapt_function(fn)
|
|
93
93
|
{
|
|
94
94
|
type: "function", name: fn.name, description: fn.description,
|
|
95
|
-
parameters:
|
|
95
|
+
parameters: fn.params.to_h.merge(additionalProperties: false), strict: false
|
|
96
96
|
}.compact
|
|
97
97
|
end
|
|
98
98
|
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -156,7 +156,7 @@ module LLM
|
|
|
156
156
|
# @param [LLM::Function] fn
|
|
157
157
|
# @return [Hash]
|
|
158
158
|
def adapt_function(fn)
|
|
159
|
-
params = fn.params
|
|
159
|
+
params = fn.params.to_h
|
|
160
160
|
{
|
|
161
161
|
type: "function", name: fn.name,
|
|
162
162
|
function: {name: fn.name, description: fn.description, parameters: params}
|
|
@@ -14,12 +14,13 @@ class LLM::Repl
|
|
|
14
14
|
# It also maintains a cursor that tracks the active row
|
|
15
15
|
# by its index number. The streaming path reuses a single
|
|
16
16
|
# row by overwriting its contents repeatedly.
|
|
17
|
-
class
|
|
18
|
-
WIDTH = 80
|
|
19
|
-
|
|
17
|
+
class Buffer
|
|
20
18
|
##
|
|
21
|
-
# @
|
|
22
|
-
|
|
19
|
+
# @param [LLM::Repl] repl
|
|
20
|
+
# An instance of {LLM::Repl LLM::Repl}.
|
|
21
|
+
# @return [LLM::Repl::Buffer]
|
|
22
|
+
def initialize(repl)
|
|
23
|
+
@repl = repl
|
|
23
24
|
@rows = [[]]
|
|
24
25
|
@cursor = nil
|
|
25
26
|
@snapshot = nil
|
|
@@ -27,37 +28,45 @@ class LLM::Repl
|
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
##
|
|
30
|
-
# @param [String] chars
|
|
31
|
+
# @param [String, Array] chars
|
|
31
32
|
# @param [Object] attrs
|
|
32
33
|
# @param [Symbol] method
|
|
33
34
|
# @return [void]
|
|
34
35
|
def write(chars, attrs = nil, method: :append)
|
|
35
|
-
|
|
36
|
+
case chars
|
|
37
|
+
when Array then chunks = chars
|
|
38
|
+
else chunks = [Node.new(chars.to_s, attrs)]
|
|
39
|
+
end
|
|
36
40
|
self.method(method).call(chunks)
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
##
|
|
40
|
-
#
|
|
41
|
-
# @param [String]
|
|
44
|
+
# @param [String] user
|
|
45
|
+
# @param [String, Array] content
|
|
42
46
|
# @param [Symbol] method
|
|
43
47
|
# @return [void]
|
|
44
|
-
def
|
|
45
|
-
chunks =
|
|
46
|
-
|
|
48
|
+
def write_message(user, content, method: :append)
|
|
49
|
+
chunks = [Node.new("#{user}: ", Curses::A_BOLD)]
|
|
50
|
+
case content
|
|
51
|
+
when Array then chunks.concat(content)
|
|
52
|
+
else chunks.push(Node.new(content))
|
|
53
|
+
end
|
|
54
|
+
chunks.push(Node.new("\n"))
|
|
55
|
+
write(chunks, method:)
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
##
|
|
50
|
-
#
|
|
59
|
+
# Open the buffer.
|
|
51
60
|
# @return [void]
|
|
52
|
-
def
|
|
61
|
+
def open
|
|
53
62
|
@cursor = @rows.size - 1
|
|
54
63
|
@snapshot = @rows.map(&:dup)
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
##
|
|
58
|
-
#
|
|
67
|
+
# Close the buffer.
|
|
59
68
|
# @return [void]
|
|
60
|
-
def
|
|
69
|
+
def close
|
|
61
70
|
@cursor = nil
|
|
62
71
|
@snapshot = nil
|
|
63
72
|
end
|
|
@@ -93,9 +102,13 @@ class LLM::Repl
|
|
|
93
102
|
|
|
94
103
|
private
|
|
95
104
|
|
|
105
|
+
##
|
|
106
|
+
# @return [LLM::Repl]
|
|
107
|
+
attr_reader :repl
|
|
108
|
+
|
|
96
109
|
##
|
|
97
110
|
# Appends a new row
|
|
98
|
-
# @param [Array<
|
|
111
|
+
# @param [Array<Node>] chunks
|
|
99
112
|
# One or more chunks.
|
|
100
113
|
# @return [void]
|
|
101
114
|
def append(chunks)
|
|
@@ -104,12 +117,12 @@ class LLM::Repl
|
|
|
104
117
|
|
|
105
118
|
##
|
|
106
119
|
# Replaces the content of the active row
|
|
107
|
-
# @param [Array<
|
|
120
|
+
# @param [Array<Node>] chunks
|
|
108
121
|
# One or more chunks.
|
|
109
122
|
# @return [void]
|
|
110
123
|
def replace(chunks)
|
|
111
124
|
@rows = @snapshot.map(&:dup)
|
|
112
|
-
|
|
125
|
+
chunks.each { wrap(_1, @rows) }
|
|
113
126
|
end
|
|
114
127
|
|
|
115
128
|
##
|
|
@@ -123,10 +136,10 @@ class LLM::Repl
|
|
|
123
136
|
chunk[:text].to_s.each_char do |char|
|
|
124
137
|
if char == "\n"
|
|
125
138
|
rows << []
|
|
126
|
-
elsif char == " " and sum(rows.last) >=
|
|
139
|
+
elsif char == " " and sum(rows.last) >= repl.width
|
|
127
140
|
rows << []
|
|
128
141
|
else
|
|
129
|
-
rows.last <<
|
|
142
|
+
rows.last << Node.new(char, attrs)
|
|
130
143
|
end
|
|
131
144
|
end
|
|
132
145
|
end
|
data/lib/llm/repl/command.rb
CHANGED
|
@@ -176,12 +176,19 @@ class LLM::Repl
|
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
##
|
|
179
|
-
# Write a string to the
|
|
180
|
-
# @param [String]
|
|
179
|
+
# Write a string to the buffer
|
|
180
|
+
# @param [String] content
|
|
181
|
+
# @return [void]
|
|
182
|
+
def write(content)
|
|
183
|
+
write_message "command(#{self.class.name})", content
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
##
|
|
187
|
+
# @param [String] user
|
|
188
|
+
# @param [String] content
|
|
181
189
|
# @return [void]
|
|
182
|
-
def
|
|
183
|
-
@repl.
|
|
184
|
-
@repl.write(str)
|
|
190
|
+
def write_message(user, content)
|
|
191
|
+
@repl.write_message(user, content)
|
|
185
192
|
end
|
|
186
193
|
|
|
187
194
|
##
|
|
@@ -11,13 +11,11 @@ class LLM::Repl
|
|
|
11
11
|
# @return [void]
|
|
12
12
|
def call(name: nil)
|
|
13
13
|
if name.nil?
|
|
14
|
-
write(
|
|
14
|
+
write(self.class.help)
|
|
15
15
|
elsif command = LLM::Command.find_by(name:)
|
|
16
|
-
write(
|
|
16
|
+
write(command.help)
|
|
17
17
|
else
|
|
18
|
-
write "
|
|
19
|
-
"\nThat command doesn't exist." \
|
|
20
|
-
"\n\n"
|
|
18
|
+
write "no help for #{name} was found"
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
21
|
end
|
|
@@ -4,6 +4,11 @@ class LLM::Repl::Markdown
|
|
|
4
4
|
##
|
|
5
5
|
# Renders Kramdown `:table` nodes as aligned columns.
|
|
6
6
|
module Table
|
|
7
|
+
##
|
|
8
|
+
# @api private
|
|
9
|
+
Node = LLM::Repl::Node
|
|
10
|
+
private_constant :Node
|
|
11
|
+
|
|
7
12
|
##
|
|
8
13
|
# Renders a table node by collecting all cells first to
|
|
9
14
|
# compute column widths, then emitting each row with
|
|
@@ -15,7 +20,6 @@ class LLM::Repl::Markdown
|
|
|
15
20
|
rows.each do |row|
|
|
16
21
|
emit("| ", attrs)
|
|
17
22
|
row.each_with_index do |chunks, i|
|
|
18
|
-
text = chunks.map { _1[:text] }.join
|
|
19
23
|
width = widths[i]
|
|
20
24
|
chunks.each { |c| emit(c[:text].ljust(width), c[:attrs]) }
|
|
21
25
|
emit(" | ", attrs) unless i == row.size - 1
|
|
@@ -51,13 +55,13 @@ class LLM::Repl::Markdown
|
|
|
51
55
|
def walk_collect(node, attrs, chunks)
|
|
52
56
|
case node.type
|
|
53
57
|
when :text
|
|
54
|
-
chunks <<
|
|
58
|
+
chunks << Node.new(node.value.to_s, attrs)
|
|
55
59
|
when :strong
|
|
56
60
|
node.children.each { walk_collect(_1, Curses::A_BOLD, chunks) }
|
|
57
61
|
when :em
|
|
58
62
|
node.children.each { walk_collect(_1, Curses::A_UNDERLINE, chunks) }
|
|
59
63
|
when :codespan
|
|
60
|
-
chunks <<
|
|
64
|
+
chunks << Node.new(node.value, Curses::A_REVERSE)
|
|
61
65
|
when :a
|
|
62
66
|
node.children.each { walk_collect(_1, Curses::A_UNDERLINE, chunks) }
|
|
63
67
|
else
|
data/lib/llm/repl/markdown.rb
CHANGED
|
@@ -22,7 +22,7 @@ class LLM::Repl
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
##
|
|
25
|
-
# @return [Array<
|
|
25
|
+
# @return [Array<Node>]
|
|
26
26
|
def ast
|
|
27
27
|
@ast.tap do
|
|
28
28
|
##
|
|
@@ -102,14 +102,14 @@ class LLM::Repl
|
|
|
102
102
|
when :a
|
|
103
103
|
node.children.each { walk(_1, Curses::A_UNDERLINE) }
|
|
104
104
|
when :img
|
|
105
|
-
emit("[image: #{node.attr[
|
|
105
|
+
emit("[image: #{node.attr["alt"]}]", attrs)
|
|
106
106
|
else
|
|
107
107
|
node.children.each { walk(_1, attrs) }
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
def emit(text, attrs)
|
|
112
|
-
@ast.push(
|
|
112
|
+
@ast.push(Node.new(text.to_s, attrs))
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Repl::Node LLM::Repl::Node} class wraps a piece
|
|
6
|
+
# of text and optional curses attributes.
|
|
7
|
+
# @api private
|
|
8
|
+
class Node
|
|
9
|
+
##
|
|
10
|
+
# @return [String]
|
|
11
|
+
attr_reader :text
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# @return [Integer, nil]
|
|
15
|
+
attr_reader :attrs
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @param [String] text
|
|
19
|
+
# @param [Integer, nil] attrs
|
|
20
|
+
# @return [LLM::Repl::Node]
|
|
21
|
+
def initialize(text, attrs = nil)
|
|
22
|
+
@text = text.to_s
|
|
23
|
+
@attrs = attrs
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Hash-like lookup.
|
|
28
|
+
# @param [Symbol] key
|
|
29
|
+
# @return [String, Integer, nil]
|
|
30
|
+
def [](key)
|
|
31
|
+
case key
|
|
32
|
+
when :text then @text
|
|
33
|
+
when :attrs then @attrs
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/llm/repl/status.rb
CHANGED
|
@@ -7,11 +7,11 @@ class LLM::Repl
|
|
|
7
7
|
# @api private
|
|
8
8
|
class Status
|
|
9
9
|
##
|
|
10
|
-
# @param [LLM::
|
|
10
|
+
# @param [LLM::Repl] repl
|
|
11
11
|
# @return [LLM::Repl::Status]
|
|
12
|
-
def initialize(
|
|
13
|
-
@agent = agent
|
|
14
|
-
@provider = agent.llm.name
|
|
12
|
+
def initialize(repl)
|
|
13
|
+
@agent = repl.agent
|
|
14
|
+
@provider = @agent.llm.name
|
|
15
15
|
@text = "idle"
|
|
16
16
|
end
|
|
17
17
|
|