diff --git a/package.json b/package.json index 9e9137f..5715c2a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@langchain/community": "^0.3.49", "@langchain/core": "^0.3.66", "@langchain/google-genai": "^0.2.15", + "@langchain/groq": "^0.2.3", "@langchain/ollama": "^0.2.3", "@langchain/openai": "^0.6.2", "@langchain/textsplitters": "^0.1.0", diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 2d53b75..ba88da6 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,11 +1,7 @@ -import prompts from '@/lib/prompts'; -import MetaSearchAgent from '@/lib/search/metaSearchAgent'; import crypto from 'crypto'; import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages'; import { EventEmitter } from 'stream'; import { - chatModelProviders, - embeddingModelProviders, getAvailableChatModelProviders, getAvailableEmbeddingModelProviders, } from '@/lib/providers'; @@ -138,6 +134,8 @@ const handleHistorySave = async ( where: eq(chats.id, message.chatId), }); + const fileData = files.map(getFileDetails); + if (!chat) { await db .insert(chats) @@ -146,9 +144,15 @@ const handleHistorySave = async ( title: message.content, createdAt: new Date().toString(), focusMode: focusMode, - files: files.map(getFileDetails), + files: fileData, }) .execute(); + } else if (JSON.stringify(chat.files ?? []) != JSON.stringify(fileData)) { + db.update(chats) + .set({ + files: files.map(getFileDetails), + }) + .where(eq(chats.id, message.chatId)); } const messageExists = await db.query.messages.findFirst({ diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts index 0c11b23..f117cce 100644 --- a/src/app/api/config/route.ts +++ b/src/app/api/config/route.ts @@ -11,6 +11,7 @@ import { getAimlApiKey, getLMStudioApiEndpoint, updateConfig, + getOllamaApiKey, } from '@/lib/config'; import { getAvailableChatModelProviders, @@ -53,6 +54,7 @@ export const GET = async (req: Request) => { config['openaiApiKey'] = getOpenaiApiKey(); config['ollamaApiUrl'] = getOllamaApiEndpoint(); + config['ollamaApiKey'] = getOllamaApiKey(); config['lmStudioApiUrl'] = getLMStudioApiEndpoint(); config['anthropicApiKey'] = getAnthropicApiKey(); config['groqApiKey'] = getGroqApiKey(); @@ -93,6 +95,7 @@ export const POST = async (req: Request) => { }, OLLAMA: { API_URL: config.ollamaApiUrl, + API_KEY: config.ollamaApiKey, }, DEEPSEEK: { API_KEY: config.deepseekApiKey, diff --git a/src/app/api/discover/route.ts b/src/app/api/discover/route.ts index d0e56a6..415aee8 100644 --- a/src/app/api/discover/route.ts +++ b/src/app/api/discover/route.ts @@ -1,55 +1,72 @@ import { searchSearxng } from '@/lib/searxng'; -const articleWebsites = [ - 'yahoo.com', - 'www.exchangewire.com', - 'businessinsider.com', - /* 'wired.com', - 'mashable.com', - 'theverge.com', - 'gizmodo.com', - 'cnet.com', - 'venturebeat.com', */ -]; +const websitesForTopic = { + tech: { + query: ['technology news', 'latest tech', 'AI', 'science and innovation'], + links: ['techcrunch.com', 'wired.com', 'theverge.com'], + }, + finance: { + query: ['finance news', 'economy', 'stock market', 'investing'], + links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com'], + }, + art: { + query: ['art news', 'culture', 'modern art', 'cultural events'], + links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'], + }, + sports: { + query: ['sports news', 'latest sports', 'cricket football tennis'], + links: ['espn.com', 'bbc.com/sport', 'skysports.com'], + }, + entertainment: { + query: ['entertainment news', 'movies', 'TV shows', 'celebrities'], + links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'], + }, +}; -const topics = ['AI', 'tech']; /* TODO: Add UI to customize this */ +type Topic = keyof typeof websitesForTopic; export const GET = async (req: Request) => { try { const params = new URL(req.url).searchParams; + const mode: 'normal' | 'preview' = (params.get('mode') as 'normal' | 'preview') || 'normal'; + const topic: Topic = (params.get('topic') as Topic) || 'tech'; + + const selectedTopic = websitesForTopic[topic]; let data = []; if (mode === 'normal') { + const seenUrls = new Set(); + data = ( - await Promise.all([ - ...new Array(articleWebsites.length * topics.length) - .fill(0) - .map(async (_, i) => { + await Promise.all( + selectedTopic.links.flatMap((link) => + selectedTopic.query.map(async (query) => { return ( - await searchSearxng( - `site:${articleWebsites[i % articleWebsites.length]} ${ - topics[i % topics.length] - }`, - { - engines: ['bing news'], - pageno: 1, - language: 'en', - }, - ) + await searchSearxng(`site:${link} ${query}`, { + engines: ['bing news'], + pageno: 1, + language: 'en', + }) ).results; }), - ]) + ), + ) ) - .map((result) => result) .flat() + .filter((item) => { + const url = item.url?.toLowerCase().trim(); + if (seenUrls.has(url)) return false; + seenUrls.add(url); + return true; + }) .sort(() => Math.random() - 0.5); } else { data = ( await searchSearxng( - `site:${articleWebsites[Math.floor(Math.random() * articleWebsites.length)]} ${topics[Math.floor(Math.random() * topics.length)]}`, + `site:${selectedTopic.links[Math.floor(Math.random() * selectedTopic.links.length)]} ${selectedTopic.query[Math.floor(Math.random() * selectedTopic.query.length)]}`, { engines: ['bing news'], pageno: 1, diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index 16325de..5f752ec 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -81,8 +81,7 @@ export const POST = async (req: Request) => { if (body.chatModel?.provider === 'custom_openai') { llm = new ChatOpenAI({ modelName: body.chatModel?.name || getCustomOpenaiModelName(), - apiKey: - body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(), + apiKey: body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(), temperature: 0.7, configuration: { baseURL: diff --git a/src/app/api/weather/route.ts b/src/app/api/weather/route.ts index 1f9867f..afaf8a6 100644 --- a/src/app/api/weather/route.ts +++ b/src/app/api/weather/route.ts @@ -1,7 +1,10 @@ export const POST = async (req: Request) => { try { - const body: { lat: number; lng: number; temperatureUnit: 'C' | 'F' } = - await req.json(); + const body: { + lat: number; + lng: number; + measureUnit: 'Imperial' | 'Metric'; + } = await req.json(); if (!body.lat || !body.lng) { return Response.json( @@ -13,7 +16,9 @@ export const POST = async (req: Request) => { } const res = await fetch( - `https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}¤t=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto${body.temperatureUnit === 'C' ? '' : '&temperature_unit=fahrenheit'}`, + `https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}¤t=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto${ + body.measureUnit === 'Metric' ? '' : '&temperature_unit=fahrenheit' + }${body.measureUnit === 'Metric' ? '' : '&wind_speed_unit=mph'}`, ); const data = await res.json(); @@ -35,13 +40,15 @@ export const POST = async (req: Request) => { windSpeed: number; icon: string; temperatureUnit: 'C' | 'F'; + windSpeedUnit: 'm/s' | 'mph'; } = { temperature: data.current.temperature_2m, condition: '', humidity: data.current.relative_humidity_2m, windSpeed: data.current.wind_speed_10m, icon: '', - temperatureUnit: body.temperatureUnit, + temperatureUnit: body.measureUnit === 'Metric' ? 'C' : 'F', + windSpeedUnit: body.measureUnit === 'Metric' ? 'm/s' : 'mph', }; const code = data.current.weather_code; diff --git a/src/app/c/[chatId]/page.tsx b/src/app/c/[chatId]/page.tsx index aac125a..672107a 100644 --- a/src/app/c/[chatId]/page.tsx +++ b/src/app/c/[chatId]/page.tsx @@ -1,9 +1,17 @@ -import ChatWindow from '@/components/ChatWindow'; -import React from 'react'; +'use client'; -const Page = ({ params }: { params: Promise<{ chatId: string }> }) => { - const { chatId } = React.use(params); - return ; +import ChatWindow from '@/components/ChatWindow'; +import { useParams } from 'next/navigation'; +import React from 'react'; +import { ChatProvider } from '@/lib/hooks/useChat'; + +const Page = () => { + const { chatId }: { chatId: string } = useParams(); + return ( + + + + ); }; export default Page; diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx index eb7de7f..8e20e50 100644 --- a/src/app/discover/page.tsx +++ b/src/app/discover/page.tsx @@ -4,6 +4,7 @@ import { Search } from 'lucide-react'; import { useEffect, useState } from 'react'; import Link from 'next/link'; import { toast } from 'sonner'; +import { cn } from '@/lib/utils'; interface Discover { title: string; @@ -12,60 +13,66 @@ interface Discover { thumbnail: string; } +const topics: { key: string; display: string }[] = [ + { + display: 'Tech & Science', + key: 'tech', + }, + { + display: 'Finance', + key: 'finance', + }, + { + display: 'Art & Culture', + key: 'art', + }, + { + display: 'Sports', + key: 'sports', + }, + { + display: 'Entertainment', + key: 'entertainment', + }, +]; + const Page = () => { const [discover, setDiscover] = useState(null); const [loading, setLoading] = useState(true); + const [activeTopic, setActiveTopic] = useState(topics[0].key); + + const fetchArticles = async (topic: string) => { + setLoading(true); + try { + const res = await fetch(`/api/discover?topic=${topic}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.message); + } + + data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail); + + setDiscover(data.blogs); + } catch (err: any) { + console.error('Error fetching data:', err.message); + toast.error('Error fetching data'); + } finally { + setLoading(false); + } + }; useEffect(() => { - const fetchData = async () => { - try { - const res = await fetch(`/api/discover`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); + fetchArticles(activeTopic); + }, [activeTopic]); - const data = await res.json(); - - if (!res.ok) { - throw new Error(data.message); - } - - data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail); - - setDiscover(data.blogs); - } catch (err: any) { - console.error('Error fetching data:', err.message); - toast.error('Error fetching data'); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, []); - - return loading ? ( -
- -
- ) : ( + return ( <>
@@ -76,35 +83,73 @@ const Page = () => {
-
- {discover && - discover?.map((item, i) => ( - - {item.title} -
-
- {item.title.slice(0, 100)}... -
-

- {item.content.slice(0, 100)}... -

-
- - ))} +
+ {topics.map((t, i) => ( +
setActiveTopic(t.key)} + > + {t.display} +
+ ))}
+ + {loading ? ( +
+ +
+ ) : ( +
+ {discover && + discover?.map((item, i) => ( + + {item.title} +
+
+ {item.title.slice(0, 100)}... +
+

+ {item.content.slice(0, 100)}... +

+
+ + ))} +
+ )}
); diff --git a/src/app/page.tsx b/src/app/page.tsx index e18aca9..25981b5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,5 @@ import ChatWindow from '@/components/ChatWindow'; +import { ChatProvider } from '@/lib/hooks/useChat'; import { Metadata } from 'next'; import { Suspense } from 'react'; @@ -11,7 +12,9 @@ const Home = () => { return (
- + + +
); diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 045226c..6fb8255 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -21,6 +21,7 @@ interface SettingsType { anthropicApiKey: string; geminiApiKey: string; ollamaApiUrl: string; + ollamaApiKey: string; lmStudioApiUrl: string; deepseekApiKey: string; aimlApiKey: string; @@ -148,7 +149,9 @@ const Page = () => { const [automaticImageSearch, setAutomaticImageSearch] = useState(false); const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); const [systemInstructions, setSystemInstructions] = useState(''); - const [temperatureUnit, setTemperatureUnit] = useState<'C' | 'F'>('C'); + const [measureUnit, setMeasureUnit] = useState<'Imperial' | 'Metric'>( + 'Metric', + ); const [savingStates, setSavingStates] = useState>({}); useEffect(() => { @@ -211,7 +214,9 @@ const Page = () => { setSystemInstructions(localStorage.getItem('systemInstructions')!); - setTemperatureUnit(localStorage.getItem('temperatureUnit')! as 'C' | 'F'); + setMeasureUnit( + localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric', + ); setIsLoading(false); }; @@ -371,8 +376,8 @@ const Page = () => { localStorage.setItem('embeddingModel', value); } else if (key === 'systemInstructions') { localStorage.setItem('systemInstructions', value); - } else if (key === 'temperatureUnit') { - localStorage.setItem('temperatureUnit', value.toString()); + } else if (key === 'measureUnit') { + localStorage.setItem('measureUnit', value.toString()); } } catch (err) { console.error('Failed to save:', err); @@ -430,22 +435,22 @@ const Page = () => {

- Temperature Unit + Measurement Units

{ + setConfig((prev) => ({ + ...prev!, + ollamaApiKey: e.target.value, + })); + }} + onSave={(value) => saveConfig('ollamaApiKey', value)} + /> +
+

GROQ API Key diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 0cf125b..a5d8cf9 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -5,28 +5,11 @@ import MessageInput from './MessageInput'; import { File, Message } from './ChatWindow'; import MessageBox from './MessageBox'; import MessageBoxLoading from './MessageBoxLoading'; +import { useChat } from '@/lib/hooks/useChat'; + +const Chat = () => { + const { messages, loading, messageAppeared } = useChat(); -const Chat = ({ - loading, - messages, - sendMessage, - messageAppeared, - rewrite, - fileIds, - setFileIds, - files, - setFiles, -}: { - messages: Message[]; - sendMessage: (message: string) => void; - loading: boolean; - messageAppeared: boolean; - rewrite: (messageId: string) => void; - fileIds: string[]; - setFileIds: (fileIds: string[]) => void; - files: File[]; - setFiles: (files: File[]) => void; -}) => { const [dividerWidth, setDividerWidth] = useState(0); const dividerRef = useRef(null); const messageEnd = useRef(null); @@ -72,12 +55,8 @@ const Chat = ({ key={i} message={msg} messageIndex={i} - history={messages} - loading={loading} dividerRef={isLast ? dividerRef : undefined} isLast={isLast} - rewrite={rewrite} - sendMessage={sendMessage} /> {!isLast && msg.role === 'assistant' && (

@@ -92,14 +71,7 @@ const Chat = ({ className="bottom-24 lg:bottom-10 fixed z-40" style={{ width: dividerWidth }} > - +
)}
diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 67a5d0c..0d40c83 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -1,17 +1,13 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; import { Document } from '@langchain/core/documents'; import Navbar from './Navbar'; import Chat from './Chat'; import EmptyChat from './EmptyChat'; -import crypto from 'crypto'; -import { toast } from 'sonner'; -import { useSearchParams } from 'next/navigation'; -import { getSuggestions } from '@/lib/actions'; import { Settings } from 'lucide-react'; import Link from 'next/link'; import NextError from 'next/error'; +import { useChat } from '@/lib/hooks/useChat'; export type Message = { messageId: string; @@ -29,539 +25,8 @@ export interface File { fileId: string; } -interface ChatModelProvider { - name: string; - provider: string; -} - -interface EmbeddingModelProvider { - name: string; - provider: string; -} - -const checkConfig = async ( - setChatModelProvider: (provider: ChatModelProvider) => void, - setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void, - setIsConfigReady: (ready: boolean) => void, - setHasError: (hasError: boolean) => void, -) => { - try { - let chatModel = localStorage.getItem('chatModel'); - let chatModelProvider = localStorage.getItem('chatModelProvider'); - let embeddingModel = localStorage.getItem('embeddingModel'); - let embeddingModelProvider = localStorage.getItem('embeddingModelProvider'); - - const autoImageSearch = localStorage.getItem('autoImageSearch'); - const autoVideoSearch = localStorage.getItem('autoVideoSearch'); - - if (!autoImageSearch) { - localStorage.setItem('autoImageSearch', 'true'); - } - - if (!autoVideoSearch) { - localStorage.setItem('autoVideoSearch', 'false'); - } - - const providers = await fetch(`/api/models`, { - headers: { - 'Content-Type': 'application/json', - }, - }).then(async (res) => { - if (!res.ok) - throw new Error( - `Failed to fetch models: ${res.status} ${res.statusText}`, - ); - return res.json(); - }); - - if ( - !chatModel || - !chatModelProvider || - !embeddingModel || - !embeddingModelProvider - ) { - if (!chatModel || !chatModelProvider) { - const chatModelProviders = providers.chatModelProviders; - const chatModelProvidersKeys = Object.keys(chatModelProviders); - - if (!chatModelProviders || chatModelProvidersKeys.length === 0) { - return toast.error('No chat models available'); - } else { - chatModelProvider = - chatModelProvidersKeys.find( - (provider) => - Object.keys(chatModelProviders[provider]).length > 0, - ) || chatModelProvidersKeys[0]; - } - - if ( - chatModelProvider === 'custom_openai' && - Object.keys(chatModelProviders[chatModelProvider]).length === 0 - ) { - toast.error( - "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.", - ); - return setHasError(true); - } - - chatModel = Object.keys(chatModelProviders[chatModelProvider])[0]; - } - - if (!embeddingModel || !embeddingModelProvider) { - const embeddingModelProviders = providers.embeddingModelProviders; - - if ( - !embeddingModelProviders || - Object.keys(embeddingModelProviders).length === 0 - ) - return toast.error('No embedding models available'); - - embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; - embeddingModel = Object.keys( - embeddingModelProviders[embeddingModelProvider], - )[0]; - } - - localStorage.setItem('chatModel', chatModel!); - localStorage.setItem('chatModelProvider', chatModelProvider); - localStorage.setItem('embeddingModel', embeddingModel!); - localStorage.setItem('embeddingModelProvider', embeddingModelProvider); - } else { - const chatModelProviders = providers.chatModelProviders; - const embeddingModelProviders = providers.embeddingModelProviders; - - if ( - Object.keys(chatModelProviders).length > 0 && - (!chatModelProviders[chatModelProvider] || - Object.keys(chatModelProviders[chatModelProvider]).length === 0) - ) { - const chatModelProvidersKeys = Object.keys(chatModelProviders); - chatModelProvider = - chatModelProvidersKeys.find( - (key) => Object.keys(chatModelProviders[key]).length > 0, - ) || chatModelProvidersKeys[0]; - - localStorage.setItem('chatModelProvider', chatModelProvider); - } - - if ( - chatModelProvider && - !chatModelProviders[chatModelProvider][chatModel] - ) { - if ( - chatModelProvider === 'custom_openai' && - Object.keys(chatModelProviders[chatModelProvider]).length === 0 - ) { - toast.error( - "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.", - ); - return setHasError(true); - } - - chatModel = Object.keys( - chatModelProviders[ - Object.keys(chatModelProviders[chatModelProvider]).length > 0 - ? chatModelProvider - : Object.keys(chatModelProviders)[0] - ], - )[0]; - - localStorage.setItem('chatModel', chatModel); - } - - if ( - Object.keys(embeddingModelProviders).length > 0 && - !embeddingModelProviders[embeddingModelProvider] - ) { - embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; - localStorage.setItem('embeddingModelProvider', embeddingModelProvider); - } - - if ( - embeddingModelProvider && - !embeddingModelProviders[embeddingModelProvider][embeddingModel] - ) { - embeddingModel = Object.keys( - embeddingModelProviders[embeddingModelProvider], - )[0]; - localStorage.setItem('embeddingModel', embeddingModel); - } - } - - setChatModelProvider({ - name: chatModel!, - provider: chatModelProvider, - }); - - setEmbeddingModelProvider({ - name: embeddingModel!, - provider: embeddingModelProvider, - }); - - setIsConfigReady(true); - } catch (err) { - console.error('An error occurred while checking the configuration:', err); - setIsConfigReady(false); - setHasError(true); - } -}; - -const loadMessages = async ( - chatId: string, - setMessages: (messages: Message[]) => void, - setIsMessagesLoaded: (loaded: boolean) => void, - setChatHistory: (history: [string, string][]) => void, - setFocusMode: (mode: string) => void, - setNotFound: (notFound: boolean) => void, - setFiles: (files: File[]) => void, - setFileIds: (fileIds: string[]) => void, -) => { - const res = await fetch(`/api/chats/${chatId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (res.status === 404) { - setNotFound(true); - setIsMessagesLoaded(true); - return; - } - - const data = await res.json(); - - const messages = data.messages.map((msg: any) => { - return { - ...msg, - ...JSON.parse(msg.metadata), - }; - }) as Message[]; - - setMessages(messages); - - const history = messages.map((msg) => { - return [msg.role, msg.content]; - }) as [string, string][]; - - console.debug(new Date(), 'app:messages_loaded'); - - document.title = messages[0].content; - - const files = data.chat.files.map((file: any) => { - return { - fileName: file.name, - fileExtension: file.name.split('.').pop(), - fileId: file.fileId, - }; - }); - - setFiles(files); - setFileIds(files.map((file: File) => file.fileId)); - - setChatHistory(history); - setFocusMode(data.chat.focusMode); - setIsMessagesLoaded(true); -}; - -const ChatWindow = ({ id }: { id?: string }) => { - const searchParams = useSearchParams(); - const initialMessage = searchParams.get('q'); - - const [chatId, setChatId] = useState(id); - const [newChatCreated, setNewChatCreated] = useState(false); - - const [chatModelProvider, setChatModelProvider] = useState( - { - name: '', - provider: '', - }, - ); - - const [embeddingModelProvider, setEmbeddingModelProvider] = - useState({ - name: '', - provider: '', - }); - - const [isConfigReady, setIsConfigReady] = useState(false); - const [hasError, setHasError] = useState(false); - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - checkConfig( - setChatModelProvider, - setEmbeddingModelProvider, - setIsConfigReady, - setHasError, - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const [loading, setLoading] = useState(false); - const [messageAppeared, setMessageAppeared] = useState(false); - - const [chatHistory, setChatHistory] = useState<[string, string][]>([]); - const [messages, setMessages] = useState([]); - - const [files, setFiles] = useState([]); - const [fileIds, setFileIds] = useState([]); - - const [focusMode, setFocusMode] = useState('webSearch'); - const [optimizationMode, setOptimizationMode] = useState('speed'); - - const [isMessagesLoaded, setIsMessagesLoaded] = useState(false); - - const [notFound, setNotFound] = useState(false); - - useEffect(() => { - if ( - chatId && - !newChatCreated && - !isMessagesLoaded && - messages.length === 0 - ) { - loadMessages( - chatId, - setMessages, - setIsMessagesLoaded, - setChatHistory, - setFocusMode, - setNotFound, - setFiles, - setFileIds, - ); - } else if (!chatId) { - setNewChatCreated(true); - setIsMessagesLoaded(true); - setChatId(crypto.randomBytes(20).toString('hex')); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const messagesRef = useRef([]); - - useEffect(() => { - messagesRef.current = messages; - }, [messages]); - - useEffect(() => { - if (isMessagesLoaded && isConfigReady) { - setIsReady(true); - console.debug(new Date(), 'app:ready'); - } else { - setIsReady(false); - } - }, [isMessagesLoaded, isConfigReady]); - - const sendMessage = async (message: string, messageId?: string) => { - if (loading) return; - if (!isConfigReady) { - toast.error('Cannot send message before the configuration is ready'); - return; - } - - setLoading(true); - setMessageAppeared(false); - - let sources: Document[] | undefined = undefined; - let recievedMessage = ''; - let added = false; - - messageId = messageId ?? crypto.randomBytes(7).toString('hex'); - - setMessages((prevMessages) => [ - ...prevMessages, - { - content: message, - messageId: messageId, - chatId: chatId!, - role: 'user', - createdAt: new Date(), - }, - ]); - - const messageHandler = async (data: any) => { - if (data.type === 'error') { - toast.error(data.data); - setLoading(false); - return; - } - - if (data.type === 'sources') { - sources = data.data; - if (!added) { - setMessages((prevMessages) => [ - ...prevMessages, - { - content: '', - messageId: data.messageId, - chatId: chatId!, - role: 'assistant', - sources: sources, - createdAt: new Date(), - }, - ]); - added = true; - } - setMessageAppeared(true); - } - - if (data.type === 'message') { - if (!added) { - setMessages((prevMessages) => [ - ...prevMessages, - { - content: data.data, - messageId: data.messageId, - chatId: chatId!, - role: 'assistant', - sources: sources, - createdAt: new Date(), - }, - ]); - added = true; - } - - setMessages((prev) => - prev.map((message) => { - if (message.messageId === data.messageId) { - return { ...message, content: message.content + data.data }; - } - - return message; - }), - ); - - recievedMessage += data.data; - setMessageAppeared(true); - } - - if (data.type === 'messageEnd') { - setChatHistory((prevHistory) => [ - ...prevHistory, - ['human', message], - ['assistant', recievedMessage], - ]); - - setLoading(false); - - const lastMsg = messagesRef.current[messagesRef.current.length - 1]; - - const autoImageSearch = localStorage.getItem('autoImageSearch'); - const autoVideoSearch = localStorage.getItem('autoVideoSearch'); - - if (autoImageSearch === 'true') { - document - .getElementById(`search-images-${lastMsg.messageId}`) - ?.click(); - } - - if (autoVideoSearch === 'true') { - document - .getElementById(`search-videos-${lastMsg.messageId}`) - ?.click(); - } - - if ( - lastMsg.role === 'assistant' && - lastMsg.sources && - lastMsg.sources.length > 0 && - !lastMsg.suggestions - ) { - const suggestions = await getSuggestions(messagesRef.current); - setMessages((prev) => - prev.map((msg) => { - if (msg.messageId === lastMsg.messageId) { - return { ...msg, suggestions: suggestions }; - } - return msg; - }), - ); - } - } - }; - - const res = await fetch('/api/chat', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - content: message, - message: { - messageId: messageId, - chatId: chatId!, - content: message, - }, - chatId: chatId!, - files: fileIds, - focusMode: focusMode, - optimizationMode: optimizationMode, - history: chatHistory, - chatModel: { - name: chatModelProvider.name, - provider: chatModelProvider.provider, - }, - embeddingModel: { - name: embeddingModelProvider.name, - provider: embeddingModelProvider.provider, - }, - systemInstructions: localStorage.getItem('systemInstructions'), - }), - }); - - if (!res.body) throw new Error('No response body'); - - const reader = res.body?.getReader(); - const decoder = new TextDecoder('utf-8'); - - let partialChunk = ''; - - while (true) { - const { value, done } = await reader.read(); - if (done) break; - - partialChunk += decoder.decode(value, { stream: true }); - - try { - const messages = partialChunk.split('\n'); - for (const msg of messages) { - if (!msg.trim()) continue; - const json = JSON.parse(msg); - messageHandler(json); - } - partialChunk = ''; - } catch (error) { - console.warn('Incomplete JSON, waiting for next chunk...'); - } - } - }; - - const rewrite = (messageId: string) => { - const index = messages.findIndex((msg) => msg.messageId === messageId); - - if (index === -1) return; - - const message = messages[index - 1]; - - setMessages((prev) => { - return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; - }); - setChatHistory((prev) => { - return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; - }); - - sendMessage(message.content, message.messageId); - }; - - useEffect(() => { - if (isReady && initialMessage && isConfigReady) { - sendMessage(initialMessage); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isConfigReady, isReady, initialMessage]); - +const ChatWindow = () => { + const { hasError, isReady, notFound, messages } = useChat(); if (hasError) { return (
@@ -586,31 +51,11 @@ const ChatWindow = ({ id }: { id?: string }) => {
{messages.length > 0 ? ( <> - - + + ) : ( - + )}
) diff --git a/src/components/EmptyChat.tsx b/src/components/EmptyChat.tsx index 0eb76ac..e40a338 100644 --- a/src/components/EmptyChat.tsx +++ b/src/components/EmptyChat.tsx @@ -5,27 +5,7 @@ import Link from 'next/link'; import WeatherWidget from './WeatherWidget'; import NewsArticleWidget from './NewsArticleWidget'; -const EmptyChat = ({ - sendMessage, - focusMode, - setFocusMode, - optimizationMode, - setOptimizationMode, - fileIds, - setFileIds, - files, - setFiles, -}: { - sendMessage: (message: string) => void; - focusMode: string; - setFocusMode: (mode: string) => void; - optimizationMode: string; - setOptimizationMode: (mode: string) => void; - fileIds: string[]; - setFileIds: (fileIds: string[]) => void; - files: File[]; - setFiles: (files: File[]) => void; -}) => { +const EmptyChat = () => { return (
@@ -38,17 +18,7 @@ const EmptyChat = ({

Research begins here.

- +
diff --git a/src/components/EmptyChatMessageInput.tsx b/src/components/EmptyChatMessageInput.tsx index 43d1e28..3c5ff6b 100644 --- a/src/components/EmptyChatMessageInput.tsx +++ b/src/components/EmptyChatMessageInput.tsx @@ -1,34 +1,15 @@ import { ArrowRight } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; -import CopilotToggle from './MessageInputActions/Copilot'; import Focus from './MessageInputActions/Focus'; import Optimization from './MessageInputActions/Optimization'; import Attach from './MessageInputActions/Attach'; -import { File } from './ChatWindow'; +import { useChat } from '@/lib/hooks/useChat'; -const EmptyChatMessageInput = ({ - sendMessage, - focusMode, - setFocusMode, - optimizationMode, - setOptimizationMode, - fileIds, - setFileIds, - files, - setFiles, -}: { - sendMessage: (message: string) => void; - focusMode: string; - setFocusMode: (mode: string) => void; - optimizationMode: string; - setOptimizationMode: (mode: string) => void; - fileIds: string[]; - setFileIds: (fileIds: string[]) => void; - files: File[]; - setFiles: (files: File[]) => void; -}) => { - const [copilotEnabled, setCopilotEnabled] = useState(false); +const EmptyChatMessageInput = () => { + const { sendMessage } = useChat(); + + /* const [copilotEnabled, setCopilotEnabled] = useState(false); */ const [message, setMessage] = useState(''); const inputRef = useRef(null); @@ -84,20 +65,11 @@ const EmptyChatMessageInput = ({ />
- - + +
- +
); diff --git a/src/components/ThinkBox.tsx b/src/components/ThinkBox.tsx index 9c6a576..f830945 100644 --- a/src/components/ThinkBox.tsx +++ b/src/components/ThinkBox.tsx @@ -1,15 +1,23 @@ 'use client'; -import { useState } from 'react'; -import { cn } from '@/lib/utils'; +import { useEffect, useState } from 'react'; import { ChevronDown, ChevronUp, BrainCircuit } from 'lucide-react'; interface ThinkBoxProps { content: string; + thinkingEnded: boolean; } -const ThinkBox = ({ content }: ThinkBoxProps) => { - const [isExpanded, setIsExpanded] = useState(false); +const ThinkBox = ({ content, thinkingEnded }: ThinkBoxProps) => { + const [isExpanded, setIsExpanded] = useState(true); + + useEffect(() => { + if (thinkingEnded) { + setIsExpanded(false); + } else { + setIsExpanded(true); + } + }, [thinkingEnded]); return (
diff --git a/src/components/WeatherWidget.tsx b/src/components/WeatherWidget.tsx index 669b9ff..8eaf871 100644 --- a/src/components/WeatherWidget.tsx +++ b/src/components/WeatherWidget.tsx @@ -10,6 +10,7 @@ const WeatherWidget = () => { windSpeed: 0, icon: '', temperatureUnit: 'C', + windSpeedUnit: 'm/s', }); const [loading, setLoading] = useState(true); @@ -75,7 +76,7 @@ const WeatherWidget = () => { body: JSON.stringify({ lat: location.latitude, lng: location.longitude, - temperatureUnit: localStorage.getItem('temperatureUnit') ?? 'C', + measureUnit: localStorage.getItem('measureUnit') ?? 'Metric', }), }); @@ -95,6 +96,7 @@ const WeatherWidget = () => { windSpeed: data.windSpeed, icon: data.icon, temperatureUnit: data.temperatureUnit, + windSpeedUnit: data.windSpeedUnit, }); setLoading(false); }); @@ -139,7 +141,7 @@ const WeatherWidget = () => { - {data.windSpeed} km/h + {data.windSpeed} {data.windSpeedUnit}
diff --git a/src/lib/chains/imageSearchAgent.ts b/src/lib/chains/imageSearchAgent.ts index 4fd684f..a91b7bb 100644 --- a/src/lib/chains/imageSearchAgent.ts +++ b/src/lib/chains/imageSearchAgent.ts @@ -3,32 +3,18 @@ import { RunnableMap, RunnableLambda, } from '@langchain/core/runnables'; -import { PromptTemplate } from '@langchain/core/prompts'; +import { ChatPromptTemplate } from '@langchain/core/prompts'; import formatChatHistoryAsString from '../utils/formatHistory'; import { BaseMessage } from '@langchain/core/messages'; import { StringOutputParser } from '@langchain/core/output_parsers'; import { searchSearxng } from '../searxng'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import LineOutputParser from '../outputParsers/lineOutputParser'; const imageSearchChainPrompt = ` You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images. You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. - -Example: -1. Follow up question: What is a cat? -Rephrased: A cat - -2. Follow up question: What is a car? How does it works? -Rephrased: Car working - -3. Follow up question: How does an AC work? -Rephrased: AC working - -Conversation: -{chat_history} - -Follow up question: {query} -Rephrased question: +Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text. `; type ImageSearchChainInput = { @@ -54,12 +40,39 @@ const createImageSearchChain = (llm: BaseChatModel) => { return input.query; }, }), - PromptTemplate.fromTemplate(imageSearchChainPrompt), + ChatPromptTemplate.fromMessages([ + ['system', imageSearchChainPrompt], + [ + 'user', + '\n\n\nWhat is a cat?\n', + ], + ['assistant', 'A cat'], + + [ + 'user', + '\n\n\nWhat is a car? How does it work?\n', + ], + ['assistant', 'Car working'], + [ + 'user', + '\n\n\nHow does an AC work?\n', + ], + ['assistant', 'AC working'], + [ + 'user', + '{chat_history}\n\n{query}\n', + ], + ]), llm, strParser, RunnableLambda.from(async (input: string) => { - input = input.replace(/.*?<\/think>/g, ''); + const queryParser = new LineOutputParser({ + key: 'query', + }); + return await queryParser.parse(input); + }), + RunnableLambda.from(async (input: string) => { const res = await searchSearxng(input, { engines: ['bing images', 'google images'], }); diff --git a/src/lib/chains/videoSearchAgent.ts b/src/lib/chains/videoSearchAgent.ts index f7cb156..3f878a8 100644 --- a/src/lib/chains/videoSearchAgent.ts +++ b/src/lib/chains/videoSearchAgent.ts @@ -3,33 +3,19 @@ import { RunnableMap, RunnableLambda, } from '@langchain/core/runnables'; -import { PromptTemplate } from '@langchain/core/prompts'; +import { ChatPromptTemplate } from '@langchain/core/prompts'; import formatChatHistoryAsString from '../utils/formatHistory'; import { BaseMessage } from '@langchain/core/messages'; import { StringOutputParser } from '@langchain/core/output_parsers'; import { searchSearxng } from '../searxng'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import LineOutputParser from '../outputParsers/lineOutputParser'; -const VideoSearchChainPrompt = ` - You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. - You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. - - Example: - 1. Follow up question: How does a car work? - Rephrased: How does a car work? - - 2. Follow up question: What is the theory of relativity? - Rephrased: What is theory of relativity - - 3. Follow up question: How does an AC work? - Rephrased: How does an AC work - - Conversation: - {chat_history} - - Follow up question: {query} - Rephrased question: - `; +const videoSearchChainPrompt = ` +You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search Youtube for videos. +You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation. +Output only the rephrased query wrapped in an XML element. Do not include any explanation or additional text. +`; type VideoSearchChainInput = { chat_history: BaseMessage[]; @@ -55,12 +41,37 @@ const createVideoSearchChain = (llm: BaseChatModel) => { return input.query; }, }), - PromptTemplate.fromTemplate(VideoSearchChainPrompt), + ChatPromptTemplate.fromMessages([ + ['system', videoSearchChainPrompt], + [ + 'user', + '\n\n\nHow does a car work?\n', + ], + ['assistant', 'How does a car work?'], + [ + 'user', + '\n\n\nWhat is the theory of relativity?\n', + ], + ['assistant', 'Theory of relativity'], + [ + 'user', + '\n\n\nHow does an AC work?\n', + ], + ['assistant', 'AC working'], + [ + 'user', + '{chat_history}\n\n{query}\n', + ], + ]), llm, strParser, RunnableLambda.from(async (input: string) => { - input = input.replace(/.*?<\/think>/g, ''); - + const queryParser = new LineOutputParser({ + key: 'query', + }); + return await queryParser.parse(input); + }), + RunnableLambda.from(async (input: string) => { const res = await searchSearxng(input, { engines: ['youtube'], }); @@ -92,8 +103,8 @@ const handleVideoSearch = ( input: VideoSearchChainInput, llm: BaseChatModel, ) => { - const VideoSearchChain = createVideoSearchChain(llm); - return VideoSearchChain.invoke(input); + const videoSearchChain = createVideoSearchChain(llm); + return videoSearchChain.invoke(input); }; export default handleVideoSearch; diff --git a/src/lib/config.ts b/src/lib/config.ts index d885e13..79d69dc 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -31,6 +31,7 @@ interface Config { }; OLLAMA: { API_URL: string; + API_KEY: string; }; DEEPSEEK: { API_KEY: string; @@ -86,6 +87,8 @@ export const getSearxngApiEndpoint = () => export const getOllamaApiEndpoint = () => loadConfig().MODELS.OLLAMA.API_URL; +export const getOllamaApiKey = () => loadConfig().MODELS.OLLAMA.API_KEY; + export const getDeepseekApiKey = () => loadConfig().MODELS.DEEPSEEK.API_KEY; export const getAimlApiKey = () => loadConfig().MODELS.AIMLAPI.API_KEY; diff --git a/src/lib/hooks/useChat.tsx b/src/lib/hooks/useChat.tsx new file mode 100644 index 0000000..573ac6b --- /dev/null +++ b/src/lib/hooks/useChat.tsx @@ -0,0 +1,643 @@ +'use client'; + +import { Message } from '@/components/ChatWindow'; +import { createContext, useContext, useEffect, useRef, useState } from 'react'; +import crypto from 'crypto'; +import { useSearchParams } from 'next/navigation'; +import { toast } from 'sonner'; +import { Document } from '@langchain/core/documents'; +import { getSuggestions } from '../actions'; + +type ChatContext = { + messages: Message[]; + chatHistory: [string, string][]; + files: File[]; + fileIds: string[]; + focusMode: string; + chatId: string | undefined; + optimizationMode: string; + isMessagesLoaded: boolean; + loading: boolean; + notFound: boolean; + messageAppeared: boolean; + isReady: boolean; + hasError: boolean; + setOptimizationMode: (mode: string) => void; + setFocusMode: (mode: string) => void; + setFiles: (files: File[]) => void; + setFileIds: (fileIds: string[]) => void; + sendMessage: ( + message: string, + messageId?: string, + rewrite?: boolean, + ) => Promise; + rewrite: (messageId: string) => void; +}; + +export interface File { + fileName: string; + fileExtension: string; + fileId: string; +} + +interface ChatModelProvider { + name: string; + provider: string; +} + +interface EmbeddingModelProvider { + name: string; + provider: string; +} + +const checkConfig = async ( + setChatModelProvider: (provider: ChatModelProvider) => void, + setEmbeddingModelProvider: (provider: EmbeddingModelProvider) => void, + setIsConfigReady: (ready: boolean) => void, + setHasError: (hasError: boolean) => void, +) => { + try { + let chatModel = localStorage.getItem('chatModel'); + let chatModelProvider = localStorage.getItem('chatModelProvider'); + let embeddingModel = localStorage.getItem('embeddingModel'); + let embeddingModelProvider = localStorage.getItem('embeddingModelProvider'); + + const autoImageSearch = localStorage.getItem('autoImageSearch'); + const autoVideoSearch = localStorage.getItem('autoVideoSearch'); + + if (!autoImageSearch) { + localStorage.setItem('autoImageSearch', 'true'); + } + + if (!autoVideoSearch) { + localStorage.setItem('autoVideoSearch', 'false'); + } + + const providers = await fetch(`/api/models`, { + headers: { + 'Content-Type': 'application/json', + }, + }).then(async (res) => { + if (!res.ok) + throw new Error( + `Failed to fetch models: ${res.status} ${res.statusText}`, + ); + return res.json(); + }); + + if ( + !chatModel || + !chatModelProvider || + !embeddingModel || + !embeddingModelProvider + ) { + if (!chatModel || !chatModelProvider) { + const chatModelProviders = providers.chatModelProviders; + const chatModelProvidersKeys = Object.keys(chatModelProviders); + + if (!chatModelProviders || chatModelProvidersKeys.length === 0) { + return toast.error('No chat models available'); + } else { + chatModelProvider = + chatModelProvidersKeys.find( + (provider) => + Object.keys(chatModelProviders[provider]).length > 0, + ) || chatModelProvidersKeys[0]; + } + + if ( + chatModelProvider === 'custom_openai' && + Object.keys(chatModelProviders[chatModelProvider]).length === 0 + ) { + toast.error( + "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.", + ); + return setHasError(true); + } + + chatModel = Object.keys(chatModelProviders[chatModelProvider])[0]; + } + + if (!embeddingModel || !embeddingModelProvider) { + const embeddingModelProviders = providers.embeddingModelProviders; + + if ( + !embeddingModelProviders || + Object.keys(embeddingModelProviders).length === 0 + ) + return toast.error('No embedding models available'); + + embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; + embeddingModel = Object.keys( + embeddingModelProviders[embeddingModelProvider], + )[0]; + } + + localStorage.setItem('chatModel', chatModel!); + localStorage.setItem('chatModelProvider', chatModelProvider); + localStorage.setItem('embeddingModel', embeddingModel!); + localStorage.setItem('embeddingModelProvider', embeddingModelProvider); + } else { + const chatModelProviders = providers.chatModelProviders; + const embeddingModelProviders = providers.embeddingModelProviders; + + if ( + Object.keys(chatModelProviders).length > 0 && + (!chatModelProviders[chatModelProvider] || + Object.keys(chatModelProviders[chatModelProvider]).length === 0) + ) { + const chatModelProvidersKeys = Object.keys(chatModelProviders); + chatModelProvider = + chatModelProvidersKeys.find( + (key) => Object.keys(chatModelProviders[key]).length > 0, + ) || chatModelProvidersKeys[0]; + + localStorage.setItem('chatModelProvider', chatModelProvider); + } + + if ( + chatModelProvider && + !chatModelProviders[chatModelProvider][chatModel] + ) { + if ( + chatModelProvider === 'custom_openai' && + Object.keys(chatModelProviders[chatModelProvider]).length === 0 + ) { + toast.error( + "Looks like you haven't configured any chat model providers. Please configure them from the settings page or the config file.", + ); + return setHasError(true); + } + + chatModel = Object.keys( + chatModelProviders[ + Object.keys(chatModelProviders[chatModelProvider]).length > 0 + ? chatModelProvider + : Object.keys(chatModelProviders)[0] + ], + )[0]; + + localStorage.setItem('chatModel', chatModel); + } + + if ( + Object.keys(embeddingModelProviders).length > 0 && + !embeddingModelProviders[embeddingModelProvider] + ) { + embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; + localStorage.setItem('embeddingModelProvider', embeddingModelProvider); + } + + if ( + embeddingModelProvider && + !embeddingModelProviders[embeddingModelProvider][embeddingModel] + ) { + embeddingModel = Object.keys( + embeddingModelProviders[embeddingModelProvider], + )[0]; + localStorage.setItem('embeddingModel', embeddingModel); + } + } + + setChatModelProvider({ + name: chatModel!, + provider: chatModelProvider, + }); + + setEmbeddingModelProvider({ + name: embeddingModel!, + provider: embeddingModelProvider, + }); + + setIsConfigReady(true); + } catch (err) { + console.error('An error occurred while checking the configuration:', err); + setIsConfigReady(false); + setHasError(true); + } +}; + +const loadMessages = async ( + chatId: string, + setMessages: (messages: Message[]) => void, + setIsMessagesLoaded: (loaded: boolean) => void, + setChatHistory: (history: [string, string][]) => void, + setFocusMode: (mode: string) => void, + setNotFound: (notFound: boolean) => void, + setFiles: (files: File[]) => void, + setFileIds: (fileIds: string[]) => void, +) => { + const res = await fetch(`/api/chats/${chatId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (res.status === 404) { + setNotFound(true); + setIsMessagesLoaded(true); + return; + } + + const data = await res.json(); + + const messages = data.messages.map((msg: any) => { + return { + ...msg, + ...JSON.parse(msg.metadata), + }; + }) as Message[]; + + setMessages(messages); + + const history = messages.map((msg) => { + return [msg.role, msg.content]; + }) as [string, string][]; + + console.debug(new Date(), 'app:messages_loaded'); + + document.title = messages[0].content; + + const files = data.chat.files.map((file: any) => { + return { + fileName: file.name, + fileExtension: file.name.split('.').pop(), + fileId: file.fileId, + }; + }); + + setFiles(files); + setFileIds(files.map((file: File) => file.fileId)); + + setChatHistory(history); + setFocusMode(data.chat.focusMode); + setIsMessagesLoaded(true); +}; + +export const chatContext = createContext({ + chatHistory: [], + chatId: '', + fileIds: [], + files: [], + focusMode: '', + hasError: false, + isMessagesLoaded: false, + isReady: false, + loading: false, + messageAppeared: false, + messages: [], + notFound: false, + optimizationMode: '', + rewrite: () => {}, + sendMessage: async () => {}, + setFileIds: () => {}, + setFiles: () => {}, + setFocusMode: () => {}, + setOptimizationMode: () => {}, +}); + +export const ChatProvider = ({ + children, + id, +}: { + children: React.ReactNode; + id?: string; +}) => { + const searchParams = useSearchParams(); + const initialMessage = searchParams.get('q'); + + const [chatId, setChatId] = useState(id); + const [newChatCreated, setNewChatCreated] = useState(false); + + const [loading, setLoading] = useState(false); + const [messageAppeared, setMessageAppeared] = useState(false); + + const [chatHistory, setChatHistory] = useState<[string, string][]>([]); + const [messages, setMessages] = useState([]); + + const [files, setFiles] = useState([]); + const [fileIds, setFileIds] = useState([]); + + const [focusMode, setFocusMode] = useState('webSearch'); + const [optimizationMode, setOptimizationMode] = useState('speed'); + + const [isMessagesLoaded, setIsMessagesLoaded] = useState(false); + + const [notFound, setNotFound] = useState(false); + + const [chatModelProvider, setChatModelProvider] = useState( + { + name: '', + provider: '', + }, + ); + + const [embeddingModelProvider, setEmbeddingModelProvider] = + useState({ + name: '', + provider: '', + }); + + const [isConfigReady, setIsConfigReady] = useState(false); + const [hasError, setHasError] = useState(false); + const [isReady, setIsReady] = useState(false); + + const messagesRef = useRef([]); + + useEffect(() => { + checkConfig( + setChatModelProvider, + setEmbeddingModelProvider, + setIsConfigReady, + setHasError, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if ( + chatId && + !newChatCreated && + !isMessagesLoaded && + messages.length === 0 + ) { + loadMessages( + chatId, + setMessages, + setIsMessagesLoaded, + setChatHistory, + setFocusMode, + setNotFound, + setFiles, + setFileIds, + ); + } else if (!chatId) { + setNewChatCreated(true); + setIsMessagesLoaded(true); + setChatId(crypto.randomBytes(20).toString('hex')); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + messagesRef.current = messages; + }, [messages]); + + useEffect(() => { + if (isMessagesLoaded && isConfigReady) { + setIsReady(true); + console.debug(new Date(), 'app:ready'); + } else { + setIsReady(false); + } + }, [isMessagesLoaded, isConfigReady]); + + const rewrite = (messageId: string) => { + const index = messages.findIndex((msg) => msg.messageId === messageId); + + if (index === -1) return; + + const message = messages[index - 1]; + + setMessages((prev) => { + return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; + }); + setChatHistory((prev) => { + return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; + }); + + sendMessage(message.content, message.messageId, true); + }; + + useEffect(() => { + if (isReady && initialMessage && isConfigReady) { + if (!isConfigReady) { + toast.error('Cannot send message before the configuration is ready'); + return; + } + sendMessage(initialMessage); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConfigReady, isReady, initialMessage]); + + const sendMessage: ChatContext['sendMessage'] = async ( + message, + messageId, + rewrite = false, + ) => { + if (loading) return; + setLoading(true); + setMessageAppeared(false); + + let sources: Document[] | undefined = undefined; + let recievedMessage = ''; + let added = false; + + messageId = messageId ?? crypto.randomBytes(7).toString('hex'); + + setMessages((prevMessages) => [ + ...prevMessages, + { + content: message, + messageId: messageId, + chatId: chatId!, + role: 'user', + createdAt: new Date(), + }, + ]); + + const messageHandler = async (data: any) => { + if (data.type === 'error') { + toast.error(data.data); + setLoading(false); + return; + } + + if (data.type === 'sources') { + sources = data.data; + if (!added) { + setMessages((prevMessages) => [ + ...prevMessages, + { + content: '', + messageId: data.messageId, + chatId: chatId!, + role: 'assistant', + sources: sources, + createdAt: new Date(), + }, + ]); + added = true; + } + setMessageAppeared(true); + } + + if (data.type === 'message') { + if (!added) { + setMessages((prevMessages) => [ + ...prevMessages, + { + content: data.data, + messageId: data.messageId, + chatId: chatId!, + role: 'assistant', + sources: sources, + createdAt: new Date(), + }, + ]); + added = true; + } + + setMessages((prev) => + prev.map((message) => { + if (message.messageId === data.messageId) { + return { ...message, content: message.content + data.data }; + } + + return message; + }), + ); + + recievedMessage += data.data; + setMessageAppeared(true); + } + + if (data.type === 'messageEnd') { + setChatHistory((prevHistory) => [ + ...prevHistory, + ['human', message], + ['assistant', recievedMessage], + ]); + + setLoading(false); + + const lastMsg = messagesRef.current[messagesRef.current.length - 1]; + + const autoImageSearch = localStorage.getItem('autoImageSearch'); + const autoVideoSearch = localStorage.getItem('autoVideoSearch'); + + if (autoImageSearch === 'true') { + document + .getElementById(`search-images-${lastMsg.messageId}`) + ?.click(); + } + + if (autoVideoSearch === 'true') { + document + .getElementById(`search-videos-${lastMsg.messageId}`) + ?.click(); + } + + if ( + lastMsg.role === 'assistant' && + lastMsg.sources && + lastMsg.sources.length > 0 && + !lastMsg.suggestions + ) { + const suggestions = await getSuggestions(messagesRef.current); + setMessages((prev) => + prev.map((msg) => { + if (msg.messageId === lastMsg.messageId) { + return { ...msg, suggestions: suggestions }; + } + return msg; + }), + ); + } + } + }; + + const messageIndex = messages.findIndex((m) => m.messageId === messageId); + + const res = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: message, + message: { + messageId: messageId, + chatId: chatId!, + content: message, + }, + chatId: chatId!, + files: fileIds, + focusMode: focusMode, + optimizationMode: optimizationMode, + history: rewrite + ? chatHistory.slice(0, messageIndex === -1 ? undefined : messageIndex) + : chatHistory, + chatModel: { + name: chatModelProvider.name, + provider: chatModelProvider.provider, + }, + embeddingModel: { + name: embeddingModelProvider.name, + provider: embeddingModelProvider.provider, + }, + systemInstructions: localStorage.getItem('systemInstructions'), + }), + }); + + if (!res.body) throw new Error('No response body'); + + const reader = res.body?.getReader(); + const decoder = new TextDecoder('utf-8'); + + let partialChunk = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + partialChunk += decoder.decode(value, { stream: true }); + + try { + const messages = partialChunk.split('\n'); + for (const msg of messages) { + if (!msg.trim()) continue; + const json = JSON.parse(msg); + messageHandler(json); + } + partialChunk = ''; + } catch (error) { + console.warn('Incomplete JSON, waiting for next chunk...'); + } + } + }; + + return ( + + {children} + + ); +}; + +export const useChat = () => { + const ctx = useContext(chatContext); + return ctx; +}; diff --git a/src/lib/prompts/webSearch.ts b/src/lib/prompts/webSearch.ts index 5952562..1a431ea 100644 --- a/src/lib/prompts/webSearch.ts +++ b/src/lib/prompts/webSearch.ts @@ -1,41 +1,63 @@ export const webSearchRetrieverPrompt = ` -You are an AI question rephraser. You will be given a conversation and a follow-up question; rephrase it into a standalone question that another LLM can use to search the web. +You are an AI question rephraser. You will be given a conversation and a follow-up question, you will have to rephrase the follow up question so it is a standalone question and can be used by another LLM to search the web for information to answer it. +If it is a simple writing task or a greeting (unless the greeting contains a question after it) like Hi, Hello, How are you, etc. than a question then you need to return \`not_needed\` as the response (This is because the LLM won't need to search the web for finding information on this topic). +If the user asks some question from some URL or wants you to summarize a PDF or a webpage (via URL) you need to return the links inside the \`links\` XML block and the question inside the \`question\` XML block. If the user wants to you to summarize the webpage or the PDF you need to return \`summarize\` inside the \`question\` XML block in place of a question and the link to summarize in the \`links\` XML block. +You must always return the rephrased question inside the \`question\` XML block, if there are no links in the follow-up question then don't insert a \`links\` XML block in your response. -Return ONLY a JSON object that matches this schema: -query: string // the standalone question (or "summarize") -links: string[] // URLs extracted from the user query (empty if none) -searchRequired: boolean // true if web search is needed, false for greetings/simple writing tasks -searchMode: "" | "normal" | "news" // "" when searchRequired is false; "news" if the user asks for news/articles, otherwise "normal" +There are several examples attached for your reference inside the below \`examples\` XML block -Rules -- Greetings / simple writing tasks → query:"", links:[], searchRequired:false, searchMode:"" -- Summarizing a URL → query:"summarize", links:[url...], searchRequired:true, searchMode:"normal" -- Asking for news/articles → searchMode:"news" - -Examples -1. Follow-up: What is the capital of France? -"query":"capital of France","links":[],"searchRequired":true,"searchMode":"normal" + +1. Follow up question: What is the capital of France +Rephrased question:\` + +Capital of france + +\` 2. Hi, how are you? -"query":"","links":[],"searchRequired":false,"searchMode":"" +Rephrased question\` + +not_needed + +\` -3. Follow-up: What is Docker? -"query":"what is Docker","links":[],"searchRequired":true,"searchMode":"normal" +3. Follow up question: What is Docker? +Rephrased question: \` + +What is Docker + +\` -4. Follow-up: Can you tell me what is X from https://example.com? -"query":"what is X","links":["https://example.com"],"searchRequired":true,"searchMode":"normal" +4. Follow up question: Can you tell me what is X from https://example.com +Rephrased question: \` + +Can you tell me what is X? + -5. Follow-up: Summarize the content from https://example.com -"query":"summarize","links":["https://example.com"],"searchRequired":true,"searchMode":"normal" + +https://example.com + +\` -6. Follow-up: Latest news about AI -"query":"latest news about AI","links":[],"searchRequired":true,"searchMode":"news" +5. Follow up question: Summarize the content from https://example.com +Rephrased question: \` + +summarize + + + +https://example.com + +\` + + +Anything below is the part of the actual conversation and you need to use conversation and the follow-up question to rephrase the follow-up question as a standalone question based on the guidelines shared above. {chat_history} -Follow-up question: {query} +Follow up question: {query} Rephrased question: `; diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index 2b0f2cc..6af2115 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -9,6 +9,18 @@ export const PROVIDER_INFO = { import { BaseChatModel } from '@langchain/core/language_models/chat_models'; const anthropicChatModels: Record[] = [ + { + displayName: 'Claude 4.1 Opus', + key: 'claude-opus-4-1-20250805', + }, + { + displayName: 'Claude 4 Opus', + key: 'claude-opus-4-20250514', + }, + { + displayName: 'Claude 4 Sonnet', + key: 'claude-sonnet-4-20250514', + }, { displayName: 'Claude 3.7 Sonnet', key: 'claude-3-7-sonnet-20250219', diff --git a/src/lib/providers/gemini.ts b/src/lib/providers/gemini.ts index b84e899..418e0a4 100644 --- a/src/lib/providers/gemini.ts +++ b/src/lib/providers/gemini.ts @@ -14,16 +14,16 @@ import { Embeddings } from '@langchain/core/embeddings'; const geminiChatModels: Record[] = [ { - displayName: 'Gemini 2.5 Flash Preview 05-20', - key: 'gemini-2.5-flash-preview-05-20', + displayName: 'Gemini 2.5 Flash', + key: 'gemini-2.5-flash', }, { - displayName: 'Gemini 2.5 Pro Preview', - key: 'gemini-2.5-pro-preview-05-06', + displayName: 'Gemini 2.5 Flash-Lite', + key: 'gemini-2.5-flash-lite', }, { - displayName: 'Gemini 2.5 Pro Experimental', - key: 'gemini-2.5-pro-preview-05-06', + displayName: 'Gemini 2.5 Pro', + key: 'gemini-2.5-pro', }, { displayName: 'Gemini 2.0 Flash', @@ -75,7 +75,7 @@ export const loadGeminiChatModels = async () => { displayName: model.displayName, model: new ChatGoogleGenerativeAI({ apiKey: geminiApiKey, - modelName: model.key, + model: model.key, temperature: 0.7, }) as unknown as BaseChatModel, }; @@ -108,7 +108,7 @@ export const loadGeminiEmbeddingModels = async () => { return embeddingModels; } catch (err) { - console.error(`Error loading OpenAI embeddings models: ${err}`); + console.error(`Error loading Gemini embeddings models: ${err}`); return {}; } }; diff --git a/src/lib/providers/groq.ts b/src/lib/providers/groq.ts index 6a196ee..4e7db51 100644 --- a/src/lib/providers/groq.ts +++ b/src/lib/providers/groq.ts @@ -1,4 +1,4 @@ -import { ChatOpenAI } from '@langchain/openai'; +import { ChatGroq } from '@langchain/groq'; import { getGroqApiKey } from '../config'; import { ChatModel } from '.'; @@ -28,16 +28,10 @@ export const loadGroqChatModels = async () => { groqChatModels.forEach((model: any) => { chatModels[model.id] = { displayName: model.id, - model: new ChatOpenAI({ + model: new ChatGroq({ apiKey: groqApiKey, - modelName: model.id, + model: model.id, temperature: 0.7, - configuration: { - baseURL: 'https://api.groq.com/openai/v1', - }, - metadata: { - 'model-type': 'groq', - }, }) as unknown as BaseChatModel, }; }); diff --git a/src/lib/providers/ollama.ts b/src/lib/providers/ollama.ts index d5c7899..cb0b848 100644 --- a/src/lib/providers/ollama.ts +++ b/src/lib/providers/ollama.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { getKeepAlive, getOllamaApiEndpoint } from '../config'; +import { getKeepAlive, getOllamaApiEndpoint, getOllamaApiKey } from '../config'; import { ChatModel, EmbeddingModel } from '.'; export const PROVIDER_INFO = { @@ -11,6 +11,7 @@ import { OllamaEmbeddings } from '@langchain/ollama'; export const loadOllamaChatModels = async () => { const ollamaApiEndpoint = getOllamaApiEndpoint(); + const ollamaApiKey = getOllamaApiKey(); if (!ollamaApiEndpoint) return {}; @@ -33,6 +34,9 @@ export const loadOllamaChatModels = async () => { model: model.model, temperature: 0.7, keepAlive: getKeepAlive(), + ...(ollamaApiKey + ? { headers: { Authorization: `Bearer ${ollamaApiKey}` } } + : {}), }), }; }); @@ -46,6 +50,7 @@ export const loadOllamaChatModels = async () => { export const loadOllamaEmbeddingModels = async () => { const ollamaApiEndpoint = getOllamaApiEndpoint(); + const ollamaApiKey = getOllamaApiKey(); if (!ollamaApiEndpoint) return {}; @@ -66,6 +71,9 @@ export const loadOllamaEmbeddingModels = async () => { model: new OllamaEmbeddings({ baseUrl: ollamaApiEndpoint, model: model.model, + ...(ollamaApiKey + ? { headers: { Authorization: `Bearer ${ollamaApiKey}` } } + : {}), }), }; }); diff --git a/src/lib/providers/openai.ts b/src/lib/providers/openai.ts index c857b0e..7e26763 100644 --- a/src/lib/providers/openai.ts +++ b/src/lib/providers/openai.ts @@ -42,6 +42,18 @@ const openaiChatModels: Record[] = [ displayName: 'GPT 4.1', key: 'gpt-4.1', }, + { + displayName: 'GPT 5 nano', + key: 'gpt-5-nano', + }, + { + displayName: 'GPT 5 mini', + key: 'gpt-5-mini', + }, + { + displayName: 'GPT 5', + key: 'gpt-5', + }, ]; const openaiEmbeddingModels: Record[] = [ @@ -69,7 +81,7 @@ export const loadOpenAIChatModels = async () => { model: new ChatOpenAI({ apiKey: openaiApiKey, modelName: model.key, - temperature: 0.7, + temperature: model.key.includes('gpt-5') ? 1 : 0.7, }) as unknown as BaseChatModel, }; }); diff --git a/src/lib/search/metaSearchAgent.ts b/src/lib/search/metaSearchAgent.ts index c3bf389..67b7c58 100644 --- a/src/lib/search/metaSearchAgent.ts +++ b/src/lib/search/metaSearchAgent.ts @@ -24,7 +24,6 @@ import computeSimilarity from '../utils/computeSimilarity'; import formatChatHistoryAsString from '../utils/formatHistory'; import eventEmitter from 'events'; import { StreamEvent } from '@langchain/core/tracers/log_stream'; -import { z } from 'zod'; export interface MetaSearchAgentType { searchAndAnswer: ( @@ -53,17 +52,6 @@ type BasicChainInput = { query: string; }; -const retrieverLLMOutputSchema = z.object({ - query: z.string().describe('The query to search the web for.'), - links: z - .array(z.string()) - .describe('The links to search/summarize if present'), - searchRequired: z - .boolean() - .describe('Wether there is a need to search the web'), - searchMode: z.enum(['', 'normal', 'news']).describe('The search mode.'), -}); - class MetaSearchAgent implements MetaSearchAgentType { private config: Config; private strParser = new StringOutputParser(); @@ -74,71 +62,73 @@ class MetaSearchAgent implements MetaSearchAgentType { private async createSearchRetrieverChain(llm: BaseChatModel) { (llm as unknown as ChatOpenAI).temperature = 0; + return RunnableSequence.from([ PromptTemplate.fromTemplate(this.config.queryGeneratorPrompt), - Object.assign( - Object.create(Object.getPrototypeOf(llm)), - llm, - ).withStructuredOutput(retrieverLLMOutputSchema, { - ...(llm.metadata?.['model-type'] === 'groq' - ? { - method: 'json-object', - } - : {}), - }), - RunnableLambda.from( - async (input: z.infer) => { - let question = input.query; - const links = input.links; + llm, + this.strParser, + RunnableLambda.from(async (input: string) => { + const linksOutputParser = new LineListOutputParser({ + key: 'links', + }); - if (!input.searchRequired) { - return { query: '', docs: [] }; + const questionOutputParser = new LineOutputParser({ + key: 'question', + }); + + const links = await linksOutputParser.parse(input); + let question = this.config.summarizer + ? await questionOutputParser.parse(input) + : input; + + if (question === 'not_needed') { + return { query: '', docs: [] }; + } + + if (links.length > 0) { + if (question.length === 0) { + question = 'summarize'; } - if (links.length > 0) { - if (question.length === 0) { - question = 'summarize'; + let docs: Document[] = []; + + const linkDocs = await getDocumentsFromLinks({ links }); + + const docGroups: Document[] = []; + + linkDocs.map((doc) => { + const URLDocExists = docGroups.find( + (d) => + d.metadata.url === doc.metadata.url && + d.metadata.totalDocs < 10, + ); + + if (!URLDocExists) { + docGroups.push({ + ...doc, + metadata: { + ...doc.metadata, + totalDocs: 1, + }, + }); } - let docs: Document[] = []; + const docIndex = docGroups.findIndex( + (d) => + d.metadata.url === doc.metadata.url && + d.metadata.totalDocs < 10, + ); - const linkDocs = await getDocumentsFromLinks({ links }); + if (docIndex !== -1) { + docGroups[docIndex].pageContent = + docGroups[docIndex].pageContent + `\n\n` + doc.pageContent; + docGroups[docIndex].metadata.totalDocs += 1; + } + }); - const docGroups: Document[] = []; - - linkDocs.map((doc) => { - const URLDocExists = docGroups.find( - (d) => - d.metadata.url === doc.metadata.url && - d.metadata.totalDocs < 10, - ); - - if (!URLDocExists) { - docGroups.push({ - ...doc, - metadata: { - ...doc.metadata, - totalDocs: 1, - }, - }); - } - - const docIndex = docGroups.findIndex( - (d) => - d.metadata.url === doc.metadata.url && - d.metadata.totalDocs < 10, - ); - - if (docIndex !== -1) { - docGroups[docIndex].pageContent = - docGroups[docIndex].pageContent + `\n\n` + doc.pageContent; - docGroups[docIndex].metadata.totalDocs += 1; - } - }); - - await Promise.all( - docGroups.map(async (doc) => { - const res = await llm.invoke(` + await Promise.all( + docGroups.map(async (doc) => { + const res = await llm.invoke(` You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the text into a detailed, 2-4 paragraph explanation that captures the main ideas and provides a comprehensive answer to the query. If the query is \"summarize\", you should provide a detailed summary of the text. If the query is a specific question, you should answer it in the summary. @@ -199,50 +189,46 @@ class MetaSearchAgent implements MetaSearchAgentType { Make sure to answer the query in the summary. `); - const document = new Document({ - pageContent: res.content as string, - metadata: { - title: doc.metadata.title, - url: doc.metadata.url, - }, - }); + const document = new Document({ + pageContent: res.content as string, + metadata: { + title: doc.metadata.title, + url: doc.metadata.url, + }, + }); - docs.push(document); + docs.push(document); + }), + ); + + return { query: question, docs: docs }; + } else { + question = question.replace(/.*?<\/think>/g, ''); + + const res = await searchSearxng(question, { + language: 'en', + engines: this.config.activeEngines, + }); + + const documents = res.results.map( + (result) => + new Document({ + pageContent: + result.content || + (this.config.activeEngines.includes('youtube') + ? result.title + : '') /* Todo: Implement transcript grabbing using Youtubei (source: https://www.npmjs.com/package/youtubei) */, + metadata: { + title: result.title, + url: result.url, + ...(result.img_src && { img_src: result.img_src }), + }, }), - ); + ); - return { query: question, docs: docs }; - } else { - question = question.replace(/.*?<\/think>/g, ''); - - const res = await searchSearxng(question, { - language: 'en', - engines: - input.searchMode === 'normal' - ? this.config.activeEngines - : ['bing news'], - }); - - const documents = res.results.map( - (result) => - new Document({ - pageContent: - result.content || - (this.config.activeEngines.includes('youtube') - ? result.title - : '') /* Todo: Implement transcript grabbing using Youtubei (source: https://www.npmjs.com/package/youtubei) */, - metadata: { - title: result.title, - url: result.url, - ...(result.img_src && { img_src: result.img_src }), - }, - }), - ); - - return { query: question, docs: documents }; - } - }, - ), + return { query: question, docs: documents }; + } + }), ]); } diff --git a/src/lib/utils/formatHistory.ts b/src/lib/utils/formatHistory.ts index 6d0d309..733ffc0 100644 --- a/src/lib/utils/formatHistory.ts +++ b/src/lib/utils/formatHistory.ts @@ -1,8 +1,11 @@ -import { BaseMessage } from '@langchain/core/messages'; +import { BaseMessage, isAIMessage } from '@langchain/core/messages'; const formatChatHistoryAsString = (history: BaseMessage[]) => { return history - .map((message) => `${message._getType()}: ${message.content}`) + .map( + (message) => + `${isAIMessage(message) ? 'AI' : 'User'}: ${message.content}`, + ) .join('\n'); }; diff --git a/yarn.lock b/yarn.lock index b8893e7..8a6859a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -653,6 +653,14 @@ "@google/generative-ai" "^0.24.0" uuid "^11.1.0" +"@langchain/groq@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@langchain/groq/-/groq-0.2.3.tgz#3bfcbfc827cf469df3a1b5bb9799f4b0212b4625" + integrity sha512-r+yjysG36a0IZxTlCMr655Feumfb4IrOyA0jLLq4l7gEhVyMpYXMwyE6evseyU2LRP+7qOPbGRVpGqAIK0MsUA== + dependencies: + groq-sdk "^0.19.0" + zod "^3.22.4" + "@langchain/ollama@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.3.tgz#4868e66db4fc480f08c42fc652274abbab0416f0" @@ -2732,6 +2740,19 @@ graphql@^16.11.0: resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.11.0.tgz#96d17f66370678027fdf59b2d4c20b4efaa8a633" integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw== +groq-sdk@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/groq-sdk/-/groq-sdk-0.19.0.tgz#564ce018172dc3e2e2793398e0227a035a357d09" + integrity sha512-vdh5h7ORvwvOvutA80dKF81b0gPWHxu6K/GOJBOM0n6p6CSqAVLhFfeS79Ef0j/yCycDR09jqY7jkYz9dLiS6w== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + guid-typescript@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc"