phronomy 0.8.0 → 0.9.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -41
  3. data/benchmark/baseline.json +1 -1
  4. data/benchmark/bench_agent_invoke.rb +1 -1
  5. data/benchmark/bench_context_assembler.rb +9 -1
  6. data/benchmark/bench_regression.rb +8 -8
  7. data/benchmark/bench_tool_schema.rb +2 -2
  8. data/benchmark/bench_vector_store.rb +1 -1
  9. data/docs/decisions/011-build-context-as-single-llm-input-authority.md +224 -0
  10. data/lib/phronomy/agent/base.rb +253 -351
  11. data/lib/phronomy/agent/concerns/suspendable.rb +6 -6
  12. data/lib/phronomy/agent/context/capability/base.rb +689 -0
  13. data/lib/phronomy/agent/context/capability/scope_policy.rb +54 -0
  14. data/lib/phronomy/agent/context/knowledge/base.rb +58 -0
  15. data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +102 -0
  16. data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +58 -0
  17. data/lib/phronomy/agent/invocation_pipeline.rb +10 -1
  18. data/lib/phronomy/agent/react_agent.rb +24 -23
  19. data/lib/phronomy/agent/shared_state.rb +2 -2
  20. data/lib/phronomy/agent/tool_executor.rb +1 -1
  21. data/lib/phronomy/concurrency/gate_registry.rb +0 -1
  22. data/lib/phronomy/configuration.rb +0 -6
  23. data/lib/phronomy/llm_context_window/assembler.rb +77 -44
  24. data/lib/phronomy/multi_agent/handoff.rb +4 -4
  25. data/lib/phronomy/multi_agent/orchestrator.rb +1 -1
  26. data/lib/phronomy/multi_agent/team_coordinator.rb +2 -2
  27. data/lib/phronomy/runtime/runtime_metrics.rb +0 -1
  28. data/lib/phronomy/runtime.rb +1 -2
  29. data/lib/phronomy/tool.rb +3 -4
  30. data/lib/phronomy/{tool/agent_tool.rb → tools/agent.rb} +6 -6
  31. data/lib/phronomy/{tool/mcp_tool.rb → tools/mcp.rb} +9 -9
  32. data/lib/phronomy/tools/vector_search.rb +70 -0
  33. data/lib/phronomy/vector_store/async_backend.rb +110 -0
  34. data/lib/phronomy/vector_store/base.rb +89 -0
  35. data/lib/phronomy/vector_store/embeddings/base.rb +41 -0
  36. data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +47 -0
  37. data/lib/phronomy/vector_store/in_memory.rb +103 -0
  38. data/lib/phronomy/vector_store/loader/base.rb +27 -0
  39. data/lib/phronomy/vector_store/loader/csv_loader.rb +58 -0
  40. data/lib/phronomy/vector_store/loader/markdown_loader.rb +78 -0
  41. data/lib/phronomy/vector_store/loader/plain_text_loader.rb +24 -0
  42. data/lib/phronomy/vector_store/pgvector.rb +127 -0
  43. data/lib/phronomy/vector_store/redis_search.rb +192 -0
  44. data/lib/phronomy/vector_store/splitter/base.rb +49 -0
  45. data/lib/phronomy/vector_store/splitter/fixed_size_splitter.rb +53 -0
  46. data/lib/phronomy/vector_store/splitter/recursive_splitter.rb +107 -0
  47. data/lib/phronomy/vector_store.rb +16 -4
  48. data/lib/phronomy/version.rb +1 -1
  49. data/lib/phronomy.rb +2 -1
  50. data/scripts/api_snapshot.rb +11 -9
  51. metadata +28 -32
  52. data/lib/phronomy/agent/context/conversation/compaction_context.rb +0 -117
  53. data/lib/phronomy/agent/context/conversation/trigger_context.rb +0 -43
  54. data/lib/phronomy/agent/context/conversation/trim_context.rb +0 -82
  55. data/lib/phronomy/agent/context/knowledge/embeddings/base.rb +0 -45
  56. data/lib/phronomy/agent/context/knowledge/embeddings/ruby_llm_embeddings.rb +0 -51
  57. data/lib/phronomy/agent/context/knowledge/loader/base.rb +0 -31
  58. data/lib/phronomy/agent/context/knowledge/loader/csv_loader.rb +0 -62
  59. data/lib/phronomy/agent/context/knowledge/loader/markdown_loader.rb +0 -82
  60. data/lib/phronomy/agent/context/knowledge/loader/plain_text_loader.rb +0 -28
  61. data/lib/phronomy/agent/context/knowledge/source/base.rb +0 -60
  62. data/lib/phronomy/agent/context/knowledge/source/entity_knowledge.rb +0 -102
  63. data/lib/phronomy/agent/context/knowledge/source/rag_knowledge.rb +0 -63
  64. data/lib/phronomy/agent/context/knowledge/source/static_knowledge.rb +0 -58
  65. data/lib/phronomy/agent/context/knowledge/splitter/base.rb +0 -53
  66. data/lib/phronomy/agent/context/knowledge/splitter/fixed_size_splitter.rb +0 -57
  67. data/lib/phronomy/agent/context/knowledge/splitter/recursive_splitter.rb +0 -111
  68. data/lib/phronomy/agent/context/knowledge/vector_store/async_backend.rb +0 -116
  69. data/lib/phronomy/agent/context/knowledge/vector_store/base.rb +0 -95
  70. data/lib/phronomy/agent/context/knowledge/vector_store/in_memory.rb +0 -109
  71. data/lib/phronomy/agent/context/knowledge/vector_store/pgvector.rb +0 -133
  72. data/lib/phronomy/agent/context/knowledge/vector_store/redis_search.rb +0 -198
  73. data/lib/phronomy/embeddings.rb +0 -11
  74. data/lib/phronomy/loader.rb +0 -13
  75. data/lib/phronomy/splitter.rb +0 -12
  76. data/lib/phronomy/tool/base.rb +0 -685
  77. data/lib/phronomy/tool/scope_policy.rb +0 -50
