phronomy 0.20.0 → 0.22.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/.mutant.yml +3 -1
- data/CHANGELOG.md +41 -11
- data/CONTRIBUTING.md +44 -6
- data/README.md +26 -6
- data/docs/decisions/010-cooperative-first-concurrency.md +86 -69
- data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +184 -0
- data/docs/features.md +22 -13
- data/docs/getting-started.md +36 -18
- data/docs/runtime-and-concurrency.md +110 -147
- data/examples/workflows/agent_event_mapping.rb +27 -30
- data/lib/phronomy/agent/async_event_api.rb +14 -4
- data/lib/phronomy/agent/tool_executor.rb +7 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +142 -245
- data/lib/phronomy/engine/task.rb +50 -6
- data/lib/phronomy/invocation_context.rb +11 -52
- data/lib/phronomy/llm_adapter/base.rb +29 -32
- data/lib/phronomy/llm_adapter/ruby_llm.rb +13 -12
- data/lib/phronomy/llm_adapter.rb +10 -7
- data/lib/phronomy/output_parser/base.rb +5 -1
- data/lib/phronomy/tool/base.rb +15 -0
- data/lib/phronomy/tool.rb +11 -0
- data/lib/phronomy/vector_store/async_backend.rb +15 -38
- data/lib/phronomy/vector_store/base.rb +12 -13
- data/lib/phronomy/vector_store/embeddings/base.rb +11 -9
- data/lib/phronomy/version.rb +1 -1
- data/scripts/run_mutation.sh +2 -1
- data/sig/phronomy/agent.rbs +36 -0
- data/sig/phronomy/extensions.rbs +50 -0
- data/sig/phronomy/llm_adapter.rbs +11 -0
- data/sig/phronomy/persistence.rbs +70 -0
- data/sig/phronomy/runtime.rbs +43 -0
- data/sig/phronomy/tool.rbs +39 -0
- data/sig/phronomy/workflow.rbs +30 -0
- data/sig/phronomy.rbs +49 -1
- metadata +12 -2
data/lib/phronomy/engine/task.rb
CHANGED
|
@@ -3,20 +3,29 @@
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
# A thread-free asynchronous completion handle.
|
|
5
5
|
#
|
|
6
|
-
# Task
|
|
7
|
-
# OffloadPool. Task
|
|
8
|
-
#
|
|
6
|
+
# Task does not execute work. Execution belongs to EventLoop/FSMSession or
|
|
7
|
+
# OffloadPool. Task represents completion, failure, cancellation, callbacks,
|
|
8
|
+
# and a blocking wait for callers outside EventLoop.
|
|
9
|
+
#
|
|
10
|
+
# Framework components own Task settlement. Application code should observe a
|
|
11
|
+
# Task through {#wait_result}, {#on_complete}, {#map}, and state readers rather
|
|
12
|
+
# than calling {#complete}, {#fail}, or {#cancel!}. Operation-wide cancellation
|
|
13
|
+
# is supplied through the CancellationToken accepted by the API that created
|
|
14
|
+
# the Task.
|
|
9
15
|
class Task
|
|
10
16
|
STATES = %i[pending completed failed cancelled].freeze
|
|
11
17
|
TERMINAL_STATES = %i[completed failed cancelled].freeze
|
|
12
18
|
private_constant :TERMINAL_STATES
|
|
13
19
|
|
|
20
|
+
# Creates an unsettled completion handle for framework-owned execution.
|
|
21
|
+
# @api private
|
|
14
22
|
def self.deferred(name: nil, parent: nil)
|
|
15
23
|
new(name: name, parent: parent)
|
|
16
24
|
end
|
|
17
25
|
|
|
18
26
|
attr_reader :name, :parent
|
|
19
27
|
|
|
28
|
+
# @api private
|
|
20
29
|
def initialize(name: nil, parent: nil)
|
|
21
30
|
@name = name
|
|
22
31
|
@parent = parent
|
|
@@ -30,20 +39,35 @@ module Phronomy
|
|
|
30
39
|
parent&.register_child(self)
|
|
31
40
|
end
|
|
32
41
|
|
|
42
|
+
# @return [Symbol] :pending, :completed, :failed, or :cancelled
|
|
43
|
+
# @api public
|
|
33
44
|
def status
|
|
34
45
|
@mutex.synchronize { @status }
|
|
35
46
|
end
|
|
36
47
|
|
|
48
|
+
# @return [Boolean] whether the Task has reached a terminal state
|
|
49
|
+
# @api public
|
|
37
50
|
def done?
|
|
38
51
|
@mutex.synchronize { TERMINAL_STATES.include?(@status) }
|
|
39
52
|
end
|
|
40
53
|
|
|
54
|
+
# @return [Boolean] whether the Task has not yet reached a terminal state
|
|
55
|
+
# @api public
|
|
41
56
|
def alive?
|
|
42
57
|
!done?
|
|
43
58
|
end
|
|
44
59
|
|
|
45
|
-
# Blocks the calling thread until settlement.
|
|
46
|
-
#
|
|
60
|
+
# Blocks the calling thread until settlement.
|
|
61
|
+
#
|
|
62
|
+
# EventLoop is never allowed to wait for a Task; framework continuation must
|
|
63
|
+
# proceed through explicit events. The optional timeout is waiter-local: it
|
|
64
|
+
# does not settle or cancel the Task.
|
|
65
|
+
#
|
|
66
|
+
# @param timeout [Numeric, nil] maximum seconds this caller will block
|
|
67
|
+
# @return [Object] the completed value
|
|
68
|
+
# @raise [Phronomy::TimeoutError] when the waiter-local timeout expires
|
|
69
|
+
# @raise [Exception] the error that settled the Task
|
|
70
|
+
# @api public
|
|
47
71
|
def wait_result(timeout: nil)
|
|
48
72
|
if Phronomy::Runtime.in_event_loop_context? && !done?
|
|
49
73
|
raise Phronomy::EventLoopReentrancyError,
|
|
@@ -71,8 +95,9 @@ module Phronomy
|
|
|
71
95
|
value
|
|
72
96
|
end
|
|
73
97
|
|
|
74
|
-
# Compatibility wait that does not re-raise the
|
|
98
|
+
# Compatibility wait that does not re-raise the Task error.
|
|
75
99
|
# Returns self when settled, nil on timeout.
|
|
100
|
+
# @api private
|
|
76
101
|
def join(limit = nil)
|
|
77
102
|
if Phronomy::Runtime.in_event_loop_context? && !done?
|
|
78
103
|
raise Phronomy::EventLoopReentrancyError,
|
|
@@ -96,8 +121,15 @@ module Phronomy
|
|
|
96
121
|
|
|
97
122
|
# Registers an independent completion notification.
|
|
98
123
|
#
|
|
124
|
+
# The callback execution thread is not guaranteed. It may be the caller that
|
|
125
|
+
# registers after settlement, an OffloadPool worker, or a framework control
|
|
126
|
+
# thread. Callbacks must therefore be thread-safe and should complete quickly.
|
|
99
127
|
# A callback failure is logged and does not suppress delivery to other
|
|
100
128
|
# completion callbacks or change the Task's already-settled result.
|
|
129
|
+
#
|
|
130
|
+
# @yield [value, error]
|
|
131
|
+
# @return [self]
|
|
132
|
+
# @api public
|
|
101
133
|
def on_complete(&callback)
|
|
102
134
|
raise ArgumentError, "on_complete requires a block" unless callback
|
|
103
135
|
|
|
@@ -113,15 +145,25 @@ module Phronomy
|
|
|
113
145
|
self
|
|
114
146
|
end
|
|
115
147
|
|
|
148
|
+
# Settles this Task successfully. Framework-owned settlement API.
|
|
149
|
+
# @api private
|
|
116
150
|
def complete(value = nil)
|
|
117
151
|
settle!(:completed, value: value)
|
|
118
152
|
end
|
|
119
153
|
|
|
154
|
+
# Settles this Task with a failure. Framework-owned settlement API.
|
|
155
|
+
# @api private
|
|
120
156
|
def fail(error)
|
|
121
157
|
raise ArgumentError, "error is required" unless error
|
|
122
158
|
settle!(:failed, error: error)
|
|
123
159
|
end
|
|
124
160
|
|
|
161
|
+
# Settles this Task as cancelled. Framework-owned settlement API.
|
|
162
|
+
#
|
|
163
|
+
# This method does not propagate backwards into a CancellationToken that may
|
|
164
|
+
# have been used to create the Task. Tokens can be shared across operations;
|
|
165
|
+
# operation-wide cancellation is owned by the creating API.
|
|
166
|
+
# @api private
|
|
125
167
|
def cancel!(error = Phronomy::CancellationError.new("Task cancelled"))
|
|
126
168
|
changed = settle!(:cancelled, error: error)
|
|
127
169
|
if changed
|
|
@@ -131,6 +173,8 @@ module Phronomy
|
|
|
131
173
|
self
|
|
132
174
|
end
|
|
133
175
|
|
|
176
|
+
# Creates a derived Task by transforming this Task's successful result.
|
|
177
|
+
# @api public
|
|
134
178
|
def map(&block)
|
|
135
179
|
raise ArgumentError, "map requires a block" unless block
|
|
136
180
|
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
# Carries all per-invocation context values through the call stack.
|
|
5
5
|
#
|
|
6
|
-
# +InvocationContext+ is a plain
|
|
7
|
-
#
|
|
8
|
-
# Pass it explicitly wherever context needs to cross a method boundary
|
|
9
|
-
# or be handed to a child {Task} / {TaskGroup}.
|
|
6
|
+
# +InvocationContext+ is a plain struct-like value carrier that replaces
|
|
7
|
+
# ad-hoc +Thread.current[...]+ propagation.
|
|
8
|
+
# Pass it explicitly wherever context needs to cross a method boundary.
|
|
10
9
|
#
|
|
11
10
|
# @example Build a context for a new agent invocation
|
|
12
11
|
# ctx = Phronomy::InvocationContext.new(
|
|
@@ -14,40 +13,12 @@ module Phronomy
|
|
|
14
13
|
# cancellation_token: Phronomy::Concurrency::CancellationToken.timeout_after(30)
|
|
15
14
|
# )
|
|
16
15
|
# agent.invoke("Hello", invocation_context: ctx)
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
17
18
|
class InvocationContext
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# @return [String, nil] session identifier (e.g. Rails session id)
|
|
22
|
-
attr_reader :session_id
|
|
23
|
-
|
|
24
|
-
# @return [String, nil] end-user identifier for tracing / audit
|
|
25
|
-
attr_reader :user_id
|
|
26
|
-
|
|
27
|
-
# @return [CancellationToken, nil]
|
|
28
|
-
attr_reader :cancellation_token
|
|
29
|
-
|
|
30
|
-
# @return [Deadline, nil]
|
|
31
|
-
attr_reader :deadline
|
|
32
|
-
|
|
33
|
-
# @return [Object, nil] OpenTelemetry / tracing span
|
|
34
|
-
attr_reader :tracer_span
|
|
35
|
-
|
|
36
|
-
# @return [Integer, nil] max tokens the agent may consume this invocation
|
|
37
|
-
attr_reader :token_budget
|
|
38
|
-
|
|
39
|
-
# @return [#call, nil] invocation-specific Tool approval policy. The callable
|
|
40
|
-
# receives Phronomy::Agent::ApprovalEvaluationRequest.
|
|
41
|
-
attr_reader :approval_policy
|
|
42
|
-
|
|
43
|
-
# @return [Object, nil] redaction policy applied to tool args / results
|
|
44
|
-
attr_reader :redaction_policy
|
|
45
|
-
|
|
46
|
-
# @return [String, nil] unique identifier for this task in the trace tree
|
|
47
|
-
attr_reader :task_id
|
|
48
|
-
|
|
49
|
-
# @return [String, nil] task_id of the parent span / task
|
|
50
|
-
attr_reader :parent_task_id
|
|
19
|
+
attr_reader :thread_id, :session_id, :user_id, :cancellation_token,
|
|
20
|
+
:deadline, :tracer_span, :token_budget, :approval_policy,
|
|
21
|
+
:redaction_policy, :task_id, :parent_task_id
|
|
51
22
|
|
|
52
23
|
# @param thread_id [String, nil]
|
|
53
24
|
# @param session_id [String, nil]
|
|
@@ -56,11 +27,11 @@ module Phronomy
|
|
|
56
27
|
# @param deadline [Deadline, nil]
|
|
57
28
|
# @param tracer_span [Object, nil]
|
|
58
29
|
# @param token_budget [Integer, nil]
|
|
59
|
-
# @param approval_policy [#call, nil]
|
|
30
|
+
# @param approval_policy [#call, nil]
|
|
60
31
|
# @param redaction_policy [Object, nil]
|
|
61
32
|
# @param task_id [String, nil]
|
|
62
33
|
# @param parent_task_id [String, nil]
|
|
63
|
-
# @api
|
|
34
|
+
# @api public
|
|
64
35
|
def initialize(
|
|
65
36
|
thread_id: nil,
|
|
66
37
|
session_id: nil,
|
|
@@ -88,10 +59,6 @@ module Phronomy
|
|
|
88
59
|
end
|
|
89
60
|
|
|
90
61
|
# Returns a new +InvocationContext+ with the given attributes merged in.
|
|
91
|
-
# All other attributes are carried over unchanged.
|
|
92
|
-
#
|
|
93
|
-
# @param overrides [Hash] keyword arguments to override
|
|
94
|
-
# @return [InvocationContext]
|
|
95
62
|
# @api private
|
|
96
63
|
def merge(**overrides)
|
|
97
64
|
InvocationContext.new(
|
|
@@ -110,21 +77,13 @@ module Phronomy
|
|
|
110
77
|
end
|
|
111
78
|
|
|
112
79
|
# Convenience: returns the cancellation token or a new never-cancelled token.
|
|
113
|
-
# @return [CancellationToken]
|
|
114
80
|
# @api private
|
|
115
81
|
def effective_cancellation_token
|
|
116
82
|
@cancellation_token || Phronomy::Concurrency::CancellationToken.new
|
|
117
83
|
end
|
|
118
84
|
|
|
119
85
|
# Returns the cancellation token to use for an invocation, taking both the
|
|
120
|
-
# explicit
|
|
121
|
-
#
|
|
122
|
-
# - When +cancellation_token+ is set, it is returned unchanged.
|
|
123
|
-
# - When only +deadline+ is set, a new {CancellationToken} is created and
|
|
124
|
-
# the deadline is attached to it via {Deadline#attach_to}.
|
|
125
|
-
# - When neither is set, returns +nil+.
|
|
126
|
-
#
|
|
127
|
-
# @return [CancellationToken, nil]
|
|
86
|
+
# explicit cancellation_token and deadline into account.
|
|
128
87
|
# @api private
|
|
129
88
|
def effective_timeout_token
|
|
130
89
|
return @cancellation_token if @cancellation_token
|
|
@@ -2,56 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module LLMAdapter
|
|
5
|
-
#
|
|
5
|
+
# Beta extension SPI for LLM call adapters.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# External adapters implement {#complete} and {#stream}. The adapter or the
|
|
8
8
|
# underlying provider client owns transport timeout, retry, backoff, and
|
|
9
|
-
# rate-limit behavior. Phronomy
|
|
10
|
-
#
|
|
9
|
+
# rate-limit behavior. Phronomy owns cooperative cancellation and isolates
|
|
10
|
+
# synchronous provider calls in {OffloadPool}.
|
|
11
11
|
#
|
|
12
|
-
# The
|
|
13
|
-
#
|
|
12
|
+
# The Agent pipeline calls the framework-owned {#complete_async} and
|
|
13
|
+
# {#stream_async} wrappers. Adapter implementers do not need to depend on
|
|
14
|
+
# EventLoop, FSMSession, OffloadPool, AgentInvocation, or ExecutionCoordinator.
|
|
15
|
+
#
|
|
16
|
+
# The current input to this SPI is the configured/materialized chat runtime
|
|
17
|
+
# object. Formalizing this SPI therefore does not imply a provider-neutral
|
|
18
|
+
# replacement for RubyLLMMaterializer.
|
|
19
|
+
#
|
|
20
|
+
# @api public
|
|
14
21
|
class Base
|
|
15
22
|
# Performs a blocking (non-streaming) LLM completion.
|
|
16
|
-
# Implementors must call +chat.ask(message)+ (or equivalent) and
|
|
17
|
-
# return the response object.
|
|
18
23
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
24
|
+
# Implementors call the configured chat/runtime client and return its
|
|
25
|
+
# response object. Transport/retry policy remains adapter-owned.
|
|
26
|
+
#
|
|
27
|
+
# @param chat [Object] the configured/materialized chat runtime object
|
|
28
|
+
# @param message [String, nil] user message, or nil to continue without adding a new user turn
|
|
21
29
|
# @param config [Hash] invocation config (e.g. +:cancellation_token+)
|
|
22
30
|
# @return [Object] LLM response object
|
|
23
31
|
# @raise [NotImplementedError]
|
|
24
|
-
# @api
|
|
32
|
+
# @api public
|
|
25
33
|
def complete(chat, message, config: {})
|
|
26
34
|
raise NotImplementedError, "#{self.class}#complete is not implemented"
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
# Performs a blocking streaming LLM completion.
|
|
30
|
-
# Implementors must call +chat.ask(message) { |chunk| block.call(chunk) }+
|
|
31
|
-
# (or equivalent) and return the response object.
|
|
32
38
|
#
|
|
33
|
-
# @param chat [Object] the configured chat
|
|
34
|
-
# @param message [String]
|
|
39
|
+
# @param chat [Object] the configured/materialized chat runtime object
|
|
40
|
+
# @param message [String, nil] user message, or nil to continue without adding a new user turn
|
|
35
41
|
# @param config [Hash] invocation config
|
|
36
42
|
# @yield [chunk] streaming chunk from the LLM
|
|
37
43
|
# @return [Object] LLM response object
|
|
38
44
|
# @raise [NotImplementedError]
|
|
39
|
-
# @api
|
|
45
|
+
# @api public
|
|
40
46
|
def stream(chat, message, config: {}, &block)
|
|
41
47
|
raise NotImplementedError, "#{self.class}#stream is not implemented"
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
# Submits a non-streaming LLM call to {OffloadPool}
|
|
45
|
-
# an {OffloadPool::PendingOperation}.
|
|
50
|
+
# Submits a non-streaming LLM call to {OffloadPool}.
|
|
46
51
|
#
|
|
47
52
|
# Transport timeout and retry remain the responsibility of the adapter or
|
|
48
53
|
# provider client; Phronomy does not attach an additional operation timeout.
|
|
49
54
|
#
|
|
50
|
-
# @
|
|
51
|
-
# @param message [String] user message
|
|
52
|
-
# @param config [Hash] invocation config
|
|
53
|
-
# @param pool [OffloadPool] pool to submit to
|
|
54
|
-
# @return [OffloadPool::PendingOperation]
|
|
55
|
+
# @return [Phronomy::Task] caller-facing completion handle
|
|
55
56
|
# @api private
|
|
56
57
|
def complete_async(chat, message, config: {}, pool: default_pool)
|
|
57
58
|
token = config[:cancellation_token]
|
|
@@ -60,18 +61,14 @@ module Phronomy
|
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
# Submits a streaming LLM call to {OffloadPool}
|
|
64
|
-
# an {OffloadPool::PendingOperation}.
|
|
65
|
-
#
|
|
66
|
-
# The block is invoked on an OffloadPool worker thread. Agent code must
|
|
67
|
-
# pass only a lightweight internal sink that posts a value to EventLoop;
|
|
68
|
-
# Application callbacks must never be passed directly to this method.
|
|
64
|
+
# Submits a streaming LLM call to {OffloadPool}.
|
|
69
65
|
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
66
|
+
# The block is invoked on an OffloadPool worker thread. Agent code must pass
|
|
67
|
+
# only a lightweight internal sink that posts a value to EventLoop;
|
|
68
|
+
# application callbacks must never be passed directly to this method.
|
|
72
69
|
#
|
|
73
70
|
# @yield [chunk] streaming chunk on the worker thread
|
|
74
|
-
# @return [
|
|
71
|
+
# @return [Phronomy::Task] caller-facing completion handle
|
|
75
72
|
# @api private
|
|
76
73
|
def stream_async(chat, message, config: {}, pool: default_pool, &block)
|
|
77
74
|
raise ArgumentError, "stream_async requires a block" unless block
|
|
@@ -2,42 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module LLMAdapter
|
|
5
|
-
#
|
|
5
|
+
# Default LLMAdapter SPI implementation backed by RubyLLM.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# The synchronous +chat.ask+ / +chat.complete+ calls are invoked through the
|
|
8
|
+
# framework-owned async bridge in {LLMAdapter::Base}, so adapter consumers do
|
|
9
|
+
# not need to manage OffloadPool themselves.
|
|
10
10
|
#
|
|
11
11
|
# @example Explicitly configuring this adapter
|
|
12
12
|
# Phronomy.configure do |c|
|
|
13
13
|
# c.llm_adapter = Phronomy::LLMAdapter::RubyLLM.new
|
|
14
14
|
# end
|
|
15
|
+
#
|
|
16
|
+
# @api public
|
|
15
17
|
class RubyLLM < Base
|
|
16
18
|
# Delegates to +chat.ask(message)+ or +chat.complete+ when message is nil.
|
|
17
19
|
#
|
|
18
20
|
# Passing +nil+ for +message+ is used by the ReAct loop for continuation
|
|
19
21
|
# turns where the user message has already been added to the chat history
|
|
20
|
-
# (
|
|
21
|
-
# user turn.
|
|
22
|
+
# (for example after a Tool result).
|
|
22
23
|
#
|
|
23
24
|
# @param chat [Object] RubyLLM chat session
|
|
24
25
|
# @param message [String, nil] user message, or nil to continue the chat
|
|
25
|
-
# @param config [Hash] invocation config (not used directly
|
|
26
|
+
# @param config [Hash] invocation config (not used directly here)
|
|
26
27
|
# @return [Object] RubyLLM response
|
|
27
|
-
# @api
|
|
28
|
+
# @api public
|
|
28
29
|
def complete(chat, message, config: {})
|
|
29
30
|
message ? chat.ask(message) : chat.complete
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
# Delegates to +chat.ask(message) { |chunk|
|
|
33
|
-
#
|
|
33
|
+
# Delegates to +chat.ask(message) { |chunk| ... }+ or +chat.complete(&block)+
|
|
34
|
+
# when message is nil.
|
|
34
35
|
#
|
|
35
36
|
# @param chat [Object] RubyLLM chat session
|
|
36
37
|
# @param message [String, nil] user message, or nil to continue the chat
|
|
37
38
|
# @param config [Hash] invocation config
|
|
38
|
-
# @yield [chunk] streaming chunk forwarded from
|
|
39
|
+
# @yield [chunk] streaming chunk forwarded from RubyLLM
|
|
39
40
|
# @return [Object] RubyLLM response
|
|
40
|
-
# @api
|
|
41
|
+
# @api public
|
|
41
42
|
def stream(chat, message, config: {}, &block)
|
|
42
43
|
message ? chat.ask(message, &block) : chat.complete(&block)
|
|
43
44
|
end
|
data/lib/phronomy/llm_adapter.rb
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
# Namespace for LLM
|
|
4
|
+
# Namespace for LLM call adapters.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# The public extension boundary is {LLMAdapter::Base#complete} and
|
|
7
|
+
# {LLMAdapter::Base#stream}. Those methods receive Phronomy's currently
|
|
8
|
+
# materialized chat/runtime object and perform the provider call. Phronomy owns
|
|
9
|
+
# the async/offload bridge around that synchronous contract.
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# This SPI does not by itself make the complete input-materialization pipeline
|
|
12
|
+
# provider-neutral: the current Agent pipeline still materializes the canonical
|
|
13
|
+
# LLM input through RubyLLM-specific runtime objects before invoking the adapter.
|
|
13
14
|
#
|
|
14
15
|
# @example Configuring a custom adapter
|
|
15
16
|
# Phronomy.configure do |c|
|
|
16
17
|
# c.llm_adapter = MyCustomAdapter.new
|
|
17
18
|
# end
|
|
19
|
+
#
|
|
20
|
+
# @api public
|
|
18
21
|
module LLMAdapter
|
|
19
22
|
end
|
|
20
23
|
end
|
|
@@ -14,7 +14,11 @@ module Phronomy
|
|
|
14
14
|
parse(input.is_a?(String) ? input : input.to_s)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# Extension point implemented by custom parsers.
|
|
18
|
+
#
|
|
19
|
+
# @param text [String]
|
|
20
|
+
# @return [Object]
|
|
21
|
+
# @api public
|
|
18
22
|
def parse(text)
|
|
19
23
|
raise NotImplementedError, "#{self.class}#parse is not implemented"
|
|
20
24
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Tool
|
|
5
|
+
# Public authoring façade for Phronomy Tools.
|
|
6
|
+
#
|
|
7
|
+
# This constant intentionally refers to the exact same Class object as
|
|
8
|
+
# {Phronomy::Agent::Context::Capability::Base}. The implementation namespace
|
|
9
|
+
# remains canonical internally so the existing Tool DSL state, inheritance,
|
|
10
|
+
# built-in Tools, and compatibility surface are not duplicated.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
Base = Phronomy::Agent::Context::Capability::Base
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -2,43 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module VectorStore
|
|
5
|
-
#
|
|
5
|
+
# Framework-owned async convenience methods for VectorStore backends.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# The backend extension contract is the synchronous interface defined by
|
|
8
|
+
# {VectorStore::Base}: add/search/remove/clear/size. These async convenience
|
|
9
|
+
# methods are inherited by backends and route that synchronous work through
|
|
10
|
+
# Phronomy's bounded {OffloadPool}. This keeps OS-thread creation, queue
|
|
11
|
+
# backpressure, submit timeout/cancellation, and completion semantics owned by
|
|
12
|
+
# the framework rather than by each backend.
|
|
8
13
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
14
|
+
# A future genuine native-async backend may adapt its completion into a
|
|
15
|
+
# {Phronomy::Task} without an OffloadPool worker, but native async override is
|
|
16
|
+
# not part of the current backend SPI.
|
|
11
17
|
#
|
|
12
|
-
#
|
|
13
|
-
# backend has a native async driver, while the remaining methods fall back
|
|
14
|
-
# to the pool.
|
|
15
|
-
#
|
|
16
|
-
# 3. **Implement all natively** — override all async methods to avoid pool
|
|
17
|
-
# allocation entirely.
|
|
18
|
-
#
|
|
19
|
-
# @example Native async search (no pool worker thread allocated)
|
|
20
|
-
# class MyFastStore < Phronomy::VectorStore::Base
|
|
21
|
-
# include Phronomy::VectorStore::AsyncBackend
|
|
22
|
-
#
|
|
23
|
-
# def search_async(query_embedding:, k: 5, cancellation_token: nil, timeout: nil)
|
|
24
|
-
# # Returns a PendingOperation backed by a native async driver.
|
|
25
|
-
# native_async_search(query_embedding, k)
|
|
26
|
-
# end
|
|
27
|
-
# end
|
|
28
|
-
#
|
|
29
|
-
# @api public
|
|
18
|
+
# @api private
|
|
30
19
|
module AsyncBackend
|
|
31
20
|
# Async variant of {VectorStore::Base#add}.
|
|
32
21
|
#
|
|
33
|
-
# Submits the add call to {OffloadPool} by default.
|
|
34
|
-
# Override to use a native async driver.
|
|
35
|
-
#
|
|
36
22
|
# @param id [String]
|
|
37
23
|
# @param embedding [Array<Float>]
|
|
38
24
|
# @param metadata [Hash]
|
|
39
25
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
40
26
|
# @param timeout [Numeric, nil]
|
|
41
|
-
# @return [
|
|
27
|
+
# @return [Phronomy::Task]
|
|
42
28
|
# @api public
|
|
43
29
|
def add_async(id:, embedding:, metadata: {}, cancellation_token: nil, timeout: nil)
|
|
44
30
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -52,14 +38,11 @@ module Phronomy
|
|
|
52
38
|
|
|
53
39
|
# Async variant of {VectorStore::Base#search}.
|
|
54
40
|
#
|
|
55
|
-
# Submits the search call to {OffloadPool} by default.
|
|
56
|
-
# Override to use a native async driver.
|
|
57
|
-
#
|
|
58
41
|
# @param query_embedding [Array<Float>]
|
|
59
42
|
# @param k [Integer]
|
|
60
43
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
61
44
|
# @param timeout [Numeric, nil]
|
|
62
|
-
# @return [
|
|
45
|
+
# @return [Phronomy::Task]
|
|
63
46
|
# @api public
|
|
64
47
|
def search_async(query_embedding:, k: 5, cancellation_token: nil, timeout: nil)
|
|
65
48
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -73,13 +56,10 @@ module Phronomy
|
|
|
73
56
|
|
|
74
57
|
# Async variant of {VectorStore::Base#remove}.
|
|
75
58
|
#
|
|
76
|
-
# Submits the remove call to {OffloadPool} by default.
|
|
77
|
-
# Override to use a native async driver.
|
|
78
|
-
#
|
|
79
59
|
# @param id [String]
|
|
80
60
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
81
61
|
# @param timeout [Numeric, nil]
|
|
82
|
-
# @return [
|
|
62
|
+
# @return [Phronomy::Task]
|
|
83
63
|
# @api public
|
|
84
64
|
def remove_async(id:, cancellation_token: nil, timeout: nil)
|
|
85
65
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -93,12 +73,9 @@ module Phronomy
|
|
|
93
73
|
|
|
94
74
|
# Async variant of {VectorStore::Base#clear}.
|
|
95
75
|
#
|
|
96
|
-
# Submits the clear call to {OffloadPool} by default.
|
|
97
|
-
# Override to use a native async driver.
|
|
98
|
-
#
|
|
99
76
|
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
100
77
|
# @param timeout [Numeric, nil]
|
|
101
|
-
# @return [
|
|
78
|
+
# @return [Phronomy::Task]
|
|
102
79
|
# @api public
|
|
103
80
|
def clear_async(cancellation_token: nil, timeout: nil)
|
|
104
81
|
Phronomy::Runtime.instance.offload.submit(
|
|
@@ -2,24 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module VectorStore
|
|
5
|
-
#
|
|
5
|
+
# Public extension SPI for vector stores.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Backends implement the synchronous add/search/remove/clear/size contract.
|
|
8
|
+
# Phronomy supplies async convenience methods through {AsyncBackend}; blocking
|
|
9
|
+
# backend work is offloaded through the framework-owned bounded OffloadPool.
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
-
# are provided by the {AsyncBackend} mixin which defaults to routing calls
|
|
12
|
-
# through {OffloadPool}. Backends with native async drivers may override
|
|
13
|
-
# individual async methods without touching the pool at all.
|
|
11
|
+
# @api public
|
|
14
12
|
class Base
|
|
15
13
|
include AsyncBackend
|
|
16
14
|
|
|
17
15
|
# Add a document with its vector embedding.
|
|
18
16
|
#
|
|
19
|
-
# @param id [String]
|
|
20
|
-
# @param embedding [Array<Float>]
|
|
21
|
-
# @param metadata [Hash]
|
|
22
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
17
|
+
# @param id [String] unique document identifier
|
|
18
|
+
# @param embedding [Array<Float>] vector embedding
|
|
19
|
+
# @param metadata [Hash] arbitrary metadata
|
|
20
|
+
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
23
21
|
# @api public
|
|
24
22
|
def add(id:, embedding:, metadata: {}, cancellation_token: nil)
|
|
25
23
|
cancellation_token&.raise_if_cancelled!
|
|
@@ -29,8 +27,8 @@ module Phronomy
|
|
|
29
27
|
# Return the k most similar documents to the query embedding.
|
|
30
28
|
#
|
|
31
29
|
# @param query_embedding [Array<Float>]
|
|
32
|
-
# @param k [Integer]
|
|
33
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
30
|
+
# @param k [Integer] number of results
|
|
31
|
+
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
34
32
|
# @return [Array<Hash>] each element: { id:, score:, metadata: }
|
|
35
33
|
# @api public
|
|
36
34
|
def search(query_embedding:, k: 5, cancellation_token: nil)
|
|
@@ -47,6 +45,7 @@ module Phronomy
|
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
# Remove all documents.
|
|
48
|
+
# @api public
|
|
50
49
|
def clear
|
|
51
50
|
raise NotImplementedError, "#{self.class}#clear is not implemented"
|
|
52
51
|
end
|