@adia-ai/a2ui-retrieval 0.6.0 → 0.6.2
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 +19 -0
- package/embedding/chunk-embedding-retriever.ts +156 -0
- package/embedding/embedding-provider.ts +111 -0
- package/embedding/index.ts +10 -0
- package/feedback/dialog-recorder.ts +172 -0
- package/feedback/feedback-analyzer.ts +250 -0
- package/feedback/feedback-store.ts +229 -0
- package/feedback/feedback.ts +201 -0
- package/feedback/gap-registry.ts +137 -0
- package/feedback/index.ts +14 -0
- package/intent/clarity.ts +224 -0
- package/intent/decomposer.ts +229 -0
- package/intent/index.ts +20 -0
- package/intent/intent-alignment.ts +267 -0
- package/intent/intent-categorizer.ts +104 -0
- package/intent/intent-gate.ts +151 -0
- package/intent/prompt-analyzer.ts +231 -0
- package/package.json +2 -2
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
// `process` is not in scope under "types": []
|
|
17
|
+
declare const process: { versions?: { node?: string } } | undefined;
|
|
18
|
+
|
|
19
|
+
// Lazy top-level import pattern for Node-only modules
|
|
20
|
+
let fs: typeof import('node:fs/promises') | null = null;
|
|
21
|
+
let path: typeof import('node:path') | null = null;
|
|
22
|
+
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
23
|
+
if (IS_NODE) {
|
|
24
|
+
try {
|
|
25
|
+
fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
26
|
+
path = await import(/* @vite-ignore */ 'node:path');
|
|
27
|
+
} catch {
|
|
28
|
+
// Node builtins unavailable
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// packages/a2ui/retrieval/feedback → up 3 → packages/a2ui → corpus/gaps/registry.json
|
|
33
|
+
const REGISTRY_PATH: string | null = path
|
|
34
|
+
? path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'a2ui/corpus', 'gaps', 'registry.json')
|
|
35
|
+
: null;
|
|
36
|
+
|
|
37
|
+
export type GapStatus = 'open' | 'in-progress' | 'resolved';
|
|
38
|
+
|
|
39
|
+
export type Gap = {
|
|
40
|
+
intentCategory: string;
|
|
41
|
+
detectedAt: string;
|
|
42
|
+
lastSeen: string;
|
|
43
|
+
sampleCount: number;
|
|
44
|
+
avgScore: number;
|
|
45
|
+
avgRating: number;
|
|
46
|
+
status: GapStatus;
|
|
47
|
+
resolution: string | null;
|
|
48
|
+
sampleIntents: string[];
|
|
49
|
+
resolvedAt?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type AddGapInput = {
|
|
53
|
+
intentCategory: string;
|
|
54
|
+
sampleCount?: number;
|
|
55
|
+
avgScore?: number;
|
|
56
|
+
avgRating?: number;
|
|
57
|
+
sampleIntents?: string[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load all gaps from the registry file.
|
|
62
|
+
*/
|
|
63
|
+
export async function loadGaps(): Promise<Gap[]> {
|
|
64
|
+
if (!fs || !REGISTRY_PATH) return [];
|
|
65
|
+
try {
|
|
66
|
+
const content = await fs.readFile(REGISTRY_PATH, 'utf8');
|
|
67
|
+
return JSON.parse(content) as Gap[];
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Save the full gaps array to disk.
|
|
75
|
+
*/
|
|
76
|
+
export async function saveGaps(gaps: Gap[]): Promise<void> {
|
|
77
|
+
if (!fs || !REGISTRY_PATH) return;
|
|
78
|
+
const dir = path!.dirname(REGISTRY_PATH);
|
|
79
|
+
await fs.mkdir(dir, { recursive: true });
|
|
80
|
+
await fs.writeFile(REGISTRY_PATH, JSON.stringify(gaps, null, 2) + '\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add a new gap to the registry. Merges with existing if same intentCategory.
|
|
85
|
+
*/
|
|
86
|
+
export async function addGap(gap: AddGapInput): Promise<Gap[]> {
|
|
87
|
+
const gaps = await loadGaps();
|
|
88
|
+
const existing = gaps.find(g => g.intentCategory === gap.intentCategory && g.status !== 'resolved');
|
|
89
|
+
|
|
90
|
+
if (existing) {
|
|
91
|
+
// Merge: update stats, add new sample intents
|
|
92
|
+
existing.sampleCount = gap.sampleCount ?? existing.sampleCount;
|
|
93
|
+
existing.avgScore = gap.avgScore ?? existing.avgScore;
|
|
94
|
+
existing.avgRating = gap.avgRating ?? existing.avgRating;
|
|
95
|
+
existing.lastSeen = new Date().toISOString();
|
|
96
|
+
const intentSet = new Set([...existing.sampleIntents, ...(gap.sampleIntents ?? [])]);
|
|
97
|
+
existing.sampleIntents = [...intentSet].slice(0, 10);
|
|
98
|
+
} else {
|
|
99
|
+
gaps.push({
|
|
100
|
+
intentCategory: gap.intentCategory,
|
|
101
|
+
detectedAt: new Date().toISOString(),
|
|
102
|
+
lastSeen: new Date().toISOString(),
|
|
103
|
+
sampleCount: gap.sampleCount ?? 0,
|
|
104
|
+
avgScore: gap.avgScore ?? 0,
|
|
105
|
+
avgRating: gap.avgRating ?? 0,
|
|
106
|
+
status: 'open',
|
|
107
|
+
resolution: null,
|
|
108
|
+
sampleIntents: (gap.sampleIntents ?? []).slice(0, 10),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await saveGaps(gaps);
|
|
113
|
+
return gaps;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Update the status of a gap by intent category.
|
|
118
|
+
*/
|
|
119
|
+
export async function updateGapStatus(category: string, status: GapStatus, resolution?: string): Promise<Gap> {
|
|
120
|
+
const gaps = await loadGaps();
|
|
121
|
+
const gap = gaps.find(g => g.intentCategory === category && g.status !== 'resolved');
|
|
122
|
+
|
|
123
|
+
if (!gap) {
|
|
124
|
+
throw new Error(`No open gap found for category: ${category}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
gap.status = status;
|
|
128
|
+
if (resolution) {
|
|
129
|
+
gap.resolution = resolution;
|
|
130
|
+
}
|
|
131
|
+
if (status === 'resolved') {
|
|
132
|
+
gap.resolvedAt = new Date().toISOString();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await saveGaps(gaps);
|
|
136
|
+
return gap;
|
|
137
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @adia-ai/a2ui-retrieval/feedback — feedback-loop surface.
|
|
3
|
+
*
|
|
4
|
+
* Pairs the feedback store, analyzer, dialog recorder, and gap registry
|
|
5
|
+
* — all the artifacts that compose the user-feedback ingestion loop.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { feedbackStore } from './feedback-store.js';
|
|
9
|
+
export { FeedbackAnalyzer } from './feedback-analyzer.js';
|
|
10
|
+
export * from './feedback.js';
|
|
11
|
+
export { recordTurn, isRecording } from './dialog-recorder.js';
|
|
12
|
+
export type { TurnRecord } from './dialog-recorder.js';
|
|
13
|
+
export { loadGaps, addGap, updateGapStatus } from './gap-registry.js';
|
|
14
|
+
export type { Gap, GapStatus, AddGapInput } from './gap-registry.js';
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity Assessment — Evaluates whether a user intent is clear enough
|
|
3
|
+
* to generate quality UI, or needs clarifying questions first.
|
|
4
|
+
*
|
|
5
|
+
* Scores intents across 5 dimensions:
|
|
6
|
+
* domain — Is the domain clear? (forms, data, layout, etc.)
|
|
7
|
+
* scope — Is the scope defined? (how many items, what sections?)
|
|
8
|
+
* content — Is the content specified? (labels, values, data?)
|
|
9
|
+
* layout — Is the layout preference stated? (grid, stack, sidebar?)
|
|
10
|
+
* action — Are actions/interactions mentioned? (buttons, forms, links?)
|
|
11
|
+
*
|
|
12
|
+
* Returns a clarity score (0-1) and targeted questions for what's missing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { classifyIntent } from '../domain-router.js';
|
|
16
|
+
import { searchAll as searchCompositions } from '../../compose/strategies/zettel/composition-library.js';
|
|
17
|
+
|
|
18
|
+
// ── Dimension detectors ──────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Scope indicators: quantity, enumeration, specificity */
|
|
21
|
+
const SCOPE_SIGNALS = [
|
|
22
|
+
/\b\d+\b/, // contains a number
|
|
23
|
+
/\b(three|two|four|five|six)\b/i, // spelled-out numbers
|
|
24
|
+
/\b(with|containing|including|showing|displaying)\b/i, // enumerates content
|
|
25
|
+
/\b(and|plus|also)\b/i, // multiple items
|
|
26
|
+
/\b(columns?|rows?|items?|cards?|sections?|fields?|buttons?|tabs?|steps?)\b/i, // structural counts
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Content specificity: labels, values, named data */
|
|
30
|
+
const CONTENT_SIGNALS = [
|
|
31
|
+
/\b(name|email|password|title|description|price|date|status|role|avatar)\b/i,
|
|
32
|
+
/\b(revenue|users|growth|sales|orders|metrics|analytics)\b/i,
|
|
33
|
+
/\b(todo|done|in progress|pending|active|completed)\b/i,
|
|
34
|
+
/\b(bleed|margin|trim|crop|preview|artwork|brand|design system)\b/i,
|
|
35
|
+
/\b(approved|approval|production|settings|configure|preference)\b/i,
|
|
36
|
+
/["'][\w\s]+["']/, // quoted strings (specific labels)
|
|
37
|
+
/\$[\d,.]+/, // dollar amounts
|
|
38
|
+
/\d+%/, // percentages
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** Layout preferences */
|
|
42
|
+
const LAYOUT_SIGNALS = [
|
|
43
|
+
/\b(grid|row|column|sidebar|split|horizontal|vertical|stack|centered)\b/i,
|
|
44
|
+
/\b(full.?width|responsive|mobile|compact|wide|narrow)\b/i,
|
|
45
|
+
/\b(\d+.?col(umn)?s?)\b/i, // "3 columns", "2-col"
|
|
46
|
+
/\b(left|right|top|bottom|center)\b/i,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Action/interaction indicators */
|
|
50
|
+
const ACTION_SIGNALS = [
|
|
51
|
+
/\b(button|submit|cancel|save|delete|edit|close|open|toggle|click)\b/i,
|
|
52
|
+
/\b(form|input|select|search|filter|sort|upload|download)\b/i,
|
|
53
|
+
/\b(login|signup|register|checkout|confirm|approve|reject)\b/i,
|
|
54
|
+
/\b(navigate|link|redirect|route)\b/i,
|
|
55
|
+
/\b(drag|drop|resize|expand|collapse)\b/i,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export type ClarityDimensions = {
|
|
59
|
+
domain: number;
|
|
60
|
+
scope: number;
|
|
61
|
+
content: number;
|
|
62
|
+
layout: number;
|
|
63
|
+
action: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ClarityQuestion = {
|
|
67
|
+
text: string;
|
|
68
|
+
dimension: string;
|
|
69
|
+
priority: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ClarityResult = {
|
|
73
|
+
clear: boolean;
|
|
74
|
+
score: number;
|
|
75
|
+
dimensions: ClarityDimensions;
|
|
76
|
+
questions: ClarityQuestion[];
|
|
77
|
+
summary: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type ClassificationArg = {
|
|
81
|
+
domain: string;
|
|
82
|
+
confidence: number;
|
|
83
|
+
matchedSignals: string[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Assess clarity of a user intent for UI generation.
|
|
88
|
+
*/
|
|
89
|
+
export function assessClarity(intent: string, classification?: ClassificationArg): ClarityResult {
|
|
90
|
+
const text = (intent ?? '').trim();
|
|
91
|
+
if (!text) {
|
|
92
|
+
return {
|
|
93
|
+
clear: false,
|
|
94
|
+
score: 0,
|
|
95
|
+
dimensions: { domain: 0, scope: 0, content: 0, layout: 0, action: 0 },
|
|
96
|
+
questions: [{ text: 'What would you like me to build?', dimension: 'domain', priority: 1 }],
|
|
97
|
+
summary: 'No intent provided',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cls = classification || classifyIntent(text);
|
|
102
|
+
const compositionMatches = searchCompositions(text);
|
|
103
|
+
const wordCount = text.split(/\s+/).length;
|
|
104
|
+
|
|
105
|
+
// ── Score each dimension ──
|
|
106
|
+
|
|
107
|
+
// Domain: how confident is the domain classification?
|
|
108
|
+
const domainScore = cls.confidence >= 0.3 ? 1 : cls.confidence >= 0.15 ? 0.6 : cls.matchedSignals.length > 0 ? 0.3 : 0;
|
|
109
|
+
|
|
110
|
+
// Scope: is the scope specific?
|
|
111
|
+
const scopeHits = SCOPE_SIGNALS.filter(r => r.test(text)).length;
|
|
112
|
+
const scopeScore = Math.min(1, scopeHits / 2);
|
|
113
|
+
|
|
114
|
+
// Content: are concrete labels/values mentioned?
|
|
115
|
+
const contentHits = CONTENT_SIGNALS.filter(r => r.test(text)).length;
|
|
116
|
+
const contentScore = Math.min(1, contentHits / 2);
|
|
117
|
+
|
|
118
|
+
// Layout: is a layout preference stated?
|
|
119
|
+
const layoutHits = LAYOUT_SIGNALS.filter(r => r.test(text)).length;
|
|
120
|
+
const layoutScore = Math.min(1, layoutHits);
|
|
121
|
+
|
|
122
|
+
// Action: are interactions mentioned?
|
|
123
|
+
const actionHits = ACTION_SIGNALS.filter(r => r.test(text)).length;
|
|
124
|
+
const actionScore = Math.min(1, actionHits / 2);
|
|
125
|
+
|
|
126
|
+
// ── Overall score (weighted) ──
|
|
127
|
+
const score = (
|
|
128
|
+
domainScore * 0.25 +
|
|
129
|
+
scopeScore * 0.25 +
|
|
130
|
+
contentScore * 0.20 +
|
|
131
|
+
layoutScore * 0.15 +
|
|
132
|
+
actionScore * 0.15
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// ── Bonus: composition match adds confidence ──
|
|
136
|
+
const patternBonus = compositionMatches.length > 0 ? 0.15 : 0;
|
|
137
|
+
// Bonus: long intents are usually more specific
|
|
138
|
+
const lengthBonus = wordCount > 10 ? 0.1 : wordCount > 6 ? 0.05 : 0;
|
|
139
|
+
|
|
140
|
+
const finalScore = Math.min(1, score + patternBonus + lengthBonus);
|
|
141
|
+
|
|
142
|
+
// ── Generate targeted questions for weak dimensions ──
|
|
143
|
+
const questions: ClarityQuestion[] = [];
|
|
144
|
+
|
|
145
|
+
if (domainScore < 0.3) {
|
|
146
|
+
questions.push({
|
|
147
|
+
text: 'What type of UI is this? (e.g., a form, dashboard, profile card, settings page)',
|
|
148
|
+
dimension: 'domain',
|
|
149
|
+
priority: 1,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (scopeScore < 0.5) {
|
|
154
|
+
const domainQuestions: Record<string, string> = {
|
|
155
|
+
forms: 'What fields should the form include?',
|
|
156
|
+
data: 'What data should be displayed? How many items or metrics?',
|
|
157
|
+
layout: 'How many sections or cards should it have?',
|
|
158
|
+
agent: 'What actions should the assistant support?',
|
|
159
|
+
navigation: 'What pages or sections should be navigable?',
|
|
160
|
+
};
|
|
161
|
+
questions.push({
|
|
162
|
+
text: domainQuestions[cls.domain] ?? 'Can you be more specific about what it should contain?',
|
|
163
|
+
dimension: 'scope',
|
|
164
|
+
priority: 2,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (contentScore < 0.3 && scopeScore >= 0.3) {
|
|
169
|
+
const contentQuestions: Record<string, string> = {
|
|
170
|
+
forms: 'What labels should the fields have? (e.g., "Email", "Password", "Full Name")',
|
|
171
|
+
data: 'What metrics or values should be shown? (e.g., revenue, users, growth rate)',
|
|
172
|
+
layout: 'What content goes in each section?',
|
|
173
|
+
agent: 'What kind of messages or responses should be shown?',
|
|
174
|
+
navigation: 'What should the menu items or links be labeled?',
|
|
175
|
+
};
|
|
176
|
+
questions.push({
|
|
177
|
+
text: contentQuestions[cls.domain] ?? 'What specific content should be displayed?',
|
|
178
|
+
dimension: 'content',
|
|
179
|
+
priority: 3,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (layoutScore < 0.3 && scopeScore >= 0.5) {
|
|
184
|
+
questions.push({
|
|
185
|
+
text: 'Any layout preference? (e.g., grid of cards, single column, sidebar + content)',
|
|
186
|
+
dimension: 'layout',
|
|
187
|
+
priority: 4,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (actionScore < 0.3 && domainScore >= 0.3) {
|
|
192
|
+
questions.push({
|
|
193
|
+
text: 'What actions should users be able to take? (e.g., submit, edit, delete, filter)',
|
|
194
|
+
dimension: 'action',
|
|
195
|
+
priority: 5,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Sort by priority, limit to 3
|
|
200
|
+
questions.sort((a, b) => a.priority - b.priority);
|
|
201
|
+
const topQuestions = questions.slice(0, 3);
|
|
202
|
+
|
|
203
|
+
// ── Determine if clear enough to proceed ──
|
|
204
|
+
// Clear only if dimensional score meets threshold
|
|
205
|
+
const clear = finalScore >= 0.4;
|
|
206
|
+
|
|
207
|
+
const summary = clear
|
|
208
|
+
? `Intent is ${finalScore >= 0.7 ? 'clear' : 'adequate'} (${Math.round(finalScore * 100)}%)`
|
|
209
|
+
: `Intent needs clarification (${Math.round(finalScore * 100)}% — below 40% threshold)`;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
clear,
|
|
213
|
+
score: Math.round(finalScore * 100) / 100,
|
|
214
|
+
dimensions: {
|
|
215
|
+
domain: Math.round(domainScore * 100) / 100,
|
|
216
|
+
scope: Math.round(scopeScore * 100) / 100,
|
|
217
|
+
content: Math.round(contentScore * 100) / 100,
|
|
218
|
+
layout: Math.round(layoutScore * 100) / 100,
|
|
219
|
+
action: Math.round(actionScore * 100) / 100,
|
|
220
|
+
},
|
|
221
|
+
questions: topQuestions,
|
|
222
|
+
summary,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent Decomposer — breaks complex intents into independent subtasks.
|
|
3
|
+
*
|
|
4
|
+
* When an intent describes multiple sections, areas, or features,
|
|
5
|
+
* the decomposer splits it into atomic generation units that can be
|
|
6
|
+
* generated independently and composed into a layout.
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* "settings page with profile, notifications, security, and billing"
|
|
10
|
+
* → 4 subtasks + a composition plan (Tabs layout)
|
|
11
|
+
*
|
|
12
|
+
* The decomposition is the single most important capability per the
|
|
13
|
+
* Software Factory Manifesto: "Get decomposition right and generation
|
|
14
|
+
* is almost trivial."
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { classifyIntent } from '../domain-router.js';
|
|
18
|
+
|
|
19
|
+
// ── Section Detection ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Connectors that separate sections in an intent */
|
|
22
|
+
const SECTION_SPLITTERS = /\band\b|\bwith\b|\bplus\b|\balso\b|,\s*(?:and\s+)?/gi;
|
|
23
|
+
|
|
24
|
+
/** Words that signal enumerated sections */
|
|
25
|
+
const SECTION_SIGNALS = [
|
|
26
|
+
/\b(\d+)\s+(sections?|areas?|parts?|panels?|columns?|cards?|tabs?|pages?|views?)\b/i,
|
|
27
|
+
/\bsections?:\s/i,
|
|
28
|
+
/\bincluding\b/i,
|
|
29
|
+
/\beach\s+(with|having|containing)\b/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** Layout containers that imply multi-section structure */
|
|
33
|
+
const LAYOUT_KEYWORDS: Record<string, { component: string; child: string }> = {
|
|
34
|
+
tabs: { component: 'Tabs', child: 'Tab' },
|
|
35
|
+
sections: { component: 'Column', child: 'Card' },
|
|
36
|
+
columns: { component: 'Grid', child: 'Column' },
|
|
37
|
+
cards: { component: 'Grid', child: 'Card' },
|
|
38
|
+
panels: { component: 'Accordion', child: 'Panel' },
|
|
39
|
+
pages: { component: 'Tabs', child: 'Tab' },
|
|
40
|
+
areas: { component: 'Grid', child: 'Card' },
|
|
41
|
+
steps: { component: 'Steps', child: 'Step' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type Subtask = { intent: string; label: string };
|
|
45
|
+
export type LayoutInfo = { component: string; child: string };
|
|
46
|
+
|
|
47
|
+
export type DecomposeResult = {
|
|
48
|
+
shouldDecompose: boolean;
|
|
49
|
+
subtasks: Subtask[];
|
|
50
|
+
layout: LayoutInfo | null;
|
|
51
|
+
original: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Analyze an intent for decomposition potential.
|
|
56
|
+
*/
|
|
57
|
+
export function decomposeIntent(intent: string): DecomposeResult {
|
|
58
|
+
const trimmed = (intent || '').trim();
|
|
59
|
+
if (!trimmed) return { shouldDecompose: false, subtasks: [], layout: null, original: trimmed };
|
|
60
|
+
|
|
61
|
+
// ── Detect explicit section count ──
|
|
62
|
+
for (const pattern of SECTION_SIGNALS) {
|
|
63
|
+
if (pattern.test(trimmed)) {
|
|
64
|
+
const sections = extractSections(trimmed);
|
|
65
|
+
if (sections.length >= 2) {
|
|
66
|
+
const layout = detectLayout(trimmed, sections.length);
|
|
67
|
+
return {
|
|
68
|
+
shouldDecompose: true,
|
|
69
|
+
subtasks: sections,
|
|
70
|
+
layout,
|
|
71
|
+
original: trimmed,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Detect enumerated items ──
|
|
78
|
+
const sections = extractSections(trimmed);
|
|
79
|
+
if (sections.length >= 2) {
|
|
80
|
+
// 2+ distinct sections → decompose
|
|
81
|
+
const layout = detectLayout(trimmed, sections.length);
|
|
82
|
+
return {
|
|
83
|
+
shouldDecompose: true,
|
|
84
|
+
subtasks: sections,
|
|
85
|
+
layout,
|
|
86
|
+
original: trimmed,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Short or simple intent → don't decompose ──
|
|
91
|
+
return { shouldDecompose: false, subtasks: [], layout: null, original: trimmed };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract individual section descriptions from an intent.
|
|
96
|
+
*/
|
|
97
|
+
function extractSections(intent: string): Subtask[] {
|
|
98
|
+
// Find the "with X, Y, Z, and W" pattern
|
|
99
|
+
const withMatch = intent.match(/\bwith\s+(.+)$/i);
|
|
100
|
+
const payload = withMatch ? withMatch[1] : intent;
|
|
101
|
+
|
|
102
|
+
// Split on connectors
|
|
103
|
+
const parts = payload.split(SECTION_SPLITTERS)
|
|
104
|
+
.map(s => s.trim())
|
|
105
|
+
.filter(s => s.length > 2);
|
|
106
|
+
|
|
107
|
+
if (parts.length < 2) return [];
|
|
108
|
+
|
|
109
|
+
// Extract the base context (everything before "with")
|
|
110
|
+
const baseContext = withMatch ? intent.slice(0, withMatch.index).trim() : '';
|
|
111
|
+
const domain = classifyIntent(intent).domain;
|
|
112
|
+
void domain; // used for context only in the original
|
|
113
|
+
|
|
114
|
+
return parts.map(part => {
|
|
115
|
+
// Build a self-contained subtask intent
|
|
116
|
+
const label = part.replace(/\b(section|area|panel|tab|page)\b/gi, '').trim();
|
|
117
|
+
const subtaskIntent = baseContext
|
|
118
|
+
? `${label} section for a ${baseContext}`
|
|
119
|
+
: `${label}`;
|
|
120
|
+
|
|
121
|
+
return { intent: subtaskIntent, label: capitalizeFirst(label) };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Detect the best layout container for the decomposed sections.
|
|
127
|
+
*/
|
|
128
|
+
function detectLayout(intent: string, sectionCount: number): LayoutInfo {
|
|
129
|
+
const lower = intent.toLowerCase();
|
|
130
|
+
|
|
131
|
+
// Explicit layout keywords in the intent
|
|
132
|
+
for (const [keyword, layout] of Object.entries(LAYOUT_KEYWORDS)) {
|
|
133
|
+
if (lower.includes(keyword)) return layout;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Heuristic: settings/profile pages → Tabs
|
|
137
|
+
if (lower.includes('settings') || lower.includes('preferences') || lower.includes('account')) {
|
|
138
|
+
return { component: 'Tabs', child: 'Tab' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Heuristic: dashboard with metrics → Grid of Cards
|
|
142
|
+
if (lower.includes('dashboard') || lower.includes('overview') || lower.includes('stat')) {
|
|
143
|
+
return { component: 'Grid', child: 'Card' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Default: small count → Tabs, large count → Grid
|
|
147
|
+
if (sectionCount <= 4) return { component: 'Tabs', child: 'Tab' };
|
|
148
|
+
return { component: 'Grid', child: 'Card' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type SubtaskResult = {
|
|
152
|
+
label: string;
|
|
153
|
+
messages: Array<{ components?: ComponentItem[] }>;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
type ComponentItem = {
|
|
157
|
+
id: string;
|
|
158
|
+
component?: string;
|
|
159
|
+
children?: string[];
|
|
160
|
+
text?: string;
|
|
161
|
+
columns?: string;
|
|
162
|
+
gap?: string;
|
|
163
|
+
[key: string]: unknown;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Compose independently generated subtask results into a layout.
|
|
168
|
+
* Returns A2UI messages for the composed layout.
|
|
169
|
+
*/
|
|
170
|
+
export function composeSubtasks(layout: LayoutInfo, subtaskResults: SubtaskResult[]): object[] {
|
|
171
|
+
const rootChildren: string[] = [];
|
|
172
|
+
const allComponents: ComponentItem[] = [];
|
|
173
|
+
let idCounter = 0;
|
|
174
|
+
|
|
175
|
+
for (const { label, messages } of subtaskResults) {
|
|
176
|
+
const prefix = `s${++idCounter}`;
|
|
177
|
+
const subtaskComponents: ComponentItem[] = messages?.[0]?.components ?? [];
|
|
178
|
+
|
|
179
|
+
// Re-prefix all component IDs to avoid collisions
|
|
180
|
+
const idMap = new Map<string, string>();
|
|
181
|
+
const remapped = subtaskComponents.map(c => {
|
|
182
|
+
const newId = c.id === 'root' ? prefix : `${prefix}-${c.id}`;
|
|
183
|
+
idMap.set(c.id, newId);
|
|
184
|
+
return { ...c, id: newId };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Fix child references
|
|
188
|
+
for (const c of remapped) {
|
|
189
|
+
if (c.children) {
|
|
190
|
+
c.children = c.children.map(id => idMap.get(id) ?? id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Create the layout wrapper for this subtask
|
|
195
|
+
if (layout.child === 'Tab') {
|
|
196
|
+
// Tabs: wrap in a Tab with a label
|
|
197
|
+
const tabId = `${prefix}-tab`;
|
|
198
|
+
allComponents.push({ id: tabId, component: 'Tab', text: label, children: [prefix] });
|
|
199
|
+
rootChildren.push(tabId);
|
|
200
|
+
} else {
|
|
201
|
+
// Grid/Column: subtask root becomes a direct child
|
|
202
|
+
rootChildren.push(prefix);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
allComponents.push(...remapped);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Build the root layout
|
|
209
|
+
const root: ComponentItem = {
|
|
210
|
+
id: 'root',
|
|
211
|
+
component: layout.component,
|
|
212
|
+
children: rootChildren,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (layout.component === 'Grid') {
|
|
216
|
+
root['columns'] = String(Math.min(subtaskResults.length, 4));
|
|
217
|
+
root['gap'] = 'md';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return [{
|
|
221
|
+
type: 'updateComponents',
|
|
222
|
+
surfaceId: 'default',
|
|
223
|
+
components: [root, ...allComponents],
|
|
224
|
+
}];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function capitalizeFirst(str: string): string {
|
|
228
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
229
|
+
}
|
package/intent/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @adia-ai/a2ui-retrieval/intent — intent classification + decomposition surface.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the intent-shaped retrieval primitives. Importers can reach
|
|
5
|
+
* individual files directly (e.g. `@adia-ai/a2ui-retrieval/intent/clarity`)
|
|
6
|
+
* or pull the bundle via this barrel.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { extractExpectations, verifyAlignment, checkIntentAlignment } from './intent-alignment.js';
|
|
10
|
+
export type { Expectations, AlignmentCheck, AlignmentResult, QuantityExpectation } from './intent-alignment.js';
|
|
11
|
+
export { categorizeIntent } from './intent-categorizer.js';
|
|
12
|
+
export type { IntentCategory } from './intent-categorizer.js';
|
|
13
|
+
export { isConversational } from './intent-gate.js';
|
|
14
|
+
export type { IntentGateResult } from './intent-gate.js';
|
|
15
|
+
export { analyzePrompt, formatAnalysisForPrompt } from './prompt-analyzer.js';
|
|
16
|
+
export type { PromptAnalysis } from './prompt-analyzer.js';
|
|
17
|
+
export { decomposeIntent, composeSubtasks } from './decomposer.js';
|
|
18
|
+
export type { DecomposeResult, Subtask, LayoutInfo } from './decomposer.js';
|
|
19
|
+
export { assessClarity } from './clarity.js';
|
|
20
|
+
export type { ClarityResult, ClarityDimensions, ClarityQuestion } from './clarity.js';
|