UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

360 lines (310 loc) 12.1 kB
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();