Nathan Brake commited on
Commit
de37bdf
·
unverified ·
1 Parent(s): 14653c6

Some re-arranging to make MCP work, add some doc and tests (#3)

Browse files

* Some re-arranging to make MCP work, add some doc and tests

* prompt grammar fix

* Add Phoenix info

* Updates from code review

.gitignore CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  # Byte-compiled / optimized / DLL files
2
  __pycache__/
3
  *.py[cod]
 
1
+ # Custom gitignores
2
+ uv.lock
3
+
4
+
5
  # Byte-compiled / optimized / DLL files
6
  __pycache__/
7
  *.py[cod]
README.md CHANGED
@@ -9,20 +9,49 @@
9
  </picture>
10
  </p>
11
 
12
- This blueprint guides you to ...
 
 
13
 
14
- 📘 To explore this project further and discover other Blueprints, visit the [**Blueprints Hub**](https://developer-hub.mozilla.ai/).
 
 
 
 
 
15
 
16
- 👉 📖 For more detailed guidance on using this project, please visit our [**Docs here**](https://mozilla-ai.github.io/surf-spot-finder/)
 
 
 
 
 
 
17
 
18
  ### Built with
19
- - Python 3.10+
20
- - Open-Source Tool 1
21
- - Open-Source Tool 2
22
- - ...
 
23
 
24
- ## Quick-start
 
 
 
 
25
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  ## How it Works
28
 
@@ -36,8 +65,26 @@ This blueprint guides you to ...
36
  - Disk space:
37
 
38
  - **Dependencies**:
 
39
  - Dependencies listed in `pyproject.toml`
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  ## Troubleshooting
43
 
 
9
  </picture>
10
  </p>
11
 
12
+ Many Large Language Model (LLM) capabilities are unlocked when they are given access to tools and given control of their
13
+ own runtime and execution path. However, it's important that as they are given greater capabilities, they are properly
14
+ evaluated and controlled.
15
 
16
+ In this Blueprint, we demonstrate an AI agent designed for an extremely specific task (some refer to this as a "Vertical Agent")
17
+ that is given the web and searching access it needs to find an answer the same way you would find the answer as a human.
18
+
19
+ This agent is designed for help in finding the next great surf spot near you: the agent is provided with a location, a distance,
20
+ a timestamp, and it's able to independently search and browse the web to recommend the best spot to you along with the
21
+ relevant information!
22
 
23
+ Although this exact use-case may not be useful to you directly, the framework we provide here is intended to be easily
24
+ adapted to the Agent use case you have in mind.
25
+
26
+ This implementation uses the [smolagents](https://huggingface.co/docs/smolagents/index) library for Agentic capabilities, alongside
27
+ of the increasingly Model Context Protocol (MCP) which allows for a standard access communication standard for a large number of tools.
28
+
29
+ 📘 To explore this project further and discover other Blueprints, visit the [**Blueprints Hub**](https://developer-hub.mozilla.ai/).
30
 
31
  ### Built with
32
+ ![Python](https://img.shields.io/badge/Python-3.10%2B-blue)
33
+ [![smolagents](https://img.shields.io/badge/Smolagents-%F0%9F%A4%97-yellow)](https://huggingface.co/docs/smolagents/index)
34
+
35
+
36
+ ## 🚀 Quick Start
37
 
38
+ ### 1️⃣ Clone the Project
39
+ ```bash
40
+ git clone https://github.com/mozilla-ai/surf-spot-finder.git
41
+ cd surf-spot-finder
42
+ ```
43
 
44
+ ### 2️⃣ Update submodule and install dependencies
45
+ ```bash
46
+ pip install -e . # Install root project dependencies
47
+ ```
48
+
49
+ ### 3️⃣ Run
50
+
51
+ ```bash
52
+ export OPENAI_API_KEY=yourkeyhere
53
+ surf-spot-finder --location="Pittsburgh Pennsylvania" --date="2025-03-11 22:00" --max-driving-hours=5 --model-id="openai/o1" --api-key-var="OPENAI_API_KEY"
54
+ ```
55
 
56
  ## How it Works
57
 
 
65
  - Disk space:
66
 
67
  - **Dependencies**:
68
+ - Docker
69
  - Dependencies listed in `pyproject.toml`
70
 
71
+ ## Run Tests
72
+
73
+ ```bash
74
+ pip install -e .[tests]
75
+ ```
76
+
77
+ ### Unit Tests
78
+
79
+ ```bash
80
+ pytest
81
+ ```
82
+
83
+ ### Integration Tests
84
+
85
+ ```bash
86
+ INTEGRATION_TESTS=Y pytest # Requires docker and OPENAI_API_KEY
87
+ ```
88
 
89
  ## Troubleshooting
90
 
pyproject.toml CHANGED
@@ -34,6 +34,11 @@ tests = [
34
  "pytest-sugar>=0.9.6",
35
  ]
36
 
 
 
 
 
 
37
  [project.urls]
38
  Documentation = "https://mozilla-ai.github.io/surf-spot-finder/"
39
  Issues = "https://github.com/mozilla-ai/surf-spot-finder/issues"
@@ -47,4 +52,6 @@ namespaces = false
47
  [tool.setuptools_scm]
48
 
49
  [project.scripts]
50
- find-surf-spot = "surf_spot_finder.cli:main"
 
 
 
34
  "pytest-sugar>=0.9.6",
35
  ]
36
 
37
+ # TODO maybe we don't want to keep this, or we want to swap this to Lumigator SDK
38
+ tracing = [
39
+ "arize-phoenix>=8.12.1",
40
+ ]
41
+
42
  [project.urls]
43
  Documentation = "https://mozilla-ai.github.io/surf-spot-finder/"
44
  Issues = "https://github.com/mozilla-ai/surf-spot-finder/issues"
 
52
  [tool.setuptools_scm]
53
 
54
  [project.scripts]
55
+ surf-spot-finder = "surf_spot_finder.cli:main"
56
+ # TODO maybe this would be lumigator
57
+ start-phoenix = "phoenix.server.main:main"
src/surf_spot_finder/agents/smolagents.py CHANGED
@@ -8,53 +8,67 @@ if TYPE_CHECKING:
8
 
9
 
10
  @logger.catch(reraise=True)
11
- def load_smolagent(model_id: str, api_key_var: Optional[str]) -> "CodeAgent":
12
- """ """
13
- from smolagents import (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  CodeAgent,
15
- ToolCollection,
16
  DuckDuckGoSearchTool,
17
- VisitWebpageTool,
18
  LiteLLMModel,
 
 
 
 
 
 
 
19
  )
 
20
  from mcp import StdioServerParameters
21
 
22
  model = LiteLLMModel(
23
  model_id=model_id,
24
- api_key_var=os.environ[api_key_var] if api_key_var else None,
 
25
  )
26
 
27
- if "GOOGLE_MAPS_API_KEY" in os.environ:
28
- # We could easily use any of the MCPs at https://github.com/modelcontextprotocol/servers
29
- # or at https://glama.ai/mcp/servers
30
- # or at https://smithery.ai/
31
- # https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps
32
- server_parameters = StdioServerParameters(
33
- command="npx",
34
- args=["@modelcontextprotocol/server-google-maps"],
35
- env={**os.environ},
36
- )
37
- # https://huggingface.co/docs/smolagents/v1.10.0/en/reference/tools#smolagents.ToolCollection.from_mcp
38
- with ToolCollection.from_mcp(server_parameters) as tool_collection:
39
- agent = CodeAgent(
40
- tools=[
41
- *tool_collection.tools,
42
- DuckDuckGoSearchTool(),
43
- VisitWebpageTool(),
44
- ],
45
- model=model,
46
- add_base_tools=True,
47
- additional_authorized_imports=["json"],
48
- )
49
- else:
50
- logger.debug(
51
- "GOOGLE_MAPS_api_key_var not set, running without Google Maps tool"
52
- )
53
  agent = CodeAgent(
54
- tools=[DuckDuckGoSearchTool(), VisitWebpageTool()],
 
 
 
55
  model=model,
56
- add_base_tools=True,
57
- additional_authorized_imports=["json"],
58
  )
 
59
 
60
  return agent
 
8
 
9
 
10
  @logger.catch(reraise=True)
11
+ def run_smolagent(
12
+ model_id: str,
13
+ prompt: str,
14
+ api_key_var: Optional[str] = None,
15
+ api_base: Optional[str] = None,
16
+ ) -> "CodeAgent":
17
+ """
18
+ Create and configure a Smolagents CodeAgent with the specified model.
19
+ See https://docs.litellm.ai/docs/providers for details on available LiteLLM providers.
20
+ Args:
21
+ model_id (str): Model identifier using LiteLLM syntax (e.g., 'openai/o1', 'anthropic/claude-3-sonnet')
22
+ prompt (str): Prompt to provide to the model
23
+ api_key_var (Optional[str]): Name of environment variable containing the API key
24
+ api_base (Optional[str]): Custom API base URL, if needed for non-default endpoints
25
+
26
+ Returns:
27
+ CodeAgent: Configured agent ready to process requests
28
+
29
+ Example:
30
+ >>> agent = run_smolagent("anthropic/claude-3-haiku", "my prompt here", "ANTHROPIC_API_KEY", None, None)
31
+ >>> agent.run("Find surf spots near San Diego")
32
+ """
33
+ from smolagents import ( # pylint: disable=import-outside-toplevel
34
  CodeAgent,
 
35
  DuckDuckGoSearchTool,
 
36
  LiteLLMModel,
37
+ ToolCollection,
38
+ )
39
+
40
+ model = LiteLLMModel(
41
+ model_id=model_id,
42
+ api_base=api_base if api_base else None,
43
+ api_key=os.environ[api_key_var] if api_key_var else None,
44
  )
45
+
46
  from mcp import StdioServerParameters
47
 
48
  model = LiteLLMModel(
49
  model_id=model_id,
50
+ api_base=api_base if api_base else None,
51
+ api_key=os.environ[api_key_var] if api_key_var else None,
52
  )
53
 
54
+ # We could easily use any of the MCPs at https://github.com/modelcontextprotocol/servers
55
+ # or at https://glama.ai/mcp/servers
56
+ # or at https://smithery.ai/
57
+ server_parameters = StdioServerParameters(
58
+ command="docker",
59
+ args=["run", "-i", "--rm", "mcp/fetch"],
60
+ env={**os.environ},
61
+ )
62
+ # https://huggingface.co/docs/smolagents/v1.10.0/en/reference/tools#smolagents.ToolCollection.from_mcp
63
+ with ToolCollection.from_mcp(server_parameters) as tool_collection:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  agent = CodeAgent(
65
+ tools=[
66
+ *tool_collection.tools,
67
+ DuckDuckGoSearchTool(),
68
+ ],
69
  model=model,
70
+ add_base_tools=False, # Turn this on if you want to let it run python code as it sees fit
 
71
  )
72
+ agent.run(prompt)
73
 
74
  return agent
src/surf_spot_finder/cli.py CHANGED
@@ -7,7 +7,7 @@ from surf_spot_finder.config import (
7
  Config,
8
  DEFAULT_PROMPT,
9
  )
10
- from surf_spot_finder.agents.smolagents import load_smolagent
11
  from surf_spot_finder.tracing import setup_tracing
12
 
13
 
@@ -20,6 +20,7 @@ def find_surf_spot(
20
  api_key_var: Optional[str] = None,
21
  prompt: str = DEFAULT_PROMPT,
22
  json_tracer: bool = True,
 
23
  ):
24
  logger.info("Loading config")
25
  config = Config(
@@ -30,21 +31,22 @@ def find_surf_spot(
30
  api_key_var=api_key_var,
31
  prompt=prompt,
32
  json_tracer=json_tracer,
 
33
  )
34
 
35
- logger.info("Loading agent")
36
- agent = load_smolagent(config.model_id, config.api_key_var)
37
-
38
  logger.info("Setting up tracing")
39
- setup_tracing(project_name="find-surf-spot", json_tracer=config.json_tracer)
40
 
41
  logger.info("Running agent")
42
- agent.run(
43
- config.prompt.format(
 
 
 
44
  LOCATION=config.location,
45
  MAX_DRIVING_HOURS=config.max_driving_hours,
46
  DATE=config.date,
47
- )
48
  )
49
 
50
 
 
7
  Config,
8
  DEFAULT_PROMPT,
9
  )
10
+ from surf_spot_finder.agents.smolagents import run_smolagent
11
  from surf_spot_finder.tracing import setup_tracing
12
 
13
 
 
20
  api_key_var: Optional[str] = None,
21
  prompt: str = DEFAULT_PROMPT,
22
  json_tracer: bool = True,
23
+ api_base: Optional[str] = None,
24
  ):
25
  logger.info("Loading config")
26
  config = Config(
 
31
  api_key_var=api_key_var,
32
  prompt=prompt,
33
  json_tracer=json_tracer,
34
+ api_base=api_base,
35
  )
36
 
 
 
 
37
  logger.info("Setting up tracing")
38
+ setup_tracing(project_name="surf-spot-finder", json_tracer=config.json_tracer)
39
 
40
  logger.info("Running agent")
41
+ run_smolagent(
42
+ model_id=config.model_id,
43
+ api_key_var=config.api_key_var,
44
+ api_base=config.api_base,
45
+ prompt=config.prompt.format(
46
  LOCATION=config.location,
47
  MAX_DRIVING_HOURS=config.max_driving_hours,
48
  DATE=config.date,
49
+ ),
50
  )
51
 
52
 
src/surf_spot_finder/config.py CHANGED
@@ -1,26 +1,32 @@
1
  from typing import Annotated, Optional
2
  from pydantic import AfterValidator, BaseModel, FutureDatetime, PositiveInt
 
3
 
 
4
 
5
  DEFAULT_PROMPT = (
6
  "What will be the best surf spot around {LOCATION}"
7
- ", in a radio of {MAX_DRIVING_HOURS} hours driving"
8
- ", at {DATE}?"
 
 
 
9
  )
10
 
11
 
12
- def validate_prompt(value):
13
- for placeholder in ("{LOCATION}", "{MAX_DRIVING_HOURS}"):
14
  if placeholder not in value:
15
  raise ValueError(f"prompt must contain {placeholder}")
16
  return value
17
 
18
 
19
  class Config(BaseModel):
20
- prompt: str = Annotated[str, AfterValidator(validate_prompt)]
21
  location: str
22
  max_driving_hours: PositiveInt
23
  date: FutureDatetime
24
  model_id: str
25
  api_key_var: Optional[str] = None
26
  json_tracer: bool = True
 
 
1
  from typing import Annotated, Optional
2
  from pydantic import AfterValidator, BaseModel, FutureDatetime, PositiveInt
3
+ from datetime import datetime
4
 
5
+ CURRENT_DATE = datetime.now().strftime("%Y-%m-%d")
6
 
7
  DEFAULT_PROMPT = (
8
  "What will be the best surf spot around {LOCATION}"
9
+ ", in a {MAX_DRIVING_HOURS} hour driving radius"
10
+ ", at {DATE}? it is currently "
11
+ + CURRENT_DATE
12
+ + ". find me the best surf spot and the"
13
+ " up to date weather forecast for that day."
14
  )
15
 
16
 
17
+ def validate_prompt(value) -> str:
18
+ for placeholder in ("{LOCATION}", "{MAX_DRIVING_HOURS}", "{DATE}"):
19
  if placeholder not in value:
20
  raise ValueError(f"prompt must contain {placeholder}")
21
  return value
22
 
23
 
24
  class Config(BaseModel):
25
+ prompt: Annotated[str, AfterValidator(validate_prompt)]
26
  location: str
27
  max_driving_hours: PositiveInt
28
  date: FutureDatetime
29
  model_id: str
30
  api_key_var: Optional[str] = None
31
  json_tracer: bool = True
32
+ api_base: Optional[str] = None
src/surf_spot_finder/tracing.py CHANGED
@@ -24,7 +24,7 @@ class JsonFileSpanExporter(SpanExporter):
24
  pass
25
 
26
 
27
- def setup_tracing(project_name: str, json_tracer: bool = True) -> TracerProvider:
28
  """
29
  Set up tracing configuration based on the selected mode.
30
 
 
24
  pass
25
 
26
 
27
+ def setup_tracing(project_name: str, json_tracer: bool) -> TracerProvider:
28
  """
29
  Set up tracing configuration based on the selected mode.
30
 
tests/integration/agents/test_integration_smolagents.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pytest
3
+ from unittest.mock import patch
4
+
5
+ from surf_spot_finder.agents.smolagents import run_smolagent
6
+
7
+ # TODO I'd rather not use openai
8
+ INTEGRATION_MODEL = "openai/gpt-3.5-turbo"
9
+ API_KEY_VAR = "OPENAI_API_KEY"
10
+
11
+
12
+ @pytest.mark.skipif(
13
+ "INTEGRATION_TESTS" not in os.environ,
14
+ reason="Integration tests require INTEGRATION_TESTS env var",
15
+ )
16
+ def test_smolagent_integration():
17
+ """
18
+ Full integration test of the smolagent functionality.
19
+
20
+ Requires:
21
+ - Docker to be running
22
+ - OPENAI_API_KEY in environment variables
23
+ - INTEGRATION_TESTS env var to be set
24
+ """
25
+ with patch("smolagents.CodeAgent") as MockCodeAgent:
26
+ # Create a mock agent that returns itself from run()
27
+ mock_agent = MockCodeAgent.return_value
28
+ mock_agent.run.return_value = mock_agent
29
+
30
+ # Run the agent
31
+ result = run_smolagent(
32
+ INTEGRATION_MODEL,
33
+ "Find popular surf spots in California",
34
+ api_key_var=API_KEY_VAR,
35
+ )
36
+
37
+ # Verify the agent was created and run
38
+ MockCodeAgent.assert_called_once()
39
+ mock_agent.run.assert_called_once_with("Find popular surf spots in California")
40
+ assert result is mock_agent
41
+
42
+
43
+ @pytest.mark.skipif(
44
+ "INTEGRATION_TESTS" not in os.environ,
45
+ reason="Full integration tests require INTEGRATION_TESTS env var",
46
+ )
47
+ def test_smolagent_real_execution():
48
+ """
49
+ Tests the actual execution of the agent against real APIs.
50
+
51
+ WARNING: This will make actual API calls and incur costs.
52
+ Only run when explicitly needed for full system testing.
53
+
54
+ Requires:
55
+ - Docker to be running
56
+ - OPENAI_API_KEY in environment variables
57
+ - INTEGRATION_TESTS env var to be set
58
+ """
59
+ # Run with a simple, inexpensive request
60
+ agent = run_smolagent(
61
+ INTEGRATION_MODEL,
62
+ "What are three popular surf spots in California?",
63
+ api_key_var=API_KEY_VAR,
64
+ )
65
+
66
+ # Basic verification that we got an agent back
67
+ assert agent is not None
tests/unit/agents/test_load_smolagents.py DELETED
@@ -1,27 +0,0 @@
1
- from surf_spot_finder.agents.smolagents import load_smolagent
2
-
3
-
4
- def test_google_maps_tool(monkeypatch):
5
- monkeypatch.setenv("GEMINI_API_KEY", "FOO")
6
-
7
- no_google_maps_agent = load_smolagent("gemini/gemini-2.0-flash", "GEMINI_API_KEY")
8
- assert sorted(list(no_google_maps_agent.tools.keys())) == [
9
- "final_answer",
10
- "visit_webpage",
11
- "web_search",
12
- ]
13
-
14
- monkeypatch.setenv("GOOGLE_MAPS_API_KEY", "BAR")
15
- google_maps_agent = load_smolagent("gemini/gemini-2.0-flash", "GEMINI_API_KEY")
16
- assert sorted(list(google_maps_agent.tools.keys())) == [
17
- "final_answer",
18
- "maps_directions",
19
- "maps_distance_matrix",
20
- "maps_elevation",
21
- "maps_geocode",
22
- "maps_place_details",
23
- "maps_reverse_geocode",
24
- "maps_search_places",
25
- "visit_webpage",
26
- "web_search",
27
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/agents/test_unit_smolagents.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pytest
3
+ from unittest.mock import patch, MagicMock
4
+
5
+ from surf_spot_finder.agents.smolagents import run_smolagent
6
+
7
+
8
+ @pytest.fixture
9
+ def mock_smolagents_imports():
10
+ """Mock the smolagents imports to avoid actual instantiation."""
11
+ mock_code_agent = MagicMock()
12
+ mock_ddg_tool = MagicMock()
13
+ mock_litellm_model = MagicMock()
14
+ mock_tool_collection = MagicMock()
15
+
16
+ # Configure the mock tool collection to work as a context manager
17
+ mock_tool_collection.from_mcp.return_value.__enter__.return_value = (
18
+ mock_tool_collection
19
+ )
20
+ mock_tool_collection.from_mcp.return_value.__exit__.return_value = None
21
+ mock_tool_collection.tools = ["mock_tool"]
22
+
23
+ with patch.dict(
24
+ "sys.modules",
25
+ {
26
+ "smolagents": MagicMock(
27
+ CodeAgent=mock_code_agent,
28
+ DuckDuckGoSearchTool=mock_ddg_tool,
29
+ LiteLLMModel=mock_litellm_model,
30
+ ToolCollection=mock_tool_collection,
31
+ ),
32
+ "mcp": MagicMock(
33
+ StdioServerParameters=MagicMock(),
34
+ ),
35
+ },
36
+ ):
37
+ yield {
38
+ "CodeAgent": mock_code_agent,
39
+ "DuckDuckGoSearchTool": mock_ddg_tool,
40
+ "LiteLLMModel": mock_litellm_model,
41
+ "ToolCollection": mock_tool_collection,
42
+ }
43
+
44
+
45
+ @pytest.mark.usefixtures("mock_smolagents_imports")
46
+ def test_run_smolagent_with_api_key_var():
47
+ """Test smolagent creation with an API key from environment variable."""
48
+ # The patch.dict(os.environ, {"TEST_API_KEY": "test-key-12345"})
49
+ # is a testing construct that temporarily modifies the environment variables
50
+ # for the duration of the test.
51
+ # some tests use TEST_API_KEY while others don't
52
+ with patch.dict(os.environ, {"TEST_API_KEY": "test-key-12345"}):
53
+ from smolagents import CodeAgent, LiteLLMModel
54
+
55
+ run_smolagent("openai/gpt-4", "Test prompt", api_key_var="TEST_API_KEY")
56
+
57
+ LiteLLMModel.assert_called()
58
+ model_call_kwargs = LiteLLMModel.call_args[1]
59
+ assert model_call_kwargs["model_id"] == "openai/gpt-4"
60
+ assert model_call_kwargs["api_key"] == "test-key-12345"
61
+ assert model_call_kwargs["api_base"] is None
62
+
63
+ CodeAgent.assert_called_once()
64
+ CodeAgent.return_value.run.assert_called_once_with("Test prompt")
65
+
66
+
67
+ @pytest.mark.usefixtures("mock_smolagents_imports")
68
+ def test_run_smolagent_with_custom_api_base():
69
+ """Test smolagent creation with a custom API base."""
70
+ with patch.dict(os.environ, {"TEST_API_KEY": "test-key-12345"}):
71
+ from smolagents import LiteLLMModel
72
+
73
+ # Act
74
+ run_smolagent(
75
+ "anthropic/claude-3-sonnet",
76
+ "Test prompt",
77
+ api_key_var="TEST_API_KEY",
78
+ api_base="https://custom-api.example.com",
79
+ )
80
+ last_call = LiteLLMModel.call_args_list[-1]
81
+
82
+ assert last_call[1]["model_id"] == "anthropic/claude-3-sonnet"
83
+ assert last_call[1]["api_key"] == "test-key-12345"
84
+ assert last_call[1]["api_base"] == "https://custom-api.example.com"
85
+
86
+
87
+ @pytest.mark.usefixtures("mock_smolagents_imports")
88
+ def test_run_smolagent_without_api_key():
89
+ """You should be able to run the smolagent without an API key."""
90
+ from smolagents import LiteLLMModel
91
+
92
+ run_smolagent("ollama_chat/deepseek-r1", "Test prompt")
93
+
94
+ last_call = LiteLLMModel.call_args_list[-1]
95
+ assert last_call[1]["model_id"] == "ollama_chat/deepseek-r1"
96
+ assert last_call[1]["api_key"] is None
97
+
98
+
99
+ def test_run_smolagent_environment_error():
100
+ """Test that passing a bad api_key_var throws an error"""
101
+ with patch.dict(os.environ, {}, clear=True):
102
+ with pytest.raises(KeyError, match="MISSING_KEY"):
103
+ run_smolagent("test-model", "Test prompt", api_key_var="MISSING_KEY")