Toggle navigation
Toggle navigation
This project
Loading...
Sign in
万朱浩
/
Venue-Ops
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
马一丁
2025-11-13 22:49:59 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
e267b1fc0499ff6d674913484e6da9c76525870f
e267b1fc
1 parent
82152547
Add Comments
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
500 additions
and
145 deletions
ReportEngine/__init__.py
ReportEngine/agent.py
ReportEngine/core/__init__.py
ReportEngine/core/chapter_storage.py
ReportEngine/core/stitcher.py
ReportEngine/core/template_parser.py
ReportEngine/flask_interface.py
ReportEngine/ir/validator.py
ReportEngine/llms/__init__.py
ReportEngine/llms/base.py
ReportEngine/nodes/__init__.py
ReportEngine/nodes/base_node.py
ReportEngine/nodes/chapter_generation_node.py
ReportEngine/nodes/document_layout_node.py
ReportEngine/nodes/template_selection_node.py
ReportEngine/nodes/word_budget_node.py
ReportEngine/prompts/__init__.py
ReportEngine/prompts/prompts.py
ReportEngine/renderers/__init__.py
ReportEngine/renderers/html_renderer.py
ReportEngine/state/__init__.py
ReportEngine/state/state.py
ReportEngine/utils/__init__.py
ReportEngine/__init__.py
View file @
e267b1f
"""
Report Engine
一个智能报告生成AI代理实现
基于三个子agent的输出和论坛日志生成综合HTML报告
Report Engine。
一个智能报告生成AI代理实现,聚合 Query/Media/Insight 三个子引擎的
Markdown 与论坛讨论,最终落地结构化HTML报告。
"""
from
.agent
import
ReportAgent
,
create_agent
...
...
ReportEngine/agent.py
View file @
e267b1f
"""
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
()
# 以空配置初始化,而从从环境变量初始化
...
...
ReportEngine/core/__init__.py
View file @
e267b1f
"""
Report Engine核心工具集合。
包含模板切片、章节存储等基础能力,供agent流水线复用。
该包封装了模板切片、章节存储与章节装订三大基础能力,
所有上层节点都会复用这些工具保证结构一致。
"""
from
.template_parser
import
TemplateSection
,
parse_template_sections
...
...
ReportEngine/core/chapter_storage.py
View file @
e267b1f
...
...
@@ -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"
,
[])
...
...
ReportEngine/core/stitcher.py
View file @
e267b1f
"""
章节装订器:负责把多个章节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
:
...
...
ReportEngine/core/template_parser.py
View file @
e267b1f
...
...
@@ -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
...
...
ReportEngine/flask_interface.py
View file @
e267b1f
"""
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/<task_id>'
,
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/<task_id>'
,
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
({
...
...
ReportEngine/ir/validator.py
View file @
e267b1f
...
...
@@ -20,6 +20,7 @@ class IRValidator:
说明:
- validate_chapter返回(是否通过, 错误列表)
- 错误定位采用path语法,便于快速追踪
- 内置对heading/paragraph/list/table等所有区块的细粒度校验
"""
def
__init__
(
self
,
schema_version
:
str
=
IR_VERSION
):
...
...
ReportEngine/llms/__init__.py
View file @
e267b1f
"""
LLM module for the Report Engine.
Report Engine LLM子模块。
目前主要暴露 OpenAI 兼容的 `LLMClient` 封装。
"""
from
.base
import
LLMClient
...
...
ReportEngine/llms/base.py
View file @
e267b1f
"""
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
},
...
...
ReportEngine/nodes/__init__.py
View file @
e267b1f
"""
Report Engine节点处理模块
实现报告生成的各个处理步骤
Report Engine节点处理模块。
封装模板选择、章节生成、文档布局、篇幅规划等流水线节点。
"""
from
.base_node
import
BaseNode
,
StateMutationNode
...
...
ReportEngine/nodes/base_node.py
View file @
e267b1f
"""
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: 输入数据
...
...
ReportEngine/nodes/chapter_generation_node.py
View file @
e267b1f
...
...
@@ -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__"
...
...
ReportEngine/nodes/document_layout_node.py
View file @
e267b1f
...
...
@@ -18,7 +18,11 @@ from .base_node import BaseNode
class
DocumentLayoutNode
(
BaseNode
):
"""负责生成全局标题、目录与Hero设计"""
"""
负责生成全局标题、目录与Hero设计。
结合模板切片、报告摘要与论坛讨论,指导整本书的视觉与结构基调。
"""
def
__init__
(
self
,
llm_client
):
"""记录LLM客户端并设置节点名字,供BaseNode日志使用"""
...
...
ReportEngine/nodes/template_selection_node.py
View file @
e267b1f
"""
模板选择节点
根据查询内容和可用模板选择最合适的报告模板
模板选择节点。
综合用户查询、三引擎报告、论坛日志与本地模板库,
调用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
{
...
...
ReportEngine/nodes/word_budget_node.py
View file @
e267b1f
...
...
@@ -18,7 +18,11 @@ from .base_node import BaseNode
class
WordBudgetNode
(
BaseNode
):
"""规划各章节字数与重点"""
"""
规划各章节字数与重点。
输出总字数、全局写作准则以及每章/小节的 target/min/max 字数约束。
"""
def
__init__
(
self
,
llm_client
):
"""仅记录LLM客户端引用,方便run阶段发起请求"""
...
...
ReportEngine/prompts/__init__.py
View file @
e267b1f
"""
Report Engine提示词模块
定义报告生成各个阶段使用的系统提示词
Report Engine提示词模块。
集中导出各阶段系统提示词与辅助函数,其他模块可直接from prompts import。
"""
from
.prompts
import
(
...
...
ReportEngine/prompts/prompts.py
View file @
e267b1f
"""
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
)
...
...
ReportEngine/renderers/__init__.py
View file @
e267b1f
"""
Report Engine渲染器集合。
目前仅提供 HTMLRenderer,未来可扩展为PDF/Markdown等输出。
"""
from
.html_renderer
import
HTMLRenderer
...
...
ReportEngine/renderers/html_renderer.py
View file @
e267b1f
...
...
@@ -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
):
"""初始化渲染器缓存并允许注入额外配置(如主题覆盖)"""
...
...
ReportEngine/state/__init__.py
View file @
e267b1f
"""
Report Engine状态管理模块
定义报告生成过程中的简化状态数据结构
Report Engine状态管理模块。
导出 ReportState/ReportMetadata,供Agent与Flask接口共享。
"""
from
.state
import
ReportState
,
ReportMetadata
...
...
ReportEngine/state/state.py
View file @
e267b1f
...
...
@@ -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
...
...
ReportEngine/utils/__init__.py
View file @
e267b1f
"""
Report Engine工具模块
包含配置管理
Report Engine工具模块。
当前主要暴露配置读取逻辑,后续可扩展更多通用工具。
"""
...
...
Please
register
or
login
to post a comment