json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
360 lines (310 loc) • 12.1 kB
JavaScript
const { run } = require("googleapis/build/src/apis/run");
const OpenAI = require("openai");
function ChatGPTAssistants() {
const self = this;
function coloredLog(message) {
console.log(JOE.Utils.color('[chatgpt-assistants]', 'plugin', false), message);
}
function getAPIKey() {
const setting = JOE.Utils.Settings('OPENAI_API_KEY');
if (!setting) throw new Error("Missing OPENAI_API_KEY setting");
return setting;
}
function newClient() {
return new OpenAI({ apiKey: getAPIKey() });
}
function validateThreadId(thread_id) {
const validPattern = /^[a-zA-Z0-9_-]+$/;
if (!validPattern.test(thread_id)) {
console.error("❌ Invalid characters in thread_id!");
return false;
}
return true;
}
function stripHtml(html) {
if (!html) return "";
return html.replace(/<[^>]*>?/gm, '').trim();
}
this.getRandomTheme = async function(data, req, res) {
try {
const themes = ["hope", "peace", "perseverance", "joy", "strength", "love", "faith", "healing"];
const randomTheme = themes[Math.floor(Math.random() * themes.length)];
coloredLog("🎯 Selected Random Theme: " + randomTheme);
return res.jsonp({
theme: randomTheme
});
} catch (err) {
coloredLog("❌ Error in getRandomTheme: " + err.message);
return res.jsonp({ error: err.message });
}
};
this.testAssistant = async function(data, req, res){
const openai = newClient();
const assistant = await openai.beta.assistants.retrieve("asst_HFzrtyAVyzDDwn1PLOIwU26W");
thread = await openai.beta.threads.create();
// 2. Add a user message
await openai.beta.threads.messages.create({
thread_id: thread.id,
role: 'user',
content: `Please analyze this business: welltyme.com`
});
coloredLog('⚡ Starting Assistant thread');
console.log(assistant);
return res.jsonp({ assistant });
}
this.syncAssistantToOpenAI = async function(data, req, res) {
const openai = newClient();
try {
coloredLog("🔄 Starting syncAssistantToOpenAI");
const assistant = JOE.Data.ai_assistant.find(a => a._id === data._id);
if (!assistant) {
return ({ error: "AI Assistant not found in Joe." });
}
// Check if updating or creating
let existing = null;
if (assistant.assistant_id) {
try {
existing = await openai.beta.assistants.retrieve(assistant.assistant_id);
} catch (e) {
console.warn('Assistant ID exists but not found at OpenAI, will create new.');
existing = null;
}
}
const payload = {
name: assistant.name,
instructions: stripHtml(assistant.instructions || ""),
model: assistant.ai_model || "gpt-4o",
tools: [],
//file_search: { enabled: !!assistant.file_search_enabled },
//code_interpreter: { enabled: !!assistant.code_interpreter_enabled }
};
// 1. Add user-defined function tools
const parsedTools = Array.isArray(assistant.tools) ? assistant.tools : JSON.parse(assistant.tools || "[]");
payload.tools.push(...parsedTools);
// 2. Add built-in tools if toggled ON
if (assistant.file_search_enabled) {
payload.tools.push({ type: "file_search" });
}
if (assistant.code_interpreter_enabled) {
payload.tools.push({ type: "code_interpreter" });
}
// if (existing && existing.file_ids?.length) {
// payload.file_ids = existing.file_ids;
// }
// Only set file_ids if handling files manually
if (assistant.file_ids && assistant.file_ids.length > 0) {
payload.file_ids = assistant.file_ids;
}
coloredLog("📦 Payload for OpenAI sync:");
coloredLog(JSON.stringify(payload, null, 2));
let apiResponse;
if (!assistant.assistant_id) {
coloredLog("🆕 No assistant_id found. Creating new Assistant...");
apiResponse = await openai.beta.assistants.create(payload);
assistant.assistant_id = apiResponse.id;
} else {
coloredLog("♻️ assistant_id found. Updating existing Assistant...");
apiResponse = await openai.beta.assistants.update(assistant.assistant_id, payload);
}
assistant.last_synced = new Date().toISOString();
// 💾 Set status to "assistant_synced" if found
const syncStatus = JOE.Data.status.find(s => s.code === "assistant_synced");
if (syncStatus) {
assistant.status = syncStatus._id;
}
await new Promise((resolve, reject) => {
JOE.Storage.save(assistant, 'ai_assistant', function(err, saved) {
if (err) {
return reject(err);
}
resolve(saved);
});
});
coloredLog("💾 Assistant sync saved back to Joe successfully.");
return ({
success: true,
assistant_id: assistant.assistant_id,
message: assistant.assistant_id ? "Assistant synced successfully." : "Assistant created successfully."
});
} catch (err) {
coloredLog("❌ Error in syncAssistantToOpenAI: " + err.message);
return ({ error: err.message });
}
};
this.getThreadMessages = async function(data, req, res) {
coloredLog("🔄 Starting getThreadMessages");
try {
const thread_id = data.thread_id;
if (!thread_id) {
return ({ error: "Missing thread ID." });
}
if(data.polling){
coloredLog('Polling for thread messages: ' + thread_id);
}
const openai = newClient();
const messages = await openai.beta.threads.messages.list(thread_id);
//return res.jsonp({ success: true, messages: messages.data || [] });
var succResp = { success: true, messages: messages.data || [] }
return(succResp);
} catch (err) {
console.error('❌ getThreadMessages error:', err);
return({ error: "Failed to load thread messages." });
}
};
this.addMessage = async function(data, req, res) {
try {
const { conversation_id, content,object_id} = data;
if (!conversation_id || !content) {
return({ error: "Missing conversation ID or content." });
}
const openai = newClient();
const convo = await new Promise((resolve, reject) => {
JOE.Storage.load('ai_conversation', { _id: conversation_id }, (err, results) => {
if (err || !results || !results.length) {
return reject("Conversation not found.");
}
resolve(results[0]);
});
});
var resave = false;
//add objectID
if(object_id && !convo.context_objects.includes(object_id)){
if(!convo.context_objects) convo.context_objects = [];
convo.context_objects.push(object_id);
resave = true;
}
if (!convo.thread_id) {
const thread = await openai.beta.threads.create();
convo.thread_id = thread.id;
resave = true;
// await new Promise((resolve, reject) => {
// JOE.Storage.save(convo, 'ai_conversation', function(err, saved) {
// if (err) return reject(err);
// resolve(saved);
// });
// });
}
if(resave){
await new Promise((resolve, reject) => {
JOE.Storage.save(convo, 'ai_conversation', function(err, saved) {
if (err) return reject(err);
resolve(saved);
});
});
}
await openai.beta.threads.messages.create(convo.thread_id, {
role: "user",
content: content
});
var runObj = null;
// NOW ➔ trigger assistant reply if assistant is selected
if (convo.assistants && convo.assistants.length > 0) {
// const assistant_id = convo.assistants[0]; // Assuming you store OpenAI assistant ID here
//get assistant object by id
const assistant_id = data.assistant_id || convo.assistants?.[0]?.openai_id;
const assistant = JOE.Data.ai_assistant.find(a => a._id === assistant_id);
if (assistant.assistant_id) {
runObj = await openai.beta.threads.runs.create(convo.thread_id, {
assistant_id: assistant.assistant_id
});
console.log(`Assistant run started: ${runObj.id}`);
// You could optionally poll for completion here
}
}else if(convo.assistant){
const assistant = $J.get(convo.assistant);
runObj = await openai.beta.threads.runs.create(convo.thread_id, {
assistant_id: assistant.assistant_id // Assuming you store OpenAI assistant ID here
});
coloredLog(`Assistant run started: ${runObj.id}`);
}
return({ success: true,runObj });
} catch (err) {
console.error('❌ addMessage error:', err);
return({ error: "Failed to send message.", message: err.message });
}
};
this.getRunStatus = async function(data, req, res) {
try {
const run_id = data.run_id;
const thread_id = data.thread_id;
var errors = [];
if (!run_id) errors.push("Missing run ID.");
if(!thread_id) errors.push("Missing thread ID.");
if (errors.length) {
return { error: errors.join(" ") };
}
const openai = newClient();
const run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
coloredLog("🔄 Run status retrieved: " + run.status +'assistant_id: ' + run.assistant_id);
return {
id: run.id,
status: run.status,
assistant_id: run.assistant_id,
completed_at: run.completed_at,
usage: run.usage || {}
};
} catch (err) {
console.error("❌ getRunStatus error:", err);
return { error: "Failed to check run status." };
}
};
function getDefaultAssistant() {
var asst_id = JOE.Utils.Settings('DEFAULT_AI_ASSISTANT');
const defaultAssistant = $J.get(asst_id);
if (!defaultAssistant) {
throw new Error("Default Assistant not found.");
}
return defaultAssistant;
}
this.createConversation = async function(data, req, res) {
//this function creates a new conversation
//it should use the default assistant and add the context of the passed object id after being flattend to the conversation.
//save user_is to the conversation object
try {
const { object_id, user_id } = data;
const openai = newClient();
const user = $J.get(user_id);
const contextObject = $J.get(object_id);
var newConvo = null;
var assistant = getDefaultAssistant();
var assistants = assistant? [assistant._id]:[];
const convo ={
_id:cuid(),
created: new Date().toISOString(),
_joeUpdated: new Date().toISOString(),
itemtype: 'ai_conversation',
name:`${user.name}'s conversation about ${contextObject.name} [${contextObject.itemtype}]`,
user:user_id,
members:[],
assistant:assistant._id,
context_objects:[object_id]
}
if (!convo.thread_id) {
const thread = await openai.beta.threads.create();
convo.thread_id = thread.id;
newConvo = await new Promise((resolve, reject) => {
JOE.Storage.save(convo, 'ai_conversation', function(err, saved) {
if (err) return reject(err);
resolve(saved);
});
});
}
return({ success: true,conversation:newConvo });
} catch (err) {
console.error('❌ conversation creation error:', err);
return({ error: "Failed to create conversation.",message: err.message });
}
};
//self.profileBusinessFromURL = profileBusinessFromURL;
this.async = {
syncAssistantToOpenAI : this.syncAssistantToOpenAI ,
getRunStatus: this.getRunStatus,
getThreadMessages: this.getThreadMessages,
addMessage: this.addMessage,
getRandomTheme: this.getRandomTheme,
testAssistant: this.testAssistant,
createConversation: this.createConversation
};
return self;
}
module.exports = new ChatGPTAssistants();