@agent-native/creative-context 0.5.4 → 0.5.6
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/embeddings/providers.d.ts +1 -6
- package/dist/embeddings/providers.d.ts.map +1 -1
- package/dist/embeddings/providers.js +1 -175
- package/dist/embeddings/providers.js.map +1 -1
- package/dist/embeddings/types.d.ts +1 -18
- package/dist/embeddings/types.d.ts.map +1 -1
- package/dist/search/fusion.d.ts +1 -14
- package/dist/search/fusion.d.ts.map +1 -1
- package/dist/search/fusion.js +1 -24
- package/dist/search/fusion.js.map +1 -1
- package/dist/search/postgres-fts.d.ts +1 -21
- package/dist/search/postgres-fts.d.ts.map +1 -1
- package/dist/search/postgres-fts.js +1 -82
- package/dist/search/postgres-fts.js.map +1 -1
- package/dist/vector/pgvector.d.ts +1 -27
- package/dist/vector/pgvector.d.ts.map +1 -1
- package/dist/vector/pgvector.js +1 -116
- package/dist/vector/pgvector.js.map +1 -1
- package/package.json +3 -3
- package/src/embeddings/providers.ts +1 -228
- package/src/embeddings/types.ts +6 -24
- package/src/search/fusion.ts +1 -39
- package/src/search/postgres-fts.spec.ts +10 -18
- package/src/search/postgres-fts.ts +1 -112
- package/src/vector/pgvector.spec.ts +19 -2
- package/src/vector/pgvector.ts +1 -164
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function createGeminiEmbeddingFamily(apiKey: string, dimensions?: number): EmbeddingFamily;
|
|
3
|
-
export declare function createCohereEmbeddingFamily(apiKey: string, dimensions?: number): EmbeddingFamily;
|
|
4
|
-
export declare function createVoyageEmbeddingFamily(apiKey: string): EmbeddingFamily;
|
|
5
|
-
export declare function availableEmbeddingFamilies(): Promise<EmbeddingFamily[]>;
|
|
6
|
-
export declare function defaultEmbeddingFamily(families: readonly EmbeddingFamily[]): EmbeddingFamily | null;
|
|
1
|
+
export * from "@agent-native/core/embeddings";
|
|
7
2
|
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/embeddings/providers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/embeddings/providers.ts"],"names":[],"mappings":"AAAA,cAAc,+BAA+B,CAAC"}
|
|
@@ -1,176 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { resolveSecret } from "@agent-native/core/server";
|
|
3
|
-
const DEFAULT_DIMENSIONS = 1024;
|
|
4
|
-
function dataUrl(image) {
|
|
5
|
-
return `data:${image.mimeType};base64,${image.base64}`;
|
|
6
|
-
}
|
|
7
|
-
function normalizedInput(input) {
|
|
8
|
-
const text = input.text?.trim();
|
|
9
|
-
const images = input.images ?? [];
|
|
10
|
-
if (!text && images.length === 0) {
|
|
11
|
-
throw new Error("Embedding input needs text, an image, or both.");
|
|
12
|
-
}
|
|
13
|
-
return { text, images };
|
|
14
|
-
}
|
|
15
|
-
async function postJson(url, headers, body, providerModel) {
|
|
16
|
-
const controller = new AbortController();
|
|
17
|
-
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
18
|
-
try {
|
|
19
|
-
const response = await fetch(url, {
|
|
20
|
-
method: "POST",
|
|
21
|
-
headers: { "Content-Type": "application/json", ...headers },
|
|
22
|
-
body: JSON.stringify(body),
|
|
23
|
-
signal: controller.signal,
|
|
24
|
-
});
|
|
25
|
-
if (!response.ok) {
|
|
26
|
-
throw new Error(`Embedding provider ${providerModel} failed with status ${response.status}.`);
|
|
27
|
-
}
|
|
28
|
-
const bytes = await readBoundedResponseBytes(response, 1_000_000);
|
|
29
|
-
return JSON.parse(new TextDecoder().decode(bytes));
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
if (controller.signal.aborted) {
|
|
33
|
-
throw new Error(`Embedding provider ${providerModel} timed out.`);
|
|
34
|
-
}
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
finally {
|
|
38
|
-
clearTimeout(timeout);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function numberVectors(value) {
|
|
42
|
-
if (!Array.isArray(value))
|
|
43
|
-
throw new Error("Embedding response was malformed.");
|
|
44
|
-
return value.map((vector) => {
|
|
45
|
-
if (!Array.isArray(vector) ||
|
|
46
|
-
vector.some((entry) => !Number.isFinite(entry))) {
|
|
47
|
-
throw new Error("Embedding response contained an invalid vector.");
|
|
48
|
-
}
|
|
49
|
-
return vector.map(Number);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
export function createGeminiEmbeddingFamily(apiKey, dimensions = DEFAULT_DIMENSIONS) {
|
|
53
|
-
const model = "gemini-embedding-2";
|
|
54
|
-
return {
|
|
55
|
-
id: `gemini:${model}:${dimensions}`,
|
|
56
|
-
provider: "gemini",
|
|
57
|
-
model,
|
|
58
|
-
version: "stable-2026-04",
|
|
59
|
-
dimensions,
|
|
60
|
-
supportedImageMimeTypes: ["image/png", "image/jpeg"],
|
|
61
|
-
async embed(inputs, purpose) {
|
|
62
|
-
const vectors = [];
|
|
63
|
-
for (const raw of inputs) {
|
|
64
|
-
const input = normalizedInput(raw);
|
|
65
|
-
const instruction = purpose === "query"
|
|
66
|
-
? "task: search result | query:"
|
|
67
|
-
: "title: none | text:";
|
|
68
|
-
const parts = [
|
|
69
|
-
{ text: `${instruction} ${input.text ?? ""}`.trim() },
|
|
70
|
-
...input.images.map((image) => ({
|
|
71
|
-
inlineData: { mimeType: image.mimeType, data: image.base64 },
|
|
72
|
-
})),
|
|
73
|
-
];
|
|
74
|
-
const result = await postJson(`https://generativelanguage.googleapis.com/v1beta/models/${model}:embedContent`, { "x-goog-api-key": apiKey }, {
|
|
75
|
-
content: { parts },
|
|
76
|
-
output_dimensionality: dimensions,
|
|
77
|
-
}, `gemini/${model}`);
|
|
78
|
-
const embedding = result.embedding;
|
|
79
|
-
vectors.push(...numberVectors([embedding?.values]));
|
|
80
|
-
}
|
|
81
|
-
return vectors;
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
export function createCohereEmbeddingFamily(apiKey, dimensions = DEFAULT_DIMENSIONS) {
|
|
86
|
-
const model = "embed-v4.0";
|
|
87
|
-
return {
|
|
88
|
-
id: `cohere:${model}:${dimensions}`,
|
|
89
|
-
provider: "cohere",
|
|
90
|
-
model,
|
|
91
|
-
version: "v4.0",
|
|
92
|
-
dimensions,
|
|
93
|
-
supportedImageMimeTypes: [
|
|
94
|
-
"image/png",
|
|
95
|
-
"image/jpeg",
|
|
96
|
-
"image/webp",
|
|
97
|
-
"image/gif",
|
|
98
|
-
],
|
|
99
|
-
async embed(inputs, purpose) {
|
|
100
|
-
const contentInputs = inputs.map((raw) => {
|
|
101
|
-
const input = normalizedInput(raw);
|
|
102
|
-
return {
|
|
103
|
-
content: [
|
|
104
|
-
...(input.text ? [{ type: "text", text: input.text }] : []),
|
|
105
|
-
...input.images.map((image) => ({
|
|
106
|
-
type: "image_url",
|
|
107
|
-
image_url: { url: dataUrl(image) },
|
|
108
|
-
})),
|
|
109
|
-
],
|
|
110
|
-
};
|
|
111
|
-
});
|
|
112
|
-
const result = await postJson("https://api.cohere.com/v2/embed", { Authorization: `Bearer ${apiKey}` }, {
|
|
113
|
-
model,
|
|
114
|
-
inputs: contentInputs,
|
|
115
|
-
input_type: purpose === "query" ? "search_query" : "search_document",
|
|
116
|
-
embedding_types: ["float"],
|
|
117
|
-
output_dimension: dimensions,
|
|
118
|
-
}, `cohere/${model}`);
|
|
119
|
-
const embeddings = result.embeddings;
|
|
120
|
-
return numberVectors(embeddings?.float ?? embeddings?.float_);
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
export function createVoyageEmbeddingFamily(apiKey) {
|
|
125
|
-
const model = "voyage-multimodal-3.5";
|
|
126
|
-
return {
|
|
127
|
-
id: `voyage:${model}:1024`,
|
|
128
|
-
provider: "voyage",
|
|
129
|
-
model,
|
|
130
|
-
version: "3.5",
|
|
131
|
-
dimensions: 1024,
|
|
132
|
-
supportedImageMimeTypes: [
|
|
133
|
-
"image/png",
|
|
134
|
-
"image/jpeg",
|
|
135
|
-
"image/webp",
|
|
136
|
-
"image/gif",
|
|
137
|
-
],
|
|
138
|
-
async embed(inputs, purpose) {
|
|
139
|
-
const voyageInputs = inputs.map((raw) => {
|
|
140
|
-
const input = normalizedInput(raw);
|
|
141
|
-
return {
|
|
142
|
-
content: [
|
|
143
|
-
...(input.text ? [{ type: "text", text: input.text }] : []),
|
|
144
|
-
...input.images.map((image) => ({
|
|
145
|
-
type: "image_base64",
|
|
146
|
-
image_base64: dataUrl(image),
|
|
147
|
-
})),
|
|
148
|
-
],
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
const result = await postJson("https://api.voyageai.com/v1/multimodalembeddings", { Authorization: `Bearer ${apiKey}` }, {
|
|
152
|
-
model,
|
|
153
|
-
inputs: voyageInputs,
|
|
154
|
-
input_type: purpose,
|
|
155
|
-
truncation: true,
|
|
156
|
-
}, `voyage/${model}`);
|
|
157
|
-
return numberVectors(result.embeddings);
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
export async function availableEmbeddingFamilies() {
|
|
162
|
-
const [gemini, cohere, voyage] = await Promise.all([
|
|
163
|
-
resolveSecret("GEMINI_API_KEY").catch(() => null),
|
|
164
|
-
resolveSecret("COHERE_API_KEY").catch(() => null),
|
|
165
|
-
resolveSecret("VOYAGE_API_KEY").catch(() => null),
|
|
166
|
-
]);
|
|
167
|
-
return [
|
|
168
|
-
...(gemini ? [createGeminiEmbeddingFamily(gemini)] : []),
|
|
169
|
-
...(cohere ? [createCohereEmbeddingFamily(cohere)] : []),
|
|
170
|
-
...(voyage ? [createVoyageEmbeddingFamily(voyage)] : []),
|
|
171
|
-
];
|
|
172
|
-
}
|
|
173
|
-
export function defaultEmbeddingFamily(families) {
|
|
174
|
-
return families.length === 1 ? (families[0] ?? null) : null;
|
|
175
|
-
}
|
|
1
|
+
export * from "@agent-native/core/embeddings";
|
|
176
2
|
//# sourceMappingURL=providers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/embeddings/providers.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/embeddings/providers.ts"],"names":[],"mappings":"AAAA,cAAc,+BAA+B,CAAC"}
|
|
@@ -1,19 +1,2 @@
|
|
|
1
|
-
export type EmbeddingInputPurpose
|
|
2
|
-
export interface EmbeddingImageInput {
|
|
3
|
-
mimeType: "image/png" | "image/jpeg" | "image/webp" | "image/gif";
|
|
4
|
-
base64: string;
|
|
5
|
-
}
|
|
6
|
-
export interface MultimodalEmbeddingInput {
|
|
7
|
-
text?: string;
|
|
8
|
-
images?: EmbeddingImageInput[];
|
|
9
|
-
}
|
|
10
|
-
export interface EmbeddingFamily {
|
|
11
|
-
id: string;
|
|
12
|
-
provider: "gemini" | "cohere" | "voyage" | (string & {});
|
|
13
|
-
model: string;
|
|
14
|
-
version: string;
|
|
15
|
-
dimensions: number;
|
|
16
|
-
supportedImageMimeTypes?: readonly EmbeddingImageInput["mimeType"][];
|
|
17
|
-
embed(inputs: readonly MultimodalEmbeddingInput[], purpose: EmbeddingInputPurpose): Promise<number[][]>;
|
|
18
|
-
}
|
|
1
|
+
export type { EmbeddingFamily, EmbeddingImageInput, EmbeddingInputPurpose, MultimodalEmbeddingInput, } from "@agent-native/core/embeddings";
|
|
19
2
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/embeddings/types.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/embeddings/types.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC"}
|
package/dist/search/fusion.d.ts
CHANGED
|
@@ -1,15 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
key: string;
|
|
3
|
-
value: T;
|
|
4
|
-
score: number;
|
|
5
|
-
reason?: string;
|
|
6
|
-
}
|
|
7
|
-
export interface FusedCandidate<T> extends RankedCandidate<T> {
|
|
8
|
-
laneRanks: Record<string, number>;
|
|
9
|
-
reasons: string[];
|
|
10
|
-
}
|
|
11
|
-
export declare function reciprocalRankFusion<T>(lanes: Readonly<Record<string, readonly RankedCandidate<T>[]>>, options?: {
|
|
12
|
-
rankConstant?: number;
|
|
13
|
-
limit?: number;
|
|
14
|
-
}): FusedCandidate<T>[];
|
|
1
|
+
export * from "@agent-native/core/search";
|
|
15
2
|
//# sourceMappingURL=fusion.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fusion.d.ts","sourceRoot":"","sources":["../../src/search/fusion.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"fusion.d.ts","sourceRoot":"","sources":["../../src/search/fusion.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC"}
|
package/dist/search/fusion.js
CHANGED
|
@@ -1,25 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
const rankConstant = Math.max(1, options.rankConstant ?? 60);
|
|
3
|
-
const fused = new Map();
|
|
4
|
-
for (const [lane, candidates] of Object.entries(lanes)) {
|
|
5
|
-
candidates.forEach((candidate, index) => {
|
|
6
|
-
const rank = index + 1;
|
|
7
|
-
const current = fused.get(candidate.key) ?? {
|
|
8
|
-
...candidate,
|
|
9
|
-
score: 0,
|
|
10
|
-
laneRanks: {},
|
|
11
|
-
reasons: [],
|
|
12
|
-
};
|
|
13
|
-
current.score += 1 / (rankConstant + rank);
|
|
14
|
-
current.laneRanks[lane] = rank;
|
|
15
|
-
if (candidate.reason && !current.reasons.includes(candidate.reason)) {
|
|
16
|
-
current.reasons.push(candidate.reason);
|
|
17
|
-
}
|
|
18
|
-
fused.set(candidate.key, current);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
return [...fused.values()]
|
|
22
|
-
.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key))
|
|
23
|
-
.slice(0, Math.max(1, options.limit ?? 40));
|
|
24
|
-
}
|
|
1
|
+
export * from "@agent-native/core/search";
|
|
25
2
|
//# sourceMappingURL=fusion.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fusion.js","sourceRoot":"","sources":["../../src/search/fusion.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fusion.js","sourceRoot":"","sources":["../../src/search/fusion.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC"}
|
|
@@ -1,22 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export interface PostgresFtsHit {
|
|
3
|
-
chunkId: string;
|
|
4
|
-
itemVersionId: string;
|
|
5
|
-
score: number;
|
|
6
|
-
}
|
|
7
|
-
export declare function ensurePostgresFts(db: DbExec): Promise<boolean>;
|
|
8
|
-
export declare function upsertPostgresFtsDocument(db: DbExec, input: {
|
|
9
|
-
chunkId: string;
|
|
10
|
-
itemVersionId: string;
|
|
11
|
-
title: string;
|
|
12
|
-
summary?: string | null;
|
|
13
|
-
body: string;
|
|
14
|
-
updatedAt?: string;
|
|
15
|
-
}): Promise<boolean>;
|
|
16
|
-
export declare function queryPostgresFts(db: DbExec, input: {
|
|
17
|
-
query: string;
|
|
18
|
-
allowedChunkIds?: readonly string[];
|
|
19
|
-
limit?: number;
|
|
20
|
-
}): Promise<PostgresFtsHit[]>;
|
|
21
|
-
export declare function deletePostgresFtsDocuments(db: DbExec, chunkIds: readonly string[]): Promise<number>;
|
|
1
|
+
export * from "@agent-native/core/search";
|
|
22
2
|
//# sourceMappingURL=postgres-fts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-fts.d.ts","sourceRoot":"","sources":["../../src/search/postgres-fts.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"postgres-fts.d.ts","sourceRoot":"","sources":["../../src/search/postgres-fts.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC"}
|
|
@@ -1,83 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
const FTS_TABLE = "creative_context_search_documents";
|
|
3
|
-
export async function ensurePostgresFts(db) {
|
|
4
|
-
if (!isPostgres())
|
|
5
|
-
return false;
|
|
6
|
-
await db.execute(`
|
|
7
|
-
CREATE TABLE IF NOT EXISTS ${FTS_TABLE} (
|
|
8
|
-
chunk_id TEXT PRIMARY KEY,
|
|
9
|
-
item_version_id TEXT NOT NULL,
|
|
10
|
-
document TSVECTOR NOT NULL,
|
|
11
|
-
updated_at TEXT NOT NULL
|
|
12
|
-
)
|
|
13
|
-
`);
|
|
14
|
-
await db.execute(`CREATE INDEX IF NOT EXISTS creative_context_search_documents_gin ON ${FTS_TABLE} USING GIN (document)`);
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
export async function upsertPostgresFtsDocument(db, input) {
|
|
18
|
-
if (!(await ensurePostgresFts(db)))
|
|
19
|
-
return false;
|
|
20
|
-
await db.execute({
|
|
21
|
-
sql: `
|
|
22
|
-
INSERT INTO ${FTS_TABLE} (chunk_id, item_version_id, document, updated_at)
|
|
23
|
-
VALUES (
|
|
24
|
-
?,
|
|
25
|
-
?,
|
|
26
|
-
setweight(to_tsvector('simple', ?), 'A') ||
|
|
27
|
-
setweight(to_tsvector('simple', ?), 'B') ||
|
|
28
|
-
setweight(to_tsvector('simple', ?), 'C'),
|
|
29
|
-
?
|
|
30
|
-
)
|
|
31
|
-
ON CONFLICT (chunk_id) DO UPDATE SET
|
|
32
|
-
item_version_id = EXCLUDED.item_version_id,
|
|
33
|
-
document = EXCLUDED.document,
|
|
34
|
-
updated_at = EXCLUDED.updated_at
|
|
35
|
-
`,
|
|
36
|
-
args: [
|
|
37
|
-
input.chunkId,
|
|
38
|
-
input.itemVersionId,
|
|
39
|
-
input.title,
|
|
40
|
-
input.summary ?? "",
|
|
41
|
-
input.body,
|
|
42
|
-
input.updatedAt ?? new Date().toISOString(),
|
|
43
|
-
],
|
|
44
|
-
});
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
export async function queryPostgresFts(db, input) {
|
|
48
|
-
if (!isPostgres() || input.allowedChunkIds?.length === 0)
|
|
49
|
-
return [];
|
|
50
|
-
await ensurePostgresFts(db);
|
|
51
|
-
const placeholders = input.allowedChunkIds?.map(() => "?").join(", ");
|
|
52
|
-
const accessClause = placeholders ? `chunk_id IN (${placeholders}) AND` : "";
|
|
53
|
-
const limit = Math.max(1, Math.min(200, Math.floor(input.limit ?? 40)));
|
|
54
|
-
const result = await db.execute({
|
|
55
|
-
sql: `
|
|
56
|
-
SELECT chunk_id, item_version_id,
|
|
57
|
-
ts_rank_cd(document, websearch_to_tsquery('simple', ?)) AS score
|
|
58
|
-
FROM ${FTS_TABLE}
|
|
59
|
-
WHERE ${accessClause}
|
|
60
|
-
document @@ websearch_to_tsquery('simple', ?)
|
|
61
|
-
ORDER BY score DESC, chunk_id ASC
|
|
62
|
-
LIMIT ?
|
|
63
|
-
`,
|
|
64
|
-
args: [input.query, ...(input.allowedChunkIds ?? []), input.query, limit],
|
|
65
|
-
});
|
|
66
|
-
return result.rows.map((row) => ({
|
|
67
|
-
chunkId: String(row.chunk_id),
|
|
68
|
-
itemVersionId: String(row.item_version_id),
|
|
69
|
-
score: Number(row.score),
|
|
70
|
-
}));
|
|
71
|
-
}
|
|
72
|
-
export async function deletePostgresFtsDocuments(db, chunkIds) {
|
|
73
|
-
if (!isPostgres() || chunkIds.length === 0)
|
|
74
|
-
return 0;
|
|
75
|
-
await ensurePostgresFts(db);
|
|
76
|
-
const placeholders = chunkIds.map(() => "?").join(", ");
|
|
77
|
-
const result = await db.execute({
|
|
78
|
-
sql: `DELETE FROM ${FTS_TABLE} WHERE chunk_id IN (${placeholders})`,
|
|
79
|
-
args: [...chunkIds],
|
|
80
|
-
});
|
|
81
|
-
return result.rowsAffected;
|
|
82
|
-
}
|
|
1
|
+
export * from "@agent-native/core/search";
|
|
83
2
|
//# sourceMappingURL=postgres-fts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-fts.js","sourceRoot":"","sources":["../../src/search/postgres-fts.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"postgres-fts.js","sourceRoot":"","sources":["../../src/search/postgres-fts.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC"}
|
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const PGVECTOR_REQUIRED_MESSAGE = "Visual search requires Postgres with the pgvector extension in the configured DATABASE_URL database.";
|
|
3
|
-
export interface PgVectorHit {
|
|
4
|
-
vectorKey: string;
|
|
5
|
-
embeddingSetId: string;
|
|
6
|
-
score: number;
|
|
7
|
-
}
|
|
8
|
-
export declare function assertPgVectorAvailable(postgres?: boolean): void;
|
|
9
|
-
export declare function ensurePgVectorIndex(db: DbExec, dimensions: number, postgres?: boolean): Promise<void>;
|
|
10
|
-
export declare function upsertPgVector(db: DbExec, input: {
|
|
11
|
-
vectorKey: string;
|
|
12
|
-
embeddingSetId: string;
|
|
13
|
-
dimensions: number;
|
|
14
|
-
vector: readonly number[];
|
|
15
|
-
updatedAt?: string;
|
|
16
|
-
}, postgres?: boolean): Promise<void>;
|
|
17
|
-
export declare function deletePgVectors(db: DbExec, input: {
|
|
18
|
-
dimensions: number;
|
|
19
|
-
vectorKeys: readonly string[];
|
|
20
|
-
}, postgres?: boolean): Promise<number>;
|
|
21
|
-
export declare function queryPgVectorIndex(db: DbExec, input: {
|
|
22
|
-
embeddingSetId: string;
|
|
23
|
-
dimensions: number;
|
|
24
|
-
vector: readonly number[];
|
|
25
|
-
limit?: number;
|
|
26
|
-
allowedVectorKeys: readonly string[];
|
|
27
|
-
}, postgres?: boolean): Promise<PgVectorHit[]>;
|
|
1
|
+
export * from "@agent-native/core/search";
|
|
28
2
|
//# sourceMappingURL=pgvector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pgvector.d.ts","sourceRoot":"","sources":["../../src/vector/pgvector.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"pgvector.d.ts","sourceRoot":"","sources":["../../src/vector/pgvector.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC"}
|
package/dist/vector/pgvector.js
CHANGED
|
@@ -1,117 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export const PGVECTOR_REQUIRED_MESSAGE = "Visual search requires Postgres with the pgvector extension in the configured DATABASE_URL database.";
|
|
3
|
-
function checkedDimensions(value) {
|
|
4
|
-
if (!Number.isInteger(value) || value < 1 || value > 16_000) {
|
|
5
|
-
throw new Error("Embedding dimensions must be an integer from 1 to 16000.");
|
|
6
|
-
}
|
|
7
|
-
return value;
|
|
8
|
-
}
|
|
9
|
-
function vectorTable(dimensions) {
|
|
10
|
-
return `creative_context_vectors_${checkedDimensions(dimensions)}`;
|
|
11
|
-
}
|
|
12
|
-
function vectorIndex(dimensions) {
|
|
13
|
-
return `creative_context_vectors_${checkedDimensions(dimensions)}_hnsw`;
|
|
14
|
-
}
|
|
15
|
-
function vectorLiteral(vector, dimensions) {
|
|
16
|
-
if (vector.length !== dimensions) {
|
|
17
|
-
throw new Error(`Embedding has ${vector.length} values; expected ${dimensions}.`);
|
|
18
|
-
}
|
|
19
|
-
for (const value of vector) {
|
|
20
|
-
if (!Number.isFinite(value)) {
|
|
21
|
-
throw new Error("Embedding vectors may contain only finite numbers.");
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return `[${vector.join(",")}]`;
|
|
25
|
-
}
|
|
26
|
-
export function assertPgVectorAvailable(postgres = isPostgres()) {
|
|
27
|
-
if (!postgres)
|
|
28
|
-
throw new Error(PGVECTOR_REQUIRED_MESSAGE);
|
|
29
|
-
}
|
|
30
|
-
export async function ensurePgVectorIndex(db, dimensions, postgres = isPostgres()) {
|
|
31
|
-
assertPgVectorAvailable(postgres);
|
|
32
|
-
const table = vectorTable(dimensions);
|
|
33
|
-
const index = vectorIndex(dimensions);
|
|
34
|
-
try {
|
|
35
|
-
await db.execute("CREATE EXTENSION IF NOT EXISTS vector");
|
|
36
|
-
await db.execute(`
|
|
37
|
-
CREATE TABLE IF NOT EXISTS ${table} (
|
|
38
|
-
vector_key TEXT PRIMARY KEY,
|
|
39
|
-
embedding_set_id TEXT NOT NULL,
|
|
40
|
-
embedding vector(${checkedDimensions(dimensions)}) NOT NULL,
|
|
41
|
-
updated_at TEXT NOT NULL
|
|
42
|
-
)
|
|
43
|
-
`);
|
|
44
|
-
await db.execute(`CREATE INDEX IF NOT EXISTS ${index} ON ${table} USING hnsw (embedding vector_cosine_ops)`);
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
throw new Error(PGVECTOR_REQUIRED_MESSAGE, { cause: error });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export async function upsertPgVector(db, input, postgres = isPostgres()) {
|
|
51
|
-
assertPgVectorAvailable(postgres);
|
|
52
|
-
await ensurePgVectorIndex(db, input.dimensions, postgres);
|
|
53
|
-
const table = vectorTable(input.dimensions);
|
|
54
|
-
await db.execute({
|
|
55
|
-
sql: `
|
|
56
|
-
INSERT INTO ${table} (vector_key, embedding_set_id, embedding, updated_at)
|
|
57
|
-
VALUES (?, ?, ?::vector, ?)
|
|
58
|
-
ON CONFLICT (vector_key) DO UPDATE SET
|
|
59
|
-
embedding_set_id = EXCLUDED.embedding_set_id,
|
|
60
|
-
embedding = EXCLUDED.embedding,
|
|
61
|
-
updated_at = EXCLUDED.updated_at
|
|
62
|
-
`,
|
|
63
|
-
args: [
|
|
64
|
-
input.vectorKey,
|
|
65
|
-
input.embeddingSetId,
|
|
66
|
-
vectorLiteral(input.vector, input.dimensions),
|
|
67
|
-
input.updatedAt ?? new Date().toISOString(),
|
|
68
|
-
],
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
export async function deletePgVectors(db, input, postgres = isPostgres()) {
|
|
72
|
-
assertPgVectorAvailable(postgres);
|
|
73
|
-
if (input.vectorKeys.length === 0)
|
|
74
|
-
return 0;
|
|
75
|
-
const table = vectorTable(input.dimensions);
|
|
76
|
-
const placeholders = input.vectorKeys.map(() => "?").join(", ");
|
|
77
|
-
const result = await db.execute({
|
|
78
|
-
sql: `DELETE FROM ${table} WHERE vector_key IN (${placeholders})`,
|
|
79
|
-
args: [...input.vectorKeys],
|
|
80
|
-
});
|
|
81
|
-
return result.rowsAffected;
|
|
82
|
-
}
|
|
83
|
-
export async function queryPgVectorIndex(db, input, postgres = isPostgres()) {
|
|
84
|
-
assertPgVectorAvailable(postgres);
|
|
85
|
-
const limit = Math.max(1, Math.min(200, Math.floor(input.limit ?? 40)));
|
|
86
|
-
const allowed = input.allowedVectorKeys;
|
|
87
|
-
if (allowed.length === 0)
|
|
88
|
-
return [];
|
|
89
|
-
const table = vectorTable(input.dimensions);
|
|
90
|
-
const literal = vectorLiteral(input.vector, input.dimensions);
|
|
91
|
-
const allowedSql = allowed.length
|
|
92
|
-
? ` AND vector_key IN (${allowed.map(() => "?").join(", ")})`
|
|
93
|
-
: "";
|
|
94
|
-
try {
|
|
95
|
-
await db.execute("SET hnsw.iterative_scan = strict_order");
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
// pgvector before iterative scans still supports the filtered exact query.
|
|
99
|
-
}
|
|
100
|
-
const result = await db.execute({
|
|
101
|
-
sql: `
|
|
102
|
-
SELECT vector_key, embedding_set_id,
|
|
103
|
-
1 - (embedding <=> ?::vector) AS score
|
|
104
|
-
FROM ${table}
|
|
105
|
-
WHERE embedding_set_id = ?${allowedSql}
|
|
106
|
-
ORDER BY embedding <=> ?::vector
|
|
107
|
-
LIMIT ?
|
|
108
|
-
`,
|
|
109
|
-
args: [literal, input.embeddingSetId, ...allowed, literal, limit],
|
|
110
|
-
});
|
|
111
|
-
return result.rows.map((row) => ({
|
|
112
|
-
vectorKey: String(row.vector_key),
|
|
113
|
-
embeddingSetId: String(row.embedding_set_id),
|
|
114
|
-
score: Number(row.score),
|
|
115
|
-
}));
|
|
116
|
-
}
|
|
1
|
+
export * from "@agent-native/core/search";
|
|
117
2
|
//# sourceMappingURL=pgvector.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pgvector.js","sourceRoot":"","sources":["../../src/vector/pgvector.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pgvector.js","sourceRoot":"","sources":["../../src/vector/pgvector.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/creative-context",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Shared creative context ingestion, versioning, search, brand DNA, and context-pack primitives for agent-native apps.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"react-dom": "^19.2.7",
|
|
55
55
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
56
56
|
"vitest": "^4.1.5",
|
|
57
|
-
"@agent-native/
|
|
58
|
-
"@agent-native/
|
|
57
|
+
"@agent-native/core": "0.113.0",
|
|
58
|
+
"@agent-native/toolkit": "0.7.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -1,228 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { resolveSecret } from "@agent-native/core/server";
|
|
3
|
-
|
|
4
|
-
import type {
|
|
5
|
-
EmbeddingFamily,
|
|
6
|
-
EmbeddingImageInput,
|
|
7
|
-
EmbeddingInputPurpose,
|
|
8
|
-
MultimodalEmbeddingInput,
|
|
9
|
-
} from "./types.js";
|
|
10
|
-
|
|
11
|
-
const DEFAULT_DIMENSIONS = 1024;
|
|
12
|
-
|
|
13
|
-
function dataUrl(image: EmbeddingImageInput): string {
|
|
14
|
-
return `data:${image.mimeType};base64,${image.base64}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizedInput(input: MultimodalEmbeddingInput) {
|
|
18
|
-
const text = input.text?.trim();
|
|
19
|
-
const images = input.images ?? [];
|
|
20
|
-
if (!text && images.length === 0) {
|
|
21
|
-
throw new Error("Embedding input needs text, an image, or both.");
|
|
22
|
-
}
|
|
23
|
-
return { text, images };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function postJson(
|
|
27
|
-
url: string,
|
|
28
|
-
headers: Record<string, string>,
|
|
29
|
-
body: unknown,
|
|
30
|
-
providerModel: string,
|
|
31
|
-
): Promise<Record<string, unknown>> {
|
|
32
|
-
const controller = new AbortController();
|
|
33
|
-
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
34
|
-
try {
|
|
35
|
-
const response = await fetch(url, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: { "Content-Type": "application/json", ...headers },
|
|
38
|
-
body: JSON.stringify(body),
|
|
39
|
-
signal: controller.signal,
|
|
40
|
-
});
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
`Embedding provider ${providerModel} failed with status ${response.status}.`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
const bytes = await readBoundedResponseBytes(response, 1_000_000);
|
|
47
|
-
return JSON.parse(new TextDecoder().decode(bytes)) as Record<
|
|
48
|
-
string,
|
|
49
|
-
unknown
|
|
50
|
-
>;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (controller.signal.aborted) {
|
|
53
|
-
throw new Error(`Embedding provider ${providerModel} timed out.`);
|
|
54
|
-
}
|
|
55
|
-
throw error;
|
|
56
|
-
} finally {
|
|
57
|
-
clearTimeout(timeout);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function numberVectors(value: unknown): number[][] {
|
|
62
|
-
if (!Array.isArray(value))
|
|
63
|
-
throw new Error("Embedding response was malformed.");
|
|
64
|
-
return value.map((vector) => {
|
|
65
|
-
if (
|
|
66
|
-
!Array.isArray(vector) ||
|
|
67
|
-
vector.some((entry) => !Number.isFinite(entry))
|
|
68
|
-
) {
|
|
69
|
-
throw new Error("Embedding response contained an invalid vector.");
|
|
70
|
-
}
|
|
71
|
-
return vector.map(Number);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function createGeminiEmbeddingFamily(
|
|
76
|
-
apiKey: string,
|
|
77
|
-
dimensions = DEFAULT_DIMENSIONS,
|
|
78
|
-
): EmbeddingFamily {
|
|
79
|
-
const model = "gemini-embedding-2";
|
|
80
|
-
return {
|
|
81
|
-
id: `gemini:${model}:${dimensions}`,
|
|
82
|
-
provider: "gemini",
|
|
83
|
-
model,
|
|
84
|
-
version: "stable-2026-04",
|
|
85
|
-
dimensions,
|
|
86
|
-
supportedImageMimeTypes: ["image/png", "image/jpeg"],
|
|
87
|
-
async embed(inputs, purpose) {
|
|
88
|
-
const vectors: number[][] = [];
|
|
89
|
-
for (const raw of inputs) {
|
|
90
|
-
const input = normalizedInput(raw);
|
|
91
|
-
const instruction =
|
|
92
|
-
purpose === "query"
|
|
93
|
-
? "task: search result | query:"
|
|
94
|
-
: "title: none | text:";
|
|
95
|
-
const parts: Record<string, unknown>[] = [
|
|
96
|
-
{ text: `${instruction} ${input.text ?? ""}`.trim() },
|
|
97
|
-
...input.images.map((image) => ({
|
|
98
|
-
inlineData: { mimeType: image.mimeType, data: image.base64 },
|
|
99
|
-
})),
|
|
100
|
-
];
|
|
101
|
-
const result = await postJson(
|
|
102
|
-
`https://generativelanguage.googleapis.com/v1beta/models/${model}:embedContent`,
|
|
103
|
-
{ "x-goog-api-key": apiKey },
|
|
104
|
-
{
|
|
105
|
-
content: { parts },
|
|
106
|
-
output_dimensionality: dimensions,
|
|
107
|
-
},
|
|
108
|
-
`gemini/${model}`,
|
|
109
|
-
);
|
|
110
|
-
const embedding = result.embedding as { values?: unknown } | undefined;
|
|
111
|
-
vectors.push(...numberVectors([embedding?.values]));
|
|
112
|
-
}
|
|
113
|
-
return vectors;
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function createCohereEmbeddingFamily(
|
|
119
|
-
apiKey: string,
|
|
120
|
-
dimensions = DEFAULT_DIMENSIONS,
|
|
121
|
-
): EmbeddingFamily {
|
|
122
|
-
const model = "embed-v4.0";
|
|
123
|
-
return {
|
|
124
|
-
id: `cohere:${model}:${dimensions}`,
|
|
125
|
-
provider: "cohere",
|
|
126
|
-
model,
|
|
127
|
-
version: "v4.0",
|
|
128
|
-
dimensions,
|
|
129
|
-
supportedImageMimeTypes: [
|
|
130
|
-
"image/png",
|
|
131
|
-
"image/jpeg",
|
|
132
|
-
"image/webp",
|
|
133
|
-
"image/gif",
|
|
134
|
-
],
|
|
135
|
-
async embed(inputs, purpose) {
|
|
136
|
-
const contentInputs = inputs.map((raw) => {
|
|
137
|
-
const input = normalizedInput(raw);
|
|
138
|
-
return {
|
|
139
|
-
content: [
|
|
140
|
-
...(input.text ? [{ type: "text", text: input.text }] : []),
|
|
141
|
-
...input.images.map((image) => ({
|
|
142
|
-
type: "image_url",
|
|
143
|
-
image_url: { url: dataUrl(image) },
|
|
144
|
-
})),
|
|
145
|
-
],
|
|
146
|
-
};
|
|
147
|
-
});
|
|
148
|
-
const result = await postJson(
|
|
149
|
-
"https://api.cohere.com/v2/embed",
|
|
150
|
-
{ Authorization: `Bearer ${apiKey}` },
|
|
151
|
-
{
|
|
152
|
-
model,
|
|
153
|
-
inputs: contentInputs,
|
|
154
|
-
input_type: purpose === "query" ? "search_query" : "search_document",
|
|
155
|
-
embedding_types: ["float"],
|
|
156
|
-
output_dimension: dimensions,
|
|
157
|
-
},
|
|
158
|
-
`cohere/${model}`,
|
|
159
|
-
);
|
|
160
|
-
const embeddings = result.embeddings as
|
|
161
|
-
| { float?: unknown; float_?: unknown }
|
|
162
|
-
| undefined;
|
|
163
|
-
return numberVectors(embeddings?.float ?? embeddings?.float_);
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function createVoyageEmbeddingFamily(apiKey: string): EmbeddingFamily {
|
|
169
|
-
const model = "voyage-multimodal-3.5";
|
|
170
|
-
return {
|
|
171
|
-
id: `voyage:${model}:1024`,
|
|
172
|
-
provider: "voyage",
|
|
173
|
-
model,
|
|
174
|
-
version: "3.5",
|
|
175
|
-
dimensions: 1024,
|
|
176
|
-
supportedImageMimeTypes: [
|
|
177
|
-
"image/png",
|
|
178
|
-
"image/jpeg",
|
|
179
|
-
"image/webp",
|
|
180
|
-
"image/gif",
|
|
181
|
-
],
|
|
182
|
-
async embed(inputs, purpose) {
|
|
183
|
-
const voyageInputs = inputs.map((raw) => {
|
|
184
|
-
const input = normalizedInput(raw);
|
|
185
|
-
return {
|
|
186
|
-
content: [
|
|
187
|
-
...(input.text ? [{ type: "text", text: input.text }] : []),
|
|
188
|
-
...input.images.map((image) => ({
|
|
189
|
-
type: "image_base64",
|
|
190
|
-
image_base64: dataUrl(image),
|
|
191
|
-
})),
|
|
192
|
-
],
|
|
193
|
-
};
|
|
194
|
-
});
|
|
195
|
-
const result = await postJson(
|
|
196
|
-
"https://api.voyageai.com/v1/multimodalembeddings",
|
|
197
|
-
{ Authorization: `Bearer ${apiKey}` },
|
|
198
|
-
{
|
|
199
|
-
model,
|
|
200
|
-
inputs: voyageInputs,
|
|
201
|
-
input_type: purpose,
|
|
202
|
-
truncation: true,
|
|
203
|
-
},
|
|
204
|
-
`voyage/${model}`,
|
|
205
|
-
);
|
|
206
|
-
return numberVectors(result.embeddings);
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export async function availableEmbeddingFamilies(): Promise<EmbeddingFamily[]> {
|
|
212
|
-
const [gemini, cohere, voyage] = await Promise.all([
|
|
213
|
-
resolveSecret("GEMINI_API_KEY").catch(() => null),
|
|
214
|
-
resolveSecret("COHERE_API_KEY").catch(() => null),
|
|
215
|
-
resolveSecret("VOYAGE_API_KEY").catch(() => null),
|
|
216
|
-
]);
|
|
217
|
-
return [
|
|
218
|
-
...(gemini ? [createGeminiEmbeddingFamily(gemini)] : []),
|
|
219
|
-
...(cohere ? [createCohereEmbeddingFamily(cohere)] : []),
|
|
220
|
-
...(voyage ? [createVoyageEmbeddingFamily(voyage)] : []),
|
|
221
|
-
];
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function defaultEmbeddingFamily(
|
|
225
|
-
families: readonly EmbeddingFamily[],
|
|
226
|
-
): EmbeddingFamily | null {
|
|
227
|
-
return families.length === 1 ? (families[0] ?? null) : null;
|
|
228
|
-
}
|
|
1
|
+
export * from "@agent-native/core/embeddings";
|
package/src/embeddings/types.ts
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
export type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface MultimodalEmbeddingInput {
|
|
9
|
-
text?: string;
|
|
10
|
-
images?: EmbeddingImageInput[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface EmbeddingFamily {
|
|
14
|
-
id: string;
|
|
15
|
-
provider: "gemini" | "cohere" | "voyage" | (string & {});
|
|
16
|
-
model: string;
|
|
17
|
-
version: string;
|
|
18
|
-
dimensions: number;
|
|
19
|
-
supportedImageMimeTypes?: readonly EmbeddingImageInput["mimeType"][];
|
|
20
|
-
embed(
|
|
21
|
-
inputs: readonly MultimodalEmbeddingInput[],
|
|
22
|
-
purpose: EmbeddingInputPurpose,
|
|
23
|
-
): Promise<number[][]>;
|
|
24
|
-
}
|
|
1
|
+
export type {
|
|
2
|
+
EmbeddingFamily,
|
|
3
|
+
EmbeddingImageInput,
|
|
4
|
+
EmbeddingInputPurpose,
|
|
5
|
+
MultimodalEmbeddingInput,
|
|
6
|
+
} from "@agent-native/core/embeddings";
|
package/src/search/fusion.ts
CHANGED
|
@@ -1,39 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
key: string;
|
|
3
|
-
value: T;
|
|
4
|
-
score: number;
|
|
5
|
-
reason?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface FusedCandidate<T> extends RankedCandidate<T> {
|
|
9
|
-
laneRanks: Record<string, number>;
|
|
10
|
-
reasons: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function reciprocalRankFusion<T>(
|
|
14
|
-
lanes: Readonly<Record<string, readonly RankedCandidate<T>[]>>,
|
|
15
|
-
options: { rankConstant?: number; limit?: number } = {},
|
|
16
|
-
): FusedCandidate<T>[] {
|
|
17
|
-
const rankConstant = Math.max(1, options.rankConstant ?? 60);
|
|
18
|
-
const fused = new Map<string, FusedCandidate<T>>();
|
|
19
|
-
for (const [lane, candidates] of Object.entries(lanes)) {
|
|
20
|
-
candidates.forEach((candidate, index) => {
|
|
21
|
-
const rank = index + 1;
|
|
22
|
-
const current = fused.get(candidate.key) ?? {
|
|
23
|
-
...candidate,
|
|
24
|
-
score: 0,
|
|
25
|
-
laneRanks: {},
|
|
26
|
-
reasons: [],
|
|
27
|
-
};
|
|
28
|
-
current.score += 1 / (rankConstant + rank);
|
|
29
|
-
current.laneRanks[lane] = rank;
|
|
30
|
-
if (candidate.reason && !current.reasons.includes(candidate.reason)) {
|
|
31
|
-
current.reasons.push(candidate.reason);
|
|
32
|
-
}
|
|
33
|
-
fused.set(candidate.key, current);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
return [...fused.values()]
|
|
37
|
-
.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key))
|
|
38
|
-
.slice(0, Math.max(1, options.limit ?? 40));
|
|
39
|
-
}
|
|
1
|
+
export * from "@agent-native/core/search";
|
|
@@ -21,20 +21,16 @@ describe("Postgres creative-context FTS", () => {
|
|
|
21
21
|
it("filters every Postgres candidate through accessible chunk ids", async () => {
|
|
22
22
|
isPostgres.mockReturnValue(true);
|
|
23
23
|
const db = {
|
|
24
|
-
execute: vi
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.mockResolvedValueOnce({
|
|
29
|
-
rows: [{ chunk_id: "allowed", item_version_id: "v1", score: 0.8 }],
|
|
30
|
-
rowsAffected: 0,
|
|
31
|
-
}),
|
|
24
|
+
execute: vi.fn().mockResolvedValueOnce({
|
|
25
|
+
rows: [{ chunk_id: "allowed", item_version_id: "v1", score: 0.8 }],
|
|
26
|
+
rowsAffected: 0,
|
|
27
|
+
}),
|
|
32
28
|
};
|
|
33
29
|
const hits = await queryPostgresFts(db, {
|
|
34
30
|
query: "pricing slide",
|
|
35
31
|
allowedChunkIds: ["allowed"],
|
|
36
32
|
});
|
|
37
|
-
const query = db.execute.mock.calls[
|
|
33
|
+
const query = db.execute.mock.calls[0]?.[0] as {
|
|
38
34
|
sql: string;
|
|
39
35
|
args: unknown[];
|
|
40
36
|
};
|
|
@@ -46,19 +42,15 @@ describe("Postgres creative-context FTS", () => {
|
|
|
46
42
|
it("can search the global FTS index before access-scoped hydration", async () => {
|
|
47
43
|
isPostgres.mockReturnValue(true);
|
|
48
44
|
const db = {
|
|
49
|
-
execute: vi
|
|
50
|
-
.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.mockResolvedValueOnce({
|
|
54
|
-
rows: [{ chunk_id: "late", item_version_id: "v9", score: 0.9 }],
|
|
55
|
-
rowsAffected: 0,
|
|
56
|
-
}),
|
|
45
|
+
execute: vi.fn().mockResolvedValueOnce({
|
|
46
|
+
rows: [{ chunk_id: "late", item_version_id: "v9", score: 0.9 }],
|
|
47
|
+
rowsAffected: 0,
|
|
48
|
+
}),
|
|
57
49
|
};
|
|
58
50
|
const hits = await queryPostgresFts(db, {
|
|
59
51
|
query: "pricing slide",
|
|
60
52
|
});
|
|
61
|
-
const query = db.execute.mock.calls[
|
|
53
|
+
const query = db.execute.mock.calls[0]?.[0] as {
|
|
62
54
|
sql: string;
|
|
63
55
|
args: unknown[];
|
|
64
56
|
};
|
|
@@ -1,112 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { isPostgres } from "@agent-native/core/db";
|
|
3
|
-
|
|
4
|
-
const FTS_TABLE = "creative_context_search_documents";
|
|
5
|
-
|
|
6
|
-
export interface PostgresFtsHit {
|
|
7
|
-
chunkId: string;
|
|
8
|
-
itemVersionId: string;
|
|
9
|
-
score: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function ensurePostgresFts(db: DbExec): Promise<boolean> {
|
|
13
|
-
if (!isPostgres()) return false;
|
|
14
|
-
await db.execute(`
|
|
15
|
-
CREATE TABLE IF NOT EXISTS ${FTS_TABLE} (
|
|
16
|
-
chunk_id TEXT PRIMARY KEY,
|
|
17
|
-
item_version_id TEXT NOT NULL,
|
|
18
|
-
document TSVECTOR NOT NULL,
|
|
19
|
-
updated_at TEXT NOT NULL
|
|
20
|
-
)
|
|
21
|
-
`);
|
|
22
|
-
await db.execute(
|
|
23
|
-
`CREATE INDEX IF NOT EXISTS creative_context_search_documents_gin ON ${FTS_TABLE} USING GIN (document)`,
|
|
24
|
-
);
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function upsertPostgresFtsDocument(
|
|
29
|
-
db: DbExec,
|
|
30
|
-
input: {
|
|
31
|
-
chunkId: string;
|
|
32
|
-
itemVersionId: string;
|
|
33
|
-
title: string;
|
|
34
|
-
summary?: string | null;
|
|
35
|
-
body: string;
|
|
36
|
-
updatedAt?: string;
|
|
37
|
-
},
|
|
38
|
-
): Promise<boolean> {
|
|
39
|
-
if (!(await ensurePostgresFts(db))) return false;
|
|
40
|
-
await db.execute({
|
|
41
|
-
sql: `
|
|
42
|
-
INSERT INTO ${FTS_TABLE} (chunk_id, item_version_id, document, updated_at)
|
|
43
|
-
VALUES (
|
|
44
|
-
?,
|
|
45
|
-
?,
|
|
46
|
-
setweight(to_tsvector('simple', ?), 'A') ||
|
|
47
|
-
setweight(to_tsvector('simple', ?), 'B') ||
|
|
48
|
-
setweight(to_tsvector('simple', ?), 'C'),
|
|
49
|
-
?
|
|
50
|
-
)
|
|
51
|
-
ON CONFLICT (chunk_id) DO UPDATE SET
|
|
52
|
-
item_version_id = EXCLUDED.item_version_id,
|
|
53
|
-
document = EXCLUDED.document,
|
|
54
|
-
updated_at = EXCLUDED.updated_at
|
|
55
|
-
`,
|
|
56
|
-
args: [
|
|
57
|
-
input.chunkId,
|
|
58
|
-
input.itemVersionId,
|
|
59
|
-
input.title,
|
|
60
|
-
input.summary ?? "",
|
|
61
|
-
input.body,
|
|
62
|
-
input.updatedAt ?? new Date().toISOString(),
|
|
63
|
-
],
|
|
64
|
-
});
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function queryPostgresFts(
|
|
69
|
-
db: DbExec,
|
|
70
|
-
input: {
|
|
71
|
-
query: string;
|
|
72
|
-
allowedChunkIds?: readonly string[];
|
|
73
|
-
limit?: number;
|
|
74
|
-
},
|
|
75
|
-
): Promise<PostgresFtsHit[]> {
|
|
76
|
-
if (!isPostgres() || input.allowedChunkIds?.length === 0) return [];
|
|
77
|
-
await ensurePostgresFts(db);
|
|
78
|
-
const placeholders = input.allowedChunkIds?.map(() => "?").join(", ");
|
|
79
|
-
const accessClause = placeholders ? `chunk_id IN (${placeholders}) AND` : "";
|
|
80
|
-
const limit = Math.max(1, Math.min(200, Math.floor(input.limit ?? 40)));
|
|
81
|
-
const result = await db.execute({
|
|
82
|
-
sql: `
|
|
83
|
-
SELECT chunk_id, item_version_id,
|
|
84
|
-
ts_rank_cd(document, websearch_to_tsquery('simple', ?)) AS score
|
|
85
|
-
FROM ${FTS_TABLE}
|
|
86
|
-
WHERE ${accessClause}
|
|
87
|
-
document @@ websearch_to_tsquery('simple', ?)
|
|
88
|
-
ORDER BY score DESC, chunk_id ASC
|
|
89
|
-
LIMIT ?
|
|
90
|
-
`,
|
|
91
|
-
args: [input.query, ...(input.allowedChunkIds ?? []), input.query, limit],
|
|
92
|
-
});
|
|
93
|
-
return result.rows.map((row) => ({
|
|
94
|
-
chunkId: String(row.chunk_id),
|
|
95
|
-
itemVersionId: String(row.item_version_id),
|
|
96
|
-
score: Number(row.score),
|
|
97
|
-
}));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function deletePostgresFtsDocuments(
|
|
101
|
-
db: DbExec,
|
|
102
|
-
chunkIds: readonly string[],
|
|
103
|
-
): Promise<number> {
|
|
104
|
-
if (!isPostgres() || chunkIds.length === 0) return 0;
|
|
105
|
-
await ensurePostgresFts(db);
|
|
106
|
-
const placeholders = chunkIds.map(() => "?").join(", ");
|
|
107
|
-
const result = await db.execute({
|
|
108
|
-
sql: `DELETE FROM ${FTS_TABLE} WHERE chunk_id IN (${placeholders})`,
|
|
109
|
-
args: [...chunkIds],
|
|
110
|
-
});
|
|
111
|
-
return result.rowsAffected;
|
|
112
|
-
}
|
|
1
|
+
export * from "@agent-native/core/search";
|
|
@@ -29,7 +29,7 @@ describe("pgvector creative-context lane", () => {
|
|
|
29
29
|
expect(String(db.execute.mock.calls[1]?.[0])).toContain(
|
|
30
30
|
"creative_context_vectors_1024",
|
|
31
31
|
);
|
|
32
|
-
expect(String(db.execute.mock.calls[
|
|
32
|
+
expect(String(db.execute.mock.calls[3]?.[0])).toContain("USING hnsw");
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it("binds vector values and validates the selected dimensions", async () => {
|
|
@@ -44,7 +44,7 @@ describe("pgvector creative-context lane", () => {
|
|
|
44
44
|
},
|
|
45
45
|
true,
|
|
46
46
|
);
|
|
47
|
-
const write = db.execute.mock.calls[
|
|
47
|
+
const write = db.execute.mock.calls[5]?.[0] as {
|
|
48
48
|
sql: string;
|
|
49
49
|
args: unknown[];
|
|
50
50
|
};
|
|
@@ -68,6 +68,23 @@ describe("pgvector creative-context lane", () => {
|
|
|
68
68
|
).rejects.toThrow("expected 2");
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
it("can write a batch after its namespace and dimensions are provisioned", async () => {
|
|
72
|
+
const db = mockDb();
|
|
73
|
+
await ensurePgVectorIndex(db, 3, true);
|
|
74
|
+
await upsertPgVector(
|
|
75
|
+
db,
|
|
76
|
+
{
|
|
77
|
+
vectorKey: "chunk:1",
|
|
78
|
+
embeddingSetId: "set:1",
|
|
79
|
+
dimensions: 3,
|
|
80
|
+
vector: [0.25, 0.5, 0.75],
|
|
81
|
+
},
|
|
82
|
+
{ postgres: true, indexInitialized: true },
|
|
83
|
+
);
|
|
84
|
+
expect(db.execute).toHaveBeenCalledTimes(6);
|
|
85
|
+
expect(String(db.execute.mock.calls[5]?.[0].sql)).toContain("INSERT INTO");
|
|
86
|
+
});
|
|
87
|
+
|
|
71
88
|
it("limits vector results to access-filtered vector keys", async () => {
|
|
72
89
|
const db = mockDb([
|
|
73
90
|
{ vector_key: "media:1", embedding_set_id: "set:1", score: 0.91 },
|
package/src/vector/pgvector.ts
CHANGED
|
@@ -1,164 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { isPostgres } from "@agent-native/core/db";
|
|
3
|
-
|
|
4
|
-
export const PGVECTOR_REQUIRED_MESSAGE =
|
|
5
|
-
"Visual search requires Postgres with the pgvector extension in the configured DATABASE_URL database.";
|
|
6
|
-
|
|
7
|
-
export interface PgVectorHit {
|
|
8
|
-
vectorKey: string;
|
|
9
|
-
embeddingSetId: string;
|
|
10
|
-
score: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function checkedDimensions(value: number): number {
|
|
14
|
-
if (!Number.isInteger(value) || value < 1 || value > 16_000) {
|
|
15
|
-
throw new Error("Embedding dimensions must be an integer from 1 to 16000.");
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function vectorTable(dimensions: number): string {
|
|
21
|
-
return `creative_context_vectors_${checkedDimensions(dimensions)}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function vectorIndex(dimensions: number): string {
|
|
25
|
-
return `creative_context_vectors_${checkedDimensions(dimensions)}_hnsw`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function vectorLiteral(vector: readonly number[], dimensions: number): string {
|
|
29
|
-
if (vector.length !== dimensions) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`Embedding has ${vector.length} values; expected ${dimensions}.`,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
for (const value of vector) {
|
|
35
|
-
if (!Number.isFinite(value)) {
|
|
36
|
-
throw new Error("Embedding vectors may contain only finite numbers.");
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return `[${vector.join(",")}]`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function assertPgVectorAvailable(postgres = isPostgres()): void {
|
|
43
|
-
if (!postgres) throw new Error(PGVECTOR_REQUIRED_MESSAGE);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function ensurePgVectorIndex(
|
|
47
|
-
db: DbExec,
|
|
48
|
-
dimensions: number,
|
|
49
|
-
postgres = isPostgres(),
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
assertPgVectorAvailable(postgres);
|
|
52
|
-
const table = vectorTable(dimensions);
|
|
53
|
-
const index = vectorIndex(dimensions);
|
|
54
|
-
try {
|
|
55
|
-
await db.execute("CREATE EXTENSION IF NOT EXISTS vector");
|
|
56
|
-
await db.execute(`
|
|
57
|
-
CREATE TABLE IF NOT EXISTS ${table} (
|
|
58
|
-
vector_key TEXT PRIMARY KEY,
|
|
59
|
-
embedding_set_id TEXT NOT NULL,
|
|
60
|
-
embedding vector(${checkedDimensions(dimensions)}) NOT NULL,
|
|
61
|
-
updated_at TEXT NOT NULL
|
|
62
|
-
)
|
|
63
|
-
`);
|
|
64
|
-
await db.execute(
|
|
65
|
-
`CREATE INDEX IF NOT EXISTS ${index} ON ${table} USING hnsw (embedding vector_cosine_ops)`,
|
|
66
|
-
);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
throw new Error(PGVECTOR_REQUIRED_MESSAGE, { cause: error });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function upsertPgVector(
|
|
73
|
-
db: DbExec,
|
|
74
|
-
input: {
|
|
75
|
-
vectorKey: string;
|
|
76
|
-
embeddingSetId: string;
|
|
77
|
-
dimensions: number;
|
|
78
|
-
vector: readonly number[];
|
|
79
|
-
updatedAt?: string;
|
|
80
|
-
},
|
|
81
|
-
postgres = isPostgres(),
|
|
82
|
-
): Promise<void> {
|
|
83
|
-
assertPgVectorAvailable(postgres);
|
|
84
|
-
await ensurePgVectorIndex(db, input.dimensions, postgres);
|
|
85
|
-
const table = vectorTable(input.dimensions);
|
|
86
|
-
await db.execute({
|
|
87
|
-
sql: `
|
|
88
|
-
INSERT INTO ${table} (vector_key, embedding_set_id, embedding, updated_at)
|
|
89
|
-
VALUES (?, ?, ?::vector, ?)
|
|
90
|
-
ON CONFLICT (vector_key) DO UPDATE SET
|
|
91
|
-
embedding_set_id = EXCLUDED.embedding_set_id,
|
|
92
|
-
embedding = EXCLUDED.embedding,
|
|
93
|
-
updated_at = EXCLUDED.updated_at
|
|
94
|
-
`,
|
|
95
|
-
args: [
|
|
96
|
-
input.vectorKey,
|
|
97
|
-
input.embeddingSetId,
|
|
98
|
-
vectorLiteral(input.vector, input.dimensions),
|
|
99
|
-
input.updatedAt ?? new Date().toISOString(),
|
|
100
|
-
],
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function deletePgVectors(
|
|
105
|
-
db: DbExec,
|
|
106
|
-
input: {
|
|
107
|
-
dimensions: number;
|
|
108
|
-
vectorKeys: readonly string[];
|
|
109
|
-
},
|
|
110
|
-
postgres = isPostgres(),
|
|
111
|
-
): Promise<number> {
|
|
112
|
-
assertPgVectorAvailable(postgres);
|
|
113
|
-
if (input.vectorKeys.length === 0) return 0;
|
|
114
|
-
const table = vectorTable(input.dimensions);
|
|
115
|
-
const placeholders = input.vectorKeys.map(() => "?").join(", ");
|
|
116
|
-
const result = await db.execute({
|
|
117
|
-
sql: `DELETE FROM ${table} WHERE vector_key IN (${placeholders})`,
|
|
118
|
-
args: [...input.vectorKeys],
|
|
119
|
-
});
|
|
120
|
-
return result.rowsAffected;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function queryPgVectorIndex(
|
|
124
|
-
db: DbExec,
|
|
125
|
-
input: {
|
|
126
|
-
embeddingSetId: string;
|
|
127
|
-
dimensions: number;
|
|
128
|
-
vector: readonly number[];
|
|
129
|
-
limit?: number;
|
|
130
|
-
allowedVectorKeys: readonly string[];
|
|
131
|
-
},
|
|
132
|
-
postgres = isPostgres(),
|
|
133
|
-
): Promise<PgVectorHit[]> {
|
|
134
|
-
assertPgVectorAvailable(postgres);
|
|
135
|
-
const limit = Math.max(1, Math.min(200, Math.floor(input.limit ?? 40)));
|
|
136
|
-
const allowed = input.allowedVectorKeys;
|
|
137
|
-
if (allowed.length === 0) return [];
|
|
138
|
-
const table = vectorTable(input.dimensions);
|
|
139
|
-
const literal = vectorLiteral(input.vector, input.dimensions);
|
|
140
|
-
const allowedSql = allowed.length
|
|
141
|
-
? ` AND vector_key IN (${allowed.map(() => "?").join(", ")})`
|
|
142
|
-
: "";
|
|
143
|
-
try {
|
|
144
|
-
await db.execute("SET hnsw.iterative_scan = strict_order");
|
|
145
|
-
} catch {
|
|
146
|
-
// pgvector before iterative scans still supports the filtered exact query.
|
|
147
|
-
}
|
|
148
|
-
const result = await db.execute({
|
|
149
|
-
sql: `
|
|
150
|
-
SELECT vector_key, embedding_set_id,
|
|
151
|
-
1 - (embedding <=> ?::vector) AS score
|
|
152
|
-
FROM ${table}
|
|
153
|
-
WHERE embedding_set_id = ?${allowedSql}
|
|
154
|
-
ORDER BY embedding <=> ?::vector
|
|
155
|
-
LIMIT ?
|
|
156
|
-
`,
|
|
157
|
-
args: [literal, input.embeddingSetId, ...allowed, literal, limit],
|
|
158
|
-
});
|
|
159
|
-
return result.rows.map((row) => ({
|
|
160
|
-
vectorKey: String(row.vector_key),
|
|
161
|
-
embeddingSetId: String(row.embedding_set_id),
|
|
162
|
-
score: Number(row.score),
|
|
163
|
-
}));
|
|
164
|
-
}
|
|
1
|
+
export * from "@agent-native/core/search";
|