Skip to content

Commit 2a08f7f

Browse files
committed
Harden long-run runtime and eval pipeline
1 parent 00a96cf commit 2a08f7f

17 files changed

Lines changed: 819 additions & 22 deletions

File tree

‎research_agent/inno/environment/utils.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
from pathlib import Path
66
import urllib.request
77

8+
9+
def dataset_source_path(category: str) -> Path:
10+
repo_root = Path(__file__).resolve().parents[3]
11+
return repo_root / "benchmark" / "process" / "dataset_candidate" / category
12+
813
def setup_metachain():
914
cmd = "pip list | grep metachain"
1015
response = run_command_in_container(cmd)
@@ -31,8 +36,8 @@ def setup_dataset(category: str, local_workplace: str):
3136
return
3237

3338
# 检查源目录是否存在
34-
source_path = f"../benchmark/process/dataset_candidate/{category}"
35-
if not os.path.exists(source_path):
39+
source_path = dataset_source_path(category)
40+
if not source_path.exists():
3641
raise Exception(f"source path {source_path} not exists")
3742

3843
try:

‎research_agent/inno/evals/adapter.py‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,86 @@
66
from datetime import datetime
77
import json
88
from pathlib import Path
9+
import re
910
import uuid
1011
from typing import Any, Dict, List, Optional
1112

1213
from research_agent.inno.evals.evaluator import build_default_research_evaluator
1314
from research_agent.inno.evals.evaluator import GoalDrivenEvalReport
14-
from research_agent.inno.evals.trace import AgentStepTrace, ResearchRunTrace, ToolCallTrace
15+
from research_agent.inno.evals.trace import AgentStepTrace, ResearchRunTrace, RetrievalItem, ToolCallTrace
16+
17+
18+
def _split_claim_block(text: str) -> List[str]:
19+
normalized = text.replace("\r", "").strip()
20+
if not normalized:
21+
return []
22+
23+
sections = re.split(r"\n\s*\n+", normalized)
24+
claims: List[str] = []
25+
for section in sections:
26+
section = re.sub(r"^\s*(\d+\.\s*)", "", section.strip())
27+
section = re.sub(r"^\s*[-*]\s*", "", section)
28+
if len(section) < 8:
29+
continue
30+
claims.append(section)
31+
return claims or [normalized]
32+
33+
34+
def _normalize_claims(claims: Optional[List[str]]) -> List[str]:
35+
normalized_claims: List[str] = []
36+
for claim in claims or []:
37+
claim = claim.strip()
38+
if not claim:
39+
continue
40+
normalized_claims.extend(_split_claim_block(claim))
41+
return normalized_claims
42+
43+
44+
def _collect_evidence_items(
45+
plan: Optional[Dict[str, Any]],
46+
final_output: Optional[Dict[str, Any]],
47+
) -> List[RetrievalItem]:
48+
items: List[RetrievalItem] = []
49+
for section_name, section_value in (plan or {}).items():
50+
if isinstance(section_value, str) and section_value.strip():
51+
items.append(
52+
RetrievalItem(
53+
source_type="other",
54+
identifier=f"plan:{section_name}",
55+
title=section_name,
56+
content=section_value,
57+
)
58+
)
59+
elif isinstance(section_value, dict) and section_value:
60+
items.append(
61+
RetrievalItem(
62+
source_type="other",
63+
identifier=f"plan:{section_name}",
64+
title=section_name,
65+
content=json.dumps(section_value, ensure_ascii=False),
66+
)
67+
)
68+
69+
for output_name, output_value in (final_output or {}).items():
70+
if isinstance(output_value, str) and output_value.strip():
71+
items.append(
72+
RetrievalItem(
73+
source_type="tool_output",
74+
identifier=f"final:{output_name}",
75+
title=output_name,
76+
content=output_value,
77+
)
78+
)
79+
elif isinstance(output_value, dict) and output_value:
80+
items.append(
81+
RetrievalItem(
82+
source_type="tool_output",
83+
identifier=f"final:{output_name}",
84+
title=output_name,
85+
content=json.dumps(output_value, ensure_ascii=False),
86+
)
87+
)
88+
return items
1589

