Urban planners spend hours cross-referencing development proposals against zoning ordinances and design guidelines. In this tutorial, I will build a compliance assistant that reads a natural language project description, checks it against a mock municipal code, and returns a structured markdown report. The entire tool runs on Oxlo.ai, so you pay per request rather than burning tokens on long ordinance texts.
What you'll need
- Python 3.10 or higher
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK:
pip install openai
Step 1: Configure the Oxlo.ai client
I start by importing the OpenAI SDK and pointing it at Oxlo.ai's endpoint. I use llama-3.3-70b for general instruction following, but you could swap in qwen-3-32b if you need stronger multilingual support for international building codes.
from openai import OpenAI
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key="YOUR_OXLO_API_KEY" # from https://portal.oxlo.ai
)
# Quick connectivity test
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": "Say hello"}],
max_tokens=10
)
print(response.choices[0].message.content)
Step 2: Define the zoning ordinance database
Instead of connecting to a live government database, I hardcode a small but realistic zoning ordinance as a Python dictionary. This keeps the tutorial runnable without external accounts.
ZONING_DB = {
"R-1": {
"name": "Single Family Residential",
"max_height_ft": 35,
"max_lot_coverage_pct": 40,
"min_front_setback_ft": 25,
"min_side_setback_ft": 10,
"max_units": 1,
"parking_req": "2 spaces per unit",
"notes": "No commercial activity permitted."
},
"C-2": {
"name": "Neighborhood Commercial",
"max_height_ft": 60,
"max_lot_coverage_pct": 80,
"min_front_setback_ft": 10,
"min_side_setback_ft": 5,
"max_units": None,
"parking_req": "1 space per 300 sq ft of floor area",
"notes": "Ground floor retail required if abutting a street."
},
"M-1": {
"name": "Light Industrial",
"max_height_ft": 75,
"max_lot_coverage_pct": 85,
"min_front_setback_ft": 30,
"min_side_setback_ft": 15,
"max_units": 0,
"parking_req": "1 space per 500 sq ft of floor area",
"notes": "Environmental impact review required above 50,000 sq ft."
}
}
Step 3: Build the proposal parser
The LLM needs structured data to compare against the ordinance. I send the raw proposal text to Oxlo.ai with a JSON mode request so the model extracts fields like height, zoning district, and use type.
import json
def parse_proposal(proposal_text: str) -> dict:
parser_prompt = (
"You are a zoning intake clerk. Extract the following fields from the "
"development proposal text as JSON. Use null if a field is not mentioned. "
"Fields: zoning_district, proposed_use, height_ft, lot_coverage_pct, "
"front_setback_ft, side_setback_ft, estimated_floor_area_sq_ft, units, "
"parking_spaces, commercial_ground_floor."
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": parser_prompt},
{"role": "user", "content": proposal_text},
],
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
# Example
proposal = (
"We propose a four-story mixed-use building in the C-2 district. "
"The structure will be 52 feet tall with 85% lot coverage. "
"Front setback is 12 feet, side setback is 6 feet. "
"Estimated floor area is 24,000 sq ft. Ground floor will be retail. "
"We plan to provide 80 parking spaces."
)
parsed = parse_proposal(proposal)
print(json.dumps(parsed, indent=2))
Step 4: Write the compliance checker
Now I write a function that takes the parsed proposal and the zoning database, then asks the LLM to flag every violation and cite the specific rule. I use deepseek-v3.2 here because it handles structured reasoning and coding tasks well.
def check_compliance(parsed: dict, ordinance: dict) -> dict:
if not ordinance:
return {"error": "Unknown zoning district"}
checker_prompt = (
"You are a senior zoning reviewer. Compare the PROPOSAL against the ORDINANCE. "
"List every numeric or categorical violation. For each issue, state the rule, "
"the proposal value, the limit, and whether it is a violation. "
"Return JSON with keys: summary (string), violations (list), compliant (boolean)."
)
payload = {
"proposal": parsed,
"ordinance": ordinance
}
response = client.chat.completions.create(
model="deepseek-v3.2",
messages=[
{"role": "system", "content": checker_prompt},
{"role": "user", "content": json.dumps(payload, indent=2)},
],
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
compliance = check_compliance(
parsed, ZONING_DB.get(parsed.get("zoning_district"), {})
)
print(json.dumps(compliance, indent=2))
Step 5: Define the agent system prompt
The system prompt is the only part an end user should need to edit to change personality or report format. I keep it strict so the model does not hallucinate ordinance text.
SYSTEM_PROMPT = """
You are ZoningBot, a municipal planning assistant. Your job is to:
1. Accept a development proposal in plain English.
2. Parse it into structured fields.
3. Look up the relevant zoning district from the provided ordinance database.
4. Check compliance and produce a markdown report.
Rules:
- Always cite the specific ordinance field you are checking (for example, max_height_ft).
- If a value exceeds a limit, call it a VIOLATION. If it is within limits, call it COMPLIANT.
- Flag missing information as NEEDS REVIEW rather than guessing.
- Output only the final markdown report. Do not include internal reasoning.
Report format:
# Zoning Compliance Report
## Project Summary
## Applicable Zoning
## Findings
## Recommendations
"""
Step 6: Assemble the agent class
I wire the parser and checker into a single callable agent class that calls Oxlo.ai. I use kimi-k2.6 for the final report generation because it handles long context and structured markdown output well. Because Oxlo.ai charges per request, feeding it long ordinance PDFs later will not spike your bill the way token-based pricing would.
class ZoningAgent:
def __init__(self, client, ordinance_db, model="kimi-k2.6"):
self.client = client
self.ordinance_db = ordinance_db
self.model = model
self.system = SYSTEM_PROMPT
def analyze(self, proposal_text: str) -> str:
# Step A: parse
parser_prompt = (
"Extract these fields as JSON: zoning_district, proposed_use, height_ft, "
"lot_coverage_pct, front_setback_ft, side_setback_ft, "
"estimated_floor_area_sq_ft, units, parking_spaces, commercial_ground_floor. "
"Use null if missing."
)
parse_resp = self.client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": parser_prompt},
{"role": "user", "content": proposal_text},
],
response_format={"type": "json_object"},
)
parsed = json.loads(parse_resp.choices[0].message.content)
# Step B: look up ordinance
zone_key = parsed.get("zoning_district")
ordinance = self.ordinance_db.get(zone_key, {})
# Step C: generate report
user_content = (
f"PROPOSAL JSON:\n{json.dumps(parsed, indent=2)}\n\n"
f"ORDINANCE JSON:\n{json.dumps(ordinance, indent=2)}"
)
report_resp = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self.system},
{"role": "user", "content": user_content},
],
)
return report_resp.choices[0].message.content
agent = ZoningAgent(client, ZONING_DB)
Run it
Pass a natural language proposal to the agent and print the report.
proposal = (
"We propose a four-story mixed-use building in the C-2 district. "
"The structure will be 52 feet tall with 85% lot coverage. "
"Front setback is 12 feet, side setback is 6 feet. "
"Estimated floor area is 24,000 sq ft. Ground floor will be retail. "
"We plan to provide 80 parking spaces."
)
report = agent.analyze(proposal)
print(report)
Expected output:
# Zoning Compliance Report
## Project Summary
A four-story mixed-use building proposed in the C-2 district with ground-floor retail and an estimated 24,000 sq ft of floor area.
## Applicable Zoning
C-2 Neighborhood Commercial
## Findings
- **Height**: 52 ft proposed. Limit is 60 ft. Status: COMPLIANT.
- **Lot Coverage**: 85% proposed. Limit is 80%. Status: VIOLATION. Exceeds by 5 percentage points.
- **Front Setback**: 12 ft proposed. Limit is 10 ft minimum. Status: COMPLIANT.
- **Side Setback**: 6 ft proposed. Limit is 5 ft minimum. Status: COMPLIANT.
- **Parking**: 80 spaces proposed. Requirement is 1 space per 300 sq ft (80 required). Status: COMPLIANT.
- **Ground Floor Retail**: Confirmed present. Status: COMPLIANT.
## Recommendations
Reduce lot coverage to 80% or apply for a C-2 variance. All other metrics appear to meet code.
Next steps
Replace the hardcoded ZONING_DB with a PDF parser that feeds live municipal codes into the agent. Because Oxlo.ai uses request-based pricing, you can stuff those long documents into context without the runaway token costs you would see on other providers. If you want to turn this into a web app, wrap the ZoningAgent class in a FastAPI endpoint and stream the report back with Oxlo.ai's streaming option. See https://oxlo.ai/pricing to compare plans.
Top comments (0)