@babav/knowledge-core-client 0.28.0 → 0.30.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/index.d.ts CHANGED
@@ -181,6 +181,7 @@ export interface Corpus {
181
181
  id: UUID;
182
182
  name: string;
183
183
  description: string | null;
184
+ custom_metadata: Record<string, unknown>;
184
185
  }
185
186
  export interface Folder {
186
187
  id: UUID;
@@ -530,6 +531,16 @@ export declare class KnowledgeCoreClient extends HttpBase {
530
531
  * error). Pass `signal` and abort() on unmount / when the user cancels or navigates away so the
531
532
  * connection doesn't linger. Transient (ends on `done`) — no reopen logic needed. */
532
533
  queryStream(agentId: UUID, body: QueryRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void>;
534
+ /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
535
+ * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
536
+ * `query`/`queryStream` ONLY for programmatic one-shots (tools).
537
+ * New chat: const chat = kc.chat(agentId, corpusIds);
538
+ * Resume: const chat = kc.chat(agentId, corpusIds, { conversationId });
539
+ * The conversation is created LAZILY on the first `send()` (opening a "new chat" and never
540
+ * sending leaves nothing behind). See ChatSession.send. */
541
+ chat(agentId: UUID, corpusIds: UUID[], opts?: {
542
+ conversationId?: UUID;
543
+ }): ChatSession;
533
544
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
534
545
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
535
546
  * cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
@@ -549,16 +560,25 @@ export declare class KnowledgeCoreClient extends HttpBase {
549
560
  create: (b: {
550
561
  name: string;
551
562
  description?: string;
563
+ custom_metadata?: Record<string, unknown>;
552
564
  }) => Promise<Corpus>;
553
565
  list: (q?: {
554
566
  limit?: number;
555
567
  cursor?: string;
556
568
  }) => Promise<Page<Corpus>>;
557
569
  listAll: () => Promise<Corpus[]>;
570
+ /** Filter corpora by custom_metadata using the same metadata filter as documents. */
571
+ search: (b: {
572
+ filter?: MetadataFilter;
573
+ limit?: number;
574
+ cursor?: string;
575
+ }) => Promise<Page<Corpus>>;
558
576
  get: (id: UUID) => Promise<Corpus>;
577
+ /** `custom_metadata` merges (provided keys upsert, a null value deletes a key). */
559
578
  update: (id: UUID, b: {
560
579
  name?: string;
561
580
  description?: string;
581
+ custom_metadata?: Record<string, unknown>;
562
582
  }) => Promise<Corpus>;
563
583
  delete: (id: UUID, confirmName: string) => Promise<void>;
564
584
  listFolders: (id: UUID) => Promise<Folder[]>;
@@ -886,3 +906,37 @@ export declare class AdminClient extends HttpBase {
886
906
  delete: (id: UUID) => Promise<void>;
887
907
  };
888
908
  }
909
+ export interface ChatSendOptions extends StreamHandlers {
910
+ overrides?: QueryRequest["overrides"];
911
+ filter?: QueryRequest["filter"];
912
+ visual?: QueryRequest["visual"];
913
+ /** Abort on unmount / navigation so the SSE connection doesn't linger. */
914
+ signal?: AbortSignal;
915
+ /** Applied ONLY when the conversation is lazily created on the FIRST message. `title` defaults
916
+ * to the message text (truncated) so a chat is never left untitled. */
917
+ title?: string;
918
+ customMetadata?: Record<string, unknown>;
919
+ ephemeral?: boolean;
920
+ /** Fired with the conversation id the moment it is created (first message) or resumed — use it to
921
+ * update the sidebar/URL/route IMMEDIATELY, before the answer streams. */
922
+ onConversationId?: (id: UUID) => void;
923
+ }
924
+ /** A chat session bound to ONE conversation (created via `kc.chat(...)`). Every `send()` is ALWAYS
925
+ * conversational: the conversation_id is attached for you, so a chat turn can never be a
926
+ * non-persisted one-shot. Combined with server-side persist-on-close salvage + the orphan reaper,
927
+ * a chat turn is never lost and a chat conversation is never left blank. */
928
+ export declare class ChatSession {
929
+ #private;
930
+ /** The conversation id — null until the first `send()` (unless resumed). */
931
+ conversationId: UUID | null;
932
+ constructor(kc: KnowledgeCoreClient, agentId: UUID, corpusIds: UUID[], conversationId: UUID | null);
933
+ /** Send a chat message (streaming). Lazily creates the conversation on the first message and
934
+ * ALWAYS passes conversation_id, so the turn is persisted. `onConversationId` fires before the
935
+ * stream so you can show the conversation immediately. Pass `signal` to cancel on unmount. */
936
+ send(text: string, o?: ChatSendOptions): Promise<void>;
937
+ /** The conversation history (messages), once it exists. Empty page before the first send. */
938
+ messages(q?: {
939
+ limit?: number;
940
+ cursor?: string;
941
+ }): Promise<Page<Message>>;
942
+ }
package/dist/index.js CHANGED
@@ -178,6 +178,16 @@ export class KnowledgeCoreClient extends HttpBase {
178
178
  if (buf.trim())
179
179
  dispatchSse(buf, handlers);
180
180
  }
181
+ /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
182
+ * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
183
+ * `query`/`queryStream` ONLY for programmatic one-shots (tools).
184
+ * New chat: const chat = kc.chat(agentId, corpusIds);
185
+ * Resume: const chat = kc.chat(agentId, corpusIds, { conversationId });
186
+ * The conversation is created LAZILY on the first `send()` (opening a "new chat" and never
187
+ * sending leaves nothing behind). See ChatSession.send. */
188
+ chat(agentId, corpusIds, opts) {
189
+ return new ChatSession(this, agentId, corpusIds, opts?.conversationId ?? null);
190
+ }
181
191
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
182
192
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
183
193
  * cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
@@ -219,7 +229,10 @@ export class KnowledgeCoreClient extends HttpBase {
219
229
  create: (b) => this.request("POST", "/v1/corpora", { json: b }),
220
230
  list: (q) => this.request("GET", "/v1/corpora", { query: q }),
221
231
  listAll: () => this.pageAll("/v1/corpora"),
232
+ /** Filter corpora by custom_metadata using the same metadata filter as documents. */
233
+ search: (b) => this.request("POST", "/v1/corpora/search", { json: b }),
222
234
  get: (id) => this.request("GET", `/v1/corpora/${id}`),
235
+ /** `custom_metadata` merges (provided keys upsert, a null value deletes a key). */
223
236
  update: (id, b) => this.request("PATCH", `/v1/corpora/${id}`, { json: b }),
224
237
  delete: (id, confirmName) => this.request("DELETE", `/v1/corpora/${id}`, { query: { confirm: confirmName } }),
225
238
  listFolders: (id) => this.pageAll(`/v1/corpora/${id}/folders`),
@@ -457,6 +470,50 @@ export class AdminClient extends HttpBase {
457
470
  delete: (id) => this.request("DELETE", `/v1/agents/${id}`),
458
471
  };
459
472
  }
473
+ /** A chat session bound to ONE conversation (created via `kc.chat(...)`). Every `send()` is ALWAYS
474
+ * conversational: the conversation_id is attached for you, so a chat turn can never be a
475
+ * non-persisted one-shot. Combined with server-side persist-on-close salvage + the orphan reaper,
476
+ * a chat turn is never lost and a chat conversation is never left blank. */
477
+ export class ChatSession {
478
+ #kc;
479
+ #agentId;
480
+ #corpusIds;
481
+ #creating = null;
482
+ /** The conversation id — null until the first `send()` (unless resumed). */
483
+ conversationId;
484
+ constructor(kc, agentId, corpusIds, conversationId) {
485
+ this.#kc = kc;
486
+ this.#agentId = agentId;
487
+ this.#corpusIds = corpusIds;
488
+ this.conversationId = conversationId;
489
+ }
490
+ /** Lazy create-once (concurrency-safe): the first send starts creation; a racing send awaits it. */
491
+ #ensure(o, text) {
492
+ if (this.conversationId)
493
+ return Promise.resolve(this.conversationId);
494
+ if (!this.#creating) {
495
+ this.#creating = this.#kc.conversations
496
+ .create({ title: o.title ?? text.slice(0, 80), custom_metadata: o.customMetadata, ephemeral: o.ephemeral })
497
+ .then((c) => { this.conversationId = c.id; return c.id; });
498
+ }
499
+ return this.#creating;
500
+ }
501
+ /** Send a chat message (streaming). Lazily creates the conversation on the first message and
502
+ * ALWAYS passes conversation_id, so the turn is persisted. `onConversationId` fires before the
503
+ * stream so you can show the conversation immediately. Pass `signal` to cancel on unmount. */
504
+ async send(text, o = {}) {
505
+ const convId = await this.#ensure(o, text);
506
+ o.onConversationId?.(convId);
507
+ await this.#kc.queryStream(this.#agentId, { corpus_ids: this.#corpusIds, query: text, conversation_id: convId,
508
+ overrides: o.overrides, filter: o.filter, visual: o.visual }, o, o.signal);
509
+ }
510
+ /** The conversation history (messages), once it exists. Empty page before the first send. */
511
+ messages(q) {
512
+ if (!this.conversationId)
513
+ return Promise.resolve({ items: [], next_cursor: null });
514
+ return this.#kc.conversations.listMessages(this.conversationId, q);
515
+ }
516
+ }
460
517
  // ---------------------------------------------------------------------------
