@warlock.js/ai 4.5.0 → 4.6.1
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/CHANGELOG.md +14 -0
- package/cjs/index.cjs +20 -1
- package/cjs/{src-DFibP2FQ.cjs → src-Bmajk4Qg.cjs} +1 -1
- package/cjs/{src-C02yzsLs.cjs → src-OZyDYHxm.cjs} +2789 -691
- package/cjs/src-OZyDYHxm.cjs.map +1 -0
- package/esm/agent/agent-config.type.d.mts +29 -0
- package/esm/agent/agent-config.type.d.mts.map +1 -1
- package/esm/agent/agent.d.mts.map +1 -1
- package/esm/agent/agent.mjs +126 -7
- package/esm/agent/agent.mjs.map +1 -1
- package/esm/agent/signature.mjs +57 -0
- package/esm/agent/signature.mjs.map +1 -0
- package/esm/agent/snapshot.mjs +101 -0
- package/esm/agent/snapshot.mjs.map +1 -0
- package/esm/ai-openai/src/image.mjs +5 -0
- package/esm/ai-openai/src/index.mjs +3 -0
- package/esm/ai-openai/src/sdk.mjs +3 -0
- package/esm/ai-openai/src/speech.mjs +5 -0
- package/esm/ai-openai/src/transcription.mjs +6 -0
- package/esm/ai-openai/src/utils/index.mjs +1 -0
- package/esm/ai-openai/src/utils/to-openai-messages.mjs +3 -0
- package/esm/ai.d.mts +45 -0
- package/esm/ai.d.mts.map +1 -1
- package/esm/ai.mjs +37 -1
- package/esm/ai.mjs.map +1 -1
- package/esm/contracts/agent/agent-options.type.d.mts +22 -2
- package/esm/contracts/agent/agent-options.type.d.mts.map +1 -1
- package/esm/contracts/agent/agent-snapshot.type.d.mts +90 -0
- package/esm/contracts/agent/agent-snapshot.type.d.mts.map +1 -0
- package/esm/contracts/agent/agent.contract.d.mts +29 -1
- package/esm/contracts/agent/agent.contract.d.mts.map +1 -1
- package/esm/contracts/agent/index.d.mts +2 -1
- package/esm/contracts/image-model.contract.d.mts +156 -0
- package/esm/contracts/image-model.contract.d.mts.map +1 -0
- package/esm/contracts/index.d.mts +8 -3
- package/esm/contracts/planner/index.d.mts +3 -2
- package/esm/contracts/planner/planner-config.type.d.mts +30 -0
- package/esm/contracts/planner/planner-config.type.d.mts.map +1 -1
- package/esm/contracts/planner/planner-execute-options.type.d.mts +13 -1
- package/esm/contracts/planner/planner-execute-options.type.d.mts.map +1 -1
- package/esm/contracts/planner/planner-snapshot.type.d.mts +77 -0
- package/esm/contracts/planner/planner-snapshot.type.d.mts.map +1 -0
- package/esm/contracts/planner/planner.contract.d.mts +21 -1
- package/esm/contracts/planner/planner.contract.d.mts.map +1 -1
- package/esm/contracts/result/base-report.type.d.mts +1 -1
- package/esm/contracts/result/base-report.type.d.mts.map +1 -1
- package/esm/contracts/result/base-report.type.mjs.map +1 -1
- package/esm/contracts/sdk-adapter.contract.d.mts +37 -0
- package/esm/contracts/sdk-adapter.contract.d.mts.map +1 -1
- package/esm/contracts/speech-model.contract.d.mts +97 -0
- package/esm/contracts/speech-model.contract.d.mts.map +1 -0
- package/esm/contracts/transcription-model.contract.d.mts +101 -0
- package/esm/contracts/transcription-model.contract.d.mts.map +1 -0
- package/esm/errors/agent-drift-error.d.mts +32 -0
- package/esm/errors/agent-drift-error.d.mts.map +1 -0
- package/esm/errors/agent-drift-error.mjs +31 -0
- package/esm/errors/agent-drift-error.mjs.map +1 -0
- package/esm/errors/error-code.type.d.mts +1 -1
- package/esm/errors/index.d.mts +2 -0
- package/esm/errors/index.mjs +2 -0
- package/esm/errors/planner-drift-error.d.mts +34 -0
- package/esm/errors/planner-drift-error.d.mts.map +1 -0
- package/esm/errors/planner-drift-error.mjs +33 -0
- package/esm/errors/planner-drift-error.mjs.map +1 -0
- package/esm/image/image-cost.d.mts +32 -0
- package/esm/image/image-cost.d.mts.map +1 -0
- package/esm/image/image-cost.mjs +55 -0
- package/esm/image/image-cost.mjs.map +1 -0
- package/esm/image/image.d.mts +92 -0
- package/esm/image/image.d.mts.map +1 -0
- package/esm/image/image.mjs +113 -0
- package/esm/image/image.mjs.map +1 -0
- package/esm/image/index.mjs +4 -0
- package/esm/index.d.mts +26 -4
- package/esm/index.mjs +20 -1
- package/esm/mock/index.d.mts +3 -0
- package/esm/mock/index.mjs +3 -0
- package/esm/mock/mock-config.type.d.mts +22 -0
- package/esm/mock/mock-config.type.d.mts.map +1 -1
- package/esm/mock/mock-image-model.d.mts +41 -0
- package/esm/mock/mock-image-model.d.mts.map +1 -0
- package/esm/mock/mock-image-model.mjs +52 -0
- package/esm/mock/mock-image-model.mjs.map +1 -0
- package/esm/mock/mock-sdk.d.mts +7 -1
- package/esm/mock/mock-sdk.d.mts.map +1 -1
- package/esm/mock/mock-sdk.mjs +27 -0
- package/esm/mock/mock-sdk.mjs.map +1 -1
- package/esm/mock/mock-speech-model.d.mts +31 -0
- package/esm/mock/mock-speech-model.d.mts.map +1 -0
- package/esm/mock/mock-speech-model.mjs +39 -0
- package/esm/mock/mock-speech-model.mjs.map +1 -0
- package/esm/mock/mock-transcription-model.d.mts +32 -0
- package/esm/mock/mock-transcription-model.d.mts.map +1 -0
- package/esm/mock/mock-transcription-model.mjs +36 -0
- package/esm/mock/mock-transcription-model.mjs.map +1 -0
- package/esm/planner/planner-run.d.mts +8 -0
- package/esm/planner/planner-run.d.mts.map +1 -1
- package/esm/planner/planner-run.mjs +161 -6
- package/esm/planner/planner-run.mjs.map +1 -1
- package/esm/planner/planner.d.mts.map +1 -1
- package/esm/planner/planner.mjs +25 -1
- package/esm/planner/planner.mjs.map +1 -1
- package/esm/planner/snapshot.mjs +95 -0
- package/esm/planner/snapshot.mjs.map +1 -0
- package/esm/rag/index.d.mts +7 -0
- package/esm/rag/index.mjs +7 -0
- package/esm/rag/loaders/errors.d.mts +19 -0
- package/esm/rag/loaders/errors.d.mts.map +1 -0
- package/esm/rag/loaders/errors.mjs +25 -0
- package/esm/rag/loaders/errors.mjs.map +1 -0
- package/esm/rag/loaders/index.mjs +7 -0
- package/esm/rag/loaders/load-html.d.mts +26 -0
- package/esm/rag/loaders/load-html.d.mts.map +1 -0
- package/esm/rag/loaders/load-html.mjs +138 -0
- package/esm/rag/loaders/load-html.mjs.map +1 -0
- package/esm/rag/loaders/load-pdf.d.mts +38 -0
- package/esm/rag/loaders/load-pdf.d.mts.map +1 -0
- package/esm/rag/loaders/load-pdf.mjs +150 -0
- package/esm/rag/loaders/load-pdf.mjs.map +1 -0
- package/esm/rag/loaders/load-text.d.mts +47 -0
- package/esm/rag/loaders/load-text.d.mts.map +1 -0
- package/esm/rag/loaders/load-text.mjs +60 -0
- package/esm/rag/loaders/load-text.mjs.map +1 -0
- package/esm/rag/loaders/load-web.d.mts +42 -0
- package/esm/rag/loaders/load-web.d.mts.map +1 -0
- package/esm/rag/loaders/load-web.mjs +89 -0
- package/esm/rag/loaders/load-web.mjs.map +1 -0
- package/esm/rag/loaders/loader.type.d.mts +89 -0
- package/esm/rag/loaders/loader.type.d.mts.map +1 -0
- package/esm/rag/store/pg-vector-store.d.mts +139 -0
- package/esm/rag/store/pg-vector-store.d.mts.map +1 -0
- package/esm/rag/store/pg-vector-store.mjs +328 -0
- package/esm/rag/store/pg-vector-store.mjs.map +1 -0
- package/esm/speech/index.mjs +3 -0
- package/esm/speech/speech.d.mts +65 -0
- package/esm/speech/speech.d.mts.map +1 -0
- package/esm/speech/speech.mjs +123 -0
- package/esm/speech/speech.mjs.map +1 -0
- package/esm/supervisor/entries.mjs +2 -2
- package/esm/supervisor/entries.mjs.map +1 -1
- package/esm/transcribe/audio-input.d.mts +47 -0
- package/esm/transcribe/audio-input.d.mts.map +1 -0
- package/esm/transcribe/audio-input.mjs +84 -0
- package/esm/transcribe/audio-input.mjs.map +1 -0
- package/esm/transcribe/index.mjs +4 -0
- package/esm/transcribe/transcribe.d.mts +64 -0
- package/esm/transcribe/transcribe.d.mts.map +1 -0
- package/esm/transcribe/transcribe.mjs +128 -0
- package/esm/transcribe/transcribe.mjs.map +1 -0
- package/llms-full.txt +753 -0
- package/llms.txt +5 -0
- package/package.json +3 -3
- package/skills/README.md +4 -0
- package/skills/durable-agent-runs/SKILL.md +135 -0
- package/skills/generate-images/SKILL.md +138 -0
- package/skills/generate-speech/SKILL.md +139 -0
- package/skills/rag-loaders-and-stores/SKILL.md +164 -0
- package/skills/transcribe-audio/SKILL.md +157 -0
- package/cjs/src-C02yzsLs.cjs.map +0 -1
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
//#region ../@warlock.js/ai/src/rag/store/pg-vector-store.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default backing table — provisions the store with no extra config when
|
|
4
|
+
* the dev runs {@link VectorStore.schema | ensureSchema} through their
|
|
5
|
+
* migration tool.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_TABLE = "warlock_ai_rag_vectors";
|
|
8
|
+
/** Default embedding width baked into the DDL (OpenAI `text-embedding-3-small`). */
|
|
9
|
+
const DEFAULT_DIMENSIONS = 1536;
|
|
10
|
+
/** Default `ivfflat` list count when that index strategy is chosen. */
|
|
11
|
+
const DEFAULT_IVFFLAT_LISTS = 100;
|
|
12
|
+
/**
|
|
13
|
+
* Allowed characters in a Postgres identifier (table name). The table name
|
|
14
|
+
* is interpolated into DDL/DML, so anything outside this conservative
|
|
15
|
+
* ASCII subset is rejected — interpolating an arbitrary string would be a
|
|
16
|
+
* SQL-injection footgun (mirrors the snapshot / human-interrupt pg stores
|
|
17
|
+
* and `@warlock.js/cache`'s `PgCacheDriver`).
|
|
18
|
+
*/
|
|
19
|
+
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
20
|
+
/**
|
|
21
|
+
* Module specifier for the optional `pg` driver. Held in a `string`
|
|
22
|
+
* variable so the dynamic `import()` is not statically resolved at compile
|
|
23
|
+
* time — `pg` is an optional peer that need not be installed for this
|
|
24
|
+
* package to type-check or for a cache-only consumer to run.
|
|
25
|
+
*/
|
|
26
|
+
const PG_MODULE = "pg";
|
|
27
|
+
/**
|
|
28
|
+
* Curated install string surfaced (at use time) when a `connectionString`
|
|
29
|
+
* is configured but the optional `pg` driver is absent. Never thrown at
|
|
30
|
+
* import — a cache-only consumer must be able to load this module.
|
|
31
|
+
*/
|
|
32
|
+
const PG_INSTALL_INSTRUCTIONS = `
|
|
33
|
+
The @warlock.js/ai Postgres vector store requires the pg package and a
|
|
34
|
+
Postgres database with the pgvector extension. Install the driver with:
|
|
35
|
+
|
|
36
|
+
npm install pg
|
|
37
|
+
|
|
38
|
+
Or with your preferred package manager:
|
|
39
|
+
|
|
40
|
+
pnpm add pg
|
|
41
|
+
yarn add pg
|
|
42
|
+
`.trim();
|
|
43
|
+
/**
|
|
44
|
+
* Lazily import `pg` and return a `Pool` built from `connectionString`. A
|
|
45
|
+
* bare `catch` rethrows the curated install string — a missing optional
|
|
46
|
+
* peer surfaces as actionable guidance, never a raw resolution error.
|
|
47
|
+
*/
|
|
48
|
+
async function buildPgClient(connectionString) {
|
|
49
|
+
let sdk;
|
|
50
|
+
try {
|
|
51
|
+
sdk = await import(PG_MODULE);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(PG_INSTALL_INSTRUCTIONS);
|
|
54
|
+
}
|
|
55
|
+
return new sdk.Pool({ connectionString });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Serialize a JS `number[]` to the pgvector text literal: `[1,2,3]`.
|
|
59
|
+
* pgvector accepts a vector either as this bracketed literal or via a
|
|
60
|
+
* typed parameter; passing the literal string + an explicit `::vector`
|
|
61
|
+
* cast keeps the store driver-agnostic (no dependency on a registered
|
|
62
|
+
* `pg` type parser).
|
|
63
|
+
*
|
|
64
|
+
* Non-finite components (`NaN` / `±Infinity`) are rejected — pgvector
|
|
65
|
+
* stores only finite floats, and silently coercing them would corrupt the
|
|
66
|
+
* index. The check is cheap relative to the embed call that produced the
|
|
67
|
+
* vector.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* vectorLiteral([1, 0.5, -2]); // "[1,0.5,-2]"
|
|
71
|
+
*/
|
|
72
|
+
function vectorLiteral(vector) {
|
|
73
|
+
let literal = "[";
|
|
74
|
+
for (let index = 0; index < vector.length; index++) {
|
|
75
|
+
const component = vector[index];
|
|
76
|
+
if (!Number.isFinite(component)) throw new TypeError(`pgVectorStore: embedding component at index ${index} is not finite (${component}); pgvector stores only finite floats.`);
|
|
77
|
+
if (index > 0) literal += ",";
|
|
78
|
+
literal += String(component);
|
|
79
|
+
}
|
|
80
|
+
return literal + "]";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Coerce a `value` JSONB column back into the stored payload. node-postgres
|
|
84
|
+
* parses `JSONB` into a JS value already, but some pool wrappers hand back
|
|
85
|
+
* the raw string — be defensive across both (mirrors the snapshot store's
|
|
86
|
+
* `parsePayload`).
|
|
87
|
+
*/
|
|
88
|
+
function parseValue(value) {
|
|
89
|
+
if (typeof value === "string") return JSON.parse(value);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Coerce a pgvector cosine **distance** (`<=>`, in `[0, 2]`, 0 = identical)
|
|
94
|
+
* into the cosine **similarity** score the {@link VectorStore} contract
|
|
95
|
+
* declares (`[0, 1]`, 1 = identical). `pg` returns the computed distance
|
|
96
|
+
* column as a string for `double precision`; parse then map `1 - distance`,
|
|
97
|
+
* clamped to `[0, 1]` so a tiny floating-point overshoot never yields a
|
|
98
|
+
* score just outside the contract's range.
|
|
99
|
+
*/
|
|
100
|
+
function distanceToScore(distance) {
|
|
101
|
+
const score = 1 - (typeof distance === "string" ? Number(distance) : distance);
|
|
102
|
+
if (score < 0) return 0;
|
|
103
|
+
if (score > 1) return 1;
|
|
104
|
+
return score;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Postgres + pgvector-backed {@link VectorStore} — one durable row per
|
|
108
|
+
* indexed chunk, keyed by the RAG pipeline's dotted `key`
|
|
109
|
+
* (`ai.rag.<name>.<sourceId>.<chunkIndex>`), with the chunk payload in a
|
|
110
|
+
* `value` JSONB column and the embedding in a `vector` column.
|
|
111
|
+
*
|
|
112
|
+
* Owns: the three RAG vector operations against a pgvector index —
|
|
113
|
+
* `upsert` (INSERT … ON CONFLICT DO UPDATE), `query` (cosine
|
|
114
|
+
* `ORDER BY embedding <=> $vec` with a `threshold` floor + optional `tags`
|
|
115
|
+
* overlap filter, capped at `topK`), and `removeNamespace` (prefix DELETE).
|
|
116
|
+
* Also emits the reference DDL via {@link PgVectorStore.schema} (alias
|
|
117
|
+
* {@link PgVectorStore.ensureSchema}).
|
|
118
|
+
*
|
|
119
|
+
* Does NOT own: the connection lifecycle (a dev-supplied `client` is never
|
|
120
|
+
* closed; a store-built `Pool` from a `connectionString` is also left open
|
|
121
|
+
* for the process to reuse) or schema migration — the dev runs the DDL
|
|
122
|
+
* through their own tool; the framework never auto-migrates.
|
|
123
|
+
*
|
|
124
|
+
* Front it with the {@link pgVectorStore} factory — callers never `new` it.
|
|
125
|
+
*/
|
|
126
|
+
var PgVectorStore = class {
|
|
127
|
+
constructor(options) {
|
|
128
|
+
const table = options.table ?? DEFAULT_TABLE;
|
|
129
|
+
if (!SAFE_IDENTIFIER.test(table)) throw new TypeError(`pgVectorStore: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`);
|
|
130
|
+
this.table = table;
|
|
131
|
+
this.dimensions = options.dimensions ?? DEFAULT_DIMENSIONS;
|
|
132
|
+
this.index = options.index ?? "hnsw";
|
|
133
|
+
this.ivfflatLists = options.ivfflatLists ?? DEFAULT_IVFFLAT_LISTS;
|
|
134
|
+
if (options.client) {
|
|
135
|
+
if (typeof options.client.query !== "function") throw new TypeError("pgVectorStore requires a 'client' option implementing { query(text, params) } — pass a pg.Pool or pg.Client.");
|
|
136
|
+
this.clientPromise = Promise.resolve(options.client);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (options.connectionString) {
|
|
140
|
+
this.clientPromise = buildPgClient(options.connectionString);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
throw new TypeError("pgVectorStore requires either a 'client' or a 'connectionString' option.");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the backing client, surfacing the lazy `pg` import's curated
|
|
147
|
+
* install string on the first call that needs it.
|
|
148
|
+
*/
|
|
149
|
+
client() {
|
|
150
|
+
return this.clientPromise;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Index `value` under `key` with its embedding `vector`. Upserts on the
|
|
154
|
+
* `key` primary key — re-indexing the same chunk overwrites its payload,
|
|
155
|
+
* embedding, and tags rather than appending a duplicate row. Optional
|
|
156
|
+
* `tags` ride a `text[]` column so {@link query} can restrict the
|
|
157
|
+
* candidate set with an array-overlap filter.
|
|
158
|
+
*
|
|
159
|
+
* The embedding is sent as a pgvector text literal (`$3`) cast to
|
|
160
|
+
* `::vector`, so the store needs no registered `pg` type parser. `tags`
|
|
161
|
+
* defaults to an empty array (never `NULL`) to keep the overlap filter's
|
|
162
|
+
* `&&` semantics simple.
|
|
163
|
+
*/
|
|
164
|
+
async upsert(key, value, vector, tags) {
|
|
165
|
+
await (await this.client()).query(`INSERT INTO ${this.table} (key, value, embedding, tags)
|
|
166
|
+
VALUES ($1, $2::jsonb, $3::vector, $4::text[])
|
|
167
|
+
ON CONFLICT (key) DO UPDATE
|
|
168
|
+
SET value = EXCLUDED.value,
|
|
169
|
+
embedding = EXCLUDED.embedding,
|
|
170
|
+
tags = EXCLUDED.tags`, [
|
|
171
|
+
key,
|
|
172
|
+
JSON.stringify(value),
|
|
173
|
+
vectorLiteral(vector),
|
|
174
|
+
tags ?? []
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Return the cosine-nearest rows to `vector`, mapped to the contract's
|
|
179
|
+
* `{ key, value, score }` shape. The SQL:
|
|
180
|
+
*
|
|
181
|
+
* - computes `embedding <=> $1::vector` (cosine **distance**) once, aliased
|
|
182
|
+
* `distance`, and `ORDER BY` it ascending (nearest first);
|
|
183
|
+
* - applies the `threshold` floor as `distance <= 1 - threshold`
|
|
184
|
+
* (similarity `>=` threshold), so the default `0.5` floor maps to a
|
|
185
|
+
* `<= 0.5` distance bound — the filter runs in SQL, not in JS, so a
|
|
186
|
+
* below-floor row never crosses the wire;
|
|
187
|
+
* - when `tags` are given, restricts to rows whose `tags` array overlaps
|
|
188
|
+
* the requested set via `tags && $tags::text[]` (one-of semantics,
|
|
189
|
+
* matching the cache store);
|
|
190
|
+
* - caps the result at `topK` with `LIMIT`.
|
|
191
|
+
*
|
|
192
|
+
* The returned `score` is `1 - distance`, clamped to `[0, 1]`, so callers
|
|
193
|
+
* see the same cosine-similarity scale the cache store emits.
|
|
194
|
+
*/
|
|
195
|
+
async query(vector, options) {
|
|
196
|
+
const client = await this.client();
|
|
197
|
+
const params = [vectorLiteral(vector), options.topK];
|
|
198
|
+
const conditions = [];
|
|
199
|
+
if (options.threshold !== void 0) {
|
|
200
|
+
params.push(1 - options.threshold);
|
|
201
|
+
conditions.push(`(embedding <=> $1::vector) <= $${params.length}`);
|
|
202
|
+
}
|
|
203
|
+
if (options.tags !== void 0 && options.tags.length > 0) {
|
|
204
|
+
params.push(options.tags);
|
|
205
|
+
conditions.push(`tags && $${params.length}::text[]`);
|
|
206
|
+
}
|
|
207
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
208
|
+
const { rows } = await client.query(`SELECT key, value, (embedding <=> $1::vector) AS distance
|
|
209
|
+
FROM ${this.table}
|
|
210
|
+
${where}
|
|
211
|
+
ORDER BY embedding <=> $1::vector
|
|
212
|
+
LIMIT $2`, params);
|
|
213
|
+
return rows.map((row) => ({
|
|
214
|
+
key: row.key,
|
|
215
|
+
value: parseValue(row.value),
|
|
216
|
+
score: distanceToScore(row.distance)
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Drop every entry written under `namespace`. The RAG pipeline keys
|
|
221
|
+
* chunks as `<namespace>.<sourceId>.<chunkIndex>`, so a row belongs to
|
|
222
|
+
* the namespace when its `key` equals it OR begins with `<namespace>.`
|
|
223
|
+
* — deleting `ai.rag.docs` must not also catch `ai.rag.docs2`. The `_`
|
|
224
|
+
* and `%` LIKE wildcards in the namespace are escaped so a namespace
|
|
225
|
+
* that happens to contain them is matched literally.
|
|
226
|
+
*/
|
|
227
|
+
async removeNamespace(namespace) {
|
|
228
|
+
const client = await this.client();
|
|
229
|
+
const escaped = namespace.replace(/\\/g, "\\\\").replace(/_/g, "\\_").replace(/%/g, "\\%");
|
|
230
|
+
await client.query(`DELETE FROM ${this.table}
|
|
231
|
+
WHERE key = $1 OR key LIKE $2 ESCAPE '\\'`, [namespace, `${escaped}.%`]);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Return the reference migration DDL for this store's backing table,
|
|
235
|
+
* interpolating the configured table name, embedding width, and ANN
|
|
236
|
+
* index strategy. The dev runs it once through their migration tool —
|
|
237
|
+
* the framework never auto-migrates.
|
|
238
|
+
*
|
|
239
|
+
* The emitted statements:
|
|
240
|
+
* 1. `CREATE EXTENSION IF NOT EXISTS vector;` — enables pgvector (needs
|
|
241
|
+
* a superuser or a role with `CREATE` on the database the first time).
|
|
242
|
+
* 2. `CREATE TABLE IF NOT EXISTS <table> (key TEXT PRIMARY KEY, value
|
|
243
|
+
* JSONB NOT NULL, embedding vector(<dimensions>) NOT NULL, tags
|
|
244
|
+
* text[] NOT NULL DEFAULT '{}');`
|
|
245
|
+
* 3. A GIN index on `tags` so the array-overlap filter stays sargable.
|
|
246
|
+
* 4. The chosen ANN index over `embedding` using `vector_cosine_ops`:
|
|
247
|
+
* - `"hnsw"` → `USING hnsw (embedding vector_cosine_ops)`;
|
|
248
|
+
* - `"ivfflat"` → `USING ivfflat (embedding vector_cosine_ops)
|
|
249
|
+
* WITH (lists = <ivfflatLists>)`;
|
|
250
|
+
* - `"none"` → emitted as a comment (exact scan, no ANN index).
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* const store = pgVectorStore({ client: pool, dimensions: 1536 });
|
|
254
|
+
* await pool.query(store.ensureSchema());
|
|
255
|
+
*/
|
|
256
|
+
schema() {
|
|
257
|
+
const lines = [
|
|
258
|
+
`CREATE EXTENSION IF NOT EXISTS vector;`,
|
|
259
|
+
`CREATE TABLE IF NOT EXISTS ${this.table} (`,
|
|
260
|
+
` key TEXT PRIMARY KEY,`,
|
|
261
|
+
` value JSONB NOT NULL,`,
|
|
262
|
+
` embedding vector(${this.dimensions}) NOT NULL,`,
|
|
263
|
+
` tags TEXT[] NOT NULL DEFAULT '{}'`,
|
|
264
|
+
`);`,
|
|
265
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.table}_tags`,
|
|
266
|
+
` ON ${this.table} USING gin (tags);`
|
|
267
|
+
];
|
|
268
|
+
if (this.index === "hnsw") lines.push(`CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding`, ` ON ${this.table} USING hnsw (embedding vector_cosine_ops);`);
|
|
269
|
+
else if (this.index === "ivfflat") lines.push(`CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding`, ` ON ${this.table} USING ivfflat (embedding vector_cosine_ops)`, ` WITH (lists = ${this.ivfflatLists});`);
|
|
270
|
+
else lines.push(`-- No ANN index requested (index: "none"): cosine queries fall back`, `-- to an exact sequential scan, which is correct but linear in rows.`);
|
|
271
|
+
return lines.join("\n");
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Alias for {@link PgVectorStore.schema} — reads more naturally in a
|
|
275
|
+
* migration script (`await pool.query(store.ensureSchema())`). Returns
|
|
276
|
+
* the identical DDL string; it does NOT execute anything against the
|
|
277
|
+
* database (the store never auto-migrates).
|
|
278
|
+
*/
|
|
279
|
+
ensureSchema() {
|
|
280
|
+
return this.schema();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Create a Postgres + pgvector-backed {@link VectorStore} for the RAG
|
|
285
|
+
* pipeline. Either pass a live `pg.Pool` / `pg.Client` (`{ client }`) —
|
|
286
|
+
* `@warlock.js/ai` never imports `pg` in that case — or a
|
|
287
|
+
* `{ connectionString }` and let the store lazily `import("pg")` to build
|
|
288
|
+
* its own pool. When `pg` is not installed, a curated install string
|
|
289
|
+
* surfaces on first use, never at import.
|
|
290
|
+
*
|
|
291
|
+
* Run {@link PgVectorStoreInstance.ensureSchema} through your migration
|
|
292
|
+
* tool once before use (it enables the `vector` extension, creates the
|
|
293
|
+
* table, and builds the tag + ANN indexes); the store never auto-migrates.
|
|
294
|
+
*
|
|
295
|
+
* Index and query MUST use the same embedding model — the `vector(N)`
|
|
296
|
+
* column width is fixed at table-creation time from `dimensions`.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* import { Pool } from "pg";
|
|
300
|
+
* import { ai } from "@warlock.js/ai";
|
|
301
|
+
*
|
|
302
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
303
|
+
* const store = ai.rag.pgVectorStore({ client: pool, dimensions: 1536 });
|
|
304
|
+
*
|
|
305
|
+
* // Once, via your migration tooling:
|
|
306
|
+
* // await pool.query(store.ensureSchema());
|
|
307
|
+
*
|
|
308
|
+
* const kb = ai.rag({
|
|
309
|
+
* name: "docs",
|
|
310
|
+
* embedder: openai.embedder({ name: "text-embedding-3-small" }),
|
|
311
|
+
* store,
|
|
312
|
+
* });
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* // Let the store build its own pool from a connection string:
|
|
316
|
+
* const store = ai.rag.pgVectorStore({
|
|
317
|
+
* connectionString: process.env.DATABASE_URL,
|
|
318
|
+
* index: "ivfflat",
|
|
319
|
+
* ivfflatLists: 200,
|
|
320
|
+
* });
|
|
321
|
+
*/
|
|
322
|
+
function pgVectorStore(options) {
|
|
323
|
+
return new PgVectorStore(options);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
export { pgVectorStore, vectorLiteral };
|
|
328
|
+
//# sourceMappingURL=pg-vector-store.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pg-vector-store.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai/src/rag/store/pg-vector-store.ts"],"sourcesContent":["import type { VectorStore } from \"./vector-store.contract\";\n\n/**\n * Minimal `pg`-compatible client surface the Postgres {@link VectorStore}\n * depends on. Both `pg.Pool` and `pg.Client` satisfy it — the store only\n * ever calls `query`.\n *\n * `@warlock.js/ai` takes **no** hard dependency on `pg`; the dev installs\n * it (an optional peer) and passes the client in. Structurally identical\n * to the snapshot / human-interrupt stores' `PgClientLike`, so a single\n * pool can back the orchestrator checkpoint/snapshot tables, the\n * interrupt table, and this vectors table alike.\n */\nexport interface PgClientLike {\n query(text: string, params?: unknown[]): Promise<{ rows: unknown[] }>;\n}\n\n/**\n * Options for the Postgres {@link VectorStore}.\n *\n * Two mutually-supportive ways to supply the connection (mirroring\n * `ai.human.interrupt.pg`):\n * - **`client`** — pass an already-built `pg.Pool` / `pg.Client` (anything\n * satisfying {@link PgClientLike}). The store only ever calls `query`\n * and never opens or closes it; one pool can back several stores.\n * - **`connectionString`** — let the store lazily `import(\"pg\")` and build\n * its own `Pool`. `@warlock.js/ai` takes **no** hard dependency on\n * `pg` (an optional peer); when it is absent the store throws a curated\n * install string at first use, never a raw module-resolution stack trace\n * at import.\n *\n * Exactly one of the two must be present.\n */\nexport interface PgVectorStoreOptions {\n /** An already-built `pg.Pool` / `pg.Client` — anything matching {@link PgClientLike}. */\n client?: PgClientLike;\n\n /** Connection string the store passes to a lazily-imported `pg.Pool`. */\n connectionString?: string;\n\n /**\n * Backing table name. Defaults to `warlock_ai_rag_vectors`. Must be a\n * safe SQL identifier — it is interpolated into DDL/DML.\n */\n table?: string;\n\n /**\n * Embedding dimensionality used in the `CREATE TABLE` DDL emitted by\n * {@link VectorStore.schema | ensureSchema}. Defaults to `1536`\n * (OpenAI `text-embedding-3-small`). The column is declared\n * `vector(N)`; queries and upserts never re-state it, so an existing\n * table provisioned at a different size is unaffected — only the DDL\n * helper reads this.\n */\n dimensions?: number;\n\n /**\n * Approximate-nearest-neighbour index strategy baked into the DDL\n * emitted by {@link VectorStore.schema | ensureSchema}. Defaults to\n * `\"hnsw\"` (better recall/latency on modern pgvector). Use `\"ivfflat\"`\n * for the classic list-partitioned index, or `\"none\"` to emit no ANN\n * index (exact scan — correct, but linear in row count).\n */\n index?: \"hnsw\" | \"ivfflat\" | \"none\";\n\n /**\n * `lists` parameter for an `ivfflat` index (ignored for `hnsw` / `none`).\n * Defaults to `100`. Tune toward `rows / 1000` for large tables.\n */\n ivfflatLists?: number;\n}\n\n/**\n * Default backing table — provisions the store with no extra config when\n * the dev runs {@link VectorStore.schema | ensureSchema} through their\n * migration tool.\n */\nconst DEFAULT_TABLE = \"warlock_ai_rag_vectors\";\n\n/** Default embedding width baked into the DDL (OpenAI `text-embedding-3-small`). */\nconst DEFAULT_DIMENSIONS = 1536;\n\n/** Default `ivfflat` list count when that index strategy is chosen. */\nconst DEFAULT_IVFFLAT_LISTS = 100;\n\n/**\n * Allowed characters in a Postgres identifier (table name). The table name\n * is interpolated into DDL/DML, so anything outside this conservative\n * ASCII subset is rejected — interpolating an arbitrary string would be a\n * SQL-injection footgun (mirrors the snapshot / human-interrupt pg stores\n * and `@warlock.js/cache`'s `PgCacheDriver`).\n */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Module specifier for the optional `pg` driver. Held in a `string`\n * variable so the dynamic `import()` is not statically resolved at compile\n * time — `pg` is an optional peer that need not be installed for this\n * package to type-check or for a cache-only consumer to run.\n */\nconst PG_MODULE = \"pg\";\n\n/**\n * Curated install string surfaced (at use time) when a `connectionString`\n * is configured but the optional `pg` driver is absent. Never thrown at\n * import — a cache-only consumer must be able to load this module.\n */\nconst PG_INSTALL_INSTRUCTIONS = `\nThe @warlock.js/ai Postgres vector store requires the pg package and a\nPostgres database with the pgvector extension. Install the driver with:\n\n npm install pg\n\nOr with your preferred package manager:\n\n pnpm add pg\n yarn add pg\n`.trim();\n\n/**\n * Minimal structural view of a `pg.Pool` constructor — just enough of the\n * `pg` module surface for the store to build a client when handed a\n * `connectionString`. Declared locally (rather than `typeof import(\"pg\")`)\n * so this module type-checks even when `pg` is not installed.\n */\ninterface PgModuleLike {\n Pool: new (config: { connectionString: string }) => PgClientLike;\n}\n\n/**\n * Lazily import `pg` and return a `Pool` built from `connectionString`. A\n * bare `catch` rethrows the curated install string — a missing optional\n * peer surfaces as actionable guidance, never a raw resolution error.\n */\nasync function buildPgClient(connectionString: string): Promise<PgClientLike> {\n let sdk: PgModuleLike;\n\n try {\n sdk = (await import(PG_MODULE)) as unknown as PgModuleLike;\n } catch {\n throw new Error(PG_INSTALL_INSTRUCTIONS);\n }\n\n return new sdk.Pool({ connectionString });\n}\n\n/**\n * Serialize a JS `number[]` to the pgvector text literal: `[1,2,3]`.\n * pgvector accepts a vector either as this bracketed literal or via a\n * typed parameter; passing the literal string + an explicit `::vector`\n * cast keeps the store driver-agnostic (no dependency on a registered\n * `pg` type parser).\n *\n * Non-finite components (`NaN` / `±Infinity`) are rejected — pgvector\n * stores only finite floats, and silently coercing them would corrupt the\n * index. The check is cheap relative to the embed call that produced the\n * vector.\n *\n * @example\n * vectorLiteral([1, 0.5, -2]); // \"[1,0.5,-2]\"\n */\nexport function vectorLiteral(vector: number[]): string {\n let literal = \"[\";\n\n for (let index = 0; index < vector.length; index++) {\n const component = vector[index];\n\n if (!Number.isFinite(component)) {\n throw new TypeError(\n `pgVectorStore: embedding component at index ${index} is not finite (${component}); pgvector stores only finite floats.`,\n );\n }\n\n if (index > 0) {\n literal += \",\";\n }\n\n literal += String(component);\n }\n\n return literal + \"]\";\n}\n\n/**\n * Coerce a `value` JSONB column back into the stored payload. node-postgres\n * parses `JSONB` into a JS value already, but some pool wrappers hand back\n * the raw string — be defensive across both (mirrors the snapshot store's\n * `parsePayload`).\n */\nfunction parseValue<T>(value: unknown): T {\n if (typeof value === \"string\") {\n return JSON.parse(value) as T;\n }\n\n return value as T;\n}\n\n/**\n * Coerce a pgvector cosine **distance** (`<=>`, in `[0, 2]`, 0 = identical)\n * into the cosine **similarity** score the {@link VectorStore} contract\n * declares (`[0, 1]`, 1 = identical). `pg` returns the computed distance\n * column as a string for `double precision`; parse then map `1 - distance`,\n * clamped to `[0, 1]` so a tiny floating-point overshoot never yields a\n * score just outside the contract's range.\n */\nfunction distanceToScore(distance: unknown): number {\n const value = typeof distance === \"string\" ? Number(distance) : (distance as number);\n const score = 1 - value;\n\n if (score < 0) {\n return 0;\n }\n\n if (score > 1) {\n return 1;\n }\n\n return score;\n}\n\n/**\n * Postgres + pgvector-backed {@link VectorStore} — one durable row per\n * indexed chunk, keyed by the RAG pipeline's dotted `key`\n * (`ai.rag.<name>.<sourceId>.<chunkIndex>`), with the chunk payload in a\n * `value` JSONB column and the embedding in a `vector` column.\n *\n * Owns: the three RAG vector operations against a pgvector index —\n * `upsert` (INSERT … ON CONFLICT DO UPDATE), `query` (cosine\n * `ORDER BY embedding <=> $vec` with a `threshold` floor + optional `tags`\n * overlap filter, capped at `topK`), and `removeNamespace` (prefix DELETE).\n * Also emits the reference DDL via {@link PgVectorStore.schema} (alias\n * {@link PgVectorStore.ensureSchema}).\n *\n * Does NOT own: the connection lifecycle (a dev-supplied `client` is never\n * closed; a store-built `Pool` from a `connectionString` is also left open\n * for the process to reuse) or schema migration — the dev runs the DDL\n * through their own tool; the framework never auto-migrates.\n *\n * Front it with the {@link pgVectorStore} factory — callers never `new` it.\n */\nclass PgVectorStore implements VectorStore {\n /** Validated backing table name, safe to interpolate into SQL. */\n private readonly table: string;\n\n /** Embedding width baked into the DDL. */\n private readonly dimensions: number;\n\n /** ANN index strategy baked into the DDL. */\n private readonly index: \"hnsw\" | \"ivfflat\" | \"none\";\n\n /** `lists` parameter for an `ivfflat` index. */\n private readonly ivfflatLists: number;\n\n /**\n * A ready client, or a promise resolving to one the store builds lazily\n * from a `connectionString`. Resolved once and memoized so the optional\n * `pg` import happens at most once.\n */\n private readonly clientPromise: Promise<PgClientLike>;\n\n public constructor(options: PgVectorStoreOptions) {\n const table = options.table ?? DEFAULT_TABLE;\n\n if (!SAFE_IDENTIFIER.test(table)) {\n throw new TypeError(\n `pgVectorStore: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n );\n }\n\n this.table = table;\n this.dimensions = options.dimensions ?? DEFAULT_DIMENSIONS;\n this.index = options.index ?? \"hnsw\";\n this.ivfflatLists = options.ivfflatLists ?? DEFAULT_IVFFLAT_LISTS;\n\n if (options.client) {\n if (typeof options.client.query !== \"function\") {\n throw new TypeError(\n \"pgVectorStore requires a 'client' option implementing { query(text, params) } — pass a pg.Pool or pg.Client.\",\n );\n }\n\n this.clientPromise = Promise.resolve(options.client);\n\n return;\n }\n\n if (options.connectionString) {\n // Defer the optional `pg` import to first use — a curated install\n // string surfaces from `buildPgClient`, not at construction.\n this.clientPromise = buildPgClient(options.connectionString);\n\n return;\n }\n\n throw new TypeError(\n \"pgVectorStore requires either a 'client' or a 'connectionString' option.\",\n );\n }\n\n /**\n * Resolve the backing client, surfacing the lazy `pg` import's curated\n * install string on the first call that needs it.\n */\n private client(): Promise<PgClientLike> {\n return this.clientPromise;\n }\n\n /**\n * Index `value` under `key` with its embedding `vector`. Upserts on the\n * `key` primary key — re-indexing the same chunk overwrites its payload,\n * embedding, and tags rather than appending a duplicate row. Optional\n * `tags` ride a `text[]` column so {@link query} can restrict the\n * candidate set with an array-overlap filter.\n *\n * The embedding is sent as a pgvector text literal (`$3`) cast to\n * `::vector`, so the store needs no registered `pg` type parser. `tags`\n * defaults to an empty array (never `NULL`) to keep the overlap filter's\n * `&&` semantics simple.\n */\n public async upsert(\n key: string,\n value: unknown,\n vector: number[],\n tags?: string[],\n ): Promise<void> {\n const client = await this.client();\n\n await client.query(\n `INSERT INTO ${this.table} (key, value, embedding, tags)\n VALUES ($1, $2::jsonb, $3::vector, $4::text[])\n ON CONFLICT (key) DO UPDATE\n SET value = EXCLUDED.value,\n embedding = EXCLUDED.embedding,\n tags = EXCLUDED.tags`,\n [key, JSON.stringify(value), vectorLiteral(vector), tags ?? []],\n );\n }\n\n /**\n * Return the cosine-nearest rows to `vector`, mapped to the contract's\n * `{ key, value, score }` shape. The SQL:\n *\n * - computes `embedding <=> $1::vector` (cosine **distance**) once, aliased\n * `distance`, and `ORDER BY` it ascending (nearest first);\n * - applies the `threshold` floor as `distance <= 1 - threshold`\n * (similarity `>=` threshold), so the default `0.5` floor maps to a\n * `<= 0.5` distance bound — the filter runs in SQL, not in JS, so a\n * below-floor row never crosses the wire;\n * - when `tags` are given, restricts to rows whose `tags` array overlaps\n * the requested set via `tags && $tags::text[]` (one-of semantics,\n * matching the cache store);\n * - caps the result at `topK` with `LIMIT`.\n *\n * The returned `score` is `1 - distance`, clamped to `[0, 1]`, so callers\n * see the same cosine-similarity scale the cache store emits.\n */\n public async query<T>(\n vector: number[],\n options: { topK: number; threshold?: number; tags?: string[] },\n ): Promise<{ key: string; value: T; score: number }[]> {\n const client = await this.client();\n const queryVector = vectorLiteral(vector);\n\n // $1 = query vector, $2 = topK. Optional threshold + tags are appended\n // as $3 / $4 only when present, so the prepared statement carries no\n // unused placeholders.\n const params: unknown[] = [queryVector, options.topK];\n const conditions: string[] = [];\n\n if (options.threshold !== undefined) {\n params.push(1 - options.threshold);\n conditions.push(`(embedding <=> $1::vector) <= $${params.length}`);\n }\n\n if (options.tags !== undefined && options.tags.length > 0) {\n params.push(options.tags);\n conditions.push(`tags && $${params.length}::text[]`);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n\n const { rows } = await client.query(\n `SELECT key, value, (embedding <=> $1::vector) AS distance\n FROM ${this.table}\n ${where}\n ORDER BY embedding <=> $1::vector\n LIMIT $2`,\n params,\n );\n\n return (rows as Record<string, unknown>[]).map((row) => ({\n key: row.key as string,\n value: parseValue<T>(row.value),\n score: distanceToScore(row.distance),\n }));\n }\n\n /**\n * Drop every entry written under `namespace`. The RAG pipeline keys\n * chunks as `<namespace>.<sourceId>.<chunkIndex>`, so a row belongs to\n * the namespace when its `key` equals it OR begins with `<namespace>.`\n * — deleting `ai.rag.docs` must not also catch `ai.rag.docs2`. The `_`\n * and `%` LIKE wildcards in the namespace are escaped so a namespace\n * that happens to contain them is matched literally.\n */\n public async removeNamespace(namespace: string): Promise<void> {\n const client = await this.client();\n\n const escaped = namespace\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/_/g, \"\\\\_\")\n .replace(/%/g, \"\\\\%\");\n\n await client.query(\n `DELETE FROM ${this.table}\n WHERE key = $1 OR key LIKE $2 ESCAPE '\\\\'`,\n [namespace, `${escaped}.%`],\n );\n }\n\n /**\n * Return the reference migration DDL for this store's backing table,\n * interpolating the configured table name, embedding width, and ANN\n * index strategy. The dev runs it once through their migration tool —\n * the framework never auto-migrates.\n *\n * The emitted statements:\n * 1. `CREATE EXTENSION IF NOT EXISTS vector;` — enables pgvector (needs\n * a superuser or a role with `CREATE` on the database the first time).\n * 2. `CREATE TABLE IF NOT EXISTS <table> (key TEXT PRIMARY KEY, value\n * JSONB NOT NULL, embedding vector(<dimensions>) NOT NULL, tags\n * text[] NOT NULL DEFAULT '{}');`\n * 3. A GIN index on `tags` so the array-overlap filter stays sargable.\n * 4. The chosen ANN index over `embedding` using `vector_cosine_ops`:\n * - `\"hnsw\"` → `USING hnsw (embedding vector_cosine_ops)`;\n * - `\"ivfflat\"` → `USING ivfflat (embedding vector_cosine_ops)\n * WITH (lists = <ivfflatLists>)`;\n * - `\"none\"` → emitted as a comment (exact scan, no ANN index).\n *\n * @example\n * const store = pgVectorStore({ client: pool, dimensions: 1536 });\n * await pool.query(store.ensureSchema());\n */\n public schema(): string {\n const lines = [\n `CREATE EXTENSION IF NOT EXISTS vector;`,\n `CREATE TABLE IF NOT EXISTS ${this.table} (`,\n ` key TEXT PRIMARY KEY,`,\n ` value JSONB NOT NULL,`,\n ` embedding vector(${this.dimensions}) NOT NULL,`,\n ` tags TEXT[] NOT NULL DEFAULT '{}'`,\n `);`,\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_tags`,\n ` ON ${this.table} USING gin (tags);`,\n ];\n\n if (this.index === \"hnsw\") {\n lines.push(\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding`,\n ` ON ${this.table} USING hnsw (embedding vector_cosine_ops);`,\n );\n } else if (this.index === \"ivfflat\") {\n lines.push(\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding`,\n ` ON ${this.table} USING ivfflat (embedding vector_cosine_ops)`,\n ` WITH (lists = ${this.ivfflatLists});`,\n );\n } else {\n lines.push(\n `-- No ANN index requested (index: \"none\"): cosine queries fall back`,\n `-- to an exact sequential scan, which is correct but linear in rows.`,\n );\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * Alias for {@link PgVectorStore.schema} — reads more naturally in a\n * migration script (`await pool.query(store.ensureSchema())`). Returns\n * the identical DDL string; it does NOT execute anything against the\n * database (the store never auto-migrates).\n */\n public ensureSchema(): string {\n return this.schema();\n }\n}\n\n/**\n * The {@link VectorStore} surface plus the pg store's extra DDL helpers.\n * `schema()` / `ensureSchema()` are not part of the base contract (the\n * cache store has no backing table), so the factory's return type widens\n * it for callers that want the migration SQL.\n */\nexport interface PgVectorStoreInstance extends VectorStore {\n /** Reference migration DDL (extension + table + indexes). Never executed. */\n schema(): string;\n /** Alias for {@link PgVectorStoreInstance.schema} — reads better in a migration script. */\n ensureSchema(): string;\n}\n\n/**\n * Create a Postgres + pgvector-backed {@link VectorStore} for the RAG\n * pipeline. Either pass a live `pg.Pool` / `pg.Client` (`{ client }`) —\n * `@warlock.js/ai` never imports `pg` in that case — or a\n * `{ connectionString }` and let the store lazily `import(\"pg\")` to build\n * its own pool. When `pg` is not installed, a curated install string\n * surfaces on first use, never at import.\n *\n * Run {@link PgVectorStoreInstance.ensureSchema} through your migration\n * tool once before use (it enables the `vector` extension, creates the\n * table, and builds the tag + ANN indexes); the store never auto-migrates.\n *\n * Index and query MUST use the same embedding model — the `vector(N)`\n * column width is fixed at table-creation time from `dimensions`.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { ai } from \"@warlock.js/ai\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n * const store = ai.rag.pgVectorStore({ client: pool, dimensions: 1536 });\n *\n * // Once, via your migration tooling:\n * // await pool.query(store.ensureSchema());\n *\n * const kb = ai.rag({\n * name: \"docs\",\n * embedder: openai.embedder({ name: \"text-embedding-3-small\" }),\n * store,\n * });\n *\n * @example\n * // Let the store build its own pool from a connection string:\n * const store = ai.rag.pgVectorStore({\n * connectionString: process.env.DATABASE_URL,\n * index: \"ivfflat\",\n * ivfflatLists: 200,\n * });\n */\nexport function pgVectorStore(options: PgVectorStoreOptions): PgVectorStoreInstance {\n return new PgVectorStore(options);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BaseReport } from "../contracts/result/base-report.type.mjs";
|
|
2
|
+
import { ExecuteResult } from "../contracts/result/execute-result.type.mjs";
|
|
3
|
+
import { FlowObserveOption } from "../observe/resolve-observers.mjs";
|
|
4
|
+
import { GeneratedAudio, SpeechModelContract } from "../contracts/speech-model.contract.mjs";
|
|
5
|
+
|
|
6
|
+
//#region ../@warlock.js/ai/src/speech/speech.d.ts
|
|
7
|
+
/** Parameters for {@link speech}. `model` comes from `sdk.speech({ name })`. */
|
|
8
|
+
type SpeechParams = {
|
|
9
|
+
/** The TTS model to synthesize with. */model: SpeechModelContract; /** The text to speak. */
|
|
10
|
+
text: string; /** Voice id/name; overrides the model's default voice. */
|
|
11
|
+
voice?: string; /** Output container (`"mp3"` / `"opus"` / `"aac"` / `"flac"` / `"wav"` / `"pcm"`). */
|
|
12
|
+
format?: string; /** Playback speed multiplier. */
|
|
13
|
+
speed?: number; /** Extra tone/delivery steering (model-dependent). */
|
|
14
|
+
instructions?: string; /** Cancellation handle. */
|
|
15
|
+
signal?: AbortSignal; /** Observability routing — same `observe` seam as agents. */
|
|
16
|
+
observe?: FlowObserveOption; /** Groups this call into a session for flat cost/trace queries. */
|
|
17
|
+
sessionId?: string; /** Report node name (defaults to `"speech"`). */
|
|
18
|
+
name?: string; /** Provider-specific options forwarded verbatim to the adapter. */
|
|
19
|
+
options?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
/** Success payload of a {@link speech} run. */
|
|
22
|
+
type SpeechData = {
|
|
23
|
+
/** The synthesized audio, normalized to the discriminated shape. */audio: GeneratedAudio;
|
|
24
|
+
};
|
|
25
|
+
/** The report node a {@link speech} run produces (`type: "speech"`). */
|
|
26
|
+
type SpeechReport = BaseReport & {
|
|
27
|
+
type: "speech"; /** Identity of the TTS model this run used. */
|
|
28
|
+
model: {
|
|
29
|
+
name: string;
|
|
30
|
+
provider: string;
|
|
31
|
+
}; /** Number of input characters synthesized (0 on failure). */
|
|
32
|
+
characters: number;
|
|
33
|
+
};
|
|
34
|
+
/** Result envelope of {@link speech} — the uniform `{ data, error, usage, report }`. */
|
|
35
|
+
type SpeechResult = ExecuteResult<SpeechData> & {
|
|
36
|
+
type: "speech";
|
|
37
|
+
report: SpeechReport;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Synthesize speech from text — the text-to-speech verb of the
|
|
41
|
+
* output-modality track (Theme I), sibling to `ai.image()`. Wraps a
|
|
42
|
+
* {@link SpeechModelContract} (from `openai.speech(...)`) in the
|
|
43
|
+
* framework's uniform result contract:
|
|
44
|
+
*
|
|
45
|
+
* - **Never throws.** Provider failures surface as a typed `AIError` on
|
|
46
|
+
* `result.error`; `result.data` is then `undefined`.
|
|
47
|
+
* - **Cost-truth.** `result.usage.cost` is filled per-character
|
|
48
|
+
* (`tts-1`) or per-token (`gpt-4o-mini-tts`), folding into the same
|
|
49
|
+
* `Usage.cost` rollup as text.
|
|
50
|
+
* - **Observable.** The completed {@link SpeechReport} routes to any
|
|
51
|
+
* registered `Observer` (panoptic, OTel, …) via the `observe` seam.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* const openai = new OpenAISDK({ apiKey });
|
|
55
|
+
* const { data, error } = await ai.speech({
|
|
56
|
+
* model: openai.speech({ name: "tts-1", voice: "alloy" }),
|
|
57
|
+
* text: "Your order has shipped.",
|
|
58
|
+
* format: "mp3",
|
|
59
|
+
* });
|
|
60
|
+
* if (!error) await fs.writeFile("ship.mp3", Buffer.from(data.audio.base64, "base64"));
|
|
61
|
+
*/
|
|
62
|
+
declare function speech(params: SpeechParams): Promise<SpeechResult>;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { SpeechData, SpeechParams, SpeechReport, SpeechResult, speech };
|
|
65
|
+
//# sourceMappingURL=speech.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speech.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai/src/speech/speech.ts"],"mappings":";;;;;;;KAmBY,YAAA;EAAA,wCAEV,KAAA,EAAO,mBAAA;EAEP,IAAA,UAUS;EART,KAAA,WAgBU;EAdV,MAAA,WAcgB;EAZhB,KAAA,WARO;EAUP,YAAA,WANA;EAQA,MAAA,GAAS,WAAA,EAJT;EAMA,OAAA,GAAU,iBAAA,EAFV;EAIA,SAAA,WAFA;EAIA,IAAA,WAFA;EAIA,OAAA,GAAU,MAAA;AAAA;;KAIA,UAAA;EAJM,oEAMhB,KAAA,EAAO,cAAc;AAAA;;KAIX,YAAA,GAAe,UAAU;EACnC,IAAA,YADU;EAGV,KAAA;IAAS,IAAA;IAAc,QAAA;EAAA,GAFvB;EAIA,UAAA;AAAA;;KAIU,YAAA,GAAe,aAAA,CAAc,UAAA;EACvC,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;;;;;;;;;;;;;;AAAY;AA0BtB;;;;;;;;;iBAAsB,MAAA,CAAO,MAAA,EAAQ,YAAA,GAAe,OAAA,CAAQ,YAAA"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { AIError } from "../errors/ai-error.mjs";
|
|
2
|
+
import { ProviderError } from "../errors/provider-error.mjs";
|
|
3
|
+
import { computeCost } from "../utils/compute-cost.mjs";
|
|
4
|
+
import { generateRunId } from "../utils/generate-run-id.mjs";
|
|
5
|
+
import { REPORT_SCHEMA_VERSION } from "../contracts/result/base-report.type.mjs";
|
|
6
|
+
import { stampReportLineage } from "../utils/stamp-report-lineage.mjs";
|
|
7
|
+
import { notifyObservers } from "../observe/resolve-observers.mjs";
|
|
8
|
+
|
|
9
|
+
//#region ../@warlock.js/ai/src/speech/speech.ts
|
|
10
|
+
/**
|
|
11
|
+
* Synthesize speech from text — the text-to-speech verb of the
|
|
12
|
+
* output-modality track (Theme I), sibling to `ai.image()`. Wraps a
|
|
13
|
+
* {@link SpeechModelContract} (from `openai.speech(...)`) in the
|
|
14
|
+
* framework's uniform result contract:
|
|
15
|
+
*
|
|
16
|
+
* - **Never throws.** Provider failures surface as a typed `AIError` on
|
|
17
|
+
* `result.error`; `result.data` is then `undefined`.
|
|
18
|
+
* - **Cost-truth.** `result.usage.cost` is filled per-character
|
|
19
|
+
* (`tts-1`) or per-token (`gpt-4o-mini-tts`), folding into the same
|
|
20
|
+
* `Usage.cost` rollup as text.
|
|
21
|
+
* - **Observable.** The completed {@link SpeechReport} routes to any
|
|
22
|
+
* registered `Observer` (panoptic, OTel, …) via the `observe` seam.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const openai = new OpenAISDK({ apiKey });
|
|
26
|
+
* const { data, error } = await ai.speech({
|
|
27
|
+
* model: openai.speech({ name: "tts-1", voice: "alloy" }),
|
|
28
|
+
* text: "Your order has shipped.",
|
|
29
|
+
* format: "mp3",
|
|
30
|
+
* });
|
|
31
|
+
* if (!error) await fs.writeFile("ship.mp3", Buffer.from(data.audio.base64, "base64"));
|
|
32
|
+
*/
|
|
33
|
+
async function speech(params) {
|
|
34
|
+
const { model, text } = params;
|
|
35
|
+
const runId = generateRunId("speech");
|
|
36
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37
|
+
const startPerf = performance.now();
|
|
38
|
+
const usage = {
|
|
39
|
+
input: 0,
|
|
40
|
+
output: 0,
|
|
41
|
+
total: 0
|
|
42
|
+
};
|
|
43
|
+
let data;
|
|
44
|
+
let error;
|
|
45
|
+
let status = "completed";
|
|
46
|
+
let characters = 0;
|
|
47
|
+
try {
|
|
48
|
+
const response = await model.generate(text, {
|
|
49
|
+
voice: params.voice,
|
|
50
|
+
format: params.format,
|
|
51
|
+
speed: params.speed,
|
|
52
|
+
instructions: params.instructions,
|
|
53
|
+
signal: params.signal,
|
|
54
|
+
...params.options
|
|
55
|
+
});
|
|
56
|
+
Object.assign(usage, response.usage);
|
|
57
|
+
characters = response.characters;
|
|
58
|
+
if (usage.cost === void 0) {
|
|
59
|
+
const cost = computeSpeechCost(usage, characters, model.pricing);
|
|
60
|
+
if (cost !== void 0) usage.cost = cost;
|
|
61
|
+
}
|
|
62
|
+
data = { audio: response.audio };
|
|
63
|
+
} catch (thrown) {
|
|
64
|
+
error = thrown instanceof AIError ? thrown : new ProviderError(toMessage(thrown), { cause: thrown });
|
|
65
|
+
status = params.signal?.aborted ? "cancelled" : "failed";
|
|
66
|
+
}
|
|
67
|
+
const report = {
|
|
68
|
+
runId,
|
|
69
|
+
rootRunId: runId,
|
|
70
|
+
name: params.name ?? "speech",
|
|
71
|
+
type: "speech",
|
|
72
|
+
status,
|
|
73
|
+
error,
|
|
74
|
+
startedAt,
|
|
75
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
76
|
+
duration: performance.now() - startPerf,
|
|
77
|
+
usage,
|
|
78
|
+
children: [],
|
|
79
|
+
model: {
|
|
80
|
+
name: model.name,
|
|
81
|
+
provider: model.provider
|
|
82
|
+
},
|
|
83
|
+
characters,
|
|
84
|
+
reportSchemaVersion: 1
|
|
85
|
+
};
|
|
86
|
+
stampReportLineage(report, {
|
|
87
|
+
rootRunId: runId,
|
|
88
|
+
sessionId: params.sessionId
|
|
89
|
+
});
|
|
90
|
+
await notifyObservers(params.observe, report);
|
|
91
|
+
return {
|
|
92
|
+
type: "speech",
|
|
93
|
+
data,
|
|
94
|
+
error,
|
|
95
|
+
usage,
|
|
96
|
+
report
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Price a TTS run: `perMillionCharacters × characters` (per-character
|
|
101
|
+
* metering, attributed to `cost.input`) wins when configured, otherwise
|
|
102
|
+
* the standard token math. Returns `undefined` when no usable pricing
|
|
103
|
+
* is present.
|
|
104
|
+
*/
|
|
105
|
+
function computeSpeechCost(usage, characters, pricing) {
|
|
106
|
+
if (!pricing) return;
|
|
107
|
+
if (pricing.perMillionCharacters !== void 0) return {
|
|
108
|
+
input: characters * pricing.perMillionCharacters / 1e6,
|
|
109
|
+
output: 0
|
|
110
|
+
};
|
|
111
|
+
if (pricing.input !== void 0 && pricing.output !== void 0) return computeCost(usage, {
|
|
112
|
+
input: pricing.input,
|
|
113
|
+
output: pricing.output
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Best-effort message for a non-`AIError` thrown value. */
|
|
117
|
+
function toMessage(thrown) {
|
|
118
|
+
return thrown instanceof Error ? thrown.message : String(thrown);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { speech };
|
|
123
|
+
//# sourceMappingURL=speech.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speech.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai/src/speech/speech.ts"],"sourcesContent":["import type {\n GeneratedAudio,\n SpeechModelContract,\n SpeechModelPricing,\n} from \"../contracts/speech-model.contract\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { ExecuteResult } from \"../contracts/result/execute-result.type\";\nimport type { ModelPricing } from \"../contracts/result/model-pricing.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { AIError } from \"../errors/ai-error\";\nimport { ProviderError } from \"../errors/provider-error\";\nimport type { FlowObserveOption } from \"../observe/resolve-observers\";\nimport { notifyObservers } from \"../observe/resolve-observers\";\nimport { computeCost } from \"../utils/compute-cost\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { stampReportLineage } from \"../utils/stamp-report-lineage\";\n\n/** Parameters for {@link speech}. `model` comes from `sdk.speech({ name })`. */\nexport type SpeechParams = {\n /** The TTS model to synthesize with. */\n model: SpeechModelContract;\n /** The text to speak. */\n text: string;\n /** Voice id/name; overrides the model's default voice. */\n voice?: string;\n /** Output container (`\"mp3\"` / `\"opus\"` / `\"aac\"` / `\"flac\"` / `\"wav\"` / `\"pcm\"`). */\n format?: string;\n /** Playback speed multiplier. */\n speed?: number;\n /** Extra tone/delivery steering (model-dependent). */\n instructions?: string;\n /** Cancellation handle. */\n signal?: AbortSignal;\n /** Observability routing — same `observe` seam as agents. */\n observe?: FlowObserveOption;\n /** Groups this call into a session for flat cost/trace queries. */\n sessionId?: string;\n /** Report node name (defaults to `\"speech\"`). */\n name?: string;\n /** Provider-specific options forwarded verbatim to the adapter. */\n options?: Record<string, unknown>;\n};\n\n/** Success payload of a {@link speech} run. */\nexport type SpeechData = {\n /** The synthesized audio, normalized to the discriminated shape. */\n audio: GeneratedAudio;\n};\n\n/** The report node a {@link speech} run produces (`type: \"speech\"`). */\nexport type SpeechReport = BaseReport & {\n type: \"speech\";\n /** Identity of the TTS model this run used. */\n model: { name: string; provider: string };\n /** Number of input characters synthesized (0 on failure). */\n characters: number;\n};\n\n/** Result envelope of {@link speech} — the uniform `{ data, error, usage, report }`. */\nexport type SpeechResult = ExecuteResult<SpeechData> & {\n type: \"speech\";\n report: SpeechReport;\n};\n\n/**\n * Synthesize speech from text — the text-to-speech verb of the\n * output-modality track (Theme I), sibling to `ai.image()`. Wraps a\n * {@link SpeechModelContract} (from `openai.speech(...)`) in the\n * framework's uniform result contract:\n *\n * - **Never throws.** Provider failures surface as a typed `AIError` on\n * `result.error`; `result.data` is then `undefined`.\n * - **Cost-truth.** `result.usage.cost` is filled per-character\n * (`tts-1`) or per-token (`gpt-4o-mini-tts`), folding into the same\n * `Usage.cost` rollup as text.\n * - **Observable.** The completed {@link SpeechReport} routes to any\n * registered `Observer` (panoptic, OTel, …) via the `observe` seam.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey });\n * const { data, error } = await ai.speech({\n * model: openai.speech({ name: \"tts-1\", voice: \"alloy\" }),\n * text: \"Your order has shipped.\",\n * format: \"mp3\",\n * });\n * if (!error) await fs.writeFile(\"ship.mp3\", Buffer.from(data.audio.base64, \"base64\"));\n */\nexport async function speech(params: SpeechParams): Promise<SpeechResult> {\n const { model, text } = params;\n\n const runId = generateRunId(\"speech\");\n const startedAt = new Date().toISOString();\n const startPerf = performance.now();\n\n const usage: Usage = { input: 0, output: 0, total: 0 };\n let data: SpeechData | undefined;\n let error: AIError | undefined;\n let status: SpeechReport[\"status\"] = \"completed\";\n let characters = 0;\n\n try {\n const response = await model.generate(text, {\n voice: params.voice,\n format: params.format,\n speed: params.speed,\n instructions: params.instructions,\n signal: params.signal,\n ...params.options,\n });\n\n Object.assign(usage, response.usage);\n characters = response.characters;\n\n if (usage.cost === undefined) {\n const cost = computeSpeechCost(usage, characters, model.pricing);\n if (cost !== undefined) {\n usage.cost = cost;\n }\n }\n\n data = { audio: response.audio };\n } catch (thrown) {\n error =\n thrown instanceof AIError ? thrown : new ProviderError(toMessage(thrown), { cause: thrown });\n status = params.signal?.aborted ? \"cancelled\" : \"failed\";\n }\n\n const report: SpeechReport = {\n runId,\n rootRunId: runId,\n name: params.name ?? \"speech\",\n type: \"speech\",\n status,\n error,\n startedAt,\n endedAt: new Date().toISOString(),\n duration: performance.now() - startPerf,\n usage,\n children: [],\n model: { name: model.name, provider: model.provider },\n characters,\n reportSchemaVersion: REPORT_SCHEMA_VERSION,\n };\n\n stampReportLineage(report, { rootRunId: runId, sessionId: params.sessionId });\n\n await notifyObservers(params.observe, report);\n\n return { type: \"speech\", data, error, usage, report };\n}\n\n/**\n * Price a TTS run: `perMillionCharacters × characters` (per-character\n * metering, attributed to `cost.input`) wins when configured, otherwise\n * the standard token math. Returns `undefined` when no usable pricing\n * is present.\n */\nfunction computeSpeechCost(\n usage: Usage,\n characters: number,\n pricing: SpeechModelPricing | undefined,\n): ModelPricing | undefined {\n if (!pricing) {\n return undefined;\n }\n\n if (pricing.perMillionCharacters !== undefined) {\n return { input: (characters * pricing.perMillionCharacters) / 1_000_000, output: 0 };\n }\n\n if (pricing.input !== undefined && pricing.output !== undefined) {\n return computeCost(usage, { input: pricing.input, output: pricing.output });\n }\n\n return undefined;\n}\n\n/** Best-effort message for a non-`AIError` thrown value. */\nfunction toMessage(thrown: unknown): string {\n return thrown instanceof Error ? thrown.message : String(thrown);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwFA,eAAsB,OAAO,QAA6C;CACxE,MAAM,EAAE,OAAO,SAAS;CAExB,MAAM,QAAQ,cAAc,QAAQ;CACpC,MAAM,6BAAY,IAAI,KAAK,EAAC,CAAC,YAAY;CACzC,MAAM,YAAY,YAAY,IAAI;CAElC,MAAM,QAAe;EAAE,OAAO;EAAG,QAAQ;EAAG,OAAO;CAAE;CACrD,IAAI;CACJ,IAAI;CACJ,IAAI,SAAiC;CACrC,IAAI,aAAa;CAEjB,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,SAAS,MAAM;GAC1C,OAAO,OAAO;GACd,QAAQ,OAAO;GACf,OAAO,OAAO;GACd,cAAc,OAAO;GACrB,QAAQ,OAAO;GACf,GAAG,OAAO;EACZ,CAAC;EAED,OAAO,OAAO,OAAO,SAAS,KAAK;EACnC,aAAa,SAAS;EAEtB,IAAI,MAAM,SAAS,QAAW;GAC5B,MAAM,OAAO,kBAAkB,OAAO,YAAY,MAAM,OAAO;GAC/D,IAAI,SAAS,QACX,MAAM,OAAO;EAEjB;EAEA,OAAO,EAAE,OAAO,SAAS,MAAM;CACjC,SAAS,QAAQ;EACf,QACE,kBAAkB,UAAU,SAAS,IAAI,cAAc,UAAU,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC;EAC7F,SAAS,OAAO,QAAQ,UAAU,cAAc;CAClD;CAEA,MAAM,SAAuB;EAC3B;EACA,WAAW;EACX,MAAM,OAAO,QAAQ;EACrB,MAAM;EACN;EACA;EACA;EACA,0BAAS,IAAI,KAAK,EAAC,CAAC,YAAY;EAChC,UAAU,YAAY,IAAI,IAAI;EAC9B;EACA,UAAU,CAAC;EACX,OAAO;GAAE,MAAM,MAAM;GAAM,UAAU,MAAM;EAAS;EACpD;EACA;CACF;CAEA,mBAAmB,QAAQ;EAAE,WAAW;EAAO,WAAW,OAAO;CAAU,CAAC;CAE5E,MAAM,gBAAgB,OAAO,SAAS,MAAM;CAE5C,OAAO;EAAE,MAAM;EAAU;EAAM;EAAO;EAAO;CAAO;AACtD;;;;;;;AAQA,SAAS,kBACP,OACA,YACA,SAC0B;CAC1B,IAAI,CAAC,SACH;CAGF,IAAI,QAAQ,yBAAyB,QACnC,OAAO;EAAE,OAAQ,aAAa,QAAQ,uBAAwB;EAAW,QAAQ;CAAE;CAGrF,IAAI,QAAQ,UAAU,UAAa,QAAQ,WAAW,QACpD,OAAO,YAAY,OAAO;EAAE,OAAO,QAAQ;EAAO,QAAQ,QAAQ;CAAO,CAAC;AAI9E;;AAGA,SAAS,UAAU,QAAyB;CAC1C,OAAO,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;AACjE"}
|
|
@@ -178,8 +178,8 @@ function isDispatchableUnit(value) {
|
|
|
178
178
|
return typeof candidate.name === "string" && typeof candidate.execute === "function";
|
|
179
179
|
}
|
|
180
180
|
function detectType(unit) {
|
|
181
|
-
if (typeof unit.
|
|
182
|
-
return "
|
|
181
|
+
if (typeof unit.stream === "function") return "agent";
|
|
182
|
+
return "workflow";
|
|
183
183
|
}
|
|
184
184
|
function resolveAgentLikeDescription(intent, entryForm, unit, supervisorName) {
|
|
185
185
|
const entryOverride = entryForm?.description;
|