Preserve completed simulation replay flow
This commit is contained in:
parent
e1467b53e2
commit
57263d5337
|
|
@ -32,6 +32,21 @@ def allowed_file(filename: str) -> bool:
|
|||
return ext in Config.ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
def _derive_project_name(raw_name: str, uploaded_files) -> str:
|
||||
"""Use a readable project name when the client did not provide one."""
|
||||
name = (raw_name or "").strip()
|
||||
if name and name.lower() not in {"unnamed project", "untitled", "new project"}:
|
||||
return name
|
||||
|
||||
filenames = [getattr(f, "filename", "") for f in (uploaded_files or []) if getattr(f, "filename", "")]
|
||||
if filenames:
|
||||
stem = os.path.splitext(os.path.basename(filenames[0]))[0].strip()
|
||||
if stem:
|
||||
return stem
|
||||
|
||||
return "Foresight 演示项目"
|
||||
|
||||
|
||||
def _prepare_graph_for_visualization(graph_data: dict) -> dict:
|
||||
"""Keep graph visualization dense and readable by showing the top connected nodes."""
|
||||
if not isinstance(graph_data, dict):
|
||||
|
|
@ -205,24 +220,26 @@ def generate_ontology():
|
|||
|
||||
# 获取参数
|
||||
simulation_requirement = request.form.get('simulation_requirement', '')
|
||||
project_name = request.form.get('project_name', 'Unnamed Project')
|
||||
raw_project_name = request.form.get('project_name', 'Unnamed Project')
|
||||
additional_context = request.form.get('additional_context', '')
|
||||
|
||||
# token 追踪:标记当前 stage
|
||||
from ..utils import token_tracker
|
||||
token_tracker.set_stage("step1_ontology")
|
||||
|
||||
# 获取上传的文件
|
||||
uploaded_files = request.files.getlist('files')
|
||||
project_name = _derive_project_name(raw_project_name, uploaded_files)
|
||||
|
||||
logger.debug(f"项目名称: {project_name}")
|
||||
logger.debug(f"模拟需求: {simulation_requirement[:100]}...")
|
||||
|
||||
|
||||
if not simulation_requirement:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": t('api.requireSimulationRequirement')
|
||||
}), 400
|
||||
|
||||
# 获取上传的文件
|
||||
uploaded_files = request.files.getlist('files')
|
||||
|
||||
if not uploaded_files or all(not f.filename for f in uploaded_files):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,37 @@ logger = get_logger('foresight.api.simulation')
|
|||
INTERVIEW_PROMPT_PREFIX = "结合你的人设、所有的过往记忆与行动,不调用任何工具直接用文本回复我:"
|
||||
|
||||
|
||||
def _runner_status_value(run_state) -> str:
|
||||
if not run_state:
|
||||
return ""
|
||||
status = getattr(run_state, "runner_status", "")
|
||||
return status.value if hasattr(status, "value") else str(status)
|
||||
|
||||
|
||||
def _has_completed_run(run_state) -> bool:
|
||||
if not run_state:
|
||||
return False
|
||||
status = _runner_status_value(run_state)
|
||||
total_rounds = int(getattr(run_state, "total_rounds", 0) or 0)
|
||||
current_round = int(getattr(run_state, "current_round", 0) or 0)
|
||||
return status in {"completed", "stopped"} or (total_rounds > 0 and current_round >= total_rounds)
|
||||
|
||||
|
||||
def _display_project_name(project) -> str:
|
||||
if not project:
|
||||
return "未命名项目"
|
||||
name = (getattr(project, "name", "") or "").strip()
|
||||
if name and name.lower() not in {"unnamed project", "untitled", "new project"}:
|
||||
return name
|
||||
files = getattr(project, "files", []) or []
|
||||
if files:
|
||||
filename = files[0].get("filename") or files[0].get("original_filename") or ""
|
||||
stem = os.path.splitext(os.path.basename(filename))[0].strip()
|
||||
if stem:
|
||||
return stem
|
||||
return "未命名项目"
|
||||
|
||||
|
||||
def optimize_interview_prompt(prompt: str) -> str:
|
||||
"""
|
||||
优化Interview提问,添加前缀避免Agent调用工具
|
||||
|
|
@ -1063,9 +1094,13 @@ def get_simulation_history():
|
|||
run_state = SimulationRunner.get_run_state(sim.simulation_id)
|
||||
if run_state:
|
||||
sim_dict["current_round"] = run_state.current_round
|
||||
sim_dict["runner_status"] = run_state.runner_status.value
|
||||
sim_dict["runner_status"] = _runner_status_value(run_state)
|
||||
# 使用用户设置的 total_rounds,若无则使用推荐轮数
|
||||
sim_dict["total_rounds"] = run_state.total_rounds if run_state.total_rounds > 0 else recommended_rounds
|
||||
if _has_completed_run(run_state):
|
||||
sim_dict["status"] = "completed"
|
||||
sim_dict["current_round"] = sim_dict["total_rounds"]
|
||||
sim_dict["has_replay"] = True
|
||||
else:
|
||||
sim_dict["current_round"] = 0
|
||||
sim_dict["runner_status"] = "idle"
|
||||
|
|
@ -1074,11 +1109,13 @@ def get_simulation_history():
|
|||
# 获取关联项目的文件列表(最多3个)
|
||||
project = ProjectManager.get_project(sim.project_id)
|
||||
if project and hasattr(project, 'files') and project.files:
|
||||
sim_dict["project_name"] = _display_project_name(project)
|
||||
sim_dict["files"] = [
|
||||
{"filename": f.get("filename", "未知文件")}
|
||||
for f in project.files[:3]
|
||||
]
|
||||
else:
|
||||
sim_dict["project_name"] = sim_dict.get("project_name") or "未命名项目"
|
||||
sim_dict["files"] = []
|
||||
|
||||
# 获取关联的 report_id(查找该 simulation 最新的 report)
|
||||
|
|
@ -2279,7 +2316,7 @@ def get_simulation_replay(simulation_id: str):
|
|||
if project:
|
||||
project_info = {
|
||||
"project_id": project.project_id,
|
||||
"name": project.name,
|
||||
"name": _display_project_name(project),
|
||||
"status": project.status.value if hasattr(project.status, 'value') else str(project.status),
|
||||
"graph_id": project.graph_id,
|
||||
"simulation_requirement": project.simulation_requirement,
|
||||
|
|
@ -2421,6 +2458,9 @@ def get_simulation_replay(simulation_id: str):
|
|||
|
||||
# ---------- 6. Workflow 时间线 ----------
|
||||
status_str = state.status.value if hasattr(state.status, 'value') else str(state.status)
|
||||
run_state = SimulationRunner.get_run_state(simulation_id)
|
||||
if _has_completed_run(run_state):
|
||||
status_str = "completed"
|
||||
workflow = [
|
||||
{
|
||||
"step": 1,
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
startSimulation,
|
||||
|
|
@ -314,6 +314,7 @@ const props = defineProps({
|
|||
const emit = defineEmits(['go-back', 'next-step', 'add-log', 'update-status'])
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// State
|
||||
const isGeneratingReport = ref(false)
|
||||
|
|
@ -325,6 +326,7 @@ const runStatus = ref({})
|
|||
const allActions = ref([]) // 所有动作(增量累积)
|
||||
const actionIds = ref(new Set()) // 用于去重的动作ID集合
|
||||
const scrollContainer = ref(null)
|
||||
const reviewMode = ref(false)
|
||||
|
||||
// Computed
|
||||
// 按时间顺序显示动作(最新的在最后面,即底部)
|
||||
|
|
@ -368,6 +370,7 @@ const addLog = (msg) => {
|
|||
// 重置所有状态(用于重新启动模拟)
|
||||
const resetAllState = () => {
|
||||
phase.value = 0
|
||||
reviewMode.value = false
|
||||
runStatus.value = {}
|
||||
allActions.value = []
|
||||
actionIds.value = new Set()
|
||||
|
|
@ -379,6 +382,50 @@ const resetAllState = () => {
|
|||
stopPolling() // 停止之前可能存在的轮询
|
||||
}
|
||||
|
||||
const hasCompletedRun = (data) => {
|
||||
if (!data) return false
|
||||
const totalRounds = Number(data.total_rounds || 0)
|
||||
const currentRound = Number(data.current_round || 0)
|
||||
return data.runner_status === 'completed' ||
|
||||
data.runner_status === 'stopped' ||
|
||||
(totalRounds > 0 && currentRound >= totalRounds)
|
||||
}
|
||||
|
||||
const loadExistingRunForReview = async (statusData = null) => {
|
||||
reviewMode.value = true
|
||||
phase.value = 2
|
||||
emit('update-status', 'completed')
|
||||
|
||||
if (statusData) {
|
||||
runStatus.value = statusData
|
||||
}
|
||||
|
||||
await fetchRunStatusDetail()
|
||||
await fetchRunStatus()
|
||||
addLog(`Replay mode: using completed dual-world result (${runStatus.value.total_rounds || props.maxRounds || '-'} rounds · ${runStatus.value.total_actions_count || allActions.value.length} actions).`)
|
||||
}
|
||||
|
||||
const startOrReviewSimulation = async () => {
|
||||
if (!props.simulationId) {
|
||||
addLog(t('log.errorMissingSimId'))
|
||||
return
|
||||
}
|
||||
|
||||
if (route.query.rerun !== '1') {
|
||||
try {
|
||||
const statusRes = await getRunStatus(props.simulationId)
|
||||
if (statusRes.success && hasCompletedRun(statusRes.data)) {
|
||||
await loadExistingRunForReview(statusRes.data)
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`Completed replay check failed, falling back to live start: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
await doStartSimulation()
|
||||
}
|
||||
|
||||
// 启动模拟
|
||||
const doStartSimulation = async () => {
|
||||
if (!props.simulationId) {
|
||||
|
|
@ -533,9 +580,10 @@ const fetchRunStatus = async () => {
|
|||
|
||||
if (isCompleted || platformsCompleted) {
|
||||
if (platformsCompleted && !isCompleted) {
|
||||
addLog(t('log.allPlatformsCompleted'))
|
||||
addLog(t('log.allPlatformsCompleted'))
|
||||
}
|
||||
addLog(t('log.simCompleted'))
|
||||
reviewMode.value = true
|
||||
phase.value = 2
|
||||
stopPolling()
|
||||
emit('update-status', 'completed')
|
||||
|
|
@ -710,7 +758,7 @@ watch(() => props.systemLogs?.length, () => {
|
|||
onMounted(() => {
|
||||
addLog(t('log.step3Init'))
|
||||
if (props.simulationId) {
|
||||
doStartSimulation()
|
||||
startOrReviewSimulation()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue