spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1"""Deterministic natural-language parser for `/ask` (used when `companyatlas.services.llm.ask` is not available).23"companies hiring AI engineers in Canada" → {event_types: [HIRING], ai: true, country: "CA", window: "30d"}. Only vocabulary from the4taxonomy and the reference tables is recognised; anything else becomes free-text `terms` used for company search. Nothing is guessed:5when no filter matches, the interpretation says so and the answer falls back to "no monitored evidence".6"""7from __future__ import annotations89import re10from dataclasses import asdict, dataclass, field11from typing import Any1213from companyatlas.ids import normalize_alias1415WINDOW_PATTERNS: list[tuple[re.Pattern[str], str]] = [16 (re.compile(r"\b(today|last 24 ?h(ours)?|past 24 ?h(ours)?)\b", re.IGNORECASE), "24h"),17 (re.compile(r"\b(yesterday)\b", re.IGNORECASE), "24h"),18 (re.compile(r"\b(this week|last (7|seven) days|past (7|seven) days|last week|weekly)\b", re.IGNORECASE), "7d"),19 (re.compile(r"\b(this month|last (30|thirty) days|past (30|thirty) days|last month|monthly|recently|recent)\b", re.IGNORECASE), "30d"),20 (re.compile(r"\b(this quarter|last (90|ninety) days|past (90|ninety) days|last quarter|quarterly|last (3|three) months)\b", re.IGNORECASE), "90d"),21 (re.compile(r"\b(this year|last (12|twelve) months|past year|last year|yearly|annual)\b", re.IGNORECASE), "1y"),22]2324EVENT_KEYWORDS: dict[str, tuple[str, ...]] = {25 "HIRING": ("hiring", "hire", "hires", "jobs", "job", "recruit", "recruiting", "openings", "positions", "careers", "vacanc", "headcount", "layoff", "layoffs"),26 "PRICING": ("pricing", "price", "prices", "plan", "plans", "tier", "tiers", "subscription", "cheaper", "expensive"),27 "PRODUCT": ("launch", "launched", "launches", "product", "products", "release", "released", "feature", "features", "shipped", "ship"),28 "LEADERSHIP": ("ceo", "cfo", "cto", "coo", "executive", "executives", "leadership", "appointed", "appoint", "board", "founder", "president", "chief"),29 "LOCATION": ("office", "offices", "location", "locations", "expansion", "expanding", "expand", "opened", "opening", "headquarters", "hq", "store", "stores", "factory"),30 "FINANCING": ("funding", "raised", "raise", "round", "series a", "series b", "investment", "investors", "ipo", "valuation"),31 "M&A": ("acquisition", "acquired", "acquire", "acquires", "merger", "merged", "buyout", "divest", "divestiture"),32 "PARTNERSHIP": ("partnership", "partner", "partners", "partnered", "alliance", "integration"),33 "DEVELOPER": ("api", "apis", "sdk", "developer", "developers", "docs", "documentation", "changelog"),34 "LEGAL": ("legal", "terms", "privacy", "policy", "compliance", "regulatory", "regulation", "gdpr"),35 "SECURITY": ("security", "breach", "incident", "vulnerability", "cve"),36 "TECHNOLOGY": ("technology", "tech stack", "adopted", "adoption", "platform"),37 "SUSTAINABILITY": ("sustainability", "esg", "climate", "carbon", "net zero"),38 "INVESTOR_RELATIONS": ("earnings", "investor", "quarterly results", "annual report", "guidance"),39 "COMMUNICATION": ("news", "press", "announcement", "announced", "blog", "published"),40}41AI_TERMS = ("ai", "a.i.", "artificial intelligence", "machine learning", "ml", "llm", "llms", "generative", "genai", "copilot", "agentic", "deep learning")42RANKING_INTENT: list[tuple[re.Pattern[str], str]] = [43 (re.compile(r"\b(most active|fastest moving|moving fastest|busiest)\b", re.IGNORECASE), "most_active"),44 (re.compile(r"\b(hiring (the )?(most|fastest)|fastest hiring|hiring growth|growing headcount)\b", re.IGNORECASE), "hiring_growth"),45 (re.compile(r"\b(hiring (decline|freeze|slowdown)|cutting|fewer jobs|shrinking)\b", re.IGNORECASE), "hiring_decline"),46 (re.compile(r"\b(product velocity|shipping (the )?most|most launches)\b", re.IGNORECASE), "product_velocity"),47 (re.compile(r"\b(most ai|ai[- ]active|ai adoption|ai leaders)\b", re.IGNORECASE), "ai_active"),48 (re.compile(r"\b(expanding (abroad|internationally|geographically)|geographic expansion|new countries)\b", re.IGNORECASE), "geo_expansion"),49 (re.compile(r"\b(developer momentum|developer[- ]focused)\b", re.IGNORECASE), "developer_momentum"),50 (re.compile(r"\b(pricing changes|changed (their )?pricing|price (increase|hike)s?)\b", re.IGNORECASE), "pricing_changes"),51 (re.compile(r"\b(unusual|anomal|abnormal|behaving (unusually|strangely))", re.IGNORECASE), "unusual_activity"),52]53DEMONYMS: dict[str, str] = {54 "canadian": "CA", "american": "US", "us": "US", "usa": "US", "u.s.": "US", "british": "GB", "uk": "GB", "u.k.": "GB", "english": "GB", "french": "FR",55 "german": "DE", "japanese": "JP", "korean": "KR", "indian": "IN", "australian": "AU", "chinese": "CN", "brazilian": "BR", "mexican": "MX",56 "dutch": "NL", "swedish": "SE", "swiss": "CH", "spanish": "ES", "italian": "IT", "israeli": "IL", "singaporean": "SG", "irish": "IE",57 "european": None, # region, not a country58}59STOPWORDS = {"the", "a", "an", "of", "in", "on", "at", "for", "to", "and", "or", "with", "which", "what", "who", "are", "is", "was", "were", "has",60 "have", "had", "do", "does", "did", "show", "me", "list", "find", "companies", "company", "that", "this", "these", "those", "any",61 "all", "from", "by", "about", "their", "its", "new", "recently", "recent", "engineers", "engineer", "people", "roles", "many", "how",62 "much", "top", "best", "biggest", "largest", "most", "events", "event", "changes", "change", "signal", "signals", "atlas"}636465@dataclass66class Interpretation:67 query: str68 window: str = "30d"69 event_types: list[str] = field(default_factory=list)70 country: str | None = None71 country_name: str | None = None72 industry: str | None = None73 industry_name: str | None = None74 ai: bool = False75 ranking_kind: str | None = None76 company_terms: list[str] = field(default_factory=list)77 terms: list[str] = field(default_factory=list)78 matched: list[str] = field(default_factory=list)7980 def to_json(self) -> dict[str, Any]:81 d = asdict(self)82 d["filters"] = {k: v for k, v in {"event_types": self.event_types or None, "country": self.country, "industry": self.industry,83 "ai": self.ai or None, "window": self.window}.items() if v}84 return d858687def _contains(text: str, phrase: str) -> bool:88 return re.search(rf"(?<![a-z0-9]){re.escape(phrase)}(?![a-z0-9])", text) is not None899091def interpret(query: str, *, countries: list[dict[str, Any]], industries: list[dict[str, Any]]) -> Interpretation:92 """`countries`: [{code, name}], `industries`: [{slug, name, keywords}]. Deterministic, order-independent."""93 text = " " + re.sub(r"\s+", " ", (query or "").strip().lower()) + " "94 it = Interpretation(query=query.strip())95 consumed: list[str] = []96 for pat, w in WINDOW_PATTERNS:97 m = pat.search(text)98 if m:99 it.window = w100 consumed.append(m.group(0))101 it.matched.append(f"window:{w}")102 break103 for pat, kind in RANKING_INTENT:104 m = pat.search(text)105 if m:106 it.ranking_kind = kind107 consumed.append(m.group(0))108 it.matched.append(f"ranking:{kind}")109 break110 if any(_contains(text, t) for t in AI_TERMS):111 it.ai = True112 it.matched.append("ai")113 consumed.extend(t for t in AI_TERMS if _contains(text, t))114 for etype, words in EVENT_KEYWORDS.items():115 hits = [w for w in words if _contains(text, w)]116 if hits:117 it.event_types.append(etype)118 consumed.extend(hits)119 it.matched.append(f"event_type:{etype}")120 by_len = sorted(countries, key=lambda c: -len(c.get("name") or ""))121 for c in by_len:122 name = (c.get("name") or "").lower()123 if name and _contains(text, name):124 it.country, it.country_name = c["code"], c["name"]125 consumed.append(name)126 it.matched.append(f"country:{c['code']}")127 break128 if it.country is None:129 codes = {c["code"].upper(): c for c in countries}130 for dem, code in DEMONYMS.items():131 if code and _contains(text, dem) and code in codes:132 it.country, it.country_name = code, codes[code]["name"]133 consumed.append(dem)134 it.matched.append(f"country:{code}")135 break136 for ind in sorted(industries, key=lambda i: -len(i.get("name") or "")):137 candidates = [ind.get("name") or "", ind.get("slug", "").replace("-", " ")] + list(ind.get("keywords") or [])138 hit = next((c for c in candidates if c and len(c) >= 3 and _contains(text, c.lower())), None)139 if hit:140 it.industry, it.industry_name = ind["slug"], ind.get("name") or ind["slug"]141 consumed.append(hit.lower())142 it.matched.append(f"industry:{ind['slug']}")143 break144 residual = text145 for phrase in sorted(set(consumed), key=len, reverse=True):146 residual = re.sub(rf"(?<![a-z0-9]){re.escape(phrase)}(?![a-z0-9])", " ", residual)147 tokens = [t for t in re.findall(r"[a-z0-9][a-z0-9.&'-]*", residual) if t not in STOPWORDS and len(t) > 1]148 it.terms = tokens[:8]149 it.company_terms = [t for t in tokens if len(t) >= 3][:4]150 return it151152153def compose_answer(it: Interpretation, *, events_total: int, companies_count: int, sample_titles: list[str]) -> str:154 """Careful, non-fabricating summary sentence (spec §162–168)."""155 scope = []156 if it.event_types:157 scope.append(" / ".join(it.event_types).lower() + " events")158 else:159 scope.append("events")160 if it.ai:161 scope.append("with observable AI signals")162 if it.industry_name:163 scope.append(f"in {it.industry_name}")164 if it.country_name:165 scope.append(f"in {it.country_name}")166 window = {"24h": "the last 24 hours", "7d": "the last 7 days", "30d": "the last 30 days", "90d": "the last 90 days", "1y": "the last 12 months"}[it.window]167 if events_total == 0 and companies_count == 0:168 return f"No monitored evidence matches this question yet ({' '.join(scope)}, {window}). Coverage grows as sensors observe more pages."169 parts = [f"Detected {events_total:,} {' '.join(scope)} across {companies_count:,} monitored compan{'y' if companies_count == 1 else 'ies'} in {window}."]170 if sample_titles:171 parts.append("Most recent: " + "; ".join(t[:90] for t in sample_titles[:3]) + ".")172 parts.append("Each event links to its public source; interpretations are signals, not verified facts.")173 return " ".join(parts)174175176def alias_key(term: str) -> str:177 return normalize_alias(term)178179180__all__ = ["EVENT_KEYWORDS", "Interpretation", "alias_key", "compose_answer", "interpret"]181