MicroFish/backend/app/api/report.py

1020 lines
30 KiB
Python

"""
Report API routes
Cung cấp các API cho việc generate báo cáo simulation, lấy báo cáo, và chat
"""
import os
import traceback
import threading
from flask import request, jsonify, send_file
from . import report_bp
from ..config import Config
from ..services.report_agent import ReportAgent, ReportManager, ReportStatus
from ..services.simulation_manager import SimulationManager
from ..models.project import ProjectManager
from ..models.task import TaskManager, TaskStatus
from ..utils.logger import get_logger
logger = get_logger('mirofish.api.report')
# ============== Report generation API ==============
@report_bp.route('/generate', methods=['POST'])
def generate_report():
"""
Generate báo cáo phân tích simulation (async task)
Đây là thao tác tốn thời gian, API sẽ trả về task_id ngay lập tức,
dùng GET /api/report/generate/status để kiểm tra progress
Request (JSON):
{
"simulation_id": "sim_xxxx", // bắt buộc
"force_regenerate": false // optional, force regenerate
}
Response:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"task_id": "task_xxxx",
"status": "generating",
"message": "Report generation task started"
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
force_regenerate = data.get('force_regenerate', False)
# Lấy thông tin simulation
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"Simulation does not exist: {simulation_id}"
}), 404
# Kiểm tra đã có report chưa
if not force_regenerate:
existing_report = ReportManager.get_report_by_simulation(simulation_id)
if existing_report and existing_report.status == ReportStatus.COMPLETED:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"report_id": existing_report.report_id,
"status": "completed",
"message": "Report already exists",
"already_generated": True
}
})
# Lấy thông tin project
project = ProjectManager.get_project(state.project_id)
if not project:
return jsonify({
"success": False,
"error": f"Project does not exist: {state.project_id}"
}), 404
graph_id = state.graph_id or project.graph_id
if not graph_id:
return jsonify({
"success": False,
"error": "Missing graph_id, please ensure the graph has been built"
}), 400
simulation_requirement = project.simulation_requirement
if not simulation_requirement:
return jsonify({
"success": False,
"error": "Missing simulation requirement description"
}), 400
# Tạo report_id trước để có thể trả về ngay cho frontend
import uuid
report_id = f"report_{uuid.uuid4().hex[:12]}"
# Tạo async task
task_manager = TaskManager()
task_id = task_manager.create_task(
task_type="report_generate",
metadata={
"simulation_id": simulation_id,
"graph_id": graph_id,
"report_id": report_id
}
)
# Định nghĩa background task
def run_generate():
try:
task_manager.update_task(
task_id,
status=TaskStatus.PROCESSING,
progress=0,
message="Initializing Report Agent..."
)
# Tạo Report Agent
agent = ReportAgent(
graph_id=graph_id,
simulation_id=simulation_id,
project_id=state.project_id,
simulation_requirement=simulation_requirement
)
# Callback cập nhật progress
def progress_callback(stage, progress, message):
task_manager.update_task(
task_id,
progress=progress,
message=f"[{stage}] {message}"
)
# Generate report
report = agent.generate_report(
progress_callback=progress_callback,
report_id=report_id
)
# Lưu report
ReportManager.save_report(report)
if report.status == ReportStatus.COMPLETED:
task_manager.complete_task(
task_id,
result={
"report_id": report.report_id,
"simulation_id": simulation_id,
"status": "completed"
}
)
else:
task_manager.fail_task(task_id, report.error or "Report generation failed")
except Exception as e:
logger.error(f"Report generation failed: {str(e)}")
task_manager.fail_task(task_id, str(e))
# Chạy background thread
thread = threading.Thread(target=run_generate, daemon=True)
thread.start()
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"report_id": report_id,
"task_id": task_id,
"status": "generating",
"message": "Report generation task started. Use /api/report/generate/status to check progress",
"already_generated": False
}
})
except Exception as e:
logger.error(f"Failed to start report generation task: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/generate/status', methods=['POST'])
def get_generate_status():
"""
Truy vấn progress của task generate report
Request (JSON):
{
"task_id": "task_xxxx", // optional, task_id trả về từ generate
"simulation_id": "sim_xxxx" // optional
}
Response:
{
"success": true,
"data": {
"task_id": "task_xxxx",
"status": "processing|completed|failed",
"progress": 45,
"message": "..."
}
}
"""
try:
data = request.get_json() or {}
task_id = data.get('task_id')
simulation_id = data.get('simulation_id')
# Nếu có simulation_id, kiểm tra xem report đã hoàn thành chưa
if simulation_id:
existing_report = ReportManager.get_report_by_simulation(simulation_id)
if existing_report and existing_report.status == ReportStatus.COMPLETED:
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"report_id": existing_report.report_id,
"status": "completed",
"progress": 100,
"message": "Report generated",
"already_completed": True
}
})
if not task_id:
return jsonify({
"success": False,
"error": "Please provide task_id or simulation_id"
}), 400
task_manager = TaskManager()
task = task_manager.get_task(task_id)
if not task:
return jsonify({
"success": False,
"error": f"Task does not exist: {task_id}"
}), 404
return jsonify({
"success": True,
"data": task.to_dict()
})
except Exception as e:
logger.error(f"Failed to query task status: {str(e)}")
return jsonify({
"success": False,
"error": str(e)
}), 500
# ============== Report retrieval APIs ==============
@report_bp.route('/<report_id>', methods=['GET'])
def get_report(report_id: str):
"""
Lấy chi tiết report
Response:
{
"success": true,
"data": {
"report_id": "report_xxxx",
"simulation_id": "sim_xxxx",
"status": "completed",
"outline": {...},
"markdown_content": "...",
"created_at": "...",
"completed_at": "..."
}
}
"""
try:
report = ReportManager.get_report(report_id)
if not report:
return jsonify({
"success": False,
"error": f"Report does not exist: {report_id}"
}), 404
return jsonify({
"success": True,
"data": report.to_dict()
})
except Exception as e:
logger.error(f"Failed to retrieve report: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/by-simulation/<simulation_id>', methods=['GET'])
def get_report_by_simulation(simulation_id: str):
"""
Lấy report theo simulation_id
Response:
{
"success": true,
"data": {
"report_id": "report_xxxx",
...
}
}
"""
try:
report = ReportManager.get_report_by_simulation(simulation_id)
if not report:
return jsonify({
"success": False,
"error": f"No report found for this simulation: {simulation_id}",
"has_report": False
}), 404
return jsonify({
"success": True,
"data": report.to_dict(),
"has_report": True
})
except Exception as e:
logger.error(f"Failed to retrieve report: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/list', methods=['GET'])
def list_reports():
"""
Liệt kê tất cả reports
Query parameters:
simulation_id: Filter theo simulation_id (optional)
limit: Giới hạn số lượng trả về (default 50)
Response:
{
"success": true,
"data": [...],
"count": 10
}
"""
try:
simulation_id = request.args.get('simulation_id')
limit = request.args.get('limit', 50, type=int)
reports = ReportManager.list_reports(
simulation_id=simulation_id,
limit=limit
)
return jsonify({
"success": True,
"data": [r.to_dict() for r in reports],
"count": len(reports)
})
except Exception as e:
logger.error(f"Failed to list reports: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/<report_id>/download', methods=['GET'])
def download_report(report_id: str):
"""
Download report (Markdown format)
Trả về file Markdown
"""
try:
report = ReportManager.get_report(report_id)
if not report:
return jsonify({
"success": False,
"error": f"Report does not exist: {report_id}"
}), 404
md_path = ReportManager._get_report_markdown_path(report_id)
if not os.path.exists(md_path):
# Nếu file MD không tồn tại, tạo file tạm
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write(report.markdown_content)
temp_path = f.name
return send_file(
temp_path,
as_attachment=True,
download_name=f"{report_id}.md"
)
return send_file(
md_path,
as_attachment=True,
download_name=f"{report_id}.md"
)
except Exception as e:
logger.error(f"Failed to download report: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/<report_id>', methods=['DELETE'])
def delete_report(report_id: str):
"""Xóa report"""
try:
success = ReportManager.delete_report(report_id)
if not success:
return jsonify({
"success": False,
"error": f"Report does not exist: {report_id}"
}), 404
return jsonify({
"success": True,
"message": f"Report deleted: {report_id}"
})
except Exception as e:
logger.error(f"Failed to delete report: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== Report Agent chat API ==============
@report_bp.route('/chat', methods=['POST'])
def chat_with_report_agent():
"""
Chat với Report Agent
Report Agent có thể tự động gọi các retrieval tool trong quá trình trả lời
Request (JSON):
{
"simulation_id": "sim_xxxx", // bắt buộc
"message": "Please explain the public opinion trend", // bắt buộc
"chat_history": [ // optional
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
]
}
Response:
{
"success": true,
"data": {
"response": "Agent reply...",
"tool_calls": [list of called tools],
"sources": [information sources]
}
}
"""
try:
data = request.get_json() or {}
simulation_id = data.get('simulation_id')
message = data.get('message')
chat_history = data.get('chat_history', [])
if not simulation_id:
return jsonify({
"success": False,
"error": "Please provide simulation_id"
}), 400
if not message:
return jsonify({
"success": False,
"error": "Please provide message"
}), 400
# Lấy thông tin simulation và project
manager = SimulationManager()
state = manager.get_simulation(simulation_id)
if not state:
return jsonify({
"success": False,
"error": f"Simulation does not exist: {simulation_id}"
}), 404
project = ProjectManager.get_project(state.project_id)
if not project:
return jsonify({
"success": False,
"error": f"Project does not exist: {state.project_id}"
}), 404
graph_id = state.graph_id or project.graph_id
if not graph_id:
return jsonify({
"success": False,
"error": "Missing graph_id"
}), 400
simulation_requirement = project.simulation_requirement or ""
# Tạo Agent và chat
agent = ReportAgent(
graph_id=graph_id,
simulation_id=simulation_id,
project_id=state.project_id,
simulation_requirement=simulation_requirement
)
result = agent.chat(message=message, chat_history=chat_history)
return jsonify({
"success": True,
"data": result
})
except Exception as e:
logger.error(f"Chat failed: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== Report progress & sections APIs ==============
@report_bp.route('/<report_id>/progress', methods=['GET'])
def get_report_progress(report_id: str):
"""
Lấy progress generate report (real-time)
Response:
{
"success": true,
"data": {
"status": "generating",
"progress": 45,
"message": "Generating section: Key Findings",
"current_section": "Key Findings",
"completed_sections": ["Executive Summary", "Simulation Background"],
"updated_at": "2025-12-09T..."
}
}
"""
try:
progress = ReportManager.get_progress(report_id)
if not progress:
return jsonify({
"success": False,
"error": f"Report does not exist or progress unavailable: {report_id}"
}), 404
return jsonify({
"success": True,
"data": progress
})
except Exception as e:
logger.error(f"Failed to retrieve report progress: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/<report_id>/sections', methods=['GET'])
def get_report_sections(report_id: str):
"""
Lấy danh sách các section đã generate (output theo từng section)
Frontend có thể poll API này để lấy section đã generate
mà không cần chờ toàn bộ report hoàn thành
Response:
{
"success": true,
"data": {
"report_id": "report_xxxx",
"sections": [
{
"filename": "section_01.md",
"section_index": 1,
"content": "## Executive Summary\\n\\n..."
}
],
"total_sections": 3,
"is_complete": false
}
}
"""
try:
sections = ReportManager.get_generated_sections(report_id)
# Lấy trạng thái report
report = ReportManager.get_report(report_id)
is_complete = report is not None and report.status == ReportStatus.COMPLETED
return jsonify({
"success": True,
"data": {
"report_id": report_id,
"sections": sections,
"total_sections": len(sections),
"is_complete": is_complete
}
})
except Exception as e:
logger.error(f"Failed to retrieve section list: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/<report_id>/section/<int:section_index>', methods=['GET'])
def get_single_section(report_id: str, section_index: int):
"""
Lấy nội dung một section
Response:
{
"success": true,
"data": {
"filename": "section_01.md",
"content": "## Executive Summary\\n\\n..."
}
}
"""
try:
section_path = ReportManager._get_section_path(report_id, section_index)
if not os.path.exists(section_path):
return jsonify({
"success": False,
"error": f"Section does not exist: section_{section_index:02d}.md"
}), 404
with open(section_path, 'r', encoding='utf-8') as f:
content = f.read()
return jsonify({
"success": True,
"data": {
"filename": f"section_{section_index:02d}.md",
"section_index": section_index,
"content": content
}
})
except Exception as e:
logger.error(f"Failed to retrieve section content: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== 报告状态检查接口 ==============
@report_bp.route('/check/<simulation_id>', methods=['GET'])
def check_report_status(simulation_id: str):
"""
检查模拟是否有报告,以及报告状态
用于前端判断是否解锁Interview功能
返回:
{
"success": true,
"data": {
"simulation_id": "sim_xxxx",
"has_report": true,
"report_status": "completed",
"report_id": "report_xxxx",
"interview_unlocked": true
}
}
"""
try:
report = ReportManager.get_report_by_simulation(simulation_id)
has_report = report is not None
report_status = report.status.value if report else None
report_id = report.report_id if report else None
# 只有报告完成后才解锁interview
interview_unlocked = has_report and report.status == ReportStatus.COMPLETED
return jsonify({
"success": True,
"data": {
"simulation_id": simulation_id,
"has_report": has_report,
"report_status": report_status,
"report_id": report_id,
"interview_unlocked": interview_unlocked
}
})
except Exception as e:
logger.error(f"检查报告状态失败: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API nhật ký Agent ==============
@report_bp.route('/<report_id>/agent-log', methods=['GET'])
def get_agent_log(report_id: str):
"""
Lấy nhật ký thực thi chi tiết của Report Agent
Lấy theo thời gian thực từng bước trong quá trình tạo báo cáo, bao gồm:
- Bắt đầu báo cáo, bắt đầu / hoàn thành planning
- Bắt đầu từng section, tool call, LLM response, hoàn thành
- Báo cáo hoàn thành hoặc thất bại
Query parameters:
from_line: đọc từ dòng nào (optional, mặc định 0, dùng cho incremental fetch)
Response:
{
"success": true,
"data": {
"logs": [
{
"timestamp": "2025-12-13T...",
"elapsed_seconds": 12.5,
"report_id": "report_xxxx",
"action": "tool_call",
"stage": "generating",
"section_title": "Executive Summary",
"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"Failed to get Agent log: {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):
"""
Lấy toàn bộ Agent logs (fetch toàn bộ một lần)
Response:
{
"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"Failed to get Agent log: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API nhật ký Console ==============
@report_bp.route('/<report_id>/console-log', methods=['GET'])
def get_console_log(report_id: str):
"""
Lấy console output logs của Report Agent
Lấy theo thời gian thực các console output trong quá trình tạo báo cáo
(INFO, WARNING, ...).
Khác với API agent-log trả về structured JSON logs,
API này trả về console-style log dạng text thuần.
Query parameters:
from_line: đọc từ dòng nào (optional, mặc định 0, dùng cho incremental fetch)
Response:
{
"success": true,
"data": {
"logs": [
"[19:46:14] INFO: Search completed: found 15 relevant facts",
"[19:46:14] INFO: Graph search: 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"Failed to get console log: {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):
"""
Lấy toàn bộ console logs (fetch toàn bộ một lần)
Response:
{
"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"Failed to get console log: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
# ============== API gọi Tool (dùng cho debugging) ==============
@report_bp.route('/tools/search', methods=['POST'])
def search_graph_tool():
"""
API công cụ graph search (dùng cho debugging)
Request (JSON):
{
"graph_id": "mirofish_xxxx",
"query": "search query",
"limit": 10
}
"""
try:
data = request.get_json() or {}
graph_id = data.get('graph_id')
query = data.get('query')
limit = data.get('limit', 10)
if not graph_id or not query:
return jsonify({
"success": False,
"error": "Please provide graph_id and query"
}), 400
from ..services.zep_tools import ZepToolsService
tools = ZepToolsService()
result = tools.search_graph(
graph_id=graph_id,
query=query,
limit=limit
)
return jsonify({
"success": True,
"data": result.to_dict()
})
except Exception as e:
logger.error(f"Graph search failed: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500
@report_bp.route('/tools/statistics', methods=['POST'])
def get_graph_statistics_tool():
"""
API công cụ thống kê graph (dùng cho debugging)
Request (JSON):
{
"graph_id": "mirofish_xxxx"
}
"""
try:
data = request.get_json() or {}
graph_id = data.get('graph_id')
if not graph_id:
return jsonify({
"success": False,
"error": "Please provide graph_id"
}), 400
from ..services.zep_tools import ZepToolsService
tools = ZepToolsService()
result = tools.get_graph_statistics(graph_id)
return jsonify({
"success": True,
"data": result
})
except Exception as e:
logger.error(f"Failed to get graph statistics: {str(e)}")
return jsonify({
"success": False,
"error": str(e),
"traceback": traceback.format_exc()
}), 500