diff --git a/.github/scripts/sync_code_blocks.py b/.github/scripts/sync_code_blocks.py index 93befc54..9dfca3e2 100755 --- a/.github/scripts/sync_code_blocks.py +++ b/.github/scripts/sync_code_blocks.py @@ -3,11 +3,15 @@ Sync code blocks in documentation files with their corresponding source files. This script: -1. Scans MDX files for code blocks with file references (e.g., ```python expandable examples/01_standalone_sdk/02_custom_tools.py) +1. Scans MDX files for code blocks with file references (e.g., ```python expandable examples/01_standalone_sdk/02_custom_tools.py or ```yaml expandable examples/03_github_workflows/02_pr_review/workflow.yml) 2. Extracts the file path from the code block metadata 3. Reads the actual content from the source file in agent-sdk/ 4. Compares the code block content with the actual file content 5. Updates the documentation if there are differences + +Supported file types: +- Python files (.py) with ```python blocks +- YAML files (.yml, .yaml) with ```yaml blocks """ import os @@ -26,26 +30,46 @@ def find_mdx_files(docs_path: Path) -> list[Path]: return mdx_files -def extract_code_blocks(content: str) -> list[tuple[str, str, int, int]]: +def extract_code_blocks(content: str) -> list[tuple[str, str, str, int, int]]: """ Extract code blocks that reference source files. - Returns list of tuples: (file_reference, code_content, start_pos, end_pos) + Returns list of tuples: (language, file_reference, code_content, start_pos, end_pos) Pattern matches blocks like: ```python icon="python" expandable examples/01_standalone_sdk/02_custom_tools.py ``` + + OR + + ```yaml icon="yaml" expandable examples/03_github_workflows/02_pr_review/workflow.yml + + ``` """ - # Captures ...*.py after the first line, then the body up to ``` - pattern = r'```python[^\n]*\s+([^\s]+\.py)\n(.*?)```' - matches: list[tuple[str, str, int, int]] = [] - for match in re.finditer(pattern, content, re.DOTALL): + matches: list[tuple[str, str, str, int, int]] = [] + + # Pattern for Python files + python_pattern = r'```python[^\n]*\s+([^\s]+\.py)\n(.*?)```' + for match in re.finditer(python_pattern, content, re.DOTALL): + file_ref = match.group(1) + code_content = match.group(2) + start_pos = match.start() + end_pos = match.end() + matches.append(('python', file_ref, code_content, start_pos, end_pos)) + + # Pattern for YAML files + yaml_pattern = r'```yaml[^\n]*\s+([^\s]+\.ya?ml)\n(.*?)```' + for match in re.finditer(yaml_pattern, content, re.DOTALL): file_ref = match.group(1) code_content = match.group(2) start_pos = match.start() end_pos = match.end() - matches.append((file_ref, code_content, start_pos, end_pos)) + matches.append(('yaml', file_ref, code_content, start_pos, end_pos)) + + # Sort by position to maintain order + matches.sort(key=lambda x: x[3]) + return matches @@ -123,7 +147,7 @@ def resolve_paths() -> tuple[Path, Path]: def update_doc_file( doc_path: Path, content: str, - code_blocks: list[tuple[str, str, int, int]], + code_blocks: list[tuple[str, str, str, int, int]], agent_sdk_path: Path, ) -> bool: """ @@ -135,7 +159,7 @@ def update_doc_file( new_content = content offset = 0 # Track offset due to content changes - for file_ref, old_code, start_pos, end_pos in code_blocks: + for language, file_ref, old_code, start_pos, end_pos in code_blocks: actual_content = read_source_file(agent_sdk_path, file_ref) if actual_content is None: continue @@ -150,8 +174,9 @@ def update_doc_file( adj_start = start_pos + offset adj_end = end_pos + offset + # Match opening line with the appropriate language opening_line_match = re.search( - r"```python[^\n]*\s+" + re.escape(file_ref), + r"```" + re.escape(language) + r"[^\n]*\s+" + re.escape(file_ref), new_content[adj_start:adj_end], ) if opening_line_match: diff --git a/sdk/guides/github-workflows/pr-review.mdx b/sdk/guides/github-workflows/pr-review.mdx index a91c1672..d51ffb60 100644 --- a/sdk/guides/github-workflows/pr-review.mdx +++ b/sdk/guides/github-workflows/pr-review.mdx @@ -11,8 +11,125 @@ Automatically review pull requests, providing feedback on code quality, security - Requesting `openhands-agent` as a reviewer - Adding the `review-this` label to the PR -```yaml icon="yaml" expandable agent-sdk/examples/03_github_workflows/01_basic_action/workflow.yml + +The reference workflow triggers on either the "review-this" label or when the openhands-agent account is requested as a reviewer. In OpenHands organization repositories, openhands-agent has access, so this works as-is. In your own repositories, requesting openhands-agent will only work if that account is added as a collaborator or is part of a team with access. If you don't plan to grant access, use the label trigger instead, or change the condition to a reviewer handle that exists in your repo. + + +```yaml icon="yaml" expandable examples/03_github_workflows/02_pr_review/workflow.yml +--- +# To set this up: +# 1. Copy this file to .github/workflows/pr-review.yml in your repository +# 2. Add your LLM_API_KEY to the repository secrets +# 3. Commit this file to your repository +# 4. Trigger the review by either: +# - Adding the "review-this" label to any PR, OR +# - Requesting openhands-agent as a reviewer +name: PR Review by OpenHands + +on: + # Trigger when a label is added or a reviewer is requested + pull_request: + types: [labeled, review_requested] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + pr-review: + # Run when review-this label is added OR openhands-agent is requested as reviewer + if: | + github.event.label.name == 'review-this' || + github.event.requested_reviewer.login == 'openhands-agent' + runs-on: ubuntu-latest + env: + # Configuration (modify these values as needed) + LLM_MODEL: + LLM_BASE_URL: + # PR context will be automatically provided by the agent script + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + PR_BASE_BRANCH: ${{ github.event.pull_request.base.ref }} + PR_HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} + REPO_NAME: ${{ github.repository }} + steps: + - name: Checkout agent-sdk repository + uses: actions/checkout@v4 + with: + repository: OpenHands/agent-sdk + path: agent-sdk + + - name: Checkout PR repository + uses: actions/checkout@v4 + with: + # Fetch the full history to get the diff + fetch-depth: 0 + path: pr-repo + # Check out the feature branch so agent can inspect the PR changes + ref: ${{ github.event.pull_request.head.ref }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install GitHub CLI + run: | + # Install GitHub CLI for posting review comments + sudo apt-get update + sudo apt-get install -y gh + + - name: Install OpenHands dependencies + run: | + # Install OpenHands SDK and tools from git repository + uv pip install --system "openhands-sdk @ git+https://github.com/OpenHands/agent-sdk.git@main#subdirectory=openhands-sdk" + uv pip install --system "openhands-tools @ git+https://github.com/OpenHands/agent-sdk.git@main#subdirectory=openhands-tools" + + - name: Check required configuration + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + run: | + if [ -z "$LLM_API_KEY" ]; then + echo "Error: LLM_API_KEY secret is not set." + exit 1 + fi + + echo "PR Number: $PR_NUMBER" + echo "PR Title: $PR_TITLE" + echo "Repository: $REPO_NAME" + echo "LLM model: $LLM_MODEL" + if [ -n "$LLM_BASE_URL" ]; then + echo "LLM base URL: $LLM_BASE_URL" + fi + + - name: Run PR review + env: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Change to the PR repository directory so agent can analyze the code + cd pr-repo + + # Run the PR review script from the agent-sdk checkout + uv run python ../agent-sdk/examples/03_github_workflows/02_pr_review/agent_script.py + + - name: Upload logs as artifact + uses: actions/upload-artifact@v4 + if: always() + with: + name: openhands-pr-review-logs + path: | + *.log + output/ + retention-days: 7 ``` ## Quick Start