1690

1791
def build_research_run_trace(
@@ -39,11 +113,13 @@ def build_research_run_trace(
39113
task_id=task_id,
40114
query=query,
41115
goal=goal,
42-
claims=list(claims or []),
116+
claims=_normalize_claims(claims),
43117
plan=dict(plan or {}),
44118
final_output=dict(final_output or {}),
45119
metadata=dict(metadata or {}),
46120
)
121+
for item in _collect_evidence_items(plan, final_output):
122+
trace.add_retrieval(item)
47123
for tool_call in tool_calls or []:
48124
trace.add_tool_call(tool_call)
49125
for agent_step in agent_steps or []:

‎research_agent/inno/evals/metrics.py‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import re
56
from typing import Dict, Iterable, List, Sequence
67

78
from research_agent.inno.evals.trace import ResearchRunTrace
@@ -16,6 +17,39 @@ def _contains_any(haystack: str, needles: Iterable[str]) -> bool:
1617
return any(_normalize(needle) in normalized_haystack for needle in needles if needle)
1718

1819

20+
def _tokenize(text: str) -> set[str]:
21+
return {
22+
token
23+
for token in re.findall(r"[a-z0-9]+", _normalize(text))
24+
if len(token) >= 4
25+
}
26+
27+
28+
def _is_supported_by_evidence(claim: str, evidence_texts: Sequence[str]) -> bool:
29+
if not claim.strip():
30+
return True
31+
32+
normalized_claim = _normalize(claim)
33+
claim_tokens = _tokenize(claim)
34+
if not claim_tokens:
35+
return False
36+
37+
for evidence in evidence_texts:
38+
normalized_evidence = _normalize(evidence)
39+
if normalized_claim in normalized_evidence:
40+
return True
41+
42+
evidence_tokens = _tokenize(evidence)
43+
if not evidence_tokens:
44+
continue
45+
46+
overlap = len(claim_tokens & evidence_tokens) / len(claim_tokens)
47+
if overlap >= 0.45:
48+
return True
49+
50+
return False
51+
52+
1953
def evidence_coverage(trace: ResearchRunTrace) -> Dict[str, object]:
2054
"""Score how many claims are backed by retrieved evidence or tool outputs."""
2155

@@ -32,10 +66,15 @@ def evidence_coverage(trace: ResearchRunTrace) -> Dict[str, object]:
3266
for call in trace.tool_calls
3367
if call.success
3468
)
69+
evidence_texts.extend(
70+
f"{step.agent_name} {step.output_summary}".strip()
71+
for step in trace.agent_steps
72+
if step.output_summary
73+
)
3574

3675
supported, unsupported = [], []
3776
for claim in claims:
38-
if _contains_any(" ".join(evidence_texts), [claim]):
77+
if _is_supported_by_evidence(claim, evidence_texts):
3978
supported.append(claim)
4079
else:
4180
unsupported.append(claim)

‎research_agent/inno_common.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import logging
1010
import os
1111
import re
12+
from datetime import datetime, timezone
1213
from typing import Any, Dict, List
1314

1415
from pydantic import BaseModel, Field
@@ -20,6 +21,14 @@
2021
logger = logging.getLogger(__name__)
2122

2223

