Skip to content
Merged
7 changes: 4 additions & 3 deletions python/configs/agents/research_agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ models:
# Embedding model for knowledge base
# Note: If not specified, will auto-select from available providers
embedding:
# model_id: "openai/text-embedding-3-small" # Optional, uses provider default
# provider: "openrouter" # Optional, auto-detects if not specified
model_id: "Qwen/Qwen3-Embedding-4B" # Optional, uses provider default
provider: "siliconflow" # Optional, auto-detects if not specified
parameters:
dimensions: ${EMBEDDER_DIMENSION:1536}
dimensions: ${EMBEDDER_DIMENSION:2560}
# encoding_format: "float"

# Environment Variable Overrides
Expand All @@ -35,5 +35,6 @@ env_overrides:

# Embedding model overrides (backward compatible with existing env vars)
EMBEDDER_MODEL_ID: "models.embedding.model_id"
EMBEDDER_MODEL_PROVIDER: "models.embedding.provider"
EMBEDDER_DIMENSION: "models.embedding.parameters.dimensions"

7 changes: 1 addition & 6 deletions python/configs/agents/super_agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ models:
model_id: "anthropic/claude-haiku-4.5"
provider: "openrouter" # Can be: openrouter, siliconflow, or null (uses system primary_provider)





# Environment Variable Overrides
# Format: ENV_VAR_NAME -> config.path
# These allow runtime configuration through environment variables
Expand Down Expand Up @@ -56,9 +52,8 @@ capabilities:
advanced:
# Debug mode (can be overridden by AGENT_DEBUG_MODE env var)
debug_mode: false

# Output format
use_json_mode: true
markdown: false

# Context settings
Expand Down
62 changes: 62 additions & 0 deletions python/valuecell/adapters/models/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,53 @@ def create_model_for_agent(
**merged_params,
)

def create_embedder_for_agent(
self, agent_name: str, use_fallback: bool = True, **kwargs
):
"""
Create an embedder for a specific agent using its configuration.

This method will use the agent's `embedding_model` configuration when
present. If no embedding config is provided for the agent it will
attempt to use the agent's primary model provider and select the
provider's default embedding model.
"""
# Get agent configuration
agent_config = self.config_manager.get_agent_config(agent_name)

if not agent_config:
raise ValueError(f"Agent configuration not found: {agent_name}")

if not agent_config.enabled:
raise ValueError(f"Agent is disabled: {agent_name}")

# If agent specifies an embedding_model, use it
if agent_config.embedding_model:
emb = agent_config.embedding_model
merged_params = {**emb.parameters, **kwargs}
logger.info(
f"Creating embedder for agent '{agent_name}': model_id={emb.model_id}, provider={emb.provider}, params={merged_params}"
)
return self.create_embedder(
model_id=emb.model_id or None,
provider=emb.provider,
use_fallback=use_fallback,
**merged_params,
)

# Fallback: use primary model's provider and let factory pick provider default
primary = agent_config.primary_model
merged_params = {**primary.parameters, **kwargs}
logger.info(
f"Creating embedder for agent '{agent_name}' using primary provider: {primary.provider}"
)
return self.create_embedder(
model_id=None,
provider=primary.provider,
use_fallback=use_fallback,
**merged_params,
)

def get_available_providers(self) -> list[str]:
"""
Get list of available providers (with valid credentials)
Expand Down Expand Up @@ -752,3 +799,18 @@ def create_embedder(
"""
factory = get_model_factory()
return factory.create_embedder(model_id, provider, **kwargs)


def create_embedder_for_agent(agent_name: str, **kwargs):
"""
Convenience function to create an embedder configured for a specific agent.

Args:
agent_name: Agent name
**kwargs: Override parameters

Returns:
Embedder instance
"""
factory = get_model_factory()
return factory.create_embedder_for_agent(agent_name, **kwargs)
4 changes: 2 additions & 2 deletions python/valuecell/agents/research_agent/vdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
from agno.vectordb.lancedb import LanceDb
from agno.vectordb.search import SearchType

import valuecell.utils.model as model_utils_mod
from valuecell.utils.db import resolve_lancedb_uri
from valuecell.utils.model import get_embedder

# Create embedder using the configuration system
# This will:
# - Check EMBEDDER_MODEL_ID env var first
# - Auto-select provider with embedding support (e.g., SiliconFlow if SILICONFLOW_API_KEY is set)
# - Use provider's default embedding model if not specified
# - Fall back to other providers if primary fails
embedder = get_embedder("EMBEDDER_MODEL_ID")
embedder = model_utils_mod.get_embedder_for_agent("research_agent")

# Alternative usage examples:
# embedder = get_embedder() # Use default env key
Expand Down
5 changes: 3 additions & 2 deletions python/valuecell/config/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,11 @@ def get_agent_config(self, agent_name: str) -> Optional[AgentConfig]:
model_id = provider_config.default_model

# Get parameters
parameters = primary.get("parameters", {})
parameters = primary.get("parameters") or {}

# Merge with global defaults
global_defaults = self._config.get("models", {}).get("defaults", {})
global_models = self._config.get("models") or {}
global_defaults = global_models.get("defaults") or {}
merged_params = {**global_defaults, **parameters}

primary_model = AgentModelConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async def test_orchestrator_buffer_store_e2e(tmp_path, monkeypatch):
factory_mod, "create_embedder", lambda *args, **kwargs: "stub-embedder"
)
monkeypatch.setattr(
model_utils_mod, "create_model", lambda *args, **kwargs: "stub-model"
model_utils_mod, "get_model_for_agent", lambda *args, **kwargs: "stub-model"
)
monkeypatch.setattr(
model_utils_mod, "create_embedder", lambda *args, **kwargs: "stub-embedder"
Expand Down
7 changes: 5 additions & 2 deletions python/valuecell/core/plan/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
from agno.agent import Agent
from agno.db.in_memory import InMemoryDb

import valuecell.utils.model as model_utils_mod
from valuecell.core.agent.connect import RemoteConnections
from valuecell.core.task.models import Task, TaskStatus
from valuecell.core.types import UserInput
from valuecell.utils import generate_uuid
from valuecell.utils.env import agent_debug_mode_enabled
from valuecell.utils.model import get_model
from valuecell.utils.uuid import generate_conversation_id

from .models import ExecutionPlan, PlannerInput, PlannerResponse
Expand Down Expand Up @@ -88,8 +88,10 @@ def __init__(
agent_connections: RemoteConnections,
):
self.agent_connections = agent_connections
# Fetch model via utils module reference so tests can monkeypatch it reliably
model = model_utils_mod.get_model_for_agent("super_agent")
self.planner_agent = Agent(
model=get_model("PLANNER_MODEL_ID"),
model=model,
tools=[
# TODO: enable UserControlFlowTools when stable
# UserControlFlowTools(),
Expand All @@ -101,6 +103,7 @@ def __init__(
markdown=False,
output_schema=PlannerResponse,
expected_output=PLANNER_EXPECTED_OUTPUT,
use_json_mode=model_utils_mod.model_should_use_json_mode(model),
# context
db=InMemoryDb(),
add_datetime_to_context=True,
Expand Down
6 changes: 3 additions & 3 deletions python/valuecell/core/plan/tests/test_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def continue_run(self, *args, **kwargs):

monkeypatch.setattr(planner_mod, "Agent", FakeAgent)
monkeypatch.setattr(
model_utils_mod, "create_model", lambda *args, **kwargs: "stub-model"
model_utils_mod, "get_model_for_agent", lambda *args, **kwargs: "stub-model"
)
monkeypatch.setattr(planner_mod, "agent_debug_mode_enabled", lambda: False)

Expand Down Expand Up @@ -120,7 +120,7 @@ def run(self, *args, **kwargs):

monkeypatch.setattr(planner_mod, "Agent", FakeAgent)
monkeypatch.setattr(
model_utils_mod, "create_model", lambda *args, **kwargs: "stub-model"
model_utils_mod, "get_model_for_agent", lambda *args, **kwargs: "stub-model"
)
monkeypatch.setattr(planner_mod, "agent_debug_mode_enabled", lambda: False)

Expand All @@ -142,7 +142,7 @@ async def callback(request):
def test_tool_get_enabled_agents_formats_cards(monkeypatch: pytest.MonkeyPatch):
# Mock create_model to avoid API key validation in CI
monkeypatch.setattr(
model_utils_mod, "create_model", lambda *args, **kwargs: "stub-model"
model_utils_mod, "get_model_for_agent", lambda *args, **kwargs: "stub-model"
)
monkeypatch.setattr(planner_mod, "agent_debug_mode_enabled", lambda: False)

Expand Down
25 changes: 13 additions & 12 deletions python/valuecell/core/super_agent/core.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import asyncio
import logging
from enum import Enum
from typing import Optional

from agno.agent import Agent
from agno.db.in_memory import InMemoryDb
from pydantic import BaseModel, Field

import valuecell.utils.model as model_utils_mod
from valuecell.core.super_agent.prompts import (
SUPER_AGENT_EXPECTED_OUTPUT,
SUPER_AGENT_INSTRUCTION,
)
from valuecell.core.types import UserInput
from valuecell.utils.env import agent_debug_mode_enabled
from valuecell.utils.model import get_model, get_model_for_agent

logger = logging.getLogger(__name__)

# Backward-compatible helper so tests can monkeypatch `get_model` on this module
def get_model(agent_name: str):
"""Return a model for the given agent via the centralized utils module.

Exposed at module-level to support test monkeypatching without touching
global provider configuration.
"""
return model_utils_mod.get_model_for_agent(agent_name)


class SuperAgentDecision(str, Enum):
Expand Down Expand Up @@ -48,15 +55,8 @@ class SuperAgent:
def __init__(self) -> None:
# Try to use super_agent specific configuration first,
# fallback to PLANNER_MODEL_ID for backward compatibility
try:
model = get_model_for_agent("super_agent")
except Exception:
# Fallback to old behavior for backward compatibility
logger.warning(
"Failed to create model for super_agent, falling back to PLANNER_MODEL_ID"
)
model = get_model("PLANNER_MODEL_ID")

# Use module-level get_model indirection so tests can stub it easily
model = get_model("super_agent")
self.agent = Agent(
model=model,
# TODO: enable tools when needed
Expand All @@ -67,6 +67,7 @@ def __init__(self) -> None:
# output format
expected_output=SUPER_AGENT_EXPECTED_OUTPUT,
output_schema=SuperAgentOutcome,
use_json_mode=model_utils_mod.model_should_use_json_mode(model),
# context
db=InMemoryDb(),
add_datetime_to_context=True,
Expand Down
42 changes: 42 additions & 0 deletions python/valuecell/utils/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,31 @@
import os
from typing import Optional

from agno.models.base import Model as AgnoModel
from agno.models.google import Gemini as AgnoGeminiModel

from valuecell.adapters.models.factory import (
create_embedder,
create_embedder_for_agent,
create_model,
create_model_for_agent,
)

logger = logging.getLogger(__name__)


def model_should_use_json_mode(model: AgnoModel) -> bool:
try:
provider = getattr(model, "provider", None)
name = getattr(model, "name", None)
if provider == AgnoGeminiModel.provider and name == AgnoGeminiModel.name:
return True
except Exception:
# Any unexpected condition falls back to standard (non-JSON) mode
return False
return False


def get_model(env_key: str, **kwargs):
"""
Get model instance using configuration system with environment variable override.
Expand Down Expand Up @@ -230,6 +246,32 @@ def get_embedder(env_key: str = "EMBEDDER_MODEL_ID", **kwargs):
raise


def get_embedder_for_agent(agent_name: str, **kwargs):
"""
Get an embedder instance configured specifically for an agent.

This mirrors `get_model_for_agent` but for embedders. It delegates to
the adapters/models factory which will resolve the agent's embedding
configuration and provider using the three-tier configuration system.

Args:
agent_name: Agent name matching the config file
**kwargs: Override parameters for this specific call

Returns:
Embedder instance configured for the agent

Raises:
ValueError: If agent configuration not found or embedder creation fails
"""

try:
return create_embedder_for_agent(agent_name, **kwargs)
except Exception as e:
logger.error(f"Failed to create embedder for agent '{agent_name}': {e}")
raise


def create_embedder_with_provider(
provider: str, model_id: Optional[str] = None, **kwargs
):
Expand Down