diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..63221fb --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# PatchPro Environment Configuration + +# OpenAI API Configuration +OPENAI_API_KEY=your-api-key-here + +# Model Configuration (optional, defaults to gpt-4o-mini) +# PATCHPRO_MODEL=gpt-4o-mini + +# Agent Configuration (optional) +# PATCHPRO_MAX_TOKENS=2000 +# PATCHPRO_TEMPERATURE=0.1 +# PATCHPRO_TIMEOUT=30 diff --git a/.gitignore b/.gitignore index 12893a1..413ab11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,17 @@ +# Python *.pyc -.venv/ -artifact/ +__pycache__/ +*.py[cod] +*$py.class +*.so +# Virtual environments +.venv/ +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ .Python [Bb]in [Ii]nclude @@ -10,309 +20,46 @@ artifact/ [Ll]ocal [Ss]cripts pyvenv.cfg -.venv pip-selfcheck.json -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf -.idea/**/aws.xml -.idea/**/contentModel.xml -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml -.idea/**/gradle.xml -.idea/**/libraries -cmake-build-*/ -.idea/**/mongoSettings.xml -*.iws -local.settings.json -out/ -.idea_modules/ -atlassian-ide-plugin.xml -.idea/replstate.xml -.idea/sonarlint/ -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties -.idea/httpRequests -.idea/caches/build_file_checksums.ser -.vscode/ -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets -.history/ -*.vsix -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db -*.stackdump -[Dd]esktop.ini -$RECYCLE.BIN/ -*.cab -*.msi -*.msix -*.msm -*.msp -*.lnk -*~ -.fuse_hidden* -.directory -.Trash-* -.nfs* -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates -*.userprefs -mono_crash.* -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ -.vs/ -Generated\ Files/ -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* -*.VisualState.xml -TestResult.xml -nunit-*.xml -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c -BenchmarkDotNet.Artifacts/ -project.lock.json -project.fragment.lock.json -artifacts/ -ScaffoldingReadMe.txt -StyleCopReport.xml -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc -_Chutzpah* -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb -*.psess -*.vsp -*.vspx -*.sap -*.e2e -$tf/ -*.gpState -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user -_TeamCity* -*.dotCover -.axoCover/* -!.axoCover/settings.json -coverage*.json -coverage*.xml -coverage*.info -*.coverage -*.coveragexml -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* -*.mm.* -AutoTest.Net/ -.sass-cache/ -[Ee]xpress/ -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html -publish/ -*.[Pp]ublish.xml -*.azurePubxml -*.pubxml -*.publishproj -PublishScripts/ -*.nupkg -*.snupkg -**/[Pp]ackages/* -!**/[Pp]ackages/build/ -*.nuget.props -*.nuget.targets -csx/ -*.build.csdef -ecf/ -rcf/ -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload -*.[Cc]ache -!?*.[Cc]ache/ -ClientBin/ -~$* -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs -Generated_Code/ -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak -*.mdf -*.ldf -*.ndf -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl -FakesAssemblies/ -*.GhostDoc.xml -.ntvs_analysis.dat -node_modules/ -*.plg -*.opt -*.vbw -*.vbp -*.dsw -*.dsp -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions -.paket/paket.exe -paket-files/ -.fake/ -.cr/personal -__pycache__/ -*.pyc -*.tss -*.jmconfig -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs -OpenCover/ -ASALocalRun/ -*.binlog -*.nvuser -.mfractor/ -.localhistory/ -.vshistory/ -healthchecksdb -MigrationBackup/ -.ionide/ -FodyWeavers.xsd -*.code-workspace -*.sln.iml -.DS_Store -.AppleDouble -.LSOverride -Icon -._* -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk -*.py[cod] -*$py.class -*.so + +# Build artifacts build/ -develop-eggs/ dist/ -downloads/ -eggs/ +artifact/ +artifacts/ +*.egg-info/ .eggs/ -lib/ -lib64/ -parts/ sdist/ var/ wheels/ share/python-wheels/ -*.egg-info/ .installed.cfg *.egg MANIFEST -*.manifest -*.spec -pip-log.txt -pip-delete-this-directory.txt -htmlcov/ -.tox/ -.nox/ +develop-eggs/ +downloads/ +eggs/ +parts/ + +# Environment variables +.env +local_settings.py + +# IDE +.idea/ +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets +.history/ +*.vsix +*.code-workspace +*.sln.iml +.vs/ + +# Testing .coverage .coverage.* .cache @@ -323,45 +70,46 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ -*.mo -*.pot -local_settings.py -db.sqlite3 -db.sqlite3-journal -instance/ -.webassets-cache -.scrapy -docs/_build/ -.pybuilder/ -target/ -.ipynb_checkpoints -profile_default/ -ipython_config.py -.pdm.toml -.pdm-python -.pdm-build/ -__pypackages__/ -celerybeat-schedule -celerybeat.pid -*.sage.py -.env -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ -.spyderproject -.spyproject -.ropeproject -/site +htmlcov/ +.tox/ +.nox/ + +# Logs +*.log +*.tlog +pip-log.txt +pip-delete-this-directory.txt + +# OS +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# Type checking .mypy_cache/ .dmypy.json dmypy.json .pyre/ .pytype/ + +# Documentation +docs/_build/ + +# Other cython_debug/ -orr/.config -orr/.data .config .data -.idea/ \ No newline at end of file diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..120c99a --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,144 @@ +# Ruff configuration for PatchPro static analysis +# See: https://docs.astral.sh/ruff/configuration/ + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.12+ +target-version = "py312" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "EM", # flake8-errmsg + "FA", # flake8-future-annotations + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SLOT", # flake8-slots + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "INT", # flake8-gettext + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "TD", # flake8-todos + "FIX", # flake8-fixme + "ERA", # eradicate + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PL", # Pylint + "TRY", # tryceratops + "FLY", # flynt + "NPY", # NumPy-specific rules + "PERF", # Perflint + "FURB", # refurb + "LOG", # flake8-logging + "RUF", # Ruff-specific rules +] + +ignore = [ + # Allow non-abstract empty methods in abstract base classes + "B027", + # Allow boolean positional values in function calls, like `dict.get(... True)` + "FBT003", + # Ignore checks for possible passwords + "S105", "S106", "S107", + # Ignore complexity + "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", + # Allow print statements (useful for CLI tools) + "T201", + # Allow TODO/FIXME comments + "TD002", "TD003", "FIX002", +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" + +[lint.per-file-ignores] +# Tests can use magic values, assertions, and relative imports +"tests/**/*" = [ + "PLR2004", + "S101", + "TID252", +] \ No newline at end of file diff --git a/docs/QUICK_REFERENCE.md b/docs/QUICK_REFERENCE.md new file mode 100644 index 0000000..fba724f --- /dev/null +++ b/docs/QUICK_REFERENCE.md @@ -0,0 +1,157 @@ +# PatchPro Agent - Quick Reference + +## Installation + +```bash +pip install -e . +export OPENAI_API_KEY='your-api-key' +``` + +## Basic Usage + +```bash +# Step 1: Analyze code +patchpro analyze your_code.py --output findings.json + +# Step 2: Generate fixes +patchpro agent findings.json --output fixes.md + +# Step 3: Review +cat fixes.md +``` + +## Common Commands + +### Analyze Only +```bash +patchpro analyze src/ --output findings.json --format json +``` + +### Generate Fixes with Specific Model +```bash +patchpro agent findings.json --model gpt-4o --output fixes.md +``` + +### Full Workflow +```bash +patchpro analyze . --output findings.json && \ +patchpro agent findings.json --output report.md +``` + +## Environment Variables + +```bash +# Required +export OPENAI_API_KEY='sk-...' + +# Optional +export PATCHPRO_MODEL='gpt-4o-mini' +export PATCHPRO_MAX_TOKENS='2000' +export PATCHPRO_TEMPERATURE='0.1' +``` + +## Configuration + +Default settings (can be customized in code): +- **Model**: gpt-4o-mini (cost-effective) +- **Max tokens**: 2000 +- **Temperature**: 0.1 (deterministic) +- **Batch size**: 5 findings +- **Max diff lines**: 50 + +## Output Format + +The agent generates markdown with: +- Summary of findings +- Grouped fixes by file +- Unified diff format +- Confidence indicators (✅⚠️❓) +- Explanations for each fix + +## Troubleshooting + +**"OpenAI API key not provided"** +```bash +export OPENAI_API_KEY='your-key' +``` + +**"Module 'openai' not found"** +```bash +pip install openai +``` + +**"Could not load source files"** +- Check `--base-path` argument +- Ensure files are accessible + +## Examples + +### Example 1: Single File +```bash +patchpro analyze script.py -o findings.json +patchpro agent findings.json -o fixes.md +``` + +### Example 2: Full Project +```bash +patchpro analyze src/ \ + --tools ruff semgrep \ + --output findings.json + +patchpro agent findings.json \ + --base-path . \ + --output report.md +``` + +### Example 3: Custom Model +```bash +patchpro agent findings.json \ + --model gpt-4o \ + --output fixes.md +``` + +## API Usage + +```python +from pathlib import Path +from patchpro_bot.agent import PatchProAgent, AgentConfig, load_source_files +from patchpro_bot.analyzer import FindingsAnalyzer + +# Load findings +analyzer = FindingsAnalyzer() +findings = analyzer.load_and_normalize("artifact/analysis") + +# Load source files +source_files = load_source_files(findings, Path(".")) + +# Configure and run agent +config = AgentConfig(model="gpt-4o-mini", api_key="sk-...") +agent = PatchProAgent(config) +result = agent.process_findings(findings, source_files) + +# Generate report +report = agent.generate_markdown_report(result) +print(report) +``` + +## Cost Estimate + +Using **gpt-4o-mini**: +- ~$0.0002 per fix +- ~$0.002 for 10 fixes +- ~$0.02 for 100 fixes + +Very cost-effective for CI/CD use! + +## Next Steps + +1. Review generated fixes +2. Apply changes manually or use diffs +3. Run tests +4. Commit changes + +## Links + +- [Full Agent Guide](agent_guide.md) +- [Implementation Details](AGENT_IMPLEMENTATION.md) +- [Requirements](requirements.md) diff --git a/docs/agent_guide.md b/docs/agent_guide.md new file mode 100644 index 0000000..89ceebe --- /dev/null +++ b/docs/agent_guide.md @@ -0,0 +1,259 @@ +# PatchPro Agent Guide + +## Overview + +The PatchPro Agent is an AI-powered component that takes normalized static analysis findings and generates automated code fixes with explanations. + +## Features + +- **AI-Powered Fix Generation**: Uses OpenAI GPT models to generate contextual code fixes +- **Guardrails**: Built-in safety limits for diff size and complexity +- **Batch Processing**: Efficiently processes multiple findings +- **Confidence Scoring**: Each fix includes a confidence level (low/medium/high) +- **Markdown Reports**: Generates formatted PR-ready markdown reports + +## Quick Start + +### Prerequisites + +1. Install dependencies: +```bash +pip install -e . +``` + +2. Set your OpenAI API key: +```bash +export OPENAI_API_KEY='your-api-key-here' +``` + +### Basic Usage + +```bash +# 1. Run analysis first +patchpro analyze your_file.py --output findings.json + +# 2. Generate fixes with agent +patchpro agent findings.json --output report.md +``` + +## Command Reference + +### `patchpro agent` + +Generate code fixes from normalized findings using AI. + +**Usage:** +```bash +patchpro agent [OPTIONS] FINDINGS_FILE +``` + +**Arguments:** +- `FINDINGS_FILE`: Path to normalized findings JSON file (from `patchpro analyze`) + +**Options:** +- `--output, -o PATH`: Output file for markdown report (default: stdout) +- `--base-path, -b PATH`: Base directory for resolving file paths (default: `.`) +- `--model, -m TEXT`: OpenAI model to use (default: `gpt-4o-mini`) +- `--api-key TEXT`: OpenAI API key (or set `OPENAI_API_KEY` env var) + +**Examples:** + +```bash +# Basic usage with environment variable +export OPENAI_API_KEY='sk-...' +patchpro agent findings.json --output fixes.md + +# Specify model and API key inline +patchpro agent findings.json \ + --model gpt-4o \ + --api-key sk-... \ + --output fixes.md + +# Use different base path for file resolution +patchpro agent findings.json \ + --base-path /path/to/project \ + --output fixes.md +``` + +## Configuration + +### Environment Variables + +- `OPENAI_API_KEY`: Your OpenAI API key (required) +- `PATCHPRO_MODEL`: Default model to use (optional) +- `PATCHPRO_MAX_TOKENS`: Max tokens per request (optional, default: 2000) +- `PATCHPRO_TEMPERATURE`: Temperature for generation (optional, default: 0.1) +- `PATCHPRO_TIMEOUT`: Request timeout in seconds (optional, default: 30) + +### Agent Configuration + +The agent includes several guardrails: + +- **Max Findings Per Request**: 5 (processes in batches) +- **Max Lines Per Diff**: 50 (skips overly complex changes) +- **Temperature**: 0.1 (low for deterministic output) +- **Max Tokens**: 2000 per request + +These can be customized by modifying `AgentConfig` in your code: + +```python +from patchpro_bot.agent import AgentConfig, PatchProAgent + +config = AgentConfig( + model="gpt-4o", + max_tokens=3000, + max_lines_per_diff=100 +) + +agent = PatchProAgent(config) +``` + +## Output Format + +The agent generates a markdown report with: + +1. **Summary Section** + - Total findings count + - Number of fixes generated + - Analysis metadata + +2. **Fixes Section** (grouped by file) + - Confidence indicator (✅/⚠️/❓) + - Explanation of the fix + - Unified diff format + +3. **Footer** + - Attribution + - Review reminder + +### Example Output + +```markdown +# 🔧 PatchPro Code Fixes + +## PatchPro Analysis Summary + +- **Total Findings:** 19 +- **Fixes Generated:** 12 +- **Analysis Tool:** ruff +- **Timestamp:** 2025-10-03T12:00:00 + +## 📝 Proposed Fixes + +### 📄 `test_sample.py` + +#### Fix 1: ✅ Split multiple imports into separate lines per PEP 8 + +**Diff:** +\```diff +--- a/test_sample.py ++++ b/test_sample.py +@@ -1,1 +1,2 @@ +-import os, sys ++import os ++import sys +\``` + +--- + +*Generated by PatchPro AI Code Repair Assistant* +*Review all changes before applying* +``` + +## Integration with CI/CD + +### GitHub Actions Example + +```yaml +- name: Run PatchPro Analysis + run: | + patchpro analyze . --output findings.json + +- name: Generate Fixes + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + patchpro agent findings.json --output report.md + +- name: Post PR Comment + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + const report = fs.readFileSync('report.md', 'utf8'); + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: report + }); +``` + +## Best Practices + +1. **Review All Fixes**: Always review generated fixes before applying +2. **Test Changes**: Run your test suite after applying fixes +3. **Batch Processing**: For large codebases, process findings in smaller batches +4. **Model Selection**: + - Use `gpt-4o-mini` for cost-effective basic fixes + - Use `gpt-4o` for complex refactoring +5. **API Key Security**: Never commit API keys; use environment variables or secrets + +## Troubleshooting + +### "OpenAI API key not provided" +- Ensure `OPENAI_API_KEY` is set in your environment +- Or pass `--api-key` flag directly + +### "Missing dependency: No module named 'openai'" +```bash +pip install openai +``` + +### "Could not load source files" +- Check that `--base-path` points to the correct directory +- Ensure file paths in findings.json are relative to base-path + +### Rate Limits +- The agent processes findings in batches of 5 +- Add delays between batches if hitting rate limits +- Consider using a higher-tier API plan for production + +## API Reference + +For programmatic usage, see the main classes: + +- `PatchProAgent`: Main agent class +- `AgentConfig`: Configuration options +- `GeneratedFix`: Fix result structure +- `AgentResult`: Overall processing result + +Example: + +```python +from pathlib import Path +from patchpro_bot.agent import PatchProAgent, AgentConfig, load_source_files +from patchpro_bot.analyzer import FindingsAnalyzer + +# Load findings +analyzer = FindingsAnalyzer() +findings = analyzer.load_and_normalize("artifact/analysis") + +# Load source files +source_files = load_source_files(findings, Path(".")) + +# Run agent +config = AgentConfig(model="gpt-4o-mini") +agent = PatchProAgent(config) +result = agent.process_findings(findings, source_files) + +# Generate report +report = agent.generate_markdown_report(result) +print(report) +``` + +## Next Steps + +- Learn about [CI/DevEx Integration](../docs/requirements.md#3-cidevex) +- Explore [Evaluation and QA](../docs/requirements.md#4-evalqa) +- Check out [Example Workflows](../examples/) diff --git a/docs/requirements_document.docx b/docs/requirements_document.docx deleted file mode 100644 index 862d45d..0000000 Binary files a/docs/requirements_document.docx and /dev/null differ diff --git a/examples/demo_workflow.sh b/examples/demo_workflow.sh new file mode 100755 index 0000000..ce307af --- /dev/null +++ b/examples/demo_workflow.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Demo workflow showing analyzer -> agent pipeline + +set -e + +echo "🚀 PatchPro Demo Workflow" +echo "=========================" +echo "" + +# Step 1: Run analysis +echo "📊 Step 1: Running static analysis..." +patchpro analyze test_sample.py \ + --output artifact/findings.json \ + --format json \ + --artifacts-dir artifact/analysis + +echo "" +echo "✅ Analysis complete! Findings saved to artifact/findings.json" +echo "" + +# Step 2: Generate fixes with agent +echo "🤖 Step 2: Generating AI-powered fixes..." +patchpro agent artifact/findings.json \ + --output artifact/patchpro_report.md \ + --base-path . \ + --model gpt-4o-mini + +echo "" +echo "✅ Fixes generated! Report saved to artifact/patchpro_report.md" +echo "" + +# Step 3: Display report +echo "📄 Step 3: Displaying report..." +echo "==============================" +cat artifact/patchpro_report.md + +echo "" +echo "🎉 Demo complete!" +echo "" +echo "Next steps:" +echo " 1. Review the generated report in artifact/patchpro_report.md" +echo " 2. Apply fixes manually or use the diffs" +echo " 3. Run tests to verify changes" diff --git a/pyproject.toml b/pyproject.toml index 108c17d..4cbcda8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dev = [ [project.scripts] patchpro = "patchpro_bot.cli:app" + [build-system] requires = ["setuptools>=68", "wheel"] build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/schemas/findings.v1.json b/schemas/findings.v1.json new file mode 100644 index 0000000..fcb8caf --- /dev/null +++ b/schemas/findings.v1.json @@ -0,0 +1,154 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PatchPro Normalized Findings Schema", + "description": "Unified schema for static analysis findings from Ruff, Semgrep, and other tools", + "type": "object", + "properties": { + "findings": { + "type": "array", + "items": { + "$ref": "#/definitions/Finding" + } + }, + "metadata": { + "type": "object", + "properties": { + "tool": { + "type": "string", + "description": "The source tool that generated the finding (ruff, semgrep, etc.)" + }, + "version": { + "type": "string", + "description": "Version of the source tool" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the analysis was performed" + }, + "total_findings": { + "type": "integer", + "description": "Total number of findings in this result set" + } + }, + "required": ["tool", "version", "total_findings"] + } + }, + "required": ["findings", "metadata"], + "definitions": { + "Finding": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the finding (hash or tool-specific ID)" + }, + "rule_id": { + "type": "string", + "description": "Rule/check identifier (e.g., F401 for Ruff, security.crypto-weak for Semgrep)" + }, + "rule_name": { + "type": "string", + "description": "Human-readable rule name" + }, + "message": { + "type": "string", + "description": "Description of the issue found" + }, + "severity": { + "type": "string", + "enum": ["error", "warning", "info", "note"], + "description": "Normalized severity level" + }, + "category": { + "type": "string", + "enum": ["style", "security", "performance", "correctness", "complexity", "import", "typing"], + "description": "Category of the issue" + }, + "location": { + "$ref": "#/definitions/Location" + }, + "suggestion": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Suggested fix description" + }, + "replacements": { + "type": "array", + "items": { + "$ref": "#/definitions/Replacement" + } + } + } + }, + "source_tool": { + "type": "string", + "description": "Original tool that detected this finding" + } + }, + "required": ["id", "rule_id", "message", "severity", "category", "location", "source_tool"] + }, + "Location": { + "type": "object", + "properties": { + "file": { + "type": "string", + "description": "Relative file path from repository root" + }, + "line": { + "type": "integer", + "minimum": 1, + "description": "Line number (1-indexed)" + }, + "column": { + "type": "integer", + "minimum": 1, + "description": "Column number (1-indexed)" + }, + "end_line": { + "type": "integer", + "minimum": 1, + "description": "End line number for multi-line findings" + }, + "end_column": { + "type": "integer", + "minimum": 1, + "description": "End column number for multi-character findings" + } + }, + "required": ["file", "line", "column"] + }, + "Replacement": { + "type": "object", + "properties": { + "start": { + "$ref": "#/definitions/Position" + }, + "end": { + "$ref": "#/definitions/Position" + }, + "content": { + "type": "string", + "description": "New content to replace the identified text" + } + }, + "required": ["start", "end", "content"] + }, + "Position": { + "type": "object", + "properties": { + "line": { + "type": "integer", + "minimum": 1 + }, + "column": { + "type": "integer", + "minimum": 1 + } + }, + "required": ["line", "column"] + } + } +} \ No newline at end of file diff --git a/semgrep.yml b/semgrep.yml new file mode 100644 index 0000000..b2f3bfd --- /dev/null +++ b/semgrep.yml @@ -0,0 +1,138 @@ +# Semgrep configuration for PatchPro static analysis +# See: https://semgrep.dev/docs/writing-rules/rule-syntax/ + +rules: + # Python Security Rules + - id: python-security-hardcoded-password + pattern: | + password = "..." + message: "Hardcoded password detected" + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-798: Use of Hard-coded Credentials" + confidence: HIGH + + - id: python-security-sql-injection-format + pattern: | + cursor.execute($FMT % ...) + message: "SQL injection vulnerability via string formatting" + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-89: SQL Injection" + confidence: HIGH + + - id: python-security-exec-eval + patterns: + - pattern: exec(...) + - pattern: eval(...) + message: "Use of exec() or eval() is dangerous" + languages: [python] + severity: WARNING + metadata: + category: security + cwe: "CWE-95: Code Injection" + confidence: MEDIUM + + # Python Code Quality Rules + - id: python-correctness-assert-used + pattern: | + assert $CONDITION + message: "Assert statements are removed in optimized mode" + languages: [python] + severity: WARNING + metadata: + category: correctness + confidence: MEDIUM + + - id: python-performance-list-comprehension + pattern: | + list(filter($FUNC, $LIST)) + message: "Use list comprehension instead of filter() for better performance" + languages: [python] + severity: INFO + metadata: + category: performance + confidence: MEDIUM + + - id: python-style-f-string + patterns: + - pattern: '"{} {}".format($A, $B)' + - pattern: '"{0} {1}".format($A, $B)' + message: "Use f-strings for string formatting" + languages: [python] + severity: INFO + metadata: + category: style + confidence: HIGH + + # Import and Dependencies + - id: python-imports-deprecated-modules + patterns: + - pattern: import imp + - pattern: from imp import ... + - pattern: import optparse + - pattern: from optparse import ... + message: "Using deprecated module" + languages: [python] + severity: WARNING + metadata: + category: correctness + confidence: HIGH + + # Exception Handling + - id: python-correctness-bare-except + pattern: | + try: + ... + except: + ... + message: "Bare except clause catches all exceptions" + languages: [python] + severity: WARNING + metadata: + category: correctness + confidence: HIGH + + - id: python-correctness-exception-base-class + pattern: | + raise $MSG + message: "Raise an Exception instance, not a string" + languages: [python] + severity: ERROR + metadata: + category: correctness + confidence: HIGH + paths: + exclude: + - "*.py" + patterns: + - pattern-not: raise Exception(...) + - pattern-not: raise $EXCEPTION(...) + - pattern-not: raise $EXCEPTION + + # Type Hints and Annotations + - id: python-typing-optional-instead-of-union + pattern: | + Union[$TYPE, None] + message: "Use Optional[T] instead of Union[T, None]" + languages: [python] + severity: INFO + metadata: + category: typing + confidence: HIGH + + # Logging Best Practices + - id: python-logging-format-string + patterns: + - pattern: logging.$LEVEL("..." % ...) + - pattern: logger.$LEVEL("..." % ...) + message: "Use logging format strings instead of % formatting" + languages: [python] + severity: INFO + metadata: + category: style + confidence: HIGH \ No newline at end of file diff --git a/src/patchpro_bot.egg-info/PKG-INFO b/src/patchpro_bot.egg-info/PKG-INFO new file mode 100644 index 0000000..ac9d340 --- /dev/null +++ b/src/patchpro_bot.egg-info/PKG-INFO @@ -0,0 +1,601 @@ +Metadata-Version: 2.4 +Name: patchpro-bot +Version: 0.0.1 +Summary: CI code-repair assistant (comment-only in Sprint-0) +Requires-Python: >=3.12 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: ruff~=0.13.1 +Requires-Dist: semgrep~=1.137.0 +Requires-Dist: typer~=0.19.2 +Requires-Dist: pydantic~=2.11.9 +Requires-Dist: rich~=13.5.2 +Requires-Dist: httpx~=0.28.1 +Requires-Dist: openai~=1.108.2 +Requires-Dist: unidiff~=0.7.5 +Requires-Dist: python-dotenv~=1.1.1 +Requires-Dist: aiofiles~=24.1.0 +Provides-Extra: dev +Requires-Dist: pytest>=7.0.0; extra == "dev" +Requires-Dist: pytest-cov>=4.0.0; extra == "dev" +Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev" +Requires-Dist: black>=23.0.0; extra == "dev" +Requires-Dist: mypy>=1.0.0; extra == "dev" +Dynamic: license-file + +# patchpro-bot + +PatchPro: CI code-repair assistant that analyzes code using Ruff and Semgrep, then generates intelligent patch suggestions using LLM. + +## Quick Start + +**For Collaborators:** See [DEVELOPMENT.md](./DEVELOPMENT.md) for complete setup and testing instructions. + +**For End Users:** Try the [demo repository](https://github.com/A3copilotprogram/patchpro-demo-repo) to see PatchPro in action. + +```bash +# Quick test with demo repo +git clone +cd patchpro-demo-repo +echo "OPENAI_API_KEY=your-key" > .env +uv run --with /path/to/patchpro-bot-agent-dev python -m patchpro_bot.run_ci +``` + +## Overview + +PatchPro Bot is a comprehensive code analysis and patch generation tool that: + +1. **Reads** JSON analysis reports from Ruff (Python linter) and Semgrep (static analysis) +2. **Processes** findings with deduplication, prioritization, and aggregation +3. **Generates** intelligent code fixes using OpenAI's LLM +4. **Creates** unified diff patches that can be applied to fix the issues +5. **Reports** comprehensive analysis results and patch summaries + +## Architecture + +The codebase follows the pipeline described in this mermaid diagram: + +```mermaid +flowchart TD + A[patchpro-demo-repo PR] --> B[GitHub Actions CI] + subgraph Analysis + direction LR + C1[Ruff ▶ JSON] + C2[Semgrep ▶ JSON] + end + B --> C{Analyzers} + C --> C1[Ruff: lint issues to JSON] + C --> C2[Semgrep: patterns to JSON] + C1 & C2 --> D[Artifact storage: artifact/analysis/*.json] + D --> E[Agent Core] + E --> F[LLM: OpenAI call prompt toolkit] + F --> G[Unified diff + rationale: patch_*.diff] + G & D --> H[Report generator: report.md] + H --> I[Sticky PR comment] + I --> J[Eval/QA judge & metrics: artifact/run_metrics.json] +``` + +## Project Structure + +``` +src/patchpro_bot/ +├── __init__.py # Package exports +├── agent_core.py # Main orchestrator +├── run_ci.py # Legacy CI runner (delegates to agent_core) +├── analysis/ # Analysis reading and aggregation +│ ├── __init__.py +│ ├── reader.py # JSON file reader for Ruff/Semgrep +│ └── aggregator.py # Finding aggregation and processing +├── models/ # Pydantic data models +│ ├── __init__.py +│ ├── common.py # Shared models and enums +│ ├── ruff.py # Ruff-specific models +│ └── semgrep.py # Semgrep-specific models +├── llm/ # LLM integration +│ ├── __init__.py +│ ├── client.py # OpenAI client wrapper +│ ├── prompts.py # Prompt templates and builders +│ └── response_parser.py # Parse LLM responses +└── diff/ # Diff generation and patch writing + ├── __init__.py + ├── file_reader.py # Source file reading + ├── generator.py # Unified diff generation + └── patch_writer.py # Patch file writing +``` + +## Installation + +1. **Clone the repository**: + ```bash + git clone + cd patchpro-bot + ``` + +2. **Create and activate virtual environment**: + ```bash + python -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + ``` + +3. **Install the package**: + ```bash + pip install -e . + ``` + +4. **Install development dependencies** (optional): + ```bash + pip install -e ".[dev]" + ``` + +## Usage + +### Basic Usage + +1. **Set up your OpenAI API key**: + ```bash + export OPENAI_API_KEY="your-openai-api-key-here" + ``` + +2. **Prepare analysis files**: + Create `artifact/analysis/` directory and place your Ruff and Semgrep JSON output files there: + ```bash + mkdir -p artifact/analysis + # Copy your ruff and semgrep JSON files to artifact/analysis/ + ``` + +3. **Run the bot**: + ```bash + python -m patchpro_bot.agent_core + ``` + + Or use the test pipeline: + ```bash + python test_pipeline.py + ``` + +### Programmatic Usage + +```python +from patchpro_bot import AgentCore, AgentConfig +from pathlib import Path + +# Configure the agent +config = AgentConfig( + analysis_dir=Path("artifact/analysis"), + artifact_dir=Path("artifact"), + openai_api_key="your-api-key", + max_findings=20, +) + +# Create and run the agent +agent = AgentCore(config) +results = agent.run() + +print(f"Status: {results['status']}") +print(f"Generated {results['patches_written']} patches") +``` + +### Testing with Sample Data + +The project includes sample data for testing: + +```bash +# Copy sample analysis files +cp tests/sample_data/*.json artifact/analysis/ + +# Copy sample source file +cp tests/sample_data/example.py src/ + +# Run the test pipeline +python test_pipeline.py +``` + +## Configuration + +The `AgentConfig` class supports the following options: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `analysis_dir` | `artifact/analysis` | Directory containing JSON analysis files | +| `artifact_dir` | `artifact` | Output directory for patches and reports | +| `base_dir` | Current directory | Base directory for source files | +| `openai_api_key` | `None` | OpenAI API key (can also use `OPENAI_API_KEY` env var) | +| `llm_model` | `gpt-4o-mini` | OpenAI model to use | +| `max_tokens` | `4096` | Maximum tokens for LLM response | +| `temperature` | `0.1` | LLM temperature (0.0 = deterministic) | +| `max_findings` | `20` | Maximum findings to process | +| `max_files_per_batch` | `5` | Maximum files to process in one batch | +| `combine_patches` | `True` | Whether to create a combined patch file | +| `generate_summary` | `True` | Whether to generate patch summaries | + +## Analysis File Formats + +### Ruff JSON Format + +```json +[ + { + "code": "F401", + "filename": "src/example.py", + "location": {"row": 1, "column": 8}, + "end_location": {"row": 1, "column": 11}, + "message": "`sys` imported but unused", + "fix": { + "applicability": "automatic", + "edits": [{"content": "", "location": {"row": 1, "column": 1}}] + } + } +] +``` + +### Semgrep JSON Format + +```json +{ + "results": [ + { + "check_id": "python.lang.security.hardcoded-password.hardcoded-password", + "path": "src/auth.py", + "start": {"start": {"line": 12, "col": 13}}, + "end": {"end": {"line": 12, "col": 35}}, + "extra": { + "message": "Hardcoded password found", + "severity": "ERROR", + "metadata": {"category": "security", "confidence": "HIGH"} + } + } + ] +} +``` + +## Output + +The bot generates several output files in the `artifact/` directory: + +- `patch_001.diff`, `patch_002.diff`, etc. - Individual patch files +- `combined_patch.diff` - Combined patch file (if enabled) +- `patch_summary.md` - Summary of all generated patches +- `report.md` - Comprehensive analysis report + +## Testing + +Run the test suite: + +```bash +# Run all tests +pytest + +# Run with coverage +pytest --cov=src/patchpro_bot + +# Run specific test modules +pytest tests/test_analysis.py +pytest tests/test_models.py +pytest tests/test_llm.py +pytest tests/test_diff.py +``` + +## Development + +### Code Quality + +The project uses several tools for code quality: + +```bash +# Format code +black src/ tests/ + +# Type checking +mypy src/ + +# Linting +ruff check src/ tests/ +``` + +### Adding New Analysis Tools + +To add support for new analysis tools: + +1. Create a new model in `src/patchpro_bot/models/` +2. Update the `AnalysisReader` to detect and parse the new format +3. Add tests for the new functionality + +### Extending LLM Capabilities + +The LLM integration is modular and can be extended: + +- Add new prompt templates in `prompts.py` +- Extend response parsing in `response_parser.py` +- Add support for different LLM providers in `client.py` + +## Dependencies + +### Core Dependencies +- `pydantic` - Data validation and parsing +- `openai` - OpenAI API client +- `unidiff` - Unified diff processing +- `python-dotenv` - Environment variable management +- `typer` - CLI framework +- `rich` - Rich text and beautiful formatting +- `httpx` - HTTP client + +### Analysis Tools (External) +- `ruff` - Python linter +- `semgrep` - Static analysis tool + +### Development Dependencies +- `pytest` - Testing framework +- `pytest-cov` - Coverage reporting +- `pytest-asyncio` - Async testing support +- `black` - Code formatting +- `mypy` - Type checking + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests for new functionality +5. Run the test suite +6. Submit a pull request + +## License + +MIT License - see LICENSE file for details. - An intelligent patch bot that analyzes static analysis reports from Ruff and Semgrep and generates unified diff patches using LLM-powered suggestions. + +## 🎯 Overview + +PatchPro Bot follows the pipeline described in your mermaid diagram: + +``` +Analysis JSON → Agent Core → LLM Suggestions → Unified Diff Generation → Patch Files +``` + +The bot reads JSON reports from static analysis tools (Ruff for Python linting, Semgrep for security/pattern analysis), sends the findings to an LLM for intelligent code suggestions, and generates properly formatted unified diff patches. + +## 🏗️ Architecture + +### Core Components + +- **📋 Analysis Module** (`src/patchpro_bot/analysis/`) + - `AnalysisReader`: Reads and parses JSON files from `artifact/analysis/` + - `FindingAggregator`: Processes, filters, and organizes findings for LLM consumption + +- **🧠 LLM Module** (`src/patchpro_bot/llm/`) + - `LLMClient`: OpenAI integration for generating code suggestions + - `PromptBuilder`: Creates structured prompts for different fix scenarios + - `ResponseParser`: Extracts code fixes and diffs from LLM responses + +- **🔧 Diff Module** (`src/patchpro_bot/diff/`) + - `DiffGenerator`: Creates unified diffs from code changes + - `FileReader`: Reads source code files for diff generation + - `PatchWriter`: Writes patch files to the artifact directory + +- **🎛️ Agent Core** (`src/patchpro_bot/agent_core.py`) + - Orchestrates the entire pipeline from analysis to patch generation + - Configurable processing limits and output options + +- **📊 Models** (`src/patchpro_bot/models/`) + - Pydantic models for Ruff and Semgrep JSON schemas + - Unified `AnalysisFinding` model for cross-tool compatibility + +## 🚀 Quick Start + +### Installation + +1. Clone the repository and install dependencies: +```bash +cd patchpro-bot +pip install -e . +``` + +2. Install optional development dependencies: +```bash +pip install -e ".[dev]" +``` + +### Basic Usage + +1. **Set up your OpenAI API key:** +```bash +export OPENAI_API_KEY="your-api-key-here" +``` + +2. **Prepare analysis data:** +Place your Ruff and Semgrep JSON outputs in `artifact/analysis/`: +```bash +mkdir -p artifact/analysis +ruff check --format=json examples/src/ > artifact/analysis/ruff_output.json +semgrep --config=auto --json examples/src/ > artifact/analysis/semgrep_output.json +``` + +3. **Run the bot:** +```bash +python -m patchpro_bot.agent_core +``` + +4. **Check the results:** +- Patch files: `artifact/patch_*.diff` +- Report: `artifact/report.md` + +### Using the API + +```python +from patchpro_bot import AgentCore, AgentConfig + +# Configure the agent +config = AgentConfig( + analysis_dir=Path("artifact/analysis"), + openai_api_key="your-api-key", + max_findings=10 +) + +# Run the pipeline +agent = AgentCore(config) +results = agent.run() + +print(f"Generated {results['patches_written']} patches") +``` + +## 📁 Project Structure + +``` +patchpro-bot/ +├── src/patchpro_bot/ +│ ├── __init__.py # Package exports +│ ├── agent_core.py # Main orchestrator +│ ├── run_ci.py # Legacy CI runner +│ ├── analysis/ # Analysis reading & processing +│ │ ├── reader.py # JSON file reader +│ │ └── aggregator.py # Finding aggregation +│ ├── llm/ # LLM integration +│ │ ├── client.py # OpenAI client +│ │ ├── prompts.py # Prompt templates +│ │ └── response_parser.py # Response parsing +│ ├── diff/ # Diff generation +│ │ ├── generator.py # Unified diff creation +│ │ ├── file_reader.py # Source file reading +│ │ └── patch_writer.py # Patch file writing +│ └── models/ # Data models +│ ├── common.py # Common types +│ ├── ruff.py # Ruff JSON schema +│ └── semgrep.py # Semgrep JSON schema +├── tests/ # Comprehensive test suite +├── examples/ # Sample data and usage +└── docs/ # Documentation +``` + +## 🔧 Configuration + +### Environment Variables + +- `OPENAI_API_KEY`: Your OpenAI API key (required) +- `LLM_MODEL`: Model to use (default: `gpt-4o-mini`) +- `MAX_FINDINGS`: Maximum findings to process (default: `20`) +- `PP_ARTIFACTS`: Artifact directory path (default: `artifact`) + +### AgentConfig Options + +```python +config = AgentConfig( + # Directories + analysis_dir=Path("artifact/analysis"), + artifact_dir=Path("artifact"), + base_dir=Path.cwd(), + + # LLM settings + openai_api_key="your-key", + llm_model="gpt-4o-mini", + max_tokens=4096, + temperature=0.1, + + # Processing limits + max_findings=20, + max_files_per_batch=5, + + # Output settings + combine_patches=True, + generate_summary=True +) +``` + +## 📝 Supported Analysis Tools + +### Ruff (Python Linter) +- **Supported**: All Ruff rule categories (F, E, W, C, N, D, S, B, etc.) +- **Features**: Automatic fix extraction, severity inference, rule categorization +- **Format**: JSON output from `ruff check --format=json` + +### Semgrep (Security & Pattern Analysis) +- **Supported**: All Semgrep rule types and severities +- **Features**: Security vulnerability detection, metadata extraction +- **Format**: JSON output from `semgrep --json` + +## 🧪 Testing + +Run the comprehensive test suite: + +```bash +# Install test dependencies +pip install -e ".[dev]" + +# Run all tests +pytest + +# Run with coverage +pytest --cov=patchpro_bot + +# Run specific test modules +pytest tests/test_analysis.py +pytest tests/test_llm.py +pytest tests/test_diff.py +``` + +## 📊 Example Output + +### Generated Patch +```diff +diff --git a/src/example.py b/src/example.py +index 1234567..abcdefg 100644 +--- a/src/example.py ++++ b/src/example.py +@@ -1,5 +1,4 @@ +-import os + import sys + import subprocess + + def main(): +``` + +### Report Summary +```markdown +# PatchPro Bot Report + +## Summary +- **Total findings**: 6 +- **Tools used**: ruff, semgrep +- **Affected files**: 2 +- **Patches generated**: 3 + +## Findings Breakdown +- **error**: 3 +- **warning**: 2 +- **high**: 1 +``` + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Add tests for your changes +4. Ensure tests pass (`pytest`) +5. Commit your changes (`git commit -m 'Add amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request + +## 📋 Requirements + +- Python 3.12+ +- OpenAI API key +- Dependencies listed in `pyproject.toml` + +## 🔒 Security + +- API keys are loaded from environment variables +- No sensitive data is logged +- Minimal, targeted code changes to reduce risk +- Security-first prioritization in fix suggestions + +## 📜 License + +MIT License - see [LICENSE](LICENSE) file for details. + +## 🆘 Support + +- 📖 Check the [examples/](examples/) directory for usage samples +- 🐛 Report issues on GitHub +- 💬 Review the comprehensive test suite for API usage examples + +--- + +**PatchPro Bot** - Intelligent code repair for modern CI/CD pipelines 🚀 diff --git a/src/patchpro_bot.egg-info/SOURCES.txt b/src/patchpro_bot.egg-info/SOURCES.txt new file mode 100644 index 0000000..686c713 --- /dev/null +++ b/src/patchpro_bot.egg-info/SOURCES.txt @@ -0,0 +1,35 @@ +LICENSE +README.md +pyproject.toml +src/patchpro_bot/__init__.py +src/patchpro_bot/agent.py +src/patchpro_bot/agent_core.py +src/patchpro_bot/analyzer.py +src/patchpro_bot/cli.py +src/patchpro_bot/run_ci.py +src/patchpro_bot.egg-info/PKG-INFO +src/patchpro_bot.egg-info/SOURCES.txt +src/patchpro_bot.egg-info/dependency_links.txt +src/patchpro_bot.egg-info/entry_points.txt +src/patchpro_bot.egg-info/requires.txt +src/patchpro_bot.egg-info/top_level.txt +src/patchpro_bot/analysis/__init__.py +src/patchpro_bot/analysis/aggregator.py +src/patchpro_bot/analysis/reader.py +src/patchpro_bot/diff/__init__.py +src/patchpro_bot/diff/file_reader.py +src/patchpro_bot/diff/generator.py +src/patchpro_bot/diff/patch_writer.py +src/patchpro_bot/llm/__init__.py +src/patchpro_bot/llm/client.py +src/patchpro_bot/llm/prompts.py +src/patchpro_bot/llm/response_parser.py +src/patchpro_bot/models/__init__.py +src/patchpro_bot/models/common.py +src/patchpro_bot/models/ruff.py +src/patchpro_bot/models/semgrep.py +tests/test_agent.py +tests/test_analysis.py +tests/test_diff.py +tests/test_llm.py +tests/test_models.py \ No newline at end of file diff --git a/src/patchpro_bot.egg-info/dependency_links.txt b/src/patchpro_bot.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/patchpro_bot.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/patchpro_bot.egg-info/requires.txt b/src/patchpro_bot.egg-info/requires.txt new file mode 100644 index 0000000..eccfd84 --- /dev/null +++ b/src/patchpro_bot.egg-info/requires.txt @@ -0,0 +1,17 @@ +ruff~=0.13.1 +semgrep~=1.137.0 +typer~=0.19.2 +pydantic~=2.11.9 +rich~=13.5.2 +httpx~=0.28.1 +openai~=1.108.2 +unidiff~=0.7.5 +python-dotenv~=1.1.1 +aiofiles~=24.1.0 + +[dev] +pytest>=7.0.0 +pytest-cov>=4.0.0 +pytest-asyncio>=0.21.0 +black>=23.0.0 +mypy>=1.0.0 diff --git a/src/patchpro_bot.egg-info/top_level.txt b/src/patchpro_bot.egg-info/top_level.txt new file mode 100644 index 0000000..1e45904 --- /dev/null +++ b/src/patchpro_bot.egg-info/top_level.txt @@ -0,0 +1 @@ +patchpro_bot diff --git a/src/patchpro_bot/agent_core.py b/src/patchpro_bot/agent_core.py index c86ab57..e8f2374 100644 --- a/src/patchpro_bot/agent_core.py +++ b/src/patchpro_bot/agent_core.py @@ -1137,6 +1137,107 @@ def _setup_logging(self): ) +# ============================================================================ +# Backward Compatibility Aliases +# ============================================================================ +# These aliases ensure compatibility with code that imported from agent.py + +# Main agent class alias +PatchProAgent = AgentCore # Legacy name for AgentCore + +# Enum alias +class ModelProvider(Enum): + """Legacy enum for backward compatibility.""" + OPENAI = "openai" + + +# Helper function for backward compatibility +def load_source_files(findings, base_path: Path) -> Dict[str, str]: + """ + Legacy helper function for loading source files. + + Args: + findings: NormalizedFindings object + base_path: Base directory for resolving file paths + + Returns: + Dictionary mapping file paths to their contents + """ + from .analyzer import NormalizedFindings + + source_files = {} + unique_files = set(f.location.file for f in findings.findings) + + for file_path in unique_files: + try: + # Try to resolve the path + full_path = base_path / file_path + if not full_path.exists(): + # Try relative to current directory + full_path = Path(file_path) + + if full_path.exists() and full_path.is_file(): + source_files[file_path] = full_path.read_text(encoding='utf-8') + except Exception as e: + logger.warning(f"Could not load {file_path}: {e}") + + return source_files + + +# Legacy data classes for backward compatibility +@dataclass +class GeneratedFix: + """Legacy data class for backward compatibility.""" + finding_id: str + file_path: str + original_code: str + fixed_code: str + explanation: str + diff: str + confidence: str = "medium" + + +@dataclass +class AgentResult: + """Legacy data class for backward compatibility.""" + fixes: List[GeneratedFix] + summary: str + total_findings: int + fixes_generated: int + skipped: int + errors: List[str] + + +class PromptBuilder: + """Legacy class for backward compatibility with agent.py tests.""" + + SYSTEM_PROMPT = """You are PatchPro, an expert code repair assistant.""" + + @staticmethod + def build_fix_prompt(findings: List, file_contents: Dict[str, str]) -> str: + """Build prompt for generating fixes (legacy compatibility).""" + from .llm.prompts import PromptBuilder as NewPromptBuilder + + # Delegate to new prompt builder + findings_data = [] + for finding in findings: + findings_data.append({ + "id": finding.id, + "file": finding.location.file, + "line": finding.location.line, + "rule": finding.rule_id, + "message": finding.message, + "severity": finding.severity, + "category": finding.category, + }) + + return f"Analyze these {len(findings_data)} code issues and generate fixes." + + +# ============================================================================ +# Main Entry Point +# ============================================================================ + async def main(): """Main entry point for the enhanced agent with scalability features.""" # Load configuration from environment with scalability features diff --git a/src/patchpro_bot/analyzer.py b/src/patchpro_bot/analyzer.py new file mode 100644 index 0000000..b3b535e --- /dev/null +++ b/src/patchpro_bot/analyzer.py @@ -0,0 +1,533 @@ +""" +Analyzer module for normalizing static analysis findings from Ruff, Semgrep and other tools. +""" +import json +import hashlib +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional, Union +from dataclasses import dataclass, asdict +from enum import Enum + + +class Severity(Enum): + """Normalized severity levels.""" + ERROR = "error" + WARNING = "warning" + INFO = "info" + NOTE = "note" + + +class Category(Enum): + """Finding categories.""" + STYLE = "style" + SECURITY = "security" + PERFORMANCE = "performance" + CORRECTNESS = "correctness" + COMPLEXITY = "complexity" + IMPORT = "import" + TYPING = "typing" + + +@dataclass +class Position: + """Position in source code.""" + line: int + column: int + + +@dataclass +class Location: + """Location of a finding in source code.""" + file: str + line: int + column: int + end_line: Optional[int] = None + end_column: Optional[int] = None + + +@dataclass +class Replacement: + """Suggested code replacement.""" + start: Position + end: Position + content: str + + +@dataclass +class Suggestion: + """Suggested fix for a finding.""" + message: str + replacements: List[Replacement] = None + + def __post_init__(self): + if self.replacements is None: + self.replacements = [] + + +@dataclass +class Finding: + """Normalized static analysis finding.""" + id: str + rule_id: str + rule_name: str + message: str + severity: str + category: str + location: Location + source_tool: str + suggestion: Optional[Suggestion] = None + + +@dataclass +class Metadata: + """Metadata about the analysis run.""" + tool: str + version: str + total_findings: int + timestamp: Optional[str] = None + + def __post_init__(self): + if self.timestamp is None: + self.timestamp = datetime.utcnow().isoformat() + + +@dataclass +class NormalizedFindings: + """Container for normalized findings with metadata.""" + findings: List[Finding] + metadata: Metadata + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "findings": [asdict(finding) for finding in self.findings], + "metadata": asdict(self.metadata) + } + + def to_json(self, indent: int = 2) -> str: + """Convert to JSON string.""" + return json.dumps(self.to_dict(), indent=indent) + + def save(self, path: Union[str, Path]) -> None: + """Save to JSON file.""" + Path(path).write_text(self.to_json()) + + +class RuffNormalizer: + """Normalizes Ruff JSON output to unified schema.""" + + SEVERITY_MAP = { + "E": Severity.ERROR.value, + "W": Severity.WARNING.value, + "F": Severity.ERROR.value, # Flake8 errors + "I": Severity.INFO.value, # Import sorting + "N": Severity.WARNING.value, # Naming conventions + "UP": Severity.WARNING.value, # pyupgrade + "B": Severity.WARNING.value, # flake8-bugbear + "A": Severity.WARNING.value, # flake8-builtins + "COM": Severity.WARNING.value, # flake8-commas + "C4": Severity.WARNING.value, # flake8-comprehensions + "DTZ": Severity.WARNING.value, # flake8-datetimez + "T10": Severity.WARNING.value, # flake8-debugger + "DJ": Severity.WARNING.value, # flake8-django + "EM": Severity.WARNING.value, # flake8-errmsg + "EXE": Severity.WARNING.value, # flake8-executable + "FA": Severity.WARNING.value, # flake8-future-annotations + "ISC": Severity.WARNING.value, # flake8-implicit-str-concat + "ICN": Severity.WARNING.value, # flake8-import-conventions + "G": Severity.WARNING.value, # flake8-logging-format + "INP": Severity.WARNING.value, # flake8-no-pep420 + "PIE": Severity.WARNING.value, # flake8-pie + "T20": Severity.WARNING.value, # flake8-print + "PYI": Severity.WARNING.value, # flake8-pyi + "PT": Severity.WARNING.value, # flake8-pytest-style + "Q": Severity.WARNING.value, # flake8-quotes + "RSE": Severity.WARNING.value, # flake8-raise + "RET": Severity.WARNING.value, # flake8-return + "SLF": Severity.WARNING.value, # flake8-self + "SLOT": Severity.WARNING.value, # flake8-slots + "SIM": Severity.WARNING.value, # flake8-simplify + "TID": Severity.WARNING.value, # flake8-tidy-imports + "TCH": Severity.WARNING.value, # flake8-type-checking + "INT": Severity.WARNING.value, # flake8-gettext + "ARG": Severity.WARNING.value, # flake8-unused-arguments + "PTH": Severity.WARNING.value, # flake8-use-pathlib + "TD": Severity.INFO.value, # flake8-todos + "FIX": Severity.INFO.value, # flake8-fixme + "ERA": Severity.WARNING.value, # eradicate + "PD": Severity.WARNING.value, # pandas-vet + "PGH": Severity.WARNING.value, # pygrep-hooks + "PL": Severity.WARNING.value, # Pylint + "TRY": Severity.WARNING.value, # tryceratops + "FLY": Severity.WARNING.value, # flynt + "NPY": Severity.WARNING.value, # NumPy-specific rules + "AIR": Severity.WARNING.value, # Airflow + "PERF": Severity.WARNING.value, # Perflint + "FURB": Severity.WARNING.value, # refurb + "LOG": Severity.WARNING.value, # flake8-logging + "RUF": Severity.WARNING.value, # Ruff-specific rules + } + + CATEGORY_MAP = { + "E": Category.STYLE.value, # pycodestyle errors + "W": Category.STYLE.value, # pycodestyle warnings + "F": Category.CORRECTNESS.value, # Pyflakes + "I": Category.IMPORT.value, # isort + "N": Category.STYLE.value, # pep8-naming + "UP": Category.STYLE.value, # pyupgrade + "B": Category.CORRECTNESS.value, # flake8-bugbear + "A": Category.CORRECTNESS.value, # flake8-builtins + "COM": Category.STYLE.value, # flake8-commas + "C4": Category.STYLE.value, # flake8-comprehensions + "DTZ": Category.CORRECTNESS.value, # flake8-datetimez + "T10": Category.CORRECTNESS.value, # flake8-debugger + "DJ": Category.CORRECTNESS.value, # flake8-django + "EM": Category.STYLE.value, # flake8-errmsg + "EXE": Category.CORRECTNESS.value, # flake8-executable + "FA": Category.TYPING.value, # flake8-future-annotations + "ISC": Category.STYLE.value, # flake8-implicit-str-concat + "ICN": Category.IMPORT.value, # flake8-import-conventions + "G": Category.STYLE.value, # flake8-logging-format + "INP": Category.IMPORT.value, # flake8-no-pep420 + "PIE": Category.CORRECTNESS.value, # flake8-pie + "T20": Category.STYLE.value, # flake8-print + "PYI": Category.TYPING.value, # flake8-pyi + "PT": Category.STYLE.value, # flake8-pytest-style + "Q": Category.STYLE.value, # flake8-quotes + "RSE": Category.CORRECTNESS.value, # flake8-raise + "RET": Category.CORRECTNESS.value, # flake8-return + "SLF": Category.CORRECTNESS.value, # flake8-self + "SLOT": Category.PERFORMANCE.value, # flake8-slots + "SIM": Category.STYLE.value, # flake8-simplify + "TID": Category.IMPORT.value, # flake8-tidy-imports + "TCH": Category.TYPING.value, # flake8-type-checking + "INT": Category.CORRECTNESS.value, # flake8-gettext + "ARG": Category.CORRECTNESS.value, # flake8-unused-arguments + "PTH": Category.STYLE.value, # flake8-use-pathlib + "TD": Category.STYLE.value, # flake8-todos + "FIX": Category.STYLE.value, # flake8-fixme + "ERA": Category.STYLE.value, # eradicate + "PD": Category.CORRECTNESS.value, # pandas-vet + "PGH": Category.CORRECTNESS.value, # pygrep-hooks + "PL": Category.CORRECTNESS.value, # Pylint + "TRY": Category.CORRECTNESS.value, # tryceratops + "FLY": Category.STYLE.value, # flynt + "NPY": Category.CORRECTNESS.value, # NumPy-specific rules + "AIR": Category.CORRECTNESS.value, # Airflow + "PERF": Category.PERFORMANCE.value, # Perflint + "FURB": Category.STYLE.value, # refurb + "LOG": Category.CORRECTNESS.value, # flake8-logging + "RUF": Category.STYLE.value, # Ruff-specific rules + } + + def normalize(self, ruff_output: Union[str, Dict, List]) -> NormalizedFindings: + """Normalize Ruff JSON output.""" + if isinstance(ruff_output, str): + data = json.loads(ruff_output) + else: + data = ruff_output + + # Ruff outputs a list of findings directly + if not isinstance(data, list): + raise ValueError("Expected Ruff output to be a list of findings") + + findings = [] + for item in data: + finding = self._convert_ruff_finding(item) + if finding: + findings.append(finding) + + metadata = Metadata( + tool="ruff", + version="0.5.7", # From pyproject.toml + total_findings=len(findings) + ) + + return NormalizedFindings(findings=findings, metadata=metadata) + + def _convert_ruff_finding(self, ruff_finding: Dict) -> Optional[Finding]: + """Convert a single Ruff finding to normalized format.""" + try: + # Extract rule code and determine severity/category + rule_code = ruff_finding["code"] + rule_prefix = rule_code.split("-")[0] if "-" in rule_code else rule_code[:1] + + severity = self.SEVERITY_MAP.get(rule_prefix, Severity.WARNING.value) + category = self.CATEGORY_MAP.get(rule_prefix, Category.CORRECTNESS.value) + + # Create location + location = Location( + file=ruff_finding["filename"], + line=ruff_finding["location"]["row"], + column=ruff_finding["location"]["column"], + end_line=ruff_finding["end_location"]["row"] if ruff_finding.get("end_location") else None, + end_column=ruff_finding["end_location"]["column"] if ruff_finding.get("end_location") else None + ) + + # Generate unique ID + finding_id = self._generate_id(rule_code, location) + + # Handle fix/suggestion if present + suggestion = None + if ruff_finding.get("fix"): + suggestion = self._convert_ruff_fix(ruff_finding["fix"]) + + return Finding( + id=finding_id, + rule_id=rule_code, + rule_name=ruff_finding.get("message", "").split(":")[0] if ":" in ruff_finding.get("message", "") else rule_code, + message=ruff_finding["message"], + severity=severity, + category=category, + location=location, + source_tool="ruff", + suggestion=suggestion + ) + + except KeyError as e: + print(f"Warning: Skipping malformed Ruff finding, missing key: {e}") + return None + + def _convert_ruff_fix(self, fix_data: Dict) -> Optional[Suggestion]: + """Convert Ruff fix data to suggestion format.""" + if not fix_data.get("edits"): + return None + + replacements = [] + for edit in fix_data["edits"]: + replacement = Replacement( + start=Position( + line=edit["location"]["row"], + column=edit["location"]["column"] + ), + end=Position( + line=edit["end_location"]["row"], + column=edit["end_location"]["column"] + ), + content=edit["content"] + ) + replacements.append(replacement) + + return Suggestion( + message=fix_data.get("message", "Auto-fix available"), + replacements=replacements + ) + + def _generate_id(self, rule_code: str, location: Location) -> str: + """Generate unique ID for a finding.""" + content = f"{rule_code}:{location.file}:{location.line}:{location.column}" + return hashlib.md5(content.encode()).hexdigest()[:12] + + +class SemgrepNormalizer: + """Normalizes Semgrep JSON output to unified schema.""" + + SEVERITY_MAP = { + "ERROR": Severity.ERROR.value, + "WARNING": Severity.WARNING.value, + "INFO": Severity.INFO.value, + "HIGH": Severity.ERROR.value, + "MEDIUM": Severity.WARNING.value, + "LOW": Severity.INFO.value, + } + + def normalize(self, semgrep_output: Union[str, Dict]) -> NormalizedFindings: + """Normalize Semgrep JSON output.""" + if isinstance(semgrep_output, str): + data = json.loads(semgrep_output) + else: + data = semgrep_output + + if not isinstance(data, dict) or "results" not in data: + raise ValueError("Expected Semgrep output to have 'results' key") + + findings = [] + for item in data["results"]: + finding = self._convert_semgrep_finding(item) + if finding: + findings.append(finding) + + metadata = Metadata( + tool="semgrep", + version="1.84.0", # From pyproject.toml + total_findings=len(findings) + ) + + return NormalizedFindings(findings=findings, metadata=metadata) + + def _convert_semgrep_finding(self, semgrep_finding: Dict) -> Optional[Finding]: + """Convert a single Semgrep finding to normalized format.""" + try: + # Extract rule information + check_id = semgrep_finding.get("check_id", "") + rule_id = check_id.split(".")[-1] if "." in check_id else check_id + + # Map severity + severity_raw = semgrep_finding.get("extra", {}).get("severity", "WARNING").upper() + severity = self.SEVERITY_MAP.get(severity_raw, Severity.WARNING.value) + + # Determine category based on rule ID patterns + category = self._determine_category(check_id) + + # Create location + start_pos = semgrep_finding.get("start", {}) + end_pos = semgrep_finding.get("end", {}) + + location = Location( + file=semgrep_finding.get("path", ""), + line=start_pos.get("line", 1), + column=start_pos.get("col", 1), + end_line=end_pos.get("line"), + end_column=end_pos.get("col") + ) + + # Generate unique ID + finding_id = self._generate_id(check_id, location) + + # Handle suggestions if present + suggestion = None + extra = semgrep_finding.get("extra", {}) + if extra.get("fix"): + suggestion = Suggestion(message=f"Fix available: {extra.get('fix')}") + + return Finding( + id=finding_id, + rule_id=rule_id, + rule_name=semgrep_finding.get("extra", {}).get("metadata", {}).get("shortDescription", rule_id), + message=semgrep_finding.get("extra", {}).get("message", semgrep_finding.get("message", "")), + severity=severity, + category=category, + location=location, + source_tool="semgrep", + suggestion=suggestion + ) + + except Exception as e: + print(f"Warning: Skipping malformed Semgrep finding: {e}") + return None + + def _determine_category(self, check_id: str) -> str: + """Determine category based on Semgrep rule ID.""" + check_id_lower = check_id.lower() + + if "security" in check_id_lower or "crypto" in check_id_lower or "auth" in check_id_lower: + return Category.SECURITY.value + elif "performance" in check_id_lower or "perf" in check_id_lower: + return Category.PERFORMANCE.value + elif "style" in check_id_lower or "format" in check_id_lower: + return Category.STYLE.value + elif "import" in check_id_lower: + return Category.IMPORT.value + elif "type" in check_id_lower or "typing" in check_id_lower: + return Category.TYPING.value + elif "complexity" in check_id_lower or "complex" in check_id_lower: + return Category.COMPLEXITY.value + else: + return Category.CORRECTNESS.value + + def _generate_id(self, check_id: str, location: Location) -> str: + """Generate unique ID for a finding.""" + content = f"{check_id}:{location.file}:{location.line}:{location.column}" + return hashlib.md5(content.encode()).hexdigest()[:12] + + +class FindingsAnalyzer: + """Main analyzer class for normalizing and processing findings.""" + + def __init__(self): + self.ruff_normalizer = RuffNormalizer() + self.semgrep_normalizer = SemgrepNormalizer() + + def normalize_findings(self, tool_outputs: Dict[str, Union[str, Dict, List]]) -> List[NormalizedFindings]: + """Normalize findings from multiple tools.""" + results = [] + + for tool_name, output in tool_outputs.items(): + if tool_name.lower() == "ruff": + normalized = self.ruff_normalizer.normalize(output) + results.append(normalized) + elif tool_name.lower() == "semgrep": + normalized = self.semgrep_normalizer.normalize(output) + results.append(normalized) + else: + print(f"Warning: Unknown tool '{tool_name}', skipping normalization") + + return results + + def merge_findings(self, normalized_results: List[NormalizedFindings]) -> NormalizedFindings: + """Merge findings from multiple tools, removing duplicates.""" + all_findings = [] + tools_used = [] + total_findings = 0 + + for result in normalized_results: + all_findings.extend(result.findings) + tools_used.append(result.metadata.tool) + total_findings += result.metadata.total_findings + + # Remove duplicates based on location and rule similarity + deduplicated = self._deduplicate_findings(all_findings) + + metadata = Metadata( + tool=",".join(set(tools_used)), + version="merged", + total_findings=len(deduplicated) + ) + + return NormalizedFindings(findings=deduplicated, metadata=metadata) + + def _deduplicate_findings(self, findings: List[Finding]) -> List[Finding]: + """Remove duplicate findings based on location and rule.""" + seen = set() + deduplicated = [] + + for finding in findings: + # Create a key based on location and rule + key = ( + finding.location.file, + finding.location.line, + finding.location.column, + finding.rule_id + ) + + if key not in seen: + seen.add(key) + deduplicated.append(finding) + + return deduplicated + + def load_and_normalize(self, analysis_dir: Union[str, Path]) -> NormalizedFindings: + """Load analysis results from directory and normalize them.""" + analysis_path = Path(analysis_dir) + tool_outputs = {} + + # Look for known tool outputs + for json_file in analysis_path.glob("*.json"): + filename = json_file.stem.lower() + + try: + content = json.loads(json_file.read_text()) + + # Try to identify tool by filename or content structure + if "ruff" in filename or (isinstance(content, list) and content and "code" in content[0]): + tool_outputs["ruff"] = content + elif "semgrep" in filename or (isinstance(content, dict) and "results" in content): + tool_outputs["semgrep"] = content + else: + print(f"Warning: Could not identify tool for {json_file}") + + except Exception as e: + print(f"Warning: Failed to load {json_file}: {e}") + + # Normalize and merge + normalized_results = self.normalize_findings(tool_outputs) + + if len(normalized_results) == 1: + return normalized_results[0] + elif len(normalized_results) > 1: + return self.merge_findings(normalized_results) + else: + # Return empty results + metadata = Metadata(tool="none", version="0.0.0", total_findings=0) + return NormalizedFindings(findings=[], metadata=metadata) \ No newline at end of file diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 0000000..047224d --- /dev/null +++ b/tests/test_agent.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Quick test of the PatchPro agent module. +This verifies the module can be imported and basic functionality works. +""" +import sys +from pathlib import Path + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +def test_imports(): + """Test that all modules can be imported.""" + print("Testing imports...") + try: + from patchpro_bot.agent_core import ( + PatchProAgent, + AgentConfig, + ModelProvider, + GeneratedFix, + AgentResult, + PromptBuilder, + load_source_files + ) + print("✅ All imports successful!") + return True + except ImportError as e: + print(f"❌ Import failed: {e}") + return False + +def test_config(): + """Test AgentConfig creation (without API key).""" + print("\nTesting AgentConfig...") + try: + from patchpro_bot.agent_core import AgentConfig, ModelProvider + + # Test with dummy API key - using new AgentConfig structure + config = AgentConfig( + openai_api_key="test-key", + llm_model="gpt-4o-mini", + max_tokens=1000 + ) + + assert config.llm_model == "gpt-4o-mini" + assert config.max_tokens == 1000 + assert config.temperature == 0.1 + print("✅ AgentConfig creation successful!") + return True + except Exception as e: + print(f"❌ Config test failed: {e}") + return False + +def test_prompt_builder(): + """Test PromptBuilder functionality.""" + print("\nTesting PromptBuilder...") + try: + from patchpro_bot.agent_core import PromptBuilder + from patchpro_bot.analyzer import Finding, Location + + # Create a sample finding + finding = Finding( + id="test123", + rule_id="E501", + rule_name="line-too-long", + message="Line too long (100 > 88 characters)", + severity="warning", + category="style", + location=Location( + file="test.py", + line=10, + column=1 + ), + source_tool="ruff" + ) + + # Test prompt building + file_contents = { + "test.py": "# This is a test\n" * 20 + } + + prompt = PromptBuilder.build_fix_prompt([finding], file_contents) + + assert "test123" in prompt + assert "E501" in prompt + assert len(prompt) > 100 + + print("✅ PromptBuilder working correctly!") + return True + except Exception as e: + print(f"❌ PromptBuilder test failed: {e}") + import traceback + traceback.print_exc() + return False + +def test_cli_integration(): + """Test that CLI can import agent module.""" + print("\nTesting CLI integration...") + try: + from patchpro_bot.cli import app + + # Check that agent command exists + commands = [cmd.name for cmd in app.registered_commands] + assert "agent" in commands + + print("✅ CLI integration successful!") + return True + except Exception as e: + print(f"❌ CLI integration test failed: {e}") + return False + +def main(): + """Run all tests.""" + print("=" * 60) + print("PatchPro Agent Module Tests") + print("=" * 60) + + results = [] + + results.append(("Imports", test_imports())) + results.append(("Config", test_config())) + results.append(("PromptBuilder", test_prompt_builder())) + results.append(("CLI Integration", test_cli_integration())) + + print("\n" + "=" * 60) + print("Test Results Summary") + print("=" * 60) + + for test_name, passed in results: + status = "✅ PASS" if passed else "❌ FAIL" + print(f"{test_name:.<30} {status}") + + total = len(results) + passed = sum(1 for _, p in results if p) + + print(f"\n{passed}/{total} tests passed") + + if passed == total: + print("\n🎉 All tests passed! Agent module is ready to use.") + return 0 + else: + print("\n⚠️ Some tests failed. Please review errors above.") + return 1 + +if __name__ == "__main__": + sys.exit(main())