diff --git a/ReportEngine/agent.py b/ReportEngine/agent.py index 2dc28a1..997600c 100644 --- a/ReportEngine/agent.py +++ b/ReportEngine/agent.py @@ -296,8 +296,19 @@ class ReportAgent: 4. 将章节装订成Document IR,再交给HTML渲染器生成成品; 5. 可选地将HTML/IR/状态落盘,并向外界回传路径信息。 - Returns: - dict: HTML内容以及保存的文件路径信息 + 参数: + query: 最终要生成的报告主题或提问语句。 + reports: 来自 Query/Media/Insight 等分析引擎的原始输出,允许传入字符串或更复杂的对象。 + forum_logs: 论坛/协同记录,供LLM理解多人讨论上下文。 + custom_template: 用户指定的Markdown模板,如为空则交由模板节点自动挑选。 + save_report: 是否在生成后自动将HTML、IR与状态写入磁盘。 + stream_handler: 可选的流式事件回调,接收阶段标签与payload,用于UI实时展示。 + + 返回: + dict: 包含 `html_content` 以及HTML/IR/状态文件路径的字典;若 `save_report=False` 则仅返回HTML字符串。 + + 异常: + Exception: 任一子节点或渲染阶段失败时抛出,外层调用方负责兜底。 """ start_time = datetime.now() report_id = f"report-{uuid4().hex[:8]}" @@ -538,6 +549,15 @@ class ReportAgent: 优先使用用户指定的模板;否则将查询、三引擎报告与论坛日志 作为上下文交给 TemplateSelectionNode,由 LLM 返回最契合的 模板名称、内容及理由,并自动记录在状态中。 + + 参数: + query: 报告主题,用于提示词聚焦行业/事件。 + reports: 多来源报告原文,帮助LLM判断结构复杂度。 + forum_logs: 对应论坛或协作讨论的文本,用于补充背景。 + custom_template: CLI/前端传入的自定义Markdown模板,非空时直接采用。 + + 返回: + dict: 包含 `template_name`、`template_content` 与 `selection_reason` 的结构化结果,供后续节点消费。 """ logger.info("选择报告模板...") @@ -584,6 +604,12 @@ class ReportAgent: 委托 `parse_template_sections` 将Markdown标题/编号解析为 `TemplateSection` 列表,确保后续章节生成有稳定的章节ID。 当模板格式异常时,会回退到内置的简单骨架避免崩溃。 + + 参数: + template_markdown: 完整的模板Markdown文本。 + + 返回: + list[TemplateSection]: 解析后的章节序列;如解析失败则返回单章兜底结构。 """ sections = parse_template_sections(template_markdown) if sections: @@ -618,6 +644,19 @@ class ReportAgent: 将模板名称、布局设计、主题配色、篇幅规划、论坛日志等 一次性整合为 `generation_context`,后续每章调用 LLM 时 直接复用,确保所有章节共享一致的语调和视觉约束。 + + 参数: + query: 用户查询词。 + reports: 归一化后的 query/media/insight 报告映射。 + forum_logs: 三引擎讨论记录。 + template_result: 模板节点返回的模板元信息。 + layout_design: 文档布局节点产出的标题/目录/主题设计。 + chapter_directives: 字数规划节点返回的章节指令映射。 + word_plan: 篇幅规划原始结果,包含全局字数约束。 + template_overview: 模板切片提炼的章节骨架摘要。 + + 返回: + dict: LLM章节生成所需的全集上下文,包含主题色、布局、约束等键。 """ # 优先使用设计稿定制的主题色,否则退回默认主题 theme_tokens = ( @@ -650,6 +689,12 @@ class ReportAgent: 约定顺序为 Query/Media/Insight,引擎提供的对象可能是 字典或自定义类型,因此统一走 `_stringify` 做容错。 + + 参数: + reports: 任意类型的报告列表,允许缺失或顺序混乱。 + + 返回: + dict: 包含 `query_engine`/`media_engine`/`insight_engine` 三个字符串字段的映射。 """ keys = ["query_engine", "media_engine", "insight_engine"] normalized: Dict[str, str] = {} @@ -664,6 +709,12 @@ class ReportAgent: 当检测到供应商返回的错误包含特定关键词时,允许章节生成 重新尝试,以便绕过偶发的内容审查触发。 + + 参数: + error: LLM客户端抛出的异常对象。 + + 返回: + bool: 若匹配到内容审查关键词则返回True,否则为False。 """ message = str(error) if error else "" if not message: @@ -683,6 +734,12 @@ class ReportAgent: - dict/list 统一序列化为格式化 JSON,便于提示词消费; - 其他类型走 `str()`,None 则返回空串,避免 None 传播。 + + 参数: + value: 任意Python对象。 + + 返回: + str: 适配提示词/日志的字符串表现。 """ if value is None: return "" @@ -700,6 +757,9 @@ class ReportAgent: 构造默认主题变量,供渲染器/LLM共用。 当布局节点未返回专属配色时使用该套色板,保持报告风格统一。 + + 返回: + dict: 包含颜色、字体、间距、布尔开关等渲染参数的主题字典。 """ return { "colors": { @@ -735,6 +795,13 @@ class ReportAgent: 提取模板标题与章节骨架,供设计/篇幅规划统一引用。 同时记录章节ID/slug/order等辅助字段,保证多节点对齐。 + + 参数: + template_markdown: 模板原文,用于解析全局标题。 + sections: `TemplateSection` 列表,作为章节骨架。 + + 返回: + dict: 包含模板标题与章节元数据的概览结构。 """ fallback_title = sections[0].title if sections else "" overview = { @@ -763,6 +830,13 @@ class ReportAgent: 优先返回首个 `#` 语法标题;如果模板首行就是正文,则回退到 第一行非空文本或调用方提供的 fallback。 + + 参数: + template_markdown: 模板原文。 + fallback: 备用标题,当文档缺少显式标题时使用。 + + 返回: + str: 解析到的标题文本。 """ for line in template_markdown.splitlines(): stripped = line.strip() @@ -834,6 +908,14 @@ class ReportAgent: 生成基于查询和时间戳的易读文件名,同时也把运行态的 `ReportState` 写入 JSON,方便下游排障或断点续跑。 + + 参数: + html_content: 渲染后的HTML正文。 + document_ir: Document IR结构化数据。 + report_id: 当前任务ID,用于创建独立文件名。 + + 返回: + dict: 记录HTML/IR/State文件的绝对与相对路径信息。 """ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") query_safe = "".join( @@ -879,6 +961,14 @@ class ReportAgent: `Document IR` 与 HTML 解耦保存,便于调试渲染差异以及 在不重新跑 LLM 的情况下再次渲染或导出其他格式。 + + 参数: + document_ir: 整本报告的IR结构。 + query_safe: 已清洗的查询短语,用于文件命名。 + timestamp: 运行时间戳,保证文件名唯一。 + + 返回: + Path: 指向保存后的IR文件路径。 """ filename = f"report_ir_{query_safe}_{timestamp}.json" ir_path = Path(self.config.DOCUMENT_IR_OUTPUT_DIR) / filename @@ -901,6 +991,12 @@ class ReportAgent: 这些中间件文件(document_layout/word_plan/template_overview) 方便在调试或复盘时快速定位:标题/目录/主题是如何确定的、 字数分配有什么要求,以便后续人工校正。 + + 参数: + run_dir: 章节输出根目录。 + layout_design: 文档布局节点的原始输出。 + word_plan: 篇幅规划节点输出。 + template_overview: 模板概览JSON。 """ artifacts = { "document_layout": layout_design, diff --git a/ReportEngine/core/chapter_storage.py b/ReportEngine/core/chapter_storage.py index 481ed1e..04e1cd8 100644 --- a/ReportEngine/core/chapter_storage.py +++ b/ReportEngine/core/chapter_storage.py @@ -75,6 +75,13 @@ class ChapterStorage: 为本次报告创建独立的章节输出目录与manifest。 同时把全局metadata写入 `manifest.json`,供渲染/调试查询。 + + 参数: + report_id: 任务ID。 + metadata: Report元数据(标题、主题等)。 + + 返回: + Path: 新建的run目录。 """ run_dir = self.base_dir / report_id run_dir.mkdir(parents=True, exist_ok=True) @@ -93,6 +100,13 @@ class ChapterStorage: 创建章节子目录并在manifest中标记为streaming状态。 会生成 `order-slug` 风格的子目录,并提前登记 raw 文件路径。 + + 参数: + run_dir: 会话根目录。 + chapter_meta: 包含 chapterId/title/slug/order 的元数据。 + + 返回: + Path: 章节目录。 """ slug_value = str( chapter_meta.get("slug") or chapter_meta.get("chapterId") or "section" @@ -124,6 +138,15 @@ class ChapterStorage: 章节流式生成完毕后写入最终JSON并更新manifest状态。 若校验失败,错误信息会被写入manifest,供前端展示。 + + 参数: + run_dir: 会话根目录。 + chapter_meta: 章节元信息。 + payload: 校验通过的章节JSON。 + errors: 可选的错误列表,用于标记invalid状态。 + + 返回: + Path: 最终的 `chapter.json` 文件路径。 """ slug_value = str( chapter_meta.get("slug") or chapter_meta.get("chapterId") or "section" @@ -159,6 +182,12 @@ class ChapterStorage: 从指定run目录读取全部chapter.json并按order排序返回。 常用于 DocumentComposer 将多个章节装订成整本IR。 + + 参数: + run_dir: 会话根目录。 + + 返回: + list[dict]: 章节payload列表。 """ payloads: List[Dict[str, object]] = [] for child in sorted(run_dir.iterdir()): @@ -183,6 +212,12 @@ class ChapterStorage: 将流式输出实时写入raw文件。 通过 contextmanager 暴露文件句柄,简化章节节点的写入逻辑。 + + 参数: + chapter_dir: 当前章节目录。 + + 返回: + Generator[TextIO]: 作为上下文管理器使用的文件对象。 """ raw_path = self._raw_stream_path(chapter_dir) raw_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/ReportEngine/core/stitcher.py b/ReportEngine/core/stitcher.py index 90008a0..ef1c062 100644 --- a/ReportEngine/core/stitcher.py +++ b/ReportEngine/core/stitcher.py @@ -36,6 +36,14 @@ class DocumentComposer: 把所有章节按order排序并注入唯一锚点,形成整本IR。 同时合并 metadata/themeTokens/assets,供渲染器直接消费。 + + 参数: + report_id: 本次报告ID。 + metadata: 全局元信息(标题、主题、toc等)。 + chapters: 章节payload列表。 + + 返回: + dict: 满足渲染器需求的Document IR。 """ ordered = sorted(chapters, key=lambda c: c.get("order", 0)) for idx, chapter in enumerate(ordered, start=1): diff --git a/ReportEngine/core/template_parser.py b/ReportEngine/core/template_parser.py index 9f54ae5..f6d47e2 100644 --- a/ReportEngine/core/template_parser.py +++ b/ReportEngine/core/template_parser.py @@ -63,6 +63,12 @@ def parse_template_sections(template_md: str) -> List[TemplateSection]: 返回的每个TemplateSection都携带slug/order/章节号, 方便后续分章调用与锚点生成。解析时会同时兼容 “# 标题”“无符号编号”“列表提纲”等不同写法。 + + 参数: + template_md: 模板Markdown全文。 + + 返回: + list[TemplateSection]: 结构化的章节序列。 """ sections: List[TemplateSection] = [] @@ -113,6 +119,13 @@ def _classify_line(stripped: str, indent: int) -> Optional[dict]: 借助正则判断当前行是章节标题、提纲还是普通列表项, 并衍生 depth/slug/number 等派生信息。 + + 参数: + stripped: 去除前后空格后的原始行。 + indent: 行首空格数量,用于区分层级。 + + 返回: + dict | None: 识别后的元数据;无法识别时返回None。 """ heading_match = heading_pattern.match(stripped) @@ -181,6 +194,12 @@ def _split_number(payload: str) -> dict: 例如 `1.2 市场趋势` 会被拆成 number=1.2、label=市场趋势, 并提供 display 用于回填标题。 + + 参数: + payload: 原始标题字符串。 + + 返回: + dict: 包含 number/title/display。 """ match = number_pattern.match(payload) number = match.group("num") if match else "" @@ -196,7 +215,16 @@ def _split_number(payload: str) -> dict: def _build_slug(number: str, title: str) -> str: - """根据编号/标题生成锚点,优先复用编号,缺失时对标题slug化。""" + """ + 根据编号/标题生成锚点,优先复用编号,缺失时对标题slug化。 + + 参数: + number: 章节编号。 + title: 标题文本。 + + 返回: + str: 形如 `section-1-0` 的slug。 + """ if number: token = number.replace(".", "-") else: @@ -223,6 +251,13 @@ def _ensure_unique_slug(slug: str, used: set) -> str: 若slug重复则自动追加序号,直到在used集合中唯一。 通过 `-2/-3...` 的方式保证相同标题不会产生重复锚点。 + + 参数: + slug: 初始slug。 + used: 已使用集合。 + + 返回: + str: 去重后的slug。 """ if slug not in used: used.add(slug) diff --git a/ReportEngine/flask_interface.py b/ReportEngine/flask_interface.py index 367da3e..08233f6 100644 --- a/ReportEngine/flask_interface.py +++ b/ReportEngine/flask_interface.py @@ -43,6 +43,12 @@ def _register_stream(task_id: str) -> Queue: 为指定任务注册一个事件队列,供SSE监听器消费。 返回的 Queue 会存入 `stream_subscribers`,SSE 生成器将不断读取。 + + 参数: + task_id: 需要监听的任务ID。 + + 返回: + Queue: 线程安全的事件队列。 """ queue = Queue() with stream_lock: @@ -55,6 +61,10 @@ def _unregister_stream(task_id: str, queue: Queue): 安全移除事件队列,避免内存泄漏。 需要在finally中调用,保证异常情况下资源也能释放。 + + 参数: + task_id: 任务ID。 + queue: 之前注册的事件队列。 """ with stream_lock: listeners = stream_subscribers.get(task_id, []) @@ -69,6 +79,10 @@ def _broadcast_event(task_id: str, event: Dict[str, Any]): 将事件推送给所有监听者,失败时做好异常捕获。 采用浅拷贝监听列表,防止并发移除导致遍历异常。 + + 参数: + task_id: 待推送的任务ID。 + event: 结构化事件payload。 """ with stream_lock: listeners = list(stream_subscribers.get(task_id, [])) @@ -84,6 +98,9 @@ def _prune_task_history_locked(): 在task_lock持有期间调用,清理过多的历史任务。 仅保留最近 `MAX_TASK_HISTORY` 个任务,避免长时间运行占用过多内存。 + + 说明: + 该函数假设调用方已获取 `task_lock`,否则存在竞态风险。 """ if len(tasks_registry) <= MAX_TASK_HISTORY: return @@ -98,6 +115,12 @@ def _get_task(task_id: str) -> Optional['ReportTask']: 统一的任务查找方法,优先返回当前任务。 避免重复写锁逻辑,便于多个API共享。 + + 参数: + task_id: 任务ID。 + + 返回: + ReportTask | None: 命中时返回任务实例,否则为None。 """ with task_lock: if current_task and current_task.task_id == task_id: @@ -110,6 +133,12 @@ def _format_sse(event: Dict[str, Any]) -> str: 按SSE协议格式化消息。 输出形如 `id:/event:/data:` 的三段文本,供浏览器端直接消费。 + + 参数: + event: 事件payload,至少包含 id/type。 + + 返回: + str: SSE协议要求的字符串。 """ payload = json.dumps(event, ensure_ascii=False) event_id = event.get('id', 0) @@ -122,6 +151,9 @@ def initialize_report_engine(): 初始化Report Engine。 单例化 ReportAgent,方便 API 启动后直接接收任务。 + + 返回: + bool: 初始化成功返回True,异常时返回False。 """ global report_agent try: @@ -176,6 +208,11 @@ class ReportTask: 更新任务状态并广播事件。 会自动刷新 `updated_at`、错误信息,并触发 `status` 类型的 SSE。 + + 参数: + status: 任务阶段(pending/running/completed/error/cancelled)。 + progress: 可选的进度百分比。 + error_message: 出错时的人类可读说明。 """ self.status = status if progress is not None: @@ -214,7 +251,13 @@ class ReportTask: } def publish_event(self, event_type: str, payload: Dict[str, Any]) -> None: - """将任意事件放入缓存并广播,所有新增逻辑均配套中文说明。""" + """ + 将任意事件放入缓存并广播,所有新增逻辑均配套中文说明。 + + 参数: + event_type: SSE中的event名称。 + payload: 实际业务数据。 + """ timestamp = datetime.utcnow().isoformat() + 'Z' event: Dict[str, Any] = { 'id': 0, @@ -230,7 +273,15 @@ class ReportTask: _broadcast_event(self.task_id, event) def history_since(self, last_event_id: Optional[int]) -> List[Dict[str, Any]]: - """根据Last-Event-ID补发历史事件,确保断线重连无遗漏。""" + """ + 根据Last-Event-ID补发历史事件,确保断线重连无遗漏。 + + 参数: + last_event_id: SSE客户端记录的最后一个事件ID。 + + 返回: + list[dict]: 从 last_event_id 之后的事件列表。 + """ with self._event_lock: if last_event_id is None: return list(self.event_history) @@ -272,6 +323,11 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " 包括:检查输入→加载文档→调用ReportAgent→持久化输出→ 推送阶段性事件。出现错误会自动推送并写状态。 + + 参数: + task: 本次任务对象,内部持有事件队列。 + query: 报告主题。 + custom_template: 可选的自定义模板字符串。 """ global current_task @@ -385,7 +441,12 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " @report_bp.route('/status', methods=['GET']) def get_status(): - """获取Report Engine状态,包括引擎就绪情况与当前任务信息。""" + """ + 获取Report Engine状态,包括引擎就绪情况与当前任务信息。 + + 返回: + Response: JSON结构包含initialized/engines_ready/当前任务等。 + """ try: engines_status = check_engines_ready() @@ -411,6 +472,13 @@ def generate_report(): 开始生成报告。 负责排队、创建后台线程、清空日志并返回SSE地址。 + + 请求体: + query: 报告主题(可选)。 + custom_template: 自定义模板字符串(可选)。 + + 返回: + Response: JSON,包含 task_id 与 SSE stream url。 """ global current_task @@ -498,7 +566,15 @@ def generate_report(): @report_bp.route('/progress/', methods=['GET']) def get_progress(task_id: str): - """获取报告生成进度,若任务被清理则返回一个完成态兜底。""" + """ + 获取报告生成进度,若任务被清理则返回一个完成态兜底。 + + 参数: + task_id: 任务唯一标识。 + + 返回: + Response: JSON包含任务当前状态。 + """ try: task = _get_task(task_id) if not task: @@ -540,6 +616,12 @@ def stream_task(task_id: str): - 自动补发Last-Event-ID之后的历史事件; - 周期性发送心跳以防代理中断; - 任务结束后自动注销监听。 + + 参数: + task_id: 任务唯一标识。 + + 返回: + Response: `text/event-stream` 类型响应。 """ task = _get_task(task_id) if not task: @@ -592,7 +674,15 @@ def stream_task(task_id: str): @report_bp.route('/result/', methods=['GET']) def get_result(task_id: str): - """获取报告生成结果""" + """ + 获取报告生成结果。 + + 参数: + task_id: 任务ID。 + + 返回: + Response: JSON,包含HTML预览与文件路径。 + """ try: task = _get_task(task_id) if not task: @@ -655,7 +745,15 @@ def get_result_json(task_id: str): @report_bp.route('/download/', methods=['GET']) def download_report(task_id: str): - """下载已生成的报告HTML文件""" + """ + 下载已生成的报告HTML文件。 + + 参数: + task_id: 任务ID。 + + 返回: + Response: HTML文件的附件下载响应。 + """ try: task = _get_task(task_id) if not task: @@ -694,7 +792,15 @@ def download_report(task_id: str): @report_bp.route('/cancel/', methods=['POST']) def cancel_task(task_id: str): - """取消报告生成任务""" + """ + 取消报告生成任务。 + + 参数: + task_id: 需要被取消的任务ID。 + + 返回: + Response: JSON,包含取消结果或错误信息。 + """ global current_task try: @@ -735,7 +841,12 @@ def cancel_task(task_id: str): @report_bp.route('/templates', methods=['GET']) def get_templates(): - """获取可用模板列表,便于前端展示可选Markdown骨架。""" + """ + 获取可用模板列表,便于前端展示可选Markdown骨架。 + + 返回: + Response: JSON,列出模板名称/描述/大小。 + """ try: if not report_agent: return jsonify({ @@ -799,7 +910,12 @@ def internal_error(error): def clear_report_log(): - """清空report.log文件,方便新任务只查看本次运行日志。""" + """ + 清空report.log文件,方便新任务只查看本次运行日志。 + + 返回: + None + """ try: log_file = settings.LOG_FILE with open(log_file, 'w', encoding='utf-8') as f: @@ -811,7 +927,12 @@ def clear_report_log(): @report_bp.route('/log', methods=['GET']) def get_report_log(): - """获取report.log内容,并按行去除空白返回。""" + """ + 获取report.log内容,并按行去除空白返回。 + + 返回: + Response: JSON,包含最新日志行数组。 + """ try: log_file = settings.LOG_FILE @@ -842,7 +963,12 @@ def get_report_log(): @report_bp.route('/log/clear', methods=['POST']) def clear_log(): - """手动清空日志,提供REST入口供前端一键重置。""" + """ + 手动清空日志,提供REST入口供前端一键重置。 + + 返回: + Response: JSON,标记是否清理成功。 + """ try: clear_report_log() return jsonify({ diff --git a/ReportEngine/llms/base.py b/ReportEngine/llms/base.py index 7823faf..733a8c2 100644 --- a/ReportEngine/llms/base.py +++ b/ReportEngine/llms/base.py @@ -101,15 +101,15 @@ class LLMClient: def stream_invoke(self, system_prompt: str, user_prompt: str, **kwargs) -> Generator[str, None, None]: """ - 流式调用LLM,逐步返回响应内容 + 流式调用LLM,逐步返回响应内容。 - Args: - system_prompt: 系统提示词 - user_prompt: 用户提示词 - **kwargs: 额外参数(temperature, top_p等) + 参数: + system_prompt: 系统提示词。 + user_prompt: 用户提示词。 + **kwargs: 采样参数(temperature、top_p等)。 - Yields: - 响应文本块(str),调用方可边读边写入磁盘或透传到UI + 产出: + str: 每次yield一段delta文本,方便上层实时渲染。 """ messages = [ {"role": "system", "content": system_prompt}, @@ -143,15 +143,15 @@ class LLMClient: @with_retry(LLM_RETRY_CONFIG) def stream_invoke_to_string(self, system_prompt: str, user_prompt: str, **kwargs) -> str: """ - 流式调用LLM并安全地拼接为完整字符串(避免UTF-8多字节字符截断) + 流式调用LLM并安全地拼接为完整字符串(避免UTF-8多字节字符截断)。 - Args: - system_prompt: 系统提示词 - user_prompt: 用户提示词 - **kwargs: 额外参数(temperature, top_p等) + 参数: + system_prompt: 系统提示词。 + user_prompt: 用户提示词。 + **kwargs: 采样或超时配置。 - Returns: - 完整的响应字符串 + 返回: + str: 将所有delta拼接后的完整响应。 """ # 以字节形式收集所有块 byte_chunks = [] diff --git a/ReportEngine/nodes/chapter_generation_node.py b/ReportEngine/nodes/chapter_generation_node.py index d647c23..52a6fec 100644 --- a/ReportEngine/nodes/chapter_generation_node.py +++ b/ReportEngine/nodes/chapter_generation_node.py @@ -107,7 +107,23 @@ class ChapterGenerationNode(BaseNode): stream_callback: Optional[Callable[[str, Dict[str, Any]], None]] = None, **kwargs, ) -> Dict[str, Any]: - """针对单个章节调用LLM,校验/落盘章节JSON并返回结构化结果""" + """ + 针对单个章节调用LLM,校验/落盘章节JSON并返回结构化结果。 + + 参数: + section: 模板切片生成的章节对象,包含标题/顺序/slug。 + context: Agent构造的共享上下文(主题、篇幅、布局等)。 + run_dir: 章节存盘目录,由 `ChapterStorage.start_session` 返回。 + stream_callback: 可选流式回调,将LLM delta 推送给前端。 + **kwargs: 透传温度、top_p等采样参数。 + + 返回: + dict: 通过IR校验的章节JSON。 + + 异常: + ChapterJsonParseError: 多次尝试后仍无法解析合法JSON。 + ChapterContentError: 正文密度不足或只有标题,需要触发重试。 + """ chapter_meta = { "chapterId": section.chapter_id, "slug": section.slug, @@ -167,7 +183,16 @@ class ChapterGenerationNode(BaseNode): # ====== 内部方法 ====== def _build_payload(self, section: TemplateSection, context: Dict[str, Any]) -> Dict[str, Any]: - """构造LLM输入payload""" + """ + 构造LLM输入payload。 + + 参数: + section: 当前要生成的章节,提供标题/编号/提纲。 + context: 全局上下文字典,包含主题、三引擎报告、篇幅规划等。 + + 返回: + dict: 可以直接序列化进提示词的payload,兼顾章节信息与全局约束。 + """ reports = context.get("reports", {}) # 章节篇幅规划(来自WordBudgetNode),用于指导字数与强调点 chapter_plan_map = context.get("chapter_directives", {}) @@ -233,7 +258,19 @@ class ChapterGenerationNode(BaseNode): section_meta: Optional[Dict[str, Any]] = None, **kwargs, ) -> str: - """流式调用LLM并实时写入raw文件,同时通过回调将delta抛出。""" + """ + 流式调用LLM并实时写入raw文件,同时通过回调将delta抛出。 + + 参数: + user_message: 拼装好的用户提示词。 + chapter_dir: 章节的本地缓存目录,用于存放 stream.raw。 + stream_callback: SSE流式推送的回调函数。 + section_meta: 附带的章节ID/标题,用于回调payload。 + **kwargs: 透传温度、top_p等参数。 + + 返回: + str: 将所有delta拼接后的原始文本。 + """ chunks: List[str] = [] with self.storage.capture_stream(chapter_dir) as stream_fp: stream = self.llm_client.stream_invoke( @@ -254,7 +291,18 @@ class ChapterGenerationNode(BaseNode): return "".join(chunks) def _parse_chapter(self, raw_text: str) -> Dict[str, Any]: - """清洗LLM输出并解析JSON""" + """ + 清洗LLM输出并解析JSON。 + + 参数: + raw_text: LLM原始输出(可能包含```包裹或额外说明)。 + + 返回: + dict: 章节JSON对象,至少包含 chapterId/title/blocks。 + + 异常: + ChapterJsonParseError: 多种修复策略仍无法解析合法JSON。 + """ cleaned = raw_text.strip() if cleaned.startswith("```json"): cleaned = cleaned[7:] @@ -304,7 +352,15 @@ class ChapterGenerationNode(BaseNode): raise ValueError("章节JSON缺少chapter字段") def _repair_llm_json(self, text: str) -> str: - """处理常见的LLM错误(如\":=导致的非法JSON)""" + """ + 处理常见的LLM错误(如":=导致的非法JSON)。 + + 参数: + text: 原始章节JSON文本。 + + 返回: + str: 修复后的文本;若未做改动则返回原内容。 + """ repaired = text mutated = False @@ -482,7 +538,12 @@ class ChapterGenerationNode(BaseNode): return fixed def _sanitize_chapter_blocks(self, chapter: Dict[str, Any]): - """修正常见的结构性错误(例如list.items嵌套过深)""" + """ + 修正常见的结构性错误(例如list.items嵌套过深)。 + + 参数: + chapter: 章节JSON对象,会在原地被清理和规整。 + """ def walk(blocks: List[Dict[str, Any]] | None): """递归检查并修复嵌套结构,保证每个block合法""" @@ -527,6 +588,12 @@ class ChapterGenerationNode(BaseNode): 若blocks缺失、除标题外无有效区块,或正文字符数低于阈值, 则视为章节内容异常,触发ChapterContentError以便上游重试。 + + 参数: + chapter: 当前章节JSON。 + + 异常: + ChapterContentError: 当正文区块数量或字符数达不到下限时抛出。 """ blocks = chapter.get("blocks") if not isinstance(blocks, list) or not blocks: @@ -552,6 +619,12 @@ class ChapterGenerationNode(BaseNode): - 忽略heading/divider/widget等非正文类型; - 对paragraph/list/table/callout等结构抽取嵌套文本; - 仅用于粗粒度判断篇幅是否合理。 + + 参数: + blocks: 章节的 blocks 列表或子树。 + + 返回: + int: 估算的正文字符数量。 """ def walk(node: Any) -> int: diff --git a/ReportEngine/nodes/document_layout_node.py b/ReportEngine/nodes/document_layout_node.py index df0ab20..096a92e 100644 --- a/ReportEngine/nodes/document_layout_node.py +++ b/ReportEngine/nodes/document_layout_node.py @@ -37,7 +37,20 @@ class DocumentLayoutNode(BaseNode): query: str, template_overview: Dict[str, Any] | None = None, ) -> Dict[str, Any]: - """综合模板+多源内容,生成全书的标题、目录结构与主题色板""" + """ + 综合模板+多源内容,生成全书的标题、目录结构与主题色板。 + + 参数: + sections: 模板切片后的章节列表。 + template_markdown: 模板原文,用于LLM理解上下文。 + reports: 三个引擎的内容映射。 + forum_logs: 论坛讨论摘要。 + query: 用户查询词。 + template_overview: 预生成的模板概览,可复用以减少提示词长度。 + + 返回: + dict: 包含 title/subtitle/toc/hero/themeTokens 等设计信息的字典。 + """ # 将模板原文、切片结构与多源报告一并喂给LLM,便于其理解层级与素材 payload = { "query": query, @@ -66,7 +79,18 @@ class DocumentLayoutNode(BaseNode): return design def _parse_response(self, raw: str) -> Dict[str, Any]: - """解析LLM返回的JSON文本,若失败则抛出友好错误""" + """ + 解析LLM返回的JSON文本,若失败则抛出友好错误。 + + 参数: + raw: LLM原始返回字符串,允许带```包裹。 + + 返回: + dict: 结构化的设计稿。 + + 异常: + ValueError: 当响应为空或JSON解析失败时抛出。 + """ cleaned = raw.strip() if cleaned.startswith("```json"): cleaned = cleaned[7:] diff --git a/ReportEngine/nodes/template_selection_node.py b/ReportEngine/nodes/template_selection_node.py index 9b00a71..a48c55c 100644 --- a/ReportEngine/nodes/template_selection_node.py +++ b/ReportEngine/nodes/template_selection_node.py @@ -79,6 +79,15 @@ class TemplateSelectionNode(BaseNode): 构造模板列表与报告摘要 → 调用LLM → 解析JSON → 验证模板是否存在并返回标准结构。 + + 参数: + query: 用户输入的主题词。 + reports: 多个分析引擎的报告内容。 + forum_logs: 论坛日志,可能为空。 + available_templates: 本地可用模板清单。 + + 返回: + dict | None: 若LLM成功返回合法结果则包含模板信息,否则为None。 """ logger.info("尝试使用LLM进行模板选择...") @@ -166,6 +175,12 @@ class TemplateSelectionNode(BaseNode): 清理LLM响应。 去掉 ```json``` 包裹以及前后空白,方便 `json.loads`。 + + 参数: + response: LLM原始响应。 + + 返回: + str: 适合直接做JSON解析的纯文本。 """ # 移除可能的markdown代码块标记 if '```json' in response: @@ -183,6 +198,13 @@ class TemplateSelectionNode(BaseNode): 从文本响应中提取模板信息。 当LLM未输出合法JSON时,尝试匹配模板名称关键字做降级。 + + 参数: + response: 非结构化的LLM文本。 + available_templates: 可选模板列表。 + + 返回: + dict | None: 匹配成功时返回模板详情,否则为None。 """ logger.info("尝试从文本响应中提取模板信息") @@ -210,6 +232,9 @@ class TemplateSelectionNode(BaseNode): 获取可用的模板列表。 枚举模板目录下的 `.md` 文件并读取内容与描述字段。 + + 返回: + list[dict]: 每项包含 name/path/content/description。 """ templates = [] @@ -259,7 +284,12 @@ class TemplateSelectionNode(BaseNode): def _get_fallback_template(self) -> Dict[str, Any]: - """获取备用默认模板(空模板,让LLM自行发挥)。""" + """ + 获取备用默认模板(空模板,让LLM自行发挥)。 + + 返回: + dict: 结构体字段与LLM返回一致,方便直接替换。 + """ logger.info("未找到合适模板,使用空模板让LLM自行发挥") return { diff --git a/ReportEngine/nodes/word_budget_node.py b/ReportEngine/nodes/word_budget_node.py index b1802af..51a2881 100644 --- a/ReportEngine/nodes/word_budget_node.py +++ b/ReportEngine/nodes/word_budget_node.py @@ -37,7 +37,20 @@ class WordBudgetNode(BaseNode): query: str, template_overview: Dict[str, Any] | None = None, ) -> Dict[str, Any]: - """根据设计稿和所有素材规划章节字数,让LLM写作时有明确篇幅目标""" + """ + 根据设计稿和所有素材规划章节字数,让LLM写作时有明确篇幅目标。 + + 参数: + sections: 模板章节列表。 + design: 布局节点返回的设计稿(title/toc/hero等)。 + reports: 三引擎报告映射。 + forum_logs: 论坛日志原文。 + query: 用户查询词。 + template_overview: 可选的模板概览,含章节元信息。 + + 返回: + dict: 章节篇幅规划结果,包含 `totalWords`、`globalGuidelines` 与逐章 `chapters`。 + """ # 输入中除了章节骨架外,还包含布局节点输出,方便约束篇幅时参考视觉主次 payload = { "query": query, @@ -63,7 +76,18 @@ class WordBudgetNode(BaseNode): return plan def _parse_response(self, raw: str) -> Dict[str, Any]: - """将LLM输出的JSON文本转为字典,失败时提示规划异常""" + """ + 将LLM输出的JSON文本转为字典,失败时提示规划异常。 + + 参数: + raw: LLM返回值,可能包含```包裹。 + + 返回: + dict: 合法的篇幅规划JSON。 + + 异常: + ValueError: 当响应为空或JSON解析失败时抛出。 + """ cleaned = raw.strip() if cleaned.startswith("```json"): cleaned = cleaned[7:] diff --git a/ReportEngine/renderers/html_renderer.py b/ReportEngine/renderers/html_renderer.py index e41447c..48fe847 100644 --- a/ReportEngine/renderers/html_renderer.py +++ b/ReportEngine/renderers/html_renderer.py @@ -62,7 +62,15 @@ class HTMLRenderer: # ====== 公共入口 ====== def render(self, document_ir: Dict[str, Any]) -> str: - """接收Document IR,重置内部状态并输出完整HTML""" + """ + 接收Document IR,重置内部状态并输出完整HTML。 + + 参数: + document_ir: 由 DocumentComposer 生成的整本报告数据。 + + 返回: + str: 可直接写入磁盘的完整HTML文档。 + """ self.document = document_ir or {} self.widget_scripts = [] self.chart_counter = 0 @@ -89,7 +97,16 @@ class HTMLRenderer: # ====== Head / Body ====== def _render_head(self, title: str, theme_tokens: Dict[str, Any]) -> str: - """渲染部分,加载主题CSS与必要的脚本依赖""" + """ + 渲染部分,加载主题CSS与必要的脚本依赖。 + + 参数: + title: 页面title标签内容。 + theme_tokens: 主题变量,用于注入CSS。 + + 返回: + str: head片段HTML。 + """ css = self._build_css(theme_tokens) return f""" @@ -124,7 +141,12 @@ class HTMLRenderer: """.strip() def _render_body(self) -> str: - """拼装结构,包含头部、导航、章节和脚本""" + """ + 拼装结构,包含头部、导航、章节和脚本。 + + 返回: + str: body片段HTML。 + """ header = self._render_header() cover = self._render_cover() hero = self._render_hero() @@ -152,7 +174,12 @@ class HTMLRenderer: # ====== Header / Meta / TOC ====== def _render_header(self) -> str: - """渲染吸顶头部,包含标题、副标题与功能按钮""" + """ + 渲染吸顶头部,包含标题、副标题与功能按钮。 + + 返回: + str: header HTML。 + """ metadata = self.metadata title = metadata.get("title") or "智能舆情分析报告" subtitle = metadata.get("subtitle") or metadata.get("templateName") or "自动生成" @@ -172,14 +199,24 @@ class HTMLRenderer: """.strip() def _render_tagline(self) -> str: - """渲染标题下方的标语,如无标语则返回空字符串""" + """ + 渲染标题下方的标语,如无标语则返回空字符串。 + + 返回: + str: tagline HTML或空串。 + """ tagline = self.metadata.get("tagline") if not tagline: return "" return f'

