phronomy 0.7.0 → 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/.mutant.yml +8 -7
- data/CHANGELOG.md +151 -1
- data/README.md +170 -47
- data/Rakefile +33 -0
- data/benchmark/baseline.json +1 -1
- data/benchmark/bench_context_assembler.rb +2 -2
- data/benchmark/bench_regression.rb +6 -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/docs/decisions/004-invoke-timeout-is-not-cancellation.md +24 -0
- data/docs/decisions/006-no-built-in-guardrails.md +20 -2
- data/docs/decisions/010-cooperative-first-concurrency.md +248 -0
- data/lib/phronomy/agent/base.rb +285 -137
- data/lib/phronomy/agent/checkpoint.rb +118 -0
- data/lib/phronomy/agent/concerns/suspendable.rb +15 -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 +42 -65
- 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 +27 -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/concurrency/gate_registry.rb +52 -0
- data/lib/phronomy/concurrency/pool_registry.rb +57 -0
- data/lib/phronomy/configuration.rb +142 -0
- data/lib/phronomy/context.rb +2 -8
- data/lib/phronomy/diagnostics.rb +62 -0
- data/lib/phronomy/embeddings.rb +2 -2
- data/lib/phronomy/eval/runner.rb +13 -9
- data/lib/phronomy/eval/scorer/llm_judge.rb +12 -1
- data/lib/phronomy/event_loop.rb +184 -46
- data/lib/phronomy/guardrail/prompt_injection_guardrail.rb +58 -0
- data/lib/phronomy/invocation_context.rb +152 -0
- data/lib/phronomy/knowledge_source.rb +0 -5
- data/lib/phronomy/llm_adapter/base.rb +104 -0
- data/lib/phronomy/llm_adapter/ruby_llm.rb +47 -0
- data/lib/phronomy/llm_adapter.rb +20 -0
- 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/metrics.rb +38 -0
- data/lib/phronomy/{agent → multi_agent}/handoff.rb +2 -2
- data/lib/phronomy/{agent → multi_agent}/orchestrator.rb +151 -126
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +149 -0
- data/lib/phronomy/{agent → multi_agent}/team_coordinator.rb +2 -2
- data/lib/phronomy/runtime/deterministic_scheduler.rb +412 -0
- data/lib/phronomy/runtime/fake_scheduler.rb +165 -0
- data/lib/phronomy/runtime/runtime_metrics.rb +117 -0
- data/lib/phronomy/runtime/scheduler.rb +98 -0
- data/lib/phronomy/runtime/scheduler_timer_adapter.rb +79 -0
- data/lib/phronomy/runtime/task_registry.rb +48 -0
- data/lib/phronomy/runtime/thread_scheduler.rb +30 -0
- data/lib/phronomy/runtime/timer_queue.rb +106 -0
- data/lib/phronomy/runtime/timer_service.rb +42 -0
- data/lib/phronomy/runtime.rb +389 -0
- data/lib/phronomy/splitter.rb +3 -3
- data/lib/phronomy/task/backend.rb +80 -0
- data/lib/phronomy/task/fiber_backend.rb +157 -0
- data/lib/phronomy/task/immediate_backend.rb +89 -0
- data/lib/phronomy/task/thread_backend.rb +84 -0
- data/lib/phronomy/task.rb +275 -0
- data/lib/phronomy/task_group.rb +265 -0
- data/lib/phronomy/testing/fake_clock.rb +109 -0
- data/lib/phronomy/testing/fake_scheduler.rb +104 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +59 -0
- data/lib/phronomy/testing.rb +12 -0
- data/lib/phronomy/tool/base.rb +156 -7
- data/lib/phronomy/tool/mcp_tool.rb +47 -16
- data/lib/phronomy/tool/scope_policy.rb +50 -0
- data/lib/phronomy/tracing/null_tracer.rb +3 -1
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +34 -0
- data/lib/phronomy/vector_store.rb +2 -2
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +52 -5
- data/lib/phronomy/workflow_context.rb +37 -2
- data/lib/phronomy/workflow_runner.rb +28 -77
- data/lib/phronomy.rb +43 -0
- metadata +73 -33
- data/lib/phronomy/agent/parallel_tool_chat.rb +0 -92
- data/lib/phronomy/cancellation_token.rb +0 -92
- 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/embeddings/base.rb +0 -22
- data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +0 -45
- data/lib/phronomy/fsm_session.rb +0 -201
- data/lib/phronomy/knowledge_source/base.rb +0 -36
- 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/vector_store/base.rb +0 -82
- 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,192 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module Phronomy
|
|
6
|
-
module VectorStore
|
|
7
|
-
# Redis-backed vector store using the RediSearch module (FT.* commands).
|
|
8
|
-
#
|
|
9
|
-
# Requires:
|
|
10
|
-
# - The +redis+ gem (add to your Gemfile)
|
|
11
|
-
# - A Redis server with the RediSearch (RedisSearch) module enabled
|
|
12
|
-
# (or Redis Stack which bundles RediSearch)
|
|
13
|
-
#
|
|
14
|
-
# Vectors are stored as FLOAT32 binary blobs in Redis Hash fields and
|
|
15
|
-
# searched using the KNN approximate-nearest-neighbour algorithm.
|
|
16
|
-
#
|
|
17
|
-
# @example Usage
|
|
18
|
-
# redis = Redis.new(url: "redis://localhost:6379")
|
|
19
|
-
# store = Phronomy::VectorStore::RedisSearch.new(redis: redis, dimension: 1536)
|
|
20
|
-
# store.add(id: "doc1", embedding: [0.1, 0.9], metadata: {text: "hello"})
|
|
21
|
-
# results = store.search(query_embedding: [0.1, 0.8], k: 5)
|
|
22
|
-
class RedisSearch < Base
|
|
23
|
-
DOC_PREFIX = "phronomy_doc:"
|
|
24
|
-
private_constant :DOC_PREFIX
|
|
25
|
-
|
|
26
|
-
# @param redis [Redis] configured Redis client
|
|
27
|
-
# @param index_name [String] RediSearch index name
|
|
28
|
-
# @param dimension [Integer, nil] vector dimension; auto-detected on first add.
|
|
29
|
-
# When connecting to an **existing** RediSearch index, you MUST pass
|
|
30
|
-
# dimension: explicitly. Without it, a freshly constructed instance
|
|
31
|
-
# treats the index as uninitialized until #add is called, and #search
|
|
32
|
-
# silently returns [] in the meantime.
|
|
33
|
-
# @api public
|
|
34
|
-
def initialize(redis:, index_name: "phronomy_vectors", dimension: nil)
|
|
35
|
-
begin
|
|
36
|
-
require "redis"
|
|
37
|
-
rescue LoadError
|
|
38
|
-
raise LoadError,
|
|
39
|
-
"redis gem is required for Phronomy::VectorStore::RedisSearch. " \
|
|
40
|
-
"Add `gem 'redis'` to your Gemfile."
|
|
41
|
-
end
|
|
42
|
-
@redis = redis
|
|
43
|
-
@index_name = index_name
|
|
44
|
-
@dimension = dimension
|
|
45
|
-
@index_created = false
|
|
46
|
-
@mutex = Mutex.new
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# @param id [String]
|
|
50
|
-
# @param embedding [Array<Float>]
|
|
51
|
-
# @param metadata [Hash]
|
|
52
|
-
# @param cancellation_token [Phronomy::CancellationToken, nil]
|
|
53
|
-
# @api public
|
|
54
|
-
def add(id:, embedding:, metadata: {}, cancellation_token: nil)
|
|
55
|
-
cancellation_token&.raise_if_cancelled!
|
|
56
|
-
# Establish expected dimension on first add (not race-free for concurrent
|
|
57
|
-
# first adds), then validate, then create/reuse the index.
|
|
58
|
-
@dimension ||= embedding.size
|
|
59
|
-
validate_embedding_dimension!(embedding, @dimension)
|
|
60
|
-
ensure_index!(@dimension)
|
|
61
|
-
@redis.call(
|
|
62
|
-
"HSET", "#{DOC_PREFIX}#{id}",
|
|
63
|
-
"embedding", pack_vector(embedding),
|
|
64
|
-
"metadata", metadata.to_json
|
|
65
|
-
)
|
|
66
|
-
self
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# @param query_embedding [Array<Float>]
|
|
70
|
-
# @param k [Integer]
|
|
71
|
-
# @param cancellation_token [Phronomy::CancellationToken, nil]
|
|
72
|
-
# @return [Array<Hash>] sorted by descending similarity score
|
|
73
|
-
# @api public
|
|
74
|
-
def search(query_embedding:, k: 5, cancellation_token: nil)
|
|
75
|
-
cancellation_token&.raise_if_cancelled!
|
|
76
|
-
# search never establishes dimension. If dimension is unknown and the
|
|
77
|
-
# index has not been created yet, there are no documents to return.
|
|
78
|
-
return [] if @dimension.nil? && !@index_created
|
|
79
|
-
|
|
80
|
-
validate_embedding_dimension!(query_embedding, @dimension)
|
|
81
|
-
ensure_index!(@dimension)
|
|
82
|
-
k_safe = validate_k!(k)
|
|
83
|
-
blob = pack_vector(query_embedding)
|
|
84
|
-
|
|
85
|
-
raw = @redis.call(
|
|
86
|
-
"FT.SEARCH", @index_name,
|
|
87
|
-
"*=>[KNN #{k_safe} @embedding $BLOB AS score]",
|
|
88
|
-
"PARAMS", 2, "BLOB", blob,
|
|
89
|
-
"SORTBY", "score",
|
|
90
|
-
"RETURN", 2, "score", "metadata",
|
|
91
|
-
"DIALECT", 2
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
parse_results(raw)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def remove(id:)
|
|
98
|
-
@redis.call("DEL", "#{DOC_PREFIX}#{id}")
|
|
99
|
-
self
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Returns the number of documents indexed.
|
|
103
|
-
# Queries FT.INFO when the index has been created; returns 0 otherwise.
|
|
104
|
-
def size
|
|
105
|
-
return 0 unless @index_created
|
|
106
|
-
|
|
107
|
-
raw = @redis.call("FT.INFO", @index_name)
|
|
108
|
-
return 0 unless raw.is_a?(Array)
|
|
109
|
-
|
|
110
|
-
idx = raw.index("num_docs")
|
|
111
|
-
idx ? raw[idx + 1].to_i : 0
|
|
112
|
-
rescue
|
|
113
|
-
0
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def clear
|
|
117
|
-
@mutex.synchronize do
|
|
118
|
-
begin
|
|
119
|
-
@redis.call("FT.DROPINDEX", @index_name, "DD")
|
|
120
|
-
rescue => e
|
|
121
|
-
raise unless e.message.to_s.include?("Unknown Index name")
|
|
122
|
-
end
|
|
123
|
-
@index_created = false
|
|
124
|
-
end
|
|
125
|
-
self
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
private
|
|
129
|
-
|
|
130
|
-
def ensure_index!(dim)
|
|
131
|
-
@mutex.synchronize do
|
|
132
|
-
return if @index_created
|
|
133
|
-
|
|
134
|
-
@dimension ||= dim
|
|
135
|
-
begin
|
|
136
|
-
@redis.call(
|
|
137
|
-
"FT.CREATE", @index_name,
|
|
138
|
-
"ON", "HASH",
|
|
139
|
-
"PREFIX", 1, DOC_PREFIX,
|
|
140
|
-
"SCHEMA",
|
|
141
|
-
"embedding", "VECTOR", "FLAT", 6,
|
|
142
|
-
"TYPE", "FLOAT32",
|
|
143
|
-
"DIM", @dimension,
|
|
144
|
-
"DISTANCE_METRIC", "COSINE",
|
|
145
|
-
"metadata", "TEXT"
|
|
146
|
-
)
|
|
147
|
-
rescue => e
|
|
148
|
-
raise unless e.message.to_s.include?("Index already exists")
|
|
149
|
-
end
|
|
150
|
-
@index_created = true
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Pack a Float array as a FLOAT32 binary string for RediSearch.
|
|
155
|
-
def pack_vector(embedding)
|
|
156
|
-
embedding.map { |v| Float(v) }.pack("f*")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# Parse the raw FT.SEARCH response into the standard Hash format.
|
|
160
|
-
#
|
|
161
|
-
# Redis FT.SEARCH returns: [count, key1, [field, value, ...], key2, ...]
|
|
162
|
-
def parse_results(raw)
|
|
163
|
-
return [] if raw.nil? || !raw.is_a?(Array) || raw.size < 2
|
|
164
|
-
|
|
165
|
-
results = []
|
|
166
|
-
i = 1
|
|
167
|
-
while i < raw.size
|
|
168
|
-
key = raw[i]
|
|
169
|
-
fields = raw[i + 1]
|
|
170
|
-
i += 2
|
|
171
|
-
|
|
172
|
-
next unless fields.is_a?(Array)
|
|
173
|
-
|
|
174
|
-
field_hash = fields.each_slice(2).to_h
|
|
175
|
-
score_str = field_hash["score"]
|
|
176
|
-
metadata_str = field_hash["metadata"]
|
|
177
|
-
|
|
178
|
-
next if score_str.nil?
|
|
179
|
-
|
|
180
|
-
id = key.to_s.delete_prefix(DOC_PREFIX)
|
|
181
|
-
# RediSearch returns cosine distance (0=identical, 2=opposite);
|
|
182
|
-
# convert to cosine similarity for consistency with other backends.
|
|
183
|
-
score = 1.0 - score_str.to_f
|
|
184
|
-
metadata = metadata_str ? JSON.parse(metadata_str, symbolize_names: true) : {}
|
|
185
|
-
|
|
186
|
-
results << {id: id, score: score, metadata: metadata}
|
|
187
|
-
end
|
|
188
|
-
results
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|