spb/uqo-research-db
Public
1# ============================================================2# SPID — project taxonomy & lightweight value extractors3#4# Shared by both the rule-based and the LLM extractor so that5# `project_type`, `technology`, money, location and dates are6# normalised the same way regardless of backend.7# ============================================================8from __future__ import annotations910import re1112# ----------------------------------------------------------------------13# Canonical project types. `signals` are high-precision regex fragments14# (already lower-cased) used by the rule extractor and as the controlled15# vocabulary the LLM is asked to classify into.16# ----------------------------------------------------------------------17PROJECT_TYPES: dict[str, dict] = {18 "manufacturing_expansion": {19 "label": "Manufacturing / Capacity Expansion",20 "signals": [21 r"(expand|increas\w+|add\w*|ramp\w*|scal\w+|new|build\w*|construct\w*|open\w*)"22 r"[^.]{0,60}(manufactur\w+|production|plant|factory|facilit\w+|capacity|assembly|fab\b)",23 ],24 },25 "plant_construction": {26 "label": "Plant / Site Construction",27 "signals": [r"(construct\w*|build\w*|broke ground|commission\w*|new (plant|site|facility))"28 r"[^.]{0,60}(plant|facility|site|campus|complex|refinery|mill)"],29 },30 "data_center": {31 "label": "Data Center",32 "signals": [r"data\s?cent(er|re)s?", r"hyperscal\w+", r"server farm"],33 },34 "cloud_migration": {35 "label": "Cloud Migration",36 "signals": [r"(migrat\w+|mov\w+|transition\w*)[^.]{0,40}(cloud)",37 r"cloud (migration|platform|adoption|infrastructure|first)"],38 },39 "ai_initiative": {40 "label": "AI / ML Initiative",41 "signals": [r"\b(artificial intelligence|machine learning|generative ai|gen ?ai|deep learning|"42 r"large language model|\bllm\b|neural network)\b",43 r"\bai[- ](powered|driven|enabled|capabilit\w+|platform|model|tools?)\b"],44 },45 "digital_transformation": {46 "label": "Digital Transformation",47 "signals": [r"digital (transformation|transition|initiative|strateg\w+|platform)",48 r"digitali[sz]ation"],49 },50 "erp_implementation": {51 "label": "ERP / Systems Implementation",52 "signals": [r"\b(erp|sap|oracle|workday|s/?4 ?hana)\b[^.]{0,40}(implement\w+|deploy\w+|migrat\w+|system|platform)",53 r"(implement\w+|deploy\w+|upgrad\w+)[^.]{0,40}\berp\b",54 r"enterprise resource planning"],55 },56 "rd_program": {57 "label": "R&D Program",58 "signals": [r"research and development", r"\br&d\b",59 r"(develop\w+|invest\w+)[^.]{0,40}(next[- ]generation|new technolog\w+|new product)"],60 },61 "product_launch": {62 "label": "New Product / Launch",63 "signals": [r"(launch\w*|introduc\w+|releas\w+|roll\w*[- ]?out|unveil\w+)"64 r"[^.]{0,50}(product|platform|service|offering|solution|model|line)",65 r"new product (line|launch|introduction)"],66 },67 "drug_pipeline": {68 "label": "Drug / Clinical Pipeline",69 "signals": [r"(phase\s?(i{1,3}|1|2|3)\b)", r"clinical trial", r"\bfda (approval|clearance|submission)\b",70 r"(drug|therap\w+|candidate|molecule|pipeline)[^.]{0,40}(develop\w+|trial|approval)"],71 },72 "ma_integration": {73 "label": "M&A / Integration",74 "signals": [r"(acqui\w+|merg\w+|takeover|business combination)",75 r"integration of [a-z]", r"purchase of [a-z]"],76 },77 "geographic_expansion": {78 "label": "Geographic Expansion",79 "signals": [r"(expand\w*|enter\w*|launch\w*|establish\w*)[^.]{0,50}"80 r"(market|region|countr\w+|geograph\w+|international|overseas|abroad)",81 r"market entry"],82 },83 "capex_program": {84 "label": "Capital Expenditure Program",85 "signals": [r"capital (expenditure|expenditures|investment|spending|program|project|budget)",86 r"\bcapex\b", r"invest\w+[^.]{0,30}(\$|usd)"],87 },88 "supply_chain": {89 "label": "Supply Chain Initiative",90 "signals": [r"supply chain", r"(reshor\w+|nearshor\w+|onshor\w+|offshor\w+)",91 r"(distribution|logistics|fulfillment|warehouse|inventory)[^.]{0,40}"92 r"(network|center|optimi[sz]\w+|expansion|investment)"],93 },94 "automation": {95 "label": "Automation / Robotics",96 "signals": [r"automat\w+", r"robot\w+", r"(industrial|warehouse) automation"],97 },98 "sustainability": {99 "label": "Sustainability / ESG",100 "signals": [r"sustainabilit\w+", r"\besg\b", r"net[- ]zero", r"carbon (neutral|reduction|footprint|emissions?)",101 r"decarboni[sz]\w+", r"emission reduction"],102 },103 "energy_transition": {104 "label": "Energy Transition / Renewables",105 "signals": [r"renewable (energy|power)", r"(solar|wind|hydrogen|geothermal|battery storage)[^.]{0,40}"106 r"(project|facility|farm|plant|capacity|investment)",107 r"clean energy", r"energy transition"],108 },109 "infrastructure": {110 "label": "Infrastructure Investment",111 "signals": [r"infrastructure (investment|project|upgrade|expansion|spending|build\w*)",112 r"(network|grid|pipeline|broadband|fiber) (expansion|build\w*|upgrade|deployment)"],113 },114 "cost_reduction": {115 "label": "Cost Reduction / Restructuring",116 "signals": [r"(cost (reduction|savings|cutting)|restructur\w+|efficiency (program|initiative)|"117 r"productivity (program|initiative)|streamlin\w+|optimi[sz]ation (program|initiative))"],118 },119 "partnership": {120 "label": "Strategic Partnership",121 "signals": [r"(strategic (partnership|alliance|collaboration)|joint venture|\bjv\b|"122 r"(partner\w+|collaborat\w+|team\w* up)[^.]{0,40}(with))"],123 },124 "technology_deployment": {125 "label": "Technology Deployment",126 "signals": [r"(deploy\w+|implement\w+|roll\w*[- ]?out|adopt\w+|install\w+)[^.]{0,50}"127 r"(technolog\w+|system|platform|software|solution|tool)"],128 },129}130131# ----------------------------------------------------------------------132# Technology vocabulary -> canonical tag133# ----------------------------------------------------------------------134TECHNOLOGY_TERMS: dict[str, str] = {135 r"\b(artificial intelligence|machine learning|generative ai|gen ?ai|deep learning|llm|neural)\b": "AI/ML",136 r"\bcloud\b": "Cloud",137 r"\b(erp|sap|oracle|workday|s/?4 ?hana|peoplesoft)\b": "ERP",138 r"\b(robot\w+|automation|cobots?)\b": "Robotics/Automation",139 r"\b(semiconductor|chip|wafer|fab|node\b|nanometer|foundry)\b": "Semiconductors",140 r"\b(solar|wind|renewable|hydrogen|geothermal)\b": "Renewable Energy",141 r"\b(battery|batteries|energy storage|lithium)\b": "Battery/Storage",142 r"\b(ev|electric vehicle|charging)\b": "Electric Vehicles",143 r"\b(blockchain|crypto|digital asset|tokeni[sz])\b": "Blockchain",144 r"\b(5g|broadband|fiber|fibre|network infrastructure)\b": "Connectivity",145 r"\b(data\s?cent(er|re)|hyperscale|server)\b": "Data Center",146 r"\b(cyber ?security|encryption|zero trust)\b": "Cybersecurity",147 r"\b(iot|internet of things|sensors?|edge computing)\b": "IoT/Edge",148 r"\b(quantum)\b": "Quantum",149 r"\b(biolog\w+|gene|mrna|crispr|cell therapy|molecul\w+)\b": "Biotech",150 r"\b(saas|software[- ]as[- ]a[- ]service|platform)\b": "Software/SaaS",151}152153# ----------------------------------------------------------------------154# Status keywords -> canonical status155# ----------------------------------------------------------------------156STATUS_TERMS: list[tuple[str, str]] = [157 (r"\b(completed|commission\w+|operational|in service|came online|opened|finalized)\b", "completed"),158 (r"\b(under construction|underway|in progress|ongoing|ramping|being (built|implemented))\b", "in_progress"),159 (r"\b(announc\w+|plan\w+|intend\w+|expect\w+|will (build|invest|launch|open)|propos\w+|"160 r"anticipat\w+|target\w+|by (20\d\d)|slated)\b", "planned"),161]162163# ----------------------------------------------------------------------164# Geography165# ----------------------------------------------------------------------166US_STATES = {167 "alabama","alaska","arizona","arkansas","california","colorado","connecticut","delaware",168 "florida","georgia","hawaii","idaho","illinois","indiana","iowa","kansas","kentucky",169 "louisiana","maine","maryland","massachusetts","michigan","minnesota","mississippi",170 "missouri","montana","nebraska","nevada","new hampshire","new jersey","new mexico",171 "new york","north carolina","north dakota","ohio","oklahoma","oregon","pennsylvania",172 "rhode island","south carolina","south dakota","tennessee","texas","utah","vermont",173 "virginia","washington","west virginia","wisconsin","wyoming",174}175COUNTRIES = {176 "united states","u.s.","usa","canada","mexico","china","india","japan","germany","france",177 "united kingdom","u.k.","ireland","brazil","singapore","south korea","korea","taiwan",178 "vietnam","thailand","malaysia","indonesia","australia","spain","italy","netherlands",179 "switzerland","sweden","poland","israel","saudi arabia","united arab emirates","uae",180 "philippines","czech republic","hungary","turkey","egypt","south africa","argentina","chile",181}182183_MONEY_RE = re.compile(184 r"(?:US)?\$\s?(\d[\d,]*\.?\d*)\s?(billion|bn|million|mm|mn|m\b|thousand|k\b|trillion)?",185 re.IGNORECASE,186)187_MULT = {188 "trillion": 1e12, "billion": 1e9, "bn": 1e9, "million": 1e6, "mm": 1e6, "mn": 1e6,189 "m": 1e6, "thousand": 1e3, "k": 1e3,190}191_YEAR_RE = re.compile(r"\b(?:fiscal\s+|fy\s?)?(20[0-3]\d|19[89]\d)\b", re.IGNORECASE)192_QUARTER_RE = re.compile(r"\bQ([1-4])\s?(20\d\d)\b", re.IGNORECASE)193_TECH_COMPILED = [(re.compile(p, re.IGNORECASE), tag) for p, tag in TECHNOLOGY_TERMS.items()]194_STATUS_COMPILED = [(re.compile(p, re.IGNORECASE), s) for p, s in STATUS_TERMS]195_CITY_STATE_RE = re.compile(r"\bin\s+([A-Z][a-zA-Z.]+(?:\s[A-Z][a-zA-Z.]+){0,2}),\s+([A-Z][a-zA-Z]+)")196197198def extract_money(text: str) -> tuple[float | None, str | None]:199 """Return (usd_amount, raw_span) for the largest explicit dollar figure."""200 best_val, best_raw = None, None201 for m in _MONEY_RE.finditer(text):202 try:203 num = float(m.group(1).replace(",", ""))204 except ValueError:205 continue206 unit = (m.group(2) or "").lower().strip()207 val = num * _MULT.get(unit, 1.0)208 if best_val is None or val > best_val:209 best_val, best_raw = val, m.group(0).strip()210 return best_val, best_raw211212213def extract_locations(text: str) -> list[str]:214 low = text.lower()215 found: list[str] = []216 for st in US_STATES:217 if re.search(rf"\b{re.escape(st)}\b", low):218 found.append(st.title())219 for co in COUNTRIES:220 if re.search(rf"\b{re.escape(co)}\b", low):221 found.append(co.upper() if len(co) <= 4 else co.title())222 for m in _CITY_STATE_RE.finditer(text):223 found.append(f"{m.group(1)}, {m.group(2)}")224 # de-dup preserving order225 seen, out = set(), []226 for f in found:227 k = f.lower()228 if k not in seen:229 seen.add(k); out.append(f)230 return out[:6]231232233def extract_years(text: str) -> list[int]:234 yrs = {int(y) for y in _YEAR_RE.findall(text)}235 for m in _QUARTER_RE.finditer(text):236 yrs.add(int(m.group(2)))237 return sorted(yrs)238239240def extract_technologies(text: str) -> list[str]:241 tags = []242 for rx, tag in _TECH_COMPILED:243 if rx.search(text) and tag not in tags:244 tags.append(tag)245 return tags246247248def detect_status(text: str) -> str:249 for rx, status in _STATUS_COMPILED:250 if rx.search(text):251 return status252 return "mentioned"253254255# pre-compile project-type signals256TYPE_SIGNALS = {257 ptype: [re.compile(sig, re.IGNORECASE) for sig in spec["signals"]]258 for ptype, spec in PROJECT_TYPES.items()259}260TYPE_LABELS = {k: v["label"] for k, v in PROJECT_TYPES.items()}261ALL_TYPES = list(PROJECT_TYPES.keys())262