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:
parent
f8a58819fa
commit
0577ecdae8
7 changed files with 4628 additions and 928 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ class SimulationStatus(str, Enum):
|
|||
READY = "ready"
|
||||
RUNNING = "running"
|
||||
PAUSED = "paused"
|
||||
COMPLETED = "completed"
|
||||
STOPPED = "stopped" # 模拟被手动停止
|
||||
COMPLETED = "completed" # 模拟自然完成
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""停止模拟"""
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
|||
3832
模拟详情接口返回示例.json
Normal file
3832
模拟详情接口返回示例.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue