import operator from typing import Annotated, List, Literal, Optional, TypedDict # ── 素材 ────────────────────────────────────────────────────────────────────── class Material(TypedDict, total=False): id: str title: str content: str # 截断到 500 字 source: str published_at: str relevance_score: float source_type: str # "internet" | "intranet" | "skill" url: str recUuid: str # skill 类素材附带的元信息(skill_name / container_path / category),其他类型为空 extra_metadata: dict class SkillStep(TypedDict, total=False): """技能检索过程的单步记录,用于前端时间线展示。 type=match_skills:根据用户输入流式分析有没有匹配技能(无 skill_names) type=list_skills:列出可用技能 type=select_skills:LLM 从中选出的相关技能 type=invoke_skill:实际调用某一个技能检索 type=stop_early:达到素材阈值,提前停止后续技能调用 """ type: str message: str skill_names: List[str] skill_name: str matched_count: int reason: str class MaterialPackage(TypedDict, total=False): keywords: List[str] materials: List[Material] summary: str # 是否本次素材来自技能检索(覆盖式替换 web 检索) skill_search_used: bool # 技能检索的步骤记录(前端展示"使用了 X 技能"卡片) skill_search_steps: List[SkillStep] # 内网知识库接口诊断:端点、每个检索词的命中数/耗时/错误、原始响应片段等。 # 用于排查 "检索很快返回 0 条" 时到底是否真正调用了内网接口。 intranet_debug: dict # ── 大纲 ────────────────────────────────────────────────────────────────────── # 三级结构:章节(##) → 小节(###) → 子要点(bullet -)。 # Outline.sections[] = Level 1 章节(渲染为 ## 由系统注入) # OutlineSection.key_points[] = Level 2 小节(渲染为 ### 标题) # KeyPoint.sub_points[] = Level 3 子要点(bullet "- ...",非标题) # # 历史上 key_points 曾是 List[str](2 级):每个 str 既是小节标题又是渲染单位。 # 升级到 3 级后,key_points 变成 List[KeyPoint],每个 KeyPoint 自带 sub_points # 列表;旧 session 里的 string 项由 writer_outline._parse_outline 自动 lift 成 # {point_title: <原 string>, sub_points: []},无需迁移数据。 class KeyPoint(TypedDict): point_title: str # 小节标题(渲染为 ### 二级小标题) sub_points: List[str] # 子要点(可为空;非空时在小节正文里以 bullet 列表呈现) class OutlineSection(TypedDict): section_title: str key_points: List[KeyPoint] # 注意:旧版是 List[str],解析层做了兼容 material_ids: List[str] class Outline(TypedDict): title: str sections: List[OutlineSection] # ── 草稿 ────────────────────────────────────────────────────────────────────── class DraftArticle(TypedDict): title: str outline: Outline full_markdown: str word_count: int revision_count: int # ── 样文仿写 ────────────────────────────────────────────────────────────────── class SampleStyleProfile(TypedDict, total=False): summary: str article_type_guess: str tone: str structure: List[str] language_features: List[str] sentence_style: str opening_style: str ending_style: str imitation_guidance: List[str] avoid_copying: List[str] sample_outline: List[str] # ── 编辑审核 ────────────────────────────────────────────────────────────────── class ReviewIssue(TypedDict): category: str # "fact" | "logic" | "language" severity: str # "major" | "minor" location: str issue: str suggestion: str class ReviewResult(TypedDict): verdict: str # "pass" | "reject" fact_score: int logic_score: int language_score: int overall_score: int pass_threshold: int revision_notes: List[ReviewIssue] # ── 用户干预 ────────────────────────────────────────────────────────────────── class UserInterventionPayload(TypedDict, total=False): action: str approved_material_ids: List[str] extra_keywords: List[str] # PAUSE-1 素材确认 / enable_skill_search 共用:用户在单输入框里填的自然语言 # (可能是要使用的技能名、想检索的来源、或者一段补充检索意图)。 user_query: str # PAUSE-1 素材确认卡切换的检索方式:general / knowledge_base / notebook。 material_source: str edited_outline: Optional[Outline] outline_feedback: str user_revision_notes: str override_verdict: bool # ── 主状态 ──────────────────────────────────────────────────────────────────── WritingStatus = Literal[ "analyzing_sample", "researching", "awaiting_material_confirm", "writing_outline", "awaiting_outline_confirm", "writing_draft", "awaiting_section_help", "awaiting_draft_confirm", "reviewing", "awaiting_review_confirm", "revising", "done", "error", ] class BlockedSection(TypedDict): """严格模式下素材不足、无法写作的章节。""" section_title: str reason: str class AIWritingState(TypedDict): # 写作配置 user_intent: str # 用户在写作表单粘贴的「已有大纲」原文(可选)。非空时 writer_outline 跳过 LLM 规划, # 直接解析这份大纲并按用户结构生成,做到「完全按我的大纲写」。留空走正常自动规划。 user_outline_raw: str # True when setup chat has already produced structured intent fields. skip_intent_parser: bool # 写作入口模式:normal=普通写作 / imitate=样文仿写。仿写模式下先分析 sample_text, # 得到 sample_style_profile,再将其注入大纲与草稿提示词。 writing_mode_type: str sample_text: str sample_filename: str sample_style_profile: Optional[SampleStyleProfile] imitate_strength: str preserve_sample_structure: bool article_type: str # 当 article_type 是「已有类型库里没有」的新类型时,配置助手识别出的一句话特征说明。 # 仅本次写作临时使用(不入库):在大纲生成时作为 hint 附在类型后,影响全文结构与侧重。 article_type_detail: str target_audience: str word_count_target: int # 素材检索关键词数量(用户指定,1~10,默认 6) keyword_count: int # 写作模式:strict=严格依据素材 / loose=可结合通用知识(默认 loose) writing_mode: str # 是否在成稿里加参考文献:正文引用素材处加角标 [n],文末按出现顺序生成「参考文献」列表(像论文)。 add_references: bool # 检索类型:general=通用检索(按素材收集专家 agent 配置的技能编排,默认)/ # knowledge_base=知识库检索(直连 ai_writing.intranet_search_url 内网 ES 接口, # 不走技能编排——弱模型识别不了技能时用这条)/ notebook=我的空间(仅笔记检索)。 # 旧值 skill 由 researcher 节点按 general 兼容处理。 material_source: str # 跳过素材收集:用户在初始表单勾选「不收集素材,直接用通用知识写作」时为 True。 # researcher 节点在**首次**进入(无任何 pause 干预)时短路,跳过整个检索流程直接 # 去大纲规划;强制 loose 写作模式(没有素材可严格依据)。 skip_research: bool # 用户在写作表单选定的主模型。大纲/写作/审核严格使用它(不回退默认)。 # 由 intent_parser 在首次 invocation 从 configurable.model_name 捕获进 state, # 经 checkpoint 持久化 —— interrupt/resume 是新 invocation、其 config 不带原 # model_name,存进 state 才能让 resume 后才运行的节点拿到用户模型。 model_name: str # 笔记来源(可选):传入后 researcher 阶段会额外从笔记检索素材 notebook_search_space_id: Optional[int] notebook_source_ids: Optional[List[int]] # Agent A 输出 material_package: Optional[MaterialPackage] # Agent B 输出 current_outline: Optional[Outline] drafts: Annotated[List[DraftArticle], operator.add] current_draft: Optional[DraftArticle] # Agent C 输出 review_result: Optional[ReviewResult] # 用户干预数据(每个干预点会覆盖) last_user_intervention: Optional[UserInterventionPayload] # 严格模式下素材不足、待用户决策的章节(写作节点产出,求助暂停点消费) blocked_sections: Optional[List[BlockedSection]] # 流程控制 revision_count: int max_revisions: int status: WritingStatus # SSE 进度推送队列(每个节点追加) progress_events: Annotated[List[dict], operator.add]