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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Function::Async
|
|
4
|
+
##
|
|
5
|
+
# Wraps an array of {Async::Task} objects running on a shared
|
|
6
|
+
# {LLM::Function::Async::Reactor}. The reactor is created on
|
|
7
|
+
# demand if not provided.
|
|
8
|
+
class Group < LLM::Function::Group
|
|
9
|
+
##
|
|
10
|
+
# @param [Array<Async::Task>] tasks
|
|
11
|
+
# @param [Hash] options
|
|
12
|
+
# @option options [LLM::Function::Async::Reactor] :reactor
|
|
13
|
+
def initialize(tasks, options = {})
|
|
14
|
+
@tasks = tasks
|
|
15
|
+
@reactor = options[:reactor] || LLM::Function::Async::Reactor.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# @return [nil]
|
|
20
|
+
def spawn
|
|
21
|
+
@tasks.each do |task|
|
|
22
|
+
task.reactor = @reactor
|
|
23
|
+
task.spawn
|
|
24
|
+
end
|
|
25
|
+
nil
|
|
26
|
+
ensure
|
|
27
|
+
@spawned = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
def alive?
|
|
33
|
+
@tasks.any?(&:alive?)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# @return [nil]
|
|
38
|
+
def interrupt!
|
|
39
|
+
@tasks.each(&:interrupt!)
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
alias_method :cancel!, :interrupt!
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# @return [Array<LLM::Function::Return>]
|
|
46
|
+
def wait
|
|
47
|
+
spawn unless @spawned
|
|
48
|
+
@tasks.map(&:wait)
|
|
49
|
+
ensure
|
|
50
|
+
@reactor.stop
|
|
51
|
+
end
|
|
52
|
+
alias_method :value, :wait
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Function::Async
|
|
4
|
+
##
|
|
5
|
+
# Manages an {::Async::Reactor} on a background thread. Work
|
|
6
|
+
# is submitted through a thread-safe queue and run inside the
|
|
7
|
+
# reactor. The reactor and its fibers stay on one thread.
|
|
8
|
+
class Reactor
|
|
9
|
+
##
|
|
10
|
+
# @return [Thread]
|
|
11
|
+
attr_reader :thread
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@inbox = Queue.new
|
|
15
|
+
@thread = ::Thread.new { run }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Submit a block to run inside the reactor.
|
|
20
|
+
# @return [nil]
|
|
21
|
+
def submit(&block)
|
|
22
|
+
@inbox << block
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Stop the reactor and wait for the thread to finish.
|
|
28
|
+
def stop
|
|
29
|
+
@inbox << :stop
|
|
30
|
+
@thread.join(5)
|
|
31
|
+
@thread.kill if @thread.alive?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def run
|
|
37
|
+
reactor = ::Async::Reactor.new
|
|
38
|
+
reactor.async do
|
|
39
|
+
loop do
|
|
40
|
+
work = @inbox.pop
|
|
41
|
+
break if work == :stop
|
|
42
|
+
reactor.async { work.call }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
reactor.run
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Function::Async
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Function::Async::Task} wraps a function call in an
|
|
6
|
+
# {Async::Task} running on a shared
|
|
7
|
+
# {LLM::Function::Async::Reactor}. The task is spawned lazily
|
|
8
|
+
# in {#wait} or explicitly in {#spawn}.
|
|
9
|
+
#
|
|
10
|
+
# Work is submitted to the reactor through its inbox queue.
|
|
11
|
+
# Results are bridged back through the task's own queue.
|
|
12
|
+
class Task < LLM::Function::Task
|
|
13
|
+
##
|
|
14
|
+
# @param [LLM::Function] fn
|
|
15
|
+
# @param [Hash] options
|
|
16
|
+
# @option options [LLM::Function::Async::Reactor] :reactor
|
|
17
|
+
def initialize(fn, options = {})
|
|
18
|
+
super
|
|
19
|
+
@reactor = options[:reactor]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# Assign the reactor. Used when a task is created before its
|
|
24
|
+
# reactor is available.
|
|
25
|
+
# @param [LLM::Function::Async::Reactor] reactor
|
|
26
|
+
def reactor=(reactor)
|
|
27
|
+
@reactor = reactor
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Submit the function call to the reactor. The result is
|
|
32
|
+
# pushed to a queue that {#wait} consumes.
|
|
33
|
+
# @return [nil]
|
|
34
|
+
def spawn
|
|
35
|
+
@queue = Queue.new
|
|
36
|
+
@alive = true
|
|
37
|
+
@reactor.submit do
|
|
38
|
+
@queue << function.call
|
|
39
|
+
rescue LLM::Interrupt => e
|
|
40
|
+
@queue << e
|
|
41
|
+
raise
|
|
42
|
+
end
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def alive?
|
|
49
|
+
@alive || false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Push an interrupt sentinel to the result queue. The reactor
|
|
54
|
+
# thread continues running but the result is discarded.
|
|
55
|
+
# @return [nil]
|
|
56
|
+
def interrupt!
|
|
57
|
+
if @queue
|
|
58
|
+
@alive = false
|
|
59
|
+
@queue << LLM::Interrupt.new
|
|
60
|
+
end
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
alias_method :cancel!, :interrupt!
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Wait for the result queue to contain a value.
|
|
67
|
+
# @return [LLM::Function::Return]
|
|
68
|
+
def wait
|
|
69
|
+
spawn unless @queue
|
|
70
|
+
result = @queue.pop
|
|
71
|
+
@alive = false
|
|
72
|
+
raise result if LLM::Interrupt === result
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
alias_method :value, :wait
|
|
76
|
+
|
|
77
|
+
##
|
|
78
|
+
# @return [Class]
|
|
79
|
+
def group_class
|
|
80
|
+
LLM::Function::Async::Group
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Function::Fiber
|
|
4
|
+
##
|
|
5
|
+
# Wraps an array of {Fiber::Task} objects for cooperative
|
|
6
|
+
# concurrent execution. Interrupts all tasks and waits
|
|
7
|
+
# for them to complete.
|
|
8
|
+
class Group < LLM::Function::Group
|
|
9
|
+
##
|
|
10
|
+
# @param [Array<LLM::Function::Fiber::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,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Function::Fiber
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Function::Fiber::Task LLM::Function::Fiber::Task}
|
|
6
|
+
# wraps a function call in a scheduler-backed fiber for
|
|
7
|
+
# cooperative concurrent execution. The fiber is created
|
|
8
|
+
# lazily when {#wait} is called, not at construction time.
|
|
9
|
+
#
|
|
10
|
+
# Requires `Fiber.scheduler` — without one, raise early in
|
|
11
|
+
# {#wait}. Interrupting a running task raises
|
|
12
|
+
# {LLM::Interrupt} on the fiber, which stops it at the next
|
|
13
|
+
# yield point.
|
|
14
|
+
class Task < LLM::Function::Task
|
|
15
|
+
##
|
|
16
|
+
# @param [LLM::Function] fn
|
|
17
|
+
# @param [Hash] options
|
|
18
|
+
def initialize(fn, options = {})
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# @return [nil]
|
|
24
|
+
def spawn
|
|
25
|
+
if Fiber.scheduler.nil?
|
|
26
|
+
raise ArgumentError, "Fiber concurrency requires Fiber.scheduler"
|
|
27
|
+
else
|
|
28
|
+
@fiber = Fiber.schedule { function.call }
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def alive?
|
|
36
|
+
@fiber&.alive? || false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# @return [nil]
|
|
41
|
+
def interrupt!
|
|
42
|
+
@fiber&.raise(LLM::Interrupt) if @fiber&.alive?
|
|
43
|
+
function.interrupt!
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
alias_method :cancel!, :interrupt!
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @return [LLM::Function::Return]
|
|
50
|
+
def wait
|
|
51
|
+
spawn unless @fiber
|
|
52
|
+
@result ||= @fiber.value
|
|
53
|
+
end
|
|
54
|
+
alias_method :value, :wait
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# @return [Class]
|
|
58
|
+
def group_class
|
|
59
|
+
LLM::Function::Fiber::Group
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module LLM::Function::Fork
|
|
4
4
|
##
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
class
|
|
5
|
+
# Wraps an array of {Fork::Task} objects that are running
|
|
6
|
+
# in forked child processes.
|
|
7
|
+
class Group < LLM::Function::Group
|
|
8
8
|
##
|
|
9
9
|
# @param [Array<LLM::Function::Task>] tasks
|
|
10
|
-
# @return [LLM::Function::Fork::Group]
|
|
11
10
|
def initialize(tasks)
|
|
12
11
|
@tasks = tasks
|
|
13
12
|
end
|
|
14
13
|
|
|
14
|
+
##
|
|
15
|
+
# @return [nil]
|
|
16
|
+
def spawn
|
|
17
|
+
@tasks.each(&:spawn)
|
|
18
|
+
nil
|
|
19
|
+
ensure
|
|
20
|
+
@spawned = true
|
|
21
|
+
end
|
|
22
|
+
|
|
15
23
|
##
|
|
16
24
|
# @return [Boolean]
|
|
17
25
|
def alive?
|
|
@@ -29,6 +37,7 @@ class LLM::Function
|
|
|
29
37
|
##
|
|
30
38
|
# @return [Array<LLM::Function::Return>]
|
|
31
39
|
def wait
|
|
40
|
+
spawn unless @spawned
|
|
32
41
|
@tasks.map(&:wait)
|
|
33
42
|
end
|
|
34
43
|
alias_method :value, :wait
|
|
@@ -24,6 +24,8 @@ class LLM::Function
|
|
|
24
24
|
runner = @function.runner
|
|
25
25
|
controller = setup(runner)
|
|
26
26
|
@ch.result.write([:result, call!(runner)])
|
|
27
|
+
rescue LLM::Interrupt
|
|
28
|
+
@ch.result.write([:interrupt])
|
|
27
29
|
rescue => ex
|
|
28
30
|
@ch.result.write([:result, error(ex)])
|
|
29
31
|
ensure
|
|
@@ -52,12 +54,11 @@ class LLM::Function
|
|
|
52
54
|
|
|
53
55
|
def setup(runner)
|
|
54
56
|
ready = Queue.new
|
|
55
|
-
thread = Thread.new do
|
|
57
|
+
thread = ::Thread.new do
|
|
56
58
|
ready << true
|
|
57
59
|
kind = @ch.control.recv
|
|
58
60
|
next unless kind == :interrupt
|
|
59
|
-
|
|
60
|
-
runner.public_send(hook) if hook
|
|
61
|
+
::Thread.main.raise(LLM::Interrupt)
|
|
61
62
|
rescue IOError, ArgumentError
|
|
62
63
|
end
|
|
63
64
|
ready.pop
|
|
@@ -4,31 +4,39 @@ class LLM::Function
|
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::Function::Fork::Task} class wraps a fork-backed function call
|
|
6
6
|
# and exchanges control and result messages with the child process.
|
|
7
|
-
class Fork::Task
|
|
7
|
+
class Fork::Task < LLM::Function::Task
|
|
8
8
|
##
|
|
9
|
-
# @param [LLM::Function]
|
|
10
|
-
# @param [
|
|
11
|
-
# @
|
|
9
|
+
# @param [LLM::Function] fn
|
|
10
|
+
# @param [Hash] options
|
|
11
|
+
# @option options [LLM::Tracer, nil] :tracer
|
|
12
12
|
# @return [LLM::Function::Fork::Task]
|
|
13
|
-
def initialize(
|
|
14
|
-
|
|
15
|
-
@tracer = tracer
|
|
16
|
-
@
|
|
13
|
+
def initialize(fn, options = {})
|
|
14
|
+
super
|
|
15
|
+
@tracer = options.fetch(:tracer, nil)
|
|
16
|
+
@spawned = false
|
|
17
17
|
@waited = false
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
##
|
|
21
21
|
# @return [LLM::Function::Fork::Task]
|
|
22
22
|
def spawn
|
|
23
|
-
@
|
|
23
|
+
@span = @tracer&.on_tool_start(
|
|
24
|
+
id: @function.id, name: @function.name,
|
|
25
|
+
arguments: @function.arguments, model: @function.model
|
|
26
|
+
)
|
|
27
|
+
@ch = LLM::Object.from(
|
|
28
|
+
control: xchan(:marshal),
|
|
29
|
+
result: xchan(:marshal, sock: Socket::SOCK_STREAM)
|
|
30
|
+
)
|
|
24
31
|
@pid = Kernel.fork { Fork::Job.new(@function, @ch).call }
|
|
32
|
+
@spawned = true
|
|
25
33
|
self
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
##
|
|
29
37
|
# @return [Boolean]
|
|
30
38
|
def alive?
|
|
31
|
-
return false if @waited
|
|
39
|
+
return false if @waited || !@pid
|
|
32
40
|
result = ::Process.waitpid(@pid, ::Process::WNOHANG)
|
|
33
41
|
@waited = !result.nil?
|
|
34
42
|
!@waited
|
|
@@ -51,7 +59,9 @@ class LLM::Function
|
|
|
51
59
|
##
|
|
52
60
|
# @return [LLM::Function::Return]
|
|
53
61
|
def wait
|
|
62
|
+
spawn unless @spawned
|
|
54
63
|
kind, data = @ch.result.recv
|
|
64
|
+
raise LLM::Interrupt if kind == :interrupt
|
|
55
65
|
raise ArgumentError, "Unknown fork message: #{kind.inspect}" unless kind == :result
|
|
56
66
|
result = Return.new(data[:id], data[:name], data[:value])
|
|
57
67
|
reap
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Function
|
|
4
|
+
##
|
|
5
|
+
# Abstract base class for all concurrency strategy groups.
|
|
6
|
+
# Each strategy (sequential, thread, fiber, async, fork, ractor)
|
|
7
|
+
# subclasses this and implements {#alive?}, {#interrupt!}, and
|
|
8
|
+
# {#wait}.
|
|
9
|
+
class Group
|
|
10
|
+
##
|
|
11
|
+
# @abstract
|
|
12
|
+
# @return [nil]
|
|
13
|
+
def spawn
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @abstract
|
|
19
|
+
# @return [Boolean]
|
|
20
|
+
def alive?
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# @abstract
|
|
26
|
+
# @return [nil]
|
|
27
|
+
def interrupt!
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
end
|
|
30
|
+
alias_method :cancel!, :interrupt!
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @abstract
|
|
34
|
+
# @return [Array<LLM::Function::Return>]
|
|
35
|
+
def wait
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
alias_method :value, :wait
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module LLM::Function::Ractor
|
|
4
4
|
##
|
|
5
|
-
#
|
|
6
|
-
# {LLM::Function::Ractor::Task} objects that are running
|
|
5
|
+
# Wraps an array of {Ractor::Task} objects that are running
|
|
7
6
|
# {LLM::Function} calls concurrently.
|
|
8
|
-
class
|
|
7
|
+
class Group < LLM::Function::Group
|
|
9
8
|
##
|
|
10
9
|
# @param [Array<LLM::Function::Task>] tasks
|
|
11
|
-
# @return [LLM::Function::Ractor::Group]
|
|
12
10
|
def initialize(tasks)
|
|
13
11
|
@tasks = tasks
|
|
14
12
|
end
|
|
15
13
|
|
|
14
|
+
##
|
|
15
|
+
# @return [nil]
|
|
16
|
+
def spawn
|
|
17
|
+
@tasks.each(&:spawn)
|
|
18
|
+
nil
|
|
19
|
+
ensure
|
|
20
|
+
@spawned = true
|
|
21
|
+
end
|
|
22
|
+
|
|
16
23
|
##
|
|
17
24
|
# @return [Boolean]
|
|
18
25
|
def alive?
|
|
@@ -30,6 +37,7 @@ class LLM::Function
|
|
|
30
37
|
##
|
|
31
38
|
# @return [Array<LLM::Function::Return>]
|
|
32
39
|
def wait
|
|
40
|
+
spawn unless @spawned
|
|
33
41
|
@tasks.map(&:wait)
|
|
34
42
|
end
|
|
35
43
|
alias_method :value, :wait
|
|
@@ -35,22 +35,38 @@ class LLM::Function
|
|
|
35
35
|
waiters = []
|
|
36
36
|
loop do
|
|
37
37
|
case ::Ractor.receive
|
|
38
|
-
in [:done, *
|
|
38
|
+
in [:done, *data]
|
|
39
|
+
result ||= data
|
|
39
40
|
done = true
|
|
40
41
|
waiters.each { _1.send(result) }
|
|
42
|
+
break unless waiters.empty?
|
|
41
43
|
waiters.clear
|
|
42
44
|
in [:alive?, reply]
|
|
43
45
|
reply.send(!done)
|
|
44
46
|
in [:wait, reply]
|
|
45
|
-
done
|
|
47
|
+
if done
|
|
48
|
+
reply.send(result)
|
|
49
|
+
break
|
|
50
|
+
else
|
|
51
|
+
waiters << reply
|
|
52
|
+
end
|
|
53
|
+
in [:interrupt]
|
|
54
|
+
@tool&.send(:interrupt)
|
|
46
55
|
end
|
|
47
56
|
end
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
def spawn
|
|
51
|
-
::Ractor.new(@mailbox, @runner_class, @id, @name, @arguments) do |mailbox, runner_class, id, name, arguments|
|
|
60
|
+
@tool = ::Ractor.new(@mailbox, @runner_class, @id, @name, @arguments) do |mailbox, runner_class, id, name, arguments|
|
|
61
|
+
::Thread.new do
|
|
62
|
+
::Ractor.receive == :interrupt or next
|
|
63
|
+
::Thread.main.raise(LLM::Interrupt)
|
|
64
|
+
rescue ::Ractor::Error
|
|
65
|
+
end
|
|
52
66
|
kwargs = Hash === arguments ? arguments.transform_keys(&:to_sym) : arguments
|
|
53
67
|
mailbox.send([:done, id, name, runner_class.new.call(**kwargs)])
|
|
68
|
+
rescue LLM::Interrupt
|
|
69
|
+
mailbox.send([:done, id, name, {cancelled: true, reason: "interrupted"}])
|
|
54
70
|
rescue => ex
|
|
55
71
|
mailbox.send([:done, id, name, {error: true, type: ex.class.name, message: ex.message}])
|
|
56
72
|
end
|
|
@@ -20,6 +20,8 @@ class LLM::Function
|
|
|
20
20
|
# @return [Boolean]
|
|
21
21
|
def alive?
|
|
22
22
|
request(:alive?)
|
|
23
|
+
rescue ::Ractor::ClosedError
|
|
24
|
+
false
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
##
|
|
@@ -28,6 +30,13 @@ class LLM::Function
|
|
|
28
30
|
request(:wait)
|
|
29
31
|
end
|
|
30
32
|
|
|
33
|
+
##
|
|
34
|
+
# @return [nil]
|
|
35
|
+
def interrupt!
|
|
36
|
+
task.send([:interrupt])
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
private
|
|
32
41
|
|
|
33
42
|
def request(type)
|
|
@@ -5,31 +5,38 @@ class LLM::Function
|
|
|
5
5
|
# The {LLM::Function::Ractor::Task} class wraps a ractor-backed function
|
|
6
6
|
# call and delegates mailbox coordination to
|
|
7
7
|
# {LLM::Function::Ractor::Mailbox}.
|
|
8
|
-
class Ractor::Task
|
|
8
|
+
class Ractor::Task < LLM::Function::Task
|
|
9
9
|
##
|
|
10
10
|
# @return [LLM::Function::Ractor::Mailbox]
|
|
11
11
|
attr_reader :mailbox
|
|
12
12
|
|
|
13
13
|
##
|
|
14
|
-
# @param [
|
|
15
|
-
# @param [
|
|
16
|
-
# @
|
|
17
|
-
# @
|
|
18
|
-
# @
|
|
19
|
-
# @
|
|
14
|
+
# @param [LLM::Function] fn
|
|
15
|
+
# @param [Hash] options
|
|
16
|
+
# @option options [LLM::Tracer, nil] :tracer
|
|
17
|
+
# @option options [Class] :runner_class
|
|
18
|
+
# @option options [String, nil] :id
|
|
19
|
+
# @option options [String] :name
|
|
20
|
+
# @option options [Hash, Array, nil] :arguments
|
|
21
|
+
# @option options [String, nil] :model
|
|
20
22
|
# @return [LLM::Function::Ractor::Task]
|
|
21
|
-
def initialize(
|
|
22
|
-
|
|
23
|
-
@
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@
|
|
23
|
+
def initialize(fn, options = {})
|
|
24
|
+
super
|
|
25
|
+
@runner_class = options.fetch(:runner_class)
|
|
26
|
+
@id = options.fetch(:id)
|
|
27
|
+
@name = options.fetch(:name)
|
|
28
|
+
@arguments = options.fetch(:arguments)
|
|
29
|
+
@model = options.fetch(:model, nil)
|
|
30
|
+
@tracer = options.fetch(:tracer, nil)
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
##
|
|
31
34
|
# @return [LLM::Function::Ractor::Task]
|
|
32
35
|
def spawn
|
|
36
|
+
@span = @tracer&.on_tool_start(
|
|
37
|
+
id: @id, name: @name,
|
|
38
|
+
arguments: @arguments, model: @model
|
|
39
|
+
)
|
|
33
40
|
@mailbox = Ractor::Mailbox.new(build_task)
|
|
34
41
|
self
|
|
35
42
|
end
|
|
@@ -37,12 +44,13 @@ class LLM::Function
|
|
|
37
44
|
##
|
|
38
45
|
# @return [Boolean]
|
|
39
46
|
def alive?
|
|
40
|
-
mailbox
|
|
47
|
+
@mailbox&.alive? || false
|
|
41
48
|
end
|
|
42
49
|
|
|
43
50
|
##
|
|
44
51
|
# @return [nil]
|
|
45
52
|
def interrupt!
|
|
53
|
+
mailbox&.interrupt!
|
|
46
54
|
nil
|
|
47
55
|
end
|
|
48
56
|
alias_method :cancel!, :interrupt!
|
|
@@ -50,6 +58,7 @@ class LLM::Function
|
|
|
50
58
|
##
|
|
51
59
|
# @return [LLM::Function::Return]
|
|
52
60
|
def wait
|
|
61
|
+
spawn unless @mailbox
|
|
53
62
|
id, name, value = mailbox.wait
|
|
54
63
|
result = Return.new(id, name, value)
|
|
55
64
|
@tracer&.on_tool_finish(result:, span: @span)
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module LLM::Function::Sequential
|
|
4
4
|
##
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# It provides the same basic interface as the concurrent group
|
|
5
|
+
# Wraps an array of {LLM::Function} objects for sequential
|
|
6
|
+
# execution. Provides the same interface as concurrent group
|
|
9
7
|
# wrappers so callers can flow through `spawn(strategy).wait`
|
|
10
|
-
# uniformly
|
|
11
|
-
class
|
|
8
|
+
# uniformly.
|
|
9
|
+
class Group < LLM::Function::Group
|
|
12
10
|
##
|
|
13
11
|
# @param [Array<LLM::Function>] functions
|
|
14
|
-
# @return [LLM::Function::CallGroup]
|
|
15
12
|
def initialize(functions)
|
|
16
13
|
@functions = functions
|
|
14
|
+
@owner = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @return [nil]
|
|
19
|
+
def spawn
|
|
20
|
+
# no-op — execution happens in wait
|
|
21
|
+
nil
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
##
|
|
@@ -25,6 +30,7 @@ class LLM::Function
|
|
|
25
30
|
##
|
|
26
31
|
# @return [nil]
|
|
27
32
|
def interrupt!
|
|
33
|
+
@owner&.raise(LLM::Interrupt)
|
|
28
34
|
nil
|
|
29
35
|
end
|
|
30
36
|
alias_method :cancel!, :interrupt!
|
|
@@ -32,7 +38,10 @@ class LLM::Function
|
|
|
32
38
|
##
|
|
33
39
|
# @return [Array<LLM::Function::Return>]
|
|
34
40
|
def wait
|
|
41
|
+
@owner = Thread.current
|
|
35
42
|
@functions.map(&:call)
|
|
43
|
+
ensure
|
|
44
|
+
@owner = nil
|
|
36
45
|
end
|
|
37
46
|
alias_method :value, :wait
|
|
38
47
|
end
|