"""Normalize and compile DeerFlow-owned Wiki generation guidance. The values accepted by the public API are raw user configuration. They are kept separately from the effective strings sent to WeKnora so editing a focus or directory constraint never appends a second copy of an older compiled block. """ from __future__ import annotations from dataclasses import dataclass WEKNORA_CUSTOM_INSTRUCTION_MAX = 4000 WIKI_FOCUS_INSTRUCTION_MAX = 2000 WIKI_CONFIG_SCHEMA_VERSION = 3 @dataclass(frozen=True, slots=True) class CompiledWikiInstructions: content_instructions: str extraction_instructions: str directory_prompt_coverage: str directory_prompt_covered_nodes: int directory_prompt_total_nodes: int def normalize_focus_instructions(value: str | None) -> str: """Return the canonical raw focus text used for persistence and hashing.""" if value is None: return "" normalized = str(value).replace("\r\n", "\n").replace("\r", "\n").strip() if len(normalized) > WIKI_FOCUS_INSTRUCTION_MAX: raise ValueError( f"重点要求不能超过 {WIKI_FOCUS_INSTRUCTION_MAX} 个字符" ) return normalized def _join_blocks(*blocks: str) -> str: return "\n\n".join(block.strip() for block in blocks if block.strip()) def _focus_content_block(focus: str) -> str: if not focus: return "" return _join_blocks( f"【用户重点要求】\n{focus}", ( "这是生成范围的硬约束:只生成与上述重点直接相关且有来源支撑的页面;" "不要为背景材料中与重点无关的人物、机构、地点或主题单独生成页面。" "不得为了满足重点要求而编造信息。" ), ) def _focus_extraction_block(focus: str) -> str: if not focus: return "" return _join_blocks( f"【用户重点要求】\n{focus}", ( "这是抽取范围的硬约束:只抽取与上述重点直接相关、可追溯到来源的" "实体、事件、时间线、指标和关系;与重点无关的内容必须跳过。" ), ) def _directory_lines(directory_paths: list[list[str]]) -> list[str]: return [f"{' ' * (len(path) - 1)}- {path[-1]}" for path in directory_paths] def compile_wiki_instructions( *, content_instructions: str | None, extraction_instructions: str | None, focus_instructions: str | None, directory_paths: list[list[str]], max_length: int = WEKNORA_CUSTOM_INSTRUCTION_MAX, ) -> CompiledWikiInstructions: """Compile raw inputs into the two instruction strings supported upstream. Raw free text and focus guidance are never truncated. Directory nodes are appended in deterministic parent-before-child order until the remaining content budget is exhausted. They are examples for the generator rather than a post-generation allowlist: WeKnora may adapt the hierarchy when the source material needs a clearer organization. """ raw_content = str(content_instructions or "").strip() raw_extraction = str(extraction_instructions or "").strip() focus = normalize_focus_instructions(focus_instructions) content_base = _join_blocks(raw_content, _focus_content_block(focus)) extraction = _join_blocks(raw_extraction, _focus_extraction_block(focus)) if len(content_base) > max_length: raise ValueError( f"内容生成要求与重点要求合并后不能超过 {max_length} 个字符" ) if len(extraction) > max_length: raise ValueError( f"信息抽取要求与重点要求合并后不能超过 {max_length} 个字符" ) total_nodes = len(directory_paths) covered_nodes = 0 content = content_base if total_nodes: header = "【参考目录层级】" footer = ( "上述目录仅用于帮助理解期望的组织方式,不是必须逐项套用的目录模板,也不是目录" "白名单。请结合原始资料自行合并、拆分、改名或补充目录;内容结构与参考目录冲突时," "以资料实际内容和清晰表达为准。目录只用于组织,不得作为事实来源。" ) accepted: list[str] = [] for line in _directory_lines(directory_paths): candidate_block = "\n".join([header, *accepted, line, "", footer]) candidate = _join_blocks(content_base, candidate_block) if len(candidate) > max_length: break accepted.append(line) if accepted: content = _join_blocks( content_base, "\n".join([header, *accepted, "", footer]), ) covered_nodes = len(accepted) coverage = ( "none" if total_nodes == 0 or covered_nodes == 0 else "full" if covered_nodes == total_nodes else "partial" ) return CompiledWikiInstructions( content_instructions=content, extraction_instructions=extraction, directory_prompt_coverage=coverage, directory_prompt_covered_nodes=covered_nodes, directory_prompt_total_nodes=total_nodes, ) __all__ = [ "CompiledWikiInstructions", "WEKNORA_CUSTOM_INSTRUCTION_MAX", "WIKI_CONFIG_SCHEMA_VERSION", "WIKI_FOCUS_INSTRUCTION_MAX", "compile_wiki_instructions", "normalize_focus_instructions", ]