From dc952f6dafb02c31d0dd739064b35c9d47c1757f 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:10:46 +0800 Subject: [PATCH] feat: add ReviewImport and ReviewAtomize nodes --- custom_nodes/research/__init__.py | 6 +- custom_nodes/research/review_atomize.py | 109 ++++++++++++++++++++++++ custom_nodes/research/review_import.py | 48 +++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 custom_nodes/research/review_atomize.py create mode 100644 custom_nodes/research/review_import.py diff --git a/custom_nodes/research/__init__.py b/custom_nodes/research/__init__.py index 1462e9865..7e8945233 100644 --- a/custom_nodes/research/__init__.py +++ b/custom_nodes/research/__init__.py @@ -18,7 +18,11 @@ class ResearchExtension(ComfyExtension): from custom_nodes.research.methods_draft import MethodsDraft from custom_nodes.research.consistency_check import ConsistencyCheck from custom_nodes.research.export_manuscript import ExportManuscript - return [PaperSearch, PaperClaimExtract, ClaimEvidenceAssemble, StyleProfileExtract, ReferencePaperSelect, SectionPlan, AbstractDraft, IntroductionDraft, MethodsDraft, ConsistencyCheck, ExportManuscript] + from custom_nodes.research.review_import import ReviewImport + from custom_nodes.research.review_atomize import ReviewAtomize + from custom_nodes.research.review_classify import ReviewClassify + from custom_nodes.research.review_map import ReviewMap + return [PaperSearch, PaperClaimExtract, ClaimEvidenceAssemble, StyleProfileExtract, ReferencePaperSelect, SectionPlan, AbstractDraft, IntroductionDraft, MethodsDraft, ConsistencyCheck, ExportManuscript, ReviewImport, ReviewAtomize, ReviewClassify, ReviewMap] async def comfy_entrypoint() -> ComfyExtension: diff --git a/custom_nodes/research/review_atomize.py b/custom_nodes/research/review_atomize.py new file mode 100644 index 000000000..6a4cfe0ac --- /dev/null +++ b/custom_nodes/research/review_atomize.py @@ -0,0 +1,109 @@ +# custom_nodes/research/review_atomize.py +"""ReviewAtomize node - split review into atomic items.""" +import json +import re +from typing_extensions import override +from comfy_api.latest import ComfyNode, io + + +class ReviewAtomize(io.ComfyNode): + """Split review text into atomic feedback items.""" + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="ReviewAtomize", + display_name="Atomize Review", + category="Research", + inputs=[ + io.String.Input( + "raw_review", + display_name="Raw Review (JSON)", + default="{}", + multiline=True, + ), + io.Int.Input( + "min_severity", + display_name="Min Severity to Extract", + default=1, + min=1, + max=3, + step=1, + ), + ], + outputs=[ + io.String.Output(display_name="Review Items (JSON)"), + ], + ) + + @classmethod + def execute(cls, raw_review: str, min_severity: int) -> io.NodeOutput: + try: + review = json.loads(raw_review) if raw_review else {} + except json.JSONDecodeError: + review = {"original_text": raw_review} + + original_text = review.get("original_text", "") + paragraphs = review.get("paragraphs", []) + + items = [] + item_id = 1 + + # Common reviewer phrases that indicate feedback + feedback_patterns = [ + (r"the (?:paper|method|approach|model|results?|experiments?) (?:is|are|should|could|may|might) (.+)", "methodology"), + (r"(?:it is|this is|there is) (?:not|rarely|insufficiently) (.+)", "gap"), + (r"(?:more|additional|further) (.+) (?:is|are) needed", "missing"), + (r"(?:unclear|confusing|vague|ambiguous) (.+)", "clarity"), + (r"(?:incorrect|inaccurate|wrong|error) (.+)", "error"), + (r"(?:major|significant|critical|important) (.+)", "major"), + (r"(?:minor|small|small) (.+)", "minor"), + (r"see (?:Section|Figure|Table) (\d+)", "reference"), + ] + + # Split by sentence and extract items + sentences = re.split(r'(?<=[.!?])\s+', original_text) + + for sentence in sentences: + sentence = sentence.strip() + if len(sentence) < 20: # Skip very short sentences + continue + + # Classify the item + item_type = "general" + severity = 1 + + sentence_lower = sentence.lower() + + if any(w in sentence_lower for w in ["major", "significant", "critical", "important", "must"]): + severity = 3 + item_type = "major_concern" + elif any(w in sentence_lower for w in ["minor", "small", "suggestion"]): + severity = 1 + item_type = "suggestion" + elif any(w in sentence_lower for w in ["unclear", "confusing", "vague", "ambiguous"]): + severity = 2 + item_type = "clarity" + elif any(w in sentence_lower for w in ["error", "incorrect", "wrong", "mistake"]): + severity = 3 + item_type = "error" + elif any(w in sentence_lower for w in ["missing", "lacking", "insufficient", "not enough"]): + severity = 2 + item_type = "missing_info" + + if severity >= min_severity: + items.append({ + "id": f"item_{item_id}", + "text": sentence, + "type": item_type, + "severity": severity, + "quoted_claims": [], + }) + item_id += 1 + + result = { + "total_items": len(items), + "items": items, + } + + return io.NodeOutput(review_items=json.dumps(result, indent=2)) diff --git a/custom_nodes/research/review_import.py b/custom_nodes/research/review_import.py new file mode 100644 index 000000000..f727f3c7b --- /dev/null +++ b/custom_nodes/research/review_import.py @@ -0,0 +1,48 @@ +# custom_nodes/research/review_import.py +"""ReviewImport node - import review text.""" +import json +from typing_extensions import override +from comfy_api.latest import ComfyNode, io + + +class ReviewImport(io.ComfyNode): + """Import reviewer feedback text for analysis.""" + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="ReviewImport", + display_name="Import Review", + category="Research", + inputs=[ + io.String.Input( + "review_text", + display_name="Review Text", + default="", + multiline=True, + ), + io.String.Input( + "reviewer_role", + display_name="Reviewer Role", + default="reviewer", + ), + ], + outputs=[ + io.String.Output(display_name="Raw Review (JSON)"), + ], + ) + + @classmethod + def execute(cls, review_text: str, reviewer_role: str) -> io.NodeOutput: + if not review_text.strip(): + return io.NodeOutput(raw_review=json.dumps({"error": "No review text provided"})) + + # Parse the review into structured format + raw_review = { + "reviewer_role": reviewer_role, + "original_text": review_text, + "paragraphs": [p.strip() for p in review_text.split("\n\n") if p.strip()], + "word_count": len(review_text.split()), + } + + return io.NodeOutput(raw_review=json.dumps(raw_review, indent=2))