refactor(eval+memoir):精简内部评测路由与服务,composite/对话摘要与 judge 能力补强
- 访谈:新增 interview_state_hints,联动 orchestrator 与提示词 - 回忆录:story_pipeline_sync/state/memory/post_commit 与 Celery 任务调整 - 基建:开发用 celery broker、compose/development 脚本、依赖注入 - eval-web:移除数据集/实验/版本等页面与流式轮询,突出 Playground - 文档与单测同步
This commit is contained in:
@@ -617,6 +617,242 @@ def _ensure_chapter_record(
|
||||
return chapter
|
||||
|
||||
|
||||
def _resolve_append_target(
|
||||
session: Session,
|
||||
*,
|
||||
route_decision: str,
|
||||
route_target_story_id: str | None,
|
||||
user_id: str,
|
||||
chapter_category: str,
|
||||
oral_norm: str,
|
||||
candidate_stories: list,
|
||||
story_meta: dict[str, dict[str, int]],
|
||||
decision_source: str,
|
||||
memoir_correlation_id: str | None,
|
||||
) -> tuple[str | None, str, str]:
|
||||
"""Resolve append target and return (target_story_id, existing_for_narrative, decision_source)."""
|
||||
max_chars = int(settings.story_append_max_canonical_chars)
|
||||
max_ver = int(settings.story_append_max_versions)
|
||||
target_story_id: str | None = None
|
||||
existing_for_narrative = ""
|
||||
|
||||
if route_decision == "append_story" and route_target_story_id:
|
||||
st = session.get(Story, route_target_story_id)
|
||||
if st and st.user_id == user_id:
|
||||
canon = (st.canonical_markdown or "").strip()
|
||||
vc = count_story_versions_sync(session, str(st.id))
|
||||
if len(canon) > max_chars or vc >= max_ver:
|
||||
logger.info(
|
||||
"event=append_overflow_to_new story_id={} canonical_chars={} "
|
||||
"versions={} decision_source={}",
|
||||
str(st.id),
|
||||
len(canon),
|
||||
vc,
|
||||
decision_source,
|
||||
)
|
||||
decision_source = "forced_new_due_to_append_limit"
|
||||
else:
|
||||
target_story_id = st.id
|
||||
existing_for_narrative = canon
|
||||
elif (
|
||||
route_decision == "new_story"
|
||||
and chapter_category in APPEND_FIRST_CHAPTER_CATEGORIES
|
||||
and candidate_stories
|
||||
and len(oral_norm)
|
||||
<= int(settings.memoir_story_route_append_guardrail_oral_chars)
|
||||
):
|
||||
tid_g = default_append_target_story_id(
|
||||
candidate_stories, story_meta, settings
|
||||
)
|
||||
if tid_g:
|
||||
st = session.get(Story, tid_g)
|
||||
if st and st.user_id == user_id:
|
||||
canon = (st.canonical_markdown or "").strip()
|
||||
vc = count_story_versions_sync(session, str(st.id))
|
||||
if len(canon) <= max_chars and vc < max_ver:
|
||||
target_story_id = st.id
|
||||
existing_for_narrative = canon
|
||||
decision_source = "append_guardrail_short_oral"
|
||||
logger.info(
|
||||
"event=story_route_append_guardrail memoir_correlation_id={} "
|
||||
"chapter_category={} oral_len={} story_id={}",
|
||||
memoir_correlation_id or "",
|
||||
chapter_category,
|
||||
len(oral_norm),
|
||||
tid_g,
|
||||
)
|
||||
|
||||
return target_story_id, existing_for_narrative, decision_source
|
||||
|
||||
|
||||
def _execute_narrative_unit(
|
||||
session: Session,
|
||||
*,
|
||||
oral_text: str,
|
||||
evidence_text: str,
|
||||
evidence: dict,
|
||||
evidence_top_k: int,
|
||||
chapter: Chapter,
|
||||
chapter_category: str,
|
||||
slot_snippets: dict[str, str],
|
||||
user_id: str,
|
||||
user_profile: str,
|
||||
user_birth_year: int | None,
|
||||
llm: Any,
|
||||
narrative_agent: NarrativeAgent,
|
||||
target_story_id: str | None,
|
||||
existing_for_narrative: str,
|
||||
decision_source: str,
|
||||
route_decision: str,
|
||||
route_type: str,
|
||||
segment_ids: list[str],
|
||||
category_segments: list,
|
||||
background_voice: str = "default",
|
||||
occupation: str = "",
|
||||
memoir_correlation_id: str | None = None,
|
||||
) -> tuple[str | None, bool]:
|
||||
"""
|
||||
Unified narrative unit executor: generate narrative, apply fidelity/safety,
|
||||
persist story. Returns (story_id, is_append).
|
||||
"""
|
||||
t0 = time.perf_counter()
|
||||
oral_norm = (oral_text or "").strip()
|
||||
new_content_input = format_narrative_user_content(oral_text, evidence_text)
|
||||
|
||||
raw_gen = narrative_agent.generate_narrative(
|
||||
stage=chapter_category,
|
||||
slots=slot_snippets,
|
||||
new_content=new_content_input,
|
||||
existing_content=existing_for_narrative,
|
||||
user_profile=user_profile,
|
||||
birth_year=user_birth_year,
|
||||
llm=llm,
|
||||
background_voice=background_voice,
|
||||
occupation=occupation,
|
||||
fallback_plain_oral=oral_norm,
|
||||
)
|
||||
json_invalid = False
|
||||
s0 = (raw_gen or "").strip()
|
||||
if s0.startswith("{") and "paragraphs" in s0:
|
||||
try:
|
||||
json.loads(s0)
|
||||
except json.JSONDecodeError:
|
||||
json_invalid = True
|
||||
|
||||
narrative_raw, fb_gate = _gate_narrative_fidelity(
|
||||
oral_text,
|
||||
raw_gen,
|
||||
llm,
|
||||
existing_canonical=existing_for_narrative or None,
|
||||
)
|
||||
narrative_raw, fb_apply = _apply_narrative_fallbacks(
|
||||
narrative_raw,
|
||||
oral_text,
|
||||
existing_for_narrative,
|
||||
chapter_category=chapter_category,
|
||||
)
|
||||
fallback_type = _merge_fallback_type(fb_gate, fb_apply)
|
||||
if json_invalid and fallback_type == "none":
|
||||
fallback_type = "json_invalid"
|
||||
|
||||
md = _coalesce_story_markdown(
|
||||
narrative_to_markdown(narrative_raw).strip(),
|
||||
oral_text.strip(),
|
||||
existing_for_narrative or "",
|
||||
)
|
||||
md, inv_fb = _apply_narrative_body_safety(
|
||||
md,
|
||||
oral=oral_text,
|
||||
existing_for_narrative=existing_for_narrative or "",
|
||||
evidence_text=evidence_text,
|
||||
chapter_category=chapter_category,
|
||||
)
|
||||
if inv_fb != "none":
|
||||
fallback_type = (
|
||||
inv_fb if fallback_type == "none" else f"{fallback_type}+{inv_fb}"
|
||||
)
|
||||
|
||||
dlg = _dialogue_lineage_dict_for_segment_ids(category_segments, segment_ids)
|
||||
|
||||
if target_story_id:
|
||||
sid_s = str(target_story_id)
|
||||
ver = append_story_version_sync(session, sid_s, md)
|
||||
_persist_story_lineage_sync(
|
||||
session,
|
||||
story_id=sid_s,
|
||||
version=ver,
|
||||
evidence=evidence,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
top_k=evidence_top_k,
|
||||
dialogue_lineage=dlg,
|
||||
)
|
||||
ensure_chapter_story_link_sync(
|
||||
session, chapter_id=str(chapter.id), story_id=sid_s
|
||||
)
|
||||
sid_log = target_story_id
|
||||
is_append = True
|
||||
else:
|
||||
story_title = _maybe_generate_title(
|
||||
narrative_agent,
|
||||
chapter_category=chapter_category,
|
||||
md=md,
|
||||
slot_snippets=slot_snippets,
|
||||
user_profile=user_profile,
|
||||
user_birth_year=user_birth_year,
|
||||
llm=llm,
|
||||
oral_scope=oral_norm,
|
||||
)
|
||||
st = create_story_with_version_sync(
|
||||
session,
|
||||
user_id=user_id,
|
||||
title=story_title,
|
||||
canonical_markdown=md,
|
||||
stage=chapter_category,
|
||||
)
|
||||
ensure_chapter_story_link_sync(
|
||||
session, chapter_id=str(chapter.id), story_id=str(st.id)
|
||||
)
|
||||
sid_log = st.id
|
||||
is_append = False
|
||||
if st.current_version_id:
|
||||
ver0 = session.get(StoryVersion, st.current_version_id)
|
||||
if ver0:
|
||||
_persist_story_lineage_sync(
|
||||
session,
|
||||
story_id=str(st.id),
|
||||
version=ver0,
|
||||
evidence=evidence,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
top_k=evidence_top_k,
|
||||
dialogue_lineage=dlg,
|
||||
)
|
||||
|
||||
elapsed = time.perf_counter() - t0
|
||||
logger.info(
|
||||
"event=story_generated memoir_correlation_id={} route_type={} "
|
||||
"decision_source={} route_decision={} "
|
||||
"unit_segments={} used_evidence={} narrative_json_valid={} fidelity_passed={} "
|
||||
"fallback_type={} oral_len={} md_len={} chapter_category={} is_append={} "
|
||||
"story_id={} seconds={:.3f}",
|
||||
memoir_correlation_id or "",
|
||||
route_type,
|
||||
decision_source,
|
||||
"append_story" if is_append else "new_story",
|
||||
len(segment_ids),
|
||||
bool(evidence_text.strip()),
|
||||
_is_json_narrative(raw_gen),
|
||||
fb_gate == "none",
|
||||
fallback_type,
|
||||
len(oral_norm),
|
||||
len(md.strip()),
|
||||
chapter_category,
|
||||
is_append,
|
||||
sid_log,
|
||||
elapsed,
|
||||
)
|
||||
return str(sid_log), is_append
|
||||
|
||||
|
||||
def _run_batch_plan_writes(
|
||||
session: Session,
|
||||
*,
|
||||
@@ -640,210 +876,50 @@ def _run_batch_plan_writes(
|
||||
memoir_correlation_id: str | None = None,
|
||||
) -> set[str]:
|
||||
dispatch_ids: set[str] = set()
|
||||
max_chars = int(settings.story_append_max_canonical_chars)
|
||||
max_ver = int(settings.story_append_max_versions)
|
||||
for unit in plan.units:
|
||||
t0 = time.perf_counter()
|
||||
unit_text = _ordered_text_for_segment_ids(category_segments, unit.segment_ids)
|
||||
oral_unit = normalize_oral_for_memoir(unit_text, llm=llm)
|
||||
ut_raw = (unit_text or "").strip()
|
||||
ut_norm = (oral_unit or "").strip()
|
||||
if ut_raw != ut_norm:
|
||||
logger.info(
|
||||
"event=oral_normalized context=batch_unit raw_len={} norm_len={}",
|
||||
len(ut_raw),
|
||||
len(ut_norm),
|
||||
)
|
||||
new_content_input = format_narrative_user_content(oral_unit, evidence_text)
|
||||
|
||||
target_story_id: str | None = None
|
||||
existing_for_narrative = ""
|
||||
decision_source = "batch_plan"
|
||||
if unit.decision == "append_story" and unit.target_story_id:
|
||||
st = session.get(Story, unit.target_story_id)
|
||||
if st and st.user_id == user_id:
|
||||
canon = (st.canonical_markdown or "").strip()
|
||||
vc = count_story_versions_sync(session, str(st.id))
|
||||
if len(canon) > max_chars or vc >= max_ver:
|
||||
logger.info(
|
||||
"event=append_overflow_to_new story_id={} canonical_chars={} "
|
||||
"versions={} decision_source=batch_plan",
|
||||
str(st.id),
|
||||
len(canon),
|
||||
vc,
|
||||
)
|
||||
target_story_id = None
|
||||
existing_for_narrative = ""
|
||||
decision_source = "forced_new_due_to_append_limit"
|
||||
else:
|
||||
target_story_id = st.id
|
||||
existing_for_narrative = canon
|
||||
elif (
|
||||
unit.decision == "new_story"
|
||||
and chapter_category in APPEND_FIRST_CHAPTER_CATEGORIES
|
||||
and candidate_stories
|
||||
and len(ut_norm)
|
||||
<= int(settings.memoir_story_route_append_guardrail_oral_chars)
|
||||
):
|
||||
tid_g = default_append_target_story_id(
|
||||
candidate_stories, story_meta, settings
|
||||
)
|
||||
if tid_g:
|
||||
st = session.get(Story, tid_g)
|
||||
if st and st.user_id == user_id:
|
||||
canon = (st.canonical_markdown or "").strip()
|
||||
vc = count_story_versions_sync(session, str(st.id))
|
||||
if len(canon) <= max_chars and vc < max_ver:
|
||||
target_story_id = st.id
|
||||
existing_for_narrative = canon
|
||||
decision_source = "append_guardrail_short_oral"
|
||||
logger.info(
|
||||
"event=story_route_append_guardrail memoir_correlation_id={} "
|
||||
"chapter_category={} oral_len={} story_id={}",
|
||||
memoir_correlation_id or "",
|
||||
chapter_category,
|
||||
len(ut_norm),
|
||||
tid_g,
|
||||
)
|
||||
target_story_id, existing_for_narrative, decision_source = _resolve_append_target(
|
||||
session,
|
||||
route_decision=unit.decision,
|
||||
route_target_story_id=unit.target_story_id,
|
||||
user_id=user_id,
|
||||
chapter_category=chapter_category,
|
||||
oral_norm=(oral_unit or "").strip(),
|
||||
candidate_stories=candidate_stories,
|
||||
story_meta=story_meta,
|
||||
decision_source="batch_plan",
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
)
|
||||
|
||||
raw_gen = narrative_agent.generate_narrative(
|
||||
stage=chapter_category,
|
||||
slots=slot_snippets,
|
||||
new_content=new_content_input,
|
||||
existing_content=existing_for_narrative,
|
||||
sid, _ = _execute_narrative_unit(
|
||||
session,
|
||||
oral_text=oral_unit,
|
||||
evidence_text=evidence_text,
|
||||
evidence=evidence,
|
||||
evidence_top_k=evidence_top_k,
|
||||
chapter=chapter,
|
||||
chapter_category=chapter_category,
|
||||
slot_snippets=slot_snippets,
|
||||
user_id=user_id,
|
||||
user_profile=user_profile,
|
||||
birth_year=user_birth_year,
|
||||
user_birth_year=user_birth_year,
|
||||
llm=llm,
|
||||
narrative_agent=narrative_agent,
|
||||
target_story_id=target_story_id,
|
||||
existing_for_narrative=existing_for_narrative,
|
||||
decision_source=decision_source,
|
||||
route_decision=unit.decision,
|
||||
route_type="batch",
|
||||
segment_ids=list(unit.segment_ids),
|
||||
category_segments=category_segments,
|
||||
background_voice=background_voice,
|
||||
occupation=occupation,
|
||||
fallback_plain_oral=ut_norm,
|
||||
)
|
||||
json_invalid = False
|
||||
s0 = (raw_gen or "").strip()
|
||||
if s0.startswith("{") and "paragraphs" in s0:
|
||||
try:
|
||||
json.loads(s0)
|
||||
except json.JSONDecodeError:
|
||||
json_invalid = True
|
||||
|
||||
narrative_raw, fb_gate = _gate_narrative_fidelity(
|
||||
oral_unit,
|
||||
raw_gen,
|
||||
llm,
|
||||
existing_canonical=existing_for_narrative or None,
|
||||
)
|
||||
narrative_raw, fb_apply = _apply_narrative_fallbacks(
|
||||
narrative_raw,
|
||||
oral_unit,
|
||||
existing_for_narrative,
|
||||
chapter_category=chapter_category,
|
||||
)
|
||||
fallback_type = _merge_fallback_type(fb_gate, fb_apply)
|
||||
if json_invalid and fallback_type == "none":
|
||||
fallback_type = "json_invalid"
|
||||
|
||||
md = _coalesce_story_markdown(
|
||||
narrative_to_markdown(narrative_raw).strip(),
|
||||
oral_unit.strip(),
|
||||
existing_for_narrative or "",
|
||||
)
|
||||
md, inv_fb = _apply_narrative_body_safety(
|
||||
md,
|
||||
oral=oral_unit,
|
||||
existing_for_narrative=existing_for_narrative or "",
|
||||
evidence_text=evidence_text,
|
||||
chapter_category=chapter_category,
|
||||
)
|
||||
if inv_fb != "none":
|
||||
fallback_type = (
|
||||
inv_fb if fallback_type == "none" else f"{fallback_type}+{inv_fb}"
|
||||
)
|
||||
|
||||
if target_story_id:
|
||||
sid_s = str(target_story_id)
|
||||
ver = append_story_version_sync(session, sid_s, md)
|
||||
dlg = _dialogue_lineage_dict_for_segment_ids(
|
||||
category_segments, list(unit.segment_ids)
|
||||
)
|
||||
_persist_story_lineage_sync(
|
||||
session,
|
||||
story_id=sid_s,
|
||||
version=ver,
|
||||
evidence=evidence,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
top_k=evidence_top_k,
|
||||
dialogue_lineage=dlg,
|
||||
)
|
||||
dispatch_ids.add(sid_s)
|
||||
ensure_chapter_story_link_sync(
|
||||
session, chapter_id=str(chapter.id), story_id=sid_s
|
||||
)
|
||||
sid_log = target_story_id
|
||||
is_append = True
|
||||
else:
|
||||
story_title = _maybe_generate_title(
|
||||
narrative_agent,
|
||||
chapter_category=chapter_category,
|
||||
md=md,
|
||||
slot_snippets=slot_snippets,
|
||||
user_profile=user_profile,
|
||||
user_birth_year=user_birth_year,
|
||||
llm=llm,
|
||||
oral_scope=ut_norm,
|
||||
)
|
||||
st = create_story_with_version_sync(
|
||||
session,
|
||||
user_id=user_id,
|
||||
title=story_title,
|
||||
canonical_markdown=md,
|
||||
stage=chapter_category,
|
||||
)
|
||||
dispatch_ids.add(str(st.id))
|
||||
ensure_chapter_story_link_sync(
|
||||
session, chapter_id=str(chapter.id), story_id=str(st.id)
|
||||
)
|
||||
sid_log = st.id
|
||||
is_append = False
|
||||
if st.current_version_id:
|
||||
ver0 = session.get(StoryVersion, st.current_version_id)
|
||||
if ver0:
|
||||
dlg = _dialogue_lineage_dict_for_segment_ids(
|
||||
category_segments, list(unit.segment_ids)
|
||||
)
|
||||
_persist_story_lineage_sync(
|
||||
session,
|
||||
story_id=str(st.id),
|
||||
version=ver0,
|
||||
evidence=evidence,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
top_k=evidence_top_k,
|
||||
dialogue_lineage=dlg,
|
||||
)
|
||||
|
||||
elapsed = time.perf_counter() - t0
|
||||
logger.info(
|
||||
"event=story_generated memoir_correlation_id={} route_type=batch "
|
||||
"decision_source={} route_decision={} route_planned={} "
|
||||
"unit_segments={} used_evidence={} narrative_json_valid={} fidelity_passed={} "
|
||||
"fallback_type={} oral_len={} md_len={} chapter_category={} is_append={} "
|
||||
"story_id={} seconds={:.3f} oral_normalize_changed={}",
|
||||
memoir_correlation_id or "",
|
||||
decision_source,
|
||||
"append_story" if is_append else "new_story",
|
||||
unit.decision,
|
||||
len(unit.segment_ids),
|
||||
bool(evidence_text.strip()),
|
||||
_is_json_narrative(raw_gen),
|
||||
fb_gate == "none",
|
||||
fallback_type,
|
||||
len(ut_norm),
|
||||
len(md.strip()),
|
||||
chapter_category,
|
||||
is_append,
|
||||
sid_log,
|
||||
elapsed,
|
||||
ut_raw != ut_norm,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
)
|
||||
if sid:
|
||||
dispatch_ids.add(sid)
|
||||
return dispatch_ids
|
||||
|
||||
|
||||
@@ -864,6 +940,7 @@ def run_story_pipeline_for_category_batch(
|
||||
"""
|
||||
返回 (chapter, needs_cover_enqueue, story_ids_to_dispatch_after_commit)。
|
||||
"""
|
||||
pipeline_phase_timings: dict[str, float] = {}
|
||||
narrative_agent = NarrativeAgent()
|
||||
route_agent = StoryRouteAgent()
|
||||
dispatch_ids: set[str] = set()
|
||||
@@ -878,6 +955,7 @@ def run_story_pipeline_for_category_batch(
|
||||
top_k = int(settings.evidence_top_k_large_batch)
|
||||
emb = get_embedding_provider()
|
||||
embedding_available = emb.is_available()
|
||||
_t0 = time.perf_counter()
|
||||
try:
|
||||
evidence = retrieve_evidence_sync(
|
||||
session,
|
||||
@@ -895,6 +973,7 @@ def run_story_pipeline_for_category_batch(
|
||||
"timeline_hints": [],
|
||||
"relevant_stories": [],
|
||||
}
|
||||
pipeline_phase_timings["evidence"] = time.perf_counter() - _t0
|
||||
|
||||
logger.info(
|
||||
"memoir_evidence_retrieved user_id={} chunks={} facts={} summaries={} stories={} vector_ok={}",
|
||||
@@ -907,7 +986,9 @@ def run_story_pipeline_for_category_batch(
|
||||
)
|
||||
|
||||
evidence_text = format_evidence_chunks_for_prompt(evidence)
|
||||
_t0 = time.perf_counter()
|
||||
oral_for_memoir = normalize_oral_for_memoir(combined_text, llm=llm)
|
||||
pipeline_phase_timings["oral_normalize"] = time.perf_counter() - _t0
|
||||
ct_raw = (combined_text or "").strip()
|
||||
om_norm = (oral_for_memoir or "").strip()
|
||||
if ct_raw != om_norm:
|
||||
@@ -959,6 +1040,7 @@ def run_story_pipeline_for_category_batch(
|
||||
|
||||
calculated_order_index = STAGE_TO_ORDER.get(chapter_category, 999)
|
||||
|
||||
_t0 = time.perf_counter()
|
||||
use_batch_plan = (
|
||||
llm
|
||||
and len(category_segments) >= 2
|
||||
@@ -976,6 +1058,7 @@ def run_story_pipeline_for_category_batch(
|
||||
valid_story_ids=valid_ids,
|
||||
story_meta=story_meta,
|
||||
)
|
||||
pipeline_phase_timings["route"] = time.perf_counter() - _t0
|
||||
|
||||
chapter = _ensure_chapter_record(
|
||||
session,
|
||||
@@ -986,6 +1069,7 @@ def run_story_pipeline_for_category_batch(
|
||||
calculated_order_index=calculated_order_index,
|
||||
)
|
||||
|
||||
_t0 = time.perf_counter()
|
||||
if plan is not None:
|
||||
dispatch_ids = _run_batch_plan_writes(
|
||||
session,
|
||||
@@ -1019,203 +1103,72 @@ def run_story_pipeline_for_category_batch(
|
||||
story_meta=story_meta,
|
||||
)
|
||||
|
||||
t0 = time.perf_counter()
|
||||
target_story_id: str | None = None
|
||||
existing_for_narrative = ""
|
||||
decision_source = "fallback_no_llm" if not llm else "single_decide"
|
||||
max_chars = int(settings.story_append_max_canonical_chars)
|
||||
max_ver = int(settings.story_append_max_versions)
|
||||
if route.decision == "append_story" and route.target_story_id:
|
||||
st = session.get(Story, route.target_story_id)
|
||||
if st and st.user_id == user_id:
|
||||
canon = (st.canonical_markdown or "").strip()
|
||||
vc = count_story_versions_sync(session, str(st.id))
|
||||
if len(canon) > max_chars or vc >= max_ver:
|
||||
logger.info(
|
||||
"event=append_overflow_to_new story_id={} canonical_chars={} "
|
||||
"versions={} decision_source=single_decide",
|
||||
str(st.id),
|
||||
len(canon),
|
||||
vc,
|
||||
)
|
||||
target_story_id = None
|
||||
existing_for_narrative = ""
|
||||
decision_source = "forced_new_due_to_append_limit"
|
||||
else:
|
||||
target_story_id = st.id
|
||||
existing_for_narrative = canon
|
||||
elif (
|
||||
route.decision == "new_story"
|
||||
and chapter_category in APPEND_FIRST_CHAPTER_CATEGORIES
|
||||
and candidates
|
||||
and len(om_norm)
|
||||
<= int(settings.memoir_story_route_append_guardrail_oral_chars)
|
||||
):
|
||||
tid_g = default_append_target_story_id(candidates, story_meta, settings)
|
||||
if tid_g:
|
||||
st = session.get(Story, tid_g)
|
||||
if st and st.user_id == user_id:
|
||||
canon = (st.canonical_markdown or "").strip()
|
||||
vc = count_story_versions_sync(session, str(st.id))
|
||||
if len(canon) <= max_chars and vc < max_ver:
|
||||
target_story_id = st.id
|
||||
existing_for_narrative = canon
|
||||
decision_source = "append_guardrail_short_oral"
|
||||
logger.info(
|
||||
"event=story_route_append_guardrail memoir_correlation_id={} "
|
||||
"chapter_category={} oral_len={} story_id={} route_type=single",
|
||||
memoir_correlation_id or "",
|
||||
chapter_category,
|
||||
len(om_norm),
|
||||
tid_g,
|
||||
)
|
||||
target_story_id, existing_for_narrative, decision_source = _resolve_append_target(
|
||||
session,
|
||||
route_decision=route.decision,
|
||||
route_target_story_id=route.target_story_id,
|
||||
user_id=user_id,
|
||||
chapter_category=chapter_category,
|
||||
oral_norm=om_norm,
|
||||
candidate_stories=candidates,
|
||||
story_meta=story_meta,
|
||||
decision_source=decision_source,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
)
|
||||
|
||||
raw_gen = narrative_agent.generate_narrative(
|
||||
stage=chapter_category,
|
||||
slots=slot_snippets,
|
||||
new_content=new_content_input,
|
||||
existing_content=existing_for_narrative,
|
||||
sid, _ = _execute_narrative_unit(
|
||||
session,
|
||||
oral_text=oral_for_memoir,
|
||||
evidence_text=evidence_text,
|
||||
evidence=evidence,
|
||||
evidence_top_k=top_k,
|
||||
chapter=chapter,
|
||||
chapter_category=chapter_category,
|
||||
slot_snippets=slot_snippets,
|
||||
user_id=user_id,
|
||||
user_profile=user_profile,
|
||||
birth_year=user_birth_year,
|
||||
user_birth_year=user_birth_year,
|
||||
llm=llm,
|
||||
narrative_agent=narrative_agent,
|
||||
target_story_id=target_story_id,
|
||||
existing_for_narrative=existing_for_narrative,
|
||||
decision_source=decision_source,
|
||||
route_decision=route.decision,
|
||||
route_type="single",
|
||||
segment_ids=[str(s.id) for s in category_segments],
|
||||
category_segments=category_segments,
|
||||
background_voice=background_voice,
|
||||
occupation=occupation,
|
||||
fallback_plain_oral=om_norm,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
)
|
||||
json_invalid = False
|
||||
s0 = (raw_gen or "").strip()
|
||||
if s0.startswith("{") and "paragraphs" in s0:
|
||||
try:
|
||||
json.loads(s0)
|
||||
except json.JSONDecodeError:
|
||||
json_invalid = True
|
||||
if sid:
|
||||
dispatch_ids.add(sid)
|
||||
|
||||
narrative_raw, fb_gate = _gate_narrative_fidelity(
|
||||
oral_for_memoir,
|
||||
raw_gen,
|
||||
llm,
|
||||
existing_canonical=existing_for_narrative or None,
|
||||
)
|
||||
|
||||
narrative_raw, fb_apply = _apply_narrative_fallbacks(
|
||||
narrative_raw,
|
||||
oral_for_memoir,
|
||||
existing_for_narrative,
|
||||
chapter_category=chapter_category,
|
||||
)
|
||||
fallback_type = _merge_fallback_type(fb_gate, fb_apply)
|
||||
if json_invalid and fallback_type == "none":
|
||||
fallback_type = "json_invalid"
|
||||
|
||||
md = _coalesce_story_markdown(
|
||||
narrative_to_markdown(narrative_raw).strip(),
|
||||
oral_for_memoir.strip(),
|
||||
existing_for_narrative or "",
|
||||
)
|
||||
md, inv_fb = _apply_narrative_body_safety(
|
||||
md,
|
||||
oral=oral_for_memoir,
|
||||
existing_for_narrative=existing_for_narrative or "",
|
||||
evidence_text=evidence_text,
|
||||
chapter_category=chapter_category,
|
||||
)
|
||||
if inv_fb != "none":
|
||||
fallback_type = (
|
||||
inv_fb if fallback_type == "none" else f"{fallback_type}+{inv_fb}"
|
||||
)
|
||||
|
||||
do_append = target_story_id is not None
|
||||
|
||||
dlg_single = _dialogue_lineage_dict_for_segment_ids(
|
||||
category_segments,
|
||||
[str(s.id) for s in category_segments],
|
||||
)
|
||||
|
||||
if do_append:
|
||||
sid_s = str(target_story_id)
|
||||
ver = append_story_version_sync(session, sid_s, md)
|
||||
_persist_story_lineage_sync(
|
||||
session,
|
||||
story_id=sid_s,
|
||||
version=ver,
|
||||
evidence=evidence,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
top_k=top_k,
|
||||
dialogue_lineage=dlg_single,
|
||||
)
|
||||
dispatch_ids.add(sid_s)
|
||||
ensure_chapter_story_link_sync(
|
||||
session, chapter_id=str(chapter.id), story_id=sid_s
|
||||
)
|
||||
sid_log = target_story_id
|
||||
is_append = True
|
||||
else:
|
||||
story_title = _maybe_generate_title(
|
||||
narrative_agent,
|
||||
chapter_category=chapter_category,
|
||||
md=md,
|
||||
slot_snippets=slot_snippets,
|
||||
user_profile=user_profile,
|
||||
user_birth_year=user_birth_year,
|
||||
llm=llm,
|
||||
oral_scope=om_norm,
|
||||
)
|
||||
st = create_story_with_version_sync(
|
||||
session,
|
||||
user_id=user_id,
|
||||
title=story_title,
|
||||
canonical_markdown=md,
|
||||
stage=chapter_category,
|
||||
)
|
||||
dispatch_ids.add(str(st.id))
|
||||
ensure_chapter_story_link_sync(
|
||||
session, chapter_id=str(chapter.id), story_id=str(st.id)
|
||||
)
|
||||
sid_log = st.id
|
||||
is_append = False
|
||||
if st.current_version_id:
|
||||
ver0 = session.get(StoryVersion, st.current_version_id)
|
||||
if ver0:
|
||||
_persist_story_lineage_sync(
|
||||
session,
|
||||
story_id=str(st.id),
|
||||
version=ver0,
|
||||
evidence=evidence,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
top_k=top_k,
|
||||
dialogue_lineage=dlg_single,
|
||||
)
|
||||
|
||||
elapsed = time.perf_counter() - t0
|
||||
logger.info(
|
||||
"event=story_generated memoir_correlation_id={} route_type=single "
|
||||
"decision_source={} route_decision={} "
|
||||
"unit_segments={} used_evidence={} narrative_json_valid={} fidelity_passed={} "
|
||||
"fallback_type={} oral_len={} md_len={} chapter_category={} is_append={} "
|
||||
"story_id={} seconds={:.3f} oral_normalize_changed={}",
|
||||
memoir_correlation_id or "",
|
||||
decision_source,
|
||||
route.decision,
|
||||
len(category_segments),
|
||||
bool(evidence_text.strip()),
|
||||
_is_json_narrative(raw_gen),
|
||||
fb_gate == "none",
|
||||
fallback_type,
|
||||
len(om_norm),
|
||||
len(md.strip()),
|
||||
chapter_category,
|
||||
is_append,
|
||||
sid_log,
|
||||
elapsed,
|
||||
ct_raw != om_norm,
|
||||
)
|
||||
pipeline_phase_timings["narrative_writes"] = time.perf_counter() - _t0
|
||||
|
||||
_t0 = time.perf_counter()
|
||||
reorder_chapter_story_links_by_life_order_sync(session, str(chapter.id))
|
||||
mark_chapter_dirty_sync(session, str(chapter.id))
|
||||
session.flush()
|
||||
refresh_chapter_evidence_snapshot_with_retry_sync(session, str(chapter.id))
|
||||
pipeline_phase_timings["finalize"] = time.perf_counter() - _t0
|
||||
|
||||
image_settings = MemoirImageSettings.from_env()
|
||||
needs_cover = image_settings.enabled and chapter_needs_cover_enqueue(chapter)
|
||||
|
||||
timing_parts = " ".join(
|
||||
f"{k}_seconds={v:.3f}" for k, v in pipeline_phase_timings.items()
|
||||
)
|
||||
logger.info(
|
||||
"event=memoir_pipeline_phases memoir_correlation_id={} user_id={} "
|
||||
"chapter_category={} segment_count={} route_type={} {}",
|
||||
memoir_correlation_id or "",
|
||||
user_id,
|
||||
chapter_category,
|
||||
len(category_segments),
|
||||
"batch" if plan is not None else "single",
|
||||
timing_parts,
|
||||
)
|
||||
|
||||
return chapter, needs_cover, dispatch_ids
|
||||
|
||||
Reference in New Issue
Block a user