Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/ci-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,30 @@ on:

jobs:
lint:
name: Lint Python Code Base with Ruff
name: Lint Python Code Base
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
- name: Lint Python Code (Ruff)
uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
with:
version: "latest"
args: "check"
src: "./compass"
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
- name: Check Python Code Format (Ruff)
uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
with:
version: "latest"
args: "format --check"
src: "./compass"
- name: Check Python Code Complexity (Complexipy)
uses: rohaquinlop/complexipy-action@e2b05bcc06d899a24e2b6bb8b1354ac42800a95e # v7.0.1
with:
paths: "./compass"
max_complexity_allowed: 10
failed: false # true
Comment thread
ppinchuk marked this conversation as resolved.
sort: desc
ignore_complexity: false # Set to true to ignore complexity checks

locked-tests:
needs: lint
Expand Down
6 changes: 3 additions & 3 deletions compass/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ def setup_participating_owner(**kwargs):
return G


# complexipy: ignore
def setup_graph_extra_restriction(is_numerical=True, **kwargs):
"""Setup Graph to extract non-setback ordinance values from text

Expand All @@ -434,9 +435,8 @@ def setup_graph_extra_restriction(is_numerical=True, **kwargs):
kwargs.setdefault("unit_clarification", "")
kwargs.setdefault("feature_clarifications", "")
feature_id = kwargs.get("feature_id", "")
G = setup_graph_no_nodes( # ruff:ignore[non-lowercase-variable-in-function]
d_tree_name="Extra restriction", **kwargs
)
# ruff:ignore[non-lowercase-variable-in-function]
G = setup_graph_no_nodes(d_tree_name="Extra restriction", **kwargs)

G.add_node(
"init",
Expand Down
38 changes: 31 additions & 7 deletions compass/llm/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Ordinances LLM Configurations"""

import os
import logging
Comment thread
ppinchuk marked this conversation as resolved.
from collections import Counter
from abc import ABC, abstractmethod
from functools import partial, cached_property
Expand All @@ -14,6 +15,9 @@
from compass.exceptions import COMPASSValueError


logger = logging.getLogger(__name__)


class _PrintableRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter):
"""RecursiveCharacterTextSplitter with __str__ method"""

Expand Down Expand Up @@ -86,7 +90,7 @@ def text_splitter(self):
RTS_SEPARATORS,
chunk_size=self.text_splitter_chunk_size,
chunk_overlap=self.text_splitter_chunk_overlap,
length_function=partial(ApiBase.count_tokens, model=self.name),
length_function=partial(_count_tokens_safely, model=self.name),
is_separator_regex=True,
)

Expand Down Expand Up @@ -204,23 +208,23 @@ def _validate_tag(self):
@cached_property
def client_kwargs(self):
"""dict: Parameters to pass to client initializer"""

arg_env_pairs = []
if self.client_type == "azure":
arg_env_pairs = [
("api_key", "AZURE_OPENAI_API_KEY"),
("api_version", "AZURE_OPENAI_VERSION"),
("azure_endpoint", "AZURE_OPENAI_ENDPOINT"),
]
for key, env_var in arg_env_pairs:
if self._client_kwargs.get(key) is None:
self._client_kwargs[key] = os.environ.get(env_var)
elif self.client_type == "openai":
arg_env_pairs = [
("api_key", "OPENAI_API_KEY"),
("base_url", "OPENAI_BASE_URL"),
]
for key, env_var in arg_env_pairs:
if self._client_kwargs.get(key) is None:
self._client_kwargs[key] = os.environ.get(env_var)

for key, env_var in arg_env_pairs:
val = self._client_kwargs.get(key)
self._client_kwargs[key] = val or os.environ.get(env_var)
Comment thread
ppinchuk marked this conversation as resolved.

return self._client_kwargs

Expand All @@ -234,3 +238,23 @@ def llm_service(self):
rate_limit=self.llm_service_rate_limit,
service_tag=self._tag,
)


def _count_tokens_safely(text, model):
"""Count tokens with a conservative fallback

``len(text.encode("utf-8"))`` is a conservative upper bound on BPE
token count, so it will not permit oversized chunks
"""
Comment thread
ppinchuk marked this conversation as resolved.
try:
return ApiBase.count_tokens(text, model=model)
except ValueError as err:
if "Max stack size exceeded for backtracking" not in str(err):
raise

logger.warning(
"Using byte length after tokenizer backtracking failure "
"for %d characters",
len(text),
)
return len(text.encode("utf-8"))
79 changes: 47 additions & 32 deletions compass/pipeline/collection/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def __init__(self, workflow):
"""
self.workflow = workflow
self.de_duplicator = DocumentDeDuplicator()
self._collection_info = {}
self._completed_steps = set()

@cached_property
def steps(self):
Expand Down Expand Up @@ -121,22 +123,9 @@ async def execute(self, *, eager_extract=False):
structured data was extracted, or ``None`` if no structured
data was extracted from any of the collected documents.
"""
collection_info = await self._load_persisted_docs()
completed_steps = set(
collection_info.get("completed_step_document_counts", {})
)
for step in self.steps:
if step.STEP_NAME in completed_steps:
logger.info(
"Skipping completed collection step %s for %s",
step.STEP_NAME,
self.workflow.jurisdiction.full_name,
)
continue

docs = await step.collect(self.workflow)
self.de_duplicator.add_docs(docs, step_name=str(step.STEP_NAME))
completed_steps.add(step.STEP_NAME)
await self._load_persisted_docs()
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(
Expand All @@ -146,15 +135,55 @@ async def execute(self, *, eager_extract=False):
if context is not None:
return context
else:
collection_info = (
self._collection_info = (
await self.workflow.write_collection_shard_no_fail(
self.de_duplicator, completed_steps
self.de_duplicator, self._completed_steps
)
)

if eager_extract:
return None

self._log_execute_results()
return self._collection_info

async def _load_persisted_docs(self):
"""Get any previously persisted documents and completed steps"""
self._collection_info = (
await self.workflow.load_existing_collection_shard()
) or {}

docs = [
_PersistedDocument(doc_info)
for doc_info in self._collection_info.get("documents", [])
]
self.de_duplicator.add_docs(docs)

self._completed_steps |= set(
self._collection_info.get("completed_step_document_counts", {})
)

def _unfinished_steps(self):
"""Yield unfinished collection steps"""
for step in self.steps:
if step.STEP_NAME in self._completed_steps:
logger.info(
"Skipping completed collection step %s for %s",
step.STEP_NAME,
self.workflow.jurisdiction.full_name,
)
continue
yield step

async def _run_collection_step(self, step):
"""Run collection step and record results"""
docs = await step.collect(self.workflow)
self.de_duplicator.add_docs(docs, step_name=str(step.STEP_NAME))
self._completed_steps.add(step.STEP_NAME)
return docs

def _log_execute_results(self):
"""Log the results of the collection execution"""
if self.de_duplicator:
logger.debug(
"Collected the following documents for %s:\n\n%s",
Expand All @@ -168,17 +197,3 @@ async def execute(self, *, eager_extract=False):
"No documents were collected for %s",
self.workflow.jurisdiction.full_name,
)

return collection_info

async def _load_persisted_docs(self):
"""Get any previously persisted documents and completed steps"""
existing_collection_info = (
await self.workflow.load_existing_collection_shard()
) or {}
docs = [
_PersistedDocument(doc_info)
for doc_info in existing_collection_info.get("documents", [])
]
self.de_duplicator.add_docs(docs)
return existing_collection_info
38 changes: 24 additions & 14 deletions compass/pipeline/collection/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,24 @@ async def load_collection_manifest_jurisdictions(manifest_fp, expected_tech):
if isinstance(manifest_fp, (str, os.PathLike)):
manifest_fp = [str(manifest_fp)]

manifests = await _load_jur_manifests(manifest_fp, expected_tech)

jurisdictions_by_fips = {}
for jurisdiction in chain.from_iterable(
manifest.get("jurisdictions", []) for manifest in manifests
):
if jurisdiction is None:
continue

fips = jurisdiction.get("FIPS")
_validate_not_duplicate_jurisdiction(fips, jurisdictions_by_fips)
jurisdictions_by_fips[fips] = jurisdiction

return jurisdictions_by_fips


async def _load_jur_manifests(manifest_fp, expected_tech):
"""Load one or more collection manifest(s) for jurisdictions"""
task_fps = []
for maybe_glob in manifest_fp:
# ruff: ignore[glob]
Expand All @@ -178,22 +196,14 @@ async def load_collection_manifest_jurisdictions(manifest_fp, expected_tech):
GenericFuncRunner.call(_load_collection_manifest, fp, expected_tech)
for fp in task_fps
]
manifests = await asyncio.gather(*tasks)

jurisdictions_by_fips = {}
for jurisdiction in chain.from_iterable(
manifest.get("jurisdictions", []) for manifest in manifests
):
if jurisdiction is None:
continue
return await asyncio.gather(*tasks)

fips = jurisdiction.get("FIPS")
if fips in jurisdictions_by_fips:
msg = f"Duplicate collection manifest entry for FIPS '{fips}'"
raise COMPASSValueError(msg)
jurisdictions_by_fips[fips] = jurisdiction

return jurisdictions_by_fips
def _validate_not_duplicate_jurisdiction(fips, jurisdictions_by_fips):
"""Validate that a jurisdiction is not duplicated in the manifest"""
if fips in jurisdictions_by_fips:
msg = f"Duplicate collection manifest entry for FIPS '{fips}'"
raise COMPASSValueError(msg)


async def load_specific_collection_manifest_shard(shard_dir, jurisdiction):
Expand Down
47 changes: 31 additions & 16 deletions compass/pipeline/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1290,22 +1290,39 @@ def build_models(user_input, *, allow_empty=False):

caller_instances = {}
for raw_kwargs in user_input:
kwargs = dict(raw_kwargs)
tasks = kwargs.pop("tasks", LLMTasks.DEFAULT)
if isinstance(tasks, str):
tasks = [tasks]

model_config = OpenAIConfig(**kwargs)
for task in tasks:
if task in caller_instances:
msg = (
f"Found duplicated task: {task!r}. Please ensure "
"each LLM caller definition has uniquely-assigned "
"tasks."
)
raise COMPASSValueError(msg)
for task, model_config in _config_for_tasks(raw_kwargs):
_verify_task_not_duplicate(task, caller_instances)
caller_instances[task] = model_config

_verify_default_case_handled(caller_instances, allow_empty)
return caller_instances


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

model_config = OpenAIConfig(**kwargs)
for task in tasks:
yield task, model_config


def _verify_task_not_duplicate(task, caller_instances):
"""Verify that the given task has not already been defined"""
if task in caller_instances:
msg = (
f"Found duplicated task: {task!r}. Please ensure "
"each LLM caller definition has uniquely-assigned "
"tasks."
)
raise COMPASSValueError(msg)


def _verify_default_case_handled(caller_instances, allow_empty):
"""Verify that the default LLM task is handled correctly"""
if not allow_empty and LLMTasks.DEFAULT not in caller_instances:
msg = (
"No 'default' LLM caller defined in the `model` portion "
Expand All @@ -1314,5 +1331,3 @@ def build_models(user_input, *, allow_empty=False):
f"unspecified. Found tasks: {list(caller_instances)}"
)
raise COMPASSValueError(msg)

return caller_instances
Loading
Loading