json-object-editor 0.10.670 → 0.10.671

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,704 @@
1
+ const OpenAI = require("openai");
2
+ const MCP = require("../modules/MCP.js");
3
+
4
+ function IntentOperator() {
5
+ var self = this;
6
+
7
+ function coloredLog(message) {
8
+ console.log(JOE.Utils.color('[intent-operator]', 'plugin', false), message);
9
+ }
10
+
11
+ function getAPIKey() {
12
+ const setting = JOE.Utils.Settings('OPENAI_API_KEY');
13
+ if (!setting) throw new Error("Missing OPENAI_API_KEY setting");
14
+ return setting;
15
+ }
16
+
17
+ function newClient() {
18
+ return new OpenAI({ apiKey: getAPIKey() });
19
+ }
20
+
21
+ /**
22
+ * Default intents (hard-coded)
23
+ */
24
+ const DEFAULT_INTENTS = [
25
+ {
26
+ name: 'create_object',
27
+ description: 'User wants to create a new object',
28
+ handoff: 'handoff_worker',
29
+ examples: ['add a task', 'create a project', 'new task called clean my room'],
30
+ operators: ['add', 'create', 'new', 'make'],
31
+ required_information: ['primary_itemtype','name of item']
32
+ },
33
+ {
34
+ name: 'update_object',
35
+ description: 'Modify existing object',
36
+ handoff: 'handoff_worker',
37
+ examples: ['update the status', 'change the name', 'edit task abc123'],
38
+ operators: ['update', 'edit', 'change', 'modify'],
39
+ required_information: ['primary_itemtype', 'object_id']
40
+ },
41
+ {
42
+ name: 'search',
43
+ description: 'Retrieve or list objects',
44
+ handoff: 'handoff_search',
45
+ examples: ['find all projects', 'show me tasks', 'list users'],
46
+ operators: ['find', 'search', 'show', 'list', 'get'],
47
+ required_information: []
48
+ },
49
+ {
50
+ name: 'chat',
51
+ description: 'Conversational/help/explanation',
52
+ handoff: 'handoff_chat',
53
+ examples: ['what can you help me with?', 'explain how this works', 'help me'],
54
+ operators: ['ask', 'help', 'explain', 'what', 'how'],
55
+ required_information: []
56
+ },
57
+ {
58
+ name: 'autofill',
59
+ description: 'Generate field values',
60
+ handoff: 'handoff_autofill',
61
+ examples: ['generate a name', 'fill in the description', 'suggest tags'],
62
+ operators: ['generate', 'fill', 'suggest', 'autofill'],
63
+ required_information: ['primary_itemtype']
64
+ },
65
+ {
66
+ name: 'analyze',
67
+ description: 'Analytical reasoning over objects',
68
+ handoff: 'handoff_worker',
69
+ examples: ['analyze the data', 'summarize projects', 'count tasks'],
70
+ operators: ['analyze', 'summarize', 'count', 'aggregate'],
71
+ required_information: ['primary_itemtype']
72
+ }
73
+ ];
74
+
75
+ /**
76
+ * Load and merge intents from setting with defaults
77
+ */
78
+ function loadIntents() {
79
+ const intents = DEFAULT_INTENTS.slice(); // Copy defaults
80
+ const intentMap = {};
81
+
82
+ // Create map of default intents by name
83
+ intents.forEach(function(intent) {
84
+ intentMap[intent.name] = intent;
85
+ });
86
+
87
+ // Load custom intents from setting
88
+ try {
89
+ const customIntents = JOE.Utils.Settings('ai_intents');
90
+ if (Array.isArray(customIntents) && customIntents.length > 0) {
91
+ customIntents.forEach(function(custom) {
92
+ if (!custom || !custom.name) return; // Skip invalid entries
93
+
94
+ if (intentMap[custom.name]) {
95
+ // Overwrite existing intent
96
+ const index = intents.findIndex(function(i) { return i.name === custom.name; });
97
+ if (index !== -1) {
98
+ intents[index] = Object.assign({}, intents[index], custom, { overwrite: true });
99
+ }
100
+ } else {
101
+ // Add new intent
102
+ intents.push(Object.assign({}, custom, { overwrite: false }));
103
+ }
104
+ });
105
+ }
106
+ } catch (e) {
107
+ coloredLog('Warning: Failed to load ai_intents setting: ' + e.message);
108
+ }
109
+
110
+ return intents;
111
+ }
112
+
113
+ /**
114
+ * Create an empty intent object structure
115
+ */
116
+ function createEmptyIntent() {
117
+ return {
118
+ intent: null,
119
+ operator: null,
120
+ operator_span: null,
121
+ primary_itemtype: null,
122
+ secondary_itemtypes: [],
123
+ needs_clarification: true,
124
+ questions: [],
125
+ confidence: 0,
126
+ next_step: null
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Calculate elapsed time in seconds with 3 decimal places
132
+ */
133
+ function calculateElapsed(startTime) {
134
+ return parseFloat(((Date.now() - startTime) / 1000).toFixed(3));
135
+ }
136
+
137
+ /**
138
+ * Create a standardized error response
139
+ */
140
+ function createErrorResponse(startTime, errors, additionalFields) {
141
+ return {
142
+ intent: createEmptyIntent(),
143
+ errors: Array.isArray(errors) ? errors : [errors],
144
+ failedat: 'intent-operator',
145
+ elapsed: calculateElapsed(startTime),
146
+ ...additionalFields
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Build context pack from all loaded schemas and MCP tools
152
+ */
153
+ function buildContextPack() {
154
+ const summaries = JOE.Schemas.summary || {};
155
+ const allowed_itemtypes = Object.keys(summaries).sort();
156
+ const itemtype_fields = {};
157
+
158
+ // Build itemtype_relationships from schema summaries
159
+ const itemtype_relationships = {};
160
+
161
+ allowed_itemtypes.forEach(function(itemtype) {
162
+ const summary = summaries[itemtype];
163
+ if (!summary || !Array.isArray(summary.fields)) {
164
+ itemtype_fields[itemtype] = [];
165
+ itemtype_relationships[itemtype] = [];
166
+ return;
167
+ }
168
+ // Extract field names, excluding system-managed fields
169
+ const fieldNames = summary.fields
170
+ .map(function(f) { return f && f.name; })
171
+ .filter(function(name) {
172
+ return name &&
173
+ name !== '_id' &&
174
+ name !== 'itemtype' &&
175
+ name !== 'joeUpdated' &&
176
+ name !== 'created';
177
+ });
178
+ itemtype_fields[itemtype] = fieldNames;
179
+
180
+ // Extract relationships from summary.relationships.outbound
181
+ if (summary.relationships && Array.isArray(summary.relationships.outbound)) {
182
+ const targetSchemas = summary.relationships.outbound.map(function(rel) {
183
+ return rel && rel.targetSchema;
184
+ }).filter(function(schema) {
185
+ return schema && allowed_itemtypes.indexOf(schema) !== -1;
186
+ });
187
+ itemtype_relationships[itemtype] = targetSchemas;
188
+ } else {
189
+ itemtype_relationships[itemtype] = [];
190
+ }
191
+ });
192
+
193
+ // Get MCP tools from minimal toolset (slimmed, safe subset)
194
+ const mcpToolNames = MCP && MCP.getToolNamesForToolset ? MCP.getToolNamesForToolset('minimal', null) : [];
195
+ const mcpTools = [];
196
+ if (mcpToolNames.length > 0 && MCP && MCP.descriptions) {
197
+ mcpToolNames.forEach(function(toolName) {
198
+ const description = MCP.descriptions[toolName] || '';
199
+ mcpTools.push({
200
+ name: toolName,
201
+ description: description
202
+ });
203
+ });
204
+ }
205
+
206
+ return {
207
+ allowed_itemtypes: allowed_itemtypes,
208
+ itemtype_fields: itemtype_fields,
209
+ itemtype_relationships: itemtype_relationships,
210
+ mcp_tools: mcpTools
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Infer schemas_to_load from itemtypes and relationships
216
+ */
217
+ function inferSchemasToLoad(primaryItemtype, secondaryItemtypes, contextPack) {
218
+ const schemas = new Set();
219
+
220
+ // Add primary and secondary itemtypes
221
+ if (primaryItemtype) {
222
+ schemas.add(primaryItemtype);
223
+ }
224
+ if (Array.isArray(secondaryItemtypes)) {
225
+ secondaryItemtypes.forEach(function(it) {
226
+ if (it) schemas.add(it);
227
+ });
228
+ }
229
+
230
+ // Add all target schemas from relationships
231
+ if (primaryItemtype && contextPack.itemtype_relationships) {
232
+ const relationships = contextPack.itemtype_relationships[primaryItemtype] || [];
233
+ relationships.forEach(function(targetSchema) {
234
+ if (targetSchema) schemas.add(targetSchema);
235
+ });
236
+ }
237
+
238
+ // Add relationships from secondary itemtypes too
239
+ if (Array.isArray(secondaryItemtypes) && contextPack.itemtype_relationships) {
240
+ secondaryItemtypes.forEach(function(secondary) {
241
+ if (secondary) {
242
+ const relationships = contextPack.itemtype_relationships[secondary] || [];
243
+ relationships.forEach(function(targetSchema) {
244
+ if (targetSchema) schemas.add(targetSchema);
245
+ });
246
+ }
247
+ });
248
+ }
249
+
250
+ return Array.from(schemas).sort();
251
+ }
252
+
253
+ /**
254
+ * Extract JSON from response text (handles markdown code blocks)
255
+ */
256
+ function extractJsonText(text) {
257
+ if (!text) return null;
258
+ // Try to find JSON in markdown code blocks
259
+ const jsonMatch = text.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/);
260
+ if (jsonMatch) return jsonMatch[1];
261
+ // Try to find JSON object directly
262
+ const objMatch = text.match(/\{[\s\S]*\}/);
263
+ if (objMatch) return objMatch[0];
264
+ return null;
265
+ }
266
+
267
+ /**
268
+ * Validate and normalize intent response
269
+ */
270
+ function validateIntent(rawResponse, contextPack, intents) {
271
+ const errors = [];
272
+ const warnings = [];
273
+ const allowedIntents = intents.map(function(i) { return i.name; });
274
+ const allowedItemtypes = contextPack.allowed_itemtypes || [];
275
+
276
+ // Ensure required fields exist
277
+ const result = {
278
+ intent: rawResponse.intent || null,
279
+ operator: rawResponse.operator || null,
280
+ operator_span: rawResponse.operator_span || null,
281
+ primary_itemtype: rawResponse.primary_itemtype || null,
282
+ secondary_itemtypes: Array.isArray(rawResponse.secondary_itemtypes) ? rawResponse.secondary_itemtypes : [],
283
+ needs_clarification: rawResponse.needs_clarification === true,
284
+ questions: Array.isArray(rawResponse.questions) ? rawResponse.questions : [],
285
+ confidence: typeof rawResponse.confidence === 'number' ? Math.max(0, Math.min(1, rawResponse.confidence)) : 0,
286
+ next_step: rawResponse.next_step || null
287
+ };
288
+
289
+ // Extract extracted_raw for validation (will be moved to next_step.data)
290
+ const extracted_raw = (rawResponse.extracted_raw && typeof rawResponse.extracted_raw === 'object') ? rawResponse.extracted_raw : {};
291
+
292
+ // Validate intent
293
+ if (!result.intent) {
294
+ errors.push('Missing required field: intent');
295
+ } else if (allowedIntents.indexOf(result.intent) === -1) {
296
+ errors.push('Invalid intent: ' + result.intent + ' (must be one of: ' + allowedIntents.join(', ') + ')');
297
+ result.intent = null;
298
+ }
299
+
300
+ // Validate primary_itemtype
301
+ if (result.primary_itemtype && allowedItemtypes.indexOf(result.primary_itemtype) === -1) {
302
+ warnings.push('primary_itemtype "' + result.primary_itemtype + '" not in allowed list; setting to null');
303
+ result.primary_itemtype = null;
304
+ }
305
+
306
+ // Filter secondary_itemtypes
307
+ const validSecondary = result.secondary_itemtypes.filter(function(it) {
308
+ return allowedItemtypes.indexOf(it) !== -1;
309
+ });
310
+ if (validSecondary.length !== result.secondary_itemtypes.length) {
311
+ warnings.push('Some secondary_itemtypes were filtered out (not in allowed list)');
312
+ }
313
+ result.secondary_itemtypes = validSecondary;
314
+
315
+ // Check semantic consistency between primary_itemtype and secondary_itemtypes
316
+ if (result.primary_itemtype && result.secondary_itemtypes.length > 0 && contextPack.itemtype_relationships) {
317
+ const primaryRelationships = contextPack.itemtype_relationships[result.primary_itemtype] || [];
318
+ const mismatches = result.secondary_itemtypes.filter(function(secondary) {
319
+ // Check if primary can reference this secondary itemtype
320
+ return primaryRelationships.indexOf(secondary) === -1;
321
+ });
322
+
323
+ if (mismatches.length > 0) {
324
+ warnings.push('Semantic mismatch: ' + result.primary_itemtype + ' does not reference ' + mismatches.join(', ') +
325
+ ' (valid references: ' + (primaryRelationships.length > 0 ? primaryRelationships.join(', ') : 'none') + ')');
326
+ // Force clarification when semantic mismatch detected
327
+ if (!result.needs_clarification) {
328
+ result.needs_clarification = true;
329
+ }
330
+ if (result.questions.length === 0) {
331
+ result.questions.push('Did you mean a different itemtype? ' + result.primary_itemtype +
332
+ ' typically relates to ' + (primaryRelationships.length > 0 ? primaryRelationships.join(', ') : 'nothing') +
333
+ ', but you mentioned ' + mismatches.join(', ') + '.');
334
+ }
335
+ }
336
+ }
337
+
338
+ // Filter extracted fields by field whitelist
339
+ let validatedExtracted = {};
340
+ if (result.primary_itemtype && contextPack.itemtype_fields[result.primary_itemtype]) {
341
+ const allowedFields = contextPack.itemtype_fields[result.primary_itemtype];
342
+ const rawKeys = Object.keys(extracted_raw);
343
+ rawKeys.forEach(function(key) {
344
+ if (allowedFields.indexOf(key) === -1) {
345
+ warnings.push('Field "' + key + '" in extracted_raw not in whitelist for ' + result.primary_itemtype);
346
+ } else {
347
+ validatedExtracted[key] = extracted_raw[key];
348
+ }
349
+ });
350
+ } else {
351
+ validatedExtracted = extracted_raw;
352
+ }
353
+
354
+ // Set next_step based on intent if missing
355
+ if (result.intent && !result.next_step) {
356
+ const intentDef = intents.find(function(i) { return i.name === result.intent; });
357
+ if (intentDef && intentDef.handoff) {
358
+ // Convert handoff string to structured object
359
+ result.next_step = {
360
+ handler: intentDef.handoff,
361
+ prompt: null,
362
+ data: null
363
+ };
364
+ }
365
+ }
366
+
367
+ // Validate next_step structure if present
368
+ if (result.next_step) {
369
+ if (typeof result.next_step === 'string') {
370
+ // Legacy string format - convert to structured
371
+ result.next_step = {
372
+ handler: result.next_step,
373
+ prompt: null,
374
+ data: null
375
+ };
376
+ } else if (typeof result.next_step === 'object' && result.next_step !== null) {
377
+ // Ensure structured format has required fields
378
+ if (!result.next_step.handler) {
379
+ result.next_step.handler = null;
380
+ }
381
+ if (result.next_step.prompt === undefined) {
382
+ result.next_step.prompt = null;
383
+ }
384
+ if (result.next_step.data === undefined) {
385
+ result.next_step.data = null;
386
+ }
387
+ } else {
388
+ result.next_step = null;
389
+ }
390
+ }
391
+
392
+ // Move extracted_raw to next_step.data (if next_step exists)
393
+ if (result.next_step && Object.keys(validatedExtracted).length > 0) {
394
+ // Merge with existing data if present, otherwise set to extracted fields
395
+ if (result.next_step.data && typeof result.next_step.data === 'object') {
396
+ result.next_step.data = Object.assign({}, result.next_step.data, validatedExtracted);
397
+ } else {
398
+ result.next_step.data = validatedExtracted;
399
+ }
400
+ }
401
+
402
+ return {
403
+ intent: result,
404
+ errors: errors,
405
+ warnings: warnings
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Validate objects_to_resolve array
411
+ */
412
+ function validateObjectsToResolve(objectsToResolve, contextPack, primaryItemtype) {
413
+ const errors = [];
414
+ const warnings = [];
415
+ const validated = [];
416
+
417
+ if (!Array.isArray(objectsToResolve)) {
418
+ return { objects: [], errors: ['objects_to_resolve must be an array'], warnings: [] };
419
+ }
420
+
421
+ objectsToResolve.forEach(function(obj, index) {
422
+ if (!obj || typeof obj !== 'object') {
423
+ warnings.push('objects_to_resolve[' + index + '] is not a valid object');
424
+ return;
425
+ }
426
+
427
+ const itemtype = obj.itemtype;
428
+ const name = obj.name;
429
+ const field = obj.field;
430
+
431
+ if (!itemtype || typeof itemtype !== 'string') {
432
+ errors.push('objects_to_resolve[' + index + '] missing or invalid itemtype');
433
+ return;
434
+ }
435
+
436
+ if (!name || typeof name !== 'string') {
437
+ errors.push('objects_to_resolve[' + index + '] missing or invalid name');
438
+ return;
439
+ }
440
+
441
+ if (!field || typeof field !== 'string') {
442
+ errors.push('objects_to_resolve[' + index + '] missing or invalid field');
443
+ return;
444
+ }
445
+
446
+ // Validate itemtype exists
447
+ if (contextPack.allowed_itemtypes.indexOf(itemtype) === -1) {
448
+ warnings.push('objects_to_resolve[' + index + '] itemtype "' + itemtype + '" not in allowed list');
449
+ }
450
+
451
+ // Validate field exists and is a reference field for primary_itemtype
452
+ if (primaryItemtype && contextPack.itemtype_fields[primaryItemtype]) {
453
+ const allowedFields = contextPack.itemtype_fields[primaryItemtype];
454
+ if (allowedFields.indexOf(field) === -1) {
455
+ warnings.push('objects_to_resolve[' + index + '] field "' + field + '" not in allowed fields for ' + primaryItemtype);
456
+ }
457
+ }
458
+
459
+ // Build validated object
460
+ const validatedObj = {
461
+ itemtype: itemtype,
462
+ name: name,
463
+ field: field,
464
+ operation: obj.operation || 'link',
465
+ purpose: obj.purpose || 'Resolve ' + itemtype + ' name "' + name + '" to _id for ' + field + ' field'
466
+ };
467
+
468
+ validated.push(validatedObj);
469
+ });
470
+
471
+ return {
472
+ objects: validated,
473
+ errors: errors,
474
+ warnings: warnings
475
+ };
476
+ }
477
+
478
+ /**
479
+ * Main handler: classify intent from natural language
480
+ */
481
+ this.default = async function(data, req, res) {
482
+ const startTime = Date.now();
483
+ try {
484
+ const text = (data && data.text) || '';
485
+ if (!text || typeof text !== 'string' || !text.trim()) {
486
+ return createErrorResponse(startTime, ['Missing or empty "text" parameter']);
487
+ }
488
+
489
+ // Build context pack
490
+ const contextPack = buildContextPack();
491
+
492
+ // Check if context pack is empty
493
+ if (!contextPack.allowed_itemtypes || contextPack.allowed_itemtypes.length === 0) {
494
+ return createErrorResponse(startTime, ['No schemas loaded - context pack is empty']);
495
+ }
496
+
497
+ coloredLog('Context pack: ' + contextPack.allowed_itemtypes.length + ' itemtypes');
498
+
499
+ // Log prompt size for debugging
500
+ const contextPackSize = JSON.stringify(contextPack.itemtype_fields).length;
501
+ coloredLog('Context pack size: ' + contextPackSize + ' characters');
502
+
503
+ // Load intents (used for both prompt and validation)
504
+ const intents = loadIntents();
505
+ const allowedIntents = intents.map(function(i) { return i.name; });
506
+
507
+ // Build intent descriptions for prompt (including required_information)
508
+ const intentDescriptions = intents.map(function(i) {
509
+ let desc = i.name + ': ' + i.description;
510
+ if (i.required_information && Array.isArray(i.required_information) && i.required_information.length > 0) {
511
+ desc += ' (requires: ' + i.required_information.join(', ') + ')';
512
+ }
513
+ return desc;
514
+ }).join('\n');
515
+
516
+ // Build MCP tools description for prompt
517
+ let mcpToolsText = '';
518
+ if (contextPack.mcp_tools && contextPack.mcp_tools.length > 0) {
519
+ mcpToolsText = '\nAvailable MCP tools:\n' +
520
+ contextPack.mcp_tools.map(function(tool) {
521
+ return '- ' + tool.name + ': ' + tool.description;
522
+ }).join('\n') + '\n';
523
+ }
524
+
525
+ // Build relationships description for prompt
526
+ let relationshipsText = '';
527
+ if (contextPack.itemtype_relationships) {
528
+ relationshipsText = '\nItemtype relationships (which itemtypes can reference which):\n';
529
+ Object.keys(contextPack.itemtype_relationships).forEach(function(itemtype) {
530
+ const refs = contextPack.itemtype_relationships[itemtype];
531
+ if (refs && refs.length > 0) {
532
+ relationshipsText += '- ' + itemtype + ' can reference: ' + refs.join(', ') + '\n';
533
+ }
534
+ });
535
+ relationshipsText += '\n';
536
+ }
537
+
538
+ // Build prompt
539
+ const prompt = [
540
+ 'You are JOE (Json Object Editor) Intent Classifier.',
541
+ '',
542
+ 'Task: Analyze the user input and return a structured intent JSON object.',
543
+ '',
544
+ 'Allowed intents:',
545
+ intentDescriptions,
546
+ '',
547
+ 'Intent names: ' + allowedIntents.join(', '),
548
+ '',
549
+ 'Available itemtypes: ' + contextPack.allowed_itemtypes.join(', '),
550
+ '',
551
+ 'For each itemtype, allowed fields:',
552
+ JSON.stringify(contextPack.itemtype_fields, null, 2),
553
+ relationshipsText,
554
+ mcpToolsText,
555
+ 'Output format (JSON only, no markdown):',
556
+ '{',
557
+ ' "intent": "one of the allowed intents",',
558
+ ' "operator": "normalized verb label (e.g., add, create, edit, find, show)",',
559
+ ' "operator_span": "literal phrase from input (optional)",',
560
+ ' "primary_itemtype": "main object type being acted on (or null)",',
561
+ ' "secondary_itemtypes": ["related object types"],',
562
+ ' "needs_clarification": false,',
563
+ ' "questions": [],',
564
+ ' "confidence": 0.0,',
565
+ ' "next_step": { "handler": "handler_name", "prompt": "optional prompt text", "data": { "field_name": "extracted value" } },',
566
+ ' "execution_plan": ["step 1", "step 2", "step 3"],',
567
+ ' "objects_to_resolve": [',
568
+ ' { "itemtype": "project", "name": "home", "field": "project", "operation": "link", "purpose": "Resolve project name to _id" }',
569
+ ' ],',
570
+ ' "schemas_to_load": ["task", "project", "status"]',
571
+ '}',
572
+ '',
573
+ 'Rules:',
574
+ '- Choose intent from allowed list only.',
575
+ '- Choose itemtypes from available list only.',
576
+ '- CRITICAL: Check semantic consistency using the itemtype_relationships data. If the input mentions a secondary itemtype (e.g., "project"), ensure your primary_itemtype can reference it. If primary_itemtype cannot reference the mentioned secondary_itemtype, set needs_clarification=true and add a clarifying question.',
577
+ '- CRITICAL: If you detect a typo or uncertain word that might be an itemtype (e.g., "tack" might be "task", "projct" might be "project"), set needs_clarification=true and add a question like "Did you mean \'task\' instead of \'tack\'?"',
578
+ '- CRITICAL: If there is a semantic mismatch (e.g., primary_itemtype="page" but context mentions "project", when page references "site" not "project"), set needs_clarification=true and add a clarifying question.',
579
+ '- Only extract fields that exist in the field whitelist for the primary_itemtype.',
580
+ '- Put extracted field values in next_step.data (as an object with field names as keys).',
581
+ '- CRITICAL: Identify objects that need resolution: If a field value in next_step.data is a name (not an _id/CUID format), check the itemtype_relationships for the primary_itemtype. If the field name matches a relationship target (e.g., if task can reference "project" and you have field "project": "home"), then "home" is a name that needs fuzzy search to get _id. Add to objects_to_resolve with: itemtype (the targetSchema from relationships), name (the value), field (field name), operation (link/update/reference), and purpose.',
582
+ '- Example: If next_step.data has { "project": "home" } and primary_itemtype is "task", check itemtype_relationships["task"]. If it includes "project", then add { "itemtype": "project", "name": "home", "field": "project", "operation": "link", "purpose": "Resolve project name \'home\' to _id for linking task" } to objects_to_resolve.',
583
+ '- Only add to objects_to_resolve if the value looks like a name (not a CUID - CUIDs are long alphanumeric strings like "clx123abc456def789").',
584
+ '- CRITICAL: For schemas_to_load, include primary_itemtype, secondary_itemtypes, and all targetSchema values from itemtype_relationships. You can also add additional schemas if needed for the operation (e.g., if you need to validate field types).',
585
+ '- Set needs_clarification=true if ambiguous or missing required info (check required_information for each intent).',
586
+ '- Set confidence conservatively - if itemtype is uncertain, there are typos, or semantic mismatches, use lower confidence (0.3-0.6) and set needs_clarification=true.',
587
+ '- next_step should be a structured object with handler (the handoff handler name), prompt (optional descriptive text), and data (object with extracted field values).',
588
+ '- Always generate execution_plan as an array of strings describing the steps needed.',
589
+ '- In execution_plan, reference MCP tools consistently using format: "Call MCP tool: toolName" (e.g., "Call MCP tool: getObject to fetch task abc123", "Call MCP tool: saveObject to persist the task").',
590
+ '- Use real schema names and field names from the context pack.',
591
+ '- Keep steps at medium detail level (e.g., "Create a task object", "Set name field to clean my room", "Call MCP tool: saveObject to persist").',
592
+ '- Even if clarification is needed, describe what you think the plan should be.',
593
+ '',
594
+ 'User input: ' + text
595
+ ].join('\n');
596
+
597
+ // Call OpenAI
598
+ // Using gpt-4o-mini as it's proven fast and available (gpt-5-nano may not exist yet)
599
+ const openai = newClient();
600
+ const apiStartTime = Date.now();
601
+ const response = await openai.responses.create({
602
+ //model: 'gpt-4o-mini',
603
+ model: 'gpt-4.1-nano',
604
+
605
+ instructions: 'You are a JSON-only output assistant. Return only valid JSON, no markdown, no explanations.',
606
+ input: prompt
607
+ });
608
+ const apiElapsed = ((Date.now() - apiStartTime) / 1000).toFixed(3);
609
+ coloredLog('OpenAI API call took: ' + apiElapsed + ' seconds');
610
+
611
+ const rawText = response.output_text || '';
612
+ const jsonText = extractJsonText(rawText);
613
+
614
+ if (!jsonText) {
615
+ return createErrorResponse(startTime, ['Failed to extract JSON from model response'], {
616
+ raw_response: rawText.substring(0, 500)
617
+ });
618
+ }
619
+
620
+ let rawResponse;
621
+ try {
622
+ rawResponse = JSON.parse(jsonText);
623
+ } catch (e) {
624
+ return createErrorResponse(startTime, ['Failed to parse JSON: ' + e.message], {
625
+ raw_response: rawText.substring(0, 500)
626
+ });
627
+ }
628
+
629
+ // Validate and normalize (using intents loaded above)
630
+ const validated = validateIntent(rawResponse, contextPack, intents);
631
+
632
+ // Extract execution_plan from raw response (not from intent object)
633
+ const execution_plan = Array.isArray(rawResponse.execution_plan) ? rawResponse.execution_plan : [];
634
+
635
+ // Validate objects_to_resolve
636
+ const objectsToResolveRaw = Array.isArray(rawResponse.objects_to_resolve) ? rawResponse.objects_to_resolve : [];
637
+ const objectsToResolveValidated = validateObjectsToResolve(objectsToResolveRaw, contextPack, validated.intent.primary_itemtype);
638
+
639
+ // Infer schemas_to_load and merge with model-provided ones
640
+ const inferredSchemas = inferSchemasToLoad(
641
+ validated.intent.primary_itemtype,
642
+ validated.intent.secondary_itemtypes,
643
+ contextPack
644
+ );
645
+ const modelSchemas = Array.isArray(rawResponse.schemas_to_load) ? rawResponse.schemas_to_load : [];
646
+ const allSchemas = new Set([...inferredSchemas, ...modelSchemas]);
647
+ const schemas_to_load = Array.from(allSchemas).sort();
648
+
649
+ // Combine errors and warnings
650
+ const allErrors = [...validated.errors, ...objectsToResolveValidated.errors];
651
+ const allWarnings = [...validated.warnings, ...objectsToResolveValidated.warnings];
652
+
653
+ const result = {
654
+ raw_request: text,
655
+ intent: validated.intent,
656
+ execution_plan: execution_plan,
657
+ objects_to_resolve: objectsToResolveValidated.objects.length > 0 ? objectsToResolveValidated.objects : undefined,
658
+ schemas_to_load: schemas_to_load.length > 0 ? schemas_to_load : undefined,
659
+ errors: allErrors.length > 0 ? allErrors : undefined,
660
+ warnings: allWarnings.length > 0 ? allWarnings : undefined,
661
+ validation_log: (allErrors.length > 0 || allWarnings.length > 0) ? {
662
+ errors: allErrors,
663
+ warnings: allWarnings
664
+ } : undefined,
665
+ elapsed: calculateElapsed(startTime)
666
+ };
667
+
668
+ return result;
669
+
670
+ } catch (e) {
671
+ coloredLog('Error: ' + e.message);
672
+ return createErrorResponse(startTime, ['Plugin error: ' + e.message]);
673
+ }
674
+ };
675
+
676
+ /**
677
+ * API endpoint: Get all intents (defaults + custom from setting)
678
+ */
679
+ this.intents = function(data, req, res) {
680
+ try {
681
+ const intents = loadIntents();
682
+ return {
683
+ intents: intents,
684
+ count: intents.length,
685
+ defaults_count: DEFAULT_INTENTS.length,
686
+ custom_count: intents.length - DEFAULT_INTENTS.length
687
+ };
688
+ } catch (e) {
689
+ coloredLog('Error loading intents: ' + e.message);
690
+ return {
691
+ errors: ['Failed to load intents: ' + e.message],
692
+ failedat: 'intent-operator'
693
+ };
694
+ }
695
+ };
696
+
697
+ this.async = {
698
+ default: true
699
+ };
700
+ this.protected = [];
701
+ return self;
702
+ }
703
+
704
+ module.exports = new IntentOperator();