feat. Review foot note, docker fix, pass message to reviewer , update tests
Some checks failed
ci / test (push) Failing after 16s
ci / publish (push) Has been skipped

This commit is contained in:
Space-Banane
2026-05-22 22:16:09 +02:00
parent b32bf9eb82
commit e7c7d82f84
18 changed files with 322 additions and 14 deletions

View File

@@ -219,6 +219,7 @@ async def gitea_webhook(
pr_number=pr_number,
head_sha=head_sha,
trigger_comment_id=comment_id,
trigger_comment_body=comment_body,
requested_by=sender_username,
command=parsed_command,
)
@@ -232,6 +233,7 @@ async def gitea_webhook(
pr_number=pr_number,
head_sha=head_sha,
trigger_comment_id=comment_id,
trigger_comment_body=comment_body,
requested_by=sender_username,
command=parsed_command,
)

View File

@@ -49,6 +49,7 @@ class ReviewJob(Base):
pr_number: Mapped[int] = mapped_column(Integer, nullable=False)
head_sha: Mapped[str] = mapped_column(String(64), nullable=False)
trigger_comment_id: Mapped[int] = mapped_column(Integer, nullable=False)
trigger_comment_body: Mapped[str | None] = mapped_column(Text, nullable=True)
command: Mapped[str] = mapped_column(String(64), nullable=False, default="review")
command_args: Mapped[str | None] = mapped_column(Text, nullable=True)
requested_by: Mapped[str] = mapped_column(String(255), nullable=False)

View File

@@ -61,6 +61,7 @@ def enqueue_job(
pr_number: int,
head_sha: str,
trigger_comment_id: int,
trigger_comment_body: str | None,
requested_by: str,
command: ParsedCommand,
) -> ReviewJob:
@@ -69,6 +70,7 @@ def enqueue_job(
pr_number=pr_number,
head_sha=head_sha,
trigger_comment_id=trigger_comment_id,
trigger_comment_body=trigger_comment_body,
command=command.name,
command_args=" ".join(command.arguments) if command.arguments else None,
requested_by=requested_by,

View File

@@ -34,9 +34,13 @@ def format_unsupported_ack(command: ParsedCommand) -> str:
def format_result_comment(head_sha: str, result: dict) -> str:
usage_note = _format_usage_note(result)
markdown_comment = result.get("markdown_comment")
if isinstance(markdown_comment, str) and markdown_comment.strip():
return _inject_head_sha_marker(head_sha, markdown_comment)
body = markdown_comment.strip()
if usage_note:
body = f"{body}\n\n{usage_note}"
return _inject_head_sha_marker(head_sha, body)
verdict = result.get("verdict", "has_issues")
confidence = float(result.get("confidence", 0.0))
@@ -64,4 +68,32 @@ def format_result_comment(head_sha: str, result: dict) -> str:
f" Suggestion: {suggestion}" if suggestion else " Suggestion: n/a",
]
)
return _inject_head_sha_marker(head_sha, "\n".join(lines).strip())
body = "\n".join(lines).strip()
if usage_note:
body = f"{body}\n\n{usage_note}"
return _inject_head_sha_marker(head_sha, body)
def _format_usage_note(result: dict) -> str:
meta = result.get("_meta")
if not isinstance(meta, dict):
return ""
model = meta.get("model")
model_text = model.strip() if isinstance(model, str) and model.strip() else "unknown"
usage = meta.get("usage")
if not isinstance(usage, dict):
return f"_Note: model `{model_text}`._"
input_tokens = usage.get("input_tokens")
output_tokens = usage.get("output_tokens")
total_tokens = usage.get("total_tokens")
parts = [f"model `{model_text}`"]
if isinstance(input_tokens, int):
parts.append(f"input `{input_tokens}`")
if isinstance(output_tokens, int):
parts.append(f"output `{output_tokens}`")
if isinstance(total_tokens, int):
parts.append(f"total `{total_tokens}`")
return f"_Note: {', '.join(parts)} tokens used._"

View File