24+
def append_stage_event(cache_path: str, event: Dict[str, Any]) -> str:
25+
events_path = os.path.join(cache_path, "stage_events.jsonl")
26+
os.makedirs(cache_path, exist_ok=True)
27+
with open(events_path, "a", encoding="utf-8") as f:
28+
f.write(json.dumps(event, ensure_ascii=False) + "\n")
29+
return events_path
30+
31+
2332
def _read_json_file(path: str) -> Dict:
2433
if not path or not os.path.exists(path):
2534
return {}
@@ -47,6 +56,16 @@ def update_stage_state(
4756
}
4857
with open(stage_state_path, "w", encoding="utf-8") as f:
4958
json.dump(stage_state, f, ensure_ascii=False, indent=4)
59+
append_stage_event(
60+
cache_path,
61+
{
62+
"timestamp": datetime.now(timezone.utc).isoformat(),
63+
"stage": stage_name,
64+
"status": status,
65+
"artifacts": artifacts or {},
66+
"metadata": metadata or {},
67+
},
68+
)
5069
return stage_state_path
5170

5271

@@ -493,6 +512,28 @@ def find_json_boundaries(text: str):
493512
return {}
494513

495514

515+
def resolve_experiment_analysis(
516+
context_variables: Dict[str, Any],
517+
fallback_output: str,
518+
) -> tuple[str, dict]:
519+
"""Resolve experiment analysis content from structured context or fallback text."""
520+
521+
experiment_report = context_variables.get("experiment_report")
522+
if isinstance(experiment_report, list) and experiment_report:
523+
latest_report = experiment_report[-1] or {}
524+
analysis_report = latest_report.get("analysis_report")
525+
if analysis_report:
526+
return analysis_report, latest_report.get("further_plan", {}) or {}
527+
528+
parsed_output = extract_json_from_output(fallback_output)
529+
if parsed_output:
530+
analysis_report = parsed_output.get("analysis_report") or fallback_output
531+
further_plan = parsed_output.get("further_plan", {}) or {}
532+
return analysis_report, further_plan
533+
534+
return fallback_output, {}
535+
536+
496537
def get_args():
497538
parser = argparse.ArgumentParser()
498539
parser.add_argument("--instance_path", type=str, default="benchmark/gnn.json")

