Compare commits

...

13 Commits

Author SHA1 Message Date
9773afe155 docs: minor fixes in the compliance.md file 2025-07-18 14:31:16 +03:00
Tal
0a8a263809 Merge pull request #1934 from abhinav-1305/fix-tag
docs: update Google Tag Manager ID in custom analytics integration
2025-07-18 09:27:28 +03:00
Tal
597f553dd5 Merge pull request #1933 from abhinav-1305/config-docs-update
docs: add detailed configuration examples for GitHub Actions models
2025-07-18 09:26:45 +03:00
Tal
4b6fcfe60e Merge pull request #1944 from qodo-ai/tr/describe_Redesign
Tr/describe redesign
2025-07-18 09:23:41 +03:00
Tal
7cc4206b70 Update pr_agent/tools/pr_description.py
Co-authored-by: qodo-merge-for-open-source[bot] <189517486+qodo-merge-for-open-source[bot]@users.noreply.github.com>
2025-07-18 09:20:41 +03:00
8906a81a2e fix(docs): fix table row structure in describe tool documentation 2025-07-18 09:17:14 +03:00
6179eeca58 fix(pr_description): fix template syntax errors and improve formatting in prompts 2025-07-18 09:14:52 +03:00
e8c73e7baa refactor(utils): improve file walkthrough parsing with regex and better error handling 2025-07-18 08:54:52 +03:00
754d47f187 refactor(pr_description): redesign changes walkthrough section and improve file processing 2025-07-18 08:51:48 +03:00
c4dd07b3b8 squashing revert commits 2025-07-13 23:11:22 +05:30
3aaa727e05 docs: update fallback model configuration and clarify self-hosted runner requirements 2025-07-13 00:20:03 +05:30
6108f96bff docs: update installation and usage guide to remove OPENAI_KEY for non-OpenAI models 2025-07-13 00:18:47 +05:30
5a00897cbe docs: add detailed configuration examples for GitHub Actions models 2025-07-13 00:10:36 +05:30
11 changed files with 601 additions and 106 deletions

View File

