@adia-ai/a2ui-retrieval 0.6.4 → 0.6.7

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/domain-router.js +362 -117
  3. package/embedding/chunk-embedding-retriever.js +47 -79
  4. package/embedding/embedding-provider.js +35 -71
  5. package/embedding/index.js +2 -10
  6. package/feedback/dialog-recorder.js +61 -145
  7. package/feedback/feedback-analyzer.js +46 -102
  8. package/feedback/feedback-store.js +91 -107
  9. package/feedback/feedback.js +36 -117
  10. package/feedback/gap-registry.js +40 -82
  11. package/feedback/index.js +14 -12
  12. package/index.d.ts +4 -0
  13. package/index.js +53 -16
  14. package/intent/clarity.js +61 -129
  15. package/intent/decomposer.js +51 -143
  16. package/intent/index.js +18 -14
  17. package/intent/intent-alignment.js +79 -150
  18. package/intent/intent-categorizer.js +34 -62
  19. package/intent/intent-gate.js +43 -102
  20. package/intent/prompt-analyzer.js +68 -126
  21. package/package.json +4 -2
  22. package/wiring-catalog.js +95 -146
  23. package/embedding/chunk-embedding-retriever.ts +0 -156
  24. package/embedding/embedding-provider.ts +0 -111
  25. package/embedding/index.ts +0 -10
  26. package/feedback/dialog-recorder.ts +0 -172
  27. package/feedback/feedback-analyzer.ts +0 -250
  28. package/feedback/feedback-store.ts +0 -229
  29. package/feedback/feedback.ts +0 -201
  30. package/feedback/gap-registry.ts +0 -137
  31. package/feedback/index.ts +0 -14
  32. package/intent/clarity.ts +0 -224
  33. package/intent/decomposer.ts +0 -229
  34. package/intent/index.ts +0 -20
  35. package/intent/intent-alignment.ts +0 -267
  36. package/intent/intent-categorizer.ts +0 -104
  37. package/intent/intent-gate.ts +0 -151
  38. package/intent/prompt-analyzer.ts +0 -231
