Source code for compass.services.usage
"""Ordinances usage tracking utilities"""
import time
import logging
from collections import UserDict, deque
from functools import total_ordering
logger = logging.getLogger(__name__)
[docs]
@total_ordering
class TimedEntry:
"""An entry that performs comparisons based on time added, not value
Examples
--------
>>> a = TimedEntry(100)
>>> a > 1000
True
"""
def __init__(self, value):
"""
Parameters
----------
value : obj
Some value to store as an entry.
"""
self.value = value
self._time = time.monotonic()
def __eq__(self, other):
return self._time == other
def __lt__(self, other):
return self._time < other
def __hash__(self):
return hash((self.value, self._time))
[docs]
class TimeBoundedUsageTracker:
"""Track usage of a resource over time
This class wraps a double-ended queue, and any inputs older than
a certain time are dropped. Those values are also subtracted from
the running total.
References
----------
https://stackoverflow.com/questions/51485656/efficient-time-bound-queue-in-python
"""
def __init__(self, max_seconds=70):
"""
Parameters
----------
max_seconds : int, optional
Maximum age in seconds of an element before it is dropped
from consideration. By default, ``65``.
"""
self.max_seconds = max_seconds
self._total = 0
self._q = deque()
@property
def total(self):
"""float: Total of all entries younger than `max_seconds`"""
self._discard_old_values()
return self._total
[docs]
def add(self, value):
"""Add a value to track
Parameters
----------
value : int or float
A new value to add to the queue. It's total will be added to
the running total, and it will live for `max_seconds` before
being discarded.
"""
self._q.append(TimedEntry(value))
self._total += value
def _discard_old_values(self):
"""Discard 'old' values from the queue"""
cutoff_time = time.monotonic() - self.max_seconds
try:
while self._q[0] < cutoff_time:
self._total -= self._q.popleft().value
except IndexError:
pass
[docs]
class UsageTracker(UserDict):
"""Rate or AIP usage tracker"""
UNKNOWN_MODEL_LABEL = "unknown_model"
"""Label used in the usage dictionary for unknown models"""
def __init__(self, label, response_parser):
"""
Parameters
----------
label : str
Top-level label to use when adding this usage information to
another dictionary.
response_parser : callable
A callable that takes the current usage info (in dictionary
format) and an LLm response as inputs, updates the usage
dictionary with usage info based on the response, and
returns the updated dictionary. See, for example,
:func:`compass.services.openai.usage_from_response`.
"""
super().__init__()
self.label = label
self.response_parser = response_parser
[docs]
def add_to(self, other):
"""Add the contents of this usage information to another dict
The contents of this dictionary are stored under the `label`
key that this object was initialized with.
Parameters
----------
other : dict
A dictionary to add the contents of this one to.
"""
other.update({self.label: {**self, "tracker_totals": self.totals}})
@property
def totals(self):
"""Compute total usage across all sub-labels
Returns
-------
dict
Dictionary containing usage information totaled across all
sub-labels.
"""
totals = {}
for model, model_usage in self.items():
total_model_usage = totals[model] = {}
for report in model_usage.values():
try:
sub_label_report = report.items()
except AttributeError:
continue
for tracked_value, count in sub_label_report:
total_model_usage[tracked_value] = (
total_model_usage.get(tracked_value, 0) + count
)
return totals
[docs]
def update_from_model(
self, model=None, response=None, sub_label="default"
):
"""Update usage from a model response
Parameters
----------
model : str, optional
Name of model that usage is being recorded for. If ``None``
or empty string, the usage will be placed under the
:obj:`UsageTracker.UNKNOWN_MODEL_LABEL` label.
response : object, optional
Model call response, which either contains usage information
or can be used to infer/compute usage. If ``None``, no
update is made. By default, ``None``.
sub_label : str, optional
Optional label to categorize usage under. This can be used
to track usage related to certain categories.
By default, ``"default"``.
"""
if response is None:
return
model_usage = self.setdefault(model or self.UNKNOWN_MODEL_LABEL, {})
model_usage[sub_label] = self.response_parser(
model_usage.get(sub_label, {}), response
)