@pylonsync/functions 0.4.19 → 0.4.21
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 +1 -1
- package/dist/types.d.ts +80 -0
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/runtime-vector.test.ts +87 -0
- package/src/runtime.ts +22 -0
- package/src/types.ts +81 -0
package/dist/index.d.ts
CHANGED
|
@@ -24,4 +24,4 @@ export type { WorkflowDefinition, WorkflowRun, WorkflowRunRequest, WorkflowRunne
|
|
|
24
24
|
export { resetDb, installTestIsolation } from "./testing";
|
|
25
25
|
export { slugifyName, availableSlug } from "./slugify";
|
|
26
26
|
export type { SsrResponse, SsrCookieOptions, SsrMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, } from "./ssr-runtime";
|
|
27
|
-
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, MemberRow, Workflows, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, } from "./types";
|
|
27
|
+
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, MemberRow, Workflows, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, } from "./types";
|
package/dist/types.d.ts
CHANGED
|
@@ -128,6 +128,32 @@ export interface DbReader {
|
|
|
128
128
|
* entities without a `search:` config (`SEARCH_NOT_CONFIGURED`).
|
|
129
129
|
*/
|
|
130
130
|
search(entity: string, query: Record<string, unknown>): Promise<SearchResult>;
|
|
131
|
+
/**
|
|
132
|
+
* Exact k-nearest-neighbor search over a `field.vector(dims)` field.
|
|
133
|
+
* Cosine similarity by default (`metric: "dot" | "l2"` to change);
|
|
134
|
+
* hits come back best-first with the full row on `doc` (vector
|
|
135
|
+
* fields stripped — re-fetch by id if you need the embedding).
|
|
136
|
+
*
|
|
137
|
+
* Available wherever `ctx.db` is — queries and mutations. Actions
|
|
138
|
+
* have no `ctx.db`: embed there, then `ctx.runQuery` a query that
|
|
139
|
+
* searches with the vector.
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* // In a query: the vector arrives as an arg (an action embedded it).
|
|
143
|
+
* const { hits } = await ctx.db.vectorSearch("Doc", {
|
|
144
|
+
* field: "embedding",
|
|
145
|
+
* vector,
|
|
146
|
+
* limit: 5,
|
|
147
|
+
* filter: { status: "published" }, // equality / IN pre-filter
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* Exact scan, not ANN: every non-NULL embedding is scored. Fine to
|
|
152
|
+
* ~100k rows per entity; past that, a dedicated vector store wins.
|
|
153
|
+
* Throws `VECTOR_FIELD_NOT_FOUND` when `field` isn't a vector field
|
|
154
|
+
* and `INVALID_QUERY` on dimension mismatch or bad filters.
|
|
155
|
+
*/
|
|
156
|
+
vectorSearch(entity: string, query: VectorSearchQuery): Promise<VectorSearchResult>;
|
|
131
157
|
/**
|
|
132
158
|
* Cursor-paginated list. Pass `cursor` from a previous page's `nextCursor`
|
|
133
159
|
* to continue; pass `null` for the first page.
|
|
@@ -165,6 +191,32 @@ export interface SearchResult<T = Record<string, unknown>> {
|
|
|
165
191
|
/** Milliseconds spent in the search engine. */
|
|
166
192
|
tookMs: number;
|
|
167
193
|
}
|
|
194
|
+
/** Request shape for [`DbReader.vectorSearch`]. */
|
|
195
|
+
export interface VectorSearchQuery {
|
|
196
|
+
/** The `vector(dims)` field to search. */
|
|
197
|
+
field: string;
|
|
198
|
+
/** Query embedding; length must equal the field's declared dims. */
|
|
199
|
+
vector: number[];
|
|
200
|
+
/** Max hits. Default 10, capped at 200. */
|
|
201
|
+
limit?: number;
|
|
202
|
+
/** Similarity metric. Default "cosine" (higher = closer);
|
|
203
|
+
* "dot" (higher = closer); "l2" (Euclidean distance, lower = closer). */
|
|
204
|
+
metric?: "cosine" | "dot" | "l2";
|
|
205
|
+
/** Equality pre-filter applied in SQL before scoring. A plain value
|
|
206
|
+
* means equality; an array means SQL `IN`; `null` means IS NULL. */
|
|
207
|
+
filter?: Record<string, unknown>;
|
|
208
|
+
}
|
|
209
|
+
/** Result shape for [`DbReader.vectorSearch`]. */
|
|
210
|
+
export interface VectorSearchResult<T = Record<string, unknown>> {
|
|
211
|
+
/** Best-first hits for the chosen metric. */
|
|
212
|
+
hits: Array<{
|
|
213
|
+
id: string;
|
|
214
|
+
score: number;
|
|
215
|
+
doc: T;
|
|
216
|
+
}>;
|
|
217
|
+
/** Milliseconds spent scanning + scoring. */
|
|
218
|
+
tookMs: number;
|
|
219
|
+
}
|
|
168
220
|
export interface DbWriter extends DbReader {
|
|
169
221
|
/**
|
|
170
222
|
* Escape hatch — same shape as [`DbReader.unsafe`] but with the
|
|
@@ -339,6 +391,34 @@ export interface Llm {
|
|
|
339
391
|
* `complete` would refuse.
|
|
340
392
|
*/
|
|
341
393
|
stream(request: LlmCompleteRequest, onEvent: (event: LlmStreamEvent) => void): Promise<LlmCompleteResponse>;
|
|
394
|
+
/**
|
|
395
|
+
* Batch-embed texts via the configured embeddings provider. One
|
|
396
|
+
* embedding per input, in input order. Pair with a
|
|
397
|
+
* `field.vector(dims)` field and `ctx.db.vectorSearch` for
|
|
398
|
+
* retrieval. From an action (which has no `ctx.db`), store via a
|
|
399
|
+
* mutation:
|
|
400
|
+
*
|
|
401
|
+
* ```ts
|
|
402
|
+
* const [vec] = await ctx.llm.embed([doc.body]);
|
|
403
|
+
* await ctx.runMutation("saveEmbedding", { docId: doc.id, embedding: vec });
|
|
404
|
+
* ```
|
|
405
|
+
*
|
|
406
|
+
* The embeddings provider is a separate axis from chat: with
|
|
407
|
+
* `OPENAI_API_KEY` set it defaults to OpenAI
|
|
408
|
+
* `text-embedding-3-small` (1536 dims) even when chat runs
|
|
409
|
+
* Anthropic; `PYLON_EMBEDDINGS_PROVIDER=voyage` + `VOYAGE_API_KEY`
|
|
410
|
+
* selects Voyage `voyage-3.5` (1024 dims).
|
|
411
|
+
* `PYLON_EMBEDDINGS_MODEL` overrides the model.
|
|
412
|
+
*
|
|
413
|
+
* Not available in queries (reactive re-runs would re-bill the
|
|
414
|
+
* provider) — embed in a mutation/action and store the vector.
|
|
415
|
+
* Errors carry `err.code`: `EMBEDDINGS_NOT_CONFIGURED`,
|
|
416
|
+
* `PROVIDER_HTTP_<code>`, `PROVIDER_UNREACHABLE`,
|
|
417
|
+
* `INVALID_REQUEST`.
|
|
418
|
+
*/
|
|
419
|
+
embed(input: string[], opts?: {
|
|
420
|
+
model?: string;
|
|
421
|
+
}): Promise<number[][]>;
|
|
342
422
|
}
|
|
343
423
|
/**
|
|
344
424
|
* One event from an in-flight {@link Llm.stream} call.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -63,6 +63,10 @@ export type {
|
|
|
63
63
|
RequireMemberOptions,
|
|
64
64
|
MemberRow,
|
|
65
65
|
Workflows,
|
|
66
|
+
VectorSearchQuery,
|
|
67
|
+
VectorSearchResult,
|
|
68
|
+
SearchResult,
|
|
69
|
+
PaginationResult,
|
|
66
70
|
// LLM + realtime surfaces. A handler writing an agent tool loop
|
|
67
71
|
// builds its own message array and branches on stream events, so
|
|
68
72
|
// these have to be nameable from app code.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-shape tests for the vector surfaces (runtime.ts):
|
|
3
|
+
*
|
|
4
|
+
* 1. `ctx.db.vectorSearch` emits `{type:"db", op:"vector_search"}`
|
|
5
|
+
* frames with the query on `data` and the unsafe_op / ssr_read
|
|
6
|
+
* flags the Rust policy gate keys on.
|
|
7
|
+
* 2. `ctx.llm.embed` emits `{type:"llm_embed", request:{input,model}}`
|
|
8
|
+
* with an op_id so concurrent embeds demux.
|
|
9
|
+
*
|
|
10
|
+
* Same child-process harness as runtime-db.test.ts — runtime.ts runs
|
|
11
|
+
* main() on import, so the builders are exercised in a probe script
|
|
12
|
+
* whose emitted NDJSON frames are the assertion target.
|
|
13
|
+
*/
|
|
14
|
+
import { expect, test } from "bun:test";
|
|
15
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
const RUNTIME = join(import.meta.dir, "runtime.ts");
|
|
20
|
+
|
|
21
|
+
const SCRIPT = `
|
|
22
|
+
import { buildDbReader, buildLlm } from ${JSON.stringify(RUNTIME)};
|
|
23
|
+
|
|
24
|
+
const reader = buildDbReader("c_r");
|
|
25
|
+
const llm = buildLlm("c_l");
|
|
26
|
+
|
|
27
|
+
// No host replies — don't await; the emitted frames are the target.
|
|
28
|
+
reader.vectorSearch("Doc", {
|
|
29
|
+
field: "embedding",
|
|
30
|
+
vector: [1, 0, 0],
|
|
31
|
+
limit: 5,
|
|
32
|
+
metric: "l2",
|
|
33
|
+
filter: { kind: "a" },
|
|
34
|
+
}).catch(() => {});
|
|
35
|
+
reader.unsafe.vectorSearch("Doc", { field: "embedding", vector: [1] }).catch(() => {});
|
|
36
|
+
llm.embed(["hello", "world"], { model: "text-embedding-3-large" }).catch(() => {});
|
|
37
|
+
|
|
38
|
+
setTimeout(() => process.exit(0), 300);
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
test("vectorSearch + llm.embed emit the exact wire shapes the host parses", async () => {
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), "pylon-fn-vec-"));
|
|
43
|
+
const scriptPath = join(dir, "probe.ts");
|
|
44
|
+
writeFileSync(scriptPath, SCRIPT);
|
|
45
|
+
|
|
46
|
+
const proc = Bun.spawn(["bun", scriptPath], {
|
|
47
|
+
stdin: "pipe",
|
|
48
|
+
stdout: "pipe",
|
|
49
|
+
stderr: "pipe",
|
|
50
|
+
});
|
|
51
|
+
const [stdout] = await Promise.all([
|
|
52
|
+
new Response(proc.stdout).text(),
|
|
53
|
+
proc.exited,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const frames = stdout
|
|
57
|
+
.split("\n")
|
|
58
|
+
.filter((l) => l.trim().startsWith("{"))
|
|
59
|
+
.map((l) => JSON.parse(l) as Record<string, any>);
|
|
60
|
+
|
|
61
|
+
const vs = frames.filter((f) => f.type === "db" && f.op === "vector_search");
|
|
62
|
+
expect(vs.length).toBe(2);
|
|
63
|
+
const safe = vs.find((f) => f.unsafe_op === false)!;
|
|
64
|
+
expect(safe).toMatchObject({
|
|
65
|
+
entity: "Doc",
|
|
66
|
+
ssr_read: false,
|
|
67
|
+
data: {
|
|
68
|
+
field: "embedding",
|
|
69
|
+
vector: [1, 0, 0],
|
|
70
|
+
limit: 5,
|
|
71
|
+
metric: "l2",
|
|
72
|
+
filter: { kind: "a" },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
expect(safe.op_id).toBeDefined();
|
|
76
|
+
const unsafe = vs.find((f) => f.unsafe_op === true)!;
|
|
77
|
+
expect(unsafe.data.field).toBe("embedding");
|
|
78
|
+
|
|
79
|
+
const embed = frames.find((f) => f.type === "llm_embed")!;
|
|
80
|
+
expect(embed).toBeDefined();
|
|
81
|
+
expect(embed.call_id).toBe("c_l");
|
|
82
|
+
expect(embed.op_id).toBeDefined();
|
|
83
|
+
expect(embed.request).toMatchObject({
|
|
84
|
+
input: ["hello", "world"],
|
|
85
|
+
model: "text-embedding-3-large",
|
|
86
|
+
});
|
|
87
|
+
});
|
package/src/runtime.ts
CHANGED
|
@@ -689,6 +689,16 @@ function buildReaderOps(
|
|
|
689
689
|
ssr_read: ssrRead,
|
|
690
690
|
})) as any;
|
|
691
691
|
},
|
|
692
|
+
async vectorSearch(entity, query) {
|
|
693
|
+
return (await rpcDb(callId, {
|
|
694
|
+
type: "db",
|
|
695
|
+
op: "vector_search",
|
|
696
|
+
entity,
|
|
697
|
+
data: query,
|
|
698
|
+
unsafe_op: unsafeOp,
|
|
699
|
+
ssr_read: ssrRead,
|
|
700
|
+
})) as any;
|
|
701
|
+
},
|
|
692
702
|
};
|
|
693
703
|
}
|
|
694
704
|
|
|
@@ -915,6 +925,18 @@ export function buildLlm(callId: string): Llm {
|
|
|
915
925
|
(event) => onEvent(event as LlmStreamEvent),
|
|
916
926
|
)) as LlmCompleteResponse;
|
|
917
927
|
},
|
|
928
|
+
|
|
929
|
+
async embed(
|
|
930
|
+
input: string[],
|
|
931
|
+
opts?: { model?: string },
|
|
932
|
+
): Promise<number[][]> {
|
|
933
|
+
// op_id-keyed rpc so concurrent embeds in one handler demux.
|
|
934
|
+
const resp = (await rpcDb(callId, {
|
|
935
|
+
type: "llm_embed",
|
|
936
|
+
request: { input, model: opts?.model },
|
|
937
|
+
})) as { embeddings: number[][] };
|
|
938
|
+
return resp.embeddings;
|
|
939
|
+
},
|
|
918
940
|
};
|
|
919
941
|
}
|
|
920
942
|
|
package/src/types.ts
CHANGED
|
@@ -156,6 +156,36 @@ export interface DbReader {
|
|
|
156
156
|
query: Record<string, unknown>
|
|
157
157
|
): Promise<SearchResult>;
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Exact k-nearest-neighbor search over a `field.vector(dims)` field.
|
|
161
|
+
* Cosine similarity by default (`metric: "dot" | "l2"` to change);
|
|
162
|
+
* hits come back best-first with the full row on `doc` (vector
|
|
163
|
+
* fields stripped — re-fetch by id if you need the embedding).
|
|
164
|
+
*
|
|
165
|
+
* Available wherever `ctx.db` is — queries and mutations. Actions
|
|
166
|
+
* have no `ctx.db`: embed there, then `ctx.runQuery` a query that
|
|
167
|
+
* searches with the vector.
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* // In a query: the vector arrives as an arg (an action embedded it).
|
|
171
|
+
* const { hits } = await ctx.db.vectorSearch("Doc", {
|
|
172
|
+
* field: "embedding",
|
|
173
|
+
* vector,
|
|
174
|
+
* limit: 5,
|
|
175
|
+
* filter: { status: "published" }, // equality / IN pre-filter
|
|
176
|
+
* });
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* Exact scan, not ANN: every non-NULL embedding is scored. Fine to
|
|
180
|
+
* ~100k rows per entity; past that, a dedicated vector store wins.
|
|
181
|
+
* Throws `VECTOR_FIELD_NOT_FOUND` when `field` isn't a vector field
|
|
182
|
+
* and `INVALID_QUERY` on dimension mismatch or bad filters.
|
|
183
|
+
*/
|
|
184
|
+
vectorSearch(
|
|
185
|
+
entity: string,
|
|
186
|
+
query: VectorSearchQuery
|
|
187
|
+
): Promise<VectorSearchResult>;
|
|
188
|
+
|
|
159
189
|
/**
|
|
160
190
|
* Cursor-paginated list. Pass `cursor` from a previous page's `nextCursor`
|
|
161
191
|
* to continue; pass `null` for the first page.
|
|
@@ -196,6 +226,30 @@ export interface SearchResult<T = Record<string, unknown>> {
|
|
|
196
226
|
tookMs: number;
|
|
197
227
|
}
|
|
198
228
|
|
|
229
|
+
/** Request shape for [`DbReader.vectorSearch`]. */
|
|
230
|
+
export interface VectorSearchQuery {
|
|
231
|
+
/** The `vector(dims)` field to search. */
|
|
232
|
+
field: string;
|
|
233
|
+
/** Query embedding; length must equal the field's declared dims. */
|
|
234
|
+
vector: number[];
|
|
235
|
+
/** Max hits. Default 10, capped at 200. */
|
|
236
|
+
limit?: number;
|
|
237
|
+
/** Similarity metric. Default "cosine" (higher = closer);
|
|
238
|
+
* "dot" (higher = closer); "l2" (Euclidean distance, lower = closer). */
|
|
239
|
+
metric?: "cosine" | "dot" | "l2";
|
|
240
|
+
/** Equality pre-filter applied in SQL before scoring. A plain value
|
|
241
|
+
* means equality; an array means SQL `IN`; `null` means IS NULL. */
|
|
242
|
+
filter?: Record<string, unknown>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Result shape for [`DbReader.vectorSearch`]. */
|
|
246
|
+
export interface VectorSearchResult<T = Record<string, unknown>> {
|
|
247
|
+
/** Best-first hits for the chosen metric. */
|
|
248
|
+
hits: Array<{ id: string; score: number; doc: T }>;
|
|
249
|
+
/** Milliseconds spent scanning + scoring. */
|
|
250
|
+
tookMs: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
199
253
|
// ---------------------------------------------------------------------------
|
|
200
254
|
// Database — write operations (extends read)
|
|
201
255
|
// ---------------------------------------------------------------------------
|
|
@@ -422,6 +476,33 @@ export interface Llm {
|
|
|
422
476
|
request: LlmCompleteRequest,
|
|
423
477
|
onEvent: (event: LlmStreamEvent) => void,
|
|
424
478
|
): Promise<LlmCompleteResponse>;
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Batch-embed texts via the configured embeddings provider. One
|
|
482
|
+
* embedding per input, in input order. Pair with a
|
|
483
|
+
* `field.vector(dims)` field and `ctx.db.vectorSearch` for
|
|
484
|
+
* retrieval. From an action (which has no `ctx.db`), store via a
|
|
485
|
+
* mutation:
|
|
486
|
+
*
|
|
487
|
+
* ```ts
|
|
488
|
+
* const [vec] = await ctx.llm.embed([doc.body]);
|
|
489
|
+
* await ctx.runMutation("saveEmbedding", { docId: doc.id, embedding: vec });
|
|
490
|
+
* ```
|
|
491
|
+
*
|
|
492
|
+
* The embeddings provider is a separate axis from chat: with
|
|
493
|
+
* `OPENAI_API_KEY` set it defaults to OpenAI
|
|
494
|
+
* `text-embedding-3-small` (1536 dims) even when chat runs
|
|
495
|
+
* Anthropic; `PYLON_EMBEDDINGS_PROVIDER=voyage` + `VOYAGE_API_KEY`
|
|
496
|
+
* selects Voyage `voyage-3.5` (1024 dims).
|
|
497
|
+
* `PYLON_EMBEDDINGS_MODEL` overrides the model.
|
|
498
|
+
*
|
|
499
|
+
* Not available in queries (reactive re-runs would re-bill the
|
|
500
|
+
* provider) — embed in a mutation/action and store the vector.
|
|
501
|
+
* Errors carry `err.code`: `EMBEDDINGS_NOT_CONFIGURED`,
|
|
502
|
+
* `PROVIDER_HTTP_<code>`, `PROVIDER_UNREACHABLE`,
|
|
503
|
+
* `INVALID_REQUEST`.
|
|
504
|
+
*/
|
|
505
|
+
embed(input: string[], opts?: { model?: string }): Promise<number[][]>;
|
|
425
506
|
}
|
|
426
507
|
|
|
427
508
|
/**
|