From a56ccc71b9e538e0e5495b8920f90210f22f21c5 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Tue, 9 Dec 2025 00:29:45 +0530 Subject: [PATCH 1/2] Added UI and automated review button --- ui/README_UI.md | 25 +++ ui/app.py | 393 ++++++++++++++++++++++++++++++++++++++++ ui/requirements-ui.txt | 4 + ui/templates/index.html | 253 ++++++++++++++++++++++++++ 4 files changed, 675 insertions(+) create mode 100644 ui/README_UI.md create mode 100644 ui/app.py create mode 100644 ui/requirements-ui.txt create mode 100644 ui/templates/index.html diff --git a/ui/README_UI.md b/ui/README_UI.md new file mode 100644 index 00000000..9e7c45c6 --- /dev/null +++ b/ui/README_UI.md @@ -0,0 +1,25 @@ +# PR-Agent UI (single-page) + +This is a minimal single-page UI for the PR-Agent repository. It lists the repository (current workspace) and shows details when you click the repo. + +Quick start (macOS / zsh): + +```bash +cd /path/to/pr-agent +python3 -m venv .venv-ui +source .venv-ui/bin/activate +pip install -r ui/requirements-ui.txt +python ui/app.py +``` + +Then open `http://127.0.0.1:8080/` in your browser. + +Notes: +- This is intentionally minimal: it reads Git information by running `git` in the repository root. +- The app locates the repo root by walking up from the current working directory until it finds a `.git` folder. +- It is safe to run locally and intended as a starting point — you can extend it to call the CLI for review/describe actions and show results inline. + +Next steps you may want: +- Add endpoints that call the internal CLI (e.g., trigger `python cli.py --pr_url=... review`) and stream results back to the browser. +- Add authentication for multi-user environments. +- Use GitPython instead of shelling out to `git` for richer information. diff --git a/ui/app.py b/ui/app.py new file mode 100644 index 00000000..9108abe3 --- /dev/null +++ b/ui/app.py @@ -0,0 +1,393 @@ +from flask import Flask, jsonify, render_template, request, Response +from pathlib import Path +import subprocess +import json + +# GitPython +from git import Repo, InvalidGitRepositoryError + +app = Flask(__name__, template_folder='templates', static_folder='static') + + +def find_repo_root(start: Path) -> Path: + root = start.resolve() + while root != root.parent: + if (root / '.git').exists(): + return root + root = root.parent + return start.resolve() + + +def get_repo_info(path: Path): + repo = {} + repo['name'] = path.name + repo['path'] = str(path) + try: + r = Repo(path) + remotes = [] + for remote in r.remotes: + for url in remote.urls: + remotes.append(f"{remote.name}\t{url}") + repo['remotes'] = remotes + + branches = [b.name for b in r.branches] + # add remote heads names + branches += [h.name for h in r.remotes] + repo['branches'] = branches + + commit = r.head.commit + repo['last_commit'] = f"{commit.hexsha[:7]} - {commit.message.splitlines()[0]} ({commit.committed_datetime.isoformat()}) <{commit.author.name}>" + + files = [p.name for p in path.iterdir() if p.is_file()] + repo['files'] = files + except InvalidGitRepositoryError: + repo['remotes'] = [] + repo['branches'] = [] + repo['last_commit'] = '' + repo['files'] = [p.name for p in path.iterdir() if p.is_file()] + except Exception as e: + repo['remotes'] = [str(e)] + repo['branches'] = [] + repo['last_commit'] = '' + repo['files'] = [] + return repo + + +def _parse_github_remote_url(url: str): + # support formats: git@github.com:owner/repo.git or https://github.com/owner/repo.git + try: + if url.startswith('git@'): + # git@github.com:owner/repo.git + _, path = url.split(':', 1) + owner_repo = path + if owner_repo.endswith('.git'): + owner_repo = owner_repo[:-4] + owner, repo = owner_repo.split('/', 1) + return owner, repo + + # handle https:// or http:// + if url.startswith('http'): + # strip possible trailing .git + if url.endswith('.git'): + url = url[:-4] + parts = url.rstrip('/').split('/') + # expect .../owner/repo + if len(parts) >= 2: + owner = parts[-2] + repo = parts[-1] + return owner, repo + except Exception: + return None, None + return None, None + + +def get_open_prs(path: Path): + # Determine owner/repo from remotes (prefer origin) + try: + r = Repo(path) + origin = None + for remote in r.remotes: + if remote.name == 'origin': + origin = next(iter(remote.urls), None) + break + if origin is None and r.remotes: + origin = next(iter(r.remotes[0].urls), None) + except Exception: + origin = None + + if not origin: + return {'error': 'no git remote found', 'prs': []} + + owner, repo = _parse_github_remote_url(origin) + if not owner or not repo: + return {'error': 'unable to parse remote url', 'prs': []} + + # Get token via helper (env or pr_agent/settings/.secrets.toml in repo root) + gh_token = _get_github_token() + headers = {'Accept': 'application/vnd.github+json'} + if gh_token: + headers['Authorization'] = f'token {gh_token}' + + import requests + api = f'https://api.github.com/repos/{owner}/{repo}/pulls?state=open' + try: + r = requests.get(api, headers=headers, timeout=10) + if r.status_code != 200: + # include response text to aid debugging (e.g., 404 for wrong owner/repo) + text = r.text + return {'error': f'GitHub API status {r.status_code}: {text}', 'prs': []} + items = r.json() + prs = [] + for it in items: + prs.append({ + 'number': it.get('number'), + 'title': it.get('title'), + 'user': it.get('user', {}).get('login'), + 'html_url': it.get('html_url'), + 'created_at': it.get('created_at') + }) + return {'error': None, 'prs': prs} + except Exception as e: + return {'error': str(e), 'prs': []} + + +def _get_github_token(): + import os + gh_token = os.environ.get('GITHUB_TOKEN') or os.environ.get('GH_TOKEN') + if gh_token: + return gh_token + # fallback to local secrets file located relative to the repository root + try: + import toml + repo_root = find_repo_root(Path.cwd()) + sec_path = repo_root / 'pr_agent' / 'settings' / '.secrets.toml' + if sec_path.is_file(): + data = toml.load(sec_path) + gh = data.get('github') or {} + gh_token = gh.get('user_token') or gh.get('user-token') or gh.get('token') + return gh_token + except Exception: + return None + return None + + +@app.route('/api/github/repos') +def github_repos(): + """List repositories accessible by the configured GitHub token. + Falls back to public repos if no token is provided. + """ + import requests + token = _get_github_token() + headers = {'Accept': 'application/vnd.github+json'} + if token: + headers['Authorization'] = f'token {token}' + + repos = [] + try: + # user repos (includes org repos the user has access to) + url = 'https://api.github.com/user/repos?per_page=100&type=all' + r = requests.get(url, headers=headers, timeout=10) + if r.status_code != 200: + return jsonify({'error': f'GitHub API status {r.status_code}: {r.text}', 'repos': []}), 200 + items = r.json() + for it in items: + repos.append({ + 'full_name': it.get('full_name'), + 'name': it.get('name'), + 'owner': it.get('owner', {}).get('login'), + 'private': it.get('private'), + 'html_url': it.get('html_url') + }) + return jsonify({'error': None, 'repos': repos}) + except Exception as e: + return jsonify({'error': str(e), 'repos': []}), 200 + + +@app.route('/api/github/repos///prs') +def github_repo_prs(owner, repo): + import requests + token = _get_github_token() + headers = {'Accept': 'application/vnd.github+json'} + if token: + headers['Authorization'] = f'token {token}' + api = f'https://api.github.com/repos/{owner}/{repo}/pulls?state=open&per_page=100' + try: + r = requests.get(api, headers=headers, timeout=10) + if r.status_code != 200: + return jsonify({'error': f'GitHub API status {r.status_code}: {r.text}', 'prs': []}), 200 + items = r.json() + prs = [] + for it in items: + prs.append({ + 'number': it.get('number'), + 'title': it.get('title'), + 'user': it.get('user', {}).get('login'), + 'html_url': it.get('html_url'), + 'created_at': it.get('created_at') + }) + return jsonify({'error': None, 'prs': prs}), 200 + except Exception as e: + return jsonify({'error': str(e), 'prs': []}), 200 + + +@app.route('/api/github/repos///setup-automation', methods=['POST']) +def setup_repo_automation(owner, repo): + """Setup GitHub Actions workflow for automated PR review, describe, and improve.""" + import requests + import base64 + + token = _get_github_token() + if not token: + return jsonify({'error': 'GitHub token not configured', 'success': False}), 200 + + headers = { + 'Accept': 'application/vnd.github+json', + 'Authorization': f'token {token}' + } + + # Workflow content + workflow_content = """name: PR-Agent Automation + +on: + pull_request: + types: [opened, reopened, synchronize] + issue_comment: + types: [created] + +permissions: + issues: write + pull-requests: write + contents: read + +jobs: + pr_agent_job: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && github.event.issue.pull_request) + name: Run PR-Agent + steps: + - name: PR Agent action step + id: pragent + uses: Codium-ai/pr-agent@main + env: + OPENAI_KEY: ${{ secrets.OPENAI_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTION_CONFIG.AUTO_DESCRIBE: true + GITHUB_ACTION_CONFIG.AUTO_REVIEW: true + GITHUB_ACTION_CONFIG.AUTO_IMPROVE: true +""" + + try: + # Check if workflow already exists + workflow_path = '.github/workflows/pr-agent-automation.yml' + check_url = f'https://api.github.com/repos/{owner}/{repo}/contents/{workflow_path}' + check_response = requests.get(check_url, headers=headers, timeout=10) + + encoded_content = base64.b64encode(workflow_content.encode()).decode() + + if check_response.status_code == 200: + # File exists, update it + existing_data = check_response.json() + sha = existing_data.get('sha') + + update_data = { + 'message': 'Update PR-Agent automation workflow', + 'content': encoded_content, + 'sha': sha + } + + update_response = requests.put(check_url, headers=headers, json=update_data, timeout=10) + + if update_response.status_code in [200, 201]: + return jsonify({ + 'success': True, + 'message': 'Workflow updated successfully', + 'action': 'updated' + }), 200 + else: + return jsonify({ + 'error': f'Failed to update workflow: {update_response.text}', + 'success': False + }), 200 + + elif check_response.status_code == 404: + # File doesn't exist, create it + create_data = { + 'message': 'Add PR-Agent automation workflow', + 'content': encoded_content + } + + create_response = requests.put(check_url, headers=headers, json=create_data, timeout=10) + + if create_response.status_code in [200, 201]: + return jsonify({ + 'success': True, + 'message': 'Workflow created successfully', + 'action': 'created' + }), 200 + else: + return jsonify({ + 'error': f'Failed to create workflow: {create_response.text}', + 'success': False + }), 200 + + else: + return jsonify({ + 'error': f'Failed to check workflow existence: {check_response.text}', + 'success': False + }), 200 + + except Exception as e: + return jsonify({'error': str(e), 'success': False}), 200 + + +@app.route('/') +def index(): + return render_template('index.html') + + +@app.route('/api/repos') +def repos(): + cwd = Path.cwd() + root = find_repo_root(cwd) + repo = get_repo_info(root) + return jsonify([repo]) + + +@app.route('/api/repos/') +def repo_details(name): + cwd = Path.cwd() + root = find_repo_root(cwd) + repo = get_repo_info(root) + if repo['name'] != name: + return jsonify({'error': 'repo not found'}), 404 + return jsonify(repo) + + +@app.route('/api/repos//prs') +def repo_prs(name): + cwd = Path.cwd() + root = find_repo_root(cwd) + repo = get_repo_info(root) + if repo['name'] != name: + return jsonify({'error': 'repo not found'}), 404 + prs = get_open_prs(root) + return jsonify(prs) + + +def stream_subprocess(cmd_list, cwd=None): + process = subprocess.Popen(cmd_list, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + try: + for line in iter(process.stdout.readline, ''): + if line: + yield line + process.stdout.close() + rc = process.wait() + yield f"\nProcess exited with code {rc}\n" + except GeneratorExit: + try: + process.kill() + except Exception: + pass + + +@app.route('/api/run', methods=['POST']) +def run_action(): + data = request.get_json() or {} + action = data.get('action') + pr_url = data.get('pr_url') + repo = data.get('repo') + if action not in ('review', 'describe', 'improve'): + return jsonify({'error': 'invalid action'}), 400 + + cwd = find_repo_root(Path.cwd()) + # command: run CLI in pr_agent folder; pass repo if provided via --repo + cli_cwd = cwd / 'pr_agent' + cmd = ['python3', 'cli.py'] + if pr_url: + cmd.append(f'--pr_url={pr_url}') + cmd.append(action) + return Response(stream_subprocess(cmd, cwd=str(cli_cwd)), mimetype='text/plain') + + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/ui/requirements-ui.txt b/ui/requirements-ui.txt new file mode 100644 index 00000000..472178e4 --- /dev/null +++ b/ui/requirements-ui.txt @@ -0,0 +1,4 @@ +Flask>=2.0 +GitPython>=3.1 +requests>=2.0 +toml>=0.10 diff --git a/ui/templates/index.html b/ui/templates/index.html new file mode 100644 index 00000000..8ae24319 --- /dev/null +++ b/ui/templates/index.html @@ -0,0 +1,253 @@ + + + + + + PR-Agent — Repo UI + + + +
+
+

