Ctrl K
ComunidadGuías…

scripts/ingest_reference_user_corpus.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Parse reference/用户语料/*.md → reference_utterance_catalog.yaml (CI-sized)."""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any

import yaml

ROOT = Path(__file__).resolve().parents[1]
REF_DIR = ROOT / "reference" / "用户语料"
OUT_PATH = (
    ROOT
    / "tests"
    / "corpus"
    / "suites"
    / "wechat_real"
    / "lw_real"
    / "reference_utterance_catalog.yaml"
)
EXISTING_PATH = ROOT / "tests" / "corpus" / "suites" / "wechat_real" / "lw_real" / "utterance_catalog.yaml"

_SECTION_CAT = {
    "A.": "project_session",
    "B.": "read_only",
    "C.": "delegate_file",
    "D.": "detail_report",
    "E.": "memory_ops",
    "F.": "workflow_runtime",
    "G.": "safety_boundary",
    "H.": "intro_capability",
}


def _is_doc_noise(user: str) -> bool:
    """Skip markdown doc bullets from 4.md/5.md (not executable utterances)."""
    if not user or len(user) < 2:
        return True
    if "**" in user or "拼写错误" in user:
        return True
    if user.strip() in ("切换", "/new", "/new`"):
        return True
    if " / " in user and ("“" in user or "**" in user):
        return True
    if user.startswith("**") or user.endswith("**:"):
        return True
    if user.count("“") >= 2 and " / " in user:
        return True
    if user.startswith("“") and "(" in user:
        return True
    return False


def _norm_user(text: str) -> str:
    t = (text or "").strip()
    t = re.sub(r"<br\s*/?>", " ", t, flags=re.I)
    t = re.sub(r"\s+", " ", t)
    return t.strip("「」\"' `")


def _existing_users() -> set[str]:
    if not EXISTING_PATH.is_file():
        return set()
    data = yaml.safe_load(EXISTING_PATH.read_text(encoding="utf-8"))
    return {
        _norm_user(str(row.get("user") or "")).lower()
        for row in (data.get("utterance_catalog") or [])
        if row.get("user")
    }


def _infer_kind(user: str, category: str) -> str:
    u = user.strip()
    if u.startswith("/"):
        return "detail" if u.startswith("/详细") else "command"
    if any(x in u for x in ("详细", "完整报告", "详细信息")) and "删除" not in u[:6]:
        if u in ("详细", "看一下详细", "查看详细", "完整报告") or u.endswith("详细信息"):
            return "detail"
    if any(k in u for k in ("切换", "切到", "切回", "换项目")) and "文件" not in u:
        if "继续" in u or "刚才" in u:
            return "llm"
        return "command"
    if any(k in u for k in ("在哪个项目", "什么项目", "哪个项目", "当前项目")):
        return "command"
    return "llm"


def _infer_fixture(category: str, user: str) -> str:
    u = user
    if "演示" in u:
        return "dual"
    if "novel-factory" in u or "workflow_state" in u or "小说工厂" in u:
        return "lingwen_workflow"
    if category in ("project_session", "general") and ("切换" in u or "项目" in u):
        return "dual"
    return "lingwen"


def _infer_script(kind: str, category: str, user: str) -> str | None:
    if kind in ("command", "detail"):
        return None
    u = user.lower()
    if any(x in u for x in ("/etc/passwd", "api key", "apikey", "删库", "rm -rf", "密钥")):
        return "refuse_unsafe"
    if category == "safety_boundary":
        return "refuse_unsafe"
    if "报错" in user or "出错" in user:
        return "error_guide"
    if "记住" in user or "记忆" in user and "待审" not in user:
        return "project_memory_answer"
    if "刚才" in user and ("聊" in user or "说" in user):
        return "deny_prior_details"
    if "继续" in user:
        return "continue_delete_hello"
    if "干什么" in user or "你能" in user or "功能" in user:
        return "capabilities"
    if "介绍" in user and "自己" in user:
        return "greeting"
    if "委派" in user or "代理" in user:
        if "删" in user:
            return "generic_delegate_delete"
        if "测试" in user or "pytest" in u:
            return "delegate_pytest"
        if "审核" in user:
            return "delegate_review"
        if "检查" in user or "是否存在" in user:
            return "dev_readonly_smoke"
        return "generic_delegate_write"
    if "workflow_state" in u or "workflow state" in u:
        return "read_workflow_state"
    if "读" in user or "列出" in user or "目录" in user or "readme" in u:
        return "read_readme"
    if "project.yaml" in u or "配置" in user and "看" in user:
        return "read_readme"
    if "方案" in user or "先别写" in user:
        return "plan_only"
    return "generic_ack"


