Source code for toolscore.adapters.base
"""Base adapter interface for trace format conversion."""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any
[docs]
@dataclass
class ToolCall:
"""Represents a single tool call in a trace.
Attributes:
tool: Name of the tool/function called.
args: Arguments provided to the tool. ``None`` carries a specific
meaning for **gold/expected** calls: "do not check arguments" —
the tool name must match but its arguments are ignored. For
**actual/trace** calls, ``None`` simply means no arguments were
recorded and is treated as an empty mapping. Adapters that parse
real traces always set a concrete dict, so ``None`` in practice
only originates from gold specifications that omit ``args``.
result: Result returned by the tool (optional).
timestamp: Unix timestamp of when the call was made (optional).
duration: Duration of the call in seconds (optional).
cost: Cost associated with this call in USD (optional).
metadata: Additional metadata about the call. Failures are recorded as
``metadata["is_error"]`` and/or ``metadata["error"]`` (the message);
see :attr:`is_error`.
"""
tool: str
args: dict[str, Any] | None = None
result: Any = None
timestamp: float | None = None
duration: float | None = None
cost: float | None = None
metadata: dict[str, Any] = field(default_factory=dict)
[docs]
def __post_init__(self) -> None:
"""Validate tool call data after initialization.
``args`` is intentionally **not** coerced from ``None`` to ``{}`` so
that gold calls can express "do not check arguments" (``args is None``)
distinctly from "expect exactly zero arguments" (``args == {}``).
Consumers that need a concrete mapping use ``call.args or {}``.
"""
if not self.tool:
raise ValueError("Tool name cannot be empty")
@property
def is_error(self) -> bool:
"""Whether the call failed.
Read from ``metadata["is_error"]`` or a non-empty ``metadata["error"]``,
which is how the MCP and custom adapters and :func:`toolscore.evaluate`
record failures. A call with no error information is not an error.
"""
if self.metadata.get("is_error") is True:
return True
error = self.metadata.get("error")
return bool(error) and error is not False
[docs]
class BaseAdapter(ABC):
"""Abstract base class for trace format adapters.
All trace format adapters must inherit from this class and implement
the parse method to convert provider-specific formats into a normalized
list of ToolCall objects.
"""
[docs]
@abstractmethod
def parse(self, trace_data: dict[str, Any] | list[Any]) -> list[ToolCall]:
"""Parse trace data into a normalized list of tool calls.
Args:
trace_data: The raw trace data from the LLM provider.
Returns:
A list of ToolCall objects in chronological order.
Raises:
ValueError: If the trace data is invalid or cannot be parsed.
"""
pass
def _validate_trace_data(self, trace_data: Any) -> None:
"""Validate that trace data is in expected format.
Args:
trace_data: The raw trace data to validate.
Raises:
ValueError: If trace data is None or not dict/list.
"""
if trace_data is None:
raise ValueError("Trace data cannot be None")
if not isinstance(trace_data, (dict, list)):
raise ValueError(f"Trace data must be dict or list, got {type(trace_data).__name__}")