Compare commits

..

5 Commits

Author SHA1 Message Date
ItzCrazyKns
341aae4587 Merge branch 'pr/830' 2025-07-19 21:36:23 +05:30
Willie Zutz
7f62907385 feat(weather): update measurement units to Imperial/Metric 2025-07-19 08:53:11 -06:00
ItzCrazyKns
7c4aa683a2 feat(chains): remove unused imports 2025-07-19 17:57:32 +05:30
ItzCrazyKns
b48b0eeb0e feat(imageSearch): use XML parsing, implement few shot prompting 2025-07-19 17:52:30 +05:30
ItzCrazyKns
cddc793915 feat(videoSearch): use XML parsing, use few shot prompting 2025-07-19 17:52:14 +05:30
21 changed files with 652 additions and 509 deletions

View File

@@ -15,12 +15,11 @@
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@icons-pack/react-simple-icons": "^12.3.0", "@icons-pack/react-simple-icons": "^12.3.0",
"@langchain/anthropic": "^0.3.24", "@langchain/anthropic": "^0.3.15",
"@langchain/community": "^0.3.49", "@langchain/community": "^0.3.36",
"@langchain/core": "^0.3.66", "@langchain/core": "^0.3.42",
"@langchain/google-genai": "^0.2.15", "@langchain/google-genai": "^0.1.12",
"@langchain/ollama": "^0.2.3", "@langchain/openai": "^0.0.25",
"@langchain/openai": "^0.6.2",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@tailwindcss/typography": "^0.5.12", "@tailwindcss/typography": "^0.5.12",
"@xenova/transformers": "^2.17.2", "@xenova/transformers": "^2.17.2",
@@ -32,7 +31,7 @@
"drizzle-orm": "^0.40.1", "drizzle-orm": "^0.40.1",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"jspdf": "^3.0.1", "jspdf": "^3.0.1",
"langchain": "^0.3.30", "langchain": "^0.1.30",
"lucide-react": "^0.363.0", "lucide-react": "^0.363.0",
"mammoth": "^1.9.1", "mammoth": "^1.9.1",
"markdown-to-jsx": "^7.7.2", "markdown-to-jsx": "^7.7.2",

View File

