Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
25 changes: 20 additions & 5 deletions .github/skills/extraction-run/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,30 @@ A successful run produces these files under `out_dir`:
| `ordinance_files/*.pdf` | Downloaded source documents |
| `cleaned_text/*.txt` | Heuristic-filtered extracted text |
| `jurisdiction_dbs/*.csv` | Per-jurisdiction raw extraction rows |
| `quantitative_ordinances.csv` | Final compiled numeric features |
| `qualitative_ordinances.csv` | Final compiled qualitative features |
| `ordinances.csv` | Final compiled features (quantitative and qualitative) |
| `usage.json` | Per-jurisdiction LLM token and request counts |
| `meta.json` | Run metadata (cost, timing, version) |

Final CSV columns: `county`, `state`, `subdivision`, `jurisdiction_type`,
`FIPS`, `feature`, `value`, `units`, `adder`, `min_dist`, `max_dist`,
`summary`, `year`, `section`, `source`.
`summary`, `ordinance_text`, `explanation`, `year`, `section`, `source`.

Quantitative and qualitative rows share one file. Select the qualitative
rows with `units == "str"` — qualitative features have no measurable
units, so `units` carries that literal instead.

The four content columns have distinct jobs:

| Column | Contents |
|---|---|
| `value` | The extracted answer — a number or category for quantitative features. On qualitative rows the LLM returns null and the `summary` text is copied in, so the column is never blank. |
| `summary` | Prose restatement of the rule. For quantitative features it carries caveats and conditions that `value` and `units` cannot; for qualitative features it *is* the requirement. |
| `ordinance_text` | Exact quotes copied from the source document, trimmed to 5000 characters. |
| `explanation` | The model's reasoning about how it interpreted the requirement. |

The LLM only ever writes the requirement once: qualitative rows are
asked for a `summary` and a null `value`, and the copy happens when the
output is written.

## Interpreting output status correctly

Expand All @@ -186,8 +202,7 @@ Check in order:

1. `outputs/*/cleaned_text/*.txt` (text extraction present)
2. `outputs/*/jurisdiction_dbs/*.csv` (per-jurisdiction parsed rows)
3. `outputs/*/quantitative_ordinances.csv` and
`outputs/*/qualitative_ordinances.csv` (final compiled results)
3. `outputs/*/ordinances.csv` (final compiled results)

Treat the run as **failed for extraction quality** when either is true:
- `Number of jurisdictions with extracted data: 0`
Expand Down
51 changes: 41 additions & 10 deletions compass/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@
"If you had to make a selection when reporting the ordinance, be sure to "
"list out all the other options and their conditions in the summary."
)
_ORDINANCE_TEXT_PROMPT = (
"The value of the 'ordinance_text' key should be the complete relevant "
"text excerpt containing the ordinance information, copied verbatim from "
"the document. The first sentence must be the beginning of the ordinance "
"text pertaining to this extraction, reproduced in full without "
"elision. After that first sentence, you may use an ellipsis ('...') to "
"omit passages that are not relevant to this ordinance. Do not "
"paraphrase, summarize, normalize, or add commentary of your own; every "
"character outside of the ellipses must appear exactly as written in the "
"document."
)
_EXPLANATION_PROMPT = (
"The value of the 'explanation' key should be a brief rationale "
"explaining why the cited ordinance text matches this feature, "
"referencing the specific evidence in 'ordinance_text' and how it "
"supports the extracted value and units (or, for qualitative features, "
"the inclusion criteria)."
)
_YES_NO_PROMPT = (
"Please start your response with either 'Yes' or 'No' and briefly "
"explain your answer."
Expand Down Expand Up @@ -69,6 +87,8 @@ def setup_graph_no_nodes(d_tree_name="Unknown Decision Tree", **kwargs):
return nx.DiGraph(
SECTION_PROMPT=_SECTION_PROMPT,
SUMMARY_PROMPT=_SUMMARY_PROMPT,
ORDINANCE_TEXT_PROMPT=_ORDINANCE_TEXT_PROMPT,
EXPLANATION_PROMPT=_EXPLANATION_PROMPT,
YES_NO_PROMPT=_YES_NO_PROMPT,
UNITS_IN_SUMMARY_PROMPT=_UNITS_IN_SUMMARY_PROMPT,
_d_tree_name=d_tree_name,
Expand Down Expand Up @@ -490,7 +510,8 @@ def setup_graph_extra_restriction(is_numerical=True, **kwargs):
"Please respond based on our entire conversation so far. "
"Return your answer as a dictionary in "
"JSON format (not markdown). Your JSON file must include "
"exactly four keys. The keys are 'value', 'units', 'summary', "
"exactly six keys. The keys are 'value', 'units', 'summary', "
"'ordinance_text', 'explanation', "
"and 'section'. The value of the 'value' key "
"should be a numerical value corresponding to the "
"{restriction} for {tech}, or `null` if the text "
Expand All @@ -502,7 +523,9 @@ def setup_graph_extra_restriction(is_numerical=True, **kwargs):
"restriction. "
"As before, focus only on {restriction} specifically for "
"{system_size_reminder}"
"{SUMMARY_PROMPT} {UNITS_IN_SUMMARY_PROMPT} {SECTION_PROMPT}"
"{SUMMARY_PROMPT} {UNITS_IN_SUMMARY_PROMPT} "
"{ORDINANCE_TEXT_PROMPT} {EXPLANATION_PROMPT} "
"{SECTION_PROMPT}"
),
)

Expand All @@ -517,8 +540,10 @@ def setup_graph_extra_restriction(is_numerical=True, **kwargs):
"Please respond based on our entire conversation so far. "
"Return your answer as a dictionary in "
"JSON format (not markdown). Your JSON file must include "
"exactly two keys. The keys are 'summary' and 'section'. "
"{SUMMARY_PROMPT} {SECTION_PROMPT}"
"exactly four keys. The keys are 'summary', "
"'ordinance_text', 'explanation', and 'section'. "
"{SUMMARY_PROMPT} {ORDINANCE_TEXT_PROMPT} "
"{EXPLANATION_PROMPT} {SECTION_PROMPT}"
),
)

Expand Down Expand Up @@ -559,12 +584,14 @@ def setup_graph_extra_restriction(is_numerical=True, **kwargs):
prompt=(
"Please respond based on our entire conversation so far. "
"Return your answer as a dictionary in JSON format (not "
"markdown). Your JSON file must include exactly three keys. "
"The keys are 'value', 'summary', and 'section'. The value of "
"markdown). Your JSON file must include exactly five keys. "
"The keys are 'value', 'summary', 'ordinance_text', "
"'explanation', and 'section'. The value of "
"the 'value' key should be the string 'ENR' if the text "
"clearly states that the jurisdiction does not regulate "
"{restriction} for {tech} or `null` otherwise. "
"{SUMMARY_PROMPT} {SECTION_PROMPT}"
"{SUMMARY_PROMPT} {ORDINANCE_TEXT_PROMPT} {EXPLANATION_PROMPT} "
"{SECTION_PROMPT}"
),
)

