|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import argparse |
| 6 | +import ast |
6 | 7 | import csv |
7 | 8 | import hashlib |
8 | 9 | import json |
@@ -270,13 +271,72 @@ def render_candidate(candidate: dict) -> str: |
270 | 271 |
|
271 | 272 | def _extract_json(text: str) -> dict: |
272 | 273 | stripped = text.strip() |
273 | | - if stripped.startswith("```"): |
274 | | - stripped = re.sub(r"^```(?:json)?\\s*", "", stripped) |
275 | | - stripped = re.sub(r"\\s*```$", "", stripped) |
276 | | - start, end = stripped.find("{"), stripped.rfind("}") |
277 | | - if start < 0 or end <= start: |
278 | | - raise ValueError("strategy agent returned no JSON object") |
279 | | - return json.loads(stripped[start:end + 1]) |
| 274 | + decoder = json.JSONDecoder() |
| 275 | + for start, character in enumerate(stripped): |
| 276 | + if character != "{": |
| 277 | + continue |
| 278 | + try: |
| 279 | + value, _ = decoder.raw_decode(stripped[start:]) |
| 280 | + except json.JSONDecodeError: |
| 281 | + continue |
| 282 | + if isinstance(value, dict): |
| 283 | + value["strategy_parse_mode"] = "json" |
| 284 | + return value |
| 285 | + for block in re.findall( |
| 286 | + r"```(?:json|python)?\s*(.*?)```", |
| 287 | + stripped, |
| 288 | + re.DOTALL | re.IGNORECASE, |
| 289 | + ): |
| 290 | + try: |
| 291 | + value = ast.literal_eval(block.strip()) |
| 292 | + except (SyntaxError, ValueError): |
| 293 | + continue |
| 294 | + if isinstance(value, dict): |
| 295 | + value["strategy_parse_mode"] = "python-literal" |
| 296 | + return value |
| 297 | + target_matches = re.findall( |
| 298 | + r"(?:Targeting\s+Leaf|pending\s+leaf)\*{0,2}\s*:?\s*" |
| 299 | + r"(?:\*\*)?`?([A-Za-z0-9]+(?:-[A-Za-z0-9]+)+)`?(?:\*\*)?", |
| 300 | + stripped, |
| 301 | + re.IGNORECASE, |
| 302 | + ) |
| 303 | + objective_match = re.search( |
| 304 | + r"Objective\*{0,2}\s*:\s*(.+)$", |
| 305 | + stripped, |
| 306 | + re.MULTILINE | re.IGNORECASE, |
| 307 | + ) |
| 308 | + steps = [ |
| 309 | + re.sub(r"^\s*(?:[-*]|\d+\.)\s*", "", line).strip() |
| 310 | + for line in stripped.splitlines() |
| 311 | + if re.match(r"^\s*(?:[-*]|\d+\.)\s+\S", line) |
| 312 | + and "Objective" not in line |
| 313 | + and "Constraint" not in line |
| 314 | + ] |
| 315 | + objective = ( |
| 316 | + objective_match.group(1).strip() |
| 317 | + if objective_match is not None |
| 318 | + else "" |
| 319 | + ) |
| 320 | + if not objective: |
| 321 | + prose = [ |
| 322 | + line.strip() |
| 323 | + for line in stripped.splitlines() |
| 324 | + if line.strip() |
| 325 | + and not line.strip().startswith(("#", "```")) |
| 326 | + ] |
| 327 | + objective = " ".join(prose[:3])[:1200] |
| 328 | + if not objective: |
| 329 | + raise ValueError("strategy agent returned no usable candidate") |
| 330 | + digest = hashlib.sha256(stripped.encode()).hexdigest()[:12] |
| 331 | + return { |
| 332 | + "candidate_id": f"candidate-prose-{digest}", |
| 333 | + "target_obligation_id": ( |
| 334 | + target_matches[-1] if target_matches else "" |
| 335 | + ), |
| 336 | + "hypothesis": objective, |
| 337 | + "plan": {"steps": steps[:8]}, |
| 338 | + "strategy_parse_mode": "prose", |
| 339 | + } |
280 | 340 |
|
281 | 341 |
|
282 | 342 | def parse_research_verdict(output: str, candidate_id: str) -> dict: |
@@ -498,6 +558,11 @@ def propose_candidate( |
498 | 558 | flush=True, |
499 | 559 | ) |
500 | 560 | candidate = _extract_json(strategy_output) |
| 561 | + parse_mode = candidate.pop("strategy_parse_mode", "unknown") |
| 562 | + print( |
| 563 | + f"[autoresearch] phase=strategy-parse mode={parse_mode}", |
| 564 | + flush=True, |
| 565 | + ) |
501 | 566 | candidate, repaired_fields = repair_candidate_schema( |
502 | 567 | candidate, |
503 | 568 | current=current, |
|
0 commit comments