From 920037c33bd66441de69dc92b9e0aeb73afa77b1 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 14:02:28 -0600 Subject: [PATCH 01/44] TimedEntry.time is now publicly accessible --- compass/services/usage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compass/services/usage.py b/compass/services/usage.py index a12ab2560..5456964d1 100644 --- a/compass/services/usage.py +++ b/compass/services/usage.py @@ -29,16 +29,16 @@ def __init__(self, value): Some value to store as an entry. """ self.value = value - self._time = time.perf_counter() + self.time = time.perf_counter() def __eq__(self, other): - return self._time == other + return self.time == other def __lt__(self, other): - return self._time < other + return self.time < other def __hash__(self): - return hash((self.value, self._time)) + return hash((self.value, self.time)) class TimeBoundedUsageTracker: From 871e4b1cd9600529217b687bc348d4141d9531f7 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 14:03:15 -0600 Subject: [PATCH 02/44] time prop no longer writable --- compass/services/usage.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compass/services/usage.py b/compass/services/usage.py index 5456964d1..d6de53bfa 100644 --- a/compass/services/usage.py +++ b/compass/services/usage.py @@ -29,7 +29,12 @@ def __init__(self, value): Some value to store as an entry. """ self.value = value - self.time = time.perf_counter() + self._time = time.perf_counter() + + @property + def time(self): + """float: Time the entry was added to the tracker""" + return self._time def __eq__(self, other): return self.time == other From 100ffda35092dfd133a0344c8db74ae16c027f28 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 14:03:57 -0600 Subject: [PATCH 03/44] Entry value no longer writable --- compass/services/usage.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compass/services/usage.py b/compass/services/usage.py index d6de53bfa..72b7f8da1 100644 --- a/compass/services/usage.py +++ b/compass/services/usage.py @@ -28,7 +28,7 @@ def __init__(self, value): value : object Some value to store as an entry. """ - self.value = value + self._value = value self._time = time.perf_counter() @property @@ -36,6 +36,11 @@ def time(self): """float: Time the entry was added to the tracker""" return self._time + @property + def value(self): + """object: Value that was added to the tracker""" + return self._value + def __eq__(self, other): return self.time == other From c2f103bec9855e2138f6e11fbe01013dd5f67d6f Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 14:05:34 -0600 Subject: [PATCH 04/44] Adding an entry now returns its time --- compass/services/usage.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compass/services/usage.py b/compass/services/usage.py index 72b7f8da1..12831caea 100644 --- a/compass/services/usage.py +++ b/compass/services/usage.py @@ -91,9 +91,16 @@ def add(self, value): 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. + + Returns + ------- + float + Timestamp stored with the value. """ - self._q.append(TimedEntry(value)) + entry = TimedEntry(value) + self._q.append(entry) self._total += value + return entry.time def _discard_old_values(self): """Discard 'old' values from the queue""" From 73314c41397271c289f699cc21984209aad90b73 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 14:36:44 -0600 Subject: [PATCH 05/44] Base LLM service now accepts a rate stats tracker --- compass/services/base.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/compass/services/base.py b/compass/services/base.py index 539346914..8f9b145eb 100644 --- a/compass/services/base.py +++ b/compass/services/base.py @@ -138,7 +138,14 @@ class LLMService(Service): LLM service for OpenAI models. """ - def __init__(self, model_name, rate_limit, rate_tracker, service_tag=None): + def __init__( + self, + model_name, + rate_limit, + rate_tracker, + service_tag=None, + rate_stats_tracker=None, + ): """ Parameters @@ -157,11 +164,15 @@ def __init__(self, model_name, rate_limit, rate_tracker, service_tag=None): Optional tag to use to distinguish service (i.e. make unique from other services). Must set this if multiple models with the same name are run concurrently. By default, ``None``. + rate_stats_tracker : LLMUsageRateTracker, optional + Run-wide tracker that records request and token rate + summaries. By default, ``None``. """ self.model_name = model_name self.rate_limit = rate_limit self.rate_tracker = rate_tracker self.service_tag = service_tag or "" + self.rate_stats_tracker = rate_stats_tracker @property def can_process(self): @@ -173,6 +184,12 @@ def name(self): """str: Unique service name used to pull the correct queue""" return f"{self.__class__.__name__}-{self.model_name}{self.service_tag}" + def _record_request_rate(self, timestamp): + """Record the submitted request in rate statistics""" + if self.rate_stats_tracker is None: + return + self.rate_stats_tracker.record_request(self.model_name, timestamp) + def _queue(self): """Return the service queue for this instance""" queue = get_service_queue(self.name) From 32bc6be0bf98b97625bea587b29759bc7872c803 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 14:37:15 -0600 Subject: [PATCH 06/44] `OpenAIService` now accepts a rate stats tracker --- compass/services/openai.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compass/services/openai.py b/compass/services/openai.py index 2205c4451..4a427c1e2 100644 --- a/compass/services/openai.py +++ b/compass/services/openai.py @@ -104,6 +104,7 @@ def __init__( rate_limit=1e3, rate_tracker=None, service_tag=None, + rate_stats_tracker=None, ): """ @@ -128,12 +129,16 @@ def __init__( Optional tag to use to distinguish service (i.e. make unique from other services). Must set this if multiple models with the same name are run concurrently. By default, ``None``. + rate_stats_tracker : LLMUsageRateTracker, optional + Run-wide tracker that records request and token rate + summaries. By default, ``None``. """ super().__init__( model_name=model_name, rate_limit=rate_limit, rate_tracker=rate_tracker or TimeBoundedUsageTracker(), service_tag=service_tag, + rate_stats_tracker=rate_stats_tracker, ) self.client = client From 8e82c29f3e9bff016a07e3f7612d6fe2b7816c70 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 14:37:37 -0600 Subject: [PATCH 07/44] `OpenAIService` now tracks rate stats --- compass/services/openai.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/compass/services/openai.py b/compass/services/openai.py index 4a427c1e2..a7738a14d 100644 --- a/compass/services/openai.py +++ b/compass/services/openai.py @@ -175,9 +175,11 @@ async def process( Chat GPT response as a string, or ``None`` if the call failed. """ - self._record_prompt_tokens(kwargs) + prompt_timestamp = self._record_prompt_tokens(kwargs) + self._record_request_rate(prompt_timestamp) response = await self._call_gpt(model=self.model_name, **kwargs) - self._record_completion_tokens(response) + completion_timestamp = self._record_completion_tokens(response) + self._record_token_rate(response, completion_timestamp) self._record_usage(response, usage_tracker, usage_sub_label) self._update_pb_cost(response) return _get_response_message(response) @@ -185,13 +187,25 @@ async def process( def _record_prompt_tokens(self, kwargs): """Add prompt token count to rate tracker""" num_tokens = count_tokens(kwargs.get("messages", []), self.model_name) - self.rate_tracker.add(num_tokens) + return self.rate_tracker.add(num_tokens) def _record_completion_tokens(self, response): """Add completion token count to rate tracker""" if response is None: + return None + return self.rate_tracker.add(response.usage.completion_tokens) + + def _record_token_rate(self, response, timestamp): + """Record successful-response tokens in rate statistics""" + if response is None or self.rate_stats_tracker is None: return - self.rate_tracker.add(response.usage.completion_tokens) + + tokens = ( + response.usage.prompt_tokens + response.usage.completion_tokens + ) + self.rate_stats_tracker.record_tokens( + self.model_name, tokens, timestamp + ) def _record_usage(self, response, usage_tracker, usage_sub_label): """Record token usage for user""" From f6bf2390f7b38b5bcab3610248db5ef2425a5a5e Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:00:15 -0600 Subject: [PATCH 08/44] `UsageUpdater` now takes two inputs to call method --- compass/services/threaded.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/compass/services/threaded.py b/compass/services/threaded.py index dbe5ab562..1a4e6d0a2 100644 --- a/compass/services/threaded.py +++ b/compass/services/threaded.py @@ -416,7 +416,7 @@ def can_process(self): """bool: ``True`` if file not currently being written to""" return not self._is_processing - async def process(self, tracker): + async def process(self, usage_tracker, rate_tracker): """Add usage from tracker to file Any existing usage info in the file will remain unchanged @@ -425,9 +425,14 @@ async def process(self, tracker): Parameters ---------- - tracker : UsageTracker + usage_tracker : UsageTracker A usage tracker instance that contains usage info to be - added to output file. + added to output file. Can also be ``None`` to not record + usage. + rate_tracker : LLMRateTracker + Run-level LLM rate tracker to serialize with this usage + update. Can also be ``None`` to not record + rates. Returns ------- @@ -438,7 +443,11 @@ async def process(self, tracker): try: loop = asyncio.get_running_loop() out = await loop.run_in_executor( - self.pool, _dump_usage, self.usage_fp, tracker + self.pool, + _dump_usage, + self.usage_fp, + usage_tracker, + rate_tracker, ) finally: self._is_processing = False @@ -579,7 +588,7 @@ async def process(self, func, *args): return await loop.run_in_executor(self.pool, func, *args) -def _dump_usage(fp, tracker): +def _dump_usage(fp, usage_tracker, rate_tracker): """Dump usage to an existing file""" if not Path(fp).exists(): usage_info = {} @@ -587,9 +596,13 @@ def _dump_usage(fp, tracker): with Path.open(fp, encoding="utf-8") as fh: usage_info = json.load(fh) - if tracker is not None: - tracker.add_to(usage_info) + if usage_tracker is not None: + usage_tracker.add_to(usage_info) + + if rate_tracker is not None: + rate_tracker.add_to(usage_info) + if usage_tracker is not None or rate_tracker is not None: with Path.open(fp, "w", encoding="utf-8") as fh: json.dump(usage_info, fh, indent=4) From dcd07f3be6f7414561aaba2632a5aa5229e7a0bc Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:00:49 -0600 Subject: [PATCH 09/44] Docs --- compass/services/base.py | 2 +- compass/services/openai.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compass/services/base.py b/compass/services/base.py index 8f9b145eb..8a17cfa2f 100644 --- a/compass/services/base.py +++ b/compass/services/base.py @@ -164,7 +164,7 @@ def __init__( Optional tag to use to distinguish service (i.e. make unique from other services). Must set this if multiple models with the same name are run concurrently. By default, ``None``. - rate_stats_tracker : LLMUsageRateTracker, optional + rate_stats_tracker : LLMRateTracker, optional Run-wide tracker that records request and token rate summaries. By default, ``None``. """ diff --git a/compass/services/openai.py b/compass/services/openai.py index a7738a14d..ff8674bfe 100644 --- a/compass/services/openai.py +++ b/compass/services/openai.py @@ -129,7 +129,7 @@ def __init__( Optional tag to use to distinguish service (i.e. make unique from other services). Must set this if multiple models with the same name are run concurrently. By default, ``None``. - rate_stats_tracker : LLMUsageRateTracker, optional + rate_stats_tracker : LLMRateTracker, optional Run-wide tracker that records request and token rate summaries. By default, ``None``. """ From 89aad8575ee974343d0484e43d66652326a8b0b7 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:06:42 -0600 Subject: [PATCH 10/44] Service now accepts a rate tracker --- compass/services/base.py | 19 +------------------ compass/services/openai.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/compass/services/base.py b/compass/services/base.py index 8a17cfa2f..539346914 100644 --- a/compass/services/base.py +++ b/compass/services/base.py @@ -138,14 +138,7 @@ class LLMService(Service): LLM service for OpenAI models. """ - def __init__( - self, - model_name, - rate_limit, - rate_tracker, - service_tag=None, - rate_stats_tracker=None, - ): + def __init__(self, model_name, rate_limit, rate_tracker, service_tag=None): """ Parameters @@ -164,15 +157,11 @@ def __init__( Optional tag to use to distinguish service (i.e. make unique from other services). Must set this if multiple models with the same name are run concurrently. By default, ``None``. - rate_stats_tracker : LLMRateTracker, optional - Run-wide tracker that records request and token rate - summaries. By default, ``None``. """ self.model_name = model_name self.rate_limit = rate_limit self.rate_tracker = rate_tracker self.service_tag = service_tag or "" - self.rate_stats_tracker = rate_stats_tracker @property def can_process(self): @@ -184,12 +173,6 @@ def name(self): """str: Unique service name used to pull the correct queue""" return f"{self.__class__.__name__}-{self.model_name}{self.service_tag}" - def _record_request_rate(self, timestamp): - """Record the submitted request in rate statistics""" - if self.rate_stats_tracker is None: - return - self.rate_stats_tracker.record_request(self.model_name, timestamp) - def _queue(self): """Return the service queue for this instance""" queue = get_service_queue(self.name) diff --git a/compass/services/openai.py b/compass/services/openai.py index ff8674bfe..dc1d65ab1 100644 --- a/compass/services/openai.py +++ b/compass/services/openai.py @@ -104,7 +104,6 @@ def __init__( rate_limit=1e3, rate_tracker=None, service_tag=None, - rate_stats_tracker=None, ): """ @@ -129,22 +128,19 @@ def __init__( Optional tag to use to distinguish service (i.e. make unique from other services). Must set this if multiple models with the same name are run concurrently. By default, ``None``. - rate_stats_tracker : LLMRateTracker, optional - Run-wide tracker that records request and token rate - summaries. By default, ``None``. """ super().__init__( model_name=model_name, rate_limit=rate_limit, rate_tracker=rate_tracker or TimeBoundedUsageTracker(), service_tag=service_tag, - rate_stats_tracker=rate_stats_tracker, ) self.client = client async def process( self, usage_tracker=None, + rate_tracker=None, usage_sub_label=LLMUsageCategory.DEFAULT, **kwargs, ): @@ -161,6 +157,9 @@ async def process( UsageTracker instance. Providing this input will update your tracker with this call's token usage info. By default, ``None``. + rate_tracker : LLMRateTracker, optional + Run-wide tracker that records request and token rate + summaries. By default, ``None``. usage_sub_label : str, optional Optional label to categorize usage under. This can be used to track usage related to certain categories. @@ -176,10 +175,10 @@ async def process( failed. """ prompt_timestamp = self._record_prompt_tokens(kwargs) - self._record_request_rate(prompt_timestamp) + self._record_request_rate(prompt_timestamp, rate_tracker) response = await self._call_gpt(model=self.model_name, **kwargs) completion_timestamp = self._record_completion_tokens(response) - self._record_token_rate(response, completion_timestamp) + self._record_token_rate(response, completion_timestamp, rate_tracker) self._record_usage(response, usage_tracker, usage_sub_label) self._update_pb_cost(response) return _get_response_message(response) @@ -189,23 +188,27 @@ def _record_prompt_tokens(self, kwargs): num_tokens = count_tokens(kwargs.get("messages", []), self.model_name) return self.rate_tracker.add(num_tokens) + def _record_request_rate(self, timestamp, rate_tracker): + """Record the submitted request in rate statistics""" + if rate_tracker is None: + return + rate_tracker.record_request(self.model_name, timestamp) + def _record_completion_tokens(self, response): """Add completion token count to rate tracker""" if response is None: return None return self.rate_tracker.add(response.usage.completion_tokens) - def _record_token_rate(self, response, timestamp): + def _record_token_rate(self, response, timestamp, rate_tracker): """Record successful-response tokens in rate statistics""" - if response is None or self.rate_stats_tracker is None: + if response is None or rate_tracker is None: return tokens = ( response.usage.prompt_tokens + response.usage.completion_tokens ) - self.rate_stats_tracker.record_tokens( - self.model_name, tokens, timestamp - ) + rate_tracker.record_tokens(self.model_name, tokens, timestamp) def _record_usage(self, response, usage_tracker, usage_sub_label): """Record token usage for user""" From c67462cb0833cd86ec9ddbec4e646ef3434acf19 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:09:18 -0600 Subject: [PATCH 11/44] Update param name --- compass/services/base.py | 10 ++++++---- compass/services/openai.py | 10 +++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/compass/services/base.py b/compass/services/base.py index 539346914..7fac14036 100644 --- a/compass/services/base.py +++ b/compass/services/base.py @@ -138,7 +138,9 @@ class LLMService(Service): LLM service for OpenAI models. """ - def __init__(self, model_name, rate_limit, rate_tracker, service_tag=None): + def __init__( + self, model_name, rate_limit, timed_tracker, service_tag=None + ): """ Parameters @@ -150,7 +152,7 @@ def __init__(self, model_name, rate_limit, rate_tracker, service_tag=None): if the rate tracker is set to compute the total over minute-long intervals, this value should be the max usage per minute. - rate_tracker : TimeBoundedUsageTracker + timed_tracker : TimeBoundedUsageTracker Instance used to track usage per time interval and compare to `rate_limit` input. service_tag : str, optional @@ -160,13 +162,13 @@ def __init__(self, model_name, rate_limit, rate_tracker, service_tag=None): """ self.model_name = model_name self.rate_limit = rate_limit - self.rate_tracker = rate_tracker + self.timed_tracker = timed_tracker self.service_tag = service_tag or "" @property def can_process(self): """bool: Check if usage is under the rate limit""" - return self.rate_tracker.total < self.rate_limit + return self.timed_tracker.total < self.rate_limit @property def name(self): diff --git a/compass/services/openai.py b/compass/services/openai.py index dc1d65ab1..540112af2 100644 --- a/compass/services/openai.py +++ b/compass/services/openai.py @@ -102,7 +102,7 @@ def __init__( client, model_name, rate_limit=1e3, - rate_tracker=None, + timed_tracker=None, service_tag=None, ): """ @@ -119,7 +119,7 @@ def __init__( Token rate limit (typically per minute, but the time interval is ultimately controlled by the `rate_tracker` instance). By default, ``1e3``. - rate_tracker : TimeBoundedUsageTracker, optional + timed_tracker : TimeBoundedUsageTracker, optional Instance used to track usage per time interval and compare to `rate_limit` input. If ``None``, a TimeBoundedUsageTracker instance is created with default @@ -132,7 +132,7 @@ def __init__( super().__init__( model_name=model_name, rate_limit=rate_limit, - rate_tracker=rate_tracker or TimeBoundedUsageTracker(), + timed_tracker=timed_tracker or TimeBoundedUsageTracker(), service_tag=service_tag, ) self.client = client @@ -186,7 +186,7 @@ async def process( def _record_prompt_tokens(self, kwargs): """Add prompt token count to rate tracker""" num_tokens = count_tokens(kwargs.get("messages", []), self.model_name) - return self.rate_tracker.add(num_tokens) + return self.timed_tracker.add(num_tokens) def _record_request_rate(self, timestamp, rate_tracker): """Record the submitted request in rate statistics""" @@ -198,7 +198,7 @@ def _record_completion_tokens(self, response): """Add completion token count to rate tracker""" if response is None: return None - return self.rate_tracker.add(response.usage.completion_tokens) + return self.timed_tracker.add(response.usage.completion_tokens) def _record_token_rate(self, response, timestamp, rate_tracker): """Record successful-response tokens in rate statistics""" From 5437e6b8c90b07f50230e514467759b19b327d06 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:20:16 -0600 Subject: [PATCH 12/44] Add rate tracker --- compass/services/usage.py | 169 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/compass/services/usage.py b/compass/services/usage.py index 12831caea..8e97ba13d 100644 --- a/compass/services/usage.py +++ b/compass/services/usage.py @@ -5,8 +5,11 @@ from collections import UserDict, deque from functools import total_ordering +from compass.exceptions import COMPASSValueError + logger = logging.getLogger(__name__) +LLM_USAGE_RATES_KEY = "_llm_usage_rates" @total_ordering @@ -112,6 +115,172 @@ def _discard_old_values(self): pass +class _OnlineUsageSummary: + """Track online minimum, mean, and maximum values""" + + def __init__(self, count=0, total=0, minimum=None, maximum=None): + self._count = count + self._total = total + self._minimum = minimum + self._maximum = maximum + + def add(self, value): + """Add one value to the summary""" + self._count += 1 + self._total += value + self._minimum = ( + value if self._minimum is None else min(self._minimum, value) + ) + self._maximum = ( + value if self._maximum is None else max(self._maximum, value) + ) + + def copy(self): + """Return an independent copy of this summary""" + return self.__class__( + self._count, self._total, self._minimum, self._maximum + ) + + def as_dict(self): + """dict: Serialized minimum, mean, and maximum values""" + if self._count == 0: + return {"min": 0, "mean": 0, "max": 0} + + return { + "min": self._minimum, + "mean": self._total / self._count, + "max": self._maximum, + } + + +class _FixedWindowUsageTracker: + """Track values in fixed windows with constant-size state""" + + def __init__(self, window_seconds, start_time): + self.window_seconds = window_seconds + self.start_time = start_time + self._bucket_index = 0 + self._current_value = 0 + self._summary = _OnlineUsageSummary() + + def add(self, value, timestamp=None): + """Add a value at a monotonic timestamp""" + if timestamp is None: + timestamp = time.perf_counter() + + bucket_index = int( + (timestamp - self.start_time) // self.window_seconds + ) + if bucket_index < self._bucket_index: + msg = "Usage timestamps must be monotonically increasing" + raise COMPASSValueError(msg) + + if bucket_index == self._bucket_index: + self._current_value += value + return + + self._summary.add(self._current_value) + self._bucket_index = bucket_index + self._current_value = value + + def snapshot(self): + """dict: Summary including the current partial time window""" + timestamp = time.perf_counter() + + bucket_index = int( + (timestamp - self.start_time) // self.window_seconds + ) + bucket_index = max(bucket_index, self._bucket_index) + summary = self._summary.copy() + summary.add(self._current_value) + return summary.as_dict() + + +class _ModelUsageRateTracker: + """Track fixed-window request and token rates for one scope""" + + def __init__(self, start_time): + self.requests_per_second = _FixedWindowUsageTracker(1, start_time) + self.requests_per_minute = _FixedWindowUsageTracker(60, start_time) + self.tokens_per_minute = _FixedWindowUsageTracker(60, start_time) + + def record_request(self, timestamp): + """Record a submitted request""" + self.requests_per_second.add(1, timestamp) + self.requests_per_minute.add(1, timestamp) + + def record_tokens(self, tokens, timestamp): + """Record tokens returned by a completed request""" + self.tokens_per_minute.add(tokens, timestamp) + + def snapshot(self): + """dict: Serialized rate summaries""" + return { + "requests_per_second": self.requests_per_second.snapshot(), + "requests_per_minute": self.requests_per_minute.snapshot(), + "tokens_per_minute": self.tokens_per_minute.snapshot(), + } + + +class LLMRateTracker(UserDict): + """Track run-wide and per-model LLM usage rates on calls""" + + def __init__(self, models=None, label=LLM_USAGE_RATES_KEY): + """ + + Parameters + ---------- + models : iterable of str, optional + Model names to include in output even when they receive no + calls. By default, ``None``. + label : str, optional + Top-level label to use when persisting rate statistics. + By default, ``"_llm_usage_rates"``. + """ + super().__init__() + self.label = label + self._start_time = time.perf_counter() + self._overall = _ModelUsageRateTracker(self._start_time) + self._models = {} + for model in models or []: + self.register_model(model) + + def register_model(self, model): + """Register a model for per-model reporting""" + self._models.setdefault( + model, _ModelUsageRateTracker(self._start_time) + ) + + def record_request(self, model, timestamp): + """Record a submitted LLM request""" + self.register_model(model) + self._overall.record_request(timestamp) + self._models[model].record_request(timestamp) + return timestamp + + def record_tokens(self, model, tokens, timestamp): + """Record actual tokens from a completed LLM request""" + self.register_model(model) + self._overall.record_tokens(tokens, timestamp) + self._models[model].record_tokens(tokens, timestamp) + return timestamp + + def snapshot(self): + """dict: Run-wide and per-model rate summaries""" + self.data = { + "overall": self._overall.snapshot(), + "models": { + model: tracker.snapshot() + for model, tracker in self._models.items() + }, + } + return self + + def add_to(self, other): + """Add the current rate statistics to another dictionary""" + other.update({self.label: dict(self.snapshot())}) + + class UsageTracker(UserDict): """Rate or API usage tracker""" From 9c5da25dd3eb8e62ad5251cc23e6447ec552b56d Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:20:48 -0600 Subject: [PATCH 13/44] Extraction plugin keeps a rate tracker --- compass/plugin/base.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/compass/plugin/base.py b/compass/plugin/base.py index 1ad36bd53..10c885b04 100644 --- a/compass/plugin/base.py +++ b/compass/plugin/base.py @@ -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 @@ -29,10 +35,14 @@ def __init__(self, jurisdiction, model_configs, usage_tracker=None): usage_tracker : UsageTracker, 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 to jurisdiction CSV @@ -169,10 +179,12 @@ 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) From d71851d2895d3c0275837e36d2716d41db9a3c12 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:37:20 -0600 Subject: [PATCH 14/44] `LLMRateTracker` no longer need to know models ahead of time --- compass/services/usage.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/compass/services/usage.py b/compass/services/usage.py index 8e97ba13d..32e9924cf 100644 --- a/compass/services/usage.py +++ b/compass/services/usage.py @@ -225,14 +225,11 @@ def snapshot(self): class LLMRateTracker(UserDict): """Track run-wide and per-model LLM usage rates on calls""" - def __init__(self, models=None, label=LLM_USAGE_RATES_KEY): + def __init__(self, label=LLM_USAGE_RATES_KEY): """ Parameters ---------- - models : iterable of str, optional - Model names to include in output even when they receive no - calls. By default, ``None``. label : str, optional Top-level label to use when persisting rate statistics. By default, ``"_llm_usage_rates"``. @@ -242,27 +239,21 @@ def __init__(self, models=None, label=LLM_USAGE_RATES_KEY): self._start_time = time.perf_counter() self._overall = _ModelUsageRateTracker(self._start_time) self._models = {} - for model in models or []: - self.register_model(model) - - def register_model(self, model): - """Register a model for per-model reporting""" - self._models.setdefault( - model, _ModelUsageRateTracker(self._start_time) - ) def record_request(self, model, timestamp): """Record a submitted LLM request""" - self.register_model(model) self._overall.record_request(timestamp) - self._models[model].record_request(timestamp) + self._models.setdefault( + model, _ModelUsageRateTracker(self._start_time) + ).record_request(timestamp) return timestamp def record_tokens(self, model, tokens, timestamp): """Record actual tokens from a completed LLM request""" - self.register_model(model) self._overall.record_tokens(tokens, timestamp) - self._models[model].record_tokens(tokens, timestamp) + self._models.setdefault( + model, _ModelUsageRateTracker(self._start_time) + ).record_tokens(tokens, timestamp) return timestamp def snapshot(self): From d00e8ab994ac1576728c6ee0a78dbf56096232af Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:38:34 -0600 Subject: [PATCH 15/44] `BaseRequest` carries a rate tracker --- compass/pipeline/data_classes.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/compass/pipeline/data_classes.py b/compass/pipeline/data_classes.py index 20127c1e1..87d5121ff 100644 --- a/compass/pipeline/data_classes.py +++ b/compass/pipeline/data_classes.py @@ -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 @@ -685,13 +686,17 @@ 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 + ) class ProcessRequest(BaseRequest): @@ -1280,7 +1285,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()} @@ -1290,6 +1295,8 @@ def build_models(user_input, *, allow_empty=False): caller_instances = {} for raw_kwargs in user_input: + if rate_tracker is not None: + raw_kwargs["rate_tracker"] = rate_tracker for task, model_config in _config_for_tasks(raw_kwargs): _verify_task_not_duplicate(task, caller_instances) caller_instances[task] = model_config From a87ec75cc43ee49639a38884e0e4a02128bfb32a Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:40:06 -0600 Subject: [PATCH 16/44] Fix directive --- compass/plugin/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compass/plugin/base.py b/compass/plugin/base.py index 10c885b04..be62dbd6e 100644 --- a/compass/plugin/base.py +++ b/compass/plugin/base.py @@ -188,5 +188,6 @@ async def record_usage(self): 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""" From 3176eecdbc4b8b6f89bbb8aa95bccf2f8eb5f561 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:43:22 -0600 Subject: [PATCH 17/44] Make tracker read only --- compass/pipeline/data_classes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compass/pipeline/data_classes.py b/compass/pipeline/data_classes.py index 87d5121ff..025aa48ee 100644 --- a/compass/pipeline/data_classes.py +++ b/compass/pipeline/data_classes.py @@ -698,6 +698,11 @@ def models(self): 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): """Parameter Object for full process mode""" From 2a622b309cf43c2f39b93f63a7f1c70d2b73e7b5 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:43:30 -0600 Subject: [PATCH 18/44] Runtime exposes tracker --- compass/pipeline/runtime.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compass/pipeline/runtime.py b/compass/pipeline/runtime.py index 7a3996c42..8527952f2 100644 --- a/compass/pipeline/runtime.py +++ b/compass/pipeline/runtime.py @@ -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 From 634d964a583f790e4ad7789e0a09293649707c71 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:45:51 -0600 Subject: [PATCH 19/44] llm_usage_rates now written to meta --- compass/utilities/finalize.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compass/utilities/finalize.py b/compass/utilities/finalize.py index 975dae175..c2ef30f60 100644 --- a/compass/utilities/finalize.py +++ b/compass/utilities/finalize.py @@ -27,6 +27,7 @@ def save_run_meta( num_jurisdictions_found, total_cost, models, + llm_usage_rates=None, ): """Persist metadata describing an ordinance collection run @@ -54,6 +55,9 @@ def save_run_meta( objects (:class:`~compass.llm.config.OpenAIConfig`) used throughout the run. The function records a condensed summary of each configuration. + llm_usage_rates : dict, optional + Run-wide and per-model LLM request and token rate summaries. + By default, ``None``. Returns ------- @@ -77,7 +81,6 @@ def save_run_meta( "username": username, "versions": {"compass": compass_version, "elm": elm_version}, "technology": tech, - "models": _extract_model_info_from_all_models(models), "time_start_utc": start_date.isoformat(), "time_end_utc": end_date.isoformat(), "total_time": time_elapsed.total_seconds(), @@ -85,6 +88,8 @@ def save_run_meta( "num_jurisdictions_searched": num_jurisdictions_searched, "num_jurisdictions_found": num_jurisdictions_found, "cost": total_cost or None, + "models": _extract_model_info_from_all_models(models), + "llm_usage_rates": llm_usage_rates, "manifest": {}, } manifest = { From 369c6cb248b133f211a159d38e619999f9d1f728 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:46:41 -0600 Subject: [PATCH 20/44] Pass through rate info to meta --- compass/pipeline/coordinator.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/compass/pipeline/coordinator.py b/compass/pipeline/coordinator.py index 55e14847c..d1550f65a 100644 --- a/compass/pipeline/coordinator.py +++ b/compass/pipeline/coordinator.py @@ -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, @@ -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, @@ -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, @@ -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), + ) From 840f2257917748a240f3fd1522202065fe15d085 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:47:08 -0600 Subject: [PATCH 21/44] Attempt to recover rates from partial run --- compass/_cli/finalize.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/compass/_cli/finalize.py b/compass/_cli/finalize.py index bb3718fb2..06926c72b 100644 --- a/compass/_cli/finalize.py +++ b/compass/_cli/finalize.py @@ -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 @@ -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 = [] From 8af45e542bd059d6f8f3d52f76c15df6b85b0f98 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 16:49:51 -0600 Subject: [PATCH 22/44] Add/fix tests --- .../test_integrated_pipeline_orchestration.py | 2 +- .../pipeline/test_pipeline_orchestration.py | 12 +++- .../unit/services/test_services_base.py | 8 +-- .../unit/services/test_services_openai.py | 29 ++++++-- .../unit/services/test_services_threaded.py | 34 +++++++-- .../unit/services/test_services_usage.py | 70 +++++++++++++++++++ .../unit/utilities/test_utilities_finalize.py | 32 +++++++++ 7 files changed, 170 insertions(+), 17 deletions(-) diff --git a/tests/python/integration/test_integrated_pipeline_orchestration.py b/tests/python/integration/test_integrated_pipeline_orchestration.py index d9fa39467..55dc24d4a 100644 --- a/tests/python/integration/test_integrated_pipeline_orchestration.py +++ b/tests/python/integration/test_integrated_pipeline_orchestration.py @@ -149,7 +149,7 @@ def registered_roundtrip_plugin(): def patched_model_configs(monkeypatch): """Replace pipeline model config setup with a deterministic stub""" - def _dummy_build_models(request): + def _dummy_build_models(model_input, rate_tracker): return {LLMTasks.DEFAULT: _DummyModelConfig()} monkeypatch.setattr( diff --git a/tests/python/unit/pipeline/test_pipeline_orchestration.py b/tests/python/unit/pipeline/test_pipeline_orchestration.py index b204afa30..4e3160727 100644 --- a/tests/python/unit/pipeline/test_pipeline_orchestration.py +++ b/tests/python/unit/pipeline/test_pipeline_orchestration.py @@ -66,7 +66,11 @@ async def run(self, jurisdictions_df): DummyWorkflow.LAST_MODE_USED = self.runtime.mode return f"processed {self.runtime.mode}" - monkeypatch.setattr(data_classes_module, "build_models", lambda __: {}) + monkeypatch.setattr( + data_classes_module, + "build_models", + lambda model_input, rate_tracker: {}, + ) monkeypatch.setattr( coordinator_module, "_load_jurisdictions_to_process", @@ -224,7 +228,11 @@ def _load_single_jurisdiction(_): "_load_jurisdictions_to_process", _load_single_jurisdiction, ) - monkeypatch.setattr(data_classes_module, "build_models", lambda __: {}) + monkeypatch.setattr( + data_classes_module, + "build_models", + lambda model_input, rate_tracker: {}, + ) monkeypatch.setattr( coordinator_module, "_select_workflow", diff --git a/tests/python/unit/services/test_services_base.py b/tests/python/unit/services/test_services_base.py index 4e406c09d..da13b429d 100644 --- a/tests/python/unit/services/test_services_base.py +++ b/tests/python/unit/services/test_services_base.py @@ -18,16 +18,16 @@ async def process(self, *args, **kwargs): """Always return 0""" return 0 - rate_tracker = TimeBoundedUsageTracker(max_seconds=0.1) + timed_tracker = TimeBoundedUsageTracker(max_seconds=0.1) service = TestService( - model_name="test", rate_limit=100, rate_tracker=rate_tracker + model_name="test", rate_limit=100, timed_tracker=timed_tracker ) assert service.can_process - service.rate_tracker.add(50) + service.timed_tracker.add(50) assert service.can_process patched_clock.advance(0.01) - service.rate_tracker.add(75) + service.timed_tracker.add(75) assert not service.can_process patched_clock.advance(0.099) assert service.can_process diff --git a/tests/python/unit/services/test_services_openai.py b/tests/python/unit/services/test_services_openai.py index 4e0e21321..7b4276ea8 100644 --- a/tests/python/unit/services/test_services_openai.py +++ b/tests/python/unit/services/test_services_openai.py @@ -11,7 +11,7 @@ usage_from_response, OpenAIService, ) -from compass.services.usage import UsageTracker +from compass.services.usage import LLMRateTracker, UsageTracker from compass.utilities.enums import LLMUsageCategory @@ -55,7 +55,9 @@ def test_usage_from_response( @pytest.mark.asyncio -async def test_openai_service(sample_openai_response, monkeypatch): +async def test_openai_service( + sample_openai_response, monkeypatch, patched_clock +): """Test querying OpenAI while tracking limits and usage""" async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] @@ -76,12 +78,15 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] _test_response, raising=True, ) + rate_tracker = LLMRateTracker() openai_service = OpenAIService(client, model_name="gpt-4") usage_tracker = UsageTracker("my_county", usage_from_response) - message = await openai_service.process(usage_tracker=usage_tracker) - assert openai_service.rate_tracker.total == 13 + message = await openai_service.process( + usage_tracker=usage_tracker, rate_tracker=rate_tracker + ) + assert openai_service.timed_tracker.total == 13 assert message == "test_response" assert usage_tracker == { @@ -93,13 +98,20 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] } } } + assert rate_tracker.snapshot()["overall"] == { + "requests_per_second": {"min": 1, "mean": 1, "max": 1}, + "requests_per_minute": {"min": 1, "mean": 1, "max": 1}, + "tokens_per_minute": {"min": 110, "mean": 110, "max": 110}, + } with pytest.raises(openai.NotFoundError): message = await openai_service.process( - usage_tracker=usage_tracker, bad_request=True + usage_tracker=usage_tracker, + rate_tracker=rate_tracker, + bad_request=True, ) - assert openai_service.rate_tracker.total == 16 + assert openai_service.timed_tracker.total == 16 assert usage_tracker == { "gpt-4": { LLMUsageCategory.DEFAULT: { @@ -109,6 +121,11 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] } } } + assert rate_tracker.snapshot()["overall"] == { + "requests_per_second": {"min": 2, "mean": 2, "max": 2}, + "requests_per_minute": {"min": 2, "mean": 2, "max": 2}, + "tokens_per_minute": {"min": 110, "mean": 110, "max": 110}, + } await openai_service.process() assert usage_tracker == { diff --git a/tests/python/unit/services/test_services_threaded.py b/tests/python/unit/services/test_services_threaded.py index 6eb4397bf..0b5d123a9 100644 --- a/tests/python/unit/services/test_services_threaded.py +++ b/tests/python/unit/services/test_services_threaded.py @@ -15,6 +15,7 @@ from compass.extraction.context import ExtractionContext from compass.services import threaded from compass.services.provider import RunningAsyncServices +from compass.services.usage import LLM_USAGE_RATES_KEY, LLMRateTracker from compass.services.threaded import ( CLEANED_FP_REGISTRY, CleanedFileWriter, @@ -449,7 +450,7 @@ async def test_ord_db_file_writer_process(tmp_path): @pytest.mark.asyncio -async def test_usage_updater_process(tmp_path): +async def test_usage_updater_process(tmp_path, patched_clock): """UsageUpdater should serialize tracker info to json""" class StubTracker: @@ -468,11 +469,13 @@ def add_to(self, other): usage_fp = tmp_path / "usage.json" tracker = StubTracker() + rate_tracker = LLMRateTracker() + rate_tracker.record_request("gpt-4o", patched_clock()) updater = UsageUpdater(usage_fp) updater.acquire_resources() assert updater.can_process is True - usage_info = await updater.process(tracker) + usage_info = await updater.process(tracker, rate_tracker) assert updater.can_process is True assert tracker.add_called is True @@ -482,10 +485,30 @@ def add_to(self, other): persisted = json.load(fh) assert "stub" in persisted + assert LLM_USAGE_RATES_KEY in persisted assert persisted == usage_info # Existing data path without tracker - assert threaded._dump_usage(usage_fp, tracker=None) == persisted + assert ( + threaded._dump_usage(usage_fp, usage_tracker=None, rate_tracker=None) + == persisted + ) + + +@pytest.mark.asyncio +async def test_usage_updater_persists_llm_usage_rates(tmp_path, patched_clock): + """Persist rate snapshots without jurisdiction-specific usage""" + + rate_tracker = LLMRateTracker() + rate_tracker.record_request("gpt-4o", patched_clock()) + usage_fp = tmp_path / "usage.json" + updater = UsageUpdater(usage_fp) + updater.acquire_resources() + + persisted = await updater.process(None, rate_tracker) + updater.release_resources() + + assert persisted == {LLM_USAGE_RATES_KEY: rate_tracker.snapshot()} @pytest.mark.asyncio @@ -589,7 +612,10 @@ def test_dump_usage_without_tracker_returns_existing_data(tmp_path): initial = {"existing": True} usage_fp.write_text(json.dumps(initial), encoding="utf-8") - assert threaded._dump_usage(usage_fp, tracker=None) == initial + assert ( + threaded._dump_usage(usage_fp, usage_tracker=None, rate_tracker=None) + == initial + ) @pytest.mark.asyncio diff --git a/tests/python/unit/services/test_services_usage.py b/tests/python/unit/services/test_services_usage.py index d28dd37ef..856024139 100644 --- a/tests/python/unit/services/test_services_usage.py +++ b/tests/python/unit/services/test_services_usage.py @@ -1,10 +1,13 @@ """Test COMPASS Ordinance service usage functions and classes""" from pathlib import Path +from collections import UserDict import pytest from compass.services.usage import ( + LLM_USAGE_RATES_KEY, + LLMRateTracker, TimedEntry, TimeBoundedUsageTracker, UsageTracker, @@ -54,6 +57,73 @@ def test_time_bounded_usage_tracker(patched_clock): assert tracker.total == 0 +def test_rate_tracker(patched_clock): + """Track call-driven fixed-window LLM usage rates""" + + tracker = LLMRateTracker() + tracker.record_request("model_a", patched_clock()) + + patched_clock.advance(1.2) + tracker.record_request("model_a", patched_clock()) + + patched_clock.advance(1) + tracker.record_tokens("model_a", 10, patched_clock()) + + patched_clock.advance(59.3) + rates = tracker.snapshot() + + expected_requests_per_second = {"min": 1, "mean": 1, "max": 1} + expected_requests_per_minute = {"min": 2, "mean": 2, "max": 2} + expected_tokens_per_minute = {"min": 10, "mean": 10, "max": 10} + expected = { + "requests_per_second": expected_requests_per_second, + "requests_per_minute": expected_requests_per_minute, + "tokens_per_minute": expected_tokens_per_minute, + } + + assert rates == {"overall": expected, "models": {"model_a": expected}} + assert isinstance(tracker, UserDict) + + output = {"some": "value"} + tracker.add_to(output) + assert output == {"some": "value", LLM_USAGE_RATES_KEY: tracker.data} + + +def test_rate_tracker_snapshot_does_not_mutate(patched_clock): + """Keep current windows open after generating a rate snapshot""" + + tracker = LLMRateTracker() + tracker.record_request("model_a", patched_clock()) + + patched_clock.advance(1.5) + first_snapshot = tracker.snapshot() + tracker.record_request("model_a", patched_clock()) + tracker.record_tokens("model_b", 20, patched_clock()) + + expected_request_rates = {"min": 1, "mean": 1, "max": 1} + expected_token_rates = {"min": 20, "mean": 20, "max": 20} + rates = tracker.snapshot() + + assert first_snapshot["overall"]["requests_per_second"] == { + "min": 1, + "mean": 1, + "max": 1, + } + assert rates["overall"]["requests_per_second"] == expected_request_rates + assert rates["overall"]["tokens_per_minute"] == expected_token_rates + assert rates["models"]["model_a"]["requests_per_second"] == ( + expected_request_rates + ) + assert rates["models"]["model_b"]["requests_per_second"] == { + "min": 0, + "mean": 0, + "max": 0, + } + assert rates["models"]["model_b"]["tokens_per_minute"] == ( + expected_token_rates + ) + + def test_usage_tracker(): """Test the `UsageTracker` class""" diff --git a/tests/python/unit/utilities/test_utilities_finalize.py b/tests/python/unit/utilities/test_utilities_finalize.py index 643baae9b..7be364784 100644 --- a/tests/python/unit/utilities/test_utilities_finalize.py +++ b/tests/python/unit/utilities/test_utilities_finalize.py @@ -174,6 +174,38 @@ def _raise_os_error(): assert meta["models"] == [] +def test_save_run_meta_writes_llm_usage_rates(tmp_path, monkeypatch): + """Write LLM usage rates into run metadata""" + + monkeypatch.setattr(finalize.getpass, "getuser", lambda: "testuser") + dirs = SimpleNamespace( + logs=tmp_path / "logs", + clean_files=tmp_path / "clean", + jurisdiction_dbs=tmp_path / "jurisdictions", + ordinance_files=tmp_path / "ordinances", + out=tmp_path, + ) + llm_usage_rates = { + "overall": {"requests_per_second": {"min": 0}}, + "models": {"gpt-4o": {"requests_per_second": {"max": 2}}}, + } + + finalize.save_run_meta( + dirs, + "solar", + datetime(2025, 1, 1), + datetime(2025, 1, 1, 0, 1), + num_jurisdictions_searched=1, + num_jurisdictions_found=0, + total_cost=0, + models={}, + llm_usage_rates=llm_usage_rates, + ) + + meta = json.loads((tmp_path / "meta.json").read_text(encoding="utf-8")) + assert meta["llm_usage_rates"] == llm_usage_rates + + def test_save_run_meta_manifest_walks_up_for_sibling_dirs( tmp_path, monkeypatch ): From ba1f263a890577a63d650eaca37c6778b2c7e3cb Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 17:16:20 -0600 Subject: [PATCH 23/44] Add concurrent request tracking --- compass/services/openai.py | 11 +++- compass/services/usage.py | 57 +++++++++++++++++-- .../unit/services/test_services_openai.py | 6 ++ .../unit/services/test_services_usage.py | 47 +++++++++++++++ 4 files changed, 113 insertions(+), 8 deletions(-) diff --git a/compass/services/openai.py b/compass/services/openai.py index 540112af2..be19245d5 100644 --- a/compass/services/openai.py +++ b/compass/services/openai.py @@ -176,7 +176,9 @@ async def process( """ prompt_timestamp = self._record_prompt_tokens(kwargs) self._record_request_rate(prompt_timestamp, rate_tracker) - response = await self._call_gpt(model=self.model_name, **kwargs) + response = await self._call_gpt( + model=self.model_name, rate_tracker=rate_tracker, **kwargs + ) completion_timestamp = self._record_completion_tokens(response) self._record_token_rate(response, completion_timestamp, rate_tracker) self._record_usage(response, usage_tracker, usage_sub_label) @@ -243,8 +245,10 @@ def _update_pb_cost(self, response): openai.APIConnectionError, ), ) - async def _call_gpt(self, **kwargs): + async def _call_gpt(self, rate_tracker=None, **kwargs): """Query Chat GPT with user inputs""" + if rate_tracker is not None: + rate_tracker.start_request_attempt(self.model_name) try: return await self.client.chat.completions.create(**kwargs) except openai.BadRequestError: @@ -257,6 +261,9 @@ async def _call_gpt(self, **kwargs): else: logger.exception("Got 'BadRequestError'") raise + finally: + if rate_tracker is not None: + rate_tracker.end_request_attempt(self.model_name) def _get_response_message(response): diff --git a/compass/services/usage.py b/compass/services/usage.py index 32e9924cf..6cde1dfac 100644 --- a/compass/services/usage.py +++ b/compass/services/usage.py @@ -196,6 +196,29 @@ def snapshot(self): return summary.as_dict() +class _ConcurrentRequestsTracker: + """Track the number of concurrent requests""" + + def __init__(self): + self._active_requests = 0 + self._summary = _OnlineUsageSummary() + + def start_request(self): + """Start tracking a new concurrent request""" + self._active_requests += 1 + self._summary.add(self._active_requests) + + def end_request(self): + """Stop tracking an active concurrent request""" + self._active_requests = max(0, self._active_requests - 1) + + def snapshot(self): + """dict: Snapshot of concurrent requests summary""" + summary = self._summary.copy() + summary.add(self._active_requests) + return summary.as_dict() + + class _ModelUsageRateTracker: """Track fixed-window request and token rates for one scope""" @@ -203,6 +226,7 @@ def __init__(self, start_time): self.requests_per_second = _FixedWindowUsageTracker(1, start_time) self.requests_per_minute = _FixedWindowUsageTracker(60, start_time) self.tokens_per_minute = _FixedWindowUsageTracker(60, start_time) + self.concurrent_requests = _ConcurrentRequestsTracker() def record_request(self, timestamp): """Record a submitted request""" @@ -213,12 +237,21 @@ def record_tokens(self, tokens, timestamp): """Record tokens returned by a completed request""" self.tokens_per_minute.add(tokens, timestamp) + def start_request_attempt(self): + """Record the concurrency when a request attempt starts""" + self.concurrent_requests.start_request() + + def end_request_attempt(self): + """Record that an active request attempt ended""" + self.concurrent_requests.end_request() + def snapshot(self): """dict: Serialized rate summaries""" return { "requests_per_second": self.requests_per_second.snapshot(), "requests_per_minute": self.requests_per_minute.snapshot(), "tokens_per_minute": self.tokens_per_minute.snapshot(), + "concurrent_requests": self.concurrent_requests.snapshot(), } @@ -243,19 +276,31 @@ def __init__(self, label=LLM_USAGE_RATES_KEY): def record_request(self, model, timestamp): """Record a submitted LLM request""" self._overall.record_request(timestamp) - self._models.setdefault( - model, _ModelUsageRateTracker(self._start_time) - ).record_request(timestamp) + self._model_tracker(model).record_request(timestamp) return timestamp def record_tokens(self, model, tokens, timestamp): """Record actual tokens from a completed LLM request""" self._overall.record_tokens(tokens, timestamp) - self._models.setdefault( - model, _ModelUsageRateTracker(self._start_time) - ).record_tokens(tokens, timestamp) + self._model_tracker(model).record_tokens(tokens, timestamp) return timestamp + def start_request_attempt(self, model): + """Record the start of an LLM request attempt""" + self._overall.start_request_attempt() + self._model_tracker(model).start_request_attempt() + + def end_request_attempt(self, model): + """Record the end of an LLM request attempt""" + self._overall.end_request_attempt() + self._model_tracker(model).end_request_attempt() + + def _model_tracker(self, model): + """Return the rate tracker for a model""" + return self._models.setdefault( + model, _ModelUsageRateTracker(self._start_time) + ) + def snapshot(self): """dict: Run-wide and per-model rate summaries""" self.data = { diff --git a/tests/python/unit/services/test_services_openai.py b/tests/python/unit/services/test_services_openai.py index 7b4276ea8..bddf9c8f8 100644 --- a/tests/python/unit/services/test_services_openai.py +++ b/tests/python/unit/services/test_services_openai.py @@ -102,6 +102,7 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] "requests_per_second": {"min": 1, "mean": 1, "max": 1}, "requests_per_minute": {"min": 1, "mean": 1, "max": 1}, "tokens_per_minute": {"min": 110, "mean": 110, "max": 110}, + "concurrent_requests": {"min": 0, "mean": 0.5, "max": 1}, } with pytest.raises(openai.NotFoundError): @@ -125,6 +126,11 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] "requests_per_second": {"min": 2, "mean": 2, "max": 2}, "requests_per_minute": {"min": 2, "mean": 2, "max": 2}, "tokens_per_minute": {"min": 110, "mean": 110, "max": 110}, + "concurrent_requests": { + "min": 0, + "mean": pytest.approx(2 / 3), + "max": 1, + }, } await openai_service.process() diff --git a/tests/python/unit/services/test_services_usage.py b/tests/python/unit/services/test_services_usage.py index 856024139..79eb5fae4 100644 --- a/tests/python/unit/services/test_services_usage.py +++ b/tests/python/unit/services/test_services_usage.py @@ -75,10 +75,12 @@ def test_rate_tracker(patched_clock): expected_requests_per_second = {"min": 1, "mean": 1, "max": 1} expected_requests_per_minute = {"min": 2, "mean": 2, "max": 2} expected_tokens_per_minute = {"min": 10, "mean": 10, "max": 10} + expected_concurrent_requests = {"min": 0, "mean": 0, "max": 0} expected = { "requests_per_second": expected_requests_per_second, "requests_per_minute": expected_requests_per_minute, "tokens_per_minute": expected_tokens_per_minute, + "concurrent_requests": expected_concurrent_requests, } assert rates == {"overall": expected, "models": {"model_a": expected}} @@ -89,6 +91,51 @@ def test_rate_tracker(patched_clock): assert output == {"some": "value", LLM_USAGE_RATES_KEY: tracker.data} +def test_rate_tracker_tracks_concurrent_request_attempts(): + """Track concurrent requests run-wide and per model""" + + tracker = LLMRateTracker() + tracker.start_request_attempt("model_a") + tracker.start_request_attempt("model_b") + + active_rates = tracker.snapshot() + assert active_rates["overall"]["concurrent_requests"] == { + "min": 1, + "mean": pytest.approx(5 / 3), + "max": 2, + } + assert active_rates["models"]["model_a"]["concurrent_requests"] == { + "min": 1, + "mean": 1, + "max": 1, + } + assert active_rates["models"]["model_b"]["concurrent_requests"] == { + "min": 1, + "mean": 1, + "max": 1, + } + + tracker.end_request_attempt("model_a") + tracker.end_request_attempt("model_b") + + completed_rates = tracker.snapshot() + assert completed_rates["overall"]["concurrent_requests"] == { + "min": 0, + "mean": 1, + "max": 2, + } + assert completed_rates["models"]["model_a"]["concurrent_requests"] == { + "min": 0, + "mean": pytest.approx(0.5), + "max": 1, + } + assert completed_rates["models"]["model_b"]["concurrent_requests"] == { + "min": 0, + "mean": pytest.approx(0.5), + "max": 1, + } + + def test_rate_tracker_snapshot_does_not_mutate(patched_clock): """Keep current windows open after generating a rate snapshot""" From a88c7fc3672fdd2627ea546b54550b20bdad2fa2 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 17:23:31 -0600 Subject: [PATCH 24/44] Rename class --- compass/extraction/apply.py | 4 ++-- compass/llm/calling.py | 4 ++-- compass/pipeline/coordinator.py | 6 +++--- compass/pipeline/jurisdiction.py | 2 +- compass/plugin/base.py | 2 +- compass/scripts/download.py | 4 ++-- compass/services/openai.py | 6 +++--- compass/services/threaded.py | 4 ++-- compass/services/usage.py | 4 ++-- compass/utilities/costs.py | 5 ++--- docs/source/conf.py | 7 +++++-- evals/test_run_date_extraction_evals.py | 4 ++-- support/jurisdictions/update_jur_websites.py | 4 ++-- tests/python/integration/test_integrated.py | 4 ++-- .../unit/services/test_services_openai.py | 4 ++-- .../unit/services/test_services_usage.py | 18 +++++++++--------- 16 files changed, 42 insertions(+), 40 deletions(-) diff --git a/compass/extraction/apply.py b/compass/extraction/apply.py index 85f38558b..287dc43b1 100644 --- a/compass/extraction/apply.py +++ b/compass/extraction/apply.py @@ -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 @@ -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``. diff --git a/compass/llm/calling.py b/compass/llm/calling.py index bac7228e8..f49f5f290 100644 --- a/compass/llm/calling.py +++ b/compass/llm/calling.py @@ -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 @@ -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 diff --git a/compass/pipeline/coordinator.py b/compass/pipeline/coordinator.py index d1550f65a..614f1c12d 100644 --- a/compass/pipeline/coordinator.py +++ b/compass/pipeline/coordinator.py @@ -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, @@ -186,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) @@ -316,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) diff --git a/compass/pipeline/jurisdiction.py b/compass/pipeline/jurisdiction.py index a98ea2ef1..67a0b4439 100644 --- a/compass/pipeline/jurisdiction.py +++ b/compass/pipeline/jurisdiction.py @@ -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``. diff --git a/compass/plugin/base.py b/compass/plugin/base.py index be62dbd6e..bd99c5f83 100644 --- a/compass/plugin/base.py +++ b/compass/plugin/base.py @@ -32,7 +32,7 @@ def __init__( :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 diff --git a/compass/scripts/download.py b/compass/scripts/download.py index e915a5762..1e41192e6 100644 --- a/compass/scripts/download.py +++ b/compass/scripts/download.py @@ -207,7 +207,7 @@ async def find_jurisdiction_website( Semaphore instance that can be used to limit the number of playwright browsers open concurrently. If ``None``, no limits are applied. By default, ``None``. - usage_tracker : UsageTracker, optional + usage_tracker : LLMUsageTracker, optional Optional tracker instance to monitor token usage during LLM calls. By default, ``None``. url_ignore_substrings : list of str, optional @@ -634,7 +634,7 @@ async def filter_ordinance_docs( 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``. diff --git a/compass/services/openai.py b/compass/services/openai.py index be19245d5..fda6ae892 100644 --- a/compass/services/openai.py +++ b/compass/services/openai.py @@ -153,9 +153,9 @@ async def process( ---------- model : str OpenAI GPT model to query. - usage_tracker : UsageTracker, optional - UsageTracker instance. Providing this input will update your - tracker with this call's token usage info. + usage_tracker : LLMUsageTracker, optional + LLMUsageTracker instance. Providing this input will update + your tracker with this call's token usage info. By default, ``None``. rate_tracker : LLMRateTracker, optional Run-wide tracker that records request and token rate diff --git a/compass/services/threaded.py b/compass/services/threaded.py index 1a4e6d0a2..118d089b6 100644 --- a/compass/services/threaded.py +++ b/compass/services/threaded.py @@ -425,7 +425,7 @@ async def process(self, usage_tracker, rate_tracker): Parameters ---------- - usage_tracker : UsageTracker + usage_tracker : LLMUsageTracker A usage tracker instance that contains usage info to be added to output file. Can also be ``None`` to not record usage. @@ -502,7 +502,7 @@ async def process( seconds_elapsed : int or float Total number of seconds it took to look for (and possibly parse) this document. - usage_tracker : UsageTracker, optional + usage_tracker : LLMUsageTracker, optional Optional tracker instance to monitor token usage during LLM calls. By default, ``None``. """ diff --git a/compass/services/usage.py b/compass/services/usage.py index 6cde1dfac..b8cc7b79d 100644 --- a/compass/services/usage.py +++ b/compass/services/usage.py @@ -317,7 +317,7 @@ def add_to(self, other): other.update({self.label: dict(self.snapshot())}) -class UsageTracker(UserDict): +class LLMUsageTracker(UserDict): """Rate or API usage tracker""" UNKNOWN_MODEL_LABEL = "unknown_model" @@ -383,7 +383,7 @@ def update_from_model( 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. + :obj:`LLMUsageTracker.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 diff --git a/compass/utilities/costs.py b/compass/utilities/costs.py index 47f9bba29..efb08f24a 100644 --- a/compass/utilities/costs.py +++ b/compass/utilities/costs.py @@ -86,8 +86,7 @@ def compute_cost_from_totals(totals): should contain "prompt_tokens" and "response_tokens" keys indicating the number of tokens used for prompts and responses, respectively. This dictionary is typically obtained from the - `tracker_totals` property of a - :class:`compass.services.usage.UsageTracker` instance. + `tracker_totals` property of a LLMUsageTracker instance. Returns ------- @@ -149,7 +148,7 @@ def compute_total_cost_from_usage(tracked_usage): Parameters ---------- - tracked_usage : compass.services.usage.UsageTracker or dict + tracked_usage : LLMUsageTracker or dict Dictionary where keys are usage categories (typically jurisdiction names) and values are dictionaries containing usage details. The usage details dictionaries should have a diff --git a/docs/source/conf.py b/docs/source/conf.py index d5e48580f..efe93e1fc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -302,7 +302,9 @@ def _skip_internal_api(name, obj): if (getattr(obj, "__doc__", None) or "").startswith("[NOT PUBLIC API]"): return True - return name in {"copy", "fromkeys"} and "UsageTracker" in str(obj) + return name in {"copy", "fromkeys"} and ( + "LLMUsageTracker" in str(obj) or "LLMRateTracker" in str(obj) + ) def _skip_member(app, what, name, obj, skip, options): @@ -407,6 +409,7 @@ def setup(app): "JSONFromTextLLMCaller": ":class:`~compass.llm.calling.JSONFromTextLLMCaller`", "SchemaOutputLLMCaller": ":class:`~compass.llm.calling.SchemaOutputLLMCaller`", "TimeBoundedUsageTracker": ":class:`~compass.services.usage.TimeBoundedUsageTracker`", - "UsageTracker": ":class:`~compass.services.usage.UsageTracker`", + "LLMUsageTracker": ":class:`~compass.services.usage.LLMUsageTracker`", + "LLMRateTracker": ":class:`~compass.services.usage.LLMRateTracker`", "WindOrdinanceTextExtractor": ":class:`~compass.extraction.wind.ordinance.WindOrdinanceTextExtractor`", } diff --git a/evals/test_run_date_extraction_evals.py b/evals/test_run_date_extraction_evals.py index 1c1d42ba3..5923cc6dd 100644 --- a/evals/test_run_date_extraction_evals.py +++ b/evals/test_run_date_extraction_evals.py @@ -17,7 +17,7 @@ ) from compass.utilities.enums import LLMTasks from compass.services.openai import usage_from_response -from compass.services.usage import UsageTracker +from compass.services.usage import LLMUsageTracker from compass.services.provider import RunningAsyncServices from compass.services.cpu import ( FileLoader, @@ -226,7 +226,7 @@ async def _run_case( **build_local_file_loader_kwargs(pytesseract_exe_fp="tesseract"), doc_attrs={"source": case["source"]}, ) - usage_tracker = UsageTracker(label, usage_from_response) + usage_tracker = LLMUsageTracker(label, usage_from_response) async def _load_and_extract(): doc = await loader.fetch(case["fp"]) diff --git a/support/jurisdictions/update_jur_websites.py b/support/jurisdictions/update_jur_websites.py index adede1d3e..50c76f567 100755 --- a/support/jurisdictions/update_jur_websites.py +++ b/support/jurisdictions/update_jur_websites.py @@ -34,7 +34,7 @@ from compass.services.cpu import FileLoader from compass.services.openai import usage_from_response from compass.services.provider import RunningAsyncServices -from compass.services.usage import UsageTracker +from compass.services.usage import LLMUsageTracker from compass.utilities.costs import ( compute_cost_from_totals, compute_total_cost_from_usage, @@ -472,7 +472,7 @@ async def _process_one_jurisdiction( search_semaphore, ): """Validate or discover one jurisdiction website""" - usage_tracker = UsageTracker( + usage_tracker = LLMUsageTracker( f"row_{row_index + 1}_{jurisdiction.full_name}", usage_from_response, ) diff --git a/tests/python/integration/test_integrated.py b/tests/python/integration/test_integrated.py index 075264aa9..ee3098242 100644 --- a/tests/python/integration/test_integrated.py +++ b/tests/python/integration/test_integrated.py @@ -16,7 +16,7 @@ from elm.web.document import HTMLDocument from flaky import flaky -from compass.services.usage import TimeBoundedUsageTracker, UsageTracker +from compass.services.usage import TimeBoundedUsageTracker, LLMUsageTracker from compass.services.openai import OpenAIService, usage_from_response from compass.services.threaded import TempFileCache from compass.services.provider import RunningAsyncServices @@ -115,7 +115,7 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] client, model_name="gpt-4", rate_limit=3, rate_tracker=rate_tracker ) - usage_tracker = UsageTracker("my_county", usage_from_response) + usage_tracker = LLMUsageTracker("my_county", usage_from_response) async with RunningAsyncServices([openai_service]): start_time = time.perf_counter() message = await openai_service.call(usage_tracker=usage_tracker) diff --git a/tests/python/unit/services/test_services_openai.py b/tests/python/unit/services/test_services_openai.py index bddf9c8f8..317c85be3 100644 --- a/tests/python/unit/services/test_services_openai.py +++ b/tests/python/unit/services/test_services_openai.py @@ -11,7 +11,7 @@ usage_from_response, OpenAIService, ) -from compass.services.usage import LLMRateTracker, UsageTracker +from compass.services.usage import LLMRateTracker, LLMUsageTracker from compass.utilities.enums import LLMUsageCategory @@ -81,7 +81,7 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] rate_tracker = LLMRateTracker() openai_service = OpenAIService(client, model_name="gpt-4") - usage_tracker = UsageTracker("my_county", usage_from_response) + usage_tracker = LLMUsageTracker("my_county", usage_from_response) message = await openai_service.process( usage_tracker=usage_tracker, rate_tracker=rate_tracker diff --git a/tests/python/unit/services/test_services_usage.py b/tests/python/unit/services/test_services_usage.py index 79eb5fae4..d8b22db47 100644 --- a/tests/python/unit/services/test_services_usage.py +++ b/tests/python/unit/services/test_services_usage.py @@ -8,9 +8,9 @@ from compass.services.usage import ( LLM_USAGE_RATES_KEY, LLMRateTracker, + LLMUsageTracker, TimedEntry, TimeBoundedUsageTracker, - UsageTracker, ) @@ -172,9 +172,9 @@ def test_rate_tracker_snapshot_does_not_mutate(patched_clock): def test_usage_tracker(): - """Test the `UsageTracker` class""" + """Test the `LLMUsageTracker` class""" - tracker = UsageTracker("test", response_parser=_sample_response_parser) + tracker = LLMUsageTracker("test", response_parser=_sample_response_parser) assert tracker == {} assert tracker.totals == {} @@ -184,12 +184,12 @@ def test_usage_tracker(): tracker.update_from_model(response={}) assert tracker == { - UsageTracker.UNKNOWN_MODEL_LABEL: { + LLMUsageTracker.UNKNOWN_MODEL_LABEL: { "default": {"requests": 1, "inputs": 0} } } assert tracker.totals == { - UsageTracker.UNKNOWN_MODEL_LABEL: {"requests": 1, "inputs": 0} + LLMUsageTracker.UNKNOWN_MODEL_LABEL: {"requests": 1, "inputs": 0} } tracker.update_from_model(response={"inputs": 100}, sub_label="parsing") @@ -199,28 +199,28 @@ def test_usage_tracker(): tracker.update_from_model() assert tracker == { - UsageTracker.UNKNOWN_MODEL_LABEL: { + LLMUsageTracker.UNKNOWN_MODEL_LABEL: { "default": {"requests": 1, "inputs": 0}, "parsing": {"requests": 1, "inputs": 100}, }, "my_model": {"parsing": {"requests": 1, "inputs": 200}}, } assert tracker.totals == { - UsageTracker.UNKNOWN_MODEL_LABEL: {"requests": 2, "inputs": 100}, + LLMUsageTracker.UNKNOWN_MODEL_LABEL: {"requests": 2, "inputs": 100}, "my_model": {"requests": 1, "inputs": 200}, } tracker.update_from_model(response={"tokens": 5}) assert tracker == { - UsageTracker.UNKNOWN_MODEL_LABEL: { + LLMUsageTracker.UNKNOWN_MODEL_LABEL: { "default": {"requests": 2, "inputs": 0, "tokens": 5}, "parsing": {"requests": 1, "inputs": 100}, }, "my_model": {"parsing": {"requests": 1, "inputs": 200}}, } assert tracker.totals == { - UsageTracker.UNKNOWN_MODEL_LABEL: { + LLMUsageTracker.UNKNOWN_MODEL_LABEL: { "requests": 3, "inputs": 100, "tokens": 5, From 5dc63e4a299569611ab89cdc6ed3fce1922b5560 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 17:28:31 -0600 Subject: [PATCH 25/44] Remove bad input --- compass/pipeline/data_classes.py | 8 ++------ .../unit/pipeline/test_pipeline_data_classes.py | 16 ++++++++++++++++ .../unit/pipeline/test_pipeline_orchestration.py | 4 ++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/compass/pipeline/data_classes.py b/compass/pipeline/data_classes.py index 025aa48ee..a607df31b 100644 --- a/compass/pipeline/data_classes.py +++ b/compass/pipeline/data_classes.py @@ -694,9 +694,7 @@ def models(self): if not self.user_model_input: return {} - return build_models( - self.user_model_input, rate_tracker=self._rate_tracker - ) + return build_models(self.user_model_input) @property def rate_tracker(self): @@ -1290,7 +1288,7 @@ def __bool__(self): return self.ord_db_fp is not None -def build_models(user_input, *, allow_empty=False, rate_tracker=None): +def build_models(user_input, *, allow_empty=False): """[NOT PUBLIC API] Build configured model registry""" if user_input is None: return {} if allow_empty else {LLMTasks.DEFAULT: OpenAIConfig()} @@ -1300,8 +1298,6 @@ def build_models(user_input, *, allow_empty=False, rate_tracker=None): caller_instances = {} for raw_kwargs in user_input: - if rate_tracker is not None: - raw_kwargs["rate_tracker"] = rate_tracker for task, model_config in _config_for_tasks(raw_kwargs): _verify_task_not_duplicate(task, caller_instances) caller_instances[task] = model_config diff --git a/tests/python/unit/pipeline/test_pipeline_data_classes.py b/tests/python/unit/pipeline/test_pipeline_data_classes.py index 167c963bd..778660fe6 100644 --- a/tests/python/unit/pipeline/test_pipeline_data_classes.py +++ b/tests/python/unit/pipeline/test_pipeline_data_classes.py @@ -4,6 +4,7 @@ import pytest +from compass.pipeline import ProcessRequest from compass.pipeline.data_classes import WebSearchParams @@ -66,5 +67,20 @@ def test_wsp_se_kwargs(): ) +def test_request_models_accepts_runtime_rate_tracker(tmp_path): + """Build model configs without passing runtime state to them""" + request = ProcessRequest( + out_dir=tmp_path / "outputs", + tech="solar", + jurisdiction_fp=tmp_path / "jurisdictions.csv", + model=[{"name": "gpt-4o-mini", "client_type": "openai"}], + ) + + models = request.models + + assert models["default"].name == "gpt-4o-mini" + assert request.rate_tracker is not None + + if __name__ == "__main__": pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/python/unit/pipeline/test_pipeline_orchestration.py b/tests/python/unit/pipeline/test_pipeline_orchestration.py index 4e3160727..4c24517b4 100644 --- a/tests/python/unit/pipeline/test_pipeline_orchestration.py +++ b/tests/python/unit/pipeline/test_pipeline_orchestration.py @@ -69,7 +69,7 @@ async def run(self, jurisdictions_df): monkeypatch.setattr( data_classes_module, "build_models", - lambda model_input, rate_tracker: {}, + lambda _model_input: {}, ) monkeypatch.setattr( coordinator_module, @@ -231,7 +231,7 @@ def _load_single_jurisdiction(_): monkeypatch.setattr( data_classes_module, "build_models", - lambda model_input, rate_tracker: {}, + lambda _model_input: {}, ) monkeypatch.setattr( coordinator_module, From e6b6e4615a855823fe7c62379814d4f26b2e5308 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 18:21:15 -0600 Subject: [PATCH 26/44] Pop non-serializable arg --- compass/utilities/finalize.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compass/utilities/finalize.py b/compass/utilities/finalize.py index c2ef30f60..ceb2ee197 100644 --- a/compass/utilities/finalize.py +++ b/compass/utilities/finalize.py @@ -278,10 +278,15 @@ def _extract_model_info_from_all_models(models): for task, caller_args in models.items(): models_to_tasks.setdefault(caller_args, []).append(task) + llm_call_kwargs = { + k: v + for k, v in (caller_args.llm_call_kwargs or {}).items() + if k != "rate_tracker" + } return [ { "name": caller_args.name, - "llm_call_kwargs": caller_args.llm_call_kwargs or None, + "llm_call_kwargs": llm_call_kwargs, "llm_service_rate_limit": caller_args.llm_service_rate_limit, "text_splitter_chunk_size": caller_args.text_splitter_chunk_size, "text_splitter_chunk_overlap": ( From dc401c8935e1be006af6be6d6bacf20e7f18aa26 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 18:22:34 -0600 Subject: [PATCH 27/44] Fix input for llm call --- compass/pipeline/data_classes.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/compass/pipeline/data_classes.py b/compass/pipeline/data_classes.py index a607df31b..0d24c5f48 100644 --- a/compass/pipeline/data_classes.py +++ b/compass/pipeline/data_classes.py @@ -694,7 +694,9 @@ def models(self): 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): @@ -1288,7 +1290,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()} @@ -1298,7 +1300,7 @@ 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 @@ -1306,7 +1308,7 @@ def build_models(user_input, *, allow_empty=False): 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) @@ -1314,6 +1316,7 @@ def _config_for_tasks(kwargs): tasks = [tasks] model_config = OpenAIConfig(**kwargs) + model_config.llm_call_kwargs.update({"rate_tracker": rate_tracker}) for task in tasks: yield task, model_config From 89287558646d25f76c8a7bb573f60213bed43c80 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 26 Aug 2026 18:24:32 -0600 Subject: [PATCH 28/44] Fix --- compass/utilities/finalize.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/compass/utilities/finalize.py b/compass/utilities/finalize.py index ceb2ee197..ce95f0295 100644 --- a/compass/utilities/finalize.py +++ b/compass/utilities/finalize.py @@ -278,15 +278,14 @@ def _extract_model_info_from_all_models(models): for task, caller_args in models.items(): models_to_tasks.setdefault(caller_args, []).append(task) - llm_call_kwargs = { - k: v - for k, v in (caller_args.llm_call_kwargs or {}).items() - if k != "rate_tracker" - } return [ { "name": caller_args.name, - "llm_call_kwargs": llm_call_kwargs, + "llm_call_kwargs": { + k: v + for k, v in (caller_args.llm_call_kwargs or {}).items() + if k != "rate_tracker" + }, "llm_service_rate_limit": caller_args.llm_service_rate_limit, "text_splitter_chunk_size": caller_args.text_splitter_chunk_size, "text_splitter_chunk_overlap": ( From c2f382240be199dee7bf9149e1a79b10132da955 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 27 Aug 2026 17:10:55 -0600 Subject: [PATCH 29/44] Fix tests --- tests/python/unit/pipeline/test_pipeline_orchestration.py | 2 +- tests/python/unit/utilities/test_utilities_finalize.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/python/unit/pipeline/test_pipeline_orchestration.py b/tests/python/unit/pipeline/test_pipeline_orchestration.py index 4c24517b4..2d5d5624e 100644 --- a/tests/python/unit/pipeline/test_pipeline_orchestration.py +++ b/tests/python/unit/pipeline/test_pipeline_orchestration.py @@ -69,7 +69,7 @@ async def run(self, jurisdictions_df): monkeypatch.setattr( data_classes_module, "build_models", - lambda _model_input: {}, + lambda _model_input, **_kwargs: {}, ) monkeypatch.setattr( coordinator_module, diff --git a/tests/python/unit/utilities/test_utilities_finalize.py b/tests/python/unit/utilities/test_utilities_finalize.py index 7be364784..0c1aa298f 100644 --- a/tests/python/unit/utilities/test_utilities_finalize.py +++ b/tests/python/unit/utilities/test_utilities_finalize.py @@ -638,7 +638,7 @@ def test_extract_model_info_from_all_models_groups_tasks(): first, second = info assert first["name"] == "gpt" assert first["tasks"] == ["task_one", "task_two"] - assert first["llm_call_kwargs"] is None + assert first["llm_call_kwargs"] == {} assert second["name"] == "gpt-4" assert second["tasks"] == ["task_three"] From 280a9372ac7355f73f136137e7725b61be7d2dd6 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 10:15:02 -0600 Subject: [PATCH 30/44] Fix integration tests --- tests/python/integration/test_integrated.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/python/integration/test_integrated.py b/tests/python/integration/test_integrated.py index ee3098242..6256f8d31 100644 --- a/tests/python/integration/test_integrated.py +++ b/tests/python/integration/test_integrated.py @@ -112,7 +112,7 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] max_seconds=time_limit * sleep_mult * 0.8 ) openai_service = OpenAIService( - client, model_name="gpt-4", rate_limit=3, rate_tracker=rate_tracker + client, model_name="gpt-4", rate_limit=3, timed_tracker=rate_tracker ) usage_tracker = LLMUsageTracker("my_county", usage_from_response) @@ -122,7 +122,7 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] patched_clock.advance(time_limit * 3) message2 = await openai_service.call() - assert openai_service.rate_tracker.total == 13 + assert openai_service.timed_tracker.total == 13 assert message == "test_response" assert message2 == "test_response" assert len(elapsed_times) == 3 @@ -141,7 +141,7 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] } patched_clock.advance(time_limit * sleep_mult) - assert openai_service.rate_tracker.total == 0 + assert openai_service.timed_tracker.total == 0 start_time = time.perf_counter() - time_limit - 1 await openai_service.call() @@ -155,14 +155,14 @@ async def _test_response(*args, **kwargs): # ruff:ignore[unused-async] patched_clock.advance(time_limit * sleep_mult) start_time = time.perf_counter() - time_limit - 1 - assert openai_service.rate_tracker.total == 0 + assert openai_service.timed_tracker.total == 0 with pytest.raises(openai.NotFoundError): message = await openai_service.call( usage_tracker=usage_tracker, bad_request=True ) - assert openai_service.rate_tracker.total <= 3 + assert openai_service.timed_tracker.total <= 3 assert usage_tracker == { "gpt-4": { LLMUsageCategory.DEFAULT: { From fd8b385d1334dca0c5c546a42daf50571d8fed6a Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 10:38:06 -0600 Subject: [PATCH 31/44] Keep as dict while ranking --- compass/scripts/download.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compass/scripts/download.py b/compass/scripts/download.py index 9797687e1..f2e4afa51 100644 --- a/compass/scripts/download.py +++ b/compass/scripts/download.py @@ -755,11 +755,14 @@ async def _docs_from_web_search( **kwargs, ) ranked_results = { - res.get("url"): res.get("overall_rank") or 1 + res.get("url"): res for res in out["results"] if res.get("filtered_reason") is None and res.get("url") is not None } - urls = sorted(ranked_results, key=ranked_results.get) + urls = sorted( + ranked_results, + key=lambda url: ranked_results[url].get("overall_rank") or 1, + ) if not urls: return [] From cc0acd626ff65c478b5312d796e1e234fcc0d1be Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 10:40:13 -0600 Subject: [PATCH 32/44] Add search engines attr if available --- compass/scripts/download.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/compass/scripts/download.py b/compass/scripts/download.py index f2e4afa51..0907bea64 100644 --- a/compass/scripts/download.py +++ b/compass/scripts/download.py @@ -770,9 +770,14 @@ async def _docs_from_web_search( urls, jurisdiction.full_name, browser_semaphore, **kwargs ) for doc in docs: - doc.attrs[_COLLECTION_SCORE_KEY] = ranked_results.get( - doc.attrs.get("source") - ) + result = ranked_results.get(doc.attrs.get("source")) + if result is None: + doc.attrs[_COLLECTION_SCORE_KEY] = None + continue + + doc.attrs[_COLLECTION_SCORE_KEY] = result.get("overall_rank") or 1 + if "search_engines" in result: + doc.attrs["search_engines"] = list(result["search_engines"]) return docs From 424114f74fb19394c9b9d5b6bdad12cf2507dd6c Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 10:40:33 -0600 Subject: [PATCH 33/44] Store an overall search engines attr --- compass/web/search.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compass/web/search.py b/compass/web/search.py index 5db0098d4..375eb5560 100644 --- a/compass/web/search.py +++ b/compass/web/search.py @@ -225,6 +225,9 @@ def _apply_duplicate_filters(results): config only wins among entries that are otherwise tied (same ``query_rank`` and ``query_index``). """ + for entry in results: + entry["search_engines"] = [entry["search_engine"]] + winners = {} for entry in _active_results_sorted(results): key = entry["url"] @@ -241,6 +244,8 @@ def _apply_duplicate_filters(results): "query_rank": entry["query_rank"], } ) + if entry["search_engine"] not in winner["search_engines"]: + winner["search_engines"].append(entry["search_engine"]) entry["filtered_reason"] = "duplicate" From ef0a323622b1be1bc11187bc25628e51c72d244d Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 10:40:38 -0600 Subject: [PATCH 34/44] Add tests --- tests/python/unit/scripts/test_download.py | 51 ++++++++++++++++++++++ tests/python/unit/web/test_web_search.py | 5 +++ 2 files changed, 56 insertions(+) diff --git a/tests/python/unit/scripts/test_download.py b/tests/python/unit/scripts/test_download.py index 18fb580ab..5384c531b 100644 --- a/tests/python/unit/scripts/test_download.py +++ b/tests/python/unit/scripts/test_download.py @@ -57,6 +57,57 @@ async def check(self, url, jurisdiction): assert out == "https://prattvilleal.gov/" +@pytest.mark.asyncio +async def test_docs_from_web_search_adds_search_engine_attrs(monkeypatch): + """Copy selected URL search engine provenance to document attrs""" + + async def fake_search_single_jurisdiction( # ruff:ignore[unused-async] + *_args, **_kwargs + ): + return { + "results": [ + { + "url": "https://example.com/ordinance.pdf", + "overall_rank": 2, + "filtered_reason": None, + "search_engines": ["GoogleSearch", "BingSearch"], + } + ] + } + + async def fake_docs_from_urls( # ruff:ignore[unused-async] + urls, *_args, **_kwargs + ): + assert urls == ["https://example.com/ordinance.pdf"] + return [ + SimpleNamespace( + attrs={"source": "https://example.com/ordinance.pdf"} + ) + ] + + monkeypatch.setattr( + download_module, + "search_single_jurisdiction", + fake_search_single_jurisdiction, + ) + monkeypatch.setattr( + download_module, "_docs_from_urls", fake_docs_from_urls + ) + + docs = await download_module._docs_from_web_search( + query_templates=["{jurisdiction} ordinance"], + num_urls=5, + search_semaphore=None, + browser_semaphore=None, + url_ignore_substrings=None, + jurisdiction=SimpleNamespace(full_name="Example County, Test"), + simple_se_result_sort=False, + ) + + assert docs[0].attrs["collection_step_rank"] == 2 + assert docs[0].attrs["search_engines"] == ["GoogleSearch", "BingSearch"] + + @pytest.mark.asyncio async def test_elm_crawl_tracks_accepted_partial_results(monkeypatch): """ELM crawl should retain accepted docs and completed pages early""" diff --git a/tests/python/unit/web/test_web_search.py b/tests/python/unit/web/test_web_search.py index b7299ec50..d946ddc36 100644 --- a/tests/python/unit/web/test_web_search.py +++ b/tests/python/unit/web/test_web_search.py @@ -75,6 +75,7 @@ def test_apply_duplicate_filters_keeps_best_and_tracks_duplicates(): "query_rank": 2, } ] + assert winner["search_engines"] == ["SerpAPIGoogleSearch"] assert loser["filtered_reason"] == "duplicate" @@ -117,6 +118,10 @@ def test_apply_duplicate_filters_collapses_across_search_engines(): assert winner["filtered_reason"] is None assert winner["search_engine"] == "SerpAPIGoogleSearch" + assert winner["search_engines"] == [ + "SerpAPIGoogleSearch", + "TestSearch", + ] assert winner["duplicates"] == [ { "url": "https://example.com/a.pdf", From d00e9dd00389d129c07daad247ee8d933c752527 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 12:48:18 -0600 Subject: [PATCH 35/44] update elm dep --- pixi.lock | 192 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/pixi.lock b/pixi.lock index fb170bd65..4b33d0cdf 100644 --- a/pixi.lock +++ b/pixi.lock @@ -311,6 +311,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -321,7 +322,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl @@ -691,6 +691,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/78/47dc570e7aee6b0a1ecc2520b30639cc2b06003154c9ab0672d86bf720d5/tree_sitter_c-0.24.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/11/6f2b1847d9fa457b3b7251afc2bba2706d104a0c6f01431dfae5d679a839/pypdfium2-5.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl @@ -700,7 +701,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl @@ -1112,13 +1112,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7f/80/c4885a783a7493e396d89a592ba19fce63ef6bd6ad47230924a884a30ec0/primp-1.3.1-cp310-abi3-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/31/7824913ec71e7421d6a57bc06228f3e2d946d8e8f738f898572dded0dc57/pyjson5-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/91/6e/09949370a413a7b5b791e8784f527bfa216539451c8c9b801ff06e61effe/maxminddb-3.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/de/e3e0cf5704fe569d54b8cd5dc316c9fbf08b1b74728732f86e90168b7a3f/cython-3.2.5-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl @@ -1518,12 +1518,12 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/30/f648409de61fd74ae63090071061145059664cc9b9ff8578197601a3beb6/cython-3.2.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/01/0bff084d31b4441ec00e8ec84fa961efe5dffd3359d5318a557db8302a09/maxminddb-3.1.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/7d/4ddb249563a838425242342d2cf67976ccc292a831b543a92fa1a8a83b11/pyjson5-2.0.1-cp313-cp313-macosx_11_0_arm64.whl @@ -1855,11 +1855,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/68/f0c6a60fadff0c185aef232b951a6fa4bbb64511facc48d34734db14f16f/primp-1.3.1-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/27/a64da9ae7dea91d24a12a5649bd62a0eda49ad5ecf184075947971e523ad/maxminddb-3.1.1-cp313-cp313-win_amd64.whl @@ -2221,6 +2221,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -2230,7 +2231,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a5/f5/80029e68ac9b2bd99f388e97ab3623fc0ce314f2dcbb95cfd7804527aa24/docling_parse-5.11.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl @@ -2629,6 +2629,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/09/e6126d32175f96ea963616debbb8e380e7c987ca913efeb59bf7e7f39438/patchright-1.51.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/87/78/47dc570e7aee6b0a1ecc2520b30639cc2b06003154c9ab0672d86bf720d5/tree_sitter_c-0.24.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/11/6f2b1847d9fa457b3b7251afc2bba2706d104a0c6f01431dfae5d679a839/pypdfium2-5.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl @@ -2638,7 +2639,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl @@ -3078,13 +3078,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/77/30/b3a6f6a2e00f8153549c2fa345c58ae1ce8e5f3153c2fe0484d444c3abcb/google_search_results-2.4.2.tar.gz - pypi: https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7f/80/c4885a783a7493e396d89a592ba19fce63ef6bd6ad47230924a884a30ec0/primp-1.3.1-cp310-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/31/7824913ec71e7421d6a57bc06228f3e2d946d8e8f738f898572dded0dc57/pyjson5-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/91/6e/09949370a413a7b5b791e8784f527bfa216539451c8c9b801ff06e61effe/maxminddb-3.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/de/e3e0cf5704fe569d54b8cd5dc316c9fbf08b1b74728732f86e90168b7a3f/cython-3.2.5-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ba/b1/061c322319072225beba45e8c6695b7c1429f83bb97bdb5ed51ea3a009fc/playwright-1.51.0-py3-none-macosx_11_0_universal2.whl @@ -3508,10 +3508,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/52/211774912511af0c1fa5d57affecd8806f611f08d5a1c71834a1511c1be7/rebrowser_playwright-1.49.1-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/7e/30/f648409de61fd74ae63090071061145059664cc9b9ff8578197601a3beb6/cython-3.2.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8a/01/0bff084d31b4441ec00e8ec84fa961efe5dffd3359d5318a557db8302a09/maxminddb-3.1.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/7d/4ddb249563a838425242342d2cf67976ccc292a831b543a92fa1a8a83b11/pyjson5-2.0.1-cp313-cp313-macosx_11_0_arm64.whl @@ -3873,10 +3873,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/68/f0c6a60fadff0c185aef232b951a6fa4bbb64511facc48d34734db14f16f/primp-1.3.1-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/27/a64da9ae7dea91d24a12a5649bd62a0eda49ad5ecf184075947971e523ad/maxminddb-3.1.1-cp313-cp313-win_amd64.whl @@ -4381,6 +4381,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -4390,7 +4391,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a5/f5/80029e68ac9b2bd99f388e97ab3623fc0ce314f2dcbb95cfd7804527aa24/docling_parse-5.11.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl @@ -4939,6 +4939,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/09/e6126d32175f96ea963616debbb8e380e7c987ca913efeb59bf7e7f39438/patchright-1.51.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/87/78/47dc570e7aee6b0a1ecc2520b30639cc2b06003154c9ab0672d86bf720d5/tree_sitter_c-0.24.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/11/6f2b1847d9fa457b3b7251afc2bba2706d104a0c6f01431dfae5d679a839/pypdfium2-5.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl @@ -4948,7 +4949,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl @@ -5536,13 +5536,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/77/30/b3a6f6a2e00f8153549c2fa345c58ae1ce8e5f3153c2fe0484d444c3abcb/google_search_results-2.4.2.tar.gz - pypi: https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7f/80/c4885a783a7493e396d89a592ba19fce63ef6bd6ad47230924a884a30ec0/primp-1.3.1-cp310-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/31/7824913ec71e7421d6a57bc06228f3e2d946d8e8f738f898572dded0dc57/pyjson5-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/91/6e/09949370a413a7b5b791e8784f527bfa216539451c8c9b801ff06e61effe/maxminddb-3.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/de/e3e0cf5704fe569d54b8cd5dc316c9fbf08b1b74728732f86e90168b7a3f/cython-3.2.5-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ba/b1/061c322319072225beba45e8c6695b7c1429f83bb97bdb5ed51ea3a009fc/playwright-1.51.0-py3-none-macosx_11_0_universal2.whl @@ -6112,10 +6112,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/52/211774912511af0c1fa5d57affecd8806f611f08d5a1c71834a1511c1be7/rebrowser_playwright-1.49.1-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/7e/30/f648409de61fd74ae63090071061145059664cc9b9ff8578197601a3beb6/cython-3.2.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8a/01/0bff084d31b4441ec00e8ec84fa961efe5dffd3359d5318a557db8302a09/maxminddb-3.1.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/7d/4ddb249563a838425242342d2cf67976ccc292a831b543a92fa1a8a83b11/pyjson5-2.0.1-cp313-cp313-macosx_11_0_arm64.whl @@ -6626,10 +6626,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/68/f0c6a60fadff0c185aef232b951a6fa4bbb64511facc48d34734db14f16f/primp-1.3.1-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/27/a64da9ae7dea91d24a12a5649bd62a0eda49ad5ecf184075947971e523ad/maxminddb-3.1.1-cp313-cp313-win_amd64.whl @@ -6985,6 +6985,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -6994,7 +6995,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a5/f5/80029e68ac9b2bd99f388e97ab3623fc0ce314f2dcbb95cfd7804527aa24/docling_parse-5.11.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl @@ -7393,6 +7393,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/09/e6126d32175f96ea963616debbb8e380e7c987ca913efeb59bf7e7f39438/patchright-1.51.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/87/78/47dc570e7aee6b0a1ecc2520b30639cc2b06003154c9ab0672d86bf720d5/tree_sitter_c-0.24.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/11/6f2b1847d9fa457b3b7251afc2bba2706d104a0c6f01431dfae5d679a839/pypdfium2-5.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl @@ -7402,7 +7403,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl @@ -7844,13 +7844,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/77/30/b3a6f6a2e00f8153549c2fa345c58ae1ce8e5f3153c2fe0484d444c3abcb/google_search_results-2.4.2.tar.gz - pypi: https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7f/80/c4885a783a7493e396d89a592ba19fce63ef6bd6ad47230924a884a30ec0/primp-1.3.1-cp310-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/31/7824913ec71e7421d6a57bc06228f3e2d946d8e8f738f898572dded0dc57/pyjson5-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/91/6e/09949370a413a7b5b791e8784f527bfa216539451c8c9b801ff06e61effe/maxminddb-3.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/de/e3e0cf5704fe569d54b8cd5dc316c9fbf08b1b74728732f86e90168b7a3f/cython-3.2.5-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ba/b1/061c322319072225beba45e8c6695b7c1429f83bb97bdb5ed51ea3a009fc/playwright-1.51.0-py3-none-macosx_11_0_universal2.whl @@ -8276,10 +8276,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/52/211774912511af0c1fa5d57affecd8806f611f08d5a1c71834a1511c1be7/rebrowser_playwright-1.49.1-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/7e/30/f648409de61fd74ae63090071061145059664cc9b9ff8578197601a3beb6/cython-3.2.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8a/01/0bff084d31b4441ec00e8ec84fa961efe5dffd3359d5318a557db8302a09/maxminddb-3.1.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/7d/4ddb249563a838425242342d2cf67976ccc292a831b543a92fa1a8a83b11/pyjson5-2.0.1-cp313-cp313-macosx_11_0_arm64.whl @@ -8642,10 +8642,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/68/f0c6a60fadff0c185aef232b951a6fa4bbb64511facc48d34734db14f16f/primp-1.3.1-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/27/a64da9ae7dea91d24a12a5649bd62a0eda49ad5ecf184075947971e523ad/maxminddb-3.1.1-cp313-cp313-win_amd64.whl @@ -9008,6 +9008,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -9017,7 +9018,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a5/f5/80029e68ac9b2bd99f388e97ab3623fc0ce314f2dcbb95cfd7804527aa24/docling_parse-5.11.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl @@ -9422,6 +9422,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/09/e6126d32175f96ea963616debbb8e380e7c987ca913efeb59bf7e7f39438/patchright-1.51.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/87/78/47dc570e7aee6b0a1ecc2520b30639cc2b06003154c9ab0672d86bf720d5/tree_sitter_c-0.24.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/11/6f2b1847d9fa457b3b7251afc2bba2706d104a0c6f01431dfae5d679a839/pypdfium2-5.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl @@ -9431,7 +9432,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ad/e7/2ae54c088041620292fbd08dc5702ce5a4193f3183bf7b30f0298ff0bf79/docling_slim-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl @@ -9880,13 +9880,13 @@ environments: - pypi: https://files.pythonhosted.org/packages/77/30/b3a6f6a2e00f8153549c2fa345c58ae1ce8e5f3153c2fe0484d444c3abcb/google_search_results-2.4.2.tar.gz - pypi: https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/7f/80/c4885a783a7493e396d89a592ba19fce63ef6bd6ad47230924a884a30ec0/primp-1.3.1-cp310-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/31/7824913ec71e7421d6a57bc06228f3e2d946d8e8f738f898572dded0dc57/pyjson5-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/91/6e/09949370a413a7b5b791e8784f527bfa216539451c8c9b801ff06e61effe/maxminddb-3.1.1-cp313-cp313-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/de/e3e0cf5704fe569d54b8cd5dc316c9fbf08b1b74728732f86e90168b7a3f/cython-3.2.5-cp39-abi3-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ba/b1/061c322319072225beba45e8c6695b7c1429f83bb97bdb5ed51ea3a009fc/playwright-1.51.0-py3-none-macosx_11_0_universal2.whl @@ -10319,10 +10319,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/79/52/211774912511af0c1fa5d57affecd8806f611f08d5a1c71834a1511c1be7/rebrowser_playwright-1.49.1-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/7e/30/f648409de61fd74ae63090071061145059664cc9b9ff8578197601a3beb6/cython-3.2.5-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8a/01/0bff084d31b4441ec00e8ec84fa961efe5dffd3359d5318a557db8302a09/maxminddb-3.1.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/3b/c9f4b7bdab7d95be31f7444eae973e17786471c609b8297ace6dcf68a41c/docling-2.95.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/10/607c409712c02a26c4cb794820514cb7fdaaeac15fb05bed917fb8a354b3/playwright_stealth-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/7d/4ddb249563a838425242342d2cf67976ccc292a831b543a92fa1a8a83b11/pyjson5-2.0.1-cp313-cp313-macosx_11_0_arm64.whl @@ -10692,10 +10692,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/8f/68/f0c6a60fadff0c185aef232b951a6fa4bbb64511facc48d34734db14f16f/primp-1.3.1-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/50/0a8fab45fa374820c27cc4c3178c4914c60902ba9d6404a692a979e20dbc/marko-2.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/fc/584f75ca31aa6694fed5338ecb54dc4c8341704b1e5b7b6a4528651f12fa/docling_ibm_models-3.13.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/7c/6367995ff57aaa2d9e1055adbaec2519cf5a979780a83a93fdf8c6ec37be/ua_parser-1.0.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/27/a64da9ae7dea91d24a12a5649bd62a0eda49ad5ecf184075947971e523ad/maxminddb-3.1.1-cp313-cp313-win_amd64.whl @@ -34228,7 +34228,7 @@ packages: - langchain-text-splitters>=1.0.0,<2 - networkx>=3.4.2,<4 - nltk>=3.9.1,<4 - - nlr-elm>=0.0.48,<1 + - nlr-elm>=0.0.50,<1 - numpy>=2.4.3,<3 - openai>=2.34.0 - pandas>=2.2.3,<3 @@ -36008,6 +36008,76 @@ packages: - mypy>=1.14.1 ; extra == 'dev' - ruff>=0.9.2 ; extra == 'dev' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/90/75/e4a164c4d8de0b5a79de971e2d5a07e401c2c233293099181cb605daf527/nlr_elm-0.0.50-py3-none-any.whl + name: nlr-elm + version: 0.0.50 + sha256: 15b848dc3b63a7bc9c33abd45b15e447698037e3bc10c492e2d6b05b89aff87f + requires_dist: + - openai>=1.1.0 + - aiohttp + - beautifulsoup4 + - camoufox + - click + - crawl4ai>=0.8.6 + - ddgs + - fake-useragent>=2.0.3 + - google-api-python-client + - google-search-results + - html2text + - html5lib + - httpx + - langchain-text-splitters + - lxml + - matplotlib + - networkx + - nltk + - numpy + - pandas + - playwright-stealth + - pypdf2 + - python-slugify + - rebrowser-playwright + - scipy + - scrapling<0.3 + - tabulate + - tavily-python + - tiktoken + - openai>=1.1.0 ; extra == 'dev' + - aiohttp ; extra == 'dev' + - beautifulsoup4 ; extra == 'dev' + - camoufox ; extra == 'dev' + - click ; extra == 'dev' + - crawl4ai>=0.8.6 ; extra == 'dev' + - ddgs ; extra == 'dev' + - fake-useragent>=2.0.3 ; extra == 'dev' + - google-api-python-client ; extra == 'dev' + - google-search-results ; extra == 'dev' + - html2text ; extra == 'dev' + - html5lib ; extra == 'dev' + - httpx ; extra == 'dev' + - langchain-text-splitters ; extra == 'dev' + - lxml ; extra == 'dev' + - matplotlib ; extra == 'dev' + - networkx ; extra == 'dev' + - nltk ; extra == 'dev' + - numpy ; extra == 'dev' + - pandas ; extra == 'dev' + - playwright-stealth ; extra == 'dev' + - pypdf2 ; extra == 'dev' + - python-slugify ; extra == 'dev' + - rebrowser-playwright ; extra == 'dev' + - scipy ; extra == 'dev' + - scrapling<0.3 ; extra == 'dev' + - tabulate ; extra == 'dev' + - tavily-python ; extra == 'dev' + - tiktoken ; extra == 'dev' + - nlr-rex>=0.5.0 ; extra == 'dev' + - pytest>=5.2 ; extra == 'dev' + - pytest-mock ; extra == 'dev' + - pytest-asyncio ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - flaky>=3.8.1 ; extra == 'dev' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/91/31/7824913ec71e7421d6a57bc06228f3e2d946d8e8f738f898572dded0dc57/pyjson5-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl name: pyjson5 version: 2.0.1 @@ -36258,76 +36328,6 @@ packages: - google-re2 ; extra == 're2' - ua-parser-rs ; extra == 'regex' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/aa/99/7ad2c37a4d7d0e5173888ede7b08b15d6e09e896ced6d3606d11e0cf4e9f/nlr_elm-0.0.48-py3-none-any.whl - name: nlr-elm - version: 0.0.48 - sha256: d2e0c4729934960297437b76c3392ba3921aaff788224fca85f2a561782f814e - requires_dist: - - openai>=1.1.0 - - aiohttp - - beautifulsoup4 - - camoufox - - click - - crawl4ai>=0.8.6 - - ddgs - - fake-useragent>=2.0.3 - - google-api-python-client - - google-search-results - - html2text - - html5lib - - httpx - - langchain-text-splitters - - lxml - - matplotlib - - networkx - - nltk - - numpy - - pandas - - playwright-stealth - - pypdf2 - - python-slugify - - rebrowser-playwright - - scipy - - scrapling<0.3 - - tabulate - - tavily-python - - tiktoken - - openai>=1.1.0 ; extra == 'dev' - - aiohttp ; extra == 'dev' - - beautifulsoup4 ; extra == 'dev' - - camoufox ; extra == 'dev' - - click ; extra == 'dev' - - crawl4ai>=0.8.6 ; extra == 'dev' - - ddgs ; extra == 'dev' - - fake-useragent>=2.0.3 ; extra == 'dev' - - google-api-python-client ; extra == 'dev' - - google-search-results ; extra == 'dev' - - html2text ; extra == 'dev' - - html5lib ; extra == 'dev' - - httpx ; extra == 'dev' - - langchain-text-splitters ; extra == 'dev' - - lxml ; extra == 'dev' - - matplotlib ; extra == 'dev' - - networkx ; extra == 'dev' - - nltk ; extra == 'dev' - - numpy ; extra == 'dev' - - pandas ; extra == 'dev' - - playwright-stealth ; extra == 'dev' - - pypdf2 ; extra == 'dev' - - python-slugify ; extra == 'dev' - - rebrowser-playwright ; extra == 'dev' - - scipy ; extra == 'dev' - - scrapling<0.3 ; extra == 'dev' - - tabulate ; extra == 'dev' - - tavily-python ; extra == 'dev' - - tiktoken ; extra == 'dev' - - nlr-rex>=0.5.0 ; extra == 'dev' - - pytest>=5.2 ; extra == 'dev' - - pytest-mock ; extra == 'dev' - - pytest-asyncio ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - flaky>=3.8.1 ; extra == 'dev' - requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl name: tree-sitter-python version: 0.25.0 diff --git a/pyproject.toml b/pyproject.toml index ecd9fe535..957c577fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "langchain-text-splitters>=1.0.0,<2", "networkx>=3.4.2,<4", "nltk>=3.9.1,<4", - "nlr-elm>=0.0.48,<1", + "nlr-elm>=0.0.50,<1", "numpy>=2.4.3,<3", "openai>=2.34.0", "pandas>=2.2.3,<3", From 10566b7b4ba14bcb24654246d188ea1245b309b0 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 12:52:57 -0600 Subject: [PATCH 36/44] Update to use new elm functionality --- compass/web/search.py | 5 ++- tests/python/unit/web/test_web_search.py | 44 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/compass/web/search.py b/compass/web/search.py index 375eb5560..bf4ab9a2d 100644 --- a/compass/web/search.py +++ b/compass/web/search.py @@ -2,7 +2,7 @@ import logging -from elm.web.search.run import search_with_fallback, search_all_se +from elm.web.search.run import search_all_se, search_with_fallback_with_attrs logger = logging.getLogger(__name__) @@ -116,7 +116,7 @@ async def _run_simple_sort_search( **se_kwargs, ): """Run search with fallback search engines, applying simple sort""" - urls = await search_with_fallback( + return await search_with_fallback_with_attrs( queries, num_urls=num_urls, url_ignore_substrings=url_ignore_substrings, @@ -125,7 +125,6 @@ async def _run_simple_sort_search( task_name=jurisdiction_full_name, **se_kwargs, ) - return [{"url": url} for url in urls] async def _run_holistic_sort_search( diff --git a/tests/python/unit/web/test_web_search.py b/tests/python/unit/web/test_web_search.py index d946ddc36..d9e9263c1 100644 --- a/tests/python/unit/web/test_web_search.py +++ b/tests/python/unit/web/test_web_search.py @@ -7,6 +7,50 @@ import compass.web.search as search_module +@pytest.mark.asyncio +async def test_run_simple_sort_search_preserves_search_engine_attributes( + monkeypatch, +): + """Simple search should retain Elm's engine metadata""" + expected = [ + { + "url": "https://example.com/ordinance.pdf", + "search_engines": ["SerpAPIGoogleSearch"], + } + ] + + async def fake_search_with_fallback_with_attrs( # ruff:ignore[unused-async] + queries, **kwargs + ): + assert queries == ["Example, CO ordinance"] + assert kwargs == { + "num_urls": 5, + "url_ignore_substrings": ["ignore"], + "url_keep_substrings": ["keep"], + "browser_semaphore": "semaphore", + "task_name": "Example, CO", + } + return expected + + monkeypatch.setattr( + search_module, + "search_with_fallback_with_attrs", + fake_search_with_fallback_with_attrs, + ) + + result = await search_module._run_simple_sort_search( + ["Example, CO ordinance"], + 5, + ["ignore"], + ["keep"], + "semaphore", + "Example, CO", + ) + + assert result == expected + assert result[0]["search_engines"] == ["SerpAPIGoogleSearch"] + + def test_apply_blacklist_filters_is_case_insensitive(): """Blacklist should match URL substrings regardless of case""" results = [ From fb36e705efa083c2ed5449b625a28a95c12ae43d Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 13:19:41 -0600 Subject: [PATCH 37/44] Add output --- compass/services/threaded.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compass/services/threaded.py b/compass/services/threaded.py index 3e08b2267..07e67107a 100644 --- a/compass/services/threaded.py +++ b/compass/services/threaded.py @@ -676,6 +676,7 @@ def _compile_doc_info(doc): "mean_confidence": doc.attrs.get("mean_confidence"), "low_score_confidence": doc.attrs.get("low_score_confidence"), "collection_step_rank": doc.attrs.get("collection_step_rank"), + "search_engines": doc.attrs.get("search_engines"), "relevant_text_ngram_score": doc.attrs.get( "relevant_text_ngram_score" ), From b76fa66b73568783202b8f24744b4e63c018ffc8 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 13:20:58 -0600 Subject: [PATCH 38/44] Move key --- compass/services/threaded.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass/services/threaded.py b/compass/services/threaded.py index 07e67107a..206f81f7f 100644 --- a/compass/services/threaded.py +++ b/compass/services/threaded.py @@ -676,7 +676,6 @@ def _compile_doc_info(doc): "mean_confidence": doc.attrs.get("mean_confidence"), "low_score_confidence": doc.attrs.get("low_score_confidence"), "collection_step_rank": doc.attrs.get("collection_step_rank"), - "search_engines": doc.attrs.get("search_engines"), "relevant_text_ngram_score": doc.attrs.get( "relevant_text_ngram_score" ), @@ -684,6 +683,7 @@ def _compile_doc_info(doc): "permitted_use_text_ngram_score" ), "from_steps": doc.attrs.get("from_steps"), + "search_engines": doc.attrs.get("search_engines"), } From f9a6a2d962080f082280eedf3f00c7c1781ea9d8 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 13:34:36 -0600 Subject: [PATCH 39/44] Add info retrieval method --- compass/pipeline/collection/dedupe.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/compass/pipeline/collection/dedupe.py b/compass/pipeline/collection/dedupe.py index c4ab41b63..fc6497402 100644 --- a/compass/pipeline/collection/dedupe.py +++ b/compass/pipeline/collection/dedupe.py @@ -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""" From d0f1c5bb051c0fda36b47259247b2cb1a4527fb2 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 13:35:04 -0600 Subject: [PATCH 40/44] Add doc from steps to extracted doc --- compass/pipeline/collection/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compass/pipeline/collection/base.py b/compass/pipeline/collection/base.py index 5b236ab26..00ac81be5 100644 --- a/compass/pipeline/collection/base.py +++ b/compass/pipeline/collection/base.py @@ -133,6 +133,10 @@ async def execute(self, *, eager_extract=False): ) ) if context is not None: + for doc in context.data_docs: + doc.attrs["from_steps"] = list( + self.de_duplicator.info(doc).from_steps + ) return context else: self._collection_info = ( From 371f0b64fce69412f06c66786393fae85a47bc9f Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 13:36:06 -0600 Subject: [PATCH 41/44] Update tests --- .../unit/pipeline/test_pipeline_collection_dedupe.py | 11 +++++++++++ tests/python/unit/services/test_services_threaded.py | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/tests/python/unit/pipeline/test_pipeline_collection_dedupe.py b/tests/python/unit/pipeline/test_pipeline_collection_dedupe.py index 9f6c22d0d..c39fa83f0 100644 --- a/tests/python/unit/pipeline/test_pipeline_collection_dedupe.py +++ b/tests/python/unit/pipeline/test_pipeline_collection_dedupe.py @@ -28,6 +28,9 @@ def test_add_docs_keeps_from_steps_unique_for_same_doc_and_step(): assert values[0].from_steps == [ "Look for document on jurisdiction website" ] + assert deduplicator.info(doc).from_steps == [ + "Look for document on jurisdiction website" + ] def test_add_docs_preserves_restored_artifacts_and_merges_provenance(): @@ -60,6 +63,14 @@ def test_add_docs_preserves_restored_artifacts_and_merges_provenance(): assert len(values) == 1 assert values[0].doc is saved_doc assert values[0].from_steps == ["known_local_docs", "search_engine"] + assert deduplicator.info(saved_doc).from_steps == [ + "known_local_docs", + "search_engine", + ] + assert deduplicator.info(duplicate_doc).from_steps == [ + "known_local_docs", + "search_engine", + ] if __name__ == "__main__": diff --git a/tests/python/unit/services/test_services_threaded.py b/tests/python/unit/services/test_services_threaded.py index 854f9a70b..11b626edf 100644 --- a/tests/python/unit/services/test_services_threaded.py +++ b/tests/python/unit/services/test_services_threaded.py @@ -550,6 +550,8 @@ async def test_jurisdiction_updater_process(tmp_path): "out_fp": tmp_path / "ord" / "doc.pdf", "checksum": "sha256:abc", "from_ocr": True, + "from_steps": ["search_engine"], + "search_engines": ["SerpAPIGoogleSearch"], "relevant_text_ngram_score": 0.9, "permitted_use_text_ngram_score": 0.8, "ordinance_values": pd.DataFrame( @@ -601,6 +603,8 @@ async def test_jurisdiction_updater_process(tmp_path): assert second["documents"][0]["ord_filename"] == "doc.pdf" assert second["documents"][0]["effective_year"] == 2023 assert second["documents"][0]["num_pages"] == len(doc.pages) + assert second["documents"][0]["from_steps"] == ["search_engine"] + assert second["documents"][0]["search_engines"] == ["SerpAPIGoogleSearch"] updater.release_resources() From 56f914a05588130d03c8255a55db764b5f7e2fb2 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 17:38:38 -0600 Subject: [PATCH 42/44] MInor refactor --- compass/pipeline/collection/base.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/compass/pipeline/collection/base.py b/compass/pipeline/collection/base.py index 00ac81be5..aca35c2fc 100644 --- a/compass/pipeline/collection/base.py +++ b/compass/pipeline/collection/base.py @@ -133,11 +133,7 @@ async def execute(self, *, eager_extract=False): ) ) if context is not None: - for doc in context.data_docs: - doc.attrs["from_steps"] = list( - self.de_duplicator.info(doc).from_steps - ) - return context + return self._context_with_documented_steps(context) else: self._collection_info = ( await self.workflow.write_collection_shard_no_fail( @@ -186,6 +182,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: From 088cca5d3884888f7d3527fc81b0a09a225b9930 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 28 Aug 2026 17:41:13 -0600 Subject: [PATCH 43/44] Rename var for less verbosity --- compass/pipeline/collection/base.py | 6 ++---- compass/pipeline/jurisdiction.py | 16 ++++++---------- .../unit/pipeline/test_pipeline_collection.py | 2 +- .../unit/pipeline/test_pipeline_jurisdiction.py | 2 +- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/compass/pipeline/collection/base.py b/compass/pipeline/collection/base.py index aca35c2fc..d8e7556d8 100644 --- a/compass/pipeline/collection/base.py +++ b/compass/pipeline/collection/base.py @@ -127,10 +127,8 @@ 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 self._context_with_documented_steps(context) diff --git a/compass/pipeline/jurisdiction.py b/compass/pipeline/jurisdiction.py index 67a0b4439..583a41836 100644 --- a/compass/pipeline/jurisdiction.py +++ b/compass/pipeline/jurisdiction.py @@ -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 @@ -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() @@ -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", @@ -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( diff --git a/tests/python/unit/pipeline/test_pipeline_collection.py b/tests/python/unit/pipeline/test_pipeline_collection.py index c698ea8bb..d887f5e52 100644 --- a/tests/python/unit/pipeline/test_pipeline_collection.py +++ b/tests/python/unit/pipeline/test_pipeline_collection.py @@ -39,7 +39,7 @@ def _workflow(existing_collection_info=None): """Build a minimal collection workflow""" workflow = SimpleNamespace( jurisdiction=SimpleNamespace(full_name="Example Township"), - extraction_workflow=None, + extraction=None, ) workflow.writes = [] diff --git a/tests/python/unit/pipeline/test_pipeline_jurisdiction.py b/tests/python/unit/pipeline/test_pipeline_jurisdiction.py index b3f1a4c25..774193377 100644 --- a/tests/python/unit/pipeline/test_pipeline_jurisdiction.py +++ b/tests/python/unit/pipeline/test_pipeline_jurisdiction.py @@ -89,7 +89,7 @@ async def execute(self, **kwargs): captured.update(kwargs) return collection_info - jurisdiction_run.collection_workflow = _CollectionWorkflow() + jurisdiction_run.collection = _CollectionWorkflow() out = await jurisdiction_run.collect() From 704f635fbc1b3333b929f1802be6ca1fc28c9190 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 30 Aug 2026 16:14:38 -0600 Subject: [PATCH 44/44] Minor doc changes --- compass/services/openai.py | 6 +++--- compass/utilities/costs.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compass/services/openai.py b/compass/services/openai.py index fda6ae892..3e734056d 100644 --- a/compass/services/openai.py +++ b/compass/services/openai.py @@ -117,7 +117,7 @@ def __init__( Name of model being used. rate_limit : int or float, optional Token rate limit (typically per minute, but the time - interval is ultimately controlled by the `rate_tracker` + interval is ultimately controlled by the `timed_tracker` instance). By default, ``1e3``. timed_tracker : TimeBoundedUsageTracker, optional Instance used to track usage per time interval and compare @@ -186,7 +186,7 @@ async def process( return _get_response_message(response) def _record_prompt_tokens(self, kwargs): - """Add prompt token count to rate tracker""" + """Add prompt token count to timed tracker""" num_tokens = count_tokens(kwargs.get("messages", []), self.model_name) return self.timed_tracker.add(num_tokens) @@ -197,7 +197,7 @@ def _record_request_rate(self, timestamp, rate_tracker): rate_tracker.record_request(self.model_name, timestamp) def _record_completion_tokens(self, response): - """Add completion token count to rate tracker""" + """Add completion token count to timed tracker""" if response is None: return None return self.timed_tracker.add(response.usage.completion_tokens) diff --git a/compass/utilities/costs.py b/compass/utilities/costs.py index efb08f24a..19030cb3a 100644 --- a/compass/utilities/costs.py +++ b/compass/utilities/costs.py @@ -86,7 +86,7 @@ def compute_cost_from_totals(totals): should contain "prompt_tokens" and "response_tokens" keys indicating the number of tokens used for prompts and responses, respectively. This dictionary is typically obtained from the - `tracker_totals` property of a LLMUsageTracker instance. + `tracker_totals` entry of a LLMUsageTracker instance. Returns -------