Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
920037c
TimedEntry.time is now publicly accessible
ppinchuk Aug 26, 2026
871e4b1
time prop no longer writable
ppinchuk Aug 26, 2026
100ffda
Entry value no longer writable
ppinchuk Aug 26, 2026
c2f103b
Adding an entry now returns its time
ppinchuk Aug 26, 2026
73314c4
Base LLM service now accepts a rate stats tracker
ppinchuk Aug 26, 2026
32bc6be
`OpenAIService` now accepts a rate stats tracker
ppinchuk Aug 26, 2026
8e82c29
`OpenAIService` now tracks rate stats
ppinchuk Aug 26, 2026
f6bf239
`UsageUpdater` now takes two inputs to call method
ppinchuk Aug 26, 2026
dcd07f3
Docs
ppinchuk Aug 26, 2026
89aad85
Service now accepts a rate tracker
ppinchuk Aug 26, 2026
c67462c
Update param name
ppinchuk Aug 26, 2026
5437e6b
Add rate tracker
ppinchuk Aug 26, 2026
9c5da25
Extraction plugin keeps a rate tracker
ppinchuk Aug 26, 2026
d71851d
`LLMRateTracker` no longer need to know models ahead of time
ppinchuk Aug 26, 2026
d00e8ab
`BaseRequest` carries a rate tracker
ppinchuk Aug 26, 2026
a87ec75
Fix directive
ppinchuk Aug 26, 2026
3176eec
Make tracker read only
ppinchuk Aug 26, 2026
2a622b3
Runtime exposes tracker
ppinchuk Aug 26, 2026
634d964
llm_usage_rates now written to meta
ppinchuk Aug 26, 2026
369c6cb
Pass through rate info to meta
ppinchuk Aug 26, 2026
840f225
Attempt to recover rates from partial run
ppinchuk Aug 26, 2026
8af45e5
Add/fix tests
ppinchuk Aug 26, 2026
ba1f263
Add concurrent request tracking
ppinchuk Aug 26, 2026
a88c7fc
Rename class
ppinchuk Aug 26, 2026
5dc63e4
Remove bad input
ppinchuk Aug 26, 2026
e6b6e46
Pop non-serializable arg
ppinchuk Aug 27, 2026
dc401c8
Fix input for llm call
ppinchuk Aug 27, 2026
8928755
Fix
ppinchuk Aug 27, 2026
9dbd3db
Merge remote-tracking branch 'origin/main' into pp/more_runtime_stats
ppinchuk Aug 27, 2026
c2f3822
Fix tests
ppinchuk Aug 27, 2026
280a937
Fix integration tests
ppinchuk Aug 28, 2026
fd8b385
Keep as dict while ranking
ppinchuk Aug 28, 2026
cc0acd6
Add search engines attr if available
ppinchuk Aug 28, 2026
424114f
Store an overall search engines attr
ppinchuk Aug 28, 2026
ef0a323
Add tests
ppinchuk Aug 28, 2026
d00e9dd
update elm dep
ppinchuk Aug 28, 2026
10566b7
Update to use new elm functionality
ppinchuk Aug 28, 2026
fb36e70
Add output
ppinchuk Aug 28, 2026
b76fa66
Move key
ppinchuk Aug 28, 2026
f9a6a2d
Add info retrieval method
ppinchuk Aug 28, 2026
d0f1c5b
Add doc from steps to extracted doc
ppinchuk Aug 28, 2026
371f0b6
Update tests
ppinchuk Aug 28, 2026
56f914a
MInor refactor
ppinchuk Aug 28, 2026
088cca5
Rename var for less verbosity
ppinchuk Aug 28, 2026
d2cd2be
Merge remote-tracking branch 'origin/main' into pp/more_runtime_stats
ppinchuk Aug 30, 2026
e3efbfd
Merge branch 'pp/more_runtime_stats' into pp/se_in_attrs
ppinchuk Aug 30, 2026
704f635
Minor doc changes
ppinchuk Aug 30, 2026
f7785e8
Merge remote-tracking branch 'origin/main' into pp/se_in_attrs
ppinchuk Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions compass/_cli/finalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from compass.utilities.io import load_config
from compass.utilities.jurisdictions import Jurisdiction
from compass.utilities.finalize import save_run_meta, doc_infos_to_db, save_db
from compass.services.usage import LLM_USAGE_RATES_KEY
from compass.pipeline import build_models