‎research_agent/run_infer_idea.py‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
load_cached_survey_result,
4646
load_cached_prepare_result,
4747
persist_stage_result,
48+
resolve_experiment_analysis,
4849
resolve_prepare_result,
4950
persist_survey_result,
5051
get_args,
@@ -61,6 +62,14 @@ def _persist_stage_output(cache_path: str, stage_name: str, payload: Dict[str, A
6162
json.dump(payload, f, ensure_ascii=False, indent=4)
6263
return stage_path
6364

65+
66+
def _update_runtime_progress(runtime: MasterRuntime, run_id: str, task_level: str) -> None:
67+
runtime.write_runtime_status(
68+
run_id=run_id,
69+
status="running",
70+
metadata={"entrypoint": "run_infer_idea", "task_level": task_level},
71+
)
72+
6473
class InnoFlow(FlowModule):
6574
def __init__(self, cache_path: str, log_path: Union[str, None, MetaChainLogger] = None, model: str = "gpt-4o-2024-08-06", code_env: DockerEnv = None, web_env: BrowserEnv = None, file_env: RequestsMarkdownBrowser = None):
6675
super().__init__(cache_path, log_path, model)
@@ -151,6 +160,7 @@ async def forward(self, instance_path: str, task_level: str, local_root: str, wo
151160
artifacts={"prepare_result": os.path.join(self.cache_path, "prepare_stage", "prepare_result.json")},
152161
)
153162
context_variables["stage_state"] = runtime.load_state()
163+
_update_runtime_progress(runtime, run_id, task_level)
154164
paper_list = prepare_dict["reference_papers"]
155165
download_res = self.download_papaer({"paper_list": paper_list, "local_root": local_root, "workplace_name": workplace_name})
156166

@@ -229,6 +239,7 @@ async def forward(self, instance_path: str, task_level: str, local_root: str, wo
229239
artifacts={"survey_result": survey_result_path},
230240
)
231241
context_variables["stage_state"] = runtime.load_state()
242+
_update_runtime_progress(runtime, run_id, task_level)
232243

233244
plan_query = f"""\
234245
I have an innovative ideas related to machine learning:
@@ -283,6 +294,7 @@ async def forward(self, instance_path: str, task_level: str, local_root: str, wo
283294
artifacts=context_variables.get("plan_artifacts", {}),
284295
)
285296
context_variables["stage_state"] = runtime.load_state()
297+
_update_runtime_progress(runtime, run_id, task_level)
286298

287299
if not runtime.can_run_stage("implement"):
288300
raise RuntimeError("Implement stage cannot start before required prior stages are completed.")
@@ -427,6 +439,7 @@ async def forward(self, instance_path: str, task_level: str, local_root: str, wo
427439
artifacts={"project_manifest": implement_path},
428440
)
429441
context_variables["stage_state"] = runtime.load_state()
442+
_update_runtime_progress(runtime, run_id, task_level)
430443

431444
if not runtime.can_run_stage("judge"):
432445
raise RuntimeError("Judge stage cannot start before implement stage is completed.")
@@ -478,6 +491,7 @@ async def forward(self, instance_path: str, task_level: str, local_root: str, wo
478491
artifacts={"judge_report": judge_path},
479492
)
480493
context_variables["stage_state"] = runtime.load_state()
494+
_update_runtime_progress(runtime, run_id, task_level)
481495

482496
MAX_ITER_TIMES = max_iter_times
483497
for i in range(MAX_ITER_TIMES):
@@ -573,6 +587,7 @@ async def forward(self, instance_path: str, task_level: str, local_root: str, wo
573587
artifacts={"submit_result": submit_path},
574588
)
575589
context_variables["stage_state"] = runtime.load_state()
590+
_update_runtime_progress(runtime, run_id, task_level)
576591

577592
EXP_ITER_TIMES = 2
578593
analysis_report = ""
@@ -598,9 +613,10 @@ async def forward(self, instance_path: str, task_level: str, local_root: str, wo
598613
judge_messages.append({"role": "user", "content": exp_planner_query})
599614
judge_messages, context_variables = await self.exp_analyser(judge_messages, context_variables, iter_times=f"refine_{i+1}")
600615
analysis_report = judge_messages[-1]["content"]
601-
602-
analysis_report = context_variables["experiment_report"][-1]["analysis_report"]
603-
further_plan = context_variables["experiment_report"][-1]["further_plan"]
616+
analysis_report, further_plan = resolve_experiment_analysis(
617+
context_variables,
618+
analysis_report,
619+
)
604620
# print(analysis_report)
605621
refine_query = f"""\
606622
You are given an innovative idea:
@@ -636,6 +652,7 @@ async def forward(self, instance_path: str, task_level: str, local_root: str, wo
636652
"analyze",
637653
artifacts={"analysis_report": analysis_path},
638654
)
655+
_update_runtime_progress(runtime, run_id, task_level)
639656

640657
print(refine_res)
641658
runtime.sync_stage_state()
@@ -710,13 +727,14 @@ def main(args, references):
710727
with open(args.instance_path, "r", encoding="utf-8") as f:
711728
eval_instance = json.load(f)
712729
instance_id = eval_instance["instance_id"] + "_idea"
713-
cache_path = args.cache_path + "_" + instance_id + "_" + COMPLETION_MODEL.replace("/", "__")
730+
model_suffix = args.model.replace("/", "__")
731+
cache_path = args.cache_path + "_" + instance_id + "_" + model_suffix
714732
local_root = os.path.join(
715733
os.getcwd(),
716734
"workplace_paper",
717-
f"task_{instance_id}" + "_" + COMPLETION_MODEL.replace("/", "__"),
735+
f"task_{instance_id}" + "_" + model_suffix,
718736
)
719-
container_name = args.container_name + "_" + instance_id + "_" + COMPLETION_MODEL.replace("/", "__")
737+
container_name = args.container_name + "_" + instance_id + "_" + model_suffix
720738
os.makedirs(local_root, exist_ok=True)
721739
env_config = DockerConfig(container_name = container_name,
722740
workplace_name = args.workplace_name,

0 commit comments

Comments
 (0)