active_harness 0.2.8 → 0.2.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7139860d4aa461b1d86eca144c4929d4bf362192099498eadf61401b85acf8d
4
- data.tar.gz: 3f00f021d6fd660748767349fb6a00ba9187c6baf507d18b01ac668c52c67904
3
+ metadata.gz: 79db5ea3cf1416b2d9e8d3958d9a6e69c9ab65ea36ddf52bcdd8e41ff2e07b0c
4
+ data.tar.gz: 0fde20074cb5d215226fdac581b84220ae2d5462e14cae4bfc6e1216dff84164
5
5
  SHA512:
6
- metadata.gz: 1c335d3ecce967dc10424ef0cbe15922561627539e7cbf5eb2a34ad7e672c964bd7fd796ed129229a41a7f32992c22241353e16b8e314aec7cd71878ea86148d
7
- data.tar.gz: 930e5ad7ad85e5e269772e90c45453fb6672dc2f8752c3d546f9888d3d2c8cd2020a24190f7391f8217bf1c8e24cde67945788870c3b1d1ec60e01747c0d2698
6
+ metadata.gz: f6ccb47cc9f4c176d44503323ee642e879c66961521d69b102616ca92be8eeb8c0b38ce12e72bab1dbf67756c2d984edb8fb238d26778b4755f49370290a7861
7
+ data.tar.gz: 0c0478912520ed1a466a16dd4f453996c25f20878a6d1b10399faf89a7668fbfac58f0ef00d72c39b10db442f54366d0f886f1e70eb9908cc4bbbf6035412e0f
@@ -47,7 +47,7 @@ module ActiveHarness
47
47
  messages = build_messages(system_prompt, @input)
48
48
  opts = { model: entry[:model], messages: messages }
49
49
  opts[:temperature] = entry[:temperature] if entry[:temperature]
50
- opts[:stream] = @stream if @stream
50
+ opts[:stream] = @token_stream if @token_stream
51
51
  opts[:name] = entry[:name] if entry[:name]
52
52
  provider.call(**opts)
53
53
  end
@@ -54,8 +54,8 @@ module ActiveHarness
54
54
  chat = backend.call(params)
55
55
  chat.with_instructions(system_prompt) if system_prompt
56
56
 
57
- if @stream
58
- response = chat.ask(@input) { |chunk| @stream.call(chunk.content) if chunk.content }
57
+ if @token_stream
58
+ response = chat.ask(@input) { |chunk| @token_stream.call(chunk.content) if chunk.content }
59
59
  else
60
60
  response = chat.ask(@input)
61
61
  end
@@ -11,8 +11,8 @@ module ActiveHarness
11
11
  # SupportAgent.call(input: "Hi")
12
12
  # SupportAgent.call(input: "Hi", context: { user_id: 42 })
13
13
  # SupportAgent.call(input: "Hi", memory: memory)
14
- def call(input: nil, context: {}, models: nil, memory: nil, stream: nil)
15
- new(input: input, context: context, models: models, memory: memory, stream: stream).call.result
14
+ def call(input: nil, context: {}, models: nil, memory: nil, stream: nil, token_stream: nil, event_stream: nil)
15
+ new(input: input, context: context, models: models, memory: memory, stream: stream, token_stream: token_stream, event_stream: event_stream).call
16
16
  end
17
17
 
18
18
  # Each subclass gets its own isolated config hash.
@@ -28,7 +28,7 @@ module ActiveHarness
28
28
  # -------------------------------------------------------------------------
29
29
  # Instance API
30
30
  # -------------------------------------------------------------------------
31
- attr_accessor :input, :context
31
+ attr_accessor :input, :context, :stream, :token_stream, :event_stream
32
32
  attr_reader :result
33
33
 
34
34
  def models=(list)
@@ -36,12 +36,18 @@ module ActiveHarness
36
36
  @model_list_proxy = nil
37
37
  end
38
38
 
