@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.
@@ -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
+ }