Rifqi Hafizuddin
Claude Opus 5.5
[NOTICKET] feat(scope): the analysis objective's period is applied by code (P0-1)
5e5b0e3 Download src/agents/scope/models.py from DataEyond/Agentic-Service-Data-Eyond-Catalog: direct link, hf CLI and curl.
- Browser
- Download file 5.75 kB
-
https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/refs%2Fpr%2F27/src/agents/scope/models.py
- Command line
-
hf download hf://spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog@refs/pr/27/src/agents/scope/models.py
-
curl -L -o models.py https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/refs%2Fpr%2F27/src/agents/scope/models.py
5.75 kB
| """Conversation scope — the slice of data an answer covers (2026-10-29). | |
| Scope used to exist only as text the planner might or might not apply: the | |
| objective's site/section default was applied on 7 of 27 turns that relied on it, | |
| and 4 of 42 answers named a scope their query never filtered (RCA 2026-10-29, | |
| docs in the session sprint folder). Here scope becomes state: | |
| - code resolves it: the objective and the user's own words matched against the | |
| value lists of the small categorical columns (`values.py`), the router's rewrite | |
| for follow-ups, the planner's grounded filters (`engine.py`); | |
| - the planner only declares "every value of this column" (`ScopeChange`); | |
| - code applies it to every `retrieve_data` IR before validation, renders the line | |
| the user sees, and persists a `TurnScope` on the turn's `AnalysisRecord`. | |
| Generic on purpose: an entry is any categorical catalog column by NAME plus its | |
| values. Nothing here knows what a "site" or "section" is. The period is not part of | |
| scope — the planner keeps resolving periods (a forecast or a year-over-year question | |
| needs data outside any fixed window); the line only shows the period that ran. | |
| """ | |
| from __future__ import annotations | |
| from typing import Literal | |
| from pydantic import BaseModel, Field | |
| class ScopeEntry(BaseModel): | |
| """One scoped column. `values=None` means ALL (the column is not filtered).""" | |
| column: str = Field( | |
| description="Catalog column NAME as shown in the catalog (e.g. a code column), " | |
| "never a column id." | |
| ) | |
| values: list[str] | None = Field( | |
| default=None, | |
| description="The values to keep, spelled as the catalog spells them. " | |
| "null = ALL values (the user asked for every value of this column).", | |
| ) | |
| class ScopeChange(BaseModel): | |
| """The planner's only scope output: a column the user asked to see ALL values of. | |
| Everything else is code (gate run 2, 2026-10-29): code matches the user's words | |
| against the column value lists and carries follow-ups from the router's rewrite. | |
| The planner asked for less is the planner that fails less — asked for a `base` | |
| and a full declaration, it invented `${scope.*}` placeholders. Concrete values | |
| are still read for columns too large to list (a model, a unit), if typed. | |
| """ | |
| set: list[ScopeEntry] = Field( | |
| default_factory=list, | |
| description="Only when the user asks for EVERY value of a column the analysis " | |
| "objective fixes ('semua section', 'per site', 'all sites'): that column with " | |
| "values null. Otherwise leave empty.", | |
| ) | |
| class ScopeInputs(BaseModel): | |
| """What a turn knows about scope before planning, read from the latest record. | |
| `focus=None` means not pinned yet (no earlier data answer resolved it): the planner | |
| is then asked to resolve the objective into `TaskList.focus`. `user_message` is the | |
| user's own words — the planner otherwise sees only the router's rewrite, which can | |
| carry history, and grounding a scope change needs what the user actually typed. | |
| """ | |
| focus: list[ScopeEntry] | None = None | |
| previous: list[ScopeEntry] | None = None | |
| user_message: str = "" | |
| # Full value lists of the small categorical columns (`values.load_value_index`), | |
| # loaded every data turn (cached per process): code resolves the objective and | |
| # the user's words with them. Never persisted. | |
| value_index: dict[str, list[str]] | None = None | |
| def from_record_scope(cls, scope: dict | None, user_message: str) -> ScopeInputs: | |
| """Build from the latest persisted `TurnScope` dict; malformed → empty.""" | |
| try: | |
| prior = TurnScope.model_validate(scope) if scope else None | |
| except Exception: | |
| prior = None | |
| if prior is None: | |
| return cls(user_message=user_message) | |
| return cls( | |
| focus=(prior.focus or None) if prior.pinned else None, | |
| previous=prior.effective, | |
| user_message=user_message, | |
| ) | |
| class TurnScope(BaseModel): | |
| """The scope of one data answer, as persisted on its `AnalysisRecord`. | |
| `focus` is copied forward every turn so the latest record alone tells the next | |
| turn both the pinned focus and the previous answer's scope. | |
| """ | |
| focus: list[ScopeEntry] = Field(default_factory=list) | |
| effective: list[ScopeEntry] = Field(default_factory=list) | |
| base: Literal["focus", "previous"] = "focus" | |
| # Human-readable notes the line shows: entries that could not be applied to a | |
| # table, codes a lookup did not find, empty results. | |
| not_applied: list[str] = Field(default_factory=list) | |
| unknown_values: list[str] = Field(default_factory=list) | |
| line: str = "" | |
| # Review 2026-10-29 (adversarial pass on "code owns scope"): | |
| # - `pinned`: False when the focus came from an INCOMPLETE value index (a list | |
| # timed out); the next turn resolves it again instead of keeping half a focus. | |
| # - `partial`: columns some answering queries filtered and others did not (a | |
| # subset-vs-total structure, an unreachable table) — the line says so. | |
| # - `typed`: values the user typed this turn; only these can be "not found". | |
| pinned: bool = True | |
| partial: list[str] = Field(default_factory=list) | |
| typed: list[str] = Field(default_factory=list) | |
| # Focus columns a named unit set aside for this turn (the unit carries its own | |
| # site and section) — shown and reported as inside the focus, not a detour. | |
| lifted: list[str] = Field(default_factory=list) | |
| # 2026-11-03 (D1a): some query read a table with a `year` column — the line then | |
| # always says which period was used ("all periods" when none was filtered). | |
| periodic: bool = False | |