mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-14 09:01:22 +00:00
refactor(home): reuse structured report parsing
This commit is contained in:
parent
3441303d4d
commit
255f6ef5bc
5 changed files with 83 additions and 65 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
|
@ -26,6 +25,7 @@ from .diagnostic import diagnostic_tool, evaluation_paths
|
|||
from .draft import create_draft_tools
|
||||
from .bounded import RunTimeBudgetTimer, _format_minutes, _remove_hint_by_identity
|
||||
from .compression import RecoverableCompressionAgent
|
||||
from .structured_report import parse_json_object
|
||||
from .workflow_common import required_job, save_record, workflow_reply
|
||||
from .wrapper import CaseStudyAgentWrapper
|
||||
|
||||
|
|
@ -206,23 +206,8 @@ class OptimizationResult(SpecModel):
|
|||
|
||||
|
||||
def parse_result(text):
|
||||
"""Accept JSON, including a single JSON fence surrounded by commentary."""
|
||||
text = text.strip()
|
||||
original = text
|
||||
if text.startswith("```") and text.endswith("```"):
|
||||
text = text.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
||||
try:
|
||||
payload = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
fences = re.findall(
|
||||
r"^[ \t]*```json[ \t]*\r?\n(.*?)^[ \t]*```[ \t]*\r?$",
|
||||
original,
|
||||
flags=re.MULTILINE | re.DOTALL | re.IGNORECASE,
|
||||
)
|
||||
if len(fences) != 1 or len(re.findall(r"^[ \t]*```", original, flags=re.MULTILINE)) != 2:
|
||||
raise
|
||||
payload = json.loads(fences[0])
|
||||
return OptimizationResult.model_validate(payload)
|
||||
"""Parse the final report using the shared formatting recovery rules."""
|
||||
return OptimizationResult.model_validate(parse_json_object(text, report_name="optimizer"))
|
||||
|
||||
|
||||
EVALUATION_OPTIONS = {
|
||||
|
|
|
|||
|
|
@ -65,46 +65,44 @@ class StructuredReportAgent(BoundedReActAgent):
|
|||
)
|
||||
|
||||
|
||||
def parse_json_object(message: Msg, *, report_name: str) -> dict[str, Any]:
|
||||
"""Parse an object with conservative repairs for common model formatting errors."""
|
||||
|
||||
text = message.get_text_content() or ""
|
||||
candidates = [text.strip()]
|
||||
without_thinking = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE).strip()
|
||||
if without_thinking and without_thinking != candidates[0]:
|
||||
candidates.append(without_thinking)
|
||||
fenced_candidates = [
|
||||
match.group(1).strip()
|
||||
for match in re.finditer(r"```(?:json)?\s*(.*?)```", without_thinking, flags=re.DOTALL | re.IGNORECASE)
|
||||
]
|
||||
if len(fenced_candidates) == 1:
|
||||
candidates.extend(fenced_candidates)
|
||||
object_candidates = _extract_json_objects(without_thinking)
|
||||
if len(object_candidates) == 1:
|
||||
candidates.extend(object_candidates)
|
||||
|
||||
saw_non_object = False
|
||||
seen: set[str] = set()
|
||||
for candidate in candidates:
|
||||
if not candidate or candidate in seen:
|
||||
continue
|
||||
seen.add(candidate)
|
||||
repaired = _remove_trailing_commas(candidate)
|
||||
for attempt in (candidate,) if repaired == candidate else (candidate, repaired):
|
||||
try:
|
||||
payload = json.loads(attempt)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
saw_non_object = True
|
||||
if saw_non_object:
|
||||
def parse_json_object(message: Msg | str, *, report_name: str) -> dict[str, Any]:
|
||||
"""Recover one JSON object, rejecting competing reports and non-object results."""
|
||||
text = message if isinstance(message, str) else message.get_text_content() or ""
|
||||
# Preserve valid JSON strings containing literal thinking tags or Markdown fences.
|
||||
try:
|
||||
payload = json.loads(_remove_trailing_commas(text.strip()))
|
||||
except json.JSONDecodeError:
|
||||
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE).strip()
|
||||
if "```" in text:
|
||||
fences = list(
|
||||
re.finditer(
|
||||
r"^[ \t]*```(?:json)?[ \t]*\r?\n(.*?)^[ \t]*```[ \t]*\r?$",
|
||||
text,
|
||||
flags=re.MULTILINE | re.DOTALL | re.IGNORECASE,
|
||||
),
|
||||
)
|
||||
if len(fences) != 1 or len(re.findall(r"^[ \t]*```", text, flags=re.MULTILINE)) != 2:
|
||||
raise ValueError(f"{report_name} did not return a single JSON report") from None
|
||||
fence = fences[0]
|
||||
if _extract_json_containers(text[: fence.start()] + text[fence.end() :]):
|
||||
raise ValueError(f"{report_name} returned competing JSON reports") from None
|
||||
candidate = fence.group(1)
|
||||
else:
|
||||
candidates = _extract_json_containers(text)
|
||||
if len(candidates) != 1:
|
||||
raise ValueError(f"{report_name} did not return a single JSON report") from None
|
||||
candidate = candidates[0]
|
||||
try:
|
||||
payload = json.loads(_remove_trailing_commas(candidate))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"{report_name} did not return a JSON object") from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"{report_name} report must be a JSON object")
|
||||
raise ValueError(f"{report_name} did not return a JSON object") from None
|
||||
return payload
|
||||
|
||||
|
||||
def _extract_json_objects(text: str) -> list[str]:
|
||||
"""Extract balanced JSON-looking objects without interpreting braces inside strings."""
|
||||
def _extract_json_containers(text: str) -> list[str]:
|
||||
"""Extract whole containers so arrays cannot be mistaken for their nested objects."""
|
||||
|
||||
objects: list[str] = []
|
||||
start: int | None = None
|
||||
|
|
@ -122,11 +120,11 @@ def _extract_json_objects(text: str) -> list[str]:
|
|||
continue
|
||||
if character == '"':
|
||||
in_string = True
|
||||
elif character == "{":
|
||||
elif character in "{[":
|
||||
if depth == 0:
|
||||
start = index
|
||||
depth += 1
|
||||
elif character == "}" and depth:
|
||||
elif character in "}]" and depth:
|
||||
depth -= 1
|
||||
if depth == 0 and start is not None:
|
||||
objects.append(text[start : index + 1])
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from ..code.repository import validate_repository
|
|||
from ..evaluation.artifacts import contained, write_json
|
||||
from .case_study import python_tool
|
||||
from .sandbox import create_case_study_sandbox
|
||||
from .structured_report import parse_json_object
|
||||
|
||||
INSPECTION_JOBS = (
|
||||
"home_list_validation_results",
|
||||
|
|
@ -203,10 +204,7 @@ async def review_report(
|
|||
permission_mode="bypass",
|
||||
)
|
||||
report.update({key: reply.get(key) for key in ("session_id", "usage", "result")})
|
||||
text = reply["result"].strip()
|
||||
if text.startswith("```") and text.endswith("```"):
|
||||
text = text.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
||||
report.update(report_model.model_validate(json.loads(text)).model_dump())
|
||||
report.update(report_model.model_validate(parse_json_object(reply["result"], report_name=role)).model_dump())
|
||||
except Exception as exc:
|
||||
report.update(
|
||||
decision=failure_decision,
|
||||
|
|
|
|||
|
|
@ -31,12 +31,39 @@ def test_parse_review_formats(text):
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", ["invalid", "[]", '{"decision":"accept"} {"decision":"reject"}'])
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"invalid",
|
||||
"[]",
|
||||
'[{"decision":"accept","reason":"Valid schema inside an invalid array"}]',
|
||||
'Result: [{"decision":"accept","reason":"Still an array"}]',
|
||||
'{"decision":"accept"} {"decision":"reject"}',
|
||||
'```json\n{"decision":"accept"}\n```\n{"decision":"reject"}',
|
||||
'```json\n{"decision":"accept"}\n```\n```json\n{"decision":"reject"}\n```',
|
||||
'```text\nexample\n```\n```json\n{"decision":"accept"}\n```',
|
||||
'```python\n{"decision":"accept"}\n```',
|
||||
'```json\n{"decision":"accept"}',
|
||||
],
|
||||
)
|
||||
def test_invalid_or_ambiguous_reports_require_correction(text):
|
||||
with pytest.raises(ValueError):
|
||||
parse_json_object(AssistantMsg(name="review", content=text), report_name="review")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("template", ["{}", "Completed.\n```json\n{}\n```"])
|
||||
def test_text_input_preserves_literal_formatting_inside_json_strings(template):
|
||||
text = template.format('{"reason":"literal ```json"}')
|
||||
assert parse_json_object(text, report_name="review") == {
|
||||
"reason": "literal ```json",
|
||||
}
|
||||
|
||||
|
||||
def test_bare_json_preserves_literal_thinking_tags():
|
||||
text = '{"reason":"literal <think>evidence</think>"}'
|
||||
assert parse_json_object(text, report_name="review") == {"reason": "literal <think>evidence</think>"}
|
||||
|
||||
|
||||
def test_repairs_preserve_strings_and_nested_json():
|
||||
text = 'Report: {"reason":"literal ,} and escaped \\"quote\\" {", "items":[{"value":1,},],}'
|
||||
assert parse_json_object(AssistantMsg(name="review", content=text), report_name="review") == {
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ def final_report(home, branch="init", path=None):
|
|||
"```json\n{}\n```",
|
||||
"Completed.\n```json\n{}\n```\nSee report.",
|
||||
"Completed.\r\n```JSON\r\n{}\r\n```",
|
||||
"Completed. {}",
|
||||
"<think>Comparing candidates.</think>\n{}",
|
||||
],
|
||||
)
|
||||
def test_parse_optimizer_result_formats(template):
|
||||
|
|
@ -113,20 +115,28 @@ def test_parse_optimizer_result_formats(template):
|
|||
assert optimizer_module.parse_result(template.format(json.dumps(claim))).model_dump() == claim
|
||||
|
||||
|
||||
def test_optimizer_shared_recovery_rejects_competing_and_non_object_reports():
|
||||
claim = {"branch_name": "candidate", "commit_sha": "a" * 40, "evaluation_path": "run", "report": "Evidence"}
|
||||
text = json.dumps(claim)
|
||||
repaired = f"Completed.\n```json\n{text[:-1]},}}\n```"
|
||||
assert optimizer_module.parse_result(repaired).model_dump() == claim
|
||||
for invalid in (f"[{text}]", f"```json\n{text}\n```\n{text}"):
|
||||
with pytest.raises(ValueError):
|
||||
optimizer_module.parse_result(invalid)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
[
|
||||
"No result",
|
||||
'Completed. {"report":"Evidence"}',
|
||||
"Completed.\n```json\n{}",
|
||||
"Completed.\n```python\n{}\n```",
|
||||
"Completed.\n```json\n{}\n```\n```json\n{}\n```",
|
||||
"Completed.\n```text\nexample\n```\n```json\n{}\n```",
|
||||
'Completed.\n```json\n{"report":"Evidence",}\n```',
|
||||
],
|
||||
)
|
||||
def test_parse_optimizer_result_rejects_invalid_or_multiple_fences(text):
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
with pytest.raises(ValueError):
|
||||
optimizer_module.parse_result(text)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue