feat(judge_ngrams): enhance phrase judgment with alias filtering and strict boolean handling
This commit is contained in:
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
from dataclasses import replace
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -45,9 +45,6 @@ if TYPE_CHECKING:
|
|||||||
from python.ebook_search.protected_phrases.models import PhraseCandidate
|
from python.ebook_search.protected_phrases.models import PhraseCandidate
|
||||||
from python.orm.richie import EbookProtectedPhrase
|
from python.orm.richie import EbookProtectedPhrase
|
||||||
|
|
||||||
JSON_OBJECT_RE = re.compile(r"\{.*\}", re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -262,7 +259,9 @@ async def judge_candidate_async(
|
|||||||
Returns:
|
Returns:
|
||||||
LLMJudgment: The parsed judgment.
|
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)
|
return parse_llm_judgment(content, config)
|
||||||
|
|
||||||
|
|
||||||
@@ -287,12 +286,23 @@ async def persist_book_judgments(
|
|||||||
book_started_at = perf_counter()
|
book_started_at = perf_counter()
|
||||||
async with AsyncSession(engine, expire_on_commit=False) as session:
|
async with AsyncSession(engine, expire_on_commit=False) as session:
|
||||||
try:
|
try:
|
||||||
|
normalized_book_text = normalize_text(await load_book_text(session, source_id))
|
||||||
protected: list[EbookProtectedPhrase] = []
|
protected: list[EbookProtectedPhrase] = []
|
||||||
for candidate_id, candidate, judgment, promote in judged:
|
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:
|
if promote:
|
||||||
protected.append(
|
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(
|
logger.info(
|
||||||
f"ebook_candidate_phrase_judgment_candidate_complete {source_id=} {candidate_id=} "
|
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):
|
if not isinstance(aliases, list | tuple):
|
||||||
aliases = ()
|
aliases = ()
|
||||||
return LLMJudgment(
|
return LLMJudgment(
|
||||||
keep=bool(body.get("keep", False)),
|
keep=strict_bool(body.get("keep"), default=False),
|
||||||
canonical=optional_text(body.get("canonical")),
|
canonical=optional_text(body.get("canonical")),
|
||||||
category=optional_text(body.get("category")),
|
category=optional_text(body.get("category")),
|
||||||
aliases=tuple(str(alias) for alias in aliases if isinstance(alias, str) and alias.strip()),
|
aliases=tuple(str(alias) for alias in aliases if isinstance(alias, str) and alias.strip()),
|
||||||
confidence=clamped_float(body.get("confidence"), default=0.0),
|
confidence=clamped_float(body.get("confidence"), default=0.0),
|
||||||
importance=clamped_float(body.get("importance"), default=0.5),
|
importance=clamped_float(body.get("importance"), default=0.5),
|
||||||
allow_nested=bool(body.get("allow_nested", config.phrase_default_allow_nested)),
|
allow_nested=strict_bool(body.get("allow_nested"), default=config.phrase_default_allow_nested),
|
||||||
suppress_children=bool(body.get("suppress_children", config.phrase_default_suppress_children)),
|
suppress_children=strict_bool(body.get("suppress_children"), default=config.phrase_default_suppress_children),
|
||||||
reason=optional_text(body.get("reason")),
|
reason=optional_text(body.get("reason")),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -419,14 +429,13 @@ def extract_json_object(content: str) -> str:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If no JSON object is found in the response.
|
ValueError: If no JSON object is found in the response.
|
||||||
"""
|
"""
|
||||||
stripped = content.strip()
|
return content.strip()
|
||||||
if stripped.startswith("{") and stripped.endswith("}"):
|
|
||||||
return stripped
|
|
||||||
match = JSON_OBJECT_RE.search(stripped)
|
def alias_occurs_in_book(alias: str, normalized_book_text: str) -> bool:
|
||||||
if match is None:
|
"""Return whether a normalized alias occurs as a complete phrase in the source book."""
|
||||||
msg = "LLM phrase judge response did not contain a JSON object"
|
alias_norm = normalize_text(alias)
|
||||||
raise ValueError(msg)
|
return bool(alias_norm) and f" {alias_norm} " in f" {normalized_book_text} "
|
||||||
return match.group(0)
|
|
||||||
|
|
||||||
|
|
||||||
def optional_text(value: object) -> str | None:
|
def optional_text(value: object) -> str | None:
|
||||||
@@ -444,6 +453,11 @@ def optional_text(value: object) -> str | None:
|
|||||||
return stripped or 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:
|
def clamped_float(value: object, *, default: float) -> float:
|
||||||
"""Coerce a JSON number into the 0.0 to 1.0 range.
|
"""Coerce a JSON number into the 0.0 to 1.0 range.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user