1// This file contains functions to interact with OpenAI's Assistants API v2 2 3// LOCAL DEVELOPMENT: For local testing with php -S localhost:8000 4// const LOCAL_PHP_PROXY = 'http://localhost:8000/proxy.php?endpoint='; 5 6// WORDPRESS: Use this path for WordPress 7const WP_PHP_PROXY = '/wp-content/plugins/ican-assistant/proxy.php?endpoint='; 8 9const CONFIG = { 10 OPENAI_API_KEY: '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', 11 ASSISTANT_ID: 'asst_HcIO8BjlSawUUv0wYyBWOAMI', 12 API_BASE_URL: 'https://api.openai.com/v1' 13}; 14 15class OpenAIService { 16 constructor() { 17 this.threadId = localStorage.getItem('threadId') || null; 18 19 // Use the WordPress PHP proxy 20 this.apiBaseUrl = WP_PHP_PROXY; 21 } 22 23 // Updated createThread method for Assistants API v2 24 async createThread() { 25 try { 26 console.log('Creating thread via PHP proxy...'); 27 const response = await fetch(`${this.apiBaseUrl}/threads`, { 28 method: 'POST', 29 headers: { 30 'Content-Type': 'application/json' 31 // No Auth header needed - handled by the PHP proxy 32 }, 33 body: JSON.stringify({}) 34 }); 35 36 if (!response.ok) { 37 const errorData = await response.json().catch(() => ({})); 38 console.error('Thread creation error details:', errorData); 39 if (errorData.error && errorData.error.message) { 40 console.error('Error message:', errorData.error.message); 41 } 42 throw new Error(`Failed to create thread: ${response.status}`); 43 } 44 45 const data = await response.json(); 46 this.threadId = data.id; 47 localStorage.setItem('threadId', this.threadId); 48 console.log('Thread created:', this.threadId); 49 return this.threadId; 50 } catch (error) { 51 console.error('Error creating thread:', error); 52 throw error; 53 } 54 } 55 56 async sendMessage(message) { 57 try { 58 // Ensure we have a thread 59 if (!this.threadId) { 60 await this.createThread(); 61 } 62 63 // Check if we need to summarize and create a new thread 64 // (perform this before sending the new message) 65 await this.summarizeAndManageThread(); 66 67 console.log('Sending message to thread:', this.threadId); 68 69 // Rest of your existing sendMessage method... 70 71 // Add the message to the thread through PHP proxy 72 const messageResponse = await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/messages`, { 73 method: 'POST', 74 headers: { 75 'Content-Type': 'application/json' 76 // No Auth header needed 77 }, 78 body: JSON.stringify({ 79 role: 'user', 80 content: message 81 }) 82 }); 83 84 if (!messageResponse.ok) { 85 const errorData = await messageResponse.json().catch(() => ({})); 86 console.error('Message error details:', errorData); 87 throw new Error(`Failed to add message: ${messageResponse.status}`); 88 } 89 90 const messageData = await messageResponse.json(); 91 console.log('Message added:', messageData.id); 92 93 // Run the assistant on the thread through PHP proxy 94 const runResponse = await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/runs`, { 95 method: 'POST', 96 headers: { 97 'Content-Type': 'application/json' 98 // No Auth header needed 99 }, 100 body: JSON.stringify({ 101 assistant_id: CONFIG.ASSISTANT_ID 102 }) 103 }); 104 105 if (!runResponse.ok) { 106 const errorData = await runResponse.json().catch(() => ({})); 107 console.error('Run error details:', errorData); 108 throw new Error(`Failed to run assistant: ${runResponse.status}`); 109 } 110
111 const runData = await runResponse.json(); 112 const runId = runData.id; 113 console.log('Run created:', runId); 114 115 // Poll for the run completion 116 return await this.pollRunStatus(runId); 117 118 } catch (error) { 119 console.error('Error sending message:', error); 120 throw error; 121 } 122 } 123 124 async pollRunStatus(runId) { 125 try { 126 let runStatus = 'queued'; 127 console.log('Starting to poll run status for:', runId); 128 129 while (runStatus !== 'completed' && runStatus !== 'failed' && runStatus !== 'expired') { 130 // Wait for 1 second before checking status 131 await new Promise(resolve => setTimeout(resolve, 1000)); 132 133 // Get status via PHP proxy 134 const response = await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/runs/${runId}`, { 135 headers: { 136 'Content-Type': 'application/json' 137 // No Auth header needed 138 } 139 }); 140 141 if (!response.ok) { 142 const errorData = await response.json().catch(() => ({})); 143 console.error('Poll status error details:', errorData); 144 throw new Error(`Failed to get run status: ${response.status}`); 145 } 146 147 const data = await response.json(); 148 runStatus = data.status; 149 console.log('Run status:', runStatus); 150 151 if (runStatus === 'requires_action') { 152 console.log('Run requires action', data); 153 // Handle function calling if needed 154 } 155 } 156 157 if (runStatus === 'completed') { 158 return await this.getMessages(); 159 } else { 160 throw new Error(`Run ended with status: ${runStatus}`); 161 } 162 } catch (error) { 163 console.error('Error polling run status:', error); 164 throw error; 165 } 166 } 167 168 // Update the getMessages method to preserve formatting 169 170 async getMessages() { 171 try { 172 console.log('Getting messages from thread:', this.threadId); 173 174 // Get messages via PHP proxy 175 const response = await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/messages?limit=1`, { 176 headers: { 177 'Content-Type': 'application/json' 178 // No Auth header needed 179 } 180 }); 181 182 if (!response.ok) { 183 const errorData = await response.json().catch(() => ({})); 184 console.error('Get messages error details:', errorData); 185 throw new Error(`Failed to get messages: ${response.status}`); 186 } 187 188 const data = await response.json(); 189 console.log('Got messages response:', data); 190 191 const messages = data.data; 192 193 // Return the most recent assistant message 194 const assistantMessages = messages.filter(msg => msg.role === 'assistant'); 195 if (assistantMessages.length > 0) { 196 const messageContent = assistantMessages[0].content[0].text.value; 197 console.log('Assistant response:', messageContent); 198 return messageContent; 199 } else { 200 return "I'm sorry, I couldn't generate a response."; 201 } 202 } catch (error) { 203 console.error('Error getting messages:', error); 204 throw error; 205 } 206 } 207 208 async summarizeAndManageThread() { 209 try { 210 if (!this.threadId) return; 211 212 // Get all messages in the thread 213 const response = await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/messages?limit=30&order=asc`, { 214 headers: { 215 'Content-Type': 'application/json' 216 } 217 }); 218 219 if (!response.ok) { 220 throw new Error(`Failed to get messages for summarization: ${response.status}`); 221 } 222 223 const data = await response.json(); 224 const messages = data.data; 225 226 // If we have more than 30 messages, summarize and create a new thread 227 if (messages.length >= 30) { 228 console.log('Thread has reached 30+ messages. Summarizing conversation...'); 229 230 // Ask the assistant to summarize the conversation 231 const summaryMessage = await this.createSummaryRequest(); 232 233 // Create a new thread 234 const oldThreadId = this.threadId; 235 await this.createThread(); 236 237 // Add the summary as the first message in the new thread 238 await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/messages`, { 239 method: 'POST', 240 headers: { 241 'Content-Type': 'application/json' 242 }, 243 body: JSON.stringify({ 244 role: 'user', 245 content: `Previous conversation summary: ${summaryMessage}` 246 }) 247 }); 248 249 console.log('Created new thread with conversation summary'); 250 return true; 251 } 252 253 return false;
254 } catch (error) { 255 console.error('Error managing thread:', error); 256 return false; 257 } 258 } 259 260 async createSummaryRequest() { 261 try { 262 console.log('Requesting conversation summary...'); 263 264 // Add a request for summarization 265 const messageResponse = await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/messages`, { 266 method: 'POST', 267 headers: { 268 'Content-Type': 'application/json' 269 }, 270 body: JSON.stringify({ 271 role: 'user', 272 content: "Please summarize our conversation so far, capturing all important points, advice given, and context needed for continuing our discussion. This summary will be used as context for our continued conversation." 273 }) 274 }); 275 276 if (!messageResponse.ok) { 277 throw new Error(`Failed to add summary request: ${messageResponse.status}`); 278 } 279 280 // Run the assistant to generate the summary 281 const runResponse = await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/runs`, { 282 method: 'POST', 283 headers: { 284 'Content-Type': 'application/json' 285 }, 286 body: JSON.stringify({ 287 assistant_id: CONFIG.ASSISTANT_ID 288 }) 289 }); 290 291 if (!runResponse.ok) { 292 throw new Error(`Failed to run summary generation: ${runResponse.status}`); 293 } 294 295 const runData = await runResponse.json(); 296 297 // Wait for the summary to be generated 298 await this.pollRunStatus(runData.id); 299 300 // Get the summary message 301 const summaryResponse = await fetch(`${this.apiBaseUrl}/threads/${this.threadId}/messages?limit=1`, { 302 headers: { 303 'Content-Type': 'application/json' 304 } 305 }); 306 307 if (!summaryResponse.ok) { 308 throw new Error(`Failed to get summary: ${summaryResponse.status}`); 309 } 310 311 const summaryData = await summaryResponse.json(); 312 const assistantMessages = summaryData.data.filter(msg => msg.role === 'assistant'); 313 314 if (assistantMessages.length > 0) { 315 const summaryText = assistantMessages[0].content[0].text.value; 316 console.log('Summary generated:', summaryText.substring(0, 100) + '...'); 317 return summaryText; 318 } else { 319 return "Summary of previous conversation."; 320 } 321 } catch (error) { 322 console.error('Error creating summary:', error); 323 return "Error summarizing previous conversation."; 324 } 325 } 326 327 async resetConversation() { 328 try { 329 // Clear the thread ID from memory and localStorage 330 this.threadId = null; 331 localStorage.removeItem('threadId'); 332 333 // Create a new thread right away so it's ready for the next message 334 await this.createThread(); 335 336 console.log('Conversation reset, new thread created:', this.threadId); 337 return true; 338 } catch (error) { 339 console.error('Error resetting conversation:', error); 340 return false; 341 } 342 } 343} 344 345const openaiService = new OpenAIService();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.