active_harness 0.3.0 → 0.3.1
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/configuration.rb +9 -0
- data/lib/active_harness/providers/vercel.rb +120 -0
- data/lib/active_harness/request/models.rb +3 -2
- data/lib/active_harness/request/providers.rb +3 -1
- data/lib/active_harness.rb +2 -1
- data/lib/generators/active_harness/install/install_generator.rb +5 -5
- data/lib/generators/active_harness/install/templates/controllers/ai_controller.rb +9 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7de169d09a7a2e65620b93b57b9b760e8cf740342d35383deea404d1edf0f91e
|
|
4
|
+
data.tar.gz: 4a36da37f5953611eb9c9fe5ad6710381ec332e6ea49e1355af50540baab2fd0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab66c8538123326d8bbe8a634ee22420646cfd66f316ea503499585d058a521a5903b4e1487d29628117ddb4d7d8872839359f7977e5dda905ac7eb4c05453a4
|
|
7
|
+
data.tar.gz: 60e8a27f74ad023a94c039d09b374525fa571f4b2e09b15c42678c1acbd1ba16bd0a4012c1399f343f10eb24785af05ac4f102d11cf039580faa7381510a0f56
|
|
@@ -99,6 +99,12 @@ module ActiveHarness
|
|
|
99
99
|
attr_accessor :azure_api_base # e.g. "https://my-resource.openai.azure.com"
|
|
100
100
|
attr_accessor :azure_api_version # e.g. "2024-05-01-preview"
|
|
101
101
|
|
|
102
|
+
# -------------------------------------------------------------------------
|
|
103
|
+
# Vercel AI Gateway — TypeSafe-compatible evaluation endpoint (Jev)
|
|
104
|
+
# -------------------------------------------------------------------------
|
|
105
|
+
attr_accessor :vercel_api_key
|
|
106
|
+
attr_accessor :vercel_api_url
|
|
107
|
+
|
|
102
108
|
# -------------------------------------------------------------------------
|
|
103
109
|
# Custom providers
|
|
104
110
|
#
|
|
@@ -170,6 +176,9 @@ module ActiveHarness
|
|
|
170
176
|
@azure_ai_auth_token = ENV["AZURE_AI_AUTH_TOKEN"]
|
|
171
177
|
@azure_api_base = ENV["AZURE_API_BASE"]
|
|
172
178
|
@azure_api_version = ENV.fetch("AZURE_API_VERSION", "2024-05-01-preview")
|
|
179
|
+
|
|
180
|
+
@vercel_api_key = ENV["VERCEL_API_KEY"]
|
|
181
|
+
@vercel_api_url = "https://ai-gateway.vercel.sh/typesafe/v1/systemone"
|
|
173
182
|
end
|
|
174
183
|
end
|
|
175
184
|
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module ActiveHarness
|
|
4
|
+
module Providers
|
|
5
|
+
# Vercel AI Gateway — TypeSafe-compatible "System One" evaluation endpoint (Jev).
|
|
6
|
+
#
|
|
7
|
+
# This is fundamentally different from every other provider here: Jev does not
|
|
8
|
+
# take free-form chat messages and does not return free text. It evaluates a
|
|
9
|
+
# single `state` string against typed `questions` and returns typed answers
|
|
10
|
+
# (a score/probability/choice per question), e.g.:
|
|
11
|
+
#
|
|
12
|
+
# POST https://ai-gateway.vercel.sh/typesafe/v1/systemone
|
|
13
|
+
# { "model": "typesafe-ai/jev", "state": "...", "questions": { "answer": { "type": "noul", "instructions": "..." } } }
|
|
14
|
+
# => { "answers": { "answer": { "type": "noul", "noul": 0.8 } }, "usage": {...} }
|
|
15
|
+
#
|
|
16
|
+
# Question types are "noul" (0..1 probability, optionally scoped by a
|
|
17
|
+
# true/false `criteria` hash), "choice" (requires `criteria` — a name=>
|
|
18
|
+
# description hash), and "score" (requires `criteria` — an ordered array of
|
|
19
|
+
# at least 2 labels).
|
|
20
|
+
#
|
|
21
|
+
# To fit the standard Request(messages:) call shape without building a whole
|
|
22
|
+
# typed-questions DSL into Request itself, this bridges it minimally: the
|
|
23
|
+
# last user message becomes `state`, and — unless the model-chain entry
|
|
24
|
+
# supplies its own `questions:` hash (already in the API's own shape) — the
|
|
25
|
+
# system message (if any) becomes a single default "noul" question's
|
|
26
|
+
# `instructions`. Either way the raw `answers` payload is returned as-is
|
|
27
|
+
# (pretty JSON) as `content` — no interpretation of the typed answer is
|
|
28
|
+
# attempted.
|
|
29
|
+
#
|
|
30
|
+
# # simplest form — one implicit yes/no-ish question from the system prompt
|
|
31
|
+
# model do
|
|
32
|
+
# use provider: :vercel, model: "typesafe-ai/jev"
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # explicit multi-question form — full control over the question set
|
|
36
|
+
# model do
|
|
37
|
+
# use provider: :vercel, model: "typesafe-ai/jev", questions: {
|
|
38
|
+
# sentiment: { type: "score", instructions: "...", criteria: ["negative", "neutral", "positive"] },
|
|
39
|
+
# topic: { type: "choice", instructions: "...", criteria: { support: "...", spam: "..." } },
|
|
40
|
+
# urgent: { type: "noul", instructions: "..." }
|
|
41
|
+
# }
|
|
42
|
+
# end
|
|
43
|
+
class Vercel < Base
|
|
44
|
+
DEFAULT_INSTRUCTIONS = "Evaluate the given state.".freeze
|
|
45
|
+
|
|
46
|
+
def call(model:, messages:, temperature: nil, stream: nil, questions: nil)
|
|
47
|
+
raise Errors::InvalidRequestError, "provider: :vercel (Jev) does not support token streaming" if stream
|
|
48
|
+
|
|
49
|
+
state = messages.reverse.find { |m| m[:role] == "user" }&.fetch(:content, nil).to_s
|
|
50
|
+
|
|
51
|
+
headers = {
|
|
52
|
+
"Content-Type" => "application/json",
|
|
53
|
+
"Authorization" => "Bearer #{api_key}"
|
|
54
|
+
}
|
|
55
|
+
body = {
|
|
56
|
+
model: model,
|
|
57
|
+
state: state,
|
|
58
|
+
questions: questions || default_questions(messages)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
raw = post_json(URI(config.vercel_api_url), headers: headers, body: body)
|
|
62
|
+
data = parse!(raw)
|
|
63
|
+
handle_error!(data)
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
content: JSON.pretty_generate(data["answers"]),
|
|
67
|
+
provider: :vercel,
|
|
68
|
+
model: data["model"] || model,
|
|
69
|
+
usage: extract_usage(data)
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def default_questions(messages)
|
|
76
|
+
instructions = messages.find { |m| m[:role] == "system" }&.fetch(:content, nil).to_s
|
|
77
|
+
instructions = DEFAULT_INSTRUCTIONS if instructions.empty?
|
|
78
|
+
{ answer: { type: "noul", instructions: instructions } }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def api_key
|
|
82
|
+
key = config.vercel_api_key.to_s
|
|
83
|
+
raise Errors::InvalidApiKeyError, "vercel_api_key is not configured" if key.empty?
|
|
84
|
+
key
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Jev's usage object only has input/output tokens, OpenAI-shaped but
|
|
88
|
+
# under different keys than the chat-completions providers use.
|
|
89
|
+
def extract_usage(data)
|
|
90
|
+
u = data["usage"]
|
|
91
|
+
return nil unless u
|
|
92
|
+
|
|
93
|
+
input = u["input_tokens"].to_i
|
|
94
|
+
output = u["output_tokens"].to_i
|
|
95
|
+
{ input_tokens: input, output_tokens: output, total_tokens: input + output }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Two error shapes seen in practice:
|
|
99
|
+
# - TypeSafe request-validation errors: { "message": "...", "error_type": "..." }
|
|
100
|
+
# - AI Gateway account/billing errors (OpenAI-style, nested): { "error": { "message": "...", "type": "..." } }
|
|
101
|
+
def handle_error!(data)
|
|
102
|
+
msg, type =
|
|
103
|
+
if data["message"] && data["error_type"]
|
|
104
|
+
[data["message"].to_s, data["error_type"].to_s]
|
|
105
|
+
elsif data["error"].is_a?(Hash)
|
|
106
|
+
[data["error"]["message"].to_s, data["error"]["type"].to_s]
|
|
107
|
+
end
|
|
108
|
+
return unless msg
|
|
109
|
+
|
|
110
|
+
case type
|
|
111
|
+
when "invalid_request" then raise Errors::InvalidRequestError.new(msg, error_code: type)
|
|
112
|
+
when "unauthorized", "authentication_error" then raise Errors::InvalidApiKeyError.new(msg, error_code: type)
|
|
113
|
+
when "rate_limit", "rate_limit_error" then raise Errors::RateLimitError.new(msg, error_code: type)
|
|
114
|
+
when "customer_verification_required" then raise Errors::InvalidApiKeyError.new(msg, error_code: type)
|
|
115
|
+
else raise Errors::ProviderError.new(msg, error_code: type)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -134,7 +134,7 @@ module ActiveHarness
|
|
|
134
134
|
@models = []
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
def use(provider:, model:, temperature: nil, name: nil, size: nil, quality: nil, retry_attempts: nil, retry_delay: nil)
|
|
137
|
+
def use(provider:, model:, temperature: nil, name: nil, size: nil, quality: nil, retry_attempts: nil, retry_delay: nil, questions: nil)
|
|
138
138
|
@models << {
|
|
139
139
|
provider: provider,
|
|
140
140
|
model: model,
|
|
@@ -143,7 +143,8 @@ module ActiveHarness
|
|
|
143
143
|
size: size,
|
|
144
144
|
quality: quality,
|
|
145
145
|
retry_attempts: retry_attempts,
|
|
146
|
-
retry_delay: retry_delay
|
|
146
|
+
retry_delay: retry_delay,
|
|
147
|
+
questions: questions
|
|
147
148
|
}.compact
|
|
148
149
|
end
|
|
149
150
|
|
|
@@ -35,7 +35,8 @@ module ActiveHarness
|
|
|
35
35
|
azure: -> { Providers::Azure.new },
|
|
36
36
|
bedrock: -> { Providers::Bedrock.new },
|
|
37
37
|
vertexai: -> { Providers::VertexAI.new },
|
|
38
|
-
custom: -> { Providers::Custom.new }
|
|
38
|
+
custom: -> { Providers::Custom.new },
|
|
39
|
+
vercel: -> { Providers::Vercel.new }
|
|
39
40
|
}.freeze
|
|
40
41
|
|
|
41
42
|
IMAGE_PROVIDERS = {
|
|
@@ -61,6 +62,7 @@ module ActiveHarness
|
|
|
61
62
|
opts[:temperature] = entry[:temperature] if entry[:temperature]
|
|
62
63
|
opts[:stream] = @token if @token
|
|
63
64
|
opts[:name] = entry[:name] if entry[:name]
|
|
65
|
+
opts[:questions] = entry[:questions] if entry[:questions]
|
|
64
66
|
provider.call(**opts)
|
|
65
67
|
end
|
|
66
68
|
|
data/lib/active_harness.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "active_harness/providers/azure"
|
|
|
21
21
|
require_relative "active_harness/providers/bedrock"
|
|
22
22
|
require_relative "active_harness/providers/vertexai"
|
|
23
23
|
require_relative "active_harness/providers/custom"
|
|
24
|
+
require_relative "active_harness/providers/vercel"
|
|
24
25
|
require_relative "active_harness/providers/images/openai"
|
|
25
26
|
require_relative "active_harness/providers/images/openrouter"
|
|
26
27
|
require_relative "active_harness/providers/audio/openai"
|
|
@@ -34,7 +35,7 @@ require_relative "active_harness/pipeline"
|
|
|
34
35
|
require_relative "active_harness/railtie" if defined?(Rails::Railtie)
|
|
35
36
|
|
|
36
37
|
module ActiveHarness
|
|
37
|
-
VERSION = "0.3.
|
|
38
|
+
VERSION = "0.3.1"
|
|
38
39
|
|
|
39
40
|
class << self
|
|
40
41
|
# Configure ActiveHarness.
|
|
@@ -43,11 +43,11 @@ module ActiveHarness
|
|
|
43
43
|
def inject_routes
|
|
44
44
|
route <<~ROUTES.strip
|
|
45
45
|
# ActiveHarness — AI support endpoints
|
|
46
|
-
post "ai/agent",
|
|
47
|
-
post "ai/
|
|
48
|
-
post "ai/tribunal",
|
|
49
|
-
post "ai/pipeline",
|
|
50
|
-
get "ai/
|
|
46
|
+
post "ai/agent", to: "ai_support#agent" # not "request" — would override ActionController::Base#request
|
|
47
|
+
post "ai/request_memory", to: "ai_support#request_memory"
|
|
48
|
+
post "ai/tribunal", to: "ai_support#tribunal"
|
|
49
|
+
post "ai/pipeline", to: "ai_support#pipeline"
|
|
50
|
+
get "ai/request_stream", to: "ai_support#request_stream"
|
|
51
51
|
ROUTES
|
|
52
52
|
end
|
|
53
53
|
|
|
@@ -6,6 +6,10 @@ class AiSupportController < ApplicationController
|
|
|
6
6
|
# ---------------------------------------------------------------------------
|
|
7
7
|
# POST /ai/agent
|
|
8
8
|
# body: { input: "What is your return policy?" }
|
|
9
|
+
#
|
|
10
|
+
# Kept named "agent" (not "request") — defining a #request method on a Rails
|
|
11
|
+
# controller would override ActionController::Base#request and break
|
|
12
|
+
# request.env / CSRF / anything else that relies on the real HTTP request.
|
|
9
13
|
# ---------------------------------------------------------------------------
|
|
10
14
|
def agent
|
|
11
15
|
result = SupportRequest.call(input: params.require(:input))
|
|
@@ -18,13 +22,13 @@ class AiSupportController < ApplicationController
|
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
# ---------------------------------------------------------------------------
|
|
21
|
-
# POST /ai/
|
|
25
|
+
# POST /ai/request_memory
|
|
22
26
|
# body: { input: "Does that apply to accessories?", session_id: "user_42" }
|
|
23
27
|
#
|
|
24
28
|
# Uses AppMemory so the same session keeps conversational context
|
|
25
29
|
# across multiple requests.
|
|
26
30
|
# ---------------------------------------------------------------------------
|
|
27
|
-
def
|
|
31
|
+
def request_memory
|
|
28
32
|
memory = AppMemory.new(session_id: params.require(:session_id))
|
|
29
33
|
result = SupportRequest.call(input: params.require(:input), memory: memory)
|
|
30
34
|
|
|
@@ -71,21 +75,21 @@ class AiSupportController < ApplicationController
|
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
# ---------------------------------------------------------------------------
|
|
74
|
-
# GET /ai/
|
|
78
|
+
# GET /ai/request_stream?input=What+is+your+return+policy%3F
|
|
75
79
|
#
|
|
76
80
|
# Streams the response token by token using Server-Sent Events.
|
|
77
81
|
# Each token arrives as: data: {"token":"..."}
|
|
78
82
|
# End of stream is marked: data: {"done":true}
|
|
79
83
|
#
|
|
80
84
|
# JavaScript client example:
|
|
81
|
-
# const es = new EventSource('/ai/
|
|
85
|
+
# const es = new EventSource('/ai/request_stream?input=Hello');
|
|
82
86
|
# es.onmessage = ({ data }) => {
|
|
83
87
|
# const { token, done } = JSON.parse(data);
|
|
84
88
|
# if (done) { es.close(); return; }
|
|
85
89
|
# document.querySelector('#output').insertAdjacentText('beforeend', token);
|
|
86
90
|
# };
|
|
87
91
|
# ---------------------------------------------------------------------------
|
|
88
|
-
def
|
|
92
|
+
def request_stream
|
|
89
93
|
input = params.require(:input)
|
|
90
94
|
|
|
91
95
|
response.headers["Content-Type"] = "text/event-stream"
|
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.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- the-teacher
|
|
@@ -80,6 +80,7 @@ files:
|
|
|
80
80
|
- lib/active_harness/providers/openai.rb
|
|
81
81
|
- lib/active_harness/providers/openrouter.rb
|
|
82
82
|
- lib/active_harness/providers/perplexity.rb
|
|
83
|
+
- lib/active_harness/providers/vercel.rb
|
|
83
84
|
- lib/active_harness/providers/vertexai.rb
|
|
84
85
|
- lib/active_harness/providers/xai.rb
|
|
85
86
|
- lib/active_harness/railtie.rb
|