@adia-ai/a2ui-retrieval 0.6.2 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,243 +1,172 @@
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
1
  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,
2
+ /\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
3
  ];
18
-
19
- /** Component type expectations — "button", "form", "table", etc. */
20
4
  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,
5
+ /\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
6
  ];
23
-
24
- /** Quantity expectations — "3 cards", "two columns", etc. */
25
7
  const QUANTITY_MAP = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 };
26
8
  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
9
  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,
10
+ /\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
11
  ];
32
-
33
- /** Specific content expectations — quoted strings, dollar amounts, percentages */
34
12
  const CONTENT_PATTERNS = [
35
- /["']([^"']+)["']/g, // quoted strings
36
- /\$[\d,.]+/g, // dollar amounts
37
- /\d+%/g, // percentages
13
+ /["']([^"']+)["']/g,
14
+ // quoted strings
15
+ /\$[\d,.]+/g,
16
+ // dollar amounts
17
+ /\d+%/g
18
+ // percentages
38
19
  ];
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) {
20
+ function extractExpectations(intent) {
47
21
  const lower = intent.toLowerCase();
48
-
49
- // Fields
50
22
  const fields = [];
51
23
  for (const pattern of FIELD_PATTERNS) {
52
24
  pattern.lastIndex = 0;
53
25
  let match;
54
- while ((match = pattern.exec(lower))) {
55
- const f = match[1].toLowerCase();
26
+ while (match = pattern.exec(lower)) {
27
+ const f = (match[1] ?? "").toLowerCase();
56
28
  if (!fields.includes(f)) fields.push(f);
57
29
  }
58
30
  }
59
-
60
- // Component types
61
31
  const componentTypes = [];
62
32
  for (const pattern of COMPONENT_PATTERNS) {
63
33
  pattern.lastIndex = 0;
64
34
  let match;
65
- while ((match = pattern.exec(lower))) {
66
- const t = match[1].toLowerCase();
35
+ while (match = pattern.exec(lower)) {
36
+ const t = (match[1] ?? "").toLowerCase();
67
37
  if (!componentTypes.includes(t)) componentTypes.push(t);
68
38
  }
69
39
  }
70
-
71
- // Quantities
72
40
  const quantities = [];
73
41
  QUANTITY_PATTERN.lastIndex = 0;
74
42
  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
43
+ while (qMatch = QUANTITY_PATTERN.exec(lower)) {
44
+ const raw = qMatch[1] ?? "";
45
+ const num = QUANTITY_MAP[raw] ?? parseInt(raw, 10);
46
+ const type = (qMatch[2] ?? "").replace(/s$/, "");
78
47
  if (!isNaN(num)) quantities.push({ count: num, type });
79
48
  }
80
-
81
- // Actions
82
49
  const actions = [];
83
50
  for (const pattern of ACTION_PATTERNS) {
84
51
  pattern.lastIndex = 0;
85
52
  let match;
86
- while ((match = pattern.exec(lower))) {
87
- const a = match[1].toLowerCase();
53
+ while (match = pattern.exec(lower)) {
54
+ const a = (match[1] ?? "").toLowerCase();
88
55
  if (!actions.includes(a)) actions.push(a);
89
56
  }
90
57
  }
91
-
92
- // Specific content
93
58
  const content = [];
94
59
  for (const pattern of CONTENT_PATTERNS) {
95
60
  pattern.lastIndex = 0;
96
61
  let match;
97
- while ((match = pattern.exec(intent))) {
98
- content.push(match[1] || match[0]);
62
+ while (match = pattern.exec(intent)) {
63
+ content.push(match[1] ?? match[0]);
99
64
  }
100
65
  }
101
-
102
66
  return { fields, componentTypes, quantities, actions, content };
103
67
  }
104
-
105
- // ── A2UI Type Mapping ───────────────────────────────────────────────────
106
-
107
- /** Map intent keywords to A2UI component types */
108
68
  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'],
69
+ button: ["Button"],
70
+ form: ["FormContainer", "TextField", "Column"],
71
+ table: ["Table"],
72
+ chart: ["Chart"],
73
+ card: ["Card"],
74
+ avatar: ["Avatar"],
75
+ badge: ["Badge"],
76
+ alert: ["Alert"],
77
+ modal: ["Modal", "Dialog"],
78
+ drawer: ["Drawer"],
79
+ tab: ["Tabs", "Tab"],
80
+ sidebar: ["Sidebar"],
81
+ navbar: ["Nav"],
82
+ breadcrumb: ["Breadcrumb"],
83
+ pagination: ["Pagination"],
84
+ progress: ["Progress"],
85
+ slider: ["Slider"],
86
+ toggle: ["Toggle"],
87
+ checkbox: ["CheckBox"],
88
+ radio: ["Radio"],
89
+ dropdown: ["ChoicePicker", "Select"],
90
+ select: ["ChoicePicker", "Select"],
91
+ upload: ["Upload"],
92
+ calendar: ["CalendarPicker", "DateTimeInput"],
93
+ timeline: ["Timeline"],
94
+ step: ["Steps"],
95
+ accordion: ["Accordion"]
136
96
  };
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) {
97
+ function verifyAlignment(components, expectations) {
146
98
  const checks = [];
147
99
  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 ──
100
+ const allText = components.map((c) => {
101
+ const texts = [c["textContent"], c["text"], c["label"], c["placeholder"], c["description"], c["name"]];
102
+ return texts.filter(Boolean).join(" ").toLowerCase();
103
+ }).join(" ");
104
+ const allTypes = new Set(components.map((c) => c.component));
158
105
  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));
106
+ const found = allText.includes(field) || components.some((c) => (c["label"] ?? "").toLowerCase().includes(field) || (c["placeholder"] ?? "").toLowerCase().includes(field) || (c["name"] ?? "").toLowerCase().includes(field));
163
107
  checks.push({
164
- category: 'field',
108
+ category: "field",
165
109
  expected: field,
166
110
  found,
167
- detail: found ? `Found "${field}" in component properties` : `Missing field: "${field}"`,
111
+ detail: found ? `Found "${field}" in component properties` : `Missing field: "${field}"`
168
112
  });
169
113
  if (!found) gaps.push(`Missing field "${field}"`);
170
114
  }
171
-
172
- // ── Check component types ──
173
115
  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));