@@ -1,11 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- # Vector store implementations for embedding-based semantic search.
4
+ # Vector store infrastructure: backends, embeddings adapters, document loaders,
5
+ # and text splitters.
5
6
  #
6
- # Sub-classes are auto-loaded by Zeitwerk:
7
- # Phronomy::Agent::Context::Knowledge::VectorStore::Base
8
- # Phronomy::Agent::Context::Knowledge::VectorStore::InMemory
7
+ # Sub-namespaces are auto-loaded by Zeitwerk:
8
+ # Phronomy::VectorStore::Base
9
+ # Phronomy::VectorStore::InMemory
10
+ # Phronomy::VectorStore::Pgvector
11
+ # Phronomy::VectorStore::RedisSearch
12
+ # Phronomy::VectorStore::Embeddings::Base
13
+ # Phronomy::VectorStore::Embeddings::RubyLLMEmbeddings
14
+ # Phronomy::VectorStore::Loader::Base
15
+ # Phronomy::VectorStore::Loader::PlainTextLoader
16
+ # Phronomy::VectorStore::Loader::MarkdownLoader
17
+ # Phronomy::VectorStore::Loader::CsvLoader
18
+ # Phronomy::VectorStore::Splitter::Base
19
+ # Phronomy::VectorStore::Splitter::FixedSizeSplitter
20
+ # Phronomy::VectorStore::Splitter::RecursiveSplitter
9
21
  module VectorStore
10
22
  end
11
23
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/phronomy.rb CHANGED
@@ -8,7 +8,8 @@ loader = Zeitwerk::Loader.for_gem
8
8
  # Teach Zeitwerk that "llm" maps to "LLM" so that file names such as
9
9
  # ruby_llm_embeddings.rb resolve to RubyLLMEmbeddings (not RubyLlmEmbeddings).
10
10
  loader.inflector.inflect("ruby_llm_embeddings" => "RubyLLMEmbeddings")
11
- loader.inflector.inflect("rag_knowledge" => "RAGKnowledge")
11
+ # RAG: Zeitwerk would infer "Rag" override to "RAG".
12
+ loader.inflector.inflect("rag" => "RAG")
12
13
  # FSMSession: Zeitwerk would infer "FsmSession" — override to "FSMSession".
13
14
  loader.inflector.inflect("fsm_session" => "FSMSession")
