E5K7 commited on
Commit
35c8868
·
1 Parent(s): cd3b358

✨ Feature: Add Auto-Pull Request creation via GitHub API directly from Streamlit UI

Browse files
Files changed (3) hide show
  1. app.py +27 -7
  2. backend/config.py +2 -0
  3. 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, 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):
 
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: