"""Regex-based extraction solver: emails, URLs, phone numbers, numbers, dates. Only fires when the prompt unambiguously asks for exactly one category of entity from an embedded text blob and the regex finds exactly one match (or the ask is explicitly plural, in which case all matches are joined). Multiple categories requested at once, or zero matches, fall through with confident=False. """ from __future__ import annotations import re from routing_agent.classifier import TaskType from routing_agent.models import Task from routing_agent.solvers import SolverResult _EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}") # Excludes a trailing "." from the match so a sentence-ending period right # after the URL (e.g. "...v2/docs.") isn't swallowed into it; a "." that is # genuinely part of the URL (mid-path, e.g. "v2.1") is unaffected since the # exclusion only applies to the final character of the match. _URL_RE = re.compile(r"https?://[^\s,;\"')\]]+[^\s,;\"')\].]") _PHONE_RE = re.compile(r"(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b") # Allows comma thousands-separators (e.g. "1,250,000") as a single number. _NUMBER_RE = re.compile(r"-?\d[\d,]*(?:\.\d+)?") _ISO_DATE_RE = re.compile(r"\b\d{4}-\d{2}-\d{2}\b") # Ordered most-specific-first: "phone number" must be checked (and, if # matched, exclusively used) before the generic "number" pattern, since the # latter's word boundary matches inside "phone number" too. _ASK_PATTERNS: tuple[tuple[re.Pattern[str], re.Pattern[str], bool], ...] = ( (re.compile(r"\bemails?\b", re.IGNORECASE), _EMAIL_RE, True), (re.compile(r"\burls?\b|\blinks?\b", re.IGNORECASE), _URL_RE, True), (re.compile(r"\bphone numbers?\b", re.IGNORECASE), _PHONE_RE, True), (re.compile(r"\bdates?\b", re.IGNORECASE), _ISO_DATE_RE, True), ( re.compile(r"(?" style prompts from their source # text. Word-boundary anchored so "from"/"in" don't match inside other words # (e.g. "Find"); greedy search finds the rightmost separator so a leading # "Find the phone number in: ..." splits on the colon, not the first "in". _SOURCE_SPLIT_RE = re.compile(r"(?:\bfrom\b|\bin\b|:)\s*[\"']?(?P.+)$", re.DOTALL) def _find_source_text(prompt: str) -> str: last_match = None for match in _SOURCE_SPLIT_RE.finditer(prompt): last_match = match return last_match.group("blob") if last_match else prompt def try_solve(task: Task, task_type: TaskType) -> SolverResult: """Attempt to extract a single unambiguous entity category from the prompt. Requires the ask to name exactly one supported category (email/url/ phone/date/number); if more than one category keyword appears, or the regex finds no matches, the solver declines. """ if task_type != TaskType.EXTRACTION: return SolverResult(answer=None, confident=False) prompt = task.prompt.strip() matched_categories = [ (ask_re, entity_re, is_plural) for ask_re, entity_re, is_plural in _ASK_PATTERNS if ask_re.search(prompt) ] if len(matched_categories) != 1: return SolverResult(answer=None, confident=False) _, entity_re, _ = matched_categories[0] source_text = _find_source_text(prompt) matches = entity_re.findall(source_text) if not matches: return SolverResult(answer=None, confident=False) wants_all = bool(_PLURAL_HINT_RE.search(prompt)) if wants_all or len(matches) > 1: return SolverResult(answer=", ".join(matches), confident=True) return SolverResult(answer=matches[0], confident=True)