phronomy 0.15.1 → 0.17.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 +8 -9
- data/CHANGELOG.md +159 -28
- data/CONTRIBUTING.md +28 -16
- data/README.md +400 -143
- data/benchmark/baseline.json +2 -3
- data/benchmark/bench_agent_invoke.rb +7 -4
- data/benchmark/bench_context_assembler.rb +134 -34
- data/benchmark/bench_regression.rb +3 -19
- data/benchmark/bench_tool_schema.rb +2 -34
- data/docs/decisions/005-static-knowledge-class-level-cache.md +12 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +7 -0
- data/docs/decisions/011-build-context-as-single-llm-input-authority.md +40 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +69 -0
- data/docs/decisions/013-journal-backed-knowledge-as-context-candidates.md +122 -0
- data/lib/phronomy/agent/activation_registry.rb +28 -0
- data/lib/phronomy/agent/agent_execution.rb +97 -0
- data/lib/phronomy/agent/agent_execution_activation.rb +172 -0
- data/lib/phronomy/agent/agent_invocation.rb +44 -46
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +206 -104
- data/lib/phronomy/agent/agent_root.rb +66 -0
- data/lib/phronomy/agent/async_event_api.rb +55 -475
- data/lib/phronomy/agent/base.rb +351 -514
- data/lib/phronomy/agent/concerns/before_llm_input.rb +66 -0
- data/lib/phronomy/agent/context/capability/base.rb +166 -297
- data/lib/phronomy/agent/context_assembler.rb +357 -0
- data/lib/phronomy/agent/context_candidate.rb +47 -0
- data/lib/phronomy/agent/context_candidate_resolver.rb +65 -0
- data/lib/phronomy/agent/context_importer.rb +217 -0
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +53 -0
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +56 -0
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +30 -0
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +118 -0
- data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +37 -0
- data/lib/phronomy/agent/context_plan.rb +25 -0
- data/lib/phronomy/agent/context_plan_validator.rb +134 -0
- data/lib/phronomy/agent/context_policies/default.rb +53 -0
- data/lib/phronomy/agent/context_policy.rb +15 -0
- data/lib/phronomy/agent/context_policy_descriptor.rb +49 -0
- data/lib/phronomy/agent/context_policy_registry.rb +46 -0
- data/lib/phronomy/agent/context_request.rb +35 -0
- data/lib/phronomy/agent/context_selection_unit.rb +38 -0
- data/lib/phronomy/agent/derived_content_spec.rb +34 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1122 -0
- data/lib/phronomy/agent/immutable.rb +31 -0
- data/lib/phronomy/agent/journal_projection.rb +60 -0
- data/lib/phronomy/agent/journal_record.rb +67 -0
- data/lib/phronomy/agent/llm_call_record.rb +51 -0
- data/lib/phronomy/agent/llm_input_build_context.rb +17 -0
- data/lib/phronomy/agent/llm_input_manifest.rb +103 -0
- data/lib/phronomy/agent/llm_input_patch.rb +21 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +12 -0
- data/lib/phronomy/agent/provider_call_outcome.rb +90 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +189 -0
- data/lib/phronomy/agent/shared_state.rb +46 -138
- data/lib/phronomy/agent/token_budget_resolver.rb +70 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -4
- data/lib/phronomy/agent/tool_definition_set.rb +55 -0
- data/lib/phronomy/agent/tool_invocation.rb +108 -314
- data/lib/phronomy/agent.rb +10 -16
- data/lib/phronomy/agent_busy_error.rb +5 -0
- data/lib/phronomy/canonical_json.rb +136 -0
- data/lib/phronomy/configuration.rb +17 -155
- data/lib/phronomy/content_store/base.rb +51 -0
- data/lib/phronomy/context_budget_exceeded_error.rb +8 -0
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +7 -80
- data/lib/phronomy/engine/event_loop.rb +3 -0
- data/lib/phronomy/engine/runtime.rb +15 -230
- data/lib/phronomy/engine/task_group.rb +30 -102
- data/lib/phronomy/execution_rehydration_required_error.rb +5 -0
- data/lib/phronomy/invalid_context_budget_configuration_error.rb +8 -0
- data/lib/phronomy/llm_context_window/token_budget.rb +8 -79
- data/lib/phronomy/multi_agent/orchestrator.rb +153 -204
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +7 -5
- data/lib/phronomy/multi_agent/team_coordinator.rb +46 -133
- data/lib/phronomy/persistence/in_memory.rb +247 -0
- data/lib/phronomy/persistence.rb +39 -0
- data/lib/phronomy/tools/agent.rb +14 -36
- data/lib/phronomy/vector_store/in_memory.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy.rb +9 -115
- data/scripts/add_to_h_to_token_doubles.rb +33 -0
- data/scripts/add_to_h_unnamed_doubles.rb +27 -0
- data/scripts/api_snapshot.rb +1 -12
- data/scripts/migrate_spec_agent_definition.rb +108 -0
- data/scripts/migrate_spec_agent_definition_pass2.rb +53 -0
- data/scripts/migrate_spec_inline_pass3.rb +24 -0
- metadata +54 -13
- data/lib/phronomy/agent/agent_invocation_registry.rb +0 -75
- data/lib/phronomy/agent/before_completion_context.rb +0 -47
- data/lib/phronomy/agent/concerns/before_completion.rb +0 -111
- data/lib/phronomy/agent/context/knowledge/base.rb +0 -58
- data/lib/phronomy/agent/context/knowledge/entity_knowledge.rb +0 -102
- data/lib/phronomy/agent/context/knowledge/static_knowledge.rb +0 -58
- data/lib/phronomy/knowledge_source.rb +0 -12
- data/lib/phronomy/llm_context_window/assembler.rb +0 -191
- data/lib/phronomy/llm_context_window/context_version_cache.rb +0 -52
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.
|
|
4
|
+
version: 0.17.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-08-
|
|
11
|
+
date: 2026-08-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -16,7 +16,7 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1.
|
|
19
|
+
version: '1.15'
|
|
20
20
|
- - "<"
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
22
|
version: '2'
|
|
@@ -26,7 +26,7 @@ dependencies:
|
|
|
26
26
|
requirements:
|
|
27
27
|
- - ">="
|
|
28
28
|
- !ruby/object:Gem::Version
|
|
29
|
-
version: '1.
|
|
29
|
+
version: '1.15'
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '2'
|
|
@@ -116,37 +116,72 @@ files:
|
|
|
116
116
|
- docs/decisions/010-cooperative-first-concurrency.md
|
|
117
117
|
- docs/decisions/011-build-context-as-single-llm-input-authority.md
|
|
118
118
|
- docs/decisions/011-delegate-transport-policy-to-adapters.md
|
|
119
|
+
- docs/decisions/012-canonical-execution-log-and-context-policy.md
|
|
120
|
+
- docs/decisions/013-journal-backed-knowledge-as-context-candidates.md
|
|
119
121
|
- docs/mcp-client.md
|
|
120
122
|
- examples/workflows/agent_event_mapping.rb
|
|
121
123
|
- examples/workflows/generic_task_event_mapping.rb
|
|
122
124
|
- gemfiles/mcp_1_0.gemfile
|
|
123
125
|
- lib/phronomy.rb
|
|
124
126
|
- lib/phronomy/agent.rb
|
|
127
|
+
- lib/phronomy/agent/activation_registry.rb
|
|
128
|
+
- lib/phronomy/agent/agent_execution.rb
|
|
129
|
+
- lib/phronomy/agent/agent_execution_activation.rb
|
|
125
130
|
- lib/phronomy/agent/agent_invocation.rb
|
|
126
|
-
- lib/phronomy/agent/agent_invocation_registry.rb
|
|
127
131
|
- lib/phronomy/agent/agent_invocation_session_builder.rb
|
|
132
|
+
- lib/phronomy/agent/agent_root.rb
|
|
128
133
|
- lib/phronomy/agent/approval_evaluation_request.rb
|
|
129
134
|
- lib/phronomy/agent/async_event_api.rb
|
|
130
135
|
- lib/phronomy/agent/base.rb
|
|
131
|
-
- lib/phronomy/agent/
|
|
132
|
-
- lib/phronomy/agent/concerns/before_completion.rb
|
|
136
|
+
- lib/phronomy/agent/concerns/before_llm_input.rb
|
|
133
137
|
- lib/phronomy/agent/concerns/error_translation.rb
|
|
134
138
|
- lib/phronomy/agent/concerns/filterable.rb
|
|
135
139
|
- lib/phronomy/agent/context/capability/base.rb
|
|
136
140
|
- lib/phronomy/agent/context/instruction/prompt_template.rb
|
|
137
|
-
- lib/phronomy/agent/
|
|
138
|
-
- lib/phronomy/agent/
|
|
139
|
-
- lib/phronomy/agent/
|
|
141
|
+
- lib/phronomy/agent/context_assembler.rb
|
|
142
|
+
- lib/phronomy/agent/context_candidate.rb
|
|
143
|
+
- lib/phronomy/agent/context_candidate_resolver.rb
|
|
144
|
+
- lib/phronomy/agent/context_importer.rb
|
|
145
|
+
- lib/phronomy/agent/context_parts/budget/token_budget_packer.rb
|
|
146
|
+
- lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb
|
|
147
|
+
- lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb
|
|
148
|
+
- lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb
|
|
149
|
+
- lib/phronomy/agent/context_parts/validators/final_budget_validator.rb
|
|
150
|
+
- lib/phronomy/agent/context_plan.rb
|
|
151
|
+
- lib/phronomy/agent/context_plan_validator.rb
|
|
152
|
+
- lib/phronomy/agent/context_policies/default.rb
|
|
153
|
+
- lib/phronomy/agent/context_policy.rb
|
|
154
|
+
- lib/phronomy/agent/context_policy_descriptor.rb
|
|
155
|
+
- lib/phronomy/agent/context_policy_registry.rb
|
|
156
|
+
- lib/phronomy/agent/context_request.rb
|
|
157
|
+
- lib/phronomy/agent/context_selection_unit.rb
|
|
158
|
+
- lib/phronomy/agent/derived_content_spec.rb
|
|
159
|
+
- lib/phronomy/agent/execution_coordinator.rb
|
|
160
|
+
- lib/phronomy/agent/immutable.rb
|
|
161
|
+
- lib/phronomy/agent/journal_projection.rb
|
|
162
|
+
- lib/phronomy/agent/journal_record.rb
|
|
163
|
+
- lib/phronomy/agent/llm_call_record.rb
|
|
164
|
+
- lib/phronomy/agent/llm_input_build_context.rb
|
|
165
|
+
- lib/phronomy/agent/llm_input_manifest.rb
|
|
166
|
+
- lib/phronomy/agent/llm_input_patch.rb
|
|
140
167
|
- lib/phronomy/agent/llm_operation_result.rb
|
|
141
168
|
- lib/phronomy/agent/phase_machine_builder.rb
|
|
169
|
+
- lib/phronomy/agent/provider_call_outcome.rb
|
|
170
|
+
- lib/phronomy/agent/ruby_llm_materializer.rb
|
|
142
171
|
- lib/phronomy/agent/runner.rb
|
|
143
172
|
- lib/phronomy/agent/shared_state.rb
|
|
173
|
+
- lib/phronomy/agent/token_budget_resolver.rb
|
|
144
174
|
- lib/phronomy/agent/tool_approval_request.rb
|
|
145
175
|
- lib/phronomy/agent/tool_call_intercepted.rb
|
|
176
|
+
- lib/phronomy/agent/tool_definition_set.rb
|
|
146
177
|
- lib/phronomy/agent/tool_executor.rb
|
|
147
178
|
- lib/phronomy/agent/tool_invocation.rb
|
|
148
179
|
- lib/phronomy/agent/tool_invocation_session_builder.rb
|
|
180
|
+
- lib/phronomy/agent_busy_error.rb
|
|
181
|
+
- lib/phronomy/canonical_json.rb
|
|
149
182
|
- lib/phronomy/configuration.rb
|
|
183
|
+
- lib/phronomy/content_store/base.rb
|
|
184
|
+
- lib/phronomy/context_budget_exceeded_error.rb
|
|
150
185
|
- lib/phronomy/diagnostics.rb
|
|
151
186
|
- lib/phronomy/engine/concurrency/async_queue.rb
|
|
152
187
|
- lib/phronomy/engine/concurrency/blocking_adapter_pool.rb
|
|
@@ -188,6 +223,7 @@ files:
|
|
|
188
223
|
- lib/phronomy/eval/scorer/includes_scorer.rb
|
|
189
224
|
- lib/phronomy/eval/scorer/llm_judge.rb
|
|
190
225
|
- lib/phronomy/event.rb
|
|
226
|
+
- lib/phronomy/execution_rehydration_required_error.rb
|
|
191
227
|
- lib/phronomy/filter.rb
|
|
192
228
|
- lib/phronomy/filter/base.rb
|
|
193
229
|
- lib/phronomy/filter/prompt_injection_filter.rb
|
|
@@ -195,13 +231,11 @@ files:
|
|
|
195
231
|
- lib/phronomy/invalid_async_entry_action_error.rb
|
|
196
232
|
- lib/phronomy/invalid_async_transition_action_error.rb
|
|
197
233
|
- lib/phronomy/invalid_async_workflow_action_error.rb
|
|
234
|
+
- lib/phronomy/invalid_context_budget_configuration_error.rb
|
|
198
235
|
- lib/phronomy/invocation_context.rb
|
|
199
|
-
- lib/phronomy/knowledge_source.rb
|
|
200
236
|
- lib/phronomy/llm_adapter.rb
|
|
201
237
|
- lib/phronomy/llm_adapter/base.rb
|
|
202
238
|
- lib/phronomy/llm_adapter/ruby_llm.rb
|
|
203
|
-
- lib/phronomy/llm_context_window/assembler.rb
|
|
204
|
-
- lib/phronomy/llm_context_window/context_version_cache.rb
|
|
205
239
|
- lib/phronomy/llm_context_window/token_budget.rb
|
|
206
240
|
- lib/phronomy/llm_context_window/token_estimator.rb
|
|
207
241
|
- lib/phronomy/metrics.rb
|
|
@@ -213,6 +247,8 @@ files:
|
|
|
213
247
|
- lib/phronomy/output_parser/base.rb
|
|
214
248
|
- lib/phronomy/output_parser/json_parser.rb
|
|
215
249
|
- lib/phronomy/output_parser/structured_parser.rb
|
|
250
|
+
- lib/phronomy/persistence.rb
|
|
251
|
+
- lib/phronomy/persistence/in_memory.rb
|
|
216
252
|
- lib/phronomy/ruby_llm_patches.rb
|
|
217
253
|
- lib/phronomy/runnable.rb
|
|
218
254
|
- lib/phronomy/state_store/base.rb
|
|
@@ -251,11 +287,16 @@ files:
|
|
|
251
287
|
- lib/phronomy/workflow/phase_machine_builder.rb
|
|
252
288
|
- lib/phronomy/workflow_context.rb
|
|
253
289
|
- lib/phronomy/workflow_runner.rb
|
|
290
|
+
- scripts/add_to_h_to_token_doubles.rb
|
|
291
|
+
- scripts/add_to_h_unnamed_doubles.rb
|
|
254
292
|
- scripts/api_snapshot.rb
|
|
255
293
|
- scripts/check_api_annotations.rb
|
|
256
294
|
- scripts/check_private_enforcement.rb
|
|
257
295
|
- scripts/check_readme_ruby.rb
|
|
258
296
|
- scripts/check_readme_runnable.rb
|
|
297
|
+
- scripts/migrate_spec_agent_definition.rb
|
|
298
|
+
- scripts/migrate_spec_agent_definition_pass2.rb
|
|
299
|
+
- scripts/migrate_spec_inline_pass3.rb
|
|
259
300
|
- scripts/run_mutation.sh
|
|
260
301
|
- sig/phronomy.rbs
|
|
261
302
|
homepage: https://github.com/Raizo-TCS/phronomy
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
# In-process registry for suspended AgentInvocation aggregates.
|
|
6
|
-
#
|
|
7
|
-
# The registry is the single in-process source for pending Human approval.
|
|
8
|
-
# Cross-process persistence remains outside the scope of this implementation.
|
|
9
|
-
#
|
|
10
|
-
# @api private
|
|
11
|
-
module AgentInvocationRegistry
|
|
12
|
-
Entry = Struct.new(:invocation, :approval_request)
|
|
13
|
-
|
|
14
|
-
@entries = {}
|
|
15
|
-
@approval_index = {}
|
|
16
|
-
@mutex = Mutex.new
|
|
17
|
-
|
|
18
|
-
def self.store_suspended(invocation, approval_request)
|
|
19
|
-
@mutex.synchronize do
|
|
20
|
-
invocation_id = invocation.id
|
|
21
|
-
request_id = approval_request.id
|
|
22
|
-
if @entries.key?(invocation_id) || @approval_index.key?(request_id)
|
|
23
|
-
raise Phronomy::Error,
|
|
24
|
-
"Suspended AgentInvocation is already registered: #{invocation_id}"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
@entries[invocation_id] = Entry.new(
|
|
28
|
-
invocation: invocation,
|
|
29
|
-
approval_request: approval_request
|
|
30
|
-
)
|
|
31
|
-
@approval_index[request_id] = invocation_id
|
|
32
|
-
end
|
|
33
|
-
approval_request
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Atomically removes and returns one pending approval aggregate.
|
|
37
|
-
# Duplicate approval commands therefore cannot execute the Tool twice.
|
|
38
|
-
def self.consume_approval(agent_invocation_id, approval_request_id)
|
|
39
|
-
@mutex.synchronize do
|
|
40
|
-
indexed_invocation_id = @approval_index[approval_request_id.to_s]
|
|
41
|
-
return nil unless indexed_invocation_id == agent_invocation_id.to_s
|
|
42
|
-
|
|
43
|
-
entry = @entries.delete(indexed_invocation_id)
|
|
44
|
-
return nil unless entry
|
|
45
|
-
|
|
46
|
-
@approval_index.delete(entry.approval_request.id)
|
|
47
|
-
entry
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def self.lookup(agent_invocation_id)
|
|
52
|
-
@mutex.synchronize { @entries[agent_invocation_id.to_s] }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def self.exists?(agent_invocation_id)
|
|
56
|
-
@mutex.synchronize { @entries.key?(agent_invocation_id.to_s) }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def self.remove_terminal(agent_invocation_id)
|
|
60
|
-
@mutex.synchronize do
|
|
61
|
-
entry = @entries.delete(agent_invocation_id.to_s)
|
|
62
|
-
@approval_index.delete(entry.approval_request.id) if entry
|
|
63
|
-
entry
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def self.clear!
|
|
68
|
-
@mutex.synchronize do
|
|
69
|
-
@entries.clear
|
|
70
|
-
@approval_index.clear
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
# Passed to every before_completion hook callable.
|
|
6
|
-
# Provides read access to the agent, assembled messages, and invocation config.
|
|
7
|
-
# Hooks may inspect these and return a Hash of params to merge into the LLM request.
|
|
8
|
-
#
|
|
9
|
-
# @example Reading context inside a hook
|
|
10
|
-
# Phronomy.configure do |cfg|
|
|
11
|
-
# cfg.before_completion = lambda do |ctx|
|
|
12
|
-
# Rails.logger.info "LLM request: model=#{ctx.params[:model]}"
|
|
13
|
-
# { temperature: 0.2 }
|
|
14
|
-
# end
|
|
15
|
-
# end
|
|
16
|
-
class BeforeCompletionContext
|
|
17
|
-
# The agent instance making the LLM call.
|
|
18
|
-
# @return [Phronomy::Agent::Base]
|
|
19
|
-
attr_reader :agent
|
|
20
|
-
|
|
21
|
-
# Messages currently assembled for this invocation (read-only snapshot).
|
|
22
|
-
# @return [Array]
|
|
23
|
-
attr_reader :messages
|
|
24
|
-
|
|
25
|
-
# Runtime config hash passed to invoke/stream (e.g. thread_id, memory).
|
|
26
|
-
# @return [Hash]
|
|
27
|
-
attr_reader :config
|
|
28
|
-
|
|
29
|
-
# Current LLM params being built (model, temperature, etc.).
|
|
30
|
-
# Read-only; return a Hash from the hook to merge overrides.
|
|
31
|
-
# @return [Hash]
|
|
32
|
-
attr_reader :params
|
|
33
|
-
|
|
34
|
-
# @param agent [Phronomy::Agent::Base]
|
|
35
|
-
# @param messages [Array]
|
|
36
|
-
# @param config [Hash]
|
|
37
|
-
# @param params [Hash] initial params (model, temperature already set on chat)
|
|
38
|
-
# @api public
|
|
39
|
-
def initialize(agent:, messages:, config:, params: {})
|
|
40
|
-
@agent = agent
|
|
41
|
-
@messages = messages.dup.freeze
|
|
42
|
-
@config = config
|
|
43
|
-
@params = params.dup.freeze
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
module Concerns
|
|
6
|
-
# Adds before_completion hook support to an agent.
|
|
7
|
-
#
|
|
8
|
-
# Included in {Phronomy::Agent::Base}. Hooks are executed just before every
|
|
9
|
-
# LLM call (global → class → instance order) and may inject or override
|
|
10
|
-
# LLM parameters such as temperature or model.
|
|
11
|
-
# @api private
|
|
12
|
-
module BeforeCompletion
|
|
13
|
-
def self.included(base)
|
|
14
|
-
base.extend(ClassMethods)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Class-level DSL methods mixed into the including agent class.
|
|
18
|
-
module ClassMethods
|
|
19
|
-
# Sets or reads the class-level before_completion hook.
|
|
20
|
-
# The hook is called before every LLM request for instances of this class.
|
|
21
|
-
# Receives a {Phronomy::Agent::BeforeCompletionContext}; must return a Hash
|
|
22
|
-
# of params to merge into the LLM call, or nil to pass through unchanged.
|
|
23
|
-
#
|
|
24
|
-
# @param callable [#call, nil] lambda/proc to register, or nil to clear
|
|
25
|
-
# @return [#call, nil]
|
|
26
|
-
# @example
|
|
27
|
-
# class MyAgent < Phronomy::Agent::Base
|
|
28
|
-
# before_completion ->(ctx) { { temperature: 0.2 } }
|
|
29
|
-
# end
|
|
30
|
-
# @api private
|
|
31
|
-
def before_completion(callable = nil)
|
|
32
|
-
if callable.nil? && !block_given?
|
|
33
|
-
@before_completion
|
|
34
|
-
else
|
|
35
|
-
@before_completion = callable
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# @return [#call, nil]
|
|
40
|
-
# @api private
|
|
41
|
-
def _before_completion
|
|
42
|
-
@before_completion
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Instance-level before_completion hook. When set, takes precedence over
|
|
47
|
-
# the class-level hook for this specific agent instance only.
|
|
48
|
-
# @return [#call, nil]
|
|
49
|
-
attr_accessor :before_completion
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
# Collects and runs all registered before_completion hooks in order
|
|
54
|
-
# (global → class → instance) and applies the merged params to the chat.
|
|
55
|
-
#
|
|
56
|
-
# @param chat [RubyLLM::Chat] the assembled chat object
|
|
57
|
-
# @param config [Hash] the invocation config hash
|
|
58
|
-
# @return [Hash] the merged params applied to the chat
|
|
59
|
-
# @api private
|
|
60
|
-
def run_before_completion_hooks!(chat, config)
|
|
61
|
-
hooks = [
|
|
62
|
-
Phronomy.configuration.before_completion,
|
|
63
|
-
self.class._before_completion,
|
|
64
|
-
@before_completion
|
|
65
|
-
].compact
|
|
66
|
-
|
|
67
|
-
return {} if hooks.empty?
|
|
68
|
-
|
|
69
|
-
ctx = BeforeCompletionContext.new(
|
|
70
|
-
agent: self,
|
|
71
|
-
messages: chat.messages,
|
|
72
|
-
config: config,
|
|
73
|
-
params: {}
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
merged = {}
|
|
77
|
-
hooks.each do |hook|
|
|
78
|
-
result = hook.call(ctx)
|
|
79
|
-
check_cancellation!(config, "invocation cancelled during before_completion hook")
|
|
80
|
-
merged.merge!(result) if result.is_a?(Hash)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
apply_before_completion_params!(chat, merged)
|
|
84
|
-
merged
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Applies a merged param hash returned by before_completion hooks to
|
|
88
|
-
# the chat object using the appropriate RubyLLM::Chat API methods.
|
|
89
|
-
# When overriding the model, reuses the agent's configured provider and
|
|
90
|
-
# assume_exists setting so that local/namespaced models continue to work.
|
|
91
|
-
#
|
|
92
|
-
# @param chat [RubyLLM::Chat]
|
|
93
|
-
# @param params [Hash]
|
|
94
|
-
# @api private
|
|
95
|
-
def apply_before_completion_params!(chat, params)
|
|
96
|
-
params.each do |key, value|
|
|
97
|
-
case key
|
|
98
|
-
when :model
|
|
99
|
-
prov = self.class.provider
|
|
100
|
-
chat.with_model(value, provider: prov, assume_exists: !prov.nil?)
|
|
101
|
-
when :temperature
|
|
102
|
-
chat.with_temperature(value)
|
|
103
|
-
else
|
|
104
|
-
chat.with_params(key => value)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
module Context
|
|
6
|
-
module Knowledge
|
|
7
|
-
# Abstract base class for all KnowledgeSource implementations.
|
|
8
|
-
#
|
|
9
|
-
# Subclasses must implement #fetch(query:) and return an Array of chunk Hashes.
|
|
10
|
-
# Each chunk Hash must contain:
|
|
11
|
-
# :content [String] the text to inject into the context
|
|
12
|
-
# :type [Symbol] semantic tag (e.g. :static, :rag, :entity)
|
|
13
|
-
class Base
|
|
14
|
-
# Retrieve knowledge chunks relevant to the given query.
|
|
15
|
-
#
|
|
16
|
-
# @param query [String, nil] the current user input used to select relevant chunks
|
|
17
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil] optional token; raises CancellationError when cancelled
|
|
18
|
-
# @return [Array<Hash>] array of { content: String, type: Symbol }
|
|
19
|
-
# @api public
|
|
20
|
-
def fetch(query: nil, cancellation_token: nil)
|
|
21
|
-
cancellation_token&.raise_if_cancelled!
|
|
22
|
-
raise NotImplementedError, "#{self.class}#fetch is not implemented"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Submits a {#fetch} call to {BlockingAdapterPool} and returns a
|
|
26
|
-
# {BlockingAdapterPool::PendingOperation}.
|
|
27
|
-
# Callers can fan out multiple fetches in parallel and await them all.
|
|
28
|
-
#
|
|
29
|
-
# @param query [String, nil]
|
|
30
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil]
|
|
31
|
-
# @param timeout [Numeric, nil] seconds before the operation is abandoned
|
|
32
|
-
# @return [BlockingAdapterPool::PendingOperation]
|
|
33
|
-
# @api public
|
|
34
|
-
def fetch_async(query: nil, cancellation_token: nil, timeout: nil)
|
|
35
|
-
Phronomy::Runtime.instance.blocking_io.submit(
|
|
36
|
-
timeout: timeout,
|
|
37
|
-
cancellation_token: cancellation_token
|
|
38
|
-
) do
|
|
39
|
-
fetch(query: query, cancellation_token: cancellation_token)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Returns true when this source's content is considered static (i.e. does
|
|
44
|
-
# not change between agent invocations). Static sources are eligible for
|
|
45
|
-
# fingerprint-based caching in ContextVersionCache.
|
|
46
|
-
#
|
|
47
|
-
# Override in subclasses that return fixed content.
|
|
48
|
-
#
|
|
49
|
-
# @return [Boolean]
|
|
50
|
-
# @api public
|
|
51
|
-
def static?
|
|
52
|
-
false
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
module Context
|
|
6
|
-
module Knowledge
|
|
7
|
-
# A KnowledgeSource that extracts named-entity facts from conversation history.
|
|
8
|
-
#
|
|
9
|
-
# This is the knowledge-injection counterpart of the old EntityMemory.
|
|
10
|
-
# It scans saved user messages with a regex heuristic (no LLM call) and
|
|
11
|
-
# returns the discovered facts as a single knowledge chunk tagged :entity.
|
|
12
|
-
#
|
|
13
|
-
# EntityKnowledge is stateful: it accumulates extracted facts via #update(messages:)
|
|
14
|
-
# which should be called each time new messages are saved.
|
|
15
|
-
#
|
|
16
|
-
# Supported extraction patterns (case-insensitive):
|
|
17
|
-
# "my name is Alice" → { name: "Alice" }
|
|
18
|
-
# "I am Alice" → { identity: "Alice" }
|
|
19
|
-
# "I'm a software engineer" → { occupation: "software engineer" }
|
|
20
|
-
# "I work at / for Acme" → { workplace: "Acme" }
|
|
21
|
-
# "I live in Tokyo" → { location: "Tokyo" }
|
|
22
|
-
# "I'm from Tokyo" → { location: "Tokyo" }
|
|
23
|
-
# "I like / love Ruby" → { preference: "Ruby" }
|
|
24
|
-
#
|
|
25
|
-
# @example
|
|
26
|
-
# ks = Phronomy::Agent::Context::Knowledge::EntityKnowledge.new
|
|
27
|
-
# ks.update(messages: chat_messages)
|
|
28
|
-
# agent = MyAgent.new
|
|
29
|
-
# agent.add_knowledge_source(ks)
|
|
30
|
-
# agent.invoke("What is my name?")
|
|
31
|
-
class EntityKnowledge < Base
|
|
32
|
-
PATTERNS = [
|
|
33
|
-
[:name, /\bmy name is\s+([A-Za-z][A-Za-z0-9 \-']*)/i],
|
|
34
|
-
[:identity, /\bI\s+am\s+([A-Z][A-Za-z0-9 \-']+)/],
|
|
35
|
-
[:occupation, /\bI(?:'m| am) a(?:n)?\s+([A-Za-z][A-Za-z0-9 \-']*)/i],
|
|
36
|
-
[:workplace, /\bI (?:work|worked) (?:at|for|in)\s+([A-Za-z0-9][A-Za-z0-9 \-'.&,]*)/i],
|
|
37
|
-
[:location, /\bI live in\s+([A-Za-z][A-Za-z0-9 \-']*)/i],
|
|
38
|
-
[:location, /\bI(?:'m| am) from\s+([A-Za-z][A-Za-z0-9 \-']*)/i],
|
|
39
|
-
[:preference, /\bI (?:like|love|enjoy)\s+([A-Za-z][A-Za-z0-9 \-']*)/i]
|
|
40
|
-
].freeze
|
|
41
|
-
|
|
42
|
-
def initialize
|
|
43
|
-
@entities = {}
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Scan messages and accumulate entity facts.
|
|
47
|
-
# Call this after saving a new set of messages (e.g. from a ConversationManager save hook).
|
|
48
|
-
#
|
|
49
|
-
# @param messages [Array] message objects responding to #role and #content
|
|
50
|
-
# @api public
|
|
51
|
-
def update(messages:)
|
|
52
|
-
messages.each do |msg|
|
|
53
|
-
next unless msg.role.to_sym == :user
|
|
54
|
-
|
|
55
|
-
extract(msg.content.to_s).each { |key, value| @entities[key] = value }
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Returns a single chunk containing all known entity facts in XML context format.
|
|
60
|
-
# Returns an empty array when no entities have been discovered.
|
|
61
|
-
#
|
|
62
|
-
# @param query [String, nil] unused — entity knowledge is always fully injected
|
|
63
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil] optional; raises CancellationError when cancelled
|
|
64
|
-
# @return [Array<Hash>]
|
|
65
|
-
# @api public
|
|
66
|
-
def fetch(query: nil, cancellation_token: nil)
|
|
67
|
-
cancellation_token&.raise_if_cancelled!
|
|
68
|
-
return [] if @entities.empty?
|
|
69
|
-
|
|
70
|
-
lines = @entities.map { |key, value| "- #{key}: #{value}" }.join("\n")
|
|
71
|
-
content = <<~CONTENT.chomp
|
|
72
|
-
Known facts about the user:
|
|
73
|
-
#{lines}
|
|
74
|
-
CONTENT
|
|
75
|
-
[{content: content, type: :entity}]
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Returns the current entity store (primarily for testing).
|
|
79
|
-
#
|
|
80
|
-
# @return [Hash]
|
|
81
|
-
# @api public
|
|
82
|
-
def entities
|
|
83
|
-
@entities.dup
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
def extract(text)
|
|
89
|
-
found = {}
|
|
90
|
-
PATTERNS.each do |key, pattern|
|
|
91
|
-
if (match = text.match(pattern))
|
|
92
|
-
value = match[1].strip.sub(/[.!?]\s+.*$/, "").gsub(/[.,;!?]+$/, "")
|
|
93
|
-
found[key] = value unless value.empty?
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
found
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
module Context
|
|
6
|
-
module Knowledge
|
|
7
|
-
# A KnowledgeSource backed by fixed text provided at construction time.
|
|
8
|
-
#
|
|
9
|
-
# Useful for injecting static documents, policy files, or configuration
|
|
10
|
-
# knowledge that does not change per request.
|
|
11
|
-
#
|
|
12
|
-
# @example
|
|
13
|
-
# ks = Phronomy::Agent::Context::Knowledge::StaticKnowledge.new(
|
|
14
|
-
# "Our refund policy: ...",
|
|
15
|
-
# type: :policy
|
|
16
|
-
# )
|
|
17
|
-
# agent = MyAgent.new
|
|
18
|
-
# agent.add_knowledge_source(ks)
|
|
19
|
-
# agent.invoke("What is the refund policy?")
|
|
20
|
-
class StaticKnowledge < Base
|
|
21
|
-
# @param text [String] the static knowledge text to inject
|
|
22
|
-
# @param type [Symbol] semantic tag for the chunk (default :static)
|
|
23
|
-
# @param source [String, nil] label identifying where this knowledge came from
|
|
24
|
-
# (e.g. a filename). Included in the context XML tag and exposed to the LLM
|
|
25
|
-
# so that agents can produce grounded citations.
|
|
26
|
-
# @api public
|
|
27
|
-
def initialize(text, type: :static, source: nil)
|
|
28
|
-
@text = text.to_s
|
|
29
|
-
@type = type
|
|
30
|
-
@source = source
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Returns the fixed text as a single chunk, regardless of query.
|
|
34
|
-
#
|
|
35
|
-
# @param query [String, nil] ignored for static knowledge
|
|
36
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil] optional; raises CancellationError when cancelled
|
|
37
|
-
# @return [Array<Hash>]
|
|
38
|
-
# @api public
|
|
39
|
-
def fetch(query: nil, cancellation_token: nil)
|
|
40
|
-
cancellation_token&.raise_if_cancelled!
|
|
41
|
-
return [] if @text.empty?
|
|
42
|
-
|
|
43
|
-
chunk = {content: @text, type: @type}
|
|
44
|
-
chunk[:source] = @source if @source
|
|
45
|
-
[chunk]
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Static knowledge content never changes between invocations.
|
|
49
|
-
# @return [true]
|
|
50
|
-
# @api public
|
|
51
|
-
def static?
|
|
52
|
-
true
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
# KnowledgeSource provides the interface for supplying context region 3 (Knowledge)
|
|
5
|
-
# to the Context::Assembler.
|
|
6
|
-
#
|
|
7
|
-
# Each implementation returns an array of knowledge chunks via #fetch(query:).
|
|
8
|
-
# Each chunk is a Hash with :content (String) and :type (Symbol) keys.
|
|
9
|
-
# The Assembler wraps each chunk in an XML context tag before injecting it.
|
|
10
|
-
module KnowledgeSource
|
|
11
|
-
end
|
|
12
|
-
end
|