14
15
  # AgentFSM: Zeitwerk would infer "Fsm" — override to "FSM".
@@ -24,26 +24,28 @@ require_relative "../lib/phronomy"
24
24
  PUBLIC_API_ENTRIES = [
25
25
  # Stable
26
26
  Phronomy::Agent::Base,
27
- Phronomy::Tool::Base,
27
+ Phronomy::Agent::Context::Capability::Base,
28
28
  Phronomy::Workflow,
29
29
  Phronomy::WorkflowContext,
30
30
  Phronomy::Runnable,
31
- Phronomy::PromptTemplate,
31
+ Phronomy::Agent::Context::Instruction::PromptTemplate,
32
32
  # Beta
33
33
  Phronomy::Agent::ReactAgent,
34
- Phronomy::Agent::Orchestrator,
35
- Phronomy::Agent::TeamCoordinator,
34
+ Phronomy::MultiAgent::Orchestrator,
35
+ Phronomy::MultiAgent::TeamCoordinator,
36
36
  Phronomy::Guardrail::InputGuardrail,
37
37
  Phronomy::Guardrail::OutputGuardrail,
38
38
  Phronomy::VectorStore::Base,
39
39
  Phronomy::VectorStore::InMemory,
40
- Phronomy::Embeddings::Base,
41
- Phronomy::KnowledgeSource::Base,
42
- Phronomy::KnowledgeSource::StaticKnowledge,
43
- Phronomy::KnowledgeSource::RAGKnowledge,
40
+ Phronomy::VectorStore::Embeddings::Base,
41
+ Phronomy::Agent::Context::Knowledge::Base,
42
+ Phronomy::Agent::Context::Knowledge::StaticKnowledge,
44
43
  Phronomy::Tracing::Base,
45
44
  Phronomy::Tracing::NullTracer,
46
- Phronomy::Eval::Runner
45
+ Phronomy::Eval::Runner,
46
+ Phronomy::Tools::Mcp,
47
+ Phronomy::Tools::Agent,
48
+ Phronomy::Tools::VectorSearch
47
49
  ].freeze
48
50
 
49
51
  # Baseline methods common to all Ruby objects — excluded from the snapshot.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phronomy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raizo T.C.S
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-30 00:00:00.000000000 Z
11
+ date: 2026-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -64,8 +64,9 @@ dependencies:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
66
  version: '0.6'
67
- description: Phronomy provides Agent, Workflow, Tool, Guardrail, RAG, and Multi-agent
68
- capabilities for building AI agents in Ruby. Powered by RubyLLM for LLM abstraction.
67
+ description: Phronomy provides composable building blocks — Agents, Workflows, Tools,
68
+ Guardrails, and Tracing — for building AI agents in Ruby. Powered by RubyLLM for
69
+ LLM abstraction.
69
70
  email:
70
71
  - raizo.tcs@gmail.com
71
72
  executables: []
@@ -99,6 +100,7 @@ files:
99
100
  - docs/decisions/008-orchestrator-uses-os-threads.md
100
101
  - docs/decisions/009-state-store-abstraction.md
101
102
  - docs/decisions/010-cooperative-first-concurrency.md
103
+ - docs/decisions/011-build-context-as-single-llm-input-authority.md
102
104
  - lib/phronomy.rb
103
105
  - lib/phronomy/agent.rb
104
106
  - lib/phronomy/agent/base.rb
@@ -109,28 +111,12 @@ files:
109
111
  - lib/phronomy/agent/concerns/guardrailable.rb
110
112
  - lib/phronomy/agent/concerns/retryable.rb
111
113
  - lib/phronomy/agent/concerns/suspendable.rb
112
- - lib/phronomy/agent/context/conversation/compaction_context.rb
113
- - lib/phronomy/agent/context/conversation/trigger_context.rb
114
- - lib/phronomy/agent/context/conversation/trim_context.rb
114
+ - lib/phronomy/agent/context/capability/base.rb
115
+ - lib/phronomy/agent/context/capability/scope_policy.rb
115
116
  - lib/phronomy/agent/context/instruction/prompt_template.rb
