AgentSkillsCN

scientific-peer-review-response

系统性回应审稿意见并生成复审信件的技能。对审稿意见进行结构化分析(按重大/次要/编辑意见分类),逐条生成回复要点,标记修改部位,并支持多轮反馈与沟通。 以“回复审稿意见”“撰写审稿人回复”“起草复审信件”为关键步骤,助力论文修改与完善。

SKILL.md
--- frontmatter
name: scientific-peer-review-response
description: |
  査読コメントへの体系的対応とリバッタルレター生成スキル。
  査読コメントの構造化解析(Major/Minor/Editorial 分類)、
  ポイント・バイ・ポイント回答生成、改訂箇所マッピング、
  複数ラウンド対応を行う。
  「査読に回答して」「reviewer response を作成して」「リバッタルを書いて」で発火。

Scientific Peer Review Response

査読者コメントを構造化し、ポイント・バイ・ポイントの回答レターを生成するスキル。 改訂原稿との対応関係を明示し、エディタ・査読者への効果的なコミュニケーションを支援する。

When to Use

  • ジャーナルから査読結果(Decision Letter)を受け取ったとき
  • 各査読者コメントに対する回答レターを作成するとき
  • 改訂原稿における変更箇所と査読コメントのマッピングを行うとき
  • 2nd / 3rd ラウンドの再査読に対応するとき
  • エディタへのカバーレター(改訂版)を作成するとき

Quick Start

1. 査読対応ワークフロー

code
Decision Letter 受領
  ├─ Phase 1: コメント構造化
  │   ├─ 査読者ごとにコメント分離
  │   ├─ 各コメントを Major / Minor / Editorial に分類
  │   ├─ コメント間の関連性を検出
  │   └─ 対応優先度の決定
  ├─ Phase 2: 対応戦略策定
  │   ├─ 受容 (Accept): そのまま修正
  │   ├─ 部分受容 (Partially Accept): 一部修正 + 説明
  │   ├─ 反論 (Rebut): エビデンスに基づく反論
  │   └─ 追加実験/解析の要否判断
  ├─ Phase 3: 回答レター生成
  │   ├─ ポイント・バイ・ポイント回答
  │   ├─ 引用テキスト + 回答 + 改訂箇所の3点セット
  │   └─ 追加データ/図表の挿入
  ├─ Phase 4: 改訂箇所マッピング
  │   ├─ コメント → 改訂箇所の対応表
  │   ├─ 変更テキストのハイライト指示
  │   └─ ページ/行番号参照
  └─ Phase 5: ファイル出力
      ├─ manuscript/response_to_reviewers.md
      ├─ manuscript/response_mapping.json
      └─ manuscript/cover_letter_revised.md

2. コメント構造化エンジン

python
import re
import json
from pathlib import Path

COMMENT_SEVERITY = {
    "major": {
        "keywords": [
            "fundamental", "significant", "serious", "critical",
            "major concern", "major issue", "major revision",
            "additional experiment", "new analysis", "must address",
            "strongly recommend", "重大", "根本的",
        ],
        "weight": 3,
    },
    "minor": {
        "keywords": [
            "minor", "small", "slight", "consider",
            "suggest", "would benefit", "could improve",
            "clarify", "explain", "軽微", "検討",
        ],
        "weight": 1,
    },
    "editorial": {
        "keywords": [
            "typo", "grammar", "spelling", "formatting",
            "reference", "citation", "figure label", "table format",
            "誤字", "体裁",
        ],
        "weight": 0,
    },
}


def parse_decision_letter(decision_text):
    """
    Decision Letter を解析し、査読者ごとのコメントを構造化する。

    Args:
        decision_text: str — エディタ/査読者からのテキスト全文

    Returns:
        dict: {
            "editor_decision": "major_revision" | "minor_revision" | "reject",
            "editor_comments": str,
            "reviewers": [
                {
                    "id": "Reviewer 1",
                    "comments": [
                        {
                            "number": 1,
                            "text": "...",
                            "severity": "major" | "minor" | "editorial",
                            "requires_new_data": bool,
                            "related_section": str,
                        }
                    ]
                }
            ]
        }
    """
    result = {
        "editor_decision": _detect_decision(decision_text),
        "editor_comments": "",
        "reviewers": [],
    }

    # エディタコメントの抽出
    editor_match = re.search(
        r'(?:Editor|Associate Editor|AE)[\'s]*\s*(?:Comments?|Decision)[\s:]*\n(.*?)(?=Reviewer\s+\d|$)',
        decision_text, re.DOTALL | re.IGNORECASE
    )
    if editor_match:
        result["editor_comments"] = editor_match.group(1).strip()

    # 査読者ごとのセクション分割
    reviewer_sections = re.split(
        r'(Reviewer\s+\d+|Referee\s+\d+)',
        decision_text, flags=re.IGNORECASE
    )

    i = 1
    while i < len(reviewer_sections) - 1:
        reviewer_id = reviewer_sections[i].strip()
        reviewer_text = reviewer_sections[i + 1].strip()
        comments = _extract_comments(reviewer_text, reviewer_id)
        result["reviewers"].append({
            "id": reviewer_id,
            "comments": comments,
        })
        i += 2

    return result


def _detect_decision(text):
    """エディタの判定を検出する。"""
    text_lower = text.lower()
    if "reject" in text_lower and "resubmi" not in text_lower:
        return "reject"
    if "major revision" in text_lower or "major revisions" in text_lower:
        return "major_revision"
    if "minor revision" in text_lower or "minor revisions" in text_lower:
        return "minor_revision"
    if "accept" in text_lower:
        return "accept"
    return "unknown"


def _extract_comments(text, reviewer_id):
    """テキストから個別コメントを抽出・分類する。"""
    comments = []

    # 番号付きコメントの抽出: "1.", "1)", "(1)", "Comment 1:" 等
    numbered = re.split(
        r'(?:^|\n)\s*(?:(\d+)[.\):]|\((\d+)\)|Comment\s+(\d+)\s*:)',
        text
    )

    if len(numbered) > 1:
        # 番号付きコメントが見つかった場合
        idx = 0
        for i in range(1, len(numbered), 2):
            num_str = numbered[i] or numbered[i + 1] if i + 1 < len(numbered) else None
            body = numbered[i + 1] if i + 1 < len(numbered) else ""
            if body and body.strip():
                idx += 1
                comments.append(_classify_comment(body.strip(), idx))
    else:
        # 段落ベースで分割
        paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
        for idx, para in enumerate(paragraphs, 1):
            if len(para) > 20:  # 短すぎるものはスキップ
                comments.append(_classify_comment(para, idx))

    return comments


def _classify_comment(text, number):
    """コメントの重要度を自動分類する。"""
    text_lower = text.lower()
    severity = "minor"  # デフォルト

    for sev, config in COMMENT_SEVERITY.items():
        for kw in config["keywords"]:
            if kw.lower() in text_lower:
                severity = sev
                break

    # 追加実験/解析が必要かの判定
    requires_new_data = bool(re.search(
        r'additional\s+(?:experiment|data|analysis|result)|'
        r'new\s+(?:experiment|data|analysis)|'
        r'perform\s+(?:additional|further)|'
        r'追加(?:実験|解析|データ)',
        text_lower
    ))

    # 関連セクションの推定
    related_section = _infer_section(text)

    return {
        "number": number,
        "text": text,
        "severity": severity,
        "requires_new_data": requires_new_data,
        "related_section": related_section,
    }


def _infer_section(text):
    """コメントが関連する論文セクションを推定する。"""
    text_lower = text.lower()
    section_keywords = {
        "Abstract": ["abstract", "summary"],
        "Introduction": ["introduction", "background", "motivation"],
        "Methods": ["method", "experimental", "procedure", "protocol"],
        "Results": ["result", "figure", "table", "data"],
        "Discussion": ["discussion", "interpretation", "mechanism", "implication"],
        "Conclusion": ["conclusion", "concluding"],
        "References": ["reference", "citation", "bibliography"],
        "SI": ["supplementary", "supporting information", "SI"],
    }
    for section, keywords in section_keywords.items():
        if any(kw in text_lower for kw in keywords):
            return section
    return "General"

3. 回答レター生成

