coolhand 0.6.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/AGENTS.harness.md +49 -14
- data/CHANGELOG.md +48 -0
- data/README.md +45 -4
- data/docs/configuration.md +27 -2
- data/docs/feedback-links.md +48 -0
- data/docs/log-search.md +102 -0
- data/docs/openai.md +1 -1
- data/docs/template-search.md +46 -4
- data/docs/vertex.md +3 -0
- data/docs/workload-search.md +116 -0
- data/lib/coolhand/api_service.rb +4 -3
- data/lib/coolhand/base_interceptor.rb +42 -7
- data/lib/coolhand/configuration.rb +2 -1
- data/lib/coolhand/default_exclude_api_patterns.yml +11 -0
- data/lib/coolhand/default_intercept_addresses.yml +54 -0
- data/lib/coolhand/errors.rb +2 -1
- data/lib/coolhand/log_queue.rb +179 -0
- data/lib/coolhand/log_service.rb +67 -0
- data/lib/coolhand/net_http_interceptor.rb +144 -109
- data/lib/coolhand/open_ai/batch_result_processor.rb +30 -29
- data/lib/coolhand/open_ai/webhook_validator.rb +2 -1
- data/lib/coolhand/optimization_feedback_link_service.rb +85 -0
- data/lib/coolhand/read_query.rb +61 -0
- data/lib/coolhand/read_requests.rb +26 -13
- data/lib/coolhand/template_service.rb +13 -29
- data/lib/coolhand/version.rb +1 -1
- data/lib/coolhand/vertex/batch_result_processor.rb +9 -1
- data/lib/coolhand/workload_service.rb +46 -0
- data/lib/coolhand/write_requests.rb +47 -0
- data/lib/coolhand.rb +33 -0
- metadata +11 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "uri"
|
|
4
3
|
require_relative "api_service"
|
|
5
4
|
require_relative "pagination"
|
|
5
|
+
require_relative "read_query"
|
|
6
6
|
|
|
7
7
|
module Coolhand
|
|
8
8
|
# Rows stay plain Symbol-keyed Hashes so a field the server adds later is not dropped in transit.
|
|
@@ -14,6 +14,8 @@ module Coolhand
|
|
|
14
14
|
# Not a port of the MCP `search_templates` tool and does not agree with its `log_count`.
|
|
15
15
|
# See docs/template-search.md.
|
|
16
16
|
class TemplateService < ApiService
|
|
17
|
+
include ReadQuery
|
|
18
|
+
|
|
17
19
|
ERROR_NOUN = "Template"
|
|
18
20
|
BLANK_ID_MESSAGE = "get_template: id must be a non-empty template hashid"
|
|
19
21
|
|
|
@@ -23,13 +25,16 @@ module Coolhand
|
|
|
23
25
|
|
|
24
26
|
# Filters, error semantics and server behaviour: docs/template-search.md.
|
|
25
27
|
def search_templates(search: nil, workload_id: nil, status: nil, include_deprecated: nil,
|
|
26
|
-
include_system: nil, page: nil, per: nil)
|
|
28
|
+
include_system: nil, include_metrics: nil, days_back: nil, since: nil, until: nil, page: nil, per: nil)
|
|
27
29
|
query = {
|
|
28
30
|
search: search,
|
|
29
31
|
workload_id: workload_id,
|
|
30
32
|
status: status,
|
|
31
33
|
include_deprecated: include_deprecated,
|
|
32
34
|
include_system: include_system,
|
|
35
|
+
include_metrics: include_metrics,
|
|
36
|
+
days_back: days_back,
|
|
37
|
+
**window_params(since, binding.local_variable_get(:until)),
|
|
33
38
|
page: page,
|
|
34
39
|
per: per
|
|
35
40
|
}.compact
|
|
@@ -44,33 +49,12 @@ module Coolhand
|
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
# Adds `user_prompt_pattern` / `system_prompt_pattern`, which the list omits, and unlike the
|
|
47
|
-
# list reaches deprecated and system templates by id with no opt-in flag.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def list_url(query)
|
|
55
|
-
uri = URI.parse(api_endpoint)
|
|
56
|
-
uri.query = URI.encode_www_form(query) unless query.empty?
|
|
57
|
-
uri
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def resource_url(id)
|
|
61
|
-
raise Error, BLANK_ID_MESSAGE unless id.is_a?(String)
|
|
62
|
-
|
|
63
|
-
trimmed = id.strip
|
|
64
|
-
# A blank id resolves to the index route (bare array, not one template); a bare dot segment
|
|
65
|
-
# retargets the request at another path. Neither would 404, so both are rejected here.
|
|
66
|
-
raise Error, BLANK_ID_MESSAGE if trimmed.empty? || [".", ".."].include?(trimmed)
|
|
67
|
-
|
|
68
|
-
URI.parse("#{api_endpoint}/#{escape_path_segment(trimmed)}")
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Escapes to RFC 3986 unreserved, so an id carrying `/`, `?` or `#` cannot retarget the request.
|
|
72
|
-
def escape_path_segment(value)
|
|
73
|
-
URI::DEFAULT_PARSER.escape(value, /[^A-Za-z0-9\-._~]/)
|
|
52
|
+
# list reaches deprecated and system templates by id with no opt-in flag. Carries `metrics`
|
|
53
|
+
# unless `include_metrics: false`, which also skips the window validation.
|
|
54
|
+
def get_template(id, include_metrics: nil, days_back: nil, since: nil, until: nil)
|
|
55
|
+
query = { include_metrics: include_metrics, days_back: days_back,
|
|
56
|
+
**window_params(since, binding.local_variable_get(:until)) }.compact
|
|
57
|
+
get_json(resource_url(id, BLANK_ID_MESSAGE, query), ERROR_NOUN)
|
|
74
58
|
end
|
|
75
59
|
end
|
|
76
60
|
end
|
data/lib/coolhand/version.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Coolhand
|
|
|
31
31
|
when "JOB_STATE_PENDING", "JOB_STATE_RUNNING", "JOB_STATE_QUEUED"
|
|
32
32
|
Rails.logger.info("[Interceptor] Vertex batch #{batch_info} still processing")
|
|
33
33
|
when "JOB_STATE_SUCCEEDED"
|
|
34
|
-
|
|
34
|
+
LogQueue.submit(lane: :batch) { process_completed_batch_logging_errors(batch_results) }
|
|
35
35
|
when "JOB_STATE_FAILED"
|
|
36
36
|
handle_failed_batch
|
|
37
37
|
else
|
|
@@ -43,6 +43,14 @@ module Coolhand
|
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
|
+
# Runs on the batch lane's worker thread, where an escaping error would only reach
|
|
47
|
+
# Coolhand.log (silenceable) rather than the Rails log `call` reports to.
|
|
48
|
+
def process_completed_batch_logging_errors(batch_results)
|
|
49
|
+
process_completed_batch(batch_results)
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
Rails.logger.error("[Interceptor] Failed to process Vertex batch results for #{batch_info}: #{e.message}")
|
|
52
|
+
end
|
|
53
|
+
|
|
46
54
|
def process_completed_batch(batch_results)
|
|
47
55
|
name = batch_info["name"].to_s.sub(%r{\A/+}, "")
|
|
48
56
|
unless name.match?(VALID_NAME_PATTERN)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "api_service"
|
|
4
|
+
require_relative "pagination"
|
|
5
|
+
require_relative "read_query"
|
|
6
|
+
|
|
7
|
+
module Coolhand
|
|
8
|
+
# Rows stay plain Symbol-keyed Hashes so a field the server adds later is not dropped in transit.
|
|
9
|
+
WorkloadSearchResult = Struct.new(:workloads, :pagination, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
# Read-only. Requires the client's **private** key. There is no per-id endpoint.
|
|
12
|
+
# See docs/workload-search.md.
|
|
13
|
+
class WorkloadService < ApiService
|
|
14
|
+
include ReadQuery
|
|
15
|
+
|
|
16
|
+
ERROR_NOUN = "Workload"
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
super("v2/workloads")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Filters, windows, error semantics and server behaviour: docs/workload-search.md.
|
|
23
|
+
def search_workloads(search: nil, include_archived: nil, include_system: nil, include_templates: nil,
|
|
24
|
+
include_metrics: nil, days_back: nil, since: nil, until: nil, page: nil, per: nil)
|
|
25
|
+
query = {
|
|
26
|
+
search: search,
|
|
27
|
+
include_archived: include_archived,
|
|
28
|
+
include_system: include_system,
|
|
29
|
+
include_templates: include_templates,
|
|
30
|
+
include_metrics: include_metrics,
|
|
31
|
+
days_back: days_back,
|
|
32
|
+
**window_params(since, binding.local_variable_get(:until)),
|
|
33
|
+
page: page,
|
|
34
|
+
per: per
|
|
35
|
+
}.compact
|
|
36
|
+
|
|
37
|
+
workloads, response = get_json_with_headers(list_url(query), ERROR_NOUN)
|
|
38
|
+
raise Error, "#{ERROR_NOUN} response was not a JSON array" unless workloads.is_a?(Array)
|
|
39
|
+
|
|
40
|
+
WorkloadSearchResult.new(
|
|
41
|
+
workloads: workloads,
|
|
42
|
+
pagination: Pagination.from_headers(response, items: workloads, page: page, per: per)
|
|
43
|
+
).freeze
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
require_relative "read_requests"
|
|
8
|
+
|
|
9
|
+
module Coolhand
|
|
10
|
+
# POST/DELETE counterpart of {ReadRequests} for callers that act on the response: raises on any
|
|
11
|
+
# failure where {ApiService#send_request} logs and returns nil. Timeouts are the read ones, for
|
|
12
|
+
# the same server-side statement-bound reason. Mix into an {ApiService} subclass: it relies on
|
|
13
|
+
# `api_key`, `apply_headers` and `format_error_body` from there and the shared HTTP helpers from
|
|
14
|
+
# {ReadRequests}.
|
|
15
|
+
module WriteRequests
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
# Returns the parsed body, or nil for an empty one (204).
|
|
19
|
+
def request_json(verb, url, noun, body: nil)
|
|
20
|
+
raise Error, "#{noun} request failed: an API key is required" unless Coolhand.required_field?(api_key)
|
|
21
|
+
|
|
22
|
+
response = perform_write(verb, url, noun, body)
|
|
23
|
+
|
|
24
|
+
raise_http_error(response, noun) unless response.is_a?(Net::HTTPSuccess)
|
|
25
|
+
|
|
26
|
+
response.body.to_s.strip.empty? ? nil : parse_json_body(response.body, noun)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def perform_write(verb, url, noun, body)
|
|
32
|
+
uri = url.is_a?(URI::Generic) ? url : URI.parse(url.to_s)
|
|
33
|
+
http = build_http(uri)
|
|
34
|
+
|
|
35
|
+
request = Net::HTTP.const_get(verb.to_s.capitalize).new(uri.request_uri)
|
|
36
|
+
apply_headers(request, "Accept" => "application/json", "X-API-Key" => api_key)
|
|
37
|
+
unless body.nil?
|
|
38
|
+
apply_headers(request, "Content-Type" => "application/json")
|
|
39
|
+
request.body = JSON.generate(body).force_encoding("UTF-8")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Coolhand.without_capture { http.request(request) }
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
raise Error, "#{noun} request failed: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/coolhand.rb
CHANGED
|
@@ -10,12 +10,16 @@ require_relative "coolhand/errors"
|
|
|
10
10
|
require_relative "coolhand/version"
|
|
11
11
|
require_relative "coolhand/configuration"
|
|
12
12
|
require_relative "coolhand/collector"
|
|
13
|
+
require_relative "coolhand/log_queue"
|
|
13
14
|
require_relative "coolhand/base_interceptor"
|
|
14
15
|
require_relative "coolhand/net_http_interceptor"
|
|
15
16
|
require_relative "coolhand/api_service"
|
|
16
17
|
require_relative "coolhand/logger_service"
|
|
17
18
|
require_relative "coolhand/feedback_service"
|
|
18
19
|
require_relative "coolhand/template_service"
|
|
20
|
+
require_relative "coolhand/optimization_feedback_link_service"
|
|
21
|
+
require_relative "coolhand/workload_service"
|
|
22
|
+
require_relative "coolhand/log_service"
|
|
19
23
|
require_relative "coolhand/webhook_interceptor"
|
|
20
24
|
|
|
21
25
|
# The main module for the Coolhand gem.
|
|
@@ -86,11 +90,22 @@ module Coolhand
|
|
|
86
90
|
Thread.current[:coolhand_capture_override] = previous
|
|
87
91
|
end
|
|
88
92
|
|
|
93
|
+
# Blocks until log requests queued by async logging have been sent, or the timeout (in
|
|
94
|
+
# seconds) elapses. Returns true if everything was sent. Pending logs are also flushed
|
|
95
|
+
# automatically at process exit.
|
|
96
|
+
def flush(timeout: LogQueue::DEFAULT_FLUSH_TIMEOUT)
|
|
97
|
+
LogQueue.flush(timeout)
|
|
98
|
+
end
|
|
99
|
+
|
|
89
100
|
# A simple logger that respects the 'silent' configuration option.
|
|
90
101
|
def log(message)
|
|
91
102
|
return if configuration.silent
|
|
92
103
|
|
|
93
104
|
puts "COOLHAND: #{message}"
|
|
105
|
+
rescue StandardError
|
|
106
|
+
# Logging must never raise into the host app (e.g. a closed stdout) — least of all from
|
|
107
|
+
# the interceptor's ensure block, where it would replace the host's own response.
|
|
108
|
+
nil
|
|
94
109
|
end
|
|
95
110
|
|
|
96
111
|
# Creates a new FeedbackService instance
|
|
@@ -109,6 +124,24 @@ module Coolhand
|
|
|
109
124
|
TemplateService.new
|
|
110
125
|
end
|
|
111
126
|
|
|
127
|
+
# Creates a new WorkloadService instance, for listing workloads (with optional cost and
|
|
128
|
+
# performance metrics). Needs the private API key.
|
|
129
|
+
def workload_service
|
|
130
|
+
WorkloadService.new
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Creates a new LogService instance, for reading logs back out of Coolhand. Distinct from
|
|
134
|
+
# {logger_service}, which writes them. Needs the private API key.
|
|
135
|
+
def log_service
|
|
136
|
+
LogService.new
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Creates a new OptimizationFeedbackLinkService instance, for linking feedback to an
|
|
140
|
+
# optimization as evidence. Needs the private API key.
|
|
141
|
+
def optimization_feedback_link_service
|
|
142
|
+
OptimizationFeedbackLinkService.new
|
|
143
|
+
end
|
|
144
|
+
|
|
112
145
|
def required_field?(value)
|
|
113
146
|
return false if value.nil?
|
|
114
147
|
return false if value.respond_to?(:empty?) && value.empty?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: coolhand
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Carroll
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: base64
|
|
@@ -48,10 +48,13 @@ files:
|
|
|
48
48
|
- docs/anthropic.md
|
|
49
49
|
- docs/configuration.md
|
|
50
50
|
- docs/elevenlabs.md
|
|
51
|
+
- docs/feedback-links.md
|
|
51
52
|
- docs/feedback.md
|
|
53
|
+
- docs/log-search.md
|
|
52
54
|
- docs/openai.md
|
|
53
55
|
- docs/template-search.md
|
|
54
56
|
- docs/vertex.md
|
|
57
|
+
- docs/workload-search.md
|
|
55
58
|
- lib/coolhand.rb
|
|
56
59
|
- lib/coolhand/api_service.rb
|
|
57
60
|
- lib/coolhand/base_interceptor.rb
|
|
@@ -62,17 +65,23 @@ files:
|
|
|
62
65
|
- lib/coolhand/default_intercept_path_patterns.yml
|
|
63
66
|
- lib/coolhand/errors.rb
|
|
64
67
|
- lib/coolhand/feedback_service.rb
|
|
68
|
+
- lib/coolhand/log_queue.rb
|
|
69
|
+
- lib/coolhand/log_service.rb
|
|
65
70
|
- lib/coolhand/logger_service.rb
|
|
66
71
|
- lib/coolhand/net_http_interceptor.rb
|
|
67
72
|
- lib/coolhand/open_ai/batch_result_processor.rb
|
|
68
73
|
- lib/coolhand/open_ai/webhook_id_store.rb
|
|
69
74
|
- lib/coolhand/open_ai/webhook_validator.rb
|
|
75
|
+
- lib/coolhand/optimization_feedback_link_service.rb
|
|
70
76
|
- lib/coolhand/pagination.rb
|
|
77
|
+
- lib/coolhand/read_query.rb
|
|
71
78
|
- lib/coolhand/read_requests.rb
|
|
72
79
|
- lib/coolhand/template_service.rb
|
|
73
80
|
- lib/coolhand/version.rb
|
|
74
81
|
- lib/coolhand/vertex/batch_result_processor.rb
|
|
75
82
|
- lib/coolhand/webhook_interceptor.rb
|
|
83
|
+
- lib/coolhand/workload_service.rb
|
|
84
|
+
- lib/coolhand/write_requests.rb
|
|
76
85
|
- sig/coolhand/ruby.rbs
|
|
77
86
|
homepage: https://coolhandlabs.com/
|
|
78
87
|
licenses:
|