melaya 0.1.4 → 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/LICENSE +158 -0
- data/README.md +164 -16
- data/lib/melaya/accounts.rb +55 -0
- data/lib/melaya/assistant.rb +41 -0
- data/lib/melaya/auth.rb +137 -0
- data/lib/melaya/backtest.rb +38 -0
- data/lib/melaya/billing.rb +98 -0
- data/lib/melaya/bugs.rb +67 -0
- data/lib/melaya/connector_tools.rb +177 -0
- data/lib/melaya/connectors.rb +144 -0
- data/lib/melaya/credentials.rb +373 -0
- data/lib/melaya/errors.rb +36 -2
- data/lib/melaya/evals.rb +108 -0
- data/lib/melaya/events.rb +425 -0
- data/lib/melaya/hitl.rb +114 -0
- data/lib/melaya/http_client.rb +255 -36
- data/lib/melaya/market.rb +42 -0
- data/lib/melaya/namespaces.rb +119 -0
- data/lib/melaya/phone.rb +98 -0
- data/lib/melaya/pipelines.rb +579 -0
- data/lib/melaya/projects.rb +58 -0
- data/lib/melaya/runner.rb +44 -0
- data/lib/melaya/strategies.rb +15 -0
- data/lib/melaya/stream.rb +5 -3
- data/lib/melaya/team.rb +112 -0
- data/lib/melaya/templates.rb +148 -0
- data/lib/melaya/version.rb +1 -1
- data/lib/melaya.rb +271 -30
- data/melaya.gemspec +10 -5
- metadata +29 -7
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Melaya
|
|
4
|
+
# Pipelines API — overview dashboard, pipeline run listing, traces, and
|
|
5
|
+
# cron-based scheduling.
|
|
6
|
+
#
|
|
7
|
+
# Maps to:
|
|
8
|
+
# /api/v1/private/overview/* — dashboard + run listing
|
|
9
|
+
# /api/v1/private/runs/:id/traces/* — distributed traces
|
|
10
|
+
# /api/v1/private/pipeline-schedule — cron scheduling
|
|
11
|
+
# /api/v1/version — server version (public)
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# runs = melaya.pipelines.list(project: "my-project", limit: 20)
|
|
15
|
+
# melaya.pipelines.upsert_schedule("my-project", "nightly-report",
|
|
16
|
+
# cron: "0 2 * * *")
|
|
17
|
+
class PipelinesAPI
|
|
18
|
+
def initialize(http)
|
|
19
|
+
@http = http
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# ── Overview ───────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
# GET /api/v1/private/overview
|
|
25
|
+
# Dashboard overview: usage stats, active strategies, recent runs.
|
|
26
|
+
def overview
|
|
27
|
+
@http.get("/api/v1/private/overview")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# GET /api/v1/private/overview/model-prices
|
|
31
|
+
# Get pricing data for available AI models.
|
|
32
|
+
def model_prices
|
|
33
|
+
@http.get("/api/v1/private/overview/model-prices")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# GET /api/v1/private/overview/chart
|
|
37
|
+
# Get chart data for overview dashboard (cost/usage over time).
|
|
38
|
+
def chart_data(params = {})
|
|
39
|
+
@http.get("/api/v1/private/overview/chart", params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# GET /api/v1/private/overview/cost-breakdown
|
|
43
|
+
# Get cost breakdown by model/provider.
|
|
44
|
+
def cost_breakdown(params = {})
|
|
45
|
+
@http.get("/api/v1/private/overview/cost-breakdown", params)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# GET /api/v1/private/overview/pipeline-count
|
|
49
|
+
# Count of pipeline runs grouped by status.
|
|
50
|
+
def count
|
|
51
|
+
@http.get("/api/v1/private/overview/pipeline-count")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# GET /api/v1/private/overview/pipelines
|
|
55
|
+
# Paginated list of pipeline runs.
|
|
56
|
+
# @param project [String, nil]
|
|
57
|
+
# @param pipeline_name [String, nil]
|
|
58
|
+
# @param status [String, nil]
|
|
59
|
+
# @param limit [Integer, nil]
|
|
60
|
+
# @param offset [Integer, nil]
|
|
61
|
+
def list(project: nil, pipeline_name: nil, status: nil, limit: nil, offset: nil)
|
|
62
|
+
@http.get("/api/v1/private/overview/pipelines",
|
|
63
|
+
compact("project" => project, "pipelineName" => pipeline_name,
|
|
64
|
+
"status" => status, "limit" => limit,
|
|
65
|
+
"offset" => offset))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# GET /api/v1/private/overview/pipelines/recent
|
|
69
|
+
# Most recent pipeline runs for a dashboard widget.
|
|
70
|
+
def recent
|
|
71
|
+
@http.get("/api/v1/private/overview/pipelines/recent")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ── Traces ─────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
# GET /api/v1/private/runs/:runId/traces
|
|
77
|
+
# List traces for a run (paginated).
|
|
78
|
+
#
|
|
79
|
+
# Returns a paginated envelope, NOT a flat array:
|
|
80
|
+
# {
|
|
81
|
+
# "data" => {
|
|
82
|
+
# "list" => Array<Hash>, # trace summaries
|
|
83
|
+
# "total" => Integer, # total matching traces
|
|
84
|
+
# "page" => Integer,
|
|
85
|
+
# "pageSize" => Integer
|
|
86
|
+
# }
|
|
87
|
+
# }
|
|
88
|
+
#
|
|
89
|
+
# @param run_id [String]
|
|
90
|
+
# @param params [Hash] optional pagination / filter params (page:, pageSize:, ...)
|
|
91
|
+
# @return [Hash] paginated envelope as described above
|
|
92
|
+
def traces(run_id, params = {})
|
|
93
|
+
@http.get("/api/v1/private/runs/#{enc(run_id)}/traces", params)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# GET /api/v1/private/runs/:runId/traces/:traceId
|
|
97
|
+
# Get a single trace by ID.
|
|
98
|
+
def trace(run_id, trace_id)
|
|
99
|
+
@http.get("/api/v1/private/runs/#{enc(run_id)}/traces/#{enc(trace_id)}")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# GET /api/v1/private/runs/:runId/traces/:traceId/stats
|
|
103
|
+
# Get statistics for a specific trace.
|
|
104
|
+
def trace_stats(run_id, trace_id)
|
|
105
|
+
@http.get("/api/v1/private/runs/#{enc(run_id)}/traces/#{enc(trace_id)}/stats")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# DELETE /api/v1/private/runs/:runId/traces
|
|
109
|
+
# Delete all traces (and their spans) for a run by runId.
|
|
110
|
+
# No request body required — the runId in the path identifies the target.
|
|
111
|
+
#
|
|
112
|
+
# @param run_id [String]
|
|
113
|
+
# @return [Hash] { "deletedSpans" => Integer, "requestedTraces" => Integer (optional) }
|
|
114
|
+
def delete_traces(run_id)
|
|
115
|
+
@http.delete("/api/v1/private/runs/#{enc(run_id)}/traces")
|
|
116
|
+
end
|
|
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
|
+
|
|
178
|
+
# ── Schedule ───────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
# GET /api/v1/private/pipeline-schedule
|
|
181
|
+
# List all pipeline schedules accessible to the caller.
|
|
182
|
+
def list_schedules
|
|
183
|
+
@http.get("/api/v1/private/pipeline-schedule")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# GET /api/v1/private/pipeline-schedule/:project/:pipelineName
|
|
187
|
+
# Get schedule status for a pipeline.
|
|
188
|
+
# @param project [String]
|
|
189
|
+
# @param pipeline_name [String]
|
|
190
|
+
def get_schedule(project, pipeline_name)
|
|
191
|
+
@http.get("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# PUT /api/v1/private/pipeline-schedule/:project/:pipelineName
|
|
195
|
+
# Create or update a pipeline schedule (cron expression + optional config).
|
|
196
|
+
# @param project [String]
|
|
197
|
+
# @param pipeline_name [String]
|
|
198
|
+
# @param cron [String] cron expression e.g. "0 2 * * *"
|
|
199
|
+
# @param config [Hash, nil] optional pipeline config overrides
|
|
200
|
+
def upsert_schedule(project, pipeline_name, cron:, config: nil)
|
|
201
|
+
body = compact("cron" => cron, "config" => config)
|
|
202
|
+
@http.put("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}", body)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# POST /api/v1/private/pipeline-schedule/:project/:pipelineName/pause
|
|
206
|
+
# Pause a pipeline schedule.
|
|
207
|
+
def pause_schedule(project, pipeline_name)
|
|
208
|
+
@http.post("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}/pause")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# POST /api/v1/private/pipeline-schedule/:project/:pipelineName/resume
|
|
212
|
+
# Resume a paused pipeline schedule.
|
|
213
|
+
def resume_schedule(project, pipeline_name)
|
|
214
|
+
@http.post("/api/v1/private/pipeline-schedule/#{enc(project)}/#{enc(pipeline_name)}/resume")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# ── Pipeline lifecycle (CRUD + run + outputs + AI build) ──────────────────
|
|
218
|
+
|
|
219
|
+
# GET /api/v1/private/pipelines
|
|
220
|
+
# List all pipeline configs accessible to the caller.
|
|
221
|
+
# Returns { "pipelines" => [...] }.
|
|
222
|
+
def list_pipelines
|
|
223
|
+
@http.get("/api/v1/private/pipelines")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# POST /api/v1/private/pipelines
|
|
227
|
+
# Create a new pipeline config.
|
|
228
|
+
# @param name [String] pipeline name
|
|
229
|
+
# @param project [String] owning project
|
|
230
|
+
# @param description [String, nil]
|
|
231
|
+
# @param config [Hash] additional config keys merged into the request body
|
|
232
|
+
# @return [Hash] created pipeline config
|
|
233
|
+
def create(name:, project:, description: nil, **config)
|
|
234
|
+
body = compact(
|
|
235
|
+
"name" => name,
|
|
236
|
+
"project" => project,
|
|
237
|
+
"description" => description
|
|
238
|
+
).merge(stringify_keys(config))
|
|
239
|
+
@http.post("/api/v1/private/pipelines", body)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# GET /api/v1/private/pipelines/:name
|
|
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
|
+
#
|
|
250
|
+
# @param name [String] pipeline name
|
|
251
|
+
# @param project [String, nil] owning project (disambiguates when multiple projects share a name)
|
|
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")
|
|
259
|
+
def get(name, project: nil)
|
|
260
|
+
params = compact("project" => project)
|
|
261
|
+
@http.get("/api/v1/private/pipelines/#{enc(name)}", params)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# PUT /api/v1/private/pipelines/:name
|
|
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
|
+
#
|
|
287
|
+
# @param name [String] pipeline name
|
|
288
|
+
# @param config [Hash] full pipeline config payload (see +get+)
|
|
289
|
+
# @param project [String, nil] owning project
|
|
290
|
+
# @return [Hash] updated pipeline config
|
|
291
|
+
def update(name, config:, project: nil)
|
|
292
|
+
body = compact("config" => config, "project" => project)
|
|
293
|
+
@http.put("/api/v1/private/pipelines/#{enc(name)}", body)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# DELETE /api/v1/private/pipelines/:name
|
|
297
|
+
# Delete a pipeline config.
|
|
298
|
+
# @param name [String] pipeline name
|
|
299
|
+
# @param project [String, nil] owning project
|
|
300
|
+
# @return [Hash] empty hash on success
|
|
301
|
+
def delete_pipeline(name, project: nil)
|
|
302
|
+
params = compact("project" => project)
|
|
303
|
+
@http.delete("/api/v1/private/pipelines/#{enc(name)}", params)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# POST /api/v1/private/pipelines/:name/run
|
|
307
|
+
# Enqueue a pipeline run.
|
|
308
|
+
# @param name [String] pipeline name
|
|
309
|
+
# @param project [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.
|
|
314
|
+
# @param studio_url [String, nil] override studio URL
|
|
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)
|
|
326
|
+
body = compact(
|
|
327
|
+
"project" => project,
|
|
328
|
+
"executionTarget" => execution_target,
|
|
329
|
+
"studio_url" => studio_url,
|
|
330
|
+
"env_overrides" => env_overrides,
|
|
331
|
+
"run_inputs" => run_inputs
|
|
332
|
+
)
|
|
333
|
+
@http.post("/api/v1/private/pipelines/#{enc(name)}/run", body.empty? ? nil : body)
|
|
334
|
+
end
|
|
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
|
+
|
|
388
|
+
# GET /api/v1/private/pipelines/:name/runs
|
|
389
|
+
# List all run IDs for a pipeline.
|
|
390
|
+
# @param name [String] pipeline name
|
|
391
|
+
# @return [Hash] { "run_ids" => Array<String> }
|
|
392
|
+
def run_ids(name)
|
|
393
|
+
@http.get("/api/v1/private/pipelines/#{enc(name)}/runs")
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# GET /api/v1/private/pipelines/:name/runs/:run_id
|
|
397
|
+
# Get the status of a specific pipeline run.
|
|
398
|
+
# @param name [String] pipeline name
|
|
399
|
+
# @param run_id [String] run identifier
|
|
400
|
+
# @return [Hash] { "runId", "status", "createdAt", "executionTarget", "cost" }
|
|
401
|
+
def run_status(name, run_id)
|
|
402
|
+
@http.get("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}")
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# DELETE /api/v1/private/pipelines/:name/runs/:run_id
|
|
406
|
+
# Cancel an in-progress pipeline run.
|
|
407
|
+
# @param name [String] pipeline name
|
|
408
|
+
# @param run_id [String] run identifier
|
|
409
|
+
# @return [Hash] empty hash on success
|
|
410
|
+
def cancel_run(name, run_id)
|
|
411
|
+
@http.delete("/api/v1/private/pipelines/#{enc(name)}/runs/#{enc(run_id)}")
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# GET /api/v1/private/pipelines/:name/outputs
|
|
415
|
+
# List all output artifacts produced by a pipeline.
|
|
416
|
+
# @param name [String] pipeline name
|
|
417
|
+
# @return artifacts listing
|
|
418
|
+
def outputs(name)
|
|
419
|
+
@http.get("/api/v1/private/pipelines/#{enc(name)}/outputs")
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# GET /api/v1/private/pipelines/:name/outputs/:path
|
|
423
|
+
# Fetch a specific output artifact. Each segment of +path+ is individually
|
|
424
|
+
# URL-encoded so slashes in segment values are preserved as separators.
|
|
425
|
+
# @param name [String] pipeline name
|
|
426
|
+
# @param path [String] artifact path, e.g. "reports/2024-01/summary.json"
|
|
427
|
+
# @param download [Boolean] if true, adds +?download=1+ to trigger a download response
|
|
428
|
+
# @return artifact content or download redirect
|
|
429
|
+
def output(name, path, download: false)
|
|
430
|
+
encoded_path = path.to_s.split("/").map { |seg| enc(seg) }.join("/")
|
|
431
|
+
params = download ? { "download" => 1 } : {}
|
|
432
|
+
@http.get("/api/v1/private/pipelines/#{enc(name)}/outputs/#{encoded_path}", params)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# POST /api/v1/private/pipelines/preview-code
|
|
436
|
+
# Generate a code preview for a pipeline config without saving it.
|
|
437
|
+
# @param config [Hash] pipeline config to preview
|
|
438
|
+
# @return preview payload
|
|
439
|
+
def preview_code(config)
|
|
440
|
+
@http.post("/api/v1/private/pipelines/preview-code", config)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# GET /api/v1/private/pipelines/tools
|
|
444
|
+
# Fetch the registry of tools available to pipeline steps.
|
|
445
|
+
# @return [Hash] tool registry
|
|
446
|
+
def tools
|
|
447
|
+
@http.get("/api/v1/private/pipelines/tools")
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# GET /api/v1/private/pipelines/subagents
|
|
451
|
+
# Fetch the registry of sub-agent definitions available to pipelines.
|
|
452
|
+
# @return [Hash] sub-agent registry
|
|
453
|
+
def subagents
|
|
454
|
+
@http.get("/api/v1/private/pipelines/subagents")
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# POST /api/v1/private/templates/:template_id/instantiate
|
|
458
|
+
# Instantiate a platform template into a new pipeline config.
|
|
459
|
+
# @param template_id [String] the template to instantiate
|
|
460
|
+
# @param name [String] name for the resulting pipeline
|
|
461
|
+
# @param project [String] target project
|
|
462
|
+
# @param overrides [Hash, nil] optional config overrides applied on top of the template defaults
|
|
463
|
+
# @return [Hash] { "pipeline" => config }
|
|
464
|
+
def instantiate_template(template_id, name:, project:, overrides: nil)
|
|
465
|
+
body = compact("name" => name, "project" => project, "overrides" => overrides)
|
|
466
|
+
@http.post("/api/v1/private/templates/#{enc(template_id)}/instantiate", body)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# POST /api/v1/private/ai/build-pipeline/sync
|
|
470
|
+
# Generate a pipeline config from a natural-language brief using AI.
|
|
471
|
+
# @param brief [Hash] free-form brief payload sent to the AI builder
|
|
472
|
+
# @return [Hash] generated pipeline config
|
|
473
|
+
def build_with_ai(brief)
|
|
474
|
+
@http.post("/api/v1/private/ai/build-pipeline/sync", brief)
|
|
475
|
+
end
|
|
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
|
+
|
|
542
|
+
# ── Misc ───────────────────────────────────────────────────────────────────
|
|
543
|
+
|
|
544
|
+
# GET /api/v1/version (public)
|
|
545
|
+
# Get current server version string.
|
|
546
|
+
def server_version
|
|
547
|
+
@http.get("/api/v1/version")
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
private
|
|
551
|
+
|
|
552
|
+
def enc(s)
|
|
553
|
+
URI.encode_www_form_component(s.to_s)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def compact(hash)
|
|
557
|
+
hash.reject { |_, v| v.nil? }
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def stringify_keys(hash)
|
|
561
|
+
hash.transform_keys(&:to_s)
|
|
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
|
|
578
|
+
end
|
|
579
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Melaya
|
|
4
|
+
# Projects API — create and list agent projects.
|
|
5
|
+
#
|
|
6
|
+
# Projects are the top-level namespace for pipelines, connectors, and team
|
|
7
|
+
# membership in the Melaya platform.
|
|
8
|
+
#
|
|
9
|
+
# Maps to /api/v1/private/projects/*.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# projects = melaya.projects.list
|
|
13
|
+
# project = melaya.projects.create(name: "my-project", description: "...")
|
|
14
|
+
# melaya.projects.rename(old_name: "my-project", new_name: "renamed-project")
|
|
15
|
+
class ProjectsAPI
|
|
16
|
+
def initialize(http)
|
|
17
|
+
@http = http
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# GET /api/v1/private/projects
|
|
21
|
+
# List all projects the authenticated user can access
|
|
22
|
+
# (owned projects + projects they are a member of).
|
|
23
|
+
def list
|
|
24
|
+
@http.get("/api/v1/private/projects")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# POST /api/v1/private/projects
|
|
28
|
+
# Create a new agent project.
|
|
29
|
+
# @param name [String]
|
|
30
|
+
# @param description [String, nil]
|
|
31
|
+
def create(name:, description: nil)
|
|
32
|
+
body = compact("name" => name, "description" => description)
|
|
33
|
+
@http.post("/api/v1/private/projects", body)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# PATCH /api/v1/private/projects/rename
|
|
37
|
+
# Rename a project (body: oldName, newName).
|
|
38
|
+
# @param old_name [String]
|
|
39
|
+
# @param new_name [String]
|
|
40
|
+
def rename(old_name:, new_name:)
|
|
41
|
+
@http.patch("/api/v1/private/projects/rename",
|
|
42
|
+
"oldName" => old_name,
|
|
43
|
+
"newName" => new_name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# GET /api/v1/private/projects/runner
|
|
47
|
+
# Get projects list (runner-facing, authed).
|
|
48
|
+
def runner_projects
|
|
49
|
+
@http.get("/api/v1/private/projects/runner")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def compact(hash)
|
|
55
|
+
hash.reject { |_, v| v.nil? }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Melaya
|
|
4
|
+
# Runner API — mint, list, and revoke runner tokens.
|
|
5
|
+
#
|
|
6
|
+
# Runner tokens (mel_run_ prefix) authenticate the Melaya runner CLI process
|
|
7
|
+
# that executes agent pipelines on your infrastructure.
|
|
8
|
+
# The plaintext token is returned only once on creation — store it securely.
|
|
9
|
+
#
|
|
10
|
+
# Maps to /api/v1/private/runner/tokens/*.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# result = melaya.runner.create_token(label: "prod-server-1")
|
|
14
|
+
# token = result["token"] # store securely — shown only once
|
|
15
|
+
#
|
|
16
|
+
# tokens = melaya.runner.list_tokens
|
|
17
|
+
# melaya.runner.revoke_token(tokens.first["id"])
|
|
18
|
+
class RunnerAPI
|
|
19
|
+
def initialize(http)
|
|
20
|
+
@http = http
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# POST /api/v1/private/runner/tokens
|
|
24
|
+
# Mint a new mel_run_ runner token.
|
|
25
|
+
# @param label [String, nil] human-readable label for the token
|
|
26
|
+
def create_token(label: nil)
|
|
27
|
+
body = label ? { "label" => label } : nil
|
|
28
|
+
@http.post("/api/v1/private/runner/tokens", body)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# GET /api/v1/private/runner/tokens
|
|
32
|
+
# List all runner tokens for the caller (masked, with last_seen).
|
|
33
|
+
def list_tokens
|
|
34
|
+
@http.get("/api/v1/private/runner/tokens")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# DELETE /api/v1/private/runner/tokens/:tokenId
|
|
38
|
+
# Revoke a runner token by ID.
|
|
39
|
+
# @param token_id [String]
|
|
40
|
+
def revoke_token(token_id)
|
|
41
|
+
@http.delete("/api/v1/private/runner/tokens/#{URI.encode_www_form_component(token_id.to_s)}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|