3034 lines
135 KiB
Python
3034 lines
135 KiB
Python
"""
|
|
Dịch vụ Report Agent
|
|
Sử dụng LangChain + Zep để thực hiện tạo báo cáo mô phỏng theo mô hình ReACT
|
|
|
|
Chức năng:
|
|
1. Dựa trên yêu cầu mô phỏng và thông tin đồ thị Zep để tạo ra báo cáo
|
|
2. Lên kế hoạch cho cấu trúc mục lục trước, sau đó tạo từng đoạn
|
|
3. Mỗi đoạn áp dụng mô hình ReACT để suy nghĩ đa vòng và phản xạ
|
|
4. Hỗ trợ hội thoại với người dùng, tự động gọi công cụ tìm kiếm trong hội thoại
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import time
|
|
import re
|
|
from typing import Dict, Any, List, Optional, Callable
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
from ..config import Config
|
|
from ..utils.llm_client import LLMClient
|
|
from ..utils.logger import get_logger
|
|
from .zep_tools import (
|
|
ZepToolsService,
|
|
SearchResult,
|
|
InsightForgeResult,
|
|
PanoramaResult,
|
|
InterviewResult
|
|
)
|
|
|
|
logger = get_logger('mirofish.report_agent')
|
|
|
|
|
|
class ReportLogger:
|
|
"""
|
|
Trình ghi chi tiết log của Report Agent
|
|
|
|
Tạo file agent_log.jsonl trong thư mục báo cáo, ghi lại chi tiết từng bước.
|
|
Mỗi dòng là một đối tượng JSON hoàn chỉnh, bao gồm timestamp, loại hành động, nội dung chi tiết...
|
|
"""
|
|
|
|
def __init__(self, report_id: str):
|
|
"""
|
|
Khởi tạo trình ghi log
|
|
|
|
Args:
|
|
report_id: ID của báo cáo, dùng để quyết định đường dẫn file log
|
|
"""
|
|
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):
|
|
"""Đảm bảo thư mục chứa file log đã tồn tại"""
|
|
log_dir = os.path.dirname(self.log_file_path)
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
def _get_elapsed_time(self) -> float:
|
|
"""Lấy thời gian tiêu tốn từ lúc bắt đầu tới hiện tại (giây)"""
|
|
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
|
|
):
|
|
"""
|
|
Ghi lại một dòng log
|
|
|
|
Args:
|
|
action: Loại hành động, ví dụ 'start', 'tool_call', 'llm_response', 'section_complete' v.v..
|
|
stage: Giai đoạn hiện tại, ví dụ 'planning', 'generating', 'completed'
|
|
details: Dictionary chứa nội dung chi tiết
|
|
section_title: Tiêu đề chương hiện tại (tùy chọn)
|
|
section_index: Index của chương hiện tại (tùy chọn)
|
|
"""
|
|
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
|
|
}
|
|
|
|
# Ghi nối tiếp vào file 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):
|
|
"""Ghi log báo cáo bắt đầu tạo"""
|
|
self.log(
|
|
action="report_start",
|
|
stage="pending",
|
|
details={
|
|
"simulation_id": simulation_id,
|
|
"graph_id": graph_id,
|
|
"simulation_requirement": simulation_requirement,
|
|
"message": "Report generation task started"
|
|
}
|
|
)
|
|
|
|
def log_planning_start(self):
|
|
"""Ghi log kế hoạch dàn ý bắt đầu"""
|
|
self.log(
|
|
action="planning_start",
|
|
stage="planning",
|
|
details={"message": "Start planning report outline"}
|
|
)
|
|
|
|
def log_planning_context(self, context: Dict[str, Any]):
|
|
"""Ghi log thông tin context lấy được khi lên kế hoạch"""
|
|
self.log(
|
|
action="planning_context",
|
|
stage="planning",
|
|
details={
|
|
"message": "Fetch simulation context info",
|
|
"context": context
|
|
}
|
|
)
|
|
|
|
def log_planning_complete(self, outline_dict: Dict[str, Any]):
|
|
"""Ghi log kế hoạch dàn ý hoàn thành"""
|
|
self.log(
|
|
action="planning_complete",
|
|
stage="planning",
|
|
details={
|
|
"message": "Outline planning completed",
|
|
"outline": outline_dict
|
|
}
|
|
)
|
|
|
|
def log_section_start(self, section_title: str, section_index: int):
|
|
"""Ghi log tiến trình bắt đầu tạo chương"""
|
|
self.log(
|
|
action="section_start",
|
|
stage="generating",
|
|
section_title=section_title,
|
|
section_index=section_index,
|
|
details={"message": f"Start generating section: {section_title}"}
|
|
)
|
|
|
|
def log_react_thought(self, section_title: str, section_index: int, iteration: int, thought: str):
|
|
"""Ghi log quá trình suy nghĩ ReACT"""
|
|
self.log(
|
|
action="react_thought",
|
|
stage="generating",
|
|
section_title=section_title,
|
|
section_index=section_index,
|
|
details={
|
|
"iteration": iteration,
|
|
"thought": thought,
|
|
"message": f"ReACT thought iteration {iteration}"
|
|
}
|
|
)
|
|
|
|
def log_tool_call(
|
|
self,
|
|
section_title: str,
|
|
section_index: int,
|
|
tool_name: str,
|
|
parameters: Dict[str, Any],
|
|
iteration: int
|
|
):
|
|
"""Ghi log thao tác gọi công cụ"""
|
|
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"Calling tool: {tool_name}"
|
|
}
|
|
)
|
|
|
|
def log_tool_result(
|
|
self,
|
|
section_title: str,
|
|
section_index: int,
|
|
tool_name: str,
|
|
result: str,
|
|
iteration: int
|
|
):
|
|
"""Ghi log kết quả gọi công cụ (Toàn bộ nội dung)"""
|
|
self.log(
|
|
action="tool_result",
|
|
stage="generating",
|
|
section_title=section_title,
|
|
section_index=section_index,
|
|
details={
|
|
"iteration": iteration,
|
|
"tool_name": tool_name,
|
|
"result": result, # Kết quả đầy đủ, không cắt ngắn
|
|
"result_length": len(result),
|
|
"message": f"Tool {tool_name} returned result"
|
|
}
|
|
)
|
|
|
|
def log_llm_response(
|
|
self,
|
|
section_title: str,
|
|
section_index: int,
|
|
response: str,
|
|
iteration: int,
|
|
has_tool_calls: bool,
|
|
has_final_answer: bool
|
|
):
|
|
"""Ghi nhận phản hồi LLM (nội dung đầy đủ, không cắt ngắn)"""
|
|
self.log(
|
|
action="llm_response",
|
|
stage="generating",
|
|
section_title=section_title,
|
|
section_index=section_index,
|
|
details={
|
|
"iteration": iteration,
|
|
"response": response, # Phản hồi đầy đủ, không cắt ngắn
|
|
"response_length": len(response),
|
|
"has_tool_calls": has_tool_calls,
|
|
"has_final_answer": has_final_answer,
|
|
"message": f"LLM response (Tool call: {has_tool_calls}, Final answer: {has_final_answer})"
|
|
}
|
|
)
|
|
|
|
def log_section_content(
|
|
self,
|
|
section_title: str,
|
|
section_index: int,
|
|
content: str,
|
|
tool_calls_count: int
|
|
):
|
|
"""Ghi nhận nội dung chương đã tạo (chỉ ghi nhận nội dung, không có nghĩa là toàn bộ chương đã hoàn thành)"""
|
|
self.log(
|
|
action="section_content",
|
|
stage="generating",
|
|
section_title=section_title,
|
|
section_index=section_index,
|
|
details={
|
|
"content": content, # Nội dung đầy đủ, không cắt ngắn
|
|
"content_length": len(content),
|
|
"tool_calls_count": tool_calls_count,
|
|
"message": f"Section {section_title} content generation completed"
|
|
}
|
|
)
|
|
|
|
def log_section_full_complete(
|
|
self,
|
|
section_title: str,
|
|
section_index: int,
|
|
full_content: str
|
|
):
|
|
"""
|
|
Ghi nhận chương đã hoàn thành
|
|
|
|
Frontend nên lắng nghe nhật ký này để xác định chương đó có thực sự hoàn thành hay không, và lấy nội dung đầy đủ
|
|
"""
|
|
self.log(
|
|
action="section_complete",
|
|
stage="generating",
|
|
section_title=section_title,
|
|
section_index=section_index,
|
|
details={
|
|
"content": full_content,
|
|
"content_length": len(full_content),
|
|
"message": f"Section {section_title} generation completed"
|
|
}
|
|
)
|
|
|
|
def log_report_complete(self, total_sections: int, total_time_seconds: float):
|
|
"""Ghi nhận việc tạo báo cáo hoàn tất"""
|
|
self.log(
|
|
action="report_complete",
|
|
stage="completed",
|
|
details={
|
|
"total_sections": total_sections,
|
|
"total_time_seconds": round(total_time_seconds, 2),
|
|
"message": "Report generation completed"
|
|
}
|
|
)
|
|
|
|
def log_error(self, error_message: str, stage: str, section_title: str = None):
|
|
"""Ghi nhận lỗi"""
|
|
self.log(
|
|
action="error",
|
|
stage=stage,
|
|
section_title=section_title,
|
|
section_index=None,
|
|
details={
|
|
"error": error_message,
|
|
"message": f"An error occurred: {error_message}"
|
|
}
|
|
)
|
|
|
|
|
|
class ReportConsoleLogger:
|
|
"""
|
|
Trình ghi Log qua console của Report Agent
|
|
|
|
Ghi nhật ký kiểu console (INFO, WARNING, v.v.) vào tệp console_log.txt trong mục lưu báo cáo.
|
|
Khác với agent_log.jsonl, những nhật ký này ở định dạng văn bản thuần túy.
|
|
"""
|
|
|
|
def __init__(self, report_id: str):
|
|
"""
|
|
Khởi tạo trình ghi log console
|
|
|
|
Args:
|
|
report_id: ID báo cáo, dùng để tự xác định đường dẫn file log
|
|
"""
|
|
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):
|
|
"""Đảm bảo thư mục lưu file log tồn tại"""
|
|
log_dir = os.path.dirname(self.log_file_path)
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
def _setup_file_handler(self):
|
|
"""Cấu hình trình xử lý tệp (FileHandler) để ghi nhật ký vào tệp"""
|
|
import logging
|
|
|
|
# Tạo file handler
|
|
self._file_handler = logging.FileHandler(
|
|
self.log_file_path,
|
|
mode='a',
|
|
encoding='utf-8'
|
|
)
|
|
self._file_handler.setLevel(logging.INFO)
|
|
|
|
# Cấu trúc log đơn giản tương tự như phiên làm việc console
|
|
formatter = logging.Formatter(
|
|
'[%(asctime)s] %(levelname)s: %(message)s',
|
|
datefmt='%H:%M:%S'
|
|
)
|
|
self._file_handler.setFormatter(formatter)
|
|
|
|
# Thêm file hander vào logger của report_agent
|
|
loggers_to_attach = [
|
|
'mirofish.report_agent',
|
|
'mirofish.zep_tools',
|
|
]
|
|
|
|
for logger_name in loggers_to_attach:
|
|
target_logger = logging.getLogger(logger_name)
|
|
# Tránh thêm lại handler trùng lặp
|
|
if self._file_handler not in target_logger.handlers:
|
|
target_logger.addHandler(self._file_handler)
|
|
|
|
def close(self):
|
|
"""Đóng file handler và gỡ nó khỏi cấu hình 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):
|
|
"""Đảm bảo đóng file handler khi hàm hủy (destructor) gọi"""
|
|
self.close()
|
|
|
|
|
|
class ReportStatus(str, Enum):
|
|
"""Trạng thái báo cáo"""
|
|
PENDING = "pending"
|
|
PLANNING = "planning"
|
|
GENERATING = "generating"
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
|
|
|
|
@dataclass
|
|
class ReportSection:
|
|
"""Chương báo cáo"""
|
|
title: str
|
|
content: str = ""
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"title": self.title,
|
|
"content": self.content
|
|
}
|
|
|
|
def to_markdown(self, level: int = 2) -> str:
|
|
"""Chuyển đổi sang định dạng Markdown"""
|
|
md = f"{'#' * level} {self.title}\n\n"
|
|
if self.content:
|
|
md += f"{self.content}\n\n"
|
|
return md
|
|
|
|
|
|
@dataclass
|
|
class ReportOutline:
|
|
"""Dàn ý báo cáo"""
|
|
title: str
|
|
summary: str
|
|
sections: List[ReportSection]
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"title": self.title,
|
|
"summary": self.summary,
|
|
"sections": [s.to_dict() for s in self.sections]
|
|
}
|
|
|
|
def to_markdown(self) -> str:
|
|
"""Chuyển đổi sang định dạng Markdown"""
|
|
md = f"# {self.title}\n\n"
|
|
md += f"> {self.summary}\n\n"
|
|
for section in self.sections:
|
|
md += section.to_markdown()
|
|
return md
|
|
|
|
|
|
@dataclass
|
|
class Report:
|
|
"""Báo cáo đầy đủ"""
|
|
report_id: str
|
|
simulation_id: str
|
|
graph_id: str
|
|
simulation_requirement: str
|
|
status: ReportStatus
|
|
outline: Optional[ReportOutline] = None
|
|
markdown_content: str = ""
|
|
created_at: str = ""
|
|
completed_at: str = ""
|
|
error: Optional[str] = None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"report_id": self.report_id,
|
|
"simulation_id": self.simulation_id,
|
|
"graph_id": self.graph_id,
|
|
"simulation_requirement": self.simulation_requirement,
|
|
"status": self.status.value,
|
|
"outline": self.outline.to_dict() if self.outline else None,
|
|
"markdown_content": self.markdown_content,
|
|
"created_at": self.created_at,
|
|
"completed_at": self.completed_at,
|
|
"error": self.error
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Hằng số Prompt Mẫu
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# ── Mô tả Công cụ ──
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Prompt English
|
|
# TOOL_DESC_INSIGHT_FORGE = """\
|
|
# [Deep Insight Retrieval - Powerful Retrieval Tool]
|
|
# This is our powerful retrieval function, specifically designed for deep analysis. It will:
|
|
# 1. Automatically decompose your question into multiple sub-questions
|
|
# 2. Retrieve information from the simulation graph across multiple dimensions
|
|
# 3. Integrate the results of semantic search, entity analysis, and relationship chain tracking
|
|
# 4. Return the most comprehensive and deeply retrieved content
|
|
|
|
# [Usage Scenarios]
|
|
# - When you need to analyze a topic deeply
|
|
# - When you need to understand multiple aspects of an event
|
|
# - When you need rich material to support a report section
|
|
|
|
# [Returned Content]
|
|
# - Relevant original facts (can be cited directly)
|
|
# - Core entity insights
|
|
# - Relationship chain analysis
|
|
# """
|
|
|
|
# Prompt Vietnamese
|
|
TOOL_DESC_INSIGHT_FORGE = """\
|
|
[Truy xuất sâu - Công cụ truy xuất mạnh mẽ]
|
|
Đây là chức năng truy xuất mạnh mẽ của chúng tôi, được thiết kế chuyên cho phân tích sâu. Nó sẽ:
|
|
1. Tự động chia câu hỏi của bạn thành nhiều câu hỏi con
|
|
2. Truy xuất thông tin từ đồ thị mô phỏng theo nhiều chiều
|
|
3. Tích hợp kết quả từ tìm kiếm ngữ nghĩa, phân tích thực thể, và theo dõi chuỗi quan hệ
|
|
4. Trả về nội dung truy xuất toàn diện và sâu sắc nhất
|
|
|
|
[Trường hợp sử dụng]
|
|
- Cần phân tích sâu một chủ đề
|
|
- Cần hiểu nhiều khía cạnh của một sự kiện
|
|
- Cần lấy tài liệu phong phú để hỗ trợ các chương báo cáo
|
|
|
|
[Nội dung trả về]
|
|
- Các sự thật liên quan gốc (có thể trích dẫn trực tiếp)
|
|
- Sự sâu sắc về thực thể cốt lõi
|
|
- Phân tích chuỗi quan hệ
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# TOOL_DESC_PANORAMA_SEARCH = """\
|
|
# [Panoramic Search - Get Complete Overview]
|
|
# This tool is used to get a complete overview of the simulation results, especially suitable for understanding the evolution of of events. It will:
|
|
# 1. Get all relevant nodes and relationships
|
|
# 2. Distinguish between current valid facts and historical/expired facts
|
|
# 3. Help you understand how public opinion has evolved
|
|
|
|
# [Usage Scenarios]
|
|
# - Need to understand the complete development trajectory of an event
|
|
# - Need to compare public opinion changes across different stages
|
|
# - Need comprehensive entity and relationship information
|
|
|
|
# [Returned Content]
|
|
# - Current valid facts (latest simulation results)
|
|
# - Historical/expired facts (evolution records)
|
|
# - All involved entities
|
|
# """
|
|
|
|
TOOL_DESC_PANORAMA_SEARCH = """\
|
|
[Tìm kiếm toàn cảnh - Lấy tổng quan hoàn chỉnh]
|
|
Công cụ này được sử dụng để lấy tổng quan hoàn chỉnh của kết quả mô phỏng, đặc biệt phù hợp để hiểu quá trình tiến hóa của sự kiện. Nó sẽ:
|
|
1. Lấy tất cả các nút và quan hệ liên quan
|
|
2. Phân biệt giữa các sự kiện hợp lệ hiện tại và các sự kiện lịch sử/hết hạn
|
|
3. Giúp bạn hiểu dư luận đã tiến hóa như thế nào
|
|
|
|
[Trường hợp sử dụng]
|
|
- Cần hiểu quỹ đạo phát triển hoàn chỉnh của một sự kiện
|
|
- Cần so sánh thay đổi dư luận ở các giai đoạn khác nhau
|
|
- Cần lấy thông tin thực thể và quan hệ toàn diện
|
|
|
|
[Nội dung trả về]
|
|
- Các sự kiện hợp lệ hiện tại (kết quả mô phỏng mới nhất)
|
|
- Các sự kiện lịch sử/hết hạn (ghi lại tiến hóa)
|
|
- Tất cả các thực thể liên quan
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# TOOL_DESC_QUICK_SEARCH = """\
|
|
# [Quick Search - Fast Retrieval]
|
|
# A lightweight fast retrieval tool, suitable for simple, direct information queries.
|
|
|
|
# [Usage Scenarios]
|
|
# - Need to quickly look up a specific piece of information
|
|
# - Need to verify a fact
|
|
# - Simple information retrieval
|
|
|
|
# [Returned Content]
|
|
# - List of facts most relevant to the query
|
|
# """
|
|
|
|
TOOL_DESC_QUICK_SEARCH = """\
|
|
[Tìm kiếm đơn giản - Truy xuất nhanh]
|
|
Công cụ truy xuất nhanh nhẹn, phù hợp cho các truy vấn thông tin đơn giản, trực tiếp.
|
|
|
|
[Trường hợp sử dụng]
|
|
- Cần tìm nhanh thông tin cụ thể
|
|
- Cần xác minh một sự kiện
|
|
- Truy xuất thông tin đơn giản
|
|
|
|
[Nội dung trả về]
|
|
- Danh sách các sự kiện liên quan nhất đến truy vấn
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# TOOL_DESC_INTERVIEW_AGENTS = """\
|
|
# [Deep Interview - Real Agent Interview (Dual Platform)]
|
|
# Call the OASIS simulation environment's interview API to conduct real interviews with currently running simulation Agents!
|
|
# This is not an LLM simulation, but calls the real interview endpoint to get the simulation Agent's original answer.
|
|
# By default, interviews are conducted simultaneously on both Twitter and Reddit platforms to get more comprehensive perspectives.
|
|
|
|
# Functional Process:
|
|
# 1. Automatically reads persona files to understand all simulation Agents
|
|
# 2. Intelligently select agents most relevant to the interview topic (such as students, media, officials, etc.)
|
|
# 3. Automatically generates interview questions
|
|
# 2. Intelligently select agents most relevant to the interview topic (such as students, media, officials, etc.)
|
|
# 5. Integrates all interview results, providing multi-perspective analysis
|
|
|
|
# [Usage Scenarios]
|
|
# - Need to understand event views from different role perspectives (How do students see it? How does media see it? How do officials say it?)
|
|
# - Need to collect multi-party opinions and positions
|
|
# - Need to get real responses from simulation agents (from OASIS simulation environment)
|
|
# - Want to make the report more vivid, including "interview records"
|
|
|
|
# [Returned Content]
|
|
# - Identity information of interviewed agents
|
|
# - Interview responses of each agent on Twitter and Reddit platforms
|
|
# - Key quotes (can be cited directly)
|
|
# - Interview summary and perspective comparison
|
|
|
|
# [IMPORTANT] Requires OASIS simulation environment to be running to use this function!
|
|
# """
|
|
|
|
TOOL_DESC_INTERVIEW_AGENTS = """\
|
|
[Phỏng vấn sâu - Phỏng vấn Agent thực (Nền tảng kép)]
|
|
Gọi API phỏng vấn của môi trường mô phỏng OASIS để tiến hành phỏng vấn thực với các agent mô phỏng đang chạy!
|
|
Đây không phải là mô phỏng LLM, mà là gọi các giao diện phỏng vấn thực để lấy phản hồi gốc từ các agent mô phỏng.
|
|
Mặc định, phỏng vấn được tiến hành đồng thời trên cả hai nền tảng Twitter và Reddit để có được góc nhìn toàn diện hơn.
|
|
|
|
Quy trình chức năng:
|
|
1. Tự động đọc file nhân cách để hiểu tất cả các agent mô phỏng
|
|
2. Chọn thông minh các agent liên quan nhất đến chủ đề phỏng vấn (như sinh viên, truyền thông, quan chức, v.v.)
|
|
3. Tự động tạo câu hỏi phỏng vấn
|
|
4. Gọi giao diện /api/simulation/interview/batch để tiến hành phỏng vấn thực trên nền tảng kép
|
|
5. Tích hợp tất cả kết quả phỏng vấn để cung cấp phân tích đa góc nhìn
|
|
|
|
[Trường hợp sử dụng]
|
|
- Cần hiểu quan điểm sự kiện từ các góc nhìn vai trò khác nhau (Sinh viên nghĩ gì? Truyền thông nghĩ gì? Quan chức nói gì?)
|
|
- Cần thu thập ý kiến và lập trường đa phương
|
|
- Cần lấy phản hồi thực từ các agent mô phỏng (từ môi trường mô phỏng OASIS)
|
|
- Muốn làm cho báo cáo sống động hơn, bao gồm "ghi chép phỏng vấn"
|
|
|
|
[Nội dung trả về]
|
|
- Thông tin danh tính của các agent được phỏng vấn
|
|
- Phản hồi phỏng vấn của mỗi agent trên nền tảng Twitter và Reddit
|
|
- Các trích dẫn chính (có thể trích dẫn trực tiếp)
|
|
- Tóm tắt phỏng vấn và so sánh góc nhìn
|
|
|
|
[QUAN TRỌNG] Yêu cầu môi trường mô phỏng OASIS đang chạy để sử dụng chức năng này!
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# PLAN_SYSTEM_PROMPT = """\
|
|
# You are a writing expert for "Future Prediction Reports", possessing a "God's eye view" of the simulated world - you can observe the behaviors, speeches, and interactions of every Agent in the simulation.
|
|
|
|
# [Core Concept]
|
|
# We have built a simulated world and injected specific "simulation requirements" into it as variables. The evolutionary outcome of the simulated world is the prediction of what might happen in the future. What you are observing is not "experimental data", but a "preview of the future".
|
|
|
|
# [Your Task]
|
|
# Write a "Future Prediction Report" to answer:
|
|
# 1. Under our set conditions, what happened in the future?
|
|
# 2. How did various Agents (groups) react and act?
|
|
# 3. What noteworthy future trends and risks did this simulation reveal?
|
|
|
|
# [Report Positioning]
|
|
# - ✅ This is a simulation-based future prediction report, revealing "if this, what will the future be like"
|
|
# - ✅ Focus on prediction results: event trends, group reactions, emergent phenomena, potential risks
|
|
# - ✅ The actions and words of Agents in the simulated world are predictions of future human behavior
|
|
# - ❌ Not an analysis of the current real-world situation
|
|
# - ❌ Not a general public opinion summary
|
|
|
|
# [Chapter Quantity Limit]
|
|
# - Minimum of 2 chapters, maximum of 5 chapters
|
|
# - No sub-chapters needed, write complete content directly for each chapter
|
|
# - Content should be refined, focusing on core prediction findings
|
|
# - Chapter structure should be designed by you independently based on prediction results
|
|
|
|
# Please output the report outline in JSON format as follows:
|
|
# {
|
|
# "title": "Report Title",
|
|
# "summary": "Report Summary (One sentence summarizing the core prediction findings)",
|
|
# "sections": [
|
|
# {
|
|
# "title": "Chapter Title",
|
|
# "description": "Chapter Content Description"
|
|
# }
|
|
# ]
|
|
# }
|
|
|
|
# Note: The sections array must have a minimum of 2 and a maximum of 5 elements!
|
|
# """
|
|
|
|
PLAN_SYSTEM_PROMPT = """\
|
|
Bạn là một chuyên gia viết "Báo cáo Dự báo Tương lai", với "góc nhìn của Chúa" về thế giới mô phỏng — bạn có thể thấu hiểu hành vi, lời nói, và tương tác của mọi agent trong mô phỏng.
|
|
|
|
[Khái niệm Cốt lõi]
|
|
Chúng tôi đã xây dựng một thế giới mô phỏng và tiêm các "yêu cầu mô phỏng" cụ thể làm biến số. Kết quả tiến hóa của thế giới mô phỏng là một dự báo về những gì có thể xảy ra trong tương lai. Những gì bạn đang quan sát không phải là "dữ liệu thử nghiệm," mà là "bản xem trước của tương lai."
|
|
|
|
[Nhiệm vụ của bạn]
|
|
Viết một "Báo cáo Dự báo Tương lai" để trả lời:
|
|
1. Trong điều kiện chúng tôi đặt ra, tương lai đã xảy ra điều gì?
|
|
2. Các agent (nhóm) khác nhau đã phản ứng và hành động như thế nào?
|
|
3. Mô phỏng này tiết lộ những xu hướng và rủi ro tương lai nào đáng chú ý?
|
|
|
|
[Định vị Báo cáo]
|
|
- ✅ Đây là báo cáo dự báo tương lai dựa trên mô phỏng, tiết lộ "nếu thế này, thì sẽ thế nào"
|
|
- ✅ Tập trung vào kết quả dự báo: xu hướng sự kiện, phản ứng nhóm, hiện tượng nổi lên, rủi ro tiềm ẩn
|
|
- ✅ Lời nói và hành động của các agent trong thế giới mô phỏng là dự báo về hành vi con người tương lai
|
|
- ❌ Không phải là phân tích tình hình thế giới thực hiện tại
|
|
- ❌ Không phải là tóm tắt dư luận chung chung
|
|
|
|
[Giới hạn Số lượng Chương]
|
|
- Tối thiểu 2 chương, tối đa 5 chương
|
|
- Không cần chương con, viết nội dung hoàn chỉnh trực tiếp cho mỗi chương
|
|
- Nội dung nên được tinh gọn, tập trung vào các phát hiện dự báo cốt lõi
|
|
- Cấu trúc chương do bạn thiết kế dựa trên kết quả dự báo
|
|
|
|
Vui lòng xuất cấu trúc báo cáo theo định dạng JSON như sau:
|
|
{
|
|
"title": "Tiêu đề Báo cáo",
|
|
"summary": "Tóm tắt Báo cáo (một câu tóm tắt các phát hiện dự báo cốt lõi)",
|
|
"sections": [
|
|
{
|
|
"title": "Tiêu đề Chương",
|
|
"description": "Mô tả Nội dung Chương"
|
|
}
|
|
]
|
|
}
|
|
|
|
Lưu ý: Mảng sections phải có ít nhất 2 và tối đa 5 phần tử!
|
|
"""
|
|
|
|
# PLAN_USER_PROMPT_TEMPLATE = """\
|
|
# [Prediction Scenario Setting]
|
|
# The variables (simulation requirements) we injected into the simulated world: {simulation_requirement}
|
|
|
|
# [Simulated World Scale]
|
|
# - Number of entities participating in the simulation: {total_nodes}
|
|
# - Number of relationships generated between entities: {total_edges}
|
|
# - Entity type distribution: {entity_types}
|
|
# - Number of active Agents: {total_entities}
|
|
|
|
# [Sample Future Facts Predicted by Simulation]
|
|
# {related_facts_json}
|
|
|
|
# Please examine this future preview from a "God's eye view":
|
|
# 1. Under our set conditions, what state did the future present?
|
|
# 2. How did various groups (Agents) react and act?
|
|
# 3. What noteworthy future trends did this simulation reveal?
|
|
|
|
# Based on the prediction results, design the most suitable report chapter structure.
|
|
|
|
# [Reminder again] Report chapter quantity: Minimum 2, maximum 5, content should be concise and focused on core prediction findings.
|
|
# """
|
|
|
|
PLAN_USER_PROMPT_TEMPLATE = """\
|
|
[Cài đặt kịch bản dự báo]
|
|
Các biến số (yêu cầu mô phỏng) chúng tôi tiêm vào thế giới mô phỏng: {simulation_requirement}
|
|
|
|
[Quy mô Thế giới Mô phỏng]
|
|
- Số lượng thực thể tham gia mô phỏng: {total_nodes}
|
|
- Số lượng quan hệ được tạo giữa các thực thể: {total_edges}
|
|
- Phân phối loại thực thể: {entity_types}
|
|
- Số lượng agent hoạt động: {total_entities}
|
|
|
|
[Mẫu sự kiện tương lai được dự báo bởi mô phỏng]
|
|
{related_facts_json}
|
|
|
|
Vui lòng xem xét bản xem trước tương lai này từ "góc nhìn của Chúa":
|
|
1. Trong điều kiện chúng tôi đặt ra, tương lai đã trình bày trạng thái gì?
|
|
2. Các nhóm (agent) khác nhau đã phản ứng và hành động như thế nào?
|
|
3. Mô phỏng này tiết lộ những xu hướng tương lai nào?
|
|
|
|
Dựa trên kết quả dự báo, thiết kế cấu trúc chương báo cáo phù hợp nhất.
|
|
|
|
[Nhắc lại] Số lượng chương báo cáo: tối thiểu 2, tối đa 5, nội dung nên được tinh gọn và tập trung vào các phát hiện dự báo cốt lõi.
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# SECTION_SYSTEM_PROMPT_TEMPLATE = """\
|
|
# You are a writing expert for "Future Prediction Reports", currently writing one section of the report.
|
|
|
|
# Report Title: {report_title}
|
|
# Report Summary: {report_summary}
|
|
# Prediction Scenario (Simulation Requirement): {simulation_requirement}
|
|
|
|
# Section currently being written: {section_title}
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# [Core Concept]
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# The simulated world is a preview of the future. We injected specific conditions (simulation requirements) into the simulated world.
|
|
# The behaviors and interactions of Agents in the simulation are predictions of future human behavior.
|
|
|
|
# Your task is to:
|
|
# - Reveal what happened in the future under the set conditions
|
|
# - Predict how various groups (Agents) reacted and acted
|
|
# - Discover noteworthy future trends, risks, and opportunities
|
|
|
|
# ❌ Do not write this as an analysis of the real world's current status
|
|
# ✅ Focus on "what the future will be" - the simulation results are the predicted future
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# [Most Important Rules - MUST Obey]
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# 1. [MUST use tools to observe the simulated world]
|
|
# - You are observing the future preview from a "God's eye view"
|
|
# - All content MUST come from events, words, and actions of Agents occurred in the simulated world
|
|
# - It is strictly forbidden to use your own knowledge to write report content
|
|
# - For each chapter, you MUST call tools at least 3 times (maximum 5 times) to observe the simulated world, which represents the future
|
|
|
|
# 2. [MUST quote the exact original words and actions of Agents]
|
|
# - The Agent's statements and behaviors are predictions of future human behavior
|
|
# - Use quote formatting in the report to display these predictions, for example:
|
|
# > "A certain group of people will say: Original content..."
|
|
# - These quotes are the core evidence of the simulation prediction
|
|
|
|
# 3. [Language Consistency - Quoted Content Must Be Translated to Report Language]
|
|
# - The content returned by the tools may contain English or mixed Vietnamese and English expressions
|
|
# - If the simulation requirements and original materials are in Vietnamese, the report must be written entirely in Vietnamese
|
|
# - When you quote English or mixed content returned by the tool, you must translate it into fluent Vietnamese before writing it into the report
|
|
# - Keep the original meaning unchanged when translating, and ensure the expression is natural and fluent
|
|
# - This rule applies to both the main text and the content in the quote block (> format)
|
|
|
|
# 4. [Faithful Presentation of Prediction Results]
|
|
# - Report content must reflect the simulation results representing the future in the simulated world
|
|
# - Do not add information that does not exist in the simulation
|
|
# - If information in a certain aspect is insufficient, state it truthfully
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# [⚠️ Formatting Specifications - Extremely Important!]
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# [One Chapter = Minimum Content Unit]
|
|
# - Each chapter is the minimum blocking unit of the report
|
|
# - ❌ Do not use any Markdown headings (#, ##, ###, ####, etc.) within the chapter
|
|
# - ❌ Do not add a main chapter heading at the beginning of the content
|
|
# - ✅ Chapter titles are added automatically by the system, you only need to write the plain text content
|
|
# - ✅ Use **bold text**, paragraph breaks, quotes, and lists to organize content, but do not use headings
|
|
|
|
# [Correct Example]
|
|
# ```
|
|
# This chapter analyzes the public opinion dissemination trend of the event. Through deep analysis of simulation data, we found...
|
|
|
|
# **Initial Outbreak Stage**
|
|
|
|
# Weibo, as the first scene of public opinion, assumed the core function of initial information release:
|
|
|
|
# > "Weibo contributed 68% of the initial buzz..."
|
|
|
|
# **Emotion Amplification Stage**
|
|
|
|
# The Douyin platform further amplified the event's impact:
|
|
|
|
# - Strong visual impact
|
|
# - High emotional resonance
|
|
# ```
|
|
|
|
# [Incorrect Example]
|
|
# ```
|
|
# ## Executive Summary ← Error! Do not add any headings
|
|
# ### 1. Initial Stage ← Error! Do not use ### for sub-sections
|
|
# #### 1.1 Detailed Analysis ← Error! Do not use #### for further division
|
|
|
|
# This chapter analyzes...
|
|
# ```
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# [Available Retrieval Tools] (Call 3-5 times per section)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# {tools_description}
|
|
|
|
# [Tool Usage Suggestions - Please mix different tools, do not just use one]
|
|
# - insight_forge: Deep insight analysis, automatically decomposes questions and retrieves facts and relationships from multiple dimensions
|
|
# - panorama_search: Wide-angle panoramic search, understands the whole picture, timeline, and evolution process of an event
|
|
# - quick_search: Quickly verifies a specific information point
|
|
# - interview_agents: Interviews simulation Agents to get first-person views and real reactions from different roles
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# [Workflow]
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# For each reply you can only do one of the following two things (not both simultaneously):
|
|
|
|
# Option A - Call a tool:
|
|
# Output your thoughts, then use the following format to call a tool:
|
|
# <tool_call>
|
|
# {{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}}
|
|
# </tool_call>
|
|
# The system will execute the tool and return the result to you. You do not need to and cannot write the tool return result yourself.
|
|
|
|
# Option B - Output Final Content:
|
|
# When you have obtained enough information through tools, output the chapter content starting with "Final Answer:".
|
|
|
|
# ⚠️ Strictly Forbidden:
|
|
# - Forbidden to include both tool calls and Final Answer in a single reply
|
|
# - Forbidden to fabricate tool return results (Observation) yourself, all tool results are injected by the system
|
|
# - Call a maximum of one tool per reply
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# [Chapter Content Requirements]
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# 1. Content must be based on simulation data retrieved by tools
|
|
# 2. Quote the original text extensively to demonstrate the simulation effect
|
|
# 3. Use Markdown format (but forbid using headings):
|
|
# - Use **bold text** to mark key points (instead of subheadings)
|
|
# - Use lists (- or 1. 2. 3.) to organize points
|
|
# - Use blank lines to separate different paragraphs
|
|
# - ❌ Forbidden to use #, ##, ###, #### and any other heading syntax
|
|
# 4. [Quote Formatting Specifications - Must be a separate paragraph]
|
|
# Quotes must be an independent paragraph, with a blank line before and after, cannot be mixed in the paragraph:
|
|
|
|
# ✅ Correct format:
|
|
# ```
|
|
# The school's response was considered to lack substantive content.
|
|
|
|
# > "The school's response model appears rigid and slow in the rapidly changing social media environment."
|
|
|
|
# This evaluation reflects the general dissatisfaction of the public.
|
|
# ```
|
|
|
|
# ❌ Incorrect format:
|
|
# ```
|
|
# The school's response was considered to lack substantive content. > "The school's response model..." This evaluation reflects...
|
|
# ```
|
|
# 5. Maintain logical coherence with other chapters
|
|
# 6. [Avoid Repetition] Carefully read the completed chapter content below, do not repeat the same information
|
|
# 7. [Emphasize Again] Do not add any headings! Use **bold** instead of section headings"""
|
|
|
|
# SECTION_USER_PROMPT_TEMPLATE = """\
|
|
# Completed Chapter Content (Please read carefully to avoid duplication):
|
|
# {previous_content}
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# [Current Task] Writing Chapter: {section_title}
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# [Important Reminders]
|
|
# 1. Read the completed chapters above carefully to avoid repeating the same content!
|
|
# 2. Must call tools first to get simulation data before starting
|
|
# 3. Please mix different tools, do not use only one
|
|
# 4. Report content must come from retrieval results, do not use your own knowledge
|
|
|
|
# [⚠️ Formatting Warning - Must be Obeyed]
|
|
# - ❌ Do not write any headings (no #, ##, ###, ####)
|
|
# - ❌ Do not write "{section_title}" as the beginning
|
|
# - ✅ Chapter titles are automatically added by the system
|
|
# - ✅ Write the main text directly, use **bold** instead of section headings
|
|
|
|
# Please begin:
|
|
# 1. First, think (Thought) what information this chapter needs
|
|
# 2. Then, call tools (Action) to get simulation data
|
|
# 3. After collecting enough information, output Final Answer (plain text, no headings)
|
|
# """
|
|
|
|
SECTION_SYSTEM_PROMPT_TEMPLATE = """\
|
|
Bạn là một chuyên gia viết "Báo cáo Dự đoán Tương lai", hiện đang viết một phần trong báo cáo đó.
|
|
|
|
Tiêu đề báo cáo: {report_title}
|
|
Tóm tắt báo cáo: {report_summary}
|
|
Kịch bản Dự đoán (Yêu cầu Mô phỏng): {simulation_requirement}
|
|
|
|
Phần đang được viết: {section_title}
|
|
|
|
═══════════════════════════════════════════════════════════════
|
|
[Khái niệm Cốt lõi]
|
|
═══════════════════════════════════════════════════════════════
|
|
|
|
Thế giới mô phỏng là một bản xem trước của tương lai. Chúng tôi đã đưa các điều kiện cụ thể (yêu cầu mô phỏng) vào thế giới này.
|
|
Các hành vi và tương tác của các Tác nhân (Agents) trong quá trình mô phỏng chính là những dự đoán về hành vi của con người trong tương lai.
|
|
|
|
Nhiệm vụ của bạn là:
|
|
- Tiết lộ những gì đã xảy ra trong tương lai theo các điều kiện đã thiết lập
|
|
- Dự đoán cách các nhóm khác nhau (Agents) đã phản ứng và hành động
|
|
- Phát hiện các xu hướng, rủi ro và cơ hội đáng chú ý trong tương lai
|
|
|
|
❌ Không viết nội dung này như một bài phân tích về hiện trạng của thế giới thực
|
|
✅ Tập trung vào "tương lai sẽ như thế nào" - kết quả mô phỏng chính là tương lai được dự đoán
|
|
|
|
═══════════════════════════════════════════════════════════════
|
|
[Quy tắc QUAN TRỌNG NHẤT - PHẢI Tuân thủ]
|
|
═══════════════════════════════════════════════════════════════
|
|
|
|
1. [PHẢI sử dụng công cụ để quan sát thế giới mô phỏng]
|
|
- Bạn đang quan sát bản xem trước tương lai từ "góc nhìn của Chúa"
|
|
- Tất cả nội dung PHẢI đến từ các sự kiện, lời nói và hành động của các Tác nhân đã xảy ra trong thế giới mô phỏng
|
|
- Nghiêm cấm sử dụng kiến thức cá nhân của bạn để viết nội dung báo cáo
|
|
- Đối với mỗi chương, bạn PHẢI gọi công cụ ít nhất 3 lần (tối đa 5 lần) để quan sát thế giới mô phỏng
|
|
|
|
2. [PHẢI trích dẫn chính xác nguyên văn lời nói và hành động của các Tác nhân]
|
|
- Các tuyên bố và hành vi của Tác nhân là những dự đoán về hành vi con người trong tương lai
|
|
- Sử dụng định dạng trích dẫn trong báo cáo để hiển thị các dự đoán này, ví dụ:
|
|
> "Một nhóm người nhất định sẽ nói: [Nội dung gốc]..."
|
|
- Những trích dẫn này là bằng chứng cốt lõi của dự đoán mô phỏng
|
|
|
|
3. [Tính nhất quán về Ngôn ngữ - Nội dung trích dẫn phải được dịch sang ngôn ngữ báo cáo]
|
|
- Nội dung trả về từ các công cụ có thể chứa tiếng Anh hoặc hỗn hợp tiếng Việt và tiếng Anh.
|
|
- Nếu yêu cầu mô phỏng và tài liệu gốc bằng tiếng Việt, báo cáo phải được viết hoàn toàn bằng tiếng Việt
|
|
- Khi bạn trích dẫn nội dung tiếng Anh hoặc hỗn hợp từ công cụ, bạn phải dịch sang tiếng Việt lưu loát trước khi đưa vào báo cáo
|
|
- Giữ nguyên ý nghĩa gốc khi dịch và đảm bảo cách diễn đạt tự nhiên
|
|
- Quy tắc này áp dụng cho cả văn bản chính và nội dung trong khối trích dẫn (định dạng >)
|
|
|
|
4. [Trình bày trung thực kết quả dự đoán]
|
|
- Nội dung báo cáo phải phản ánh kết quả mô phỏng đại diện cho tương lai
|
|
- Không thêm thông tin không tồn tại trong mô phỏng
|
|
- Nếu thông tin ở một khía cạnh nào đó không đủ, hãy nêu rõ sự thật
|
|
|
|
═══════════════════════════════════════════════════════════════
|
|
[⚠️ Quy cách Định dạng - Cực kỳ Quan trọng!]
|
|
═══════════════════════════════════════════════════════════════
|
|
|
|
[Một Chương = Đơn vị Nội dung Tối thiểu]
|
|
- Mỗi chương là đơn vị chặn tối thiểu của báo cáo
|
|
- ❌ Không sử dụng bất kỳ tiêu đề Markdown nào (#, ##, ###, ####, v.v.) trong chương
|
|
- ❌ Không thêm tiêu đề chương chính ở đầu nội dung
|
|
- ✅ Tiêu đề chương được hệ thống tự động thêm vào, bạn chỉ cần viết nội dung văn bản thuần túy
|
|
- ✅ Sử dụng **chữ đậm**, ngắt đoạn, trích dẫn và danh sách để tổ chức nội dung, nhưng không dùng tiêu đề (headings)
|
|
|
|
[Ví dụ Đúng]
|
|
```
|
|
Chương này phân tích xu hướng lan truyền dư luận của sự kiện. Thông qua phân tích sâu dữ liệu mô phỏng, chúng tôi nhận thấy...
|
|
|
|
**Giai đoạn bùng phát ban đầu**
|
|
|
|
Weibo, với tư cách là bối cảnh đầu tiên của dư luận, đã đảm nhận chức năng cốt lõi là phát hành thông tin ban đầu:
|
|
|
|
> "Weibo đã đóng góp 68% mức độ thảo luận ban đầu..."
|
|
|
|
**Giai đoạn khuếch đại cảm xúc**
|
|
|
|
Nền tảng Douyin đã khuếch đại thêm tác động của sự kiện:
|
|
|
|
- Tác động thị giác mạnh mẽ
|
|
- Cộng hưởng cảm xúc cao
|
|
```
|
|
|
|
[Ví dụ Sai]
|
|
```
|
|
## Tóm tắt Điều hành ← Lỗi! Không thêm bất kỳ tiêu đề nào
|
|
### 1. Giai đoạn đầu ← Lỗi! Không sử dụng ### cho các mục con
|
|
#### 1.1 Phân tích chi tiết ← Lỗi! Không sử dụng #### để chia nhỏ hơn nữa
|
|
|
|
Chương này phân tích...
|
|
```
|
|
|
|
═══════════════════════════════════════════════════════════════
|
|
[Các công cụ truy xuất hiện có] (Gọi 3-5 lần mỗi phần)
|
|
═══════════════════════════════════════════════════════════════
|
|
|
|
{tools_description}
|
|
|
|
[Gợi ý Sử dụng Công cụ - Vui lòng phối hợp nhiều công cụ, không chỉ dùng một loại]
|
|
- insight_forge: Phân tích chuyên sâu, tự động phân tách câu hỏi và truy xuất sự thật cũng như các mối quan hệ từ nhiều chiều
|
|
- panorama_search: Tìm kiếm toàn cảnh góc rộng, hiểu bức tranh tổng thể, dòng thời gian và quá trình diễn biến của một sự kiện
|
|
- quick_search: Xác minh nhanh một điểm thông tin cụ thể
|
|
- interview_agents: Phỏng vấn các Tác nhân (Agents) mô phỏng để lấy góc nhìn thứ nhất và phản ứng thực tế từ các vai trò khác nhau
|
|
|
|
═══════════════════════════════════════════════════════════════
|
|
[Quy trình làm việc]
|
|
═══════════════════════════════════════════════════════════════
|
|
|
|
Đối với mỗi phản hồi, bạn chỉ có thể thực hiện một trong hai việc sau (không làm đồng thời):
|
|
|
|
Lựa chọn A - Gọi công cụ:
|
|
Đưa ra suy nghĩ (Thought) của bạn, sau đó sử dụng định dạng sau để gọi công cụ:
|
|
<tool_call>
|
|
{{"name": "Tên công cụ", "parameters": {{"Tên tham số": "Giá trị tham số"}}}}
|
|
</tool_call>
|
|
Hệ thống sẽ thực thi công cụ và trả về kết quả cho bạn. Bạn không cần và không được phép tự viết kết quả trả về của công cụ.
|
|
|
|
Lựa chọn B - Xuất nội dung cuối cùng:
|
|
Khi bạn đã thu thập đủ thông tin thông qua các công cụ, hãy xuất nội dung chương bắt đầu bằng "Final Answer:".
|
|
|
|
⚠️ Nghiêm cấm:
|
|
- Cấm bao gồm cả lệnh gọi công cụ và Final Answer trong cùng một phản hồi
|
|
- Cấm tự bịa đặt kết quả trả về của công cụ (Quan sát), tất cả kết quả công cụ đều do hệ thống đưa vào
|
|
- Chỉ gọi tối đa một công cụ cho mỗi phản hồi
|
|
|
|
═══════════════════════════════════════════════════════════════
|
|
[Yêu cầu Nội dung Chương]
|
|
═══════════════════════════════════════════════════════════════
|
|
|
|
1. Nội dung phải dựa trên dữ liệu mô phỏng do công cụ truy xuất.
|
|
2. Trích dẫn rộng rãi văn bản gốc để chứng minh hiệu quả mô phỏng.
|
|
3. Sử dụng định dạng Markdown (nhưng cấm sử dụng tiêu đề):
|
|
- Sử dụng **chữ đậm** để đánh dấu các điểm chính (thay vì dùng tiêu đề phụ).
|
|
- Sử dụng danh sách (- hoặc 1. 2. 3.) để tổ chức các ý.
|
|
- Sử dụng các dòng trống để phân tách các đoạn văn khác nhau.
|
|
- ❌ Cấm sử dụng #, ##, ###, #### và bất kỳ cú pháp tiêu đề nào khác.
|
|
4. [Quy cách Định dạng Trích dẫn - Phải là một đoạn riêng biệt]
|
|
Trích dẫn phải là một đoạn văn độc lập, có dòng trống ở trước và sau, không được viết lẫn vào trong đoạn văn:
|
|
|
|
✅ Định dạng đúng:
|
|
```
|
|
Phản ứng của nhà trường bị coi là thiếu nội dung thực chất.
|
|
|
|
> "Mô hình phản ứng của nhà trường có vẻ cứng nhắc và chậm chạp trong môi trường mạng xã hội thay đổi nhanh chóng."
|
|
|
|
Đánh giá này phản ánh sự không hài lòng chung của công chúng.
|
|
```
|
|
|
|
❌ Định dạng sai:
|
|
```
|
|
Phản ứng của nhà trường bị coi là thiếu nội dung thực chất. > "Mô hình phản ứng của nhà trường..." Đánh giá này phản ánh...
|
|
```
|
|
5. Duy trì tính logic nhất quán với các chương khác.
|
|
6. [Tránh Lặp lại] Đọc kỹ nội dung các chương đã hoàn thành bên dưới, không lặp lại cùng một thông tin.
|
|
7. [Nhấn mạnh lại lần nữa] Không thêm bất kỳ tiêu đề nào! Sử dụng **chữ đậm** thay cho tiêu đề mục.
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# SECTION_USER_PROMPT_TEMPLATE = """\
|
|
# Completed Chapter Content (Please read carefully to avoid duplication):
|
|
# {previous_content}
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# [Current Task] Writing Chapter: {section_title}
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# [Important Reminders]
|
|
# 1. Read the completed chapters above carefully to avoid repeating the same content!
|
|
# 2. Must call tools first to get simulation data before starting
|
|
# 3. Please mix different tools, do not use only one
|
|
# 4. Report content must come from retrieval results, do not use your own knowledge
|
|
|
|
# [⚠️ Formatting Warning - Must be Obeyed]
|
|
# - ❌ Do not write any headings (no #, ##, ###, ####)
|
|
# - ❌ Do not write "{section_title}" as the beginning
|
|
# - ✅ Chapter titles are automatically added by the system
|
|
# - ✅ Write the main text directly, use **bold** instead of section headings
|
|
|
|
# Please begin:
|
|
# 1. First, think (Thought) what information this chapter needs
|
|
# 2. Then, call tools (Action) to get simulation data
|
|
# 3. After collecting enough information, output Final Answer (plain text, no headings)
|
|
# """
|
|
|
|
SECTION_USER_PROMPT_TEMPLATE = """\
|
|
Nội dung Chương đã Hoàn thành (Vui lòng đọc kỹ để tránh trùng lặp):
|
|
{previous_content}
|
|
|
|
═══════════════════════════════════════════════════════════════
|
|
[Nhiệm vụ Hiện tại] Viết Chương: {section_title}
|
|
═══════════════════════════════════════════════════════════════
|
|
|
|
[Nhắc nhở Quan trọng]
|
|
1. Đọc kỹ các chương đã hoàn thành ở trên để tránh lặp lại nội dung!
|
|
2. Phải gọi công cụ trước để lấy dữ liệu mô phỏng trước khi bắt đầu viết.
|
|
3. Vui lòng sử dụng kết hợp nhiều công cụ khác nhau, không chỉ dùng một loại.
|
|
4. Nội dung báo cáo phải đến từ kết quả truy xuất, không sử dụng kiến thức cá nhân của bạn.
|
|
|
|
[⚠️ Cảnh báo Định dạng - Phải Tuân thủ Tuyệt đối]
|
|
- ❌ Không viết bất kỳ tiêu đề nào (không dùng các ký tự #, ##, ###, ####).
|
|
- ❌ Không viết "{section_title}" ở phần bắt đầu nội dung.
|
|
- ✅ Tiêu đề chương sẽ được hệ thống tự động thêm vào sau đó.
|
|
- ✅ Viết trực tiếp vào nội dung chính, sử dụng văn bản **in đậm** thay cho tiêu đề các mục.
|
|
|
|
Vui lòng bắt đầu:
|
|
1. Đầu tiên, hãy suy nghĩ (Thought) xem chương này cần những thông tin gì.
|
|
2. Sau đó, gọi công cụ (Action) để lấy dữ liệu mô phỏng.
|
|
3. Sau khi thu thập đủ thông tin, xuất Câu trả lời cuối cùng (Final Answer) dưới dạng văn bản thuần túy, không chứa tiêu đề.
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# REACT_OBSERVATION_TEMPLATE = """\
|
|
# Observation (Retrieval Result):
|
|
|
|
# ═══ Tool {tool_name} Returned ═══
|
|
# {result}
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Tool called {tool_calls_count}/{max_tool_calls} times (Used: {used_tools_str}) {unused_hint}
|
|
# - If information is sufficient: Output section content starting with "Final Answer:" (Must quote the above original text)
|
|
# - If more information is needed: Call a tool to continue retrieving
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# """
|
|
|
|
REACT_OBSERVATION_TEMPLATE = """\
|
|
Quan sát (Kết quả Truy xuất):
|
|
|
|
═══ Công cụ {tool_name} đã trả về ═══
|
|
{result}
|
|
|
|
═══════════════════════════════════════════════════════════════
|
|
Công cụ đã được gọi {tool_calls_count}/{max_tool_calls} lần (Đã dùng: {used_tools_str}) {unused_hint}
|
|
- Nếu thông tin đã đủ: Xuất nội dung phần báo cáo bắt đầu bằng "Final Answer:" (Bắt buộc trích dẫn văn bản gốc ở trên)
|
|
- Nếu cần thêm thông tin: Tiếp tục gọi công cụ để truy xuất
|
|
═══════════════════════════════════════════════════════════════
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# REACT_INSUFFICIENT_TOOLS_MSG = (
|
|
# "[Notice] You only called the tool {tool_calls_count} times, at least {min_tool_calls} times are needed. "
|
|
# "Please call the tool again to fetch more simulation data, and then output Final Answer. {unused_hint}"
|
|
# )
|
|
|
|
REACT_INSUFFICIENT_TOOLS_MSG = (
|
|
"[Thông báo] Bạn mới chỉ gọi công cụ {tool_calls_count} lần, trong khi yêu cầu tối thiểu là {min_tool_calls} lần. "
|
|
"Vui lòng gọi lại công cụ để lấy thêm dữ liệu mô phỏng, sau đó mới xuất Câu trả lời cuối cùng (Final Answer). {unused_hint}"
|
|
)
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# REACT_INSUFFICIENT_TOOLS_MSG_ALT = (
|
|
# "Currently tool called {tool_calls_count} times, at least {min_tool_calls} times are needed. "
|
|
# "Please call tools to fetch simulation data. {unused_hint}"
|
|
# )
|
|
|
|
REACT_INSUFFICIENT_TOOLS_MSG_ALT = (
|
|
"Hiện tại công cụ mới được gọi {tool_calls_count} lần, yêu cầu ít nhất {min_tool_calls} lần. "
|
|
"Vui lòng gọi các công cụ để truy xuất dữ liệu mô phỏng. {unused_hint}"
|
|
)
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# REACT_TOOL_LIMIT_MSG = (
|
|
# "Tool call limit reached ({tool_calls_count}/{max_tool_calls}), cannot call tools anymore. "
|
|
# 'Please output your section content starting with "Final Answer:" immediately based on retrieved information.'
|
|
# )
|
|
|
|
REACT_TOOL_LIMIT_MSG = (
|
|
"Đã đạt giới hạn gọi công cụ ({tool_calls_count}/{max_tool_calls}), không thể gọi thêm công cụ nữa. "
|
|
'Vui lòng xuất nội dung phần báo cáo bắt đầu bằng "Final Answer:" ngay lập tức dựa trên những thông tin đã truy xuất được.'
|
|
)
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# REACT_UNUSED_TOOLS_HINT = "\n💡 You haven't used: {unused_list}, suggesting trying different tools for multiple perspectives"
|
|
|
|
REACT_UNUSED_TOOLS_HINT = "\n💡 Bạn chưa sử dụng: {unused_list}, hãy thử các công cụ khác nhau để có cái nhìn đa chiều hơn"
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# REACT_FORCE_FINAL_MSG = "Tool call limit reached, please output Final Answer: and generate section content directly."
|
|
|
|
REACT_FORCE_FINAL_MSG = "Đã đạt giới hạn gọi công cụ, vui lòng xuất Final Answer: và trực tiếp tạo nội dung cho phần này."
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# CHAT_SYSTEM_PROMPT_TEMPLATE = """\
|
|
# You are a concise and efficient simulation prediction assistant.
|
|
|
|
# [Background]
|
|
# Prediction condition: {simulation_requirement}
|
|
|
|
# [Generated Analysis Report]
|
|
# {report_content}
|
|
|
|
# [Rules]
|
|
# 1. Prioritize answering based on the report content above
|
|
# 2. Answer the question directly, avoid lengthy reasoning
|
|
# 3. Only call tools to retrieve more data if the report content is insufficient to answer
|
|
# 4. Answers must be concise, clear, and organized
|
|
|
|
# [Available Tools] (Use only when necessary, call 1-2 times max)
|
|
# {tools_description}
|
|
|
|
# [Tool Call Format]
|
|
# <tool_call>
|
|
# {{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}}
|
|
# </tool_call>
|
|
|
|
# [Answering Style]
|
|
# - Concise and direct, avoid long paragraphs
|
|
# - Use > format to quote key content
|
|
# - Provide conclusion first, then explain the reason
|
|
# """
|
|
|
|
CHAT_SYSTEM_PROMPT_TEMPLATE = """
|
|
Bạn là một trợ lý dự đoán mô phỏng súc tích và hiệu quả.
|
|
|
|
[Bối cảnh]
|
|
Điều kiện dự đoán: {simulation_requirement}
|
|
|
|
[Báo cáo Phân tích Đã tạo]
|
|
{report_content}
|
|
|
|
[Quy tắc]
|
|
1. Ưu tiên trả lời dựa trên nội dung báo cáo ở trên.
|
|
2. Trả lời câu hỏi trực tiếp, tránh lập luận dài dòng.
|
|
3. Chỉ gọi công cụ để truy xuất thêm dữ liệu nếu nội dung báo cáo không đủ để trả lời.
|
|
4. Câu trả lời phải súc tích, rõ ràng và có tổ chức.
|
|
|
|
[Các Công cụ Hiện có] (Chỉ sử dụng khi cần thiết, gọi tối đa 1-2 lần)
|
|
{tools_description}
|
|
|
|
[Định dạng Gọi Công cụ]
|
|
<tool_call>
|
|
{{"name": "Tên Công cụ", "parameters": {{"Tên Tham số": "Giá trị Tham số"}}}}
|
|
</tool_call>
|
|
|
|
[Phong cách Trả lời]
|
|
- Ngắn gọn và trực tiếp, tránh các đoạn văn dài.
|
|
- Sử dụng định dạng > để trích dẫn nội dung chính.
|
|
- Đưa ra kết luận trước, sau đó mới giải thích lý do.
|
|
"""
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# CHAT_OBSERVATION_SUFFIX = "\n\nPlease answer the question concisely."
|
|
|
|
CHAT_OBSERVATION_SUFFIX = "\n\nVui lòng trả lời câu hỏi một cách súc tích."
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Class chính: ReportAgent
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
class ReportAgent:
|
|
"""
|
|
Report Agent - Tác nhân tạo báo cáo mô phỏng
|
|
|
|
Sử dụng chế độ ReACT (Reasoning + Acting):
|
|
1. Giai đoạn lập kế hoạch (Planning): Phân tích yêu cầu mô phỏng, hoạch định cấu trúc thư mục báo cáo
|
|
2. Giai đoạn tạo văn bản (Generating): Tạo nội dung theo từng cấu trúc, mỗi cấu trúc có thể gọi công cụ nhiều lần để lấy thông tin
|
|
3. Giai đoạn xem xét (Reflecting): Kiểm tra tính toàn vẹn và độ chính xác của nội dung
|
|
"""
|
|
|
|
# Số lần gọi công cụ tối đa (mỗi chương)
|
|
MAX_TOOL_CALLS_PER_SECTION = 5
|
|
|
|
# Số vòng xem xét tối đa
|
|
MAX_REFLECTION_ROUNDS = 3
|
|
|
|
# Số lần gọi công cụ tối đa trong quá trình trò chuyện
|
|
MAX_TOOL_CALLS_PER_CHAT = 2
|
|
|
|
def __init__(
|
|
self,
|
|
graph_id: str,
|
|
simulation_id: str,
|
|
project_id: str,
|
|
simulation_requirement: str,
|
|
llm_client: Optional[LLMClient] = None,
|
|
zep_tools: Optional[ZepToolsService] = None
|
|
):
|
|
"""
|
|
Khởi tạo Report Agent
|
|
|
|
Args:
|
|
graph_id: ID Đồ thị
|
|
simulation_id: ID mô phỏng
|
|
simulation_requirement: Mô tả yêu cầu mô phỏng
|
|
llm_client: Client của LLM (không bắt buộc)
|
|
zep_tools: Dịch vụ công cụ Zep (không bắt buộc)
|
|
"""
|
|
self.graph_id = graph_id
|
|
self.simulation_id = simulation_id
|
|
self.project_id = project_id
|
|
self.simulation_requirement = simulation_requirement
|
|
|
|
self.llm = llm_client or LLMClient(
|
|
component="report_agent",
|
|
metadata={
|
|
"simulation_id": simulation_id,
|
|
"project_id": project_id,
|
|
"phase": "report_generation",
|
|
},
|
|
)
|
|
self.zep_tools = zep_tools or ZepToolsService(
|
|
llm_client=LLMClient(
|
|
component="zep_tools",
|
|
metadata={
|
|
"simulation_id": simulation_id,
|
|
"project_id": project_id,
|
|
"phase": "zep_tools",
|
|
},
|
|
)
|
|
)
|
|
|
|
# Định nghĩa các công cụ
|
|
self.tools = self._define_tools()
|
|
|
|
# Trình ghi log báo cáo (được khởi tạo trong generate_report)
|
|
self.report_logger: Optional[ReportLogger] = None
|
|
# Trình ghi log console (được khởi tạo trong generate_report)
|
|
self.console_logger: Optional[ReportConsoleLogger] = None
|
|
|
|
logger.info(
|
|
f"ReportAgent initialized: graph_id={graph_id}, simulation_id={simulation_id}, project_id={project_id}"
|
|
)
|
|
|
|
def _define_tools(self) -> Dict[str, Dict[str, Any]]:
|
|
"""Định nghĩa các công cụ khả dụng"""
|
|
return {
|
|
"insight_forge": {
|
|
"name": "insight_forge",
|
|
"description": TOOL_DESC_INSIGHT_FORGE,
|
|
"parameters": {
|
|
"query": "The question or topic you want to deeply analyze",
|
|
"report_context": "The context of the current report section (optional, helps generate more precise sub-questions)"
|
|
}
|
|
},
|
|
"panorama_search": {
|
|
"name": "panorama_search",
|
|
"description": TOOL_DESC_PANORAMA_SEARCH,
|
|
"parameters": {
|
|
"query": "Search query used for relevance ranking",
|
|
"include_expired": "Whether to include expired/historical content (default True)"
|
|
}
|
|
},
|
|
"quick_search": {
|
|
"name": "quick_search",
|
|
"description": TOOL_DESC_QUICK_SEARCH,
|
|
"parameters": {
|
|
"query": "Search query string",
|
|
"limit": "Number of returned results (optional, default 10)"
|
|
}
|
|
},
|
|
"interview_agents": {
|
|
"name": "interview_agents",
|
|
"description": TOOL_DESC_INTERVIEW_AGENTS,
|
|
"parameters": {
|
|
"interview_topic": "Interview topic or requirement description (e.g.: 'Understand students opinions on dorm formaldehyde issue')",
|
|
"max_agents": "Maximum number of agents to interview (optional, default 5, maximum 10)"
|
|
}
|
|
}
|
|
}
|
|
|
|
def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], report_context: str = "") -> str:
|
|
"""
|
|
Thực thi lệnh gọi công cụ
|
|
|
|
Args:
|
|
tool_name: Tên công cụ
|
|
parameters: Các tham số cho công cụ
|
|
report_context: Ngữ cảnh của đoạn báo cáo (dành cho InsightForge)
|
|
|
|
Returns:
|
|
Kết quả của công cụ trả về (định dạng text)
|
|
"""
|
|
logger.info(f"Executing tool: {tool_name}, Parameters: {parameters}")
|
|
|
|
try:
|
|
if tool_name == "insight_forge":
|
|
query = parameters.get("query", "")
|
|
ctx = parameters.get("report_context", "") or report_context
|
|
result = self.zep_tools.insight_forge(
|
|
graph_id=self.graph_id,
|
|
query=query,
|
|
simulation_requirement=self.simulation_requirement,
|
|
report_context=ctx
|
|
)
|
|
return result.to_text()
|
|
|
|
elif tool_name == "panorama_search":
|
|
# Tìm kiếm diện rộng - Lấy toàn cảnh kết quả
|
|
query = parameters.get("query", "")
|
|
include_expired = parameters.get("include_expired", True)
|
|
if isinstance(include_expired, str):
|
|
include_expired = include_expired.lower() in ['true', '1', 'yes']
|
|
result = self.zep_tools.panorama_search(
|
|
graph_id=self.graph_id,
|
|
query=query,
|
|
include_expired=include_expired
|
|
)
|
|
return result.to_text()
|
|
|
|
elif tool_name == "quick_search":
|
|
# Tìm kiếm đơn giản - Lấy dữ liệu nhanh
|
|
query = parameters.get("query", "")
|
|
limit = parameters.get("limit", 10)
|
|
if isinstance(limit, str):
|
|
limit = int(limit)
|
|
result = self.zep_tools.quick_search(
|
|
graph_id=self.graph_id,
|
|
query=query,
|
|
limit=limit
|
|
)
|
|
return result.to_text()
|
|
|
|
elif tool_name == "interview_agents":
|
|
# Phỏng vấn chuyên sâu - Gọi API phỏng vấn OASIS thật để lấy ý kiến của các Agent đang chạy (đa nền tảng)
|
|
interview_topic = parameters.get("interview_topic", parameters.get("query", ""))
|
|
max_agents = parameters.get("max_agents", 5)
|
|
if isinstance(max_agents, str):
|
|
max_agents = int(max_agents)
|
|
max_agents = min(max_agents, 10)
|
|
result = self.zep_tools.interview_agents(
|
|
simulation_id=self.simulation_id,
|
|
interview_requirement=interview_topic,
|
|
simulation_requirement=self.simulation_requirement,
|
|
max_agents=max_agents
|
|
)
|
|
return result.to_text()
|
|
|
|
# ========== Công cụ cũ giữ lại để tương thích (chuyển hướng sang công cụ mới) ==========
|
|
|
|
elif tool_name == "search_graph":
|
|
# Lái qua quick_search
|
|
logger.info("search_graph redirected to quick_search")
|
|
return self._execute_tool("quick_search", parameters, report_context)
|
|
|
|
elif tool_name == "get_graph_statistics":
|
|
result = self.zep_tools.get_graph_statistics(self.graph_id)
|
|
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
|
|
elif tool_name == "get_entity_summary":
|
|
entity_name = parameters.get("entity_name", "")
|
|
result = self.zep_tools.get_entity_summary(
|
|
graph_id=self.graph_id,
|
|
entity_name=entity_name
|
|
)
|
|
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
|
|
elif tool_name == "get_simulation_context":
|
|
# Lái qua insight_forge do nó xịn hơn
|
|
logger.info("get_simulation_context redirected to insight_forge")
|
|
query = parameters.get("query", self.simulation_requirement)
|
|
return self._execute_tool("insight_forge", {"query": query}, report_context)
|
|
|
|
elif tool_name == "get_entities_by_type":
|
|
entity_type = parameters.get("entity_type", "")
|
|
nodes = self.zep_tools.get_entities_by_type(
|
|
graph_id=self.graph_id,
|
|
entity_type=entity_type
|
|
)
|
|
result = [n.to_dict() for n in nodes]
|
|
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
|
|
else:
|
|
return f"Unknown tool: {tool_name}. Please use one of: insight_forge, panorama_search, quick_search"
|
|
|
|
except Exception as e:
|
|
logger.error(f"Tool execution failed: {tool_name}, Error: {str(e)}")
|
|
return f"Tool execution failed: {str(e)}"
|
|
|
|
# Tập hợp các tool khả dụng, để kiểm tra tính hợp lệ khi quét JSON
|
|
VALID_TOOL_NAMES = {"insight_forge", "panorama_search", "quick_search", "interview_agents"}
|
|
|
|
def _parse_tool_calls(self, response: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Phân tích kết quả trả về từ LLM để lấy thông tin gọi công cụ
|
|
|
|
Định dạng hỗ trợ (theo mức độ ưu tiên):
|
|
1. <tool_call>{"name": "tool_name", "parameters": {...}}</tool_call>
|
|
2. JSON trần (Toàn bộ chuỗi response hoặc từng dòng là một JSON trực tiếp cho công cụ)
|
|
"""
|
|
tool_calls = []
|
|
|
|
# Định dạng 1: Phong cách XML (Định dạng tiêu chuẩn)
|
|
xml_pattern = r'<tool_call>\s*(\{.*?\})\s*</tool_call>'
|
|
for match in re.finditer(xml_pattern, response, re.DOTALL):
|
|
try:
|
|
call_data = json.loads(match.group(1))
|
|
tool_calls.append(call_data)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
if tool_calls:
|
|
return tool_calls
|
|
|
|
# Định dạng 2: Dự phòng phòng hờ LLM trả về chuỗi JSON trần không có tag <tool_call>
|
|
# Chỉ chạy logic này khi không thấy định dạng 1, tránh dính JSON vô ý trong văn bản chính
|
|
stripped = response.strip()
|
|
if stripped.startswith('{') and stripped.endswith('}'):
|
|
try:
|
|
call_data = json.loads(stripped)
|
|
if self._is_valid_tool_call(call_data):
|
|
tool_calls.append(call_data)
|
|
return tool_calls
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
# Reply có thể chứa cả nội dung suy nghĩ (Thought) + JSON trần, thử móc ra object JSON cuối cùng
|
|
json_pattern = r'(\{"(?:name|tool)"\s*:.*?\})\s*$'
|
|
match = re.search(json_pattern, stripped, re.DOTALL)
|
|
if match:
|
|
try:
|
|
call_data = json.loads(match.group(1))
|
|
if self._is_valid_tool_call(call_data):
|
|
tool_calls.append(call_data)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
return tool_calls
|
|
|
|
def _is_valid_tool_call(self, data: dict) -> bool:
|
|
"""Kiểm tra JSON giải mã được có phải là lời gọi công cụ hợp lệ hay không"""
|
|
# Hỗ trợ cả 2 định dạng {"name": ..., "parameters": ...} và {"tool": ..., "params": ...}
|
|
tool_name = data.get("name") or data.get("tool")
|
|
if tool_name and tool_name in self.VALID_TOOL_NAMES:
|
|
# Đồng nhất key về chuẩn name / parameters
|
|
if "tool" in data:
|
|
data["name"] = data.pop("tool")
|
|
if "params" in data and "parameters" not in data:
|
|
data["parameters"] = data.pop("params")
|
|
return True
|
|
return False
|
|
|
|
def _get_tools_description(self) -> str:
|
|
"""Tạo đoạn văn mô tả công cụ"""
|
|
desc_parts = ["Available tools:"]
|
|
for name, tool in self.tools.items():
|
|
params_desc = ", ".join([f"{k}: {v}" for k, v in tool["parameters"].items()])
|
|
desc_parts.append(f"- {name}: {tool['description']}")
|
|
if params_desc:
|
|
desc_parts.append(f" Parameters: {params_desc}")
|
|
return "\n".join(desc_parts)
|
|
|
|
def plan_outline(
|
|
self,
|
|
progress_callback: Optional[Callable] = None
|
|
) -> ReportOutline:
|
|
"""
|
|
Hoạch định dàn ý báo cáo
|
|
|
|
Sử dụng LLM để phân tích yêu cầu mô phỏng, hoạch định cấu trúc thư mục của báo cáo
|
|
|
|
Args:
|
|
progress_callback: Hàm callback tiến trình
|
|
|
|
Returns:
|
|
ReportOutline: Dàn ý báo cáo
|
|
"""
|
|
logger.info("Start planning report outline...")
|
|
|
|
if progress_callback:
|
|
progress_callback("planning", 0, "Analyzing simulation requirements...")
|
|
|
|
# Đầu tiên cần lấy ngữ cảnh mô phỏng
|
|
context = self.zep_tools.get_simulation_context(
|
|
graph_id=self.graph_id,
|
|
simulation_requirement=self.simulation_requirement
|
|
)
|
|
|
|
if progress_callback:
|
|
progress_callback("planning", 30, "Generating report outline...")
|
|
|
|
system_prompt = PLAN_SYSTEM_PROMPT
|
|
user_prompt = PLAN_USER_PROMPT_TEMPLATE.format(
|
|
simulation_requirement=self.simulation_requirement,
|
|
total_nodes=context.get('graph_statistics', {}).get('total_nodes', 0),
|
|
total_edges=context.get('graph_statistics', {}).get('total_edges', 0),
|
|
entity_types=list(context.get('graph_statistics', {}).get('entity_types', {}).keys()),
|
|
total_entities=context.get('total_entities', 0),
|
|
related_facts_json=json.dumps(context.get('related_facts', [])[:10], ensure_ascii=False, indent=2),
|
|
)
|
|
|
|
try:
|
|
response = self.llm.chat_json(
|
|
messages=[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_prompt}
|
|
],
|
|
temperature=0.3
|
|
)
|
|
|
|
if progress_callback:
|
|
progress_callback("planning", 80, "Parsing outline structure...")
|
|
|
|
# Phân tích cú pháp lấy dàn ý
|
|
sections = []
|
|
for section_data in response.get("sections", []):
|
|
sections.append(ReportSection(
|
|
title=section_data.get("title", ""),
|
|
content=""
|
|
))
|
|
|
|
outline = ReportOutline(
|
|
title=response.get("title", "Simulation Analysis Report"),
|
|
summary=response.get("summary", ""),
|
|
sections=sections
|
|
)
|
|
|
|
if progress_callback:
|
|
progress_callback("planning", 100, "Outline planning completed")
|
|
|
|
logger.info(f"Outline planning completed: {len(sections)} chapters")
|
|
return outline
|
|
|
|
except Exception as e:
|
|
logger.error(f"Outline planning failed: {str(e)}")
|
|
# Trả về dàn ý mặc định (3 chương, đóng vai trò bản dự phòng)
|
|
return ReportOutline(
|
|
title="Future Prediction Report",
|
|
summary="Future trends and risk analysis based on simulation prediction",
|
|
sections=[
|
|
ReportSection(title="Prediction Scenarios and Core Findings"),
|
|
ReportSection(title="Population Behavior Prediction Analysis"),
|
|
ReportSection(title="Trend Outlook and Risk Warning")
|
|
]
|
|
)
|
|
|
|
def _generate_section_react(
|
|
self,
|
|
section: ReportSection,
|
|
outline: ReportOutline,
|
|
previous_sections: List[str],
|
|
progress_callback: Optional[Callable] = None,
|
|
section_index: int = 0
|
|
) -> str:
|
|
"""
|
|
Sử dụng chế độ ReACT để tạo nội dung cho từng chương
|
|
|
|
Vòng lặp ReACT:
|
|
1. Thought (Suy nghĩ) - Phân tích xem cần thông tin gì
|
|
2. Action (Hành động) - Gọi công cụ để lấy thông tin
|
|
3. Observation (Quan sát) - Phân tích kết quả công cụ trả về
|
|
4. Lặp lại quá trình tới khi đủ thông tin hoặc chạy hết số lượt cho phép
|
|
5. Final Answer (Câu trả lời cuối cùng) - Sinh nội dung chương
|
|
|
|
Args:
|
|
section: Chương cần sinh
|
|
outline: Dàn ý đầy đủ
|
|
previous_sections: Nội dung các chương trước (dành cho việc duy trì tính gắn kết logic)
|
|
progress_callback: Callback theo dõi tiến trình
|
|
section_index: Chỉ mục của chương hiện tại (dùng để log)
|
|
|
|
Returns:
|
|
Nội dung chương (Định dạng Markdown)
|
|
"""
|
|
logger.info(f"ReACT generating chapter: {section.title}")
|
|
|
|
# Ghi nhận log khởi tạo chương
|
|
if self.report_logger:
|
|
self.report_logger.log_section_start(section.title, section_index)
|
|
|
|
system_prompt = SECTION_SYSTEM_PROMPT_TEMPLATE.format(
|
|
report_title=outline.title,
|
|
report_summary=outline.summary,
|
|
simulation_requirement=self.simulation_requirement,
|
|
section_title=section.title,
|
|
tools_description=self._get_tools_description(),
|
|
)
|
|
|
|
# Xây dựng prompt cho user - mỗi chương trước đó sẽ truyền vào tối đa 4000 ký tự
|
|
if previous_sections:
|
|
previous_parts = []
|
|
for sec in previous_sections:
|
|
# Mỗi chương tối đa 4000 ký tự
|
|
truncated = sec[:4000] + "..." if len(sec) > 4000 else sec
|
|
previous_parts.append(truncated)
|
|
previous_content = "\n\n---\n\n".join(previous_parts)
|
|
else:
|
|
previous_content = "(This is the first chapter)"
|
|
|
|
user_prompt = SECTION_USER_PROMPT_TEMPLATE.format(
|
|
previous_content=previous_content,
|
|
section_title=section.title,
|
|
)
|
|
|
|
messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_prompt}
|
|
]
|
|
|
|
# Vòng lặp ReACT
|
|
tool_calls_count = 0
|
|
max_iterations = 5 # Số vòng lặp tối đa
|
|
min_tool_calls = 3 # Số lần gọi công cụ tối thiểu
|
|
conflict_retries = 0 # Số lần gọi công cụ và trả về Final Answer bị xung đột liên tiếp
|
|
used_tools = set() # Lưu lại tên các công cụ đã được gọi
|
|
all_tools = {"insight_forge", "panorama_search", "quick_search", "interview_agents"}
|
|
|
|
# Ngữ cảnh báo cáo, dùng để tự sinh câu hỏi thứ cấp trong InsightForge
|
|
report_context = f"Chapter Title: {section.title}\nSimulation Requirement: {self.simulation_requirement}"
|
|
|
|
for iteration in range(max_iterations):
|
|
if progress_callback:
|
|
progress_callback(
|
|
"generating",
|
|
int((iteration / max_iterations) * 100),
|
|
f"Deep retrieval and writing in progress ({tool_calls_count}/{self.MAX_TOOL_CALLS_PER_SECTION})"
|
|
)
|
|
|
|
# Gọi LLM
|
|
response = self.llm.chat(
|
|
messages=messages,
|
|
temperature=0.5,
|
|
# max_tokens=4096
|
|
)
|
|
|
|
# Kiểm tra xem phản hồi có rỗng/chưa có (None) không (do API lỗi hoặc content null)
|
|
if response is None:
|
|
logger.warning(f"Chapter {section.title} iteration {iteration + 1}: LLM returned None")
|
|
# Nếu còn lượt thử, thêm message tiếp
|
|
if iteration < max_iterations - 1:
|
|
messages.append({"role": "assistant", "content": "(Empty response)"})
|
|
messages.append({"role": "user", "content": "Please continue generating content."})
|
|
continue
|
|
# Còn nếu nó về None lần cuối thì nhảy thoát vòng lặp kết thúc
|
|
break
|
|
|
|
logger.debug(f"LLM response: {response[:200]}...")
|
|
|
|
# Parse dữ liệu một lần để tiết kiệm
|
|
tool_calls = self._parse_tool_calls(response)
|
|
has_tool_calls = bool(tool_calls)
|
|
has_final_answer = "Final Answer:" in response
|
|
|
|
# ── Giải quyết xung đột: LLM cùng lúc nhả cả tool gọi lẫn output Final Answer ──
|
|
if has_tool_calls and has_final_answer:
|
|
conflict_retries += 1
|
|
logger.warning(
|
|
f"Chapter {section.title} iteration {iteration+1}: "
|
|
f"LLM output tool call and Final Answer simultaneously (Conflict #{conflict_retries})"
|
|
)
|
|
|
|
if conflict_retries <= 2:
|
|
# Trong 2 lần đầu: Vứt phản hồi này, bắt LLM tạo lại
|
|
messages.append({"role": "assistant", "content": response})
|
|
messages.append({
|
|
"role": "user",
|
|
"content": (
|
|
"[Format Error] You included both tool call and Final Answer in a single reply, which is not allowed.\n"
|
|
"Each reply can only do one of two things:\n"
|
|
"- Call a tool (output a <tool_call> block, do not write Final Answer)\n"
|
|
"- Output final content (starting with 'Final Answer:', do not include <tool_call>)\n"
|
|
"Please reply again, doing only one of these things."
|
|
),
|
|
})
|
|
continue
|
|
else:
|
|
# Lần 3: Hạ cấp xử lý, ngắt cứng đến lệnh gọi công cụ đầu tiên và tiếp tục bám theo
|
|
logger.warning(
|
|
f"Chapter {section.title}: {conflict_retries} consecutive conflicts, "
|
|
"degrading to truncated execution of the first tool call"
|
|
)
|
|
first_tool_end = response.find('</tool_call>')
|
|
if first_tool_end != -1:
|
|
response = response[:first_tool_end + len('</tool_call>')]
|
|
tool_calls = self._parse_tool_calls(response)
|
|
has_tool_calls = bool(tool_calls)
|
|
has_final_answer = False
|
|
conflict_retries = 0
|
|
|
|
# Ghi lại log phản hồi của 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
|
|
)
|
|
|
|
# ── Trường hợp 1: LLM đã xuất ra Final Answer ──
|
|
if has_final_answer:
|
|
# Nếu số lần gọi công cụ chưa đủ, từ chối và yêu cầu tiếp tục gọi
|
|
if tool_calls_count < min_tool_calls:
|
|
messages.append({"role": "assistant", "content": response})
|
|
unused_tools = all_tools - used_tools
|
|
unused_hint = f"(These tools are unused, suggest using them: {', '.join(unused_tools)})" if unused_tools else ""
|
|
messages.append({
|
|
"role": "user",
|
|
"content": REACT_INSUFFICIENT_TOOLS_MSG.format(
|
|
tool_calls_count=tool_calls_count,
|
|
min_tool_calls=min_tool_calls,
|
|
unused_hint=unused_hint,
|
|
),
|
|
})
|
|
continue
|
|
|
|
# Kết thúc bình thường
|
|
final_answer = response.split("Final Answer:")[-1].strip()
|
|
logger.info(f"Chapter {section.title} generation completed (Tool calls: {tool_calls_count})")
|
|
|
|
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
|
|
)
|
|
return final_answer
|
|
|
|
# ── Trường hợp 2: LLM cố gắng gọi công cụ ──
|
|
if has_tool_calls:
|
|
# Đã hết hạn mức gọi công cụ → Thông báo rõ ràng, yêu cầu xuất Final Answer
|
|
if tool_calls_count >= self.MAX_TOOL_CALLS_PER_SECTION:
|
|
messages.append({"role": "assistant", "content": response})
|
|
messages.append({
|
|
"role": "user",
|
|
"content": REACT_TOOL_LIMIT_MSG.format(
|
|
tool_calls_count=tool_calls_count,
|
|
max_tool_calls=self.MAX_TOOL_CALLS_PER_SECTION,
|
|
),
|
|
})
|
|
continue
|
|
|
|
# Chỉ thực thi lệnh gọi đầu tiên
|
|
call = tool_calls[0]
|
|
if len(tool_calls) > 1:
|
|
logger.info(f"LLM attempted to call {len(tool_calls)} tools, only executing the first one: {call['name']}")
|
|
|
|
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(
|
|
call["name"],
|
|
call.get("parameters", {}),
|
|
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_calls_count += 1
|
|
used_tools.add(call['name'])
|
|
|
|
# Tạo gợi ý cho các công cụ chưa dùng
|
|
unused_tools = all_tools - used_tools
|
|
unused_hint = ""
|
|
if unused_tools and tool_calls_count < self.MAX_TOOL_CALLS_PER_SECTION:
|
|
unused_hint = REACT_UNUSED_TOOLS_HINT.format(unused_list="、".join(unused_tools))
|
|
|
|
messages.append({"role": "assistant", "content": response})
|
|
messages.append({
|
|
"role": "user",
|
|
"content": REACT_OBSERVATION_TEMPLATE.format(
|
|
tool_name=call["name"],
|
|
result=result,
|
|
tool_calls_count=tool_calls_count,
|
|
max_tool_calls=self.MAX_TOOL_CALLS_PER_SECTION,
|
|
used_tools_str=", ".join(used_tools),
|
|
unused_hint=unused_hint,
|
|
),
|
|
})
|
|
continue
|
|
|
|
# ── Trường hợp 3: Không có gọi công cụ, cũng không có Final Answer ──
|
|
messages.append({"role": "assistant", "content": response})
|
|
|
|
if tool_calls_count < min_tool_calls:
|
|
# Gọi công cụ chưa đủ, gợi ý các công cụ khác
|
|
unused_tools = all_tools - used_tools
|
|
unused_hint = f"(These tools are unused, suggest using them: {', '.join(unused_tools)})" if unused_tools else ""
|
|
|
|
messages.append({
|
|
"role": "user",
|
|
"content": REACT_INSUFFICIENT_TOOLS_MSG_ALT.format(
|
|
tool_calls_count=tool_calls_count,
|
|
min_tool_calls=min_tool_calls,
|
|
unused_hint=unused_hint,
|
|
),
|
|
})
|
|
continue
|
|
|
|
# Đã đủ số lần gọi công cụ, LLM sinh nội dung nhưng quên mất tiền tố "Final Answer:"
|
|
# Cứ lấy thẳng nội dung này làm kết quả luôn cho khỏi vòng vo
|
|
logger.info(f"Chapter {section.title} 'Final Answer:' prefix not detected, directly adopting LLM output as final content (Tool calls: {tool_calls_count})")
|
|
final_answer = response.strip()
|
|
|
|
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
|
|
)
|
|
return final_answer
|
|
|
|
# Hết số vòng lặp tối đa, bắt buộc bắt đầu tự sản xuất
|
|
logger.warning(f"Chapter {section.title} reached max iterations, forcing generation")
|
|
messages.append({"role": "user", "content": REACT_FORCE_FINAL_MSG})
|
|
|
|
response = self.llm.chat(
|
|
messages=messages,
|
|
temperature=0.5,
|
|
# max_tokens=4096
|
|
)
|
|
|
|
# Kiểm tra nếu ép buộc kết thúc mà LLM vẫn nhả None
|
|
if response is None:
|
|
logger.error(f"Chapter {section.title} LLM returned None during forced completion, using default error prompt")
|
|
final_answer = f"(This chapter generation failed: LLM returned empty response, please try again later)"
|
|
elif "Final Answer:" in response:
|
|
final_answer = response.split("Final Answer:")[-1].strip()
|
|
else:
|
|
final_answer = response
|
|
|
|
# Ghi log quá trình tạo nội dung chương hoàn tất
|
|
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
|
|
)
|
|
|
|
return final_answer
|
|
|
|
def generate_report(
|
|
self,
|
|
progress_callback: Optional[Callable[[str, int, str], None]] = None,
|
|
report_id: Optional[str] = None
|
|
) -> Report:
|
|
"""
|
|
Tạo báo cáo hoàn chỉnh (Xuất theo thời gian thực từng chương)
|
|
|
|
Mỗi chương sau khi hoàn thành sẽ được lưu ngay vào mục, không cần đợi cả báo cáo xong.
|
|
Cấu trúc thư mục:
|
|
reports/{report_id}/
|
|
meta.json - Thông tin meta
|
|
outline.json - Dàn ý
|
|
progress.json - Tiến độ
|
|
section_01.md - Chương 1
|
|
section_02.md - Chương 2
|
|
...
|
|
full_report.md - Báo cáo tổng
|
|
|
|
Args:
|
|
progress_callback: Callback tiến độ (stage, progress, message)
|
|
report_id: ID báo cáo (Có thể rỗng để tự phát sinh)
|
|
|
|
Returns:
|
|
Report: Báo cáo đầy đủ
|
|
"""
|
|
import uuid
|
|
|
|
# Tạo report_id mới tự động nếu chưa có truyền vào
|
|
if not report_id:
|
|
report_id = f"report_{uuid.uuid4().hex[:12]}"
|
|
start_time = datetime.now()
|
|
|
|
# Đồng bộ metadata chi phí LLM theo report hiện tại
|
|
# Mục tiêu: mọi call trong giai đoạn generate report đều mang report_id.
|
|
self.llm.default_metadata["report_id"] = report_id
|
|
self.llm.default_metadata["project_id"] = self.project_id
|
|
self.llm.default_metadata["simulation_id"] = self.simulation_id
|
|
self.llm.default_metadata["phase"] = "report_generation"
|
|
|
|
zep_llm = self.zep_tools.llm
|
|
zep_llm.default_metadata["report_id"] = report_id
|
|
zep_llm.default_metadata["project_id"] = self.project_id
|
|
zep_llm.default_metadata["simulation_id"] = self.simulation_id
|
|
zep_llm.default_metadata["phase"] = "zep_tools"
|
|
|
|
report = Report(
|
|
report_id=report_id,
|
|
simulation_id=self.simulation_id,
|
|
graph_id=self.graph_id,
|
|
simulation_requirement=self.simulation_requirement,
|
|
status=ReportStatus.PENDING,
|
|
created_at=datetime.now().isoformat()
|
|
)
|
|
|
|
# Cập nhật mảng những tiêu đề chương xong (Để tính tiến độ)
|
|
completed_section_titles = []
|
|
|
|
try:
|
|
# Khởi tạo: Tạo thư mục lưu và ghi nhận trạng thái
|
|
ReportManager._ensure_report_folder(report_id)
|
|
|
|
# Khởi tạo bộ ghi log (Log có cấu trúc ghi ra file 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
|
|
)
|
|
|
|
# Khởi tạo logger để lưu thêm console (console_log.txt)
|
|
self.console_logger = ReportConsoleLogger(report_id)
|
|
|
|
ReportManager.update_progress(
|
|
report_id, "pending", 0, "Initializing report...",
|
|
completed_sections=[]
|
|
)
|
|
ReportManager.save_report(report)
|
|
|
|
# Bước 1: Lên dàn ý
|
|
report.status = ReportStatus.PLANNING
|
|
ReportManager.update_progress(
|
|
report_id, "planning", 5, "Start planning report outline...",
|
|
completed_sections=[]
|
|
)
|
|
|
|
# Ghi log bắt đầu giai đoạn lên dàn ý
|
|
self.report_logger.log_planning_start()
|
|
|
|
if progress_callback:
|
|
progress_callback("planning", 0, "Start planning report outline...")
|
|
|
|
outline = self.plan_outline(
|
|
progress_callback=lambda stage, prog, msg:
|
|
progress_callback(stage, prog // 5, msg) if progress_callback else None
|
|
)
|
|
report.outline = outline
|
|
|
|
# Ghi log hoàn thành dàn ý
|
|
self.report_logger.log_planning_complete(outline.to_dict())
|
|
|
|
# Lưu file dàn ý
|
|
ReportManager.save_outline(report_id, outline)
|
|
ReportManager.update_progress(
|
|
report_id, "planning", 15, f"Outline planning completed, total {len(outline.sections)} chapters",
|
|
completed_sections=[]
|
|
)
|
|
ReportManager.save_report(report)
|
|
|
|
logger.info(f"Outline saved to file: {report_id}/outline.json")
|
|
|
|
# Giai đoạn 2: Tạo từng chương (lưu theo từng chương)
|
|
report.status = ReportStatus.GENERATING
|
|
|
|
total_sections = len(outline.sections)
|
|
generated_sections = [] # Lưu nội dung lại cho ngữ cảnh những lần sau
|
|
|
|
for i, section in enumerate(outline.sections):
|
|
section_num = i + 1
|
|
base_progress = 20 + int((i / total_sections) * 70)
|
|
|
|
# Cập nhật tiến độ
|
|
ReportManager.update_progress(
|
|
report_id, "generating", base_progress,
|
|
f"Generating chapter: {section.title} ({section_num}/{total_sections})",
|
|
current_section=section.title,
|
|
completed_sections=completed_section_titles
|
|
)
|
|
|
|
if progress_callback:
|
|
progress_callback(
|
|
"generating",
|
|
base_progress,
|
|
f"Generating chapter: {section.title} ({section_num}/{total_sections})"
|
|
)
|
|
|
|
# Tạo nội dung chương
|
|
section_content = self._generate_section_react(
|
|
section=section,
|
|
outline=outline,
|
|
previous_sections=generated_sections,
|
|
progress_callback=lambda stage, prog, msg:
|
|
progress_callback(
|
|
stage,
|
|
base_progress + int(prog * 0.7 / total_sections),
|
|
msg
|
|
) if progress_callback else None,
|
|
section_index=section_num
|
|
)
|
|
|
|
section.content = section_content
|
|
generated_sections.append(f"## {section.title}\n\n{section_content}")
|
|
|
|
# Lưu chương
|
|
ReportManager.save_section(report_id, section_num, section)
|
|
completed_section_titles.append(section.title)
|
|
|
|
# Ghi lại kết quả khi chương ra lò
|
|
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()
|
|
)
|
|
|
|
logger.info(f"Chapter saved: {report_id}/section_{section_num:02d}.md")
|
|
|
|
# Cập nhật thanh tiến độ
|
|
ReportManager.update_progress(
|
|
report_id, "generating",
|
|
base_progress + int(70 / total_sections),
|
|
f"Chapter {section.title} completed",
|
|
current_section=None,
|
|
completed_sections=completed_section_titles
|
|
)
|
|
|
|
# Giai đoạn 3: Ghép lại toàn bộ báo cáo
|
|
if progress_callback:
|
|
progress_callback("generating", 95, "Assembling full report...")
|
|
|
|
ReportManager.update_progress(
|
|
report_id, "generating", 95, "Assembling full report...",
|
|
completed_sections=completed_section_titles
|
|
)
|
|
|
|
# Dùng ReportManager để ráp báo cáo
|
|
report.markdown_content = ReportManager.assemble_full_report(report_id, outline)
|
|
report.status = ReportStatus.COMPLETED
|
|
report.completed_at = datetime.now().isoformat()
|
|
|
|
# Tính toán thời gian
|
|
total_time_seconds = (datetime.now() - start_time).total_seconds()
|
|
|
|
# Ghi nhận log khi tạo thành công
|
|
if self.report_logger:
|
|
self.report_logger.log_report_complete(
|
|
total_sections=total_sections,
|
|
total_time_seconds=total_time_seconds
|
|
)
|
|
|
|
# Lưu kết quả cuối cùng
|
|
ReportManager.save_report(report)
|
|
ReportManager.update_progress(
|
|
report_id, "completed", 100, "Report generation completed",
|
|
completed_sections=completed_section_titles
|
|
)
|
|
|
|
if progress_callback:
|
|
progress_callback("completed", 100, "Report generation completed")
|
|
|
|
logger.info(f"Report generation completed: {report_id}")
|
|
|
|
# Tắt ghi log console
|
|
if self.console_logger:
|
|
self.console_logger.close()
|
|
self.console_logger = None
|
|
|
|
return report
|
|
|
|
except Exception as e:
|
|
logger.error(f"Report generation failed: {str(e)}")
|
|
report.status = ReportStatus.FAILED
|
|
report.error = str(e)
|
|
|
|
# Ghi lại log lỗi
|
|
if self.report_logger:
|
|
self.report_logger.log_error(str(e), "failed")
|
|
|
|
# Lưu lại trạng thái failed
|
|
try:
|
|
ReportManager.save_report(report)
|
|
ReportManager.update_progress(
|
|
report_id, "failed", -1, f"Report generation failed: {str(e)}",
|
|
completed_sections=completed_section_titles
|
|
)
|
|
except Exception:
|
|
pass # Lờ đi nếu bị lỗi trong khi lưu
|
|
|
|
# Tắt console log
|
|
if self.console_logger:
|
|
self.console_logger.close()
|
|
self.console_logger = None
|
|
|
|
return report
|
|
|
|
def chat(
|
|
self,
|
|
message: str,
|
|
chat_history: List[Dict[str, str]] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Trò chuyện cùng Report Agent
|
|
|
|
Trong quá trình chat, Agent có khả năng tự gọi công cụ tìm kiếm để trả lời
|
|
|
|
Args:
|
|
message: Tin nhắn mới của người dùng
|
|
chat_history: Lịch sử đàm thoại
|
|
|
|
Returns:
|
|
{
|
|
"response": "Nội dung trả lời của Agent",
|
|
"tool_calls": [Danh sách các công cụ đã sử dụng],
|
|
"sources": [Nguồn thông tin]
|
|
}
|
|
"""
|
|
logger.info(f"Report Agent chat: {message[:50]}...")
|
|
|
|
chat_history = chat_history or []
|
|
|
|
# Nhúng nội dung báo cáo cũ vào
|
|
report_content = ""
|
|
try:
|
|
report = ReportManager.get_report_by_simulation(self.simulation_id)
|
|
if report and report.markdown_content:
|
|
# Cắt bớt độ dài vì tránh nghẽn bộ nhớ
|
|
report_content = report.markdown_content[:15000]
|
|
if len(report.markdown_content) > 15000:
|
|
report_content += "\n\n... [Report content truncated] ..."
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get report content: {e}")
|
|
|
|
system_prompt = CHAT_SYSTEM_PROMPT_TEMPLATE.format(
|
|
simulation_requirement=self.simulation_requirement,
|
|
report_content=report_content if report_content else "(No report available)",
|
|
tools_description=self._get_tools_description(),
|
|
)
|
|
|
|
# Xây dựng message
|
|
messages = [{"role": "system", "content": system_prompt}]
|
|
|
|
# Nạp lịch sử cuộc trò chuyện
|
|
for h in chat_history[-10:]: # Giới hạn số lượng tin nhắn quá khứ
|
|
messages.append(h)
|
|
|
|
# Nạp tin nhắn mới nhất
|
|
messages.append({
|
|
"role": "user",
|
|
"content": message
|
|
})
|
|
|
|
# Vòng lặp ReACT (phiên bản thu gọn)
|
|
tool_calls_made = []
|
|
max_iterations = 2 # Giảm số lần lặp
|
|
|
|
for iteration in range(max_iterations):
|
|
response = self.llm.chat(
|
|
messages=messages,
|
|
temperature=0.5
|
|
)
|
|
|
|
# Phân tích lệnh gọi công cụ
|
|
tool_calls = self._parse_tool_calls(response)
|
|
|
|
if not tool_calls:
|
|
# Không thấy gọi công cụ, trả luôn kết quả
|
|
clean_response = re.sub(r'<tool_call>.*?</tool_call>', '', response, flags=re.DOTALL)
|
|
clean_response = re.sub(r'\[TOOL_CALL\].*?\)', '', clean_response)
|
|
|
|
return {
|
|
"response": clean_response.strip(),
|
|
"tool_calls": tool_calls_made,
|
|
"sources": [tc.get("parameters", {}).get("query", "") for tc in tool_calls_made]
|
|
}
|
|
|
|
# Khởi chạy công cụ (giới hạn số lượng)
|
|
tool_results = []
|
|
for call in tool_calls[:1]: # Cả vòng chỉ cho chạy công cụ tối đa 1 lần
|
|
if len(tool_calls_made) >= self.MAX_TOOL_CALLS_PER_CHAT:
|
|
break
|
|
result = self._execute_tool(call["name"], call.get("parameters", {}))
|
|
tool_results.append({
|
|
"tool": call["name"],
|
|
"result": result[:1500] # Giới hạn kích thước kết quả để tránh nghẽn
|
|
})
|
|
tool_calls_made.append(call)
|
|
|
|
# Đắp kết quả vào message
|
|
messages.append({"role": "assistant", "content": response})
|
|
observation = "\n".join([f"[{r['tool']} result]\n{r['result']}" for r in tool_results])
|
|
messages.append({
|
|
"role": "user",
|
|
"content": observation + CHAT_OBSERVATION_SUFFIX
|
|
})
|
|
|
|
# Đã đạt giới hạn lặp, sinh câu trả lời chốt chặn
|
|
final_response = self.llm.chat(
|
|
messages=messages,
|
|
temperature=0.5
|
|
)
|
|
|
|
# Dọn dẹp phản hồi
|
|
clean_response = re.sub(r'<tool_call>.*?</tool_call>', '', final_response, flags=re.DOTALL)
|
|
clean_response = re.sub(r'\[TOOL_CALL\].*?\)', '', clean_response)
|
|
|
|
return {
|
|
"response": clean_response.strip(),
|
|
"tool_calls": tool_calls_made,
|
|
"sources": [tc.get("parameters", {}).get("query", "") for tc in tool_calls_made]
|
|
}
|
|
|
|
|
|
class ReportManager:
|
|
"""
|
|
Trình Quản lý Báo cáo
|
|
|
|
Phụ trách việc lưu trữ lâu dài và trích xuất báo cáo
|
|
|
|
Cấu trúc thư mục (Lưu báo cáo phân thành từng chương):
|
|
reports/
|
|
{report_id}/
|
|
meta.json - File metadata và trạng thái
|
|
outline.json - Dàn ý
|
|
progress.json - Tiến trình phát sinh báo cáo
|
|
section_01.md - Chương 1
|
|
section_02.md - Chương 2
|
|
...
|
|
full_report.md - Báo cáo hoàn chỉnh
|
|
"""
|
|
|
|
# Phân vùng lưu trữ gốc
|
|
REPORTS_DIR = os.path.join(Config.UPLOAD_FOLDER, 'reports')
|
|
|
|
@classmethod
|
|
def _ensure_reports_dir(cls):
|
|
"""Đảm bảo thư mục lưu trữ gốc luôn tồn tại"""
|
|
os.makedirs(cls.REPORTS_DIR, exist_ok=True)
|
|
|
|
@classmethod
|
|
def _get_report_folder(cls, report_id: str) -> str:
|
|
"""Lấy đường dẫn thư mục báo cáo"""
|
|
return os.path.join(cls.REPORTS_DIR, report_id)
|
|
|
|
@classmethod
|
|
def _ensure_report_folder(cls, report_id: str) -> str:
|
|
"""Đảm bảo thư mục báo cáo tồn tại (và trả lại đường dẫn)"""
|
|
folder = cls._get_report_folder(report_id)
|
|
os.makedirs(folder, exist_ok=True)
|
|
return folder
|
|
|
|
@classmethod
|
|
def _get_report_path(cls, report_id: str) -> str:
|
|
"""Lấy file chi tiết cơ bản của báo cáo"""
|
|
return os.path.join(cls._get_report_folder(report_id), "meta.json")
|
|
|
|
@classmethod
|
|
def _get_report_markdown_path(cls, report_id: str) -> str:
|
|
"""Lấy file báo cáo dạng Markdown hợp nhất"""
|
|
return os.path.join(cls._get_report_folder(report_id), "full_report.md")
|
|
|
|
@classmethod
|
|
def _get_outline_path(cls, report_id: str) -> str:
|
|
"""Lấy file mang nội dung dàn ý"""
|
|
return os.path.join(cls._get_report_folder(report_id), "outline.json")
|
|
|
|
@classmethod
|
|
def _get_progress_path(cls, report_id: str) -> str:
|
|
"""Lấy file tiến độ tạo báo cáo"""
|
|
return os.path.join(cls._get_report_folder(report_id), "progress.json")
|
|
|
|
@classmethod
|
|
def _get_section_path(cls, report_id: str, section_index: int) -> str:
|
|
"""Lấy file cho một chương Markdown chỉ định"""
|
|
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:
|
|
"""Đường dẫn của Nhật ký 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:
|
|
"""Đường dẫn của file console log"""
|
|
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]:
|
|
"""
|
|
Lấy nội dung ghi chép file console_log
|
|
|
|
Đây là text được in ra khi tạo báo cáo (INFO, WARNING, v.v.),
|
|
Khác với báo cáo có cấu trúc JSON của agent_log.jsonl
|
|
|
|
Args:
|
|
report_id: ID báo cáo
|
|
from_line: Bắt đầu lấy từ dòng nào (để phục vụ chế độ tải tuần tự cho front-end, truyền 0 để lấy từ đầu)
|
|
|
|
Returns:
|
|
{
|
|
"logs": [Danh sách text log],
|
|
"total_lines": Tổng dòng hiện có,
|
|
"from_line": Số thứ tự dòng bắt đầu,
|
|
"has_more": Cờ xét còn trang sau không
|
|
}
|
|
"""
|
|
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:
|
|
# Giữ nguyên bản text, gạt bỏ breakline cuối chuỗi
|
|
logs.append(line.rstrip('\n\r'))
|
|
|
|
return {
|
|
"logs": logs,
|
|
"total_lines": total_lines,
|
|
"from_line": from_line,
|
|
"has_more": False # Vì đã đọc kịch sàn file
|
|
}
|
|
|
|
|
|
@classmethod
|
|
def get_console_log_stream(cls, report_id: str) -> List[str]:
|
|
"""
|
|
Lấy toàn bộ console log (lấy một lần duy nhất)
|
|
|
|
Args:
|
|
report_id: ID báo cáo
|
|
|
|
Returns:
|
|
Danh sách dòng log
|
|
"""
|
|
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]:
|
|
"""
|
|
Lấy nội dung Agent log
|
|
|
|
Args:
|
|
report_id: ID báo cáo
|
|
from_line: Bắt đầu lấy từ dòng nào (để phục vụ chế độ tải tuần tự cho front-end, truyền 0 để lấy từ đầu)
|
|
|
|
Returns:
|
|
{
|
|
"logs": [Danh sách đối tượng log],
|
|
"total_lines": Tổng dòng hiện có,
|
|
"from_line": Số thứ tự dòng bắt đầu,
|
|
"has_more": Cờ xét còn trang sau không
|
|
}
|
|
"""
|
|
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:
|
|
# Bỏ qua những dòng parse trượt
|
|
continue
|
|
|
|
return {
|
|
"logs": logs,
|
|
"total_lines": total_lines,
|
|
"from_line": from_line,
|
|
"has_more": False # Đã tới kịch trần file
|
|
}
|
|
|
|
@classmethod
|
|
def get_agent_log_stream(cls, report_id: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Lấy toàn bộ nội dung agent_log (để lấy một cú tóm gọn)
|
|
|
|
Args:
|
|
report_id: ID báo cáo
|
|
|
|
Returns:
|
|
Danh sách đối tượng log
|
|
"""
|
|
result = cls.get_agent_log(report_id, from_line=0)
|
|
return result["logs"]
|
|
|
|
@classmethod
|
|
def save_outline(cls, report_id: str, outline: ReportOutline) -> None:
|
|
"""
|
|
Lưu báo cáo dàn ý
|
|
|
|
Gọi ngay lập tức khi hoàn thành việc lên kế hoạch (planning)
|
|
"""
|
|
cls._ensure_report_folder(report_id)
|
|
|
|
with open(cls._get_outline_path(report_id), 'w', encoding='utf-8') as f:
|
|
json.dump(outline.to_dict(), f, ensure_ascii=False, indent=2)
|
|
|
|
logger.info(f"Outline saved: {report_id}")
|
|
|
|
@classmethod
|
|
def save_section(
|
|
cls,
|
|
report_id: str,
|
|
section_index: int,
|
|
section: ReportSection
|
|
) -> str:
|
|
"""
|
|
Lưu từng chương đơn lẻ
|
|
|
|
Gọi ngay sau khi tạo xong chương báo cáo, thực hiện việc in ấn từng phần
|
|
|
|
Args:
|
|
report_id: ID báo cáo
|
|
section_index: Chỉ mục chương (Cơ sở 1)
|
|
section: Đối tượng chương
|
|
|
|
Returns:
|
|
Đường dẫn thư mục được ghi
|
|
"""
|
|
cls._ensure_report_folder(report_id)
|
|
|
|
# Build Markdown content cho chương - xóa những tiêu đề trùng (nếu có)
|
|
cleaned_content = cls._clean_section_content(section.content, section.title)
|
|
md_content = f"## {section.title}\n\n"
|
|
if cleaned_content:
|
|
md_content += f"{cleaned_content}\n\n"
|
|
|
|
# Ghi đè tệp tin
|
|
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"Chapter saved: {report_id}/{file_suffix}")
|
|
return file_path
|
|
|
|
@classmethod
|
|
def _clean_section_content(cls, content: str, section_title: str) -> str:
|
|
"""
|
|
Làm sạch nội dung chương
|
|
|
|
1. Xóa các dòng tiêu đề Markdown ở đầu trùng lặp với tiêu đề chương
|
|
2. Biến đổi những tiêu đề cấp ### trở xuống thành in đậm
|
|
|
|
Args:
|
|
content: Nội dung thô
|
|
section_title: Tiêu đề chương
|
|
|
|
Returns:
|
|
Nội dung sau khi làm sạch
|
|
"""
|
|
import re
|
|
|
|
if not content:
|
|
return content
|
|
|
|
content = content.strip()
|
|
lines = content.split('\n')
|
|
cleaned_lines = []
|
|
skip_next_empty = False
|
|
|
|
for i, line in enumerate(lines):
|
|
stripped = line.strip()
|
|
|
|
# Kiểm tra dòng này có phải heading sinh từ MD không
|
|
heading_match = re.match(r'^(#{1,6})\s+(.+)$', stripped)
|
|
|
|
if heading_match:
|
|
level = len(heading_match.group(1))
|
|
title_text = heading_match.group(2).strip()
|
|
|
|
# Bỏ qua nếu tiêu đề ở tầm 5 dòng đầu lại trùng khớp với nhan đề chính
|
|
if i < 5:
|
|
if title_text == section_title or title_text.replace(' ', '') == section_title.replace(' ', ''):
|
|
skip_next_empty = True
|
|
continue
|
|
|
|
# Đổi toàn bộ các tag MD heading (#, ##, ###, v.v...) sang in đậm Text
|
|
# Do heading đã được config sinh từ tool gốc, không nên có MD style tại đây
|
|
cleaned_lines.append(f"**{title_text}**")
|
|
cleaned_lines.append("") # Gắn thêm dòng trống
|
|
continue
|
|
|
|
# Nếu dòng kề là dòng trắng thuộc chuỗi heading trùng lặp, cần lược bỏ luôn
|
|
if skip_next_empty and stripped == '':
|
|
skip_next_empty = False
|
|
continue
|
|
|
|
skip_next_empty = False
|
|
cleaned_lines.append(line)
|
|
|
|
# Bỏ đi mấy block dòng trắng vô danh ở mở đầu
|
|
while cleaned_lines and cleaned_lines[0].strip() == '':
|
|
cleaned_lines.pop(0)
|
|
|
|
# Xóa các gạch đường phân cách ở đầu
|
|
while cleaned_lines and cleaned_lines[0].strip() in ['---', '***', '___']:
|
|
cleaned_lines.pop(0)
|
|
# Remove đồng thời new lines gắn theo gạch ngang
|
|
while cleaned_lines and cleaned_lines[0].strip() == '':
|
|
cleaned_lines.pop(0)
|
|
|
|
return '\n'.join(cleaned_lines)
|
|
|
|
@classmethod
|
|
def update_progress(
|
|
cls,
|
|
report_id: str,
|
|
status: str,
|
|
progress: int,
|
|
message: str,
|
|
current_section: str = None,
|
|
completed_sections: List[str] = None
|
|
) -> None:
|
|
"""
|
|
Cập nhật tiến độ tạo báo cáo
|
|
|
|
Frontend có thể lấy tiến độ real-time theo progress.json
|
|
"""
|
|
cls._ensure_report_folder(report_id)
|
|
|
|
progress_data = {
|
|
"status": status,
|
|
"progress": progress,
|
|
"message": message,
|
|
"current_section": current_section,
|
|
"completed_sections": completed_sections or [],
|
|
"updated_at": datetime.now().isoformat()
|
|
}
|
|
|
|
with open(cls._get_progress_path(report_id), 'w', encoding='utf-8') as f:
|
|
json.dump(progress_data, f, ensure_ascii=False, indent=2)
|
|
|
|
@classmethod
|
|
def get_progress(cls, report_id: str) -> Optional[Dict[str, Any]]:
|
|
"""Lấy tiến độ tạo báo cáo"""
|
|
path = cls._get_progress_path(report_id)
|
|
|
|
if not os.path.exists(path):
|
|
return None
|
|
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
|
|
@classmethod
|
|
def get_generated_sections(cls, report_id: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Lấy danh sách các chương đã tạo xong
|
|
|
|
Trả về toàn bộ thông tin tệp từng phần
|
|
"""
|
|
folder = cls._get_report_folder(report_id)
|
|
|
|
if not os.path.exists(folder):
|
|
return []
|
|
|
|
sections = []
|
|
for filename in sorted(os.listdir(folder)):
|
|
if filename.startswith('section_') and filename.endswith('.md'):
|
|
file_path = os.path.join(folder, filename)
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Phân tích index của chương từ tên tệp
|
|
parts = filename.replace('.md', '').split('_')
|
|
section_index = int(parts[1])
|
|
|
|
sections.append({
|
|
"filename": filename,
|
|
"section_index": section_index,
|
|
"content": content
|
|
})
|
|
|
|
return sections
|
|
|
|
@classmethod
|
|
def assemble_full_report(cls, report_id: str, outline: ReportOutline) -> str:
|
|
"""
|
|
Lắp ráp toàn bộ báo cáo
|
|
|
|
Từ các chương đã lưu, tổng chắp báo cáo lại và dọn dẹp nhan đề
|
|
"""
|
|
folder = cls._get_report_folder(report_id)
|
|
|
|
# Build phần Header báo cáo
|
|
md_content = f"# {outline.title}\n\n"
|
|
md_content += f"> {outline.summary}\n\n"
|
|
md_content += f"---\n\n"
|
|
|
|
# Đọc files từ mọi chương theo thứ tự
|
|
sections = cls.get_generated_sections(report_id)
|
|
for section_info in sections:
|
|
md_content += section_info["content"]
|
|
|
|
# Hậu kiểm: dọn dẹp cấu trúc nhan đề trong toàn bộ file Markdown
|
|
md_content = cls._post_process_report(md_content, outline)
|
|
|
|
# Ghi lại tệp rốt cục
|
|
full_path = cls._get_report_markdown_path(report_id)
|
|
with open(full_path, 'w', encoding='utf-8') as f:
|
|
f.write(md_content)
|
|
|
|
logger.info(f"Full report assembled: {report_id}")
|
|
return md_content
|
|
|
|
@classmethod
|
|
def _post_process_report(cls, content: str, outline: ReportOutline) -> str:
|
|
"""
|
|
Hậu kiểm nội dung báo cáo
|
|
|
|
1. Xóa nhan đề trùng lặp
|
|
2. Dữ nguyên nhan đề chính (#) và nhan đề chương (##), xóa/giảm các tag nhan đề phụ (###, #### v.v)
|
|
3. Dọn dẹp khoảng trống và đường ranh giới thừa thãi
|
|
|
|
Args:
|
|
content: Nội dung thô của báo cáo
|
|
outline: Dàn ý
|
|
|
|
Returns:
|
|
Nội dung sau khi xử lý
|
|
"""
|
|
import re
|
|
|
|
lines = content.split('\n')
|
|
processed_lines = []
|
|
prev_was_heading = False
|
|
|
|
# Gom góp tất thảy nhan đề từ outline
|
|
section_titles = set()
|
|
for section in outline.sections:
|
|
section_titles.add(section.title)
|
|
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
stripped = line.strip()
|
|
|
|
# Kiểm tra coi đây có là nhan đề hay không
|
|
heading_match = re.match(r'^(#{1,6})\s+(.+)$', stripped)
|
|
|
|
if heading_match:
|
|
level = len(heading_match.group(1))
|
|
title = heading_match.group(2).strip()
|
|
|
|
# Kiểm tra nhan đề này đang quẩn quanh lặp lại chuỗi cũ chăng (ngưỡng 5 dòng)
|
|
is_duplicate = False
|
|
for j in range(max(0, len(processed_lines) - 5), len(processed_lines)):
|
|
prev_line = processed_lines[j].strip()
|
|
prev_match = re.match(r'^(#{1,6})\s+(.+)$', prev_line)
|
|
if prev_match:
|
|
prev_title = prev_match.group(2).strip()
|
|
if prev_title == title:
|
|
is_duplicate = True
|
|
break
|
|
|
|
if is_duplicate:
|
|
# Rũ đi dòng nhan đề trùng lặp và các dòng newline ngay sau
|
|
i += 1
|
|
while i < len(lines) and lines[i].strip() == '':
|
|
i += 1
|
|
continue
|
|
|
|
# Xử lý quy mô cấp bậc tiêu đề:
|
|
# - # (level=1) Chỉ dành cho nhan đề lõi của toàn văn kiện
|
|
# - ## (level=2) Bám dính tiêu đề chương mục
|
|
# - ### và thấp hơn (level>=3) Băm thành chữ in đậm
|
|
|
|
if level == 1:
|
|
if title == outline.title:
|
|
# Giữ nhan đề gốc
|
|
processed_lines.append(line)
|
|
prev_was_heading = True
|
|
elif title in section_titles:
|
|
# Do nhan đề chương đánh bừa ra thẻ #, buộc ép về lại ##
|
|
processed_lines.append(f"## {title}")
|
|
prev_was_heading = True
|
|
else:
|
|
# Các tiêu đề cấp 1 tạp nham cho in đậm
|
|
processed_lines.append(f"**{title}**")
|
|
processed_lines.append("")
|
|
prev_was_heading = False
|
|
elif level == 2:
|
|
if title in section_titles or title == outline.title:
|
|
# Bảo lưu nhan đề chương
|
|
processed_lines.append(line)
|
|
prev_was_heading = True
|
|
else:
|
|
# Nếu chả phải chương thì in đậm nốt thẻ cấp 2
|
|
processed_lines.append(f"**{title}**")
|
|
processed_lines.append("")
|
|
prev_was_heading = False
|
|
else:
|
|
# Thẻ cấp 3 trở đi cho in đậm
|
|
processed_lines.append(f"**{title}**")
|
|
processed_lines.append("")
|
|
prev_was_heading = False
|
|
|
|
i += 1
|
|
continue
|
|
|
|
elif stripped == '---' and prev_was_heading:
|
|
# Tránh lôi đường chia gạch dưới title
|
|
i += 1
|
|
continue
|
|
|
|
elif stripped == '' and prev_was_heading:
|
|
# Ngậm 1 dòng trắng kề sau nhan đề
|
|
if processed_lines and processed_lines[-1].strip() != '':
|
|
processed_lines.append(line)
|
|
prev_was_heading = False
|
|
|
|
else:
|
|
processed_lines.append(line)
|
|
prev_was_heading = False
|
|
|
|
i += 1
|
|
|
|
# Vuốt láng khoảng trắng đa tầng (chừa max 2)
|
|
result_lines = []
|
|
empty_count = 0
|
|
for line in processed_lines:
|
|
if line.strip() == '':
|
|
empty_count += 1
|
|
if empty_count <= 2:
|
|
result_lines.append(line)
|
|
else:
|
|
empty_count = 0
|
|
result_lines.append(line)
|
|
|
|
return '\n'.join(result_lines)
|
|
|
|
@classmethod
|
|
def save_report(cls, report: Report) -> None:
|
|
"""Lưu lại metadata và toàn văn báo cáo"""
|
|
cls._ensure_report_folder(report.report_id)
|
|
|
|
# Ghi meta file dạng JSON
|
|
with open(cls._get_report_path(report.report_id), 'w', encoding='utf-8') as f:
|
|
json.dump(report.to_dict(), f, ensure_ascii=False, indent=2)
|
|
|
|
# Ghi mục lục tổng quan
|
|
if report.outline:
|
|
cls.save_outline(report.report_id, report.outline)
|
|
|
|
# Lưu file MD trọn gói
|
|
if report.markdown_content:
|
|
with open(cls._get_report_markdown_path(report.report_id), 'w', encoding='utf-8') as f:
|
|
f.write(report.markdown_content)
|
|
|
|
logger.info(f"Report saved: {report.report_id}")
|
|
|
|
@classmethod
|
|
def get_report(cls, report_id: str) -> Optional[Report]:
|
|
"""Lấy về toàn văn mục báo cáo"""
|
|
path = cls._get_report_path(report_id)
|
|
|
|
if not os.path.exists(path):
|
|
# Tương thích ngược: thử xem file .json trần trụi ở ngay thư mục gốc
|
|
old_path = os.path.join(cls.REPORTS_DIR, f"{report_id}.json")
|
|
if os.path.exists(old_path):
|
|
path = old_path
|
|
else:
|
|
return None
|
|
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
# Cấu trúc lại model
|
|
outline = None
|
|
if data.get('outline'):
|
|
outline_data = data['outline']
|
|
sections = []
|
|
for s in outline_data.get('sections', []):
|
|
sections.append(ReportSection(
|
|
title=s['title'],
|
|
content=s.get('content', '')
|
|
))
|
|
outline = ReportOutline(
|
|
title=outline_data['title'],
|
|
summary=outline_data['summary'],
|
|
sections=sections
|
|
)
|
|
|
|
# Nếu chưa bếch được đoạn markdown thì tìm từ file .md
|
|
markdown_content = data.get('markdown_content', '')
|
|
if not markdown_content:
|
|
full_report_path = cls._get_report_markdown_path(report_id)
|
|
if os.path.exists(full_report_path):
|
|
with open(full_report_path, 'r', encoding='utf-8') as f:
|
|
markdown_content = f.read()
|
|
|
|
return Report(
|
|
report_id=data['report_id'],
|
|
simulation_id=data['simulation_id'],
|
|
graph_id=data['graph_id'],
|
|
simulation_requirement=data['simulation_requirement'],
|
|
status=ReportStatus(data['status']),
|
|
outline=outline,
|
|
markdown_content=markdown_content,
|
|
created_at=data.get('created_at', ''),
|
|
completed_at=data.get('completed_at', ''),
|
|
error=data.get('error')
|
|
)
|
|
|
|
@classmethod
|
|
def get_report_by_simulation(cls, simulation_id: str) -> Optional[Report]:
|
|
"""Tìm báo cáo thông qua ID phiến giả lập"""
|
|
cls._ensure_reports_dir()
|
|
|
|
for item in os.listdir(cls.REPORTS_DIR):
|
|
item_path = os.path.join(cls.REPORTS_DIR, item)
|
|
# Định dạng mới coi nó là directory
|
|
if os.path.isdir(item_path):
|
|
report = cls.get_report(item)
|
|
if report and report.simulation_id == simulation_id:
|
|
return report
|
|
# Tương thích format cũ: .json
|
|
elif item.endswith('.json'):
|
|
report_id = item[:-5]
|
|
report = cls.get_report(report_id)
|
|
if report and report.simulation_id == simulation_id:
|
|
return report
|
|
|
|
return None
|
|
|
|
@classmethod
|
|
def list_reports(cls, simulation_id: Optional[str] = None, limit: int = 50) -> List[Report]:
|
|
"""Liệt kê rổ báo cáo"""
|
|
cls._ensure_reports_dir()
|
|
|
|
reports = []
|
|
for item in os.listdir(cls.REPORTS_DIR):
|
|
item_path = os.path.join(cls.REPORTS_DIR, item)
|
|
# Standard mới
|
|
if os.path.isdir(item_path):
|
|
report = cls.get_report(item)
|
|
if report:
|
|
if simulation_id is None or report.simulation_id == simulation_id:
|
|
reports.append(report)
|
|
# Standard quá độ .json
|
|
elif item.endswith('.json'):
|
|
report_id = item[:-5]
|
|
report = cls.get_report(report_id)
|
|
if report:
|
|
if simulation_id is None or report.simulation_id == simulation_id:
|
|
reports.append(report)
|
|
|
|
# Sort descending bởi thời gian tạo
|
|
reports.sort(key=lambda r: r.created_at, reverse=True)
|
|
|
|
return reports[:limit]
|
|
|
|
@classmethod
|
|
def delete_report(cls, report_id: str) -> bool:
|
|
"""Xóa rễ cụm folder tương ứng với report"""
|
|
import shutil
|
|
|
|
folder_path = cls._get_report_folder(report_id)
|
|
|
|
# Định dạng mới quăng folder là mượt
|
|
if os.path.exists(folder_path) and os.path.isdir(folder_path):
|
|
shutil.rmtree(folder_path)
|
|
logger.info(f"Report folder removed: {report_id}")
|
|
return True
|
|
|
|
# Mode tương thích: xóa file
|
|
deleted = False
|
|
old_json_path = os.path.join(cls.REPORTS_DIR, f"{report_id}.json")
|
|
old_md_path = os.path.join(cls.REPORTS_DIR, f"{report_id}.md")
|
|
|
|
if os.path.exists(old_json_path):
|
|
os.remove(old_json_path)
|
|
deleted = True
|
|
if os.path.exists(old_md_path):
|
|
os.remove(old_md_path)
|
|
deleted = True
|
|
|
|
return deleted
|