@ -51,6 +51,430 @@ When you open your next PR, you should see a comment from `github-actions` bot w
See detailed usage instructions in the [USAGE GUIDE](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-action)
## Configuration Examples
This section provides detailed, step-by-step examples for configuring PR-Agent with different models and advanced options in GitHub Actions.
### Quick Start Examples
#### Basic Setup (OpenAI Default)
Copy this minimal workflow to get started with the default OpenAI models:
```yaml
name: PR Agent
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
steps:
- name: PR Agent action step
uses: qodo-ai/pr-agent@main
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
#### Gemini Setup
Ready-to-use workflow for Gemini models:
```yaml
name: PR Agent (Gemini)
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
steps:
- name: PR Agent action step
uses: qodo-ai/pr-agent@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
config.model: "gemini/gemini-1.5-flash"
config.fallback_models: '["gemini/gemini-1.5-flash"]'
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
#### Claude Setup
Ready-to-use workflow for Claude models:
```yaml
name: PR Agent (Claude)
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
steps:
- name: PR Agent action step
uses: qodo-ai/pr-agent@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
config.model: "anthropic/claude-3-opus-20240229"
config.fallback_models: '["anthropic/claude-3-haiku-20240307"]'
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
### Basic Configuration with Tool Controls
Start with this enhanced workflow that includes tool configuration:
```yaml
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: qodo-ai/pr-agent@main
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Enable/disable automatic tools
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
# Configure which PR events trigger the action
github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested"]'
```
### Switching Models
#### Using Gemini (Google AI Studio)
To use Gemini models instead of the default OpenAI models:
```yaml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Set the model to Gemini
config.model: "gemini/gemini-1.5-flash"
config.fallback_models: '["gemini/gemini-1.5-flash"]'
# Add your Gemini API key
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
# Tool configuration
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
**Required Secrets:**
- Add `GEMINI_API_KEY` to your repository secrets (get it from [Google AI Studio](https://aistudio.google.com/))
**Note:** When using non-OpenAI models like Gemini, you don't need to set `OPENAI_KEY` - only the model-specific API key is required.
#### Using Claude (Anthropic)
To use Claude models:
```yaml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Set the model to Claude
config.model: "anthropic/claude-3-opus-20240229"
config.fallback_models: '["anthropic/claude-3-haiku-20240307"]'
# Add your Anthropic API key
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
# Tool configuration
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
**Required Secrets:**
- Add `ANTHROPIC_KEY` to your repository secrets (get it from [Anthropic Console](https://console.anthropic.com/))
**Note:** When using non-OpenAI models like Claude, you don't need to set `OPENAI_KEY` - only the model-specific API key is required.
#### Using Azure OpenAI
To use Azure OpenAI services:
```yaml
env:
OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Azure OpenAI configuration
OPENAI.API_TYPE: "azure"
OPENAI.API_VERSION: "2023-05-15"
OPENAI.API_BASE: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
OPENAI.DEPLOYMENT_ID: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
# Set the model to match your Azure deployment
config.model: "gpt-4o"
config.fallback_models: '["gpt-4o"]'
# Tool configuration
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
**Required Secrets:**
- `AZURE_OPENAI_KEY`: Your Azure OpenAI API key
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint URL
- `AZURE_OPENAI_DEPLOYMENT`: Your deployment name
#### Using Local Models (Ollama)
To use local models via Ollama:
```yaml
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Set the model to a local Ollama model
config.model: "ollama/qwen2.5-coder:32b"
config.fallback_models: '["ollama/qwen2.5-coder:32b"]'
config.custom_model_max_tokens: "128000"
# Ollama configuration
OLLAMA.API_BASE: "http://localhost:11434"
# Tool configuration
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
**Note:** For local models, you'll need to use a self-hosted runner with Ollama installed, as GitHub Actions hosted runners cannot access localhost services.
### Advanced Configuration Options
#### Custom Review Instructions
Add specific instructions for the review process:
```yaml
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Custom review instructions
pr_reviewer.extra_instructions: "Focus on security vulnerabilities and performance issues. Check for proper error handling."
# Tool configuration
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
#### Language-Specific Configuration
Configure for specific programming languages:
```yaml
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Language-specific settings
pr_reviewer.extra_instructions: "Focus on Python best practices, type hints, and docstrings."
pr_code_suggestions.num_code_suggestions: "8"
pr_code_suggestions.suggestions_score_threshold: "7"
# Tool configuration
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
#### Selective Tool Execution
Run only specific tools automatically:
```yaml
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Only run review and describe, skip improve
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "false"
# Only trigger on PR open and reopen
github_action_config.pr_actions: '["opened", "reopened"]'
```
### Using Configuration Files
Instead of setting all options via environment variables, you can use a `.pr_agent.toml` file in your repository root:
1. Create a `.pr_agent.toml` file in your repository root:
```toml
[config]
model = "gemini/gemini-1.5-flash"
fallback_models = ["anthropic/claude-3-opus-20240229"]
[pr_reviewer]
extra_instructions = "Focus on security issues and code quality."
[pr_code_suggestions]
num_code_suggestions = 6
suggestions_score_threshold = 7
```
2. Use a simpler workflow file:
```yaml
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: qodo-ai/pr-agent@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
### Troubleshooting Common Issues
#### Model Not Found Errors
If you get model not found errors:
1. **Check model name format**: Ensure you're using the correct model identifier format (e.g., `gemini/gemini-1.5-flash`, not just `gemini-1.5-flash`)
2. **Verify API keys**: Make sure your API keys are correctly set as repository secrets
3. **Check model availability**: Some models may not be available in all regions or may require specific access
#### Environment Variable Format
Remember these key points about environment variables:
- Use dots (`.`) or double underscores (`__`) to separate sections and keys
- Boolean values should be strings: `"true"` or `"false"`
- Arrays should be JSON strings: `'["item1", "item2"]'`
- Model names are case-sensitive
#### Rate Limiting
If you encounter rate limiting:
```yaml
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Add fallback models for better reliability
config.fallback_models: '["gpt-4o", "gpt-3.5-turbo"]'
# Increase timeout for slower models
config.ai_timeout: "300"
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
#### Common Error Messages and Solutions
**Error: "Model not found"**
- **Solution**: Check the model name format and ensure it matches the exact identifier. See the [Changing a model in PR-Agent](../usage-guide/changing_a_model.md) guide for supported models and their correct identifiers.
**Error: "API key not found"**
- **Solution**: Verify that your API key is correctly set as a repository secret and the environment variable name matches exactly
- **Note**: For non-OpenAI models (Gemini, Claude, etc.), you only need the model-specific API key, not `OPENAI_KEY`
**Error: "Rate limit exceeded"**
- **Solution**: Add fallback models or increase the `config.ai_timeout` value
**Error: "Permission denied"**
- **Solution**: Ensure your workflow has the correct permissions set:
```yaml
permissions:
issues: write
pull-requests: write
contents: write
```
**Error: "Invalid JSON format"**
- **Solution**: Check that arrays are properly formatted as JSON strings:
```yaml
# Correct
config.fallback_models: '["model1", "model2"]'
# Incorrect (interpreted as a YAML list, not a string)
config.fallback_models: ["model1", "model2"]
```
#### Debugging Tips
1. **Enable verbose logging**: Add `config.verbosity_level: "2"` to see detailed logs
2. **Check GitHub Actions logs**: Look at the step output for specific error messages
3. **Test with minimal configuration**: Start with just the basic setup and add options one by one
4. **Verify secrets**: Double-check that all required secrets are set in your repository settings
#### Performance Optimization
For better performance with large repositories:
```yaml
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Optimize for large PRs
config.large_patch_policy: "clip"
config.max_model_tokens: "32000"
config.patch_extra_lines_before: "3"
config.patch_extra_lines_after: "1"
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
```
### Reference
For more detailed configuration options, see:
- [Changing a model in PR-Agent](../usage-guide/changing_a_model.md)
- [Configuration options](../usage-guide/configuration_options.md)
- [Automations and usage](../usage-guide/automations_and_usage.md#github-action)
### Using a specific release
!!! tip ""
@ -296,4 +720,4 @@ After you set up AWS CodeCommit using the instructions above, here is an example
PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
--pr_url https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/MY_REPO_NAME/pull-requests/321 \
review
```
```

View File

@ -2,7 +2,7 @@
## Overview
The `compliance` tool performs comprehensive compliance checks on PR code changes, validating them against security standards, ticket requirements, and custom organizational compliance checklists, thereby helping teams maintain consistent code quality and security practices while ensuring that development work aligns with business requirements.
The `compliance` tool performs comprehensive compliance checks on PR code changes, validating them against security standards, ticket requirements, and custom organizational compliance checklists, thereby helping teams, enterprises, and agents maintain consistent code quality and security practices while ensuring that development work aligns with business requirements.
=== "Fully Compliant"
![compliance_overview](https://codium.ai/images/pr_agent/compliance_full.png){width=256}
@ -67,7 +67,7 @@ Scans for security vulnerabilities and potential exploits in the PR code changes
- **Verified Security Concerns** 🔴: Clear security vulnerabilities that require immediate attention
- **Possible Security Risks** ⚪: Potential security issues that need human verification
- **No Security Concerns** 🟢: No security vulnerabilities were detected
- **No Security Concerns** 🟢: No security vulnerabilities detected
Examples of security issues:
@ -103,7 +103,7 @@ Validates against an organization-specific compliance checklist:
### Setting Up Custom Compliance
Each compliance is defined in a YAML file, as follows:
Each compliance is defined in a YAML file as follows:
- `title`: Used to provide a clear name for the compliance
- `compliance_label`: Used to automatically generate labels for non-compliance issues
- `objective`, `success_criteria`, and `failure_criteria`: These fields are used to clearly define what constitutes compliance
@ -122,7 +122,7 @@ Each compliance is defined in a YAML file, as follows:
...
```
???+ tip "Writing effective compliance checklist"
???+ tip "Writing effective compliance checklists"
- Avoid overly complex or subjective compliances that are hard to verify
- Keep compliances focused on security, business requirements, and critical standards
- Use clear, actionable language that developers can understand
@ -135,7 +135,7 @@ Qodo Merge supports hierarchical compliance checklists using a dedicated global
#### Setting up global hierarchical compliance
1\. Create a new repository named `pr-agent-settings` in your organization/workspace.
1\. Create a new repository named `pr-agent-settings` in your organization or workspace.
2\. Build the folder hierarchy in your `pr-agent-settings` repository:
@ -153,14 +153,14 @@ pr-agent-settings/
├── qodo-merge/ # For standalone repositories
│ └── pr_compliance_checklist.yaml
└── qodo-monorepo/ # For monorepo-specific compliance
├── pr_compliance_checklist.yaml # Root level monorepo compliance
├── pr_compliance_checklist.yaml # Root-level monorepo compliance
├── qodo-github/ # Subproject compliance
│ └── pr_compliance_checklist.yaml
└── qodo-gitlab/ # Another subproject
└── pr_compliance_checklist.yaml
```
3\. Define the metadata file `metadata.yaml` in the `pr-agent-settings` root:
3\. Define the metadata file `metadata.yaml` in the root of `pr-agent-settings`:
```yaml
# Standalone repos
@ -225,7 +225,7 @@ enable_global_pr_compliance = true
<table>
<tr>
<td><b>extra_instructions</b></td>
<td>Optional extra instructions for the tool. For example: "focus on the changes in the file X. Ignore changes in ...". Default is empty string.</td>
<td>Optional extra instructions for the tool. For example: "Ensure that all error-handling paths in the code contain appropriate logging statements". Default is empty string.</td>
</tr>
<tr>
<td><b>persistent_comment</b></td>
@ -233,11 +233,11 @@ enable_global_pr_compliance = true
</tr>
<tr>
<td><b>enable_user_defined_compliance_labels</b></td>
<td>If set to true, the tool will add labels for custom compliance violations. Default is true.</td>
<td>If set to true, the tool will add the label `Failed compliance check` for custom compliance violations. Default is true.</td>
</tr>
<tr>
<td><b>enable_estimate_effort_to_review</b></td>
<td>If set to true, the tool will estimate the effort required to review the PR (1-5 scale). Default is true.</td>
<td>If set to true, the tool will estimate the effort required to review the PR (1-5 scale) as a label. Default is true.</td>
</tr>
<tr>
<td><b>enable_todo_scan</b></td>
@ -245,11 +245,11 @@ enable_global_pr_compliance = true
</tr>
<tr>
<td><b>enable_update_pr_compliance_checkbox</b></td>
<td>If set to true, the tool will add an update checkbox to refresh compliance status. Default is true.</td>
<td>If set to true, the tool will add an update checkbox to refresh compliance status following push events. Default is true.</td>
</tr>
<tr>
<td><b>enable_help_text</b></td>
<td>If set to true, the tool will display a help text in the comment. Default is false.</td>
<td>If set to true, the tool will display help text in the comment. Default is false.</td>
</tr>
</table>
@ -262,7 +262,7 @@ enable_global_pr_compliance = true
</tr>
<tr>
<td><b>enable_compliance_labels_security</b></td>
<td>If set to true, the tool will add security-related labels to the PR. Default is true.</td>
<td>If set to true, the tool will add a `Possible security concern` label to the PR when security-related concerns are detected. Default is true.</td>
</tr>
</table>

View File

@ -124,6 +124,10 @@ This option is enabled by default via the `pr_description.enable_pr_diagram` par
<td><b>enable_semantic_files_types</b></td>
<td>If set to true, "Changes walkthrough" section will be generated. Default is true.</td>
</tr>
<tr>
<td><b>file_table_collapsible_open_by_default</b></td>
<td>If set to true, the file list in the "Changes walkthrough" section will be open by default. If set to false, it will be closed by default. Default is false.</td>
</tr>
<tr>
<td><b>collapsible_file_list</b></td>
<td>If set to true, the file list in the "Changes walkthrough" section will be collapsible. If set to "adaptive", the file list will be collapsible only if there are more than 8 files. Default is "adaptive".</td>

View File

@ -202,6 +202,25 @@ publish_labels = false
to prevent Qodo Merge from publishing labels when running the `describe` tool.
#### Quick Reference: Model Configuration in GitHub Actions
For detailed step-by-step examples of configuring different models (Gemini, Claude, Azure OpenAI, etc.) in GitHub Actions, see the [Configuration Examples](../installation/github.md#configuration-examples) section in the installation guide.
**Common Model Configuration Patterns:**
- **OpenAI**: Set `config.model: "gpt-4o"` and `OPENAI_KEY`
- **Gemini**: Set `config.model: "gemini/gemini-1.5-flash"` and `GOOGLE_AI_STUDIO.GEMINI_API_KEY` (no `OPENAI_KEY` needed)
- **Claude**: Set `config.model: "anthropic/claude-3-opus-20240229"` and `ANTHROPIC.KEY` (no `OPENAI_KEY` needed)
- **Azure OpenAI**: Set `OPENAI.API_TYPE: "azure"`, `OPENAI.API_BASE`, and `OPENAI.DEPLOYMENT_ID`
- **Local Models**: Set `config.model: "ollama/model-name"` and `OLLAMA.API_BASE`
**Environment Variable Format:**
- Use dots (`.`) to separate sections and keys: `config.model`, `pr_reviewer.extra_instructions`
- Boolean values as strings: `"true"` or `"false"`
- Arrays as JSON strings: `'["item1", "item2"]'`
For complete model configuration details, see [Changing a model in PR-Agent](changing_a_model.md).
### GitLab Webhook
After setting up a GitLab webhook, to control which commands will run automatically when a new MR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:

View File

@ -3,5 +3,5 @@
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-M6PJSFV');</script>
})(window,document,'script','dataLayer','GTM-5C9KZBM3');</script>
<!-- End Google Tag Manager -->

View File

@ -70,7 +70,8 @@ class ReasoningEffort(str, Enum):
class PRDescriptionHeader(str, Enum):
CHANGES_WALKTHROUGH = "### **Changes walkthrough** 📝"
DIAGRAM_WALKTHROUGH = "Diagram Walkthrough"
FILE_WALKTHROUGH = "File Walkthrough"
def get_setting(key: str) -> Any:
@ -1284,14 +1285,35 @@ def process_description(description_full: str) -> Tuple[str, List]:
if not description_full:
return "", []
description_split = description_full.split(PRDescriptionHeader.CHANGES_WALKTHROUGH.value)
base_description_str = description_split[0]
changes_walkthrough_str = ""
files = []
if len(description_split) > 1:
changes_walkthrough_str = description_split[1]
# description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value)
if PRDescriptionHeader.FILE_WALKTHROUGH.value in description_full:
try:
# FILE_WALKTHROUGH are presented in a collapsible section in the description
regex_pattern = r'<details.*?>\s*<summary>\s*<h3>\s*' + re.escape(PRDescriptionHeader.FILE_WALKTHROUGH.value) + r'\s*</h3>\s*</summary>'
description_split = re.split(regex_pattern, description_full, maxsplit=1, flags=re.DOTALL)
# If the regex pattern is not found, fallback to the previous method
if len(description_split) == 1:
get_logger().debug("Could not find regex pattern for file walkthrough, falling back to simple split")
description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value, 1)
except Exception as e:
get_logger().warning(f"Failed to split description using regex, falling back to simple split: {e}")
description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value, 1)
if len(description_split) < 2:
get_logger().error("Failed to split description into base and changes walkthrough", artifact={'description': description_full})
return description_full.strip(), []
base_description_str = description_split[0].strip()
changes_walkthrough_str = ""
files = []
if len(description_split) > 1:
changes_walkthrough_str = description_split[1]
else:
get_logger().debug("No changes walkthrough found")
else:
get_logger().debug("No changes walkthrough found")
base_description_str = description_full.strip()
return base_description_str, []
try:
if changes_walkthrough_str:
@ -1314,18 +1336,20 @@ def process_description(description_full: str) -> Tuple[str, List]:
try:
if isinstance(file_data, tuple):
file_data = file_data[0]
pattern = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*<li>(.*?)</details>'
pattern = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*(?:<li>|•)(.*?)</details>'
res = re.search(pattern, file_data, re.DOTALL)
if not res or res.lastindex != 4:
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong><dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\n\n\s*(.*?)</details>'
res = re.search(pattern_back, file_data, re.DOTALL)
if not res or res.lastindex != 4:
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*-\s*(.*?)\s*</details>' # looking for hyphen ('- ')
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*-\s*(.*?)\s*</details>' # looking for hypen ('- ')
res = re.search(pattern_back, file_data, re.DOTALL)
if res and res.lastindex == 4:
short_filename = res.group(1).strip()
short_summary = res.group(2).strip()
long_filename = res.group(3).strip()
if long_filename.endswith('<ul>'):
long_filename = long_filename[:-4].strip()
long_summary = res.group(4).strip()
long_summary = long_summary.replace('<br> *', '\n*').replace('<br>','').replace('\n','<br>')
long_summary = h.handle(long_summary).strip()
@ -1344,7 +1368,7 @@ def process_description(description_full: str) -> Tuple[str, List]:
if '<code>...</code>' in file_data:
pass # PR with many files. some did not get analyzed
else:
get_logger().error(f"Failed to parse description", artifact={'description': file_data})
get_logger().warning(f"Failed to parse description", artifact={'description': file_data})
except Exception as e:
get_logger().exception(f"Failed to process description: {e}", artifact={'description': file_data})

