research-agent / tools.py
JamesDominiqueAI
Initial Hugging Face Space deployment
7843baa
Raw
History Blame Contribute Delete
3.26 kB
"""
tools.py
--------
Tool definitions sent to the Anthropic API and any local helper utilities.
The web_search tool is a *hosted* Anthropic tool β€” we declare it here and
the API executes it on our behalf. No scraping credentials needed.
"""
from __future__ import annotations
import json
import logging
from typing import Any
logger = logging.getLogger(__name__)
# ── Tool Schemas ─────────────────────────────────────────────────────────────
WEB_SEARCH_TOOL: dict[str, Any] = {
"type": "web_search_20250305",
"name": "web_search",
}
ALL_TOOLS: list[dict[str, Any]] = [WEB_SEARCH_TOOL]
# ── Tool-use Event Parser ─────────────────────────────────────────────────────
class ToolEvent:
"""Lightweight representation of a tool-use or tool-result block."""
def __init__(self, block: dict[str, Any]):
self.type: str = block.get("type", "")
self.name: str = block.get("name", "")
self.tool_use_id: str = block.get("id", block.get("tool_use_id", ""))
self.input: dict = block.get("input", {})
self.content: Any = block.get("content", "")
# ── helpers ──
@property
def is_tool_use(self) -> bool:
# server-side web_search returns type "tool_use" or "server_tool_use"
return self.type in ("tool_use", "server_tool_use")
@property
def is_web_search(self) -> bool:
return self.is_tool_use and self.name == "web_search"
@property
def search_query(self) -> str:
return self.input.get("query", "")
def __repr__(self) -> str: # pragma: no cover
return f"<ToolEvent type={self.type!r} name={self.name!r}>"
def parse_tool_events(content_blocks: list[dict]) -> list[ToolEvent]:
"""Convert raw API content blocks into ToolEvent objects."""
return [ToolEvent(b) for b in content_blocks]
def build_tool_result_messages(content_blocks: list[dict]) -> list[dict] | None:
"""
When stop_reason == 'tool_use', the API expects us to return a user message
containing tool_result blocks for every tool_use block.
For the hosted web_search tool the API handles execution itself, so we
return a minimal acknowledgement that keeps the conversation loop alive.
"""
tool_uses = [b for b in content_blocks if b.get("type") == "tool_use"]
if not tool_uses:
return None
tool_results = [
{
"type": "tool_result",
"tool_use_id": tu["id"],
"content": "Search executed successfully.",
}
for tu in tool_uses
]
return [{"role": "user", "content": tool_results}]
def extract_text_blocks(content_blocks: list[dict]) -> str:
"""Concatenate all text-type blocks from a response."""
return "\n".join(
b.get("text", "") for b in content_blocks if b.get("type") == "text"
)
def safe_json(obj: Any) -> str:
"""Best-effort JSON serialisation for logging."""
try:
return json.dumps(obj, indent=2, default=str)
except Exception:
return str(obj)