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
18 changes: 7 additions & 11 deletions uc-0a/agents.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
# agents.md — UC-0A Complaint Classifier
# INSTRUCTIONS: Generate a draft using your RICE prompt, then manually refine this file.
# Delete these comments before committing.

role: >
[FILL IN: Who is this agent? What is its operational boundary?]
You are a municipal complaint classifier. Your job is to read citizen complaints and classify them into predefined categories with correct priority and justification.

intent: >
[FILL IN: What does a correct output look like — make it verifiable]
Classify each complaint into a strict taxonomy, assigning correct priority based on severity keywords, and providing a verifiable, single-sentence reason citing the original text. Identify ambiguous complaints for human review without false confidence.

context: >
[FILL IN: What information is the agent allowed to use? State exclusions explicitly.]
You receive citizen complaint descriptions. You must follow the rules in this file exactly. Do not invent sub-categories or default to external reasoning. Ensure outputs perfectly match the exact strings for categories and priority conditions specified.

enforcement:
- "[FILL IN: Specific testable rule 1 — e.g. Category must be exactly one of: Pothole, Flooding, ...]"
- "[FILL IN: Specific testable rule 2 — e.g. Priority must be Urgent if description contains: injury, child, school, ...]"
- "[FILL IN: Specific testable rule 3 — e.g. Every output row must include a reason field citing specific words from the description]"
- "[FILL IN: Refusal condition — e.g. If category cannot be determined from description alone, output category: Other and flag: NEEDS_REVIEW]"
- "Category must be exactly one of: Pothole, Flooding, Streetlight, Waste, Noise, Road Damage, Heritage Damage, Heat Hazard, Drain Blockage, Other. Exact strings only — no variations."
- "Priority must be Urgent if any of these severity keywords are present: injury, child, school, hospital, ambulance, fire, hazard, fell, collapse. Otherwise use Standard or Low."
- "The reason field must be exactly one sentence and must cite specific words from the description to justify the classification."
- "Set the flag field to NEEDS_REVIEW when the category is genuinely ambiguous; otherwise leave it blank."
247 changes: 234 additions & 13 deletions uc-0a/classifier.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,255 @@
"""
UC-0A — Complaint Classifier
Starter file. Build this using the RICE → agents.md → skills.md → CRAFT workflow.
Rule-based implementation driven by RICE enforcement rules defined in agents.md and skills.md.
No external LLM dependency — classification is deterministic and fully testable.
"""
import argparse
import csv
import sys

# ──────────────────────────────────────────────
# ENFORCEMENT: Exact allowed values (agents.md)
# ──────────────────────────────────────────────
ALLOWED_CATEGORIES = [
"Pothole",
"Flooding",
"Streetlight",
"Waste",
"Noise",
"Road Damage",
"Heritage Damage",
"Heat Hazard",
"Drain Blockage",
"Other",
]

ALLOWED_PRIORITIES = ["Urgent", "Standard", "Low"]

# ENFORCEMENT: Severity keywords that MUST trigger Urgent priority (agents.md)
URGENT_KEYWORDS = [
"injury", "child", "school", "hospital", "ambulance",
"fire", "hazard", "fell", "collapse",
]