Expand Down Expand Up @@ -94,10 +95,19 @@ def finalize(ctx, config):
num_jurisdictions_found=num_jurisdictions_found,
total_cost=total_cost,
models=models,
llm_usage_rates=_load_llm_usage_rates(dirs.out),
)
console.print(f"✅ Finalized COMPASS run in {dirs.out!s}!")


def _load_llm_usage_rates(out_dir):
"""Load persisted LLM rate statistics when available"""
usage_fp = out_dir / "usage.json"
if not usage_fp.exists():
return None
return load_config(usage_fp).get(LLM_USAGE_RATES_KEY)


def _compile_db(jurisdictions, dirs, tech):
"""Merge all jurisdiction dbs into one"""
all_doc_infos = []
Expand Down
4 changes: 2 additions & 2 deletions compass/extraction/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async def check_for_relevant_text(
If the document already contains text collected by a given
collector (i.e. the collector's ``OUT_LABEL`` is found in
``doc.attrs``), that collector will be skipped.
usage_tracker : UsageTracker, optional
usage_tracker : LLMUsageTracker, optional
Optional tracker instance to monitor token usage during
LLM calls. By default, ``None``.
min_chunks_to_process : int, optional
Expand Down Expand Up @@ -154,7 +154,7 @@ async def extract_date(doc, model_config, usage_tracker=None):
model_config : compass.llm.config.LLMConfig
Configuration describing which LLM service, splitter, and call
parameters should be used for date extraction.
usage_tracker : UsageTracker, optional
usage_tracker : LLMUsageTracker, optional
Optional tracker instance to monitor token usage during
LLM calls. By default, ``None``.

Expand Down
4 changes: 2 additions & 2 deletions compass/llm/calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(self, llm_service, usage_tracker=None, **kwargs):
----------
llm_service : Service
LLM service used for queries.
usage_tracker : UsageTracker, optional
usage_tracker : LLMUsageTracker, optional
Optional tracker instance to monitor token usage during
LLM calls. By default, ``None``.
**kwargs
Expand Down Expand Up @@ -130,7 +130,7 @@ def __init__(
LLM service used for queries.
system_message : str
System message to use for chat with LLM.
usage_tracker : UsageTracker, optional
usage_tracker : LLMUsageTracker, optional
Optional tracker instance to monitor token usage during
LLM calls. By default, ``None``.
**kwargs
Expand Down
16 changes: 11 additions & 5 deletions compass/pipeline/collection/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,11 @@ async def execute(self, *, eager_extract=False):
for step in self._unfinished_steps():
docs = await self._run_collection_step(step)
if eager_extract:
context = (
await self.workflow.extraction_workflow.extract_from_docs(
docs
)
context = await self.workflow.extraction.extract_from_docs(
docs
)
if context is not None:
return context
return self._context_with_documented_steps(context)
else:
self._collection_info = (
await self.workflow.write_collection_shard_no_fail(
Expand Down Expand Up @@ -182,6 +180,14 @@ async def _run_collection_step(self, step):
self._completed_steps.add(step.STEP_NAME)
return docs

def _context_with_documented_steps(self, context):
"""Attach collection steps to each document in the context"""
for doc in context.data_docs:
doc.attrs["from_steps"] = list(
self.de_duplicator.info(doc).from_steps
)
return context

def _log_execute_results(self):
"""Log the results of the collection execution"""
if self.de_duplicator:
Expand Down
17 changes: 17 additions & 0 deletions compass/pipeline/collection/dedupe.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ def add_docs(self, docs, *, step_name=None):
doc_info = self.data.setdefault(key, _DocInfo.from_doc(doc))
doc_info.add_step(step_name)

def info(self, doc):
"""Get the info for a given document

Parameters
----------
doc : BaseDocument
The document for which to retrieve the deduplication info.

Returns
-------
object
The deduplication info for the given document.
"""
return self.data.get(
_collection_doc_key(doc.attrs), _DocInfo(doc=doc, from_steps=[])
)


def _collection_doc_key(doc_info):
"""Build the deduplication key for a collected document"""
Expand Down
26 changes: 17 additions & 9 deletions compass/pipeline/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from abc import ABC, abstractmethod

from compass.services.openai import usage_from_response
from compass.services.usage import UsageTracker
from compass.services.usage import LLMUsageTracker
from compass.exceptions import COMPASSError, COMPASSValueError
from compass.utilities import (
compile_collection_summary_message,
Expand Down Expand Up @@ -128,6 +128,7 @@ def _create(self, jurisdiction, *, usage_tracker=None):
jurisdiction=jurisdiction,
model_configs=self.runtime.models,
usage_tracker=usage_tracker,
rate_tracker=self.runtime.rate_tracker,
)
return SingleJurisdictionRun(
self.runtime,
Expand Down Expand Up @@ -185,7 +186,7 @@ async def run(self, jurisdictions_df):
)
tasks = []
for jurisdiction in jurisdictions_from_df(jurisdictions_df):
usage_tracker = UsageTracker(
usage_tracker = LLMUsageTracker(
jurisdiction.full_name, usage_from_response
)
workflow = self._create(jurisdiction, usage_tracker=usage_tracker)
Expand Down Expand Up @@ -315,7 +316,7 @@ async def run(self, jurisdictions_df):
)
continue

usage_tracker = UsageTracker(
usage_tracker = LLMUsageTracker(
jurisdiction.full_name, usage_from_response
)
workflow = self._create(jurisdiction, usage_tracker=usage_tracker)
Expand Down Expand Up @@ -419,7 +420,9 @@ async def _finalize_extraction(
runtime, results, start_date, num_jurisdictions
):
"""Finalize process or extraction mode outputs"""
total_cost = await _compute_total_cost()
total_cost, llm_usage_rates = await _compute_total_cost(
runtime.rate_tracker
)
doc_infos = [
{
"jurisdiction": result.jurisdiction,
Expand All @@ -445,6 +448,7 @@ async def _finalize_extraction(
num_jurisdictions_found=num_docs_found,
total_cost=total_cost,
models=runtime.models,
llm_usage_rates=llm_usage_rates,
)
run_msg = compile_run_summary_message(
total_seconds=total_time,
Expand All @@ -457,9 +461,13 @@ async def _finalize_extraction(
return run_msg


async def _compute_total_cost():
"""Compute total cost from tracked usage"""
total_usage = await UsageUpdater.call(None)
async def _compute_total_cost(rate_tracker):
"""Compute total cost and load LLM rate statistics from usage"""
total_usage = await UsageUpdater.call(None, rate_tracker)
if not total_usage:
return 0
return compute_total_cost_from_usage(total_usage)
return 0, None

return (
compute_total_cost_from_usage(total_usage),
total_usage.get(rate_tracker.label),
)
19 changes: 15 additions & 4 deletions compass/pipeline/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from elm.web.search.run import SEARCH_ENGINE_OPTIONS

from compass.llm import OpenAIConfig
from compass.services.usage import LLMRateTracker
from compass.utilities.enums import COMPASSRunMode, LLMTasks
from compass.utilities.io import load_config
from compass.exceptions import COMPASSValueError
Expand Down Expand Up @@ -691,13 +692,22 @@ def __init__( # ruff:ignore[too-many-arguments]
)
self.user_model_input = model
self.llm_costs = llm_costs
self._rate_tracker = LLMRateTracker()

@cached_property
def models(self):
"""dict: Mapping of LLM task to OpenAIConfig for this request"""
if not self.user_model_input:
return {}
return build_models(self.user_model_input)

return build_models(
self.user_model_input, rate_tracker=self._rate_tracker
)

@property
def rate_tracker(self):
"""LLMRateTracker: Rate tracker for LLM calls"""
return self._rate_tracker


class ProcessRequest(BaseRequest):
Expand Down Expand Up @@ -1287,7 +1297,7 @@ def __bool__(self):
return self.ord_db_fp is not None


def build_models(user_input, *, allow_empty=False):
def build_models(user_input, *, allow_empty=False, rate_tracker=None):
"""[NOT PUBLIC API] Build configured model registry"""
if user_input is None:
return {} if allow_empty else {LLMTasks.DEFAULT: OpenAIConfig()}
Expand All @@ -1297,22 +1307,23 @@ def build_models(user_input, *, allow_empty=False):

caller_instances = {}
for raw_kwargs in user_input:
for task, model_config in _config_for_tasks(raw_kwargs):
for task, model_config in _config_for_tasks(raw_kwargs, rate_tracker):
_verify_task_not_duplicate(task, caller_instances)
caller_instances[task] = model_config

_verify_default_case_handled(caller_instances, allow_empty)
return caller_instances


def _config_for_tasks(kwargs):
def _config_for_tasks(kwargs, rate_tracker=None):
"""Yield (task, model_config) pairs for the given raw kwargs"""
kwargs = dict(kwargs)
tasks = kwargs.pop("tasks", LLMTasks.DEFAULT)
if isinstance(tasks, str):
tasks = [tasks]

model_config = OpenAIConfig(**kwargs)
model_config.llm_call_kwargs.update({"rate_tracker": rate_tracker})
for task in tasks:
yield task, model_config

Expand Down
18 changes: 7 additions & 11 deletions compass/pipeline/jurisdiction.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def __init__(
Configured extraction plugin instance responsible for
parsing collected documents and persisting structured
output for this jurisdiction.
usage_tracker : UsageTracker, optional
usage_tracker : LLMUsageTracker, optional
Optional tracker instance used to accumulate token usage
and cost information for LLM calls made during the
jurisdiction workflow. By default, ``None``.
Expand Down Expand Up @@ -81,8 +81,8 @@ def __init__(
self.perform_website_search = perform_website_search
self.jurisdiction_website = jurisdiction.website_url
self.last_scrape_results = []
self.extraction_workflow = DocumentExtraction(self)
self.collection_workflow = DocumentCollection(self)
self.collection = DocumentCollection(self)
self.extraction = DocumentExtraction(self)

async def process(self):
"""Run process mode for one jurisdiction
Expand All @@ -101,8 +101,8 @@ async def process(self):
self.jurisdiction.code,
)
try:
extraction_context = await self.collection_workflow.execute(
eager_extract=True,
extraction_context = await self.collection.execute(
eager_extract=True
)
finally:
await self.extractor.record_usage()
Expand Down Expand Up @@ -143,9 +143,7 @@ async def collect(self):
self.jurisdiction.full_name,
)

collection_info = await self.collection_workflow.execute(
eager_extract=False
)
collection_info = await self.collection.execute(eager_extract=False)

logger.info(
"Completed collection for jurisdiction: %s",
Expand Down Expand Up @@ -187,9 +185,7 @@ async def extract_from_collection_info(self, collection_info):
collection_info, task_name=self.jurisdiction.full_name
)
docs = [doc for doc in docs if doc is not None]
extraction_context = (
await self.extraction_workflow.extract_from_docs(docs)
)
extraction_context = await self.extraction.extract_from_docs(docs)
finally:
await self.extractor.record_usage()
await _record_jurisdiction_info(
Expand Down
1 change: 1 addition & 0 deletions compass/pipeline/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def __init__(self, request):
self.mode = request.MODE
self.tech = request.tech
self.models = request.models
self.rate_tracker = request.rate_tracker
self.search_params = request.search_settings
self.log_level = _normalize_log_level(
request.runtime_settings.log_level
Expand Down
23 changes: 18 additions & 5 deletions compass/plugin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ class BaseExtractionPlugin(ABC):
implementer must define most functionality on their own.
"""

def __init__(self, jurisdiction, model_configs, usage_tracker=None):
def __init__(
self,
jurisdiction,
model_configs,
usage_tracker=None,
rate_tracker=None,
):
"""

Parameters
Expand All @@ -26,13 +32,17 @@ def __init__(self, jurisdiction, model_configs, usage_tracker=None):
:class:`~compass.utilities.enums.LLMTasks` and values are
:class:`~compass.llm.config.LLMConfig` instances to be used
for those tasks.
usage_tracker : UsageTracker, optional
usage_tracker : LLMUsageTracker, optional
Usage tracker instance that can be used to record the LLM
call cost. By default, ``None``.
rate_tracker : LLMRateTracker, optional
Run-level LLM rate tracker used to persist LLM request and
token rate statistics. By default, ``None``.
"""
self.jurisdiction = jurisdiction
self.model_configs = model_configs
self.usage_tracker = usage_tracker
self.rate_tracker = rate_tracker

JURISDICTION_DATA_FP = None
""":term:`path-like <path-like object>`: Path to jurisdiction CSV
Expand Down Expand Up @@ -169,12 +179,15 @@ def save_structured_data(cls, doc_infos, out_dir):

async def record_usage(self):
"""Persist usage tracking data when a tracker is available"""
if self.usage_tracker is None:
if self.usage_tracker is None and self.rate_tracker is None:
return

total_usage = await UsageUpdater.call(self.usage_tracker)
total_usage = await UsageUpdater.call(
self.usage_tracker, self.rate_tracker
)
total_cost = compute_total_cost_from_usage(total_usage)
COMPASS_PB.update_total_cost(total_cost, replace=True)

def validate_plugin_configuration(self): # ruff:ignore[empty-method-without-abstract-decorator]
# ruff:ignore[empty-method-without-abstract-decorator]
def validate_plugin_configuration(self):
"""[NOT PUBLIC API] Validate plugin is properly configured"""
Loading
Loading