Repositories

+
+
+ +
+ + + + From acf5ee1f8798428db55fa9ea4d352d23e1212808 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Tue, 9 Dec 2025 00:30:15 +0530 Subject: [PATCH 2/2] fixes --- README_SIMPLE.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 README_SIMPLE.md diff --git a/README_SIMPLE.md b/README_SIMPLE.md new file mode 100644 index 00000000..42593772 --- /dev/null +++ b/README_SIMPLE.md @@ -0,0 +1,73 @@ +# PR-Agent — Quick Capabilities Summary + +**Overview:** +- PR-Agent is a CLI and integration tool that automates pull request (PR) review tasks using LLMs. It provides automated PR reviews, descriptions, code suggestions, documentation helpers, and related workflows for multiple Git providers. + +**Core Capabilities:** +- **/review (PR review):** Generate a reviewer-style audit of a PR including security, tests, effort, and best-practice suggestions. +- **/describe (PR description):** Produce a clear PR description and changelog updates from the diff and user notes. +- **/improve (Code suggestions):** Suggest code improvements, refactorings, and non-functional improvements (style, tests, docs). +- **/add_docs (Docs generation):** Create or update documentation for changed code or new APIs. +- **/custom_prompt:** Run a custom prompt against the PR context. +- **/ask (Interactive questions):** Ask follow-up questions about a PR and receive focused answers. +- **CLI:** `pr-agent` command-line tool to run review/describe/improve locally or in CI. +- **GitHub Actions & App:** Integrations for automated runs in workflows and as a GitHub App. + +**Integration / Providers:** +- **Git providers:** GitHub, GitLab, Gitea, Bitbucket, Bitbucket Server, Gerrit, local git. +- **LLM Providers via LiteLLM:** OpenAI-compatible endpoints (including Blackbox.ai via `OPENAI_API_BASE`), Anthropic, Cohere, HuggingFace, and others supported by LiteLLM. Configuration uses provider-prefixed model strings (e.g., `openai/gpt-4o`). +- **Secret providers:** AWS Secrets Manager and Google Cloud Storage secret provider are supported for secure secret management. + +**Configuration & Secrets:** +- Settings are managed with `dynaconf` and loaded from `pr_agent/settings/*` and optional repository-level `pyproject.toml` under `tool.pr-agent`. +- Local secret file: `pr_agent/settings/.secrets.toml` (DO NOT commit real tokens). The template is `pr_agent/settings/.secrets_template.toml`. +- Key settings: + - `openai.key` and `openai.api_base` for OpenAI-compatible endpoints. + - `config.model` controls default model (use provider-prefixed strings LiteLLM expects). + +**Extensibility:** +- The codebase provides modular AI handler layers (`algo/ai_handlers`) so new providers/adapters can be added. +- Token handling and patch generation are pluggable to tune prompt size and chunking. + +**Development / Quick Start:** +- Create a Python virtualenv and install: + + ```bash + python3 -m venv .venv + source .venv/bin/activate + pip install -e . + ``` + +- Provide secrets locally in `pr_agent/settings/.secrets.toml` or export env vars (preferred for CI): + + ```bash + export OPENAI_API_KEY="" + export OPENAI_API_BASE="https://api.blackbox.ai" # if using Blackbox + ``` + +- Example local run (review): + + ```bash + python cli.py --pr_url="https://github.com///pull/123" review + ``` + +**Security & Best Practices:** +- Never commit API keys or `.secrets.toml` to version control. +- If secrets are accidentally pushed, rotate and remove from history (BFG or git filter-repo) immediately. +- Use a secrets manager for CI and production deployments. + +**Where to get help / docs:** +- Project docs are under `docs/` and `pr_agent/settings/*.toml` contain configuration docs and prompts. +- For LiteLLM provider formats, see: https://docs.litellm.ai/docs/providers + +**Files of interest:** +- `pr_agent/cli.py` — CLI entrypoint. +- `pr_agent/config_loader.py` — dynaconf initialization and settings loading. +- `pr_agent/settings/` — configuration and prompt files. +- `pr_agent/algo/` — core logic: token handling, prompt preparation, AI handlers. +- `pr_agent/git_providers/` — adapters for Git hosting providers. + +If you want, I can: +- Add this content into the main `README.md` replacing or merging with the existing one. +- Generate a one-page `CONTRIBUTING` snippet for local development and secret handling. +- Commit the new README file and create a small PR with the change.