116
- - lib/phronomy/agent/context/knowledge/embeddings/base.rb
117
- - lib/phronomy/agent/context/knowledge/embeddings/ruby_llm_embeddings.rb
118
- - lib/phronomy/agent/context/knowledge/loader/base.rb
119
- - lib/phronomy/agent/context/knowledge/loader/csv_loader.rb
120
- - lib/phronomy/agent/context/knowledge/loader/markdown_loader.rb
121
- - lib/phronomy/agent/context/knowledge/loader/plain_text_loader.rb
122
- - lib/phronomy/agent/context/knowledge/source/base.rb
123
- - lib/phronomy/agent/context/knowledge/source/entity_knowledge.rb
124
- - lib/phronomy/agent/context/knowledge/source/rag_knowledge.rb
125
- - lib/phronomy/agent/context/knowledge/source/static_knowledge.rb
126
- - lib/phronomy/agent/context/knowledge/splitter/base.rb
127
- - lib/phronomy/agent/context/knowledge/splitter/fixed_size_splitter.rb
128
- - lib/phronomy/agent/context/knowledge/splitter/recursive_splitter.rb
129
- - lib/phronomy/agent/context/knowledge/vector_store/async_backend.rb
130
- - lib/phronomy/agent/context/knowledge/vector_store/base.rb
131
- - lib/phronomy/agent/context/knowledge/vector_store/in_memory.rb
132
- - lib/phronomy/agent/context/knowledge/vector_store/pgvector.rb
133
- - lib/phronomy/agent/context/knowledge/vector_store/redis_search.rb
117
+ - lib/phronomy/agent/context/knowledge/base.rb
118
+ - lib/phronomy/agent/context/knowledge/entity_knowledge.rb
119
+ - lib/phronomy/agent/context/knowledge/static_knowledge.rb
134
120
  - lib/phronomy/agent/fsm.rb
135
121
  - lib/phronomy/agent/invocation_pipeline.rb
136
122
  - lib/phronomy/agent/lifecycle/fsm_session.rb
@@ -151,7 +137,6 @@ files:
151
137
  - lib/phronomy/configuration.rb
152
138
  - lib/phronomy/context.rb
153
139
  - lib/phronomy/diagnostics.rb
154
- - lib/phronomy/embeddings.rb
155
140
  - lib/phronomy/eval.rb
156
141
  - lib/phronomy/eval/comparison.rb
157
142
  - lib/phronomy/eval/dataset.rb
@@ -181,7 +166,6 @@ files:
181
166
  - lib/phronomy/llm_context_window/context_version_cache.rb
182
167
  - lib/phronomy/llm_context_window/token_budget.rb
183
168
  - lib/phronomy/llm_context_window/token_estimator.rb
184
- - lib/phronomy/loader.rb
185
169
  - lib/phronomy/metrics.rb
186
170
  - lib/phronomy/multi_agent/handoff.rb
187
171
  - lib/phronomy/multi_agent/orchestrator.rb
@@ -203,7 +187,6 @@ files:
203
187
  - lib/phronomy/runtime/thread_scheduler.rb
204
188
  - lib/phronomy/runtime/timer_queue.rb
205
189
  - lib/phronomy/runtime/timer_service.rb
206
- - lib/phronomy/splitter.rb
207
190
  - lib/phronomy/state_store/base.rb
208
191
  - lib/phronomy/state_store/in_memory.rb
209
192
  - lib/phronomy/task.rb
@@ -218,16 +201,29 @@ files:
218
201
  - lib/phronomy/testing/scheduler_helpers.rb
219
202
  - lib/phronomy/token_usage.rb
220
203
  - lib/phronomy/tool.rb
221
- - lib/phronomy/tool/agent_tool.rb
222
- - lib/phronomy/tool/base.rb
223
- - lib/phronomy/tool/mcp_tool.rb
224
- - lib/phronomy/tool/scope_policy.rb
204
+ - lib/phronomy/tools/agent.rb
205
+ - lib/phronomy/tools/mcp.rb
206
+ - lib/phronomy/tools/vector_search.rb
225
207
  - lib/phronomy/tracing.rb
226
208
  - lib/phronomy/tracing/base.rb
227
209
  - lib/phronomy/tracing/langfuse_tracer.rb
228
210
  - lib/phronomy/tracing/null_tracer.rb
229
211
  - lib/phronomy/tracing/open_telemetry_tracer.rb
