Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 64 additions & 9 deletions api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,14 +16,16 @@
from api.schemas import (
JobStatus,
JobStatusResponse,
OpenPrRequest,
OpenPrResponse,
RefineRequest,
RefineResponse,
RunRequest,
RunResponse,
)

# ── 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"])
Expand All @@ -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.",
)

Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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)
23 changes: 23 additions & 0 deletions api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 ───────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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

Expand Down
55 changes: 53 additions & 2 deletions tools/agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from __future__ import annotations

import logging
import os
import tempfile
from pathlib import Path

Expand Down Expand Up @@ -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.
Expand All @@ -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"
Expand All @@ -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)

Expand Down Expand Up @@ -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")
Expand All @@ -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,
Expand All @@ -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

9 changes: 9 additions & 0 deletions utils/job_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Loading