@promptev/client 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@
12
12
  * - `uploadDocument()` — put files into the key's project corpus.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
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;
15
+ exports.PromptevClient = exports.NetworkError = exports.ServerError = exports.InsufficientCreditsError = exports.RateLimitError = exports.NotFoundError = exports.AuthenticationError = exports.ValidationError = exports.PromptevError = exports.KNOWLEDGE_ACTIONS = 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
16
  // ── Configuration ───────────────────────────────────────
17
17
  /**
18
18
  * The default host. NOT `api.promptev.ai` — that host still serves the
@@ -39,6 +39,13 @@ exports.MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
39
39
  /** Files ONE orchestration run may carry — the same ceiling a chat turn has.
40
40
  * Server-side: `run_inputs.MAX_FILE_PARTS`. */
41
41
  exports.MAX_FILES_PER_RUN = 5;
42
+ /** The knowledge base's actions, as the project MCP's `search_knowledge_base`
43
+ * offers them. Informational: the server decides, so an action added there
44
+ * works here without a new SDK. */
45
+ exports.KNOWLEDGE_ACTIONS = [
46
+ "discover", "search", "get_doc", "list", "query_meta", "compute", "get_chunks",
47
+ "get_docs", "map_reduce", "traverse", "find_related", "get_neighbors", "community_summary",
48
+ ];
42
49
  // ── Errors ──────────────────────────────────────────────
