electric-horses-infra/stacks/eh-search/app/intent_router.py

83 lines
2.2 KiB
Python
Raw Permalink Normal View History

"""Intent routing — pure regex, no AI, sub-millisecond.
Loco-Soft Komm-Nr Format:
Type-Buchstabe + 4-6 stellige Zahl, optional Space dazwischen.
Type-Letter: N=Neu, T=Tageszul., V=Vorfuehr, D=Differenzbest., G=Gebraucht, L=Leihgabe.
Beispiele: 'D9094', 'd9094', 'D 9094', 'n8093', 'L9083'
Pure 4-6 stellige Zahlen werden als DVN-Lookup behandelt (DVN ist eindeutig).
"""
import re
from dataclasses import dataclass
from typing import Literal
# Komm-Nr: Type + DVN (mit oder ohne Space)
KOMM_NR_RE = re.compile(r"^[NTVDGLntvdgl]\s*\d{4,6}$")
# DVN allein: pure Zahl 4-6 Stellen
DVN_RE = re.compile(r"^\d{4,6}$")
# VIN: 17 chars, no I/O/Q
VIN_RE = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
@dataclass
class Intent:
type: Literal[
"komm_nr",
"dvn",
"vin",
"autocomplete_only",
"keyword_search",
"empty",
]
direct_redirect: bool = False
normalized_query: str = ""
def route(raw_query: str) -> Intent:
q = (raw_query or "").strip()
if not q:
return Intent(type="empty", normalized_query="")
# Rule 1: Komm-Nr (Type + DVN, e.g. 'D9094', 'D 9094', 'd9094')
if KOMM_NR_RE.match(q):
# Normalize: remove spaces, uppercase the type letter
cleaned = q.replace(" ", "")
normalized = cleaned[0].upper() + cleaned[1:]
return Intent(
type="komm_nr",
direct_redirect=True,
normalized_query=normalized,
)
# Rule 2: Pure 4-6 digit number = DVN lookup (eindeutig)
if DVN_RE.match(q):
return Intent(
type="dvn",
direct_redirect=True,
normalized_query=q,
)
# Rule 3: 17-char alphanumeric (no IOQ) = VIN
upper = q.upper().replace(" ", "")
if VIN_RE.match(upper):
return Intent(
type="vin",
direct_redirect=True,
normalized_query=upper,
)
# Rule 4: Single char only -> autocomplete (FTS unbrauchbar bei 1 Zeichen)
if len(q) < 2:
return Intent(
type="autocomplete_only",
normalized_query=q.lower(),
)
# Rule 5: Default — keyword + fuzzy search
return Intent(
type="keyword_search",
normalized_query=q.lower(),
)