From 7c407882f41500e7aa98fe3baf017b95b288456b Mon Sep 17 00:00:00 2001 From: Hensley Joo Date: Wed, 13 Aug 2025 16:47:44 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20AI=20=EC=9E=90=EB=8F=99=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A6=AC=EB=B7=B0=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GitHub Actions를 통한 자동 코드리뷰 워크플로우 추가 - MCP를 활용한 GitHub PR 분석 및 리뷰 스크립트 구현 - 로컬 테스트용 스크립트 및 결과 저장 기능 추가 - 환경변수 관리 (.env.example, .gitignore) 설정 - 상세한 문서화 및 사용 가이드 제공 --- .env.example | 24 + .github/workflows/auto-code-review.yml | 38 ++ .gitignore | 6 +- ...354\204\261\355\225\230\352\270\260.ipynb" | 245 ++++++++++ README-auto-code-review.md | 160 +++++++ aa.html | 447 ++++++++++++++++++ docs/github-app-setup.md | 36 ++ requirements.txt | 4 + scripts/README-test.md | 121 +++++ .../auto_code_review.cpython-312.pyc | Bin 0 -> 7688 bytes .../auto_code_review.cpython-37.pyc | Bin 0 -> 4152 bytes scripts/auto_code_review.py | 212 +++++++++ scripts/test_code_review.py | 179 +++++++ 13 files changed, 1471 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .github/workflows/auto-code-review.yml create mode 100644 "24. MCP\353\245\274 \355\231\234\354\232\251\355\225\234 \354\275\224\353\223\234\353\246\254\353\267\260 \354\227\220\354\235\264\354\240\204\355\212\270 \354\203\235\354\204\261\355\225\230\352\270\260.ipynb" create mode 100644 README-auto-code-review.md create mode 100644 aa.html create mode 100644 docs/github-app-setup.md create mode 100644 requirements.txt create mode 100644 scripts/README-test.md create mode 100644 scripts/__pycache__/auto_code_review.cpython-312.pyc create mode 100644 scripts/__pycache__/auto_code_review.cpython-37.pyc create mode 100644 scripts/auto_code_review.py create mode 100644 scripts/test_code_review.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fb5f2f7 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# GitHub 설정 +# GitHub Personal Access Token (https://github.com/settings/tokens) +# 필요한 권한: repo, pull_requests, issues +GITHUB_PAT=your_github_personal_access_token + +# Slack 설정 (현재 비활성화됨) +# Slack Bot Token (https://api.slack.com/apps) +# SLACK_BOT_TOKEN=your_slack_bot_token + +# Slack Team ID (https://api.slack.com/methods/team.info/test) +# SLACK_TEAM_ID=your_slack_team_id + +# Slack Channel IDs (쉼표로 구분, 예: C1234567890,C0987654321) +# 채널 ID 확인: https://api.slack.com/methods/conversations.list/test +# SLACK_CHANNEL_IDS=channel_id1,channel_id2 + +# OpenAI 설정 +# OpenAI API Key (https://platform.openai.com/api-keys) +OPENAI_API_KEY=your_openai_api_key + +# 사용법: +# 1. 이 파일을 .env로 복사: cp .env.example .env +# 2. .env 파일을 편집하여 실제 토큰들을 입력 +# 3. 테스트 실행: python scripts/test_code_review.py --test-data diff --git a/.github/workflows/auto-code-review.yml b/.github/workflows/auto-code-review.yml new file mode 100644 index 0000000..c3775de --- /dev/null +++ b/.github/workflows/auto-code-review.yml @@ -0,0 +1,38 @@ +name: Auto Code Review + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [main, develop] + +jobs: + code-review: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install langchain-mcp-adapters langgraph openai python-dotenv + + - name: Run code review + env: + GITHUB_PAT: ${{ secrets.GITHUB_PAT }} + # SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + # SLACK_TEAM_ID: ${{ secrets.SLACK_TEAM_ID }} + # SLACK_CHANNEL_IDS: ${{ secrets.SLACK_CHANNEL_IDS }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + python scripts/auto_code_review.py \ + --owner ${{ github.repository_owner }} \ + --repo ${{ github.event.repository.name }} \ + --pr-number ${{ github.event.pull_request.number }} \ + --pr-url ${{ github.event.pull_request.html_url }} diff --git a/.gitignore b/.gitignore index 69ccdb7..0e93df9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,8 @@ build-storybook.log chromatic.log chromatic-build-*.xml chromatic-diagnostics.json -.turbo/ \ No newline at end of file +.turbo/ +# Environment variables +.env +.env.local +.env.*.local diff --git "a/24. MCP\353\245\274 \355\231\234\354\232\251\355\225\234 \354\275\224\353\223\234\353\246\254\353\267\260 \354\227\220\354\235\264\354\240\204\355\212\270 \354\203\235\354\204\261\355\225\230\352\270\260.ipynb" "b/24. MCP\353\245\274 \355\231\234\354\232\251\355\225\234 \354\275\224\353\223\234\353\246\254\353\267\260 \354\227\220\354\235\264\354\240\204\355\212\270 \354\203\235\354\204\261\355\225\230\352\270\260.ipynb" new file mode 100644 index 0000000..76c0c97 --- /dev/null +++ "b/24. MCP\353\245\274 \355\231\234\354\232\251\355\225\234 \354\275\224\353\223\234\353\246\254\353\267\260 \354\227\220\354\235\264\354\240\204\355\212\270 \354\203\235\354\204\261\355\225\230\352\270\260.ipynb" @@ -0,0 +1,245 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import os\n", + "from langchain_mcp_adapters.client import MultiServerMCPClient\n", + "\n", + "github_pat = os.getenv(\"GITHUB_PAT\")\n", + "slack_bot_token = os.getenv(\"SLACK_BOT_TOKEN\")\n", + "slack_team_id = os.getenv(\"SLACK_TEAM_ID\")\n", + "slack_channel_ids = os.getenv(\"SLACK_CHANNEL_IDS\")\n", + "\n", + "mcp_client = MultiServerMCPClient({\n", + " \"github\": {\n", + " \"command\": \"docker\",\n", + " \"args\": [\n", + " \"run\",\n", + " \"-i\",\n", + " \"--rm\",\n", + " \"-e\",\n", + " \"GITHUB_TOOLSETS\",\n", + " \"-e\",\n", + " \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n", + " \"ghcr.io/github/github-mcp-server\"\n", + " ],\n", + " \"env\": {\n", + " \"GITHUB_TOOLSETS\": \"context,pull_requests\",\n", + " \"GITHUB_PERSONAL_ACCESS_TOKEN\": github_pat\n", + " },\n", + " \"transport\": \"stdio\"\n", + " },\n", + " \"slack\": {\n", + " \"command\": \"docker\",\n", + " \"args\": [\n", + " \"run\",\n", + " \"-i\",\n", + " \"--rm\",\n", + " \"-e\",\n", + " \"SLACK_BOT_TOKEN\",\n", + " \"-e\",\n", + " \"SLACK_TEAM_ID\",\n", + " \"-e\",\n", + " \"SLACK_CHANNEL_IDS\",\n", + " \"mcp/slack\"\n", + " ],\n", + " \"env\": {\n", + " \"SLACK_BOT_TOKEN\": slack_bot_token,\n", + " \"SLACK_TEAM_ID\": slack_team_id,\n", + " \"SLACK_CHANNEL_IDS\": slack_channel_ids # ,로 구분된 문자열\n", + " },\n", + " \"transport\": \"stdio\"\n", + " }\n", + "})" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "tool_list = await mcp_client.get_tools()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.prebuilt import create_react_agent\n", + "\n", + "agent = create_react_agent(\n", + " model=\"openai:gpt-4.1\",\n", + " tools=tool_list,\n", + " prompt=\"Use the tools provided to you to answer the user's question\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "async def process_stream(stream_generator):\n", + " results = []\n", + " try:\n", + " async for chunk in stream_generator:\n", + "\n", + " key = list(chunk.keys())[0]\n", + " if key == 'agent':\n", + " # Agent 메시지의 내용을 가져옴. 메세지가 비어있는 경우 어떤 도구를 어떻게 호출할지 정보를 가져옴\n", + " content = chunk['agent']['messages'][0].content if chunk['agent']['messages'][0].content != '' else chunk['agent']['messages'][0].additional_kwargs\n", + " print(f\"'agent': '{content}'\")\n", + " \n", + " elif key == 'tools':\n", + " # 도구 메시지의 내용을 가져옴\n", + " for tool_msg in chunk['tools']['messages']:\n", + " print(f\"'tools': '{tool_msg.content}'\")\n", + " \n", + " results.append(chunk)\n", + " return results\n", + " except Exception as e:\n", + " print(f\"Error processing stream: {e}\")\n", + " return results" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "human_message = \"\"\"깃헙의 Pull Request를 확인하고 코드 리뷰를 작성해주세요. \n", + "PR의 코드를 리뷰한 후에, 아래 항목을 확인해주세요;\n", + "1. 코드가 개선되었는지\n", + "2. 예측하지 못한 side effect가 있는지\n", + "3. 보안상 문제가 될 수 있는 부분이 없는지\n", + "\n", + "위 내용을 확인해서 PR에 코멘트로 남겨주세요.\n", + "그리고 코멘트를 남긴 후에, 슬랙 채널에도 메세지를 전송해서 엔지니어에게 알려주세요\n", + "채널: <@C092QEJH6LB>\n", + "엔지니어: <@U092VN0DHBP>\n", + "\n", + "PR URL:https://github.com/jasonkang14/sat-reading-client/pull/3 \"\"\"\n", + "\n", + "stream_generator = agent.astream({\"messages\": [HumanMessage(human_message)]}, stream_mode=\"updates\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "'agent': '{'tool_calls': [{'id': 'call_c3kJJAv0icV21zJi1r2E5FbO', 'function': {'arguments': '{\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3}', 'name': 'get_pull_request'}, 'type': 'function'}], 'refusal': None}'\n", + "'tools': '{\"id\":2626689112,\"number\":3,\"state\":\"open\",\"locked\":false,\"title\":\"commit for the sake of review\",\"body\":\"회원가입 화면의 코드 구조 개선\",\"created_at\":\"2025-06-29T05:36:11Z\",\"updated_at\":\"2025-06-29T05:39:15Z\",\"user\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"},\"draft\":false,\"url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3\",\"html_url\":\"https://github.com/jasonkang14/sat-reading-client/pull/3\",\"issue_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/3\",\"statuses_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/statuses/82150ea51ad8ff040a963edb0b87403a40997eef\",\"diff_url\":\"https://github.com/jasonkang14/sat-reading-client/pull/3.diff\",\"patch_url\":\"https://github.com/jasonkang14/sat-reading-client/pull/3.patch\",\"commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3/commits\",\"comments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/3/comments\",\"review_comments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3/comments\",\"review_comment_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/comments{/number}\",\"author_association\":\"OWNER\",\"node_id\":\"PR_kwDONwC6Is6ckBhY\",\"merged\":false,\"mergeable\":true,\"mergeable_state\":\"clean\",\"rebaseable\":true,\"merge_commit_sha\":\"3e41c85c6f032eb302360831fb919e7cc8a3239b\",\"comments\":1,\"commits\":1,\"additions\":330,\"deletions\":106,\"changed_files\":1,\"maintainer_can_modify\":false,\"review_comments\":0,\"_links\":{\"self\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3\"},\"html\":{\"href\":\"https://github.com/jasonkang14/sat-reading-client/pull/3\"},\"issue\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/3\"},\"comments\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/3/comments\"},\"review_comments\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3/comments\"},\"review_comment\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/comments{/number}\"},\"commits\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3/commits\"},\"statuses\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/statuses/82150ea51ad8ff040a963edb0b87403a40997eef\"}},\"head\":{\"label\":\"jasonkang14:fix/register\",\"ref\":\"fix/register\",\"sha\":\"82150ea51ad8ff040a963edb0b87403a40997eef\",\"repo\":{\"id\":922794530,\"node_id\":\"R_kgDONwC6Ig\",\"owner\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"},\"name\":\"sat-reading-client\",\"full_name\":\"jasonkang14/sat-reading-client\",\"default_branch\":\"main\",\"created_at\":\"2025-01-27T04:39:08Z\",\"pushed_at\":\"2025-06-29T05:35:46Z\",\"updated_at\":\"2025-06-29T01:18:34Z\",\"html_url\":\"https://github.com/jasonkang14/sat-reading-client\",\"clone_url\":\"https://github.com/jasonkang14/sat-reading-client.git\",\"git_url\":\"git://github.com/jasonkang14/sat-reading-client.git\",\"ssh_url\":\"git@github.com:jasonkang14/sat-reading-client.git\",\"svn_url\":\"https://github.com/jasonkang14/sat-reading-client\",\"language\":\"C\",\"fork\":false,\"forks_count\":0,\"open_issues_count\":2,\"open_issues\":2,\"stargazers_count\":0,\"watchers_count\":0,\"watchers\":0,\"size\":1145,\"allow_forking\":true,\"web_commit_signoff_required\":false,\"archived\":false,\"disabled\":false,\"private\":true,\"has_issues\":true,\"has_wiki\":false,\"has_pages\":false,\"has_projects\":true,\"has_downloads\":true,\"has_discussions\":false,\"is_template\":false,\"url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client\",\"archive_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/{archive_format}{/ref}\",\"assignees_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/assignees{/user}\",\"blobs_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/blobs{/sha}\",\"branches_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/branches{/branch}\",\"collaborators_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/collaborators{/collaborator}\",\"comments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/comments{/number}\",\"commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/commits{/sha}\",\"compare_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/compare/{base}...{head}\",\"contents_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contents/{+path}\",\"contributors_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contributors\",\"deployments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/deployments\",\"downloads_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/downloads\",\"events_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/events\",\"forks_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/forks\",\"git_commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/commits{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/refs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/tags{/sha}\",\"hooks_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/hooks\",\"issue_comment_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/comments{/number}\",\"issue_events_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/events{/number}\",\"issues_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues{/number}\",\"keys_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/keys{/key_id}\",\"labels_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/labels{/name}\",\"languages_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/languages\",\"merges_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/merges\",\"milestones_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/notifications{?since,all,participating}\",\"pulls_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls{/number}\",\"releases_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/releases{/id}\",\"stargazers_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/stargazers\",\"statuses_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/statuses/{sha}\",\"subscribers_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/subscribers\",\"subscription_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/subscription\",\"tags_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/tags\",\"trees_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/trees{/sha}\",\"teams_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/teams\",\"visibility\":\"private\"},\"user\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"}},\"base\":{\"label\":\"jasonkang14:main\",\"ref\":\"main\",\"sha\":\"a9565cebe08826456a8690ebc0d2a2641f9404f8\",\"repo\":{\"id\":922794530,\"node_id\":\"R_kgDONwC6Ig\",\"owner\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"},\"name\":\"sat-reading-client\",\"full_name\":\"jasonkang14/sat-reading-client\",\"default_branch\":\"main\",\"created_at\":\"2025-01-27T04:39:08Z\",\"pushed_at\":\"2025-06-29T05:35:46Z\",\"updated_at\":\"2025-06-29T01:18:34Z\",\"html_url\":\"https://github.com/jasonkang14/sat-reading-client\",\"clone_url\":\"https://github.com/jasonkang14/sat-reading-client.git\",\"git_url\":\"git://github.com/jasonkang14/sat-reading-client.git\",\"ssh_url\":\"git@github.com:jasonkang14/sat-reading-client.git\",\"svn_url\":\"https://github.com/jasonkang14/sat-reading-client\",\"language\":\"C\",\"fork\":false,\"forks_count\":0,\"open_issues_count\":2,\"open_issues\":2,\"stargazers_count\":0,\"watchers_count\":0,\"watchers\":0,\"size\":1145,\"allow_forking\":true,\"web_commit_signoff_required\":false,\"archived\":false,\"disabled\":false,\"private\":true,\"has_issues\":true,\"has_wiki\":false,\"has_pages\":false,\"has_projects\":true,\"has_downloads\":true,\"has_discussions\":false,\"is_template\":false,\"url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client\",\"archive_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/{archive_format}{/ref}\",\"assignees_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/assignees{/user}\",\"blobs_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/blobs{/sha}\",\"branches_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/branches{/branch}\",\"collaborators_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/collaborators{/collaborator}\",\"comments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/comments{/number}\",\"commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/commits{/sha}\",\"compare_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/compare/{base}...{head}\",\"contents_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contents/{+path}\",\"contributors_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contributors\",\"deployments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/deployments\",\"downloads_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/downloads\",\"events_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/events\",\"forks_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/forks\",\"git_commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/commits{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/refs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/tags{/sha}\",\"hooks_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/hooks\",\"issue_comment_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/comments{/number}\",\"issue_events_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/events{/number}\",\"issues_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues{/number}\",\"keys_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/keys{/key_id}\",\"labels_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/labels{/name}\",\"languages_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/languages\",\"merges_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/merges\",\"milestones_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/notifications{?since,all,participating}\",\"pulls_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls{/number}\",\"releases_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/releases{/id}\",\"stargazers_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/stargazers\",\"statuses_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/statuses/{sha}\",\"subscribers_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/subscribers\",\"subscription_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/subscription\",\"tags_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/tags\",\"trees_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/trees{/sha}\",\"teams_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/teams\",\"visibility\":\"private\"},\"user\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"}}}'\n", + "'agent': '{'tool_calls': [{'id': 'call_76mwgFTAwiCJcatE60uh43bO', 'function': {'arguments': '{\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3}', 'name': 'get_pull_request_files'}, 'type': 'function'}], 'refusal': None}'\n", + "'tools': '[{\"sha\":\"f01fdb151eedfa459f8459264b2f5cab6932ea8e\",\"filename\":\"lib/app/screens/login/register_screen.dart\",\"additions\":330,\"deletions\":106,\"changes\":436,\"status\":\"modified\",\"patch\":\"@@ -13,145 +13,369 @@ class RegisterScreen extends ConsumerStatefulWidget {\\n }\\n \\n class _RegisterScreenState extends ConsumerState\\u003cRegisterScreen\\u003e {\\n+ static const double _formPadding = 24.0;\\n+ static const double _fieldSpacing = 20.0;\\n+ static const int _minPasswordLength = 8;\\n+ static const double _borderRadius = 12.0;\\n+\\n final _formKey = GlobalKey\\u003cFormState\\u003e();\\n final _emailController = TextEditingController();\\n final _passwordController = TextEditingController();\\n final _confirmPasswordController = TextEditingController();\\n+\\n+ final _emailFocusNode = FocusNode();\\n+ final _passwordFocusNode = FocusNode();\\n+ final _confirmPasswordFocusNode = FocusNode();\\n+\\n bool _isLoading = false;\\n- String _errorMessage = '';\\n+ bool _isPasswordVisible = false;\\n+ bool _isConfirmPasswordVisible = false;\\n+ String? _errorMessage;\\n+\\n+ // Email validation regex\\n+ static final _emailRegex = RegExp(\\n+ r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$',\\n+ );\\n \\n @override\\n void dispose() {\\n _emailController.dispose();\\n _passwordController.dispose();\\n _confirmPasswordController.dispose();\\n+ _emailFocusNode.dispose();\\n+ _passwordFocusNode.dispose();\\n+ _confirmPasswordFocusNode.dispose();\\n super.dispose();\\n }\\n \\n+ String? _validateEmail(String? value) {\\n+ if (value == null || value.trim().isEmpty) {\\n+ return 'Email is required';\\n+ }\\n+\\n+ final trimmedValue = value.trim();\\n+ if (!_emailRegex.hasMatch(trimmedValue)) {\\n+ return 'Please enter a valid email address';\\n+ }\\n+\\n+ return null;\\n+ }\\n+\\n+ String? _validatePassword(String? value) {\\n+ if (value == null || value.isEmpty) {\\n+ return 'Password is required';\\n+ }\\n+\\n+ if (value.length \\u003c _minPasswordLength) {\\n+ return 'Password must be at least $_minPasswordLength characters';\\n+ }\\n+\\n+ // Check for at least one uppercase letter, one lowercase letter, and one digit\\n+ if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d).+$').hasMatch(value)) {\\n+ return 'Password must contain uppercase, lowercase, and numbers';\\n+ }\\n+\\n+ return null;\\n+ }\\n+\\n+ String? _validateConfirmPassword(String? value) {\\n+ if (value == null || value.isEmpty) {\\n+ return 'Please confirm your password';\\n+ }\\n+\\n+ if (value != _passwordController.text) {\\n+ return 'Passwords do not match';\\n+ }\\n+\\n+ return null;\\n+ }\\n+\\n+ String _getErrorMessage(String error) {\\n+ // Convert technical errors to user-friendly messages\\n+ if (error.toLowerCase().contains('email-already-in-use')) {\\n+ return 'This email is already registered. Please use a different email or sign in.';\\n+ } else if (error.toLowerCase().contains('weak-password')) {\\n+ return 'The password is too weak. Please choose a stronger password.';\\n+ } else if (error.toLowerCase().contains('invalid-email')) {\\n+ return 'Please enter a valid email address.';\\n+ } else if (error.toLowerCase().contains('network')) {\\n+ return 'Network error. Please check your connection and try again.';\\n+ }\\n+\\n+ return 'Registration failed. Please try again.';\\n+ }\\n+\\n Future\\u003cvoid\\u003e _register() async {\\n- if (_formKey.currentState!.validate()) {\\n- setState(() {\\n- _isLoading = true;\\n- _errorMessage = '';\\n- });\\n-\\n- try {\\n- final authModel = await ref.read(authProvider.notifier).emailSignUp(\\n- _emailController.text.trim(),\\n- _passwordController.text,\\n- );\\n- final questionId = await ref.read(questionProvider.notifier).findUnsolvedQuestionId(authModel.id);\\n- if (questionId != null \\u0026\\u0026 mounted) {\\n- context.go('${AppRouteConstants.question}/$questionId');\\n- }\\n- } catch (e) {\\n+ // Dismiss keyboard\\n+ FocusScope.of(context).unfocus();\\n+\\n+ if (!_formKey.currentState!.validate()) {\\n+ return;\\n+ }\\n+\\n+ setState(() {\\n+ _isLoading = true;\\n+ _errorMessage = null;\\n+ });\\n+\\n+ try {\\n+ final authModel = await ref.read(authProvider.notifier).emailSignUp(\\n+ _emailController.text.trim(),\\n+ _passwordController.text,\\n+ );\\n+\\n+ final questionId = await ref.read(questionProvider.notifier).findUnsolvedQuestionId(authModel.id);\\n+\\n+ if (questionId != null \\u0026\\u0026 mounted) {\\n+ context.go('${AppRouteConstants.question}/$questionId');\\n+ }\\n+ } catch (e) {\\n+ if (mounted) {\\n+ setState(() {\\n+ _errorMessage = _getErrorMessage(e.toString());\\n+ });\\n+ }\\n+ } finally {\\n+ if (mounted) {\\n setState(() {\\n- _errorMessage = e.toString();\\n+ _isLoading = false;\\n });\\n- } finally {\\n- if (mounted) {\\n- setState(() {\\n- _isLoading = false;\\n- });\\n- }\\n }\\n }\\n }\\n \\n+ InputDecoration _buildInputDecoration({\\n+ required String labelText,\\n+ required IconData prefixIcon,\\n+ Widget? suffixIcon,\\n+ }) {\\n+ return InputDecoration(\\n+ labelText: labelText,\\n+ prefixIcon: Icon(prefixIcon, color: Theme.of(context).primaryColor),\\n+ suffixIcon: suffixIcon,\\n+ filled: true,\\n+ fillColor: Colors.grey[50],\\n+ border: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: BorderSide(color: Colors.grey[300]!),\\n+ ),\\n+ enabledBorder: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: BorderSide(color: Colors.grey[300]!),\\n+ ),\\n+ focusedBorder: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),\\n+ ),\\n+ errorBorder: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: const BorderSide(color: Colors.red),\\n+ ),\\n+ focusedErrorBorder: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: const BorderSide(color: Colors.red, width: 2),\\n+ ),\\n+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),\\n+ );\\n+ }\\n+\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n backgroundColor: Colors.white,\\n appBar: AppBar(\\n- title: const Text('Register'),\\n+ title: const Text(\\n+ 'Create Account',\\n+ style: TextStyle(fontWeight: FontWeight.w600),\\n+ ),\\n+ backgroundColor: Colors.transparent,\\n+ elevation: 0,\\n+ centerTitle: true,\\n ),\\n- body: SingleChildScrollView(\\n- padding: const EdgeInsets.all(16),\\n- child: Form(\\n- key: _formKey,\\n- child: Column(\\n- crossAxisAlignment: CrossAxisAlignment.stretch,\\n- children: [\\n- TextFormField(\\n- controller: _emailController,\\n- decoration: const InputDecoration(\\n- labelText: 'Email',\\n- filled: true,\\n+ body: SafeArea(\\n+ child: SingleChildScrollView(\\n+ padding: const EdgeInsets.all(_formPadding),\\n+ child: Form(\\n+ key: _formKey,\\n+ child: Column(\\n+ crossAxisAlignment: CrossAxisAlignment.stretch,\\n+ children: [\\n+ // Welcome text\\n+ Text(\\n+ 'Welcome!',\\n+ style: Theme.of(context).textTheme.headlineMedium?.copyWith(\\n+ fontWeight: FontWeight.bold,\\n+ color: Colors.black87,\\n+ ),\\n+ textAlign: TextAlign.center,\\n ),\\n- keyboardType: TextInputType.emailAddress,\\n- validator: (value) {\\n- if (value == null || value.isEmpty) {\\n- return 'Please enter your email';\\n- }\\n- if (!value.contains('@')) {\\n- return 'Please enter a valid email';\\n- }\\n- return null;\\n- },\\n- ),\\n- const SizedBox(height: 16),\\n- TextFormField(\\n- controller: _passwordController,\\n- decoration: const InputDecoration(\\n- labelText: 'Password',\\n- filled: true,\\n+ const SizedBox(height: 8),\\n+ Text(\\n+ 'Create your account to get started',\\n+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(\\n+ color: Colors.grey[600],\\n+ ),\\n+ textAlign: TextAlign.center,\\n ),\\n- obscureText: true,\\n- validator: (value) {\\n- if (value == null || value.isEmpty) {\\n- return 'Please enter your password';\\n- }\\n- if (value.length \\u003c 6) {\\n- return 'Password must be at least 6 characters';\\n- }\\n- return null;\\n- },\\n- ),\\n- const SizedBox(height: 16),\\n- TextFormField(\\n- controller: _confirmPasswordController,\\n- decoration: const InputDecoration(\\n- labelText: 'Confirm Password',\\n- filled: true,\\n+ const SizedBox(height: 32),\\n+\\n+ // Email field\\n+ TextFormField(\\n+ controller: _emailController,\\n+ focusNode: _emailFocusNode,\\n+ decoration: _buildInputDecoration(\\n+ labelText: 'Email Address',\\n+ prefixIcon: Icons.email_outlined,\\n+ ),\\n+ keyboardType: TextInputType.emailAddress,\\n+ textInputAction: TextInputAction.next,\\n+ autocorrect: false,\\n+ validator: _validateEmail,\\n+ onFieldSubmitted: (_) =\\u003e _passwordFocusNode.requestFocus(),\\n+ ),\\n+ const SizedBox(height: _fieldSpacing),\\n+\\n+ // Password field\\n+ TextFormField(\\n+ controller: _passwordController,\\n+ focusNode: _passwordFocusNode,\\n+ decoration: _buildInputDecoration(\\n+ labelText: 'Password',\\n+ prefixIcon: Icons.lock_outline,\\n+ suffixIcon: IconButton(\\n+ icon: Icon(\\n+ _isPasswordVisible ? Icons.visibility : Icons.visibility_off,\\n+ color: Colors.grey[600],\\n+ ),\\n+ onPressed: () {\\n+ setState(() {\\n+ _isPasswordVisible = !_isPasswordVisible;\\n+ });\\n+ },\\n+ ),\\n+ ),\\n+ obscureText: !_isPasswordVisible,\\n+ textInputAction: TextInputAction.next,\\n+ validator: _validatePassword,\\n+ onFieldSubmitted: (_) =\\u003e _confirmPasswordFocusNode.requestFocus(),\\n ),\\n- obscureText: true,\\n- validator: (value) {\\n- if (value == null || value.isEmpty) {\\n- return 'Please confirm your password';\\n- }\\n- if (value != _passwordController.text) {\\n- return 'Passwords do not match';\\n- }\\n- return null;\\n- },\\n- ),\\n- const SizedBox(height: 16),\\n- if (_errorMessage.isNotEmpty)\\n- Padding(\\n- padding: const EdgeInsets.only(bottom: 16),\\n- child: Text(\\n- _errorMessage,\\n- style: const TextStyle(color: Colors.red),\\n- textAlign: TextAlign.center,\\n+ const SizedBox(height: _fieldSpacing),\\n+\\n+ // Confirm password field\\n+ TextFormField(\\n+ controller: _confirmPasswordController,\\n+ focusNode: _confirmPasswordFocusNode,\\n+ decoration: _buildInputDecoration(\\n+ labelText: 'Confirm Password',\\n+ prefixIcon: Icons.lock_outline,\\n+ suffixIcon: IconButton(\\n+ icon: Icon(\\n+ _isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,\\n+ color: Colors.grey[600],\\n+ ),\\n+ onPressed: () {\\n+ setState(() {\\n+ _isConfirmPasswordVisible = !_isConfirmPasswordVisible;\\n+ });\\n+ },\\n+ ),\\n ),\\n+ obscureText: !_isConfirmPasswordVisible,\\n+ textInputAction: TextInputAction.done,\\n+ validator: _validateConfirmPassword,\\n+ onFieldSubmitted: (_) =\\u003e _register(),\\n ),\\n- ElevatedButton(\\n- onPressed: _isLoading ? null : _register,\\n- child: _isLoading\\n- ? const SizedBox(\\n- width: 20,\\n- height: 20,\\n- child: CircularProgressIndicator(\\n- strokeWidth: 2,\\n+ const SizedBox(height: 24),\\n+\\n+ // Error message\\n+ if (_errorMessage != null)\\n+ Container(\\n+ padding: const EdgeInsets.all(12),\\n+ margin: const EdgeInsets.only(bottom: 16),\\n+ decoration: BoxDecoration(\\n+ color: Colors.red[50],\\n+ borderRadius: BorderRadius.circular(8),\\n+ border: Border.all(color: Colors.red[200]!),\\n+ ),\\n+ child: Row(\\n+ children: [\\n+ Icon(Icons.error_outline, color: Colors.red[700], size: 20),\\n+ const SizedBox(width: 8),\\n+ Expanded(\\n+ child: Text(\\n+ _errorMessage!,\\n+ style: TextStyle(\\n+ color: Colors.red[700],\\n+ fontSize: 14,\\n+ ),\\n+ ),\\n ),\\n- )\\n- : const Text('Register'),\\n- ),\\n- TextButton(\\n- onPressed: () =\\u003e context.pop(),\\n- child: const Text('Already have an account? Sign in'),\\n- ),\\n- ],\\n+ ],\\n+ ),\\n+ ),\\n+\\n+ // Register button\\n+ SizedBox(\\n+ height: 50,\\n+ child: ElevatedButton(\\n+ onPressed: _isLoading ? null : _register,\\n+ style: ElevatedButton.styleFrom(\\n+ backgroundColor: Theme.of(context).primaryColor,\\n+ foregroundColor: Colors.white,\\n+ elevation: 2,\\n+ shape: RoundedRectangleBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ ),\\n+ disabledBackgroundColor: Colors.grey[300],\\n+ ),\\n+ child: _isLoading\\n+ ? const SizedBox(\\n+ width: 20,\\n+ height: 20,\\n+ child: CircularProgressIndicator(\\n+ strokeWidth: 2,\\n+ valueColor: AlwaysStoppedAnimation\\u003cColor\\u003e(Colors.white),\\n+ ),\\n+ )\\n+ : const Text(\\n+ 'Create Account',\\n+ style: TextStyle(\\n+ fontSize: 16,\\n+ fontWeight: FontWeight.w600,\\n+ ),\\n+ ),\\n+ ),\\n+ ),\\n+ const SizedBox(height: 24),\\n+\\n+ // Sign in link\\n+ TextButton(\\n+ onPressed: _isLoading ? null : () =\\u003e context.pop(),\\n+ style: TextButton.styleFrom(\\n+ foregroundColor: Theme.of(context).primaryColor,\\n+ padding: const EdgeInsets.symmetric(vertical: 12),\\n+ ),\\n+ child: RichText(\\n+ text: TextSpan(\\n+ style: TextStyle(\\n+ color: Colors.grey[600],\\n+ fontSize: 14,\\n+ ),\\n+ children: [\\n+ const TextSpan(text: 'Already have an account? '),\\n+ TextSpan(\\n+ text: 'Sign In',\\n+ style: TextStyle(\\n+ color: Theme.of(context).primaryColor,\\n+ fontWeight: FontWeight.w600,\\n+ ),\\n+ ),\\n+ ],\\n+ ),\\n+ ),\\n+ ),\\n+ ],\\n+ ),\\n ),\\n ),\\n ),\",\"blob_url\":\"https://github.com/jasonkang14/sat-reading-client/blob/82150ea51ad8ff040a963edb0b87403a40997eef/lib%2Fapp%2Fscreens%2Flogin%2Fregister_screen.dart\",\"raw_url\":\"https://github.com/jasonkang14/sat-reading-client/raw/82150ea51ad8ff040a963edb0b87403a40997eef/lib%2Fapp%2Fscreens%2Flogin%2Fregister_screen.dart\",\"contents_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contents/lib%2Fapp%2Fscreens%2Flogin%2Fregister_screen.dart?ref=82150ea51ad8ff040a963edb0b87403a40997eef\"}]'\n", + "'agent': '{'tool_calls': [{'id': 'call_FF5I8SaaHhsdkYLDuePwtpgU', 'function': {'arguments': '{\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3}', 'name': 'request_copilot_review'}, 'type': 'function'}], 'refusal': None}'\n", + "'tools': ''\n", + "'agent': '{'tool_calls': [{'id': 'call_yz0QTvDvtGDAk8ZEjhIEMhaA', 'function': {'arguments': '{\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3,\"commitID\":\"82150ea51ad8ff040a963edb0b87403a40997eef\"}', 'name': 'create_pending_pull_request_review'}, 'type': 'function'}], 'refusal': None}'\n", + "'tools': 'pending pull request created'\n", + "'agent': '{'tool_calls': [{'id': 'call_LK2zZoz3wY0XDialmEpnFaue', 'function': {'arguments': '{\"body\":\"코드 리뷰 결과(회원가입 화면 구조 개선)\\\\n\\\\n1. 코드 개선 여부:\\\\n- 기존의 단순한 폼에서 가독성이 좋은 입력 검증 함수들과 시맨틱한 위젯 구조로 리팩터링되었습니다.\\\\n- 사용자 경험(UX) 측면, 에러 메시지와 유효성 검사 상세화 및 비밀번호 가시성 토글 추가 등 실질적인 이득이 확인됩니다. \\\\n- 코드의 재사용성, 유지보수성 모두 향상되었습니다.\\\\n\\\\n2. 부작용(Side effect) 여부:\\\\n- 기존 회원가입 flow를 대체하는 구조이므로, 신규/기존 사용자에 대한 edge case(ex: focusNode 미해제 등) 대비가 필요해 보이나 dispose에서 모두 정상 처리되고 있음이 확인되었습니다.\\\\n- 멀티 submit이나 중복 요청에 대한 방지는 `_isLoading` 플래그로 처리되어 안정적입니다.\\\\n\\\\n3. 보안 이슈:\\\\n- 이메일 및 패스워드 검증 로직이 추가되었으며, 입력제한(정규식)과 최소길이, 비밀번호 복잡도 체크가 정책에 맞게 잘 반영되어 있습니다.\\\\n- 비밀번호 필드를 기본적으로 가리고, 토글을 통해 사용자가 선택적으로 볼 수 있도록 UI가 구현되어 있습니다.\\\\n- 잠재적으로 입력 검증 bypass 등의 이슈는 없어 보이나, 추가 검증이 서버에서도 이루어지는지 확인 권장합니다.\\\\n\\\\n전체적으로 구조 개선 의도가 잘 반영된 PR이며, 사용자 및 보안 측면에서 실질적 개선이 확인됩니다. 추가적으로 서버 검증 로직에 대한 확인만 함께 진행하면 좋겠습니다.\",\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3,\"path\":\"lib/app/screens/login/register_screen.dart\",\"subjectType\":\"FILE\"}', 'name': 'add_pull_request_review_comment_to_pending_review'}, 'type': 'function'}], 'refusal': None}'\n", + "'tools': 'pull request review comment successfully added to pending review'\n", + "'agent': '{'tool_calls': [{'id': 'call_k2SAx48hvDnHfYva8tcmfRmz', 'function': {'arguments': '{\"body\":\"전체 코드 리뷰 & 코멘트 참고 부탁드립니다. \\\\n- 구조적 개선 및 UX, 보안 측면에서 모두 의미 있는 개선이 적용되었습니다.\\\\n- 리뷰 코멘트에 상세 내용을 남겼으니 참고해 주세요!\",\"event\":\"COMMENT\",\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3}', 'name': 'submit_pending_pull_request_review'}, 'type': 'function'}], 'refusal': None}'\n", + "'tools': 'pending pull request review successfully submitted'\n", + "'agent': '{'tool_calls': [{'id': 'call_ULA58itqZc9uhWhVhzdUCoTJ', 'function': {'arguments': '{\"channel_id\":\"C092QEJH6LB\",\"text\":\"<@U092VN0DHBP>님, PR(#3)에 코드리뷰 코멘트가 등록되었습니다.\\\\n\\\\n- PR 링크: https://github.com/jasonkang14/sat-reading-client/pull/3\\\\n- 리뷰 주요내용: 구조 개선, UX 및 보안 측면 강화가 확인되었습니다.\\\\n- 상세한 내용은 PR 코멘트 참고 부탁드립니다.\"}', 'name': 'slack_post_message'}, 'type': 'function'}], 'refusal': None}'\n", + "'tools': '{\"ok\":true,\"channel\":\"C092QEJH6LB\",\"ts\":\"1751178754.334099\",\"message\":{\"user\":\"U0946DSQYHE\",\"type\":\"message\",\"ts\":\"1751178754.334099\",\"bot_id\":\"B0946DSQKL0\",\"app_id\":\"A0946DRU6PJ\",\"text\":\"<@U092VN0DHBP>님, PR(#3)에 코드리뷰 코멘트가 등록되었습니다.\\n\\n- PR 링크: \\n- 리뷰 주요내용: 구조 개선, UX 및 보안 측면 강화가 확인되었습니다.\\n- 상세한 내용은 PR 코멘트 참고 부탁드립니다.\",\"team\":\"T092VN0DA85\",\"bot_profile\":{\"id\":\"B0946DSQKL0\",\"app_id\":\"A0946DRU6PJ\",\"user_id\":\"U0946DSQYHE\",\"name\":\"pr-bot\",\"icons\":{\"image_36\":\"https://a.slack-edge.com/80588/img/plugins/app/bot_36.png\",\"image_48\":\"https://a.slack-edge.com/80588/img/plugins/app/bot_48.png\",\"image_72\":\"https://a.slack-edge.com/80588/img/plugins/app/service_72.png\"},\"deleted\":false,\"updated\":1751163460,\"team_id\":\"T092VN0DA85\"},\"blocks\":[{\"type\":\"rich_text\",\"block_id\":\"1vF\",\"elements\":[{\"type\":\"rich_text_section\",\"elements\":[{\"type\":\"user\",\"user_id\":\"U092VN0DHBP\"},{\"type\":\"text\",\"text\":\"님, PR(#3)에 코드리뷰 코멘트가 등록되었습니다.\\n\\n- PR 링크: \"},{\"type\":\"link\",\"url\":\"https://github.com/jasonkang14/sat-reading-client/pull/3\"},{\"type\":\"text\",\"text\":\"\\n- 리뷰 주요내용: 구조 개선, UX 및 보안 측면 강화가 확인되었습니다.\\n- 상세한 내용은 PR 코멘트 참고 부탁드립니다.\"}]}]}]},\"warning\":\"missing_charset\",\"response_metadata\":{\"warnings\":[\"missing_charset\"]}}'\n", + "'agent': '코드 리뷰 및 알림 절차가 모두 완료되었습니다. 진행된 사항은 아래와 같습니다:\n", + "\n", + "1. PR(https://github.com/jasonkang14/sat-reading-client/pull/3) 코드 리뷰를 진행하였고, 다음 항목을 중심으로 코멘트를 남겼습니다.\n", + " - 코드가 구조적으로 확실히 개선되었음 (가독성, 유지보수성, UX 모두 향상)\n", + " - 예측하지 못한 side effect는 없음(메모리/이벤트 누수 등 기본 처리 정상)\n", + " - 비밀번호/회원가입 검증 및 UI 상 보안 강화 확인됨 (변경된 입력 정책이 코드에 잘 반영)\n", + " - 간단 검증은 모두 클라이언트에서 이뤄지지만 backend(서버)에서도 검증이 필요함을 제안\n", + "\n", + "2. 리뷰 코멘트 등록이 완료되었으며, PR 내 해당 파일(`register_screen.dart`)에 남겨두었습니다.\n", + "\n", + "3. 슬랙 채널 <@C092QEJH6LB>에 <@U092VN0DHBP>님을 멘션하여, PR 리뷰 및 코멘트 등록 사실을 안내했습니다.\n", + "\n", + "궁금하신 사항이나 추가 요청이 있으면 말씀해주세요!'\n", + "\n", + "Final result: {'agent': {'messages': [AIMessage(content='코드 리뷰 및 알림 절차가 모두 완료되었습니다. 진행된 사항은 아래와 같습니다:\\n\\n1. PR(https://github.com/jasonkang14/sat-reading-client/pull/3) 코드 리뷰를 진행하였고, 다음 항목을 중심으로 코멘트를 남겼습니다.\\n - 코드가 구조적으로 확실히 개선되었음 (가독성, 유지보수성, UX 모두 향상)\\n - 예측하지 못한 side effect는 없음(메모리/이벤트 누수 등 기본 처리 정상)\\n - 비밀번호/회원가입 검증 및 UI 상 보안 강화 확인됨 (변경된 입력 정책이 코드에 잘 반영)\\n - 간단 검증은 모두 클라이언트에서 이뤄지지만 backend(서버)에서도 검증이 필요함을 제안\\n\\n2. 리뷰 코멘트 등록이 완료되었으며, PR 내 해당 파일(`register_screen.dart`)에 남겨두었습니다.\\n\\n3. 슬랙 채널 <@C092QEJH6LB>에 <@U092VN0DHBP>님을 멘션하여, PR 리뷰 및 코멘트 등록 사실을 안내했습니다.\\n\\n궁금하신 사항이나 추가 요청이 있으면 말씀해주세요!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 267, 'prompt_tokens': 12354, 'total_tokens': 12621, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 11776}}, 'model_name': 'gpt-4.1-2025-04-14', 'system_fingerprint': 'fp_799e4ca3f1', 'id': 'chatcmpl-BnfsZXM0fX077VSjxvlIqw8gqPvMM', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--929605e2-fddd-4c1e-80a0-d631e9977d26-0', usage_metadata={'input_tokens': 12354, 'output_tokens': 267, 'total_tokens': 12621, 'input_token_details': {'audio': 0, 'cache_read': 11776}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n" + ] + } + ], + "source": [ + "\n", + "all_chunks = await process_stream(stream_generator)\n", + "\n", + "\n", + "if all_chunks:\n", + " final_result = all_chunks[-1]\n", + " print(\"\\nFinal result:\", final_result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/README-auto-code-review.md b/README-auto-code-review.md new file mode 100644 index 0000000..ccbb36a --- /dev/null +++ b/README-auto-code-review.md @@ -0,0 +1,160 @@ +# 자동 코드리뷰 설정 가이드 + +## 개요 + +이 프로젝트는 MCP(Model Context Protocol)를 활용하여 GitHub PR에 대한 자동 코드리뷰를 수행하는 시스템입니다. + +**현재 상태**: Slack 알림 기능은 비활성화되어 있으며, GitHub PR 코멘트만 작성됩니다. + +## 설정 방법 + +### 1. 환경변수 설정 + +#### 방법 1: .env.example 템플릿 사용 (권장) + +```bash +# 1. 템플릿 파일을 .env로 복사 +cp .env.example .env + +# 2. .env 파일을 편집하여 실제 토큰들을 입력 +# 예: GITHUB_PAT=ghp_xxxxxxxxxxxxxxxxxxxx +``` + +#### 방법 2: 직접 .env 파일 생성 + +`.env` 파일을 생성하고 다음 환경변수들을 설정하세요: + +```bash +# GitHub 설정 +GITHUB_PAT=your_github_personal_access_token + +# OpenAI 설정 +OPENAI_API_KEY=your_openai_api_key + +# Slack 설정 (현재 비활성화됨) +# SLACK_BOT_TOKEN=your_slack_bot_token +# SLACK_TEAM_ID=your_slack_team_id +# SLACK_CHANNEL_IDS=channel_id1,channel_id2 +``` + +### 2. 필요한 토큰 및 권한 + +#### GitHub Personal Access Token + +- **생성 위치**: https://github.com/settings/tokens +- **필요 권한**: `repo`, `pull_requests`, `issues` +- **설명**: PR 정보를 읽고 리뷰 코멘트를 작성하기 위해 필요 + +#### OpenAI API Key + +- **생성 위치**: https://platform.openai.com/api-keys +- **설명**: GPT-4를 사용한 코드 분석을 위해 필요 + +#### Slack Bot Token (현재 비활성화됨) + +- **생성 위치**: https://api.slack.com/apps +- **필요 권한**: `chat:write`, `channels:read` +- **설명**: 리뷰 완료 알림을 Slack 채널에 전송하기 위해 필요 (현재 사용하지 않음) + +### 3. GitHub Actions Secrets 설정 + +GitHub 저장소의 Settings > Secrets and variables > Actions에서 다음 secrets를 추가하세요: + +- `GITHUB_PAT` +- `OPENAI_API_KEY` +- `SLACK_BOT_TOKEN` (선택사항, 현재 비활성화됨) +- `SLACK_TEAM_ID` (선택사항, 현재 비활성화됨) +- `SLACK_CHANNEL_IDS` (선택사항, 현재 비활성화됨) + +### 4. Docker 설치 확인 + +MCP 서버들이 Docker 컨테이너로 실행되므로 Docker가 설치되어 있어야 합니다. + +### 5. 테스트 실행 + +```bash +# 의존성 설치 +pip install -r requirements.txt + +# 로컬 테스트 (테스트 데이터 사용) +python scripts/test_code_review.py --test-data + +# 로컬 테스트 (실제 PR 사용) +python scripts/test_code_review.py + +# 특정 PR 번호로 테스트 +python scripts/test_code_review.py --pr 123 +``` + +## 동작 방식 + +1. **PR 생성/업데이트**: GitHub Actions가 PR 이벤트를 감지 +2. **MCP 서버 실행**: GitHub MCP 서버를 Docker로 실행 +3. **코드리뷰 수행**: OpenAI GPT-4를 사용하여 코드 분석 +4. **리뷰 코멘트 작성**: PR에 자동으로 리뷰 코멘트 작성 +5. **Slack 알림**: 현재 비활성화됨 + +## 커스터마이징 + +### 리뷰 기준 수정 + +`scripts/auto_code_review.py`의 `human_message` 부분을 수정하여 리뷰 기준을 변경할 수 있습니다. + +### Slack 알림 활성화 + +Slack 알림을 다시 활성화하려면: + +1. `scripts/auto_code_review.py`에서 Slack 관련 주석을 해제 +2. `.github/workflows/auto-code-review.yml`에서 Slack 환경변수 주석 해제 +3. `.env` 파일에 Slack 토큰들 설정 + +### 특정 파일만 리뷰 + +GitHub Actions 워크플로우에 조건을 추가하여 특정 파일이 변경된 경우에만 리뷰를 실행할 수 있습니다. + +## 문제 해결 + +### Docker 권한 오류 + +```bash +sudo usermod -aG docker $USER +``` + +### MCP 서버 연결 실패 + +- Docker가 실행 중인지 확인 +- 네트워크 연결 상태 확인 +- 환경변수가 올바르게 설정되었는지 확인 + +### OpenAI API 오류 + +- API 키가 유효한지 확인 +- API 사용량 한도 확인 +- 모델명이 올바른지 확인 + +### 환경변수 오류 + +```bash +# .env 파일 존재 확인 +ls -la .env + +# 환경변수 로드 확인 +python -c "from dotenv import load_dotenv; load_dotenv(); import os; print('GITHUB_PAT:', bool(os.getenv('GITHUB_PAT')))" +``` + +## 모니터링 + +GitHub Actions 탭에서 워크플로우 실행 상태를 확인할 수 있습니다. + +## 비용 고려사항 + +- OpenAI API 사용량에 따른 비용 발생 +- GitHub Actions 실행 시간에 따른 비용 발생 (공개 저장소는 무료) +- Docker 컨테이너 실행 비용 + +## 보안 고려사항 + +- GitHub PAT는 최소 권한으로 설정 +- Slack Bot Token은 필요한 채널만 접근 가능하도록 설정 +- 환경변수는 절대 코드에 하드코딩하지 않음 +- `.env` 파일은 절대 Git에 커밋하지 않음 diff --git a/aa.html b/aa.html new file mode 100644 index 0000000..b948387 --- /dev/null +++ b/aa.html @@ -0,0 +1,447 @@ + + + + + + Hensley - Frontend Dev + + + +
+ +
+
📁
+ My Computer +
+
+
💻
+ Projects +
+
+
📧
+ Contact +
+
+
🗑️
+ Recycle Bin +
+ + +
+
+
+ 📋 + About Hensley - Profile.exe +
+
+
_
+
□
+
✕
+
+
+
+
+
+
+

