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)
|
"error": t('api.projectNotFound', id=state.project_id)
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
graph_id = state.graph_id or project.graph_id
|
graph_id = state.graph_id or project.get("graph_id")
|
||||||
if not graph_id:
|
if not graph_id:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": t('api.missingGraphIdEnsure')
|
"error": t('api.missingGraphIdEnsure')
|
||||||
}), 400
|
}), 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:
|
if not simulation_requirement:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
|
|
@ -119,11 +122,11 @@ def generate_report():
|
||||||
task_type="report_generate",
|
task_type="report_generate",
|
||||||
metadata={
|
metadata={
|
||||||
"simulation_id": simulation_id,
|
"simulation_id": simulation_id,
|
||||||
"graph_id": graph_id,
|
"graph_id": effective_graph_id,
|
||||||
"report_id": report_id
|
"report_id": report_id
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Capture locale before spawning background thread
|
# Capture locale before spawning background thread
|
||||||
current_locale = get_locale()
|
current_locale = get_locale()
|
||||||
|
|
||||||
|
|
@ -138,9 +141,9 @@ def generate_report():
|
||||||
message=t('api.initReportAgent')
|
message=t('api.initReportAgent')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create Report Agent
|
# Create Report Agent (use per-simulation graph if available)
|
||||||
agent = ReportAgent(
|
agent = ReportAgent(
|
||||||
graph_id=graph_id,
|
graph_id=effective_graph_id,
|
||||||
simulation_id=simulation_id,
|
simulation_id=simulation_id,
|
||||||
simulation_requirement=simulation_requirement
|
simulation_requirement=simulation_requirement
|
||||||
)
|
)
|
||||||
|
|
@ -595,18 +598,21 @@ def chat_with_report_agent():
|
||||||
"error": t('api.projectNotFound', id=state.project_id)
|
"error": t('api.projectNotFound', id=state.project_id)
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
graph_id = state.graph_id or project.graph_id
|
graph_id = state.graph_id or project.get("graph_id")
|
||||||
if not graph_id:
|
if not graph_id:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": t('api.missingGraphId')
|
"error": t('api.missingGraphId')
|
||||||
}), 400
|
}), 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
|
# Create agent and start chat
|
||||||
agent = ReportAgent(
|
agent = ReportAgent(
|
||||||
graph_id=graph_id,
|
graph_id=effective_graph_id,
|
||||||
simulation_id=simulation_id,
|
simulation_id=simulation_id,
|
||||||
simulation_requirement=simulation_requirement
|
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}")
|
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
|
# Start simulation
|
||||||
run_state = SimulationRunner.start_simulation(
|
run_state = SimulationRunner.start_simulation(
|
||||||
simulation_id=simulation_id,
|
simulation_id=simulation_id,
|
||||||
|
|
@ -1611,6 +1638,8 @@ def start_simulation():
|
||||||
response_data['force_restarted'] = force_restarted
|
response_data['force_restarted'] = force_restarted
|
||||||
if enable_graph_memory_update:
|
if enable_graph_memory_update:
|
||||||
response_data['graph_id'] = graph_id
|
response_data['graph_id'] = graph_id
|
||||||
|
if graph_id_simulation:
|
||||||
|
response_data['graph_id_simulation'] = graph_id_simulation
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|
|
||||||
|
|
@ -94,3 +94,50 @@ def test_clone_source_not_found_returns_404(app_client):
|
||||||
client, tmp_path = app_client
|
client, tmp_path = app_client
|
||||||
resp = client.post("/api/simulation/nonexistent_sim/clone", json={"project_id": "proj_x"})
|
resp = client.post("/api/simulation/nonexistent_sim/clone", json={"project_id": "proj_x"})
|
||||||
assert resp.status_code == 404
|
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