43
50
  class PromptevError extends Error {
44
51
  constructor(message, statusCode, responseText, field) {
@@ -305,8 +312,8 @@ class PromptevClient {
305
312
  * @param agentSlug The agent's slug, as shown on its page in Promptev. Only
306
313
  * PUBLISHED agents in the key's own project can be reached; anything else
307
314
  * answers 404 ({@link NotFoundError}).
308
- * @param message What to say. Required, non-empty after trimming, at most
309
- * {@link MAX_MESSAGE_CHARS} characters.
315
+ * @param message What to say. Non-empty after trimming unless
316
+ * `options.files` are sent, at most {@link MAX_MESSAGE_CHARS} characters.
310
317
  *
311
318
  * @example Stream the answer
312
319
  * ```ts
@@ -333,10 +340,11 @@ class PromptevClient {
333
340
  throw new ValidationError("Name the agent to talk to.", undefined, undefined, "agentSlug");
334
341
  }
335
342
  const text = String(message ?? "").trim();
336
- if (!text) {
343
+ const files = options?.files ?? [];
344
+ if (!text && files.length === 0) {
337
345
  // The server answers 422 for this; refusing here saves the round trip
338
346
  // AND the api_call the workspace would still have been metered for.
339
- throw new ValidationError("Say something to the agent.", undefined, undefined, "message");
347
+ throw new ValidationError("Say something to the agent, or attach a file.", undefined, undefined, "message");
340
348
  }
341
349
  if (text.length > exports.MAX_MESSAGE_CHARS) {
342
350
  throw new ValidationError(`A message can be at most ${exports.MAX_MESSAGE_CHARS} characters.`, undefined, undefined, "message");
@@ -359,11 +367,7 @@ class PromptevClient {
359
367
  // POST could run the agent, and its tools, a second time.
360
368
  const resp = await this.fetchOnce(url, {
361
369
  method: "POST",
362
- headers: this.authHeaders({
363
- "Content-Type": "application/json",
364
- Accept: "text/event-stream",
365
- }),
366
- body: JSON.stringify(payload),
370
+ ...chatBody(payload, files, (extra) => this.authHeaders(extra)),
367
371
  signal: options?.signal,
368
372
  });
369
373
  if (resp.status >= 400) {
@@ -462,6 +466,43 @@ class PromptevClient {
462
466
  * );
463
467
  * ```
464
468
  */
469
+ /**
470
+ * Read the key's project knowledge base through the Context Engine: the
471
+ * same tool, with the same actions, as `search_knowledge_base` on the
472
+ * project's MCP server (`POST /api/v1/knowledge`).
473
+ *
474
+ * ```ts
475
+ * const overview = await client.searchKnowledgeBase("discover");
476
+ * const found = await client.searchKnowledgeBase("search", { query: "how many days do customers have to ask for a refund", top_k: 5 });
477
+ * for (const hit of found.hits) console.log(hit.document_name, hit.chunk_text);
478
+ * const doc = await client.searchKnowledgeBase("get_doc", { document_id: found.hits[0].document_id });
479
+ * ```
480
+ *
481
+ * `fields` are that action's fields (`query`, `top_k`, `document_id`,
482
+ * `source_ids`, `mode`, `limit`, `cursor`, ...); undefined and null ones
483
+ * are not sent. Returns the knowledge base's answer as an object.
484
+ */
485
+ async searchKnowledgeBase(action, fields = {}, options) {
486
+ if (typeof action !== "string" || !action.trim()) {
487
+ throw new ValidationError('Name the action, e.g. "search".', undefined, undefined, "action");
488
+ }
489
+ const body = { action: action.trim() };
490
+ for (const [key, value] of Object.entries(fields ?? {})) {
491
+ if (value !== undefined && value !== null)
492
+ body[key] = value;
493
+ }
494
+ const headers = this.authHeaders();
495
+ headers["Content-Type"] = "application/json";
496
+ const resp = await this.fetchWithRetry(`${this.baseUrl}/api/v1/knowledge`, {
497
+ method: "POST",
498
+ headers,
499
+ body: JSON.stringify(body),
500
+ signal: options?.signal,
501
+ });
502
+ const text = await resp.text();
503
+ PromptevClient.raiseForStatus(resp.status, text, resp.headers);
504
+ return text ? JSON.parse(text) : {};
505
+ }
465
506
  async runOrchestration(slug, inputs = {}, options) {
466
507
  if (!slug || !slug.trim()) {
467
508
  throw new ValidationError("Name the orchestration to run.", undefined, undefined, "slug");
@@ -536,6 +577,33 @@ class PromptevClient {
536
577
  }
537
578
  }
538
579
  exports.PromptevClient = PromptevClient;
580
+ /**
581
+ * The body of a chat call: JSON when there are no files; MULTIPART when there
582
+ * are, the JSON in a `payload` part and each file in a `files` part. Count and
583
+ * size are judged here, before the wire.
584
+ */
585
+ function chatBody(payload, files, headers) {
586
+ if (files.length === 0) {
587
+ return {
588
+ headers: headers({ "Content-Type": "application/json", Accept: "text/event-stream" }),
589
+ body: JSON.stringify(payload),
590
+ };
591
+ }
592
+ if (files.length > exports.MAX_FILES_PER_RUN) {
593
+ throw new ValidationError(`At most ${exports.MAX_FILES_PER_RUN} files can be sent with one call.`, undefined, undefined, "files");
594
+ }
595
+ const form = new FormData();
596
+ form.append("payload", JSON.stringify(payload));
597
+ for (const file of files) {
598
+ const [name, blob] = toBlob(file);
599
+ if (blob.size > exports.MAX_UPLOAD_BYTES) {
600
+ 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");
601
+ }
602
+ form.append("files", blob, name);
603
+ }
604
+ // NO Content-Type — fetch sets the multipart boundary itself.
605
+ return { headers: headers({ Accept: "text/event-stream" }), body: form };
606
+ }
539
607
  /** One upload input, as a named Blob — the only shape `FormData` accepts. */
540
608
  function toBlob(file) {
541
609
  if (typeof Blob !== "undefined" && file instanceof Blob) {
@@ -35,6 +35,11 @@ export declare const MAX_UPLOAD_BYTES: number;
35
35
  /** Files ONE orchestration run may carry — the same ceiling a chat turn has.
36
36
  * Server-side: `run_inputs.MAX_FILE_PARTS`. */
37
37
  export declare const MAX_FILES_PER_RUN = 5;
38
+ /** The knowledge base's actions, as the project MCP's `search_knowledge_base`
39
+ * offers them. Informational: the server decides, so an action added there
40
+ * works here without a new SDK. */
41
+ export declare const KNOWLEDGE_ACTIONS: readonly ["discover", "search", "get_doc", "list", "query_meta", "compute", "get_chunks", "get_docs", "map_reduce", "traverse", "find_related", "get_neighbors", "community_summary"];
42
+ export type KnowledgeAction = (typeof KNOWLEDGE_ACTIONS)[number];
38
43
  export interface PromptevClientConfig {
39
44
  /**
40
45
  * Your Promptev API key — `fk_…`, minted in Settings → API keys of the
@@ -127,6 +132,16 @@ export interface ChatOptions {
127
132
  * keyed by tool id (shown on the tool's page) — see {@link ToolInput}.
128
133
  */
129
134
  toolInputs?: Record<string, ToolInput>;
135
+ /**
136
+ * Files for the agent to read on this turn, sent in the same call: at most
137
+ * {@link MAX_FILES_PER_RUN}, each at most 25 MB. Accepted only when the
138
+ * agent's "Chat attachments" is on (409 otherwise), and only the types the
139
+ * promptev chat takes (415 otherwise). An image is shown to a model that can
140
+ * see, and read into text for one that cannot. A document is read before
141
+ * the answer starts, so a long scan makes a slower first event. With files,
142
+ * `message` may be empty.
143
+ */
144
+ files?: UploadInput[];
130
145
  /** Abort the turn from your own signal (in addition to the client timeout). */
131
146
  signal?: AbortSignal;
132
147
  }
@@ -451,8 +466,8 @@ export declare class PromptevClient {
451
466
  * @param agentSlug The agent's slug, as shown on its page in Promptev. Only
452
467
  * PUBLISHED agents in the key's own project can be reached; anything else
453
468
  * answers 404 ({@link NotFoundError}).
454
- * @param message What to say. Required, non-empty after trimming, at most
455
- * {@link MAX_MESSAGE_CHARS} characters.
469
+ * @param message What to say. Non-empty after trimming unless
470
+ * `options.files` are sent, at most {@link MAX_MESSAGE_CHARS} characters.
456
471
  *
457
472
  * @example Stream the answer
458
473
  * ```ts
@@ -529,6 +544,25 @@ export declare class PromptevClient {
529
544
  * );
530
545
  * ```
531
546
  */
547
+ /**
548
+ * Read the key's project knowledge base through the Context Engine: the
549
+ * same tool, with the same actions, as `search_knowledge_base` on the
550
+ * project's MCP server (`POST /api/v1/knowledge`).
551
+ *
552
+ * ```ts
553
+ * const overview = await client.searchKnowledgeBase("discover");
554
+ * const found = await client.searchKnowledgeBase("search", { query: "how many days do customers have to ask for a refund", top_k: 5 });
555
+ * for (const hit of found.hits) console.log(hit.document_name, hit.chunk_text);
556
+ * const doc = await client.searchKnowledgeBase("get_doc", { document_id: found.hits[0].document_id });
557
+ * ```
558
+ *
559
+ * `fields` are that action's fields (`query`, `top_k`, `document_id`,
560
+ * `source_ids`, `mode`, `limit`, `cursor`, ...); undefined and null ones
561
+ * are not sent. Returns the knowledge base's answer as an object.
562
+ */
563
+ searchKnowledgeBase(action: KnowledgeAction | (string & {}), fields?: Record<string, unknown>, options?: {
564
+ signal?: AbortSignal;
565
+ }): Promise<Record<string, any>>;
532
566
  runOrchestration(slug: string, inputs?: RunInputs, options?: RunOptions): Promise<StartedRun>;
533
567
  /**
534
568
  * What became of a run — poll this after {@link runOrchestration}.
package/dist/esm/index.js CHANGED
@@ -36,6 +36,13 @@ export const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
36
36
  /** Files ONE orchestration run may carry — the same ceiling a chat turn has.
37
37
  * Server-side: `run_inputs.MAX_FILE_PARTS`. */
38
38
  export const MAX_FILES_PER_RUN = 5;
39
+ /** The knowledge base's actions, as the project MCP's `search_knowledge_base`
40
+ * offers them. Informational: the server decides, so an action added there
41
+ * works here without a new SDK. */
42
+ export const KNOWLEDGE_ACTIONS = [
43
+ "discover", "search", "get_doc", "list", "query_meta", "compute", "get_chunks",
44
+ "get_docs", "map_reduce", "traverse", "find_related", "get_neighbors", "community_summary",
45
+ ];
39
46
  // ── Errors ──────────────────────────────────────────────
40
47
  export class PromptevError extends Error {
41
48
  constructor(message, statusCode, responseText, field) {
@@ -294,8 +301,8 @@ export class PromptevClient {
294
301
  * @param agentSlug The agent's slug, as shown on its page in Promptev. Only
295
302
  * PUBLISHED agents in the key's own project can be reached; anything else
296
303
  * answers 404 ({@link NotFoundError}).
297
- * @param message What to say. Required, non-empty after trimming, at most
298
- * {@link MAX_MESSAGE_CHARS} characters.
304
+ * @param message What to say. Non-empty after trimming unless
305
+ * `options.files` are sent, at most {@link MAX_MESSAGE_CHARS} characters.
299
306
  *
300
307
  * @example Stream the answer
301
308
  * ```ts
@@ -322,10 +329,11 @@ export class PromptevClient {
322
329
  throw new ValidationError("Name the agent to talk to.", undefined, undefined, "agentSlug");
323
330
  }
324
331
  const text = String(message ?? "").trim();
325
- if (!text) {
332
+ const files = options?.files ?? [];
333
+ if (!text && files.length === 0) {
326
334
  // The server answers 422 for this; refusing here saves the round trip
327
335
  // AND the api_call the workspace would still have been metered for.
328
- throw new ValidationError("Say something to the agent.", undefined, undefined, "message");
336
+ throw new ValidationError("Say something to the agent, or attach a file.", undefined, undefined, "message");
329
337
  }
330
338
  if (text.length > MAX_MESSAGE_CHARS) {
331
339
  throw new ValidationError(`A message can be at most ${MAX_MESSAGE_CHARS} characters.`, undefined, undefined, "message");
@@ -348,11 +356,7 @@ export class PromptevClient {
348
356
  // POST could run the agent, and its tools, a second time.
349
357
  const resp = await this.fetchOnce(url, {
350
358
  method: "POST",
351
- headers: this.authHeaders({
352
- "Content-Type": "application/json",
353
- Accept: "text/event-stream",
354
- }),
355
- body: JSON.stringify(payload),
359
+ ...chatBody(payload, files, (extra) => this.authHeaders(extra)),
356
360
  signal: options?.signal,
357
361
  });
358
362
  if (resp.status >= 400) {
@@ -451,6 +455,43 @@ export class PromptevClient {
451
455
  * );
452
456
  * ```
453
457
  */
458
+ /**
459
+ * Read the key's project knowledge base through the Context Engine: the
460
+ * same tool, with the same actions, as `search_knowledge_base` on the
461
+ * project's MCP server (`POST /api/v1/knowledge`).
462
+ *
463
+ * ```ts
464
+ * const overview = await client.searchKnowledgeBase("discover");
465
+ * const found = await client.searchKnowledgeBase("search", { query: "how many days do customers have to ask for a refund", top_k: 5 });
466
+ * for (const hit of found.hits) console.log(hit.document_name, hit.chunk_text);
467
+ * const doc = await client.searchKnowledgeBase("get_doc", { document_id: found.hits[0].document_id });
468
+ * ```
469
+ *
470
+ * `fields` are that action's fields (`query`, `top_k`, `document_id`,
471
+ * `source_ids`, `mode`, `limit`, `cursor`, ...); undefined and null ones
472
+ * are not sent. Returns the knowledge base's answer as an object.
473
+ */
474
+ async searchKnowledgeBase(action, fields = {}, options) {
475
+ if (typeof action !== "string" || !action.trim()) {
476
+ throw new ValidationError('Name the action, e.g. "search".', undefined, undefined, "action");
477
+ }
478
+ const body = { action: action.trim() };
479
+ for (const [key, value] of Object.entries(fields ?? {})) {
480
+ if (value !== undefined && value !== null)
481
+ body[key] = value;
482
+ }
483
+ const headers = this.authHeaders();
484
+ headers["Content-Type"] = "application/json";
485
+ const resp = await this.fetchWithRetry(`${this.baseUrl}/api/v1/knowledge`, {
486
+ method: "POST",
487
+ headers,
488
+ body: JSON.stringify(body),
489
+ signal: options?.signal,
490
+ });
491
+ const text = await resp.text();
492
+ PromptevClient.raiseForStatus(resp.status, text, resp.headers);
493
+ return text ? JSON.parse(text) : {};
494
+ }
454
495
  async runOrchestration(slug, inputs = {}, options) {
455
496
  if (!slug || !slug.trim()) {
456
497
  throw new ValidationError("Name the orchestration to run.", undefined, undefined, "slug");
@@ -524,6 +565,33 @@ export class PromptevClient {
524
565
  return JSON.parse(text);
525
566
  }
526
567
  }
568
+ /**
569
+ * The body of a chat call: JSON when there are no files; MULTIPART when there
570
+ * are, the JSON in a `payload` part and each file in a `files` part. Count and
571
+ * size are judged here, before the wire.
572
+ */
573
+ function chatBody(payload, files, headers) {
574
+ if (files.length === 0) {
575
+ return {
576
+ headers: headers({ "Content-Type": "application/json", Accept: "text/event-stream" }),
577
+ body: JSON.stringify(payload),
578
+ };
579
+ }
580
+ if (files.length > MAX_FILES_PER_RUN) {
581
+ throw new ValidationError(`At most ${MAX_FILES_PER_RUN} files can be sent with one call.`, undefined, undefined, "files");
582
+ }
583
+ const form = new FormData();
584
+ form.append("payload", JSON.stringify(payload));
585
+ for (const file of files) {
586
+ const [name, blob] = toBlob(file);
587
+ if (blob.size > MAX_UPLOAD_BYTES) {
588
+ throw new ValidationError(`${name}: larger than ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB — add bigger files to the agent's knowledge instead.`, undefined, undefined, "files");
589
+ }
590
+ form.append("files", blob, name);
591
+ }
592
+ // NO Content-Type — fetch sets the multipart boundary itself.
593
+ return { headers: headers({ Accept: "text/event-stream" }), body: form };
594
+ }
527
595
  /** One upload input, as a named Blob — the only shape `FormData` accepts. */
528
596
  function toBlob(file) {
529
597
  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.3.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
@@ -1,6 +1,6 @@
1
1
  # Promptev JavaScript client
2
2
 
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`.
3
+ The official JavaScript/TypeScript client for [Promptev](https://promptev.ai) — stream your AI **agents**, put documents into the Context Engine and search it, from anywhere that has `fetch`.
4
4
 
5
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
6
 
@@ -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.
@@ -252,6 +274,25 @@ A per-file failure is *reported*, not thrown: every file gets a row in `results`
252
274
 
253
275
  > 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 })`.
254
276
 
277
+ ## `searchKnowledgeBase` — search the knowledge base
278
+
279
+ ```ts
280
+ client.searchKnowledgeBase(action, fields?, options?): Promise<Record<string, any>>
281
+ ```
282
+
283
+ Reads your key's project knowledge base through the Context Engine. It is the same tool, with the same actions, as `search_knowledge_base` on the project's MCP server. Start with `discover` to see what is there, then `search` for passages or `get_doc` for one document:
284
+
285
+ ```ts
286
+ const overview = await client.searchKnowledgeBase("discover");
287
+
288
+ const found = await client.searchKnowledgeBase("search", { query: "how many days do customers have to ask for a refund", top_k: 5 });
289
+ for (const hit of found.hits) console.log(hit.document_name, hit.score, hit.chunk_text);
290
+
291
+ const doc = await client.searchKnowledgeBase("get_doc", { document_id: found.hits[0].document_id });
292
+ ```
293
+
294
+ The actions are exported as `KNOWLEDGE_ACTIONS`: `discover`, `search`, `get_doc`, `list`, `query_meta`, `compute`, `get_chunks`, `get_docs`, `map_reduce`, `traverse`, `find_related`, `get_neighbors` and `community_summary`. `fields` are that action's fields (`query`, `top_k`, `document_id`, `source_ids`, `mode`, `limit`, `cursor`, ...). The key reads only its own project.
295
+
255
296
  ## `runOrchestration` — run a team of agents
256
297
 
257
298
  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.
@@ -372,9 +413,7 @@ All errors extend `PromptevError` and carry:
372
413
  ## What a call costs
373
414
 
374
415
  Promptev is pay as you go and this API is metered. The rates below are the
375
- managed ones; a workspace running on its own database and embedding provider
376
- (BYO) is charged a flat **1** per operation instead, because those bills are
377
- already yours.
416
+ same for every workspace, including one on its own database or models.
378
417
 
379
418
  | what happens | credits |
380
419
  |---|---|
@@ -384,6 +423,7 @@ already yours.
384
423
  | retrieval | **1** per knowledge base searched |
385
424
  | retrieval in graph mode | **5** per knowledge base searched |
386
425
  | a document you upload | **1** per page, slide or MB (minimum 1) |
426
+ | `searchKnowledgeBase` | the same as on the MCP server: **1** per call, **+1** for a search (**+5** when the graph is searched); no `api_call` charge |
387
427
 
388
428
  So an ordinary `chat` costs **2** — one for the request, one for the turn — and
389
429
  more if the agent calls tools or searches knowledge. The same conversation held