@@ -129,6 +129,7 @@ def _build_prompt(
"}\n\n"
f"PR URL: {pr.html_url}\n"
f"Mode: {mode}\n"
f"Trigger message: {command.raw}\n"
f"Repo focus: {', '.join(repo_cfg.focus)}\n"
f"Diff truncated: {diff_context['truncated']}\n"
f"Changed files:\n{os.linesep.join(diff_context['changed_files'])}\n\n"
@@ -166,10 +167,31 @@ def _call_openai_review(settings: Settings, prompt: str) -> dict[str, Any]:
for content in item.get("content", []):
text_value = content.get("text")
if text_value:
return json.loads(text_value)
result = json.loads(text_value)
if isinstance(result, dict):
result["_meta"] = _build_openai_result_meta(payload, settings)
return result
raise ReviewError("OpenAI response did not contain JSON output text.")
def _build_openai_result_meta(payload: dict[str, Any], settings: Settings) -> dict[str, Any]:
usage_raw = payload.get("usage")
usage: dict[str, int] = {}
if isinstance(usage_raw, dict):
for output_key, source_key in (
("input_tokens", "input_tokens"),
("output_tokens", "output_tokens"),
("total_tokens", "total_tokens"),
):
value = usage_raw.get(source_key)
if isinstance(value, int):
usage[output_key] = value
model = payload.get("model")
if not isinstance(model, str) or not model.strip():
model = settings.openai_review_model
return {"source": "openai_api", "model": model, "usage": usage}
def _summarize_openai_failure(exc: Exception) -> str:
if isinstance(exc, httpx.HTTPStatusError):
status = exc.response.status_code

View File

@@ -48,6 +48,7 @@ def run_review_ephemeral(
if completed.returncode != 0:
raise RuntimeError(_format_runner_failure(completed))
parsed = _parse_codex_exec_stdout(completed.stdout)
parsed["_meta"] = _extract_result_meta_from_codex_stdout(completed.stdout, settings)
return normalize_review_result(parsed)
except Exception as exc:
if settings.codex_auth_mode == "chatgpt":
@@ -202,6 +203,67 @@ def _parse_codex_exec_stdout(stdout: str) -> dict[str, Any]:
raise RuntimeError(f"codex exec output text did not contain review JSON; text_tail={_tail_text(last_text, 400)}")
def _extract_result_meta_from_codex_stdout(stdout: str, settings: Settings) -> dict[str, Any]:
model = settings.openai_review_model
usage: dict[str, int] = {}
for line in stdout.splitlines():
line = line.strip()
if not line:
continue
try:
payload = json.loads(line)
except json.JSONDecodeError:
continue
discovered_model = _find_first_string_for_key(payload, "model")
if discovered_model:
model = discovered_model
discovered_usage = _find_first_dict_for_key(payload, "usage")
if isinstance(discovered_usage, dict):
for output_key, source_key in (
("input_tokens", "input_tokens"),
("output_tokens", "output_tokens"),
("total_tokens", "total_tokens"),
):
value = discovered_usage.get(source_key)
if isinstance(value, int):
usage[output_key] = value
return {"source": "ephemeral_runner", "model": model, "usage": usage}
def _find_first_string_for_key(payload: Any, key: str) -> str | None:
if isinstance(payload, dict):
value = payload.get(key)
if isinstance(value, str) and value.strip():
return value
for nested in payload.values():
found = _find_first_string_for_key(nested, key)
if found:
return found
if isinstance(payload, list):
for item in payload:
found = _find_first_string_for_key(item, key)
if found:
return found
return None
def _find_first_dict_for_key(payload: Any, key: str) -> dict[str, Any] | None:
if isinstance(payload, dict):
value = payload.get(key)
if isinstance(value, dict):
return value
for nested in payload.values():
found = _find_first_dict_for_key(nested, key)
if found:
return found
if isinstance(payload, list):
for item in payload:
found = _find_first_dict_for_key(item, key)
if found:
return found
return None
def _parse_review_json_from_text(text: str) -> dict[str, Any] | None:
candidates: list[str] = [text.strip()]
fenced = re.search(r"```(?:json)?\s*(\{.*\})\s*```", text, flags=re.DOTALL | re.IGNORECASE)

View File

@@ -24,7 +24,8 @@ logger = logging.getLogger(__name__)
def _command_from_job(job: ReviewJob) -> ParsedCommand:
args = job.command_args.split() if job.command_args else []
return ParsedCommand(name=job.command, raw=f"@codex {job.command}", arguments=args, full="--full" in args, branch_fix="--branch" in args)
raw = (job.trigger_comment_body or "").strip() or f"@codex {job.command}"
return ParsedCommand(name=job.command, raw=raw, arguments=args, full="--full" in args, branch_fix="--branch" in args)
def _handle_non_review_command(