phronomy 0.7.1 → 0.8.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/README.md +16 -16
- data/benchmark/bench_context_assembler.rb +2 -2
- data/benchmark/bench_regression.rb +5 -5
- data/benchmark/bench_token_estimator.rb +5 -5
- data/benchmark/bench_tool_schema.rb +1 -1
- data/benchmark/bench_vector_store.rb +1 -1
- data/lib/phronomy/agent/base.rb +86 -123
- data/lib/phronomy/agent/checkpoint.rb +118 -0
- data/lib/phronomy/agent/context/conversation/compaction_context.rb +117 -0
- data/lib/phronomy/agent/context/conversation/trigger_context.rb +43 -0
- data/lib/phronomy/agent/context/conversation/trim_context.rb +82 -0
- data/lib/phronomy/agent/context/instruction/prompt_template.rb +102 -0
- data/lib/phronomy/agent/context/knowledge/embeddings/base.rb +45 -0
- data/lib/phronomy/agent/context/knowledge/embeddings/ruby_llm_embeddings.rb +51 -0
- data/lib/phronomy/agent/context/knowledge/loader/base.rb +31 -0
- data/lib/phronomy/agent/context/knowledge/loader/csv_loader.rb +62 -0
- data/lib/phronomy/agent/context/knowledge/loader/markdown_loader.rb +82 -0
- data/lib/phronomy/agent/context/knowledge/loader/plain_text_loader.rb +28 -0
- data/lib/phronomy/agent/context/knowledge/source/base.rb +60 -0
- data/lib/phronomy/agent/context/knowledge/source/entity_knowledge.rb +102 -0
- data/lib/phronomy/agent/context/knowledge/source/rag_knowledge.rb +63 -0
- data/lib/phronomy/agent/context/knowledge/source/static_knowledge.rb +58 -0
- data/lib/phronomy/agent/context/knowledge/splitter/base.rb +53 -0
- data/lib/phronomy/agent/context/knowledge/splitter/fixed_size_splitter.rb +57 -0
- data/lib/phronomy/agent/context/knowledge/splitter/recursive_splitter.rb +111 -0
- data/lib/phronomy/agent/context/knowledge/vector_store/async_backend.rb +116 -0
- data/lib/phronomy/agent/context/knowledge/vector_store/base.rb +95 -0
- data/lib/phronomy/agent/context/knowledge/vector_store/in_memory.rb +109 -0
- data/lib/phronomy/agent/context/knowledge/vector_store/pgvector.rb +133 -0
- data/lib/phronomy/agent/context/knowledge/vector_store/redis_search.rb +198 -0
- data/lib/phronomy/agent/fsm.rb +1 -1
- data/lib/phronomy/agent/invocation_pipeline.rb +99 -0
- data/lib/phronomy/agent/lifecycle/fsm_session.rb +251 -0
- data/lib/phronomy/agent/lifecycle/phase_machine_builder.rb +249 -0
- data/lib/phronomy/agent/react_agent.rb +19 -14
- data/lib/phronomy/agent/runner.rb +2 -2
- data/lib/phronomy/agent/tool_executor.rb +108 -0
- data/lib/phronomy/concurrency/async_queue.rb +157 -0
- data/lib/phronomy/concurrency/blocking_adapter_pool.rb +443 -0
- data/lib/phronomy/concurrency/cancellation_scope.rb +125 -0
- data/lib/phronomy/concurrency/cancellation_token.rb +140 -0
- data/lib/phronomy/concurrency/concurrency_gate.rb +157 -0
- data/lib/phronomy/concurrency/deadline.rb +65 -0
- data/lib/phronomy/{runtime → concurrency}/gate_registry.rb +1 -1
- data/lib/phronomy/{runtime → concurrency}/pool_registry.rb +1 -1
- data/lib/phronomy/context.rb +2 -8
- data/lib/phronomy/embeddings.rb +2 -2
- data/lib/phronomy/eval/runner.rb +4 -0
- data/lib/phronomy/eval/scorer/llm_judge.rb +12 -1
- data/lib/phronomy/event_loop.rb +7 -7
- data/lib/phronomy/invocation_context.rb +3 -3
- data/lib/phronomy/knowledge_source.rb +0 -5
- data/lib/phronomy/llm_adapter/ruby_llm.rb +17 -11
- data/lib/phronomy/{context → llm_context_window}/assembler.rb +18 -3
- data/lib/phronomy/{context → llm_context_window}/context_version_cache.rb +1 -1
- data/lib/phronomy/{context → llm_context_window}/token_budget.rb +7 -4
- data/lib/phronomy/{context → llm_context_window}/token_estimator.rb +3 -3
- data/lib/phronomy/loader.rb +4 -4
- data/lib/phronomy/{agent → multi_agent}/handoff.rb +2 -2
- data/lib/phronomy/{agent → multi_agent}/orchestrator.rb +6 -6
- data/lib/phronomy/{agent → multi_agent}/parallel_tool_chat.rb +4 -4
- data/lib/phronomy/{agent → multi_agent}/team_coordinator.rb +2 -2
- data/lib/phronomy/runtime.rb +19 -4
- data/lib/phronomy/splitter.rb +3 -3
- data/lib/phronomy/task_group.rb +1 -1
- data/lib/phronomy/tool/base.rb +50 -9
- data/lib/phronomy/tracing/null_tracer.rb +3 -1
- data/lib/phronomy/vector_store.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow_context.rb +8 -0
- data/lib/phronomy/workflow_runner.rb +11 -131
- data/lib/phronomy.rb +1 -0
- metadata +44 -42
- data/lib/phronomy/async_queue.rb +0 -155
- data/lib/phronomy/blocking_adapter_pool.rb +0 -435
- data/lib/phronomy/cancellation_scope.rb +0 -123
- data/lib/phronomy/cancellation_token.rb +0 -133
- data/lib/phronomy/concurrency_gate.rb +0 -155
- data/lib/phronomy/context/compaction_context.rb +0 -111
- data/lib/phronomy/context/trigger_context.rb +0 -39
- data/lib/phronomy/context/trim_context.rb +0 -75
- data/lib/phronomy/deadline.rb +0 -63
- data/lib/phronomy/embeddings/base.rb +0 -39
- data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +0 -45
- data/lib/phronomy/fsm_session.rb +0 -247
- data/lib/phronomy/knowledge_source/base.rb +0 -54
- data/lib/phronomy/knowledge_source/entity_knowledge.rb +0 -96
- data/lib/phronomy/knowledge_source/rag_knowledge.rb +0 -57
- data/lib/phronomy/knowledge_source/static_knowledge.rb +0 -52
- data/lib/phronomy/loader/base.rb +0 -25
- data/lib/phronomy/loader/csv_loader.rb +0 -56
- data/lib/phronomy/loader/markdown_loader.rb +0 -76
- data/lib/phronomy/loader/plain_text_loader.rb +0 -22
- data/lib/phronomy/prompt_template.rb +0 -96
- data/lib/phronomy/splitter/base.rb +0 -47
- data/lib/phronomy/splitter/fixed_size_splitter.rb +0 -51
- data/lib/phronomy/splitter/recursive_splitter.rb +0 -105
- data/lib/phronomy/tool_executor.rb +0 -106
- data/lib/phronomy/vector_store/async_backend.rb +0 -110
- data/lib/phronomy/vector_store/base.rb +0 -89
- data/lib/phronomy/vector_store/in_memory.rb +0 -93
- data/lib/phronomy/vector_store/pgvector.rb +0 -127
- data/lib/phronomy/vector_store/redis_search.rb +0 -192
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
# A counting semaphore that enforces a concurrency cap across a named
|
|
5
|
-
# resource category (e.g. agent tasks, tool tasks, LLM calls).
|
|
6
|
-
#
|
|
7
|
-
# When +max_concurrent+ is +nil+ the gate is a no-op and all callers
|
|
8
|
-
# pass through immediately without acquiring a slot.
|
|
9
|
-
#
|
|
10
|
-
# Backpressure behaviour when the gate is full is controlled by the
|
|
11
|
-
# +on_full:+ keyword:
|
|
12
|
-
# +:reject+ — raise {Phronomy::BackpressureError} immediately
|
|
13
|
-
# +:wait+ — block the calling fiber/thread until a slot is free
|
|
14
|
-
# +:timeout+ — like +:wait+ but raises {Phronomy::BackpressureError}
|
|
15
|
-
# after +timeout:+ seconds if no slot becomes available
|
|
16
|
-
#
|
|
17
|
-
# @example
|
|
18
|
-
# gate = Phronomy::ConcurrencyGate.new(max_concurrent: 5, name: :agent)
|
|
19
|
-
# gate.acquire(on_full: :reject) do
|
|
20
|
-
# run_agent_task
|
|
21
|
-
# end
|
|
22
|
-
class ConcurrencyGate
|
|
23
|
-
# @param max_concurrent [Integer, nil] concurrency cap; nil = unlimited
|
|
24
|
-
# @param name [Symbol, String, nil] human-readable label used in error messages
|
|
25
|
-
# @api private
|
|
26
|
-
def initialize(max_concurrent:, name: nil)
|
|
27
|
-
@max = max_concurrent
|
|
28
|
-
@name = name
|
|
29
|
-
@mutex = Mutex.new
|
|
30
|
-
@cond = ConditionVariable.new
|
|
31
|
-
@count = 0
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Returns the configured cap (or nil when unlimited).
|
|
35
|
-
attr_reader :max
|
|
36
|
-
|
|
37
|
-
# Returns the name label.
|
|
38
|
-
attr_reader :name
|
|
39
|
-
|
|
40
|
-
# Returns the number of slots currently in use.
|
|
41
|
-
def current_count
|
|
42
|
-
@mutex.synchronize { @count }
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Acquires a slot, executes +block+, then releases the slot.
|
|
46
|
-
# When the gate is unlimited (max is nil) the block runs directly.
|
|
47
|
-
#
|
|
48
|
-
# @param on_full [:reject, :wait, :timeout] backpressure strategy
|
|
49
|
-
# @param timeout [Numeric, nil] seconds before +:timeout+ gives up
|
|
50
|
-
# @yield
|
|
51
|
-
# @return block return value
|
|
52
|
-
# @raise [Phronomy::BackpressureError] when +:reject+ or +:timeout+ fires
|
|
53
|
-
# @api private
|
|
54
|
-
def acquire(on_full: :wait, timeout: nil, &block)
|
|
55
|
-
return block.call if @max.nil?
|
|
56
|
-
|
|
57
|
-
_acquire_slot(on_full: on_full, timeout: timeout)
|
|
58
|
-
begin
|
|
59
|
-
block.call
|
|
60
|
-
ensure
|
|
61
|
-
_release_slot
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
def _acquire_slot(on_full:, timeout:)
|
|
68
|
-
scheduler = Phronomy::Runtime::Scheduler.current
|
|
69
|
-
if scheduler
|
|
70
|
-
_acquire_slot_coop(scheduler, on_full: on_full, timeout: timeout)
|
|
71
|
-
else
|
|
72
|
-
_acquire_slot_threaded(on_full: on_full, timeout: timeout)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def _acquire_slot_coop(scheduler, on_full:, timeout:)
|
|
77
|
-
# In cooperative mode all tasks run on the same thread, so no mutex needed.
|
|
78
|
-
deadline = timeout ? (scheduler.virtual_time + timeout) : nil
|
|
79
|
-
@coop_signal ||= scheduler.new_signal
|
|
80
|
-
|
|
81
|
-
loop do
|
|
82
|
-
if @count < @max
|
|
83
|
-
@count += 1
|
|
84
|
-
return
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
case on_full
|
|
88
|
-
when :reject
|
|
89
|
-
raise Phronomy::BackpressureError,
|
|
90
|
-
"ConcurrencyGate[#{@name}] at capacity (#{@max}); " \
|
|
91
|
-
"increase max_concurrent_#{@name}_tasks or retry later"
|
|
92
|
-
when :timeout
|
|
93
|
-
if deadline && scheduler.virtual_time >= deadline
|
|
94
|
-
raise Phronomy::BackpressureError,
|
|
95
|
-
"ConcurrencyGate[#{@name}] timed out waiting for a free slot (cap: #{@max})"
|
|
96
|
-
end
|
|
97
|
-
scheduler.wait_for_signal(@coop_signal)
|
|
98
|
-
if deadline && scheduler.virtual_time >= deadline
|
|
99
|
-
raise Phronomy::BackpressureError,
|
|
100
|
-
"ConcurrencyGate[#{@name}] timed out waiting for a free slot (cap: #{@max})"
|
|
101
|
-
end
|
|
102
|
-
else # :wait
|
|
103
|
-
scheduler.wait_for_signal(@coop_signal)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def _acquire_slot_threaded(on_full:, timeout:)
|
|
109
|
-
deadline = timeout ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout) : nil
|
|
110
|
-
|
|
111
|
-
@mutex.synchronize do
|
|
112
|
-
loop do
|
|
113
|
-
if @count < @max
|
|
114
|
-
@count += 1
|
|
115
|
-
return
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
case on_full
|
|
119
|
-
when :reject
|
|
120
|
-
raise Phronomy::BackpressureError,
|
|
121
|
-
"ConcurrencyGate[#{@name}] at capacity (#{@max}); " \
|
|
122
|
-
"increase max_concurrent_#{@name}_tasks or retry later"
|
|
123
|
-
when :timeout
|
|
124
|
-
remaining = deadline ? (deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)) : nil
|
|
125
|
-
if remaining && remaining <= 0
|
|
126
|
-
raise Phronomy::BackpressureError,
|
|
127
|
-
"ConcurrencyGate[#{@name}] timed out waiting for a free slot (cap: #{@max})"
|
|
128
|
-
end
|
|
129
|
-
@cond.wait(@mutex, remaining || nil)
|
|
130
|
-
# re-check deadline after wakeup
|
|
131
|
-
if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
132
|
-
raise Phronomy::BackpressureError,
|
|
133
|
-
"ConcurrencyGate[#{@name}] timed out waiting for a free slot (cap: #{@max})"
|
|
134
|
-
end
|
|
135
|
-
else # :wait
|
|
136
|
-
@cond.wait(@mutex)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def _release_slot
|
|
143
|
-
scheduler = Phronomy::Runtime::Scheduler.current
|
|
144
|
-
if scheduler && @coop_signal
|
|
145
|
-
@count -= 1
|
|
146
|
-
scheduler.raise_signal(@coop_signal)
|
|
147
|
-
else
|
|
148
|
-
@mutex.synchronize do
|
|
149
|
-
@count -= 1
|
|
150
|
-
@cond.signal
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Context
|
|
5
|
-
# Context object passed to the +on_compact+ callback registered on an agent.
|
|
6
|
-
#
|
|
7
|
-
# The callback calls #compact one or more times to specify which ranges of
|
|
8
|
-
# messages to replace with a summary. Each call:
|
|
9
|
-
# 1. Yields the selected message elements to the block.
|
|
10
|
-
# 2. Receives the block's return value as the summary text.
|
|
11
|
-
# 3. Persists a compaction record to the memory store (if available).
|
|
12
|
-
# 4. Updates #result_messages so that the compacted range is replaced
|
|
13
|
-
# by a single +:system+ summary message.
|
|
14
|
-
#
|
|
15
|
-
# The agent reads #result_messages after the callback returns and uses it
|
|
16
|
-
# as the new message list for this invocation.
|
|
17
|
-
#
|
|
18
|
-
# @example Summarise the oldest half of the conversation
|
|
19
|
-
# on_compact do |ctx|
|
|
20
|
-
# half = ctx.message_elements.length / 2
|
|
21
|
-
# ctx.compact(0...half) do |elements|
|
|
22
|
-
# texts = elements.map { |e| "#{e[:role]}: #{e[:message].content}" }.join("\n")
|
|
23
|
-
# "Summary of earlier conversation:\n#{texts}"
|
|
24
|
-
# end
|
|
25
|
-
# end
|
|
26
|
-
class CompactionContext
|
|
27
|
-
# @return [Array<Hash>] message elements at compaction time
|
|
28
|
-
attr_reader :message_elements
|
|
29
|
-
|
|
30
|
-
# @return [Phronomy::Context::TokenBudget, nil]
|
|
31
|
-
attr_reader :budget
|
|
32
|
-
|
|
33
|
-
# @return [Integer] total estimated token count before compaction
|
|
34
|
-
attr_reader :total_tokens
|
|
35
|
-
|
|
36
|
-
# The current message list to be used after all compact calls have been made.
|
|
37
|
-
# Updated by each call to #compact.
|
|
38
|
-
#
|
|
39
|
-
# @return [Array]
|
|
40
|
-
attr_reader :result_messages
|
|
41
|
-
|
|
42
|
-
# @param message_elements [Array<Hash>]
|
|
43
|
-
# each element: { seq: Integer, message: Object, tokens: Integer, role: Symbol }
|
|
44
|
-
# @param budget [Phronomy::Context::TokenBudget, nil]
|
|
45
|
-
# @param thread_id [String, nil] used when saving compaction records
|
|
46
|
-
# @param memory [Object, nil] memory object; must respond to #save_compaction
|
|
47
|
-
# for compaction records to be persisted
|
|
48
|
-
# @api private
|
|
49
|
-
def initialize(message_elements:, budget:, thread_id: nil, memory: nil)
|
|
50
|
-
@message_elements = message_elements.dup
|
|
51
|
-
@budget = budget
|
|
52
|
-
@total_tokens = message_elements.sum { |e| e[:tokens] }
|
|
53
|
-
@thread_id = thread_id
|
|
54
|
-
@memory = memory
|
|
55
|
-
@result_messages = @message_elements.map { |e| e[:message] }
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Replace a range of messages with a summary produced by the block.
|
|
59
|
-
#
|
|
60
|
-
# The block receives the selected Array<Hash> elements and must return a
|
|
61
|
-
# String that serves as the summary text. After the call, #result_messages
|
|
62
|
-
# reflects the replacement.
|
|
63
|
-
#
|
|
64
|
-
# If the memory object responds to #save_compaction, a compaction record
|
|
65
|
-
# { start_seq:, end_seq:, summary_text: } is persisted for auditability.
|
|
66
|
-
#
|
|
67
|
-
# @param range [Range, Integer] index range into message_elements (0-based)
|
|
68
|
-
# @yieldparam elements [Array<Hash>] the selected message elements
|
|
69
|
-
# @yieldreturn [String] summary text to replace the selected messages
|
|
70
|
-
# @return [Array] the updated result_messages array
|
|
71
|
-
# @api private
|
|
72
|
-
def compact(range)
|
|
73
|
-
# Normalise: Integer index → single-element Array; Range → Array slice.
|
|
74
|
-
raw = @message_elements[range]
|
|
75
|
-
elements = if raw.is_a?(Array)
|
|
76
|
-
raw
|
|
77
|
-
elsif raw.nil?
|
|
78
|
-
[]
|
|
79
|
-
else
|
|
80
|
-
[raw]
|
|
81
|
-
end
|
|
82
|
-
return @result_messages if elements.empty?
|
|
83
|
-
|
|
84
|
-
summary_text = yield(elements).to_s
|
|
85
|
-
|
|
86
|
-
start_seq = elements.first[:seq]
|
|
87
|
-
end_seq = elements.last[:seq]
|
|
88
|
-
|
|
89
|
-
if @memory && @thread_id && @memory.respond_to?(:save_compaction)
|
|
90
|
-
@memory.save_compaction(
|
|
91
|
-
thread_id: @thread_id,
|
|
92
|
-
start_seq: start_seq,
|
|
93
|
-
end_seq: end_seq,
|
|
94
|
-
summary_text: summary_text
|
|
95
|
-
)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Compute the last included index in the original @message_elements array.
|
|
99
|
-
last_idx = if range.is_a?(Range)
|
|
100
|
-
range.exclude_end? ? range.last - 1 : range.last
|
|
101
|
-
else
|
|
102
|
-
range.to_i
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
remaining = (@message_elements[(last_idx + 1)..] || []).map { |e| e[:message] }
|
|
106
|
-
summary_msg = RubyLLM::Message.new(role: :system, content: summary_text)
|
|
107
|
-
@result_messages = [summary_msg] + remaining
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Context
|
|
5
|
-
# Read-only context passed to the +on_compaction_trigger+ callback.
|
|
6
|
-
#
|
|
7
|
-
# The callback inspects the current message list and budget, then returns
|
|
8
|
-
# a truthy value to trigger compaction or a falsy value to skip it.
|
|
9
|
-
#
|
|
10
|
-
# No mutations are allowed through this object; use CompactionContext
|
|
11
|
-
# (passed to +on_compact+) for actual modifications.
|
|
12
|
-
#
|
|
13
|
-
# @example Trigger compaction when messages exceed 80% of the input budget
|
|
14
|
-
# on_compaction_trigger do |ctx|
|
|
15
|
-
# limit = ctx.budget&.available(used: 0) || Float::INFINITY
|
|
16
|
-
# ctx.total_tokens > limit * 0.8
|
|
17
|
-
# end
|
|
18
|
-
class TriggerContext
|
|
19
|
-
# @return [Array<Hash>] frozen snapshot of message elements
|
|
20
|
-
# each element: { seq: Integer, message: Object, tokens: Integer, role: Symbol }
|
|
21
|
-
attr_reader :message_elements
|
|
22
|
-
|
|
23
|
-
# @return [Phronomy::Context::TokenBudget, nil] token budget for this invocation
|
|
24
|
-
attr_reader :budget
|
|
25
|
-
|
|
26
|
-
# @return [Integer] total estimated token count of all message elements
|
|
27
|
-
attr_reader :total_tokens
|
|
28
|
-
|
|
29
|
-
# @param message_elements [Array<Hash>]
|
|
30
|
-
# @param budget [Phronomy::Context::TokenBudget, nil]
|
|
31
|
-
# @api private
|
|
32
|
-
def initialize(message_elements:, budget:)
|
|
33
|
-
@message_elements = message_elements.dup.freeze
|
|
34
|
-
@budget = budget
|
|
35
|
-
@total_tokens = message_elements.sum { |e| e[:tokens] }
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Context
|
|
5
|
-
# Context object passed to the +on_trim+ callback registered on an agent class.
|
|
6
|
-
#
|
|
7
|
-
# The callback receives a TrimContext and may call #remove to drop specific
|
|
8
|
-
# messages from the conversation before the LLM is called. Changes affect
|
|
9
|
-
# only the current invocation; the underlying memory store is not modified.
|
|
10
|
-
#
|
|
11
|
-
# Message elements are identified by a +:seq+ integer that is assigned
|
|
12
|
-
# sequentially (0-based) when messages are loaded from memory each turn.
|
|
13
|
-
#
|
|
14
|
-
# @example Remove the oldest two messages when the budget is tight
|
|
15
|
-
# on_trim do |ctx|
|
|
16
|
-
# if ctx.total_tokens > ctx.budget.available(used: 0) * 0.9
|
|
17
|
-
# seqs_to_drop = ctx.message_elements.first(2).map { |e| e[:seq] }
|
|
18
|
-
# ctx.remove(seqs_to_drop)
|
|
19
|
-
# end
|
|
20
|
-
# end
|
|
21
|
-
class TrimContext
|
|
22
|
-
# @return [Phronomy::Context::TokenBudget, nil] token budget for this invocation
|
|
23
|
-
attr_reader :budget
|
|
24
|
-
|
|
25
|
-
# @return [Integer] total estimated token count of all current message elements
|
|
26
|
-
attr_reader :total_tokens
|
|
27
|
-
|
|
28
|
-
# @param message_elements [Array<Hash>]
|
|
29
|
-
# each element: { seq: Integer, message: Object, tokens: Integer, role: Symbol }
|
|
30
|
-
# @param budget [Phronomy::Context::TokenBudget, nil]
|
|
31
|
-
# @api private
|
|
32
|
-
def initialize(message_elements:, budget:)
|
|
33
|
-
@message_elements = message_elements.dup
|
|
34
|
-
@budget = budget
|
|
35
|
-
recalculate!
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Returns a snapshot of the current message elements (defensive copy).
|
|
39
|
-
# Each element is a Hash with +:seq+, +:message+, +:tokens+, and +:role+.
|
|
40
|
-
#
|
|
41
|
-
# @return [Array<Hash>]
|
|
42
|
-
# @api private
|
|
43
|
-
def message_elements
|
|
44
|
-
@message_elements.dup
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Remove messages identified by seq numbers.
|
|
48
|
-
# Calling this multiple times accumulates removals.
|
|
49
|
-
#
|
|
50
|
-
# @param seqs [Integer, Array<Integer>] seq number(s) to remove
|
|
51
|
-
# @return [self]
|
|
52
|
-
# @api private
|
|
53
|
-
def remove(seqs)
|
|
54
|
-
seqs_set = Array(seqs).to_set
|
|
55
|
-
@message_elements.reject! { |e| seqs_set.include?(e[:seq]) }
|
|
56
|
-
recalculate!
|
|
57
|
-
self
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Convenience: returns the plain message objects (without element metadata).
|
|
61
|
-
#
|
|
62
|
-
# @return [Array]
|
|
63
|
-
# @api private
|
|
64
|
-
def messages
|
|
65
|
-
@message_elements.map { |e| e[:message] }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
def recalculate!
|
|
71
|
-
@total_tokens = @message_elements.sum { |e| e[:tokens] }
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
data/lib/phronomy/deadline.rb
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
# A point in time used as an upper bound for an operation.
|
|
5
|
-
#
|
|
6
|
-
# Uses the monotonic clock (+Process::CLOCK_MONOTONIC+) internally to avoid
|
|
7
|
-
# skew from NTP adjustments or DST transitions.
|
|
8
|
-
#
|
|
9
|
-
# @example Create a 30-second deadline and check remaining time
|
|
10
|
-
# deadline = Phronomy::Deadline.in(30)
|
|
11
|
-
# sleep 1
|
|
12
|
-
# deadline.remaining_seconds # => ~29.0
|
|
13
|
-
# deadline.expired? # => false
|
|
14
|
-
class Deadline
|
|
15
|
-
# Creates a deadline that expires +seconds+ from now.
|
|
16
|
-
#
|
|
17
|
-
# @param seconds [Numeric] seconds from now until expiry
|
|
18
|
-
# @return [Deadline]
|
|
19
|
-
# @api private
|
|
20
|
-
def self.in(seconds)
|
|
21
|
-
new(Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# @param monotonic_at [Float] absolute monotonic timestamp of expiry
|
|
25
|
-
# @api private
|
|
26
|
-
def initialize(monotonic_at)
|
|
27
|
-
@monotonic_at = monotonic_at
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Returns +true+ when the deadline has passed.
|
|
31
|
-
# @return [Boolean]
|
|
32
|
-
# @api private
|
|
33
|
-
def expired?
|
|
34
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @monotonic_at
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Seconds remaining until expiry. Returns 0 when already expired.
|
|
38
|
-
# @return [Float]
|
|
39
|
-
# @api private
|
|
40
|
-
def remaining_seconds
|
|
41
|
-
remaining = @monotonic_at - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
42
|
-
[remaining, 0.0].max
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Attaches this deadline to a {CancellationToken} by cancelling the token
|
|
46
|
-
# when the deadline expires. Uses the Runtime timer queue (a single
|
|
47
|
-
# background thread shared by all deadlines) instead of spawning one thread
|
|
48
|
-
# per deadline.
|
|
49
|
-
#
|
|
50
|
-
# @param token [CancellationToken]
|
|
51
|
-
# @param timer_queue [Runtime::TimerQueue, nil] queue to register with;
|
|
52
|
-
# defaults to +Phronomy::Runtime.instance.timer_queue+
|
|
53
|
-
# @return [self]
|
|
54
|
-
# @api private
|
|
55
|
-
def attach_to(token, timer_queue: Phronomy::Runtime.instance.timer_queue)
|
|
56
|
-
seconds = remaining_seconds
|
|
57
|
-
return self if seconds <= 0
|
|
58
|
-
|
|
59
|
-
timer_queue.schedule(seconds: seconds) { token.cancel! }
|
|
60
|
-
self
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Embeddings
|
|
5
|
-
# Abstract interface for embedding adapters.
|
|
6
|
-
#
|
|
7
|
-
# Concrete implementations must override {#embed} and return a vector
|
|
8
|
-
# as an +Array<Float>+.
|
|
9
|
-
class Base
|
|
10
|
-
# Embed the given text and return a vector representation.
|
|
11
|
-
#
|
|
12
|
-
# @param text [String] the text to embed
|
|
13
|
-
# @param cancellation_token [Phronomy::CancellationToken, nil] optional; raises CancellationError when cancelled
|
|
14
|
-
# @return [Array<Float>] the embedding vector
|
|
15
|
-
# @api public
|
|
16
|
-
def embed(text, cancellation_token = nil)
|
|
17
|
-
cancellation_token&.raise_if_cancelled!
|
|
18
|
-
raise NotImplementedError, "#{self.class}#embed is not implemented"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Submits an {#embed} call to {BlockingAdapterPool} and returns a
|
|
22
|
-
# {BlockingAdapterPool::PendingOperation}.
|
|
23
|
-
#
|
|
24
|
-
# @param text [String]
|
|
25
|
-
# @param cancellation_token [Phronomy::CancellationToken, nil]
|
|
26
|
-
# @param timeout [Numeric, nil] seconds before the operation is abandoned
|
|
27
|
-
# @return [BlockingAdapterPool::PendingOperation]
|
|
28
|
-
# @api public
|
|
29
|
-
def embed_async(text, cancellation_token = nil, timeout: nil)
|
|
30
|
-
Phronomy::Runtime.instance.blocking_io.submit(
|
|
31
|
-
timeout: timeout,
|
|
32
|
-
cancellation_token: cancellation_token
|
|
33
|
-
) do
|
|
34
|
-
embed(text, cancellation_token)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Embeddings
|
|
5
|
-
# Embeddings adapter backed by RubyLLM.
|
|
6
|
-
#
|
|
7
|
-
# Delegates to +RubyLLM.embed+ and returns the resulting vector as an
|
|
8
|
-
# +Array<Float>+.
|
|
9
|
-
#
|
|
10
|
-
# @example Default model
|
|
11
|
-
# embeddings = Phronomy::Embeddings::RubyLLMEmbeddings.new
|
|
12
|
-
# vector = embeddings.embed("Hello, world!")
|
|
13
|
-
#
|
|
14
|
-
# @example Explicit model
|
|
15
|
-
# embeddings = Phronomy::Embeddings::RubyLLMEmbeddings.new(model: "text-embedding-3-small")
|
|
16
|
-
# vector = embeddings.embed("Hello, world!")
|
|
17
|
-
class RubyLLMEmbeddings < Base
|
|
18
|
-
# @param model [String, nil] embedding model identifier; nil uses the RubyLLM default
|
|
19
|
-
# @param provider [Symbol, nil] provider override (e.g. :openai); nil uses the RubyLLM default
|
|
20
|
-
# @param assume_model_exists [Boolean] when true, skips RubyLLM model-registry validation
|
|
21
|
-
# (useful for locally hosted models not in the registry)
|
|
22
|
-
# @api public
|
|
23
|
-
def initialize(model: nil, provider: nil, assume_model_exists: false)
|
|
24
|
-
@model = model
|
|
25
|
-
@provider = provider
|
|
26
|
-
@assume_model_exists = assume_model_exists
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Embed text via RubyLLM.
|
|
30
|
-
#
|
|
31
|
-
# @param text [String]
|
|
32
|
-
# @param cancellation_token [Phronomy::CancellationToken, nil] optional; raises CancellationError when cancelled
|
|
33
|
-
# @return [Array<Float>]
|
|
34
|
-
# @api public
|
|
35
|
-
def embed(text, cancellation_token = nil)
|
|
36
|
-
cancellation_token&.raise_if_cancelled!
|
|
37
|
-
opts = {}
|
|
38
|
-
opts[:model] = @model if @model
|
|
39
|
-
opts[:provider] = @provider if @provider
|
|
40
|
-
opts[:assume_model_exists] = true if @assume_model_exists
|
|
41
|
-
RubyLLM.embed(text, **opts).vectors
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|