Merge branch 'eval/elapsed-time-memoir-batch-chunk' into development
This commit is contained in:
@@ -10,9 +10,10 @@ import json
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.agents.memoir.narrative_agent import NarrativeAgent
|
||||
@@ -405,11 +406,13 @@ def _gate_narrative_fidelity(
|
||||
llm: Any,
|
||||
*,
|
||||
existing_canonical: str | None = None,
|
||||
fidelity_llm: Any | None = None,
|
||||
) -> tuple[str, str]:
|
||||
"""返回 (文本, fallback 原因);忠实度不通过时第二项为 fidelity_failed。"""
|
||||
from app.agents.memoir.fidelity_check_agent import FidelityCheckAgent
|
||||
|
||||
if not settings.memoir_fidelity_check_enabled or not llm:
|
||||
check_llm = fidelity_llm if fidelity_llm is not None else llm
|
||||
if not settings.memoir_fidelity_check_enabled or not check_llm:
|
||||
return narrative_raw, "none"
|
||||
agent = FidelityCheckAgent()
|
||||
ex = (existing_canonical or "").strip() or None
|
||||
@@ -417,7 +420,7 @@ def _gate_narrative_fidelity(
|
||||
if agent.passes(
|
||||
oral_text=oral_text,
|
||||
narrative_json=narrative_raw,
|
||||
llm=llm,
|
||||
llm=check_llm,
|
||||
existing_canonical_markdown=ex,
|
||||
is_append=is_append,
|
||||
):
|
||||
@@ -564,14 +567,23 @@ def _merge_fallback_type(gate_ft: str, apply_ft: str) -> str:
|
||||
def _story_meta_for_route(
|
||||
session: Session, candidates: list
|
||||
) -> dict[str, dict[str, int]]:
|
||||
meta: dict[str, dict[str, int]] = {}
|
||||
for s in candidates:
|
||||
sid = str(s.id)
|
||||
meta[sid] = {
|
||||
if not candidates:
|
||||
return {}
|
||||
sids = [str(s.id) for s in candidates]
|
||||
stmt = (
|
||||
select(StoryVersion.story_id, func.count(StoryVersion.id))
|
||||
.where(StoryVersion.story_id.in_(sids))
|
||||
.group_by(StoryVersion.story_id)
|
||||
)
|
||||
rows = session.execute(stmt).all()
|
||||
counts: dict[str, int] = {str(r[0]): int(r[1] or 0) for r in rows}
|
||||
return {
|
||||
str(s.id): {
|
||||
"char_count": len((s.canonical_markdown or "").strip()),
|
||||
"version_count": count_story_versions_sync(session, sid),
|
||||
"version_count": counts.get(str(s.id), 0),
|
||||
}
|
||||
return meta
|
||||
for s in candidates
|
||||
}
|
||||
|
||||
|
||||
def _ensure_chapter_record(
|
||||
@@ -615,7 +627,6 @@ def _ensure_chapter_record(
|
||||
)
|
||||
chapter.is_new = True
|
||||
session.flush()
|
||||
refresh_chapter_evidence_snapshot_with_retry_sync(session, str(chapter.id))
|
||||
return chapter
|
||||
|
||||
|
||||
@@ -710,6 +721,7 @@ def _execute_narrative_unit(
|
||||
background_voice: str = "default",
|
||||
occupation: str = "",
|
||||
memoir_correlation_id: str | None = None,
|
||||
fidelity_llm: Any | None = None,
|
||||
) -> tuple[str | None, bool]:
|
||||
"""
|
||||
Unified narrative unit executor: generate narrative, apply fidelity/safety,
|
||||
@@ -744,6 +756,7 @@ def _execute_narrative_unit(
|
||||
raw_gen,
|
||||
llm,
|
||||
existing_canonical=existing_for_narrative or None,
|
||||
fidelity_llm=fidelity_llm,
|
||||
)
|
||||
narrative_raw, fb_apply = _apply_narrative_fallbacks(
|
||||
narrative_raw,
|
||||
@@ -792,16 +805,7 @@ def _execute_narrative_unit(
|
||||
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,
|
||||
)
|
||||
story_title = _placeholder_title(chapter_category)
|
||||
st = create_story_with_version_sync(
|
||||
session,
|
||||
user_id=user_id,
|
||||
@@ -809,6 +813,21 @@ def _execute_narrative_unit(
|
||||
canonical_markdown=md,
|
||||
stage=chapter_category,
|
||||
)
|
||||
try:
|
||||
from app.tasks.story_title_tasks import generate_story_title_after_create
|
||||
|
||||
generate_story_title_after_create.delay(
|
||||
str(st.id),
|
||||
chapter_category,
|
||||
oral_norm,
|
||||
user_id,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"event=story_title_enqueue_failed story_id={} err={}",
|
||||
st.id,
|
||||
exc,
|
||||
)
|
||||
ensure_chapter_story_link_sync(
|
||||
session, chapter_id=str(chapter.id), story_id=str(st.id)
|
||||
)
|
||||
@@ -874,6 +893,7 @@ def _run_batch_plan_writes(
|
||||
background_voice: str = "default",
|
||||
occupation: str = "",
|
||||
memoir_correlation_id: str | None = None,
|
||||
fidelity_llm: Any | None = None,
|
||||
) -> set[str]:
|
||||
dispatch_ids: set[str] = set()
|
||||
for unit in plan.units:
|
||||
@@ -919,6 +939,7 @@ def _run_batch_plan_writes(
|
||||
background_voice=background_voice,
|
||||
occupation=occupation,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
fidelity_llm=fidelity_llm,
|
||||
)
|
||||
if sid:
|
||||
dispatch_ids.add(sid)
|
||||
@@ -938,6 +959,7 @@ def run_story_pipeline_for_category_batch(
|
||||
background_voice: str = "default",
|
||||
occupation: str = "",
|
||||
memoir_correlation_id: str | None = None,
|
||||
llm_fast: Any | None = None,
|
||||
) -> tuple[Chapter | None, bool, set[str]]:
|
||||
"""
|
||||
返回 (chapter, needs_cover_enqueue, story_ids_to_dispatch_after_commit)。
|
||||
@@ -946,6 +968,8 @@ def run_story_pipeline_for_category_batch(
|
||||
narrative_agent = NarrativeAgent()
|
||||
route_agent = StoryRouteAgent()
|
||||
dispatch_ids: set[str] = set()
|
||||
llm_route = llm_fast if llm_fast is not None else llm
|
||||
llm_fidelity = llm_fast if llm_fast is not None else llm
|
||||
|
||||
segment_texts = [seg.user_input_text or "" for seg in category_segments]
|
||||
combined_text = "\n\n".join(segment_texts)
|
||||
@@ -957,25 +981,40 @@ 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,
|
||||
user_id,
|
||||
combined_text,
|
||||
top_k=top_k,
|
||||
embedding_provider=emb,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Evidence 检索跳过: {}", e)
|
||||
evidence = {
|
||||
"relevant_chunks": [],
|
||||
"relevant_summaries": [],
|
||||
"relevant_facts": [],
|
||||
"timeline_hints": [],
|
||||
"relevant_stories": [],
|
||||
}
|
||||
pipeline_phase_timings["evidence"] = time.perf_counter() - _t0
|
||||
|
||||
def _oral_job() -> tuple[str, float]:
|
||||
t_oral = time.perf_counter()
|
||||
out = normalize_oral_for_memoir(combined_text, llm=llm)
|
||||
return out, time.perf_counter() - t_oral
|
||||
|
||||
_t_parallel = time.perf_counter()
|
||||
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||
oral_future = pool.submit(_oral_job)
|
||||
_t_ev = time.perf_counter()
|
||||
try:
|
||||
evidence = retrieve_evidence_sync(
|
||||
session,
|
||||
user_id,
|
||||
combined_text,
|
||||
top_k=top_k,
|
||||
embedding_provider=emb,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("Evidence 检索跳过: {}", e)
|
||||
evidence = {
|
||||
"relevant_chunks": [],
|
||||
"relevant_summaries": [],
|
||||
"relevant_facts": [],
|
||||
"timeline_hints": [],
|
||||
"relevant_stories": [],
|
||||
}
|
||||
ev_elapsed = time.perf_counter() - _t_ev
|
||||
oral_for_memoir, oral_elapsed = oral_future.result()
|
||||
pipeline_phase_timings["evidence"] = ev_elapsed
|
||||
pipeline_phase_timings["oral_normalize"] = oral_elapsed
|
||||
pipeline_phase_timings["evidence_oral_parallel_wall"] = (
|
||||
time.perf_counter() - _t_parallel
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"memoir_evidence_retrieved user_id={} chunks={} facts={} summaries={} stories={} vector_ok={}",
|
||||
@@ -988,9 +1027,6 @@ 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:
|
||||
@@ -999,7 +1035,6 @@ def run_story_pipeline_for_category_batch(
|
||||
len(ct_raw),
|
||||
len(om_norm),
|
||||
)
|
||||
new_content_input = format_narrative_user_content(oral_for_memoir, evidence_text)
|
||||
logger.info(
|
||||
"event=memoir_story_pipeline_start memoir_correlation_id={} user_id={} "
|
||||
"chapter_category={} segment_count={}",
|
||||
@@ -1044,7 +1079,7 @@ def run_story_pipeline_for_category_batch(
|
||||
|
||||
_t0 = time.perf_counter()
|
||||
use_batch_plan = (
|
||||
llm
|
||||
llm_route
|
||||
and len(category_segments) >= 2
|
||||
and len(category_segments) <= PLAN_BATCH_MAX_SEGMENTS
|
||||
)
|
||||
@@ -1056,7 +1091,7 @@ def run_story_pipeline_for_category_batch(
|
||||
chapter_title=title,
|
||||
segments=segs,
|
||||
candidate_stories=candidates,
|
||||
llm=llm,
|
||||
llm=llm_route,
|
||||
valid_story_ids=valid_ids,
|
||||
story_meta=story_meta,
|
||||
)
|
||||
@@ -1093,6 +1128,7 @@ def run_story_pipeline_for_category_batch(
|
||||
background_voice=background_voice,
|
||||
occupation=occupation,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
fidelity_llm=llm_fidelity,
|
||||
)
|
||||
else:
|
||||
route = route_agent.decide(
|
||||
@@ -1100,7 +1136,7 @@ def run_story_pipeline_for_category_batch(
|
||||
chapter_title=title,
|
||||
batch_transcript=route_transcript,
|
||||
candidate_stories=candidates,
|
||||
llm=llm,
|
||||
llm=llm_route,
|
||||
valid_story_ids=valid_ids,
|
||||
story_meta=story_meta,
|
||||
)
|
||||
@@ -1145,6 +1181,7 @@ def run_story_pipeline_for_category_batch(
|
||||
background_voice=background_voice,
|
||||
occupation=occupation,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
fidelity_llm=llm_fidelity,
|
||||
)
|
||||
if sid:
|
||||
dispatch_ids.add(sid)
|
||||
|
||||
Reference in New Issue
Block a user