python
RESPONSE_TEMPLATES = {
    "accept": {
        "prefix": "We thank the reviewer for this insightful comment.",
        "action": "As suggested, we have revised the manuscript as follows:",
        "location": "Please see the revised text in {section}, {page_info}.",
    },
    "partially_accept": {
        "prefix": "We appreciate this constructive suggestion.",
        "action": "We have partially addressed this point as follows:",
        "location": "The relevant changes can be found in {section}, {page_info}.",
    },
    "rebut": {
        "prefix": "We respectfully appreciate the reviewer raising this point.",
        "action": "After careful consideration, we believe that:",
        "location": "We have added a clarification in {section}, {page_info}.",
    },
    "new_data": {
        "prefix": "We thank the reviewer for suggesting this additional analysis.",
        "action": "We have performed the requested analysis, and the results show:",
        "location": "The new data are presented in {section} ({figure_info}).",
    },
}


def generate_response_letter(parsed_comments, responses, round_number=1):
    """
    ポイント・バイ・ポイントの回答レターを生成する。

    Args:
        parsed_comments: dict — parse_decision_letter() の結果
        responses: list[dict] — 各コメントへの対応方針
            [{"reviewer": "Reviewer 1", "comment_num": 1,
              "strategy": "accept" | "partially_accept" | "rebut" | "new_data",
              "response_text": "...",
              "revised_location": {"section": "...", "page": "...", "lines": "..."},
              "new_figure": None | "Figure S3"
            }]
        round_number: int — 査読ラウンド(1st, 2nd, ...)

    Returns:
        str: 回答レター全文(Markdown)
    """
    round_label = _ordinal(round_number)

    letter = [
        f"# Response to Reviewers ({round_label} Revision)",
        "",
        "We sincerely thank the editor and reviewers for their constructive "
        "comments and suggestions. We have carefully addressed all the points "
        "raised in the review. Below, we provide our point-by-point responses.",
        "",
        "**Notation:**",
        "- Reviewer comments are shown in **bold italic**.",
        "- Our responses are in regular text.",
        "- Revised text in the manuscript is shown in blue.",
        "",
        "---",
        "",
    ]

    # エディタコメント
    if parsed_comments.get("editor_comments"):
        letter.extend([
            "## Editor Comments",
            "",
            f"***{parsed_comments['editor_comments']}***",
            "",
            "**Response:** We thank the editor for handling our manuscript and "
            "providing these comments. We have addressed all reviewer concerns "
            "as detailed below.",
            "",
            "---",
            "",
        ])

    # 査読者ごとの回答
    for reviewer in parsed_comments["reviewers"]:
        letter.extend([
            f"## {reviewer['id']}",
            "",
        ])

        reviewer_responses = [
            r for r in responses
            if r["reviewer"] == reviewer["id"]
        ]

        for comment in reviewer["comments"]:
            matching = [
                r for r in reviewer_responses
                if r["comment_num"] == comment["number"]
            ]

            letter.extend([
                f"### Comment {comment['number']} [{comment['severity'].upper()}]",
                "",
                f"***{comment['text']}***",
                "",
            ])

            if matching:
                resp = matching[0]
                tmpl = RESPONSE_TEMPLATES.get(resp["strategy"], RESPONSE_TEMPLATES["accept"])

                letter.append(f"**Response:** {tmpl['prefix']}")
                letter.append("")
                letter.append(resp["response_text"])
                letter.append("")

                loc = resp.get("revised_location", {})
                if loc:
                    location_text = tmpl["location"].format(
                        section=loc.get("section", "[Section]"),
                        page_info=f"page {loc.get('page', 'X')}, lines {loc.get('lines', 'X-Y')}",
                        figure_info=resp.get("new_figure", ""),
                    )
                    letter.append(location_text)
                    letter.append("")
            else:
                letter.extend([
                    "**Response:** [回答を記入してください]",
                    "",
                ])

            letter.extend(["---", ""])

    return "\n".join(letter)


def _ordinal(n):
    """数値を序数表現に変換する。"""
    suffixes = {1: "1st", 2: "2nd", 3: "3rd"}
    return suffixes.get(n, f"{n}th")

4. 改訂箇所マッピング

