fix: stop Step2 polling after preparation failure
This commit is contained in:
parent
7970d74a86
commit
05bea4a330
|
|
@ -612,12 +612,17 @@ def prepare_simulation():
|
|||
progress_callback=progress_callback,
|
||||
parallel_profile_count=parallel_profile_count
|
||||
)
|
||||
|
||||
# 任务完成
|
||||
task_manager.complete_task(
|
||||
task_id,
|
||||
result=result_state.to_simple_dict()
|
||||
)
|
||||
|
||||
if result_state.status == SimulationStatus.FAILED:
|
||||
task_manager.fail_task(
|
||||
task_id,
|
||||
result_state.error or "模拟准备失败"
|
||||
)
|
||||
else:
|
||||
task_manager.complete_task(
|
||||
task_id,
|
||||
result=result_state.to_simple_dict()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"准备模拟失败: {str(e)}")
|
||||
|
|
@ -1123,6 +1128,8 @@ def get_simulation_profiles_realtime(simulation_id: str):
|
|||
# 检查是否正在生成(通过 state.json 判断)
|
||||
is_generating = False
|
||||
total_expected = None
|
||||
status = None
|
||||
error = None
|
||||
|
||||
state_file = os.path.join(sim_dir, "state.json")
|
||||
if os.path.exists(state_file):
|
||||
|
|
@ -1132,6 +1139,7 @@ def get_simulation_profiles_realtime(simulation_id: str):
|
|||
status = state_data.get("status", "")
|
||||
is_generating = status == "preparing"
|
||||
total_expected = state_data.get("entities_count")
|
||||
error = state_data.get("error")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -1143,6 +1151,8 @@ def get_simulation_profiles_realtime(simulation_id: str):
|
|||
"count": len(profiles),
|
||||
"total_expected": total_expected,
|
||||
"is_generating": is_generating,
|
||||
"status": status,
|
||||
"error": error,
|
||||
"file_exists": file_exists,
|
||||
"file_modified_at": file_modified_at,
|
||||
"profiles": profiles
|
||||
|
|
@ -1218,6 +1228,9 @@ def get_simulation_config_realtime(simulation_id: str):
|
|||
# 检查是否正在生成(通过 state.json 判断)
|
||||
is_generating = False
|
||||
generation_stage = None
|
||||
status = None
|
||||
error = None
|
||||
profiles_generated = False
|
||||
config_generated = False
|
||||
|
||||
state_file = os.path.join(sim_dir, "state.json")
|
||||
|
|
@ -1226,17 +1239,21 @@ def get_simulation_config_realtime(simulation_id: str):
|
|||
with open(state_file, 'r', encoding='utf-8') as f:
|
||||
state_data = json.load(f)
|
||||
status = state_data.get("status", "")
|
||||
error = state_data.get("error")
|
||||
is_generating = status == "preparing"
|
||||
profiles_generated = state_data.get("profiles_generated", False)
|
||||
config_generated = state_data.get("config_generated", False)
|
||||
|
||||
# 判断当前阶段
|
||||
if is_generating:
|
||||
if state_data.get("profiles_generated", False):
|
||||
if profiles_generated:
|
||||
generation_stage = "generating_config"
|
||||
else:
|
||||
generation_stage = "generating_profiles"
|
||||
elif status == "ready":
|
||||
generation_stage = "completed"
|
||||
elif status == "failed":
|
||||
generation_stage = "failed"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -1246,7 +1263,10 @@ def get_simulation_config_realtime(simulation_id: str):
|
|||
"file_exists": file_exists,
|
||||
"file_modified_at": file_modified_at,
|
||||
"is_generating": is_generating,
|
||||
"status": status,
|
||||
"error": error,
|
||||
"generation_stage": generation_stage,
|
||||
"profiles_generated": profiles_generated,
|
||||
"config_generated": config_generated,
|
||||
"config": config
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class SimulationState:
|
|||
entity_types: List[str] = field(default_factory=list)
|
||||
|
||||
# 配置生成信息
|
||||
profiles_generated: bool = False
|
||||
config_generated: bool = False
|
||||
config_reasoning: str = ""
|
||||
|
||||
|
|
@ -87,6 +88,7 @@ class SimulationState:
|
|||
"entities_count": self.entities_count,
|
||||
"profiles_count": self.profiles_count,
|
||||
"entity_types": self.entity_types,
|
||||
"profiles_generated": self.profiles_generated,
|
||||
"config_generated": self.config_generated,
|
||||
"config_reasoning": self.config_reasoning,
|
||||
"current_round": self.current_round,
|
||||
|
|
@ -116,6 +118,7 @@ class SimulationState:
|
|||
"entities_count": self.entities_count,
|
||||
"profiles_count": self.profiles_count,
|
||||
"entity_types": self.entity_types,
|
||||
"profiles_generated": self.profiles_generated,
|
||||
"config_generated": self.config_generated,
|
||||
"error": self.error,
|
||||
}
|
||||
|
|
@ -187,6 +190,7 @@ class SimulationManager:
|
|||
entities_count=data.get("entities_count", 0),
|
||||
profiles_count=data.get("profiles_count", 0),
|
||||
entity_types=data.get("entity_types", []),
|
||||
profiles_generated=data.get("profiles_generated", False),
|
||||
config_generated=data.get("config_generated", False),
|
||||
config_reasoning=data.get("config_reasoning", ""),
|
||||
current_round=data.get("current_round", 0),
|
||||
|
|
@ -274,6 +278,10 @@ class SimulationManager:
|
|||
|
||||
try:
|
||||
state.status = SimulationStatus.PREPARING
|
||||
state.error = None
|
||||
state.profiles_generated = False
|
||||
state.config_generated = False
|
||||
state.config_reasoning = ""
|
||||
self._save_simulation_state(state)
|
||||
|
||||
sim_dir = self._get_simulation_dir(simulation_id)
|
||||
|
|
@ -308,7 +316,7 @@ class SimulationManager:
|
|||
state.status = SimulationStatus.FAILED
|
||||
state.error = "没有找到符合条件的实体,请检查图谱是否正确构建"
|
||||
self._save_simulation_state(state)
|
||||
return state
|
||||
raise ValueError(state.error)
|
||||
|
||||
# ========== 阶段2: 生成Agent Profile ==========
|
||||
total_entities = len(filtered.entities)
|
||||
|
|
@ -356,6 +364,8 @@ class SimulationManager:
|
|||
)
|
||||
|
||||
state.profiles_count = len(profiles)
|
||||
state.profiles_generated = len(profiles) > 0
|
||||
self._save_simulation_state(state)
|
||||
|
||||
# 保存Profile文件(注意:Twitter使用CSV格式,Reddit使用JSON格式)
|
||||
# Reddit 已经在生成过程中实时保存了,这里再保存一次确保完整性
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from app import create_app
|
||||
from app.config import Config
|
||||
from app.services import simulation_manager as simulation_manager_module
|
||||
from app.services.simulation_manager import (
|
||||
SimulationManager,
|
||||
SimulationState,
|
||||
SimulationStatus,
|
||||
)
|
||||
from app.services.zep_entity_reader import FilteredEntities
|
||||
|
||||
|
||||
def _write_failed_state(root, simulation_id="sim_failed"):
|
||||
sim_dir = root / simulation_id
|
||||
sim_dir.mkdir(parents=True)
|
||||
(sim_dir / "state.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "failed",
|
||||
"error": "no usable entities",
|
||||
"entities_count": 0,
|
||||
"profiles_generated": False,
|
||||
"config_generated": False,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return simulation_id
|
||||
|
||||
|
||||
def test_realtime_endpoints_expose_terminal_failure(tmp_path, monkeypatch):
|
||||
simulation_id = _write_failed_state(tmp_path)
|
||||
monkeypatch.setattr(Config, "OASIS_SIMULATION_DATA_DIR", str(tmp_path))
|
||||
|
||||
app = create_app()
|
||||
app.config.update(TESTING=True)
|
||||
client = app.test_client()
|
||||
|
||||
config_response = client.get(f"/api/simulation/{simulation_id}/config/realtime")
|
||||
profile_response = client.get(f"/api/simulation/{simulation_id}/profiles/realtime")
|
||||
|
||||
assert config_response.status_code == 200
|
||||
expected_config_state = {
|
||||
"status": "failed",
|
||||
"error": "no usable entities",
|
||||
"generation_stage": "failed",
|
||||
"profiles_generated": False,
|
||||
"config_generated": False,
|
||||
"is_generating": False,
|
||||
}
|
||||
for key, expected in expected_config_state.items():
|
||||
assert config_response.json["data"][key] == expected
|
||||
assert profile_response.status_code == 200
|
||||
assert profile_response.json["data"]["status"] == "failed"
|
||||
assert profile_response.json["data"]["error"] == "no usable entities"
|
||||
assert profile_response.json["data"]["is_generating"] is False
|
||||
|
||||
|
||||
def test_zero_entities_persists_failed_state_and_raises(tmp_path, monkeypatch):
|
||||
class EmptyReader:
|
||||
def filter_defined_entities(self, **kwargs):
|
||||
return FilteredEntities(
|
||||
entities=[],
|
||||
entity_types=set(),
|
||||
total_count=3,
|
||||
filtered_count=0,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(SimulationManager, "SIMULATION_DATA_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(simulation_manager_module, "ZepEntityReader", EmptyReader)
|
||||
|
||||
manager = SimulationManager()
|
||||
state = SimulationState(
|
||||
simulation_id="sim_empty",
|
||||
project_id="project",
|
||||
graph_id="graph",
|
||||
status=SimulationStatus.READY,
|
||||
profiles_generated=True,
|
||||
config_generated=True,
|
||||
config_reasoning="stale",
|
||||
error="stale",
|
||||
)
|
||||
manager._save_simulation_state(state)
|
||||
|
||||
with pytest.raises(ValueError, match="没有找到符合条件的实体"):
|
||||
manager.prepare_simulation(
|
||||
simulation_id=state.simulation_id,
|
||||
simulation_requirement="requirement",
|
||||
document_text="document",
|
||||
)
|
||||
|
||||
persisted = json.loads(
|
||||
(tmp_path / state.simulation_id / "state.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert persisted["status"] == "failed"
|
||||
assert persisted["profiles_generated"] is False
|
||||
assert persisted["config_generated"] is False
|
||||
assert persisted["config_reasoning"] == ""
|
||||
assert "没有找到符合条件的实体" in persisted["error"]
|
||||
|
|
@ -740,6 +740,14 @@ const addLog = (msg) => {
|
|||
emit('add-log', msg)
|
||||
}
|
||||
|
||||
const handlePrepareFailure = (message) => {
|
||||
stopPolling()
|
||||
stopProfilesPolling()
|
||||
stopConfigPolling()
|
||||
addLog(t('log.prepareFailedWithError', { error: message || t('common.unknownError') }))
|
||||
emit('update-status', 'error')
|
||||
}
|
||||
|
||||
// 处理开始模拟按钮点击
|
||||
const handleStartSimulation = () => {
|
||||
// 构建传递给父组件的参数
|
||||
|
|
@ -898,9 +906,7 @@ const pollPrepareStatus = async () => {
|
|||
stopProfilesPolling()
|
||||
await loadPreparedData()
|
||||
} else if (data.status === 'failed') {
|
||||
addLog(t('log.prepareFailedWithError', { error: data.error || t('common.unknownError') }))
|
||||
stopPolling()
|
||||
stopProfilesPolling()
|
||||
handlePrepareFailure(data.error)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -972,6 +978,11 @@ const fetchConfigRealtime = async () => {
|
|||
|
||||
if (res.success && res.data) {
|
||||
const data = res.data
|
||||
|
||||
if (data.status === 'failed' || data.error) {
|
||||
handlePrepareFailure(data.error)
|
||||
return
|
||||
}
|
||||
|
||||
// 输出配置生成阶段日志(避免重复)
|
||||
if (data.generation_stage && data.generation_stage !== lastLoggedConfigStage) {
|
||||
|
|
@ -1032,29 +1043,36 @@ const loadPreparedData = async () => {
|
|||
try {
|
||||
const res = await getSimulationConfigRealtime(props.simulationId)
|
||||
if (res.success && res.data) {
|
||||
if (res.data.config_generated && res.data.config) {
|
||||
simulationConfig.value = res.data.config
|
||||
const configState = res.data
|
||||
|
||||
if (configState.status === 'failed' || configState.error) {
|
||||
handlePrepareFailure(configState.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (configState.config_generated && configState.config) {
|
||||
simulationConfig.value = configState.config
|
||||
addLog(t('log.configLoadSuccess'))
|
||||
|
||||
// 显示详细配置摘要
|
||||
if (res.data.summary) {
|
||||
addLog(t('log.configSummaryAgents', { count: res.data.summary.total_agents }))
|
||||
addLog(t('log.configSummaryHours', { hours: res.data.summary.simulation_hours }))
|
||||
addLog(t('log.configSummaryPostsAlt', { count: res.data.summary.initial_posts_count }))
|
||||
if (configState.summary) {
|
||||
addLog(t('log.configSummaryAgents', { count: configState.summary.total_agents }))
|
||||
addLog(t('log.configSummaryHours', { hours: configState.summary.simulation_hours }))
|
||||
addLog(t('log.configSummaryPostsAlt', { count: configState.summary.initial_posts_count }))
|
||||
}
|
||||
|
||||
addLog(t('log.envSetupComplete'))
|
||||
phase.value = 4
|
||||
emit('update-status', 'completed')
|
||||
} else {
|
||||
// 配置尚未生成,开始轮询
|
||||
} else if (configState.is_generating) {
|
||||
addLog(t('log.configGenerating'))
|
||||
startConfigPolling()
|
||||
} else {
|
||||
handlePrepareFailure(t('log.configNotGenerating'))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(t('log.loadConfigFailed', { error: err.message }))
|
||||
emit('update-status', 'error')
|
||||
handlePrepareFailure(t('log.loadConfigFailed', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -521,6 +521,7 @@
|
|||
"configLoadSuccess": "✓ Simulation config loaded",
|
||||
"configSummaryPostsAlt": " └─ Initial posts: {count}",
|
||||
"configGenerating": "Config generating, polling...",
|
||||
"configNotGenerating": "Simulation config is missing and the backend is no longer generating it",
|
||||
"loadConfigFailed": "Failed to load config: {error}",
|
||||
"step2Init": "Step 2 environment setup initialized",
|
||||
"step3Init": "Step 3 simulation run initialized",
|
||||
|
|
|
|||
|
|
@ -521,6 +521,7 @@
|
|||
"configLoadSuccess": "✓ 模拟配置加载成功",
|
||||
"configSummaryPostsAlt": " └─ 初始帖子: {count}条",
|
||||
"configGenerating": "配置生成中,开始轮询等待...",
|
||||
"configNotGenerating": "模拟配置尚未生成,且后端已不再生成配置",
|
||||
"loadConfigFailed": "加载配置失败: {error}",
|
||||
"step2Init": "Step2 环境搭建初始化",
|
||||
"step3Init": "Step3 模拟运行初始化",
|
||||
|
|
|
|||
Loading…
Reference in New Issue