json-object-editor 0.10.657 → 0.10.662
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 +15 -1
- package/css/joe-styles.css +1 -1
- package/css/joe.css +2 -2
- package/css/joe.min.css +1 -1
- package/docs/JOE_AI_Overview.md +73 -17
- package/docs/React_Form_Integration_Example.md +299 -0
- package/docs/React_Form_Integration_Strategy.md +397 -0
- package/dummy +9 -1
- package/form-qs.json +1007 -0
- package/js/joe-ai.js +257 -31
- package/js/joe-react-form.js +608 -0
- package/js/joe.js +1 -1
- package/package.json +1 -1
- package/react-form-spa-ex.js +570 -0
- package/readme.md +13 -1
- package/server/fields/core.js +52 -2
- package/server/modules/MCP.js +4 -0
- package/server/modules/Sites.js +28 -2
- package/server/plugins/chatgpt.js +304 -24
- package/server/plugins/formBuilder.js +98 -0
- package/server/schemas/ai_assistant.js +58 -45
- package/server/schemas/ai_prompt.js +4 -58
- package/server/schemas/ai_widget_conversation.js +31 -0
- package/server/schemas/include.js +8 -3
- package/server/schemas/page.js +6 -0
package/server/modules/Sites.js
CHANGED
|
@@ -48,6 +48,9 @@ function getInclude(req,res,next){
|
|
|
48
48
|
case 'css':
|
|
49
49
|
res.set('Content-Type', 'text/css');
|
|
50
50
|
break;
|
|
51
|
+
case 'json':
|
|
52
|
+
res.set('Content-Type', 'application/json');
|
|
53
|
+
break;
|
|
51
54
|
}
|
|
52
55
|
res.send(content);
|
|
53
56
|
}
|
|
@@ -536,8 +539,31 @@ var routeWithoutSlash = route.startsWith('/') ? route.slice(1) : route;
|
|
|
536
539
|
page.content = Editor.wrap(page.content,'jpe-content',{name:'page content'})
|
|
537
540
|
}
|
|
538
541
|
|
|
539
|
-
|
|
540
|
-
|
|
542
|
+
// First, process page content with fillTemplate to resolve variables like ${INCLUDES}, ${this.PAGE.name}, etc.
|
|
543
|
+
// This is needed because these variables are INSIDE the page content
|
|
544
|
+
var pageContent = content.PAGE.content || '';
|
|
545
|
+
pageContent = pageContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
546
|
+
|
|
547
|
+
// Preserve newlines before fillTemplate strips them
|
|
548
|
+
// Replace newlines with a temporary marker that we'll restore later
|
|
549
|
+
var newlineMarker = '__JOE_NEWLINE__' + Date.now() + '__';
|
|
550
|
+
var pageContentWithMarkers = pageContent.replace(/\n/g, newlineMarker);
|
|
551
|
+
|
|
552
|
+
// Process page content with fillTemplate to resolve template variables
|
|
553
|
+
var processedPageContent = fillTemplate(pageContentWithMarkers, content);
|
|
554
|
+
|
|
555
|
+
// Restore newlines after fillTemplate processing
|
|
556
|
+
processedPageContent = processedPageContent.replace(new RegExp(newlineMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '\n');
|
|
557
|
+
|
|
558
|
+
// Use a placeholder for ${this.PAGE.content} in layout template
|
|
559
|
+
var placeholder = '__JOE_PAGE_CONTENT_PLACEHOLDER__' + Date.now() + '__';
|
|
560
|
+
var template = layout.template.replace(/\$\{this\.PAGE\.content\}/g, placeholder);
|
|
561
|
+
|
|
562
|
+
// Process layout template with fillTemplate (which will strip newlines from layout, but our placeholder remains)
|
|
563
|
+
var html = fillTemplate(template, content);
|
|
564
|
+
|
|
565
|
+
// Replace placeholder with processed page content (which has newlines preserved)
|
|
566
|
+
html = html.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), processedPageContent);
|
|
541
567
|
if(editor){
|
|
542
568
|
html = html.replace('</head>',Editor.styles+'</head>')
|
|
543
569
|
.replace('</body>',Editor.gui(layout)+Editor.gui(page)+Editor.menu
|
|
@@ -19,7 +19,28 @@ function ChatGPT() {
|
|
|
19
19
|
var self = this;
|
|
20
20
|
this.async ={};
|
|
21
21
|
function coloredLog(message){
|
|
22
|
-
|
|
22
|
+
try{
|
|
23
|
+
// Only emit verbose plugin logs in non‑production environments.
|
|
24
|
+
// This keeps consoles clean in production while preserving rich
|
|
25
|
+
// traces (assistant resolution, MCP config, systemText) during
|
|
26
|
+
// local/dev debugging.
|
|
27
|
+
var env = null;
|
|
28
|
+
if (typeof JOE !== 'undefined' && JOE && JOE.webconfig && JOE.webconfig.env){
|
|
29
|
+
env = JOE.webconfig.env;
|
|
30
|
+
} else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV){
|
|
31
|
+
env = process.env.NODE_ENV;
|
|
32
|
+
}
|
|
33
|
+
if (env && env.toLowerCase() === 'production'){
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(JOE.Utils.color('[chatgpt]', 'plugin', false), message);
|
|
37
|
+
}catch(_e){
|
|
38
|
+
// If anything goes wrong determining env, default to logging so
|
|
39
|
+
// that development debugging is not silently broken.
|
|
40
|
+
try{
|
|
41
|
+
console.log('[chatgpt]', message);
|
|
42
|
+
}catch(__e){}
|
|
43
|
+
}
|
|
23
44
|
}
|
|
24
45
|
//xx -setup and send a test prompt to chatgpt
|
|
25
46
|
//xx get the api key from joe settings
|
|
@@ -58,6 +79,23 @@ function ChatGPT() {
|
|
|
58
79
|
const summary = JOE.Schemas && JOE.Schemas.summary && JOE.Schemas.summary[name];
|
|
59
80
|
return { full, summary };
|
|
60
81
|
}
|
|
82
|
+
function buildMcpToolsFromConfig(cfg) {
|
|
83
|
+
if (!cfg || !cfg.mcp_enabled) {
|
|
84
|
+
return { tools: null, names: [] };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const names = MCP.getToolNamesForToolset(
|
|
88
|
+
cfg.mcp_toolset || 'read-only',
|
|
89
|
+
Array.isArray(cfg.mcp_selected_tools) ? cfg.mcp_selected_tools : null
|
|
90
|
+
);
|
|
91
|
+
const defs = MCP.getToolDefinitions(names);
|
|
92
|
+
return { tools: defs, names: names };
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.warn('[chatgpt] buildMcpToolsFromConfig failed', e);
|
|
95
|
+
return { tools: null, names: [] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
61
99
|
/**
|
|
62
100
|
* callMCPTool
|
|
63
101
|
*
|
|
@@ -270,29 +308,32 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
|
|
|
270
308
|
const attachmentsMode = opts.attachments_mode || null;
|
|
271
309
|
const openaiFileIds = opts.openai_file_ids || null;
|
|
272
310
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
311
|
+
// Debug/trace: log the effective system instructions going into this
|
|
312
|
+
// Responses+tools call. This helps verify assistant + MCP instructions
|
|
313
|
+
// wiring across prompts, assists, autofill, and widget chat.
|
|
314
|
+
try{
|
|
315
|
+
coloredLog('runWithTools systemText:\n' + systemText);
|
|
316
|
+
}catch(_e){}
|
|
317
|
+
|
|
318
|
+
// Normalize tools: manual assistant.tools plus optional MCP tools
|
|
319
|
+
let manualTools = null;
|
|
276
320
|
if (assistant && assistant.tools) {
|
|
277
321
|
if (Array.isArray(assistant.tools)) {
|
|
278
|
-
|
|
322
|
+
manualTools = assistant.tools;
|
|
279
323
|
} else if (typeof assistant.tools === 'string') {
|
|
280
324
|
try {
|
|
281
325
|
const parsed = JSON.parse(assistant.tools);
|
|
282
326
|
if (Array.isArray(parsed)) {
|
|
283
|
-
|
|
327
|
+
manualTools = parsed;
|
|
284
328
|
}
|
|
285
329
|
} catch (e) {
|
|
286
330
|
console.error('[chatgpt] Failed to parse assistant.tools JSON', e);
|
|
287
331
|
}
|
|
288
332
|
}
|
|
289
333
|
}
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// { type:'function', name:'x', description:'...', parameters:{...} }
|
|
294
|
-
if (Array.isArray(tools)) {
|
|
295
|
-
tools = tools.map(function (t) {
|
|
334
|
+
// Flatten any Assistants-style function definitions
|
|
335
|
+
if (Array.isArray(manualTools)) {
|
|
336
|
+
manualTools = manualTools.map(function (t) {
|
|
296
337
|
if (t && t.type === 'function' && t.function && !t.name) {
|
|
297
338
|
const fn = t.function || {};
|
|
298
339
|
return {
|
|
@@ -306,6 +347,26 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
|
|
|
306
347
|
});
|
|
307
348
|
}
|
|
308
349
|
|
|
350
|
+
// Merge manual tools with MCP tools (manual wins on name collisions).
|
|
351
|
+
let tools = null;
|
|
352
|
+
const mergedByName = {};
|
|
353
|
+
const mcp = buildMcpToolsFromConfig(assistant || {});
|
|
354
|
+
const mcpTools = Array.isArray(mcp.tools) ? mcp.tools : null;
|
|
355
|
+
|
|
356
|
+
if (Array.isArray(manualTools) || Array.isArray(mcpTools)) {
|
|
357
|
+
tools = [];
|
|
358
|
+
(manualTools || []).forEach(function(t){
|
|
359
|
+
if (!t) { return; }
|
|
360
|
+
if (t.name) { mergedByName[t.name] = true; }
|
|
361
|
+
tools.push(t);
|
|
362
|
+
});
|
|
363
|
+
(mcpTools || []).forEach(function(t){
|
|
364
|
+
if (!t || !t.name) { return; }
|
|
365
|
+
if (mergedByName[t.name]) { return; }
|
|
366
|
+
tools.push(t);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
309
370
|
// No tools configured – do a simple single Responses call.
|
|
310
371
|
if (!tools) {
|
|
311
372
|
const resp = await openai.responses.create({
|
|
@@ -1096,6 +1157,24 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1096
1157
|
return { error: "Prompt not found." };
|
|
1097
1158
|
}
|
|
1098
1159
|
|
|
1160
|
+
// If this prompt run is associated with a JOE ai_assistant, log which
|
|
1161
|
+
// assistant is being used so we can debug "which agent handled this?"
|
|
1162
|
+
try{
|
|
1163
|
+
const aiAssistantId = data.ai_assistant_id || null;
|
|
1164
|
+
if (aiAssistantId) {
|
|
1165
|
+
let asst = null;
|
|
1166
|
+
try{
|
|
1167
|
+
// Prefer explicit ai_assistant schema lookup, then fallback
|
|
1168
|
+
// to a generic get in case datasets are flattened.
|
|
1169
|
+
asst = $J.get(aiAssistantId,'ai_assistant') || $J.get(aiAssistantId);
|
|
1170
|
+
}catch(_e){}
|
|
1171
|
+
const label = asst && (asst.name || asst.title || asst.info || asst._id) || aiAssistantId;
|
|
1172
|
+
coloredLog('[prompt] executeJOEAiPrompt using ai_assistant: '
|
|
1173
|
+
+ label + ' [' + aiAssistantId + ']'
|
|
1174
|
+
+ ' for prompt: ' + (prompt.name || prompt._id));
|
|
1175
|
+
}
|
|
1176
|
+
}catch(_e){}
|
|
1177
|
+
|
|
1099
1178
|
let instructions = prompt.instructions || "";
|
|
1100
1179
|
let finalInstructions=instructions;
|
|
1101
1180
|
let finalInput='';
|
|
@@ -1474,9 +1553,15 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1474
1553
|
system: system,
|
|
1475
1554
|
messages: [],
|
|
1476
1555
|
source: body.source || "widget",
|
|
1556
|
+
// Optional scope for object-scoped widget chats
|
|
1557
|
+
scope_itemtype: body.scope_itemtype || null,
|
|
1558
|
+
scope_id: body.scope_id || null,
|
|
1477
1559
|
created: new Date().toISOString(),
|
|
1478
1560
|
joeUpdated: new Date().toISOString()
|
|
1479
1561
|
};
|
|
1562
|
+
if (body.name && !convo.name) {
|
|
1563
|
+
convo.name = String(body.name);
|
|
1564
|
+
}
|
|
1480
1565
|
|
|
1481
1566
|
const saved = await new Promise(function (resolve, reject) {
|
|
1482
1567
|
// Widget conversations are lightweight and do not need full history diffs.
|
|
@@ -1598,7 +1683,76 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1598
1683
|
return { success: false, error: "Conversation not found" };
|
|
1599
1684
|
}
|
|
1600
1685
|
|
|
1686
|
+
// Best-effort: if this is an object-scoped conversation and we have
|
|
1687
|
+
// not yet attached any files, walk the scoped object for uploader
|
|
1688
|
+
// style files that have OpenAI ids and cache them on the convo.
|
|
1689
|
+
try{
|
|
1690
|
+
if ((!convo.attached_openai_file_ids || !convo.attached_openai_file_ids.length) &&
|
|
1691
|
+
convo.scope_itemtype && convo.scope_id) {
|
|
1692
|
+
var scopedObj = null;
|
|
1693
|
+
try{
|
|
1694
|
+
scopedObj = $J.get(convo.scope_id, convo.scope_itemtype) || $J.get(convo.scope_id);
|
|
1695
|
+
}catch(_e){}
|
|
1696
|
+
if (scopedObj && typeof scopedObj === 'object') {
|
|
1697
|
+
var ids = [];
|
|
1698
|
+
var meta = [];
|
|
1699
|
+
Object.keys(scopedObj).forEach(function(field){
|
|
1700
|
+
var val = scopedObj[field];
|
|
1701
|
+
if (!Array.isArray(val)) { return; }
|
|
1702
|
+
val.forEach(function(f){
|
|
1703
|
+
if (f && f.openai_file_id) {
|
|
1704
|
+
ids.push(f.openai_file_id);
|
|
1705
|
+
meta.push({
|
|
1706
|
+
itemtype: scopedObj.itemtype || convo.scope_itemtype,
|
|
1707
|
+
field: field,
|
|
1708
|
+
name: f.filename || '',
|
|
1709
|
+
role: f.file_role || null,
|
|
1710
|
+
openai_file_id: f.openai_file_id
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
});
|
|
1715
|
+
if (ids.length) {
|
|
1716
|
+
convo.attached_openai_file_ids = ids;
|
|
1717
|
+
convo.attached_files_meta = meta;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}catch(_e){ /* non-fatal */ }
|
|
1722
|
+
|
|
1601
1723
|
convo.messages = normalizeMessages(convo.messages);
|
|
1724
|
+
|
|
1725
|
+
// On the very first turn of an object-scoped widget conversation,
|
|
1726
|
+
// pre-load a slimmed understandObject snapshot so the assistant
|
|
1727
|
+
// immediately knows which record "this client/task/..." refers to
|
|
1728
|
+
// without having to remember to call MCP. We keep this snapshot
|
|
1729
|
+
// concise via slimUnderstandObjectResult and only inject it once.
|
|
1730
|
+
try{
|
|
1731
|
+
var isObjectChat = (convo.source === 'object_chat') && convo.scope_id;
|
|
1732
|
+
var hasMessages = Array.isArray(convo.messages) && convo.messages.length > 0;
|
|
1733
|
+
if (isObjectChat && !hasMessages){
|
|
1734
|
+
const uo = await callMCPTool('understandObject', {
|
|
1735
|
+
_id: convo.scope_id,
|
|
1736
|
+
itemtype: convo.scope_itemtype || undefined,
|
|
1737
|
+
depth: 1,
|
|
1738
|
+
slim: true
|
|
1739
|
+
}, { req });
|
|
1740
|
+
const slimmed = slimUnderstandObjectResult(uo);
|
|
1741
|
+
if (slimmed) {
|
|
1742
|
+
convo.messages = convo.messages || [];
|
|
1743
|
+
convo.messages.push({
|
|
1744
|
+
role: 'system',
|
|
1745
|
+
content: JSON.stringify({
|
|
1746
|
+
tool: 'understandObject',
|
|
1747
|
+
scope_object: slimmed
|
|
1748
|
+
})
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}catch(_e){
|
|
1753
|
+
console.warn('[chatgpt] widgetMessage understandObject preload failed', _e && _e.message || _e);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1602
1756
|
const nowIso = new Date().toISOString();
|
|
1603
1757
|
|
|
1604
1758
|
// Append user message
|
|
@@ -1630,37 +1784,163 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1630
1784
|
}
|
|
1631
1785
|
}
|
|
1632
1786
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1787
|
+
// Resolve the JOE ai_assistant driving this conversation. We support
|
|
1788
|
+
// both the modern flow (ai_assistant_id / convo.assistant, which are
|
|
1789
|
+
// JOE cuid references) and the legacy OpenAI Assistants flow
|
|
1790
|
+
// (assistant_id / convo.assistant_id, which are OpenAI ids).
|
|
1791
|
+
var assistantObj = null;
|
|
1792
|
+
var joeAssistantId = body.ai_assistant_id || convo.assistant || null; // JOE cuid
|
|
1793
|
+
if (joeAssistantId) {
|
|
1794
|
+
try{
|
|
1795
|
+
// Prefer a direct lookup via the ai_assistant schema, but fall
|
|
1796
|
+
// back to scanning the in-memory dataset if needed. In some
|
|
1797
|
+
// server contexts $J.get may not be wired for ai_assistant yet,
|
|
1798
|
+
// while JOE.Data.ai_assistant is available.
|
|
1799
|
+
assistantObj = $J.get(joeAssistantId,'ai_assistant') || $J.get(joeAssistantId) || null;
|
|
1800
|
+
}catch(_e){}
|
|
1801
|
+
if (!assistantObj && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
|
|
1802
|
+
assistantObj = JOE.Data.ai_assistant.find(function(a){
|
|
1803
|
+
return a && (a._id === joeAssistantId);
|
|
1642
1804
|
}) || null;
|
|
1643
1805
|
}
|
|
1806
|
+
}
|
|
1807
|
+
const assistantId = body.assistant_id || convo.assistant_id || (assistantObj && assistantObj.assistant_id) || null;
|
|
1808
|
+
// Legacy fallback: if we only have an OpenAI assistant_id, try to
|
|
1809
|
+
// locate the JOE ai_assistant by that id.
|
|
1810
|
+
if (!assistantObj && assistantId && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
|
|
1811
|
+
assistantObj = JOE.Data.ai_assistant.find(function (a) {
|
|
1812
|
+
return a && a.assistant_id === assistantId;
|
|
1813
|
+
}) || null;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Log which ai_assistant (if any) is being used for this widget
|
|
1817
|
+
// conversation so we can easily confirm the active agent when
|
|
1818
|
+
// debugging object chat vs AI Hub behavior.
|
|
1819
|
+
try{
|
|
1820
|
+
coloredLog('[widget] assistant resolution: '
|
|
1821
|
+
+ 'convo.assistant=' + String(convo.assistant || '')
|
|
1822
|
+
+ ' convo.assistant_id=' + String(convo.assistant_id || '')
|
|
1823
|
+
+ ' body.ai_assistant_id=' + String(body.ai_assistant_id || '')
|
|
1824
|
+
+ ' body.assistant_id=' + String(body.assistant_id || '')
|
|
1825
|
+
+ ' resolvedJoe=' + String(assistantObj && assistantObj._id || ''));
|
|
1826
|
+
if (assistantObj) {
|
|
1827
|
+
var asstLabel = assistantObj.name || assistantObj.title || assistantObj.info || assistantObj._id || assistantId;
|
|
1828
|
+
coloredLog('[widget] widgetMessage using ai_assistant: '
|
|
1829
|
+
+ asstLabel + ' [' + (assistantObj._id || assistantId || '') + ']'
|
|
1830
|
+
+ ' source=' + String(convo.source || 'widget')
|
|
1831
|
+
+ ' convo=' + String(convo._id || ''));
|
|
1832
|
+
} else if (assistantId) {
|
|
1833
|
+
coloredLog('[widget] widgetMessage assistant_id (no JOE ai_assistant found): '
|
|
1834
|
+
+ assistantId
|
|
1835
|
+
+ ' source=' + String(convo.source || 'widget')
|
|
1836
|
+
+ ' convo=' + String(convo._id || ''));
|
|
1837
|
+
}
|
|
1838
|
+
}catch(_e){}
|
|
1839
|
+
|
|
1644
1840
|
const openai = newClient();
|
|
1645
1841
|
const model = (assistantObj && assistantObj.ai_model) || convo.model || body.model || "gpt-5.1";
|
|
1646
1842
|
|
|
1647
1843
|
// Prefer explicit system text on the conversation, then assistant instructions.
|
|
1648
|
-
const
|
|
1844
|
+
const baseSystemText = (convo.system && String(convo.system)) ||
|
|
1649
1845
|
(assistantObj && assistantObj.instructions) ||
|
|
1650
1846
|
"";
|
|
1847
|
+
|
|
1848
|
+
// When this conversation was launched from an object ("Start Chat"
|
|
1849
|
+
// on a record), include a small scope hint so the assistant knows
|
|
1850
|
+
// which object id/itemtype to use with MCP tools like
|
|
1851
|
+
// understandObject/search. We keep this concise to avoid
|
|
1852
|
+
// unnecessary tokens but still make the scope unambiguous.
|
|
1853
|
+
let systemText = baseSystemText;
|
|
1854
|
+
try{
|
|
1855
|
+
if (convo.source === 'object_chat' && convo.scope_id) {
|
|
1856
|
+
const scopeLine = '\n\n---\nJOE scope_object:\n'
|
|
1857
|
+
+ '- itemtype: ' + String(convo.scope_itemtype || 'unknown') + '\n'
|
|
1858
|
+
+ '- _id: ' + String(convo.scope_id) + '\n'
|
|
1859
|
+
+ 'When you need this object\'s details, call the MCP tool "understandObject" '
|
|
1860
|
+
+ 'with these identifiers, or search for related records using the MCP search tools.\n';
|
|
1861
|
+
systemText = (baseSystemText || '') + scopeLine;
|
|
1862
|
+
}
|
|
1863
|
+
}catch(_e){ /* non-fatal */ }
|
|
1864
|
+
|
|
1865
|
+
// Append MCP tool instructions for assistants that have MCP enabled,
|
|
1866
|
+
// using the same helper as other MCP-aware surfaces.
|
|
1867
|
+
try{
|
|
1868
|
+
if (assistantObj && assistantObj.mcp_enabled) {
|
|
1869
|
+
const mcpCfg = {
|
|
1870
|
+
mcp_enabled: assistantObj.mcp_enabled,
|
|
1871
|
+
mcp_toolset: assistantObj.mcp_toolset,
|
|
1872
|
+
mcp_selected_tools: assistantObj.mcp_selected_tools,
|
|
1873
|
+
mcp_instructions_mode: assistantObj.mcp_instructions_mode || 'auto'
|
|
1874
|
+
};
|
|
1875
|
+
const mcp = buildMcpToolsFromConfig(mcpCfg);
|
|
1876
|
+
coloredLog('[widget] MCP config for assistant '
|
|
1877
|
+
+ String(assistantObj._id || '') + ': enabled=' + String(mcpCfg.mcp_enabled)
|
|
1878
|
+
+ ' toolset=' + String(mcpCfg.mcp_toolset || '')
|
|
1879
|
+
+ ' names=' + JSON.stringify(mcp.names || []));
|
|
1880
|
+
if (mcp.names && mcp.names.length) {
|
|
1881
|
+
const txt = MCP.buildToolInstructions(mcp.names, mcpCfg.mcp_instructions_mode || 'auto');
|
|
1882
|
+
if (txt) {
|
|
1883
|
+
coloredLog('[widget] appending MCP instructions block to systemText');
|
|
1884
|
+
systemText = (systemText || '') + '\n\n' + txt;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
} else {
|
|
1888
|
+
coloredLog('[widget] MCP disabled for this assistant or assistant missing; no MCP block appended.');
|
|
1889
|
+
}
|
|
1890
|
+
}catch(_e){ /* non-fatal */ }
|
|
1891
|
+
|
|
1892
|
+
// Build the messages array for the model. We deliberately separate
|
|
1893
|
+
// the stored `convo.messages` from the model-facing payload so we
|
|
1894
|
+
// can annotate the latest user turn with uploaded_files metadata
|
|
1895
|
+
// without altering the persisted history.
|
|
1651
1896
|
const messagesForModel = convo.messages.map(function (m) {
|
|
1652
1897
|
return { role: m.role, content: m.content };
|
|
1653
1898
|
});
|
|
1899
|
+
// If we have attached file metadata, wrap the latest user turn in a
|
|
1900
|
+
// small JSON envelope so the model can see which files exist and how
|
|
1901
|
+
// they are labeled (role, name, origin field) while still receiving
|
|
1902
|
+
// the raw user input as `input`.
|
|
1903
|
+
try{
|
|
1904
|
+
if (convo.attached_files_meta && convo.attached_files_meta.length && messagesForModel.length) {
|
|
1905
|
+
var lastMsg = messagesForModel[messagesForModel.length - 1];
|
|
1906
|
+
if (lastMsg && lastMsg.role === role && typeof lastMsg.content === 'string') {
|
|
1907
|
+
lastMsg.content = JSON.stringify({
|
|
1908
|
+
uploaded_files: convo.attached_files_meta,
|
|
1909
|
+
input: lastMsg.content
|
|
1910
|
+
}, null, 2);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}catch(_e){ /* non-fatal */ }
|
|
1914
|
+
|
|
1915
|
+
// Collect OpenAI file ids from scoped object attachments and any
|
|
1916
|
+
// assistant-level files so they are available to the model via the
|
|
1917
|
+
// shared attachFilesToResponsesPayload helper inside runWithTools.
|
|
1918
|
+
var openaiFileIds = [];
|
|
1919
|
+
if (Array.isArray(convo.attached_openai_file_ids) && convo.attached_openai_file_ids.length){
|
|
1920
|
+
openaiFileIds = openaiFileIds.concat(convo.attached_openai_file_ids);
|
|
1921
|
+
}
|
|
1922
|
+
try{
|
|
1923
|
+
if (assistantObj && Array.isArray(assistantObj.assistant_files)) {
|
|
1924
|
+
assistantObj.assistant_files.forEach(function(f){
|
|
1925
|
+
if (f && f.openai_file_id) {
|
|
1926
|
+
openaiFileIds.push(f.openai_file_id);
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
}catch(_e){}
|
|
1654
1931
|
|
|
1655
1932
|
// Use runWithTools so that, when an assistant has tools configured,
|
|
1656
|
-
// we let the model call those tools via MCP
|
|
1657
|
-
// final response.
|
|
1933
|
+
// we let the model call those tools via MCP / function tools before
|
|
1934
|
+
// generating a final response. Attach any discovered OpenAI files
|
|
1935
|
+
// so the model can read from them as needed.
|
|
1658
1936
|
const runResult = await runWithTools({
|
|
1659
1937
|
openai: openai,
|
|
1660
1938
|
model: model,
|
|
1661
1939
|
systemText: systemText,
|
|
1662
1940
|
messages: messagesForModel,
|
|
1663
1941
|
assistant: assistantObj,
|
|
1942
|
+
attachments_mode: (body.attachments_mode || 'direct'),
|
|
1943
|
+
openai_file_ids: openaiFileIds.length ? openaiFileIds : null,
|
|
1664
1944
|
req: req
|
|
1665
1945
|
});
|
|
1666
1946
|
|
|
@@ -335,6 +335,104 @@ function FormBuilder(){
|
|
|
335
335
|
}
|
|
336
336
|
}//end bodycontent
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Get form definition JSON from an include
|
|
340
|
+
* GET /API/plugin/formBuilder/definition?include_id={id}
|
|
341
|
+
* GET /API/plugin/formBuilder/definition?formid={form_id}&field=json_definition_include
|
|
342
|
+
* GET /API/plugin/formBuilder/definition?formid={form_id}&pageid={page_id} (auto-finds JSON from page includes)
|
|
343
|
+
*/
|
|
344
|
+
this.definition = function(data, req, res) {
|
|
345
|
+
var includeId = data.include_id;
|
|
346
|
+
var formId = data.formid;
|
|
347
|
+
var pageId = data.pageid;
|
|
348
|
+
var includeField = data.field || 'json_definition_include';
|
|
349
|
+
|
|
350
|
+
// If formid provided, look up the include from form's meta, page includes, or specified field
|
|
351
|
+
if (formId && !includeId) {
|
|
352
|
+
var form = JOE.Cache.findByID('form', formId);
|
|
353
|
+
if (!form) {
|
|
354
|
+
return res.status(404).json({errors: 'form not found', failedat: 'formBuilder'});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// First try: Check if form has a reference to include in meta or a direct field
|
|
358
|
+
if (form.meta && form.meta[includeField]) {
|
|
359
|
+
includeId = form.meta[includeField];
|
|
360
|
+
} else if (form[includeField]) {
|
|
361
|
+
includeId = form[includeField];
|
|
362
|
+
}
|
|
363
|
+
// Second try: If pageid provided, find JSON include from page's includes
|
|
364
|
+
else if (pageId) {
|
|
365
|
+
var page = JOE.Cache.findByID('page', pageId);
|
|
366
|
+
if (page && page.includes && Array.isArray(page.includes)) {
|
|
367
|
+
for (var i = 0; i < page.includes.length; i++) {
|
|
368
|
+
var inc = JOE.Cache.findByID('include', page.includes[i]);
|
|
369
|
+
if (inc && inc.filetype === 'json') {
|
|
370
|
+
includeId = inc._id;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!includeId) {
|
|
378
|
+
return res.status(404).json({errors: 'form does not have json definition include reference. Set it in form.meta.json_definition_include or add a JSON include to the page', failedat: 'formBuilder'});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!includeId) {
|
|
383
|
+
return res.status(400).json({errors: 'include_id or formid required', failedat: 'formBuilder'});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
var include = JOE.Cache.findByID('include', includeId);
|
|
387
|
+
if (!include) {
|
|
388
|
+
return res.status(404).json({errors: 'include not found', failedat: 'formBuilder'});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (include.filetype !== 'json') {
|
|
392
|
+
return res.status(400).json({errors: 'include is not a JSON file', failedat: 'formBuilder'});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
var content = include.content;
|
|
396
|
+
|
|
397
|
+
// If fillTemplate is enabled, process template variables
|
|
398
|
+
if (include.fillTemplate) {
|
|
399
|
+
var payload = {
|
|
400
|
+
WEBCONFIG: JOE.webconfig,
|
|
401
|
+
SETTING: JOE.Cache.settings,
|
|
402
|
+
REQUEST: req,
|
|
403
|
+
FORM: formId ? JOE.Cache.findByID('form', formId) : null
|
|
404
|
+
};
|
|
405
|
+
try {
|
|
406
|
+
var fillTemplate = require('../modules/Sites.js').fillTemplate || function(str, data) {
|
|
407
|
+
return str.replace(/\$\{this\.([^}]+)\}/g, function(match, path) {
|
|
408
|
+
var parts = path.split('.');
|
|
409
|
+
var val = data;
|
|
410
|
+
for (var i = 0; i < parts.length; i++) {
|
|
411
|
+
if (val && typeof val === 'object') {
|
|
412
|
+
val = val[parts[i]];
|
|
413
|
+
} else {
|
|
414
|
+
return match;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return val != null ? val : match;
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
content = fillTemplate(content, payload);
|
|
421
|
+
} catch(e) {
|
|
422
|
+
console.error('[formBuilder.definition] template fill error:', e);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Parse and return JSON
|
|
427
|
+
try {
|
|
428
|
+
var jsonData = typeof content === 'string' ? JSON.parse(content) : content;
|
|
429
|
+
res.set('Content-Type', 'application/json');
|
|
430
|
+
return res.json(jsonData);
|
|
431
|
+
} catch(e) {
|
|
432
|
+
return res.status(500).json({errors: 'invalid JSON: ' + e.message, failedat: 'formBuilder'});
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
338
436
|
return self;
|
|
339
437
|
}
|
|
340
438
|
module.exports = new FormBuilder();
|