[feat]. Add @codex -h help summary command
This commit is contained in:
@@ -495,7 +495,7 @@ async def gitea_webhook(
|
||||
gitea.post_issue_comment(repo, pr_number, format_queue_ack(head_sha))
|
||||
return {"accepted": True, "job_id": job.id, "status": "queued"}
|
||||
|
||||
if parsed_command.name in {"fix", "explain", "ignore"}:
|
||||
if parsed_command.name in {"fix", "explain", "ignore", "help"}:
|
||||
job = enqueue_job(
|
||||
session,
|
||||
repo=repo,
|
||||
|
||||
@@ -5,12 +5,14 @@ from collections.abc import Iterable
|
||||
|
||||
from gitea_codex_bot.types import ParsedCommand
|
||||
|
||||
COMMAND_RE = re.compile(r"^@([^\s]+)\s+(review|explain|fix|ignore|rerun)\b(.*)$", re.IGNORECASE | re.DOTALL)
|
||||
PREFIX_RE = re.compile(r"^@([^\s]+)\s+(.+)$", re.IGNORECASE | re.DOTALL)
|
||||
HELP_ALIASES = {"-h", "--help", "help"}
|
||||
SUPPORTED_COMMANDS = {"review", "explain", "fix", "ignore", "rerun"}
|
||||
|
||||
|
||||
def parse_command(body: str, aliases: Iterable[str] | None = None) -> ParsedCommand | None:
|
||||
stripped = body.strip()
|
||||
match = COMMAND_RE.match(stripped)
|
||||
match = PREFIX_RE.match(stripped)
|
||||
if not match:
|
||||
return None
|
||||
command_alias = match.group(1).lstrip("@").lower()
|
||||
@@ -18,8 +20,17 @@ def parse_command(body: str, aliases: Iterable[str] | None = None) -> ParsedComm
|
||||
if command_alias not in allowed_aliases:
|
||||
return None
|
||||
|
||||
name = match.group(2).lower()
|
||||
rest = match.group(3).strip()
|
||||
remainder = match.group(2).strip()
|
||||
if not remainder:
|
||||
return None
|
||||
parts = remainder.split(maxsplit=1)
|
||||
raw_name = parts[0].lower()
|
||||
rest = parts[1].strip() if len(parts) > 1 else ""
|
||||
if raw_name in HELP_ALIASES:
|
||||
return ParsedCommand(name="help", raw=stripped, arguments=[token for token in rest.split() if token])
|
||||
if raw_name not in SUPPORTED_COMMANDS:
|
||||
return None
|
||||
name = raw_name
|
||||
tokens = [token for token in rest.split() if token]
|
||||
|
||||
parsed = ParsedCommand(name=name, raw=stripped, arguments=tokens)
|
||||
|
||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
|
||||
|
||||
CommandName = Literal["review", "explain", "fix", "ignore", "rerun"]
|
||||
CommandName = Literal["review", "explain", "fix", "ignore", "rerun", "help"]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
||||
@@ -4,12 +4,12 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from gitea_codex_bot.config import Settings
|
||||
from gitea_codex_bot.db import get_session_factory
|
||||
from gitea_codex_bot.models import ReviewJob
|
||||
from gitea_codex_bot.models import JobStatus, ReviewJob
|
||||
from gitea_codex_bot.services.comments import upsert_persistent_review_comment_id
|
||||
from gitea_codex_bot.services.gitea import GiteaClient
|
||||
from gitea_codex_bot.services.jobs import claim_next_job, finish_job
|
||||
@@ -34,6 +34,13 @@ def _handle_non_review_command(
|
||||
job: ReviewJob,
|
||||
command: ParsedCommand,
|
||||
) -> tuple[bool, bool, dict[str, Any] | None, str | None]:
|
||||
if command.name == "help":
|
||||
try:
|
||||
message = _build_help_comment(settings, session, gitea, job)
|
||||
gitea.post_issue_comment(job.repo, job.pr_number, message)
|
||||
return True, True, {"summary": "Help/status summary posted."}, None
|
||||
except Exception as exc:
|
||||
return True, False, None, f"Failed to post help summary: {exc}"
|
||||
if command.name == "ignore":
|
||||
return True, True, {"summary": "Ignore command acknowledged. No review run executed."}, None
|
||||
if command.name == "explain":
|
||||
@@ -74,6 +81,91 @@ def _handle_non_review_command(
|
||||
return False, False, None, None
|
||||
|
||||
|
||||
def _build_help_comment(settings: Settings, session: Session, gitea: GiteaClient, job: ReviewJob) -> str:
|
||||
comments = gitea.list_issue_comments(job.repo, job.pr_number)
|
||||
comment_summaries = _summarize_comments(comments, settings.gitea_bot_username)
|
||||
latest_review = session.execute(
|
||||
select(ReviewJob)
|
||||
.where(
|
||||
ReviewJob.repo == job.repo,
|
||||
ReviewJob.pr_number == job.pr_number,
|
||||
ReviewJob.command.in_(["review", "rerun"]),
|
||||
)
|
||||
.order_by(ReviewJob.id.desc())
|
||||
.limit(1)
|
||||
).scalar_one_or_none()
|
||||
pending_count = session.execute(
|
||||
select(func.count(ReviewJob.id)).where(
|
||||
ReviewJob.repo == job.repo,
|
||||
ReviewJob.pr_number == job.pr_number,
|
||||
ReviewJob.status.in_([JobStatus.queued, JobStatus.running]),
|
||||
)
|
||||
).scalar_one()
|
||||
latest_status_line = "No previous review run."
|
||||
if latest_review is not None:
|
||||
latest_status = latest_review.status.value if hasattr(latest_review.status, "value") else str(latest_review.status)
|
||||
latest_summary = ""
|
||||
if isinstance(latest_review.result_json, dict):
|
||||
summary_raw = latest_review.result_json.get("summary")
|
||||
if isinstance(summary_raw, str):
|
||||
latest_summary = " ".join(summary_raw.split())
|
||||
latest_status_line = f"Latest review command: `{latest_review.command}` status `{latest_status}`."
|
||||
if latest_summary:
|
||||
latest_status_line = f"{latest_status_line} Summary: {latest_summary[:180]}"
|
||||
|
||||
lines = [
|
||||
"## Codex Help",
|
||||
"",
|
||||
"Supported commands:",
|
||||
"- `@codex review [security|performance|tests] [--full]`",
|
||||
"- `@codex rerun`",
|
||||
"- `@codex explain`",
|
||||
"- `@codex fix [--branch ...]`",
|
||||
"- `@codex ignore`",
|
||||
"- `@codex -h` / `@codex --help` / `@codex help`",
|
||||
"",
|
||||
"Status note:",
|
||||
f"- Pending jobs on this PR: `{pending_count}`",
|
||||
f"- Fix command enabled: `{str(settings.enable_fix_commands).lower()}`",
|
||||
f"- {latest_status_line}",
|
||||
"",
|
||||
f"Discussion summary ({comment_summaries['total']} comments, human `{comment_summaries['human']}`, bot `{comment_summaries['bot']}`):",
|
||||
]
|
||||
if comment_summaries["items"]:
|
||||
lines.extend(comment_summaries["items"])
|
||||
else:
|
||||
lines.append("- No comments available to summarize.")
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def _summarize_comments(comments: list[dict[str, Any]], bot_username: str) -> dict[str, Any]:
|
||||
normalized_bot = (bot_username or "").strip().lower()
|
||||
bot_count = 0
|
||||
summarized: list[str] = []
|
||||
recent = comments[-8:] if comments else []
|
||||
for row in comments:
|
||||
user = row.get("user")
|
||||
username = ""
|
||||
if isinstance(user, dict):
|
||||
username = str(user.get("username") or user.get("login") or "").strip().lower()
|
||||
if username and username == normalized_bot:
|
||||
bot_count += 1
|
||||
for row in recent:
|
||||
body_raw = str(row.get("body") or "").strip()
|
||||
if not body_raw:
|
||||
continue
|
||||
one_line = " ".join(body_raw.split())
|
||||
preview = one_line if len(one_line) <= 180 else f"{one_line[:180]}..."
|
||||
user = row.get("user")
|
||||
username = "unknown"
|
||||
if isinstance(user, dict):
|
||||
username = str(user.get("username") or user.get("login") or "unknown").strip() or "unknown"
|
||||
summarized.append(f"- @{username}: {preview}")
|
||||
total = len(comments)
|
||||
human_count = max(total - bot_count, 0)
|
||||
return {"total": total, "human": human_count, "bot": bot_count, "items": summarized}
|
||||
|
||||
|
||||
def _post_review_failure_comment(gitea: GiteaClient, job: ReviewJob, error_message: str) -> None:
|
||||
message = (
|
||||
"⚠️ Codex review run failed after queueing.\n\n"
|
||||
|
||||
Reference in New Issue
Block a user