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,373 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Melaya
|
|
4
|
+
# Credentials API — store, retrieve, test, and delete secrets and
|
|
5
|
+
# third-party service connections at user scope.
|
|
6
|
+
#
|
|
7
|
+
# Credentials are envelope-encrypted at rest. Use +Melaya::ConnectorsAPI+
|
|
8
|
+
# for project-scoped connector credentials.
|
|
9
|
+
#
|
|
10
|
+
# Maps to /api/v1/private/credentials/*.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# melaya.credentials.set("openai", value: "sk-...")
|
|
14
|
+
# melaya.credentials.test("openai")
|
|
15
|
+
# melaya.credentials.delete("openai")
|
|
16
|
+
class CredentialsAPI
|
|
17
|
+
def initialize(http)
|
|
18
|
+
@http = http
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# GET /api/v1/private/credentials
|
|
22
|
+
# List all stored credentials (services, OAuth connections, env handles).
|
|
23
|
+
def list
|
|
24
|
+
@http.get("/api/v1/private/credentials")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# GET /api/v1/private/credentials/services
|
|
28
|
+
# List connected third-party services for the caller.
|
|
29
|
+
def connected_services
|
|
30
|
+
@http.get("/api/v1/private/credentials/services")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# GET /api/v1/private/credentials/:service
|
|
34
|
+
# Get a stored credential value by service name.
|
|
35
|
+
# @param service [String]
|
|
36
|
+
# @param key [String, nil] optional named key within the service
|
|
37
|
+
def get(service, key: nil)
|
|
38
|
+
params = key ? { "key" => key } : {}
|
|
39
|
+
@http.get("/api/v1/private/credentials/#{enc(service)}", params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# PUT /api/v1/private/credentials/:service
|
|
43
|
+
# Store or update a credential (envelope-encrypted at rest).
|
|
44
|
+
# @param service [String]
|
|
45
|
+
# @param value [String] the secret value
|
|
46
|
+
# @param key [String, nil]
|
|
47
|
+
# @param label [String, nil]
|
|
48
|
+
def set(service, value:, key: nil, label: nil)
|
|
49
|
+
body = compact("value" => value, "key" => key, "label" => label)
|
|
50
|
+
@http.put("/api/v1/private/credentials/#{enc(service)}", body)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# DELETE /api/v1/private/credentials/:service
|
|
54
|
+
# Delete a stored credential by service name.
|
|
55
|
+
# @param service [String]
|
|
56
|
+
def delete(service)
|
|
57
|
+
@http.delete("/api/v1/private/credentials/#{enc(service)}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# POST /api/v1/private/credentials/:service/test
|
|
61
|
+
# Test a stored credential (e.g. validate API key against its service).
|
|
62
|
+
# @param service [String]
|
|
63
|
+
def test(service)
|
|
64
|
+
@http.post("/api/v1/private/credentials/#{enc(service)}/test")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ── Operator profile ───────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
# GET /api/v1/private/credentials/operator-profile
|
|
70
|
+
# Get operator profile (persona config for agent context).
|
|
71
|
+
def get_operator_profile
|
|
72
|
+
@http.get("/api/v1/private/credentials/operator-profile")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# PUT /api/v1/private/credentials/operator-profile
|
|
76
|
+
# Save operator profile.
|
|
77
|
+
# @param profile [Hash]
|
|
78
|
+
def set_operator_profile(profile)
|
|
79
|
+
@http.put("/api/v1/private/credentials/operator-profile", profile)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ── AI models ──────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
# GET /api/v1/private/credentials/models
|
|
85
|
+
# List available AI models across all configured providers.
|
|
86
|
+
# Collapses 19+ provider fan-out into a parameterized query.
|
|
87
|
+
# @param provider [String, nil]
|
|
88
|
+
# @param capability [String, nil]
|
|
89
|
+
def list_models(provider: nil, capability: nil)
|
|
90
|
+
@http.get("/api/v1/private/credentials/models",
|
|
91
|
+
compact("provider" => provider, "capability" => capability))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ── Melaya sub-accounts ────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
# GET /api/v1/private/credentials/melaya-accounts
|
|
97
|
+
# List Melaya sub-accounts available to the caller.
|
|
98
|
+
def melaya_accounts
|
|
99
|
+
@http.get("/api/v1/private/credentials/melaya-accounts")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ── RAG ────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
# POST /api/v1/private/rag/ingest
|
|
105
|
+
# Start a RAG document ingestion job.
|
|
106
|
+
# @param body [Hash]
|
|
107
|
+
def rag_ingest_start(body = {})
|
|
108
|
+
@http.post("/api/v1/private/rag/ingest", body)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# GET /api/v1/private/rag/ingest/:sessionId
|
|
112
|
+
# Poll RAG ingestion job status.
|
|
113
|
+
# @param session_id [String]
|
|
114
|
+
def rag_ingest_status(session_id)
|
|
115
|
+
@http.get("/api/v1/private/rag/ingest/#{enc(session_id)}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# POST /api/v1/private/rag/retrieve
|
|
119
|
+
# Start a RAG retrieval query.
|
|
120
|
+
# @param body [Hash]
|
|
121
|
+
def rag_retrieve_start(body = {})
|
|
122
|
+
@http.post("/api/v1/private/rag/retrieve", body)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# GET /api/v1/private/rag/retrieve/:sessionId
|
|
126
|
+
# Poll RAG retrieval result.
|
|
127
|
+
# @param session_id [String]
|
|
128
|
+
def rag_retrieve_status(session_id)
|
|
129
|
+
@http.get("/api/v1/private/rag/retrieve/#{enc(session_id)}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# ── Folder picker ──────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
# POST /api/v1/private/rag/pick-folder
|
|
135
|
+
# Initiate native folder picker for file ingestion.
|
|
136
|
+
# @param body [Hash]
|
|
137
|
+
def pick_folder_start(body = {})
|
|
138
|
+
@http.post("/api/v1/private/rag/pick-folder", body)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# GET /api/v1/private/rag/pick-folder/:sessionId
|
|
142
|
+
# Poll folder picker result.
|
|
143
|
+
# @param session_id [String]
|
|
144
|
+
def pick_folder_status(session_id)
|
|
145
|
+
@http.get("/api/v1/private/rag/pick-folder/#{enc(session_id)}")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# ── LinkedIn OAuth ─────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
# POST /api/v1/private/credentials/linkedin/connect
|
|
151
|
+
# Start LinkedIn OAuth flow.
|
|
152
|
+
def linkedin_connect_start(body = {})
|
|
153
|
+
@http.post("/api/v1/private/credentials/linkedin/connect", body)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# DELETE /api/v1/private/credentials/linkedin/connect
|
|
157
|
+
# Cancel an in-progress LinkedIn OAuth flow.
|
|
158
|
+
def linkedin_connect_cancel
|
|
159
|
+
@http.delete("/api/v1/private/credentials/linkedin/connect")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# GET /api/v1/private/credentials/linkedin/connect/status
|
|
163
|
+
# Poll LinkedIn OAuth connection status.
|
|
164
|
+
def linkedin_connect_status
|
|
165
|
+
@http.get("/api/v1/private/credentials/linkedin/connect/status")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# ── Luma OAuth ────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
# POST /api/v1/private/credentials/luma/connect
|
|
171
|
+
# Start Luma OAuth flow.
|
|
172
|
+
def luma_connect_start(body = {})
|
|
173
|
+
@http.post("/api/v1/private/credentials/luma/connect", body)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# GET /api/v1/private/credentials/luma/connect/status
|
|
177
|
+
# Poll Luma OAuth connection status.
|
|
178
|
+
def luma_connect_status
|
|
179
|
+
@http.get("/api/v1/private/credentials/luma/connect/status")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# DELETE /api/v1/private/credentials/luma/connect
|
|
183
|
+
# Cancel an in-progress Luma OAuth flow.
|
|
184
|
+
def luma_connect_cancel
|
|
185
|
+
@http.delete("/api/v1/private/credentials/luma/connect")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# GET /api/v1/private/credentials/luma/schema
|
|
189
|
+
# Get the Luma event registration form schema.
|
|
190
|
+
# @param params [Hash]
|
|
191
|
+
def luma_schema(params = {})
|
|
192
|
+
@http.get("/api/v1/private/credentials/luma/schema", params)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ── Google OAuth ───────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
# POST /api/v1/private/credentials/google/oauth
|
|
198
|
+
# Start Google OAuth flow for credential storage.
|
|
199
|
+
def google_oauth_start(body = {})
|
|
200
|
+
@http.post("/api/v1/private/credentials/google/oauth", body)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# ── CLI auth ───────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
# POST /api/v1/private/credentials/cli-auth
|
|
206
|
+
# Start CLI authentication flow (device-code style).
|
|
207
|
+
def cli_auth_start(body = {})
|
|
208
|
+
@http.post("/api/v1/private/credentials/cli-auth", body)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# ── NotebookLM ─────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
# POST /api/v1/private/credentials/notebooklm/login
|
|
214
|
+
# Store NotebookLM credentials.
|
|
215
|
+
def notebooklm_login(body = {})
|
|
216
|
+
@http.post("/api/v1/private/credentials/notebooklm/login", body)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# GET /api/v1/private/credentials/notebooklm/status
|
|
220
|
+
# Check NotebookLM connection status.
|
|
221
|
+
def notebooklm_status
|
|
222
|
+
@http.get("/api/v1/private/credentials/notebooklm/status")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# ── Telegram auth ──────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
# POST /api/v1/private/credentials/telegram/auth
|
|
228
|
+
# Start Telegram user auth (phone number step).
|
|
229
|
+
def telegram_auth_start(body = {})
|
|
230
|
+
@http.post("/api/v1/private/credentials/telegram/auth", body)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# POST /api/v1/private/credentials/telegram/auth/code
|
|
234
|
+
# Submit Telegram SMS verification code.
|
|
235
|
+
def telegram_auth_code(body = {})
|
|
236
|
+
@http.post("/api/v1/private/credentials/telegram/auth/code", body)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# POST /api/v1/private/credentials/telegram/auth/2fa
|
|
240
|
+
# Submit Telegram 2FA password.
|
|
241
|
+
def telegram_auth_2fa(body = {})
|
|
242
|
+
@http.post("/api/v1/private/credentials/telegram/auth/2fa", body)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# ── Telegram user QR login (alternative to the phone-number flow above) ────
|
|
246
|
+
|
|
247
|
+
# POST /api/v1/private/credentials/telegram/auth/qr/start
|
|
248
|
+
# Start Telegram user QR login.
|
|
249
|
+
# @param api_id [Integer]
|
|
250
|
+
# @param api_hash [String]
|
|
251
|
+
# @return [Hash] { "handle" => String, ... } — handle starts with "tgauth_"
|
|
252
|
+
def telegram_qr_start(api_id, api_hash)
|
|
253
|
+
@http.post("/api/v1/private/credentials/telegram/auth/qr/start",
|
|
254
|
+
"api_id" => api_id, "api_hash" => api_hash)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# POST /api/v1/private/credentials/telegram/auth/qr/poll
|
|
258
|
+
# Poll Telegram user QR login.
|
|
259
|
+
# @param handle [String] starts with "tgauth_"
|
|
260
|
+
def telegram_qr_poll(handle)
|
|
261
|
+
@http.post("/api/v1/private/credentials/telegram/auth/qr/poll", "handle" => handle)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# ── Google OAuth (status / defaults / disconnect) ──────────────────────────
|
|
265
|
+
|
|
266
|
+
# GET /api/v1/private/credentials/google/status
|
|
267
|
+
# List the Google OAuth capabilities actually granted to the caller.
|
|
268
|
+
def google_status
|
|
269
|
+
@http.get("/api/v1/private/credentials/google/status")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# PUT /api/v1/private/credentials/google/default
|
|
273
|
+
# Select the connected Google account used by one capability.
|
|
274
|
+
# @param capability [String] e.g. "gmail", "calendar", "drive", "sheets",
|
|
275
|
+
# "docs", "search_console", "youtube", "google_ads", "analytics", "meet", "slides"
|
|
276
|
+
# @param account_id [String] 24-hex-char connected-account id
|
|
277
|
+
def google_set_default(capability, account_id)
|
|
278
|
+
@http.put("/api/v1/private/credentials/google/default",
|
|
279
|
+
"capability" => capability, "accountId" => account_id)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# DELETE /api/v1/private/credentials/google/access
|
|
283
|
+
# Disconnect one Google product, or an entire Google account.
|
|
284
|
+
# @param account_id [String] 24-hex-char connected-account id
|
|
285
|
+
# @param capability [String, nil] omit to disconnect the whole account
|
|
286
|
+
def google_disconnect(account_id, capability: nil)
|
|
287
|
+
body = compact("accountId" => account_id, "capability" => capability)
|
|
288
|
+
@http.delete("/api/v1/private/credentials/google/access", {}, body)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# ── Database connector test (runner-probed) ────────────────────────────────
|
|
292
|
+
|
|
293
|
+
# POST /api/v1/private/credentials/db-test
|
|
294
|
+
# Test a database connector from the user's own runner (reaches
|
|
295
|
+
# IP-allow-listed / VPC hosts a cloud probe never could).
|
|
296
|
+
# @param service [String] one of "postgres", "mysql", "snowflake", "databricks", "sqlite"
|
|
297
|
+
# @param credentials [Hash, nil] freshly-typed credentials to test instead of the stored ones
|
|
298
|
+
# @return [Hash] { "sessionId" => String, ... }
|
|
299
|
+
def db_test_start(service, credentials: nil)
|
|
300
|
+
body = compact("service" => service, "credentials" => credentials)
|
|
301
|
+
@http.post("/api/v1/private/credentials/db-test", body)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# GET /api/v1/private/credentials/db-test/:sessionId
|
|
305
|
+
# Poll a DB connector runner-test result.
|
|
306
|
+
# @param session_id [String]
|
|
307
|
+
def db_test_status(session_id)
|
|
308
|
+
@http.get("/api/v1/private/credentials/db-test/#{enc(session_id)}")
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# ── WhatsApp Embedded Signup ────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
# GET /api/v1/private/credentials/whatsapp/embedded-signup/config
|
|
314
|
+
# WhatsApp Embedded Signup config (appId/configId) for the client SDK.
|
|
315
|
+
def whatsapp_signup_config
|
|
316
|
+
@http.get("/api/v1/private/credentials/whatsapp/embedded-signup/config")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# POST /api/v1/private/credentials/whatsapp/embedded-signup/exchange
|
|
320
|
+
# Exchange a WhatsApp Embedded Signup code for a connected number.
|
|
321
|
+
# @param code [String]
|
|
322
|
+
# @param phone_number_id [String]
|
|
323
|
+
# @param waba_id [String]
|
|
324
|
+
# @param project [String, nil]
|
|
325
|
+
def whatsapp_signup_exchange(code:, phone_number_id:, waba_id:, project: nil)
|
|
326
|
+
body = compact(
|
|
327
|
+
"code" => code,
|
|
328
|
+
"phoneNumberId" => phone_number_id,
|
|
329
|
+
"wabaId" => waba_id,
|
|
330
|
+
"project" => project
|
|
331
|
+
)
|
|
332
|
+
@http.post("/api/v1/private/credentials/whatsapp/embedded-signup/exchange", body)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# ── TikTok ──────────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
# GET /api/v1/private/credentials/tiktok/creator-info
|
|
338
|
+
# The connected TikTok account's creator info (nickname, allowed privacy
|
|
339
|
+
# levels, interaction availability) for the compliant Post-to-TikTok
|
|
340
|
+
# approval UI.
|
|
341
|
+
def tiktok_creator_info
|
|
342
|
+
@http.get("/api/v1/private/credentials/tiktok/creator-info")
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# ── Substack email-link sign-in ─────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
# POST /api/v1/private/credentials/substack/email-link
|
|
348
|
+
# Ask Substack to email a sign-in link.
|
|
349
|
+
# @param email [String]
|
|
350
|
+
def substack_email_link_send(email)
|
|
351
|
+
@http.post("/api/v1/private/credentials/substack/email-link", "email" => email)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# POST /api/v1/private/credentials/substack/email-link/redeem
|
|
355
|
+
# Finish Substack sign-in with the emailed link.
|
|
356
|
+
# @param link [String]
|
|
357
|
+
# @param email [String, nil]
|
|
358
|
+
def substack_email_link_redeem(link, email: nil)
|
|
359
|
+
body = compact("link" => link, "email" => email)
|
|
360
|
+
@http.post("/api/v1/private/credentials/substack/email-link/redeem", body)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
private
|
|
364
|
+
|
|
365
|
+
def enc(s)
|
|
366
|
+
URI.encode_www_form_component(s.to_s)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def compact(hash)
|
|
370
|
+
hash.reject { |_, v| v.nil? }
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
data/lib/melaya/errors.rb
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Melaya
|
|
4
|
-
# Raised for non-2xx REST responses or ok:false envelopes.
|
|
4
|
+
# Raised for non-2xx REST responses or ok:false response envelopes.
|
|
5
|
+
#
|
|
6
|
+
# Two server error shapes are normalised here:
|
|
7
|
+
# 1. { error: 'tier_insufficient', tier: '...' } → HTTP 403
|
|
8
|
+
# 2. { error: '...', message: '...', code: '...' } → other 4xx/5xx
|
|
5
9
|
class MelayaError < StandardError
|
|
6
|
-
|
|
10
|
+
# HTTP status code (Integer), or 0 for transport/WS errors.
|
|
11
|
+
attr_reader :status
|
|
12
|
+
# Short machine-readable error code from the server (String or nil).
|
|
13
|
+
attr_reader :code
|
|
14
|
+
# Raw response body (Hash, String, or nil). Not included in #message so
|
|
15
|
+
# secrets embedded in error payloads do not leak to log lines.
|
|
16
|
+
attr_reader :body
|
|
7
17
|
|
|
8
18
|
def initialize(message, status: 0, code: nil, body: nil)
|
|
9
19
|
super(message)
|
|
@@ -12,4 +22,28 @@ module Melaya
|
|
|
12
22
|
@body = body
|
|
13
23
|
end
|
|
14
24
|
end
|
|
25
|
+
|
|
26
|
+
# Raised when the server returns { error: 'tier_insufficient' } (HTTP 403).
|
|
27
|
+
# Check +#tier+ for the minimum required tier string.
|
|
28
|
+
class TierInsufficientError < MelayaError
|
|
29
|
+
attr_reader :tier
|
|
30
|
+
|
|
31
|
+
def initialize(tier: nil, body: nil)
|
|
32
|
+
@tier = tier
|
|
33
|
+
super("Melaya: feature requires a higher tier (#{tier})", status: 403, code: "tier_insufficient", body: body)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Raised when the server returns HTTP 429 (rate-limited) after all retries
|
|
38
|
+
# are exhausted.
|
|
39
|
+
class RateLimitError < MelayaError
|
|
40
|
+
# Retry-After header value in seconds, if present.
|
|
41
|
+
attr_reader :retry_after
|
|
42
|
+
|
|
43
|
+
def initialize(retry_after: nil, body: nil)
|
|
44
|
+
@retry_after = retry_after
|
|
45
|
+
super("Melaya: rate limit exceeded#{retry_after ? " (retry after #{retry_after}s)" : ""}",
|
|
46
|
+
status: 429, code: "rate_limited", body: body)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
15
49
|
end
|
data/lib/melaya/evals.rb
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Melaya
|
|
4
|
+
# Evals API — list and inspect agent evaluation run results.
|
|
5
|
+
#
|
|
6
|
+
# Maps to /api/v1/private/evals/*.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# runs = melaya.evals.list_runs
|
|
10
|
+
# summary = melaya.evals.summary
|
|
11
|
+
# detail = melaya.evals.run_detail(runs.first["id"])
|
|
12
|
+
class EvalsAPI
|
|
13
|
+
def initialize(http)
|
|
14
|
+
@http = http
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# GET /api/v1/private/evals/runs
|
|
18
|
+
# List eval run results for the caller's tenant.
|
|
19
|
+
def list_runs(params = {})
|
|
20
|
+
@http.get("/api/v1/private/evals/runs", params)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# GET /api/v1/private/evals/summary
|
|
24
|
+
# Get aggregate summary of eval results.
|
|
25
|
+
def summary
|
|
26
|
+
@http.get("/api/v1/private/evals/summary")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# GET /api/v1/private/evals/runs/:runId
|
|
30
|
+
# Get detailed results for a specific eval run.
|
|
31
|
+
# @param run_id [String]
|
|
32
|
+
def run_detail(run_id)
|
|
33
|
+
@http.get("/api/v1/private/evals/runs/#{enc(run_id)}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# GET /api/v1/private/evals/compare
|
|
37
|
+
# Compare results across multiple eval runs.
|
|
38
|
+
# @param params [Hash] e.g. { "runIds" => "id1,id2" }
|
|
39
|
+
def compare(params = {})
|
|
40
|
+
@http.get("/api/v1/private/evals/compare", params)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# GET /api/v1/private/memory/graph
|
|
44
|
+
# Get memory graph visualization data for eval runs.
|
|
45
|
+
def memory_graph(params = {})
|
|
46
|
+
@http.get("/api/v1/private/memory/graph", params)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# GET /api/v1/private/memory/runs/:runId
|
|
50
|
+
# Get memory usage for a specific eval run.
|
|
51
|
+
# @param run_id [String]
|
|
52
|
+
def run_memory(run_id)
|
|
53
|
+
@http.get("/api/v1/private/memory/runs/#{enc(run_id)}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# GET /api/v1/private/memory/crew
|
|
57
|
+
# Get agent crew memory for a pipeline.
|
|
58
|
+
# @param pipeline [String]
|
|
59
|
+
# @param project [String]
|
|
60
|
+
def crew_memory(pipeline:, project:)
|
|
61
|
+
@http.get("/api/v1/private/memory/crew",
|
|
62
|
+
"pipeline" => pipeline,
|
|
63
|
+
"project" => project)
|
|
64
|
+
end
|
|
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
|
+
|
|
92
|
+
# GET /api/v1/private/evals/benchmarks
|
|
93
|
+
# Get benchmark scores across eval runs.
|
|
94
|
+
def benchmarks(params = {})
|
|
95
|
+
@http.get("/api/v1/private/evals/benchmarks", params)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def enc(s)
|
|
101
|
+
URI.encode_www_form_component(s.to_s)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def compact(hash)
|
|
105
|
+
hash.reject { |_, v| v.nil? }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|