Files
wehub-resource-sync 76d991c447
Auto Update PR / update-prs (push) Has been cancelled
CI / format-check (push) Has been cancelled
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / live-api-tests (push) Has been cancelled
CI / plugin-integration-test (push) Has been cancelled
CI / ollama-integration-test (push) Has been cancelled
CI / test-fork-pr (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:14 +08:00

270 lines
8.5 KiB
Python

# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Prompt validation for alignment checks on few-shot examples."""
from __future__ import annotations
from collections.abc import Sequence
import copy
import dataclasses
import enum
from absl import logging
from langextract import resolver
from langextract.core import data
from langextract.core import tokenizer as tokenizer_lib
__all__ = [
"IssueKind",
"PromptValidationLevel",
"ValidationIssue",
"ValidationReport",
"PromptAlignmentError",
"AlignmentPolicy",
"validate_prompt_alignment",
"handle_alignment_report",
]
_FUZZY_ALIGNMENT_MIN_THRESHOLD = 0.75
_FUZZY_ALIGNMENT_MIN_DENSITY = 1 / 3
_DEFAULT_FUZZY_ALGORITHM = "lcs"
_DEFAULT_EXACT_ALGORITHM = "dp"
class PromptValidationLevel(enum.Enum):
"""Validation levels for prompt alignment checks."""
OFF = "off"
WARNING = "warning"
ERROR = "error"
class IssueKind(enum.Enum):
"""Categorization of alignment issues."""
FAILED = "failed" # alignment_status is None
NON_EXACT = "non_exact" # MATCH_FUZZY or MATCH_LESSER
@dataclasses.dataclass(frozen=True)
class ValidationIssue:
"""Represents a single validation issue found during alignment."""
example_index: int
example_id: str | None
extraction_class: str
extraction_text_preview: str
alignment_status: data.AlignmentStatus | None
issue_kind: IssueKind
char_interval: tuple[int, int] | None = None
token_interval: tuple[int, int] | None = None
def short_msg(self) -> str:
"""Returns a concise message describing the issue."""
ex_id = f" id={self.example_id}" if self.example_id else ""
span = ""
if self.char_interval:
span = f" char_span={self.char_interval}"
return (
f"[example#{self.example_index}{ex_id}] "
f"class='{self.extraction_class}' "
f"status={self.alignment_status} "
f"text='{self.extraction_text_preview}'{span}"
)
@dataclasses.dataclass
class ValidationReport:
"""Collection of validation issues from prompt alignment checks."""
issues: list[ValidationIssue]
@property
def has_failed(self) -> bool:
"""Returns True if any extraction failed to align."""
return any(i.issue_kind is IssueKind.FAILED for i in self.issues)
@property
def has_non_exact(self) -> bool:
"""Returns True if any extraction has non-exact alignment."""
return any(i.issue_kind is IssueKind.NON_EXACT for i in self.issues)
class PromptAlignmentError(RuntimeError):
"""Raised when prompt alignment validation fails under ERROR mode."""
@dataclasses.dataclass(frozen=True)
class AlignmentPolicy:
"""Configuration for alignment validation behavior."""
enable_fuzzy_alignment: bool = True
fuzzy_alignment_threshold: float = _FUZZY_ALIGNMENT_MIN_THRESHOLD
accept_match_lesser: bool = True
_: dataclasses.KW_ONLY
fuzzy_alignment_algorithm: str = _DEFAULT_FUZZY_ALGORITHM
fuzzy_alignment_min_density: float = _FUZZY_ALIGNMENT_MIN_DENSITY
exact_alignment_algorithm: str = _DEFAULT_EXACT_ALGORITHM
def _preview(s: str, n: int = 120) -> str:
"""Creates a preview of text for logging, collapsing whitespace."""
s = " ".join(s.split()) # Collapse whitespace for logs
return s if len(s) <= n else s[: n - 1] + "…"
def validate_prompt_alignment(
examples: Sequence[data.ExampleData],
aligner: resolver.WordAligner | None = None,
policy: AlignmentPolicy | None = None,
tokenizer: tokenizer_lib.Tokenizer | None = None,
) -> ValidationReport:
"""Align extractions to their own example text and collect issues.
Args:
examples: The few-shot examples to validate.
aligner: WordAligner instance to use (creates new if None).
policy: Alignment configuration (uses defaults if None).
tokenizer: Optional tokenizer to use for alignment. If None, defaults to
RegexTokenizer.
Returns:
ValidationReport containing any alignment issues found.
"""
if not examples:
return ValidationReport(issues=[])
aligner = aligner or resolver.WordAligner()
policy = policy or AlignmentPolicy()
issues: list[ValidationIssue] = []
for idx, ex in enumerate(examples):
# Defensive copy so validation never mutates user examples.
copied_extractions = [[copy.deepcopy(e) for e in ex.extractions]]
aligned_groups = aligner.align_extractions(
extraction_groups=copied_extractions,
source_text=ex.text,
token_offset=0,
char_offset=0,
enable_fuzzy_alignment=policy.enable_fuzzy_alignment,
fuzzy_alignment_threshold=policy.fuzzy_alignment_threshold,
fuzzy_alignment_algorithm=policy.fuzzy_alignment_algorithm,
fuzzy_alignment_min_density=policy.fuzzy_alignment_min_density,
exact_alignment_algorithm=policy.exact_alignment_algorithm,
accept_match_lesser=policy.accept_match_lesser,
tokenizer_impl=tokenizer,
)
for aligned in aligned_groups[0]:
status = getattr(aligned, "alignment_status", None)
char_interval = getattr(aligned, "char_interval", None)
token_interval = getattr(aligned, "token_interval", None)
klass = getattr(aligned, "extraction_class", "<unknown>")
text = getattr(aligned, "extraction_text", "")
if status is None:
issues.append(
ValidationIssue(
example_index=idx,
example_id=getattr(ex, "example_id", None),
extraction_class=klass,
extraction_text_preview=_preview(text),
alignment_status=None,
issue_kind=IssueKind.FAILED,
char_interval=None,
token_interval=None,
)
)
elif status in (
data.AlignmentStatus.MATCH_FUZZY,
data.AlignmentStatus.MATCH_LESSER,
):
char_interval_tuple = None
token_interval_tuple = None
if char_interval:
char_interval_tuple = (char_interval.start_pos, char_interval.end_pos)
if token_interval:
token_interval_tuple = (
token_interval.start_index,
token_interval.end_index,
)
issues.append(
ValidationIssue(
example_index=idx,
example_id=getattr(ex, "example_id", None),
extraction_class=klass,
extraction_text_preview=_preview(text),
alignment_status=status,
issue_kind=IssueKind.NON_EXACT,
char_interval=char_interval_tuple,
token_interval=token_interval_tuple,
)
)
return ValidationReport(issues=issues)
def handle_alignment_report(
report: ValidationReport,
level: PromptValidationLevel,
*,
strict_non_exact: bool = False,
) -> None:
"""Log or raise based on validation level.
Args:
report: The validation report to handle.
level: The validation level determining behavior.
strict_non_exact: If True, treat non-exact matches as errors in ERROR mode.
Raises:
PromptAlignmentError: If validation fails in ERROR mode.
"""
if level is PromptValidationLevel.OFF:
return
for issue in report.issues:
if issue.issue_kind is IssueKind.NON_EXACT:
logging.warning(
"Prompt alignment: non-exact match: %s", issue.short_msg()
)
else:
logging.warning(
"Prompt alignment: FAILED to align: %s", issue.short_msg()
)
if level is PromptValidationLevel.ERROR:
failed = [i for i in report.issues if i.issue_kind is IssueKind.FAILED]
non_exact = [
i for i in report.issues if i.issue_kind is IssueKind.NON_EXACT
]
if failed:
sample = failed[0].short_msg()
raise PromptAlignmentError(
f"Prompt alignment validation failed: {len(failed)} extraction(s) "
f"could not be aligned (e.g., {sample})"
)
if strict_non_exact and non_exact:
sample = non_exact[0].short_msg()
raise PromptAlignmentError(
"Prompt alignment validation failed under strict mode: "
f"{len(non_exact)} non-exact match(es) found (e.g., {sample})"
)