feat(simulation): clone graph on start for per-simulation isolation, use graph_id_simulation in report
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6ea83f31a4
commit
0343cad632
|
|
@ -95,14 +95,17 @@ def generate_report():
|
|||
"error": t('api.projectNotFound', id=state.project_id)
|
||||
}), 404
|
||||
|
||||
graph_id = state.graph_id or project.graph_id
|
||||
graph_id = state.graph_id or project.get("graph_id")
|
||||
if not graph_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": t('api.missingGraphIdEnsure')
|
||||
}), 400
|
||||
|
||||
simulation_requirement = project.simulation_requirement
|
||||
|
||||
# Use per-simulation graph if available (set during simulation start for isolation)
|
||||
effective_graph_id = getattr(state, 'graph_id_simulation', None) or graph_id
|
||||
|
||||
simulation_requirement = project.get("simulation_requirement")
|
||||
if not simulation_requirement:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
|
|
@ -119,11 +122,11 @@ def generate_report():
|
|||
task_type="report_generate",
|
||||
metadata={
|
||||
"simulation_id": simulation_id,
|
||||
"graph_id": graph_id,
|
||||
"graph_id": effective_graph_id,
|
||||
"report_id": report_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Capture locale before spawning background thread
|
||||
current_locale = get_locale()
|
||||
|
||||
|
|
@ -138,9 +141,9 @@ def generate_report():
|
|||
message=t('api.initReportAgent')
|
||||
)
|
||||
|
||||
# Create Report Agent
|
||||
# Create Report Agent (use per-simulation graph if available)
|
||||
agent = ReportAgent(
|
||||
graph_id=graph_id,
|
||||
graph_id=effective_graph_id,
|
||||
simulation_id=simulation_id,
|
||||
simulation_requirement=simulation_requirement
|
||||
)
|
||||
|
|
@ -595,18 +598,21 @@ def chat_with_report_agent():
|
|||
"error": t('api.projectNotFound', id=state.project_id)
|
||||
}), 404
|
||||
|
||||
graph_id = state.graph_id or project.graph_id
|
||||
graph_id = state.graph_id or project.get("graph_id")
|
||||
if not graph_id:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": t('api.missingGraphId')
|
||||
}), 400
|
||||
|
||||
simulation_requirement = project.simulation_requirement or ""
|
||||
|
||||
|
||||
# Use per-simulation graph if available
|
||||
effective_graph_id = getattr(state, 'graph_id_simulation', None) or graph_id
|
||||
|
||||
simulation_requirement = project.get("simulation_requirement") or ""
|
||||
|
||||
# Create agent and start chat
|
||||
agent = ReportAgent(
|
||||
graph_id=graph_id,
|
||||
graph_id=effective_graph_id,
|
||||
simulation_id=simulation_id,
|
||||
simulation_requirement=simulation_requirement
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1591,6 +1591,33 @@ def start_simulation():
|
|||
|
||||
logger.info(f"Graph memory update enabled: simulation_id={simulation_id}, graph_id={graph_id}")
|
||||
|
||||
# Clone graph for per-simulation isolation
|
||||
graph_id_simulation = None
|
||||
if enable_graph_memory_update and graph_id:
|
||||
try:
|
||||
graph_id_sim = f"mirofish_{simulation_id}_sim"
|
||||
from ..graph import get_graph_backend
|
||||
graph_backend = get_graph_backend()
|
||||
if hasattr(graph_backend, 'clone_graph'):
|
||||
import asyncio as _asyncio
|
||||
import concurrent.futures as _futures
|
||||
try:
|
||||
loop = _asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
with _futures.ThreadPoolExecutor() as pool:
|
||||
future = pool.submit(_asyncio.run, graph_backend.clone_graph(graph_id, graph_id_sim))
|
||||
future.result()
|
||||
else:
|
||||
loop.run_until_complete(graph_backend.clone_graph(graph_id, graph_id_sim))
|
||||
except RuntimeError:
|
||||
_asyncio.run(graph_backend.clone_graph(graph_id, graph_id_sim))
|
||||
state.graph_id_simulation = graph_id_sim
|
||||
manager._save_simulation_state(state)
|
||||
graph_id_simulation = graph_id_sim
|
||||
logger.info(f"Graph cloned for simulation isolation: {graph_id_sim}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Graph cloning failed, simulation uses shared graph: {e}")
|
||||
|
||||
# Start simulation
|
||||
run_state = SimulationRunner.start_simulation(
|
||||
simulation_id=simulation_id,
|
||||
|
|
@ -1611,6 +1638,8 @@ def start_simulation():
|
|||
response_data['force_restarted'] = force_restarted
|
||||
if enable_graph_memory_update:
|
||||
response_data['graph_id'] = graph_id
|
||||
if graph_id_simulation:
|
||||
response_data['graph_id_simulation'] = graph_id_simulation
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
|
|
|
|||
|
|
@ -94,3 +94,50 @@ def test_clone_source_not_found_returns_404(app_client):
|
|||
client, tmp_path = app_client
|
||||
resp = client.post("/api/simulation/nonexistent_sim/clone", json={"project_id": "proj_x"})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_start_simulation_clones_graph_when_enabled(app_client, monkeypatch):
|
||||
"""When enable_graph_memory_update=true and graph backend supports clone_graph, it should be called."""
|
||||
client, tmp_path = app_client
|
||||
|
||||
# Create a sim in 'ready' status with all required files
|
||||
import json
|
||||
sim_id = "sim_ready001"
|
||||
sim_dir = tmp_path / sim_id
|
||||
sim_dir.mkdir()
|
||||
state = {
|
||||
"simulation_id": sim_id, "project_id": "proj_r1", "graph_id": "g_original",
|
||||
"status": "ready", "entities_count": 2, "profiles_count": 2,
|
||||
"entity_types": [], "config_generated": True,
|
||||
"parent_simulation_id": None, "graph_id_simulation": None,
|
||||
"enable_twitter": False, "enable_reddit": True,
|
||||
}
|
||||
(sim_dir / "state.json").write_text(json.dumps(state))
|
||||
config = {"time_config": {"total_hours": 24}, "reddit_config": {}}
|
||||
(sim_dir / "simulation_config.json").write_text(json.dumps(config))
|
||||
profiles = [{"user_id": 0, "name": "Alice"}]
|
||||
(sim_dir / "reddit_profiles.json").write_text(json.dumps(profiles))
|
||||
(sim_dir / "twitter_profiles.csv").write_text("user_id,name\n0,Alice\n")
|
||||
|
||||
# Mock graph backend
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
mock_backend = MagicMock()
|
||||
mock_backend.clone_graph = AsyncMock(return_value=None)
|
||||
|
||||
import backend.app.graph as graph_module
|
||||
monkeypatch.setattr(graph_module, "get_graph_backend", lambda: mock_backend)
|
||||
|
||||
# Mock SimulationRunner.start_simulation
|
||||
import backend.app.services.simulation_runner as runner_module
|
||||
fake_run_state = MagicMock()
|
||||
fake_run_state.to_dict.return_value = {"runner_status": "running"}
|
||||
monkeypatch.setattr(runner_module.SimulationRunner, "start_simulation",
|
||||
staticmethod(lambda **kw: fake_run_state))
|
||||
|
||||
resp = client.post("/api/simulation/start", json={
|
||||
"simulation_id": sim_id,
|
||||
"enable_graph_memory_update": True,
|
||||
})
|
||||
# Test that clone_graph was called (or at minimum the request didn't 500)
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()["success"] is True
|
||||
|
|
|
|||
Loading…
Reference in New Issue