edit prompts v2
This commit is contained in:
parent
e0f2308e4d
commit
ec26745e08
|
|
@ -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
|
|
@ -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
|
|
@ -984,7 +984,7 @@ Nhiệm vụ của bạn là:
|
|||
|
||||
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
|
||||
- **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 >)
|
||||
|
|
|
|||
|
|
@ -615,7 +615,7 @@ Hãy tạo JSON cấu hình thời gian.
|
|||
|
||||
Ví dụ Format như sau:
|
||||
{{
|
||||
"total_simulation_hours": 72,
|
||||
"total_simulation_hours": 72,
|
||||
"minutes_per_round": 60,
|
||||
"agents_per_hour_min": 5,
|
||||
"agents_per_hour_max": 50,
|
||||
|
|
|
|||
|
|
@ -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,104 +353,151 @@ 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"
|
||||
|
||||
# Nền tảng dùng cho metadata cost.
|
||||
# --- 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:
|
||||
|
|
@ -343,75 +505,90 @@ class SimulationManager:
|
|||
elif state.enable_reddit:
|
||||
runtime_platform = "reddit"
|
||||
else:
|
||||
runtime_platform = None
|
||||
|
||||
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
|
||||
metadata_platform=runtime_platform,
|
||||
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,
|
||||
|
|
@ -422,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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1419,7 +1419,7 @@ Trả về danh sách các câu hỏi phụ dưới định dạng JSON."""
|
|||
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')}")
|
||||
|
|
@ -1832,7 +1832,7 @@ Hãy tạo bản tóm tắt phỏng vấn."""
|
|||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=800
|
||||
max_tokens=4096
|
||||
)
|
||||
return summary
|
||||
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1057,6 +1057,7 @@ def create_model(config: Dict[str, Any], use_boost: bool = False):
|
|||
return ModelFactory.create(
|
||||
model_platform=ModelPlatformType.OPENAI,
|
||||
model_type=llm_model,
|
||||
timeout=1000
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1179,7 +1180,7 @@ async def run_twitter_simulation(
|
|||
agent_graph=result.agent_graph,
|
||||
platform=oasis.DefaultPlatformType.TWITTER,
|
||||
database_path=db_path,
|
||||
semaphore=30, # Giới hạn số request LLM đồng thời để tránh quá tải API
|
||||
semaphore=3, # Giới hạn số request LLM đồng thời để tránh quá tải API
|
||||
)
|
||||
|
||||
await result.env.reset()
|
||||
|
|
@ -1370,7 +1371,7 @@ async def run_reddit_simulation(
|
|||
agent_graph=result.agent_graph,
|
||||
platform=oasis.DefaultPlatformType.REDDIT,
|
||||
database_path=db_path,
|
||||
semaphore=30, # Giới hạn số request LLM đồng thời để tránh quá tải API
|
||||
semaphore=3, # Giới hạn số request LLM đồng thời để tránh quá tải API
|
||||
)
|
||||
|
||||
await result.env.reset()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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,262 @@
|
|||
# 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"
|
||||
|
||||
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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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-16
|
||||
|
||||
# ─── 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"
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
"""
|
||||
Test: Knowledge Graph build on Zep Cloud
|
||||
Input : - output/ontology/ontology_*.json (latest, or pass path as arg)
|
||||
- news articles (same date range as ontology)
|
||||
Output: output/graph/
|
||||
graph_<graph_id>.json — build summary (node/edge count, timing)
|
||||
entities_<graph_id>.json — entity detail: raw nodes + filtered result
|
||||
|
||||
Run:
|
||||
python build_graph.py # auto-picks latest ontology
|
||||
python build_graph.py output/ontology/ontology_xyz.json # specific ontology file
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
from datetime import datetime, date
|
||||
from pathlib import Path
|
||||
|
||||
# ── Path setup ───────────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent.parent
|
||||
BACKEND_DIR = PROJECT_ROOT / "backend"
|
||||
ONTOLOGY_DIR = SCRIPT_DIR.parent / "output" / "ontology"
|
||||
GRAPH_DIR = SCRIPT_DIR.parent / "output" / "graph"
|
||||
NEWS_DIR = PROJECT_ROOT / "data" / "news" / "2026-04"
|
||||
|
||||
sys.path.insert(0, str(BACKEND_DIR))
|
||||
GRAPH_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ── Config ───────────────────────────────────────────────────────────────────
|
||||
START_DATE = date(2026, 4, 20)
|
||||
END_DATE = date(2026, 4, 22)
|
||||
GRAPH_NAME = "MiroFish_Test_Apr2026"
|
||||
CHUNK_SIZE = 500
|
||||
CHUNK_OVERLAP = 50
|
||||
BATCH_SIZE = 3
|
||||
POLL_TIMEOUT = 600
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
def log(msg: str):
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}")
|
||||
|
||||
|
||||
def extract_body(md_text: str) -> str:
|
||||
s = md_text.strip()
|
||||
if s.startswith("---"):
|
||||
end = s.find("---", 3)
|
||||
if end != -1:
|
||||
return s[end + 3:].strip()
|
||||
return s
|
||||
|
||||
|
||||
def load_combined_text(news_dir: Path, start: date, end: date) -> str:
|
||||
parts = []
|
||||
for md_file in sorted(news_dir.glob("*.md")):
|
||||
try:
|
||||
file_date = date.fromisoformat(md_file.stem.split("_")[0])
|
||||
except ValueError:
|
||||
continue
|
||||
if not (start <= file_date <= end):
|
||||
continue
|
||||
body = extract_body(md_file.read_text(encoding="utf-8"))
|
||||
if body:
|
||||
parts.append(body)
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
|
||||
def pick_ontology_file(arg_path: str | None) -> Path:
|
||||
if arg_path:
|
||||
p = Path(arg_path)
|
||||
if not p.is_absolute():
|
||||
p = ONTOLOGY_DIR / p
|
||||
if not p.exists():
|
||||
raise FileNotFoundError(f"Ontology file not found: {p}")
|
||||
return p
|
||||
|
||||
candidates = sorted(ONTOLOGY_DIR.glob("ontology_*.json"), key=lambda f: f.stat().st_mtime)
|
||||
if not candidates:
|
||||
raise FileNotFoundError(
|
||||
f"No ontology_*.json found in {ONTOLOGY_DIR}. Run gen_ontology.py first."
|
||||
)
|
||||
return candidates[-1] # latest
|
||||
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Build Zep Knowledge Graph from ontology")
|
||||
parser.add_argument("ontology_file", nargs="?", help="Path to ontology JSON (default: latest in output/)")
|
||||
args = parser.parse_args()
|
||||
|
||||
from app.services.graph_builder import GraphBuilderService
|
||||
from app.services.text_processor import TextProcessor
|
||||
from app.services.zep_entity_reader import ZepEntityReader
|
||||
|
||||
# Load ontology
|
||||
ontology_path = pick_ontology_file(args.ontology_file)
|
||||
log(f"Using ontology: {ontology_path.name}")
|
||||
with open(ontology_path, encoding="utf-8") as f:
|
||||
payload = json.load(f)
|
||||
ontology = payload["ontology"]
|
||||
|
||||
# Load news text
|
||||
log(f"Loading news text: {START_DATE} → {END_DATE}")
|
||||
combined_text = load_combined_text(NEWS_DIR, START_DATE, END_DATE)
|
||||
log(f" Text length: {len(combined_text):,} chars")
|
||||
|
||||
svc = GraphBuilderService()
|
||||
t_total = time.time()
|
||||
|
||||
# Create graph
|
||||
log("Creating Zep graph...")
|
||||
graph_id = svc.create_graph(GRAPH_NAME)
|
||||
log(f" graph_id = {graph_id}")
|
||||
|
||||
# Apply ontology
|
||||
log("Applying ontology schema...")
|
||||
svc.set_ontology(graph_id, ontology)
|
||||
|
||||
# Chunk & upload
|
||||
chunks = TextProcessor.split_text(combined_text, CHUNK_SIZE, CHUNK_OVERLAP)
|
||||
log(f"Uploading {len(chunks)} chunks (batch_size={BATCH_SIZE})...")
|
||||
|
||||
episode_uuids = svc.add_text_batches(
|
||||
graph_id, chunks, BATCH_SIZE,
|
||||
progress_callback=lambda msg, _: log(f" {msg}"),
|
||||
)
|
||||
log(f" {len(episode_uuids)} episodes uploaded")
|
||||
|
||||
# Wait for processing
|
||||
log("Waiting for Zep to process episodes...")
|
||||
svc._wait_for_episodes(
|
||||
episode_uuids,
|
||||
progress_callback=lambda msg, _: log(f" {msg}"),
|
||||
timeout=POLL_TIMEOUT,
|
||||
)
|
||||
|
||||
# Fetch result
|
||||
log("Fetching graph stats...")
|
||||
graph_info = svc._get_graph_info(graph_id)
|
||||
elapsed = round(time.time() - t_total, 2)
|
||||
|
||||
log(f"Done in {elapsed}s — nodes={graph_info.node_count}, edges={graph_info.edge_count}")
|
||||
log(f" Entity types: {graph_info.entity_types}")
|
||||
|
||||
out_path = GRAPH_DIR / f"graph_{graph_id}.json"
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"graph_id": graph_id,
|
||||
"graph_name": GRAPH_NAME,
|
||||
"built_at": datetime.now().isoformat(),
|
||||
"elapsed_seconds": elapsed,
|
||||
"ontology_source": ontology_path.name,
|
||||
"date_range": {"start": str(START_DATE), "end": str(END_DATE)},
|
||||
"chunks_uploaded": len(chunks),
|
||||
"node_count": graph_info.node_count,
|
||||
"edge_count": graph_info.edge_count,
|
||||
"entity_types": graph_info.entity_types,
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
log(f"Saved → {out_path.name}")
|
||||
|
||||
# ── Entity detail ─────────────────────────────────────────────────────────
|
||||
# Raw graph data — lưu nguyên output của get_graph_data()
|
||||
log("Fetching raw graph data...")
|
||||
graph_data = svc.get_graph_data(graph_id)
|
||||
log(f" nodes={graph_data['node_count']}, edges={graph_data['edge_count']}")
|
||||
|
||||
# Filtered entities — lưu nguyên output của filter_defined_entities().to_dict()
|
||||
log("Running entity filter...")
|
||||
defined_types = [e["name"] for e in ontology.get("entity_types", [])]
|
||||
reader = ZepEntityReader()
|
||||
filtered = reader.filter_defined_entities(
|
||||
graph_id=graph_id,
|
||||
defined_entity_types=defined_types,
|
||||
enrich_with_edges=True,
|
||||
)
|
||||
log(f" total={filtered.total_count}, matched={filtered.filtered_count}")
|
||||
log(f" types found: {sorted(filtered.entity_types)}")
|
||||
|
||||
entities_path = GRAPH_DIR / f"entities_{graph_id}.json"
|
||||
with open(entities_path, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"graph_id": graph_id,
|
||||
"fetched_at": datetime.now().isoformat(),
|
||||
"ontology_entity_types": defined_types,
|
||||
"raw_graph_data": graph_data,
|
||||
"filtered_entities": filtered.to_dict(),
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
log(f"Saved → {entities_path.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
"""
|
||||
Test: Ontology generation from news articles
|
||||
Input : data/news/2026-04/ (date range configured below)
|
||||
Output: test_code_backend/output/ontology/ontology_<daterange>_<timestamp>.json
|
||||
|
||||
Run:
|
||||
python gen_ontology.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime, date
|
||||
from pathlib import Path
|
||||
|
||||
# ── Path setup ───────────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent.parent
|
||||
BACKEND_DIR = PROJECT_ROOT / "backend"
|
||||
ONTOLOGY_DIR = SCRIPT_DIR.parent / "output" / "ontology"
|
||||
NEWS_DIR = PROJECT_ROOT / "data" / "news" / "2026-04"
|
||||
|
||||
sys.path.insert(0, str(BACKEND_DIR))
|
||||
ONTOLOGY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ── Config ───────────────────────────────────────────────────────────────────
|
||||
START_DATE = date(2026, 4, 20)
|
||||
END_DATE = date(2026, 4, 22)
|
||||
|
||||
SIMULATION_REQUIREMENT = (
|
||||
"Simulate social media public opinion dynamics around global oil/energy markets "
|
||||
"and US-Iran geopolitical tensions during April 2026. "
|
||||
"Focus on how governments, corporations, media outlets, financial analysts, and "
|
||||
"ordinary citizens react and interact on platforms like Twitter and Reddit."
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
def log(msg: str):
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}")
|
||||
|
||||
|
||||
def extract_body(md_text: str) -> str:
|
||||
"""Strip YAML frontmatter (---...---), return only article body."""
|
||||
s = md_text.strip()
|
||||
if s.startswith("---"):
|
||||
end = s.find("---", 3)
|
||||
if end != -1:
|
||||
return s[end + 3:].strip()
|
||||
return s
|
||||
|
||||
|
||||
def load_articles(news_dir: Path, start: date, end: date) -> list[dict]:
|
||||
articles = []
|
||||
for md_file in sorted(news_dir.glob("*.md")):
|
||||
try:
|
||||
file_date = date.fromisoformat(md_file.stem.split("_")[0])
|
||||
except ValueError:
|
||||
continue
|
||||
if not (start <= file_date <= end):
|
||||
continue
|
||||
body = extract_body(md_file.read_text(encoding="utf-8"))
|
||||
if body:
|
||||
articles.append({"filename": md_file.name, "date": str(file_date), "body": body})
|
||||
return articles
|
||||
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
from app.services.ontology_generator import OntologyGenerator
|
||||
|
||||
log(f"Loading news: {START_DATE} → {END_DATE}")
|
||||
articles = load_articles(NEWS_DIR, START_DATE, END_DATE)
|
||||
log(f" {len(articles)} articles loaded")
|
||||
|
||||
log("Calling LLM to generate ontology...")
|
||||
t0 = time.time()
|
||||
ontology = OntologyGenerator().generate(
|
||||
document_texts=[a["body"] for a in articles],
|
||||
simulation_requirement=SIMULATION_REQUIREMENT,
|
||||
)
|
||||
elapsed = round(time.time() - t0, 2)
|
||||
|
||||
entity_names = [e["name"] for e in ontology.get("entity_types", [])]
|
||||
edge_names = [e["name"] for e in ontology.get("edge_types", [])]
|
||||
log(f"Done in {elapsed}s")
|
||||
log(f" Entities ({len(entity_names)}): {entity_names}")
|
||||
log(f" Edges ({len(edge_names)}): {edge_names}")
|
||||
|
||||
date_range = f"{START_DATE.strftime('%Y%m%d')}-{END_DATE.strftime('%Y%m%d')}"
|
||||
ts = datetime.now().strftime("%H%M%S")
|
||||
out_path = ONTOLOGY_DIR / f"ontology_{date_range}_{ts}.json"
|
||||
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"meta": {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"elapsed_seconds": elapsed,
|
||||
"date_range": {"start": str(START_DATE), "end": str(END_DATE)},
|
||||
"article_count": len(articles),
|
||||
},
|
||||
"ontology": ontology,
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
log(f"Saved → {out_path.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue