feat: add Phase 4 Chunk 5 review nodes (CoverageCheck, RevisionPlan)

This commit is contained in:
诺斯费拉图 2026-04-12 18:17:32 +08:00
parent 3473712bf9
commit 66d6fcd2ae
3 changed files with 266 additions and 1 deletions

View File

@ -27,7 +27,9 @@ class ResearchExtension(ComfyExtension):
from custom_nodes.research.evidence_pack_assemble import EvidencePackAssemble from custom_nodes.research.evidence_pack_assemble import EvidencePackAssemble
from custom_nodes.research.response_draft import ResponseDraft from custom_nodes.research.response_draft import ResponseDraft
from custom_nodes.research.tone_control import ToneControl 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: async def comfy_entrypoint() -> ComfyExtension:

View File

@ -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))

View File

@ -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"