diff --git a/ReportEngine/__init__.py b/ReportEngine/__init__.py index 299633c..a372b50 100644 --- a/ReportEngine/__init__.py +++ b/ReportEngine/__init__.py @@ -1,7 +1,8 @@ """ -Report Engine -一个智能报告生成AI代理实现 -基于三个子agent的输出和论坛日志生成综合HTML报告 +Report Engine。 + +一个智能报告生成AI代理实现,聚合 Query/Media/Insight 三个子引擎的 +Markdown 与论坛讨论,最终落地结构化HTML报告。 """ from .agent import ReportAgent, create_agent diff --git a/ReportEngine/agent.py b/ReportEngine/agent.py index 9c21d20..1d49cf2 100644 --- a/ReportEngine/agent.py +++ b/ReportEngine/agent.py @@ -1,6 +1,11 @@ """ -Report Agent主类 -整合所有模块,实现完整的报告生成流程 +Report Agent主类。 + +该模块串联模板选择、布局设计、章节生成、IR装订与HTML渲染等 +所有子流程,是Report Engine的总调度中心。核心职责包括: +1. 管理输入数据与状态,协调三个分析引擎、论坛日志与模板; +2. 按节点顺序驱动模板选择→布局生成→篇幅规划→章节写作→装订渲染; +3. 负责错误兜底、流式事件分发、落盘清单与最终成果保存。 """ import json @@ -33,15 +38,32 @@ from .utils.config import settings, Settings class FileCountBaseline: - """文件数量基准管理器""" + """ + 文件数量基准管理器。 + + 该工具用于: + - 在任务启动时记录 Insight/Media/Query 三个引擎导出的 Markdown 数量; + - 在后续轮询中快速判断是否有新报告落地; + - 为 Flask 层提供“输入是否准备完毕”的依据。 + """ def __init__(self): - """在初始化阶段加载或创建文件数量基准快照""" + """ + 初始化时优先尝试读取既有的基准快照。 + + 若 `logs/report_baseline.json` 不存在则会自动创建一份空快照, + 以便后续 `initialize_baseline` 在首次运行时写入真实基准。 + """ self.baseline_file = 'logs/report_baseline.json' self.baseline_data = self._load_baseline() def _load_baseline(self) -> Dict[str, int]: - """加载基准数据""" + """ + 加载基准数据。 + + - 当快照文件存在时直接解析JSON; + - 捕获所有加载异常并返回空字典,保证调用方逻辑简洁。 + """ try: if os.path.exists(self.baseline_file): with open(self.baseline_file, 'r', encoding='utf-8') as f: @@ -51,7 +73,12 @@ class FileCountBaseline: return {} def _save_baseline(self): - """保存基准数据""" + """ + 将当前基准写入磁盘。 + + 采用 `ensure_ascii=False` + 缩进格式,方便人工查看; + 若目标目录缺失则自动创建。 + """ try: os.makedirs(os.path.dirname(self.baseline_file), exist_ok=True) with open(self.baseline_file, 'w', encoding='utf-8') as f: @@ -60,7 +87,12 @@ class FileCountBaseline: logger.exception(f"保存基准数据失败: {e}") def initialize_baseline(self, directories: Dict[str, str]) -> Dict[str, int]: - """初始化文件数量基准""" + """ + 初始化文件数量基准。 + + 遍历每个引擎目录并统计 `.md` 文件数量,将结果持久化为 + 初始基准。后续 `check_new_files` 会据此对比增量。 + """ current_counts = {} for engine, directory in directories.items(): @@ -78,7 +110,13 @@ class FileCountBaseline: return current_counts def check_new_files(self, directories: Dict[str, str]) -> Dict[str, Any]: - """检查是否有新文件""" + """ + 检查是否有新文件。 + + 对比当前目录文件数与基准: + - 统计新增数量,并判定是否所有引擎都已准备就绪; + - 返回详细计数、缺失列表,供 Web 层提示给用户。 + """ current_counts = {} new_files_found = {} all_have_new = True @@ -108,7 +146,12 @@ class FileCountBaseline: } def get_latest_files(self, directories: Dict[str, str]) -> Dict[str, str]: - """获取每个目录的最新文件""" + """ + 获取每个目录的最新文件。 + + 通过 `os.path.getmtime` 找出最近写入的 Markdown, + 以确保生成流程永远使用最新一版三引擎报告。 + """ latest_files = {} for engine, directory in directories.items(): @@ -122,14 +165,27 @@ class FileCountBaseline: class ReportAgent: - """Report Agent主类""" + """ + Report Agent主类。 + + 负责集成: + - LLM客户端及其上层四个推理节点; + - 章节存储、IR装订、渲染器等产出链路; + - 状态管理、日志、输入输出校验与持久化。 + """ def __init__(self, config: Optional[Settings] = None): """ - 初始化Report Agent + 初始化Report Agent。 Args: config: 配置对象,如果不提供则自动加载 + + 步骤概览: + 1. 解析配置并接入日志/LLM/渲染等核心组件; + 2. 构造四个推理节点(模板、布局、篇幅、章节); + 3. 初始化文件基准与章节落盘目录; + 4. 构建可序列化的状态容器,供外部服务查询。 """ # 加载配置 self.config = config or settings @@ -166,7 +222,13 @@ class ReportAgent: logger.info(f"使用LLM: {self.llm_client.get_model_info()}") def _setup_logging(self): - """设置日志""" + """ + 设置日志。 + + - 确保日志目录存在; + - 使用独立的 loguru sink 写入 Report Engine 专属 log 文件, + 避免与其他子系统混淆。 + """ # 确保日志目录存在 log_dir = os.path.dirname(self.config.LOG_FILE) os.makedirs(log_dir, exist_ok=True) @@ -175,7 +237,12 @@ class ReportAgent: logger.add(self.config.LOG_FILE, level="INFO") def _initialize_file_baseline(self): - """初始化文件数量基准""" + """ + 初始化文件数量基准。 + + 将 Insight/Media/Query 三个目录传入 `FileCountBaseline`, + 生成一次性的参考值,之后按增量判断三引擎是否产出新报告。 + """ directories = { 'insight': 'insight_engine_streamlit_reports', 'media': 'media_engine_streamlit_reports', @@ -184,7 +251,12 @@ class ReportAgent: self.file_baseline.initialize_baseline(directories) def _initialize_llm(self) -> LLMClient: - """初始化LLM客户端""" + """ + 初始化LLM客户端。 + + 利用配置中的 API Key / 模型 / Base URL 构建统一的 + `LLMClient` 实例,为所有节点提供复用的推理入口。 + """ return LLMClient( api_key=self.config.REPORT_ENGINE_API_KEY, model_name=self.config.REPORT_ENGINE_MODEL_NAME, @@ -192,7 +264,12 @@ class ReportAgent: ) def _initialize_nodes(self): - """初始化处理节点""" + """ + 初始化处理节点。 + + 顺序实例化模板选择、文档布局、篇幅规划、章节生成四个节点, + 其中章节节点额外依赖 IR 校验器与章节存储器。 + """ self.template_selection_node = TemplateSelectionNode( self.llm_client, self.config.TEMPLATE_DIR @@ -209,7 +286,14 @@ class ReportAgent: custom_template: str = "", save_report: bool = True, stream_handler: Optional[Callable[[str, Dict[str, Any]], None]] = None) -> str: """ - 生成综合报告(章节JSON → IR → HTML) + 生成综合报告(章节JSON → IR → HTML)。 + + 主要阶段: + 1. 归一化三引擎报告 + 论坛日志,并输出流式事件; + 2. 模板选择 → 模板切片 → 文档布局 → 篇幅规划; + 3. 结合篇幅目标逐章调用LLM,遇到解析错误会自动重试; + 4. 将章节装订成Document IR,再交给HTML渲染器生成成品; + 5. 可选地将HTML/IR/状态落盘,并向外界回传路径信息。 Returns: dict: HTML内容以及保存的文件路径信息 @@ -441,7 +525,13 @@ class ReportAgent: raise def _select_template(self, query: str, reports: List[Any], forum_logs: str, custom_template: str): - """选择报告模板""" + """ + 选择报告模板。 + + 优先使用用户指定的模板;否则将查询、三引擎报告与论坛日志 + 作为上下文交给 TemplateSelectionNode,由 LLM 返回最契合的 + 模板名称、内容及理由,并自动记录在状态中。 + """ logger.info("选择报告模板...") # 如果用户提供了自定义模板,直接使用 @@ -481,7 +571,13 @@ class ReportAgent: return fallback_template def _slice_template(self, template_markdown: str) -> List[TemplateSection]: - """将模板切成章节列表,若为空则提供fallback""" + """ + 将模板切成章节列表,若为空则提供fallback。 + + 委托 `parse_template_sections` 将Markdown标题/编号解析为 + `TemplateSection` 列表,确保后续章节生成有稳定的章节ID。 + 当模板格式异常时,会回退到内置的简单骨架避免崩溃。 + """ sections = parse_template_sections(template_markdown) if sections: return sections @@ -510,10 +606,11 @@ class ReportAgent: template_overview: Dict[str, Any], ) -> Dict[str, Any]: """ - 构造章节生成所需的共享上下文 + 构造章节生成所需的共享上下文。 - 这里把“全书设计稿”“章节篇幅约束”“统一主题配色”等一次性整理好, - 避免每次章节调用都重新拼装上下文。 + 将模板名称、布局设计、主题配色、篇幅规划、论坛日志等 + 一次性整合为 `generation_context`,后续每章调用 LLM 时 + 直接复用,确保所有章节共享一致的语调和视觉约束。 """ # 优先使用设计稿定制的主题色,否则退回默认主题 theme_tokens = ( @@ -541,7 +638,12 @@ class ReportAgent: } def _normalize_reports(self, reports: List[Any]) -> Dict[str, str]: - """将不同来源的报告统一转为字符串""" + """ + 将不同来源的报告统一转为字符串。 + + 约定顺序为 Query/Media/Insight,引擎提供的对象可能是 + 字典或自定义类型,因此统一走 `_stringify` 做容错。 + """ keys = ["query_engine", "media_engine", "insight_engine"] normalized: Dict[str, str] = {} for idx, key in enumerate(keys): @@ -551,7 +653,10 @@ class ReportAgent: def _should_retry_inappropriate_content_error(self, error: Exception) -> bool: """ - 判断LLM异常是否由内容安全/不当内容导致,满足时允许重新生成整章。 + 判断LLM异常是否由内容安全/不当内容导致。 + + 当检测到供应商返回的错误包含特定关键词时,允许章节生成 + 重新尝试,以便绕过偶发的内容审查触发。 """ message = str(error) if error else "" if not message: @@ -566,7 +671,12 @@ class ReportAgent: return any(keyword in normalized for keyword in keywords) def _stringify(self, value: Any) -> str: - """安全地将对象转成字符串""" + """ + 安全地将对象转成字符串。 + + - dict/list 统一序列化为格式化 JSON,便于提示词消费; + - 其他类型走 `str()`,None 则返回空串,避免 None 传播。 + """ if value is None: return "" if isinstance(value, str): @@ -579,7 +689,11 @@ class ReportAgent: return str(value) def _default_theme_tokens(self) -> Dict[str, Any]: - """默认的主题变量,供渲染器/LLM共用""" + """ + 构造默认主题变量,供渲染器/LLM共用。 + + 当布局节点未返回专属配色时使用该套色板,保持报告风格统一。 + """ return { "colors": { "bg": "#f8f9fa", @@ -610,7 +724,11 @@ class ReportAgent: template_markdown: str, sections: List[TemplateSection], ) -> Dict[str, Any]: - """提取模板标题与章节骨架,供设计/篇幅规划统一引用""" + """ + 提取模板标题与章节骨架,供设计/篇幅规划统一引用。 + + 同时记录章节ID/slug/order等辅助字段,保证多节点对齐。 + """ fallback_title = sections[0].title if sections else "" overview = { "title": self._extract_template_title(template_markdown, fallback_title), @@ -633,7 +751,12 @@ class ReportAgent: @staticmethod def _extract_template_title(template_markdown: str, fallback: str = "") -> str: - """尝试从Markdown中提取首个标题,找不到时使用fallback""" + """ + 尝试从Markdown中提取首个标题。 + + 优先返回首个 `#` 语法标题;如果模板首行就是正文,则回退到 + 第一行非空文本或调用方提供的 fallback。 + """ for line in template_markdown.splitlines(): stripped = line.strip() if not stripped: @@ -645,7 +768,12 @@ class ReportAgent: return fallback or "智能舆情分析报告" def _get_fallback_template_content(self) -> str: - """获取备用模板内容""" + """ + 获取备用模板内容。 + + 当模板目录不可用或LLM选择失败时使用该 Markdown 模板, + 保证后续流程仍能给出结构化章节。 + """ return """# 社会公共热点事件分析报告 ## 执行摘要 @@ -694,7 +822,12 @@ class ReportAgent: """ def _save_report(self, html_content: str, document_ir: Dict[str, Any], report_id: str) -> Dict[str, Any]: - """保存HTML与IR到文件并返回路径信息""" + """ + 保存HTML与IR到文件并返回路径信息。 + + 生成基于查询和时间戳的易读文件名,同时也把运行态的 + `ReportState` 写入 JSON,方便下游排障或断点续跑。 + """ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") query_safe = "".join( c for c in self.state.metadata.query if c.isalnum() or c in (" ", "-", "_") @@ -734,7 +867,12 @@ class ReportAgent: } def _save_document_ir(self, document_ir: Dict[str, Any], query_safe: str, timestamp: str) -> Path: - """将整本IR写入独立目录""" + """ + 将整本IR写入独立目录。 + + `Document IR` 与 HTML 解耦保存,便于调试渲染差异以及 + 在不重新跑 LLM 的情况下再次渲染或导出其他格式。 + """ filename = f"report_ir_{query_safe}_{timestamp}.json" ir_path = Path(self.config.DOCUMENT_IR_OUTPUT_DIR) / filename ir_path.write_text( @@ -751,8 +889,9 @@ class ReportAgent: template_overview: Dict[str, Any], ): """ - 将文档设计稿、篇幅规划与模板概览另存成JSON + 将文档设计稿、篇幅规划与模板概览另存成JSON。 + 这些中间件文件(document_layout/word_plan/template_overview) 方便在调试或复盘时快速定位:标题/目录/主题是如何确定的、 字数分配有什么要求,以便后续人工校正。 """ @@ -771,22 +910,22 @@ class ReportAgent: logger.warning(f"写入{name}失败: {exc}") def get_progress_summary(self) -> Dict[str, Any]: - """获取进度摘要""" + """获取进度摘要,直接返回可序列化的状态字典供API层查询。""" return self.state.to_dict() def load_state(self, filepath: str): - """从文件加载状态""" + """从文件加载状态并覆盖当前state,便于断点恢复。""" self.state = ReportState.load_from_file(filepath) logger.info(f"状态已从 {filepath} 加载") def save_state(self, filepath: str): - """保存状态到文件""" + """保存状态到文件,通常用于任务完成后的分析与备份。""" self.state.save_to_file(filepath) logger.info(f"状态已保存到 {filepath}") def check_input_files(self, insight_dir: str, media_dir: str, query_dir: str, forum_log_path: str) -> Dict[str, Any]: """ - 检查输入文件是否准备就绪(基于文件数量增加) + 检查输入文件是否准备就绪(基于文件数量增加)。 Args: insight_dir: InsightEngine报告目录 @@ -795,7 +934,7 @@ class ReportAgent: forum_log_path: 论坛日志文件路径 Returns: - 检查结果字典 + 检查结果字典,包含文件计数、缺失列表、最新文件路径等 """ # 检查各个报告目录的文件数量变化 directories = { @@ -853,7 +992,7 @@ class ReportAgent: file_paths: 文件路径字典 Returns: - 加载的内容字典 + 加载的内容字典,包含 `reports` 列表与 `forum_logs` 字符串 """ content = { 'reports': [], @@ -887,13 +1026,15 @@ class ReportAgent: def create_agent(config_file: Optional[str] = None) -> ReportAgent: """ - 创建Report Agent实例的便捷函数 + 创建Report Agent实例的便捷函数。 Args: config_file: 配置文件路径 Returns: ReportAgent实例 + + 目前以环境变量驱动 `Settings`,保留 `config_file` 参数便于未来扩展。 """ config = Settings() # 以空配置初始化,而从从环境变量初始化 diff --git a/ReportEngine/core/__init__.py b/ReportEngine/core/__init__.py index 9b189bc..8bb45ad 100644 --- a/ReportEngine/core/__init__.py +++ b/ReportEngine/core/__init__.py @@ -1,7 +1,8 @@ """ Report Engine核心工具集合。 -包含模板切片、章节存储等基础能力,供agent流水线复用。 +该包封装了模板切片、章节存储与章节装订三大基础能力, +所有上层节点都会复用这些工具保证结构一致。 """ from .template_parser import TemplateSection, parse_template_sections diff --git a/ReportEngine/core/chapter_storage.py b/ReportEngine/core/chapter_storage.py index 99f6e1f..481ed1e 100644 --- a/ReportEngine/core/chapter_storage.py +++ b/ReportEngine/core/chapter_storage.py @@ -17,7 +17,12 @@ from typing import Dict, Generator, List, Optional @dataclass class ChapterRecord: - """manifest中记录的章节元数据""" + """ + manifest中记录的章节元数据。 + + 该结构用于在 `manifest.json` 中追踪每章的状态、文件位置、 + 以及可能的错误列表,方便前端或调试工具读取。 + """ chapter_id: str slug: str @@ -46,12 +51,10 @@ class ChapterStorage: """ 章节JSON写入与manifest管理器。 - 用法: - run_dir = storage.start_session(report_id, {...}) - chapter_dir = storage.begin_chapter(run_dir, meta) - with storage.capture_stream(chapter_dir) as fp: - fp.write(chunk) - storage.persist_chapter(run_dir, meta, payload, errors) + 负责: + - 为每次报告创建独立run目录与manifest快照; + - 在章节流式生成时即时写入 `stream.raw`; + - 校验通过后持久化 `chapter.json` 并更新manifest状态。 """ def __init__(self, base_dir: str): @@ -68,7 +71,11 @@ class ChapterStorage: # ======== 会话 & manifest ======== def start_session(self, report_id: str, metadata: Dict[str, object]) -> Path: - """为本次报告创建独立的章节输出目录与manifest""" + """ + 为本次报告创建独立的章节输出目录与manifest。 + + 同时把全局metadata写入 `manifest.json`,供渲染/调试查询。 + """ run_dir = self.base_dir / report_id run_dir.mkdir(parents=True, exist_ok=True) manifest = { @@ -82,7 +89,11 @@ class ChapterStorage: return run_dir def begin_chapter(self, run_dir: Path, chapter_meta: Dict[str, object]) -> Path: - """创建章节子目录并在manifest中标记为streaming状态""" + """ + 创建章节子目录并在manifest中标记为streaming状态。 + + 会生成 `order-slug` 风格的子目录,并提前登记 raw 文件路径。 + """ slug_value = str( chapter_meta.get("slug") or chapter_meta.get("chapterId") or "section" ) @@ -109,7 +120,11 @@ class ChapterStorage: payload: Dict[str, object], errors: Optional[List[str]] = None, ) -> Path: - """章节流式生成完毕后写入最终JSON并更新manifest状态""" + """ + 章节流式生成完毕后写入最终JSON并更新manifest状态。 + + 若校验失败,错误信息会被写入manifest,供前端展示。 + """ slug_value = str( chapter_meta.get("slug") or chapter_meta.get("chapterId") or "section" ) @@ -140,7 +155,11 @@ class ChapterStorage: return final_path def load_chapters(self, run_dir: Path) -> List[Dict[str, object]]: - """从指定run目录读取全部chapter.json并按order排序返回""" + """ + 从指定run目录读取全部chapter.json并按order排序返回。 + + 常用于 DocumentComposer 将多个章节装订成整本IR。 + """ payloads: List[Dict[str, object]] = [] for child in sorted(run_dir.iterdir()): if not child.is_dir(): @@ -160,7 +179,11 @@ class ChapterStorage: @contextmanager def capture_stream(self, chapter_dir: Path) -> Generator: - """将流式输出实时写入raw文件""" + """ + 将流式输出实时写入raw文件。 + + 通过 contextmanager 暴露文件句柄,简化章节节点的写入逻辑。 + """ raw_path = self._raw_stream_path(chapter_dir) raw_path.parent.mkdir(parents=True, exist_ok=True) with raw_path.open("w", encoding="utf-8") as fp: @@ -169,7 +192,7 @@ class ChapterStorage: # ======== 内部工具 ======== def _chapter_dir(self, run_dir: Path, slug: str, order: int) -> Path: - """根据slug/order生成稳定的章节目录,确保各章分隔存盘""" + """根据slug/order生成稳定目录,确保各章分隔存盘且可排序。""" safe_slug = self._safe_slug(slug) folder = f"{order:03d}-{safe_slug}" path = run_dir / folder @@ -177,38 +200,46 @@ class ChapterStorage: return path def _safe_slug(self, slug: str) -> str: - """移除危险字符,避免生成非法文件夹名""" + """移除危险字符,避免生成非法文件夹名。""" slug = slug.replace(" ", "-").replace("/", "-") return slug or "section" def _raw_stream_path(self, chapter_dir: Path) -> Path: - """返回某章节流式输出对应的raw文件路径""" + """返回某章节流式输出对应的raw文件路径。""" return chapter_dir / "stream.raw" def _key(self, run_dir: Path) -> str: - """将run目录解析为字典缓存的键,避免重复读取磁盘""" + """将run目录解析为字典缓存的键,避免重复读取磁盘。""" return str(run_dir.resolve()) def _manifest_path(self, run_dir: Path) -> Path: - """获取manifest.json的实际文件路径""" + """获取manifest.json的实际文件路径。""" return run_dir / "manifest.json" def _write_manifest(self, run_dir: Path, manifest: Dict[str, object]): - """将内存中的manifest快照全量写回磁盘""" + """将内存中的manifest快照全量写回磁盘。""" self._manifest_path(run_dir).write_text( json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8", ) def _read_manifest(self, run_dir: Path) -> Dict[str, object]: - """从磁盘读取已有manifest,用于进程重启或多实例协作""" + """ + 从磁盘读取已有manifest。 + + 进程重启或多实例写盘时可借助它恢复上下文。 + """ manifest_path = self._manifest_path(run_dir) if manifest_path.exists(): return json.loads(manifest_path.read_text(encoding="utf-8")) return {"reportId": run_dir.name, "chapters": []} def _upsert_record(self, run_dir: Path, record: ChapterRecord): - """更新或追加manifest中的章节记录,保证顺序一致""" + """ + 更新或追加manifest中的章节记录,保证顺序一致。 + + 内部会自动排序并写回缓存+磁盘。 + """ key = self._key(run_dir) manifest = self._manifests.get(key) or self._read_manifest(run_dir) chapters: List[Dict[str, object]] = manifest.get("chapters", []) diff --git a/ReportEngine/core/stitcher.py b/ReportEngine/core/stitcher.py index ffa3b6b..90008a0 100644 --- a/ReportEngine/core/stitcher.py +++ b/ReportEngine/core/stitcher.py @@ -1,5 +1,7 @@ """ 章节装订器:负责把多个章节JSON合并为整本IR。 + +DocumentComposer 会注入缺失锚点、统一顺序,并补齐 IR 级元数据。 """ from __future__ import annotations @@ -13,6 +15,11 @@ from ..ir import IR_VERSION class DocumentComposer: """ 将章节拼接成Document IR的简单装订器。 + + 作用: + - 按order排序章节,补充默认chapterId; + - 防止anchor重复,生成全局唯一锚点; + - 注入 IR 版本与生成时间戳。 """ def __init__(self): @@ -25,7 +32,11 @@ class DocumentComposer: metadata: Dict[str, object], chapters: List[Dict[str, object]], ) -> Dict[str, object]: - """把所有章节按order排序并注入唯一锚点,形成整本IR""" + """ + 把所有章节按order排序并注入唯一锚点,形成整本IR。 + + 同时合并 metadata/themeTokens/assets,供渲染器直接消费。 + """ ordered = sorted(chapters, key=lambda c: c.get("order", 0)) for idx, chapter in enumerate(ordered, start=1): chapter.setdefault("chapterId", f"S{idx}") @@ -48,7 +59,7 @@ class DocumentComposer: return document def _ensure_unique_anchor(self, anchor: str) -> str: - """若存在重复锚点则追加序号,确保全局唯一""" + """若存在重复锚点则追加序号,确保全局唯一。""" base = anchor counter = 2 while anchor in self._seen_anchors: diff --git a/ReportEngine/core/template_parser.py b/ReportEngine/core/template_parser.py index 9525f00..9f54ae5 100644 --- a/ReportEngine/core/template_parser.py +++ b/ReportEngine/core/template_parser.py @@ -18,7 +18,12 @@ SECTION_ORDER_STEP = 10 @dataclass class TemplateSection: - """模板章节实体""" + """ + 模板章节实体。 + + 记录标题、slug、序号、层级、原始标题、章节编号与提纲, + 方便后续节点在提示词中引用并保持锚点一致。 + """ title: str slug: str @@ -30,7 +35,11 @@ class TemplateSection: outline: List[str] = field(default_factory=list) def to_dict(self) -> dict: - """将章节实体序列化为字典,方便传给LLM或落盘""" + """ + 将章节实体序列化为字典。 + + 该结构广泛用于提示词上下文以及 layout/word budget 节点的输入。 + """ return { "title": self.title, "slug": self.slug, @@ -52,7 +61,8 @@ def parse_template_sections(template_md: str) -> List[TemplateSection]: 将Markdown模板切分成章节列表(按大标题)。 返回的每个TemplateSection都携带slug/order/章节号, - 方便后续分章调用与锚点生成。 + 方便后续分章调用与锚点生成。解析时会同时兼容 + “# 标题”“无符号编号”“列表提纲”等不同写法。 """ sections: List[TemplateSection] = [] @@ -98,7 +108,12 @@ def parse_template_sections(template_md: str) -> List[TemplateSection]: def _classify_line(stripped: str, indent: int) -> Optional[dict]: - """根据缩进与符号分类行""" + """ + 根据缩进与符号分类行。 + + 借助正则判断当前行是章节标题、提纲还是普通列表项, + 并衍生 depth/slug/number 等派生信息。 + """ heading_match = heading_pattern.match(stripped) if heading_match: @@ -154,14 +169,19 @@ def _classify_line(stripped: str, indent: int) -> Optional[dict]: def _strip_markup(text: str) -> str: - """去除包裹的**、__等简单强调标记""" + """去除包裹的**、__等强调标记,避免干扰标题匹配。""" if text.startswith(("**", "__")) and text.endswith(("**", "__")) and len(text) > 4: return text[2:-2].strip() return text def _split_number(payload: str) -> dict: - """拆分编号与标题""" + """ + 拆分编号与标题。 + + 例如 `1.2 市场趋势` 会被拆成 number=1.2、label=市场趋势, + 并提供 display 用于回填标题。 + """ match = number_pattern.match(payload) number = match.group("num") if match else "" label = match.group("label") if match else payload @@ -176,7 +196,7 @@ def _split_number(payload: str) -> dict: def _build_slug(number: str, title: str) -> str: - """根据编号/标题生成锚点""" + """根据编号/标题生成锚点,优先复用编号,缺失时对标题slug化。""" if number: token = number.replace(".", "-") else: @@ -186,7 +206,11 @@ def _build_slug(number: str, title: str) -> str: def _slugify_text(text: str) -> str: - """对任意文本做降噪与转写,得到URL友好的slug片段""" + """ + 对任意文本做降噪与转写,得到URL友好的slug片段。 + + 会规整大小写、移除特殊符号并保留汉字,确保锚点可读。 + """ text = unicodedata.normalize("NFKD", text) text = text.replace("·", "-").replace(" ", "-") text = re.sub(r"[^0-9a-zA-Z\u4e00-\u9fff-]+", "-", text) @@ -195,7 +219,11 @@ def _slugify_text(text: str) -> str: def _ensure_unique_slug(slug: str, used: set) -> str: - """若slug重复则自动追加序号,直到在used集合中唯一""" + """ + 若slug重复则自动追加序号,直到在used集合中唯一。 + + 通过 `-2/-3...` 的方式保证相同标题不会产生重复锚点。 + """ if slug not in used: used.add(slug) return slug diff --git a/ReportEngine/flask_interface.py b/ReportEngine/flask_interface.py index 75679cc..367da3e 100644 --- a/ReportEngine/flask_interface.py +++ b/ReportEngine/flask_interface.py @@ -1,6 +1,10 @@ """ -Report Engine Flask接口 -提供HTTP API用于报告生成 +Report Engine Flask接口。 + +该模块为前端/CLI提供统一HTTP/SSE入口,负责: +1. 初始化 ReportAgent 并串联后台线程; +2. 管理任务排队、进度查询、流式推送与日志下载; +3. 提供模板列表、输入文件检查等周边能力。 """ import os @@ -35,7 +39,11 @@ tasks_registry: Dict[str, 'ReportTask'] = {} def _register_stream(task_id: str) -> Queue: - """为指定任务注册一个事件队列,供SSE监听器消费。""" + """ + 为指定任务注册一个事件队列,供SSE监听器消费。 + + 返回的 Queue 会存入 `stream_subscribers`,SSE 生成器将不断读取。 + """ queue = Queue() with stream_lock: stream_subscribers[task_id].append(queue) @@ -43,7 +51,11 @@ def _register_stream(task_id: str) -> Queue: def _unregister_stream(task_id: str, queue: Queue): - """安全移除事件队列,避免内存泄漏。""" + """ + 安全移除事件队列,避免内存泄漏。 + + 需要在finally中调用,保证异常情况下资源也能释放。 + """ with stream_lock: listeners = stream_subscribers.get(task_id, []) if queue in listeners: @@ -53,7 +65,11 @@ def _unregister_stream(task_id: str, queue: Queue): def _broadcast_event(task_id: str, event: Dict[str, Any]): - """将事件推送给所有监听者,失败时做好异常捕获。""" + """ + 将事件推送给所有监听者,失败时做好异常捕获。 + + 采用浅拷贝监听列表,防止并发移除导致遍历异常。 + """ with stream_lock: listeners = list(stream_subscribers.get(task_id, [])) for queue in listeners: @@ -64,7 +80,11 @@ def _broadcast_event(task_id: str, event: Dict[str, Any]): def _prune_task_history_locked(): - """在task_lock持有期间调用,清理过多的历史任务以控制内存。""" + """ + 在task_lock持有期间调用,清理过多的历史任务。 + + 仅保留最近 `MAX_TASK_HISTORY` 个任务,避免长时间运行占用过多内存。 + """ if len(tasks_registry) <= MAX_TASK_HISTORY: return # 按创建时间排序,移除最旧的任务 @@ -74,7 +94,11 @@ def _prune_task_history_locked(): def _get_task(task_id: str) -> Optional['ReportTask']: - """统一的任务查找方法,优先返回当前任务。""" + """ + 统一的任务查找方法,优先返回当前任务。 + + 避免重复写锁逻辑,便于多个API共享。 + """ with task_lock: if current_task and current_task.task_id == task_id: return current_task @@ -82,7 +106,11 @@ def _get_task(task_id: str) -> Optional['ReportTask']: def _format_sse(event: Dict[str, Any]) -> str: - """按SSE协议格式化消息。""" + """ + 按SSE协议格式化消息。 + + 输出形如 `id:/event:/data:` 的三段文本,供浏览器端直接消费。 + """ payload = json.dumps(event, ensure_ascii=False) event_id = event.get('id', 0) event_type = event.get('type', 'message') @@ -90,7 +118,11 @@ def _format_sse(event: Dict[str, Any]) -> str: def initialize_report_engine(): - """初始化Report Engine""" + """ + 初始化Report Engine。 + + 单例化 ReportAgent,方便 API 启动后直接接收任务。 + """ global report_agent try: report_agent = create_agent() @@ -102,7 +134,12 @@ def initialize_report_engine(): class ReportTask: - """报告生成任务""" + """ + 报告生成任务。 + + 该对象串联运行状态、进度、事件历史及最终文件路径, + 既供后台线程更新,也供HTTP接口读取。 + """ def __init__(self, query: str, task_id: str, custom_template: str = ""): """ @@ -135,7 +172,11 @@ class ReportTask: self.last_event_id = 0 def update_status(self, status: str, progress: int = None, error_message: str = ""): - """更新任务状态""" + """ + 更新任务状态并广播事件。 + + 会自动刷新 `updated_at`、错误信息,并触发 `status` 类型的 SSE。 + """ self.status = status if progress is not None: self.progress = progress @@ -155,7 +196,7 @@ class ReportTask: ) def to_dict(self) -> Dict[str, Any]: - """转换为字典格式""" + """转换为字典格式,方便直接返回给JSON API。""" return { 'task_id': self.task_id, 'query': self.query, @@ -197,7 +238,12 @@ class ReportTask: def check_engines_ready() -> Dict[str, Any]: - """检查三个子引擎是否都有新文件""" + """ + 检查三个子引擎是否都有新文件。 + + 调用 ReportAgent 的基准检测逻辑,并附带论坛日志存在性, + 是 /status、/generate 的前置校验。 + """ directories = { 'insight': 'insight_engine_streamlit_reports', 'media': 'media_engine_streamlit_reports', @@ -221,7 +267,12 @@ def check_engines_ready() -> Dict[str, Any]: def run_report_generation(task: ReportTask, query: str, custom_template: str = ""): - """在后台线程中运行报告生成""" + """ + 在后台线程中运行报告生成。 + + 包括:检查输入→加载文档→调用ReportAgent→持久化输出→ + 推送阶段性事件。出现错误会自动推送并写状态。 + """ global current_task try: @@ -334,7 +385,7 @@ def run_report_generation(task: ReportTask, query: str, custom_template: str = " @report_bp.route('/status', methods=['GET']) def get_status(): - """获取Report Engine状态""" + """获取Report Engine状态,包括引擎就绪情况与当前任务信息。""" try: engines_status = check_engines_ready() @@ -356,7 +407,11 @@ def get_status(): @report_bp.route('/generate', methods=['POST']) def generate_report(): - """开始生成报告""" + """ + 开始生成报告。 + + 负责排队、创建后台线程、清空日志并返回SSE地址。 + """ global current_task try: @@ -443,7 +498,7 @@ def generate_report(): @report_bp.route('/progress/', methods=['GET']) def get_progress(task_id: str): - """获取报告生成进度""" + """获取报告生成进度,若任务被清理则返回一个完成态兜底。""" try: task = _get_task(task_id) if not task: @@ -479,7 +534,13 @@ def get_progress(task_id: str): @report_bp.route('/stream/', methods=['GET']) def stream_task(task_id: str): - """基于SSE的实时推送接口,向前端持续广播阶段事件。""" + """ + 基于SSE的实时推送接口。 + + - 自动补发Last-Event-ID之后的历史事件; + - 周期性发送心跳以防代理中断; + - 任务结束后自动注销监听。 + """ task = _get_task(task_id) if not task: return jsonify({'success': False, 'error': '任务不存在'}), 404 @@ -674,7 +735,7 @@ def cancel_task(task_id: str): @report_bp.route('/templates', methods=['GET']) def get_templates(): - """获取可用模板列表""" + """获取可用模板列表,便于前端展示可选Markdown骨架。""" try: if not report_agent: return jsonify({ @@ -738,7 +799,7 @@ def internal_error(error): def clear_report_log(): - """清空report.log文件""" + """清空report.log文件,方便新任务只查看本次运行日志。""" try: log_file = settings.LOG_FILE with open(log_file, 'w', encoding='utf-8') as f: @@ -750,7 +811,7 @@ def clear_report_log(): @report_bp.route('/log', methods=['GET']) def get_report_log(): - """获取report.log内容""" + """获取report.log内容,并按行去除空白返回。""" try: log_file = settings.LOG_FILE @@ -781,7 +842,7 @@ def get_report_log(): @report_bp.route('/log/clear', methods=['POST']) def clear_log(): - """手动清空日志""" + """手动清空日志,提供REST入口供前端一键重置。""" try: clear_report_log() return jsonify({ diff --git a/ReportEngine/ir/validator.py b/ReportEngine/ir/validator.py index 4db7bde..d67f802 100644 --- a/ReportEngine/ir/validator.py +++ b/ReportEngine/ir/validator.py @@ -20,6 +20,7 @@ class IRValidator: 说明: - validate_chapter返回(是否通过, 错误列表) - 错误定位采用path语法,便于快速追踪 + - 内置对heading/paragraph/list/table等所有区块的细粒度校验 """ def __init__(self, schema_version: str = IR_VERSION): diff --git a/ReportEngine/llms/__init__.py b/ReportEngine/llms/__init__.py index f601b43..d0102f5 100644 --- a/ReportEngine/llms/__init__.py +++ b/ReportEngine/llms/__init__.py @@ -1,5 +1,7 @@ """ -LLM module for the Report Engine. +Report Engine LLM子模块。 + +目前主要暴露 OpenAI 兼容的 `LLMClient` 封装。 """ from .base import LLMClient diff --git a/ReportEngine/llms/base.py b/ReportEngine/llms/base.py index bfabc2e..7823faf 100644 --- a/ReportEngine/llms/base.py +++ b/ReportEngine/llms/base.py @@ -1,5 +1,7 @@ """ -Report Engine 默认的OpenAI兼容LLM客户端封装,内置重试/流式能力。 +Report Engine 默认的OpenAI兼容LLM客户端封装。 + +提供统一的非流式/流式调用、可选重试、字节安全拼接与模型元信息查询。 """ import os @@ -107,7 +109,7 @@ class LLMClient: **kwargs: 额外参数(temperature, top_p等) Yields: - 响应文本块(str) + 响应文本块(str),调用方可边读边写入磁盘或透传到UI """ messages = [ {"role": "system", "content": system_prompt}, diff --git a/ReportEngine/nodes/__init__.py b/ReportEngine/nodes/__init__.py index 995c2c9..a24dc88 100644 --- a/ReportEngine/nodes/__init__.py +++ b/ReportEngine/nodes/__init__.py @@ -1,6 +1,7 @@ """ -Report Engine节点处理模块 -实现报告生成的各个处理步骤 +Report Engine节点处理模块。 + +封装模板选择、章节生成、文档布局、篇幅规划等流水线节点。 """ from .base_node import BaseNode, StateMutationNode diff --git a/ReportEngine/nodes/base_node.py b/ReportEngine/nodes/base_node.py index d2910ce..212185c 100644 --- a/ReportEngine/nodes/base_node.py +++ b/ReportEngine/nodes/base_node.py @@ -1,6 +1,7 @@ """ -Report Engine节点基类 -定义所有处理节点的基础接口 +Report Engine节点基类。 + +所有高阶推理节点都继承于此,统一日志、输入校验与状态变更接口。 """ from abc import ABC, abstractmethod @@ -10,7 +11,12 @@ from ..state.state import ReportState from loguru import logger class BaseNode(ABC): - """节点基类""" + """ + 节点基类。 + + 统一实现日志工具、输入/输出钩子以及LLM客户端依赖注入, + 便于所有节点只专注业务逻辑。 + """ def __init__(self, llm_client: LLMClient, node_name: str = ""): """ @@ -19,6 +25,8 @@ class BaseNode(ABC): Args: llm_client: LLM客户端 node_name: 节点名称 + + BaseNode 会保存节点名以便统一输出日志前缀。 """ self.llm_client = llm_client self.node_name = node_name or self.__class__.__name__ @@ -39,7 +47,8 @@ class BaseNode(ABC): def validate_input(self, input_data: Any) -> bool: """ - 验证输入数据 + 验证输入数据。 + 默认直接通过,子类可按需覆写实现字段检查。 Args: input_data: 输入数据 @@ -51,7 +60,8 @@ class BaseNode(ABC): def process_output(self, output: Any) -> Any: """ - 处理输出数据 + 处理输出数据。 + 子类可覆写进行结构化或校验。 Args: output: 原始输出 @@ -62,23 +72,29 @@ class BaseNode(ABC): return output def log_info(self, message: str): - """记录信息日志""" + """记录信息日志,并自动带上节点名作为前缀。""" formatted_message = f"[{self.node_name}] {message}" logger.info(formatted_message) def log_error(self, message: str): - """记录错误日志""" + """记录错误日志,便于排障。""" formatted_message = f"[{self.node_name}] {message}" logger.error(formatted_message) class StateMutationNode(BaseNode): - """带状态修改功能的节点基类""" + """ + 带状态修改功能的节点基类。 + + 适用于节点需要直接写入 ReportState 的场景。 + """ @abstractmethod def mutate_state(self, input_data: Any, state: ReportState, **kwargs) -> ReportState: """ - 修改状态 + 修改状态。 + + 子类需返回新的状态对象或在原地修改后回传,供流水线记录。 Args: input_data: 输入数据 diff --git a/ReportEngine/nodes/chapter_generation_node.py b/ReportEngine/nodes/chapter_generation_node.py index 0e38a12..a87c324 100644 --- a/ReportEngine/nodes/chapter_generation_node.py +++ b/ReportEngine/nodes/chapter_generation_node.py @@ -29,7 +29,7 @@ except ImportError: # pragma: no cover - optional dependency class ChapterJsonParseError(ValueError): - """Raised when the LLM output for a chapter cannot be parsed as valid JSON.""" + """章节LLM输出无法解析为合法JSON时抛出的异常,附带原始文本方便排查。""" def __init__(self, message: str, raw_text: Optional[str] = None): super().__init__(message) @@ -37,7 +37,15 @@ class ChapterJsonParseError(ValueError): class ChapterGenerationNode(BaseNode): - """负责按章节调用LLM并校验JSON结构""" + """ + 负责按章节调用LLM并校验JSON结构。 + + 核心能力: + - 构造章节级 payload 与提示词; + - 以流式形式写入 raw 文件并透传 delta; + - 尝试修复/解析LLM输出,并使用 IRValidator 校验; + - 对block结构做容错修复,确保最终JSON可渲染。 + """ _COLON_EQUALS_PATTERN = re.compile(r'(":\s*)=') _LINE_BREAK_SENTINEL = "__LINE_BREAK__" diff --git a/ReportEngine/nodes/document_layout_node.py b/ReportEngine/nodes/document_layout_node.py index a23409a..df0ab20 100644 --- a/ReportEngine/nodes/document_layout_node.py +++ b/ReportEngine/nodes/document_layout_node.py @@ -18,7 +18,11 @@ from .base_node import BaseNode class DocumentLayoutNode(BaseNode): - """负责生成全局标题、目录与Hero设计""" + """ + 负责生成全局标题、目录与Hero设计。 + + 结合模板切片、报告摘要与论坛讨论,指导整本书的视觉与结构基调。 + """ def __init__(self, llm_client): """记录LLM客户端并设置节点名字,供BaseNode日志使用""" diff --git a/ReportEngine/nodes/template_selection_node.py b/ReportEngine/nodes/template_selection_node.py index 832d08e..9b00a71 100644 --- a/ReportEngine/nodes/template_selection_node.py +++ b/ReportEngine/nodes/template_selection_node.py @@ -1,6 +1,8 @@ """ -模板选择节点 -根据查询内容和可用模板选择最合适的报告模板 +模板选择节点。 + +综合用户查询、三引擎报告、论坛日志与本地模板库, +调用LLM挑选最合适的报告骨架。 """ import os @@ -13,7 +15,12 @@ from ..prompts import SYSTEM_PROMPT_TEMPLATE_SELECTION class TemplateSelectionNode(BaseNode): - """模板选择处理节点""" + """ + 模板选择处理节点。 + + 负责准备模板候选列表、构建提示词、解析LLM返回结果, + 并在失败时回退到内置模板。 + """ def __init__(self, llm_client, template_dir: str = "ReportEngine/report_template"): """ @@ -28,7 +35,7 @@ class TemplateSelectionNode(BaseNode): def run(self, input_data: Dict[str, Any], **kwargs) -> Dict[str, Any]: """ - 执行模板选择 + 执行模板选择。 Args: input_data: 包含查询和报告内容的字典 @@ -37,7 +44,7 @@ class TemplateSelectionNode(BaseNode): - forum_logs: 论坛日志内容 Returns: - 选择的模板信息 + 选择的模板信息,包含名称、内容与选择理由 """ logger.info("开始模板选择...") @@ -67,7 +74,12 @@ class TemplateSelectionNode(BaseNode): def _llm_template_selection(self, query: str, reports: List[Any], forum_logs: str, available_templates: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """使用LLM进行模板选择""" + """ + 使用LLM进行模板选择。 + + 构造模板列表与报告摘要 → 调用LLM → 解析JSON → + 验证模板是否存在并返回标准结构。 + """ logger.info("尝试使用LLM进行模板选择...") # 构建模板列表 @@ -150,7 +162,11 @@ class TemplateSelectionNode(BaseNode): return self._extract_template_from_text(response, available_templates) def _clean_llm_response(self, response: str) -> str: - """清理LLM响应""" + """ + 清理LLM响应。 + + 去掉 ```json``` 包裹以及前后空白,方便 `json.loads`。 + """ # 移除可能的markdown代码块标记 if '```json' in response: response = response.split('```json')[1].split('```')[0] @@ -163,7 +179,11 @@ class TemplateSelectionNode(BaseNode): return response def _extract_template_from_text(self, response: str, available_templates: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """从文本响应中提取模板信息""" + """ + 从文本响应中提取模板信息。 + + 当LLM未输出合法JSON时,尝试匹配模板名称关键字做降级。 + """ logger.info("尝试从文本响应中提取模板信息") # 查找响应中是否包含模板名称 @@ -186,7 +206,11 @@ class TemplateSelectionNode(BaseNode): return None def _get_available_templates(self) -> List[Dict[str, Any]]: - """获取可用的模板列表""" + """ + 获取可用的模板列表。 + + 枚举模板目录下的 `.md` 文件并读取内容与描述字段。 + """ templates = [] if not os.path.exists(self.template_dir): @@ -216,7 +240,7 @@ class TemplateSelectionNode(BaseNode): return templates def _extract_template_description(self, template_name: str) -> str: - """根据模板名称生成描述""" + """根据模板名称生成描述,方便LLM理解模板定位。""" if '企业品牌' in template_name: return "适用于企业品牌声誉和形象分析" elif '市场竞争' in template_name: @@ -235,7 +259,7 @@ class TemplateSelectionNode(BaseNode): def _get_fallback_template(self) -> Dict[str, Any]: - """获取备用默认模板(空模板,让LLM自行发挥)""" + """获取备用默认模板(空模板,让LLM自行发挥)。""" logger.info("未找到合适模板,使用空模板让LLM自行发挥") return { diff --git a/ReportEngine/nodes/word_budget_node.py b/ReportEngine/nodes/word_budget_node.py index 7876160..b1802af 100644 --- a/ReportEngine/nodes/word_budget_node.py +++ b/ReportEngine/nodes/word_budget_node.py @@ -18,7 +18,11 @@ from .base_node import BaseNode class WordBudgetNode(BaseNode): - """规划各章节字数与重点""" + """ + 规划各章节字数与重点。 + + 输出总字数、全局写作准则以及每章/小节的 target/min/max 字数约束。 + """ def __init__(self, llm_client): """仅记录LLM客户端引用,方便run阶段发起请求""" diff --git a/ReportEngine/prompts/__init__.py b/ReportEngine/prompts/__init__.py index 71f3d09..76b1455 100644 --- a/ReportEngine/prompts/__init__.py +++ b/ReportEngine/prompts/__init__.py @@ -1,6 +1,7 @@ """ -Report Engine提示词模块 -定义报告生成各个阶段使用的系统提示词 +Report Engine提示词模块。 + +集中导出各阶段系统提示词与辅助函数,其他模块可直接from prompts import。 """ from .prompts import ( diff --git a/ReportEngine/prompts/prompts.py b/ReportEngine/prompts/prompts.py index 03d0321..a8170fc 100644 --- a/ReportEngine/prompts/prompts.py +++ b/ReportEngine/prompts/prompts.py @@ -1,6 +1,8 @@ """ -Report Engine 的所有提示词定义 -参考MediaEngine的结构,专门用于报告生成 +Report Engine 的所有提示词定义。 + +集中声明模板选择、章节JSON、文档布局、篇幅规划等阶段的系统提示词, +并提供输入输出Schema文本,方便LLM理解结构约束。 """ import json @@ -359,15 +361,17 @@ SYSTEM_PROMPT_WORD_BUDGET = f""" def build_chapter_user_prompt(payload: dict) -> str: """ 将章节上下文序列化为提示词输入。 + + 统一使用 `json.dumps(..., indent=2, ensure_ascii=False)`,便于LLM读取。 """ return json.dumps(payload, ensure_ascii=False, indent=2) def build_document_layout_prompt(payload: dict) -> str: - """将文档设计所需的上下文序列化为JSON字符串""" + """将文档设计所需的上下文序列化为JSON字符串,供布局节点发送给LLM。""" return json.dumps(payload, ensure_ascii=False, indent=2) def build_word_budget_prompt(payload: dict) -> str: - """将篇幅规划输入转为字符串,便于送入LLM""" + """将篇幅规划输入转为字符串,便于送入LLM并保持字段精确。""" return json.dumps(payload, ensure_ascii=False, indent=2) diff --git a/ReportEngine/renderers/__init__.py b/ReportEngine/renderers/__init__.py index 67d27c5..bcc897e 100644 --- a/ReportEngine/renderers/__init__.py +++ b/ReportEngine/renderers/__init__.py @@ -1,5 +1,7 @@ """ Report Engine渲染器集合。 + +目前仅提供 HTMLRenderer,未来可扩展为PDF/Markdown等输出。 """ from .html_renderer import HTMLRenderer diff --git a/ReportEngine/renderers/html_renderer.py b/ReportEngine/renderers/html_renderer.py index 0a9f199..5930563 100644 --- a/ReportEngine/renderers/html_renderer.py +++ b/ReportEngine/renderers/html_renderer.py @@ -11,7 +11,13 @@ from typing import Any, Dict, List class HTMLRenderer: - """Document IR → HTML 渲染器""" + """ + Document IR → HTML 渲染器。 + + - 读取 IR metadata/chapters,将结构映射为响应式HTML; + - 动态构造目录、锚点、Chart.js脚本及互动逻辑; + - 提供主题变量、编号映射等辅助功能。 + """ def __init__(self, config: Dict[str, Any] | None = None): """初始化渲染器缓存并允许注入额外配置(如主题覆盖)""" diff --git a/ReportEngine/state/__init__.py b/ReportEngine/state/__init__.py index a712106..edcab0b 100644 --- a/ReportEngine/state/__init__.py +++ b/ReportEngine/state/__init__.py @@ -1,6 +1,7 @@ """ -Report Engine状态管理模块 -定义报告生成过程中的简化状态数据结构 +Report Engine状态管理模块。 + +导出 ReportState/ReportMetadata,供Agent与Flask接口共享。 """ from .state import ReportState, ReportMetadata diff --git a/ReportEngine/state/state.py b/ReportEngine/state/state.py index fd16bdc..4045b6e 100644 --- a/ReportEngine/state/state.py +++ b/ReportEngine/state/state.py @@ -29,7 +29,11 @@ class ReportMetadata: @dataclass class ReportState: - """简化的报告状态管理""" + """ + 简化的报告状态管理。 + + 存储任务基本信息、输入、输出与元数据,供Agent与Flask层共享。 + """ # 基本信息 task_id: str = "" # 任务ID query: str = "" # 原始查询 @@ -55,24 +59,24 @@ class ReportState: self.metadata.query = self.query def mark_processing(self): - """标记为处理中""" + """标记为处理中,后台线程开始调度生成流程。""" self.status = "processing" def mark_completed(self): - """标记为完成""" + """标记为完成,同时意味着 `html_content` 已可用。""" self.status = "completed" def mark_failed(self, error_message: str = ""): - """标记为失败""" + """标记为失败,并记录最后一次错误消息。""" self.status = "failed" self.error_message = error_message def is_completed(self) -> bool: - """检查是否完成""" + """检查是否完成,包括状态为completed且存在HTML内容。""" return self.status == "completed" and bool(self.html_content) def get_progress(self) -> float: - """获取进度百分比""" + """获取进度百分比,按照模板/内容两个阶段粗略估算。""" if self.status == "completed": return 100.0 elif self.status == "processing": @@ -87,7 +91,7 @@ class ReportState: return 0.0 def to_dict(self) -> Dict[str, Any]: - """转换为字典格式""" + """转换为字典格式,方便序列化给前端。""" return { "task_id": self.task_id, "query": self.query, @@ -100,7 +104,7 @@ class ReportState: } def save_to_file(self, file_path: str): - """保存状态到文件""" + """保存状态到文件,排除HTML正文以控制体积。""" try: state_data = self.to_dict() # 不保存完整的HTML内容到状态文件(太大) @@ -113,7 +117,7 @@ class ReportState: @classmethod def load_from_file(cls, file_path: str) -> Optional["ReportState"]: - """从文件加载状态""" + """从文件加载状态,仅恢复关键字段便于调试。""" try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) @@ -135,4 +139,4 @@ class ReportState: except Exception as e: print(f"加载状态文件失败: {str(e)}") - return None \ No newline at end of file + return None diff --git a/ReportEngine/utils/__init__.py b/ReportEngine/utils/__init__.py index 24414d8..3ff3c63 100644 --- a/ReportEngine/utils/__init__.py +++ b/ReportEngine/utils/__init__.py @@ -1,6 +1,7 @@ """ -Report Engine工具模块 -包含配置管理 +Report Engine工具模块。 + +当前主要暴露配置读取逻辑,后续可扩展更多通用工具。 """