|
| 1 | +'use server'; |
| 2 | + |
| 3 | +import envConfig from '@/src/constants/envConfig'; |
| 4 | + |
| 5 | +interface ChatMessage { |
| 6 | + role: 'user' | 'assistant'; |
| 7 | + content: string; |
| 8 | +} |
| 9 | + |
| 10 | +interface ChatWithMemoryRequest { |
| 11 | + messages: ChatMessage[]; |
| 12 | + transcript: string; |
| 13 | +} |
| 14 | + |
| 15 | +export interface ChatWithMemoryResponse { |
| 16 | + message: string; |
| 17 | +} |
| 18 | + |
| 19 | +const OPENAI_API_KEY = envConfig.OPENAI_API_KEY; |
| 20 | + |
| 21 | +if (!OPENAI_API_KEY) { |
| 22 | + throw new Error('OPENAI_API_KEY is not configured. Please set it in your environment variables.'); |
| 23 | +} |
| 24 | + |
| 25 | +// Rough token estimation: ~4 characters per token |
| 26 | +function estimateTokens(text: string): number { |
| 27 | + return Math.ceil(text.length / 4); |
| 28 | +} |
| 29 | + |
| 30 | +// Truncate transcript to fit within token budget |
| 31 | +function truncateTranscript(transcript: string, maxTokens: number): string { |
| 32 | + const estimatedTokens = estimateTokens(transcript); |
| 33 | + if (estimatedTokens <= maxTokens) { |
| 34 | + return transcript; |
| 35 | + } |
| 36 | + |
| 37 | + // If transcript is too long, take the beginning and end |
| 38 | + const targetLength = maxTokens * 4; // Convert tokens back to characters |
| 39 | + const startLength = Math.floor(targetLength * 0.6); // 60% from start |
| 40 | + const endLength = Math.floor(targetLength * 0.4); // 40% from end |
| 41 | + |
| 42 | + const start = transcript.substring(0, startLength); |
| 43 | + const end = transcript.substring(transcript.length - endLength); |
| 44 | + |
| 45 | + return `${start}\n\n[... transcript truncated ...]\n\n${end}`; |
| 46 | +} |
| 47 | + |
| 48 | +export default async function chatWithMemory( |
| 49 | + data: ChatWithMemoryRequest, |
| 50 | +): Promise<ChatWithMemoryResponse | null> { |
| 51 | + try { |
| 52 | + // Use gpt-4.1 which has 128k context window, or fallback to gpt-3.5-turbo-16k |
| 53 | + const model = 'gpt-4.1'; |
| 54 | + |
| 55 | + // Estimate tokens for conversation messages (reserve ~2000 tokens for system message and response) |
| 56 | + const conversationTokens = data.messages.reduce( |
| 57 | + (sum, msg) => sum + estimateTokens(msg.content), |
| 58 | + 0 |
| 59 | + ); |
| 60 | + |
| 61 | + // Reserve tokens: 2000 for system message overhead, 2000 for response, 2000 for conversation |
| 62 | + const maxTranscriptTokens = 120000 - conversationTokens - 2000 - 2000; |
| 63 | + |
| 64 | + // Truncate transcript if needed |
| 65 | + const processedTranscript = truncateTranscript(data.transcript, maxTranscriptTokens); |
| 66 | + |
| 67 | + // Create system message with transcript context |
| 68 | + const systemMessage = { |
| 69 | + role: 'system' as const, |
| 70 | + content: `You are a helpful chatbot assistant. You have access to the following conversation transcript. Use this context to answer questions accurately and helpfully. |
| 71 | +
|
| 72 | +Important: As a chatbot, provide short and concise answers. Be direct and to the point while still being helpful. |
| 73 | +
|
| 74 | +Critical: Always try to reference things from the conversation transcript, even when the user asks questions that seem unrelated to the conversation. Find connections, examples, or relevant details from the transcript that relate to their question, and incorporate those references into your response. |
| 75 | +
|
| 76 | +Transcript: |
| 77 | +${processedTranscript} |
| 78 | +
|
| 79 | +Please answer questions based on the transcript above. Even if a question seems unrelated, always try to find and reference relevant information from the conversation.`, |
| 80 | + }; |
| 81 | + |
| 82 | + // Keep only recent conversation messages to avoid token limit issues |
| 83 | + // Keep last 10 messages (5 exchanges) to maintain context |
| 84 | + const recentMessages = data.messages.slice(-10); |
| 85 | + const messages = [systemMessage, ...recentMessages]; |
| 86 | + |
| 87 | + const response = await fetch('https://api.openai.com/v1/chat/completions', { |
| 88 | + method: 'POST', |
| 89 | + headers: { |
| 90 | + 'Content-Type': 'application/json', |
| 91 | + Authorization: `Bearer ${OPENAI_API_KEY}`, |
| 92 | + }, |
| 93 | + body: JSON.stringify({ |
| 94 | + model: model, |
| 95 | + messages: messages, |
| 96 | + temperature: 0.7, |
| 97 | + }), |
| 98 | + }); |
| 99 | + |
| 100 | + if (!response.ok) { |
| 101 | + const errorData = await response.json().catch(() => ({})); |
| 102 | + console.error('OpenAI API error:', response.status, errorData); |
| 103 | + |
| 104 | + // If context length error, try with gpt-3.5-turbo-16k as fallback |
| 105 | + if (errorData.error?.code === 'context_length_exceeded') { |
| 106 | + const fallbackModel = 'gpt-3.5-turbo-16k'; |
| 107 | + const fallbackMaxTokens = 14000 - conversationTokens - 2000 - 2000; |
| 108 | + const fallbackTranscript = truncateTranscript(data.transcript, fallbackMaxTokens); |
| 109 | + |
| 110 | + const fallbackSystemMessage = { |
| 111 | + role: 'system' as const, |
| 112 | + content: `You are a helpful chatbot assistant. You have access to the following conversation transcript. Use this context to answer questions accurately and helpfully. |
| 113 | +
|
| 114 | +Important: As a chatbot, provide short and concise answers. Be direct and to the point while still being helpful. |
| 115 | +
|
| 116 | +Critical: Always try to reference things from the conversation transcript, even when the user asks questions that seem unrelated to the conversation. Find connections, examples, or relevant details from the transcript that relate to their question, and incorporate those references into your response. |
| 117 | +
|
| 118 | +Try to say things like "like mentioned by x" |
| 119 | +
|
| 120 | +Transcript: |
| 121 | +${fallbackTranscript} |
| 122 | +
|
| 123 | +Please answer questions based on the transcript above. Even if a question seems unrelated, always try to find and reference relevant information from the conversation.`, |
| 124 | + }; |
| 125 | + |
| 126 | + const fallbackResponse = await fetch('https://api.openai.com/v1/chat/completions', { |
| 127 | + method: 'POST', |
| 128 | + headers: { |
| 129 | + 'Content-Type': 'application/json', |
| 130 | + Authorization: `Bearer ${OPENAI_API_KEY}`, |
| 131 | + }, |
| 132 | + body: JSON.stringify({ |
| 133 | + model: fallbackModel, |
| 134 | + messages: [fallbackSystemMessage, ...recentMessages], |
| 135 | + temperature: 0.7, |
| 136 | + }), |
| 137 | + }); |
| 138 | + |
| 139 | + if (!fallbackResponse.ok) { |
| 140 | + const fallbackErrorData = await fallbackResponse.json().catch(() => ({})); |
| 141 | + console.error('OpenAI API fallback error:', fallbackResponse.status, fallbackErrorData); |
| 142 | + return null; |
| 143 | + } |
| 144 | + |
| 145 | + const fallbackResult = await fallbackResponse.json(); |
| 146 | + const assistantMessage = fallbackResult.choices[0]?.message?.content || 'Sorry, I could not generate a response.'; |
| 147 | + return { message: assistantMessage }; |
| 148 | + } |
| 149 | + |
| 150 | + return null; |
| 151 | + } |
| 152 | + |
| 153 | + const result = await response.json(); |
| 154 | + const assistantMessage = result.choices[0]?.message?.content || 'Sorry, I could not generate a response.'; |
| 155 | + |
| 156 | + return { |
| 157 | + message: assistantMessage, |
| 158 | + }; |
| 159 | + } catch (error) { |
| 160 | + console.error('Error chatting with memory:', error); |
| 161 | + return null; |
| 162 | + } |
| 163 | +} |
| 164 | + |
0 commit comments