@promptev/client 0.2.0 → 1.0.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.
- package/dist/cjs/index.cjs +429 -145
- package/dist/esm/index.d.ts +460 -53
- package/dist/esm/index.js +427 -144
- package/package.json +4 -3
- package/readme.md +392 -288
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,55 +1,115 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* @promptev/client — the official JavaScript/TypeScript client for Promptev.
|
|
4
|
+
*
|
|
5
|
+
* 1.0 talks to Promptev's `/api/v1` surface, authenticated with a workspace
|
|
6
|
+
* API key (`fk_…`) sent as a Bearer token. The key is bound to ONE project and
|
|
7
|
+
* carries its own retrieval identity, so what a caller may read is part of the
|
|
8
|
+
* credential rather than a parameter on the call.
|
|
9
|
+
*
|
|
10
|
+
* Two methods:
|
|
11
|
+
* - `chat()` — stream a turn from an agent (SSE).
|
|
12
|
+
* - `uploadDocument()` — put files into the key's project corpus.
|
|
13
|
+
*/
|
|
3
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.PromptevClient = exports.NetworkError = exports.ServerError = exports.RateLimitError = exports.NotFoundError = exports.AuthenticationError = exports.ValidationError = exports.PromptevError = void 0;
|
|
15
|
+
exports.PromptevClient = exports.NetworkError = exports.ServerError = exports.InsufficientCreditsError = exports.RateLimitError = exports.NotFoundError = exports.AuthenticationError = exports.ValidationError = exports.PromptevError = exports.MAX_FILES_PER_RUN = exports.MAX_UPLOAD_BYTES = exports.MAX_FILES_PER_REQUEST = exports.MAX_MESSAGE_CHARS = exports.DEFAULT_BASE_URL = void 0;
|
|
16
|
+
// ── Configuration ───────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* The default host. NOT `api.promptev.ai` — that host still serves the
|
|
19
|
+
* previous product and would answer 404 for every `/api/v1` path here.
|
|
20
|
+
* FLIP THIS the day `api.promptev.ai` is repointed at the console backend.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Where the client talks by default.
|
|
24
|
+
*
|
|
25
|
+
* `api.promptev.ai` is the API's permanent home. At the time 1.0 was written
|
|
26
|
+
* that host still served the PREVIOUS product, and it is being repointed at
|
|
27
|
+
* the new backend — so this default is correct for where things are going and
|
|
28
|
+
* must not ship to npm before the DNS moves. Point `baseUrl` at
|
|
29
|
+
* `https://console.promptev.ai` in the meantime, or at your own host if you
|
|
30
|
+
* run Promptev yourself.
|
|
31
|
+
*/
|
|
32
|
+
exports.DEFAULT_BASE_URL = "https://api.promptev.ai";
|
|
33
|
+
/** The server's own ceiling on one message (`ApiChatBody.message`). */
|
|
34
|
+
exports.MAX_MESSAGE_CHARS = 20000;
|
|
35
|
+
/** `uploads.MAX_FILES_PER_REQUEST` — files in ONE upload request. */
|
|
36
|
+
exports.MAX_FILES_PER_REQUEST = 20;
|
|
37
|
+
/** `file_types.MAX_UPLOAD_BYTES` — 25 MB per file. */
|
|
38
|
+
exports.MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
39
|
+
/** Files ONE orchestration run may carry — the same ceiling a chat turn has.
|
|
40
|
+
* Server-side: `run_inputs.MAX_FILE_PARTS`. */
|
|
41
|
+
exports.MAX_FILES_PER_RUN = 5;
|
|
5
42
|
// ── Errors ──────────────────────────────────────────────
|
|
6
43
|
class PromptevError extends Error {
|
|
7
|
-
constructor(message, statusCode, responseText) {
|
|
44
|
+
constructor(message, statusCode, responseText, field) {
|
|
8
45
|
super(message);
|
|
9
46
|
this.name = "PromptevError";
|
|
10
47
|
this.statusCode = statusCode;
|
|
11
48
|
this.responseText = responseText;
|
|
49
|
+
this.field = field;
|
|
12
50
|
}
|
|
13
51
|
}
|
|
14
52
|
exports.PromptevError = PromptevError;
|
|
53
|
+
/** 400/422 — the request itself was wrong. `field` says which part. */
|
|
15
54
|
class ValidationError extends PromptevError {
|
|
16
|
-
constructor(
|
|
17
|
-
super(
|
|
55
|
+
constructor() {
|
|
56
|
+
super(...arguments);
|
|
18
57
|
this.name = "ValidationError";
|
|
19
58
|
}
|
|
20
59
|
}
|
|
21
60
|
exports.ValidationError = ValidationError;
|
|
61
|
+
/** 401/403 — the key is missing, malformed, revoked or expired. */
|
|
22
62
|
class AuthenticationError extends PromptevError {
|
|
23
|
-
constructor(
|
|
24
|
-
super(
|
|
63
|
+
constructor() {
|
|
64
|
+
super(...arguments);
|
|
25
65
|
this.name = "AuthenticationError";
|
|
26
66
|
}
|
|
27
67
|
}
|
|
28
68
|
exports.AuthenticationError = AuthenticationError;
|
|
69
|
+
/**
|
|
70
|
+
* 404 — no such agent, OR an agent your key cannot reach, OR one that has
|
|
71
|
+
* never been published. All three answer identically on purpose: a key holder
|
|
72
|
+
* learns nothing about projects it was not given.
|
|
73
|
+
*/
|
|
29
74
|
class NotFoundError extends PromptevError {
|
|
30
|
-
constructor(
|
|
31
|
-
super(
|
|
75
|
+
constructor() {
|
|
76
|
+
super(...arguments);
|
|
32
77
|
this.name = "NotFoundError";
|
|
33
78
|
}
|
|
34
79
|
}
|
|
35
80
|
exports.NotFoundError = NotFoundError;
|
|
81
|
+
/** 429 — too many requests. `retryAfter` is the server's own answer, in seconds. */
|
|
36
82
|
class RateLimitError extends PromptevError {
|
|
37
|
-
constructor(message, statusCode, responseText) {
|
|
38
|
-
super(message, statusCode, responseText);
|
|
83
|
+
constructor(message, statusCode, responseText, field, retryAfter) {
|
|
84
|
+
super(message, statusCode, responseText, field);
|
|
39
85
|
this.name = "RateLimitError";
|
|
86
|
+
this.retryAfter = retryAfter;
|
|
40
87
|
}
|
|
41
88
|
}
|
|
42
89
|
exports.RateLimitError = RateLimitError;
|
|
90
|
+
/**
|
|
91
|
+
* 402 — the workspace is out of credits. Not a rate limit: waiting does not
|
|
92
|
+
* fix it, a top-up does.
|
|
93
|
+
*/
|
|
94
|
+
class InsufficientCreditsError extends PromptevError {
|
|
95
|
+
constructor() {
|
|
96
|
+
super(...arguments);
|
|
97
|
+
this.name = "InsufficientCreditsError";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.InsufficientCreditsError = InsufficientCreditsError;
|
|
101
|
+
/** 5xx — the server failed, after the client's retries were exhausted. */
|
|
43
102
|
class ServerError extends PromptevError {
|
|
44
|
-
constructor(
|
|
45
|
-
super(
|
|
103
|
+
constructor() {
|
|
104
|
+
super(...arguments);
|
|
46
105
|
this.name = "ServerError";
|
|
47
106
|
}
|
|
48
107
|
}
|
|
49
108
|
exports.ServerError = ServerError;
|
|
109
|
+
/** The request never got an answer: DNS, connection, abort, or the client timeout. */
|
|
50
110
|
class NetworkError extends PromptevError {
|
|
51
|
-
constructor(
|
|
52
|
-
super(
|
|
111
|
+
constructor() {
|
|
112
|
+
super(...arguments);
|
|
53
113
|
this.name = "NetworkError";
|
|
54
114
|
}
|
|
55
115
|
}
|
|
@@ -60,67 +120,84 @@ const DEFAULT_MAX_RETRIES = 2;
|
|
|
60
120
|
const DEFAULT_BACKOFF = 500; // ms
|
|
61
121
|
class PromptevClient {
|
|
62
122
|
constructor(config) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
123
|
+
if (!config || !config.apiKey || !String(config.apiKey).trim()) {
|
|
124
|
+
throw new ValidationError("Pass your Promptev API key: new PromptevClient({ apiKey: 'fk_…' }).", undefined, undefined, "apiKey");
|
|
125
|
+
}
|
|
126
|
+
this.apiKey = String(config.apiKey).trim();
|
|
127
|
+
this.baseUrl = (config.baseUrl ?? exports.DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
128
|
+
this.headers = { ...(config.headers ?? {}) };
|
|
69
129
|
this.timeout = config.timeout ?? 30000;
|
|
70
130
|
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
71
131
|
}
|
|
72
132
|
// ── Error handling ──────────────────────────────────
|
|
73
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Every refusal on `/api/v1` is `{"error": {"message", "field"?}}`. The
|
|
135
|
+
* older `{"detail": …}` shape is still read as a fallback, so a proxy or an
|
|
136
|
+
* un-migrated route can never produce an empty message.
|
|
137
|
+
*/
|
|
138
|
+
static raiseForStatus(status, text, headers) {
|
|
74
139
|
if (status < 400)
|
|
75
140
|
return;
|
|
76
|
-
let detail;
|
|
141
|
+
let detail = text || "Unknown error";
|
|
142
|
+
let field;
|
|
77
143
|
try {
|
|
78
144
|
const body = JSON.parse(text);
|
|
79
|
-
|
|
80
|
-
|
|
145
|
+
const err = body?.error;
|
|
146
|
+
if (err && typeof err === "object") {
|
|
147
|
+
if (typeof err.message === "string")
|
|
148
|
+
detail = err.message;
|
|
149
|
+
if (typeof err.field === "string")
|
|
150
|
+
field = err.field;
|
|
151
|
+
}
|
|
152
|
+
else if (typeof body?.detail === "string") {
|
|
153
|
+
detail = body.detail;
|
|
154
|
+
}
|
|
81
155
|
}
|
|
82
156
|
catch {
|
|
83
|
-
|
|
157
|
+
/* not JSON — the raw body is the best message there is */
|
|
84
158
|
}
|
|
85
|
-
if (status === 400)
|
|
86
|
-
throw new ValidationError(detail, status, text);
|
|
87
|
-
if (status === 401)
|
|
88
|
-
throw new AuthenticationError(detail, status, text);
|
|
89
|
-
if (status ===
|
|
90
|
-
throw new
|
|
159
|
+
if (status === 400 || status === 422)
|
|
160
|
+
throw new ValidationError(detail, status, text, field);
|
|
161
|
+
if (status === 401 || status === 403)
|
|
162
|
+
throw new AuthenticationError(detail, status, text, field);
|
|
163
|
+
if (status === 402)
|
|
164
|
+
throw new InsufficientCreditsError(detail, status, text, field);
|
|
91
165
|
if (status === 404)
|
|
92
|
-
throw new NotFoundError(detail, status, text);
|
|
93
|
-
if (status === 429)
|
|
94
|
-
|
|
166
|
+
throw new NotFoundError(detail, status, text, field);
|
|
167
|
+
if (status === 429) {
|
|
168
|
+
const raw = headers?.get("retry-after");
|
|
169
|
+
const seconds = raw != null && raw !== "" ? Number(raw) : NaN;
|
|
170
|
+
throw new RateLimitError(detail, status, text, field, Number.isFinite(seconds) ? seconds : undefined);
|
|
171
|
+
}
|
|
95
172
|
if (status >= 500)
|
|
96
|
-
throw new ServerError(detail, status, text);
|
|
97
|
-
throw new PromptevError(detail, status, text);
|
|
173
|
+
throw new ServerError(detail, status, text, field);
|
|
174
|
+
throw new PromptevError(detail, status, text, field);
|
|
175
|
+
}
|
|
176
|
+
authHeaders(extra) {
|
|
177
|
+
// Authorization goes on AFTER the caller's headers: a stray `Authorization`
|
|
178
|
+
// in `config.headers` must not be able to displace the key silently.
|
|
179
|
+
return {
|
|
180
|
+
...this.headers,
|
|
181
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
182
|
+
...(extra ?? {}),
|
|
183
|
+
};
|
|
98
184
|
}
|
|
99
185
|
// ── Retry helper ────────────────────────────────────
|
|
100
186
|
async fetchWithRetry(url, init) {
|
|
101
187
|
let lastError = null;
|
|
102
188
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
103
189
|
try {
|
|
104
|
-
const
|
|
105
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
106
|
-
const resp = await fetch(url, {
|
|
107
|
-
...init,
|
|
108
|
-
signal: controller.signal,
|
|
109
|
-
});
|
|
110
|
-
clearTimeout(timeoutId);
|
|
190
|
+
const resp = await this.fetchOnce(url, init);
|
|
111
191
|
if (!RETRYABLE_STATUS.has(resp.status) || attempt === this.maxRetries) {
|
|
112
192
|
return resp;
|
|
113
193
|
}
|
|
114
194
|
lastError = new ServerError(`Server error (${resp.status})`, resp.status);
|
|
115
195
|
}
|
|
116
196
|
catch (err) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
lastError = new NetworkError(`Network error: ${err.message}`);
|
|
122
|
-
}
|
|
123
|
-
if (attempt === this.maxRetries)
|
|
197
|
+
lastError = err;
|
|
198
|
+
// A caller who aborted wants the call to STOP, not to be tried again.
|
|
199
|
+
const caller = init.signal;
|
|
200
|
+
if (caller?.aborted || attempt === this.maxRetries)
|
|
124
201
|
throw lastError;
|
|
125
202
|
}
|
|
126
203
|
const delay = DEFAULT_BACKOFF * Math.pow(2, attempt);
|
|
@@ -128,8 +205,50 @@ class PromptevClient {
|
|
|
128
205
|
}
|
|
129
206
|
throw lastError;
|
|
130
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* One `fetch`, with the client timeout applied to the RESPONSE only.
|
|
210
|
+
*
|
|
211
|
+
* The timer is cleared the moment the response resolves, which is what lets
|
|
212
|
+
* a `chat()` stream run for minutes — a tool call, or a human deciding an
|
|
213
|
+
* approval, is not a stalled request.
|
|
214
|
+
*/
|
|
215
|
+
async fetchOnce(url, init) {
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
const caller = init.signal;
|
|
218
|
+
const onAbort = () => controller.abort();
|
|
219
|
+
if (caller) {
|
|
220
|
+
if (caller.aborted)
|
|
221
|
+
controller.abort();
|
|
222
|
+
else
|
|
223
|
+
caller.addEventListener("abort", onAbort, { once: true });
|
|
224
|
+
}
|
|
225
|
+
let timedOut = false;
|
|
226
|
+
const timeoutId = setTimeout(() => {
|
|
227
|
+
timedOut = true;
|
|
228
|
+
controller.abort();
|
|
229
|
+
}, this.timeout);
|
|
230
|
+
try {
|
|
231
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err?.name === "AbortError") {
|
|
235
|
+
if (timedOut)
|
|
236
|
+
throw new NetworkError("Request timed out");
|
|
237
|
+
throw new NetworkError("Request was aborted");
|
|
238
|
+
}
|
|
239
|
+
throw new NetworkError(`Network error: ${err?.message ?? err}`);
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
clearTimeout(timeoutId);
|
|
243
|
+
if (caller)
|
|
244
|
+
caller.removeEventListener("abort", onAbort);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
131
247
|
// ── SSE parser ──────────────────────────────────────
|
|
132
248
|
async *parseSSE(response) {
|
|
249
|
+
if (!response.body) {
|
|
250
|
+
throw new NetworkError("The server sent no response body to stream.");
|
|
251
|
+
}
|
|
133
252
|
const reader = response.body.getReader();
|
|
134
253
|
const decoder = new TextDecoder();
|
|
135
254
|
let buffer = "";
|
|
@@ -141,137 +260,302 @@ class PromptevClient {
|
|
|
141
260
|
buffer += decoder.decode(value, { stream: true });
|
|
142
261
|
const lines = buffer.split("\n");
|
|
143
262
|
buffer = lines.pop() || "";
|
|
144
|
-
for (const
|
|
263
|
+
for (const raw of lines) {
|
|
264
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
145
265
|
if (!line.startsWith("data: "))
|
|
146
266
|
continue;
|
|
267
|
+
let data;
|
|
147
268
|
try {
|
|
148
|
-
|
|
149
|
-
const event = {
|
|
150
|
-
type: data.type || "unknown",
|
|
151
|
-
output: data.output || "",
|
|
152
|
-
raw: data,
|
|
153
|
-
};
|
|
154
|
-
yield event;
|
|
155
|
-
if (event.type === "done" || event.type === "error")
|
|
156
|
-
return;
|
|
269
|
+
data = JSON.parse(line.slice(6));
|
|
157
270
|
}
|
|
158
271
|
catch {
|
|
159
|
-
continue;
|
|
272
|
+
continue; // a frame we cannot parse is never a reason to stop
|
|
160
273
|
}
|
|
274
|
+
if (!data || typeof data !== "object" || typeof data.type !== "string")
|
|
275
|
+
continue;
|
|
276
|
+
const event = data;
|
|
277
|
+
yield event;
|
|
278
|
+
if (event.type === "done" || event.type === "error")
|
|
279
|
+
return;
|
|
161
280
|
}
|
|
162
281
|
}
|
|
163
282
|
}
|
|
164
283
|
finally {
|
|
284
|
+
// Cancel, not just release: a caller who breaks out of the loop early
|
|
285
|
+
// (or stops at `done`) would otherwise leave the connection open until
|
|
286
|
+
// the socket is collected.
|
|
287
|
+
try {
|
|
288
|
+
await reader.cancel();
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
/* already closed */
|
|
292
|
+
}
|
|
165
293
|
reader.releaseLock();
|
|
166
294
|
}
|
|
167
295
|
}
|
|
168
296
|
// ── Agent API ───────────────────────────────────────
|
|
169
297
|
/**
|
|
170
|
-
*
|
|
298
|
+
* Send a message to one of your project's agents and stream the answer.
|
|
171
299
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
300
|
+
* This is the whole conversational surface: it starts a thread or continues
|
|
301
|
+
* one, streams every event the turn produces, and ends with `done` (or
|
|
302
|
+
* `error`). There is no separate "start" call and no session token — the API
|
|
303
|
+
* key IS the credential, and `conversationId` is the thread.
|
|
174
304
|
*
|
|
175
|
-
* @param
|
|
176
|
-
*
|
|
177
|
-
* @
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* @returns `{ response, sessionId }` — pass `sessionId` back on the next call
|
|
181
|
-
* to continue the same conversation.
|
|
305
|
+
* @param agentSlug The agent's slug, as shown on its page in Promptev. Only
|
|
306
|
+
* PUBLISHED agents in the key's own project can be reached; anything else
|
|
307
|
+
* answers 404 ({@link NotFoundError}).
|
|
308
|
+
* @param message What to say. Required, non-empty after trimming, at most
|
|
309
|
+
* {@link MAX_MESSAGE_CHARS} characters.
|
|
182
310
|
*
|
|
183
|
-
* @example
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
311
|
+
* @example Stream the answer
|
|
312
|
+
* ```ts
|
|
313
|
+
* let conversationId: string | undefined;
|
|
314
|
+
* for await (const ev of client.chat("support-agent", "What is the refund policy?")) {
|
|
315
|
+
* switch (ev.type) {
|
|
316
|
+
* case "conversation": conversationId = ev.id; break;
|
|
317
|
+
* case "token": process.stdout.write(ev.content); break;
|
|
318
|
+
* case "approval_required":
|
|
319
|
+
* console.log(`\n${ev.tool_name} needs approval — ${ev.reason ?? "policy"}`);
|
|
320
|
+
* break;
|
|
321
|
+
* case "error": throw new Error(ev.message);
|
|
322
|
+
* }
|
|
323
|
+
* }
|
|
324
|
+
* // Continue the thread — and see far fewer approval prompts:
|
|
325
|
+
* for await (const ev of client.chat("support-agent", "And for digital goods?", { conversationId })) {
|
|
326
|
+
* if (ev.type === "token") process.stdout.write(ev.content);
|
|
327
|
+
* }
|
|
328
|
+
* ```
|
|
191
329
|
*/
|
|
192
|
-
async
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
330
|
+
async *chat(agentSlug, message, options) {
|
|
331
|
+
const slug = String(agentSlug ?? "").trim();
|
|
332
|
+
if (!slug) {
|
|
333
|
+
throw new ValidationError("Name the agent to talk to.", undefined, undefined, "agentSlug");
|
|
334
|
+
}
|
|
335
|
+
const text = String(message ?? "").trim();
|
|
336
|
+
if (!text) {
|
|
337
|
+
// The server answers 422 for this; refusing here saves the round trip
|
|
338
|
+
// AND the api_call the workspace would still have been metered for.
|
|
339
|
+
throw new ValidationError("Say something to the agent.", undefined, undefined, "message");
|
|
340
|
+
}
|
|
341
|
+
if (text.length > exports.MAX_MESSAGE_CHARS) {
|
|
342
|
+
throw new ValidationError(`A message can be at most ${exports.MAX_MESSAGE_CHARS} characters.`, undefined, undefined, "message");
|
|
343
|
+
}
|
|
344
|
+
const payload = { message: text };
|
|
345
|
+
if (options?.conversationId != null)
|
|
346
|
+
payload.conversation_id = options.conversationId;
|
|
347
|
+
if (options?.onBehalfOf != null) {
|
|
348
|
+
const obo = { subject: options.onBehalfOf.subject };
|
|
349
|
+
if (options.onBehalfOf.groups != null)
|
|
350
|
+
obo.groups = options.onBehalfOf.groups;
|
|
351
|
+
payload.on_behalf_of = obo;
|
|
352
|
+
}
|
|
353
|
+
const url = `${this.baseUrl}/api/v1/agents/${encodeURIComponent(slug)}/chat`;
|
|
354
|
+
// Deliberately NOT fetchWithRetry: a turn is not idempotent — a retried
|
|
355
|
+
// POST could run the agent, and its tools, a second time.
|
|
356
|
+
const resp = await this.fetchOnce(url, {
|
|
204
357
|
method: "POST",
|
|
205
|
-
headers: this.
|
|
358
|
+
headers: this.authHeaders({
|
|
359
|
+
"Content-Type": "application/json",
|
|
360
|
+
Accept: "text/event-stream",
|
|
361
|
+
}),
|
|
206
362
|
body: JSON.stringify(payload),
|
|
363
|
+
signal: options?.signal,
|
|
207
364
|
});
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
sessionId: data.session_id,
|
|
214
|
-
};
|
|
365
|
+
if (resp.status >= 400) {
|
|
366
|
+
const body = await resp.text();
|
|
367
|
+
PromptevClient.raiseForStatus(resp.status, body, resp.headers);
|
|
368
|
+
}
|
|
369
|
+
yield* this.parseSSE(resp);
|
|
215
370
|
}
|
|
216
371
|
/**
|
|
217
|
-
*
|
|
372
|
+
* Put one or more documents into the project's corpus — the same "Uploads"
|
|
373
|
+
* source the app writes to, so what you upload here is what the agents in
|
|
374
|
+
* that project retrieve.
|
|
375
|
+
*
|
|
376
|
+
* The key names the project, so nothing about the destination is in the
|
|
377
|
+
* request: a key cannot upload into a project it does not reach.
|
|
378
|
+
*
|
|
379
|
+
* Limits, checked here and again on the server: at most
|
|
380
|
+
* {@link MAX_FILES_PER_REQUEST} files per call, at most
|
|
381
|
+
* {@link MAX_UPLOAD_BYTES} (25 MB) per file — a larger set belongs in a
|
|
382
|
+
* connector source. Ingestion is content-addressed: re-uploading identical
|
|
383
|
+
* bytes updates the existing document instead of duplicating it.
|
|
384
|
+
*
|
|
385
|
+
* Ingestion is slow, and the request does not return until every file is
|
|
386
|
+
* indexed — raise `timeout` well above its 30 s default for real documents.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* ```ts
|
|
390
|
+
* import { readFile } from "node:fs/promises";
|
|
218
391
|
*
|
|
219
|
-
*
|
|
392
|
+
* const { results } = await client.uploadDocument(
|
|
393
|
+
* { name: "handbook.pdf", content: await readFile("handbook.pdf") },
|
|
394
|
+
* { mode: "hybrid" }
|
|
395
|
+
* );
|
|
396
|
+
* for (const r of results) console.log(r.filename, r.status, r.error ?? "");
|
|
397
|
+
* ```
|
|
220
398
|
*/
|
|
221
|
-
async
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
399
|
+
async uploadDocument(files, options) {
|
|
400
|
+
const list = Array.isArray(files) ? files : [files];
|
|
401
|
+
if (list.length === 0) {
|
|
402
|
+
throw new ValidationError("Attach at least one file.", undefined, undefined, "files");
|
|
403
|
+
}
|
|
404
|
+
if (list.length > exports.MAX_FILES_PER_REQUEST) {
|
|
405
|
+
throw new ValidationError(`Upload at most ${exports.MAX_FILES_PER_REQUEST} files at a time — sync a larger set through a connector source.`, undefined, undefined, "files");
|
|
406
|
+
}
|
|
407
|
+
if (options?.mode != null && !["hybrid", "graph"].includes(options.mode)) {
|
|
408
|
+
throw new ValidationError("Mode must be 'hybrid' or 'graph'.", undefined, undefined, "mode");
|
|
409
|
+
}
|
|
410
|
+
const form = new FormData();
|
|
411
|
+
for (const file of list) {
|
|
412
|
+
const [name, blob] = toBlob(file);
|
|
413
|
+
if (blob.size > exports.MAX_UPLOAD_BYTES) {
|
|
414
|
+
throw new ValidationError(`${name}: larger than ${exports.MAX_UPLOAD_BYTES / (1024 * 1024)} MB — sync bigger files through a connector source.`, undefined, undefined, "files");
|
|
415
|
+
}
|
|
416
|
+
form.append("files", blob, name);
|
|
417
|
+
}
|
|
418
|
+
if (options?.mode != null)
|
|
419
|
+
form.append("mode", options.mode);
|
|
420
|
+
const url = `${this.baseUrl}/api/v1/documents`;
|
|
421
|
+
// Retried on 502/503/504, unlike chat: the same bytes ingest to the same
|
|
422
|
+
// document (its id is a hash of the content), so a retry cannot duplicate one.
|
|
228
423
|
const resp = await this.fetchWithRetry(url, {
|
|
229
424
|
method: "POST",
|
|
230
|
-
|
|
231
|
-
|
|
425
|
+
// NO Content-Type — fetch sets the multipart boundary itself.
|
|
426
|
+
headers: this.authHeaders(),
|
|
427
|
+
body: form,
|
|
428
|
+
signal: options?.signal,
|
|
232
429
|
});
|
|
233
|
-
const
|
|
234
|
-
PromptevClient.raiseForStatus(resp.status,
|
|
235
|
-
const data = JSON.parse(
|
|
236
|
-
return {
|
|
237
|
-
sessionToken: data.session_token,
|
|
238
|
-
agentId: data.chatbot_id,
|
|
239
|
-
name: data.name,
|
|
240
|
-
memoryEnabled: data.memory_enabled ?? false,
|
|
241
|
-
messages: data.messages ?? [],
|
|
242
|
-
};
|
|
430
|
+
const body = await resp.text();
|
|
431
|
+
PromptevClient.raiseForStatus(resp.status, body, resp.headers);
|
|
432
|
+
const data = JSON.parse(body);
|
|
433
|
+
return { results: Array.isArray(data?.results) ? data.results : [] };
|
|
243
434
|
}
|
|
244
435
|
/**
|
|
245
|
-
*
|
|
436
|
+
* Start a run of a published orchestration. Answers as soon as the run is
|
|
437
|
+
* recorded — **not** when it finishes.
|
|
438
|
+
*
|
|
439
|
+
* An orchestration is a team of agents; it runs in a worker for as long as it
|
|
440
|
+
* takes, so this returns a `run_id` and {@link getRun} tells you what became
|
|
441
|
+
* of it. That is the difference from {@link chat}, which streams one agent's
|
|
442
|
+
* answer inline.
|
|
246
443
|
*
|
|
247
|
-
*
|
|
444
|
+
* ```ts
|
|
445
|
+
* const { run_id } = await client.runOrchestration("month-end-close", {
|
|
446
|
+
* period: "2026-03",
|
|
447
|
+
* entity: "Acme GmbH",
|
|
448
|
+
* });
|
|
449
|
+
* ```
|
|
450
|
+
*
|
|
451
|
+
* With files — the part is named after the input it fills:
|
|
452
|
+
*
|
|
453
|
+
* ```ts
|
|
454
|
+
* const { run_id } = await client.runOrchestration(
|
|
455
|
+
* "contract-review",
|
|
456
|
+
* { matter: "Acme renewal" },
|
|
457
|
+
* { files: { contract: { name: "lease.pdf", content: bytes } } },
|
|
458
|
+
* );
|
|
459
|
+
* ```
|
|
248
460
|
*/
|
|
249
|
-
async
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
253
|
-
let resp;
|
|
254
|
-
try {
|
|
255
|
-
resp = await fetch(url, {
|
|
256
|
-
method: "POST",
|
|
257
|
-
headers: this.headers,
|
|
258
|
-
body: JSON.stringify({ query: options.query }),
|
|
259
|
-
signal: controller.signal,
|
|
260
|
-
});
|
|
461
|
+
async runOrchestration(slug, inputs = {}, options) {
|
|
462
|
+
if (!slug || !slug.trim()) {
|
|
463
|
+
throw new ValidationError("Name the orchestration to run.", undefined, undefined, "slug");
|
|
261
464
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
throw new NetworkError("Request timed out");
|
|
266
|
-
throw new NetworkError(`Network error: ${err.message}`);
|
|
465
|
+
const fileEntries = Object.entries(options?.files ?? {}).flatMap(([key, value]) => (Array.isArray(value) ? value : [value]).map((file) => [key, file]));
|
|
466
|
+
if (fileEntries.length > exports.MAX_FILES_PER_RUN) {
|
|
467
|
+
throw new ValidationError(`At most ${exports.MAX_FILES_PER_RUN} files can be sent with one call.`, undefined, undefined, "files");
|
|
267
468
|
}
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
PromptevClient.raiseForStatus(resp.status, text);
|
|
469
|
+
const headers = this.authHeaders();
|
|
470
|
+
if (options?.idempotencyKey) {
|
|
471
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
272
472
|
}
|
|
273
|
-
|
|
473
|
+
let body;
|
|
474
|
+
if (fileEntries.length > 0) {
|
|
475
|
+
// MULTIPART when there are files, JSON when there are not — the two
|
|
476
|
+
// shapes the server accepts, and the JSON rides in a `payload` part so
|
|
477
|
+
// the input keys survive untouched.
|
|
478
|
+
const form = new FormData();
|
|
479
|
+
form.append("payload", JSON.stringify(inputs ?? {}));
|
|
480
|
+
for (const [key, file] of fileEntries) {
|
|
481
|
+
const [name, blob] = toBlob(file);
|
|
482
|
+
if (blob.size > exports.MAX_UPLOAD_BYTES) {
|
|
483
|
+
throw new ValidationError(`${name}: larger than ${exports.MAX_UPLOAD_BYTES / (1024 * 1024)} MB — add bigger files to the agent's knowledge instead.`, undefined, undefined, key);
|
|
484
|
+
}
|
|
485
|
+
form.append(key, blob, name);
|
|
486
|
+
}
|
|
487
|
+
body = form;
|
|
488
|
+
// NO Content-Type — fetch sets the multipart boundary itself.
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
headers["Content-Type"] = "application/json";
|
|
492
|
+
body = JSON.stringify(inputs ?? {});
|
|
493
|
+
}
|
|
494
|
+
const url = `${this.baseUrl}/api/v1/orchestrations/${encodeURIComponent(slug)}/run`;
|
|
495
|
+
// NOT retried without an idempotency key: a retried start could fan the
|
|
496
|
+
// team out twice, and only the caller can say whether two calls are one
|
|
497
|
+
// event. With a key the server collapses them, so retrying is safe.
|
|
498
|
+
const resp = options?.idempotencyKey
|
|
499
|
+
? await this.fetchWithRetry(url, { method: "POST", headers, body, signal: options?.signal })
|
|
500
|
+
: await fetch(url, { method: "POST", headers, body, signal: options?.signal });
|
|
501
|
+
const text = await resp.text();
|
|
502
|
+
PromptevClient.raiseForStatus(resp.status, text, resp.headers);
|
|
503
|
+
const data = JSON.parse(text);
|
|
504
|
+
return { run_id: String(data?.run_id ?? ""), status: "accepted" };
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* What became of a run — poll this after {@link runOrchestration}.
|
|
508
|
+
*
|
|
509
|
+
* ```ts
|
|
510
|
+
* let run = await client.getRun(run_id);
|
|
511
|
+
* while (run.status === "running" || run.status === "revising") {
|
|
512
|
+
* await new Promise((r) => setTimeout(r, 2000));
|
|
513
|
+
* run = await client.getRun(run_id);
|
|
514
|
+
* }
|
|
515
|
+
* console.log(run.output);
|
|
516
|
+
* ```
|
|
517
|
+
*/
|
|
518
|
+
async getRun(runId, options) {
|
|
519
|
+
if (!runId || !runId.trim()) {
|
|
520
|
+
throw new ValidationError("Name the run to read.", undefined, undefined, "runId");
|
|
521
|
+
}
|
|
522
|
+
const url = `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}`;
|
|
523
|
+
// A read is safe to retry.
|
|
524
|
+
const resp = await this.fetchWithRetry(url, {
|
|
525
|
+
method: "GET",
|
|
526
|
+
headers: this.authHeaders(),
|
|
527
|
+
signal: options?.signal,
|
|
528
|
+
});
|
|
529
|
+
const text = await resp.text();
|
|
530
|
+
PromptevClient.raiseForStatus(resp.status, text, resp.headers);
|
|
531
|
+
return JSON.parse(text);
|
|
274
532
|
}
|
|
275
533
|
}
|
|
276
534
|
exports.PromptevClient = PromptevClient;
|
|
535
|
+
/** One upload input, as a named Blob — the only shape `FormData` accepts. */
|
|
536
|
+
function toBlob(file) {
|
|
537
|
+
if (typeof Blob !== "undefined" && file instanceof Blob) {
|
|
538
|
+
// A `File` is a Blob that already has a name; a bare Blob needs one.
|
|
539
|
+
const name = file.name;
|
|
540
|
+
return [typeof name === "string" && name ? name : "upload", file];
|
|
541
|
+
}
|
|
542
|
+
const named = file;
|
|
543
|
+
if (!named || typeof named.name !== "string" || !named.name.trim()) {
|
|
544
|
+
throw new ValidationError("Give each uploaded file a name — the extension is what picks the parser.", undefined, undefined, "files");
|
|
545
|
+
}
|
|
546
|
+
const content = named.content;
|
|
547
|
+
if (content == null) {
|
|
548
|
+
throw new ValidationError(`${named.name}: no content to upload.`, undefined, undefined, "files");
|
|
549
|
+
}
|
|
550
|
+
if (typeof Blob !== "undefined" && content instanceof Blob) {
|
|
551
|
+
return [named.name, content];
|
|
552
|
+
}
|
|
553
|
+
const part = ArrayBuffer.isView(content)
|
|
554
|
+
? content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength)
|
|
555
|
+
: content;
|
|
556
|
+
return [
|
|
557
|
+
named.name,
|
|
558
|
+
new Blob([part], named.type ? { type: named.type } : undefined),
|
|
559
|
+
];
|
|
560
|
+
}
|
|
277
561
|
exports.default = PromptevClient;
|