Add new JSON data file and enhance simulation management features

- Introduced a new JSON data file containing detailed actions and quotes related to the 武大声誉修复基金 initiative.
- Updated the OasisProfileGenerator to ensure compatibility with the new JSON format, emphasizing the inclusion of user_id.
- Modified simulation management to support independent tracking of Twitter and Reddit platforms, including completion status and round information.
- Enhanced the SimulationRunner to accurately reflect the completion state of each platform and added checks for overall simulation completion.
- Improved the GraphPanel and Step3Simulation components to provide real-time updates and better user feedback during simulations.
This commit is contained in:
666ghj 2025-12-12 16:13:08 +08:00
parent f8a58819fa
commit 0577ecdae8
7 changed files with 4628 additions and 928 deletions

View file

@ -1142,27 +1142,31 @@ class OasisProfileGenerator:
"""
保存Reddit Profile为JSON格式
OASIS Reddit支持两种JSON格式
1. 基础格式: user_id, user_name, name, bio, karma, created_at
2. 详细格式: realname, username, bio, persona, age, gender, mbti, country, profession, interested_topics
使用与 to_reddit_format() 一致的格式确保 OASIS 能正确读取
必须包含 user_id 字段这是 OASIS agent_graph.get_agent() 匹配的关键
我们使用详细格式与用户示例数据(36个简单人设.json)保持一致
OASIS要求所有字段都必须存在
- age: 整数
必需字段
- user_id: 用户ID整数用于匹配 initial_posts 中的 poster_agent_id
- username: 用户名
- name: 显示名称
- bio: 简介
- persona: 详细人设
- age: 年龄整数
- gender: "male", "female", "other"
- mbti: MBTI类型字符串
- country: 国家字符串
- mbti: MBTI类型
- country: 国家
"""
data = []
for profile in profiles:
# 使用详细格式(与用户示例兼容)
# 确保所有必需字段都有有效值
for idx, profile in enumerate(profiles):
# 使用与 to_reddit_format() 一致的格式
item = {
"realname": profile.name,
"user_id": profile.user_id if profile.user_id is not None else idx, # 关键:必须包含 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,
# OASIS必需字段 - 确保都有默认值
"age": profile.age if profile.age else 30,
"gender": self._normalize_gender(profile.gender),
@ -1181,7 +1185,7 @@ class OasisProfileGenerator:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"已保存 {len(profiles)} 个Reddit Profile到 {file_path} (JSON详细格式已标准化gender字段)")
logger.info(f"已保存 {len(profiles)} 个Reddit Profile到 {file_path} (JSON格式包含user_id字段)")
# 保留旧方法名作为别名,保持向后兼容
def save_profiles_to_json(

View file

@ -28,7 +28,8 @@ class SimulationStatus(str, Enum):
READY = "ready"
RUNNING = "running"
PAUSED = "paused"
COMPLETED = "completed"
STOPPED = "stopped" # 模拟被手动停止
COMPLETED = "completed" # 模拟自然完成
FAILED = "failed"

View file

@ -106,12 +106,22 @@ class SimulationRunState:
simulated_hours: int = 0
total_simulation_hours: int = 0
# 各平台独立轮次和模拟时间(用于双平台并行显示)
twitter_current_round: int = 0
reddit_current_round: int = 0
twitter_simulated_hours: int = 0
reddit_simulated_hours: int = 0
# 平台状态
twitter_running: bool = False
reddit_running: bool = False
twitter_actions_count: int = 0
reddit_actions_count: int = 0
# 平台完成状态(通过检测 actions.jsonl 中的 simulation_end 事件)
twitter_completed: bool = False
reddit_completed: bool = False
# 每轮摘要
rounds: List[RoundSummary] = field(default_factory=list)
@ -152,8 +162,15 @@ class SimulationRunState:
"simulated_hours": self.simulated_hours,
"total_simulation_hours": self.total_simulation_hours,
"progress_percent": round(self.current_round / max(self.total_rounds, 1) * 100, 1),
# 各平台独立轮次和时间
"twitter_current_round": self.twitter_current_round,
"reddit_current_round": self.reddit_current_round,
"twitter_simulated_hours": self.twitter_simulated_hours,
"reddit_simulated_hours": self.reddit_simulated_hours,
"twitter_running": self.twitter_running,
"reddit_running": self.reddit_running,
"twitter_completed": self.twitter_completed,
"reddit_completed": self.reddit_completed,
"twitter_actions_count": self.twitter_actions_count,
"reddit_actions_count": self.reddit_actions_count,
"total_actions_count": self.twitter_actions_count + self.reddit_actions_count,
@ -236,8 +253,15 @@ class SimulationRunner:
total_rounds=data.get("total_rounds", 0),
simulated_hours=data.get("simulated_hours", 0),
total_simulation_hours=data.get("total_simulation_hours", 0),
# 各平台独立轮次和时间
twitter_current_round=data.get("twitter_current_round", 0),
reddit_current_round=data.get("reddit_current_round", 0),
twitter_simulated_hours=data.get("twitter_simulated_hours", 0),
reddit_simulated_hours=data.get("reddit_simulated_hours", 0),
twitter_running=data.get("twitter_running", False),
reddit_running=data.get("reddit_running", False),
twitter_completed=data.get("twitter_completed", False),
reddit_completed=data.get("reddit_completed", False),
twitter_actions_count=data.get("twitter_actions_count", 0),
reddit_actions_count=data.get("reddit_actions_count", 0),
started_at=data.get("started_at"),
@ -575,8 +599,51 @@ class SimulationRunner:
try:
action_data = json.loads(line)
# 跳过事件类型的条目(如 simulation_start, round_start 等)
# 处理事件类型的条目
if "event_type" in action_data:
event_type = action_data.get("event_type")
# 检测 simulation_end 事件,标记平台已完成
if event_type == "simulation_end":
if platform == "twitter":
state.twitter_completed = True
state.twitter_running = False
logger.info(f"Twitter 模拟已完成: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}")
elif platform == "reddit":
state.reddit_completed = True
state.reddit_running = False
logger.info(f"Reddit 模拟已完成: {state.simulation_id}, total_rounds={action_data.get('total_rounds')}, total_actions={action_data.get('total_actions')}")
# 检查是否所有启用的平台都已完成
# 如果只运行了一个平台,只检查那个平台
# 如果运行了两个平台,需要两个都完成
all_completed = cls._check_all_platforms_completed(state)
if all_completed:
state.runner_status = RunnerStatus.COMPLETED
state.completed_at = datetime.now().isoformat()
logger.info(f"所有平台模拟已完成: {state.simulation_id}")
# 更新轮次信息(从 round_end 事件)
elif event_type == "round_end":
round_num = action_data.get("round", 0)
simulated_hours = action_data.get("simulated_hours", 0)
# 更新各平台独立的轮次和时间
if platform == "twitter":
if round_num > state.twitter_current_round:
state.twitter_current_round = round_num
state.twitter_simulated_hours = simulated_hours
elif platform == "reddit":
if round_num > state.reddit_current_round:
state.reddit_current_round = round_num
state.reddit_simulated_hours = simulated_hours
# 总体轮次取两个平台的最大值
if round_num > state.current_round:
state.current_round = round_num
# 总体时间取两个平台的最大值
state.simulated_hours = max(state.twitter_simulated_hours, state.reddit_simulated_hours)
continue
action = AgentAction(
@ -607,6 +674,33 @@ class SimulationRunner:
logger.warning(f"读取动作日志失败: {log_path}, error={e}")
return position
@classmethod
def _check_all_platforms_completed(cls, state: SimulationRunState) -> bool:
"""
检查所有启用的平台是否都已完成模拟
通过检查对应的 actions.jsonl 文件是否存在来判断平台是否被启用
Returns:
True 如果所有启用的平台都已完成
"""
sim_dir = os.path.join(cls.RUN_STATE_DIR, state.simulation_id)
twitter_log = os.path.join(sim_dir, "twitter", "actions.jsonl")
reddit_log = os.path.join(sim_dir, "reddit", "actions.jsonl")
# 检查哪些平台被启用(通过文件是否存在判断)
twitter_enabled = os.path.exists(twitter_log)
reddit_enabled = os.path.exists(reddit_log)
# 如果平台被启用但未完成,则返回 False
if twitter_enabled and not state.twitter_completed:
return False
if reddit_enabled and not state.reddit_completed:
return False
# 至少有一个平台被启用且已完成
return twitter_enabled or reddit_enabled
@classmethod
def stop_simulation(cls, simulation_id: str) -> SimulationRunState:
"""停止模拟"""

View file

@ -19,10 +19,15 @@
<div v-if="graphData" class="graph-view">
<svg ref="graphSvg" class="graph-svg"></svg>
<!-- 构建中提示 -->
<div v-if="currentPhase === 1" class="graph-building-hint">
<span class="building-dot"></span>
实时更新中...
<!-- 构建中/模拟中提示 -->
<div v-if="currentPhase === 1 || isSimulating" class="graph-building-hint">
<div class="memory-icon-wrapper">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="memory-icon">
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 4.44-4.04z" />
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-4.44-4.04z" />
</svg>
</div>
{{ isSimulating ? 'GraphRAG长短期记忆实时更新中' : '实时更新中...' }}
</div>
<!-- 节点/边详情面板 -->
@ -219,7 +224,8 @@ import * as d3 from 'd3'
const props = defineProps({
graphData: Object,
loading: Boolean,
currentPhase: Number
currentPhase: Number,
isSimulating: Boolean
})
const emit = defineEmits(['refresh', 'toggle-maximize'])
@ -1153,30 +1159,41 @@ input:checked + .slider:before {
/* Building hint */
.graph-building-hint {
position: absolute;
bottom: 80px;
bottom: 160px; /* Moved up from 80px */
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.75);
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(8px);
color: #fff;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
padding: 10px 20px;
border-radius: 30px;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
gap: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.1);
font-weight: 500;
letter-spacing: 0.5px;
z-index: 100;
}
.building-dot {
width: 8px;
height: 8px;
background: #4CAF50;
border-radius: 50%;
animation: pulse 1.5s ease-in-out infinite;
.memory-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
animation: breathe 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
.memory-icon {
width: 18px;
height: 18px;
color: #4CAF50;
}
@keyframes breathe {
0%, 100% { opacity: 0.7; transform: scale(1); filter: drop-shadow(0 0 2px rgba(76, 175, 80, 0.3)); }
50% { opacity: 1; transform: scale(1.15); filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); }
}
/* Loading spinner */

File diff suppressed because it is too large Load diff

View file

@ -41,6 +41,7 @@
:graphData="graphData"
:loading="graphLoading"
:currentPhase="3"
:isSimulating="isSimulating"
@refresh="refreshGraph"
@toggle-maximize="toggleMaximize('graph')"
/>
@ -65,7 +66,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import GraphPanel from '../components/GraphPanel.vue'
import Step3Simulation from '../components/Step3Simulation.vue'
@ -117,6 +118,8 @@ const statusText = computed(() => {
return 'Running'
})
const isSimulating = computed(() => currentStatus.value === 'processing')
// --- Helpers ---
const addLog = (msg) => {
const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) + '.' + new Date().getMilliseconds().toString().padStart(3, '0')
@ -182,12 +185,19 @@ const loadSimulationData = async () => {
}
const loadGraph = async (graphId) => {
graphLoading.value = true
// loading
// loading
if (!isSimulating.value) {
graphLoading.value = true
}
try {
const res = await getGraphData(graphId)
if (res.success) {
graphData.value = res.data
addLog('图谱数据加载成功')
if (!isSimulating.value) {
addLog('图谱数据加载成功')
}
}
} catch (err) {
addLog(`图谱加载失败: ${err.message}`)
@ -202,6 +212,32 @@ const refreshGraph = () => {
}
}
// --- Auto Refresh Logic ---
let graphRefreshTimer = null
const startGraphRefresh = () => {
if (graphRefreshTimer) return
addLog('开启图谱实时刷新 (30s)')
// 30
graphRefreshTimer = setInterval(refreshGraph, 30000)
}
const stopGraphRefresh = () => {
if (graphRefreshTimer) {
clearInterval(graphRefreshTimer)
graphRefreshTimer = null
addLog('停止图谱实时刷新')
}
}
watch(isSimulating, (newValue) => {
if (newValue) {
startGraphRefresh()
} else {
stopGraphRefresh()
}
}, { immediate: true })
onMounted(() => {
addLog('SimulationRunView 初始化')
@ -212,6 +248,10 @@ onMounted(() => {
loadSimulationData()
})
onUnmounted(() => {
stopGraphRefresh()
})
</script>
<style scoped>

File diff suppressed because it is too large Load diff