Spaces:
Running
Running
✨ Feature: Add Auto-Pull Request creation via GitHub API directly from Streamlit UI
Browse files- app.py +27 -7
- backend/config.py +2 -0
- backend/github_client.py +62 -0
app.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import Optional
|
|
| 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 |
|
|
@@ -388,8 +388,8 @@ def init_session():
|
|
| 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 |
}
|
|
@@ -433,7 +433,7 @@ with st.sidebar:
|
|
| 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 |
)
|
|
@@ -517,7 +517,6 @@ if issue_url and not repo_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
|
|
@@ -786,15 +785,36 @@ if st.session_state.result:
|
|
| 786 |
# Copy button for full diff
|
| 787 |
if result.diff_formatted and result.diffs:
|
| 788 |
st.markdown("---")
|
| 789 |
-
copy_col, _ = st.columns([1,
|
| 790 |
with copy_col:
|
| 791 |
st.download_button(
|
| 792 |
-
"📋
|
| 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):
|
|
|
|
| 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, GLM_API_KEY, GITHUB_TOKEN
|
| 14 |
from backend.github_client import GitHubClient
|
| 15 |
from backend.llm_client import GLMClient
|
| 16 |
|
|
|
|
| 388 |
"step_messages": {},
|
| 389 |
"stream_buffer": "",
|
| 390 |
"error": None,
|
| 391 |
+
"glm_api_key": GLM_API_KEY,
|
| 392 |
+
"github_token": GITHUB_TOKEN,
|
| 393 |
"model": GLM_MODEL,
|
| 394 |
"run_confidence": False,
|
| 395 |
}
|
|
|
|
| 433 |
|
| 434 |
model_choice = st.selectbox(
|
| 435 |
"GLM Model",
|
| 436 |
+
options=["glm-5", "glm-5.1", "glm-5-plus", "glm-4-plus", "glm-4"],
|
| 437 |
index=0,
|
| 438 |
key="model_select",
|
| 439 |
)
|
|
|
|
| 517 |
import re
|
| 518 |
m = re.match(r"(https://github\.com/[^/]+/[^/]+)/issues/\d+", issue_url.strip())
|
| 519 |
if m:
|
|
|
|
| 520 |
repo_url = m.group(1)
|
| 521 |
|
| 522 |
# Example buttons
|
|
|
|
| 785 |
# Copy button for full diff
|
| 786 |
if result.diff_formatted and result.diffs:
|
| 787 |
st.markdown("---")
|
| 788 |
+
copy_col, pr_col, _ = st.columns([1, 1, 2])
|
| 789 |
with copy_col:
|
| 790 |
st.download_button(
|
| 791 |
+
"📋 Download .diff Patch",
|
| 792 |
data=result.diff_formatted,
|
| 793 |
file_name="fixflow.diff",
|
| 794 |
mime="text/plain",
|
| 795 |
use_container_width=True,
|
| 796 |
)
|
| 797 |
+
with pr_col:
|
| 798 |
+
if st.button("🚀 Open GitHub Pull Request", use_container_width=True, type="primary"):
|
| 799 |
+
if not st.session_state.github_token:
|
| 800 |
+
st.error("⚠️ A GitHub Token with write access is required to open a PR.")
|
| 801 |
+
else:
|
| 802 |
+
with st.spinner("🚀 Creating Pull Request..."):
|
| 803 |
+
gh = GitHubClient(token=st.session_state.github_token)
|
| 804 |
+
try:
|
| 805 |
+
branch_name = f"fixflow-patch-{int(time.time())}"
|
| 806 |
+
title = f"Fix: {result.issue_data.get('title', 'Issue')}"
|
| 807 |
+
body = result.fix_explanation + "\n\n---\n*Generated autonomously by FixFlow*"
|
| 808 |
+
pr_url = gh.create_pull_request(
|
| 809 |
+
repo_url=result.repo_url,
|
| 810 |
+
branch_name=branch_name,
|
| 811 |
+
files_content=result.fixed_files,
|
| 812 |
+
title=title,
|
| 813 |
+
body=body
|
| 814 |
+
)
|
| 815 |
+
st.success(f"✅ Created successfully: [View PR]({pr_url})")
|
| 816 |
+
except Exception as e:
|
| 817 |
+
st.error(f"Failed to create PR: {e}")
|
| 818 |
|
| 819 |
# ── Step 5: Fix Explanation ───────────────────────────────────────────────
|
| 820 |
with st.expander("📝 Step 5: PR Description & Fix Explanation", expanded=True):
|
backend/config.py
CHANGED
|
@@ -14,6 +14,8 @@ 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"))
|
|
|
|
| 14 |
|
| 15 |
# ── GitHub Config ────────────────────────────────────────────────────────────
|
| 16 |
GITHUB_TOKEN: str = os.getenv("GITHUB_TOKEN", "")
|
| 17 |
+
if GITHUB_TOKEN == "your_github_token_here":
|
| 18 |
+
GITHUB_TOKEN = ""
|
| 19 |
|
| 20 |
# ── Agent Limits ─────────────────────────────────────────────────────────────
|
| 21 |
MAX_FILES_TO_SCAN: int = int(os.getenv("MAX_FILES_TO_SCAN", "100"))
|
backend/github_client.py
CHANGED
|
@@ -228,6 +228,68 @@ class GitHubClient:
|
|
| 228 |
result[path] = content
|
| 229 |
return result
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
# ── Rate Limit Info ───────────────────────────────────────────────────────
|
| 232 |
|
| 233 |
def get_rate_limit_info(self) -> Dict:
|
|
|
|
| 228 |
result[path] = content
|
| 229 |
return result
|
| 230 |
|
| 231 |
+
# ── Pull Request Creation ─────────────────────────────────────────────────
|
| 232 |
+
|
| 233 |
+
def create_pull_request(
|
| 234 |
+
self,
|
| 235 |
+
repo_url: str,
|
| 236 |
+
branch_name: str,
|
| 237 |
+
files_content: Dict[str, str],
|
| 238 |
+
title: str,
|
| 239 |
+
body: str,
|
| 240 |
+
) -> str:
|
| 241 |
+
"""
|
| 242 |
+
Creates a new branch and commits all changed files, then opens a pull request.
|
| 243 |
+
Requires a GitHub token with write access to the repository.
|
| 244 |
+
Returns the HTML URL of the created PR.
|
| 245 |
+
"""
|
| 246 |
+
if not self._gh.get_user():
|
| 247 |
+
raise RuntimeError("A valid GitHub Token with write access is required to create a PR.")
|
| 248 |
+
|
| 249 |
+
owner, repo_name = parse_repo_url(repo_url)
|
| 250 |
+
logger.info("Creating PR on %s/%s branch %s", owner, repo_name, branch_name)
|
| 251 |
+
|
| 252 |
+
try:
|
| 253 |
+
repo = self._gh.get_repo(f"{owner}/{repo_name}")
|
| 254 |
+
from github import InputGitTreeElement
|
| 255 |
+
|
| 256 |
+
base_branch = repo.default_branch
|
| 257 |
+
base_ref = repo.get_git_ref(f"heads/{base_branch}")
|
| 258 |
+
|
| 259 |
+
# Create new branch off base branch
|
| 260 |
+
try:
|
| 261 |
+
repo.create_git_ref(ref=f"refs/heads/{branch_name}", sha=base_ref.object.sha)
|
| 262 |
+
except GithubException:
|
| 263 |
+
logger.warning(f"Branch {branch_name} may already exist, proceeding to update it.")
|
| 264 |
+
|
| 265 |
+
base_tree = repo.get_git_tree(base_ref.object.sha)
|
| 266 |
+
|
| 267 |
+
# Create a blob for each changed file
|
| 268 |
+
elements = []
|
| 269 |
+
for filepath, content in files_content.items():
|
| 270 |
+
blob = repo.create_git_blob(content, "utf-8")
|
| 271 |
+
elements.append(
|
| 272 |
+
InputGitTreeElement(path=filepath, mode='100644', type='blob', sha=blob.sha)
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
# Create new tree with all blob changes batched together
|
| 276 |
+
new_tree = repo.create_git_tree(elements, base_tree)
|
| 277 |
+
parent = repo.get_git_commit(base_ref.object.sha)
|
| 278 |
+
commit = repo.create_git_commit(message=title, tree=new_tree, parents=[parent])
|
| 279 |
+
|
| 280 |
+
# Update the branch reference to point to the new commit
|
| 281 |
+
ref = repo.get_git_ref(f"heads/{branch_name}")
|
| 282 |
+
ref.edit(commit.sha)
|
| 283 |
+
|
| 284 |
+
# Create the actual PR
|
| 285 |
+
pr = repo.create_pull(title=title, body=body, head=branch_name, base=base_branch)
|
| 286 |
+
return pr.html_url
|
| 287 |
+
|
| 288 |
+
except GithubException as e:
|
| 289 |
+
raise RuntimeError(
|
| 290 |
+
f"Failed to create PR. Ensure your GitHub token has write access to {owner}/{repo_name}. Detail: {e.data.get('message', str(e))}"
|
| 291 |
+
) from e
|
| 292 |
+
|
| 293 |
# ── Rate Limit Info ───────────────────────────────────────────────────────
|
| 294 |
|
| 295 |
def get_rate_limit_info(self) -> Dict:
|