python
def generate_response_mapping(parsed_comments, responses, filepath=None):
    """
    査読コメントと改訂箇所の対応表(トレーサビリティマトリクス)を生成する。

    Args:
        parsed_comments: dict
        responses: list[dict]
        filepath: Path

    Returns:
        dict: マッピングデータ
    """
    if filepath is None:
        filepath = BASE_DIR / "manuscript" / "response_mapping.json"
    filepath.parent.mkdir(parents=True, exist_ok=True)

    mapping = {
        "decision": parsed_comments["editor_decision"],
        "total_comments": sum(
            len(r["comments"]) for r in parsed_comments["reviewers"]
        ),
        "summary": {
            "major": 0, "minor": 0, "editorial": 0,
            "accepted": 0, "partially_accepted": 0,
            "rebutted": 0, "new_data_added": 0,
        },
        "reviewers": [],
    }

    for reviewer in parsed_comments["reviewers"]:
        reviewer_map = {
            "id": reviewer["id"],
            "comments": [],
        }

        for comment in reviewer["comments"]:
            mapping["summary"][comment["severity"]] += 1

            matched = [
                r for r in responses
                if r["reviewer"] == reviewer["id"]
                and r["comment_num"] == comment["number"]
            ]

            strategy = matched[0]["strategy"] if matched else "pending"
            location = matched[0].get("revised_location", {}) if matched else {}

            if strategy == "accept":
                mapping["summary"]["accepted"] += 1
            elif strategy == "partially_accept":
                mapping["summary"]["partially_accepted"] += 1
            elif strategy == "rebut":
                mapping["summary"]["rebutted"] += 1
            elif strategy == "new_data":
                mapping["summary"]["new_data_added"] += 1

            reviewer_map["comments"].append({
                "number": comment["number"],
                "severity": comment["severity"],
                "strategy": strategy,
                "section": comment["related_section"],
                "revised_location": location,
                "requires_new_data": comment["requires_new_data"],
            })

        mapping["reviewers"].append(reviewer_map)

    with open(filepath, "w", encoding="utf-8") as f:
        json.dump(mapping, f, indent=2, ensure_ascii=False)

    print(f"  → マッピングファイルを保存: {filepath}")
    return mapping

5. 改訂版カバーレター

python
def generate_revised_cover_letter(parsed_comments, mapping,
                                    journal_name="", manuscript_id="",
                                    filepath=None):
    """
    改訂版投稿時のカバーレターを生成する。

    Args:
        parsed_comments: dict
        mapping: dict — generate_response_mapping() の結果
        journal_name: str
        manuscript_id: str
    """
    if filepath is None:
        filepath = BASE_DIR / "manuscript" / "cover_letter_revised.md"
    filepath.parent.mkdir(parents=True, exist_ok=True)

    s = mapping["summary"]
    total = s["major"] + s["minor"] + s["editorial"]

    letter = f"""# Cover Letter (Revised Manuscript)

**Date:** [日付]
**Manuscript ID:** {manuscript_id or "[ID]"}
**Journal:** {journal_name or "[ジャーナル名]"}

Dear Editor,

Thank you for the opportunity to revise our manuscript entitled "[タイトル]"
(Manuscript ID: {manuscript_id or "[ID]"}).

We are grateful to the reviewers for their constructive comments, which have
significantly improved the quality of our manuscript. We have carefully
addressed all {total} comments raised by the reviewers.

**Summary of revisions:**

- **Major comments:** {s["major"]} (all addressed)
- **Minor comments:** {s["minor"]} (all addressed)
- **Editorial comments:** {s["editorial"]} (all corrected)

**Key changes in the revised manuscript:**

1. [主要な変更点 1 — コメント番号への参照]
2. [主要な変更点 2]
3. [主要な変更点 3]

All changes in the revised manuscript are highlighted in blue for easy
identification. A detailed point-by-point response to each reviewer comment
is provided in the accompanying "Response to Reviewers" document.

We believe that the revised manuscript addresses all concerns raised by the
reviewers and is now suitable for publication in {journal_name or "[ジャーナル名]"}.

Sincerely,

[著者名]
"""

    with open(filepath, "w", encoding="utf-8") as f:
        f.write(letter)

    print(f"  → カバーレター(改訂版)を保存: {filepath}")
    return filepath

6. パイプライン統合

