Files
ThothII/harness/tht/session/language.py
Codex bd416f7327
Publish documentation / publish (push) Successful in 34s
Fix new-question landing and question-language HITL
Reset the activity panel when starting a new question so the landing navigation is restored. Detect and persist the original question language, pass it through runtime and widget descriptors, and scope HITL controls to that language.

Validated with gate, session, backend and frontend tests, TypeScript checks, Ruff and strict docs build. Rebuilt and restarted local core/frontend; both healthy and serving HTTP successfully.
2026-09-21 19:47:22 +02:00

61 lines
2.3 KiB
Python

"""Session language policy, independent of workspace document content."""
import re
from functools import lru_cache
from langdetect import DetectorFactory, LangDetectException
from langdetect.detector_factory import PROFILES_DIRECTORY
_LANGUAGE_TAG = re.compile(
r"(?P<language>[A-Za-z]{2,8})(?:-(?P<script>[A-Za-z]{4}))?"
r"(?:-(?P<region>[A-Za-z]{2}|[0-9]{3}))?"
r"(?P<rest>(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*"
r"(?:-[0-9A-WY-Za-wy-z](?:-[A-Za-z0-9]{2,8})+)*"
r"(?:-[xX](?:-[A-Za-z0-9]{1,8})+)?)"
)
def validate_language_tag(value: str) -> str:
"""Validate syntax and normalize casing without depending on UI catalogs."""
match = _LANGUAGE_TAG.fullmatch(value) if isinstance(value, str) else None
if match is None:
raise ValueError("interaction_language must be a BCP-47 language tag")
parts = [match["language"].lower()]
if match["script"]:
parts.append(match["script"].title())
if match["region"]:
parts.append(match["region"].upper())
return "-".join(parts) + match["rest"].lower()
@lru_cache(maxsize=1)
def _question_detector_factory() -> DetectorFactory:
factory = DetectorFactory()
factory.seed = 0
factory.load_profile(PROFILES_DIRECTORY)
return factory
def question_interaction_language(question: str, fallback: str) -> str:
"""Pin confidently detected prose language; short/ambiguous/code input uses the hint."""
fallback = validate_language_tag(fallback)
prose = re.sub(r"```[\s\S]*?```|`[^`]*`|\b\w*_\w*\b", " ", question)
if re.match(
r"\s*(?:SELECT\s+.+\s+FROM\b|WITH\s+\w+\s+AS\s*\(|INSERT\s+INTO\b|"
r"UPDATE\s+\w+\s+SET\b|DELETE\s+FROM\b|CREATE\s+TABLE\b)",
prose, re.IGNORECASE | re.DOTALL,
):
return fallback
if sum(character.isalpha() for character in prose) < 12:
return fallback
detector = _question_detector_factory().create()
detector.append(prose)
try:
candidates = detector.get_probabilities()
except LangDetectException:
return fallback
if not candidates or candidates[0].prob < 0.90:
return fallback
detected = validate_language_tag(candidates[0].lang)
# Preserve a regional preference when it agrees with the detected language.
return fallback if detected.split("-")[0] == fallback.split("-")[0] else detected