@babav/knowledge-core-client 0.31.0 → 0.33.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
@@ -94,36 +94,37 @@ export interface QueryRequest {
94
94
  * "free" = renderer's judgment / not a declared arrangement (also the value for non-conceptual
95
95
  * registers). */
96
96
  export type VisualLayout = "linear-flow" | "ring-cycle" | "nesting" | "radial-hub" | "side-by-side" | "stacked-layers" | "two-state" | "free";
97
- /** A visual attached to a response, anchored by char offsets into `answer` (SAME coordinate
98
- * system as citations).
97
+ /** A visual attached to a response.
99
98
  *
100
- * UNIFORM DELIVERY: every register (chart / structural / conceptual / scenic) carries `image` — a
101
- * PNG served BY KC. To display it, GET `image.url` (resolve a relative path against your KC base
102
- * URL) with your normal API key; the response body is the PNG bytes. The URL is a stable, non-
103
- * expiring KC endpoint for persisted/conversational visuals (`expires_at` absent), so it works on
104
- * reload/copy/export and on later conversation retrieval the client never fetches GCS and never
105
- * parses inline SVG. `rendered_svg` appears ONLY as a rare fail-soft fallback (server rasterization
106
- * failed); prefer `image` and treat `rendered_svg` as a last resort. */
99
+ * IDs-ONLY: the API never returns a URL. Every visual (chart / structural / conceptual / scenic) is
100
+ * a PNG served by KC and identified by its `id`. To display it, fetch the bytes by id —
101
+ * `getVisualImage(visual.id)` for one, or `getVisualImages([...ids])` to fetch a whole answer's
102
+ * visuals in a SINGLE call (no per-visual fan-out). The image is authenticated + tenant-scoped
103
+ * exactly like every other KC call; there is no URL, no signed GCS link. */
107
104
  export interface Visual {
108
105
  id: string;
109
- anchor: {
106
+ /** Char offsets into `answer` (SAME coordinate system as citations). OPTIONAL: a visual may be
107
+ * anchorless (belongs in the trailing strip) — when absent, place it after the answer. */
108
+ anchor?: {
110
109
  start: number;
111
110
  end: number;
112
111
  };
113
112
  register: "chart" | "structural" | "conceptual" | "scenic";
114
- priority: number;
113
+ /** Client display ordering (higher first). OPTIONAL — default to 0 / arrival order if absent. */
114
+ priority?: number;
115
115
  payload: Record<string, unknown>;
116
116
  /** Declared spatial arrangement (meaningful for `conceptual`; "free" otherwise). */
117
117
  layout: VisualLayout;
118
- /** PRIMARY: GET this (relative to your KC base, with your API key) -> PNG bytes. */
119
- image?: {
120
- url: string;
118
+ /** Pixel dimensions of the PNG ALWAYS present on a shipped visual, so the UI can reserve space
119
+ * before the bytes arrive (no layout shift). Fetch the bytes with getVisualImage(id). */
120
+ image: {
121
121
  width: number;
122
122
  height: number;
123
- expires_at?: number;
124
123
  };
125
- /** Fail-soft fallback only (rasterize/store failed); normally absent. */
126
- rendered_svg?: string;
124
+ /** Short KC-authored figure caption (the idea the visual carries). Present when authored. */
125
+ caption?: string;
126
+ /** Plain-text description for screen readers / accessible PDF·DOCX·LaTeX export. Present when authored. */
127
+ alt?: string;
127
128
  }
128
129
  /** Visual stage (streaming): generation has started. MAY be emitted twice — first with
129
130
  * `pending: null` (started, count unknown), then again with the ACCURATE post-vet count before
@@ -132,8 +133,24 @@ export interface VisualsPending {
132
133
  pending: number | null;
133
134
  }
134
135
  export interface VisualsMeta {
136
+ /** Visuals that shipped (authoritative — always matches the number of `visual` events). */
135
137
  count: number;
138
+ /** Concepts accepted by the vet but not shipped (render_failed + timeout). */
136
139
  dropped: number;
140
+ /** Counts-by-cause for every PROPOSED concept that did not become a visible visual, so a missing
141
+ * visual is explainable rather than silent. Keys (zero causes omitted):
142
+ * - `declined` the vet declined/trimmed the concept (never promised; reduces the pending count)
143
+ * - `render_failed` accepted but authoring/rendering/vision-review dropped it
144
+ * - `timeout` accepted but the post-answer render budget cut it off
145
+ * `dropped` = render_failed + timeout; `declined` is extra context beyond `dropped`. */
146
+ dropped_reasons?: Record<string, number>;
147
+ }
148
+ /** One image returned by getVisualImages(). `blob` is the PNG (browser: `URL.createObjectURL(blob)`). */
149
+ export interface VisualImage {
150
+ id: string;
151
+ blob: Blob;
152
+ width: number;
153
+ height: number;
137
154
  }
138
155
  export interface QueryResponse {
139
156
  conversation_id: UUID | null;
@@ -541,6 +558,20 @@ export declare class KnowledgeCoreClient extends HttpBase {
541
558
  * error). Pass `signal` and abort() on unmount / when the user cancels or navigates away so the
542
559
  * connection doesn't linger. Transient (ends on `done`) — no reopen logic needed. */
543
560
  queryStream(agentId: UUID, body: QueryRequest, handlers: StreamHandlers, signal?: AbortSignal): Promise<void>;
561
+ /** Fetch a visual's PNG bytes BY ID. This is the ONLY way to get a visual image — the API never
562
+ * returns a URL. Authenticated + tenant-scoped like every call. Use `visual.id` from a query
563
+ * response / streamed `visual` event / persisted message. Returns a Blob (browser: `URL.
564
+ * createObjectURL(blob)` for an <img>; Node: `Buffer.from(await blob.arrayBuffer())`). */
565
+ getVisualImage(visualId: UUID | string, signal?: AbortSignal): Promise<Blob>;
566
+ /** Fetch MANY visual PNGs in ONE call — the way to load all of an answer's (or a conversation's)
567
+ * visuals without a per-visual round trip. Pass up to 32 ids; over that, split into chunks (the
568
+ * API returns 400 `too_many_ids`). Returns the found images (as Blobs, ready for
569
+ * `URL.createObjectURL`) plus `missing` — ids with no stored image (reaped/expired), which the UI
570
+ * can show as "visual no longer available". Order of `images` is not guaranteed; key by `id`. */
571
+ getVisualImages(visualIds: Array<UUID | string>, signal?: AbortSignal): Promise<{
572
+ images: VisualImage[];
573
+ missing: string[];
574
+ }>;
544
575
  /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
545
576
  * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
546
577
  * `query`/`queryStream` ONLY for programmatic one-shots (tools).
package/dist/index.js CHANGED
@@ -178,6 +178,33 @@ export class KnowledgeCoreClient extends HttpBase {
178
178
  if (buf.trim())
179
179
  dispatchSse(buf, handlers);
180
180
  }
181
+ /** Fetch a visual's PNG bytes BY ID. This is the ONLY way to get a visual image — the API never
182
+ * returns a URL. Authenticated + tenant-scoped like every call. Use `visual.id` from a query
183
+ * response / streamed `visual` event / persisted message. Returns a Blob (browser: `URL.
184
+ * createObjectURL(blob)` for an <img>; Node: `Buffer.from(await blob.arrayBuffer())`). */
185
+ async getVisualImage(visualId, signal) {
186
+ const res = await this.raw("GET", `/v1/visuals/${visualId}/image`, { signal });
187
+ if (!res.ok) {
188
+ const t = await res.text();
189
+ throw new KnowledgeCoreError(res.status, safeJson(t), `/v1/visuals/${visualId}/image`);
190
+ }
191
+ return await res.blob();
192
+ }
193
+ /** Fetch MANY visual PNGs in ONE call — the way to load all of an answer's (or a conversation's)
194
+ * visuals without a per-visual round trip. Pass up to 32 ids; over that, split into chunks (the
195
+ * API returns 400 `too_many_ids`). Returns the found images (as Blobs, ready for
196
+ * `URL.createObjectURL`) plus `missing` — ids with no stored image (reaped/expired), which the UI
197
+ * can show as "visual no longer available". Order of `images` is not guaranteed; key by `id`. */
198
+ async getVisualImages(visualIds, signal) {
199
+ if (visualIds.length === 0)
200
+ return { images: [], missing: [] };
201
+ const r = await this.request("POST", "/v1/visuals/images", { json: { ids: visualIds }, signal });
202
+ const images = r.images.map((im) => {
203
+ const bytes = Uint8Array.from(atob(im.png_base64), (c) => c.charCodeAt(0));
204
+ return { id: im.id, blob: new Blob([bytes], { type: "image/png" }), width: im.width, height: im.height };
205
+ });
206
+ return { images, missing: r.missing ?? [] };
207
+ }
181
208
  /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
182
209
  * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
183
210
  * `query`/`queryStream` ONLY for programmatic one-shots (tools).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babav/knowledge-core-client",
3
- "version": "0.31.0",
3
+ "version": "0.33.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
@@ -128,28 +128,31 @@ export type VisualLayout =
128
128
  | "two-state"
129
129
  | "free";
130
130
 
131
- /** A visual attached to a response, anchored by char offsets into `answer` (SAME coordinate
132
- * system as citations).
131
+ /** A visual attached to a response.
133
132
  *
134
- * UNIFORM DELIVERY: every register (chart / structural / conceptual / scenic) carries `image` — a
135
- * PNG served BY KC. To display it, GET `image.url` (resolve a relative path against your KC base
136
- * URL) with your normal API key; the response body is the PNG bytes. The URL is a stable, non-
137
- * expiring KC endpoint for persisted/conversational visuals (`expires_at` absent), so it works on
138
- * reload/copy/export and on later conversation retrieval the client never fetches GCS and never
139
- * parses inline SVG. `rendered_svg` appears ONLY as a rare fail-soft fallback (server rasterization
140
- * failed); prefer `image` and treat `rendered_svg` as a last resort. */
133
+ * IDs-ONLY: the API never returns a URL. Every visual (chart / structural / conceptual / scenic) is
134
+ * a PNG served by KC and identified by its `id`. To display it, fetch the bytes by id —
135
+ * `getVisualImage(visual.id)` for one, or `getVisualImages([...ids])` to fetch a whole answer's
136
+ * visuals in a SINGLE call (no per-visual fan-out). The image is authenticated + tenant-scoped
137
+ * exactly like every other KC call; there is no URL, no signed GCS link. */
141
138
  export interface Visual {
142
139
  id: string;
143
- anchor: { start: number; end: number };
140
+ /** Char offsets into `answer` (SAME coordinate system as citations). OPTIONAL: a visual may be
141
+ * anchorless (belongs in the trailing strip) — when absent, place it after the answer. */
142
+ anchor?: { start: number; end: number };
144
143
  register: "chart" | "structural" | "conceptual" | "scenic";
145
- priority: number;
146
- payload: Record<string, unknown>; // {kind: "grammar"|"vega_lite"|"svg"|"image", ...}
144
+ /** Client display ordering (higher first). OPTIONAL — default to 0 / arrival order if absent. */
145
+ priority?: number;
146
+ payload: Record<string, unknown>; // {kind: "grammar"|"vega_lite"|"image", ...}
147
147
  /** Declared spatial arrangement (meaningful for `conceptual`; "free" otherwise). */
148
148
  layout: VisualLayout;
149
- /** PRIMARY: GET this (relative to your KC base, with your API key) -> PNG bytes. */
150
- image?: { url: string; width: number; height: number; expires_at?: number };
151
- /** Fail-soft fallback only (rasterize/store failed); normally absent. */
152
- rendered_svg?: string;
149
+ /** Pixel dimensions of the PNG ALWAYS present on a shipped visual, so the UI can reserve space
150
+ * before the bytes arrive (no layout shift). Fetch the bytes with getVisualImage(id). */
151
+ image: { width: number; height: number };
152
+ /** Short KC-authored figure caption (the idea the visual carries). Present when authored. */
153
+ caption?: string;
154
+ /** Plain-text description for screen readers / accessible PDF·DOCX·LaTeX export. Present when authored. */
155
+ alt?: string;
153
156
  }
154
157
 
155
158
  /** Visual stage (streaming): generation has started. MAY be emitted twice — first with
@@ -160,8 +163,25 @@ export interface VisualsPending {
160
163
  }
161
164
 
162
165
  export interface VisualsMeta {
166
+ /** Visuals that shipped (authoritative — always matches the number of `visual` events). */
163
167
  count: number;
168
+ /** Concepts accepted by the vet but not shipped (render_failed + timeout). */
164
169
  dropped: number;
170
+ /** Counts-by-cause for every PROPOSED concept that did not become a visible visual, so a missing
171
+ * visual is explainable rather than silent. Keys (zero causes omitted):
172
+ * - `declined` the vet declined/trimmed the concept (never promised; reduces the pending count)
173
+ * - `render_failed` accepted but authoring/rendering/vision-review dropped it
174
+ * - `timeout` accepted but the post-answer render budget cut it off
175
+ * `dropped` = render_failed + timeout; `declined` is extra context beyond `dropped`. */
176
+ dropped_reasons?: Record<string, number>;
177
+ }
178
+
179
+ /** One image returned by getVisualImages(). `blob` is the PNG (browser: `URL.createObjectURL(blob)`). */
180
+ export interface VisualImage {
181
+ id: string;
182
+ blob: Blob;
183
+ width: number;
184
+ height: number;
165
185
  }
166
186
 
167
187
  export interface QueryResponse {
@@ -359,7 +379,7 @@ export interface Message {
359
379
  groundedness: Record<string, unknown> | null;
360
380
  retrieval_contents: unknown[] | null;
361
381
  usage: Record<string, unknown> | null;
362
- visuals?: { items: Visual[]; meta: VisualsMeta } | null; // post-generation visuals (re-signed URLs on read)
382
+ visuals?: { items: Visual[]; meta: VisualsMeta } | null; // persisted visuals; fetch bytes by id (getVisualImages)
363
383
  }
364
384
  export interface Attachment {
365
385
  id: UUID;
@@ -655,6 +675,39 @@ export class KnowledgeCoreClient extends HttpBase {
655
675
  if (buf.trim()) dispatchSse(buf, handlers);
656
676
  }
657
677
 
678
+ /** Fetch a visual's PNG bytes BY ID. This is the ONLY way to get a visual image — the API never
679
+ * returns a URL. Authenticated + tenant-scoped like every call. Use `visual.id` from a query
680
+ * response / streamed `visual` event / persisted message. Returns a Blob (browser: `URL.
681
+ * createObjectURL(blob)` for an <img>; Node: `Buffer.from(await blob.arrayBuffer())`). */
682
+ async getVisualImage(visualId: UUID | string, signal?: AbortSignal): Promise<Blob> {
683
+ const res = await this.raw("GET", `/v1/visuals/${visualId}/image`, { signal });
684
+ if (!res.ok) {
685
+ const t = await res.text();
686
+ throw new KnowledgeCoreError(res.status, safeJson(t), `/v1/visuals/${visualId}/image`);
687
+ }
688
+ return await res.blob();
689
+ }
690
+
691
+ /** Fetch MANY visual PNGs in ONE call — the way to load all of an answer's (or a conversation's)
692
+ * visuals without a per-visual round trip. Pass up to 32 ids; over that, split into chunks (the
693
+ * API returns 400 `too_many_ids`). Returns the found images (as Blobs, ready for
694
+ * `URL.createObjectURL`) plus `missing` — ids with no stored image (reaped/expired), which the UI
695
+ * can show as "visual no longer available". Order of `images` is not guaranteed; key by `id`. */
696
+ async getVisualImages(
697
+ visualIds: Array<UUID | string>,
698
+ signal?: AbortSignal,
699
+ ): Promise<{ images: VisualImage[]; missing: string[] }> {
700
+ if (visualIds.length === 0) return { images: [], missing: [] };
701
+ const r = await this.request<{ images: Array<{ id: string; png_base64: string; width: number; height: number }>; missing: string[] }>(
702
+ "POST", "/v1/visuals/images", { json: { ids: visualIds }, signal },
703
+ );
704
+ const images = r.images.map((im) => {
705
+ const bytes = Uint8Array.from(atob(im.png_base64), (c) => c.charCodeAt(0));
706
+ return { id: im.id, blob: new Blob([bytes], { type: "image/png" }), width: im.width, height: im.height };
707
+ });
708
+ return { images, missing: r.missing ?? [] };
709
+ }
710
+
658
711
  /** DUMMY-PROOF CHAT. Every message goes through a conversation — it is structurally impossible
659
712
  * to send a chat turn as a non-persisted one-shot. Use this for ANY chat UI. Use the low-level
660
713
  * `query`/`queryStream` ONLY for programmatic one-shots (tools).