refactor(report_agent, Step4Report): simplify logging and remove subsection handling; update UI to reflect changes in section content generation

This commit is contained in:
666ghj 2026-02-06 18:13:30 +08:00
parent 54f1291967
commit f9abaf8e9f
2 changed files with 62 additions and 237 deletions

View file

@ -238,13 +238,11 @@ class ReportLogger:
section_title: str, section_title: str,
section_index: int, section_index: int,
content: str, content: str,
tool_calls_count: int, tool_calls_count: int
is_subsection: bool = False
): ):
"""记录章节/子章节内容生成完成(仅记录内容,不代表整个章节完成)""" """记录章节内容生成完成(仅记录内容,不代表整个章节完成)"""
action = "subsection_content" if is_subsection else "section_content"
self.log( self.log(
action=action, action="section_content",
stage="generating", stage="generating",
section_title=section_title, section_title=section_title,
section_index=section_index, section_index=section_index,
@ -252,8 +250,7 @@ class ReportLogger:
"content": content, # 完整内容,不截断 "content": content, # 完整内容,不截断
"content_length": len(content), "content_length": len(content),
"tool_calls_count": tool_calls_count, "tool_calls_count": tool_calls_count,
"is_subsection": is_subsection, "message": f"章节 {section_title} 内容生成完成"
"message": f"{'子章节' if is_subsection else '主章节'} {section_title} 内容生成完成"
} }
) )
@ -261,11 +258,10 @@ class ReportLogger:
self, self,
section_title: str, section_title: str,
section_index: int, section_index: int,
full_content: str, full_content: str
subsection_count: int
): ):
""" """
记录完整章节生成完成包含所有子章节的合并内容 记录章节生成完成
前端应监听此日志来判断一个章节是否真正完成并获取完整内容 前端应监听此日志来判断一个章节是否真正完成并获取完整内容
""" """
@ -275,10 +271,9 @@ class ReportLogger:
section_title=section_title, section_title=section_title,
section_index=section_index, section_index=section_index,
details={ details={
"content": full_content, # 完整章节内容(含子章节),不截断 "content": full_content,
"content_length": len(full_content), "content_length": len(full_content),
"subsection_count": subsection_count, "message": f"章节 {section_title} 生成完成"
"message": f"章节 {section_title} 完整生成完成(含 {subsection_count} 个子章节)"
} }
) )
@ -404,13 +399,11 @@ class ReportSection:
"""报告章节""" """报告章节"""
title: str title: str
content: str = "" content: str = ""
subsections: List['ReportSection'] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
return { return {
"title": self.title, "title": self.title,
"content": self.content, "content": self.content
"subsections": [s.to_dict() for s in self.subsections]
} }
def to_markdown(self, level: int = 2) -> str: def to_markdown(self, level: int = 2) -> str:
@ -418,8 +411,6 @@ class ReportSection:
md = f"{'#' * level} {self.title}\n\n" md = f"{'#' * level} {self.title}\n\n"
if self.content: if self.content:
md += f"{self.content}\n\n" md += f"{self.content}\n\n"
for sub in self.subsections:
md += sub.to_markdown(level + 1)
return md return md
@ -854,8 +845,8 @@ class ReportAgent:
- 不是泛泛而谈的舆情综述 - 不是泛泛而谈的舆情综述
章节数量限制 章节数量限制
- 最少2个章节最多5个章节 - 最少2个章节最多5个章节
- 每个章节可以有0-2个子章节 - 不需要子章节每个章节直接撰写完整内容
- 内容要精炼聚焦于核心预测发现 - 内容要精炼聚焦于核心预测发现
- 章节结构由你根据预测结果自主设计 - 章节结构由你根据预测结果自主设计
@ -866,10 +857,7 @@ class ReportAgent:
"sections": [ "sections": [
{ {
"title": "章节标题", "title": "章节标题",
"description": "章节内容描述", "description": "章节内容描述"
"subsections": [
{"title": "子章节标题", "description": "子章节描述"}
]
} }
] ]
} }
@ -912,17 +900,9 @@ class ReportAgent:
# 解析大纲 # 解析大纲
sections = [] sections = []
for section_data in response.get("sections", []): for section_data in response.get("sections", []):
subsections = []
for sub_data in section_data.get("subsections", []):
subsections.append(ReportSection(
title=sub_data.get("title", ""),
content=""
))
sections.append(ReportSection( sections.append(ReportSection(
title=section_data.get("title", ""), title=section_data.get("title", ""),
content="", content=""
subsections=subsections
)) ))
outline = ReportOutline( outline = ReportOutline(
@ -1241,16 +1221,13 @@ class ReportAgent:
final_answer = response.split("Final Answer:")[-1].strip() final_answer = response.split("Final Answer:")[-1].strip()
logger.info(f"章节 {section.title} 生成完成(工具调用: {tool_calls_count}次)") logger.info(f"章节 {section.title} 生成完成(工具调用: {tool_calls_count}次)")
# 记录章节内容生成完成日志(注意:这只是内容完成,不代表整个章节完成) # 记录章节内容生成完成日志
# 如果是子章节section_index >= 100
is_subsection = section_index >= 100
if self.report_logger: if self.report_logger:
self.report_logger.log_section_content( self.report_logger.log_section_content(
section_title=section.title, section_title=section.title,
section_index=section_index, section_index=section_index,
content=final_answer, content=final_answer,
tool_calls_count=tool_calls_count, tool_calls_count=tool_calls_count
is_subsection=is_subsection
) )
return final_answer return final_answer
@ -1359,15 +1336,13 @@ class ReportAgent:
else: else:
final_answer = response final_answer = response
# 记录章节内容生成完成日志(注意:这只是内容完成,不代表整个章节完成) # 记录章节内容生成完成日志
is_subsection = section_index >= 100
if self.report_logger: if self.report_logger:
self.report_logger.log_section_content( self.report_logger.log_section_content(
section_title=section.title, section_title=section.title,
section_index=section_index, section_index=section_index,
content=final_answer, content=final_answer,
tool_calls_count=tool_calls_count, tool_calls_count=tool_calls_count
is_subsection=is_subsection
) )
return final_answer return final_answer
@ -1512,61 +1487,21 @@ class ReportAgent:
section.content = section_content section.content = section_content
generated_sections.append(f"## {section.title}\n\n{section_content}") generated_sections.append(f"## {section.title}\n\n{section_content}")
# 如果有子章节,也一并生成并合并到主章节中 # 保存章节
subsection_contents = [] ReportManager.save_section(report_id, section_num, section)
for j, subsection in enumerate(section.subsections):
subsection_num = j + 1
if progress_callback:
progress_callback(
"generating",
base_progress + int(((j + 1) / max(len(section.subsections), 1)) * 5),
f"正在生成子章节: {subsection.title}"
)
ReportManager.update_progress(
report_id, "generating",
base_progress + int(((j + 1) / max(len(section.subsections), 1)) * 5),
f"正在生成子章节: {subsection.title}",
current_section=subsection.title,
completed_sections=completed_section_titles
)
subsection_content = self._generate_section_react(
section=subsection,
outline=outline,
previous_sections=generated_sections,
progress_callback=None,
section_index=section_num * 100 + subsection_num # 子章节索引
)
subsection.content = subsection_content
generated_sections.append(f"### {subsection.title}\n\n{subsection_content}")
subsection_contents.append((subsection.title, subsection_content))
completed_section_titles.append(f" └─ {subsection.title}")
logger.info(f"子章节已生成: {subsection.title}")
# 【关键】将主章节和所有子章节合并保存到一个文件
ReportManager.save_section_with_subsections(
report_id, section_num, section, subsection_contents
)
completed_section_titles.append(section.title) completed_section_titles.append(section.title)
# 【重要】记录完整章节完成日志,包含合并后的完整内容 # 记录章节完成日志
# 构建完整章节内容(主章节 + 所有子章节) full_section_content = f"## {section.title}\n\n{section_content}"
full_section_content = f"## {section.title}\n\n{section_content}\n\n"
for sub_title, sub_content in subsection_contents:
full_section_content += f"### {sub_title}\n\n{sub_content}\n\n"
if self.report_logger: if self.report_logger:
self.report_logger.log_section_full_complete( self.report_logger.log_section_full_complete(
section_title=section.title, section_title=section.title,
section_index=section_num, section_index=section_num,
full_content=full_section_content.strip(), full_content=full_section_content.strip()
subsection_count=len(subsection_contents)
) )
logger.info(f"章节已保存(包含{len(subsection_contents)}个子章节): {report_id}/section_{section_num:02d}.md") logger.info(f"章节已保存: {report_id}/section_{section_num:02d}.md")
# 更新进度 # 更新进度
ReportManager.update_progress( ReportManager.update_progress(
@ -2000,12 +1935,10 @@ class ReportManager:
cls, cls,
report_id: str, report_id: str,
section_index: int, section_index: int,
section: ReportSection, section: ReportSection
is_subsection: bool = False,
parent_index: int = None
) -> str: ) -> str:
""" """
保存单个章节不推荐使用建议使用 save_section_with_subsections 保存单个章节
在每个章节生成完成后立即调用实现分章节输出 在每个章节生成完成后立即调用实现分章节输出
@ -2013,29 +1946,20 @@ class ReportManager:
report_id: 报告ID report_id: 报告ID
section_index: 章节索引从1开始 section_index: 章节索引从1开始
section: 章节对象 section: 章节对象
is_subsection: 是否是子章节
parent_index: 父章节索引子章节时使用
Returns: Returns:
保存的文件路径 保存的文件路径
""" """
cls._ensure_report_folder(report_id) cls._ensure_report_folder(report_id)
# 确定章节级别和标题格式
if is_subsection and parent_index is not None:
level = "###"
file_suffix = f"section_{parent_index:02d}_{section_index:02d}.md"
else:
level = "##"
file_suffix = f"section_{section_index:02d}.md"
# 构建章节Markdown内容 - 清理可能存在的重复标题 # 构建章节Markdown内容 - 清理可能存在的重复标题
cleaned_content = cls._clean_section_content(section.content, section.title) cleaned_content = cls._clean_section_content(section.content, section.title)
md_content = f"{level} {section.title}\n\n" md_content = f"## {section.title}\n\n"
if cleaned_content: if cleaned_content:
md_content += f"{cleaned_content}\n\n" md_content += f"{cleaned_content}\n\n"
# 保存文件 # 保存文件
file_suffix = f"section_{section_index:02d}.md"
file_path = os.path.join(cls._get_report_folder(report_id), file_suffix) file_path = os.path.join(cls._get_report_folder(report_id), file_suffix)
with open(file_path, 'w', encoding='utf-8') as f: with open(file_path, 'w', encoding='utf-8') as f:
f.write(md_content) f.write(md_content)
@ -2043,50 +1967,6 @@ class ReportManager:
logger.info(f"章节已保存: {report_id}/{file_suffix}") logger.info(f"章节已保存: {report_id}/{file_suffix}")
return file_path return file_path
@classmethod
def save_section_with_subsections(
cls,
report_id: str,
section_index: int,
section: ReportSection,
subsection_contents: List[tuple]
) -> str:
"""
保存章节及其所有子章节到一个文件
Args:
report_id: 报告ID
section_index: 章节索引从1开始
section: 主章节对象
subsection_contents: 子章节列表 [(title, content), ...]
Returns:
保存的文件路径
"""
cls._ensure_report_folder(report_id)
# 构建主章节Markdown内容
cleaned_main_content = cls._clean_section_content(section.content, section.title)
md_content = f"## {section.title}\n\n"
if cleaned_main_content:
md_content += f"{cleaned_main_content}\n\n"
# 添加所有子章节内容
for sub_title, sub_content in subsection_contents:
cleaned_sub_content = cls._clean_section_content(sub_content, sub_title)
md_content += f"### {sub_title}\n\n"
if cleaned_sub_content:
md_content += f"{cleaned_sub_content}\n\n"
# 保存文件
file_suffix = f"section_{section_index:02d}.md"
file_path = os.path.join(cls._get_report_folder(report_id), file_suffix)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(md_content)
logger.info(f"章节已保存(含{len(subsection_contents)}个子章节): {report_id}/{file_suffix}")
return file_path
@classmethod @classmethod
def _clean_section_content(cls, content: str, section_title: str) -> str: def _clean_section_content(cls, content: str, section_title: str) -> str:
""" """
@ -2217,14 +2097,11 @@ class ReportManager:
# 从文件名解析章节索引 # 从文件名解析章节索引
parts = filename.replace('.md', '').split('_') parts = filename.replace('.md', '').split('_')
section_index = int(parts[1]) section_index = int(parts[1])
subsection_index = int(parts[2]) if len(parts) > 2 else None
sections.append({ sections.append({
"filename": filename, "filename": filename,
"section_index": section_index, "section_index": section_index,
"subsection_index": subsection_index, "content": content
"content": content,
"is_subsection": subsection_index is not None
}) })
return sections return sections
@ -2243,12 +2120,9 @@ class ReportManager:
md_content += f"> {outline.summary}\n\n" md_content += f"> {outline.summary}\n\n"
md_content += f"---\n\n" md_content += f"---\n\n"
# 按顺序读取所有章节文件(只读取主章节文件,不读取子章节文件) # 按顺序读取所有章节文件
sections = cls.get_generated_sections(report_id) sections = cls.get_generated_sections(report_id)
for section_info in sections: for section_info in sections:
# 跳过子章节文件(已合并到主章节中)
if section_info.get("is_subsection", False):
continue
md_content += section_info["content"] md_content += section_info["content"]
# 后处理:清理整个报告的标题问题 # 后处理:清理整个报告的标题问题
@ -2288,8 +2162,6 @@ class ReportManager:
section_titles = set() section_titles = set()
for section in outline.sections: for section in outline.sections:
section_titles.add(section.title) section_titles.add(section.title)
for sub in section.subsections:
section_titles.add(sub.title)
i = 0 i = 0
while i < len(lines): while i < len(lines):
@ -2432,14 +2304,9 @@ class ReportManager:
outline_data = data['outline'] outline_data = data['outline']
sections = [] sections = []
for s in outline_data.get('sections', []): for s in outline_data.get('sections', []):
subsections = [
ReportSection(title=sub['title'], content=sub.get('content', ''))
for sub in s.get('subsections', [])
]
sections.append(ReportSection( sections.append(ReportSection(
title=s['title'], title=s['title'],
content=s.get('content', ''), content=s.get('content', '')
subsections=subsections
)) ))
outline = ReportOutline( outline = ReportOutline(
title=outline_data['title'], title=outline_data['title'],

View file

@ -194,26 +194,24 @@
</div> </div>
</template> </template>
<!-- Section/Subsection Content Generated (内容生成完成但整个章节可能还没完成) --> <!-- Section Content Generated (内容生成完成但整个章节可能还没完成) -->
<template v-if="log.action === 'section_content' || log.action === 'subsection_content'"> <template v-if="log.action === 'section_content'">
<div class="section-tag content-ready" :class="{ 'is-subsection': log.action === 'subsection_content' }"> <div class="section-tag content-ready">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 20h9"></path> <path d="M12 20h9"></path>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path> <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
</svg> </svg>
<span class="tag-title">{{ log.section_title }}</span> <span class="tag-title">{{ log.section_title }}</span>
<span v-if="log.action === 'subsection_content'" class="tag-sub">(subsection)</span>
</div> </div>
</template> </template>
<!-- Section Complete (完整章节生成完成含所有子章节) --> <!-- Section Complete (章节生成完成) -->
<template v-if="log.action === 'section_complete'"> <template v-if="log.action === 'section_complete'">
<div class="section-tag completed"> <div class="section-tag completed">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline> <polyline points="20 6 9 17 4 12"></polyline>
</svg> </svg>
<span class="tag-title">{{ log.section_title }}</span> <span class="tag-title">{{ log.section_title }}</span>
<span v-if="log.details?.subsection_count > 0" class="tag-sub">(+{{ log.details.subsection_count }} subsections)</span>
</div> </div>
</template> </template>
@ -1800,20 +1798,6 @@ const isSectionCompleted = (sectionIndex) => {
return !!generatedSections.value[sectionIndex] return !!generatedSections.value[sectionIndex]
} }
// section_index
// 1,2,3... 101,10211,2
const getMainSectionIndex = (sectionIndex) => {
if (sectionIndex >= 100) {
return Math.floor(sectionIndex / 100)
}
return sectionIndex
}
//
const isSubsection = (sectionIndex) => {
return sectionIndex >= 100
}
const formatTime = (timestamp) => { const formatTime = (timestamp) => {
if (!timestamp) return '' if (!timestamp) return ''
try { try {
@ -1929,7 +1913,6 @@ const getActionLabel = (action) => {
'planning_complete': 'Plan Complete', 'planning_complete': 'Plan Complete',
'section_start': 'Section Start', 'section_start': 'Section Start',
'section_content': 'Content Ready', 'section_content': 'Content Ready',
'subsection_content': 'Subsection Ready',
'section_complete': 'Section Done', 'section_complete': 'Section Done',
'tool_call': 'Tool Call', 'tool_call': 'Tool Call',
'tool_result': 'Tool Result', 'tool_result': 'Tool Result',
@ -1968,32 +1951,17 @@ const fetchAgentLog = async () => {
} }
if (log.action === 'section_start') { if (log.action === 'section_start') {
// currentSectionIndex.value = log.section_index
// 1,2,3... 101,10211,2
const mainIndex = getMainSectionIndex(log.section_index)
currentSectionIndex.value = mainIndex
} }
// section_content / subsection_content - // section_complete -
// generatedSections
if (log.action === 'section_content' || log.action === 'subsection_content') {
// loading
// section_complete
}
// section_complete -
// details.content
// complete complete
if (log.action === 'section_complete') { if (log.action === 'section_complete') {
const mainIndex = getMainSectionIndex(log.section_index) if (log.details?.content) {
// section_index < 100 loading generatedSections.value[log.section_index] = log.details.content
if (!isSubsection(log.section_index) && log.details?.content) {
generatedSections.value[mainIndex] = log.details.content
// //
expandedContent.value.add(mainIndex - 1) expandedContent.value.add(log.section_index - 1)
currentSectionIndex.value = null currentSectionIndex.value = null
} }
// currentSectionIndex loading
} }
if (log.action === 'report_complete') { if (log.action === 'report_complete') {
@ -3055,10 +3023,6 @@ watch(() => props.reportId, (newId) => {
color: var(--wf-active-dot); color: var(--wf-active-dot);
} }
.section-tag.content-ready.is-subsection {
background: var(--wf-active-bg);
border-color: var(--wf-active-border);
}
.section-tag.completed { .section-tag.completed {
background: #ECFDF5; background: #ECFDF5;
@ -3085,12 +3049,6 @@ watch(() => props.reportId, (newId) => {
color: #374151; color: #374151;
} }
.tag-sub {
font-size: 11px;
color: #6B7280;
margin-left: 4px;
}
.tool-badge { .tool-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;