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
|
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 ---------
|
# --------- helpers ---------
|
||||||
def now_iso() -> str:
|
def now_iso() -> str:
|
||||||
return datetime.now(timezone.utc).isoformat()
|
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
|
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(
|
def build_self_improve_prompt(
|
||||||
payload: AssistantSelfImprovePayload,
|
payload: AssistantSelfImprovePayload,
|
||||||
feedback_rows: List[Dict[str, Any]],
|
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)
|
@app.post("/assistant/draft", response_model=AssistantDraftResponse)
|
||||||
async def assistant_draft(payload: AssistantDraftPayload, x_admin_api_key: Optional[str] = Header(default=None)):
|
async def assistant_draft(payload: AssistantDraftPayload, x_admin_api_key: Optional[str] = Header(default=None)):
|
||||||
check_admin_api_key(x_admin_api_key)
|
check_admin_api_key(x_admin_api_key)
|
||||||
|
|
|
||||||
|
|
@ -237,15 +237,42 @@ async function runSelfImprove() {
|
||||||
include_blocked_actions: true,
|
include_blocked_actions: true,
|
||||||
apply: false,
|
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}`;
|
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><strong>${p.proposal_id}: ${p.title}</strong></div>
|
||||||
<div>${p.problem}</div>
|
<div>${p.problem}</div>
|
||||||
<div class="meta">risk=${p.risk} | auto_apply_safe=${p.auto_apply_safe}</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">files=${(p.files || []).join(", ") || "-"}</div>
|
||||||
<div class="meta">tests=${(p.tests || []).join(" | ") || "-"}</div>
|
<div class="meta">tests=${(p.tests || []).join(" | ") || "-"}</div>
|
||||||
<div style="margin-top:6px">${p.change || ""}</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) {
|
} catch (e) {
|
||||||
summary.textContent = `Error: ${String(e)}`;
|
summary.textContent = `Error: ${String(e)}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<input id="improveObjective" type="text" placeholder="Objective" />
|
<input id="improveObjective" type="text" placeholder="Objective" />
|
||||||
<input id="improveMax" type="number" min="1" max="20" value="5" style="width:90px" />
|
<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>
|
<button id="runImprove">Generate Proposals</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue