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
1 change: 1 addition & 0 deletions compass/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# Temporarily import to register plugins
# Can drop once plugins register themselves
from .extraction import (
COMPASSDataCentersExtractor,
COMPASSGeoHeatPumpExtractor,
COMPASSGeoElectricityExtractor,
COMPASSNaturalGasPipelinesExtractor,
Expand Down
1 change: 1 addition & 0 deletions compass/extraction/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# Temporarily import to register plugins
# Can drop once plugins register themselves
from .data_centers import COMPASSDataCentersExtractor
from .ghp import COMPASSGeoHeatPumpExtractor
from .geothermal_electricity import COMPASSGeoElectricityExtractor
from .natural_gas import COMPASSNaturalGasExtractor
Expand Down
12 changes: 12 additions & 0 deletions compass/extraction/data_centers/__init__.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registers the plugin.

Builds COMPASSDataCentersExtractor from plugin_config.yaml with the existing one-shot factory, under tech="data_centers". The two one-line imports in compass/extraction/__init__.py and compass/__init__.py make it load with the package, the same way the other bundled plugins do.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""COMPASS data centers plugin"""

import importlib.resources

from compass.plugin import create_schema_based_one_shot_extraction_plugin


COMPASSDataCentersExtractor = create_schema_based_one_shot_extraction_plugin(
importlib.resources.files("compass.extraction.data_centers")
/ "plugin_config.yaml",
tech="data_centers",
)
340 changes: 340 additions & 0 deletions compass/extraction/data_centers/data_centers_schema.json

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New: the extraction schema for data center ordinances.

A one-shot schema: the model returns a sparse table with one row per feature the ordinance addresses, plus extraction_notes saying what was and was not extracted and why.

Points worth reviewing:

  • 35 features, with IDs that mirror the vocabulary of the hand-coded research workbook so output can be compared against it directly. Each has a definition under $definitions stating what counts, what value to record, and what to ignore.
  • data_center_type is a fixed list: Micro, Small, Medium, Large or Hyperscale Data Center, Accessory Data Center, or null for data centers of any size. The extraction call uses strict structured output, so the model cannot emit anything else. This replaced free-text class names, which produced near-duplicates across runs ('Large-Scale Data Centers' / 'Large-Scale Data Center'). The jurisdiction's own class name goes at the start of summary.
  • One row per feature and type. Rows are typed by how the ordinance states each requirement (per tier, or generally). Other distinctions, such as whether a site is part of a unified development plan, are conditions: the most restrictive value is recorded and the others are listed in summary.
  • Numeric features carry a single number, the most restrictive one, chosen by the rules in $instructions.numeric_prioritization.
  • Drafts and other jurisdictions' text are excluded. Meeting packets often attach a draft ordinance or reproduce neighbouring counties' rules for comparison. Redline markup alone does not make a text a draft.

tests/python/unit/extraction/test_extraction_data_centers_schema.py checks the contract (feature list matches the definitions, the type list, unit and value rules).

Large diffs are not rendered by default.

184 changes: 184 additions & 0 deletions compass/extraction/data_centers/plugin_config.yaml

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New: plugin configuration for data centers.

Search query templates, website and heuristic keywords, and the extraction system prompt. The prompt names the jurisdiction ({jur_full_name}), which the model was previously never told, and repeats the schema's key rules in prose: the fixed type list, one row per feature and type, most restrictive value, and no rows from drafts or from other jurisdictions' regulations.

No Python is needed for the plugin; __init__.py builds it from this file with the existing create_schema_based_one_shot_extraction_plugin.

Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
schema: ./data_centers_schema.json

data_type_short_desc: data center ordinance

cache_llm_generated_content: true

query_templates:
- "{jurisdiction} data center ordinance"
# Recently adopted ordinances are often published only as a PDF on the
# jurisdiction's own site, before they are codified in a code library.
- "filetype:pdf {jurisdiction} data center ordinance"
- "site:{jurisdiction_website} data center"
- "{jurisdiction} zoning ordinance"

website_keywords:
pdf: 92160
"data center": 46080
datacenter: 46080
ordinance: 23040
zoning: 11520
setback: 5760
noise: 5760
"conditional use": 2880
"special use": 2880
hyperscale: 1440
colocation: 1440
generator: 1440
"code of ordinances": 1440
"municipal code": 1440
ecode360: 1440
municode: 1440
"zoning ordinance": 1440
"unified development ordinance": 720
"ordinance adopted": 720
"land use code": 720
"text amendment": 720
"land development": 720
cooling: 360
screening: 360
county: 360
municipal: 360
chapter: 180
section: 180
planning: 180

heuristic_keywords:
# One mention of a data center term is enough for text to count, so
# every entry here must be specific to data centers. Generic zoning
# terms are left out: with a threshold of one they would let any
# zoning text through on their own.
min_matches: 1
GOOD_TECH_KEYWORDS:
- datacenter
- hyperscale
- colocation
GOOD_TECH_PHRASES:
- data center

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The heuristic counts distinct matching entries against a threshold of two. A plugin can now set min_matches in its heuristic keywords (default unchanged at 2). Data centers use 1, and the keyword list is trimmed to terms specific to data centers, since generic zoning terms would pass any text at a threshold of one. Added tests for a single-mention provision and for generic zoning text.

- data processing center
- data processing facility
- data center campus
- computer server
- server farm
- colocation facility
GOOD_TECH_ACRONYMS:
- PUE
- WUE
NOT_TECH_WORDS:
- community center
- recreation center
- civic center
- convention center
- shopping center
- medical center
- senior center
- visitor center
- town center
- detention center
- child care center
- call center
- distribution center
- service center
- data collection
- database

collection_prompts: true

extraction_system_prompt: |-
You are a legal scholar extracting structured data from local data
center siting ordinances.

Extract ONLY the following features:
- setbacks from residential and protected receptors, property lines,
roads, transit facilities, parks and natural areas, other data
centers, and setbacks that apply to mechanical equipment under a
standard separate from the building
- minimum lot size, maximum lot coverage, minimum vegetative cover,
maximum height, maximum building size, data center share of project
floor area, and parking
- primary, special, accessory, and prohibited use districts, and
jurisdiction-wide prohibitions or moratoria
- noise limits, low-frequency and tonal noise limits, noise
attenuation standards for enclosures and barriers, vibration, light
illumination at a receptor, and thermal impact
- buffer and landscaping standards, equipment screening and
enclosure, and perimeter fencing
- backup generator operating restrictions, on-site power technology,
and decommissioning
- cooling technology, water consumption limits, and reclaimed water
requirements

The jurisdiction is {jur_full_name}. Extract only requirements that
this jurisdiction has itself enacted. The text may be a meeting packet
or staff report rather than the code itself. Such a document often
attaches text labelled as a draft or proposal, or reproduces other
jurisdictions' regulations for comparison: emit no rows from either,
however detailed they are. If nothing else remains, return no rows
and say why in extraction_notes.

Markup alone does not make a text a draft. Ordinances that amend a
code are commonly published showing additions and strikethroughs.
Unless the text is labelled a draft or proposal, or says it was not
adopted, extract it as amended: keep the added wording and leave out
the struck wording.

Extract only enacted local requirements that explicitly govern data
centers, or that govern a use the ordinance defines as a data center,
such as a data processing center or a computer server facility. The
source may be a general zoning code, unified development ordinance,
municipal code, or a standalone adopted ordinance rather than a
technology-specific title.

Data center ordinances frequently regulate by facility size class.
Follow the ordinance's own structure, feature by feature. Where it
states a requirement separately for a size tier, emit a row for that
data_center_type. Where it states a requirement for data centers
generally, emit one row with a null data_center_type. Do not split a
general requirement into a row per tier, and do not merge separately
stated tier requirements into one row. One feature being stated per
tier does not make every feature tiered.

No two rows may share both feature and data_center_type: local
classes that map to the same type share a row, and a rule the text
states in more than one section is one row that cites each section.
Before answering, check the rows for a repeated feature and
data_center_type pair and merge any you find.

For a numeric feature, value is a single number: the most restrictive
value the ordinance sets for that feature and facility class, such as
the largest setback or the lowest noise limit. Do not combine several
numbers into one value. List every other value in summary with the
condition it applies under, such as the time period, receiving use,
zoning district, street class, or yard. Keep units to the unit alone.

data_center_type is a size tier from the schema's fixed list, or
Accessory Data Center. Map the jurisdiction's tier onto the list by
its name, such as Large Data Center for a Large-Scale or Major tier.
Set it to null when the requirement applies to data centers of any
size: generic labels such as Data Center, DATA CENTER, and Data
Processing Center are not size tiers. Any other distinction, such as
whether the site is part of a unified development plan, is a
condition rather than a type: take the most restrictive value and
list the others with their conditions in summary. Start summary with
the jurisdiction's own class name whenever data_center_type is not
null, and retain the complete source wording in ordinance_text.

Do not extract text focused only on office, warehouse, distribution,
telecommunications, or utility uses unless the same clause directly
applies to data centers. Do not extract from proposed, denied, or
superseded provisions.

For noise, ignore complaint-response and monitoring protocol details
unless the clause states an explicit numeric limit.

Follow schema feature definitions literally. Keep the jurisdiction's
own vocabulary exactly as written, except for the data_center_type
values above. Write units with the schema's canonical unit names, such
as feet, acres, sq ft, percent, and dBA, even where the ordinance
abbreviates them. Do not convert values into a different unit. Do not
invent values.

Write summary in your own words. Copy the supporting source passages
verbatim into ordinance_text, retaining the applicable facility class,
conditions, and exceptions. Use explanation to state how that evidence
supports the selected feature, facility class, value, and units.
33 changes: 32 additions & 1 deletion compass/plugin/one_shot/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@

logger = logging.getLogger(__name__)
_SCHEMA_DIR = importlib.resources.files("compass.plugin.one_shot.schemas")
_MIN_MATCHES = "MIN_MATCHES"
"""Heuristic keywords entry holding the plugin's match threshold"""
_QT_SEMAPHORE = Semaphore(1)
_WK_SEMAPHORE = Semaphore(1)
_HK_SEMAPHORE = Semaphore(1)
Expand Down Expand Up @@ -99,7 +101,11 @@ def create_schema_based_one_shot_extraction_plugin(config, tech):
lists used by the heuristic document filter. The
dictionary must include ``not_tech_words``,
``good_tech_keywords``, ``good_tech_acronyms``, and
``good_tech_phrases`` keys. Alternatively, this input can
``good_tech_phrases`` keys. It may also set
``min_matches``, the number of distinct entries that must
appear for text to pass (default ``2``); use ``1`` only
when every entry is specific to the technology, so one
mention is enough. Alternatively, this input can
simply be ``True``, in which case the LLM will be used to
generate heuristic keyword lists based on the schema
input. If ``False``, ``None``, or not provided, a `NoOp`
Expand Down Expand Up @@ -291,6 +297,9 @@ class SchemaBasedHeuristic(KeywordBasedHeuristic):
GOOD_TECH_KEYWORDS = hk["GOOD_TECH_KEYWORDS"]
GOOD_TECH_ACRONYMS = hk["GOOD_TECH_ACRONYMS"]
GOOD_TECH_PHRASES = hk["GOOD_TECH_PHRASES"]
MIN_DEFAULT_MATCHES = hk.get(
_MIN_MATCHES, KeywordBasedHeuristic.MIN_DEFAULT_MATCHES
)

self.__class__.HEURISTIC_KEYWORDS = hk
self.__class__.HEURISTIC = SchemaBasedHeuristic
Expand Down Expand Up @@ -614,16 +623,38 @@ def _normalize_heuristic_keywords(raw):
"GOOD_TECH_PHRASES",
}

raw, min_matches = _split_min_matches(raw)
normalized = _normalize_input_kw(raw, expected_keys)

_verify_expected_kw_are_not_missing(normalized, expected_keys)
_verify_kw_list_not_empty(normalized)
_verify_min_number_of_kw_provided(normalized)
_warn_if_not_enough_kw_provided(normalized)

if min_matches is not None:
normalized[_MIN_MATCHES] = min_matches
return normalized


def _split_min_matches(raw):
"""Separate the optional match threshold from the keyword lists"""
lists, min_matches = {}, None
for key, value in raw.items():
if isinstance(key, str) and key.strip().upper() == _MIN_MATCHES:
min_matches = value
else:
lists[key] = value
if min_matches is None:
return lists, None
if isinstance(min_matches, bool) or not isinstance(min_matches, int):
msg = "Heuristic `min_matches` must be a positive integer."
raise COMPASSPluginConfigurationError(msg)
if min_matches < 1:
msg = "Heuristic `min_matches` must be a positive integer."
raise COMPASSPluginConfigurationError(msg)
return lists, min_matches


def _normalize_input_kw(raw, expected_keys):
"""Normalize the input keyword dictionary"""
normalized = {}
Expand Down
11 changes: 8 additions & 3 deletions compass/plugin/one_shot/components.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two changes to the shared one-shot components. Both apply to every one-shot plugin, not only data centers.

  • The relevance prompt gains a sentence: the content must be about the specific technology or use the schema targets, and a definition or provision for some other use that merely mentions a related activity does not count. Without it, zoning-code definitions chapters were kept as relevant because one definition (for example, a business office that "includes data processing") matched.
  • When the parser extracts nothing, it now logs at INFO and includes the schema's extraction_notes, so the log says why. Schemas without that field log "no explanation given".

Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@
navigation links, cross-reference lists, or citation-only indexes as relevant \
on their own; mark them relevant only when they also include operative \
provisions, definitions, prohibitions, or other substantive regulatory text \
that supports extraction. If relevant, summarize the specific matching \
that supports extraction. The content must be about the specific technology \
or use the schema targets; tangentially related material does not count, \
such as a general definition or provision for some other use that merely \
mentions a related activity in passing. \
If relevant, summarize the specific matching \
content; if not, state why it does not meet the schema's requirements. \
Keep the response concise and consistent.\
"""
Expand Down Expand Up @@ -516,8 +520,9 @@ async def parse(self, text, jurisdiction):
)
data = extraction["outputs"]
if not data:
logger.debug(
"LLM did not extract any relevant features from the text"
logger.info(
"LLM did not extract any relevant features from the text: %s",
extraction.get("extraction_notes") or "no explanation given",
)
return None

Expand Down
2 changes: 2 additions & 0 deletions compass/plugin/ordinance.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"definition" is added to EXCLUDE_FROM_ORD_DOC_CHECK.

That set lists features that do not, on their own, make a document count as an ordinance. A definitions chapter defines terms but regulates nothing, so a document yielding only definition rows is not accepted as the jurisdiction's ordinance and the pipeline moves on to the next document.

Worth knowing: together with the district features already in this set, this means a document whose only rows are definitions and district lists is discarded. In a recent 22-jurisdiction run that dropped two jurisdictions whose adopted ordinance contains little else.

Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"primary use districts",
"special use districts",
"accessory use districts",
# a definitions chapter defines terms but regulates nothing
"definition",
}


Expand Down
1 change: 1 addition & 0 deletions compass/utilities/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"gpt-6-luna": {"prompt": 0.1, "response": 0.5},
"gpt-6-sol": {"prompt": 2, "response": 10},
"gpt-6-astra": {"prompt": 10, "response": 50},
"gpt-6.1-sol": {"prompt": 2, "response": 10},
"text-embedding-ada-002": {"prompt": 0.10},
}
"""LLM Costs registry
Expand Down
17 changes: 12 additions & 5 deletions compass/utilities/finalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,18 +324,25 @@ def _normalize_qualitative_rows(db):
they share a single output file and the ``quantitative`` flag is
not published.

Their ``summary`` is also copied into ``value``. The schemas ask the
LLM for a summary and a null value on these rows, so filling the
value here means the model only ever writes the requirement once,
while readers still find it in the column they expect.
Their ``summary`` is also copied into ``value`` where the value is
empty. The schemas ask the LLM for a summary and a null value on
most of these rows, so filling the value here means the model only
ever writes the requirement once, while readers still find it in
the column they expect. A value the model did supply (such as the
verbatim text of a definition) is kept.
"""
qualitative = ~db["quantitative"]
db.loc[qualitative, "units"] = QUALITATIVE_UNITS

# a numeric-only "value" column is float-typed, so it has to be
# widened before qualitative text can be written into it
db["value"] = db["value"].astype(object)
db.loc[qualitative, "value"] = db.loc[qualitative, "summary"]
empty = db["value"].isna() | ~db["value"].astype(str).str.strip().astype(
bool
)
db.loc[qualitative & empty, "value"] = db.loc[
qualitative & empty, "summary"
]
return db


Expand Down
Loading
Loading