From 4224502c44ff210631aff9e4f56b82317e81bd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=86=E6=B2=89?= Date: Sat, 24 Aug 2024 14:32:12 +0800 Subject: [PATCH] feat: support issue comments --- server/agent/prompts/issue_helper.py | 33 ++++++++++++ server/agent/qa_chat.py | 21 +++++--- server/agent/tools/issue.py | 13 +++-- server/chat/router.py | 8 ++- server/event_handler/issue.py | 76 ++++++++++++++++++++++++---- server/github_app/handlers.py | 5 +- 6 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 server/agent/prompts/issue_helper.py diff --git a/server/agent/prompts/issue_helper.py b/server/agent/prompts/issue_helper.py new file mode 100644 index 00000000..6f1cac26 --- /dev/null +++ b/server/agent/prompts/issue_helper.py @@ -0,0 +1,33 @@ +ISSUE_PROMPT = """ +# Task +You have required to resolve an issue {issue_url} now: + +## Issue Content: +{issue_content} + +# Constraints: +First, carefully analyze the user’s requirements. +Then, search similar issues. +Make sure your suggestions are well-explained and align with the user’s needs. + +""" + +ISSUE_COMMENT_PROMPT = """ +# Task +You have required to resolve an issue {issue_url} now: + +## Issue Content: +{issue_content} + +# Constraints: +- Summarize user needs based on the issue content and information. +- Avoid repeating answers. If you have previously given a similar response, please apologize. + +""" + +def generate_issue_prompt(issue_url: str, issue_content: str): + return ISSUE_PROMPT.format(issue_url=issue_url, issue_content=issue_content) + +def generate_issue_comment_prompt(issue_url: str, issue_content: str): + return ISSUE_COMMENT_PROMPT.format(issue_url=issue_url, issue_content=issue_content) + diff --git a/server/agent/qa_chat.py b/server/agent/qa_chat.py index bff0c681..65f69867 100644 --- a/server/agent/qa_chat.py +++ b/server/agent/qa_chat.py @@ -1,4 +1,5 @@ from typing import AsyncIterator, Optional +from github import Auth from agent.base import AgentBuilder from agent.llm import get_llm from core.dao.botDAO import BotDAO @@ -9,8 +10,8 @@ from agent.tools import issue, sourcecode, knowledge, git_info -def get_tools(bot: Bot, token: Optional[str]): - issue_tools = issue.factory(access_token=token) +def get_tools(bot: Bot, token: Optional[Auth.Token]): + issue_tools = issue.factory(token=token) return { "search_knowledge": knowledge.factory(bot_id=bot.id), "create_issue": issue_tools["create_issue"], @@ -20,25 +21,31 @@ def get_tools(bot: Bot, token: Optional[str]): "search_repo": git_info.search_repo, } -def agent_stream_chat(input_data: ChatData, user_token: str) -> AsyncIterator[str]: +def agent_stream_chat(input_data: ChatData, token: Auth.Token) -> AsyncIterator[str]: bot_dao = BotDAO() bot = bot_dao.get_bot(input_data.bot_id) agent = AgentBuilder( chat_model=get_llm(bot.llm), prompt=bot.prompt or generate_prompt_by_repo_name("ant-design"), - tools=get_tools(bot=bot, token=user_token), + tools=get_tools(bot=bot, token=token), ) return agent.run_stream_chat(input_data) -def agent_chat(input_data: ChatData, user_token: Optional[str], llm: Optional[str] = "openai") -> AsyncIterator[str]: +def agent_chat(input_data: ChatData, token: Auth.Token) -> AsyncIterator[str]: bot_dao = BotDAO() bot = bot_dao.get_bot(input_data.bot_id) + prompt = bot.prompt or generate_prompt_by_repo_name("ant-design") + if input_data.prompt is not None: + prompt = f"{prompt}\n\n{input_data.prompt}" + print(f"agent_chat: prompt={prompt}") + agent = AgentBuilder( chat_model=get_llm(bot.llm), - prompt=bot.prompt or generate_prompt_by_repo_name("ant-design"), - tools=get_tools(bot, token=user_token), + prompt=prompt, + tools=get_tools(bot, token=token), ) + return agent.run_chat(input_data) diff --git a/server/agent/tools/issue.py b/server/agent/tools/issue.py index 0111f4cb..d64df011 100644 --- a/server/agent/tools/issue.py +++ b/server/agent/tools/issue.py @@ -7,7 +7,7 @@ DEFAULT_REPO_NAME = "ant-design/ant-design" -def factory(access_token: Optional[str]): +def factory(token: Optional[Auth.Token]): @tool def create_issue(repo_name: str, title: str, body: str): """ @@ -19,10 +19,9 @@ def create_issue(repo_name: str, title: str, body: str): :param title: The title of the issue to be created :param body: The content of the issue to be created """ - if access_token is None: + if token is None: return need_github_login() - auth = Auth.Token(token=access_token) - g = Github(auth=auth) + g = Github(auth=token) try: # Get the repository object repo = g.get_repo(repo_name) @@ -93,7 +92,11 @@ def search_issues( :param order: The order of the sorting, e.g: asc, desc :param state: The state of the issue, e.g: open, closed, all """ - g = Github() + if token is None: + g = Github() + else: + g = Github(auth=token) + try: search_query = f"{keyword} in:title,body,comments repo:{repo_name}" # Retrieve a list of open issues from the repository diff --git a/server/chat/router.py b/server/chat/router.py index c1bb740f..3b052eff 100644 --- a/server/chat/router.py +++ b/server/chat/router.py @@ -1,4 +1,5 @@ from typing import Annotated, Optional +from github import Auth from fastapi import APIRouter, Depends from fastapi.responses import StreamingResponse from petercat_utils.data_class import ChatData @@ -32,8 +33,10 @@ def run_qa_chat( print( f"run_qa_chat: input_data={input_data}, user_access_token={user_access_token}" ) + + token = Auth.Token(user_access_token) if user_access_token is not None else None result = qa_chat.agent_stream_chat( - input_data=input_data, user_token=user_access_token + input_data=input_data, token=token ) return StreamingResponse(result, media_type="text/event-stream") @@ -43,7 +46,8 @@ async def run_issue_helper( input_data: ChatData, user_access_token: Annotated[str | None, Depends(get_user_access_token)] = None, ): - result = await qa_chat.agent_chat(input_data, user_access_token) + token = Auth.Token(user_access_token) if user_access_token is not None else None + result = await qa_chat.agent_chat(input_data, token) return result diff --git a/server/event_handler/issue.py b/server/event_handler/issue.py index f595e4f3..4fc207fe 100644 --- a/server/event_handler/issue.py +++ b/server/event_handler/issue.py @@ -1,6 +1,9 @@ -from typing import Any +from typing import Any, Tuple from github import Github, Auth +from github.Issue import Issue +from github.Repository import Repository from github import GithubException +from agent.prompts.issue_helper import generate_issue_comment_prompt, generate_issue_prompt from core.dao.repositoryConfigDAO import RepositoryConfigDAO from petercat_utils.data_class import ChatData, Message, TextContentBlock @@ -8,6 +11,7 @@ from agent.qa_chat import agent_chat + class IssueEventHandler: event: Any auth: Auth.AppAuth @@ -18,25 +22,75 @@ def __init__(self, payload: Any, auth: Auth.AppAuth, installation_id: int) -> No self.auth: Auth.AppAuth = auth self.g: Github = Github(auth=auth) + def get_issue(self) -> Tuple[Issue, Repository]: + repo_name = self.event["repository"]["full_name"] + issue_number = self.event["issue"]["number"] + repo = self.g.get_repo(repo_name) + # GET ISSUES + issue = repo.get_issue(number=issue_number) + return issue, repo + async def execute(self): - repository_config = RepositoryConfigDAO() try: print("actions:", self.event["action"]) if self.event["action"] == "opened": - repo_name = self.event["repository"]["full_name"] - issue_number = self.event["issue"]["number"] - repo = self.g.get_repo(repo_name) - issue = repo.get_issue(number=issue_number) + issue, repo = self.get_issue() + + prompt = generate_issue_prompt(issue_url=issue.url, issue_content=issue.body) issue_content = f"{issue.title}: {issue.body}" - text_block = TextContentBlock(type="text", text=issue_content) - issue_content = issue.body - message = Message(role="user", content=[text_block]) + message = Message(role="user", content=[TextContentBlock(type="text", text=issue_content)]) + + repository_config = RepositoryConfigDAO() + repo_config = repository_config.get_by_repo_name(repo.full_name) + + analysis_result = await agent_chat( + ChatData( + prompt=prompt, + messages=[message], + bot_id=repo_config.robot_id + ), self.auth) - repo_config = repository_config.get_by_repo_name(repo_name) - analysis_result = await agent_chat(ChatData(messages=[message], bot_id=repo_config.robot_id), None) issue.create_comment(analysis_result["output"]) return {"success": True} except GithubException as e: print(f"处理 GitHub 请求时出错:{e}") return {"success": False, "error": str(e)} + +class IssueCommentEventHandler(IssueEventHandler): + async def execute(self): + try: + print(f"actions={self.event['action']},sender={self.event['sender']}") + # 忽略机器人回复 + if self.event['sender']['type'] == "Bot": + return {"success": True} + if self.event["action"] == "created": + issue, repo = self.get_issue() + issue_comments = issue.get_comments() + + messages = [ + Message( + role="assistant" if comment.user.type == "Bot" else "user", + content=[TextContentBlock(type="text", text=comment.body)], + ) for comment in issue_comments + ] + + print(f"messages={messages}") + issue_content = f"{issue.title}: {issue.body}" + prompt = generate_issue_comment_prompt(issue_url=issue.url, issue_content=issue_content) + + repository_config = RepositoryConfigDAO() + repo_config = repository_config.get_by_repo_name(repo.full_name) + + analysis_result = await agent_chat( + ChatData( + prompt=prompt, + messages=messages, + bot_id=repo_config.robot_id + ), self.auth) + + issue.create_comment(analysis_result["output"]) + + except GithubException as e: + print(f"处理 GitHub 请求时出错:{e}") + return {"success": False, "error": str(e)} \ No newline at end of file diff --git a/server/github_app/handlers.py b/server/github_app/handlers.py index 0b29aa73..fa63813a 100644 --- a/server/github_app/handlers.py +++ b/server/github_app/handlers.py @@ -5,15 +5,16 @@ from event_handler.pull_request import PullRequestEventHandler from event_handler.discussion import DiscussionEventHandler -from event_handler.issue import IssueEventHandler +from event_handler.issue import IssueEventHandler, IssueCommentEventHandler APP_ID = get_env_variable("X_GITHUB_APP_ID") -def get_handler(event: str, payload: dict, auth: Auth.AppAuth, installation_id: int) -> Union[PullRequestEventHandler, IssueEventHandler, DiscussionEventHandler, None]: +def get_handler(event: str, payload: dict, auth: Auth.AppAuth, installation_id: int) -> Union[PullRequestEventHandler, IssueCommentEventHandler, IssueEventHandler, DiscussionEventHandler, None]: handlers = { 'pull_request': PullRequestEventHandler, 'issues': IssueEventHandler, + "issue_comment": IssueCommentEventHandler, 'discussion': DiscussionEventHandler } return handlers.get(event)(payload=payload, auth=auth, installation_id=installation_id) if event in handlers else None