| 1 | from __future__ import annotations |
| 2 | |
| 3 | import re |
| 4 | from urllib.parse import urlparse |
| 5 | |
| 6 | from app.models import CompanyResearchProfile, EntityResolution |
| 7 | |
| 8 | |
| 9 | class EntityResolver: |
| 10 | def __init__(self, profiles: list[CompanyResearchProfile]): |
| 11 | self.profiles = profiles |
| 12 | |
| 13 | def resolve(self, title: str | None, text: str, url: str) -> EntityResolution: |
| 14 | haystack = f"{title or ''} {text}".lower() |
| 15 | host = (urlparse(url).hostname or "").lower() |
| 16 | best = EntityResolution(instrument_id=None, company_id=None, confidence=0.0, matched_on=[]) |
| 17 | for profile in self.profiles: |
| 18 | score = 0.0 |
| 19 | matched: list[str] = [] |
| 20 | if profile.isin and profile.isin.lower() in haystack: |
| 21 | score += 0.45 |
| 22 | matched.append("isin") |
| 23 | if _contains_identity(haystack, profile.company_name): |
| 24 | score += 0.30 |
| 25 | matched.append("company_name") |
| 26 | for alias in profile.aliases: |
| 27 | if _contains_identity(haystack, alias): |
| 28 | score += 0.30 if len(alias.strip()) >= 4 else 0.18 |
| 29 | matched.append("alias") |
| 30 | break |
| 31 | if _contains_identity(haystack, profile.ticker) and _contains_identity(haystack, profile.exchange): |
| 32 | score += 0.18 |
| 33 | matched.append("ticker_exchange") |
| 34 | if _contains_identity(haystack, profile.country): |
| 35 | score += 0.04 |
| 36 | matched.append("country") |
| 37 | if any(host.endswith(domain.lower()) for domain in profile.known_domains): |
| 38 | score += 0.35 |
| 39 | matched.append("known_domain") |
| 40 | score = min(score, 1.0) |
| 41 | if score > best.confidence: |
| 42 | best = EntityResolution( |
| 43 | instrument_id=profile.instrument_id, |
| 44 | company_id=profile.company_id, |
| 45 | confidence=score, |
| 46 | matched_on=matched, |
| 47 | ) |
| 48 | return best |
| 49 | |
| 50 | |
| 51 | def _contains_identity(haystack: str, value: str) -> bool: |
| 52 | identity = value.strip().lower() |
| 53 | if not identity: |
| 54 | return False |
| 55 | pattern = r"(?<![a-z0-9])" + re.escape(identity) + r"(?![a-z0-9])" |
| 56 | return re.search(pattern, haystack) is not None |