fix: Fix chat title generation. Also improve how errors are reported

This commit is contained in:
bkellam 2025-07-24 12:05:39 -07:00
parent d46615c4b2
commit e150310c98
3 changed files with 89 additions and 96 deletions

View file

@ -59,6 +59,11 @@ export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> =>
} catch (e) { } catch (e) {
Sentry.captureException(e); Sentry.captureException(e);
logger.error(e); logger.error(e);
if (e instanceof Error) {
return unexpectedError(e.message);
}
return unexpectedError(`An unexpected error occurred. Please try again later.`); return unexpectedError(`An unexpected error occurred. Please try again later.`);
} }
} }

View file

@ -10,21 +10,21 @@ import { isServiceError } from "@/lib/utils";
import { prisma } from "@/prisma"; import { prisma } from "@/prisma";
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
import { AnthropicProviderOptions, createAnthropic } from '@ai-sdk/anthropic'; import { AnthropicProviderOptions, createAnthropic } from '@ai-sdk/anthropic';
import { createAzure } from '@ai-sdk/azure';
import { createDeepSeek } from '@ai-sdk/deepseek';
import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createVertex } from '@ai-sdk/google-vertex'; import { createVertex } from '@ai-sdk/google-vertex';
import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic'; import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic';
import { createOpenAI, OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
import { createMistral } from '@ai-sdk/mistral'; import { createMistral } from '@ai-sdk/mistral';
import { createXai } from '@ai-sdk/xai'; import { createOpenAI, OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider"; import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider";
import { createXai } from '@ai-sdk/xai';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import * as Sentry from "@sentry/nextjs"; import * as Sentry from "@sentry/nextjs";
import { getTokenFromConfig } from "@sourcebot/crypto"; import { getTokenFromConfig } from "@sourcebot/crypto";
import { OrgRole } from "@sourcebot/db"; import { OrgRole } from "@sourcebot/db";
import { createLogger } from "@sourcebot/logger"; import { createLogger } from "@sourcebot/logger";
import { LanguageModel } from "@sourcebot/schemas/v3/index.type"; import { LanguageModel } from "@sourcebot/schemas/v3/index.type";
import { createAzure } from '@ai-sdk/azure';
import { createDeepSeek } from '@ai-sdk/deepseek';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { import {
createUIMessageStream, createUIMessageStream,
createUIMessageStreamResponse, createUIMessageStreamResponse,
@ -139,7 +139,6 @@ const chatHandler = ({ messages, id, selectedRepos, languageModelId }: ChatHandl
const { model, providerOptions, headers } = await getAISDKLanguageModelAndOptions(languageModelConfig, org.id); const { model, providerOptions, headers } = await getAISDKLanguageModelAndOptions(languageModelConfig, org.id);
// @todo: refactor this
if ( if (
messages.length === 1 && messages.length === 1 &&
messages[0].role === "user" && messages[0].role === "user" &&
@ -149,15 +148,10 @@ const chatHandler = ({ messages, id, selectedRepos, languageModelId }: ChatHandl
const content = messages[0].parts[0].text; const content = messages[0].parts[0].text;
const title = await generateChatTitle(content, model); const title = await generateChatTitle(content, model);
if (title) { await updateChatName({
updateChatName({ chatId: id,
chatId: id, name: title,
name: title, }, domain);
}, domain);
}
else {
logger.error("Failed to generate chat title.");
}
} }
const traceId = randomUUID(); const traceId = randomUUID();
@ -184,86 +178,73 @@ const chatHandler = ({ messages, id, selectedRepos, languageModelId }: ChatHandl
} }
}).filter(message => message !== undefined); }).filter(message => message !== undefined);
try { const stream = createUIMessageStream<SBChatMessage>({
const stream = createUIMessageStream<SBChatMessage>({ execute: async ({ writer }) => {
execute: async ({ writer }) => { writer.write({
writer.write({ type: 'start',
type: 'start', });
});
const startTime = new Date(); const startTime = new Date();
const researchStream = await createAgentStream({ const researchStream = await createAgentStream({
model, model,
providerOptions, providerOptions,
headers, headers,
inputMessages: messageHistory, inputMessages: messageHistory,
inputSources: sources, inputSources: sources,
selectedRepos, selectedRepos,
onWriteSource: (source) => { onWriteSource: (source) => {
writer.write({ writer.write({
type: 'data-source', type: 'data-source',
data: source, data: source,
}); });
}, },
traceId,
});
await mergeStreamAsync(researchStream, writer, {
sendReasoning: true,
sendStart: false,
sendFinish: false,
});
const totalUsage = await researchStream.totalUsage;
writer.write({
type: 'message-metadata',
messageMetadata: {
totalTokens: totalUsage.totalTokens,
totalInputTokens: totalUsage.inputTokens,
totalOutputTokens: totalUsage.outputTokens,
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
traceId, traceId,
}); }
})
await mergeStreamAsync(researchStream, writer, {
sendReasoning: true,
sendStart: false,
sendFinish: false,
});
const totalUsage = await researchStream.totalUsage;
writer.write({
type: 'message-metadata',
messageMetadata: {
totalTokens: totalUsage.totalTokens,
totalInputTokens: totalUsage.inputTokens,
totalOutputTokens: totalUsage.outputTokens,
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
traceId,
}
})
writer.write({ writer.write({
type: 'finish', type: 'finish',
}); });
}, },
onError: errorHandler, onError: errorHandler,
originalMessages: messages, originalMessages: messages,
onFinish: async ({ messages }) => { onFinish: async ({ messages }) => {
await updateChatMessages({ await updateChatMessages({
chatId: id, chatId: id,
messages messages
}, domain); }, domain);
}, },
}); });
return createUIMessageStreamResponse({ return createUIMessageStreamResponse({
stream, stream,
}); });
} catch (error) {
logger.error(error)
logger.error("Error stack:", error instanceof Error ? error.stack : "No stack trace")
Sentry.captureException(error);
return serviceErrorResponse({
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.UNEXPECTED_ERROR,
message: error instanceof Error ? error.message : "Unknown error",
});
}
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true }, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
)); ));
const generateChatTitle = async (message: string, model: AISDKLanguageModelV2) => { const generateChatTitle = async (message: string, model: AISDKLanguageModelV2) => {
try { const prompt = `Convert this question into a short topic title (max 50 characters).
const prompt = `Convert this question into a short topic title (max 50 characters).
Rules: Rules:
- Do NOT include question words (what, where, how, why, when, which) - Do NOT include question words (what, where, how, why, when, which)
@ -279,17 +260,12 @@ Examples:
User question: ${message}`; User question: ${message}`;
const result = await generateText({ const result = await generateText({
model, model,
prompt, prompt,
maxOutputTokens: 20, });
});
return result.text; return result.text;
} catch (error) {
logger.error("Error generating summary:", error)
return undefined;
}
} }
const getAISDKLanguageModelAndOptions = async (config: LanguageModel, orgId: number): Promise<{ const getAISDKLanguageModelAndOptions = async (config: LanguageModel, orgId: number): Promise<{

View file

@ -1,7 +1,9 @@
'use client'; 'use client';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { serviceErrorSchema } from '@/lib/serviceError';
import { AlertCircle, X } from "lucide-react"; import { AlertCircle, X } from "lucide-react";
import { useMemo } from 'react';
interface ErrorBannerProps { interface ErrorBannerProps {
error: Error; error: Error;
@ -14,6 +16,16 @@ export const ErrorBanner = ({ error, isVisible, onClose }: ErrorBannerProps) =>
return null; return null;
} }
const errorMessage = useMemo(() => {
try {
const errorJson = JSON.parse(error.message);
const serviceError = serviceErrorSchema.parse(errorJson);
return serviceError.message;
} catch {
return error.message;
}
}, [error]);
return ( return (
<div className="bg-red-50 border-b border-red-200 dark:bg-red-950/20 dark:border-red-800"> <div className="bg-red-50 border-b border-red-200 dark:bg-red-950/20 dark:border-red-800">
<div className="max-w-5xl mx-auto px-4 py-3"> <div className="max-w-5xl mx-auto px-4 py-3">
@ -24,7 +36,7 @@ export const ErrorBanner = ({ error, isVisible, onClose }: ErrorBannerProps) =>
Error occurred Error occurred
</span> </span>
<span className="text-sm text-red-600 dark:text-red-400"> <span className="text-sm text-red-600 dark:text-red-400">
{error.message || "An unexpected error occurred. Please try again."} {errorMessage}
</span> </span>
</div> </div>
<Button <Button