Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 56 additions & 15 deletions python/valuecell/core/super_agent/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,35 +52,75 @@ 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,
num_history_runs=5,
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."""
Expand Down
Original file line number Diff line number Diff line change
@@ -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"