39
- def initialize(input: nil, context: {}, models: nil, memory: nil, stream: nil)
39
+ def memory=(obj)
40
+ @memory = obj
41
+ end
42
+
43
+ def initialize(input: nil, context: {}, models: nil, memory: nil, stream: nil, token_stream: nil, event_stream: nil)
40
44
  @input = input
41
45
  @context = context
42
46
  @config = self.class.agent_config
43
47
  @models_override = Array(models) if models
44
48
  @stream = stream
49
+ @token_stream = token_stream
50
+ @event_stream = event_stream
45
51
  # memory: can be passed directly or via context[:memory]
46
52
  @memory = memory || @context[:memory]
47
53
  run_hook(:setup)
@@ -53,9 +59,9 @@ module ActiveHarness
53
59
  # Optionally accepts input and stream callback inline:
54
60
  # agent.call("What is the capital of Japan?")
55
61
  # agent.call("...", stream: ->(token) { print token })
56
- def call(input = nil, stream: nil)
57
- @input = input if input
58
- @stream = stream if stream
62
+ def call(input = nil, token_stream: nil)
63
+ @input = input if input
64
+ @token_stream = token_stream if token_stream
59
65
  @memory&.load
60
66
  @system_prompt = resolve_system_prompt
61
67
  run_hook(:before_call)
@@ -25,6 +25,7 @@ module ActiveHarness
25
25
 
26
26
  buffer = ""
27
27
  content = ""
28
+ usage = nil
28
29
 
29
30
  http.request(req) do |response|
30
31
  response.read_body do |chunk|
@@ -42,11 +43,12 @@ module ActiveHarness
42
43
  on_token.call(token)
43
44
  content += token
44
45
  end
46
+ usage ||= parsed["usage"] if parsed.key?("usage")
45
47
  end
46
48
  end
47
49
  end
48
50
 
49
- content
51
+ { content: content, raw_usage: usage }
50
52
  rescue Net::OpenTimeout, Net::ReadTimeout
51
53
  raise Errors::TimeoutError, "Request to #{url.host} timed out"
52
54
  rescue JSON::ParserError
@@ -45,6 +45,21 @@ module ActiveHarness
45
45
  }
46
46
  end
47
47
 
48
+ # Streaming call for OpenAI-compatible providers.
49
+ # Adds stream: true and stream_options to body, calls StreamingClient,
50
+ # and returns the same { content:, provider:, model:, usage: } shape
51
+ # as non-streaming calls so callers need no special handling.
52
+ def call_streaming(url:, headers:, body:, stream:, provider:, model:)
53
+ body = body.merge(stream: true, stream_options: { include_usage: true })
54
+ result = post_json_stream(URI(url), headers: headers, body: body, on_token: stream)
55
+ {
56
+ content: result[:content],
57
+ provider: provider,
58
+ model: model,
59
+ usage: extract_usage_openai({ "usage" => result[:raw_usage] })
60
+ }
61
+ end
62
+
48
63
  def parse!(raw)
49
64
  JSON.parse(raw)
50
65
  rescue JSON::ParserError => e
@@ -5,14 +5,13 @@ module ActiveHarness
5
5
  # DeepSeek — OpenAI-compatible API.
6
6
  # https://platform.deepseek.com/api-docs
7
7
  class DeepSeek < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.deepseek_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.deepseek_api_url, headers: headers, body: body, stream: stream, provider: :deepseek, model: model) if stream
