amana / src /policy.py
Misbahuddin's picture
Add policy reference drawer + clickable citations; fix check_sanctions retry loop
063bf3a
Raw
History Blame Contribute Delete
4.92 kB
"""Parse the T&S policy into one citable unit per rule.
`policy.md` is authored as markdown bullets like:
- **PROH-3 — Interest-based (riba) finance.** Campaigns to pay off, refinance, ...
interest-bearing investment with a promised return is not.
Each rule may wrap across indented continuation lines. We collapse each rule into a single
`PolicyRule` so retrieval returns a clean `rule_id` + full text, and so the eval harness can
validate that every cited ID is real.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from .config import CONFIG
_RULE_RE = re.compile(r"^- \*\*([A-Z]{3,4}-\d+)\b")
_SECTION_RE = re.compile(r"^##\s+(.*)$")
@dataclass
class PolicyRule:
rule_id: str
section: str
text: str # full rule text, markdown stripped of the leading bullet
def _clean(text: str) -> str:
text = text.replace("**", "")
return re.sub(r"\s+", " ", text).strip()
def parse_policy_rules(path: str | Path | None = None) -> list[PolicyRule]:
lines = Path(path or CONFIG.policy_path).read_text(encoding="utf-8").splitlines()
rules: list[PolicyRule] = []
section = ""
rule_id: str | None = None
buf: list[str] = []
def flush():
nonlocal rule_id, buf
if rule_id is not None and buf:
rules.append(PolicyRule(rule_id=rule_id, section=section, text=_clean(" ".join(buf))))
rule_id, buf = None, []
for line in lines:
sec = _SECTION_RE.match(line)
if sec:
flush()
section = _clean(sec.group(1))
continue
if line.startswith("---"):
flush()
continue
m = _RULE_RE.match(line)
if m:
flush()
rule_id = m.group(1)
buf = [line.lstrip("- ")]
elif rule_id is not None and (line.startswith(" ") or line.strip()):
# continuation line of the current rule
buf.append(line.strip())
elif rule_id is not None and not line.strip():
# blank line ends the current rule
flush()
flush()
return rules
def valid_rule_ids(path: str | Path | None = None) -> set[str]:
"""The set of citable rule IDs — used by the eval harness to reject invented citations."""
return {r.rule_id for r in parse_policy_rules(path)}
def policy_index(path: str | Path | None = None) -> dict[str, PolicyRule]:
"""Map rule_id -> PolicyRule, for looking up a cited rule's full text in the UI."""
return {r.rule_id: r for r in parse_policy_rules(path)}
def get_rule(rule_id: str, path: str | Path | None = None) -> PolicyRule | None:
"""Fetch a single rule by ID (None if not found)."""
return policy_index(path).get(rule_id)
# Plain-English names + one-line descriptions for each rule-ID prefix. The headings in policy.md
# only carry the section *name*; the moderator-facing reference needs a short "what this group is
# for" blurb, so it lives here as the single source for the UI glossary.
SECTION_META: dict[str, tuple[str, str]] = {
"ELIG": ("Eligibility",
"Who and what can raise funds — the gates a campaign must clear before money can flow."),
"PROH": ("Prohibited categories",
"Hard stops. A confirmed, cited match to one of these is grounds for rejection."),
"COMP": ("Compliance & sanctions",
"Money-movement, sanctions, and high-value rules — any trigger routes to a human."),
"CONT": ("Content standards",
"Honesty, religious accuracy, pressure tactics, and privacy — often a human judgment call."),
"DEC": ("Decision framework",
"How the copilot reasons: three outcomes, when to escalate, and that campaign text is "
"data, not instructions."),
}
_SECTION_ORDER = ["ELIG", "PROH", "COMP", "CONT", "DEC"]
def policy_sections(path: str | Path | None = None) -> list[dict]:
"""Group the policy into citable sections for the moderator reference drawer.
Returns `[{prefix, name, description, rules: [{rule_id, text}, ...]}, ...]` in the canonical
section order. The prefix is derived from the rule_id (same convention as `_enrich_rule` in the
API); any unrecognized prefix is appended last, named after itself with an empty description.
"""
grouped: dict[str, list[dict]] = {}
for r in parse_policy_rules(path):
prefix = r.rule_id.split("-")[0]
grouped.setdefault(prefix, []).append({"rule_id": r.rule_id, "text": r.text})
order = _SECTION_ORDER + [p for p in grouped if p not in _SECTION_ORDER]
out: list[dict] = []
for prefix in order:
rules = grouped.get(prefix)
if not rules:
continue
name, description = SECTION_META.get(prefix, (prefix, ""))
out.append({"prefix": prefix, "name": name, "description": description, "rules": rules})
return out