Add report ID generation and logging features for report generation process

- Introduced a unique report ID generation mechanism to enhance tracking and management of reports.
- Implemented detailed logging for the report generation process, including agent actions, planning stages, and tool calls, improving traceability and debugging.
- Added new API endpoints for retrieving agent and console logs, allowing users to access detailed execution logs and console outputs during report generation.
- Enhanced the frontend GraphPanel component with a notification for users when simulations finish, improving user experience and feedback.
This commit is contained in:
666ghj 2025-12-13 21:11:14 +08:00
parent 8949ae6761
commit b4435e273a
3 changed files with 856 additions and 11 deletions

View file

@ -105,13 +105,18 @@ def generate_report():
"error": "缺少模拟需求描述" "error": "缺少模拟需求描述"
}), 400 }), 400
# 提前生成 report_id以便立即返回给前端
import uuid
report_id = f"report_{uuid.uuid4().hex[:12]}"
# 创建异步任务 # 创建异步任务
task_manager = TaskManager() task_manager = TaskManager()
task_id = task_manager.create_task( task_id = task_manager.create_task(
task_type="report_generate", task_type="report_generate",
metadata={ metadata={
"simulation_id": simulation_id, "simulation_id": simulation_id,
"graph_id": graph_id "graph_id": graph_id,
"report_id": report_id
} }
) )
@ -140,8 +145,11 @@ def generate_report():
message=f"[{stage}] {message}" message=f"[{stage}] {message}"
) )
# 生成报告 # 生成报告(传入预先生成的 report_id
report = agent.generate_report(progress_callback=progress_callback) report = agent.generate_report(
progress_callback=progress_callback,
report_id=report_id
)
# 保存报告 # 保存报告
ReportManager.save_report(report) ReportManager.save_report(report)
@ -170,6 +178,7 @@ def generate_report():
"success": True, "success": True,
"data": { "data": {
"simulation_id": simulation_id, "simulation_id": simulation_id,
"report_id": report_id,
"task_id": task_id, "task_id": task_id,
"status": "generating", "status": "generating",
"message": "报告生成任务已启动,请通过 /api/report/generate/status 查询进度", "message": "报告生成任务已启动,请通过 /api/report/generate/status 查询进度",
@ -739,6 +748,183 @@ def check_report_status(simulation_id: str):
}), 500 }), 500
# ============== Agent 日志接口 ==============
@report_bp.route('/<report_id>/agent-log', methods=['GET'])
def get_agent_log(report_id: str):
"""
获取 Report Agent 的详细执行日志
实时获取报告生成过程中的每一步动作包括
- 报告开始规划开始/完成
- 每个章节的开始工具调用LLM响应完成
- 报告完成或失败
Query参数
from_line: 从第几行开始读取可选默认0用于增量获取
返回
{
"success": true,
"data": {
"logs": [
{
"timestamp": "2025-12-13T...",
"elapsed_seconds": 12.5,
"report_id": "report_xxxx",
"action": "tool_call",
"stage": "generating",
"section_title": "执行摘要",
"section_index": 1,
"details": {
"tool_name": "insight_forge",
"parameters": {...},
...
}
},
...
],
"total_lines": 25,
"from_line": 0,
"has_more": false
}
}
"""
try:
from_line = request.args.get('from_line', 0, type=int)
log_data = ReportManager.get_agent_log(report_id, from_line=from_line)
return jsonify({
"success": True,
"data": log_data
})
except Exception as e:
logger.error(f"获取Agent日志失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/<report_id>/agent-log/stream', methods=['GET'])
def stream_agent_log(report_id: str):
"""
获取完整的 Agent 日志一次性获取全部
返回
{
"success": true,
"data": {
"logs": [...],
"count": 25
}
}
"""
try:
logs = ReportManager.get_agent_log_stream(report_id)
return jsonify({
"success": True,
"data": {
"logs": logs,
"count": len(logs)
}
})
except Exception as e:
logger.error(f"获取Agent日志失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== 控制台日志接口 ==============
@report_bp.route('/<report_id>/console-log', methods=['GET'])
def get_console_log(report_id: str):
"""
获取 Report Agent 的控制台输出日志
实时获取报告生成过程中的控制台输出INFOWARNING等
这与 agent-log 接口返回的结构化 JSON 日志不同
是纯文本格式的控制台风格日志
Query参数
from_line: 从第几行开始读取可选默认0用于增量获取
返回
{
"success": true,
"data": {
"logs": [
"[19:46:14] INFO: 搜索完成: 找到 15 条相关事实",
"[19:46:14] INFO: 图谱搜索: graph_id=xxx, query=...",
...
],
"total_lines": 100,
"from_line": 0,
"has_more": false
}
}
"""
try:
from_line = request.args.get('from_line', 0, type=int)
log_data = ReportManager.get_console_log(report_id, from_line=from_line)
return jsonify({
"success": True,
"data": log_data
})
except Exception as e:
logger.error(f"获取控制台日志失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/<report_id>/console-log/stream', methods=['GET'])
def stream_console_log(report_id: str):
"""
获取完整的控制台日志一次性获取全部
返回
{
"success": true,
"data": {
"logs": [...],
"count": 100
}
}
"""
try:
logs = ReportManager.get_console_log_stream(report_id)
return jsonify({
"success": True,
"data": {
"logs": logs,
"count": len(logs)
}
})
except Exception as e:
logger.error(f"获取控制台日志失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== 工具调用接口(供调试使用)============== # ============== 工具调用接口(供调试使用)==============
@report_bp.route('/tools/search', methods=['POST']) @report_bp.route('/tools/search', methods=['POST'])

View file

@ -32,6 +32,336 @@ from .zep_tools import (
logger = get_logger('mirofish.report_agent') logger = get_logger('mirofish.report_agent')
class ReportLogger:
"""
Report Agent 详细日志记录器
在报告文件夹中生成 agent_log.jsonl 文件记录每一步详细动作
每行是一个完整的 JSON 对象包含时间戳动作类型详细内容等
"""
def __init__(self, report_id: str):
"""
初始化日志记录器
Args:
report_id: 报告ID用于确定日志文件路径
"""
self.report_id = report_id
self.log_file_path = os.path.join(
Config.UPLOAD_FOLDER, 'reports', report_id, 'agent_log.jsonl'
)
self.start_time = datetime.now()
self._ensure_log_file()
def _ensure_log_file(self):
"""确保日志文件所在目录存在"""
log_dir = os.path.dirname(self.log_file_path)
os.makedirs(log_dir, exist_ok=True)
def _get_elapsed_time(self) -> float:
"""获取从开始到现在的耗时(秒)"""
return (datetime.now() - self.start_time).total_seconds()
def log(
self,
action: str,
stage: str,
details: Dict[str, Any],
section_title: str = None,
section_index: int = None
):
"""
记录一条日志
Args:
action: 动作类型 'start', 'tool_call', 'llm_response', 'section_complete'
stage: 当前阶段 'planning', 'generating', 'completed'
details: 详细内容字典不截断
section_title: 当前章节标题可选
section_index: 当前章节索引可选
"""
log_entry = {
"timestamp": datetime.now().isoformat(),
"elapsed_seconds": round(self._get_elapsed_time(), 2),
"report_id": self.report_id,
"action": action,
"stage": stage,
"section_title": section_title,
"section_index": section_index,
"details": details
}
# 追加写入 JSONL 文件
with open(self.log_file_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
def log_start(self, simulation_id: str, graph_id: str, simulation_requirement: str):
"""记录报告生成开始"""
self.log(
action="report_start",
stage="pending",
details={
"simulation_id": simulation_id,
"graph_id": graph_id,
"simulation_requirement": simulation_requirement,
"message": "报告生成任务开始"
}
)
def log_planning_start(self):
"""记录大纲规划开始"""
self.log(
action="planning_start",
stage="planning",
details={"message": "开始规划报告大纲"}
)
def log_planning_context(self, context: Dict[str, Any]):
"""记录规划时获取的上下文信息"""
self.log(
action="planning_context",
stage="planning",
details={
"message": "获取模拟上下文信息",
"context": context
}
)
def log_planning_complete(self, outline_dict: Dict[str, Any]):
"""记录大纲规划完成"""
self.log(
action="planning_complete",
stage="planning",
details={
"message": "大纲规划完成",
"outline": outline_dict
}
)
def log_section_start(self, section_title: str, section_index: int):
"""记录章节生成开始"""
self.log(
action="section_start",
stage="generating",
section_title=section_title,
section_index=section_index,
details={"message": f"开始生成章节: {section_title}"}
)
def log_react_thought(self, section_title: str, section_index: int, iteration: int, thought: str):
"""记录 ReACT 思考过程"""
self.log(
action="react_thought",
stage="generating",
section_title=section_title,
section_index=section_index,
details={
"iteration": iteration,
"thought": thought,
"message": f"ReACT 第{iteration}轮思考"
}
)
def log_tool_call(
self,
section_title: str,
section_index: int,
tool_name: str,
parameters: Dict[str, Any],
iteration: int
):
"""记录工具调用"""
self.log(
action="tool_call",
stage="generating",
section_title=section_title,
section_index=section_index,
details={
"iteration": iteration,
"tool_name": tool_name,
"parameters": parameters,
"message": f"调用工具: {tool_name}"
}
)
def log_tool_result(
self,
section_title: str,
section_index: int,
tool_name: str,
result: str,
iteration: int
):
"""记录工具调用结果(完整内容,不截断)"""
self.log(
action="tool_result",
stage="generating",
section_title=section_title,
section_index=section_index,
details={
"iteration": iteration,
"tool_name": tool_name,
"result": result, # 完整结果,不截断
"result_length": len(result),
"message": f"工具 {tool_name} 返回结果"
}
)
def log_llm_response(
self,
section_title: str,
section_index: int,
response: str,
iteration: int,
has_tool_calls: bool,
has_final_answer: bool
):
"""记录 LLM 响应(完整内容,不截断)"""
self.log(
action="llm_response",
stage="generating",
section_title=section_title,
section_index=section_index,
details={
"iteration": iteration,
"response": response, # 完整响应,不截断
"response_length": len(response),
"has_tool_calls": has_tool_calls,
"has_final_answer": has_final_answer,
"message": f"LLM 响应 (工具调用: {has_tool_calls}, 最终答案: {has_final_answer})"
}
)
def log_section_complete(
self,
section_title: str,
section_index: int,
content: str,
tool_calls_count: int
):
"""记录章节生成完成(完整内容,不截断)"""
self.log(
action="section_complete",
stage="generating",
section_title=section_title,
section_index=section_index,
details={
"content": content, # 完整章节内容,不截断
"content_length": len(content),
"tool_calls_count": tool_calls_count,
"message": f"章节 {section_title} 生成完成"
}
)
def log_report_complete(self, total_sections: int, total_time_seconds: float):
"""记录报告生成完成"""
self.log(
action="report_complete",
stage="completed",
details={
"total_sections": total_sections,
"total_time_seconds": round(total_time_seconds, 2),
"message": "报告生成完成"
}
)
def log_error(self, error_message: str, stage: str, section_title: str = None):
"""记录错误"""
self.log(
action="error",
stage=stage,
section_title=section_title,
section_index=None,
details={
"error": error_message,
"message": f"发生错误: {error_message}"
}
)
class ReportConsoleLogger:
"""
Report Agent 控制台日志记录器
将控制台风格的日志INFOWARNING等写入报告文件夹中的 console_log.txt 文件
这些日志与 agent_log.jsonl 不同是纯文本格式的控制台输出
"""
def __init__(self, report_id: str):
"""
初始化控制台日志记录器
Args:
report_id: 报告ID用于确定日志文件路径
"""
self.report_id = report_id
self.log_file_path = os.path.join(
Config.UPLOAD_FOLDER, 'reports', report_id, 'console_log.txt'
)
self._ensure_log_file()
self._file_handler = None
self._setup_file_handler()
def _ensure_log_file(self):
"""确保日志文件所在目录存在"""
log_dir = os.path.dirname(self.log_file_path)
os.makedirs(log_dir, exist_ok=True)
def _setup_file_handler(self):
"""设置文件处理器,将日志同时写入文件"""
import logging
# 创建文件处理器
self._file_handler = logging.FileHandler(
self.log_file_path,
mode='a',
encoding='utf-8'
)
self._file_handler.setLevel(logging.INFO)
# 使用与控制台相同的简洁格式
formatter = logging.Formatter(
'[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%H:%M:%S'
)
self._file_handler.setFormatter(formatter)
# 添加到 report_agent 相关的 logger
loggers_to_attach = [
'mirofish.report_agent',
'mirofish.zep_tools',
]
for logger_name in loggers_to_attach:
target_logger = logging.getLogger(logger_name)
# 避免重复添加
if self._file_handler not in target_logger.handlers:
target_logger.addHandler(self._file_handler)
def close(self):
"""关闭文件处理器并从 logger 中移除"""
import logging
if self._file_handler:
loggers_to_detach = [
'mirofish.report_agent',
'mirofish.zep_tools',
]
for logger_name in loggers_to_detach:
target_logger = logging.getLogger(logger_name)
if self._file_handler in target_logger.handlers:
target_logger.removeHandler(self._file_handler)
self._file_handler.close()
self._file_handler = None
def __del__(self):
"""析构时确保关闭文件处理器"""
self.close()
class ReportStatus(str, Enum): class ReportStatus(str, Enum):
"""报告状态""" """报告状态"""
PENDING = "pending" PENDING = "pending"
@ -171,6 +501,11 @@ class ReportAgent:
# 工具定义 # 工具定义
self.tools = self._define_tools() self.tools = self._define_tools()
# 日志记录器(在 generate_report 中初始化)
self.report_logger: Optional[ReportLogger] = None
# 控制台日志记录器(在 generate_report 中初始化)
self.console_logger: Optional[ReportConsoleLogger] = None
logger.info(f"ReportAgent 初始化完成: graph_id={graph_id}, simulation_id={simulation_id}") logger.info(f"ReportAgent 初始化完成: graph_id={graph_id}, simulation_id={simulation_id}")
def _define_tools(self) -> Dict[str, Dict[str, Any]]: def _define_tools(self) -> Dict[str, Dict[str, Any]]:
@ -579,7 +914,8 @@ class ReportAgent:
section: ReportSection, section: ReportSection,
outline: ReportOutline, outline: ReportOutline,
previous_sections: List[str], previous_sections: List[str],
progress_callback: Optional[Callable] = None progress_callback: Optional[Callable] = None,
section_index: int = 0
) -> str: ) -> str:
""" """
使用ReACT模式生成单个章节内容 使用ReACT模式生成单个章节内容
@ -596,12 +932,17 @@ class ReportAgent:
outline: 完整大纲 outline: 完整大纲
previous_sections: 之前章节的内容用于保持连贯性 previous_sections: 之前章节的内容用于保持连贯性
progress_callback: 进度回调 progress_callback: 进度回调
section_index: 章节索引用于日志记录
Returns: Returns:
章节内容Markdown格式 章节内容Markdown格式
""" """
logger.info(f"ReACT生成章节: {section.title}") logger.info(f"ReACT生成章节: {section.title}")
# 记录章节开始日志
if self.report_logger:
self.report_logger.log_section_start(section.title, section_index)
# 构建系统prompt - 优化后强调工具使用和引用原文 # 构建系统prompt - 优化后强调工具使用和引用原文
# 确定当前章节的标题级别 # 确定当前章节的标题级别
section_level = 2 # 默认为二级标题(## section_level = 2 # 默认为二级标题(##
@ -795,8 +1136,23 @@ class ReportAgent:
logger.debug(f"LLM响应: {response[:200]}...") logger.debug(f"LLM响应: {response[:200]}...")
# 检查是否有工具调用和最终答案
has_tool_calls = bool(self._parse_tool_calls(response))
has_final_answer = "Final Answer:" in response
# 记录 LLM 响应日志
if self.report_logger:
self.report_logger.log_llm_response(
section_title=section.title,
section_index=section_index,
response=response,
iteration=iteration + 1,
has_tool_calls=has_tool_calls,
has_final_answer=has_final_answer
)
# 检查是否有最终答案 # 检查是否有最终答案
if "Final Answer:" in response: if has_final_answer:
# 如果工具调用次数不足,提醒需要更多检索 # 如果工具调用次数不足,提醒需要更多检索
if tool_calls_count < min_tool_calls: if tool_calls_count < min_tool_calls:
messages.append({"role": "assistant", "content": response}) messages.append({"role": "assistant", "content": response})
@ -816,6 +1172,16 @@ 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}次)")
# 记录章节完成日志
if self.report_logger:
self.report_logger.log_section_complete(
section_title=section.title,
section_index=section_index,
content=final_answer,
tool_calls_count=tool_calls_count
)
return final_answer return final_answer
# 解析工具调用 # 解析工具调用
@ -854,11 +1220,32 @@ class ReportAgent:
if tool_calls_count >= self.MAX_TOOL_CALLS_PER_SECTION: if tool_calls_count >= self.MAX_TOOL_CALLS_PER_SECTION:
break break
# 记录工具调用日志
if self.report_logger:
self.report_logger.log_tool_call(
section_title=section.title,
section_index=section_index,
tool_name=call["name"],
parameters=call.get("parameters", {}),
iteration=iteration + 1
)
result = self._execute_tool( result = self._execute_tool(
call["name"], call["name"],
call.get("parameters", {}), call.get("parameters", {}),
report_context=report_context report_context=report_context
) )
# 记录工具结果日志
if self.report_logger:
self.report_logger.log_tool_result(
section_title=section.title,
section_index=section_index,
tool_name=call["name"],
result=result,
iteration=iteration + 1
)
tool_results.append(f"═══ 工具 {call['name']} 返回 ═══\n{result}") tool_results.append(f"═══ 工具 {call['name']} 返回 ═══\n{result}")
tool_calls_count += 1 tool_calls_count += 1
@ -893,13 +1280,25 @@ class ReportAgent:
) )
if "Final Answer:" in response: if "Final Answer:" in response:
return response.split("Final Answer:")[-1].strip() final_answer = response.split("Final Answer:")[-1].strip()
else:
final_answer = response
return response # 记录章节完成日志
if self.report_logger:
self.report_logger.log_section_complete(
section_title=section.title,
section_index=section_index,
content=final_answer,
tool_calls_count=tool_calls_count
)
return final_answer
def generate_report( def generate_report(
self, self,
progress_callback: Optional[Callable[[str, int, str], None]] = None progress_callback: Optional[Callable[[str, int, str], None]] = None,
report_id: Optional[str] = None
) -> Report: ) -> Report:
""" """
生成完整报告分章节实时输出 生成完整报告分章节实时输出
@ -917,13 +1316,17 @@ class ReportAgent:
Args: Args:
progress_callback: 进度回调函数 (stage, progress, message) progress_callback: 进度回调函数 (stage, progress, message)
report_id: 报告ID可选如果不传则自动生成
Returns: Returns:
Report: 完整报告 Report: 完整报告
""" """
import uuid import uuid
# 如果没有传入 report_id则自动生成
if not report_id:
report_id = f"report_{uuid.uuid4().hex[:12]}" report_id = f"report_{uuid.uuid4().hex[:12]}"
start_time = datetime.now()
report = Report( report = Report(
report_id=report_id, report_id=report_id,
@ -940,6 +1343,18 @@ class ReportAgent:
try: try:
# 初始化:创建报告文件夹并保存初始状态 # 初始化:创建报告文件夹并保存初始状态
ReportManager._ensure_report_folder(report_id) ReportManager._ensure_report_folder(report_id)
# 初始化日志记录器(结构化日志 agent_log.jsonl
self.report_logger = ReportLogger(report_id)
self.report_logger.log_start(
simulation_id=self.simulation_id,
graph_id=self.graph_id,
simulation_requirement=self.simulation_requirement
)
# 初始化控制台日志记录器console_log.txt
self.console_logger = ReportConsoleLogger(report_id)
ReportManager.update_progress( ReportManager.update_progress(
report_id, "pending", 0, "初始化报告...", report_id, "pending", 0, "初始化报告...",
completed_sections=[] completed_sections=[]
@ -953,6 +1368,9 @@ class ReportAgent:
completed_sections=[] completed_sections=[]
) )
# 记录规划开始日志
self.report_logger.log_planning_start()
if progress_callback: if progress_callback:
progress_callback("planning", 0, "开始规划报告大纲...") progress_callback("planning", 0, "开始规划报告大纲...")
@ -962,6 +1380,9 @@ class ReportAgent:
) )
report.outline = outline report.outline = outline
# 记录规划完成日志
self.report_logger.log_planning_complete(outline.to_dict())
# 保存大纲到文件 # 保存大纲到文件
ReportManager.save_outline(report_id, outline) ReportManager.save_outline(report_id, outline)
ReportManager.update_progress( ReportManager.update_progress(
@ -1007,7 +1428,8 @@ class ReportAgent:
stage, stage,
base_progress + int(prog * 0.7 / total_sections), base_progress + int(prog * 0.7 / total_sections),
msg msg
) if progress_callback else None ) if progress_callback else None,
section_index=section_num
) )
section.content = section_content section.content = section_content
@ -1037,7 +1459,8 @@ class ReportAgent:
section=subsection, section=subsection,
outline=outline, outline=outline,
previous_sections=generated_sections, previous_sections=generated_sections,
progress_callback=None progress_callback=None,
section_index=section_num * 100 + subsection_num # 子章节索引
) )
subsection.content = subsection_content subsection.content = subsection_content
generated_sections.append(f"### {subsection.title}\n\n{subsection_content}") generated_sections.append(f"### {subsection.title}\n\n{subsection_content}")
@ -1077,6 +1500,16 @@ class ReportAgent:
report.status = ReportStatus.COMPLETED report.status = ReportStatus.COMPLETED
report.completed_at = datetime.now().isoformat() report.completed_at = datetime.now().isoformat()
# 计算总耗时
total_time_seconds = (datetime.now() - start_time).total_seconds()
# 记录报告完成日志
if self.report_logger:
self.report_logger.log_report_complete(
total_sections=total_sections,
total_time_seconds=total_time_seconds
)
# 保存最终报告 # 保存最终报告
ReportManager.save_report(report) ReportManager.save_report(report)
ReportManager.update_progress( ReportManager.update_progress(
@ -1088,6 +1521,12 @@ class ReportAgent:
progress_callback("completed", 100, "报告生成完成") progress_callback("completed", 100, "报告生成完成")
logger.info(f"报告生成完成: {report_id}") logger.info(f"报告生成完成: {report_id}")
# 关闭控制台日志记录器
if self.console_logger:
self.console_logger.close()
self.console_logger = None
return report return report
except Exception as e: except Exception as e:
@ -1095,6 +1534,10 @@ class ReportAgent:
report.status = ReportStatus.FAILED report.status = ReportStatus.FAILED
report.error = str(e) report.error = str(e)
# 记录错误日志
if self.report_logger:
self.report_logger.log_error(str(e), "failed")
# 保存失败状态 # 保存失败状态
try: try:
ReportManager.save_report(report) ReportManager.save_report(report)
@ -1105,6 +1548,11 @@ class ReportAgent:
except Exception: except Exception:
pass # 忽略保存失败的错误 pass # 忽略保存失败的错误
# 关闭控制台日志记录器
if self.console_logger:
self.console_logger.close()
self.console_logger = None
return report return report
def chat( def chat(
@ -1342,6 +1790,139 @@ class ReportManager:
"""获取章节Markdown文件路径""" """获取章节Markdown文件路径"""
return os.path.join(cls._get_report_folder(report_id), f"section_{section_index:02d}.md") return os.path.join(cls._get_report_folder(report_id), f"section_{section_index:02d}.md")
@classmethod
def _get_agent_log_path(cls, report_id: str) -> str:
"""获取 Agent 日志文件路径"""
return os.path.join(cls._get_report_folder(report_id), "agent_log.jsonl")
@classmethod
def _get_console_log_path(cls, report_id: str) -> str:
"""获取控制台日志文件路径"""
return os.path.join(cls._get_report_folder(report_id), "console_log.txt")
@classmethod
def get_console_log(cls, report_id: str, from_line: int = 0) -> Dict[str, Any]:
"""
获取控制台日志内容
这是报告生成过程中的控制台输出日志INFOWARNING等
agent_log.jsonl 的结构化日志不同
Args:
report_id: 报告ID
from_line: 从第几行开始读取用于增量获取0 表示从头开始
Returns:
{
"logs": [日志行列表],
"total_lines": 总行数,
"from_line": 起始行号,
"has_more": 是否还有更多日志
}
"""
log_path = cls._get_console_log_path(report_id)
if not os.path.exists(log_path):
return {
"logs": [],
"total_lines": 0,
"from_line": 0,
"has_more": False
}
logs = []
total_lines = 0
with open(log_path, 'r', encoding='utf-8') as f:
for i, line in enumerate(f):
total_lines = i + 1
if i >= from_line:
# 保留原始日志行,去掉末尾换行符
logs.append(line.rstrip('\n\r'))
return {
"logs": logs,
"total_lines": total_lines,
"from_line": from_line,
"has_more": False # 已读取到末尾
}
@classmethod
def get_console_log_stream(cls, report_id: str) -> List[str]:
"""
获取完整的控制台日志一次性获取全部
Args:
report_id: 报告ID
Returns:
日志行列表
"""
result = cls.get_console_log(report_id, from_line=0)
return result["logs"]
@classmethod
def get_agent_log(cls, report_id: str, from_line: int = 0) -> Dict[str, Any]:
"""
获取 Agent 日志内容
Args:
report_id: 报告ID
from_line: 从第几行开始读取用于增量获取0 表示从头开始
Returns:
{
"logs": [日志条目列表],
"total_lines": 总行数,
"from_line": 起始行号,
"has_more": 是否还有更多日志
}
"""
log_path = cls._get_agent_log_path(report_id)
if not os.path.exists(log_path):
return {
"logs": [],
"total_lines": 0,
"from_line": 0,
"has_more": False
}
logs = []
total_lines = 0
with open(log_path, 'r', encoding='utf-8') as f:
for i, line in enumerate(f):
total_lines = i + 1
if i >= from_line:
try:
log_entry = json.loads(line.strip())
logs.append(log_entry)
except json.JSONDecodeError:
# 跳过解析失败的行
continue
return {
"logs": logs,
"total_lines": total_lines,
"from_line": from_line,
"has_more": False # 已读取到末尾
}
@classmethod
def get_agent_log_stream(cls, report_id: str) -> List[Dict[str, Any]]:
"""
获取完整的 Agent 日志用于一次性获取全部
Args:
report_id: 报告ID
Returns:
日志条目列表
"""
result = cls.get_agent_log(report_id, from_line=0)
return result["logs"]
@classmethod @classmethod
def save_outline(cls, report_id: str, outline: ReportOutline) -> None: def save_outline(cls, report_id: str, outline: ReportOutline) -> None:
""" """

View file

@ -30,6 +30,24 @@
{{ isSimulating ? 'GraphRAG长短期记忆实时更新中' : '实时更新中...' }} {{ isSimulating ? 'GraphRAG长短期记忆实时更新中' : '实时更新中...' }}
</div> </div>
<!-- 模拟结束后的提示 -->
<div v-if="showSimulationFinishedHint" class="graph-building-hint finished-hint">
<div class="hint-icon-wrapper">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="hint-icon">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
</div>
<span class="hint-text">还有少量内容处理中建议稍后手动刷新图谱</span>
<button class="hint-close-btn" @click="dismissFinishedHint" title="关闭提示">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<!-- 节点/边详情面板 --> <!-- 节点/边详情面板 -->
<div v-if="selectedItem" class="detail-panel"> <div v-if="selectedItem" class="detail-panel">
<div class="detail-panel-header"> <div class="detail-panel-header">
@ -235,6 +253,22 @@ const graphSvg = ref(null)
const selectedItem = ref(null) const selectedItem = ref(null)
const showEdgeLabels = ref(true) // const showEdgeLabels = ref(true) //
const expandedSelfLoops = ref(new Set()) // const expandedSelfLoops = ref(new Set()) //
const showSimulationFinishedHint = ref(false) //
const wasSimulating = ref(false) //
//
const dismissFinishedHint = () => {
showSimulationFinishedHint.value = false
}
// isSimulating
watch(() => props.isSimulating, (newValue, oldValue) => {
if (wasSimulating.value && !newValue) {
//
showSimulationFinishedHint.value = true
}
wasSimulating.value = newValue
}, { immediate: true })
// / // /
const toggleSelfLoop = (id) => { const toggleSelfLoop = (id) => {
@ -1196,6 +1230,50 @@ input:checked + .slider:before {
50% { opacity: 1; transform: scale(1.15); filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); } 50% { opacity: 1; transform: scale(1.15); filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); }
} }
/* 模拟结束后的提示样式 */
.graph-building-hint.finished-hint {
background: rgba(0, 0, 0, 0.65);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.finished-hint .hint-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.finished-hint .hint-icon {
width: 18px;
height: 18px;
color: #FFF;
}
.finished-hint .hint-text {
flex: 1;
white-space: nowrap;
}
.hint-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 50%;
cursor: pointer;
color: #FFF;
transition: all 0.2s;
margin-left: 8px;
flex-shrink: 0;
}
.hint-close-btn:hover {
background: rgba(255, 255, 255, 0.35);
transform: scale(1.1);
}
/* Loading spinner */ /* Loading spinner */
.loading-spinner { .loading-spinner {
width: 40px; width: 40px;