active_harness 0.2.13 → 0.2.15
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/lib/active_harness/tribunal/dsl.rb +54 -0
- data/lib/active_harness/tribunal/hooks.rb +87 -0
- data/lib/active_harness/tribunal/processing.rb +44 -0
- data/lib/active_harness/tribunal.rb +60 -103
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc551806a5848e9157c21ee668ee38c139a55c46fb7e8c2a6a2e8022200c90cb
|
|
4
|
+
data.tar.gz: f6110846af88ce73b7fe7a30221d18960a58e80adeaf01e2b19b5e14fac0640d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8884d36a5da2581707584068acc7315a00d417bd88a7776d1702c375e05470257c21cedb6c323fec5aa75562e6f118d635ae1b1a2053d3e7d60b48b0aca8d3f
|
|
7
|
+
data.tar.gz: 02fa25dca8678816e76b2c2b2fad6131e26a7aa8b5a97cb78a5bcee84d7a9923507eb5981d0508663ecced5a6eb85c5f92799a00d80b59098252dc08438ff22f
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module ActiveHarness
|
|
2
|
+
class Tribunal
|
|
3
|
+
class << self
|
|
4
|
+
# Declare agents at the class level.
|
|
5
|
+
#
|
|
6
|
+
# agents PolitenessAgent, ConstructivenessAgent
|
|
7
|
+
# agents [PolitenessAgent, ConstructivenessAgent]
|
|
8
|
+
def agents(*list)
|
|
9
|
+
tribunal_config[:agents] = list.flatten
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Class-level process block — defines how to compute the verdict from all results.
|
|
13
|
+
# Receives the full results array; return value becomes #verdict.
|
|
14
|
+
# Takes priority over +verdict+ strategy if both are declared.
|
|
15
|
+
#
|
|
16
|
+
# process { |results| results.all? { |r| r.parsed["result"] == true } }
|
|
17
|
+
def process(&block)
|
|
18
|
+
tribunal_config[:process] = block
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Declarative verdict — built-in aggregation strategy with a per-result evaluator.
|
|
22
|
+
#
|
|
23
|
+
# Strategies:
|
|
24
|
+
# :unanimous — verdict true when every successful result evaluates to true
|
|
25
|
+
# :majority — verdict true when more than half of successful results evaluate to true
|
|
26
|
+
#
|
|
27
|
+
# Options:
|
|
28
|
+
# may_fail: N — tolerate up to N agent errors before raising AllAgentsFailed
|
|
29
|
+
# (default: nil — raise only when all agents fail, preserving legacy behavior)
|
|
30
|
+
#
|
|
31
|
+
# The block receives a single Result and must return a truthy/falsy value.
|
|
32
|
+
#
|
|
33
|
+
# verdict :unanimous do |result|
|
|
34
|
+
# result.parsed["result"] == true
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# verdict :majority, may_fail: 1 do |result|
|
|
38
|
+
# result.parsed["result"] == true
|
|
39
|
+
# end
|
|
40
|
+
VALID_STRATEGIES = %i[unanimous majority].freeze
|
|
41
|
+
|
|
42
|
+
def verdict(strategy, may_fail: nil, &block)
|
|
43
|
+
unless VALID_STRATEGIES.include?(strategy)
|
|
44
|
+
raise ArgumentError,
|
|
45
|
+
"Unknown verdict strategy :#{strategy}. Valid strategies: #{VALID_STRATEGIES.map { |s| ":#{s}" }.join(", ")}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
tribunal_config[:strategy] = strategy
|
|
49
|
+
tribunal_config[:may_fail] = may_fail
|
|
50
|
+
tribunal_config[:evaluate_block] = block
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module ActiveHarness
|
|
2
|
+
class Tribunal
|
|
3
|
+
VALID_HOOKS = %i[
|
|
4
|
+
before_call
|
|
5
|
+
before_agent
|
|
6
|
+
after_agent
|
|
7
|
+
agent_error
|
|
8
|
+
after_call
|
|
9
|
+
before_verdict
|
|
10
|
+
after_verdict
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Class-level hook registration.
|
|
15
|
+
#
|
|
16
|
+
# on :before_call do ... end
|
|
17
|
+
# on :before_agent do |agent| ... end
|
|
18
|
+
# on :after_agent do |result| ... end
|
|
19
|
+
# on :agent_error do |name, error| ... end
|
|
20
|
+
# on :after_call do |results, errors| ... end
|
|
21
|
+
# on :before_verdict do |results| results end # transform hook
|
|
22
|
+
# on :after_verdict do |verdict| ... end
|
|
23
|
+
def on(event, &block)
|
|
24
|
+
unless VALID_HOOKS.include?(event)
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.map { |h| ":#{h}" }.join(", ")}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
tribunal_config[:hooks][event] = block
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Rails-style aliases for +on+:
|
|
33
|
+
#
|
|
34
|
+
# before :call do ... end # → on :before_call
|
|
35
|
+
# before :agent do |agent| end # → on :before_agent
|
|
36
|
+
# before :verdict do |results| end # → on :before_verdict (transform)
|
|
37
|
+
# after :call do |r, e| end # → on :after_call
|
|
38
|
+
# after :agent do |result| end # → on :after_agent
|
|
39
|
+
# after :verdict do |verdict| end # → on :after_verdict
|
|
40
|
+
# callback :agent_error do |name, e| end # → on :agent_error
|
|
41
|
+
def before(event, &block)
|
|
42
|
+
on(:"before_#{event}", &block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def after(event, &block)
|
|
46
|
+
on(:"after_#{event}", &block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def callback(event, &block)
|
|
50
|
+
on(event, &block)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Instance-level hook registration — overrides class-level hooks for this instance.
|
|
55
|
+
# :before_verdict is a transform hook: its return value replaces the results array
|
|
56
|
+
# passed to the process block.
|
|
57
|
+
def on(event, &block)
|
|
58
|
+
unless VALID_HOOKS.include?(event)
|
|
59
|
+
raise ArgumentError,
|
|
60
|
+
"Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.map { |h| ":#{h}" }.join(", ")}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@hooks[event] = block
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def run_hook(event, *args)
|
|
70
|
+
return unless @hooks[event]
|
|
71
|
+
|
|
72
|
+
if args.any?
|
|
73
|
+
instance_exec(*args, &@hooks[event])
|
|
74
|
+
else
|
|
75
|
+
instance_eval(&@hooks[event])
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Like run_hook but uses the return value to replace the passed value.
|
|
80
|
+
# Used by :before_verdict to allow results transformation before verdict computation.
|
|
81
|
+
def transform_hook(event, value)
|
|
82
|
+
return value unless @hooks[event]
|
|
83
|
+
|
|
84
|
+
instance_exec(value, &@hooks[event])
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module ActiveHarness
|
|
2
|
+
class Tribunal
|
|
3
|
+
# Instance-level process block — overrides class-level block.
|
|
4
|
+
#
|
|
5
|
+
# tribunal.process { |results| results.count { |r| r.parsed["ok"] } >= 2 }
|
|
6
|
+
def process(&block)
|
|
7
|
+
@process_block = block
|
|
8
|
+
self
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Selects and executes the verdict computation in order of priority:
|
|
14
|
+
# 1. Instance-level process block (set via tribunal.process { ... })
|
|
15
|
+
# 2. Class-level process block (set via `process do ... end` in subclass)
|
|
16
|
+
# 3. Class-level strategy (set via `verdict :unanimous/:majority`)
|
|
17
|
+
# 4. nil (no verdict computation declared)
|
|
18
|
+
def compute_verdict(results)
|
|
19
|
+
if @process_block
|
|
20
|
+
@process_block.call(results)
|
|
21
|
+
elsif @strategy
|
|
22
|
+
apply_strategy(@strategy, results)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Built-in aggregation strategies.
|
|
27
|
+
#
|
|
28
|
+
# Without an evaluate_block every successful result is treated as a positive vote.
|
|
29
|
+
# With an evaluate_block the block decides whether each result counts as positive.
|
|
30
|
+
#
|
|
31
|
+
# :unanimous — all positive votes required
|
|
32
|
+
# :majority — more than 50% positive votes required
|
|
33
|
+
def apply_strategy(strategy, results)
|
|
34
|
+
evaluate = @evaluate_block || ->(r) { r }
|
|
35
|
+
votes = results.map { |r| evaluate.call(r) ? true : false }
|
|
36
|
+
positive = votes.count(true)
|
|
37
|
+
|
|
38
|
+
case strategy
|
|
39
|
+
when :unanimous then positive == votes.size
|
|
40
|
+
when :majority then positive > votes.size / 2.0
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -29,62 +29,11 @@ module ActiveHarness
|
|
|
29
29
|
# ContentQualityTribunal.new(input: "...").call
|
|
30
30
|
#
|
|
31
31
|
class Tribunal
|
|
32
|
-
VALID_HOOKS = %i[
|
|
33
|
-
before_call
|
|
34
|
-
after_agent
|
|
35
|
-
agent_error
|
|
36
|
-
after_call
|
|
37
|
-
before_verdict
|
|
38
|
-
after_verdict
|
|
39
|
-
].freeze
|
|
40
|
-
|
|
41
32
|
# -------------------------------------------------------------------------
|
|
42
|
-
# Class-level DSL —
|
|
33
|
+
# Class-level DSL — core
|
|
43
34
|
# -------------------------------------------------------------------------
|
|
44
35
|
class << self
|
|
45
|
-
#
|
|
46
|
-
# agents PolitenessAgent, ConstructivenessAgent
|
|
47
|
-
def agents(*list)
|
|
48
|
-
tribunal_config[:agents] = list.flatten
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Class-level hook registration.
|
|
52
|
-
# on(:after_agent) { |result| puts result.model }
|
|
53
|
-
def on(event, &block)
|
|
54
|
-
unless VALID_HOOKS.include?(event)
|
|
55
|
-
raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.join(", ")}"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
tribunal_config[:hooks][event] = block
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Rails-style aliases for +on+:
|
|
62
|
-
#
|
|
63
|
-
# before :call do ... end # → on :before_call
|
|
64
|
-
# before :agent do ... end # → on :before_agent (not used yet)
|
|
65
|
-
# before :verdict do |r| end # → on :before_verdict
|
|
66
|
-
# after :call do ... end # → on :after_call
|
|
67
|
-
# after :agent do |r| end # → on :after_agent
|
|
68
|
-
# after :verdict do |v| end # → on :after_verdict
|
|
69
|
-
# callback :agent_error do |n,e| end # → on :agent_error
|
|
70
|
-
def before(event, &block)
|
|
71
|
-
on(:"before_#{event}", &block)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def after(event, &block)
|
|
75
|
-
on(:"after_#{event}", &block)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def callback(event, &block)
|
|
79
|
-
on(event, &block)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Class-level process block.
|
|
83
|
-
# process { |results| results.all? { |r| r.parsed["result"] == true } }
|
|
84
|
-
def process(&block)
|
|
85
|
-
tribunal_config[:process] = block
|
|
86
|
-
end
|
|
87
|
-
|
|
36
|
+
# Each subclass gets its own isolated config hash.
|
|
88
37
|
def tribunal_config
|
|
89
38
|
@tribunal_config ||= { agents: [], hooks: {} }
|
|
90
39
|
end
|
|
@@ -94,42 +43,36 @@ module ActiveHarness
|
|
|
94
43
|
end
|
|
95
44
|
end
|
|
96
45
|
|
|
97
|
-
|
|
46
|
+
# -------------------------------------------------------------------------
|
|
47
|
+
# Instance API
|
|
48
|
+
# -------------------------------------------------------------------------
|
|
49
|
+
attr_accessor :input, :context, :stream, :agent_event_stream, :tribunal_event_stream
|
|
98
50
|
attr_reader :results, :errors, :verdict, :execution_time, :agent_execution_times
|
|
99
51
|
|
|
100
|
-
def initialize(input: nil, context: {}, agents: nil, timeout: 7
|
|
52
|
+
def initialize(input: nil, context: {}, agents: nil, timeout: 7,
|
|
53
|
+
stream: nil, agent_event_stream: nil, tribunal_event_stream: nil,
|
|
54
|
+
may_fail: :_unset)
|
|
101
55
|
config = self.class.tribunal_config
|
|
102
56
|
|
|
103
|
-
@input
|
|
104
|
-
@context
|
|
105
|
-
@agents
|
|
106
|
-
@timeout
|
|
107
|
-
@process_block
|
|
108
|
-
@
|
|
109
|
-
@
|
|
110
|
-
@
|
|
111
|
-
@
|
|
112
|
-
@
|
|
57
|
+
@input = input
|
|
58
|
+
@context = context
|
|
59
|
+
@agents = agents || config[:agents]
|
|
60
|
+
@timeout = timeout
|
|
61
|
+
@process_block = config[:process]
|
|
62
|
+
@strategy = config[:strategy]
|
|
63
|
+
@evaluate_block = config[:evaluate_block]
|
|
64
|
+
@may_fail = may_fail == :_unset ? config[:may_fail] : may_fail
|
|
65
|
+
@hooks = config[:hooks].dup
|
|
66
|
+
@stream = stream
|
|
67
|
+
@agent_event_stream = agent_event_stream
|
|
68
|
+
@tribunal_event_stream = tribunal_event_stream
|
|
69
|
+
@results = []
|
|
70
|
+
@errors = []
|
|
71
|
+
@verdict = nil
|
|
72
|
+
@execution_time = nil
|
|
113
73
|
@agent_execution_times = []
|
|
114
74
|
end
|
|
115
75
|
|
|
116
|
-
# Instance-level hook registration — overrides class-level hooks.
|
|
117
|
-
# :before_verdict is a transform hook: its return value replaces the results array.
|
|
118
|
-
def on(event, &block)
|
|
119
|
-
unless VALID_HOOKS.include?(event)
|
|
120
|
-
raise ArgumentError, "Unknown Tribunal hook :#{event}. Valid hooks: #{VALID_HOOKS.join(", ")}"
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
@hooks[event] = block
|
|
124
|
-
self
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Instance-level process block — overrides class-level block.
|
|
128
|
-
def process(&block)
|
|
129
|
-
@process_block = block
|
|
130
|
-
self
|
|
131
|
-
end
|
|
132
|
-
|
|
133
76
|
# Run all agents in parallel, then compute the verdict.
|
|
134
77
|
# Returns self so calls can be chained: tribunal.call.verdict
|
|
135
78
|
#
|
|
@@ -143,14 +86,15 @@ module ActiveHarness
|
|
|
143
86
|
|
|
144
87
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
145
88
|
|
|
146
|
-
futures = agents.map do |agent|
|
|
89
|
+
futures = agents.each_with_index.map do |agent, index|
|
|
90
|
+
run_hook(:before_agent, agent, index)
|
|
147
91
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
148
92
|
future = Concurrent::Future.execute { agent.call }
|
|
149
93
|
[future, t0]
|
|
150
94
|
end
|
|
151
95
|
|
|
152
|
-
@results
|
|
153
|
-
@errors
|
|
96
|
+
@results = []
|
|
97
|
+
@errors = []
|
|
154
98
|
@agent_execution_times = []
|
|
155
99
|
|
|
156
100
|
futures.each_with_index do |(future, t0), index|
|
|
@@ -159,19 +103,19 @@ module ActiveHarness
|
|
|
159
103
|
@agent_execution_times << { agent: agents[index].class.name, time: elapsed }
|
|
160
104
|
|
|
161
105
|
if future.fulfilled?
|
|
162
|
-
value
|
|
106
|
+
value = future.value
|
|
163
107
|
result = value.is_a?(ActiveHarness::Agent) ? value.result : value
|
|
164
108
|
@results << result
|
|
165
|
-
run_hook(:after_agent, result)
|
|
109
|
+
run_hook(:after_agent, result, index)
|
|
166
110
|
elsif future.incomplete?
|
|
167
111
|
error = Errors::TimeoutError.new(
|
|
168
112
|
"Agent #{agents[index].class.name} timed out after #{@timeout}s"
|
|
169
113
|
)
|
|
170
114
|
@errors << { agent: agents[index].class.name, error: error }
|
|
171
|
-
run_hook(:agent_error, agents[index].class.name, error)
|
|
115
|
+
run_hook(:agent_error, agents[index].class.name, error, index)
|
|
172
116
|
else
|
|
173
117
|
@errors << { agent: agents[index].class.name, error: future.reason }
|
|
174
|
-
run_hook(:agent_error, agents[index].class.name, future.reason)
|
|
118
|
+
run_hook(:agent_error, agents[index].class.name, future.reason, index)
|
|
175
119
|
end
|
|
176
120
|
end
|
|
177
121
|
|
|
@@ -179,13 +123,13 @@ module ActiveHarness
|
|
|
179
123
|
|
|
180
124
|
run_hook(:after_call, @results, @errors)
|
|
181
125
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
end
|
|
126
|
+
# If all agents failed, raise an exception.
|
|
127
|
+
# Otherwise, compute the verdict based on successful results.
|
|
128
|
+
check_failure_threshold!
|
|
186
129
|
|
|
187
130
|
verdict_input = transform_hook(:before_verdict, @results)
|
|
188
|
-
@verdict
|
|
131
|
+
@verdict = compute_verdict(verdict_input)
|
|
132
|
+
|
|
189
133
|
run_hook(:after_verdict, @verdict)
|
|
190
134
|
|
|
191
135
|
self
|
|
@@ -193,26 +137,39 @@ module ActiveHarness
|
|
|
193
137
|
|
|
194
138
|
private
|
|
195
139
|
|
|
196
|
-
def
|
|
197
|
-
@
|
|
140
|
+
def check_failure_threshold!
|
|
141
|
+
if !@may_fail.nil? && @errors.size > @may_fail
|
|
142
|
+
raise Errors::AllAgentsFailed,
|
|
143
|
+
"Too many agents failed (#{@errors.size} > may_fail: #{@may_fail}) — #{error_summary}"
|
|
144
|
+
elsif @results.empty?
|
|
145
|
+
raise Errors::AllAgentsFailed, "All agents failed — #{error_summary}"
|
|
146
|
+
end
|
|
198
147
|
end
|
|
199
148
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return value unless @hooks[event]
|
|
203
|
-
|
|
204
|
-
@hooks[event].call(value)
|
|
149
|
+
def error_summary
|
|
150
|
+
@errors.map { |e| "#{e[:agent]}: #{e[:error].message}" }.join("; ")
|
|
205
151
|
end
|
|
206
152
|
|
|
207
153
|
def resolve_agents
|
|
208
154
|
@agents.map do |agent|
|
|
209
155
|
if agent.is_a?(Class)
|
|
210
|
-
agent.new(
|
|
156
|
+
agent.new(
|
|
157
|
+
input: @input,
|
|
158
|
+
context: @context.dup,
|
|
159
|
+
stream: @stream,
|
|
160
|
+
event_stream: @agent_event_stream
|
|
161
|
+
)
|
|
211
162
|
else
|
|
212
|
-
agent.input
|
|
163
|
+
agent.input = @input if @input
|
|
164
|
+
agent.stream = @stream if @stream
|
|
165
|
+
agent.event_stream = @agent_event_stream if @agent_event_stream
|
|
213
166
|
agent
|
|
214
167
|
end
|
|
215
168
|
end
|
|
216
169
|
end
|
|
217
170
|
end
|
|
218
171
|
end
|
|
172
|
+
|
|
173
|
+
require_relative "tribunal/hooks"
|
|
174
|
+
require_relative "tribunal/dsl"
|
|
175
|
+
require_relative "tribunal/processing"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_harness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- the-teacher
|
|
@@ -72,6 +72,9 @@ files:
|
|
|
72
72
|
- lib/active_harness/railtie.rb
|
|
73
73
|
- lib/active_harness/result.rb
|
|
74
74
|
- lib/active_harness/tribunal.rb
|
|
75
|
+
- lib/active_harness/tribunal/dsl.rb
|
|
76
|
+
- lib/active_harness/tribunal/hooks.rb
|
|
77
|
+
- lib/active_harness/tribunal/processing.rb
|
|
75
78
|
- lib/generators/active_harness/agent/agent_generator.rb
|
|
76
79
|
- lib/generators/active_harness/agent/templates/agent.rb.tt
|
|
77
80
|
- lib/generators/active_harness/install/install_generator.rb
|