From 94b88b613cb523638c3c1a94336b6d86cd20e79b Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:07:57 +0800 Subject: [PATCH 1/2] feat(agent): enhance preload_local_agent_classes to filter by names list --- python/valuecell/core/agent/connect.py | 10 +- .../core/agent/tests/test_connect.py | 97 +++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/python/valuecell/core/agent/connect.py b/python/valuecell/core/agent/connect.py index 09d7ee5f3..0fdd4cd5e 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,10 @@ 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..815273f23 100644 --- a/python/valuecell/core/agent/tests/test_connect.py +++ b/python/valuecell/core/agent/tests/test_connect.py @@ -240,6 +240,103 @@ 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 From 7800aa88d9067aba78c5608ce1882ded21140a93 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:17:14 +0800 Subject: [PATCH 2/2] feat(agent): update preload_local_agent_classes to accept specific agent names --- python/valuecell/core/agent/connect.py | 4 +++- .../core/agent/tests/test_connect.py | 24 ++++++++++++++----- .../server/services/agent_stream_service.py | 4 +++- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/python/valuecell/core/agent/connect.py b/python/valuecell/core/agent/connect.py index 0fdd4cd5e..54f6d549a 100644 --- a/python/valuecell/core/agent/connect.py +++ b/python/valuecell/core/agent/connect.py @@ -304,7 +304,9 @@ def preload_local_agent_classes(self, names: list[str] | None = None) -> None: 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) + 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) diff --git a/python/valuecell/core/agent/tests/test_connect.py b/python/valuecell/core/agent/tests/test_connect.py index 815273f23..68b271563 100644 --- a/python/valuecell/core/agent/tests/test_connect.py +++ b/python/valuecell/core/agent/tests/test_connect.py @@ -245,11 +245,15 @@ def test_preload_with_names_loads_only_specified(tmp_path: Path): 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 = 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 = 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" } @@ -274,7 +278,9 @@ def test_preload_with_names_not_present_skips_all(tmp_path: Path): 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 = 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" } @@ -295,7 +301,9 @@ def test_preload_with_names_empty_list_skips_all(tmp_path: Path): 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 = 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" } @@ -316,11 +324,15 @@ def test_preload_with_names_includes_agent_without_spec(tmp_path: Path): 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 = 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) + 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) 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}")