@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.
@@ -1,116 +1,78 @@
1
- /**
2
- * Feedback Analyzer
3
- *
4
- * Reads JSONL feedback files, aggregates by intent category,
5
- * and surfaces weak intents, promotion candidates, and pattern gaps.
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(/* @vite-ignore */ 'node:fs/promises');
23
- path = await import(/* @vite-ignore */ 'node:path');
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
- // packages/a2ui/retrieval/feedback → up 3 → packages/a2ui → corpus/feedback
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
- .filter(f => f.endsWith('.jsonl'))
59
- .filter(f => {
60
- const dateStr = f.replace('.jsonl', '');
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), 'utf8');
68
- const lines = content.trim().split('\n').filter(Boolean);
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._file = file;
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
- // First pass: group executions by category
99
- const executions = entries.filter(e => e.type === 'execution');
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 || 0;
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((bucket.totalRating / bucket.ratingCount) * 10) / 10 : 0,
158
- patternMatchRate: bucket.count > 0 ? Math.round((bucket.patternMatchCount / bucket.count) * 100) : 0,
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
- * Persistent Feedback Store
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(/* @vite-ignore */ 'node:fs/promises');
20
- path = await import(/* @vite-ignore */ 'node:path');
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) + '\n');
26
+ await fs.appendFile(todayFile(), JSON.stringify(entry) + "\n");
41
27
  } catch (e) {
42
- console.warn('FeedbackStore: write failed', e.message);
28
+ console.warn("FeedbackStore: write failed", e.message);
43
29
  }
44
30
  }
45
-
46
- export const feedbackStore = {
47
- /**
48
- * Log a completed generation execution.
49
- */
31
+ const feedbackStore = {
32
+ /** Log a completed generation execution. */
50
33
  async logExecution({
51
- executionId, intent, model, domain, mode,
52
- patternMatch, patternConfidence,
53
- score, componentCount, tokenCount,
54
- meta, // LLM self-critique
55
- messages, // A2UI output (optional — can be large)
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: 'execution',
59
- timestamp: new Date().toISOString(),
60
- executionId, intent, model, domain, mode,
61
- patternMatch, patternConfidence,
62
- score, componentCount, tokenCount,
63
- meta: meta || null,
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: 'rating',
73
- timestamp: new Date().toISOString(),
74
- executionId, rating, intent,
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: 'pattern_save',
84
- timestamp: new Date().toISOString(),
85
- executionId, patternName, intent,
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: 'gap',
95
- gapType: type, // 'pattern' | 'domain' | 'component' | 'prompt'
96
- timestamp: new Date().toISOString(),
97
- description, source, executionId,
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), 'utf8');
116
- const lines = content.trim().split('\n').filter(Boolean).reverse();
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 { entries.push(JSON.parse(line)); } catch {}
106
+ try {
107
+ entries.push(JSON.parse(line));
108
+ } catch {
109
+ }
120
110
  }
121
111
  }
122
112
  return entries;
123
- } catch { return []; }
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.type === 'gap');
120
+ const gaps = entries.filter((e) => e["type"] === "gap");
132
121
  const byType = {};
133
122
  for (const g of gaps) {
134
- byType[g.gapType] = 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.type === 'execution');
146
- const ratings = entries.filter(e => e.type === 'rating');
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 avgScore = executions.reduce((s, e) => s + (e.score || 0), 0) / executions.length;
151
- const avgTokens = executions.reduce((s, e) => s + (e.tokenCount || 0), 0) / executions.length;
152
- const thumbsUp = ratings.filter(r => r.rating >= 4).length;
153
- const thumbsDown = ratings.filter(r => r.rating < 4).length;
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 || 'unknown';
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 || 0;
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