feat(judge_ngrams): enhance phrase judgment with alias filtering and strict boolean handling

This commit is contained in:
2026-07-24 11:38:51 -04:00
parent 31ecad881f
commit 62bcc4e156
@@ -5,7 +5,7 @@ from __future__ import annotations
import asyncio
import json
import logging
import re
from dataclasses import replace
from time import perf_counter
from typing import TYPE_CHECKING
@@ -45,9 +45,6 @@ if TYPE_CHECKING:
from python.ebook_search.protected_phrases.models import PhraseCandidate
from python.orm.richie import EbookProtectedPhrase
JSON_OBJECT_RE = re.compile(r"\{.*\}", re.DOTALL)
logger = logging.getLogger(__name__)
@@ -262,7 +259,9 @@ async def judge_candidate_async(
Returns:
LLMJudgment: The parsed judgment.
"""
content = await request_chat_completion(client, config, build_judge_messages(candidate))
content = await request_chat_completion(
client, config, build_judge_messages(candidate), response_format={"type": "json_object"}
)
return parse_llm_judgment(content, config)
@@ -287,12 +286,23 @@ async def persist_book_judgments(
book_started_at = perf_counter()
async with AsyncSession(engine, expire_on_commit=False) as session:
try:
normalized_book_text = normalize_text(await load_book_text(session, source_id))
protected: list[EbookProtectedPhrase] = []
for candidate_id, candidate, judgment, promote in judged:
candidate_row = await save_candidate_to_db(session, source_id, None, candidate, judgment=judgment)
filtered_judgment = replace(
judgment,
aliases=tuple(
alias for alias in judgment.aliases if alias_occurs_in_book(alias, normalized_book_text)
),
)
candidate_row = await save_candidate_to_db(
session, source_id, None, candidate, judgment=filtered_judgment
)
if promote:
protected.append(
await upsert_protected_phrase(session, source_id, None, candidate, judgment, candidate_row)
await upsert_protected_phrase(
session, source_id, None, candidate, filtered_judgment, candidate_row
)
)
logger.info(
f"ebook_candidate_phrase_judgment_candidate_complete {source_id=} {candidate_id=} "
@@ -395,14 +405,14 @@ def parse_llm_judgment(content: str, config: EbookSearchConfig) -> LLMJudgment:
if not isinstance(aliases, list | tuple):
aliases = ()
return LLMJudgment(
keep=bool(body.get("keep", False)),
keep=strict_bool(body.get("keep"), default=False),
canonical=optional_text(body.get("canonical")),
category=optional_text(body.get("category")),
aliases=tuple(str(alias) for alias in aliases if isinstance(alias, str) and alias.strip()),
confidence=clamped_float(body.get("confidence"), default=0.0),
importance=clamped_float(body.get("importance"), default=0.5),
allow_nested=bool(body.get("allow_nested", config.phrase_default_allow_nested)),
suppress_children=bool(body.get("suppress_children", config.phrase_default_suppress_children)),
allow_nested=strict_bool(body.get("allow_nested"), default=config.phrase_default_allow_nested),
suppress_children=strict_bool(body.get("suppress_children"), default=config.phrase_default_suppress_children),
reason=optional_text(body.get("reason")),
)
@@ -419,14 +429,13 @@ def extract_json_object(content: str) -> str:
Raises:
ValueError: If no JSON object is found in the response.
"""
stripped = content.strip()
if stripped.startswith("{") and stripped.endswith("}"):
return stripped
match = JSON_OBJECT_RE.search(stripped)
if match is None:
msg = "LLM phrase judge response did not contain a JSON object"
raise ValueError(msg)
return match.group(0)
return content.strip()
def alias_occurs_in_book(alias: str, normalized_book_text: str) -> bool:
"""Return whether a normalized alias occurs as a complete phrase in the source book."""
alias_norm = normalize_text(alias)
return bool(alias_norm) and f" {alias_norm} " in f" {normalized_book_text} "
def optional_text(value: object) -> str | None:
@@ -444,6 +453,11 @@ def optional_text(value: object) -> str | None:
return stripped or None
def strict_bool(value: object, *, default: bool) -> bool:
"""Return a JSON boolean, falling back when the value has another type."""
return value if isinstance(value, bool) else default
def clamped_float(value: object, *, default: float) -> float:
"""Coerce a JSON number into the 0.0 to 1.0 range.