Hensley

+
Frontend Developer / 💻
+
+
+ +
+ 🛠️ Tech Stack: +
+ TypeScript + React + Node.js +
+
+ +
+ 📦 Building npm packages | ☕️ Coffee lover | 🏃‍♂️ daily jogger +
+ + +
+ +
+ System Info:
+ 💻 OS: Windows 95 Retro Edition
+ 🎮 Status: Ready to code!
+ 📈 GitHub: github.com/sooster910
+ 🌐 Location: Seoul, South Korea +
+
+
+ + +
+
+ 🏁 + Start +
+
+
📋 About Hensley
+
+
+ 🔊 + 2:30 PM +
+
+
+ + + + diff --git a/docs/github-app-setup.md b/docs/github-app-setup.md new file mode 100644 index 0000000..358a0ad --- /dev/null +++ b/docs/github-app-setup.md @@ -0,0 +1,36 @@ +# GitHub App을 활용한 자동 코드리뷰 설정 + +## 1. GitHub App 생성 + +1. GitHub에서 Settings > Developer settings > GitHub Apps로 이동 +2. "New GitHub App" 클릭 +3. 다음 권한 설정: + - Repository permissions: + - Pull requests: Read & Write + - Contents: Read + - Issues: Read & Write + - Subscribe to events: + - Pull request + +## 2. Webhook 설정 + +GitHub App이 PR 이벤트를 받을 수 있도록 webhook 엔드포인트 설정이 필요합니다. + +## 3. 환경변수 설정 + +GitHub Actions Secrets에 다음을 추가: + +- `GITHUB_APP_ID`: GitHub App ID +- `GITHUB_APP_PRIVATE_KEY`: GitHub App private key +- `GITHUB_PAT`: Personal Access Token +- `SLACK_BOT_TOKEN`: Slack Bot Token +- `SLACK_TEAM_ID`: Slack Team ID +- `SLACK_CHANNEL_IDS`: Slack Channel IDs +- `OPENAI_API_KEY`: OpenAI API Key + +## 4. 장점 + +- 더 세밀한 이벤트 제어 가능 +- 특정 조건에서만 코드리뷰 실행 +- 커스텀 로직 추가 가능 +- 실시간 응답 가능 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..85db7d3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +langchain-mcp-adapters +langgraph +openai +python-dotenv diff --git a/scripts/README-test.md b/scripts/README-test.md new file mode 100644 index 0000000..1fb261c --- /dev/null +++ b/scripts/README-test.md @@ -0,0 +1,121 @@ +# 코드리뷰 테스트 스크립트 사용법 + +## 개요 + +`test_code_review.py` 스크립트는 로컬에서 자동 코드리뷰 기능을 테스트할 수 있는 도구입니다. + +## 사용법 + +### 1. 최신 PR로 테스트 + +```bash +python scripts/test_code_review.py +``` + +- 현재 저장소의 가장 최근에 생성된 PR을 자동으로 찾아서 테스트합니다. + +### 2. 특정 PR 번호로 테스트 + +```bash +python scripts/test_code_review.py --pr 123 +``` + +- PR 번호 123을 지정하여 테스트합니다. + +### 3. 도움말 보기 + +```bash +python scripts/test_code_review.py --help +``` + +## 사전 요구사항 + +### 1. GitHub CLI 설치 및 로그인 + +```bash +# macOS +brew install gh + +# 로그인 +gh auth login +``` + +### 2. 환경변수 설정 + +`.env` 파일에 다음 환경변수들이 설정되어 있어야 합니다: + +```bash +GITHUB_PAT=your_github_personal_access_token +SLACK_BOT_TOKEN=your_slack_bot_token +SLACK_TEAM_ID=your_slack_team_id +SLACK_CHANNEL_IDS=channel_id1,channel_id2 +OPENAI_API_KEY=your_openai_api_key +``` + +### 3. Python 패키지 설치 + +```bash +pip install -r requirements.txt +``` + +## 동작 과정 + +1. **저장소 정보 확인**: `git remote`에서 현재 저장소의 owner/repo 정보를 추출 +2. **PR 정보 가져오기**: GitHub CLI를 사용하여 PR 정보를 가져옴 +3. **MCP 서버 실행**: GitHub 및 Slack MCP 서버를 Docker로 실행 +4. **코드리뷰 수행**: OpenAI GPT-4를 사용하여 코드 분석 +5. **결과 확인**: PR에 리뷰 코멘트가 작성되었는지 확인 + +## 예시 출력 + +``` +🔍 저장소 정보 확인 중... +📁 저장소: hyunsujoo/hensley-ui +🔍 최신 PR 확인 중... +📋 테스트할 PR: #15 - 새로운 기능 추가 +🔗 URL: https://github.com/hyunsujoo/hensley-ui/pull/15 +🚀 테스트 코드리뷰 시작... +'agent': '코드리뷰를 시작하겠습니다...' +'tools': 'PR 정보를 가져왔습니다...' +✅ 테스트 성공! +📝 PR #15에 리뷰 코멘트가 작성되었습니다. +``` + +## 문제 해결 + +### GitHub CLI 오류 + +```bash +# GitHub CLI 상태 확인 +gh auth status + +# 다시 로그인 +gh auth login +``` + +### Docker 오류 + +```bash +# Docker 실행 상태 확인 +docker ps + +# Docker 권한 문제 해결 +sudo usermod -aG docker $USER +``` + +### 환경변수 오류 + +```bash +# .env 파일 존재 확인 +ls -la .env + +# 환경변수 로드 확인 +python -c "from dotenv import load_dotenv; load_dotenv(); import os; print('GITHUB_PAT:', bool(os.getenv('GITHUB_PAT')))" +``` + +## 팁 + +- 테스트 전에 PR이 실제로 존재하는지 확인하세요 +- 환경변수가 올바르게 설정되어 있는지 확인하세요 +- Docker가 실행 중인지 확인하세요 +- 네트워크 연결이 안정적인지 확인하세요 diff --git a/scripts/__pycache__/auto_code_review.cpython-312.pyc b/scripts/__pycache__/auto_code_review.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..859b4bb6adf46d41c44da723bb1596213458f973 GIT binary patch literal 7688 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 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/auto_code_review.cpython-37.pyc b/scripts/__pycache__/auto_code_review.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3f0f09874662ec078c3c39e4fb929b1ce542853 GIT binary patch literal 4152 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 K%3kFOh5tY4Hk^F` literal 0 HcmV?d00001 diff --git a/scripts/auto_code_review.py b/scripts/auto_code_review.py new file mode 100644 index 0000000..54dce61 --- /dev/null +++ b/scripts/auto_code_review.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +""" +자동 코드리뷰 스크립트 +GitHub Actions에서 실행되어 PR에 대한 자동 코드리뷰를 수행합니다. +""" + +import os +import sys +import argparse +import asyncio +import json +from datetime import datetime +from dotenv import load_dotenv +from langchain_mcp_adapters.client import MultiServerMCPClient +from langgraph.prebuilt import create_react_agent +from langchain_core.messages import HumanMessage + +# 환경변수 로드 +load_dotenv() + +async def setup_mcp_client(): + """MCP 클라이언트 설정""" + github_pat = os.getenv("GITHUB_PAT") + # slack_bot_token = os.getenv("SLACK_BOT_TOKEN") + # slack_team_id = os.getenv("SLACK_TEAM_ID") + # slack_channel_ids = os.getenv("SLACK_CHANNEL_IDS") + + if not github_pat: + raise ValueError("GITHUB_PAT 환경변수가 설정되지 않았습니다.") + + mcp_config = { + "github": { + "command": "docker", + "args": [ + "run", "-i", "--rm", + "-e", "GITHUB_TOOLSETS", + "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghcr.io/github/github-mcp-server" + ], + "env": { + "GITHUB_TOOLSETS": "context,pull_requests", + "GITHUB_PERSONAL_ACCESS_TOKEN": github_pat + }, + "transport": "stdio" + } + } + + # Slack 관련 설정 주석 처리 + # if all([slack_bot_token, slack_team_id, slack_channel_ids]): + # mcp_config["slack"] = { + # "command": "docker", + # "args": [ + # "run", "-i", "--rm", + # "-e", "SLACK_BOT_TOKEN", + # "-e", "SLACK_TEAM_ID", + # "-e", "SLACK_CHANNEL_IDS", + # "mcp/slack" + # ], + # "env": { + # "SLACK_BOT_TOKEN": slack_bot_token, + # "SLACK_TEAM_ID": slack_team_id, + # "SLACK_CHANNEL_IDS": slack_channel_ids + # }, + # "transport": "stdio" + # } + + mcp_client = MultiServerMCPClient(mcp_config) + return mcp_client + +async def process_stream(stream_generator): + """스트림 처리 함수""" + results = [] + try: + async for chunk in stream_generator: + key = list(chunk.keys())[0] + if key == 'agent': + content = chunk['agent']['messages'][0].content if chunk['agent']['messages'][0].content != '' else chunk['agent']['messages'][0].additional_kwargs + print(f"'agent': '{content}'") + elif key == 'tools': + for tool_msg in chunk['tools']['messages']: + print(f"'tools': '{tool_msg.content}'") + results.append(chunk) + return results + except Exception as e: + print(f"Error processing stream: {e}") + return results + +def save_review_result(owner, repo, pr_number, review_content, output_dir="review_results"): + """코드리뷰 결과를 파일로 저장""" + try: + # 결과 디렉토리 생성 + os.makedirs(output_dir, exist_ok=True) + + # 파일명 생성 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"{owner}_{repo}_PR{pr_number}_{timestamp}.json" + filepath = os.path.join(output_dir, filename) + + # 결과 데이터 구성 + review_data = { + "metadata": { + "owner": owner, + "repo": repo, + "pr_number": pr_number, + "reviewed_at": datetime.now().isoformat(), + "timestamp": timestamp + }, + "review_content": review_content + } + + # JSON 파일로 저장 + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(review_data, f, ensure_ascii=False, indent=2) + + print(f"📄 리뷰 결과가 저장되었습니다: {filepath}") + return filepath + + except Exception as e: + print(f"❌ 리뷰 결과 저장 중 오류: {e}") + return None + +async def run_code_review(owner, repo, pr_number, pr_url, save_result=True): + """코드리뷰 실행""" + try: + # MCP 클라이언트 설정 + mcp_client = await setup_mcp_client() + + # 도구 목록 가져오기 + tool_list = await mcp_client.get_tools() + + # 에이전트 생성 + agent = create_react_agent( + model="openai:gpt-4.1", + tools=tool_list, + prompt="Use the tools provided to you to answer the user's question" + ) + + # 코드리뷰 요청 메시지 생성 (Slack 관련 부분 제거) + human_message = f"""깃헙의 Pull Request를 확인하고 코드 리뷰를 작성해주세요. +PR의 코드를 리뷰한 후에, 아래 항목을 확인해주세요; +1. 코드가 개선되었는지 +2. 예측하지 못한 side effect가 있는지 +3. 보안상 문제가 될 수 있는 부분이 없는지 + +위 내용을 확인해서 PR에 코멘트로 남겨주세요. + +PR URL: {pr_url}""" + + # 스트림 실행 + stream_generator = agent.astream( + {"messages": [HumanMessage(human_message)]}, + stream_mode="updates" + ) + + # 결과 처리 + all_chunks = await process_stream(stream_generator) + + if all_chunks: + final_result = all_chunks[-1] + print("\nFinal result:", final_result) + + # 리뷰 내용 추출 + review_content = None + if 'agent' in final_result and 'messages' in final_result['agent']: + for message in final_result['agent']['messages']: + if hasattr(message, 'content') and message.content: + review_content = message.content + break + + # 결과 저장 + if save_result and review_content: + save_review_result(owner, repo, pr_number, review_content) + + return True + else: + print("No results from code review") + return False + + except Exception as e: + print(f"코드리뷰 실행 중 오류 발생: {e}") + return False + +def main(): + parser = argparse.ArgumentParser(description='자동 코드리뷰 스크립트') + parser.add_argument('--owner', required=True, help='GitHub 저장소 소유자') + parser.add_argument('--repo', required=True, help='GitHub 저장소 이름') + parser.add_argument('--pr-number', required=True, type=int, help='PR 번호') + parser.add_argument('--pr-url', required=True, help='PR URL') + parser.add_argument('--no-save', action='store_true', help='결과를 파일로 저장하지 않음') + + args = parser.parse_args() + + print(f"코드리뷰 시작: {args.owner}/{args.repo} PR #{args.pr_number}") + + # 비동기 실행 + success = asyncio.run(run_code_review( + args.owner, + args.repo, + args.pr_number, + args.pr_url, + save_result=not args.no_save + )) + + if success: + print("코드리뷰가 성공적으로 완료되었습니다.") + sys.exit(0) + else: + print("코드리뷰 실행 중 오류가 발생했습니다.") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/scripts/test_code_review.py b/scripts/test_code_review.py new file mode 100644 index 0000000..27ca71f --- /dev/null +++ b/scripts/test_code_review.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +로컬 테스트용 코드리뷰 스크립트 +현재 저장소의 최신 PR을 자동으로 가져와서 테스트합니다. +""" + +import asyncio +import os +import subprocess +import json +import argparse +from auto_code_review import run_code_review + +def get_pr_by_number(pr_number): + """특정 번호의 PR 정보를 가져옵니다.""" + try: + result = subprocess.run( + ["gh", "pr", "view", str(pr_number), "--json", "number,url,title,headRefName"], + capture_output=True, + text=True, + check=True + ) + + pr = json.loads(result.stdout) + print(f"📋 테스트할 PR: #{pr['number']} - {pr['title']}") + print(f"🔗 URL: {pr['url']}") + + return { + 'number': pr['number'], + 'url': pr['url'], + 'title': pr['title'], + 'branch': pr['headRefName'] + } + + except subprocess.CalledProcessError as e: + print(f"❌ PR #{pr_number}을 찾을 수 없습니다: {e}") + return None + except json.JSONDecodeError as e: + print(f"❌ JSON 파싱 오류: {e}") + return None + +def get_latest_pr(): + """현재 저장소의 최신 PR 정보를 가져옵니다.""" + try: + # GitHub CLI를 사용하여 최신 PR 정보 가져오기 + result = subprocess.run( + ["gh", "pr", "list", "--limit", "1", "--json", "number,url,title,headRefName"], + capture_output=True, + text=True, + check=True + ) + + prs = json.loads(result.stdout) + if not prs: + print("❌ 열린 PR이 없습니다.") + return None + + pr = prs[0] + print(f"📋 테스트할 PR: #{pr['number']} - {pr['title']}") + print(f"🔗 URL: {pr['url']}") + + return { + 'number': pr['number'], + 'url': pr['url'], + 'title': pr['title'], + 'branch': pr['headRefName'] + } + + except subprocess.CalledProcessError as e: + print(f"❌ GitHub CLI 오류: {e}") + print("GitHub CLI가 설치되어 있고 로그인되어 있는지 확인하세요.") + return None + except json.JSONDecodeError as e: + print(f"❌ JSON 파싱 오류: {e}") + return None + +def get_repo_info(): + """현재 저장소 정보를 가져옵니다.""" + try: + # git remote에서 저장소 정보 추출 + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + capture_output=True, + text=True, + check=True + ) + + remote_url = result.stdout.strip() + + # SSH 또는 HTTPS URL에서 owner/repo 추출 + if remote_url.startswith('git@github.com:'): + # SSH 형식: git@github.com:owner/repo.git + repo_path = remote_url.replace('git@github.com:', '').replace('.git', '') + elif remote_url.startswith('https://github.com/'): + # HTTPS 형식: https://github.com/owner/repo.git + repo_path = remote_url.replace('https://github.com/', '').replace('.git', '') + else: + print(f"❌ 지원하지 않는 remote URL 형식: {remote_url}") + return None + + owner, repo = repo_path.split('/', 1) + return {'owner': owner, 'repo': repo} + + except subprocess.CalledProcessError as e: + print(f"❌ git remote 오류: {e}") + return None + +def get_test_pr_info(): + """테스트용 PR 정보를 반환합니다.""" + return { + 'owner': 'sooster910', + 'repo': 'hensley-ui', + 'number': 29, + 'url': 'https://github.com/sooster910/hensley-ui/pull/29' + } + +async def test_code_review(pr_number=None, use_test_data=False, save_result=True): + """테스트용 코드리뷰 실행""" + + if use_test_data: + print("🧪 테스트 데이터를 사용합니다.") + repo_info = {'owner': 'sooster910', 'repo': 'hensley-ui'} + pr_info = get_test_pr_info() + else: + print("🔍 저장소 정보 확인 중...") + repo_info = get_repo_info() + if not repo_info: + print("❌ 저장소 정보를 가져올 수 없습니다.") + return + + print(f"📁 저장소: {repo_info['owner']}/{repo_info['repo']}") + + # PR 정보 가져오기 + if pr_number: + print(f"🔍 PR #{pr_number} 확인 중...") + pr_info = get_pr_by_number(pr_number) + else: + print("🔍 최신 PR 확인 중...") + pr_info = get_latest_pr() + + if not pr_info: + print("❌ 테스트할 PR을 찾을 수 없습니다.") + return + + print(f"📋 테스트할 PR: #{pr_info['number']}") + if 'title' in pr_info: + print(f"📝 제목: {pr_info['title']}") + print(f"🔗 URL: {pr_info['url']}") + + print("🚀 테스트 코드리뷰 시작...") + + success = await run_code_review( + repo_info['owner'], + repo_info['repo'], + pr_info['number'], + pr_info['url'], + save_result=save_result + ) + + if success: + print("✅ 테스트 성공!") + print(f"📝 PR #{pr_info['number']}에 리뷰 코멘트가 작성되었습니다.") + if save_result: + print("📄 리뷰 결과가 review_results 폴더에 저장되었습니다.") + else: + print("❌ 테스트 실패!") + +def main(): + parser = argparse.ArgumentParser(description='로컬 테스트용 코드리뷰 스크립트') + parser.add_argument('--pr', type=int, help='테스트할 PR 번호 (지정하지 않으면 최신 PR 사용)') + parser.add_argument('--test-data', action='store_true', help='테스트 데이터 사용 (PR이 없을 때)') + parser.add_argument('--no-save', action='store_true', help='결과를 파일로 저장하지 않음') + + args = parser.parse_args() + + asyncio.run(test_code_review(args.pr, args.test_data, save_result=not args.no_save)) + +if __name__ == "__main__": + main() From 3e6a9af17d2dbfafed15777007baa48d75dacbbc Mon Sep 17 00:00:00 2001 From: Hensley Joo Date: Wed, 13 Aug 2025 18:08:29 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20GitHub=20Actions=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=9D=B4=EB=A6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(AUTO=5FCODE=5FREVIEW=5FTOKEN)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-code-review.yml b/.github/workflows/auto-code-review.yml index c3775de..ada70da 100644 --- a/.github/workflows/auto-code-review.yml +++ b/.github/workflows/auto-code-review.yml @@ -25,7 +25,7 @@ jobs: - name: Run code review env: - GITHUB_PAT: ${{ secrets.GITHUB_PAT }} + GITHUB_PAT: ${{ secrets.AUTO_CODE_REVIEW_TOKEN }} # SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} # SLACK_TEAM_ID: ${{ secrets.SLACK_TEAM_ID }} # SLACK_CHANNEL_IDS: ${{ secrets.SLACK_CHANNEL_IDS }} From aaed418191f395addcacbf731571f5c0646951e5 Mon Sep 17 00:00:00 2001 From: Hensley Joo Date: Wed, 13 Aug 2025 18:12:31 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20PR=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(=EA=B8=B0=EB=B3=B8,=20=EA=B8=B0=EB=8A=A5,?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pull_request_templates/bugfix.md | 32 ++++++++++++++++++++ .github/pull_request_templates/default.md | 37 +++++++++++++++++++++++ .github/pull_request_templates/feature.md | 34 +++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 .github/pull_request_templates/bugfix.md create mode 100644 .github/pull_request_templates/default.md create mode 100644 .github/pull_request_templates/feature.md diff --git a/.github/pull_request_templates/bugfix.md b/.github/pull_request_templates/bugfix.md new file mode 100644 index 0000000..dbc663e --- /dev/null +++ b/.github/pull_request_templates/bugfix.md @@ -0,0 +1,32 @@ +## 🐛 버그 수정 + +### 🐛 버그 설명 + + +### 🔍 원인 분석 + + +### 🔧 수정 내용 + + +#### 📁 변경된 파일들 +- [ ] 파일명: 수정 내용 설명 + +### 🧪 테스트 +- [ ] 버그가 수정되었는지 확인 +- [ ] 기존 기능에 영향 없음 확인 +- [ ] 유사한 버그가 다른 곳에 없는지 확인 + +### 🔗 관련 이슈 +Fixes #(이슈번호) + +### ✅ 체크리스트 +- [ ] 코드 리뷰를 요청했습니다 +- [ ] 테스트 코드를 작성했습니다 (필요시) +- [ ] 문서를 업데이트했습니다 (필요시) +- [ ] 브랜치 이름이 의미있게 지어졌습니다 +- [ ] 커밋 메시지가 명확합니다 + +--- + +**참고**: 이 PR은 AI 자동 코드리뷰 시스템에 의해 자동으로 리뷰됩니다! 🤖 diff --git a/.github/pull_request_templates/default.md b/.github/pull_request_templates/default.md new file mode 100644 index 0000000..2e690e9 --- /dev/null +++ b/.github/pull_request_templates/default.md @@ -0,0 +1,37 @@ +## 📋 Pull Request + +### 🎯 변경사항 요약 + + +### 🔍 변경사항 상세 + + +#### 📁 변경된 파일들 +- [ ] 파일명: 변경 내용 설명 + +### 🧪 테스트 + +- [ ] 로컬 테스트 완료 +- [ ] 기존 기능에 영향 없음 확인 +- [ ] 새로운 기능 테스트 완료 + +### 📸 스크린샷 (UI 변경시) + + +### 🔗 관련 이슈 + +Closes #(이슈번호) + +### ✅ 체크리스트 +- [ ] 코드 리뷰를 요청했습니다 +- [ ] 테스트 코드를 작성했습니다 (필요시) +- [ ] 문서를 업데이트했습니다 (필요시) +- [ ] 브랜치 이름이 의미있게 지어졌습니다 +- [ ] 커밋 메시지가 명확합니다 + +### 📝 추가 정보 + + +--- + +**참고**: 이 PR은 AI 자동 코드리뷰 시스템에 의해 자동으로 리뷰됩니다! 🤖 diff --git a/.github/pull_request_templates/feature.md b/.github/pull_request_templates/feature.md new file mode 100644 index 0000000..8102e86 --- /dev/null +++ b/.github/pull_request_templates/feature.md @@ -0,0 +1,34 @@ +## 🚀 새로운 기능 추가 + +### 🎯 기능 설명 + + +### 🔧 구현 내용 + + +#### 📁 변경된 파일들 +- [ ] 파일명: 변경 내용 설명 + +### 🧪 테스트 +- [ ] 새로운 기능 테스트 완료 +- [ ] 기존 기능에 영향 없음 확인 +- [ ] 에러 케이스 테스트 완료 + +### 📚 문서 업데이트 +- [ ] README 업데이트 (필요시) +- [ ] API 문서 업데이트 (필요시) +- [ ] 주석 추가 (필요시) + +### 🔗 관련 이슈 +Closes #(이슈번호) + +### ✅ 체크리스트 +- [ ] 코드 리뷰를 요청했습니다 +- [ ] 테스트 코드를 작성했습니다 +- [ ] 문서를 업데이트했습니다 +- [ ] 브랜치 이름이 의미있게 지어졌습니다 +- [ ] 커밋 메시지가 명확합니다 + +--- + +**참고**: 이 PR은 AI 자동 코드리뷰 시스템에 의해 자동으로 리뷰됩니다! 🤖 From f729e48d93ec8dd4bbb629c0906a9c27e1b0e5e3 Mon Sep 17 00:00:00 2001 From: Hensley Joo Date: Wed, 13 Aug 2025 18:41:00 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20langchain=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20GitHub=20Actions?= =?UTF-8?q?=20requirements.txt=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-code-review.yml | 2 +- requirements.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/auto-code-review.yml b/.github/workflows/auto-code-review.yml index ada70da..0bc5359 100644 --- a/.github/workflows/auto-code-review.yml +++ b/.github/workflows/auto-code-review.yml @@ -21,7 +21,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install langchain-mcp-adapters langgraph openai python-dotenv + pip install -r requirements.txt - name: Run code review env: diff --git a/requirements.txt b/requirements.txt index 85db7d3..2420933 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ langchain-mcp-adapters langgraph openai python-dotenv +langchain +langchain-openai From e4e42b44abd26b0532c9af81b5d791c5d874858d Mon Sep 17 00:00:00 2001 From: Hensley Joo Date: Wed, 13 Aug 2025 18:59:08 +0900 Subject: [PATCH 5/8] =?UTF-8?q?chore:=20Jupyter=20notebook=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=82=AD=EC=A0=9C=20(=ED=94=84=EB=A1=9C=EB=8D=95?= =?UTF-8?q?=EC=85=98=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EB=8C=80=EC=B2=B4=EB=90=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...354\204\261\355\225\230\352\270\260.ipynb" | 245 ------------------ 1 file changed, 245 deletions(-) delete mode 100644 "24. MCP\353\245\274 \355\231\234\354\232\251\355\225\234 \354\275\224\353\223\234\353\246\254\353\267\260 \354\227\220\354\235\264\354\240\204\355\212\270 \354\203\235\354\204\261\355\225\230\352\270\260.ipynb" diff --git "a/24. MCP\353\245\274 \355\231\234\354\232\251\355\225\234 \354\275\224\353\223\234\353\246\254\353\267\260 \354\227\220\354\235\264\354\240\204\355\212\270 \354\203\235\354\204\261\355\225\230\352\270\260.ipynb" "b/24. MCP\353\245\274 \355\231\234\354\232\251\355\225\234 \354\275\224\353\223\234\353\246\254\353\267\260 \354\227\220\354\235\264\354\240\204\355\212\270 \354\203\235\354\204\261\355\225\230\352\270\260.ipynb" deleted file mode 100644 index 76c0c97..0000000 --- "a/24. MCP\353\245\274 \355\231\234\354\232\251\355\225\234 \354\275\224\353\223\234\353\246\254\353\267\260 \354\227\220\354\235\264\354\240\204\355\212\270 \354\203\235\354\204\261\355\225\230\352\270\260.ipynb" +++ /dev/null @@ -1,245 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from dotenv import load_dotenv\n", - "\n", - "load_dotenv()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "import os\n", - "from langchain_mcp_adapters.client import MultiServerMCPClient\n", - "\n", - "github_pat = os.getenv(\"GITHUB_PAT\")\n", - "slack_bot_token = os.getenv(\"SLACK_BOT_TOKEN\")\n", - "slack_team_id = os.getenv(\"SLACK_TEAM_ID\")\n", - "slack_channel_ids = os.getenv(\"SLACK_CHANNEL_IDS\")\n", - "\n", - "mcp_client = MultiServerMCPClient({\n", - " \"github\": {\n", - " \"command\": \"docker\",\n", - " \"args\": [\n", - " \"run\",\n", - " \"-i\",\n", - " \"--rm\",\n", - " \"-e\",\n", - " \"GITHUB_TOOLSETS\",\n", - " \"-e\",\n", - " \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n", - " \"ghcr.io/github/github-mcp-server\"\n", - " ],\n", - " \"env\": {\n", - " \"GITHUB_TOOLSETS\": \"context,pull_requests\",\n", - " \"GITHUB_PERSONAL_ACCESS_TOKEN\": github_pat\n", - " },\n", - " \"transport\": \"stdio\"\n", - " },\n", - " \"slack\": {\n", - " \"command\": \"docker\",\n", - " \"args\": [\n", - " \"run\",\n", - " \"-i\",\n", - " \"--rm\",\n", - " \"-e\",\n", - " \"SLACK_BOT_TOKEN\",\n", - " \"-e\",\n", - " \"SLACK_TEAM_ID\",\n", - " \"-e\",\n", - " \"SLACK_CHANNEL_IDS\",\n", - " \"mcp/slack\"\n", - " ],\n", - " \"env\": {\n", - " \"SLACK_BOT_TOKEN\": slack_bot_token,\n", - " \"SLACK_TEAM_ID\": slack_team_id,\n", - " \"SLACK_CHANNEL_IDS\": slack_channel_ids # ,로 구분된 문자열\n", - " },\n", - " \"transport\": \"stdio\"\n", - " }\n", - "})" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "tool_list = await mcp_client.get_tools()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.prebuilt import create_react_agent\n", - "\n", - "agent = create_react_agent(\n", - " model=\"openai:gpt-4.1\",\n", - " tools=tool_list,\n", - " prompt=\"Use the tools provided to you to answer the user's question\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "async def process_stream(stream_generator):\n", - " results = []\n", - " try:\n", - " async for chunk in stream_generator:\n", - "\n", - " key = list(chunk.keys())[0]\n", - " if key == 'agent':\n", - " # Agent 메시지의 내용을 가져옴. 메세지가 비어있는 경우 어떤 도구를 어떻게 호출할지 정보를 가져옴\n", - " content = chunk['agent']['messages'][0].content if chunk['agent']['messages'][0].content != '' else chunk['agent']['messages'][0].additional_kwargs\n", - " print(f\"'agent': '{content}'\")\n", - " \n", - " elif key == 'tools':\n", - " # 도구 메시지의 내용을 가져옴\n", - " for tool_msg in chunk['tools']['messages']:\n", - " print(f\"'tools': '{tool_msg.content}'\")\n", - " \n", - " results.append(chunk)\n", - " return results\n", - " except Exception as e:\n", - " print(f\"Error processing stream: {e}\")\n", - " return results" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "human_message = \"\"\"깃헙의 Pull Request를 확인하고 코드 리뷰를 작성해주세요. \n", - "PR의 코드를 리뷰한 후에, 아래 항목을 확인해주세요;\n", - "1. 코드가 개선되었는지\n", - "2. 예측하지 못한 side effect가 있는지\n", - "3. 보안상 문제가 될 수 있는 부분이 없는지\n", - "\n", - "위 내용을 확인해서 PR에 코멘트로 남겨주세요.\n", - "그리고 코멘트를 남긴 후에, 슬랙 채널에도 메세지를 전송해서 엔지니어에게 알려주세요\n", - "채널: <@C092QEJH6LB>\n", - "엔지니어: <@U092VN0DHBP>\n", - "\n", - "PR URL:https://github.com/jasonkang14/sat-reading-client/pull/3 \"\"\"\n", - "\n", - "stream_generator = agent.astream({\"messages\": [HumanMessage(human_message)]}, stream_mode=\"updates\")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "'agent': '{'tool_calls': [{'id': 'call_c3kJJAv0icV21zJi1r2E5FbO', 'function': {'arguments': '{\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3}', 'name': 'get_pull_request'}, 'type': 'function'}], 'refusal': None}'\n", - "'tools': '{\"id\":2626689112,\"number\":3,\"state\":\"open\",\"locked\":false,\"title\":\"commit for the sake of review\",\"body\":\"회원가입 화면의 코드 구조 개선\",\"created_at\":\"2025-06-29T05:36:11Z\",\"updated_at\":\"2025-06-29T05:39:15Z\",\"user\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"},\"draft\":false,\"url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3\",\"html_url\":\"https://github.com/jasonkang14/sat-reading-client/pull/3\",\"issue_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/3\",\"statuses_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/statuses/82150ea51ad8ff040a963edb0b87403a40997eef\",\"diff_url\":\"https://github.com/jasonkang14/sat-reading-client/pull/3.diff\",\"patch_url\":\"https://github.com/jasonkang14/sat-reading-client/pull/3.patch\",\"commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3/commits\",\"comments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/3/comments\",\"review_comments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3/comments\",\"review_comment_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/comments{/number}\",\"author_association\":\"OWNER\",\"node_id\":\"PR_kwDONwC6Is6ckBhY\",\"merged\":false,\"mergeable\":true,\"mergeable_state\":\"clean\",\"rebaseable\":true,\"merge_commit_sha\":\"3e41c85c6f032eb302360831fb919e7cc8a3239b\",\"comments\":1,\"commits\":1,\"additions\":330,\"deletions\":106,\"changed_files\":1,\"maintainer_can_modify\":false,\"review_comments\":0,\"_links\":{\"self\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3\"},\"html\":{\"href\":\"https://github.com/jasonkang14/sat-reading-client/pull/3\"},\"issue\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/3\"},\"comments\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/3/comments\"},\"review_comments\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3/comments\"},\"review_comment\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/comments{/number}\"},\"commits\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls/3/commits\"},\"statuses\":{\"href\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/statuses/82150ea51ad8ff040a963edb0b87403a40997eef\"}},\"head\":{\"label\":\"jasonkang14:fix/register\",\"ref\":\"fix/register\",\"sha\":\"82150ea51ad8ff040a963edb0b87403a40997eef\",\"repo\":{\"id\":922794530,\"node_id\":\"R_kgDONwC6Ig\",\"owner\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"},\"name\":\"sat-reading-client\",\"full_name\":\"jasonkang14/sat-reading-client\",\"default_branch\":\"main\",\"created_at\":\"2025-01-27T04:39:08Z\",\"pushed_at\":\"2025-06-29T05:35:46Z\",\"updated_at\":\"2025-06-29T01:18:34Z\",\"html_url\":\"https://github.com/jasonkang14/sat-reading-client\",\"clone_url\":\"https://github.com/jasonkang14/sat-reading-client.git\",\"git_url\":\"git://github.com/jasonkang14/sat-reading-client.git\",\"ssh_url\":\"git@github.com:jasonkang14/sat-reading-client.git\",\"svn_url\":\"https://github.com/jasonkang14/sat-reading-client\",\"language\":\"C\",\"fork\":false,\"forks_count\":0,\"open_issues_count\":2,\"open_issues\":2,\"stargazers_count\":0,\"watchers_count\":0,\"watchers\":0,\"size\":1145,\"allow_forking\":true,\"web_commit_signoff_required\":false,\"archived\":false,\"disabled\":false,\"private\":true,\"has_issues\":true,\"has_wiki\":false,\"has_pages\":false,\"has_projects\":true,\"has_downloads\":true,\"has_discussions\":false,\"is_template\":false,\"url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client\",\"archive_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/{archive_format}{/ref}\",\"assignees_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/assignees{/user}\",\"blobs_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/blobs{/sha}\",\"branches_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/branches{/branch}\",\"collaborators_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/collaborators{/collaborator}\",\"comments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/comments{/number}\",\"commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/commits{/sha}\",\"compare_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/compare/{base}...{head}\",\"contents_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contents/{+path}\",\"contributors_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contributors\",\"deployments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/deployments\",\"downloads_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/downloads\",\"events_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/events\",\"forks_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/forks\",\"git_commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/commits{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/refs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/tags{/sha}\",\"hooks_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/hooks\",\"issue_comment_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/comments{/number}\",\"issue_events_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/events{/number}\",\"issues_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues{/number}\",\"keys_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/keys{/key_id}\",\"labels_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/labels{/name}\",\"languages_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/languages\",\"merges_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/merges\",\"milestones_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/notifications{?since,all,participating}\",\"pulls_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls{/number}\",\"releases_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/releases{/id}\",\"stargazers_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/stargazers\",\"statuses_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/statuses/{sha}\",\"subscribers_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/subscribers\",\"subscription_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/subscription\",\"tags_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/tags\",\"trees_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/trees{/sha}\",\"teams_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/teams\",\"visibility\":\"private\"},\"user\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"}},\"base\":{\"label\":\"jasonkang14:main\",\"ref\":\"main\",\"sha\":\"a9565cebe08826456a8690ebc0d2a2641f9404f8\",\"repo\":{\"id\":922794530,\"node_id\":\"R_kgDONwC6Ig\",\"owner\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"},\"name\":\"sat-reading-client\",\"full_name\":\"jasonkang14/sat-reading-client\",\"default_branch\":\"main\",\"created_at\":\"2025-01-27T04:39:08Z\",\"pushed_at\":\"2025-06-29T05:35:46Z\",\"updated_at\":\"2025-06-29T01:18:34Z\",\"html_url\":\"https://github.com/jasonkang14/sat-reading-client\",\"clone_url\":\"https://github.com/jasonkang14/sat-reading-client.git\",\"git_url\":\"git://github.com/jasonkang14/sat-reading-client.git\",\"ssh_url\":\"git@github.com:jasonkang14/sat-reading-client.git\",\"svn_url\":\"https://github.com/jasonkang14/sat-reading-client\",\"language\":\"C\",\"fork\":false,\"forks_count\":0,\"open_issues_count\":2,\"open_issues\":2,\"stargazers_count\":0,\"watchers_count\":0,\"watchers\":0,\"size\":1145,\"allow_forking\":true,\"web_commit_signoff_required\":false,\"archived\":false,\"disabled\":false,\"private\":true,\"has_issues\":true,\"has_wiki\":false,\"has_pages\":false,\"has_projects\":true,\"has_downloads\":true,\"has_discussions\":false,\"is_template\":false,\"url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client\",\"archive_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/{archive_format}{/ref}\",\"assignees_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/assignees{/user}\",\"blobs_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/blobs{/sha}\",\"branches_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/branches{/branch}\",\"collaborators_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/collaborators{/collaborator}\",\"comments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/comments{/number}\",\"commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/commits{/sha}\",\"compare_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/compare/{base}...{head}\",\"contents_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contents/{+path}\",\"contributors_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contributors\",\"deployments_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/deployments\",\"downloads_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/downloads\",\"events_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/events\",\"forks_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/forks\",\"git_commits_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/commits{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/refs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/tags{/sha}\",\"hooks_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/hooks\",\"issue_comment_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/comments{/number}\",\"issue_events_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues/events{/number}\",\"issues_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/issues{/number}\",\"keys_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/keys{/key_id}\",\"labels_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/labels{/name}\",\"languages_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/languages\",\"merges_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/merges\",\"milestones_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/notifications{?since,all,participating}\",\"pulls_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/pulls{/number}\",\"releases_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/releases{/id}\",\"stargazers_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/stargazers\",\"statuses_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/statuses/{sha}\",\"subscribers_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/subscribers\",\"subscription_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/subscription\",\"tags_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/tags\",\"trees_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/git/trees{/sha}\",\"teams_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/teams\",\"visibility\":\"private\"},\"user\":{\"login\":\"jasonkang14\",\"id\":45306565,\"node_id\":\"MDQ6VXNlcjQ1MzA2NTY1\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/45306565?v=4\",\"html_url\":\"https://github.com/jasonkang14\",\"gravatar_id\":\"\",\"type\":\"User\",\"site_admin\":false,\"url\":\"https://api.github.com/users/jasonkang14\",\"events_url\":\"https://api.github.com/users/jasonkang14/events{/privacy}\",\"following_url\":\"https://api.github.com/users/jasonkang14/following{/other_user}\",\"followers_url\":\"https://api.github.com/users/jasonkang14/followers\",\"gists_url\":\"https://api.github.com/users/jasonkang14/gists{/gist_id}\",\"organizations_url\":\"https://api.github.com/users/jasonkang14/orgs\",\"received_events_url\":\"https://api.github.com/users/jasonkang14/received_events\",\"repos_url\":\"https://api.github.com/users/jasonkang14/repos\",\"starred_url\":\"https://api.github.com/users/jasonkang14/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/jasonkang14/subscriptions\"}}}'\n", - "'agent': '{'tool_calls': [{'id': 'call_76mwgFTAwiCJcatE60uh43bO', 'function': {'arguments': '{\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3}', 'name': 'get_pull_request_files'}, 'type': 'function'}], 'refusal': None}'\n", - "'tools': '[{\"sha\":\"f01fdb151eedfa459f8459264b2f5cab6932ea8e\",\"filename\":\"lib/app/screens/login/register_screen.dart\",\"additions\":330,\"deletions\":106,\"changes\":436,\"status\":\"modified\",\"patch\":\"@@ -13,145 +13,369 @@ class RegisterScreen extends ConsumerStatefulWidget {\\n }\\n \\n class _RegisterScreenState extends ConsumerState\\u003cRegisterScreen\\u003e {\\n+ static const double _formPadding = 24.0;\\n+ static const double _fieldSpacing = 20.0;\\n+ static const int _minPasswordLength = 8;\\n+ static const double _borderRadius = 12.0;\\n+\\n final _formKey = GlobalKey\\u003cFormState\\u003e();\\n final _emailController = TextEditingController();\\n final _passwordController = TextEditingController();\\n final _confirmPasswordController = TextEditingController();\\n+\\n+ final _emailFocusNode = FocusNode();\\n+ final _passwordFocusNode = FocusNode();\\n+ final _confirmPasswordFocusNode = FocusNode();\\n+\\n bool _isLoading = false;\\n- String _errorMessage = '';\\n+ bool _isPasswordVisible = false;\\n+ bool _isConfirmPasswordVisible = false;\\n+ String? _errorMessage;\\n+\\n+ // Email validation regex\\n+ static final _emailRegex = RegExp(\\n+ r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$',\\n+ );\\n \\n @override\\n void dispose() {\\n _emailController.dispose();\\n _passwordController.dispose();\\n _confirmPasswordController.dispose();\\n+ _emailFocusNode.dispose();\\n+ _passwordFocusNode.dispose();\\n+ _confirmPasswordFocusNode.dispose();\\n super.dispose();\\n }\\n \\n+ String? _validateEmail(String? value) {\\n+ if (value == null || value.trim().isEmpty) {\\n+ return 'Email is required';\\n+ }\\n+\\n+ final trimmedValue = value.trim();\\n+ if (!_emailRegex.hasMatch(trimmedValue)) {\\n+ return 'Please enter a valid email address';\\n+ }\\n+\\n+ return null;\\n+ }\\n+\\n+ String? _validatePassword(String? value) {\\n+ if (value == null || value.isEmpty) {\\n+ return 'Password is required';\\n+ }\\n+\\n+ if (value.length \\u003c _minPasswordLength) {\\n+ return 'Password must be at least $_minPasswordLength characters';\\n+ }\\n+\\n+ // Check for at least one uppercase letter, one lowercase letter, and one digit\\n+ if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d).+$').hasMatch(value)) {\\n+ return 'Password must contain uppercase, lowercase, and numbers';\\n+ }\\n+\\n+ return null;\\n+ }\\n+\\n+ String? _validateConfirmPassword(String? value) {\\n+ if (value == null || value.isEmpty) {\\n+ return 'Please confirm your password';\\n+ }\\n+\\n+ if (value != _passwordController.text) {\\n+ return 'Passwords do not match';\\n+ }\\n+\\n+ return null;\\n+ }\\n+\\n+ String _getErrorMessage(String error) {\\n+ // Convert technical errors to user-friendly messages\\n+ if (error.toLowerCase().contains('email-already-in-use')) {\\n+ return 'This email is already registered. Please use a different email or sign in.';\\n+ } else if (error.toLowerCase().contains('weak-password')) {\\n+ return 'The password is too weak. Please choose a stronger password.';\\n+ } else if (error.toLowerCase().contains('invalid-email')) {\\n+ return 'Please enter a valid email address.';\\n+ } else if (error.toLowerCase().contains('network')) {\\n+ return 'Network error. Please check your connection and try again.';\\n+ }\\n+\\n+ return 'Registration failed. Please try again.';\\n+ }\\n+\\n Future\\u003cvoid\\u003e _register() async {\\n- if (_formKey.currentState!.validate()) {\\n- setState(() {\\n- _isLoading = true;\\n- _errorMessage = '';\\n- });\\n-\\n- try {\\n- final authModel = await ref.read(authProvider.notifier).emailSignUp(\\n- _emailController.text.trim(),\\n- _passwordController.text,\\n- );\\n- final questionId = await ref.read(questionProvider.notifier).findUnsolvedQuestionId(authModel.id);\\n- if (questionId != null \\u0026\\u0026 mounted) {\\n- context.go('${AppRouteConstants.question}/$questionId');\\n- }\\n- } catch (e) {\\n+ // Dismiss keyboard\\n+ FocusScope.of(context).unfocus();\\n+\\n+ if (!_formKey.currentState!.validate()) {\\n+ return;\\n+ }\\n+\\n+ setState(() {\\n+ _isLoading = true;\\n+ _errorMessage = null;\\n+ });\\n+\\n+ try {\\n+ final authModel = await ref.read(authProvider.notifier).emailSignUp(\\n+ _emailController.text.trim(),\\n+ _passwordController.text,\\n+ );\\n+\\n+ final questionId = await ref.read(questionProvider.notifier).findUnsolvedQuestionId(authModel.id);\\n+\\n+ if (questionId != null \\u0026\\u0026 mounted) {\\n+ context.go('${AppRouteConstants.question}/$questionId');\\n+ }\\n+ } catch (e) {\\n+ if (mounted) {\\n+ setState(() {\\n+ _errorMessage = _getErrorMessage(e.toString());\\n+ });\\n+ }\\n+ } finally {\\n+ if (mounted) {\\n setState(() {\\n- _errorMessage = e.toString();\\n+ _isLoading = false;\\n });\\n- } finally {\\n- if (mounted) {\\n- setState(() {\\n- _isLoading = false;\\n- });\\n- }\\n }\\n }\\n }\\n \\n+ InputDecoration _buildInputDecoration({\\n+ required String labelText,\\n+ required IconData prefixIcon,\\n+ Widget? suffixIcon,\\n+ }) {\\n+ return InputDecoration(\\n+ labelText: labelText,\\n+ prefixIcon: Icon(prefixIcon, color: Theme.of(context).primaryColor),\\n+ suffixIcon: suffixIcon,\\n+ filled: true,\\n+ fillColor: Colors.grey[50],\\n+ border: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: BorderSide(color: Colors.grey[300]!),\\n+ ),\\n+ enabledBorder: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: BorderSide(color: Colors.grey[300]!),\\n+ ),\\n+ focusedBorder: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),\\n+ ),\\n+ errorBorder: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: const BorderSide(color: Colors.red),\\n+ ),\\n+ focusedErrorBorder: OutlineInputBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ borderSide: const BorderSide(color: Colors.red, width: 2),\\n+ ),\\n+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),\\n+ );\\n+ }\\n+\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n backgroundColor: Colors.white,\\n appBar: AppBar(\\n- title: const Text('Register'),\\n+ title: const Text(\\n+ 'Create Account',\\n+ style: TextStyle(fontWeight: FontWeight.w600),\\n+ ),\\n+ backgroundColor: Colors.transparent,\\n+ elevation: 0,\\n+ centerTitle: true,\\n ),\\n- body: SingleChildScrollView(\\n- padding: const EdgeInsets.all(16),\\n- child: Form(\\n- key: _formKey,\\n- child: Column(\\n- crossAxisAlignment: CrossAxisAlignment.stretch,\\n- children: [\\n- TextFormField(\\n- controller: _emailController,\\n- decoration: const InputDecoration(\\n- labelText: 'Email',\\n- filled: true,\\n+ body: SafeArea(\\n+ child: SingleChildScrollView(\\n+ padding: const EdgeInsets.all(_formPadding),\\n+ child: Form(\\n+ key: _formKey,\\n+ child: Column(\\n+ crossAxisAlignment: CrossAxisAlignment.stretch,\\n+ children: [\\n+ // Welcome text\\n+ Text(\\n+ 'Welcome!',\\n+ style: Theme.of(context).textTheme.headlineMedium?.copyWith(\\n+ fontWeight: FontWeight.bold,\\n+ color: Colors.black87,\\n+ ),\\n+ textAlign: TextAlign.center,\\n ),\\n- keyboardType: TextInputType.emailAddress,\\n- validator: (value) {\\n- if (value == null || value.isEmpty) {\\n- return 'Please enter your email';\\n- }\\n- if (!value.contains('@')) {\\n- return 'Please enter a valid email';\\n- }\\n- return null;\\n- },\\n- ),\\n- const SizedBox(height: 16),\\n- TextFormField(\\n- controller: _passwordController,\\n- decoration: const InputDecoration(\\n- labelText: 'Password',\\n- filled: true,\\n+ const SizedBox(height: 8),\\n+ Text(\\n+ 'Create your account to get started',\\n+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(\\n+ color: Colors.grey[600],\\n+ ),\\n+ textAlign: TextAlign.center,\\n ),\\n- obscureText: true,\\n- validator: (value) {\\n- if (value == null || value.isEmpty) {\\n- return 'Please enter your password';\\n- }\\n- if (value.length \\u003c 6) {\\n- return 'Password must be at least 6 characters';\\n- }\\n- return null;\\n- },\\n- ),\\n- const SizedBox(height: 16),\\n- TextFormField(\\n- controller: _confirmPasswordController,\\n- decoration: const InputDecoration(\\n- labelText: 'Confirm Password',\\n- filled: true,\\n+ const SizedBox(height: 32),\\n+\\n+ // Email field\\n+ TextFormField(\\n+ controller: _emailController,\\n+ focusNode: _emailFocusNode,\\n+ decoration: _buildInputDecoration(\\n+ labelText: 'Email Address',\\n+ prefixIcon: Icons.email_outlined,\\n+ ),\\n+ keyboardType: TextInputType.emailAddress,\\n+ textInputAction: TextInputAction.next,\\n+ autocorrect: false,\\n+ validator: _validateEmail,\\n+ onFieldSubmitted: (_) =\\u003e _passwordFocusNode.requestFocus(),\\n+ ),\\n+ const SizedBox(height: _fieldSpacing),\\n+\\n+ // Password field\\n+ TextFormField(\\n+ controller: _passwordController,\\n+ focusNode: _passwordFocusNode,\\n+ decoration: _buildInputDecoration(\\n+ labelText: 'Password',\\n+ prefixIcon: Icons.lock_outline,\\n+ suffixIcon: IconButton(\\n+ icon: Icon(\\n+ _isPasswordVisible ? Icons.visibility : Icons.visibility_off,\\n+ color: Colors.grey[600],\\n+ ),\\n+ onPressed: () {\\n+ setState(() {\\n+ _isPasswordVisible = !_isPasswordVisible;\\n+ });\\n+ },\\n+ ),\\n+ ),\\n+ obscureText: !_isPasswordVisible,\\n+ textInputAction: TextInputAction.next,\\n+ validator: _validatePassword,\\n+ onFieldSubmitted: (_) =\\u003e _confirmPasswordFocusNode.requestFocus(),\\n ),\\n- obscureText: true,\\n- validator: (value) {\\n- if (value == null || value.isEmpty) {\\n- return 'Please confirm your password';\\n- }\\n- if (value != _passwordController.text) {\\n- return 'Passwords do not match';\\n- }\\n- return null;\\n- },\\n- ),\\n- const SizedBox(height: 16),\\n- if (_errorMessage.isNotEmpty)\\n- Padding(\\n- padding: const EdgeInsets.only(bottom: 16),\\n- child: Text(\\n- _errorMessage,\\n- style: const TextStyle(color: Colors.red),\\n- textAlign: TextAlign.center,\\n+ const SizedBox(height: _fieldSpacing),\\n+\\n+ // Confirm password field\\n+ TextFormField(\\n+ controller: _confirmPasswordController,\\n+ focusNode: _confirmPasswordFocusNode,\\n+ decoration: _buildInputDecoration(\\n+ labelText: 'Confirm Password',\\n+ prefixIcon: Icons.lock_outline,\\n+ suffixIcon: IconButton(\\n+ icon: Icon(\\n+ _isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,\\n+ color: Colors.grey[600],\\n+ ),\\n+ onPressed: () {\\n+ setState(() {\\n+ _isConfirmPasswordVisible = !_isConfirmPasswordVisible;\\n+ });\\n+ },\\n+ ),\\n ),\\n+ obscureText: !_isConfirmPasswordVisible,\\n+ textInputAction: TextInputAction.done,\\n+ validator: _validateConfirmPassword,\\n+ onFieldSubmitted: (_) =\\u003e _register(),\\n ),\\n- ElevatedButton(\\n- onPressed: _isLoading ? null : _register,\\n- child: _isLoading\\n- ? const SizedBox(\\n- width: 20,\\n- height: 20,\\n- child: CircularProgressIndicator(\\n- strokeWidth: 2,\\n+ const SizedBox(height: 24),\\n+\\n+ // Error message\\n+ if (_errorMessage != null)\\n+ Container(\\n+ padding: const EdgeInsets.all(12),\\n+ margin: const EdgeInsets.only(bottom: 16),\\n+ decoration: BoxDecoration(\\n+ color: Colors.red[50],\\n+ borderRadius: BorderRadius.circular(8),\\n+ border: Border.all(color: Colors.red[200]!),\\n+ ),\\n+ child: Row(\\n+ children: [\\n+ Icon(Icons.error_outline, color: Colors.red[700], size: 20),\\n+ const SizedBox(width: 8),\\n+ Expanded(\\n+ child: Text(\\n+ _errorMessage!,\\n+ style: TextStyle(\\n+ color: Colors.red[700],\\n+ fontSize: 14,\\n+ ),\\n+ ),\\n ),\\n- )\\n- : const Text('Register'),\\n- ),\\n- TextButton(\\n- onPressed: () =\\u003e context.pop(),\\n- child: const Text('Already have an account? Sign in'),\\n- ),\\n- ],\\n+ ],\\n+ ),\\n+ ),\\n+\\n+ // Register button\\n+ SizedBox(\\n+ height: 50,\\n+ child: ElevatedButton(\\n+ onPressed: _isLoading ? null : _register,\\n+ style: ElevatedButton.styleFrom(\\n+ backgroundColor: Theme.of(context).primaryColor,\\n+ foregroundColor: Colors.white,\\n+ elevation: 2,\\n+ shape: RoundedRectangleBorder(\\n+ borderRadius: BorderRadius.circular(_borderRadius),\\n+ ),\\n+ disabledBackgroundColor: Colors.grey[300],\\n+ ),\\n+ child: _isLoading\\n+ ? const SizedBox(\\n+ width: 20,\\n+ height: 20,\\n+ child: CircularProgressIndicator(\\n+ strokeWidth: 2,\\n+ valueColor: AlwaysStoppedAnimation\\u003cColor\\u003e(Colors.white),\\n+ ),\\n+ )\\n+ : const Text(\\n+ 'Create Account',\\n+ style: TextStyle(\\n+ fontSize: 16,\\n+ fontWeight: FontWeight.w600,\\n+ ),\\n+ ),\\n+ ),\\n+ ),\\n+ const SizedBox(height: 24),\\n+\\n+ // Sign in link\\n+ TextButton(\\n+ onPressed: _isLoading ? null : () =\\u003e context.pop(),\\n+ style: TextButton.styleFrom(\\n+ foregroundColor: Theme.of(context).primaryColor,\\n+ padding: const EdgeInsets.symmetric(vertical: 12),\\n+ ),\\n+ child: RichText(\\n+ text: TextSpan(\\n+ style: TextStyle(\\n+ color: Colors.grey[600],\\n+ fontSize: 14,\\n+ ),\\n+ children: [\\n+ const TextSpan(text: 'Already have an account? '),\\n+ TextSpan(\\n+ text: 'Sign In',\\n+ style: TextStyle(\\n+ color: Theme.of(context).primaryColor,\\n+ fontWeight: FontWeight.w600,\\n+ ),\\n+ ),\\n+ ],\\n+ ),\\n+ ),\\n+ ),\\n+ ],\\n+ ),\\n ),\\n ),\\n ),\",\"blob_url\":\"https://github.com/jasonkang14/sat-reading-client/blob/82150ea51ad8ff040a963edb0b87403a40997eef/lib%2Fapp%2Fscreens%2Flogin%2Fregister_screen.dart\",\"raw_url\":\"https://github.com/jasonkang14/sat-reading-client/raw/82150ea51ad8ff040a963edb0b87403a40997eef/lib%2Fapp%2Fscreens%2Flogin%2Fregister_screen.dart\",\"contents_url\":\"https://api.github.com/repos/jasonkang14/sat-reading-client/contents/lib%2Fapp%2Fscreens%2Flogin%2Fregister_screen.dart?ref=82150ea51ad8ff040a963edb0b87403a40997eef\"}]'\n", - "'agent': '{'tool_calls': [{'id': 'call_FF5I8SaaHhsdkYLDuePwtpgU', 'function': {'arguments': '{\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3}', 'name': 'request_copilot_review'}, 'type': 'function'}], 'refusal': None}'\n", - "'tools': ''\n", - "'agent': '{'tool_calls': [{'id': 'call_yz0QTvDvtGDAk8ZEjhIEMhaA', 'function': {'arguments': '{\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3,\"commitID\":\"82150ea51ad8ff040a963edb0b87403a40997eef\"}', 'name': 'create_pending_pull_request_review'}, 'type': 'function'}], 'refusal': None}'\n", - "'tools': 'pending pull request created'\n", - "'agent': '{'tool_calls': [{'id': 'call_LK2zZoz3wY0XDialmEpnFaue', 'function': {'arguments': '{\"body\":\"코드 리뷰 결과(회원가입 화면 구조 개선)\\\\n\\\\n1. 코드 개선 여부:\\\\n- 기존의 단순한 폼에서 가독성이 좋은 입력 검증 함수들과 시맨틱한 위젯 구조로 리팩터링되었습니다.\\\\n- 사용자 경험(UX) 측면, 에러 메시지와 유효성 검사 상세화 및 비밀번호 가시성 토글 추가 등 실질적인 이득이 확인됩니다. \\\\n- 코드의 재사용성, 유지보수성 모두 향상되었습니다.\\\\n\\\\n2. 부작용(Side effect) 여부:\\\\n- 기존 회원가입 flow를 대체하는 구조이므로, 신규/기존 사용자에 대한 edge case(ex: focusNode 미해제 등) 대비가 필요해 보이나 dispose에서 모두 정상 처리되고 있음이 확인되었습니다.\\\\n- 멀티 submit이나 중복 요청에 대한 방지는 `_isLoading` 플래그로 처리되어 안정적입니다.\\\\n\\\\n3. 보안 이슈:\\\\n- 이메일 및 패스워드 검증 로직이 추가되었으며, 입력제한(정규식)과 최소길이, 비밀번호 복잡도 체크가 정책에 맞게 잘 반영되어 있습니다.\\\\n- 비밀번호 필드를 기본적으로 가리고, 토글을 통해 사용자가 선택적으로 볼 수 있도록 UI가 구현되어 있습니다.\\\\n- 잠재적으로 입력 검증 bypass 등의 이슈는 없어 보이나, 추가 검증이 서버에서도 이루어지는지 확인 권장합니다.\\\\n\\\\n전체적으로 구조 개선 의도가 잘 반영된 PR이며, 사용자 및 보안 측면에서 실질적 개선이 확인됩니다. 추가적으로 서버 검증 로직에 대한 확인만 함께 진행하면 좋겠습니다.\",\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3,\"path\":\"lib/app/screens/login/register_screen.dart\",\"subjectType\":\"FILE\"}', 'name': 'add_pull_request_review_comment_to_pending_review'}, 'type': 'function'}], 'refusal': None}'\n", - "'tools': 'pull request review comment successfully added to pending review'\n", - "'agent': '{'tool_calls': [{'id': 'call_k2SAx48hvDnHfYva8tcmfRmz', 'function': {'arguments': '{\"body\":\"전체 코드 리뷰 & 코멘트 참고 부탁드립니다. \\\\n- 구조적 개선 및 UX, 보안 측면에서 모두 의미 있는 개선이 적용되었습니다.\\\\n- 리뷰 코멘트에 상세 내용을 남겼으니 참고해 주세요!\",\"event\":\"COMMENT\",\"owner\":\"jasonkang14\",\"repo\":\"sat-reading-client\",\"pullNumber\":3}', 'name': 'submit_pending_pull_request_review'}, 'type': 'function'}], 'refusal': None}'\n", - "'tools': 'pending pull request review successfully submitted'\n", - "'agent': '{'tool_calls': [{'id': 'call_ULA58itqZc9uhWhVhzdUCoTJ', 'function': {'arguments': '{\"channel_id\":\"C092QEJH6LB\",\"text\":\"<@U092VN0DHBP>님, PR(#3)에 코드리뷰 코멘트가 등록되었습니다.\\\\n\\\\n- PR 링크: https://github.com/jasonkang14/sat-reading-client/pull/3\\\\n- 리뷰 주요내용: 구조 개선, UX 및 보안 측면 강화가 확인되었습니다.\\\\n- 상세한 내용은 PR 코멘트 참고 부탁드립니다.\"}', 'name': 'slack_post_message'}, 'type': 'function'}], 'refusal': None}'\n", - "'tools': '{\"ok\":true,\"channel\":\"C092QEJH6LB\",\"ts\":\"1751178754.334099\",\"message\":{\"user\":\"U0946DSQYHE\",\"type\":\"message\",\"ts\":\"1751178754.334099\",\"bot_id\":\"B0946DSQKL0\",\"app_id\":\"A0946DRU6PJ\",\"text\":\"<@U092VN0DHBP>님, PR(#3)에 코드리뷰 코멘트가 등록되었습니다.\\n\\n- PR 링크: \\n- 리뷰 주요내용: 구조 개선, UX 및 보안 측면 강화가 확인되었습니다.\\n- 상세한 내용은 PR 코멘트 참고 부탁드립니다.\",\"team\":\"T092VN0DA85\",\"bot_profile\":{\"id\":\"B0946DSQKL0\",\"app_id\":\"A0946DRU6PJ\",\"user_id\":\"U0946DSQYHE\",\"name\":\"pr-bot\",\"icons\":{\"image_36\":\"https://a.slack-edge.com/80588/img/plugins/app/bot_36.png\",\"image_48\":\"https://a.slack-edge.com/80588/img/plugins/app/bot_48.png\",\"image_72\":\"https://a.slack-edge.com/80588/img/plugins/app/service_72.png\"},\"deleted\":false,\"updated\":1751163460,\"team_id\":\"T092VN0DA85\"},\"blocks\":[{\"type\":\"rich_text\",\"block_id\":\"1vF\",\"elements\":[{\"type\":\"rich_text_section\",\"elements\":[{\"type\":\"user\",\"user_id\":\"U092VN0DHBP\"},{\"type\":\"text\",\"text\":\"님, PR(#3)에 코드리뷰 코멘트가 등록되었습니다.\\n\\n- PR 링크: \"},{\"type\":\"link\",\"url\":\"https://github.com/jasonkang14/sat-reading-client/pull/3\"},{\"type\":\"text\",\"text\":\"\\n- 리뷰 주요내용: 구조 개선, UX 및 보안 측면 강화가 확인되었습니다.\\n- 상세한 내용은 PR 코멘트 참고 부탁드립니다.\"}]}]}]},\"warning\":\"missing_charset\",\"response_metadata\":{\"warnings\":[\"missing_charset\"]}}'\n", - "'agent': '코드 리뷰 및 알림 절차가 모두 완료되었습니다. 진행된 사항은 아래와 같습니다:\n", - "\n", - "1. PR(https://github.com/jasonkang14/sat-reading-client/pull/3) 코드 리뷰를 진행하였고, 다음 항목을 중심으로 코멘트를 남겼습니다.\n", - " - 코드가 구조적으로 확실히 개선되었음 (가독성, 유지보수성, UX 모두 향상)\n", - " - 예측하지 못한 side effect는 없음(메모리/이벤트 누수 등 기본 처리 정상)\n", - " - 비밀번호/회원가입 검증 및 UI 상 보안 강화 확인됨 (변경된 입력 정책이 코드에 잘 반영)\n", - " - 간단 검증은 모두 클라이언트에서 이뤄지지만 backend(서버)에서도 검증이 필요함을 제안\n", - "\n", - "2. 리뷰 코멘트 등록이 완료되었으며, PR 내 해당 파일(`register_screen.dart`)에 남겨두었습니다.\n", - "\n", - "3. 슬랙 채널 <@C092QEJH6LB>에 <@U092VN0DHBP>님을 멘션하여, PR 리뷰 및 코멘트 등록 사실을 안내했습니다.\n", - "\n", - "궁금하신 사항이나 추가 요청이 있으면 말씀해주세요!'\n", - "\n", - "Final result: {'agent': {'messages': [AIMessage(content='코드 리뷰 및 알림 절차가 모두 완료되었습니다. 진행된 사항은 아래와 같습니다:\\n\\n1. PR(https://github.com/jasonkang14/sat-reading-client/pull/3) 코드 리뷰를 진행하였고, 다음 항목을 중심으로 코멘트를 남겼습니다.\\n - 코드가 구조적으로 확실히 개선되었음 (가독성, 유지보수성, UX 모두 향상)\\n - 예측하지 못한 side effect는 없음(메모리/이벤트 누수 등 기본 처리 정상)\\n - 비밀번호/회원가입 검증 및 UI 상 보안 강화 확인됨 (변경된 입력 정책이 코드에 잘 반영)\\n - 간단 검증은 모두 클라이언트에서 이뤄지지만 backend(서버)에서도 검증이 필요함을 제안\\n\\n2. 리뷰 코멘트 등록이 완료되었으며, PR 내 해당 파일(`register_screen.dart`)에 남겨두었습니다.\\n\\n3. 슬랙 채널 <@C092QEJH6LB>에 <@U092VN0DHBP>님을 멘션하여, PR 리뷰 및 코멘트 등록 사실을 안내했습니다.\\n\\n궁금하신 사항이나 추가 요청이 있으면 말씀해주세요!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 267, 'prompt_tokens': 12354, 'total_tokens': 12621, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 11776}}, 'model_name': 'gpt-4.1-2025-04-14', 'system_fingerprint': 'fp_799e4ca3f1', 'id': 'chatcmpl-BnfsZXM0fX077VSjxvlIqw8gqPvMM', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='run--929605e2-fddd-4c1e-80a0-d631e9977d26-0', usage_metadata={'input_tokens': 12354, 'output_tokens': 267, 'total_tokens': 12621, 'input_token_details': {'audio': 0, 'cache_read': 11776}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}}\n" - ] - } - ], - "source": [ - "\n", - "all_chunks = await process_stream(stream_generator)\n", - "\n", - "\n", - "if all_chunks:\n", - " final_result = all_chunks[-1]\n", - " print(\"\\nFinal result:\", final_result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From d59dc710430fe7194e78081ae752efe5ef30c302 Mon Sep 17 00:00:00 2001 From: Hensley Joo Date: Wed, 13 Aug 2025 19:26:39 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=EC=9A=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20tiktoken=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + scripts/auto_code_review.py | 135 +++++++++++++++++++++++++----------- 2 files changed, 94 insertions(+), 42 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2420933..61b9f2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ openai python-dotenv langchain langchain-openai +tiktoken diff --git a/scripts/auto_code_review.py b/scripts/auto_code_review.py index 54dce61..1a86f57 100644 --- a/scripts/auto_code_review.py +++ b/scripts/auto_code_review.py @@ -14,10 +14,37 @@ from langchain_mcp_adapters.client import MultiServerMCPClient from langgraph.prebuilt import create_react_agent from langchain_core.messages import HumanMessage +import tiktoken # 토큰 카운팅을 위해 추가 # 환경변수 로드 load_dotenv() +def count_tokens(text, model="gpt-4"): + """텍스트의 토큰 수를 계산합니다.""" + try: + encoding = tiktoken.encoding_for_model(model) + return len(encoding.encode(text)) + except: + # 대략적인 추정: 1 토큰 ≈ 4 문자 + return len(text) // 4 + +def estimate_pr_tokens(pr_info): + """PR 정보의 대략적인 토큰 수를 추정합니다.""" + estimated_tokens = 0 + + # 기본 PR 정보 + if pr_info.get('title'): + estimated_tokens += count_tokens(pr_info['title']) + if pr_info.get('body'): + estimated_tokens += count_tokens(pr_info['body']) + + # 변경된 파일들 (대략적 추정) + # 각 파일당 평균 1000 토큰으로 가정 + changed_files = pr_info.get('changed_files', 0) + estimated_tokens += changed_files * 1000 + + return estimated_tokens + async def setup_mcp_client(): """MCP 클라이언트 설정""" github_pat = os.getenv("GITHUB_PAT") @@ -119,63 +146,87 @@ def save_review_result(owner, repo, pr_number, review_content, output_dir="revie print(f"❌ 리뷰 결과 저장 중 오류: {e}") return None -async def run_code_review(owner, repo, pr_number, pr_url, save_result=True): +async def run_code_review(owner, repo, pr_number, pr_url): """코드리뷰 실행""" try: + print(f"코드리뷰 시작: {owner}/{repo} PR #{pr_number}") + + # PR 정보 가져오기 (간단한 버전) + try: + import subprocess + result = subprocess.run( + ["gh", "pr", "view", str(pr_number), "--json", "title,body,changedFiles"], + capture_output=True, + text=True, + check=True + ) + pr_info = json.loads(result.stdout) + + # 토큰 수 추정 + estimated_tokens = estimate_pr_tokens(pr_info) + print(f"예상 토큰 수: {estimated_tokens}") + + # 토큰 제한 체크 (30,000 토큰) + if estimated_tokens > 25000: # 여유분을 두고 체크 + print(f"⚠️ 토큰 제한 초과 예상 ({estimated_tokens} > 25000)") + print("🚫 코드리뷰를 건너뜁니다.") + + # 간단한 메시지만 작성 + simple_message = f"""## 🤖 AI 자동 코드리뷰 + +PR #{pr_number}의 변경사항이 너무 커서 자동 코드리뷰를 건너뜁니다. + +**이유**: 토큰 제한 초과 (예상 {estimated_tokens} 토큰) + +**권장사항**: +- 수동으로 코드리뷰를 진행해주세요 +- 또는 PR을 더 작은 단위로 나누어주세요 + +--- +*AI 자동 코드리뷰 시스템*""" + + # PR에 코멘트 작성 + subprocess.run([ + "gh", "pr", "comment", str(pr_number), + "--body", simple_message + ], check=True) + + return True + + except Exception as e: + print(f"PR 정보 가져오기 실패: {e}") + # MCP 클라이언트 설정 mcp_client = await setup_mcp_client() # 도구 목록 가져오기 - tool_list = await mcp_client.get_tools() + tools = await mcp_client.get_tools() # 에이전트 생성 agent = create_react_agent( - model="openai:gpt-4.1", - tools=tool_list, + model="openai:gpt-4o-mini", # 더 가벼운 모델 사용 + tools=tools, prompt="Use the tools provided to you to answer the user's question" ) - # 코드리뷰 요청 메시지 생성 (Slack 관련 부분 제거) - human_message = f"""깃헙의 Pull Request를 확인하고 코드 리뷰를 작성해주세요. -PR의 코드를 리뷰한 후에, 아래 항목을 확인해주세요; -1. 코드가 개선되었는지 -2. 예측하지 못한 side effect가 있는지 -3. 보안상 문제가 될 수 있는 부분이 없는지 - -위 내용을 확인해서 PR에 코멘트로 남겨주세요. - -PR URL: {pr_url}""" + # 코드리뷰 요청 메시지 생성 (간소화) + human_message = f"""PR #{pr_number}의 코드를 간단히 리뷰해주세요. - # 스트림 실행 - stream_generator = agent.astream( - {"messages": [HumanMessage(human_message)]}, - stream_mode="updates" - ) + 주요 변경사항만 확인하고 간결한 코멘트를 작성해주세요. - # 결과 처리 - all_chunks = await process_stream(stream_generator) + PR URL: {pr_url}""" + + print("코드리뷰 실행 중...") + + # 스트림 처리 + result = await process_stream(agent.astream([HumanMessage(content=human_message)])) + + # 결과 저장 + await save_review_result(owner, repo, pr_number, result) + + print("코드리뷰 완료!") + return True - if all_chunks: - final_result = all_chunks[-1] - print("\nFinal result:", final_result) - - # 리뷰 내용 추출 - review_content = None - if 'agent' in final_result and 'messages' in final_result['agent']: - for message in final_result['agent']['messages']: - if hasattr(message, 'content') and message.content: - review_content = message.content - break - - # 결과 저장 - if save_result and review_content: - save_review_result(owner, repo, pr_number, review_content) - - return True - else: - print("No results from code review") - return False - except Exception as e: print(f"코드리뷰 실행 중 오류 발생: {e}") return False From 3b50c6929613ca969b02012f83fa26f380c64774 Mon Sep 17 00:00:00 2001 From: Hensley Joo Date: Wed, 13 Aug 2025 19:30:51 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20save=5Fresult=20=EB=A7=A4=EA=B0=9C?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/auto_code_review.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/auto_code_review.py b/scripts/auto_code_review.py index 1a86f57..3684203 100644 --- a/scripts/auto_code_review.py +++ b/scripts/auto_code_review.py @@ -237,7 +237,6 @@ def main(): parser.add_argument('--repo', required=True, help='GitHub 저장소 이름') parser.add_argument('--pr-number', required=True, type=int, help='PR 번호') parser.add_argument('--pr-url', required=True, help='PR URL') - parser.add_argument('--no-save', action='store_true', help='결과를 파일로 저장하지 않음') args = parser.parse_args() @@ -248,8 +247,7 @@ def main(): args.owner, args.repo, args.pr_number, - args.pr_url, - save_result=not args.no_save + args.pr_url )) if success: From 58f6814eaad592988fff130f72357083d18ef7af Mon Sep 17 00:00:00 2001 From: Hensley Joo Date: Wed, 13 Aug 2025 19:53:11 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20GitHub=20CLI=20=EC=98=A4=EB=A5=98,?= =?UTF-8?q?=20LangGraph=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=98=95=EC=8B=9D?= =?UTF-8?q?,=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/auto_code_review.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/auto_code_review.py b/scripts/auto_code_review.py index 3684203..92c9f39 100644 --- a/scripts/auto_code_review.py +++ b/scripts/auto_code_review.py @@ -193,6 +193,9 @@ async def run_code_review(owner, repo, pr_number, pr_url): return True + except subprocess.CalledProcessError as e: + print(f"GitHub CLI 오류: {e}") + print("GitHub CLI가 설치되어 있고 로그인되어 있는지 확인하세요.") except Exception as e: print(f"PR 정보 가져오기 실패: {e}") @@ -219,10 +222,11 @@ async def run_code_review(owner, repo, pr_number, pr_url): print("코드리뷰 실행 중...") # 스트림 처리 - result = await process_stream(agent.astream([HumanMessage(content=human_message)])) + result = await process_stream(agent.astream({"messages": [HumanMessage(content=human_message)]})) # 결과 저장 - await save_review_result(owner, repo, pr_number, result) + if result: + save_review_result(owner, repo, pr_number, str(result)) print("코드리뷰 완료!") return True