From 8db9032d46b72764cd01c8706f8bd83260eb8a7a Mon Sep 17 00:00:00 2001 From: gussewalachucha Date: Tue, 28 Jul 2026 18:25:55 +0500 Subject: [PATCH] little fix --- api/routes.py | 73 +++++++++++++++++++++++++++++++++++++------ api/schemas.py | 23 ++++++++++++++ tools/agent_runner.py | 55 ++++++++++++++++++++++++++++++-- utils/job_manager.py | 9 ++++++ 4 files changed, 149 insertions(+), 11 deletions(-) diff --git a/api/routes.py b/api/routes.py index 570e09b..534d261 100644 --- a/api/routes.py +++ b/api/routes.py @@ -5,7 +5,7 @@ import traceback from urllib.parse import urlparse -from fastapi import APIRouter, BackgroundTasks +from fastapi import APIRouter, BackgroundTasks, HTTPException from api.errors import ( InvalidInstructionError, @@ -16,6 +16,8 @@ from api.schemas import ( JobStatus, JobStatusResponse, + OpenPrRequest, + OpenPrResponse, RefineRequest, RefineResponse, RunRequest, @@ -23,7 +25,7 @@ ) # ── Real agent runner (replaces the old stub test_executor) ─────────────────── -from tools.agent_runner import run_agent +from tools.agent_runner import open_pull_request_for_job, run_agent from utils.job_manager import job_manager router = APIRouter(tags=["Agent"]) @@ -44,23 +46,35 @@ def process_job(job_id: str) -> None: session_id=job_id, # session_id == job_id → memory persists across /refine branch_name=getattr(job, "branch_name", "repomind/auto-fix"), pr_title_override=getattr(job, "pr_title", None), + create_pr=getattr(job, "create_pr", True), + github_token=getattr(job, "github_token", None), + openai_api_key=getattr(job, "openai_api_key", None), + base_branch=getattr(job, "base_branch", "main"), ) pr_url = result.get("pr_url") + summary = result.get("summary") or result.get("diff_summary") if pr_url: job_manager.update( job_id, status=JobStatus.completed, pr_url=pr_url, - diff_summary=result.get("summary"), + diff_summary=summary, + ) + elif summary and "no file changes" not in summary.lower() and "no disk changes" not in summary.lower(): + # Preview-only success (or completed without PR) + job_manager.update( + job_id, + status=JobStatus.completed, + pr_url=None, + diff_summary=summary, ) else: - # Agent ran successfully but produced no changes. job_manager.update( job_id, status=JobStatus.failed, - error_message=result.get("summary") + error_message=summary or "Agent completed but no file changes were made.", ) @@ -81,10 +95,12 @@ async def run(request: RunRequest, background_tasks: BackgroundTasks) -> RunResp repo_url=request.repo_url, instruction=request.instruction, ) - # Stash branch_name and pr_title on the job record so process_job can read them. record = job_manager.get(job_id) - record.branch_name = request.branch_name # type: ignore[attr-defined] - record.pr_title = request.pr_title # type: ignore[attr-defined] + record.branch_name = request.branch_name + record.pr_title = request.pr_title + record.create_pr = request.create_pr + record.github_token = request.github_token + record.openai_api_key = request.openai_api_key background_tasks.add_task(process_job, job_id) return RunResponse(job_id=job_id, status=JobStatus.queued) @@ -123,8 +139,12 @@ async def refine(request: RefineRequest, background_tasks: BackgroundTasks) -> R if not request.instruction.strip(): raise InvalidInstructionError() - # Append the refinement so the instruction history grows naturally. job.instruction += f"\nRefinement: {request.instruction}" + if request.github_token: + job.github_token = request.github_token + if request.openai_api_key: + job.openai_api_key = request.openai_api_key + job.create_pr = True job_manager.update(request.job_id, status=JobStatus.queued) background_tasks.add_task(process_job, request.job_id) @@ -133,3 +153,38 @@ async def refine(request: RefineRequest, background_tasks: BackgroundTasks) -> R status=JobStatus.queued, message="Refinement queued — agent will run with full prior context.", ) + + +@router.post("/open-pr", response_model=OpenPrResponse) +async def open_pr(request: OpenPrRequest) -> OpenPrResponse: + """Open a pull request for a completed preview-only job.""" + try: + job = job_manager.get(request.job_id) + except Exception: + raise JobNotFoundError(request.job_id) from None + + if job.pr_url: + return OpenPrResponse( + job_id=job.job_id, + pr_url=job.pr_url, + status=JobStatus(job.status), + ) + + if job.status != JobStatus.completed: + raise HTTPException( + status_code=400, + detail="Job must be completed before opening a pull request", + ) + + token = request.github_token or job.github_token + pr_url = open_pull_request_for_job( + repo_url=job.repo_url, + instruction=job.instruction, + branch_name=job.branch_name, + pr_title=job.pr_title, + base_branch=job.base_branch, + github_token=token, + diff_summary=job.diff_summary, + ) + job_manager.update(job.job_id, status=JobStatus.completed, pr_url=pr_url) + return OpenPrResponse(job_id=job.job_id, pr_url=pr_url, status=JobStatus.completed) diff --git a/api/schemas.py b/api/schemas.py index 0053699..794f683 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -25,6 +25,19 @@ class RunRequest(BaseModel): instruction: str # Plain-English change description branch_name: str = "repomind/auto-fix" # Branch that will be created for the PR pr_title: str = "refactor: RepoMind automated change" # Title of the Pull Request + create_pr: bool = True # When False, push branch + return diff without opening a PR + github_token: str | None = None # Optional per-user override; falls back to server env + openai_api_key: str | None = None # Optional per-user override; falls back to server env + + +class OpenPrRequest(BaseModel): + """ + POST /open-pr + Open a pull request for a completed preview-only job. + """ + + job_id: str + github_token: str | None = None class RefineRequest(BaseModel): @@ -35,6 +48,8 @@ class RefineRequest(BaseModel): job_id: str # The job to refine instruction: str # Follow-up instruction e.g. "also add type hints" + github_token: str | None = None + openai_api_key: str | None = None # ── Response Models ─────────────────────────────────────────────────────────── @@ -76,6 +91,14 @@ class RefineResponse(BaseModel): message: str | None = None +class OpenPrResponse(BaseModel): + """Returned from POST /open-pr after creating the pull request.""" + + job_id: str + pr_url: str + status: JobStatus + + # ── Internal Models ─────────────────────────────────────────────────────────── # Used between modules — not exposed directly in API responses diff --git a/tools/agent_runner.py b/tools/agent_runner.py index 08d5225..c0b2909 100644 --- a/tools/agent_runner.py +++ b/tools/agent_runner.py @@ -16,6 +16,7 @@ from __future__ import annotations import logging +import os import tempfile from pathlib import Path @@ -187,6 +188,9 @@ def run_agent( branch_name: str = "repomind/auto-fix", pr_title_override: str | None = None, base_branch: str = "main", + create_pr: bool = True, + github_token: str | None = None, + openai_api_key: str | None = None, ) -> dict: """ Full end-to-end agent run. @@ -199,6 +203,10 @@ def run_agent( } """ settings = get_settings() + token = github_token or settings.github_token + if openai_api_key: + # Prefer per-request key when provided (does not mutate global settings cache). + os.environ.setdefault("OPENAI_API_KEY", openai_api_key) with tempfile.TemporaryDirectory(prefix="repomind_") as tmp_dir: repo_path = Path(tmp_dir) / "repo" @@ -207,7 +215,7 @@ def run_agent( logger.info("Cloning %s into %s", repo_url, repo_path) authenticated_url = repo_url.replace( "https://", - f"https://{settings.github_token}@", + f"https://{token}@", ) git_repo = clone_repository(authenticated_url, repo_path) @@ -291,6 +299,14 @@ def run_agent( f"+{lines_added} lines, -{lines_removed} lines." ) + if not create_pr: + logger.info("Skipping PR creation (create_pr=False) for session %s", session_id) + return { + "pr_url": None, + "summary": diff_summary_text, + "diff_summary": diff_summary_text, + } + # 8. Open pull request repo_full_name = ( repo_url.replace("https://github.com/", "").rstrip("/").removesuffix(".git") @@ -305,7 +321,7 @@ def run_agent( logger.info("Opening PR on %s", repo_full_name) pr = create_pull_request( - token=settings.github_token, + token=token, repo_full_name=repo_full_name, title=pr_title, body=pr_body, @@ -319,3 +335,38 @@ def run_agent( "summary": diff_summary_text, "diff_summary": diff_summary_text, } + + +def open_pull_request_for_job( + repo_url: str, + instruction: str, + branch_name: str, + pr_title: str, + base_branch: str = "main", + github_token: str | None = None, + diff_summary: str | None = None, +) -> str: + """Open a PR for an already-pushed preview branch.""" + settings = get_settings() + token = github_token or settings.github_token + repo_full_name = ( + repo_url.replace("https://github.com/", "").rstrip("/").removesuffix(".git") + ) + summary_map: dict[str, str] = {} + if diff_summary: + summary_map["summary"] = diff_summary + body = build_pr_body( + instruction=instruction, + changed_files=[], + diff_summary=summary_map, + ) + pr = create_pull_request( + token=token, + repo_full_name=repo_full_name, + title=pr_title or build_pr_title(instruction), + body=body, + head_branch=branch_name, + base_branch=base_branch, + ) + return pr.html_url + diff --git a/utils/job_manager.py b/utils/job_manager.py index 567aaf4..e1e256b 100644 --- a/utils/job_manager.py +++ b/utils/job_manager.py @@ -12,6 +12,12 @@ class JobRecord: pr_url: str | None = None diff_summary: str | None = None error_message: str | None = None + branch_name: str = "repomind/auto-fix" + pr_title: str = "refactor: RepoMind automated change" + create_pr: bool = True + base_branch: str = "main" + github_token: str | None = None + openai_api_key: str | None = None created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) started_at: datetime | None = None finished_at: datetime | None = None @@ -31,6 +37,9 @@ def to_dict(self) -> dict: "pr_url": self.pr_url, "diff_summary": self.diff_summary, "error_message": self.error_message, + "branch_name": self.branch_name, + "pr_title": self.pr_title, + "create_pr": self.create_pr, "created_at": self.created_at.isoformat(), "started_at": self.started_at.isoformat() if self.started_at else None, "finished_at": self.finished_at.isoformat() if self.finished_at else None,