Skip to content

Commit cc56ad8

Browse files
fixes
1 parent 099a863 commit cc56ad8

File tree

7 files changed

+142
-87
lines changed

7 files changed

+142
-87
lines changed

packages/web/src/app/api/(server)/chat/blocking/route.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { sew } from "@/actions";
2-
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, updateChatMessages, generateAndUpdateChatNameFromMessage } from "@/features/chat/actions";
2+
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, _updateChatMessages, generateAndUpdateChatNameFromMessage, _generateChatNameFromMessage } from "@/features/chat/actions";
33
import { LanguageModelInfo, languageModelInfoSchema, SBChatMessage, SearchScope } from "@/features/chat/types";
44
import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils";
55
import { ErrorCode } from "@/lib/errorCodes";
@@ -15,6 +15,7 @@ import { z } from "zod";
1515
import { createMessageStream } from "../route";
1616
import { InferUIMessageChunk, UITools, UIDataTypes, UIMessage } from "ai";
1717
import { apiHandler } from "@/lib/apiHandler";
18+
import { captureEvent } from "@/lib/posthog";
1819

1920
const logger = createLogger('chat-blocking-api');
2021

@@ -115,6 +116,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
115116
},
116117
});
117118

119+
await captureEvent('wa_chat_thread_created', {
120+
chatId: chat.id,
121+
isAnonymous: !user,
122+
});
123+
118124
// Run the agent to completion
119125
logger.debug(`Starting blocking agent for chat ${chat.id}`, {
120126
chatId: chat.id,
@@ -155,7 +161,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
155161
// We'll capture the final messages and usage from the stream
156162
let finalMessages: SBChatMessage[] = [];
157163

164+
await captureEvent('wa_chat_message_sent', {
165+
chatId: chat.id,
166+
messageCount: 1,
167+
});
168+
158169
const stream = await createMessageStream({
170+
chatId: chat.id,
159171
messages: [userMessage],
160172
selectedSearchScopes,
161173
model,
@@ -180,21 +192,28 @@ export const POST = apiHandler(async (request: NextRequest) => {
180192
},
181193
})
182194

183-
await Promise.all([
195+
const [_, name] = await Promise.all([
184196
// Consume the stream fully to trigger onFinish
185197
blockStreamUntilFinish(stream),
186198
// Generate and update the chat name
187-
generateAndUpdateChatNameFromMessage({
188-
chatId: chat.id,
189-
languageModelId: languageModelConfig.model,
199+
_generateChatNameFromMessage({
190200
message: query,
201+
languageModelConfig,
191202
})
192203
]);
193204

194205
// Persist the messages to the chat
195-
await updateChatMessages({
196-
chatId: chat.id,
197-
messages: finalMessages,
206+
await _updateChatMessages({ chatId: chat.id, messages: finalMessages, prisma });
207+
208+
// Update the chat name
209+
await prisma.chat.update({
210+
where: {
211+
id: chat.id,
212+
orgId: org.id,
213+
},
214+
data: {
215+
name: name,
216+
},
198217
});
199218

200219
// Extract the answer text from the assistant message

packages/web/src/app/api/(server)/chat/route.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { sew } from "@/actions";
2-
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, updateChatMessages, _isOwnerOfChat } from "@/features/chat/actions";
2+
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, _updateChatMessages, _isOwnerOfChat } from "@/features/chat/actions";
33
import { createAgentStream } from "@/features/chat/agent";
44
import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types";
55
import { getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils";
@@ -12,6 +12,7 @@ import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider";
1212
import * as Sentry from "@sentry/nextjs";
1313
import { PrismaClient } from "@sourcebot/db";
1414
import { createLogger } from "@sourcebot/shared";
15+
import { captureEvent } from "@/lib/posthog";
1516
import {
1617
createUIMessageStream,
1718
createUIMessageStreamResponse,
@@ -88,7 +89,13 @@ export const POST = apiHandler(async (req: NextRequest) => {
8889

8990
const { model, providerOptions } = await _getAISDKLanguageModelAndOptions(languageModelConfig);
9091

92+
await captureEvent('wa_chat_message_sent', {
93+
chatId: id,
94+
messageCount: messages.length,
95+
});
96+
9197
const stream = await createMessageStream({
98+
chatId: id,
9299
messages,
93100
selectedSearchScopes,
94101
model,
@@ -97,10 +104,7 @@ export const POST = apiHandler(async (req: NextRequest) => {
97104
orgId: org.id,
98105
prisma,
99106
onFinish: async ({ messages }) => {
100-
await updateChatMessages({
101-
chatId: id,
102-
messages
103-
});
107+
await _updateChatMessages({ chatId: id, messages, prisma });
104108
},
105109
onError: (error: unknown) => {
106110
logger.error(error);
@@ -146,6 +150,7 @@ const mergeStreamAsync = async (stream: StreamTextResult<any, any>, writer: UIMe
146150
}
147151

148152
interface CreateMessageStreamResponseProps {
153+
chatId: string;
149154
messages: SBChatMessage[];
150155
selectedSearchScopes: SearchScope[];
151156
model: AISDKLanguageModelV2;
@@ -158,6 +163,7 @@ interface CreateMessageStreamResponseProps {
158163
}
159164

160165
export const createMessageStream = async ({
166+
chatId,
161167
messages,
162168
selectedSearchScopes,
163169
model,
@@ -242,6 +248,7 @@ export const createMessageStream = async ({
242248
});
243249
},
244250
traceId,
251+
chatId,
245252
});
246253

247254
await mergeStreamAsync(researchStream, writer, {

packages/web/src/features/chat/actions.ts

Lines changed: 96 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const _isOwnerOfChat = async (chat: Chat, user: User | undefined): Promis
6262
/**
6363
* Checks if a user has been explicitly shared access to a chat.
6464
*/
65-
export const _hasSharedAccess = async ({prisma, chatId, userId}: {prisma: PrismaClient, chatId: string, userId: string | undefined}): Promise<boolean> => {
65+
export const _hasSharedAccess = async ({ prisma, chatId, userId }: { prisma: PrismaClient, chatId: string, userId: string | undefined }): Promise<boolean> => {
6666
if (!userId) {
6767
return false;
6868
}
@@ -79,6 +79,55 @@ export const _hasSharedAccess = async ({prisma, chatId, userId}: {prisma: Prisma
7979
return share !== null;
8080
};
8181

82+
export const _updateChatMessages = async ({ chatId, messages, prisma }: { chatId: string, messages: SBChatMessage[], prisma: PrismaClient }) => {
83+
await prisma.chat.update({
84+
where: {
85+
id: chatId,
86+
},
87+
data: {
88+
messages: messages as unknown as Prisma.InputJsonValue,
89+
},
90+
});
91+
92+
if (env.DEBUG_WRITE_CHAT_MESSAGES_TO_FILE) {
93+
const chatDir = path.join(env.DATA_CACHE_DIR, 'chats');
94+
if (!fs.existsSync(chatDir)) {
95+
fs.mkdirSync(chatDir, { recursive: true });
96+
}
97+
98+
const chatFile = path.join(chatDir, `${chatId}.json`);
99+
fs.writeFileSync(chatFile, JSON.stringify(messages, null, 2));
100+
}
101+
};
102+
103+
104+
export const _generateChatNameFromMessage = async ({ message, languageModelConfig }: { message: string, languageModelConfig: LanguageModel }) => {
105+
const { model } = await _getAISDKLanguageModelAndOptions(languageModelConfig);
106+
107+
const prompt = `Convert this question into a short topic title (max 50 characters).
108+
109+
Rules:
110+
- Do NOT include question words (what, where, how, why, when, which)
111+
- Do NOT end with a question mark
112+
- Capitalize the first letter of the title
113+
- Focus on the subject/topic being discussed
114+
- Make it sound like a file name or category
115+
116+
Examples:
117+
"Where is the authentication code?" → "Authentication Code"
118+
"How to setup the database?" → "Database Setup"
119+
"What are the API endpoints?" → "API Endpoints"
120+
121+
User question: ${message}`;
122+
123+
const result = await generateText({
124+
model,
125+
prompt,
126+
});
127+
128+
return result.text;
129+
}
130+
82131
export const createChat = async () => sew(() =>
83132
withOptionalAuthV2(async ({ org, user, prisma }) => {
84133
const isGuestUser = user === undefined;
@@ -112,6 +161,11 @@ export const createChat = async () => sew(() =>
112161
});
113162
}
114163

164+
await captureEvent('wa_chat_thread_created', {
165+
chatId: chat.id,
166+
isAnonymous: isGuestUser,
167+
});
168+
115169
return {
116170
id: chat.id,
117171
isAnonymous: isGuestUser,
@@ -133,7 +187,7 @@ export const getChatInfo = async ({ chatId }: { chatId: string }) => sew(() =>
133187
}
134188

135189
const isOwner = await _isOwnerOfChat(chat, user);
136-
const isSharedWithUser = await _hasSharedAccess({prisma, chatId, userId: user?.id});
190+
const isSharedWithUser = await _hasSharedAccess({ prisma, chatId, userId: user?.id });
137191

138192
// Private chats can only be viewed by the owner or users it's been shared with
139193
if (chat.visibility === ChatVisibility.PRIVATE && !isOwner && !isSharedWithUser) {
@@ -170,24 +224,7 @@ export const updateChatMessages = async ({ chatId, messages }: { chatId: string,
170224
return notFound();
171225
}
172226

173-
await prisma.chat.update({
174-
where: {
175-
id: chatId,
176-
},
177-
data: {
178-
messages: messages as unknown as Prisma.InputJsonValue,
179-
},
180-
});
181-
182-
if (env.DEBUG_WRITE_CHAT_MESSAGES_TO_FILE) {
183-
const chatDir = path.join(env.DATA_CACHE_DIR, 'chats');
184-
if (!fs.existsSync(chatDir)) {
185-
fs.mkdirSync(chatDir, { recursive: true });
186-
}
187-
188-
const chatFile = path.join(chatDir, `${chatId}.json`);
189-
fs.writeFileSync(chatFile, JSON.stringify(messages, null, 2));
190-
}
227+
await _updateChatMessages({ chatId, messages, prisma });
191228

192229
return {
193230
success: true,
@@ -295,54 +332,51 @@ export const updateChatVisibility = async ({ chatId, visibility }: { chatId: str
295332
);
296333

297334
export const generateAndUpdateChatNameFromMessage = async ({ chatId, languageModelId, message }: { chatId: string, languageModelId: string, message: string }) => sew(() =>
298-
withOptionalAuthV2(async () => {
299-
// From the language model ID, attempt to find the
300-
// corresponding config in `config.json`.
301-
const languageModelConfig =
302-
(await _getConfiguredLanguageModelsFull())
303-
.find((model) => model.model === languageModelId);
304-
305-
if (!languageModelConfig) {
306-
return serviceErrorResponse({
307-
statusCode: StatusCodes.BAD_REQUEST,
308-
errorCode: ErrorCode.INVALID_REQUEST_BODY,
309-
message: `Language model ${languageModelId} is not configured.`,
310-
});
311-
}
312-
313-
const { model } = await _getAISDKLanguageModelAndOptions(languageModelConfig);
314-
315-
const prompt = `Convert this question into a short topic title (max 50 characters).
335+
withOptionalAuthV2(async ({ prisma, user, org }) => {
336+
const chat = await prisma.chat.findUnique({
337+
where: {
338+
id: chatId,
339+
},
340+
});
316341

317-
Rules:
318-
- Do NOT include question words (what, where, how, why, when, which)
319-
- Do NOT end with a question mark
320-
- Capitalize the first letter of the title
321-
- Focus on the subject/topic being discussed
322-
- Make it sound like a file name or category
342+
if (!chat) {
343+
return notFound();
344+
}
323345

324-
Examples:
325-
"Where is the authentication code?" → "Authentication Code"
326-
"How to setup the database?" → "Database Setup"
327-
"What are the API endpoints?" → "API Endpoints"
346+
const isOwner = await _isOwnerOfChat(chat, user);
347+
if (!isOwner) {
348+
return notFound();
349+
}
328350

329-
User question: ${message}`;
351+
const languageModelConfig =
352+
(await _getConfiguredLanguageModelsFull())
353+
.find((model) => model.model === languageModelId);
330354

331-
const result = await generateText({
332-
model,
333-
prompt,
355+
if (!languageModelConfig) {
356+
return serviceErrorResponse({
357+
statusCode: StatusCodes.BAD_REQUEST,
358+
errorCode: ErrorCode.INVALID_REQUEST_BODY,
359+
message: `Language model ${languageModelId} is not configured.`,
334360
});
361+
}
335362

336-
await updateChatName({
337-
chatId,
338-
name: result.text,
339-
});
363+
const name = await _generateChatNameFromMessage({ message, languageModelConfig });
340364

341-
return {
342-
success: true,
343-
}
365+
await prisma.chat.update({
366+
where: {
367+
id: chatId,
368+
orgId: org.id,
369+
},
370+
data: {
371+
name: name,
372+
},
344373
})
345-
)
374+
375+
return {
376+
success: true,
377+
}
378+
})
379+
)
346380

347381
export const deleteChat = async ({ chatId }: { chatId: string }) => sew(() =>
348382
withAuthV2(async ({ org, user, prisma }) => {
@@ -436,7 +470,7 @@ export const duplicateChat = async ({ chatId, newName }: { chatId: string, newNa
436470

437471
// Check if user can access the chat (owner, shared, or public)
438472
const isOwner = await _isOwnerOfChat(originalChat, user);
439-
const isSharedWithUser = await _hasSharedAccess({prisma, chatId, userId: user?.id});
473+
const isSharedWithUser = await _hasSharedAccess({ prisma, chatId, userId: user?.id });
440474
if (originalChat.visibility === ChatVisibility.PRIVATE && !isOwner && !isSharedWithUser) {
441475
return notFound();
442476
}
@@ -617,7 +651,7 @@ export const submitFeedback = async ({
617651
}
618652

619653
// When a chat is private, only the creator or shared users can submit feedback.
620-
const isSharedWithUser = await _hasSharedAccess({prisma, chatId, userId: user?.id});
654+
const isSharedWithUser = await _hasSharedAccess({ prisma, chatId, userId: user?.id });
621655
if (chat.visibility === ChatVisibility.PRIVATE && chat.createdById !== user?.id && !isSharedWithUser) {
622656
return notFound();
623657
}

packages/web/src/features/chat/agent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface AgentOptions {
2222
inputSources: Source[];
2323
onWriteSource: (source: Source) => void;
2424
traceId: string;
25+
chatId: string;
2526
}
2627

2728
export const createAgentStream = async ({
@@ -32,6 +33,7 @@ export const createAgentStream = async ({
3233
selectedRepos,
3334
onWriteSource,
3435
traceId,
36+
chatId,
3537
}: AgentOptions) => {
3638
// For every file source, resolve the source code so that we can include it in the system prompt.
3739
const fileSources = inputSources.filter((source) => source.type === 'file');
@@ -84,6 +86,7 @@ export const createAgentStream = async ({
8486
onStepFinish: ({ toolResults }) => {
8587
toolResults.forEach(({ toolName, output, dynamic }) => {
8688
captureEvent('wa_chat_tool_used', {
89+
chatId,
8790
toolName,
8891
success: !isServiceError(output),
8992
});

0 commit comments

Comments
 (0)