@adia-ai/a2ui-retrieval 0.6.4 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/domain-router.js +362 -117
- package/embedding/chunk-embedding-retriever.js +47 -79
- package/embedding/embedding-provider.js +35 -71
- package/embedding/index.js +2 -10
- package/feedback/dialog-recorder.js +61 -145
- package/feedback/feedback-analyzer.js +46 -102
- package/feedback/feedback-store.js +91 -107
- package/feedback/feedback.js +36 -117
- package/feedback/gap-registry.js +40 -82
- package/feedback/index.js +14 -12
- package/index.d.ts +4 -0
- package/index.js +53 -16
- package/intent/clarity.js +61 -129
- package/intent/decomposer.js +51 -143
- package/intent/index.js +18 -14
- package/intent/intent-alignment.js +79 -150
- package/intent/intent-categorizer.js +34 -62
- package/intent/intent-gate.js +43 -102
- package/intent/prompt-analyzer.js +68 -126
- package/package.json +4 -2
- package/wiring-catalog.js +95 -146
- package/embedding/chunk-embedding-retriever.ts +0 -156
- package/embedding/embedding-provider.ts +0 -111
- package/embedding/index.ts +0 -10
- package/feedback/dialog-recorder.ts +0 -172
- package/feedback/feedback-analyzer.ts +0 -250
- package/feedback/feedback-store.ts +0 -229
- package/feedback/feedback.ts +0 -201
- package/feedback/gap-registry.ts +0 -137
- package/feedback/index.ts +0 -14
- package/intent/clarity.ts +0 -224
- package/intent/decomposer.ts +0 -229
- package/intent/index.ts +0 -20
- package/intent/intent-alignment.ts +0 -267
- package/intent/intent-categorizer.ts +0 -104
- package/intent/intent-gate.ts +0 -151
- package/intent/prompt-analyzer.ts +0 -231
package/wiring-catalog.js
CHANGED
|
@@ -1,195 +1,144 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wiring Catalog — Knowledge base for the gen-ui pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Indexes available wiring capabilities so the generator can emit valid
|
|
5
|
-
* wireComponents declarations alongside component trees.
|
|
6
|
-
*
|
|
7
|
-
* The schema has 5 docking points:
|
|
8
|
-
* DATA — sources, params (how the surface gets its data)
|
|
9
|
-
* STATE — controllers, model (how components manage behavior)
|
|
10
|
-
* ACTIONS — UIEvent → handler chains (what happens on events)
|
|
11
|
-
* PROVIDES — context injection into subtrees
|
|
12
|
-
* LIFECYCLE — onMount, onUnmount, onModelChange hooks
|
|
13
|
-
*
|
|
14
|
-
* Each point is optional. A static display surface needs none.
|
|
15
|
-
* A form needs STATE (FormController) + ACTIONS (submit).
|
|
16
|
-
* A dashboard needs DATA (sources) + STATE (DataStreamController).
|
|
17
|
-
* A full app needs all five.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
// ── UIEvent ──────────────────────────────────────────────────
|
|
21
|
-
// A typed event object that the wiring system understands natively.
|
|
22
|
-
// Maps 1:1 to DOM events but with AdiaUI semantics and payload contracts.
|
|
23
|
-
//
|
|
24
|
-
// { "event": "press", "target": "save-btn" }
|
|
25
|
-
// { "event": "input", "target": "search", "debounce": 300 }
|
|
26
|
-
// { "event": "submit", "target": "form-col" }
|
|
27
|
-
// { "event": "mount" }
|
|
28
|
-
//
|
|
29
|
-
// "event" — UIEvent type name (see adiaEvents below)
|
|
30
|
-
// "target" — component id that emits. Omit for surface-level events.
|
|
31
|
-
// "debounce" — ms, coalesce rapid-fire (input, resize)
|
|
32
|
-
// "throttle" — ms, limit frequency (scroll, drag)
|
|
33
|
-
// "condition" — guard: { path, equals|notEquals|exists }
|
|
34
|
-
|
|
35
1
|
const adiaEvents = [
|
|
36
|
-
{ event:
|
|
37
|
-
{ event:
|
|
38
|
-
{ event:
|
|
39
|
-
{ event:
|
|
40
|
-
{ event:
|
|
41
|
-
{ event:
|
|
42
|
-
{ event:
|
|
43
|
-
{ event:
|
|
44
|
-
{ event:
|
|
45
|
-
{ event:
|
|
46
|
-
{ event:
|
|
47
|
-
{ event:
|
|
48
|
-
{ event:
|
|
49
|
-
{ event:
|
|
2
|
+
{ event: "press", payload: null, description: "Button or interactive element activated." },
|
|
3
|
+
{ event: "submit", payload: "form-values", description: "Form submission with validated field values." },
|
|
4
|
+
{ event: "input", payload: "target-value", description: "Value changed (fires on every keystroke)." },
|
|
5
|
+
{ event: "change", payload: "event-detail", description: "Value committed (fires on blur or selection)." },
|
|
6
|
+
{ event: "select", payload: "event-detail", description: "Item selected from list, table, or menu." },
|
|
7
|
+
{ event: "toggle", payload: "boolean", description: "Boolean state flipped (switch, checkbox)." },
|
|
8
|
+
{ event: "dismiss", payload: null, description: "Close/dismiss (dialog, toast, popover)." },
|
|
9
|
+
{ event: "navigate", payload: "route", description: "Internal navigation request." },
|
|
10
|
+
{ event: "mount", payload: null, description: "Surface or component entered the DOM." },
|
|
11
|
+
{ event: "unmount", payload: null, description: "Surface or component left the DOM." },
|
|
12
|
+
{ event: "focus", payload: null, description: "Element received focus." },
|
|
13
|
+
{ event: "blur", payload: null, description: "Element lost focus." },
|
|
14
|
+
{ event: "drag", payload: "event-detail", description: "Drag operation on a sortable/draggable." },
|
|
15
|
+
{ event: "drop", payload: "event-detail", description: "Drop completed on a sortable/draggable." }
|
|
50
16
|
];
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get the full wiring catalog.
|
|
54
|
-
* @returns {object}
|
|
55
|
-
*/
|
|
56
|
-
export function getWiringCatalog() {
|
|
17
|
+
function getWiringCatalog() {
|
|
57
18
|
return {
|
|
58
19
|
// ── ADIA EVENTS ──
|
|
59
20
|
adiaEvents,
|
|
60
|
-
|
|
61
21
|
// ── DATA: how the surface gets its data ──
|
|
62
22
|
data: {
|
|
63
|
-
description:
|
|
64
|
-
paramSources: [
|
|
65
|
-
uriSchemes: [
|
|
66
|
-
refreshStrategies: [
|
|
23
|
+
description: "Fetch external data into the surface model. Sources are URI-based with refresh strategies.",
|
|
24
|
+
paramSources: ["route", "store", "literal", "query", "parent"],
|
|
25
|
+
uriSchemes: ["resource://", "api://", "mock://", "ws://", "sse://"],
|
|
26
|
+
refreshStrategies: ["once", "on-focus", "interval:{ms}", "stream"],
|
|
67
27
|
example: {
|
|
68
|
-
params: { userId: { from:
|
|
28
|
+
params: { userId: { from: "route", key: "id" } },
|
|
69
29
|
sources: [
|
|
70
|
-
{ id:
|
|
71
|
-
{ id:
|
|
72
|
-
]
|
|
73
|
-
}
|
|
30
|
+
{ id: "user", uri: "resource://users/{userId}", path: "/user", refresh: "once" },
|
|
31
|
+
{ id: "activity", uri: "resource://users/{userId}/activity", path: "/activity", refresh: "on-focus" }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
74
34
|
},
|
|
75
|
-
|
|
76
35
|
// ── STATE: controllers that manage component behavior ──
|
|
77
36
|
controllers: [
|
|
78
37
|
{
|
|
79
|
-
type:
|
|
80
|
-
description:
|
|
38
|
+
type: "FormController",
|
|
39
|
+
description: "Manages field values, validation, dirty/pristine tracking. Attach to a Column/Section containing form fields.",
|
|
81
40
|
config: {
|
|
82
|
-
validateOn: { type:
|
|
41
|
+
validateOn: { type: "string", enum: ["blur", "change", "submit"], default: "blur" }
|
|
83
42
|
},
|
|
84
|
-
commands: [
|
|
85
|
-
bind: { values:
|
|
43
|
+
commands: ["validate", "reset", "setFieldError"],
|
|
44
|
+
bind: { values: "/form/values", valid: "/form/valid", dirty: "/form/dirty" }
|
|
86
45
|
},
|
|
87
46
|
{
|
|
88
|
-
type:
|
|
89
|
-
description:
|
|
47
|
+
type: "DataStreamController",
|
|
48
|
+
description: "Live data buffer for charts, tables, feeds. Supports push, SSE, WebSocket, polling.",
|
|
90
49
|
config: {
|
|
91
|
-
max: { type:
|
|
92
|
-
throttle: { type:
|
|
50
|
+
max: { type: "number", default: 100, description: "FIFO buffer size" },
|
|
51
|
+
throttle: { type: "number", default: 0, description: "Min ms between renders" }
|
|
93
52
|
},
|
|
94
|
-
commands: [
|
|
95
|
-
bind: { data:
|
|
53
|
+
commands: ["push", "pushMany", "connect", "poll", "consume", "stop", "clear"],
|
|
54
|
+
bind: { data: "/stream/data", status: "/stream/status" }
|
|
96
55
|
},
|
|
97
56
|
{
|
|
98
|
-
type:
|
|
99
|
-
description:
|
|
100
|
-
config: { mode: { type:
|
|
101
|
-
commands: [
|
|
102
|
-
bind: { selected:
|
|
57
|
+
type: "SelectionController",
|
|
58
|
+
description: "Single or multi-select state for lists, tables, grids.",
|
|
59
|
+
config: { mode: { type: "string", enum: ["single", "multi"], default: "single" } },
|
|
60
|
+
commands: ["select", "deselect", "toggle", "selectAll", "clear"],
|
|
61
|
+
bind: { selected: "/selection/items", count: "/selection/count" }
|
|
103
62
|
},
|
|
104
63
|
{
|
|
105
|
-
type:
|
|
106
|
-
description:
|
|
64
|
+
type: "ToggleController",
|
|
65
|
+
description: "Boolean on/off state with attribute reflection.",
|
|
107
66
|
config: {},
|
|
108
|
-
commands: [
|
|
109
|
-
bind: { value:
|
|
67
|
+
commands: ["toggle", "set"],
|
|
68
|
+
bind: { value: "/toggle/on" }
|
|
110
69
|
},
|
|
111
70
|
{
|
|
112
|
-
type:
|
|
113
|
-
description:
|
|
114
|
-
config: { multiple: { type:
|
|
115
|
-
commands: [
|
|
116
|
-
bind: { openPanels:
|
|
117
|
-
}
|
|
71
|
+
type: "AccordionController",
|
|
72
|
+
description: "Panel expand/collapse with mutual exclusion.",
|
|
73
|
+
config: { multiple: { type: "boolean", default: false } },
|
|
74
|
+
commands: ["open", "close", "toggle"],
|
|
75
|
+
bind: { openPanels: "/accordion/open" }
|
|
76
|
+
}
|
|
118
77
|
],
|
|
119
|
-
|
|
120
78
|
// ── ACTIONS: event → handler chains ──
|
|
121
79
|
handlers: [
|
|
122
|
-
{ name:
|
|
123
|
-
{ name:
|
|
124
|
-
{ name:
|
|
125
|
-
{ name:
|
|
126
|
-
{ name:
|
|
127
|
-
{ name:
|
|
128
|
-
{ name:
|
|
129
|
-
{ name:
|
|
80
|
+
{ name: "submit-resource", description: "POST/PUT/PATCH/DELETE to a resource URI.", config: ["uri", "method", "body"] },
|
|
81
|
+
{ name: "update-model", description: "Update the surface data model at a path.", config: ["path", "value"] },
|
|
82
|
+
{ name: "navigate", description: "Route to a path (supports {param} templates).", config: ["navigate"] },
|
|
83
|
+
{ name: "navigate-back", description: "Go back in browser history.", config: [] },
|
|
84
|
+
{ name: "controller-command", description: "Invoke a command on a controller.", config: ["controllerId", "command", "args"] },
|
|
85
|
+
{ name: "emit-event", description: "Dispatch a named CustomEvent on the surface.", config: ["eventName", "detail"] },
|
|
86
|
+
{ name: "refresh-source", description: "Re-fetch a named data source.", config: ["sourceId"] },
|
|
87
|
+
{ name: "notify", description: "Show a toast/alert notification.", config: ["message", "variant"] }
|
|
130
88
|
],
|
|
131
|
-
|
|
132
89
|
// ── ACTION SHAPE (UIEvent → handler) ──
|
|
133
90
|
actionShape: {
|
|
134
|
-
event:
|
|
135
|
-
handler:
|
|
136
|
-
config:
|
|
137
|
-
onSuccess:
|
|
138
|
-
onError:
|
|
91
|
+
event: "UIEvent object: { event, target?, debounce?, throttle?, condition? }",
|
|
92
|
+
handler: "Handler name from the handlers list.",
|
|
93
|
+
config: "Handler-specific configuration object.",
|
|
94
|
+
onSuccess: "Follow-up actions array (each is { handler, config }).",
|
|
95
|
+
onError: "Follow-up actions array on failure."
|
|
139
96
|
},
|
|
140
|
-
|
|
141
97
|
// ── PROVIDES: context injection ──
|
|
142
98
|
provides: {
|
|
143
|
-
description:
|
|
144
|
-
example: { name:
|
|
99
|
+
description: "Inject shared state into a component subtree via a2ui-provider. Children consume via context name.",
|
|
100
|
+
example: { name: "auth", host: "root", value: { user: null, token: null } }
|
|
145
101
|
},
|
|
146
|
-
|
|
147
102
|
// ── LIFECYCLE hooks ──
|
|
148
103
|
lifecycle: {
|
|
149
|
-
onMount:
|
|
150
|
-
onUnmount:
|
|
151
|
-
onModelChange:
|
|
104
|
+
onMount: "Actions to run when the surface first renders (fetch initial data, start streams).",
|
|
105
|
+
onUnmount: "Cleanup actions when the surface is destroyed (close WebSocket, stop polling).",
|
|
106
|
+
onModelChange: "Watch a model path and fire actions when it changes (debounce supported)."
|
|
152
107
|
},
|
|
153
|
-
|
|
154
108
|
// ── VALUE EXTRACTION (how actions read data) ──
|
|
155
109
|
valueSources: [
|
|
156
|
-
{ from:
|
|
157
|
-
{ from:
|
|
158
|
-
{ from:
|
|
159
|
-
{ from:
|
|
160
|
-
{ from:
|
|
161
|
-
{ from:
|
|
110
|
+
{ from: "event-detail", description: "Read from event.detail (key = property)" },
|
|
111
|
+
{ from: "event-target", description: "Read from event.target (key = property, e.g., value)" },
|
|
112
|
+
{ from: "model", description: "Read from data model (path = JSON Pointer)" },
|
|
113
|
+
{ from: "literal", description: "Static value (value = the value)" },
|
|
114
|
+
{ from: "param", description: "Read from resolved parameters (key = param name)" },
|
|
115
|
+
{ from: "controller", description: "Read from a controller state (controllerId + key)" }
|
|
162
116
|
],
|
|
163
|
-
|
|
164
117
|
// ── MULTI-SURFACE ASSOCIATIONS ──
|
|
165
118
|
associations: [
|
|
166
|
-
{ name:
|
|
167
|
-
{ name:
|
|
168
|
-
{ name:
|
|
169
|
-
{ name:
|
|
170
|
-
{ name:
|
|
171
|
-
{ name:
|
|
172
|
-
]
|
|
119
|
+
{ name: "routes-to", description: "Navigation \u2014 source links to target." },
|
|
120
|
+
{ name: "feeds", description: "Data flow \u2014 source output \u2192 target input." },
|
|
121
|
+
{ name: "shares-context", description: "Shared state \u2014 both read/write same context." },
|
|
122
|
+
{ name: "depends-on", description: "Lifecycle \u2014 source waits for target." },
|
|
123
|
+
{ name: "triggers", description: "Side effect \u2014 source event \u2192 target update." },
|
|
124
|
+
{ name: "contains", description: "Composition \u2014 target renders inside source." }
|
|
125
|
+
]
|
|
173
126
|
};
|
|
174
127
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* Get UIEvent type info.
|
|
178
|
-
*/
|
|
179
|
-
export function getAdiaEvent(type) {
|
|
180
|
-
return adiaEvents.find(e => e.event === type) || null;
|
|
128
|
+
function getAdiaEvent(type) {
|
|
129
|
+
return adiaEvents.find((e) => e.event === type) ?? void 0;
|
|
181
130
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
*/
|
|
186
|
-
export function getControllerInfo(type) {
|
|
187
|
-
return getWiringCatalog().controllers.find(c => c.type === type) || null;
|
|
131
|
+
function getControllerInfo(type) {
|
|
132
|
+
const catalog = getWiringCatalog();
|
|
133
|
+
return catalog["controllers"].find((c) => c.type === type) ?? void 0;
|
|
188
134
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
*/
|
|
193
|
-
export function getHandlerInfo(name) {
|
|
194
|
-
return getWiringCatalog().handlers.find(h => h.name === name) || null;
|
|
135
|
+
function getHandlerInfo(name) {
|
|
136
|
+
const catalog = getWiringCatalog();
|
|
137
|
+
return catalog["handlers"].find((h) => h.name === name) ?? void 0;
|
|
195
138
|
}
|
|
139
|
+
export {
|
|
140
|
+
getAdiaEvent,
|
|
141
|
+
getControllerInfo,
|
|
142
|
+
getHandlerInfo,
|
|
143
|
+
getWiringCatalog
|
|
144
|
+
};
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chunk embedding retriever — loads the build-time chunk embedding index
|
|
3
|
-
* and scores a query against every chunk via cosine similarity.
|
|
4
|
-
*
|
|
5
|
-
* Sibling to `embedding-retriever.js` (pattern embeddings); same provider
|
|
6
|
-
* conventions, same graceful-degradation contract.
|
|
7
|
-
*
|
|
8
|
-
* Index: packages/a2ui/corpus/chunk-embeddings.json (built by
|
|
9
|
-
* scripts/build/embeddings-chunks.mjs). When missing or empty, the
|
|
10
|
-
* retriever is effectively a no-op — callers see an empty score map and
|
|
11
|
-
* should fall back to keyword-only ranking.
|
|
12
|
-
*
|
|
13
|
-
* Used by chunk-library.searchChunks() to blend semantic + keyword scores.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { detectProvider, cosine, voyage, openai } from './embedding-provider.js';
|
|
17
|
-
import type { EmbedFn } from './embedding-provider.js';
|
|
18
|
-
|
|
19
|
-
// `process` is not in scope under "types": []
|
|
20
|
-
declare const process: { versions?: { node?: string } } | undefined;
|
|
21
|
-
|
|
22
|
-
const IS_NODE = typeof process !== 'undefined' && !!process.versions?.node;
|
|
23
|
-
|
|
24
|
-
type ChunkEntry = {
|
|
25
|
-
name: string;
|
|
26
|
-
vector: number[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type ChunkIndex = {
|
|
30
|
-
chunks?: ChunkEntry[];
|
|
31
|
-
provider?: string;
|
|
32
|
-
model?: string;
|
|
33
|
-
dims?: number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
let _index: ChunkIndex | null = null;
|
|
37
|
-
let _indexByName: Map<string, Float32Array> | null = null; // Map<chunk-name, Float32Array>
|
|
38
|
-
let _loadPromise: Promise<ChunkIndex | null> | null = null;
|
|
39
|
-
let _embedFn: EmbedFn | null = null;
|
|
40
|
-
let _available: boolean | null = null;
|
|
41
|
-
|
|
42
|
-
async function _loadIndex(): Promise<ChunkIndex | null> {
|
|
43
|
-
if (_index !== null) return _index;
|
|
44
|
-
if (_loadPromise) return _loadPromise;
|
|
45
|
-
_loadPromise = (async () => {
|
|
46
|
-
try {
|
|
47
|
-
if (IS_NODE) {
|
|
48
|
-
const fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
49
|
-
const path = await import(/* @vite-ignore */ 'node:path');
|
|
50
|
-
const url = await import(/* @vite-ignore */ 'node:url');
|
|
51
|
-
let p: string | null = null;
|
|
52
|
-
try {
|
|
53
|
-
const { createRequire } = await import(/* @vite-ignore */ 'node:module');
|
|
54
|
-
const require = createRequire(import.meta.url);
|
|
55
|
-
p = require.resolve('@adia-ai/a2ui-corpus/chunk-embeddings');
|
|
56
|
-
} catch {
|
|
57
|
-
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
58
|
-
p = path.resolve(here, '../../corpus/chunk-embeddings.json');
|
|
59
|
-
}
|
|
60
|
-
const raw = await fs.readFile(p, 'utf8');
|
|
61
|
-
_index = JSON.parse(raw) as ChunkIndex;
|
|
62
|
-
} else {
|
|
63
|
-
const url = new URL('../../corpus/chunk-embeddings.json', import.meta.url);
|
|
64
|
-
const res = await fetch(url).catch(() => null);
|
|
65
|
-
_index = res?.ok ? await res.json().catch(() => null) as ChunkIndex : null;
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
_index = null;
|
|
69
|
-
}
|
|
70
|
-
if (_index?.chunks?.length) {
|
|
71
|
-
_indexByName = new Map();
|
|
72
|
-
for (const c of _index.chunks) {
|
|
73
|
-
if (c?.name && Array.isArray(c.vector)) {
|
|
74
|
-
_indexByName.set(c.name, Float32Array.from(c.vector));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return _index;
|
|
79
|
-
})();
|
|
80
|
-
return _loadPromise;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function _resolveEmbed(providerName: string | undefined, model: string | undefined): EmbedFn | null {
|
|
84
|
-
// The index's recorded (provider, model) is the source of truth — query
|
|
85
|
-
// embeddings MUST be generated by the same model the corpus was indexed with.
|
|
86
|
-
let fn: EmbedFn | null = null;
|
|
87
|
-
if (providerName === 'voyage') fn = voyage({ model });
|
|
88
|
-
else if (providerName === 'openai') fn = openai({ model });
|
|
89
|
-
else {
|
|
90
|
-
// No provider recorded in index header (legacy index?). Fall back to auto-detect.
|
|
91
|
-
const auto = detectProvider();
|
|
92
|
-
return auto?.embed ?? null;
|
|
93
|
-
}
|
|
94
|
-
if (!fn && typeof console !== 'undefined') {
|
|
95
|
-
console.warn(
|
|
96
|
-
`[chunk-embedding-retriever] index was built with provider=${providerName} model=${model}, ` +
|
|
97
|
-
`but the corresponding API key is not set. Embeddings will be unavailable; falling back to ` +
|
|
98
|
-
`keyword-only retrieval. Set the matching API key, or rebuild the index with the available ` +
|
|
99
|
-
`provider via \`npm run build:embeddings:chunks\`.`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
return fn;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function available(): Promise<boolean> {
|
|
106
|
-
if (_available !== null) return _available;
|
|
107
|
-
const idx = await _loadIndex();
|
|
108
|
-
if (!idx || !idx.chunks?.length) {
|
|
109
|
-
_available = false;
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
_embedFn = _resolveEmbed(idx.provider, idx.model);
|
|
113
|
-
_available = !!_embedFn;
|
|
114
|
-
return _available;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Embed a query and return a Map<chunk-name → cosine-score>.
|
|
119
|
-
* Returns an empty Map when unavailable (no index or no API key).
|
|
120
|
-
*/
|
|
121
|
-
export async function scoreAll(query: string): Promise<Map<string, number>> {
|
|
122
|
-
if (!query || typeof query !== 'string') return new Map();
|
|
123
|
-
if (!(await available())) return new Map();
|
|
124
|
-
|
|
125
|
-
let qVec: Float32Array;
|
|
126
|
-
try {
|
|
127
|
-
const [v] = await _embedFn!([query]);
|
|
128
|
-
qVec = v!;
|
|
129
|
-
} catch (e) {
|
|
130
|
-
if (typeof console !== 'undefined') console.warn('[chunk-embedding-retriever]', (e as Error).message);
|
|
131
|
-
return new Map();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const out = new Map<string, number>();
|
|
135
|
-
if (!_indexByName) return out;
|
|
136
|
-
for (const [name, vec] of _indexByName) {
|
|
137
|
-
out.set(name, cosine(qVec, vec));
|
|
138
|
-
}
|
|
139
|
-
return out;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export async function size(): Promise<number> {
|
|
143
|
-
const idx = await _loadIndex();
|
|
144
|
-
return idx?.chunks?.length ?? 0;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export type ChunkProviderInfo = {
|
|
148
|
-
provider: string | undefined;
|
|
149
|
-
model: string | undefined;
|
|
150
|
-
dims: number | undefined;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
export async function providerInfo(): Promise<ChunkProviderInfo | null> {
|
|
154
|
-
const idx = await _loadIndex();
|
|
155
|
-
return idx ? { provider: idx.provider, model: idx.model, dims: idx.dims } : null;
|
|
156
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pluggable embedding provider. Two implementations:
|
|
3
|
-
*
|
|
4
|
-
* voyage() — Voyage AI, voyage-3-lite by default (1024 dims).
|
|
5
|
-
* Env: VOYAGE_API_KEY. Anthropic-recommended; generous free tier.
|
|
6
|
-
*
|
|
7
|
-
* openai() — OpenAI text-embedding-3-small (1536 dims).
|
|
8
|
-
* Env: OPENAI_API_KEY. Cheap ($0.02/1M tokens).
|
|
9
|
-
*
|
|
10
|
-
* detectProvider() picks Voyage if available, else OpenAI, else null.
|
|
11
|
-
* A null provider means "embeddings unavailable" — callers should fall back
|
|
12
|
-
* to keyword retrieval cleanly.
|
|
13
|
-
*
|
|
14
|
-
* Both implementations batch inputs to minimize round-trips. Every provider
|
|
15
|
-
* returns a Promise<Float32Array[]> of the same length as the input.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const VOYAGE_URL = 'https://api.voyageai.com/v1/embeddings';
|
|
19
|
-
const OPENAI_URL = 'https://api.openai.com/v1/embeddings';
|
|
20
|
-
|
|
21
|
-
// `process` is not in scope under "types": []
|
|
22
|
-
declare const process: { env?: Record<string, string | undefined> } | undefined;
|
|
23
|
-
|
|
24
|
-
function getEnv(key: string): string {
|
|
25
|
-
if (typeof process !== 'undefined' && process.env?.[key]) return process.env[key] as string;
|
|
26
|
-
try {
|
|
27
|
-
const env = (import.meta as { env?: Record<string, string | undefined> }).env;
|
|
28
|
-
if (env?.[`VITE_${key}`]) return env[`VITE_${key}`] as string;
|
|
29
|
-
if (env?.[key]) return env[key] as string;
|
|
30
|
-
} catch {}
|
|
31
|
-
return '';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type EmbedFn = (texts: string[]) => Promise<Float32Array[]>;
|
|
35
|
-
|
|
36
|
-
export type ProviderInfo = {
|
|
37
|
-
name: string;
|
|
38
|
-
model: string;
|
|
39
|
-
embed: EmbedFn;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Voyage AI embedding provider.
|
|
44
|
-
* Returns an embed function, or null if no key.
|
|
45
|
-
*/
|
|
46
|
-
export function voyage({ apiKey, model = 'voyage-3-lite' }: { apiKey?: string; model?: string } = {}): EmbedFn | null {
|
|
47
|
-
const key = apiKey ?? getEnv('VOYAGE_API_KEY');
|
|
48
|
-
if (!key) return null;
|
|
49
|
-
|
|
50
|
-
return async function embed(texts: string[]): Promise<Float32Array[]> {
|
|
51
|
-
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
52
|
-
const res = await fetch(VOYAGE_URL, {
|
|
53
|
-
method: 'POST',
|
|
54
|
-
headers: {
|
|
55
|
-
'content-type': 'application/json',
|
|
56
|
-
'authorization': `Bearer ${key}`,
|
|
57
|
-
},
|
|
58
|
-
body: JSON.stringify({ input: texts, model, input_type: 'document' }),
|
|
59
|
-
});
|
|
60
|
-
if (!res.ok) throw new Error(`Voyage ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
61
|
-
const data = await res.json() as { data: Array<{ embedding: number[] }> };
|
|
62
|
-
return data.data.map(d => new Float32Array(d.embedding));
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* OpenAI text-embedding-3-small provider.
|
|
68
|
-
* Returns an embed function, or null if no key.
|
|
69
|
-
*/
|
|
70
|
-
export function openai({ apiKey, model = 'text-embedding-3-small' }: { apiKey?: string; model?: string } = {}): EmbedFn | null {
|
|
71
|
-
const key = apiKey ?? getEnv('OPENAI_API_KEY');
|
|
72
|
-
if (!key) return null;
|
|
73
|
-
|
|
74
|
-
return async function embed(texts: string[]): Promise<Float32Array[]> {
|
|
75
|
-
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
76
|
-
const res = await fetch(OPENAI_URL, {
|
|
77
|
-
method: 'POST',
|
|
78
|
-
headers: {
|
|
79
|
-
'content-type': 'application/json',
|
|
80
|
-
'authorization': `Bearer ${key}`,
|
|
81
|
-
},
|
|
82
|
-
body: JSON.stringify({ input: texts, model }),
|
|
83
|
-
});
|
|
84
|
-
if (!res.ok) throw new Error(`OpenAI ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
85
|
-
const data = await res.json() as { data: Array<{ embedding: number[] }> };
|
|
86
|
-
return data.data.map(d => new Float32Array(d.embedding));
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Auto-detect the best available provider. Voyage first (cheaper, dense vectors
|
|
92
|
-
* at 1024 dims), then OpenAI, then null.
|
|
93
|
-
*/
|
|
94
|
-
export function detectProvider(): ProviderInfo | null {
|
|
95
|
-
const v = voyage(); if (v) return { name: 'voyage', model: 'voyage-3-lite', embed: v };
|
|
96
|
-
const o = openai(); if (o) return { name: 'openai', model: 'text-embedding-3-small', embed: o };
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Cosine similarity between two Float32Arrays of the same length. */
|
|
101
|
-
export function cosine(a: Float32Array, b: Float32Array): number {
|
|
102
|
-
if (!a || !b || a.length !== b.length) return 0;
|
|
103
|
-
let dot = 0, na = 0, nb = 0;
|
|
104
|
-
for (let i = 0; i < a.length; i++) {
|
|
105
|
-
dot += (a[i] ?? 0) * (b[i] ?? 0);
|
|
106
|
-
na += (a[i] ?? 0) * (a[i] ?? 0);
|
|
107
|
-
nb += (b[i] ?? 0) * (b[i] ?? 0);
|
|
108
|
-
}
|
|
109
|
-
if (na === 0 || nb === 0) return 0;
|
|
110
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
111
|
-
}
|
package/embedding/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @adia-ai/a2ui-retrieval/embedding — embedding-provider + chunk retriever surface.
|
|
3
|
-
*
|
|
4
|
-
* Since §65 (v0.4.7), the legacy pattern-embedding retriever was retired
|
|
5
|
-
* along with its only consumer (concept-mapper.js, dead post-§64). The
|
|
6
|
-
* canonical retrieval surface post-§40 is chunk-embedding-retriever.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export * from './embedding-provider.js';
|
|
10
|
-
export * from './chunk-embedding-retriever.js';
|