"""cheap_genre_route.py — brief → genre 确定性关键词路由(生产 create 路最小品类接线)。 背景(M3 cutover plan「放行检查单增补 2026-07-04」):live Service 创建路 cheap_service_driver 此前硬编码 scaffold_template=None,不选品类骨架、不透传 genre——W-GENRE 四件套(品类 skill/骨架/few-shot/评分尺)在 生产 create 路不通电,而 S5 复验(bake_off)是显式传品类的,复验口径与放量后生产口径不同轨。本模块补最小 路由:纯词表匹配、零 LLM、确定性(同 brief 恒同结果),词表映射到既有 GENRE_BY_TEMPLATE 的五类模板; 无命中回退 None(走通用 _template,与旧行为一致——路由只加不减,失配面=原行为)。 规则表数据化:_GENRE_RULES 是唯一规则源(顺序即先验序),改词表/调先验只动这一张表,不动逻辑。 """ from typing import Optional, Tuple # 品类路由规则表(唯一规则源;列表顺序 = 先验序)。 # 先验序依据:按【词面特异性】降序——特异词品类在前(非遗/TRPG 的词几乎不出现在别的品类 brief 里, # 误伤率最低),泛词品类垫后(「剧情/故事」在各品类 brief 里都常见,最易过泛匹配)。多类词共现时取 # 序前者:如「经营一家店铺,顾客的故事各不相同」同时命中经营与剧情 → 判经营(特异性高者定品类, # 泛词只是修饰)。每条:(genre 键, scaffold 模板名, 关键词元组)——genre/模板对齐 cheap_verify.GENRE_BY_TEMPLATE。 _GENRE_RULES: Tuple[Tuple[str, str, Tuple[str, ...]], ...] = ( # ① 非遗(heritage):非遗工艺词特异性最高,与其他品类零重叠。 ("heritage", "_template-feiyi", ( "非遗", "剪纸", "皮影", "刺绣", "年画", "扎染", "泥塑", "糖画", "风筝制作", "传统工艺", "传统手艺", "民俗", "戏曲", "京剧", "昆曲", "陶艺", "木雕", "漆器", )), # ② TRPG:跑团/骰子/地下城词特异性高。 ("trpg", "_template-trpg", ( "trpg", "跑团", "骰子", "地下城", "龙与地下城", "dnd", "角色扮演冒险", "冒险者小队", "桌面角色扮演", "属性检定", "掷骰", )), # ③ 经营(sim-business):开店/客人/生意词较特异(「服务」单字面泛,组合词收窄)。 ("sim-business", "_template-shop", ( "经营", "开店", "店铺", "商店", "餐厅", "饭店", "奶茶店", "咖啡店", "咖啡馆", "面馆", "小卖部", "杂货店", "顾客", "客人", "接客", "点单", "上菜", "生意", "收银", "摆摊", )), # ④ 解谜(puzzle):谜题/推理词。 ("puzzle", "_template-puzzle", ( "解谜", "谜题", "谜语", "推理", "密室", "华容道", "数独", "拼图", "解密", "机关", "找不同", "找茬", )), # ⑤ 剧情(narrative):故事/分支词最泛,垫底(仅当没有更特异品类命中时才路由到剧情)。 ("narrative", "_template-story", ( "剧情", "故事", "文字冒险", "互动小说", "视觉小说", "对话选择", "分支", "多结局", "章节", "叙事", )), ) def route_genre(brief: Optional[str]) -> Tuple[Optional[str], Optional[str]]: """把用户 brief 确定性路由到 (scaffold_template, genre);无命中 → (None, None) 走通用模板(原行为)。 匹配口径:大小写不敏感的子串包含(brief 是自然语言短句,词表词都是低歧义中文词/固定拼写,子串够用、 不引分词依赖);按 _GENRE_RULES 先验序扫,首个命中即定(多类词共现取先验序前者)。空/非字符串 brief → 无命中回退。纯函数、零 IO、零 LLM——同 brief 恒同结果(生产路由必须可复验)。 Returns: (scaffold_template, genre):如 ("_template-shop", "sim-business");无命中 (None, None)。 """ if not brief or not isinstance(brief, str): return None, None text = brief.lower() for genre, template, keywords in _GENRE_RULES: for kw in keywords: if kw.lower() in text: return template, genre return None, None