llm.rb 12.5.1 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +482 -0
- data/LICENSE +21 -93
- data/README.md +49 -159
- 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_agent.rb +32 -0
- data/lib/llm/active_record/acts_as_llm.rb +6 -6
- data/lib/llm/agent.rb +101 -26
- 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 +33 -37
- data/lib/llm/contract.rb +4 -25
- data/lib/llm/function/array.rb +15 -14
- 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 +4 -3
- data/lib/llm/function/fork/task.rb +20 -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 +19 -3
- data/lib/llm/function/ractor/mailbox.rb +9 -0
- data/lib/llm/function/ractor/task.rb +24 -15
- data/lib/llm/function/{call_group.rb → sequential/group.rb} +17 -8
- data/lib/llm/function/sequential/task.rb +49 -0
- data/lib/llm/function/task.rb +25 -37
- data/lib/llm/function/thread/group.rb +46 -0
- data/lib/llm/function/thread/task.rb +60 -0
- data/lib/llm/function/tracing.rb +2 -0
- data/lib/llm/function.rb +56 -64
- 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/google.rb +2 -2
- data/lib/llm/repl/command.rb +35 -8
- data/lib/llm/repl/commands/compact.rb +33 -0
- data/lib/llm/repl/input.rb +80 -15
- data/lib/llm/repl/markdown/table.rb +76 -0
- data/lib/llm/repl/markdown.rb +31 -1
- data/lib/llm/repl/status.rb +1 -1
- data/lib/llm/repl/stream.rb +10 -3
- data/lib/llm/repl/transcript.rb +1 -1
- data/lib/llm/repl/walker.rb +46 -0
- data/lib/llm/repl.rb +18 -12
- 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/agent.rb +32 -0
- data/lib/llm/sequel/plugin.rb +6 -6
- data/lib/llm/stream.rb +24 -17
- data/lib/llm/tool/param.rb +12 -0
- data/lib/llm/tool.rb +20 -4
- data/lib/llm/tools/chdir.rb +0 -2
- data/lib/llm/tools/git.rb +8 -4
- data/lib/llm/tools/mkdir.rb +1 -1
- data/lib/llm/tools/pwd.rb +0 -2
- data/lib/llm/tools/read_file.rb +0 -2
- data/lib/llm/tools/rg.rb +8 -4
- data/lib/llm/tools/shell.rb +8 -4
- data/lib/llm/tools/utils.rb +31 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +25 -5
- data/llm.gemspec +3 -3
- data/resources/deepdive.md +693 -57
- metadata +24 -13
- 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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Function::Sequential
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Function::Sequential::Task} class wraps a single
|
|
6
|
+
# direct function call behind the same task-like interface
|
|
7
|
+
# used by spawned concurrency modes.
|
|
8
|
+
class Task < LLM::Function::Task
|
|
9
|
+
##
|
|
10
|
+
# @param [LLM::Function] fn
|
|
11
|
+
# @param [Hash] options
|
|
12
|
+
def initialize(fn, options = {})
|
|
13
|
+
@function = fn
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# @return [nil]
|
|
18
|
+
def spawn
|
|
19
|
+
# no-op — execution happens in wait
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
def alive?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# @return [nil]
|
|
30
|
+
def interrupt!
|
|
31
|
+
function.interrupt!
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
alias_method :cancel!, :interrupt!
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# @return [LLM::Function::Return]
|
|
38
|
+
def wait
|
|
39
|
+
@result ||= function.call
|
|
40
|
+
end
|
|
41
|
+
alias_method :value, :wait
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# @return [Class]
|
|
45
|
+
def group_class
|
|
46
|
+
LLM::Function::Sequential::Group
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/llm/function/task.rb
CHANGED
|
@@ -2,71 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
class LLM::Function
|
|
4
4
|
##
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# This class is the superclass that all concurrency
|
|
6
|
+
# strategies must subclass in order to implement their
|
|
7
|
+
# own Task class. It provides a common interface that
|
|
8
|
+
# is the same across all concurrency strategies.
|
|
8
9
|
class Task
|
|
9
10
|
##
|
|
10
|
-
# @return [
|
|
11
|
-
attr_reader :
|
|
11
|
+
# @return [LLM::Function]
|
|
12
|
+
attr_reader :function
|
|
12
13
|
|
|
13
14
|
##
|
|
14
|
-
# @
|
|
15
|
-
|
|
15
|
+
# @param [LLM::Function] fn
|
|
16
|
+
# @param [Hash] options
|
|
17
|
+
# An optional set of options that are specific
|
|
18
|
+
# to a given concurrency strategy.
|
|
19
|
+
def initialize(fn, options = {})
|
|
20
|
+
@function = fn
|
|
21
|
+
end
|
|
16
22
|
|
|
17
23
|
##
|
|
18
|
-
# @
|
|
19
|
-
# @
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@task = task
|
|
23
|
-
@function = function
|
|
24
|
+
# @abstract
|
|
25
|
+
# @return [nil]
|
|
26
|
+
def spawn
|
|
27
|
+
raise NotImplementedError
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
##
|
|
31
|
+
# @abstract
|
|
27
32
|
# @return [Boolean]
|
|
28
33
|
def alive?
|
|
29
|
-
|
|
30
|
-
false
|
|
34
|
+
raise NotImplementedError
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
##
|
|
38
|
+
# @abstract
|
|
34
39
|
# @return [nil]
|
|
35
40
|
def interrupt!
|
|
36
|
-
|
|
37
|
-
function&.interrupt!
|
|
38
|
-
nil
|
|
41
|
+
raise NotImplementedError
|
|
39
42
|
end
|
|
40
43
|
alias_method :cancel!, :interrupt!
|
|
41
44
|
|
|
42
45
|
##
|
|
46
|
+
# @abstract
|
|
43
47
|
# @return [LLM::Function::Return]
|
|
44
48
|
def wait
|
|
45
|
-
|
|
46
|
-
task.value
|
|
47
|
-
elsif Fiber === task
|
|
48
|
-
fiber.alive? ? scheduler.run : nil
|
|
49
|
-
task.value
|
|
50
|
-
else
|
|
51
|
-
task.wait
|
|
52
|
-
end
|
|
49
|
+
raise NotImplementedError
|
|
53
50
|
end
|
|
54
51
|
alias_method :value, :wait
|
|
55
52
|
|
|
56
53
|
##
|
|
54
|
+
# @abstract
|
|
57
55
|
# @return [Class]
|
|
58
56
|
def group_class
|
|
59
|
-
|
|
60
|
-
when Thread then LLM::Function::ThreadGroup
|
|
61
|
-
when Fiber then LLM::Function::FiberGroup
|
|
62
|
-
else LLM::Function::TaskGroup
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def scheduler
|
|
69
|
-
Fiber.scheduler
|
|
57
|
+
raise NotImplementedError
|
|
70
58
|
end
|
|
71
59
|
end
|
|
72
60
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Function::Thread
|
|
4
|
+
##
|
|
5
|
+
# Wraps an array of {Thread::Task} objects for concurrent
|
|
6
|
+
# thread-based execution. Interrupts all tasks and waits
|
|
7
|
+
# for them to complete.
|
|
8
|
+
class Group < LLM::Function::Group
|
|
9
|
+
##
|
|
10
|
+
# @param [Array<LLM::Function::Thread::Task>] tasks
|
|
11
|
+
def initialize(tasks)
|
|
12
|
+
@tasks = tasks
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# @return [nil]
|
|
17
|
+
def spawn
|
|
18
|
+
@tasks.each(&:spawn)
|
|
19
|
+
nil
|
|
20
|
+
ensure
|
|
21
|
+
@spawned = true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def alive?
|
|
27
|
+
@tasks.any?(&:alive?)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @return [nil]
|
|
32
|
+
def interrupt!
|
|
33
|
+
@tasks.each(&:interrupt!)
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
alias_method :cancel!, :interrupt!
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# @return [Array<LLM::Function::Return>]
|
|
40
|
+
def wait
|
|
41
|
+
spawn unless @spawned
|
|
42
|
+
@tasks.map(&:wait)
|
|
43
|
+
end
|
|
44
|
+
alias_method :value, :wait
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Function::Thread
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Function::Thread::Task LLM::Function::Thread::Task}
|
|
6
|
+
# wraps a function call in a background thread for concurrent
|
|
7
|
+
# tool execution. The thread is created lazily when {#wait} is
|
|
8
|
+
# called, not when the task is constructed — so you can build
|
|
9
|
+
# a task, pass it around, and decide when to run it.
|
|
10
|
+
#
|
|
11
|
+
# Interrupting a running task raises {LLM::Interrupt} inside
|
|
12
|
+
# the thread, which stops the tool call mid-flight. The thread
|
|
13
|
+
# is created with `report_on_exception` disabled so unhandled
|
|
14
|
+
# exceptions propagate through {#wait} instead of to stderr.
|
|
15
|
+
class Task < LLM::Function::Task
|
|
16
|
+
##
|
|
17
|
+
# @param [LLM::Function] fn
|
|
18
|
+
# @param [Hash] options
|
|
19
|
+
def initialize(fn, options = {})
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @return [nil]
|
|
25
|
+
def spawn
|
|
26
|
+
@thread = ::Thread.new { function.call }
|
|
27
|
+
@thread.report_on_exception = false
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def alive?
|
|
34
|
+
@thread&.alive? || false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# @return [nil]
|
|
39
|
+
def interrupt!
|
|
40
|
+
@thread&.raise(LLM::Interrupt) if @thread&.alive?
|
|
41
|
+
function.interrupt!
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
alias_method :cancel!, :interrupt!
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# @return [LLM::Function::Return]
|
|
48
|
+
def wait
|
|
49
|
+
spawn unless @thread
|
|
50
|
+
@thread.value
|
|
51
|
+
end
|
|
52
|
+
alias_method :value, :wait
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# @return [Class]
|
|
56
|
+
def group_class
|
|
57
|
+
LLM::Function::Thread::Group
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/llm/function/tracing.rb
CHANGED
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
|
|
@@ -213,70 +219,60 @@ 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
|
|
|
230
|
+
|
|
223
231
|
##
|
|
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}.
|
|
232
|
+
# Returns a function as a {LLM::Function::Task LLM::Function::Task}.
|
|
230
233
|
#
|
|
231
234
|
# @example
|
|
232
|
-
# #
|
|
233
|
-
# ctx.talk(ctx.
|
|
235
|
+
# # As a group
|
|
236
|
+
# ctx.talk(ctx.pending_functions.wait)
|
|
234
237
|
#
|
|
235
|
-
# #
|
|
236
|
-
# task = tool.
|
|
238
|
+
# # As a task
|
|
239
|
+
# task = tool.task(:thread)
|
|
237
240
|
# result = task.value
|
|
238
241
|
#
|
|
239
242
|
# @param [Symbol] strategy
|
|
240
243
|
# Controls concurrency strategy:
|
|
241
|
-
# - `:
|
|
244
|
+
# - `:sequential`: Call the function sequentially
|
|
242
245
|
# - `:thread`: Use threads
|
|
243
|
-
# - `:
|
|
246
|
+
# - `:async`: Use async tasks (requires async gem)
|
|
244
247
|
# - `:fork`: Use a forked child process (requires xchan.rb support)
|
|
245
248
|
# - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
|
|
246
|
-
# - `:fork`: Use a forked child process (requires xchan.rb support)
|
|
247
249
|
# - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
|
|
248
250
|
#
|
|
249
251
|
# @return [LLM::Function::Task]
|
|
250
252
|
# Returns a task whose `#value` is an {LLM::Function::Return}.
|
|
251
|
-
def
|
|
252
|
-
|
|
253
|
-
when :
|
|
254
|
-
|
|
255
|
-
when :
|
|
253
|
+
def task(strategy, options = {})
|
|
254
|
+
case strategy
|
|
255
|
+
when :sequential
|
|
256
|
+
Sequential::Task.new(self, options)
|
|
257
|
+
when :async
|
|
256
258
|
LLM.require "async" unless defined?(::Async)
|
|
257
|
-
Async
|
|
259
|
+
Async::Task.new(self, options)
|
|
258
260
|
when :thread
|
|
259
|
-
Thread.new
|
|
261
|
+
Thread::Task.new(self, options)
|
|
260
262
|
when :fiber
|
|
261
|
-
|
|
262
|
-
Fiber.schedule { call! }
|
|
263
|
+
Fiber::Task.new(self, options)
|
|
263
264
|
when :fork
|
|
264
|
-
LLM.require "xchan" unless defined?(::Chan::UNIXSocket)
|
|
265
|
-
|
|
266
|
-
Fork::Task.new(self, tracer: @tracer, span:).spawn
|
|
265
|
+
LLM.require "xchan", "~> 0.22" unless defined?(::Chan::UNIXSocket)
|
|
266
|
+
Fork::Task.new(self, options.merge(tracer: @tracer))
|
|
267
267
|
when :ractor
|
|
268
268
|
raise LLM::RactorError, "Ractor concurrency only supports class-based tools" unless Class === @runner
|
|
269
269
|
if @runner.respond_to?(:skill?) && @runner.skill?
|
|
270
270
|
raise LLM::RactorError, "Ractor concurrency does not support skill-backed tools"
|
|
271
271
|
end
|
|
272
|
-
|
|
273
|
-
Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:).spawn
|
|
272
|
+
Ractor::Task.new(self, options.merge(runner_class: @runner, id:, name:, arguments:, tracer: @tracer, model:))
|
|
274
273
|
else
|
|
275
|
-
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :
|
|
274
|
+
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :sequential, :thread, :fiber, :async, :fork, or :ractor"
|
|
276
275
|
end
|
|
277
|
-
Task.new(task, self)
|
|
278
|
-
ensure
|
|
279
|
-
@called = true
|
|
280
276
|
end
|
|
281
277
|
|
|
282
278
|
##
|
|
@@ -285,7 +281,7 @@ class LLM::Function
|
|
|
285
281
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
286
282
|
# ctx = LLM::Context.new(llm, tools: [fn1, fn2])
|
|
287
283
|
# ctx.talk "I want to run the functions"
|
|
288
|
-
# ctx.talk ctx.
|
|
284
|
+
# ctx.talk ctx.pending_functions.map(&:cancel)
|
|
289
285
|
# @return [LLM::Function::Return]
|
|
290
286
|
def cancel(reason: "function call cancelled")
|
|
291
287
|
Return.new(id, name, {cancelled: true, reason:})
|
|
@@ -382,13 +378,9 @@ class LLM::Function
|
|
|
382
378
|
runner = self.runner
|
|
383
379
|
kwargs = arguments.respond_to?(:to_h) ? arguments.to_h.transform_keys(&:to_sym) : arguments
|
|
384
380
|
Return.new(id, name, runner.call(**kwargs))
|
|
381
|
+
rescue LLM::Interrupt
|
|
382
|
+
raise
|
|
385
383
|
rescue => ex
|
|
386
384
|
Return.new(id, name, {error: true, type: ex.class.name, message: ex.message})
|
|
387
385
|
end
|
|
388
|
-
|
|
389
|
-
def call!
|
|
390
|
-
llm = @tracer&.llm
|
|
391
|
-
return call unless llm.respond_to?(:with_tracer)
|
|
392
|
-
llm.with_tracer(@tracer) { call }
|
|
393
|
-
end
|
|
394
386
|
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)
|
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,11 +159,20 @@ 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
|
##
|
|
@@ -191,6 +217,7 @@ class LLM::Repl
|
|
|
191
217
|
self.class.parameters
|
|
192
218
|
end
|
|
193
219
|
|
|
220
|
+
require_relative "commands/compact"
|
|
194
221
|
require_relative "commands/exit"
|
|
195
222
|
require_relative "commands/help"
|
|
196
223
|
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\n")
|
|
21
|
+
compactor.call(keep: n)
|
|
22
|
+
write("compact complete\n\n")
|
|
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
|