diff --git a/python/configs/agents/research_agent.yaml b/python/configs/agents/research_agent.yaml index a22a298a5..a4f69de80 100644 --- a/python/configs/agents/research_agent.yaml +++ b/python/configs/agents/research_agent.yaml @@ -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 @@ -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" diff --git a/python/configs/agents/super_agent.yaml b/python/configs/agents/super_agent.yaml index 240bbe759..67fe76944 100644 --- a/python/configs/agents/super_agent.yaml +++ b/python/configs/agents/super_agent.yaml @@ -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 @@ -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 diff --git a/python/valuecell/adapters/models/factory.py b/python/valuecell/adapters/models/factory.py index f4d9aca41..edfb7441f 100644 --- a/python/valuecell/adapters/models/factory.py +++ b/python/valuecell/adapters/models/factory.py @@ -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) @@ -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) diff --git a/python/valuecell/agents/research_agent/vdb.py b/python/valuecell/agents/research_agent/vdb.py index d65e3b631..4aa04f4ac 100644 --- a/python/valuecell/agents/research_agent/vdb.py +++ b/python/valuecell/agents/research_agent/vdb.py @@ -17,8 +17,8 @@ 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: @@ -26,7 +26,7 @@ # - 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 diff --git a/python/valuecell/config/manager.py b/python/valuecell/config/manager.py index d659a4576..d365f2328 100644 --- a/python/valuecell/config/manager.py +++ b/python/valuecell/config/manager.py @@ -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( diff --git a/python/valuecell/core/coordinate/tests/test_e2e_persistence.py b/python/valuecell/core/coordinate/tests/test_e2e_persistence.py index 5866b7e39..231a93056 100644 --- a/python/valuecell/core/coordinate/tests/test_e2e_persistence.py +++ b/python/valuecell/core/coordinate/tests/test_e2e_persistence.py @@ -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" diff --git a/python/valuecell/core/plan/planner.py b/python/valuecell/core/plan/planner.py index 64d65e00d..26fa26b63 100644 --- a/python/valuecell/core/plan/planner.py +++ b/python/valuecell/core/plan/planner.py @@ -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 @@ -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(), @@ -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, diff --git a/python/valuecell/core/plan/tests/test_planner.py b/python/valuecell/core/plan/tests/test_planner.py index 98f53d7cb..d65c9131d 100644 --- a/python/valuecell/core/plan/tests/test_planner.py +++ b/python/valuecell/core/plan/tests/test_planner.py @@ -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) @@ -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) @@ -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) diff --git a/python/valuecell/core/super_agent/core.py b/python/valuecell/core/super_agent/core.py index f5ef5aab0..09a34d23e 100644 --- a/python/valuecell/core/super_agent/core.py +++ b/python/valuecell/core/super_agent/core.py @@ -1,5 +1,4 @@ import asyncio -import logging from enum import Enum from typing import Optional @@ -7,15 +6,23 @@ 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): @@ -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 @@ -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, diff --git a/python/valuecell/utils/model.py b/python/valuecell/utils/model.py index 5eb8f45f3..3bd4fa7ad 100644 --- a/python/valuecell/utils/model.py +++ b/python/valuecell/utils/model.py @@ -13,8 +13,12 @@ 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, ) @@ -22,6 +26,18 @@ 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. @@ -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 ):