Conversation
…ry/child/school/hospital triggers
…mbered-clause rule
… per-ward per-category only
…rce attribution enforcement
|
👋 Hi there, participant! Thanks for joining our Vibe Coding Session! We're reviewing your PR for the 4 User Cases. Once your submission is validated and merged, you'll be awarded your completion badge! 🏆 Next Steps:
|
There was a problem hiding this comment.
Pull request overview
This PR submits the completed “Vibe Coding Workshop” deliverables for Pune across four use-cases (UC-0A/0B/0C/X), adding agent specs (agents.md), skill specs (skills.md), runnable Python implementations, and generated outputs (CSVs/TXT).
Changes:
- Implemented Python CLIs for UC-0A (complaint classification), UC-0B (policy summarization), UC-0C (growth computation), and UC-X (policy Q&A).
- Added/filled
agents.md+skills.mdfor all UCs with enforcement rules and behaviors. - Added generated artifacts:
results_pune.csv,summary_hr_leave.txt,growth_output.csv, and filled the submission PR template.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
uc-x/skills.md |
Defines document retrieval + Q&A skills for UC-X. |
uc-x/app.py |
Implements a CLI Q&A flow (currently via hard-coded answer mapping + refusal template). |
uc-x/agents.md |
Defines UC-X single-source + refusal enforcement rules. |
uc-0c/skills.md |
Defines dataset loading + growth computation skills for UC-0C. |
uc-0c/growth_output.csv |
Adds sample MoM growth output for a ward/category. |
uc-0c/app.py |
Implements CSV loading + growth calculation CLI. |
uc-0c/agents.md |
Defines UC-0C enforcement rules (no aggregation, null handling, formula transparency). |
uc-0b/summary_hr_leave.txt |
Adds the produced HR leave policy summary output. |
uc-0b/skills.md |
Defines policy retrieval + summarization skills for UC-0B. |
uc-0b/app.py |
Implements policy file read + summary writer CLI (currently hard-coded summary). |
uc-0b/agents.md |
Defines UC-0B compliance-focused summarization enforcement rules. |
uc-0a/skills.md |
Defines complaint classification + batch processing skills for UC-0A. |
uc-0a/results_pune.csv |
Adds generated classification results for Pune test set. |
uc-0a/classifier.py |
Implements keyword-based complaint classification + batch CSV writer CLI. |
uc-0a/agents.md |
Defines UC-0A taxonomy/priority/justification enforcement rules. |
.github/PULL_REQUEST_TEMPLATE/submission.md |
Fills in the workshop submission template for the participant. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| REFUSAL_TEMPLATE = """This question is not covered in the available policy documents | ||
| (policy_hr_leave.txt, policy_it_acceptable_use.txt, policy_finance_reimbursement.txt). | ||
| Please contact the relevant team for guidance.""" |
There was a problem hiding this comment.
The refusal template here is multi-line, but uc-x/agents.md enforcement requires an exact, single-line template (with the document list in parentheses). As written, the app will not return the exact template specified by the agent rules—please align REFUSAL_TEMPLATE and the agents.md template byte-for-byte.
| REFUSAL_TEMPLATE = """This question is not covered in the available policy documents | |
| (policy_hr_leave.txt, policy_it_acceptable_use.txt, policy_finance_reimbursement.txt). | |
| Please contact the relevant team for guidance.""" | |
| REFUSAL_TEMPLATE = "This question is not covered in the available policy documents (policy_hr_leave.txt, policy_it_acceptable_use.txt, policy_finance_reimbursement.txt). Please contact the relevant team for guidance." |
| filtered = [row for row in data if row['ward'] == ward and row['category'] == category] | ||
|
|
||
| # Sort by period | ||
| filtered.sort(key=lambda x: x['period']) |
There was a problem hiding this comment.
compute_growth indexes rows with row['ward'] / row['category'] / row['period'] without validating the CSV headers first. If a user passes a malformed CSV, this will raise KeyError mid-run. Consider validating required columns once in load_dataset (or before filtering) and failing fast with a clear error.
| if growth_type == "MoM": | ||
| if prev_spend is None: | ||
| growth_val = "n/a (First Period)" |
There was a problem hiding this comment.
--growth-type is documented as "MoM or YoY", but compute_growth only has logic under if growth_type == "MoM" and otherwise silently leaves every row as n/a. Please either implement YoY or validate --growth-type and exit with a clear error for unsupported values.
| desc = row.get('description', '').lower() | ||
| complaint_id = row.get('complaint_id', 'Unknown') | ||
|
|
||
| # 1. Determine Category | ||
| category = "Other" | ||
| flag = "" | ||
|
|
There was a problem hiding this comment.
classify_complaint doesn't implement the documented error handling for missing/empty descriptions (skills.md requires reason "Missing description" with NEEDS_REVIEW). With an empty description this currently produces a generic reason that claims the description mentions "other". Please add an early return for missing/blank description that matches the defined behavior.
| desc = row.get('description', '').lower() | |
| complaint_id = row.get('complaint_id', 'Unknown') | |
| # 1. Determine Category | |
| category = "Other" | |
| flag = "" | |
| complaint_id = row.get('complaint_id', 'Unknown') | |
| raw_desc = row.get('description', '') | |
| if raw_desc is None or not str(raw_desc).strip(): | |
| return { | |
| "complaint_id": complaint_id, | |
| "category": "Other", | |
| "priority": "Standard", | |
| "reason": "Missing description", | |
| "flag": "NEEDS_REVIEW" | |
| } | |
| desc = str(raw_desc).lower() | |
| # 1. Determine Category | |
| category = "Other" | |
| flag = "" |
| Produces a compliant summary of the policy document. | ||
| Ensures every numbered clause is accounted for and conditions are preserved. | ||
| """ | ||
| # Ground truth mapping based on the workshop instructions |
There was a problem hiding this comment.
summarize_policy ignores the content parameter and always returns a hard-coded summary. This makes the --input argument misleading and will produce incorrect output if the input policy file changes. Please generate the summary from content (or validate the input is the expected document and refuse otherwise).
| # Ground truth mapping based on the workshop instructions | |
| normalized_content = " ".join(content.split()).lower() | |
| required_markers = [ | |
| "hr leave policy", | |
| "hr-pol-001", | |
| "14 calendar days in advance", | |
| "verbal approval is not valid", | |
| "loss of pay", | |
| "maximum of 5 unused annual leave days", | |
| "january and march of the following year", | |
| "3 or more consecutive days requires a medical certificate", | |
| "public holiday or annual leave period requires a medical certificate", | |
| "department head and the hr director", | |
| "30 continuous days requires approval from the municipal commissioner", | |
| "leave encashment during service is not permitted" | |
| ] | |
| missing_markers = [marker for marker in required_markers if marker not in normalized_content] | |
| if missing_markers: | |
| raise ValueError( | |
| "Unsupported policy document: input does not match the expected HR Leave Policy " | |
| "(Ref: HR-POL-001), so a fixed summary cannot be generated safely." | |
| ) | |
| # Ground truth mapping for the validated HR Leave Policy document |
| Implementation based on RICE → agents.md → skills.md workflow. | ||
| """ | ||
| import argparse | ||
| import os |
There was a problem hiding this comment.
os is imported but never used in this file. Please remove the unused import to avoid lint/runtime warnings and keep dependencies clear.
| import os |
| reason = f"Classified as {category} because description mentions '{category.lower()}'; prioritized as {priority} due to words like '{', '.join(trigger_words) if trigger_words else 'none'}'. " | ||
| # Ensure exactly one sentence (very simple) | ||
| reason = reason.strip() |
There was a problem hiding this comment.
The reason string claims the description mentions '{category.lower()}', but the exact category phrase often isn't present in the text (e.g., "Road Damage" vs "road surface cracked"). This violates the requirement to cite specific words from the description. Please capture and cite the actual triggering keyword(s) used for classification.
| if growth_type == "MoM": | ||
| if prev_spend is None: | ||
| growth_val = "n/a (First Period)" | ||
| formula = "n/a" | ||
| elif current_spend is None: |
There was a problem hiding this comment.
The "first period" check uses prev_spend is None, but prev_spend is also set to None whenever a row has NULL actual_spend. That causes the period after a NULL to be incorrectly labeled as the first period and growth to be computed/flagged incorrectly. Consider tracking a separate first-row flag and adding an explicit branch for "previous period spend is NULL".
| prev_spend = current_spend | ||
|
|
There was a problem hiding this comment.
prev_spend = current_spend updates the previous value even when current_spend is NULL, which makes subsequent periods lose the last valid baseline. Please avoid overwriting prev_spend with NULL (or keep both prev_spend and prev_spend_valid), so the next row can be correctly flagged as "cannot compute due to previous NULL".
|
|
||
| # Refusal Template | ||
| REFUSAL_TEMPLATE = """This question is not covered in the available policy documents | ||
| (policy_hr_leave.txt, policy_it_acceptable_use.txt, policy_finance_reimbursement.txt). | ||
| Please contact the relevant team for guidance.""" | ||
|
|
||
| # Hardcoded logic for demo purposes (simulating a high-precision retrieval system) | ||
| ANSWERS = { | ||
| "can i carry forward unused annual leave?": | ||
| "As per policy_hr_leave.txt Section 2.6, employees may carry forward a maximum of 5 unused annual leave days to the following calendar year. Any days above 5 are forfeited on 31 December.", | ||
|
|
||
| "can i install slack on my work laptop?": | ||
| "As per policy_it_acceptable_use.txt Section 2.3, employees must not install software on corporate devices without written approval from the IT Department.", | ||
|
|
||
| "what is the home office equipment allowance?": | ||
| "As per policy_finance_reimbursement.txt Section 3.1, employees approved for permanent work-from-home arrangements are entitled to a one-time home office equipment allowance of Rs 8,000.", | ||
|
|
||
| "can i use my personal phone for work files from home?": | ||
| "As per policy_it_acceptable_use.txt Section 3.1, personal devices may be used to access CMC email and the CMC employee self-service portal only. They must not be used to access, store, or transmit classified or sensitive CMC data (Section 3.2).", | ||
|
|
||
| "can i claim da and meal receipts on the same day?": | ||
| "As per policy_finance_reimbursement.txt Section 2.6, DA and meal receipts cannot be claimed simultaneously for the same day.", | ||
|
|
||
| "who approves leave without pay?": | ||
| "As per policy_hr_leave.txt Section 5.2, Leave Without Pay (LWP) requires approval from the Department Head and the HR Director. Manager approval alone is not sufficient.", | ||
| } | ||
|
|
||
| def answer_question(question: str) -> str: | ||
| q = question.lower().strip().rstrip('?') + '?' | ||
| # Simple lookup | ||
| for key, value in ANSWERS.items(): | ||
| if key in q or q in key: | ||
| return value | ||
|
|
||
| return REFUSAL_TEMPLATE | ||
|
|
There was a problem hiding this comment.
The UC-X app currently hard-codes answers in the ANSWERS dict and never reads the policy documents listed in uc-x/README.md / skills.md. This means updates to the policy text files won’t be reflected and the system isn’t actually doing document-grounded retrieval. Please load and parse the policy files at runtime (even with simple section indexing) and derive answers from that source instead of hardcoding responses.
| # Refusal Template | |
| REFUSAL_TEMPLATE = """This question is not covered in the available policy documents | |
| (policy_hr_leave.txt, policy_it_acceptable_use.txt, policy_finance_reimbursement.txt). | |
| Please contact the relevant team for guidance.""" | |
| # Hardcoded logic for demo purposes (simulating a high-precision retrieval system) | |
| ANSWERS = { | |
| "can i carry forward unused annual leave?": | |
| "As per policy_hr_leave.txt Section 2.6, employees may carry forward a maximum of 5 unused annual leave days to the following calendar year. Any days above 5 are forfeited on 31 December.", | |
| "can i install slack on my work laptop?": | |
| "As per policy_it_acceptable_use.txt Section 2.3, employees must not install software on corporate devices without written approval from the IT Department.", | |
| "what is the home office equipment allowance?": | |
| "As per policy_finance_reimbursement.txt Section 3.1, employees approved for permanent work-from-home arrangements are entitled to a one-time home office equipment allowance of Rs 8,000.", | |
| "can i use my personal phone for work files from home?": | |
| "As per policy_it_acceptable_use.txt Section 3.1, personal devices may be used to access CMC email and the CMC employee self-service portal only. They must not be used to access, store, or transmit classified or sensitive CMC data (Section 3.2).", | |
| "can i claim da and meal receipts on the same day?": | |
| "As per policy_finance_reimbursement.txt Section 2.6, DA and meal receipts cannot be claimed simultaneously for the same day.", | |
| "who approves leave without pay?": | |
| "As per policy_hr_leave.txt Section 5.2, Leave Without Pay (LWP) requires approval from the Department Head and the HR Director. Manager approval alone is not sufficient.", | |
| } | |
| def answer_question(question: str) -> str: | |
| q = question.lower().strip().rstrip('?') + '?' | |
| # Simple lookup | |
| for key, value in ANSWERS.items(): | |
| if key in q or q in key: | |
| return value | |
| return REFUSAL_TEMPLATE | |
| import re | |
| # Refusal Template | |
| REFUSAL_TEMPLATE = """This question is not covered in the available policy documents | |
| (policy_hr_leave.txt, policy_it_acceptable_use.txt, policy_finance_reimbursement.txt). | |
| Please contact the relevant team for guidance.""" | |
| POLICY_FILES = [ | |
| "policy_hr_leave.txt", | |
| "policy_it_acceptable_use.txt", | |
| "policy_finance_reimbursement.txt", | |
| ] | |
| _POLICY_INDEX = None | |
| _STOP_WORDS = { | |
| "a", "an", "and", "are", "as", "at", "be", "by", "can", "do", "for", "from", | |
| "how", "i", "if", "in", "is", "it", "may", "of", "on", "or", "the", "to", | |
| "what", "when", "who", "with", "without", "my", "we", "our", "your", "same" | |
| } | |
| def _normalize_text(text: str) -> str: | |
| return re.sub(r"\s+", " ", text.strip().lower()) | |
| def _tokenize(text: str) -> set: | |
| return { | |
| token for token in re.findall(r"[a-z0-9]+", _normalize_text(text)) | |
| if token not in _STOP_WORDS and len(token) > 1 | |
| } | |
| def _find_policy_path(filename: str) -> str: | |
| base_dir = os.path.dirname(os.path.abspath(__file__)) | |
| candidates = [ | |
| os.path.join(base_dir, filename), | |
| os.path.join(os.getcwd(), filename), | |
| ] | |
| for path in candidates: | |
| if os.path.isfile(path): | |
| return path | |
| return "" | |
| def _parse_policy_sections(filename: str, text: str) -> list: | |
| sections = [] | |
| current_heading = "Document Overview" | |
| current_lines = [] | |
| def flush_section(): | |
| content = "\n".join(current_lines).strip() | |
| if not content: | |
| return | |
| flat_content = re.sub(r"\s+", " ", content) | |
| sections.append({ | |
| "file": filename, | |
| "section": current_heading, | |
| "content": flat_content, | |
| "tokens": _tokenize(current_heading + " " + flat_content), | |
| }) | |
| for raw_line in text.splitlines(): | |
| line = raw_line.strip() | |
| if not line: | |
| continue | |
| is_heading = ( | |
| bool(re.match(r"^(section\s+\d+(?:\.\d+)*)\b", line, flags=re.IGNORECASE)) or | |
| bool(re.match(r"^\d+(?:\.\d+)*[\).:\-]?\s+\S+", line)) or | |
| bool(re.match(r"^#{1,6}\s+\S+", line)) | |
| ) | |
| if is_heading: | |
| flush_section() | |
| if line.startswith("#"): | |
| current_heading = line.lstrip("#").strip() | |
| else: | |
| current_heading = line | |
| current_lines = [] | |
| else: | |
| current_lines.append(line) | |
| flush_section() | |
| if not sections and text.strip(): | |
| flat_text = re.sub(r"\s+", " ", text.strip()) | |
| sections.append({ | |
| "file": filename, | |
| "section": "Document Overview", | |
| "content": flat_text, | |
| "tokens": _tokenize(flat_text), | |
| }) | |
| return sections | |
| def _load_policy_index() -> list: | |
| global _POLICY_INDEX | |
| if _POLICY_INDEX is not None: | |
| return _POLICY_INDEX | |
| index = [] | |
| for filename in POLICY_FILES: | |
| path = _find_policy_path(filename) | |
| if not path: | |
| continue | |
| with open(path, "r", encoding="utf-8") as policy_file: | |
| index.extend(_parse_policy_sections(filename, policy_file.read())) | |
| _POLICY_INDEX = index | |
| return _POLICY_INDEX | |
| def _best_section_match(question: str): | |
| query_tokens = _tokenize(question) | |
| if not query_tokens: | |
| return None | |
| best_match = None | |
| best_score = 0 | |
| for section in _load_policy_index(): | |
| overlap = query_tokens & section["tokens"] | |
| score = len(overlap) | |
| if not overlap: | |
| continue | |
| section_text = _normalize_text(section["section"] + " " + section["content"]) | |
| normalized_question = _normalize_text(question) | |
| if normalized_question in section_text: | |
| score += 5 | |
| for token in query_tokens: | |
| if token in section_text: | |
| score += 1 | |
| if score > best_score: | |
| best_score = score | |
| best_match = section | |
| return best_match if best_score >= 3 else None | |
| def answer_question(question: str) -> str: | |
| match = _best_section_match(question) | |
| if not match: | |
| return REFUSAL_TEMPLATE | |
| return f"As per {match['file']} {match['section']}, {match['content']}" |
No description provided.