@@ -223,7 +223,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
apiKey: getCustomOpenaiApiKey(), openAIApiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -49,7 +49,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
apiKey: getCustomOpenaiApiKey(), openAIApiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -81,7 +81,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
modelName: body.chatModel?.name || getCustomOpenaiModelName(), modelName: body.chatModel?.name || getCustomOpenaiModelName(),
apiKey: openAIApiKey:
body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(), body.chatModel?.customOpenAIKey || getCustomOpenaiApiKey(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -48,7 +48,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
apiKey: getCustomOpenaiApiKey(), openAIApiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -49,7 +49,7 @@ export const POST = async (req: Request) => {
if (body.chatModel?.provider === 'custom_openai') { if (body.chatModel?.provider === 'custom_openai') {
llm = new ChatOpenAI({ llm = new ChatOpenAI({
apiKey: getCustomOpenaiApiKey(), openAIApiKey: getCustomOpenaiApiKey(),
modelName: getCustomOpenaiModelName(), modelName: getCustomOpenaiModelName(),
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -1,7 +1,10 @@
export const POST = async (req: Request) => { export const POST = async (req: Request) => {
try { try {
const body: { lat: number; lng: number; temperatureUnit: 'C' | 'F' } = const body: {
await req.json(); lat: number;
lng: number;
measureUnit: 'Imperial' | 'Metric';
} = await req.json();
if (!body.lat || !body.lng) { if (!body.lat || !body.lng) {
return Response.json( return Response.json(
@@ -13,7 +16,9 @@ export const POST = async (req: Request) => {
} }
const res = await fetch( const res = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${body.lat}&longitude=${body.lng}&current=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}&current=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(); const data = await res.json();
@@ -35,13 +40,15 @@ export const POST = async (req: Request) => {
windSpeed: number; windSpeed: number;
icon: string; icon: string;
temperatureUnit: 'C' | 'F'; temperatureUnit: 'C' | 'F';
windSpeedUnit: 'm/s' | 'mph';
} = { } = {
temperature: data.current.temperature_2m, temperature: data.current.temperature_2m,
condition: '', condition: '',
humidity: data.current.relative_humidity_2m, humidity: data.current.relative_humidity_2m,
windSpeed: data.current.wind_speed_10m, windSpeed: data.current.wind_speed_10m,
icon: '', icon: '',
temperatureUnit: body.temperatureUnit, temperatureUnit: body.measureUnit === 'Metric' ? 'C' : 'F',
windSpeedUnit: body.measureUnit === 'Metric' ? 'm/s' : 'mph',
}; };
const code = data.current.weather_code; const code = data.current.weather_code;

View File

@@ -148,7 +148,9 @@ const Page = () => {
const [automaticImageSearch, setAutomaticImageSearch] = useState(false); const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
const [systemInstructions, setSystemInstructions] = useState<string>(''); const [systemInstructions, setSystemInstructions] = useState<string>('');
const [temperatureUnit, setTemperatureUnit] = useState<'C' | 'F'>('C'); const [measureUnit, setMeasureUnit] = useState<'Imperial' | 'Metric'>(
'Metric',
);
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({}); const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
useEffect(() => { useEffect(() => {
@@ -211,7 +213,9 @@ const Page = () => {
setSystemInstructions(localStorage.getItem('systemInstructions')!); setSystemInstructions(localStorage.getItem('systemInstructions')!);
setTemperatureUnit(localStorage.getItem('temperatureUnit')! as 'C' | 'F'); setMeasureUnit(
localStorage.getItem('measureUnit')! as 'Imperial' | 'Metric',
);
setIsLoading(false); setIsLoading(false);
}; };
@@ -371,8 +375,8 @@ const Page = () => {
localStorage.setItem('embeddingModel', value); localStorage.setItem('embeddingModel', value);
} else if (key === 'systemInstructions') { } else if (key === 'systemInstructions') {
localStorage.setItem('systemInstructions', value); localStorage.setItem('systemInstructions', value);
} else if (key === 'temperatureUnit') { } else if (key === 'measureUnit') {
localStorage.setItem('temperatureUnit', value.toString()); localStorage.setItem('measureUnit', value.toString());
} }
} catch (err) { } catch (err) {
console.error('Failed to save:', err); console.error('Failed to save:', err);
@@ -430,22 +434,22 @@ const Page = () => {
</div> </div>
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm"> <p className="text-black/70 dark:text-white/70 text-sm">
Temperature Unit Measurement Units
</p> </p>
<Select <Select
value={temperatureUnit ?? undefined} value={measureUnit ?? undefined}
onChange={(e) => { onChange={(e) => {
setTemperatureUnit(e.target.value as 'C' | 'F'); setMeasureUnit(e.target.value as 'Imperial' | 'Metric');
saveConfig('temperatureUnit', e.target.value); saveConfig('measureUnit', e.target.value);
}} }}
options={[ options={[
{ {
label: 'Celsius', label: 'Metric',
value: 'C', value: 'Metric',
}, },
{ {
label: 'Fahrenheit', label: 'Imperial',
value: 'F', value: 'Imperial',
}, },
]} ]}
/> />

View File

@@ -10,6 +10,7 @@ const WeatherWidget = () => {
windSpeed: 0, windSpeed: 0,
icon: '', icon: '',
temperatureUnit: 'C', temperatureUnit: 'C',
windSpeedUnit: 'm/s',
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -75,7 +76,7 @@ const WeatherWidget = () => {
body: JSON.stringify({ body: JSON.stringify({
lat: location.latitude, lat: location.latitude,
lng: location.longitude, lng: location.longitude,
temperatureUnit: localStorage.getItem('temperatureUnit') ?? 'C', measureUnit: localStorage.getItem('measureUnit') ?? 'Metric',
}), }),
}); });
@@ -95,6 +96,7 @@ const WeatherWidget = () => {
windSpeed: data.windSpeed, windSpeed: data.windSpeed,
icon: data.icon, icon: data.icon,
temperatureUnit: data.temperatureUnit, temperatureUnit: data.temperatureUnit,
windSpeedUnit: data.windSpeedUnit,
}); });
setLoading(false); setLoading(false);
}); });
@@ -139,7 +141,7 @@ const WeatherWidget = () => {
</span> </span>
<span className="flex items-center text-xs text-black/60 dark:text-white/60"> <span className="flex items-center text-xs text-black/60 dark:text-white/60">
<Wind className="w-3 h-3 mr-1" /> <Wind className="w-3 h-3 mr-1" />
{data.windSpeed} km/h {data.windSpeed} {data.windSpeedUnit}
</span> </span>
</div> </div>
<span className="text-xs text-black/60 dark:text-white/60 mt-1"> <span className="text-xs text-black/60 dark:text-white/60 mt-1">

View File

@@ -3,32 +3,18 @@ import {
RunnableMap, RunnableMap,
RunnableLambda, RunnableLambda,
} from '@langchain/core/runnables'; } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts'; import { ChatPromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory'; import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages'; import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers'; import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../searxng'; import { searchSearxng } from '../searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import LineOutputParser from '../outputParsers/lineOutputParser';
const imageSearchChainPrompt = ` 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 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. 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 <query> element. Do not include any explanation or additional text.
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:
`; `;
type ImageSearchChainInput = { type ImageSearchChainInput = {
@@ -54,12 +40,39 @@ const createImageSearchChain = (llm: BaseChatModel) => {
return input.query; return input.query;
}, },
}), }),
PromptTemplate.fromTemplate(imageSearchChainPrompt), ChatPromptTemplate.fromMessages([
['system', imageSearchChainPrompt],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is a cat?\n</follow_up>',
],
['assistant', '<query>A cat</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is a car? How does it work?\n</follow_up>',
],
['assistant', '<query>Car working</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
],
['assistant', '<query>AC working</query>'],
[
'user',
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
],
]),
llm, llm,
strParser, strParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (input: string) => {
input = input.replace(/<think>.*?<\/think>/g, ''); const queryParser = new LineOutputParser({
key: 'query',
});
return await queryParser.parse(input);
}),
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, { const res = await searchSearxng(input, {
engines: ['bing images', 'google images'], engines: ['bing images', 'google images'],
}); });

View File

@@ -3,33 +3,19 @@ import {
RunnableMap, RunnableMap,
RunnableLambda, RunnableLambda,
} from '@langchain/core/runnables'; } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts'; import { ChatPromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory'; import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages'; import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers'; import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../searxng'; import { searchSearxng } from '../searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import LineOutputParser from '../outputParsers/lineOutputParser';
const VideoSearchChainPrompt = ` 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 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. 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 <query> element. Do not include any explanation or additional text.
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:
`;
type VideoSearchChainInput = { type VideoSearchChainInput = {
chat_history: BaseMessage[]; chat_history: BaseMessage[];
@@ -55,12 +41,37 @@ const createVideoSearchChain = (llm: BaseChatModel) => {
return input.query; return input.query;
}, },
}), }),
PromptTemplate.fromTemplate(VideoSearchChainPrompt), ChatPromptTemplate.fromMessages([
['system', videoSearchChainPrompt],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does a car work?\n</follow_up>',
],
['assistant', '<query>How does a car work?</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nWhat is the theory of relativity?\n</follow_up>',
],
['assistant', '<query>Theory of relativity</query>'],
[
'user',
'<conversation>\n</conversation>\n<follow_up>\nHow does an AC work?\n</follow_up>',
],
['assistant', '<query>AC working</query>'],
[
'user',
'<conversation>{chat_history}</conversation>\n<follow_up>\n{query}\n</follow_up>',
],
]),
llm, llm,
strParser, strParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (input: string) => {
input = input.replace(/<think>.*?<\/think>/g, ''); const queryParser = new LineOutputParser({
key: 'query',
});
return await queryParser.parse(input);
}),
RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, { const res = await searchSearxng(input, {
engines: ['youtube'], engines: ['youtube'],
}); });
@@ -92,8 +103,8 @@ const handleVideoSearch = (
input: VideoSearchChainInput, input: VideoSearchChainInput,
llm: BaseChatModel, llm: BaseChatModel,
) => { ) => {
const VideoSearchChain = createVideoSearchChain(llm); const videoSearchChain = createVideoSearchChain(llm);
return VideoSearchChain.invoke(input); return videoSearchChain.invoke(input);
}; };
export default handleVideoSearch; export default handleVideoSearch;

View File

@@ -1,41 +1,63 @@
export const webSearchRetrieverPrompt = ` 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: There are several examples attached for your reference inside the below \`examples\` XML block
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"
Rules <examples>
- Greetings / simple writing tasks → query:"", links:[], searchRequired:false, searchMode:"" 1. Follow up question: What is the capital of France
- Summarizing a URL → query:"summarize", links:[url...], searchRequired:true, searchMode:"normal" Rephrased question:\`
- Asking for news/articles → searchMode:"news" <question>
Capital of france
Examples </question>
1. Follow-up: What is the capital of France? \`
"query":"capital of France","links":[],"searchRequired":true,"searchMode":"normal"
2. Hi, how are you? 2. Hi, how are you?
"query":"","links":[],"searchRequired":false,"searchMode":"" Rephrased question\`
<question>
not_needed
</question>
\`
3. Follow-up: What is Docker? 3. Follow up question: What is Docker?
"query":"what is Docker","links":[],"searchRequired":true,"searchMode":"normal" Rephrased question: \`
<question>
What is Docker
</question>
\`
4. Follow-up: Can you tell me what is X from https://example.com? 4. Follow up question: Can you tell me what is X from https://example.com
"query":"what is X","links":["https://example.com"],"searchRequired":true,"searchMode":"normal" Rephrased question: \`
<question>
Can you tell me what is X?
</question>
5. Follow-up: Summarize the content from https://example.com <links>
"query":"summarize","links":["https://example.com"],"searchRequired":true,"searchMode":"normal" https://example.com
</links>
\`
6. Follow-up: Latest news about AI 5. Follow up question: Summarize the content from https://example.com
"query":"latest news about AI","links":[],"searchRequired":true,"searchMode":"news" Rephrased question: \`
<question>
summarize
</question>
<links>
https://example.com
</links>
\`
</examples>
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.
<conversation> <conversation>
{chat_history} {chat_history}
</conversation> </conversation>
Follow-up question: {query} Follow up question: {query}
Rephrased question: Rephrased question:
`; `;

View File

@@ -38,7 +38,7 @@ export const loadAimlApiChatModels = async () => {
chatModels[model.id] = { chatModels[model.id] = {
displayName: model.name || model.id, displayName: model.name || model.id,
model: new ChatOpenAI({ model: new ChatOpenAI({
apiKey: apiKey, openAIApiKey: apiKey,
modelName: model.id, modelName: model.id,
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {
@@ -76,7 +76,7 @@ export const loadAimlApiEmbeddingModels = async () => {
embeddingModels[model.id] = { embeddingModels[model.id] = {
displayName: model.name || model.id, displayName: model.name || model.id,
model: new OpenAIEmbeddings({ model: new OpenAIEmbeddings({
apiKey: apiKey, openAIApiKey: apiKey,
modelName: model.id, modelName: model.id,
configuration: { configuration: {
baseURL: API_URL, baseURL: API_URL,

View File

@@ -31,7 +31,7 @@ export const loadDeepseekChatModels = async () => {
chatModels[model.key] = { chatModels[model.key] = {
displayName: model.displayName, displayName: model.displayName,
model: new ChatOpenAI({ model: new ChatOpenAI({
apiKey: deepseekApiKey, openAIApiKey: deepseekApiKey,
modelName: model.key, modelName: model.key,
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -29,15 +29,12 @@ export const loadGroqChatModels = async () => {
chatModels[model.id] = { chatModels[model.id] = {
displayName: model.id, displayName: model.id,
model: new ChatOpenAI({ model: new ChatOpenAI({
apiKey: groqApiKey, openAIApiKey: groqApiKey,
modelName: model.id, modelName: model.id,
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {
baseURL: 'https://api.groq.com/openai/v1', baseURL: 'https://api.groq.com/openai/v1',
}, },
metadata: {
'model-type': 'groq',
},
}) as unknown as BaseChatModel, }) as unknown as BaseChatModel,
}; };
}); });

View File

@@ -118,7 +118,7 @@ export const getAvailableChatModelProviders = async () => {
[customOpenAiModelName]: { [customOpenAiModelName]: {
displayName: customOpenAiModelName, displayName: customOpenAiModelName,
model: new ChatOpenAI({ model: new ChatOpenAI({
apiKey: customOpenAiApiKey, openAIApiKey: customOpenAiApiKey,
modelName: customOpenAiModelName, modelName: customOpenAiModelName,
temperature: 0.7, temperature: 0.7,
configuration: { configuration: {

View File

@@ -47,7 +47,7 @@ export const loadLMStudioChatModels = async () => {
chatModels[model.id] = { chatModels[model.id] = {
displayName: model.name || model.id, displayName: model.name || model.id,
model: new ChatOpenAI({ model: new ChatOpenAI({
apiKey: 'lm-studio', openAIApiKey: 'lm-studio',
configuration: { configuration: {
baseURL: ensureV1Endpoint(endpoint), baseURL: ensureV1Endpoint(endpoint),
}, },
@@ -83,7 +83,7 @@ export const loadLMStudioEmbeddingsModels = async () => {
embeddingsModels[model.id] = { embeddingsModels[model.id] = {
displayName: model.name || model.id, displayName: model.name || model.id,
model: new OpenAIEmbeddings({ model: new OpenAIEmbeddings({
apiKey: 'lm-studio', openAIApiKey: 'lm-studio',
configuration: { configuration: {
baseURL: ensureV1Endpoint(endpoint), baseURL: ensureV1Endpoint(endpoint),
}, },

View File

@@ -6,8 +6,8 @@ export const PROVIDER_INFO = {
key: 'ollama', key: 'ollama',
displayName: 'Ollama', displayName: 'Ollama',
}; };
import { ChatOllama } from '@langchain/ollama'; import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { OllamaEmbeddings } from '@langchain/ollama'; import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
export const loadOllamaChatModels = async () => { export const loadOllamaChatModels = async () => {
const ollamaApiEndpoint = getOllamaApiEndpoint(); const ollamaApiEndpoint = getOllamaApiEndpoint();

View File

@@ -67,7 +67,7 @@ export const loadOpenAIChatModels = async () => {
chatModels[model.key] = { chatModels[model.key] = {
displayName: model.displayName, displayName: model.displayName,
model: new ChatOpenAI({ model: new ChatOpenAI({
apiKey: openaiApiKey, openAIApiKey: openaiApiKey,
modelName: model.key, modelName: model.key,
temperature: 0.7, temperature: 0.7,
}) as unknown as BaseChatModel, }) as unknown as BaseChatModel,
@@ -93,7 +93,7 @@ export const loadOpenAIEmbeddingModels = async () => {
embeddingModels[model.key] = { embeddingModels[model.key] = {
displayName: model.displayName, displayName: model.displayName,
model: new OpenAIEmbeddings({ model: new OpenAIEmbeddings({
apiKey: openaiApiKey, openAIApiKey: openaiApiKey,
modelName: model.key, modelName: model.key,
}) as unknown as Embeddings, }) as unknown as Embeddings,
}; };

View File

@@ -24,7 +24,6 @@ import computeSimilarity from '../utils/computeSimilarity';
import formatChatHistoryAsString from '../utils/formatHistory'; import formatChatHistoryAsString from '../utils/formatHistory';
import eventEmitter from 'events'; import eventEmitter from 'events';
import { StreamEvent } from '@langchain/core/tracers/log_stream'; import { StreamEvent } from '@langchain/core/tracers/log_stream';
import { z } from 'zod';
export interface MetaSearchAgentType { export interface MetaSearchAgentType {
searchAndAnswer: ( searchAndAnswer: (
@@ -53,17 +52,6 @@ type BasicChainInput = {
query: string; 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 { class MetaSearchAgent implements MetaSearchAgentType {
private config: Config; private config: Config;
private strParser = new StringOutputParser(); private strParser = new StringOutputParser();
@@ -74,71 +62,73 @@ class MetaSearchAgent implements MetaSearchAgentType {
private async createSearchRetrieverChain(llm: BaseChatModel) { private async createSearchRetrieverChain(llm: BaseChatModel) {
(llm as unknown as ChatOpenAI).temperature = 0; (llm as unknown as ChatOpenAI).temperature = 0;
return RunnableSequence.from([ return RunnableSequence.from([
PromptTemplate.fromTemplate(this.config.queryGeneratorPrompt), PromptTemplate.fromTemplate(this.config.queryGeneratorPrompt),
Object.assign( llm,
Object.create(Object.getPrototypeOf(llm)), this.strParser,
llm, RunnableLambda.from(async (input: string) => {
).withStructuredOutput(retrieverLLMOutputSchema, { const linksOutputParser = new LineListOutputParser({
...(llm.metadata?.['model-type'] === 'groq' key: 'links',
? { });
method: 'json-object',
}
: {}),
}),
RunnableLambda.from(
async (input: z.infer<typeof retrieverLLMOutputSchema>) => {
let question = input.query;
const links = input.links;
if (!input.searchRequired) { const questionOutputParser = new LineOutputParser({
return { query: '', docs: [] }; 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) { let docs: Document[] = [];
if (question.length === 0) {
question = 'summarize'; 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[] = []; await Promise.all(
docGroups.map(async (doc) => {
linkDocs.map((doc) => { const res = await llm.invoke(`
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(`
You are a web search summarizer, tasked with summarizing a piece of text retrieved from a web search. Your job is to summarize the 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. 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. 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. Make sure to answer the query in the summary.
`); `);
const document = new Document({ const document = new Document({
pageContent: res.content as string, pageContent: res.content as string,
metadata: { metadata: {
title: doc.metadata.title, title: doc.metadata.title,
url: doc.metadata.url, url: doc.metadata.url,
}, },
}); });
docs.push(document); docs.push(document);
}),
);
return { query: question, docs: docs };
} else {
question = question.replace(/<think>.*?<\/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 }; return { query: question, docs: documents };
} else { }
question = question.replace(/<think>.*?<\/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 };
}
},
),
]); ]);
} }

676
yarn.lock

File diff suppressed because it is too large Load Diff