diff --git a/python/valuecell/core/agent/connect.py b/python/valuecell/core/agent/connect.py index 09d7ee5f3..54f6d549a 100644 --- a/python/valuecell/core/agent/connect.py +++ b/python/valuecell/core/agent/connect.py @@ -286,9 +286,13 @@ def _ensure_remote_contexts_loaded(self) -> None: if not self._remote_contexts_loaded: self._load_remote_contexts() - def preload_local_agent_classes(self) -> None: + def preload_local_agent_classes(self, names: list[str] | None = None) -> None: """Preload all local agent classes synchronously at startup. + If `names` is provided (a list of agent names), only agents whose + names appear in the list will be considered for preload; others are + skipped. This preserves the original behavior when `names` is None. + This method should be called during application startup (before the event loop processes requests) to avoid import deadlocks on Windows. Importing Python modules in a worker thread while the main thread holds @@ -298,6 +302,12 @@ def preload_local_agent_classes(self) -> None: self._ensure_remote_contexts_loaded() preloaded_count = 0 for name, ctx in self._contexts.items(): + # If caller passed a filter list, skip contexts not in that list + if names is not None and name not in names: + logger.debug( + "Skipping preload for '{}': not in provided names list", name + ) + continue if not ctx.agent_class_spec: logger.debug("Skipping preload for '{}': no agent_class_spec", name) continue diff --git a/python/valuecell/core/agent/tests/test_connect.py b/python/valuecell/core/agent/tests/test_connect.py index 989846507..68b271563 100644 --- a/python/valuecell/core/agent/tests/test_connect.py +++ b/python/valuecell/core/agent/tests/test_connect.py @@ -240,6 +240,115 @@ def test_preload_handles_failed_import(tmp_path: Path, monkeypatch: pytest.Monke assert ctx.agent_instance_class is None +def test_preload_with_names_loads_only_specified(tmp_path: Path): + """When a `names` filter is provided, only those agents are preloaded.""" + dir_path = tmp_path / "agent_cards" + dir_path.mkdir(parents=True) + + card1 = make_card_dict( + "AgentOne", "http://127.0.0.1:9001", push_notifications=False + ) + card1["metadata"] = { + "local_agent_class": "valuecell.agents.prompt_strategy_agent.core:PromptBasedStrategyAgent" + } + card2 = make_card_dict( + "AgentTwo", "http://127.0.0.1:9002", push_notifications=False + ) + card2["metadata"] = { + "local_agent_class": "valuecell.agents.prompt_strategy_agent.core:PromptBasedStrategyAgent" + } + + with open(dir_path / "AgentOne.json", "w", encoding="utf-8") as f: + json.dump(card1, f) + with open(dir_path / "AgentTwo.json", "w", encoding="utf-8") as f: + json.dump(card2, f) + + rc = RemoteConnections() + rc.load_from_dir(str(dir_path)) + + # Only preload AgentOne + rc.preload_local_agent_classes(names=["AgentOne"]) + + assert rc._contexts["AgentOne"].agent_instance_class is not None + assert rc._contexts["AgentTwo"].agent_instance_class is None + + +def test_preload_with_names_not_present_skips_all(tmp_path: Path): + """Providing names that don't match any context should skip preloading.""" + dir_path = tmp_path / "agent_cards" + dir_path.mkdir(parents=True) + + card = make_card_dict( + "OnlyAgent", "http://127.0.0.1:9003", push_notifications=False + ) + card["metadata"] = { + "local_agent_class": "valuecell.agents.prompt_strategy_agent.core:PromptBasedStrategyAgent" + } + with open(dir_path / "OnlyAgent.json", "w", encoding="utf-8") as f: + json.dump(card, f) + + rc = RemoteConnections() + rc.load_from_dir(str(dir_path)) + + # Provide a names list that does not include 'OnlyAgent' + rc.preload_local_agent_classes(names=["NoSuchAgent"]) + + assert rc._contexts["OnlyAgent"].agent_instance_class is None + + +def test_preload_with_names_empty_list_skips_all(tmp_path: Path): + """Providing an empty `names` list should skip preloading all agents.""" + dir_path = tmp_path / "agent_cards" + dir_path.mkdir(parents=True) + + card = make_card_dict( + "SomeAgent", "http://127.0.0.1:9004", push_notifications=False + ) + card["metadata"] = { + "local_agent_class": "valuecell.agents.prompt_strategy_agent.core:PromptBasedStrategyAgent" + } + with open(dir_path / "SomeAgent.json", "w", encoding="utf-8") as f: + json.dump(card, f) + + rc = RemoteConnections() + rc.load_from_dir(str(dir_path)) + + # Provide empty list -> nothing should be preloaded + rc.preload_local_agent_classes(names=[]) + + assert rc._contexts["SomeAgent"].agent_instance_class is None + + +def test_preload_with_names_includes_agent_without_spec(tmp_path: Path): + """When `names` includes an agent that lacks a class spec, it should be skipped without error.""" + dir_path = tmp_path / "agent_cards" + dir_path.mkdir(parents=True) + + card_spec = make_card_dict( + "WithSpec", "http://127.0.0.1:9005", push_notifications=False + ) + card_spec["metadata"] = { + "local_agent_class": "valuecell.agents.prompt_strategy_agent.core:PromptBasedStrategyAgent" + } + card_nospec = make_card_dict( + "NoSpec", "http://127.0.0.1:9006", push_notifications=False + ) + + with open(dir_path / "WithSpec.json", "w", encoding="utf-8") as f: + json.dump(card_spec, f) + with open(dir_path / "NoSpec.json", "w", encoding="utf-8") as f: + json.dump(card_nospec, f) + + rc = RemoteConnections() + rc.load_from_dir(str(dir_path)) + + # Request preload for both; only WithSpec should be loaded + rc.preload_local_agent_classes(names=["WithSpec", "NoSpec"]) + + assert rc._contexts["WithSpec"].agent_instance_class is not None + assert rc._contexts["NoSpec"].agent_instance_class is None + + @pytest.mark.asyncio async def test_start_agent_without_listener( tmp_path: Path, monkeypatch: pytest.MonkeyPatch diff --git a/python/valuecell/server/services/agent_stream_service.py b/python/valuecell/server/services/agent_stream_service.py index 52155b32b..1367d20ea 100644 --- a/python/valuecell/server/services/agent_stream_service.py +++ b/python/valuecell/server/services/agent_stream_service.py @@ -33,7 +33,9 @@ def _preload_agent_classes_once() -> None: try: logger.info("Preloading local agent classes...") rc = RemoteConnections() - rc.preload_local_agent_classes() + rc.preload_local_agent_classes( + names=["GridStrategyAgent", "PromptBasedStrategyAgent"] + ) logger.info("✓ Local agent classes preloaded") except Exception as e: logger.warning(f"✗ Failed to preload local agent classes: {e}")