UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

403 lines (373 loc) 19.9 kB
// Joe AI Assistant Schema // This schema is used to manage AI Assistants within Joe, including syncing with OpenAI. var schema = { title: "Ai Assistant | ${name}", display: "Ai Assist", info: "An AI Assistant configuration linked to OpenAI, managed within Joe.", // Curated summary for agents and tools summary:{ description:'Configuration record for an AI assistant connected to OpenAI.', purpose:'Use ai_assistant to define the model, instructions, tools (MCP-backed), color, and OpenAI assistant_id for chat experiences in JOE (in-app chatbox and embeddable widget). One ai_assistant typically maps to a single OpenAI assistant and can be marked as the default via the DEFAULT_AI_ASSISTANT setting.', labelField:'name', defaultSort:{ field:'joeUpdated', dir:'desc' }, searchableFields:['name','info','assistant_id','ai_model','tags','datasets'], allowedSorts:['joeUpdated','created','name','last_synced'], relationships:{ outbound:[ { field:'status', targetSchema:'status', cardinality:'one' }, { field:'datasets', targetSchema:'<schemaName>', cardinality:'many' }, { field:'files', targetSchema:'file', cardinality:'many' } ], inbound:{ graphRef:'server/relationships.graph.json' } }, joeManagedFields:['created','joeUpdated'], fields:[ { name:'_id', type:'string', required:true }, { name:'itemtype', type:'string', required:true, const:'ai_assistant' }, { name:'name', type:'string', required:true }, { name:'info', type:'string' }, { name:'ai_model', type:'string' }, { name:'assistant_id', type:'string' }, { name:'openai_assistant_version', type:'string' }, { name:'file_search_enabled', type:'boolean' }, { name:'code_interpreter_enabled', type:'boolean' }, { name:'assistant_thinking_text', type:'string' }, { name:'assistant_color', type:'string' }, { name:'instructions', type:'string' }, { name:'tools', type:'string' }, { name:'files', type:'string', isArray:true, isReference:true, targetSchema:'file' }, { name:'file_ids', type:'string', isArray:true }, { name:'datasets', type:'string', isArray:true }, { name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' }, { name:'status', type:'string', isReference:true, targetSchema:'status' }, { name:'last_synced', type:'string', format:'date-time' }, { name:'joeUpdated', type:'string', format:'date-time' }, { name:'created', type:'string', format:'date-time' } ] }, menuicon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"> <path d="M294.49 618.18v64.49c0 22.42.95 35.46 2.84 39.12 2.4 4.94 6.93 8.52 12.29 9.71 9.18 2.13 18.6 3.01 28.02 2.64h17.31c18.81 0 28.22 6.04 28.22 18.11 0 13.96-9.93 22.51-29.8 25.64-24.41 2.74-48.98 3.87-73.55 3.37-29.87 0-51.01-5.79-63.44-17.38-12.42-11.59-18.66-31.41-18.7-59.47v-81.08c.85-11.33-1.15-22.68-5.81-33.04-5.69-7.66-14.07-12.88-23.46-14.6-11.09-2.78-18.54-13.19-17.58-24.58-1.07-11.43 6.42-21.9 17.58-24.58 9.41-1.95 17.77-7.32 23.46-15.07 4.7-10.35 6.7-21.71 5.81-33.04v-80.88c0-27.62 6.21-47.22 18.63-58.81 12.42-11.59 33.57-17.36 63.44-17.31 26.13-.73 52.27.66 78.17 4.16 6.92 1.05 13.37 4.16 18.5 8.92 4.27 4.08 6.66 9.75 6.61 15.66 0 12.07-9.41 18.11-28.22 18.11h-17.84c-16.04 0-27.05 1.72-33.04 5.15-5.92 3.96-9.36 10.72-9.05 17.84v92.71c-.35 31.23-8.72 52.03-25.11 62.38-1.29.88-2.06 2.34-2.05 3.9 0 2.42 2.93 6.06 8.79 10.9 6.54 5.96 11.3 13.63 13.74 22.14 3.19 11.34 4.62 23.11 4.23 34.89Zm435.01-134.4v-64.23c0-22.42-.95-35.46-2.84-39.12-2.4-4.94-6.93-8.52-12.29-9.71-9.18-2.13-18.6-3.01-28.02-2.64h-17.18c-18.81 0-28.22-6.04-28.22-18.11 0-13.96 9.93-22.42 29.8-25.37 24.42-2.73 48.98-3.86 73.55-3.37 29.87 0 51.01 5.79 63.44 17.38 12.42 11.59 18.63 31.19 18.63 58.81v81.28c-.88 11.33 1.12 22.69 5.81 33.04 5.75 7.62 14.1 12.87 23.46 14.74 11.16 2.71 18.62 13.22 17.51 24.65.74 9.18-4.03 17.93-12.16 22.27-4.09 1.86-8.33 3.34-12.69 4.43-8.51 2.69-15.35 9.08-18.63 17.38-2.71 9.15-3.85 18.69-3.37 28.22v81.01c0 27.8-6.21 47.47-18.63 59.01-12.42 11.54-33.57 17.33-63.44 17.38-26.13.74-52.27-.65-78.17-4.16-6.92-1.05-13.37-4.16-18.5-8.92-4.31-4.17-6.7-9.93-6.61-15.93 0-12.07 9.41-18.11 28.22-18.11 2.25 0 8.19.18 17.84.53 11.25.56 22.5-1.19 33.04-5.15 4.1-2.12 7.02-5.98 7.93-10.51 1.33-11.91 1.82-23.9 1.45-35.88v-64.49c-.51-12.88 1.38-25.74 5.55-37.93 4.19-9.77 11.07-18.15 19.82-24.18 1.29-.88 2.06-2.34 2.05-3.9 0-2.42-2.93-6.06-8.79-10.9-6.55-5.96-11.31-13.63-13.74-22.14-3.43-11.46-5.05-23.39-4.82-35.35Z"/> <rect width="74.5" height="189" x="409.75" y="456.75" rx="33.94" ry="33.94"/> <rect width="74.5" height="189" x="539.75" y="456.75" rx="33.94" ry="33.94"/> </svg>`, listView:{ title: '<joe-title>${name}</joe-title><joe-subtitle>${info}</joe-subtitle><joe-subtitle>${assistant_id}</joe-subtitle>', listWindowTitle: 'Ai Assistants' }, subsets:function(){ var schemas = []; var subs = []; _joe.current.list.map(function(asst){ schemas = schemas.concat(asst.datasets||[]); }); (new Set(schemas)).map(function(schema){ subs.push({name:schema,filter:{datasets:{$in:[schema]}}}) }); return subs; }, stripeColor: function(ai_assistant) { //use the stripe color from ai_assistant status object if it has one if (ai_assistant.status){ var status = _joe.Cache.get(ai_assistant.status); if (status && status.color) { return {color:status.color,title:status.name}; } } }, bgColor: function(ai_assistant) { //if it's the default assistant, use goldenrod color let def_id = _joe.Data.setting.where({name:'DEFAULT_AI_ASSISTANT'})[0]||false; if (def_id && def_id.value == ai_assistant._id) { return {color:'#DAA520'}; } }, methods: { syncAssistantToOpenAI: function(currentObject, event) { var $button = $(event.target).closest('joe-button'); var originalHTML = $button.html(); if (!currentObject || !currentObject._id) { alert("No valid record to sync."); return; } const url = `/API/plugin/chatgpt-assistants/syncAssistantToOpenAI?_id=${currentObject._id}`; // Disable button and show spinner $button.prop('disabled', true); $button.html(`<svg class="joe-loading-spinner" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"/></svg> Syncing...`); fetch(url) .then(response => response.json()) .then(response => { if (response && response.success) { console.log("Assistant synced successfully:", response); // 🔄 Refresh the object after sync _joe.Object.getFromServer(currentObject._id, { goto: true }, function(success) { if (success) { console.log("Assistant reloaded into Joe UI."); //_joe.updateObject(null,null,true); } else { console.warn("Failed to reload Assistant object."); } $button.prop('disabled', false); $button.html(originalHTML); }); } else { alert("Sync failed. See console for details."); console.error(response); $button.prop('disabled', false); $button.html(originalHTML); } }) .catch(err => { alert("Error syncing Assistant. See console."); console.error(err); $button.prop('disabled', false); $button.html(originalHTML); }); }, loadMCPTolsIntoAssistant: async function(currentObject, event){ try{ var $button = $(event.target).closest('joe-button'); var originalHTML = $button.html(); $button.prop('disabled', true); $button.html('Loading MCP tools...'); var base = location.origin.replace(/\/$/,''); var manifestUrl = base + '/.well-known/mcp/manifest.json'; const manifest = await fetch(manifestUrl).then(r=>{ if(!r.ok){ throw new Error('HTTP '+r.status); } return r.json(); }); var tools = (manifest.tools||[]).map(function(t){ return { type:'function', function:{ name: t.name, description: t.description || '', parameters: t.params || { type:'object' } } }; }); var json = JSON.stringify(tools, null, 2); // Update current object and visible tools field via core helper currentObject.tools = json; if (window._joe && _joe.Fields && typeof _joe.Fields.set === 'function') { _joe.Fields.set('tools', json); } else { // Fallback to direct DOM update if helper is unavailable var $field = $('.joe-object-field[data-name="tools"]').find('.joe-field').eq(0); if($field && $field.length){ $field.val(json); $field.trigger('input').trigger('change'); } } $button.prop('disabled', false); $button.html(originalHTML); (_joe.toast && _joe.toast('Loaded '+tools.length+' MCP tools into assistant.')) || alert('Loaded '+tools.length+' MCP tools.'); }catch(e){ console.error('loadMCPTolsIntoAssistant error', e); alert('Failed to load MCP tools: '+(e.message||e)); try{ var $button = $(event.target).closest('joe-button'); $button.prop('disabled', false); }catch(_e){} } }, setAsDefaultAssistant: async function(currentObject, event){ try{ if(!currentObject || !currentObject._id){ alert('No assistant loaded'); return; } var $button = $(event.target).closest('joe-button'); var originalHTML = $button.html(); $button.prop('disabled', true); $button.html('Setting default...'); // Find existing DEFAULT_AI_ASSISTANT setting (if any) var settings = (_joe && _joe.Data && _joe.Data.setting) || []; var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null; // Build querystring for /API/save var params = new URLSearchParams(); params.set('itemtype','setting'); params.set('name','DEFAULT_AI_ASSISTANT'); params.set('setting_type', (existing && existing.setting_type) || 'text'); params.set('info', (existing && existing.info) || 'Default ai_assistant used for chats and widgets.'); params.set('value', currentObject._id); var url; if(existing && existing._id){ url = '/API/save/' + encodeURIComponent(existing._id) + '?' + params.toString(); }else{ url = '/API/save/?' + params.toString(); } const resp = await fetch(url, { method: 'GET', credentials: 'include' }).then(r => r.json()); if(!resp || resp.status !== 'success'){ const msg = (resp && resp.error) || 'Save failed'; throw new Error(msg); } var saved = resp.results || resp.object || null; if(saved){ if(existing){ Object.assign(existing, saved); }else{ _joe.Data.setting = _joe.Data.setting || []; _joe.Data.setting.push(saved); } } $button.prop('disabled', false); $button.html(originalHTML); (_joe.toast && _joe.toast('Default assistant set.')) || alert('Default assistant set.'); }catch(e){ console.error('setAsDefaultAssistant error', e); alert('Failed to set default assistant: '+(e.message||e)); try{ var $button = $(event.target).closest('joe-button'); $button.prop('disabled', false); }catch(_e){} } } }, fields: function() { return [ "name:text:40%", // Assistant Name "info:text:60%", // Assistant Subtitle (optional) { section_start: "openai_config", display: "OpenAI" }, 'ai_model', { name: "assistant_id", type: "text", display: "OpenAI Assistant ID", locked: true, comment: function(ai_assistant) { if (!ai_assistant.assistant_id) { return 'No Assistant created yet. Save to generate a Playground link.'; } return `<a href="https://platform.openai.com/playground/assistants?assistant=${ai_assistant.assistant_id}" target="_blank" style="color: #3498db;">View this Assistant in OpenAI Playground</a>`; } }, { name: "openai_assistant_version", type: "text", default: "v1", hidden: true }, { name: "file_search_enabled", type: "boolean", display: "Enable File Search", default: false, width:"50%" }, { name: "code_interpreter_enabled", type: "boolean", display: "Enable Code Interpreter", default: false, width:"50%" }, { section_end: "openai_config" }, { section_start: "instructions", display: "instructions" }, { name: "assistant_thinking_text", type: "text", display: "Assistant Thinking Text", info: "What the user should see while the Assistant is thinking.", placeholder: "e.g. 'Assistant is preparing a response...'" }, { name: "assistant_color", type: "color", display: "Assistant Color", default: "teal" }, { name: "instructions", type: "wysiwyg" }, // Full TinyMCE editor { section_end: "instructions" }, { section_start: "tools", display: "Function Tools", collapsed:function(asst) { return !asst.tools; } }, { name: "tools", type: "code", display: "Tool Definitions (JSON)", comment: '<div>Paste or edit full JSON function definitions here:</div><div><a href="https://platform.openai.com/docs/assistants/tools/function-calling" target="_blank" style="color: #3498db;">View OpenAI Function Calling Documentation</a></div><pre>{<br/>&nbsp;&nbsp;"type": "function",<br/>&nbsp;&nbsp;"function": {<br/>&nbsp;&nbsp;&nbsp;&nbsp;"name": "your_function",<br/>&nbsp;&nbsp;&nbsp;&nbsp;"description": "Describe what your function does.",<br/>&nbsp;&nbsp;&nbsp;&nbsp;"parameters": {<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"type": "object",<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"properties": {<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"theme": {<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"type": "string",<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"description": "The theme to suggest (e.g., hope, perseverance)."<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br/>&nbsp;&nbsp;&nbsp;&nbsp;}<br/>&nbsp;&nbsp;}<br/>}</pre><br/>' , height: "300px" }, { name: "load_mcp_tools", type: "button", display: "Load MCP Tools", method: "loadMCPTolsIntoAssistant", color: "blue", title: "Fetch MCP tools from this JOE instance and populate the tools JSON array." }, { section_end: "tools" }, { section_start: "files" }, { name: "files", type: "group", display: "Attached Files", values: function() { return _joe.getDataset("file"); }, template: "${name}", idprop: "_id" }, { name: "file_ids", type: "content", display: "File IDs (auto populated on sync)", comment: "Auto-managed list of OpenAI file_ids attached to this Assistant." }, { section_end: "files" }, {section_start:'categorization',collapsed:true}, 'datasets', {section_end:'categorization'}, { section_start: "system", collapsed: true }, "_id", "created", "itemtype", { section_end: "system" }, { sidebar_start: "right", collapsed: false }, "tags", "status", { name: "last_synced", type: "date", display: "Last Synced", locked:true, comment: "Last time this Assistant was updated at OpenAI." }, { name: "sync_button", type: "button", display: "Sync to OpenAI", method: "syncAssistantToOpenAI", color: "green", title: "Sync this Assistant with OpenAI" }, { name: "set_default_assistant", type: "button", display: "Set as Default Assistant", method: "setAsDefaultAssistant", color: "orange", hidden:function(ai_assistant){ try{ var settings = (_joe && _joe.Data && _joe.Data.setting) || []; var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null; return !!(existing && existing.value === ai_assistant._id); }catch(e){ return false; } }, title: "Mark this assistant as the DEFAULT_AI_ASSISTANT setting.", locked:function(ai_assistant){ try{ var settings = (_joe && _joe.Data && _joe.Data.setting) || []; var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null; return !!(existing && existing.value === ai_assistant._id); }catch(e){ return false; } } }, // { // name: "test_button", // type: "button", // display: "Test Button", // method: "testButtonClick", // <- the method name you want to call // color: "orange", // <- Joe color class (joe-orange-button) // title: "Click to test button functionality", // schema: "ai_assistant" // <- schema to pull the method from (optional for now, we lock it anyway) // }, { sidebar_end: "right" }, ]; }, idprop: "_id" }; module.exports = schema;