thangvip's picture
docs(codex): publish Build Small submission package (#4)
b535bf5
Raw
History Blame Contribute Delete
11.6 kB
from __future__ import annotations
import re
from collections.abc import Sequence
from .schema import Clearing, SituationPlan
_WORD_PATTERN = re.compile(r"[A-Za-z][A-Za-z'-]{2,}")
_DUPLICATE_WORD_PATTERN = re.compile(r"[A-Za-z][A-Za-z'-]*")
_DUPLICATE_FIELDS = ("scene_intro", "narration", "reflection", "spell")
_ARC_ORDER = ("arrive", "steady", "widen", "step", "carry")
_ABSTRACT_STOCK_PHRASES = (
"leave space around the worry",
"more than one future remains possible",
"the whole path is already settled",
"the whole path is settled",
"keep your own pace",
"return to what is known",
"stay with what is known",
"treating its forecast as a settled fact",
"what deserves attention now",
"when the worry grows loud",
)
_PRACTICAL_ACTION_PATTERN = re.compile(
r"\b(?:could|might|can|consider|try|one option is(?: to)?|"
r"(?:one|a) small step is(?: to)?)\b"
r"(?:\W+\w+){0,5}\W+"
r"(?:ask|break|check|choos(?:e|ing)|compar(?:e|ing)|contact|"
r"focus|identif(?:y|ying)|list|look|not(?:e|ing)|pick|practic(?:e|ing)|"
r"read|review|schedul(?:e|ing)|start|stud(?:y|ying)|talk|test|"
r"tackl(?:e|ing)|revisit|writ(?:e|ing))\b",
re.IGNORECASE,
)
_NUMBER_WORDS = {
"zero",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
}
_DATE_WORDS = {
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
"january",
"february",
"march",
"april",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
}
_UNSUPPORTED_DETAIL_PHRASES = {
"application",
"applications",
"cafe",
"coffee break",
"coffee breaks",
"company",
"cover letter",
"cover letters",
"hiring manager",
"hiring managers",
"interview",
"interviews",
"mentor",
"old boss",
"team meeting",
"team meetings",
}
_ACTION_FORMS = {
"apply": {"applied", "apply"},
"ask": {"asked", "ask"},
"book": {"booked", "book"},
"complete": {"completed", "complete"},
"draft": {"drafted", "draft"},
"finish": {"finished", "finish"},
"fix": {"fixed", "fix"},
"include": {"included", "include"},
"make": {"made", "make"},
"meet": {"met", "meet"},
"notice": {"noticed", "notice"},
"practice": {"practiced", "practised", "practice", "practise"},
"prepare": {"prepared", "prepare"},
"remember": {"remembered", "remember"},
"research": {"researched", "research"},
"revise": {"revised", "revise"},
"schedule": {"scheduled", "schedule"},
"send": {"sent", "send"},
"show": {"showed", "shown", "show"},
"speak": {"spoke", "spoken", "speak"},
"start": {"started", "start"},
"talk": {"talked", "talk"},
"write": {"wrote", "written", "write"},
}
_ACTION_LOOKUP = {form: root for root, forms in _ACTION_FORMS.items() for form in forms}
_PAST_ACTION_LOOKUP = {
form: root
for root, forms in _ACTION_FORMS.items()
for form in forms
if form not in {root, "practise"}
}
_PERFECT_CLAIM_PATTERN = re.compile(
r"\byou(?:'ve| have| had)\s+(?:already\s+)?"
r"(?P<verb>" + "|".join(sorted(_ACTION_LOOKUP, key=len, reverse=True)) + r")\b",
re.IGNORECASE,
)
_SIMPLE_PAST_CLAIM_PATTERN = re.compile(
r"\byou\s+(?P<verb>"
+ "|".join(sorted(_PAST_ACTION_LOOKUP, key=len, reverse=True))
+ r")\b",
re.IGNORECASE,
)
_DIRECT_CLAIM_PATTERN = re.compile(
r"(?:^|[.!?]\s+)[\"'“”]?\s*you\s+(?P<verb>[a-z][a-z'-]*)\b",
re.IGNORECASE,
)
_NON_BIOGRAPHICAL_YOU_VERBS = {
"are",
"can",
"could",
"deserve",
"do",
"don't",
"fear",
"feel",
"hope",
"know",
"matter",
"may",
"might",
"need",
"seem",
"should",
"want",
"wonder",
"would",
"worry",
}
_STOP_WORDS = {
"about",
"after",
"again",
"and",
"are",
"because",
"before",
"being",
"but",
"can",
"for",
"from",
"have",
"help",
"into",
"need",
"new",
"next",
"starting",
"staying",
"that",
"the",
"their",
"them",
"this",
"with",
"you",
"your",
}
def extract_situation_terms(situation: str) -> set[str]:
return {
token.casefold()
for token in _WORD_PATTERN.findall(situation)
if token.casefold() not in _STOP_WORDS
}
def groundedness_score(line: str, situation: str) -> int:
situation_terms = extract_situation_terms(situation)
line_terms = {token.casefold() for token in _WORD_PATTERN.findall(line)}
return len(situation_terms & line_terms)
def is_situation_grounded(line: str, situation: str) -> bool:
return groundedness_score(line, situation) >= 1
def _normalized_phrase(text: str) -> str:
return " ".join(_DUPLICATE_WORD_PATTERN.findall(text.casefold()))
def source_phrase_in_situation(source_phrase: str, situation: str) -> bool:
source = _normalized_phrase(source_phrase)
return bool(source) and source in _normalized_phrase(situation)
def invalid_fact_anchor_indices(
plan: SituationPlan,
situation: str,
) -> list[int]:
return [
index
for index, anchor in enumerate(plan.fact_anchors)
if not source_phrase_in_situation(anchor.source_phrase, situation)
]
def _number_tokens(text: str) -> set[str]:
return {
token
for token in re.findall(r"\b(?:\d+|[a-z]+)\b", text.casefold())
if token.isdigit() or token in _NUMBER_WORDS
}
def _date_tokens(text: str) -> set[str]:
tokens = set(re.findall(r"\b[a-z]+\b", text.casefold()))
result = tokens & _DATE_WORDS
result.update(re.findall(r"\b\d{1,2}(?::\d{2})?\s*(?:a\.?m\.?|p\.?m\.?)\b", text.casefold()))
return result
def _action_roots(text: str) -> set[str]:
tokens = set(re.findall(r"\b[a-z]+\b", text.casefold()))
return {root for form, root in _ACTION_LOOKUP.items() if form in tokens}
def _match_is_in_question(text: str, start: int) -> bool:
sentence_start = max(
text.rfind(".", 0, start),
text.rfind("!", 0, start),
text.rfind("?", 0, start),
)
sentence_end_candidates = [
position
for punctuation in ".!?"
if (position := text.find(punctuation, start)) >= 0
]
if not sentence_end_candidates:
return False
sentence_end = min(sentence_end_candidates)
return text[sentence_start + 1 : sentence_end + 1].strip().endswith("?")
def unsupported_specificity(text: str, situation: str) -> set[str]:
"""Find concrete claims in generated prose that the user did not provide."""
issues: set[str] = set()
if _number_tokens(text) - _number_tokens(situation):
issues.add("invented_number")
if _date_tokens(text) - _date_tokens(situation):
issues.add("invented_date")
normalized_text = _normalized_phrase(text)
normalized_situation = _normalized_phrase(situation)
if any(
phrase in normalized_text and phrase not in normalized_situation
for phrase in _UNSUPPORTED_DETAIL_PHRASES
):
issues.add("unsupported_detail")
situation_actions = _action_roots(situation)
past_claims = (
(_PERFECT_CLAIM_PATTERN, _ACTION_LOOKUP),
(_SIMPLE_PAST_CLAIM_PATTERN, _PAST_ACTION_LOOKUP),
)
for pattern, lookup in past_claims:
for match in pattern.finditer(text):
if _match_is_in_question(text, match.start()):
continue
root = lookup[match.group("verb").casefold()]
if root not in situation_actions:
issues.add("unsupported_past_claim")
break
if "unsupported_past_claim" in issues:
break
situation_tokens = set(re.findall(r"\b[a-z][a-z'-]*\b", situation.casefold()))
for match in _DIRECT_CLAIM_PATTERN.finditer(text):
verb = match.group("verb").casefold()
if verb not in _NON_BIOGRAPHICAL_YOU_VERBS and verb not in situation_tokens:
issues.add("unsupported_direct_claim")
break
return issues
def content_quality_issues(clearing: Clearing) -> set[str]:
"""Report stock abstraction and missing practical help in a clearing."""
issues: set[str] = set()
normalized = _normalized_phrase(
" ".join((clearing.scene_intro, clearing.narration, clearing.reflection))
)
if any(phrase in normalized for phrase in _ABSTRACT_STOCK_PHRASES):
issues.add("abstract_language")
if clearing.arc_role == "step" and (
"abstract_language" in issues
or not _PRACTICAL_ACTION_PATTERN.search(clearing.narration)
):
issues.add("missing_practical_step")
return issues
def repeated_source_phrase_indices(
clearings: Sequence[Clearing],
*,
minimum_words: int = 5,
) -> set[int]:
"""Flag later narrations that repeat the same long source phrase."""
seen: set[str] = set()
repeated: set[int] = set()
for index, clearing in enumerate(clearings):
source = _normalized_phrase(clearing.source_phrase)
if len(source.split()) < minimum_words:
continue
if source not in _normalized_phrase(clearing.narration):
continue
if source in seen:
repeated.add(index)
seen.add(source)
return repeated
def valid_arc_indices(clearings: Sequence[Clearing]) -> list[int]:
"""Return the first clearing for each arc role in narrative order."""
by_role: dict[str, int] = {}
for index, clearing in enumerate(clearings):
by_role.setdefault(clearing.arc_role, index)
return [by_role[role] for role in _ARC_ORDER if role in by_role]
def _normalized_tokens(text: str) -> set[str]:
return {token.casefold().strip("'") for token in _DUPLICATE_WORD_PATTERN.findall(text)}
def _token_containment(left: str, right: str) -> float:
left_tokens = _normalized_tokens(left)
right_tokens = _normalized_tokens(right)
if not left_tokens or not right_tokens:
return 0
return len(left_tokens & right_tokens) / min(len(left_tokens), len(right_tokens))
def duplicate_fields(
clearings: Sequence[Clearing],
indices: Sequence[int] | None = None,
*,
threshold: float = 0.78,
) -> dict[int, list[str]]:
"""Report fields in later clearings that substantially repeat an earlier one."""
candidate_indices = list(indices) if indices is not None else list(range(len(clearings)))
duplicates: dict[int, list[str]] = {}
for position, index in enumerate(candidate_indices):
for prior_index in candidate_indices[:position]:
for field in _DUPLICATE_FIELDS:
if field in duplicates.get(index, []):
continue
left = getattr(clearings[prior_index], field)
right = getattr(clearings[index], field)
if _token_containment(left, right) >= threshold:
duplicates.setdefault(index, []).append(field)
return duplicates
def distinct_indices(
clearings: Sequence[Clearing],
indices: Sequence[int],
*,
threshold: float = 0.78,
) -> list[int]:
"""Keep the earliest clearing from each group of repetitive prose."""
selected: list[int] = []
for index in indices:
if not duplicate_fields(
clearings,
[*selected, index],
threshold=threshold,
).get(index):
selected.append(index)
return selected