Compare commits

...

10 Commits

Author SHA1 Message Date
ItzCrazyKns
6f9eb3b1f3 feat(package): add langgraph 2025-10-05 22:24:06 +05:30
ItzCrazyKns
f18e132d1b feat(news-widget): fix loading animation 2025-10-02 17:55:56 +05:30
ItzCrazyKns
37317992b4 feat(app): lint & beautify 2025-10-02 17:17:52 +05:30
ItzCrazyKns
8b588824f2 feat(css): add line-clamp-2 class 2025-10-02 17:17:44 +05:30
ItzCrazyKns
a19cf00873 feat(lineOutputParser): return undefined on invalid tags 2025-10-02 17:17:31 +05:30
ItzCrazyKns
5cc11ac0bf feat(message-handling): fix repeated first token, think tag handling
closes #889
2025-10-02 17:15:54 +05:30
Kushagra Srivastava
d611ddaab9 Merge pull request #880 from ruturaj-rathod/fix/body-validation-579
validate the request body of api/chat to prevent malformed request body
2025-09-27 19:52:14 +05:30
ItzCrazyKns
3b41905abb feat(app): lint & beautify 2025-09-27 19:51:36 +05:30
ruturaj
fabb48cc2f move schema validation into route file and remove validation file 2025-09-23 14:57:55 +05:30
ruturaj
c46b421219 validate the request body of api/chat to prevent malformed request body 2025-09-19 16:06:46 +05:30
9 changed files with 144 additions and 57 deletions

View File

@@ -20,6 +20,7 @@
"@langchain/core": "^0.3.66", "@langchain/core": "^0.3.66",
"@langchain/google-genai": "^0.2.15", "@langchain/google-genai": "^0.2.15",
"@langchain/groq": "^0.2.3", "@langchain/groq": "^0.2.3",
"@langchain/langgraph": "^0.4.9",
"@langchain/ollama": "^0.2.3", "@langchain/ollama": "^0.2.3",
"@langchain/openai": "^0.6.2", "@langchain/openai": "^0.6.2",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",

View File

