feat(self-improve): add apply-to-branch endpoint and UI action buttons
This commit is contained in:
parent
c5b2776978
commit
6b75c0b596
141
app.py
141
app.py
|
|
@ -323,6 +323,24 @@ class AssistantSelfImproveResponse(BaseModel):
|
|||
apply_block_reason: Optional[str] = None
|
||||
|
||||
|
||||
class AssistantSelfImproveApplyPayload(BaseModel):
|
||||
objective: Optional[str] = None
|
||||
release_name: Optional[str] = None
|
||||
proposal: AssistantSelfImproveProposal
|
||||
dry_run: bool = False
|
||||
|
||||
|
||||
class AssistantSelfImproveApplyResponse(BaseModel):
|
||||
applied: bool
|
||||
dry_run: bool
|
||||
repo_dir: str
|
||||
branch_name: str
|
||||
proposal_file: str
|
||||
commit: Optional[str] = None
|
||||
commands: List[str] = Field(default_factory=list)
|
||||
detail: Optional[str] = None
|
||||
|
||||
|
||||
# --------- helpers ---------
|
||||
def now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
|
@ -1115,6 +1133,24 @@ def _append_chat_turn(session_id: str, role: str, content: str) -> None:
|
|||
ASSISTANT_CHAT_SESSIONS[session_id] = turns
|
||||
|
||||
|
||||
def _slugify(text: str) -> str:
|
||||
t = re.sub(r"[^a-zA-Z0-9]+", "-", (text or "").strip().lower()).strip("-")
|
||||
return t[:60] or "proposal"
|
||||
|
||||
|
||||
async def _run_local_cmd(args: List[str], cwd: Path) -> Dict[str, Any]:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*args,
|
||||
cwd=str(cwd),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
out = stdout.decode("utf-8", errors="replace")
|
||||
err = stderr.decode("utf-8", errors="replace")
|
||||
return {"code": proc.returncode, "stdout": out, "stderr": err}
|
||||
|
||||
|
||||
def build_self_improve_prompt(
|
||||
payload: AssistantSelfImprovePayload,
|
||||
feedback_rows: List[Dict[str, Any]],
|
||||
|
|
@ -2550,6 +2586,111 @@ async def assistant_self_improve(
|
|||
)
|
||||
|
||||
|
||||
@app.post("/assistant/self-improve/apply", response_model=AssistantSelfImproveApplyResponse)
|
||||
async def assistant_self_improve_apply(
|
||||
payload: AssistantSelfImproveApplyPayload,
|
||||
x_admin_api_key: Optional[str] = Header(default=None),
|
||||
):
|
||||
check_admin_api_key(x_admin_api_key)
|
||||
repo_dir = Path(__file__).resolve().parent
|
||||
proposal = payload.proposal
|
||||
|
||||
branch_slug = _slugify(f"{proposal.proposal_id}-{proposal.title}")
|
||||
branch_name = f"assistant/{branch_slug}"
|
||||
proposals_dir = repo_dir / "proposals"
|
||||
proposals_dir.mkdir(parents=True, exist_ok=True)
|
||||
proposal_file = proposals_dir / f"{branch_slug}.md"
|
||||
commands: List[str] = [
|
||||
f"git -C {repo_dir} rev-parse --is-inside-work-tree",
|
||||
f"git -C {repo_dir} checkout -B {branch_name}",
|
||||
f"write {proposal_file}",
|
||||
f"git -C {repo_dir} add {proposal_file}",
|
||||
f"git -C {repo_dir} commit -m 'chore(self-improve): apply {proposal.proposal_id} {proposal.title}'",
|
||||
]
|
||||
|
||||
md = "\n".join(
|
||||
[
|
||||
f"# {proposal.proposal_id}: {proposal.title}",
|
||||
"",
|
||||
f"- Objective: {payload.objective or ''}",
|
||||
f"- Release: {payload.release_name or ''}",
|
||||
f"- Risk: {proposal.risk}",
|
||||
f"- Auto apply safe: {proposal.auto_apply_safe}",
|
||||
"",
|
||||
"## Problem",
|
||||
proposal.problem,
|
||||
"",
|
||||
"## Change",
|
||||
proposal.change,
|
||||
"",
|
||||
"## Files",
|
||||
*(f"- `{f}`" for f in (proposal.files or [])),
|
||||
"",
|
||||
"## Tests",
|
||||
*(f"- {t}" for t in (proposal.tests or [])),
|
||||
"",
|
||||
f"_Generated at {now_iso()}_",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
if payload.dry_run:
|
||||
return AssistantSelfImproveApplyResponse(
|
||||
applied=False,
|
||||
dry_run=True,
|
||||
repo_dir=str(repo_dir),
|
||||
branch_name=branch_name,
|
||||
proposal_file=str(proposal_file.relative_to(repo_dir)),
|
||||
commit=None,
|
||||
commands=commands,
|
||||
detail="Dry-run only. No branch or commit created.",
|
||||
)
|
||||
|
||||
chk = await _run_local_cmd(["git", "rev-parse", "--is-inside-work-tree"], repo_dir)
|
||||
if chk["code"] != 0:
|
||||
raise HTTPException(status_code=500, detail={"message": "Not a git repository", "stderr": chk["stderr"]})
|
||||
|
||||
co = await _run_local_cmd(["git", "checkout", "-B", branch_name], repo_dir)
|
||||
if co["code"] != 0:
|
||||
raise HTTPException(status_code=500, detail={"message": "Failed to create/switch branch", "stderr": co["stderr"]})
|
||||
|
||||
proposal_file.write_text(md, encoding="utf-8")
|
||||
add = await _run_local_cmd(["git", "add", str(proposal_file.relative_to(repo_dir))], repo_dir)
|
||||
if add["code"] != 0:
|
||||
raise HTTPException(status_code=500, detail={"message": "Failed to add proposal file", "stderr": add["stderr"]})
|
||||
|
||||
commit_msg = f"chore(self-improve): apply {proposal.proposal_id} {proposal.title}"
|
||||
cm = await _run_local_cmd(["git", "commit", "-m", commit_msg], repo_dir)
|
||||
if cm["code"] != 0:
|
||||
# Common case: nothing to commit. still return branch + file info.
|
||||
detail = (cm["stderr"] or cm["stdout"] or "").strip()
|
||||
head = await _run_local_cmd(["git", "rev-parse", "HEAD"], repo_dir)
|
||||
commit = head["stdout"].strip() if head["code"] == 0 else None
|
||||
return AssistantSelfImproveApplyResponse(
|
||||
applied=True,
|
||||
dry_run=False,
|
||||
repo_dir=str(repo_dir),
|
||||
branch_name=branch_name,
|
||||
proposal_file=str(proposal_file.relative_to(repo_dir)),
|
||||
commit=commit,
|
||||
commands=commands,
|
||||
detail=detail or "No new commit created (possibly no diff).",
|
||||
)
|
||||
|
||||
head = await _run_local_cmd(["git", "rev-parse", "HEAD"], repo_dir)
|
||||
commit = head["stdout"].strip() if head["code"] == 0 else None
|
||||
return AssistantSelfImproveApplyResponse(
|
||||
applied=True,
|
||||
dry_run=False,
|
||||
repo_dir=str(repo_dir),
|
||||
branch_name=branch_name,
|
||||
proposal_file=str(proposal_file.relative_to(repo_dir)),
|
||||
commit=commit,
|
||||
commands=commands,
|
||||
detail="Branch and proposal commit created.",
|
||||
)
|
||||
|
||||
|
||||
@app.post("/assistant/draft", response_model=AssistantDraftResponse)
|
||||
async def assistant_draft(payload: AssistantDraftPayload, x_admin_api_key: Optional[str] = Header(default=None)):
|
||||
check_admin_api_key(x_admin_api_key)
|
||||
|
|
|
|||
|
|
@ -237,15 +237,42 @@ async function runSelfImprove() {
|
|||
include_blocked_actions: true,
|
||||
apply: false,
|
||||
});
|
||||
window.selfImproveLast = data.proposals || [];
|
||||
summary.textContent = `${data.summary || ""}\n\nsignals: feedback=${data.signals?.feedback_rows ?? 0}, blocked_actions=${data.signals?.blocked_action_rows ?? 0}`;
|
||||
renderRows(list, data.proposals || [], (p) => `
|
||||
renderRows(list, data.proposals || [], (p, idx) => `
|
||||
<div><strong>${p.proposal_id}: ${p.title}</strong></div>
|
||||
<div>${p.problem}</div>
|
||||
<div class="meta">risk=${p.risk} | auto_apply_safe=${p.auto_apply_safe}</div>
|
||||
<div class="meta">files=${(p.files || []).join(", ") || "-"}</div>
|
||||
<div class="meta">tests=${(p.tests || []).join(" | ") || "-"}</div>
|
||||
<div style="margin-top:6px">${p.change || ""}</div>
|
||||
<div style="margin-top:6px"><button class="apply-proposal" data-proposal-id="${p.proposal_id}">Apply as branch</button></div>
|
||||
`);
|
||||
document.querySelectorAll(".apply-proposal").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const proposalId = btn.getAttribute("data-proposal-id");
|
||||
const found = (window.selfImproveLast || []).find((x) => x.proposal_id === proposalId);
|
||||
if (!found) return;
|
||||
const dryRun = document.getElementById("improveDryRun").checked;
|
||||
summary.textContent = `Applying ${proposalId}...`;
|
||||
try {
|
||||
const applied = await apiPost("/assistant/self-improve/apply", {
|
||||
objective,
|
||||
release_name: cfg.releaseName || null,
|
||||
proposal: found,
|
||||
dry_run: dryRun,
|
||||
});
|
||||
summary.textContent =
|
||||
`apply result: applied=${applied.applied} dry_run=${applied.dry_run}\n` +
|
||||
`branch=${applied.branch_name}\n` +
|
||||
`proposal_file=${applied.proposal_file}\n` +
|
||||
`commit=${applied.commit || "-"}\n` +
|
||||
`${applied.detail || ""}`;
|
||||
} catch (e) {
|
||||
summary.textContent = `Apply error: ${String(e)}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
summary.textContent = `Error: ${String(e)}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@
|
|||
<div class="controls">
|
||||
<input id="improveObjective" type="text" placeholder="Objective" />
|
||||
<input id="improveMax" type="number" min="1" max="20" value="5" style="width:90px" />
|
||||
<label><input id="improveDryRun" type="checkbox" /> Dry-run apply</label>
|
||||
<button id="runImprove">Generate Proposals</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue