Skip to content

Commit

Permalink
feat: 支持小猫猫做 codeReview (#260)
Browse files Browse the repository at this point in the history
- 支持生成摘要
- 支持 CR
  • Loading branch information
RaoHai authored Aug 26, 2024
2 parents 58cd01a + c4c8621 commit 417548e
Show file tree
Hide file tree
Showing 17 changed files with 1,152 additions and 39 deletions.
17 changes: 10 additions & 7 deletions server/agent/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from langchain.agents.format_scratchpad.openai_tools import (
format_to_openai_tool_messages,
)
from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage
from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage, SystemMessage
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
from langchain.prompts import MessagesPlaceholder
from langchain_core.prompts import ChatPromptTemplate
Expand Down Expand Up @@ -92,12 +92,15 @@ def get_prompt(self):
def chat_history_transform(self, messages: list[Message]):
transformed_messages = []
for message in messages:
if message.role == "user":
transformed_messages.append(HumanMessage(self.chat_model.parse_content(content=message.content)))
elif message.role == "assistant":
transformed_messages.append(AIMessage(content=message.content))
else:
transformed_messages.append(FunctionMessage(content=message.content))
match message.role:
case "user":
transformed_messages.append(HumanMessage(self.chat_model.parse_content(content=message.content)))
case "assistant":
transformed_messages.append(AIMessage(content=message.content))
case "system":
transformed_messages.append(SystemMessage(content=message.content))
case _:
transformed_messages.append(FunctionMessage(content=message.content))
return transformed_messages

async def run_stream_chat(self, input_data: ChatData) -> AsyncIterator[str]:
Expand Down
File renamed without changes.
9 changes: 9 additions & 0 deletions server/agent/bot/get_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

from core.dao.botDAO import BotDAO
from core.models.bot import Bot
from petercat_utils.data_class import ChatData

def get_bot(input_data: ChatData) -> Bot:
bot_dao = BotDAO()
bot = bot_dao.get_bot(input_data.bot_id)
return bot
4 changes: 2 additions & 2 deletions server/agent/prompts/issue_helper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ISSUE_PROMPT = """
# Task
You have required to resolve an issue {issue_url} now:
Introduce yourself and briefly explain the assistance you can provide.
If users need further support, prompt them to @ you for help. Generate a response suitable for this scenario.
## Issue Content:
{issue_content}
Expand Down
56 changes: 56 additions & 0 deletions server/agent/prompts/pull_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
PULL_REQUEST_ROLE = """
# Character Description
You are an experienced Code Reviewer, specializing in identifying critical issues and potentially incorrect code in Pull Requests (PRs).
# Skills Description
## Skill 1: Pull Request Summarize
You excel at analyzing users' code changes and get summaries.
Offering specific and targeted in pinpointing code that may lead to errors, security vulnerabilities, or significant performance issues.
You focus only on identifying and addressing severe or fundamentally flawed code practices.
You are equipped with two powerful tool2, used to leave a summary and code review comments:
- create_pr_summary; This tools is used to create summary of PR.
- create_review_comment: This tool is used to leave review comment of file.
Constraints
- Focus exclusively on identifying and reviewing highly inappropriate code usage or potential errors.
- Avoid reviewing minor style inconsistencies or non-critical issues.
- Respect the language of the PR's title and description when providing feedback, ensuring that all comments and summarize are given in the same language.
- Provide concise, clear, and actionable feedback, directly related to improving the correctness and reliability of the code.
"""

PULL_REQUEST_SUMMARY = """
# Task
You have two Pull Requst review task with basic infomation:
```
repo_name: {repo_name}
pull_number: {pull_number}
title: {title}
description: {description}
```
## Task 1: Summarize the Pull Request
Using `create_pr_summary` tool to create PR summary.
Provider your response in markdown with the following content. follow the user's language.
- **Walkthrough**: A high-level summary of the overall change instead of specific files within 80 words.
- **Changes**: A markdown table of files and their summaries. Group files with similar changes together into a single row to save space.
## Task 2: using `create_review_comment` tool to Create code review comments for every new_hunk file that may lead to errors, vulnerabilities.
## File Diff:
{file_diff}
# Constraints
- After completing the tasks, only output "All task finished".
"""

def get_role_prompt(repo_name: str, ref: str):
return PULL_REQUEST_ROLE.format(repo_name=repo_name, ref=ref)

def get_pr_summary(repo_name: str, pull_number: int, title: str, description: str, file_diff: str):
return PULL_REQUEST_SUMMARY.format(
repo_name=repo_name,
pull_number={pull_number},
title={title},
description={description},
file_diff=file_diff
)
20 changes: 10 additions & 10 deletions server/agent/qa_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,31 @@
from github import Auth
from agent.base import AgentBuilder
from agent.llm import get_llm
from core.dao.botDAO import BotDAO

from core.models.bot import Bot
from agent.prompts.bot_template import generate_prompt_by_repo_name
from petercat_utils.data_class import ChatData

from agent.tools import issue, sourcecode, knowledge, git_info
from agent.tools import issue, pull_request, sourcecode, knowledge, git_info


def get_tools(bot: Bot, token: Optional[Auth.Token]):
issue_tools = issue.factory(token=token)
pull_request_tools = pull_request.factory(token=token)

return {
"search_knowledge": knowledge.factory(bot_id=bot.id),
"create_issue": issue_tools["create_issue"],
"get_issues": issue_tools["get_issues"],
"get_file_content": pull_request_tools["get_file_content"],
"create_review_comment": pull_request_tools["create_review_comment"],
"create_pr_summary": pull_request_tools["create_pr_summary"],
"search_issues": issue_tools["search_issues"],
"search_code": sourcecode.search_code,
"search_repo": git_info.search_repo,
}

def agent_stream_chat(input_data: ChatData, token: Auth.Token) -> AsyncIterator[str]:
bot_dao = BotDAO()
bot = bot_dao.get_bot(input_data.bot_id)

def agent_stream_chat(input_data: ChatData, token: Auth.Token, bot: Bot) -> AsyncIterator[str]:
agent = AgentBuilder(
chat_model=get_llm(bot.llm),
prompt=bot.prompt or generate_prompt_by_repo_name("ant-design"),
Expand All @@ -33,14 +35,12 @@ def agent_stream_chat(input_data: ChatData, token: Auth.Token) -> AsyncIterator[
return agent.run_stream_chat(input_data)


def agent_chat(input_data: ChatData, token: Auth.Token) -> AsyncIterator[str]:
bot_dao = BotDAO()
bot = bot_dao.get_bot(input_data.bot_id)
def agent_chat(input_data: ChatData, token: Auth.Token, bot: Bot) -> AsyncIterator[str]:

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),
Expand Down
83 changes: 83 additions & 0 deletions server/agent/tools/pull_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

from typing import Optional
from github import Github, Auth
import json

from langchain.tools import tool
from agent.tools.helper import need_github_login

def factory(token: Optional[Auth.Token]):

@tool
def get_file_content(repo_name: str, path: str, ref: str):
"""
Get content of specified pull requst file
:param repo_name: The name of the repository, e.g., "ant-design/ant-design"
:param path: The path of file, e.g., /contents/file1.txt
:param ref: The name of the commit/branch/tag. Default: the repository’s default branch.
"""
if token is None:
return need_github_login()

g = Github(auth=token)
try:
repo = g.get_repo(repo_name)
content = repo.get_contents(path=path, ref=ref)
print(f"get_content: f{content}")
return json.dumps([])
except Exception as e:
print(f"An error occurred: {e}")
return json.dumps([])

@tool
def create_pr_summary(repo_name: str, pull_number: int, summary: str):
"""
Create a code review of specified pull requst file
:param repo_name: The name of the repository, e.g., "ant-design/ant-design"
:param pull_number: The number of pull requst: e.g., 123
:param summary: markdown content of PR summary
"""
if token is None:
return need_github_login()

g = Github(auth=token)
repo = g.get_repo(repo_name)
pull_request = repo.get_pull(pull_number)
pull_request.create_issue_comment(summary)

@tool
def create_review_comment(repo_name: str, pull_number: int, sha: str, path: str, line: int, comment: str):
"""
Create a code review of specified pull requst file
:param repo_name: The name of the repository, e.g., "ant-design/ant-design"
:param pull_number: The number of pull requst: e.g., 123
:param sha: The sha of file. e.g., 6dcb09b5b57875f334f61aebed695e2e4193db5e
:param path: The path of file, e.g., /contents/file1.txt
:param line: The line number to create comment at. e.g., 19
:param comment: Content of review comments
"""
if token is None:
return need_github_login()

g = Github(auth=token)
try:
repo = g.get_repo(repo_name)
pull_request = repo.get_pull(pull_number)
commit = repo.get_commit(sha=sha)
# print(f"create_review_comment, pull_request={pull_request}, commit={commit}, comment={comment}")
pull_request.create_review_comment(
body=comment,
path=path,
commit=commit,
line=line,
)

except Exception as e:
print(f"An error occurred: {e}")
return json.dumps([])

return {
"get_file_content": get_file_content,
"create_pr_summary": create_pr_summary,
"create_review_comment": create_review_comment,
}
6 changes: 2 additions & 4 deletions server/auth/get_user_info.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from fastapi import Request
import httpx
import secrets
import random
import string

from utils.random_str import random_str

from .get_oauth_token import get_oauth_token
from petercat_utils import get_client, get_env_variable

def random_str(N):
return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(N))

AUTH0_DOMAIN = get_env_variable("AUTH0_DOMAIN")

Expand Down
12 changes: 8 additions & 4 deletions server/chat/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
from github import Auth
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from agent.bot import bot_builder
from agent.bot.get_bot import get_bot
from core.models.bot import Bot
from petercat_utils.data_class import ChatData

from agent import qa_chat, bot_builder
from agent import qa_chat
from auth.rate_limit import verify_rate_limit
from auth.get_user_info import get_user_access_token, get_user_id


router = APIRouter(
prefix="/api/chat",
tags=["chat"],
Expand All @@ -29,14 +31,15 @@ async def generate_auth_failed_stream():
def run_qa_chat(
input_data: ChatData,
user_access_token: Annotated[str | None, Depends(get_user_access_token)] = None,
bot: Annotated[Bot | None, Depends(get_bot)] = None,
):
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, token=token
input_data=input_data, token=token, bot=bot
)
return StreamingResponse(result, media_type="text/event-stream")

Expand All @@ -45,9 +48,10 @@ def run_qa_chat(
async def run_issue_helper(
input_data: ChatData,
user_access_token: Annotated[str | None, Depends(get_user_access_token)] = None,
bot: Annotated[Bot | None, Depends(get_bot)] = None,
):
token = Auth.Token(user_access_token) if user_access_token is not None else None
result = await qa_chat.agent_chat(input_data, token)
result = await qa_chat.agent_chat(input_data, token, bot)
return result


Expand Down
2 changes: 1 addition & 1 deletion server/core/models/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ class Bot(BaseModel):
prompt: Optional[str] = ""
name: str
llm: Optional[str] = "openai"
created_at: datetime
created_at: datetime = datetime.now()
16 changes: 12 additions & 4 deletions server/event_handler/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
from github import GithubException
from agent.prompts.issue_helper import generate_issue_comment_prompt, generate_issue_prompt

from core.dao.botDAO import BotDAO
from core.dao.repositoryConfigDAO import RepositoryConfigDAO
from petercat_utils.data_class import ChatData, Message, TextContentBlock

from agent.qa_chat import agent_chat



class IssueEventHandler:
event: Any
auth: Auth.AppAuth
Expand Down Expand Up @@ -45,13 +45,15 @@ async def execute(self):

repository_config = RepositoryConfigDAO()
repo_config = repository_config.get_by_repo_name(repo.full_name)

bot_dao = BotDAO()
bot = bot_dao.get_bot(repo_config.robot_id)

analysis_result = await agent_chat(
ChatData(
prompt=prompt,
messages=[message],
bot_id=repo_config.robot_id
), self.auth)
), self.auth, bot)

issue.create_comment(analysis_result["output"])

Expand All @@ -61,13 +63,20 @@ async def execute(self):
return {"success": False, "error": str(e)}

class IssueCommentEventHandler(IssueEventHandler):
def not_mentioned_me(self):
return "@petercat-bot" not in self.event["comment"]["body"]

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":
# 如果没有 AT 我。就算了
if self.not_mentioned_me():
return {"success": True}

issue, repo = self.get_issue()
issue_comments = issue.get_comments()

Expand All @@ -78,7 +87,6 @@ async def execute(self):
) 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)

Expand Down
Loading

0 comments on commit 417548e

Please sign in to comment.