116
+ const mappedTypes = TYPE_MAP[type] ?? [type.charAt(0).toUpperCase() + type.slice(1)];
117
+ const found = mappedTypes.some((t) => allTypes.has(t));
176
118
  checks.push({
177
- category: 'componentType',
119
+ category: "componentType",
178
120
  expected: type,
179
121
  found,
180
- detail: found ? `Found ${type} component` : `Missing component type: ${type}`,
122
+ detail: found ? `Found ${type} component` : `Missing component type: ${type}`
181
123
  });
182
124
  if (!found) gaps.push(`Missing ${type} component`);
183
125
  }
184
-
185
- // ── Check quantities ──
186
126
  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;
127
+ const mappedTypes = TYPE_MAP[type] ?? [type.charAt(0).toUpperCase() + type.slice(1)];
128
+ const actual = components.filter((c) => mappedTypes.some((t) => c.component === t)).length;
189
129
  const found = actual >= count;
190
130
  checks.push({
191
- category: 'quantity',
131
+ category: "quantity",
192
132
  expected: `${count} ${type}(s)`,
193
133
  found,
194
- detail: found ? `Found ${actual} ${type}(s) (expected ${count})` : `Only ${actual} ${type}(s), expected ${count}`,
134
+ detail: found ? `Found ${actual} ${type}(s) (expected ${count})` : `Only ${actual} ${type}(s), expected ${count}`
195
135
  });
196
136
  if (!found) gaps.push(`Expected ${count} ${type}(s), found ${actual}`);
197
137
  }
198
-
199
- // ── Check actions ──
200
138
  for (const action of expectations.actions) {
201
- const found = allText.includes(action) ||
202
- components.some(c => c.component === 'Button' && (c.text || '').toLowerCase().includes(action));
139
+ const found = allText.includes(action) || components.some((c) => c.component === "Button" && (c["text"] ?? "").toLowerCase().includes(action));
203
140
  checks.push({
204
- category: 'action',
141
+ category: "action",
205
142
  expected: action,
206
143
  found,
207
- detail: found ? `Found "${action}" action` : `Missing action: "${action}"`,
144
+ detail: found ? `Found "${action}" action` : `Missing action: "${action}"`
208
145
  });
209
146
  if (!found) gaps.push(`Missing "${action}" action`);
210
147
  }
