Spaces:
Paused
Paused
File size: 6,847 Bytes
0127c21 | 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | """
Location + time extraction for the query router.
Phase 13 (P0/P1 β closes a gap flagged during review: the router
(agent/router.py) classified TASK type but never surfaced the location
or time information embedded in the query text, even though the query
itself (and the map's drawn point/rectangle/polygon) are what actually
carry location, and phrases like "between June and August" carry time).
Deliberately rule-based/regex, NOT an LLM call β consistent with
router.py's existing "deterministic rules... do NOT over-engineer the
agent" approach, and keeps this dependency-free (no spaCy/NER model to
download mid-hackathon).
SCOPE β be honest about what this is:
- Time extraction: regex over explicit dates, years, month names, and a
small set of common relative-time phrases ("last year", "this
monsoon", "between X and Y"). This is pattern matching, not a
temporal-reasoning model β it will miss unusual phrasing.
- Location extraction: a small gazetteer of India/ISRO-relevant place
names (matches the hero-demo geography) PLUS a generic
capitalized-phrase heuristic as a fallback for names not in the list.
This is NOT a trained NER model and will both miss valid places and
occasionally flag a capitalized non-place word β surfaced confidence
is "gazetteer" (higher trust) vs "heuristic" (lower trust) so the UI
and judges can see which kind of match this was.
- The map's click/rectangle/polygon selection remains the authoritative
source of location for actually running a model β this module is a
transparency/UX feature (showing what the query *said* about
location/time) not a replacement for that geometry.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
# Small gazetteer biased toward the PS's own geography (ISRO/Cartosat-2S/
# RISAT evaluation set, Assam monsoon hero demo) plus commonly-used
# generic regions. Not exhaustive β extend as needed, don't pretend it's
# a full geocoder.
_GAZETTEER = [
"assam", "kerala", "mumbai", "delhi", "chennai", "kolkata", "bengaluru",
"bangalore", "hyderabad", "pune", "ahmedabad", "gujarat", "punjab",
"rajasthan", "odisha", "bihar", "uttar pradesh", "west bengal",
"tamil nadu", "andhra pradesh", "telangana", "maharashtra", "goa",
"himachal pradesh", "uttarakhand", "jammu and kashmir", "ladakh",
"sundarbans", "brahmaputra", "ganga", "ganges", "western ghats",
"northeast india", "north east india", "india", "bangladesh",
]
_MONTHS = (
"january|february|march|april|may|june|july|august|september|"
"october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|sept|oct|nov|dec"
)
# Ordered so more specific patterns are tried first.
_TIME_PATTERNS = [
re.compile(rf"\bbetween\s+({_MONTHS})\s*(?:\d{{4}})?\s+and\s+({_MONTHS})\s*(?:\d{{4}})?\b", re.I),
re.compile(rf"\b({_MONTHS})\s+(\d{{4}})\b", re.I),
re.compile(r"\b(19|20)\d{2}\b"),
re.compile(r"\blast\s+(year|month|monsoon|season|week)\b", re.I),
re.compile(r"\bthis\s+(year|month|monsoon|season)\b", re.I),
re.compile(r"\b(before|after|pre[- ]|post[- ])(monsoon|flood|construction)\b", re.I),
re.compile(r"\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b"), # dd/mm/yyyy style dates
]
# Generic capitalized multi-word / single-word phrase, used only as a
# fallback when nothing in the gazetteer matched. Deliberately excludes
# common sentence-initial and query-scaffolding words so "Where is..."
# doesn't get flagged as a place called "Where".
_MONTH_WORDS = {
"january", "february", "march", "april", "may", "june", "july",
"august", "september", "october", "november", "december",
"jan", "feb", "mar", "apr", "jun", "jul", "aug", "sep", "sept",
"oct", "nov", "dec",
}
_STOPWORDS = {
"where", "what", "which", "how", "has", "did", "is", "are", "the",
"this", "these", "those", "image", "images", "region", "area",
"compare", "describe", "highlight", "locate", "assess", "identify",
"use", "combine", "show", "find", "sar", "optical", "vqa",
} | _MONTH_WORDS
@dataclass
class ExtractionResult:
locations: list[str] = field(default_factory=list)
location_source: str = "none" # "gazetteer" | "heuristic" | "gazetteer+heuristic" | "none"
time_expressions: list[str] = field(default_factory=list)
note: str = ""
def extract_locations(query: str) -> tuple[list[str], str]:
q_lower = query.lower()
gazetteer_hits = sorted({name for name in _GAZETTEER if re.search(rf"\b{re.escape(name)}\b", q_lower)})
heuristic_hits: list[str] = []
for match in re.finditer(r"\b([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*)\b", query):
phrase = match.group(1)
if phrase.lower() in _STOPWORDS:
continue
if match.start() == 0:
# Skip a capitalized first word β too likely to just be
# normal sentence capitalization, not a place name.
continue
if phrase.lower() in gazetteer_hits:
continue
heuristic_hits.append(phrase)
locations = gazetteer_hits + [h for h in heuristic_hits if h.lower() not in gazetteer_hits]
if gazetteer_hits and heuristic_hits:
source = "gazetteer+heuristic"
elif gazetteer_hits:
source = "gazetteer"
elif heuristic_hits:
source = "heuristic"
else:
source = "none"
return locations, source
def extract_time_expressions(query: str) -> list[str]:
hits: list[str] = []
for pattern in _TIME_PATTERNS:
for match in pattern.finditer(query):
span = match.group(0).strip()
if span and span not in hits:
hits.append(span)
return hits
def extract_location_time(query: str) -> ExtractionResult:
locations, source = extract_locations(query)
time_expressions = extract_time_expressions(query)
if not locations and not time_expressions:
note = (
"No location or time expression detected in the query text. This is "
"pattern-matching over the query string only β it does not mean the "
"query lacks spatial/temporal scope (the uploaded image / map "
"selection may already define that)."
)
else:
parts = []
if locations:
parts.append(f"location candidate(s) via {source} match")
if time_expressions:
parts.append("time expression(s) via pattern match")
note = (
"Detected " + " and ".join(parts) + ". Rule-based extraction, not a "
"trained NER/temporal model β verify against the actual uploaded "
"imagery/map selection before treating this as ground truth scope."
)
return ExtractionResult(
locations=locations,
location_source=source,
time_expressions=time_expressions,
note=note,
)
|