From e8d419e7278fc00c082cf6bf5025eef171533a1b Mon Sep 17 00:00:00 2001 From: paisley Date: Tue, 25 Nov 2025 18:32:40 +0800 Subject: [PATCH 1/2] fix super agent update config --- python/valuecell/core/super_agent/core.py | 71 ++++++++++++++++++----- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/python/valuecell/core/super_agent/core.py b/python/valuecell/core/super_agent/core.py index b2b7be9c6..2f14d56d3 100644 --- a/python/valuecell/core/super_agent/core.py +++ b/python/valuecell/core/super_agent/core.py @@ -4,6 +4,7 @@ from agno.agent import Agent from agno.db.in_memory import InMemoryDb +from loguru import logger from pydantic import BaseModel, Field import valuecell.utils.model as model_utils_mod @@ -51,23 +52,16 @@ def _get_or_init_agent(self) -> Optional[Agent]: Returns the initialized Agent or None if initialization fails. """ - if self.agent is not None: - return self.agent - try: - model = model_utils_mod.get_model_for_agent("super_agent") - self.agent = Agent( - model=model, - # TODO: enable tools when needed - # tools=[Crawl4aiTools()], + def _build_agent(with_model) -> Agent: + return Agent( + model=with_model, markdown=False, debug_mode=agent_debug_mode_enabled(), instructions=[SUPER_AGENT_INSTRUCTION], - # output format expected_output=SUPER_AGENT_EXPECTED_OUTPUT, output_schema=SuperAgentOutcome, - use_json_mode=model_utils_mod.model_should_use_json_mode(model), - # context + use_json_mode=model_utils_mod.model_should_use_json_mode(with_model), db=InMemoryDb(), add_datetime_to_context=True, add_history_to_context=True, @@ -75,11 +69,58 @@ def _get_or_init_agent(self) -> Optional[Agent]: read_chat_history=True, enable_session_summaries=True, ) - return self.agent + + try: + expected_model = model_utils_mod.get_model_for_agent("super_agent") + except Exception as e: + logger.warning(f"SuperAgent: failed to resolve expected model: {e}") + expected_model = None + + # Initialize if not present + if self.agent is None: + if expected_model is None: + self.agent = None + return None + try: + self.agent = _build_agent(expected_model) + return self.agent + except Exception as e: + logger.warning(f"SuperAgent: initialization failed: {e}") + self.agent = None + return None + + # If present, check consistency with current environment-configured model + try: + current = getattr(self.agent, "model", None) + current_pair = ( + getattr(current, "id", None), + getattr(current, "provider", None), + ) except Exception: - # Swallow to avoid startup failure; will fallback in run() - self.agent = None - return None + current_pair = (None, None) + + try: + expected_pair = ( + getattr(expected_model, "id", None), + getattr(expected_model, "provider", None), + ) + except Exception: + expected_pair = current_pair + + needs_restart = expected_model is not None and (current_pair != expected_pair) + + if needs_restart: + logger.info( + f"SuperAgent: detected model change {current_pair} -> {expected_pair}, restarting agent" + ) + try: + self.agent = _build_agent(expected_model) + except Exception as e: + logger.warning( + f"SuperAgent: restart failed, continuing with existing agent: {e}" + ) + + return self.agent async def run(self, user_input: UserInput) -> SuperAgentOutcome: """Run super agent triage.""" From 3f0aabe5fcd992bea402a206b6304c6e4b6e5f5c Mon Sep 17 00:00:00 2001 From: paisley Date: Tue, 25 Nov 2025 18:40:51 +0800 Subject: [PATCH 2/2] add test case --- .../tests/test_model_consistency_restart.py | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 python/valuecell/core/super_agent/tests/test_model_consistency_restart.py diff --git a/python/valuecell/core/super_agent/tests/test_model_consistency_restart.py b/python/valuecell/core/super_agent/tests/test_model_consistency_restart.py new file mode 100644 index 000000000..1d51d4aaa --- /dev/null +++ b/python/valuecell/core/super_agent/tests/test_model_consistency_restart.py @@ -0,0 +1,168 @@ +""" +Unit tests for SuperAgent model consistency detection and restart logic. +""" + +from __future__ import annotations + +import pytest + +import valuecell.core.super_agent.core as super_agent_mod + + +class DummyModel: + def __init__(self, mid: str, provider: str): + self.id = mid + self.provider = provider + + +class StubAgent: + def __init__(self, model, **kwargs): + # keep minimal surface: just store the model for inspection + self.model = model + self.kwargs = kwargs + + +@pytest.fixture(autouse=True) +def stub_agent_class(monkeypatch: pytest.MonkeyPatch): + # Replace real Agent with a stub to avoid external dependencies + monkeypatch.setattr(super_agent_mod, "Agent", StubAgent) + # Make json-mode decision deterministic and lightweight + monkeypatch.setattr( + super_agent_mod.model_utils_mod, "model_should_use_json_mode", lambda _m: False + ) + + +def test_init_builds_agent_with_expected_model(monkeypatch: pytest.MonkeyPatch): + # Arrange + expected = DummyModel("m1", "p1") + monkeypatch.setattr( + super_agent_mod.model_utils_mod, + "get_model_for_agent", + lambda name: expected if name == "super_agent" else None, + ) + + sa = super_agent_mod.SuperAgent() + + # Act + agent = sa._get_or_init_agent() + + # Assert + assert agent is not None + assert agent.model is expected + assert agent.model.id == "m1" + assert agent.model.provider == "p1" + + +def test_restart_when_model_differs(monkeypatch: pytest.MonkeyPatch): + # Arrange: current agent has old model + sa = super_agent_mod.SuperAgent() + current = DummyModel("old", "p1") + sa.agent = StubAgent(current) + + expected = DummyModel("new", "p1") + monkeypatch.setattr( + super_agent_mod.model_utils_mod, + "get_model_for_agent", + lambda _name: expected, + ) + + # Act + agent = sa._get_or_init_agent() + + # Assert: agent replaced with new model + assert agent is not None + assert agent.model is expected + assert agent.model.id == "new" + assert agent.model.provider == "p1" + + +def test_keep_agent_when_model_same(monkeypatch: pytest.MonkeyPatch): + # Arrange: current and expected models match on id+provider + sa = super_agent_mod.SuperAgent() + current = DummyModel("same", "p1") + old_agent = StubAgent(current) + sa.agent = old_agent + + expected = DummyModel("same", "p1") + monkeypatch.setattr( + super_agent_mod.model_utils_mod, + "get_model_for_agent", + lambda _name: expected, + ) + + # Act + agent = sa._get_or_init_agent() + + # Assert: no restart; instance identity is preserved + assert agent is old_agent + assert agent.model.id == "same" + assert agent.model.provider == "p1" + + +def test_init_returns_none_when_expected_model_unavailable( + monkeypatch: pytest.MonkeyPatch, +): + # Arrange: model resolution fails + def _raise(*_a, **_k): + raise RuntimeError("no model") + + monkeypatch.setattr(super_agent_mod.model_utils_mod, "get_model_for_agent", _raise) + + sa = super_agent_mod.SuperAgent() + + # Act + agent = sa._get_or_init_agent() + + # Assert + assert agent is None + + +def test_existing_agent_kept_when_model_resolution_fails( + monkeypatch: pytest.MonkeyPatch, +): + # Arrange: model resolution fails but an agent already exists + def _raise(*_a, **_k): + raise RuntimeError("no model") + + monkeypatch.setattr(super_agent_mod.model_utils_mod, "get_model_for_agent", _raise) + + sa = super_agent_mod.SuperAgent() + current = DummyModel("old", "p1") + old_agent = StubAgent(current) + sa.agent = old_agent + + # Act + agent = sa._get_or_init_agent() + + # Assert: existing agent is preserved + assert agent is old_agent + assert agent.model.id == "old" + + +def test_restart_failure_keeps_existing_agent(monkeypatch: pytest.MonkeyPatch): + # Arrange: expected model differs, but Agent construction fails + sa = super_agent_mod.SuperAgent() + current = DummyModel("old", "p1") + old_agent = StubAgent(current) + sa.agent = old_agent + + expected = DummyModel("new", "p1") + monkeypatch.setattr( + super_agent_mod.model_utils_mod, + "get_model_for_agent", + lambda _name: expected, + ) + + class FailingAgent: + def __init__(self, *args, **kwargs): + raise RuntimeError("boom") + + # Replace Agent with failing constructor just for this restart + monkeypatch.setattr(super_agent_mod, "Agent", FailingAgent) + + # Act + agent = sa._get_or_init_agent() + + # Assert: keep old agent on failure + assert agent is old_agent + assert agent.model.id == "old"