Spaces:
Running
Running
Download news/classify.py from Success-dev/venthrive-api: direct link, hf CLI and curl.
- Browser
- Download file 5.88 kB
-
https://huggingface.co/spaces/Success-dev/venthrive-api/resolve/main/news/classify.py
- Command line
-
hf download hf://spaces/Success-dev/venthrive-api/news/classify.py
-
curl -L -o classify.py https://huggingface.co/spaces/Success-dev/venthrive-api/resolve/main/news/classify.py
5.88 kB
| """ | |
| Classifies one scraped item's own text directly — no scraping of other | |
| platforms that already classify this kind of news (see the news-system design | |
| discussion referenced in ADR-0015). A constrained-enum + few-shot prompt sent | |
| to the light (20B) Groq model, modeled on DataWiz-Project's bounded, | |
| admin-validated "autofill" step. | |
| """ | |
| import json | |
| from datetime import date | |
| from utils.groq_client import call_groq_json, LIGHT_MODEL | |
| from news.business_types import labels_for_prompt, valid_subset | |
| IMPACTS = ("opportunity", "neutral") | |
| CATEGORIES = ("policy", "weather", "infrastructure", "funding", "security", "other") | |
| _SYSTEM_PROMPT = f"""You classify a single news/bulletin item for VenThrive, a Nigerian small-business platform. | |
| Read the title and text given, then output ONLY a JSON object with these exact fields: | |
| - "impact": one of {list(IMPACTS)} | |
| - "category": one of {list(CATEGORIES)} | |
| - "business_types": a list of 1-4 ids from the taxonomy below that this item affects. Empty list if none clearly apply. | |
| - "location": the Nigerian state, region, or "Nationwide" this affects. Null if genuinely unclear. | |
| - "deadline": an ISO date (YYYY-MM-DD) ONLY if impact is "opportunity" AND the text states an explicit application/closing deadline. Otherwise null. | |
| - "confidence": your confidence in this classification, 0.0 to 1.0. | |
| Business type taxonomy (use these ids exactly, do not invent new ones): | |
| {labels_for_prompt()} | |
| There is no "threat" value. Reporting that something bad already happened has no value to a | |
| business owner who can no longer act on it — a cost increase, a flood, or a security incident is | |
| only useful here if it is a live, ongoing condition the owner can still respond to today (e.g. a | |
| flood warning still in effect, a tax change not yet in force). Forward-looking, predictive risk | |
| ("what's likely to happen") is a separate system, not this classifier's job — don't try to infer | |
| or imply it. | |
| "opportunity" is reserved for something a business owner could actually apply to, access, or | |
| benefit from now: a scheme/grant/program with open eligibility, a new customer segment or channel | |
| opening up, a still-pending deadline. Everything else — including a cost going up, a disruption, | |
| an incident, an aspirational government target, or a roadmap with no program to act on today — is | |
| "neutral". Err toward "neutral" whenever in doubt; "opportunity" is the narrow, specific case. | |
| Examples: | |
| - A state SME grant with an application deadline of 2026-12-01 -> impact=opportunity, category=funding, deadline=2026-12-01 | |
| - A tax holiday for new small businesses with no stated end date -> impact=opportunity, category=policy, deadline=null | |
| - A new CBN forex directive raising import costs -> impact=neutral, category=policy, business_types=[relevant import/trade subsectors], location=Nationwide (nothing a business can apply to or act on, just a cost change) | |
| - A flood warning for Lagos -> impact=neutral, category=weather, business_types=[subsectors with physical premises/stock in the area], location=Lagos | |
| - A plane makes an emergency landing with no stated regulatory fallout -> impact=neutral, category=other (an isolated incident, not a business-operational change) | |
| - A senator publicly criticising a regulator's pace, with no rule actually changed yet -> impact=neutral, category=policy (nothing has changed for a business yet) | |
| - A 10-year national capital-market target or roadmap with no program a business can apply to today -> impact=neutral, category=policy (aspirational, not actionable) | |
| - A stock exchange index falling on a single trading day -> impact=neutral, category=other | |
| If the item is not genuinely business-relevant, or you cannot determine a category, business | |
| type, or location with reasonable confidence, leave the relevant field null/empty rather than | |
| guessing — an incomplete classification is flagged for human review instead of shown to users, | |
| which is the correct outcome for a low-confidence item.""" | |
| class ClassificationResult: | |
| def __init__(self, raw: dict): | |
| impact = raw.get("impact") | |
| category = raw.get("category") | |
| business_types = valid_subset(raw.get("business_types") or []) | |
| location = (raw.get("location") or "").strip() or None | |
| confidence = raw.get("confidence") | |
| deadline_str = raw.get("deadline") | |
| deadline: date | None = None | |
| if deadline_str: | |
| try: | |
| deadline = date.fromisoformat(deadline_str) | |
| except ValueError: | |
| deadline = None # malformed date from the model — treat as not given | |
| self.impact = impact if impact in IMPACTS else None | |
| self.category = category if category in CATEGORIES else None | |
| self.business_types = business_types | |
| self.location = location | |
| self.deadline = deadline if self.impact == "opportunity" else None | |
| self.confidence = float(confidence) if isinstance(confidence, (int, float)) else None | |
| def is_complete(self) -> bool: | |
| """The ADR-0015 completeness bar: category + >=1 business type + location.""" | |
| return bool(self.category and self.business_types and self.location) | |
| def review_reason(self) -> str | None: | |
| if self.is_complete: | |
| return None | |
| missing = [] | |
| if not self.category: missing.append("category") | |
| if not self.business_types: missing.append("business_types") | |
| if not self.location: missing.append("location") | |
| return f"Missing: {', '.join(missing)}" | |
| def classify_item(title: str, text: str, fixture: str | None = None) -> ClassificationResult: | |
| user_prompt = f"Title: {title}\n\nText: {text[:4000]}" | |
| raw = call_groq_json(_SYSTEM_PROMPT, user_prompt, model=LIGHT_MODEL, max_tokens=500, fixture=fixture) | |
| if isinstance(raw, str): | |
| raw = json.loads(raw) | |
| return ClassificationResult(raw) | |