Expand Down Expand Up @@ -864,10 +891,12 @@ def _add_prohibitions_extraction_nodes(G): # ruff:ignore[invalid-argument-name]
"Please respond based on our entire conversation so far. "
"Return your answer as a dictionary in "
"JSON format (not markdown). Your JSON file must include "
"exactly two keys. The keys are 'summary' and 'section'. "
"exactly four keys. The keys are 'summary', 'ordinance_text', "
"'explanation', and 'section'. "
"{SUMMARY_PROMPT} If the prohibition is a moratorium, be "
"sure to include that distinction in your summary and "
"provide any relevant expiration dates. {SECTION_PROMPT}"
"provide any relevant expiration dates. "
"{ORDINANCE_TEXT_PROMPT} {EXPLANATION_PROMPT} {SECTION_PROMPT}"
),
)
return G
Expand Down Expand Up @@ -953,13 +982,15 @@ def setup_graph_permitted_use_districts(**kwargs):
"Please respond based on our entire conversation so far. "
"Return your answer as a dictionary in "
"JSON format (not markdown). Your JSON file must include "
"exactly three keys. The keys are 'value', 'summary', "
"exactly five keys. The keys are 'value', 'summary', "
"'ordinance_text', 'explanation', "
"and 'section'. The value of the 'value' key "
"should be a list of all district names (and abbreviations if "
"given) where {tech} (or similar) "
"are {use_type}, or `null` if the text does not "
"mention this use type for {tech} (or similar). Use our "
"conversation to fill out this value. {SUMMARY_PROMPT} "
"{ORDINANCE_TEXT_PROMPT} {EXPLANATION_PROMPT} "
"{SECTION_PROMPT}"
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"units",
"section",
"summary",
"ordinance_text",
"explanation"
],
"additionalProperties": false,
Expand Down Expand Up @@ -84,7 +85,11 @@
},
"summary": {
"type": "string",
"description": "A short summary with direct ordinance excerpts or quotes whenever possible. For qualitative features such as permitting, fencing, lighting, seismic monitoring, decommissioning, and prohibitions, this is the primary output field and should contain direct ordinance language. For numeric features, summary must support the same requirement used to extract value and units. Must be a non-null, non-empty string. Do not output absence placeholders such as 'No explicit requirement found'; omit the feature instead when no requirement is present."
"description": "A natural-language restatement of the requirement in your own words. For qualitative features this is the primary output field: state the requirement in full here, since the value column is left null and is filled in from this field when the output is written. For quantitative features, use it to capture caveats, conditions, exceptions, and alternative thresholds that the value and units columns cannot hold on their own (for example tiered limits, conditional reductions, or requirements that vary by district or system size). Do not quote the document here: verbatim text belongs in ordinance_text, and this field should read as prose rather than an excerpt. Must be a non-null, non-empty string."

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.

Maybe include the "such as permitting, fencing, lighting, seismic monitoring, decommissioning, and prohibitions"

},
"ordinance_text": {
"type": "string",
"description": "The complete relevant text excerpt containing the ordinance information, copy-pasted verbatim from the source document. The first sentence must be the beginning of the ordinance text pertaining to this extraction, reproduced in full without elision. After that first sentence, you may use an ellipsis ('...') to omit passages that are not relevant to this ordinance, keeping the excerpt focused while preserving all text that supports the extracted value, units, or qualitative requirement. Every character outside of the ellipses must appear exactly as written in the source: no paraphrasing, summarizing, normalization, added context, or commentary. Must be a non-null, non-empty string."

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.

Refine the prompt to allow going both up and down the document to include other relevant excerpts while maintaining that the first sentence should correspond to the extracted value / summary.

},
"explanation": {
"type": "string",
Expand Down Expand Up @@ -241,7 +246,8 @@
"value": 500,
"units": "feet",
"section": "Section 8.4 - Setbacks",
"summary": "'Geothermal production wells and associated facilities shall be set back at least 500 feet from all property lines.'",
"summary": "Wells and related facilities must sit at least 500 feet from any property line. The setback is measured to the outer edge of the wellhead cellar rather than the well bore itself.",
"ordinance_text": "Geothermal production wells and associated facilities shall be set back at least 500 feet from all property lines. ... For purposes of this section, the setback shall be measured from the outer edge of the wellhead cellar.",
"explanation": "The excerpt states an explicit numeric minimum separation from property lines, so it maps directly to 'property lines distance' with value 500 feet."
},
{
Expand All @@ -252,7 +258,8 @@
],
"units": null,
"section": "Table 4 - Allowed Uses",
"summary": "'Geothermal power plants may be approved as conditional uses in the Industrial and Agricultural districts subject to county review.'",
"summary": "Geothermal power plants are not permitted outright anywhere. They are allowed in the Industrial and Agricultural districts only after conditional-use approval by the county.",
"ordinance_text": "Geothermal power plants may be approved as conditional uses in the Industrial and Agricultural districts subject to county review.",
"explanation": "The ordinance explicitly lists two districts where geothermal power plants are allowed only through conditional-use approval, so this belongs under 'special use districts' with the district names preserved as an array."
},
{
Expand All @@ -263,31 +270,35 @@
],
"units": null,
"section": "Section 5.2 - Approval Process",
"summary": "'A conditional use permit and county drilling permit shall be obtained prior to the construction or operation of any geothermal power plant or exploratory well.'",
"summary": "Two separate approvals are needed before work begins: a conditional use permit and a county drilling permit. Both must be in hand prior to construction or operation, including for exploratory wells.",
"ordinance_text": "A conditional use permit and county drilling permit shall be obtained prior to the construction or operation of any geothermal power plant or exploratory well.",
"explanation": "The clause explicitly requires two project approvals before geothermal development can start, so it belongs under 'required permits' with both permit names preserved as an array."
},
{
"feature": "drilling start time",
"value": "07:00",
"units": "HH:MM (24-hour)",
"section": "Section 6.7 - Drilling Operations",
"summary": "'Routine geothermal drilling activities may occur only between 7:00 a.m. and 7:00 p.m., Monday through Saturday, and shall not occur on Sundays or legal holidays except in an emergency.'",
"summary": "Routine drilling may not begin before 7:00 a.m. The window runs Monday through Saturday, with no work on Sundays or legal holidays unless an emergency applies.",
"ordinance_text": "Routine geothermal drilling activities may occur only between 7:00 a.m. and 7:00 p.m., Monday through Saturday, and shall not occur on Sundays or legal holidays except in an emergency.",
"explanation": "The ordinance gives an explicit drilling window beginning at 7:00 a.m., normalized to 24-hour time as 07:00."
},
{
"feature": "drilling end time",
"value": "19:00",
"units": "HH:MM (24-hour)",
"section": "Section 6.7 - Drilling Operations",
"summary": "'Routine geothermal drilling activities may occur only between 7:00 a.m. and 7:00 p.m., Monday through Saturday, and shall not occur on Sundays or legal holidays except in an emergency.'",
"summary": "Routine drilling must stop by 7:00 p.m. The same Monday through Saturday window applies, and emergency work is the only exception to the Sunday and holiday prohibition.",
"ordinance_text": "Routine geothermal drilling activities may occur only between 7:00 a.m. and 7:00 p.m., Monday through Saturday, and shall not occur on Sundays or legal holidays except in an emergency.",
"explanation": "The ordinance gives an explicit drilling window ending at 7:00 p.m., normalized to 24-hour time as 19:00."
},
{
"feature": "bond requirement",
"value": null,
"units": null,
"section": "Section 9.3 - Financial Assurance",
"summary": "'Prior to permit issuance, the operator shall provide financial assurance in a form acceptable to the state oil, gas, and geothermal agency in an amount sufficient to cover plugging, abandonment, reclamation, and decommissioning costs as determined by the agency engineer.'",
"summary": "Financial assurance is required before a permit issues, but the ordinance sets no dollar figure. The amount is determined by the state agency engineer and must cover plugging, abandonment, reclamation, and decommissioning, so no numeric value can be extracted.",
"ordinance_text": "Prior to permit issuance, the operator shall provide financial assurance in a form acceptable to the state oil, gas, and geothermal agency in an amount sufficient to cover plugging, abandonment, reclamation, and decommissioning costs as determined by the agency engineer.",
"explanation": "The clause imposes an enforceable financial assurance requirement but leaves the amount to an agency-determined formula, so it fits 'bond requirement' with value and units set to null."
}
]
Expand All @@ -301,6 +312,7 @@
"For any numeric feature, the summary must support the same requirement that produced value and units for that row. Never pair a numeric value from one clause with qualitative-only language from another clause that has no numeric threshold.",
"Standardize units in the units field using this schema's canonical vocabulary, while preserving ordinance-specific wording in summary.",
"Summary is the primary data carrier for all features in this schema; every row must have a non-null, non-empty string for summary.",
"Every row must include an ordinance_text holding the complete relevant excerpt for the ordinance being extracted, copy-pasted verbatim from the source document. Its first sentence must be the start of the ordinance text for this extraction, reproduced in full; after that first sentence an ellipsis ('...') may be used to drop irrelevant passages. Unlike summary, ordinance_text is never paraphrased or normalized — every character outside the ellipses appears exactly as written in the source.",
"Every row must include an explanation that briefly justifies why the cited summary evidence matches the selected feature under this schema's rules.",
"Emit only positively matched features. Never emit a row to explain why a feature does not apply.",
"The outputs array is a sparse long-form extraction table and does not need to contain every enumerated feature.",
Expand Down
Loading
Loading