461
518
  // helpers
462
519
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babav/knowledge-core-client",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "TypeScript client for the Babav Knowledge Core API (Deno + Node 18+, zero deps). Includes the babav.visual grammar TYPES at the ./visual subpath (types only; all visual rendering is server-side).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -211,6 +211,7 @@ export interface Corpus {
211
211
  id: UUID;
212
212
  name: string;
213
213
  description: string | null;
214
+ custom_metadata: Record<string, unknown>;
214
215
  }
215
216
  export interface Folder {
216
217
  id: UUID;
@@ -644,6 +645,17 @@ export class KnowledgeCoreClient extends HttpBase {
644
645
  if (buf.trim()) dispatchSse(buf, handlers);
645
646
  }
646
647
 
648
+ /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
649
+ * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
650
+ * `query`/`queryStream` ONLY for programmatic one-shots (tools).
651
+ * New chat: const chat = kc.chat(agentId, corpusIds);
652
+ * Resume: const chat = kc.chat(agentId, corpusIds, { conversationId });
653
+ * The conversation is created LAZILY on the first `send()` (opening a "new chat" and never
654
+ * sending leaves nothing behind). See ChatSession.send. */
655
+ chat(agentId: UUID, corpusIds: UUID[], opts?: { conversationId?: UUID }): ChatSession {
656
+ return new ChatSession(this, agentId, corpusIds, opts?.conversationId ?? null);
657
+ }
658
+
647
659
  /** Shared SSE reader for the document-status streams (documents.events / folders.events).
648
660
  * Resolves when the stream ends (server sends `complete` once nothing is in-flight, or the
649
661
  * cap is hit). Abort via the signal to stop watching. Throws 501 if the env has no bus. */
@@ -679,12 +691,16 @@ export class KnowledgeCoreClient extends HttpBase {
679
691
 
680
692
  // --- corpora ---
681
693
  corpora = {
682
- create: (b: { name: string; description?: string }) =>
694
+ create: (b: { name: string; description?: string; custom_metadata?: Record<string, unknown> }) =>
683
695
  this.request<Corpus>("POST", "/v1/corpora", { json: b }),
684
696
  list: (q?: { limit?: number; cursor?: string }) => this.request<Page<Corpus>>("GET", "/v1/corpora", { query: q }),
685
697
  listAll: () => this.pageAll<Corpus>("/v1/corpora"),
698
+ /** Filter corpora by custom_metadata using the same metadata filter as documents. */
699
+ search: (b: { filter?: MetadataFilter; limit?: number; cursor?: string }) =>
700
+ this.request<Page<Corpus>>("POST", "/v1/corpora/search", { json: b }),
686
701
  get: (id: UUID) => this.request<Corpus>("GET", `/v1/corpora/${id}`),
687
- update: (id: UUID, b: { name?: string; description?: string }) =>
702
+ /** `custom_metadata` merges (provided keys upsert, a null value deletes a key). */
703
+ update: (id: UUID, b: { name?: string; description?: string; custom_metadata?: Record<string, unknown> }) =>
688
704
  this.request<Corpus>("PATCH", `/v1/corpora/${id}`, { json: b }),
689
705
  delete: (id: UUID, confirmName: string) => this.request<void>("DELETE", `/v1/corpora/${id}`, { query: { confirm: confirmName } }),
690
706
  listFolders: (id: UUID) => this.pageAll<Folder>(`/v1/corpora/${id}/folders`),
@@ -996,6 +1012,74 @@ export class AdminClient extends HttpBase {
996
1012
  };
997
1013
  }
998
1014
 
1015
+ export interface ChatSendOptions extends StreamHandlers {
1016
+ overrides?: QueryRequest["overrides"];
1017
+ filter?: QueryRequest["filter"];
1018
+ visual?: QueryRequest["visual"];
1019
+ /** Abort on unmount / navigation so the SSE connection doesn't linger. */
1020
+ signal?: AbortSignal;
1021
+ /** Applied ONLY when the conversation is lazily created on the FIRST message. `title` defaults
1022
+ * to the message text (truncated) so a chat is never left untitled. */
1023
+ title?: string;
1024
+ customMetadata?: Record<string, unknown>;
1025
+ ephemeral?: boolean;
1026
+ /** Fired with the conversation id the moment it is created (first message) or resumed — use it to
1027
+ * update the sidebar/URL/route IMMEDIATELY, before the answer streams. */
1028
+ onConversationId?: (id: UUID) => void;
1029
+ }
1030
+
1031
+ /** A chat session bound to ONE conversation (created via `kc.chat(...)`). Every `send()` is ALWAYS
1032
+ * conversational: the conversation_id is attached for you, so a chat turn can never be a
1033
+ * non-persisted one-shot. Combined with server-side persist-on-close salvage + the orphan reaper,
1034
+ * a chat turn is never lost and a chat conversation is never left blank. */
1035
+ export class ChatSession {
1036
+ #kc: KnowledgeCoreClient;
1037
+ #agentId: UUID;
1038
+ #corpusIds: UUID[];
1039
+ #creating: Promise<UUID> | null = null;
1040
+ /** The conversation id — null until the first `send()` (unless resumed). */
1041
+ conversationId: UUID | null;
1042
+
1043
+ constructor(kc: KnowledgeCoreClient, agentId: UUID, corpusIds: UUID[], conversationId: UUID | null) {
1044
+ this.#kc = kc;
1045
+ this.#agentId = agentId;
1046
+ this.#corpusIds = corpusIds;
1047
+ this.conversationId = conversationId;
1048
+ }
1049
+
1050
+ /** Lazy create-once (concurrency-safe): the first send starts creation; a racing send awaits it. */
1051
+ #ensure(o: ChatSendOptions, text: string): Promise<UUID> {
1052
+ if (this.conversationId) return Promise.resolve(this.conversationId);
1053
+ if (!this.#creating) {
1054
+ this.#creating = this.#kc.conversations
1055
+ .create({ title: o.title ?? text.slice(0, 80), custom_metadata: o.customMetadata, ephemeral: o.ephemeral })
1056
+ .then((c) => { this.conversationId = c.id; return c.id; });
1057
+ }
1058
+ return this.#creating;
1059
+ }
1060
+
1061
+ /** Send a chat message (streaming). Lazily creates the conversation on the first message and
1062
+ * ALWAYS passes conversation_id, so the turn is persisted. `onConversationId` fires before the
1063
+ * stream so you can show the conversation immediately. Pass `signal` to cancel on unmount. */
1064
+ async send(text: string, o: ChatSendOptions = {}): Promise<void> {
1065
+ const convId = await this.#ensure(o, text);
1066
+ o.onConversationId?.(convId);
1067
+ await this.#kc.queryStream(
1068
+ this.#agentId,
1069
+ { corpus_ids: this.#corpusIds, query: text, conversation_id: convId,
1070
+ overrides: o.overrides, filter: o.filter, visual: o.visual },
1071
+ o,
1072
+ o.signal,
1073
+ );
1074
+ }
1075
+
1076
+ /** The conversation history (messages), once it exists. Empty page before the first send. */
1077
+ messages(q?: { limit?: number; cursor?: string }): Promise<Page<Message>> {
1078
+ if (!this.conversationId) return Promise.resolve({ items: [], next_cursor: null });
1079
+ return this.#kc.conversations.listMessages(this.conversationId, q);
1080
+ }
1081
+ }
1082
+
999
1083
  // ---------------------------------------------------------------------------
1000
1084
  // helpers
1001
1085
  // ---------------------------------------------------------------------------