Spaces:
Running
Running
File size: 5,880 Bytes
1f5f31a a32af27 1f5f31a a32af27 b44e8fd 1f5f31a a32af27 b44e8fd a32af27 1f5f31a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | """
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
@property
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)
|