diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index cb7f0dff..abd35d3d 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -14,6 +14,7 @@ from ..services.ontology_generator import OntologyGenerator from ..services.graph_builder import GraphBuilderService from ..services.text_processor import TextProcessor from ..utils.file_parser import FileParser +from ..utils.llm_client import LLMClient from ..utils.logger import get_logger from ..models.task import TaskManager, TaskStatus from ..models.project import ProjectManager, ProjectStatus @@ -30,8 +31,6 @@ def allowed_file(filename: str) -> bool: return ext in Config.ALLOWED_EXTENSIONS -# ============== 项目管理接口 ============== - @graph_bp.route('/project/', methods=['GET']) def get_project(project_id: str): """ @@ -213,7 +212,15 @@ def generate_ontology(): # Generate ontology definition using LLM logger.info("Calling LLM to generate ontology definition...") - generator = OntologyGenerator() + generator = OntologyGenerator( + llm_client=LLMClient( + component="ontology_generator", + metadata={ + "project_id": project.project_id, + "phase": "ontology_generation", + }, + ) + ) ontology = generator.generate( document_texts=document_texts, simulation_requirement=simulation_requirement, @@ -256,6 +263,7 @@ def generate_ontology(): # ============== API 2: Build graph ============== +# API ROUTE DEFINITION - Flash route handler for graph construction requests @graph_bp.route('/build', methods=['POST']) def build_graph(): """ @@ -278,6 +286,12 @@ def build_graph(): "message": "Graph build task started" } } + + Graph builder workflow + - [1] - create_graph() + - [2] - set_ontology() + - [3] - split_text() + - [4] - add_text_batches() """ try: logger.info("=== Start building graph ===") @@ -304,7 +318,8 @@ def build_graph(): "error": "Please provide project_id" }), 400 - # Lấy project + # RETRIEVE PROJECT CONTEXT + # Fetches project stae including ontology from previous phase project = ProjectManager.get_project(project_id) if not project: return jsonify({ @@ -381,7 +396,8 @@ def build_graph(): message="Initializing graph builder service..." ) - # Tạo graph builder service + # INITIALIZE GRAPH BUILDER + # Creates service instance with Zep Cloud API client builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) # Chunk text @@ -390,6 +406,7 @@ def build_graph(): message="Splitting text into chunks...", progress=5 ) + # [3] - Splits document into overlapping chunks for processing chunks = TextProcessor.split_text( text, chunk_size=chunk_size, @@ -403,6 +420,8 @@ def build_graph(): message="Creating Zep graph...", progress=10 ) + + # [1] - Initializes empty graph in Zep Cloud with unique ID graph_id = builder.create_graph(name=graph_name) # Cập nhật graph_id của project @@ -415,11 +434,12 @@ def build_graph(): message="Setting ontology definition...", progress=15 ) + # [2] - Defines entity and relationship types for the graph builder.set_ontology(graph_id, ontology) # Callback cập nhật progress khi add text def add_progress_callback(msg, progress_ratio): - progress = 15 + int(progress_ratio * 40) + progress = 15 + int(progress_ratio * 40) # 15% - 55% task_manager.update_task( task_id, message=msg, @@ -432,6 +452,7 @@ def build_graph(): progress=15 ) + # [4] - Sends text chunks as episodes to Zep for entity extraction episode_uuids = builder.add_text_batches( graph_id, chunks, @@ -446,14 +467,16 @@ def build_graph(): progress=55 ) + # Progress callback updates UI def wait_progress_callback(msg, progress_ratio): - progress = 55 + int(progress_ratio * 35) + progress = 55 + int(progress_ratio * 35) # # 55% - 90% task_manager.update_task( task_id, message=msg, progress=progress ) + # WAITING FOR PROCESSING - Initiates polling loop for episode completion builder._wait_for_episodes(episode_uuids, wait_progress_callback) # Lấy graph data @@ -464,7 +487,7 @@ def build_graph(): ) graph_data = builder.get_graph_data(graph_id) - # Cập nhật trạng thái project + # Update project status project.status = ProjectStatus.GRAPH_COMPLETED ProjectManager.save_project(project) @@ -503,7 +526,8 @@ def build_graph(): error=traceback.format_exc() ) - # Chạy background thread + # START ASYNC TASK + # Lauches background thread for long-running graph construction thread = threading.Thread(target=build_task, daemon=True) thread.start() diff --git a/backend/app/api/report.py b/backend/app/api/report.py index 1111d281..6715f5aa 100644 --- a/backend/app/api/report.py +++ b/backend/app/api/report.py @@ -134,6 +134,7 @@ def generate_report(): agent = ReportAgent( graph_id=graph_id, simulation_id=simulation_id, + project_id=state.project_id, simulation_requirement=simulation_requirement ) @@ -540,6 +541,7 @@ def chat_with_report_agent(): agent = ReportAgent( graph_id=graph_id, simulation_id=simulation_id, + project_id=state.project_id, simulation_requirement=simulation_requirement ) diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 48b6e99f..71960cf7 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -21,8 +21,9 @@ logger = get_logger('mirofish.api.simulation') # Tiền tố tối ưu cho Interview prompt # Thêm tiền tố này để tránh Agent gọi công cụ, trả lời trực tiếp bằng văn bản -INTERVIEW_PROMPT_PREFIX = "Based on your persona, all past memories and actions, reply directly in plain text without calling any tools:" +# INTERVIEW_PROMPT_PREFIX = "Based on your persona, all past memories and actions, reply directly in plain text without calling any tools:" +INTERVIEW_PROMPT_PREFIX = "Dựa trên nhân vật, tất cả ký ức và hành động trong quá khứ của bạn, hãy trả lời trực tiếp bằng văn bản thuần túy mà không gọi bất kỳ công cụ nào:" def optimize_interview_prompt(prompt: str) -> str: """ @@ -1397,6 +1398,7 @@ def generate_profiles(): entity_types = data.get('entity_types') use_llm = data.get('use_llm', True) platform = data.get('platform', 'reddit') + project_id = data.get('project_id') reader = ZepEntityReader() filtered = reader.filter_defined_entities( @@ -1414,7 +1416,8 @@ def generate_profiles(): generator = OasisProfileGenerator() profiles = generator.generate_profiles_from_entities( entities=filtered.entities, - use_llm=use_llm + use_llm=use_llm, + project_id=project_id, ) if platform == "reddit": diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 2464eba4..f49a9427 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -183,7 +183,7 @@ class ProjectManager: project_id: Project ID Returns: - Project对象,如果不存在返回None + Project object; if it does not exist, return None. """ meta_path = cls._get_project_meta_path(project_id) diff --git a/backend/app/services/graph_builder.py b/backend/app/services/graph_builder.py index efa5fb77..bbff79d1 100644 --- a/backend/app/services/graph_builder.py +++ b/backend/app/services/graph_builder.py @@ -84,6 +84,7 @@ class GraphBuilderService: ) # Bắt đầu gọi Workder ở luồng ảo (Back ground Thread) để người dùng không tắc giao diện đợi xử lý + # Asynchronous Execution: Graph building runs in background threads with progress tracking through a task management system thread = threading.Thread( target=self._build_graph_worker, args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size) @@ -120,7 +121,7 @@ class GraphBuilderService: message=f"Created empty graph: {graph_id}" ) - # 2. 设置本体 + # 2. Thiết lập ontology self.set_ontology(graph_id, ontology) self.task_manager.update_task( task_id, @@ -129,7 +130,8 @@ class GraphBuilderService: ) # Bước 3. Chia nhỏ văn bản gốc - chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap) + # Text Processing: Chunks documents and sends them to Zep for entity extraction + chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap) # Split into chunks total_chunks = len(chunks) self.task_manager.update_task( task_id, @@ -138,6 +140,7 @@ class GraphBuilderService: ) # Bước 4. Gửi các đợt chunk tới Zep dưới dạng batch + # Send in batches (size=3) episode_uuids = self.add_text_batches( graph_id, chunks, batch_size, lambda msg, prog: self.task_manager.update_task( @@ -154,6 +157,7 @@ class GraphBuilderService: message="Waiting for Zep to process data..." ) + # Wait for Zep processing self._wait_for_episodes( episode_uuids, lambda msg, prog: self.task_manager.update_task( @@ -170,6 +174,7 @@ class GraphBuilderService: message="Fetching finalized graph info..." ) + # Retrieve graph statistics graph_info = self._get_graph_info(graph_id) # Thông báo hoàn tất @@ -185,9 +190,12 @@ class GraphBuilderService: self.task_manager.fail_task(task_id, error_msg) def create_graph(self, name: str) -> str: - """Khai báo một Graph mới với Zep API (Sử dụng công khai public)""" + """Graph Creation: Initializes a new graph in Zep Cloud with a unique ID""" + + # Generate unique graph_id graph_id = f"mirofish_{uuid.uuid4().hex[:16]}" + # Call Zep API create() self.client.graph.create( graph_id=graph_id, name=name, @@ -197,7 +205,10 @@ class GraphBuilderService: return graph_id def set_ontology(self, graph_id: str, ontology: Dict[str, Any]): - """Cấu hình dữ liệu Ontology (Bản thể học) cho Graph trên server Zep (Public access)""" + """ + Cấu hình dữ liệu Ontology (Bản thể học) cho Graph trên server Zep (Public access) + Ontology Setup: Defines the schema for entities and relationships using dynamic class creation. + """ import warnings from typing import Optional from pydantic import Field @@ -218,6 +229,8 @@ class GraphBuilderService: # Khởi tạo động (Dynamic Class Creation) các Model Loại Thực thể từ JSON đầu vào entity_types = {} + + # Processes each entity type from ontology definition for entity_def in ontology.get("entity_types", []): name = entity_def["name"] description = entity_def.get("description", f"A {name} entity.") @@ -235,10 +248,10 @@ class GraphBuilderService: attrs["__annotations__"] = annotations - # Dựng Class ảo + # Dynamic class generation - Create Pydnatic model class for entity type at runtime entity_class = type(name, (EntityModel,), attrs) entity_class.__doc__ = description - entity_types[name] = entity_class + entity_types[name] = entity_class # Store in entity_types dict # Tương tự, dựa vào JSON để khởi tạo động khai báo các Model Loại Quan Hệ edge_definitions = {} @@ -261,6 +274,7 @@ class GraphBuilderService: # Khởi tạo Class động với Tên chuẩn format (PascalCase) class_name = ''.join(word.capitalize() for word in name.split('_')) + # Creates Pydantic model class for relationship type edge_class = type(class_name, (EdgeModel,), attrs) edge_class.__doc__ = description @@ -275,10 +289,11 @@ class GraphBuilderService: ) if source_targets: - edge_definitions[name] = (edge_class, source_targets) + edge_definitions[name] = (edge_class, source_targets) # Store in edge_definitions dict # Action Gọi lệnh thay đổi Ontology cho môi trường GraphID của Zep if entity_types or edge_definitions: + # Call Zep set_ontology() self.client.graph.set_ontology( graph_ids=[graph_id], entities=entity_types if entity_types else None, @@ -308,7 +323,7 @@ class GraphBuilderService: progress ) - # Chuẩn bị định dạng gói dữ liệu (Episode data) để tương thích Zep Graph + # Create EpisodeData objects episodes = [ EpisodeData(data=chunk, type="text") for chunk in batch_chunks @@ -316,6 +331,7 @@ class GraphBuilderService: # Khởi chạy gửi cho Zep Server try: + # UPLOAD BATCH TO ZEP - Sends batch of episodes for entity extraction batch_result = self.client.graph.add_batch( graph_id=graph_id, episodes=episodes @@ -371,10 +387,13 @@ class GraphBuilderService: # Duyệt vòng lặp mỗi episode uuid để lấy cập nhật tiến trình check của từng episode một for ep_uuid in list(pending_episodes): try: + # CHECK EPISODE STATUS - Queries individual episode processing status episode = self.client.graph.episode.get(uuid_=ep_uuid) + # CHECK PROCESSED FLAG - Determines if Zep is_processed = getattr(episode, 'processed', False) if is_processed: + # if processed: remove from set pending_episodes.remove(ep_uuid) completed_count += 1 @@ -383,6 +402,7 @@ class GraphBuilderService: pass elapsed = int(time.time() - start_time) + # update progress callback if progress_callback: progress_callback( f"Zep is processing in the background... {completed_count}/{total_episodes} done, {len(pending_episodes)} tasks remaining ({elapsed}s elapsed)", @@ -393,6 +413,7 @@ class GraphBuilderService: time.sleep(3) # Lặp chu kỳ check mỗi 3 giây if progress_callback: + # Final completion message progress_callback(f"Data upload process completed: {completed_count}/{total_episodes}", 1.0) def _get_graph_info(self, graph_id: str) -> GraphInfo: diff --git a/backend/app/services/oasis_profile_generator.py b/backend/app/services/oasis_profile_generator.py index 4d2c5a97..d5425571 100644 --- a/backend/app/services/oasis_profile_generator.py +++ b/backend/app/services/oasis_profile_generator.py @@ -20,6 +20,7 @@ from zep_cloud.client import Zep from ..config import Config from ..utils.logger import get_logger +from ..utils.llm_cost import create_tracked_chat_completion from .zep_entity_reader import EntityNode, ZepEntityReader logger = get_logger('mirofish.oasis_profile') @@ -196,6 +197,10 @@ class OasisProfileGenerator: api_key=self.api_key, base_url=self.base_url ) + self._runtime_metadata: Dict[str, Any] = { + "component": "oasis_profile_generator", + "phase": "generate_profiles", + } # Kết nối lên trên ZEP Database Search Context self.zep_api_key = zep_api_key or Config.ZEP_API_KEY @@ -525,15 +530,16 @@ class OasisProfileGenerator: for attempt in range(max_attempts): try: - response = self.client.chat.completions.create( + response = create_tracked_chat_completion( + client=self.client, model=self.model_name, messages=[ {"role": "system", "content": self._get_system_prompt(is_individual)}, {"role": "user", "content": prompt} ], response_format={"type": "json_object"}, - temperature=0.7 - (attempt * 0.1) # Giảm tính sáng tạo ngẫu nhiên đi một chút mỗi khi fail để tăng khả năng thành công ở vòng tiếp theo - # Thả Rông Max tokens + temperature=0.7 - (attempt * 0.1), # Giảm tính sáng tạo ngẫu nhiên đi một chút mỗi khi fail để tăng khả năng thành công ở vòng tiếp theo + metadata=self._runtime_metadata, ) content = response.choices[0].message.content @@ -669,7 +675,10 @@ class OasisProfileGenerator: def _get_system_prompt(self, is_individual: bool) -> str: """Lấy prompt cho hệ thống""" - base_prompt = "Bạn là một chuyên gia tạo chân dung người dùng mạng xã hội. Tạo một nhân vật chi tiết, chân thực để mô phỏng dư luận, phục hồi bối cảnh thế giới thực ở mức tối đa. Bắt buộc phải trả về định dạng JSON hợp lệ, tất cả các chuỗi không được chứa ký tự xuống dòng chưa được escape. Sử dụng tiếng Việt." + + # base_prompt = "You are an expert in generating social media user personas. Generate detailed and realistic personas for public opinion simulation to recreate existing real-world conditions to the greatest extent possible. You must return a valid JSON format; all string values must not contain unescaped line breaks. Use Chinese." + + base_prompt = "Bạn là chuyên gia tạo hồ sơ người dùng mạng xã hội. Hãy tạo các nhân vật chi tiết và chân thực phục vụ cho việc mô phỏng dư luận, nhằm tái hiện tối đa các tình huống thực tế hiện có. Phải trả về định dạng JSON hợp lệ; tất cả các giá trị chuỗi không được chứa ký tự xuống dòng chưa được xử lý (unescaped). Sử dụng tiếng Việt." return base_prompt def _build_individual_persona_prompt( @@ -685,7 +694,42 @@ class OasisProfileGenerator: attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "Không có" context_str = context[:3000] if context else "Không có ngữ cảnh bổ sung" - return f"""Tạo một thiết lập người dùng mạng xã hội chi tiết cho thực thể, phản ánh tối đa tình hình thực tế hiện có. +# return f"""Generate a detailed social media user persona for the entity, recreating existing real-world conditions to the greatest extent possible. + +# Entity Name: {entity_name} +# Entity Type: {entity_type} +# Entity Summary: {entity_summary} +# Entity Attributes: {attrs_str} + +# Context Information: +# {context_str} + +# Please generate a JSON containing the following fields: + +# 1. bio: Social media biography, 200 characters. +# 2. persona: Detailed persona description (2000 words of plain text), which must include: +# - Basic information (age, occupation, educational background, location) +# - Background (significant experiences, connection to the event, social relationships) +# - Personality traits (MBTI type, core personality, emotional expression style) +# - Social media behavior (posting frequency, content preferences, interaction style, linguistic characteristics) +# - Stance and views (attitude toward the topic, content that might provoke or move them) +# - Unique features (catchphrases, special experiences, personal hobbies) +# - Personal memory (a vital part of the persona, describing the individual's connection to the event and their existing actions/reactions) +# 3. age: Age as a number (must be an integer) +# 4. gender: Gender, must be in English: "male" or "female" +# 5. mbti: MBTI type (e.g., INTJ, ENFP, etc.) +# 6. country: Country (use Chinese, e.g., "中国") +# 7. profession: Occupation +# 8. interested_topics: An array of interested topics + +# IMPORTANT: +# - All field values must be strings or numbers; do not use line breaks. +# - The 'persona' must be a coherent block of text description. +# - Use Chinese (except for the 'gender' field, which must be English male/female). +# - Content must remain consistent with the entity information. +# - 'age' must be a valid integer; 'gender' must be "male" or "female".""" + + return f"""Tạo hồ sơ người dùng mạng xã hội chi tiết cho thực thể, tái hiện tối đa các tình huống thực tế hiện có. Tên thực thể: {entity_name} Loại thực thể: {entity_type} @@ -695,30 +739,30 @@ Thuộc tính thực thể: {attrs_str} Thông tin ngữ cảnh: {context_str} -Vui lòng tạo JSON, bao gồm các trường sau: - -1. bio: Giới thiệu mạng xã hội, 200 chữ -2. persona: Mô tả chi tiết nhân vật (văn bản thuần 2000 chữ), cần bao gồm: - - Thông tin cơ bản (Tuổi, Nghề nghiệp, Nền tảng học vấn, Vị trí hiện tại) - - Bối cảnh nhân vật (Kinh nghiệm quan trọng, Mối liên hệ với sự kiện, Quan hệ xã hội) - - Đặc điểm tính cách (Loại MBTI, Tính cách cốt lõi, Cách thể hiện cảm xúc) - - Hành vi Mạng xã hội (Tần suất đăng bài, Sở thích nội dung, Phong cách tương tác, Đặc điểm ngôn ngữ) - - Quan điểm lập trường (Thái độ đối với chủ đề, Nội dung có thể gây phẫn nộ/cảm động) - - Đặc điểm độc đáo (Câu cửa miệng, Kinh nghiệm đặc biệt, Sở thích cá nhân) - - Ký ức cá nhân (Phần quan trọng của nhân vật, cần giới thiệu rõ mối liên hệ giữa cá nhân này với sự kiện, cũng như các hành động và phản ứng của người này trong sự kiện) -3. age: Tuổi (bắt buộc phải là số nguyên) -4. gender: Giới tính, bắt buộc bằng tiếng Anh: "male" hoặc "female" -5. mbti: Loại MBTI (ví dụ: INTJ, ENFP, v.v.) -6. country: Quốc gia (sử dụng tiếng Việt, ví dụ "Việt Nam") +Vui lòng tạo JSON bao gồm các trường sau: + +1. bio: Tiểu sử mạng xã hội, 200 ký tự. +2. persona: Mô tả nhân vật chi tiết (văn bản thuần túy khoảng 2000 từ), cần bao gồm: + - Thông tin cơ bản (tuổi, nghề nghiệp, trình độ học vấn, nơi ở) + - Nền tảng nhân vật (trải nghiệm quan trọng, mối liên hệ với sự kiện, quan hệ xã hội) + - Đặc điểm tính cách (loại MBTI, tính cách cốt lõi, cách biểu đạt cảm xúc) + - Hành vi mạng xã hội (tần suất đăng bài, sở thích nội dung, phong cách tương tác, đặc điểm ngôn ngữ) + - Lập trường quan điểm (thái độ đối với chủ đề, nội dung dễ gây kích động hoặc gây xúc động) + - Đặc điểm độc đáo (câu cửa miệng, trải nghiệm đặc biệt, sở thích cá nhân) + - Ký ức cá nhân (phần quan trọng của nhân vật, giới thiệu mối liên hệ của cá nhân này với sự kiện, cũng như các hành động và phản ứng đã có của họ trong sự kiện) +3. age: Con số tuổi (phải là số nguyên) +4. gender: Giới tính, phải là tiếng Anh: "male" hoặc "female" +5. mbti: Loại MBTI (như INTJ, ENFP, v.v.) +6. country: Quốc gia (sử dụng tiếng Trung, ví dụ: "中国") 7. profession: Nghề nghiệp 8. interested_topics: Mảng các chủ đề quan tâm - -Quan trọng: -- Tất cả giá trị các trường phải là chuỗi hoặc số, không sử dụng ký tự xuống dòng (\n) -- persona phải là một đoạn mô tả văn bản liên tục, không ngắt quãng -- Sử dụng tiếng Việt (ngoại trừ trường gender bắt buộc dùng tiếng Anh male/female) -- Nội dung phải nhất quán với thông tin của thực thể -- age phải là số nguyên hợp lệ, gender phải là "male" hoặc "female" + +QUAN TRỌNG: +- Tất cả giá trị các trường phải là chuỗi hoặc số, không sử dụng ký tự xuống dòng. +- 'persona' phải là một đoạn mô tả văn bản mạch lạc. +- Sử dụng tiếng Trung (ngoại trừ trường 'gender' phải dùng tiếng Anh male/female). +- Nội dung phải nhất quán với thông tin thực thể. +- 'age' phải là số nguyên hợp lệ, 'gender' phải là "male" hoặc "female". """ def _build_group_persona_prompt( @@ -731,10 +775,46 @@ Quan trọng: ) -> str: """Tạo prompt chi tiết cho tài khoản đại diện tổ chức/nhóm""" - attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "Không có" - context_str = context[:3000] if context else "Không có ngữ cảnh bổ sung" + attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "None" + context_str = context[:3000] if context else "No additional context" + +# return f"""Generate a detailed social media account persona for an organization/group entity, recreating existing real-world conditions to the greatest extent possible. + +# Entity Name: {entity_name} +# Entity Type: {entity_type} +# Entity Summary: {entity_summary} +# Entity Attributes: {attrs_str} + +# Context Information: +# {context_str} + +# Please generate a JSON containing the following fields: + +# 1. bio: Official account biography, 200 characters, professional and appropriate. +# 2. persona: Detailed account setting description (2000 words of plain text), which must include: +# - Basic information (formal name, nature of the organization, establishment background, primary functions) +# - Account positioning (account type, target audience, core functions) +# - Communication style (linguistic characteristics, common expressions, taboo topics) +# - Content characteristics (content types, posting frequency, active time periods) +# - Stance and attitude (official stance on core topics, handling of controversies) +# - Special notes (persona of the group represented, operational habits) +# - Organizational memory (a vital part of the persona, describing the organization's connection to the event and its existing actions/reactions) +# 3. age: Fixed at 30 (virtual age for an organizational account) +# 4. gender: Fixed as "other" (representing non-individual accounts) +# 5. mbti: MBTI type used to describe the account's style (e.g., ISTJ for rigorous/conservative) +# 6. country: Country (use Chinese, e.g., "中国") +# 7. profession: Description of organizational functions +# 8. interested_topics: An array of focused fields/areas of interest + +# IMPORTANT: +# - All field values must be strings or numbers; null values are not allowed. +# - 'persona' must be a coherent block of text description; do not use line breaks. +# - Use Chinese (except for the 'gender' field, which must be the English string "other"). +# - 'age' must be the integer 30; 'gender' must be the string "other". +# - The account's tone and discourse must strictly align with its institutional identity and positioning. +# """ - return f"""Tạo một thiết lập tài khoản mạng xã hội chi tiết cho tổ chức/nhóm, phản ánh tối đa tình hình thực tế. + return f"""Tạo thiết lập tài khoản mạng xã hội chi tiết cho thực thể tổ chức/nhóm, tái hiện tối đa các tình huống thực tế hiện có. Tên thực thể: {entity_name} Loại thực thể: {entity_type} @@ -744,30 +824,31 @@ Thuộc tính thực thể: {attrs_str} Thông tin ngữ cảnh: {context_str} -Vui lòng tạo JSON, bao gồm các trường sau: - -1. bio: Giới thiệu tài khoản chính thức, 200 chữ, chuyên nghiệp và đúng mực -2. persona: Mô tả chi tiết thiết lập tài khoản (văn bản thuần 2000 chữ), cần bao gồm: - - Thông tin cơ bản của tổ chức (Tên chính thức, Tính chất tổ chức, Bối cảnh thành lập, Chức năng chính) - - Định hướng tài khoản (Loại tài khoản, Đối tượng mục tiêu, Chức năng cốt lõi) - - Phong cách phát ngôn (Đặc điểm ngôn ngữ, Các biểu đạt thường dùng, Chủ đề cấm kỵ) - - Đặc điểm nội dung đăng tải (Loại nội dung, Tần suất đăng, Khung giờ hoạt động) - - Lập trường thái độ (Lập trường chính thức đối với chủ đề cốt lõi, Cách xử lý khi đối mặt với tranh cãi) - - Lưu ý đặc biệt (Chân dung nhóm đại diện, Thói quen vận hành) - - Ký ức tổ chức (Phần quan trọng của thiết lập tổ chức, cần giới thiệu rõ mối liên hệ giữa tổ chức này với sự kiện, cũng như các hành động và phản ứng của tổ chức trong sự kiện) -3. age: Điền cố định 30 (Tuổi ảo của tài khoản tổ chức) -4. gender: Điền cố định "other" (Tài khoản tổ chức sử dụng other để biểu thị tính phi cá nhân) -5. mbti: Loại MBTI, dùng để mô tả phong cách tài khoản, ví dụ ISTJ đại diện cho sự nghiêm ngặt, bảo thủ -6. country: Quốc gia (sử dụng tiếng Việt, ví dụ "Việt Nam") -7. profession: Mô tả chức năng tổ chức +Vui lòng tạo JSON bao gồm các trường sau: + +1. bio: Tiểu sử tài khoản chính thức, 200 ký tự, chuyên nghiệp và chuẩn mực. +2. persona: Mô tả chi tiết thiết lập tài khoản (văn bản thuần túy khoảng 2000 từ), cần bao gồm: + - Thông tin cơ bản về tổ chức (tên chính thức, tính chất tổ chức, bối cảnh thành lập, chức năng chính) + - Định vị tài khoản (loại tài khoản, đối tượng mục tiêu, chức năng cốt lõi) + - Phong cách phát ngôn (đặc điểm ngôn ngữ, biểu đạt thường dùng, các chủ đề cấm kỵ) + - Đặc điểm nội dung đăng tải (loại nội dung, tần suất đăng, khung giờ hoạt động) + - Lập trường thái độ (quan điểm chính thức về các chủ đề cốt lõi, cách xử lý tranh cãi) + - Ghi chú đặc biệt (hồ sơ của nhóm mà tổ chức đại diện, thói quen vận hành) + - Ký ức tổ chức (phần quan trọng của hồ sơ, giới thiệu mối liên hệ của tổ chức này với sự kiện, cũng như các hành động và phản ứng đã có của tổ chức trong sự kiện) +3. age: Cố định là 30 (tuổi ảo cho tài khoản tổ chức) +4. gender: Cố định là "other" (biểu thị tài khoản tổ chức, không phải cá nhân) +5. mbti: Loại MBTI dùng để mô tả phong cách tài khoản (ví dụ: ISTJ đại diện cho sự nghiêm túc, bảo thủ) +6. country: Quốc gia (sử dụng tiếng Trung, ví dụ: "中国") +7. profession: Mô tả chức năng của tổ chức 8. interested_topics: Mảng các lĩnh vực quan tâm - -Quan trọng: -- Tất cả giá trị các trường phải là chuỗi hoặc số, không cho phép giá trị null -- persona phải là một đoạn mô tả văn bản liên tục, không sử dụng ký tự xuống dòng (\n) -- Sử dụng tiếng Việt (ngoại trừ trường gender bắt buộc dùng tiếng Anh "other") -- age phải là số nguyên 30, gender phải là chuỗi "other" -- Phát ngôn của tài khoản tổ chức phải phù hợp với định vị danh tính của nó""" + +QUAN TRỌNG: +- Tất cả giá trị các trường phải là chuỗi hoặc số, không cho phép giá trị null. +- 'persona' phải là một đoạn mô tả văn bản mạch lạc, không sử dụng ký tự xuống dòng. +- Sử dụng tiếng Trung (ngoại trừ trường 'gender' phải dùng chuỗi tiếng Anh "other"). +- 'age' phải là số nguyên 30, 'gender' phải là chuỗi "other". +- Phát ngôn và giọng điệu của tài khoản phải phù hợp tuyệt đối với định vị danh tính và đặc thù của tổ chức. +""" def _generate_profile_rule_based( self, @@ -854,7 +935,9 @@ Quan trọng: graph_id: Optional[str] = None, parallel_count: int = 5, realtime_output_path: Optional[str] = None, - output_platform: str = "reddit" + output_platform: str = "reddit", + simulation_id: Optional[str] = None, + project_id: Optional[str] = None, ) -> List[OasisAgentProfile]: """ Khởi tạo hàng loạt các Agent Profile từ các thực thể (Hỗ trợ Gen đa luồng song song) @@ -877,6 +960,14 @@ Quan trọng: # Lưu Graph ID lại cho Zep xử lý search if graph_id: self.graph_id = graph_id + + self._runtime_metadata = { + "component": "oasis_profile_generator", + "phase": "generate_profiles", + "simulation_id": simulation_id, + "project_id": project_id, + "platform": output_platform, + } total = len(entities) profiles = [None] * total # Cấp trước 1 mảng để giữ đúng thứ tự Index diff --git a/backend/app/services/ontology_generator.py b/backend/app/services/ontology_generator.py index 3303ad1f..cc44a899 100644 --- a/backend/app/services/ontology_generator.py +++ b/backend/app/services/ontology_generator.py @@ -196,8 +196,7 @@ class OntologyGenerator: # Gửi request đến LLM result = self.llm_client.chat_json( messages=messages, - temperature=0.3, - max_tokens=4096 + temperature=0.3, ) # Kiểm tra tính hợp lệ và xử lý tinh chỉnh kết quả đầu ra @@ -206,7 +205,7 @@ class OntologyGenerator: return result # Định mức giới hạn độ dài ký tự tối đa của đoạn văn bản có thể gửi cho LLM (5 vạn chữ) - MAX_TEXT_LENGTH_FOR_LLM = 50000 + MAX_TEXT_LENGTH_FOR_LLM = 80000 def _build_user_message( self, diff --git a/backend/app/services/report_agent.py b/backend/app/services/report_agent.py index 687c0924..e8074195 100644 --- a/backend/app/services/report_agent.py +++ b/backend/app/services/report_agent.py @@ -472,389 +472,817 @@ class Report: # ── 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 = """\ -[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 +[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ệ +""" -[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 +# ═══════════════════════════════════════════════════════════════ +# 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 -[Returned Content] -- Relevant original facts (can be cited directly) -- Core entity insights -- Relationship chain analysis""" +# [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 = """\ -[Panorama Search - Get a Full View] -This tool is used to get a complete overview of the simulation results, especially suitable for understanding the evolution of an event. 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 evolves +[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 +""" -[Usage Scenarios] -- Need to understand the full development context of an event -- Need to compare public opinion changes across different stages -- Need comprehensive entity and relationship information +# ═══════════════════════════════════════════════════════════════ +# TOOL_DESC_QUICK_SEARCH = """\ +# [Quick Search - Fast Retrieval] +# A lightweight fast retrieval tool, suitable for simple, direct information queries. -[Returned Content] -- Current valid facts (latest simulation results) -- Historical/expired facts (evolution record) -- All involved entities""" +# [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 = """\ -[Quick Search - Fast Retrieval] -A lightweight fast retrieval tool, suitable for simple, direct information queries. +[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 +""" -[Usage Scenarios] -- Need to quickly look up a specific piece of information -- Need to verify a fact -- Simple information retrieval +# ═══════════════════════════════════════════════════════════════ +# 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. -[Returned Content] -- List of facts most relevant to the query""" +# 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 = """\ -[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, it interviews simultaneously on Twitter and Reddit to get a more comprehensive perspective. +[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! +""" -Functional Flow: -1. Automatically reads persona files to understand all simulation Agents -2. Smartly selects Agents most relevant to the interview topic (e.g., student, media, official) -3. Automatically generates interview questions -4. Calls the /api/simulation/interview/batch endpoint for real interviews on dual platforms -5. Integrates all interview results, providing multi-perspective analysis +# ═══════════════════════════════════════════════════════════════ +# 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. -[Usage Scenarios] -- Need to understand views on an event from different role perspectives (What do students think? Media? Officials?) -- Need to collect multiple opinions and stances -- Need to get the simulation Agent's real answer (from the Oasis simulation environment) -- Want to make the report more vivid, including "interview transcripts" +# [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". -[Returned Content] -- Identity info of the interviewed Agents -- Each Agent's interview answers on both Twitter and Reddit -- Key quotes (can be cited directly) -- Interview summary and perspective comparison +# [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? -[IMPORTANT] The Oasis simulation environment MUST be running to use this feature!""" +# [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 -# ── Prompt hoạch định dàn ý ── +# [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 = """\ -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. +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ử! +""" -[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". +# PLAN_USER_PROMPT_TEMPLATE = """\ +# [Prediction Scenario Setting] +# The variables (simulation requirements) we injected into the simulated world: {simulation_requirement} -[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? +# [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} -[Report Positioning] -- ✅ This is a simulation-based future prediction report, revealing "if this, what will the future be like" -- ✅ Focus on prediction results: event direction, 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 real world's current status -- ❌ Not a general public opinion overview +# [Sample Future Facts Predicted by Simulation] +# {related_facts_json} -[Chapter Quantity Limit] -- Minimum of 2 chapters, maximum of 5 chapters -- No sub-chapters needed, write complete content directly for each chapter -- Content must be concise, focused on core prediction findings -- Chapter structure should be designed by you independently based on prediction results +# 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? -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" - } - ] -} +# Based on the prediction results, design the most suitable report chapter structure. -Note: The sections array must have a minimum of 2 and a maximum of 5 elements!""" +# [Reminder again] Report chapter quantity: Minimum 2, maximum 5, content should be concise and focused on core prediction findings. +# """ PLAN_USER_PROMPT_TEMPLATE = """\ -[Prediction Scenario Context] -Variables (simulation requirements) we injected into the simulated world: {simulation_requirement} +[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. +""" -[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} +# ═══════════════════════════════════════════════════════════════ +# SECTION_SYSTEM_PROMPT_TEMPLATE = """\ +# You are a writing expert for "Future Prediction Reports", currently writing one section of the report. -[Sample of some future facts predicted by the simulation] -{related_facts_json} +# Report Title: {report_title} +# Report Summary: {report_summary} +# Prediction Scenario (Simulation Requirement): {simulation_requirement} -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? +# Section currently being written: {section_title} -Based on the prediction results, design the most suitable report chapter structure. +# ═══════════════════════════════════════════════════════════════ +# [Core Concept] +# ═══════════════════════════════════════════════════════════════ -[Reminder] Report chapter quantity: Minimum 2, maximum 5, content should be concise and focused on core prediction findings.""" +# 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. -# ── Prompt tạo chương ── +# 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: +# +# {{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}} +# +# 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 = """\ -You are a writing expert for "Future Prediction Reports", currently writing one section of the report. +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 đó. -Report Title: {report_title} -Report Summary: {report_summary} -Prediction Scenario (Simulation Requirement): {simulation_requirement} +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} -Section currently being written: {section_title} +Phần đang được viết: {section_title} ═══════════════════════════════════════════════════════════════ -[Core Concept] +[Khái niệm Cốt lõi] ═══════════════════════════════════════════════════════════════ -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. +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. -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 +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 -❌ 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 +❌ 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 ═══════════════════════════════════════════════════════════════ -[Most Important Rules - MUST Obey] +[Quy tắc QUAN TRỌNG NHẤT - PHẢI Tuân thủ] ═══════════════════════════════════════════════════════════════ -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 +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. [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 +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. [Language Consistency - Quoted Content Must Be Translated to Report Language] - - The content returned by the tools may contain English or mixed Chinese and English expressions - - If the simulation requirements and original materials are in Chinese, the report must be written entirely in Chinese - - When you quote English or mixed content returned by the tool, you must translate it into fluent Chinese 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) +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. [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 +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 ═══════════════════════════════════════════════════════════════ -[⚠️ Formatting Specifications - Extremely Important!] +[⚠️ Quy cách Định dạng - Cực kỳ Quan trọng!] ═══════════════════════════════════════════════════════════════ -[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 +[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) -[Correct Example] +[Ví dụ Đúng] ``` -This chapter analyzes the public opinion dissemination trend of the event. Through deep analysis of simulation data, we found... +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... -**Initial Outbreak Stage** +**Giai đoạn bùng phát ban đầu** -Weibo, as the first scene of public opinion, assumed the core function of initial information release: +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 contributed 68% of the initial buzz..." +> "Weibo đã đóng góp 68% mức độ thảo luận ban đầu..." -**Emotion Amplification Stage** +**Giai đoạn khuếch đại cảm xúc** -The Douyin platform further amplified the event's impact: +Nền tảng Douyin đã khuếch đại thêm tác động của sự kiện: -- Strong visual impact -- High emotional resonance +- Tác động thị giác mạnh mẽ +- Cộng hưởng cảm xúc cao ``` -[Incorrect Example] +[Ví dụ Sai] ``` -## 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 +## 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 -This chapter analyzes... +Chương này phân tích... ``` ═══════════════════════════════════════════════════════════════ -[Available Retrieval Tools] (Call 3-5 times per section) +[Các công cụ truy xuất hiện có] (Gọi 3-5 lần mỗi phần) ═══════════════════════════════════════════════════════════════ {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 +[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 ═══════════════════════════════════════════════════════════════ -[Workflow] +[Quy trình làm việc] ═══════════════════════════════════════════════════════════════ -For each reply you can only do one of the following two things (not both simultaneously): +Đố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): -Option A - Call a tool: -Output your thoughts, then use the following format to call a tool: +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ụ: -{{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}} +{{"name": "Tên công cụ", "parameters": {{"Tên tham số": "Giá trị tham số"}}}} -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. +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ụ. -Option B - Output Final Content: -When you have obtained enough information through tools, output the chapter content starting with "Final Answer:". +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:". -⚠️ 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 +⚠️ 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 ═══════════════════════════════════════════════════════════════ -[Chapter Content Requirements] +[Yêu cầu Nội dung Chương] ═══════════════════════════════════════════════════════════════ -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: +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: - ✅ Correct format: + ✅ Định dạng đúng: ``` - The school's response was considered to lack substantive content. + Phản ứng của nhà trường bị coi là thiếu nội dung thực chất. - > "The school's response model appears rigid and slow in the rapidly changing social media environment." + > "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." - This evaluation reflects the general dissatisfaction of the public. + Đánh giá này phản ánh sự không hài lòng chung của công chúng. ``` - ❌ Incorrect format: + ❌ Định dạng sai: ``` - The school's response was considered to lack substantive content. > "The school's response model..." This evaluation reflects... + 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. 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""" +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 = """\ -Completed Chapter Content (Please read carefully to avoid duplication): +Nội dung Chương đã Hoàn thành (Vui lòng đọc kỹ để tránh trùng lặp): {previous_content} ═══════════════════════════════════════════════════════════════ -[Current Task] Writing Chapter: {section_title} +[Nhiệm vụ Hiện tại] Viết Chương: {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 +[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. -[⚠️ 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 +[⚠️ 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. -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)""" +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 Message Templates ── +# ═══════════════════════════════════════════════════════════════ +# 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 = """\ -Observation (Retrieval Result): +Quan sát (Kết quả Truy xuất): -═══ Tool {tool_name} Returned ═══ +═══ Công cụ {tool_name} đã trả về ═══ {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 -═══════════════════════════════════════════════════════════════""" +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 = ( - "[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}" + "[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 = ( - "Currently tool called {tool_calls_count} times, at least {min_tool_calls} times are needed. " - "Please call tools to fetch simulation data. {unused_hint}" + "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 = ( - "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.' + "Đã đạ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💡 You haven't used: {unused_list}, suggesting trying different tools for multiple perspectives" -REACT_FORCE_FINAL_MSG = "Tool call limit reached, please output Final Answer: and generate section content directly." +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" -# ── Chat prompt ── +# ═══════════════════════════════════════════════════════════════ +# REACT_FORCE_FINAL_MSG = "Tool call limit reached, please output Final Answer: and generate section content directly." -CHAT_SYSTEM_PROMPT_TEMPLATE = """\ -You are a concise and efficient simulation prediction assistant. +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." -[Background] -Prediction condition: {simulation_requirement} +# ═══════════════════════════════════════════════════════════════ +# CHAT_SYSTEM_PROMPT_TEMPLATE = """\ +# You are a concise and efficient simulation prediction assistant. -[Generated Analysis Report] +# [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] +# +# {{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}} +# + +# [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} -[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 +[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. -[Available Tools] (Use only when necessary, call 1-2 times max) +[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} -[Tool Call Format] +[Định dạng Gọi Công cụ] -{{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}} +{{"name": "Tên Công cụ", "parameters": {{"Tên Tham số": "Giá trị Tham số"}}}} -[Answering Style] -- Concise and direct, avoid long paragraphs -- Use > format to quote key content -- Provide conclusion first, then explain the reason""" +[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\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 @@ -884,6 +1312,7 @@ class ReportAgent: self, graph_id: str, simulation_id: str, + project_id: str, simulation_requirement: str, llm_client: Optional[LLMClient] = None, zep_tools: Optional[ZepToolsService] = None @@ -900,10 +1329,27 @@ class ReportAgent: """ 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() - self.zep_tools = zep_tools or ZepToolsService() + 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() @@ -913,7 +1359,9 @@ class ReportAgent: # 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}") + 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""" @@ -1303,7 +1751,7 @@ class ReportAgent: response = self.llm.chat( messages=messages, temperature=0.5, - max_tokens=4096 + # 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) @@ -1506,7 +1954,7 @@ class ReportAgent: response = self.llm.chat( messages=messages, temperature=0.5, - max_tokens=4096 + # max_tokens=4096 ) # Kiểm tra nếu ép buộc kết thúc mà LLM vẫn nhả None @@ -1561,6 +2009,19 @@ class ReportAgent: 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, diff --git a/backend/app/services/simulation_config_generator.py b/backend/app/services/simulation_config_generator.py index 926ff1b4..3b794969 100644 --- a/backend/app/services/simulation_config_generator.py +++ b/backend/app/services/simulation_config_generator.py @@ -20,6 +20,7 @@ from openai import OpenAI from ..config import Config from ..utils.logger import get_logger +from ..utils.llm_cost import create_tracked_chat_completion from .zep_entity_reader import EntityNode, ZepEntityReader logger = get_logger('mirofish.simulation_config') @@ -238,6 +239,7 @@ class SimulationConfigGenerator: api_key=self.api_key, base_url=self.base_url ) + self._runtime_metadata: Dict[str, Any] = {} def generate_config( self, @@ -269,6 +271,12 @@ class SimulationConfigGenerator: SimulationParameters: Bộ tổng cấu hình thông số đầy đủ """ logger.info(f"Start generating simulation configuration: simulation_id={simulation_id}, entity_count={len(entities)}") + self._runtime_metadata = { + "simulation_id": simulation_id, + "project_id": project_id, + "component": "simulation_config_generator", + "phase": "prepare_simulation_config", + } # Tính toán tổng số bước num_batches = math.ceil(len(entities) / self.AGENTS_PER_BATCH) @@ -438,15 +446,16 @@ class SimulationConfigGenerator: for attempt in range(max_attempts): try: - response = self.client.chat.completions.create( + response = create_tracked_chat_completion( + client=self.client, model=self.model_name, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ], response_format={"type": "json_object"}, - temperature=0.7 - (attempt * 0.1) # Giảm temperature cho mỗi lần retry - # Không đặt max_tokens, cho AI sáng tạo tự do tối đa + temperature=0.7 - (attempt * 0.1), # Giảm temperature cho mỗi lần retry + metadata=self._runtime_metadata, ) content = response.choices[0].message.content diff --git a/backend/app/services/simulation_manager.py b/backend/app/services/simulation_manager.py index d9d7b022..8d6b78c3 100644 --- a/backend/app/services/simulation_manager.py +++ b/backend/app/services/simulation_manager.py @@ -342,7 +342,9 @@ class SimulationManager: graph_id=state.graph_id, # Để tìm kiếm Zep Search Index parallel_count=parallel_profile_count, # Số dòng luồng Async realtime_output_path=realtime_output_path, # Lưu log thời gian thực - output_platform=realtime_platform # Đuôi file xuất + output_platform=realtime_platform, # Đuôi file xuất + simulation_id=simulation_id, + project_id=state.project_id, ) state.profiles_count = len(profiles) diff --git a/backend/app/services/zep_tools.py b/backend/app/services/zep_tools.py index 95c1ec47..d5230d2f 100644 --- a/backend/app/services/zep_tools.py +++ b/backend/app/services/zep_tools.py @@ -436,7 +436,12 @@ class ZepToolsService: def llm(self) -> LLMClient: """Khởi tạo muộn (lazy init) cho LLM client""" if self._llm_client is None: - self._llm_client = LLMClient() + self._llm_client = LLMClient( + component="zep_tools", + metadata={ + "phase": "zep_tools", + }, + ) return self._llm_client def _call_with_retry(self, func, operation_name: str, max_retries: int = None): diff --git a/backend/app/utils/file_parser.py b/backend/app/utils/file_parser.py index f486fd4e..028d772b 100644 --- a/backend/app/utils/file_parser.py +++ b/backend/app/utils/file_parser.py @@ -84,6 +84,7 @@ class FileParser: if suffix not in cls.SUPPORTED_EXTENSIONS: raise ValueError(f"Unsupported file format: {suffix}") + # Format Support: Leverages the existing extract_text method which supports PDF, Markdown, and TXT formats if suffix == '.pdf': return cls._extract_from_pdf(file_path) elif suffix in {'.md', '.markdown'}: @@ -123,21 +124,27 @@ class FileParser: @classmethod def extract_from_multiple(cls, file_paths: List[str]) -> str: """ - Trích xuất văn bản từ nhiều tệp và gộp lại + Usage Context + This function is typically used in the early stages of the GraphRAG pipeline when users upload multiple documents that need to be processed together for entity extraction and relationship mapping. The combined output serves as input for text chunking and subsequent LLM-based analysis in the knowledge graph construction workflow + Trích xuất văn bản từ nhiều tệp và gộp lại Args: file_paths: Danh sách đường dẫn tệp Returns: Văn bản đã gộp + + The extract_from_multiple method is a class method of the FileParser class that processes multiple document files simultaneously. It's designed to aggregate content from various source files (PDF, Markdown, TXT) into a single text string that can be fed into the knowledge graph construction pipeline. """ all_texts = [] + # Batch Processing: Takes a list of file paths and processes each one sequentially for i, file_path in enumerate(file_paths, 1): try: text = cls.extract_text(file_path) filename = Path(file_path).name all_texts.append(f"=== Document {i}: {filename} ===\n{text}") + # Error Handling: If a file fails to extract, it includes an error message in the output rather than failing completely except Exception as e: all_texts.append(f"=== Document {i}: {file_path} (extract failed: {str(e)}) ===") diff --git a/backend/app/utils/llm_client.py b/backend/app/utils/llm_client.py index faa60294..987959c5 100644 --- a/backend/app/utils/llm_client.py +++ b/backend/app/utils/llm_client.py @@ -9,6 +9,7 @@ from typing import Optional, Dict, Any, List from openai import OpenAI from ..config import Config +from .llm_cost import create_tracked_chat_completion class LLMClient: @@ -18,11 +19,15 @@ class LLMClient: self, api_key: Optional[str] = None, base_url: Optional[str] = None, - model: Optional[str] = None + model: Optional[str] = None, + component: str = "llm_client", + metadata: Optional[Dict[str, Any]] = None, ): self.api_key = api_key or Config.LLM_API_KEY self.base_url = base_url or Config.LLM_BASE_URL self.model = model or Config.LLM_MODEL_NAME + self.component = component + self.default_metadata = metadata or {} if not self.api_key: raise ValueError("LLM_API_KEY is not configured") @@ -37,7 +42,8 @@ class LLMClient: messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 4096, - response_format: Optional[Dict] = None + response_format: Optional[Dict] = None, + metadata: Optional[Dict[str, Any]] = None, ) -> str: """ Gửi yêu cầu chat @@ -60,8 +66,19 @@ class LLMClient: if response_format: kwargs["response_format"] = response_format + + call_metadata = dict(self.default_metadata) + if metadata: + call_metadata.update(metadata) + call_metadata.setdefault("component", self.component) - response = self.client.chat.completions.create(**kwargs) + response = create_tracked_chat_completion( + client=self.client, + model=self.model, + messages=messages, + metadata=call_metadata, + **{k: v for k, v in kwargs.items() if k not in {"model", "messages"}}, + ) content = response.choices[0].message.content # Một số model (vd MiniMax M2.5) chèn nội dung vào content, cần loại bỏ content = re.sub(r'[\s\S]*?', '', content).strip() @@ -71,7 +88,8 @@ class LLMClient: self, messages: List[Dict[str, str]], temperature: float = 0.3, - max_tokens: int = 4096 + max_tokens: int = 4096, + metadata: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """ Gửi yêu cầu chat và trả về JSON @@ -88,7 +106,8 @@ class LLMClient: messages=messages, temperature=temperature, max_tokens=max_tokens, - response_format={"type": "json_object"} + response_format={"type": "json_object"}, + metadata=metadata, ) # Làm sạch markdown code fence cleaned_response = response.strip() diff --git a/backend/app/utils/llm_cost.py b/backend/app/utils/llm_cost.py new file mode 100644 index 00000000..6b2e14dc --- /dev/null +++ b/backend/app/utils/llm_cost.py @@ -0,0 +1,218 @@ +""" +Centralized LLM cost tracking utility. + +Responsibilities: +1. Calculate token cost per model. +2. Persist cost logs to logs/{project_id}/cost_{model_name}.log. +3. Persist structured JSONL records to logs/{project_id}/cost_{model_name}.jsonl. +4. Provide a single wrapped OpenAI chat-completion call so all components can reuse + one cost-accounting flow. +""" + +from __future__ import annotations + +import json +import os +import re +import threading +from datetime import datetime +from decimal import Decimal, ROUND_HALF_UP +from typing import Any, Dict, Optional + + +MODEL_COSTS_PER_1M_TOKENS: Dict[str, Dict[str, float]] = { + "Qwen/Qwen3.5-27B": {"input": 0.5, "output": 3.0}, + "gemini-3-flash": {"input": 0.5, "output": 3.0}, + "gemini-3.1-flash-lite": {"input": 0.25, "output": 1.5}, +} + + +_COUNTER_LOCK = threading.Lock() +_COUNTERS: Dict[str, Dict[str, Decimal]] = {} + + +def _decimal(value: Any) -> Decimal: + return Decimal(str(value)) + + +def _quantize_8(value: Decimal) -> Decimal: + return value.quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) + + +def _safe_model_name(model_name: str) -> str: + if not model_name: + return "unknown_model" + safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name) + return safe or "unknown_model" + + +def _resolve_project_id(metadata: Optional[Dict[str, Any]]) -> str: + metadata = metadata or {} + value = metadata.get("project_id") + if value is not None and str(value).strip(): + return str(value).strip() + return "global" + + +def _resolve_component(metadata: Optional[Dict[str, Any]]) -> str: + metadata = metadata or {} + component = metadata.get("component") + if component is None: + return "unknown_component" + component = str(component).strip() + return component if component else "unknown_component" + + +def _normalize_usage(usage: Any) -> Dict[str, int]: + if usage is None: + return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0} + + prompt_tokens = int(getattr(usage, "prompt_tokens", 0) or 0) + completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0) + total_tokens = int(getattr(usage, "total_tokens", prompt_tokens + completion_tokens) or 0) + + return { + "input_tokens": prompt_tokens, + "output_tokens": completion_tokens, + "total_tokens": total_tokens, + } + + +def _resolve_model_rates(model_name: str) -> Dict[str, Decimal]: + env_in = os.environ.get("LLM_COST_INPUT_PER_1M") + env_out = os.environ.get("LLM_COST_OUTPUT_PER_1M") + if env_in is not None and env_out is not None: + return {"input": _decimal(env_in), "output": _decimal(env_out)} + + rates = MODEL_COSTS_PER_1M_TOKENS.get(model_name, {"input": 0.0, "output": 0.0}) + return {"input": _decimal(rates.get("input", 0.0)), "output": _decimal(rates.get("output", 0.0))} + + +def _get_logs_root() -> str: + # backend/app/utils -> backend/logs + return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") + + +def _ensure_project_log_dir(project_id: str) -> str: + root = _get_logs_root() + project_dir = os.path.join(root, project_id) + os.makedirs(project_dir, exist_ok=True) + return project_dir + + +def _build_log_paths(project_id: str, model_name: str) -> Dict[str, str]: + project_dir = _ensure_project_log_dir(project_id) + model_safe = _safe_model_name(model_name) + return { + "log": os.path.join(project_dir, f"cost_{model_safe}.log"), + "jsonl": os.path.join(project_dir, f"cost_{model_safe}.jsonl"), + } + + +def _update_counters(counter_key: str, request_cost: Decimal) -> Dict[str, Decimal]: + with _COUNTER_LOCK: + current = _COUNTERS.get(counter_key) + if current is None: + current = { + "requests": Decimal("0"), + "total_cost": Decimal("0"), + } + current["requests"] += Decimal("1") + current["total_cost"] += request_cost + _COUNTERS[counter_key] = current + return { + "requests": current["requests"], + "total_cost": current["total_cost"], + } + + +def record_llm_cost( + *, + model_name: str, + usage: Any, + metadata: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + Calculate and persist LLM usage cost for one request. + + Returns a normalized cost payload that is also written into JSONL. + """ + metadata = metadata or {} + timestamp = datetime.now().isoformat() + project_id = _resolve_project_id(metadata) + component = _resolve_component(metadata) + usage_dict = _normalize_usage(usage) + + rates = _resolve_model_rates(model_name) + input_cost = _quantize_8(_decimal(usage_dict["input_tokens"]) * rates["input"] / _decimal(1_000_000)) + output_cost = _quantize_8(_decimal(usage_dict["output_tokens"]) * rates["output"] / _decimal(1_000_000)) + total_cost = _quantize_8(input_cost + output_cost) + + record = { + "timestamp": timestamp, + "model": model_name, + "input_tokens": usage_dict["input_tokens"], + "output_tokens": usage_dict["output_tokens"], + "total_tokens": usage_dict["total_tokens"], + "input_cost_usd": float(input_cost), + "output_cost_usd": float(output_cost), + "total_cost_usd": float(total_cost), + "metadata": { + "component": component, + "simulation_id": metadata.get("simulation_id"), + "platform": metadata.get("platform"), + "phase": metadata.get("phase"), + "project_id": project_id, + "report_id": metadata.get("report_id"), + }, + } + + paths = _build_log_paths(project_id=project_id, model_name=model_name) + counter_key = f"{project_id}::{model_name}" + counter = _update_counters(counter_key, total_cost) + request_no = int(counter["requests"]) + cumulative_cost = _quantize_8(counter["total_cost"]) + + line = ( + f"[{timestamp}] [Request {request_no}] [{component}] " + f"Called model: {model_name}, " + f"input_tokens: {usage_dict['input_tokens']} | " + f"output_tokens: {usage_dict['output_tokens']} | " + f"total_tokens: {usage_dict['total_tokens']} | " + f"input_cost_usd: {float(input_cost):.8f} | " + f"output_cost_usd: {float(output_cost):.8f} | " + f"total_cost_usd: {float(total_cost):.8f} | " + f"cumulative_total_cost_usd: {float(cumulative_cost):.8f}" + ) + + with open(paths["log"], "a", encoding="utf-8") as log_f: + log_f.write(line + "\n") + + with open(paths["jsonl"], "a", encoding="utf-8") as jsonl_f: + jsonl_f.write(json.dumps(record, ensure_ascii=False) + "\n") + + return record + + +def create_tracked_chat_completion( + *, + client: Any, + model: str, + messages: Any, + metadata: Optional[Dict[str, Any]] = None, + **kwargs: Any, +) -> Any: + """ + Single entry-point for OpenAI-compatible chat completion + cost logging. + + All modules should call this wrapper instead of calling + client.chat.completions.create directly. + """ + response = client.chat.completions.create( + model=model, + messages=messages, + **kwargs, + ) + record_llm_cost(model_name=model, usage=getattr(response, "usage", None), metadata=metadata) + return response + diff --git a/backend/scripts/llm_cost_patch.py b/backend/scripts/llm_cost_patch.py new file mode 100644 index 00000000..b02cde31 --- /dev/null +++ b/backend/scripts/llm_cost_patch.py @@ -0,0 +1,87 @@ +"""Patch OpenAI chat completion calls in simulation scripts for centralized cost logging.""" + +from __future__ import annotations + +import copy +from typing import Any, Dict, Optional + +from app.utils.llm_cost import record_llm_cost + + +_PATCHED = False +_PATCH_CONTEXT: Dict[str, Any] = {} + + +def _build_metadata(model: str) -> Dict[str, Any]: + metadata = copy.deepcopy(_PATCH_CONTEXT) + metadata.setdefault("component", "scripts.simulation") + metadata.setdefault("phase", "simulation_run") + metadata.setdefault("model", model) + return metadata + + +def install_openai_cost_patch( + *, + simulation_id: Optional[str], + project_id: Optional[str], + platform: str, + component: str, + phase: str = "simulation_run", +) -> None: + """ + Install monkey-patch for OpenAI SDK calls used indirectly by CAMEL/OASIS. + + The patch is process-scoped and idempotent. + """ + global _PATCHED + global _PATCH_CONTEXT + + _PATCH_CONTEXT = { + "simulation_id": simulation_id, + "project_id": project_id, + "platform": platform, + "component": component, + "phase": phase, + } + + if _PATCHED: + return + + try: + from openai.resources.chat.completions.completions import Completions, AsyncCompletions + except Exception as exc: # pragma: no cover + print(f"[llm_cost_patch] Failed to import OpenAI completion classes: {exc}") + return + + original_sync_create = Completions.create + original_async_create = AsyncCompletions.create + + def sync_create_wrapper(self, *args, **kwargs): + model = kwargs.get("model", "unknown_model") + response = original_sync_create(self, *args, **kwargs) + try: + record_llm_cost( + model_name=model, + usage=getattr(response, "usage", None), + metadata=_build_metadata(model), + ) + except Exception as exc: + print(f"[llm_cost_patch] Failed to record sync cost: {exc}") + return response + + async def async_create_wrapper(self, *args, **kwargs): + model = kwargs.get("model", "unknown_model") + response = await original_async_create(self, *args, **kwargs) + try: + record_llm_cost( + model_name=model, + usage=getattr(response, "usage", None), + metadata=_build_metadata(model), + ) + except Exception as exc: + print(f"[llm_cost_patch] Failed to record async cost: {exc}") + return response + + Completions.create = sync_create_wrapper + AsyncCompletions.create = async_create_wrapper + _PATCHED = True diff --git a/backend/scripts/run_parallel_simulation.py b/backend/scripts/run_parallel_simulation.py index 4ccde2d5..689fc103 100644 --- a/backend/scripts/run_parallel_simulation.py +++ b/backend/scripts/run_parallel_simulation.py @@ -156,6 +156,7 @@ def init_logging_for_simulation(simulation_dir: str): from action_logger import SimulationLogManager, PlatformActionLogger +from llm_cost_patch import install_openai_cost_patch try: from camel.models import ModelFactory @@ -1533,6 +1534,14 @@ async def main(): config = load_config(args.config) simulation_dir = os.path.dirname(args.config) or "." wait_for_commands = not args.no_wait + + install_openai_cost_patch( + simulation_id=config.get("simulation_id"), + project_id=config.get("project_id"), + platform="parallel", + component="scripts.run_parallel_simulation", + phase="simulation_run", + ) # Khởi tạo cấu hình log (tắt log OASIS, dọn file cũ) init_logging_for_simulation(simulation_dir) diff --git a/backend/scripts/run_reddit_simulation.py b/backend/scripts/run_reddit_simulation.py index cc063616..897a75fb 100644 --- a/backend/scripts/run_reddit_simulation.py +++ b/backend/scripts/run_reddit_simulation.py @@ -48,6 +48,7 @@ else: import re +from llm_cost_patch import install_openai_cost_patch class UnicodeFormatter(logging.Formatter): @@ -731,6 +732,15 @@ async def main(): config_path=args.config, wait_for_commands=not args.no_wait ) + + install_openai_cost_patch( + simulation_id=runner.config.get("simulation_id"), + project_id=runner.config.get("project_id"), + platform="reddit", + component="scripts.run_reddit_simulation", + phase="simulation_run", + ) + await runner.run(max_rounds=args.max_rounds) diff --git a/backend/scripts/run_twitter_simulation.py b/backend/scripts/run_twitter_simulation.py index aa3cec53..244dcac5 100644 --- a/backend/scripts/run_twitter_simulation.py +++ b/backend/scripts/run_twitter_simulation.py @@ -48,6 +48,7 @@ else: import re +from llm_cost_patch import install_openai_cost_patch class UnicodeFormatter(logging.Formatter): @@ -743,6 +744,15 @@ async def main(): config_path=args.config, wait_for_commands=not args.no_wait ) + + install_openai_cost_patch( + simulation_id=runner.config.get("simulation_id"), + project_id=runner.config.get("project_id"), + platform="twitter", + component="scripts.run_twitter_simulation", + phase="simulation_run", + ) + await runner.run(max_rounds=args.max_rounds)