# ──────────────────────────────────────────────────────────────────────────────
# CATEGORY RULES — ordered from most specific to most general.
# Each rule is a dict: { "category": str, "keywords": list[str], "reason_template": str }
# The first rule whose ANY keyword matches the description wins.
# ──────────────────────────────────────────────────────────────────────────────
CATEGORY_RULES = [
{
"category": "Heritage Damage",
"keywords": ["heritage", "historical", "monument", "old city"],
"reason_template": "Description mentions '{matched}', indicating damage or concern at a heritage location.",
},
{
"category": "Heat Hazard",
"keywords": ["heat", "temperature", "hot", "heatwave", "sunstroke"],
"reason_template": "Description mentions '{matched}', indicating a heat-related hazard.",
},
{
"category": "Drain Blockage",
"keywords": ["drain blocked", "drain blockage", "blocked drain", "drain choked", "clogged drain"],
"reason_template": "Description mentions '{matched}', indicating a blocked or choked drain.",
},
{
"category": "Flooding",
"keywords": ["flood", "flooded", "waterlogged", "knee-deep", "submerged", "inundated", "standing water"],
"reason_template": "Description mentions '{matched}', indicating a flooding situation.",
},
{
"category": "Pothole",
"keywords": ["pothole", "pot hole", "crater", "tyre damage", "tyre burst"],
"reason_template": "Description mentions '{matched}', indicating a pothole on the road.",
},
{
"category": "Streetlight",
"keywords": ["streetlight", "street light", "light out", "lights out", "lamp", "flickering", "sparking light", "no light", "dark at night"],
"reason_template": "Description mentions '{matched}', indicating a streetlight malfunction.",
},
{
"category": "Waste",
"keywords": ["garbage", "waste", "rubbish", "trash", "litter", "dump", "dumped", "dead animal", "overflowing bin", "bins"],
"reason_template": "Description mentions '{matched}', indicating a waste or garbage issue.",
},
{
"category": "Noise",
"keywords": ["noise", "loud", "music", "sound", "midnight", "midnight music", "blaring"],
"reason_template": "Description mentions '{matched}', indicating an excessive noise complaint.",
},
{
"category": "Road Damage",
"keywords": ["road damage", "cracked road", "road surface", "sinking", "manhole", "manhole cover", "broken road", "footpath", "tiles broken", "upturned"],
"reason_template": "Description mentions '{matched}', indicating road surface damage or a hazardous road condition.",
},
]


def _find_urgent_keyword(description: str) -> str | None:
"""Return the first severity keyword found in the description, else None."""
lower = description.lower()
for kw in URGENT_KEYWORDS:
if kw in lower:
return kw
return None


def _determine_category(description: str) -> tuple[str, str]:
"""
Match description against CATEGORY_RULES (first-match wins).
Returns (category, reason_sentence).
Falls back to ('Other', reason) if no rule matches.
"""
lower = description.lower()
for rule in CATEGORY_RULES:
for kw in rule["keywords"]:
if kw in lower:
reason = rule["reason_template"].replace("{matched}", f"'{kw}'")
return rule["category"], reason

# No rule matched — return Other with NEEDS_REVIEW flag
return "Other", "No matching category keyword found in description; requires manual review."


def _determine_priority(description: str, category: str) -> tuple[str, str]:
"""
Determine priority per enforcement rules:
- Urgent if any severity keyword is present in the description.
- Standard otherwise (default for actionable complaints).
- Low for Noise complaints with no severity keyword (low public safety impact).
Returns (priority, urgent_keyword_matched_or_empty).
"""
matched_kw = _find_urgent_keyword(description)
if matched_kw:
return "Urgent", matched_kw

if category == "Noise":
return "Low", ""

return "Standard", ""


def classify_complaint(row: dict) -> dict:
"""
Classify a single complaint row.
Returns: dict with keys: complaint_id, category, priority, reason, flag

TODO: Build this using your AI tool guided by your agents.md and skills.md.
Your RICE enforcement rules must be reflected in this function's behaviour.

Implements skills.md → classify_complaint:
Input : dict with at minimum a 'description' key (plus optional 'complaint_id').
Output: dict with keys: complaint_id, category, priority, reason, flag.

Enforcement (agents.md):
- category : Exact match from ALLOWED_CATEGORIES only.
- priority : 'Urgent' when URGENT_KEYWORDS found; 'Standard' or 'Low' otherwise.
- reason : One sentence citing specific words from the description.
- flag : 'NEEDS_REVIEW' when category is genuinely ambiguous; else blank.
"""
raise NotImplementedError("Build this using your AI tool + RICE prompt")
complaint_id = row.get("complaint_id", "UNKNOWN").strip()
description = row.get("description", "").strip()

if not description:
# skills.md error_handling: invalid/empty input → flag for review
return {
"complaint_id": complaint_id,
"category": "Other",
"priority": "Low",
"reason": "No description provided; cannot classify without input text.",
"flag": "NEEDS_REVIEW",
}

category, base_reason = _determine_category(description)
priority, urgent_kw = _determine_priority(description, category)

