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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -16
  3. data/benchmark/bench_context_assembler.rb +2 -2
  4. data/benchmark/bench_regression.rb +5 -5
  5. data/benchmark/bench_token_estimator.rb +5 -5
  6. data/benchmark/bench_tool_schema.rb +1 -1
  7. data/benchmark/bench_vector_store.rb +1 -1
  8. data/lib/phronomy/agent/base.rb +86 -123
  9. data/lib/phronomy/agent/checkpoint.rb +118 -0
  10. data/lib/phronomy/agent/context/conversation/compaction_context.rb +117 -0
  11. data/lib/phronomy/agent/context/conversation/trigger_context.rb +43 -0
  12. data/lib/phronomy/agent/context/conversation/trim_context.rb +82 -0
  13. data/lib/phronomy/agent/context/instruction/prompt_template.rb +102 -0
  14. data/lib/phronomy/agent/context/knowledge/embeddings/base.rb +45 -0
  15. data/lib/phronomy/agent/context/knowledge/embeddings/ruby_llm_embeddings.rb +51 -0
  16. data/lib/phronomy/agent/context/knowledge/loader/base.rb +31 -0
  17. data/lib/phronomy/agent/context/knowledge/loader/csv_loader.rb +62 -0
  18. data/lib/phronomy/agent/context/knowledge/loader/markdown_loader.rb +82 -0
  19. data/lib/phronomy/agent/context/knowledge/loader/plain_text_loader.rb +28 -0
  20. data/lib/phronomy/agent/context/knowledge/source/base.rb +60 -0
  21. data/lib/phronomy/agent/context/knowledge/source/entity_knowledge.rb +102 -0
  22. data/lib/phronomy/agent/context/knowledge/source/rag_knowledge.rb +63 -0
  23. data/lib/phronomy/agent/context/knowledge/source/static_knowledge.rb +58 -0
  24. data/lib/phronomy/agent/context/knowledge/splitter/base.rb +53 -0
  25. data/lib/phronomy/agent/context/knowledge/splitter/fixed_size_splitter.rb +57 -0
  26. data/lib/phronomy/agent/context/knowledge/splitter/recursive_splitter.rb +111 -0
  27. data/lib/phronomy/agent/context/knowledge/vector_store/async_backend.rb +116 -0
  28. data/lib/phronomy/agent/context/knowledge/vector_store/base.rb +95 -0
  29. data/lib/phronomy/agent/context/knowledge/vector_store/in_memory.rb +109 -0
  30. data/lib/phronomy/agent/context/knowledge/vector_store/pgvector.rb +133 -0
  31. data/lib/phronomy/agent/context/knowledge/vector_store/redis_search.rb +198 -0
  32. data/lib/phronomy/agent/fsm.rb +1 -1
  33. data/lib/phronomy/agent/invocation_pipeline.rb +99 -0
  34. data/lib/phronomy/agent/lifecycle/fsm_session.rb +251 -0
  35. data/lib/phronomy/agent/lifecycle/phase_machine_builder.rb +249 -0
  36. data/lib/phronomy/agent/react_agent.rb +19 -14
  37. data/lib/phronomy/agent/runner.rb +2 -2
  38. data/lib/phronomy/agent/tool_executor.rb +108 -0
  39. data/lib/phronomy/concurrency/async_queue.rb +157 -0
  40. data/lib/phronomy/concurrency/blocking_adapter_pool.rb +443 -0
  41. data/lib/phronomy/concurrency/cancellation_scope.rb +125 -0
  42. data/lib/phronomy/concurrency/cancellation_token.rb +140 -0
  43. data/lib/phronomy/concurrency/concurrency_gate.rb +157 -0
  44. data/lib/phronomy/concurrency/deadline.rb +65 -0
  45. data/lib/phronomy/{runtime → concurrency}/gate_registry.rb +1 -1
  46. data/lib/phronomy/{runtime → concurrency}/pool_registry.rb +1 -1
  47. data/lib/phronomy/context.rb +2 -8
  48. data/lib/phronomy/embeddings.rb +2 -2
  49. data/lib/phronomy/eval/runner.rb +4 -0
  50. data/lib/phronomy/eval/scorer/llm_judge.rb +12 -1
  51. data/lib/phronomy/event_loop.rb +7 -7
  52. data/lib/phronomy/invocation_context.rb +3 -3
  53. data/lib/phronomy/knowledge_source.rb +0 -5
  54. data/lib/phronomy/llm_adapter/ruby_llm.rb +17 -11
  55. data/lib/phronomy/{context → llm_context_window}/assembler.rb +18 -3
  56. data/lib/phronomy/{context → llm_context_window}/context_version_cache.rb +1 -1
  57. data/lib/phronomy/{context → llm_context_window}/token_budget.rb +7 -4
  58. data/lib/phronomy/{context → llm_context_window}/token_estimator.rb +3 -3
  59. data/lib/phronomy/loader.rb +4 -4
  60. data/lib/phronomy/{agent → multi_agent}/handoff.rb +2 -2
  61. data/lib/phronomy/{agent → multi_agent}/orchestrator.rb +6 -6
  62. data/lib/phronomy/{agent → multi_agent}/parallel_tool_chat.rb +4 -4
  63. data/lib/phronomy/{agent → multi_agent}/team_coordinator.rb +2 -2
  64. data/lib/phronomy/runtime.rb +19 -4
  65. data/lib/phronomy/splitter.rb +3 -3
  66. data/lib/phronomy/task_group.rb +1 -1
  67. data/lib/phronomy/tool/base.rb +50 -9
  68. data/lib/phronomy/tracing/null_tracer.rb +3 -1
  69. data/lib/phronomy/vector_store.rb +2 -2
  70. data/lib/phronomy/version.rb +1 -1
  71. data/lib/phronomy/workflow_context.rb +8 -0
  72. data/lib/phronomy/workflow_runner.rb +11 -131
  73. data/lib/phronomy.rb +1 -0
  74. metadata +44 -42
  75. data/lib/phronomy/async_queue.rb +0 -155
  76. data/lib/phronomy/blocking_adapter_pool.rb +0 -435
  77. data/lib/phronomy/cancellation_scope.rb +0 -123
  78. data/lib/phronomy/cancellation_token.rb +0 -133
  79. data/lib/phronomy/concurrency_gate.rb +0 -155
  80. data/lib/phronomy/context/compaction_context.rb +0 -111
  81. data/lib/phronomy/context/trigger_context.rb +0 -39
  82. data/lib/phronomy/context/trim_context.rb +0 -75
  83. data/lib/phronomy/deadline.rb +0 -63
  84. data/lib/phronomy/embeddings/base.rb +0 -39
  85. data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +0 -45
  86. data/lib/phronomy/fsm_session.rb +0 -247
  87. data/lib/phronomy/knowledge_source/base.rb +0 -54
  88. data/lib/phronomy/knowledge_source/entity_knowledge.rb +0 -96
  89. data/lib/phronomy/knowledge_source/rag_knowledge.rb +0 -57
  90. data/lib/phronomy/knowledge_source/static_knowledge.rb +0 -52
  91. data/lib/phronomy/loader/base.rb +0 -25
  92. data/lib/phronomy/loader/csv_loader.rb +0 -56
  93. data/lib/phronomy/loader/markdown_loader.rb +0 -76
  94. data/lib/phronomy/loader/plain_text_loader.rb +0 -22
  95. data/lib/phronomy/prompt_template.rb +0 -96
  96. data/lib/phronomy/splitter/base.rb +0 -47
  97. data/lib/phronomy/splitter/fixed_size_splitter.rb +0 -51
  98. data/lib/phronomy/splitter/recursive_splitter.rb +0 -105
  99. data/lib/phronomy/tool_executor.rb +0 -106
  100. data/lib/phronomy/vector_store/async_backend.rb +0 -110
  101. data/lib/phronomy/vector_store/base.rb +0 -89
  102. data/lib/phronomy/vector_store/in_memory.rb +0 -93
  103. data/lib/phronomy/vector_store/pgvector.rb +0 -127
  104. 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
@@ -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