@midsummer-ai/sdk 0.0.0-stage → 0.1.0-next.1723
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/LICENSE +177 -0
- package/README.md +4 -2
- package/agents/contract.d.mts +9 -0
- package/agents/contract.mjs +221 -0
- package/agents/index.d.mts +128 -0
- package/agents/index.mjs +203 -0
- package/agents/stand-in.d.mts +35 -0
- package/agents/stand-in.mjs +149 -0
- package/ai/contract.d.mts +10 -0
- package/ai/contract.mjs +77 -0
- package/ai/index.d.mts +94 -0
- package/ai/index.mjs +193 -0
- package/ai/stand-in.d.mts +7 -0
- package/ai/stand-in.mjs +41 -0
- package/build-SQ44jwSa.d.mts +41 -0
- package/client.d.mts +45 -0
- package/client.mjs +106 -0
- package/connection-BHjDaLYA.mjs +2009 -0
- package/contract.d.mts +10 -0
- package/contract.mjs +469 -0
- package/database/build.d.mts +2 -0
- package/database/build.mjs +579 -0
- package/database/client.d.mts +60 -0
- package/database/client.mjs +133 -0
- package/database/contract.d.mts +11 -0
- package/database/contract.mjs +164 -0
- package/database/errors.d.mts +23 -0
- package/database/errors.mjs +13 -0
- package/database/server.d.mts +3 -0
- package/database/server.mjs +320 -0
- package/database/stand-in.d.mts +99 -0
- package/database/stand-in.mjs +430 -0
- package/email/client.d.mts +34 -0
- package/email/client.mjs +131 -0
- package/email/contract.d.mts +11 -0
- package/email/contract.mjs +99 -0
- package/email/index.d.mts +74 -0
- package/email/index.mjs +59 -0
- package/email/smtp.d.mts +22 -0
- package/email/smtp.mjs +296 -0
- package/email/stand-in.d.mts +13 -0
- package/email/stand-in.mjs +51 -0
- package/index-DdoK0y7D.d.mts +192 -0
- package/index-sgdyoA2P.d.mts +212 -0
- package/index.d.mts +2 -0
- package/index.mjs +137 -0
- package/names-DfCBKupJ.mjs +115 -0
- package/package.json +152 -4
- package/predict-Drfm_8pO.mjs +179 -0
- package/queues/contract.d.mts +13 -0
- package/queues/contract.mjs +278 -0
- package/queues/index.d.mts +184 -0
- package/queues/index.mjs +126 -0
- package/queues/stand-in.d.mts +7 -0
- package/queues/stand-in.mjs +110 -0
- package/queues/wire.d.mts +9 -0
- package/queues/wire.mjs +199 -0
- package/s3.d.mts +43 -0
- package/s3.mjs +1003 -0
- package/schema-BF-Y1EVu.mjs +142 -0
- package/schema-DS8x1Dn8.mjs +115 -0
- package/schema-f7E9uzUn.d.mts +74 -0
- package/server-C7O7CICU.d.mts +182 -0
- package/socket.d.mts +67 -0
- package/socket.mjs +263 -0
- package/stand-in.d.mts +7 -0
- package/stand-in.mjs +185 -0
- package/store-BA8K-bAR.mjs +237 -0
- package/tabs-DEeMee7M.mjs +457 -0
- package/vector/contract.d.mts +10 -0
- package/vector/contract.mjs +635 -0
- package/vector/index.d.mts +2 -0
- package/vector/index.mjs +108 -0
- package/vector/stand-in.d.mts +11 -0
- package/vector/stand-in.mjs +169 -0
- package/vector/wire.d.mts +25 -0
- package/vector/wire.mjs +175 -0
- package/wire.d.mts +54 -0
- package/wire.mjs +731 -0
package/vector/index.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { a as MAX_TEXT, c as NamespaceName, d as Vectors, f as checked, i as MAX_METADATA_BYTES, l as Query, m as vectorErrorCodes, n as MAX_BATCH, o as MAX_TOP_K, p as VectorError, r as MAX_DIMENSION, s as MAX_VALUE, t as Ids } from "../schema-DS8x1Dn8.mjs";
|
|
2
|
+
//#region src/vector/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Vector DB: a Team's Vectors in Vector namespaces, searched by closeness and filtered by their
|
|
5
|
+
* metadata.
|
|
6
|
+
*
|
|
7
|
+
* In your Deployment's server, Midsummer hands in the Environment's Vector DB as `context.vector`.
|
|
8
|
+
* A namespace is named by the name the Environment binds it under (`midsummer env binding set`):
|
|
9
|
+
*
|
|
10
|
+
* import { vectorDb } from "@midsummer-ai/sdk/vector"
|
|
11
|
+
* import { remote } from "@midsummer-ai/sdk/vector/wire"
|
|
12
|
+
*
|
|
13
|
+
* const docs = vectorDb(remote(context.vector)).namespace("docs")
|
|
14
|
+
* await docs.upsert([{ id: "intro", values: [0.1, 0.2, 0.3], metadata: { lang: "en" } }])
|
|
15
|
+
* const matches = await docs.query([0.1, 0.2, 0.25], { topK: 5, filter: { lang: "en" }, includeMetadata: true })
|
|
16
|
+
*
|
|
17
|
+
* // A namespace made with an embedding model takes text: it is embedded as it is written or asked.
|
|
18
|
+
* await notes.upsert([{ id: "reset", text: "How to reset a forgotten password", metadata: { lang: "en" } }])
|
|
19
|
+
* const [best] = await notes.query("I forgot my password", { topK: 1 })
|
|
20
|
+
* const [intro] = await docs.fetch(["intro"])
|
|
21
|
+
* await docs.delete(["intro"])
|
|
22
|
+
*
|
|
23
|
+
* In tests and `midsummer dev`, `standIn()` from `@midsummer-ai/sdk/vector/stand-in` keeps Vectors in
|
|
24
|
+
* memory. Each is held to the cases in `@midsummer-ai/sdk/vector/contract`; a behaviour that is not a
|
|
25
|
+
* case there is not promised.
|
|
26
|
+
*
|
|
27
|
+
* A Vector namespace is made with its dimension and metric, which never change: through the
|
|
28
|
+
* platform (`midsummer vector namespace create`), or the Stand-in's `create`. Made with an embedding
|
|
29
|
+
* model (`embed`), which never changes either, it takes text wherever it takes values: each text is
|
|
30
|
+
* embedded by that model as part of the write or query that carries it, and its vector is stored or
|
|
31
|
+
* searched. The text itself is not kept; a Vector that needs it back keeps it in its metadata. Code reads and writes
|
|
32
|
+
* the Vectors of the namespaces its Environment has; it never makes or drops one.
|
|
33
|
+
*
|
|
34
|
+
* Calls are checked here, before anything leaves the caller. Every failure is a `VectorError`; its
|
|
35
|
+
* `code` tells you which kind, and is the same code the wire carries.
|
|
36
|
+
*/
|
|
37
|
+
/** Vector DB over `engine`. */
|
|
38
|
+
function vectorDb(engine) {
|
|
39
|
+
return { namespace(name) {
|
|
40
|
+
checked(NamespaceName, name);
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
async upsert(vectors) {
|
|
44
|
+
return engine.upsert(name, checked(Vectors, vectors));
|
|
45
|
+
},
|
|
46
|
+
async query(vector, options = {}) {
|
|
47
|
+
return engine.query(name, checked(Query, {
|
|
48
|
+
...typeof vector === "string" ? { text: vector } : { vector },
|
|
49
|
+
topK: options.topK ?? 10,
|
|
50
|
+
includeValues: options.includeValues ?? false,
|
|
51
|
+
includeMetadata: options.includeMetadata ?? false,
|
|
52
|
+
...options.filter !== void 0 ? { filter: options.filter } : {},
|
|
53
|
+
...options.consistency !== void 0 ? { consistency: options.consistency } : {}
|
|
54
|
+
}));
|
|
55
|
+
},
|
|
56
|
+
async fetch(ids) {
|
|
57
|
+
return engine.fetch(name, checked(Ids, ids));
|
|
58
|
+
},
|
|
59
|
+
async delete(ids) {
|
|
60
|
+
return engine.delete(name, checked(Ids, ids));
|
|
61
|
+
},
|
|
62
|
+
async describe() {
|
|
63
|
+
return engine.describe(name);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `engine`, which keeps only values, taking text too: the texts of each call are embedded by `embed`,
|
|
70
|
+
* given the namespace and the texts in order, before `engine` sees the call. `embed` returns
|
|
71
|
+
* `undefined` for a namespace made without an embedding model, which is `invalid`.
|
|
72
|
+
*
|
|
73
|
+
* const site = embedded(engine, (namespace, texts) => vector.embed(namespace, texts))
|
|
74
|
+
*/
|
|
75
|
+
function embedded(engine, embed) {
|
|
76
|
+
async function values(namespace, texts) {
|
|
77
|
+
const found = await embed(namespace, texts);
|
|
78
|
+
if (!found) throw new VectorError("invalid", "This Vector namespace was made without an embedding model, so it takes values, not text.");
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
describe: (namespace) => engine.describe(namespace),
|
|
83
|
+
async upsert(namespace, vectors) {
|
|
84
|
+
const texts = vectors.flatMap((vector) => "text" in vector ? [vector.text] : []);
|
|
85
|
+
if (!texts.length) return engine.upsert(namespace, vectors);
|
|
86
|
+
const embeddings = await values(namespace, texts);
|
|
87
|
+
let next = 0;
|
|
88
|
+
return engine.upsert(namespace, vectors.map((vector) => "text" in vector ? {
|
|
89
|
+
id: vector.id,
|
|
90
|
+
values: embeddings[next++],
|
|
91
|
+
...vector.metadata ? { metadata: vector.metadata } : {}
|
|
92
|
+
} : vector));
|
|
93
|
+
},
|
|
94
|
+
async query(namespace, query) {
|
|
95
|
+
if (!("text" in query)) return engine.query(namespace, query);
|
|
96
|
+
const { text, ...rest } = query;
|
|
97
|
+
const [vector] = await values(namespace, [text]);
|
|
98
|
+
return engine.query(namespace, {
|
|
99
|
+
...rest,
|
|
100
|
+
vector
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
fetch: (namespace, ids) => engine.fetch(namespace, ids),
|
|
104
|
+
delete: (namespace, ids) => engine.delete(namespace, ids)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
export { MAX_BATCH, MAX_DIMENSION, MAX_METADATA_BYTES, MAX_TEXT, MAX_TOP_K, MAX_VALUE, VectorError, embedded, vectorDb, vectorErrorCodes };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { g as VectorEngine, m as VectorControl } from "../index-DdoK0y7D.mjs";
|
|
2
|
+
//#region src/vector/stand-in.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A text's Stand-in embedding of `dimension` numbers: each word (letters and digits, in lower case)
|
|
5
|
+
* adds 1 or -1 to the place its hash picks, and the whole is scaled to length 1. A text with no word
|
|
6
|
+
* counts as one word. Never all zeros.
|
|
7
|
+
*/
|
|
8
|
+
declare function localEmbedding(text: string, dimension: number): number[];
|
|
9
|
+
declare function standIn(): VectorEngine & VectorControl;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { localEmbedding, standIn };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { p as VectorError } from "../schema-DS8x1Dn8.mjs";
|
|
2
|
+
import { embedded } from "./index.mjs";
|
|
3
|
+
//#region src/vector/stand-in.ts
|
|
4
|
+
/**
|
|
5
|
+
* Vector DB's Stand-in: the Capability's behaviour in memory, for `midsummer dev` and tests. It holds
|
|
6
|
+
* no engine code and passes the contract (`@midsummer-ai/sdk/vector/contract`) except durability and
|
|
7
|
+
* scale. A query is an exact search: every Vector of the namespace is scored, so it returns what an
|
|
8
|
+
* approximate engine aims for.
|
|
9
|
+
*
|
|
10
|
+
* A namespace made with an embedding model, whatever its name, embeds text with `localEmbedding`:
|
|
11
|
+
* a hash of its words, free and the same everywhere, so a text and the words it shares are close and
|
|
12
|
+
* the same text is found first. It is not a model's sense of meaning.
|
|
13
|
+
*
|
|
14
|
+
* const engine = standIn()
|
|
15
|
+
* await engine.create("docs", { dimension: 3, metric: "cosine" })
|
|
16
|
+
* const docs = vectorDb(engine).namespace("docs")
|
|
17
|
+
*
|
|
18
|
+
* Everything is lost when the process ends.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* A text's Stand-in embedding of `dimension` numbers: each word (letters and digits, in lower case)
|
|
22
|
+
* adds 1 or -1 to the place its hash picks, and the whole is scaled to length 1. A text with no word
|
|
23
|
+
* counts as one word. Never all zeros.
|
|
24
|
+
*/
|
|
25
|
+
function localEmbedding(text, dimension) {
|
|
26
|
+
const values = new Array(dimension).fill(0);
|
|
27
|
+
const words = text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [text];
|
|
28
|
+
for (const word of words) {
|
|
29
|
+
let hash = 2166136261;
|
|
30
|
+
for (let index = 0; index < word.length; index += 1) hash = Math.imul(hash ^ word.charCodeAt(index), 16777619);
|
|
31
|
+
values[(hash >>> 1) % dimension] += hash & 1 ? 1 : -1;
|
|
32
|
+
}
|
|
33
|
+
const length = Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
|
|
34
|
+
if (length === 0) return values.map((_, index) => index === 0 ? 1 : 0);
|
|
35
|
+
return values.map((value) => value / length);
|
|
36
|
+
}
|
|
37
|
+
function standIn() {
|
|
38
|
+
const engine = valuesOnly();
|
|
39
|
+
return {
|
|
40
|
+
...embedded(engine, async (name, texts) => {
|
|
41
|
+
const namespace = engine.held(name);
|
|
42
|
+
return namespace.embed === void 0 ? void 0 : texts.map((text) => localEmbedding(text, namespace.dimension));
|
|
43
|
+
}),
|
|
44
|
+
create: engine.create,
|
|
45
|
+
drop: engine.drop
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The Stand-in over values alone, and its namespaces as they are held. */
|
|
49
|
+
function valuesOnly() {
|
|
50
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
51
|
+
const held = (name) => {
|
|
52
|
+
const namespace = namespaces.get(name);
|
|
53
|
+
if (!namespace) throw new VectorError("not_found", `There is no Vector namespace named ${name}.`);
|
|
54
|
+
return namespace;
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
held,
|
|
58
|
+
async create(name, settings) {
|
|
59
|
+
const current = namespaces.get(name);
|
|
60
|
+
if (!current) return void namespaces.set(name, {
|
|
61
|
+
...settings,
|
|
62
|
+
vectors: /* @__PURE__ */ new Map()
|
|
63
|
+
});
|
|
64
|
+
if (current.dimension !== settings.dimension || current.metric !== settings.metric || current.embed !== settings.embed) throw new VectorError("conflict", `The Vector namespace ${name} exists with dimension ${current.dimension} and metric ${current.metric}${current.embed ? `, embedded by ${current.embed}` : ""}.`);
|
|
65
|
+
},
|
|
66
|
+
async drop(name) {
|
|
67
|
+
namespaces.delete(name);
|
|
68
|
+
},
|
|
69
|
+
async describe(name) {
|
|
70
|
+
const namespace = held(name);
|
|
71
|
+
return {
|
|
72
|
+
dimension: namespace.dimension,
|
|
73
|
+
metric: namespace.metric,
|
|
74
|
+
count: namespace.vectors.size
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
async upsert(name, vectors) {
|
|
78
|
+
const namespace = held(name);
|
|
79
|
+
for (const vector of vectors) fits(namespace, vector.values, `Vector ${vector.id}`);
|
|
80
|
+
for (const vector of vectors) namespace.vectors.set(vector.id, {
|
|
81
|
+
...copy(vector),
|
|
82
|
+
values: vector.values.map((value) => Math.fround(value))
|
|
83
|
+
});
|
|
84
|
+
return { upserted: vectors.length };
|
|
85
|
+
},
|
|
86
|
+
async query(name, query) {
|
|
87
|
+
const namespace = held(name);
|
|
88
|
+
fits(namespace, query.vector, "The query's vector");
|
|
89
|
+
const scored = [];
|
|
90
|
+
for (const vector of namespace.vectors.values()) {
|
|
91
|
+
if (query.filter && !matches(query.filter, vector.metadata ?? {})) continue;
|
|
92
|
+
scored.push({
|
|
93
|
+
vector,
|
|
94
|
+
score: score(namespace.metric, query.vector, vector.values)
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const closer = namespace.metric === "euclidean" ? (a, b) => a - b : (a, b) => b - a;
|
|
98
|
+
scored.sort((a, b) => closer(a.score, b.score) || (a.vector.id < b.vector.id ? -1 : a.vector.id > b.vector.id ? 1 : 0));
|
|
99
|
+
return scored.slice(0, query.topK).map(({ vector, score }) => ({
|
|
100
|
+
id: vector.id,
|
|
101
|
+
score,
|
|
102
|
+
...query.includeValues ? { values: [...vector.values] } : {},
|
|
103
|
+
...query.includeMetadata ? { metadata: { ...vector.metadata } } : {}
|
|
104
|
+
}));
|
|
105
|
+
},
|
|
106
|
+
async fetch(name, ids) {
|
|
107
|
+
const namespace = held(name);
|
|
108
|
+
return ids.flatMap((id) => {
|
|
109
|
+
const vector = namespace.vectors.get(id);
|
|
110
|
+
return vector ? [copy(vector)] : [];
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
async delete(name, ids) {
|
|
114
|
+
const namespace = held(name);
|
|
115
|
+
for (const id of ids) namespace.vectors.delete(id);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function copy(vector) {
|
|
120
|
+
return {
|
|
121
|
+
id: vector.id,
|
|
122
|
+
values: [...vector.values],
|
|
123
|
+
...vector.metadata ? { metadata: { ...vector.metadata } } : {}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Refuses `values` that are not the namespace's dimension, or, for cosine, all zero: they have no direction. */
|
|
127
|
+
function fits(namespace, values, what) {
|
|
128
|
+
if (values.length !== namespace.dimension) throw new VectorError("invalid", `${what} has ${values.length} values; this Vector namespace's dimension is ${namespace.dimension}.`);
|
|
129
|
+
if (namespace.metric === "cosine" && values.every((value) => value === 0)) throw new VectorError("invalid", `${what} is all zeros, which has no direction for cosine.`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The metric's score, kept finite for any values a Vector may hold. Cosine is the dot product of the
|
|
133
|
+
* two Vectors each scaled by its largest value first, so tiny values do not vanish when squared and
|
|
134
|
+
* large ones do not overflow; euclidean scales the differences the same way.
|
|
135
|
+
*/
|
|
136
|
+
function score(metric, a, b) {
|
|
137
|
+
if (metric === "dot") {
|
|
138
|
+
let dot = 0;
|
|
139
|
+
for (let index = 0; index < a.length; index++) dot += a[index] * b[index];
|
|
140
|
+
return dot;
|
|
141
|
+
}
|
|
142
|
+
if (metric === "euclidean") {
|
|
143
|
+
const differences = a.map((x, index) => x - b[index]);
|
|
144
|
+
const largest = Math.max(...differences.map(Math.abs));
|
|
145
|
+
if (largest === 0) return 0;
|
|
146
|
+
return largest * Math.sqrt(differences.reduce((sum, d) => sum + (d / largest) ** 2, 0));
|
|
147
|
+
}
|
|
148
|
+
const unit = (values) => {
|
|
149
|
+
const largest = Math.max(...values.map(Math.abs));
|
|
150
|
+
const scaled = values.map((value) => value / largest);
|
|
151
|
+
const length = Math.sqrt(scaled.reduce((sum, value) => sum + value * value, 0));
|
|
152
|
+
return scaled.map((value) => value / length);
|
|
153
|
+
};
|
|
154
|
+
const x = unit(a);
|
|
155
|
+
const y = unit(b);
|
|
156
|
+
let cosine = 0;
|
|
157
|
+
for (let index = 0; index < x.length; index++) cosine += x[index] * y[index];
|
|
158
|
+
return Math.max(-1, Math.min(1, cosine));
|
|
159
|
+
}
|
|
160
|
+
function matches(filter, metadata) {
|
|
161
|
+
return Object.entries(filter).every(([field, condition]) => holds(condition, Object.hasOwn(metadata, field) ? metadata[field] : void 0));
|
|
162
|
+
}
|
|
163
|
+
function holds(condition, value) {
|
|
164
|
+
if (typeof condition !== "object") return value === condition;
|
|
165
|
+
const ordered = (bound, test) => value !== void 0 && typeof value === typeof bound && typeof value !== "boolean" && test(value < bound ? -1 : value > bound ? 1 : 0);
|
|
166
|
+
return (condition.$eq === void 0 || value === condition.$eq) && (condition.$ne === void 0 || value !== condition.$ne) && (condition.$in === void 0 || value !== void 0 && condition.$in.includes(value)) && (condition.$nin === void 0 || value === void 0 || !condition.$nin.includes(value)) && (condition.$gt === void 0 || ordered(condition.$gt, (order) => order > 0)) && (condition.$gte === void 0 || ordered(condition.$gte, (order) => order >= 0)) && (condition.$lt === void 0 || ordered(condition.$lt, (order) => order < 0)) && (condition.$lte === void 0 || ordered(condition.$lte, (order) => order <= 0));
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
export { localEmbedding, standIn };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { g as VectorEngine, m as VectorControl } from "../index-DdoK0y7D.mjs";
|
|
2
|
+
//#region src/vector/wire.d.ts
|
|
3
|
+
/** Carries one request frame to a server and brings back its answer frame. */
|
|
4
|
+
interface Transport {
|
|
5
|
+
send(frame: Uint8Array): Promise<Uint8Array>;
|
|
6
|
+
}
|
|
7
|
+
/** An engine that sends each call over `transport`. A transport that fails is `unavailable`. */
|
|
8
|
+
declare function remote(transport: Transport): VectorEngine & VectorControl;
|
|
9
|
+
/**
|
|
10
|
+
* Serves request frames from `engine`. A frame comes from the network, so it is checked here as the
|
|
11
|
+
* client would check it; a refusal is returned as `invalid`. Making and dropping namespaces is
|
|
12
|
+
* served only by an engine that has them, else `forbidden`. A failure that is not a `VectorError`
|
|
13
|
+
* is the engine's own fault and is thrown to the transport, never returned.
|
|
14
|
+
*/
|
|
15
|
+
declare function serve(engine: VectorEngine & Partial<VectorControl>): (frame: Uint8Array) => Promise<Uint8Array>;
|
|
16
|
+
/**
|
|
17
|
+
* `engine` as a Site of one Environment reaches it: `namespaceId` returns, for the one name a call
|
|
18
|
+
* names, the id the engine knows that Environment's Vector namespace by, so a call costs the lookup
|
|
19
|
+
* of its own namespace and never a listing of the others. A name with no id is `not_found`, as a
|
|
20
|
+
* namespace that does not exist is, so a Site learns nothing of other Environments. It cannot make
|
|
21
|
+
* or drop one.
|
|
22
|
+
*/
|
|
23
|
+
declare function scoped(engine: VectorEngine, namespaceId: (name: string) => Promise<string | undefined>): VectorEngine;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { Transport, remote, scoped, serve };
|
package/vector/wire.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { c as NamespaceName, d as Vectors, f as checked, l as Query, m as vectorErrorCodes, p as VectorError, t as Ids, u as Settings } from "../schema-DS8x1Dn8.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/vector/wire.ts
|
|
4
|
+
/**
|
|
5
|
+
* Vector DB's wire: one call is one frame, the UTF-8 JSON of `{ op, namespace, … }`, and its answer
|
|
6
|
+
* is one frame, `{ ok }` or `{ error, why }` with a `VectorErrorCode`. A transport only carries
|
|
7
|
+
* frames; a Site's own handle is one (`{ send }`), handed to it by the platform for its Environment.
|
|
8
|
+
*
|
|
9
|
+
* const docs = vectorDb(remote(context.vector)).namespace("docs") // a Site
|
|
10
|
+
* const answer = await serve(scoped(engine, (name) => vector.siteNamespace(repositoryId, environment, name)))(frame) // the platform
|
|
11
|
+
*
|
|
12
|
+
* `scoped` is how the platform serves a Site: names mapped to the ids the engine knows them by, and
|
|
13
|
+
* making or dropping a namespace refused.
|
|
14
|
+
*/
|
|
15
|
+
const call = z.discriminatedUnion("op", [
|
|
16
|
+
z.object({
|
|
17
|
+
op: z.literal("describe"),
|
|
18
|
+
namespace: z.string()
|
|
19
|
+
}),
|
|
20
|
+
z.object({
|
|
21
|
+
op: z.literal("upsert"),
|
|
22
|
+
namespace: z.string(),
|
|
23
|
+
vectors: z.unknown()
|
|
24
|
+
}),
|
|
25
|
+
z.object({
|
|
26
|
+
op: z.literal("query"),
|
|
27
|
+
namespace: z.string(),
|
|
28
|
+
query: z.unknown()
|
|
29
|
+
}),
|
|
30
|
+
z.object({
|
|
31
|
+
op: z.literal("fetch"),
|
|
32
|
+
namespace: z.string(),
|
|
33
|
+
ids: z.unknown()
|
|
34
|
+
}),
|
|
35
|
+
z.object({
|
|
36
|
+
op: z.literal("delete"),
|
|
37
|
+
namespace: z.string(),
|
|
38
|
+
ids: z.unknown()
|
|
39
|
+
}),
|
|
40
|
+
z.object({
|
|
41
|
+
op: z.literal("create"),
|
|
42
|
+
namespace: z.string(),
|
|
43
|
+
settings: z.unknown()
|
|
44
|
+
}),
|
|
45
|
+
z.object({
|
|
46
|
+
op: z.literal("drop"),
|
|
47
|
+
namespace: z.string()
|
|
48
|
+
})
|
|
49
|
+
]);
|
|
50
|
+
const answer = z.union([z.object({
|
|
51
|
+
error: z.enum(vectorErrorCodes),
|
|
52
|
+
why: z.string()
|
|
53
|
+
}), z.object({ ok: z.unknown() })]);
|
|
54
|
+
const encode = (value) => new TextEncoder().encode(JSON.stringify(value));
|
|
55
|
+
/** An engine that sends each call over `transport`. A transport that fails is `unavailable`. */
|
|
56
|
+
function remote(transport) {
|
|
57
|
+
async function ask(request) {
|
|
58
|
+
let frame;
|
|
59
|
+
try {
|
|
60
|
+
frame = await transport.send(encode(request));
|
|
61
|
+
} catch {
|
|
62
|
+
throw new VectorError("unavailable", "Vector DB could not be reached. Try again.");
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = answer.parse(JSON.parse(new TextDecoder().decode(frame)));
|
|
67
|
+
} catch {
|
|
68
|
+
throw new VectorError("unavailable", "Vector DB answered something that is not a Vector DB frame");
|
|
69
|
+
}
|
|
70
|
+
if ("error" in parsed) throw new VectorError(parsed.error, parsed.why);
|
|
71
|
+
return parsed.ok;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
describe: (namespace) => ask({
|
|
75
|
+
op: "describe",
|
|
76
|
+
namespace
|
|
77
|
+
}),
|
|
78
|
+
upsert: (namespace, vectors) => ask({
|
|
79
|
+
op: "upsert",
|
|
80
|
+
namespace,
|
|
81
|
+
vectors
|
|
82
|
+
}),
|
|
83
|
+
query: (namespace, query) => ask({
|
|
84
|
+
op: "query",
|
|
85
|
+
namespace,
|
|
86
|
+
query
|
|
87
|
+
}),
|
|
88
|
+
fetch: (namespace, ids) => ask({
|
|
89
|
+
op: "fetch",
|
|
90
|
+
namespace,
|
|
91
|
+
ids
|
|
92
|
+
}),
|
|
93
|
+
delete: async (namespace, ids) => void await ask({
|
|
94
|
+
op: "delete",
|
|
95
|
+
namespace,
|
|
96
|
+
ids
|
|
97
|
+
}),
|
|
98
|
+
create: async (namespace, settings) => void await ask({
|
|
99
|
+
op: "create",
|
|
100
|
+
namespace,
|
|
101
|
+
settings
|
|
102
|
+
}),
|
|
103
|
+
drop: async (namespace) => void await ask({
|
|
104
|
+
op: "drop",
|
|
105
|
+
namespace
|
|
106
|
+
})
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Serves request frames from `engine`. A frame comes from the network, so it is checked here as the
|
|
111
|
+
* client would check it; a refusal is returned as `invalid`. Making and dropping namespaces is
|
|
112
|
+
* served only by an engine that has them, else `forbidden`. A failure that is not a `VectorError`
|
|
113
|
+
* is the engine's own fault and is thrown to the transport, never returned.
|
|
114
|
+
*/
|
|
115
|
+
function serve(engine) {
|
|
116
|
+
return async (frame) => {
|
|
117
|
+
try {
|
|
118
|
+
let json;
|
|
119
|
+
try {
|
|
120
|
+
json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(frame));
|
|
121
|
+
} catch {
|
|
122
|
+
throw new VectorError("invalid", "the request frame is not JSON");
|
|
123
|
+
}
|
|
124
|
+
const parsed = call.safeParse(json);
|
|
125
|
+
if (!parsed.success) throw new VectorError("invalid", "the request frame is not a Vector DB call");
|
|
126
|
+
const request = parsed.data;
|
|
127
|
+
const namespace = checked(NamespaceName, request.namespace);
|
|
128
|
+
return encode({ ok: await answerTo(engine, namespace, request) ?? null });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof VectorError) return encode({
|
|
131
|
+
error: error.code,
|
|
132
|
+
why: error.message
|
|
133
|
+
});
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function answerTo(engine, namespace, request) {
|
|
139
|
+
switch (request.op) {
|
|
140
|
+
case "describe": return engine.describe(namespace);
|
|
141
|
+
case "upsert": return engine.upsert(namespace, checked(Vectors, request.vectors));
|
|
142
|
+
case "query": return engine.query(namespace, checked(Query, request.query));
|
|
143
|
+
case "fetch": return engine.fetch(namespace, checked(Ids, request.ids));
|
|
144
|
+
case "delete": return engine.delete(namespace, checked(Ids, request.ids));
|
|
145
|
+
case "create":
|
|
146
|
+
if (!engine.create) throw new VectorError("forbidden", "Vector namespaces are made through the platform");
|
|
147
|
+
return engine.create(namespace, checked(Settings, request.settings));
|
|
148
|
+
case "drop":
|
|
149
|
+
if (!engine.drop) throw new VectorError("forbidden", "Vector namespaces are dropped through the platform");
|
|
150
|
+
return engine.drop(namespace);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `engine` as a Site of one Environment reaches it: `namespaceId` returns, for the one name a call
|
|
155
|
+
* names, the id the engine knows that Environment's Vector namespace by, so a call costs the lookup
|
|
156
|
+
* of its own namespace and never a listing of the others. A name with no id is `not_found`, as a
|
|
157
|
+
* namespace that does not exist is, so a Site learns nothing of other Environments. It cannot make
|
|
158
|
+
* or drop one.
|
|
159
|
+
*/
|
|
160
|
+
function scoped(engine, namespaceId) {
|
|
161
|
+
const id = async (name) => {
|
|
162
|
+
const found = await namespaceId(name);
|
|
163
|
+
if (found === void 0) throw new VectorError("not_found", `There is no Vector namespace named ${name}.`);
|
|
164
|
+
return found;
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
describe: async (namespace) => engine.describe(await id(namespace)),
|
|
168
|
+
upsert: async (namespace, vectors) => engine.upsert(await id(namespace), vectors),
|
|
169
|
+
query: async (namespace, query) => engine.query(await id(namespace), query),
|
|
170
|
+
fetch: async (namespace, ids) => engine.fetch(await id(namespace), ids),
|
|
171
|
+
delete: async (namespace, ids) => engine.delete(await id(namespace), ids)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { remote, scoped, serve };
|
package/wire.d.mts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { m as StorageEngine } from "./index-sgdyoA2P.mjs";
|
|
2
|
+
//#region src/wire.d.ts
|
|
3
|
+
/** Carries one request frame to a server and brings back its answer frame. */
|
|
4
|
+
interface Transport {
|
|
5
|
+
send(frame: Uint8Array): Promise<Uint8Array>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The engine on the client's side of the wire. With `batch`, the calls made in the same turn travel
|
|
9
|
+
* as one batch frame (the platform's own calls do not batch; clients that issue many at once do).
|
|
10
|
+
*/
|
|
11
|
+
declare function remote(transport: Transport, options?: {
|
|
12
|
+
readonly batch?: boolean;
|
|
13
|
+
}): StorageEngine;
|
|
14
|
+
/**
|
|
15
|
+
* Whether sending `frame` a second time, after the first may have been carried out, returns the same as the
|
|
16
|
+
* first would have and stores nothing more: reads, an unconditional put, a delete, a part sent again.
|
|
17
|
+
* A conditional put, a copy (its source may have changed since), and starting, completing or aborting
|
|
18
|
+
* an upload are not: a second `ifNoneMatch` put is a `conflict`, a second completion `not_found`. A
|
|
19
|
+
* batch is when all it holds is and no call in it writes what another call reads or writes, so sending
|
|
20
|
+
* it again cannot see one call's effect in another's answer. A transport resends a frame it lost the
|
|
21
|
+
* answer to only when this allows it.
|
|
22
|
+
*/
|
|
23
|
+
declare function repeatable(frame: Uint8Array): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The Bucket names that the calls in `frame` use, so a server can look up only those before serving it.
|
|
26
|
+
* A call it cannot read uses none: `serve` returns `invalid` for it without reaching a Bucket.
|
|
27
|
+
*/
|
|
28
|
+
declare function bucketsOf(frame: Uint8Array): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Serves request frames from `engine`. A frame comes from the network, so its header is checked
|
|
31
|
+
* here as the client would check it; a refusal is returned as `invalid`. A failure that is not a
|
|
32
|
+
* `StorageError` is the engine's own fault and is thrown to the transport, never returned.
|
|
33
|
+
*/
|
|
34
|
+
declare function serve(engine: StorageEngine): (frame: Uint8Array) => Promise<Uint8Array>;
|
|
35
|
+
/**
|
|
36
|
+
* A transport over HTTP: each frame is one POST to `url` with the engine's bearer token, replied to
|
|
37
|
+
* by `serveHttp` on the other side. Refused credentials are `forbidden`; a server that cannot be
|
|
38
|
+
* reached, or replies with anything but a frame, is `unavailable`.
|
|
39
|
+
*/
|
|
40
|
+
declare function http(url: string, options: {
|
|
41
|
+
readonly token: string;
|
|
42
|
+
readonly fetch?: (url: string, init: RequestInit) => Promise<Response>;
|
|
43
|
+
}): Transport;
|
|
44
|
+
/** Answers `http` transports: a POST of one frame, with `token` as its bearer token, gets one answer frame. */
|
|
45
|
+
declare function serveHttp(engine: StorageEngine, options: {
|
|
46
|
+
readonly token: string;
|
|
47
|
+
}): (request: Request) => Promise<Response>;
|
|
48
|
+
declare function encodeFrame(header: unknown, body?: Uint8Array): Uint8Array;
|
|
49
|
+
declare function decodeFrame(frame: Uint8Array): {
|
|
50
|
+
header: unknown;
|
|
51
|
+
body: Uint8Array;
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Transport, bucketsOf, decodeFrame, encodeFrame, http, remote, repeatable, serve, serveHttp };
|