1203 lines
54 KiB
Python
1203 lines
54 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 .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 = [
|
|
"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
|
|
)
|
|
|
|
# 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 = self.client.chat.completions.create(
|
|
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
|
|
)
|
|
|
|
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 = "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."
|
|
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"""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ó.
|
|
|
|
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: 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")
|
|
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"
|
|
"""
|
|
|
|
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 "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 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ế.
|
|
|
|
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: 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
|
|
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ó"""
|
|
|
|
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"
|
|
) -> 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")
|
|
|
|
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
|
|
|
|
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)
|
|
|