@adia-ai/a2ui-retrieval 0.6.4 → 0.6.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/CHANGELOG.md +21 -0
- package/domain-router.js +362 -117
- package/embedding/chunk-embedding-retriever.js +47 -79
- package/embedding/chunk-embedding-retriever.ts +2 -2
- package/embedding/embedding-provider.js +35 -71
- package/embedding/index.js +2 -10
- package/feedback/dialog-recorder.js +61 -145
- package/feedback/dialog-recorder.ts +11 -8
- package/feedback/feedback-analyzer.js +46 -102
- package/feedback/feedback-analyzer.ts +2 -2
- package/feedback/feedback-store.js +91 -107
- package/feedback/feedback-store.ts +1 -1
- package/feedback/feedback.js +36 -117
- package/feedback/gap-registry.js +40 -82
- package/feedback/index.js +14 -12
- package/index.js +53 -16
- package/intent/clarity.js +61 -129
- package/intent/decomposer.js +51 -143
- package/intent/decomposer.ts +1 -1
- package/intent/index.js +18 -14
- package/intent/intent-alignment.js +79 -150
- package/intent/intent-alignment.ts +5 -5
- package/intent/intent-categorizer.js +34 -62
- package/intent/intent-gate.js +43 -102
- package/intent/prompt-analyzer.js +68 -126
- package/intent/prompt-analyzer.ts +1 -1
- package/package.json +1 -1
- package/wiring-catalog.js +95 -146
|
@@ -1,55 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
18
|
-
const IS_NODE = typeof process !== 'undefined' && !!process.versions?.node;
|
|
19
|
-
|
|
1
|
+
import { detectProvider, cosine, voyage, openai } from "./embedding-provider.js";
|
|
2
|
+
const IS_NODE = typeof process !== "undefined" && !!process.versions?.node;
|
|
20
3
|
let _index = null;
|
|
21
|
-
let _indexByName = null;
|
|
4
|
+
let _indexByName = null;
|
|
22
5
|
let _loadPromise = null;
|
|
23
6
|
let _embedFn = null;
|
|
24
7
|
let _available = null;
|
|
25
|
-
|
|
26
8
|
async function _loadIndex() {
|
|
27
9
|
if (_index !== null) return _index;
|
|
28
10
|
if (_loadPromise) return _loadPromise;
|
|
29
11
|
_loadPromise = (async () => {
|
|
30
12
|
try {
|
|
31
13
|
if (IS_NODE) {
|
|
32
|
-
const fs = await import(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
14
|
+
const fs = await import(
|
|
15
|
+
/* @vite-ignore */
|
|
16
|
+
"node:fs/promises"
|
|
17
|
+
);
|
|
18
|
+
const path = await import(
|
|
19
|
+
/* @vite-ignore */
|
|
20
|
+
"node:path"
|
|
21
|
+
);
|
|
22
|
+
const url = await import(
|
|
23
|
+
/* @vite-ignore */
|
|
24
|
+
"node:url"
|
|
25
|
+
);
|
|
40
26
|
let p = null;
|
|
41
27
|
try {
|
|
42
|
-
const { createRequire } = await import(
|
|
43
|
-
|
|
44
|
-
|
|
28
|
+
const { createRequire } = await import(
|
|
29
|
+
/* @vite-ignore */
|
|
30
|
+
"node:module"
|
|
31
|
+
);
|
|
32
|
+
const require2 = createRequire(import.meta.url);
|
|
33
|
+
p = require2.resolve("@adia-ai/a2ui-corpus/chunk-embeddings");
|
|
45
34
|
} catch {
|
|
46
35
|
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
47
|
-
p = path.resolve(here,
|
|
36
|
+
p = path.resolve(here, "../../corpus/chunk-embeddings.json");
|
|
48
37
|
}
|
|
49
|
-
const raw = await fs.readFile(p,
|
|
38
|
+
const raw = await fs.readFile(p, "utf8");
|
|
50
39
|
_index = JSON.parse(raw);
|
|
51
40
|
} else {
|
|
52
|
-
const url = new URL(
|
|
41
|
+
const url = new URL("../../corpus/chunk-embeddings.json", import.meta.url);
|
|
53
42
|
const res = await fetch(url).catch(() => null);
|
|
54
43
|
_index = res?.ok ? await res.json().catch(() => null) : null;
|
|
55
44
|
}
|
|
@@ -57,7 +46,7 @@ async function _loadIndex() {
|
|
|
57
46
|
_index = null;
|
|
58
47
|
}
|
|
59
48
|
if (_index?.chunks?.length) {
|
|
60
|
-
_indexByName = new Map();
|
|
49
|
+
_indexByName = /* @__PURE__ */ new Map();
|
|
61
50
|
for (const c of _index.chunks) {
|
|
62
51
|
if (c?.name && Array.isArray(c.vector)) {
|
|
63
52
|
_indexByName.set(c.name, Float32Array.from(c.vector));
|
|
@@ -68,41 +57,22 @@ async function _loadIndex() {
|
|
|
68
57
|
})();
|
|
69
58
|
return _loadPromise;
|
|
70
59
|
}
|
|
71
|
-
|
|
72
60
|
function _resolveEmbed(providerName, model) {
|
|
73
|
-
// The index's recorded (provider, model) is the source of truth — query
|
|
74
|
-
// embeddings MUST be generated by the same model the corpus was indexed
|
|
75
|
-
// with. If they're different models (e.g. voyage-3-lite vs.
|
|
76
|
-
// text-embedding-3-small), the cosine similarity is meaningless, and even
|
|
77
|
-
// same-provider/different-model (e.g. text-embedding-3-small@1536 vs
|
|
78
|
-
// text-embedding-3-large@3072) will emit different dim vectors which
|
|
79
|
-
// cosine() short-circuits to 0 — silent retrieval failure.
|
|
80
|
-
//
|
|
81
|
-
// Fail loud: do NOT silently fall back to a different provider's auto-pick.
|
|
82
|
-
// Caller (available()) returns false → keyword-only retrieval, which is
|
|
83
|
-
// the right behavior, but with a console.warn so the cause is visible.
|
|
84
61
|
let fn = null;
|
|
85
|
-
if (providerName ===
|
|
86
|
-
else if (providerName ===
|
|
62
|
+
if (providerName === "voyage") fn = voyage({ ...model !== void 0 ? { model } : {} });
|
|
63
|
+
else if (providerName === "openai") fn = openai({ ...model !== void 0 ? { model } : {} });
|
|
87
64
|
else {
|
|
88
|
-
// No provider recorded in index header (legacy index?). Fall back to
|
|
89
|
-
// auto-detect — this preserves the old behavior for indexes that pre-date
|
|
90
|
-
// the provider/model header convention.
|
|
91
65
|
const auto = detectProvider();
|
|
92
|
-
return auto?.embed
|
|
66
|
+
return auto?.embed ?? null;
|
|
93
67
|
}
|
|
94
|
-
if (!fn && typeof console !==
|
|
68
|
+
if (!fn && typeof console !== "undefined") {
|
|
95
69
|
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\`.`
|
|
70
|
+
`[chunk-embedding-retriever] index was built with provider=${providerName} model=${model}, but the corresponding API key is not set. Embeddings will be unavailable; falling back to keyword-only retrieval. Set the matching API key, or rebuild the index with the available provider via \`npm run build:embeddings:chunks\`.`
|
|
100
71
|
);
|
|
101
72
|
}
|
|
102
73
|
return fn;
|
|
103
74
|
}
|
|
104
|
-
|
|
105
|
-
export async function available() {
|
|
75
|
+
async function available() {
|
|
106
76
|
if (_available !== null) return _available;
|
|
107
77
|
const idx = await _loadIndex();
|
|
108
78
|
if (!idx || !idx.chunks?.length) {
|
|
@@ -113,37 +83,35 @@ export async function available() {
|
|
|
113
83
|
_available = !!_embedFn;
|
|
114
84
|
return _available;
|
|
115
85
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
* Returns an empty Map when unavailable (no index or no API key).
|
|
120
|
-
*/
|
|
121
|
-
export async function scoreAll(query) {
|
|
122
|
-
if (!query || typeof query !== 'string') return new Map();
|
|
123
|
-
if (!(await available())) return new Map();
|
|
124
|
-
|
|
86
|
+
async function scoreAll(query) {
|
|
87
|
+
if (!query || typeof query !== "string") return /* @__PURE__ */ new Map();
|
|
88
|
+
if (!await available()) return /* @__PURE__ */ new Map();
|
|
125
89
|
let qVec;
|
|
126
90
|
try {
|
|
127
91
|
const [v] = await _embedFn([query]);
|
|
128
92
|
qVec = v;
|
|
129
93
|
} catch (e) {
|
|
130
|
-
if (typeof console !==
|
|
131
|
-
return new Map();
|
|
94
|
+
if (typeof console !== "undefined") console.warn("[chunk-embedding-retriever]", e.message);
|
|
95
|
+
return /* @__PURE__ */ new Map();
|
|
132
96
|
}
|
|
133
|
-
|
|
134
|
-
|
|
97
|
+
const out = /* @__PURE__ */ new Map();
|
|
98
|
+
if (!_indexByName) return out;
|
|
135
99
|
for (const [name, vec] of _indexByName) {
|
|
136
100
|
out.set(name, cosine(qVec, vec));
|
|
137
101
|
}
|
|
138
102
|
return out;
|
|
139
103
|
}
|
|
140
|
-
|
|
141
|
-
export async function size() {
|
|
104
|
+
async function size() {
|
|
142
105
|
const idx = await _loadIndex();
|
|
143
|
-
return idx?.chunks?.length
|
|
106
|
+
return idx?.chunks?.length ?? 0;
|
|
144
107
|
}
|
|
145
|
-
|
|
146
|
-
export async function providerInfo() {
|
|
108
|
+
async function providerInfo() {
|
|
147
109
|
const idx = await _loadIndex();
|
|
148
110
|
return idx ? { provider: idx.provider, model: idx.model, dims: idx.dims } : null;
|
|
149
111
|
}
|
|
112
|
+
export {
|
|
113
|
+
available,
|
|
114
|
+
providerInfo,
|
|
115
|
+
scoreAll,
|
|
116
|
+
size
|
|
117
|
+
};
|
|
@@ -84,8 +84,8 @@ function _resolveEmbed(providerName: string | undefined, model: string | undefin
|
|
|
84
84
|
// The index's recorded (provider, model) is the source of truth — query
|
|
85
85
|
// embeddings MUST be generated by the same model the corpus was indexed with.
|
|
86
86
|
let fn: EmbedFn | null = null;
|
|
87
|
-
if (providerName === 'voyage') fn = voyage({ model });
|
|
88
|
-
else if (providerName === 'openai') fn = openai({ model });
|
|
87
|
+
if (providerName === 'voyage') fn = voyage({ ...(model !== undefined ? { model } : {}) });
|
|
88
|
+
else if (providerName === 'openai') fn = openai({ ...(model !== undefined ? { model } : {}) });
|
|
89
89
|
else {
|
|
90
90
|
// No provider recorded in index header (legacy index?). Fall back to auto-detect.
|
|
91
91
|
const auto = detectProvider();
|
|
@@ -1,108 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
1
|
+
const VOYAGE_URL = "https://api.voyageai.com/v1/embeddings";
|
|
2
|
+
const OPENAI_URL = "https://api.openai.com/v1/embeddings";
|
|
21
3
|
function getEnv(key) {
|
|
22
|
-
if (typeof process !==
|
|
4
|
+
if (typeof process !== "undefined" && process.env?.[key]) return process.env[key];
|
|
23
5
|
try {
|
|
24
6
|
const env = import.meta.env;
|
|
25
7
|
if (env?.[`VITE_${key}`]) return env[`VITE_${key}`];
|
|
26
8
|
if (env?.[key]) return env[key];
|
|
27
|
-
} catch {
|
|
28
|
-
|
|
9
|
+
} catch {
|
|
10
|
+
}
|
|
11
|
+
return "";
|
|
29
12
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* Voyage AI embedding provider.
|
|
33
|
-
*
|
|
34
|
-
* @param {object} [opts]
|
|
35
|
-
* @param {string} [opts.apiKey] — defaults to env VOYAGE_API_KEY
|
|
36
|
-
* @param {string} [opts.model] — voyage-3-lite | voyage-3 | voyage-code-3
|
|
37
|
-
* @returns {(texts: string[]) => Promise<Float32Array[]>} embedder, or null if no key
|
|
38
|
-
*/
|
|
39
|
-
export function voyage({ apiKey, model = 'voyage-3-lite' } = {}) {
|
|
40
|
-
const key = apiKey || getEnv('VOYAGE_API_KEY');
|
|
13
|
+
function voyage({ apiKey, model = "voyage-3-lite" } = {}) {
|
|
14
|
+
const key = apiKey ?? getEnv("VOYAGE_API_KEY");
|
|
41
15
|
if (!key) return null;
|
|
42
|
-
|
|
43
16
|
return async function embed(texts) {
|
|
44
17
|
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
45
18
|
const res = await fetch(VOYAGE_URL, {
|
|
46
|
-
method:
|
|
19
|
+
method: "POST",
|
|
47
20
|
headers: {
|
|
48
|
-
|
|
49
|
-
|
|
21
|
+
"content-type": "application/json",
|
|
22
|
+
"authorization": `Bearer ${key}`
|
|
50
23
|
},
|
|
51
|
-
body: JSON.stringify({ input: texts, model, input_type:
|
|
24
|
+
body: JSON.stringify({ input: texts, model, input_type: "document" })
|
|
52
25
|
});
|
|
53
26
|
if (!res.ok) throw new Error(`Voyage ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
54
27
|
const data = await res.json();
|
|
55
|
-
return data.data.map(d => new Float32Array(d.embedding));
|
|
28
|
+
return data.data.map((d) => new Float32Array(d.embedding));
|
|
56
29
|
};
|
|
57
30
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* OpenAI text-embedding-3-small provider.
|
|
61
|
-
*
|
|
62
|
-
* @param {object} [opts]
|
|
63
|
-
* @param {string} [opts.apiKey] — defaults to env OPENAI_API_KEY
|
|
64
|
-
* @param {string} [opts.model] — text-embedding-3-small | text-embedding-3-large
|
|
65
|
-
* @returns {(texts: string[]) => Promise<Float32Array[]>} embedder, or null if no key
|
|
66
|
-
*/
|
|
67
|
-
export function openai({ apiKey, model = 'text-embedding-3-small' } = {}) {
|
|
68
|
-
const key = apiKey || getEnv('OPENAI_API_KEY');
|
|
31
|
+
function openai({ apiKey, model = "text-embedding-3-small" } = {}) {
|
|
32
|
+
const key = apiKey ?? getEnv("OPENAI_API_KEY");
|
|
69
33
|
if (!key) return null;
|
|
70
|
-
|
|
71
34
|
return async function embed(texts) {
|
|
72
35
|
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
73
36
|
const res = await fetch(OPENAI_URL, {
|
|
74
|
-
method:
|
|
37
|
+
method: "POST",
|
|
75
38
|
headers: {
|
|
76
|
-
|
|
77
|
-
|
|
39
|
+
"content-type": "application/json",
|
|
40
|
+
"authorization": `Bearer ${key}`
|
|
78
41
|
},
|
|
79
|
-
body: JSON.stringify({ input: texts, model })
|
|
42
|
+
body: JSON.stringify({ input: texts, model })
|
|
80
43
|
});
|
|
81
44
|
if (!res.ok) throw new Error(`OpenAI ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
82
45
|
const data = await res.json();
|
|
83
|
-
return data.data.map(d => new Float32Array(d.embedding));
|
|
46
|
+
return data.data.map((d) => new Float32Array(d.embedding));
|
|
84
47
|
};
|
|
85
48
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
export function detectProvider() {
|
|
92
|
-
const v = voyage(); if (v) return { name: 'voyage', model: 'voyage-3-lite', embed: v };
|
|
93
|
-
const o = openai(); if (o) return { name: 'openai', model: 'text-embedding-3-small', embed: o };
|
|
49
|
+
function detectProvider() {
|
|
50
|
+
const v = voyage();
|
|
51
|
+
if (v) return { name: "voyage", model: "voyage-3-lite", embed: v };
|
|
52
|
+
const o = openai();
|
|
53
|
+
if (o) return { name: "openai", model: "text-embedding-3-small", embed: o };
|
|
94
54
|
return null;
|
|
95
55
|
}
|
|
96
|
-
|
|
97
|
-
/** Cosine similarity between two Float32Arrays of the same length. */
|
|
98
|
-
export function cosine(a, b) {
|
|
56
|
+
function cosine(a, b) {
|
|
99
57
|
if (!a || !b || a.length !== b.length) return 0;
|
|
100
58
|
let dot = 0, na = 0, nb = 0;
|
|
101
59
|
for (let i = 0; i < a.length; i++) {
|
|
102
|
-
dot += a[i] * b[i];
|
|
103
|
-
na
|
|
104
|
-
nb
|
|
60
|
+
dot += (a[i] ?? 0) * (b[i] ?? 0);
|
|
61
|
+
na += (a[i] ?? 0) * (a[i] ?? 0);
|
|
62
|
+
nb += (b[i] ?? 0) * (b[i] ?? 0);
|
|
105
63
|
}
|
|
106
64
|
if (na === 0 || nb === 0) return 0;
|
|
107
65
|
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
108
66
|
}
|
|
67
|
+
export {
|
|
68
|
+
cosine,
|
|
69
|
+
detectProvider,
|
|
70
|
+
openai,
|
|
71
|
+
voyage
|
|
72
|
+
};
|
package/embedding/index.js
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
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';
|
|
1
|
+
export * from "./embedding-provider.js";
|
|
2
|
+
export * from "./chunk-embedding-retriever.js";
|
|
@@ -1,191 +1,107 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* regression analysis, and training-data bootstrapping.
|
|
4
|
-
*
|
|
5
|
-
* Output: logs/dialogs/<sessionId>/<NNN>-<turnId>.json
|
|
6
|
-
* - <sessionId>: storeId / executionId of the multi-turn session, or
|
|
7
|
-
* 'standalone-<isodate>' for one-shot generations.
|
|
8
|
-
* - <NNN>: zero-padded turn index within the session (000, 001, …)
|
|
9
|
-
* - <turnId>: short slug from the intent + a random suffix for uniqueness
|
|
10
|
-
*
|
|
11
|
-
* Each file contains a flat JSON object with everything the pipeline knew at
|
|
12
|
-
* generation time: intent, mode, model, the analyzer's structured signals,
|
|
13
|
-
* pattern matches, the FULL system prompt, the raw LLM response, parsed A2UI
|
|
14
|
-
* messages, validation, drift, suggestions, and timing/token telemetry.
|
|
15
|
-
*
|
|
16
|
-
* Gated by ADIA_LOG_DIALOGS=1 — opt-in, zero overhead when disabled. Browser
|
|
17
|
-
* environment is also a no-op (no fs); only Node-side generation paths log.
|
|
18
|
-
*
|
|
19
|
-
* Use cases:
|
|
20
|
-
* - replay a single bad turn locally without paying the LLM cost again
|
|
21
|
-
* - diff two turns visually after wiring the headless renderer
|
|
22
|
-
* - bootstrap an eval set from real dogfood — every turn becomes a labeled example
|
|
23
|
-
* - regression detection on prompt / corpus / catalog changes
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const IS_NODE = typeof process !== 'undefined' && !!process.versions?.node;
|
|
27
|
-
const ENABLED = IS_NODE && (process.env.ADIA_LOG_DIALOGS === '1' || process.env.ADIA_LOG_DIALOGS === 'true');
|
|
28
|
-
|
|
1
|
+
const IS_NODE = typeof process !== "undefined" && !!process.versions?.node;
|
|
2
|
+
const ENABLED = IS_NODE && (process?.env?.["ADIA_LOG_DIALOGS"] === "1" || process?.env?.["ADIA_LOG_DIALOGS"] === "true");
|
|
29
3
|
let _fs = null;
|
|
30
4
|
let _path = null;
|
|
31
5
|
let _url = null;
|
|
32
6
|
let _logsRoot = null;
|
|
33
|
-
|
|
34
7
|
async function _ensureModules() {
|
|
35
8
|
if (_fs) return;
|
|
36
|
-
_fs = await import(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
9
|
+
_fs = await import(
|
|
10
|
+
/* @vite-ignore */
|
|
11
|
+
"node:fs/promises"
|
|
12
|
+
);
|
|
13
|
+
_path = await import(
|
|
14
|
+
/* @vite-ignore */
|
|
15
|
+
"node:path"
|
|
16
|
+
);
|
|
17
|
+
_url = await import(
|
|
18
|
+
/* @vite-ignore */
|
|
19
|
+
"node:url"
|
|
20
|
+
);
|
|
40
21
|
const __dirname = _path.dirname(_url.fileURLToPath(import.meta.url));
|
|
41
|
-
_logsRoot = _path.resolve(__dirname,
|
|
22
|
+
_logsRoot = _path.resolve(__dirname, "..", "..", "..", "..", "logs", "dialogs");
|
|
42
23
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const _turnCounter = new Map();
|
|
47
|
-
// Tracks which sessions we've already written `_session.json` for. Per-process
|
|
48
|
-
// memory; if the process restarts mid-session, the file is rewritten, which is
|
|
49
|
-
// fine — content is idempotent.
|
|
50
|
-
const _sessionMetaWritten = new Set();
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Record one generation turn. Safe to call unconditionally — when the env var
|
|
54
|
-
* is unset, this is a no-op that returns immediately.
|
|
55
|
-
*
|
|
56
|
-
* @param {object} record
|
|
57
|
-
* @param {string} record.sessionId — multi-turn session handle (storeId / executionId)
|
|
58
|
-
* @param {string} record.intent — user's prompt this turn
|
|
59
|
-
* @param {string} [record.mode] — instant | pro | thinking | stream
|
|
60
|
-
* @param {string} [record.engine] — monolithic | zettel | <custom>
|
|
61
|
-
* @param {string} [record.model] — LLM model id
|
|
62
|
-
* @param {object} [record.analysis] — output of analyzePrompt() (concepts, steelman, ...)
|
|
63
|
-
* @param {object} [record.currentCanvas] — { components } or { messages } provided by caller
|
|
64
|
-
* @param {object[]} [record.patterns] — patterns retrieved by searchBlocks()
|
|
65
|
-
* @param {string} [record.systemPrompt] — full system prompt sent to LLM
|
|
66
|
-
* @param {string} [record.rawLLMResponse] — raw LLM text (pre-parse)
|
|
67
|
-
* @param {object[]} [record.messages] — parsed A2UI messages (the result)
|
|
68
|
-
* @param {object} [record.validation] — validateSchema() result
|
|
69
|
-
* @param {object} [record.drift] — getDriftMetrics() result
|
|
70
|
-
* @param {string[]} [record.suggestions] — follow-up suggestions
|
|
71
|
-
* @param {object} [record.timing] — { totalMs, llmMs, ... }
|
|
72
|
-
* @param {object} [record.tokens] — { input, output }
|
|
73
|
-
* @param {object} [record.engineDebug] — engine-specific extras (strategy, composition, fragmentsUsed for zettel; raw stage reports, etc.)
|
|
74
|
-
* @param {boolean} [record.isIteration] — derived from executionId / currentCanvas presence
|
|
75
|
-
* @returns {Promise<string|null>} the path written, or null when logging is disabled
|
|
76
|
-
*/
|
|
77
|
-
export async function recordTurn(record) {
|
|
24
|
+
const _turnCounter = /* @__PURE__ */ new Map();
|
|
25
|
+
const _sessionMetaWritten = /* @__PURE__ */ new Set();
|
|
26
|
+
async function recordTurn(record) {
|
|
78
27
|
if (!ENABLED) return null;
|
|
79
|
-
|
|
80
28
|
try {
|
|
81
29
|
await _ensureModules();
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
const turnIdx = (_turnCounter.get(sessionId) ?? 0);
|
|
30
|
+
const sessionId = record.sessionId ?? `standalone-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
31
|
+
const turnIdx = _turnCounter.get(sessionId) ?? 0;
|
|
85
32
|
_turnCounter.set(sessionId, turnIdx + 1);
|
|
86
|
-
|
|
87
|
-
const slug = String(record.intent || 'turn')
|
|
88
|
-
.toLowerCase()
|
|
89
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
90
|
-
.replace(/^-+|-+$/g, '')
|
|
91
|
-
.slice(0, 32) || 'turn';
|
|
33
|
+
const slug = String(record.intent ?? "turn").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "turn";
|
|
92
34
|
const rand = Math.random().toString(36).slice(2, 6);
|
|
93
|
-
const fileName = `${String(turnIdx).padStart(3,
|
|
94
|
-
|
|
35
|
+
const fileName = `${String(turnIdx).padStart(3, "0")}-${slug}-${rand}.json`;
|
|
95
36
|
const sessionDir = _path.join(_logsRoot, sessionId);
|
|
96
37
|
await _fs.mkdir(sessionDir, { recursive: true });
|
|
97
|
-
|
|
98
|
-
// Write per-session header on the first turn so a directory listing is
|
|
99
|
-
// immediately legible — origin intent, model, mode, first-turn analysis,
|
|
100
|
-
// start time. Future headless renderer reads this for its session list UI.
|
|
101
38
|
if (!_sessionMetaWritten.has(sessionId) && turnIdx === 0) {
|
|
102
39
|
_sessionMetaWritten.add(sessionId);
|
|
103
40
|
const sessionMeta = {
|
|
104
41
|
sessionId,
|
|
105
|
-
startedAt: new Date().toISOString(),
|
|
106
|
-
originIntent: record.intent
|
|
107
|
-
engine: record.engine
|
|
108
|
-
mode: record.mode
|
|
109
|
-
model: record.model
|
|
110
|
-
// The analyzer's enriched brief + concept tags from turn 0 — these
|
|
111
|
-
// characterize the session's intent space, not just the latest turn.
|
|
42
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43
|
+
originIntent: record.intent ?? null,
|
|
44
|
+
engine: record.engine ?? null,
|
|
45
|
+
mode: record.mode ?? null,
|
|
46
|
+
model: record.model ?? null,
|
|
112
47
|
originAnalysis: record.analysis ? {
|
|
113
|
-
steelman: record.analysis.steelman
|
|
114
|
-
concepts: record.analysis.concepts
|
|
115
|
-
impliedComponents: record.analysis.impliedComponents
|
|
116
|
-
styleHints: record.analysis.styleHints
|
|
117
|
-
} : null
|
|
48
|
+
steelman: record.analysis.steelman ?? null,
|
|
49
|
+
concepts: record.analysis.concepts ?? [],
|
|
50
|
+
impliedComponents: record.analysis.impliedComponents ?? [],
|
|
51
|
+
styleHints: record.analysis.styleHints ?? []
|
|
52
|
+
} : null
|
|
118
53
|
};
|
|
119
54
|
await _fs.writeFile(
|
|
120
|
-
_path.join(sessionDir,
|
|
121
|
-
JSON.stringify(sessionMeta, null, 2) +
|
|
55
|
+
_path.join(sessionDir, "_session.json"),
|
|
56
|
+
JSON.stringify(sessionMeta, null, 2) + "\n"
|
|
122
57
|
);
|
|
123
58
|
}
|
|
124
|
-
|
|
125
59
|
const payload = {
|
|
126
60
|
// ── Identity ────────────────────────────────────────────────
|
|
127
61
|
sessionId,
|
|
128
62
|
turnIndex: turnIdx,
|
|
129
|
-
timestamp: new Date().toISOString(),
|
|
63
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
130
64
|
isIteration: !!record.isIteration,
|
|
131
|
-
|
|
132
65
|
// ── Request ─────────────────────────────────────────────────
|
|
133
66
|
intent: record.intent,
|
|
134
|
-
mode: record.mode
|
|
135
|
-
engine: record.engine
|
|
136
|
-
model: record.model
|
|
137
|
-
currentCanvas: record.currentCanvas
|
|
138
|
-
|
|
67
|
+
mode: record.mode ?? null,
|
|
68
|
+
engine: record.engine ?? null,
|
|
69
|
+
model: record.model ?? null,
|
|
70
|
+
currentCanvas: record.currentCanvas ?? null,
|
|
139
71
|
// ── Pipeline-side reasoning ─────────────────────────────────
|
|
140
|
-
analysis: record.analysis
|
|
141
|
-
patterns: (record.patterns
|
|
72
|
+
analysis: record.analysis ?? null,
|
|
73
|
+
patterns: (record.patterns ?? []).slice(0, 10).map((p) => ({
|
|
142
74
|
name: p.name,
|
|
143
75
|
score: p.score ?? p.confidence ?? null,
|
|
144
|
-
keywords: p.keywords
|
|
76
|
+
keywords: p.keywords ?? null
|
|
145
77
|
})),
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
rawLLMResponse: record.rawLLMResponse || null,
|
|
150
|
-
|
|
78
|
+
// ── LLM I/O ─────────────────────────────────────────────────
|
|
79
|
+
systemPrompt: record.systemPrompt ?? null,
|
|
80
|
+
rawLLMResponse: record.rawLLMResponse ?? null,
|
|
151
81
|
// ── Result ──────────────────────────────────────────────────
|
|
152
|
-
messages: record.messages
|
|
153
|
-
validation: record.validation
|
|
154
|
-
drift: record.drift
|
|
155
|
-
suggestions: record.suggestions
|
|
156
|
-
|
|
82
|
+
messages: record.messages ?? [],
|
|
83
|
+
validation: record.validation ?? null,
|
|
84
|
+
drift: record.drift ?? null,
|
|
85
|
+
suggestions: record.suggestions ?? [],
|
|
157
86
|
// ── Telemetry ───────────────────────────────────────────────
|
|
158
|
-
timing: record.timing
|
|
159
|
-
tokens: record.tokens
|
|
160
|
-
|
|
161
|
-
// monolithic: nothing yet — but reserved). Whatever the engine put on
|
|
162
|
-
// result._debug above and beyond the standard fields lands here.
|
|
163
|
-
engineDebug: record.engineDebug || null,
|
|
87
|
+
timing: record.timing ?? null,
|
|
88
|
+
tokens: record.tokens ?? null,
|
|
89
|
+
engineDebug: record.engineDebug ?? null
|
|
164
90
|
};
|
|
165
|
-
|
|
166
91
|
const filePath = _path.join(sessionDir, fileName);
|
|
167
|
-
await _fs.writeFile(filePath, JSON.stringify(payload, null, 2) +
|
|
92
|
+
await _fs.writeFile(filePath, JSON.stringify(payload, null, 2) + "\n");
|
|
168
93
|
return filePath;
|
|
169
94
|
} catch (err) {
|
|
170
|
-
|
|
171
|
-
console.warn('[dialog-recorder] failed to record turn:', err.message);
|
|
95
|
+
console.warn("[dialog-recorder] failed to record turn:", err.message);
|
|
172
96
|
return null;
|
|
173
97
|
}
|
|
174
98
|
}
|
|
175
|
-
|
|
176
|
-
/** True when logging is on. Useful for guarding expensive capture work.
|
|
177
|
-
*
|
|
178
|
-
* Also returns true when `A2UI_COMPOSE_TRACE` is set, since the compose-trace
|
|
179
|
-
* flag in `compose/core/generator.js` consumes the same `_debug` payload —
|
|
180
|
-
* if we don't force-enable here, traces would be empty when the dialog
|
|
181
|
-
* recorder is off (the common case).
|
|
182
|
-
*/
|
|
183
|
-
export function isRecording() {
|
|
184
|
-
// ENABLED is already gated by IS_NODE at module init. The A2UI_COMPOSE_TRACE
|
|
185
|
-
// bypass must guard `process` again — without it, browser callers
|
|
186
|
-
// (compose/core/generator.js → generate-pro.js → isRecording()) throw
|
|
187
|
-
// `ReferenceError: process is not defined` after a successful LLM round-trip.
|
|
99
|
+
function isRecording() {
|
|
188
100
|
if (ENABLED) return true;
|
|
189
|
-
const
|
|
190
|
-
return
|
|
101
|
+
const IS_NODE_RT = typeof process !== "undefined" && process.versions?.node;
|
|
102
|
+
return IS_NODE_RT ? !!process?.env?.["A2UI_COMPOSE_TRACE"] : false;
|
|
191
103
|
}
|
|
104
|
+
export {
|
|
105
|
+
isRecording,
|
|
106
|
+
recordTurn
|
|
107
|
+
};
|
|
@@ -19,19 +19,22 @@ const IS_NODE = typeof process !== 'undefined' && !!process.versions?.node;
|
|
|
19
19
|
const ENABLED = IS_NODE && (process?.env?.['ADIA_LOG_DIALOGS'] === '1' || process?.env?.['ADIA_LOG_DIALOGS'] === 'true');
|
|
20
20
|
|
|
21
21
|
// Lazy-loaded Node modules (only when ENABLED)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
type _FsModule = { readFile(p: string, enc: string): Promise<string>; writeFile(p: string, d: string): Promise<void>; mkdir(p: string, opts?: { recursive?: boolean }): Promise<string | undefined> };
|
|
23
|
+
type _PathModule = { dirname(p: string): string; resolve(...args: string[]): string; join(...args: string[]): string };
|
|
24
|
+
type _UrlModule = { fileURLToPath(u: string): string };
|
|
25
|
+
let _fs: _FsModule | null = null;
|
|
26
|
+
let _path: _PathModule | null = null;
|
|
27
|
+
let _url: _UrlModule | null = null;
|
|
25
28
|
let _logsRoot: string | null = null;
|
|
26
29
|
|
|
27
30
|
async function _ensureModules(): Promise<void> {
|
|
28
31
|
if (_fs) return;
|
|
29
|
-
_fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
30
|
-
_path = await import(/* @vite-ignore */ 'node:path');
|
|
31
|
-
_url = await import(/* @vite-ignore */ 'node:url');
|
|
32
|
+
_fs = await import(/* @vite-ignore */ 'node:fs/promises') as unknown as _FsModule;
|
|
33
|
+
_path = await import(/* @vite-ignore */ 'node:path') as unknown as _PathModule;
|
|
34
|
+
_url = await import(/* @vite-ignore */ 'node:url') as unknown as _UrlModule;
|
|
32
35
|
// logs/ lives at the repo root: packages/a2ui/retrieval/feedback → up 4 → repo root
|
|
33
|
-
const __dirname = _path
|
|
34
|
-
_logsRoot = _path
|
|
36
|
+
const __dirname = _path!.dirname(_url!.fileURLToPath(import.meta.url));
|
|
37
|
+
_logsRoot = _path!.resolve(__dirname, '..', '..', '..', '..', 'logs', 'dialogs');
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
// In-memory turn counter per session
|