13
+
14
+ raw = post_json(URI(config.deepseek_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
@@ -5,23 +5,17 @@ module ActiveHarness
5
5
  # Google Gemini — OpenAI-compatible endpoint (beta).
6
6
  # https://ai.google.dev/gemini-api/docs/openai
7
7
  class Gemini < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.gemini_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.gemini_api_url, headers: headers, body: body, stream: stream, provider: :gemini, model: model) if stream
13
+
14
+ raw = post_json(URI(config.gemini_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
19
- {
20
- content: data.dig("choices", 0, "message", "content").to_s.strip,
21
- provider: :gemini,
22
- model: data["model"] || model,
23
- usage: extract_usage_openai(data)
24
- }
18
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :gemini, model: data["model"] || model, usage: extract_usage_openai(data) }
25
19
  end
26
20
 
27
21
  private
@@ -13,26 +13,20 @@ module ActiveHarness
13
13
  # use provider: :gpustack, model: "Qwen/Qwen2.5-7B-Instruct-GGUF"
14
14
  # end
15
15
  class GPUStack < Base
16
- def call(model:, messages:, temperature: 0.7)
17
- url = URI("#{api_base}/v1/chat/completions")
18
-
16
+ def call(model:, messages:, temperature: 0.7, stream: nil)
17
+ url = "#{api_base}/v1/chat/completions"
19
18
  headers = { "Content-Type" => "application/json" }
20
19
  key = api_key
21
20
  headers["Authorization"] = "Bearer #{key}" if key
21
+ body = { model: model, messages: messages, temperature: temperature }
22
+
23
+ return call_streaming(url: url, headers: headers, body: body, stream: stream, provider: :gpustack, model: model) if stream
22
24
 
23
- raw = post_json(url,
24
- headers: headers,
25
- body: { model: model, messages: messages, temperature: temperature }
26
- )
25
+ raw = post_json(URI(url), headers: headers, body: body)
27
26
  data = parse!(raw)
28
27
  handle_error!(data)
29
28
 
30
- {
31
- content: data.dig("choices", 0, "message", "content").to_s.strip,
32
- provider: :gpustack,
33
- model: data["model"] || model,
34
- usage: extract_usage_openai(data)
35
- }
29
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :gpustack, model: data["model"] || model, usage: extract_usage_openai(data) }
36
30
  end
37
31
 
38
32
  private
@@ -5,23 +5,17 @@ module ActiveHarness
5
5
  # Groq — OpenAI-compatible API with fast inference.
6
6
  # https://console.groq.com/docs/openai
7
7
  class Groq < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.groq_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.groq_api_url, headers: headers, body: body, stream: stream, provider: :groq, model: model) if stream
13
+
14
+ raw = post_json(URI(config.groq_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
19
- {
20
- content: data.dig("choices", 0, "message", "content").to_s.strip,
21
- provider: :groq,
22
- model: data["model"] || model,
23
- usage: extract_usage_openai(data)
24
- }
18
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :groq, model: data["model"] || model, usage: extract_usage_openai(data) }
25
19
  end
26
20
 
27
21
  private
@@ -5,14 +5,13 @@ module ActiveHarness
5
5
  # Mistral AI — OpenAI-compatible API.
6
6
  # https://docs.mistral.ai/api
7
7
  class Mistral < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.mistral_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.mistral_api_url, headers: headers, body: body, stream: stream, provider: :mistral, model: model) if stream
