Merge pull request #3 from TQuynh109/feature/modified-prompts

Feature/modified prompts
This commit is contained in:
Nguyễn Ngọc Thanh Thư 2026-03-30 01:45:38 +07:00 committed by GitHub
commit 052042c26d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1317 additions and 340 deletions

View File

@ -14,6 +14,7 @@ from ..services.ontology_generator import OntologyGenerator
from ..services.graph_builder import GraphBuilderService
from ..services.text_processor import TextProcessor
from ..utils.file_parser import FileParser
from ..utils.llm_client import LLMClient
from ..utils.logger import get_logger
from ..models.task import TaskManager, TaskStatus
from ..models.project import ProjectManager, ProjectStatus
@ -30,8 +31,6 @@ def allowed_file(filename: str) -> bool:
return ext in Config.ALLOWED_EXTENSIONS
# ============== 项目管理接口 ==============
@graph_bp.route('/project/<project_id>', methods=['GET'])
def get_project(project_id: str):
"""
@ -213,7 +212,15 @@ def generate_ontology():
# Generate ontology definition using LLM
logger.info("Calling LLM to generate ontology definition...")
generator = OntologyGenerator()
generator = OntologyGenerator(
llm_client=LLMClient(
component="ontology_generator",
metadata={
"project_id": project.project_id,
"phase": "ontology_generation",
},
)
)
ontology = generator.generate(
document_texts=document_texts,
simulation_requirement=simulation_requirement,
@ -256,6 +263,7 @@ def generate_ontology():
# ============== API 2: Build graph ==============
# API ROUTE DEFINITION - Flash route handler for graph construction requests
@graph_bp.route('/build', methods=['POST'])
def build_graph():
"""
@ -278,6 +286,12 @@ def build_graph():
"message": "Graph build task started"
}
}
Graph builder workflow
- [1] - create_graph()
- [2] - set_ontology()
- [3] - split_text()
- [4] - add_text_batches()
"""
try:
logger.info("=== Start building graph ===")
@ -304,7 +318,8 @@ def build_graph():
"error": "Please provide project_id"
}), 400
# Lấy project
# RETRIEVE PROJECT CONTEXT
# Fetches project stae including ontology from previous phase
project = ProjectManager.get_project(project_id)
if not project:
return jsonify({
@ -381,7 +396,8 @@ def build_graph():
message="Initializing graph builder service..."
)
# Tạo graph builder service
# INITIALIZE GRAPH BUILDER
# Creates service instance with Zep Cloud API client
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
# Chunk text
@ -390,6 +406,7 @@ def build_graph():
message="Splitting text into chunks...",
progress=5
)
# [3] - Splits document into overlapping chunks for processing
chunks = TextProcessor.split_text(
text,
chunk_size=chunk_size,
@ -403,6 +420,8 @@ def build_graph():
message="Creating Zep graph...",
progress=10
)
# [1] - Initializes empty graph in Zep Cloud with unique ID
graph_id = builder.create_graph(name=graph_name)
# Cập nhật graph_id của project
@ -415,11 +434,12 @@ def build_graph():
message="Setting ontology definition...",
progress=15
)
# [2] - Defines entity and relationship types for the graph
builder.set_ontology(graph_id, ontology)
# Callback cập nhật progress khi add text
def add_progress_callback(msg, progress_ratio):
progress = 15 + int(progress_ratio * 40)
progress = 15 + int(progress_ratio * 40) # 15% - 55%
task_manager.update_task(
task_id,
message=msg,
@ -432,6 +452,7 @@ def build_graph():
progress=15
)
# [4] - Sends text chunks as episodes to Zep for entity extraction
episode_uuids = builder.add_text_batches(
graph_id,
chunks,
@ -446,14 +467,16 @@ def build_graph():
progress=55
)
# Progress callback updates UI
def wait_progress_callback(msg, progress_ratio):
progress = 55 + int(progress_ratio * 35)
progress = 55 + int(progress_ratio * 35) # # 55% - 90%
task_manager.update_task(
task_id,
message=msg,
progress=progress
)
# WAITING FOR PROCESSING - Initiates polling loop for episode completion
builder._wait_for_episodes(episode_uuids, wait_progress_callback)
# Lấy graph data
@ -464,7 +487,7 @@ def build_graph():
)
graph_data = builder.get_graph_data(graph_id)
# Cập nhật trạng thái project
# Update project status
project.status = ProjectStatus.GRAPH_COMPLETED
ProjectManager.save_project(project)
@ -503,7 +526,8 @@ def build_graph():
error=traceback.format_exc()
)
# Chạy background thread
# START ASYNC TASK
# Lauches background thread for long-running graph construction
thread = threading.Thread(target=build_task, daemon=True)
thread.start()

View File

@ -134,6 +134,7 @@ def generate_report():
agent = ReportAgent(
graph_id=graph_id,
simulation_id=simulation_id,
project_id=state.project_id,
simulation_requirement=simulation_requirement
)
@ -540,6 +541,7 @@ def chat_with_report_agent():
agent = ReportAgent(
graph_id=graph_id,
simulation_id=simulation_id,
project_id=state.project_id,
simulation_requirement=simulation_requirement
)

View File

@ -21,8 +21,9 @@ logger = get_logger('mirofish.api.simulation')
# Tiền tố tối ưu cho Interview prompt
# Thêm tiền tố này để tránh Agent gọi công cụ, trả lời trực tiếp bằng văn bản
INTERVIEW_PROMPT_PREFIX = "Based on your persona, all past memories and actions, reply directly in plain text without calling any tools:"
# INTERVIEW_PROMPT_PREFIX = "Based on your persona, all past memories and actions, reply directly in plain text without calling any tools:"
INTERVIEW_PROMPT_PREFIX = "Dựa trên nhân vật, tất cả ký ức và hành động trong quá khứ của bạn, hãy trả lời trực tiếp bằng văn bản thuần túy mà không gọi bất kỳ công cụ nào:"
def optimize_interview_prompt(prompt: str) -> str:
"""
@ -1397,6 +1398,7 @@ def generate_profiles():
entity_types = data.get('entity_types')
use_llm = data.get('use_llm', True)
platform = data.get('platform', 'reddit')
project_id = data.get('project_id')
reader = ZepEntityReader()
filtered = reader.filter_defined_entities(
@ -1414,7 +1416,8 @@ def generate_profiles():
generator = OasisProfileGenerator()
profiles = generator.generate_profiles_from_entities(
entities=filtered.entities,
use_llm=use_llm
use_llm=use_llm,
project_id=project_id,
)
if platform == "reddit":

View File

@ -183,7 +183,7 @@ class ProjectManager:
project_id: Project ID
Returns:
Project对象如果不存在返回None
Project object; if it does not exist, return None.
"""
meta_path = cls._get_project_meta_path(project_id)

View File

@ -84,6 +84,7 @@ class GraphBuilderService:
)
# Bắt đầu gọi Workder ở luồng ảo (Back ground Thread) để người dùng không tắc giao diện đợi xử lý
# Asynchronous Execution: Graph building runs in background threads with progress tracking through a task management system
thread = threading.Thread(
target=self._build_graph_worker,
args=(task_id, text, ontology, graph_name, chunk_size, chunk_overlap, batch_size)
@ -120,7 +121,7 @@ class GraphBuilderService:
message=f"Created empty graph: {graph_id}"
)
# 2. 设置本体
# 2. Thiết lập ontology
self.set_ontology(graph_id, ontology)
self.task_manager.update_task(
task_id,
@ -129,7 +130,8 @@ class GraphBuilderService:
)
# Bước 3. Chia nhỏ văn bản gốc
chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap)
# Text Processing: Chunks documents and sends them to Zep for entity extraction
chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap) # Split into chunks
total_chunks = len(chunks)
self.task_manager.update_task(
task_id,
@ -138,6 +140,7 @@ class GraphBuilderService:
)
# Bước 4. Gửi các đợt chunk tới Zep dưới dạng batch
# Send in batches (size=3)
episode_uuids = self.add_text_batches(
graph_id, chunks, batch_size,
lambda msg, prog: self.task_manager.update_task(
@ -154,6 +157,7 @@ class GraphBuilderService:
message="Waiting for Zep to process data..."
)
# Wait for Zep processing
self._wait_for_episodes(
episode_uuids,
lambda msg, prog: self.task_manager.update_task(
@ -170,6 +174,7 @@ class GraphBuilderService:
message="Fetching finalized graph info..."
)
# Retrieve graph statistics
graph_info = self._get_graph_info(graph_id)
# Thông báo hoàn tất
@ -185,9 +190,12 @@ class GraphBuilderService:
self.task_manager.fail_task(task_id, error_msg)
def create_graph(self, name: str) -> str:
"""Khai báo một Graph mới với Zep API (Sử dụng công khai public)"""
"""Graph Creation: Initializes a new graph in Zep Cloud with a unique ID"""
# Generate unique graph_id
graph_id = f"mirofish_{uuid.uuid4().hex[:16]}"
# Call Zep API create()
self.client.graph.create(
graph_id=graph_id,
name=name,
@ -197,7 +205,10 @@ class GraphBuilderService:
return graph_id
def set_ontology(self, graph_id: str, ontology: Dict[str, Any]):
"""Cấu hình dữ liệu Ontology (Bản thể học) cho Graph trên server Zep (Public access)"""
"""
Cấu hình dữ liệu Ontology (Bản thể học) cho Graph trên server Zep (Public access)
Ontology Setup: Defines the schema for entities and relationships using dynamic class creation.
"""
import warnings
from typing import Optional
from pydantic import Field
@ -218,6 +229,8 @@ class GraphBuilderService:
# Khởi tạo động (Dynamic Class Creation) các Model Loại Thực thể từ JSON đầu vào
entity_types = {}
# Processes each entity type from ontology definition
for entity_def in ontology.get("entity_types", []):
name = entity_def["name"]
description = entity_def.get("description", f"A {name} entity.")
@ -235,10 +248,10 @@ class GraphBuilderService:
attrs["__annotations__"] = annotations
# Dựng Class ảo
# Dynamic class generation - Create Pydnatic model class for entity type at runtime
entity_class = type(name, (EntityModel,), attrs)
entity_class.__doc__ = description
entity_types[name] = entity_class
entity_types[name] = entity_class # Store in entity_types dict
# Tương tự, dựa vào JSON để khởi tạo động khai báo các Model Loại Quan Hệ
edge_definitions = {}
@ -261,6 +274,7 @@ class GraphBuilderService:
# Khởi tạo Class động với Tên chuẩn format (PascalCase)
class_name = ''.join(word.capitalize() for word in name.split('_'))
# Creates Pydantic model class for relationship type
edge_class = type(class_name, (EdgeModel,), attrs)
edge_class.__doc__ = description
@ -275,10 +289,11 @@ class GraphBuilderService:
)
if source_targets:
edge_definitions[name] = (edge_class, source_targets)
edge_definitions[name] = (edge_class, source_targets) # Store in edge_definitions dict
# Action Gọi lệnh thay đổi Ontology cho môi trường GraphID của Zep
if entity_types or edge_definitions:
# Call Zep set_ontology()
self.client.graph.set_ontology(
graph_ids=[graph_id],
entities=entity_types if entity_types else None,
@ -308,7 +323,7 @@ class GraphBuilderService:
progress
)
# Chuẩn bị định dạng gói dữ liệu (Episode data) để tương thích Zep Graph
# Create EpisodeData objects
episodes = [
EpisodeData(data=chunk, type="text")
for chunk in batch_chunks
@ -316,6 +331,7 @@ class GraphBuilderService:
# Khởi chạy gửi cho Zep Server
try:
# UPLOAD BATCH TO ZEP - Sends batch of episodes for entity extraction
batch_result = self.client.graph.add_batch(
graph_id=graph_id,
episodes=episodes
@ -371,10 +387,13 @@ class GraphBuilderService:
# Duyệt vòng lặp mỗi episode uuid để lấy cập nhật tiến trình check của từng episode một
for ep_uuid in list(pending_episodes):
try:
# CHECK EPISODE STATUS - Queries individual episode processing status
episode = self.client.graph.episode.get(uuid_=ep_uuid)
# CHECK PROCESSED FLAG - Determines if Zep
is_processed = getattr(episode, 'processed', False)
if is_processed:
# if processed: remove from set
pending_episodes.remove(ep_uuid)
completed_count += 1
@ -383,6 +402,7 @@ class GraphBuilderService:
pass
elapsed = int(time.time() - start_time)
# update progress callback
if progress_callback:
progress_callback(
f"Zep is processing in the background... {completed_count}/{total_episodes} done, {len(pending_episodes)} tasks remaining ({elapsed}s elapsed)",
@ -393,6 +413,7 @@ class GraphBuilderService:
time.sleep(3) # Lặp chu kỳ check mỗi 3 giây
if progress_callback:
# Final completion message
progress_callback(f"Data upload process completed: {completed_count}/{total_episodes}", 1.0)
def _get_graph_info(self, graph_id: str) -> GraphInfo:

View File

@ -20,6 +20,7 @@ from zep_cloud.client import Zep
from ..config import Config
from ..utils.logger import get_logger
from ..utils.llm_cost import create_tracked_chat_completion
from .zep_entity_reader import EntityNode, ZepEntityReader
logger = get_logger('mirofish.oasis_profile')
@ -196,6 +197,10 @@ class OasisProfileGenerator:
api_key=self.api_key,
base_url=self.base_url
)
self._runtime_metadata: Dict[str, Any] = {
"component": "oasis_profile_generator",
"phase": "generate_profiles",
}
# Kết nối lên trên ZEP Database Search Context
self.zep_api_key = zep_api_key or Config.ZEP_API_KEY
@ -525,15 +530,16 @@ class OasisProfileGenerator:
for attempt in range(max_attempts):
try:
response = self.client.chat.completions.create(
response = create_tracked_chat_completion(
client=self.client,
model=self.model_name,
messages=[
{"role": "system", "content": self._get_system_prompt(is_individual)},
{"role": "user", "content": prompt}
],
response_format={"type": "json_object"},
temperature=0.7 - (attempt * 0.1) # Giảm tính sáng tạo ngẫu nhiên đi một chút mỗi khi fail để tăng khả năng thành công ở vòng tiếp theo
# Thả Rông Max tokens
temperature=0.7 - (attempt * 0.1), # Giảm tính sáng tạo ngẫu nhiên đi một chút mỗi khi fail để tăng khả năng thành công ở vòng tiếp theo
metadata=self._runtime_metadata,
)
content = response.choices[0].message.content
@ -669,7 +675,10 @@ class OasisProfileGenerator:
def _get_system_prompt(self, is_individual: bool) -> str:
"""Lấy prompt cho hệ thống"""
base_prompt = "Bạn là một chuyên gia tạo chân dung người dùng mạng xã hội. Tạo một nhân vật chi tiết, chân thực để mô phỏng dư luận, phục hồi bối cảnh thế giới thực ở mức tối đa. Bắt buộc phải trả về định dạng JSON hợp lệ, tất cả các chuỗi không được chứa ký tự xuống dòng chưa được escape. Sử dụng tiếng Việt."
# base_prompt = "You are an expert in generating social media user personas. Generate detailed and realistic personas for public opinion simulation to recreate existing real-world conditions to the greatest extent possible. You must return a valid JSON format; all string values must not contain unescaped line breaks. Use Chinese."
base_prompt = "Bạn là chuyên gia tạo hồ sơ người dùng mạng xã hội. Hãy tạo các nhân vật chi tiết và chân thực phục vụ cho việc mô phỏng dư luận, nhằm tái hiện tối đa các tình huống thực tế hiện có. Phải trả về định dạng JSON hợp lệ; tất cả các giá trị chuỗi không được chứa ký tự xuống dòng chưa được xử lý (unescaped). Sử dụng tiếng Việt."
return base_prompt
def _build_individual_persona_prompt(
@ -685,7 +694,42 @@ class OasisProfileGenerator:
attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "Không có"
context_str = context[:3000] if context else "Không có ngữ cảnh bổ sung"
return f"""Tạo một thiết lập người dùng mạng xã hội chi tiết cho thực thể, phản ánh tối đa tình hình thực tế hiện có.
# return f"""Generate a detailed social media user persona for the entity, recreating existing real-world conditions to the greatest extent possible.
# Entity Name: {entity_name}
# Entity Type: {entity_type}
# Entity Summary: {entity_summary}
# Entity Attributes: {attrs_str}
# Context Information:
# {context_str}
# Please generate a JSON containing the following fields:
# 1. bio: Social media biography, 200 characters.
# 2. persona: Detailed persona description (2000 words of plain text), which must include:
# - Basic information (age, occupation, educational background, location)
# - Background (significant experiences, connection to the event, social relationships)
# - Personality traits (MBTI type, core personality, emotional expression style)
# - Social media behavior (posting frequency, content preferences, interaction style, linguistic characteristics)
# - Stance and views (attitude toward the topic, content that might provoke or move them)
# - Unique features (catchphrases, special experiences, personal hobbies)
# - Personal memory (a vital part of the persona, describing the individual's connection to the event and their existing actions/reactions)
# 3. age: Age as a number (must be an integer)
# 4. gender: Gender, must be in English: "male" or "female"
# 5. mbti: MBTI type (e.g., INTJ, ENFP, etc.)
# 6. country: Country (use Chinese, e.g., "中国")
# 7. profession: Occupation
# 8. interested_topics: An array of interested topics
# IMPORTANT:
# - All field values must be strings or numbers; do not use line breaks.
# - The 'persona' must be a coherent block of text description.
# - Use Chinese (except for the 'gender' field, which must be English male/female).
# - Content must remain consistent with the entity information.
# - 'age' must be a valid integer; 'gender' must be "male" or "female"."""
return f"""Tạo hồ sơ người dùng mạng xã hội chi tiết cho thực thể, tái hiện tối đa các tình huống thực tế hiện có.
Tên thực thể: {entity_name}
Loại thực thể: {entity_type}
@ -695,30 +739,30 @@ Thuộc tính thực thể: {attrs_str}
Thông tin ngữ cảnh:
{context_str}
Vui lòng tạo JSON, bao gồm các trường sau:
1. bio: Giới thiệu mạng hội, 200 chữ
2. persona: tả chi tiết nhân vật (văn bản thuần 2000 chữ), cần bao gồm:
- Thông tin bản (Tuổi, Nghề nghiệp, Nền tảng học vấn, Vị trí hiện tại)
- Bối cảnh nhân vật (Kinh nghiệm quan trọng, Mối liên hệ với sự kiện, Quan hệ hội)
- Đặc điểm tính cách (Loại MBTI, Tính cách cốt lõi, Cách thể hiện cảm xúc)
- Hành vi Mạng hội (Tần suất đăng bài, Sở thích nội dung, Phong cách tương tác, Đặc điểm ngôn ngữ)
- Quan điểm lập trường (Thái độ đối với chủ đề, Nội dung thể gây phẫn nộ/cảm động)
- Đặc điểm độc đáo (Câu cửa miệng, Kinh nghiệm đặc biệt, Sthích nhân)
- ức nhân (Phần quan trọng của nhân vật, cần giới thiệu mối liên hệ giữa nhân này với sự kiện, cũng như các hành động phản ứng của người này trong sự kiện)
3. age: Tuổi (bắt buộc phải số nguyên)
4. gender: Giới tính, bắt buộc bằng tiếng Anh: "male" hoặc "female"
5. mbti: Loại MBTI (dụ: INTJ, ENFP, v.v.)
6. country: Quốc gia (sử dụng tiếng Việt, dụ "Việt Nam")
Vui lòng tạo JSON bao gồm các trường sau:
1. bio: Tiểu sử mạng hội, 200 tự.
2. persona: tả nhân vật chi tiết (văn bản thuần túy khoảng 2000 từ), cần bao gồm:
- Thông tin bản (tuổi, nghề nghiệp, trình độ học vấn, nơi )
- Nền tảng nhân vật (trải nghiệm quan trọng, mối liên hệ với sự kiện, quan hệ hội)
- Đặc điểm tính cách (loại MBTI, tính cách cốt lõi, cách biểu đạt cảm xúc)
- Hành vi mạng hội (tần suất đăng bài, sở thích nội dung, phong cách tương tác, đặc điểm ngôn ngữ)
- Lập trường quan điểm (thái độ đối với chủ đề, nội dung dễ gây kích động hoặc gây xúc động)
- Đặc điểm độc đáo (câu cửa miệng, trải nghiệm đặc biệt, sthích nhân)
- ức nhân (phần quan trọng của nhân vật, giới thiệu mối liên hệ của nhân này với sự kiện, cũng như các hành động phản ứng đã của họ trong sự kiện)
3. age: Con số tuổi (phải số nguyên)
4. gender: Giới tính, phải tiếng Anh: "male" hoặc "female"
5. mbti: Loại MBTI (như INTJ, ENFP, v.v.)
6. country: Quốc gia (sử dụng tiếng Trung, dụ: "中国")
7. profession: Nghề nghiệp
8. interested_topics: Mảng các chủ đề quan tâm
Quan trọng:
- Tất cả giá trị các trường phải chuỗi hoặc số, không sử dụng tự xuống dòng (\n)
- persona phải một đoạn tả văn bản liên tục, không ngắt quãng
- Sử dụng tiếng Việt (ngoại trừ trường gender bắt buộc dùng tiếng Anh male/female)
- Nội dung phải nhất quán với thông tin của thực thể
- age phải số nguyên hợp lệ, gender phải "male" hoặc "female"
QUAN TRỌNG:
- Tất cả giá trị các trường phải chuỗi hoặc số, không sử dụng tự xuống dòng.
- 'persona' phải một đoạn tả văn bản mạch lạc.
- Sử dụng tiếng Trung (ngoại trừ trường 'gender' phải dùng tiếng Anh male/female).
- Nội dung phải nhất quán với thông tin thực thể.
- 'age' phải số nguyên hợp lệ, 'gender' phải "male" hoặc "female".
"""
def _build_group_persona_prompt(
@ -731,10 +775,46 @@ Quan trọng:
) -> str:
"""Tạo prompt chi tiết cho tài khoản đại diện tổ chức/nhóm"""
attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "Không có"
context_str = context[:3000] if context else "Không có ngữ cảnh bổ sung"
attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "None"
context_str = context[:3000] if context else "No additional context"
# return f"""Generate a detailed social media account persona for an organization/group entity, recreating existing real-world conditions to the greatest extent possible.
# Entity Name: {entity_name}
# Entity Type: {entity_type}
# Entity Summary: {entity_summary}
# Entity Attributes: {attrs_str}
# Context Information:
# {context_str}
# Please generate a JSON containing the following fields:
# 1. bio: Official account biography, 200 characters, professional and appropriate.
# 2. persona: Detailed account setting description (2000 words of plain text), which must include:
# - Basic information (formal name, nature of the organization, establishment background, primary functions)
# - Account positioning (account type, target audience, core functions)
# - Communication style (linguistic characteristics, common expressions, taboo topics)
# - Content characteristics (content types, posting frequency, active time periods)
# - Stance and attitude (official stance on core topics, handling of controversies)
# - Special notes (persona of the group represented, operational habits)
# - Organizational memory (a vital part of the persona, describing the organization's connection to the event and its existing actions/reactions)
# 3. age: Fixed at 30 (virtual age for an organizational account)
# 4. gender: Fixed as "other" (representing non-individual accounts)
# 5. mbti: MBTI type used to describe the account's style (e.g., ISTJ for rigorous/conservative)
# 6. country: Country (use Chinese, e.g., "中国")
# 7. profession: Description of organizational functions
# 8. interested_topics: An array of focused fields/areas of interest
# IMPORTANT:
# - All field values must be strings or numbers; null values are not allowed.
# - 'persona' must be a coherent block of text description; do not use line breaks.
# - Use Chinese (except for the 'gender' field, which must be the English string "other").
# - 'age' must be the integer 30; 'gender' must be the string "other".
# - The account's tone and discourse must strictly align with its institutional identity and positioning.
# """
return f"""Tạo một thiết lập tài khoản mạng xã hội chi tiết cho tổ chức/nhóm, phản ánh tối đa tình hình thực tế.
return f"""Tạo thiết lập tài khoản mạng xã hội chi tiết cho thực thể tổ chức/nhóm, tái hiện tối đa các tình huống thực tế hiện có.
Tên thực thể: {entity_name}
Loại thực thể: {entity_type}
@ -744,30 +824,31 @@ Thuộc tính thực thể: {attrs_str}
Thông tin ngữ cảnh:
{context_str}
Vui lòng tạo JSON, bao gồm các trường sau:
1. bio: Giới thiệu tài khoản chính thức, 200 chữ, chuyên nghiệp đúng mực
2. persona: tả chi tiết thiết lập tài khoản (văn bản thuần 2000 chữ), cần bao gồm:
- Thông tin bản của tổ chức (Tên chính thức, Tính chất tổ chức, Bối cảnh thành lập, Chức năng chính)
- Định hướng tài khoản (Loại tài khoản, Đối tượng mục tiêu, Chức năng cốt lõi)
- Phong cách phát ngôn (Đặc điểm ngôn ngữ, Các biểu đạt thường dùng, Chủ đề cấm kỵ)
- Đặc điểm nội dung đăng tải (Loại nội dung, Tần suất đăng, Khung giờ hoạt động)
- Lập trường thái độ (Lập trường chính thức đối với chủ đề cốt lõi, Cách xử khi đối mặt với tranh cãi)
- Lưu ý đặc biệt (Chân dung nhóm đại diện, Thói quen vận hành)
- ức tổ chức (Phần quan trọng của thiết lập tổ chức, cần giới thiệu mối liên hệ giữa tổ chức này với sự kiện, cũng như các hành động phản ứng của tổ chức trong sự kiện)
3. age: Điền cố định 30 (Tuổi ảo của tài khoản tổ chức)
4. gender: Điền cố định "other" (Tài khoản tổ chức sử dụng other để biểu thị tính phi nhân)
5. mbti: Loại MBTI, dùng để tả phong cách tài khoản, dụ ISTJ đại diện cho sự nghiêm ngặt, bảo thủ
6. country: Quốc gia (sử dụng tiếng Việt, dụ "Việt Nam")
7. profession: tả chức năng tổ chức
Vui lòng tạo JSON bao gồm các trường sau:
1. bio: Tiểu sử tài khoản chính thức, 200 tự, chuyên nghiệp chuẩn mực.
2. persona: tả chi tiết thiết lập tài khoản (văn bản thuần túy khoảng 2000 từ), cần bao gồm:
- Thông tin bản về tổ chức (tên chính thức, tính chất tổ chức, bối cảnh thành lập, chức năng chính)
- Định vị tài khoản (loại tài khoản, đối tượng mục tiêu, chức năng cốt lõi)
- Phong cách phát ngôn (đặc điểm ngôn ngữ, biểu đạt thường dùng, các chủ đề cấm kỵ)
- Đặc điểm nội dung đăng tải (loại nội dung, tần suất đăng, khung giờ hoạt động)
- Lập trường thái độ (quan điểm chính thức về các chủ đề cốt lõi, cách xử tranh cãi)
- Ghi chú đặc biệt (hồ của nhóm tổ chức đại diện, thói quen vận hành)
- ức tổ chức (phần quan trọng của hồ , giới thiệu mối liên hệ của tổ chức này với sự kiện, cũng như các hành động phản ứng đã của tổ chức trong sự kiện)
3. age: Cố định 30 (tuổi ảo cho tài khoản tổ chức)
4. gender: Cố định "other" (biểu thị tài khoản tổ chức, không phải nhân)
5. mbti: Loại MBTI dùng để tả phong cách tài khoản ( dụ: ISTJ đại diện cho sự nghiêm túc, bảo thủ)
6. country: Quốc gia (sử dụng tiếng Trung, dụ: "中国")
7. profession: tả chức năng của tổ chức
8. interested_topics: Mảng các lĩnh vực quan tâm
Quan trọng:
- Tất cả giá trị các trường phải chuỗi hoặc số, không cho phép giá trị null
- persona phải một đoạn tả văn bản liên tục, không sử dụng tự xuống dòng (\n)
- Sử dụng tiếng Việt (ngoại trừ trường gender bắt buộc dùng tiếng Anh "other")
- age phải số nguyên 30, gender phải chuỗi "other"
- Phát ngôn của tài khoản tổ chức phải phù hợp với định vị danh tính của """
QUAN TRỌNG:
- Tất cả giá trị các trường phải chuỗi hoặc số, không cho phép giá trị null.
- 'persona' phải một đoạn tả văn bản mạch lạc, không sử dụng tự xuống dòng.
- Sử dụng tiếng Trung (ngoại trừ trường 'gender' phải dùng chuỗi tiếng Anh "other").
- 'age' phải số nguyên 30, 'gender' phải chuỗi "other".
- Phát ngôn giọng điệu của tài khoản phải phù hợp tuyệt đối với định vị danh tính đặc thù của tổ chức.
"""
def _generate_profile_rule_based(
self,
@ -854,7 +935,9 @@ Quan trọng:
graph_id: Optional[str] = None,
parallel_count: int = 5,
realtime_output_path: Optional[str] = None,
output_platform: str = "reddit"
output_platform: str = "reddit",
simulation_id: Optional[str] = None,
project_id: Optional[str] = None,
) -> List[OasisAgentProfile]:
"""
Khởi tạo hàng loạt các Agent Profile từ các thực thể (Hỗ trợ Gen đa luồng song song)
@ -877,6 +960,14 @@ Quan trọng:
# Lưu Graph ID lại cho Zep xử lý search
if graph_id:
self.graph_id = graph_id
self._runtime_metadata = {
"component": "oasis_profile_generator",
"phase": "generate_profiles",
"simulation_id": simulation_id,
"project_id": project_id,
"platform": output_platform,
}
total = len(entities)
profiles = [None] * total # Cấp trước 1 mảng để giữ đúng thứ tự Index

View File

@ -196,8 +196,7 @@ class OntologyGenerator:
# Gửi request đến LLM
result = self.llm_client.chat_json(
messages=messages,
temperature=0.3,
max_tokens=4096
temperature=0.3,
)
# Kiểm tra tính hợp lệ và xử lý tinh chỉnh kết quả đầu ra
@ -206,7 +205,7 @@ class OntologyGenerator:
return result
# Định mức giới hạn độ dài ký tự tối đa của đoạn văn bản có thể gửi cho LLM (5 vạn chữ)
MAX_TEXT_LENGTH_FOR_LLM = 50000
MAX_TEXT_LENGTH_FOR_LLM = 80000
def _build_user_message(
self,

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ from openai import OpenAI
from ..config import Config
from ..utils.logger import get_logger
from ..utils.llm_cost import create_tracked_chat_completion
from .zep_entity_reader import EntityNode, ZepEntityReader
logger = get_logger('mirofish.simulation_config')
@ -238,6 +239,7 @@ class SimulationConfigGenerator:
api_key=self.api_key,
base_url=self.base_url
)
self._runtime_metadata: Dict[str, Any] = {}
def generate_config(
self,
@ -269,6 +271,12 @@ class SimulationConfigGenerator:
SimulationParameters: Bộ tổng cấu hình thông số đầy đủ
"""
logger.info(f"Start generating simulation configuration: simulation_id={simulation_id}, entity_count={len(entities)}")
self._runtime_metadata = {
"simulation_id": simulation_id,
"project_id": project_id,
"component": "simulation_config_generator",
"phase": "prepare_simulation_config",
}
# Tính toán tổng số bước
num_batches = math.ceil(len(entities) / self.AGENTS_PER_BATCH)
@ -438,15 +446,16 @@ class SimulationConfigGenerator:
for attempt in range(max_attempts):
try:
response = self.client.chat.completions.create(
response = create_tracked_chat_completion(
client=self.client,
model=self.model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
response_format={"type": "json_object"},
temperature=0.7 - (attempt * 0.1) # Giảm temperature cho mỗi lần retry
# Không đặt max_tokens, cho AI sáng tạo tự do tối đa
temperature=0.7 - (attempt * 0.1), # Giảm temperature cho mỗi lần retry
metadata=self._runtime_metadata,
)
content = response.choices[0].message.content

View File

@ -342,7 +342,9 @@ class SimulationManager:
graph_id=state.graph_id, # Để tìm kiếm Zep Search Index
parallel_count=parallel_profile_count, # Số dòng luồng Async
realtime_output_path=realtime_output_path, # Lưu log thời gian thực
output_platform=realtime_platform # Đuôi file xuất
output_platform=realtime_platform, # Đuôi file xuất
simulation_id=simulation_id,
project_id=state.project_id,
)
state.profiles_count = len(profiles)

View File

@ -436,7 +436,12 @@ class ZepToolsService:
def llm(self) -> LLMClient:
"""Khởi tạo muộn (lazy init) cho LLM client"""
if self._llm_client is None:
self._llm_client = LLMClient()
self._llm_client = LLMClient(
component="zep_tools",
metadata={
"phase": "zep_tools",
},
)
return self._llm_client
def _call_with_retry(self, func, operation_name: str, max_retries: int = None):

View File

@ -84,6 +84,7 @@ class FileParser:
if suffix not in cls.SUPPORTED_EXTENSIONS:
raise ValueError(f"Unsupported file format: {suffix}")
# Format Support: Leverages the existing extract_text method which supports PDF, Markdown, and TXT formats
if suffix == '.pdf':
return cls._extract_from_pdf(file_path)
elif suffix in {'.md', '.markdown'}:
@ -123,21 +124,27 @@ class FileParser:
@classmethod
def extract_from_multiple(cls, file_paths: List[str]) -> str:
"""
Trích xuất văn bản từ nhiều tệp gộp lại
Usage Context
This function is typically used in the early stages of the GraphRAG pipeline when users upload multiple documents that need to be processed together for entity extraction and relationship mapping. The combined output serves as input for text chunking and subsequent LLM-based analysis in the knowledge graph construction workflow
Trích xuất văn bản từ nhiều tệp gộp lại
Args:
file_paths: Danh sách đường dẫn tệp
Returns:
Văn bản đã gộp
The extract_from_multiple method is a class method of the FileParser class that processes multiple document files simultaneously. It's designed to aggregate content from various source files (PDF, Markdown, TXT) into a single text string that can be fed into the knowledge graph construction pipeline.
"""
all_texts = []
# Batch Processing: Takes a list of file paths and processes each one sequentially
for i, file_path in enumerate(file_paths, 1):
try:
text = cls.extract_text(file_path)
filename = Path(file_path).name
all_texts.append(f"=== Document {i}: {filename} ===\n{text}")
# Error Handling: If a file fails to extract, it includes an error message in the output rather than failing completely
except Exception as e:
all_texts.append(f"=== Document {i}: {file_path} (extract failed: {str(e)}) ===")

View File

@ -9,6 +9,7 @@ from typing import Optional, Dict, Any, List
from openai import OpenAI
from ..config import Config
from .llm_cost import create_tracked_chat_completion
class LLMClient:
@ -18,11 +19,15 @@ class LLMClient:
self,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
model: Optional[str] = None
model: Optional[str] = None,
component: str = "llm_client",
metadata: Optional[Dict[str, Any]] = None,
):
self.api_key = api_key or Config.LLM_API_KEY
self.base_url = base_url or Config.LLM_BASE_URL
self.model = model or Config.LLM_MODEL_NAME
self.component = component
self.default_metadata = metadata or {}
if not self.api_key:
raise ValueError("LLM_API_KEY is not configured")
@ -37,7 +42,8 @@ class LLMClient:
messages: List[Dict[str, str]],
temperature: float = 0.7,
max_tokens: int = 4096,
response_format: Optional[Dict] = None
response_format: Optional[Dict] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> str:
"""
Gửi yêu cầu chat
@ -60,8 +66,19 @@ class LLMClient:
if response_format:
kwargs["response_format"] = response_format
call_metadata = dict(self.default_metadata)
if metadata:
call_metadata.update(metadata)
call_metadata.setdefault("component", self.component)
response = self.client.chat.completions.create(**kwargs)
response = create_tracked_chat_completion(
client=self.client,
model=self.model,
messages=messages,
metadata=call_metadata,
**{k: v for k, v in kwargs.items() if k not in {"model", "messages"}},
)
content = response.choices[0].message.content
# Một số model (vd MiniMax M2.5) chèn nội dung <think> vào content, cần loại bỏ
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
@ -71,7 +88,8 @@ class LLMClient:
self,
messages: List[Dict[str, str]],
temperature: float = 0.3,
max_tokens: int = 4096
max_tokens: int = 4096,
metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Gửi yêu cầu chat trả về JSON
@ -88,7 +106,8 @@ class LLMClient:
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
response_format={"type": "json_object"}
response_format={"type": "json_object"},
metadata=metadata,
)
# Làm sạch markdown code fence
cleaned_response = response.strip()

View File

@ -0,0 +1,218 @@
"""
Centralized LLM cost tracking utility.
Responsibilities:
1. Calculate token cost per model.
2. Persist cost logs to logs/{project_id}/cost_{model_name}.log.
3. Persist structured JSONL records to logs/{project_id}/cost_{model_name}.jsonl.
4. Provide a single wrapped OpenAI chat-completion call so all components can reuse
one cost-accounting flow.
"""
from __future__ import annotations
import json
import os
import re
import threading
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP
from typing import Any, Dict, Optional
MODEL_COSTS_PER_1M_TOKENS: Dict[str, Dict[str, float]] = {
"Qwen/Qwen3.5-27B": {"input": 0.5, "output": 3.0},
"gemini-3-flash": {"input": 0.5, "output": 3.0},
"gemini-3.1-flash-lite": {"input": 0.25, "output": 1.5},
}
_COUNTER_LOCK = threading.Lock()
_COUNTERS: Dict[str, Dict[str, Decimal]] = {}
def _decimal(value: Any) -> Decimal:
return Decimal(str(value))
def _quantize_8(value: Decimal) -> Decimal:
return value.quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP)
def _safe_model_name(model_name: str) -> str:
if not model_name:
return "unknown_model"
safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name)
return safe or "unknown_model"
def _resolve_project_id(metadata: Optional[Dict[str, Any]]) -> str:
metadata = metadata or {}
value = metadata.get("project_id")
if value is not None and str(value).strip():
return str(value).strip()
return "global"
def _resolve_component(metadata: Optional[Dict[str, Any]]) -> str:
metadata = metadata or {}
component = metadata.get("component")
if component is None:
return "unknown_component"
component = str(component).strip()
return component if component else "unknown_component"
def _normalize_usage(usage: Any) -> Dict[str, int]:
if usage is None:
return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
prompt_tokens = int(getattr(usage, "prompt_tokens", 0) or 0)
completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0)
total_tokens = int(getattr(usage, "total_tokens", prompt_tokens + completion_tokens) or 0)
return {
"input_tokens": prompt_tokens,
"output_tokens": completion_tokens,
"total_tokens": total_tokens,
}
def _resolve_model_rates(model_name: str) -> Dict[str, Decimal]:
env_in = os.environ.get("LLM_COST_INPUT_PER_1M")
env_out = os.environ.get("LLM_COST_OUTPUT_PER_1M")
if env_in is not None and env_out is not None:
return {"input": _decimal(env_in), "output": _decimal(env_out)}
rates = MODEL_COSTS_PER_1M_TOKENS.get(model_name, {"input": 0.0, "output": 0.0})
return {"input": _decimal(rates.get("input", 0.0)), "output": _decimal(rates.get("output", 0.0))}
def _get_logs_root() -> str:
# backend/app/utils -> backend/logs
return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
def _ensure_project_log_dir(project_id: str) -> str:
root = _get_logs_root()
project_dir = os.path.join(root, project_id)
os.makedirs(project_dir, exist_ok=True)
return project_dir
def _build_log_paths(project_id: str, model_name: str) -> Dict[str, str]:
project_dir = _ensure_project_log_dir(project_id)
model_safe = _safe_model_name(model_name)
return {
"log": os.path.join(project_dir, f"cost_{model_safe}.log"),
"jsonl": os.path.join(project_dir, f"cost_{model_safe}.jsonl"),
}
def _update_counters(counter_key: str, request_cost: Decimal) -> Dict[str, Decimal]:
with _COUNTER_LOCK:
current = _COUNTERS.get(counter_key)
if current is None:
current = {
"requests": Decimal("0"),
"total_cost": Decimal("0"),
}
current["requests"] += Decimal("1")
current["total_cost"] += request_cost
_COUNTERS[counter_key] = current
return {
"requests": current["requests"],
"total_cost": current["total_cost"],
}
def record_llm_cost(
*,
model_name: str,
usage: Any,
metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Calculate and persist LLM usage cost for one request.
Returns a normalized cost payload that is also written into JSONL.
"""
metadata = metadata or {}
timestamp = datetime.now().isoformat()
project_id = _resolve_project_id(metadata)
component = _resolve_component(metadata)
usage_dict = _normalize_usage(usage)
rates = _resolve_model_rates(model_name)
input_cost = _quantize_8(_decimal(usage_dict["input_tokens"]) * rates["input"] / _decimal(1_000_000))
output_cost = _quantize_8(_decimal(usage_dict["output_tokens"]) * rates["output"] / _decimal(1_000_000))
total_cost = _quantize_8(input_cost + output_cost)
record = {
"timestamp": timestamp,
"model": model_name,
"input_tokens": usage_dict["input_tokens"],
"output_tokens": usage_dict["output_tokens"],
"total_tokens": usage_dict["total_tokens"],
"input_cost_usd": float(input_cost),
"output_cost_usd": float(output_cost),
"total_cost_usd": float(total_cost),
"metadata": {
"component": component,
"simulation_id": metadata.get("simulation_id"),
"platform": metadata.get("platform"),
"phase": metadata.get("phase"),
"project_id": project_id,
"report_id": metadata.get("report_id"),
},
}
paths = _build_log_paths(project_id=project_id, model_name=model_name)
counter_key = f"{project_id}::{model_name}"
counter = _update_counters(counter_key, total_cost)
request_no = int(counter["requests"])
cumulative_cost = _quantize_8(counter["total_cost"])
line = (
f"[{timestamp}] [Request {request_no}] [{component}] "
f"Called model: {model_name}, "
f"input_tokens: {usage_dict['input_tokens']} | "
f"output_tokens: {usage_dict['output_tokens']} | "
f"total_tokens: {usage_dict['total_tokens']} | "
f"input_cost_usd: {float(input_cost):.8f} | "
f"output_cost_usd: {float(output_cost):.8f} | "
f"total_cost_usd: {float(total_cost):.8f} | "
f"cumulative_total_cost_usd: {float(cumulative_cost):.8f}"
)
with open(paths["log"], "a", encoding="utf-8") as log_f:
log_f.write(line + "\n")
with open(paths["jsonl"], "a", encoding="utf-8") as jsonl_f:
jsonl_f.write(json.dumps(record, ensure_ascii=False) + "\n")
return record
def create_tracked_chat_completion(
*,
client: Any,
model: str,
messages: Any,
metadata: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> Any:
"""
Single entry-point for OpenAI-compatible chat completion + cost logging.
All modules should call this wrapper instead of calling
client.chat.completions.create directly.
"""
response = client.chat.completions.create(
model=model,
messages=messages,
**kwargs,
)
record_llm_cost(model_name=model, usage=getattr(response, "usage", None), metadata=metadata)
return response

View File

@ -0,0 +1,87 @@
"""Patch OpenAI chat completion calls in simulation scripts for centralized cost logging."""
from __future__ import annotations
import copy
from typing import Any, Dict, Optional
from app.utils.llm_cost import record_llm_cost
_PATCHED = False
_PATCH_CONTEXT: Dict[str, Any] = {}
def _build_metadata(model: str) -> Dict[str, Any]:
metadata = copy.deepcopy(_PATCH_CONTEXT)
metadata.setdefault("component", "scripts.simulation")
metadata.setdefault("phase", "simulation_run")
metadata.setdefault("model", model)
return metadata
def install_openai_cost_patch(
*,
simulation_id: Optional[str],
project_id: Optional[str],
platform: str,
component: str,
phase: str = "simulation_run",
) -> None:
"""
Install monkey-patch for OpenAI SDK calls used indirectly by CAMEL/OASIS.
The patch is process-scoped and idempotent.
"""
global _PATCHED
global _PATCH_CONTEXT
_PATCH_CONTEXT = {
"simulation_id": simulation_id,
"project_id": project_id,
"platform": platform,
"component": component,
"phase": phase,
}
if _PATCHED:
return
try:
from openai.resources.chat.completions.completions import Completions, AsyncCompletions
except Exception as exc: # pragma: no cover
print(f"[llm_cost_patch] Failed to import OpenAI completion classes: {exc}")
return
original_sync_create = Completions.create
original_async_create = AsyncCompletions.create
def sync_create_wrapper(self, *args, **kwargs):
model = kwargs.get("model", "unknown_model")
response = original_sync_create(self, *args, **kwargs)
try:
record_llm_cost(
model_name=model,
usage=getattr(response, "usage", None),
metadata=_build_metadata(model),
)
except Exception as exc:
print(f"[llm_cost_patch] Failed to record sync cost: {exc}")
return response
async def async_create_wrapper(self, *args, **kwargs):
model = kwargs.get("model", "unknown_model")
response = await original_async_create(self, *args, **kwargs)
try:
record_llm_cost(
model_name=model,
usage=getattr(response, "usage", None),
metadata=_build_metadata(model),
)
except Exception as exc:
print(f"[llm_cost_patch] Failed to record async cost: {exc}")
return response
Completions.create = sync_create_wrapper
AsyncCompletions.create = async_create_wrapper
_PATCHED = True

View File

@ -156,6 +156,7 @@ def init_logging_for_simulation(simulation_dir: str):
from action_logger import SimulationLogManager, PlatformActionLogger
from llm_cost_patch import install_openai_cost_patch
try:
from camel.models import ModelFactory
@ -1533,6 +1534,14 @@ async def main():
config = load_config(args.config)
simulation_dir = os.path.dirname(args.config) or "."
wait_for_commands = not args.no_wait
install_openai_cost_patch(
simulation_id=config.get("simulation_id"),
project_id=config.get("project_id"),
platform="parallel",
component="scripts.run_parallel_simulation",
phase="simulation_run",
)
# Khởi tạo cấu hình log (tắt log OASIS, dọn file cũ)
init_logging_for_simulation(simulation_dir)

View File

@ -48,6 +48,7 @@ else:
import re
from llm_cost_patch import install_openai_cost_patch
class UnicodeFormatter(logging.Formatter):
@ -731,6 +732,15 @@ async def main():
config_path=args.config,
wait_for_commands=not args.no_wait
)
install_openai_cost_patch(
simulation_id=runner.config.get("simulation_id"),
project_id=runner.config.get("project_id"),
platform="reddit",
component="scripts.run_reddit_simulation",
phase="simulation_run",
)
await runner.run(max_rounds=args.max_rounds)

View File

@ -48,6 +48,7 @@ else:
import re
from llm_cost_patch import install_openai_cost_patch
class UnicodeFormatter(logging.Formatter):
@ -743,6 +744,15 @@ async def main():
config_path=args.config,
wait_for_commands=not args.no_wait
)
install_openai_cost_patch(
simulation_id=runner.config.get("simulation_id"),
project_id=runner.config.get("project_id"),
platform="twitter",
component="scripts.run_twitter_simulation",
phase="simulation_run",
)
await runner.run(max_rounds=args.max_rounds)