@promptev/client 1.1.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.
@@ -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. Required, non-empty after trimming, at most
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
- if (!text) {
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");
@@ -359,11 +360,7 @@ class PromptevClient {
359
360
  // POST could run the agent, and its tools, a second time.
360
361
  const resp = await this.fetchOnce(url, {
361
362
  method: "POST",
362
- headers: this.authHeaders({
363
- "Content-Type": "application/json",
364
- Accept: "text/event-stream",
365
- }),
366
- body: JSON.stringify(payload),
363
+ ...chatBody(payload, files, (extra) => this.authHeaders(extra)),
367
364
  signal: options?.signal,
368
365
  });
369
366
  if (resp.status >= 400) {
@@ -536,6 +533,33 @@ class PromptevClient {
536
533
  }
537
534
  }
538
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
+ }
539
563
  /** One upload input, as a named Blob — the only shape `FormData` accepts. */
540
564
  function toBlob(file) {
541
565
  if (typeof Blob !== "undefined" && file instanceof Blob) {
@@ -127,6 +127,16 @@ export interface ChatOptions {
127
127
  * keyed by tool id (shown on the tool's page) — see {@link ToolInput}.
128
128
  */
129
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[];
130
140
  /** Abort the turn from your own signal (in addition to the client timeout). */
131
141
  signal?: AbortSignal;
132
142
  }
@@ -451,8 +461,8 @@ export declare class PromptevClient {
451
461
  * @param agentSlug The agent's slug, as shown on its page in Promptev. Only
452
462
  * PUBLISHED agents in the key's own project can be reached; anything else
453
463
  * answers 404 ({@link NotFoundError}).
454
- * @param message What to say. Required, non-empty after trimming, at most
455
- * {@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.
456
466
  *
457
467
  * @example Stream the answer
458
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. Required, non-empty after trimming, at most
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
- if (!text) {
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");
@@ -348,11 +349,7 @@ export class PromptevClient {
348
349
  // POST could run the agent, and its tools, a second time.
349
350
  const resp = await this.fetchOnce(url, {
350
351
  method: "POST",
351
- headers: this.authHeaders({
352
- "Content-Type": "application/json",
353
- Accept: "text/event-stream",
354
- }),
355
- body: JSON.stringify(payload),
352
+ ...chatBody(payload, files, (extra) => this.authHeaders(extra)),
356
353
  signal: options?.signal,
357
354
  });
358
355
  if (resp.status >= 400) {
@@ -524,6 +521,33 @@ export class PromptevClient {
524
521
  return JSON.parse(text);
525
522
  }
526
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
+ }
527
551
  /** One upload input, as a named Blob — the only shape `FormData` accepts. */
528
552
  function toBlob(file) {
529
553
  if (typeof Blob !== "undefined" && file instanceof Blob) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptev/client",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "JavaScript/TypeScript client for Promptev — stream your AI agents and upload documents with a workspace API key",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",
package/readme.md CHANGED
@@ -105,6 +105,28 @@ Passing `conversationId` is worth doing for a second reason: a tool set to **Ask
105
105
 
106
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.
107
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
+
108
130
  ## Changing the model or a tool's values for one call
109
131
 
110
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.