211
-
212
- // ── Check specific content ──
213
148
  for (const text of expectations.content) {
214
149
  const found = allText.includes(text.toLowerCase());
215
150
  checks.push({
216
- category: 'content',
151
+ category: "content",
217
152
  expected: text,
218
153
  found,
219
- detail: found ? `Found content "${text}"` : `Missing content: "${text}"`,
154
+ detail: found ? `Found content "${text}"` : `Missing content: "${text}"`
220
155
  });
221
156
  if (!found) gaps.push(`Missing content "${text}"`);
222
157
  }
223
-
224
- // ── Score ──
225
158
  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
-
159
+ const passed = checks.filter((c) => c.found).length;
160
+ const score = total > 0 ? Math.round(passed / total * 100) / 100 : 1;
229
161
  return { score, checks, gaps };
230
162
  }
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) {
163
+ function checkIntentAlignment(intent, components) {
240
164
  const expectations = extractExpectations(intent);
241
165
  const result = verifyAlignment(components, expectations);
242
166
  return { ...result, expectations };
243
167
  }
168
+ export {
169
+ checkIntentAlignment,
170
+ extractExpectations,
171
+ verifyAlignment
172
+ };
@@ -72,7 +72,7 @@ export function extractExpectations(intent: string): Expectations {
72
72
  pattern.lastIndex = 0;
73
73
  let match;
74
74
  while ((match = pattern.exec(lower))) {
75
- const f = match[1].toLowerCase();
75
+ const f = (match[1] ?? '').toLowerCase();
76
76
  if (!fields.includes(f)) fields.push(f);
77
77
  }
78
78
  }
@@ -83,7 +83,7 @@ export function extractExpectations(intent: string): Expectations {
83
83
  pattern.lastIndex = 0;
84
84
  let match;
85
85
  while ((match = pattern.exec(lower))) {
86
- const t = match[1].toLowerCase();
86
+ const t = (match[1] ?? '').toLowerCase();
87
87
  if (!componentTypes.includes(t)) componentTypes.push(t);
88
88
  }
89
89
  }
@@ -93,9 +93,9 @@ export function extractExpectations(intent: string): Expectations {
93
93
  QUANTITY_PATTERN.lastIndex = 0;
94
94
  let qMatch;
95
95
  while ((qMatch = QUANTITY_PATTERN.exec(lower))) {
96
- const raw = qMatch[1];
96
+ const raw = qMatch[1] ?? '';
97
97
  const num = QUANTITY_MAP[raw] ?? parseInt(raw, 10);
98
- const type = qMatch[2].replace(/s$/, ''); // singularize
98
+ const type = (qMatch[2] ?? '').replace(/s$/, ''); // singularize
99
99
  if (!isNaN(num)) quantities.push({ count: num, type });
100
100
  }
101
101
 
@@ -105,7 +105,7 @@ export function extractExpectations(intent: string): Expectations {
105
105
  pattern.lastIndex = 0;
106
106
  let match;
107
107
  while ((match = pattern.exec(lower))) {
108
- const a = match[1].toLowerCase();
108
+ const a = (match[1] ?? '').toLowerCase();
109
109
  if (!actions.includes(a)) actions.push(a);
110
110
  }
111
111
  }
@@ -1,97 +1,69 @@
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
1
  const CATEGORY_RULES = [
9
2
  // 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
-
3
+ { category: "form/login", keywords: ["login", "sign in", "signin", "log in", "authentication"] },
4
+ { category: "form/signup", keywords: ["signup", "sign up", "register", "registration", "create account", "onboarding"] },
5
+ { category: "form/contact", keywords: ["contact", "contact us", "reach out", "get in touch", "enquiry", "inquiry"] },
6
+ { category: "form/settings", keywords: ["settings", "preferences", "config", "configuration", "account settings", "profile edit"] },
7
+ { category: "form/checkout", keywords: ["checkout", "check out", "payment", "billing", "purchase"] },
16
8
  // 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
-
9
+ { category: "data/table", keywords: ["table", "data table", "spreadsheet", "grid view", "list view", "datagrid"] },
10
+ { category: "data/dashboard", keywords: ["dashboard", "kpi", "metrics", "analytics", "overview", "summary panel", "stats"] },
11
+ { category: "data/chart", keywords: ["chart", "graph", "visualization", "pie chart", "bar chart", "line chart", "histogram"] },
21
12
  // 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
-
13
+ { category: "layout/landing", keywords: ["landing", "landing page", "homepage", "hero", "splash"] },
14
+ { category: "layout/profile", keywords: ["profile", "user profile", "avatar", "bio", "about me"] },
15
+ { category: "layout/pricing", keywords: ["pricing", "pricing table", "plans", "subscription", "tier"] },
26
16
  // 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
-
17
+ { category: "nav/sidebar", keywords: ["sidebar", "side nav", "navigation menu", "drawer", "side panel"] },
18
+ { category: "nav/tabs", keywords: ["tabs", "tab bar", "tabbed", "tab navigation", "tab panel"] },
30
19
  // 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
-
20
+ { category: "agent/chat", keywords: ["chat", "chatbot", "messenger", "conversation", "messaging", "chat interface"] },
21
+ { category: "agent/notification", keywords: ["notification", "alert", "toast", "snackbar", "banner", "announcement"] },
34
22
  // 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
-
23
+ { category: "content/blog", keywords: ["blog", "article", "post", "news", "editorial", "content feed"] },
24
+ { category: "content/faq", keywords: ["faq", "frequently asked", "questions", "help center", "knowledge base", "accordion"] },
38
25
  // 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
-
26
+ { category: "commerce/product", keywords: ["product", "product card", "product page", "product detail", "item detail", "catalog"] },
27
+ { category: "commerce/cart", keywords: ["cart", "shopping cart", "basket", "bag"] },
28
+ { category: "commerce/order", keywords: ["order", "order history", "order summary", "receipt", "invoice", "order tracking"] },
43
29
  // 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
-
30
+ { category: "workflow/wizard", keywords: ["wizard", "stepper", "multi-step", "step form", "onboard flow", "setup wizard"] },
31
+ { category: "workflow/kanban", keywords: ["kanban", "board", "task board", "trello", "project board", "drag and drop"] },
47
32
  // 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'] },
33
+ { category: "status/error", keywords: ["error", "error page", "404", "500", "not found", "something went wrong"] },
34
+ { category: "status/empty", keywords: ["empty state", "no results", "no data", "zero state", "blank slate"] },
35
+ { category: "status/loading", keywords: ["loading", "skeleton", "spinner", "progress", "placeholder"] }
51
36
  ];
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 };
37
+ function categorizeIntent(intent) {
38
+ if (!intent || typeof intent !== "string") {
39
+ return { category: "other", confidence: 0 };
62
40
  }
63
-
64
41
  const lower = intent.toLowerCase().trim();
65
- let bestCategory = 'other';
42
+ let bestCategory = "other";
66
43
  let bestScore = 0;
67
-
68
44
  for (const rule of CATEGORY_RULES) {
69
45
  let matchCount = 0;
70
46
  let longestMatch = 0;
71
-
72
47
  for (const kw of rule.keywords) {
73
48
  if (lower.includes(kw)) {
74
49
  matchCount++;
75
50
  longestMatch = Math.max(longestMatch, kw.length);
76
51
  }
77
52
  }
78
-
79
53
  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
54
+ const kwLenBonus = longestMatch / lower.length;
84
55
  const multiMatchBonus = Math.min(matchCount * 0.15, 0.3);
85
56
  const score = 0.5 + kwLenBonus * 0.3 + multiMatchBonus;
86
-
87
57
  if (score > bestScore) {
88
58
  bestScore = score;
89
59
  bestCategory = rule.category;
90
60
  }
91
61
  }
92
-
93
62
  return {
94
63
  category: bestCategory,
95
- confidence: Math.min(Math.round(bestScore * 100) / 100, 1),
64
+ confidence: Math.min(Math.round(bestScore * 100) / 100, 1)
96
65
  };
97
66
  }
67
+ export {
68
+ categorizeIntent
69
+ };