Skip to content
← How we build

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.

No spam. Unsubscribe anytime.