View File

@ -380,7 +380,7 @@ class AzureDevopsProvider(GitProvider):
pr_body = pr_body[:ind]
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
changes_walkthrough_text = PRDescriptionHeader.CHANGES_WALKTHROUGH.value
changes_walkthrough_text = PRDescriptionHeader.FILE_WALKTHROUGH.value
ind = pr_body.find(changes_walkthrough_text)
if ind != -1:
pr_body = pr_body[:ind]

View File

@ -106,7 +106,7 @@ extra_instructions = ""
enable_pr_type=true
final_update_message = true
enable_help_text=false
enable_help_comment=true
enable_help_comment=false
enable_pr_diagram=true # adds a section with a diagram of the PR changes
# describe as comment
publish_description_as_comment=false

View File

@ -48,8 +48,8 @@ class PRDescription(BaseModel):
description: str = Field(description="summarize the PR changes in up to four bullet points, each up to 8 words. For large PRs, add sub-bullets if needed. Order bullets by importance, with each bullet highlighting a key change group.")
title: str = Field(description="a concise and descriptive title that captures the PR's main theme")
{%- if enable_pr_diagram %}
changes_diagram: str = Field(description="a horizontal diagram that represents the main PR changes, in the format of a valid mermaid LR flowchart. The diagram should be concise and easy to read. Leave empty if no diagram is relevant. To create robust Mermaid diagrams, follow this two-step process: (1) Declare the nodes: nodeID[\"node description\"]. (2) Then define the links: nodeID1 -- \"link text\" --> nodeID2. Node description must always be surrounded with quotation marks.")
{%- endif %}
changes_diagram: str = Field(description='a horizontal diagram that represents the main PR changes, in the format of a valid mermaid LR flowchart. The diagram should be concise and easy to read. Leave empty if no diagram is relevant. To create robust Mermaid diagrams, follow this two-step process: (1) Declare the nodes: nodeID["node description"]. (2) Then define the links: nodeID1 -- "link text" --> nodeID2. Node description must always be surrounded with double quotation marks')
'{%- endif %}
{%- if enable_semantic_files_types %}
pr_files: List[FileDescription] = Field(max_items=20, description="a list of all the files that were changed in the PR, and summary of their changes. Each file must be analyzed regardless of change size.")
{%- endif %}
@ -67,11 +67,11 @@ description: |
title: |
...
{%- if enable_pr_diagram %}
changes_diagram: |
```mermaid
flowchart LR
...
```
changes_diagram: |
```mermaid
flowchart LR
...
```
{%- endif %}
{%- if enable_semantic_files_types %}
pr_files:
@ -155,11 +155,11 @@ description: |
title: |
...
{%- if enable_pr_diagram %}
changes_diagram: |
```mermaid
flowchart LR
...
```
changes_diagram: |
```mermaid
flowchart LR
...
```
{%- endif %}
{%- if enable_semantic_files_types %}
pr_files:

View File

@ -128,7 +128,7 @@ class PRDescription:
pr_title, pr_body, changes_walkthrough, pr_file_changes = self._prepare_pr_answer()
if not self.git_provider.is_supported(
"publish_file_comments") or not get_settings().pr_description.inline_file_summary:
pr_body += "\n\n" + changes_walkthrough
pr_body += "\n\n" + changes_walkthrough + "___\n\n"
get_logger().debug("PR output", artifact={"title": pr_title, "body": pr_body})
# Add help text if gfm_markdown is supported
@ -331,7 +331,8 @@ class PRDescription:
else:
original_prediction_dict = original_prediction_loaded
if original_prediction_dict:
filenames_predicted = [file.get('filename', '').strip() for file in original_prediction_dict.get('pr_files', [])]
files = original_prediction_dict.get('pr_files', [])
filenames_predicted = [file.get('filename', '').strip() for file in files if isinstance(file, dict)]
else:
filenames_predicted = []
@ -555,15 +556,11 @@ class PRDescription:
"""
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
markdown_text = ""
# Don't display 'PR Labels'
if 'labels' in self.data and self.git_provider.is_supported("get_labels"):
self.data.pop('labels')
if not get_settings().pr_description.enable_pr_type:
self.data.pop('type')
for key, value in self.data.items():
markdown_text += f"## **{key}**\n\n"
markdown_text += f"{value}\n\n"
# Remove the 'PR Title' key from the dictionary
ai_title = self.data.pop('title', self.vars["title"])
@ -579,6 +576,10 @@ class PRDescription:
pr_body, changes_walkthrough = "", ""
pr_file_changes = []
for idx, (key, value) in enumerate(self.data.items()):
if key == 'changes_diagram':
pr_body += f"### {PRDescriptionHeader.DIAGRAM_WALKTHROUGH.value}\n\n"
pr_body += f"{value}\n\n"
continue
if key == 'pr_files':
value = self.file_label_dict
else:
@ -597,9 +598,15 @@ class PRDescription:
pr_body += f'- `{filename}`: {description}\n'
if self.git_provider.is_supported("gfm_markdown"):
pr_body += "</details>\n"
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types:
changes_walkthrough, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
changes_walkthrough = f"{PRDescriptionHeader.CHANGES_WALKTHROUGH.value}\n{changes_walkthrough}"
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types: # 'File Walkthrough' section
changes_walkthrough_table, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
if get_settings().pr_description.get('file_table_collapsible_open_by_default', False):
initial_status = " open"
else:
initial_status = ""
changes_walkthrough = f"<details{initial_status}> <summary><h3> {PRDescriptionHeader.FILE_WALKTHROUGH.value}</h3></summary>\n\n"
changes_walkthrough += f"{changes_walkthrough_table}\n\n"
changes_walkthrough += "</details>\n\n"
elif key.lower().strip() == 'description':
if isinstance(value, list):
value = ', '.join(v.rstrip() for v in value)
@ -633,14 +640,19 @@ class PRDescription:
artifact={"file": file})
continue
filename = file['filename'].replace("'", "`").replace('"', '`')
changes_summary = file.get('changes_summary', "").strip()
changes_summary = file.get('changes_summary', "")
if not changes_summary:
get_logger().warning(f"Empty changes summary in file label dict, skipping file",
artifact={"file": file})
continue
changes_summary = changes_summary.strip()
changes_title = file['changes_title'].strip()
label = file.get('label').strip().lower()
if label not in file_label_dict:
file_label_dict[label] = []
file_label_dict[label].append((filename, changes_title, changes_summary))
except Exception as e:
get_logger().error(f"Error preparing file label dict {self.pr_id}: {e}")
get_logger().exception(f"Error preparing file label dict {self.pr_id}")
pass
return file_label_dict
@ -720,7 +732,7 @@ class PRDescription:
pr_body += """</tr></tbody></table>"""
except Exception as e:
get_logger().error(f"Error processing PR files to markdown {self.pr_id}: {str(e)}")
get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {str(e)}")
pass
return pr_body, pr_comments
@ -776,14 +788,21 @@ def insert_br_after_x_chars(text: str, x=70):
if count_chars_without_html(text) < x:
return text
is_list = text.lstrip().startswith(("- ", "* "))
# replace odd instances of ` with <code> and even instances of ` with </code>
text = replace_code_tags(text)
# convert list items to <li>
if text.startswith("- ") or text.startswith("* "):
text = "<li>" + text[2:]
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
text = text.replace("\n* ", '<br><li> ').replace("\n * ", '<br><li> ')
# convert list items to <li> only if the text is identified as a list
if is_list:
# To handle lists that start with indentation
leading_whitespace = text[:len(text) - len(text.lstrip())]
body = text.lstrip()
body = "<li>" + body[2:]
text = leading_whitespace + body
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
text = text.replace("\n* ", '<br><li> ').replace("\n * ", '<br><li> ')
# convert new lines to <br>
text = text.replace("\n", '<br>')
@ -823,7 +842,13 @@ def insert_br_after_x_chars(text: str, x=70):
is_inside_code = True
if "</code>" in word:
is_inside_code = False
return ''.join(new_text).strip()
processed_text = ''.join(new_text).strip()
if is_list:
processed_text = f"<ul>{processed_text}</ul>"
return processed_text
def replace_code_tags(text):

View File

@ -51,7 +51,7 @@ class TestConvertToMarkdown:
input_data = {'review': {
'estimated_effort_to_review_[1-5]': '1, because the changes are minimal and straightforward, focusing on a single functionality addition.\n',
'relevant_tests': 'No\n', 'possible_issues': 'No\n', 'security_concerns': 'No\n'}}
expected_output = textwrap.dedent(f"""\
{PRReviewHeader.REGULAR.value} 🔍
@ -67,12 +67,12 @@ class TestConvertToMarkdown:
""")
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
def test_simple_dictionary_input_without_gfm_supported(self):
input_data = {'review': {
'estimated_effort_to_review_[1-5]': '1, because the changes are minimal and straightforward, focusing on a single functionality addition.\n',
'relevant_tests': 'No\n', 'possible_issues': 'No\n', 'security_concerns': 'No\n'}}
expected_output = textwrap.dedent("""\
## PR Reviewer Guide 🔍
@ -89,74 +89,74 @@ class TestConvertToMarkdown:
""")
assert convert_to_markdown_v2(input_data, gfm_supported=False).strip() == expected_output.strip()
def test_key_issues_to_review(self):
input_data = {'review': {
'key_issues_to_review': [
{
'relevant_file' : 'src/utils.py',
'issue_header' : 'Code Smell',
'issue_content' : 'The function is too long and complex.',
'relevant_file': 'src/utils.py',
'issue_header': 'Code Smell',
'issue_content': 'The function is too long and complex.',
'start_line': 30,
'end_line': 50,
}
]
}}
mock_git_provider = Mock()
reference_link = 'https://github.com/qodo/pr-agent/pull/1/files#diff-hashvalue-R174'
reference_link = 'https://github.com/qodo/pr-agent/pull/1/files#diff-hashvalue-R174'
mock_git_provider.get_line_link.return_value = reference_link
expected_output = textwrap.dedent(f"""\
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br>
<a href='{reference_link}'><strong>Code Smell</strong></a><br>The function is too long and complex.
</td></tr>
</table>
""")
assert convert_to_markdown_v2(input_data, git_provider=mock_git_provider).strip() == expected_output.strip()
mock_git_provider.get_line_link.assert_called_with('src/utils.py', 30, 50)
def test_ticket_compliance(self):
input_data = {'review': {
'ticket_compliance_check': [
{
'ticket_url': 'https://example.com/ticket/123',
'ticket_requirements': '- Requirement 1\n- Requirement 2\n',
'fully_compliant_requirements': '- Requirement 1\n- Requirement 2\n',
'ticket_requirements': '- Requirement 1\n- Requirement 2\n',
'fully_compliant_requirements': '- Requirement 1\n- Requirement 2\n',
'not_compliant_requirements': '',
'requires_further_human_verification': '',
}
]
}}
expected_output = textwrap.dedent("""\
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>
**🎫 Ticket compliance analysis ✅**
**[123](https://example.com/ticket/123) - Fully compliant**
Compliant requirements:
- Requirement 1
- Requirement 2
</td></tr>
</table>
""")
@ -179,43 +179,43 @@ class TestConvertToMarkdown:
],
'title': 'Bug Fix',
}
]
}
]
}
}
expected_output = textwrap.dedent("""\
## PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
<table>
<tr><td>🔀 <strong>Multiple PR themes</strong><br><br>
<details><summary>
Sub-PR theme: <b>Refactoring</b></summary>
___
Relevant files:
- src/file1.py
- src/file2.py
___
</details>
<details><summary>
Sub-PR theme: <b>Bug Fix</b></summary>
___
Relevant files:
- src/file3.py
___
</details>
</td></tr>
</table>
""")
@ -228,7 +228,6 @@ class TestConvertToMarkdown:
expected_output = ''
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
def test_dictionary_with_empty_dictionaries(self):
@ -236,16 +235,16 @@ class TestConvertToMarkdown:
expected_output = ''
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
class TestBR:
def test_br1(self):
file_change_description = '- Imported `FilePatchInfo` and `EDIT_TYPE` from `pr_agent.algo.types` instead of `pr_agent.git_providers.git_provider`.'
file_change_description_br = insert_br_after_x_chars(file_change_description)
expected_output = ('<li>Imported <code>FilePatchInfo</code> and <code>EDIT_TYPE</code> from '
expected_output = ('<ul><li>Imported <code>FilePatchInfo</code> and <code>EDIT_TYPE</code> from '
'<code>pr_agent.algo.types</code> instead <br>of '
'<code>pr_agent.git_providers.git_provider</code>.')
'<code>pr_agent.git_providers.git_provider</code>.</ul>')
assert file_change_description_br == expected_output
# print("-----")
# print(file_change_description_br)
@ -255,9 +254,9 @@ class TestBR:
'- Created a - new -class `ColorPaletteResourcesCollection ColorPaletteResourcesCollection '
'ColorPaletteResourcesCollection ColorPaletteResourcesCollection`')
file_change_description_br = insert_br_after_x_chars(file_change_description)
expected_output = ('<li>Created a - new -class <code>ColorPaletteResourcesCollection </code><br><code>'
expected_output = ('<ul><li>Created a - new -class <code>ColorPaletteResourcesCollection </code><br><code>'
'ColorPaletteResourcesCollection ColorPaletteResourcesCollection '
'</code><br><code>ColorPaletteResourcesCollection</code>')
'</code><br><code>ColorPaletteResourcesCollection</code></ul>')
assert file_change_description_br == expected_output
# print("-----")
# print(file_change_description_br)