diff --git a/backend/app/api/report.py b/backend/app/api/report.py index deba8420..397eab3c 100644 --- a/backend/app/api/report.py +++ b/backend/app/api/report.py @@ -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 ) diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 145db5fc..db44c397 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -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, diff --git a/backend/tests/test_simulation_clone.py b/backend/tests/test_simulation_clone.py index c427260e..c3100b0d 100644 --- a/backend/tests/test_simulation_clone.py +++ b/backend/tests/test_simulation_clone.py @@ -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