165 lines
6.1 KiB
Python
165 lines
6.1 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Unit tests for ModelFactory instantiation and grounding tool wiring."""
|
|
|
|
from artemis.llm.router import ModelEndpoint, ModelFactory, ModelProvider
|
|
|
|
|
|
def test_model_factory_anthropic_instantiation():
|
|
"""Verify Anthropic Claude model instantiation with thinking budget."""
|
|
anth_ep = ModelEndpoint(
|
|
provider=ModelProvider.ANTHROPIC,
|
|
model_name="claude-3-7-sonnet-20250219",
|
|
api_key="sk-ant-test-key",
|
|
reasoning_effort="high",
|
|
)
|
|
model = ModelFactory.create_model(anth_ep)
|
|
assert model.model == "claude-3-7-sonnet-20250219"
|
|
assert getattr(model, "thinking", None) == {"type": "enabled", "budget_tokens": 32768}
|
|
|
|
|
|
def test_model_factory_openai_instantiation():
|
|
"""Verify OpenAI model instantiation with reasoning effort."""
|
|
oai_ep = ModelEndpoint(
|
|
provider=ModelProvider.OPENAI,
|
|
model_name="o3-mini",
|
|
api_key="sk-test-key",
|
|
reasoning_effort="medium",
|
|
)
|
|
model = ModelFactory.create_model(oai_ep)
|
|
assert model.model_name == "o3-mini"
|
|
assert getattr(model, "reasoning_effort", None) == "medium"
|
|
|
|
|
|
def test_robust_chat_model_wrapper_grounding_google():
|
|
"""Verify RobustChatModelWrapper auto-injects google_search and server-side tool config for Gemini."""
|
|
from artemis.services.llm import RobustChatModelWrapper
|
|
from langchain_core.tools import tool
|
|
|
|
@tool
|
|
def dummy_tool(query: str) -> str:
|
|
"""A test tool."""
|
|
return query
|
|
|
|
class MockGeminiModel:
|
|
def __init__(self):
|
|
self.bound_tools = []
|
|
self.bound_kwargs = {}
|
|
|
|
def bind_tools(self, tools, *args, **kwargs):
|
|
self.bound_tools = tools
|
|
self.bound_kwargs = kwargs
|
|
return self
|
|
|
|
# Case 1: enable_grounding=True automatically attaches google_search and tool_config
|
|
ep_google = ModelEndpoint(
|
|
provider=ModelProvider.GOOGLE,
|
|
model_name="gemini-2.5-flash",
|
|
enable_grounding=True,
|
|
)
|
|
wrapper = RobustChatModelWrapper(MockGeminiModel(), endpoint=ep_google)
|
|
bound = wrapper.bind_tools([dummy_tool])
|
|
|
|
assert any(isinstance(t, dict) and "google_search" in t for t in bound.base_model.bound_tools)
|
|
assert bound.base_model.bound_kwargs.get("tool_config") == {
|
|
"include_server_side_tool_invocations": True
|
|
}
|
|
|
|
# Case 2: Explicit google_search in tools automatically ensures tool_config
|
|
ep_google_no_ground = ModelEndpoint(
|
|
provider=ModelProvider.GOOGLE,
|
|
model_name="gemini-2.5-flash",
|
|
enable_grounding=False,
|
|
)
|
|
wrapper_explicit = RobustChatModelWrapper(MockGeminiModel(), endpoint=ep_google_no_ground)
|
|
bound_explicit = wrapper_explicit.bind_tools([dummy_tool, {"google_search": {}}])
|
|
|
|
assert any(
|
|
isinstance(t, dict) and "google_search" in t for t in bound_explicit.base_model.bound_tools
|
|
)
|
|
assert bound_explicit.base_model.bound_kwargs.get("tool_config") == {
|
|
"include_server_side_tool_invocations": True
|
|
}
|
|
|
|
|
|
def test_robust_chat_model_wrapper_grounding_non_google():
|
|
"""Verify RobustChatModelWrapper ignores grounding and strips google_search for non-Google providers."""
|
|
from artemis.services.llm import RobustChatModelWrapper
|
|
from langchain_core.tools import tool
|
|
|
|
@tool
|
|
def dummy_tool(query: str) -> str:
|
|
"""A test tool."""
|
|
return query
|
|
|
|
class MockOpenAIModel:
|
|
def __init__(self):
|
|
self.bound_tools = []
|
|
self.bound_kwargs = {}
|
|
|
|
def bind_tools(self, tools, *args, **kwargs):
|
|
self.bound_tools = tools
|
|
self.bound_kwargs = kwargs
|
|
return self
|
|
|
|
ep_openai = ModelEndpoint(
|
|
provider=ModelProvider.OPENAI,
|
|
model_name="gpt-4o",
|
|
enable_grounding=True,
|
|
)
|
|
wrapper = RobustChatModelWrapper(MockOpenAIModel(), endpoint=ep_openai)
|
|
bound = wrapper.bind_tools([dummy_tool, {"google_search": {}}])
|
|
|
|
# Should ignore grounding and strip {"google_search": {}}
|
|
assert len(bound.base_model.bound_tools) == 1
|
|
assert bound.base_model.bound_tools[0].name == "dummy_tool"
|
|
assert "tool_config" not in bound.base_model.bound_kwargs
|
|
|
|
|
|
def test_chat_google_generative_ai_process_tool_config_patch():
|
|
"""Verify patched ChatGoogleGenerativeAI preserves include_server_side_tool_invocations even with tool_choice."""
|
|
from artemis.llm.router import ModelFactory
|
|
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
from langchain_core.tools import tool
|
|
|
|
@tool
|
|
def dummy_tool(query: str) -> str:
|
|
"""A test tool."""
|
|
return query
|
|
|
|
# Trigger patching via Google model creation
|
|
ep = ModelEndpoint(provider=ModelProvider.GOOGLE, model_name="gemini-2.5-flash", api_key="fake")
|
|
model = ModelFactory.create_model(ep)
|
|
assert isinstance(model, ChatGoogleGenerativeAI)
|
|
|
|
formatted_tools = model._format_tools([dummy_tool, {"google_search": {}}], None)
|
|
|
|
# 1. With tool_choice="auto"
|
|
config_auto = model._process_tool_config("auto", None, formatted_tools)
|
|
assert config_auto is not None
|
|
assert config_auto.include_server_side_tool_invocations is True
|
|
|
|
# 2. With tool_choice="any"
|
|
config_any = model._process_tool_config("any", None, formatted_tools)
|
|
assert config_any is not None
|
|
assert config_any.include_server_side_tool_invocations is True
|
|
|
|
# 3. With explicit tool_config
|
|
config_explicit = model._process_tool_config(
|
|
None, {"include_server_side_tool_invocations": True}, formatted_tools
|
|
)
|
|
assert config_explicit is not None
|
|
assert config_explicit.include_server_side_tool_invocations is True
|