mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Let a deterministic guard override the fallback classifier per description 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe663deef5
commit
4396e5fb22
3 changed files with 76 additions and 0 deletions
31
domain/data_transformation/guarded_column_classifier.py
Normal file
31
domain/data_transformation/guarded_column_classifier.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Callable, Optional, TypeVar
|
||||||
|
|
||||||
|
from domain.data_transformation.column_classifier import ColumnClassifier
|
||||||
|
|
||||||
|
E = TypeVar("E", bound=Enum)
|
||||||
|
|
||||||
|
|
||||||
|
class GuardedColumnClassifier(ColumnClassifier[E]):
|
||||||
|
"""A ``ColumnClassifier`` that resolves the descriptions a deterministic guard
|
||||||
|
is certain about, and delegates the rest to a fallback classifier.
|
||||||
|
|
||||||
|
The ``guard`` maps a raw description to a category member when it recognises it
|
||||||
|
deterministically (e.g. a party-ceiling roof marker — #1376), else ``None``.
|
||||||
|
Guard hits never reach the fallback, so an unreliable classifier (the LLM)
|
||||||
|
cannot override a description the guard is sure of — and the LLM is not billed
|
||||||
|
for it. Every description still appears in the result (guarded or fallen-back).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
guard: Callable[[str], Optional[E]],
|
||||||
|
fallback: ColumnClassifier[E],
|
||||||
|
) -> None:
|
||||||
|
self._guard = guard
|
||||||
|
self._fallback = fallback
|
||||||
|
|
||||||
|
def classify(self, descriptions: set[str]) -> dict[str, E]:
|
||||||
|
raise NotImplementedError
|
||||||
0
tests/domain/data_transformation/__init__.py
Normal file
0
tests/domain/data_transformation/__init__.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from domain.data_transformation.column_classifier import ColumnClassifier
|
||||||
|
from domain.data_transformation.guarded_column_classifier import (
|
||||||
|
GuardedColumnClassifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Category(Enum):
|
||||||
|
GUARDED = "guarded"
|
||||||
|
FALLBACK = "fallback"
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingFallback(ColumnClassifier[_Category]):
|
||||||
|
"""Stands in for the LLM: records what it was asked and maps everything it
|
||||||
|
sees to FALLBACK, so the test can see which descriptions reached it."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.asked: set[str] = set()
|
||||||
|
|
||||||
|
def classify(self, descriptions: set[str]) -> dict[str, _Category]:
|
||||||
|
self.asked = set(descriptions)
|
||||||
|
return {d: _Category.FALLBACK for d in descriptions}
|
||||||
|
|
||||||
|
|
||||||
|
def _guard(description: str) -> Optional[_Category]:
|
||||||
|
return _Category.GUARDED if description == "marker" else None
|
||||||
|
|
||||||
|
|
||||||
|
def test_guard_hits_win_and_misses_fall_through_to_the_fallback() -> None:
|
||||||
|
# Arrange
|
||||||
|
fallback = _RecordingFallback()
|
||||||
|
classifier = GuardedColumnClassifier(guard=_guard, fallback=fallback)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = classifier.classify({"marker", "other"})
|
||||||
|
|
||||||
|
# Assert — the guarded description takes the guard's member and never reaches
|
||||||
|
# the fallback; the unrecognised one is resolved by the fallback.
|
||||||
|
assert result == {"marker": _Category.GUARDED, "other": _Category.FALLBACK}
|
||||||
|
assert fallback.asked == {"other"}
|
||||||
Loading…
Add table
Reference in a new issue