MicroFish/backend/app/services/oasis_profile_generator.py

1296 lines
59 KiB
Python

"""
Trình tạo tạo ra Profile Agent (Hồ sơ Nhân vật) cho Agent bằng Framework OASIS
Chuyển đổi dữ liệu Thực thể được Query từ Zep ra chuẩn định dạng của các Agent tham gia vào mạng
Nâng cấp cải thiện:
1. Kết hợp dùng chức năng Search trên Zep để lấy Profile giàu sắc thái
2. Gen các cấu hình về tính cách một cách sắc xảo và cực sâu cho Prompt
3. Nhận dạng rạch ròi người với Group/Công ty/Phe phái trên social network
"""
import json
import random
import time
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
from datetime import datetime
from openai import OpenAI
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')
@dataclass
class OasisAgentProfile:
"""Cấu trúc của Dataclass Profile Agent qua quy định của OASIS"""
# Các Field thông dụng (Common data)
user_id: int
user_name: str
name: str
bio: str
persona: str
# Chọn bổ sung (Tùy chọn) - Thông số nền tảng Reddit (Karma)
karma: int = 1000
# Chọn bổ sung (Tùy chọn) - Thông số nền tảng Twitter
friend_count: int = 100
follower_count: int = 150
statuses_count: int = 500
# Một số Thông tin Data cá nhân bổ sung (Bóp để tăng tính Thực tế nếu LLM sinh ra)
age: Optional[int] = None
gender: Optional[str] = None
mbti: Optional[str] = None
country: Optional[str] = None
profession: Optional[str] = None
interested_topics: List[str] = field(default_factory=list)
# Lịch sử thông tin entity gốc được lấy
source_entity_uuid: Optional[str] = None
source_entity_type: Optional[str] = None
created_at: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d"))
def to_reddit_format(self) -> Dict[str, Any]:
"""Convert trả ra cho định dạng Agent reddit"""
profile = {
"user_id": self.user_id,
"username": self.user_name, # Source mã của OASIS Library yêu cầu không có dấu "_" cho param username
"name": self.name,
"bio": self.bio,
"persona": self.persona,
"karma": self.karma,
"created_at": self.created_at,
}
# Merge Thông tin Data Profile Cá nhân (Nếu CÓ)
if self.age:
profile["age"] = self.age
if self.gender:
profile["gender"] = self.gender
if self.mbti:
profile["mbti"] = self.mbti
if self.country:
profile["country"] = self.country
if self.profession:
profile["profession"] = self.profession
if self.interested_topics:
profile["interested_topics"] = self.interested_topics
return profile
def to_twitter_format(self) -> Dict[str, Any]:
"""Convert trả ra cho định dạng Agent Twitter"""
profile = {
"user_id": self.user_id,
"username": self.user_name, # Tương tự như trên
"name": self.name,
"bio": self.bio,
"persona": self.persona,
"friend_count": self.friend_count,
"follower_count": self.follower_count,
"statuses_count": self.statuses_count,
"created_at": self.created_at,
}
# Merge Thông tin Data Profile Cả nhân
if self.age:
profile["age"] = self.age
if self.gender:
profile["gender"] = self.gender
if self.mbti:
profile["mbti"] = self.mbti
if self.country:
profile["country"] = self.country
if self.profession:
profile["profession"] = self.profession
if self.interested_topics:
profile["interested_topics"] = self.interested_topics
return profile
def to_dict(self) -> Dict[str, Any]:
"""Quy đổi thành toàn bộ Dictionary Cấu Trúc Khép Kín """
return {
"user_id": self.user_id,
"user_name": self.user_name,
"name": self.name,
"bio": self.bio,
"persona": self.persona,
"karma": self.karma,
"friend_count": self.friend_count,
"follower_count": self.follower_count,
"statuses_count": self.statuses_count,
"age": self.age,
"gender": self.gender,
"mbti": self.mbti,
"country": self.country,
"profession": self.profession,
"interested_topics": self.interested_topics,
"source_entity_uuid": self.source_entity_uuid,
"source_entity_type": self.source_entity_type,
"created_at": self.created_at,
}
class OasisProfileGenerator:
"""
Trình Gen Profile cho Simulation (Hệ OASIS)
Sử dụng các Node Entity lấy được từ ZEP -> OASIS Mocks cho Simulation Agent
Các Option Cải Tiến Tối Ưu Tích Hợp:
1. Có liên kết với Server Zep API cho bước Query Dữ liệu từ Vector Database
2. Tập trung Mô tả Tiểu sửa (Background) (Nhấn mạnh Nghề nghiệp/Tính cách/StatusMXH/vvv)
3. Ngắt rời loại Person ra bên ngoài để nhận thức Phân Cấp Group
"""
# 16 tính cách của Con người (Quy Chuẩn)
MBTI_TYPES = [
"INTJ", "INTP", "ENTJ", "ENTP",
"INFJ", "INFP", "ENFJ", "ENFP",
"ISTJ", "ISFJ", "ESTJ", "ESFJ",
"ISTP", "ISFP", "ESTP", "ESFP"
]
# List mảng quốc tịch Cơ Bản
COUNTRIES = [
"Vietnam", "China", "US", "UK", "Japan", "Germany", "France",
"Canada", "Australia", "Brazil", "India", "South Korea"
]
# Thực thể nhận biết là người 1 mình (Độc Lập, 1 Person)
INDIVIDUAL_ENTITY_TYPES = [
"student", "alumni", "professor", "person", "publicfigure",
"expert", "faculty", "official", "journalist", "activist"
]
# Thực Thể được xem là một tổ chức
GROUP_ENTITY_TYPES = [
"university", "governmentagency", "organization", "ngo",
"mediaoutlet", "company", "institution", "group", "community"
]
def __init__(
self,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
model_name: Optional[str] = None,
zep_api_key: Optional[str] = None,
graph_id: Optional[str] = None
):
self.api_key = api_key or Config.LLM_API_KEY
self.base_url = base_url or Config.LLM_BASE_URL
self.model_name = model_name or Config.LLM_MODEL_NAME
if not self.api_key:
raise ValueError("Không tìm thấy LLM_API_KEY")
self.client = OpenAI(
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
self.zep_client = None
self.graph_id = graph_id
if self.zep_api_key:
try:
self.zep_client = Zep(api_key=self.zep_api_key)
except Exception as e:
logger.warning(f"Failed to initialize Zep client: {e}")
def generate_profile_from_entity(
self,
entity: EntityNode,
user_id: int,
use_llm: bool = True
) -> OasisAgentProfile:
"""
Bắt đầu Gen Profile từ Entity được móc từ data từ zep
Args:
entity: Thực thể Zep
user_id: Số ID để map (sử dụng trên OASIS)
use_llm: Chọn bật tắt xem tạo Profile có dùng gen nhân vật bằng LLM
Returns:
OasisAgentProfile
"""
entity_type = entity.get_entity_type() or "Entity"
# Mức cơ bản thông tin
name = entity.name
user_name = self._generate_username(name)
# Build các Context thông tin liên quan lại
context = self._build_entity_context(entity)
if use_llm:
# Gửi Prompt lên LLM
profile_data = self._generate_profile_with_llm(
entity_name=name,
entity_type=entity_type,
entity_summary=entity.summary,
entity_attributes=entity.attributes,
context=context
)
else:
# Chạy hàm Auto rule nếu LLm tắt
profile_data = self._generate_profile_rule_based(
entity_name=name,
entity_type=entity_type,
entity_summary=entity.summary,
entity_attributes=entity.attributes
)
return OasisAgentProfile(
user_id=user_id,
user_name=user_name,
name=name,
bio=profile_data.get("bio", f"{entity_type}: {name}"),
persona=profile_data.get("persona", entity.summary or f"A {entity_type} named {name}."),
karma=profile_data.get("karma", random.randint(500, 5000)),
friend_count=profile_data.get("friend_count", random.randint(50, 500)),
follower_count=profile_data.get("follower_count", random.randint(100, 1000)),
statuses_count=profile_data.get("statuses_count", random.randint(100, 2000)),
age=profile_data.get("age"),
gender=profile_data.get("gender"),
mbti=profile_data.get("mbti"),
country=profile_data.get("country"),
profession=profile_data.get("profession"),
interested_topics=profile_data.get("interested_topics", []),
source_entity_uuid=entity.uuid,
source_entity_type=entity_type,
)
def _generate_username(self, name: str) -> str:
"""Thêm chức năng Generate Username username ngẫu nhiên"""
# Hút bỏ khoảng trống và dấu đặc biệt
username = name.lower().replace(" ", "_")
username = ''.join(c for c in username if c.isalnum() or c == '_')
# Chèn thêm hậu tố cho bớt đụng hàng
suffix = random.randint(100, 999)
return f"{username}_{suffix}"
def _search_zep_for_entity(self, entity: EntityNode) -> Dict[str, Any]:
"""
Dùng hỗn hợp lệnh Query DB Vector qua Zep để lấy các fact/sự kiện liên quan về 1 thực thể.
Vì Zep chưa hỗ trợ hỗn hợp cả 2 cùng một lúc, nên cần tìm song song từ Edge và Node sau đó gộp kết quả.
Args:
entity: Đầu cắm Thực Thể Node
Returns:
Dictionary gồm facts, node_summaries, context
"""
import concurrent.futures
if not self.zep_client:
return {"facts": [], "node_summaries": [], "context": ""}
entity_name = entity.name
results = {
"facts": [],
"node_summaries": [],
"context": ""
}
# Yêu cầu graph_id mới truy vấn được
if not self.graph_id:
logger.debug(f"Skipping Zep search: graph_id not set")
return results
comprehensive_query = f"Provide all facts, activities, relationships, and context about: {entity_name}"
def search_edges():
"""Lookup cạnh relations - Kết hợp cơ chế retry"""
max_retries = 3
last_exception = None
delay = 2.0
for attempt in range(max_retries):
try:
return self.zep_client.graph.search(
query=comprehensive_query,
graph_id=self.graph_id,
limit=30,
scope="edges",
reranker="rrf"
)
except Exception as e:
last_exception = e
if attempt < max_retries - 1:
logger.debug(f"Zep Edge search failed on attempt {attempt + 1}: {str(e)[:80]}, retrying...")
time.sleep(delay)
delay *= 2
else:
logger.debug(f"Zep Edge search entirely failed after {max_retries} attempts: {e}")
return None
def search_nodes():
"""Lookup mảng Node entity tóm tắt - Kết hợp cơ chế retry"""
max_retries = 3
last_exception = None
delay = 2.0
for attempt in range(max_retries):
try:
return self.zep_client.graph.search(
query=comprehensive_query,
graph_id=self.graph_id,
limit=20,
scope="nodes",
reranker="rrf"
)
except Exception as e:
last_exception = e
if attempt < max_retries - 1:
logger.debug(f"Zep Node search failed on attempt {attempt + 1}: {str(e)[:80]}, retrying...")
time.sleep(delay)
delay *= 2
else:
logger.debug(f"Zep Node search entirely failed after {max_retries} attempts: {e}")
return None
try:
# Cho chạy cả task Cạnh và Node song song
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
edge_future = executor.submit(search_edges)
node_future = executor.submit(search_nodes)
# Fetch result trả về
edge_result = edge_future.result(timeout=30)
node_result = node_future.result(timeout=30)
# Quản lý kết quả fact của các cạnh Edge
all_facts = set()
if edge_result and hasattr(edge_result, 'edges') and edge_result.edges:
for edge in edge_result.edges:
if hasattr(edge, 'fact') and edge.fact:
all_facts.add(edge.fact)
results["facts"] = list(all_facts)
# Quản lý kết quả tên thực thể và summary của quá trình search Node
all_summaries = set()
if node_result and hasattr(node_result, 'nodes') and node_result.nodes:
for node in node_result.nodes:
if hasattr(node, 'summary') and node.summary:
all_summaries.add(node.summary)
if hasattr(node, 'name') and node.name and node.name != entity_name:
all_summaries.add(f"Related Entities: {node.name}")
results["node_summaries"] = list(all_summaries)
# Tổng hợp ra 1 chuỗi Context bao quanh
context_parts = []
if results["facts"]:
context_parts.append("Facts & Infomation:\n" + "\n".join(f"- {f}" for f in results["facts"][:20]))
if results["node_summaries"]:
context_parts.append("Related Entities:\n" + "\n".join(f"- {s}" for s in results["node_summaries"][:10]))
results["context"] = "\n\n".join(context_parts)
logger.info(f"Zep unified search completed: {entity_name}, fetched {len(results['facts'])} facts, {len(results['node_summaries'])} related nodes")
except concurrent.futures.TimeoutError:
logger.warning(f"Zep Retrieval Time-Out ({entity_name})")
except Exception as e:
logger.warning(f"Zep Retrieval Failed ({entity_name}): {e}")
return results
def _build_entity_context(self, entity: EntityNode) -> str:
"""
Nối tất cả info thu được liên quan thành 1 chuỗi Context bao quanh hoàn chỉnh cho Entity
Nó sẽ lấy:
1. Context từ các cạnh hiện tại đã gắn Entity (Dữ liệu về relation/fact)
2. Mô tả sơ lược thêm của các Node dính liền
3. Cuối cùng nhồi thêm những thứ moi được từ quá trình chạy Zep search hỗn hợp bên trên
"""
context_parts = []
# 1. Thu thập Attributes/Properties của node nếu có
if entity.attributes:
attrs = []
for key, value in entity.attributes.items():
if value and str(value).strip():
attrs.append(f"- {key}: {value}")
if attrs:
context_parts.append("### Entity Attributes\n" + "\n".join(attrs))
# 2. Add các facts và mô phỏng cạnh (Relationship/Facts)
existing_facts = set()
if entity.related_edges:
relationships = []
for edge in entity.related_edges: # Khum bị giới hạn SL
fact = edge.get("fact", "")
edge_name = edge.get("edge_name", "")
direction = edge.get("direction", "")
if fact:
relationships.append(f"- {fact}")
existing_facts.add(fact)
elif edge_name:
if direction == "outgoing":
relationships.append(f"- {entity.name} --[{edge_name}]--> (Related Entity)")
else:
relationships.append(f"- (Related Entity) --[{edge_name}]--> {entity.name}")
if relationships:
context_parts.append("### Facts & Relationships\n" + "\n".join(relationships))
# 3. Kẹp chi tiết miêu tả về node anh em cạnh bên
if entity.related_nodes:
related_info = []
for node in entity.related_nodes: # Không block giới hạn số lượng
node_name = node.get("name", "")
node_labels = node.get("labels", [])
node_summary = node.get("summary", "")
# Bỏ nhãn mặc định khỏi string xuất ra
custom_labels = [l for l in node_labels if l not in ["Entity", "Node"]]
label_str = f" ({', '.join(custom_labels)})" if custom_labels else ""
if node_summary:
related_info.append(f"- **{node_name}**{label_str}: {node_summary}")
else:
related_info.append(f"- **{node_name}**{label_str}")
if related_info:
context_parts.append("### Related Entity Info\n" + "\n".join(related_info))
# 4. Sử dụng kết quả Query Search từ hàm zep
zep_results = self._search_zep_for_entity(entity)
if zep_results.get("facts"):
# Lọc bớt cặn trùng lắp: không add những Fact đã có ở mục số 2
new_facts = [f for f in zep_results["facts"] if f not in existing_facts]
if new_facts:
context_parts.append("### Facts retrieved via ZEP\n" + "\n".join(f"- {f}" for f in new_facts[:15]))
if zep_results.get("node_summaries"):
context_parts.append("### Entity nodes retrieved via Zep\n" + "\n".join(f"- {s}" for s in zep_results["node_summaries"][:10]))
return "\n\n".join(context_parts)
def _is_individual_entity(self, entity_type: str) -> bool:
"""KTra và True cho các dạng người Single Person"""
return entity_type.lower() in self.INDIVIDUAL_ENTITY_TYPES
def _is_group_entity(self, entity_type: str) -> bool:
"""KTra xem thực thể hiện tại là Group/Media/Company ..."""
return entity_type.lower() in self.GROUP_ENTITY_TYPES
def _generate_profile_with_llm(
self,
entity_name: str,
entity_type: str,
entity_summary: str,
entity_attributes: Dict[str, Any],
context: str
) -> Dict[str, Any]:
"""
Dùng LLM cấp lại Profile mô phỏng tính cách cụ thể và rõ nét nhất
Kiểm tra đầu vào Entity type để chia nhánh:
- Cá nhân: Tạo setting miêu tả cá nhân, công việc riêng
- Tập Thể/Cơ quan/Tổ chức: Tạo Profile cho 1 tài khoản đại điện tổ chức đó
"""
is_individual = self._is_individual_entity(entity_type)
if is_individual:
prompt = self._build_individual_persona_prompt(
entity_name, entity_type, entity_summary, entity_attributes, context
)
else:
prompt = self._build_group_persona_prompt(
entity_name, entity_type, entity_summary, entity_attributes, context
)
# Retry liên tục vòng lặp nếu LLM timeout hoặc fail
max_attempts = 3
last_error = None
for attempt in range(max_attempts):
try:
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
metadata=self._runtime_metadata,
)
content = response.choices[0].message.content
# Check LLM trả về vì sao bị kẹt lại/Dừng lại (Finish_Reason khác "stop")
finish_reason = response.choices[0].finish_reason
if finish_reason == 'length':
logger.warning(f"LLM output truncated (attempt {attempt+1}), attempting to fix...")
content = self._fix_truncated_json(content)
# Parse chép vào JSON
try:
result = json.loads(content)
# Xác minh tham số được Bot gen thành công chưa
if "bio" not in result or not result["bio"]:
result["bio"] = entity_summary[:200] if entity_summary else f"{entity_type}: {entity_name}"
if "persona" not in result or not result["persona"]:
result["persona"] = entity_summary or f"{entity_name} is a {entity_type}."
return result
except json.JSONDecodeError as je:
logger.warning(f"JSON Parsing Failed (attempt {attempt+1}): {str(je)[:80]}")
# Tool sửa lỗi JSON syntax tự chế
result = self._try_fix_json(content, entity_name, entity_type, entity_summary)
if result.get("_fixed"):
del result["_fixed"]
return result
last_error = je
except Exception as e:
logger.warning(f"LLM call Failed (attempt {attempt+1}): {str(e)[:80]}")
last_error = e
import time
time.sleep(1 * (attempt + 1)) # Exponential backoff
logger.warning(f"Generating profile through LLM failed totally after {max_attempts} attempts: {last_error}, switching to basic hard-code rules configs")
return self._generate_profile_rule_based(
entity_name, entity_type, entity_summary, entity_attributes
)
def _fix_truncated_json(self, content: str) -> str:
"""Fix Output JSON bị Max_tokens đè cắt gãy"""
import re
# Bọc ngoài
content = content.strip()
# Điểm kiểm chứng xem dấu ngoặc được đầy đủ hay chưa
open_braces = content.count('{') - content.count('}')
open_brackets = content.count('[') - content.count(']')
# Check string xem đủ không
# Nếu phần tử cuối cùng k phải dấu câu đóng block, tự chèn vào
if content and content[-1] not in '",}]':
# Ngoặc cho đít chuỗi
content += '"'
# Ngoặc block
content += ']' * open_brackets
content += '}' * open_braces
return content
def _try_fix_json(self, content: str, entity_name: str, entity_type: str, entity_summary: str = "") -> Dict[str, Any]:
"""Thử Fix nội dung JSON"""
import re
# 1. Bọc json trước
content = self._fix_truncated_json(content)
# 2. Extract block lớn
json_match = re.search(r'\{[\s\S]*\}', content)
if json_match:
json_str = json_match.group()
# 3. Clean lại các chuỗi xuống dòng
# Regex lôi code ra ngoài
def fix_string_newlines(match):
s = match.group(0)
# Escape code line chuyển cho space cho an toàn
s = s.replace('\n', ' ').replace('\r', ' ')
# Chém các khoảng trống còn dư quá gắt
s = re.sub(r'\s+', ' ', s)
return s
# Khớp lại nội dung
json_str = re.sub(r'"[^"\\]*(?:\\.[^"\\]*)*"', fix_string_newlines, json_str)
# 4. Bắt đầu json parse
try:
result = json.loads(json_str)
result["_fixed"] = True
return result
except json.JSONDecodeError as e:
# 5. Phá lấu clean xóa nếu còn bị lỗi Control char ẩn (0x00 đến 0x1f ...)
try:
# Chém control character
json_str = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', json_str)
# Gọt lại khoảng trống dư
json_str = re.sub(r'\s+', ' ', json_str)
result = json.loads(json_str)
result["_fixed"] = True
return result
except:
pass
# 6. Rescue lấy các property còn lại mót ra từ đóng hỗn độn
bio_match = re.search(r'"bio"\s*:\s*"([^"]*)"', content)
persona_match = re.search(r'"persona"\s*:\s*"([^"]*)', content) # Bị cắt khúc thì ráng chịu
bio = bio_match.group(1) if bio_match else (entity_summary[:200] if entity_summary else f"{entity_type}: {entity_name}")
persona = persona_match.group(1) if persona_match else (entity_summary or f"{entity_name} is a {entity_type}.")
# Lụm mót được data xịn thì mark là fix thành công
if bio_match or persona_match:
logger.info(f"Successfully extracted partial info from corrupted JSON")
return {
"bio": bio,
"persona": persona,
"_fixed": True
}
# 7. Failed sạch, quăng cái khung mặc định ra
logger.warning(f"Failed to fix JSON, returning basic structured data")
return {
"bio": entity_summary[:200] if entity_summary else f"{entity_type}: {entity_name}",
"persona": entity_summary or f"{entity_name} is a {entity_type}."
}
def _get_system_prompt(self, is_individual: bool) -> str:
"""Lấy prompt cho hệ thống"""
# 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 Vietnamese."
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(
self,
entity_name: str,
entity_type: str,
entity_summary: str,
entity_attributes: Dict[str, Any],
context: str
) -> str:
"""Tạo prompt nhân vật chi tiết cho thực thể cá nhân"""
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"""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 Vietnamese, e.g., "Việt Nam")
# 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 Vietnamese (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}
Tóm tắt thực thể: {entity_summary}
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: 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 Việt, ví dụ: "Việt Nam")
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.
- 'persona' phải là một đoạn mô tả văn bản mạch lạc.
- Sử dụng tiếng Việt (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(
self,
entity_name: str,
entity_type: str,
entity_summary: str,
entity_attributes: Dict[str, Any],
context: str
) -> 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 "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 Vietnamese, e.g., "Việt Nam")
# 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 Vietnamese (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 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}
Tóm tắt thực thể: {entity_summary}
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: 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 Việt, ví dụ: "Việt Nam")
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 mạch lạc, không sử dụng ký tự xuống dòng.
- Sử dụng tiếng Việt (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,
entity_name: str,
entity_type: str,
entity_summary: str,
entity_attributes: Dict[str, Any]
) -> Dict[str, Any]:
"""Sử dụng rule để tạo Profile cơ bản khi dự phòng"""
# Phân nhánh theo loại thực thể để tạo Profile thủ công
entity_type_lower = entity_type.lower()
if entity_type_lower in ["student", "alumni"]:
return {
"bio": f"{entity_type} with interests in academics and social issues.",
"persona": f"{entity_name} is a {entity_type.lower()} who is actively engaged in academic and social discussions. They enjoy sharing perspectives and connecting with peers.",
"age": random.randint(18, 30),
"gender": random.choice(["male", "female"]),
"mbti": random.choice(self.MBTI_TYPES),
"country": random.choice(self.COUNTRIES),
"profession": "Student",
"interested_topics": ["Education", "Social Issues", "Technology"],
}
elif entity_type_lower in ["publicfigure", "expert", "faculty"]:
return {
"bio": f"Expert and thought leader in their field.",
"persona": f"{entity_name} is a recognized {entity_type.lower()} who shares insights and opinions on important matters. They are known for their expertise and influence in public discourse.",
"age": random.randint(35, 60),
"gender": random.choice(["male", "female"]),
"mbti": random.choice(["ENTJ", "INTJ", "ENTP", "INTP"]),
"country": random.choice(self.COUNTRIES),
"profession": entity_attributes.get("occupation", "Expert"),
"interested_topics": ["Politics", "Economics", "Culture & Society"],
}
elif entity_type_lower in ["mediaoutlet", "socialmediaplatform"]:
return {
"bio": f"Official account for {entity_name}. News and updates.",
"persona": f"{entity_name} is a media entity that reports news and facilitates public discourse. The account shares timely updates and engages with the audience on current events.",
"age": 30, # Tuổi ảo của cơ quan/tổ chức
"gender": "other", # Cơ quan dùng "other"
"mbti": "ISTJ", # Phong cách tổ chức: nghiêm túc bảo thủ
"country": "Việt Nam",
"profession": "Media",
"interested_topics": ["General News", "Current Events", "Public Affairs"],
}
elif entity_type_lower in ["university", "governmentagency", "ngo", "organization"]:
return {
"bio": f"Official account of {entity_name}.",
"persona": f"{entity_name} is an institutional entity that communicates official positions, announcements, and engages with stakeholders on relevant matters.",
"age": 30, # Tuổi ảo của cơ quan/tổ chức
"gender": "other", # Cơ quan dùng "other"
"mbti": "ISTJ", # Phong cách tổ chức: nghiêm túc bảo thủ
"country": "Việt Nam",
"profession": entity_type,
"interested_topics": ["Public Policy", "Community", "Official Announcements"],
}
else:
# Profile mặc định (Fallback default)
return {
"bio": entity_summary[:150] if entity_summary else f"{entity_type}: {entity_name}",
"persona": entity_summary or f"{entity_name} is a {entity_type.lower()} participating in social discussions.",
"age": random.randint(25, 50),
"gender": random.choice(["male", "female"]),
"mbti": random.choice(self.MBTI_TYPES),
"country": random.choice(self.COUNTRIES),
"profession": entity_type,
"interested_topics": ["General", "Social Issues"],
}
def set_graph_id(self, graph_id: str):
"""Lưu lại Graph ID để dùng cho việc tra cứu Zep"""
self.graph_id = graph_id
def generate_profiles_from_entities(
self,
entities: List[EntityNode],
use_llm: bool = True,
progress_callback: Optional[callable] = None,
graph_id: Optional[str] = None,
parallel_count: int = 5,
realtime_output_path: Optional[str] = None,
output_platform: str = "reddit",
metadata_platform: Optional[str] = None,
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)
Args:
entities: Danh sách thực thể
use_llm: Có sử dụng LLM để tạo tính cách chi tiết hay không
progress_callback: Hàm CallBack báo tiến độ (current, total, message)
graph_id: Đưa Graph ID vào để Zep retrieval thêm nhiều ngữ cảnh phong phú
parallel_count: Số luồng song song, mặc định 5
realtime_output_path: Đường dẫn lưu file realtime (Gen ra đứa nào auto save đứa đó luôn)
output_platform: Format lưu trữ output ("reddit" hoạc "twitter")
metadata_platform: Nền tảng dùng cho metadata cost
Returns:
Danh sách Profile Agent
"""
import concurrent.futures
from threading import Lock
# 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": metadata_platform,
}
total = len(entities)
profiles = [None] * total # Cấp trước 1 mảng để giữ đúng thứ tự Index
completed_count = [0] # Phải dùng List để closure của các Sub Thread update được
lock = Lock()
# Hàm con hỗ trợ việc ghi realtime file trong Thread
def save_profiles_realtime():
"""Lưu file json ngay lập tức khi profile được tạo mới thành công"""
if not realtime_output_path:
return
with lock:
# Lọc ra những profile đã làm xong
existing_profiles = [p for p in profiles if p is not None]
if not existing_profiles:
return
try:
if output_platform == "reddit":
# Cấu trúc dành cho định dạng Reddit
profiles_data = [p.to_reddit_format() for p in existing_profiles]
with open(realtime_output_path, 'w', encoding='utf-8') as f:
json.dump(profiles_data, f, ensure_ascii=False, indent=2)
else:
# Cấu trúc dành cho định dạng Twitter (CSV)
import csv
profiles_data = [p.to_twitter_format() for p in existing_profiles]
if profiles_data:
fieldnames = list(profiles_data[0].keys())
with open(realtime_output_path, 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(profiles_data)
except Exception as e:
logger.warning(f"Failed to save profile in realtime: {e}")
def generate_single_profile(idx: int, entity: EntityNode) -> tuple:
"""Hàm Worker gen từng profile riêng lẻ"""
entity_type = entity.get_entity_type() or "Entity"
try:
profile = self.generate_profile_from_entity(
entity=entity,
user_id=idx,
use_llm=use_llm
)
# Print output để nhìn trực tiếp Log terminal
self._print_generated_profile(entity.name, entity_type, profile)
return idx, profile, None
except Exception as e:
logger.error(f"Failed to generate profile for entity {entity.name}: {str(e)}")
# Rơi vào tạo Profile dự phòng (Fallback)
fallback_profile = OasisAgentProfile(
user_id=idx,
user_name=self._generate_username(entity.name),
name=entity.name,
bio=f"{entity_type}: {entity.name}",
persona=entity.summary or f"A participant in social discussions.",
source_entity_uuid=entity.uuid,
source_entity_type=entity_type,
)
return idx, fallback_profile, str(e)
logger.info(f"Start parallel profile generation for {total} entities (Concurrency: {parallel_count})...")
print(f"\n{'='*60}")
print(f"Starting Agent Profile Generation - Total {total} entities, concurrency: {parallel_count}")
print(f"{'='*60}\n")
# Chạy đa luồng thread pool
with concurrent.futures.ThreadPoolExecutor(max_workers=parallel_count) as executor:
# Giao Task
future_to_entity = {
executor.submit(generate_single_profile, idx, entity): (idx, entity)
for idx, entity in enumerate(entities)
}
# Thu gom kết quả
for future in concurrent.futures.as_completed(future_to_entity):
idx, entity = future_to_entity[future]
entity_type = entity.get_entity_type() or "Entity"
try:
result_idx, profile, error = future.result()
profiles[result_idx] = profile
with lock:
completed_count[0] += 1
current = completed_count[0]
# Ghi file Realtime
save_profiles_realtime()
if progress_callback:
progress_callback(
current,
total,
f"Completed {current}/{total}: {entity.name} ({entity_type})"
)
if error:
logger.warning(f"[{current}/{total}] Entity {entity.name} applied fallback profile due to error: {error}")
else:
logger.info(f"[{current}/{total}] Automatically generated profile for: {entity.name} ({entity_type})")
except Exception as e:
logger.error(f"Error handling profile for entity {entity.name}: {str(e)}")
with lock:
completed_count[0] += 1
profiles[idx] = OasisAgentProfile(
user_id=idx,
user_name=self._generate_username(entity.name),
name=entity.name,
bio=f"{entity_type}: {entity.name}",
persona=entity.summary or "A participant in social discussions.",
source_entity_uuid=entity.uuid,
source_entity_type=entity_type,
)
# Ghi file Realtime file (Dù là profile xài fallback)
save_profiles_realtime()
print(f"\n{'='*60}")
print(f"Profile generation complete! Successfully created {len([p for p in profiles if p])} Agents")
print(f"{'='*60}\n")
return profiles
def _print_generated_profile(self, entity_name: str, entity_type: str, profile: OasisAgentProfile):
"""Xuất thông tin Profile vưa gen ra Terminal để review dễ dàng (Kéo dài không bị gãy log)"""
separator = "-" * 70
# Xây cấu trúc Log
topics_str = ', '.join(profile.interested_topics) if profile.interested_topics else 'Không có'
output_lines = [
f"\n{separator}",
f"[Generated] {entity_name} ({entity_type})",
f"{separator}",
f"Tên tài khoản (Username): {profile.user_name}",
f"",
f"【Tiểu sử / Bio】",
f"{profile.bio}",
f"",
f"【Nhân cách cụ thể / Persona】",
f"{profile.persona}",
f"",
f"【Thuộc tính cơ bản / Attributes】",
f"Tuổi: {profile.age} | Giới tính: {profile.gender} | MBTI: {profile.mbti}",
f"Nghề nghiệp: {profile.profession} | Quốc gia: {profile.country}",
f"Chủ đề quan tâm: {topics_str}",
separator
]
output = "\n".join(output_lines)
# Chỉ in ra Console bằng lệnh print (Logger sẽ làm rối và có thể bị truncate)
print(output)
def save_profiles(
self,
profiles: List[OasisAgentProfile],
file_path: str,
platform: str = "reddit"
):
"""
Ghi file Profile xuống thư mục (Cấu trúc file tuỳ thuộc vào nền tảng)
Định dạng mặc định của Framework OASIS yêu cầu:
- Twitter: Định dạng file CSV
- Reddit: Định dạng file JSON
Args:
profiles: Danh sách Profile
file_path: Đường dẫn lưu file
platform: Tên nền tảng ("reddit" hoặc "twitter")
"""
if platform == "twitter":
self._save_twitter_csv(profiles, file_path)
else:
self._save_reddit_json(profiles, file_path)
def _save_twitter_csv(self, profiles: List[OasisAgentProfile], file_path: str):
"""
Lưu Profile hệ Twitter ở định dạng CSV (Bám vào yêu cầu kỹ thuật do OASIS ban hành)
Các trường bắt buộc để tương thích OASIS Twitter File CSV:
- user_id: Mã định danh ID (Từ 0 theo Index mảng)
- name: Tên thật của Agent đó
- username: Tên Alias/Tài khoản xài trong hệ thống
- user_char: Bản nháp Setting cụ thể truyền vào System Prompt LLM, định hình mọi ý nghĩ/phát ngôn
- description: Bản Bio gắn ngoài hiển thị cho các User khác thấy (Ngắn gọn)
Sự khác biệt user_char và description:
- user_char: Data nội bộ chỉ Gen AI thấy (Giống prompt điều khiển não)
- description: Public Info đưa lên trang cá nhân
"""
import csv
# Check đuôi file có nhầm thành json không
if not file_path.endswith('.csv'):
file_path = file_path.replace('.json', '.csv')
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# Khởi tạo Header theo chuẩn file OASIS
headers = ['user_id', 'name', 'username', 'user_char', 'description']
writer.writerow(headers)
# Xuất từng dòng Dữ liệu Profile
for idx, profile in enumerate(profiles):
# user_char: Nhân cách tổng (bio + persona) - Thả cho Prompt System LLM
user_char = profile.bio
if profile.persona and profile.persona != profile.bio:
user_char = f"{profile.bio} {profile.persona}"
# Làm sạch dấu newline để nhét vào dòng CSV
user_char = user_char.replace('\n', ' ').replace('\r', ' ')
# description: Thông tin Bio hiển thị công khai mạng xã hội
description = profile.bio.replace('\n', ' ').replace('\r', ' ')
row = [
idx, # user_id: ID bắt đầu từ 0
profile.name, # name: Tên thực
profile.user_name, # username: Tên định danh
user_char, # user_char: Mô tả ẩn của Bot
description # description: Bảng mô tả Công khai
]
writer.writerow(row)
logger.info(f"Saved {len(profiles)} Twitter Profiles to {file_path} (OASIS CSV Format)")
def _normalize_gender(self, gender: Optional[str]) -> str:
"""
Biên dịch, chuẩn hóa cột Gender về đúng dạng mà OASIS engine chấp nhận
OASIS quy định buộc xài enum: male, female, other
"""
if not gender:
return "other"
gender_lower = gender.lower().strip()
# Mapping các Keyword
gender_map = {
"": "male",
"": "female",
"机构": "other",
"其他": "other",
"nam": "male",
"nữ": "female",
"tổ chức": "other",
# Giữ nguyên Tiếng Anh Default
"male": "male",
"female": "female",
"other": "other",
}
return gender_map.get(gender_lower, "other")
def _save_reddit_json(self, profiles: List[OasisAgentProfile], file_path: str):
"""
Lưu Profile hệ Reddit bằng JSON (Bám vào yêu cầu kỹ thuật do OASIS ban hành)
Format cấu trúc dựa tương đồng với hàm to_reddit_format().
Luôn luôn phải có thuộc tính user_id, KEY QUAN TRỌNG ĐỂ HỖ TRỢ HÀM agent_graph.get_agent() MAP CÁC PROFILE !!!
Các field bắt buộc:
- user_id: User ID dạng Int
- username: ID Account
- name: Tên hiển thị
- bio: Thông tin hiển thị Bio cá nhân
- persona: Prompt Settings điều khiển Bot nội bộ
- age: Tuổi (Int)
- gender: "male", "female", hoặc "other"
- mbti: Kiểu loại nhóm MBTI
- country: Quốc gia Country
"""
data = []
for idx, profile in enumerate(profiles):
# Parse Format chung với hàm class to_reddit_format()
item = {
"user_id": profile.user_id if profile.user_id is not None else idx, # Quan trọng: Bắt buộc kèm "user_id"
"username": profile.user_name,
"name": profile.name,
"bio": profile.bio[:150] if profile.bio else f"{profile.name}",
"persona": profile.persona or f"{profile.name} is a participant in social discussions.",
"karma": profile.karma if profile.karma else 1000,
"created_at": profile.created_at,
# Fix bù tham số ảo cho các properties bị trống
"age": profile.age if profile.age else 30,
"gender": self._normalize_gender(profile.gender),
"mbti": profile.mbti if profile.mbti else "ISTJ",
"country": profile.country if profile.country else "Việt Nam",
}
# Cột Tuỳ chọn
if profile.profession:
item["profession"] = profile.profession
if profile.interested_topics:
item["interested_topics"] = profile.interested_topics
data.append(item)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"Saved {len(profiles)} Reddit Profiles to {file_path} (JSON Config File - with user_id mapped)")
# Giữ lại Function name cũ để hệ thống vẫn tương thích backward.
def save_profiles_to_json(
self,
profiles: List[OasisAgentProfile],
file_path: str,
platform: str = "reddit"
):
"""[Deprecated - Hết hạn dùng] KHUYÊN DÙNG LỆNH save_profiles() THAY VÌ PHƯƠNG THỨC NÀY"""
logger.warning("save_profiles_to_json is Deprecated. Use save_profiles method instead!")
self.save_profiles(profiles, file_path, platform)