@adia-ai/a2ui-retrieval 0.0.1
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 +50 -0
- package/README.md +40 -0
- package/anti-patterns.js +148 -0
- package/catalog.js +215 -0
- package/clarity.js +207 -0
- package/component-entry.js +80 -0
- package/concept-mapper.js +127 -0
- package/context-assembler.js +168 -0
- package/decomposer.js +216 -0
- package/dialog-recorder.js +179 -0
- package/domain-router.js +172 -0
- package/embedding-provider.js +108 -0
- package/embedding-retriever.js +120 -0
- package/feedback-analyzer.js +235 -0
- package/feedback-store.js +175 -0
- package/feedback.js +198 -0
- package/gap-registry.js +121 -0
- package/index.js +16 -0
- package/intent-alignment.js +243 -0
- package/intent-categorizer.js +97 -0
- package/intent-gate.js +155 -0
- package/package.json +29 -0
- package/pattern-library.js +659 -0
- package/pattern-promotion.js +135 -0
- package/prompt-analyzer.js +211 -0
- package/synthetic-data.js +446 -0
- package/web-research.js +186 -0
- package/wiring-catalog.js +195 -0
package/gap-registry.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gap Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages a persistent JSON file tracking identified pattern/training gaps.
|
|
5
|
+
* Used by the feedback pipeline to record and track resolution of weak areas.
|
|
6
|
+
*
|
|
7
|
+
* Registry file: packages/a2ui/corpus/gaps/registry.json
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { loadGaps, addGap, updateGapStatus } from './gap-registry.js';
|
|
11
|
+
* const gaps = await loadGaps();
|
|
12
|
+
* await addGap({ intentCategory: 'form/checkout', ... });
|
|
13
|
+
* await updateGapStatus('form/checkout', 'resolved', 'Added checkout pattern');
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
let fs, path;
|
|
17
|
+
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
18
|
+
if (IS_NODE) {
|
|
19
|
+
try {
|
|
20
|
+
fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
21
|
+
path = await import(/* @vite-ignore */ 'node:path');
|
|
22
|
+
} catch {
|
|
23
|
+
// Node builtins unavailable
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const REGISTRY_PATH = path
|
|
28
|
+
? path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', 'a2ui/corpus', 'gaps', 'registry.json')
|
|
29
|
+
: null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load all gaps from the registry file.
|
|
33
|
+
* @returns {Promise<object[]>}
|
|
34
|
+
*/
|
|
35
|
+
export async function loadGaps() {
|
|
36
|
+
if (!fs || !REGISTRY_PATH) return [];
|
|
37
|
+
try {
|
|
38
|
+
const content = await fs.readFile(REGISTRY_PATH, 'utf8');
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Save the full gaps array to disk.
|
|
47
|
+
* @param {object[]} gaps
|
|
48
|
+
*/
|
|
49
|
+
export async function saveGaps(gaps) {
|
|
50
|
+
if (!fs || !REGISTRY_PATH) return;
|
|
51
|
+
const dir = path.dirname(REGISTRY_PATH);
|
|
52
|
+
await fs.mkdir(dir, { recursive: true });
|
|
53
|
+
await fs.writeFile(REGISTRY_PATH, JSON.stringify(gaps, null, 2) + '\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add a new gap to the registry. Merges with existing if same intentCategory.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} gap
|
|
60
|
+
* @param {string} gap.intentCategory
|
|
61
|
+
* @param {number} gap.sampleCount
|
|
62
|
+
* @param {number} gap.avgScore
|
|
63
|
+
* @param {number} gap.avgRating
|
|
64
|
+
* @param {string[]} gap.sampleIntents
|
|
65
|
+
*/
|
|
66
|
+
export async function addGap(gap) {
|
|
67
|
+
const gaps = await loadGaps();
|
|
68
|
+
const existing = gaps.find(g => g.intentCategory === gap.intentCategory && g.status !== 'resolved');
|
|
69
|
+
|
|
70
|
+
if (existing) {
|
|
71
|
+
// Merge: update stats, add new sample intents
|
|
72
|
+
existing.sampleCount = gap.sampleCount;
|
|
73
|
+
existing.avgScore = gap.avgScore;
|
|
74
|
+
existing.avgRating = gap.avgRating;
|
|
75
|
+
existing.lastSeen = new Date().toISOString();
|
|
76
|
+
const intentSet = new Set([...existing.sampleIntents, ...(gap.sampleIntents || [])]);
|
|
77
|
+
existing.sampleIntents = [...intentSet].slice(0, 10);
|
|
78
|
+
} else {
|
|
79
|
+
gaps.push({
|
|
80
|
+
intentCategory: gap.intentCategory,
|
|
81
|
+
detectedAt: new Date().toISOString(),
|
|
82
|
+
lastSeen: new Date().toISOString(),
|
|
83
|
+
sampleCount: gap.sampleCount || 0,
|
|
84
|
+
avgScore: gap.avgScore || 0,
|
|
85
|
+
avgRating: gap.avgRating || 0,
|
|
86
|
+
status: 'open',
|
|
87
|
+
resolution: null,
|
|
88
|
+
sampleIntents: (gap.sampleIntents || []).slice(0, 10),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await saveGaps(gaps);
|
|
93
|
+
return gaps;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Update the status of a gap by intent category.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} category — Intent category to update
|
|
100
|
+
* @param {'open'|'in-progress'|'resolved'} status
|
|
101
|
+
* @param {string} [resolution] — Description of how the gap was resolved
|
|
102
|
+
*/
|
|
103
|
+
export async function updateGapStatus(category, status, resolution) {
|
|
104
|
+
const gaps = await loadGaps();
|
|
105
|
+
const gap = gaps.find(g => g.intentCategory === category && g.status !== 'resolved');
|
|
106
|
+
|
|
107
|
+
if (!gap) {
|
|
108
|
+
throw new Error(`No open gap found for category: ${category}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
gap.status = status;
|
|
112
|
+
if (resolution) {
|
|
113
|
+
gap.resolution = resolution;
|
|
114
|
+
}
|
|
115
|
+
if (status === 'resolved') {
|
|
116
|
+
gap.resolvedAt = new Date().toISOString();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await saveGaps(gaps);
|
|
120
|
+
return gap;
|
|
121
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2UI Intelligence System — Public API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { getCatalog, getComponent, getComponentsByCategory, getTraits, getTraitsByCategory, getFullCatalog } from './catalog.js';
|
|
6
|
+
export { serializeEntry } from './component-entry.js';
|
|
7
|
+
export { getAntiPatterns, checkAntiPattern, checkAllAntiPatterns } from './anti-patterns.js';
|
|
8
|
+
export { classifyIntent, getDomain, getAllDomains } from './domain-router.js';
|
|
9
|
+
export { isConversational } from './intent-gate.js';
|
|
10
|
+
export { assessClarity } from './clarity.js';
|
|
11
|
+
export { detectReferences, researchIntent } from './web-research.js';
|
|
12
|
+
export { assembleContext } from './context-assembler.js';
|
|
13
|
+
export { getPattern, searchPatterns, semanticSearchPatterns, getAllPatterns, registerPattern } from './pattern-library.js';
|
|
14
|
+
export { extractExpectations, verifyAlignment, checkIntentAlignment } from './intent-alignment.js';
|
|
15
|
+
export { decomposeIntent, composeSubtasks } from './decomposer.js';
|
|
16
|
+
export { getWiringCatalog, getControllerInfo, getHandlerInfo } from './wiring-catalog.js';
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent Alignment Verification — checks whether generated output
|
|
3
|
+
* actually contains what the user asked for.
|
|
4
|
+
*
|
|
5
|
+
* Extracts expectations from the intent (field labels, component types,
|
|
6
|
+
* quantities, action verbs) and verifies them against the A2UI output.
|
|
7
|
+
*
|
|
8
|
+
* Returns a structured alignment report with per-expectation pass/fail,
|
|
9
|
+
* an overall alignment score (0-1), and specific gaps.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Expectation Extractors ──────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** Field/label expectations — "email", "password", "name", etc. */
|
|
15
|
+
const FIELD_PATTERNS = [
|
|
16
|
+
/\b(email|password|username|name|phone|address|city|state|zip|country|company|title|role|bio|url|website|date|time|description|message|subject|amount|price|quantity|search|notes?)\b/gi,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/** Component type expectations — "button", "form", "table", etc. */
|
|
20
|
+
const COMPONENT_PATTERNS = [
|
|
21
|
+
/\b(button|form|table|chart|card|avatar|badge|alert|modal|drawer|tabs?|sidebar|navbar|breadcrumb|pagination|progress|slider|toggle|checkbox|radio|dropdown|select|upload|calendar|timeline|steps?|accordion)\b/gi,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** Quantity expectations — "3 cards", "two columns", etc. */
|
|
25
|
+
const QUANTITY_MAP = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 };
|
|
26
|
+
const QUANTITY_PATTERN = /\b(\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+(cards?|columns?|items?|fields?|buttons?|sections?|rows?|tiles?|steps?|tabs?|metrics?|stats?)\b/gi;
|
|
27
|
+
|
|
28
|
+
/** Action expectations — "submit", "cancel", "delete", etc. */
|
|
29
|
+
const ACTION_PATTERNS = [
|
|
30
|
+
/\b(submit|cancel|save|delete|edit|close|open|search|filter|sort|login|signup|register|checkout|confirm|reset|send|upload|download|share|copy|print)\b/gi,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Specific content expectations — quoted strings, dollar amounts, percentages */
|
|
34
|
+
const CONTENT_PATTERNS = [
|
|
35
|
+
/["']([^"']+)["']/g, // quoted strings
|
|
36
|
+
/\$[\d,.]+/g, // dollar amounts
|
|
37
|
+
/\d+%/g, // percentages
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract expectations from a natural language intent.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} intent
|
|
44
|
+
* @returns {{ fields: string[], componentTypes: string[], quantities: { count: number, type: string }[], actions: string[], content: string[] }}
|
|
45
|
+
*/
|
|
46
|
+
export function extractExpectations(intent) {
|
|
47
|
+
const lower = intent.toLowerCase();
|
|
48
|
+
|
|
49
|
+
// Fields
|
|
50
|
+
const fields = [];
|
|
51
|
+
for (const pattern of FIELD_PATTERNS) {
|
|
52
|
+
pattern.lastIndex = 0;
|
|
53
|
+
let match;
|
|
54
|
+
while ((match = pattern.exec(lower))) {
|
|
55
|
+
const f = match[1].toLowerCase();
|
|
56
|
+
if (!fields.includes(f)) fields.push(f);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Component types
|
|
61
|
+
const componentTypes = [];
|
|
62
|
+
for (const pattern of COMPONENT_PATTERNS) {
|
|
63
|
+
pattern.lastIndex = 0;
|
|
64
|
+
let match;
|
|
65
|
+
while ((match = pattern.exec(lower))) {
|
|
66
|
+
const t = match[1].toLowerCase();
|
|
67
|
+
if (!componentTypes.includes(t)) componentTypes.push(t);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Quantities
|
|
72
|
+
const quantities = [];
|
|
73
|
+
QUANTITY_PATTERN.lastIndex = 0;
|
|
74
|
+
let qMatch;
|
|
75
|
+
while ((qMatch = QUANTITY_PATTERN.exec(lower))) {
|
|
76
|
+
const num = QUANTITY_MAP[qMatch[1]] || parseInt(qMatch[1], 10);
|
|
77
|
+
const type = qMatch[2].replace(/s$/, ''); // singularize
|
|
78
|
+
if (!isNaN(num)) quantities.push({ count: num, type });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Actions
|
|
82
|
+
const actions = [];
|
|
83
|
+
for (const pattern of ACTION_PATTERNS) {
|
|
84
|
+
pattern.lastIndex = 0;
|
|
85
|
+
let match;
|
|
86
|
+
while ((match = pattern.exec(lower))) {
|
|
87
|
+
const a = match[1].toLowerCase();
|
|
88
|
+
if (!actions.includes(a)) actions.push(a);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Specific content
|
|
93
|
+
const content = [];
|
|
94
|
+
for (const pattern of CONTENT_PATTERNS) {
|
|
95
|
+
pattern.lastIndex = 0;
|
|
96
|
+
let match;
|
|
97
|
+
while ((match = pattern.exec(intent))) {
|
|
98
|
+
content.push(match[1] || match[0]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { fields, componentTypes, quantities, actions, content };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── A2UI Type Mapping ───────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/** Map intent keywords to A2UI component types */
|
|
108
|
+
const TYPE_MAP = {
|
|
109
|
+
button: ['Button'],
|
|
110
|
+
form: ['FormContainer', 'TextField', 'Column'],
|
|
111
|
+
table: ['Table'],
|
|
112
|
+
chart: ['Chart'],
|
|
113
|
+
card: ['Card'],
|
|
114
|
+
avatar: ['Avatar'],
|
|
115
|
+
badge: ['Badge'],
|
|
116
|
+
alert: ['Alert'],
|
|
117
|
+
modal: ['Modal', 'Dialog'],
|
|
118
|
+
drawer: ['Drawer'],
|
|
119
|
+
tab: ['Tabs', 'Tab'],
|
|
120
|
+
sidebar: ['Sidebar'],
|
|
121
|
+
navbar: ['Nav'],
|
|
122
|
+
breadcrumb: ['Breadcrumb'],
|
|
123
|
+
pagination: ['Pagination'],
|
|
124
|
+
progress: ['Progress'],
|
|
125
|
+
slider: ['Slider'],
|
|
126
|
+
toggle: ['Toggle'],
|
|
127
|
+
checkbox: ['CheckBox'],
|
|
128
|
+
radio: ['Radio'],
|
|
129
|
+
dropdown: ['ChoicePicker', 'Select'],
|
|
130
|
+
select: ['ChoicePicker', 'Select'],
|
|
131
|
+
upload: ['Upload'],
|
|
132
|
+
calendar: ['CalendarPicker', 'DateTimeInput'],
|
|
133
|
+
timeline: ['Timeline'],
|
|
134
|
+
step: ['Steps'],
|
|
135
|
+
accordion: ['Accordion'],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Verify generated A2UI output against extracted expectations.
|
|
140
|
+
*
|
|
141
|
+
* @param {object[]} components — Flat adjacency array from messages[0].components
|
|
142
|
+
* @param {{ fields: string[], componentTypes: string[], quantities: { count: number, type: string }[], actions: string[], content: string[] }} expectations
|
|
143
|
+
* @returns {{ score: number, checks: { category: string, expected: string, found: boolean, detail: string }[], gaps: string[] }}
|
|
144
|
+
*/
|
|
145
|
+
export function verifyAlignment(components, expectations) {
|
|
146
|
+
const checks = [];
|
|
147
|
+
const gaps = [];
|
|
148
|
+
|
|
149
|
+
// Collect all text content and properties from components
|
|
150
|
+
const allText = components.map(c => {
|
|
151
|
+
const texts = [c.textContent, c.text, c.label, c.placeholder, c.description, c.name];
|
|
152
|
+
return texts.filter(Boolean).join(' ').toLowerCase();
|
|
153
|
+
}).join(' ');
|
|
154
|
+
|
|
155
|
+
const allTypes = new Set(components.map(c => c.component));
|
|
156
|
+
|
|
157
|
+
// ── Check fields ──
|
|
158
|
+
for (const field of expectations.fields) {
|
|
159
|
+
const found = allText.includes(field) ||
|
|
160
|
+
components.some(c => (c.label || '').toLowerCase().includes(field) ||
|
|
161
|
+
(c.placeholder || '').toLowerCase().includes(field) ||
|
|
162
|
+
(c.name || '').toLowerCase().includes(field));
|
|
163
|
+
checks.push({
|
|
164
|
+
category: 'field',
|
|
165
|
+
expected: field,
|
|
166
|
+
found,
|
|
167
|
+
detail: found ? `Found "${field}" in component properties` : `Missing field: "${field}"`,
|
|
168
|
+
});
|
|
169
|
+
if (!found) gaps.push(`Missing field "${field}"`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Check component types ──
|
|
173
|
+
for (const type of expectations.componentTypes) {
|
|
174
|
+
const mappedTypes = TYPE_MAP[type] || [type.charAt(0).toUpperCase() + type.slice(1)];
|
|
175
|
+
const found = mappedTypes.some(t => allTypes.has(t));
|
|
176
|
+
checks.push({
|
|
177
|
+
category: 'componentType',
|
|
178
|
+
expected: type,
|
|
179
|
+
found,
|
|
180
|
+
detail: found ? `Found ${type} component` : `Missing component type: ${type}`,
|
|
181
|
+
});
|
|
182
|
+
if (!found) gaps.push(`Missing ${type} component`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Check quantities ──
|
|
186
|
+
for (const { count, type } of expectations.quantities) {
|
|
187
|
+
const mappedTypes = TYPE_MAP[type] || [type.charAt(0).toUpperCase() + type.slice(1)];
|
|
188
|
+
const actual = components.filter(c => mappedTypes.some(t => c.component === t)).length;
|
|
189
|
+
const found = actual >= count;
|
|
190
|
+
checks.push({
|
|
191
|
+
category: 'quantity',
|
|
192
|
+
expected: `${count} ${type}(s)`,
|
|
193
|
+
found,
|
|
194
|
+
detail: found ? `Found ${actual} ${type}(s) (expected ${count})` : `Only ${actual} ${type}(s), expected ${count}`,
|
|
195
|
+
});
|
|
196
|
+
if (!found) gaps.push(`Expected ${count} ${type}(s), found ${actual}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Check actions ──
|
|
200
|
+
for (const action of expectations.actions) {
|
|
201
|
+
const found = allText.includes(action) ||
|
|
202
|
+
components.some(c => c.component === 'Button' && (c.text || '').toLowerCase().includes(action));
|
|
203
|
+
checks.push({
|
|
204
|
+
category: 'action',
|
|
205
|
+
expected: action,
|
|
206
|
+
found,
|
|
207
|
+
detail: found ? `Found "${action}" action` : `Missing action: "${action}"`,
|
|
208
|
+
});
|
|
209
|
+
if (!found) gaps.push(`Missing "${action}" action`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Check specific content ──
|
|
213
|
+
for (const text of expectations.content) {
|
|
214
|
+
const found = allText.includes(text.toLowerCase());
|
|
215
|
+
checks.push({
|
|
216
|
+
category: 'content',
|
|
217
|
+
expected: text,
|
|
218
|
+
found,
|
|
219
|
+
detail: found ? `Found content "${text}"` : `Missing content: "${text}"`,
|
|
220
|
+
});
|
|
221
|
+
if (!found) gaps.push(`Missing content "${text}"`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Score ──
|
|
225
|
+
const total = checks.length;
|
|
226
|
+
const passed = checks.filter(c => c.found).length;
|
|
227
|
+
const score = total > 0 ? Math.round((passed / total) * 100) / 100 : 1;
|
|
228
|
+
|
|
229
|
+
return { score, checks, gaps };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Full intent alignment check — extract + verify in one call.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} intent
|
|
236
|
+
* @param {object[]} components
|
|
237
|
+
* @returns {{ score: number, checks: object[], gaps: string[], expectations: object }}
|
|
238
|
+
*/
|
|
239
|
+
export function checkIntentAlignment(intent, components) {
|
|
240
|
+
const expectations = extractExpectations(intent);
|
|
241
|
+
const result = verifyAlignment(components, expectations);
|
|
242
|
+
return { ...result, expectations };
|
|
243
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent Categorizer
|
|
3
|
+
*
|
|
4
|
+
* Maps free-text intents to a taxonomy of UI categories using keyword matching.
|
|
5
|
+
* Used by the feedback analyzer to aggregate metrics per intent type.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CATEGORY_RULES = [
|
|
9
|
+
// Form categories
|
|
10
|
+
{ category: 'form/login', keywords: ['login', 'sign in', 'signin', 'log in', 'authentication'] },
|
|
11
|
+
{ category: 'form/signup', keywords: ['signup', 'sign up', 'register', 'registration', 'create account', 'onboarding'] },
|
|
12
|
+
{ category: 'form/contact', keywords: ['contact', 'contact us', 'reach out', 'get in touch', 'enquiry', 'inquiry'] },
|
|
13
|
+
{ category: 'form/settings', keywords: ['settings', 'preferences', 'config', 'configuration', 'account settings', 'profile edit'] },
|
|
14
|
+
{ category: 'form/checkout', keywords: ['checkout', 'check out', 'payment', 'billing', 'purchase'] },
|
|
15
|
+
|
|
16
|
+
// Data categories
|
|
17
|
+
{ category: 'data/table', keywords: ['table', 'data table', 'spreadsheet', 'grid view', 'list view', 'datagrid'] },
|
|
18
|
+
{ category: 'data/dashboard', keywords: ['dashboard', 'kpi', 'metrics', 'analytics', 'overview', 'summary panel', 'stats'] },
|
|
19
|
+
{ category: 'data/chart', keywords: ['chart', 'graph', 'visualization', 'pie chart', 'bar chart', 'line chart', 'histogram'] },
|
|
20
|
+
|
|
21
|
+
// Layout categories
|
|
22
|
+
{ category: 'layout/landing', keywords: ['landing', 'landing page', 'homepage', 'hero', 'splash'] },
|
|
23
|
+
{ category: 'layout/profile', keywords: ['profile', 'user profile', 'avatar', 'bio', 'about me'] },
|
|
24
|
+
{ category: 'layout/pricing', keywords: ['pricing', 'pricing table', 'plans', 'subscription', 'tier'] },
|
|
25
|
+
|
|
26
|
+
// Navigation categories
|
|
27
|
+
{ category: 'nav/sidebar', keywords: ['sidebar', 'side nav', 'navigation menu', 'drawer', 'side panel'] },
|
|
28
|
+
{ category: 'nav/tabs', keywords: ['tabs', 'tab bar', 'tabbed', 'tab navigation', 'tab panel'] },
|
|
29
|
+
|
|
30
|
+
// Agent categories
|
|
31
|
+
{ category: 'agent/chat', keywords: ['chat', 'chatbot', 'messenger', 'conversation', 'messaging', 'chat interface'] },
|
|
32
|
+
{ category: 'agent/notification', keywords: ['notification', 'alert', 'toast', 'snackbar', 'banner', 'announcement'] },
|
|
33
|
+
|
|
34
|
+
// Content categories
|
|
35
|
+
{ category: 'content/blog', keywords: ['blog', 'article', 'post', 'news', 'editorial', 'content feed'] },
|
|
36
|
+
{ category: 'content/faq', keywords: ['faq', 'frequently asked', 'questions', 'help center', 'knowledge base', 'accordion'] },
|
|
37
|
+
|
|
38
|
+
// Commerce categories
|
|
39
|
+
{ category: 'commerce/product', keywords: ['product', 'product card', 'product page', 'product detail', 'item detail', 'catalog'] },
|
|
40
|
+
{ category: 'commerce/cart', keywords: ['cart', 'shopping cart', 'basket', 'bag'] },
|
|
41
|
+
{ category: 'commerce/order', keywords: ['order', 'order history', 'order summary', 'receipt', 'invoice', 'order tracking'] },
|
|
42
|
+
|
|
43
|
+
// Workflow categories
|
|
44
|
+
{ category: 'workflow/wizard', keywords: ['wizard', 'stepper', 'multi-step', 'step form', 'onboard flow', 'setup wizard'] },
|
|
45
|
+
{ category: 'workflow/kanban', keywords: ['kanban', 'board', 'task board', 'trello', 'project board', 'drag and drop'] },
|
|
46
|
+
|
|
47
|
+
// Status categories
|
|
48
|
+
{ category: 'status/error', keywords: ['error', 'error page', '404', '500', 'not found', 'something went wrong'] },
|
|
49
|
+
{ category: 'status/empty', keywords: ['empty state', 'no results', 'no data', 'zero state', 'blank slate'] },
|
|
50
|
+
{ category: 'status/loading', keywords: ['loading', 'skeleton', 'spinner', 'progress', 'placeholder'] },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Categorize a free-text intent into a UI taxonomy category.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} intent — Free-text intent string
|
|
57
|
+
* @returns {{ category: string, confidence: number }}
|
|
58
|
+
*/
|
|
59
|
+
export function categorizeIntent(intent) {
|
|
60
|
+
if (!intent || typeof intent !== 'string') {
|
|
61
|
+
return { category: 'other', confidence: 0 };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lower = intent.toLowerCase().trim();
|
|
65
|
+
let bestCategory = 'other';
|
|
66
|
+
let bestScore = 0;
|
|
67
|
+
|
|
68
|
+
for (const rule of CATEGORY_RULES) {
|
|
69
|
+
let matchCount = 0;
|
|
70
|
+
let longestMatch = 0;
|
|
71
|
+
|
|
72
|
+
for (const kw of rule.keywords) {
|
|
73
|
+
if (lower.includes(kw)) {
|
|
74
|
+
matchCount++;
|
|
75
|
+
longestMatch = Math.max(longestMatch, kw.length);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (matchCount === 0) continue;
|
|
80
|
+
|
|
81
|
+
// Score: multi-word keyword matches get higher confidence,
|
|
82
|
+
// more matches within a category = higher confidence
|
|
83
|
+
const kwLenBonus = longestMatch / lower.length; // longer keyword relative to intent = more specific
|
|
84
|
+
const multiMatchBonus = Math.min(matchCount * 0.15, 0.3);
|
|
85
|
+
const score = 0.5 + kwLenBonus * 0.3 + multiMatchBonus;
|
|
86
|
+
|
|
87
|
+
if (score > bestScore) {
|
|
88
|
+
bestScore = score;
|
|
89
|
+
bestCategory = rule.category;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
category: bestCategory,
|
|
95
|
+
confidence: Math.min(Math.round(bestScore * 100) / 100, 1),
|
|
96
|
+
};
|
|
97
|
+
}
|
package/intent-gate.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent Gate — Detect whether a user message is conversational (question,
|
|
3
|
+
* greeting, meta-question) vs. a UI generation request.
|
|
4
|
+
*
|
|
5
|
+
* Used by the chat setup to route non-generation messages to a text reply
|
|
6
|
+
* instead of the full 6-stage pipeline.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Question starters that signal conversational intent */
|
|
10
|
+
const QUESTION_STARTERS = /^\s*(what|how|why|can you|could you|tell me|explain|describe|help|who|where|when|is there|are there|do you|does|did|will|should|would|which|list|show me how)\b/i;
|
|
11
|
+
|
|
12
|
+
/** Greetings and social signals */
|
|
13
|
+
const GREETINGS = /^\s*(hi|hey|hello|thanks|thank you|bye|goodbye|ok|okay|sure|great|nice|cool|awesome|good|perfect)\b/i;
|
|
14
|
+
|
|
15
|
+
/** Explicit meta-questions about the system itself */
|
|
16
|
+
const META_PATTERNS = [
|
|
17
|
+
/what\s+(components?|types?|elements?)\s+(are|do|can)/i,
|
|
18
|
+
/available\s+(components?|types?|elements?)/i,
|
|
19
|
+
/how\s+(does|do)\s+(this|it|the system|gen ui|a2ui)/i,
|
|
20
|
+
/what\s+can\s+(you|this|it)\s+(do|generate|create|build)/i,
|
|
21
|
+
/help\s+me\s+understand/i,
|
|
22
|
+
/list\s+(of\s+)?(components?|types?|elements?)/i,
|
|
23
|
+
/what\s+(is|are)\s+(a2ui|agentui|agent ui|gen ui)/i,
|
|
24
|
+
/show\s+me\s+(the\s+)?(components?|options?|catalog)/i,
|
|
25
|
+
/explain\s+(the|how|what|a)/i,
|
|
26
|
+
/how\s+(to|do\s+I)\s+(use|create|build|make)/i,
|
|
27
|
+
/what\s+would\s+.+\s+look\s+like/i,
|
|
28
|
+
/show\s+me\s+how/i,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** Content that is clearly not a UI intent (injection, noise, gibberish) */
|
|
32
|
+
const NOISE_PATTERNS = [
|
|
33
|
+
/^<[^>]+>/, // Starts with HTML tag
|
|
34
|
+
/^\s*[^\w\s]{3,}/, // Starts with 3+ non-word chars
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** Vague, motivational, or abstract phrases with no UI-describing language.
|
|
38
|
+
* These are common conversational inputs that the LLM hallucinates UIs from. */
|
|
39
|
+
const VAGUE_PATTERNS = [
|
|
40
|
+
/^(do|just|go|let'?s|get|keep|stay|stop|try|move|make it|finish|start|begin|end|run)\b.{0,30}$/i, // short imperative without UI nouns
|
|
41
|
+
/\b(you gotta|gotta do|no excuses|believe|motivat|inspir|goal|dream|life|love|hate|feel|think about)\b/i,
|
|
42
|
+
/\b(what is preventing|what stops|why can't|why won't)\b/i, // philosophical/motivational questions
|
|
43
|
+
/\b(do it|get it done|let'?s go|make it happen|just do it|never give up)\b/i, // motivational slogans
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Tool-use and action-oriented intents that should be handled conversationally,
|
|
47
|
+
* not as UI generation requests. These are commands directed at the system. */
|
|
48
|
+
const TOOL_USE_PATTERNS = [
|
|
49
|
+
/\b(create|file|write|open|submit|log)\s+(a\s+)?(ticket|bug|issue|report|feature request)/i,
|
|
50
|
+
/\b(report|flag)\s+(a\s+)?(bug|issue|problem|error|regression)/i,
|
|
51
|
+
/\b(why|how come)\s+(does|did|is|was|are)\b/i, // "why does X do Y" — asking for rationale
|
|
52
|
+
/\bthis\s+(is|looks?)\s+(broken|wrong|buggy|off|weird|strange)/i,
|
|
53
|
+
/\b(fix|repair|debug|investigate)\s+(this|the|that)/i,
|
|
54
|
+
/\bsomething\s+(is|seems|looks?)\s+(wrong|broken|off)/i,
|
|
55
|
+
/\bmark\s+(this|it)\s+as\b/i,
|
|
56
|
+
/\b(save|bookmark|pin)\s+(this|the)\s+(pattern|output|result)/i,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Determine if a user message is conversational (should get a text reply)
|
|
61
|
+
* or a UI generation request (should go through the pipeline).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} text — Raw user input
|
|
64
|
+
* @param {{ confidence: number, matchedSignals: string[] }} classifyResult — From classifyIntent()
|
|
65
|
+
* @returns {{ conversational: boolean, reason: string }}
|
|
66
|
+
*/
|
|
67
|
+
export function isConversational(text, classifyResult) {
|
|
68
|
+
const trimmed = (text ?? '').trim();
|
|
69
|
+
if (!classifyResult || typeof classifyResult !== 'object') {
|
|
70
|
+
classifyResult = { confidence: 0, matchedSignals: [] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Very short messages without generation keywords are likely conversational
|
|
74
|
+
if (trimmed.length < 5 && classifyResult.confidence === 0) {
|
|
75
|
+
return { conversational: true, reason: 'short-input' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Noise/injection detection — not a real intent
|
|
79
|
+
for (const pattern of NOISE_PATTERNS) {
|
|
80
|
+
if (pattern.test(trimmed) && classifyResult.confidence === 0) {
|
|
81
|
+
return { conversational: true, reason: 'noise-input' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Explicit meta-questions about the system → always conversational
|
|
86
|
+
for (const pattern of META_PATTERNS) {
|
|
87
|
+
if (pattern.test(trimmed)) {
|
|
88
|
+
return { conversational: true, reason: 'meta-question' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Tool-use commands (create ticket, report bug, etc.) → conversational
|
|
93
|
+
for (const pattern of TOOL_USE_PATTERNS) {
|
|
94
|
+
if (pattern.test(trimmed)) {
|
|
95
|
+
return { conversational: true, reason: 'tool-use' };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Vague / motivational / abstract input with no or weak domain signals
|
|
100
|
+
// Catches "do what you gotta do", "just do it", "what is preventing you?", etc.
|
|
101
|
+
// These have zero genuine UI intent but the LLM hallucinates full UIs from them.
|
|
102
|
+
if (classifyResult.confidence < 0.2) {
|
|
103
|
+
for (const pattern of VAGUE_PATTERNS) {
|
|
104
|
+
if (pattern.test(trimmed)) {
|
|
105
|
+
return { conversational: true, reason: 'vague-or-abstract' };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Greetings with no generation signals
|
|
111
|
+
if (GREETINGS.test(trimmed) && classifyResult.confidence === 0) {
|
|
112
|
+
return { conversational: true, reason: 'greeting' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// High confidence in a domain → generation request
|
|
116
|
+
if (classifyResult.confidence >= 0.3) {
|
|
117
|
+
return { conversational: false, reason: 'high-domain-confidence' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Has matched at least 2 domain signals → generation
|
|
121
|
+
if (classifyResult.matchedSignals?.length >= 2) {
|
|
122
|
+
return { conversational: false, reason: 'multiple-domain-signals' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Question pattern with single low-confidence signal → conversational
|
|
126
|
+
// Key insight: "explain the card content model" has 1 signal ("card") but
|
|
127
|
+
// the leading "explain" means the user wants information, not generation.
|
|
128
|
+
if (QUESTION_STARTERS.test(trimmed) && classifyResult.matchedSignals?.length <= 1) {
|
|
129
|
+
return { conversational: true, reason: 'question-single-signal' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Greetings even with some confidence → still conversational
|
|
133
|
+
if (GREETINGS.test(trimmed) && classifyResult.confidence < 0.2) {
|
|
134
|
+
return { conversational: true, reason: 'greeting-low-confidence' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Default: if there's any domain signal at all, treat as generation
|
|
138
|
+
if (classifyResult.confidence > 0) {
|
|
139
|
+
return { conversational: false, reason: 'has-domain-signal' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// No signals — check for question marks or question patterns
|
|
143
|
+
if (trimmed.endsWith('?')) {
|
|
144
|
+
return { conversational: true, reason: 'ends-with-question-mark' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// No domain signals at all — this input doesn't describe UI.
|
|
148
|
+
// Phrases like "do what you gotta do", "what is preventing you?", or
|
|
149
|
+
// motivational text have zero keyword overlap with any UI domain.
|
|
150
|
+
// Route to conversational so the system can ask for clarification
|
|
151
|
+
// instead of hallucinating a UI from abstract text.
|
|
152
|
+
// Legitimate UI intents like "sidebar with navigation" will have
|
|
153
|
+
// domain signals (sidebar → layout/navigation) and be caught above.
|
|
154
|
+
return { conversational: true, reason: 'no-ui-signals' };
|
|
155
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adia-ai/a2ui-retrieval",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "AdiaUI A2UI retrieval layer — catalog lookup, intent classification, domain routing, pattern + anti-pattern matching, clarity + context assembly. Consumed by the compose engine and any A2UI-protocol tooling that needs to reason about user intent against the catalog.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./*": "./*.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"*.js",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"registry": "https://registry.npmjs.org"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/adiahealth/gen-ui-kit.git",
|
|
24
|
+
"directory": "packages/a2ui/retrieval"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@adia-ai/a2ui-utils": "^0.0.2"
|
|
28
|
+
}
|
|
29
|
+
}
|