From 66d6fcd2aeca3e22f842d7a1865e36fed8228cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AF=BA=E6=96=AF=E8=B4=B9=E6=8B=89=E5=9B=BE?= <1132505822@qq.com> Date: Sun, 12 Apr 2026 18:17:32 +0800 Subject: [PATCH] feat: add Phase 4 Chunk 5 review nodes (CoverageCheck, RevisionPlan) --- custom_nodes/research/__init__.py | 4 +- custom_nodes/research/coverage_check.py | 90 ++++++++++++ custom_nodes/research/revision_plan.py | 173 ++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 custom_nodes/research/coverage_check.py create mode 100644 custom_nodes/research/revision_plan.py diff --git a/custom_nodes/research/__init__.py b/custom_nodes/research/__init__.py index fa97d6f54..c56b78ea6 100644 --- a/custom_nodes/research/__init__.py +++ b/custom_nodes/research/__init__.py @@ -27,7 +27,9 @@ class ResearchExtension(ComfyExtension): from custom_nodes.research.evidence_pack_assemble import EvidencePackAssemble from custom_nodes.research.response_draft import ResponseDraft from custom_nodes.research.tone_control import ToneControl - return [PaperSearch, PaperClaimExtract, ClaimEvidenceAssemble, StyleProfileExtract, ReferencePaperSelect, SectionPlan, AbstractDraft, IntroductionDraft, MethodsDraft, ConsistencyCheck, ExportManuscript, ReviewImport, ReviewAtomize, ReviewClassify, ReviewMap, EvidenceGapDetect, ActionRoute, EvidencePackAssemble, ResponseDraft, ToneControl] + from custom_nodes.research.coverage_check import CoverageCheck + from custom_nodes.research.revision_plan import RevisionPlan + return [PaperSearch, PaperClaimExtract, ClaimEvidenceAssemble, StyleProfileExtract, ReferencePaperSelect, SectionPlan, AbstractDraft, IntroductionDraft, MethodsDraft, ConsistencyCheck, ExportManuscript, ReviewImport, ReviewAtomize, ReviewClassify, ReviewMap, EvidenceGapDetect, ActionRoute, EvidencePackAssemble, ResponseDraft, ToneControl, CoverageCheck, RevisionPlan] async def comfy_entrypoint() -> ComfyExtension: diff --git a/custom_nodes/research/coverage_check.py b/custom_nodes/research/coverage_check.py new file mode 100644 index 000000000..64e393f69 --- /dev/null +++ b/custom_nodes/research/coverage_check.py @@ -0,0 +1,90 @@ +"""CoverageCheck node - check response coverage of review items.""" +import json +from typing_extensions import override +from comfy_api.latest import ComfyNode, io + + +class CoverageCheck(io.ComfyNode): + """Check if responses adequately cover all review items.""" + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="CoverageCheck", + display_name="Coverage Check", + category="Research", + inputs=[ + io.String.Input( + "finalized_responses", + display_name="Finalized Responses (JSON)", + default="{}", + multiline=True, + ), + io.String.Input( + "review_items", + display_name="Review Items (JSON)", + default="[]", + multiline=True, + ), + ], + outputs=[ + io.String.Output(display_name="Coverage Report (JSON)"), + ], + ) + + @classmethod + def execute(cls, finalized_responses: str, review_items: str) -> io.NodeOutput: + try: + responses_data = json.loads(finalized_responses) if finalized_responses else {} + items = json.loads(review_items) if review_items else [] + except json.JSONDecodeError: + responses_data = {} + items = [] + + responses = responses_data.get("responses", []) + + coverage = [] + covered_ids = set() + uncovered_items = [] + + for resp in responses: + gap_id = resp.get("gap_id", "") + response_text = resp.get("response_text", "") + action_type = resp.get("action_type", "") + + coverage.append({ + "gap_id": gap_id, + "covered": True, + "response_length": len(response_text), + "action_type": action_type, + "assessment": "adequate" if len(response_text) > 50 else "too_brief", + }) + covered_ids.add(gap_id) + + # Check for uncovered items + for item in items: + item_id = item.get("item_id", f"item_{len(coverage)}") + if item_id not in covered_ids: + uncovered_items.append({ + "item_id": item_id, + "issue": item.get("item_text", "")[:100], + "severity": item.get("severity", 1), + "status": "not_addressed", + }) + + # Determine overall coverage + total = len(coverage) + len(uncovered_items) + covered_count = len(coverage) + coverage_pct = (covered_count / total * 100) if total > 0 else 100 + + report = { + "total_items": total, + "covered_items": covered_count, + "uncovered_items": len(uncovered_items), + "coverage_percentage": round(coverage_pct, 1), + "overall_status": "complete" if coverage_pct >= 90 else "partial" if coverage_pct >= 50 else "incomplete", + "item_coverage": coverage, + "uncovered": uncovered_items, + } + + return io.NodeOutput(coverage_report=json.dumps(report, indent=2)) diff --git a/custom_nodes/research/revision_plan.py b/custom_nodes/research/revision_plan.py new file mode 100644 index 000000000..f237c050f --- /dev/null +++ b/custom_nodes/research/revision_plan.py @@ -0,0 +1,173 @@ +"""RevisionPlan node - create revision task plan from coverage report.""" +import json +from typing_extensions import override +from comfy_api.latest import ComfyNode, io + + +class RevisionPlan(io.ComfyNode): + """Create a structured revision task plan from coverage report.""" + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="RevisionPlan", + display_name="Revision Plan", + category="Research", + inputs=[ + io.String.Input( + "coverage_report", + display_name="Coverage Report (JSON)", + default="{}", + multiline=True, + ), + io.String.Input( + "action_routes", + display_name="Action Routes (JSON)", + default="{}", + multiline=True, + ), + ], + outputs=[ + io.String.Output(display_name="Revision Plan (JSON)"), + ], + ) + + @classmethod + def execute(cls, coverage_report: str, action_routes: str) -> io.NodeOutput: + try: + coverage = json.loads(coverage_report) if coverage_report else {} + routes_data = json.loads(action_routes) if action_routes else {} + except json.JSONDecodeError: + coverage = {} + routes_data = {} + + uncovered = coverage.get("uncovered_items", []) + routes = routes_data.get("routes", []) + item_coverage = coverage.get("item_coverage", []) + + # Group by action type + by_action = {"experiment": [], "citation": [], "revise": [], "respond": []} + for route in routes: + action_type = route.get("action_type", "respond") + if action_type in by_action: + by_action[action_type].append(route) + + # Create revision tasks + tasks = [] + task_id = 1 + + # Priority 1: High severity uncovered + for item in uncovered: + if item.get("severity", 1) >= 3: + tasks.append({ + "task_id": f"task_{task_id}", + "priority": "high", + "action": "address_immediately", + "description": f"Uncovered high-severity item: {item.get('issue', '')[:80]}", + "estimated_time": "2-4 hours", + }) + task_id += 1 + + # Priority 2: Experiment tasks + for route in by_action.get("experiment", []): + tasks.append({ + "task_id": f"task_{task_id}", + "priority": "high", + "action": "run_experiment", + "description": route.get("description", ""), + "effort": route.get("estimated_effort", "high"), + "action_items": route.get("action_items", []), + }) + task_id += 1 + + # Priority 3: Citation tasks + for route in by_action.get("citation", []): + tasks.append({ + "task_id": f"task_{task_id}", + "priority": "medium", + "action": "add_citations", + "description": route.get("description", ""), + "effort": route.get("estimated_effort", "low"), + "action_items": route.get("action_items", []), + }) + task_id += 1 + + # Priority 4: Revise tasks + for route in by_action.get("revise", []): + tasks.append({ + "task_id": f"task_{task_id}", + "priority": "medium", + "action": "revise_manuscript", + "description": route.get("description", ""), + "effort": route.get("estimated_effort", "medium"), + "action_items": route.get("action_items", []), + }) + task_id += 1 + + # Priority 5: Response tasks + for route in by_action.get("respond", []): + tasks.append({ + "task_id": f"task_{task_id}", + "priority": "low", + "action": "write_response", + "description": route.get("description", ""), + "effort": route.get("estimated_effort", "medium"), + "action_items": route.get("action_items", []), + }) + task_id += 1 + + # Add brief uncovered items + for item in uncovered: + if item.get("severity", 1) < 3: + tasks.append({ + "task_id": f"task_{task_id}", + "priority": "low", + "action": "review_uncovered", + "description": f"Review item: {item.get('issue', '')[:80]}", + "estimated_time": "30 minutes", + }) + task_id += 1 + + # Summarize + summary = { + "total_tasks": len(tasks), + "by_priority": { + "high": len([t for t in tasks if t.get("priority") == "high"]), + "medium": len([t for t in tasks if t.get("priority") == "medium"]), + "low": len([t for t in tasks if t.get("priority") == "low"]), + }, + "by_action": { + "run_experiment": len(by_action.get("experiment", [])), + "add_citations": len(by_action.get("citation", [])), + "revise_manuscript": len(by_action.get("revise", [])), + "write_response": len(by_action.get("respond", [])), + }, + } + + plan = { + "summary": summary, + "tasks": tasks, + "estimated_total_time": _estimate_time(tasks), + } + + return io.NodeOutput(revision_plan=json.dumps(plan, indent=2)) + + +def _estimate_time(tasks: list) -> str: + """Estimate total time for all tasks.""" + hours = 0 + for task in tasks: + time_str = task.get("estimated_time", "1 hour") + if "hour" in time_str: + try: + hours += float(time_str.split()[0]) + except (ValueError, IndexError): + hours += 1 + elif "minute" in time_str: + try: + hours += float(time_str.split()[0]) / 60 + except (ValueError, IndexError): + pass + if hours < 1: + return f"{int(hours * 60)} minutes" + return f"{int(hours)}-{int(hours + 2)} hours"