added the classification taxonomy table in the db, team scoped. Allow admins to edit the taxonomy and set up the backend to query it and send it to python each time it's requested. No longer requires a backend restart to make it get the latest taxonomy. Also fixed up how the ordeering of policies is selected and how they chain together in that order. Also made a tiny fix to the my files section to keep it up to date with the indexdb

This commit is contained in:
EthanHealy01
2026-07-02 14:28:02 +01:00
parent 0ae7052dcb
commit c27fd4db69
51 changed files with 3029 additions and 178 deletions
@@ -1,5 +1,5 @@
{
"_generated": "AUTO-GENERATED from frontend/editor/src/proprietary/data/classificationTaxonomy.ts by editor/scripts/generate-taxonomy.mts — do NOT edit by hand; run `task frontend:classifier-categories`.",
"_generated": "AUTO-GENERATED from frontend/editor/src/proprietary/data/classificationTaxonomy.ts by editor/scripts/generate-classification-taxonomy.mts — do NOT edit by hand; run `task frontend:classifier-categories`.",
"categories": [
{
"id": "invoice",
@@ -20,8 +20,10 @@ from stirling.services import AppRuntime
logger = logging.getLogger(__name__)
# Sentinel for a label that fell outside the supplied vocabulary.
# Sentinel id for an answer that fell outside the supplied vocabulary.
UNKNOWN_LABEL = "unknown"
# Human-readable label shown for the off-list sentinel.
UNKNOWN_DISPLAY_LABEL = "Unknown"
# An off-list answer can never be reported as more confident than this, so a
# confident-but-wrong model answer can't clear an organisation's accept
# threshold downstream. See the design doc's "Validate" step.
@@ -36,7 +38,7 @@ WINDOW_PAGES = 2
# (frontend/editor/src/proprietary/data/classificationTaxonomy.ts) via
# `task frontend:classifier-categories` — edit that file, not this JSON. Validated into the
# typed contract on import, so a malformed entry fails fast.
_DEFAULT_TAXONOMY_PATH = Path(__file__).with_name("default_taxonomy.generated.json")
_DEFAULT_TAXONOMY_PATH = Path(__file__).with_name("default_classification_taxonomy.generated.json")
# The file carries an underscore-prefixed "_generated" notice (JSON has no
# comments); drop meta keys before validating against the strict contract.
_raw_taxonomy = json.loads(_DEFAULT_TAXONOMY_PATH.read_text(encoding="utf-8"))
@@ -126,7 +128,9 @@ def validate_against_taxonomy(
if category is None:
return DocumentClassificationResponse(
category=UNKNOWN_LABEL,
category_label=UNKNOWN_DISPLAY_LABEL,
doc_type=UNKNOWN_LABEL,
doc_type_label=UNKNOWN_DISPLAY_LABEL,
type_confidence=min(output.type_confidence, UNKNOWN_MAX_CONFIDENCE),
tags=kept_tags,
)
@@ -136,14 +140,18 @@ def validate_against_taxonomy(
if doc_type is None:
return DocumentClassificationResponse(
category=category.id,
category_label=category.label,
doc_type=UNKNOWN_LABEL,
doc_type_label=UNKNOWN_DISPLAY_LABEL,
type_confidence=min(output.type_confidence, UNKNOWN_MAX_CONFIDENCE),
tags=kept_tags,
)
return DocumentClassificationResponse(
category=category.id,
category_label=category.label,
doc_type=doc_type.id,
doc_type_label=doc_type.label,
type_confidence=output.type_confidence,
tags=kept_tags,
)
@@ -50,15 +50,20 @@ class ClassifyDocumentRequest(ApiModel):
class DocumentClassificationResponse(ApiModel):
"""Terminal classification result.
``category`` and ``doc_type`` are ids drawn from the taxonomy, or the
sentinel ``"unknown"`` when the model's answer fell outside it. ``tags`` are
the subset of the model's tags that exist in the taxonomy. This is a plain
answer from a dedicated endpoint — it carries no ``outcome`` discriminator
(it isn't one of the orchestrator's WorkflowOutcome-routed union responses).
``category`` and ``doc_type`` are ids drawn from the taxonomy (the internal
matching keys), or the sentinel ``"unknown"`` when the model's answer fell
outside it. ``category_label`` and ``doc_type_label`` are the human-readable
labels for those ids (what the UI shows); Python derives them from the
matched taxonomy entry so the two never drift. ``tags`` are the subset of the
model's tags that exist in the taxonomy. This is a plain answer from a
dedicated endpoint — it carries no ``outcome`` discriminator (it isn't one of
the orchestrator's WorkflowOutcome-routed union responses).
"""
category: str
category_label: str
doc_type: str
doc_type_label: str
type_confidence: float = Field(ge=0.0, le=1.0)
tags: list[str] = Field(default_factory=list)
@@ -28,7 +28,12 @@ class StubClassifierAgent:
def classification_client() -> Iterator[TestClient]:
app.dependency_overrides[get_document_classifier_agent] = lambda: StubClassifierAgent(
DocumentClassificationResponse(
category="contract", doc_type="nda", type_confidence=0.96, tags=["legal", "signed"]
category="contract",
category_label="Contract",
doc_type="nda",
doc_type_label="Non-disclosure agreement",
type_confidence=0.96,
tags=["legal", "signed"],
)
)
try:
@@ -45,7 +50,9 @@ def test_classify_returns_camel_cased_result(classification_client: TestClient)
assert response.status_code == 200
body = response.json()
assert body["category"] == "contract"
assert body["categoryLabel"] == "Contract"
assert body["docType"] == "nda"
assert body["docTypeLabel"] == "Non-disclosure agreement"
assert body["typeConfidence"] == 0.96
assert body["tags"] == ["legal", "signed"]