@@ -1,151 +0,0 @@
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
- export type IntentGateResult = {
60
- conversational: boolean;
61
- reason: string;
62
- };
63
-
64
- type ClassifyResult = {
65
- confidence: number;
66
- matchedSignals?: string[];
67
- };
68
-
69
- /**
70
- * Determine if a user message is conversational (should get a text reply)
71
- * or a UI generation request (should go through the pipeline).
72
- */
73
- export function isConversational(text: string, classifyResult: ClassifyResult | null | undefined): IntentGateResult {
74
- const trimmed = (text ?? '').trim();
75
- let cls: ClassifyResult = classifyResult && typeof classifyResult === 'object'
76
- ? classifyResult
77
- : { confidence: 0, matchedSignals: [] };
78
-
79
- // Very short messages without generation keywords are likely conversational
80
- if (trimmed.length < 5 && cls.confidence === 0) {
81
- return { conversational: true, reason: 'short-input' };
82
- }
83
-
84
- // Noise/injection detection — not a real intent
85
- for (const pattern of NOISE_PATTERNS) {
86
- if (pattern.test(trimmed) && cls.confidence === 0) {
87
- return { conversational: true, reason: 'noise-input' };
88
- }
89
- }
90
-
91
- // Explicit meta-questions about the system → always conversational
92
- for (const pattern of META_PATTERNS) {
93
- if (pattern.test(trimmed)) {
94
- return { conversational: true, reason: 'meta-question' };
95
- }
96
- }
97
-
98
- // Tool-use commands (create ticket, report bug, etc.) → conversational
99
- for (const pattern of TOOL_USE_PATTERNS) {
100
- if (pattern.test(trimmed)) {
101
- return { conversational: true, reason: 'tool-use' };
102
- }
103
- }
104
-
105
- // Vague / motivational / abstract input with no or weak domain signals
106
- if (cls.confidence < 0.2) {
107
- for (const pattern of VAGUE_PATTERNS) {
108
- if (pattern.test(trimmed)) {
109
- return { conversational: true, reason: 'vague-or-abstract' };
110
- }
111
- }
112
- }
113
-
114
- // Greetings with no generation signals
115
- if (GREETINGS.test(trimmed) && cls.confidence === 0) {
116
- return { conversational: true, reason: 'greeting' };
117
- }
118
-
119
- // High confidence in a domain → generation request
120
- if (cls.confidence >= 0.3) {
121
- return { conversational: false, reason: 'high-domain-confidence' };
122
- }
123
-
124
- // Has matched at least 2 domain signals → generation
125
- if ((cls.matchedSignals?.length ?? 0) >= 2) {
126
- return { conversational: false, reason: 'multiple-domain-signals' };
127
- }
128
-
129
- // Question pattern with single low-confidence signal → conversational
130
- if (QUESTION_STARTERS.test(trimmed) && (cls.matchedSignals?.length ?? 0) <= 1) {
131
- return { conversational: true, reason: 'question-single-signal' };
132
- }
133
-
134
- // Greetings even with some confidence → still conversational
135
- if (GREETINGS.test(trimmed) && cls.confidence < 0.2) {
136
- return { conversational: true, reason: 'greeting-low-confidence' };
137
- }
138
-
139
- // Default: if there's any domain signal at all, treat as generation
140
- if (cls.confidence > 0) {
141
- return { conversational: false, reason: 'has-domain-signal' };
142
- }
143
-
144
- // No signals — check for question marks or question patterns
145
- if (trimmed.endsWith('?')) {
146
- return { conversational: true, reason: 'ends-with-question-mark' };
147
- }
148
-
149
- // No domain signals at all — route to conversational for clarification
150
- return { conversational: true, reason: 'no-ui-signals' };
151
- }
@@ -1,231 +0,0 @@
1
- /**
2
- * Prompt Analyzer — baseline ingestion stage for the gen-ui pipeline.
3
- *
4
- * Every user intent enters through here, regardless of engine (monolithic,
5
- * zettel) or mode (instant, pro, thinking). The analyzer extracts structured
6
- * signals from the raw prompt — what concepts the user is gesturing at, which
7
- * AdiaUI components are implied, what implicit requirements a good designer
8
- * would add — and produces a "steelmanned" enriched brief that downstream
9
- * stages reason against.
10
- *
11
- * Output schema:
12
- * {
13
- * raw: string // user's original intent, verbatim
14
- * concepts: string[] // 2-5 short concept tags ("commerce", "auth", "data-display")
15
- * entities: string[] // explicit nouns from the prompt ("image", "title", "price")
16
- * impliedComponents: string[] // 4-12 AdiaUI component names ("Card", "Image", "Button")
17
- * styleHints: string[] // visual/style hints ("3D", "minimal", "dense"), [] if none
18
- * steelman: string // 1-2 sentence enriched brief
19
- * analyzed: boolean // true if LLM call succeeded; false if fell back to passthrough
20
- * }
21
- *
22
- * Failure mode: if the LLM call throws or returns unparseable JSON, we
23
- * gracefully degrade to a passthrough analysis (steelman = raw intent, no
24
- * extracted signals). Generation continues; quality may suffer but nothing
25
- * breaks.
26
- */
27
-
28
- // Node's `process` global is not in scope under `"types": []` — declare
29
- // minimally for the typeof guard below (erased at runtime by erasableSyntaxOnly).
30
- declare const process: { versions?: { node?: string } } | undefined;
31
-
32
- // Component vocabulary derived from the catalog. Two artifacts:
33
- // - displayList: human-readable list shown to the LLM, with aliases inlined
34
- // - allowedSet: canonical AND alias names accepted in impliedComponents
35
- let _vocab: { displayList: string[]; allowedSet: Set<string> } | null = null;
36
-
37
- async function getVocab(): Promise<{ displayList: string[]; allowedSet: Set<string> }> {
38
- if (_vocab) return _vocab;
39
- let catalogJson: Record<string, unknown> = {};
40
- try {
41
- const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
42
- if (IS_NODE) {
43
- const fs = await import(/* @vite-ignore */ 'node:fs/promises');
44
- const path = await import(/* @vite-ignore */ 'node:path');
45
- const url = await import(/* @vite-ignore */ 'node:url');
46
- const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
47
- const raw = await fs.readFile(path.join(__dirname, '../../corpus/catalog-a2ui_0_9.json'), 'utf8');
48
- catalogJson = JSON.parse(raw) as Record<string, unknown>;
49
- } else {
50
- const resp = await fetch(new URL('../../corpus/catalog-a2ui_0_9.json', import.meta.url));
51
- if (resp.ok) catalogJson = await resp.json() as Record<string, unknown>;
52
- }
53
- } catch { /* empty vocab — analyzer will still work, just unconstrained */ }
54
-
55
- // Adapt v0.9 catalog shape
56
- const comps = (catalogJson?.['components'] ?? {}) as Record<string, unknown>;
57
- const displayList: string[] = [];
58
- const allowedSet = new Set<string>();
59
- for (const [name, def] of Object.entries(comps)) {
60
- allowedSet.add(name);
61
- const ext = (def as Record<string, unknown>)?.['x-adiaui'] ?? {};
62
- const syns = (ext && typeof ext === 'object') ? (ext as Record<string, unknown>)['synonyms'] : null;
63
- const tags = (syns && typeof syns === 'object') ? (syns as Record<string, unknown>)['tags'] : null;
64
- const aliases: string[] = Array.isArray(tags) ? (tags as unknown[]).filter((t): t is string => typeof t === 'string') : [];
65
- for (const a of aliases) allowedSet.add(a);
66
- displayList.push(aliases.length ? `${name} (also: ${aliases.join(', ')})` : name);
67
- }
68
- _vocab = { displayList, allowedSet };
69
- return _vocab;
70
- }
71
-
72
- export type PromptAnalysis = {
73
- raw: string;
74
- concepts: string[];
75
- entities: string[];
76
- impliedComponents: string[];
77
- styleHints: string[];
78
- steelman: string;
79
- analyzed: boolean;
80
- };
81
-
82
- type LLMAdapter = {
83
- complete(opts: { messages: Array<{ role: string; content: string }>; systemPrompt: string }): Promise<{ content?: string; stopReason?: string }>;
84
- };
85
-
86
- type DomainHint = {
87
- domain?: string;
88
- confidence?: number;
89
- };
90
-
91
- /**
92
- * Analyze a user prompt — extract concepts, entities, implied components,
93
- * style hints, and produce a steelmanned brief.
94
- */
95
- export async function analyzePrompt({ intent, llmAdapter, domain }: {
96
- intent: string;
97
- llmAdapter?: LLMAdapter;
98
- domain?: DomainHint;
99
- }): Promise<PromptAnalysis> {
100
- const passthrough: PromptAnalysis = {
101
- raw: intent,
102
- concepts: [],
103
- entities: [],
104
- impliedComponents: [],
105
- styleHints: [],
106
- steelman: intent,
107
- analyzed: false,
108
- };
109
-
110
- if (!intent || !llmAdapter) return passthrough;
111
-
112
- const { displayList, allowedSet } = await getVocab();
113
- const componentList = displayList.length
114
- ? displayList.join(', ')
115
- : 'Card, Section, Header, Footer, Column, Row, Grid, Button, Input, Text, Image, Badge, Icon, Avatar, Alert, Modal, Tabs, Accordion';
116
-
117
- const domainHint = domain?.['domain'] && (domain?.['confidence'] ?? 0) > 0
118
- ? `\nDomain classifier suggests: ${domain['domain']} (confidence ${domain['confidence']})`
119
- : '';
120
-
121
- const systemPrompt = `You are a UI brief enricher for the AdiaUI design system. Given a terse or under-specified user prompt, you extract structured signals and produce an enriched brief that a downstream UI generator can use to produce a polished, complete UI.
122
-
123
- Your output is JSON with this exact shape — no markdown, no explanation:
124
- {
125
- "concepts": [string], // 2-5 short concept tags (e.g. "commerce", "authentication", "data-display", "navigation")
126
- "entities": [string], // explicit nouns from the prompt (lowercase, singular)
127
- "implied_components": [string],// 4-12 AdiaUI components a good designer would include
128
- "style_hints": [string], // visual/style adjectives ("minimal", "dense", "3D", "playful"), [] if none stated
129
- "steelman": string // 1-2 sentence enriched brief
130
- }
131
-
132
- AdiaUI components you may name in implied_components (use these names exactly): ${componentList}
133
-
134
- Rules:
135
- - Extract entities verbatim from the prompt — do not invent.
136
- - implied_components should include both the obvious (Card, Button) AND the ones a thoughtful designer would add (Badge for sale tag on a product, Avatar for a user row, EmptyState for empty data).
137
- - The steelman expands the prompt into what the user MEANT, not what they SAID. Add the implicit requirements: typical sub-elements, sensible defaults, a header, a footer, common variants. Do NOT add scope the user wouldn't want (e.g. don't turn "login form" into "login form with social auth" unless that's a near-universal expectation).
138
- - Keep steelman under 250 characters.
139
- - If the prompt is already well-specified (>15 words, multiple entities), the steelman is mostly a faithful restatement.
140
-
141
- Examples:
142
- Input: "product card"
143
- Output: {"concepts":["commerce","product-display"],"entities":["product"],"implied_components":["Card","Image","Text","Badge","Button"],"style_hints":[],"steelman":"E-commerce product card with image, title, price, optional sale badge, and primary 'Add to cart' button. Card layout, single product."}
144
-
145
- Input: "settings page with toggles for notifications, dark mode, and privacy"
146
- Output: {"concepts":["settings","preferences"],"entities":["notifications","dark mode","privacy","toggles"],"implied_components":["Card","Header","Section","Toggle","Text","Divider"],"style_hints":[],"steelman":"Settings page with three toggle rows for notifications, dark mode, and privacy. Each row has a label, description, and toggle. Grouped under a header card."}
147
-
148
- Input: "make it 3D"
149
- Output: {"concepts":["style-modifier"],"entities":[],"implied_components":[],"style_hints":["3D","depth","shadow"],"steelman":"Modify the existing canvas to feel three-dimensional — add elevation, layered shadows, and depth-suggesting transforms. Apply to existing components without changing structure."}`;
150
-
151
- const userMsg = `Prompt: "${intent}"${domainHint}`;
152
-
153
- let response: { content?: string; stopReason?: string } | undefined;
154
- try {
155
- response = await llmAdapter.complete({
156
- messages: [{ role: 'user', content: userMsg }],
157
- systemPrompt,
158
- });
159
- } catch {
160
- return passthrough;
161
- }
162
-
163
- const text = response?.content;
164
- if (!text) return passthrough;
165
-
166
- // Truncation: if the analyzer LLM hit max_tokens we'd rather use the raw
167
- // prompt than a half-parsed analysis that misleads downstream stages.
168
- if (response.stopReason && response.stopReason !== 'end' && response.stopReason !== 'end_turn' && response.stopReason !== 'stop') {
169
- return passthrough;
170
- }
171
-
172
- // Try direct parse, then code-fence extraction, then outermost JSON.
173
- let parsed: unknown = null;
174
- try { parsed = JSON.parse(text.trim()); } catch { /* fall through */ }
175
- if (!parsed) {
176
- const fence = text.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
177
- if (fence) try { parsed = JSON.parse(fence[1].trim()); } catch { /* fall through */ }
178
- }
179
- if (!parsed) {
180
- const start = text.indexOf('{');
181
- const end = text.lastIndexOf('}');
182
- if (start >= 0 && end > start) {
183
- try { parsed = JSON.parse(text.slice(start, end + 1)); } catch { /* fall through */ }
184
- }
185
- }
186
- if (!parsed || typeof parsed !== 'object') return passthrough;
187
-
188
- const p = parsed as Record<string, unknown>;
189
-
190
- // Constrain impliedComponents to the allowed vocab
191
- const impliedComponents = Array.isArray(p['implied_components'])
192
- ? (p['implied_components'] as unknown[]).filter((c): c is string => typeof c === 'string' && (allowedSet.size === 0 || allowedSet.has(c)))
193
- : [];
194
-
195
- return {
196
- raw: intent,
197
- concepts: asStringArray(p['concepts']).slice(0, 8),
198
- entities: asStringArray(p['entities']).slice(0, 16),
199
- impliedComponents: impliedComponents.slice(0, 16),
200
- styleHints: asStringArray(p['style_hints']).slice(0, 8),
201
- steelman: (typeof p['steelman'] === 'string' && p['steelman'].trim()) || intent,
202
- analyzed: true,
203
- };
204
- }
205
-
206
- function asStringArray(v: unknown): string[] {
207
- if (!Array.isArray(v)) return [];
208
- return (v as unknown[]).filter((x): x is string => typeof x === 'string' && x.trim().length > 0).map(x => (x as string).trim());
209
- }
210
-
211
- /**
212
- * Format an analysis object for inclusion in a downstream system prompt.
213
- * Keeps the shape compact so it doesn't blow the context budget.
214
- */
215
- export function formatAnalysisForPrompt(analysis: PromptAnalysis | null | undefined): string {
216
- if (!analysis || !analysis.analyzed) return '';
217
- const parts: string[] = [];
218
- if (analysis.steelman && analysis.steelman !== analysis.raw) {
219
- parts.push(`ENRICHED BRIEF: ${analysis.steelman}`);
220
- }
221
- if (analysis.impliedComponents?.length) {
222
- parts.push(`EXPECTED COMPONENTS: ${analysis.impliedComponents.join(', ')}`);
223
- }
224
- if (analysis.concepts?.length) {
225
- parts.push(`CONCEPTS: ${analysis.concepts.join(', ')}`);
226
- }
227
- if (analysis.styleHints?.length) {
228
- parts.push(`STYLE HINTS: ${analysis.styleHints.join(', ')}`);
229
- }
230
- return parts.join('\n');
231
- }