@promptev/client 0.1.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 -162
- package/dist/esm/index.d.ts +464 -36
- package/dist/esm/index.js +427 -161
- package/package.json +8 -8
- package/readme.md +403 -286
package/readme.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
# Promptev JavaScript
|
|
1
|
+
# Promptev JavaScript client
|
|
2
2
|
|
|
3
|
-
The official JavaScript/TypeScript
|
|
3
|
+
The official JavaScript/TypeScript client for [Promptev](https://promptev.ai) — stream your AI **agents** and upload documents to their knowledge base, from anywhere that has `fetch`.
|
|
4
|
+
|
|
5
|
+
> In Promptev, an **agent** is your deployed AI assistant: instructions + model + tools (Jira, Slack, GitHub, your own APIs) + the documents it can retrieve, all configured in the app. This client runs those agents. Tools execute server-side, retrieval is governed server-side, and conversation memory is built in.
|
|
6
|
+
|
|
7
|
+
**Upgrading from 0.2.x? Read [Migrating from 0.2.x](#migrating-from-02x) first — 1.0 is a rewrite against a new API, and none of the 0.2 calls exist any more.**
|
|
4
8
|
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
@@ -14,214 +18,244 @@ pnpm add @promptev/client
|
|
|
14
18
|
|
|
15
19
|
Requires Node.js 18+ or any modern browser. Zero dependencies.
|
|
16
20
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
Works everywhere — backend, frontend, and edge runtimes. Built entirely on [Web Platform APIs](https://wintercg.org/) (`fetch`, `ReadableStream`, `AbortController`, `TextDecoder`) with no Node.js-specific modules.
|
|
21
|
+
## Quick Start
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|---|---|---|
|
|
23
|
-
| **Node.js 18+** | ✅ | ESM and CommonJS |
|
|
24
|
-
| **Bun** | ✅ | Native Web API support |
|
|
25
|
-
| **Deno** | ✅ | Native Web API support |
|
|
26
|
-
| **React / Next.js** | ✅ | Works with any bundler |
|
|
27
|
-
| **Angular** | ✅ | Full TypeScript types included |
|
|
28
|
-
| **Vue / Nuxt** | ✅ | Standard ESM import |
|
|
29
|
-
| **Svelte / SvelteKit** | ✅ | Standard ESM import |
|
|
30
|
-
| **Vanilla JS** | ✅ | No framework required |
|
|
31
|
-
| **Cloudflare Workers** | ✅ | Edge runtime compatible |
|
|
32
|
-
| **Vercel Edge Functions** | ✅ | Edge runtime compatible |
|
|
23
|
+
Mint an API key in Promptev under **Settings → API keys** of the project you want to reach. It looks like `fk_…`, it is shown exactly once, and it is bound to that one project.
|
|
33
24
|
|
|
34
|
-
```
|
|
35
|
-
// ESM (Node 18+, React, Next.js, Vue, Angular, Svelte, etc.)
|
|
25
|
+
```ts
|
|
36
26
|
import { PromptevClient } from "@promptev/client";
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY! });
|
|
29
|
+
|
|
30
|
+
let conversationId: string | undefined;
|
|
31
|
+
|
|
32
|
+
for await (const event of client.chat("support-agent", "What is the refund policy?")) {
|
|
33
|
+
switch (event.type) {
|
|
34
|
+
case "conversation":
|
|
35
|
+
conversationId = event.id; // keep it to continue this thread
|
|
36
|
+
break;
|
|
37
|
+
case "token":
|
|
38
|
+
process.stdout.write(event.content); // the answer, one delta at a time
|
|
39
|
+
break;
|
|
40
|
+
case "approval_required":
|
|
41
|
+
console.log(`\n⏸ ${event.tool_name} is waiting for a human — ${event.reason}`);
|
|
42
|
+
break;
|
|
43
|
+
case "error":
|
|
44
|
+
throw new Error(event.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
40
47
|
```
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { PromptevClient } from "@promptev/client";
|
|
46
|
-
|
|
47
|
-
const client = new PromptevClient({ projectKey: "pv_sk_your_key_here" });
|
|
49
|
+
That is the whole conversational surface: one call that starts or continues a thread and streams everything the turn produces.
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
const result = await client.runPrompt("support-agent",
|
|
51
|
-
"What is the refund policy?",
|
|
52
|
-
{ company: "Acme Corp" }
|
|
53
|
-
);
|
|
54
|
-
console.log(result);
|
|
51
|
+
## Authentication
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
const session = await client.startAgent("your-agent-id");
|
|
53
|
+
The key travels in a header, never in a URL:
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
query: "Summarize our Q4 sales report",
|
|
62
|
-
})) {
|
|
63
|
-
if (event.type === "done") console.log(event.output);
|
|
64
|
-
}
|
|
55
|
+
```
|
|
56
|
+
Authorization: Bearer fk_live_xxxxxxxx
|
|
65
57
|
```
|
|
66
58
|
|
|
67
|
-
|
|
59
|
+
Three things follow from that, and they are the reason 1.0 exists:
|
|
68
60
|
|
|
69
|
-
|
|
61
|
+
- **The key never leaks into logs.** A key in a URL path ends up in access logs, proxy logs and `Referer` headers. A Bearer token does not.
|
|
62
|
+
- **The key carries its own retrieval identity.** What the agent may read when *you* call it is part of the credential (`retrieves_as`), not a parameter you send. A caller cannot widen its own reach.
|
|
63
|
+
- **The key is bound to one project.** An agent in a sibling project answers exactly the same `404` an unknown slug does — a key holder learns nothing about projects it was not given.
|
|
70
64
|
|
|
71
|
-
|
|
65
|
+
Keys are minted by workspace admins, expire (at most a year), and can be revoked at any time. Keep them server-side.
|
|
66
|
+
|
|
67
|
+
## `chat` — stream a turn
|
|
72
68
|
|
|
73
69
|
```ts
|
|
74
|
-
|
|
75
|
-
"How do I reset my password?",
|
|
76
|
-
{ company: "Acme Corp", tone: "professional" }
|
|
77
|
-
);
|
|
70
|
+
client.chat(agentSlug, message, options?): AsyncGenerator<AgentEvent>
|
|
78
71
|
```
|
|
79
72
|
|
|
80
|
-
|
|
73
|
+
| Parameter | Type | Description |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `agentSlug` | `string` | The agent's slug, from its page in Promptev. It must be **published**, and it must be in the key's project — otherwise `NotFoundError`. |
|
|
76
|
+
| `message` | `string` | What to say. Required, non-empty after trimming, at most 20,000 characters. |
|
|
77
|
+
| `options.conversationId` | `string?` | Continue a thread. Take it from the first `conversation` event. |
|
|
78
|
+
| `options.onBehalfOf` | `object?` | Act for one of your end users — see [On behalf of your users](#on-behalf-of-your-users). |
|
|
79
|
+
| `options.signal` | `AbortSignal?` | Cancel the turn. |
|
|
80
|
+
|
|
81
|
+
Returns an async generator of [events](#events). It ends after `done`, or after `error`.
|
|
82
|
+
|
|
83
|
+
### Multi-turn
|
|
81
84
|
|
|
82
85
|
```ts
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
+
let conversationId: string | undefined;
|
|
87
|
+
|
|
88
|
+
async function ask(question: string) {
|
|
89
|
+
let answer = "";
|
|
90
|
+
for await (const ev of client.chat("support-agent", question, { conversationId })) {
|
|
91
|
+
if (ev.type === "conversation") conversationId = ev.id;
|
|
92
|
+
if (ev.type === "token") answer += ev.content;
|
|
93
|
+
if (ev.type === "error") throw new Error(ev.message);
|
|
94
|
+
}
|
|
95
|
+
return answer;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(await ask("Summarize our Q4 sales report"));
|
|
99
|
+
console.log(await ask("Compare that with Q3")); // remembers the first turn
|
|
86
100
|
```
|
|
87
101
|
|
|
88
|
-
|
|
102
|
+
Passing `conversationId` is worth doing for a second reason: a tool set to **Ask once** asks for approval on the first call in a conversation and not again for the rest of it, so a continued thread interrupts you far less often.
|
|
103
|
+
|
|
104
|
+
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.
|
|
89
105
|
|
|
90
|
-
|
|
106
|
+
## Events
|
|
107
|
+
|
|
108
|
+
Every frame the server can send, typed. Narrow on `event.type`.
|
|
109
|
+
|
|
110
|
+
A frame from a **newer** server is passed through rather than thrown on, so an event added by a Promptev deploy can never break your integration — it simply matches none of your cases. If you want to log them, `import type { UnknownEvent }` and cast in your `default` branch.
|
|
111
|
+
|
|
112
|
+
| Event | Fields | What it means |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `conversation` | `id` | Always first. The thread id — pass it back as `conversationId`. |
|
|
115
|
+
| `thinking` | `content` | One delta of the model's reasoning. Only from models that stream it. |
|
|
116
|
+
| `token` | `content` | One delta of the answer. Concatenate them all to get the reply. |
|
|
117
|
+
| `step` | `name`, `status`, `detail?` | A stage of the turn: reading attachments, retrieving, waiting on an emailed approval. `status` is `started` / `completed` / `failed`. |
|
|
118
|
+
| `sources` | `items[]` (`name`, `document_id`) | The documents the answer is grounded in. |
|
|
119
|
+
| `tool_call` | `id`, `name`, `args?` | The agent invoked a tool. |
|
|
120
|
+
| `tool_result` | `id`, `name`, `ok`, `summary?` | That tool answered. `id` matches the `tool_call`. |
|
|
121
|
+
| `approval_required` | `approval_id`, `tool_name`, `args?`, `reason?`, `channel?`, `expires_at?` | **The turn is paused, waiting for a human** — see below. |
|
|
122
|
+
| `artifact` | `id`, `name`, `mime`, `size` | A file the agent produced this turn. Fetch it from the conversation in Promptev. |
|
|
123
|
+
| `suggestions` | `questions[]` | 2–3 follow-ups the same person would plausibly ask next. |
|
|
124
|
+
| `usage` | `units`, `tokens?`, `tool_calls?` | What the turn cost, just before `done`. |
|
|
125
|
+
| `done` | — | The stream finished cleanly. |
|
|
126
|
+
| `error` | `message` | The turn failed mid-stream. |
|
|
127
|
+
|
|
128
|
+
### `approval_required` — handle this one
|
|
129
|
+
|
|
130
|
+
A tool the agent wants to run can be gated on human approval. When that happens the turn **pauses and the stream stays open**: a person approves it in the Promptev app, by email, or in Slack, and the turn then continues on that same connection. You do not re-send anything. If nobody decides in time, the approval expires and the turn reports it as a failed `tool_result`.
|
|
131
|
+
|
|
132
|
+
An integration that ignores this event looks *hung* to its user for as long as the approval window lasts. Show it:
|
|
91
133
|
|
|
92
134
|
```ts
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
console.log(
|
|
135
|
+
for await (const ev of client.chat("ops-agent", "Refund invoice 4182")) {
|
|
136
|
+
if (ev.type === "approval_required") {
|
|
137
|
+
console.log(`Waiting on a human: ${ev.tool_name}`);
|
|
138
|
+
console.log(` why: ${ev.reason}`);
|
|
139
|
+
console.log(` args: ${JSON.stringify(ev.args)}`);
|
|
140
|
+
console.log(` decide: in Promptev (${ev.channel}), by ${ev.expires_at ?? "the policy deadline"}`);
|
|
141
|
+
}
|
|
142
|
+
if (ev.type === "tool_result" && ev.approval) {
|
|
143
|
+
console.log(`Approval ended: ${ev.approval}`); // rejected / timeout / expired
|
|
144
|
+
}
|
|
145
|
+
if (ev.type === "token") process.stdout.write(ev.content);
|
|
146
|
+
}
|
|
98
147
|
```
|
|
99
148
|
|
|
100
|
-
|
|
149
|
+
`channel` is where a human can decide it — `"inline"` (the Promptev app) or `"slack"`. Approvals routed to an **approver by email** send no card at all; they show up as a `step` named `waiting for approval — <tool>`, which closes when the decision lands.
|
|
101
150
|
|
|
102
|
-
|
|
151
|
+
## On behalf of your users
|
|
152
|
+
|
|
153
|
+
If you are putting Promptev behind your own UI, tell it which of your end users a call is for:
|
|
103
154
|
|
|
104
155
|
```ts
|
|
105
|
-
|
|
156
|
+
for await (const ev of client.chat("support-agent", question, {
|
|
157
|
+
onBehalfOf: { subject: "ana@acme.com", groups: ["finance", "emea"] },
|
|
158
|
+
})) { /* … */ }
|
|
159
|
+
```
|
|
106
160
|
|
|
107
|
-
|
|
108
|
-
|
|
161
|
+
- `subject` is recorded as the conversation's actor, for the audit trail.
|
|
162
|
+
- `groups` are **intersected** with the key's own grant. They can only ever narrow what the caller may retrieve, never widen it — so a compromised caller of yours still cannot read past what the key was given. (Because the assertion is unverified, the key's own per-user grants drop for that call; only an omitted `onBehalfOf` gets the key's full reach.)
|
|
109
163
|
|
|
110
|
-
|
|
111
|
-
"How do I reset my password?",
|
|
112
|
-
{ company: "Acme Corp", tone: "professional" }
|
|
113
|
-
);
|
|
164
|
+
## `uploadDocument` — add to the knowledge base
|
|
114
165
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
messages: [
|
|
118
|
-
{ role: "system", content: systemPrompt },
|
|
119
|
-
{ role: "user", content: "How do I reset my password?" },
|
|
120
|
-
],
|
|
121
|
-
});
|
|
122
|
-
console.log(response.choices[0].message.content);
|
|
166
|
+
```ts
|
|
167
|
+
client.uploadDocument(files, options?): Promise<{ results: UploadResult[] }>
|
|
123
168
|
```
|
|
124
169
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
When a prompt has tools attached (Jira, Slack, GitHub, etc.) or you want real-time output, use `streamPrompt`. It returns SSE events — same format as agent streaming:
|
|
170
|
+
Files land in the project's **Uploads** source — the same corpus the app writes to, so what you upload is what that project's agents retrieve. The key names the project; there is nothing to address.
|
|
128
171
|
|
|
129
172
|
```ts
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
173
|
+
import { readFile } from "node:fs/promises";
|
|
174
|
+
|
|
175
|
+
const { results } = await client.uploadDocument([
|
|
176
|
+
{ name: "handbook.pdf", content: await readFile("handbook.pdf") },
|
|
177
|
+
{ name: "policies.md", content: "# Refunds\n30 days." },
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
for (const r of results) {
|
|
181
|
+
console.log(`${r.filename}: ${r.status}${r.error ? ` — ${r.error}` : ""} (${r.chunks} chunks)`);
|
|
137
182
|
}
|
|
138
183
|
```
|
|
139
184
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
185
|
+
In a browser, pass `File` objects straight from an `<input type="file">`.
|
|
186
|
+
|
|
187
|
+
| Parameter | Type | Description |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `files` | `File \| Blob \| { name, content, type? }`, or an array of them | `content` may be a `string`, `ArrayBuffer`, typed array, or `Blob`. |
|
|
190
|
+
| `options.mode` | `"hybrid" \| "graph"` | How it is indexed. `"graph"` needs a graph database and an extraction model configured for the workspace. |
|
|
191
|
+
| `options.signal` | `AbortSignal?` | Cancel the upload. |
|
|
192
|
+
|
|
193
|
+
Limits (enforced here and again on the server): **20 files per call**, **25 MB per file**. A larger set belongs in a connector source (Google Drive, SharePoint, Dropbox, …), configured in the app. Re-uploading identical bytes updates the existing document instead of duplicating it.
|
|
194
|
+
|
|
195
|
+
A per-file failure is *reported*, not thrown: every file gets a row in `results` with its own `status` and `error`. Only a whole-request failure (a bad key, no credits, a rejected file type) raises.
|
|
143
196
|
|
|
144
|
-
|
|
197
|
+
> Ingestion is slow and the request does not return until every file is indexed. Raise `timeout` well above its 30-second default: `new PromptevClient({ apiKey, timeout: 300_000 })`.
|
|
145
198
|
|
|
146
|
-
|
|
199
|
+
## `runOrchestration` — run a team of agents
|
|
147
200
|
|
|
148
|
-
|
|
201
|
+
An orchestration is a **team of agents** working one job. `chat` streams one agent's answer inline; an orchestration runs in a worker for as long as it takes, so `runOrchestration` hands back a run id straight away and `getRun` says what became of it.
|
|
149
202
|
|
|
150
203
|
```ts
|
|
151
|
-
const
|
|
204
|
+
const started = await client.runOrchestration("month-end-close", {
|
|
205
|
+
period: "2026-03",
|
|
206
|
+
});
|
|
152
207
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
208
|
+
let run = await client.getRun(started.run_id);
|
|
209
|
+
while (run.status === "running" || run.status === "revising") {
|
|
210
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
211
|
+
run = await client.getRun(started.run_id);
|
|
212
|
+
}
|
|
157
213
|
|
|
158
|
-
|
|
214
|
+
console.log(run.status); // "delivered"
|
|
215
|
+
console.log(run.output);
|
|
216
|
+
```
|
|
159
217
|
|
|
160
|
-
The
|
|
218
|
+
The orchestration must be **published**, **switched on**, and in your key's project — one that is not published, or lives in a sibling project, throws the same `NotFoundError` an invented name does. One that exists and is published but is switched off says so plainly, so you can tell "turn it on" from "wrong name".
|
|
161
219
|
|
|
162
|
-
|
|
163
|
-
|---|---|
|
|
164
|
-
| `thoughts` | Agent's internal reasoning |
|
|
165
|
-
| `processing` | Tool execution status (e.g., "Searching Jira...") |
|
|
166
|
-
| `approval_required` | Agent needs permission to run a tool |
|
|
167
|
-
| `done` | Final response text |
|
|
168
|
-
| `error` | Something went wrong |
|
|
220
|
+
The keys in the second argument are the orchestration's **own declared inputs** — the ones its Inputs step lists. A required one you leave out is refused before anything runs, with the field named:
|
|
169
221
|
|
|
170
222
|
```ts
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (event.type === "thoughts") console.log(`Thinking: ${event.output}`);
|
|
176
|
-
else if (event.type === "processing") console.log(`Running: ${event.output}`);
|
|
177
|
-
else if (event.type === "done") console.log(`\n${event.output}`);
|
|
178
|
-
else if (event.type === "error") console.log(`Error: ${event.output}`);
|
|
223
|
+
try {
|
|
224
|
+
await client.runOrchestration("month-end-close", {});
|
|
225
|
+
} catch (e) {
|
|
226
|
+
if (e instanceof ValidationError) console.log(e.field); // "period"
|
|
179
227
|
}
|
|
180
228
|
```
|
|
181
229
|
|
|
182
|
-
###
|
|
230
|
+
### Sending files
|
|
183
231
|
|
|
184
|
-
|
|
232
|
+
An input declared `File(s)` takes a file, keyed by that input:
|
|
185
233
|
|
|
186
234
|
```ts
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
query: "Summarize our Q4 sales report",
|
|
193
|
-
})) {
|
|
194
|
-
if (event.type === "done") console.log(event.output);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Follow-up — agent remembers the previous context
|
|
198
|
-
for await (const event of client.streamAgent(session.chatbotId, {
|
|
199
|
-
sessionToken: session.sessionToken,
|
|
200
|
-
query: "Compare that with Q3",
|
|
201
|
-
})) {
|
|
202
|
-
if (event.type === "done") console.log(event.output);
|
|
203
|
-
}
|
|
235
|
+
await client.runOrchestration(
|
|
236
|
+
"contract-review",
|
|
237
|
+
{ matter: "Acme renewal" },
|
|
238
|
+
{ files: { contract: { name: "lease.pdf", content: bytes } } },
|
|
239
|
+
);
|
|
204
240
|
```
|
|
205
241
|
|
|
206
|
-
|
|
242
|
+
Up to **five files per call**, 25 MB each. Each part is named after the input it fills, which is what lets one call fill two different file inputs. Files take the same shapes `uploadDocument` does — a `File`, a `Blob`, or `{ name, content }`.
|
|
207
243
|
|
|
208
|
-
|
|
209
|
-
async function askAgent(client, session, query) {
|
|
210
|
-
for await (const event of client.streamAgent(session.chatbotId, {
|
|
211
|
-
sessionToken: session.sessionToken,
|
|
212
|
-
query,
|
|
213
|
-
})) {
|
|
214
|
-
if (event.type === "done") return event.output;
|
|
215
|
-
if (event.type === "error") throw new Error(event.output);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
244
|
+
### Retrying safely
|
|
218
245
|
|
|
219
|
-
|
|
246
|
+
Starting a run twice runs the team twice. Pass an `idempotencyKey` and a retry returns the run you already started:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
await client.runOrchestration(
|
|
250
|
+
"month-end-close",
|
|
251
|
+
{ period: "2026-03" },
|
|
252
|
+
{ idempotencyKey: "close-2026-03" },
|
|
253
|
+
);
|
|
220
254
|
```
|
|
221
255
|
|
|
222
|
-
|
|
256
|
+
Only your caller knows whether two calls are one event, which is why this is yours to send rather than something the client invents. Without one the start is **not** retried on a transient failure, because a retried start could fan the team out twice.
|
|
223
257
|
|
|
224
|
-
|
|
258
|
+
## Error Handling
|
|
225
259
|
|
|
226
260
|
```ts
|
|
227
261
|
import {
|
|
@@ -230,214 +264,297 @@ import {
|
|
|
230
264
|
AuthenticationError,
|
|
231
265
|
NotFoundError,
|
|
232
266
|
RateLimitError,
|
|
267
|
+
InsufficientCreditsError,
|
|
233
268
|
ServerError,
|
|
234
269
|
NetworkError,
|
|
235
270
|
} from "@promptev/client";
|
|
236
271
|
|
|
237
|
-
const client = new PromptevClient({ projectKey: "pv_sk_..." });
|
|
238
|
-
|
|
239
272
|
try {
|
|
240
|
-
const
|
|
273
|
+
for await (const ev of client.chat("support-agent", "Hello")) {
|
|
274
|
+
if (ev.type === "token") process.stdout.write(ev.content);
|
|
275
|
+
}
|
|
241
276
|
} catch (err) {
|
|
242
277
|
if (err instanceof ValidationError) {
|
|
243
|
-
|
|
244
|
-
console.log(`Invalid request: ${err.message}`);
|
|
245
|
-
} else if (err instanceof NotFoundError) {
|
|
246
|
-
// 404 — prompt or project not found
|
|
247
|
-
console.log(`Not found: ${err.message}`);
|
|
278
|
+
console.log(`Invalid request: ${err.message} (field: ${err.field})`);
|
|
248
279
|
} else if (err instanceof AuthenticationError) {
|
|
249
|
-
|
|
250
|
-
|
|
280
|
+
console.log(`Bad or revoked API key: ${err.message}`);
|
|
281
|
+
} else if (err instanceof NotFoundError) {
|
|
282
|
+
console.log(`No such published agent in this key's project: ${err.message}`);
|
|
283
|
+
} else if (err instanceof InsufficientCreditsError) {
|
|
284
|
+
console.log(`Out of credits — top up in Billing: ${err.message}`);
|
|
251
285
|
} else if (err instanceof RateLimitError) {
|
|
252
|
-
|
|
253
|
-
console.log(`Rate limited: ${err.message}`);
|
|
286
|
+
console.log(`Rate limited; retry in ${err.retryAfter ?? "a moment"}s`);
|
|
254
287
|
} else if (err instanceof ServerError) {
|
|
255
|
-
|
|
256
|
-
console.log(`Server error: ${err.message}`);
|
|
288
|
+
console.log(`Server error (after retries): ${err.message}`);
|
|
257
289
|
} else if (err instanceof NetworkError) {
|
|
258
|
-
|
|
259
|
-
console.log(`Network error: ${err.message}`);
|
|
290
|
+
console.log(`Could not reach Promptev: ${err.message}`);
|
|
260
291
|
}
|
|
261
292
|
}
|
|
262
293
|
```
|
|
263
294
|
|
|
264
|
-
All errors extend `PromptevError` and
|
|
265
|
-
|
|
266
|
-
- `err.
|
|
295
|
+
All errors extend `PromptevError` and carry:
|
|
296
|
+
|
|
297
|
+
- `err.statusCode` — the HTTP status, when there was one
|
|
298
|
+
- `err.field` — which input the server objected to, when it named one
|
|
299
|
+
- `err.responseText` — the raw body, for debugging
|
|
300
|
+
|
|
301
|
+
| Error | HTTP | When |
|
|
302
|
+
|---|---|---|
|
|
303
|
+
| `ValidationError` | 400, 422 | Empty or over-long message, bad `mode`, a rejected file type — and the same checks made client-side before a request goes out |
|
|
304
|
+
| `AuthenticationError` | 401, 403 | Missing, malformed, revoked or expired key |
|
|
305
|
+
| `InsufficientCreditsError` | 402 | The workspace is out of credits. Waiting does not fix it; a top-up does |
|
|
306
|
+
| `NotFoundError` | 404 | Unknown agent slug, an unpublished agent, or one outside the key's project — all three answer identically on purpose |
|
|
307
|
+
| `RateLimitError` | 429 | Too many requests. `err.retryAfter` is the server's own answer in seconds |
|
|
308
|
+
| `ServerError` | 5xx | The server failed, after retries |
|
|
309
|
+
| `NetworkError` | — | DNS, connection, abort, or the client timeout |
|
|
310
|
+
| `PromptevError` | any | Base class for all of the above |
|
|
311
|
+
|
|
312
|
+
**Retries.** `uploadDocument` retries 502/503/504 with exponential backoff (`maxRetries`, default 2), which is safe because a document's id is a hash of its bytes — the same upload twice is the same document. `chat` is deliberately **not** retried: re-sending a turn could run the agent, and its tools, a second time.
|
|
313
|
+
|
|
314
|
+
**A failed turn may also arrive as an `error` event** rather than an exception, when the stream had already started. Handle both.
|
|
315
|
+
|
|
316
|
+
## What a call costs
|
|
317
|
+
|
|
318
|
+
Promptev is pay as you go and this API is metered. The rates below are the
|
|
319
|
+
managed ones; a workspace running on its own database and embedding provider
|
|
320
|
+
(BYO) is charged a flat **1** per operation instead, because those bills are
|
|
321
|
+
already yours.
|
|
322
|
+
|
|
323
|
+
| what happens | credits |
|
|
324
|
+
|---|---|
|
|
325
|
+
| any request to this API | **1** (`api_call`) |
|
|
326
|
+
| a chat turn | **1** (`chat_turn`) |
|
|
327
|
+
| each tool the agent calls | **1** |
|
|
328
|
+
| retrieval | **1** per knowledge base searched |
|
|
329
|
+
| retrieval in graph mode | **5** per knowledge base searched |
|
|
330
|
+
| a document you upload | **1** per page, slide or MB (minimum 1) |
|
|
331
|
+
|
|
332
|
+
So an ordinary `chat` costs **2** — one for the request, one for the turn — and
|
|
333
|
+
more if the agent calls tools or searches knowledge. The same conversation held
|
|
334
|
+
in the Promptev app costs one, because there is no request to the programmatic
|
|
335
|
+
door.
|
|
336
|
+
|
|
337
|
+
Two things worth knowing before they surprise you:
|
|
338
|
+
|
|
339
|
+
- **A refused request is still billed.** An unknown agent slug, a rate-limited
|
|
340
|
+
call and a rejected upload each consumed the same capacity a successful one
|
|
341
|
+
would, so each costs its `api_call` credit. Only the outcome differs.
|
|
342
|
+
- **A request this client rejects locally is never billed**, because it never
|
|
343
|
+
leaves your process. That is why the empty-message and upload-limit checks
|
|
344
|
+
exist here rather than being left to the server.
|
|
345
|
+
|
|
346
|
+
Model tokens are not billed by Promptev at all. Model calls go to your provider
|
|
347
|
+
on your own keys, and that bill is yours.
|
|
267
348
|
|
|
268
349
|
## Configuration
|
|
269
350
|
|
|
270
351
|
```ts
|
|
271
352
|
const client = new PromptevClient({
|
|
272
|
-
|
|
273
|
-
baseUrl: "https://api.promptev.ai",
|
|
274
|
-
timeout: 30000,
|
|
275
|
-
maxRetries: 2,
|
|
276
|
-
headers: { "X-Custom": "value" },
|
|
353
|
+
apiKey: "fk_...", // Required
|
|
354
|
+
baseUrl: "https://api.promptev.ai", // Default — override for self-hosted or during the cutover
|
|
355
|
+
timeout: 30000, // Default — ms to wait for the response
|
|
356
|
+
maxRetries: 2, // Default — retries for 502/503/504
|
|
357
|
+
headers: { "X-Custom": "value" }, // Optional — extra HTTP headers
|
|
277
358
|
});
|
|
278
359
|
```
|
|
279
360
|
|
|
280
361
|
| Parameter | Default | Description |
|
|
281
362
|
|---|---|---|
|
|
282
|
-
| `
|
|
283
|
-
| `baseUrl` | `https://api.promptev.ai` | API base URL |
|
|
284
|
-
| `timeout` | `30000` |
|
|
285
|
-
| `maxRetries` | `2` | Automatic retries for transient server errors (502, 503, 504) |
|
|
363
|
+
| `apiKey` | *required* | Your Promptev API key (`fk_…`) |
|
|
364
|
+
| `baseUrl` | `https://api.promptev.ai` | API base URL. Self-hosted? Point it at your own host. |
|
|
365
|
+
| `timeout` | `30000` | Milliseconds to wait for the **response**. A `chat()` stream is not bounded by it — a turn that runs tools, or waits on an approval, may run for minutes. |
|
|
366
|
+
| `maxRetries` | `2` | Automatic retries for transient server errors (502, 503, 504) on non-streaming calls |
|
|
286
367
|
| `headers` | `{}` | Additional HTTP headers |
|
|
287
368
|
|
|
288
|
-
##
|
|
369
|
+
## Platform Support
|
|
289
370
|
|
|
290
|
-
|
|
371
|
+
Works everywhere — backend, frontend, and edge runtimes. Built entirely on [Web Platform APIs](https://wintercg.org/) (`fetch`, `ReadableStream`, `AbortController`, `TextDecoder`, `FormData`) with no Node.js-specific modules.
|
|
291
372
|
|
|
292
|
-
|
|
|
373
|
+
| Environment | Supported | Notes |
|
|
293
374
|
|---|---|---|
|
|
294
|
-
|
|
|
295
|
-
|
|
|
296
|
-
|
|
|
297
|
-
|
|
|
375
|
+
| **Node.js 18+** | ✅ | ESM and CommonJS |
|
|
376
|
+
| **Bun** | ✅ | Native Web API support |
|
|
377
|
+
| **Deno** | ✅ | Native Web API support |
|
|
378
|
+
| **React / Next.js** | ✅ | Server-side — see the warning below |
|
|
379
|
+
| **Angular** | ✅ | Full TypeScript types included |
|
|
380
|
+
| **Vue / Nuxt** | ✅ | Standard ESM import |
|
|
381
|
+
| **Svelte / SvelteKit** | ✅ | Standard ESM import |
|
|
382
|
+
| **Cloudflare Workers** | ✅ | Edge runtime compatible |
|
|
383
|
+
| **Vercel Edge Functions** | ✅ | Edge runtime compatible |
|
|
298
384
|
|
|
299
|
-
|
|
385
|
+
```js
|
|
386
|
+
// ESM (Node 18+, React, Next.js, Vue, Angular, Svelte, etc.)
|
|
387
|
+
import { PromptevClient } from "@promptev/client";
|
|
300
388
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
| `chatbotId` | `string` | Agent identifier |
|
|
305
|
-
| `name` | `string` | Agent display name |
|
|
306
|
-
| `memoryEnabled` | `boolean` | Whether agent retains conversation context |
|
|
307
|
-
| `messages` | `array` | Previous messages (populated when resuming a session) |
|
|
389
|
+
// CommonJS (legacy Node.js projects)
|
|
390
|
+
const { PromptevClient } = require("@promptev/client");
|
|
391
|
+
```
|
|
308
392
|
|
|
309
|
-
|
|
393
|
+
> **Do not ship your API key to a browser.** The key carries a retrieval identity: whoever holds it can read everything it was granted. Call Promptev from your own server, or put an agent in a page with Promptev's embed widget, which issues a short-lived per-visitor token instead.
|
|
310
394
|
|
|
311
|
-
|
|
312
|
-
|---|---|---|
|
|
313
|
-
| `type` | `string` | Event type: `thoughts`, `processing`, `done`, `error`, `approval_required` |
|
|
314
|
-
| `output` | `string` | Event content text |
|
|
315
|
-
| `raw` | `object` | Full parsed SSE event data |
|
|
395
|
+
## Framework Examples
|
|
316
396
|
|
|
317
|
-
###
|
|
397
|
+
### Next.js route handler — stream to your own UI
|
|
318
398
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
| `AuthenticationError` | 401, 403 | Invalid API key, agent not active |
|
|
323
|
-
| `NotFoundError` | 404 | Project, prompt, or agent not found |
|
|
324
|
-
| `RateLimitError` | 429 | API usage quota exceeded |
|
|
325
|
-
| `ServerError` | 5xx | Server error (after retries exhausted) |
|
|
326
|
-
| `NetworkError` | — | Connection failed, timeout, DNS error |
|
|
327
|
-
| `PromptevError` | any | Base class for all above errors |
|
|
399
|
+
```ts
|
|
400
|
+
// app/api/ask/route.ts
|
|
401
|
+
import { PromptevClient } from "@promptev/client";
|
|
328
402
|
|
|
329
|
-
|
|
403
|
+
const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY! });
|
|
404
|
+
|
|
405
|
+
export async function POST(req: Request) {
|
|
406
|
+
const { question, conversationId, user } = await req.json();
|
|
407
|
+
|
|
408
|
+
const stream = new ReadableStream({
|
|
409
|
+
async start(controller) {
|
|
410
|
+
const encoder = new TextEncoder();
|
|
411
|
+
try {
|
|
412
|
+
for await (const ev of client.chat("support-agent", question, {
|
|
413
|
+
conversationId,
|
|
414
|
+
onBehalfOf: { subject: user.email, groups: user.groups },
|
|
415
|
+
})) {
|
|
416
|
+
// Forward the events your UI cares about, as your own SSE stream.
|
|
417
|
+
if (ev.type === "token" || ev.type === "approval_required" || ev.type === "conversation") {
|
|
418
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "error", message: (err as Error).message })}\n\n`));
|
|
423
|
+
} finally {
|
|
424
|
+
controller.close();
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
|
|
430
|
+
}
|
|
431
|
+
```
|
|
330
432
|
|
|
331
|
-
###
|
|
433
|
+
### Express — collect the answer server-side
|
|
332
434
|
|
|
333
|
-
```
|
|
334
|
-
import
|
|
435
|
+
```js
|
|
436
|
+
import express from "express";
|
|
335
437
|
import { PromptevClient } from "@promptev/client";
|
|
336
438
|
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
export function AskAI() {
|
|
340
|
-
const [answer, setAnswer] = useState("");
|
|
341
|
-
const [loading, setLoading] = useState(false);
|
|
439
|
+
const app = express();
|
|
440
|
+
const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY });
|
|
342
441
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
442
|
+
app.post("/api/ask", express.json(), async (req, res) => {
|
|
443
|
+
let answer = "";
|
|
444
|
+
let conversationId = req.body.conversationId;
|
|
445
|
+
let pendingApproval = null;
|
|
446
|
+
|
|
447
|
+
for await (const ev of client.chat("support-agent", req.body.question, { conversationId })) {
|
|
448
|
+
if (ev.type === "conversation") conversationId = ev.id;
|
|
449
|
+
if (ev.type === "token") answer += ev.content;
|
|
450
|
+
if (ev.type === "approval_required") pendingApproval = ev; // tell the user!
|
|
451
|
+
if (ev.type === "error") return res.status(502).json({ error: ev.message });
|
|
351
452
|
}
|
|
352
453
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
</div>
|
|
358
|
-
);
|
|
359
|
-
}
|
|
454
|
+
res.json({ answer, conversationId, pendingApproval });
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
app.listen(3000);
|
|
360
458
|
```
|
|
361
459
|
|
|
362
|
-
###
|
|
460
|
+
### Upload from a browser form (through your backend)
|
|
363
461
|
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
|
|
462
|
+
```ts
|
|
463
|
+
// Your server route, holding the key:
|
|
464
|
+
const files = form.getAll("files") as File[];
|
|
465
|
+
const { results } = await client.uploadDocument(files, { mode: "hybrid" });
|
|
466
|
+
```
|
|
367
467
|
|
|
368
|
-
|
|
468
|
+
## Migrating from 0.2.x
|
|
369
469
|
|
|
370
|
-
|
|
371
|
-
const [messages, setMessages] = useState<string[]>([]);
|
|
372
|
-
const [input, setInput] = useState("");
|
|
373
|
-
const sessionRef = useRef(null);
|
|
470
|
+
**1.0 talks to a different backend.** 0.2.x targeted `POST /api/sdk/v1/agent/{project_key}/{agent_id}/…` on the previous product; Promptev's current API is `/api/v1`, which does not serve those paths at all. Every 0.2 call would 404. Upgrading is a rewrite of your call sites, not a version bump — 0.2.x still works if you have not moved yet, so migrate deliberately.
|
|
374
471
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
472
|
+
### 1. Authentication: a Bearer key, not a key in the URL
|
|
473
|
+
|
|
474
|
+
```diff
|
|
475
|
+
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
|
|
476
|
+
+ const client = new PromptevClient({ apiKey: "fk_..." });
|
|
477
|
+
```
|
|
378
478
|
|
|
379
|
-
|
|
380
|
-
if (!sessionRef.current) await startChat();
|
|
479
|
+
Mint the new key in **Settings → API keys** of the project you want to reach. This is the change everything else follows from: the key is now a header, it is bound to one project, and it carries the retrieval identity the agent answers under. Your old `pv_sk_…` project key does not work against the new API.
|
|
381
480
|
|
|
382
|
-
|
|
383
|
-
const query = input;
|
|
384
|
-
setInput("");
|
|
481
|
+
### 2. Base URL
|
|
385
482
|
|
|
386
|
-
|
|
387
|
-
sessionToken: sessionRef.current.sessionToken,
|
|
388
|
-
query,
|
|
389
|
-
})) {
|
|
390
|
-
if (event.type === "done") {
|
|
391
|
-
setMessages((prev) => [...prev, `Agent: ${event.output}`]);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
483
|
+
The default is `https://api.promptev.ai`. That host is being moved onto the new backend — until it is, pass `baseUrl: "https://console.promptev.ai"` explicitly. If you had hard-coded a `baseUrl`, remove it once the move is done, unless you are self-hosted.
|
|
395
484
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
485
|
+
### 3. `startAgent` + `streamAgent` → `chat`
|
|
486
|
+
|
|
487
|
+
There is no session token any more. The key is the credential, and a `conversationId` is the thread.
|
|
488
|
+
|
|
489
|
+
```diff
|
|
490
|
+
- const session = await client.startAgent(AGENT_ID, { visitor: "user_123" });
|
|
491
|
+
- for await (const ev of client.streamAgent(session.agentId, {
|
|
492
|
+
- sessionToken: session.sessionToken,
|
|
493
|
+
- query: "What is the refund policy?",
|
|
494
|
+
- })) {
|
|
495
|
+
- if (ev.type === "done") console.log(ev.output);
|
|
496
|
+
- }
|
|
497
|
+
+ let conversationId: string | undefined;
|
|
498
|
+
+ let answer = "";
|
|
499
|
+
+ for await (const ev of client.chat("support-agent", "What is the refund policy?")) {
|
|
500
|
+
+ if (ev.type === "conversation") conversationId = ev.id;
|
|
501
|
+
+ if (ev.type === "token") answer += ev.content; // the answer arrives in deltas
|
|
502
|
+
+ }
|
|
503
|
+
+ console.log(answer);
|
|
404
504
|
```
|
|
405
505
|
|
|
406
|
-
|
|
506
|
+
Other differences to expect:
|
|
407
507
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
508
|
+
- **Agents are addressed by slug**, not by a UUID from the Deploy tab.
|
|
509
|
+
- **Events changed shape.** There is no `event.output`; the answer arrives as `token` deltas that you concatenate, and `done` carries no text. See the [events table](#events). `thoughts` is now `thinking`; `processing` is now `step` / `tool_call` / `tool_result`.
|
|
510
|
+
- **`visitor` is now `onBehalfOf`**, and it does more than label a session — see [On behalf of your users](#on-behalf-of-your-users).
|
|
511
|
+
- **`variables` is gone.** Prompt variables were a property of the old product's templates; configure the agent's instructions in Promptev instead, or put the context in the message.
|
|
512
|
+
- **`approval_required` is real now.** The old SDK had no concept of a paused turn. Handle it.
|
|
411
513
|
|
|
412
|
-
|
|
413
|
-
const client = new PromptevClient({ projectKey: "pv_sk_..." });
|
|
514
|
+
### 4. `triggerAgent` → the project MCP server
|
|
414
515
|
|
|
415
|
-
|
|
416
|
-
const answer = await client.runPrompt("support-agent", req.body.question);
|
|
417
|
-
res.json({ answer });
|
|
418
|
-
});
|
|
516
|
+
`triggerAgent` — one blocking call that ran the agent and returned JSON — has **no equivalent in this client**. The 1.0 chat endpoint only streams, and faking a blocking call by draining the stream would silently swallow `approval_required`, leaving your integration hung with no way to know why.
|
|
419
517
|
|
|
420
|
-
|
|
518
|
+
Use Promptev's **project MCP server** instead, which is exactly the shape `trigger` was:
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
POST https://api.promptev.ai/api/v1/mcp
|
|
522
|
+
Authorization: Bearer fk_... ← the same key
|
|
421
523
|
```
|
|
422
524
|
|
|
423
|
-
|
|
525
|
+
It exposes `list_agents` and `call_agent`; `call_agent(agent, message)` runs an agent and returns its answer in one call. A gated tool comes back as `status: "pending_approval"` with an id — decide it in Promptev and call again. Any MCP client works, including n8n's MCP Client node, Zapier, and Claude.
|
|
424
526
|
|
|
425
|
-
|
|
426
|
-
<script type="module">
|
|
427
|
-
import { PromptevClient } from "https://cdn.jsdelivr.net/npm/@promptev/client/dist/esm/index.js";
|
|
527
|
+
If you would rather stay in this client, collect the answer yourself — and handle the pause:
|
|
428
528
|
|
|
429
|
-
|
|
529
|
+
```ts
|
|
530
|
+
async function ask(slug: string, message: string, conversationId?: string) {
|
|
531
|
+
let answer = "";
|
|
532
|
+
const waiting: string[] = [];
|
|
533
|
+
for await (const ev of client.chat(slug, message, { conversationId })) {
|
|
534
|
+
if (ev.type === "conversation") conversationId = ev.id;
|
|
535
|
+
if (ev.type === "token") answer += ev.content;
|
|
536
|
+
if (ev.type === "approval_required") waiting.push(ev.tool_name);
|
|
537
|
+
if (ev.type === "error") throw new Error(ev.message);
|
|
538
|
+
}
|
|
539
|
+
return { answer, conversationId, waitedOn: waiting };
|
|
540
|
+
}
|
|
541
|
+
```
|
|
430
542
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
543
|
+
### 5. What did not change
|
|
544
|
+
|
|
545
|
+
The typed error hierarchy, the retry/backoff configuration (`timeout`, `maxRetries`, `headers`), and the SSE parsing all behave as they did. `InsufficientCreditsError` (402) and `err.field` are new; nothing was removed.
|
|
546
|
+
|
|
547
|
+
## Building from source
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
npm install
|
|
551
|
+
npm run build # builds both ESM (dist/esm) and CommonJS (dist/cjs)
|
|
552
|
+
npm test # runs the offline test suite against the built output
|
|
436
553
|
```
|
|
437
554
|
|
|
438
555
|
## License
|
|
439
556
|
|
|
440
|
-
This
|
|
557
|
+
This client is commercial software by [Promptev Inc](https://promptev.ai).
|
|
441
558
|
|
|
442
559
|
- Free tier use allowed
|
|
443
560
|
- Production use requires an active subscription
|