@@ -17,35 +17,71 @@ import {
getCustomOpenaiModelName, getCustomOpenaiModelName,
} from '@/lib/config'; } from '@/lib/config';
import { searchHandlers } from '@/lib/search'; import { searchHandlers } from '@/lib/search';
import { z } from 'zod';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
type Message = { const messageSchema = z.object({
messageId: string; messageId: z.string().min(1, 'Message ID is required'),
chatId: string; chatId: z.string().min(1, 'Chat ID is required'),
content: string; content: z.string().min(1, 'Message content is required'),
}; });
type ChatModel = { const chatModelSchema = z.object({
provider: string; provider: z.string().optional(),
name: string; name: z.string().optional(),
}; });
type EmbeddingModel = { const embeddingModelSchema = z.object({
provider: string; provider: z.string().optional(),
name: string; name: z.string().optional(),
}; });
type Body = { const bodySchema = z.object({
message: Message; message: messageSchema,
optimizationMode: 'speed' | 'balanced' | 'quality'; optimizationMode: z.enum(['speed', 'balanced', 'quality'], {
focusMode: string; errorMap: () => ({
history: Array<[string, string]>; message: 'Optimization mode must be one of: speed, balanced, quality',
files: Array<string>; }),
chatModel: ChatModel; }),
embeddingModel: EmbeddingModel; focusMode: z.string().min(1, 'Focus mode is required'),
systemInstructions: string; history: z
.array(
z.tuple([z.string(), z.string()], {
errorMap: () => ({
message: 'History items must be tuples of two strings',
}),
}),
)
.optional()
.default([]),
files: z.array(z.string()).optional().default([]),
chatModel: chatModelSchema.optional().default({}),
embeddingModel: embeddingModelSchema.optional().default({}),
systemInstructions: z.string().nullable().optional().default(''),
});
type Message = z.infer<typeof messageSchema>;
type Body = z.infer<typeof bodySchema>;
const safeValidateBody = (data: unknown) => {
const result = bodySchema.safeParse(data);
if (!result.success) {
return {
success: false,
error: result.error.errors.map((e) => ({
path: e.path.join('.'),
message: e.message,
})),
};
}
return {
success: true,
data: result.data,
};
}; };
const handleEmitterEvents = async ( const handleEmitterEvents = async (
@@ -190,7 +226,17 @@ const handleHistorySave = async (
export const POST = async (req: Request) => { export const POST = async (req: Request) => {
try { try {
const body = (await req.json()) as Body; const reqBody = (await req.json()) as Body;
const parseBody = safeValidateBody(reqBody);
if (!parseBody.success) {
return Response.json(
{ message: 'Invalid request body', error: parseBody.error },
{ status: 400 },
);
}
const body = parseBody.data as Body;
const { message } = body; const { message } = body;
if (message.content === '') { if (message.content === '') {
@@ -285,7 +331,7 @@ export const POST = async (req: Request) => {
embedding, embedding,
body.optimizationMode, body.optimizationMode,
body.files, body.files,
body.systemInstructions, body.systemInstructions as string,
); );
const responseStream = new TransformStream(); const responseStream = new TransformStream();

View File

@@ -20,6 +20,15 @@
} }
} }
@layer utilities {
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
@media screen and (-webkit-min-device-pixel-ratio: 0) { @media screen and (-webkit-min-device-pixel-ratio: 0) {
select, select,
textarea, textarea,

View File

@@ -69,9 +69,7 @@ const MessageBox = ({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div <div className={'w-full pt-8 break-words'}>
className={'w-full pt-8 break-words'}
>
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> <h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
{section.userMessage.content} {section.userMessage.content}
</h2> </h2>

View File

@@ -29,15 +29,13 @@ const NewsArticleWidget = () => {
return ( return (
<div className="bg-light-secondary dark:bg-dark-secondary rounded-2xl border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/25 flex flex-row items-stretch w-full h-24 min-h-[96px] max-h-[96px] p-0 overflow-hidden"> <div className="bg-light-secondary dark:bg-dark-secondary rounded-2xl border border-light-200 dark:border-dark-200 shadow-sm shadow-light-200/10 dark:shadow-black/25 flex flex-row items-stretch w-full h-24 min-h-[96px] max-h-[96px] p-0 overflow-hidden">
{loading ? ( {loading ? (
<> <div className="animate-pulse flex flex-row items-stretch w-full h-full">
<div className="animate-pulse flex flex-row items-center w-full h-full"> <div className="w-24 min-w-24 max-w-24 h-full bg-light-200 dark:bg-dark-200" />
<div className="rounded-lg w-16 min-w-16 max-w-16 h-16 min-h-16 max-h-16 bg-light-200 dark:bg-dark-200 mr-3" /> <div className="flex flex-col justify-center flex-1 px-3 py-2 gap-2">
<div className="flex flex-col justify-center flex-1 h-full w-0 gap-2">
<div className="h-4 w-3/4 rounded bg-light-200 dark:bg-dark-200" /> <div className="h-4 w-3/4 rounded bg-light-200 dark:bg-dark-200" />
<div className="h-3 w-1/2 rounded bg-light-200 dark:bg-dark-200" /> <div className="h-3 w-1/2 rounded bg-light-200 dark:bg-dark-200" />
</div> </div>
</div> </div>
</>
) : error ? ( ) : error ? (
<div className="w-full text-xs text-red-400">Could not load news.</div> <div className="w-full text-xs text-red-400">Could not load news.</div>
) : article ? ( ) : article ? (

View File

@@ -399,6 +399,7 @@ export const ChatProvider = ({
(m, j) => (m, j) =>
j > i && j > i &&
m.role === 'source' && m.role === 'source' &&
m.sources &&
(nextUserMessageIndex === -1 || j < nextUserMessageIndex), (nextUserMessageIndex === -1 || j < nextUserMessageIndex),
) as SourceMessage | undefined; ) as SourceMessage | undefined;
@@ -417,7 +418,7 @@ export const ChatProvider = ({
const closeThinkTag = const closeThinkTag =
processedMessage.match(/<\/think>/g)?.length || 0; processedMessage.match(/<\/think>/g)?.length || 0;
if (openThinkTag > closeThinkTag) { if (openThinkTag && !closeThinkTag) {
processedMessage += '</think> <a> </a>'; processedMessage += '</think> <a> </a>';
} }
} }
@@ -426,7 +427,11 @@ export const ChatProvider = ({
thinkingEnded = true; thinkingEnded = true;
} }
if (sourceMessage && sourceMessage.sources.length > 0) { if (
sourceMessage &&
sourceMessage.sources &&
sourceMessage.sources.length > 0
) {
processedMessage = processedMessage.replace( processedMessage = processedMessage.replace(
citationRegex, citationRegex,
(_, capturedContent: string) => { (_, capturedContent: string) => {
@@ -635,8 +640,8 @@ export const ChatProvider = ({
}, },
]); ]);
added = true; added = true;
} setMessageAppeared(true);
} else {
setMessages((prev) => setMessages((prev) =>
prev.map((message) => { prev.map((message) => {
if ( if (
@@ -649,9 +654,8 @@ export const ChatProvider = ({
return message; return message;
}), }),
); );
}
recievedMessage += data.data; recievedMessage += data.data;
setMessageAppeared(true);
} }
if (data.type === 'messageEnd') { if (data.type === 'messageEnd') {

View File

@@ -4,7 +4,7 @@ interface LineOutputParserArgs {
key?: string; key?: string;
} }
class LineOutputParser extends BaseOutputParser<string> { class LineOutputParser extends BaseOutputParser<string | undefined> {
private key = 'questions'; private key = 'questions';
constructor(args?: LineOutputParserArgs) { constructor(args?: LineOutputParserArgs) {
@@ -18,7 +18,7 @@ class LineOutputParser extends BaseOutputParser<string> {
lc_namespace = ['langchain', 'output_parsers', 'line_output_parser']; lc_namespace = ['langchain', 'output_parsers', 'line_output_parser'];
async parse(text: string): Promise<string> { async parse(text: string): Promise<string | undefined> {
text = text.trim() || ''; text = text.trim() || '';
const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/; const regex = /^(\s*(-|\*|\d+\.\s|\d+\)\s|\u2022)\s*)+/;
@@ -26,7 +26,7 @@ class LineOutputParser extends BaseOutputParser<string> {
const endKeyIndex = text.indexOf(`</${this.key}>`); const endKeyIndex = text.indexOf(`</${this.key}>`);
if (startKeyIndex === -1 || endKeyIndex === -1) { if (startKeyIndex === -1 || endKeyIndex === -1) {
return ''; return undefined;
} }
const questionsStartIndex = const questionsStartIndex =

View File

@@ -453,7 +453,6 @@ class MetaSearchAgent implements MetaSearchAgentType {
event.event === 'on_chain_end' && event.event === 'on_chain_end' &&
event.name === 'FinalSourceRetriever' event.name === 'FinalSourceRetriever'
) { ) {
``;
emitter.emit( emitter.emit(
'data', 'data',
JSON.stringify({ type: 'sources', data: event.data.output }), JSON.stringify({ type: 'sources', data: event.data.output }),

View File

@@ -661,6 +661,33 @@
groq-sdk "^0.19.0" groq-sdk "^0.19.0"
zod "^3.22.4" zod "^3.22.4"
"@langchain/langgraph-checkpoint@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.1.1.tgz#500569a02af4b85172d775de63eeba06afa0c189"
integrity sha512-h2bP0RUikQZu0Um1ZUPErQLXyhzroJqKRbRcxYRTAh49oNlsfeq4A3K4YEDRbGGuyPZI/Jiqwhks1wZwY73AZw==
dependencies:
uuid "^10.0.0"
"@langchain/langgraph-sdk@~0.1.0":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-0.1.9.tgz#5442bd1a4257b5d94927af6e09b0aed341ae8a1d"
integrity sha512-7WEDHtbI3pYPUiiHq+dPaF92ZN2W7lqObdpK0X+roa8zPdHUjve/HiqYuKNWS12u1N+L5QIuQWqZvVNvUA7BfQ==
dependencies:
"@types/json-schema" "^7.0.15"
p-queue "^6.6.2"
p-retry "4"
uuid "^9.0.0"
"@langchain/langgraph@^0.4.9":
version "0.4.9"
resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.4.9.tgz#470a238ea98662d6ec9dfc42859a00acad00fc81"
integrity sha512-+rcdTGi4Ium4X/VtIX3Zw4RhxEkYWpwUyz806V6rffjHOAMamg6/WZDxpJbrP33RV/wJG1GH12Z29oX3Pqq3Aw==
dependencies:
"@langchain/langgraph-checkpoint" "^0.1.1"
"@langchain/langgraph-sdk" "~0.1.0"
uuid "^10.0.0"
zod "^3.25.32"
"@langchain/ollama@^0.2.3": "@langchain/ollama@^0.2.3":
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.3.tgz#4868e66db4fc480f08c42fc652274abbab0416f0" resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.3.tgz#4868e66db4fc480f08c42fc652274abbab0416f0"
@@ -939,6 +966,11 @@
resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-9.0.4.tgz#4a83dd8ae8bfa91457d0b1ffc26f4d0537eff58c" resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-9.0.4.tgz#4a83dd8ae8bfa91457d0b1ffc26f4d0537eff58c"
integrity sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ== integrity sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/json5@^0.0.29": "@types/json5@^0.0.29":
version "0.0.29" version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -5133,7 +5165,7 @@ uuid@^11.1.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
uuid@^9.0.1: uuid@^9.0.0, uuid@^9.0.1:
version "9.0.1" version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==