{self._escape_html(tagline)}

' def _render_cover(self) -> str: - """文章开头的封面区,居中展示标题与“文章总览”提示""" + """ + 文章开头的封面区,居中展示标题与“文章总览”提示。 + + 返回: + str: cover section HTML。 + """ title = self.metadata.get("title") or "智能舆情报告" subtitle = self.metadata.get("subtitle") or self.metadata.get("templateName") or "" overview_hint = "文章总览" @@ -192,7 +229,12 @@ class HTMLRenderer: """.strip() def _render_hero(self) -> str: - """根据layout中的hero字段输出摘要/KPI/亮点区""" + """ + 根据layout中的hero字段输出摘要/KPI/亮点区。 + + 返回: + str: hero区HTML,若无数据则为空字符串。 + """ hero = self.metadata.get("hero") or {} if not hero: return "" @@ -239,7 +281,12 @@ class HTMLRenderer: return "" def _render_toc_section(self) -> str: - """生成目录模块,如无目录数据则返回空字符串""" + """ + 生成目录模块,如无目录数据则返回空字符串。 + + 返回: + str: toc HTML结构。 + """ if not self.toc_entries: return "" toc_config = self.metadata.get("toc") or {} @@ -258,7 +305,15 @@ class HTMLRenderer: """.strip() def _collect_toc_entries(self, chapters: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """根据metadata中的tocPlan或章节heading收集目录项""" + """ + 根据metadata中的tocPlan或章节heading收集目录项。 + + 参数: + chapters: Document IR中的章节数组。 + + 返回: + list[dict]: 规范化后的目录条目,包含level/text/anchor。 + """ metadata = self.metadata toc_config = metadata.get("toc") or {} custom_entries = toc_config.get("customEntries") @@ -296,7 +351,15 @@ class HTMLRenderer: return entries def _format_toc_entry(self, entry: Dict[str, Any]) -> str: - """将单个目录项转为带描述的HTML行""" + """ + 将单个目录项转为带描述的HTML行。 + + 参数: + entry: 目录条目,需包含 `text` 与 `anchor`。 + + 返回: + str: `
  • ` 形式的HTML。 + """ desc = entry.get("description") desc_html = f'

    {self._escape_html(desc)}

    ' if desc else "" level = entry.get("level", 2) @@ -304,7 +367,15 @@ class HTMLRenderer: return f'
  • {self._escape_html(entry["text"])}{desc_html}
  • ' def _compute_heading_labels(self, chapters: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: - """预计算各级标题的编号(章:一、二;节:1.1;小节:1.1.1)""" + """ + 预计算各级标题的编号(章:一、二;节:1.1;小节:1.1.1)。 + + 参数: + chapters: Document IR中的章节数组。 + + 返回: + dict: 锚点到编号/描述的映射,方便TOC与正文引用。 + """ label_map: Dict[str, Dict[str, Any]] = {} for chap_idx, chapter in enumerate(chapters or [], start=1): @@ -394,17 +465,41 @@ class HTMLRenderer: # ====== 章节 & Block 渲染 ====== def _render_chapter(self, chapter: Dict[str, Any]) -> str: - """将章节blocks包裹进
    ,便于CSS控制""" + """ + 将章节blocks包裹进
    ,便于CSS控制。 + + 参数: + chapter: 单个章节JSON。 + + 返回: + str: section包裹的HTML。 + """ section_id = self._escape_attr(chapter.get("anchor") or f"chapter-{chapter.get('chapterId', 'x')}") blocks_html = self._render_blocks(chapter.get("blocks", [])) return f'
    \n{blocks_html}\n
    ' def _render_blocks(self, blocks: List[Dict[str, Any]]) -> str: - """顺序渲染章节内所有block""" + """ + 顺序渲染章节内所有block。 + + 参数: + blocks: 章节内部的block数组。 + + 返回: + str: 拼接后的HTML。 + """ return "".join(self._render_block(block) for block in blocks or []) def _render_block(self, block: Dict[str, Any]) -> str: - """根据block.type分派到不同的渲染函数""" + """ + 根据block.type分派到不同的渲染函数。 + + 参数: + block: 单个block对象。 + + 返回: + str: 渲染后的HTML,未知类型会输出JSON调试信息。 + """ block_type = block.get("type") handlers = { "heading": self._render_heading, @@ -468,7 +563,15 @@ class HTMLRenderer: return f'<{tag}{class_attr}>{items_html}' def _render_table(self, block: Dict[str, Any]) -> str: - """渲染表格,同时保留caption与单元格属性""" + """ + 渲染表格,同时保留caption与单元格属性。 + + 参数: + block: table类型的block。 + + 返回: + str: 包含结构的HTML。 + """ rows = self._normalize_table_rows(block.get("rows") or []) rows_html = "" for row in rows: @@ -491,7 +594,15 @@ class HTMLRenderer: return f'
    {caption_html}{rows_html}
    ' def _normalize_table_rows(self, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """检测并修正仅有单列的竖排表,转换为标准网格""" + """ + 检测并修正仅有单列的竖排表,转换为标准网格。 + + 参数: + rows: 原始表格行。 + + 返回: + list[dict]: 若检测到竖排表则返回转置后的行,否则原样返回。 + """ if not rows: return [] if not all(len((row.get("cells") or [])) == 1 for row in rows): @@ -611,7 +722,15 @@ class HTMLRenderer: return f'
    {self._escape_html(caption)}
    ' def _render_callout(self, block: Dict[str, Any]) -> str: - """渲染高亮提示盒,tone决定颜色""" + """ + 渲染高亮提示盒,tone决定颜色。 + + 参数: + block: callout类型的block。 + + 返回: + str: callout HTML,若内部包含不允许的块会被拆分。 + """ tone = block.get("tone", "info") title = block.get("title") safe_blocks, trailing_blocks = self._split_callout_content(block.get("blocks")) @@ -689,7 +808,15 @@ class HTMLRenderer: return f'
    {cards}
    ' def _render_widget(self, block: Dict[str, Any]) -> str: - """渲染Chart.js等交互组件的占位容器,并记录配置JSON""" + """ + 渲染Chart.js等交互组件的占位容器,并记录配置JSON。 + + 参数: + block: widget类型的block,包含widgetId/props/data。 + + 返回: + str: 含canvas与配置脚本的HTML。 + """ self.chart_counter += 1 canvas_id = f"chart-{self.chart_counter}" config_id = f"chart-config-{self.chart_counter}" @@ -830,7 +957,15 @@ class HTMLRenderer: return payload def _render_inline(self, run: Dict[str, Any]) -> str: - """渲染单个inline run,支持多种marks叠加""" + """ + 渲染单个inline run,支持多种marks叠加。 + + 参数: + run: 含 text 与 marks 的内联节点。 + + 返回: + str: 已包裹标签/样式的HTML片段。 + """ text_value, marks = self._normalize_inline_payload(run) math_mark = next((mark for mark in marks if mark.get("type") == "math"), None) if math_mark: diff --git a/ReportEngine/utils/config.py b/ReportEngine/utils/config.py index ffa7f2a..b7dac45 100644 --- a/ReportEngine/utils/config.py +++ b/ReportEngine/utils/config.py @@ -47,7 +47,12 @@ settings = Settings() def print_config(config: Settings): - """将当前配置项按人类可读格式输出到日志,方便排障""" + """ + 将当前配置项按人类可读格式输出到日志,方便排障。 + + 参数: + config: Settings实例,通常为全局settings。 + """ message = "" message += "\n=== Report Engine 配置 ===\n" message += f"LLM 模型: {config.REPORT_ENGINE_MODEL_NAME}\n" diff --git a/templates/index.html b/templates/index.html index 1080959..e9793a3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1081,6 +1081,7 @@ +
    @@ -1158,13 +1159,14 @@
    - +
    连接中...
    +
    @@ -1187,9 +1189,10 @@
    - +
    +