A / core /perfect_answer.py
linxinhua's picture
key-pool rotation: per-session OpenAI key assignment from OPENAI_KEY_01..10 (core/perfect_answer.py)
ae2eb77 verified
Raw
History Blame Contribute Delete
7.08 kB
"""Per-session generator: produces the perfect-answer essay AND the
discussion plan in a single API call.
Called once at the moment the participant confirms their initial-argument
submission. Reads:
- the participant's argument (passed in by app.py)
- data/case_analysis.md (consolidated case material)
- data/ethics-frameworks.md (analytical lenses)
Returns a dict {"perfect_answer": str, "discussion_plan": str}.
The prompt lives in `prompts/perfect_answer_prompt.md` and is loaded via
`core.config_loader.PERFECT_ANSWER_PROMPT`. To change the generator's
behaviour, edit that markdown file.
Discussion plan schema: each sub-probe has 5 labelled lines:
- id: <int>
component: <consequence-framework component name>
gap: <one sentence on what is missing in the draft>
rationale: <2-3 sentences on why the gap matters>
hook: <participant-facing 1-2 sentence connector; no candidate answers>
Latency: around 15-30s with gpt-5.4-nano at medium reasoning. The host code
in app.py shows a "preparing the discussion" status message during the wait.
"""
import re
from core.config_loader import client as _default_client, MODEL, BASE_DIR, PERFECT_ANSWER_PROMPT
CASE_ANALYSIS_PATH = BASE_DIR / "data" / "case_analysis.md"
FRAMEWORKS_PATH = BASE_DIR / "data" / "ethics-frameworks.md"
USER_TEMPLATE = """=== PARTICIPANT'S DRAFT (for internal guidance only) ===
{argument}
=== CASE CONTENT (background; do not quote, do not list) ===
{case_analysis}
=== ANALYTICAL LENS (consequence framework) ===
{frameworks}
Now produce the three artifacts in the required format.
"""
# Tolerant marker matchers. The model occasionally decorates the markers
# (markdown bold/heading, extra spaces, varying '=' counts, case, or
# space/underscore/hyphen inside "DISCUSSION PLAN"). They must still be the
# only thing on their line. A too-strict regex here was silently dropping
# the discussion plan in earlier versions.
def _marker_re(label):
return re.compile(
r"^[ \t#*>_~`-]*=*[ \t]*" + label + r"[ \t]*=*[ \t#*>_~`-]*$",
re.IGNORECASE | re.MULTILINE,
)
_ESSAY_MK = _marker_re(r"ESSAY")
_DOMINANT_MK = _marker_re(r"DOMINANT[ \t_-]*FRAMEWORK")
_PLAN_MK = _marker_re(r"DISCUSSION[ \t_-]*PLAN")
_END_MK = _marker_re(r"END")
# Structural fallback: the plan body always opens with a numbered family
# header ("1) Family (...):") that is soon followed by a "- id:" line.
_PLAN_HEADER_RE = re.compile(r"^\s*\d+\s*[\).]\s+\S.*:\s*$", re.MULTILINE)
_ID_LINE_RE = re.compile(r"^\s*[-*]\s*id\s*:\s*\d+\s*$", re.IGNORECASE | re.MULTILINE)
def _span(mk, text, start=0):
m = mk.search(text, start)
return (m.start(), m.end()) if m else None
def _structural_plan_start(text, start=0):
"""Index of the first numbered family header that is followed (within
~600 chars) by an `id:` line, or None."""
for hm in _PLAN_HEADER_RE.finditer(text, start):
if _ID_LINE_RE.search(text[hm.start(): hm.start() + 600]):
return hm.start()
return None
def _parse_artifacts(raw):
"""Extract essay + dominant_framework + plan from the delimited response,
tolerating decorated or missing markers. If the DISCUSSION PLAN marker is
absent, the plan is recovered structurally from its numbered-family /
id-line shape. The DOMINANT FRAMEWORK block (if present, between ESSAY
and DISCUSSION PLAN) is extracted and STRIPPED from the essay so the
participant-facing essay text does not leak the internal framework
label."""
raw = raw or ""
e = _span(_ESSAY_MK, raw)
body_start = e[1] if e else 0
p = _span(_PLAN_MK, raw, body_start)
end = _span(_END_MK, raw, p[1] if p else body_start)
end_i = end[0] if end else len(raw)
if p:
essay = raw[body_start:p[0]].strip()
plan = raw[p[1]:end_i].strip()
else:
split_at = _structural_plan_start(raw, body_start)
if split_at is not None:
essay = raw[body_start:split_at].strip()
plan = raw[split_at:end_i].strip()
else:
essay = raw[body_start:end_i].strip()
plan = ""
# Extract DOMINANT FRAMEWORK from the essay region (it sits between
# `=== ESSAY ===` and `=== DISCUSSION PLAN ===`) and strip it out so
# the participant-facing essay text doesn't include the marker block.
dominant = ""
dm = _span(_DOMINANT_MK, essay)
if dm:
# essay text is everything before the DOMINANT marker.
# dominant_framework value is everything after the marker line.
dominant = essay[dm[1]:].strip()
essay = essay[:dm[0]].strip()
# Last-resort salvage: plan triples ended up inside the essay (marker
# missed and the structural split landed too late).
if not plan and _ID_LINE_RE.search(essay):
s = _structural_plan_start(essay)
if s is not None:
plan = essay[s:].strip()
essay = essay[:s].strip()
return {"perfect_answer": essay, "discussion_plan": plan,
"dominant_framework": dominant}
def generate_session_artifacts(argument_text, *, reasoning="medium", verbosity="medium", client=None):
"""Single blocking API call. Returns {"perfect_answer", "discussion_plan"}.
Raises on API error."""
api = client if client is not None else _default_client
case = CASE_ANALYSIS_PATH.read_text(encoding="utf-8")
frameworks = FRAMEWORKS_PATH.read_text(encoding="utf-8")
user_msg = USER_TEMPLATE.format(
argument=argument_text.strip(),
case_analysis=case,
frameworks=frameworks,
)
def _call():
response = api.responses.create(
model=MODEL,
reasoning={"effort": reasoning},
text={"verbosity": verbosity},
input=[
{"role": "developer", "content": PERFECT_ANSWER_PROMPT},
{"role": "user", "content": user_msg},
],
)
return _parse_artifacts(response.output_text or "")
arts = _call()
# The discussion plan is mandatory: the host-side state machine is
# disabled without it. The model occasionally drops the DISCUSSION
# PLAN block (output budget exhaustion or model flakiness). Retry
# up to 2 more times — 3 total attempts. Real production case:
# session 20260601_195128_71512 had two consecutive empty attempts;
# a third would almost certainly have caught it.
MAX_RETRIES = 2
for attempt in range(1, MAX_RETRIES + 1):
if (arts.get("discussion_plan") or "").strip():
break
print(
f"[perfect_answer] discussion_plan empty; retry {attempt}/{MAX_RETRIES}",
flush=True,
)
retry = _call()
if (retry.get("discussion_plan") or "").strip():
arts = retry
break
return arts
# Backwards-compatible alias for any external test harness still importing
# the old name. Returns just the essay string.
def generate_perfect_answer(argument_text, **kwargs):
return generate_session_artifacts(argument_text, **kwargs)["perfect_answer"]