backend/src/tools/base_tool.py
"""BaseTool contract — one external data capability, no business/routing/LLM logic.""" from __future__ import annotations import logging from abc import ABC, abstractmethod from typing import Any from models.tools import ToolResult from observability import log_event, record_metric logger = logging.getLogger("agentic.request") class BaseTool(ABC): """Abstract Tool: fetch external data and return a typed ``ToolResult``.""" name: str @abstractmethod async def call(self, **kwargs: Any) -> ToolResult: """Invoke the Tool. Must not raise past this boundary — return ``success=False``.""" def _emit_start( self, *, trace_id: str | None = None, fields: dict[str, Any] | None = None, ) -> None: log_event( "call_start", component=type(self).__name__, trace_id=trace_id, fields={"tool": self.name, **(fields or {})}, ) def _emit_complete( self, *, success: bool, latency_ms: float, source: str, trace_id: str | None = None, error: str | None = None, fields: dict[str, Any] | None = None, ) -> None: event_fields: dict[str, Any] = { "tool": self.name, "success": success, "latency_ms": round(latency_ms, 2), "source": source, **(fields or {}), } if error is not None: event_fields["error"] = error log_event( "call_complete", component=type(self).__name__, trace_id=trace_id, fields=event_fields, level=logging.INFO if success else logging.ERROR, ) record_metric( "tool.latency_ms", latency_ms, tags={"tool": self.name, "success": "true" if success else "false"}, ) record_metric( "tool.success", 1.0 if success else 0.0, tags={"tool": self.name}, )
Follow the work.
Occasional updates on SignalFoundry, MarketCompass, and what we are building at CompassFoundry Labs.