# Build the final one-sentence reason citing specific words
if urgent_kw:
reason = (
f"{base_reason.rstrip('.')} — classified as Urgent because the word '{urgent_kw}' "
f"is present in the description."
)
else:
reason = base_reason

# ENFORCEMENT: flag ambiguous / uncategorised complaints
flag = "NEEDS_REVIEW" if category == "Other" else ""

# Sanity-guard: ensure values are strictly within allowed sets
assert category in ALLOWED_CATEGORIES, f"BUG: category '{category}' not in allowed list"
assert priority in ALLOWED_PRIORITIES, f"BUG: priority '{priority}' not in allowed list"

def batch_classify(input_path: str, output_path: str):
return {
"complaint_id": complaint_id,
"category": category,
"priority": priority,
"reason": reason,
"flag": flag,
}


def batch_classify(input_path: str, output_path: str) -> None:
"""
Read input CSV, classify each row, write results CSV.

TODO: Build this using your AI tool.
Must: flag nulls, not crash on bad rows, produce output even if some rows fail.
Implements skills.md → batch_classify:
Input : filepath to CSV with complaint rows (description column required).
Output: filepath for results CSV (complaint_id, category, priority, reason, flag).

Error handling (skills.md):
- Missing / unreadable input → print error and exit cleanly (no crash).
- Bad individual rows → mark as Other + NEEDS_REVIEW, continue processing.
- Output written even if some rows fail.
"""
raise NotImplementedError("Build this using your AI tool + RICE prompt")
# skills.md: halt and report if input file is inaccessible
try:
f_in = open(input_path, newline="", encoding="utf-8")
except FileNotFoundError:
print(f"[ERROR] Input file not found: {input_path}", file=sys.stderr)
sys.exit(1)
except PermissionError:
print(f"[ERROR] Permission denied reading: {input_path}", file=sys.stderr)
sys.exit(1)

results = []
with f_in:
reader = csv.DictReader(f_in)
if "description" not in (reader.fieldnames or []):
print(
f"[ERROR] Input CSV is missing required 'description' column. "
f"Found columns: {reader.fieldnames}",
file=sys.stderr,
)
sys.exit(1)

# Preserve all input columns and append classification fields
OUTPUT_FIELDS = list(reader.fieldnames)
for field in ["category", "priority", "reason", "flag"]:
if field not in OUTPUT_FIELDS:
OUTPUT_FIELDS.append(field)

for line_num, row in enumerate(reader, start=2): # 1-indexed; row 1 = header
try:
classification = classify_complaint(row)
result = {**row, **classification}
except Exception as exc:
# skills.md: do not crash on bad rows — flag and continue
complaint_id = row.get("complaint_id", f"ROW-{line_num}")
print(f"[WARN] Row {line_num} ({complaint_id}) failed classification: {exc}", file=sys.stderr)
result = {
**row,
"category": "Other",
"priority": "Low",
"reason": f"Classification error on this row: {exc}",
"flag": "NEEDS_REVIEW",
}
results.append(result)

with open(output_path, "w", newline="", encoding="utf-8") as f_out:
writer = csv.DictWriter(f_out, fieldnames=OUTPUT_FIELDS)
writer.writeheader()
writer.writerows(results)

print(f"[INFO] Classified {len(results)} complaint(s).")


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="UC-0A Complaint Classifier")
parser.add_argument("--input", required=True, help="Path to test_[techm].csv")
parser.add_argument("--input", required=True, help="Path to test_[city].csv")
parser.add_argument("--output", required=True, help="Path to write results CSV")
args = parser.parse_args()
batch_classify(args.input, args.output)
Expand Down
16 changes: 16 additions & 0 deletions uc-0a/results_ahmedabad.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
complaint_id,date_raised,city,ward,location,description,reported_by,days_open,category,priority,reason,flag
AM-202401,2024-04-24,Ahmedabad,Ward 13 – Sabarmati,Kankaria Lake promenade,Tarmac surface melting at 44°C. Footwear sticking. Park users unsafe.,Email,1,Other,Standard,No matching category keyword found in description; requires manual review.,NEEDS_REVIEW
AM-202402,2024-04-23,Ahmedabad,Ward 12 – Maninagar,"Naroda Industrial Area, main gate",Metal bus shelter reaching dangerous temperatures. Commuters refusing to use.,Citizen Portal,4,Heat Hazard,Standard,"Description mentions ''temperature'', indicating a heat-related hazard.",
AM-202405,2024-05-08,Ahmedabad,Ward 11 – Vastrapur,"Sabarmati Riverfront, North section",Dead trees with split branches. Fall risk to walkers. 3 trees affected.,WhatsApp Helpline,19,Other,Standard,No matching category keyword found in description; requires manual review.,NEEDS_REVIEW
AM-202406,2024-04-21,Ahmedabad,Ward 15 – Chandkheda,"Chandkheda, Sector 22 park",Irrigation system broken. Grass dying in heatwave conditions.,Phone Helpline,18,Heat Hazard,Standard,"Description mentions ''heat'', indicating a heat-related hazard.",
AM-202407,2024-04-25,Ahmedabad,Ward 15 – Chandkheda,"Thaltej, residential park",Broken bench and upturned paving. Child injured last week.,Email,20,Road Damage,Urgent,"Description mentions ''upturned'', indicating road surface damage or a hazardous road condition — classified as Urgent because the word 'child' is present in the description.",
AM-202410,2024-05-02,Ahmedabad,Ward 12 – Maninagar,SG Highway near Prahladnagar,Pothole on main highway causing morning rush lane closure.,Ward Office Walk-in,4,Pothole,Standard,"Description mentions ''pothole'', indicating a pothole on the road.",
AM-202414,2024-04-20,Ahmedabad,Ward 14 – Odhav,"Jodhpur Village, off SG Highway",Residential colony unlit after 9pm. Wiring theft reported.,Citizen Portal,10,Other,Standard,No matching category keyword found in description; requires manual review.,NEEDS_REVIEW
AM-202417,2024-05-08,Ahmedabad,Ward 13 – Sabarmati,"Manek Chowk, Old City",Night market waste not cleared before morning. Heritage area affected.,Councillor Referral,21,Heritage Damage,Standard,"Description mentions ''heritage'', indicating damage or concern at a heritage location.",
AM-202421,2024-05-03,Ahmedabad,Ward 14 – Odhav,"CG Road, commercial zone",Club music audible at residential buildings at 2am.,Citizen Portal,18,Noise,Low,"Description mentions ''music'', indicating an excessive noise complaint.",
AM-202424,2024-05-04,Ahmedabad,Ward 11 – Vastrapur,"Shahibaug, near zoo",Zoo approach road surface bubbling at 45°C. Visitor complaints.,Councillor Referral,19,Road Damage,Standard,"Description mentions ''road surface'', indicating road surface damage or a hazardous road condition.",
AM-202429,2024-04-24,Ahmedabad,Ward 11 – Vastrapur,Ellis Bridge walking track,River walk surface temperature unbearable. Installed temperature reads 52°C.,Councillor Referral,3,Heat Hazard,Standard,"Description mentions ''temperature'', indicating a heat-related hazard.",
AM-202431,2024-04-26,Ahmedabad,Ward 14 – Odhav,"Paldi, Ratanpol area",Old city road subsidence near ancient step well. Heritage concern.,Citizen Portal,18,Heritage Damage,Standard,"Description mentions ''heritage'', indicating damage or concern at a heritage location.",
AM-202435,2024-05-07,Ahmedabad,Ward 11 – Vastrapur,"New CG Road, Chandkheda",Black metal road dividers storing heat. Motorists reporting burns on contact.,WhatsApp Helpline,16,Heat Hazard,Standard,"Description mentions ''heat'', indicating a heat-related hazard.",
AM-202444,2024-05-15,Ahmedabad,Ward 12 – Maninagar,"Jivraj Park, commercial area",Restaurant waste bins overflowing on Sunday night. Health risk.,Councillor Referral,20,Waste,Standard,"Description mentions ''waste'', indicating a waste or garbage issue.",
AM-202445,2024-04-19,Ahmedabad,Ward 13 – Sabarmati,Ranip Bus Rapid Transit stop,BRT shelter roof glass broken. Users exposed to full sun.,Councillor Referral,9,Other,Standard,No matching category keyword found in description; requires manual review.,NEEDS_REVIEW
Loading