Merge pull request #3 from TQuynh109/feature/modified-prompts
Feature/modified prompts
This commit is contained in:
commit
052042c26d
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from zep_cloud.client import Zep
|
|||
|
||||
from ..config import Config
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.llm_cost import create_tracked_chat_completion
|
||||
from .zep_entity_reader import EntityNode, ZepEntityReader
|
||||
|
||||
logger = get_logger('mirofish.oasis_profile')
|
||||
|
|
@ -196,6 +197,10 @@ class OasisProfileGenerator:
|
|||
api_key=self.api_key,
|
||||
base_url=self.base_url
|
||||
)
|
||||
self._runtime_metadata: Dict[str, Any] = {
|
||||
"component": "oasis_profile_generator",
|
||||
"phase": "generate_profiles",
|
||||
}
|
||||
|
||||
# Kết nối lên trên ZEP Database Search Context
|
||||
self.zep_api_key = zep_api_key or Config.ZEP_API_KEY
|
||||
|
|
@ -525,15 +530,16 @@ class OasisProfileGenerator:
|
|||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
response = create_tracked_chat_completion(
|
||||
client=self.client,
|
||||
model=self.model_name,
|
||||
messages=[
|
||||
{"role": "system", "content": self._get_system_prompt(is_individual)},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
response_format={"type": "json_object"},
|
||||
temperature=0.7 - (attempt * 0.1) # Giảm tính sáng tạo ngẫu nhiên đi một chút mỗi khi fail để tăng khả năng thành công ở vòng tiếp theo
|
||||
# Thả Rông Max tokens
|
||||
temperature=0.7 - (attempt * 0.1), # Giảm tính sáng tạo ngẫu nhiên đi một chút mỗi khi fail để tăng khả năng thành công ở vòng tiếp theo
|
||||
metadata=self._runtime_metadata,
|
||||
)
|
||||
|
||||
content = response.choices[0].message.content
|
||||
|
|
@ -669,7 +675,10 @@ class OasisProfileGenerator:
|
|||
|
||||
def _get_system_prompt(self, is_individual: bool) -> str:
|
||||
"""Lấy prompt cho hệ thống"""
|
||||
base_prompt = "Bạn là một chuyên gia tạo chân dung người dùng mạng xã hội. Tạo một nhân vật chi tiết, chân thực để mô phỏng dư luận, phục hồi bối cảnh thế giới thực ở mức tối đa. Bắt buộc phải trả về định dạng JSON hợp lệ, tất cả các chuỗi không được chứa ký tự xuống dòng chưa được escape. Sử dụng tiếng Việt."
|
||||
|
||||
# base_prompt = "You are an expert in generating social media user personas. Generate detailed and realistic personas for public opinion simulation to recreate existing real-world conditions to the greatest extent possible. You must return a valid JSON format; all string values must not contain unescaped line breaks. Use Chinese."
|
||||
|
||||
base_prompt = "Bạn là chuyên gia tạo hồ sơ người dùng mạng xã hội. Hãy tạo các nhân vật chi tiết và chân thực phục vụ cho việc mô phỏng dư luận, nhằm tái hiện tối đa các tình huống thực tế hiện có. Phải trả về định dạng JSON hợp lệ; tất cả các giá trị chuỗi không được chứa ký tự xuống dòng chưa được xử lý (unescaped). Sử dụng tiếng Việt."
|
||||
return base_prompt
|
||||
|
||||
def _build_individual_persona_prompt(
|
||||
|
|
@ -685,7 +694,42 @@ class OasisProfileGenerator:
|
|||
attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "Không có"
|
||||
context_str = context[:3000] if context else "Không có ngữ cảnh bổ sung"
|
||||
|
||||
return f"""Tạo một thiết lập người dùng mạng xã hội chi tiết cho thực thể, phản ánh tối đa tình hình thực tế hiện có.
|
||||
# return f"""Generate a detailed social media user persona for the entity, recreating existing real-world conditions to the greatest extent possible.
|
||||
|
||||
# Entity Name: {entity_name}
|
||||
# Entity Type: {entity_type}
|
||||
# Entity Summary: {entity_summary}
|
||||
# Entity Attributes: {attrs_str}
|
||||
|
||||
# Context Information:
|
||||
# {context_str}
|
||||
|
||||
# Please generate a JSON containing the following fields:
|
||||
|
||||
# 1. bio: Social media biography, 200 characters.
|
||||
# 2. persona: Detailed persona description (2000 words of plain text), which must include:
|
||||
# - Basic information (age, occupation, educational background, location)
|
||||
# - Background (significant experiences, connection to the event, social relationships)
|
||||
# - Personality traits (MBTI type, core personality, emotional expression style)
|
||||
# - Social media behavior (posting frequency, content preferences, interaction style, linguistic characteristics)
|
||||
# - Stance and views (attitude toward the topic, content that might provoke or move them)
|
||||
# - Unique features (catchphrases, special experiences, personal hobbies)
|
||||
# - Personal memory (a vital part of the persona, describing the individual's connection to the event and their existing actions/reactions)
|
||||
# 3. age: Age as a number (must be an integer)
|
||||
# 4. gender: Gender, must be in English: "male" or "female"
|
||||
# 5. mbti: MBTI type (e.g., INTJ, ENFP, etc.)
|
||||
# 6. country: Country (use Chinese, e.g., "中国")
|
||||
# 7. profession: Occupation
|
||||
# 8. interested_topics: An array of interested topics
|
||||
|
||||
# IMPORTANT:
|
||||
# - All field values must be strings or numbers; do not use line breaks.
|
||||
# - The 'persona' must be a coherent block of text description.
|
||||
# - Use Chinese (except for the 'gender' field, which must be English male/female).
|
||||
# - Content must remain consistent with the entity information.
|
||||
# - 'age' must be a valid integer; 'gender' must be "male" or "female"."""
|
||||
|
||||
return f"""Tạo hồ sơ người dùng mạng xã hội chi tiết cho thực thể, tái hiện tối đa các tình huống thực tế hiện có.
|
||||
|
||||
Tên thực thể: {entity_name}
|
||||
Loại thực thể: {entity_type}
|
||||
|
|
@ -695,30 +739,30 @@ Thuộc tính thực thể: {attrs_str}
|
|||
Thông tin ngữ cảnh:
|
||||
{context_str}
|
||||
|
||||
Vui lòng tạo JSON, bao gồm các trường sau:
|
||||
|
||||
1. bio: Giới thiệu mạng xã hội, 200 chữ
|
||||
2. persona: Mô tả chi tiết nhân vật (văn bản thuần 2000 chữ), cần bao gồm:
|
||||
- Thông tin cơ bản (Tuổi, Nghề nghiệp, Nền tảng học vấn, Vị trí hiện tại)
|
||||
- Bối cảnh nhân vật (Kinh nghiệm quan trọng, Mối liên hệ với sự kiện, Quan hệ xã hội)
|
||||
- Đặc điểm tính cách (Loại MBTI, Tính cách cốt lõi, Cách thể hiện cảm xúc)
|
||||
- Hành vi Mạng xã hội (Tần suất đăng bài, Sở thích nội dung, Phong cách tương tác, Đặc điểm ngôn ngữ)
|
||||
- Quan điểm lập trường (Thái độ đối với chủ đề, Nội dung có thể gây phẫn nộ/cảm động)
|
||||
- Đặc điểm độc đáo (Câu cửa miệng, Kinh nghiệm đặc biệt, Sở thích cá nhân)
|
||||
- Ký ức cá nhân (Phần quan trọng của nhân vật, cần giới thiệu rõ mối liên hệ giữa cá nhân này với sự kiện, cũng như các hành động và phản ứng của người này trong sự kiện)
|
||||
3. age: Tuổi (bắt buộc phải là số nguyên)
|
||||
4. gender: Giới tính, bắt buộc bằng tiếng Anh: "male" hoặc "female"
|
||||
5. mbti: Loại MBTI (ví dụ: INTJ, ENFP, v.v.)
|
||||
6. country: Quốc gia (sử dụng tiếng Việt, ví dụ "Việt Nam")
|
||||
Vui lòng tạo JSON bao gồm các trường sau:
|
||||
|
||||
1. bio: Tiểu sử mạng xã hội, 200 ký tự.
|
||||
2. persona: Mô tả nhân vật chi tiết (văn bản thuần túy khoảng 2000 từ), cần bao gồm:
|
||||
- Thông tin cơ bản (tuổi, nghề nghiệp, trình độ học vấn, nơi ở)
|
||||
- Nền tảng nhân vật (trải nghiệm quan trọng, mối liên hệ với sự kiện, quan hệ xã hội)
|
||||
- Đặc điểm tính cách (loại MBTI, tính cách cốt lõi, cách biểu đạt cảm xúc)
|
||||
- Hành vi mạng xã hội (tần suất đăng bài, sở thích nội dung, phong cách tương tác, đặc điểm ngôn ngữ)
|
||||
- Lập trường quan điểm (thái độ đối với chủ đề, nội dung dễ gây kích động hoặc gây xúc động)
|
||||
- Đặc điểm độc đáo (câu cửa miệng, trải nghiệm đặc biệt, sở thích cá nhân)
|
||||
- Ký ức cá nhân (phần quan trọng của nhân vật, giới thiệu mối liên hệ của cá nhân này với sự kiện, cũng như các hành động và phản ứng đã có của họ trong sự kiện)
|
||||
3. age: Con số tuổi (phải là số nguyên)
|
||||
4. gender: Giới tính, phải là tiếng Anh: "male" hoặc "female"
|
||||
5. mbti: Loại MBTI (như INTJ, ENFP, v.v.)
|
||||
6. country: Quốc gia (sử dụng tiếng Trung, ví dụ: "中国")
|
||||
7. profession: Nghề nghiệp
|
||||
8. interested_topics: Mảng các chủ đề quan tâm
|
||||
|
||||
Quan trọng:
|
||||
- Tất cả giá trị các trường phải là chuỗi hoặc số, không sử dụng ký tự xuống dòng (\n)
|
||||
- persona phải là một đoạn mô tả văn bản liên tục, không ngắt quãng
|
||||
- Sử dụng tiếng Việt (ngoại trừ trường gender bắt buộc dùng tiếng Anh male/female)
|
||||
- Nội dung phải nhất quán với thông tin của thực thể
|
||||
- age phải là số nguyên hợp lệ, gender phải là "male" hoặc "female"
|
||||
|
||||
QUAN TRỌNG:
|
||||
- Tất cả giá trị các trường phải là chuỗi hoặc số, không sử dụng ký tự xuống dòng.
|
||||
- 'persona' phải là một đoạn mô tả văn bản mạch lạc.
|
||||
- Sử dụng tiếng Trung (ngoại trừ trường 'gender' phải dùng tiếng Anh male/female).
|
||||
- Nội dung phải nhất quán với thông tin thực thể.
|
||||
- 'age' phải là số nguyên hợp lệ, 'gender' phải là "male" hoặc "female".
|
||||
"""
|
||||
|
||||
def _build_group_persona_prompt(
|
||||
|
|
@ -731,10 +775,46 @@ Quan trọng:
|
|||
) -> str:
|
||||
"""Tạo prompt chi tiết cho tài khoản đại diện tổ chức/nhóm"""
|
||||
|
||||
attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "Không có"
|
||||
context_str = context[:3000] if context else "Không có ngữ cảnh bổ sung"
|
||||
attrs_str = json.dumps(entity_attributes, ensure_ascii=False) if entity_attributes else "None"
|
||||
context_str = context[:3000] if context else "No additional context"
|
||||
|
||||
# return f"""Generate a detailed social media account persona for an organization/group entity, recreating existing real-world conditions to the greatest extent possible.
|
||||
|
||||
# Entity Name: {entity_name}
|
||||
# Entity Type: {entity_type}
|
||||
# Entity Summary: {entity_summary}
|
||||
# Entity Attributes: {attrs_str}
|
||||
|
||||
# Context Information:
|
||||
# {context_str}
|
||||
|
||||
# Please generate a JSON containing the following fields:
|
||||
|
||||
# 1. bio: Official account biography, 200 characters, professional and appropriate.
|
||||
# 2. persona: Detailed account setting description (2000 words of plain text), which must include:
|
||||
# - Basic information (formal name, nature of the organization, establishment background, primary functions)
|
||||
# - Account positioning (account type, target audience, core functions)
|
||||
# - Communication style (linguistic characteristics, common expressions, taboo topics)
|
||||
# - Content characteristics (content types, posting frequency, active time periods)
|
||||
# - Stance and attitude (official stance on core topics, handling of controversies)
|
||||
# - Special notes (persona of the group represented, operational habits)
|
||||
# - Organizational memory (a vital part of the persona, describing the organization's connection to the event and its existing actions/reactions)
|
||||
# 3. age: Fixed at 30 (virtual age for an organizational account)
|
||||
# 4. gender: Fixed as "other" (representing non-individual accounts)
|
||||
# 5. mbti: MBTI type used to describe the account's style (e.g., ISTJ for rigorous/conservative)
|
||||
# 6. country: Country (use Chinese, e.g., "中国")
|
||||
# 7. profession: Description of organizational functions
|
||||
# 8. interested_topics: An array of focused fields/areas of interest
|
||||
|
||||
# IMPORTANT:
|
||||
# - All field values must be strings or numbers; null values are not allowed.
|
||||
# - 'persona' must be a coherent block of text description; do not use line breaks.
|
||||
# - Use Chinese (except for the 'gender' field, which must be the English string "other").
|
||||
# - 'age' must be the integer 30; 'gender' must be the string "other".
|
||||
# - The account's tone and discourse must strictly align with its institutional identity and positioning.
|
||||
# """
|
||||
|
||||
return f"""Tạo một thiết lập tài khoản mạng xã hội chi tiết cho tổ chức/nhóm, phản ánh tối đa tình hình thực tế.
|
||||
return f"""Tạo thiết lập tài khoản mạng xã hội chi tiết cho thực thể tổ chức/nhóm, tái hiện tối đa các tình huống thực tế hiện có.
|
||||
|
||||
Tên thực thể: {entity_name}
|
||||
Loại thực thể: {entity_type}
|
||||
|
|
@ -744,30 +824,31 @@ Thuộc tính thực thể: {attrs_str}
|
|||
Thông tin ngữ cảnh:
|
||||
{context_str}
|
||||
|
||||
Vui lòng tạo JSON, bao gồm các trường sau:
|
||||
|
||||
1. bio: Giới thiệu tài khoản chính thức, 200 chữ, chuyên nghiệp và đúng mực
|
||||
2. persona: Mô tả chi tiết thiết lập tài khoản (văn bản thuần 2000 chữ), cần bao gồm:
|
||||
- Thông tin cơ bản của tổ chức (Tên chính thức, Tính chất tổ chức, Bối cảnh thành lập, Chức năng chính)
|
||||
- Định hướng tài khoản (Loại tài khoản, Đối tượng mục tiêu, Chức năng cốt lõi)
|
||||
- Phong cách phát ngôn (Đặc điểm ngôn ngữ, Các biểu đạt thường dùng, Chủ đề cấm kỵ)
|
||||
- Đặc điểm nội dung đăng tải (Loại nội dung, Tần suất đăng, Khung giờ hoạt động)
|
||||
- Lập trường thái độ (Lập trường chính thức đối với chủ đề cốt lõi, Cách xử lý khi đối mặt với tranh cãi)
|
||||
- Lưu ý đặc biệt (Chân dung nhóm đại diện, Thói quen vận hành)
|
||||
- Ký ức tổ chức (Phần quan trọng của thiết lập tổ chức, cần giới thiệu rõ mối liên hệ giữa tổ chức này với sự kiện, cũng như các hành động và phản ứng của tổ chức trong sự kiện)
|
||||
3. age: Điền cố định 30 (Tuổi ảo của tài khoản tổ chức)
|
||||
4. gender: Điền cố định "other" (Tài khoản tổ chức sử dụng other để biểu thị tính phi cá nhân)
|
||||
5. mbti: Loại MBTI, dùng để mô tả phong cách tài khoản, ví dụ ISTJ đại diện cho sự nghiêm ngặt, bảo thủ
|
||||
6. country: Quốc gia (sử dụng tiếng Việt, ví dụ "Việt Nam")
|
||||
7. profession: Mô tả chức năng tổ chức
|
||||
Vui lòng tạo JSON bao gồm các trường sau:
|
||||
|
||||
1. bio: Tiểu sử tài khoản chính thức, 200 ký tự, chuyên nghiệp và chuẩn mực.
|
||||
2. persona: Mô tả chi tiết thiết lập tài khoản (văn bản thuần túy khoảng 2000 từ), cần bao gồm:
|
||||
- Thông tin cơ bản về tổ chức (tên chính thức, tính chất tổ chức, bối cảnh thành lập, chức năng chính)
|
||||
- Định vị tài khoản (loại tài khoản, đối tượng mục tiêu, chức năng cốt lõi)
|
||||
- Phong cách phát ngôn (đặc điểm ngôn ngữ, biểu đạt thường dùng, các chủ đề cấm kỵ)
|
||||
- Đặc điểm nội dung đăng tải (loại nội dung, tần suất đăng, khung giờ hoạt động)
|
||||
- Lập trường thái độ (quan điểm chính thức về các chủ đề cốt lõi, cách xử lý tranh cãi)
|
||||
- Ghi chú đặc biệt (hồ sơ của nhóm mà tổ chức đại diện, thói quen vận hành)
|
||||
- Ký ức tổ chức (phần quan trọng của hồ sơ, giới thiệu mối liên hệ của tổ chức này với sự kiện, cũng như các hành động và phản ứng đã có của tổ chức trong sự kiện)
|
||||
3. age: Cố định là 30 (tuổi ảo cho tài khoản tổ chức)
|
||||
4. gender: Cố định là "other" (biểu thị tài khoản tổ chức, không phải cá nhân)
|
||||
5. mbti: Loại MBTI dùng để mô tả phong cách tài khoản (ví dụ: ISTJ đại diện cho sự nghiêm túc, bảo thủ)
|
||||
6. country: Quốc gia (sử dụng tiếng Trung, ví dụ: "中国")
|
||||
7. profession: Mô tả chức năng của tổ chức
|
||||
8. interested_topics: Mảng các lĩnh vực quan tâm
|
||||
|
||||
Quan trọng:
|
||||
- Tất cả giá trị các trường phải là chuỗi hoặc số, không cho phép giá trị null
|
||||
- persona phải là một đoạn mô tả văn bản liên tục, không sử dụng ký tự xuống dòng (\n)
|
||||
- Sử dụng tiếng Việt (ngoại trừ trường gender bắt buộc dùng tiếng Anh "other")
|
||||
- age phải là số nguyên 30, gender phải là chuỗi "other"
|
||||
- Phát ngôn của tài khoản tổ chức phải phù hợp với định vị danh tính của nó"""
|
||||
|
||||
QUAN TRỌNG:
|
||||
- Tất cả giá trị các trường phải là chuỗi hoặc số, không cho phép giá trị null.
|
||||
- 'persona' phải là một đoạn mô tả văn bản mạch lạc, không sử dụng ký tự xuống dòng.
|
||||
- Sử dụng tiếng Trung (ngoại trừ trường 'gender' phải dùng chuỗi tiếng Anh "other").
|
||||
- 'age' phải là số nguyên 30, 'gender' phải là chuỗi "other".
|
||||
- Phát ngôn và giọng điệu của tài khoản phải phù hợp tuyệt đối với định vị danh tính và đặc thù của tổ chức.
|
||||
"""
|
||||
|
||||
def _generate_profile_rule_based(
|
||||
self,
|
||||
|
|
@ -854,7 +935,9 @@ Quan trọng:
|
|||
graph_id: Optional[str] = None,
|
||||
parallel_count: int = 5,
|
||||
realtime_output_path: Optional[str] = None,
|
||||
output_platform: str = "reddit"
|
||||
output_platform: str = "reddit",
|
||||
simulation_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
) -> List[OasisAgentProfile]:
|
||||
"""
|
||||
Khởi tạo hàng loạt các Agent Profile từ các thực thể (Hỗ trợ Gen đa luồng song song)
|
||||
|
|
@ -877,6 +960,14 @@ Quan trọng:
|
|||
# Lưu Graph ID lại cho Zep xử lý search
|
||||
if graph_id:
|
||||
self.graph_id = graph_id
|
||||
|
||||
self._runtime_metadata = {
|
||||
"component": "oasis_profile_generator",
|
||||
"phase": "generate_profiles",
|
||||
"simulation_id": simulation_id,
|
||||
"project_id": project_id,
|
||||
"platform": output_platform,
|
||||
}
|
||||
|
||||
total = len(entities)
|
||||
profiles = [None] * total # Cấp trước 1 mảng để giữ đúng thứ tự Index
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ class FileParser:
|
|||
if suffix not in cls.SUPPORTED_EXTENSIONS:
|
||||
raise ValueError(f"Unsupported file format: {suffix}")
|
||||
|
||||
# Format Support: Leverages the existing extract_text method which supports PDF, Markdown, and TXT formats
|
||||
if suffix == '.pdf':
|
||||
return cls._extract_from_pdf(file_path)
|
||||
elif suffix in {'.md', '.markdown'}:
|
||||
|
|
@ -123,21 +124,27 @@ class FileParser:
|
|||
@classmethod
|
||||
def extract_from_multiple(cls, file_paths: List[str]) -> str:
|
||||
"""
|
||||
Trích xuất văn bản từ nhiều tệp và gộp lại
|
||||
Usage Context
|
||||
This function is typically used in the early stages of the GraphRAG pipeline when users upload multiple documents that need to be processed together for entity extraction and relationship mapping. The combined output serves as input for text chunking and subsequent LLM-based analysis in the knowledge graph construction workflow
|
||||
|
||||
Trích xuất văn bản từ nhiều tệp và gộp lại
|
||||
Args:
|
||||
file_paths: Danh sách đường dẫn tệp
|
||||
|
||||
Returns:
|
||||
Văn bản đã gộp
|
||||
|
||||
The extract_from_multiple method is a class method of the FileParser class that processes multiple document files simultaneously. It's designed to aggregate content from various source files (PDF, Markdown, TXT) into a single text string that can be fed into the knowledge graph construction pipeline.
|
||||
"""
|
||||
all_texts = []
|
||||
|
||||
# Batch Processing: Takes a list of file paths and processes each one sequentially
|
||||
for i, file_path in enumerate(file_paths, 1):
|
||||
try:
|
||||
text = cls.extract_text(file_path)
|
||||
filename = Path(file_path).name
|
||||
all_texts.append(f"=== Document {i}: {filename} ===\n{text}")
|
||||
# Error Handling: If a file fails to extract, it includes an error message in the output rather than failing completely
|
||||
except Exception as e:
|
||||
all_texts.append(f"=== Document {i}: {file_path} (extract failed: {str(e)}) ===")
|
||||
|
||||
|
|
|
|||
|
|
@ -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 và trả về JSON
|
||||
|
|
@ -88,7 +106,8 @@ class LLMClient:
|
|||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
response_format={"type": "json_object"}
|
||||
response_format={"type": "json_object"},
|
||||
metadata=metadata,
|
||||
)
|
||||
# Làm sạch markdown code fence
|
||||
cleaned_response = response.strip()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue