@adia-ai/a2ui-retrieval 0.6.2 → 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 +38 -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,116 +1,78 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* import { FeedbackAnalyzer } from './feedback-analyzer.js';
|
|
9
|
-
* const analyzer = new FeedbackAnalyzer();
|
|
10
|
-
* const entries = await analyzer.readRange(30);
|
|
11
|
-
* const aggregated = analyzer.aggregateByIntent(entries);
|
|
12
|
-
* const weak = analyzer.findWeakIntents(aggregated);
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { feedbackStore } from './feedback-store.js';
|
|
16
|
-
import { categorizeIntent } from '../intent/intent-categorizer.js';
|
|
17
|
-
|
|
18
|
-
let fs, path;
|
|
19
|
-
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
1
|
+
import { feedbackStore } from "./feedback-store.js";
|
|
2
|
+
import { categorizeIntent } from "../intent/intent-categorizer.js";
|
|
3
|
+
let fs = null;
|
|
4
|
+
let path = null;
|
|
5
|
+
const IS_NODE = typeof process !== "undefined" && process.versions?.node;
|
|
20
6
|
if (IS_NODE) {
|
|
21
7
|
try {
|
|
22
|
-
fs = await import(
|
|
23
|
-
|
|
8
|
+
fs = await import(
|
|
9
|
+
/* @vite-ignore */
|
|
10
|
+
"node:fs/promises"
|
|
11
|
+
);
|
|
12
|
+
path = await import(
|
|
13
|
+
/* @vite-ignore */
|
|
14
|
+
"node:path"
|
|
15
|
+
);
|
|
24
16
|
} catch {
|
|
25
|
-
// Node builtins unavailable
|
|
26
17
|
}
|
|
27
18
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const FEEDBACK_DIR = path
|
|
31
|
-
? path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'a2ui/corpus', 'feedback')
|
|
32
|
-
: null;
|
|
33
|
-
|
|
34
|
-
export class FeedbackAnalyzer {
|
|
19
|
+
const FEEDBACK_DIR = path ? path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "..", "a2ui/corpus", "feedback") : null;
|
|
20
|
+
class FeedbackAnalyzer {
|
|
35
21
|
/**
|
|
36
22
|
* Read JSONL feedback files for the last N days.
|
|
37
|
-
*
|
|
38
|
-
* @param {number} days — Number of days to look back (default 30)
|
|
39
|
-
* @returns {Promise<object[]>} — Array of parsed feedback entries
|
|
40
23
|
*/
|
|
41
24
|
async readRange(days = 30) {
|
|
42
25
|
if (!fs || !FEEDBACK_DIR) return [];
|
|
43
|
-
|
|
44
26
|
const entries = [];
|
|
45
|
-
const now = new Date();
|
|
46
|
-
|
|
47
|
-
// Build set of date strings we want
|
|
48
|
-
const dateStrings = new Set();
|
|
27
|
+
const now = /* @__PURE__ */ new Date();
|
|
28
|
+
const dateStrings = /* @__PURE__ */ new Set();
|
|
49
29
|
for (let i = 0; i < days; i++) {
|
|
50
30
|
const d = new Date(now);
|
|
51
31
|
d.setDate(d.getDate() - i);
|
|
52
32
|
dateStrings.add(d.toISOString().slice(0, 10));
|
|
53
33
|
}
|
|
54
|
-
|
|
55
34
|
try {
|
|
56
35
|
const files = await fs.readdir(FEEDBACK_DIR);
|
|
57
|
-
const jsonlFiles = files
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
return dateStrings.has(dateStr);
|
|
62
|
-
})
|
|
63
|
-
.sort();
|
|
64
|
-
|
|
36
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).filter((f) => {
|
|
37
|
+
const dateStr = f.replace(".jsonl", "");
|
|
38
|
+
return dateStrings.has(dateStr);
|
|
39
|
+
}).sort();
|
|
65
40
|
for (const file of jsonlFiles) {
|
|
66
41
|
try {
|
|
67
|
-
const content = await fs.readFile(path.join(FEEDBACK_DIR, file),
|
|
68
|
-
const lines = content.trim().split(
|
|
42
|
+
const content = await fs.readFile(path.join(FEEDBACK_DIR, file), "utf8");
|
|
43
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
69
44
|
for (const line of lines) {
|
|
70
45
|
try {
|
|
71
46
|
const entry = JSON.parse(line);
|
|
72
|
-
entry
|
|
47
|
+
entry["_file"] = file;
|
|
73
48
|
entries.push(entry);
|
|
74
49
|
} catch {
|
|
75
|
-
// Skip malformed lines
|
|
76
50
|
}
|
|
77
51
|
}
|
|
78
52
|
} catch {
|
|
79
|
-
// Skip unreadable files
|
|
80
53
|
}
|
|
81
54
|
}
|
|
82
55
|
} catch {
|
|
83
|
-
// Feedback dir doesn't exist yet
|
|
84
56
|
}
|
|
85
|
-
|
|
86
57
|
return entries;
|
|
87
58
|
}
|
|
88
|
-
|
|
89
59
|
/**
|
|
90
60
|
* Aggregate feedback entries by intent category.
|
|
91
|
-
*
|
|
92
|
-
* @param {object[]} entries — Array of feedback entries
|
|
93
|
-
* @returns {Map<string, { count: number, avgScore: number, avgRating: number, patternMatchRate: number, entries: object[] }>}
|
|
94
61
|
*/
|
|
95
62
|
aggregateByIntent(entries) {
|
|
96
|
-
const buckets = new Map();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const ratings = entries.filter(e => e.type === 'rating');
|
|
101
|
-
|
|
102
|
-
// Index ratings by executionId for fast lookup
|
|
103
|
-
const ratingsByExecId = new Map();
|
|
63
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
64
|
+
const executions = entries.filter((e) => e.type === "execution");
|
|
65
|
+
const ratings = entries.filter((e) => e.type === "rating");
|
|
66
|
+
const ratingsByExecId = /* @__PURE__ */ new Map();
|
|
104
67
|
for (const r of ratings) {
|
|
68
|
+
if (!r.executionId) continue;
|
|
105
69
|
if (!ratingsByExecId.has(r.executionId)) {
|
|
106
70
|
ratingsByExecId.set(r.executionId, []);
|
|
107
71
|
}
|
|
108
72
|
ratingsByExecId.get(r.executionId).push(r);
|
|
109
73
|
}
|
|
110
|
-
|
|
111
74
|
for (const exec of executions) {
|
|
112
|
-
const { category } = categorizeIntent(exec.intent);
|
|
113
|
-
|
|
75
|
+
const { category } = categorizeIntent(exec.intent ?? "");
|
|
114
76
|
if (!buckets.has(category)) {
|
|
115
77
|
buckets.set(category, {
|
|
116
78
|
count: 0,
|
|
@@ -119,57 +81,43 @@ export class FeedbackAnalyzer {
|
|
|
119
81
|
ratingCount: 0,
|
|
120
82
|
patternMatchCount: 0,
|
|
121
83
|
entries: [],
|
|
122
|
-
sampleIntents: []
|
|
84
|
+
sampleIntents: []
|
|
123
85
|
});
|
|
124
86
|
}
|
|
125
|
-
|
|
126
87
|
const bucket = buckets.get(category);
|
|
127
88
|
bucket.count++;
|
|
128
|
-
bucket.totalScore += exec.score
|
|
89
|
+
bucket.totalScore += exec.score ?? 0;
|
|
129
90
|
bucket.entries.push(exec);
|
|
130
|
-
|
|
131
91
|
if (exec.patternMatch) {
|
|
132
92
|
bucket.patternMatchCount++;
|
|
133
93
|
}
|
|
134
|
-
|
|
135
|
-
// Collect unique sample intents (up to 5)
|
|
136
94
|
if (bucket.sampleIntents.length < 5 && exec.intent) {
|
|
137
95
|
const intentLower = exec.intent.toLowerCase();
|
|
138
|
-
if (!bucket.sampleIntents.some(s => s.toLowerCase() === intentLower)) {
|
|
96
|
+
if (!bucket.sampleIntents.some((s) => s.toLowerCase() === intentLower)) {
|
|
139
97
|
bucket.sampleIntents.push(exec.intent);
|
|
140
98
|
}
|
|
141
99
|
}
|
|
142
|
-
|
|
143
|
-
// Attach ratings
|
|
144
|
-
const execRatings = ratingsByExecId.get(exec.executionId) || [];
|
|
100
|
+
const execRatings = ratingsByExecId.get(exec.executionId ?? "") ?? [];
|
|
145
101
|
for (const r of execRatings) {
|
|
146
|
-
bucket.totalRating += r.rating;
|
|
102
|
+
bucket.totalRating += r.rating ?? 0;
|
|
147
103
|
bucket.ratingCount++;
|
|
148
104
|
}
|
|
149
105
|
}
|
|
150
|
-
|
|
151
|
-
// Compute averages
|
|
152
|
-
const result = new Map();
|
|
106
|
+
const result = /* @__PURE__ */ new Map();
|
|
153
107
|
for (const [category, bucket] of buckets) {
|
|
154
108
|
result.set(category, {
|
|
155
109
|
count: bucket.count,
|
|
156
110
|
avgScore: bucket.count > 0 ? Math.round(bucket.totalScore / bucket.count) : 0,
|
|
157
|
-
avgRating: bucket.ratingCount > 0 ? Math.round(
|
|
158
|
-
patternMatchRate: bucket.count > 0 ? Math.round(
|
|
111
|
+
avgRating: bucket.ratingCount > 0 ? Math.round(bucket.totalRating / bucket.ratingCount * 10) / 10 : 0,
|
|
112
|
+
patternMatchRate: bucket.count > 0 ? Math.round(bucket.patternMatchCount / bucket.count * 100) : 0,
|
|
159
113
|
sampleIntents: bucket.sampleIntents,
|
|
160
|
-
entries: bucket.entries
|
|
114
|
+
entries: bucket.entries
|
|
161
115
|
});
|
|
162
116
|
}
|
|
163
|
-
|
|
164
117
|
return result;
|
|
165
118
|
}
|
|
166
|
-
|
|
167
119
|
/**
|
|
168
120
|
* Find intent categories with weak performance.
|
|
169
|
-
*
|
|
170
|
-
* @param {Map} aggregated — Output of aggregateByIntent
|
|
171
|
-
* @param {number} threshold — Score threshold (default 60)
|
|
172
|
-
* @returns {Array<{ category: string, count: number, avgScore: number, avgRating: number, sampleIntents: string[] }>}
|
|
173
121
|
*/
|
|
174
122
|
findWeakIntents(aggregated, threshold = 60) {
|
|
175
123
|
const weak = [];
|
|
@@ -180,19 +128,15 @@ export class FeedbackAnalyzer {
|
|
|
180
128
|
count: data.count,
|
|
181
129
|
avgScore: data.avgScore,
|
|
182
130
|
avgRating: data.avgRating,
|
|
183
|
-
sampleIntents: data.sampleIntents
|
|
131
|
+
sampleIntents: data.sampleIntents
|
|
184
132
|
});
|
|
185
133
|
}
|
|
186
134
|
}
|
|
187
135
|
return weak.sort((a, b) => a.avgScore - b.avgScore);
|
|
188
136
|
}
|
|
189
|
-
|
|
190
137
|
/**
|
|
191
138
|
* Find intent categories ready for pattern promotion.
|
|
192
139
|
* Criteria: avgScore >= 95, avgRating >= 4, count >= 3
|
|
193
|
-
*
|
|
194
|
-
* @param {Map} aggregated — Output of aggregateByIntent
|
|
195
|
-
* @returns {Array<{ category: string, count: number, avgScore: number, avgRating: number, sampleIntents: string[] }>}
|
|
196
140
|
*/
|
|
197
141
|
findPromotionCandidates(aggregated) {
|
|
198
142
|
const candidates = [];
|
|
@@ -204,18 +148,14 @@ export class FeedbackAnalyzer {
|
|
|
204
148
|
avgScore: data.avgScore,
|
|
205
149
|
avgRating: data.avgRating,
|
|
206
150
|
patternMatchRate: data.patternMatchRate,
|
|
207
|
-
sampleIntents: data.sampleIntents
|
|
151
|
+
sampleIntents: data.sampleIntents
|
|
208
152
|
});
|
|
209
153
|
}
|
|
210
154
|
}
|
|
211
155
|
return candidates.sort((a, b) => b.avgScore - a.avgScore);
|
|
212
156
|
}
|
|
213
|
-
|
|
214
157
|
/**
|
|
215
158
|
* Find intent categories with no pattern match AND low scores — gaps in pattern coverage.
|
|
216
|
-
*
|
|
217
|
-
* @param {Map} aggregated — Output of aggregateByIntent
|
|
218
|
-
* @returns {Array<{ category: string, count: number, avgScore: number, patternMatchRate: number, sampleIntents: string[] }>}
|
|
219
159
|
*/
|
|
220
160
|
findPatternGaps(aggregated) {
|
|
221
161
|
const gaps = [];
|
|
@@ -227,10 +167,14 @@ export class FeedbackAnalyzer {
|
|
|
227
167
|
avgScore: data.avgScore,
|
|
228
168
|
avgRating: data.avgRating,
|
|
229
169
|
patternMatchRate: data.patternMatchRate,
|
|
230
|
-
sampleIntents: data.sampleIntents
|
|
170
|
+
sampleIntents: data.sampleIntents
|
|
231
171
|
});
|
|
232
172
|
}
|
|
233
173
|
}
|
|
234
174
|
return gaps.sort((a, b) => a.avgScore - b.avgScore);
|
|
235
175
|
}
|
|
236
176
|
}
|
|
177
|
+
export {
|
|
178
|
+
FeedbackAnalyzer,
|
|
179
|
+
feedbackStore
|
|
180
|
+
};
|
|
@@ -78,8 +78,8 @@ export class FeedbackAnalyzer {
|
|
|
78
78
|
try {
|
|
79
79
|
const files = await fs.readdir(FEEDBACK_DIR);
|
|
80
80
|
const jsonlFiles = files
|
|
81
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
82
|
-
.filter(f => {
|
|
81
|
+
.filter((f: string) => f.endsWith('.jsonl'))
|
|
82
|
+
.filter((f: string) => {
|
|
83
83
|
const dateStr = f.replace('.jsonl', '');
|
|
84
84
|
return dateStrings.has(dateStr);
|
|
85
85
|
})
|
|
@@ -1,167 +1,148 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Writes execution metadata, ratings, LLM self-critique, and gap signals
|
|
5
|
-
* to JSONL files on disk. One file per day. Browser-safe (no-ops if no fs).
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* import { feedbackStore } from './feedback-store.js';
|
|
9
|
-
* feedbackStore.logExecution({ executionId, intent, model, domain, ... });
|
|
10
|
-
* feedbackStore.logRating({ executionId, rating, ... });
|
|
11
|
-
* feedbackStore.logGap({ type: 'pattern', description: '...' });
|
|
12
|
-
* const recent = await feedbackStore.readRecent(50);
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
let fs, path;
|
|
16
|
-
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
1
|
+
let fs = null;
|
|
2
|
+
let path = null;
|
|
3
|
+
const IS_NODE = typeof process !== "undefined" && process.versions?.node;
|
|
17
4
|
if (IS_NODE) {
|
|
18
5
|
try {
|
|
19
|
-
fs = await import(
|
|
20
|
-
|
|
6
|
+
fs = await import(
|
|
7
|
+
/* @vite-ignore */
|
|
8
|
+
"node:fs/promises"
|
|
9
|
+
);
|
|
10
|
+
path = await import(
|
|
11
|
+
/* @vite-ignore */
|
|
12
|
+
"node:path"
|
|
13
|
+
);
|
|
21
14
|
} catch {
|
|
22
|
-
// Node builtins unavailable
|
|
23
15
|
}
|
|
24
16
|
}
|
|
25
|
-
|
|
26
|
-
// packages/a2ui/retrieval/feedback → up 3 → packages/a2ui → corpus/feedback
|
|
27
|
-
const FEEDBACK_DIR = path
|
|
28
|
-
? path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'a2ui/corpus', 'feedback')
|
|
29
|
-
: null;
|
|
30
|
-
|
|
17
|
+
const FEEDBACK_DIR = path ? path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "..", "a2ui/corpus", "feedback") : null;
|
|
31
18
|
function todayFile() {
|
|
32
|
-
const d = new Date().toISOString().slice(0, 10);
|
|
19
|
+
const d = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
33
20
|
return path ? path.join(FEEDBACK_DIR, `${d}.jsonl`) : null;
|
|
34
21
|
}
|
|
35
|
-
|
|
36
22
|
async function append(entry) {
|
|
37
23
|
if (!fs || !FEEDBACK_DIR) return;
|
|
38
24
|
try {
|
|
39
25
|
await fs.mkdir(FEEDBACK_DIR, { recursive: true });
|
|
40
|
-
await fs.appendFile(todayFile(), JSON.stringify(entry) +
|
|
26
|
+
await fs.appendFile(todayFile(), JSON.stringify(entry) + "\n");
|
|
41
27
|
} catch (e) {
|
|
42
|
-
console.warn(
|
|
28
|
+
console.warn("FeedbackStore: write failed", e.message);
|
|
43
29
|
}
|
|
44
30
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Log a completed generation execution.
|
|
49
|
-
*/
|
|
31
|
+
const feedbackStore = {
|
|
32
|
+
/** Log a completed generation execution. */
|
|
50
33
|
async logExecution({
|
|
51
|
-
executionId,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
34
|
+
executionId,
|
|
35
|
+
intent,
|
|
36
|
+
model,
|
|
37
|
+
domain,
|
|
38
|
+
mode,
|
|
39
|
+
patternMatch,
|
|
40
|
+
patternConfidence,
|
|
41
|
+
score,
|
|
42
|
+
componentCount,
|
|
43
|
+
tokenCount,
|
|
44
|
+
meta
|
|
56
45
|
}) {
|
|
57
46
|
await append({
|
|
58
|
-
type:
|
|
59
|
-
timestamp: new Date().toISOString(),
|
|
60
|
-
executionId,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
47
|
+
type: "execution",
|
|
48
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49
|
+
executionId,
|
|
50
|
+
intent,
|
|
51
|
+
model,
|
|
52
|
+
domain,
|
|
53
|
+
mode,
|
|
54
|
+
patternMatch,
|
|
55
|
+
patternConfidence,
|
|
56
|
+
score,
|
|
57
|
+
componentCount,
|
|
58
|
+
tokenCount,
|
|
59
|
+
meta: meta ?? null
|
|
64
60
|
});
|
|
65
61
|
},
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Log a user rating (👍/👎).
|
|
69
|
-
*/
|
|
62
|
+
/** Log a user rating (👍/👎). */
|
|
70
63
|
async logRating({ executionId, rating, intent }) {
|
|
71
64
|
await append({
|
|
72
|
-
type:
|
|
73
|
-
timestamp: new Date().toISOString(),
|
|
74
|
-
executionId,
|
|
65
|
+
type: "rating",
|
|
66
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
67
|
+
executionId,
|
|
68
|
+
rating,
|
|
69
|
+
intent
|
|
75
70
|
});
|
|
76
71
|
},
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Log a pattern save action.
|
|
80
|
-
*/
|
|
72
|
+
/** Log a pattern save action. */
|
|
81
73
|
async logPatternSave({ executionId, patternName, intent }) {
|
|
82
74
|
await append({
|
|
83
|
-
type:
|
|
84
|
-
timestamp: new Date().toISOString(),
|
|
85
|
-
executionId,
|
|
75
|
+
type: "pattern_save",
|
|
76
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
77
|
+
executionId,
|
|
78
|
+
patternName,
|
|
79
|
+
intent
|
|
86
80
|
});
|
|
87
81
|
},
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Log a training gap identified by LLM meta or heuristics.
|
|
91
|
-
*/
|
|
82
|
+
/** Log a training gap identified by LLM meta or heuristics. */
|
|
92
83
|
async logGap({ type, description, source, executionId }) {
|
|
93
84
|
await append({
|
|
94
|
-
type:
|
|
95
|
-
gapType: type,
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
type: "gap",
|
|
86
|
+
gapType: type,
|
|
87
|
+
// 'pattern' | 'domain' | 'component' | 'prompt'
|
|
88
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
89
|
+
description,
|
|
90
|
+
source,
|
|
91
|
+
executionId
|
|
98
92
|
});
|
|
99
93
|
},
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Read recent feedback entries (Node only).
|
|
103
|
-
*/
|
|
94
|
+
/** Read recent feedback entries (Node only). */
|
|
104
95
|
async readRecent(limit = 100) {
|
|
105
96
|
if (!fs || !FEEDBACK_DIR) return [];
|
|
106
97
|
try {
|
|
107
|
-
const files = (await fs.readdir(FEEDBACK_DIR))
|
|
108
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
109
|
-
.sort()
|
|
110
|
-
.reverse();
|
|
111
|
-
|
|
98
|
+
const files = (await fs.readdir(FEEDBACK_DIR)).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
112
99
|
const entries = [];
|
|
113
100
|
for (const file of files) {
|
|
114
101
|
if (entries.length >= limit) break;
|
|
115
|
-
const content = await fs.readFile(path.join(FEEDBACK_DIR, file),
|
|
116
|
-
const lines = content.trim().split(
|
|
102
|
+
const content = await fs.readFile(path.join(FEEDBACK_DIR, file), "utf8");
|
|
103
|
+
const lines = content.trim().split("\n").filter(Boolean).reverse();
|
|
117
104
|
for (const line of lines) {
|
|
118
105
|
if (entries.length >= limit) break;
|
|
119
|
-
try {
|
|
106
|
+
try {
|
|
107
|
+
entries.push(JSON.parse(line));
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
120
110
|
}
|
|
121
111
|
}
|
|
122
112
|
return entries;
|
|
123
|
-
} catch {
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
124
116
|
},
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get gap summary — aggregate gap signals for training improvement.
|
|
128
|
-
*/
|
|
117
|
+
/** Get gap summary — aggregate gap signals for training improvement. */
|
|
129
118
|
async getGapSummary() {
|
|
130
119
|
const entries = await this.readRecent(500);
|
|
131
|
-
const gaps = entries.filter(e => e
|
|
120
|
+
const gaps = entries.filter((e) => e["type"] === "gap");
|
|
132
121
|
const byType = {};
|
|
133
122
|
for (const g of gaps) {
|
|
134
|
-
byType[g.gapType]
|
|
135
|
-
byType[g.gapType].push(g.description);
|
|
123
|
+
if (!byType[g.gapType]) byType[g.gapType] = [];
|
|
124
|
+
byType[g.gapType].push(g.description ?? "");
|
|
136
125
|
}
|
|
137
126
|
return byType;
|
|
138
127
|
},
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Get quality metrics — aggregate from recent executions.
|
|
142
|
-
*/
|
|
128
|
+
/** Get quality metrics — aggregate from recent executions. */
|
|
143
129
|
async getQualityMetrics() {
|
|
144
130
|
const entries = await this.readRecent(500);
|
|
145
|
-
const executions = entries.filter(e => e
|
|
146
|
-
const ratings = entries.filter(e => e
|
|
147
|
-
|
|
131
|
+
const executions = entries.filter((e) => e["type"] === "execution");
|
|
132
|
+
const ratings = entries.filter((e) => e["type"] === "rating");
|
|
148
133
|
if (executions.length === 0) return { executions: 0, avgScore: 0, avgTokens: 0, thumbUpRate: 0 };
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const thumbUpRate = (thumbsUp + thumbsDown) > 0 ? thumbsUp / (thumbsUp + thumbsDown) : 0;
|
|
155
|
-
|
|
156
|
-
// Per-domain breakdown
|
|
134
|
+
const avgScore = executions.reduce((s, e) => s + (e.score ?? 0), 0) / executions.length;
|
|
135
|
+
const avgTokens = executions.reduce((s, e) => s + (e.tokenCount ?? 0), 0) / executions.length;
|
|
136
|
+
const thumbsUp = ratings.filter((r) => r.rating >= 4).length;
|
|
137
|
+
const thumbsDown = ratings.filter((r) => r.rating < 4).length;
|
|
138
|
+
const thumbUpRate = thumbsUp + thumbsDown > 0 ? thumbsUp / (thumbsUp + thumbsDown) : 0;
|
|
157
139
|
const byDomain = {};
|
|
158
140
|
for (const e of executions) {
|
|
159
|
-
const d = e.domain ||
|
|
141
|
+
const d = e.domain || "unknown";
|
|
160
142
|
if (!byDomain[d]) byDomain[d] = { count: 0, totalScore: 0 };
|
|
161
143
|
byDomain[d].count++;
|
|
162
|
-
byDomain[d].totalScore += e.score
|
|
144
|
+
byDomain[d].totalScore += e.score ?? 0;
|
|
163
145
|
}
|
|
164
|
-
|
|
165
146
|
return {
|
|
166
147
|
executions: executions.length,
|
|
167
148
|
avgScore: Math.round(avgScore),
|
|
@@ -170,7 +151,10 @@ export const feedbackStore = {
|
|
|
170
151
|
byDomain: Object.fromEntries(
|
|
171
152
|
Object.entries(byDomain).map(([d, v]) => [d, { count: v.count, avgScore: Math.round(v.totalScore / v.count) }])
|
|
172
153
|
),
|
|
173
|
-
gaps: await this.getGapSummary()
|
|
154
|
+
gaps: await this.getGapSummary()
|
|
174
155
|
};
|
|
175
|
-
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
export {
|
|
159
|
+
feedbackStore
|
|
176
160
|
};
|
|
@@ -155,7 +155,7 @@ export const feedbackStore = {
|
|
|
155
155
|
if (!fs || !FEEDBACK_DIR) return [];
|
|
156
156
|
try {
|
|
157
157
|
const files = (await fs.readdir(FEEDBACK_DIR))
|
|
158
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
158
|
+
.filter((f: string) => f.endsWith('.jsonl'))
|
|
159
159
|
.sort()
|
|
160
160
|
.reverse();
|
|
161
161
|
|