import { NextApiRequest, NextApiResponse } from 'next'; import { OPENAI_API_HOST } from '@/utils/app/const'; import { cleanSourceText } from '@/utils/server/google'; import { Message } from '@/types/chat'; import { GoogleBody, GoogleSource } from '@/types/google'; import { Readability } from '@mozilla/readability'; import endent from 'endent'; import jsdom, { JSDOM } from 'jsdom'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { const { messages, key, model, googleAPIKey, googleCSEId } = req.body as GoogleBody; const userMessage = messages[messages.length - 1]; const query = encodeURIComponent(userMessage.content.trim()); const googleRes = await fetch( `https://customsearch.googleapis.com/customsearch/v1?key=${ googleAPIKey ? googleAPIKey : process.env.GOOGLE_API_KEY }&cx=${ googleCSEId ? googleCSEId : process.env.GOOGLE_CSE_ID }&q=${query}&num=5`, ); const googleData = await googleRes.json(); const sources: GoogleSource[] = googleData.items.map((item: any) => ({ title: item.title, link: item.link, displayLink: item.displayLink, snippet: item.snippet, image: item.pagemap?.cse_image?.[0]?.src, text: '', })); const sourcesWithText: any = await Promise.all( sources.map(async (source) => { try { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), 5000), ); const res = (await Promise.race([ fetch(source.link), timeoutPromise, ])) as any; // if (res) { const html = await res.text(); const virtualConsole = new jsdom.VirtualConsole(); virtualConsole.on('error', (error) => { if (!error.message.includes('Could not parse CSS stylesheet')) { console.error(error); } }); const dom = new JSDOM(html, { virtualConsole }); const doc = dom.window.document; const parsed = new Readability(doc).parse(); if (parsed) { let sourceText = cleanSourceText(parsed.textContent); return { ...source, // TODO: switch to tokens text: sourceText.slice(0, 2000), } as GoogleSource; } // } return null; } catch (error) { console.error(error); return null; } }), ); const filteredSources: GoogleSource[] = sourcesWithText.filter(Boolean); const answerPrompt = endent` Provide me with the information I requested. Use the sources to provide an accurate response. Respond in markdown format. Cite the sources you used as a markdown link as you use them at the end of each sentence by number of the source (ex: [[1]](link.com)). Provide an accurate response and then stop. Today's date is ${new Date().toLocaleDateString()}. Example Input: What's the weather in San Francisco today? Example Sources: [Weather in San Francisco](https://www.google.com/search?q=weather+san+francisco) Example Response: It's 70 degrees and sunny in San Francisco today. [[1]](https://www.google.com/search?q=weather+san+francisco) Input: ${userMessage.content.trim()} Sources: ${filteredSources.map((source) => { return endent` ${source.title} (${source.link}): ${source.text} `; })} Response: `; const answerMessage: Message = { role: 'user', content: answerPrompt }; const answerRes = await fetch(`${OPENAI_API_HOST}/v1/chat/completions`, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`, ...(process.env.OPENAI_ORGANIZATION && { 'OpenAI-Organization': process.env.OPENAI_ORGANIZATION, }), }, method: 'POST', body: JSON.stringify({ model: model.id, messages: [ { role: 'system', content: `Use the sources to provide an accurate response. Respond in markdown format. Cite the sources you used as [1](link), etc, as you use them. Maximum 4 sentences.`, }, answerMessage, ], max_tokens: 1000, temperature: 1, stream: false, }), }); const { choices: choices2 } = await answerRes.json(); const answer = choices2[0].message.content; res.status(200).json({ answer }); } catch (error) { console.error(error); res.status(500).json({ error: 'Error'}) } }; export default handler;