230
212
  - lib/phronomy/vector_store.rb
213
+ - lib/phronomy/vector_store/async_backend.rb
214
+ - lib/phronomy/vector_store/base.rb
215
+ - lib/phronomy/vector_store/embeddings/base.rb
216
+ - lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb
217
+ - lib/phronomy/vector_store/in_memory.rb
218
+ - lib/phronomy/vector_store/loader/base.rb
219
+ - lib/phronomy/vector_store/loader/csv_loader.rb
220
+ - lib/phronomy/vector_store/loader/markdown_loader.rb
221
+ - lib/phronomy/vector_store/loader/plain_text_loader.rb
222
+ - lib/phronomy/vector_store/pgvector.rb
223
+ - lib/phronomy/vector_store/redis_search.rb
224
+ - lib/phronomy/vector_store/splitter/base.rb
225
+ - lib/phronomy/vector_store/splitter/fixed_size_splitter.rb
226
+ - lib/phronomy/vector_store/splitter/recursive_splitter.rb
231
227
  - lib/phronomy/version.rb
232
228
  - lib/phronomy/workflow.rb
233
229
  - lib/phronomy/workflow_context.rb
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Context
6
- module Conversation
7
- # Context object passed to the +on_compact+ callback registered on an agent.
8
- #
9
- # The callback calls #compact one or more times to specify which ranges of
10
- # messages to replace with a summary. Each call:
11
- # 1. Yields the selected message elements to the block.
12
- # 2. Receives the block's return value as the summary text.
13
- # 3. Persists a compaction record to the memory store (if available).
14
- # 4. Updates #result_messages so that the compacted range is replaced
15
- # by a single +:system+ summary message.
16
- #
17
- # The agent reads #result_messages after the callback returns and uses it
18
- # as the new message list for this invocation.
19
- #
20
- # @example Summarise the oldest half of the conversation
21
- # on_compact do |ctx|
22
- # half = ctx.message_elements.length / 2
23
- # ctx.compact(0...half) do |elements|
24
- # texts = elements.map { |e| "#{e[:role]}: #{e[:message].content}" }.join("\n")
25
- # "Summary of earlier conversation:\n#{texts}"
26
- # end
27
- # end
28
- class CompactionContext
29
- # @return [Array<Hash>] message elements at compaction time
30
- attr_reader :message_elements
31
-
32
- # @return [Phronomy::LlmContextWindow::TokenBudget, nil]
33
- attr_reader :budget
34
-
35
- # @return [Integer] total estimated token count before compaction
36
- attr_reader :total_tokens
37
-
38
- # The current message list to be used after all compact calls have been made.
39
- # Updated by each call to #compact.
40
- #
41
- # @return [Array]
42
- attr_reader :result_messages
43
-
44
- # @param message_elements [Array<Hash>]
45
- # each element: { seq: Integer, message: Object, tokens: Integer, role: Symbol }
46
- # @param budget [Phronomy::LlmContextWindow::TokenBudget, nil]
47
- # @param thread_id [String, nil] used when saving compaction records
48
- # @param memory [Object, nil] memory object; must respond to #save_compaction
49
- # for compaction records to be persisted
50
- # @api private
51
- # mutant:disable - e[:tokens] vs e.fetch(:tokens) and e[:message] vs e.fetch(:message) are genuine equivalent mutations: elements always carry both keys
52
- def initialize(message_elements:, budget:, thread_id: nil, memory: nil)
53
- @message_elements = message_elements.dup
54
- @budget = budget
55
- @total_tokens = message_elements.sum { |e| e[:tokens] }
56
- @thread_id = thread_id
57
- @memory = memory
58
- @result_messages = @message_elements.map { |e| e[:message] }
59
- end
60
-
61
- # Replace a range of messages with a summary produced by the block.
62
- #
63
- # The block receives the selected Array<Hash> elements and must return a
64
- # String that serves as the summary text. After the call, #result_messages
65
- # reflects the replacement.
66
- #
67
- # If the memory object responds to #save_compaction, a compaction record
68
- # { start_seq:, end_seq:, summary_text: } is persisted for auditability.
69
- #
70
- # @param range [Range, Integer] index range into message_elements (0-based)
71
- # @yieldparam elements [Array<Hash>] the selected message elements
72
- # @yieldreturn [String] summary text to replace the selected messages
73
- # @return [Array] the updated result_messages array
74
- # @api private
75
- # mutant:disable - multiple genuine equivalent mutations: is_a? vs instance_of? (Array/Range have no subclasses), yield.to_s vs yield (block always returns String), [:seq]/[:message] vs .fetch(:seq)/.fetch(:message) (keys always present), range.to_i vs range/to_int/Integer() (Integer is already integer), || [] vs nothing (Array#[] never returns nil for slice), RubyLLM::Message vs Message (killfork inherits Message=Struct from integration specs, both expose identical role/content interface)
76
- def compact(range)
77
- # Normalise: Integer index → single-element Array; Range → Array slice.
78
- raw = @message_elements[range]
79
- elements = if raw.is_a?(Array)
80
- raw
81
- elsif raw.nil?
82
- []
83
- else
84
- [raw]
85
- end
86
- return @result_messages if elements.empty?
87
-
88
- summary_text = yield(elements).to_s
89
-
90
- start_seq = elements.first[:seq]
91
- end_seq = elements.last[:seq]
92
-
93
- if @memory && @thread_id && @memory.respond_to?(:save_compaction)
94
- @memory.save_compaction(
95
- thread_id: @thread_id,
96
- start_seq: start_seq,
97
- end_seq: end_seq,
98
- summary_text: summary_text
99
- )
100
- end
101
-
102
- # Compute the last included index in the original @message_elements array.
103
- last_idx = if range.is_a?(Range)
104
- range.exclude_end? ? range.last - 1 : range.last
105
- else
106
- range.to_i
107
- end
108
-
109
- remaining = (@message_elements[(last_idx + 1)..] || []).map { |e| e[:message] }
110
- summary_msg = RubyLLM::Message.new(role: :system, content: summary_text)
111
- @result_messages = [summary_msg] + remaining
112
- end
113
- end
114
- end
115
- end
116
- end
117
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Context
6
- module Conversation
7
- # Read-only context passed to the +on_compaction_trigger+ callback.
8
- #
9
- # The callback inspects the current message list and budget, then returns
10
- # a truthy value to trigger compaction or a falsy value to skip it.
11
- #
12
- # No mutations are allowed through this object; use CompactionContext
13
- # (passed to +on_compact+) for actual modifications.
14
- #
15
- # @example Trigger compaction when messages exceed 80% of the input budget
16
- # on_compaction_trigger do |ctx|
17
- # limit = ctx.budget&.available(used: 0) || Float::INFINITY
18
- # ctx.total_tokens > limit * 0.8
19
- # end
20
- class TriggerContext
21
- # @return [Array<Hash>] frozen snapshot of message elements
22
- # each element: { seq: Integer, message: Object, tokens: Integer, role: Symbol }
23
- attr_reader :message_elements
24
-
25
- # @return [Phronomy::LlmContextWindow::TokenBudget, nil] token budget for this invocation
26
- attr_reader :budget
27
-
28
- # @return [Integer] total estimated token count of all message elements
29
- attr_reader :total_tokens
30
-
31
- # @param message_elements [Array<Hash>]
32
- # @param budget [Phronomy::LlmContextWindow::TokenBudget, nil]
33
- # @api private
34
- def initialize(message_elements:, budget:)
35
- @message_elements = message_elements.dup.freeze
36
- @budget = budget
37
- @total_tokens = message_elements.sum { |e| e[:tokens] }
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Context
6
- module Conversation
7
- # Context object passed to the +on_trim+ callback registered on an agent class.
8
- #
9
- # The callback receives a TrimContext and may call #remove to drop specific
10
- # messages from the conversation before the LLM is called. Changes affect
11
- # only the current invocation; the underlying memory store is not modified.
12
- #
13
- # Message elements are identified by a +:seq+ integer that is assigned
14
- # sequentially (0-based) when messages are loaded from memory each turn.
15
- #
16
- # @example Remove the oldest two messages when the budget is tight
17
- # on_trim do |ctx|
18
- # if ctx.total_tokens > ctx.budget.available(used: 0) * 0.9
19
- # seqs_to_drop = ctx.message_elements.first(2).map { |e| e[:seq] }
20
- # ctx.remove(seqs_to_drop)
21
- # end
22
- # end
23
- class TrimContext
24
- # @return [Phronomy::LlmContextWindow::TokenBudget, nil] token budget for this invocation
25
- attr_reader :budget
26
-
27
- # @return [Integer] total estimated token count of all current message elements
28
- attr_reader :total_tokens
29
-
30
- # @param message_elements [Array<Hash>]
31
- # each element: { seq: Integer, message: Object, tokens: Integer, role: Symbol }
32
- # @param budget [Phronomy::LlmContextWindow::TokenBudget, nil]
33
- # @api private
34
- def initialize(message_elements:, budget:)
35
- @message_elements = message_elements.dup
36
- @budget = budget
37
- recalculate!
38
- end
39
-
40
- # Returns a snapshot of the current message elements (defensive copy).
41
- # Each element is a Hash with +:seq+, +:message+, +:tokens+, and +:role+.
42
- #
43
- # @return [Array<Hash>]
44
- # @api private
45
- def message_elements
46
- @message_elements.dup
47
- end
48
-
49
- # Remove messages identified by seq numbers.
50
- # Calling this multiple times accumulates removals.
51
- #
52
- # @param seqs [Integer, Array<Integer>] seq number(s) to remove
53
- # @return [self]
54
- # @api private
55
- # mutant:disable - Array(seqs).to_set vs Array(seqs) and e[:seq] vs e.fetch(:seq) are genuine equivalent: Array#include? returns identical results for both
56
- def remove(seqs)
57
- seqs_set = Array(seqs).to_set
58
- @message_elements.reject! { |e| seqs_set.include?(e[:seq]) }
59
- recalculate!
60
- self
61
- end
62
-
63
- # Convenience: returns the plain message objects (without element metadata).
64
- #
65
- # @return [Array]
66
- # @api private
67
- # mutant:disable - e[:message] vs e.fetch(:message) is a genuine equivalent mutation: elements always carry :message
68
- def messages
69
- @message_elements.map { |e| e[:message] }
70
- end
71
-
72
- private
73
-
74
- # mutant:disable - e[:tokens] vs e.fetch(:tokens) is a genuine equivalent mutation: elements always carry :tokens
75
- def recalculate!
76
- @total_tokens = @message_elements.sum { |e| e[:tokens] }
77
- end
78
- end
79
- end
80
- end
81
- end
82
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Context
6
- module Knowledge
7
- module Embeddings
8
- # Abstract interface for embedding adapters.
9
- #
10
- # Concrete implementations must override {#embed} and return a vector
11
- # as an +Array<Float>+.
12
- class Base
13
- # Embed the given text and return a vector representation.
14
- #
15
- # @param text [String] the text to embed
16
- # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil] optional; raises CancellationError when cancelled
17
- # @return [Array<Float>] the embedding vector
18
- # @api public
19
- def embed(text, cancellation_token = nil)
20
- cancellation_token&.raise_if_cancelled!
21
- raise NotImplementedError, "#{self.class}#embed is not implemented"
22
- end
23
-
24
- # Submits an {#embed} call to {BlockingAdapterPool} and returns a
25
- # {BlockingAdapterPool::PendingOperation}.
26
- #
27
- # @param text [String]
28
- # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
29
- # @param timeout [Numeric, nil] seconds before the operation is abandoned
30
- # @return [BlockingAdapterPool::PendingOperation]
31
- # @api public
32
- def embed_async(text, cancellation_token = nil, timeout: nil)
33
- Phronomy::Runtime.instance.blocking_io.submit(
34
- timeout: timeout,
35
- cancellation_token: cancellation_token
36
- ) do
37
- embed(text, cancellation_token)
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Context
6
- module Knowledge
7
- module Embeddings
8
- # Embeddings adapter backed by RubyLLM.
9
- #
10
- # Delegates to +RubyLLM.embed+ and returns the resulting vector as an
11
- # +Array<Float>+.
12
- #
13
- # @example Default model
14
- # embeddings = Phronomy::Agent::Context::Knowledge::Embeddings::RubyLLMEmbeddings.new
15
- # vector = embeddings.embed("Hello, world!")
16
- #
17
- # @example Explicit model
18
- # embeddings = Phronomy::Agent::Context::Knowledge::Embeddings::RubyLLMEmbeddings.new(model: "text-embedding-3-small")
19
- # vector = embeddings.embed("Hello, world!")
20
- class RubyLLMEmbeddings < Base
21
- # @param model [String, nil] embedding model identifier; nil uses the RubyLLM default
22
- # @param provider [Symbol, nil] provider override (e.g. :openai); nil uses the RubyLLM default
23
- # @param assume_model_exists [Boolean] when true, skips RubyLLM model-registry validation
24
- # (useful for locally hosted models not in the registry)
25
- # @api public
26
- def initialize(model: nil, provider: nil, assume_model_exists: false)
27
- @model = model
28
- @provider = provider
29
- @assume_model_exists = assume_model_exists
30
- end
31
-
32
- # Embed text via RubyLLM.
33
- #
34
- # @param text [String]
35
- # @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil] optional; raises CancellationError when cancelled
36
- # @return [Array<Float>]
37
- # @api public
38
- def embed(text, cancellation_token = nil)
39
- cancellation_token&.raise_if_cancelled!
40
- opts = {}
41
- opts[:model] = @model if @model
42
- opts[:provider] = @provider if @provider
43
- opts[:assume_model_exists] = true if @assume_model_exists
44
- RubyLLM.embed(text, **opts).vectors
45
- end
46
- end
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- module Agent
5
- module Context
6
- module Knowledge
7
- module Loader
8
- # Abstract base class for document loaders.
9
- #
10
- # A loader converts an external source (file path, URL, etc.) into an
11
- # Array of document hashes understood by the rest of the pipeline:
12
- #
13
- # [{ text: String, metadata: Hash }, ...]
14
- #
15
- # Subclasses must implement {#load}.
16
- class Base
17
- # Load documents from +source+ and return an array of document hashes.
18
- #
19
- # @param source [String] file path, URL, or other source identifier
20
- # @return [Array<Hash>] array of <tt>{ text: String, metadata: Hash }</tt>
21
- # @raise [NotImplementedError] when not overridden by a subclass
22
- # @api public
23
- def load(source)
24
- raise NotImplementedError, "#{self.class}#load is not implemented"
25
- end
26
- end
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "csv"
4
-
5
- module Phronomy
6
- module Agent
7
- module Context
8
- module Knowledge
9
- module Loader
10
- # Loads a CSV file, converting each row into a separate document.
11
- #
12
- # By default the first row is treated as a header and column names are
13
- # available in the document metadata. The full row is serialised to
14
- # a human-readable "key: value" string for embedding.
15
- #
16
- # @example
17
- # loader = Phronomy::Agent::Context::Knowledge::Loader::CsvLoader.new
18
- # docs = loader.load("products.csv")
19
- # # => [
20
- # # { text: "name: Widget\nprice: 9.99", metadata: { source: "...", row: 1, name: "Widget", price: "9.99" } },
21
- # # ...
22
- # # ]
23
- class CsvLoader < Base
24
- # @param headers [Boolean] treat the first row as headers (default: true)
25
- # @param text_column [String, nil] if set, use only this column as the document text
26
- # @api public
27
- def initialize(headers: true, text_column: nil)
28
- @headers = headers
29
- @text_column = text_column
30
- end
31
-
32
- # @param source [String] path to a CSV file
33
- # @return [Array<Hash>]
34
- # @raise [Errno::ENOENT] if the file does not exist
35
- # @api public
36
- def load(source)
37
- rows = CSV.read(source, headers: @headers, encoding: "UTF-8")
38
-
39
- if @headers
40
- rows.each_with_index.map do |row, idx|
41
- row_hash = row.to_h
42
- text = if @text_column
43
- row_hash[@text_column].to_s
44
- else
45
- row_hash.map { |k, v| "#{k}: #{v}" }.join("\n")
46
- end
47
- metadata = row_hash.transform_keys(&:to_sym).merge(source: source, row: idx + 1)
48
- {text: text, metadata: metadata}
49
- end
50
- else
51
- rows.each_with_index.map do |row, idx|
52
- text = row.join(", ")
53
- {text: text, metadata: {source: source, row: idx + 1}}
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
61
- end
62
- end