def _infer_expect(kind: str, category: str, user: str) -> dict[str, Any]:
    if kind == "command":
        u = user
        if "/新对话" in u or "新会话" in u or "新对话" in u:
            return {"response_contains_any": ["清空", "上下文", "新对话"]}
        if "切换" in u or "切到" in u or "切回" in u:
            return {"response_contains_any": ["切换", "项目"]}
        if "/状态" in u or u == "状态" or "状态" in u[:4]:
            return {"response_contains_any": ["Butler", "当前项目", "项目"]}
        if "/诊断" in u or "/health" in u:
            return {"response_contains": ["Butler 诊断"]}
        if "记忆待审" in u:
            return {"response_contains_any": ["待审", "记忆", "Pending", "没有待批准"]}
        if "记忆图谱" in u:
            return {"response_contains_any": ["记忆", "图谱", "三元组"]}
        if "/定时" in u:
            return {"response_contains_any": ["定时任务", "runtime", "jobs.yaml", "任务"]}
        if "/工作流" in u:
            return {"response_contains_any": ["工作流", "novel-factory", "用法"]}
        if "/项目" in u:
            return {"response_contains_any": ["项目", "灵文", "演示"]}
        if "项目" in u:
            return {"response_contains_any": ["灵文", "当前项目", "项目"]}
    if kind == "detail":
        return {
            "no_llm": True,
            "setup": "cached_report_delete",
            "response_contains_any": ["【本报告任务】", "暂无", "变更"],
        }
    if user.strip().startswith("/运行") or user.strip().startswith("/run-job"):
        return {
            "response_contains_any": ["运行", "任务", "job", "定时", "执行", "未找到", "不存在", "Butler"],
        }
    script = _infer_script(kind, category, user) or ""
    if script == "read_workflow_state":
        return {"response_contains_any": ["phase", "step", "workflow", "draft", "outline", "状态"]}
    if script == "error_guide":
        return {"response_contains_any": ["报错", "诊断", "栈", "/诊断", "错误", "贴"]}
    if script == "dev_readonly_smoke":
        return {"response_contains_any": ["代理", "文件", "存在", "检查", "开发", "docs"]}
    if script == "plan_only":
        return {"response_contains_any": ["方案", "先", "确认", "需求", "委派", "列"]}
    if "refuse" in script:
        return {"response_contains_any": ["拒绝", "不能", "无法", "不允许", "安全", "路径"]}
    if "delegate" in script:
        return {
            "response_contains_any": ["代理", "已完成", "委派", "删除", "详细", "开发"],
        }
    if "deny_prior" in script:
        return {
            "response_excludes": ["wechat-smoke"],
            "response_contains_any": ["清空", "无法", "复述", "新对话"],
        }
    if "read" in script:
        return {"response_contains_any": ["README", "docs", "文件", "目录", "read"]}
    return {"response_contains_any": ["好的", "完成", "代理", "项目", "灵文", "莎丽", "厂长"]}


def _build_entry(
    *,
    ref_id: str,
    user: str,
    category: str,
    source: str,
    variants: list[str] | None = None,
) -> dict[str, Any]:
    kind = _infer_kind(user, category)
    expect = dict(_infer_expect(kind, category, user))
    setup = expect.pop("setup", None)
    entry: dict[str, Any] = {
        "id": ref_id,
        "user": user,
        "category": category,
        "fixture": _infer_fixture(category, user),
        "kind": kind,
        "source_file": source,
        "tier": "reference",
        "quality": "smoke",
        "runner": "reference_smoke",
    }
    script = _infer_script(kind, category, user)
    if script:
        entry["script"] = script
    if setup:
        entry["setup"] = setup
    if expect:
        entry["expect"] = expect
    if variants:
        entry["variants"] = variants[:6]
    return entry


def _parse_yaml_blocks(text: str) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for block in re.findall(r"```yaml\s*(.*?)```", text, flags=re.S | re.I):
        try:
            data = yaml.safe_load(block)
        except yaml.YAMLError:
            continue
        if isinstance(data, list):
            for item in data:
                if isinstance(item, dict) and item.get("user"):
                    rows.append(item)
    return rows


