1- import { sew } from "@/actions" ;
2- import { _getConfiguredLanguageModelsFull , _getAISDKLanguageModelAndOptions , _updateChatMessages , _generateChatNameFromMessage } from "@/features/chat/actions" ;
3- import { LanguageModelInfo , languageModelInfoSchema , SBChatMessage , SearchScope } from "@/features/chat/types" ;
4- import { convertLLMOutputToPortableMarkdown , getAnswerPartFromAssistantMessage , getLanguageModelKey } from "@/features/chat/utils" ;
5- import { ErrorCode } from "@/lib/errorCodes" ;
6- import { requestBodySchemaValidationError , ServiceError , ServiceErrorException , serviceErrorResponse } from "@/lib/serviceError" ;
1+ import { askCodebase } from "@/features/mcp/askCodebase" ;
2+ import { languageModelInfoSchema } from "@/features/chat/types" ;
3+ import { apiHandler } from "@/lib/apiHandler" ;
4+ import { requestBodySchemaValidationError , serviceErrorResponse } from "@/lib/serviceError" ;
75import { isServiceError } from "@/lib/utils" ;
8- import { withOptionalAuthV2 } from "@/withAuthV2" ;
9- import { ChatVisibility , Prisma } from "@sourcebot/db" ;
10- import { createLogger , env } from "@sourcebot/shared" ;
11- import { randomUUID } from "crypto" ;
12- import { StatusCodes } from "http-status-codes" ;
6+ import { ChatVisibility } from "@sourcebot/db" ;
137import { NextRequest , NextResponse } from "next/server" ;
148import { z } from "zod" ;
15- import { createMessageStream } from "../route" ;
16- import { InferUIMessageChunk , UITools , UIDataTypes , UIMessage } from "ai" ;
17- import { apiHandler } from "@/lib/apiHandler" ;
18- import { captureEvent } from "@/lib/posthog" ;
19-
20- const logger = createLogger ( 'chat-blocking-api' ) ;
219
2210/**
2311 * Request schema for the blocking chat API.
@@ -40,22 +28,12 @@ const blockingChatRequestSchema = z.object({
4028 . describe ( "The visibility of the chat session. If not provided, defaults to PRIVATE for authenticated users and PUBLIC for anonymous users. Set to PUBLIC to make the chat viewable by anyone with the link. Note: Anonymous users cannot create PRIVATE chats; any PRIVATE request from an unauthenticated user will be ignored and set to PUBLIC." ) ,
4129} ) ;
4230
43- /**
44- * Response schema for the blocking chat API.
45- */
46- interface BlockingChatResponse {
47- answer : string ;
48- chatId : string ;
49- chatUrl : string ;
50- languageModel : LanguageModelInfo ;
51- }
52-
5331/**
5432 * POST /api/chat/blocking
55- *
33+ *
5634 * A blocking (non-streaming) chat endpoint designed for MCP and other integrations.
5735 * Creates a chat session, runs the agent to completion, and returns the final answer.
58- *
36+ *
5937 * The chat session is persisted to the database, allowing users to view the full
6038 * conversation (including tool calls and reasoning) in the web UI.
6139 */
@@ -67,202 +45,11 @@ export const POST = apiHandler(async (request: NextRequest) => {
6745 return serviceErrorResponse ( requestBodySchemaValidationError ( parsed . error ) ) ;
6846 }
6947
70- const { query, repos = [ ] , languageModel : requestedLanguageModel , visibility : requestedVisibility } = parsed . data ;
71-
72- const response : BlockingChatResponse | ServiceError = await sew ( ( ) =>
73- withOptionalAuthV2 ( async ( { org, user, prisma } ) => {
74- // Get all configured language models
75- const configuredModels = await _getConfiguredLanguageModelsFull ( ) ;
76- if ( configuredModels . length === 0 ) {
77- return {
78- statusCode : StatusCodes . BAD_REQUEST ,
79- errorCode : ErrorCode . INVALID_REQUEST_BODY ,
80- message : "No language models are configured. Please configure at least one language model. See: https://docs.sourcebot.dev/docs/configuration/language-model-providers" ,
81- } satisfies ServiceError ;
82- }
83-
84- // Use the requested language model if provided, otherwise default to the first configured model
85- let languageModelConfig = configuredModels [ 0 ] ;
86- if ( requestedLanguageModel ) {
87- const matchingModel = configuredModels . find (
88- ( m ) => getLanguageModelKey ( m ) === getLanguageModelKey ( requestedLanguageModel as LanguageModelInfo )
89- ) ;
90- if ( ! matchingModel ) {
91- return {
92- statusCode : StatusCodes . BAD_REQUEST ,
93- errorCode : ErrorCode . INVALID_REQUEST_BODY ,
94- message : `Language model '${ requestedLanguageModel . provider } /${ requestedLanguageModel . model } ' is not configured.` ,
95- } satisfies ServiceError ;
96- }
97- languageModelConfig = matchingModel ;
98- }
99-
100- const { model, providerOptions } = await _getAISDKLanguageModelAndOptions ( languageModelConfig ) ;
101- const modelName = languageModelConfig . displayName ?? languageModelConfig . model ;
102-
103- // Determine visibility: anonymous users cannot create private chats (they would be inaccessible)
104- // Only use requested visibility if user is authenticated, otherwise always use PUBLIC
105- const chatVisibility = ( requestedVisibility && user )
106- ? requestedVisibility
107- : ( user ? ChatVisibility . PRIVATE : ChatVisibility . PUBLIC ) ;
108-
109- // Create a new chat session
110- const chat = await prisma . chat . create ( {
111- data : {
112- orgId : org . id ,
113- createdById : user ?. id ,
114- visibility : chatVisibility ,
115- messages : [ ] as unknown as Prisma . InputJsonValue ,
116- } ,
117- } ) ;
118-
119- await captureEvent ( 'wa_chat_thread_created' , {
120- chatId : chat . id ,
121- isAnonymous : ! user ,
122- } ) ;
123-
124- // Run the agent to completion
125- logger . debug ( `Starting blocking agent for chat ${ chat . id } ` , {
126- chatId : chat . id ,
127- query : query . substring ( 0 , 100 ) ,
128- model : modelName ,
129- } ) ;
130-
131- // Create the initial user message
132- const userMessage : SBChatMessage = {
133- id : randomUUID ( ) ,
134- role : 'user' ,
135- parts : [ { type : 'text' , text : query } ] ,
136- } ;
137-
138- const selectedRepos = ( await Promise . all ( repos . map ( async ( repo ) => {
139- const repoDB = await prisma . repo . findFirst ( {
140- where : {
141- name : repo ,
142- } ,
143- } ) ;
144-
145- if ( ! repoDB ) {
146- throw new ServiceErrorException ( {
147- statusCode : StatusCodes . BAD_REQUEST ,
148- errorCode : ErrorCode . INVALID_REQUEST_BODY ,
149- message : `Repository '${ repo } ' not found.` ,
150- } )
151- }
152-
153- return {
154- type : 'repo' ,
155- value : repoDB . name ,
156- name : repoDB . displayName ?? repoDB . name . split ( '/' ) . pop ( ) ?? repoDB . name ,
157- codeHostType : repoDB . external_codeHostType ,
158- } satisfies SearchScope ;
159- } ) ) ) ;
160-
161- // We'll capture the final messages and usage from the stream
162- let finalMessages : SBChatMessage [ ] = [ ] ;
163-
164- await captureEvent ( 'wa_chat_message_sent' , {
165- chatId : chat . id ,
166- messageCount : 1 ,
167- selectedReposCount : selectedRepos . length ,
168- ...( env . EXPERIMENT_ASK_GH_ENABLED === 'true' ? {
169- selectedRepos : selectedRepos . map ( r => r . value )
170- } : { } ) ,
171- } ) ;
172-
173- const stream = await createMessageStream ( {
174- chatId : chat . id ,
175- messages : [ userMessage ] ,
176- metadata : {
177- selectedSearchScopes : selectedRepos ,
178- } ,
179- selectedRepos : selectedRepos . map ( r => r . value ) ,
180- model,
181- modelName,
182- modelProviderOptions : providerOptions ,
183- onFinish : async ( { messages } ) => {
184- finalMessages = messages ;
185- } ,
186- onError : ( error ) => {
187- if ( error instanceof ServiceErrorException ) {
188- throw error ;
189- }
190-
191- const message = error instanceof Error ? error . message : String ( error ) ;
192- throw new ServiceErrorException ( {
193- statusCode : StatusCodes . INTERNAL_SERVER_ERROR ,
194- errorCode : ErrorCode . UNEXPECTED_ERROR ,
195- message,
196- } ) ;
197- } ,
198- } )
199-
200- const [ _ , name ] = await Promise . all ( [
201- // Consume the stream fully to trigger onFinish
202- blockStreamUntilFinish ( stream ) ,
203- // Generate and update the chat name
204- _generateChatNameFromMessage ( {
205- message : query ,
206- languageModelConfig,
207- } )
208- ] ) ;
209-
210- // Persist the messages to the chat
211- await _updateChatMessages ( { chatId : chat . id , messages : finalMessages , prisma } ) ;
212-
213- // Update the chat name
214- await prisma . chat . update ( {
215- where : {
216- id : chat . id ,
217- orgId : org . id ,
218- } ,
219- data : {
220- name : name ,
221- } ,
222- } ) ;
223-
224- // Extract the answer text from the assistant message
225- const assistantMessage = finalMessages . find ( m => m . role === 'assistant' ) ;
226- const answerPart = assistantMessage
227- ? getAnswerPartFromAssistantMessage ( assistantMessage , false )
228- : undefined ;
229- const answerText = answerPart ?. text ?? '' ;
230-
231- // Build the base URL and chat URL
232- const baseUrl = env . AUTH_URL ;
233-
234- // Convert to portable markdown (replaces @file: references with markdown links)
235- const portableAnswer = convertLLMOutputToPortableMarkdown ( answerText , baseUrl ) ;
236- const chatUrl = `${ baseUrl } /${ org . domain } /chat/${ chat . id } ` ;
237-
238- logger . debug ( `Completed blocking agent for chat ${ chat . id } ` , {
239- chatId : chat . id ,
240- } ) ;
241-
242- return {
243- answer : portableAnswer ,
244- chatId : chat . id ,
245- chatUrl,
246- languageModel : {
247- provider : languageModelConfig . provider ,
248- model : languageModelConfig . model ,
249- displayName : languageModelConfig . displayName ,
250- } ,
251- } satisfies BlockingChatResponse ;
252- } )
253- ) ;
48+ const response = await askCodebase ( parsed . data ) ;
25449
25550 if ( isServiceError ( response ) ) {
25651 return serviceErrorResponse ( response ) ;
25752 }
25853
25954 return NextResponse . json ( response ) ;
26055} ) ;
261-
262- const blockStreamUntilFinish = async < T extends UIMessage < unknown , UIDataTypes , UITools > > ( stream : ReadableStream < InferUIMessageChunk < T > > ) => {
263- const reader = stream . getReader ( ) ;
264- while ( true as const ) {
265- const { done } = await reader . read ( ) ;
266- if ( done ) break ;
267- }
268- }
0 commit comments