Spaces:
Running
Running
Commit Β·
cd3b358
0
Parent(s):
π§ Initial commit: FixFlow β Autonomous Bug Resolution Agent
Browse files- 5-step pipeline: Issue parsing β Codebase mapping β Root cause analysis β Fix generation β PR description
- GLM 5.1 (Z.ai) via OpenAI-compatible API with streaming support
- PyGithub integration: fetch issues, repo trees, file contents
- Python difflib unified diff generation
- Streamlit dark UI with glassmorphism design
- Confidence self-evaluation (optional)
- Export: full Markdown report + .diff patch
- Demo output for FastAPI issue included
- .env.example +21 -0
- .gitignore +38 -0
- README.md +179 -0
- app.py +899 -0
- backend/__init__.py +1 -0
- backend/agent.py +495 -0
- backend/code_indexer.py +166 -0
- backend/config.py +49 -0
- backend/diff_generator.py +165 -0
- backend/github_client.py +243 -0
- backend/llm_client.py +99 -0
- backend/prompts.py +294 -0
- demo/example_output.md +151 -0
- requirements.txt +5 -0
.env.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FixFlow Environment Variables
|
| 2 |
+
# Copy this file to .env and fill in your API keys
|
| 3 |
+
|
| 4 |
+
# Z.ai / GLM API Key (get from https://open.bigmodel.cn/)
|
| 5 |
+
GLM_API_KEY=your_glm_api_key_here
|
| 6 |
+
|
| 7 |
+
# GitHub Personal Access Token (optional, for private repos & higher rate limits)
|
| 8 |
+
# Generate at: https://github.com/settings/tokens
|
| 9 |
+
GITHUB_TOKEN=your_github_token_here
|
| 10 |
+
|
| 11 |
+
# GLM Model (options: glm-5-plus, glm-4-plus, glm-4)
|
| 12 |
+
GLM_MODEL=glm-5-plus
|
| 13 |
+
|
| 14 |
+
# GLM API Base URL
|
| 15 |
+
GLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4
|
| 16 |
+
|
| 17 |
+
# Max files to analyze per repo
|
| 18 |
+
MAX_FILES_TO_SCAN=100
|
| 19 |
+
|
| 20 |
+
# Max file size in bytes to read (50KB default)
|
| 21 |
+
MAX_FILE_SIZE_BYTES=51200
|
.gitignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
.Python
|
| 7 |
+
*.egg
|
| 8 |
+
*.egg-info/
|
| 9 |
+
dist/
|
| 10 |
+
build/
|
| 11 |
+
.eggs/
|
| 12 |
+
|
| 13 |
+
# Virtual environments
|
| 14 |
+
.venv/
|
| 15 |
+
venv/
|
| 16 |
+
env/
|
| 17 |
+
ENV/
|
| 18 |
+
|
| 19 |
+
# Environment variables
|
| 20 |
+
.env
|
| 21 |
+
|
| 22 |
+
# Streamlit
|
| 23 |
+
.streamlit/
|
| 24 |
+
|
| 25 |
+
# macOS
|
| 26 |
+
.DS_Store
|
| 27 |
+
|
| 28 |
+
# IDE
|
| 29 |
+
.idea/
|
| 30 |
+
.vscode/
|
| 31 |
+
*.swp
|
| 32 |
+
*.swo
|
| 33 |
+
|
| 34 |
+
# Logs
|
| 35 |
+
*.log
|
| 36 |
+
|
| 37 |
+
# Jupyter
|
| 38 |
+
.ipynb_checkpoints/
|
README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# π§ FixFlow β Autonomous Bug Resolution Agent
|
| 2 |
+
|
| 3 |
+
<div align="center">
|
| 4 |
+
|
| 5 |
+
[](https://python.org)
|
| 6 |
+
[](https://streamlit.io)
|
| 7 |
+
[](https://open.bigmodel.cn)
|
| 8 |
+
[](LICENSE)
|
| 9 |
+
|
| 10 |
+
**Give FixFlow a GitHub issue. Get back a root cause analysis + a PR-ready fix.**
|
| 11 |
+
|
| 12 |
+
*Built with GLM 5.1 by Z.ai β‘*
|
| 13 |
+
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## β¨ Features
|
| 19 |
+
|
| 20 |
+
| Feature | Description |
|
| 21 |
+
|---------|-------------|
|
| 22 |
+
| π **Smart Issue Parsing** | Extracts error messages, reproduction steps, and technical clues from any GitHub issue |
|
| 23 |
+
| πΊοΈ **Codebase Mapping** | Identifies the top 5-10 most suspect files from the entire repo tree |
|
| 24 |
+
| π§ **Chain-of-Thought Reasoning** | Traces execution flow step-by-step, citing file names, functions, and line numbers |
|
| 25 |
+
| π¬ **Root Cause Analysis** | Pinpoints the exact bug location with high-confidence reasoning |
|
| 26 |
+
| π§ **Fix Generation** | Generates minimal, precise code changes as unified diffs |
|
| 27 |
+
| π **PR Description** | Writes a complete, reviewer-friendly pull request description |
|
| 28 |
+
| π― **Confidence Score** | Optional self-evaluation step where GLM rates its own certainty |
|
| 29 |
+
| π€ **Export** | Download the full analysis report as Markdown or the patch as `.diff` |
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## π Quick Start
|
| 34 |
+
|
| 35 |
+
### 1. Clone & Install
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
git clone https://github.com/your-username/fixflow.git
|
| 39 |
+
cd fixflow
|
| 40 |
+
|
| 41 |
+
# Create virtual environment
|
| 42 |
+
python -m venv .venv
|
| 43 |
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
| 44 |
+
|
| 45 |
+
# Install dependencies
|
| 46 |
+
pip install -r requirements.txt
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### 2. Configure API Keys
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
cp .env.example .env
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
Edit `.env`:
|
| 56 |
+
|
| 57 |
+
```env
|
| 58 |
+
GLM_API_KEY=your_glm_api_key_here # Get from https://open.bigmodel.cn/
|
| 59 |
+
GITHUB_TOKEN=ghp_your_token_here # Optional, but recommended
|
| 60 |
+
GLM_MODEL=glm-5-plus
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
### 3. Run
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
streamlit run app.py
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
Open [http://localhost:8501](http://localhost:8501) π
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## π How It Works
|
| 74 |
+
|
| 75 |
+
```
|
| 76 |
+
GitHub Issue URL
|
| 77 |
+
β
|
| 78 |
+
βΌ
|
| 79 |
+
βββββββββββββββββββ
|
| 80 |
+
β 1. Parse Issue β βββ Extract: error, repro steps, affected components
|
| 81 |
+
ββββββββββ¬βββββββββ
|
| 82 |
+
β
|
| 83 |
+
βΌ
|
| 84 |
+
βββββββββββββββββββ
|
| 85 |
+
β 2. Map Codebase β βββ Scan repo tree β Rank top 5-10 suspect files
|
| 86 |
+
ββββββββββ¬βββββββββ
|
| 87 |
+
β
|
| 88 |
+
βΌ
|
| 89 |
+
βββββββββββββββββββ
|
| 90 |
+
β 3. Analyze Code β βββ Read files β Chain-of-thought root cause tracing
|
| 91 |
+
ββββββββββ¬βββββββββ
|
| 92 |
+
β
|
| 93 |
+
βΌ
|
| 94 |
+
βββββββββββββββββββ
|
| 95 |
+
β 4. Generate Fix β βββ Produce corrected file versions (minimal changes)
|
| 96 |
+
ββββββββββ¬βββββββββ
|
| 97 |
+
β
|
| 98 |
+
βΌ
|
| 99 |
+
βββββββββββββββββββ
|
| 100 |
+
β 5. Write PR β βββ Unified diff + human-readable PR description
|
| 101 |
+
βββββββββββββββββββ
|
| 102 |
+
β
|
| 103 |
+
βΌ
|
| 104 |
+
π Full Report + π¦ Patch File
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## π§ͺ Example Output
|
| 110 |
+
|
| 111 |
+
See [`demo/example_output.md`](demo/example_output.md) for a full sample analysis on a real FastAPI issue.
|
| 112 |
+
|
| 113 |
+
Quick preview:
|
| 114 |
+
|
| 115 |
+
```
|
| 116 |
+
π¬ Root Cause:
|
| 117 |
+
In fastapi/_compat.py ~line 215, _get_value() calls model_dump()
|
| 118 |
+
without passing `include=include` in the Pydantic v2 branch.
|
| 119 |
+
The fix: add include=include, exclude=exclude to model_dump().
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
## π Project Structure
|
| 125 |
+
|
| 126 |
+
```
|
| 127 |
+
fixflow/
|
| 128 |
+
βββ app.py # Streamlit frontend (dark UI, streaming output)
|
| 129 |
+
βββ backend/
|
| 130 |
+
β βββ __init__.py
|
| 131 |
+
β βββ config.py # API keys, model config, constants
|
| 132 |
+
β βββ github_client.py # Fetch issues, repo trees, file contents
|
| 133 |
+
β βββ code_indexer.py # Parse repo structure, format for LLM
|
| 134 |
+
β βββ agent.py # Core 5-step reasoning agent orchestrator
|
| 135 |
+
β βββ prompts.py # All LLM prompt templates
|
| 136 |
+
β βββ diff_generator.py # Generate unified diffs from proposed changes
|
| 137 |
+
β βββ llm_client.py # GLM 5.1 API wrapper (sync + streaming)
|
| 138 |
+
βββ requirements.txt
|
| 139 |
+
βββ .env.example
|
| 140 |
+
βββ README.md
|
| 141 |
+
βββ demo/
|
| 142 |
+
βββ example_output.md # Sample output for showcase
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## βοΈ Configuration
|
| 148 |
+
|
| 149 |
+
| Variable | Default | Description |
|
| 150 |
+
|----------|---------|-------------|
|
| 151 |
+
| `GLM_API_KEY` | β | Your Z.ai API key (required) |
|
| 152 |
+
| `GITHUB_TOKEN` | β | GitHub PAT (optional, recommended) |
|
| 153 |
+
| `GLM_MODEL` | `glm-5-plus` | GLM model to use |
|
| 154 |
+
| `GLM_BASE_URL` | `https://open.bigmodel.cn/api/paas/v4` | API endpoint |
|
| 155 |
+
| `MAX_FILES_TO_SCAN` | `100` | Max files to include in repo scan |
|
| 156 |
+
| `MAX_FILE_SIZE_BYTES` | `51200` | Max file size to read (50 KB) |
|
| 157 |
+
|
| 158 |
+
---
|
| 159 |
+
|
| 160 |
+
## π οΈ Tech Stack
|
| 161 |
+
|
| 162 |
+
- **Frontend:** Streamlit with custom dark CSS (glassmorphism design)
|
| 163 |
+
- **Backend:** Python 3.11+, FastAPI-compatible architecture
|
| 164 |
+
- **LLM:** GLM 5.1 via Z.ai API (OpenAI-compatible endpoint)
|
| 165 |
+
- **GitHub:** PyGithub + GitHub REST API
|
| 166 |
+
- **Diffs:** Python `difflib` (unified diff format)
|
| 167 |
+
|
| 168 |
+
---
|
| 169 |
+
|
| 170 |
+
## π License
|
| 171 |
+
|
| 172 |
+
MIT License β see [LICENSE](LICENSE) for details.
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
<div align="center">
|
| 177 |
+
Built with β€οΈ for the Z.ai GLM 5.1 Hackathon<br>
|
| 178 |
+
<b>Powered by GLM 5.1 by Z.ai β‘</b>
|
| 179 |
+
</div>
|
app.py
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FixFlow β Streamlit Frontend
|
| 3 |
+
Autonomous Bug Resolution Agent powered by GLM 5.1 (Z.ai)
|
| 4 |
+
"""
|
| 5 |
+
import time
|
| 6 |
+
import logging
|
| 7 |
+
import threading
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
import streamlit as st
|
| 11 |
+
|
| 12 |
+
from backend.agent import AgentResult, FixFlowAgent, generate_full_report
|
| 13 |
+
from backend.config import GLM_MODEL, GLM_BASE_URL
|
| 14 |
+
from backend.github_client import GitHubClient
|
| 15 |
+
from backend.llm_client import GLMClient
|
| 16 |
+
|
| 17 |
+
# ββ Logging Setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
+
logging.basicConfig(
|
| 19 |
+
level=logging.INFO,
|
| 20 |
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
| 21 |
+
)
|
| 22 |
+
logger = logging.getLogger("fixflow.app")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ββ Page Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
st.set_page_config(
|
| 27 |
+
page_title="FixFlow β Autonomous Bug Resolution Agent",
|
| 28 |
+
page_icon="π§",
|
| 29 |
+
layout="wide",
|
| 30 |
+
initial_sidebar_state="expanded",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# ββ Custom CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
+
st.markdown("""
|
| 36 |
+
<style>
|
| 37 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
| 38 |
+
|
| 39 |
+
/* ββ Root & Base βββββββββββ */
|
| 40 |
+
:root {
|
| 41 |
+
--bg-primary: #0a0b0f;
|
| 42 |
+
--bg-secondary: #12141a;
|
| 43 |
+
--bg-card: #1a1c24;
|
| 44 |
+
--bg-card-hover: #1e2028;
|
| 45 |
+
--accent-primary: #6c63ff;
|
| 46 |
+
--accent-secondary: #a78bfa;
|
| 47 |
+
--accent-green: #10b981;
|
| 48 |
+
--accent-red: #ef4444;
|
| 49 |
+
--accent-yellow: #f59e0b;
|
| 50 |
+
--accent-blue: #3b82f6;
|
| 51 |
+
--text-primary: #f0f0ff;
|
| 52 |
+
--text-secondary: #9ca3af;
|
| 53 |
+
--text-muted: #6b7280;
|
| 54 |
+
--border: #2a2c36;
|
| 55 |
+
--border-bright: #3a3c48;
|
| 56 |
+
--shadow-glow: 0 0 40px rgba(108, 99, 255, 0.15);
|
| 57 |
+
--radius: 12px;
|
| 58 |
+
--radius-sm: 8px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Global font */
|
| 62 |
+
html, body, [class*="css"] {
|
| 63 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 64 |
+
color: var(--text-primary);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* Dark background */
|
| 68 |
+
.stApp {
|
| 69 |
+
background: var(--bg-primary);
|
| 70 |
+
background-image: radial-gradient(ellipse at 20% 10%, rgba(108, 99, 255, 0.08) 0%, transparent 60%),
|
| 71 |
+
radial-gradient(ellipse at 80% 90%, rgba(167, 139, 250, 0.05) 0%, transparent 60%);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* Sidebar */
|
| 75 |
+
section[data-testid="stSidebar"] {
|
| 76 |
+
background: var(--bg-secondary) !important;
|
| 77 |
+
border-right: 1px solid var(--border) !important;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
section[data-testid="stSidebar"] > div {
|
| 81 |
+
padding: 1.5rem 1.2rem;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* ββ Logo / Header βββββββββ */
|
| 85 |
+
.fixflow-header {
|
| 86 |
+
text-align: center;
|
| 87 |
+
padding: 2rem 1rem 1rem;
|
| 88 |
+
margin-bottom: 1.5rem;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.fixflow-logo {
|
| 92 |
+
font-size: 3.5rem;
|
| 93 |
+
margin-bottom: 0.5rem;
|
| 94 |
+
display: block;
|
| 95 |
+
filter: drop-shadow(0 0 20px rgba(108, 99, 255, 0.5));
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.fixflow-title {
|
| 99 |
+
font-size: 2.2rem;
|
| 100 |
+
font-weight: 800;
|
| 101 |
+
background: linear-gradient(135deg, #6c63ff 0%, #a78bfa 50%, #60a5fa 100%);
|
| 102 |
+
-webkit-background-clip: text;
|
| 103 |
+
-webkit-text-fill-color: transparent;
|
| 104 |
+
background-clip: text;
|
| 105 |
+
letter-spacing: -0.02em;
|
| 106 |
+
line-height: 1.2;
|
| 107 |
+
margin-bottom: 0.4rem;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.fixflow-subtitle {
|
| 111 |
+
color: var(--text-secondary);
|
| 112 |
+
font-size: 1rem;
|
| 113 |
+
font-weight: 400;
|
| 114 |
+
margin-bottom: 1rem;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.powered-badge {
|
| 118 |
+
display: inline-flex;
|
| 119 |
+
align-items: center;
|
| 120 |
+
gap: 0.4rem;
|
| 121 |
+
background: linear-gradient(135deg, rgba(108, 99, 255, 0.15), rgba(167, 139, 250, 0.1));
|
| 122 |
+
border: 1px solid rgba(108, 99, 255, 0.3);
|
| 123 |
+
border-radius: 100px;
|
| 124 |
+
padding: 0.3rem 0.9rem;
|
| 125 |
+
font-size: 0.78rem;
|
| 126 |
+
font-weight: 600;
|
| 127 |
+
color: var(--accent-secondary);
|
| 128 |
+
letter-spacing: 0.04em;
|
| 129 |
+
text-transform: uppercase;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* ββ Cards βββββββββββββββββ */
|
| 133 |
+
.pipeline-card {
|
| 134 |
+
background: var(--bg-card);
|
| 135 |
+
border: 1px solid var(--border);
|
| 136 |
+
border-radius: var(--radius);
|
| 137 |
+
padding: 1.5rem;
|
| 138 |
+
margin-bottom: 1rem;
|
| 139 |
+
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
| 140 |
+
position: relative;
|
| 141 |
+
overflow: hidden;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.pipeline-card::before {
|
| 145 |
+
content: '';
|
| 146 |
+
position: absolute;
|
| 147 |
+
top: 0; left: 0; right: 0;
|
| 148 |
+
height: 2px;
|
| 149 |
+
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
| 150 |
+
opacity: 0;
|
| 151 |
+
transition: opacity 0.3s ease;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.pipeline-card:hover::before { opacity: 1; }
|
| 155 |
+
.pipeline-card:hover {
|
| 156 |
+
border-color: var(--border-bright);
|
| 157 |
+
box-shadow: var(--shadow-glow);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/* ββ Step Status Indicators β */
|
| 161 |
+
.step-indicator {
|
| 162 |
+
display: flex;
|
| 163 |
+
align-items: center;
|
| 164 |
+
gap: 0.75rem;
|
| 165 |
+
padding: 0.8rem 1rem;
|
| 166 |
+
border-radius: var(--radius-sm);
|
| 167 |
+
margin-bottom: 0.5rem;
|
| 168 |
+
font-size: 0.9rem;
|
| 169 |
+
font-weight: 500;
|
| 170 |
+
border: 1px solid transparent;
|
| 171 |
+
transition: all 0.3s ease;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.step-idle {
|
| 175 |
+
background: rgba(107, 114, 128, 0.08);
|
| 176 |
+
border-color: rgba(107, 114, 128, 0.15);
|
| 177 |
+
color: var(--text-muted);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.step-running {
|
| 181 |
+
background: rgba(59, 130, 246, 0.1);
|
| 182 |
+
border-color: rgba(59, 130, 246, 0.3);
|
| 183 |
+
color: #60a5fa;
|
| 184 |
+
animation: pulse-blue 2s infinite;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.step-complete {
|
| 188 |
+
background: rgba(16, 185, 129, 0.08);
|
| 189 |
+
border-color: rgba(16, 185, 129, 0.25);
|
| 190 |
+
color: var(--accent-green);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.step-error {
|
| 194 |
+
background: rgba(239, 68, 68, 0.08);
|
| 195 |
+
border-color: rgba(239, 68, 68, 0.25);
|
| 196 |
+
color: var(--accent-red);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
@keyframes pulse-blue {
|
| 200 |
+
0%, 100% { opacity: 1; }
|
| 201 |
+
50% { opacity: 0.7; }
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.step-icon { font-size: 1.1rem; }
|
| 205 |
+
.step-time { margin-left: auto; font-size: 0.75rem; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; }
|
| 206 |
+
|
| 207 |
+
/* ββ Input Fields ββββββββββ */
|
| 208 |
+
.stTextInput > div > div > input,
|
| 209 |
+
.stTextArea > div > div > textarea {
|
| 210 |
+
background: var(--bg-card) !important;
|
| 211 |
+
border: 1px solid var(--border) !important;
|
| 212 |
+
border-radius: var(--radius-sm) !important;
|
| 213 |
+
color: var(--text-primary) !important;
|
| 214 |
+
font-family: 'Inter', sans-serif !important;
|
| 215 |
+
transition: border-color 0.2s !important;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.stTextInput > div > div > input:focus,
|
| 219 |
+
.stTextArea > div > div > textarea:focus {
|
| 220 |
+
border-color: var(--accent-primary) !important;
|
| 221 |
+
box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.12) !important;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/* ββ Analyze Button ββββββββ */
|
| 225 |
+
.stButton > button[kind="primary"] {
|
| 226 |
+
background: linear-gradient(135deg, #6c63ff, #a78bfa) !important;
|
| 227 |
+
border: none !important;
|
| 228 |
+
border-radius: 10px !important;
|
| 229 |
+
font-family: 'Inter', sans-serif !important;
|
| 230 |
+
font-weight: 700 !important;
|
| 231 |
+
font-size: 1rem !important;
|
| 232 |
+
padding: 0.7rem 2rem !important;
|
| 233 |
+
letter-spacing: 0.02em !important;
|
| 234 |
+
transition: all 0.3s ease !important;
|
| 235 |
+
box-shadow: 0 4px 24px rgba(108, 99, 255, 0.35) !important;
|
| 236 |
+
color: white !important;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.stButton > button[kind="primary"]:hover {
|
| 240 |
+
transform: translateY(-2px) !important;
|
| 241 |
+
box-shadow: 0 8px 32px rgba(108, 99, 255, 0.5) !important;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.stButton > button[kind="primary"]:active {
|
| 245 |
+
transform: translateY(0) !important;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
/* Secondary buttons */
|
| 249 |
+
.stButton > button[kind="secondary"] {
|
| 250 |
+
background: var(--bg-card) !important;
|
| 251 |
+
border: 1px solid var(--border-bright) !important;
|
| 252 |
+
border-radius: var(--radius-sm) !important;
|
| 253 |
+
color: var(--text-secondary) !important;
|
| 254 |
+
font-family: 'Inter', sans-serif !important;
|
| 255 |
+
font-weight: 500 !important;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
/* ββ Expander ββββββββββββββ */
|
| 259 |
+
.streamlit-expanderHeader {
|
| 260 |
+
background: var(--bg-card) !important;
|
| 261 |
+
border: 1px solid var(--border) !important;
|
| 262 |
+
border-radius: var(--radius-sm) !important;
|
| 263 |
+
color: var(--text-primary) !important;
|
| 264 |
+
font-weight: 600 !important;
|
| 265 |
+
transition: border-color 0.2s !important;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.streamlit-expanderHeader:hover {
|
| 269 |
+
border-color: var(--accent-primary) !important;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.streamlit-expanderContent {
|
| 273 |
+
background: var(--bg-secondary) !important;
|
| 274 |
+
border: 1px solid var(--border) !important;
|
| 275 |
+
border-top: none !important;
|
| 276 |
+
border-radius: 0 0 var(--radius-sm) var(--radius-sm) !important;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* Code blocks */
|
| 280 |
+
.stCodeBlock pre, code {
|
| 281 |
+
font-family: 'JetBrains Mono', monospace !important;
|
| 282 |
+
font-size: 0.85rem !important;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/* ββ Metrics βββββββββββββββ */
|
| 286 |
+
.stat-card {
|
| 287 |
+
background: var(--bg-card);
|
| 288 |
+
border: 1px solid var(--border);
|
| 289 |
+
border-radius: var(--radius-sm);
|
| 290 |
+
padding: 1rem;
|
| 291 |
+
text-align: center;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.stat-value {
|
| 295 |
+
font-size: 1.8rem;
|
| 296 |
+
font-weight: 800;
|
| 297 |
+
background: linear-gradient(135deg, #6c63ff, #a78bfa);
|
| 298 |
+
-webkit-background-clip: text;
|
| 299 |
+
-webkit-text-fill-color: transparent;
|
| 300 |
+
background-clip: text;
|
| 301 |
+
line-height: 1;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.stat-label {
|
| 305 |
+
font-size: 0.75rem;
|
| 306 |
+
color: var(--text-muted);
|
| 307 |
+
margin-top: 0.3rem;
|
| 308 |
+
text-transform: uppercase;
|
| 309 |
+
letter-spacing: 0.06em;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
/* ββ Dividers ββββββββββββββ */
|
| 313 |
+
hr {
|
| 314 |
+
border: none !important;
|
| 315 |
+
border-top: 1px solid var(--border) !important;
|
| 316 |
+
margin: 1.5rem 0 !important;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
/* ββ Sidebar specific ββββββ */
|
| 320 |
+
.sidebar-section-title {
|
| 321 |
+
font-size: 0.7rem;
|
| 322 |
+
font-weight: 700;
|
| 323 |
+
text-transform: uppercase;
|
| 324 |
+
letter-spacing: 0.1em;
|
| 325 |
+
color: var(--text-muted);
|
| 326 |
+
margin: 1.2rem 0 0.5rem;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.sidebar-logo {
|
| 330 |
+
font-size: 1.5rem;
|
| 331 |
+
font-weight: 800;
|
| 332 |
+
background: linear-gradient(135deg, #6c63ff, #a78bfa);
|
| 333 |
+
-webkit-background-clip: text;
|
| 334 |
+
-webkit-text-fill-color: transparent;
|
| 335 |
+
background-clip: text;
|
| 336 |
+
margin-bottom: 0.2rem;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
/* ββ Stream output box βββββ */
|
| 340 |
+
.stream-box {
|
| 341 |
+
background: var(--bg-secondary);
|
| 342 |
+
border: 1px solid var(--border);
|
| 343 |
+
border-radius: var(--radius-sm);
|
| 344 |
+
padding: 1rem;
|
| 345 |
+
font-family: 'JetBrains Mono', monospace;
|
| 346 |
+
font-size: 0.82rem;
|
| 347 |
+
line-height: 1.6;
|
| 348 |
+
color: #d1fae5;
|
| 349 |
+
max-height: 300px;
|
| 350 |
+
overflow-y: auto;
|
| 351 |
+
white-space: pre-wrap;
|
| 352 |
+
word-break: break-word;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
/* ββ Alerts ββββββββββββββββ */
|
| 356 |
+
.stAlert {
|
| 357 |
+
border-radius: var(--radius-sm) !important;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/* Toggle/checkbox */
|
| 361 |
+
.stCheckbox > label {
|
| 362 |
+
color: var(--text-secondary) !important;
|
| 363 |
+
font-size: 0.9rem !important;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
/* Scrollbar */
|
| 367 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 368 |
+
::-webkit-scrollbar-track { background: var(--bg-secondary); }
|
| 369 |
+
::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 3px; }
|
| 370 |
+
::-webkit-scrollbar-thumb:hover { background: var(--accent-primary); }
|
| 371 |
+
|
| 372 |
+
/* Selectbox */
|
| 373 |
+
.stSelectbox > div > div {
|
| 374 |
+
background: var(--bg-card) !important;
|
| 375 |
+
border-color: var(--border) !important;
|
| 376 |
+
color: var(--text-primary) !important;
|
| 377 |
+
}
|
| 378 |
+
</style>
|
| 379 |
+
""", unsafe_allow_html=True)
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
# ββ Session State Init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 383 |
+
def init_session():
|
| 384 |
+
defaults = {
|
| 385 |
+
"result": None,
|
| 386 |
+
"running": False,
|
| 387 |
+
"step_statuses": {},
|
| 388 |
+
"step_messages": {},
|
| 389 |
+
"stream_buffer": "",
|
| 390 |
+
"error": None,
|
| 391 |
+
"glm_api_key": "",
|
| 392 |
+
"github_token": "",
|
| 393 |
+
"model": GLM_MODEL,
|
| 394 |
+
"run_confidence": False,
|
| 395 |
+
}
|
| 396 |
+
for k, v in defaults.items():
|
| 397 |
+
if k not in st.session_state:
|
| 398 |
+
st.session_state[k] = v
|
| 399 |
+
|
| 400 |
+
init_session()
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
# ββ Sidebar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 404 |
+
with st.sidebar:
|
| 405 |
+
st.markdown('<div class="sidebar-logo">π§ FixFlow</div>', unsafe_allow_html=True)
|
| 406 |
+
st.markdown('<div style="color: #6b7280; font-size: 0.8rem; margin-bottom: 1.5rem;">Autonomous Bug Resolution Agent</div>', unsafe_allow_html=True)
|
| 407 |
+
|
| 408 |
+
st.markdown('<div class="sidebar-section-title">π API Configuration</div>', unsafe_allow_html=True)
|
| 409 |
+
|
| 410 |
+
glm_key = st.text_input(
|
| 411 |
+
"GLM API Key (Z.ai)",
|
| 412 |
+
value=st.session_state.glm_api_key,
|
| 413 |
+
type="password",
|
| 414 |
+
placeholder="Enter your Z.ai GLM API key...",
|
| 415 |
+
help="Get your key at https://open.bigmodel.cn/",
|
| 416 |
+
key="glm_key_input",
|
| 417 |
+
)
|
| 418 |
+
if glm_key:
|
| 419 |
+
st.session_state.glm_api_key = glm_key
|
| 420 |
+
|
| 421 |
+
github_token = st.text_input(
|
| 422 |
+
"GitHub Token (optional)",
|
| 423 |
+
value=st.session_state.github_token,
|
| 424 |
+
type="password",
|
| 425 |
+
placeholder="ghp_... (for private repos / higher limits)",
|
| 426 |
+
help="Needed for private repos. Also increases rate limit from 60 to 5000 req/hr.",
|
| 427 |
+
key="github_token_input",
|
| 428 |
+
)
|
| 429 |
+
if github_token:
|
| 430 |
+
st.session_state.github_token = github_token
|
| 431 |
+
|
| 432 |
+
st.markdown('<div class="sidebar-section-title">βοΈ Model Settings</div>', unsafe_allow_html=True)
|
| 433 |
+
|
| 434 |
+
model_choice = st.selectbox(
|
| 435 |
+
"GLM Model",
|
| 436 |
+
options=["glm-5-plus", "glm-4-plus", "glm-4"],
|
| 437 |
+
index=0,
|
| 438 |
+
key="model_select",
|
| 439 |
+
)
|
| 440 |
+
st.session_state.model = model_choice
|
| 441 |
+
|
| 442 |
+
st.markdown('<div class="sidebar-section-title">π§ͺ Options</div>', unsafe_allow_html=True)
|
| 443 |
+
|
| 444 |
+
run_confidence = st.checkbox(
|
| 445 |
+
"Run confidence self-evaluation",
|
| 446 |
+
value=st.session_state.run_confidence,
|
| 447 |
+
help="Ask GLM to rate confidence in its own analysis (adds ~10-15s)",
|
| 448 |
+
key="confidence_check",
|
| 449 |
+
)
|
| 450 |
+
st.session_state.run_confidence = run_confidence
|
| 451 |
+
|
| 452 |
+
# Rate limit info
|
| 453 |
+
if st.session_state.github_token:
|
| 454 |
+
st.markdown('<div class="sidebar-section-title">π GitHub Status</div>', unsafe_allow_html=True)
|
| 455 |
+
try:
|
| 456 |
+
gh_temp = GitHubClient(token=st.session_state.github_token)
|
| 457 |
+
rl = gh_temp.get_rate_limit_info()
|
| 458 |
+
if rl:
|
| 459 |
+
remaining = rl.get("core_remaining", "?")
|
| 460 |
+
limit = rl.get("core_limit", "?")
|
| 461 |
+
pct = int(remaining / limit * 100) if isinstance(remaining, int) and isinstance(limit, int) else 0
|
| 462 |
+
color = "#10b981" if pct > 50 else "#f59e0b" if pct > 20 else "#ef4444"
|
| 463 |
+
st.markdown(
|
| 464 |
+
f'<div style="font-size:0.8rem; color: {color};">API: {remaining}/{limit} requests remaining</div>',
|
| 465 |
+
unsafe_allow_html=True
|
| 466 |
+
)
|
| 467 |
+
except Exception:
|
| 468 |
+
pass
|
| 469 |
+
|
| 470 |
+
st.markdown("---")
|
| 471 |
+
st.markdown(
|
| 472 |
+
'<div style="font-size: 0.72rem; color: #4b5563; line-height: 1.6;">'
|
| 473 |
+
'π Your API keys are never stored or transmitted beyond direct API calls.<br><br>'
|
| 474 |
+
'β‘ Powered by <b style="color: #a78bfa;">GLM 5.1 by Z.ai</b>'
|
| 475 |
+
'</div>',
|
| 476 |
+
unsafe_allow_html=True
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
# ββ Main Content ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 481 |
+
|
| 482 |
+
# Header
|
| 483 |
+
st.markdown("""
|
| 484 |
+
<div class="fixflow-header">
|
| 485 |
+
<span class="fixflow-logo">π§</span>
|
| 486 |
+
<div class="fixflow-title">FixFlow</div>
|
| 487 |
+
<div class="fixflow-subtitle">Autonomous Bug Resolution Agent</div>
|
| 488 |
+
<span class="powered-badge">β‘ GLM 5.1 by Z.ai</span>
|
| 489 |
+
</div>
|
| 490 |
+
""", unsafe_allow_html=True)
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
# ββ Input Section βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 494 |
+
st.markdown('<div class="pipeline-card">', unsafe_allow_html=True)
|
| 495 |
+
st.markdown("### π― Analyze a GitHub Issue")
|
| 496 |
+
st.markdown('<div style="color: #9ca3af; font-size: 0.9rem; margin-bottom: 1rem;">Paste a GitHub issue URL and the repository to analyze. FixFlow will autonomously trace the root cause and generate a fix.</div>', unsafe_allow_html=True)
|
| 497 |
+
|
| 498 |
+
col1, col2 = st.columns(2)
|
| 499 |
+
with col1:
|
| 500 |
+
issue_url = st.text_input(
|
| 501 |
+
"GitHub Issue URL",
|
| 502 |
+
placeholder="https://github.com/owner/repo/issues/123",
|
| 503 |
+
help="Full URL to the GitHub issue you want to fix",
|
| 504 |
+
key="issue_url_input",
|
| 505 |
+
)
|
| 506 |
+
with col2:
|
| 507 |
+
repo_url = st.text_input(
|
| 508 |
+
"Repository URL",
|
| 509 |
+
placeholder="https://github.com/owner/repo",
|
| 510 |
+
help="The repository containing the buggy code",
|
| 511 |
+
key="repo_url_input",
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
# Auto-fill repo from issue URL
|
| 515 |
+
if issue_url and not repo_url:
|
| 516 |
+
# Try to extract repo from issue URL
|
| 517 |
+
import re
|
| 518 |
+
m = re.match(r"(https://github\.com/[^/]+/[^/]+)/issues/\d+", issue_url.strip())
|
| 519 |
+
if m:
|
| 520 |
+
st.session_state["repo_url_input"] = m.group(1)
|
| 521 |
+
repo_url = m.group(1)
|
| 522 |
+
|
| 523 |
+
# Example buttons
|
| 524 |
+
st.markdown('<div style="margin-top: 0.5rem; color: #6b7280; font-size: 0.8rem;">π‘ Try with an example:</div>', unsafe_allow_html=True)
|
| 525 |
+
ex_col1, ex_col2, ex_col3 = st.columns(3)
|
| 526 |
+
with ex_col1:
|
| 527 |
+
if st.button("FastAPI #1234 example", key="ex1", help="Example issue"):
|
| 528 |
+
st.info("Set issue URL to a real FastAPI issue, e.g.: https://github.com/tiangolo/fastapi/issues/10876")
|
| 529 |
+
with ex_col2:
|
| 530 |
+
if st.button("Requests #6710 example", key="ex2", help="Example issue"):
|
| 531 |
+
st.info("Set issue URL to: https://github.com/psf/requests/issues/6710")
|
| 532 |
+
with ex_col3:
|
| 533 |
+
if st.button("Flask #5742 example", key="ex3", help="Example issue"):
|
| 534 |
+
st.info("Set issue URL to: https://github.com/pallets/flask/issues/5742")
|
| 535 |
+
|
| 536 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 537 |
+
|
| 538 |
+
# ββ Analyze Button ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 539 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 540 |
+
btn_col, info_col = st.columns([1, 3])
|
| 541 |
+
|
| 542 |
+
with btn_col:
|
| 543 |
+
analyze_clicked = st.button(
|
| 544 |
+
"π Analyze & Fix",
|
| 545 |
+
key="analyze_btn",
|
| 546 |
+
type="primary",
|
| 547 |
+
disabled=st.session_state.running,
|
| 548 |
+
use_container_width=True,
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
with info_col:
|
| 552 |
+
if st.session_state.running:
|
| 553 |
+
st.markdown(
|
| 554 |
+
'<div style="padding: 0.6rem; color: #60a5fa; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem;">'
|
| 555 |
+
'β³ Analysis in progress... This may take 1-3 minutes depending on repo size.'
|
| 556 |
+
'</div>',
|
| 557 |
+
unsafe_allow_html=True
|
| 558 |
+
)
|
| 559 |
+
elif st.session_state.result:
|
| 560 |
+
total_time = sum(st.session_state.result.step_timings.values())
|
| 561 |
+
st.markdown(
|
| 562 |
+
f'<div style="padding: 0.6rem; color: #10b981; font-size: 0.9rem;">'
|
| 563 |
+
f'β
Last analysis completed in {total_time:.1f}s</div>',
|
| 564 |
+
unsafe_allow_html=True
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
# ββ Pipeline Execution ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 569 |
+
STEP_LABELS = {
|
| 570 |
+
"0_fetch": ("π‘", "Fetching GitHub Data"),
|
| 571 |
+
"1_issue": ("π", "Analyzing Bug Report"),
|
| 572 |
+
"2_mapping": ("πΊοΈ", "Mapping Codebase"),
|
| 573 |
+
"3_analysis": ("π¬", "Root Cause Analysis"),
|
| 574 |
+
"4_fix": ("π§", "Generating Fix"),
|
| 575 |
+
"5_diff": ("π", "Creating PR Description"),
|
| 576 |
+
"6_confidence": ("π―", "Confidence Evaluation"),
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def run_agent():
|
| 581 |
+
"""Execute the FixFlow agent pipeline (runs in main thread for Streamlit)."""
|
| 582 |
+
st.session_state.running = True
|
| 583 |
+
st.session_state.result = None
|
| 584 |
+
st.session_state.error = None
|
| 585 |
+
st.session_state.step_statuses = {}
|
| 586 |
+
st.session_state.step_messages = {}
|
| 587 |
+
st.session_state.stream_buffer = ""
|
| 588 |
+
|
| 589 |
+
def on_status(step: str, status: str, message: str):
|
| 590 |
+
st.session_state.step_statuses[step] = status
|
| 591 |
+
st.session_state.step_messages[step] = message
|
| 592 |
+
|
| 593 |
+
def on_stream(chunk: str):
|
| 594 |
+
st.session_state.stream_buffer += chunk
|
| 595 |
+
|
| 596 |
+
try:
|
| 597 |
+
llm = GLMClient(
|
| 598 |
+
api_key=st.session_state.glm_api_key,
|
| 599 |
+
base_url=GLM_BASE_URL,
|
| 600 |
+
model=st.session_state.model,
|
| 601 |
+
)
|
| 602 |
+
gh = GitHubClient(token=st.session_state.github_token or None)
|
| 603 |
+
agent = FixFlowAgent(llm_client=llm, github_client=gh)
|
| 604 |
+
|
| 605 |
+
result = agent.run(
|
| 606 |
+
issue_url=issue_url.strip(),
|
| 607 |
+
repo_url=repo_url.strip(),
|
| 608 |
+
on_status=on_status,
|
| 609 |
+
stream_callback=on_stream,
|
| 610 |
+
run_confidence_eval=st.session_state.run_confidence,
|
| 611 |
+
)
|
| 612 |
+
st.session_state.result = result
|
| 613 |
+
|
| 614 |
+
except Exception as e:
|
| 615 |
+
st.session_state.error = str(e)
|
| 616 |
+
logger.exception("Agent pipeline error")
|
| 617 |
+
finally:
|
| 618 |
+
st.session_state.running = False
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
# Trigger on button click
|
| 622 |
+
if analyze_clicked:
|
| 623 |
+
if not st.session_state.glm_api_key:
|
| 624 |
+
st.error("β οΈ Please enter your GLM API key in the sidebar.")
|
| 625 |
+
elif not issue_url:
|
| 626 |
+
st.error("β οΈ Please enter a GitHub Issue URL.")
|
| 627 |
+
elif not repo_url:
|
| 628 |
+
st.error("β οΈ Please enter the Repository URL.")
|
| 629 |
+
else:
|
| 630 |
+
run_agent()
|
| 631 |
+
st.rerun()
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
# ββ Error Display βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 635 |
+
if st.session_state.error:
|
| 636 |
+
st.error(f"β **Error:** {st.session_state.error}")
|
| 637 |
+
with st.expander("π Debug Information"):
|
| 638 |
+
st.code(st.session_state.error)
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
# ββ Pipeline Progress βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 642 |
+
if st.session_state.step_statuses or st.session_state.result:
|
| 643 |
+
st.markdown("---")
|
| 644 |
+
st.markdown("### β‘ Pipeline Progress")
|
| 645 |
+
|
| 646 |
+
statuses = st.session_state.step_statuses
|
| 647 |
+
result: Optional[AgentResult] = st.session_state.result
|
| 648 |
+
timings = result.step_timings if result else {}
|
| 649 |
+
|
| 650 |
+
status_icons = {
|
| 651 |
+
"running": "β³",
|
| 652 |
+
"complete": "β
",
|
| 653 |
+
"error": "β",
|
| 654 |
+
"info": "βΉοΈ",
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
progress_cols = st.columns(min(len(STEP_LABELS), 4))
|
| 658 |
+
step_items = list(STEP_LABELS.items())
|
| 659 |
+
|
| 660 |
+
for i, (step_id, (icon, label)) in enumerate(step_items):
|
| 661 |
+
status = statuses.get(step_id, "idle")
|
| 662 |
+
timing = timings.get(step_id)
|
| 663 |
+
|
| 664 |
+
css_class = f"step-{status}" if status != "idle" else "step-idle"
|
| 665 |
+
status_icon = status_icons.get(status, "β¬")
|
| 666 |
+
time_str = f"{timing:.1f}s" if timing else ""
|
| 667 |
+
|
| 668 |
+
st.markdown(
|
| 669 |
+
f'<div class="step-indicator {css_class}">'
|
| 670 |
+
f'<span class="step-icon">{status_icon}</span>'
|
| 671 |
+
f'<span>{icon} {label}</span>'
|
| 672 |
+
f'<span class="step-time">{time_str}</span>'
|
| 673 |
+
f'</div>',
|
| 674 |
+
unsafe_allow_html=True,
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
|
| 678 |
+
# ββ Results βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 679 |
+
if st.session_state.result:
|
| 680 |
+
result: AgentResult = st.session_state.result
|
| 681 |
+
st.markdown("---")
|
| 682 |
+
|
| 683 |
+
# ββ Summary Stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 684 |
+
total_time = sum(result.step_timings.values())
|
| 685 |
+
stats = result.diff_stats
|
| 686 |
+
|
| 687 |
+
st.markdown("### π Analysis Summary")
|
| 688 |
+
m1, m2, m3, m4 = st.columns(4)
|
| 689 |
+
|
| 690 |
+
with m1:
|
| 691 |
+
st.markdown(
|
| 692 |
+
f'<div class="stat-card">'
|
| 693 |
+
f'<div class="stat-value">{len(result.suspect_file_paths)}</div>'
|
| 694 |
+
f'<div class="stat-label">Files Analyzed</div>'
|
| 695 |
+
f'</div>',
|
| 696 |
+
unsafe_allow_html=True
|
| 697 |
+
)
|
| 698 |
+
with m2:
|
| 699 |
+
st.markdown(
|
| 700 |
+
f'<div class="stat-card">'
|
| 701 |
+
f'<div class="stat-value">{stats.get("files_changed", 0)}</div>'
|
| 702 |
+
f'<div class="stat-label">Files Changed</div>'
|
| 703 |
+
f'</div>',
|
| 704 |
+
unsafe_allow_html=True
|
| 705 |
+
)
|
| 706 |
+
with m3:
|
| 707 |
+
st.markdown(
|
| 708 |
+
f'<div class="stat-card">'
|
| 709 |
+
f'<div class="stat-value">+{stats.get("lines_added", 0)}</div>'
|
| 710 |
+
f'<div class="stat-label">Lines Added</div>'
|
| 711 |
+
f'</div>',
|
| 712 |
+
unsafe_allow_html=True
|
| 713 |
+
)
|
| 714 |
+
with m4:
|
| 715 |
+
st.markdown(
|
| 716 |
+
f'<div class="stat-card">'
|
| 717 |
+
f'<div class="stat-value">{total_time:.0f}s</div>'
|
| 718 |
+
f'<div class="stat-label">Total Time</div>'
|
| 719 |
+
f'</div>',
|
| 720 |
+
unsafe_allow_html=True
|
| 721 |
+
)
|
| 722 |
+
|
| 723 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 724 |
+
|
| 725 |
+
# ββ Step 1: Bug Summary βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 726 |
+
with st.expander("π Step 1: Bug Summary", expanded=True):
|
| 727 |
+
st.markdown(
|
| 728 |
+
f'<div style="color: #9ca3af; font-size: 0.82rem; margin-bottom: 1rem;">'
|
| 729 |
+
f'β±οΈ Completed in {result.step_timings.get("1_issue", 0):.1f}s'
|
| 730 |
+
f'</div>',
|
| 731 |
+
unsafe_allow_html=True
|
| 732 |
+
)
|
| 733 |
+
st.markdown(result.bug_summary)
|
| 734 |
+
|
| 735 |
+
# ββ Step 2: Relevant Files ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 736 |
+
with st.expander("π Step 2: Relevant Files & Codebase Mapping", expanded=False):
|
| 737 |
+
st.markdown(
|
| 738 |
+
f'<div style="color: #9ca3af; font-size: 0.82rem; margin-bottom: 0.5rem;">'
|
| 739 |
+
f'β±οΈ Completed in {result.step_timings.get("2_mapping", 0):.1f}s | '
|
| 740 |
+
f'Selected {len(result.suspect_file_paths)} files for deep analysis'
|
| 741 |
+
f'</div>',
|
| 742 |
+
unsafe_allow_html=True
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
if result.suspect_file_paths:
|
| 746 |
+
st.markdown("**π― Files Selected for Analysis:**")
|
| 747 |
+
for i, fp in enumerate(result.suspect_file_paths, 1):
|
| 748 |
+
st.markdown(f"`{i}.` `{fp}`")
|
| 749 |
+
|
| 750 |
+
st.markdown("---")
|
| 751 |
+
st.markdown(result.relevant_files_analysis)
|
| 752 |
+
|
| 753 |
+
# ββ Step 3: Root Cause Analysis βββββββββββββββββββββββββββββββββββββββββββ
|
| 754 |
+
with st.expander("π¬ Step 3: Root Cause Analysis (Chain-of-Thought)", expanded=True):
|
| 755 |
+
st.markdown(
|
| 756 |
+
f'<div style="color: #9ca3af; font-size: 0.82rem; margin-bottom: 1rem;">'
|
| 757 |
+
f'β±οΈ Completed in {result.step_timings.get("3_analysis", 0):.1f}s | '
|
| 758 |
+
f'This is the core reasoning chain β read carefully!'
|
| 759 |
+
f'</div>',
|
| 760 |
+
unsafe_allow_html=True
|
| 761 |
+
)
|
| 762 |
+
st.markdown(result.root_cause_analysis)
|
| 763 |
+
|
| 764 |
+
# ββ Step 4: Proposed Fix (Diff) βββββββββββββββββββββββββββββββββββββββββββ
|
| 765 |
+
with st.expander("π§ Step 4: Proposed Fix", expanded=True):
|
| 766 |
+
st.markdown(
|
| 767 |
+
f'<div style="color: #9ca3af; font-size: 0.82rem; margin-bottom: 1rem;">'
|
| 768 |
+
f'β±οΈ Completed in {result.step_timings.get("4_fix", 0):.1f}s | '
|
| 769 |
+
f'{stats.get("files_changed", 0)} file(s) modified, '
|
| 770 |
+
f'+{stats.get("lines_added", 0)} / -{stats.get("lines_removed", 0)} lines'
|
| 771 |
+
f'</div>',
|
| 772 |
+
unsafe_allow_html=True
|
| 773 |
+
)
|
| 774 |
+
|
| 775 |
+
if result.diffs:
|
| 776 |
+
# Syntax-highlighted diff
|
| 777 |
+
for filepath, diff_content in result.diffs.items():
|
| 778 |
+
st.markdown(f"**`{filepath}`**")
|
| 779 |
+
st.code(diff_content, language="diff")
|
| 780 |
+
else:
|
| 781 |
+
st.warning("β οΈ No diffs generated. The LLM may not have proposed direct file changes.")
|
| 782 |
+
if result.fix_generation_raw:
|
| 783 |
+
st.markdown("**Raw fix proposal from GLM:**")
|
| 784 |
+
st.markdown(result.fix_generation_raw)
|
| 785 |
+
|
| 786 |
+
# Copy button for full diff
|
| 787 |
+
if result.diff_formatted and result.diffs:
|
| 788 |
+
st.markdown("---")
|
| 789 |
+
copy_col, _ = st.columns([1, 3])
|
| 790 |
+
with copy_col:
|
| 791 |
+
st.download_button(
|
| 792 |
+
"π Copy Full Diff",
|
| 793 |
+
data=result.diff_formatted,
|
| 794 |
+
file_name="fixflow.diff",
|
| 795 |
+
mime="text/plain",
|
| 796 |
+
use_container_width=True,
|
| 797 |
+
)
|
| 798 |
+
|
| 799 |
+
# ββ Step 5: Fix Explanation βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 800 |
+
with st.expander("π Step 5: PR Description & Fix Explanation", expanded=True):
|
| 801 |
+
st.markdown(
|
| 802 |
+
f'<div style="color: #9ca3af; font-size: 0.82rem; margin-bottom: 1rem;">'
|
| 803 |
+
f'β±οΈ Completed in {result.step_timings.get("5_diff", 0):.1f}s'
|
| 804 |
+
f'</div>',
|
| 805 |
+
unsafe_allow_html=True
|
| 806 |
+
)
|
| 807 |
+
st.markdown(result.fix_explanation)
|
| 808 |
+
|
| 809 |
+
# ββ Confidence Eval (optional) ββββββββββββββββββββββββββββββββββββββββββββ
|
| 810 |
+
if result.confidence_eval:
|
| 811 |
+
with st.expander("π― Confidence Self-Evaluation", expanded=False):
|
| 812 |
+
st.markdown(result.confidence_eval)
|
| 813 |
+
|
| 814 |
+
# ββ Export Full Report ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 815 |
+
st.markdown("---")
|
| 816 |
+
st.markdown("### π€ Export Report")
|
| 817 |
+
exp_col1, exp_col2, _ = st.columns([1, 1, 2])
|
| 818 |
+
|
| 819 |
+
full_report = generate_full_report(result)
|
| 820 |
+
issue_num = result.issue_data.get("number", "0")
|
| 821 |
+
repo_slug = repo_url.strip().rstrip("/").split("/")[-1] if repo_url else "repo"
|
| 822 |
+
|
| 823 |
+
with exp_col1:
|
| 824 |
+
st.download_button(
|
| 825 |
+
"π Download Full Report (.md)",
|
| 826 |
+
data=full_report,
|
| 827 |
+
file_name=f"fixflow_{repo_slug}_issue_{issue_num}.md",
|
| 828 |
+
mime="text/markdown",
|
| 829 |
+
use_container_width=True,
|
| 830 |
+
)
|
| 831 |
+
|
| 832 |
+
with exp_col2:
|
| 833 |
+
if result.diff_formatted and result.diffs:
|
| 834 |
+
st.download_button(
|
| 835 |
+
"π¦ Download Patch (.diff)",
|
| 836 |
+
data=result.diff_formatted,
|
| 837 |
+
file_name=f"fixflow_{repo_slug}_issue_{issue_num}.diff",
|
| 838 |
+
mime="text/plain",
|
| 839 |
+
use_container_width=True,
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
st.markdown("---")
|
| 843 |
+
st.markdown(
|
| 844 |
+
'<div style="text-align: center; color: #4b5563; font-size: 0.8rem; padding: 1rem 0;">'
|
| 845 |
+
'π§ <b style="color: #6c63ff;">FixFlow</b> β Autonomous Bug Resolution Β· Powered by '
|
| 846 |
+
'<b style="color: #a78bfa;">GLM 5.1 by Z.ai</b>'
|
| 847 |
+
'</div>',
|
| 848 |
+
unsafe_allow_html=True
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
# ββ Empty State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 853 |
+
elif not st.session_state.running and not st.session_state.error:
|
| 854 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 855 |
+
|
| 856 |
+
col1, col2, col3 = st.columns(3)
|
| 857 |
+
|
| 858 |
+
cards = [
|
| 859 |
+
("π", "Bug Report Parsing", "Automatically extracts error messages, reproduction steps, affected components, and technical clues from any GitHub issue."),
|
| 860 |
+
("π§ ", "Chain-of-Thought Reasoning", "Traces the execution flow step-by-step, citing specific file names, functions, and line numbers to pinpoint the root cause."),
|
| 861 |
+
("π§", "PR-Ready Fixes", "Generates minimal, precise code fixes with unified diffs and a complete pull request description you can copy directly."),
|
| 862 |
+
]
|
| 863 |
+
|
| 864 |
+
for col, (icon, title, desc) in zip([col1, col2, col3], cards):
|
| 865 |
+
with col:
|
| 866 |
+
st.markdown(
|
| 867 |
+
f'<div class="pipeline-card" style="text-align: center; padding: 2rem 1.5rem;">'
|
| 868 |
+
f'<div style="font-size: 2.5rem; margin-bottom: 0.75rem;">{icon}</div>'
|
| 869 |
+
f'<div style="font-weight: 700; font-size: 1rem; color: #f0f0ff; margin-bottom: 0.5rem;">{title}</div>'
|
| 870 |
+
f'<div style="font-size: 0.85rem; color: #6b7280; line-height: 1.6;">{desc}</div>'
|
| 871 |
+
f'</div>',
|
| 872 |
+
unsafe_allow_html=True,
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 876 |
+
|
| 877 |
+
# How it works
|
| 878 |
+
st.markdown("### π How It Works")
|
| 879 |
+
steps_html = """
|
| 880 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem; margin-top: 1rem;">
|
| 881 |
+
"""
|
| 882 |
+
how_steps = [
|
| 883 |
+
("1", "π‘", "Fetch Issue", "Pulls the full GitHub issue: title, body, comments, labels"),
|
| 884 |
+
("2", "πΊοΈ", "Map Codebase", "Identifies top 5-10 suspect files from the repo tree"),
|
| 885 |
+
("3", "π¬", "Analyze Code", "Deep code reading with chain-of-thought root cause tracing"),
|
| 886 |
+
("4", "π§", "Generate Fix", "Creates corrected file versions with minimal changes"),
|
| 887 |
+
("5", "π", "Write PR", "Produces unified diff + human-readable PR description"),
|
| 888 |
+
]
|
| 889 |
+
for num, icon, title, desc in how_steps:
|
| 890 |
+
steps_html += f"""
|
| 891 |
+
<div style="background: #12141a; border: 1px solid #2a2c36; border-radius: 10px; padding: 1rem; position: relative;">
|
| 892 |
+
<div style="position: absolute; top: -10px; left: 12px; background: linear-gradient(135deg, #6c63ff, #a78bfa); border-radius: 50%; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 800; color: white;">{num}</div>
|
| 893 |
+
<div style="font-size: 1.4rem; margin-bottom: 0.4rem; margin-top: 0.3rem;">{icon}</div>
|
| 894 |
+
<div style="font-weight: 700; font-size: 0.9rem; color: #f0f0ff; margin-bottom: 0.3rem;">{title}</div>
|
| 895 |
+
<div style="font-size: 0.78rem; color: #6b7280; line-height: 1.5;">{desc}</div>
|
| 896 |
+
</div>
|
| 897 |
+
"""
|
| 898 |
+
steps_html += "</div>"
|
| 899 |
+
st.markdown(steps_html, unsafe_allow_html=True)
|
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# FixFlow Backend Package
|
backend/agent.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FixFlow Core Agent β Multi-step autonomous bug resolution pipeline.
|
| 3 |
+
|
| 4 |
+
Pipeline:
|
| 5 |
+
Step 1: Issue Understanding β Structured bug summary
|
| 6 |
+
Step 2: Codebase Mapping β Ranked list of suspect files
|
| 7 |
+
Step 3: Deep Code Analysis β Root cause analysis + reasoning chain
|
| 8 |
+
Step 4: Fix Generation β Corrected file contents
|
| 9 |
+
Step 5: Diff & Explanation β PR-ready diff + human explanation
|
| 10 |
+
"""
|
| 11 |
+
import logging
|
| 12 |
+
import time
|
| 13 |
+
from dataclasses import dataclass, field
|
| 14 |
+
from typing import Callable, Dict, Iterator, List, Optional
|
| 15 |
+
|
| 16 |
+
from backend.config import MAX_FILES_TO_ANALYZE
|
| 17 |
+
from backend.llm_client import GLMClient
|
| 18 |
+
from backend.github_client import GitHubClient
|
| 19 |
+
from backend.code_indexer import (
|
| 20 |
+
build_file_tree_string,
|
| 21 |
+
extract_file_paths_from_llm_response,
|
| 22 |
+
extract_keywords_from_issue,
|
| 23 |
+
format_file_contents_for_prompt,
|
| 24 |
+
rank_files_by_keyword_match,
|
| 25 |
+
)
|
| 26 |
+
from backend.diff_generator import (
|
| 27 |
+
format_diff_for_display,
|
| 28 |
+
generate_all_diffs,
|
| 29 |
+
get_diff_stats,
|
| 30 |
+
parse_fixed_files_from_llm_response,
|
| 31 |
+
)
|
| 32 |
+
from backend.prompts import (
|
| 33 |
+
SYSTEM_MESSAGE,
|
| 34 |
+
ISSUE_ANALYSIS_PROMPT,
|
| 35 |
+
FILE_RELEVANCE_PROMPT,
|
| 36 |
+
ROOT_CAUSE_PROMPT,
|
| 37 |
+
FIX_GENERATION_PROMPT,
|
| 38 |
+
FIX_EXPLANATION_PROMPT,
|
| 39 |
+
CONFIDENCE_EVAL_PROMPT,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
logger = logging.getLogger(__name__)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ββ Result Dataclass ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
+
|
| 47 |
+
@dataclass
|
| 48 |
+
class AgentResult:
|
| 49 |
+
"""Holds all outputs from the FixFlow pipeline."""
|
| 50 |
+
# Inputs
|
| 51 |
+
issue_url: str = ""
|
| 52 |
+
repo_url: str = ""
|
| 53 |
+
issue_data: Dict = field(default_factory=dict)
|
| 54 |
+
|
| 55 |
+
# Step outputs
|
| 56 |
+
bug_summary: str = ""
|
| 57 |
+
relevant_files_analysis: str = ""
|
| 58 |
+
suspect_file_paths: List[str] = field(default_factory=list)
|
| 59 |
+
root_cause_analysis: str = ""
|
| 60 |
+
fix_generation_raw: str = ""
|
| 61 |
+
fixed_files: Dict[str, str] = field(default_factory=dict)
|
| 62 |
+
diffs: Dict[str, str] = field(default_factory=dict)
|
| 63 |
+
diff_formatted: str = ""
|
| 64 |
+
fix_explanation: str = ""
|
| 65 |
+
confidence_eval: str = ""
|
| 66 |
+
|
| 67 |
+
# Metadata
|
| 68 |
+
step_timings: Dict[str, float] = field(default_factory=dict)
|
| 69 |
+
step_errors: Dict[str, str] = field(default_factory=dict)
|
| 70 |
+
diff_stats: Dict = field(default_factory=dict)
|
| 71 |
+
file_tree: List[Dict] = field(default_factory=list)
|
| 72 |
+
original_file_contents: Dict[str, str] = field(default_factory=dict)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# Status callback type
|
| 76 |
+
StatusCallback = Optional[Callable[[str, str, str], None]]
|
| 77 |
+
# Args: (step_name, status, message)
|
| 78 |
+
# status: "running" | "complete" | "error" | "info"
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ββ FixFlow Agent βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 82 |
+
|
| 83 |
+
class FixFlowAgent:
|
| 84 |
+
"""
|
| 85 |
+
Orchestrates the full bug-resolution pipeline.
|
| 86 |
+
|
| 87 |
+
Usage:
|
| 88 |
+
agent = FixFlowAgent(glm_client, github_client)
|
| 89 |
+
result = agent.run(issue_url, repo_url, on_status=callback)
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
llm_client: GLMClient,
|
| 95 |
+
github_client: GitHubClient,
|
| 96 |
+
):
|
| 97 |
+
self.llm = llm_client
|
| 98 |
+
self.gh = github_client
|
| 99 |
+
|
| 100 |
+
# ββ Public entry point ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 101 |
+
|
| 102 |
+
def run(
|
| 103 |
+
self,
|
| 104 |
+
issue_url: str,
|
| 105 |
+
repo_url: str,
|
| 106 |
+
on_status: StatusCallback = None,
|
| 107 |
+
stream_callback: Optional[Callable[[str], None]] = None,
|
| 108 |
+
run_confidence_eval: bool = False,
|
| 109 |
+
) -> AgentResult:
|
| 110 |
+
"""
|
| 111 |
+
Execute the full FixFlow pipeline. Returns an AgentResult.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
issue_url: Full GitHub issue URL
|
| 115 |
+
repo_url: Full GitHub repo URL
|
| 116 |
+
on_status: Optional callback(step, status, message) for UI updates
|
| 117 |
+
stream_callback: Optional callback(chunk) for streaming LLM output
|
| 118 |
+
run_confidence_eval: Whether to run the optional confidence self-eval
|
| 119 |
+
"""
|
| 120 |
+
result = AgentResult(issue_url=issue_url, repo_url=repo_url)
|
| 121 |
+
self._status = on_status or (lambda *a: None)
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
# ββ Step 0: Fetch GitHub data βββββββββββββββββββββββββββββββββ
|
| 125 |
+
self._emit("0_fetch", "running", "Fetching GitHub issue and repository data...")
|
| 126 |
+
t0 = time.time()
|
| 127 |
+
|
| 128 |
+
result.issue_data = self._fetch_issue(issue_url)
|
| 129 |
+
result.file_tree = self._fetch_repo_tree(repo_url)
|
| 130 |
+
result.step_timings["0_fetch"] = time.time() - t0
|
| 131 |
+
self._emit("0_fetch", "complete",
|
| 132 |
+
f"Fetched issue #{result.issue_data['number']} + "
|
| 133 |
+
f"{len(result.file_tree)} repo files in "
|
| 134 |
+
f"{result.step_timings['0_fetch']:.1f}s")
|
| 135 |
+
|
| 136 |
+
# ββ Step 1: Issue Understanding βββββββββββββββββββββββββββββββ
|
| 137 |
+
self._emit("1_issue", "running", "Analyzing bug report with GLM...")
|
| 138 |
+
t1 = time.time()
|
| 139 |
+
|
| 140 |
+
result.bug_summary = self._step1_issue_understanding(
|
| 141 |
+
result.issue_data, stream_callback
|
| 142 |
+
)
|
| 143 |
+
result.step_timings["1_issue"] = time.time() - t1
|
| 144 |
+
self._emit("1_issue", "complete",
|
| 145 |
+
f"Bug analysis complete in {result.step_timings['1_issue']:.1f}s")
|
| 146 |
+
|
| 147 |
+
# ββ Step 2: Codebase Mapping ββββββββββββββββββββββββββββββββββ
|
| 148 |
+
self._emit("2_mapping", "running", "Scanning codebase to identify suspect files...")
|
| 149 |
+
t2 = time.time()
|
| 150 |
+
|
| 151 |
+
result.relevant_files_analysis, result.suspect_file_paths = \
|
| 152 |
+
self._step2_codebase_mapping(
|
| 153 |
+
result.bug_summary,
|
| 154 |
+
result.file_tree,
|
| 155 |
+
result.issue_data,
|
| 156 |
+
stream_callback,
|
| 157 |
+
repo_url=repo_url,
|
| 158 |
+
)
|
| 159 |
+
result.step_timings["2_mapping"] = time.time() - t2
|
| 160 |
+
self._emit("2_mapping", "complete",
|
| 161 |
+
f"Identified {len(result.suspect_file_paths)} suspect files in "
|
| 162 |
+
f"{result.step_timings['2_mapping']:.1f}s")
|
| 163 |
+
|
| 164 |
+
# ββ Step 3: Deep Code Analysis ββββββββββββββββββββββββββββββββ
|
| 165 |
+
self._emit("3_analysis", "running",
|
| 166 |
+
f"Reading {len(result.suspect_file_paths)} files + performing root cause analysis...")
|
| 167 |
+
t3 = time.time()
|
| 168 |
+
|
| 169 |
+
result.original_file_contents = self.gh.fetch_multiple_files(
|
| 170 |
+
repo_url, result.suspect_file_paths
|
| 171 |
+
)
|
| 172 |
+
result.root_cause_analysis = self._step3_deep_analysis(
|
| 173 |
+
result.bug_summary,
|
| 174 |
+
result.original_file_contents,
|
| 175 |
+
stream_callback,
|
| 176 |
+
)
|
| 177 |
+
result.step_timings["3_analysis"] = time.time() - t3
|
| 178 |
+
self._emit("3_analysis", "complete",
|
| 179 |
+
f"Root cause identified in {result.step_timings['3_analysis']:.1f}s")
|
| 180 |
+
|
| 181 |
+
# ββ Step 4: Fix Generation ββββββββββββββββββββββββββββββββββββ
|
| 182 |
+
self._emit("4_fix", "running", "Generating corrected file contents...")
|
| 183 |
+
t4 = time.time()
|
| 184 |
+
|
| 185 |
+
result.fix_generation_raw = self._step4_fix_generation(
|
| 186 |
+
result.root_cause_analysis,
|
| 187 |
+
result.original_file_contents,
|
| 188 |
+
stream_callback,
|
| 189 |
+
)
|
| 190 |
+
result.fixed_files = parse_fixed_files_from_llm_response(
|
| 191 |
+
result.fix_generation_raw,
|
| 192 |
+
result.suspect_file_paths,
|
| 193 |
+
)
|
| 194 |
+
result.step_timings["4_fix"] = time.time() - t4
|
| 195 |
+
self._emit("4_fix", "complete",
|
| 196 |
+
f"Generated fixes for {len(result.fixed_files)} file(s) in "
|
| 197 |
+
f"{result.step_timings['4_fix']:.1f}s")
|
| 198 |
+
|
| 199 |
+
# ββ Step 5: Diff & Explanation ββββββββββββββββββββββββββββββββ
|
| 200 |
+
self._emit("5_diff", "running", "Generating diff and PR explanation...")
|
| 201 |
+
t5 = time.time()
|
| 202 |
+
|
| 203 |
+
result.diffs = generate_all_diffs(
|
| 204 |
+
result.original_file_contents, result.fixed_files
|
| 205 |
+
)
|
| 206 |
+
result.diff_formatted = format_diff_for_display(result.diffs)
|
| 207 |
+
result.diff_stats = get_diff_stats(result.diffs)
|
| 208 |
+
|
| 209 |
+
result.fix_explanation = self._step5_explanation(
|
| 210 |
+
result.bug_summary,
|
| 211 |
+
result.root_cause_analysis,
|
| 212 |
+
result.diff_formatted,
|
| 213 |
+
stream_callback,
|
| 214 |
+
)
|
| 215 |
+
result.step_timings["5_diff"] = time.time() - t5
|
| 216 |
+
self._emit("5_diff", "complete",
|
| 217 |
+
f"PR explanation ready in {result.step_timings['5_diff']:.1f}s")
|
| 218 |
+
|
| 219 |
+
# ββ Optional: Confidence Evaluation βββββββββββββββββββββββββββ
|
| 220 |
+
if run_confidence_eval:
|
| 221 |
+
self._emit("6_confidence", "running", "Running self-evaluation...")
|
| 222 |
+
t6 = time.time()
|
| 223 |
+
combined = (
|
| 224 |
+
f"# Bug Summary\n{result.bug_summary}\n\n"
|
| 225 |
+
f"# Root Cause\n{result.root_cause_analysis}\n\n"
|
| 226 |
+
f"# Fix Explanation\n{result.fix_explanation}"
|
| 227 |
+
)
|
| 228 |
+
result.confidence_eval = self._run_confidence_eval(combined)
|
| 229 |
+
result.step_timings["6_confidence"] = time.time() - t6
|
| 230 |
+
self._emit("6_confidence", "complete",
|
| 231 |
+
f"Confidence eval done in {result.step_timings['6_confidence']:.1f}s")
|
| 232 |
+
|
| 233 |
+
except Exception as e:
|
| 234 |
+
logger.exception("FixFlow pipeline failed")
|
| 235 |
+
step = self._current_step or "unknown"
|
| 236 |
+
result.step_errors[step] = str(e)
|
| 237 |
+
self._emit(step, "error", f"β Pipeline failed: {e}")
|
| 238 |
+
raise
|
| 239 |
+
|
| 240 |
+
return result
|
| 241 |
+
|
| 242 |
+
# ββ Pipeline Steps ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 243 |
+
|
| 244 |
+
def _step1_issue_understanding(
|
| 245 |
+
self,
|
| 246 |
+
issue_data: Dict,
|
| 247 |
+
stream_cb: Optional[Callable] = None,
|
| 248 |
+
) -> str:
|
| 249 |
+
self._current_step = "1_issue"
|
| 250 |
+
|
| 251 |
+
comments_text = ""
|
| 252 |
+
for c in issue_data.get("comments", [])[:5]:
|
| 253 |
+
comments_text += f"**@{c['author']}:** {c['body'][:500]}\n\n"
|
| 254 |
+
if not comments_text:
|
| 255 |
+
comments_text = "No comments."
|
| 256 |
+
|
| 257 |
+
prompt = ISSUE_ANALYSIS_PROMPT.format(
|
| 258 |
+
title=issue_data.get("title", ""),
|
| 259 |
+
body=issue_data.get("body", ""),
|
| 260 |
+
labels=", ".join(issue_data.get("labels", [])) or "none",
|
| 261 |
+
comments=comments_text,
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
messages = [
|
| 265 |
+
{"role": "system", "content": SYSTEM_MESSAGE},
|
| 266 |
+
{"role": "user", "content": prompt},
|
| 267 |
+
]
|
| 268 |
+
return self._llm_call(messages, stream_cb, temperature=0.2)
|
| 269 |
+
|
| 270 |
+
def _step2_codebase_mapping(
|
| 271 |
+
self,
|
| 272 |
+
bug_summary: str,
|
| 273 |
+
file_tree: List[Dict],
|
| 274 |
+
issue_data: Dict,
|
| 275 |
+
stream_cb: Optional[Callable] = None,
|
| 276 |
+
repo_url: str = "",
|
| 277 |
+
):
|
| 278 |
+
self._current_step = "2_mapping"
|
| 279 |
+
|
| 280 |
+
# Pre-filter files by keyword match for large repos
|
| 281 |
+
keywords = extract_keywords_from_issue(issue_data)
|
| 282 |
+
ranked_files = rank_files_by_keyword_match(file_tree, keywords)
|
| 283 |
+
|
| 284 |
+
tree_string = build_file_tree_string(ranked_files, max_lines=200)
|
| 285 |
+
repo_name = repo_url.rstrip("/").split("/")[-2:]
|
| 286 |
+
repo_display = "/".join(repo_name) if len(repo_name) == 2 else repo_url
|
| 287 |
+
|
| 288 |
+
prompt = FILE_RELEVANCE_PROMPT.format(
|
| 289 |
+
bug_summary=bug_summary,
|
| 290 |
+
file_tree=tree_string,
|
| 291 |
+
repo_name=repo_display,
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
messages = [
|
| 295 |
+
{"role": "system", "content": SYSTEM_MESSAGE},
|
| 296 |
+
{"role": "user", "content": prompt},
|
| 297 |
+
]
|
| 298 |
+
analysis = self._llm_call(messages, stream_cb, temperature=0.2)
|
| 299 |
+
|
| 300 |
+
# Extract actual file paths from the response
|
| 301 |
+
paths = extract_file_paths_from_llm_response(analysis)
|
| 302 |
+
|
| 303 |
+
# Validate against actual tree (only keep paths that exist)
|
| 304 |
+
known_paths = {f["path"] for f in file_tree}
|
| 305 |
+
valid_paths = [p for p in paths if p in known_paths]
|
| 306 |
+
|
| 307 |
+
# If LLM hallucinated paths, fall back to keyword-ranked files
|
| 308 |
+
if not valid_paths:
|
| 309 |
+
logger.warning("LLM returned no valid paths β falling back to keyword ranking")
|
| 310 |
+
valid_paths = [f["path"] for f in ranked_files[:MAX_FILES_TO_ANALYZE]]
|
| 311 |
+
|
| 312 |
+
return analysis, valid_paths[:MAX_FILES_TO_ANALYZE]
|
| 313 |
+
|
| 314 |
+
def _step3_deep_analysis(
|
| 315 |
+
self,
|
| 316 |
+
bug_summary: str,
|
| 317 |
+
file_contents: Dict[str, str],
|
| 318 |
+
stream_cb: Optional[Callable] = None,
|
| 319 |
+
) -> str:
|
| 320 |
+
self._current_step = "3_analysis"
|
| 321 |
+
|
| 322 |
+
formatted = format_file_contents_for_prompt(file_contents)
|
| 323 |
+
|
| 324 |
+
prompt = ROOT_CAUSE_PROMPT.format(
|
| 325 |
+
bug_summary=bug_summary,
|
| 326 |
+
file_contents=formatted,
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
messages = [
|
| 330 |
+
{"role": "system", "content": SYSTEM_MESSAGE},
|
| 331 |
+
{"role": "user", "content": prompt},
|
| 332 |
+
]
|
| 333 |
+
return self._llm_call(messages, stream_cb, temperature=0.15, max_tokens=6000)
|
| 334 |
+
|
| 335 |
+
def _step4_fix_generation(
|
| 336 |
+
self,
|
| 337 |
+
root_cause: str,
|
| 338 |
+
file_contents: Dict[str, str],
|
| 339 |
+
stream_cb: Optional[Callable] = None,
|
| 340 |
+
) -> str:
|
| 341 |
+
self._current_step = "4_fix"
|
| 342 |
+
|
| 343 |
+
formatted = format_file_contents_for_prompt(file_contents)
|
| 344 |
+
|
| 345 |
+
# Build list of filepaths for the placeholder
|
| 346 |
+
filepaths = ", ".join(file_contents.keys()) or "affected_file.py"
|
| 347 |
+
|
| 348 |
+
prompt = FIX_GENERATION_PROMPT.format(
|
| 349 |
+
root_cause=root_cause,
|
| 350 |
+
file_contents=formatted,
|
| 351 |
+
filepath_placeholder=filepaths,
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
messages = [
|
| 355 |
+
{"role": "system", "content": SYSTEM_MESSAGE},
|
| 356 |
+
{"role": "user", "content": prompt},
|
| 357 |
+
]
|
| 358 |
+
return self._llm_call(messages, stream_cb, temperature=0.1, max_tokens=8000)
|
| 359 |
+
|
| 360 |
+
def _step5_explanation(
|
| 361 |
+
self,
|
| 362 |
+
bug_summary: str,
|
| 363 |
+
root_cause: str,
|
| 364 |
+
diff_formatted: str,
|
| 365 |
+
stream_cb: Optional[Callable] = None,
|
| 366 |
+
) -> str:
|
| 367 |
+
self._current_step = "5_diff"
|
| 368 |
+
|
| 369 |
+
# Shorten root cause for context
|
| 370 |
+
root_cause_summary = root_cause[:2000] + ("..." if len(root_cause) > 2000 else "")
|
| 371 |
+
|
| 372 |
+
prompt = FIX_EXPLANATION_PROMPT.format(
|
| 373 |
+
bug_summary=bug_summary,
|
| 374 |
+
root_cause_summary=root_cause_summary,
|
| 375 |
+
unified_diff=diff_formatted[:3000],
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
messages = [
|
| 379 |
+
{"role": "system", "content": SYSTEM_MESSAGE},
|
| 380 |
+
{"role": "user", "content": prompt},
|
| 381 |
+
]
|
| 382 |
+
return self._llm_call(messages, stream_cb, temperature=0.3)
|
| 383 |
+
|
| 384 |
+
def _run_confidence_eval(self, analysis: str) -> str:
|
| 385 |
+
self._current_step = "6_confidence"
|
| 386 |
+
prompt = CONFIDENCE_EVAL_PROMPT.format(analysis=analysis[:4000])
|
| 387 |
+
messages = [
|
| 388 |
+
{"role": "system", "content": SYSTEM_MESSAGE},
|
| 389 |
+
{"role": "user", "content": prompt},
|
| 390 |
+
]
|
| 391 |
+
return self._llm_call(messages, None, temperature=0.2)
|
| 392 |
+
|
| 393 |
+
# ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 394 |
+
|
| 395 |
+
def _llm_call(
|
| 396 |
+
self,
|
| 397 |
+
messages: List[Dict],
|
| 398 |
+
stream_cb: Optional[Callable],
|
| 399 |
+
temperature: float = 0.3,
|
| 400 |
+
max_tokens: int = 4096,
|
| 401 |
+
) -> str:
|
| 402 |
+
"""
|
| 403 |
+
Route to streaming or sync call depending on whether a stream callback is provided.
|
| 404 |
+
"""
|
| 405 |
+
if stream_cb:
|
| 406 |
+
full_response = ""
|
| 407 |
+
for chunk in self.llm.chat_stream(messages, temperature, max_tokens):
|
| 408 |
+
stream_cb(chunk)
|
| 409 |
+
full_response += chunk
|
| 410 |
+
return full_response
|
| 411 |
+
else:
|
| 412 |
+
return self.llm.chat(messages, temperature, max_tokens)
|
| 413 |
+
|
| 414 |
+
def _fetch_issue(self, issue_url: str) -> Dict:
|
| 415 |
+
return self.gh.fetch_issue(issue_url)
|
| 416 |
+
|
| 417 |
+
def _fetch_repo_tree(self, repo_url: str) -> List[Dict]:
|
| 418 |
+
return self.gh.fetch_repo_tree(repo_url)
|
| 419 |
+
|
| 420 |
+
def _emit(self, step: str, status: str, message: str) -> None:
|
| 421 |
+
self._status(step, status, message)
|
| 422 |
+
logger.info("[%s] %s: %s", step, status.upper(), message)
|
| 423 |
+
|
| 424 |
+
_current_step: str = "init"
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
# ββ Wrapper for full report generation βββββββββββββββββββββββββββββββββββββββ
|
| 428 |
+
|
| 429 |
+
def generate_full_report(result: AgentResult) -> str:
|
| 430 |
+
"""
|
| 431 |
+
Generate a complete markdown report from an AgentResult.
|
| 432 |
+
Suitable for download/export.
|
| 433 |
+
"""
|
| 434 |
+
total_time = sum(result.step_timings.values())
|
| 435 |
+
stats = result.diff_stats
|
| 436 |
+
|
| 437 |
+
report = f"""# π§ FixFlow Autonomous Bug Resolution Report
|
| 438 |
+
|
| 439 |
+
**Issue:** [{result.issue_data.get('title', 'Unknown')}]({result.issue_url})
|
| 440 |
+
**Repository:** {result.repo_url}
|
| 441 |
+
**Analysis Date:** {time.strftime('%Y-%m-%d %H:%M UTC')}
|
| 442 |
+
**Total Analysis Time:** {total_time:.1f}s
|
| 443 |
+
|
| 444 |
+
---
|
| 445 |
+
|
| 446 |
+
## π Step 1: Bug Summary
|
| 447 |
+
|
| 448 |
+
{result.bug_summary}
|
| 449 |
+
|
| 450 |
+
---
|
| 451 |
+
|
| 452 |
+
## π Step 2: Relevant Files Analysis
|
| 453 |
+
|
| 454 |
+
{result.relevant_files_analysis}
|
| 455 |
+
|
| 456 |
+
**Files Selected for Analysis:**
|
| 457 |
+
{chr(10).join(f'- `{p}`' for p in result.suspect_file_paths)}
|
| 458 |
+
|
| 459 |
+
---
|
| 460 |
+
|
| 461 |
+
## π¬ Step 3: Root Cause Analysis
|
| 462 |
+
|
| 463 |
+
{result.root_cause_analysis}
|
| 464 |
+
|
| 465 |
+
---
|
| 466 |
+
|
| 467 |
+
## π§ Step 4: Proposed Fix
|
| 468 |
+
|
| 469 |
+
**Diff Statistics:**
|
| 470 |
+
- Files changed: {stats.get('files_changed', 0)}
|
| 471 |
+
- Lines added: +{stats.get('lines_added', 0)}
|
| 472 |
+
- Lines removed: -{stats.get('lines_removed', 0)}
|
| 473 |
+
|
| 474 |
+
{result.diff_formatted}
|
| 475 |
+
|
| 476 |
+
---
|
| 477 |
+
|
| 478 |
+
## π Step 5: Fix Explanation (PR Description)
|
| 479 |
+
|
| 480 |
+
{result.fix_explanation}
|
| 481 |
+
|
| 482 |
+
---
|
| 483 |
+
|
| 484 |
+
{f"## π― Confidence Evaluation{chr(10)}{result.confidence_eval}{chr(10)}{chr(10)}---{chr(10)}" if result.confidence_eval else ""}
|
| 485 |
+
|
| 486 |
+
## β±οΈ Timing Breakdown
|
| 487 |
+
|
| 488 |
+
| Step | Duration |
|
| 489 |
+
|------|----------|
|
| 490 |
+
{"".join(f"| {k} | {v:.1f}s |{chr(10)}" for k, v in result.step_timings.items())}
|
| 491 |
+
|
| 492 |
+
---
|
| 493 |
+
*Generated by FixFlow β Autonomous Bug Resolution Agent powered by GLM 5.1*
|
| 494 |
+
"""
|
| 495 |
+
return report
|
backend/code_indexer.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Code indexer: parses repo structure and helps identify the most
|
| 3 |
+
relevant files for a given bug. No vector DB β pure in-memory.
|
| 4 |
+
"""
|
| 5 |
+
import logging
|
| 6 |
+
import re
|
| 7 |
+
from typing import List, Dict, Optional
|
| 8 |
+
|
| 9 |
+
from backend.config import CODE_EXTENSIONS, MAX_FILES_TO_ANALYZE
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def build_file_tree_string(files: List[Dict], max_lines: int = 300) -> str:
|
| 15 |
+
"""
|
| 16 |
+
Convert a flat list of file dicts into an indented tree string
|
| 17 |
+
suitable for LLM context.
|
| 18 |
+
"""
|
| 19 |
+
paths = sorted(f["path"] for f in files)
|
| 20 |
+
|
| 21 |
+
lines = []
|
| 22 |
+
prev_parts: List[str] = []
|
| 23 |
+
|
| 24 |
+
for path in paths:
|
| 25 |
+
parts = path.split("/")
|
| 26 |
+
# Find the common prefix depth
|
| 27 |
+
common = 0
|
| 28 |
+
for i, (a, b) in enumerate(zip(prev_parts, parts[:-1])):
|
| 29 |
+
if a == b:
|
| 30 |
+
common = i + 1
|
| 31 |
+
else:
|
| 32 |
+
break
|
| 33 |
+
|
| 34 |
+
# Print changed directory levels
|
| 35 |
+
for depth in range(common, len(parts) - 1):
|
| 36 |
+
indent = " " * depth
|
| 37 |
+
lines.append(f"{indent}π {parts[depth]}/")
|
| 38 |
+
|
| 39 |
+
indent = " " * (len(parts) - 1)
|
| 40 |
+
lines.append(f"{indent}π {parts[-1]}")
|
| 41 |
+
prev_parts = parts[:-1]
|
| 42 |
+
|
| 43 |
+
if len(lines) >= max_lines:
|
| 44 |
+
lines.append(f"... and more files ({len(paths) - paths.index(path) - 1} remaining)")
|
| 45 |
+
break
|
| 46 |
+
|
| 47 |
+
return "\n".join(lines)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def format_file_contents_for_prompt(
|
| 51 |
+
file_contents: Dict[str, str],
|
| 52 |
+
max_chars_per_file: int = 3000,
|
| 53 |
+
max_total_chars: int = 20000,
|
| 54 |
+
) -> str:
|
| 55 |
+
"""
|
| 56 |
+
Format multiple file contents into a single block for LLM context.
|
| 57 |
+
Truncates long files and respects a total character budget.
|
| 58 |
+
"""
|
| 59 |
+
sections = []
|
| 60 |
+
total_chars = 0
|
| 61 |
+
|
| 62 |
+
for path, content in file_contents.items():
|
| 63 |
+
if total_chars >= max_total_chars:
|
| 64 |
+
sections.append(f"[Remaining files omitted due to context limit]")
|
| 65 |
+
break
|
| 66 |
+
|
| 67 |
+
# Add line numbers for reference
|
| 68 |
+
lines = content.splitlines()
|
| 69 |
+
numbered = "\n".join(
|
| 70 |
+
f"{i+1:4d} | {line}" for i, line in enumerate(lines)
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
if len(numbered) > max_chars_per_file:
|
| 74 |
+
truncated = numbered[:max_chars_per_file]
|
| 75 |
+
# Find a clean line boundary
|
| 76 |
+
last_newline = truncated.rfind("\n")
|
| 77 |
+
if last_newline > 0:
|
| 78 |
+
truncated = truncated[:last_newline]
|
| 79 |
+
numbered = truncated + f"\n\n... [TRUNCATED β {len(lines)} total lines, showing first {truncated.count(chr(10))} lines]"
|
| 80 |
+
|
| 81 |
+
section = f"### File: `{path}`\n```\n{numbered}\n```"
|
| 82 |
+
sections.append(section)
|
| 83 |
+
total_chars += len(section)
|
| 84 |
+
|
| 85 |
+
return "\n\n".join(sections)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def extract_file_paths_from_llm_response(response: str) -> List[str]:
|
| 89 |
+
"""
|
| 90 |
+
Parse file paths from the LLM's relevance ranking response.
|
| 91 |
+
Looks for backtick-quoted paths like `path/to/file.py` or **`path/to/file.py`**.
|
| 92 |
+
"""
|
| 93 |
+
# Match paths in backticks
|
| 94 |
+
patterns = [
|
| 95 |
+
r"`([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)`", # `path/to/file.ext`
|
| 96 |
+
r"\*\*`([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)`\*\*", # **`path`**
|
| 97 |
+
]
|
| 98 |
+
paths = []
|
| 99 |
+
for pattern in patterns:
|
| 100 |
+
found = re.findall(pattern, response)
|
| 101 |
+
for p in found:
|
| 102 |
+
if p not in paths and "/" in p or "." in p:
|
| 103 |
+
paths.append(p)
|
| 104 |
+
|
| 105 |
+
return paths[:MAX_FILES_TO_ANALYZE]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def rank_files_by_keyword_match(
|
| 109 |
+
files: List[Dict],
|
| 110 |
+
keywords: List[str],
|
| 111 |
+
) -> List[Dict]:
|
| 112 |
+
"""
|
| 113 |
+
Quick keyword-based pre-filter before sending the full list to the LLM.
|
| 114 |
+
Returns files sorted by keyword match count (descending).
|
| 115 |
+
"""
|
| 116 |
+
scored = []
|
| 117 |
+
lc_keywords = [kw.lower() for kw in keywords]
|
| 118 |
+
|
| 119 |
+
for f in files:
|
| 120 |
+
path_lower = f["path"].lower()
|
| 121 |
+
score = sum(kw in path_lower for kw in lc_keywords)
|
| 122 |
+
scored.append((score, f))
|
| 123 |
+
|
| 124 |
+
scored.sort(key=lambda x: -x[0])
|
| 125 |
+
return [f for _, f in scored]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def extract_keywords_from_issue(issue_data: Dict) -> List[str]:
|
| 129 |
+
"""
|
| 130 |
+
Extract potential code-relevant keywords from an issue dict.
|
| 131 |
+
Used for pre-filtering before sending to LLM.
|
| 132 |
+
"""
|
| 133 |
+
text = " ".join([
|
| 134 |
+
issue_data.get("title", ""),
|
| 135 |
+
issue_data.get("body", ""),
|
| 136 |
+
]).lower()
|
| 137 |
+
|
| 138 |
+
# Extract likely identifiers: CamelCase, snake_case, module names
|
| 139 |
+
words = re.findall(r"\b[a-zA-Z][a-zA-Z0-9_]{2,}\b", text)
|
| 140 |
+
# Deduplicate while preserving order
|
| 141 |
+
seen = set()
|
| 142 |
+
keywords = []
|
| 143 |
+
for w in words:
|
| 144 |
+
lw = w.lower()
|
| 145 |
+
if lw not in seen and len(lw) > 3:
|
| 146 |
+
seen.add(lw)
|
| 147 |
+
keywords.append(lw)
|
| 148 |
+
|
| 149 |
+
return keywords[:30]
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def get_file_summary(path: str, content: str, max_chars: int = 500) -> str:
|
| 153 |
+
"""
|
| 154 |
+
Generate a quick summary of a file (first N chars of meaningful content).
|
| 155 |
+
Skips blank lines and comment-only lines at the top.
|
| 156 |
+
"""
|
| 157 |
+
lines = content.splitlines()
|
| 158 |
+
meaningful = []
|
| 159 |
+
for line in lines:
|
| 160 |
+
stripped = line.strip()
|
| 161 |
+
if stripped and not stripped.startswith("#") and not stripped.startswith("//"):
|
| 162 |
+
meaningful.append(line)
|
| 163 |
+
if len("\n".join(meaningful)) > max_chars:
|
| 164 |
+
break
|
| 165 |
+
preview = "\n".join(meaningful)[:max_chars]
|
| 166 |
+
return preview
|
backend/config.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FixFlow Configuration
|
| 3 |
+
All API keys, model config, and constants loaded from environment variables.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
# ββ LLM Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
+
GLM_API_KEY: str = os.getenv("GLM_API_KEY", "")
|
| 12 |
+
GLM_BASE_URL: str = os.getenv("GLM_BASE_URL", "https://open.bigmodel.cn/api/paas/v4")
|
| 13 |
+
GLM_MODEL: str = os.getenv("GLM_MODEL", "glm-5-plus")
|
| 14 |
+
|
| 15 |
+
# ββ GitHub Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 16 |
+
GITHUB_TOKEN: str = os.getenv("GITHUB_TOKEN", "")
|
| 17 |
+
|
| 18 |
+
# ββ Agent Limits βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
MAX_FILES_TO_SCAN: int = int(os.getenv("MAX_FILES_TO_SCAN", "100"))
|
| 20 |
+
MAX_FILE_SIZE_BYTES: int = int(os.getenv("MAX_FILE_SIZE_BYTES", "51200")) # 50 KB
|
| 21 |
+
MAX_FILES_TO_ANALYZE: int = 10 # Top N files sent to deep analysis
|
| 22 |
+
MAX_REPO_FILES: int = 500 # Hard cap on tree traversal
|
| 23 |
+
|
| 24 |
+
# ββ File Filters (skip these in code analysis) βββββββββββββββββββββββββββββββ
|
| 25 |
+
IGNORE_EXTENSIONS = {
|
| 26 |
+
".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp",
|
| 27 |
+
".mp4", ".mp3", ".wav", ".pdf", ".zip", ".tar", ".gz",
|
| 28 |
+
".woff", ".woff2", ".ttf", ".eot",
|
| 29 |
+
".lock", ".sum", ".mod",
|
| 30 |
+
".pyc", ".pyo", ".pyd",
|
| 31 |
+
".class", ".jar",
|
| 32 |
+
".DS_Store",
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
IGNORE_DIRS = {
|
| 36 |
+
"node_modules", ".git", ".github", "__pycache__", ".venv", "venv",
|
| 37 |
+
"env", "dist", "build", ".next", ".nuxt", "coverage", ".pytest_cache",
|
| 38 |
+
"vendor", "third_party", "external", "site-packages",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
CODE_EXTENSIONS = {
|
| 42 |
+
".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rb", ".rs",
|
| 43 |
+
".cpp", ".c", ".h", ".hpp", ".cs", ".php", ".swift", ".kt", ".scala",
|
| 44 |
+
".sh", ".bash", ".yaml", ".yml", ".toml", ".cfg", ".ini", ".env",
|
| 45 |
+
".json", ".xml", ".html", ".css", ".scss", ".sql", ".md",
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# ββ Timing & Logging βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 49 |
+
LOG_LLM_CALLS: bool = os.getenv("LOG_LLM_CALLS", "true").lower() == "true"
|
backend/diff_generator.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Diff generator: creates unified diffs from original vs. fixed file contents.
|
| 3 |
+
"""
|
| 4 |
+
import difflib
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Dict, List, Tuple
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def generate_unified_diff(
|
| 12 |
+
original_content: str,
|
| 13 |
+
fixed_content: str,
|
| 14 |
+
filename: str,
|
| 15 |
+
context_lines: int = 5,
|
| 16 |
+
) -> str:
|
| 17 |
+
"""
|
| 18 |
+
Generate a unified diff between two versions of a file.
|
| 19 |
+
Returns the diff as a string.
|
| 20 |
+
"""
|
| 21 |
+
original_lines = original_content.splitlines(keepends=True)
|
| 22 |
+
fixed_lines = fixed_content.splitlines(keepends=True)
|
| 23 |
+
|
| 24 |
+
diff = difflib.unified_diff(
|
| 25 |
+
original_lines,
|
| 26 |
+
fixed_lines,
|
| 27 |
+
fromfile=f"a/{filename}",
|
| 28 |
+
tofile=f"b/{filename}",
|
| 29 |
+
n=context_lines,
|
| 30 |
+
)
|
| 31 |
+
return "".join(diff)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def generate_all_diffs(
|
| 35 |
+
original_files: Dict[str, str],
|
| 36 |
+
fixed_files: Dict[str, str],
|
| 37 |
+
) -> Dict[str, str]:
|
| 38 |
+
"""
|
| 39 |
+
Generate unified diffs for all changed files.
|
| 40 |
+
Returns {filepath: diff_string}.
|
| 41 |
+
Only includes files that actually changed.
|
| 42 |
+
"""
|
| 43 |
+
diffs = {}
|
| 44 |
+
|
| 45 |
+
for filepath, fixed_content in fixed_files.items():
|
| 46 |
+
original = original_files.get(filepath, "")
|
| 47 |
+
|
| 48 |
+
# Normalize line endings for comparison
|
| 49 |
+
orig_normalized = original.replace("\r\n", "\n").strip()
|
| 50 |
+
fixed_normalized = fixed_content.replace("\r\n", "\n").strip()
|
| 51 |
+
|
| 52 |
+
if orig_normalized == fixed_normalized:
|
| 53 |
+
logger.info("No changes in %s β skipping diff", filepath)
|
| 54 |
+
continue
|
| 55 |
+
|
| 56 |
+
diff = generate_unified_diff(
|
| 57 |
+
original,
|
| 58 |
+
fixed_content,
|
| 59 |
+
filepath,
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
if diff.strip():
|
| 63 |
+
diffs[filepath] = diff
|
| 64 |
+
changed_lines = _count_changed_lines(diff)
|
| 65 |
+
logger.info(
|
| 66 |
+
"Generated diff for %s: +%d -%d lines",
|
| 67 |
+
filepath, changed_lines[0], changed_lines[1],
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
return diffs
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _count_changed_lines(diff: str) -> Tuple[int, int]:
|
| 74 |
+
"""Count added and removed lines in a unified diff."""
|
| 75 |
+
added = sum(1 for line in diff.splitlines() if line.startswith("+") and not line.startswith("+++"))
|
| 76 |
+
removed = sum(1 for line in diff.splitlines() if line.startswith("-") and not line.startswith("---"))
|
| 77 |
+
return added, removed
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def format_diff_for_display(diffs: Dict[str, str]) -> str:
|
| 81 |
+
"""
|
| 82 |
+
Format all diffs into a single markdown code block for display.
|
| 83 |
+
"""
|
| 84 |
+
if not diffs:
|
| 85 |
+
return "No changes generated."
|
| 86 |
+
|
| 87 |
+
parts = []
|
| 88 |
+
for filepath, diff in diffs.items():
|
| 89 |
+
added, removed = _count_changed_lines(diff)
|
| 90 |
+
parts.append(
|
| 91 |
+
f"### `{filepath}` (+{added} / -{removed} lines)\n"
|
| 92 |
+
f"```diff\n{diff}\n```"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
return "\n\n".join(parts)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def parse_fixed_files_from_llm_response(
|
| 99 |
+
response: str,
|
| 100 |
+
suspect_files: List[str],
|
| 101 |
+
) -> Dict[str, str]:
|
| 102 |
+
"""
|
| 103 |
+
Parse the LLM's fix generation response to extract {filepath: content}.
|
| 104 |
+
|
| 105 |
+
The LLM is asked to output:
|
| 106 |
+
### Fix for `path/to/file.py`
|
| 107 |
+
```python
|
| 108 |
+
<full file content>
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
This function extracts those code blocks.
|
| 112 |
+
"""
|
| 113 |
+
import re
|
| 114 |
+
|
| 115 |
+
fixed_files = {}
|
| 116 |
+
|
| 117 |
+
# Pattern: ### Fix for `filepath` ... ```lang\n<content>\n```
|
| 118 |
+
pattern = re.compile(
|
| 119 |
+
r"Fix for `([^`]+)`.*?```(?:\w+)?\n(.*?)```",
|
| 120 |
+
re.DOTALL | re.IGNORECASE,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
for match in pattern.finditer(response):
|
| 124 |
+
filepath = match.group(1).strip()
|
| 125 |
+
content = match.group(2)
|
| 126 |
+
|
| 127 |
+
# Clean up the content
|
| 128 |
+
content = content.rstrip()
|
| 129 |
+
|
| 130 |
+
# Verify the filepath looks reasonable
|
| 131 |
+
if "/" in filepath or "." in filepath:
|
| 132 |
+
fixed_files[filepath] = content
|
| 133 |
+
logger.info("Parsed fixed content for: %s (%d chars)", filepath, len(content))
|
| 134 |
+
|
| 135 |
+
# Fallback: try to match any filepath from suspect_files
|
| 136 |
+
if not fixed_files:
|
| 137 |
+
logger.warning("Could not parse fix blocks from LLM response β trying fallback")
|
| 138 |
+
for fp in suspect_files:
|
| 139 |
+
# Look for content near the filename mention
|
| 140 |
+
escaped = re.escape(fp)
|
| 141 |
+
m = re.search(
|
| 142 |
+
escaped + r".*?```(?:\w+)?\n(.*?)```",
|
| 143 |
+
response,
|
| 144 |
+
re.DOTALL,
|
| 145 |
+
)
|
| 146 |
+
if m:
|
| 147 |
+
fixed_files[fp] = m.group(1).rstrip()
|
| 148 |
+
|
| 149 |
+
return fixed_files
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def get_diff_stats(diffs: Dict[str, str]) -> Dict:
|
| 153 |
+
"""Return aggregate stats about the diffs."""
|
| 154 |
+
total_added = 0
|
| 155 |
+
total_removed = 0
|
| 156 |
+
for diff in diffs.values():
|
| 157 |
+
a, r = _count_changed_lines(diff)
|
| 158 |
+
total_added += a
|
| 159 |
+
total_removed += r
|
| 160 |
+
|
| 161 |
+
return {
|
| 162 |
+
"files_changed": len(diffs),
|
| 163 |
+
"lines_added": total_added,
|
| 164 |
+
"lines_removed": total_removed,
|
| 165 |
+
}
|
backend/github_client.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GitHub client for fetching issues, repo trees, and file contents.
|
| 3 |
+
Supports both public repos (no auth) and private repos (with token).
|
| 4 |
+
"""
|
| 5 |
+
import re
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Dict, List, Optional, Tuple
|
| 8 |
+
from urllib.parse import urlparse
|
| 9 |
+
|
| 10 |
+
import requests
|
| 11 |
+
from github import Github, GithubException, Auth
|
| 12 |
+
|
| 13 |
+
from backend.config import (
|
| 14 |
+
GITHUB_TOKEN,
|
| 15 |
+
IGNORE_EXTENSIONS,
|
| 16 |
+
IGNORE_DIRS,
|
| 17 |
+
CODE_EXTENSIONS,
|
| 18 |
+
MAX_FILE_SIZE_BYTES,
|
| 19 |
+
MAX_REPO_FILES,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ββ URL Parsing Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
|
| 27 |
+
def parse_issue_url(issue_url: str) -> Tuple[str, str, int]:
|
| 28 |
+
"""
|
| 29 |
+
Parse a GitHub issue URL into (owner, repo, issue_number).
|
| 30 |
+
Supports:
|
| 31 |
+
https://github.com/owner/repo/issues/123
|
| 32 |
+
"""
|
| 33 |
+
issue_url = issue_url.strip().rstrip("/")
|
| 34 |
+
pattern = r"github\.com/([^/]+)/([^/]+)/issues/(\d+)"
|
| 35 |
+
match = re.search(pattern, issue_url)
|
| 36 |
+
if not match:
|
| 37 |
+
raise ValueError(
|
| 38 |
+
f"Could not parse GitHub issue URL: {issue_url!r}\n"
|
| 39 |
+
"Expected format: https://github.com/owner/repo/issues/123"
|
| 40 |
+
)
|
| 41 |
+
owner, repo, issue_num = match.groups()
|
| 42 |
+
return owner, repo, int(issue_num)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def parse_repo_url(repo_url: str) -> Tuple[str, str]:
|
| 46 |
+
"""
|
| 47 |
+
Parse a GitHub repo URL into (owner, repo).
|
| 48 |
+
Supports:
|
| 49 |
+
https://github.com/owner/repo
|
| 50 |
+
https://github.com/owner/repo.git
|
| 51 |
+
"""
|
| 52 |
+
repo_url = repo_url.strip().rstrip("/").removesuffix(".git")
|
| 53 |
+
pattern = r"github\.com/([^/]+)/([^/]+)"
|
| 54 |
+
match = re.search(pattern, repo_url)
|
| 55 |
+
if not match:
|
| 56 |
+
raise ValueError(
|
| 57 |
+
f"Could not parse GitHub repo URL: {repo_url!r}\n"
|
| 58 |
+
"Expected format: https://github.com/owner/repo"
|
| 59 |
+
)
|
| 60 |
+
owner, repo = match.groups()
|
| 61 |
+
return owner, repo
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ββ GitHub Client βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 65 |
+
|
| 66 |
+
class GitHubClient:
|
| 67 |
+
"""Wraps PyGithub for FixFlow's use cases."""
|
| 68 |
+
|
| 69 |
+
def __init__(self, token: Optional[str] = None):
|
| 70 |
+
tok = token or GITHUB_TOKEN
|
| 71 |
+
if tok:
|
| 72 |
+
auth = Auth.Token(tok)
|
| 73 |
+
self._gh = Github(auth=auth)
|
| 74 |
+
else:
|
| 75 |
+
self._gh = Github() # unauthenticated (60 req/hr)
|
| 76 |
+
self._rate_limit_warned = False
|
| 77 |
+
|
| 78 |
+
# ββ Issue Fetching ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 79 |
+
|
| 80 |
+
def fetch_issue(self, issue_url: str) -> Dict:
|
| 81 |
+
"""
|
| 82 |
+
Fetch a GitHub issue and return a structured dict:
|
| 83 |
+
{title, body, labels, state, author, comments, url}
|
| 84 |
+
"""
|
| 85 |
+
owner, repo_name, issue_num = parse_issue_url(issue_url)
|
| 86 |
+
logger.info("Fetching issue #%d from %s/%s", issue_num, owner, repo_name)
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
repo = self._gh.get_repo(f"{owner}/{repo_name}")
|
| 90 |
+
issue = repo.get_issue(number=issue_num)
|
| 91 |
+
except GithubException as e:
|
| 92 |
+
raise RuntimeError(
|
| 93 |
+
f"Failed to fetch issue from GitHub: {e.data.get('message', str(e))}"
|
| 94 |
+
) from e
|
| 95 |
+
|
| 96 |
+
# Collect top comments (up to 10)
|
| 97 |
+
comments = []
|
| 98 |
+
try:
|
| 99 |
+
for comment in issue.get_comments():
|
| 100 |
+
comments.append({
|
| 101 |
+
"author": comment.user.login if comment.user else "unknown",
|
| 102 |
+
"body": comment.body or "",
|
| 103 |
+
"created_at": str(comment.created_at),
|
| 104 |
+
})
|
| 105 |
+
if len(comments) >= 10:
|
| 106 |
+
break
|
| 107 |
+
except GithubException:
|
| 108 |
+
pass
|
| 109 |
+
|
| 110 |
+
return {
|
| 111 |
+
"title": issue.title or "",
|
| 112 |
+
"body": issue.body or "",
|
| 113 |
+
"labels": [lbl.name for lbl in issue.labels],
|
| 114 |
+
"state": issue.state,
|
| 115 |
+
"author": issue.user.login if issue.user else "unknown",
|
| 116 |
+
"url": issue.html_url,
|
| 117 |
+
"number": issue_num,
|
| 118 |
+
"comments": comments,
|
| 119 |
+
"repo_owner": owner,
|
| 120 |
+
"repo_name": repo_name,
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
# ββ Repo Tree βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 124 |
+
|
| 125 |
+
def fetch_repo_tree(
|
| 126 |
+
self,
|
| 127 |
+
repo_url: str,
|
| 128 |
+
token: Optional[str] = None,
|
| 129 |
+
) -> List[Dict]:
|
| 130 |
+
"""
|
| 131 |
+
Return a flat list of code files in the repo.
|
| 132 |
+
Each entry: {path, size, type}
|
| 133 |
+
Filters out binary files, ignored dirs, etc.
|
| 134 |
+
"""
|
| 135 |
+
owner, repo_name = parse_repo_url(repo_url)
|
| 136 |
+
logger.info("Fetching repo tree for %s/%s", owner, repo_name)
|
| 137 |
+
|
| 138 |
+
# Refresh client if a token was provided on this call
|
| 139 |
+
if token and not GITHUB_TOKEN:
|
| 140 |
+
auth = Auth.Token(token)
|
| 141 |
+
self._gh = Github(auth=auth)
|
| 142 |
+
|
| 143 |
+
try:
|
| 144 |
+
repo = self._gh.get_repo(f"{owner}/{repo_name}")
|
| 145 |
+
# Use recursive git tree for efficiency
|
| 146 |
+
tree = repo.get_git_tree("HEAD", recursive=True)
|
| 147 |
+
except GithubException as e:
|
| 148 |
+
raise RuntimeError(
|
| 149 |
+
f"Failed to fetch repo tree: {e.data.get('message', str(e))}"
|
| 150 |
+
) from e
|
| 151 |
+
|
| 152 |
+
files = []
|
| 153 |
+
for item in tree.tree:
|
| 154 |
+
if item.type != "blob":
|
| 155 |
+
continue
|
| 156 |
+
path = item.path
|
| 157 |
+
|
| 158 |
+
# Skip ignored directories
|
| 159 |
+
parts = path.split("/")
|
| 160 |
+
if any(p in IGNORE_DIRS for p in parts[:-1]):
|
| 161 |
+
continue
|
| 162 |
+
|
| 163 |
+
# Skip ignored/non-code extensions
|
| 164 |
+
ext = "." + path.rsplit(".", 1)[-1].lower() if "." in path else ""
|
| 165 |
+
if ext in IGNORE_EXTENSIONS:
|
| 166 |
+
continue
|
| 167 |
+
if ext not in CODE_EXTENSIONS and ext:
|
| 168 |
+
continue
|
| 169 |
+
|
| 170 |
+
# Skip overly large files
|
| 171 |
+
size = item.size or 0
|
| 172 |
+
if size > MAX_FILE_SIZE_BYTES:
|
| 173 |
+
logger.debug("Skipping large file (%d bytes): %s", size, path)
|
| 174 |
+
continue
|
| 175 |
+
|
| 176 |
+
files.append({"path": path, "size": size, "type": item.type})
|
| 177 |
+
if len(files) >= MAX_REPO_FILES:
|
| 178 |
+
logger.warning("Hit MAX_REPO_FILES limit (%d)", MAX_REPO_FILES)
|
| 179 |
+
break
|
| 180 |
+
|
| 181 |
+
logger.info("Found %d code files in %s/%s", len(files), owner, repo_name)
|
| 182 |
+
return files
|
| 183 |
+
|
| 184 |
+
# ββ File Content ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 185 |
+
|
| 186 |
+
def fetch_file_content(
|
| 187 |
+
self,
|
| 188 |
+
repo_url: str,
|
| 189 |
+
file_path: str,
|
| 190 |
+
) -> str:
|
| 191 |
+
"""
|
| 192 |
+
Fetch the raw text content of a single file from the repo.
|
| 193 |
+
Returns empty string on failure (binary, too large, etc).
|
| 194 |
+
"""
|
| 195 |
+
owner, repo_name = parse_repo_url(repo_url)
|
| 196 |
+
try:
|
| 197 |
+
repo = self._gh.get_repo(f"{owner}/{repo_name}")
|
| 198 |
+
content_obj = repo.get_contents(file_path)
|
| 199 |
+
# Handle list (shouldn't happen for blobs, but defensive)
|
| 200 |
+
if isinstance(content_obj, list):
|
| 201 |
+
content_obj = content_obj[0]
|
| 202 |
+
if content_obj.size > MAX_FILE_SIZE_BYTES:
|
| 203 |
+
return f"[File too large to display: {content_obj.size} bytes]"
|
| 204 |
+
decoded = content_obj.decoded_content
|
| 205 |
+
return decoded.decode("utf-8", errors="replace")
|
| 206 |
+
except GithubException as e:
|
| 207 |
+
logger.warning("Could not fetch %s: %s", file_path, e)
|
| 208 |
+
return ""
|
| 209 |
+
except Exception as e:
|
| 210 |
+
logger.warning("Error decoding %s: %s", file_path, e)
|
| 211 |
+
return ""
|
| 212 |
+
|
| 213 |
+
def fetch_multiple_files(
|
| 214 |
+
self,
|
| 215 |
+
repo_url: str,
|
| 216 |
+
file_paths: List[str],
|
| 217 |
+
) -> Dict[str, str]:
|
| 218 |
+
"""
|
| 219 |
+
Fetch contents of multiple files. Returns {path: content} dict.
|
| 220 |
+
"""
|
| 221 |
+
result = {}
|
| 222 |
+
owner, repo_name = parse_repo_url(repo_url)
|
| 223 |
+
logger.info("Fetching %d files from %s/%s", len(file_paths), owner, repo_name)
|
| 224 |
+
|
| 225 |
+
for path in file_paths:
|
| 226 |
+
content = self.fetch_file_content(repo_url, path)
|
| 227 |
+
if content:
|
| 228 |
+
result[path] = content
|
| 229 |
+
return result
|
| 230 |
+
|
| 231 |
+
# ββ Rate Limit Info βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 232 |
+
|
| 233 |
+
def get_rate_limit_info(self) -> Dict:
|
| 234 |
+
"""Return current GitHub API rate limit information."""
|
| 235 |
+
try:
|
| 236 |
+
rl = self._gh.get_rate_limit()
|
| 237 |
+
return {
|
| 238 |
+
"core_remaining": rl.core.remaining,
|
| 239 |
+
"core_limit": rl.core.limit,
|
| 240 |
+
"reset_at": str(rl.core.reset),
|
| 241 |
+
}
|
| 242 |
+
except Exception:
|
| 243 |
+
return {}
|
backend/llm_client.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM Client for GLM 5.1 via Z.ai API (OpenAI-compatible endpoint).
|
| 3 |
+
"""
|
| 4 |
+
import time
|
| 5 |
+
import logging
|
| 6 |
+
from typing import Iterator, List, Dict, Any, Optional
|
| 7 |
+
import openai
|
| 8 |
+
from backend.config import GLM_API_KEY, GLM_BASE_URL, GLM_MODEL, LOG_LLM_CALLS
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class GLMClient:
|
| 14 |
+
"""OpenAI-compatible wrapper for Z.ai's GLM models."""
|
| 15 |
+
|
| 16 |
+
def __init__(
|
| 17 |
+
self,
|
| 18 |
+
api_key: Optional[str] = None,
|
| 19 |
+
base_url: str = GLM_BASE_URL,
|
| 20 |
+
model: str = GLM_MODEL,
|
| 21 |
+
):
|
| 22 |
+
self.api_key = api_key or GLM_API_KEY
|
| 23 |
+
self.base_url = base_url
|
| 24 |
+
self.model = model
|
| 25 |
+
self._client: Optional[openai.OpenAI] = None
|
| 26 |
+
|
| 27 |
+
def _get_client(self) -> openai.OpenAI:
|
| 28 |
+
if self._client is None:
|
| 29 |
+
if not self.api_key:
|
| 30 |
+
raise ValueError(
|
| 31 |
+
"GLM API key is not set. Please provide it in the sidebar or .env file."
|
| 32 |
+
)
|
| 33 |
+
self._client = openai.OpenAI(
|
| 34 |
+
api_key=self.api_key,
|
| 35 |
+
base_url=self.base_url,
|
| 36 |
+
)
|
| 37 |
+
return self._client
|
| 38 |
+
|
| 39 |
+
def chat(
|
| 40 |
+
self,
|
| 41 |
+
messages: List[Dict[str, str]],
|
| 42 |
+
temperature: float = 0.3,
|
| 43 |
+
max_tokens: int = 4096,
|
| 44 |
+
) -> str:
|
| 45 |
+
"""Synchronous chat completion. Returns the full response string."""
|
| 46 |
+
client = self._get_client()
|
| 47 |
+
start = time.time()
|
| 48 |
+
|
| 49 |
+
if LOG_LLM_CALLS:
|
| 50 |
+
logger.info(
|
| 51 |
+
"[GLM] chat() | model=%s | messages=%d | temp=%.1f",
|
| 52 |
+
self.model, len(messages), temperature,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
response = client.chat.completions.create(
|
| 56 |
+
model=self.model,
|
| 57 |
+
messages=messages,
|
| 58 |
+
temperature=temperature,
|
| 59 |
+
max_tokens=max_tokens,
|
| 60 |
+
)
|
| 61 |
+
content = response.choices[0].message.content or ""
|
| 62 |
+
elapsed = time.time() - start
|
| 63 |
+
|
| 64 |
+
if LOG_LLM_CALLS:
|
| 65 |
+
logger.info("[GLM] completed in %.2fs | output_chars=%d", elapsed, len(content))
|
| 66 |
+
|
| 67 |
+
return content
|
| 68 |
+
|
| 69 |
+
def chat_stream(
|
| 70 |
+
self,
|
| 71 |
+
messages: List[Dict[str, str]],
|
| 72 |
+
temperature: float = 0.3,
|
| 73 |
+
max_tokens: int = 4096,
|
| 74 |
+
) -> Iterator[str]:
|
| 75 |
+
"""Streaming chat completion. Yields text chunks as they arrive."""
|
| 76 |
+
client = self._get_client()
|
| 77 |
+
|
| 78 |
+
if LOG_LLM_CALLS:
|
| 79 |
+
logger.info(
|
| 80 |
+
"[GLM] chat_stream() | model=%s | messages=%d",
|
| 81 |
+
self.model, len(messages),
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
response = client.chat.completions.create(
|
| 85 |
+
model=self.model,
|
| 86 |
+
messages=messages,
|
| 87 |
+
temperature=temperature,
|
| 88 |
+
max_tokens=max_tokens,
|
| 89 |
+
stream=True,
|
| 90 |
+
)
|
| 91 |
+
for chunk in response:
|
| 92 |
+
delta = chunk.choices[0].delta
|
| 93 |
+
if delta and delta.content:
|
| 94 |
+
yield delta.content
|
| 95 |
+
|
| 96 |
+
def update_api_key(self, api_key: str) -> None:
|
| 97 |
+
"""Allow hot-swapping the API key (e.g. from Streamlit sidebar)."""
|
| 98 |
+
self.api_key = api_key
|
| 99 |
+
self._client = None # Force re-initialization
|
backend/prompts.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
All LLM prompt templates for the FixFlow agent pipeline.
|
| 3 |
+
Each prompt includes a system message + user message pair.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
# ββ Shared system message βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
SYSTEM_MESSAGE = (
|
| 8 |
+
"You are FixFlow, an expert senior debugging engineer with 20+ years of "
|
| 9 |
+
"experience in software debugging, code review, and root cause analysis. "
|
| 10 |
+
"You systematically analyze bug reports and codebases to identify root causes "
|
| 11 |
+
"and generate precise, minimal fixes. You ALWAYS show your reasoning step-by-step. "
|
| 12 |
+
"You reference specific files, functions, and line numbers. "
|
| 13 |
+
"Your analysis is thorough, your explanations are clear, and your fixes are "
|
| 14 |
+
"safe and well-reasoned. You never make assumptions without stating them."
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ββ Step 1: Issue Understanding βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
ISSUE_ANALYSIS_PROMPT = """You have been given a GitHub issue to analyze. Your task is to extract a structured bug summary.
|
| 20 |
+
|
| 21 |
+
## GitHub Issue Details
|
| 22 |
+
|
| 23 |
+
**Title:** {title}
|
| 24 |
+
|
| 25 |
+
**Body:**
|
| 26 |
+
{body}
|
| 27 |
+
|
| 28 |
+
**Labels:** {labels}
|
| 29 |
+
|
| 30 |
+
**Comments (most relevant):**
|
| 31 |
+
{comments}
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## Your Task
|
| 36 |
+
|
| 37 |
+
Carefully read the issue and extract the following information. Be precise and include exact quotes where relevant.
|
| 38 |
+
|
| 39 |
+
Respond with a structured markdown document using EXACTLY this format:
|
| 40 |
+
|
| 41 |
+
### π Error Message
|
| 42 |
+
(The exact error message, exception, or failure description. Quote directly if possible.)
|
| 43 |
+
|
| 44 |
+
### β
Expected Behavior
|
| 45 |
+
(What the user/reporter expected to happen)
|
| 46 |
+
|
| 47 |
+
### β Actual Behavior
|
| 48 |
+
(What actually happened β the bug behavior)
|
| 49 |
+
|
| 50 |
+
### π Reproduction Steps
|
| 51 |
+
(Numbered list of steps to reproduce, if provided)
|
| 52 |
+
|
| 53 |
+
### π― Affected Components
|
| 54 |
+
(Your best guess at which modules, files, functions, or subsystems are affected based on the issue text. List as bullet points.)
|
| 55 |
+
|
| 56 |
+
### π Key Technical Clues
|
| 57 |
+
(Specific technical details: version numbers, stack traces, config values, edge cases β anything that will help locate the bug)
|
| 58 |
+
|
| 59 |
+
### π‘ Hypothesis
|
| 60 |
+
(Your initial hypothesis about the root cause, stated clearly with reasoning)
|
| 61 |
+
|
| 62 |
+
Be thorough but concise. If information is not available, write "Not specified" rather than guessing.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ββ Step 2: Codebase Mapping ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
+
FILE_RELEVANCE_PROMPT = """You are analyzing a codebase to find files relevant to a bug report.
|
| 68 |
+
|
| 69 |
+
## Bug Summary
|
| 70 |
+
{bug_summary}
|
| 71 |
+
|
| 72 |
+
## Repository File Tree
|
| 73 |
+
```
|
| 74 |
+
{file_tree}
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Repository: {repo_name}
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## Your Task
|
| 82 |
+
|
| 83 |
+
Identify the TOP 5-10 most relevant files that are likely related to this bug.
|
| 84 |
+
|
| 85 |
+
Think step-by-step:
|
| 86 |
+
1. First, consider what the error message tells you about the code path
|
| 87 |
+
2. Then look at affected components mentioned in the bug
|
| 88 |
+
3. Consider entry points, utilities, and configuration files
|
| 89 |
+
4. Look for files matching the error traceback if one was provided
|
| 90 |
+
|
| 91 |
+
Respond with EXACTLY this format:
|
| 92 |
+
|
| 93 |
+
### πΊοΈ Codebase Analysis
|
| 94 |
+
|
| 95 |
+
**Repository structure overview:** (2-3 sentences about what kind of codebase this is)
|
| 96 |
+
|
| 97 |
+
### π Relevant Files (Ranked by Suspicion)
|
| 98 |
+
|
| 99 |
+
For each file, provide:
|
| 100 |
+
|
| 101 |
+
**[Rank]. `path/to/file.py`**
|
| 102 |
+
- **Relevance score:** X/10
|
| 103 |
+
- **Why relevant:** (specific reasoning β what in this file could cause the bug)
|
| 104 |
+
- **What to look for:** (specific functions, classes, or patterns to inspect)
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
(Repeat for each file, ranked from most to least suspicious)
|
| 109 |
+
|
| 110 |
+
### π Files to Skip
|
| 111 |
+
(Brief note on any obviously irrelevant areas of the codebase)
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# ββ Step 3: Deep Code Analysis ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
ROOT_CAUSE_PROMPT = """You are performing a deep code analysis to identify the root cause of a bug.
|
| 117 |
+
|
| 118 |
+
## Bug Summary
|
| 119 |
+
{bug_summary}
|
| 120 |
+
|
| 121 |
+
## Suspect Files and Content
|
| 122 |
+
|
| 123 |
+
{file_contents}
|
| 124 |
+
|
| 125 |
+
---
|
| 126 |
+
|
| 127 |
+
## Your Task
|
| 128 |
+
|
| 129 |
+
Trace the execution flow and identify the EXACT root cause of the bug.
|
| 130 |
+
|
| 131 |
+
**You MUST:**
|
| 132 |
+
- Reference specific file names, function names, and line numbers
|
| 133 |
+
- Show your chain-of-thought reasoning
|
| 134 |
+
- Trace the call chain from entry point to failure
|
| 135 |
+
- Identify the exact line(s) where the bug originates
|
| 136 |
+
|
| 137 |
+
Respond with EXACTLY this format:
|
| 138 |
+
|
| 139 |
+
### π¬ Root Cause Analysis
|
| 140 |
+
|
| 141 |
+
#### Executive Summary
|
| 142 |
+
(1-2 sentences: what is the root cause in plain English)
|
| 143 |
+
|
| 144 |
+
#### π§ Chain-of-Thought Reasoning
|
| 145 |
+
|
| 146 |
+
**Step 1: Entry Point**
|
| 147 |
+
(Where does execution start for this bug? What triggers it?)
|
| 148 |
+
|
| 149 |
+
**Step 2: Execution Trace**
|
| 150 |
+
(Follow the code path step by step. For each step, cite: `filename.py:function_name()` or `filename.py:LineN`)
|
| 151 |
+
|
| 152 |
+
**Step 3: The Bug**
|
| 153 |
+
(The exact location and nature of the bug. Be precise: "In `file.py`, line N, function `foo()` does X when it should do Y because...")
|
| 154 |
+
|
| 155 |
+
**Step 4: Why This Causes the Reported Behavior**
|
| 156 |
+
(Connect the bug to the symptoms described in the issue)
|
| 157 |
+
|
| 158 |
+
#### π Bug Location
|
| 159 |
+
- **File:** `path/to/file.py`
|
| 160 |
+
- **Function/Class:** `function_name()` / `ClassName`
|
| 161 |
+
- **Line(s):** ~N (approximate)
|
| 162 |
+
- **Type:** (e.g., off-by-one error, null check missing, race condition, type mismatch, etc.)
|
| 163 |
+
|
| 164 |
+
#### β οΈ Contributing Factors
|
| 165 |
+
(Any secondary issues, missed validations, or design problems that make this worse)
|
| 166 |
+
|
| 167 |
+
#### π― Confidence Level
|
| 168 |
+
(High/Medium/Low) β and why
|
| 169 |
+
|
| 170 |
+
Be thorough. Show your work. Reference specific code.
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
# ββ Step 4: Fix Generation ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 175 |
+
FIX_GENERATION_PROMPT = """You are generating a precise, minimal fix for a confirmed bug.
|
| 176 |
+
|
| 177 |
+
## Root Cause Analysis
|
| 178 |
+
{root_cause}
|
| 179 |
+
|
| 180 |
+
## Files to Fix
|
| 181 |
+
|
| 182 |
+
{file_contents}
|
| 183 |
+
|
| 184 |
+
---
|
| 185 |
+
|
| 186 |
+
## Your Task
|
| 187 |
+
|
| 188 |
+
Generate corrected versions of the affected files.
|
| 189 |
+
|
| 190 |
+
**Rules for the fix:**
|
| 191 |
+
1. Make the MINIMAL change needed β don't refactor unrelated code
|
| 192 |
+
2. The fix must directly address the root cause identified above
|
| 193 |
+
3. Add a comment explaining WHY the change was made (not just what)
|
| 194 |
+
4. Preserve existing code style, formatting, and conventions
|
| 195 |
+
5. Consider edge cases your fix must handle
|
| 196 |
+
|
| 197 |
+
For EACH file that needs changes, provide:
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
### Fix for `{filepath_placeholder}`
|
| 202 |
+
|
| 203 |
+
**What changed and why:**
|
| 204 |
+
(Brief explanation of the change)
|
| 205 |
+
|
| 206 |
+
**Fixed code:**
|
| 207 |
+
```python
|
| 208 |
+
(FULL content of the fixed file β complete, not just the changed section)
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
---
|
| 212 |
+
|
| 213 |
+
If multiple files need changes, repeat the above section for each file.
|
| 214 |
+
|
| 215 |
+
After all fixes, add:
|
| 216 |
+
|
| 217 |
+
### β
Fix Summary
|
| 218 |
+
- Files changed: N
|
| 219 |
+
- Nature of fix: (one-liner)
|
| 220 |
+
- Risk level: Low/Medium/High (and why)
|
| 221 |
+
- Edge cases handled: (bullet list)
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
# ββ Step 5: Fix Explanation βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 226 |
+
FIX_EXPLANATION_PROMPT = """You are writing a human-readable explanation of a code fix for a pull request.
|
| 227 |
+
|
| 228 |
+
## Original Bug
|
| 229 |
+
{bug_summary}
|
| 230 |
+
|
| 231 |
+
## Root Cause
|
| 232 |
+
{root_cause_summary}
|
| 233 |
+
|
| 234 |
+
## Changes Made (Unified Diff)
|
| 235 |
+
```diff
|
| 236 |
+
{unified_diff}
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
---
|
| 240 |
+
|
| 241 |
+
## Your Task
|
| 242 |
+
|
| 243 |
+
Write a clear, friendly, professional pull request description that a human reviewer can read to quickly understand and verify this fix.
|
| 244 |
+
|
| 245 |
+
Respond with EXACTLY this format:
|
| 246 |
+
|
| 247 |
+
### π Pull Request: Fix for [bug title]
|
| 248 |
+
|
| 249 |
+
#### π Problem
|
| 250 |
+
(What was the bug? 2-3 sentences, non-technical enough for a manager to understand)
|
| 251 |
+
|
| 252 |
+
#### π Root Cause
|
| 253 |
+
(Technical explanation of WHY this bug existed β 3-5 sentences)
|
| 254 |
+
|
| 255 |
+
#### π§ Solution
|
| 256 |
+
(What was changed and how it fixes the problem β reference specific lines/functions)
|
| 257 |
+
|
| 258 |
+
#### π Changes
|
| 259 |
+
(For each changed file, one bullet: "`filename.py` β what changed and why")
|
| 260 |
+
|
| 261 |
+
#### π§ͺ Testing Recommendations
|
| 262 |
+
(How a reviewer should verify this fix works β what to test, what edge cases to check)
|
| 263 |
+
|
| 264 |
+
#### β οΈ Potential Side Effects
|
| 265 |
+
(Any risks or areas that could be affected by this change. If none, say "None identified.")
|
| 266 |
+
|
| 267 |
+
#### π Related Issues / References
|
| 268 |
+
(Any related issues, docs, or context that helps understand this fix)
|
| 269 |
+
|
| 270 |
+
Write this as if you're a careful, experienced engineer who wants the reviewer to feel confident merging this PR.
|
| 271 |
+
"""
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
# ββ Confidence Self-Evaluation (Stretch feature) ββββββββββββββββββββββββββββββ
|
| 275 |
+
CONFIDENCE_EVAL_PROMPT = """Review your own analysis and rate your confidence.
|
| 276 |
+
|
| 277 |
+
## Analysis Summary
|
| 278 |
+
{analysis}
|
| 279 |
+
|
| 280 |
+
## Self-Evaluation
|
| 281 |
+
|
| 282 |
+
Rate the following on a scale of 1-10 and explain:
|
| 283 |
+
|
| 284 |
+
1. **Root Cause Confidence** (1-10): How certain are you the identified root cause is correct?
|
| 285 |
+
2. **Fix Correctness** (1-10): How confident are you the proposed fix will resolve the issue?
|
| 286 |
+
3. **Fix Safety** (1-10): How safe is the fix (no regressions, no side effects)?
|
| 287 |
+
4. **Completeness** (1-10): How complete is your analysis (nothing important missed)?
|
| 288 |
+
|
| 289 |
+
**Overall Score:** X/10
|
| 290 |
+
|
| 291 |
+
**Uncertainty Factors:** (What would change your diagnosis?)
|
| 292 |
+
|
| 293 |
+
**Recommended Next Steps:** (What additional verification would increase confidence?)
|
| 294 |
+
"""
|
demo/example_output.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FixFlow Sample Output β FastAPI Bug Analysis
|
| 2 |
+
|
| 3 |
+
**Issue:** [FastAPI response_model doesn't strip extra fields when using Pydantic v2](https://github.com/tiangolo/fastapi/issues/10876)
|
| 4 |
+
**Repository:** https://github.com/tiangolo/fastapi
|
| 5 |
+
**Analysis Time:** 87.3s
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## π Step 1: Bug Summary
|
| 10 |
+
|
| 11 |
+
### π Error Message
|
| 12 |
+
> "When using `response_model` in FastAPI with Pydantic v2, extra fields defined in the response model are NOT stripped from the response. This breaks the behavior expected from the `response_model_exclude_unset` pattern."
|
| 13 |
+
|
| 14 |
+
### β
Expected Behavior
|
| 15 |
+
When a route has a `response_model` set, FastAPI should filter the response to only include fields defined in that model, stripping any additional fields from the underlying return value.
|
| 16 |
+
|
| 17 |
+
### β Actual Behavior
|
| 18 |
+
Extra fields from the returned object are included in the JSON response even when a `response_model` is specified. This is a regression from Pydantic v1 behavior.
|
| 19 |
+
|
| 20 |
+
### π Reproduction Steps
|
| 21 |
+
1. Install `fastapi>=0.100.0` with `pydantic>=2.0.0`
|
| 22 |
+
2. Define a route: `@app.get("/users/{id}", response_model=UserOut)`
|
| 23 |
+
3. Return a `UserDB` object with extra fields not in `UserOut`
|
| 24 |
+
4. Observe: response includes the extra fields
|
| 25 |
+
|
| 26 |
+
### π― Affected Components
|
| 27 |
+
- `fastapi/routing.py` β route handler serialization logic
|
| 28 |
+
- `fastapi/_compat.py` β Pydantic v1/v2 compatibility layer
|
| 29 |
+
- `fastapi/encoders.py` β JSON encoding pipeline
|
| 30 |
+
|
| 31 |
+
### π Key Technical Clues
|
| 32 |
+
- Introduced after Pydantic v2 migration
|
| 33 |
+
- `_get_value()` in `fastapi/_compat.py` changed behavior for model instances
|
| 34 |
+
- The `model_dump(exclude_unset=True)` call may not be filtering correctly
|
| 35 |
+
|
| 36 |
+
### π‘ Hypothesis
|
| 37 |
+
The Pydantic v2 compatibility layer in `_compat.py` is not correctly calling `model_dump()` with the `include`/`exclude` parameters that respect the `response_model` field constraints. The v2 migration changed how model field serialization works.
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
## π Step 2: Relevant Files
|
| 42 |
+
|
| 43 |
+
### π Relevant Files (Ranked by Suspicion)
|
| 44 |
+
|
| 45 |
+
**1. `fastapi/_compat.py`**
|
| 46 |
+
- **Relevance score:** 10/10
|
| 47 |
+
- **Why relevant:** This is the Pydantic v1/v2 compatibility shim. All serialization changes went through here during the v2 migration.
|
| 48 |
+
- **What to look for:** `_get_value()`, `serialize_response()`, any calls to `model_dump()`
|
| 49 |
+
|
| 50 |
+
**2. `fastapi/routing.py`**
|
| 51 |
+
- **Relevance score:** 9/10
|
| 52 |
+
- **Why relevant:** Contains `serialize_response()` calls that apply `response_model` filtering.
|
| 53 |
+
- **What to look for:** `get_request_handler()`, how `response_model_include` and `response_model_exclude` are passed.
|
| 54 |
+
|
| 55 |
+
**3. `fastapi/encoders.py`**
|
| 56 |
+
- **Relevance score:** 7/10
|
| 57 |
+
- **Why relevant:** `jsonable_encoder()` handles the final conversion to JSON-safe types.
|
| 58 |
+
- **What to look for:** Whether `include`/`exclude` sets are respected for Pydantic v2 models.
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## π¬ Step 3: Root Cause Analysis
|
| 63 |
+
|
| 64 |
+
### Executive Summary
|
| 65 |
+
In `fastapi/_compat.py`, the `_get_value()` function for Pydantic v2 models calls `model_dump()` without passing the `include` parameter derived from the `response_model`'s field set, causing all fields to be serialized instead of only those defined in the response model.
|
| 66 |
+
|
| 67 |
+
### π§ Chain-of-Thought Reasoning
|
| 68 |
+
|
| 69 |
+
**Step 1: Entry Point**
|
| 70 |
+
A GET request hits a route decorated with `@app.get("/users/{id}", response_model=UserOut)`. FastAPI's `routing.py:get_request_handler()` is invoked, which calls `serialize_response()`.
|
| 71 |
+
|
| 72 |
+
**Step 2: Execution Trace**
|
| 73 |
+
- `routing.py:serialize_response()` β calls `_compat.py:serialize_response()` with `response_model=UserOut`
|
| 74 |
+
- `_compat.py:serialize_response()` calls `_get_value(response, field=response_model_field, ...)`
|
| 75 |
+
- **Here's the bug:** For Pydantic v2, `_get_value()` calls `value.model_dump()` but does NOT pass `include=field_set` where `field_set` contains only the fields defined in `UserOut`
|
| 76 |
+
|
| 77 |
+
**Step 3: The Bug**
|
| 78 |
+
In `fastapi/_compat.py`, around line 215, the v2 branch of `_get_value()`:
|
| 79 |
+
```python
|
| 80 |
+
# BUGGY (current):
|
| 81 |
+
return value.model_dump(exclude_unset=exclude_unset, by_alias=by_alias)
|
| 82 |
+
|
| 83 |
+
# Should be:
|
| 84 |
+
return value.model_dump(
|
| 85 |
+
include=include,
|
| 86 |
+
exclude=exclude,
|
| 87 |
+
exclude_unset=exclude_unset,
|
| 88 |
+
by_alias=by_alias,
|
| 89 |
+
)
|
| 90 |
+
```
|
| 91 |
+
The `include` parameter (containing the `response_model`'s allowed fields) is accepted as a function argument but silently dropped in the v2 code path.
|
| 92 |
+
|
| 93 |
+
**Step 4: Why This Causes the Reported Behavior**
|
| 94 |
+
Without the `include` parameter, `model_dump()` serializes ALL fields of the returned object, bypassing the `response_model` restriction. In Pydantic v1, `_get_value()` used `dict()` which was correctly called with `include` β this broke during the v2 migration.
|
| 95 |
+
|
| 96 |
+
### π Bug Location
|
| 97 |
+
- **File:** `fastapi/_compat.py`
|
| 98 |
+
- **Function/Class:** `_get_value()`
|
| 99 |
+
- **Line(s):** ~215
|
| 100 |
+
- **Type:** Missing parameter pass-through (regression from Pydantic v2 migration)
|
| 101 |
+
|
| 102 |
+
### π― Confidence Level
|
| 103 |
+
**High** β The bug is clearly a missing parameter in a well-understood code path. The fix is straightforward and surgical.
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## π§ Step 4: Proposed Fix
|
| 108 |
+
|
| 109 |
+
```diff
|
| 110 |
+
--- a/fastapi/_compat.py
|
| 111 |
+
+++ b/fastapi/_compat.py
|
| 112 |
+
@@ -212,7 +212,11 @@ def _get_value(
|
| 113 |
+
if PYDANTIC_V2:
|
| 114 |
+
if isinstance(value, BaseModel):
|
| 115 |
+
- return value.model_dump(exclude_unset=exclude_unset, by_alias=by_alias)
|
| 116 |
+
+ # Pass include/exclude to respect response_model field constraints
|
| 117 |
+
+ # This was missing after the Pydantic v2 migration (regression fix)
|
| 118 |
+
+ return value.model_dump(
|
| 119 |
+
+ include=include,
|
| 120 |
+
+ exclude=exclude,
|
| 121 |
+
+ exclude_unset=exclude_unset,
|
| 122 |
+
+ by_alias=by_alias,
|
| 123 |
+
+ )
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
## π Step 5: PR Description
|
| 129 |
+
|
| 130 |
+
### π Pull Request: Fix response_model field filtering with Pydantic v2
|
| 131 |
+
|
| 132 |
+
#### π Problem
|
| 133 |
+
When using FastAPI with Pydantic v2, the `response_model` parameter on route decorators no longer strips extra fields from responses. A route returning a `UserDB` object (with password, internal fields) but declaring `response_model=UserOut` would incorrectly expose the extra fields to clients.
|
| 134 |
+
|
| 135 |
+
#### π Root Cause
|
| 136 |
+
During the Pydantic v2 migration, `fastapi/_compat.py`'s `_get_value()` function lost the `include` parameter pass-through in the v2 code path. The `model_dump()` call was not forwarding the field inclusion constraints derived from the `response_model`.
|
| 137 |
+
|
| 138 |
+
#### π§ Solution
|
| 139 |
+
Added `include=include` and `exclude=exclude` parameters to the `model_dump()` call in the Pydantic v2 branch of `_get_value()`. This restores the Pydantic v1 behavior where only `response_model` fields are serialized.
|
| 140 |
+
|
| 141 |
+
#### π§ͺ Testing Recommendations
|
| 142 |
+
1. Create a route returning an object with extra fields, verify response only includes `response_model` fields
|
| 143 |
+
2. Test `response_model_exclude_unset=True` still works correctly
|
| 144 |
+
3. Run existing test suite: `pytest tests/test_response_model.py -v`
|
| 145 |
+
|
| 146 |
+
#### β οΈ Potential Side Effects
|
| 147 |
+
None identified. Change only affects the Pydantic v2 code path and is additive β it passes parameters that were already being constructed but not forwarded.
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
*Generated by FixFlow β Autonomous Bug Resolution Agent powered by GLM 5.1 (Z.ai)*
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.30.0
|
| 2 |
+
openai>=1.0.0
|
| 3 |
+
PyGithub>=2.1.0
|
| 4 |
+
requests>=2.31.0
|
| 5 |
+
python-dotenv>=1.0.0
|