13
+
14
+ raw = post_json(URI(config.mistral_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
@@ -13,26 +13,20 @@ module ActiveHarness
13
13
  # use provider: :ollama, model: "llama3.2"
14
14
  # end
15
15
  class Ollama < Base
16
- def call(model:, messages:, temperature: 0.7)
17
- url = URI("#{api_base}/v1/chat/completions")
18
-
16
+ def call(model:, messages:, temperature: 0.7, stream: nil)
17
+ url = "#{api_base}/v1/chat/completions"
19
18
  headers = { "Content-Type" => "application/json" }
20
19
  key = api_key
21
20
  headers["Authorization"] = "Bearer #{key}" if key
21
+ body = { model: model, messages: messages, temperature: temperature }
22
+
23
+ return call_streaming(url: url, headers: headers, body: body, stream: stream, provider: :ollama, model: model) if stream
22
24
 
23
- raw = post_json(url,
24
- headers: headers,
25
- body: { model: model, messages: messages, temperature: temperature }
26
- )
25
+ raw = post_json(URI(url), headers: headers, body: body)
27
26
  data = parse!(raw)
28
27
  handle_error!(data)
29
28
 
30
- {
31
- content: data.dig("choices", 0, "message", "content").to_s.strip,
32
- provider: :ollama,
33
- model: data["model"] || model,
34
- usage: extract_usage_openai(data)
35
- }
29
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :ollama, model: data["model"] || model, usage: extract_usage_openai(data) }
36
30
  end
37
31
 
38
32
  private
@@ -7,23 +7,17 @@ module ActiveHarness
7
7
  # @param messages [Array<Hash>] [{role:, content:}, ...]
8
8
  # @param temperature [Float]
9
9
  # @return [Hash] { content:, provider:, model: }
10
- def call(model:, messages:, temperature: 0.7)
11
- raw = post_json(URI(config.openai_api_url),
12
- headers: {
13
- "Content-Type" => "application/json",
14
- "Authorization" => "Bearer #{api_key}"
15
- },
16
- body: { model: model, messages: messages, temperature: temperature }
17
- )
10
+ def call(model:, messages:, temperature: 0.7, stream: nil)
11
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
12
+ body = { model: model, messages: messages, temperature: temperature }
13
+
14
+ return call_streaming(url: config.openai_api_url, headers: headers, body: body, stream: stream, provider: :openai, model: model) if stream
15
+
16
+ raw = post_json(URI(config.openai_api_url), headers: headers, body: body)
18
17
  data = parse!(raw)
19
18
  handle_error!(data)
20
19
 
21
- {
22
- content: data.dig("choices", 0, "message", "content").to_s.strip,
23
- provider: :openai,
24
- model: data["model"] || model,
25
- usage: extract_usage_openai(data)
26
- }
20
+ { content: data.dig("choices", 0, "message", "content").to_s.strip, provider: :openai, model: data["model"] || model, usage: extract_usage_openai(data) }
27
21
  end
28
22
 
29
23
  private
@@ -17,11 +17,7 @@ module ActiveHarness
17
17
  }
18
18
  body = { model: model, messages: messages, temperature: temperature }
19
19
 
20
- if stream
21
- body[:stream] = true
22
- content = post_json_stream(URI(config.openrouter_api_url), headers: headers, body: body, on_token: stream)
23
- return { content: content, provider: :openrouter, model: model }
24
- end
20
+ return call_streaming(url: config.openrouter_api_url, headers: headers, body: body, stream: stream, provider: :openrouter, model: model) if stream
25
21
 
26
22
  raw = post_json(URI(config.openrouter_api_url), headers: headers, body: body)
27
23
  data = parse!(raw)
@@ -5,14 +5,13 @@ module ActiveHarness
5
5
  # Perplexity — OpenAI-compatible API with web-search-augmented models.
6
6
  # https://docs.perplexity.ai/api-reference/chat-completions
7
7
  class Perplexity < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.perplexity_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.perplexity_api_url, headers: headers, body: body, stream: stream, provider: :perplexity, model: model) if stream
13
+
14
+ raw = post_json(URI(config.perplexity_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
@@ -5,14 +5,13 @@ module ActiveHarness
5
5
  # xAI (Grok) — OpenAI-compatible API.
6
6
  # https://docs.x.ai/api
7
7
  class XAI < Base
8
- def call(model:, messages:, temperature: 0.7)
9
- raw = post_json(URI(config.xai_api_url),
10
- headers: {
11
- "Content-Type" => "application/json",
12
- "Authorization" => "Bearer #{api_key}"
13
- },
14
- body: { model: model, messages: messages, temperature: temperature }
15
- )
8
+ def call(model:, messages:, temperature: 0.7, stream: nil)
9
+ headers = { "Content-Type" => "application/json", "Authorization" => "Bearer #{api_key}" }
10
+ body = { model: model, messages: messages, temperature: temperature }
11
+
12
+ return call_streaming(url: config.xai_api_url, headers: headers, body: body, stream: stream, provider: :xai, model: model) if stream
13
+
14
+ raw = post_json(URI(config.xai_api_url), headers: headers, body: body)
16
15
  data = parse!(raw)
17
16
  handle_error!(data)
18
17
 
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.8
4
+ version: 0.2.10
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-05-22 00:00:00.000000000 Z
11
+ date: 2026-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby