UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

416 lines (404 loc) 20.5 kB
var schema = { title : 'Ai Response | ${name}', display:'AI Response', info:"An ai response from openai etc", summary:{ description:'Stored AI response objects generated from ai_prompt and the chatgpt plugin.', purpose:'Use ai_response to persist responses, token usage, and referenced objects from OpenAI calls for auditing, reuse, and comparison back to JOE objects.', labelField:'name', defaultSort:{ field:'created', dir:'desc' }, searchableFields:['name','prompt_method','model_used','response_type','response_id','tags','_id'], allowedSorts:['created','joeUpdated','name','model_used'], relationships:{ outbound:[ { field:'ai_prompt', targetSchema:'ai_prompt', cardinality:'one' }, { field:'referenced_objects', targetSchema:'<schemaName>', cardinality:'many' }, { field:'tags', targetSchema:'tag', 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_response' }, { name:'name', type:'string', required:true }, { name:'ai_prompt', type:'string', isReference:true, targetSchema:'ai_prompt' }, { name:'referenced_objects', type:'string', isArray:true }, { name:'generated_thoughts', type:'string', isArray:true, isReference:true, targetSchema:'thought' }, { name:'user_prompt', type:'string' }, { name:'prompt_method', type:'string' }, { name:'model_used', type:'string' }, { name:'response_type', type:'string' }, { name:'response', type:'string' }, { name:'response_raw', type:'string' }, { name:'response_json', type:'object' }, { name:'response_keys', type:'string', isArray:true }, { name:'response_id', type:'string' }, { name:'usage', type:'object' }, { name:'used_openai_file_ids', type:'string', isArray:true }, // MCP configuration and audit fields { name:'mcp_enabled', type:'boolean' }, { name:'mcp_toolset', type:'string' }, { name:'mcp_selected_tools', type:'string', isArray:true }, { name:'mcp_instructions_mode', type:'string' }, { name:'mcp_tools_used', type:'object', isArray:true }, // { name:'creator_type', type:'string', enumValues:['user','ai_assistant'] }, // { name:'creator_id', type:'string' }, { name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' }, { name:'joeUpdated', type:'string', format:'date-time' }, { name:'created', type:'string', format:'date-time' } ] }, menuicon:` <svg xmlns="http://www.w3.org/2000/svg" viewBox="-150 -150 1324 1324"> <path d="M314.57 597.81h376.06v28.2H314.57zm0 75.21h376.06v28.2H314.57zm0 75.21h376.06v28.2H314.57zm239.99-498.47 151.31 31.3c12.23 2.53 12.23 20 0 22.53l-151.31 31.3-31.3 151.31c-2.53 12.23-20 12.23-22.53 0l-31.3-151.31-151.31-31.3c-12.23-2.53-12.23-20 0-22.53l151.31-31.3 31.3-151.31c2.53-12.23 20-12.23 22.53 0l31.3 151.31ZM388.55 102.88l65.55 13.56c5.3 1.1 5.3 8.66 0 9.76l-65.55 13.56-13.56 65.55c-1.1 5.3-8.66 5.3-9.76 0l-13.56-65.55-65.55-13.56c-5.3-1.1-5.3-8.66 0-9.76l65.55-13.56 13.56-65.55c1.1-5.3 8.66-5.3 9.76 0l13.56 65.55Zm20.9 365.17 63.07 13.04c5.1 1.05 5.1 8.33 0 9.39l-63.07 13.04-13.04 63.07c-1.05 5.1-8.33 5.1-9.39 0l-13.04-63.07-63.07-13.04c-5.1-1.05-5.1-8.33 0-9.39l63.07-13.04 13.04-63.07c1.05-5.1 8.33-5.1 9.39 0l13.04 63.07Z"/> <path d="M689 155.94v37.61h67.44v676.9H267.56v-676.9H319v-37.61h-89.04v752.12h564.08V155.94H689z"/> </svg>`, listView:{ title: function(air){ var html = `<joe-subtext class="fright">${_joe.Utils.prettyPrintDTS(air.created)}</joe-subtext> <joe-title>${air.name}</joe-title> <joe-subtitle>${(air.response_keys?.length && `returns <b>${air.response_keys}</b>`) ||''}</joe-subtitle> ${air.prompt_method?`<joe-subtext class="fright">method ${air.prompt_method}</joe-subtext>`:''} <div>${(air.referenced_objects||[]).map(function(ref){ var obj = $J.get(ref); if(obj){ return `<joe-subtext>${obj.itemtype}:<b>${obj.name}</b> - ${obj._id}</joe-subtext>`; }else{ return `<joe-subtext>${ref}</joe-subtext>`; } }).join('')}</div>`; air.tags?.length && (html+=_joe.schemas.tag.methods.renderTags(air.tags,'fright')); return html; }, listWindowTitle: 'Ai Responses' }, subsets: function(a,b,c){ var subsets = []; // Base subsets: tag-based and by ai_prompt var subsets = subsets.concat( _joe.getDataset('ai_prompt').map(function(prompt){ var color = prompt.status && $J.get(prompt.status,'status').color; return { name: prompt.name, id: prompt._id, filter: { ai_prompt: prompt._id }, stripecolor: color || null // optional }; }) ); // Additional subset: "Generated Thoughts" – detect from raw JSON // We look inside response_json for proposed_thoughts / thoughts arrays. try{ var ds = _joe.getDataset('ai_response') || []; var ids = []; ds.map(function(r){ var j = r && r.response_json; if (!j) { return; } if (Array.isArray(j.proposed_thoughts) && j.proposed_thoughts.length) { ids.push(r._id); return; } if (Array.isArray(j.thoughts) && j.thoughts.length) { ids.push(r._id); } }); if (ids.length) { subsets.push({ name: 'Generated Thoughts', id: 'generated_thoughts', filter: { _id: { $in: ids } }, stripecolor: _joe.Colors.ai }); } }catch(_e){ } //add status subsets subsets = subsets.concat(_joe.Filter.Options.status({group:'status'})); return subsets; }, stripeColor:function(air){ if(air.response_json && air.response_json.proposed_thoughts){ return {color:_joe.Colors.ai,title:'Proposed '+air.response_json.proposed_thoughts.length+' Thoughts'}; } return null; }, bgColor:function(air){//status color if(air.status){ var status = _joe.getDataItem(air.status,'status'); if(status && status.color){ return {color:status.color,title:status.name}; } } return null; }, filters:function(){ var filters = []; // Tag filters filters = filters.concat( _joe.Filter.Options.tags({group:'tags',untagged:true,collapsed:true}) ); // creator_type simple value grouping // try{ // filters = filters.concat( // _joe.Filter.Options.getDatasetPropertyValues('ai_response','creator_type',{ group: 'creator_type', collapsed: true }) // ); // }catch(_e){} // // creator_id filters with human-readable names // try{ // var dataset = _joe.getDataset('ai_response') || []; // var byCreator = {}; // dataset.map(function(r){ // if(!r.creator_id){ return; } // var key = r.creator_type+':'+r.creator_id; // if(!byCreator[key]){ // var name = r.creator_id; // if(r.creator_type === 'user' && _joe.Data.user){ // var u = _joe.Data.user.find(function(x){ return x && x._id === r.creator_id; }); // if(u){ name = u.fullname || u.name || u.email || u._id; } // }else if(r.creator_type === 'ai_assistant' && _joe.Data.ai_assistant){ // var a = _joe.Data.ai_assistant.find(function(x){ return x && x._id === r.creator_id; }); // if(a){ name = a.name || a._id; } // } // byCreator[key] = { // name: name, // filter: { creator_type: r.creator_type, creator_id: r.creator_id } // }; // } // }); // var keys = Object.keys(byCreator); // if(keys.length){ // filters.push({ group_start:'creator', collapsed:true }); // keys.sort(function(a,b){ // var na = byCreator[a].name.toLowerCase(); // var nb = byCreator[b].name.toLowerCase(); // if(na>nb) return 1; // if(na<nb) return -1; // return 0; // }).map(function(k){ // filters.push({ // name: byCreator[k].name, // filter: byCreator[k].filter // }); // }); // filters.push({ group_end:'creator' }); // } // }catch(_e){} return filters; }, itemExpander:function(air){ if(air.response_json && air.response_json.proposed_thoughts){ //list proposed thoughts in html with header "Proposed Thoughts" var html = `<joe-card><joe-title>Proposed Thoughts</joe-title>`; air.response_json.proposed_thoughts.map(thought=>{ html += `<joe-subtitle>${thought.statement}</joe-subtitle><br/>`; }); html += `</joe-card>`; return html; } return _joe.schemas.thought.methods.listThoughts(air); }, sorter:['!created','name'], checkChanges:false, fields:[ 'name', //'info', {name:'ai_prompt',type:'objectReference', values:'ai_prompt',display:'Ai Prompt',width:'50%',locked:true}, {name:'referenced_objects',type:'objectReference',width:'50%',values:function(air,orList){ return $J.search({_id:{$in:air.referenced_objects}}); },display:'Referenced Objects',locked:true}, {name:'generated_thoughts',type:'objectReference',width:'50%',values:function(air,orList){ return $J.search({source_ai_response:air._id,itemtype:'thought'}); },display:'Generated Thoughts',locked:true}, {name:'user_prompt', display:'Sent User Prompt',comment:"The input sent by the user after all processing",locked:true,type:'rendering'}, {name:'prompt_method',locked:true,width:'50%'}, {name:'model_used',locked:true,width:'50%'}, // {name:'ai_prompt',type:'select', values:'ai_prompt',display:'Ai Prompt',width:'50%',template:function(resp,prompt){ // return prompt.name; // }}, //{name:'business',type:'objectReference', values:'business',display:'Business',locked:true}, //{name:'params', type:'rendering', locked:true}, {section_start:'response',collapsed:false}, { name:'response_type', type:'text', display:'Response Type', placeholder:'(a code for the responsetype)', locked:true, width:'50%', condition:function(v){ return v.length > 0; } }, {name:'response', type:'rendering', locked:true, height:'500px'}, { name:'response_json', type:'content', display:'Parsed Response (JSON)', locked:true, run:function(air){ if (!air || !air.response_json) { return ''; } try{ var json = JSON.stringify(air.response_json, null, 2); // basic HTML escape for safety json = String(json) .replace(/&/g,'&amp;') .replace(/</g,'&lt;') .replace(/>/g,'&gt;'); return '<pre style="white-space:pre-wrap; max-height:260px; overflow:auto;">'+json+'</pre>'; }catch(e){ return '<pre>'+String(air.response_json)+'</pre>'; } } }, {name:'response_keys', type:'text', locked:true,display:'Response Keys'}, {name:'response_id', type:'text', display:'openAI response ID',locked:true}, // {name:'used_openai_file_ids', type:'content', display:'OpenAI File IDs (Used)', locked:true, run:function(air){ // var ids = air && air.used_openai_file_ids; // if(!ids || !ids.length){ return '<joe-subtext>None</joe-subtext>'; } // return ids.map(function(id){ return '<joe-subtext>'+id+'</joe-subtext>'; }).join(''); // }}, {name:'used_openai_file_ids', type:'text', display:'OpenAI File IDs (Used)', locked:true}, {section_end:'response'}, {sidebar_start:'right', collapsed:false}, // {section_start:'creator'}, // 'creator_type','creator_id', // {section_end:'creator'}, {section_start:'workflow'}, 'status', 'tags', {section_end:'workflow'}, {section_start:'tokens'}, {name:'usage',type:'content',display:'Tokens Used',run:function(air){ if(!air.usage || !air.usage.total_tokens){return '';} var html=`<joe-text>input <b>${air.usage.input_tokens}</b></joe-text> <joe-text>output <b>${air.usage.output_tokens}</b></joe-text> <joe-text>total <b>${air.usage.total_tokens}</b></joe-text>` return html; }}, {section_end:'tokens'}, {section_start:'mcp',collapsed:true}, {name:'mcp_enabled',type:'boolean',display:'MCP Enabled',locked:true}, {name:'mcp_toolset',type:'text',display:'MCP Toolset',locked:true}, {name:'mcp_instructions_mode',type:'text',display:'MCP Instructions Mode',locked:true}, { name:'mcp_selected_tools', type:'content', display:'MCP Tools (configured)', run:function(air){ var arr = air && air.mcp_selected_tools; if(!arr || !arr.length){ return '<joe-subtext>None</joe-subtext>'; } return arr.map(function(n){ return '<joe-tag>'+n+'</joe-tag>'; }).join(' '); } }, { name:'mcp_tools_used', type:'content', display:'MCP Tools Used', run:function(air){ var calls = air && air.mcp_tools_used; if(!calls || !calls.length){ return '<joe-subtext>None</joe-subtext>'; } return calls.map(function(c){ try{ var name = c && c.name ? c.name : '(unknown)'; var args = c && c.arguments ? JSON.stringify(c.arguments) : ''; return '<div><strong>'+name+'</strong>' + (args ? (': <code>'+String(args).replace(/</g,"&lt;").replace(/>/g,"&gt;")+'</code>') : '') + '</div>'; }catch(e){ return '<div>'+JSON.stringify(c)+'</div>'; } }).join(''); } }, {section_end:'mcp'}, {sidebar_end:'right'}, //{name:'payload', type:'code', locked:true,height:'500px'}, //{name:'raw_response', type:'code', language:'json',locked:true}, {section_start:'system',collapsed:true}, '_id','created','itemtype', {section_end:'system'}, ], methods:{ compareResponseToObject:function(response_id,object_id,do_alert){ let response =$J.get(response_id); var object; let object_keys; if(_joe.current.object._id == object_id){ object = _jco(true); object_keys = _joe.current.fields.map(f=>f.name) } else{ object = $J.get(object_id); object_keys = Object.keys(object); } if(!response || !object){return false;} //evaluate the response to see if it is valid json try{ let parsed = JSON.parse(response.response); //find the keys in the response that are in the object let keys = Object.keys(parsed); let match = keys.filter(k=>object_keys.includes(k)); //if there are no keys that match, return false if(!match.length){ do_alert && alert('Response does not match any object keys: '+keys.join(',')); return false;} //return an array of matching keys do_alert && ( doit = confirm('Replace object values with responses for: '+match.join(','))); if(doit){ _joe.schemas.ai_response.methods.updateObjectFromResponse(response_id,object_id,match); } return match; }catch(e){ //if the response is not valid json, return false return false; } }, updateObjectFromResponse:function(response_id,object_id,fields){ let response =$J.get(response_id); let parsed = JSON.parse(response.response); var object; if(_joe.current.object._id == object_id){ object = _jco(true); } else{ object = $J.get(object_id); } //handle tags if(response.tags && response.tags.length){ let newTags = (object.tags||[]).concat(response.tags); //tags array to a set, then back to array newTags = [...new Set(newTags)]; _joe.Fields.set('tags',newTags); } fields.map(function(field){ let newVal = object[field]; //update the object with the response if(object[field].isArray()){ //append the response to the array newVal = object[field].concat(parsed[field]); }else if(typeof object[field] == 'string'){ //merge the response with the object newVal = parsed[field] } _joe.Fields.set(field,newVal); }); }, listResponses:function(obj){ let html =''; let responses = _joe.getDataset('ai_response').filter(r=>{ return r.referenced_objects && r.referenced_objects.includes(obj._id); }) $c.sortBy(responses,'!created'); responses.map(resp=>{ let temp = `<joe-subtitle>${resp.name}</joe-subtitle> <joe-subtext>${_joe.Utils.prettyPrintDTS(resp.created)}</joe-subtext> ${resp.tags && _joe.schemas.tag.methods.renderTags(resp.tags) || ''}`; resp.response_keys && (temp += `<joe-button-wrapper class="bottom"><joe-button class="joe-button joe-blue-button" onclick="_joe.Utils.stopPropagation(event);_joe.schemas.ai_response.methods.compareResponseToObject('${resp._id}','${obj._id}',true);">< merge ${resp.response_keys.length} fields</joe-button><joe-button-wrapper>`); html+=_joe.renderFieldListItem(resp,temp,'ai_response',{icon:'ai_assistant',link:function(item){ return location.href.replace(location.hash,`#/${item.itemtype}/${item._id}`); }}); }) return html; } }, idprop : "_id" }; module.exports = schema;