mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
fix: Fix chat title generation. Also improve how errors are reported
This commit is contained in:
parent
d46615c4b2
commit
e150310c98
3 changed files with 89 additions and 96 deletions
|
|
@ -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.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue