Skip to content

Commit 16639ff

Browse files
cpsievertclaude
andcommitted
feat: add provider-agnostic tool_web_search() and tool_web_fetch()
Add built-in web search and URL fetch tools that work across providers: - tool_web_search(): Works with OpenAI, Anthropic, and Google - tool_web_fetch(): Works with Anthropic and Google Each provider automatically translates the tool configuration to its specific API format. Supports options like allowed_domains, blocked_domains, user_location, and max_uses where applicable. This is equivalent to tidyverse/ellmer#829 but with a cleaner provider-agnostic API (one function instead of separate functions per provider). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 351c8fd commit 16639ff

File tree

7 files changed

+1029
-14
lines changed

7 files changed

+1029
-14
lines changed

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ The project uses `uv` for package management and Make for common tasks:
4949
4. **Content-Based Messaging**: All communication uses structured `Content` objects rather than raw strings
5050
5. **Tool Integration**: Seamless function calling with automatic JSON schema generation from Python type hints
5151

52+
### Typing Best Practices
53+
54+
This project prioritizes strong typing that leverages provider SDK types directly:
55+
56+
- **Use provider SDK types**: Import and use types from `openai.types`, `anthropic.types`, `google.genai.types`, etc. rather than creating custom TypedDicts or dataclasses that mirror them. This ensures compatibility with SDK updates and provides better IDE support.
57+
- **Use `@overload` for provider-specific returns**: When a method returns different types based on a provider argument, use `@overload` with `Literal` types to give callers precise return type information.
58+
- **Explore SDK types interactively**: Use `python -c "from <sdk>.types import <Type>; print(<Type>.__annotations__)"` to inspect available fields and nested types when implementing provider-specific features.
59+
5260
### Testing Structure
5361

5462
- Tests are organized by component (e.g., `test_provider_openai.py`, `test_tools.py`)

chatlas/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from ._provider_snowflake import ChatSnowflake
3636
from ._tokens import token_usage
3737
from ._tools import Tool, ToolBuiltIn, ToolRejectError
38+
from ._tools_builtin import tool_web_fetch, tool_web_search
3839
from ._turn import AssistantTurn, SystemTurn, Turn, UserTurn
3940

4041
try:
@@ -86,6 +87,8 @@
8687
"Tool",
8788
"ToolBuiltIn",
8889
"ToolRejectError",
90+
"tool_web_fetch",
91+
"tool_web_search",
8992
"Turn",
9093
"UserTurn",
9194
"SystemTurn",

chatlas/_provider_anthropic.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
)
4040
from ._tokens import get_price_info
4141
from ._tools import Tool, ToolBuiltIn, basemodel_to_param_schema
42+
from ._tools_builtin import ToolWebFetch, ToolWebSearch
4243
from ._turn import AssistantTurn, SystemTurn, Turn, UserTurn, user_turn
4344
from ._utils import split_http_client_kwargs
4445

@@ -690,6 +691,11 @@ def _as_content_block(content: Content) -> "ContentBlockParam":
690691

691692
@staticmethod
692693
def _anthropic_tool_schema(tool: "Tool | ToolBuiltIn") -> "ToolUnionParam":
694+
if isinstance(tool, ToolWebSearch):
695+
return tool.get_definition("anthropic")
696+
if isinstance(tool, ToolWebFetch):
697+
# TODO: why is this not apart of ToolUnionParam?
698+
return tool.get_definition("anthropic") # type: ignore
693699
if isinstance(tool, ToolBuiltIn):
694700
return tool.definition # type: ignore
695701

chatlas/_provider_google.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ._provider import ModelInfo, Provider, StandardModelParamNames, StandardModelParams
2323
from ._tokens import get_price_info
2424
from ._tools import Tool, ToolBuiltIn
25+
from ._tools_builtin import ToolWebFetch, ToolWebSearch
2526
from ._turn import AssistantTurn, SystemTurn, Turn, UserTurn, user_turn
2627

2728
if TYPE_CHECKING:
@@ -319,21 +320,36 @@ def _chat_perform_args(
319320
config.response_mime_type = "application/json"
320321

321322
if tools:
322-
config.tools = [
323-
GoogleTool(
324-
function_declarations=[
325-
FunctionDeclaration.from_callable(
326-
client=self._client._api_client,
327-
callable=tool.func,
328-
)
329-
for tool in tools.values()
330-
# TODO: to support built-in tools, we may need a way to make
331-
# tool names (e.g., google_search to google.genai.types.GoogleSearch())
332-
if isinstance(tool, Tool)
333-
]
334-
)
323+
# Separate regular tools from built-in tools
324+
regular_tools = [t for t in tools.values() if isinstance(t, Tool)]
325+
builtin_tools = [
326+
tool.get_definition("google")
327+
for tool in tools.values()
328+
if isinstance(tool, (ToolWebSearch, ToolWebFetch))
335329
]
336330

331+
google_tools: list[Any] = []
332+
333+
# Add regular function tools
334+
if regular_tools:
335+
google_tools.append(
336+
GoogleTool(
337+
function_declarations=[
338+
FunctionDeclaration.from_callable(
339+
client=self._client._api_client,
340+
callable=tool.func,
341+
)
342+
for tool in regular_tools
343+
]
344+
)
345+
)
346+
347+
# Add built-in tools
348+
google_tools.extend(builtin_tools)
349+
350+
if google_tools:
351+
config.tools = google_tools
352+
337353
kwargs_full["config"] = config
338354

339355
return kwargs_full

chatlas/_provider_openai.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ._provider_openai_completions import load_tool_request_args
2626
from ._provider_openai_generic import BatchResult, OpenAIAbstractProvider
2727
from ._tools import Tool, ToolBuiltIn, basemodel_to_param_schema
28+
from ._tools_builtin import ToolWebFetch, ToolWebSearch
2829
from ._turn import AssistantTurn, Turn
2930

3031
if TYPE_CHECKING:
@@ -235,7 +236,16 @@ def _chat_perform_args(
235236

236237
tool_params: list["ToolParam"] = []
237238
for tool in tools.values():
238-
if isinstance(tool, ToolBuiltIn):
239+
if isinstance(tool, ToolWebSearch):
240+
tool_params.append(tool.get_definition("openai"))
241+
elif isinstance(tool, ToolWebFetch):
242+
raise ValueError(
243+
"Web fetch is not supported by OpenAI. "
244+
"Use the MCP Fetch server instead via "
245+
"chat.register_mcp_tools_stdio_async(). "
246+
"See help(tool_web_fetch) for details."
247+
)
248+
elif isinstance(tool, ToolBuiltIn):
239249
tool_params.append(cast("ToolParam", tool.definition))
240250
else:
241251
schema = tool.schema

0 commit comments

Comments
 (0)