python
def run_review_response_pipeline(decision_letter_path, responses=None,
                                   round_number=1, journal_name="",
                                   manuscript_id=""):
    """
    査読対応パイプラインを実行する。

    Args:
        decision_letter_path: Path — Decision Letter のファイルパス
        responses: list[dict] — 各コメントへの対応(None の場合はテンプレート出力)
        round_number: int — 査読ラウンド
        journal_name: str
        manuscript_id: str

    出力ファイル:
        manuscript/response_to_reviewers.md  — ポイント・バイ・ポイント回答
        manuscript/response_mapping.json     — コメント-改訂マッピング
        manuscript/cover_letter_revised.md   — 改訂版カバーレター
    """
    print("=" * 60)
    print(f"Peer Review Response Pipeline (Round {round_number})")
    print("=" * 60)

    # Phase 1: コメント構造化
    print("\n[Phase 1] Decision Letter を解析中...")
    with open(decision_letter_path, "r", encoding="utf-8") as f:
        decision_text = f.read()

    parsed = parse_decision_letter(decision_text)
    total = sum(len(r["comments"]) for r in parsed["reviewers"])
    print(f"  → エディタ判定: {parsed['editor_decision']}")
    print(f"  → 査読者数: {len(parsed['reviewers'])}")
    print(f"  → 総コメント数: {total}")

    for reviewer in parsed["reviewers"]:
        severity_counts = {}
        for c in reviewer["comments"]:
            severity_counts[c["severity"]] = severity_counts.get(c["severity"], 0) + 1
        print(f"  → {reviewer['id']}: {severity_counts}")

    # Phase 2-3: 回答レター生成
    print("\n[Phase 2-3] 回答レターを生成中...")
    if responses is None:
        responses = _generate_template_responses(parsed)

    letter = generate_response_letter(parsed, responses, round_number)
    letter_path = BASE_DIR / "manuscript" / "response_to_reviewers.md"
    letter_path.parent.mkdir(parents=True, exist_ok=True)
    with open(letter_path, "w", encoding="utf-8") as f:
        f.write(letter)
    print(f"  → 回答レターを保存: {letter_path}")

    # Phase 4: マッピング生成
    print("\n[Phase 4] コメント-改訂マッピングを生成中...")
    mapping = generate_response_mapping(parsed, responses)

    # Phase 5: カバーレター生成
    print("\n[Phase 5] 改訂版カバーレターを生成中...")
    generate_revised_cover_letter(parsed, mapping, journal_name, manuscript_id)

    print("\n" + "=" * 60)
    print("査読対応完了!")
    print("=" * 60)

    return parsed, mapping


def _generate_template_responses(parsed):
    """未入力の回答テンプレートを生成する。"""
    responses = []
    for reviewer in parsed["reviewers"]:
        for comment in reviewer["comments"]:
            responses.append({
                "reviewer": reviewer["id"],
                "comment_num": comment["number"],
                "strategy": "accept",
                "response_text": "[ここに回答を記入してください]",
                "revised_location": {
                    "section": comment["related_section"],
                    "page": "X",
                    "lines": "X-Y",
                },
            })
    return responses

References

Output Files

ファイル形式生成タイミング
manuscript/response_to_reviewers.md回答レター(Markdown)パイプライン完了時
manuscript/response_mapping.jsonコメント-改訂マッピングパイプライン完了時
manuscript/cover_letter_revised.md改訂版カバーレターパイプライン完了時

コメント分類基準

分類説明対応方針
Major結論に影響する根本的指摘追加実験/解析・大幅改訂
Minor改善すべき点(結論には影響なし)修正・追記・説明追加
Editorial誤字・体裁・書式即座に修正

対応戦略

戦略使用場面回答トーン
Accept正当な指摘→修正感謝 + 修正内容
Partially Accept一部は正当、一部は異なる感謝 + 一部修正 + 説明
Rebutエビデンスに基づき反論丁寧な反論 + 根拠
New Data追加実験/解析が必要感謝 + 新データ提示

参照スキル

スキル連携
scientific-academic-writingCover Letter テンプレート・Response to Reviewers 雛形
scientific-critical-reviewセルフレビュー → 査読前の事前対策
scientific-revision-tracker改訂箇所の追跡・diff 生成
scientific-paper-quality改訂後の品質メトリクス再評価
scientific-citation-checker査読で追加を求められた引用の検証