Source code for loom.etl.declarative._with_agent
"""Agent declaration for ETL steps.
:class:`WithAgent` names an agent of the runner's ``ai:`` section. The
executor injects an :class:`~loom.etl.AgentMapper` bound to it into
``execute()``, as a keyword argument named like the class attribute. It is
not a frame source: no reader is involved and it never becomes a
:class:`~loom.etl.declarative.source.SourceSpec`.
"""
from __future__ import annotations
import numbers
import operator
import warnings
from decimal import Decimal
from typing import Any
from loom.etl.declarative.source._from_config import type_label
[docs]
class WithAgent:
"""Declare an agent injected into ``execute()`` to answer rows of a frame.
Args:
name: Agent name, as its artifact declares it.
output: Type every answer decodes into; must be the artifact's own
``type_ref`` output.
max_usd: Budget of one execution of the step, in USD, as a
:class:`~decimal.Decimal` or an integral number (an ``int`` or
another :class:`numbers.Integral`, such as ``numpy.int64``),
which is converted exactly. A ``float`` is deprecated until loom 3.0: it emits a
:class:`DeprecationWarning` and is converted through its
``str()``, so ``0.1`` becomes ``Decimal("0.1")``. ``None`` spends
without a ceiling beyond each run's own ``policies.max_usd``.
Raises:
TypeError: When *max_usd* is not ``None``, a :class:`~decimal.Decimal`,
an integral number or a ``float``; a ``bool`` is refused.
ValueError: When *name* is empty or *max_usd* is not a finite
amount greater than zero.
Example::
class LabelReplies(ETLStep[DailyParams]):
replies = FromTable("prep.seller_reply")
labeller = WithAgent("seller_reply", output=SellerReply, max_usd=Decimal("2"))
target = IntoTable("fact.seller_reply").upsert(keys=("message_id", "agent_version"))
def execute(self, params, *, replies, labeller: AgentMapper) -> pl.DataFrame:
return labeller.map(replies, keys=("message_id",), prompt="text")
"""
__slots__ = ("_max_usd", "_name", "_output")
def __init__(
self,
name: str,
*,
output: type[Any],
max_usd: Decimal | int | float | None = None,
) -> None:
if not name:
raise ValueError("WithAgent name must not be empty")
self._name = name
self._output = output
self._max_usd = None if max_usd is None else _budget(max_usd)
@property
def name(self) -> str:
"""Agent name."""
return self._name
@property
def output(self) -> type[Any]:
"""Type every answer decodes into."""
return self._output
@property
def max_usd(self) -> Decimal | None:
"""Budget of one execution of the step, or ``None``."""
return self._max_usd
def __repr__(self) -> str:
return f"WithAgent({self._name!r}, {type_label(self._output)})"
def _budget(max_usd: object) -> Decimal:
"""Return *max_usd* as a finite :class:`~decimal.Decimal` above zero."""
if isinstance(max_usd, Decimal):
amount = max_usd
elif isinstance(max_usd, bool):
raise TypeError("WithAgent max_usd must be a Decimal or an int, got bool")
elif isinstance(max_usd, numbers.Integral):
amount = Decimal(operator.index(max_usd))
elif isinstance(max_usd, float):
warnings.warn(
"WithAgent(max_usd=<float>) is deprecated and will be removed in loom 3.0; "
'pass a Decimal, such as Decimal("2.50"), or an int instead.',
DeprecationWarning,
stacklevel=3,
)
amount = Decimal(str(max_usd))
else:
raise TypeError(
f"WithAgent max_usd must be a Decimal or an int, got {type(max_usd).__name__}"
)
if not amount.is_finite() or amount <= 0:
raise ValueError(f"WithAgent max_usd must be a finite amount above zero, got {max_usd}")
return amount