json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
416 lines (404 loc) • 20.5 kB
JavaScript
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,'&')
.replace(/</g,'<')
.replace(/>/g,'>');
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,"<").replace(/>/g,">")+'</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;