refactor(report_agent, Step4Report): simplify logging and remove subsection handling; update UI to reflect changes in section content generation
This commit is contained in:
parent
54f1291967
commit
f9abaf8e9f
2 changed files with 62 additions and 237 deletions
|
|
@ -238,13 +238,11 @@ class ReportLogger:
|
|||
section_title: str,
|
||||
section_index: int,
|
||||
content: str,
|
||||
tool_calls_count: int,
|
||||
is_subsection: bool = False
|
||||
tool_calls_count: int
|
||||
):
|
||||
"""记录章节/子章节内容生成完成(仅记录内容,不代表整个章节完成)"""
|
||||
action = "subsection_content" if is_subsection else "section_content"
|
||||
"""记录章节内容生成完成(仅记录内容,不代表整个章节完成)"""
|
||||
self.log(
|
||||
action=action,
|
||||
action="section_content",
|
||||
stage="generating",
|
||||
section_title=section_title,
|
||||
section_index=section_index,
|
||||
|
|
@ -252,8 +250,7 @@ class ReportLogger:
|
|||
"content": content, # 完整内容,不截断
|
||||
"content_length": len(content),
|
||||
"tool_calls_count": tool_calls_count,
|
||||
"is_subsection": is_subsection,
|
||||
"message": f"{'子章节' if is_subsection else '主章节'} {section_title} 内容生成完成"
|
||||
"message": f"章节 {section_title} 内容生成完成"
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -261,12 +258,11 @@ class ReportLogger:
|
|||
self,
|
||||
section_title: str,
|
||||
section_index: int,
|
||||
full_content: str,
|
||||
subsection_count: int
|
||||
full_content: str
|
||||
):
|
||||
"""
|
||||
记录完整章节生成完成(包含所有子章节的合并内容)
|
||||
|
||||
记录章节生成完成
|
||||
|
||||
前端应监听此日志来判断一个章节是否真正完成,并获取完整内容
|
||||
"""
|
||||
self.log(
|
||||
|
|
@ -275,10 +271,9 @@ class ReportLogger:
|
|||
section_title=section_title,
|
||||
section_index=section_index,
|
||||
details={
|
||||
"content": full_content, # 完整章节内容(含子章节),不截断
|
||||
"content": full_content,
|
||||
"content_length": len(full_content),
|
||||
"subsection_count": subsection_count,
|
||||
"message": f"章节 {section_title} 完整生成完成(含 {subsection_count} 个子章节)"
|
||||
"message": f"章节 {section_title} 生成完成"
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -404,22 +399,18 @@ class ReportSection:
|
|||
"""报告章节"""
|
||||
title: str
|
||||
content: str = ""
|
||||
subsections: List['ReportSection'] = field(default_factory=list)
|
||||
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"title": self.title,
|
||||
"content": self.content,
|
||||
"subsections": [s.to_dict() for s in self.subsections]
|
||||
"content": self.content
|
||||
}
|
||||
|
||||
|
||||
def to_markdown(self, level: int = 2) -> str:
|
||||
"""转换为Markdown格式"""
|
||||
md = f"{'#' * level} {self.title}\n\n"
|
||||
if self.content:
|
||||
md += f"{self.content}\n\n"
|
||||
for sub in self.subsections:
|
||||
md += sub.to_markdown(level + 1)
|
||||
return md
|
||||
|
||||
|
||||
|
|
@ -854,8 +845,8 @@ class ReportAgent:
|
|||
- ❌ 不是泛泛而谈的舆情综述
|
||||
|
||||
【章节数量限制】
|
||||
- 最少2个主章节,最多5个主章节
|
||||
- 每个章节可以有0-2个子章节
|
||||
- 最少2个章节,最多5个章节
|
||||
- 不需要子章节,每个章节直接撰写完整内容
|
||||
- 内容要精炼,聚焦于核心预测发现
|
||||
- 章节结构由你根据预测结果自主设计
|
||||
|
||||
|
|
@ -866,10 +857,7 @@ class ReportAgent:
|
|||
"sections": [
|
||||
{
|
||||
"title": "章节标题",
|
||||
"description": "章节内容描述",
|
||||
"subsections": [
|
||||
{"title": "子章节标题", "description": "子章节描述"}
|
||||
]
|
||||
"description": "章节内容描述"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -912,17 +900,9 @@ class ReportAgent:
|
|||
# 解析大纲
|
||||
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(
|
||||
title=section_data.get("title", ""),
|
||||
content="",
|
||||
subsections=subsections
|
||||
content=""
|
||||
))
|
||||
|
||||
outline = ReportOutline(
|
||||
|
|
@ -1241,20 +1221,17 @@ class ReportAgent:
|
|||
final_answer = response.split("Final Answer:")[-1].strip()
|
||||
logger.info(f"章节 {section.title} 生成完成(工具调用: {tool_calls_count}次)")
|
||||
|
||||
# 记录章节内容生成完成日志(注意:这只是内容完成,不代表整个章节完成)
|
||||
# 如果是子章节,section_index >= 100
|
||||
is_subsection = section_index >= 100
|
||||
# 记录章节内容生成完成日志
|
||||
if self.report_logger:
|
||||
self.report_logger.log_section_content(
|
||||
section_title=section.title,
|
||||
section_index=section_index,
|
||||
content=final_answer,
|
||||
tool_calls_count=tool_calls_count,
|
||||
is_subsection=is_subsection
|
||||
tool_calls_count=tool_calls_count
|
||||
)
|
||||
|
||||
|
||||
return final_answer
|
||||
|
||||
|
||||
# 解析工具调用
|
||||
tool_calls = self._parse_tool_calls(response)
|
||||
|
||||
|
|
@ -1359,15 +1336,13 @@ class ReportAgent:
|
|||
else:
|
||||
final_answer = response
|
||||
|
||||
# 记录章节内容生成完成日志(注意:这只是内容完成,不代表整个章节完成)
|
||||
is_subsection = section_index >= 100
|
||||
# 记录章节内容生成完成日志
|
||||
if self.report_logger:
|
||||
self.report_logger.log_section_content(
|
||||
section_title=section.title,
|
||||
section_index=section_index,
|
||||
content=final_answer,
|
||||
tool_calls_count=tool_calls_count,
|
||||
is_subsection=is_subsection
|
||||
tool_calls_count=tool_calls_count
|
||||
)
|
||||
|
||||
return final_answer
|
||||
|
|
@ -1511,62 +1486,22 @@ class ReportAgent:
|
|||
|
||||
section.content = section_content
|
||||
generated_sections.append(f"## {section.title}\n\n{section_content}")
|
||||
|
||||
# 如果有子章节,也一并生成并合并到主章节中
|
||||
subsection_contents = []
|
||||
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
|
||||
)
|
||||
|
||||
# 保存章节
|
||||
ReportManager.save_section(report_id, section_num, section)
|
||||
completed_section_titles.append(section.title)
|
||||
|
||||
# 【重要】记录完整章节完成日志,包含合并后的完整内容
|
||||
# 构建完整章节内容(主章节 + 所有子章节)
|
||||
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"
|
||||
|
||||
|
||||
# 记录章节完成日志
|
||||
full_section_content = f"## {section.title}\n\n{section_content}"
|
||||
|
||||
if self.report_logger:
|
||||
self.report_logger.log_section_full_complete(
|
||||
section_title=section.title,
|
||||
section_index=section_num,
|
||||
full_content=full_section_content.strip(),
|
||||
subsection_count=len(subsection_contents)
|
||||
full_content=full_section_content.strip()
|
||||
)
|
||||
|
||||
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(
|
||||
|
|
@ -1997,94 +1932,39 @@ class ReportManager:
|
|||
|
||||
@classmethod
|
||||
def save_section(
|
||||
cls,
|
||||
report_id: str,
|
||||
section_index: int,
|
||||
section: ReportSection,
|
||||
is_subsection: bool = False,
|
||||
parent_index: int = None
|
||||
cls,
|
||||
report_id: str,
|
||||
section_index: int,
|
||||
section: ReportSection
|
||||
) -> str:
|
||||
"""
|
||||
保存单个章节(不推荐使用,建议使用 save_section_with_subsections)
|
||||
|
||||
保存单个章节
|
||||
|
||||
在每个章节生成完成后立即调用,实现分章节输出
|
||||
|
||||
|
||||
Args:
|
||||
report_id: 报告ID
|
||||
section_index: 章节索引(从1开始)
|
||||
section: 章节对象
|
||||
is_subsection: 是否是子章节
|
||||
parent_index: 父章节索引(子章节时使用)
|
||||
|
||||
|
||||
Returns:
|
||||
保存的文件路径
|
||||
"""
|
||||
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内容 - 清理可能存在的重复标题
|
||||
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:
|
||||
md_content += f"{cleaned_content}\n\n"
|
||||
|
||||
# 保存文件
|
||||
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"章节已保存: {report_id}/{file_suffix}")
|
||||
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}")
|
||||
|
||||
logger.info(f"章节已保存: {report_id}/{file_suffix}")
|
||||
return file_path
|
||||
|
||||
@classmethod
|
||||
|
|
@ -2213,20 +2093,17 @@ class ReportManager:
|
|||
file_path = os.path.join(folder, filename)
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
|
||||
# 从文件名解析章节索引
|
||||
parts = filename.replace('.md', '').split('_')
|
||||
section_index = int(parts[1])
|
||||
subsection_index = int(parts[2]) if len(parts) > 2 else None
|
||||
|
||||
|
||||
sections.append({
|
||||
"filename": filename,
|
||||
"section_index": section_index,
|
||||
"subsection_index": subsection_index,
|
||||
"content": content,
|
||||
"is_subsection": subsection_index is not None
|
||||
"content": content
|
||||
})
|
||||
|
||||
|
||||
return sections
|
||||
|
||||
@classmethod
|
||||
|
|
@ -2243,12 +2120,9 @@ class ReportManager:
|
|||
md_content += f"> {outline.summary}\n\n"
|
||||
md_content += f"---\n\n"
|
||||
|
||||
# 按顺序读取所有章节文件(只读取主章节文件,不读取子章节文件)
|
||||
# 按顺序读取所有章节文件
|
||||
sections = cls.get_generated_sections(report_id)
|
||||
for section_info in sections:
|
||||
# 跳过子章节文件(已合并到主章节中)
|
||||
if section_info.get("is_subsection", False):
|
||||
continue
|
||||
md_content += section_info["content"]
|
||||
|
||||
# 后处理:清理整个报告的标题问题
|
||||
|
|
@ -2288,8 +2162,6 @@ class ReportManager:
|
|||
section_titles = set()
|
||||
for section in outline.sections:
|
||||
section_titles.add(section.title)
|
||||
for sub in section.subsections:
|
||||
section_titles.add(sub.title)
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
|
|
@ -2432,14 +2304,9 @@ class ReportManager:
|
|||
outline_data = data['outline']
|
||||
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(
|
||||
title=s['title'],
|
||||
content=s.get('content', ''),
|
||||
subsections=subsections
|
||||
content=s.get('content', '')
|
||||
))
|
||||
outline = ReportOutline(
|
||||
title=outline_data['title'],
|
||||
|
|
|
|||
|
|
@ -194,26 +194,24 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Section/Subsection Content Generated (内容生成完成,但整个章节可能还没完成) -->
|
||||
<template v-if="log.action === 'section_content' || log.action === 'subsection_content'">
|
||||
<div class="section-tag content-ready" :class="{ 'is-subsection': log.action === 'subsection_content' }">
|
||||
<!-- Section Content Generated (内容生成完成,但整个章节可能还没完成) -->
|
||||
<template v-if="log.action === 'section_content'">
|
||||
<div class="section-tag content-ready">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
</svg>
|
||||
<span class="tag-title">{{ log.section_title }}</span>
|
||||
<span v-if="log.action === 'subsection_content'" class="tag-sub">(subsection)</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Section Complete (完整章节生成完成,含所有子章节) -->
|
||||
|
||||
<!-- Section Complete (章节生成完成) -->
|
||||
<template v-if="log.action === 'section_complete'">
|
||||
<div class="section-tag completed">
|
||||
<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>
|
||||
</svg>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
|
@ -1800,20 +1798,6 @@ const isSectionCompleted = (sectionIndex) => {
|
|||
return !!generatedSections.value[sectionIndex]
|
||||
}
|
||||
|
||||
// 从 section_index 获取主章节索引
|
||||
// 后端编号方案:主章节 1,2,3... 子章节 101,102(第1章子章节1,2)
|
||||
const getMainSectionIndex = (sectionIndex) => {
|
||||
if (sectionIndex >= 100) {
|
||||
return Math.floor(sectionIndex / 100)
|
||||
}
|
||||
return sectionIndex
|
||||
}
|
||||
|
||||
// 判断是否是子章节
|
||||
const isSubsection = (sectionIndex) => {
|
||||
return sectionIndex >= 100
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return ''
|
||||
try {
|
||||
|
|
@ -1929,7 +1913,6 @@ const getActionLabel = (action) => {
|
|||
'planning_complete': 'Plan Complete',
|
||||
'section_start': 'Section Start',
|
||||
'section_content': 'Content Ready',
|
||||
'subsection_content': 'Subsection Ready',
|
||||
'section_complete': 'Section Done',
|
||||
'tool_call': 'Tool Call',
|
||||
'tool_result': 'Tool Result',
|
||||
|
|
@ -1968,32 +1951,17 @@ const fetchAgentLog = async () => {
|
|||
}
|
||||
|
||||
if (log.action === 'section_start') {
|
||||
// 无论是主章节还是子章节开始,都映射到主章节索引
|
||||
// 后端编号:主章节 1,2,3... 子章节 101,102(第1章子章节1,2)
|
||||
const mainIndex = getMainSectionIndex(log.section_index)
|
||||
currentSectionIndex.value = mainIndex
|
||||
currentSectionIndex.value = log.section_index
|
||||
}
|
||||
|
||||
// section_content / subsection_content - 表示内容生成完成(但整个章节可能还没完成)
|
||||
// 这里不更新 generatedSections,只记录进度
|
||||
if (log.action === 'section_content' || log.action === 'subsection_content') {
|
||||
// 子章节内容生成时,保持主章节的 loading 状态
|
||||
// 因为完整内容会在 section_complete 时一次性提供
|
||||
}
|
||||
|
||||
// section_complete - 表示完整章节(含所有子章节)生成完成
|
||||
// details.content 包含合并后的完整内容
|
||||
// 注意:只有主章节 complete 时才更新内容,子章节 complete 不处理
|
||||
|
||||
// section_complete - 章节生成完成
|
||||
if (log.action === 'section_complete') {
|
||||
const mainIndex = getMainSectionIndex(log.section_index)
|
||||
// 只有主章节完成时(section_index < 100)才更新内容和清除 loading
|
||||
if (!isSubsection(log.section_index) && log.details?.content) {
|
||||
generatedSections.value[mainIndex] = log.details.content
|
||||
if (log.details?.content) {
|
||||
generatedSections.value[log.section_index] = log.details.content
|
||||
// 自动展开刚生成的章节
|
||||
expandedContent.value.add(mainIndex - 1)
|
||||
expandedContent.value.add(log.section_index - 1)
|
||||
currentSectionIndex.value = null
|
||||
}
|
||||
// 子章节完成时不清除 currentSectionIndex,继续显示 loading
|
||||
}
|
||||
|
||||
if (log.action === 'report_complete') {
|
||||
|
|
@ -3055,10 +3023,6 @@ watch(() => props.reportId, (newId) => {
|
|||
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 {
|
||||
background: #ECFDF5;
|
||||
|
|
@ -3085,12 +3049,6 @@ watch(() => props.reportId, (newId) => {
|
|||
color: #374151;
|
||||
}
|
||||
|
||||
.tag-sub {
|
||||
font-size: 11px;
|
||||
color: #6B7280;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.tool-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Reference in a new issue