docs(i18n): translate chinese docstrings/comments in backend/api

This commit is contained in:
Dominik Seemann 2026-05-09 10:59:36 +00:00
parent 6439e58eb5
commit b5a8996692
3 changed files with 640 additions and 760 deletions

View File

@ -1,6 +1,7 @@
""" """
图谱相关API路由 Graph-related API routes.
采用项目上下文机制服务端持久化状态
Uses a project context mechanism with server-side state persistence.
""" """
import os import os
@ -26,25 +27,22 @@ _graph_data_cache: dict = {} # graph_id -> {"data": ..., "ts": float}
_graph_refresh_locks: dict = {} # graph_id -> threading.Lock (one refresh at a time) _graph_refresh_locks: dict = {} # graph_id -> threading.Lock (one refresh at a time)
_GRAPH_CACHE_TTL = 300 # seconds before triggering a background refresh _GRAPH_CACHE_TTL = 300 # seconds before triggering a background refresh
# 获取日志器
logger = get_logger('mirofish.api') logger = get_logger('mirofish.api')
def allowed_file(filename: str) -> bool: def allowed_file(filename: str) -> bool:
"""检查文件扩展名是否允许""" """Return True if the file extension is in the allowed list."""
if not filename or '.' not in filename: if not filename or '.' not in filename:
return False return False
ext = os.path.splitext(filename)[1].lower().lstrip('.') ext = os.path.splitext(filename)[1].lower().lstrip('.')
return ext in Config.ALLOWED_EXTENSIONS return ext in Config.ALLOWED_EXTENSIONS
# ============== 项目管理接口 ============== # ============== Project management endpoints ==============
@graph_bp.route('/project/<project_id>', methods=['GET']) @graph_bp.route('/project/<project_id>', methods=['GET'])
def get_project(project_id: str): def get_project(project_id: str):
""" """Get project details."""
获取项目详情
"""
project = ProjectManager.get_project(project_id) project = ProjectManager.get_project(project_id)
if not project: if not project:
@ -61,9 +59,7 @@ def get_project(project_id: str):
@graph_bp.route('/project/list', methods=['GET']) @graph_bp.route('/project/list', methods=['GET'])
def list_projects(): def list_projects():
""" """List all projects."""
列出所有项目
"""
limit = request.args.get('limit', 50, type=int) limit = request.args.get('limit', 50, type=int)
projects = ProjectManager.list_projects(limit=limit) projects = ProjectManager.list_projects(limit=limit)
@ -76,9 +72,7 @@ def list_projects():
@graph_bp.route('/project/<project_id>', methods=['DELETE']) @graph_bp.route('/project/<project_id>', methods=['DELETE'])
def delete_project(project_id: str): def delete_project(project_id: str):
""" """Delete a project."""
删除项目
"""
success = ProjectManager.delete_project(project_id) success = ProjectManager.delete_project(project_id)
if not success: if not success:
@ -95,9 +89,7 @@ def delete_project(project_id: str):
@graph_bp.route('/project/<project_id>/reset', methods=['POST']) @graph_bp.route('/project/<project_id>/reset', methods=['POST'])
def reset_project(project_id: str): def reset_project(project_id: str):
""" """Reset project state (used to rebuild the graph from scratch)."""
重置项目状态用于重新构建图谱
"""
project = ProjectManager.get_project(project_id) project = ProjectManager.get_project(project_id)
if not project: if not project:
@ -106,7 +98,8 @@ def reset_project(project_id: str):
"error": t("api.error.graph.m004", project_id=project_id) "error": t("api.error.graph.m004", project_id=project_id)
}), 404 }), 404
# 重置到本体已生成状态 # Roll back to the "ontology generated" state so the next build can resume
# from the existing ontology rather than re-running ontology generation.
if project.ontology: if project.ontology:
project.status = ProjectStatus.ONTOLOGY_GENERATED project.status = ProjectStatus.ONTOLOGY_GENERATED
else: else:
@ -124,22 +117,21 @@ def reset_project(project_id: str):
}) })
# ============== 接口1上传文件并生成本体 ============== # ============== Endpoint 1: upload files and generate ontology ==============
@graph_bp.route('/ontology/generate', methods=['POST']) @graph_bp.route('/ontology/generate', methods=['POST'])
def generate_ontology(): def generate_ontology():
""" """Endpoint 1: upload files, analyze them, and generate an ontology definition.
接口1上传文件分析生成本体定义
请求方式multipart/form-data Request format: multipart/form-data.
参数 Args:
files: 上传的文件PDF/MD/TXT可多个 files: Uploaded files (PDF/MD/TXT); one or more.
simulation_requirement: 模拟需求描述必填 simulation_requirement: Description of the simulation requirement (required).
project_name: 项目名称可选 project_name: Project name (optional).
additional_context: 额外说明可选 additional_context: Additional context (optional).
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -157,7 +149,6 @@ def generate_ontology():
try: try:
logger.info(t("log.graph_api.m006")) logger.info(t("log.graph_api.m006"))
# 获取参数
simulation_requirement = request.form.get('simulation_requirement', '') simulation_requirement = request.form.get('simulation_requirement', '')
project_name = request.form.get('project_name', 'Unnamed Project') project_name = request.form.get('project_name', 'Unnamed Project')
additional_context = request.form.get('additional_context', '') additional_context = request.form.get('additional_context', '')
@ -171,7 +162,6 @@ def generate_ontology():
"error": t("api.error.graph.m009") "error": t("api.error.graph.m009")
}), 400 }), 400
# 获取上传的文件
uploaded_files = request.files.getlist('files') uploaded_files = request.files.getlist('files')
if not uploaded_files or all(not f.filename for f in uploaded_files): if not uploaded_files or all(not f.filename for f in uploaded_files):
return jsonify({ return jsonify({
@ -179,18 +169,17 @@ def generate_ontology():
"error": t("api.error.graph.m010") "error": t("api.error.graph.m010")
}), 400 }), 400
# 创建项目
project = ProjectManager.create_project(name=project_name) project = ProjectManager.create_project(name=project_name)
project.simulation_requirement = simulation_requirement project.simulation_requirement = simulation_requirement
logger.info(t("log.graph_api.m011", project=project.project_id)) logger.info(t("log.graph_api.m011", project=project.project_id))
# 保存文件并提取文本 # Persist each uploaded file under the project's directory and pull its
# text out so the ontology generator has plain text to work with.
document_texts = [] document_texts = []
all_text = "" all_text = ""
for file in uploaded_files: for file in uploaded_files:
if file and file.filename and allowed_file(file.filename): if file and file.filename and allowed_file(file.filename):
# 保存文件到项目目录
file_info = ProjectManager.save_file_to_project( file_info = ProjectManager.save_file_to_project(
project.project_id, project.project_id,
file, file,
@ -201,7 +190,6 @@ def generate_ontology():
"size": file_info["size"] "size": file_info["size"]
}) })
# 提取文本
text = FileParser.extract_text(file_info["path"]) text = FileParser.extract_text(file_info["path"])
text = TextProcessor.preprocess_text(text) text = TextProcessor.preprocess_text(text)
document_texts.append(text) document_texts.append(text)
@ -214,12 +202,10 @@ def generate_ontology():
"error": t("api.error.graph.m012") "error": t("api.error.graph.m012")
}), 400 }), 400
# 保存提取的文本
project.total_text_length = len(all_text) project.total_text_length = len(all_text)
ProjectManager.save_extracted_text(project.project_id, all_text) ProjectManager.save_extracted_text(project.project_id, all_text)
logger.info(t("log.graph_api.m013", len=len(all_text))) logger.info(t("log.graph_api.m013", len=len(all_text)))
# 生成本体
logger.info(t("log.graph_api.m014")) logger.info(t("log.graph_api.m014"))
generator = OntologyGenerator() generator = OntologyGenerator()
ontology = generator.generate( ontology = generator.generate(
@ -228,7 +214,6 @@ def generate_ontology():
additional_context=additional_context if additional_context else None additional_context=additional_context if additional_context else None
) )
# 保存本体到项目
entity_count = len(ontology.get("entity_types", [])) entity_count = len(ontology.get("entity_types", []))
edge_count = len(ontology.get("edge_types", [])) edge_count = len(ontology.get("edge_types", []))
logger.info(t("log.graph_api.m015", entity_count=entity_count, edge_count=edge_count)) logger.info(t("log.graph_api.m015", entity_count=entity_count, edge_count=edge_count))
@ -262,35 +247,33 @@ def generate_ontology():
}), 500 }), 500
# ============== 接口2构建图谱 ============== # ============== Endpoint 2: build graph ==============
@graph_bp.route('/build', methods=['POST']) @graph_bp.route('/build', methods=['POST'])
def build_graph(): def build_graph():
""" """Endpoint 2: build the graph for the given project_id.
接口2根据project_id构建图谱
请求JSON Request (JSON):
{ {
"project_id": "proj_xxxx", // 必填来自接口1 "project_id": "proj_xxxx", // required, from endpoint 1
"graph_name": "图谱名称", // 可选 "graph_name": "Graph name", // optional
"chunk_size": 500, // 可选默认500 "chunk_size": 500, // optional, default 500
"chunk_overlap": 50 // 可选默认50 "chunk_overlap": 50 // optional, default 50
} }
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
"project_id": "proj_xxxx", "project_id": "proj_xxxx",
"task_id": "task_xxxx", "task_id": "task_xxxx",
"message": "图谱构建任务已启动" "message": "Graph build task started"
} }
} }
""" """
try: try:
logger.info(t("log.graph_api.m017")) logger.info(t("log.graph_api.m017"))
# 检查配置
errors = [] errors = []
if not Config.NEO4J_PASSWORD: if not Config.NEO4J_PASSWORD:
errors.append("NEO4J未配置") errors.append("NEO4J未配置")
@ -301,7 +284,6 @@ def build_graph():
"error": "配置错误: " + "; ".join(errors) "error": "配置错误: " + "; ".join(errors)
}), 500 }), 500
# 解析请求
data = request.get_json() or {} data = request.get_json() or {}
project_id = data.get('project_id') project_id = data.get('project_id')
logger.debug(t("log.graph_api.m019", project_id=project_id)) logger.debug(t("log.graph_api.m019", project_id=project_id))
@ -312,7 +294,6 @@ def build_graph():
"error": t("api.error.graph.m020") "error": t("api.error.graph.m020")
}), 400 }), 400
# 获取项目
project = ProjectManager.get_project(project_id) project = ProjectManager.get_project(project_id)
if not project: if not project:
return jsonify({ return jsonify({
@ -320,8 +301,8 @@ def build_graph():
"error": t("api.error.graph.m021", project_id=project_id) "error": t("api.error.graph.m021", project_id=project_id)
}), 404 }), 404
# 检查项目状态 # If True, abandon any existing build progress and rebuild from scratch.
force = data.get('force', False) # 强制重新构建 force = data.get('force', False)
if project.status == ProjectStatus.CREATED: if project.status == ProjectStatus.CREATED:
return jsonify({ return jsonify({
@ -336,23 +317,20 @@ def build_graph():
"task_id": project.graph_build_task_id "task_id": project.graph_build_task_id
}), 400 }), 400
# 如果强制重建,重置状态 # On a forced rebuild, drop any prior build artifacts so we restart cleanly.
if force and project.status in [ProjectStatus.GRAPH_BUILDING, ProjectStatus.FAILED, ProjectStatus.GRAPH_COMPLETED]: if force and project.status in [ProjectStatus.GRAPH_BUILDING, ProjectStatus.FAILED, ProjectStatus.GRAPH_COMPLETED]:
project.status = ProjectStatus.ONTOLOGY_GENERATED project.status = ProjectStatus.ONTOLOGY_GENERATED
project.graph_id = None project.graph_id = None
project.graph_build_task_id = None project.graph_build_task_id = None
project.error = None project.error = None
# 获取配置
graph_name = data.get('graph_name', project.name or 'MiroFish Graph') graph_name = data.get('graph_name', project.name or 'MiroFish Graph')
chunk_size = data.get('chunk_size', project.chunk_size or Config.DEFAULT_CHUNK_SIZE) chunk_size = data.get('chunk_size', project.chunk_size or Config.DEFAULT_CHUNK_SIZE)
chunk_overlap = data.get('chunk_overlap', project.chunk_overlap or Config.DEFAULT_CHUNK_OVERLAP) chunk_overlap = data.get('chunk_overlap', project.chunk_overlap or Config.DEFAULT_CHUNK_OVERLAP)
# 更新项目配置
project.chunk_size = chunk_size project.chunk_size = chunk_size
project.chunk_overlap = chunk_overlap project.chunk_overlap = chunk_overlap
# 获取提取的文本
text = ProjectManager.get_extracted_text(project_id) text = ProjectManager.get_extracted_text(project_id)
if not text: if not text:
return jsonify({ return jsonify({
@ -360,7 +338,6 @@ def build_graph():
"error": t("api.error.graph.m024") "error": t("api.error.graph.m024")
}), 400 }), 400
# 获取本体
ontology = project.ontology ontology = project.ontology
if not ontology: if not ontology:
return jsonify({ return jsonify({
@ -368,17 +345,14 @@ def build_graph():
"error": t("api.error.graph.m025") "error": t("api.error.graph.m025")
}), 400 }), 400
# 创建异步任务
task_manager = TaskManager() task_manager = TaskManager()
task_id = task_manager.create_task(f"构建图谱: {graph_name}") task_id = task_manager.create_task(f"构建图谱: {graph_name}")
logger.info(t("log.graph_api.m026", task_id=task_id, project_id=project_id)) logger.info(t("log.graph_api.m026", task_id=task_id, project_id=project_id))
# 更新项目状态
project.status = ProjectStatus.GRAPH_BUILDING project.status = ProjectStatus.GRAPH_BUILDING
project.graph_build_task_id = task_id project.graph_build_task_id = task_id
ProjectManager.save_project(project) ProjectManager.save_project(project)
# 启动后台任务
def build_task(): def build_task():
build_logger = get_logger('mirofish.build') build_logger = get_logger('mirofish.build')
try: try:
@ -389,10 +363,8 @@ def build_graph():
message="初始化图谱构建服务..." message="初始化图谱构建服务..."
) )
# 创建图谱构建服务
builder = GraphBuilderService() builder = GraphBuilderService()
# 分块
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="文本分块中...", message="文本分块中...",
@ -405,7 +377,6 @@ def build_graph():
) )
total_chunks = len(chunks) total_chunks = len(chunks)
# 创建图谱
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="创建Zep图谱...", message="创建Zep图谱...",
@ -413,11 +384,9 @@ def build_graph():
) )
graph_id = builder.create_graph(name=graph_name) graph_id = builder.create_graph(name=graph_name)
# 更新项目的graph_id
project.graph_id = graph_id project.graph_id = graph_id
ProjectManager.save_project(project) ProjectManager.save_project(project)
# 设置本体
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="设置本体定义...", message="设置本体定义...",
@ -425,9 +394,9 @@ def build_graph():
) )
builder.set_ontology(graph_id, ontology) builder.set_ontology(graph_id, ontology)
# 添加文本progress_callback 签名是 (msg, progress_ratio) # Add text. The progress_callback signature is (msg, progress_ratio).
def add_progress_callback(msg, progress_ratio): def add_progress_callback(msg, progress_ratio):
progress = 15 + int(progress_ratio * 40) # 15% - 55% progress = 15 + int(progress_ratio * 40) # maps ratio onto 15%-55%
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message=msg, message=msg,
@ -460,7 +429,7 @@ def build_graph():
skip_chunks=skip_chunks, skip_chunks=skip_chunks,
) )
# 等待Zep处理完成查询每个episode的processed状态 # Wait for Zep to finish processing (poll each episode's processed flag).
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="等待Zep处理数据...", message="等待Zep处理数据...",
@ -468,7 +437,7 @@ def build_graph():
) )
def wait_progress_callback(msg, progress_ratio): def wait_progress_callback(msg, progress_ratio):
progress = 55 + int(progress_ratio * 35) # 55% - 90% progress = 55 + int(progress_ratio * 35) # maps ratio onto 55%-90%
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message=msg, message=msg,
@ -477,7 +446,6 @@ def build_graph():
builder._wait_for_episodes(episode_uuids, wait_progress_callback) builder._wait_for_episodes(episode_uuids, wait_progress_callback)
# 获取图谱数据
task_manager.update_task( task_manager.update_task(
task_id, task_id,
message="获取图谱数据...", message="获取图谱数据...",
@ -485,7 +453,6 @@ def build_graph():
) )
graph_data = builder.get_graph_data(graph_id) graph_data = builder.get_graph_data(graph_id)
# 更新项目状态
project.status = ProjectStatus.GRAPH_COMPLETED project.status = ProjectStatus.GRAPH_COMPLETED
ProjectManager.save_project(project) ProjectManager.save_project(project)
@ -499,7 +466,6 @@ def build_graph():
edge_count=edge_count, edge_count=edge_count,
)) ))
# 完成
task_manager.update_task( task_manager.update_task(
task_id, task_id,
status=TaskStatus.COMPLETED, status=TaskStatus.COMPLETED,
@ -515,7 +481,7 @@ def build_graph():
) )
except Exception as e: except Exception as e:
# 更新项目状态为失败 # Mark the project as FAILED so the UI can surface the error.
build_logger.error(t("log.graph_api.m029", task_id=task_id, e=str(e))) build_logger.error(t("log.graph_api.m029", task_id=task_id, e=str(e)))
build_logger.debug(traceback.format_exc()) build_logger.debug(traceback.format_exc())
@ -530,7 +496,6 @@ def build_graph():
error=traceback.format_exc() error=traceback.format_exc()
) )
# 启动后台线程
thread = threading.Thread(target=build_task, daemon=True) thread = threading.Thread(target=build_task, daemon=True)
thread.start() thread.start()
@ -551,13 +516,11 @@ def build_graph():
}), 500 }), 500
# ============== 任务查询接口 ============== # ============== Task query endpoints ==============
@graph_bp.route('/task/<task_id>', methods=['GET']) @graph_bp.route('/task/<task_id>', methods=['GET'])
def get_task(task_id: str): def get_task(task_id: str):
""" """Query the status of a task."""
查询任务状态
"""
task = TaskManager().get_task(task_id) task = TaskManager().get_task(task_id)
if not task: if not task:
@ -574,9 +537,7 @@ def get_task(task_id: str):
@graph_bp.route('/tasks', methods=['GET']) @graph_bp.route('/tasks', methods=['GET'])
def list_tasks(): def list_tasks():
""" """List all tasks."""
列出所有任务
"""
tasks = TaskManager().list_tasks() tasks = TaskManager().list_tasks()
return jsonify({ return jsonify({
@ -586,7 +547,7 @@ def list_tasks():
}) })
# ============== 图谱数据接口 ============== # ============== Graph data endpoints ==============
def _refresh_graph_cache(graph_id: str): def _refresh_graph_cache(graph_id: str):
"""Background thread: fetch graph data from Neo4j and update cache.""" """Background thread: fetch graph data from Neo4j and update cache."""
@ -613,11 +574,11 @@ def _refresh_graph_cache(graph_id: str):
@graph_bp.route('/data/<graph_id>', methods=['GET']) @graph_bp.route('/data/<graph_id>', methods=['GET'])
def get_graph_data(graph_id: str): def get_graph_data(graph_id: str):
""" """Return graph data (nodes and edges).
获取图谱数据节点和边
- 有缓存且未过期直接返回缓存不调用 Zep - Fresh cache: serve from cache without hitting Zep.
- 有缓存但已过期立即返回旧缓存后台异步刷新 - Stale cache: return the old cache immediately and refresh in the background.
- 无缓存后台线程拉取返回 202 让前端稍后重试 - No cache: kick off a background fetch and return 202 so the frontend retries.
""" """
if not Config.NEO4J_PASSWORD: if not Config.NEO4J_PASSWORD:
return jsonify({"success": False, "error": t("api.error.graph.m028")}), 500 return jsonify({"success": False, "error": t("api.error.graph.m028")}), 500
@ -645,9 +606,7 @@ def get_graph_data(graph_id: str):
@graph_bp.route('/delete/<graph_id>', methods=['DELETE']) @graph_bp.route('/delete/<graph_id>', methods=['DELETE'])
def delete_graph(graph_id: str): def delete_graph(graph_id: str):
""" """Delete a Zep graph."""
删除Zep图谱
"""
try: try:
if not Config.NEO4J_PASSWORD: if not Config.NEO4J_PASSWORD:
return jsonify({ return jsonify({

View File

@ -1,6 +1,7 @@
""" """
Report API路由 Report API routes.
提供模拟报告生成获取对话等接口
Provides endpoints for generating, retrieving, and chatting about simulation reports.
""" """
import os import os
@ -20,30 +21,30 @@ from ..utils.locale import t, get_locale, set_locale
logger = get_logger('mirofish.api.report') logger = get_logger('mirofish.api.report')
# ============== 报告生成接口 ============== # ============== Report generation endpoints ==============
@report_bp.route('/generate', methods=['POST']) @report_bp.route('/generate', methods=['POST'])
def generate_report(): def generate_report():
""" """
生成模拟分析报告异步任务 Generate a simulation analysis report (asynchronous task).
这是一个耗时操作接口会立即返回task_id This is a long-running operation. The endpoint returns a task_id immediately;
使用 GET /api/report/generate/status 查询进度 use GET /api/report/generate/status to poll progress.
请求JSON Request (JSON):
{ {
"simulation_id": "sim_xxxx", // 必填模拟ID "simulation_id": "sim_xxxx", // required, simulation ID
"force_regenerate": false // 可选强制重新生成 "force_regenerate": false // optional, force regeneration
} }
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
"simulation_id": "sim_xxxx", "simulation_id": "sim_xxxx",
"task_id": "task_xxxx", "task_id": "task_xxxx",
"status": "generating", "status": "generating",
"message": "报告生成任务已启动" "message": "Report generation task started"
} }
} }
""" """
@ -59,7 +60,6 @@ def generate_report():
force_regenerate = data.get('force_regenerate', False) force_regenerate = data.get('force_regenerate', False)
# 获取模拟信息
manager = SimulationManager() manager = SimulationManager()
state = manager.get_simulation(simulation_id) state = manager.get_simulation(simulation_id)
@ -69,7 +69,7 @@ def generate_report():
"error": t('api.simulationNotFound', id=simulation_id) "error": t('api.simulationNotFound', id=simulation_id)
}), 404 }), 404
# 检查是否已有报告 # Skip regeneration if a completed report already exists for this simulation.
if not force_regenerate: if not force_regenerate:
existing_report = ReportManager.get_report_by_simulation(simulation_id) existing_report = ReportManager.get_report_by_simulation(simulation_id)
if existing_report and existing_report.status == ReportStatus.COMPLETED: if existing_report and existing_report.status == ReportStatus.COMPLETED:
@ -84,7 +84,6 @@ def generate_report():
} }
}) })
# 获取项目信息
project = ProjectManager.get_project(state.project_id) project = ProjectManager.get_project(state.project_id)
if not project: if not project:
return jsonify({ return jsonify({
@ -106,11 +105,11 @@ def generate_report():
"error": t('api.missingSimRequirement') "error": t('api.missingSimRequirement')
}), 400 }), 400
# 提前生成 report_id以便立即返回给前端 # Generate report_id eagerly so the frontend can use it immediately
# (before the background task has actually persisted anything).
import uuid import uuid
report_id = f"report_{uuid.uuid4().hex[:12]}" 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",
@ -124,7 +123,6 @@ def generate_report():
# Capture locale before spawning background thread # Capture locale before spawning background thread
current_locale = get_locale() current_locale = get_locale()
# 定义后台任务
def run_generate(): def run_generate():
set_locale(current_locale) set_locale(current_locale)
try: try:
@ -135,14 +133,12 @@ def generate_report():
message=t('api.initReportAgent') message=t('api.initReportAgent')
) )
# 创建Report Agent
agent = ReportAgent( agent = ReportAgent(
graph_id=graph_id, graph_id=graph_id,
simulation_id=simulation_id, simulation_id=simulation_id,
simulation_requirement=simulation_requirement simulation_requirement=simulation_requirement
) )
# 进度回调
def progress_callback(stage, progress, message): def progress_callback(stage, progress, message):
task_manager.update_task( task_manager.update_task(
task_id, task_id,
@ -150,13 +146,13 @@ def generate_report():
message=f"[{stage}] {message}" message=f"[{stage}] {message}"
) )
# 生成报告(传入预先生成的 report_id # Pass in the pre-generated report_id so the persisted report matches
# the id we already returned to the frontend.
report = agent.generate_report( report = agent.generate_report(
progress_callback=progress_callback, progress_callback=progress_callback,
report_id=report_id report_id=report_id
) )
# 保存报告
ReportManager.save_report(report) ReportManager.save_report(report)
if report.status == ReportStatus.COMPLETED: if report.status == ReportStatus.COMPLETED:
@ -175,7 +171,6 @@ def generate_report():
logger.error(t("log.report_api.m001", str=str(e))) logger.error(t("log.report_api.m001", str=str(e)))
task_manager.fail_task(task_id, str(e)) task_manager.fail_task(task_id, str(e))
# 启动后台线程
thread = threading.Thread(target=run_generate, daemon=True) thread = threading.Thread(target=run_generate, daemon=True)
thread.start() thread.start()
@ -203,15 +198,15 @@ def generate_report():
@report_bp.route('/generate/status', methods=['POST']) @report_bp.route('/generate/status', methods=['POST'])
def get_generate_status(): def get_generate_status():
""" """
查询报告生成任务进度 Query the progress of a report generation task.
请求JSON Request (JSON):
{ {
"task_id": "task_xxxx", // 可选generate返回的task_id "task_id": "task_xxxx", // optional, task_id returned by generate
"simulation_id": "sim_xxxx" // 可选模拟ID "simulation_id": "sim_xxxx" // optional, simulation ID
} }
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -228,7 +223,8 @@ def get_generate_status():
task_id = data.get('task_id') task_id = data.get('task_id')
simulation_id = data.get('simulation_id') simulation_id = data.get('simulation_id')
# 如果提供了simulation_id先检查是否已有完成的报告 # If simulation_id is provided, short-circuit when a completed report already exists
# so callers don't have to track a stale task_id after a successful run.
if simulation_id: if simulation_id:
existing_report = ReportManager.get_report_by_simulation(simulation_id) existing_report = ReportManager.get_report_by_simulation(simulation_id)
if existing_report and existing_report.status == ReportStatus.COMPLETED: if existing_report and existing_report.status == ReportStatus.COMPLETED:
@ -272,14 +268,14 @@ def get_generate_status():
}), 500 }), 500
# ============== 报告获取接口 ============== # ============== Report retrieval endpoints ==============
@report_bp.route('/<report_id>', methods=['GET']) @report_bp.route('/<report_id>', methods=['GET'])
def get_report(report_id: str): def get_report(report_id: str):
""" """
获取报告详情 Get report details.
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -319,9 +315,9 @@ def get_report(report_id: str):
@report_bp.route('/by-simulation/<simulation_id>', methods=['GET']) @report_bp.route('/by-simulation/<simulation_id>', methods=['GET'])
def get_report_by_simulation(simulation_id: str): def get_report_by_simulation(simulation_id: str):
""" """
根据模拟ID获取报告 Get the report for a given simulation ID.
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -358,13 +354,13 @@ def get_report_by_simulation(simulation_id: str):
@report_bp.route('/list', methods=['GET']) @report_bp.route('/list', methods=['GET'])
def list_reports(): def list_reports():
""" """
列出所有报告 List all reports.
Query参数 Query parameters:
simulation_id: 按模拟ID过滤可选 simulation_id: optional filter by simulation ID.
limit: 返回数量限制默认50 limit: maximum number of reports to return (default 50).
返回 Returns:
{ {
"success": true, "success": true,
"data": [...], "data": [...],
@ -398,9 +394,9 @@ def list_reports():
@report_bp.route('/<report_id>/download', methods=['GET']) @report_bp.route('/<report_id>/download', methods=['GET'])
def download_report(report_id: str): def download_report(report_id: str):
""" """
下载报告Markdown格式 Download a report as a Markdown file.
返回Markdown文件 Returns the Markdown file as an attachment.
""" """
try: try:
report = ReportManager.get_report(report_id) report = ReportManager.get_report(report_id)
@ -414,7 +410,8 @@ def download_report(report_id: str):
md_path = ReportManager._get_report_markdown_path(report_id) md_path = ReportManager._get_report_markdown_path(report_id)
if not os.path.exists(md_path): if not os.path.exists(md_path):
# 如果MD文件不存在生成一个临时文件 # MD file is missing on disk; materialize a temp file from the in-memory content
# so the download still succeeds for older reports that were never persisted.
import tempfile import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write(report.markdown_content) f.write(report.markdown_content)
@ -443,7 +440,7 @@ def download_report(report_id: str):
@report_bp.route('/<report_id>', methods=['DELETE']) @report_bp.route('/<report_id>', methods=['DELETE'])
def delete_report(report_id: str): def delete_report(report_id: str):
"""删除报告""" """Delete a report."""
try: try:
success = ReportManager.delete_report(report_id) success = ReportManager.delete_report(report_id)
@ -467,32 +464,33 @@ def delete_report(report_id: str):
}), 500 }), 500
# ============== Report Agent对话接口 ============== # ============== Report Agent chat endpoints ==============
@report_bp.route('/chat', methods=['POST']) @report_bp.route('/chat', methods=['POST'])
def chat_with_report_agent(): def chat_with_report_agent():
""" """
与Report Agent对话 Chat with the Report Agent.
Report Agent可以在对话中自主调用检索工具来回答问题 The Report Agent can autonomously invoke retrieval tools during the conversation
to answer the user's question.
请求JSON Request (JSON):
{ {
"simulation_id": "sim_xxxx", // 必填模拟ID "simulation_id": "sim_xxxx", // required, simulation ID
"message": "请解释一下舆情走向", // 必填用户消息 "message": "Explain the sentiment trend", // required, user message
"chat_history": [ // 可选对话历史 "chat_history": [ // optional, prior turns
{"role": "user", "content": "..."}, {"role": "user", "content": "..."},
{"role": "assistant", "content": "..."} {"role": "assistant", "content": "..."}
] ]
} }
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
"response": "Agent回复...", "response": "Agent reply...",
"tool_calls": [调用的工具列表], "tool_calls": [list of tools invoked],
"sources": [信息来源] "sources": [information sources]
} }
} }
""" """
@ -515,7 +513,6 @@ def chat_with_report_agent():
"error": t('api.requireMessage') "error": t('api.requireMessage')
}), 400 }), 400
# 获取模拟和项目信息
manager = SimulationManager() manager = SimulationManager()
state = manager.get_simulation(simulation_id) state = manager.get_simulation(simulation_id)
@ -541,7 +538,6 @@ def chat_with_report_agent():
simulation_requirement = project.simulation_requirement or "" simulation_requirement = project.simulation_requirement or ""
# 创建Agent并进行对话
agent = ReportAgent( agent = ReportAgent(
graph_id=graph_id, graph_id=graph_id,
simulation_id=simulation_id, simulation_id=simulation_id,
@ -564,22 +560,22 @@ def chat_with_report_agent():
}), 500 }), 500
# ============== 报告进度与分章节接口 ============== # ============== Report progress and section endpoints ==============
@report_bp.route('/<report_id>/progress', methods=['GET']) @report_bp.route('/<report_id>/progress', methods=['GET'])
def get_report_progress(report_id: str): def get_report_progress(report_id: str):
""" """
获取报告生成进度实时 Get real-time report generation progress.
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
"status": "generating", "status": "generating",
"progress": 45, "progress": 45,
"message": "正在生成章节: 关键发现", "message": "Generating section: Key Findings",
"current_section": "关键发现", "current_section": "Key Findings",
"completed_sections": ["执行摘要", "模拟背景"], "completed_sections": ["Executive Summary", "Simulation Background"],
"updated_at": "2025-12-09T..." "updated_at": "2025-12-09T..."
} }
} }
@ -610,11 +606,12 @@ def get_report_progress(report_id: str):
@report_bp.route('/<report_id>/sections', methods=['GET']) @report_bp.route('/<report_id>/sections', methods=['GET'])
def get_report_sections(report_id: str): def get_report_sections(report_id: str):
""" """
获取已生成的章节列表分章节输出 Get the list of sections generated so far (per-section streaming output).
前端可以轮询此接口获取已生成的章节内容无需等待整个报告完成 The frontend can poll this endpoint to render sections incrementally,
without waiting for the entire report to finish.
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -623,7 +620,7 @@ def get_report_sections(report_id: str):
{ {
"filename": "section_01.md", "filename": "section_01.md",
"section_index": 1, "section_index": 1,
"content": "## 执行摘要\\n\\n..." "content": "## Executive Summary\\n\\n..."
}, },
... ...
], ],
@ -635,7 +632,6 @@ def get_report_sections(report_id: str):
try: try:
sections = ReportManager.get_generated_sections(report_id) sections = ReportManager.get_generated_sections(report_id)
# 获取报告状态
report = ReportManager.get_report(report_id) report = ReportManager.get_report(report_id)
is_complete = report is not None and report.status == ReportStatus.COMPLETED is_complete = report is not None and report.status == ReportStatus.COMPLETED
@ -661,14 +657,14 @@ def get_report_sections(report_id: str):
@report_bp.route('/<report_id>/section/<int:section_index>', methods=['GET']) @report_bp.route('/<report_id>/section/<int:section_index>', methods=['GET'])
def get_single_section(report_id: str, section_index: int): def get_single_section(report_id: str, section_index: int):
""" """
获取单个章节内容 Get the content of a single section.
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
"filename": "section_01.md", "filename": "section_01.md",
"content": "## 执行摘要\\n\\n..." "content": "## Executive Summary\\n\\n..."
} }
} }
""" """
@ -702,16 +698,16 @@ def get_single_section(report_id: str, section_index: int):
}), 500 }), 500
# ============== 报告状态检查接口 ============== # ============== Report status check endpoints ==============
@report_bp.route('/check/<simulation_id>', methods=['GET']) @report_bp.route('/check/<simulation_id>', methods=['GET'])
def check_report_status(simulation_id: str): def check_report_status(simulation_id: str):
""" """
检查模拟是否有报告以及报告状态 Check whether a simulation has a report, and report its status.
用于前端判断是否解锁Interview功能 Used by the frontend to decide whether to unlock the Interview feature.
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -730,7 +726,7 @@ def check_report_status(simulation_id: str):
report_status = report.status.value if report else None report_status = report.status.value if report else None
report_id = report.report_id if report else None report_id = report.report_id if report else None
# 只有报告完成后才解锁interview # Interview feature is only unlocked once a report has finished generating.
interview_unlocked = has_report and report.status == ReportStatus.COMPLETED interview_unlocked = has_report and report.status == ReportStatus.COMPLETED
return jsonify({ return jsonify({
@ -753,22 +749,22 @@ def check_report_status(simulation_id: str):
}), 500 }), 500
# ============== Agent 日志接口 ============== # ============== Agent log endpoints ==============
@report_bp.route('/<report_id>/agent-log', methods=['GET']) @report_bp.route('/<report_id>/agent-log', methods=['GET'])
def get_agent_log(report_id: str): def get_agent_log(report_id: str):
""" """
获取 Report Agent 的详细执行日志 Get the detailed execution log of the Report Agent.
实时获取报告生成过程中的每一步动作包括 Streams every step the agent took while generating the report, including:
- 报告开始规划开始/完成 - Report start, planning start/complete.
- 每个章节的开始工具调用LLM响应完成 - Per-section start, tool calls, LLM responses, and completion.
- 报告完成或失败 - Final report completion or failure.
Query参数 Query parameters:
from_line: 从第几行开始读取可选默认0用于增量获取 from_line: line offset to start reading from (optional, default 0, for incremental polling).
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -779,7 +775,7 @@ def get_agent_log(report_id: str):
"report_id": "report_xxxx", "report_id": "report_xxxx",
"action": "tool_call", "action": "tool_call",
"stage": "generating", "stage": "generating",
"section_title": "执行摘要", "section_title": "Executive Summary",
"section_index": 1, "section_index": 1,
"details": { "details": {
"tool_name": "insight_forge", "tool_name": "insight_forge",
@ -817,9 +813,9 @@ def get_agent_log(report_id: str):
@report_bp.route('/<report_id>/agent-log/stream', methods=['GET']) @report_bp.route('/<report_id>/agent-log/stream', methods=['GET'])
def stream_agent_log(report_id: str): def stream_agent_log(report_id: str):
""" """
获取完整的 Agent 日志一次性获取全部 Get the full Agent log in one shot (no pagination).
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -848,27 +844,27 @@ def stream_agent_log(report_id: str):
}), 500 }), 500
# ============== 控制台日志接口 ============== # ============== Console log endpoints ==============
@report_bp.route('/<report_id>/console-log', methods=['GET']) @report_bp.route('/<report_id>/console-log', methods=['GET'])
def get_console_log(report_id: str): def get_console_log(report_id: str):
""" """
获取 Report Agent 的控制台输出日志 Get the Report Agent's console output log.
实时获取报告生成过程中的控制台输出INFOWARNING等 Streams the console output produced during report generation (INFO, WARNING, etc.).
这与 agent-log 接口返回的结构化 JSON 日志不同 Unlike the structured JSON returned by the agent-log endpoint, this is plain-text
是纯文本格式的控制台风格日志 console-style output.
Query参数 Query parameters:
from_line: 从第几行开始读取可选默认0用于增量获取 from_line: line offset to start reading from (optional, default 0, for incremental polling).
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
"logs": [ "logs": [
"[19:46:14] INFO: 搜索完成: 找到 15 条相关事实", "[19:46:14] INFO: Search complete: found 15 relevant facts",
"[19:46:14] INFO: 图谱搜索: graph_id=xxx, query=...", "[19:46:14] INFO: Graph search: graph_id=xxx, query=...",
... ...
], ],
"total_lines": 100, "total_lines": 100,
@ -899,9 +895,9 @@ def get_console_log(report_id: str):
@report_bp.route('/<report_id>/console-log/stream', methods=['GET']) @report_bp.route('/<report_id>/console-log/stream', methods=['GET'])
def stream_console_log(report_id: str): def stream_console_log(report_id: str):
""" """
获取完整的控制台日志一次性获取全部 Get the full console log in one shot (no pagination).
返回 Returns:
{ {
"success": true, "success": true,
"data": { "data": {
@ -930,17 +926,17 @@ def stream_console_log(report_id: str):
}), 500 }), 500
# ============== 工具调用接口(供调试使用)============== # ============== Tool invocation endpoints (for debugging) ==============
@report_bp.route('/tools/search', methods=['POST']) @report_bp.route('/tools/search', methods=['POST'])
def search_graph_tool(): def search_graph_tool():
""" """
图谱搜索工具接口供调试使用 Graph search tool endpoint (for debugging).
请求JSON Request (JSON):
{ {
"graph_id": "mirofish_xxxx", "graph_id": "mirofish_xxxx",
"query": "搜索查询", "query": "search query",
"limit": 10 "limit": 10
} }
""" """
@ -983,9 +979,9 @@ def search_graph_tool():
@report_bp.route('/tools/statistics', methods=['POST']) @report_bp.route('/tools/statistics', methods=['POST'])
def get_graph_statistics_tool(): def get_graph_statistics_tool():
""" """
图谱统计工具接口供调试使用 Graph statistics tool endpoint (for debugging).
请求JSON Request (JSON):
{ {
"graph_id": "mirofish_xxxx" "graph_id": "mirofish_xxxx"
} }

File diff suppressed because it is too large Load Diff