llm.rb 12.6.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 +571 -13
- data/LICENSE +21 -93
- data/README.md +183 -167
- data/bin/llm.rb +124 -0
- data/data/deepinfra.json +3 -0
- data/data/xai.json +1 -1
- data/lib/llm/a2a.rb +1 -1
- data/lib/llm/active_record/acts_as_llm.rb +6 -6
- data/lib/llm/agent.rb +136 -27
- data/lib/llm/buffer.rb +85 -3
- data/lib/llm/compactor/null.rb +19 -0
- data/lib/llm/compactor/truncate.rb +80 -0
- data/lib/llm/compactor.rb +42 -124
- data/lib/llm/context.rb +31 -37
- data/lib/llm/contract.rb +4 -25
- data/lib/llm/function/array.rb +18 -17
- data/lib/llm/function/async/group.rb +54 -0
- data/lib/llm/function/async/reactor.rb +48 -0
- data/lib/llm/function/async/task.rb +83 -0
- data/lib/llm/function/fiber/group.rb +46 -0
- data/lib/llm/function/fiber/task.rb +62 -0
- data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
- data/lib/llm/function/fork/job.rb +2 -2
- data/lib/llm/function/fork/task.rb +19 -10
- data/lib/llm/function/group.rb +40 -0
- data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
- data/lib/llm/function/ractor/job.rb +9 -3
- data/lib/llm/function/ractor/mailbox.rb +2 -0
- data/lib/llm/function/ractor/task.rb +23 -15
- data/lib/llm/function/{call_group.rb → sequential/group.rb} +12 -8
- data/lib/llm/function/sequential/task.rb +49 -0
- data/lib/llm/function/task.rb +25 -48
- data/lib/llm/function/thread/group.rb +46 -0
- data/lib/llm/function/thread/task.rb +60 -0
- data/lib/llm/function.rb +54 -65
- data/lib/llm/loop_guard.rb +1 -2
- data/lib/llm/mcp.rb +22 -0
- data/lib/llm/object.rb +2 -1
- data/lib/llm/provider.rb +6 -3
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
- data/lib/llm/providers/google.rb +2 -2
- 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 +47 -13
- data/lib/llm/repl/commands/compact.rb +33 -0
- data/lib/llm/repl/commands/help.rb +3 -5
- data/lib/llm/repl/input.rb +80 -15
- data/lib/llm/repl/markdown/table.rb +80 -0
- data/lib/llm/repl/markdown.rb +33 -3
- data/lib/llm/repl/node.rb +37 -0
- data/lib/llm/repl/status.rb +4 -4
- data/lib/llm/repl/stream.rb +12 -5
- data/lib/llm/repl/walker.rb +46 -0
- data/lib/llm/repl/window.rb +31 -32
- data/lib/llm/repl.rb +70 -38
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +5 -0
- data/lib/llm/schema/object.rb +11 -5
- data/lib/llm/sequel/plugin.rb +6 -6
- data/lib/llm/skill.rb +20 -4
- data/lib/llm/stream.rb +24 -17
- data/lib/llm/tool.rb +20 -4
- data/lib/llm/tools/chdir.rb +0 -2
- data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
- data/lib/llm/tools/git.rb +11 -4
- data/lib/llm/tools/mkdir.rb +4 -1
- data/lib/llm/tools/pwd.rb +0 -2
- data/lib/llm/tools/read_file.rb +0 -2
- data/lib/llm/tools/rg.rb +11 -4
- data/lib/llm/tools/ruby.rb +46 -0
- data/lib/llm/tools/shell.rb +11 -4
- data/lib/llm/tools/utils.rb +31 -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/lib/llm.rb +25 -5
- data/llm.gemspec +11 -5
- data/resources/deepdive.md +45 -1198
- metadata +39 -17
- data/lib/llm/function/call_task.rb +0 -46
- data/lib/llm/function/fiber_group.rb +0 -105
- data/lib/llm/function/task_group.rb +0 -97
- data/lib/llm/function/thread_group.rb +0 -102
data/lib/llm/function.rb
CHANGED
|
@@ -2,29 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
##
|
|
4
4
|
# The {LLM::Function LLM::Function} class represents a local
|
|
5
|
-
# function that can be called by an LLM.
|
|
5
|
+
# function that can be called by an LLM. Most users should define
|
|
6
|
+
# tools as subclasses of {LLM::Tool} instead — Function is the
|
|
7
|
+
# lower-level building block that Tool wraps.
|
|
6
8
|
#
|
|
7
|
-
# @example
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# {
|
|
9
|
+
# @example Tool subclass (preferred for most users)
|
|
10
|
+
# class ReadFile < LLM::Tool
|
|
11
|
+
# name "read-file"
|
|
12
|
+
# description "Read a file from disk"
|
|
13
|
+
# parameter :path, String, "The filename or path"
|
|
14
|
+
# required %i[path]
|
|
15
|
+
#
|
|
16
|
+
# def call(path:)
|
|
17
|
+
# {contents: File.read(path)}
|
|
16
18
|
# end
|
|
17
19
|
# end
|
|
18
20
|
#
|
|
19
|
-
# @example
|
|
20
|
-
#
|
|
21
|
-
# name "
|
|
22
|
-
# description "Runs
|
|
23
|
-
# params do |schema|
|
|
21
|
+
# @example Inline function (block-form DSL)
|
|
22
|
+
# LLM.function(:run_command) do |fn|
|
|
23
|
+
# fn.name "run-command"
|
|
24
|
+
# fn.description "Runs a shell command"
|
|
25
|
+
# fn.params do |schema|
|
|
24
26
|
# schema.object(command: schema.string.required)
|
|
25
27
|
# end
|
|
26
|
-
#
|
|
27
|
-
# def call(command:)
|
|
28
|
+
# fn.define do |command:|
|
|
28
29
|
# {success: Kernel.system(command)}
|
|
29
30
|
# end
|
|
30
31
|
# end
|
|
@@ -32,16 +33,21 @@ class LLM::Function
|
|
|
32
33
|
require_relative "function/registry"
|
|
33
34
|
require_relative "function/tracing"
|
|
34
35
|
require_relative "function/array"
|
|
35
|
-
require_relative "function/
|
|
36
|
-
require_relative "function/
|
|
36
|
+
require_relative "function/group"
|
|
37
|
+
require_relative "function/sequential/group"
|
|
37
38
|
require_relative "function/task"
|
|
38
|
-
require_relative "function/
|
|
39
|
-
require_relative "function/
|
|
40
|
-
require_relative "function/
|
|
39
|
+
require_relative "function/sequential/task"
|
|
40
|
+
require_relative "function/thread/task"
|
|
41
|
+
require_relative "function/fiber/task"
|
|
42
|
+
require_relative "function/async/reactor"
|
|
43
|
+
require_relative "function/async/task"
|
|
44
|
+
require_relative "function/thread/group"
|
|
45
|
+
require_relative "function/fiber/group"
|
|
46
|
+
require_relative "function/async/group"
|
|
41
47
|
require_relative "function/fork"
|
|
42
|
-
require_relative "function/
|
|
48
|
+
require_relative "function/fork/group"
|
|
43
49
|
require_relative "function/ractor"
|
|
44
|
-
require_relative "function/
|
|
50
|
+
require_relative "function/ractor/group"
|
|
45
51
|
|
|
46
52
|
extend LLM::Function::Registry
|
|
47
53
|
prepend LLM::Function::Tracing
|
|
@@ -198,7 +204,7 @@ class LLM::Function
|
|
|
198
204
|
@params = params
|
|
199
205
|
end
|
|
200
206
|
else
|
|
201
|
-
@params
|
|
207
|
+
@params || LLM::Schema::Object.new({})
|
|
202
208
|
end
|
|
203
209
|
end
|
|
204
210
|
|
|
@@ -213,70 +219,59 @@ class LLM::Function
|
|
|
213
219
|
|
|
214
220
|
##
|
|
215
221
|
# Call the function
|
|
216
|
-
# @return [LLM::Function::Return]
|
|
222
|
+
# @return [LLM::Function::Return]
|
|
217
223
|
def call
|
|
218
|
-
|
|
224
|
+
llm = @tracer&.llm
|
|
225
|
+
llm ? llm.with_tracer(@tracer) { call_function } : call_function
|
|
219
226
|
ensure
|
|
220
227
|
@called = true
|
|
221
228
|
end
|
|
222
229
|
|
|
223
230
|
##
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
# This is the low-level method that powers concurrent tool execution.
|
|
227
|
-
# Prefer the collection methods on {LLM::Context#functions} for most
|
|
228
|
-
# use cases: {LLM::Function::Array#call}, {LLM::Function::Array#wait},
|
|
229
|
-
# or {LLM::Function::Array#spawn}.
|
|
231
|
+
# Returns a function as a {LLM::Function::Task LLM::Function::Task}.
|
|
230
232
|
#
|
|
231
233
|
# @example
|
|
232
|
-
# #
|
|
233
|
-
# ctx.talk(ctx.
|
|
234
|
+
# # As a group
|
|
235
|
+
# ctx.talk(ctx.pending_functions.wait)
|
|
234
236
|
#
|
|
235
|
-
# #
|
|
236
|
-
# task = tool.
|
|
237
|
+
# # As a task
|
|
238
|
+
# task = tool.task(:thread)
|
|
237
239
|
# result = task.value
|
|
238
240
|
#
|
|
239
241
|
# @param [Symbol] strategy
|
|
240
242
|
# Controls concurrency strategy:
|
|
241
|
-
# - `:
|
|
243
|
+
# - `:sequential`: Call the function sequentially
|
|
242
244
|
# - `:thread`: Use threads
|
|
243
|
-
# - `:
|
|
245
|
+
# - `:async`: Use async tasks (requires async gem)
|
|
244
246
|
# - `:fork`: Use a forked child process (requires xchan.rb support)
|
|
245
247
|
# - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
|
|
246
|
-
# - `:fork`: Use a forked child process (requires xchan.rb support)
|
|
247
248
|
# - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
|
|
248
249
|
#
|
|
249
250
|
# @return [LLM::Function::Task]
|
|
250
251
|
# Returns a task whose `#value` is an {LLM::Function::Return}.
|
|
251
|
-
def
|
|
252
|
-
|
|
253
|
-
when :
|
|
254
|
-
|
|
255
|
-
when :
|
|
252
|
+
def task(strategy, options = {})
|
|
253
|
+
case strategy
|
|
254
|
+
when :sequential
|
|
255
|
+
Sequential::Task.new(self, options)
|
|
256
|
+
when :async
|
|
256
257
|
LLM.require "async" unless defined?(::Async)
|
|
257
|
-
Async
|
|
258
|
+
Async::Task.new(self, options)
|
|
258
259
|
when :thread
|
|
259
|
-
Thread.new
|
|
260
|
+
Thread::Task.new(self, options)
|
|
260
261
|
when :fiber
|
|
261
|
-
|
|
262
|
-
Fiber.schedule { call! }
|
|
262
|
+
Fiber::Task.new(self, options)
|
|
263
263
|
when :fork
|
|
264
|
-
LLM.require "xchan" unless defined?(::Chan::UNIXSocket)
|
|
265
|
-
|
|
266
|
-
Fork::Task.new(self, tracer: @tracer, span:).spawn
|
|
264
|
+
LLM.require "xchan", "~> 0.22" unless defined?(::Chan::UNIXSocket)
|
|
265
|
+
Fork::Task.new(self, options.merge(tracer: @tracer))
|
|
267
266
|
when :ractor
|
|
268
267
|
raise LLM::RactorError, "Ractor concurrency only supports class-based tools" unless Class === @runner
|
|
269
268
|
if @runner.respond_to?(:skill?) && @runner.skill?
|
|
270
269
|
raise LLM::RactorError, "Ractor concurrency does not support skill-backed tools"
|
|
271
270
|
end
|
|
272
|
-
|
|
273
|
-
Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:).spawn
|
|
271
|
+
Ractor::Task.new(self, options.merge(runner_class: @runner, id:, name:, arguments:, tracer: @tracer, model:))
|
|
274
272
|
else
|
|
275
|
-
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :
|
|
273
|
+
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :sequential, :thread, :fiber, :async, :fork, or :ractor"
|
|
276
274
|
end
|
|
277
|
-
Task.new(task, self)
|
|
278
|
-
ensure
|
|
279
|
-
@called = true
|
|
280
275
|
end
|
|
281
276
|
|
|
282
277
|
##
|
|
@@ -285,7 +280,7 @@ class LLM::Function
|
|
|
285
280
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
286
281
|
# ctx = LLM::Context.new(llm, tools: [fn1, fn2])
|
|
287
282
|
# ctx.talk "I want to run the functions"
|
|
288
|
-
# ctx.talk ctx.
|
|
283
|
+
# ctx.talk ctx.pending_functions.map(&:cancel)
|
|
289
284
|
# @return [LLM::Function::Return]
|
|
290
285
|
def cancel(reason: "function call cancelled")
|
|
291
286
|
Return.new(id, name, {cancelled: true, reason:})
|
|
@@ -387,10 +382,4 @@ class LLM::Function
|
|
|
387
382
|
rescue => ex
|
|
388
383
|
Return.new(id, name, {error: true, type: ex.class.name, message: ex.message})
|
|
389
384
|
end
|
|
390
|
-
|
|
391
|
-
def call!
|
|
392
|
-
llm = @tracer&.llm
|
|
393
|
-
return call unless llm.respond_to?(:with_tracer)
|
|
394
|
-
llm.with_tracer(@tracer) { call }
|
|
395
|
-
end
|
|
396
385
|
end
|
data/lib/llm/loop_guard.rb
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
# should be blocked before the loop keeps going.
|
|
10
10
|
#
|
|
11
11
|
# {LLM::LoopGuard LLM::LoopGuard} detects when a context is repeating the same
|
|
12
|
-
# tool-call pattern instead of making progress.
|
|
13
|
-
# General Intelligence Systems and its doom-loop detection approach.
|
|
12
|
+
# tool-call pattern instead of making progress.
|
|
14
13
|
#
|
|
15
14
|
# The public interface is intentionally small:
|
|
16
15
|
# - `call(ctx)` returns `nil` when no intervention is needed
|
data/lib/llm/mcp.rb
CHANGED
|
@@ -13,6 +13,28 @@
|
|
|
13
13
|
# An MCP client is stateful. Coordinate lifecycle operations such as
|
|
14
14
|
# {#start} and {#stop}; request methods can be issued concurrently and
|
|
15
15
|
# responses are matched by JSON-RPC id.
|
|
16
|
+
#
|
|
17
|
+
# @example stdio transport
|
|
18
|
+
# llm = LLM.deepseek(key: ENV["KEY"])
|
|
19
|
+
# mcp = LLM::MCP.stdio(argv: ["npx", "-y", "@forgejo/mcp-server"])
|
|
20
|
+
# agent = LLM::Agent.new(llm)
|
|
21
|
+
#
|
|
22
|
+
# # Preferred: session keeps one process alive across multiple calls
|
|
23
|
+
# mcp.session do
|
|
24
|
+
# agent.talk "What's happening on forgejo?", tools: mcp.tools
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # Also works: one-shot, spawns a new process per call
|
|
28
|
+
# agent.talk "What's happening on forgejo?", tools: mcp.tools
|
|
29
|
+
#
|
|
30
|
+
# @example HTTP transport
|
|
31
|
+
# mcp = LLM::MCP.http(
|
|
32
|
+
# url: "https://api.githubcopilot.com/mcp/",
|
|
33
|
+
# headers: {"Authorization" => "Bearer #{ENV.fetch('GITHUB_PAT')}"},
|
|
34
|
+
# transport: :net_http_persistent
|
|
35
|
+
# )
|
|
36
|
+
# agent = LLM::Agent.new(llm)
|
|
37
|
+
# agent.talk "What's happening on GitHub?", tools: mcp.tools
|
|
16
38
|
class LLM::MCP
|
|
17
39
|
require_relative "mcp/error"
|
|
18
40
|
require_relative "mcp/command"
|
data/lib/llm/object.rb
CHANGED
|
@@ -133,7 +133,8 @@ class LLM::Object < BasicObject
|
|
|
133
133
|
# @return [Object]
|
|
134
134
|
def fetch(k = UNDEFINED, *args, &b)
|
|
135
135
|
return SINGLETON.get(@h, :fetch) if k.equal?(UNDEFINED)
|
|
136
|
-
|
|
136
|
+
key = SINGLETON.key(@h, k)
|
|
137
|
+
@h.fetch(key || k, *args, &b)
|
|
137
138
|
end
|
|
138
139
|
|
|
139
140
|
##
|
data/lib/llm/provider.rb
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
##
|
|
4
|
-
# The Provider class
|
|
5
|
-
#
|
|
4
|
+
# The Provider class is the abstract base for LLM service integrations.
|
|
5
|
+
# Most users interact with providers through {LLM::Agent} or
|
|
6
|
+
# {LLM::Context} rather than calling {#complete} directly.
|
|
6
7
|
#
|
|
7
8
|
# @abstract
|
|
8
9
|
class LLM::Provider
|
|
@@ -82,7 +83,9 @@ class LLM::Provider
|
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
##
|
|
85
|
-
# Provides an interface to the chat completions API
|
|
86
|
+
# Provides an interface to the chat completions API.
|
|
87
|
+
# Most users should use {LLM::Context#talk} or {LLM::Agent#talk} instead.
|
|
88
|
+
#
|
|
86
89
|
# @example
|
|
87
90
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
88
91
|
# messages = [{role: "system", content: "Your task is to answer all of my questions"}]
|
data/lib/llm/providers/google.rb
CHANGED
|
@@ -209,11 +209,11 @@ module LLM
|
|
|
209
209
|
role, model, stream = params.delete(:role),
|
|
210
210
|
params.delete(:model),
|
|
211
211
|
LLM::Stream.try(params.delete(:stream))
|
|
212
|
-
[params
|
|
212
|
+
[params, stream, tools, role, model]
|
|
213
213
|
end
|
|
214
214
|
|
|
215
215
|
def build_complete_request(prompt, params, role, model, stream)
|
|
216
|
-
action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
|
|
216
|
+
action = stream.enabled? ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
|
|
217
217
|
model.respond_to?(:id) ? model.id : model
|
|
218
218
|
path = ["/v1beta/models/#{model}", action].join(":")
|
|
219
219
|
req = LLM::Transport::Request.post(path, headers)
|
|
@@ -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
|
@@ -11,6 +11,17 @@ class LLM::Repl
|
|
|
11
11
|
SINGLETON = self
|
|
12
12
|
private_constant :UNDEFINED, :SINGLETON
|
|
13
13
|
|
|
14
|
+
##
|
|
15
|
+
# @param [String] str
|
|
16
|
+
# An input string
|
|
17
|
+
# @return [Array<String>]
|
|
18
|
+
# An array of command names who match the input string
|
|
19
|
+
def self.complete(str)
|
|
20
|
+
registry.keys.select do |name|
|
|
21
|
+
name.start_with?(str[1..])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
14
25
|
##
|
|
15
26
|
# @api private
|
|
16
27
|
Parameter = Struct.new(:name, :type, :description, :options, :index, :value) do
|
|
@@ -62,31 +73,37 @@ class LLM::Repl
|
|
|
62
73
|
if input != UNDEFINED
|
|
63
74
|
return nil unless input[0] == "/"
|
|
64
75
|
n, = input.split(" ")
|
|
65
|
-
registry.find { n[1..] == _1.name }
|
|
76
|
+
registry.values.find { n[1..] == _1.name }
|
|
66
77
|
elsif name != UNDEFINED
|
|
67
|
-
registry.find { name == _1.name }
|
|
78
|
+
registry.values.find { name == _1.name }
|
|
68
79
|
else
|
|
69
80
|
raise ArgumentError, "provide either an input or a name"
|
|
70
81
|
end
|
|
71
82
|
end
|
|
72
83
|
|
|
73
84
|
##
|
|
74
|
-
# @param [LLM::Repl::Command]
|
|
85
|
+
# @param [LLM::Repl::Command] outer
|
|
75
86
|
# A new subclass
|
|
76
87
|
# @return [void]
|
|
77
|
-
def self.inherited(
|
|
88
|
+
def self.inherited(outer)
|
|
78
89
|
LLM.lock(:inherited) do
|
|
79
|
-
registry
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
@registry[outer] = outer
|
|
91
|
+
outer.instance_variable_set(:@parameters, {})
|
|
92
|
+
outer.define_singleton_method(:inherited) do |inner|
|
|
93
|
+
SINGLETON.inherited(inner)
|
|
94
|
+
inner.instance_variable_set(:@name, outer.instance_variable_get(:@name))
|
|
95
|
+
inner.instance_variable_set(:@description, outer.instance_variable_get(:@description))
|
|
96
|
+
inner.instance_variable_set(:@parameters, outer.instance_variable_get(:@parameters))
|
|
97
|
+
end
|
|
82
98
|
end
|
|
83
99
|
end
|
|
84
100
|
|
|
85
101
|
##
|
|
86
102
|
# @return [Array<LLM::Repl::Command]
|
|
87
103
|
def self.registry
|
|
88
|
-
@registry
|
|
104
|
+
@registry.transform_keys(&:name)
|
|
89
105
|
end
|
|
106
|
+
@registry = {}
|
|
90
107
|
|
|
91
108
|
##
|
|
92
109
|
# Set or get a command name.
|
|
@@ -142,20 +159,36 @@ class LLM::Repl
|
|
|
142
159
|
end
|
|
143
160
|
end
|
|
144
161
|
|
|
162
|
+
##
|
|
163
|
+
# @return [LLM::Repl]
|
|
164
|
+
attr_reader :repl
|
|
165
|
+
|
|
166
|
+
##
|
|
167
|
+
# @return [LLM::Agent]
|
|
168
|
+
attr_reader :agent
|
|
169
|
+
|
|
145
170
|
##
|
|
146
171
|
# @param [LLM::Repl] repl
|
|
147
172
|
# @return [LLM::Repl::Command]
|
|
148
173
|
def initialize(repl)
|
|
149
174
|
@repl = repl
|
|
175
|
+
@agent = repl.agent
|
|
150
176
|
end
|
|
151
177
|
|
|
152
178
|
##
|
|
153
|
-
# Write a string to the
|
|
154
|
-
# @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
|
|
155
189
|
# @return [void]
|
|
156
|
-
def
|
|
157
|
-
@repl.
|
|
158
|
-
@repl.write(str)
|
|
190
|
+
def write_message(user, content)
|
|
191
|
+
@repl.write_message(user, content)
|
|
159
192
|
end
|
|
160
193
|
|
|
161
194
|
##
|
|
@@ -191,6 +224,7 @@ class LLM::Repl
|
|
|
191
224
|
self.class.parameters
|
|
192
225
|
end
|
|
193
226
|
|
|
227
|
+
require_relative "commands/compact"
|
|
194
228
|
require_relative "commands/exit"
|
|
195
229
|
require_relative "commands/help"
|
|
196
230
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The 'compact' command frees space in the
|
|
6
|
+
# context window and llm.rb is designed to
|
|
7
|
+
# support multiple compaction strategies with
|
|
8
|
+
# different trade offs. This command, though,
|
|
9
|
+
# uses the 'truncate' strategy. See
|
|
10
|
+
# {LLM::Compactor::Truncate LLM::Compactor::Truncate}
|
|
11
|
+
# for more details.
|
|
12
|
+
class Command::Compact < Command
|
|
13
|
+
name "compact"
|
|
14
|
+
description "frees space in the context window"
|
|
15
|
+
parameter :n, String, "the number of messages to keep"
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @return [void]
|
|
19
|
+
def call(n: 128)
|
|
20
|
+
write "compact in progress"
|
|
21
|
+
compactor.call(keep: n)
|
|
22
|
+
write "compact complete"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# @return [LLM::Compactor::Truncate]
|
|
29
|
+
def compactor
|
|
30
|
+
@compactor ||= LLM::Compactor::Truncate.new(agent)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -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
|