From 80e5c80919c3732eabba633648ae8537842d54fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BC=A8=E7=BC=A8?= Date: Fri, 17 Jan 2025 18:08:11 +0800 Subject: [PATCH 1/4] refactor(assistant): add the tailwind scope (#697) * refactor(assistant): add the tailwind scope * chore: release 2.0.24 --- assistant/package.json | 3 ++- assistant/postcss.config.js | 3 ++- assistant/src/Assistant/index.tsx | 2 +- assistant/src/Chat/index.tsx | 2 +- assistant/src/GitInsight/index.tsx | 2 +- assistant/src/StarterList/index.tsx | 2 +- assistant/src/ThoughtChain/index.tsx | 2 +- assistant/tailwind.css | 28 +++++++++++++++------------- assistant/yarn.lock | 7 +++++++ 9 files changed, 31 insertions(+), 20 deletions(-) diff --git a/assistant/package.json b/assistant/package.json index c993f9ff..352643b9 100644 --- a/assistant/package.json +++ b/assistant/package.json @@ -1,6 +1,6 @@ { "name": "@petercatai/assistant", - "version": "2.0.23", + "version": "2.0.24", "description": "PeterCat Assistant Application", "repository": "https://github.com/petercat-ai/petercat.git", "license": "MIT", @@ -91,6 +91,7 @@ "lint-staged": "^13.0.3", "postcss": "^8.4.49", "postcss-cli": "^11.0.0", + "postcss-nested": "^7.0.2", "postcss-prefix-selector": "^2.1.0", "prettier": "^2.7.1", "prettier-plugin-organize-imports": "^3.0.0", diff --git a/assistant/postcss.config.js b/assistant/postcss.config.js index 44eb5f6e..b8a28cf8 100644 --- a/assistant/postcss.config.js +++ b/assistant/postcss.config.js @@ -1,6 +1,7 @@ module.exports = { plugins: [ require('tailwindcss'), - require('autoprefixer') + require('autoprefixer'), + require("postcss-nested"), ] }; diff --git a/assistant/src/Assistant/index.tsx b/assistant/src/Assistant/index.tsx index f6097b0f..cb86e702 100644 --- a/assistant/src/Assistant/index.tsx +++ b/assistant/src/Assistant/index.tsx @@ -67,7 +67,7 @@ const Assistant = (props: AssistantProps) => { ); return ( -
+
= memo( // ============================ Render ============================ return (
{ }, []); return ( -
+
diff --git a/assistant/src/StarterList/index.tsx b/assistant/src/StarterList/index.tsx index 50a297fd..21b99200 100644 --- a/assistant/src/StarterList/index.tsx +++ b/assistant/src/StarterList/index.tsx @@ -10,7 +10,7 @@ export interface IProps { const StarterList: FC = ({ starters, onClick, style, className }) => { return ( -
+
= (params) => { }, []); return ( -
+
Date: Mon, 20 Jan 2025 17:02:43 +0800 Subject: [PATCH 2/4] feat: add API for data insights on PRs, issues, and code changes. (#700) * feat: init the insight about issue * feat: add the api for pr&issue&code insight * chore: add tests for the insight utils --- server/insight/router.py | 51 ++++++++++++++++++++ server/insight/service/issue.py | 14 ++++++ server/insight/service/pr.py | 21 +++++++++ server/main.py | 8 +++- server/tests/utils/test_insight.py | 74 ++++++++++++++++++++++++++++++ server/utils/insight.py | 56 ++++++++++++++++++++++ 6 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 server/insight/router.py create mode 100644 server/insight/service/issue.py create mode 100644 server/insight/service/pr.py create mode 100644 server/tests/utils/test_insight.py create mode 100644 server/utils/insight.py diff --git a/server/insight/router.py b/server/insight/router.py new file mode 100644 index 00000000..82ff1849 --- /dev/null +++ b/server/insight/router.py @@ -0,0 +1,51 @@ +import json +from typing import Optional +from fastapi import APIRouter, Depends +from insight.service.issue import get_issue_data +from insight.service.pr import get_code_changes, get_pr_data + + +router = APIRouter( + prefix="/api/insight", + tags=["insight"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("/issue") +def get_issue_insight(repo_name: str): + try: + result = get_issue_data(repo_name) + return { + "success": True, + "data": result, + } + + except Exception as e: + return json.dumps({"success": False, "message": str(e)}) + + +@router.get("/pr") +def get_pr_insight(repo_name: str): + try: + result = get_pr_data(repo_name) + return { + "success": True, + "data": result, + } + + except Exception as e: + return json.dumps({"success": False, "message": str(e)}) + + +@router.get("code_change") +def get_code_change_insight(repo_name: str): + try: + result = get_code_changes(repo_name) + return { + "success": True, + "data": result, + } + + except Exception as e: + return json.dumps({"success": False, "message": str(e)}) diff --git a/server/insight/service/issue.py b/server/insight/service/issue.py new file mode 100644 index 00000000..f41d2135 --- /dev/null +++ b/server/insight/service/issue.py @@ -0,0 +1,14 @@ +import requests +from collections import defaultdict + +from utils.insight import get_data + + +def get_issue_data(repo_name): + metrics_mapping = { + "issues_new": "open", + "issues_closed": "close", + "issue_comments": "comment", + } + issue_data = get_data(repo_name, metrics_mapping) + return issue_data diff --git a/server/insight/service/pr.py b/server/insight/service/pr.py new file mode 100644 index 00000000..290fb845 --- /dev/null +++ b/server/insight/service/pr.py @@ -0,0 +1,21 @@ +import requests +from collections import defaultdict + +from utils.insight import get_data + + +def get_pr_data(repo_name): + metrics_mapping = { + "change_requests": "open", + "change_requests_accepted": "merge", + "change_requests_reviews": "reviews", + } + return get_data(repo_name, metrics_mapping) + + +def get_code_changes(repo_name): + metrics_mapping = { + "code_change_lines_add": "add", + "code_change_lines_remove": "remove", + } + return get_data(repo_name, metrics_mapping) diff --git a/server/main.py b/server/main.py index 6089da19..4f56bcb2 100644 --- a/server/main.py +++ b/server/main.py @@ -20,6 +20,7 @@ from rag import router as rag_router from task import router as task_router from user import router as user_router +from insight import router as insight_router AUTH0_DOMAIN = get_env_variable("AUTH0_DOMAIN") API_AUDIENCE = get_env_variable("API_IDENTIFIER") @@ -62,6 +63,7 @@ app.include_router(github_app_router.router) app.include_router(aws_router.router) app.include_router(user_router.router) +app.include_router(insight_router.router) @app.get("/") @@ -75,7 +77,7 @@ def health_checker(): "ENVIRONMENT": ENVIRONMENT, "API_URL": API_URL, "WEB_URL": WEB_URL, - "CALLBACK_URL": CALLBACK_URL + "CALLBACK_URL": CALLBACK_URL, } @@ -88,4 +90,6 @@ def health_checker(): reload=True, ) else: - uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PETERCAT_PORT", "8080"))) + uvicorn.run( + app, host="0.0.0.0", port=int(os.environ.get("PETERCAT_PORT", "8080")) + ) diff --git a/server/tests/utils/test_insight.py b/server/tests/utils/test_insight.py new file mode 100644 index 00000000..acbef380 --- /dev/null +++ b/server/tests/utils/test_insight.py @@ -0,0 +1,74 @@ +import unittest +from unittest.mock import patch, Mock +from collections import defaultdict + +from utils.insight import get_data + + +class TestGetData(unittest.TestCase): + + @patch("requests.get") + def test_get_data_success(self, mock_get): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"2023-01": 10, "2023-02": 20, "2023-03": 30} + mock_get.return_value = mock_response + + repo_name = "test-repo" + metrics_mapping = {"metric1": "sum", "metric2": "average"} + + expected_result = { + "year": [ + {"type": "sum", "date": "2023", "value": 60}, + {"type": "average", "date": "2023", "value": 60}, + ], + "quarter": [ + {"type": "sum", "date": "2023Q1", "value": 60}, + {"type": "average", "date": "2023Q1", "value": 60}, + ], + "month": [ + {"type": "sum", "date": "2023-01", "value": 10}, + {"type": "average", "date": "2023-01", "value": 10}, + {"type": "sum", "date": "2023-02", "value": 20}, + {"type": "average", "date": "2023-02", "value": 20}, + {"type": "sum", "date": "2023-03", "value": 30}, + {"type": "average", "date": "2023-03", "value": 30}, + ], + } + + result = get_data(repo_name, metrics_mapping) + self.assertEqual(result, expected_result) + + @patch("requests.get") + def test_get_data_failure(self, mock_get): + mock_response = Mock() + mock_response.status_code = 500 + mock_get.return_value = mock_response + + repo_name = "test-repo" + metrics_mapping = {"metric1": "sum"} + + expected_result = { + "year": [], + "quarter": [], + "month": [], + } + + result = get_data(repo_name, metrics_mapping) + self.assertEqual(result, expected_result) + + @patch("requests.get") + def test_get_data_empty_response(self, mock_get): + # 模拟返回空数据 + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_get.return_value = mock_response + + repo_name = "test-repo" + metrics_mapping = {"metric1": "sum"} + + expected_result = {"year": [], "quarter": [], "month": []} + + result = get_data(repo_name, metrics_mapping) + self.assertEqual(result, expected_result) diff --git a/server/utils/insight.py b/server/utils/insight.py new file mode 100644 index 00000000..aa73c71b --- /dev/null +++ b/server/utils/insight.py @@ -0,0 +1,56 @@ +import requests +from collections import defaultdict + + +def get_data(repo_name, metrics_mapping): + """ + :param repo_name: GitHub 仓库名 + :param metrics_mapping: 指标名称与聚合类型的映射字典 + :return: 按年、季度、月聚合的数据字典 + """ + base_url = f"https://oss.open-digger.cn/github/{repo_name}/" + + aggregated_data = { + "year": defaultdict( + lambda: {metric_type: 0 for metric_type in metrics_mapping.values()} + ), + "quarter": defaultdict( + lambda: {metric_type: 0 for metric_type in metrics_mapping.values()} + ), + "month": defaultdict( + lambda: {metric_type: 0 for metric_type in metrics_mapping.values()} + ), + } + + for metric, metric_type in metrics_mapping.items(): + url = f"{base_url}{metric}.json" + response = requests.get(url) + + if response.status_code == 200: + data = response.json() + for date, value in data.items(): + if "-" in date: + year, month = date.split("-")[:2] + quarter = f"{year}Q{(int(month) - 1) // 3 + 1}" + + # aggregate by year, quarter, and month + aggregated_data["year"][year][metric_type] += value + aggregated_data["quarter"][quarter][metric_type] += value + aggregated_data["month"][date][metric_type] += value + else: + print( + f"Error fetching data from {url} (status code: {response.status_code})" + ) + + def format_result(data): + result = [] + for date, counts in data.items(): + for type_, value in counts.items(): + result.append({"type": type_, "date": date, "value": value}) + return result + + return { + "year": format_result(aggregated_data["year"]), + "quarter": format_result(aggregated_data["quarter"]), + "month": format_result(aggregated_data["month"]), + } From bee2c37fa2074a51b396e38ea95fbb1be943466d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BC=A8=E7=BC=A8?= Date: Mon, 20 Jan 2025 17:06:29 +0800 Subject: [PATCH 3/4] chore: fix router of the insight (#701) --- server/insight/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/insight/router.py b/server/insight/router.py index 82ff1849..92d9871a 100644 --- a/server/insight/router.py +++ b/server/insight/router.py @@ -38,7 +38,7 @@ def get_pr_insight(repo_name: str): return json.dumps({"success": False, "message": str(e)}) -@router.get("code_change") +@router.get("/code_change") def get_code_change_insight(repo_name: str): try: result = get_code_changes(repo_name) From 2c53b086bf44f04a55f13a2ec03f4fbd13640911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BC=A8=E7=BC=A8?= Date: Tue, 21 Jan 2025 11:44:12 +0800 Subject: [PATCH 4/4] feat: add the activity insight (#703) * chore: fix ci * feat: add the activity insight * chore: add the test for the activity * chore: fix ci --- server/insight/router.py | 18 +++++++++-- server/insight/service/activity.py | 28 +++++++++++++++++ server/insight/service/issue.py | 3 -- server/insight/service/pr.py | 3 -- server/tests/insight/test_activity.py | 45 +++++++++++++++++++++++++++ server/utils/insight.py | 5 +++ 6 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 server/insight/service/activity.py create mode 100644 server/tests/insight/test_activity.py diff --git a/server/insight/router.py b/server/insight/router.py index 92d9871a..3dd07a51 100644 --- a/server/insight/router.py +++ b/server/insight/router.py @@ -1,10 +1,11 @@ import json -from typing import Optional -from fastapi import APIRouter, Depends +from fastapi import APIRouter +from insight.service.activity import get_activity_data from insight.service.issue import get_issue_data from insight.service.pr import get_code_changes, get_pr_data +# ref: https://open-digger.cn/en/docs/user_docs/metrics/metrics_usage_guide router = APIRouter( prefix="/api/insight", tags=["insight"], @@ -49,3 +50,16 @@ def get_code_change_insight(repo_name: str): except Exception as e: return json.dumps({"success": False, "message": str(e)}) + + +@router.get("/activity") +def get_activity_insight(repo_name: str): + try: + result = get_activity_data(repo_name) + return { + "success": True, + "data": result, + } + + except Exception as e: + return json.dumps({"success": False, "message": str(e)}) diff --git a/server/insight/service/activity.py b/server/insight/service/activity.py new file mode 100644 index 00000000..54f62061 --- /dev/null +++ b/server/insight/service/activity.py @@ -0,0 +1,28 @@ +import datetime +import requests +from typing import List, Dict + + +def get_activity_data(repo_name: str) -> List[Dict[str, int]]: + url = f"https://oss.open-digger.cn/github/{repo_name}/activity_details.json" + + try: + response = requests.get(url) + data = response.json() + if not data: + return [] + + # Filter out only the monthly data (excluding quarters) + monthly_data = {k: v for k, v in data.items() if "-" in k} + + # Get the most recent month + most_recent_month_key = max(monthly_data.keys()) + + # Return the data for the most recent month + return [ + {"user": user, "value": value} + for user, value in monthly_data[most_recent_month_key] + ] + except Exception as e: + print(e) + return [] diff --git a/server/insight/service/issue.py b/server/insight/service/issue.py index f41d2135..2ebf2aba 100644 --- a/server/insight/service/issue.py +++ b/server/insight/service/issue.py @@ -1,6 +1,3 @@ -import requests -from collections import defaultdict - from utils.insight import get_data diff --git a/server/insight/service/pr.py b/server/insight/service/pr.py index 290fb845..7044965a 100644 --- a/server/insight/service/pr.py +++ b/server/insight/service/pr.py @@ -1,6 +1,3 @@ -import requests -from collections import defaultdict - from utils.insight import get_data diff --git a/server/tests/insight/test_activity.py b/server/tests/insight/test_activity.py new file mode 100644 index 00000000..cf0cb020 --- /dev/null +++ b/server/tests/insight/test_activity.py @@ -0,0 +1,45 @@ +import unittest +from unittest.mock import patch, MagicMock +from insight.service.activity import get_activity_data + + +class TestGetActivityData(unittest.TestCase): + + @patch("insight.service.activity.requests.get") + def test_get_activity_data(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = { + "2023-12": [("user1", 10), ("user2", 5)], + "2024-01": [("user3", 20)], + "2024-02": [("user4", 25)], + "2024-03": [("user5", 30)], + } + mock_get.return_value = mock_response + repo_name = "petercat-ai/petercat" + expected_result = [{"user": "user5", "value": 30}] + + result = get_activity_data(repo_name) + + self.assertIsInstance(result, list) + self.assertEqual(result, expected_result) + + @patch("insight.service.activity.requests.get") + def test_get_activity_data_empty(self, mock_get): + mock_response = MagicMock() + mock_response.json.return_value = {} + mock_get.return_value = mock_response + repo_name = "petercat-ai/petercat" + result = get_activity_data(repo_name) + + self.assertEqual(result, []) + + @patch("insight.service.activity.requests.get") + def test_get_activity_data_invalid_json(self, mock_get): + + mock_response = MagicMock() + mock_response.json.side_effect = ValueError("Invalid JSON") + mock_get.return_value = mock_response + + repo_name = "petercat-ai/petercat" + with self.assertRaises(ValueError): + get_activity_data(repo_name) diff --git a/server/utils/insight.py b/server/utils/insight.py index aa73c71b..544098c9 100644 --- a/server/utils/insight.py +++ b/server/utils/insight.py @@ -41,6 +41,11 @@ def get_data(repo_name, metrics_mapping): print( f"Error fetching data from {url} (status code: {response.status_code})" ) + return { + "year": [], + "quarter": [], + "month": [], + } def format_result(data): result = []