active_harness 0.2.27 → 0.2.29
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/agent/prompt.rb +8 -4
- data/lib/active_harness/agent.rb +17 -6
- data/lib/active_harness/costs.rb +35 -5
- data/lib/active_harness/memory.rb +22 -3
- data/lib/active_harness/pipeline/README.md +1 -1
- data/lib/active_harness/pipeline.rb +1 -1
- data/lib/active_harness/result.rb +2 -1
- data/lib/active_harness.rb +1 -1
- data/lib/generators/active_harness/install/templates/controllers/ai_controller.rb +4 -3
- data/lib/generators/active_harness/install/templates/pipelines/support_pipeline.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7146a492cd9454703ab47eb1c6222962587b4fe5585aaef2a5390349da394a1a
|
|
4
|
+
data.tar.gz: 476ee4d22259b10fa7d58cd25208ea5c900e6523991b1f17e288f7c3104cfd92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d211c987a80244262c35fbc83202491b61807ee98f092808ca801cfcc57eb1ad58e4f48ccf8e364078b99a267e14816e8551b55c5b2a3de2fe8e6828321bc2b6
|
|
7
|
+
data.tar.gz: 71244082f0e0c81d9fe7d0271e05dd53ebf0e5a727ced5554d637e84aed8a58b6a014e8b0537ff2dcc10cc501385905b78c5aae8f328c1d774a874fc30d3c7b3
|
|
@@ -49,11 +49,15 @@ module ActiveHarness
|
|
|
49
49
|
prompt
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Injects agent state into a prompt class instance before #call.
|
|
53
|
+
# Available in prompt classes: @input, @context, @params, @memory, @context_window, @config
|
|
52
54
|
def inject_agent_state(obj)
|
|
53
|
-
obj.instance_variable_set(:@input,
|
|
54
|
-
obj.instance_variable_set(:@context,
|
|
55
|
-
obj.instance_variable_set(:@
|
|
56
|
-
obj.instance_variable_set(:@
|
|
55
|
+
obj.instance_variable_set(:@input, @input)
|
|
56
|
+
obj.instance_variable_set(:@context, @context)
|
|
57
|
+
obj.instance_variable_set(:@params, @params)
|
|
58
|
+
obj.instance_variable_set(:@config, @config)
|
|
59
|
+
obj.instance_variable_set(:@memory, @memory)
|
|
60
|
+
obj.instance_variable_set(:@context_window, @context_window)
|
|
57
61
|
end
|
|
58
62
|
end
|
|
59
63
|
end
|
data/lib/active_harness/agent.rb
CHANGED
|
@@ -54,6 +54,7 @@ module ActiveHarness
|
|
|
54
54
|
:params,
|
|
55
55
|
:memory
|
|
56
56
|
attr_reader :result,
|
|
57
|
+
:context_window,
|
|
57
58
|
:token_stream,
|
|
58
59
|
:event_stream
|
|
59
60
|
|
|
@@ -76,9 +77,10 @@ module ActiveHarness
|
|
|
76
77
|
@context = context
|
|
77
78
|
@params = params
|
|
78
79
|
@memory = memory
|
|
79
|
-
@models_override
|
|
80
|
-
@
|
|
81
|
-
@
|
|
80
|
+
@models_override = Array(models) if models
|
|
81
|
+
@context_window = lookup_context_window(self.models.to_a.first)
|
|
82
|
+
@token_stream = streams[:token]
|
|
83
|
+
@event_stream = streams[:agent]
|
|
82
84
|
fire(:setup)
|
|
83
85
|
end
|
|
84
86
|
|
|
@@ -144,9 +146,10 @@ module ActiveHarness
|
|
|
144
146
|
end
|
|
145
147
|
|
|
146
148
|
def build_result(response, entry, attempts, elapsed)
|
|
147
|
-
raw
|
|
149
|
+
raw = response[:content]
|
|
148
150
|
processed = parse_output(raw)
|
|
149
|
-
usage
|
|
151
|
+
usage = response[:usage]
|
|
152
|
+
cw = lookup_context_window(entry)
|
|
150
153
|
|
|
151
154
|
Result.new(
|
|
152
155
|
input: @input,
|
|
@@ -160,10 +163,18 @@ module ActiveHarness
|
|
|
160
163
|
attempts: attempts,
|
|
161
164
|
execution_time: elapsed,
|
|
162
165
|
usage: usage,
|
|
163
|
-
cost: calculate_cost(entry[:model], usage)
|
|
166
|
+
cost: calculate_cost(entry[:model], usage),
|
|
167
|
+
context_window: cw
|
|
164
168
|
)
|
|
165
169
|
end
|
|
166
170
|
|
|
171
|
+
def lookup_context_window(entry)
|
|
172
|
+
return nil unless entry
|
|
173
|
+
Costs.find(entry[:model])&.context_window
|
|
174
|
+
rescue StandardError
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
167
178
|
def normalize_input!
|
|
168
179
|
return if @config.fetch(:normalize_input, true) == false
|
|
169
180
|
@input = @input&.strip&.gsub(/\s+/, " ")
|
data/lib/active_harness/costs.rb
CHANGED
|
@@ -62,12 +62,33 @@ module ActiveHarness
|
|
|
62
62
|
:output_per_million,
|
|
63
63
|
:cache_read_input_per_million,
|
|
64
64
|
:cache_write_input_per_million,
|
|
65
|
+
:context_window,
|
|
66
|
+
:max_output_tokens,
|
|
67
|
+
:input_modalities,
|
|
68
|
+
:output_modalities,
|
|
65
69
|
keyword_init: true
|
|
66
70
|
) do
|
|
71
|
+
# Returns capability tags derived from modality data and model id/name.
|
|
72
|
+
# Possible values: "vision", "pdf", "audio", "video", "imggen", "embed"
|
|
73
|
+
def categories
|
|
74
|
+
inp = input_modalities || []
|
|
75
|
+
out = output_modalities || []
|
|
76
|
+
cats = []
|
|
77
|
+
cats << "vision" if inp.include?("image")
|
|
78
|
+
cats << "pdf" if inp.include?("pdf")
|
|
79
|
+
cats << "audio" if inp.include?("audio") || out.include?("audio")
|
|
80
|
+
cats << "video" if inp.include?("video") || out.include?("video")
|
|
81
|
+
cats << "imggen" if out.include?("image")
|
|
82
|
+
cats << "embed" if id.to_s.match?(/embed/i) || name.to_s.match?(/embed/i)
|
|
83
|
+
cats
|
|
84
|
+
end
|
|
85
|
+
|
|
67
86
|
def inspect
|
|
68
87
|
parts = ["id=#{id.inspect}", "provider=#{provider.inspect}"]
|
|
69
88
|
parts << "input=$#{input_per_million}/M" if input_per_million
|
|
70
89
|
parts << "output=$#{output_per_million}/M" if output_per_million
|
|
90
|
+
parts << "ctx=#{context_window}" if context_window
|
|
91
|
+
parts << "cats=#{categories.join(',')}" if categories.any?
|
|
71
92
|
"#<ModelCost #{parts.join(' ')}>"
|
|
72
93
|
end
|
|
73
94
|
end
|
|
@@ -229,11 +250,16 @@ module ActiveHarness
|
|
|
229
250
|
cache_write_input_per_million: cost[:cache_write]
|
|
230
251
|
}.compact
|
|
231
252
|
|
|
253
|
+
mods = m[:modalities] || {}
|
|
232
254
|
{
|
|
233
|
-
id:
|
|
234
|
-
name:
|
|
235
|
-
provider:
|
|
236
|
-
|
|
255
|
+
id: m[:id],
|
|
256
|
+
name: m[:name] || m[:id],
|
|
257
|
+
provider: ah_provider,
|
|
258
|
+
context_window: m[:context_window] || m.dig(:limit, :context),
|
|
259
|
+
max_output_tokens: m[:max_output_tokens] || m.dig(:limit, :output),
|
|
260
|
+
input_modalities: Array(mods[:input]),
|
|
261
|
+
output_modalities: Array(mods[:output]),
|
|
262
|
+
pricing: standard.any? ? { text_tokens: { standard: standard } } : {}
|
|
237
263
|
}
|
|
238
264
|
end
|
|
239
265
|
end
|
|
@@ -248,7 +274,11 @@ module ActiveHarness
|
|
|
248
274
|
input_per_million: standard[:input_per_million],
|
|
249
275
|
output_per_million: standard[:output_per_million],
|
|
250
276
|
cache_read_input_per_million: standard[:cache_read_input_per_million],
|
|
251
|
-
cache_write_input_per_million: standard[:cache_write_input_per_million]
|
|
277
|
+
cache_write_input_per_million: standard[:cache_write_input_per_million],
|
|
278
|
+
context_window: raw[:context_window],
|
|
279
|
+
max_output_tokens: raw[:max_output_tokens],
|
|
280
|
+
input_modalities: Array(raw[:input_modalities]),
|
|
281
|
+
output_modalities: Array(raw[:output_modalities])
|
|
252
282
|
)
|
|
253
283
|
end
|
|
254
284
|
|
|
@@ -84,6 +84,12 @@ module ActiveHarness
|
|
|
84
84
|
async: false,
|
|
85
85
|
**adapter_opts
|
|
86
86
|
)
|
|
87
|
+
if self.class == Memory
|
|
88
|
+
raise NotImplementedError,
|
|
89
|
+
"Memory cannot be instantiated directly. " \
|
|
90
|
+
"Use Memory::JsonFile, Memory::Postgresql, or Memory::Sqlite."
|
|
91
|
+
end
|
|
92
|
+
|
|
87
93
|
@session_id = session_id
|
|
88
94
|
@depth = depth
|
|
89
95
|
@enabled = enabled
|
|
@@ -138,13 +144,15 @@ module ActiveHarness
|
|
|
138
144
|
|
|
139
145
|
# Returns messages array for LLM consumption, respecting depth.
|
|
140
146
|
# Optional filters:
|
|
141
|
-
# filter:
|
|
142
|
-
# since:
|
|
143
|
-
|
|
147
|
+
# filter: ->(turn) { turn[:agent] == "SupportAgent" }
|
|
148
|
+
# since: Time.now - 3600
|
|
149
|
+
# token_budget: 4000 # rough limit (chars / 4 estimate); trims oldest turns first
|
|
150
|
+
def to_messages(filter: nil, since: nil, token_budget: nil)
|
|
144
151
|
turns = @turns.dup
|
|
145
152
|
turns.select! { |t| filter.call(t) } if filter
|
|
146
153
|
turns.select! { |t| after?(t, since) } if since
|
|
147
154
|
turns = turns.last(@depth) if @depth
|
|
155
|
+
turns = trim_to_token_budget(turns, token_budget) if token_budget
|
|
148
156
|
|
|
149
157
|
turns.flat_map do |t|
|
|
150
158
|
[
|
|
@@ -210,5 +218,16 @@ module ActiveHarness
|
|
|
210
218
|
turn_time = Time.parse(turn[:at].to_s) rescue nil
|
|
211
219
|
turn_time ? turn_time >= time : true
|
|
212
220
|
end
|
|
221
|
+
|
|
222
|
+
def trim_to_token_budget(turns, budget)
|
|
223
|
+
return turns if turns.empty?
|
|
224
|
+
|
|
225
|
+
total = 0
|
|
226
|
+
turns.reverse.take_while do |t|
|
|
227
|
+
tokens = ((t[:request].to_s.length + t[:response].to_s.length) / 4.0).ceil
|
|
228
|
+
total += tokens
|
|
229
|
+
total <= budget
|
|
230
|
+
end.reverse
|
|
231
|
+
end
|
|
213
232
|
end
|
|
214
233
|
end
|
|
@@ -14,7 +14,7 @@ module ActiveHarness
|
|
|
14
14
|
#
|
|
15
15
|
# step :safety_tribunal do
|
|
16
16
|
# use SafetyTribunal
|
|
17
|
-
# stop_if ->(result) { result.verdict == false }
|
|
17
|
+
# stop_if ->(result) { result.processed["verdict"] == false }
|
|
18
18
|
# end
|
|
19
19
|
#
|
|
20
20
|
# on :before_step do |step_name, payload| ... end
|
|
@@ -11,7 +11,7 @@ module ActiveHarness
|
|
|
11
11
|
:input,
|
|
12
12
|
:output,
|
|
13
13
|
:processed,
|
|
14
|
-
:system_prompt,
|
|
14
|
+
:system_prompt,
|
|
15
15
|
:provider, :model,
|
|
16
16
|
:temperature,
|
|
17
17
|
:model_list,
|
|
@@ -19,5 +19,6 @@ module ActiveHarness
|
|
|
19
19
|
:execution_time,
|
|
20
20
|
:usage,
|
|
21
21
|
:cost,
|
|
22
|
+
:context_window,
|
|
22
23
|
keyword_init: true)
|
|
23
24
|
end
|
data/lib/active_harness.rb
CHANGED
|
@@ -43,11 +43,12 @@ class AiSupportController < ApplicationController
|
|
|
43
43
|
# Returns verdict: true (safe) or false (rejected).
|
|
44
44
|
# ---------------------------------------------------------------------------
|
|
45
45
|
def tribunal
|
|
46
|
-
|
|
46
|
+
tribunal = SupportGuardTribunal.new(input: params.require(:input))
|
|
47
|
+
tribunal.call
|
|
47
48
|
|
|
48
49
|
render json: {
|
|
49
|
-
verdict:
|
|
50
|
-
time:
|
|
50
|
+
verdict: tribunal.verdict,
|
|
51
|
+
time: tribunal.execution_time
|
|
51
52
|
}
|
|
52
53
|
end
|
|
53
54
|
|
|
@@ -7,7 +7,7 @@ class SupportPipeline < ActiveHarness::Pipeline
|
|
|
7
7
|
# Step 1 — GUARD: reject spam before spending tokens on an answer
|
|
8
8
|
step :spam_guard do
|
|
9
9
|
use SupportGuardTribunal
|
|
10
|
-
stop_if ->(result) { result.verdict == false }
|
|
10
|
+
stop_if ->(result) { result.processed["verdict"] == false }
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# Step 2 — RESPOND: generate the actual answer
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.29
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- the-teacher
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|