Spaces:
Sleeping
Sleeping
| """Reference resolver β ground anaphora / positional / predicate refs | |
| against session state before the planner sees them. | |
| Used by the multi-turn loop after `classify_turn` to convert phrases | |
| like "show me 20 instead", "the second one", "those", "loans over | |
| 30000" into explicit args the planner can pass straight to a tool. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import re | |
| from dataclasses import dataclass, field | |
| from typing import TYPE_CHECKING, Any, Literal | |
| import pandas as pd | |
| if TYPE_CHECKING: | |
| from lexsi_ds.agent.session import SessionContext | |
| log = logging.getLogger(__name__) | |
| ResolutionKind = Literal["slot", "positional", "predicate", "drilldown", "none"] | |
| class ResolvedReference: | |
| kind: ResolutionKind = "none" | |
| source_slot: str = "" | |
| resolved_args: dict[str, Any] = field(default_factory=dict) | |
| confidence: float = 0.0 | |
| notes: str = "" | |
| _DRILLDOWN_RE = re.compile( | |
| r"\bwhy is (?:loan |row |id |entity |case )?#?(\d+)\b", re.IGNORECASE, | |
| ) | |
| _TOP_K_RE = re.compile(r"\btop\s+(\d+)\b", re.IGNORECASE) | |
| _POSITIONAL_RE = re.compile( | |
| r"\bthe\s+(first|second|third|fourth|fifth|last)\b", re.IGNORECASE, | |
| ) | |
| _PREDICATE_RE = re.compile( | |
| r"\b(higher than|larger than|more than|greater than|over|above|>)\s+" | |
| r"\$?(\d+(?:[.,]\d{3})*(?:\.\d+)?)\b", | |
| re.IGNORECASE, | |
| ) | |
| _THOSE_RE = re.compile(r"\b(those|them|they|these)\b", re.IGNORECASE) | |
| _POSITION_MAP = { | |
| "first": 0, "second": 1, "third": 2, | |
| "fourth": 3, "fifth": 4, "last": -1, | |
| } | |
| def resolve(user_msg: str, session: "SessionContext") -> ResolvedReference: | |
| """Find the highest-priority grounded reference in `user_msg`.""" | |
| msg = user_msg or "" | |
| # 1. Drilldown β "why is loan 5314 here?" | |
| m = _DRILLDOWN_RE.search(msg) | |
| if m: | |
| return ResolvedReference( | |
| kind="drilldown", | |
| source_slot="last_predictions", | |
| resolved_args={"row_id": int(m.group(1))}, | |
| confidence=0.85, | |
| notes="drilldown on numeric id", | |
| ) | |
| # 2. Top-K override β "top 20" | |
| m = _TOP_K_RE.search(msg) | |
| if m: | |
| return ResolvedReference( | |
| kind="positional", | |
| resolved_args={"top_k": int(m.group(1))}, | |
| confidence=0.9, | |
| notes="top-k override", | |
| ) | |
| # 3. Positional β "the second one", "the last entry" | |
| m = _POSITIONAL_RE.search(msg) | |
| if m: | |
| return ResolvedReference( | |
| kind="positional", | |
| resolved_args={"index": _POSITION_MAP[m.group(1).lower()]}, | |
| confidence=0.8, | |
| notes=f"positional: {m.group(1)}", | |
| ) | |
| # 4. Predicate filter β "over 30000" | |
| m = _PREDICATE_RE.search(msg) | |
| if m: | |
| value = m.group(2).replace(",", "") | |
| cols = _columns_in_last_table(session) | |
| column_hint = cols[0] if cols else "amount" | |
| return ResolvedReference( | |
| kind="predicate", | |
| source_slot="last_predictions" if session.cache.get("last_predictions") is not None | |
| else "last_sql_result", | |
| resolved_args={"filter": f"> {value}", "column_hint": column_hint}, | |
| confidence=0.7, | |
| notes=f"predicate over {value}", | |
| ) | |
| # 5. Anaphora β "those", "them" β previous predictions if present | |
| if _THOSE_RE.search(msg): | |
| if session.cache.get("last_predictions") is not None: | |
| return ResolvedReference( | |
| kind="slot", | |
| source_slot="last_predictions", | |
| resolved_args={}, | |
| confidence=0.6, | |
| notes="anaphora to last predictions", | |
| ) | |
| return ResolvedReference(kind="none", confidence=1.0, notes="no reference detected") | |
| def _columns_in_last_table(session: "SessionContext") -> list[str]: | |
| """Return column names from the freshest cached table, if any.""" | |
| df = session.cache.get("last_predictions") | |
| if isinstance(df, pd.DataFrame) and not df.empty: | |
| return list(df.columns) | |
| df = session.cache.get("last_sql_result") | |
| if isinstance(df, pd.DataFrame) and not df.empty: | |
| return list(df.columns) | |
| return [] | |