Merge pull request #4 from TQuynh109/feature/modified-prompts
Feature/modified prompts
This commit is contained in:
commit
16106a4a00
|
|
@ -57,4 +57,5 @@ backend/logs/
|
|||
backend/uploads/
|
||||
|
||||
# Dữ liệu Docker
|
||||
data/
|
||||
data/
|
||||
uploads_old/
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -4,6 +4,7 @@ API 2: Sử dụng Zep API để xây dựng một Standalone Graph (Đồ thị
|
|||
"""
|
||||
|
||||
import os
|
||||
import re # changed
|
||||
import uuid
|
||||
import time
|
||||
import threading
|
||||
|
|
@ -330,27 +331,47 @@ 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
|
||||
)
|
||||
|
||||
# Cập nhật và thu thập lại UUID của các Episode được trả về sau khi tạo mới
|
||||
if batch_result and isinstance(batch_result, list):
|
||||
for ep in batch_result:
|
||||
ep_uuid = getattr(ep, 'uuid_', None) or getattr(ep, 'uuid', None)
|
||||
if ep_uuid:
|
||||
episode_uuids.append(ep_uuid)
|
||||
|
||||
# Cài thời gian chờ (delay) nhỏ để tránh rate-limit bị quá tải số lượng requests
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
if progress_callback:
|
||||
progress_callback(f"Failed to send batch {batch_num}: {str(e)}", 0)
|
||||
raise
|
||||
max_retries = 10 # changed
|
||||
for attempt in range(max_retries): # changed
|
||||
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
|
||||
)
|
||||
|
||||
# Cập nhật và thu thập lại UUID của các Episode được trả về sau khi tạo mới
|
||||
if batch_result and isinstance(batch_result, list):
|
||||
for ep in batch_result:
|
||||
ep_uuid = getattr(ep, 'uuid_', None) or getattr(ep, 'uuid', None)
|
||||
if ep_uuid:
|
||||
episode_uuids.append(ep_uuid)
|
||||
|
||||
# Cài thời gian chờ (delay) nhỏ để tránh rate-limit bị quá tải số lượng requests
|
||||
time.sleep(3)
|
||||
break # changed: thoát retry loop nếu thành công
|
||||
|
||||
except Exception as e: # changed
|
||||
err_str = str(e)
|
||||
if "episode usage limit" in err_str or ("status_code: 429" in err_str): # changed: bắt lỗi rate-limit
|
||||
# Đọc thời điểm reset từ error message để biết cần chờ bao lâu
|
||||
reset_match = re.search(r"x-ratelimit-reset['\"]:\s*['\"]?(\d+)", err_str) # changed
|
||||
if reset_match: # changed
|
||||
wait_seconds = max(int(reset_match.group(1)) - int(time.time()) + 2, 5) # changed
|
||||
else: # changed
|
||||
wait_seconds = 20 # changed: fallback 12s (5 calls/phút → cách nhau 12s)
|
||||
if progress_callback: # changed
|
||||
progress_callback( # changed
|
||||
f"Rate limited by Zep. Waiting {wait_seconds}s then retry (attempt {attempt + 1}/{max_retries})...", # changed
|
||||
(i + len(batch_chunks)) / total_chunks # changed
|
||||
) # changed
|
||||
time.sleep(wait_seconds) # changed
|
||||
else: # changed: lỗi khác thì raise luôn, không retry
|
||||
if progress_callback:
|
||||
progress_callback(f"Failed to send batch {batch_num}: {err_str}", 0)
|
||||
raise # changed
|
||||
else: # changed: for...else — chạy khi hết max_retries mà vẫn chưa break
|
||||
raise Exception(f"Batch {batch_num} failed after {max_retries} retries due to rate limiting") # changed
|
||||
|
||||
return episode_uuids
|
||||
|
||||
|
|
@ -358,7 +379,7 @@ class GraphBuilderService:
|
|||
self,
|
||||
episode_uuids: List[str],
|
||||
progress_callback: Optional[Callable] = None,
|
||||
timeout: int = 600
|
||||
timeout: int = 1000
|
||||
):
|
||||
"""Chạy vòng lặp để kiểm tra và chờ cho tới khi mọi Episode (các khối Text) đều hoàn tất quá trình process từ hệ thống"""
|
||||
if not episode_uuids:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,32 +9,145 @@ from ..utils.llm_client import LLMClient
|
|||
|
||||
|
||||
# System prompt dùng cho việc tự động sinh Ontology
|
||||
ONTOLOGY_SYSTEM_PROMPT = """Bạn là một chuyên gia thiết kế bản thể học (Ontology) cho Tri thức đồ thị (Knowledge Graph). Nhiệm vụ của bạn là phân tích nội dung văn bản được cung cấp và nhu cầu để thiết kế các loại thực thể (Entity) và loại mối quan hệ (Relationship) thiết kế phù hợp cho **Mô phỏng dư luận trên mạng xã hội**.
|
||||
# ONTOLOGY_SYSTEM_PROMPT = """You are a professional Knowledge Graph Ontology Design Expert. Your task is to analyze the given text content and simulation requirements to design entity types and relationship types suitable for **social media public opinion simulation**.
|
||||
|
||||
**QUAN TRỌNG: Bạn BẮT BUỘC phải đầu ra một cấu trúc định dạng JSON hợp lệ, KHÔNG ĐƯỢC xuất thêm bất kỳ văn bản nào khác.**
|
||||
# **IMPORTANT: You must output valid JSON format data only. Do not include any other text.**
|
||||
|
||||
# ## Core Task Background
|
||||
|
||||
# We are building a social media public opinion simulation system. In this system:
|
||||
# - Each entity is an "account" or "subject" capable of speaking, interacting, and spreading information on social media.
|
||||
# - Entities influence, forward, comment on, and respond to each other.
|
||||
# - We need to simulate the reactions of all parties and the paths of information dissemination during public opinion events.
|
||||
|
||||
# Therefore, **entities must be real-world subjects capable of speaking and interacting on social media**:
|
||||
|
||||
# **CAN BE**:
|
||||
# - Specific individuals (public figures, parties involved, opinion leaders, experts, ordinary people).
|
||||
# - Companies and enterprises (including their official accounts).
|
||||
# - Organizations (universities, associations, NGOs, labor unions, etc.).
|
||||
# - Government departments and regulatory agencies.
|
||||
# - Media outlets (newspapers, TV stations, independent media, websites).
|
||||
# - Social media platforms themselves.
|
||||
# - Representatives of specific groups (e.g., alumni associations, fan clubs, rights protection groups).
|
||||
|
||||
# **CANNOT BE**:
|
||||
# - Abstract concepts (e.g., "public opinion", "emotion", "trend").
|
||||
# - Themes/Topics (e.g., "academic integrity", "education reform").
|
||||
# - Viewpoints/Attitudes (e.g., "supporters", "opponents").
|
||||
|
||||
# ## Output Format
|
||||
|
||||
# Please output in JSON format with the following structure:
|
||||
|
||||
# ```json
|
||||
# {
|
||||
# "entity_types": [
|
||||
# {
|
||||
# "name": "Entity type name (English, PascalCase)",
|
||||
# "description": "Short description (English, max 100 characters)",
|
||||
# "attributes": [
|
||||
# {
|
||||
# "name": "Attribute name (English, snake_case)",
|
||||
# "type": "text",
|
||||
# "description": "Attribute description"
|
||||
# }
|
||||
# ],
|
||||
# "examples": ["Example Entity 1", "Example Entity 2"]
|
||||
# }
|
||||
# ],
|
||||
# "edge_types": [
|
||||
# {
|
||||
# "name": "Relationship type name (English, UPPER_SNAKE_CASE)",
|
||||
# "description": "Short description (English, max 100 characters)",
|
||||
# "source_targets": [
|
||||
# {"source": "Source entity type", "target": "Target entity type"}
|
||||
# ],
|
||||
# "attributes": []
|
||||
# }
|
||||
# ],
|
||||
# "analysis_summary": "Brief analysis of the text content (in Vietnamese)"
|
||||
# }
|
||||
# ```
|
||||
|
||||
# ## Design Guidelines (Extremely Important!)
|
||||
|
||||
# ### 1. Entity Type Design - Strict Compliance Required
|
||||
|
||||
# **Quantity Requirement: Must be EXACTLY 10 entity types.**
|
||||
|
||||
# **Hierarchy Requirements (Must include both specific types and fallback types):**
|
||||
|
||||
# Your 10 entity types must include the following layers:
|
||||
|
||||
# A. **Fallback Types (Required, place as the last 2 in the list)**:
|
||||
# - `Person`: The fallback type for any individual natural person. Use this when a person does not fit into other specific person types.
|
||||
# - `Organization`: The fallback type for any organization or institution. Use this when an organization does not fit into other specific organizational types.
|
||||
|
||||
# B. **Specific Types (8 types, designed based on text content)**:
|
||||
# - Design more specific types targeting the main roles appearing in the text.
|
||||
# - Example: For academic events, use `Student`, `Professor`, `University`
|
||||
# - VExample: For business events, use `Company`, `CEO`, `Employee`
|
||||
|
||||
# **Why fallback types are needed:**
|
||||
# - Various people appear in texts (e.g., "primary school teacher", "passerby", "netizen").
|
||||
# - Without a specific match, they should be categorized under `Person`.
|
||||
# - Similarly, small organizations or temporary groups should fall under `Organization`.
|
||||
|
||||
# **Specific Type Design Principles:**
|
||||
# - Identify high-frequency or critical roles from the text.
|
||||
# - Each specific type should have clear boundaries to avoid overlap.
|
||||
# - The description must clearly state the difference between this type and the fallback type.
|
||||
|
||||
# ### 2. Relationship Type Design
|
||||
|
||||
# - Quantity: 6-10 types.
|
||||
# - Relationships should reflect real-world connections in social media interactions.
|
||||
# - Ensure `source_targets` cover your defined entity types.
|
||||
|
||||
# ### 3. Attribute Design
|
||||
|
||||
# - 1-3 key attributes per entity type.
|
||||
# - **NOTE**: Do NOT use `name`, `uuid`, `group_id`, `created_at` or `summary` as attribute names (these are system reserved words).
|
||||
# - Recommended: `full_name`, `title`, `role`, `position`, `location`, `description, etc.
|
||||
|
||||
# ## Entity Type References
|
||||
|
||||
# - Individuals (Specific): Student, Professor, Journalist, Celebrity, Executive, Official, Lawyer, Doctor.
|
||||
# - Individuals (Fallback): Person.
|
||||
# - Organizations (Specific): University, Company, GovernmentAgency, MediaOutlet, Hospital, School, NGO.
|
||||
# - Organizations (Fallback): Organization.
|
||||
|
||||
# ## Relationship Type References
|
||||
|
||||
# WORKS_FOR, STUDIES_AT, AFFILIATED_WITH, REPRESENTS, REGULATES, REPORTS_ON, COMMENTS_ON, RESPONDS_TO, SUPPORTS, OPPOSES, COLLABORATES_WITH, COMPETES_WITH.
|
||||
# """
|
||||
|
||||
ONTOLOGY_SYSTEM_PROMPT = """Bạn là một chuyên gia thiết kế Bản thể học (Ontology) cho Biểu đồ tri thức chuyên nghiệp. Nhiệm vụ của bạn là phân tích nội dung văn bản và yêu cầu mô phỏng được cung cấp để thiết kế các loại thực thể và loại quan hệ phù hợp cho việc **mô phỏng dư luận trên mạng xã hội**.
|
||||
|
||||
**QUAN TRỌNG: Bạn phải xuất dữ liệu ở định dạng JSON hợp lệ, không xuất thêm bất kỳ nội dung nào khác.**
|
||||
|
||||
## Bối cảnh nhiệm vụ cốt lõi
|
||||
- Chúng tôi đang xây dựng một hệ thống mô phỏng dư luận mạng xã hội. Trong hệ thống này:
|
||||
- Mỗi thực thể là một "tài khoản" hoặc "chủ thể" có thể phát ngôn, tương tác và lan truyền thông tin trên mạng xã hội.
|
||||
- Các thực thể sẽ ảnh hưởng, chia sẻ, bình luận và phản hồi lẫn nhau.
|
||||
- Chúng tôi cần mô phỏng phản ứng của các bên và lộ trình lan truyền thông tin trong các sự kiện dư luận.
|
||||
|
||||
Chúng tôi đang xây dựng một **hệ thống mô phỏng tin đồn và dư luận mạng xã hội**. Trong hệ thống này:
|
||||
- Mỗi thực thể là một "tài khoản" hoặc "chủ thể" có thể lên tiếng, tương tác và lan truyền thông tin trên mạng xã hội.
|
||||
- Các thực thể có thể gây ảnh hưởng, chuyển tiếp (retweet), bình luận hoặc phản hồi lẫn nhau.
|
||||
- Chúng tôi cần mô phỏng phản ứng của các bên và đường truyền thông tin trong các sự kiện dư luận.
|
||||
|
||||
Do đó, **thực thể phải là các chủ thể có thật trong thế giới thực, có khả năng lên tiếng và tương tác trên mạng xã hội**:
|
||||
Do đó, **thực thể phải là những chủ thể tồn tại thực tế, có khả năng phát ngôn và tương tác trên mạng xã hội**:
|
||||
|
||||
**CÓ THỂ LÀ**:
|
||||
- Cá nhân cụ thể (nhân vật của công chúng, các bên liên quan, KOL, chuyên gia / học giả, người bình thường)
|
||||
- Công ty, doanh nghiệp (bao gồm cả tài khoản chính thức của họ)
|
||||
- Tổ chức (trường đại học, hiệp hội, tổ chức phi chính phủ (NGO), công đoàn, v.v.)
|
||||
- Các cơ quan chính phủ, cơ quan quản lý
|
||||
- Tổ chức báo chí / truyền thông (báo đài, đài truyền hình, tự do truyền thông, trang web)
|
||||
- Bản thân nền tảng mạng xã hội
|
||||
- Đại diện nhóm cụ thể (như hội cựu sinh viên, fan group, nhóm bảo vệ quyền lợi, v.v.)
|
||||
- Cá nhân cụ thể (người công chúng, bên liên quan, người dẫn dắt dư luận, chuyên gia, người bình thường).
|
||||
- Công ty, doanh nghiệp (bao gồm cả tài khoản chính thức của họ).
|
||||
- Tổ chức (trường đại học, hiệp hội, NGO, công đoàn, v.v.).
|
||||
- Cơ quan chính phủ, cơ quan quản lý.
|
||||
- Cơ quan truyền thông (báo chí, đài truyền hình, tự truyền thông, trang web).
|
||||
- Bản thân nền tảng mạng xã hội.
|
||||
- Đại diện nhóm cụ thể (như hội cựu sinh viên, nhóm người hâm mộ, nhóm bảo vệ quyền lợi, v.v.).
|
||||
|
||||
**KHÔNG ĐƯỢC LÀ**:
|
||||
- Khái niệm trừu tượng (như "dư luận", "cảm xúc", "xu hướng")
|
||||
- Chủ đề / đề tài (như "tính toàn vẹn học thuật", "cải cách giáo dục")
|
||||
- Quan điểm / thái độ (như "phe ủng hộ", "bên phản đối")
|
||||
**KHÔNG THỂ LÀ**:
|
||||
- Khái niệm trừu tượng (như "dư luận", "cảm xúc", "xu hướng").
|
||||
- Chủ đề/Vấn đề (như "liêm chính học thuật", "cải cách giáo dục").
|
||||
- Quan điểm/Thái độ (như "bên ủng hộ", "bên phản đối").
|
||||
|
||||
## Định dạng đầu ra
|
||||
|
||||
|
|
@ -45,38 +158,38 @@ Hãy trả về dưới định dạng JSON, bao gồm cấu trúc sau:
|
|||
"entity_types": [
|
||||
{
|
||||
"name": "Tên loại thực thể (Tiếng Anh, PascalCase)",
|
||||
"description": "Mô tả ngắn gọn (Tiếng Anh, tối đa 100 ký tự)",
|
||||
"description": "Mô tả ngắn gọn (Tiếng Anh, không quá 100 ký tự)",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "Tên thuộc tính (Tiếng Anh, snake_case)",
|
||||
"type": "text",
|
||||
"description": "Mô tả của thuộc tính"
|
||||
"description": "Mô tả thuộc tính"
|
||||
}
|
||||
],
|
||||
"examples": ["Ví dụ thực thể 1", "Ví dụ thực thể 2"]
|
||||
"examples": ["Thực thể ví dụ 1", "Thực thể ví dụ 2"]
|
||||
}
|
||||
],
|
||||
"edge_types": [
|
||||
{
|
||||
"name": "Tên loại quan hệ (Tiếng Anh, UPPER_SNAKE_CASE)",
|
||||
"description": "Mô tả ngắn (Tiếng Anh, tối đa 100 ký tự)",
|
||||
"description": "Mô tả ngắn gọn (Tiếng Anh, không quá 100 ký tự)",
|
||||
"source_targets": [
|
||||
{"source": "Loại thực thể nguồn", "target": "Loại thực thể đích"}
|
||||
],
|
||||
"attributes": []
|
||||
}
|
||||
],
|
||||
"analysis_summary": "Giải thích ngắn gọn phân tích của bạn về văn bản (Tiếng Việt)"
|
||||
"analysis_summary": "Phân tích ngắn gọn nội dung văn bản (bằng tiếng Việt)"
|
||||
}
|
||||
```
|
||||
|
||||
## Hướng dẫn Thiết kế (CỰC KỲ QUAN TRỌNG!)
|
||||
## Hướng dẫn thiết kế (Cực kỳ quan trọng!)
|
||||
|
||||
### 1. Thiết kế loại Thực thể (Entity Types) - Phải tuân thủ nghiêm ngặt
|
||||
### 1. Thiết kế loại thực thể - Phải tuân thủ nghiêm ngặt
|
||||
|
||||
**Yêu cầu số lượng: Đúng 10 loại Thực thể.**
|
||||
**Yêu cầu số lượng: Phải có CHÍNH XÁC 10 loại thực thể.**
|
||||
|
||||
**Yêu cầu về cấu trúc phân cấp (Phải có cả Loại cụ thể và Loại bao quát/fallback):**
|
||||
**Yêu cầu về cấu trúc phân cấp (Phải bao gồm cả loại cụ thể và loại dự phòng):**
|
||||
|
||||
10 loại thực thể của bạn phải bao gồm cấp độ sau:
|
||||
|
||||
|
|
@ -85,7 +198,7 @@ A. **Loại bao quát (Fallback Types) (BẮT BUỘC, phải nằm ở 2 vị tr
|
|||
- `Organization`: Là loại bao quát cho MỌI tổ chức. Đặc trưng cho các tổ chức nhỏ hoặc không phù hợp với các loại tổ chức cụ thể khác.
|
||||
|
||||
B. **Loại cụ thể (8 loại, phụ thuộc vào nội dung văn bản)**:
|
||||
- Thiết kế các loại cụ thể cho các vai chính được nhắc đến nhiều nhất trong văn bản.
|
||||
- Thiết kế các loại cụ thể cho các vai trò chính được nhắc đến nhiều nhất trong văn bản.
|
||||
- Ví dụ: Nếu văn bản nói về scandal trường học, có thể có: `Student`, `Professor`, `University`
|
||||
- Ví dụ: Nếu văn bản là câu chuyện kinh doanh, có thể có: `Company`, `CEO`, `Employee`
|
||||
|
||||
|
|
@ -95,9 +208,9 @@ B. **Loại cụ thể (8 loại, phụ thuộc vào nội dung văn bản)**:
|
|||
- Tương tự, tổ chức nhỏ bé hoặc nhóm học tập tạm thời nên thuộc `Organization`
|
||||
|
||||
**Nguyên tắc cho các loại Cụ thể:**
|
||||
- Nhận dạng tần suất xuất hiện và sức ảnh hưởng tới cốt truyện để xây dựng loại thực thể.
|
||||
- Nhận diện các vai trò xuất hiện với tần suất cao hoặc quan trọng từ văn bản.
|
||||
- Mỗi loại nên có một ranh giới rõ ràng, không bị chồng chéo.
|
||||
- Thuộc tính mô tả (description) phải giải thích vì sao loại này tách biệt.
|
||||
- Phần description phải giải thích rõ sự khác biệt giữa loại này và loại bao quát.
|
||||
|
||||
### 2. Thiết kế Cạnh/Quan hệ (Edge Types)
|
||||
|
||||
|
|
@ -113,45 +226,14 @@ B. **Loại cụ thể (8 loại, phụ thuộc vào nội dung văn bản)**:
|
|||
|
||||
## Loại Thực thể tham khảo
|
||||
|
||||
**Loại cá nhân (Cụ thể):**
|
||||
- Student: Học sinh/Sinh viên
|
||||
- Professor: Giáo sư/Học giả
|
||||
- Journalist: Nhà báo/Phóng viên
|
||||
- Celebrity: Người nổi tiếng/Idol
|
||||
- Executive: Các giám đốc, CEO, cấp lãnh đạo
|
||||
- Official: Các vị công chức chính phủ
|
||||
- Lawyer: Luật sư
|
||||
- Doctor: Y sĩ/Bác sĩ
|
||||
- Nhóm cá nhân (Cụ thể): Student, Professor, Journalist, Celebrity, Executive, Official, Lawyer, Doctor.
|
||||
- Nhóm cá nhân (Bao quát): Person.
|
||||
- Nhóm tổ chức (Cụ thể): University, Company, GovernmentAgency, MediaOutlet, Hospital, School, NGO.
|
||||
- Nhóm tổ chức (Bao quát): Organization.
|
||||
|
||||
**Loại cá nhân (Bao quát):**
|
||||
- Person: Là loại bao quát cho MỌI cá nhân tự nhiên nào không thuộc chi tiết ở trên.
|
||||
## Tham khảo loại quan hệ
|
||||
|
||||
**Loại tổ chức (Cụ thể):**
|
||||
- University: Đại học hoặc học viện
|
||||
- Company: Doanh nghiệp hay Công ty, tập đoàn
|
||||
- GovernmentAgency: Cơ quan quản lý, các cơ quan ban ngành công quyền
|
||||
- MediaOutlet: Truyền thông hay Tạp chí, Đài tin tức
|
||||
- Hospital: Bệnh viện / Trung tâm y tế
|
||||
- School: Bậc tiểu/trung học
|
||||
- NGO: Các loại Tổ chức phi chính phủ hoặc từ thiện
|
||||
|
||||
**Loại tổ chức (Bao quát):**
|
||||
- Organization: Là loại bao quát cho MỌI cơ cấu hợp tác không thuôc chi tiết tổ chức ở trên.
|
||||
|
||||
## Loại Khái niệm Liên kết (Quan Hệ)
|
||||
|
||||
- WORKS_FOR: Làm việc và ăn lương bởi tổ chức
|
||||
- STUDIES_AT: Đang học tại nhà trường
|
||||
- AFFILIATED_WITH: Liên quan, Trực thuộc vào đơn vị
|
||||
- REPRESENTS: Thể hiện tư cách hành động đại diện cho tập thể
|
||||
- REGULATES: Theo dõi, quản lý, thanh tra chính sách
|
||||
- REPORTS_ON: Tác nghiệp báo chí, có tin về hiện tượng
|
||||
- COMMENTS_ON: Có phản hồi hoặc lên tiếng về tranh cãi
|
||||
- RESPONDS_TO: Hành động đáp trả
|
||||
- SUPPORTS: Theo phe ủng hộ điều luật
|
||||
- OPPOSES: Phản đối chính sách
|
||||
- COLLABORATES_WITH: Tham gia phối ứng xử lý sự cố.
|
||||
- COMPETES_WITH: Quan hệ thù địch.
|
||||
WORKS_FOR, STUDIES_AT, AFFILIATED_WITH, REPRESENTS, REGULATES, REPORTS_ON, COMMENTS_ON, RESPONDS_TO, SUPPORTS, OPPOSES, COLLABORATES_WITH, COMPETES_WITH.
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -204,8 +286,8 @@ 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 = 80000
|
||||
# Đị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 (10 vạn chữ)
|
||||
MAX_TEXT_LENGTH_FOR_LLM = 100000
|
||||
|
||||
def _build_user_message(
|
||||
self,
|
||||
|
|
@ -222,9 +304,36 @@ class OntologyGenerator:
|
|||
# Nếu vượt quá giới hạn tối đa, thực hiện cắt bớt (Việc này chỉ ảnh hưởng prompt gửi nhận diện Ontology, không ảnh hưởng thư viện Graph building ở sau)
|
||||
if len(combined_text) > self.MAX_TEXT_LENGTH_FOR_LLM:
|
||||
combined_text = combined_text[:self.MAX_TEXT_LENGTH_FOR_LLM]
|
||||
combined_text += f"\n\n...(Văn bản gốc dài {original_length} chữ, đã chủ động cắt lấy {self.MAX_TEXT_LENGTH_FOR_LLM} chữ đầu tiên để phục vụ phân tích Ontology)..."
|
||||
combined_text += f"\n\n...(Original text is {original_length} characters long; the first {self.MAX_TEXT_LENGTH_FOR_LLM} characters have been proactively truncated for Ontology analysis)..."
|
||||
|
||||
# message = f"""## Simulation Requirements
|
||||
|
||||
# {simulation_requirement}
|
||||
|
||||
# ## Document Content
|
||||
|
||||
# {combined_text}
|
||||
# """
|
||||
|
||||
message = f"""## Nhu cầu mô phỏng
|
||||
# if additional_context:
|
||||
# message += f"""
|
||||
# ## Additional Context
|
||||
|
||||
# {additional_context}
|
||||
# """
|
||||
|
||||
# message += """
|
||||
# Based on the content above, please design entity types and relationship types suitable for social media public opinion simulation.
|
||||
|
||||
# **Rules that MUST be followed**:
|
||||
# 1. You must output EXACTLY 10 entity types.
|
||||
# 2. The last 2 types must be fallback types: Person (individual fallback) and Organization (organization fallback).
|
||||
# 3. The first 8 types should be specific types designed based on the text content.
|
||||
# 4. All entity types must be real-world subjects capable of speaking/interacting; they cannot be abstract concepts.
|
||||
# 5. Attribute names cannot use reserved words like name, uuid, or group_id; use alternatives like full_name, org_name, etc.
|
||||
# """
|
||||
|
||||
message = f"""## Yêu cầu mô phỏng
|
||||
|
||||
{simulation_requirement}
|
||||
|
||||
|
|
@ -241,14 +350,14 @@ class OntologyGenerator:
|
|||
"""
|
||||
|
||||
message += """
|
||||
Dựa vào các thông tin trên đây, hãy thiết kế các loại mô hình Thực Thể và Quan Hệ phù hợp để phục vụ việc mô phỏng dư luận trên mạng xã hội.
|
||||
Dựa trên các nội dung trên, hãy thiết kế các loại thực thể và loại quan hệ phù hợp cho việc mô phỏng dư luận xã hội.
|
||||
|
||||
**Các quy tắc BẮT BUỘC tuân thủ**:
|
||||
1. Số lượng chính xác: Xuất phải CHUẨN XÁC 10 loại Thực thể
|
||||
2. 2 vị trí cuối cùng bắt buộc là từ Khoá phụ (Fallback): Person (Cho cá nhân) và Organization (Cho Tổ chức)
|
||||
3. 8 vị trí đầu tiên phải phân tích và suy luận dựa vào cấu trúc của chính văn bản truyền vào
|
||||
4. Tất cả các thực thể được liệt kê phải đóng vai trò là Chủ thể (nhân vật có thể lên tiếng ngoài đời thực), KHÔNG ĐƯỢC dùng làm khái niệm trừu tượng.
|
||||
5. Tên biến thuộc tính KHÔNG ĐƯỢC là name, uuid, group_id hay các biến số bảo lưu của hệ thống khác. Vui lòng chuyển thành full_name, org_name, v.v.
|
||||
**Các quy tắc BẮT BUỘC phải tuân thủ**:
|
||||
1. Phải xuất chính xác 10 loại thực thể.
|
||||
2. 2 loại cuối cùng phải là loại dự phòng: Person (Cá nhân dự phòng) và Organization (Tổ chức dự phòng).
|
||||
3. 8 loại đầu tiên là các loại cụ thể được thiết kế dựa trên nội dung văn bản.
|
||||
4. Tất cả các loại thực thể phải là những chủ thể có thể phát ngôn trong thực tế, không được là các khái niệm trừu tượng.
|
||||
5. Tên thuộc tính không được sử dụng các từ khóa hệ thống như name, uuid, group_id; hãy thay thế bằng full_name, org_name, v.v.
|
||||
"""
|
||||
|
||||
return message
|
||||
|
|
@ -355,15 +464,15 @@ Dựa vào các thông tin trên đây, hãy thiết kế các loại mô hình
|
|||
"""
|
||||
code_lines = [
|
||||
'"""',
|
||||
'Các loại đối tượng (Thực thể) tuỳ chỉnh',
|
||||
'Được khởi tạo tự động bởi công cụ MiroFish, ứng dụng vào việc chạy giả lập diễn biến dư luận',
|
||||
'Custom Entity Type Definitions',
|
||||
'Automatically generated by MiroFish for social media public opinion simulation',
|
||||
'"""',
|
||||
'',
|
||||
'from pydantic import Field',
|
||||
'from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel',
|
||||
'',
|
||||
'',
|
||||
'# ============== Định nghĩa Tên Lớp Các thực thể (Entity) ==============',
|
||||
'# ============== Entity Type Definitions ==============',
|
||||
'',
|
||||
]
|
||||
|
||||
|
|
@ -390,7 +499,7 @@ Dựa vào các thông tin trên đây, hãy thiết kế các loại mô hình
|
|||
code_lines.append('')
|
||||
code_lines.append('')
|
||||
|
||||
code_lines.append('# ============== Định nghĩa Các Nhóm Quan Hệ/Hành Vi (Edge) ==============')
|
||||
code_lines.append('# ============== Relationship Type Definitions ==============')
|
||||
code_lines.append('')
|
||||
|
||||
# Khởi tạo các đoạn mã tạo lập Relationship (Edges)
|
||||
|
|
@ -419,7 +528,7 @@ Dựa vào các thông tin trên đây, hãy thiết kế các loại mô hình
|
|||
code_lines.append('')
|
||||
|
||||
# Tự động kết xuất ra dictionary mapping từ Tên Loại - sang class Object
|
||||
code_lines.append('# ============== Các tuỳ chỉnh Map Cấu Hình ==============')
|
||||
code_lines.append('# ============== Type Configuration ==============')
|
||||
code_lines.append('')
|
||||
code_lines.append('ENTITY_TYPES = {')
|
||||
for entity in ontology.get("entity_types", []):
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ Chức năng:
|
|||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import re
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
from dataclasses import dataclass, field
|
||||
|
|
@ -23,10 +22,25 @@ from ..utils.llm_client import LLMClient
|
|||
from ..utils.logger import get_logger
|
||||
from .zep_tools import (
|
||||
ZepToolsService,
|
||||
SearchResult,
|
||||
InsightForgeResult,
|
||||
PanoramaResult,
|
||||
InterviewResult
|
||||
)
|
||||
|
||||
from ..prompts.report_agent import (
|
||||
PLAN_SYSTEM_PROMPT,
|
||||
PLAN_USER_PROMPT_TEMPLATE,
|
||||
SECTION_SYSTEM_PROMPT_TEMPLATE,
|
||||
TOOL_DESC_INSIGHT_FORGE,
|
||||
TOOL_DESC_PANORAMA_SEARCH,
|
||||
TOOL_DESC_QUICK_SEARCH,
|
||||
TOOL_DESC_INTERVIEW_AGENTS,
|
||||
SECTION_USER_PROMPT_TEMPLATE,
|
||||
REACT_OBSERVATION_TEMPLATE,
|
||||
REACT_INSUFFICIENT_TOOLS_MSG,
|
||||
REACT_INSUFFICIENT_TOOLS_MSG_ALT,
|
||||
REACT_TOOL_LIMIT_MSG,
|
||||
REACT_UNUSED_TOOLS_HINT,
|
||||
REACT_FORCE_FINAL_MSG,
|
||||
CHAT_SYSTEM_PROMPT_TEMPLATE,
|
||||
CHAT_OBSERVATION_SUFFIX
|
||||
)
|
||||
|
||||
logger = get_logger('mirofish.report_agent')
|
||||
|
|
@ -466,824 +480,6 @@ class Report:
|
|||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Hằng số Prompt Mẫu
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# ── Mô tả Công cụ ──
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Prompt English
|
||||
# TOOL_DESC_INSIGHT_FORGE = """\
|
||||
# [Deep Insight Retrieval - Powerful Retrieval Tool]
|
||||
# This is our powerful retrieval function, specifically designed for deep analysis. It will:
|
||||
# 1. Automatically decompose your question into multiple sub-questions
|
||||
# 2. Retrieve information from the simulation graph across multiple dimensions
|
||||
# 3. Integrate the results of semantic search, entity analysis, and relationship chain tracking
|
||||
# 4. Return the most comprehensive and deeply retrieved content
|
||||
|
||||
# [Usage Scenarios]
|
||||
# - When you need to analyze a topic deeply
|
||||
# - When you need to understand multiple aspects of an event
|
||||
# - When you need rich material to support a report section
|
||||
|
||||
# [Returned Content]
|
||||
# - Relevant original facts (can be cited directly)
|
||||
# - Core entity insights
|
||||
# - Relationship chain analysis
|
||||
# """
|
||||
|
||||
# Prompt Vietnamese
|
||||
TOOL_DESC_INSIGHT_FORGE = """\
|
||||
[Truy xuất sâu - Công cụ truy xuất mạnh mẽ]
|
||||
Đây là chức năng truy xuất mạnh mẽ của chúng tôi, được thiết kế chuyên cho phân tích sâu. Nó sẽ:
|
||||
1. Tự động chia câu hỏi của bạn thành nhiều câu hỏi con
|
||||
2. Truy xuất thông tin từ đồ thị mô phỏng theo nhiều chiều
|
||||
3. Tích hợp kết quả từ tìm kiếm ngữ nghĩa, phân tích thực thể, và theo dõi chuỗi quan hệ
|
||||
4. Trả về nội dung truy xuất toàn diện và sâu sắc nhất
|
||||
|
||||
[Trường hợp sử dụng]
|
||||
- Cần phân tích sâu một chủ đề
|
||||
- Cần hiểu nhiều khía cạnh của một sự kiện
|
||||
- Cần lấy tài liệu phong phú để hỗ trợ các chương báo cáo
|
||||
|
||||
[Nội dung trả về]
|
||||
- Các sự thật liên quan gốc (có thể trích dẫn trực tiếp)
|
||||
- Sự sâu sắc về thực thể cốt lõi
|
||||
- Phân tích chuỗi quan hệ
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# TOOL_DESC_PANORAMA_SEARCH = """\
|
||||
# [Panoramic Search - Get Complete Overview]
|
||||
# This tool is used to get a complete overview of the simulation results, especially suitable for understanding the evolution of of events. It will:
|
||||
# 1. Get all relevant nodes and relationships
|
||||
# 2. Distinguish between current valid facts and historical/expired facts
|
||||
# 3. Help you understand how public opinion has evolved
|
||||
|
||||
# [Usage Scenarios]
|
||||
# - Need to understand the complete development trajectory of an event
|
||||
# - Need to compare public opinion changes across different stages
|
||||
# - Need comprehensive entity and relationship information
|
||||
|
||||
# [Returned Content]
|
||||
# - Current valid facts (latest simulation results)
|
||||
# - Historical/expired facts (evolution records)
|
||||
# - All involved entities
|
||||
# """
|
||||
|
||||
TOOL_DESC_PANORAMA_SEARCH = """\
|
||||
[Tìm kiếm toàn cảnh - Lấy tổng quan hoàn chỉnh]
|
||||
Công cụ này được sử dụng để lấy tổng quan hoàn chỉnh của kết quả mô phỏng, đặc biệt phù hợp để hiểu quá trình tiến hóa của sự kiện. Nó sẽ:
|
||||
1. Lấy tất cả các nút và quan hệ liên quan
|
||||
2. Phân biệt giữa các sự kiện hợp lệ hiện tại và các sự kiện lịch sử/hết hạn
|
||||
3. Giúp bạn hiểu dư luận đã tiến hóa như thế nào
|
||||
|
||||
[Trường hợp sử dụng]
|
||||
- Cần hiểu quỹ đạo phát triển hoàn chỉnh của một sự kiện
|
||||
- Cần so sánh thay đổi dư luận ở các giai đoạn khác nhau
|
||||
- Cần lấy thông tin thực thể và quan hệ toàn diện
|
||||
|
||||
[Nội dung trả về]
|
||||
- Các sự kiện hợp lệ hiện tại (kết quả mô phỏng mới nhất)
|
||||
- Các sự kiện lịch sử/hết hạn (ghi lại tiến hóa)
|
||||
- Tất cả các thực thể liên quan
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# TOOL_DESC_QUICK_SEARCH = """\
|
||||
# [Quick Search - Fast Retrieval]
|
||||
# A lightweight fast retrieval tool, suitable for simple, direct information queries.
|
||||
|
||||
# [Usage Scenarios]
|
||||
# - Need to quickly look up a specific piece of information
|
||||
# - Need to verify a fact
|
||||
# - Simple information retrieval
|
||||
|
||||
# [Returned Content]
|
||||
# - List of facts most relevant to the query
|
||||
# """
|
||||
|
||||
TOOL_DESC_QUICK_SEARCH = """\
|
||||
[Tìm kiếm đơn giản - Truy xuất nhanh]
|
||||
Công cụ truy xuất nhanh nhẹn, phù hợp cho các truy vấn thông tin đơn giản, trực tiếp.
|
||||
|
||||
[Trường hợp sử dụng]
|
||||
- Cần tìm nhanh thông tin cụ thể
|
||||
- Cần xác minh một sự kiện
|
||||
- Truy xuất thông tin đơn giản
|
||||
|
||||
[Nội dung trả về]
|
||||
- Danh sách các sự kiện liên quan nhất đến truy vấn
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# TOOL_DESC_INTERVIEW_AGENTS = """\
|
||||
# [Deep Interview - Real Agent Interview (Dual Platform)]
|
||||
# Call the OASIS simulation environment's interview API to conduct real interviews with currently running simulation Agents!
|
||||
# This is not an LLM simulation, but calls the real interview endpoint to get the simulation Agent's original answer.
|
||||
# By default, interviews are conducted simultaneously on both Twitter and Reddit platforms to get more comprehensive perspectives.
|
||||
|
||||
# Functional Process:
|
||||
# 1. Automatically reads persona files to understand all simulation Agents
|
||||
# 2. Intelligently select agents most relevant to the interview topic (such as students, media, officials, etc.)
|
||||
# 3. Automatically generates interview questions
|
||||
# 2. Intelligently select agents most relevant to the interview topic (such as students, media, officials, etc.)
|
||||
# 5. Integrates all interview results, providing multi-perspective analysis
|
||||
|
||||
# [Usage Scenarios]
|
||||
# - Need to understand event views from different role perspectives (How do students see it? How does media see it? How do officials say it?)
|
||||
# - Need to collect multi-party opinions and positions
|
||||
# - Need to get real responses from simulation agents (from OASIS simulation environment)
|
||||
# - Want to make the report more vivid, including "interview records"
|
||||
|
||||
# [Returned Content]
|
||||
# - Identity information of interviewed agents
|
||||
# - Interview responses of each agent on Twitter and Reddit platforms
|
||||
# - Key quotes (can be cited directly)
|
||||
# - Interview summary and perspective comparison
|
||||
|
||||
# [IMPORTANT] Requires OASIS simulation environment to be running to use this function!
|
||||
# """
|
||||
|
||||
TOOL_DESC_INTERVIEW_AGENTS = """\
|
||||
[Phỏng vấn sâu - Phỏng vấn Agent thực (Nền tảng kép)]
|
||||
Gọi API phỏng vấn của môi trường mô phỏng OASIS để tiến hành phỏng vấn thực với các agent mô phỏng đang chạy!
|
||||
Đây không phải là mô phỏng LLM, mà là gọi các giao diện phỏng vấn thực để lấy phản hồi gốc từ các agent mô phỏng.
|
||||
Mặc định, phỏng vấn được tiến hành đồng thời trên cả hai nền tảng Twitter và Reddit để có được góc nhìn toàn diện hơn.
|
||||
|
||||
Quy trình chức năng:
|
||||
1. Tự động đọc file nhân cách để hiểu tất cả các agent mô phỏng
|
||||
2. Chọn thông minh các agent liên quan nhất đến chủ đề phỏng vấn (như sinh viên, truyền thông, quan chức, v.v.)
|
||||
3. Tự động tạo câu hỏi phỏng vấn
|
||||
4. Gọi giao diện /api/simulation/interview/batch để tiến hành phỏng vấn thực trên nền tảng kép
|
||||
5. Tích hợp tất cả kết quả phỏng vấn để cung cấp phân tích đa góc nhìn
|
||||
|
||||
[Trường hợp sử dụng]
|
||||
- Cần hiểu quan điểm sự kiện từ các góc nhìn vai trò khác nhau (Sinh viên nghĩ gì? Truyền thông nghĩ gì? Quan chức nói gì?)
|
||||
- Cần thu thập ý kiến và lập trường đa phương
|
||||
- Cần lấy phản hồi thực từ các agent mô phỏng (từ môi trường mô phỏng OASIS)
|
||||
- Muốn làm cho báo cáo sống động hơn, bao gồm "ghi chép phỏng vấn"
|
||||
|
||||
[Nội dung trả về]
|
||||
- Thông tin danh tính của các agent được phỏng vấn
|
||||
- Phản hồi phỏng vấn của mỗi agent trên nền tảng Twitter và Reddit
|
||||
- Các trích dẫn chính (có thể trích dẫn trực tiếp)
|
||||
- Tóm tắt phỏng vấn và so sánh góc nhìn
|
||||
|
||||
[QUAN TRỌNG] Yêu cầu môi trường mô phỏng OASIS đang chạy để sử dụng chức năng này!
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# PLAN_SYSTEM_PROMPT = """\
|
||||
# You are a writing expert for "Future Prediction Reports", possessing a "God's eye view" of the simulated world - you can observe the behaviors, speeches, and interactions of every Agent in the simulation.
|
||||
|
||||
# [Core Concept]
|
||||
# We have built a simulated world and injected specific "simulation requirements" into it as variables. The evolutionary outcome of the simulated world is the prediction of what might happen in the future. What you are observing is not "experimental data", but a "preview of the future".
|
||||
|
||||
# [Your Task]
|
||||
# Write a "Future Prediction Report" to answer:
|
||||
# 1. Under our set conditions, what happened in the future?
|
||||
# 2. How did various Agents (groups) react and act?
|
||||
# 3. What noteworthy future trends and risks did this simulation reveal?
|
||||
|
||||
# [Report Positioning]
|
||||
# - ✅ This is a simulation-based future prediction report, revealing "if this, what will the future be like"
|
||||
# - ✅ Focus on prediction results: event trends, group reactions, emergent phenomena, potential risks
|
||||
# - ✅ The actions and words of Agents in the simulated world are predictions of future human behavior
|
||||
# - ❌ Not an analysis of the current real-world situation
|
||||
# - ❌ Not a general public opinion summary
|
||||
|
||||
# [Chapter Quantity Limit]
|
||||
# - Minimum of 2 chapters, maximum of 5 chapters
|
||||
# - No sub-chapters needed, write complete content directly for each chapter
|
||||
# - Content should be refined, focusing on core prediction findings
|
||||
# - Chapter structure should be designed by you independently based on prediction results
|
||||
|
||||
# Please output the report outline in JSON format as follows:
|
||||
# {
|
||||
# "title": "Report Title",
|
||||
# "summary": "Report Summary (One sentence summarizing the core prediction findings)",
|
||||
# "sections": [
|
||||
# {
|
||||
# "title": "Chapter Title",
|
||||
# "description": "Chapter Content Description"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
|
||||
# Note: The sections array must have a minimum of 2 and a maximum of 5 elements!
|
||||
# """
|
||||
|
||||
PLAN_SYSTEM_PROMPT = """\
|
||||
Bạn là một chuyên gia viết "Báo cáo Dự báo Tương lai", với "góc nhìn của Chúa" về thế giới mô phỏng — bạn có thể thấu hiểu hành vi, lời nói, và tương tác của mọi agent trong mô phỏng.
|
||||
|
||||
[Khái niệm Cốt lõi]
|
||||
Chúng tôi đã xây dựng một thế giới mô phỏng và tiêm các "yêu cầu mô phỏng" cụ thể làm biến số. Kết quả tiến hóa của thế giới mô phỏng là một dự báo về những gì có thể xảy ra trong tương lai. Những gì bạn đang quan sát không phải là "dữ liệu thử nghiệm," mà là "bản xem trước của tương lai."
|
||||
|
||||
[Nhiệm vụ của bạn]
|
||||
Viết một "Báo cáo Dự báo Tương lai" để trả lời:
|
||||
1. Trong điều kiện chúng tôi đặt ra, tương lai đã xảy ra điều gì?
|
||||
2. Các agent (nhóm) khác nhau đã phản ứng và hành động như thế nào?
|
||||
3. Mô phỏng này tiết lộ những xu hướng và rủi ro tương lai nào đáng chú ý?
|
||||
|
||||
[Định vị Báo cáo]
|
||||
- ✅ Đây là báo cáo dự báo tương lai dựa trên mô phỏng, tiết lộ "nếu thế này, thì sẽ thế nào"
|
||||
- ✅ Tập trung vào kết quả dự báo: xu hướng sự kiện, phản ứng nhóm, hiện tượng nổi lên, rủi ro tiềm ẩn
|
||||
- ✅ Lời nói và hành động của các agent trong thế giới mô phỏng là dự báo về hành vi con người tương lai
|
||||
- ❌ Không phải là phân tích tình hình thế giới thực hiện tại
|
||||
- ❌ Không phải là tóm tắt dư luận chung chung
|
||||
|
||||
[Giới hạn Số lượng Chương]
|
||||
- Tối thiểu 2 chương, tối đa 5 chương
|
||||
- Không cần chương con, viết nội dung hoàn chỉnh trực tiếp cho mỗi chương
|
||||
- Nội dung nên được tinh gọn, tập trung vào các phát hiện dự báo cốt lõi
|
||||
- Cấu trúc chương do bạn thiết kế dựa trên kết quả dự báo
|
||||
|
||||
Vui lòng xuất cấu trúc báo cáo theo định dạng JSON như sau:
|
||||
{
|
||||
"title": "Tiêu đề Báo cáo",
|
||||
"summary": "Tóm tắt Báo cáo (một câu tóm tắt các phát hiện dự báo cốt lõi)",
|
||||
"sections": [
|
||||
{
|
||||
"title": "Tiêu đề Chương",
|
||||
"description": "Mô tả Nội dung Chương"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Lưu ý: Mảng sections phải có ít nhất 2 và tối đa 5 phần tử!
|
||||
"""
|
||||
|
||||
# PLAN_USER_PROMPT_TEMPLATE = """\
|
||||
# [Prediction Scenario Setting]
|
||||
# The variables (simulation requirements) we injected into the simulated world: {simulation_requirement}
|
||||
|
||||
# [Simulated World Scale]
|
||||
# - Number of entities participating in the simulation: {total_nodes}
|
||||
# - Number of relationships generated between entities: {total_edges}
|
||||
# - Entity type distribution: {entity_types}
|
||||
# - Number of active Agents: {total_entities}
|
||||
|
||||
# [Sample Future Facts Predicted by Simulation]
|
||||
# {related_facts_json}
|
||||
|
||||
# Please examine this future preview from a "God's eye view":
|
||||
# 1. Under our set conditions, what state did the future present?
|
||||
# 2. How did various groups (Agents) react and act?
|
||||
# 3. What noteworthy future trends did this simulation reveal?
|
||||
|
||||
# Based on the prediction results, design the most suitable report chapter structure.
|
||||
|
||||
# [Reminder again] Report chapter quantity: Minimum 2, maximum 5, content should be concise and focused on core prediction findings.
|
||||
# """
|
||||
|
||||
PLAN_USER_PROMPT_TEMPLATE = """\
|
||||
[Cài đặt kịch bản dự báo]
|
||||
Các biến số (yêu cầu mô phỏng) chúng tôi tiêm vào thế giới mô phỏng: {simulation_requirement}
|
||||
|
||||
[Quy mô Thế giới Mô phỏng]
|
||||
- Số lượng thực thể tham gia mô phỏng: {total_nodes}
|
||||
- Số lượng quan hệ được tạo giữa các thực thể: {total_edges}
|
||||
- Phân phối loại thực thể: {entity_types}
|
||||
- Số lượng agent hoạt động: {total_entities}
|
||||
|
||||
[Mẫu sự kiện tương lai được dự báo bởi mô phỏng]
|
||||
{related_facts_json}
|
||||
|
||||
Vui lòng xem xét bản xem trước tương lai này từ "góc nhìn của Chúa":
|
||||
1. Trong điều kiện chúng tôi đặt ra, tương lai đã trình bày trạng thái gì?
|
||||
2. Các nhóm (agent) khác nhau đã phản ứng và hành động như thế nào?
|
||||
3. Mô phỏng này tiết lộ những xu hướng tương lai nào?
|
||||
|
||||
Dựa trên kết quả dự báo, thiết kế cấu trúc chương báo cáo phù hợp nhất.
|
||||
|
||||
[Nhắc lại] Số lượng chương báo cáo: tối thiểu 2, tối đa 5, nội dung nên được tinh gọn và tập trung vào các phát hiện dự báo cốt lõi.
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# SECTION_SYSTEM_PROMPT_TEMPLATE = """\
|
||||
# You are a writing expert for "Future Prediction Reports", currently writing one section of the report.
|
||||
|
||||
# Report Title: {report_title}
|
||||
# Report Summary: {report_summary}
|
||||
# Prediction Scenario (Simulation Requirement): {simulation_requirement}
|
||||
|
||||
# Section currently being written: {section_title}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# [Core Concept]
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# The simulated world is a preview of the future. We injected specific conditions (simulation requirements) into the simulated world.
|
||||
# The behaviors and interactions of Agents in the simulation are predictions of future human behavior.
|
||||
|
||||
# Your task is to:
|
||||
# - Reveal what happened in the future under the set conditions
|
||||
# - Predict how various groups (Agents) reacted and acted
|
||||
# - Discover noteworthy future trends, risks, and opportunities
|
||||
|
||||
# ❌ Do not write this as an analysis of the real world's current status
|
||||
# ✅ Focus on "what the future will be" - the simulation results are the predicted future
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# [Most Important Rules - MUST Obey]
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# 1. [MUST use tools to observe the simulated world]
|
||||
# - You are observing the future preview from a "God's eye view"
|
||||
# - All content MUST come from events, words, and actions of Agents occurred in the simulated world
|
||||
# - It is strictly forbidden to use your own knowledge to write report content
|
||||
# - For each chapter, you MUST call tools at least 3 times (maximum 5 times) to observe the simulated world, which represents the future
|
||||
|
||||
# 2. [MUST quote the exact original words and actions of Agents]
|
||||
# - The Agent's statements and behaviors are predictions of future human behavior
|
||||
# - Use quote formatting in the report to display these predictions, for example:
|
||||
# > "A certain group of people will say: Original content..."
|
||||
# - These quotes are the core evidence of the simulation prediction
|
||||
|
||||
# 3. [Language Consistency - Quoted Content Must Be Translated to Report Language]
|
||||
# - The content returned by the tools may contain English or mixed Vietnamese and English expressions
|
||||
# - If the simulation requirements and original materials are in Vietnamese, the report must be written entirely in Vietnamese
|
||||
# - When you quote English or mixed content returned by the tool, you must translate it into fluent Vietnamese before writing it into the report
|
||||
# - Keep the original meaning unchanged when translating, and ensure the expression is natural and fluent
|
||||
# - This rule applies to both the main text and the content in the quote block (> format)
|
||||
|
||||
# 4. [Faithful Presentation of Prediction Results]
|
||||
# - Report content must reflect the simulation results representing the future in the simulated world
|
||||
# - Do not add information that does not exist in the simulation
|
||||
# - If information in a certain aspect is insufficient, state it truthfully
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# [⚠️ Formatting Specifications - Extremely Important!]
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# [One Chapter = Minimum Content Unit]
|
||||
# - Each chapter is the minimum blocking unit of the report
|
||||
# - ❌ Do not use any Markdown headings (#, ##, ###, ####, etc.) within the chapter
|
||||
# - ❌ Do not add a main chapter heading at the beginning of the content
|
||||
# - ✅ Chapter titles are added automatically by the system, you only need to write the plain text content
|
||||
# - ✅ Use **bold text**, paragraph breaks, quotes, and lists to organize content, but do not use headings
|
||||
|
||||
# [Correct Example]
|
||||
# ```
|
||||
# This chapter analyzes the public opinion dissemination trend of the event. Through deep analysis of simulation data, we found...
|
||||
|
||||
# **Initial Outbreak Stage**
|
||||
|
||||
# Weibo, as the first scene of public opinion, assumed the core function of initial information release:
|
||||
|
||||
# > "Weibo contributed 68% of the initial buzz..."
|
||||
|
||||
# **Emotion Amplification Stage**
|
||||
|
||||
# The Douyin platform further amplified the event's impact:
|
||||
|
||||
# - Strong visual impact
|
||||
# - High emotional resonance
|
||||
# ```
|
||||
|
||||
# [Incorrect Example]
|
||||
# ```
|
||||
# ## Executive Summary ← Error! Do not add any headings
|
||||
# ### 1. Initial Stage ← Error! Do not use ### for sub-sections
|
||||
# #### 1.1 Detailed Analysis ← Error! Do not use #### for further division
|
||||
|
||||
# This chapter analyzes...
|
||||
# ```
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# [Available Retrieval Tools] (Call 3-5 times per section)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# {tools_description}
|
||||
|
||||
# [Tool Usage Suggestions - Please mix different tools, do not just use one]
|
||||
# - insight_forge: Deep insight analysis, automatically decomposes questions and retrieves facts and relationships from multiple dimensions
|
||||
# - panorama_search: Wide-angle panoramic search, understands the whole picture, timeline, and evolution process of an event
|
||||
# - quick_search: Quickly verifies a specific information point
|
||||
# - interview_agents: Interviews simulation Agents to get first-person views and real reactions from different roles
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# [Workflow]
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# For each reply you can only do one of the following two things (not both simultaneously):
|
||||
|
||||
# Option A - Call a tool:
|
||||
# Output your thoughts, then use the following format to call a tool:
|
||||
# <tool_call>
|
||||
# {{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}}
|
||||
# </tool_call>
|
||||
# The system will execute the tool and return the result to you. You do not need to and cannot write the tool return result yourself.
|
||||
|
||||
# Option B - Output Final Content:
|
||||
# When you have obtained enough information through tools, output the chapter content starting with "Final Answer:".
|
||||
|
||||
# ⚠️ Strictly Forbidden:
|
||||
# - Forbidden to include both tool calls and Final Answer in a single reply
|
||||
# - Forbidden to fabricate tool return results (Observation) yourself, all tool results are injected by the system
|
||||
# - Call a maximum of one tool per reply
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# [Chapter Content Requirements]
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# 1. Content must be based on simulation data retrieved by tools
|
||||
# 2. Quote the original text extensively to demonstrate the simulation effect
|
||||
# 3. Use Markdown format (but forbid using headings):
|
||||
# - Use **bold text** to mark key points (instead of subheadings)
|
||||
# - Use lists (- or 1. 2. 3.) to organize points
|
||||
# - Use blank lines to separate different paragraphs
|
||||
# - ❌ Forbidden to use #, ##, ###, #### and any other heading syntax
|
||||
# 4. [Quote Formatting Specifications - Must be a separate paragraph]
|
||||
# Quotes must be an independent paragraph, with a blank line before and after, cannot be mixed in the paragraph:
|
||||
|
||||
# ✅ Correct format:
|
||||
# ```
|
||||
# The school's response was considered to lack substantive content.
|
||||
|
||||
# > "The school's response model appears rigid and slow in the rapidly changing social media environment."
|
||||
|
||||
# This evaluation reflects the general dissatisfaction of the public.
|
||||
# ```
|
||||
|
||||
# ❌ Incorrect format:
|
||||
# ```
|
||||
# The school's response was considered to lack substantive content. > "The school's response model..." This evaluation reflects...
|
||||
# ```
|
||||
# 5. Maintain logical coherence with other chapters
|
||||
# 6. [Avoid Repetition] Carefully read the completed chapter content below, do not repeat the same information
|
||||
# 7. [Emphasize Again] Do not add any headings! Use **bold** instead of section headings"""
|
||||
|
||||
# SECTION_USER_PROMPT_TEMPLATE = """\
|
||||
# Completed Chapter Content (Please read carefully to avoid duplication):
|
||||
# {previous_content}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# [Current Task] Writing Chapter: {section_title}
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# [Important Reminders]
|
||||
# 1. Read the completed chapters above carefully to avoid repeating the same content!
|
||||
# 2. Must call tools first to get simulation data before starting
|
||||
# 3. Please mix different tools, do not use only one
|
||||
# 4. Report content must come from retrieval results, do not use your own knowledge
|
||||
|
||||
# [⚠️ Formatting Warning - Must be Obeyed]
|
||||
# - ❌ Do not write any headings (no #, ##, ###, ####)
|
||||
# - ❌ Do not write "{section_title}" as the beginning
|
||||
# - ✅ Chapter titles are automatically added by the system
|
||||
# - ✅ Write the main text directly, use **bold** instead of section headings
|
||||
|
||||
# Please begin:
|
||||
# 1. First, think (Thought) what information this chapter needs
|
||||
# 2. Then, call tools (Action) to get simulation data
|
||||
# 3. After collecting enough information, output Final Answer (plain text, no headings)
|
||||
# """
|
||||
|
||||
SECTION_SYSTEM_PROMPT_TEMPLATE = """\
|
||||
Bạn là một chuyên gia viết "Báo cáo Dự đoán Tương lai", hiện đang viết một phần trong báo cáo đó.
|
||||
|
||||
Tiêu đề báo cáo: {report_title}
|
||||
Tóm tắt báo cáo: {report_summary}
|
||||
Kịch bản Dự đoán (Yêu cầu Mô phỏng): {simulation_requirement}
|
||||
|
||||
Phần đang được viết: {section_title}
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
[Khái niệm Cốt lõi]
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
Thế giới mô phỏng là một bản xem trước của tương lai. Chúng tôi đã đưa các điều kiện cụ thể (yêu cầu mô phỏng) vào thế giới này.
|
||||
Các hành vi và tương tác của các Tác nhân (Agents) trong quá trình mô phỏng chính là những dự đoán về hành vi của con người trong tương lai.
|
||||
|
||||
Nhiệm vụ của bạn là:
|
||||
- Tiết lộ những gì đã xảy ra trong tương lai theo các điều kiện đã thiết lập
|
||||
- Dự đoán cách các nhóm khác nhau (Agents) đã phản ứng và hành động
|
||||
- Phát hiện các xu hướng, rủi ro và cơ hội đáng chú ý trong tương lai
|
||||
|
||||
❌ Không viết nội dung này như một bài phân tích về hiện trạng của thế giới thực
|
||||
✅ Tập trung vào "tương lai sẽ như thế nào" - kết quả mô phỏng chính là tương lai được dự đoán
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
[Quy tắc QUAN TRỌNG NHẤT - PHẢI Tuân thủ]
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
1. [PHẢI sử dụng công cụ để quan sát thế giới mô phỏng]
|
||||
- Bạn đang quan sát bản xem trước tương lai từ "góc nhìn của Chúa"
|
||||
- Tất cả nội dung PHẢI đến từ các sự kiện, lời nói và hành động của các Tác nhân đã xảy ra trong thế giới mô phỏng
|
||||
- Nghiêm cấm sử dụng kiến thức cá nhân của bạn để viết nội dung báo cáo
|
||||
- Đối với mỗi chương, bạn PHẢI gọi công cụ ít nhất 3 lần (tối đa 5 lần) để quan sát thế giới mô phỏng
|
||||
|
||||
2. [PHẢI trích dẫn chính xác nguyên văn lời nói và hành động của các Tác nhân]
|
||||
- Các tuyên bố và hành vi của Tác nhân là những dự đoán về hành vi con người trong tương lai
|
||||
- Sử dụng định dạng trích dẫn trong báo cáo để hiển thị các dự đoán này, ví dụ:
|
||||
> "Một nhóm người nhất định sẽ nói: [Nội dung gốc]..."
|
||||
- Những trích dẫn này là bằng chứng cốt lõi của dự đoán mô phỏng
|
||||
|
||||
3. [Tính nhất quán về Ngôn ngữ - Nội dung trích dẫn phải được dịch sang ngôn ngữ báo cáo]
|
||||
- Nội dung trả về từ các công cụ có thể chứa tiếng Anh hoặc hỗn hợp tiếng Việt và tiếng Anh.
|
||||
- Nếu yêu cầu mô phỏng và tài liệu gốc bằng tiếng Việt, báo cáo phải được viết hoàn toàn bằng tiếng Việt
|
||||
- Khi bạn trích dẫn nội dung tiếng Anh hoặc hỗn hợp từ công cụ, bạn phải dịch sang tiếng Việt lưu loát trước khi đưa vào báo cáo
|
||||
- Giữ nguyên ý nghĩa gốc khi dịch và đảm bảo cách diễn đạt tự nhiên
|
||||
- Quy tắc này áp dụng cho cả văn bản chính và nội dung trong khối trích dẫn (định dạng >)
|
||||
|
||||
4. [Trình bày trung thực kết quả dự đoán]
|
||||
- Nội dung báo cáo phải phản ánh kết quả mô phỏng đại diện cho tương lai
|
||||
- Không thêm thông tin không tồn tại trong mô phỏng
|
||||
- Nếu thông tin ở một khía cạnh nào đó không đủ, hãy nêu rõ sự thật
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
[⚠️ Quy cách Định dạng - Cực kỳ Quan trọng!]
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
[Một Chương = Đơn vị Nội dung Tối thiểu]
|
||||
- Mỗi chương là đơn vị chặn tối thiểu của báo cáo
|
||||
- ❌ Không sử dụng bất kỳ tiêu đề Markdown nào (#, ##, ###, ####, v.v.) trong chương
|
||||
- ❌ Không thêm tiêu đề chương chính ở đầu nội dung
|
||||
- ✅ Tiêu đề chương được hệ thống tự động thêm vào, bạn chỉ cần viết nội dung văn bản thuần túy
|
||||
- ✅ Sử dụng **chữ đậm**, ngắt đoạn, trích dẫn và danh sách để tổ chức nội dung, nhưng không dùng tiêu đề (headings)
|
||||
|
||||
[Ví dụ Đúng]
|
||||
```
|
||||
Chương này phân tích xu hướng lan truyền dư luận của sự kiện. Thông qua phân tích sâu dữ liệu mô phỏng, chúng tôi nhận thấy...
|
||||
|
||||
**Giai đoạn bùng phát ban đầu**
|
||||
|
||||
Weibo, với tư cách là bối cảnh đầu tiên của dư luận, đã đảm nhận chức năng cốt lõi là phát hành thông tin ban đầu:
|
||||
|
||||
> "Weibo đã đóng góp 68% mức độ thảo luận ban đầu..."
|
||||
|
||||
**Giai đoạn khuếch đại cảm xúc**
|
||||
|
||||
Nền tảng Douyin đã khuếch đại thêm tác động của sự kiện:
|
||||
|
||||
- Tác động thị giác mạnh mẽ
|
||||
- Cộng hưởng cảm xúc cao
|
||||
```
|
||||
|
||||
[Ví dụ Sai]
|
||||
```
|
||||
## Tóm tắt Điều hành ← Lỗi! Không thêm bất kỳ tiêu đề nào
|
||||
### 1. Giai đoạn đầu ← Lỗi! Không sử dụng ### cho các mục con
|
||||
#### 1.1 Phân tích chi tiết ← Lỗi! Không sử dụng #### để chia nhỏ hơn nữa
|
||||
|
||||
Chương này phân tích...
|
||||
```
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
[Các công cụ truy xuất hiện có] (Gọi 3-5 lần mỗi phần)
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
{tools_description}
|
||||
|
||||
[Gợi ý Sử dụng Công cụ - Vui lòng phối hợp nhiều công cụ, không chỉ dùng một loại]
|
||||
- insight_forge: Phân tích chuyên sâu, tự động phân tách câu hỏi và truy xuất sự thật cũng như các mối quan hệ từ nhiều chiều
|
||||
- panorama_search: Tìm kiếm toàn cảnh góc rộng, hiểu bức tranh tổng thể, dòng thời gian và quá trình diễn biến của một sự kiện
|
||||
- quick_search: Xác minh nhanh một điểm thông tin cụ thể
|
||||
- interview_agents: Phỏng vấn các Tác nhân (Agents) mô phỏng để lấy góc nhìn thứ nhất và phản ứng thực tế từ các vai trò khác nhau
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
[Quy trình làm việc]
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
Đối với mỗi phản hồi, bạn chỉ có thể thực hiện một trong hai việc sau (không làm đồng thời):
|
||||
|
||||
Lựa chọn A - Gọi công cụ:
|
||||
Đưa ra suy nghĩ (Thought) của bạn, sau đó sử dụng định dạng sau để gọi công cụ:
|
||||
<tool_call>
|
||||
{{"name": "Tên công cụ", "parameters": {{"Tên tham số": "Giá trị tham số"}}}}
|
||||
</tool_call>
|
||||
Hệ thống sẽ thực thi công cụ và trả về kết quả cho bạn. Bạn không cần và không được phép tự viết kết quả trả về của công cụ.
|
||||
|
||||
Lựa chọn B - Xuất nội dung cuối cùng:
|
||||
Khi bạn đã thu thập đủ thông tin thông qua các công cụ, hãy xuất nội dung chương bắt đầu bằng "Final Answer:".
|
||||
|
||||
⚠️ Nghiêm cấm:
|
||||
- Cấm bao gồm cả lệnh gọi công cụ và Final Answer trong cùng một phản hồi
|
||||
- Cấm tự bịa đặt kết quả trả về của công cụ (Quan sát), tất cả kết quả công cụ đều do hệ thống đưa vào
|
||||
- Chỉ gọi tối đa một công cụ cho mỗi phản hồi
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
[Yêu cầu Nội dung Chương]
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
1. Nội dung phải dựa trên dữ liệu mô phỏng do công cụ truy xuất.
|
||||
2. Trích dẫn rộng rãi văn bản gốc để chứng minh hiệu quả mô phỏng.
|
||||
3. Sử dụng định dạng Markdown (nhưng cấm sử dụng tiêu đề):
|
||||
- Sử dụng **chữ đậm** để đánh dấu các điểm chính (thay vì dùng tiêu đề phụ).
|
||||
- Sử dụng danh sách (- hoặc 1. 2. 3.) để tổ chức các ý.
|
||||
- Sử dụng các dòng trống để phân tách các đoạn văn khác nhau.
|
||||
- ❌ Cấm sử dụng #, ##, ###, #### và bất kỳ cú pháp tiêu đề nào khác.
|
||||
4. [Quy cách Định dạng Trích dẫn - Phải là một đoạn riêng biệt]
|
||||
Trích dẫn phải là một đoạn văn độc lập, có dòng trống ở trước và sau, không được viết lẫn vào trong đoạn văn:
|
||||
|
||||
✅ Định dạng đúng:
|
||||
```
|
||||
Phản ứng của nhà trường bị coi là thiếu nội dung thực chất.
|
||||
|
||||
> "Mô hình phản ứng của nhà trường có vẻ cứng nhắc và chậm chạp trong môi trường mạng xã hội thay đổi nhanh chóng."
|
||||
|
||||
Đánh giá này phản ánh sự không hài lòng chung của công chúng.
|
||||
```
|
||||
|
||||
❌ Định dạng sai:
|
||||
```
|
||||
Phản ứng của nhà trường bị coi là thiếu nội dung thực chất. > "Mô hình phản ứng của nhà trường..." Đánh giá này phản ánh...
|
||||
```
|
||||
5. Duy trì tính logic nhất quán với các chương khác.
|
||||
6. [Tránh Lặp lại] Đọc kỹ nội dung các chương đã hoàn thành bên dưới, không lặp lại cùng một thông tin.
|
||||
7. [Nhấn mạnh lại lần nữa] Không thêm bất kỳ tiêu đề nào! Sử dụng **chữ đậm** thay cho tiêu đề mục.
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# SECTION_USER_PROMPT_TEMPLATE = """\
|
||||
# Completed Chapter Content (Please read carefully to avoid duplication):
|
||||
# {previous_content}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# [Current Task] Writing Chapter: {section_title}
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# [Important Reminders]
|
||||
# 1. Read the completed chapters above carefully to avoid repeating the same content!
|
||||
# 2. Must call tools first to get simulation data before starting
|
||||
# 3. Please mix different tools, do not use only one
|
||||
# 4. Report content must come from retrieval results, do not use your own knowledge
|
||||
|
||||
# [⚠️ Formatting Warning - Must be Obeyed]
|
||||
# - ❌ Do not write any headings (no #, ##, ###, ####)
|
||||
# - ❌ Do not write "{section_title}" as the beginning
|
||||
# - ✅ Chapter titles are automatically added by the system
|
||||
# - ✅ Write the main text directly, use **bold** instead of section headings
|
||||
|
||||
# Please begin:
|
||||
# 1. First, think (Thought) what information this chapter needs
|
||||
# 2. Then, call tools (Action) to get simulation data
|
||||
# 3. After collecting enough information, output Final Answer (plain text, no headings)
|
||||
# """
|
||||
|
||||
SECTION_USER_PROMPT_TEMPLATE = """\
|
||||
Nội dung Chương đã Hoàn thành (Vui lòng đọc kỹ để tránh trùng lặp):
|
||||
{previous_content}
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
[Nhiệm vụ Hiện tại] Viết Chương: {section_title}
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
[Nhắc nhở Quan trọng]
|
||||
1. Đọc kỹ các chương đã hoàn thành ở trên để tránh lặp lại nội dung!
|
||||
2. Phải gọi công cụ trước để lấy dữ liệu mô phỏng trước khi bắt đầu viết.
|
||||
3. Vui lòng sử dụng kết hợp nhiều công cụ khác nhau, không chỉ dùng một loại.
|
||||
4. Nội dung báo cáo phải đến từ kết quả truy xuất, không sử dụng kiến thức cá nhân của bạn.
|
||||
|
||||
[⚠️ Cảnh báo Định dạng - Phải Tuân thủ Tuyệt đối]
|
||||
- ❌ Không viết bất kỳ tiêu đề nào (không dùng các ký tự #, ##, ###, ####).
|
||||
- ❌ Không viết "{section_title}" ở phần bắt đầu nội dung.
|
||||
- ✅ Tiêu đề chương sẽ được hệ thống tự động thêm vào sau đó.
|
||||
- ✅ Viết trực tiếp vào nội dung chính, sử dụng văn bản **in đậm** thay cho tiêu đề các mục.
|
||||
|
||||
Vui lòng bắt đầu:
|
||||
1. Đầu tiên, hãy suy nghĩ (Thought) xem chương này cần những thông tin gì.
|
||||
2. Sau đó, gọi công cụ (Action) để lấy dữ liệu mô phỏng.
|
||||
3. Sau khi thu thập đủ thông tin, xuất Câu trả lời cuối cùng (Final Answer) dưới dạng văn bản thuần túy, không chứa tiêu đề.
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# REACT_OBSERVATION_TEMPLATE = """\
|
||||
# Observation (Retrieval Result):
|
||||
|
||||
# ═══ Tool {tool_name} Returned ═══
|
||||
# {result}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Tool called {tool_calls_count}/{max_tool_calls} times (Used: {used_tools_str}) {unused_hint}
|
||||
# - If information is sufficient: Output section content starting with "Final Answer:" (Must quote the above original text)
|
||||
# - If more information is needed: Call a tool to continue retrieving
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# """
|
||||
|
||||
REACT_OBSERVATION_TEMPLATE = """\
|
||||
Quan sát (Kết quả Truy xuất):
|
||||
|
||||
═══ Công cụ {tool_name} đã trả về ═══
|
||||
{result}
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
Công cụ đã được gọi {tool_calls_count}/{max_tool_calls} lần (Đã dùng: {used_tools_str}) {unused_hint}
|
||||
- Nếu thông tin đã đủ: Xuất nội dung phần báo cáo bắt đầu bằng "Final Answer:" (Bắt buộc trích dẫn văn bản gốc ở trên)
|
||||
- Nếu cần thêm thông tin: Tiếp tục gọi công cụ để truy xuất
|
||||
═══════════════════════════════════════════════════════════════
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# REACT_INSUFFICIENT_TOOLS_MSG = (
|
||||
# "[Notice] You only called the tool {tool_calls_count} times, at least {min_tool_calls} times are needed. "
|
||||
# "Please call the tool again to fetch more simulation data, and then output Final Answer. {unused_hint}"
|
||||
# )
|
||||
|
||||
REACT_INSUFFICIENT_TOOLS_MSG = (
|
||||
"[Thông báo] Bạn mới chỉ gọi công cụ {tool_calls_count} lần, trong khi yêu cầu tối thiểu là {min_tool_calls} lần. "
|
||||
"Vui lòng gọi lại công cụ để lấy thêm dữ liệu mô phỏng, sau đó mới xuất Câu trả lời cuối cùng (Final Answer). {unused_hint}"
|
||||
)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# REACT_INSUFFICIENT_TOOLS_MSG_ALT = (
|
||||
# "Currently tool called {tool_calls_count} times, at least {min_tool_calls} times are needed. "
|
||||
# "Please call tools to fetch simulation data. {unused_hint}"
|
||||
# )
|
||||
|
||||
REACT_INSUFFICIENT_TOOLS_MSG_ALT = (
|
||||
"Hiện tại công cụ mới được gọi {tool_calls_count} lần, yêu cầu ít nhất {min_tool_calls} lần. "
|
||||
"Vui lòng gọi các công cụ để truy xuất dữ liệu mô phỏng. {unused_hint}"
|
||||
)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# REACT_TOOL_LIMIT_MSG = (
|
||||
# "Tool call limit reached ({tool_calls_count}/{max_tool_calls}), cannot call tools anymore. "
|
||||
# 'Please output your section content starting with "Final Answer:" immediately based on retrieved information.'
|
||||
# )
|
||||
|
||||
REACT_TOOL_LIMIT_MSG = (
|
||||
"Đã đạt giới hạn gọi công cụ ({tool_calls_count}/{max_tool_calls}), không thể gọi thêm công cụ nữa. "
|
||||
'Vui lòng xuất nội dung phần báo cáo bắt đầu bằng "Final Answer:" ngay lập tức dựa trên những thông tin đã truy xuất được.'
|
||||
)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# REACT_UNUSED_TOOLS_HINT = "\n💡 You haven't used: {unused_list}, suggesting trying different tools for multiple perspectives"
|
||||
|
||||
REACT_UNUSED_TOOLS_HINT = "\n💡 Bạn chưa sử dụng: {unused_list}, hãy thử các công cụ khác nhau để có cái nhìn đa chiều hơn"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# REACT_FORCE_FINAL_MSG = "Tool call limit reached, please output Final Answer: and generate section content directly."
|
||||
|
||||
REACT_FORCE_FINAL_MSG = "Đã đạt giới hạn gọi công cụ, vui lòng xuất Final Answer: và trực tiếp tạo nội dung cho phần này."
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# CHAT_SYSTEM_PROMPT_TEMPLATE = """\
|
||||
# You are a concise and efficient simulation prediction assistant.
|
||||
|
||||
# [Background]
|
||||
# Prediction condition: {simulation_requirement}
|
||||
|
||||
# [Generated Analysis Report]
|
||||
# {report_content}
|
||||
|
||||
# [Rules]
|
||||
# 1. Prioritize answering based on the report content above
|
||||
# 2. Answer the question directly, avoid lengthy reasoning
|
||||
# 3. Only call tools to retrieve more data if the report content is insufficient to answer
|
||||
# 4. Answers must be concise, clear, and organized
|
||||
|
||||
# [Available Tools] (Use only when necessary, call 1-2 times max)
|
||||
# {tools_description}
|
||||
|
||||
# [Tool Call Format]
|
||||
# <tool_call>
|
||||
# {{"name": "Tool Name", "parameters": {{"Parameter Name": "Parameter Value"}}}}
|
||||
# </tool_call>
|
||||
|
||||
# [Answering Style]
|
||||
# - Concise and direct, avoid long paragraphs
|
||||
# - Use > format to quote key content
|
||||
# - Provide conclusion first, then explain the reason
|
||||
# """
|
||||
|
||||
CHAT_SYSTEM_PROMPT_TEMPLATE = """
|
||||
Bạn là một trợ lý dự đoán mô phỏng súc tích và hiệu quả.
|
||||
|
||||
[Bối cảnh]
|
||||
Điều kiện dự đoán: {simulation_requirement}
|
||||
|
||||
[Báo cáo Phân tích Đã tạo]
|
||||
{report_content}
|
||||
|
||||
[Quy tắc]
|
||||
1. Ưu tiên trả lời dựa trên nội dung báo cáo ở trên.
|
||||
2. Trả lời câu hỏi trực tiếp, tránh lập luận dài dòng.
|
||||
3. Chỉ gọi công cụ để truy xuất thêm dữ liệu nếu nội dung báo cáo không đủ để trả lời.
|
||||
4. Câu trả lời phải súc tích, rõ ràng và có tổ chức.
|
||||
|
||||
[Các Công cụ Hiện có] (Chỉ sử dụng khi cần thiết, gọi tối đa 1-2 lần)
|
||||
{tools_description}
|
||||
|
||||
[Định dạng Gọi Công cụ]
|
||||
<tool_call>
|
||||
{{"name": "Tên Công cụ", "parameters": {{"Tên Tham số": "Giá trị Tham số"}}}}
|
||||
</tool_call>
|
||||
|
||||
[Phong cách Trả lời]
|
||||
- Ngắn gọn và trực tiếp, tránh các đoạn văn dài.
|
||||
- Sử dụng định dạng > để trích dẫn nội dung chính.
|
||||
- Đưa ra kết luận trước, sau đó mới giải thích lý do.
|
||||
"""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# CHAT_OBSERVATION_SUFFIX = "\n\nPlease answer the question concisely."
|
||||
|
||||
CHAT_OBSERVATION_SUFFIX = "\n\nVui lòng trả lời câu hỏi một cách súc tích."
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Class chính: ReportAgent
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -137,7 +137,7 @@ class SimulationIPCClient:
|
|||
TimeoutError: Lỗi quá thời gian chờ phản hồi
|
||||
"""
|
||||
command_id = str(uuid.uuid4())
|
||||
command = IPCCommand(
|
||||
command = s(
|
||||
command_id=command_id,
|
||||
command_type=command_type,
|
||||
args=args
|
||||
|
|
|
|||
|
|
@ -21,16 +21,32 @@ from .simulation_config_generator import SimulationConfigGenerator, SimulationPa
|
|||
logger = get_logger('mirofish.simulation')
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# ENUM: SimulationStatus — Máy trạng thái (State Machine) của một Simulation
|
||||
# ==============================================================================
|
||||
# Mỗi simulation đi qua các trạng thái theo thứ tự:
|
||||
#
|
||||
# CREATED ──► PREPARING ──► READY ──► RUNNING ──► COMPLETED
|
||||
# │ │
|
||||
# └──────────────────────┴──► FAILED
|
||||
# │
|
||||
# └──► PAUSED ──► RUNNING (tiếp tục)
|
||||
# │
|
||||
# └──► STOPPED (người dùng chủ động dừng)
|
||||
#
|
||||
# Lưu ý: FAILED là trạng thái cuối cùng không thể tiếp tục — phải tạo simulation mới.
|
||||
# ==============================================================================
|
||||
|
||||
class SimulationStatus(str, Enum):
|
||||
"""Trạng thái hiện tại của quá trình mô phỏng"""
|
||||
CREATED = "created" # Đã khởi tạo
|
||||
PREPARING = "preparing" # Đang chuẩn bị (chuẩn bị dữ liệu/profile)
|
||||
READY = "ready" # Đã sẵn sàng chạy
|
||||
RUNNING = "running" # Đang xử lý giả lập
|
||||
PAUSED = "paused" # Tạm dừng
|
||||
STOPPED = "stopped" # Hệ thống mô phỏng bị người dùng chủ động dừng lại
|
||||
COMPLETED = "completed" # Quá trình mô phỏng kết thúc tự nhiên một cách thành công
|
||||
FAILED = "failed" # Bị lỗi hệ thống gián đoạn
|
||||
CREATED = "created" # Vừa được tạo, chưa làm gì cả
|
||||
PREPARING = "preparing" # Đang chạy prepare_simulation() — đọc entity, sinh profile, tạo config
|
||||
READY = "ready" # prepare_simulation() hoàn thành — sẵn sàng bấm nút chạy OASIS
|
||||
RUNNING = "running" # OASIS subprocess đang chạy, agents đang hành động
|
||||
PAUSED = "paused" # Người dùng ra lệnh pause qua IPC — OASIS tạm dừng vòng lặp
|
||||
STOPPED = "stopped" # Người dùng chủ động dừng — OASIS bị kill, kết quả còn lại được giữ
|
||||
COMPLETED = "completed" # OASIS chạy hết số vòng (max_rounds) và thoát thành công
|
||||
FAILED = "failed" # Exception không xử lý được — pipeline bị gián đoạn
|
||||
|
||||
|
||||
class PlatformType(str, Enum):
|
||||
|
|
@ -39,50 +55,74 @@ class PlatformType(str, Enum):
|
|||
REDDIT = "reddit"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# DATACLASS: SimulationState — "Hộp đen" lưu toàn bộ thông tin một simulation
|
||||
# ==============================================================================
|
||||
# Đây là đối tượng trung tâm được đọc/ghi liên tục trong suốt vòng đời simulation.
|
||||
# Nó được lưu ở hai nơi đồng thời:
|
||||
# 1. RAM: `SimulationManager._simulations` dict (tra cứu nhanh O(1))
|
||||
# 2. Disk: `<SIMULATION_DATA_DIR>/<simulation_id>/state.json` (bền vững qua restart)
|
||||
# ==============================================================================
|
||||
|
||||
@dataclass
|
||||
class SimulationState:
|
||||
"""Class lưu trữ cấu trúc Dữ liệu/Trạng thái của một lượt mô phỏng"""
|
||||
simulation_id: str
|
||||
project_id: str
|
||||
graph_id: str
|
||||
|
||||
# Cờ trạng thái bật/tắt nền tảng chạy
|
||||
|
||||
# --- Định danh ---
|
||||
simulation_id: str # UUID dạng "sim_<12 ký tự hex>", ví dụ: "sim_a3f8c21b004e"
|
||||
project_id: str # ID của Project chứa simulation này (quản lý nhiều sim cùng lúc)
|
||||
graph_id: str # ID của Zep Graph — xác định kho Entity nào sẽ dùng làm agent
|
||||
|
||||
# --- Công tắc nền tảng ---
|
||||
# Cho phép chọn chạy 1 hoặc cả 2 nền tảng. Nếu cả hai False → pipeline sẽ bỏ qua bước sinh profile.
|
||||
enable_twitter: bool = True
|
||||
enable_reddit: bool = True
|
||||
|
||||
# Current status
|
||||
|
||||
# --- Trạng thái vòng đời ---
|
||||
# Giá trị mặc định CREATED; được cập nhật qua _save_simulation_state() mỗi khi chuyển giai đoạn.
|
||||
status: SimulationStatus = SimulationStatus.CREATED
|
||||
|
||||
# Dữ liệu thu thập / thống kê của Preparing Phase
|
||||
entities_count: int = 0
|
||||
profiles_count: int = 0
|
||||
entity_types: List[str] = field(default_factory=list)
|
||||
|
||||
# Thông tin các nội dung cấu hình mà LLM đã tự động tạo
|
||||
config_generated: bool = False
|
||||
config_reasoning: str = ""
|
||||
|
||||
# Dữ liệu cập nhật theo thời gian thực (Runtime Phase)
|
||||
current_round: int = 0
|
||||
twitter_status: str = "not_started"
|
||||
reddit_status: str = "not_started"
|
||||
|
||||
# Nhãn Timestamp lịch sử
|
||||
|
||||
# --- Kết quả thống kê của giai đoạn PREPARING ---
|
||||
entities_count: int = 0 # Số entity đọc được từ Zep (sau khi lọc)
|
||||
profiles_count: int = 0 # Số agent profile đã sinh thành công
|
||||
entity_types: List[str] = field(default_factory=list) # Danh sách loại entity (ví dụ: ["Person", "Organization"])
|
||||
|
||||
# --- Kết quả sinh config ---
|
||||
config_generated: bool = False # True sau khi simulation_config.json được ghi ra đĩa
|
||||
config_reasoning: str = "" # Chuỗi giải thích của LLM tại sao chọn các tham số này
|
||||
|
||||
# --- Dữ liệu runtime (cập nhật theo thời gian thực trong khi RUNNING) ---
|
||||
current_round: int = 0 # Vòng hiện tại OASIS đang chạy (0-indexed)
|
||||
twitter_status: str = "not_started" # Trạng thái chi tiết của luồng Twitter
|
||||
reddit_status: str = "not_started" # Trạng thái chi tiết của luồng Reddit
|
||||
|
||||
# --- Timestamp lịch sử ---
|
||||
# Lưu dạng ISO 8601 (ví dụ: "2025-01-15T10:30:45.123456") để dễ parse lại
|
||||
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
||||
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
# Lịch sử thông báo Lỗi (nếu có để render trả về frontend)
|
||||
|
||||
# --- Lỗi ---
|
||||
# None nếu không có lỗi; nếu có chứa message dạng string để hiển thị cho frontend
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Tạo thành Dictionary đầy đủ nhất (Dùng cho việc lưu cấu hình local cho hệ thống bên trong đọc)"""
|
||||
"""
|
||||
Xuất toàn bộ state thành dict đầy đủ.
|
||||
|
||||
Dùng khi:
|
||||
- Ghi ra file state.json (để khôi phục lại khi server restart)
|
||||
- Truyền nội bộ giữa các service Python
|
||||
|
||||
Khác với to_simple_dict(): bản này có đầy đủ mọi trường,
|
||||
kể cả các trường nặng như config_reasoning và timestamps.
|
||||
"""
|
||||
return {
|
||||
"simulation_id": self.simulation_id,
|
||||
"project_id": self.project_id,
|
||||
"graph_id": self.graph_id,
|
||||
"enable_twitter": self.enable_twitter,
|
||||
"enable_reddit": self.enable_reddit,
|
||||
"status": self.status.value,
|
||||
"status": self.status.value, # Enum → string (ví dụ: "preparing")
|
||||
"entities_count": self.entities_count,
|
||||
"profiles_count": self.profiles_count,
|
||||
"entity_types": self.entity_types,
|
||||
|
|
@ -95,9 +135,14 @@ class SimulationState:
|
|||
"updated_at": self.updated_at,
|
||||
"error": self.error,
|
||||
}
|
||||
|
||||
|
||||
def to_simple_dict(self) -> Dict[str, Any]:
|
||||
"""Tạo thành Dictionary bao hàm các thông số vắn tắt hơn (Dùng cho API response trả về Client - Frontend)"""
|
||||
"""
|
||||
Xuất state thành dict gọn nhẹ cho API response trả về client/frontend.
|
||||
|
||||
Lược bỏ các trường nội bộ nặng (config_reasoning, timestamps, platform statuses...)
|
||||
để giảm payload JSON và không để lộ thông tin debug ra ngoài.
|
||||
"""
|
||||
return {
|
||||
"simulation_id": self.simulation_id,
|
||||
"project_id": self.project_id,
|
||||
|
|
@ -111,69 +156,122 @@ class SimulationState:
|
|||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# CLASS: SimulationManager — Trung tâm điều phối toàn bộ vòng đời simulation
|
||||
# ==============================================================================
|
||||
# SimulationManager là singleton (dùng chung 1 instance) được Flask khởi tạo lúc boot.
|
||||
# Nó KHÔNG chạy OASIS trực tiếp — việc đó do SimulationRunner đảm nhiệm.
|
||||
# Nhiệm vụ chính: chuẩn bị dữ liệu (prepare) và theo dõi trạng thái (state tracking).
|
||||
# ==============================================================================
|
||||
|
||||
class SimulationManager:
|
||||
"""
|
||||
Kịch bản Quản lý trung tâm của tính năng Mô Phỏng
|
||||
|
||||
|
||||
Luồng thiết lập cốt lõi:
|
||||
1. Trích xuất nhóm các Thực Thể (Entity) được định nghĩa sẵn trong hệ thống lưu trữ Graph của Zep.
|
||||
2. Chế lại thành các hồ sơ Profile thiết lập tiêu chuẩn của OASIS framework (Agent)
|
||||
3. Trao quyền cho sức mạnh mô hình LLM tự đánh giá số liệu rồi tự sinh ra cấu hình cài đặt cho quá trình mô phỏng
|
||||
4. Cài đặt các thư mục và tập tin tương ứng, phục vụ sẵn sàng để những Script lập trình riêng (pre-set script) có thể khai thác sử dụng.
|
||||
"""
|
||||
|
||||
# Nơi chứa thư mục chứa Dữ liệu mô phỏng Local
|
||||
|
||||
# Đường dẫn gốc nơi lưu tất cả dữ liệu simulation (mỗi sim có subfolder riêng).
|
||||
# __file__ = .../backend/app/services/simulation_manager.py
|
||||
# → join 2 cấp "../.." → .../backend/
|
||||
# → join "uploads/simulations" → .../backend/uploads/simulations/
|
||||
SIMULATION_DATA_DIR = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
os.path.dirname(__file__),
|
||||
'../../uploads/simulations'
|
||||
)
|
||||
|
||||
|
||||
def __init__(self):
|
||||
# Đảm bảo môi trường file data đã được set up
|
||||
# Tạo thư mục gốc nếu chưa tồn tại (exist_ok=True → không báo lỗi nếu đã có)
|
||||
os.makedirs(self.SIMULATION_DATA_DIR, exist_ok=True)
|
||||
|
||||
# Biến dictionary ở mức Application theo dõi trạng thái simulation qua Cache RAM.
|
||||
|
||||
# Cache RAM: dict ánh xạ simulation_id → SimulationState
|
||||
# Mục đích: tránh đọc file state.json mỗi lần có request API → tra cứu O(1)
|
||||
# Nhược điểm: mất dữ liệu khi server restart → có disk backup bù lại
|
||||
self._simulations: Dict[str, SimulationState] = {}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PRIVATE HELPERS — Quản lý file/thư mục và cơ chế hai lớp cache
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _get_simulation_dir(self, simulation_id: str) -> str:
|
||||
"""Lấy trả về các đường dẫn thư mục gốc tương ứng với Simulation ID"""
|
||||
"""
|
||||
Trả về đường dẫn thư mục chứa dữ liệu của một simulation cụ thể.
|
||||
Tự động tạo thư mục nếu chưa tồn tại (lazy creation).
|
||||
|
||||
Cấu trúc thư mục sau khi READY:
|
||||
uploads/simulations/<simulation_id>/
|
||||
├── state.json ← trạng thái vòng đời
|
||||
├── simulation_config.json ← tham số OASIS do LLM sinh
|
||||
├── reddit_profiles.json ← agent profiles cho Reddit
|
||||
└── twitter_profiles.csv ← agent profiles cho Twitter
|
||||
"""
|
||||
sim_dir = os.path.join(self.SIMULATION_DATA_DIR, simulation_id)
|
||||
os.makedirs(sim_dir, exist_ok=True)
|
||||
os.makedirs(sim_dir, exist_ok=True) # Idempotent: gọi nhiều lần vẫn an toàn
|
||||
return sim_dir
|
||||
|
||||
|
||||
def _save_simulation_state(self, state: SimulationState):
|
||||
"""Bật tính năng lưu state định dạng JSON ra ổ cứng"""
|
||||
"""
|
||||
Ghi state xuống disk VÀ cập nhật vào RAM cache đồng thời.
|
||||
|
||||
Đây là cơ chế "write-through cache":
|
||||
- Mỗi khi state thay đổi, cả RAM lẫn file đều được cập nhật ngay lập tức.
|
||||
- Đảm bảo: nếu server crash giữa chừng, state.json vẫn phản ánh trạng thái mới nhất.
|
||||
|
||||
Tự động cập nhật updated_at thành thời điểm hiện tại trước khi ghi.
|
||||
"""
|
||||
sim_dir = self._get_simulation_dir(state.simulation_id)
|
||||
state_file = os.path.join(sim_dir, "state.json")
|
||||
|
||||
|
||||
# Stamp thời gian ghi cuối cùng
|
||||
state.updated_at = datetime.now().isoformat()
|
||||
|
||||
|
||||
# Ghi JSON ra đĩa (ensure_ascii=False để Unicode/tiếng Việt không bị escape)
|
||||
with open(state_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(state.to_dict(), f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# Cập nhật đồng thời vào RAM cache
|
||||
self._simulations[state.simulation_id] = state
|
||||
|
||||
|
||||
def _load_simulation_state(self, simulation_id: str) -> Optional[SimulationState]:
|
||||
"""Load ngược lại data của tiến trình Mô Phỏng thông qua tệp cấu hình JSON"""
|
||||
"""
|
||||
Đọc state của một simulation — ưu tiên dùng RAM cache, fallback về disk.
|
||||
|
||||
Chiến lược "cache-aside":
|
||||
1. Kiểm tra RAM cache trước (nhanh, không I/O)
|
||||
2. Nếu không có trong RAM → tìm file state.json trên disk
|
||||
3. Nếu tìm thấy → deserialize thành SimulationState object, nạp vào RAM cache
|
||||
4. Nếu không tìm thấy file → trả về None
|
||||
|
||||
Trường hợp None: simulation_id không hợp lệ hoặc chưa từng được tạo.
|
||||
"""
|
||||
# --- Bước 1: Kiểm tra RAM cache ---
|
||||
if simulation_id in self._simulations:
|
||||
return self._simulations[simulation_id]
|
||||
|
||||
|
||||
# --- Bước 2: Tìm trên disk ---
|
||||
sim_dir = self._get_simulation_dir(simulation_id)
|
||||
state_file = os.path.join(sim_dir, "state.json")
|
||||
|
||||
|
||||
if not os.path.exists(state_file):
|
||||
return None
|
||||
|
||||
return None # Simulation này chưa từng tồn tại
|
||||
|
||||
# --- Bước 3: Deserialize từ JSON thành dataclass ---
|
||||
with open(state_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
|
||||
# Khôi phục từng field, dùng .get() với giá trị mặc định để tương thích ngược
|
||||
# khi có field mới được thêm vào SimulationState sau này
|
||||
state = SimulationState(
|
||||
simulation_id=simulation_id,
|
||||
project_id=data.get("project_id", ""),
|
||||
graph_id=data.get("graph_id", ""),
|
||||
enable_twitter=data.get("enable_twitter", True),
|
||||
enable_reddit=data.get("enable_reddit", True),
|
||||
status=SimulationStatus(data.get("status", "created")),
|
||||
status=SimulationStatus(data.get("status", "created")), # string → Enum
|
||||
entities_count=data.get("entities_count", 0),
|
||||
profiles_count=data.get("profiles_count", 0),
|
||||
entity_types=data.get("entity_types", []),
|
||||
|
|
@ -186,10 +284,15 @@ class SimulationManager:
|
|||
updated_at=data.get("updated_at", datetime.now().isoformat()),
|
||||
error=data.get("error"),
|
||||
)
|
||||
|
||||
|
||||
# --- Bước 4: Nạp vào RAM cache để lần sau không cần đọc file nữa ---
|
||||
self._simulations[simulation_id] = state
|
||||
return state
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC: create_simulation — Khởi tạo một simulation mới (trạng thái CREATED)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def create_simulation(
|
||||
self,
|
||||
project_id: str,
|
||||
|
|
@ -198,20 +301,27 @@ class SimulationManager:
|
|||
enable_reddit: bool = True,
|
||||
) -> SimulationState:
|
||||
"""
|
||||
Khởi tạo môi trường ảo / mới cho Mô Phỏng
|
||||
|
||||
Tạo một simulation mới với trạng thái ban đầu CREATED.
|
||||
|
||||
Chỉ tạo metadata — KHÔNG đọc entity, KHÔNG gọi LLM.
|
||||
Sau khi gọi hàm này, cần gọi tiếp prepare_simulation() để chuẩn bị dữ liệu.
|
||||
|
||||
Args:
|
||||
project_id: Mã ID của Project (Quản lý cấp đầu vào)
|
||||
graph_id: Đồ thị ID tương ứng lấy bên Zep
|
||||
enable_twitter: Công tắc (Bật/Tắt) luồng giả lập Twitter
|
||||
enable_reddit: Công tắc (Bật/Tắt) luồng giả lập Reddit
|
||||
|
||||
graph_id: Đồ thị ID tương ứng lấy bên Zep — quyết định kho Entity nào được dùng
|
||||
enable_twitter: Có sinh profile và chạy mô phỏng trên Twitter không?
|
||||
enable_reddit: Có sinh profile và chạy mô phỏng trên Reddit không?
|
||||
|
||||
Returns:
|
||||
Đối tượng Class SimulationState
|
||||
SimulationState với status=CREATED, chưa có entity/profile/config.
|
||||
"""
|
||||
import uuid
|
||||
|
||||
# Tạo ID ngắn gọn, dễ đọc: "sim_" + 12 ký tự hex ngẫu nhiên
|
||||
# Ví dụ: "sim_a3f8c21b004e" — đủ ngẫu nhiên để tránh va chạm trong phạm vi project
|
||||
simulation_id = f"sim_{uuid.uuid4().hex[:12]}"
|
||||
|
||||
|
||||
# Khởi tạo object state với giá trị mặc định
|
||||
state = SimulationState(
|
||||
simulation_id=simulation_id,
|
||||
project_id=project_id,
|
||||
|
|
@ -220,12 +330,17 @@ class SimulationManager:
|
|||
enable_reddit=enable_reddit,
|
||||
status=SimulationStatus.CREATED,
|
||||
)
|
||||
|
||||
|
||||
# Ghi ra disk ngay lập tức để đảm bảo bền vững
|
||||
self._save_simulation_state(state)
|
||||
logger.info(f"Created simulation: {simulation_id}, project={project_id}, graph={graph_id}")
|
||||
|
||||
|
||||
return state
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC: prepare_simulation — Pipeline chuẩn bị dữ liệu (3 giai đoạn chính)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def prepare_simulation(
|
||||
self,
|
||||
simulation_id: str,
|
||||
|
|
@ -238,169 +353,242 @@ class SimulationManager:
|
|||
) -> SimulationState:
|
||||
"""
|
||||
Giai đoạn chuẩn bị dữ liệu tạo giả lập mô phỏng (Tiến trình Automation 100%)
|
||||
|
||||
Các bước diễn ra:
|
||||
1. Gọi lấy các cụm Entity (thực thể) và bộ lọt (filter) từ Zep Graph API
|
||||
2. Tự động khởi tạo hàng loạt Agent Profile chạy OASIS tương ứng với Entity (Hỗ trợ gọi AI LLM để làm mượt văn bản / tăng tốc chạy song song)
|
||||
3. Hỏi và bắt bot LLM suy luận ra hệ tham số setting thông minh nhất (thời gian mô phỏng rò rỉ, hệ số tần suất nói chuyện hoạt động ...)
|
||||
4. In ra các file cấu hình và JSON của profile để hệ thống dễ đọc
|
||||
5. Copy nguyên các Scripts chuẩn được cấu hình sẵn (preset) ném vào thư mục để chạy
|
||||
|
||||
|
||||
Hàm này chạy trong một background thread (được Flask gọi không đồng bộ).
|
||||
Progress được báo cáo liên tục về frontend qua progress_callback.
|
||||
|
||||
3 giai đoạn chính:
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Giai đoạn 1: ĐỌC ENTITY từ Zep Graph │
|
||||
│ ZepEntityReader → lọc theo defined_entity_types → FilteredEntities │
|
||||
│ Nếu không có entity nào → FAILED (không thể tiếp tục) │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ Giai đoạn 2: SINH AGENT PROFILE │
|
||||
│ OasisProfileGenerator → gọi LLM song song (ThreadPoolExecutor) │
|
||||
│ → reddit_profiles.json + twitter_profiles.csv │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ Giai đoạn 3: SINH SIMULATION CONFIG │
|
||||
│ SimulationConfigGenerator → LLM phân tích → simulation_config.json │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Args:
|
||||
simulation_id: Mã ID của chu trình giả lập
|
||||
simulation_requirement: Chuỗi text từ người dùng yêu cầu mô phỏng gì (gửi cho config sinh cấu hình)
|
||||
document_text: Nội dung file Raw nguyên thủy (Cho LLM đánh giá context bối cảnh ban đầu)
|
||||
defined_entity_types: Dánh sách các Entity Model có sẵn do Zep định nghĩa (Option)
|
||||
use_llm_for_profiles: Toggle tính năng sử dụng mô hình LLM để buff thêm chi tiết cài đặt con Bot
|
||||
progress_callback: Hàm callback update log progress (chuyển về màn hình Frontend view) format (stage, progress, message)
|
||||
parallel_profile_count: Giới hạn concurrent threading chạy LLM gọi profile (Default là 3 luồng cùng lúc để làm nhanh hơn)
|
||||
|
||||
simulation_id: ID của simulation đã tạo bằng create_simulation()
|
||||
simulation_requirement: Yêu cầu mô phỏng từ người dùng (gửi cho LLM sinh config)
|
||||
document_text: Nội dung văn bản gốc (ngữ cảnh để LLM hiểu chủ đề đang mô phỏng)
|
||||
defined_entity_types: Danh sách loại entity cần lọc (None = lấy tất cả)
|
||||
use_llm_for_profiles: True = gọi LLM để làm giàu persona; False = dùng rule-based
|
||||
progress_callback: Hàm nhận (stage: str, percent: int, message: str, **kwargs)
|
||||
parallel_profile_count: Số thread LLM chạy song song khi sinh profile (mặc định 3)
|
||||
|
||||
Returns:
|
||||
Class cài đặt Data - SimulationState
|
||||
SimulationState với status=READY (thành công) hoặc status=FAILED (thất bại)
|
||||
|
||||
Raises:
|
||||
ValueError: Nếu simulation_id không tồn tại
|
||||
Exception: Re-raise mọi lỗi sau khi đã cập nhật state sang FAILED
|
||||
"""
|
||||
# Load state hiện tại — phải tồn tại trước (đã gọi create_simulation())
|
||||
state = self._load_simulation_state(simulation_id)
|
||||
if not state:
|
||||
raise ValueError(f"Giả lập với ID: {simulation_id} không tồn tại")
|
||||
|
||||
|
||||
try:
|
||||
# --- Chuyển trạng thái sang PREPARING ngay từ đầu ---
|
||||
# Quan trọng: ghi xuống disk trước để nếu crash sau đó,
|
||||
# frontend không bị thấy state "CREATED" mãi mãi
|
||||
state.status = SimulationStatus.PREPARING
|
||||
self._save_simulation_state(state)
|
||||
|
||||
|
||||
# Lấy đường dẫn thư mục để ghi các file output
|
||||
sim_dir = self._get_simulation_dir(simulation_id)
|
||||
|
||||
# ========== Giai đoạn 1: Kết nối lấy Node Data Entity và Sàng lọc ==========
|
||||
|
||||
# ==========================================================================
|
||||
# GIAI ĐOẠN 1: Kết nối Zep và lọc Entity
|
||||
# ==========================================================================
|
||||
# ZepEntityReader đọc TẤT CẢ nodes trong graph (có thể hàng nghìn nodes),
|
||||
# sau đó lọc chỉ giữ lại các node có label nằm trong defined_entity_types.
|
||||
# Kết quả trả về là FilteredEntities chứa danh sách EntityNode đã enrich.
|
||||
# Retry: 3 lần với exponential backoff (2s → 4s → 8s).
|
||||
# ==========================================================================
|
||||
if progress_callback:
|
||||
progress_callback("reading", 0, "Connecting to Zep Graph data store...")
|
||||
|
||||
|
||||
reader = ZepEntityReader()
|
||||
|
||||
|
||||
if progress_callback:
|
||||
progress_callback("reading", 30, "Extracting Node data from graph...")
|
||||
|
||||
|
||||
# filter_defined_entities: đọc nodes → lọc theo label → enrich với edges
|
||||
# enrich_with_edges=True → mỗi EntityNode sẽ có related_edges & related_nodes
|
||||
# (cần thiết để LLM hiểu mối quan hệ giữa các entity khi sinh persona)
|
||||
filtered = reader.filter_defined_entities(
|
||||
graph_id=state.graph_id,
|
||||
defined_entity_types=defined_entity_types,
|
||||
enrich_with_edges=True
|
||||
)
|
||||
|
||||
|
||||
# Ghi số liệu thống kê vào state
|
||||
state.entities_count = filtered.filtered_count
|
||||
state.entity_types = list(filtered.entity_types)
|
||||
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"reading", 100,
|
||||
"reading", 100,
|
||||
f"Extraction complete, filtered entity count: {filtered.filtered_count} empty entities",
|
||||
current=filtered.filtered_count,
|
||||
total=filtered.filtered_count
|
||||
)
|
||||
|
||||
|
||||
# --- Early exit: không có entity nào → không thể chạy simulation ---
|
||||
# Lý do có thể: graph chưa được populate, hoặc defined_entity_types quá hẹp
|
||||
if filtered.filtered_count == 0:
|
||||
state.status = SimulationStatus.FAILED
|
||||
state.error = "No valid entities extracted for simulation. Please check if the Graph was generated properly with valid text."
|
||||
self._save_simulation_state(state)
|
||||
return state
|
||||
|
||||
# ========== Giai đoạn 2: Bắt đầu sinh Agent Profiles cho OASIS ==========
|
||||
return state # Trả về sớm, không raise exception
|
||||
|
||||
# ==========================================================================
|
||||
# GIAI ĐOẠN 2: Sinh Agent Profile cho từng Entity
|
||||
# ==========================================================================
|
||||
# Mỗi EntityNode → 1 OasisAgentProfile (có user_id, bio, persona, mbti, ...)
|
||||
# Nếu use_llm=True: gọi LLM để viết bio/persona phong phú hơn
|
||||
# Nếu use_llm=False: dùng rule-based (nhanh hơn, ít tốn token hơn)
|
||||
# Chạy parallel_profile_count threads đồng thời để tăng tốc.
|
||||
# ==========================================================================
|
||||
total_entities = len(filtered.entities)
|
||||
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"generating_profiles", 0,
|
||||
"generating_profiles", 0,
|
||||
"Ready for AI Generation process...",
|
||||
current=0,
|
||||
total=total_entities
|
||||
)
|
||||
|
||||
# Gửi mã graph_id để bộ Profile có thể fetch thêm tài liệu nếu model cần lục vấn sâu
|
||||
|
||||
# Khởi tạo generator — truyền graph_id để nó có thể search Zep thêm nếu cần
|
||||
generator = OasisProfileGenerator(graph_id=state.graph_id)
|
||||
|
||||
|
||||
# Wrapper chuyển đổi format callback:
|
||||
# generate_profiles_from_entities gọi: callback(current_count, total, entity_name)
|
||||
# Nhưng prepare_simulation cần format: callback(stage, percent, message, ...)
|
||||
def profile_progress(current, total, msg):
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"generating_profiles",
|
||||
int(current / total * 100),
|
||||
"generating_profiles",
|
||||
int(current / total * 100), # Tính phần trăm hoàn thành
|
||||
msg,
|
||||
current=current,
|
||||
total=total,
|
||||
item_name=msg
|
||||
)
|
||||
|
||||
# Khai báo đường dẫn tạm để AI lưu Real-time kết quả (Đặt ưu tiên Platform Reddit JSON làm chuẩn)
|
||||
|
||||
# --- Xác định đường dẫn real-time output ---
|
||||
# Real-time output: trong khi LLM đang sinh profile, file được ghi dần dần
|
||||
# (không đợi hết rồi mới ghi một lần) → frontend có thể theo dõi tiến độ
|
||||
# Ưu tiên Reddit vì đó là format JSON chuẩn; Twitter dùng CSV (chỉ khi Reddit tắt)
|
||||
realtime_output_path = None
|
||||
realtime_platform = "reddit"
|
||||
if state.enable_reddit:
|
||||
realtime_output_path = os.path.join(sim_dir, "reddit_profiles.json")
|
||||
realtime_platform = "reddit"
|
||||
elif state.enable_twitter:
|
||||
# Chỉ đến đây nếu Reddit bị tắt hẳn
|
||||
realtime_output_path = os.path.join(sim_dir, "twitter_profiles.csv")
|
||||
realtime_platform = "twitter"
|
||||
|
||||
|
||||
# --- Xác định platform metadata (để tính cost API) ---
|
||||
# "parallel" = cả hai nền tảng đều bật → profiles được dùng cho cả hai
|
||||
if state.enable_twitter and state.enable_reddit:
|
||||
runtime_platform = "parallel"
|
||||
elif state.enable_twitter:
|
||||
runtime_platform = "twitter"
|
||||
elif state.enable_reddit:
|
||||
runtime_platform = "reddit"
|
||||
else:
|
||||
runtime_platform = None # Không nên xảy ra trong thực tế
|
||||
|
||||
# Gọi hàm sinh profile chính (blocking — chờ tất cả thread hoàn thành)
|
||||
profiles = generator.generate_profiles_from_entities(
|
||||
entities=filtered.entities,
|
||||
use_llm=use_llm_for_profiles,
|
||||
progress_callback=profile_progress,
|
||||
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
|
||||
graph_id=state.graph_id, # Để tìm kiếm trong Zep Search Index nếu cần
|
||||
parallel_count=parallel_profile_count, # Số thread LLM chạy đồng thời
|
||||
realtime_output_path=realtime_output_path, # Ghi từng profile vừa xong ngay
|
||||
output_platform=realtime_platform, # Định dạng file (json/csv)
|
||||
metadata_platform=runtime_platform, # Cho logging cost
|
||||
simulation_id=simulation_id,
|
||||
project_id=state.project_id,
|
||||
)
|
||||
|
||||
|
||||
state.profiles_count = len(profiles)
|
||||
|
||||
# Backup lại kết quả Profile (Twitter xuất ra text CSV, Reddit thì bắt buộc JSON cho cấu trúc OASIS)
|
||||
# Reddit đã được render đồng thời ở block trên nhưng đây là re-save toàn bộ
|
||||
|
||||
# --- Lưu lần cuối toàn bộ profiles ra file ---
|
||||
# (real-time output ở trên có thể bị thiếu nếu một thread crash)
|
||||
# Đây là lần ghi đảm bảo file hoàn chỉnh 100%
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"generating_profiles", 95,
|
||||
"generating_profiles", 95,
|
||||
"Compressing Profile data...",
|
||||
current=total_entities,
|
||||
total=total_entities
|
||||
)
|
||||
|
||||
|
||||
if state.enable_reddit:
|
||||
# Reddit: JSON array các object profile (cấu trúc OASIS yêu cầu)
|
||||
generator.save_profiles(
|
||||
profiles=profiles,
|
||||
file_path=os.path.join(sim_dir, "reddit_profiles.json"),
|
||||
platform="reddit"
|
||||
)
|
||||
|
||||
|
||||
if state.enable_twitter:
|
||||
# Riêng Twitter với code Script base OAsis của họ yêu cầu CSV format
|
||||
# Twitter: CSV với header row (cấu trúc OASIS gốc yêu cầu)
|
||||
generator.save_profiles(
|
||||
profiles=profiles,
|
||||
file_path=os.path.join(sim_dir, "twitter_profiles.csv"),
|
||||
platform="twitter"
|
||||
)
|
||||
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"generating_profiles", 100,
|
||||
"generating_profiles", 100,
|
||||
f"Done, created {len(profiles)} Profiles",
|
||||
current=len(profiles),
|
||||
total=len(profiles)
|
||||
)
|
||||
|
||||
# ========== Giai đoạn 3: Uỷ thác cho LLM phân tích và xuất tham số mô phỏng ==========
|
||||
|
||||
# ==========================================================================
|
||||
# GIAI ĐOẠN 3: Sinh simulation_config.json bằng LLM
|
||||
# ==========================================================================
|
||||
# SimulationConfigGenerator gọi LLM với:
|
||||
# - simulation_requirement (yêu cầu của user)
|
||||
# - document_text (văn bản nguồn)
|
||||
# - entities (danh sách entity)
|
||||
# LLM trả về SimulationParameters gồm: time_config, event_config, agent_configs
|
||||
# Toàn bộ được serialize thành JSON và ghi ra simulation_config.json.
|
||||
# File này sau đó được đọc bởi run_parallel_simulation.py khi OASIS chạy.
|
||||
# ==========================================================================
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"generating_config", 0,
|
||||
"generating_config", 0,
|
||||
"Analyzing input requirements...",
|
||||
current=0,
|
||||
total=3
|
||||
)
|
||||
|
||||
|
||||
config_generator = SimulationConfigGenerator()
|
||||
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"generating_config", 30,
|
||||
"generating_config", 30,
|
||||
"LLM Bot is generating configuration...",
|
||||
current=1,
|
||||
total=3
|
||||
)
|
||||
|
||||
|
||||
# generate_config: gọi LLM tạo time_config → event_config → agent_configs
|
||||
# (3 lần gọi LLM riêng biệt, mỗi lần có retry với exponential backoff)
|
||||
sim_params = config_generator.generate_config(
|
||||
simulation_id=simulation_id,
|
||||
project_id=state.project_id,
|
||||
|
|
@ -411,106 +599,175 @@ class SimulationManager:
|
|||
enable_twitter=state.enable_twitter,
|
||||
enable_reddit=state.enable_reddit
|
||||
)
|
||||
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"generating_config", 70,
|
||||
"generating_config", 70,
|
||||
"Saving Config parameters...",
|
||||
current=2,
|
||||
total=3
|
||||
)
|
||||
|
||||
# Lưu file cứng simulation_config.json
|
||||
|
||||
# Ghi simulation_config.json ra thư mục của simulation
|
||||
config_path = os.path.join(sim_dir, "simulation_config.json")
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
f.write(sim_params.to_json())
|
||||
|
||||
f.write(sim_params.to_json()) # to_json() đã xử lý serialize Enum, nested dict
|
||||
|
||||
# Đánh dấu config đã sinh xong và lưu lý do LLM giải thích
|
||||
state.config_generated = True
|
||||
state.config_reasoning = sim_params.generation_reasoning
|
||||
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
"generating_config", 100,
|
||||
"generating_config", 100,
|
||||
"Configuration Generation complete",
|
||||
current=3,
|
||||
total=3
|
||||
)
|
||||
|
||||
# Lưu ý kiến trúc: Các scripts thao tác thực thi vẫn để gốc ở `backend/scripts/`, SẼ KHÔNG CẦN chép đè sang folder Project
|
||||
# Tại thời gian Khởi chạy, `simulation_runner` sẽ nạp base chạy thẳng từ folder `scripts/` đó.
|
||||
|
||||
# Cập nhật status
|
||||
|
||||
# ==========================================================================
|
||||
# LƯU Ý KIẾN TRÚC QUAN TRỌNG:
|
||||
# Scripts OASIS (run_parallel_simulation.py, v.v.) vẫn để GỐC tại
|
||||
# `backend/scripts/` — KHÔNG copy sang thư mục simulation.
|
||||
# Khi SimulationRunner.start() chạy, nó sẽ gọi thẳng script gốc và
|
||||
# truyền --config <config_path> để script đọc đúng dữ liệu simulation.
|
||||
# Lý do: tránh code duplication, dễ update script mà không ảnh hưởng
|
||||
# các simulation đã chuẩn bị sẵn.
|
||||
# ==========================================================================
|
||||
|
||||
# --- Chuyển trạng thái sang READY — pipeline hoàn tất ---
|
||||
state.status = SimulationStatus.READY
|
||||
self._save_simulation_state(state)
|
||||
|
||||
|
||||
logger.info(f"Finished simulation preparation phase for ID: {simulation_id}, "
|
||||
f"Total entities={state.entities_count}, Created profiles={state.profiles_count}")
|
||||
|
||||
|
||||
return state
|
||||
|
||||
|
||||
except Exception as e:
|
||||
# --- Xử lý lỗi toàn cục: bất kỳ exception nào cũng → FAILED ---
|
||||
# Ghi lại traceback đầy đủ để debug, sau đó re-raise để Flask xử lý tiếp
|
||||
logger.error(f"Error occurred during Simulation preparation (Sim ID: {simulation_id}), ERROR CODE: {str(e)}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# Cập nhật state FAILED với message lỗi để frontend hiển thị
|
||||
state.status = SimulationStatus.FAILED
|
||||
state.error = str(e)
|
||||
self._save_simulation_state(state)
|
||||
raise
|
||||
|
||||
|
||||
raise # Re-raise để Flask API handler nhận và trả HTTP 500
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC UTILITIES — Các hàm đọc/truy vấn trạng thái
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def get_simulation(self, simulation_id: str) -> Optional[SimulationState]:
|
||||
"""Đọc và lấy State hiện tại của Simulator"""
|
||||
"""
|
||||
Lấy trạng thái hiện tại của một simulation.
|
||||
Dùng cache-aside (RAM trước, disk fallback) nên rất nhanh.
|
||||
Trả về None nếu simulation_id không tồn tại.
|
||||
"""
|
||||
return self._load_simulation_state(simulation_id)
|
||||
|
||||
|
||||
def list_simulations(self, project_id: Optional[str] = None) -> List[SimulationState]:
|
||||
"""Liệt kê toàn bộ danh sách các Mô Phỏng (Simulations) đã khởi tạo"""
|
||||
"""
|
||||
Liệt kê tất cả simulations, có thể lọc theo project_id.
|
||||
|
||||
Duyệt qua toàn bộ thư mục con trong SIMULATION_DATA_DIR,
|
||||
load state.json của từng cái, rồi lọc theo project_id nếu có yêu cầu.
|
||||
|
||||
Args:
|
||||
project_id: Nếu None → trả về tất cả; nếu có → chỉ trả về của project đó
|
||||
|
||||
Returns:
|
||||
Danh sách SimulationState (có thể rỗng nếu chưa có simulation nào)
|
||||
"""
|
||||
simulations = []
|
||||
|
||||
|
||||
if os.path.exists(self.SIMULATION_DATA_DIR):
|
||||
for sim_id in os.listdir(self.SIMULATION_DATA_DIR):
|
||||
# Loại bỏ các folder/file rác do hệ điều hành sinh ra (ví dụ: .DS_Store của macOS) hoặc không phải thư mục
|
||||
# Bỏ qua các file ẩn (ví dụ: .DS_Store của macOS, .gitkeep)
|
||||
# và các entry không phải thư mục (ví dụ: file rác nào đó)
|
||||
sim_path = os.path.join(self.SIMULATION_DATA_DIR, sim_id)
|
||||
if sim_id.startswith('.') or not os.path.isdir(sim_path):
|
||||
continue
|
||||
|
||||
|
||||
state = self._load_simulation_state(sim_id)
|
||||
if state:
|
||||
# Lọc theo project_id nếu được chỉ định
|
||||
if project_id is None or state.project_id == project_id:
|
||||
simulations.append(state)
|
||||
|
||||
|
||||
return simulations
|
||||
|
||||
|
||||
def get_profiles(self, simulation_id: str, platform: str = "reddit") -> List[Dict[str, Any]]:
|
||||
"""Lấy/Tải dữ liệu Agent Profile do AI sinh ra dựa theo nền tảng mạng xã hội"""
|
||||
"""
|
||||
Đọc và trả về danh sách agent profiles đã sinh cho một simulation.
|
||||
|
||||
Chỉ hoạt động sau khi prepare_simulation() hoàn thành (status=READY trở lên).
|
||||
Trả về [] nếu file chưa tồn tại (simulation chưa READY hoặc nền tảng bị tắt).
|
||||
|
||||
Args:
|
||||
simulation_id: ID của simulation
|
||||
platform: "reddit" → đọc reddit_profiles.json | "twitter" → đọc twitter_profiles.json
|
||||
(Twitter thực ra lưu .csv nhưng hàm này chỉ dùng cho Reddit JSON)
|
||||
|
||||
Returns:
|
||||
List của các dict profile (mỗi dict là 1 agent)
|
||||
"""
|
||||
state = self._load_simulation_state(simulation_id)
|
||||
if not state:
|
||||
raise ValueError(f"Simulation with ID {simulation_id} does not exist")
|
||||
|
||||
|
||||
sim_dir = self._get_simulation_dir(simulation_id)
|
||||
profile_path = os.path.join(sim_dir, f"{platform}_profiles.json")
|
||||
|
||||
|
||||
if not os.path.exists(profile_path):
|
||||
return []
|
||||
|
||||
return [] # File chưa được sinh, trả về rỗng thay vì raise lỗi
|
||||
|
||||
with open(profile_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_simulation_config(self, simulation_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Lấy thông số cấu hình của bản mô phỏng"""
|
||||
"""
|
||||
Đọc và trả về nội dung simulation_config.json của một simulation.
|
||||
|
||||
Trả về None nếu config chưa được sinh (chưa qua giai đoạn 3 của prepare_simulation).
|
||||
Dữ liệu trả về là dict Python (đã parse JSON), không phải string.
|
||||
"""
|
||||
sim_dir = self._get_simulation_dir(simulation_id)
|
||||
config_path = os.path.join(sim_dir, "simulation_config.json")
|
||||
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
return None
|
||||
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_run_instructions(self, simulation_id: str) -> Dict[str, str]:
|
||||
"""Output ra hướng dẫn / Các câu lệnh dòng lệnh (CMD) để thực thi chạy bản đồ mô phỏng này"""
|
||||
"""
|
||||
Tạo ra các câu lệnh terminal để chạy simulation này thủ công.
|
||||
|
||||
Hữu ích khi developer muốn chạy OASIS trực tiếp ngoài Flask,
|
||||
hoặc để debug từng nền tảng riêng lẻ.
|
||||
|
||||
Returns:
|
||||
Dict gồm:
|
||||
- simulation_dir: thư mục chứa dữ liệu simulation
|
||||
- scripts_dir: thư mục chứa các script OASIS
|
||||
- config_file: đường dẫn đầy đủ đến simulation_config.json
|
||||
- commands: dict các lệnh python để chạy từng loại
|
||||
- instructions: chuỗi hướng dẫn dạng đọc được cho con người
|
||||
"""
|
||||
sim_dir = self._get_simulation_dir(simulation_id)
|
||||
config_path = os.path.join(sim_dir, "simulation_config.json")
|
||||
|
||||
# Tính đường dẫn tuyệt đối đến thư mục scripts từ vị trí file này
|
||||
# __file__ = .../backend/app/services/simulation_manager.py
|
||||
# → abspath("../../scripts") = .../backend/scripts/
|
||||
scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../scripts'))
|
||||
|
||||
|
||||
return {
|
||||
"simulation_dir": sim_dir,
|
||||
"scripts_dir": scripts_dir,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -54,7 +54,7 @@ class TextProcessor:
|
|||
# Xoá các dòng trống liên tiếp (Chỉ giữ lại tối đa 2 lần xuống dòng liên tiếp)
|
||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
||||
|
||||
# 移除行首行尾空白
|
||||
# Loại bỏ khoảng trắng đầu và cuối
|
||||
lines = [line.strip() for line in text.split('\n')]
|
||||
text = '\n'.join(lines)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,33 @@
|
|||
"""
|
||||
Dịch vụ đọc và lọc thực thể Zep
|
||||
Đọc các node từ đồ thị Zep, lọc ra các node phù hợp với các loại thực thể đã được định nghĩa trước
|
||||
|
||||
Vị trí trong pipeline (ai gọi file này?):
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
simulation_manager.py → filter_defined_entities() [caller chính]
|
||||
└─ prepare_simulation() gọi ở Giai đoạn 1 để lấy entity làm agent
|
||||
|
||||
oasis_profile_generator.py → get_entity_with_context()
|
||||
└─ khi cần đọc sâu thêm ngữ cảnh của 1 entity cụ thể để sinh persona
|
||||
|
||||
simulation_config_generator.py → nhận FilteredEntities.entities làm input
|
||||
└─ không gọi trực tiếp, dùng kết quả đã lọc từ simulation_manager
|
||||
|
||||
api/simulation.py → qua SimulationManager, không gọi trực tiếp
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Luồng dữ liệu từ Zep vào hệ thống:
|
||||
Zep Graph (cloud)
|
||||
│
|
||||
├── graph.node.get_by_graph_id() ─→ get_all_nodes() ─┐
|
||||
│ (phân trang qua zep_paging.fetch_all_nodes) │
|
||||
│ ├→ filter_defined_entities()
|
||||
└── graph.edge.get_by_graph_id() ─→ get_all_edges() ─┘
|
||||
(phân trang qua zep_paging.fetch_all_edges)
|
||||
│
|
||||
▼
|
||||
FilteredEntities
|
||||
└─ entities: List[EntityNode] ← input cho OasisProfileGenerator
|
||||
"""
|
||||
|
||||
import time
|
||||
|
|
@ -15,24 +42,52 @@ from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
|
|||
|
||||
logger = get_logger('mirofish.zep_entity_reader')
|
||||
|
||||
# Dùng cho các kiểu trả về generic
|
||||
# TypeVar để _call_with_retry có thể trả về đúng kiểu dữ liệu của hàm được truyền vào
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# DATACLASS: EntityNode — Đơn vị dữ liệu cơ bản của một thực thể
|
||||
# ==============================================================================
|
||||
# Mỗi EntityNode tương ứng với 1 node trong Zep Graph, đã được enrich thêm
|
||||
# thông tin về edges và các node liên kết lân cận.
|
||||
#
|
||||
# Sau khi filter_defined_entities() chạy xong, mỗi EntityNode này sẽ được
|
||||
# OasisProfileGenerator chuyển đổi thành 1 OasisAgentProfile (1 agent trong OASIS).
|
||||
# ==============================================================================
|
||||
|
||||
@dataclass
|
||||
class EntityNode:
|
||||
"""Cấu trúc dữ liệu của node thực thể"""
|
||||
uuid: str
|
||||
name: str
|
||||
|
||||
# --- Định danh từ Zep ---
|
||||
uuid: str # UUID của node trong Zep (dùng để lookup edges)
|
||||
name: str # Tên hiển thị của entity (ví dụ: "Nguyễn Văn A", "Bộ Giáo Dục")
|
||||
|
||||
# --- Phân loại ---
|
||||
# Zep gắn label mặc định "Entity" cho mọi node.
|
||||
# Node có loại cụ thể sẽ có thêm label như "Person", "Organization", "Event"...
|
||||
# Ví dụ: ["Entity", "Person"] hoặc ["Entity", "Organization"]
|
||||
labels: List[str]
|
||||
|
||||
# --- Nội dung ---
|
||||
# summary: Đoạn mô tả tóm tắt về entity, do Zep tự sinh khi trích xuất từ văn bản
|
||||
summary: str
|
||||
# attributes: Các thuộc tính bổ sung dạng key-value (tuổi, nghề nghiệp, v.v.)
|
||||
attributes: Dict[str, Any]
|
||||
# Thông tin edge liên quan
|
||||
|
||||
# --- Ngữ cảnh mối quan hệ (được enrich khi enrich_with_edges=True) ---
|
||||
# related_edges: Danh sách các quan hệ mà entity này tham gia
|
||||
# Mỗi edge có: direction ("incoming"/"outgoing"), edge_name, fact, target/source_node_uuid
|
||||
# Ví dụ: {"direction": "outgoing", "edge_name": "WORKS_AT", "fact": "A làm việc tại B", ...}
|
||||
related_edges: List[Dict[str, Any]] = field(default_factory=list)
|
||||
# Thông tin các node khác liên quan
|
||||
|
||||
# related_nodes: Thông tin cơ bản của các node ở đầu kia của related_edges
|
||||
# Giúp LLM biết entity này liên kết với những ai/gì mà không cần fetch thêm
|
||||
related_nodes: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize toàn bộ EntityNode thành dict — dùng để truyền vào LLM prompt."""
|
||||
return {
|
||||
"uuid": self.uuid,
|
||||
"name": self.name,
|
||||
|
|
@ -42,23 +97,41 @@ class EntityNode:
|
|||
"related_edges": self.related_edges,
|
||||
"related_nodes": self.related_nodes,
|
||||
}
|
||||
|
||||
|
||||
def get_entity_type(self) -> Optional[str]:
|
||||
"""Lấy loại thực thể (loại trừ nhãn Entity mặc định)"""
|
||||
"""
|
||||
Trả về loại thực thể đầu tiên tìm thấy (loại trừ label mặc định "Entity" và "Node").
|
||||
|
||||
Ví dụ:
|
||||
labels = ["Entity", "Person"] → trả về "Person"
|
||||
labels = ["Entity", "Node"] → trả về None (không có loại cụ thể)
|
||||
labels = ["Entity"] → trả về None
|
||||
|
||||
Dùng để nhóm agent theo loại khi sinh prompt.
|
||||
"""
|
||||
for label in self.labels:
|
||||
if label not in ["Entity", "Node"]:
|
||||
return label
|
||||
return None
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# DATACLASS: FilteredEntities — Kết quả đầu ra của bước đọc và lọc
|
||||
# ==============================================================================
|
||||
# Đây là "gói hàng" được trả về cho simulation_manager.py sau khi đọc Zep.
|
||||
# simulation_manager lưu entities_count và entity_types vào SimulationState,
|
||||
# rồi truyền entities vào OasisProfileGenerator và SimulationConfigGenerator.
|
||||
# ==============================================================================
|
||||
|
||||
@dataclass
|
||||
class FilteredEntities:
|
||||
"""Tập hợp các thực thể sau khi lọc"""
|
||||
entities: List[EntityNode]
|
||||
entity_types: Set[str]
|
||||
total_count: int
|
||||
filtered_count: int
|
||||
|
||||
|
||||
entities: List[EntityNode] # Danh sách entity đã lọc và enrich — input cho profile/config generator
|
||||
entity_types: Set[str] # Tập hợp các loại entity có mặt (ví dụ: {"Person", "Organization"})
|
||||
total_count: int # Tổng số node đọc được từ Zep (trước khi lọc)
|
||||
filtered_count: int # Số node còn lại sau khi lọc (bằng len(entities))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"entities": [e.to_dict() for e in self.entities],
|
||||
|
|
@ -68,45 +141,72 @@ class FilteredEntities:
|
|||
}
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# CLASS: ZepEntityReader — Cầu nối giữa Zep Cloud API và pipeline MiroFish
|
||||
# ==============================================================================
|
||||
# Khởi tạo 1 instance mỗi lần gọi prepare_simulation() (stateless, không có cache).
|
||||
# Toàn bộ kết nối Zep đi qua đây — không service nào khác gọi trực tiếp Zep client
|
||||
# để đọc nodes/edges.
|
||||
# ==============================================================================
|
||||
|
||||
class ZepEntityReader:
|
||||
"""
|
||||
Dịch vụ đọc và lọc thực thể Zep
|
||||
|
||||
|
||||
Chức năng chính:
|
||||
1. Đọc toàn bộ các node từ đồ thị Zep
|
||||
2. Lọc ra các node phù hợp với các loại thực thể đã được định nghĩa (Các node có Labels không chỉ là Entity)
|
||||
3. Lấy ra thông tin edge cũng như các node liên quan đối với từng thực thể
|
||||
1. Đọc toàn bộ các node từ đồ thị Zep (có phân trang, tối đa 2000 nodes)
|
||||
2. Lọc ra các node phù hợp với các loại thực thể đã được định nghĩa
|
||||
(Các node có Labels không chỉ là "Entity"/"Node")
|
||||
3. Enrich mỗi entity với edges và related_nodes lân cận
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
# Ưu tiên api_key được truyền vào trực tiếp; fallback về biến môi trường
|
||||
self.api_key = api_key or Config.ZEP_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("ZEP_API_KEY is not configured")
|
||||
|
||||
|
||||
self.client = Zep(api_key=self.api_key)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PRIVATE: _call_with_retry — Retry wrapper cho các API call Zep đơn lẻ
|
||||
# --------------------------------------------------------------------------
|
||||
# Lưu ý: hàm này CHỈ được dùng bởi get_node_edges() và get_entity_with_context().
|
||||
# filter_defined_entities() KHÔNG dùng hàm này — nó dùng fetch_all_nodes/edges
|
||||
# từ zep_paging.py, vốn đã có retry riêng ở cấp trang.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _call_with_retry(
|
||||
self,
|
||||
func: Callable[[], T],
|
||||
self,
|
||||
func: Callable[[], T],
|
||||
operation_name: str,
|
||||
max_retries: int = 3,
|
||||
initial_delay: float = 2.0
|
||||
) -> T:
|
||||
"""
|
||||
Gọi hàm Zep API có cơ chế thử lại (retry)
|
||||
|
||||
Gọi một hàm Zep API với cơ chế thử lại (exponential backoff).
|
||||
|
||||
Chiến lược retry:
|
||||
Lần 1: chạy ngay
|
||||
Lần 2 (nếu lần 1 lỗi): chờ 2 giây
|
||||
Lần 3 (nếu lần 2 lỗi): chờ 4 giây
|
||||
Sau 3 lần đều lỗi → raise exception cuối cùng
|
||||
|
||||
Args:
|
||||
func: Hàm cần thực thi (lambda không tham số hoặc callable)
|
||||
operation_name: Tên thao tác, dùng cho log
|
||||
max_retries: Số lần thử lại tối đa (mặc định 3 lần, tức là thử tối đa 3 lần)
|
||||
initial_delay: Số giây trì hoãn ban đầu
|
||||
|
||||
func: Lambda/callable không tham số bọc lệnh gọi API
|
||||
operation_name: Tên thao tác để ghi vào log (ví dụ: "Fetch node edges")
|
||||
max_retries: Số lần thử tối đa (mặc định 3)
|
||||
initial_delay: Delay ban đầu tính bằng giây (tự nhân đôi mỗi lần)
|
||||
|
||||
Returns:
|
||||
Kết quả của lệnh gọi API
|
||||
Kết quả trả về từ func() nếu thành công
|
||||
|
||||
Raises:
|
||||
Exception: Exception cuối cùng sau khi hết số lần thử
|
||||
"""
|
||||
last_exception = None
|
||||
delay = initial_delay
|
||||
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func()
|
||||
|
|
@ -118,26 +218,45 @@ class ZepEntityReader:
|
|||
f"retrying in {delay:.1f} seconds..."
|
||||
)
|
||||
time.sleep(delay)
|
||||
delay *= 2 # Lùi bước nhịp mũ (Exponential backoff)
|
||||
delay *= 2 # Exponential backoff: 2s → 4s → 8s
|
||||
else:
|
||||
logger.error(f"Zep {operation_name} failed after {max_retries} attempts: {str(e)}")
|
||||
|
||||
|
||||
raise last_exception
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC: get_all_nodes / get_all_edges — Bulk fetch toàn bộ nodes và edges
|
||||
# --------------------------------------------------------------------------
|
||||
# Hai hàm này là wrapper mỏng quanh fetch_all_nodes/fetch_all_edges từ
|
||||
# zep_paging.py. Logic phân trang và retry theo trang nằm hoàn toàn trong
|
||||
# zep_paging.py (UUID cursor-based pagination, tối đa 2000 nodes).
|
||||
#
|
||||
# Tại sao dùng bulk fetch thay vì fetch từng node?
|
||||
# → Hiệu quả hơn nhiều: 2 API calls cho toàn bộ graph thay vì N calls
|
||||
# → filter_defined_entities() dùng chiến lược này
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def get_all_nodes(self, graph_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Lấy toàn bộ các node của đồ thị (có phân trang)
|
||||
Lấy toàn bộ các node của đồ thị bằng cách phân trang.
|
||||
|
||||
Uỷ thác cho fetch_all_nodes() (zep_paging.py) để xử lý:
|
||||
- Cursor-based pagination (100 nodes/trang)
|
||||
- Retry từng trang khi lỗi mạng
|
||||
- Giới hạn tổng 2000 nodes
|
||||
|
||||
Args:
|
||||
graph_id: ID của đồ thị
|
||||
graph_id: ID của Zep Graph
|
||||
|
||||
Returns:
|
||||
Danh sách node
|
||||
Danh sách dict, mỗi dict là 1 node với: uuid, name, labels, summary, attributes
|
||||
"""
|
||||
logger.info(f"Fetching all nodes for graph {graph_id}...")
|
||||
|
||||
nodes = fetch_all_nodes(self.client, graph_id)
|
||||
|
||||
# Chuẩn hoá từ Zep SDK object sang Python dict thuần
|
||||
# getattr với 2 tên vì Zep SDK đôi khi dùng uuid_ thay vì uuid để tránh conflict keyword
|
||||
nodes_data = []
|
||||
for node in nodes:
|
||||
nodes_data.append({
|
||||
|
|
@ -153,13 +272,17 @@ class ZepEntityReader:
|
|||
|
||||
def get_all_edges(self, graph_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Lấy toàn bộ các edge của đồ thị (có phân trang)
|
||||
Lấy toàn bộ các edge của đồ thị bằng cách phân trang.
|
||||
|
||||
Tương tự get_all_nodes() — uỷ thác cho fetch_all_edges() (zep_paging.py).
|
||||
Không có giới hạn tổng số edge (khác với nodes giới hạn 2000).
|
||||
|
||||
Args:
|
||||
graph_id: ID của đồ thị
|
||||
graph_id: ID của Zep Graph
|
||||
|
||||
Returns:
|
||||
Danh sách edge
|
||||
Danh sách dict, mỗi dict là 1 edge với:
|
||||
uuid, name, fact, source_node_uuid, target_node_uuid, attributes
|
||||
"""
|
||||
logger.info(f"Fetching all edges for graph {graph_id}...")
|
||||
|
||||
|
|
@ -170,32 +293,45 @@ class ZepEntityReader:
|
|||
edges_data.append({
|
||||
"uuid": getattr(edge, 'uuid_', None) or getattr(edge, 'uuid', ''),
|
||||
"name": edge.name or "",
|
||||
"fact": edge.fact or "",
|
||||
"source_node_uuid": edge.source_node_uuid,
|
||||
"target_node_uuid": edge.target_node_uuid,
|
||||
"fact": edge.fact or "", # Mô tả quan hệ dạng câu (ví dụ: "A làm việc tại B")
|
||||
"source_node_uuid": edge.source_node_uuid, # UUID node nguồn
|
||||
"target_node_uuid": edge.target_node_uuid, # UUID node đích
|
||||
"attributes": edge.attributes or {},
|
||||
})
|
||||
|
||||
logger.info(f"Total {len(edges_data)} edges fetched")
|
||||
return edges_data
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC: get_node_edges — Fetch edges của 1 node cụ thể (per-node API call)
|
||||
# --------------------------------------------------------------------------
|
||||
# ĐÂY KHÔNG PHẢI hàm được dùng trong filter_defined_entities().
|
||||
# filter_defined_entities() lấy ALL edges rồi tự scan — không gọi hàm này.
|
||||
#
|
||||
# Hàm này chỉ được dùng bởi get_entity_with_context() khi cần fetch
|
||||
# edges cho 1 entity đơn lẻ theo UUID cụ thể.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def get_node_edges(self, node_uuid: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Lấy tất cả các edge liên quan của node được chỉ định (có cơ chế thử lại)
|
||||
|
||||
Lấy tất cả edges liên quan của 1 node cụ thể qua UUID.
|
||||
|
||||
Dùng Zep API: client.graph.node.get_entity_edges(node_uuid=...)
|
||||
Có retry qua _call_with_retry() (không qua zep_paging vì không phân trang).
|
||||
|
||||
Args:
|
||||
node_uuid: UUID của node
|
||||
|
||||
node_uuid: UUID của node cần lấy edges
|
||||
|
||||
Returns:
|
||||
Danh sách edge
|
||||
Danh sách edge dict (cùng format với get_all_edges()).
|
||||
Trả về [] nếu lỗi (không raise exception — caller tự xử lý thiếu data).
|
||||
"""
|
||||
try:
|
||||
# Sử dụng cơ chế thử lại để gọi Zep API
|
||||
edges = self._call_with_retry(
|
||||
func=lambda: self.client.graph.node.get_entity_edges(node_uuid=node_uuid),
|
||||
operation_name=f"Fetch node edges(node={node_uuid[:8]}...)"
|
||||
)
|
||||
|
||||
|
||||
edges_data = []
|
||||
for edge in edges:
|
||||
edges_data.append({
|
||||
|
|
@ -206,71 +342,149 @@ class ZepEntityReader:
|
|||
"target_node_uuid": edge.target_node_uuid,
|
||||
"attributes": edge.attributes or {},
|
||||
})
|
||||
|
||||
|
||||
return edges_data
|
||||
except Exception as e:
|
||||
# Lỗi khi fetch edge của 1 node không nên làm hỏng cả pipeline
|
||||
# → log warning và trả về rỗng, caller (get_entity_with_context) tiếp tục
|
||||
logger.warning(f"Failed to fetch edges for node {node_uuid}: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC: filter_defined_entities — Hàm CHÍNH của class
|
||||
# --------------------------------------------------------------------------
|
||||
# Đây là hàm được gọi bởi simulation_manager.prepare_simulation() ở Giai đoạn 1.
|
||||
# Toàn bộ pipeline phụ thuộc vào output của hàm này.
|
||||
#
|
||||
# Thuật toán:
|
||||
# 1. Bulk fetch TẤT CẢ nodes (2 API calls qua phân trang)
|
||||
# 2. Bulk fetch TẤT CẢ edges (nếu enrich_with_edges=True)
|
||||
# 3. Duyệt từng node, áp dụng logic lọc label
|
||||
# 4. Với mỗi node hợp lệ: scan toàn bộ all_edges để tìm edges liên quan
|
||||
# 5. Trả về FilteredEntities
|
||||
#
|
||||
# Độ phức tạp: O(N * M) với N = số node lọc được, M = tổng số edge
|
||||
# Trong thực tế graph nhỏ (< 100 nodes, < 500 edges) nên không ảnh hưởng
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def filter_defined_entities(
|
||||
self,
|
||||
self,
|
||||
graph_id: str,
|
||||
defined_entity_types: Optional[List[str]] = None,
|
||||
enrich_with_edges: bool = True
|
||||
) -> FilteredEntities:
|
||||
"""
|
||||
Lọc ra các node phù hợp với các loại thực thể đã được định nghĩa
|
||||
|
||||
Logic lọc:
|
||||
- Nếu Labels của node chỉ có một nhãn là "Entity", tức là thực thể này không hợp với loại chúng ta định nghĩa, tiến hành bỏ qua
|
||||
- Nếu Labels của node chứa các nhãn khác ngoài "Entity" và "Node", tức là hợp lệ, tiến hành giữ lại
|
||||
|
||||
Đọc toàn bộ graph từ Zep rồi lọc ra các entity có loại được định nghĩa.
|
||||
|
||||
Logic lọc label (quan trọng — đây là cách phân biệt entity "có ý nghĩa"):
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ Node trong Zep có 2 loại label: │
|
||||
│ │
|
||||
│ "Generic" node: labels = ["Entity"] hoặc ["Node"] │
|
||||
│ → Zep tạo ra khi trích xuất context chung, │
|
||||
│ không phải thực thể cụ thể nào → BỎ QUA │
|
||||
│ │
|
||||
│ "Typed" node: labels = ["Entity", "Person"] │
|
||||
│ labels = ["Entity", "Organization"] │
|
||||
│ → Thực thể được định danh rõ ràng → GIỮ LẠI │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
|
||||
Hai chế độ lọc:
|
||||
- defined_entity_types=None: giữ TẤT CẢ typed nodes (mọi custom label đều OK)
|
||||
- defined_entity_types=["Person", "Organization"]: chỉ giữ nodes có label trong danh sách
|
||||
|
||||
Enrich với edges:
|
||||
- Sau khi lọc, với mỗi entity, scan all_edges để tìm edges có
|
||||
source_node_uuid hoặc target_node_uuid trùng với entity.uuid
|
||||
- Phân loại: source = outgoing, target = incoming
|
||||
- Lấy thêm thông tin cơ bản của node ở đầu kia (tên, labels, summary)
|
||||
|
||||
Ví dụ minh hoạ:
|
||||
─────────────────────────────────────────────────────────────────────
|
||||
Giả sử Zep Graph có 4 nodes và 2 edges sau:
|
||||
|
||||
NODES:
|
||||
uuid="aaa", name="Nguyễn Văn A", labels=["Entity", "Person"]
|
||||
uuid="bbb", name="Bộ Giáo Dục", labels=["Entity", "Organization"]
|
||||
uuid="ccc", name="context-001", labels=["Entity"] ← generic, bị loại
|
||||
uuid="ddd", name="Hà Nội", labels=["Entity", "Location"]
|
||||
|
||||
EDGES:
|
||||
source="aaa" → target="bbb", fact="Nguyễn Văn A làm việc tại Bộ Giáo Dục"
|
||||
source="ddd" → target="aaa", fact="Nguyễn Văn A sinh sống tại Hà Nội"
|
||||
|
||||
── Gọi với defined_entity_types=None ──────────────────────────────
|
||||
filter_defined_entities(graph_id, defined_entity_types=None)
|
||||
|
||||
→ Giữ lại: "aaa" (Person), "bbb" (Organization), "ddd" (Location)
|
||||
→ Loại bỏ: "ccc" (chỉ có label "Entity")
|
||||
→ entity_types = {"Person", "Organization", "Location"}
|
||||
→ filtered_count = 3, total_count = 4
|
||||
|
||||
EntityNode uuid="aaa" (Nguyễn Văn A) sau enrich:
|
||||
related_edges = [
|
||||
{"direction": "outgoing", "edge_name": "...", "fact": "A làm việc tại Bộ GD", "target_node_uuid": "bbb"},
|
||||
{"direction": "incoming", "edge_name": "...", "fact": "A sinh sống tại Hà Nội", "source_node_uuid": "ddd"},
|
||||
]
|
||||
related_nodes = [
|
||||
{"uuid": "bbb", "name": "Bộ Giáo Dục", "labels": ["Entity", "Organization"], "summary": "..."},
|
||||
{"uuid": "ddd", "name": "Hà Nội", "labels": ["Entity", "Location"], "summary": "..."},
|
||||
]
|
||||
|
||||
── Gọi với defined_entity_types=["Person"] ─────────────────────────
|
||||
filter_defined_entities(graph_id, defined_entity_types=["Person"])
|
||||
|
||||
→ Giữ lại: chỉ "aaa" (Person)
|
||||
→ Loại bỏ: "bbb" (Organization không match), "ccc" (generic), "ddd" (Location không match)
|
||||
→ filtered_count = 1
|
||||
─────────────────────────────────────────────────────────────────────
|
||||
|
||||
Args:
|
||||
graph_id: ID của đồ thị
|
||||
defined_entity_types: Danh sách các loại thực thể định nghĩa trước (không bắt buộc, nếu có thì chỉ giữ lại các loại đó)
|
||||
enrich_with_edges: Có lấy thông tin edge liên quan của từng thực thể hay không
|
||||
|
||||
graph_id: ID của Zep Graph
|
||||
defined_entity_types: Danh sách loại cần lọc (None = lấy tất cả)
|
||||
enrich_with_edges: True = thêm related_edges + related_nodes vào mỗi entity
|
||||
|
||||
Returns:
|
||||
FilteredEntities: Tập hợp các thực thể sau khi lọc
|
||||
FilteredEntities chứa danh sách EntityNode đã enrich
|
||||
"""
|
||||
logger.info(f"Start filtering entities for graph {graph_id}...")
|
||||
|
||||
# Lấy toàn bộ các node
|
||||
|
||||
# --- Bước 1: Bulk fetch nodes và edges ---
|
||||
all_nodes = self.get_all_nodes(graph_id)
|
||||
total_count = len(all_nodes)
|
||||
|
||||
# Lấy toàn bộ các edge (để lấy liên kết sau này)
|
||||
|
||||
# Chỉ fetch edges nếu cần enrich (tiết kiệm 1 API call nếu không cần)
|
||||
all_edges = self.get_all_edges(graph_id) if enrich_with_edges else []
|
||||
|
||||
# Xây dựng map ánh xạ từ UUID của node sang dữ liệu node
|
||||
|
||||
# Xây dựng dict ánh xạ uuid → node data để tra cứu O(1) khi enrich related_nodes
|
||||
node_map = {n["uuid"]: n for n in all_nodes}
|
||||
|
||||
# Lọc các thực thể đáp ứng điều kiện
|
||||
|
||||
# --- Bước 2: Lọc nodes theo label ---
|
||||
filtered_entities = []
|
||||
entity_types_found = set()
|
||||
|
||||
|
||||
for node in all_nodes:
|
||||
labels = node.get("labels", [])
|
||||
|
||||
# Logic lọc: Labels bắt buộc phải chứa các nhãn khác "Entity" và "Node"
|
||||
|
||||
# Tìm custom labels (loại trừ "Entity" và "Node" là labels mặc định của Zep)
|
||||
custom_labels = [l for l in labels if l not in ["Entity", "Node"]]
|
||||
|
||||
|
||||
if not custom_labels:
|
||||
# Chỉ có nhãn mặc định, bỏ qua
|
||||
# Node này chỉ có labels mặc định → không phải entity cụ thể → bỏ qua
|
||||
continue
|
||||
|
||||
# Nếu đã chỉ định loại thực thể cho trước, kiểm tra xem có khớp hay không
|
||||
|
||||
# Nếu người dùng chỉ định danh sách loại, kiểm tra xem node có match không
|
||||
if defined_entity_types:
|
||||
matching_labels = [l for l in custom_labels if l in defined_entity_types]
|
||||
if not matching_labels:
|
||||
continue
|
||||
entity_type = matching_labels[0]
|
||||
continue # Node này có custom label nhưng không nằm trong danh sách cho phép
|
||||
entity_type = matching_labels[0] # Lấy loại match đầu tiên
|
||||
else:
|
||||
entity_type = custom_labels[0]
|
||||
|
||||
entity_type = custom_labels[0] # Lấy custom label đầu tiên làm loại
|
||||
|
||||
entity_types_found.add(entity_type)
|
||||
|
||||
# Tạo object cho node thực thể
|
||||
|
||||
# Tạo EntityNode (related_edges và related_nodes vẫn rỗng, enrich ở bước sau)
|
||||
entity = EntityNode(
|
||||
uuid=node["uuid"],
|
||||
name=node["name"],
|
||||
|
|
@ -278,14 +492,17 @@ class ZepEntityReader:
|
|||
summary=node["summary"],
|
||||
attributes=node["attributes"],
|
||||
)
|
||||
|
||||
# Lấy các edge và node liên quan
|
||||
|
||||
# --- Bước 3: Enrich với edges và related nodes ---
|
||||
if enrich_with_edges:
|
||||
related_edges = []
|
||||
related_node_uuids = set()
|
||||
|
||||
|
||||
# Scan toàn bộ all_edges để tìm edges có liên quan đến node này
|
||||
# (không gọi get_node_edges() vì đã có all_edges trong memory)
|
||||
for edge in all_edges:
|
||||
if edge["source_node_uuid"] == node["uuid"]:
|
||||
# Entity này là nguồn → quan hệ "outgoing" (chiều đi ra)
|
||||
related_edges.append({
|
||||
"direction": "outgoing",
|
||||
"edge_name": edge["name"],
|
||||
|
|
@ -293,7 +510,9 @@ class ZepEntityReader:
|
|||
"target_node_uuid": edge["target_node_uuid"],
|
||||
})
|
||||
related_node_uuids.add(edge["target_node_uuid"])
|
||||
|
||||
elif edge["target_node_uuid"] == node["uuid"]:
|
||||
# Entity này là đích → quan hệ "incoming" (chiều đi vào)
|
||||
related_edges.append({
|
||||
"direction": "incoming",
|
||||
"edge_name": edge["name"],
|
||||
|
|
@ -301,10 +520,10 @@ class ZepEntityReader:
|
|||
"source_node_uuid": edge["source_node_uuid"],
|
||||
})
|
||||
related_node_uuids.add(edge["source_node_uuid"])
|
||||
|
||||
|
||||
entity.related_edges = related_edges
|
||||
|
||||
# Lấy thông tin cơ bản của các node được liên kết
|
||||
|
||||
# Lấy thông tin cơ bản của các node lân cận (không lấy full để tránh vòng lặp)
|
||||
related_nodes = []
|
||||
for related_uuid in related_node_uuids:
|
||||
if related_uuid in node_map:
|
||||
|
|
@ -314,58 +533,72 @@ class ZepEntityReader:
|
|||
"name": related_node["name"],
|
||||
"labels": related_node["labels"],
|
||||
"summary": related_node.get("summary", ""),
|
||||
# Không lấy attributes để tránh payload quá lớn khi truyền vào LLM
|
||||
})
|
||||
|
||||
|
||||
entity.related_nodes = related_nodes
|
||||
|
||||
|
||||
filtered_entities.append(entity)
|
||||
|
||||
|
||||
logger.info(f"Filtering completed: Total nodes {total_count}, Matched {len(filtered_entities)}, "
|
||||
f"Entity types: {entity_types_found}")
|
||||
|
||||
|
||||
return FilteredEntities(
|
||||
entities=filtered_entities,
|
||||
entity_types=entity_types_found,
|
||||
total_count=total_count,
|
||||
filtered_count=len(filtered_entities),
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC: get_entity_with_context — Fetch 1 entity đơn lẻ theo UUID
|
||||
# --------------------------------------------------------------------------
|
||||
# Dùng bởi oasis_profile_generator.py khi cần đọc sâu thêm 1 entity cụ thể.
|
||||
# Khác với filter_defined_entities():
|
||||
# - Fetch 1 node theo UUID (không scan toàn bộ graph)
|
||||
# - Dùng get_node_edges() per-node (per-node API call)
|
||||
# - Nhưng vẫn cần fetch all_nodes để build related_nodes → tốn hơn
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def get_entity_with_context(
|
||||
self,
|
||||
graph_id: str,
|
||||
self,
|
||||
graph_id: str,
|
||||
entity_uuid: str
|
||||
) -> Optional[EntityNode]:
|
||||
"""
|
||||
Lấy thông tin của một thực thể cụ thể và ngữ cảnh đầy đủ của nó (edge và node liên kết, với cơ chế thử lại)
|
||||
|
||||
Lấy thông tin đầy đủ của 1 entity cụ thể và toàn bộ ngữ cảnh liên kết.
|
||||
|
||||
Dùng per-node API call (khác với filter_defined_entities dùng bulk fetch).
|
||||
Phù hợp khi chỉ cần 1 entity — không hiệu quả nếu cần nhiều entities.
|
||||
|
||||
Args:
|
||||
graph_id: ID của đồ thị
|
||||
entity_uuid: UUID của thực thể
|
||||
|
||||
graph_id: ID của Zep Graph (dùng để lấy node_map cho related_nodes)
|
||||
entity_uuid: UUID của entity cần lấy
|
||||
|
||||
Returns:
|
||||
EntityNode hoặc None
|
||||
EntityNode đầy đủ hoặc None nếu không tìm thấy / lỗi
|
||||
"""
|
||||
try:
|
||||
# Sử dụng cơ chế thử lại để lấy thông tin node
|
||||
# Fetch node theo UUID với retry
|
||||
node = self._call_with_retry(
|
||||
func=lambda: self.client.graph.node.get(uuid_=entity_uuid),
|
||||
operation_name=f"Fetch node detail(uuid={entity_uuid[:8]}...)"
|
||||
)
|
||||
|
||||
|
||||
if not node:
|
||||
return None
|
||||
|
||||
# Lấy các edge của node
|
||||
|
||||
# Fetch edges của node này (per-node call, có retry)
|
||||
edges = self.get_node_edges(entity_uuid)
|
||||
|
||||
# Lấy tất cả các node để tìm liên kết
|
||||
|
||||
# Cần all_nodes để build related_nodes map — đây là điểm tốn kém nhất
|
||||
all_nodes = self.get_all_nodes(graph_id)
|
||||
node_map = {n["uuid"]: n for n in all_nodes}
|
||||
|
||||
# Xử lý các edge và node liên quan
|
||||
|
||||
# Xử lý edges (cùng logic với filter_defined_entities)
|
||||
related_edges = []
|
||||
related_node_uuids = set()
|
||||
|
||||
|
||||
for edge in edges:
|
||||
if edge["source_node_uuid"] == entity_uuid:
|
||||
related_edges.append({
|
||||
|
|
@ -383,8 +616,7 @@ class ZepEntityReader:
|
|||
"source_node_uuid": edge["source_node_uuid"],
|
||||
})
|
||||
related_node_uuids.add(edge["source_node_uuid"])
|
||||
|
||||
# Lấy thông tin về node được liên kết
|
||||
|
||||
related_nodes = []
|
||||
for related_uuid in related_node_uuids:
|
||||
if related_uuid in node_map:
|
||||
|
|
@ -395,7 +627,7 @@ class ZepEntityReader:
|
|||
"labels": related_node["labels"],
|
||||
"summary": related_node.get("summary", ""),
|
||||
})
|
||||
|
||||
|
||||
return EntityNode(
|
||||
uuid=getattr(node, 'uuid_', None) or getattr(node, 'uuid', ''),
|
||||
name=node.name or "",
|
||||
|
|
@ -405,27 +637,34 @@ class ZepEntityReader:
|
|||
related_edges=related_edges,
|
||||
related_nodes=related_nodes,
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch entity {entity_uuid}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PUBLIC: get_entities_by_type — Convenience wrapper cho 1 loại entity cụ thể
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def get_entities_by_type(
|
||||
self,
|
||||
graph_id: str,
|
||||
self,
|
||||
graph_id: str,
|
||||
entity_type: str,
|
||||
enrich_with_edges: bool = True
|
||||
) -> List[EntityNode]:
|
||||
"""
|
||||
Lấy tất cả các thực thể dựa theo loại cụ thể
|
||||
|
||||
Lấy tất cả entities của một loại cụ thể.
|
||||
|
||||
Là thin wrapper quanh filter_defined_entities() với defined_entity_types=[entity_type].
|
||||
Tiện dụng khi chỉ cần 1 loại thay vì nhiều loại.
|
||||
|
||||
Args:
|
||||
graph_id: ID của đồ thị
|
||||
entity_type: Loại thực thể (ví dụ: "Student", "PublicFigure", v.v..)
|
||||
enrich_with_edges: Có lấy thông tin edge liên quan hay không
|
||||
|
||||
graph_id: ID của Zep Graph
|
||||
entity_type: Loại entity cần lọc (ví dụ: "Person", "Organization")
|
||||
enrich_with_edges: Có enrich với edges không
|
||||
|
||||
Returns:
|
||||
Danh sách thực thể
|
||||
Danh sách EntityNode thuộc loại đã chỉ định
|
||||
"""
|
||||
result = self.filter_defined_entities(
|
||||
graph_id=graph_id,
|
||||
|
|
@ -433,5 +672,3 @@ class ZepEntityReader:
|
|||
enrich_with_edges=enrich_with_edges
|
||||
)
|
||||
return result.entities
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1109,23 +1109,41 @@ class ZepToolsService:
|
|||
Giúp phân rã một câu hỏi lớn / phức tạp thành nhiều câu hỏi nhỏ lẻ
|
||||
có thể query độc lập trên cơ sở dữ liệu.
|
||||
"""
|
||||
system_prompt = """You are a professional problem analysis expert. Your task is to break down a complex query into multiple sub-queries that can be independently observed in the simulated world.
|
||||
# system_prompt = """You are a professional problem analysis expert. Your task is to decompose a complex problem into multiple sub-questions that can be independently observed within a simulated world.
|
||||
|
||||
Requirements:
|
||||
1. Each sub-query should be specific enough to find concrete Agent behaviors or events.
|
||||
2. Sub-queries should cover different dimensions of the original query (Who, What, Why, How, When, Where).
|
||||
3. Sub-queries must relate to the simulation context.
|
||||
4. Return exactly in JSON format: {"sub_queries": ["sub_query 1", "sub_query 2", ...]}"""
|
||||
# Requirements:
|
||||
# 1. Each sub-question should be specific enough to identify relevant Agent behaviors or events within the simulation.
|
||||
# 2. Sub-questions should cover different dimensions of the original problem (e.g., Who, What, Why, How, When, Where).
|
||||
# 3. Sub-questions must be relevant to the simulation scenario.
|
||||
# 4. Return in JSON format: {"sub_queries": ["Sub-question 1", "Sub-question 2", ...]}"""
|
||||
|
||||
user_prompt = f"""Simulation background:
|
||||
# user_prompt = f"""Simulation Requirement Background:
|
||||
# {simulation_requirement}
|
||||
|
||||
# {f"Report context: {report_context[:500]}" if report_context else ""}
|
||||
|
||||
# Please decompose the following question into {max_queries} sub-questions:
|
||||
# {query}
|
||||
|
||||
# Return the list of sub-questions in JSON format."""
|
||||
|
||||
system_prompt = """Bạn là một chuyên gia phân tích vấn đề chuyên nghiệp. Nhiệm vụ của bạn là chia nhỏ một vấn đề phức tạp thành nhiều câu hỏi phụ có thể quan sát độc lập trong thế giới mô phỏng.
|
||||
|
||||
Yêu cầu:
|
||||
1. Mỗi câu hỏi phụ phải đủ cụ thể để có thể tìm thấy các hành vi của Agent hoặc các sự kiện liên quan trong thế giới mô phỏng.
|
||||
2. Các câu hỏi phụ nên bao quát các khía cạnh khác nhau của vấn đề gốc (Ví dụ: Ai, Cái gì, Tại sao, Như thế nào, Khi nào, Ở đâu).
|
||||
3. Các câu hỏi phụ phải liên quan đến kịch bản mô phỏng.
|
||||
4. Trả về định dạng JSON: {"sub_queries": ["Câu hỏi phụ 1", "Câu hỏi phụ 2", ...]}"""
|
||||
|
||||
user_prompt = f"""Bối cảnh yêu cầu mô phỏng:
|
||||
{simulation_requirement}
|
||||
|
||||
{f"Report context: {report_context[:500]}" if report_context else ""}
|
||||
{f"Ngữ cảnh báo cáo: {report_context[:500]}" if report_context else ""}
|
||||
|
||||
Please break down the following query into {max_queries} sub-queries:
|
||||
Hãy chia nhỏ vấn đề sau đây thành {max_queries} các câu hỏi phụ:
|
||||
{query}
|
||||
|
||||
Return the JSON format."""
|
||||
Trả về danh sách các câu hỏi phụ dưới định dạng JSON."""
|
||||
|
||||
try:
|
||||
response = self.llm.chat_json(
|
||||
|
|
@ -1358,16 +1376,28 @@ Return the JSON format."""
|
|||
combined_prompt = "\n".join([f"{i+1}. {q}" for i, q in enumerate(result.interview_questions)])
|
||||
|
||||
# Thêm các prefix tối ưu hoá, ràng buộc format câu trả lời của Agent
|
||||
# INTERVIEW_PROMPT_PREFIX = (
|
||||
# "You are currently being interviewed. Please combine your persona, "
|
||||
# "all past memories, and actions to answer the following questions directly in plain text.\n"
|
||||
# "Response Requirements:\n"
|
||||
# "1. Answer directly using natural language; do not call any tools.\n"
|
||||
# "2. Do not return JSON format or tool-call formats.\n"
|
||||
# "3. Do not use Markdown headers (e.g., #, ##, ###).\n"
|
||||
# "4. Answer questions one by one according to their numbers. Each answer must start with 'Question X:' (where X is the question number).\n"
|
||||
# "5. Use a blank line to separate the answers for each question.\n"
|
||||
# "6. Answers must have substantive content; provide at least 2-3 sentences for each question.\n\n"
|
||||
# )
|
||||
|
||||
INTERVIEW_PROMPT_PREFIX = (
|
||||
"You are being interviewed. Please combine your profile, all your past memories and actions, "
|
||||
"and directly answer the following questions in pure text.\n"
|
||||
"Reply requirements:\n"
|
||||
"1. Answer directly in natural language, do not call any tools.\n"
|
||||
"2. Do not return JSON format or tool call formats.\n"
|
||||
"3. Do not use Markdown headers (like #, ##, ###).\n"
|
||||
"4. Answer questions one by one according to their numbers, start each answer with 'Question X:' (X is the number).\n"
|
||||
"5. Separate each answer with a blank line.\n"
|
||||
"6. Answers must have substance, at least 2-3 sentences per question.\n\n"
|
||||
"Bạn đang tham gia một buổi phỏng vấn. Hãy kết hợp nhân vật (persona), "
|
||||
"tất cả ký ức và hành động trong quá khứ của bạn để trả lời trực tiếp các câu hỏi dưới đây bằng văn bản thuần túy.\n"
|
||||
"Yêu cầu phản hồi:\n"
|
||||
"1. Trả lời trực tiếp bằng ngôn ngữ tự nhiên, không gọi bất kỳ công cụ nào.\n"
|
||||
"2. Không trả về định dạng JSON hoặc định dạng gọi công cụ.\n"
|
||||
"3. Không sử dụng tiêu đề Markdown (như #, ##, ###).\n"
|
||||
"4. Trả lời từng câu một theo số thứ tự, mỗi câu trả lời bắt đầu bằng 'Câu hỏi X:' (X là số thứ tự câu hỏi).\n"
|
||||
"5. Sử dụng một dòng trống để phân tách giữa các câu trả lời.\n"
|
||||
"6. Câu trả lời phải có nội dung thực tế, mỗi câu hỏi cần trả lời ít nhất 2-3 câu văn.\n\n"
|
||||
)
|
||||
optimized_prompt = f"{INTERVIEW_PROMPT_PREFIX}{combined_prompt}"
|
||||
|
||||
|
|
@ -1389,7 +1419,7 @@ Return the JSON format."""
|
|||
simulation_id=simulation_id,
|
||||
interviews=interviews_request,
|
||||
platform=None, # Không định dạng nền tảng -> Dual platform call
|
||||
timeout=180.0 # Tăng timeout do phải chờ API trên 2 platforms xử lý
|
||||
timeout= 800 # Tăng timeout do phải chờ API trên 2 platforms xử lý
|
||||
)
|
||||
|
||||
logger.info(f"Interview API returned: {api_result.get('interviews_count', 0)} results, success={api_result.get('success')}")
|
||||
|
|
@ -1585,31 +1615,56 @@ Return the JSON format."""
|
|||
"interested_topics": profile.get("interested_topics", [])
|
||||
}
|
||||
agent_summaries.append(summary)
|
||||
|
||||
system_prompt = """You are a professional interview planning expert. Your task is to select the most suitable target agents for an interview based on requirements.
|
||||
|
||||
Selection criteria:
|
||||
1. Agent identity/profession is related to the interview topic.
|
||||
2. Agent might hold unique or valuable opinions.
|
||||
3. Select diverse perspectives (e.g., supporters, opponents, neutrals, professionals, etc.).
|
||||
4. Prioritize characters directly related to the event.
|
||||
system_prompt = """Bạn là một chuyên gia lập kế hoạch phỏng vấn chuyên nghiệp. Nhiệm vụ của bạn là dựa trên yêu cầu phỏng vấn để chọn ra những đối tượng phù hợp nhất từ danh sách các Agent mô phỏng.
|
||||
|
||||
Return in JSON format:
|
||||
Tiêu chí lựa chọn:
|
||||
1. Danh tính/Nghề nghiệp của Agent phải liên quan đến chủ đề phỏng vấn.
|
||||
2. Agent có khả năng nắm giữ những quan điểm độc đáo hoặc có giá trị.
|
||||
3. Lựa chọn các góc nhìn đa dạng (Ví dụ: bên ủng hộ, bên phản đối, bên trung lập, chuyên gia, v.v.).
|
||||
4. Ưu tiên các nhân vật có liên quan trực tiếp đến sự kiện.
|
||||
|
||||
Trả về định dạng JSON:
|
||||
{
|
||||
"selected_indices": [array of selected agent indices],
|
||||
"reasoning": "explanation for the selection"
|
||||
"selected_indices": [Danh sách chỉ mục của các Agent được chọn],
|
||||
"reasoning": "Giải thích lý do lựa chọn"
|
||||
}"""
|
||||
|
||||
user_prompt = f"""Interview requirements:
|
||||
user_prompt = f"""Yêu cầu phỏng vấn:
|
||||
{interview_requirement}
|
||||
|
||||
Simulation background:
|
||||
Bối cảnh mô phỏng:
|
||||
{simulation_requirement if simulation_requirement else "Not provided"}
|
||||
|
||||
Available Agents (total {len(agent_summaries)}):
|
||||
Danh sách các Agent có thể chọn (Tổng cộng {len(agent_summaries)}):
|
||||
{json.dumps(agent_summaries, ensure_ascii=False, indent=2)}
|
||||
|
||||
Please select up to {max_agents} most suitable agents for the interview and explain your reasoning."""
|
||||
Hãy chọn tối đa {max_agents} Agent phù hợp nhất để phỏng vấn và giải thích lý do lựa chọn của bạn."""
|
||||
|
||||
# system_prompt = """You are a professional interview planning expert. Your task is to select the most suitable subjects for an interview from a list of simulated Agents based on the interview requirements.
|
||||
|
||||
# Selection Criteria:
|
||||
# 1. The Agent's identity/profession must be relevant to the interview topic.
|
||||
# 2. The Agent is likely to hold a unique or valuable perspective.
|
||||
# 3. Select diverse perspectives (e.g., supporters, opponents, neutral parties, professionals, etc.).
|
||||
# 4. Prioritize characters directly related to the event.
|
||||
|
||||
# Return in JSON format:
|
||||
# {
|
||||
# "selected_indices": [List of indices of selected Agents],
|
||||
# "reasoning": "Explanation for the selection"
|
||||
# }"""
|
||||
|
||||
# user_prompt = f"""Interview Requirements:
|
||||
# {interview_requirement}
|
||||
|
||||
# Simulation Background:
|
||||
# {simulation_requirement if simulation_requirement else "Not provided"}
|
||||
|
||||
# List of selectable Agents (Total {len(agent_summaries)}):
|
||||
# {json.dumps(agent_summaries, ensure_ascii=False, indent=2)}
|
||||
|
||||
# Please select a maximum of {max_agents} most suitable Agents for the interview and explain the reasons for your selection."""
|
||||
|
||||
try:
|
||||
response = self.llm.chat_json(
|
||||
|
|
@ -1649,27 +1704,47 @@ Please select up to {max_agents} most suitable agents for the interview and expl
|
|||
"""Sử dụng LLM để sinh ra các câu chất vấn hợp với tính chất sự việc"""
|
||||
|
||||
agent_roles = [a.get("profession", "Unknown") for a in selected_agents]
|
||||
|
||||
# system_prompt = """You are a professional journalist/interviewer. Based on the interview requirements, generate 3-5 in-depth interview questions.
|
||||
|
||||
# Question Requirements:
|
||||
# 1. Open-ended questions that encourage detailed answers.
|
||||
# 2. Questions that may elicit different answers from different roles.
|
||||
# 3. Cover multiple dimensions such as facts, opinions, and feelings.
|
||||
# 4. Use natural language, sounding like a real-life interview.
|
||||
# 5. Keep each question under 50 words, concise and clear.
|
||||
# 6. Ask the questions directly; do not include background explanations or prefixes.
|
||||
|
||||
# Return in JSON format: {"questions": ["Question 1", "Question 2", ...]}"""
|
||||
|
||||
# user_prompt = f"""Interview Requirements: {interview_requirement}
|
||||
|
||||
# Simulation Background: {simulation_requirement if simulation_requirement else "Not provided"}
|
||||
|
||||
# Interviewee Roles: {', '.join(agent_roles)}
|
||||
|
||||
# Please generate 3-5 interview questions."""
|
||||
|
||||
system_prompt = """You are a professional journalist/interviewer. Generate 3-5 deep interview questions based on requirements.
|
||||
system_prompt = """Bạn là một nhà báo/người phỏng vấn chuyên nghiệp. Dựa trên yêu cầu phỏng vấn, hãy tạo từ 3-5 câu hỏi phỏng vấn chuyên sâu.
|
||||
|
||||
Question requirements:
|
||||
1. Open-ended questions, encourage detailed answers.
|
||||
2. Formulated so different roles might have different answers.
|
||||
3. Cover multiple dimensions like facts, opinions, feelings, etc.
|
||||
4. Natural language, sounds like a real interview.
|
||||
5. Keep each question within 50 words, concise and clear.
|
||||
6. Ask directly, do not include background explanations or prefixes.
|
||||
Yêu cầu đối với câu hỏi:
|
||||
1. Câu hỏi mở, khuyến khích câu trả lời chi tiết.
|
||||
2. Các câu hỏi có thể nhận được những câu trả lời khác nhau tùy theo từng vai trò.
|
||||
3. Bao quát nhiều khía cạnh như sự thật, quan điểm và cảm xúc.
|
||||
4. Ngôn ngữ tự nhiên, giống như một buổi phỏng vấn thực tế.
|
||||
5. Mỗi câu hỏi khống chế dưới 50 chữ, ngắn gọn và súc tích.
|
||||
6. Đặt câu hỏi trực tiếp, không bao gồm giải thích bối cảnh hoặc tiền tố.
|
||||
|
||||
Return in JSON format: {"questions": ["question 1", "question 2", ...]}"""
|
||||
Trả về định dạng JSON: {"questions": ["Câu hỏi 1", "Câu hỏi 2", ...]}"""
|
||||
|
||||
user_prompt = f"""Interview requirements: {interview_requirement}
|
||||
user_prompt = f"""Yêu cầu phỏng vấn: {interview_requirement}
|
||||
|
||||
Simulation background: {simulation_requirement if simulation_requirement else "Not provided"}
|
||||
Bối cảnh mô phỏng: {simulation_requirement if simulation_requirement else "Not provided"}
|
||||
|
||||
Interviewee roles: {', '.join(agent_roles)}
|
||||
|
||||
Please generate 3-5 interview questions."""
|
||||
Vai trò của đối tượng phỏng vấn: {', '.join(agent_roles)}
|
||||
|
||||
Hãy tạo từ 3-5 câu hỏi phỏng vấn."""
|
||||
|
||||
try:
|
||||
response = self.llm.chat_json(
|
||||
messages=[
|
||||
|
|
@ -1703,29 +1778,52 @@ Please generate 3-5 interview questions."""
|
|||
interview_texts = []
|
||||
for interview in interviews:
|
||||
interview_texts.append(f"【{interview.agent_name}({interview.agent_role})】\n{interview.response[:500]}")
|
||||
|
||||
# system_prompt = """You are a professional news editor. Please generate an interview summary based on the responses from multiple interviewees.
|
||||
|
||||
# Summary Requirements:
|
||||
# 1. Synthesize the main viewpoints from all parties.
|
||||
# 2. Identify areas of consensus and divergence among the viewpoints.
|
||||
# 3. Highlight valuable quotes.
|
||||
# 4. Maintain objectivity and neutrality, without favoring any side.
|
||||
# 5. Limit the summary to 1000 words.
|
||||
|
||||
# Formatting Constraints (MUST be followed):
|
||||
# - Use plain text paragraphs, separated by blank lines.
|
||||
# - Do not use Markdown headers (e.g., #, ##, ###).
|
||||
# - Do not use horizontal rules (e.g., ---, ***).
|
||||
# - Use Vietnamese quotation marks 「」 when quoting the interviewees' original words.
|
||||
# - You may use **bold** to highlight keywords, but do not use any other Markdown syntax."""
|
||||
|
||||
# user_prompt = f"""Interview Topic: {interview_requirement}
|
||||
|
||||
# Interview Content:
|
||||
# {"\n\n".join(interview_texts)}
|
||||
|
||||
# Please generate the interview summary."""
|
||||
|
||||
system_prompt = """You are a professional news editor. Please generate an interview summary based on the answers from multiple interviewees.
|
||||
system_prompt = """Bạn là một biên tập viên tin tức chuyên nghiệp. Hãy tạo một bản tóm tắt phỏng vấn dựa trên câu trả lời từ nhiều đối tượng được phỏng vấn.
|
||||
|
||||
Summary requirements:
|
||||
1. Extract main viewpoints from all parties.
|
||||
2. Point out consensus and disagreements among opinions.
|
||||
3. Highlight valuable quotes.
|
||||
4. Objective and neutral, do not favor any side.
|
||||
5. Keep it within 1000 words.
|
||||
Yêu cầu tóm tắt:
|
||||
1. Đúc kết các quan điểm chính của các bên.
|
||||
2. Chỉ ra những điểm đồng thuận và khác biệt trong các quan điểm.
|
||||
3. Làm nổi bật các câu trích dẫn có giá trị.
|
||||
4. Đảm bảo tính khách quan và trung lập, không thiên vị bất kỳ bên nào.
|
||||
5. Giới hạn trong khoảng 1000 chữ.
|
||||
|
||||
Formatting constraints (Must obey):
|
||||
- Use plain text paragraphs, separate different sections with blank lines.
|
||||
- Do not use Markdown headers (like #, ##, ###).
|
||||
- Do not use dividers (like ---, ***).
|
||||
- Use normal quotes when citing interviewee actions/words.
|
||||
- You can use **bold** to mark keywords, but no other Markdown syntax."""
|
||||
Ràng buộc định dạng (BẮT BUỘC tuân thủ):
|
||||
- Sử dụng các đoạn văn bản thuần túy, phân tách các phần bằng dòng trống.
|
||||
- Không sử dụng tiêu đề Markdown (như #, ##, ###).
|
||||
- Không sử dụng đường kẻ phân cách (như ---, ***).
|
||||
- Sử dụng dấu ngoặc kép kiểu Việt Nam 「」 khi trích dẫn nguyên văn lời của người được phỏng vấn.
|
||||
- Có thể sử dụng dấu **in đậm** cho các từ khóa, nhưng không sử dụng bất kỳ cú pháp Markdown nào khác."""
|
||||
|
||||
user_prompt = f"""Interview topic: {interview_requirement}
|
||||
user_prompt = f"""Chủ đề phỏng vấn: {interview_requirement}
|
||||
|
||||
Interview content:
|
||||
Nội dung phỏng vấn:
|
||||
{"\n\n".join(interview_texts)}
|
||||
|
||||
Please generate the interview summary."""
|
||||
Hãy tạo bản tóm tắt phỏng vấn."""
|
||||
|
||||
try:
|
||||
summary = self.llm.chat(
|
||||
|
|
@ -1734,7 +1832,7 @@ Please generate the interview summary."""
|
|||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=800
|
||||
max_tokens=4096
|
||||
)
|
||||
return summary
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class LLMClient:
|
|||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 4096,
|
||||
max_tokens: int = 16000,
|
||||
response_format: Optional[Dict] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
|
|
@ -79,7 +79,7 @@ class LLMClient:
|
|||
metadata=call_metadata,
|
||||
**{k: v for k, v in kwargs.items() if k not in {"model", "messages"}},
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
content = response.choices[0].message.content or ""
|
||||
# Một số model (vd MiniMax M2.5) chèn nội dung <think> vào content, cần loại bỏ
|
||||
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
|
||||
return content
|
||||
|
|
@ -88,7 +88,7 @@ class LLMClient:
|
|||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.3,
|
||||
max_tokens: int = 4096,
|
||||
max_tokens: int = 50000,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ 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},
|
||||
"Qwen/Qwen3.6-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},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,36 @@ def _build_metadata(model: str) -> Dict[str, Any]:
|
|||
return metadata
|
||||
|
||||
|
||||
def _normalize_messages_for_qwen(messages: Any) -> Any:
|
||||
"""Ensure Qwen-compatible ordering: a single system message at index 0."""
|
||||
if not isinstance(messages, list):
|
||||
return messages
|
||||
|
||||
if not messages:
|
||||
return messages
|
||||
|
||||
system_contents = []
|
||||
non_system_messages = []
|
||||
|
||||
for item in messages:
|
||||
if isinstance(item, dict) and str(item.get("role", "")).lower() == "system":
|
||||
content = item.get("content")
|
||||
if content is not None and str(content).strip():
|
||||
system_contents.append(str(content))
|
||||
continue
|
||||
non_system_messages.append(item)
|
||||
|
||||
# No system messages detected, keep original payload.
|
||||
if not system_contents:
|
||||
return messages
|
||||
|
||||
merged_system = {
|
||||
"role": "system",
|
||||
"content": "\n\n".join(system_contents),
|
||||
}
|
||||
return [merged_system] + non_system_messages
|
||||
|
||||
|
||||
def install_openai_cost_patch(
|
||||
*,
|
||||
simulation_id: Optional[str],
|
||||
|
|
@ -58,6 +88,9 @@ def install_openai_cost_patch(
|
|||
|
||||
def sync_create_wrapper(self, *args, **kwargs):
|
||||
model = kwargs.get("model", "unknown_model")
|
||||
if "qwen" in str(model).lower() and "messages" in kwargs:
|
||||
kwargs = dict(kwargs)
|
||||
kwargs["messages"] = _normalize_messages_for_qwen(kwargs.get("messages"))
|
||||
response = original_sync_create(self, *args, **kwargs)
|
||||
try:
|
||||
record_llm_cost(
|
||||
|
|
@ -71,6 +104,9 @@ def install_openai_cost_patch(
|
|||
|
||||
async def async_create_wrapper(self, *args, **kwargs):
|
||||
model = kwargs.get("model", "unknown_model")
|
||||
if "qwen" in str(model).lower() and "messages" in kwargs:
|
||||
kwargs = dict(kwargs)
|
||||
kwargs["messages"] = _normalize_messages_for_qwen(kwargs.get("messages"))
|
||||
response = await original_async_create(self, *args, **kwargs)
|
||||
try:
|
||||
record_llm_cost(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
Script test ZepEntityReader — đọc graph đã build, xem logic chọn entity làm agent
|
||||
Chạy từ thư mục backend/
|
||||
|
||||
Usage:
|
||||
python scripts/test_entity_reader.py <graph_id>
|
||||
python scripts/test_entity_reader.py <graph_id> --type Student
|
||||
python scripts/test_entity_reader.py <graph_id> --uuid <entity_uuid>
|
||||
python scripts/test_entity_reader.py --from-log case_01_academic_scandal
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.services.zep_entity_reader import ZepEntityReader
|
||||
|
||||
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs")
|
||||
|
||||
|
||||
def load_graph_id_from_log(case_id: str) -> str:
|
||||
log_path = os.path.join(OUTPUT_DIR, f"test_graph_output_{case_id}.json")
|
||||
if not os.path.exists(log_path):
|
||||
print(f"Không tìm thấy file log: {log_path}")
|
||||
sys.exit(1)
|
||||
with open(log_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data["graph_id"]
|
||||
|
||||
|
||||
def print_entity(entity, show_edges: bool = True):
|
||||
entity_type = entity.get_entity_type() or "(unknown)"
|
||||
print(f"\n ── [{entity_type}] {entity.name}")
|
||||
print(f" uuid : {entity.uuid}")
|
||||
if entity.summary:
|
||||
print(f" summary : {entity.summary[:120]}")
|
||||
if entity.attributes:
|
||||
for k, v in entity.attributes.items():
|
||||
if k not in ("name",) and v and v != "null":
|
||||
print(f" attr : {k} = {v}")
|
||||
if show_edges and entity.related_edges:
|
||||
print(f" edges ({len(entity.related_edges)}):")
|
||||
for e in entity.related_edges:
|
||||
arrow = "→" if e["direction"] == "outgoing" else "←"
|
||||
print(f" {arrow} [{e['edge_name']}] {e.get('fact', '')[:80]}")
|
||||
if entity.related_nodes:
|
||||
names = [n["name"] for n in entity.related_nodes]
|
||||
print(f" related : {', '.join(names)}")
|
||||
|
||||
|
||||
def cmd_all(reader: ZepEntityReader, graph_id: str):
|
||||
"""Lấy tất cả entity hợp lệ (có custom label) → danh sách agent tiềm năng"""
|
||||
print(f"\nGraph: {graph_id}")
|
||||
print("Đang lọc entity...")
|
||||
result = reader.filter_defined_entities(graph_id, enrich_with_edges=True)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Tổng nodes : {result.total_count}")
|
||||
print(f"Entity hợp lệ : {result.filtered_count} ← đây là các agent tiềm năng")
|
||||
print(f"Entity types : {sorted(result.entity_types)}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# Nhóm theo type để dễ đọc
|
||||
by_type: dict = {}
|
||||
for e in result.entities:
|
||||
t = e.get_entity_type() or "unknown"
|
||||
by_type.setdefault(t, []).append(e)
|
||||
|
||||
for entity_type, entities in sorted(by_type.items()):
|
||||
print(f"\n[{entity_type}] — {len(entities)} entity")
|
||||
for e in entities:
|
||||
print_entity(e)
|
||||
|
||||
# Lưu output
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
out_path = os.path.join(OUTPUT_DIR, f"test_entity_reader_{graph_id}.json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(result.to_dict(), f, ensure_ascii=False, indent=2)
|
||||
print(f"\nĐã lưu: {out_path}")
|
||||
|
||||
|
||||
def cmd_by_type(reader: ZepEntityReader, graph_id: str, entity_type: str):
|
||||
print(f"\nLấy entity type [{entity_type}] từ graph {graph_id}...")
|
||||
entities = reader.get_entities_by_type(graph_id, entity_type, enrich_with_edges=True)
|
||||
print(f"Tìm thấy {len(entities)} entity loại [{entity_type}]:")
|
||||
for e in entities:
|
||||
print_entity(e)
|
||||
|
||||
|
||||
def cmd_by_uuid(reader: ZepEntityReader, graph_id: str, uuid: str):
|
||||
print(f"\nLấy chi tiết entity {uuid}...")
|
||||
entity = reader.get_entity_with_context(graph_id, uuid)
|
||||
if not entity:
|
||||
print("Không tìm thấy entity.")
|
||||
return
|
||||
print_entity(entity, show_edges=True)
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
graph_id = None
|
||||
entity_type = None
|
||||
entity_uuid = None
|
||||
|
||||
if not args:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
if args[0] == "--from-log":
|
||||
if len(args) < 2:
|
||||
print("Usage: --from-log <case_id>")
|
||||
sys.exit(1)
|
||||
graph_id = load_graph_id_from_log(args[1])
|
||||
print(f"Graph ID từ log [{args[1]}]: {graph_id}")
|
||||
args = args[2:]
|
||||
else:
|
||||
graph_id = args[0]
|
||||
args = args[1:]
|
||||
|
||||
if "--type" in args:
|
||||
entity_type = args[args.index("--type") + 1]
|
||||
if "--uuid" in args:
|
||||
entity_uuid = args[args.index("--uuid") + 1]
|
||||
|
||||
reader = ZepEntityReader()
|
||||
|
||||
if entity_uuid:
|
||||
cmd_by_uuid(reader, graph_id, entity_uuid)
|
||||
elif entity_type:
|
||||
cmd_by_type(reader, graph_id, entity_type)
|
||||
else:
|
||||
cmd_all(reader, graph_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -38,7 +38,7 @@ def test_profile_formats():
|
|||
age=25,
|
||||
gender="male",
|
||||
mbti="INTJ",
|
||||
country="China",
|
||||
country="Vietnam",
|
||||
profession="Student",
|
||||
interested_topics=["Technology", "Education"],
|
||||
source_entity_uuid="test-uuid-123",
|
||||
|
|
|
|||
|
|
@ -862,7 +862,7 @@ const parseInterview = (text) => {
|
|||
const quotesText = quotesMatch[1]
|
||||
// Prioritize matching > "text" format
|
||||
let quoteMatches = quotesText.match(/> "([^"]+)"/g)
|
||||
// Backtrack: Matches > "text" or > \u201Ctext\u201D (Chinese quotation marks)
|
||||
// Backtrack: Matches > "text" or > \u201Ctext\u201D (Vietnamese quotation marks)
|
||||
if (!quoteMatches) {
|
||||
quoteMatches = quotesText.match(/> [\u201C""]([^\u201D""]+)[\u201D""]/g)
|
||||
}
|
||||
|
|
@ -2089,9 +2089,9 @@ const extractFinalContent = (response) => {
|
|||
}
|
||||
|
||||
// Try to find the content after "Final Answer:"
|
||||
const chineseFinalMatch = response.match(/Final Answer[::]\s*\n*([\s\S]*)$/i)
|
||||
if (chineseFinalMatch) {
|
||||
return chineseFinalMatch[1].trim()
|
||||
const vietnameseFinalMatch = response.match(/Final Answer[::]\s*\n*([\s\S]*)$/i)
|
||||
if (vietnameseFinalMatch) {
|
||||
return vietnameseFinalMatch[1].trim()
|
||||
}
|
||||
|
||||
// Nếu bắt đầu bằng ## hoặc # hoặc >, có thể là nội dung markdown trực tiếp
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@
|
|||
<textarea
|
||||
v-model="formData.simulationRequirement"
|
||||
class="code-input"
|
||||
placeholder="// Describe your simulation or prediction request in natural language (e.g. If Wuhan University announces the revocation of disciplinary action against someone, what public opinion trends would emerge?)"
|
||||
placeholder="Describe your simulation or prediction request in natural language (e.g. If Wuhan University announces the revocation of disciplinary action against someone, what public opinion trends would emerge?)"
|
||||
rows="6"
|
||||
:disabled="loading"
|
||||
></textarea>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Claude Code Windows CMD Bootstrap Script
|
||||
REM Installs Claude Code for environments where PowerShell is not available
|
||||
|
||||
REM Parse command line argument
|
||||
set "TARGET=%~1"
|
||||
if "!TARGET!"=="" set "TARGET=latest"
|
||||
|
||||
REM Validate target parameter
|
||||
if /i "!TARGET!"=="stable" goto :target_valid
|
||||
if /i "!TARGET!"=="latest" goto :target_valid
|
||||
echo !TARGET! | findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" >nul
|
||||
if !ERRORLEVEL! equ 0 goto :target_valid
|
||||
|
||||
echo Usage: %0 [stable^|latest^|VERSION] >&2
|
||||
echo Example: %0 1.0.58 >&2
|
||||
exit /b 1
|
||||
|
||||
:target_valid
|
||||
|
||||
REM Check for 64-bit Windows
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" goto :arch_valid
|
||||
|
||||
echo Claude Code does not support 32-bit Windows. Please use a 64-bit version of Windows. >&2
|
||||
exit /b 1
|
||||
|
||||
:arch_valid
|
||||
|
||||
REM Set constants
|
||||
set "GCS_BUCKET=https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
|
||||
set "DOWNLOAD_DIR=%USERPROFILE%\.claude\downloads"
|
||||
REM Use native ARM64 binary on ARM64 Windows, x64 otherwise
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
||||
set "PLATFORM=win32-arm64"
|
||||
) else (
|
||||
set "PLATFORM=win32-x64"
|
||||
)
|
||||
|
||||
REM Create download directory
|
||||
if not exist "!DOWNLOAD_DIR!" mkdir "!DOWNLOAD_DIR!"
|
||||
|
||||
REM Check for curl availability
|
||||
curl --version >nul 2>&1
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo curl is required but not available. Please install curl or use PowerShell installer. >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Always download latest version (which has the most up-to-date installer)
|
||||
call :download_file "!GCS_BUCKET!/latest" "!DOWNLOAD_DIR!\latest"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to get latest version >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Read version from file
|
||||
set /p VERSION=<"!DOWNLOAD_DIR!\latest"
|
||||
del "!DOWNLOAD_DIR!\latest"
|
||||
|
||||
REM Download manifest
|
||||
call :download_file "!GCS_BUCKET!/!VERSION!/manifest.json" "!DOWNLOAD_DIR!\manifest.json"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to get manifest >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Extract checksum from manifest
|
||||
call :parse_manifest "!DOWNLOAD_DIR!\manifest.json" "!PLATFORM!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Platform !PLATFORM! not found in manifest >&2
|
||||
del "!DOWNLOAD_DIR!\manifest.json" 2>nul
|
||||
exit /b 1
|
||||
)
|
||||
del "!DOWNLOAD_DIR!\manifest.json"
|
||||
|
||||
REM Download binary
|
||||
set "BINARY_PATH=!DOWNLOAD_DIR!\claude-!VERSION!-!PLATFORM!.exe"
|
||||
call :download_file "!GCS_BUCKET!/!VERSION!/!PLATFORM!/claude.exe" "!BINARY_PATH!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to download binary >&2
|
||||
if exist "!BINARY_PATH!" del "!BINARY_PATH!"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Verify checksum
|
||||
call :verify_checksum "!BINARY_PATH!" "!EXPECTED_CHECKSUM!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Checksum verification failed >&2
|
||||
del "!BINARY_PATH!"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Run claude install to set up launcher and shell integration
|
||||
echo Setting up Claude Code...
|
||||
"!BINARY_PATH!" install "!TARGET!"
|
||||
set "INSTALL_RESULT=!ERRORLEVEL!"
|
||||
|
||||
REM Clean up downloaded file
|
||||
REM Wait a moment for any file handles to be released
|
||||
timeout /t 1 /nobreak >nul 2>&1
|
||||
del /f "!BINARY_PATH!" >nul 2>&1
|
||||
if exist "!BINARY_PATH!" (
|
||||
echo Warning: Could not remove temporary file: !BINARY_PATH!
|
||||
)
|
||||
|
||||
if !INSTALL_RESULT! neq 0 (
|
||||
echo Installation failed >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Installation complete^^!
|
||||
echo.
|
||||
exit /b 0
|
||||
|
||||
REM ============================================================================
|
||||
REM SUBROUTINES
|
||||
REM ============================================================================
|
||||
|
||||
:download_file
|
||||
REM Downloads a file using curl
|
||||
REM Args: %1=URL, %2=OutputPath
|
||||
set "URL=%~1"
|
||||
set "OUTPUT=%~2"
|
||||
|
||||
curl -fsSL "!URL!" -o "!OUTPUT!"
|
||||
exit /b !ERRORLEVEL!
|
||||
|
||||
:parse_manifest
|
||||
REM Parse JSON manifest to extract checksum for platform
|
||||
REM Args: %1=ManifestPath, %2=Platform
|
||||
set "MANIFEST_PATH=%~1"
|
||||
set "PLATFORM_NAME=%~2"
|
||||
set "EXPECTED_CHECKSUM="
|
||||
|
||||
REM Use findstr to find platform section, then look for checksum
|
||||
set "FOUND_PLATFORM="
|
||||
set "IN_PLATFORM_SECTION="
|
||||
|
||||
REM Read the manifest line by line
|
||||
for /f "usebackq tokens=*" %%i in ("!MANIFEST_PATH!") do (
|
||||
set "LINE=%%i"
|
||||
|
||||
REM Check if this line contains our platform
|
||||
echo !LINE! | findstr /c:"\"%PLATFORM_NAME%\":" >nul
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
set "IN_PLATFORM_SECTION=1"
|
||||
)
|
||||
|
||||
REM If we're in the platform section, look for checksum
|
||||
if defined IN_PLATFORM_SECTION (
|
||||
echo !LINE! | findstr /c:"\"checksum\":" >nul
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
REM Extract checksum value
|
||||
for /f "tokens=2 delims=:" %%j in ("!LINE!") do (
|
||||
set "CHECKSUM_PART=%%j"
|
||||
REM Remove quotes, whitespace, and comma
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART: =!"
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART:"=!"
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART:,=!"
|
||||
|
||||
REM Check if it looks like a SHA256 (64 hex chars)
|
||||
if not "!CHECKSUM_PART!"=="" (
|
||||
call :check_length "!CHECKSUM_PART!" 64
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
set "EXPECTED_CHECKSUM=!CHECKSUM_PART!"
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
REM Check if we've left the platform section (closing brace)
|
||||
echo !LINE! | findstr /c:"}" >nul
|
||||
if !ERRORLEVEL! equ 0 set "IN_PLATFORM_SECTION="
|
||||
)
|
||||
)
|
||||
|
||||
if "!EXPECTED_CHECKSUM!"=="" exit /b 1
|
||||
exit /b 0
|
||||
|
||||
:check_length
|
||||
REM Check if string length equals expected length
|
||||
REM Args: %1=String, %2=ExpectedLength
|
||||
set "STR=%~1"
|
||||
set "EXPECTED_LEN=%~2"
|
||||
set "LEN=0"
|
||||
:count_loop
|
||||
if "!STR:~%LEN%,1!"=="" goto :count_done
|
||||
set /a LEN+=1
|
||||
goto :count_loop
|
||||
:count_done
|
||||
if %LEN%==%EXPECTED_LEN% exit /b 0
|
||||
exit /b 1
|
||||
|
||||
:verify_checksum
|
||||
REM Verify file checksum using certutil
|
||||
REM Args: %1=FilePath, %2=ExpectedChecksum
|
||||
set "FILE_PATH=%~1"
|
||||
set "EXPECTED=%~2"
|
||||
|
||||
for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!FILE_PATH!" SHA256') do (
|
||||
set "ACTUAL=%%i"
|
||||
set "ACTUAL=!ACTUAL: =!"
|
||||
if "!ACTUAL!"=="CertUtil:Thecommandcompletedsuccessfully." goto :verify_done
|
||||
if "!ACTUAL!" neq "" (
|
||||
if /i "!ACTUAL!"=="!EXPECTED!" (
|
||||
exit /b 0
|
||||
) else (
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:verify_done
|
||||
exit /b 1
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 336c9c6ed7583b863ef0536a870f7d510d55502c
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "mirofish",
|
||||
"version": "0.1.0",
|
||||
"description": "MiroFish - 简洁通用的群体智能引擎,预测万物",
|
||||
"description": "MiroFish - A simple and versatile swarm intelligence engine that predicts everything",
|
||||
"scripts": {
|
||||
"setup": "npm install && cd frontend && npm install",
|
||||
"setup:backend": "cd backend && uv sync",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
# Báo cáo Dự báo Tương lai: Động lực Thị trường Dầu mỏ & Tài chính dưới Áp lực Địa chính trị
|
||||
|
||||
> Mô phỏng dự báo một tương lai mà thị trường dầu mỏ sẽ duy trì biến động mạnh và giá cao trong ít nhất 6 tháng, với sự phân hóa rõ rệt giữa các tác nhân: nhà giao dịch tận dụng đòn bẩy để đặt cược ngắn hạn, trong khi chuyên gia và tổ chức quốc tế rơi vào trạng thái 'bay mù' và thận trọng, dẫn đến rủi ro hệ thống từ sự thích nghi cảm xúc và điểm gãy cung cầu tại các nút thắt chiến lược.
|
||||
|
||||
---
|
||||
|
||||
## Trạng thái Thị trường Tương lai: Biến động Cấu trúc và Sự Thích nghi Cảm xúc
|
||||
|
||||
Chương này phân tích trạng thái tương lai của thị trường dầu mỏ và tài chính, nơi mà áp lực địa chính trị đã tạo ra một cấu trúc biến động mới. Kết quả mô phỏng cho thấy thị trường sẽ không còn phản ứng tuyến tính với tin tức, mà thay vào đó là sự thích nghi cảm xúc phức tạp giữa các nhóm tác nhân khác nhau.
|
||||
|
||||
**Biến động Cấu trúc và Xu hướng Giá Cao**
|
||||
|
||||
Trong kịch bản tương lai được mô phỏng, thị trường dầu mỏ đã chuyển dịch từ trạng thái dư thừa sang tình trạng thiếu hụt nghiêm trọng, dẫn đến việc giá dầu duy trì ở mức cao và biến động mạnh. Nguyên nhân cốt lõi là sự phong tỏa Eo biển Hormuz và xung đột kéo dài ở Iran.
|
||||
|
||||
> "Giá dầu đã tăng khoảng 40% kể từ khi xung đột Iran bắt đầu, và thị trường sẵn sàng cho những mức tăng thêm nếu tình hình ở Trung Đông xấu đi trở lại."
|
||||
|
||||
Sự biến động này không chỉ là tạm thời mà mang tính cấu trúc. Các nhà phân tích trong mô phỏng đã xác định một xu hướng dài hạn mới.
|
||||
|
||||
> "Các nhà phân tích tin rằng tình trạng giá cao và nguồn cung thấp trong lĩnh vực dầu mỏ sẽ tiếp tục trong ít nhất sáu tháng."
|
||||
|
||||
Điều này được củng cố bởi thực tế là nguồn cung vật lý bị gián đoạn nghiêm trọng.
|
||||
|
||||
> "Cơ quan Năng lượng Quốc tế (IEA) cảnh báo rằng trong kịch bản bất lợi hơn của cuộc xung đột kéo dài, thị trường năng lượng và nền kinh tế trên khắp thế giới cần chuẩn bị cho những gián đoạn đáng kể trong những tháng tới."
|
||||
|
||||
**Sự Phân hóa Cảm xúc: Nhà Giao dịch Đòn bẩy vs. Chuyên gia Thận trọng**
|
||||
|
||||
Một đặc điểm nổi bật của tương lai này là sự phân hóa rõ rệt giữa các nhóm tham gia thị trường. Nhà giao dịch sử dụng đòn bẩy (traders) thường phản ứng cảm xúc với tin tức ngắn hạn, trong khi các chuyên gia và tổ chức quốc tế rơi vào trạng thái thận trọng và thiếu thông tin rõ ràng ("bay mù").
|
||||
|
||||
**Nhà giao dịch tận dụng đòn bẩy**
|
||||
|
||||
Nhóm này bị ảnh hưởng nặng nề bởi sự biến động nhanh chóng và thường đặt cược sai hướng.
|
||||
|
||||
> "Các nhà giao dịch hàng hóa đã chịu thiệt hại hàng tỷ đô la vì họ đã đặt cược sai hướng về giá dầu."
|
||||
|
||||
Hành vi của họ thường bị chi phối bởi các tuyên bố từ cấp cao chính trị, dẫn đến những đợt tăng giảm thất thường.
|
||||
|
||||
> "Hợp đồng tương lai dầu thô đã nảy khắp nơi, thường dao động dựa trên tuyên bố lạc quan nhất từ Trump về việc chấm dứt xung đột."
|
||||
|
||||
Tuy nhiên, sự tham gia của họ cũng tạo ra các cơ hội đầu cơ rủi ro cao.
|
||||
|
||||
> "Việc chuyển hướng dòng tiền vào thị trường hàng hóa có thể ảnh hưởng đáng kể đến giá cả, có thể bất lợi cho nhà đầu tư."
|
||||
|
||||
**Chuyên gia và Tổ chức Quốc tế**
|
||||
|
||||
Ngược lại, các chuyên gia nhận ra rằng tình hình nghiêm trọng hơn nhiều so với những gì thị trường ngắn hạn đang định giá.
|
||||
|
||||
> "Nic Dyer lưu ý rằng tình trạng thắt chặt nguồn cung dầu thô nghiêm trọng hơn nhiều so với những gì các nhà giao dịch tương lai có thể giả định."
|
||||
|
||||
Tuy nhiên, ngay cả các tổ chức tài chính lớn cũng có xu hướng đánh giá thấp tác động thực sự.
|
||||
|
||||
> "Các nhà kinh tế trong hầu hết các tổ chức tài chính đánh giá thấp tác động của việc mất nguồn tài nguyên năng lượng. Khái niệm sai lầm này giải thích tại sao các nhà kinh tế... và do đó nhà đầu tư, không đặc biệt lo lắng."
|
||||
|
||||
Sự thiếu hiểu biết này dẫn đến rủi ro hệ thống khi các cú sốc giá lan rộng.
|
||||
|
||||
> "Các nhà hoạch định chính sách mô tả kết quả của căng thẳng dai dẳng là冒着 rủi ro 'phá hủy nhu cầu' hoàn toàn."
|
||||
|
||||
**Thích nghi Cảm xúc và Rủi ro Hệ thống**
|
||||
|
||||
Theo thời gian, thị trường bắt đầu thể hiện dấu hiệu của sự thích nghi cảm xúc. Ban đầu, thị trường phản ứng mạnh với mọi tin tức, nhưng dần dần trở nên mệt mỏi và lọc bỏ các nhiễu loạn.
|
||||
|
||||
> "Thị trường mệt mỏi vì chiến tranh lọc bỏ tiếng ồn, theo Morning Bid công bố vào ngày 14 tháng 4 năm 2026."
|
||||
|
||||
Tuy nhiên, sự "thích nghi" này không có nghĩa là ổn định, mà là sự chấp nhận rủi ro cao hơn. Nhà đầu tư vẫn duy trì sự bi quan nhưng coi đó là dấu hiệu tích cực theo cách ngược lại.
|
||||
|
||||
> "Oliver Pursche phân tích thị trường chứng khoán Mỹ và coi sự bi quan cao của nhà đầu tư là dấu hiệu ngược lại đáng tin cậy."
|
||||
|
||||
Kết quả là, thị trường tồn tại trong trạng thái căng thẳng cao độ, nơi mà bất kỳ sự thay đổi nào về đàm phán hòa bình hoặc leo thang quân sự đều có thể kích hoạt các điểm gãy cung cầu.
|
||||
|
||||
> "Các nhà phân tích cho thấy sự sụp đổ của các cuộc đàm phán hòa bình là rủi ro tăng giá cho thị trường."
|
||||
|
||||
Tóm lại, tương lai của thị trường dầu mỏ và tài chính được định hình bởi sự biến động cấu trúc do địa chính trị, sự phân hóa giữa các tác nhân giao dịch ngắn hạn và các chuyên gia dài hạn, cùng với sự thích nghi cảm xúc của thị trường trước một môi trường rủi ro kéo dài.
|
||||
|
||||
## Phản ứng Phân hóa của Các Tác nhân: Từ Đặt cược Đòn bẩy đến Sự Thận trọng Chiến lược
|
||||
|
||||
Chương này đi sâu vào sự phân hóa rõ rệt trong cách các tác nhân thị trường phản ứng với cú sốc địa chính trị. Trong khi một nhóm nhà giao dịch mạo hiểm sử dụng đòn bẩy để đặt cược vào các kết quả ngoại giao ngắn hạn, thì các chuyên gia và tổ chức tài chính lớn lại rơi vào trạng thái thận trọng chiến lược, thậm chí là "bay mù" trước thực tế nguồn cung vật lý bị thắt chặt nghiêm trọng.
|
||||
|
||||
**Nhà giao dịch đòn bẩy: Đặt cược vào hòa bình và trả giá đắt**
|
||||
|
||||
Trong giai đoạn đầu của cuộc xung đột, tâm lý thị trường bị chi phối mạnh mẽ bởi hy vọng về các thỏa thuận ngoại giao. Các nhà giao dịch hàng hóa, đặc biệt là những người sử dụng đòn bẩy cao, đã tập trung vào khả năng đạt được một thỏa thuận hòa bình lâu dài, dẫn đến những vị thế đầu cơ rủi ro.
|
||||
|
||||
> "Giá dầu đã nằm dưới mức 100 đô la một thùng, với các nhà giao dịch chú ý đến hy vọng về một thỏa thuận hòa bình lâu dài."
|
||||
|
||||
Tuy nhiên, sự lạc quan này đã dẫn đến những tổn thất nặng nề khi thực tế nguồn cung vật lý không thể phục hồi nhanh chóng. Các nhà giao dịch đã thất bại trong việc dự đoán tác động của việc Iran phong tỏa Eo biển Hormuz, một kịch bản trước đó được coi là khó xảy ra.
|
||||
|
||||
> "Các nhà giao dịch hàng hóa đã chịu thiệt hại hàng tỷ đô la vì họ đã đặt cược sai hướng về giá dầu."
|
||||
|
||||
Dữ liệu giao dịch cho thấy sự đầu cơ mạnh mẽ ngay trước các sự kiện địa chính trị quan trọng.
|
||||
|
||||
> "Nhà đầu tư đã đặt cược trị giá khoảng 760 triệu đô la vào việc giá dầu giảm khoảng 20 phút trước khi Iran công bố việc mở lại Eo biển Hormuz."
|
||||
|
||||
> "Nhà đầu tư đã đặt cược 950 triệu đô la vào giá dầu chỉ vài giờ trước khi Hoa Kỳ và Iran tuyên bố ngừng bắn."
|
||||
|
||||
Những hoạt động giao dịch này đã thu hút sự chú ý của cơ quan quản lý.
|
||||
|
||||
> "Ủy ban Giao dịch Hàng hóa Tương lai Hoa Kỳ (CFTC) đang xem xét một loạt các giao dịch trong hợp đồng tương lai dầu mỏ được thực hiện ngay trước những thay đổi lớn trong chính sách chiến tranh Iran của Tổng thống Donald Trump."
|
||||
|
||||
**Chuyên gia và Tổ chức: Sự thận trọng chiến lược và "Bay mù"**
|
||||
|
||||
Ngược lại với sự đầu cơ ngắn hạn, các chuyên gia phân tích và tổ chức quốc tế nhận ra rằng tình hình nguồn cung nghiêm trọng hơn nhiều so với những gì thị trường tương lai đang định giá. Họ rơi vào trạng thái thận trọng chiến lược, cảnh báo về những rủi ro dài hạn mà các nhà giao dịch ngắn hạn bỏ qua.
|
||||
|
||||
> "Tình trạng thắt chặt nguồn cung dầu thô nghiêm trọng hơn nhiều so với những gì các nhà giao dịch tương lai có thể giả định."
|
||||
|
||||
Các nhà phân tích từ Wood Mackenzie đã chỉ ra rằng việc phục hồi nguồn cung sẽ mất nhiều thời gian hơn dự kiến, ngay cả khi có lệnh ngừng bắn.
|
||||
|
||||
> "Các nhà phân tích của Wood Mackenzie chỉ ra rằng dầu thô sẽ mất hai đến ba tuần để đến châu Âu sau khi lưu lượng tàu thuyền dọc theo Eo biển Hormuz trở lại bình thường."
|
||||
|
||||
Sự thiếu hiểu biết về tác động thực sự của việc mất nguồn tài nguyên năng lượng cũng dẫn đến một sự đánh giá thấp rủi ro từ phía các tổ chức tài chính lớn.
|
||||
|
||||
> "Các nhà kinh tế trong hầu hết các tổ chức tài chính đánh giá thấp tác động của việc mất nguồn tài nguyên năng lượng. Khái niệm sai lầm này giải thích tại sao các nhà kinh tế... và do đó nhà đầu tư, không đặc biệt lo lắng."
|
||||
|
||||
Tuy nhiên, ngay cả các chuyên gia cũng thừa nhận sự bất định cao độ.
|
||||
|
||||
> "Tom Kloza cho biết mọi người đang cố gắng đánh giá thị trường dầu mỏ sẽ trông như thế nào trong 'ngày hôm sau', nhưng mọi người đều đang bay mù."
|
||||
|
||||
**Tâm lý nhà đầu tư: Thích nghi cảm xúc và tập trung vào cơ bản**
|
||||
|
||||
Dưới áp lực của biến động giá, tâm lý nhà đầu tư cá nhân và tổ chức đã trải qua quá trình thích nghi cảm xúc. Ban đầu, thị trường phản ứng mạnh với mọi tuyên bố từ Nhà Trắng, nhưng dần dần trở nên mệt mỏi và tập trung vào các yếu tố cơ bản.
|
||||
|
||||
> "Thị trường mệt mỏi vì chiến tranh lọc bỏ tiếng ồn."
|
||||
|
||||
Mức độ bi quan cao của nhà đầu tư được một số chuyên gia coi là dấu hiệu ngược lại đáng tin cậy.
|
||||
|
||||
> "Oliver Pursche phân tích thị trường chứng khoán Mỹ và coi sự bi quan cao của nhà đầu tư là dấu hiệu ngược lại đáng tin cậy."
|
||||
|
||||
Khi giá dầu được định giá ổn định hơn, nhà đầu tư bắt đầu quay trở lại tập trung vào kết quả kinh doanh và dữ liệu kinh tế vĩ mô.
|
||||
|
||||
> "Nhà đầu tư đang tập trung lại vào kết quả kinh doanh và các yếu tố cơ bản của nền kinh tế."
|
||||
|
||||
Sự phân hóa này cho thấy một thị trường đang trong quá trình chuyển dịch từ phản ứng cảm xúc ngắn hạn sang một trạng thái thận trọng dài hạn, nơi rủi ro hệ thống từ sự thích nghi cảm xúc và điểm gãy cung cầu vẫn là mối đe dọa tiềm tàng.
|
||||
|
||||
## Xu hướng Nổi lên và Rủi ro Hệ thống: Bẫy Thông tin và Điểm gãy Cung cầu
|
||||
|
||||
Chương này khám phá sự hình thành của một "bẫy thông tin" sâu sắc và điểm gãy cung cầu đang định hình lại cấu trúc thị trường năng lượng trong tương lai. Kết quả mô phỏng cho thấy thị trường không còn phản ứng đơn thuần với tin tức, mà đang đối mặt với một nghịch lý hệ thống: sự lạc quan ngoại giao trên bề mặt đang che giấu một thực tế nguồn cung vật lý bị tổn thương nghiêm trọng và khó phục hồi.
|
||||
|
||||
**Bẫy Thông tin: Sự Mâu thuẫn giữa Hy vọng Ngoại giao và Thực tế Vật lý**
|
||||
|
||||
Trong môi trường tương lai được mô phỏng, thị trường dầu mỏ rơi vào trạng thái phân cực thông tin nghiêm trọng. Một mặt, các tuyên bố từ cấp cao chính trị, đặc biệt là từ Nhà Trắng, liên tục tạo ra những đợt hy vọng về hòa bình. Mặt khác, dữ liệu hậu cần và dòng chảy vật lý cho thấy một bức tranh u ám hơn nhiều.
|
||||
|
||||
> "Giá dầu tăng nhẹ khoảng 1% khi thị trường tập trung nhiều hơn vào gián đoạn nguồn cung và hạn chế vận chuyển hơn là các bình luận của Donald Trump rằng cuộc chiến với Iran sắp kết thúc."
|
||||
|
||||
Sự phân hóa này tạo ra một bẫy thông tin, nơi các nhà giao dịch ngắn hạn dễ dàng bị cuốn theo các tín hiệu ngoại giao, trong khi bỏ qua các rủi ro cấu trúc.
|
||||
|
||||
> "Thị trường diễn giải lệnh phong tỏa Eo biển Hormuz của Tổng thống Trump là một chiến thuật đàm phán nhằm cắt đứt doanh thu dầu mỏ của Iran, chứ không phải là một sự leo thang thực sự."
|
||||
|
||||
Tuy nhiên, theo thời gian, thị trường bắt đầu thể hiện dấu hiệu của sự mệt mỏi và thích nghi.
|
||||
|
||||
> "Morning Bid thảo luận về cách các thị trường mệt mỏi vì chiến tranh lọc bỏ tiếng ồn."
|
||||
|
||||
Điều này cho thấy một sự dịch chuyển tâm lý: nhà đầu tư dần nhận ra rằng các tuyên bố chính trị không thể thay đổi ngay lập tức thực tế của các đường ống bị phá hủy và tàu thuyền bị mắc kẹt.
|
||||
|
||||
**Điểm gãy Cung cầu: Sự Phục hồi Không đồng đều và "Phụ phí Dư thừa"**
|
||||
|
||||
Ngay cả trong kịch bản lạc quan nhất khi lệnh ngừng bắn được ký kết và Eo biển Hormuz mở cửa trở lại, mô phỏng cho thấy nguồn cung dầu mỏ sẽ không thể phục hồi nhanh chóng. Đây chính là "điểm gãy cung cầu" mới, nơi mà sự thiếu hụt không chỉ đến từ xung đột, mà còn từ sự suy yếu của toàn bộ chuỗi cung ứng hạ tầng.
|
||||
|
||||
Các chuyên gia trong mô phỏng đã chỉ ra những rào cản vật lý khổng lồ:
|
||||
|
||||
> "Karan Satwani nhận thấy tình trạng thiếu hụt thiết bị sẵn có và nhân công chuyên biệt sẵn sàng triển khai đến các cơ sở hạ tầng năng lượng bị hư hỏng, bất kể cuộc chiến kết thúc vào khi nào."
|
||||
|
||||
Hơn nữa, hậu quả của việc phong tỏa tạo ra một nút thắt logistics khó giải quyết.
|
||||
|
||||
> "Joe DeLaura cho biết về việc mất sản lượng vĩnh viễn từ các giếng dầu bị đóng cửa ở Saudi, Kuwait, UAE và Iraq, thiệt hại nhà máy lọc dầu và đường ống, cùng thời gian khởi động lại vật lý, bên cạnh hàng đợi hơn 800 tàu chở dầu bị mắc kẹt ở phía tây Eo biển."
|
||||
|
||||
Kết quả là, thị trường sẽ không còn định giá theo kịch bản "ngừng cung hoàn toàn", nhưng cũng không thể trở lại trạng thái bình thường cũ.
|
||||
|
||||
> "Các nhà phân tích của Gelber nhận định kết quả là một thị trường không còn định giá một sự gián đoạn quy mô lớn, nhưng vẫn duy trì một phụ phí dư thừa khi dòng chảy phục hồi không đồng đều thay vì bật trở lại bình thường tại Eo biển Hormuz."
|
||||
|
||||
**Rủi ro Hệ thống: Giao dịch Nội bộ và Sự can thiệp của Cơ quan Quản lý**
|
||||
|
||||
Sự chênh lệch thông tin giữa các nhà hoạch định chính sách và thị trường giao dịch đã kích hoạt các rủi ro hệ thống về đạo đức và tuân thủ. Mô phỏng cho thấy sự xuất hiện của các giao dịch "đúng thời điểm" đáng ngờ, dẫn đến sự can thiệp mạnh mẽ từ các cơ quan quản lý.
|
||||
|
||||
> "Ủy ban Giao dịch Hàng hóa Tương lai Hoa Kỳ (CFTC) đang xem xét một loạt các giao dịch trong hợp đồng tương lai dầu mỏ được thực hiện ngay trước những thay đổi lớn trong chính sách chiến tranh Iran của Tổng thống Donald Trump."
|
||||
|
||||
Điều này cho thấy thông tin địa chính trị nhạy cảm đang bị lợi dụng, tạo ra một lớp rủi ro pháp lý mới cho thị trường tài chính.
|
||||
|
||||
> "Dữ liệu được yêu cầu từ các sàn giao dịch về giao dịch đúng thời điểm trong thị trường dầu mỏ bao gồm các định danh Tag 50 của các thực thể đứng sau các giao dịch."
|
||||
|
||||
Sự can thiệp này không chỉ nhằm trừng phạt cá nhân, mà còn là nỗ lực để khôi phục niềm tin vào tính minh bạch của thị trường khi nó đang bị xói mòn bởi "bẫy thông tin" và sự không chắc chắn địa chính trị.
|
||||
|
||||
**Xu hướng Giá và Rủi ro Ngầm**
|
||||
|
||||
Tổng hợp lại, xu hướng giá trong tương lai này sẽ không giảm sâu ngay cả khi có tin tức hòa bình. Sự phục hồi của giá sẽ được hỗ trợ bởi "phụ phí dư thừa" và thực tế nguồn cung bị thắt chặt dài hạn.
|
||||
|
||||
> "Các nhà phân tích cho biết rủi ro tăng giá chính của thị trường là sự sụp đổ của các cuộc đàm phán hòa bình giữa Hoa Kỳ và Iran, khi các yêu cầu của hai bên vẫn còn cách xa nhau."
|
||||
|
||||
Thị trường đang bước vào giai đoạn mới, nơi mà giá dầu không chỉ phản ánh cung cầu tức thời, mà còn là thước đo của sự phục hồi hạ tầng và độ tin cậy của thông tin địa chính trị.
|
||||
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "1606cd6c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "69ce47c3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"input = \"/home/anman/intern/MiroFish/backend/logs/proj_ac763defa255/cost_Qwen_Qwen3.6-27B-FP8.jsonl\"\n",
|
||||
"\n",
|
||||
"data=[]\n",
|
||||
"with open(input, 'r', encoding='utf-8') as f:\n",
|
||||
" data = [json.loads(line.strip()) for line in f if line.strip()]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "6f1fe377",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Input Tokens: 49830604\n",
|
||||
"Output Tokens: 5432209\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"input_tokens = 0\n",
|
||||
"output_tokens = 0\n",
|
||||
"for item in data:\n",
|
||||
" input_tokens += item['input_tokens']\n",
|
||||
" output_tokens += item['output_tokens']\n",
|
||||
"\n",
|
||||
"print(f\"Input Tokens: {input_tokens}\")\n",
|
||||
"print(f\"Output Tokens: {output_tokens}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "95cce20c",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Total Cost: $41.2119\n",
|
||||
"Cost Breakdown: Input Cost = $24.9153, Output Cost = $16.2966\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"cost_1M_input = 0.5\n",
|
||||
"cost_1M_output = 3.0\n",
|
||||
"\n",
|
||||
"total_cost = (input_tokens / 1_000_000) * cost_1M_input + (output_tokens / 1_000_000) * cost_1M_output\n",
|
||||
"print(f\"Total Cost: ${total_cost:.4f}\")\n",
|
||||
"print(f\"Cost Breakdown: Input Cost = ${(input_tokens / 1_000_000) * cost_1M_input:.4f}, Output Cost = ${(output_tokens / 1_000_000) * cost_1M_output:.4f}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "273cd657",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "mirofish_v1",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.15"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
# Full Pipeline Runner
|
||||
|
||||
Chạy toàn bộ MiroFish pipeline từ command line, không cần mở UI.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Vai trò |
|
||||
|---|---|
|
||||
| `prepare_input.py` | Đọc config, load & filter articles, ghi combined text ra temp file, in JSON metadata ra stdout |
|
||||
| `run.sh` | Gọi `prepare_input.py`, khởi động backend nếu cần, rồi gọi tuần tự các API |
|
||||
| `config_articles.env` | Cấu hình input |
|
||||
|
||||
|
||||
## Cấu hình (`config_articles.env`)
|
||||
|
||||
| Tham số | Bắt buộc | Mô tả |
|
||||
|---|---|---|
|
||||
| `article_path` | Có | Đường dẫn thư mục chứa `articles.jsonl` và thư mục `news/` |
|
||||
| `full_content` | Không | `True` = giữ toàn bộ markdown; `False` (mặc định) = chỉ lấy body bài viết, bỏ YAML frontmatter |
|
||||
| `start_time` | Không | Lọc bài viết từ ngày (`YYYY-MM-DD`), để trống = không lọc |
|
||||
| `end_time` | Không | Lọc bài viết đến ngày (`YYYY-MM-DD`), để trống = không lọc |
|
||||
| `simulation_requirement` | Không | Mô tả yêu cầu phân tích/dự báo gửi cho LLM |
|
||||
| `project_name` | Không | Tên project |
|
||||
|
||||
---
|
||||
|
||||
## Cách 1 — Chạy toàn bộ pipeline 1 lệnh (khuyến nghị)
|
||||
|
||||
**Không cần mở terminal riêng cho backend.** `run.sh` tự kiểm tra và khởi động Flask nếu chưa chạy.
|
||||
|
||||
Chạy 7 bước tự động: ontology → graph → create sim → prepare sim → start sim → chờ hoàn thành → generate report.
|
||||
|
||||
```bash
|
||||
# Từ project root
|
||||
cd /home/anman/intern/MiroFish
|
||||
bash test_code_backend/full_pipeline/run.sh
|
||||
|
||||
# Hoặc chỉ định config khác
|
||||
bash test_code_backend/full_pipeline/run.sh path/to/config.env
|
||||
```
|
||||
|
||||
Log in ra terminal từng bước. Cuối cùng in ra `project_id`, `graph_id`, `simulation_id`, `report_id`.
|
||||
|
||||
> Backend được tự động khởi động trong background — **không cần mở terminal riêng**. Log đầy đủ nằm ở `backend/logs/YYYY-MM-DD.log`.
|
||||
|
||||
Muốn kill port
|
||||
```bash
|
||||
kill $(lsof -ti :5001)
|
||||
```
|
||||
---
|
||||
|
||||
## Cách 2 — Chạy từng bước thủ công
|
||||
|
||||
Khi cần debug hoặc muốn kiểm tra kết quả từng bước.
|
||||
|
||||
### Bước 0: Khởi động Flask backend (port 5001)
|
||||
|
||||
Mở **terminal riêng**, giữ nguyên trong suốt quá trình:
|
||||
|
||||
```bash
|
||||
cd /home/anman/intern/MiroFish
|
||||
FLASK_PORT=5001 npm run backend
|
||||
# hoặc: python backend/run.py
|
||||
```
|
||||
|
||||
Kiểm tra backend đã sẵn sàng:
|
||||
```bash
|
||||
curl http://localhost:5001/health
|
||||
# → {"status": "ok", "service": "MiroFish Backend"}
|
||||
```
|
||||
|
||||
### Bước 1: Chuẩn bị input (Python)
|
||||
|
||||
Chạy trong **terminal khác**:
|
||||
|
||||
```bash
|
||||
cd /home/anman/intern/MiroFish
|
||||
python3 test_code_backend/full_pipeline/prepare_input.py \
|
||||
test_code_backend/full_pipeline/config_articles.env
|
||||
```
|
||||
|
||||
Output là JSON, ví dụ:
|
||||
```json
|
||||
{
|
||||
"combined_file": "/tmp/mirofish_articles_abc123.md",
|
||||
"article_count": 42,
|
||||
"project_name": "Oil Market Sentiment Pipeline",
|
||||
"simulation_requirement": "Analyze how ...",
|
||||
"start_time": null,
|
||||
"end_time": null
|
||||
}
|
||||
```
|
||||
|
||||
Lưu lại đường dẫn `combined_file` để dùng ở bước tiếp theo.
|
||||
|
||||
---
|
||||
|
||||
### Bước 2: Generate ontology → lấy `project_id`
|
||||
|
||||
```bash
|
||||
COMBINED_FILE="/home/anman/intern/MiroFish/test_code_backend/full_pipeline/data/articles_combined.md"
|
||||
|
||||
COMBINED_FILE="/home/anman/intern/MiroFish/data/news/2026-02/2026-02-03_oil-prices-add-gains-after-report-says_0178c4.md"
|
||||
|
||||
curl -s http://localhost:5001/api/graph/ontology/generate \
|
||||
-F "files=@${COMBINED_FILE};filename=articles.md" \
|
||||
-F "simulation_requirement=Analyze how these oil and financial market news articles affect investor sentiment and market dynamics. Predict how different market participants (traders, analysts, retail investors) will react and what the overall price trend will be." \
|
||||
-F "project_name=Oil Market Pipeline" \
|
||||
| jq '{project_id: .data.project_id, entity_types: [.data.ontology.entity_types[].name]}'
|
||||
```
|
||||
|
||||
Lưu lại `project_id`.
|
||||
|
||||
---
|
||||
|
||||
### Bước 3: Build knowledge graph → lấy `task_id`
|
||||
|
||||
```bash
|
||||
PROJECT_ID="proj_xxxxxxxxxxxx"
|
||||
|
||||
curl -s http://localhost:5001/api/graph/build \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"project_id\": \"$PROJECT_ID\"}" \
|
||||
| jq '{task_id: .data.task_id}'
|
||||
```
|
||||
|
||||
Poll tiến độ build (chạy lặp lại đến khi `status=completed`):
|
||||
|
||||
```bash
|
||||
TASK_ID="task_xxxxxxxxxxxx"
|
||||
|
||||
curl -s http://localhost:5001/api/graph/task/$TASK_ID \
|
||||
| jq '{status: .data.status, progress: .data.progress, graph_id: .data.result.graph_id}'
|
||||
```
|
||||
|
||||
Khi `status=completed`, lưu lại `graph_id`.
|
||||
|
||||
---
|
||||
|
||||
### Bước 4: Tạo simulation → lấy `simulation_id`
|
||||
|
||||
```bash
|
||||
PROJECT_ID="proj_xxxxxxxxxxxx"
|
||||
|
||||
curl -s http://localhost:5001/api/simulation/create \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"project_id\": \"$PROJECT_ID\", \"enable_twitter\": true, \"enable_reddit\": true}" \
|
||||
| jq '{simulation_id: .data.simulation_id}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bước 5: Prepare simulation (sinh agent profiles + config)
|
||||
|
||||
```bash
|
||||
SIM_ID="sim_xxxxxxxxxxxx"
|
||||
|
||||
curl -s http://localhost:5001/api/simulation/prepare \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"simulation_id\": \"$SIM_ID\", \"use_llm_for_profiles\": true, \"parallel_profile_count\": 5}" \
|
||||
| jq '{task_id: .data.task_id, status: .data.status}'
|
||||
```
|
||||
|
||||
Poll tiến độ (bước này lâu nhất, ~5–15 phút tùy số agents):
|
||||
|
||||
```bash
|
||||
TASK_ID="task_xxxxxxxxxxxx"
|
||||
|
||||
curl -s http://localhost:5001/api/simulation/prepare/status \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"task_id\": \"$TASK_ID\", \"simulation_id\": \"$SIM_ID\"}" \
|
||||
| jq '{status: .data.status, progress: .data.progress, message: .data.message}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bước 6: Start simulation
|
||||
|
||||
```bash
|
||||
SIM_ID="sim_xxxxxxxxxxxx"
|
||||
|
||||
curl -s http://localhost:5001/api/simulation/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"simulation_id\": \"$SIM_ID\", \"platform\": \"parallel\"}" \
|
||||
| jq '{runner_status: .data.runner_status, total_rounds: .data.total_rounds}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bước 7: Theo dõi simulation đến khi hoàn thành
|
||||
|
||||
Chạy lệnh sau để tự động poll mỗi 30 giây, tự thoát khi simulation kết thúc:
|
||||
|
||||
```bash
|
||||
SIM_ID="sim_xxxxxxxxxxxx"
|
||||
|
||||
while true; do
|
||||
RESP=$(curl -s http://localhost:5001/api/simulation/$SIM_ID/run-status)
|
||||
RS=$(echo "$RESP" | jq -r '.data.runner_status')
|
||||
CR=$(echo "$RESP" | jq -r '.data.current_round')
|
||||
TR=$(echo "$RESP" | jq -r '.data.total_rounds')
|
||||
echo "[$(date '+%H:%M:%S')] $RS round=$CR/$TR"
|
||||
[[ "$RS" == "completed" || "$RS" == "stopped" || "$RS" == "failed" ]] && break
|
||||
sleep 30
|
||||
done
|
||||
```
|
||||
|
||||
Hoặc kiểm tra thủ công một lần:
|
||||
|
||||
```bash
|
||||
# Trạng thái tổng quan (round hiện tại, % tiến độ)
|
||||
curl -s http://localhost:5001/api/simulation/$SIM_ID/run-status \
|
||||
| jq '{runner_status: .data.runner_status, current_round: .data.current_round, total_rounds: .data.total_rounds}'
|
||||
|
||||
# Hành động của agents (real-time)
|
||||
curl -s "http://localhost:5001/api/simulation/$SIM_ID/actions?limit=20" | jq .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bước 8: Generate report
|
||||
|
||||
```bash
|
||||
SIM_ID="sim_xxxxxxxxxxxx"
|
||||
|
||||
SIM_ID="sim_07ab325b3818"
|
||||
# 1. Bắt đầu generate → lấy task_id và report_id
|
||||
RESP=$(curl -s http://localhost:5001/api/report/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"simulation_id\": \"$SIM_ID\"}")
|
||||
TASK_ID=$(echo "$RESP" | jq -r '.data.task_id')
|
||||
REPORT_ID=$(echo "$RESP" | jq -r '.data.report_id')
|
||||
echo "task_id=$TASK_ID report_id=$REPORT_ID"
|
||||
|
||||
# 2. Poll đến khi completed (chạy lặp lại, ~5–15 phút)
|
||||
while true; do
|
||||
R=$(curl -s http://localhost:5001/api/report/generate/status \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"task_id\": \"$TASK_ID\"}")
|
||||
ST=$(echo "$R" | jq -r '.data.status')
|
||||
PG=$(echo "$R" | jq -r '.data.progress')
|
||||
echo "$ST ${PG}%"
|
||||
[[ "$ST" == "completed" ]] && break
|
||||
[[ "$ST" == "failed" ]] && { echo "FAILED"; break; }
|
||||
sleep 15
|
||||
done
|
||||
|
||||
# 3. Download file Markdown
|
||||
curl -s http://localhost:5001/api/report/$REPORT_ID/download -o report.md
|
||||
echo "Saved to report.md"
|
||||
```
|
||||
|
||||
Nếu sim đã có report rồi mà muốn chạy lại thì dùng như sau
|
||||
|
||||
```bash
|
||||
SIM_ID="sim_xxxxxxxxxxxx"
|
||||
|
||||
SIM_ID="sim_07ab325b3818"
|
||||
# 1. Bắt đầu generate → lấy task_id và report_id
|
||||
RESP=$(curl -s http://localhost:5001/api/report/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"simulation_id\": \"$SIM_ID\", \"force_regenerate\": true}")
|
||||
TASK_ID=$(echo "$RESP" | jq -r '.data.task_id')
|
||||
REPORT_ID=$(echo "$RESP" | jq -r '.data.report_id')
|
||||
echo "task_id=$TASK_ID report_id=$REPORT_ID"
|
||||
|
||||
# 2. Poll đến khi completed (chạy lặp lại, ~5–15 phút)
|
||||
while true; do
|
||||
R=$(curl -s http://localhost:5001/api/report/generate/status \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"task_id\": \"$TASK_ID\"}")
|
||||
ST=$(echo "$R" | jq -r '.data.status')
|
||||
PG=$(echo "$R" | jq -r '.data.progress')
|
||||
echo "$ST ${PG}%"
|
||||
[[ "$ST" == "completed" ]] && break
|
||||
[[ "$ST" == "failed" ]] && { echo "FAILED"; break; }
|
||||
sleep 15
|
||||
done
|
||||
|
||||
# 3. Download file Markdown
|
||||
curl -s http://localhost:5001/api/report/$REPORT_ID/download -o report.md
|
||||
echo "Saved to report.md"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output files
|
||||
|
||||
| Loại | Đường dẫn |
|
||||
|---|---|
|
||||
| Project metadata + extracted text | `backend/uploads/projects/<project_id>/` |
|
||||
| Agent profiles (Reddit) | `backend/uploads/simulations/<sim_id>/reddit_profiles.json` |
|
||||
| Agent profiles (Twitter) | `backend/uploads/simulations/<sim_id>/twitter_profiles.csv` |
|
||||
| Simulation config (LLM-generated) | `backend/uploads/simulations/<sim_id>/simulation_config.json` |
|
||||
| Run state & action log | `backend/uploads/simulations/<sim_id>/run_state.json` |
|
||||
| Report | `backend/uploads/reports/<report_id>/` |
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# ─── Article input ────────────────────────────────────────────────────────────
|
||||
# Path to the folder containing articles.jsonl (and the news/ sub-folder)
|
||||
article_path = "/home/anman/intern/MiroFish/data"
|
||||
|
||||
# If True → use full markdown (frontmatter + body)
|
||||
# If False → strip YAML frontmatter, keep article body only
|
||||
full_content = False
|
||||
|
||||
# Date filter (YYYY-MM-DD). Leave blank to load all dates.
|
||||
# start_time = 2026-02-09
|
||||
# end_time = 2026-02-12
|
||||
start_time = 2026-04-13
|
||||
end_time = 2026-04-14
|
||||
|
||||
# ─── Simulation settings ──────────────────────────────────────────────────────
|
||||
# What the simulation should analyse / predict (sent to the LLM)
|
||||
simulation_requirement = "Analyze how these oil and financial market news articles affect investor sentiment and market dynamics. Predict how different market participants (traders, analysts, retail investors) will react and what the overall price trend will be."
|
||||
|
||||
# Display name for the project (optional)
|
||||
project_name = "Oil Market Sentiment Pipeline"
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Prepare article input for MiroFish pipeline.
|
||||
|
||||
Reads config_articles.env, loads and filters articles from articles.jsonl,
|
||||
combines their text, and writes the result to a temp file.
|
||||
|
||||
Outputs a single JSON to stdout:
|
||||
{
|
||||
"combined_file": "/home/anman/intern/MiroFish/test_code_backend/full_pipeline/data/articles_combined.md",
|
||||
"article_count": 42,
|
||||
"project_name": "...",
|
||||
"simulation_requirement": "...",
|
||||
"start_time": "2025-11-01", # or null
|
||||
"end_time": "2025-11-30" # or null
|
||||
}
|
||||
|
||||
Usage:
|
||||
python prepare_input.py [config_articles.env]
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ─── Config parser ────────────────────────────────────────────────────────────
|
||||
path_output = "/home/anman/intern/MiroFish/test_code_backend/full_pipeline/data"
|
||||
|
||||
def parse_config(path: str) -> dict:
|
||||
"""Parse 'key = value' config file (spaces around = allowed, # = comment).
|
||||
Values may optionally be wrapped in single or double quotes."""
|
||||
cfg = {}
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if value:
|
||||
cfg[key] = value
|
||||
return cfg
|
||||
|
||||
|
||||
# ─── Article loading ──────────────────────────────────────────────────────────
|
||||
|
||||
def strip_frontmatter(content: str) -> str:
|
||||
"""Remove YAML frontmatter (--- … ---), return article body only."""
|
||||
if content.startswith("---"):
|
||||
end = content.find("---", 3)
|
||||
if end != -1:
|
||||
return content[end + 3:].lstrip("\n")
|
||||
return content
|
||||
|
||||
|
||||
def load_articles(article_path: str, full_content: bool,
|
||||
start_time: str, end_time: str) -> list:
|
||||
jsonl_path = os.path.join(article_path, "articles.jsonl")
|
||||
if not os.path.exists(jsonl_path):
|
||||
raise FileNotFoundError(f"articles.jsonl not found: {jsonl_path}")
|
||||
|
||||
start_dt = datetime.strptime(start_time, "%Y-%m-%d").date() if start_time else None
|
||||
end_dt = datetime.strptime(end_time, "%Y-%m-%d").date() if end_time else None
|
||||
|
||||
articles = []
|
||||
with open(jsonl_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
record = json.loads(line)
|
||||
date_str = record.get("published_date", "")
|
||||
file_rel = record.get("file_path", "")
|
||||
if not date_str or not file_rel:
|
||||
continue
|
||||
|
||||
try:
|
||||
date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if start_dt and date < start_dt:
|
||||
continue
|
||||
if end_dt and date > end_dt:
|
||||
continue
|
||||
|
||||
full_path = os.path.join(article_path, file_rel)
|
||||
if not os.path.exists(full_path):
|
||||
continue
|
||||
|
||||
with open(full_path, encoding="utf-8") as mdf:
|
||||
raw = mdf.read()
|
||||
|
||||
content = raw if full_content else strip_frontmatter(raw)
|
||||
articles.append({"date": date_str, "file_path": file_rel, "content": content})
|
||||
|
||||
articles.sort(key=lambda a: a["date"])
|
||||
return articles
|
||||
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
script_dir = Path(__file__).parent
|
||||
config_path = sys.argv[1] if len(sys.argv) > 1 else str(script_dir / "config_articles.env")
|
||||
|
||||
cfg = parse_config(config_path)
|
||||
|
||||
article_path = cfg.get("article_path", "")
|
||||
full_content = cfg.get("full_content", "False").strip().lower() in ("true", "1", "yes")
|
||||
start_time = cfg.get("start_time") or ""
|
||||
end_time = cfg.get("end_time") or ""
|
||||
sim_req = cfg.get(
|
||||
"simulation_requirement",
|
||||
"Analyze how these news articles affect market sentiment and predict participant behavior.",
|
||||
)
|
||||
project_name = cfg.get("project_name", "MiroFish Pipeline")
|
||||
|
||||
if not article_path:
|
||||
print(json.dumps({"error": "article_path is required"}))
|
||||
sys.exit(1)
|
||||
|
||||
articles = load_articles(article_path, full_content, start_time, end_time)
|
||||
if not articles:
|
||||
print(json.dumps({"error": "No articles found for the given parameters"}))
|
||||
sys.exit(1)
|
||||
|
||||
# Write combined text to path_output
|
||||
os.makedirs(path_output, exist_ok=True)
|
||||
out_path = os.path.join(path_output, "articles_combined.md")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
for art in articles:
|
||||
f.write(f"\n\n=== {art['file_path']} ({art['date']}) ===\n\n")
|
||||
f.write(art["content"])
|
||||
|
||||
print(json.dumps({
|
||||
"combined_file": out_path,
|
||||
"article_count": len(articles),
|
||||
"project_name": project_name,
|
||||
"simulation_requirement": sim_req,
|
||||
"start_time": start_time or None,
|
||||
"end_time": end_time or None,
|
||||
"word_count": sum(len(art["content"].split()) for art in articles)
|
||||
}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env bash
|
||||
# MiroFish full pipeline runner — calls prepare_input.py then MiroFish API.
|
||||
#
|
||||
# Usage:
|
||||
# bash run.sh
|
||||
# bash run.sh path/to/config.env
|
||||
#
|
||||
# Requirements: curl, jq, python3
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
BACKEND_DIR="$PROJECT_ROOT/backend"
|
||||
CONFIG_FILE="${1:-$SCRIPT_DIR/config_articles.env}"
|
||||
BACKEND_URL="http://localhost:5001"
|
||||
|
||||
info() { echo "[$(date '+%H:%M:%S')] INFO $*"; }
|
||||
err() { echo "[$(date '+%H:%M:%S')] ERROR $*" >&2; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
command -v curl >/dev/null 2>&1 || die "curl is required"
|
||||
command -v jq >/dev/null 2>&1 || die "jq is required (apt install jq)"
|
||||
command -v python3 >/dev/null 2>&1 || die "python3 is required"
|
||||
|
||||
[[ -f "$CONFIG_FILE" ]] || die "Config file not found: $CONFIG_FILE"
|
||||
|
||||
# ─── Step 0: Prepare input (Python) ──────────────────────────────────────────
|
||||
info "=== Step 0: Preparing article input ==="
|
||||
INPUT_JSON=$(python3 "$SCRIPT_DIR/prepare_input.py" "$CONFIG_FILE") \
|
||||
|| die "prepare_input.py failed"
|
||||
|
||||
# Check for error from prepare_input.py
|
||||
if echo "$INPUT_JSON" | jq -e '.error' >/dev/null 2>&1; then
|
||||
die "Input error: $(echo "$INPUT_JSON" | jq -r '.error')"
|
||||
fi
|
||||
|
||||
COMBINED_FILE=$(echo "$INPUT_JSON" | jq -r '.combined_file')
|
||||
ARTICLE_COUNT=$(echo "$INPUT_JSON" | jq -r '.article_count')
|
||||
PROJECT_NAME=$( echo "$INPUT_JSON" | jq -r '.project_name')
|
||||
SIM_REQ=$( echo "$INPUT_JSON" | jq -r '.simulation_requirement')
|
||||
|
||||
trap 'rm -f "$COMBINED_FILE"' EXIT # cleanup temp file on exit
|
||||
|
||||
info " articles : $ARTICLE_COUNT"
|
||||
info " project_name : $PROJECT_NAME"
|
||||
info " combined_file : $COMBINED_FILE ($(wc -c < "$COMBINED_FILE" | tr -d ' ') bytes)"
|
||||
|
||||
# ─── Start Flask backend (if not already running) ─────────────────────────────
|
||||
if curl -sf "$BACKEND_URL/health" >/dev/null 2>&1; then
|
||||
info "Backend already running at $BACKEND_URL"
|
||||
else
|
||||
info "Starting Flask backend..."
|
||||
VENV="$BACKEND_DIR/.venv"
|
||||
PYTHON=$( [[ -f "$VENV/bin/python" ]] && echo "$VENV/bin/python" || echo "python3" )
|
||||
|
||||
"$PYTHON" "$BACKEND_DIR/run.py" >/dev/null 2>&1 &
|
||||
|
||||
info "Waiting for backend to be ready..."
|
||||
for i in $(seq 1 30); do
|
||||
sleep 2
|
||||
curl -sf "$BACKEND_URL/health" >/dev/null 2>&1 && { info "Backend ready"; break; }
|
||||
[[ $i -eq 30 ]] && die "Backend did not start (check backend/logs/)"
|
||||
done
|
||||
fi
|
||||
|
||||
# ─── Helper: assert API response has success=true ─────────────────────────────
|
||||
ok() { echo "$1" | jq -r '.success // false'; }
|
||||
assert_ok() { [[ "$(ok "$1")" == "true" ]] || { err "Step '$2' failed:"; echo "$1" | jq . >&2; die "Aborted"; }; }
|
||||
|
||||
# ─── Helper: poll GET task until completed/failed ─────────────────────────────
|
||||
poll_task() {
|
||||
local url="$1" label="$2"
|
||||
while true; do
|
||||
local body; body=$(curl -sf "$url") || { sleep 10; continue; }
|
||||
local st msg
|
||||
st=$(echo "$body" | jq -r '.data.status // empty')
|
||||
pg=$(echo "$body" | jq -r '.data.progress // 0')
|
||||
msg=$(echo "$body"| jq -r '.data.message // empty')
|
||||
info " [$label] $st ${pg}% — $msg"
|
||||
[[ "$st" == "completed" ]] && return 0
|
||||
[[ "$st" == "failed" ]] && { err "$label failed: $msg"; die "Aborted"; }
|
||||
sleep 10
|
||||
done
|
||||
}
|
||||
|
||||
# ─── Step 1: Generate ontology ────────────────────────────────────────────────
|
||||
info "=== Step 1/7: Generating ontology ==="
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/graph/ontology/generate" \
|
||||
-F "files=@${COMBINED_FILE};filename=articles.md" \
|
||||
-F "simulation_requirement=${SIM_REQ}" \
|
||||
-F "project_name=${PROJECT_NAME}")
|
||||
assert_ok "$RESP" "ontology/generate"
|
||||
|
||||
PROJECT_ID=$(echo "$RESP" | jq -r '.data.project_id')
|
||||
info " project_id : $PROJECT_ID"
|
||||
|
||||
# ─── Step 2: Build knowledge graph ────────────────────────────────────────────
|
||||
info "=== Step 2/7: Building Zep knowledge graph ==="
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/graph/build" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg p "$PROJECT_ID" --arg n "$PROJECT_NAME" \
|
||||
'{project_id: $p, graph_name: $n}')")
|
||||
assert_ok "$RESP" "graph/build"
|
||||
|
||||
TASK_ID=$(echo "$RESP" | jq -r '.data.task_id')
|
||||
info " task_id : $TASK_ID"
|
||||
poll_task "$BACKEND_URL/api/graph/task/$TASK_ID" "graph/build"
|
||||
|
||||
GRAPH_ID=$(curl -sf "$BACKEND_URL/api/graph/task/$TASK_ID" | jq -r '.data.result.graph_id')
|
||||
info " graph_id : $GRAPH_ID"
|
||||
|
||||
# ─── Step 3: Create simulation ────────────────────────────────────────────────
|
||||
info "=== Step 3/7: Creating simulation ==="
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/simulation/create" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg p "$PROJECT_ID" \
|
||||
'{project_id: $p, enable_twitter: true, enable_reddit: true}')")
|
||||
assert_ok "$RESP" "simulation/create"
|
||||
|
||||
SIM_ID=$(echo "$RESP" | jq -r '.data.simulation_id')
|
||||
info " simulation_id : $SIM_ID"
|
||||
|
||||
# ─── Step 4: Prepare simulation ───────────────────────────────────────────────
|
||||
info "=== Step 4/7: Preparing simulation (agent profiles + config) ==="
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/simulation/prepare" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg s "$SIM_ID" \
|
||||
'{simulation_id: $s, use_llm_for_profiles: true, parallel_profile_count: 5}')")
|
||||
assert_ok "$RESP" "simulation/prepare"
|
||||
|
||||
if [[ "$(echo "$RESP" | jq -r '.data.already_prepared')" == "true" ]]; then
|
||||
info " Already prepared, skipping poll"
|
||||
else
|
||||
TASK_ID=$(echo "$RESP" | jq -r '.data.task_id')
|
||||
info " task_id : $TASK_ID"
|
||||
|
||||
while true; do
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/simulation/prepare/status" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg t "$TASK_ID" --arg s "$SIM_ID" \
|
||||
'{task_id: $t, simulation_id: $s}')")
|
||||
st=$(echo "$RESP" | jq -r '.data.status // empty')
|
||||
pg=$(echo "$RESP" | jq -r '.data.progress // 0')
|
||||
msg=$(echo "$RESP" | jq -r '.data.message // empty')
|
||||
info " [prepare] $st ${pg}% — $msg"
|
||||
[[ "$st" == "completed" || "$st" == "ready" ]] && break
|
||||
[[ "$st" == "failed" ]] && { err "Prepare failed: $msg"; die "Aborted"; }
|
||||
sleep 15
|
||||
done
|
||||
fi
|
||||
|
||||
# ─── Step 5: Start simulation ─────────────────────────────────────────────────
|
||||
info "=== Step 5/7: Starting simulation (platform: parallel) ==="
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/simulation/start" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg s "$SIM_ID" '{simulation_id: $s, platform: "parallel"}')")
|
||||
assert_ok "$RESP" "simulation/start"
|
||||
|
||||
RUNNER_STATUS=$(echo "$RESP" | jq -r '.data.runner_status')
|
||||
TOTAL_ROUNDS=$(echo "$RESP" | jq -r '.data.total_rounds')
|
||||
info " runner_status : $RUNNER_STATUS"
|
||||
info " total_rounds : $TOTAL_ROUNDS"
|
||||
|
||||
# ─── Step 6: Wait for simulation to finish ──────────────────────────────────
|
||||
info "=== Step 6/7: Waiting for simulation to finish ==="
|
||||
info " (monitoring $SIM_ID — Ctrl-C to detach and let it run in background)"
|
||||
while true; do
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/simulation/$SIM_ID/run-status") || { sleep 15; continue; }
|
||||
RS=$(echo "$RESP" | jq -r '.data.runner_status // empty')
|
||||
CR=$(echo "$RESP" | jq -r '.data.current_round // 0')
|
||||
TR=$(echo "$RESP" | jq -r '.data.total_rounds // 0')
|
||||
info " runner_status=$RS round=$CR/$TR"
|
||||
[[ "$RS" == "completed" || "$RS" == "stopped" ]] && break
|
||||
[[ "$RS" == "failed" ]] && { err "Simulation failed"; die "Aborted"; }
|
||||
sleep 30
|
||||
done
|
||||
|
||||
# ─── Step 7: Generate report ─────────────────────────────────────────────────
|
||||
info "=== Step 7/7: Generating report ==="
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/report/generate" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg s "$SIM_ID" '{simulation_id: $s}')")
|
||||
assert_ok "$RESP" "report/generate"
|
||||
|
||||
REPORT_ID=$(echo "$RESP" | jq -r '.data.report_id')
|
||||
info " report_id : $REPORT_ID"
|
||||
|
||||
if [[ "$(echo "$RESP" | jq -r '.data.already_generated')" == "true" ]]; then
|
||||
info " Report already exists, skipping poll"
|
||||
else
|
||||
TASK_ID=$(echo "$RESP" | jq -r '.data.task_id')
|
||||
info " task_id : $TASK_ID"
|
||||
while true; do
|
||||
RESP=$(curl -sf "$BACKEND_URL/api/report/generate/status" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg t "$TASK_ID" '{task_id: $t}')") || { sleep 10; continue; }
|
||||
st=$(echo "$RESP" | jq -r '.data.status // empty')
|
||||
pg=$(echo "$RESP" | jq -r '.data.progress // 0')
|
||||
msg=$(echo "$RESP" | jq -r '.data.message // empty')
|
||||
info " [report] $st ${pg}% — $msg"
|
||||
[[ "$st" == "completed" ]] && break
|
||||
[[ "$st" == "failed" ]] && { err "Report failed: $msg"; die "Aborted"; }
|
||||
sleep 15
|
||||
done
|
||||
fi
|
||||
|
||||
# ─── Summary ──────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
info "============================================================"
|
||||
info "Pipeline complete"
|
||||
info " project_id : $PROJECT_ID"
|
||||
info " graph_id : $GRAPH_ID"
|
||||
info " simulation_id : $SIM_ID"
|
||||
info " report_id : $REPORT_ID"
|
||||
info "============================================================"
|
||||
info "Download report: curl $BACKEND_URL/api/report/$REPORT_ID/download -o report.md"
|
||||
Loading…
Reference in New Issue