@@ -5,8 +5,17 @@ import { cache } from "react";
55import { ChatOpenAI } from "@langchain/openai" ;
66import { PromptTemplate } from "@langchain/core/prompts" ;
77import { StringOutputParser } from "@langchain/core/output_parsers" ;
8- import { ultraProjectChatBotPrompt } from "../prompts/ReverseArchitecture" ;
8+ import { ultraProjectChatBotPrompt , projectChatBotBugPrompt , projectChatBotTaskPrompt , projectChatBotFeaturePrompt } from "../prompts/ReverseArchitecture" ;
9+ import { createToolCallingAgent , AgentExecutor } from "langchain/agents" ;
10+ import { ChatPromptTemplate , MessagesPlaceholder } from "@langchain/core/prompts" ;
11+ import { searchCodeTool , getFileContentTool } from "./github/gitTools" ;
12+ import { getInstallationToken } from "./githubAppAuth" ;
13+ import Parallel from "parallel-web" ;
14+ import { DynamicStructuredTool } from "langchain/tools" ;
15+ import { z } from "zod" ;
916const openaiKey = process . env . OPENAI_API_KEY ;
17+ const parallelApiKey = process . env . PARALLEL_API_KEY ;
18+
1019const llm = new ChatOpenAI ( {
1120 openAIApiKey : openaiKey ,
1221 model : "gpt-5-mini-2025-08-07"
@@ -19,6 +28,89 @@ const llm2 = new ChatOpenAI({
1928const tool = { "type" : "web_search_preview" }
2029const llmWithWeb = llm2 . bindTools ( [ tool ] )
2130
31+ // Output schema for createPactProjectChatBot
32+ const pactOutputSchema = {
33+ type : "object" ,
34+ description : "Structured output containing a short response and pact details" ,
35+ properties : {
36+ shortResponse : {
37+ type : "string" ,
38+ description : "A concise response under 200 words explaining the analysis and recommendations"
39+ } ,
40+ pact : {
41+ type : "object" ,
42+ description : "The pact details with title and body" ,
43+ properties : {
44+ title : {
45+ type : "string" ,
46+ description : "Clear, actionable pact title"
47+ } ,
48+ body : {
49+ type : "string" ,
50+ description : "Detailed pact description in markdown format with context, approach, acceptance criteria, and code references"
51+ }
52+ } ,
53+ required : [ "title" , "body" ]
54+ }
55+ } ,
56+ required : [ "shortResponse" , "pact" ]
57+ } ;
58+
59+ // Parallel Web Search Tool
60+ const createParallelWebSearchTool = ( ) => {
61+ const parallelClient = new Parallel ( { apiKey : parallelApiKey } ) ;
62+
63+ return new DynamicStructuredTool ( {
64+ name : "parallelWebSearch" ,
65+ description : "Search the web for documentation, best practices, or external context related to React/Next.js development. Useful for finding latest information, framework documentation, or solutions to common problems." ,
66+ schema : z . object ( {
67+ objective : z . string ( ) . describe ( "Clear objective describing what information you're looking for" ) ,
68+ searchQueries : z . array ( z . string ( ) ) . describe ( "Array of specific search queries to execute" ) ,
69+ maxResults : z . number ( ) . optional ( ) . default ( 10 ) . describe ( "Maximum number of results to return per query (default: 10)" )
70+ } ) ,
71+
72+ func : async ( input ) : Promise < string > => {
73+ const { objective, searchQueries, maxResults } = input as {
74+ objective : string ,
75+ searchQueries : string [ ] ,
76+ maxResults ?: number
77+ } ;
78+
79+ try {
80+ const searchResult = await parallelClient . beta . search ( {
81+ objective,
82+ search_queries : searchQueries ,
83+ max_results : maxResults || 10 ,
84+ max_chars_per_result : 5000
85+ } ) ;
86+
87+ if ( ! searchResult . results || searchResult . results . length === 0 ) {
88+ return `No web search results found for objective: "${ objective } "` ;
89+ }
90+
91+ // Format results for better readability
92+ const formattedResults = searchResult . results . map ( ( result , idx ) => {
93+ const resultAny = result as any ;
94+ return `Result ${ idx + 1 } :
95+ Title: ${ result . title || 'N/A' }
96+ URL: ${ result . url || 'N/A' }
97+ Content: ${ resultAny . content ? resultAny . content . substring ( 0 , 1000 ) : resultAny . text ? resultAny . text . substring ( 0 , 1000 ) : 'No content' }
98+ ---` ;
99+ } ) . join ( '\n\n' ) ;
100+
101+ return `Web Search Results for "${ objective } ":
102+
103+ Total results: ${ searchResult . results . length }
104+
105+ ${ formattedResults } `;
106+
107+ } catch ( error ) {
108+ return `Error performing web search: ${ error instanceof Error ? error . message : "Unknown error occurred" } ` ;
109+ }
110+ }
111+ } ) ;
112+ } ;
113+
22114export interface ProjectMessage {
23115 id : string ;
24116 type : 'user' | 'assistant' ;
@@ -95,11 +187,6 @@ export async function saveProjectArchitecture(
95187 beforeCommitHash ?: string ;
96188 }
97189) {
98- ;
99-
100-
101- ;
102-
103190 try {
104191 // First verify the project belongs to the user
105192 const project = await db . project . findUnique ( {
@@ -450,8 +537,230 @@ export async function projectChatBot( userInput: string, projectFramework: strin
450537 return response ;
451538}
452539
453- export async function createPactProjectChatBot ( ) {
540+ export async function createPactProjectChatBot (
541+ projectId : string ,
542+ userInput : string ,
543+ pactType : 'BUG' | 'TASK' | 'FEATURE' ,
544+ conversationHistory : any [ ]
545+ ) : Promise < { shortResponse : string ; pact : { title : string ; body : string } } | { error : string } > {
546+ try {
547+ // 1. Authentication & Authorization
548+ const { userId } = await auth ( ) ;
549+ if ( ! userId ) {
550+ return { error : 'Unauthorized' } ;
551+ }
552+
553+ // 2. Fetch Project Data
554+ const project = await db . project . findUnique ( {
555+ where : { id : projectId , userId : userId } ,
556+ select : {
557+ id : true ,
558+ name : true ,
559+ repoFullName : true ,
560+ githubInstallationId : true ,
561+ defaultBranch : true ,
562+ framework : true ,
563+ ProjectArchitecture : {
564+ orderBy : {
565+ createdAt : 'desc' ,
566+ } ,
567+ take : 1 ,
568+ } ,
569+ } ,
570+ } ) ;
571+
572+ if ( ! project ) {
573+ return { error : 'Project not found' } ;
574+ }
575+
576+ if ( ! project . repoFullName ) {
577+ return { error : 'Project repository not configured' } ;
578+ }
579+
580+ if ( ! project . githubInstallationId ) {
581+ return { error : 'GitHub installation not configured for this project' } ;
582+ }
583+
584+ // 3. Get GitHub Access Token
585+ const installationTokenResult = await getInstallationToken ( project . githubInstallationId ) ;
586+ const accessToken = installationTokenResult . token ;
587+
588+ // Extract owner and repo from repoFullName
589+ const [ owner , repo ] = project . repoFullName . split ( '/' ) ;
590+ if ( ! owner || ! repo ) {
591+ return { error : 'Invalid repository name format' } ;
592+ }
593+
594+ // Get latest architecture
595+ const latestArchitecture = project . ProjectArchitecture && project . ProjectArchitecture . length > 0
596+ ? project . ProjectArchitecture [ 0 ]
597+ : null ;
598+
599+ const projectArchitecture = latestArchitecture
600+ ? JSON . stringify ( {
601+ components : latestArchitecture . components ,
602+ rationale : latestArchitecture . architectureRationale
603+ } )
604+ : 'No architecture available' ;
605+
606+ // 4. Initialize Tools
607+ const parallelWebSearchTool = createParallelWebSearchTool ( ) ;
608+
609+ // We need to bind the GitHub tools with the specific parameters
610+ // Create bound versions of the tools
611+ const boundSearchCodeTool = new DynamicStructuredTool ( {
612+ name : "searchCode" ,
613+ description : searchCodeTool . description ,
614+ schema : z . object ( {
615+ query : z . string ( ) . describe ( "Search query (e.g., 'useEffect', '@nestjs/', 'function component')" ) ,
616+ language : z . string ( ) . optional ( ) . describe ( "Filter by programming language (e.g., 'typescript', 'javascript')" ) ,
617+ extension : z . string ( ) . optional ( ) . describe ( "Filter by file extension (e.g., 'ts', 'tsx', 'js')" ) ,
618+ path : z . string ( ) . optional ( ) . describe ( "Filter by file path pattern (e.g., 'src/', 'components/')" ) ,
619+ } ) ,
620+ func : async ( input : any ) => {
621+ return await searchCodeTool . func ( {
622+ ...input ,
623+ owner,
624+ repo,
625+ accessToken
626+ } ) ;
627+ }
628+ } ) ;
629+
630+ const boundGetFileContentTool = new DynamicStructuredTool ( {
631+ name : "getFileContent" ,
632+ description : getFileContentTool . description ,
633+ schema : z . object ( {
634+ path : z . string ( ) . describe ( "File path within the repository (e.g., 'src/app/page.tsx')" ) ,
635+ branch : z . string ( ) . optional ( ) . describe ( "Branch name (optional, uses default branch if not specified)" ) ,
636+ } ) ,
637+ func : async ( input : any ) => {
638+ return await getFileContentTool . func ( {
639+ ...input ,
640+ owner,
641+ repo,
642+ accessToken,
643+ branch : input . branch || project . defaultBranch || 'main'
644+ } ) ;
645+ }
646+ } ) ;
647+
648+ const tools = [ boundSearchCodeTool , boundGetFileContentTool , parallelWebSearchTool ] ;
649+
650+ // 5. Format Conversation History
651+ const formattedHistory = conversationHistory . map ( msg =>
652+ `${ msg . type === 'user' ? 'User' : 'Assistant' } : ${ msg . content } `
653+ ) . join ( '\n' ) ;
654+
655+ const pactPrompt = pactType === 'BUG' ? projectChatBotBugPrompt : pactType === 'TASK' ? projectChatBotTaskPrompt : projectChatBotFeaturePrompt ;
656+
657+ // 6. Create Agent
658+ const prompt = ChatPromptTemplate . fromMessages ( [
659+ [ "system" , pactPrompt ] ,
660+ new MessagesPlaceholder ( "agent_scratchpad" ) ,
661+ ] ) ;
662+
663+ const agent = await createToolCallingAgent ( {
664+ llm,
665+ tools,
666+ prompt,
667+ } ) ;
668+
669+ const agentExecutor = new AgentExecutor ( {
670+ agent,
671+ tools,
672+ verbose : true ,
673+ maxIterations : 15 , // Reduced to encourage fewer tool calls and lower costs
674+ } ) ;
675+
676+ // 7. Execute Agent
677+ const agentResult = await agentExecutor . invoke ( {
678+ userInput,
679+ projectArchitecture,
680+ framework : project . framework || 'Unknown' ,
681+ conversationHistory : formattedHistory ,
682+ repoFullName : project . repoFullName ,
683+ } ) ;
684+
685+ // Check if agent completed successfully
686+ if ( ! agentResult . output || typeof agentResult . output !== 'string' ) {
687+ return { error : 'Agent failed to generate a response. Please try again with a simpler request.' } ;
688+ }
689+
690+ // Check for max iterations error
691+ if ( agentResult . output . includes ( 'Agent stopped due to max iterations' ) ||
692+ agentResult . output . includes ( 'Agent stopped due to iteration limit' ) ) {
693+ return { error : 'The request was too complex and exceeded the processing limit. Please try breaking it into smaller tasks.' } ;
694+ }
695+
696+ // 8. Structure Output using LLM with structured output
697+ // const structuredLlm = llm.withStructuredOutput(pactOutputSchema);
698+
699+ // Create a prompt to format the agent output into our schema
700+ // const formatterPrompt = PromptTemplate.fromTemplate(`
701+ // You are formatting an agent's analysis into a structured pact format.
702+
703+ // The agent analyzed this request and provided the following output. Your job is to extract and format it properly.
454704
705+ // Agent's Analysis:
706+ // {agentOutput}
707+
708+ // Pact Type: {pactType}
709+
710+ // Create a structured response with:
711+ // 1. shortResponse: Extract or create a concise summary (under 200 words) of the key findings and recommendations
712+ // 2. pact.title: Create a clear, actionable title for this {pactType}
713+ // 3. pact.body: Format the detailed analysis as markdown following the {pactType} template
714+
715+ // IMPORTANT: The pact body should be well-formatted markdown with proper sections based on the pact type.
716+ // `);
717+
718+ // const formatterChain = formatterPrompt.pipe(structuredLlm);
719+
720+ // const structuredOutput = await formatterChain.invoke({
721+ // agentOutput: agentCopyResult,
722+ // pactType
723+ // });
724+
725+ // // Validate structured output
726+ // if (!structuredOutput || !structuredOutput.shortResponse || !structuredOutput.pact) {
727+ // return { error: 'Failed to generate properly formatted pact. Please try again.' };
728+ // }
729+
730+ // if (!structuredOutput.pact.title || !structuredOutput.pact.body) {
731+ // return { error: 'Generated pact is missing required fields. Please try again.' };
732+ // }
733+
734+ const structuredOutput = JSON . parse ( agentResult . output ) ;
735+
736+ return {
737+ shortResponse : structuredOutput . shortResponse ,
738+ pact : {
739+ title : structuredOutput . pact . title ,
740+ body : structuredOutput . pact . body
741+ }
742+ } ;
743+
744+ } catch ( error ) {
745+ console . error ( 'Error in createPactProjectChatBot:' , error ) ;
746+
747+ // Handle specific error types
748+ if ( error instanceof Error ) {
749+ if ( error . message . includes ( 'rate limit' ) ) {
750+ return { error : 'GitHub API rate limit exceeded. Please try again later.' } ;
751+ }
752+ if ( error . message . includes ( 'authentication' ) ) {
753+ return { error : 'GitHub authentication failed. Please reconnect your repository.' } ;
754+ }
755+ if ( error . message . includes ( 'timeout' ) ) {
756+ return { error : 'Request timeout. The operation took too long. Please try again.' } ;
757+ }
758+
759+ return { error : `Failed to generate pact: ${ error . message } ` } ;
760+ }
761+
762+ return { error : 'An unexpected error occurred while generating the pact' } ;
763+ }
455764}
456765
457766
0 commit comments