def _section_category(line: str) -> str:
    for prefix, cat in _SECTION_CAT.items():
        if prefix in line or line.strip().startswith(prefix.rstrip(".")):
            return cat
    if "项目与会话" in line:
        return "project_session"
    if "只读" in line:
        return "read_only"
    if "委派" in line or "文件" in line:
        return "delegate_file"
    if "报告" in line or "详细" in line:
        return "detail_report"
    if "记忆" in line or "运维" in line:
        return "memory_ops"
    if "工作流" in line or "定时" in line:
        return "workflow_runtime"
    if "异常" in line or "边界" in line:
        return "safety_boundary"
    return "general"


def main() -> None:
    seen = _existing_users()
    entries: list[dict[str, Any]] = []
    seq = 0

    def add(
        user: str,
        category: str,
        source: str,
        *,
        ref_id: str = "",
        variants: list[str] | None = None,
    ) -> None:
        nonlocal seq
        user = _norm_user(user)
        if not user or len(user) < 2 or _is_doc_noise(user):
            return
        if user.lower() in seen:
            return
        seen.add(user.lower())
        seq += 1
        rid = ref_id if ref_id.startswith("REF-") else f"REF-{seq:04d}"
        entries.append(
            _build_entry(
                ref_id=rid,
                user=user,
                category=category,
                source=source,
                variants=variants,
            )
        )

    # 1.md — structured tables + A–G bullets
    text1 = (REF_DIR / "1.md").read_text(encoding="utf-8")
    for m in re.finditer(
        r"\|\s*(REAL-\d{8}-\d{3})\s*\|\s*([^|]+)\|",
        text1,
    ):
        rid = m.group(1).replace("REAL-", "REF-")
        user = _norm_user(m.group(2))
        add(user, "catalog_table", "reference/用户语料/1.md", ref_id=rid)

    section = "general"
    for line in text1.splitlines():
        if line.startswith("## "):
            section = _section_category(line)
        if line.strip().startswith("- "):
            add(line.strip()[2:], section, "reference/用户语料/1.md")

    # 2.md — primary YAML blocks (~110)
    text2 = (REF_DIR / "2.md").read_text(encoding="utf-8")
    for item in _parse_yaml_blocks(text2):
        uid = str(item.get("id") or "").replace("REAL-", "REF-")
        user = _norm_user(str(item.get("user") or ""))
        cat = str(item.get("category") or "yaml_block")
        variants = [
            _norm_user(str(v))
            for v in (item.get("variants") or [])
            if isinstance(v, str) and _norm_user(str(v))
        ]
        add(user, cat, "reference/用户语料/2.md", ref_id=uid, variants=variants or None)

    # 3.md — scenario tables (first column = user utterance)
    text3 = (REF_DIR / "3.md").read_text(encoding="utf-8")
    for line in text3.splitlines():
        if not line.startswith("|") or "---" in line or "用户原话" in line:
            continue
        cols = [c.strip() for c in line.split("|") if c.strip()]
        if len(cols) < 2:
            continue
        user = _norm_user(cols[0])
        if user.startswith("REAL-") or user in ("A", "B", "C", "D", "E", "F", "G"):
            continue
        cat = cols[1] if len(cols) > 1 else "table3"
        add(user, f"scenario_{cat}", "reference/用户语料/3.md")

    # 4.md / 5.md — bullet lists only (scenario ideas, deduped)
    for name in ("4.md", "5.md"):
        text = (REF_DIR / name).read_text(encoding="utf-8")
        section = "general"
        for line in text.splitlines():
            if line.startswith("###"):
                section = _section_category(line)
            if line.startswith("## "):
                section = _section_category(line)
            s = line.strip()
            if s.startswith("- "):
                body = _norm_user(s[2:])
                if 3 <= len(body) <= 120:
                    add(body, section, f"reference/用户语料/{name}")
            if s.startswith("* "):
                body = _norm_user(s[2:])
                if 3 <= len(body) <= 120:
                    add(body, section, f"reference/用户语料/{name}")

    doc = {
        "meta": {
            "version": "2026-05-24-reference-ingest-r2",
            "source_dir": "reference/用户语料",
            "source_files": ["1.md", "2.md", "3.md", "4.md", "5.md"],
            "entry_count": len(entries),
            "ingest_script": "scripts/ingest_reference_user_corpus.py",
        },
        "utterance_catalog": entries,
    }
    OUT_PATH.write_text(
        yaml.safe_dump(doc, allow_unicode=True, sort_keys=False),
        encoding="utf-8",
    )
    print(f"Wrote {len(entries)} entries → {OUT_PATH}")


if __name__ == "__main__":
    main()