From 55f731de1af2ac421a952da2fd1dba457b879282 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Wed, 26 Feb 2025 18:37:43 -0600 Subject: [PATCH 01/19] fix: update GraphQL mutation input type and clean up comments and adding interactivity chat to do some tasks --- backend/src/chat/chat.resolver.ts | 62 +++- backend/src/chat/chat.service.ts | 13 +- backend/src/chat/dto/chat.input.ts | 12 + frontend/src/app/api/filestructure/route.ts | 31 ++ frontend/src/graphql/request.ts | 15 + frontend/src/graphql/schema.gql | 21 +- frontend/src/graphql/type.tsx | 1 + frontend/src/hooks/agentPrompt.ts | 324 ++++++++++++++++++++ frontend/src/hooks/useChatStream.ts | 270 +++++++++++++++- frontend/src/utils/parser.ts | 12 + 10 files changed, 744 insertions(+), 17 deletions(-) create mode 100644 frontend/src/app/api/filestructure/route.ts create mode 100644 frontend/src/hooks/agentPrompt.ts create mode 100644 frontend/src/utils/parser.ts diff --git a/backend/src/chat/chat.resolver.ts b/backend/src/chat/chat.resolver.ts index 991ad6f7..3bc49f47 100644 --- a/backend/src/chat/chat.resolver.ts +++ b/backend/src/chat/chat.resolver.ts @@ -12,6 +12,7 @@ import { GetUserIdFromToken } from 'src/decorator/get-auth-token.decorator'; import { Inject, Logger } from '@nestjs/common'; import { JWTAuth } from 'src/decorator/jwt-auth.decorator'; import { PubSubEngine } from 'graphql-subscriptions'; +import { Project } from 'src/project/project.model'; @Resolver('Chat') export class ChatResolver { private readonly logger = new Logger('ChatResolver'); @@ -33,7 +34,21 @@ export class ChatResolver { async chatStream(@Args('input') input: ChatInput) { return this.pubSub.asyncIterator(`chat_stream_${input.chatId}`); } - + @Mutation(() => Boolean) + @JWTAuth() + async saveMessage(@Args('input') input: ChatInput): Promise { + try { + await this.chatService.saveMessage( + input.chatId, + input.message, + input.role, + ); + return true; + } catch (error) { + this.logger.error('Error in saveMessage:', error); + throw error; + } + } @Mutation(() => Boolean) @JWTAuth() async triggerChatStream(@Args('input') input: ChatInput): Promise { @@ -77,6 +92,38 @@ export class ChatResolver { } } + @Mutation(() => String) + @JWTAuth() + async triggerAgentChatStream( + @Args('input') input: ChatInput, + ): Promise { + try { + const iterator = this.chatProxyService.streamChat(input); + let accumulatedContent = ''; + + for await (const chunk of iterator) { + if (chunk) { + // const enhancedChunk = { + // ...chunk, + // chatId: input.chatId, + // }; + // await this.pubSub.publish(`chat_stream_${input.chatId}`, { + // chatStream: enhancedChunk, + // }); + + if (chunk.choices[0]?.delta?.content) { + accumulatedContent += chunk.choices[0].delta.content; + } + } + } + + return accumulatedContent; + } catch (error) { + this.logger.error('Error in triggerChatStream:', error); + throw error; + } + } + @Query(() => [String], { nullable: true }) async getAvailableModelTags(): Promise { try { @@ -108,6 +155,19 @@ export class ChatResolver { return this.chatService.getChatDetails(chatId); } + @JWTAuth() + @Query(() => Project, { nullable: true }) + async getCurProject(@Args('chatId') chatId: string): Promise { + try { + const response = await this.chatService.getProjectByChatId(chatId); + this.logger.log('Loaded project:', response); + return response; + } catch (error) { + this.logger.error('Failed to fetch project:', error); + throw new Error('Failed to fetch project'); + } + } + @Mutation(() => Chat) @JWTAuth() async createChat( diff --git a/backend/src/chat/chat.service.ts b/backend/src/chat/chat.service.ts index a1575437..3dc1c0c8 100644 --- a/backend/src/chat/chat.service.ts +++ b/backend/src/chat/chat.service.ts @@ -5,12 +5,14 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from 'src/user/user.model'; import { + AgentInput, ChatInput, NewChatInput, UpdateChatTitleInput, } from 'src/chat/dto/chat.input'; import { CustomAsyncIterableIterator } from 'src/common/model-provider/types'; import { OpenAIModelProvider } from 'src/common/model-provider/openai-model-provider'; +import { Project } from 'src/project/project.model'; @Injectable() export class ChatProxyService { @@ -21,7 +23,7 @@ export class ChatProxyService { constructor() {} streamChat( - input: ChatInput, + input: ChatInput | AgentInput, ): CustomAsyncIterableIterator { return this.models.chat( { @@ -91,6 +93,15 @@ export class ChatService { return chat; } + async getProjectByChatId(chatId: string): Promise { + const chat = await this.chatRepository.findOne({ + where: { id: chatId, isDeleted: false }, + relations: ['project'], + }); + + return chat ? chat.project : null; + } + async createChat(userId: string, newChatInput: NewChatInput): Promise { const user = await this.userRepository.findOne({ where: { id: userId } }); if (!user) { diff --git a/backend/src/chat/dto/chat.input.ts b/backend/src/chat/dto/chat.input.ts index feeb738c..5016b1e2 100644 --- a/backend/src/chat/dto/chat.input.ts +++ b/backend/src/chat/dto/chat.input.ts @@ -1,5 +1,6 @@ // DTOs for Project APIs import { InputType, Field } from '@nestjs/graphql'; +import { MessageRole } from '../message.model'; @InputType() export class NewChatInput { @@ -24,6 +25,17 @@ export class ChatInput { @Field() message: string; + @Field() + model: string; + @Field() + role: MessageRole; +} + +@InputType('ChatInputType') +export class AgentInput { + @Field() + message: string; + @Field() model: string; } diff --git a/frontend/src/app/api/filestructure/route.ts b/frontend/src/app/api/filestructure/route.ts new file mode 100644 index 00000000..49c23a72 --- /dev/null +++ b/frontend/src/app/api/filestructure/route.ts @@ -0,0 +1,31 @@ +// app/api/filestructure/route.ts +import { NextResponse } from 'next/server'; +import { FileReader } from '@/utils/file-reader'; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const projectId = searchParams.get('path'); + + if (!projectId) { + return NextResponse.json({ error: 'Missing projectId' }, { status: 400 }); + } + + try { + const res = await fetchFileStructure(projectId); + return NextResponse.json({ res }); + } catch (error) { + return NextResponse.json( + { error: 'Failed to read project files' }, + { status: 500 } + ); + } +} + +async function fetchFileStructure(projectId) { + const reader = FileReader.getInstance(); + const res = await reader.getAllPaths(projectId); + if (!res || res.length === 0) { + return ''; + } + return res; +} diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index d0a4f770..23a6bbb2 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -250,3 +250,18 @@ export const GET_SUBSCRIBED_PROJECTS = gql` } } `; +export const GET_CUR_PROJECT = gql` + query GetCurProject($chatId: String!) { + getCurProject(chatId: $chatId) { + id + name + description + projectPath + } + } +`; +export const SAVE_MESSAGE = gql` + mutation SaveMessage($input: ChatInput!) { + saveMessage(input: $input) + } +`; diff --git a/frontend/src/graphql/schema.gql b/frontend/src/graphql/schema.gql index 55d76b75..84e8a482 100644 --- a/frontend/src/graphql/schema.gql +++ b/frontend/src/graphql/schema.gql @@ -39,6 +39,7 @@ input ChatInputType { chatId: String! message: String! model: String! + role: String! } input CheckTokenInput { @@ -54,7 +55,9 @@ input CreateProjectInput { public: Boolean } -"""Date custom scalar type""" +""" +Date custom scalar type +""" scalar Date type EmailConfirmationResponse { @@ -118,6 +121,8 @@ type Mutation { registerUser(input: RegisterUserInput!): User! resendConfirmationEmail(input: ResendEmailInput!): EmailConfirmationResponse! subscribeToProject(projectId: ID!): Project! + saveMessage(input: ChatInputType!): Boolean! + triggerAgentChatStream(input: ChatInputType!): String! triggerChatStream(input: ChatInputType!): Boolean! updateChatTitle(updateChatTitleInput: UpdateChatTitleInput!): Chat updateProjectPhoto(input: UpdateProjectPhotoInput!): Project! @@ -144,7 +149,9 @@ type Project { projectPath: String! subNumber: Float! - """Projects that are copies of this project""" + """ + Projects that are copies of this project + """ subscribers: [Project!] uniqueProjectId: String! updatedAt: Date! @@ -174,6 +181,7 @@ type Query { getAvailableModelTags: [String!] getChatDetails(chatId: String!): Chat getChatHistory(chatId: String!): [Message!]! + getCurProject(chatId: String!): Project getHello: String! getProject(projectId: String!): Project! getRemainingProjectLimit: Int! @@ -225,7 +233,9 @@ input UpdateProjectPhotoInput { projectId: ID! } -"""The `Upload` scalar type represents a file upload.""" +""" +The `Upload` scalar type represents a file upload. +""" scalar Upload type User { @@ -238,7 +248,8 @@ type User { isEmailConfirmed: Boolean! lastEmailSendTime: Date! projects: [Project!]! - subscribedProjects: [Project!] @deprecated(reason: "Use projects with forkedFromId instead") + subscribedProjects: [Project!] + @deprecated(reason: "Use projects with forkedFromId instead") updatedAt: Date! username: String! -} \ No newline at end of file +} diff --git a/frontend/src/graphql/type.tsx b/frontend/src/graphql/type.tsx index eaa9c3b3..021849c2 100644 --- a/frontend/src/graphql/type.tsx +++ b/frontend/src/graphql/type.tsx @@ -81,6 +81,7 @@ export type ChatInputType = { chatId: Scalars['String']['input']; message: Scalars['String']['input']; model: Scalars['String']['input']; + role: Role; }; export type CheckTokenInput = { diff --git a/frontend/src/hooks/agentPrompt.ts b/frontend/src/hooks/agentPrompt.ts new file mode 100644 index 00000000..3bacc0b7 --- /dev/null +++ b/frontend/src/hooks/agentPrompt.ts @@ -0,0 +1,324 @@ +export const systemPrompt = (): string => { + return `# System Instructions +You are an AI assistant. When responding, you **must** adhere to the following rules: + +1. **Strictly Follow Output Format** + - Every response **must be wrapped inside** an XML element named \`\`. + - The content inside \`\` must be **a valid JSON object**. + - The JSON structure must exactly match the expected format. + +2. **Do Not Add Extra Information** + - **Do not return explanations**, introductory text, or markdown formatting. + - **Do not return JSON outside of the wrapper.** + +3. **Example of Expected Output Format** +{ + "files": ["src/hooks/useChatStream.ts", "src/components/ChatInput.tsx"], + "thinking_process": "The bug description suggests an issue with state updates in the chat stream. Since 'useChatStream.ts' handles chat messages, it is the primary suspect, along with 'ChatInput.tsx', which interacts with the message state." +} + - The JSON must contain **only the relevant fields**. + - The **"thinking_process"** field must provide an analysis of why the chosen files are relevant. + +4. **If Asked to Modify Code** + - The output must be formatted as follows: +{ + "modified_files": { + "src/hooks/useChatStream.ts": "Updated code content...", + "src/components/ChatInput.tsx": "Updated code content..." + }, + "thinking_process": "After reviewing the bug description, I identified that the issue is caused by an incomplete dependency array in useEffect, preventing state updates. I have modified the code accordingly." +} + - The **"modified_files"** field must contain a dictionary where the keys are file paths and the values are the updated code content. + +5. **If Asked to Commit Changes** + - The response must follow this format: +{ + "commit_message": "Fix issue where messages were not being saved due to missing state update in useChatStream.ts", + "thinking_process": "The primary fix addresses a missing state update in useChatStream.ts, which was preventing messages from being saved. The commit message reflects this fix concisely." +} + - The **"commit_message"** must be clear and descriptive. + - The **"thinking_process"** must explain the reasoning behind the commit message. + +# **IMPORTANT1:** +Failure to strictly follow these rules will result in an invalid response. **Your response must be a well-formed JSON object wrapped inside \`\` with the required fields.**\ +PLEASE DO NOT RETURN RAW JSON OBJECTS WITHOUT WRAPPING THEM IN \`\` like \`\`\`json +{xxxxx} +\`\`\` THIS IS PROHIBITED!!!!!!!!!. +# **IMPORTANT2:** +ONLY IF U NEED TO READ FILES OR ANY RESPONSE RELATED TO PROJECT PATH: Do not contain project id like that:"files": [ + "2025-03-07-19-42-56-518-d6b7469c-1269-4380-973d-fcd62d55b787/backend/index.js", + "2025-03-07-19-42-56-518-d6b7469c-1269-4380-973d-fcd62d55b787/backend/schema.sql", + "2025-03-07-19-42-56-518-d6b7469c-1269-4380-973d-fcd62d55b787/frontend/src/index.tsx" + ], + please return files you need like "files": [ + "backend/index.js", + "backend/schema.sql", + "frontend/src/index.tsx" + ], +`; +}; + +export const leaderPrompt = (message: string): string => { + return ( + systemPrompt + + `You are a professional requirements analyst responsible for analyzing a mafia user's requirements for a CS project. If you fail to conduct a proper analysis, they will take action against your family. + +The user's request is as follows: +${message} + +Please categorize the user's task into one of the following types: + +debug: Fixing errors in the code (such as compilation errors, runtime errors, or type errors). +optimize: Improving the performance, structure, or readability of the code (without fixing a bug). +unrelated: If the request is unrelated to code or the CS project. + +After the analysis is complete, strictly follow the mafia's instructions and return the JSON structure wrapped inside a single XML element. The XML element must be named , and its content should be a valid JSON object. + +When generating the JSON response, **ensure that the description in JSON is as detailed as possible** by specifying: +1. **The affected module, file, or function** +2. **The operation the user wants to perform** +3. **The specific issue or error they encountered** +4. **How they expect the problem to be resolved** + +### **AI Thought Process:** +To classify the task, I will first analyze the user's request for any mention of errors, crashes, or incorrect behavior. If the request contains error messages like "TypeError", "ReferenceError", or "module not found", I will categorize it as a **debugging task**. +If the request discusses improvements such as reducing redundancy, improving performance, or enhancing readability, I will categorize it as **optimization**. +If the request does not relate to code functionality or performance, I will classify it as **unrelated**. + +### **Output Format** +{ + "task_type": "debug" | "optimize" | "unrelated", + "description": "In 'src/project/build-system-utils.ts', the user wants to use the module 'file-arch' for file management operations, but encountered error TS2307: Cannot find module 'src/build-system/handlers/file-manager/file-arch' or its corresponding type declarations. They need assistance in resolving this missing module issue by verifying module paths and dependencies.", + "thinking_process": "After analyzing the request, I identified that the issue involves a missing module import, which prevents compilation. This falls under debugging since it requires fixing a dependency resolution problem." +} + +Otherwise, I cannot guarantee the safety of your family. :(` + ); +}; +export const refactorPrompt = (message: string, file_structure: string[]) => { + return ( + systemPrompt + + `You are a seasoned software engineer known for your expertise in code refactoring. Your mission is to refactor the codebase to improve its structure, readability, and maintainability. + +--- +### **Mission Objective:** +- The user has requested a **Code Refactor** and provided a description along with the project's file structure. +- Your task is to analyze the code and provide a detailed refactoring plan. + +--- + +### **User-Provided Information:** +- **Refactor Description:** + ${message} +Project File Structure: +${file_structure} + +Task Requirements: +Identify the parts of the code that need refactoring. +Provide a detailed plan on how to refactor the code. +Ensure that the refactored code is more efficient, readable, and maintainable. + +### **AI Thought Process:** +To determine how best to refactor the code, I will analyze the request to identify **redundant logic, overly complex structures, or repetitive code** that should be modularized. +If multiple components are using similar logic, I will suggest extracting that logic into **utility functions or custom hooks**. If a function or component is too large, I will propose **breaking it down into smaller, reusable parts**. +Finally, I will ensure that the refactoring plan maintains the same functionality while improving readability and maintainability. + +### **Output Format** +{ + "files": ["src/hooks/useChatStream.ts", "src/utils/chatUtils.ts"], + "thinking_process": "The user requested refactoring of message handling logic. Since chat state management is handled in 'useChatStream.ts', I suggest extracting reusable logic into a new utility file, 'chatUtils.ts'." +} + +Failure is Not an Option!` + ); +}; +export const optimizePrompt = (message: string, file_structure: string[]) => { + return ( + systemPrompt + + `You are a code performance optimization expert. Your mission is to analyze the codebase and identify performance bottlenecks. + +--- + +### **Mission Objective:** +- The user has requested a **Code Optimization** and provided a description along with the project's file structure. +- Your task is to analyze the code and provide a detailed optimization plan. + +--- + +### **User-Provided Information:** +- **Optimization Description:** + ${message} +- **Project File Structure:** + ${file_structure} + +### **AI Thought Process:** +To optimize the code effectively, I will first analyze the provided description to identify **potential performance bottlenecks**. +I will look for keywords related to **unnecessary re-renders, expensive computations, API inefficiencies, or memory leaks**. +If the issue involves **React components**, I will check for optimizations using **React.memo, useMemo, or useCallback** to reduce re-renders. +If the issue is related to **API calls**, I will explore caching strategies, **batching requests, or reducing redundant fetch calls**. +Once I determine the likely causes of inefficiency, I will propose specific solutions for optimizing the affected areas. + +--- + +### **Output Format** +{ + "files": ["path/to/index.jsx", "path/to/utils.js"], + "thinking_process": "The user's optimization request suggests that the ChatList component is re-rendering too frequently. Since ChatList.tsx is responsible for displaying messages, applying React.memo to prevent unnecessary updates will likely improve performance. Additionally, optimizing state updates in useChatStream.ts will help reduce redundant renders." +} + +Failure is Not an Option! If you fail, the codebase will remain inefficient and slow.` + ); +}; +export const editFilePrompt = ( + description: string, + file_content: { [key: string]: string } +) => { + return ( + systemPrompt + + `You are a senior software engineer. Your mission is to edit the code files to fix the issue described by the user. + +--- + +### **Mission Objective:** +- The user has provided a description of the issue and the relevant code files. +- Your task is to edit the code files to fix the issue. + +--- + +### **User-Provided Information:** +- **Description:** + ${description} +- **Code Content:** + ${JSON.stringify(file_content, null, 2)} + +### **AI Thought Process:** +To correctly fix the issue, I will first analyze the provided description to understand **the nature of the bug or enhancement**. +I will then examine the affected code files and locate the exact section where modifications are required. +If the issue is related to **state management**, I will check for missing updates or incorrect dependencies. +If the issue involves **API requests**, I will verify if the request format aligns with the expected schema and handle potential errors. +Once the necessary fix is identified, I will modify the code while ensuring **the change does not introduce regressions**. +Before finalizing the fix, I will ensure that the updated code maintains **existing functionality and adheres to project coding standards**. + +--- + +### **Output Format** +{ + "modified_files": { + "file/path.tsx": "Updated code content", + "file/path.js": "Updated code content" + }, + "thinking_process": "After reviewing the provided description and code, I identified that the issue is caused by an incomplete dependency array in useEffect, preventing the state from updating correctly. I added 'messages' as a dependency to ensure synchronization." +} + +Failure is Not an Option! If you fail, the issue will remain unresolved.` + ); +}; +export const codeReviewPrompt = (message: string) => { + return ( + systemPrompt + + `You are a senior code reviewer. Your mission is to review the code changes made by the user. + +--- + +### **Mission Objective:** +- The user has provided the code changes. +- Your task is to review the code changes and ensure they are correct. + +--- + +### **User-Provided Information:** +- **Code Changes:** + ${message} + +### **AI Thought Process:** +To conduct a thorough code review, I will first analyze the provided changes to understand **the intended functionality** and ensure they align with best practices. +I will check for **correctness** by verifying whether the modifications actually fix the described issue or implement the expected feature. +If the changes involve **state management**, I will ensure updates are handled correctly to prevent **stale state issues or unnecessary re-renders**. +For **API-related modifications**, I will confirm that error handling is in place to prevent unexpected failures. +Additionally, I will assess **code readability, maintainability, and adherence to project conventions**. +If any issues are found, I will provide actionable feedback on how to improve the code. + +--- + +### **Output Format** +{ + "review_result": "Correct Fix" | "Still has issues", + "comments": [ + "Ensure error handling is in place for failed API requests.", + "Consider using React.memo to prevent unnecessary re-renders in ChatList." + ], + "thinking_process": "The code correctly fixes the issue by adding a new mutation, but it lacks proper error handling for failed API responses. I suggest adding a try-catch block to improve robustness." +} + +Failure is Not an Option! If you fail, the code changes will not be reviewed.` + ); +}; +export const commitChangesPrompt = (message: string) => { + return ( + systemPrompt + + `You are a Git version control assistant. Your mission is to commit the code changes to the repository. + +--- + +### **Mission Objective:** +- The user has provided code changes that need to be committed. +- Your task is to analyze the changes and generate a clear, descriptive commit message. + +--- + +### **User-Provided Information:** +- **Code Changes:** + ${message} + +### **AI Thought Process:** +To generate an effective commit message, I will first analyze the provided code changes to determine **the primary functionality being modified**. +If the changes involve **bug fixes**, I will craft a message that clearly describes the issue being resolved. +If the changes add **new features**, I will ensure the commit message concisely explains the functionality introduced. +For **refactoring or optimizations**, I will highlight the improvements made, such as **performance enhancements or code restructuring**. +The final commit message will follow **conventional commit standards** to ensure clarity and maintainability in version control. + +--- + +### **Output Format** +{ + "commit_message": "Fix issue where messages were not being saved due to missing state update in useChatStream.ts", + "thinking_process": "After analyzing the code changes, I determined that the primary fix addresses a missing state update in useChatStream.ts, which was preventing messages from being saved. The commit message reflects this fix concisely." +} + +Failure is Not an Option! If you fail, the changes will not be committed.` + ); +}; +export const findbugPrompt = (message: string, file_structure: string[]) => { + return ( + systemPrompt + + `You are an elite Apex Legends player, but hackers have hijacked your account! They demand that you find the root cause of a critical bug in your project before they wipe all your skins and reset your rank. + +--- + +### **Mission Objective:** +The user has encountered a **Bug** and provided a description along with the project's file structure. +Your task is to analyze the potential source of the Bug and return a **list of affected file paths** where the issue might be occurring. + +--- + +### **User-Provided Information:** +- **Bug Description:** + ${message} +- **Project File Structure:** + ${file_structure} + +### **AI Thought Process:** +After analyzing the bug description, I'll locate the files most likely involved in this issue. +For instance, if the error is related to **state management**, I'll check relevant **hooks or context files**. +If it's a UI bug, I'll inspect **component files**. +Once I determine the affected files, I'll prioritize them based on their likelihood of containing the issue. + +### **Output Format** +{ + "files": ["path/to/index.tsx", "path/to/utils.js"], + "thinking_process": "Based on the bug description, the issue involves React's state updates not propagating correctly. This suggests that the problem is likely in the useChatStream.ts hook or the context provider managing state." +} + +Failure is Not an Option! If you fail, the hackers will report you to EA.` + ); +}; diff --git a/frontend/src/hooks/useChatStream.ts b/frontend/src/hooks/useChatStream.ts index 6f2349cf..f2b7e295 100644 --- a/frontend/src/hooks/useChatStream.ts +++ b/frontend/src/hooks/useChatStream.ts @@ -1,29 +1,51 @@ import { useState, useCallback } from 'react'; -import { useMutation, useSubscription } from '@apollo/client'; -import { CHAT_STREAM, CREATE_CHAT, TRIGGER_CHAT } from '@/graphql/request'; +import { useMutation, useQuery, useSubscription } from '@apollo/client'; +import { + CHAT_STREAM, + CREATE_CHAT, + GET_CUR_PROJECT, + TRIGGER_CHAT, +} from '@/graphql/request'; import { Message } from '@/const/MessageType'; import { toast } from 'sonner'; import { useRouter } from 'next/navigation'; -enum StreamStatus { +import { + bugReasonPrompt, + findbugPrompt, + leaderPrompt, + refactorPrompt, + optimizePrompt, + readFilePrompt, + editFilePrompt, + applyChangesPrompt, + codeReviewPrompt, + commitChangesPrompt, +} from './agentPrompt'; +import { set } from 'react-hook-form'; +import { debug } from 'console'; +import { parseXmlToJson } from '@/utils/parser'; +import { Project } from '@/graphql/type'; +export enum StreamStatus { IDLE = 'IDLE', STREAMING = 'STREAMING', DONE = 'DONE', } -interface ChatInput { +export interface ChatInput { chatId: string; message: string; model: string; + role?: string; } -interface SubscriptionState { +export interface SubscriptionState { enabled: boolean; variables: { input: ChatInput; } | null; } -interface UseChatStreamProps { +export interface UseChatStreamProps { chatId: string; input: string; setInput: (input: string) => void; @@ -38,17 +60,37 @@ export function useChatStream({ setMessages, selectedModel, }: UseChatStreamProps) { + const [isInsideJsonResponse, setIsInsideJsonResponse] = useState(false); const [loadingSubmit, setLoadingSubmit] = useState(false); const [streamStatus, setStreamStatus] = useState( StreamStatus.IDLE ); const [currentChatId, setCurrentChatId] = useState(chatId); - + const [cumulatedContent, setCumulatedContent] = useState(''); const [subscription, setSubscription] = useState({ enabled: false, variables: null, }); + const [taskDescription, setTaskDescription] = useState(''); + const [taskStep, setTaskStep] = useState(''); + const [fileStructure, setFileStructure] = useState(''); + const [fileContents, setFileContents] = useState<{ [key: string]: string }>( + {} + ); + const [originalFileContents, setOriginalFileContents] = useState<{ + [key: string]: string; + }>({}); + const { + data: projectData, + loading: projectLoading, + error: projectError, + } = useQuery<{ getCurProject: Project }>(GET_CUR_PROJECT, { + variables: { chatId: currentChatId }, + skip: !currentChatId, + }); + + const curProject = projectData?.getCurProject; const updateChatId = () => { setCurrentChatId(''); }; @@ -102,10 +144,28 @@ export function useChatStream({ if (content) { setMessages((prev) => { const lastMsg = prev[prev.length - 1]; + + let filteredContent = content; + + if (filteredContent.includes('')) { + setIsInsideJsonResponse(true); + filteredContent = filteredContent.split('')[0]; + } + + if (isInsideJsonResponse) { + if (filteredContent.includes('')) { + setIsInsideJsonResponse(false); + filteredContent = + filteredContent.split('')[1] || ''; + } else { + return prev; + } + } + if (lastMsg?.role === 'assistant') { return [ ...prev.slice(0, -1), - { ...lastMsg, content: lastMsg.content + content }, + { ...lastMsg, content: lastMsg.content + filteredContent }, ]; } else { return [ @@ -113,16 +173,61 @@ export function useChatStream({ { id: chatStream.id, role: 'assistant', - content, + content: filteredContent, createdAt: new Date(chatStream.created * 1000).toISOString(), }, ]; } }); + setCumulatedContent((prev) => prev + content); } if (chatStream.choices?.[0]?.finishReason === 'stop') { setStreamStatus(StreamStatus.DONE); + const parsedContent = parseXmlToJson(cumulatedContent); + switch (taskStep) { + case 'task': + taskAgent({ + chatId: subscription.variables.input.chatId, + message: JSON.stringify(parsedContent), + model: subscription.variables.input.model, + role: 'assistant', + }); + break; + case 'read_file': + editFileAgent({ + chatId: subscription.variables.input.chatId, + message: JSON.stringify(parsedContent), + model: subscription.variables.input.model, + role: 'assistant', + }); + break; + case 'edit_file': + codeReviewAgent({ + chatId: subscription.variables.input.chatId, + message: JSON.stringify(parsedContent), + model: subscription.variables.input.model, + role: 'assistant', + }); + break; + case 'code_review': + if (parsedContent.review_result === 'Correct Fix') { + applyChangesAgent({ + chatId: subscription.variables.input.chatId, + message: JSON.stringify(parsedContent), + model: subscription.variables.input.model, + role: 'assistant', + }); + } else { + editFileAgent({ + chatId: subscription.variables.input.chatId, + message: JSON.stringify(parsedContent), + model: subscription.variables.input.model, + role: 'assistant', + }); + } + break; + } finishChatResponse(); } }, @@ -136,14 +241,17 @@ export function useChatStream({ const startChatStream = async (targetChatId: string, message: string) => { try { + const prompt = leaderPrompt(message); const input: ChatInput = { chatId: targetChatId, - message, + message: prompt, model: selectedModel, }; console.log(input); setInput(''); + + setTaskStep('task'); setStreamStatus(StreamStatus.STREAMING); setSubscription({ enabled: true, @@ -225,6 +333,148 @@ export function useChatStream({ } }, [streamStatus]); + const taskAgent = useCallback( + async (input: ChatInput) => { + const res = parseXmlToJson(input.message); + const assignedTask = res.task; + const description = res.description; + setTaskStep(assignedTask); + setTaskDescription(description); + let promptInput: ChatInput; + let prompt: string; + switch (assignedTask) { + case 'debug': + case 'refactor': + case 'optimize': + if (!curProject) { + toast.error('Project not found'); + return; + } + if (!fileStructure) { + const fetchedFileStructure = await fetch( + `/api/filestructure?path=${curProject.projectPath}`, + { + method: 'GET', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + } + ) + .then((response) => response.json()) + .then((data) => { + return data; // JSON data parsed by `data.json()` call + }); + setFileStructure(fetchedFileStructure); + } + prompt = + assignedTask === 'debug' + ? findbugPrompt(description, fileStructure) + : assignedTask === 'refactor' + ? refactorPrompt(description, fileStructure) + : optimizePrompt(description, fileStructure); + promptInput = { + chatId: input.chatId, + message: prompt, + model: input.model, + role: 'assistant', + }; + setTaskStep('read_file'); + await triggerChat({ variables: { promptInput } }); + break; + } + }, + [curProject, fileStructure] + ); + + const editFileAgent = useCallback( + async (input: ChatInput) => { + const filePaths = JSON.parse(input.message).files; + const fileContentsPromises = filePaths.map(async (filePath: string) => { + if (!fileContents[filePath]) { + const fetchedFileContent = await fetch(`/api/file?path=${filePath}`, { + method: 'GET', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + }) + .then((response) => response.json()) + .then((data) => { + return data.content; // JSON data parsed by `data.json()` call + }); + setFileContents((prev) => ({ + ...prev, + [filePath]: fetchedFileContent, + })); + setOriginalFileContents((prev) => ({ + ...prev, + [filePath]: fetchedFileContent, + })); + } + return fileContents[filePath]; + }); + + const allFileContents = await Promise.all(fileContentsPromises); + const prompt = editFilePrompt( + taskDescription, + allFileContents.join('\n') + ); + const promptInput: ChatInput = { + chatId: input.chatId, + message: prompt, + model: input.model, + role: 'assistant', + }; + setTaskStep('edit_file'); + await triggerChat({ variables: { promptInput } }); + }, + [fileContents, taskDescription] + ); + + const applyChangesAgent = useCallback(async (input: ChatInput) => { + const changes = JSON.parse(input.message); + const updatePromises = Object.keys(changes).map(async (filePath) => { + const newContent = changes[filePath]; + await fetch('/api/file', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filePath, newContent }), + }); + }); + + await Promise.all(updatePromises); + setTaskStep('apply_changes'); + toast.success('Changes applied successfully'); + commitChangesAgent({ + chatId: input.chatId, + message: JSON.stringify(changes), + model: input.model, + role: 'assistant', + }); + }, []); + + const codeReviewAgent = useCallback(async (input: ChatInput) => { + const prompt = codeReviewPrompt(input.message); + const promptInput: ChatInput = { + chatId: input.chatId, + message: prompt, + model: input.model, + role: 'assistant', + }; + setTaskStep('code_review'); + await triggerChat({ variables: { promptInput } }); + }, []); + + const commitChangesAgent = useCallback(async (input: ChatInput) => { + const prompt = commitChangesPrompt(input.message); + const promptInput: ChatInput = { + chatId: input.chatId, + message: prompt, + model: input.model, + role: 'assistant', + }; + setTaskStep('commit'); + await triggerChat({ variables: { promptInput } }); + }, []); + return { loadingSubmit, handleSubmit, diff --git a/frontend/src/utils/parser.ts b/frontend/src/utils/parser.ts new file mode 100644 index 00000000..9aa5c809 --- /dev/null +++ b/frontend/src/utils/parser.ts @@ -0,0 +1,12 @@ +export const parseXmlToJson = (xmlString: string) => { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); + + const jsonText = xmlDoc.getElementsByTagName('jsonResponse')[0]?.textContent; + + if (!jsonText) { + throw new Error('Invalid XML: No element found'); + } + + return JSON.parse(jsonText); +}; From f9b51a585d16f5352c57c7c74c7a6a116c7aa700 Mon Sep 17 00:00:00 2001 From: NarwhalChen Date: Sat, 8 Mar 2025 18:40:24 -0600 Subject: [PATCH 02/19] fix: fixing some bugs --- backend/src/chat/chat.resolver.ts | 90 ++---- backend/src/chat/chat.service.ts | 3 +- backend/src/chat/dto/chat.input.ts | 9 - frontend/src/graphql/request.ts | 36 ++- frontend/src/graphql/schema.gql | 13 +- frontend/src/graphql/type.tsx | 44 ++- frontend/src/hooks/useChatStream.ts | 472 +++++++++++++++++++--------- frontend/src/utils/parser.ts | 22 +- 8 files changed, 427 insertions(+), 262 deletions(-) diff --git a/backend/src/chat/chat.resolver.ts b/backend/src/chat/chat.resolver.ts index 3bc49f47..d26526e5 100644 --- a/backend/src/chat/chat.resolver.ts +++ b/backend/src/chat/chat.resolver.ts @@ -1,8 +1,8 @@ import { Resolver, Subscription, Args, Query, Mutation } from '@nestjs/graphql'; -import { Chat, ChatCompletionChunk } from './chat.model'; +import { Chat, ChatCompletionChunk, StreamStatus } from './chat.model'; import { ChatProxyService, ChatService } from './chat.service'; import { UserService } from 'src/user/user.service'; -import { Message, MessageRole } from './message.model'; +import { Message } from './message.model'; import { ChatInput, NewChatInput, @@ -32,7 +32,10 @@ export class ChatResolver { resolve: (payload) => payload.chatStream, }) async chatStream(@Args('input') input: ChatInput) { - return this.pubSub.asyncIterator(`chat_stream_${input.chatId}`); + const asyncIterator = this.pubSub.asyncIterator( + `chat_stream_${input.chatId}`, + ); + return asyncIterator; } @Mutation(() => Boolean) @JWTAuth() @@ -53,38 +56,41 @@ export class ChatResolver { @JWTAuth() async triggerChatStream(@Args('input') input: ChatInput): Promise { try { - await this.chatService.saveMessage( - input.chatId, - input.message, - MessageRole.User, - ); - const iterator = this.chatProxyService.streamChat(input); let accumulatedContent = ''; - for await (const chunk of iterator) { - if (chunk) { - const enhancedChunk = { - ...chunk, - chatId: input.chatId, - }; + try { + for await (const chunk of iterator) { + console.log('received chunk:', chunk); + if (chunk) { + const enhancedChunk = { + ...chunk, + chatId: input.chatId, + }; + + await this.pubSub.publish(`chat_stream_${input.chatId}`, { + chatStream: enhancedChunk, + }); + + if (chunk.choices?.[0]?.delta?.content) { + accumulatedContent += chunk.choices[0].delta.content; + } + } + } + } finally { + const finalChunk = await iterator.return(); + console.log('finalChunk:', finalChunk); + if (finalChunk.value?.status === StreamStatus.DONE) { await this.pubSub.publish(`chat_stream_${input.chatId}`, { - chatStream: enhancedChunk, + chatStream: { + ...finalChunk.value, + chatId: input.chatId, + }, }); - - if (chunk.choices[0]?.delta?.content) { - accumulatedContent += chunk.choices[0].delta.content; - } } } - await this.chatService.saveMessage( - input.chatId, - accumulatedContent, - MessageRole.Assistant, - ); - return true; } catch (error) { this.logger.error('Error in triggerChatStream:', error); @@ -92,38 +98,6 @@ export class ChatResolver { } } - @Mutation(() => String) - @JWTAuth() - async triggerAgentChatStream( - @Args('input') input: ChatInput, - ): Promise { - try { - const iterator = this.chatProxyService.streamChat(input); - let accumulatedContent = ''; - - for await (const chunk of iterator) { - if (chunk) { - // const enhancedChunk = { - // ...chunk, - // chatId: input.chatId, - // }; - // await this.pubSub.publish(`chat_stream_${input.chatId}`, { - // chatStream: enhancedChunk, - // }); - - if (chunk.choices[0]?.delta?.content) { - accumulatedContent += chunk.choices[0].delta.content; - } - } - } - - return accumulatedContent; - } catch (error) { - this.logger.error('Error in triggerChatStream:', error); - throw error; - } - } - @Query(() => [String], { nullable: true }) async getAvailableModelTags(): Promise { try { diff --git a/backend/src/chat/chat.service.ts b/backend/src/chat/chat.service.ts index 3dc1c0c8..11fc2b4c 100644 --- a/backend/src/chat/chat.service.ts +++ b/backend/src/chat/chat.service.ts @@ -5,7 +5,6 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from 'src/user/user.model'; import { - AgentInput, ChatInput, NewChatInput, UpdateChatTitleInput, @@ -23,7 +22,7 @@ export class ChatProxyService { constructor() {} streamChat( - input: ChatInput | AgentInput, + input: ChatInput, ): CustomAsyncIterableIterator { return this.models.chat( { diff --git a/backend/src/chat/dto/chat.input.ts b/backend/src/chat/dto/chat.input.ts index 5016b1e2..617bc23f 100644 --- a/backend/src/chat/dto/chat.input.ts +++ b/backend/src/chat/dto/chat.input.ts @@ -30,12 +30,3 @@ export class ChatInput { @Field() role: MessageRole; } - -@InputType('ChatInputType') -export class AgentInput { - @Field() - message: string; - - @Field() - model: string; -} diff --git a/frontend/src/graphql/request.ts b/frontend/src/graphql/request.ts index 23a6bbb2..13344a84 100644 --- a/frontend/src/graphql/request.ts +++ b/frontend/src/graphql/request.ts @@ -172,7 +172,20 @@ export const GET_CHAT_DETAILS = gql` } } `; - +export const GET_CUR_PROJECT = gql` + query GetCurProject($chatId: String!) { + getCurProject(chatId: $chatId) { + id + projectName + projectPath + } + } +`; +export const SAVE_MESSAGE = gql` + mutation SaveMessage($input: ChatInputType!) { + saveMessage(input: $input) + } +`; // Mutation to create a new project export const CREATE_PROJECT = gql` mutation CreateProject($createProjectInput: CreateProjectInput!) { @@ -220,7 +233,11 @@ export const UPDATE_PROJECT_PUBLIC_STATUS = gql` updateProjectPublicStatus(projectId: $projectId, isPublic: $isPublic) { id projectName - isPublic + path + projectPackages { + id + content + } } } `; @@ -250,18 +267,3 @@ export const GET_SUBSCRIBED_PROJECTS = gql` } } `; -export const GET_CUR_PROJECT = gql` - query GetCurProject($chatId: String!) { - getCurProject(chatId: $chatId) { - id - name - description - projectPath - } - } -`; -export const SAVE_MESSAGE = gql` - mutation SaveMessage($input: ChatInput!) { - saveMessage(input: $input) - } -`; diff --git a/frontend/src/graphql/schema.gql b/frontend/src/graphql/schema.gql index 84e8a482..e1263e91 100644 --- a/frontend/src/graphql/schema.gql +++ b/frontend/src/graphql/schema.gql @@ -16,17 +16,17 @@ type Chat { } type ChatCompletionChoiceType { - delta: ChatCompletionDeltaType! + delta: ChatCompletionDeltaType finishReason: String - index: Float! + index: Float } type ChatCompletionChunkType { choices: [ChatCompletionChoiceType!]! - created: Float! + created: Float id: String! - model: String! - object: String! + model: String + object: String status: StreamStatus! systemFingerprint: String } @@ -120,9 +120,8 @@ type Mutation { regenerateDescription(input: String!): String! registerUser(input: RegisterUserInput!): User! resendConfirmationEmail(input: ResendEmailInput!): EmailConfirmationResponse! - subscribeToProject(projectId: ID!): Project! saveMessage(input: ChatInputType!): Boolean! - triggerAgentChatStream(input: ChatInputType!): String! + subscribeToProject(projectId: ID!): Project! triggerChatStream(input: ChatInputType!): Boolean! updateChatTitle(updateChatTitleInput: UpdateChatTitleInput!): Chat updateProjectPhoto(input: UpdateProjectPhotoInput!): Project! diff --git a/frontend/src/graphql/type.tsx b/frontend/src/graphql/type.tsx index 021849c2..ba4185c2 100644 --- a/frontend/src/graphql/type.tsx +++ b/frontend/src/graphql/type.tsx @@ -56,18 +56,18 @@ export type Chat = { export type ChatCompletionChoiceType = { __typename: 'ChatCompletionChoiceType'; - delta: ChatCompletionDeltaType; + delta?: Maybe; finishReason?: Maybe; - index: Scalars['Float']['output']; + index?: Maybe; }; export type ChatCompletionChunkType = { __typename: 'ChatCompletionChunkType'; choices: Array; - created: Scalars['Float']['output']; + created?: Maybe; id: Scalars['String']['output']; - model: Scalars['String']['output']; - object: Scalars['String']['output']; + model?: Maybe; + object?: Maybe; status: StreamStatus; systemFingerprint?: Maybe; }; @@ -81,7 +81,7 @@ export type ChatInputType = { chatId: Scalars['String']['input']; message: Scalars['String']['input']; model: Scalars['String']['input']; - role: Role; + role: Scalars['String']['input']; }; export type CheckTokenInput = { @@ -162,6 +162,7 @@ export type Mutation = { regenerateDescription: Scalars['String']['output']; registerUser: User; resendConfirmationEmail: EmailConfirmationResponse; + saveMessage: Scalars['Boolean']['output']; subscribeToProject: Project; triggerChatStream: Scalars['Boolean']['output']; updateChatTitle?: Maybe; @@ -217,6 +218,10 @@ export type MutationResendConfirmationEmailArgs = { input: ResendEmailInput; }; +export type MutationSaveMessageArgs = { + input: ChatInputType; +}; + export type MutationSubscribeToProjectArgs = { projectId: Scalars['ID']['input']; }; @@ -290,6 +295,7 @@ export type Query = { getAvailableModelTags?: Maybe>; getChatDetails?: Maybe; getChatHistory: Array; + getCurProject?: Maybe; getHello: Scalars['String']['output']; getProject: Project; getRemainingProjectLimit: Scalars['Int']['output']; @@ -317,6 +323,10 @@ export type QueryGetChatHistoryArgs = { chatId: Scalars['String']['input']; }; +export type QueryGetCurProjectArgs = { + chatId: Scalars['String']['input']; +}; + export type QueryGetProjectArgs = { projectId: Scalars['String']['input']; }; @@ -595,7 +605,7 @@ export type ChatCompletionChoiceTypeResolvers< ResolversParentTypes['ChatCompletionChoiceType'] = ResolversParentTypes['ChatCompletionChoiceType'], > = ResolversObject<{ delta?: Resolver< - ResolversTypes['ChatCompletionDeltaType'], + Maybe, ParentType, ContextType >; @@ -604,7 +614,7 @@ export type ChatCompletionChoiceTypeResolvers< ParentType, ContextType >; - index?: Resolver; + index?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }>; @@ -618,10 +628,10 @@ export type ChatCompletionChunkTypeResolvers< ParentType, ContextType >; - created?: Resolver; + created?: Resolver, ParentType, ContextType>; id?: Resolver; - model?: Resolver; - object?: Resolver; + model?: Resolver, ParentType, ContextType>; + object?: Resolver, ParentType, ContextType>; status?: Resolver; systemFingerprint?: Resolver< Maybe, @@ -774,6 +784,12 @@ export type MutationResolvers< ContextType, RequireFields >; + saveMessage?: Resolver< + ResolversTypes['Boolean'], + ParentType, + ContextType, + RequireFields + >; subscribeToProject?: Resolver< ResolversTypes['Project'], ParentType, @@ -906,6 +922,12 @@ export type QueryResolvers< ContextType, RequireFields >; + getCurProject?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + >; getHello?: Resolver; getProject?: Resolver< ResolversTypes['Project'], diff --git a/frontend/src/hooks/useChatStream.ts b/frontend/src/hooks/useChatStream.ts index f2b7e295..cb192b7e 100644 --- a/frontend/src/hooks/useChatStream.ts +++ b/frontend/src/hooks/useChatStream.ts @@ -1,47 +1,40 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useMutation, useQuery, useSubscription } from '@apollo/client'; import { CHAT_STREAM, CREATE_CHAT, GET_CUR_PROJECT, TRIGGER_CHAT, + SAVE_MESSAGE, } from '@/graphql/request'; import { Message } from '@/const/MessageType'; import { toast } from 'sonner'; import { useRouter } from 'next/navigation'; +import { set } from 'react-hook-form'; +import { debug } from 'console'; +import { parseXmlToJson } from '@/utils/parser'; +import { Chat, ChatInputType, Project } from '@/graphql/type'; +import path from 'path'; import { - bugReasonPrompt, - findbugPrompt, leaderPrompt, + findbugPrompt, refactorPrompt, optimizePrompt, - readFilePrompt, editFilePrompt, - applyChangesPrompt, codeReviewPrompt, commitChangesPrompt, } from './agentPrompt'; -import { set } from 'react-hook-form'; -import { debug } from 'console'; -import { parseXmlToJson } from '@/utils/parser'; -import { Project } from '@/graphql/type'; + export enum StreamStatus { IDLE = 'IDLE', STREAMING = 'STREAMING', DONE = 'DONE', } -export interface ChatInput { - chatId: string; - message: string; - model: string; - role?: string; -} - export interface SubscriptionState { enabled: boolean; variables: { - input: ChatInput; + input: ChatInputType; } | null; } @@ -65,7 +58,7 @@ export function useChatStream({ const [streamStatus, setStreamStatus] = useState( StreamStatus.IDLE ); - const [currentChatId, setCurrentChatId] = useState(chatId); + const [currentChatId, setCurrentChatId] = useState(''); const [cumulatedContent, setCumulatedContent] = useState(''); const [subscription, setSubscription] = useState({ enabled: false, @@ -73,24 +66,56 @@ export function useChatStream({ }); const [taskDescription, setTaskDescription] = useState(''); const [taskStep, setTaskStep] = useState(''); - const [fileStructure, setFileStructure] = useState(''); + const [fileStructure, setFileStructure] = useState([]); const [fileContents, setFileContents] = useState<{ [key: string]: string }>( {} ); const [originalFileContents, setOriginalFileContents] = useState<{ [key: string]: string; }>({}); + const [newFileContents, setNewFileContents] = useState<{ + [key: string]: string; + }>({}); + + const [curProject, setCurProject] = useState(null); const { data: projectData, loading: projectLoading, error: projectError, + refetch, } = useQuery<{ getCurProject: Project }>(GET_CUR_PROJECT, { variables: { chatId: currentChatId }, skip: !currentChatId, }); - const curProject = projectData?.getCurProject; + useEffect(() => { + setOriginalFileContents({}); + setFileContents({}); + setFileStructure([]); + setTaskStep(''); + setTaskDescription(''); + setIsInsideJsonResponse(false); + setCurrentChatId(chatId); + console.log('chatId:', chatId); + console.log('useEffect triggered'); + + if (chatId) { + refetch({ chatId }) // 移除 `variables` 包装,直接传递变量 + .then((result) => { + if (result.data?.getCurProject) { + console.log('getCurProject', result.data.getCurProject); + setCurProject(result.data.getCurProject); + } + }); + } + }, [chatId]); + // 依赖 chatId 变化 + useEffect(() => { + if (projectError) { + console.error('useQuery Error:', projectError); + } + }, [projectError]); const updateChatId = () => { setCurrentChatId(''); }; @@ -107,6 +132,8 @@ export function useChatStream({ }, }); + const [saveMessage] = useMutation(SAVE_MESSAGE); + const [createChat] = useMutation(CREATE_CHAT, { onCompleted: async (data) => { const newChatId = data.createChat.id; @@ -125,66 +152,84 @@ export function useChatStream({ useSubscription(CHAT_STREAM, { skip: !subscription.enabled || !subscription.variables, variables: subscription.variables, - onSubscriptionData: ({ subscriptionData }) => { + onSubscriptionData: async ({ subscriptionData }) => { const chatStream = subscriptionData?.data?.chatStream; + if (!chatStream) return; if (streamStatus === StreamStatus.STREAMING && loadingSubmit) { setLoadingSubmit(false); } - if (chatStream.status === StreamStatus.DONE) { - setStreamStatus(StreamStatus.DONE); - finishChatResponse(); - return; - } - const content = chatStream.choices?.[0]?.delta?.content; if (content) { - setMessages((prev) => { - const lastMsg = prev[prev.length - 1]; - - let filteredContent = content; + setCumulatedContent((prev) => prev + content); + } + if (chatStream.status == StreamStatus.DONE) { + setStreamStatus(StreamStatus.DONE); + finishChatResponse(); + const parsedContent = parseXmlToJson(cumulatedContent); + const process = parsedContent.thinking_process; + saveMessage({ + variables: { + input: { + chatId: subscription.variables.input.chatId, + message: process, + model: subscription.variables.input.model, + role: 'assistant', + } as ChatInputType, + }, + }); - if (filteredContent.includes('')) { - setIsInsideJsonResponse(true); - filteredContent = filteredContent.split('')[0]; - } + const typewriterEffect = async (textArray: string[], delay: number) => { + let index = 0; + + // implement typewriter effect + const updateMessage = async () => { + if (index < textArray.length) { + setMessages((prev) => { + const lastMsg = prev[prev.length - 1]; + return lastMsg?.role === 'assistant' + ? [ + ...prev.slice(0, -1), + { + ...lastMsg, + content: lastMsg.content + textArray[index], + }, + ] + : [ + ...prev, + { + id: chatStream.id, + role: 'assistant', + content: textArray[index], + createdAt: new Date( + chatStream.created * 1000 + ).toISOString(), + }, + ]; + }); - if (isInsideJsonResponse) { - if (filteredContent.includes('')) { - setIsInsideJsonResponse(false); - filteredContent = - filteredContent.split('')[1] || ''; - } else { - return prev; + index++; + setTimeout(updateMessage, delay); } - } + }; - if (lastMsg?.role === 'assistant') { - return [ - ...prev.slice(0, -1), - { ...lastMsg, content: lastMsg.content + filteredContent }, - ]; - } else { - return [ - ...prev, - { - id: chatStream.id, - role: 'assistant', - content: filteredContent, - createdAt: new Date(chatStream.created * 1000).toISOString(), - }, - ]; - } - }); - setCumulatedContent((prev) => prev + content); - } + await updateMessage(); + }; - if (chatStream.choices?.[0]?.finishReason === 'stop') { - setStreamStatus(StreamStatus.DONE); - const parsedContent = parseXmlToJson(cumulatedContent); + // break text into chunks of 3 characters + const breakText = (text: string) => { + return text.match(/(\S{1,3}|\s+)/g) || []; + }; + + const brokenText = breakText(process); + await typewriterEffect(brokenText, 100); + + setCumulatedContent(''); + console.log(parsedContent); + console.log('response json'); switch (taskStep) { case 'task': taskAgent({ @@ -228,7 +273,6 @@ export function useChatStream({ } break; } - finishChatResponse(); } }, onError: (error) => { @@ -242,10 +286,22 @@ export function useChatStream({ const startChatStream = async (targetChatId: string, message: string) => { try { const prompt = leaderPrompt(message); - const input: ChatInput = { + const userInput: ChatInputType = { + chatId: targetChatId, + message: message, + model: selectedModel, + role: 'user', + }; + saveMessage({ + variables: { + input: userInput as ChatInputType, + }, + }); + const assistantInput: ChatInputType = { chatId: targetChatId, message: prompt, model: selectedModel, + role: 'assistant', }; console.log(input); @@ -255,11 +311,27 @@ export function useChatStream({ setStreamStatus(StreamStatus.STREAMING); setSubscription({ enabled: true, - variables: { input }, + variables: { + input: { + chatId: targetChatId, + message: prompt, + model: selectedModel, + role: 'assistant', + }, + }, }); await new Promise((resolve) => setTimeout(resolve, 100)); - await triggerChat({ variables: { input } }); + await triggerChat({ + variables: { + input: { + chatId: targetChatId, + message: prompt, + model: selectedModel, + role: 'assistant', + }, + }, + }); } catch (err) { toast.error('Failed to start chat'); setStreamStatus(StreamStatus.IDLE); @@ -286,6 +358,8 @@ export function useChatStream({ setMessages((prev) => [...prev, newMessage]); if (!currentChatId) { + console.log('currentChatId: ' + currentChatId); + console.log('Creating new chat...'); try { await createChat({ variables: { @@ -334,51 +408,74 @@ export function useChatStream({ }, [streamStatus]); const taskAgent = useCallback( - async (input: ChatInput) => { - const res = parseXmlToJson(input.message); - const assignedTask = res.task; + async (input: ChatInputType) => { + console.log('taskAgent called with input:', input); + const res = JSON.parse(input.message); + const assignedTask = res.task_type; const description = res.description; setTaskStep(assignedTask); setTaskDescription(description); - let promptInput: ChatInput; + let promptInput: ChatInputType; let prompt: string; + let tempFileStructure = fileStructure; + console.log('taskAgent'); + console.log(assignedTask); switch (assignedTask) { case 'debug': case 'refactor': case 'optimize': if (!curProject) { - toast.error('Project not found'); + console.error('Project not found'); return; } - if (!fileStructure) { - const fetchedFileStructure = await fetch( - `/api/filestructure?path=${curProject.projectPath}`, - { - method: 'GET', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - } - ) - .then((response) => response.json()) - .then((data) => { - return data; // JSON data parsed by `data.json()` call - }); - setFileStructure(fetchedFileStructure); + + if (!fileStructure || fileStructure.length === 0) { + try { + const response = await fetch( + `/api/filestructure?path=${curProject.projectPath}`, + { + method: 'GET', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + } + ); + const fetchedFileStructure = await response.json(); + + console.log('Fetched File Structure:', fetchedFileStructure.res); + + tempFileStructure = fetchedFileStructure.res; + setFileStructure(tempFileStructure); + } catch (error) { + console.error('Error fetching file structure:', error); + } } + + console.log('Start task'); + console.log('Assigned Task:', assignedTask); + console.log('File Structure:', tempFileStructure); + prompt = assignedTask === 'debug' - ? findbugPrompt(description, fileStructure) + ? findbugPrompt(description, tempFileStructure) : assignedTask === 'refactor' - ? refactorPrompt(description, fileStructure) - : optimizePrompt(description, fileStructure); + ? refactorPrompt(description, tempFileStructure) + : optimizePrompt(description, tempFileStructure); + promptInput = { chatId: input.chatId, message: prompt, model: input.model, role: 'assistant', - }; + } as unknown as ChatInputType; + console.log('Prompt Input:', promptInput); setTaskStep('read_file'); - await triggerChat({ variables: { promptInput } }); + setStreamStatus(StreamStatus.STREAMING); + setSubscription({ + enabled: true, + variables: { input: promptInput }, + }); + await new Promise((resolve) => setTimeout(resolve, 100)); + await triggerChat({ variables: { input: promptInput } }); break; } }, @@ -386,93 +483,166 @@ export function useChatStream({ ); const editFileAgent = useCallback( - async (input: ChatInput) => { - const filePaths = JSON.parse(input.message).files; - const fileContentsPromises = filePaths.map(async (filePath: string) => { - if (!fileContents[filePath]) { - const fetchedFileContent = await fetch(`/api/file?path=${filePath}`, { - method: 'GET', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - }) - .then((response) => response.json()) - .then((data) => { - return data.content; // JSON data parsed by `data.json()` call - }); - setFileContents((prev) => ({ - ...prev, - [filePath]: fetchedFileContent, - })); - setOriginalFileContents((prev) => ({ - ...prev, - [filePath]: fetchedFileContent, - })); - } - return fileContents[filePath]; - }); + async (input: ChatInputType) => { + console.log('editFileAgent called with input:', input); + const filePaths: string[] = JSON.parse(input.message).files; + // Create a local object to store file contents, keyed by filePath + const currentFileContents: { [key: string]: string } = { + ...fileContents, + }; - const allFileContents = await Promise.all(fileContentsPromises); - const prompt = editFilePrompt( - taskDescription, - allFileContents.join('\n') + await Promise.all( + filePaths.map(async (filePath: string) => { + if (!currentFileContents[filePath]) { + const fpath = path.join(curProject.projectPath, filePath); + const fetchedFileContent = await fetch( + `/api/file?path=${encodeURIComponent(fpath)}`, + { + method: 'GET', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + } + ) + .then((response) => response.json()) + .then((data) => data.content); + console.log('Fetched File Content:', fetchedFileContent); + console.log('File Path:', filePath); + // Update the local object with the new key-value pair + currentFileContents[filePath] = fetchedFileContent; + } + }) ); - const promptInput: ChatInput = { + + setFileContents(currentFileContents); + setOriginalFileContents((prev) => ({ ...prev, ...currentFileContents })); + + console.log('All File Contents:', currentFileContents); + // Pass the object (mapping filePath to content) to the prompt function + const prompt = editFilePrompt(taskDescription, currentFileContents); + const promptInput: ChatInputType = { chatId: input.chatId, message: prompt, model: input.model, role: 'assistant', }; + console.log('Prompt Input:', promptInput); setTaskStep('edit_file'); - await triggerChat({ variables: { promptInput } }); + setStreamStatus(StreamStatus.STREAMING); + setSubscription({ + enabled: true, + variables: { input: promptInput }, + }); + await new Promise((resolve) => setTimeout(resolve, 100)); + await triggerChat({ variables: { input: promptInput } }); }, - [fileContents, taskDescription] + [fileContents, taskDescription, curProject, triggerChat] ); - const applyChangesAgent = useCallback(async (input: ChatInput) => { - const changes = JSON.parse(input.message); - const updatePromises = Object.keys(changes).map(async (filePath) => { - const newContent = changes[filePath]; - await fetch('/api/file', { - method: 'POST', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filePath, newContent }), + const applyChangesAgent = useCallback( + async (input: ChatInputType) => { + console.log('applyChangesAgent called with input:', input); + const updatePromises = Object.keys(newFileContents).map( + async (filePath) => { + const newContent = newFileContents[filePath]; + console.log('Updating file:', filePath); + console.log('New Content:', newContent); + const fpath = path.join(curProject.projectPath, filePath); + await fetch('/api/file', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filePath: fpath, newContent }), + }); + } + ); + + await Promise.all(updatePromises); + setTaskStep('apply_changes'); + toast.success('Changes applied successfully'); + commitChangesAgent({ + chatId: input.chatId, + message: JSON.stringify(newFileContents), + model: input.model, + role: 'assistant', }); - }); + }, + [newFileContents] + ); - await Promise.all(updatePromises); - setTaskStep('apply_changes'); - toast.success('Changes applied successfully'); - commitChangesAgent({ - chatId: input.chatId, - message: JSON.stringify(changes), - model: input.model, - role: 'assistant', - }); - }, []); + const codeReviewAgent = useCallback(async (input: ChatInputType) => { + console.log('codeReviewAgent called with input:', input); + + let parsedMessage: { [key: string]: string } = {}; - const codeReviewAgent = useCallback(async (input: ChatInput) => { - const prompt = codeReviewPrompt(input.message); - const promptInput: ChatInput = { + try { + if (typeof input.message === 'string') { + const parsedObject = JSON.parse(input.message); + + if ( + parsedObject.modified_files && + typeof parsedObject.modified_files === 'object' + ) { + parsedMessage = parsedObject.modified_files; + } else { + console.error('modified_files is missing or not an object'); + } + } + } catch (error) { + console.error('Failed to parse input.message:', error); + } + + console.log('Parsed modified_files:', parsedMessage); + + console.log('Parsed Message:', parsedMessage); + + setNewFileContents((prev) => ({ ...prev, ...parsedMessage })); + + const formattedMessage = Object.entries(parsedMessage) + .map( + ([filePath, content]) => + `### File: ${filePath}\n\`\`\`\n${content}\n\`\`\`` + ) + .join('\n\n'); + + const prompt = codeReviewPrompt(formattedMessage); + + const promptInput: ChatInputType = { chatId: input.chatId, message: prompt, model: input.model, role: 'assistant', }; + + console.log('Prompt Input:', promptInput); setTaskStep('code_review'); - await triggerChat({ variables: { promptInput } }); + setStreamStatus(StreamStatus.STREAMING); + setSubscription({ + enabled: true, + variables: { input: promptInput }, + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + await triggerChat({ variables: { input: promptInput } }); }, []); - const commitChangesAgent = useCallback(async (input: ChatInput) => { + const commitChangesAgent = useCallback(async (input: ChatInputType) => { + console.log('commitChangesAgent called with input:', input); const prompt = commitChangesPrompt(input.message); - const promptInput: ChatInput = { + const promptInput: ChatInputType = { chatId: input.chatId, message: prompt, model: input.model, role: 'assistant', }; + console.log('Prompt Input:', promptInput); setTaskStep('commit'); - await triggerChat({ variables: { promptInput } }); + setStreamStatus(StreamStatus.STREAMING); + setSubscription({ + enabled: true, + variables: { input: promptInput }, + }); + await new Promise((resolve) => setTimeout(resolve, 100)); + await triggerChat({ variables: { input: promptInput } }); }, []); return { diff --git a/frontend/src/utils/parser.ts b/frontend/src/utils/parser.ts index 9aa5c809..4da73609 100644 --- a/frontend/src/utils/parser.ts +++ b/frontend/src/utils/parser.ts @@ -1,12 +1,20 @@ export const parseXmlToJson = (xmlString: string) => { - const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); - - const jsonText = xmlDoc.getElementsByTagName('jsonResponse')[0]?.textContent; - - if (!jsonText) { + console.log(xmlString); + const match = xmlString.match(/([\s\S]*?)<\/jsonResponse>/); + console.log(match); + console.log(match[1]); + if (!match || !match[1]) { throw new Error('Invalid XML: No element found'); } - return JSON.parse(jsonText); + const jsonText = match[1].trim(); + console.log(jsonText); + + try { + return JSON.parse(jsonText); + } catch (error) { + throw new Error( + 'Invalid JSON: Failed to parse content inside ' + error + ); + } }; From 96eef24eb8c89d0e2b8e6f46895fe078dea9ec4c Mon Sep 17 00:00:00 2001 From: NarwhalChen Date: Sat, 8 Mar 2025 19:05:25 -0600 Subject: [PATCH 03/19] fix: fix typewritter bug --- frontend/src/hooks/useChatStream.ts | 74 ++++++++++++++++------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/frontend/src/hooks/useChatStream.ts b/frontend/src/hooks/useChatStream.ts index cb192b7e..e688b709 100644 --- a/frontend/src/hooks/useChatStream.ts +++ b/frontend/src/hooks/useChatStream.ts @@ -182,41 +182,47 @@ export function useChatStream({ }, }); - const typewriterEffect = async (textArray: string[], delay: number) => { - let index = 0; - - // implement typewriter effect - const updateMessage = async () => { - if (index < textArray.length) { - setMessages((prev) => { - const lastMsg = prev[prev.length - 1]; - return lastMsg?.role === 'assistant' - ? [ - ...prev.slice(0, -1), - { - ...lastMsg, - content: lastMsg.content + textArray[index], - }, - ] - : [ - ...prev, - { - id: chatStream.id, - role: 'assistant', - content: textArray[index], - createdAt: new Date( - chatStream.created * 1000 - ).toISOString(), - }, - ]; - }); - - index++; - setTimeout(updateMessage, delay); - } - }; + const typewriterEffect = async ( + textArray: string[], + delay: number + ): Promise => { + return new Promise((resolve) => { + let index = 0; + + const updateMessage = () => { + if (index < textArray.length) { + setMessages((prev) => { + const lastMsg = prev[prev.length - 1]; + return lastMsg?.role === 'assistant' + ? [ + ...prev.slice(0, -1), + { + ...lastMsg, + content: lastMsg.content + textArray[index], + }, + ] + : [ + ...prev, + { + id: chatStream.id, + role: 'assistant', + content: textArray[index], + createdAt: new Date( + chatStream.created * 1000 + ).toISOString(), + }, + ]; + }); + + index++; + setTimeout(updateMessage, delay); + } else { + resolve(); + } + }; - await updateMessage(); + updateMessage(); + }); }; // break text into chunks of 3 characters From 8df02620db69e9f6bbae73b5dee73ee356f8c84b Mon Sep 17 00:00:00 2001 From: Narwhal Date: Sat, 8 Mar 2025 19:48:19 -0600 Subject: [PATCH 04/19] fix: update nullable fields in chat models and improve stream handling logic --- backend/src/chat/chat.model.ts | 20 ++++---- .../model-provider/openai-model-provider.ts | 21 +++++++- frontend/src/graphql/schema.gql | 17 ++----- frontend/src/hooks/useChatStream.ts | 51 +++++++++++-------- frontend/src/utils/file-reader.ts | 5 +- 5 files changed, 67 insertions(+), 47 deletions(-) diff --git a/backend/src/chat/chat.model.ts b/backend/src/chat/chat.model.ts index d734ac5c..4ecac50c 100644 --- a/backend/src/chat/chat.model.ts +++ b/backend/src/chat/chat.model.ts @@ -75,11 +75,11 @@ class ChatCompletionDelta { @ObjectType('ChatCompletionChoiceType') class ChatCompletionChoice { - @Field() - index: number; + @Field({ nullable: true }) + index: number | null; - @Field(() => ChatCompletionDelta) - delta: ChatCompletionDelta; + @Field(() => ChatCompletionDelta, { nullable: true }) + delta: ChatCompletionDelta | null; @Field({ nullable: true }) finishReason: string | null; @@ -90,14 +90,14 @@ export class ChatCompletionChunk { @Field() id: string; - @Field() - object: string; + @Field({ nullable: true }) + object: string | null; - @Field() - created: number; + @Field({ nullable: true }) + created: number | null; - @Field() - model: string; + @Field({ nullable: true }) + model: string | null; @Field({ nullable: true }) systemFingerprint: string | null; diff --git a/backend/src/common/model-provider/openai-model-provider.ts b/backend/src/common/model-provider/openai-model-provider.ts index f5991cc6..1e92dbaf 100644 --- a/backend/src/common/model-provider/openai-model-provider.ts +++ b/backend/src/common/model-provider/openai-model-provider.ts @@ -118,7 +118,7 @@ export class OpenAIModelProvider implements IModelProvider { let streamIterator: AsyncIterator | null = null; const modelName = model || input.model; const queue = this.getQueueForModel(modelName); - + let oldStreamValue: OpenAIChatCompletionChunk | null = null; const createStream = async () => { if (!stream) { const result = await queue.add(async () => { @@ -145,6 +145,9 @@ export class OpenAIModelProvider implements IModelProvider { const currentIterator = await createStream(); const chunk = await currentIterator.next(); const chunkValue = chunk.value as OpenAIChatCompletionChunk; + console.log('isDone:', chunk.done); + console.log('chunk:', chunk); + if (!chunk.done) oldStreamValue = chunkValue; return { done: chunk.done, value: { @@ -159,9 +162,23 @@ export class OpenAIModelProvider implements IModelProvider { } }, async return() { + console.log(stream); + console.log(streamIterator); + console.log('return() called'); stream = null; streamIterator = null; - return { done: true, value: undefined }; + return { + done: true, + value: { + ...oldStreamValue, + status: StreamStatus.DONE, + choices: [ + { + finishReason: 'stop', + }, + ], + }, + }; }, async throw(error) { stream = null; diff --git a/frontend/src/graphql/schema.gql b/frontend/src/graphql/schema.gql index e1263e91..5612a2be 100644 --- a/frontend/src/graphql/schema.gql +++ b/frontend/src/graphql/schema.gql @@ -55,9 +55,7 @@ input CreateProjectInput { public: Boolean } -""" -Date custom scalar type -""" +"""Date custom scalar type""" scalar Date type EmailConfirmationResponse { @@ -148,9 +146,7 @@ type Project { projectPath: String! subNumber: Float! - """ - Projects that are copies of this project - """ + """Projects that are copies of this project""" subscribers: [Project!] uniqueProjectId: String! updatedAt: Date! @@ -232,9 +228,7 @@ input UpdateProjectPhotoInput { projectId: ID! } -""" -The `Upload` scalar type represents a file upload. -""" +"""The `Upload` scalar type represents a file upload.""" scalar Upload type User { @@ -247,8 +241,7 @@ type User { isEmailConfirmed: Boolean! lastEmailSendTime: Date! projects: [Project!]! - subscribedProjects: [Project!] - @deprecated(reason: "Use projects with forkedFromId instead") + subscribedProjects: [Project!] @deprecated(reason: "Use projects with forkedFromId instead") updatedAt: Date! username: String! -} +} \ No newline at end of file diff --git a/frontend/src/hooks/useChatStream.ts b/frontend/src/hooks/useChatStream.ts index e688b709..411fca5f 100644 --- a/frontend/src/hooks/useChatStream.ts +++ b/frontend/src/hooks/useChatStream.ts @@ -152,8 +152,8 @@ export function useChatStream({ useSubscription(CHAT_STREAM, { skip: !subscription.enabled || !subscription.variables, variables: subscription.variables, - onSubscriptionData: async ({ subscriptionData }) => { - const chatStream = subscriptionData?.data?.chatStream; + onData: async ({ data }) => { + const chatStream = data?.data?.chatStream; if (!chatStream) return; @@ -166,6 +166,7 @@ export function useChatStream({ if (content) { setCumulatedContent((prev) => prev + content); } + if (chatStream.status == StreamStatus.DONE) { setStreamStatus(StreamStatus.DONE); finishChatResponse(); @@ -193,25 +194,33 @@ export function useChatStream({ if (index < textArray.length) { setMessages((prev) => { const lastMsg = prev[prev.length - 1]; - return lastMsg?.role === 'assistant' - ? [ - ...prev.slice(0, -1), - { - ...lastMsg, - content: lastMsg.content + textArray[index], - }, - ] - : [ - ...prev, - { - id: chatStream.id, - role: 'assistant', - content: textArray[index], - createdAt: new Date( - chatStream.created * 1000 - ).toISOString(), - }, - ]; + + if ( + lastMsg?.role === 'assistant' && + lastMsg.id === chatStream.id + ) { + // **如果当前段落还在继续,就拼接内容** + return [ + ...prev.slice(0, -1), + { + ...lastMsg, + content: lastMsg.content + textArray[index], // 继续拼接 + }, + ]; + } else { + // **如果 `id` 变化(即进入新段落),新建一条消息** + return [ + ...prev, + { + id: chatStream.id, + role: 'assistant', + content: textArray[index], // 这个是新段落的第一句 + createdAt: new Date( + chatStream.created * 1000 + ).toISOString(), + }, + ]; + } }); index++; diff --git a/frontend/src/utils/file-reader.ts b/frontend/src/utils/file-reader.ts index 7a5b0385..aec3c8a9 100644 --- a/frontend/src/utils/file-reader.ts +++ b/frontend/src/utils/file-reader.ts @@ -43,7 +43,6 @@ export class FileReader { try { const items = await fs.readdir(dir, { withFileTypes: true }); - for (const item of items) { if (item.name.includes('node_modules')) continue; const fullPath = path.join(dir, item.name); @@ -87,7 +86,9 @@ export class FileReader { console.log(`📝 [FileReader] Updating file: ${fullPath}`); try { - const content = JSON.parse(newContent); + const content = newContent.trim(); + console.log(content); + console.log('log content'); await fs.writeFile(fullPath, content, 'utf-8'); console.log('[FileReader] File updated successfully'); From e1355005fd47a2954c7aaaaf1b475b7ba4333579 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Mon, 17 Mar 2025 15:24:18 -0500 Subject: [PATCH 05/19] feat: adding interactive chat and update chatstream --- backend/src/chat/chat.controller.ts | 2 + frontend/src/api/ChatStreamAPI.ts | 71 ++++ frontend/src/graphql/schema.gql | 1 + frontend/src/graphql/type.tsx | 1 + .../hooks/{ => multi-agent}/agentPrompt.ts | 230 +++++++++++-- .../src/hooks/multi-agent/managerAgent.ts | 200 ++++++++++++ frontend/src/hooks/multi-agent/toolNodes.ts | 130 ++++++++ frontend/src/hooks/multi-agent/tools.ts | 308 ++++++++++++++++++ frontend/src/hooks/useChatStream.ts | 115 ++----- frontend/src/utils/parser.ts | 3 + 10 files changed, 944 insertions(+), 117 deletions(-) create mode 100644 frontend/src/api/ChatStreamAPI.ts rename frontend/src/hooks/{ => multi-agent}/agentPrompt.ts (59%) create mode 100644 frontend/src/hooks/multi-agent/managerAgent.ts create mode 100644 frontend/src/hooks/multi-agent/toolNodes.ts create mode 100644 frontend/src/hooks/multi-agent/tools.ts diff --git a/backend/src/chat/chat.controller.ts b/backend/src/chat/chat.controller.ts index c1a5e6ef..7ee0fa97 100644 --- a/backend/src/chat/chat.controller.ts +++ b/backend/src/chat/chat.controller.ts @@ -39,6 +39,7 @@ export class ChatController { chatId: chatDto.chatId, message: chatDto.message, model: chatDto.model, + role: MessageRole.User, }); let fullResponse = ''; @@ -66,6 +67,7 @@ export class ChatController { chatId: chatDto.chatId, message: chatDto.message, model: chatDto.model, + role: MessageRole.User, }); // Save the complete message diff --git a/frontend/src/api/ChatStreamAPI.ts b/frontend/src/api/ChatStreamAPI.ts new file mode 100644 index 00000000..1caff4c9 --- /dev/null +++ b/frontend/src/api/ChatStreamAPI.ts @@ -0,0 +1,71 @@ +import { ChatInputType } from '@/graphql/type'; + +export const startChatStream = async ( + input: ChatInputType, + token: string, + stream: boolean = false // Default to non-streaming for better performance +): Promise => { + if (!token) { + throw new Error('Not authenticated'); + } + const { chatId, message, model } = input; + const response = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + chatId, + message, + model, + stream, + }), + }); + + if (!response.ok) { + throw new Error( + `Network response was not ok: ${response.status} ${response.statusText}` + ); + } + // TODO: Handle streaming responses properly + // if (stream) { + // // For streaming responses, aggregate the streamed content + // let fullContent = ''; + // const reader = response.body?.getReader(); + // if (!reader) { + // throw new Error('No reader available'); + // } + + // while (true) { + // const { done, value } = await reader.read(); + // if (done) break; + + // const text = new TextDecoder().decode(value); + // const lines = text.split('\n\n'); + + // for (const line of lines) { + // if (line.startsWith('data: ')) { + // const data = line.slice(5); + // if (data === '[DONE]') break; + // try { + // const { content } = JSON.parse(data); + // if (content) { + // fullContent += content; + // } + // } catch (e) { + // console.error('Error parsing SSE data:', e); + // } + // } + // } + // } + // return fullContent; + // } else { + // // For non-streaming responses, return the content directly + // const data = await response.json(); + // return data.content; + // } + + const data = await response.json(); + return data.content; +}; diff --git a/frontend/src/graphql/schema.gql b/frontend/src/graphql/schema.gql index df7e472c..28ee099d 100644 --- a/frontend/src/graphql/schema.gql +++ b/frontend/src/graphql/schema.gql @@ -201,6 +201,7 @@ type RefreshTokenResponse { } input RegisterUserInput { + confirmPassword: String! email: String! password: String! username: String! diff --git a/frontend/src/graphql/type.tsx b/frontend/src/graphql/type.tsx index b748e273..edcc54d4 100644 --- a/frontend/src/graphql/type.tsx +++ b/frontend/src/graphql/type.tsx @@ -358,6 +358,7 @@ export type RefreshTokenResponse = { }; export type RegisterUserInput = { + confirmPassword: Scalars['String']['input']; email: Scalars['String']['input']; password: Scalars['String']['input']; username: Scalars['String']['input']; diff --git a/frontend/src/hooks/agentPrompt.ts b/frontend/src/hooks/multi-agent/agentPrompt.ts similarity index 59% rename from frontend/src/hooks/agentPrompt.ts rename to frontend/src/hooks/multi-agent/agentPrompt.ts index 3bacc0b7..97241291 100644 --- a/frontend/src/hooks/agentPrompt.ts +++ b/frontend/src/hooks/multi-agent/agentPrompt.ts @@ -1,3 +1,34 @@ +export enum TaskType { + DEBUG = 'debug', + REFACTOR = 'refactor', + OPTIMIZE = 'optimize', +} +export interface AgentContext { + task_type: TaskType; // Current task type + request: string; // Original request + projectPath: string; // Project ID + fileStructure: string[]; // Project file structure + fileContents: { + // File content mapping + [key: string]: string; + }; + modifiedFiles: { + // Modified files + [key: string]: string; + }; + requiredFiles: string[]; // Files that need to be read/modified + reviewComments?: string[]; // Code review comments + commitMessage?: string; // Commit message + currentStep?: { + // Current execution step + tool?: string; // Tool being used + status?: string; // Execution status + description?: string; // Step description + }; + setMessages: React.Dispatch>; + saveMessage: any; + token?: string; +} export const systemPrompt = (): string => { return `# System Instructions You are an AI assistant. When responding, you **must** adhere to the following rules: @@ -13,7 +44,7 @@ You are an AI assistant. When responding, you **must** adhere to the following r 3. **Example of Expected Output Format** { - "files": ["src/hooks/useChatStream.ts", "src/components/ChatInput.tsx"], + "files": ["frontend/src/hooks/useChatStream.ts", "frontend/src/components/ChatInput.tsx"], "thinking_process": "The bug description suggests an issue with state updates in the chat stream. Since 'useChatStream.ts' handles chat messages, it is the primary suspect, along with 'ChatInput.tsx', which interacts with the message state." } - The JSON must contain **only the relevant fields**. @@ -23,8 +54,8 @@ You are an AI assistant. When responding, you **must** adhere to the following r - The output must be formatted as follows: { "modified_files": { - "src/hooks/useChatStream.ts": "Updated code content...", - "src/components/ChatInput.tsx": "Updated code content..." + "frontend/src/hooks/useChatStream.ts": "Updated code content...", + "frontend/src/components/ChatInput.tsx": "Updated code content..." }, "thinking_process": "After reviewing the bug description, I identified that the issue is caused by an incomplete dependency array in useEffect, preventing state updates. I have modified the code accordingly." } @@ -99,7 +130,7 @@ export const refactorPrompt = (message: string, file_structure: string[]) => { return ( systemPrompt + `You are a seasoned software engineer known for your expertise in code refactoring. Your mission is to refactor the codebase to improve its structure, readability, and maintainability. - + and also provide a detailed refactoring description. --- ### **Mission Objective:** - The user has requested a **Code Refactor** and provided a description along with the project's file structure. @@ -125,7 +156,8 @@ Finally, I will ensure that the refactoring plan maintains the same functionalit ### **Output Format** { - "files": ["src/hooks/useChatStream.ts", "src/utils/chatUtils.ts"], + "files": ["frontend/src/hooks/useChatStream.ts", "frontend/src/utils/chatUtils.ts"], + "description": "The user requested refactoring of message handling logic. Since chat state management is handled in 'useChatStream.ts', I suggest extracting reusable logic into a new utility file, 'chatUtils.ts'.", "thinking_process": "The user requested refactoring of message handling logic. Since chat state management is handled in 'useChatStream.ts', I suggest extracting reusable logic into a new utility file, 'chatUtils.ts'." } @@ -136,7 +168,7 @@ export const optimizePrompt = (message: string, file_structure: string[]) => { return ( systemPrompt + `You are a code performance optimization expert. Your mission is to analyze the codebase and identify performance bottlenecks. - + and also provide a detailed optimization desciption. --- ### **Mission Objective:** @@ -162,7 +194,8 @@ Once I determine the likely causes of inefficiency, I will propose specific solu ### **Output Format** { - "files": ["path/to/index.jsx", "path/to/utils.js"], + "files": ["frontend/src/components/index.tsx", "frontend/src/utils/chat.ts"], + "description" : "The user's optimization request suggests that the ChatList component is re-rendering too frequently. Since ChatList.tsx is responsible for displaying messages, applying React.memo to prevent unnecessary updates will likely improve performance. Additionally, optimizing state updates in useChatStream.ts will help reduce redundant renders.", "thinking_process": "The user's optimization request suggests that the ChatList component is re-rendering too frequently. Since ChatList.tsx is responsible for displaying messages, applying React.memo to prevent unnecessary updates will likely improve performance. Additionally, optimizing state updates in useChatStream.ts will help reduce redundant renders." } @@ -175,42 +208,65 @@ export const editFilePrompt = ( ) => { return ( systemPrompt + - `You are a senior software engineer. Your mission is to edit the code files to fix the issue described by the user. + `You are a senior software engineer with extensive experience in React and TypeScript. Your mission is to edit the code files while preserving all existing functionality and structure. --- -### **Mission Objective:** -- The user has provided a description of the issue and the relevant code files. -- Your task is to edit the code files to fix the issue. +### **Mission Objective:** +- The user has provided a description of the issue and the relevant code files. +- Your task is to edit the code files to fix the issue while maintaining all existing code functionality. +- You must preserve all existing imports, components, functions, and features unless explicitly told to remove them. +- Any modifications should be surgical and focused only on the specific issue being fixed. --- -### **User-Provided Information:** -- **Description:** +### **User-Provided Information:** +- **Description:** ${description} -- **Code Content:** +- **Code Content:** ${JSON.stringify(file_content, null, 2)} ### **AI Thought Process:** -To correctly fix the issue, I will first analyze the provided description to understand **the nature of the bug or enhancement**. -I will then examine the affected code files and locate the exact section where modifications are required. -If the issue is related to **state management**, I will check for missing updates or incorrect dependencies. -If the issue involves **API requests**, I will verify if the request format aligns with the expected schema and handle potential errors. -Once the necessary fix is identified, I will modify the code while ensuring **the change does not introduce regressions**. -Before finalizing the fix, I will ensure that the updated code maintains **existing functionality and adheres to project coding standards**. +1. First, I will carefully analyze the provided description to understand **the specific issue that needs to be fixed**. +2. I will examine the code files while noting all existing: + - Imports and dependencies + - Component structures and hierarchies + - State management and hooks + - Props and type definitions + - Existing functionality and features +3. I will identify the minimal set of changes needed to fix the issue. +4. When making modifications, I will: + - Preserve all existing imports + - Maintain component structure and naming + - Keep all existing functionality intact + - Only modify code directly related to the issue +5. Before finalizing, I will verify that: + - All original features are preserved + - The fix addresses the specific issue + - Code style and standards are maintained + - No unintended side effects are introduced + +--- + +### **Strict Requirements:** +1. NEVER remove existing imports unless explicitly told to +2. NEVER remove existing components or functions unless explicitly told to +3. NEVER simplify or reduce existing code unless explicitly told to +4. ALL changes must be surgical and focused only on the specific issue +5. Return the COMPLETE updated code with ALL original functionality preserved --- ### **Output Format** { "modified_files": { - "file/path.tsx": "Updated code content", - "file/path.js": "Updated code content" + "frontend/src/components/file.tsx": "// Complete updated code with ALL original functionality preserved\nimport React from 'react';\n// ... rest of the imports\n\n// All original components and code preserved", + "frontend/src/utils/file.ts": "// Complete updated code\n// No removal of existing functionality" }, - "thinking_process": "After reviewing the provided description and code, I identified that the issue is caused by an incomplete dependency array in useEffect, preventing the state from updating correctly. I added 'messages' as a dependency to ensure synchronization." + "thinking_process": "After analyzing the code, I identified the specific issue. The fix has been implemented while preserving all existing functionality, including [list specific preserved features]. The changes only affect [describe specific changes], and all other code remains intact." } -Failure is Not an Option! If you fail, the issue will remain unresolved.` +Failure is Not an Option! The code must be fixed while preserving ALL existing functionality.` ); }; export const codeReviewPrompt = (message: string) => { @@ -288,6 +344,122 @@ The final commit message will follow **conventional commit standards** to ensure Failure is Not an Option! If you fail, the changes will not be committed.` ); }; +export const taskPrompt = (message: string): string => { + return ( + systemPrompt() + + `You are an expert task analyzer responsible for understanding user requirements and planning the development approach. + +### User Request: +${message} + +### Available Task Types: +1. DEBUG: Fix issues, errors, or unexpected behavior + - Example: Runtime errors, type errors, incorrect functionality + - Focus: Problem resolution and stability + +2. REFACTOR: Improve code structure without changing behavior + - Example: Code organization, modularity, readability + - Focus: Maintainability and code quality + +3. OPTIMIZE: Enhance performance or efficiency + - Example: Reduce render times, improve state management + - Focus: Performance and resource utilization + +### Analysis Requirements: +1. Consider the problem description carefully +2. Look for keywords indicating the task type +3. Identify specific areas that need attention +4. Plan the general approach to solving the issue + +### Output Format: +{ + "task_type": "debug" | "refactor" | "optimize", + "description": "Detailed description of what needs to be done", + "analysis": { + "problem_area": "Specific component or functionality affected", + "key_points": ["List of main issues or improvements needed"], + "approach": "General strategy for addressing the task" + } +} + +Remember: Your analysis sets the direction for the entire development process. Be specific and thorough.` + ); +}; + +import { Message } from '@/const/MessageType'; +import { getToolUsageMap } from './toolNodes'; + +export const confirmationPrompt = ( + taskType: string, + context: AgentContext +): string => { + const toolUsageMap = getToolUsageMap(); + const toolsDescription = Object.entries(toolUsageMap) + .map(([toolName, usage], index) => { + return `${index + 1}. ${toolName}: + - Use for: ${usage.useFor} + - Input: ${usage.input} + - Output: ${usage.output} + - When to use: ${usage.whenToUse} + - Next step: ${usage.nextStep || 'Task completion'}`; + }) + .join('\n\n'); + + return ( + systemPrompt() + + `You are a task manager AI responsible for coordinating the development workflow. Your job is to analyze the current state and decide the next action. + +### Current Context: +1. Original Request: ${context.request} +2. Task Type: ${context.task_type} +3. Current Step: ${JSON.stringify(context.currentStep, null, 2)} +4. Project Files: ${JSON.stringify(context.fileStructure, null, 2)} +5. Modified Files: ${JSON.stringify(context.modifiedFiles, null, 2)} +6. File Contents: ${JSON.stringify(context.fileContents, null, 2)} +${context.reviewComments ? `6. Review Comments: ${JSON.stringify(context.reviewComments, null, 2)}` : ''} +${context.commitMessage ? `7. Commit Message: ${context.commitMessage}` : ''} + +### Available Tools: +${toolsDescription} + +### Decision Making Process: +IMPORTANT: If task_type is already specified (${context.task_type}), DO NOT use taskTool - skip directly to file handling! + +1. Start here: + - If task_type is NOT specified: use taskTool first + - If task_type IS specified: skip to step 2 +2. Before any file modifications: + - If context.fileContents is empty, MUST use readFileTool first + - NEVER proceed to editFileTool without file contents + - Always check Object.keys(context.fileContents).length before editing +3. Once file contents are ready: + - Use editFileTool to make necessary changes + - Use codeReviewTool to verify changes +4. After successful review: + - Use applyChangesTool to apply changes + - Use commitChangesTool to prepare commit +5. Critical Rules (In Order of Priority): + - FIRST: If task_type is specified (current: ${context.task_type}), NEVER use taskTool + - SECOND: NEVER edit files without contents (check context.fileContents) + - THIRD: If fileContents empty but needed, use readFileTool next + - FOURTH: Only proceed to editFileTool when file contents are available + - LAST: If any step fails, return to appropriate previous step + +### Output Format: +{ + "completed": boolean, + "next_step": { + "tool": "taskTool" | "editFileTool" | "codeReviewTool" | "applyChangesTool" | "commitChangesTool", + "description": "Detailed description of what needs to be done", + "files": ["file/paths/if/needed"] + }, + "thinking_process": "Explanation of why this step is needed and how it helps progress the task" +} + +Make your decision based on the current context and ensure a logical progression through the development workflow.` + ); +}; + export const findbugPrompt = (message: string, file_structure: string[]) => { return ( systemPrompt + @@ -297,7 +469,7 @@ export const findbugPrompt = (message: string, file_structure: string[]) => { ### **Mission Objective:** The user has encountered a **Bug** and provided a description along with the project's file structure. -Your task is to analyze the potential source of the Bug and return a **list of affected file paths** where the issue might be occurring. +Your task is to analyze the potential source of the Bug and return a **list of affected file paths** where the issue might be occurring and also provide detailed description about that bug. --- @@ -307,15 +479,11 @@ Your task is to analyze the potential source of the Bug and return a **list of a - **Project File Structure:** ${file_structure} -### **AI Thought Process:** -After analyzing the bug description, I'll locate the files most likely involved in this issue. -For instance, if the error is related to **state management**, I'll check relevant **hooks or context files**. -If it's a UI bug, I'll inspect **component files**. -Once I determine the affected files, I'll prioritize them based on their likelihood of containing the issue. ### **Output Format** { - "files": ["path/to/index.tsx", "path/to/utils.js"], + "files": ["frontend/src/components/chat/ChatList.tsx", "frontend/src/utils/chat.ts"], + "description": "The bug occurs when the chat messages are not updating in real-time. The user expects the messages to be displayed instantly after sending a message, but there is a delay in the update.", "thinking_process": "Based on the bug description, the issue involves React's state updates not propagating correctly. This suggests that the problem is likely in the useChatStream.ts hook or the context provider managing state." } diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts new file mode 100644 index 00000000..6aefa603 --- /dev/null +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -0,0 +1,200 @@ +import { ChatInputType } from '@/graphql/type'; +import { startChatStream } from '@/api/ChatStreamAPI'; +import { findToolNode } from './toolNodes'; +import { + taskPrompt, + confirmationPrompt, + AgentContext, + TaskType, +} from './agentPrompt'; +import path from 'path'; +import { parseXmlToJson } from '@/utils/parser'; +import { Message } from '@/const/MessageType'; + +/** + * Normalize file paths. + */ +function normalizePath(filePath: string): string { + return path.normalize(filePath).replace(/\\/g, '/'); +} + +/** + * Validate and normalize file list. + */ +function validateFiles(files: string[]): string[] { + return files.map((file) => { + const normalized = normalizePath(file); + if (!normalized || normalized.includes('..')) { + throw new Error(`Invalid file path: ${file}`); + } + return normalized; + }); +} + +/** + * The Manager Agent is responsible for: + * 1. Initializing the context. + * 2. Letting AI determine each step of the process. + * 3. Executing the tool selected by AI. + * 4. Repeating until AI determines the task is complete. + */ +export async function managerAgent( + input: ChatInputType, + setMessages: React.Dispatch>, + projectPath: string, + saveMessage: any, + token: string +): Promise { + console.log('managerAgent called with input:', input); + + try { + // Initialize context + const context: AgentContext = { + task_type: TaskType.DEBUG, // Initial type, AI will update it later + request: input.message, // Store the original request + projectPath, + fileStructure: [], + fileContents: {}, + modifiedFiles: {}, + requiredFiles: [], // Initialize empty array for required files + currentStep: { + tool: undefined, + status: 'initializing', + description: 'Starting task analysis', + }, + setMessages, + saveMessage, + token, + }; + + // Retrieve project file structure + try { + const response = await fetch( + `/api/filestructure?path=${encodeURIComponent(projectPath)}`, + { + method: 'GET', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + } + ); + const data = await response.json(); + context.fileStructure = validateFiles(data.res); + console.log('Initialized file structure:', context.fileStructure); + } catch (error) { + console.error('Error fetching file structure:', error); + throw error; + } + + const MAX_ITERATIONS = 60; + const TIMEOUT = 30 * 60 * 1000; // 30-minute timeout + const startTime = Date.now(); + let iteration = 0; + + // Main loop: let AI determine each step + while (iteration < MAX_ITERATIONS && Date.now() - startTime < TIMEOUT) { + // Get AI's decision + const confirmationPromptStr = confirmationPrompt( + context.task_type, + context + ); + const response = await startChatStream( + { + chatId: input.chatId, + message: confirmationPromptStr, + model: input.model, + role: `assistant-${context.task_type}`, + }, + token + ); + + // Parse AI's response using `parseXmlToJson` + let decision; + try { + decision = parseXmlToJson(response); + console.log('Parsed AI Decision:', decision); + } catch (error) { + console.error('Error parsing AI response:', error); + throw error; + } + + // If task is complete, exit loop + if (decision.completed) { + break; + } + + // Update the current step in context + context.currentStep = { + tool: decision.next_step.tool, + status: 'executing', + description: decision.next_step.description, + }; + + console.log('required Files:', decision.next_step.files); + context.requiredFiles = [ + ...context.requiredFiles, + ...decision.next_step.files.filter( + (file) => !context.requiredFiles.includes(file) + ), + ]; + + // Find and execute the tool + const toolNode = findToolNode(decision.next_step.tool); + if (!toolNode) { + throw new Error(`Tool not found: ${decision.next_step.tool}`); + } + + // Execute the tool + const toolInput = { + ...input, + role: `assistant-${context.task_type}`, + message: decision.next_step, + }; + + await toolNode.behavior(toolInput, context); + + // Update step status + context.currentStep.status = 'completed'; + + iteration++; + console.log(`Task iteration ${iteration}/${MAX_ITERATIONS}`); + } + + // Check if task exceeded limits + if (iteration >= MAX_ITERATIONS) { + throw new Error('Task exceeded maximum iterations'); + } + if (Date.now() - startTime >= TIMEOUT) { + throw new Error('Task timed out'); + } + + // Handle final file modifications + if (Object.keys(context.modifiedFiles).length > 0) { + await Promise.all( + Object.entries(context.modifiedFiles).map( + async ([filePath, content]) => { + try { + await fetch('/api/file', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + filePath: normalizePath(`${context.projectPath}/${filePath}`), + newContent: content, + }), + }); + console.log(`File updated: ${filePath}`); + } catch (error) { + console.error(`Error updating file ${filePath}:`, error); + throw error; + } + } + ) + ); + } + + console.log('Task completed successfully'); + } catch (error) { + console.error('Error in managerAgent:', error); + throw error; + } +} diff --git a/frontend/src/hooks/multi-agent/toolNodes.ts b/frontend/src/hooks/multi-agent/toolNodes.ts new file mode 100644 index 00000000..4fe6be83 --- /dev/null +++ b/frontend/src/hooks/multi-agent/toolNodes.ts @@ -0,0 +1,130 @@ +import { ChatInputType } from '@/graphql/type'; +import { AgentContext } from './agentPrompt'; +import { + taskTool, + readFileTool, + applyChangesTool, + codeReviewTool, + commitChangesTool, + editFileTool, +} from './tools'; + +export interface ToolUsage { + useFor: string; // Purpose of the tool + input: string; // Input description + output: string; // Output description + whenToUse: string; // When to use the tool + nextStep?: string; // Typical next step +} + +// Modify the Tool interface to include usage instructions +export interface Tool { + toolName: string; + behavior: (input: ChatInputType, context: AgentContext) => Promise; + description: string; + usage: ToolUsage; +} + +export const managerTools: Tool[] = [ + { + toolName: 'taskTool', + behavior: taskTool, + description: + 'Analyzes task requirements, identifies relevant files, and generates a processing plan.', + usage: { + useFor: + 'Understanding the task requirements and identifying relevant files', + input: 'Task description and current status', + output: 'List of files that need to be checked or modified', + whenToUse: 'At the beginning of a task or when new files are required', + nextStep: 'readFileTool', + }, + }, + { + toolName: 'readFileTool', + behavior: readFileTool, + description: 'Reads the content of identified files.', + usage: { + useFor: 'Retrieving file content for analysis and modification', + input: 'List of file paths', + output: 'File content', + whenToUse: 'After identifying the files to be processed', + nextStep: 'editFileTool', + }, + }, + { + toolName: 'editFileTool', + behavior: editFileTool, + description: + 'Generates editing prompts based on the file content stored in the context.', + usage: { + useFor: 'Implementing the required code changes', + input: 'File content and modification plan', + output: 'Modified code', + whenToUse: 'After analysis is complete or when fixes are needed', + nextStep: 'codeReviewTool', + }, + }, + { + toolName: 'codeReviewTool', + behavior: codeReviewTool, + description: + 'Generates a code review report based on the modified file content in the context.', + usage: { + useFor: 'Ensuring code quality and correctness', + input: 'Modified file content', + output: 'Review comments and suggestions', + whenToUse: 'After code modifications', + nextStep: + 'If issues exist, return to editFileTool; if passed, proceed to applyChangesTool', + }, + }, + { + toolName: 'applyChangesTool', + behavior: applyChangesTool, + description: + 'Synchronizes modifications to the file content in the context.', + usage: { + useFor: 'Confirming and applying verified changes', + input: 'Reviewed and approved modifications', + output: 'Updated file content', + whenToUse: 'After code review is completed', + nextStep: 'commitChangesTool', + }, + }, + { + toolName: 'commitChangesTool', + behavior: commitChangesTool, + description: + 'Generates commit messages based on modifications stored in the context.', + usage: { + useFor: 'Generating descriptive commit messages', + input: 'All applied changes', + output: 'Commit message', + whenToUse: 'After changes have been applied', + nextStep: 'Task completed', + }, + }, +]; + +export function findToolNode(toolName: string): Tool | undefined { + return managerTools.find((tool) => tool.toolName === toolName); +} + +// Helper function: Retrieve tool usage instructions +export function getToolUsageMap(): Record { + const usageMap: Record = {}; + managerTools.forEach((tool) => { + usageMap[tool.toolName] = tool.usage; + }); + return usageMap; +} + +// Helper function: Retrieve tool descriptions +export function getToolDescriptionMap(): Record { + const descMap: Record = {}; + managerTools.forEach((tool) => { + descMap[tool.toolName] = tool.description; + }); + return descMap; +} diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts new file mode 100644 index 00000000..62136ffb --- /dev/null +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -0,0 +1,308 @@ +import { ChatInputType } from '@/graphql/type'; +import { toast } from 'sonner'; +import { + taskPrompt, + findbugPrompt, + refactorPrompt, + optimizePrompt, + editFilePrompt, + codeReviewPrompt, + commitChangesPrompt, + AgentContext, + TaskType, +} from './agentPrompt'; +import { startChatStream } from '@/api/ChatStreamAPI'; +import { parseXmlToJson } from '@/utils/parser'; + +/** + * Get the corresponding prompt based on the task type. + */ +function getPromptByTaskType( + task_type: TaskType, + message: string, + fileStructure: string[] +): string { + switch (task_type) { + case TaskType.DEBUG: + return findbugPrompt(message, fileStructure); + case TaskType.REFACTOR: + return refactorPrompt(message, fileStructure); + case TaskType.OPTIMIZE: + return optimizePrompt(message, fileStructure); + default: + throw new Error(`Unsupported task type: ${task_type}`); + } +} + +/** + * Task analysis tool: + * Analyzes requirements and identifies relevant files. + */ +export async function taskTool( + input: ChatInputType, + context: AgentContext +): Promise { + console.log('taskTool called with input:', input); + + // Select the appropriate prompt based on the task type from the context. + const prompt = getPromptByTaskType( + context.task_type, + input.message, + context.fileStructure + ); + + console.log(context.task_type); + + // Get AI response + const response = await startChatStream( + { + chatId: input.chatId, + message: prompt, + model: input.model, + role: input.role, + }, + context.token + ); + + // Parse response using `parseXmlToJson` + const result = parseXmlToJson(response); + if (!result.files || !Array.isArray(result.files)) { + throw new Error('Invalid response format: missing files array'); + } + // Validate and update required files + const validFiles = (result.files || []).filter( + (path) => !!path && typeof path === 'string' + ); + if (validFiles.length === 0) { + throw new Error('No valid files identified in the response'); + } + context.requiredFiles = validFiles; + console.log('Valid files to process:', validFiles); + + // Set next step to read file content + context.currentStep = { + tool: 'readFileTool', + status: 'pending', + description: `Read ${result.files.length} identified files`, + }; + console.log('Task analysis completed, files identified:', result.files); +} + +/** + * Read file tool: + * Reads the content of identified files. + */ +export async function readFileTool( + input: ChatInputType, + context: AgentContext +): Promise { + console.log('readFileTool called with input:', input); + + if (!context.requiredFiles || context.requiredFiles.length === 0) { + throw new Error( + 'No files specified to read. Make sure requiredFiles is set.' + ); + } + + // Read file content for each required file + console.log('Reading files:', context.requiredFiles); + await Promise.all( + context.requiredFiles.map(async (filePath: string) => { + try { + const response = await fetch( + `/api/file?path=${encodeURIComponent(`${context.projectPath}/${filePath}`)}`, + { + method: 'GET', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + } + ); + const data = await response.json(); + context.fileContents[filePath] = data.content; + } catch (error) { + console.error(`Error reading file ${filePath}:`, error); + throw error; + } + }) + ); + + console.log('Files read:', context.requiredFiles); + console.log('File contents loaded:', context.fileContents); + toast.success(`${context.requiredFiles.length} files loaded successfully`); +} + +/** + * Edit file tool: + * Modifies file content based on requirements. + */ +export async function editFileTool( + input: ChatInputType, + context: AgentContext +): Promise { + console.log('editFileTool called with input:', input); + console.log('Current file contents:', context.fileContents); + + if (Object.keys(context.fileContents).length === 0) { + throw new Error( + 'No file contents available. Make sure readFileTool was called first.' + ); + } + + // Generate edit prompt + const prompt = editFilePrompt(input.message, context.fileContents); + console.log('Generated prompt with file contents for editing'); + + // Get AI response + const response = await startChatStream( + { + chatId: input.chatId, + message: prompt, + model: input.model, + role: input.role, + }, + context.token + ); + + // Parse response using `parseXmlToJson` + const result = parseXmlToJson(response); + + // Check for files that need to be read or modified + if (result.files && Array.isArray(result.files)) { + context.requiredFiles = result.files; + console.log('New files identified:', result.files); + } + + if (!result.modified_files || typeof result.modified_files !== 'object') { + throw new Error('Invalid response format: missing modified_files object'); + } + + // Update required files and content in context + const modifiedPaths = Object.keys(result.modified_files); + const validPaths = modifiedPaths.filter((path) => !!path); + console.log('Adding new paths to required files:', validPaths); + context.requiredFiles = Array.from( + new Set([...context.requiredFiles, ...validPaths]) + ); + + // Update file content in context + Object.entries(result.modified_files).forEach(([filePath, content]) => { + context.modifiedFiles[filePath] = content as string; + context.fileContents[filePath] = content as string; + }); + + console.log('Updated context with modified files'); +} + +/** + * Apply changes tool: + * Confirms and applies file modifications. + */ +export async function applyChangesTool( + input: ChatInputType, + context: AgentContext +): Promise { + console.log('applyChangesTool called with input:', input); + + // Validate modification content + Object.entries(context.modifiedFiles).forEach(([filePath, content]) => { + if (!content || typeof content !== 'string') { + throw new Error(`Invalid content for file: ${filePath}`); + } + }); + + // Update file content in context + Object.entries(context.modifiedFiles).forEach(([filePath, content]) => { + context.fileContents[filePath] = content; + }); + + console.log('Updated context with confirmed changes'); + toast.success('Changes applied successfully'); +} + +/** + * Code review tool: + * Checks whether modifications meet the requirements. + */ +export async function codeReviewTool( + input: ChatInputType, + context: AgentContext +): Promise { + console.log('codeReviewTool called with input:', input); + + // Generate review prompt + const formattedMessage = Object.entries(context.modifiedFiles) + .map( + ([filePath, content]) => + `### File: ${filePath}\n\`\`\`\n${content}\n\`\`\`` + ) + .join('\n\n'); + + const prompt = codeReviewPrompt(formattedMessage); + + // Get AI response + const response = await startChatStream( + { + chatId: input.chatId, + message: prompt, + model: input.model, + role: input.role, + }, + context.token + ); + + // Parse review results using `parseXmlToJson` + const result = parseXmlToJson(response); + if (!result.review_result || !result.comments) { + throw new Error('Invalid response format: missing review details'); + } + + // Update review results in context + context.reviewComments = result.comments; + if (result.review_result === 'Still has issues') { + context.currentStep = { + tool: 'editFileTool', + status: 'pending', + description: 'Address review comments', + }; + } + + console.log('Code review completed'); +} + +/** + * Commit changes tool: + * Generates commit messages. + */ +export async function commitChangesTool( + input: ChatInputType, + context: AgentContext +): Promise { + console.log('commitChangesTool called with input:', input); + + // Generate commit message prompt + const formattedChanges = Object.entries(context.modifiedFiles) + .map(([filePath, content]) => `Modified file: ${filePath}`) + .join('\n'); + + const prompt = commitChangesPrompt(formattedChanges); + + // Get AI response + const response = await startChatStream( + { + chatId: input.chatId, + message: prompt, + model: input.model, + role: input.role, + }, + context.token + ); + + // Parse commit message using `parseXmlToJson` + const result = parseXmlToJson(response); + if (!result.commit_message) { + throw new Error('Invalid response format: missing commit message'); + } + + context.commitMessage = result.commit_message; + console.log('Generated commit message:', result.commit_message); +} diff --git a/frontend/src/hooks/useChatStream.ts b/frontend/src/hooks/useChatStream.ts index 2b559941..24f6a82c 100644 --- a/frontend/src/hooks/useChatStream.ts +++ b/frontend/src/hooks/useChatStream.ts @@ -1,10 +1,14 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useContext, use } from 'react'; import { useMutation } from '@apollo/client'; import { CREATE_CHAT, SAVE_MESSAGE } from '@/graphql/request'; import { Message } from '@/const/MessageType'; import { toast } from 'sonner'; import { logger } from '@/app/log/logger'; import { useAuthContext } from '@/providers/AuthProvider'; +import { startChatStream } from '@/api/ChatStreamAPI'; +import { ProjectContext } from '@/components/chat/code-engine/project-context'; +import { ChatInputType } from '@/graphql/type'; +import { managerAgent } from './multi-agent/managerAgent'; export interface UseChatStreamProps { chatId: string; @@ -24,7 +28,14 @@ export const useChatStream = ({ const [loadingSubmit, setLoadingSubmit] = useState(false); const [currentChatId, setCurrentChatId] = useState(chatId); const { token } = useAuthContext(); - + const { curProject } = useContext(ProjectContext); + const [curProjectPath, setCurProjectPath] = useState(''); + useEffect(() => { + console.log('curProject:', curProject); + if (curProject) { + setCurProjectPath(curProject.projectPath); + } + }, [curProject]); // Use useEffect to handle new chat event and cleanup useEffect(() => { const updateChatId = () => { @@ -64,95 +75,27 @@ export const useChatStream = ({ }, }); - const startChatStream = async ( - targetChatId: string, - message: string, - model: string, - stream: boolean = false // Default to non-streaming for better performance - ): Promise => { - if (!token) { - throw new Error('Not authenticated'); - } - - const response = await fetch('/api/chat', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - chatId: targetChatId, - message, - model, - stream, - }), - }); - - if (!response.ok) { - throw new Error( - `Network response was not ok: ${response.status} ${response.statusText}` - ); - } - // TODO: Handle streaming responses properly - // if (stream) { - // // For streaming responses, aggregate the streamed content - // let fullContent = ''; - // const reader = response.body?.getReader(); - // if (!reader) { - // throw new Error('No reader available'); - // } - - // while (true) { - // const { done, value } = await reader.read(); - // if (done) break; - - // const text = new TextDecoder().decode(value); - // const lines = text.split('\n\n'); - - // for (const line of lines) { - // if (line.startsWith('data: ')) { - // const data = line.slice(5); - // if (data === '[DONE]') break; - // try { - // const { content } = JSON.parse(data); - // if (content) { - // fullContent += content; - // } - // } catch (e) { - // console.error('Error parsing SSE data:', e); - // } - // } - // } - // } - // return fullContent; - // } else { - // // For non-streaming responses, return the content directly - // const data = await response.json(); - // return data.content; - // } - - const data = await response.json(); - return data.content; - }; - const handleChatResponse = async (targetChatId: string, message: string) => { try { setInput(''); - const response = await startChatStream( - targetChatId, + const userInput: ChatInputType = { + chatId: targetChatId, message, - selectedModel - ); - - setMessages((prev) => [ - ...prev, - { - id: `${targetChatId}/${prev.length}`, - role: 'assistant', - content: response, - createdAt: new Date().toISOString(), + model: selectedModel, + role: 'user', + }; + saveMessage({ + variables: { + input: userInput as ChatInputType, }, - ]); + }); + await managerAgent( + userInput, + setMessages, + curProjectPath, + saveMessage, + token + ); setLoadingSubmit(false); } catch (err) { diff --git a/frontend/src/utils/parser.ts b/frontend/src/utils/parser.ts index 4da73609..3c111fe3 100644 --- a/frontend/src/utils/parser.ts +++ b/frontend/src/utils/parser.ts @@ -1,3 +1,6 @@ +/** + * Parses AI response wrapped inside tags into a JSON object. + */ export const parseXmlToJson = (xmlString: string) => { console.log(xmlString); const match = xmlString.match(/([\s\S]*?)<\/jsonResponse>/); From 0090d22cc2faa7cd654b6add0eb77ef6088c5cce Mon Sep 17 00:00:00 2001 From: Narwhal Date: Mon, 17 Mar 2025 15:24:25 -0500 Subject: [PATCH 06/19] refactor(backend): remove redundant message saving in chat controller --- backend/src/chat/chat.controller.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/backend/src/chat/chat.controller.ts b/backend/src/chat/chat.controller.ts index 7ee0fa97..1deeaa1a 100644 --- a/backend/src/chat/chat.controller.ts +++ b/backend/src/chat/chat.controller.ts @@ -22,13 +22,6 @@ export class ChatController { @GetAuthToken() userId: string, ) { try { - // Save user's message first - await this.chatService.saveMessage( - chatDto.chatId, - chatDto.message, - MessageRole.User, - ); - if (chatDto.stream) { // Streaming response res.setHeader('Content-Type', 'text/event-stream'); @@ -52,13 +45,6 @@ export class ChatController { } } - // Save the complete message - await this.chatService.saveMessage( - chatDto.chatId, - fullResponse, - MessageRole.Assistant, - ); - res.write('data: [DONE]\n\n'); res.end(); } else { From fddbe0b832a333cefca625a10cc08f3ec0a28c40 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Mon, 17 Mar 2025 16:03:11 -0500 Subject: [PATCH 07/19] feat(frontend): add refreshProjects function to managerAgent and useChatStream hooks --- frontend/src/hooks/multi-agent/managerAgent.ts | 8 ++++++-- frontend/src/hooks/useChatStream.ts | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index 6aefa603..1ba965e2 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -43,7 +43,8 @@ export async function managerAgent( setMessages: React.Dispatch>, projectPath: string, saveMessage: any, - token: string + token: string, + refreshProjects: () => Promise ): Promise { console.log('managerAgent called with input:', input); @@ -190,9 +191,12 @@ export async function managerAgent( } ) ); + + // Refresh projects to update the code display + await refreshProjects(); } - console.log('Task completed successfully'); + console.log('Task completed successfully with updated files'); } catch (error) { console.error('Error in managerAgent:', error); throw error; diff --git a/frontend/src/hooks/useChatStream.ts b/frontend/src/hooks/useChatStream.ts index 24f6a82c..dcc26ea6 100644 --- a/frontend/src/hooks/useChatStream.ts +++ b/frontend/src/hooks/useChatStream.ts @@ -28,7 +28,7 @@ export const useChatStream = ({ const [loadingSubmit, setLoadingSubmit] = useState(false); const [currentChatId, setCurrentChatId] = useState(chatId); const { token } = useAuthContext(); - const { curProject } = useContext(ProjectContext); + const { curProject, refreshProjects } = useContext(ProjectContext); const [curProjectPath, setCurProjectPath] = useState(''); useEffect(() => { console.log('curProject:', curProject); @@ -94,7 +94,8 @@ export const useChatStream = ({ setMessages, curProjectPath, saveMessage, - token + token, + refreshProjects ); setLoadingSubmit(false); From 51728e2caa1f502d3cffc3cf64aec232408b63ab Mon Sep 17 00:00:00 2001 From: Narwhal Date: Mon, 17 Mar 2025 16:39:43 -0500 Subject: [PATCH 08/19] feat(frontend): implement saveThinkingProcess helper and update message saving logic in managerAgent --- .../src/hooks/multi-agent/managerAgent.ts | 14 ++++++++ frontend/src/hooks/multi-agent/tools.ts | 35 +++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index 1ba965e2..447b3fea 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -113,6 +113,20 @@ export async function managerAgent( try { decision = parseXmlToJson(response); console.log('Parsed AI Decision:', decision); + + // Save thinking_process as assistant message + if (decision.thinking_process) { + saveMessage({ + variables: { + input: { + chatId: input.chatId, + message: decision.thinking_process, + model: input.model, + role: `assistant`, + }, + }, + }); + } } catch (error) { console.error('Error parsing AI response:', error); throw error; diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index 62136ffb..334b9fd2 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -1,4 +1,26 @@ import { ChatInputType } from '@/graphql/type'; + +/** + * Helper function to save thinking process from AI response + */ +const saveThinkingProcess = ( + result: any, + input: ChatInputType, + context: AgentContext +) => { + if (result.thinking_process) { + context.saveMessage({ + variables: { + input: { + chatId: input.chatId, + message: result.thinking_process, + model: input.model, + role: 'assistant', + }, + }, + }); + } +}; import { toast } from 'sonner'; import { taskPrompt, @@ -66,16 +88,20 @@ export async function taskTool( // Parse response using `parseXmlToJson` const result = parseXmlToJson(response); + saveThinkingProcess(result, input, context); + if (!result.files || !Array.isArray(result.files)) { throw new Error('Invalid response format: missing files array'); } - // Validate and update required files - const validFiles = (result.files || []).filter( + + // Validate and filter file paths + const validFiles = result.files.filter( (path) => !!path && typeof path === 'string' ); if (validFiles.length === 0) { throw new Error('No valid files identified in the response'); } + context.requiredFiles = validFiles; console.log('Valid files to process:', validFiles); @@ -165,6 +191,7 @@ export async function editFileTool( // Parse response using `parseXmlToJson` const result = parseXmlToJson(response); + saveThinkingProcess(result, input, context); // Check for files that need to be read or modified if (result.files && Array.isArray(result.files)) { @@ -252,6 +279,8 @@ export async function codeReviewTool( // Parse review results using `parseXmlToJson` const result = parseXmlToJson(response); + saveThinkingProcess(result, input, context); + if (!result.review_result || !result.comments) { throw new Error('Invalid response format: missing review details'); } @@ -299,6 +328,8 @@ export async function commitChangesTool( // Parse commit message using `parseXmlToJson` const result = parseXmlToJson(response); + saveThinkingProcess(result, input, context); + if (!result.commit_message) { throw new Error('Invalid response format: missing commit message'); } From 75af9cbde13c5184429419ffd004faa60a2f2bd1 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Mon, 17 Mar 2025 21:32:56 -0500 Subject: [PATCH 09/19] feat(frontend): enhance managerAgent to accumulate and save thoughts as a single message --- backend/src/chat/chat.controller.ts | 8 ----- frontend/src/hooks/multi-agent/agentPrompt.ts | 1 + .../src/hooks/multi-agent/managerAgent.ts | 33 ++++++++++--------- frontend/src/hooks/multi-agent/tools.ts | 11 +------ 4 files changed, 19 insertions(+), 34 deletions(-) diff --git a/backend/src/chat/chat.controller.ts b/backend/src/chat/chat.controller.ts index 1deeaa1a..e9e67ee1 100644 --- a/backend/src/chat/chat.controller.ts +++ b/backend/src/chat/chat.controller.ts @@ -55,14 +55,6 @@ export class ChatController { model: chatDto.model, role: MessageRole.User, }); - - // Save the complete message - await this.chatService.saveMessage( - chatDto.chatId, - response, - MessageRole.Assistant, - ); - res.json({ content: response }); } } catch (error) { diff --git a/frontend/src/hooks/multi-agent/agentPrompt.ts b/frontend/src/hooks/multi-agent/agentPrompt.ts index 97241291..c750026f 100644 --- a/frontend/src/hooks/multi-agent/agentPrompt.ts +++ b/frontend/src/hooks/multi-agent/agentPrompt.ts @@ -19,6 +19,7 @@ export interface AgentContext { requiredFiles: string[]; // Files that need to be read/modified reviewComments?: string[]; // Code review comments commitMessage?: string; // Commit message + accumulatedThoughts: string[]; // Accumulated thinking processes currentStep?: { // Current execution step tool?: string; // Tool being used diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index 447b3fea..04f2ae97 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -58,6 +58,7 @@ export async function managerAgent( fileContents: {}, modifiedFiles: {}, requiredFiles: [], // Initialize empty array for required files + accumulatedThoughts: [], // Initialize empty array for thoughts currentStep: { tool: undefined, status: 'initializing', @@ -103,7 +104,7 @@ export async function managerAgent( chatId: input.chatId, message: confirmationPromptStr, model: input.model, - role: `assistant-${context.task_type}`, + role: `assistant`, }, token ); @@ -113,20 +114,6 @@ export async function managerAgent( try { decision = parseXmlToJson(response); console.log('Parsed AI Decision:', decision); - - // Save thinking_process as assistant message - if (decision.thinking_process) { - saveMessage({ - variables: { - input: { - chatId: input.chatId, - message: decision.thinking_process, - model: input.model, - role: `assistant`, - }, - }, - }); - } } catch (error) { console.error('Error parsing AI response:', error); throw error; @@ -161,7 +148,7 @@ export async function managerAgent( // Execute the tool const toolInput = { ...input, - role: `assistant-${context.task_type}`, + role: `assistant`, message: decision.next_step, }; @@ -210,6 +197,20 @@ export async function managerAgent( await refreshProjects(); } + // Save all accumulated thoughts as one message + if (context.accumulatedThoughts.length > 0) { + context.saveMessage({ + variables: { + input: { + chatId: input.chatId, + message: context.accumulatedThoughts.join('\n\n'), + model: input.model, + role: `assistant`, + }, + }, + }); + } + console.log('Task completed successfully with updated files'); } catch (error) { console.error('Error in managerAgent:', error); diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index 334b9fd2..6920a3cc 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -9,16 +9,7 @@ const saveThinkingProcess = ( context: AgentContext ) => { if (result.thinking_process) { - context.saveMessage({ - variables: { - input: { - chatId: input.chatId, - message: result.thinking_process, - model: input.model, - role: 'assistant', - }, - }, - }); + context.accumulatedThoughts.push(result.thinking_process); } }; import { toast } from 'sonner'; From cc4366a10081d4280ae35640bdb512875dcbdf27 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Mon, 17 Mar 2025 21:47:22 -0500 Subject: [PATCH 10/19] feat(frontend): enhance saveThinkingProcess to support typewriter effect and await functionality --- .../src/hooks/multi-agent/managerAgent.ts | 2 +- frontend/src/hooks/multi-agent/tools.ts | 68 +++++++++++++++++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index 04f2ae97..41e4a944 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -197,7 +197,7 @@ export async function managerAgent( await refreshProjects(); } - // Save all accumulated thoughts as one message + // Save all accumulated thoughts if (context.accumulatedThoughts.length > 0) { context.saveMessage({ variables: { diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index 6920a3cc..10a59554 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -3,13 +3,71 @@ import { ChatInputType } from '@/graphql/type'; /** * Helper function to save thinking process from AI response */ -const saveThinkingProcess = ( +const saveThinkingProcess = async ( result: any, input: ChatInputType, context: AgentContext ) => { if (result.thinking_process) { + // Accumulate for final save context.accumulatedThoughts.push(result.thinking_process); + + // Break text into chunks for typewriter effect + const breakText = (text: string) => { + return text.match(/(\S{1,3}|\s+)/g) || []; + }; + + // Display with typewriter effect + const typewriterEffect = async ( + textArray: string[], + delay: number + ): Promise => { + return new Promise((resolve) => { + let index = 0; + + const updateMessage = () => { + if (index < textArray.length) { + context.setMessages((prev) => { + const lastMsg = prev[prev.length - 1]; + + if ( + lastMsg?.role === 'assistant' && + lastMsg.id === input.chatId + ) { + return [ + ...prev.slice(0, -1), + { + ...lastMsg, + content: lastMsg.content + textArray[index], + }, + ]; + } else { + return [ + ...prev, + { + id: input.chatId, + role: 'assistant', + content: textArray[index], + createdAt: new Date().toISOString(), + }, + ]; + } + }); + + index++; + setTimeout(updateMessage, delay); + } else { + resolve(); + } + }; + + updateMessage(); + }); + }; + + // Apply typewriter effect for immediate display + const brokenText = breakText(result.thinking_process); + await typewriterEffect(brokenText, 10); } }; import { toast } from 'sonner'; @@ -79,7 +137,7 @@ export async function taskTool( // Parse response using `parseXmlToJson` const result = parseXmlToJson(response); - saveThinkingProcess(result, input, context); + await saveThinkingProcess(result, input, context); if (!result.files || !Array.isArray(result.files)) { throw new Error('Invalid response format: missing files array'); @@ -182,7 +240,7 @@ export async function editFileTool( // Parse response using `parseXmlToJson` const result = parseXmlToJson(response); - saveThinkingProcess(result, input, context); + await saveThinkingProcess(result, input, context); // Check for files that need to be read or modified if (result.files && Array.isArray(result.files)) { @@ -270,7 +328,7 @@ export async function codeReviewTool( // Parse review results using `parseXmlToJson` const result = parseXmlToJson(response); - saveThinkingProcess(result, input, context); + await saveThinkingProcess(result, input, context); if (!result.review_result || !result.comments) { throw new Error('Invalid response format: missing review details'); @@ -319,7 +377,7 @@ export async function commitChangesTool( // Parse commit message using `parseXmlToJson` const result = parseXmlToJson(response); - saveThinkingProcess(result, input, context); + await saveThinkingProcess(result, input, context); if (!result.commit_message) { throw new Error('Invalid response format: missing commit message'); From 38bb87f6fc629d862c0fd722f1665d46017e4590 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Mon, 17 Mar 2025 22:05:43 -0500 Subject: [PATCH 11/19] feat(frontend): update saveThinkingProcess to append newlines for consecutive assistant messages --- frontend/src/hooks/multi-agent/tools.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index 10a59554..520c62ad 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -68,6 +68,20 @@ const saveThinkingProcess = async ( // Apply typewriter effect for immediate display const brokenText = breakText(result.thinking_process); await typewriterEffect(brokenText, 10); + + context.setMessages((prev) => { + const lastMsg = prev[prev.length - 1]; + if (lastMsg?.role === 'assistant' && lastMsg.id === input.chatId) { + return [ + ...prev.slice(0, -1), + { + ...lastMsg, + content: lastMsg.content + '\n\n', // 追加两个换行 + }, + ]; + } + return prev; + }); } }; import { toast } from 'sonner'; From 230699957d478dd3ee0c212d7e59036109f8e652 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Tue, 18 Mar 2025 00:35:09 -0500 Subject: [PATCH 12/19] feat(frontend): add editorRef and setFilePath to context and update related components for enhanced file handling --- .../chat/code-engine/code-engine.tsx | 4 +- .../chat/code-engine/project-context.tsx | 6 ++- frontend/src/hooks/multi-agent/agentPrompt.ts | 2 + .../src/hooks/multi-agent/managerAgent.ts | 6 ++- frontend/src/hooks/multi-agent/tools.ts | 40 +++++++++++++++++-- frontend/src/hooks/useChatStream.ts | 7 +++- 6 files changed, 54 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/chat/code-engine/code-engine.tsx b/frontend/src/components/chat/code-engine/code-engine.tsx index 8ac6da93..37358ad0 100644 --- a/frontend/src/components/chat/code-engine/code-engine.tsx +++ b/frontend/src/components/chat/code-engine/code-engine.tsx @@ -20,7 +20,7 @@ export function CodeEngine({ isProjectReady?: boolean; projectId?: string; }) { - const { curProject, projectLoading, pollChatProject } = + const { curProject, projectLoading, pollChatProject, editorRef } = useContext(ProjectContext); const [localProject, setLocalProject] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -35,8 +35,6 @@ export function CodeEngine({ const [fileStructureData, setFileStructureData] = useState< Record> >({}); - - const editorRef = useRef(null); const projectPathRef = useRef(null); // Poll for project if needed using chatId diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index dbc6d25e..f1bcbf2f 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -49,6 +49,7 @@ export interface ProjectContextType { ) => Promise<{ domain: string; containerId: string }>; takeProjectScreenshot: (projectId: string, url: string) => Promise; refreshProjects: () => Promise; + editorRef?: React.MutableRefObject; } export const ProjectContext = createContext( @@ -103,6 +104,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const [projectLoading, setProjectLoading] = useState(true); const [filePath, setFilePath] = useState(null); const [isLoading, setIsLoading] = useState(false); + const editorRef = useRef(null); interface ChatProjectCacheEntry { project: Project | null; @@ -325,7 +327,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { onCompleted: (data) => { if (!isMounted.current) return; - setProjects(data.getUserProjects); + setProjects([...data.getUserProjects]); // Trigger state sync after data update const now = Date.now(); @@ -911,6 +913,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { getWebUrl, takeProjectScreenshot, refreshProjects, + editorRef, }), [ projects, @@ -926,6 +929,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) { getWebUrl, takeProjectScreenshot, refreshProjects, + editorRef, ] ); diff --git a/frontend/src/hooks/multi-agent/agentPrompt.ts b/frontend/src/hooks/multi-agent/agentPrompt.ts index c750026f..58750756 100644 --- a/frontend/src/hooks/multi-agent/agentPrompt.ts +++ b/frontend/src/hooks/multi-agent/agentPrompt.ts @@ -20,6 +20,8 @@ export interface AgentContext { reviewComments?: string[]; // Code review comments commitMessage?: string; // Commit message accumulatedThoughts: string[]; // Accumulated thinking processes + setFilePath: (path: string) => void; // Set current file path in editor + editorRef: React.MutableRefObject; // Monaco editor reference for direct updates currentStep?: { // Current execution step tool?: string; // Tool being used diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index 41e4a944..52313938 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -44,7 +44,9 @@ export async function managerAgent( projectPath: string, saveMessage: any, token: string, - refreshProjects: () => Promise + refreshProjects: () => Promise, + setFilePath: (path: string) => void, + editorRef: React.MutableRefObject ): Promise { console.log('managerAgent called with input:', input); @@ -67,6 +69,8 @@ export async function managerAgent( setMessages, saveMessage, token, + setFilePath, + editorRef, }; // Retrieve project file structure diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index 520c62ad..76eaf821 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -274,13 +274,45 @@ export async function editFileTool( new Set([...context.requiredFiles, ...validPaths]) ); - // Update file content in context - Object.entries(result.modified_files).forEach(([filePath, content]) => { + // Update file content in context with typewriter effect + for (const [filePath, content] of Object.entries(result.modified_files)) { + // Set current file in code-engine + context.setFilePath(filePath); + await new Promise((resolve) => setTimeout(resolve, 500)); // Wait for file to load + + console.log(`Starting line-by-line changes for ${filePath}`); + const lines = (content as string).split('\n'); + let accumulatedContent = ''; + + if (context.editorRef?.current) { + // Get current editor content as baseline + accumulatedContent = context.editorRef.current.getValue(); + + // Wait for file path change to take effect + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Show each line with animation + try { + for (let i = 0; i < lines.length; i++) { + accumulatedContent = lines.slice(0, i + 1).join('\n'); + context.editorRef.current.setValue(accumulatedContent); + console.log(`Updated line ${i + 1}/${lines.length} in ${filePath}`); + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } catch (error) { + console.error('Error updating editor content:', error); + } + + // Sync final content + context.fileContents[filePath] = content as string; + } + + // Store final content context.modifiedFiles[filePath] = content as string; context.fileContents[filePath] = content as string; - }); + } - console.log('Updated context with modified files'); + console.log('Updated files with line-by-line animation of changes'); } /** diff --git a/frontend/src/hooks/useChatStream.ts b/frontend/src/hooks/useChatStream.ts index dcc26ea6..598fec9f 100644 --- a/frontend/src/hooks/useChatStream.ts +++ b/frontend/src/hooks/useChatStream.ts @@ -28,7 +28,8 @@ export const useChatStream = ({ const [loadingSubmit, setLoadingSubmit] = useState(false); const [currentChatId, setCurrentChatId] = useState(chatId); const { token } = useAuthContext(); - const { curProject, refreshProjects } = useContext(ProjectContext); + const { curProject, refreshProjects, setFilePath, editorRef } = + useContext(ProjectContext); const [curProjectPath, setCurProjectPath] = useState(''); useEffect(() => { console.log('curProject:', curProject); @@ -95,7 +96,9 @@ export const useChatStream = ({ curProjectPath, saveMessage, token, - refreshProjects + refreshProjects, + setFilePath, + editorRef ); setLoadingSubmit(false); From f56ce7d8ee1d88c8c11fcd204f8bfae0bb65e2d6 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Tue, 18 Mar 2025 00:47:47 -0500 Subject: [PATCH 13/19] feat(frontend): enhance editFileTool to parse JSON strings and store unescaped content --- frontend/src/hooks/multi-agent/agentPrompt.ts | 9 +++++---- frontend/src/hooks/multi-agent/tools.ts | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/frontend/src/hooks/multi-agent/agentPrompt.ts b/frontend/src/hooks/multi-agent/agentPrompt.ts index 58750756..14808643 100644 --- a/frontend/src/hooks/multi-agent/agentPrompt.ts +++ b/frontend/src/hooks/multi-agent/agentPrompt.ts @@ -55,10 +55,11 @@ You are an AI assistant. When responding, you **must** adhere to the following r 4. **If Asked to Modify Code** - The output must be formatted as follows: + - IMPORTANT: Code content must be RAW, not JSON-encoded strings! { "modified_files": { - "frontend/src/hooks/useChatStream.ts": "Updated code content...", - "frontend/src/components/ChatInput.tsx": "Updated code content..." + "frontend/src/hooks/useChatStream.ts": "import React from 'react';\n\nexport function Component() {\n return
Content
;\n}", + "frontend/src/components/ChatInput.tsx": "import { useState } from 'react';\n\nexport function Input() {\n // Code content...\n}" }, "thinking_process": "After reviewing the bug description, I identified that the issue is caused by an incomplete dependency array in useEffect, preventing state updates. I have modified the code accordingly." }
@@ -263,8 +264,8 @@ export const editFilePrompt = ( ### **Output Format** { "modified_files": { - "frontend/src/components/file.tsx": "// Complete updated code with ALL original functionality preserved\nimport React from 'react';\n// ... rest of the imports\n\n// All original components and code preserved", - "frontend/src/utils/file.ts": "// Complete updated code\n// No removal of existing functionality" + "frontend/src/components/file.tsx": "import React from 'react';\nimport { useState } from 'react';\n\nexport function Component() {\n const [state, setState] = useState(null);\n return
Content
;\n}", + "frontend/src/utils/file.ts": "export function helperFunction() {\n // Complete implementation\n return true;\n}" }, "thinking_process": "After analyzing the code, I identified the specific issue. The fix has been implemented while preserving all existing functionality, including [list specific preserved features]. The changes only affect [describe specific changes], and all other code remains intact." }
diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index 76eaf821..8a9dbc96 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -280,8 +280,18 @@ export async function editFileTool( context.setFilePath(filePath); await new Promise((resolve) => setTimeout(resolve, 500)); // Wait for file to load + // Parse content if it's a JSON string + let realContent = content as string; + try { + if (realContent.startsWith('"') && realContent.endsWith('"')) { + realContent = JSON.parse(realContent); + } + } catch (error) { + console.error('Error parsing code content:', error); + } + console.log(`Starting line-by-line changes for ${filePath}`); - const lines = (content as string).split('\n'); + const lines = realContent.split('\n'); let accumulatedContent = ''; if (context.editorRef?.current) { @@ -307,9 +317,9 @@ export async function editFileTool( context.fileContents[filePath] = content as string; } - // Store final content - context.modifiedFiles[filePath] = content as string; - context.fileContents[filePath] = content as string; + // Store final unescaped content + context.modifiedFiles[filePath] = realContent; + context.fileContents[filePath] = realContent; } console.log('Updated files with line-by-line animation of changes'); From 90df595cc964af77c814cf85c795e9bf08ed6a39 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Tue, 18 Mar 2025 00:55:32 -0500 Subject: [PATCH 14/19] feat(frontend): update CodeEngine to send unescaped content in newContent --- frontend/src/components/chat/code-engine/code-engine.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/chat/code-engine/code-engine.tsx b/frontend/src/components/chat/code-engine/code-engine.tsx index 37358ad0..03bc65de 100644 --- a/frontend/src/components/chat/code-engine/code-engine.tsx +++ b/frontend/src/components/chat/code-engine/code-engine.tsx @@ -196,7 +196,7 @@ export function CodeEngine({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filePath: `${projectPath}/${filePath}`, - newContent: JSON.stringify(value), + newContent: value, }), }); From e4f66ec68f8d83cec5dc1582249af02eea911e36 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Tue, 18 Mar 2025 11:08:00 -0500 Subject: [PATCH 15/19] feat(frontend): add UNRELATED task type to categorize non-technical requests and update systemPrompt calls --- frontend/src/hooks/multi-agent/agentPrompt.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/multi-agent/agentPrompt.ts b/frontend/src/hooks/multi-agent/agentPrompt.ts index 14808643..5e8f4e3f 100644 --- a/frontend/src/hooks/multi-agent/agentPrompt.ts +++ b/frontend/src/hooks/multi-agent/agentPrompt.ts @@ -2,6 +2,7 @@ export enum TaskType { DEBUG = 'debug', REFACTOR = 'refactor', OPTIMIZE = 'optimize', + UNRELATED = 'unrelated', } export interface AgentContext { task_type: TaskType; // Current task type @@ -95,7 +96,7 @@ ONLY IF U NEED TO READ FILES OR ANY RESPONSE RELATED TO PROJECT PATH: Do not con export const leaderPrompt = (message: string): string => { return ( - systemPrompt + + systemPrompt() + `You are a professional requirements analyst responsible for analyzing a mafia user's requirements for a CS project. If you fail to conduct a proper analysis, they will take action against your family. The user's request is as follows: @@ -132,7 +133,7 @@ Otherwise, I cannot guarantee the safety of your family. :(` }; export const refactorPrompt = (message: string, file_structure: string[]) => { return ( - systemPrompt + + systemPrompt() + `You are a seasoned software engineer known for your expertise in code refactoring. Your mission is to refactor the codebase to improve its structure, readability, and maintainability. and also provide a detailed refactoring description. --- @@ -170,7 +171,7 @@ Failure is Not an Option!` }; export const optimizePrompt = (message: string, file_structure: string[]) => { return ( - systemPrompt + + systemPrompt() + `You are a code performance optimization expert. Your mission is to analyze the codebase and identify performance bottlenecks. and also provide a detailed optimization desciption. --- @@ -211,7 +212,7 @@ export const editFilePrompt = ( file_content: { [key: string]: string } ) => { return ( - systemPrompt + + systemPrompt() + `You are a senior software engineer with extensive experience in React and TypeScript. Your mission is to edit the code files while preserving all existing functionality and structure. --- @@ -275,7 +276,7 @@ Failure is Not an Option! The code must be fixed while preserving ALL existing f }; export const codeReviewPrompt = (message: string) => { return ( - systemPrompt + + systemPrompt() + `You are a senior code reviewer. Your mission is to review the code changes made by the user. --- @@ -315,7 +316,7 @@ Failure is Not an Option! If you fail, the code changes will not be reviewed.` }; export const commitChangesPrompt = (message: string) => { return ( - systemPrompt + + systemPrompt() + `You are a Git version control assistant. Your mission is to commit the code changes to the repository. --- @@ -357,6 +358,9 @@ export const taskPrompt = (message: string): string => { ${message} ### Available Task Types: +0. UNRELATED: Task is not related to code or the CS project + - Example: General inquiries, non-technical requests or questions + - Focus: Task categorization and redirection 1. DEBUG: Fix issues, errors, or unexpected behavior - Example: Runtime errors, type errors, incorrect functionality - Focus: Problem resolution and stability @@ -466,7 +470,7 @@ Make your decision based on the current context and ensure a logical progression export const findbugPrompt = (message: string, file_structure: string[]) => { return ( - systemPrompt + + systemPrompt() + `You are an elite Apex Legends player, but hackers have hijacked your account! They demand that you find the root cause of a critical bug in your project before they wipe all your skins and reset your rank. --- From 464d0ea2f543ee7838c417e2182571416c2b74ac Mon Sep 17 00:00:00 2001 From: Narwhal Date: Tue, 18 Mar 2025 12:23:58 -0500 Subject: [PATCH 16/19] feat(frontend): refactor task type handling in managerAgent and agentPrompt for improved task analysis --- frontend/src/hooks/multi-agent/agentPrompt.ts | 73 ++---- .../src/hooks/multi-agent/managerAgent.ts | 12 +- frontend/src/hooks/multi-agent/tools.ts | 235 +++++++++++------- 3 files changed, 170 insertions(+), 150 deletions(-) diff --git a/frontend/src/hooks/multi-agent/agentPrompt.ts b/frontend/src/hooks/multi-agent/agentPrompt.ts index 5e8f4e3f..5aaa33b8 100644 --- a/frontend/src/hooks/multi-agent/agentPrompt.ts +++ b/frontend/src/hooks/multi-agent/agentPrompt.ts @@ -5,7 +5,7 @@ export enum TaskType { UNRELATED = 'unrelated', } export interface AgentContext { - task_type: TaskType; // Current task type + task_type: TaskType | null; // Current task type request: string; // Original request projectPath: string; // Project ID fileStructure: string[]; // Project file structure @@ -117,14 +117,32 @@ When generating the JSON response, **ensure that the description in JSON is as d 4. **How they expect the problem to be resolved** ### **AI Thought Process:** -To classify the task, I will first analyze the user's request for any mention of errors, crashes, or incorrect behavior. If the request contains error messages like "TypeError", "ReferenceError", or "module not found", I will categorize it as a **debugging task**. -If the request discusses improvements such as reducing redundancy, improving performance, or enhancing readability, I will categorize it as **optimization**. -If the request does not relate to code functionality or performance, I will classify it as **unrelated**. +First, I will determine if the request is code-related by checking: + +1. Is this about development/programming? + - Contains technical terms (function, code, error, component, etc.) + - Mentions file paths or code elements + - Discusses software functionality + +2. Is this a general question? + - About weather, time, or general knowledge + - Personal inquiries or casual conversation + - Questions unrelated to development + +For code-related tasks: +- If about errors/bugs -> DEBUG +- If about improvements -> OPTIMIZE +- If about code structure -> REFACTOR + +For non-code tasks: +- Mark as UNRELATED +- Explain why it's not a development task +- Suggest using a general assistant instead ### **Output Format** { "task_type": "debug" | "optimize" | "unrelated", - "description": "In 'src/project/build-system-utils.ts', the user wants to use the module 'file-arch' for file management operations, but encountered error TS2307: Cannot find module 'src/build-system/handlers/file-manager/file-arch' or its corresponding type declarations. They need assistance in resolving this missing module issue by verifying module paths and dependencies.", + "description": "Examples:\n\n1. Code task: In 'src/project/utils.ts', the user encountered a TypeScript module error that needs debugging.\n\n2. Unrelated task: The user asked about weather in Madison. This is not a code or development related question, but rather a general weather inquiry that should be handled by a general assistant.", "thinking_process": "After analyzing the request, I identified that the issue involves a missing module import, which prevents compilation. This falls under debugging since it requires fixing a dependency resolution problem." } @@ -349,51 +367,6 @@ The final commit message will follow **conventional commit standards** to ensure Failure is Not an Option! If you fail, the changes will not be committed.` ); }; -export const taskPrompt = (message: string): string => { - return ( - systemPrompt() + - `You are an expert task analyzer responsible for understanding user requirements and planning the development approach. - -### User Request: -${message} - -### Available Task Types: -0. UNRELATED: Task is not related to code or the CS project - - Example: General inquiries, non-technical requests or questions - - Focus: Task categorization and redirection -1. DEBUG: Fix issues, errors, or unexpected behavior - - Example: Runtime errors, type errors, incorrect functionality - - Focus: Problem resolution and stability - -2. REFACTOR: Improve code structure without changing behavior - - Example: Code organization, modularity, readability - - Focus: Maintainability and code quality - -3. OPTIMIZE: Enhance performance or efficiency - - Example: Reduce render times, improve state management - - Focus: Performance and resource utilization - -### Analysis Requirements: -1. Consider the problem description carefully -2. Look for keywords indicating the task type -3. Identify specific areas that need attention -4. Plan the general approach to solving the issue - -### Output Format: -{ - "task_type": "debug" | "refactor" | "optimize", - "description": "Detailed description of what needs to be done", - "analysis": { - "problem_area": "Specific component or functionality affected", - "key_points": ["List of main issues or improvements needed"], - "approach": "General strategy for addressing the task" - } -} - -Remember: Your analysis sets the direction for the entire development process. Be specific and thorough.` - ); -}; - import { Message } from '@/const/MessageType'; import { getToolUsageMap } from './toolNodes'; diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index 52313938..d93462c3 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -1,12 +1,7 @@ import { ChatInputType } from '@/graphql/type'; import { startChatStream } from '@/api/ChatStreamAPI'; import { findToolNode } from './toolNodes'; -import { - taskPrompt, - confirmationPrompt, - AgentContext, - TaskType, -} from './agentPrompt'; +import { confirmationPrompt, AgentContext, TaskType } from './agentPrompt'; import path from 'path'; import { parseXmlToJson } from '@/utils/parser'; import { Message } from '@/const/MessageType'; @@ -53,7 +48,7 @@ export async function managerAgent( try { // Initialize context const context: AgentContext = { - task_type: TaskType.DEBUG, // Initial type, AI will update it later + task_type: undefined, // Will be set after task analysis request: input.message, // Store the original request projectPath, fileStructure: [], @@ -163,6 +158,9 @@ export async function managerAgent( iteration++; console.log(`Task iteration ${iteration}/${MAX_ITERATIONS}`); + if (context.task_type == TaskType.UNRELATED) { + break; + } } // Check if task exceeded limits diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index 8a9dbc96..77629a26 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -1,92 +1,6 @@ -import { ChatInputType } from '@/graphql/type'; - -/** - * Helper function to save thinking process from AI response - */ -const saveThinkingProcess = async ( - result: any, - input: ChatInputType, - context: AgentContext -) => { - if (result.thinking_process) { - // Accumulate for final save - context.accumulatedThoughts.push(result.thinking_process); - - // Break text into chunks for typewriter effect - const breakText = (text: string) => { - return text.match(/(\S{1,3}|\s+)/g) || []; - }; - - // Display with typewriter effect - const typewriterEffect = async ( - textArray: string[], - delay: number - ): Promise => { - return new Promise((resolve) => { - let index = 0; - - const updateMessage = () => { - if (index < textArray.length) { - context.setMessages((prev) => { - const lastMsg = prev[prev.length - 1]; - - if ( - lastMsg?.role === 'assistant' && - lastMsg.id === input.chatId - ) { - return [ - ...prev.slice(0, -1), - { - ...lastMsg, - content: lastMsg.content + textArray[index], - }, - ]; - } else { - return [ - ...prev, - { - id: input.chatId, - role: 'assistant', - content: textArray[index], - createdAt: new Date().toISOString(), - }, - ]; - } - }); - - index++; - setTimeout(updateMessage, delay); - } else { - resolve(); - } - }; - - updateMessage(); - }); - }; - - // Apply typewriter effect for immediate display - const brokenText = breakText(result.thinking_process); - await typewriterEffect(brokenText, 10); - - context.setMessages((prev) => { - const lastMsg = prev[prev.length - 1]; - if (lastMsg?.role === 'assistant' && lastMsg.id === input.chatId) { - return [ - ...prev.slice(0, -1), - { - ...lastMsg, - content: lastMsg.content + '\n\n', // 追加两个换行 - }, - ]; - } - return prev; - }); - } -}; import { toast } from 'sonner'; import { - taskPrompt, + leaderPrompt, findbugPrompt, refactorPrompt, optimizePrompt, @@ -98,12 +12,102 @@ import { } from './agentPrompt'; import { startChatStream } from '@/api/ChatStreamAPI'; import { parseXmlToJson } from '@/utils/parser'; +import { ChatInputType } from '@/graphql/type'; + +/** + * Helper function to save and display a thinking process (or any provided text) + * with a typewriter effect. + * + * Usage: + * await saveThinkingProcess("The thinking process text", input, context); + * + * This function stores the text in context.accumulatedThoughts and then gradually + * displays it using a typewriter effect. After the entire text is shown, it appends + * two newline characters. + */ +const saveThinkingProcess = async ( + thinkingText: string, + input: ChatInputType, + context: AgentContext +): Promise => { + if (!thinkingText) return; + + // Accumulate the thinking process text for historical record. + context.accumulatedThoughts.push(thinkingText); + + // Function to break the text into small chunks for a smoother typewriter animation. + const breakText = (text: string): string[] => { + return text.match(/(\S{1,3}|\s+)/g) || []; + }; + + // Typewriter effect: gradually display the text chunks with a specified delay. + const typewriterEffect = async ( + textArray: string[], + delay: number + ): Promise => { + return new Promise((resolve) => { + let index = 0; + + const updateMessage = () => { + if (index < textArray.length) { + context.setMessages((prev) => { + const lastMsg = prev[prev.length - 1]; + if (lastMsg?.role === 'assistant' && lastMsg.id === input.chatId) { + return [ + ...prev.slice(0, -1), + { + ...lastMsg, + content: lastMsg.content + textArray[index], + }, + ]; + } else { + return [ + ...prev, + { + id: input.chatId, + role: 'assistant', + content: textArray[index], + createdAt: new Date().toISOString(), + }, + ]; + } + }); + + index++; + setTimeout(updateMessage, delay); + } else { + resolve(); + } + }; + updateMessage(); + }); + }; + + // Break the provided thinkingText into chunks and display them. + const brokenText = breakText(thinkingText); + await typewriterEffect(brokenText, 10); + + // After displaying all chunks, append two newlines to the final message. + context.setMessages((prev) => { + const lastMsg = prev[prev.length - 1]; + if (lastMsg?.role === 'assistant' && lastMsg.id === input.chatId) { + return [ + ...prev.slice(0, -1), + { + ...lastMsg, + content: lastMsg.content + '\n\n', + }, + ]; + } + return prev; + }); +}; /** * Get the corresponding prompt based on the task type. */ function getPromptByTaskType( - task_type: TaskType, + task_type: TaskType | null, message: string, fileStructure: string[] ): string { @@ -114,6 +118,8 @@ function getPromptByTaskType( return refactorPrompt(message, fileStructure); case TaskType.OPTIMIZE: return optimizePrompt(message, fileStructure); + case TaskType.UNRELATED: + return; default: throw new Error(`Unsupported task type: ${task_type}`); } @@ -129,9 +135,44 @@ export async function taskTool( ): Promise { console.log('taskTool called with input:', input); - // Select the appropriate prompt based on the task type from the context. + // First analyze the task + const taskAnalysisPrompt = leaderPrompt(input.message); + const taskResponse = await startChatStream( + { + chatId: input.chatId, + message: taskAnalysisPrompt, + model: input.model, + role: input.role, + }, + context.token + ); + + // Parse and validate task analysis response + const taskResult = parseXmlToJson(taskResponse); + // Display AI's task analysis with typewriter effect + await saveThinkingProcess( + `Task Analysis: This is a ${taskResult.task_type} task.\n${taskResult.description || ''}`, + input, + context + ); + + // Update task type based on analysis + context.task_type = taskResult.task_type; + + // Handle unrelated tasks early + if (taskResult.task_type === TaskType.UNRELATED) { + // Display AI's task analysis with typewriter effect + await saveThinkingProcess( + "I apologize, but this question isn't related to code or the project. I'm a code assistant focused on helping with programming tasks.", + input, + context + ); + return; + } + + // For code-related tasks, get the specific prompt const prompt = getPromptByTaskType( - context.task_type, + taskResult.task_type, input.message, context.fileStructure ); @@ -151,7 +192,7 @@ export async function taskTool( // Parse response using `parseXmlToJson` const result = parseXmlToJson(response); - await saveThinkingProcess(result, input, context); + await saveThinkingProcess(result.thinking_process, input, context); if (!result.files || !Array.isArray(result.files)) { throw new Error('Invalid response format: missing files array'); @@ -254,7 +295,11 @@ export async function editFileTool( // Parse response using `parseXmlToJson` const result = parseXmlToJson(response); - await saveThinkingProcess(result, input, context); + await saveThinkingProcess( + `Analyzing file changes...\nFiles to modify: ${Object.keys(result.modified_files || {}).join(', ')}`, + input, + context + ); // Check for files that need to be read or modified if (result.files && Array.isArray(result.files)) { @@ -384,7 +429,11 @@ export async function codeReviewTool( // Parse review results using `parseXmlToJson` const result = parseXmlToJson(response); - await saveThinkingProcess(result, input, context); + await saveThinkingProcess( + `Code Review:\n${result.review_result}\n\nComments:\n${result.comments?.join('\n')}`, + input, + context + ); if (!result.review_result || !result.comments) { throw new Error('Invalid response format: missing review details'); From 57e4b23fd42ddb0b35b8f191b2b2aed4b82f782e Mon Sep 17 00:00:00 2001 From: Narwhal Date: Tue, 18 Mar 2025 12:48:51 -0500 Subject: [PATCH 17/19] feat(frontend): enhance task analysis and validation in managerAgent and tools for improved error handling and context management --- .../src/hooks/multi-agent/managerAgent.ts | 2 +- frontend/src/hooks/multi-agent/tools.ts | 196 +++++++++++------- 2 files changed, 124 insertions(+), 74 deletions(-) diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index d93462c3..96125378 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -148,7 +148,7 @@ export async function managerAgent( const toolInput = { ...input, role: `assistant`, - message: decision.next_step, + message: decision.next_step.description, }; await toolNode.behavior(toolInput, context); diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index 77629a26..b15aae97 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -111,6 +111,18 @@ function getPromptByTaskType( message: string, fileStructure: string[] ): string { + // Validate inputs + if (!task_type) { + throw new Error('Task type is required'); + } + if (!message || typeof message !== 'string') { + throw new Error('Message is required and must be a string'); + } + if (!Array.isArray(fileStructure)) { + throw new Error('File structure must be an array'); + } + + // Handle task types switch (task_type) { case TaskType.DEBUG: return findbugPrompt(message, fileStructure); @@ -119,7 +131,7 @@ function getPromptByTaskType( case TaskType.OPTIMIZE: return optimizePrompt(message, fileStructure); case TaskType.UNRELATED: - return; + throw new Error('Cannot generate prompt for unrelated tasks'); default: throw new Error(`Unsupported task type: ${task_type}`); } @@ -129,94 +141,128 @@ function getPromptByTaskType( * Task analysis tool: * Analyzes requirements and identifies relevant files. */ -export async function taskTool( +export const taskTool = async ( input: ChatInputType, context: AgentContext -): Promise { - console.log('taskTool called with input:', input); +): Promise => { + if (!input || !context) { + throw new Error('Invalid input or context'); + } - // First analyze the task - const taskAnalysisPrompt = leaderPrompt(input.message); - const taskResponse = await startChatStream( - { - chatId: input.chatId, - message: taskAnalysisPrompt, - model: input.model, - role: input.role, - }, - context.token - ); + try { + console.log('Starting task analysis...'); + console.log('Current input.message:', input.message); + const prompt = leaderPrompt(input.message); + + // Get task analysis response + const taskResponse = await startChatStream( + { + chatId: input.chatId, + message: prompt, + model: input.model, + role: input.role, + }, + context.token + ); - // Parse and validate task analysis response - const taskResult = parseXmlToJson(taskResponse); - // Display AI's task analysis with typewriter effect - await saveThinkingProcess( - `Task Analysis: This is a ${taskResult.task_type} task.\n${taskResult.description || ''}`, - input, - context - ); + // Parse task analysis response + const result = parseXmlToJson(taskResponse); + if (!result || typeof result !== 'object') { + throw new Error('Invalid task analysis response format'); + } - // Update task type based on analysis - context.task_type = taskResult.task_type; + // Validate task type + const taskType = result.task_type; + if (!taskType || !Object.values(TaskType).includes(taskType)) { + throw new Error(`Invalid task type: ${taskType || 'undefined'}`); + } - // Handle unrelated tasks early - if (taskResult.task_type === TaskType.UNRELATED) { - // Display AI's task analysis with typewriter effect + // Update context and display analysis + context.task_type = taskType; await saveThinkingProcess( - "I apologize, but this question isn't related to code or the project. I'm a code assistant focused on helping with programming tasks.", + `Task Analysis: This is a ${taskType} task.\n${result.description || ''}`, input, context ); - return; - } - // For code-related tasks, get the specific prompt - const prompt = getPromptByTaskType( - taskResult.task_type, - input.message, - context.fileStructure - ); + // Handle unrelated tasks with early return + if (taskType === TaskType.UNRELATED) { + await saveThinkingProcess( + "This question isn't related to code or development. Please ask coding-related questions.", + input, + context + ); + return; + } - console.log(context.task_type); + // Get and validate file analysis prompt + const fileAnalysisPrompt = getPromptByTaskType( + taskType, + input.message, + context.fileStructure + ); + if (!fileAnalysisPrompt) { + throw new Error( + `Failed to generate analysis prompt for task type: ${taskType}` + ); + } - // Get AI response - const response = await startChatStream( - { - chatId: input.chatId, - message: prompt, - model: input.model, - role: input.role, - }, - context.token - ); + // Get file analysis from AI + await saveThinkingProcess('Analyzing project files...', input, context); + const fileResponse = await startChatStream( + { + chatId: input.chatId, + message: fileAnalysisPrompt, + model: input.model, + role: input.role, + }, + context.token + ); - // Parse response using `parseXmlToJson` - const result = parseXmlToJson(response); - await saveThinkingProcess(result.thinking_process, input, context); + // Parse and validate file analysis response + const fileResult = parseXmlToJson(fileResponse); + if (!fileResult || typeof fileResult !== 'object') { + throw new Error('Invalid file analysis response format'); + } - if (!result.files || !Array.isArray(result.files)) { - throw new Error('Invalid response format: missing files array'); - } + // Validate and process identified files + const files = fileResult.files; + if (!Array.isArray(files)) { + throw new Error('Invalid file analysis: missing files array'); + } - // Validate and filter file paths - const validFiles = result.files.filter( - (path) => !!path && typeof path === 'string' - ); - if (validFiles.length === 0) { - throw new Error('No valid files identified in the response'); - } + // Filter and validate file paths + const validFiles = files.filter( + (path) => typeof path === 'string' && path.length > 0 + ); + if (validFiles.length === 0) { + throw new Error('No valid files identified in the analysis'); + } - context.requiredFiles = validFiles; - console.log('Valid files to process:', validFiles); + // Show analysis results + await saveThinkingProcess( + `Found ${validFiles.length} relevant files:\n${validFiles.join('\n')}`, + input, + context + ); - // Set next step to read file content - context.currentStep = { - tool: 'readFileTool', - status: 'pending', - description: `Read ${result.files.length} identified files`, - }; - console.log('Task analysis completed, files identified:', result.files); -} + // Update context with next steps + context.requiredFiles = validFiles; + context.currentStep = { + tool: 'readFileTool', + status: 'pending', + description: `Reading ${validFiles.length} identified files`, + }; + } catch (error) { + console.error('Task analysis error:', error); + await saveThinkingProcess( + `Error during task analysis: ${error.message}`, + input, + context + ); + throw error; + } +}; /** * Read file tool: @@ -464,7 +510,7 @@ export async function commitChangesTool( // Generate commit message prompt const formattedChanges = Object.entries(context.modifiedFiles) - .map(([filePath, content]) => `Modified file: ${filePath}`) + .map(([filePath, content]) => `Modified file: ${filePath}: ${content}`) .join('\n'); const prompt = commitChangesPrompt(formattedChanges); @@ -482,7 +528,11 @@ export async function commitChangesTool( // Parse commit message using `parseXmlToJson` const result = parseXmlToJson(response); - await saveThinkingProcess(result, input, context); + await saveThinkingProcess( + `Generated commit message:\n${result.commit_message}`, + input, + context + ); if (!result.commit_message) { throw new Error('Invalid response format: missing commit message'); From e7b2935df8c38750fe695d39851c3bbdc1795a41 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Thu, 20 Mar 2025 23:49:06 -0500 Subject: [PATCH 18/19] refactor(frontend): remove console logs for cleaner code and improved performance --- frontend/src/hooks/multi-agent/agentPrompt.ts | 4 +-- .../src/hooks/multi-agent/managerAgent.ts | 13 -------- frontend/src/hooks/multi-agent/tools.ts | 33 ++----------------- frontend/src/hooks/useChatStream.ts | 7 ++-- frontend/src/utils/file-reader.ts | 2 -- frontend/src/utils/parser.ts | 4 --- 6 files changed, 7 insertions(+), 56 deletions(-) diff --git a/frontend/src/hooks/multi-agent/agentPrompt.ts b/frontend/src/hooks/multi-agent/agentPrompt.ts index 5aaa33b8..25c4a2a7 100644 --- a/frontend/src/hooks/multi-agent/agentPrompt.ts +++ b/frontend/src/hooks/multi-agent/agentPrompt.ts @@ -1,3 +1,5 @@ +import { Message } from '@/const/MessageType'; +import { getToolUsageMap } from './toolNodes'; export enum TaskType { DEBUG = 'debug', REFACTOR = 'refactor', @@ -367,8 +369,6 @@ The final commit message will follow **conventional commit standards** to ensure Failure is Not an Option! If you fail, the changes will not be committed.` ); }; -import { Message } from '@/const/MessageType'; -import { getToolUsageMap } from './toolNodes'; export const confirmationPrompt = ( taskType: string, diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index 96125378..f0aaec8f 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -43,8 +43,6 @@ export async function managerAgent( setFilePath: (path: string) => void, editorRef: React.MutableRefObject ): Promise { - console.log('managerAgent called with input:', input); - try { // Initialize context const context: AgentContext = { @@ -80,9 +78,7 @@ export async function managerAgent( ); const data = await response.json(); context.fileStructure = validateFiles(data.res); - console.log('Initialized file structure:', context.fileStructure); } catch (error) { - console.error('Error fetching file structure:', error); throw error; } @@ -112,9 +108,7 @@ export async function managerAgent( let decision; try { decision = parseXmlToJson(response); - console.log('Parsed AI Decision:', decision); } catch (error) { - console.error('Error parsing AI response:', error); throw error; } @@ -130,7 +124,6 @@ export async function managerAgent( description: decision.next_step.description, }; - console.log('required Files:', decision.next_step.files); context.requiredFiles = [ ...context.requiredFiles, ...decision.next_step.files.filter( @@ -157,7 +150,6 @@ export async function managerAgent( context.currentStep.status = 'completed'; iteration++; - console.log(`Task iteration ${iteration}/${MAX_ITERATIONS}`); if (context.task_type == TaskType.UNRELATED) { break; } @@ -186,9 +178,7 @@ export async function managerAgent( newContent: content, }), }); - console.log(`File updated: ${filePath}`); } catch (error) { - console.error(`Error updating file ${filePath}:`, error); throw error; } } @@ -212,10 +202,7 @@ export async function managerAgent( }, }); } - - console.log('Task completed successfully with updated files'); } catch (error) { - console.error('Error in managerAgent:', error); throw error; } } diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index b15aae97..f848fe7e 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -150,8 +150,6 @@ export const taskTool = async ( } try { - console.log('Starting task analysis...'); - console.log('Current input.message:', input.message); const prompt = leaderPrompt(input.message); // Get task analysis response @@ -254,7 +252,6 @@ export const taskTool = async ( description: `Reading ${validFiles.length} identified files`, }; } catch (error) { - console.error('Task analysis error:', error); await saveThinkingProcess( `Error during task analysis: ${error.message}`, input, @@ -272,8 +269,6 @@ export async function readFileTool( input: ChatInputType, context: AgentContext ): Promise { - console.log('readFileTool called with input:', input); - if (!context.requiredFiles || context.requiredFiles.length === 0) { throw new Error( 'No files specified to read. Make sure requiredFiles is set.' @@ -281,7 +276,6 @@ export async function readFileTool( } // Read file content for each required file - console.log('Reading files:', context.requiredFiles); await Promise.all( context.requiredFiles.map(async (filePath: string) => { try { @@ -296,14 +290,11 @@ export async function readFileTool( const data = await response.json(); context.fileContents[filePath] = data.content; } catch (error) { - console.error(`Error reading file ${filePath}:`, error); throw error; } }) ); - console.log('Files read:', context.requiredFiles); - console.log('File contents loaded:', context.fileContents); toast.success(`${context.requiredFiles.length} files loaded successfully`); } @@ -315,9 +306,6 @@ export async function editFileTool( input: ChatInputType, context: AgentContext ): Promise { - console.log('editFileTool called with input:', input); - console.log('Current file contents:', context.fileContents); - if (Object.keys(context.fileContents).length === 0) { throw new Error( 'No file contents available. Make sure readFileTool was called first.' @@ -326,7 +314,6 @@ export async function editFileTool( // Generate edit prompt const prompt = editFilePrompt(input.message, context.fileContents); - console.log('Generated prompt with file contents for editing'); // Get AI response const response = await startChatStream( @@ -350,7 +337,6 @@ export async function editFileTool( // Check for files that need to be read or modified if (result.files && Array.isArray(result.files)) { context.requiredFiles = result.files; - console.log('New files identified:', result.files); } if (!result.modified_files || typeof result.modified_files !== 'object') { @@ -360,7 +346,6 @@ export async function editFileTool( // Update required files and content in context const modifiedPaths = Object.keys(result.modified_files); const validPaths = modifiedPaths.filter((path) => !!path); - console.log('Adding new paths to required files:', validPaths); context.requiredFiles = Array.from( new Set([...context.requiredFiles, ...validPaths]) ); @@ -378,10 +363,9 @@ export async function editFileTool( realContent = JSON.parse(realContent); } } catch (error) { - console.error('Error parsing code content:', error); + throw error; } - console.log(`Starting line-by-line changes for ${filePath}`); const lines = realContent.split('\n'); let accumulatedContent = ''; @@ -397,11 +381,10 @@ export async function editFileTool( for (let i = 0; i < lines.length; i++) { accumulatedContent = lines.slice(0, i + 1).join('\n'); context.editorRef.current.setValue(accumulatedContent); - console.log(`Updated line ${i + 1}/${lines.length} in ${filePath}`); await new Promise((resolve) => setTimeout(resolve, 100)); } } catch (error) { - console.error('Error updating editor content:', error); + throw error; } // Sync final content @@ -412,8 +395,6 @@ export async function editFileTool( context.modifiedFiles[filePath] = realContent; context.fileContents[filePath] = realContent; } - - console.log('Updated files with line-by-line animation of changes'); } /** @@ -424,8 +405,6 @@ export async function applyChangesTool( input: ChatInputType, context: AgentContext ): Promise { - console.log('applyChangesTool called with input:', input); - // Validate modification content Object.entries(context.modifiedFiles).forEach(([filePath, content]) => { if (!content || typeof content !== 'string') { @@ -438,7 +417,6 @@ export async function applyChangesTool( context.fileContents[filePath] = content; }); - console.log('Updated context with confirmed changes'); toast.success('Changes applied successfully'); } @@ -450,8 +428,6 @@ export async function codeReviewTool( input: ChatInputType, context: AgentContext ): Promise { - console.log('codeReviewTool called with input:', input); - // Generate review prompt const formattedMessage = Object.entries(context.modifiedFiles) .map( @@ -494,8 +470,6 @@ export async function codeReviewTool( description: 'Address review comments', }; } - - console.log('Code review completed'); } /** @@ -506,8 +480,6 @@ export async function commitChangesTool( input: ChatInputType, context: AgentContext ): Promise { - console.log('commitChangesTool called with input:', input); - // Generate commit message prompt const formattedChanges = Object.entries(context.modifiedFiles) .map(([filePath, content]) => `Modified file: ${filePath}: ${content}`) @@ -539,5 +511,4 @@ export async function commitChangesTool( } context.commitMessage = result.commit_message; - console.log('Generated commit message:', result.commit_message); } diff --git a/frontend/src/hooks/useChatStream.ts b/frontend/src/hooks/useChatStream.ts index 598fec9f..96f06cef 100644 --- a/frontend/src/hooks/useChatStream.ts +++ b/frontend/src/hooks/useChatStream.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useContext, use } from 'react'; +import { useState, useCallback, useEffect, useContext } from 'react'; import { useMutation } from '@apollo/client'; import { CREATE_CHAT, SAVE_MESSAGE } from '@/graphql/request'; import { Message } from '@/const/MessageType'; @@ -31,12 +31,13 @@ export const useChatStream = ({ const { curProject, refreshProjects, setFilePath, editorRef } = useContext(ProjectContext); const [curProjectPath, setCurProjectPath] = useState(''); + useEffect(() => { - console.log('curProject:', curProject); if (curProject) { setCurProjectPath(curProject.projectPath); } }, [curProject]); + // Use useEffect to handle new chat event and cleanup useEffect(() => { const updateChatId = () => { @@ -127,8 +128,6 @@ export const useChatStream = ({ setMessages((prev) => [...prev, newMessage]); if (!currentChatId) { - console.log('currentChatId: ' + currentChatId); - console.log('Creating new chat...'); try { await createChat({ variables: { diff --git a/frontend/src/utils/file-reader.ts b/frontend/src/utils/file-reader.ts index b0c92797..1adaec77 100644 --- a/frontend/src/utils/file-reader.ts +++ b/frontend/src/utils/file-reader.ts @@ -88,8 +88,6 @@ export class FileReader { try { const content = newContent.trim(); - console.log(content); - console.log('log content'); await fs.writeFile(fullPath, content, 'utf-8'); logger.info('[FileReader] File updated successfully'); diff --git a/frontend/src/utils/parser.ts b/frontend/src/utils/parser.ts index 3c111fe3..a6c602ee 100644 --- a/frontend/src/utils/parser.ts +++ b/frontend/src/utils/parser.ts @@ -2,16 +2,12 @@ * Parses AI response wrapped inside tags into a JSON object. */ export const parseXmlToJson = (xmlString: string) => { - console.log(xmlString); const match = xmlString.match(/([\s\S]*?)<\/jsonResponse>/); - console.log(match); - console.log(match[1]); if (!match || !match[1]) { throw new Error('Invalid XML: No element found'); } const jsonText = match[1].trim(); - console.log(jsonText); try { return JSON.parse(jsonText); From f26d1d6c5ab916a0cfb8738813022b2bbf6d25c9 Mon Sep 17 00:00:00 2001 From: Narwhal Date: Thu, 20 Mar 2025 23:56:29 -0500 Subject: [PATCH 19/19] feat(frontend): add error handling with toast notifications in managerAgent and tools for improved user feedback --- .../src/hooks/multi-agent/managerAgent.ts | 28 +++++++++---------- frontend/src/hooks/multi-agent/tools.ts | 3 ++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/frontend/src/hooks/multi-agent/managerAgent.ts b/frontend/src/hooks/multi-agent/managerAgent.ts index f0aaec8f..a66a597e 100644 --- a/frontend/src/hooks/multi-agent/managerAgent.ts +++ b/frontend/src/hooks/multi-agent/managerAgent.ts @@ -5,6 +5,7 @@ import { confirmationPrompt, AgentContext, TaskType } from './agentPrompt'; import path from 'path'; import { parseXmlToJson } from '@/utils/parser'; import { Message } from '@/const/MessageType'; +import { toast } from 'sonner'; /** * Normalize file paths. @@ -67,20 +68,16 @@ export async function managerAgent( }; // Retrieve project file structure - try { - const response = await fetch( - `/api/filestructure?path=${encodeURIComponent(projectPath)}`, - { - method: 'GET', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - } - ); - const data = await response.json(); - context.fileStructure = validateFiles(data.res); - } catch (error) { - throw error; - } + const response = await fetch( + `/api/filestructure?path=${encodeURIComponent(projectPath)}`, + { + method: 'GET', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + } + ); + const data = await response.json(); + context.fileStructure = validateFiles(data.res); const MAX_ITERATIONS = 60; const TIMEOUT = 30 * 60 * 1000; // 30-minute timeout @@ -109,6 +106,7 @@ export async function managerAgent( try { decision = parseXmlToJson(response); } catch (error) { + toast.error('Failed to parse AI response'); throw error; } @@ -179,6 +177,7 @@ export async function managerAgent( }), }); } catch (error) { + toast.error('Failed to save file'); throw error; } } @@ -203,6 +202,7 @@ export async function managerAgent( }); } } catch (error) { + toast.error('Failed to complete task'); throw error; } } diff --git a/frontend/src/hooks/multi-agent/tools.ts b/frontend/src/hooks/multi-agent/tools.ts index f848fe7e..081d5763 100644 --- a/frontend/src/hooks/multi-agent/tools.ts +++ b/frontend/src/hooks/multi-agent/tools.ts @@ -290,6 +290,7 @@ export async function readFileTool( const data = await response.json(); context.fileContents[filePath] = data.content; } catch (error) { + toast.error(`Failed to read file: ${filePath}`); throw error; } }) @@ -363,6 +364,7 @@ export async function editFileTool( realContent = JSON.parse(realContent); } } catch (error) { + toast.error('Failed to parse file content'); throw error; } @@ -384,6 +386,7 @@ export async function editFileTool( await new Promise((resolve) => setTimeout(resolve, 100)); } } catch (error) { + toast.error('Failed to animate file content'); throw error; }