polish: rebrand to Fleet Ops, add trilingual i18n, adaptive demo guide, and UX overhaul
Rebrands the product from MobilityOps to Fleet Ops across the UI, backend defaults and knowledge base, and makes nl-BE/en-GB/fr-BE full first-class languages: i18next with eager-bundled per-namespace resources, a persisted accessible language switcher (topbar and mobile drawer), locale-aware date/number formatting, and a coverage test that fails the build on any missing or empty translation key. Backend dynamic content (demo scenarios, blocked-reason text, integration status) moves from fixed English/Dutch prose to stable message codes + params so the frontend can localize it; the demo knowledge base gains a fully translated NL/EN/FR procedure corpus (11 documents each) with per-language retrieval and localized evidence-state messages. The Demo Guide becomes breakpoint-adaptive: a docked rail on extra-wide desktop, a floating panel that auto-collapses to a persistent, closable progress chip on standard desktop/tablet, and a collapsed/half/full bottom sheet on mobile -- with scroll+focus+ highlight on "go to this step", Escape handling, and reduced-motion support. The Data Quality Workbench gets accessible choice-card decisions with a clear primary/ secondary/tertiary action hierarchy; the Automation ledger groups repeated successes and uses meaningful short refs; the Audit trail groups events by correlation id with human action labels and readable before/after diffs. Attention Queue, Today's movements, Vehicles, Bookings and Data Quality rows are fully clickable (stretched-link pattern) with independent secondary links, keyboard support and mobile touch targets. Fixes a topbar overflow on mobile caused by the new language switcher (moved into the mobile drawer at <=960px) and two dangling aria-labelledby references introduced this session. Updates all affected Playwright specs for the new nl-BE default and the new Audit/DemoGuide DOM structure, and adds new i18n-coverage, demo-guide-adaptive and clickable-rows specs. 131 backend tests, Ruff and mypy, and 71 Playwright tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
257a4cf6c0
commit
337f8716bb
@@ -16,26 +16,8 @@ from app.services.knowledge import get_knowledge_provider
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# The default name matches the project's locked fictitious tenant (see PROJECT_STATE.md
|
||||
# "Locked decisions"; the same slug already backs `ragcore_tenant`) — this surfaces that
|
||||
# existing decision in the UI rather than inventing a new one. Configurable via
|
||||
# DEMO_ORGANIZATION_NAME so a redeployment can rebrand the fictional org without a code change.
|
||||
ORGANIZATION_DESCRIPTION = (
|
||||
"MobilityOps brengt voertuig-, boekings- en operationele gegevens samen, "
|
||||
"ondersteunt verhuurprocessen, detecteert datakwaliteitsproblemen en "
|
||||
"automatiseert gecontroleerde vervolgstappen."
|
||||
)
|
||||
|
||||
_FAILED_DEMO_EVENT_ID = "00000000-0000-4000-8000-000000000020"
|
||||
|
||||
_N8N_STATE_LABELS = {
|
||||
"disabled": "Niet gekoppeld",
|
||||
"unavailable": "Verwerking mislukt",
|
||||
"degraded": "Opnieuw proberen mogelijk",
|
||||
"operational": "Operationeel",
|
||||
"no_evidence": "Voorbereid",
|
||||
}
|
||||
|
||||
|
||||
def _last_reset(db: Session) -> tuple[datetime | None, str | None]:
|
||||
marker = db.scalar(
|
||||
@@ -61,44 +43,33 @@ def _scenarios(db: Session) -> list[DemoScenarioOut]:
|
||||
select(OutboxEvent).where(OutboxEvent.event_id == _FAILED_DEMO_EVENT_ID)
|
||||
)
|
||||
knowledge_health = get_knowledge_provider().health()
|
||||
reset_hint = "Reset de demo-data om dit scenario opnieuw beschikbaar te maken."
|
||||
|
||||
# Human copy (title, problem statement, "demonstrates" summary) lives entirely in the
|
||||
# frontend's demo.json (scenarios.items.<id>.*) so it's available in all three UI
|
||||
# languages. This service only emits stable identifiers and message codes -- never
|
||||
# display prose -- per the message_code + params architecture used across the app.
|
||||
return_ready = bool(
|
||||
booking and booking.status == "active" and booking.end_odometer_km is None
|
||||
)
|
||||
duplicate_ready = bool(duplicate_issue and duplicate_issue.status == "open")
|
||||
overlap_ready = bool(overlap_issue and overlap_issue.status == "open")
|
||||
automation_ready = bool(failed_run and failed_run.delivery_status == "failed")
|
||||
|
||||
return [
|
||||
DemoScenarioOut(
|
||||
id="return-anomaly",
|
||||
title="Retour met afwijkende kilometerstand",
|
||||
operational_problem=(
|
||||
"Een voertuig komt terug met een kilometerstand die lager ligt dan de "
|
||||
"laatst geregistreerde stand — een teken van een foutieve invoer of een "
|
||||
"verwisseld voertuig."
|
||||
),
|
||||
estimated_minutes=3,
|
||||
required_roles=["rental_employee", "operations_manager"],
|
||||
start_path=f"/bookings/{booking.public_ref}" if booking else "/bookings",
|
||||
demonstrates=(
|
||||
"Retourverwerking, automatische detectie van datakwaliteitsproblemen en de "
|
||||
"audit trail die daaruit ontstaat."
|
||||
),
|
||||
ready=bool(
|
||||
booking and booking.status == "active" and booking.end_odometer_km is None
|
||||
),
|
||||
blocked_reason=(
|
||||
ready=return_ready,
|
||||
blocked_reason_code=(
|
||||
None
|
||||
if booking and booking.status == "active" and booking.end_odometer_km is None
|
||||
else (
|
||||
f"Demoboeking BK-DEMO-RETURN niet gevonden. {reset_hint}"
|
||||
if booking is None
|
||||
else f"Deze boeking is al verwerkt sinds de laatste reset. {reset_hint}"
|
||||
)
|
||||
if return_ready
|
||||
else "bookingNotFound" if booking is None else "bookingAlreadyProcessed"
|
||||
),
|
||||
),
|
||||
DemoScenarioOut(
|
||||
id="duplicate-customer",
|
||||
title="Mogelijke dubbele klant samenvoegen",
|
||||
operational_problem=(
|
||||
"Twee klantprofielen delen hetzelfde e-mailadres en telefoonnummer — "
|
||||
"waarschijnlijk dezelfde persoon, twee keer geregistreerd."
|
||||
),
|
||||
estimated_minutes=3,
|
||||
required_roles=["operations_manager"],
|
||||
start_path=(
|
||||
@@ -106,87 +77,46 @@ def _scenarios(db: Session) -> list[DemoScenarioOut]:
|
||||
if duplicate_issue
|
||||
else "/data-quality"
|
||||
),
|
||||
demonstrates=(
|
||||
"Samenvoegen van klanten met behoud van boekingsgeschiedenis en audit trail."
|
||||
),
|
||||
ready=bool(duplicate_issue and duplicate_issue.status == "open"),
|
||||
blocked_reason=(
|
||||
ready=duplicate_ready,
|
||||
blocked_reason_code=(
|
||||
None
|
||||
if duplicate_issue and duplicate_issue.status == "open"
|
||||
else (
|
||||
f"Demo-issue DQ-DEMO-DUPLICATE niet gevonden. {reset_hint}"
|
||||
if duplicate_issue is None
|
||||
else f"Dit issue is al opgelost sinds de laatste reset. {reset_hint}"
|
||||
)
|
||||
if duplicate_ready
|
||||
else "duplicateIssueNotFound" if duplicate_issue is None else "issueAlreadyResolved"
|
||||
),
|
||||
),
|
||||
DemoScenarioOut(
|
||||
id="booking-overlap",
|
||||
title="Overlappende boekingen herstellen",
|
||||
operational_problem=(
|
||||
"Eén voertuig staat dubbel gereserveerd voor overlappende periodes — een "
|
||||
"planningsfout die vóór vertrek moet worden opgelost."
|
||||
),
|
||||
estimated_minutes=2,
|
||||
required_roles=["operations_manager"],
|
||||
start_path=(
|
||||
f"/data-quality/{overlap_issue.public_ref}" if overlap_issue else "/data-quality"
|
||||
),
|
||||
demonstrates="Detectie en gecontroleerde oplossing van planningsconflicten.",
|
||||
ready=bool(overlap_issue and overlap_issue.status == "open"),
|
||||
blocked_reason=(
|
||||
ready=overlap_ready,
|
||||
blocked_reason_code=(
|
||||
None
|
||||
if overlap_issue and overlap_issue.status == "open"
|
||||
else (
|
||||
f"Demo-issue DQ-DEMO-OVERLAP niet gevonden. {reset_hint}"
|
||||
if overlap_issue is None
|
||||
else f"Dit issue is al opgelost sinds de laatste reset. {reset_hint}"
|
||||
)
|
||||
if overlap_ready
|
||||
else "overlapIssueNotFound" if overlap_issue is None else "issueAlreadyResolved"
|
||||
),
|
||||
),
|
||||
DemoScenarioOut(
|
||||
id="automation-retry",
|
||||
title="Mislukte automatisering opnieuw proberen",
|
||||
operational_problem=(
|
||||
"Eén eerdere gebeurtenis kon niet worden afgeleverd aan de automatisering "
|
||||
"door een gesimuleerde verbindingsfout."
|
||||
),
|
||||
estimated_minutes=2,
|
||||
required_roles=["operations_manager"],
|
||||
start_path="/automation",
|
||||
demonstrates=(
|
||||
"Betrouwbare aflevering met begrensde herpogingen en zichtbare foutstatus."
|
||||
),
|
||||
ready=bool(failed_run and failed_run.delivery_status == "failed"),
|
||||
blocked_reason=(
|
||||
ready=automation_ready,
|
||||
blocked_reason_code=(
|
||||
None
|
||||
if failed_run and failed_run.delivery_status == "failed"
|
||||
else (
|
||||
f"Gesimuleerde mislukte gebeurtenis niet gevonden. {reset_hint}"
|
||||
if failed_run is None
|
||||
else f"Deze gebeurtenis is al hersteld sinds de laatste reset. {reset_hint}"
|
||||
)
|
||||
if automation_ready
|
||||
else "failedEventNotFound" if failed_run is None else "eventAlreadyRecovered"
|
||||
),
|
||||
),
|
||||
DemoScenarioOut(
|
||||
id="knowledge-question",
|
||||
title="Een procedurevraag stellen",
|
||||
operational_problem=(
|
||||
"Een medewerker weet niet zeker welke procedure van toepassing is bij een "
|
||||
"specifieke operationele situatie."
|
||||
),
|
||||
estimated_minutes=2,
|
||||
required_roles=["rental_employee", "operations_manager"],
|
||||
start_path="/knowledge",
|
||||
demonstrates=(
|
||||
"Antwoorden met brongebaseerde onderbouwing uit een afgebakende demokennisbank."
|
||||
),
|
||||
ready=knowledge_health.available,
|
||||
blocked_reason=(
|
||||
None
|
||||
if knowledge_health.available
|
||||
else "De demokennisbank is momenteel niet beschikbaar."
|
||||
),
|
||||
blocked_reason_code=None if knowledge_health.available else "knowledgeUnavailable",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -198,27 +128,32 @@ def _integrations(db: Session) -> list[DemoIntegrationSummaryOut]:
|
||||
return [
|
||||
DemoIntegrationSummaryOut(
|
||||
key="n8n",
|
||||
label="Automatisering (n8n)",
|
||||
status_label=_N8N_STATE_LABELS.get(n8n.state, n8n.state),
|
||||
detail=f"{n8n.succeeded} geslaagd, {n8n.failed} mislukt, {n8n.pending} in wachtrij.",
|
||||
status_code=n8n.state,
|
||||
detail_code="n8nDetail",
|
||||
detail_params={
|
||||
"succeeded": n8n.succeeded,
|
||||
"failed": n8n.failed,
|
||||
"pending": n8n.pending,
|
||||
},
|
||||
),
|
||||
DemoIntegrationSummaryOut(
|
||||
key="ragcore",
|
||||
label="Kennisassistent (RAGcore)",
|
||||
status_label=(
|
||||
"Demomodus — lokale kennisprovider"
|
||||
if knowledge_health.provider != "ragcore"
|
||||
else "Operationeel"
|
||||
),
|
||||
detail=knowledge_health.detail,
|
||||
status_code="operational" if knowledge_health.provider == "ragcore" else "demoMode",
|
||||
detail_code="ragcoreDetail",
|
||||
detail_params={
|
||||
"count": knowledge_health.document_count,
|
||||
"collection": knowledge_health.collection,
|
||||
},
|
||||
),
|
||||
DemoIntegrationSummaryOut(
|
||||
key="mcp_hub",
|
||||
label="ITWorx MCP Hub",
|
||||
status_label=(
|
||||
"Operationeel" if settings.mcp_hub_registration_enabled else "Niet gekoppeld"
|
||||
status_code="operational" if settings.mcp_hub_registration_enabled else "notConnected",
|
||||
detail_code=(
|
||||
"mcpDetailEnabled"
|
||||
if settings.mcp_hub_registration_enabled
|
||||
else "mcpDetailNotConnected"
|
||||
),
|
||||
detail="Voorbereid voor toekomstige, gecontroleerde tool-aanroepen vanuit de Hub.",
|
||||
detail_params={},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -230,7 +165,7 @@ def scenario_integrity_report(db: Session) -> dict:
|
||||
overview already use, so this can never drift from what a visitor actually sees."""
|
||||
scenarios = _scenarios(db)
|
||||
not_ready = [
|
||||
{"id": s.id, "title": s.title, "reason": s.blocked_reason}
|
||||
{"id": s.id, "reason_code": s.blocked_reason_code}
|
||||
for s in scenarios
|
||||
if not s.ready
|
||||
]
|
||||
@@ -242,7 +177,6 @@ def build_demo_manifest(db: Session) -> DemoManifestOut:
|
||||
return DemoManifestOut(
|
||||
demo_mode=settings.mobilityops_demo_mode,
|
||||
organization_name=settings.demo_organization_name,
|
||||
organization_description=ORGANIZATION_DESCRIPTION,
|
||||
timezone=settings.demo_timezone,
|
||||
synthetic_data=True,
|
||||
allow_reset=settings.demo_allow_reset,
|
||||
|
||||
@@ -39,9 +39,11 @@ class KnowledgeHealth(BaseModel):
|
||||
class KnowledgeProvider(Protocol):
|
||||
name: str
|
||||
|
||||
def health(self) -> KnowledgeHealth: ...
|
||||
def health(self, language: str = "en-GB") -> KnowledgeHealth: ...
|
||||
|
||||
def ask(self, question: str, correlation_id: str) -> GroundedAnswer: ...
|
||||
def ask(
|
||||
self, question: str, correlation_id: str, language: str = "en-GB"
|
||||
) -> GroundedAnswer: ...
|
||||
|
||||
|
||||
@lru_cache
|
||||
|
||||
@@ -8,12 +8,31 @@ from pathlib import Path
|
||||
from app.core.config import get_settings
|
||||
from app.services.knowledge import GroundedAnswer, KnowledgeHealth, SourceCard
|
||||
|
||||
STOPWORDS = {
|
||||
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
||||
"to", "of", "in", "on", "at", "for", "and", "or", "but", "if", "then",
|
||||
"do", "does", "did", "must", "may", "can", "could", "should", "would",
|
||||
"i", "you", "it", "we", "they", "my", "your", "what", "when", "how",
|
||||
"with", "without", "this", "that", "these", "those", "not", "no",
|
||||
SUPPORTED_LANGUAGES = ("nl-BE", "en-GB", "fr-BE")
|
||||
DEFAULT_LANGUAGE = "en-GB"
|
||||
|
||||
STOPWORDS_BY_LANGUAGE: dict[str, set[str]] = {
|
||||
"en-GB": {
|
||||
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
||||
"to", "of", "in", "on", "at", "for", "and", "or", "but", "if", "then",
|
||||
"do", "does", "did", "must", "may", "can", "could", "should", "would",
|
||||
"i", "you", "it", "we", "they", "my", "your", "what", "when", "how",
|
||||
"with", "without", "this", "that", "these", "those", "not", "no",
|
||||
},
|
||||
"nl-BE": {
|
||||
"een", "de", "het", "is", "zijn", "was", "waren", "worden", "wordt",
|
||||
"van", "in", "op", "voor", "en", "of", "maar", "als", "dan",
|
||||
"moet", "mag", "kan", "kunnen", "zou", "zouden",
|
||||
"ik", "jij", "u", "we", "wij", "zij", "mijn", "jouw", "wat", "wanneer", "hoe",
|
||||
"met", "zonder", "dit", "dat", "deze", "die", "niet", "geen",
|
||||
},
|
||||
"fr-BE": {
|
||||
"un", "une", "le", "la", "les", "des", "est", "sont", "était", "être",
|
||||
"de", "du", "en", "sur", "pour", "et", "ou", "mais", "si", "alors",
|
||||
"doit", "peut", "peuvent", "pourrait", "devrait",
|
||||
"je", "tu", "vous", "il", "elle", "nous", "ils", "mon", "votre", "quoi", "quand", "comment",
|
||||
"avec", "sans", "ce", "cette", "ces", "cela", "pas", "non",
|
||||
},
|
||||
}
|
||||
|
||||
_WORD_RE = re.compile(r"[a-z0-9]+")
|
||||
@@ -28,9 +47,10 @@ def _stem(word: str) -> str:
|
||||
return word
|
||||
|
||||
|
||||
def _tokenize(text: str) -> set[str]:
|
||||
def _tokenize(text: str, language: str) -> set[str]:
|
||||
stopwords = STOPWORDS_BY_LANGUAGE.get(language, STOPWORDS_BY_LANGUAGE[DEFAULT_LANGUAGE])
|
||||
words = _WORD_RE.findall(text.lower())
|
||||
return {_stem(w) for w in words if w not in STOPWORDS and len(w) > 2}
|
||||
return {_stem(w) for w in words if w not in stopwords and len(w) > 2}
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -86,7 +106,7 @@ def _split_sections(body: str) -> list[tuple[str, str]]:
|
||||
return sections
|
||||
|
||||
|
||||
def _load_sections(procedures_dir: Path) -> list[ScoredSection]:
|
||||
def _load_sections(procedures_dir: Path, language: str) -> list[ScoredSection]:
|
||||
sections: list[ScoredSection] = []
|
||||
for path in sorted(procedures_dir.glob("*.md")):
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
@@ -96,7 +116,7 @@ def _load_sections(procedures_dir: Path) -> list[ScoredSection]:
|
||||
document_id=meta.get("document_id", path.stem),
|
||||
title=title,
|
||||
version=meta.get("version", "1.0"),
|
||||
title_tokens=_tokenize(title),
|
||||
title_tokens=_tokenize(title, language),
|
||||
)
|
||||
for heading, text in _split_sections(body):
|
||||
sections.append(
|
||||
@@ -104,19 +124,49 @@ def _load_sections(procedures_dir: Path) -> list[ScoredSection]:
|
||||
document=doc,
|
||||
heading=heading,
|
||||
text=text,
|
||||
heading_tokens=_tokenize(heading),
|
||||
body_tokens=_tokenize(text),
|
||||
heading_tokens=_tokenize(heading, language),
|
||||
body_tokens=_tokenize(text, language),
|
||||
)
|
||||
)
|
||||
return sections
|
||||
|
||||
|
||||
_NO_MATCH_TEXT = {
|
||||
"en-GB": "No matching procedure was found for this question.",
|
||||
"nl-BE": "Er werd geen passende procedure gevonden voor deze vraag.",
|
||||
"fr-BE": "Aucune procédure correspondante n'a été trouvée pour cette question.",
|
||||
}
|
||||
_LOW_CONFIDENCE_TEXT = {
|
||||
"en-GB": (
|
||||
"The available procedures do not clearly answer this question. "
|
||||
"The closest matches are included below for review."
|
||||
),
|
||||
"nl-BE": (
|
||||
"De beschikbare procedures beantwoorden deze vraag niet duidelijk. "
|
||||
"De dichtstbijzijnde overeenkomsten staan hieronder ter beoordeling."
|
||||
),
|
||||
"fr-BE": (
|
||||
"Les procédures disponibles ne répondent pas clairement à cette question. "
|
||||
"Les correspondances les plus proches sont indiquées ci-dessous pour examen."
|
||||
),
|
||||
}
|
||||
_LEAD_ANSWER_TEMPLATE = {
|
||||
"en-GB": 'Per "{title}" (v{version}), section "{heading}": {excerpt}',
|
||||
"nl-BE": 'Volgens "{title}" (v{version}), sectie "{heading}": {excerpt}',
|
||||
"fr-BE": 'Selon « {title} » (v{version}), section « {heading} » : {excerpt}',
|
||||
}
|
||||
|
||||
|
||||
class DemoKnowledgeProvider:
|
||||
"""Deterministic extractive retrieval over the local procedure Markdown files.
|
||||
|
||||
Not a generative model: it scores sections with TF-IDF-weighted keyword overlap
|
||||
(downweighting terms common across the whole corpus, like "vehicle", in favor of
|
||||
distinctive ones, like "damage") and returns real excerpts, never invented text.
|
||||
|
||||
Each supported UI language has its own translated procedure corpus under
|
||||
knowledge/procedures/<language>/ -- retrieval searches only within the requested
|
||||
language's corpus so citations always link to a same-language document.
|
||||
"""
|
||||
|
||||
name = "demo"
|
||||
@@ -124,10 +174,18 @@ class DemoKnowledgeProvider:
|
||||
def __init__(self) -> None:
|
||||
settings = get_settings()
|
||||
self._settings = settings
|
||||
self._procedures_dir = Path(settings.knowledge_dir)
|
||||
self._sections = _load_sections(self._procedures_dir)
|
||||
self._document_count = len({s.document.document_id for s in self._sections})
|
||||
self._idf = self._build_idf(self._sections)
|
||||
base_dir = Path(settings.knowledge_dir)
|
||||
self._sections_by_language: dict[str, list[ScoredSection]] = {}
|
||||
self._idf_by_language: dict[str, dict[str, float]] = {}
|
||||
self._document_count_by_language: dict[str, int] = {}
|
||||
for language in SUPPORTED_LANGUAGES:
|
||||
lang_dir = base_dir / language
|
||||
sections = _load_sections(lang_dir, language) if lang_dir.is_dir() else []
|
||||
self._sections_by_language[language] = sections
|
||||
self._idf_by_language[language] = self._build_idf(sections)
|
||||
self._document_count_by_language[language] = len(
|
||||
{s.document.document_id for s in sections}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_idf(sections: list[ScoredSection]) -> dict[str, float]:
|
||||
@@ -140,7 +198,13 @@ class DemoKnowledgeProvider:
|
||||
doc_freq[token] = doc_freq.get(token, 0) + 1
|
||||
return {token: math.log((n + 1) / (df + 1)) + 1 for token, df in doc_freq.items()}
|
||||
|
||||
def health(self) -> KnowledgeHealth:
|
||||
def _normalize_language(self, language: str | None) -> str:
|
||||
if language in SUPPORTED_LANGUAGES:
|
||||
return language
|
||||
return DEFAULT_LANGUAGE
|
||||
|
||||
def health(self, language: str = DEFAULT_LANGUAGE) -> KnowledgeHealth:
|
||||
language = self._normalize_language(language)
|
||||
return KnowledgeHealth(
|
||||
provider=self.name,
|
||||
available=True,
|
||||
@@ -148,36 +212,40 @@ class DemoKnowledgeProvider:
|
||||
tenant=self._settings.ragcore_tenant,
|
||||
workspace=self._settings.ragcore_workspace,
|
||||
collection=self._settings.ragcore_collection,
|
||||
document_count=self._document_count,
|
||||
document_count=self._document_count_by_language[language],
|
||||
)
|
||||
|
||||
def _score(self, query_tokens: set[str], section: ScoredSection) -> float:
|
||||
def _score(
|
||||
self, query_tokens: set[str], section: ScoredSection, idf: dict[str, float]
|
||||
) -> float:
|
||||
score = 0.0
|
||||
for token in query_tokens:
|
||||
idf = self._idf.get(token, 0.0)
|
||||
if idf == 0.0:
|
||||
token_idf = idf.get(token, 0.0)
|
||||
if token_idf == 0.0:
|
||||
continue
|
||||
if token in section.heading_tokens:
|
||||
score += 3 * idf
|
||||
score += 3 * token_idf
|
||||
elif token in section.document.title_tokens:
|
||||
score += 2 * idf
|
||||
score += 2 * token_idf
|
||||
elif token in section.body_tokens:
|
||||
score += idf
|
||||
score += token_idf
|
||||
return score
|
||||
|
||||
def ask(self, question: str, correlation_id: str) -> GroundedAnswer:
|
||||
query_tokens = _tokenize(question)
|
||||
scored = [
|
||||
(self._score(query_tokens, section), section)
|
||||
for section in self._sections
|
||||
]
|
||||
def ask(
|
||||
self, question: str, correlation_id: str, language: str = DEFAULT_LANGUAGE
|
||||
) -> GroundedAnswer:
|
||||
language = self._normalize_language(language)
|
||||
sections = self._sections_by_language[language]
|
||||
idf = self._idf_by_language[language]
|
||||
query_tokens = _tokenize(question, language)
|
||||
scored = [(self._score(query_tokens, section, idf), section) for section in sections]
|
||||
scored = [(score, section) for score, section in scored if score > 0]
|
||||
scored.sort(key=lambda item: item[0], reverse=True)
|
||||
top = scored[:3]
|
||||
|
||||
if not top:
|
||||
return GroundedAnswer(
|
||||
answer="No matching procedure was found for this question.",
|
||||
answer=_NO_MATCH_TEXT[language],
|
||||
evidence_state="insufficient",
|
||||
sources=[],
|
||||
provider=self.name,
|
||||
@@ -197,10 +265,7 @@ class DemoKnowledgeProvider:
|
||||
|
||||
if top[0][0] < 3:
|
||||
return GroundedAnswer(
|
||||
answer=(
|
||||
"The available procedures do not clearly answer this question. "
|
||||
"The closest matches are included below for review."
|
||||
),
|
||||
answer=_LOW_CONFIDENCE_TEXT[language],
|
||||
evidence_state="insufficient",
|
||||
sources=sources,
|
||||
provider=self.name,
|
||||
@@ -208,9 +273,11 @@ class DemoKnowledgeProvider:
|
||||
)
|
||||
|
||||
lead_section = top[0][1]
|
||||
answer = (
|
||||
f'Per "{lead_section.document.title}" (v{lead_section.document.version}), '
|
||||
f'section "{lead_section.heading}": {lead_section.text.splitlines()[0][:300]}'
|
||||
answer = _LEAD_ANSWER_TEMPLATE[language].format(
|
||||
title=lead_section.document.title,
|
||||
version=lead_section.document.version,
|
||||
heading=lead_section.heading,
|
||||
excerpt=lead_section.text.splitlines()[0][:300],
|
||||
)
|
||||
return GroundedAnswer(
|
||||
answer=answer,
|
||||
|
||||
@@ -32,7 +32,7 @@ class RAGcoreKnowledgeProvider:
|
||||
timeout=self._settings.ragcore_http_timeout_seconds,
|
||||
)
|
||||
|
||||
def health(self) -> KnowledgeHealth:
|
||||
def health(self, language: str = "en-GB") -> KnowledgeHealth:
|
||||
try:
|
||||
with self._client() as client:
|
||||
response = client.get("/health")
|
||||
@@ -52,7 +52,7 @@ class RAGcoreKnowledgeProvider:
|
||||
document_count=0,
|
||||
)
|
||||
|
||||
def ask(self, question: str, correlation_id: str) -> GroundedAnswer:
|
||||
def ask(self, question: str, correlation_id: str, language: str = "en-GB") -> GroundedAnswer:
|
||||
try:
|
||||
with self._client() as client:
|
||||
response = client.post(
|
||||
@@ -63,6 +63,7 @@ class RAGcoreKnowledgeProvider:
|
||||
"collection": self._settings.ragcore_collection,
|
||||
"question": question,
|
||||
"correlation_id": correlation_id,
|
||||
"language": language,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
Reference in New Issue
Block a user