melaya 0.2.0 → 0.3.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/README.md +92 -25
- data/lib/melaya/accounts.rb +13 -0
- data/lib/melaya/billing.rb +23 -0
- data/lib/melaya/connector_tools.rb +177 -0
- data/lib/melaya/connectors.rb +73 -0
- data/lib/melaya/credentials.rb +118 -0
- data/lib/melaya/evals.rb +36 -6
- data/lib/melaya/http_client.rb +124 -34
- data/lib/melaya/namespaces.rb +5 -1
- data/lib/melaya/phone.rb +23 -0
- data/lib/melaya/pipelines.rb +246 -9
- data/lib/melaya/team.rb +10 -0
- data/lib/melaya/version.rb +1 -1
- data/lib/melaya.rb +13 -3
- metadata +3 -2
data/lib/melaya/evals.rb
CHANGED
|
@@ -40,29 +40,55 @@ module Melaya
|
|
|
40
40
|
@http.get("/api/v1/private/evals/compare", params)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# GET /api/v1/private/
|
|
43
|
+
# GET /api/v1/private/memory/graph
|
|
44
44
|
# Get memory graph visualization data for eval runs.
|
|
45
45
|
def memory_graph(params = {})
|
|
46
|
-
@http.get("/api/v1/private/
|
|
46
|
+
@http.get("/api/v1/private/memory/graph", params)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# GET /api/v1/private/
|
|
49
|
+
# GET /api/v1/private/memory/runs/:runId
|
|
50
50
|
# Get memory usage for a specific eval run.
|
|
51
51
|
# @param run_id [String]
|
|
52
52
|
def run_memory(run_id)
|
|
53
|
-
@http.get("/api/v1/private/
|
|
53
|
+
@http.get("/api/v1/private/memory/runs/#{enc(run_id)}")
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# GET /api/v1/private/
|
|
56
|
+
# GET /api/v1/private/memory/crew
|
|
57
57
|
# Get agent crew memory for a pipeline.
|
|
58
58
|
# @param pipeline [String]
|
|
59
59
|
# @param project [String]
|
|
60
60
|
def crew_memory(pipeline:, project:)
|
|
61
|
-
@http.get("/api/v1/private/
|
|
61
|
+
@http.get("/api/v1/private/memory/crew",
|
|
62
62
|
"pipeline" => pipeline,
|
|
63
63
|
"project" => project)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# POST /api/v1/private/memory/crew/edit
|
|
67
|
+
# Edit one persisted crew-memory entry (editor/owner-gated, tenant-scoped).
|
|
68
|
+
# @param pipeline [String]
|
|
69
|
+
# @param entry_id [String]
|
|
70
|
+
# @param patch [Hash] any of "topic", "content", "tags" (partial update)
|
|
71
|
+
# @param project [String, nil]
|
|
72
|
+
def edit_crew_memory_entry(pipeline:, entry_id:, patch:, project: nil)
|
|
73
|
+
body = compact(
|
|
74
|
+
"pipeline" => pipeline,
|
|
75
|
+
"entryId" => entry_id,
|
|
76
|
+
"project" => project,
|
|
77
|
+
"patch" => patch
|
|
78
|
+
)
|
|
79
|
+
@http.post("/api/v1/private/memory/crew/edit", body)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# POST /api/v1/private/memory/crew/delete
|
|
83
|
+
# Delete one persisted crew-memory entry (editor/owner-gated, tenant-scoped).
|
|
84
|
+
# @param pipeline [String]
|
|
85
|
+
# @param entry_id [String]
|
|
86
|
+
# @param project [String, nil]
|
|
87
|
+
def delete_crew_memory_entry(pipeline:, entry_id:, project: nil)
|
|
88
|
+
body = compact("pipeline" => pipeline, "entryId" => entry_id, "project" => project)
|
|
89
|
+
@http.post("/api/v1/private/memory/crew/delete", body)
|
|
90
|
+
end
|
|
91
|
+
|
|
66
92
|
# GET /api/v1/private/evals/benchmarks
|
|
67
93
|
# Get benchmark scores across eval runs.
|
|
68
94
|
def benchmarks(params = {})
|
|
@@ -74,5 +100,9 @@ module Melaya
|
|
|
74
100
|
def enc(s)
|
|
75
101
|
URI.encode_www_form_component(s.to_s)
|
|
76
102
|
end
|
|
103
|
+
|
|
104
|
+
def compact(hash)
|
|
105
|
+
hash.reject { |_, v| v.nil? }
|
|
106
|
+
end
|
|
77
107
|
end
|
|
78
108
|
end
|
data/lib/melaya/http_client.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "net/http"
|
|
|
4
4
|
require "uri"
|
|
5
5
|
require "json"
|
|
6
6
|
require "openssl"
|
|
7
|
+
require "securerandom"
|
|
7
8
|
|
|
8
9
|
require_relative "errors"
|
|
9
10
|
|
|
@@ -42,29 +43,96 @@ module Melaya
|
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# ── Public verb helpers ────────────────────────────────────────────────────
|
|
46
|
+
#
|
|
47
|
+
# Every verb accepts an optional trailing +timeout_s+ override for that
|
|
48
|
+
# single call (e.g. a slow RAG ingest job); it defaults to the client's own
|
|
49
|
+
# timeout. NOTE: it is a plain positional parameter, not a keyword — many
|
|
50
|
+
# call sites pass a bare `"key" => value` Hash literal as +body+/+params+,
|
|
51
|
+
# and Ruby 3's keyword/Hash separation would otherwise raise
|
|
52
|
+
# "unknown keyword" on every one of them if this were `timeout_s:`.
|
|
53
|
+
|
|
54
|
+
def get(path, params = {}, timeout_s = nil)
|
|
55
|
+
request(:get, path, params: params, timeout_s: timeout_s)
|
|
56
|
+
end
|
|
45
57
|
|
|
46
|
-
def
|
|
47
|
-
request(:
|
|
58
|
+
def post(path, body = nil, timeout_s = nil)
|
|
59
|
+
request(:post, path, body: body, timeout_s: timeout_s)
|
|
48
60
|
end
|
|
49
61
|
|
|
50
|
-
def
|
|
51
|
-
request(:
|
|
62
|
+
def put(path, body = nil, timeout_s = nil)
|
|
63
|
+
request(:put, path, body: body, timeout_s: timeout_s)
|
|
52
64
|
end
|
|
53
65
|
|
|
54
|
-
def
|
|
55
|
-
request(:
|
|
66
|
+
def patch(path, body = nil, timeout_s = nil)
|
|
67
|
+
request(:patch, path, body: body, timeout_s: timeout_s)
|
|
56
68
|
end
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
# +body+ is optional: some bridged DELETE routes take structured input
|
|
71
|
+
# (e.g. googleDisconnect's { accountId, capability? }) that should not be
|
|
72
|
+
# exposed in a URL, so it travels as a JSON body instead of query params.
|
|
73
|
+
def delete(path, params = {}, body = nil, timeout_s = nil)
|
|
74
|
+
request(:delete, path, params: params, body: body, timeout_s: timeout_s)
|
|
60
75
|
end
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
# GET that returns the RAW response body (String, binary encoding) instead
|
|
78
|
+
# of JSON-parsing it — for binary downloads like +runInputFile+. Errors are
|
|
79
|
+
# still parsed and raised exactly like the JSON verb helpers. Retried like
|
|
80
|
+
# any other GET (idempotent).
|
|
81
|
+
def get_bytes(path, params = {}, timeout_s = nil)
|
|
82
|
+
request(:get, path, params: params, timeout_s: timeout_s, raw: true)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Upload a single file as `multipart/form-data` with one file part named
|
|
86
|
+
# +field_name+. Never retried (a partial multipart re-send could double an
|
|
87
|
+
# upload with side effects), and the response is parsed exactly like the
|
|
88
|
+
# JSON POST helper (same error type).
|
|
89
|
+
#
|
|
90
|
+
# @param path [String]
|
|
91
|
+
# @param query [Hash] query-string params (e.g. { "key" => ..., "project" => ... })
|
|
92
|
+
# @param field_name [String] the multipart field name the server expects (e.g. "file")
|
|
93
|
+
# @param bytes [String] raw file content
|
|
94
|
+
# @param filename [String] filename reported in the part's Content-Disposition
|
|
95
|
+
# @param content_type [String, nil] defaults to "application/octet-stream"
|
|
96
|
+
def post_multipart(path, query, field_name, bytes, filename, content_type = nil)
|
|
97
|
+
uri = build_uri(path, query || {})
|
|
98
|
+
boundary = "MelayaFormBoundary#{SecureRandom.hex(16)}"
|
|
99
|
+
|
|
100
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
101
|
+
http.use_ssl = uri.scheme == "https"
|
|
102
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
103
|
+
http.open_timeout = @timeout_s
|
|
104
|
+
http.read_timeout = @timeout_s
|
|
105
|
+
|
|
106
|
+
req = Net::HTTP::Post.new(uri)
|
|
107
|
+
req["Authorization"] = "Bearer #{@_tok}"
|
|
108
|
+
req["Accept"] = "application/json"
|
|
109
|
+
req["User-Agent"] = "melaya-ruby/#{Melaya::VERSION}"
|
|
110
|
+
req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
|
111
|
+
req.body = multipart_body(boundary, field_name, bytes, filename, content_type)
|
|
112
|
+
|
|
113
|
+
resp = http.request(req)
|
|
114
|
+
parse(resp)
|
|
64
115
|
end
|
|
65
116
|
|
|
66
117
|
private
|
|
67
118
|
|
|
119
|
+
# Builds a single-file multipart/form-data body by hand (no dependency on
|
|
120
|
+
# any multipart-encoding gem).
|
|
121
|
+
def multipart_body(boundary, field_name, bytes, filename, content_type)
|
|
122
|
+
ct = content_type || "application/octet-stream"
|
|
123
|
+
head =
|
|
124
|
+
"--#{boundary}\r\n" \
|
|
125
|
+
"Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{escape_multipart_value(filename)}\"\r\n" \
|
|
126
|
+
"Content-Type: #{ct}\r\n\r\n"
|
|
127
|
+
tail = "\r\n--#{boundary}--\r\n"
|
|
128
|
+
(head.b + bytes.to_s.b + tail.b)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Escapes double quotes / newlines out of a Content-Disposition value.
|
|
132
|
+
def escape_multipart_value(value)
|
|
133
|
+
value.to_s.gsub("\\", "\\\\\\\\").gsub('"', '\\"').tr("\r\n", " ")
|
|
134
|
+
end
|
|
135
|
+
|
|
68
136
|
def build_uri(path, params = {})
|
|
69
137
|
uri = URI.parse("#{@base_uri}#{path}")
|
|
70
138
|
# SECURITY: the credential is never placed in the query string; it is
|
|
@@ -98,14 +166,16 @@ module Melaya
|
|
|
98
166
|
req
|
|
99
167
|
end
|
|
100
168
|
|
|
101
|
-
def request(method, path, params: {}, body: nil)
|
|
169
|
+
def request(method, path, params: {}, body: nil, timeout_s: nil, raw: false)
|
|
102
170
|
uri = build_uri(path, params)
|
|
103
171
|
|
|
172
|
+
eff_timeout = timeout_s ? [timeout_s.to_f, 0.001].max.ceil : @timeout_s
|
|
173
|
+
|
|
104
174
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
105
175
|
http.use_ssl = uri.scheme == "https"
|
|
106
176
|
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
107
|
-
http.open_timeout =
|
|
108
|
-
http.read_timeout =
|
|
177
|
+
http.open_timeout = eff_timeout
|
|
178
|
+
http.read_timeout = eff_timeout
|
|
109
179
|
|
|
110
180
|
# Only GET requests are retried (idempotent); all mutating verbs fail fast.
|
|
111
181
|
retryable = (method == :get)
|
|
@@ -120,7 +190,7 @@ module Melaya
|
|
|
120
190
|
# Snapshot Retry-After before parse() consumes the response object,
|
|
121
191
|
# so we can honour the header even after the MelayaError is raised.
|
|
122
192
|
retry_after_hdr = resp["retry-after"] || resp["Retry-After"]
|
|
123
|
-
parse(resp)
|
|
193
|
+
raw ? parse_raw(resp) : parse(resp)
|
|
124
194
|
rescue MelayaError => e
|
|
125
195
|
if retryable && RETRY_STATUSES.include?(e.status) && attempt <= MAX_GET_RETRIES
|
|
126
196
|
# Build a minimal resp-like object carrying only the header we need,
|
|
@@ -192,26 +262,7 @@ module Melaya
|
|
|
192
262
|
end
|
|
193
263
|
|
|
194
264
|
status = resp.code.to_i
|
|
195
|
-
if status >= 400
|
|
196
|
-
# Two error envelope shapes:
|
|
197
|
-
# 1. { error: 'tier_insufficient', tier: '...' } -> 403
|
|
198
|
-
# 2. { error: '...', message: '...', code: '...' }
|
|
199
|
-
# Extract error code safely — never echo raw body in message
|
|
200
|
-
err_code = data.is_a?(Hash) ? data["error"] : nil
|
|
201
|
-
|
|
202
|
-
if status == 403 && err_code == "tier_insufficient"
|
|
203
|
-
raise TierInsufficientError.new(tier: data.is_a?(Hash) ? data["tier"] : nil, body: data)
|
|
204
|
-
end
|
|
205
|
-
if status == 429
|
|
206
|
-
# Raised here; the GET retry loop above may swallow-and-retry it —
|
|
207
|
-
# callers only see it once retries are exhausted.
|
|
208
|
-
ra = _parse_retry_after_header(resp["retry-after"] || resp["Retry-After"])
|
|
209
|
-
raise RateLimitError.new(retry_after: ra, body: data)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
msg = "Melaya API #{resp.code}" + (err_code ? " (#{err_code})" : "")
|
|
213
|
-
raise MelayaError.new(msg, status: status, code: err_code, body: data)
|
|
214
|
-
end
|
|
265
|
+
raise_for_status!(resp, data, status) if status >= 400
|
|
215
266
|
|
|
216
267
|
# The API may wrap payload in { "ok": false, ... } for request-level failures.
|
|
217
268
|
if data.is_a?(Hash) && data["ok"] == false
|
|
@@ -222,5 +273,44 @@ module Melaya
|
|
|
222
273
|
|
|
223
274
|
data
|
|
224
275
|
end
|
|
276
|
+
|
|
277
|
+
# Like +parse+, but for a raw binary body (a file download): on success
|
|
278
|
+
# the response body is returned unparsed; on error the same JSON error
|
|
279
|
+
# envelopes and exception types apply.
|
|
280
|
+
def parse_raw(resp)
|
|
281
|
+
status = resp.code.to_i
|
|
282
|
+
if status >= 400
|
|
283
|
+
text = resp.body.to_s.strip
|
|
284
|
+
data = begin
|
|
285
|
+
text.empty? ? nil : JSON.parse(text)
|
|
286
|
+
rescue JSON::ParserError
|
|
287
|
+
text
|
|
288
|
+
end
|
|
289
|
+
raise_for_status!(resp, data, status)
|
|
290
|
+
end
|
|
291
|
+
resp.body.to_s
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Shared 4xx/5xx handling for both +parse+ and +parse_raw+. Two error
|
|
295
|
+
# envelope shapes:
|
|
296
|
+
# 1. { error: 'tier_insufficient', tier: '...' } -> 403
|
|
297
|
+
# 2. { error: '...', message: '...', code: '...' }
|
|
298
|
+
# Extract error code safely — never echo raw body in message.
|
|
299
|
+
def raise_for_status!(resp, data, status)
|
|
300
|
+
err_code = data.is_a?(Hash) ? data["error"] : nil
|
|
301
|
+
|
|
302
|
+
if status == 403 && err_code == "tier_insufficient"
|
|
303
|
+
raise TierInsufficientError.new(tier: data.is_a?(Hash) ? data["tier"] : nil, body: data)
|
|
304
|
+
end
|
|
305
|
+
if status == 429
|
|
306
|
+
# Raised here; the GET retry loop above may swallow-and-retry it —
|
|
307
|
+
# callers only see it once retries are exhausted.
|
|
308
|
+
ra = _parse_retry_after_header(resp["retry-after"] || resp["Retry-After"])
|
|
309
|
+
raise RateLimitError.new(retry_after: ra, body: data)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
msg = "Melaya API #{resp.code}" + (err_code ? " (#{err_code})" : "")
|
|
313
|
+
raise MelayaError.new(msg, status: status, code: err_code, body: data)
|
|
314
|
+
end
|
|
225
315
|
end
|
|
226
316
|
end
|
data/lib/melaya/namespaces.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Melaya
|
|
|
7
7
|
# grouping the flat module accessors into logical planes:
|
|
8
8
|
#
|
|
9
9
|
# melaya.trading — market data, account, sim, strategies, backtest, stream, trade
|
|
10
|
-
# melaya.agents — pipelines/runs, hitl, assistant, phone, evals, models
|
|
10
|
+
# melaya.agents — pipelines/runs, hitl, assistant, phone, evals, models, connector_tools
|
|
11
11
|
# melaya.platform — projects, credentials, connectors, billing, team, templates,
|
|
12
12
|
# overview (via pipelines), runner, auth, mfa (via auth),
|
|
13
13
|
# accounts, bugs, events
|
|
@@ -60,6 +60,9 @@ module Melaya
|
|
|
60
60
|
# - +models+ — AI model list (reached via credentials#list_models; this is
|
|
61
61
|
# the CredentialsAPI instance filtered by convention — call
|
|
62
62
|
# +models.list_models(provider: "anthropic")+ etc.)
|
|
63
|
+
# - +connector_tools+ — Call already-connected connector tools directly
|
|
64
|
+
# (Gmail, Slack, Stripe, ...): list, search, describe, test,
|
|
65
|
+
# connect, call, call_status, call_and_wait.
|
|
63
66
|
AgentsNamespace = Struct.new(
|
|
64
67
|
:pipelines,
|
|
65
68
|
:hitl,
|
|
@@ -67,6 +70,7 @@ module Melaya
|
|
|
67
70
|
:phone,
|
|
68
71
|
:evals,
|
|
69
72
|
:models,
|
|
73
|
+
:connector_tools,
|
|
70
74
|
keyword_init: true
|
|
71
75
|
) do
|
|
72
76
|
# +runs+ is an ergonomic alias for +pipelines+ (agents call them "runs").
|
data/lib/melaya/phone.rb
CHANGED
|
@@ -66,10 +66,33 @@ module Melaya
|
|
|
66
66
|
@http.post("/api/v1/private/phone/active-run", "runId" => run_id)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# POST /api/v1/private/phone/apps/grant
|
|
70
|
+
# Grant ONE app into the agent allowlist (atomic append) — e.g. from an
|
|
71
|
+
# in-chat "allow this app" approval card, without replacing the whole list.
|
|
72
|
+
# @param package [String] Android package name
|
|
73
|
+
# @param label [String, nil]
|
|
74
|
+
def grant_app(package, label: nil)
|
|
75
|
+
body = compact("package" => package, "label" => label)
|
|
76
|
+
@http.post("/api/v1/private/phone/apps/grant", body)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# POST /api/v1/private/phone/request-cast
|
|
80
|
+
# Re-cast the phone screen (re-triggers the MediaProjection consent
|
|
81
|
+
# prompt) from the desktop mirror.
|
|
82
|
+
# @param device_id [String, nil]
|
|
83
|
+
def request_cast(device_id: nil)
|
|
84
|
+
body = compact("deviceId" => device_id)
|
|
85
|
+
@http.post("/api/v1/private/phone/request-cast", body.empty? ? nil : body)
|
|
86
|
+
end
|
|
87
|
+
|
|
69
88
|
private
|
|
70
89
|
|
|
71
90
|
def enc(s)
|
|
72
91
|
URI.encode_www_form_component(s.to_s)
|
|
73
92
|
end
|
|
93
|
+
|
|
94
|
+
def compact(hash)
|
|
95
|
+
hash.reject { |_, v| v.nil? }
|
|
96
|
+
end
|
|
74
97
|
end
|
|
75
98
|
end
|
data/lib/melaya/pipelines.rb
CHANGED
|
@@ -115,6 +115,66 @@ module Melaya
|
|
|
115
115
|
@http.delete("/api/v1/private/runs/#{enc(run_id)}/traces")
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# ── Tool-call audit ──────────────────────────────────────────────────────────
|
|
119
|
+
# Project-wide tool-invocation ledger — the same feed behind the Logs page.
|
|
120
|
+
# Every tool call across the project's runs, with HITL/connector/provider
|
|
121
|
+
# provenance. Argument/result previews are 4KB-truncated here; use
|
|
122
|
+
# +tool_call_detail+ for the untruncated pair on one call.
|
|
123
|
+
|
|
124
|
+
# GET /api/v1/private/projects/:project/tool-calls
|
|
125
|
+
# Keyset-paginated; pass the previous page's +nextCursor+ fields back as
|
|
126
|
+
# +before_created_at+/+before_id+ to continue.
|
|
127
|
+
# @param project [String]
|
|
128
|
+
# @param before_created_at [String, nil] keyset cursor (paired with before_id)
|
|
129
|
+
# @param before_id [String, nil]
|
|
130
|
+
# @param limit [Integer, nil] 1..100, default 30
|
|
131
|
+
# @param tool [String, nil] exact tool name
|
|
132
|
+
# @param agent [String, nil] invoking agent name (substring match)
|
|
133
|
+
# @param run_id [String, nil]
|
|
134
|
+
# @param status [String, nil] "ok" | "error"
|
|
135
|
+
# @param search [String, nil] tool-name search
|
|
136
|
+
# @param connector_source [String, nil] "project" | "personal"
|
|
137
|
+
# @param approval [String, nil] "auto" | "approved" | "by:<username>"
|
|
138
|
+
# @param provider [String, nil] AI provider that produced the tool call
|
|
139
|
+
# @param sort [String, nil] "recent" | "oldest" | "slowest" | "fastest"
|
|
140
|
+
# @return [Hash] { "items" => Array<Hash>, "nextCursor" => Hash|nil, "capped" => Boolean }
|
|
141
|
+
def project_tool_calls(project, before_created_at: nil, before_id: nil, limit: nil,
|
|
142
|
+
tool: nil, agent: nil, run_id: nil, status: nil, search: nil,
|
|
143
|
+
connector_source: nil, approval: nil, provider: nil, sort: nil)
|
|
144
|
+
params = compact(
|
|
145
|
+
"beforeCreatedAt" => before_created_at,
|
|
146
|
+
"beforeId" => before_id,
|
|
147
|
+
"limit" => limit,
|
|
148
|
+
"tool" => tool,
|
|
149
|
+
"agent" => agent,
|
|
150
|
+
"runId" => run_id,
|
|
151
|
+
"status" => status,
|
|
152
|
+
"search" => search,
|
|
153
|
+
"connectorSource" => connector_source,
|
|
154
|
+
"approval" => approval,
|
|
155
|
+
"provider" => provider,
|
|
156
|
+
"sort" => sort
|
|
157
|
+
)
|
|
158
|
+
@http.get("/api/v1/private/projects/#{enc(project)}/tool-calls", params)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# GET /api/v1/private/projects/:project/tool-calls/facets
|
|
162
|
+
# Distinct tools (with call counts) and agents seen in the project's
|
|
163
|
+
# tool-call ledger — powers the audit UI's filter dropdowns.
|
|
164
|
+
# @param project [String]
|
|
165
|
+
# @return [Hash] { "tools" => [{ "name" => String, "count" => Integer }], "agents" => Array<String> }
|
|
166
|
+
def project_tool_call_facets(project)
|
|
167
|
+
@http.get("/api/v1/private/projects/#{enc(project)}/tool-calls/facets")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# GET /api/v1/private/runs/:runId/tool-calls/:spanId
|
|
171
|
+
# Full (untruncated) arguments + result for a single tool-call span.
|
|
172
|
+
# @param run_id [String]
|
|
173
|
+
# @param span_id [String]
|
|
174
|
+
def tool_call_detail(run_id, span_id)
|
|
175
|
+
@http.get("/api/v1/private/runs/#{enc(run_id)}/tool-calls/#{enc(span_id)}")
|
|
176
|
+
end
|
|
177
|
+
|
|
118
178
|
# ── Schedule ───────────────────────────────────────────────────────────────
|
|
119
179
|
|
|
120
180
|
# GET /api/v1/private/pipeline-schedule
|
|
@@ -180,19 +240,52 @@ module Melaya
|
|
|
180
240
|
end
|
|
181
241
|
|
|
182
242
|
# GET /api/v1/private/pipelines/:name
|
|
183
|
-
# Fetch a single pipeline
|
|
243
|
+
# Fetch a single pipeline by name.
|
|
244
|
+
#
|
|
245
|
+
# Returns an ENVELOPE, not a bare config:
|
|
246
|
+
# { "name" => String, "client" => ..., "config" => Hash, "code" => String, "docs" => ... }
|
|
247
|
+
# To edit and save, mutate +envelope["config"]+ and pass THAT to +update+ —
|
|
248
|
+
# see the example below.
|
|
249
|
+
#
|
|
184
250
|
# @param name [String] pipeline name
|
|
185
251
|
# @param project [String, nil] owning project (disambiguates when multiple projects share a name)
|
|
186
|
-
# @return [Hash]
|
|
252
|
+
# @return [Hash] envelope: { "name", "client", "config", "code", "docs" }
|
|
253
|
+
#
|
|
254
|
+
# @example Edit one agent's model, then save
|
|
255
|
+
# envelope = melaya.pipelines.get("daily-digest", project: "acme")
|
|
256
|
+
# config = envelope["config"]
|
|
257
|
+
# config["steps"][0]["agent"]["model"] = { "provider" => "anthropic", "name" => "claude-opus-4-8" }
|
|
258
|
+
# melaya.pipelines.update("daily-digest", config: config, project: "acme")
|
|
187
259
|
def get(name, project: nil)
|
|
188
260
|
params = compact("project" => project)
|
|
189
261
|
@http.get("/api/v1/private/pipelines/#{enc(name)}", params)
|
|
190
262
|
end
|
|
191
263
|
|
|
192
264
|
# PUT /api/v1/private/pipelines/:name
|
|
193
|
-
# Replace a pipeline's config.
|
|
265
|
+
# Replace a pipeline's config. Pass the FULL config Hash — typically
|
|
266
|
+
# +envelope["config"]+ returned by +get+, mutated in place. This is the
|
|
267
|
+
# path for editing a per-agent prompt/instruction or swapping a model on
|
|
268
|
+
# one or all agents.
|
|
269
|
+
#
|
|
270
|
+
# The run is generated ONLY from +config["steps"]+ — a top-level
|
|
271
|
+
# +config["agents"]+ list alone produces an EMPTY pipeline. Every agent a
|
|
272
|
+
# step runs must be embedded inline on that step, e.g.:
|
|
273
|
+
# { "kind" => "agent", "agent" => {
|
|
274
|
+
# "name" => "researcher", "role" => "...", "instruction" => "...",
|
|
275
|
+
# "model" => { "provider" => "anthropic", "name" => "claude-sonnet-4-6" },
|
|
276
|
+
# "agent_tools" => [...], "human_approval_tools" => [...] } }
|
|
277
|
+
# There is no +prompt+ field — the two prompt fields are +instruction+
|
|
278
|
+
# (the task) and, optionally, +system_prompt_override+.
|
|
279
|
+
#
|
|
280
|
+
# Other config fields worth knowing:
|
|
281
|
+
# "hitl_mode" — "safe" (default) | "autonomous" | "payments_only".
|
|
282
|
+
# Only "safe" honours each agent's +human_approval_tools+.
|
|
283
|
+
# "connector_source" — "personal" | "project"
|
|
284
|
+
# "force_local_runner" — Boolean
|
|
285
|
+
# "inputs" — Array of declared run-input fields (see +run+)
|
|
286
|
+
#
|
|
194
287
|
# @param name [String] pipeline name
|
|
195
|
-
# @param config [Hash] full pipeline config payload
|
|
288
|
+
# @param config [Hash] full pipeline config payload (see +get+)
|
|
196
289
|
# @param project [String, nil] owning project
|
|
197
290
|
# @return [Hash] updated pipeline config
|
|
198
291
|
def update(name, config:, project: nil)
|
|
@@ -214,20 +307,84 @@ module Melaya
|
|
|
214
307
|
# Enqueue a pipeline run.
|
|
215
308
|
# @param name [String] pipeline name
|
|
216
309
|
# @param project [String, nil]
|
|
217
|
-
# @param execution_target [String, nil]
|
|
310
|
+
# @param execution_target [String, nil] used ONLY for the tier check made
|
|
311
|
+
# at enqueue time. Where the run actually EXECUTES is decided by the
|
|
312
|
+
# pipeline's own stored config (local model providers / +force_local_runner+),
|
|
313
|
+
# not by this value.
|
|
218
314
|
# @param studio_url [String, nil] override studio URL
|
|
219
|
-
# @param env_overrides [Hash, nil] environment variable overrides
|
|
220
|
-
#
|
|
221
|
-
|
|
315
|
+
# @param env_overrides [Hash, nil] per-run environment variable overrides,
|
|
316
|
+
# layered over the caller's stored credentials. +MEL_*+ and +MELAYA_*+
|
|
317
|
+
# keys are always stripped server-side — they can never be overridden
|
|
318
|
+
# from the client.
|
|
319
|
+
# @param run_inputs [Hash, nil] free-form run inputs:
|
|
320
|
+
# +{ brief: String, values: { key => value_or_file_ref } }+.
|
|
321
|
+
# A file value inside +values+ may be +{ "file_id" => ... }+ (from
|
|
322
|
+
# +upload_run_file+), +{ "url" => ... }+ (≤25 MB, https only), or
|
|
323
|
+
# +{ "base64" => ..., "name" => ... }+ (≤7 MB).
|
|
324
|
+
# @return [Hash] { "run_id" => String, "queued" => Boolean, "run_inputs" => Hash (optional echo) }
|
|
325
|
+
def run(name, project: nil, execution_target: nil, studio_url: nil, env_overrides: nil, run_inputs: nil)
|
|
222
326
|
body = compact(
|
|
223
327
|
"project" => project,
|
|
224
328
|
"executionTarget" => execution_target,
|
|
225
329
|
"studio_url" => studio_url,
|
|
226
|
-
"env_overrides" => env_overrides
|
|
330
|
+
"env_overrides" => env_overrides,
|
|
331
|
+
"run_inputs" => run_inputs
|
|
227
332
|
)
|
|
228
333
|
@http.post("/api/v1/private/pipelines/#{enc(name)}/run", body.empty? ? nil : body)
|
|
229
334
|
end
|
|
230
335
|
|
|
336
|
+
# POST /api/v1/private/pipelines/:name/run-files?key=...[&project=...]
|
|
337
|
+
# Upload a file for a LATER run (before calling +run+), as
|
|
338
|
+
# +multipart/form-data+ with a single field "file". Returns
|
|
339
|
+
# +{ "file_id" => String, ... }+ — single-use, valid 24 h. Reference it
|
|
340
|
+
# from +run+'s +run_inputs+ as +values: { <key> => { "file_id" => file_id } }+.
|
|
341
|
+
# @param name [String] pipeline name
|
|
342
|
+
# @param key [String] the declared run-input key this file is for
|
|
343
|
+
# @param file [String, IO] raw file bytes, or an IO/File-like object (must respond to +#read+)
|
|
344
|
+
# @param project [String, nil]
|
|
345
|
+
# @param filename [String, nil] defaults to the file's own name, else "file"
|
|
346
|
+
# @param content_type [String, nil] defaults to "application/octet-stream"
|
|
347
|
+
# @return [Hash] { "file_id" => String, ... }
|
|
348
|
+
def upload_run_file(name, key, file, project: nil, filename: nil, content_type: nil)
|
|
349
|
+
bytes, fname = file_payload(file, filename)
|
|
350
|
+
query = compact("key" => key, "project" => project)
|
|
351
|
+
@http.post_multipart(
|
|
352
|
+
"/api/v1/private/pipelines/#{enc(name)}/run-files", query,
|
|
353
|
+
"file", bytes, fname, content_type
|
|
354
|
+
)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# GET /api/v1/private/pipelines/:name/runs/:runId/inputs
|
|
358
|
+
# What a run was started with (brief, values, files echo).
|
|
359
|
+
# @param name [String] pipeline name
|
|
360
|
+
# @param run_id [String] 16 hex-char run id
|
|
361
|
+
# @return [Hash]
|
|
362
|
+
def run_inputs(name, run_id)
|
|
363
|
+
@http.get("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}/inputs")
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# GET /api/v1/private/pipelines/:name/runs/:runId/inputs/files/:index
|
|
367
|
+
# Download one input file attached to a run.
|
|
368
|
+
#
|
|
369
|
+
# Returns RAW BYTES — do not JSON-parse the result.
|
|
370
|
+
#
|
|
371
|
+
# @param name [String] pipeline name
|
|
372
|
+
# @param run_id [String] 16 hex-char run id
|
|
373
|
+
# @param index [Integer] file index, 0..99
|
|
374
|
+
# @return [String] raw binary file content
|
|
375
|
+
def run_input_file(name, run_id, index)
|
|
376
|
+
@http.get_bytes("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}/inputs/files/#{index}")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# GET /api/v1/private/pipelines/:name/runs/:runId/active
|
|
380
|
+
# Liveness poll for a run (cloud-spawn process presence).
|
|
381
|
+
# @param name [String] pipeline name
|
|
382
|
+
# @param run_id [String] run identifier
|
|
383
|
+
# @return [Hash] { "active" => Boolean }
|
|
384
|
+
def run_active(name, run_id)
|
|
385
|
+
@http.get("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}/active")
|
|
386
|
+
end
|
|
387
|
+
|
|
231
388
|
# GET /api/v1/private/pipelines/:name/runs
|
|
232
389
|
# List all run IDs for a pipeline.
|
|
233
390
|
# @param name [String] pipeline name
|
|
@@ -317,6 +474,71 @@ module Melaya
|
|
|
317
474
|
@http.post("/api/v1/private/ai/build-pipeline/sync", brief)
|
|
318
475
|
end
|
|
319
476
|
|
|
477
|
+
# ── Static-context documents ────────────────────────────────────────────────
|
|
478
|
+
# Files an agent reads as part of its context (never chunked/embedded).
|
|
479
|
+
# See also "RAG (retrieval) documents" below for the embedded-search store.
|
|
480
|
+
|
|
481
|
+
# GET /api/v1/private/pipelines/:name/docs
|
|
482
|
+
# List static-context documents attached to a pipeline.
|
|
483
|
+
# @param name [String] pipeline name
|
|
484
|
+
def list_docs(name)
|
|
485
|
+
@http.get("/api/v1/private/pipelines/#{enc(name)}/docs")
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# POST /api/v1/private/pipelines/:name/docs
|
|
489
|
+
# Upload one static-context document as +multipart/form-data+ (field
|
|
490
|
+
# "file"). Allowed extensions: .txt .md .pdf .csv .json .docx .doc .pptx .xlsx.
|
|
491
|
+
# @param name [String] pipeline name
|
|
492
|
+
# @param file [String, IO] raw file bytes, or an IO/File-like object
|
|
493
|
+
# @param filename [String, nil] defaults to the file's own name, else "file"
|
|
494
|
+
# @param content_type [String, nil] defaults to "application/octet-stream"
|
|
495
|
+
def upload_doc(name, file, filename: nil, content_type: nil)
|
|
496
|
+
bytes, fname = file_payload(file, filename)
|
|
497
|
+
@http.post_multipart("/api/v1/private/pipelines/#{enc(name)}/docs", {}, "file", bytes, fname, content_type)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# DELETE /api/v1/private/pipelines/:name/docs/:filename
|
|
501
|
+
# Remove one static-context document.
|
|
502
|
+
# @param name [String] pipeline name
|
|
503
|
+
# @param filename [String]
|
|
504
|
+
def delete_doc(name, filename)
|
|
505
|
+
@http.delete("/api/v1/private/pipelines/#{enc(name)}/docs/#{enc(filename)}")
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# ── RAG (retrieval) documents ────────────────────────────────────────────────
|
|
509
|
+
# Files chunked and embedded into the pipeline's own retrieval store, for
|
|
510
|
+
# agents that search over a document set rather than reading it whole.
|
|
511
|
+
|
|
512
|
+
# POST /api/v1/private/pipelines/:name/docs/retrieval
|
|
513
|
+
# Upload one retrieval-mode document as +multipart/form-data+ (field "file").
|
|
514
|
+
# @param name [String] pipeline name
|
|
515
|
+
# @param file [String, IO] raw file bytes, or an IO/File-like object
|
|
516
|
+
# @param filename [String, nil] defaults to the file's own name, else "file"
|
|
517
|
+
# @param content_type [String, nil] defaults to "application/octet-stream"
|
|
518
|
+
def upload_retrieval_doc(name, file, filename: nil, content_type: nil)
|
|
519
|
+
bytes, fname = file_payload(file, filename)
|
|
520
|
+
@http.post_multipart("/api/v1/private/pipelines/#{enc(name)}/docs/retrieval", {}, "file", bytes, fname, content_type)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# POST /api/v1/private/pipelines/:name/docs/retrieval/ingest
|
|
524
|
+
# Embed changed retrieval documents with the pipeline's configured
|
|
525
|
+
# embedder. Can take minutes, so this defaults to a 300 s request timeout —
|
|
526
|
+
# pass +timeout_s:+ to override.
|
|
527
|
+
# @param name [String] pipeline name
|
|
528
|
+
# @param body [Hash] request body (empty by default)
|
|
529
|
+
# @param timeout_s [Numeric] per-call timeout override (default 300)
|
|
530
|
+
def ingest_retrieval(name, body: {}, timeout_s: 300)
|
|
531
|
+
@http.post("/api/v1/private/pipelines/#{enc(name)}/docs/retrieval/ingest", body, timeout_s)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# DELETE /api/v1/private/pipelines/:name/docs/retrieval/:filename
|
|
535
|
+
# Remove one retrieval-mode document (and its chunks).
|
|
536
|
+
# @param name [String] pipeline name
|
|
537
|
+
# @param filename [String]
|
|
538
|
+
def delete_retrieval_doc(name, filename)
|
|
539
|
+
@http.delete("/api/v1/private/pipelines/#{enc(name)}/docs/retrieval/#{enc(filename)}")
|
|
540
|
+
end
|
|
541
|
+
|
|
320
542
|
# ── Misc ───────────────────────────────────────────────────────────────────
|
|
321
543
|
|
|
322
544
|
# GET /api/v1/version (public)
|
|
@@ -338,5 +560,20 @@ module Melaya
|
|
|
338
560
|
def stringify_keys(hash)
|
|
339
561
|
hash.transform_keys(&:to_s)
|
|
340
562
|
end
|
|
563
|
+
|
|
564
|
+
# Resolves a caller-supplied +file+ (raw bytes String, or an IO/File-like
|
|
565
|
+
# object responding to +#read+) into [bytes, filename] for a multipart
|
|
566
|
+
# upload. +filename_override+ wins when given; otherwise an IO's own
|
|
567
|
+
# +#path+ basename is used, falling back to "file".
|
|
568
|
+
def file_payload(file, filename_override)
|
|
569
|
+
if file.respond_to?(:read)
|
|
570
|
+
bytes = file.read
|
|
571
|
+
name = filename_override || (file.respond_to?(:path) ? File.basename(file.path) : "file")
|
|
572
|
+
else
|
|
573
|
+
bytes = file.to_s
|
|
574
|
+
name = filename_override || "file"
|
|
575
|
+
end
|
|
576
|
+
[bytes, name]
|
|
577
|
+
end
|
|
341
578
|
end
|
|
342
579
|
end
|