@promptev/client 1.0.0 → 1.2.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 +37 -9
- package/dist/esm/index.d.ts +50 -2
- package/dist/esm/index.js +37 -9
- package/package.json +1 -1
- package/readme.md +78 -0
package/dist/cjs/index.cjs
CHANGED
|
@@ -305,8 +305,8 @@ class PromptevClient {
|
|
|
305
305
|
* @param agentSlug The agent's slug, as shown on its page in Promptev. Only
|
|
306
306
|
* PUBLISHED agents in the key's own project can be reached; anything else
|
|
307
307
|
* answers 404 ({@link NotFoundError}).
|
|
308
|
-
* @param message What to say.
|
|
309
|
-
* {@link MAX_MESSAGE_CHARS} characters.
|
|
308
|
+
* @param message What to say. Non-empty after trimming unless
|
|
309
|
+
* `options.files` are sent, at most {@link MAX_MESSAGE_CHARS} characters.
|
|
310
310
|
*
|
|
311
311
|
* @example Stream the answer
|
|
312
312
|
* ```ts
|
|
@@ -333,10 +333,11 @@ class PromptevClient {
|
|
|
333
333
|
throw new ValidationError("Name the agent to talk to.", undefined, undefined, "agentSlug");
|
|
334
334
|
}
|
|
335
335
|
const text = String(message ?? "").trim();
|
|
336
|
-
|
|
336
|
+
const files = options?.files ?? [];
|
|
337
|
+
if (!text && files.length === 0) {
|
|
337
338
|
// The server answers 422 for this; refusing here saves the round trip
|
|
338
339
|
// 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
|
+
throw new ValidationError("Say something to the agent, or attach a file.", undefined, undefined, "message");
|
|
340
341
|
}
|
|
341
342
|
if (text.length > exports.MAX_MESSAGE_CHARS) {
|
|
342
343
|
throw new ValidationError(`A message can be at most ${exports.MAX_MESSAGE_CHARS} characters.`, undefined, undefined, "message");
|
|
@@ -350,16 +351,16 @@ class PromptevClient {
|
|
|
350
351
|
obo.groups = options.onBehalfOf.groups;
|
|
351
352
|
payload.on_behalf_of = obo;
|
|
352
353
|
}
|
|
354
|
+
if (options?.model != null)
|
|
355
|
+
payload.model = options.model;
|
|
356
|
+
if (options?.toolInputs != null)
|
|
357
|
+
payload.tool_inputs = options.toolInputs;
|
|
353
358
|
const url = `${this.baseUrl}/api/v1/agents/${encodeURIComponent(slug)}/chat`;
|
|
354
359
|
// Deliberately NOT fetchWithRetry: a turn is not idempotent — a retried
|
|
355
360
|
// POST could run the agent, and its tools, a second time.
|
|
356
361
|
const resp = await this.fetchOnce(url, {
|
|
357
362
|
method: "POST",
|
|
358
|
-
|
|
359
|
-
"Content-Type": "application/json",
|
|
360
|
-
Accept: "text/event-stream",
|
|
361
|
-
}),
|
|
362
|
-
body: JSON.stringify(payload),
|
|
363
|
+
...chatBody(payload, files, (extra) => this.authHeaders(extra)),
|
|
363
364
|
signal: options?.signal,
|
|
364
365
|
});
|
|
365
366
|
if (resp.status >= 400) {
|
|
@@ -532,6 +533,33 @@ class PromptevClient {
|
|
|
532
533
|
}
|
|
533
534
|
}
|
|
534
535
|
exports.PromptevClient = PromptevClient;
|
|
536
|
+
/**
|
|
537
|
+
* The body of a chat call: JSON when there are no files; MULTIPART when there
|
|
538
|
+
* are, the JSON in a `payload` part and each file in a `files` part. Count and
|
|
539
|
+
* size are judged here, before the wire.
|
|
540
|
+
*/
|
|
541
|
+
function chatBody(payload, files, headers) {
|
|
542
|
+
if (files.length === 0) {
|
|
543
|
+
return {
|
|
544
|
+
headers: headers({ "Content-Type": "application/json", Accept: "text/event-stream" }),
|
|
545
|
+
body: JSON.stringify(payload),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (files.length > exports.MAX_FILES_PER_RUN) {
|
|
549
|
+
throw new ValidationError(`At most ${exports.MAX_FILES_PER_RUN} files can be sent with one call.`, undefined, undefined, "files");
|
|
550
|
+
}
|
|
551
|
+
const form = new FormData();
|
|
552
|
+
form.append("payload", JSON.stringify(payload));
|
|
553
|
+
for (const file of files) {
|
|
554
|
+
const [name, blob] = toBlob(file);
|
|
555
|
+
if (blob.size > exports.MAX_UPLOAD_BYTES) {
|
|
556
|
+
throw new ValidationError(`${name}: larger than ${exports.MAX_UPLOAD_BYTES / (1024 * 1024)} MB — add bigger files to the agent's knowledge instead.`, undefined, undefined, "files");
|
|
557
|
+
}
|
|
558
|
+
form.append("files", blob, name);
|
|
559
|
+
}
|
|
560
|
+
// NO Content-Type — fetch sets the multipart boundary itself.
|
|
561
|
+
return { headers: headers({ Accept: "text/event-stream" }), body: form };
|
|
562
|
+
}
|
|
535
563
|
/** One upload input, as a named Blob — the only shape `FormData` accepts. */
|
|
536
564
|
function toBlob(file) {
|
|
537
565
|
if (typeof Blob !== "undefined" && file instanceof Blob) {
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -72,6 +72,34 @@ export interface OnBehalfOf {
|
|
|
72
72
|
/** The end user's groups, as your system knows them. Omit for the key's full grant. */
|
|
73
73
|
groups?: string[];
|
|
74
74
|
}
|
|
75
|
+
/** A model of a provider the workspace has connected, by the ids its model list shows. */
|
|
76
|
+
export interface ModelChoice {
|
|
77
|
+
provider: string;
|
|
78
|
+
model: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* What one call overrides on one of the agent's tools, by where each value
|
|
82
|
+
* goes. Only what the tool already has in that place can be overridden; any
|
|
83
|
+
* other name answers 400, and nothing is stored: the next call runs the agent
|
|
84
|
+
* as it was published.
|
|
85
|
+
*
|
|
86
|
+
* - An HTTP tool has `headers`, `query`, `body` and `path` (a `{{name}}` in
|
|
87
|
+
* its URL). A GET or DELETE has no body: all its parameters are `query`.
|
|
88
|
+
* - An MCP server has `headers` (sent to the server) and `arguments` (of its
|
|
89
|
+
* functions; a value reaches every function that takes that argument).
|
|
90
|
+
*
|
|
91
|
+
* A value the model would have filled is fixed for the call: the model neither
|
|
92
|
+
* sees nor changes it, and it shows wherever tool arguments show (the
|
|
93
|
+
* `tool_call` event, approvals). A header, or a value the tool was saved with,
|
|
94
|
+
* never appears in the stream. Put secrets in `headers`.
|
|
95
|
+
*/
|
|
96
|
+
export interface ToolInput {
|
|
97
|
+
headers?: Record<string, string>;
|
|
98
|
+
query?: Record<string, unknown>;
|
|
99
|
+
body?: Record<string, unknown>;
|
|
100
|
+
path?: Record<string, unknown>;
|
|
101
|
+
arguments?: Record<string, unknown>;
|
|
102
|
+
}
|
|
75
103
|
export interface ChatOptions {
|
|
76
104
|
/**
|
|
77
105
|
* Continue an existing thread. Read it off the first `conversation` event
|
|
@@ -89,6 +117,26 @@ export interface ChatOptions {
|
|
|
89
117
|
conversationId?: string;
|
|
90
118
|
/** Act for one of your end users — see {@link OnBehalfOf}. */
|
|
91
119
|
onBehalfOf?: OnBehalfOf;
|
|
120
|
+
/**
|
|
121
|
+
* Run this one call on another model. The workspace's own provider key is
|
|
122
|
+
* used; a model it cannot call answers 400 before the turn starts.
|
|
123
|
+
*/
|
|
124
|
+
model?: ModelChoice;
|
|
125
|
+
/**
|
|
126
|
+
* Override what the agent's tools are configured with, for this one call,
|
|
127
|
+
* keyed by tool id (shown on the tool's page) — see {@link ToolInput}.
|
|
128
|
+
*/
|
|
129
|
+
toolInputs?: Record<string, ToolInput>;
|
|
130
|
+
/**
|
|
131
|
+
* Files for the agent to read on this turn, sent in the same call: at most
|
|
132
|
+
* {@link MAX_FILES_PER_RUN}, each at most 25 MB. Accepted only when the
|
|
133
|
+
* agent's "Chat attachments" is on (409 otherwise), and only the types the
|
|
134
|
+
* promptev chat takes (415 otherwise). An image is shown to a model that can
|
|
135
|
+
* see, and read into text for one that cannot. A document is read before
|
|
136
|
+
* the answer starts, so a long scan makes a slower first event. With files,
|
|
137
|
+
* `message` may be empty.
|
|
138
|
+
*/
|
|
139
|
+
files?: UploadInput[];
|
|
92
140
|
/** Abort the turn from your own signal (in addition to the client timeout). */
|
|
93
141
|
signal?: AbortSignal;
|
|
94
142
|
}
|
|
@@ -413,8 +461,8 @@ export declare class PromptevClient {
|
|
|
413
461
|
* @param agentSlug The agent's slug, as shown on its page in Promptev. Only
|
|
414
462
|
* PUBLISHED agents in the key's own project can be reached; anything else
|
|
415
463
|
* answers 404 ({@link NotFoundError}).
|
|
416
|
-
* @param message What to say.
|
|
417
|
-
* {@link MAX_MESSAGE_CHARS} characters.
|
|
464
|
+
* @param message What to say. Non-empty after trimming unless
|
|
465
|
+
* `options.files` are sent, at most {@link MAX_MESSAGE_CHARS} characters.
|
|
418
466
|
*
|
|
419
467
|
* @example Stream the answer
|
|
420
468
|
* ```ts
|
package/dist/esm/index.js
CHANGED
|
@@ -294,8 +294,8 @@ export class PromptevClient {
|
|
|
294
294
|
* @param agentSlug The agent's slug, as shown on its page in Promptev. Only
|
|
295
295
|
* PUBLISHED agents in the key's own project can be reached; anything else
|
|
296
296
|
* answers 404 ({@link NotFoundError}).
|
|
297
|
-
* @param message What to say.
|
|
298
|
-
* {@link MAX_MESSAGE_CHARS} characters.
|
|
297
|
+
* @param message What to say. Non-empty after trimming unless
|
|
298
|
+
* `options.files` are sent, at most {@link MAX_MESSAGE_CHARS} characters.
|
|
299
299
|
*
|
|
300
300
|
* @example Stream the answer
|
|
301
301
|
* ```ts
|
|
@@ -322,10 +322,11 @@ export class PromptevClient {
|
|
|
322
322
|
throw new ValidationError("Name the agent to talk to.", undefined, undefined, "agentSlug");
|
|
323
323
|
}
|
|
324
324
|
const text = String(message ?? "").trim();
|
|
325
|
-
|
|
325
|
+
const files = options?.files ?? [];
|
|
326
|
+
if (!text && files.length === 0) {
|
|
326
327
|
// The server answers 422 for this; refusing here saves the round trip
|
|
327
328
|
// AND the api_call the workspace would still have been metered for.
|
|
328
|
-
throw new ValidationError("Say something to the agent.", undefined, undefined, "message");
|
|
329
|
+
throw new ValidationError("Say something to the agent, or attach a file.", undefined, undefined, "message");
|
|
329
330
|
}
|
|
330
331
|
if (text.length > MAX_MESSAGE_CHARS) {
|
|
331
332
|
throw new ValidationError(`A message can be at most ${MAX_MESSAGE_CHARS} characters.`, undefined, undefined, "message");
|
|
@@ -339,16 +340,16 @@ export class PromptevClient {
|
|
|
339
340
|
obo.groups = options.onBehalfOf.groups;
|
|
340
341
|
payload.on_behalf_of = obo;
|
|
341
342
|
}
|
|
343
|
+
if (options?.model != null)
|
|
344
|
+
payload.model = options.model;
|
|
345
|
+
if (options?.toolInputs != null)
|
|
346
|
+
payload.tool_inputs = options.toolInputs;
|
|
342
347
|
const url = `${this.baseUrl}/api/v1/agents/${encodeURIComponent(slug)}/chat`;
|
|
343
348
|
// Deliberately NOT fetchWithRetry: a turn is not idempotent — a retried
|
|
344
349
|
// POST could run the agent, and its tools, a second time.
|
|
345
350
|
const resp = await this.fetchOnce(url, {
|
|
346
351
|
method: "POST",
|
|
347
|
-
|
|
348
|
-
"Content-Type": "application/json",
|
|
349
|
-
Accept: "text/event-stream",
|
|
350
|
-
}),
|
|
351
|
-
body: JSON.stringify(payload),
|
|
352
|
+
...chatBody(payload, files, (extra) => this.authHeaders(extra)),
|
|
352
353
|
signal: options?.signal,
|
|
353
354
|
});
|
|
354
355
|
if (resp.status >= 400) {
|
|
@@ -520,6 +521,33 @@ export class PromptevClient {
|
|
|
520
521
|
return JSON.parse(text);
|
|
521
522
|
}
|
|
522
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* The body of a chat call: JSON when there are no files; MULTIPART when there
|
|
526
|
+
* are, the JSON in a `payload` part and each file in a `files` part. Count and
|
|
527
|
+
* size are judged here, before the wire.
|
|
528
|
+
*/
|
|
529
|
+
function chatBody(payload, files, headers) {
|
|
530
|
+
if (files.length === 0) {
|
|
531
|
+
return {
|
|
532
|
+
headers: headers({ "Content-Type": "application/json", Accept: "text/event-stream" }),
|
|
533
|
+
body: JSON.stringify(payload),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (files.length > MAX_FILES_PER_RUN) {
|
|
537
|
+
throw new ValidationError(`At most ${MAX_FILES_PER_RUN} files can be sent with one call.`, undefined, undefined, "files");
|
|
538
|
+
}
|
|
539
|
+
const form = new FormData();
|
|
540
|
+
form.append("payload", JSON.stringify(payload));
|
|
541
|
+
for (const file of files) {
|
|
542
|
+
const [name, blob] = toBlob(file);
|
|
543
|
+
if (blob.size > MAX_UPLOAD_BYTES) {
|
|
544
|
+
throw new ValidationError(`${name}: larger than ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB — add bigger files to the agent's knowledge instead.`, undefined, undefined, "files");
|
|
545
|
+
}
|
|
546
|
+
form.append("files", blob, name);
|
|
547
|
+
}
|
|
548
|
+
// NO Content-Type — fetch sets the multipart boundary itself.
|
|
549
|
+
return { headers: headers({ Accept: "text/event-stream" }), body: form };
|
|
550
|
+
}
|
|
523
551
|
/** One upload input, as a named Blob — the only shape `FormData` accepts. */
|
|
524
552
|
function toBlob(file) {
|
|
525
553
|
if (typeof Blob !== "undefined" && file instanceof Blob) {
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -76,6 +76,8 @@ client.chat(agentSlug, message, options?): AsyncGenerator<AgentEvent>
|
|
|
76
76
|
| `message` | `string` | What to say. Required, non-empty after trimming, at most 20,000 characters. |
|
|
77
77
|
| `options.conversationId` | `string?` | Continue a thread. Take it from the first `conversation` event. |
|
|
78
78
|
| `options.onBehalfOf` | `object?` | Act for one of your end users — see [On behalf of your users](#on-behalf-of-your-users). |
|
|
79
|
+
| `options.model` | `{ provider, model }?` | Run this one call on another model. See [Changing the model or a tool's values for one call](#changing-the-model-or-a-tools-values-for-one-call). |
|
|
80
|
+
| `options.toolInputs` | `Record<toolId, ToolInput>?` | Override what the agent's tools are set up with, for this one call. Same section. |
|
|
79
81
|
| `options.signal` | `AbortSignal?` | Cancel the turn. |
|
|
80
82
|
|
|
81
83
|
Returns an async generator of [events](#events). It ends after `done`, or after `error`.
|
|
@@ -103,6 +105,82 @@ Passing `conversationId` is worth doing for a second reason: a tool set to **Ask
|
|
|
103
105
|
|
|
104
106
|
A thread belongs to the agent, to this API, **and** to the `onBehalfOf.subject` it was opened under. Keep the subject the same across a conversation's turns, and never reuse an id from the app, the portal or an embed — any mismatch answers `404`, not a fresh thread.
|
|
105
107
|
|
|
108
|
+
## Sending files with a message
|
|
109
|
+
|
|
110
|
+
Pass `files` and the agent reads them on that turn, in the same call. This is the paperclip of the promptev chat: the agent takes files only when **Chat attachments** is switched on in its builder, and only the types that chat takes.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { readFile } from "node:fs/promises";
|
|
114
|
+
|
|
115
|
+
for await (const ev of client.chat("support-agent", "What is the total on this receipt?", {
|
|
116
|
+
files: [{ name: "receipt.png", content: await readFile("receipt.png") }], // or a File / Blob
|
|
117
|
+
})) {
|
|
118
|
+
if (ev.type === "token") process.stdout.write(ev.content);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Up to **five files per call**, 25 MB each. An image is shown to a model that can see, and read into text for one that cannot. A document is read before the answer starts, so a long scan makes a slower first event. A call may carry files and an empty message.
|
|
123
|
+
|
|
124
|
+
| Status | When |
|
|
125
|
+
|---|---|
|
|
126
|
+
| 409 | The agent's Chat attachments is off |
|
|
127
|
+
| 415 | A file type the agent cannot take |
|
|
128
|
+
| 413 | The workspace is out of storage |
|
|
129
|
+
|
|
130
|
+
## Changing the model or a tool's values for one call
|
|
131
|
+
|
|
132
|
+
Both options are optional. Leave them out and the agent runs exactly as it was published. Nothing a call sends is stored: the next call runs the published agent again.
|
|
133
|
+
|
|
134
|
+
### `model`
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
for await (const ev of client.chat("support-agent", question, {
|
|
138
|
+
model: { provider: "openai", model: "gpt-5-mini" },
|
|
139
|
+
})) { /* … */ }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`provider` and `model` are the ids shown in your workspace's model list. The workspace's own provider key is used; a call never carries a key. The model runs on its default settings, because the agent's saved settings (temperature and the like) were tuned for its own model.
|
|
143
|
+
|
|
144
|
+
The call answers `400` (a `ValidationError`, with `field` set) before the model is called when the workspace has no such provider, the provider has no key, the model is not in its list or has been retired, or the model cannot call tools and the agent has tools.
|
|
145
|
+
|
|
146
|
+
### `toolInputs`
|
|
147
|
+
|
|
148
|
+
Override what a tool is set up with, keyed by the tool's **id** (shown on the tool's page in promptev, with a copy button). Each value goes under the place it belongs to:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
for await (const ev of client.chat("support-agent", "How many open orders does ACME have?", {
|
|
152
|
+
toolInputs: {
|
|
153
|
+
"0b6f2c1e-…": { // an HTTP tool
|
|
154
|
+
headers: { Authorization: `Bearer ${endUserToken}` },
|
|
155
|
+
query: { region: "us" },
|
|
156
|
+
body: { account_id: "acme" },
|
|
157
|
+
path: { customer: "acme" }, // a {{customer}} in the tool's URL
|
|
158
|
+
},
|
|
159
|
+
"7d41a9c3-…": { // an MCP server
|
|
160
|
+
headers: { Authorization: `Bearer ${endUserToken}` },
|
|
161
|
+
arguments: { project_id: "p_123" },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
})) { /* … */ }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Tool | Place | What it is |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| HTTP | `headers` | A header the tool was saved with. Names match in any letter case. |
|
|
170
|
+
| HTTP | `query` | A query parameter: one the tool was saved with, or one the model fills. A `GET` or `DELETE` tool has no body, so all of its parameters are `query`. |
|
|
171
|
+
| HTTP | `body` | A body value (`POST`, `PUT`, `PATCH`): a key of the saved body, or a parameter the model fills. |
|
|
172
|
+
| HTTP | `path` | A `{{name}}` in the tool's URL. |
|
|
173
|
+
| MCP server | `headers` | A header the server was saved with (`Authorization` when it was saved with a token). |
|
|
174
|
+
| MCP server | `arguments` | An argument of the server's functions. The value reaches every function that takes an argument of that name. |
|
|
175
|
+
|
|
176
|
+
The rules:
|
|
177
|
+
|
|
178
|
+
- **Override only.** A name the tool does not have in that place answers `400`, and the message lists what the tool does have there. Nothing extra is ever passed through.
|
|
179
|
+
- **A value the model would have filled is fixed for the call.** The model neither sees that parameter nor can change it, so the answer's wording follows the question while the tool gets your value.
|
|
180
|
+
- **What is visible.** A value the model would have filled shows wherever tool arguments show: the `tool_call` event and approval requests. A header, and any value the tool was saved with, never appears in the stream. **Put secrets in `headers`.**
|
|
181
|
+
- **Limits.** 20 tools per call, 20 names per place, 16 KB per tool.
|
|
182
|
+
- Overriding a value the tool was **saved with** (a header, a fixed query parameter, a key of the saved body) needs a server that supports it. One that does not answers `400` saying so; it never runs the call without your value.
|
|
183
|
+
|
|
106
184
|
## Events
|
|
107
185
|
|
|
108
186
|
Every frame the server can send, typed. Narrow on `event.type`.
|