We are going to build an LLM-powered zoning analyst that reads land-use proposals and flags conflicts with accessibility and sustainability guidelines. It is useful for city planners and developers who need to review dense policy documents quickly. Because Oxlo.ai uses flat per-request pricing regardless of input length, running multi-page ordinances through this pipeline does not balloon in cost the way token-based billing would. See https://oxlo.ai/pricing for details.
What you'll need
- Python 3.10 or newer
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK installed with
pip install openai
Step 1: Set up the Oxlo.ai client
We will instantiate the OpenAI SDK pointing at Oxlo.ai. The platform is fully compatible, so this is a literal drop-in replacement. I start with a quick connectivity check using Llama 3.3 70B.
from openai import OpenAI
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key="YOUR_OXLO_API_KEY"
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": "Hello from the planning department."}],
max_tokens=20,
)
print(response.choices[0].message.content)
Step 2: Define the system prompt
This prompt turns a general chat model into a planning analyst. Keeping it in a separate variable makes A/B testing easy.
SYSTEM_PROMPT = """You are an urban planning policy analyst. Your job is to review zoning and land-use text for compliance with modern accessibility, sustainability, and transit-oriented development guidelines.
For each input, produce:
1. A concise summary of the proposal.
2. A list of potential conflicts with ADA accessibility, bike lane continuity, or affordable housing minimums.
3. A density-transit alignment score from 1 to 10.
4. One concrete recommendation to improve the proposal.
Be precise. Cite specific phrases from the text when flagging issues."""
Step 3: Build the single-proposal analyzer
The first function takes raw zoning text, wraps it with context, and returns a natural language critique. I use Llama 3.3 70B here because it handles long context windows reliably.
def analyze_zoning(proposal_text: str) -> str:
user_message = f"ZONING PROPOSAL:\n{proposal_text}"
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.2,
)
return response.choices[0].message.content
sample = """
District B-7 Mixed Use. Minimum lot size 8,000 sq ft.
Parking requirement: 1.5 spaces per residential unit.
No bicycle parking mandate. Maximum height 45 ft.
Affordable housing set-aside: 5 percent of units.
"""
print(analyze_zoning(sample))
Step 4: Enforce structured JSON output
Planning departments need machine-readable scores. We will switch to Qwen 3 32B and enable JSON mode so the model returns a predictable schema that our downstream tools can consume.
import json
def analyze_zoning_structured(proposal_text: str) -> dict:
user_message = (
"Return a JSON object evaluating the following zoning proposal. "
"Use exactly these keys: summary, conflicts, density_transit_score, recommendation. "
"Conflicts must be a list of strings. "
f"PROPOSAL:\n{proposal_text}"
)
response = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
response_format={"type": "json_object"},
temperature=0.2,
)
return json.loads(response.choices[0].message.content)
report = analyze_zoning_structured(sample)
print(json.dumps(report, indent=2))
Step 5: Batch process a district plan
Real projects rarely involve a single parcel. We will loop over a list of proposals, collect structured reports, and print a summary for the whole district.
proposals = [
{
"id": "B-7-MU",
"text": "District B-7 Mixed Use. Minimum lot size 8,000 sq ft. Parking: 1.5 spaces per unit. No bicycle parking. Max height 45 ft. Affordable housing: 5 percent."
},
{
"id": "C-3-CD",
"text": "District C-3 Central. Minimum lot size 2,400 sq ft. Parking: 0.75 spaces per unit near transit. Bicycle parking: 1 per 5 units. Max height 85 ft. Affordable housing: 15 percent."
}
]
def batch_review(proposals: list[dict]) -> list[dict]:
results = []
for p in proposals:
report = analyze_zoning_structured(p["text"])
report["proposal_id"] = p["id"]
results.append(report)
return results
if __name__ == "__main__":
reviews = batch_review(proposals)
for r in reviews:
print(f"--- {r['proposal_id']} ---")
print(json.dumps(r, indent=2))
Run it
Execute the script. You should see structured compliance summaries for every proposal in the batch. The output below is representative of what the pipeline returns.
--- B-7-MU ---
{
"summary": "Small-scale mixed-use district with moderate parking mandates and minimal affordability requirements.",
"conflicts": [
"Bicycle parking mandate absent, conflicting with complete streets policy.",
"Affordable housing set-aside of 5 percent falls below the 10 percent city goal.",
"Parking ratio of 1.5 spaces per unit may suppress density near frequent transit."
],
"density_transit_score": 4,
"recommendation": "Reduce parking minimums to 0.5 spaces per unit within 0.5 miles of bus rapid transit and add a 10 percent affordable housing requirement.",
"proposal_id": "B-7-MU"
}
--- C-3-CD ---
{
"summary": "Higher-density central district with transit-adjacent parking reductions and strong affordability requirements.",
"conflicts": [
"None identified."
],
"density_transit_score": 8,
"recommendation": "Consider adding a pedestrian priority corridor requirement along the main arterial to match the district's density.",
"proposal_id": "C-3-CD"
}
Wrap-up and next steps
From here, you can expand the pipeline in two directions. First, add vision analysis of site plans by upgrading to a vision-enabled model like Kimi K2.6 or Gemma 3 27B on Oxlo.ai. Pass base64-encoded site maps in the messages array to check setbacks and green-space ratios visually. Second, layer in public comment analysis by feeding resident feedback into a separate pipeline using DeepSeek V3.2. Extract sentiment and concerns by parcel ID, then merge those results with the zoning reports for a unified dashboard.
Top comments (0)