coolhand 0.7.0 → 0.9.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 +42 -1
- data/README.md +60 -2
- data/Rakefile +7 -0
- data/docs/configuration.md +19 -0
- data/docs/declared_workloads.md +156 -0
- data/docs/feedback-links.md +48 -0
- data/docs/log-search.md +102 -0
- data/docs/openai.md +5 -1
- data/docs/template-search.md +46 -4
- data/docs/vertex.md +1 -0
- data/docs/workload-search.md +116 -0
- data/lib/coolhand/api_service.rb +9 -4
- data/lib/coolhand/base_interceptor.rb +7 -5
- data/lib/coolhand/configuration.rb +16 -2
- data/lib/coolhand/declarable.rb +178 -0
- data/lib/coolhand/declared_context.rb +106 -0
- data/lib/coolhand/errors.rb +2 -1
- data/lib/coolhand/log_queue.rb +179 -0
- data/lib/coolhand/log_service.rb +61 -0
- data/lib/coolhand/logger_service.rb +10 -18
- data/lib/coolhand/net_http_interceptor.rb +145 -139
- data/lib/coolhand/open_ai/batch_result_processor.rb +30 -29
- data/lib/coolhand/open_ai/webhook_validator.rb +27 -15
- data/lib/coolhand/optimization_feedback_link_service.rb +85 -0
- data/lib/coolhand/rails_auto_declare.rb +55 -0
- data/lib/coolhand/railtie.rb +11 -0
- data/lib/coolhand/read_query.rb +75 -0
- data/lib/coolhand/read_requests.rb +26 -13
- data/lib/coolhand/response_warnings.rb +39 -0
- data/lib/coolhand/template_service.rb +33 -33
- data/lib/coolhand/version.rb +1 -1
- data/lib/coolhand/vertex/batch_result_processor.rb +9 -1
- data/lib/coolhand/workload_service.rb +50 -0
- data/lib/coolhand/write_requests.rb +61 -0
- data/lib/coolhand.rb +63 -12
- metadata +17 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "api_service"
|
|
4
|
+
require_relative "pagination"
|
|
5
|
+
require_relative "read_query"
|
|
6
|
+
require_relative "write_requests"
|
|
7
|
+
|
|
8
|
+
module Coolhand
|
|
9
|
+
# Rows stay plain Symbol-keyed Hashes so a field the server adds later is not dropped in transit.
|
|
10
|
+
WorkloadSearchResult = Struct.new(:workloads, :pagination, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
# Lists workloads and upserts them by key. Requires the client's **private** key. There is no per-id
|
|
13
|
+
# endpoint. See docs/workload-search.md and docs/declared_workloads.md.
|
|
14
|
+
class WorkloadService < ApiService
|
|
15
|
+
include ReadQuery
|
|
16
|
+
include WriteRequests
|
|
17
|
+
|
|
18
|
+
ERROR_NOUN = "Workload"
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
super("v2/workloads")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Filters, windows, error semantics and server behaviour: docs/workload-search.md.
|
|
25
|
+
def search_workloads(search: nil, include_archived: nil, include_system: nil, include_templates: nil,
|
|
26
|
+
include_metrics: nil, days_back: nil, since: nil, until: nil, page: nil, per: nil)
|
|
27
|
+
query = {
|
|
28
|
+
search: search,
|
|
29
|
+
include_archived: include_archived,
|
|
30
|
+
include_system: include_system,
|
|
31
|
+
include_templates: include_templates,
|
|
32
|
+
include_metrics: include_metrics,
|
|
33
|
+
days_back: days_back,
|
|
34
|
+
**window_params(since, binding.local_variable_get(:until)),
|
|
35
|
+
page: page,
|
|
36
|
+
per: per
|
|
37
|
+
}.compact
|
|
38
|
+
|
|
39
|
+
fetch_list(query, WorkloadSearchResult, :workloads, page: page, per: per)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Creates the workload with this `key`, or updates the existing one. The key is immutable; a new
|
|
43
|
+
# `name` renames the display name. Returns the workload Hash (the server answers 201 on create,
|
|
44
|
+
# 200 on update). Raises {Coolhand::HttpError} on a non-2xx response. See docs/declared_workloads.md.
|
|
45
|
+
def upsert(key:, name: nil, description: nil, reporter: nil)
|
|
46
|
+
body = { name: name, description: description, reporter: reporter }.compact
|
|
47
|
+
request_json(:put, by_key_url("upsert", key), ERROR_NOUN, body: body)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
DOT_SEGMENTS = [".", ".."].freeze
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
# Returns the parsed body, or nil for an empty one (204).
|
|
21
|
+
def request_json(verb, url, noun, body: nil)
|
|
22
|
+
raise Error, "#{noun} request failed: an API key is required" unless Coolhand.required_field?(api_key)
|
|
23
|
+
|
|
24
|
+
response = perform_write(verb, url, noun, body)
|
|
25
|
+
|
|
26
|
+
raise_http_error(response, noun) unless response.is_a?(Net::HTTPSuccess)
|
|
27
|
+
|
|
28
|
+
response.body.to_s.strip.empty? ? nil : parse_json_body(response.body, noun)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `<endpoint>/by_key/<key>` for the upsert routes, which are globs: a key like "Parser#call/squish:L2"
|
|
32
|
+
# keeps its slashes and each segment is escaped, so `#` or `?` in a key cannot retarget the request.
|
|
33
|
+
def by_key_url(method_name, key)
|
|
34
|
+
trimmed = key.is_a?(String) ? key.strip : ""
|
|
35
|
+
segments = trimmed.split("/", -1)
|
|
36
|
+
if trimmed.empty? || segments.any? { |segment| segment.empty? || DOT_SEGMENTS.include?(segment) }
|
|
37
|
+
raise Error, "#{method_name}: key must be a non-empty string without empty or dot path segments"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
URI.parse("#{api_endpoint}/by_key/#{segments.map { |segment| escape_path_segment(segment) }.join('/')}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def perform_write(verb, url, noun, body)
|
|
46
|
+
uri = url.is_a?(URI::Generic) ? url : URI.parse(url.to_s)
|
|
47
|
+
http = build_http(uri)
|
|
48
|
+
|
|
49
|
+
request = Net::HTTP.const_get(verb.to_s.capitalize).new(uri.request_uri)
|
|
50
|
+
apply_headers(request, "Accept" => "application/json", "X-API-Key" => api_key)
|
|
51
|
+
unless body.nil?
|
|
52
|
+
apply_headers(request, "Content-Type" => "application/json")
|
|
53
|
+
request.body = JSON.generate(body).force_encoding("UTF-8")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Coolhand.without_capture { http.request(request) }
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
raise Error, "#{noun} request failed: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/coolhand.rb
CHANGED
|
@@ -10,13 +10,22 @@ 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/declared_context"
|
|
14
|
+
require_relative "coolhand/declarable"
|
|
15
|
+
require_relative "coolhand/rails_auto_declare"
|
|
16
|
+
require_relative "coolhand/log_queue"
|
|
13
17
|
require_relative "coolhand/base_interceptor"
|
|
14
18
|
require_relative "coolhand/net_http_interceptor"
|
|
15
19
|
require_relative "coolhand/api_service"
|
|
16
20
|
require_relative "coolhand/logger_service"
|
|
17
21
|
require_relative "coolhand/feedback_service"
|
|
18
22
|
require_relative "coolhand/template_service"
|
|
23
|
+
require_relative "coolhand/optimization_feedback_link_service"
|
|
24
|
+
require_relative "coolhand/workload_service"
|
|
25
|
+
require_relative "coolhand/log_service"
|
|
19
26
|
require_relative "coolhand/webhook_interceptor"
|
|
27
|
+
# Only when Rails is already loaded; the gem has no Rails dependency.
|
|
28
|
+
require_relative "coolhand/railtie" if defined?(Rails::Railtie)
|
|
20
29
|
|
|
21
30
|
# The main module for the Coolhand gem.
|
|
22
31
|
# It provides the configuration interface and initializes the patching.
|
|
@@ -31,6 +40,7 @@ module Coolhand
|
|
|
31
40
|
def reset_configuration!
|
|
32
41
|
@configuration = Configuration.new
|
|
33
42
|
Thread.current[:coolhand_capture_override] = nil
|
|
43
|
+
DeclaredContext.clear!
|
|
34
44
|
end
|
|
35
45
|
|
|
36
46
|
# Provides a block to configure the gem.
|
|
@@ -70,20 +80,32 @@ module Coolhand
|
|
|
70
80
|
end
|
|
71
81
|
end
|
|
72
82
|
|
|
73
|
-
def without_capture
|
|
74
|
-
|
|
75
|
-
Thread.current[:coolhand_capture_override] = false
|
|
76
|
-
yield
|
|
77
|
-
ensure
|
|
78
|
-
Thread.current[:coolhand_capture_override] = previous
|
|
83
|
+
def without_capture(&block)
|
|
84
|
+
override_capture(false, &block)
|
|
79
85
|
end
|
|
80
86
|
|
|
81
|
-
def with_capture
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
def with_capture(&block)
|
|
88
|
+
override_capture(true, &block)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Declares the workload/template that LLM calls captured inside the block belong to. Nested
|
|
92
|
+
# blocks override per field (`reporter`, `workload`, `template`), innermost wins. `workload` and
|
|
93
|
+
# `template` are Hashes with `id:` (an existing record) or `key:` (find-or-created), plus optional
|
|
94
|
+
# `name:` (and `description:` on a workload). See docs/declared_workloads.md.
|
|
95
|
+
#
|
|
96
|
+
# Example:
|
|
97
|
+
# Coolhand.with_context(reporter: "squishling", workload: { key: "Parser#call" }) { client.chat(...) }
|
|
98
|
+
def with_context(reporter: nil, workload: nil, template: nil, &block)
|
|
99
|
+
raise ArgumentError, "Coolhand.with_context requires a block" unless block
|
|
100
|
+
|
|
101
|
+
DeclaredContext.with(reporter: reporter, workload: workload, template: template, &block)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Blocks until log requests queued by async logging have been sent, or the timeout (in
|
|
105
|
+
# seconds) elapses. Returns true if everything was sent. Pending logs are also flushed
|
|
106
|
+
# automatically at process exit.
|
|
107
|
+
def flush(timeout: LogQueue::DEFAULT_FLUSH_TIMEOUT)
|
|
108
|
+
LogQueue.flush(timeout)
|
|
87
109
|
end
|
|
88
110
|
|
|
89
111
|
# A simple logger that respects the 'silent' configuration option.
|
|
@@ -113,6 +135,24 @@ module Coolhand
|
|
|
113
135
|
TemplateService.new
|
|
114
136
|
end
|
|
115
137
|
|
|
138
|
+
# Creates a new WorkloadService instance, for listing workloads (with optional cost and
|
|
139
|
+
# performance metrics). Needs the private API key.
|
|
140
|
+
def workload_service
|
|
141
|
+
WorkloadService.new
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Creates a new LogService instance, for reading logs back out of Coolhand. Distinct from
|
|
145
|
+
# {logger_service}, which writes them. Needs the private API key.
|
|
146
|
+
def log_service
|
|
147
|
+
LogService.new
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Creates a new OptimizationFeedbackLinkService instance, for linking feedback to an
|
|
151
|
+
# optimization as evidence. Needs the private API key.
|
|
152
|
+
def optimization_feedback_link_service
|
|
153
|
+
OptimizationFeedbackLinkService.new
|
|
154
|
+
end
|
|
155
|
+
|
|
116
156
|
def required_field?(value)
|
|
117
157
|
return false if value.nil?
|
|
118
158
|
return false if value.respond_to?(:empty?) && value.empty?
|
|
@@ -120,5 +160,16 @@ module Coolhand
|
|
|
120
160
|
|
|
121
161
|
true
|
|
122
162
|
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
# Forces capture on or off for the block on this thread, restoring the previous override after.
|
|
167
|
+
def override_capture(enabled)
|
|
168
|
+
previous = Thread.current[:coolhand_capture_override]
|
|
169
|
+
Thread.current[:coolhand_capture_override] = enabled
|
|
170
|
+
yield
|
|
171
|
+
ensure
|
|
172
|
+
Thread.current[:coolhand_capture_override] = previous
|
|
173
|
+
end
|
|
123
174
|
end
|
|
124
175
|
end
|
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.9.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-18 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: base64
|
|
@@ -47,32 +47,47 @@ files:
|
|
|
47
47
|
- SECURITY.md
|
|
48
48
|
- docs/anthropic.md
|
|
49
49
|
- docs/configuration.md
|
|
50
|
+
- docs/declared_workloads.md
|
|
50
51
|
- docs/elevenlabs.md
|
|
52
|
+
- docs/feedback-links.md
|
|
51
53
|
- docs/feedback.md
|
|
54
|
+
- docs/log-search.md
|
|
52
55
|
- docs/openai.md
|
|
53
56
|
- docs/template-search.md
|
|
54
57
|
- docs/vertex.md
|
|
58
|
+
- docs/workload-search.md
|
|
55
59
|
- lib/coolhand.rb
|
|
56
60
|
- lib/coolhand/api_service.rb
|
|
57
61
|
- lib/coolhand/base_interceptor.rb
|
|
58
62
|
- lib/coolhand/collector.rb
|
|
59
63
|
- lib/coolhand/configuration.rb
|
|
64
|
+
- lib/coolhand/declarable.rb
|
|
65
|
+
- lib/coolhand/declared_context.rb
|
|
60
66
|
- lib/coolhand/default_exclude_api_patterns.yml
|
|
61
67
|
- lib/coolhand/default_intercept_addresses.yml
|
|
62
68
|
- lib/coolhand/default_intercept_path_patterns.yml
|
|
63
69
|
- lib/coolhand/errors.rb
|
|
64
70
|
- lib/coolhand/feedback_service.rb
|
|
71
|
+
- lib/coolhand/log_queue.rb
|
|
72
|
+
- lib/coolhand/log_service.rb
|
|
65
73
|
- lib/coolhand/logger_service.rb
|
|
66
74
|
- lib/coolhand/net_http_interceptor.rb
|
|
67
75
|
- lib/coolhand/open_ai/batch_result_processor.rb
|
|
68
76
|
- lib/coolhand/open_ai/webhook_id_store.rb
|
|
69
77
|
- lib/coolhand/open_ai/webhook_validator.rb
|
|
78
|
+
- lib/coolhand/optimization_feedback_link_service.rb
|
|
70
79
|
- lib/coolhand/pagination.rb
|
|
80
|
+
- lib/coolhand/rails_auto_declare.rb
|
|
81
|
+
- lib/coolhand/railtie.rb
|
|
82
|
+
- lib/coolhand/read_query.rb
|
|
71
83
|
- lib/coolhand/read_requests.rb
|
|
84
|
+
- lib/coolhand/response_warnings.rb
|
|
72
85
|
- lib/coolhand/template_service.rb
|
|
73
86
|
- lib/coolhand/version.rb
|
|
74
87
|
- lib/coolhand/vertex/batch_result_processor.rb
|
|
75
88
|
- lib/coolhand/webhook_interceptor.rb
|
|
89
|
+
- lib/coolhand/workload_service.rb
|
|
90
|
+
- lib/coolhand/write_requests.rb
|
|
76
91
|
- sig/coolhand/ruby.rbs
|
|
77
92
|
homepage: https://coolhandlabs.com/
|
|
78
93
|
licenses:
|