218 lines
9.7 KiB
Python
218 lines
9.7 KiB
Python
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]
|