refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)

配置 SSOT(TOML + .env)
统一错误契约
Auth 与事务边界
Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client
可观测性(OpenTelemetry + LGTM)
This commit is contained in:
Sully
2026-05-22 13:44:50 +08:00
committed by GitHub
parent f09ae248f9
commit 53e0065e3e
298 changed files with 15247 additions and 4344 deletions

View File

@@ -48,6 +48,9 @@ from app.core.config import settings
from app.core.llm_gateway import LlmGateway, LlmUseCase
from app.core.logging import get_logger
from app.features.conversation.input_normalize import normalize_chat_input_for_agent
from app.core.runtime_constants import agent_log_defaults
from app.features.conversation.constants import chat
from app.features.story.constants import story
logger = get_logger(__name__)
@@ -171,8 +174,8 @@ class InterviewAgent:
if normalized_user_message is not None:
return (normalized_user_message or "").strip()
llm_n = None
if settings.chat_input_normalize_enabled and (
(settings.chat_input_normalize_mode or "").strip().lower() == "llm"
if chat.input_normalize_enabled and (
(chat.input_normalize_mode or "").strip().lower() == "llm"
):
llm_n = self.llm
return normalize_chat_input_for_agent(user_message or "", llm=llm_n)
@@ -218,16 +221,16 @@ class InterviewAgent:
du = self._detect_user_stage(text_for_model)
hw = await get_history_with_window(
conversation_id,
max_pairs=settings.chat_history_max_pairs,
max_chars=settings.chat_history_max_chars,
max_pairs=chat.history_max_pairs,
max_chars=chat.history_max_chars,
)
recent_questions = extract_recent_questions(hw.window)
conversation_turn_total = hw.turn_total
all_stages_coverage = narrative_state.all_stages_coverage()
persona = normalize_interview_persona(settings.chat_interview_persona)
max_segments = int(settings.chat_interview_max_segments)
max_tokens = int(settings.chat_interview_max_tokens)
max_chars = int(settings.chat_interview_max_chars_per_segment)
persona = normalize_interview_persona(chat.interview_persona)
max_segments = int(chat.interview_max_segments)
max_tokens = int(chat.interview_max_tokens)
max_chars = int(chat.interview_max_chars_per_segment)
turn_plan = plan_interview_turn(
current_stage=memoir_state.current_stage,
@@ -246,7 +249,7 @@ class InterviewAgent:
reply_planner_raw = ""
baseline_mode = turn_plan.mode
baseline_primary_focus = turn_plan.primary_focus
if settings.chat_reply_planner_llm_enabled:
if chat.reply_planner_llm_enabled:
rq_preview = (
"\n".join(recent_questions[-4:])
if recent_questions
@@ -258,8 +261,8 @@ class InterviewAgent:
text_for_model=text_for_model,
memory_evidence_text=(memory_planner_text or memory_evidence_text)
or "",
max_tokens=int(settings.chat_reply_planner_max_tokens),
temperature=float(settings.chat_reply_planner_temperature),
max_tokens=int(chat.reply_planner_max_tokens),
temperature=float(chat.reply_planner_temperature),
scene_cues_for_planner=scene_cues_for_planner or [],
recent_questions_preview=rq_preview,
)
@@ -310,12 +313,12 @@ class InterviewAgent:
"InterviewAgent.generate_response.prompt",
format_history_string(
messages,
omit_system_body=settings.agent_log_omit_system_message_body,
omit_system_body=agent_log_defaults.omit_system_message_body,
),
)
chat_llm = self.llm.bind(
max_tokens=max_tokens,
temperature=float(settings.chat_interview_temperature),
temperature=float(chat.interview_temperature),
)
prompt_chars = _message_contents_char_count(messages)
llm_t0 = time.perf_counter()
@@ -377,7 +380,7 @@ class InterviewAgent:
"InterviewAgent.generate_response.retry_prompt",
format_history_string(
retry_messages,
omit_system_body=settings.agent_log_omit_system_message_body,
omit_system_body=agent_log_defaults.omit_system_message_body,
),
)
llm_t1 = time.perf_counter()
@@ -445,7 +448,7 @@ class InterviewAgent:
"duplicate_question_guard_llm_retry": retry_used,
"autobiographical_boundary_guard_triggered": auto_bio,
"reply_planner_llm_used": bool(
settings.chat_reply_planner_llm_enabled
chat.reply_planner_llm_enabled
and (reply_planner_raw or "").strip()
),
"reply_planner_raw_preview": (reply_planner_raw or "")[:800],
@@ -483,7 +486,7 @@ class InterviewAgent:
)
slot_table = SLOT_NAME_MAP_EN if language == "en" else SLOT_NAME_MAP
empty_slots_readable = [slot_table.get(s, s) for s in empty_slots]
persona = normalize_interview_persona(settings.chat_interview_persona)
persona = normalize_interview_persona(chat.interview_persona)
prompt = get_opening_prompt(
current_stage=memoir_state.current_stage,
empty_slots_readable=empty_slots_readable,
@@ -497,8 +500,8 @@ class InterviewAgent:
)
hw = await get_history_with_window(
conversation_id,
max_pairs=settings.chat_history_max_pairs,
max_chars=settings.chat_history_max_chars,
max_pairs=chat.history_max_pairs,
max_chars=chat.history_max_chars,
)
messages: List[Any] = [SystemMessage(content=prompt)]
messages.extend(hw.window)
@@ -520,12 +523,12 @@ class InterviewAgent:
"InterviewAgent.opening.prompt",
format_history_string(
messages,
omit_system_body=settings.agent_log_omit_system_message_body,
omit_system_body=agent_log_defaults.omit_system_message_body,
),
)
opening_llm = self.llm.bind(
max_tokens=settings.chat_opening_max_tokens,
temperature=float(settings.chat_interview_temperature),
max_tokens=chat.opening_max_tokens,
temperature=float(chat.interview_temperature),
)
prompt_chars = _message_contents_char_count(messages)
llm_t0 = time.perf_counter()
@@ -564,7 +567,7 @@ class InterviewAgent:
raw_list = segments_from_llm_response(response_text, max_segments=2)
if not raw_list:
raw_list = [response_text.strip()]
max_chars = int(settings.chat_interview_max_chars_per_segment)
max_chars = int(chat.interview_max_chars_per_segment)
out = truncate_chat_segments(
raw_list,
max_segments=2,
@@ -612,7 +615,7 @@ class InterviewAgent:
)
slot_table = SLOT_NAME_MAP_EN if language == "en" else SLOT_NAME_MAP
empty_slots_readable = [slot_table.get(s, s) for s in empty_slots]
persona = normalize_interview_persona(settings.chat_interview_persona)
persona = normalize_interview_persona(chat.interview_persona)
prompt = get_re_greeting_prompt(
current_stage=memoir_state.current_stage,
empty_slots_readable=empty_slots_readable,
@@ -627,8 +630,8 @@ class InterviewAgent:
)
hw = await get_history_with_window(
conversation_id,
max_pairs=settings.chat_history_max_pairs,
max_chars=settings.chat_history_max_chars,
max_pairs=chat.history_max_pairs,
max_chars=chat.history_max_chars,
)
messages: List[Any] = [SystemMessage(content=prompt)]
messages.extend(hw.window)
@@ -647,12 +650,12 @@ class InterviewAgent:
"InterviewAgent.re_greeting.prompt",
format_history_string(
messages,
omit_system_body=settings.agent_log_omit_system_message_body,
omit_system_body=agent_log_defaults.omit_system_message_body,
),
)
re_greet_llm = self.llm.bind(
max_tokens=settings.chat_opening_max_tokens,
temperature=float(settings.chat_interview_temperature),
max_tokens=chat.opening_max_tokens,
temperature=float(chat.interview_temperature),
)
llm_t0 = time.perf_counter()
with agent_span(
@@ -691,7 +694,7 @@ class InterviewAgent:
raw_list = segments_from_llm_response(response_text, max_segments=2)
if not raw_list:
raw_list = [response_text.strip()]
max_chars = int(settings.chat_interview_max_chars_per_segment)
max_chars = int(chat.interview_max_chars_per_segment)
out = truncate_chat_segments(
raw_list,
max_segments=2,

View File

@@ -35,6 +35,9 @@ from app.features.memoir.state_service import (
switch_stage,
)
from app.features.memory.prompt_adapter import MemoryPromptAdapter
from app.features.conversation.constants import chat
from app.features.memory.constants import memory
from app.features.story.constants import story
def _llm_for_chat_input_normalize():
@@ -80,7 +83,7 @@ async def _fetch_interview_memory_bundle(
)
from app.features.memory.service import MemoryService
if not settings.chat_memory_retrieval_enabled:
if not chat.memory_retrieval_enabled:
logger.debug(
"event=chat_memory_retrieval_skip reason=disabled user_id={}", user_id
)
@@ -94,7 +97,7 @@ async def _fetch_interview_memory_bundle(
try:
emb = get_embedding_provider_fn()
ms = MemoryService(db, embedding_provider=emb)
top_k = settings.chat_memory_top_k
top_k = chat.memory_top_k
bundle = await ms.retrieve(user_id, msg, top_k=top_k)
bd = bundle.model_dump()
trace = chat_memory_retrieval_trace_from_bundle(
@@ -164,17 +167,17 @@ class ChatOrchestrator:
if missing:
hw_profile = await get_history_with_window(
conversation_id,
max_pairs=settings.chat_history_max_pairs,
max_chars=settings.chat_history_max_chars,
max_pairs=chat.history_max_pairs,
max_chars=chat.history_max_chars,
)
profile_turn_total = hw_profile.turn_total
if profile_turn_total >= settings.chat_profile_max_turns:
if profile_turn_total >= chat.profile_max_turns:
logger.info(
"event=chat_profile_cap_skip conversation_id={} "
"turn_total={} cap={} missing_fields={}",
conversation_id,
profile_turn_total,
settings.chat_profile_max_turns,
chat.profile_max_turns,
missing,
)
else:
@@ -269,8 +272,8 @@ class ChatOrchestrator:
len(user_message or ""),
)
llm_n = None
if settings.chat_input_normalize_enabled and (
(settings.chat_input_normalize_mode or "").strip().lower() == "llm"
if chat.input_normalize_enabled and (
(chat.input_normalize_mode or "").strip().lower() == "llm"
):
llm_n = _llm_for_chat_input_normalize()
normalized_user_message = normalize_chat_input_for_agent(
@@ -290,8 +293,10 @@ class ChatOrchestrator:
state = await switch_stage(user_id, detected, db)
if conversation and conversation.conversation_stage != state.current_stage:
conversation.conversation_stage = state.current_stage
await db.commit()
from app.core.db import transactional
async with transactional(db):
conversation.conversation_stage = state.current_stage
from app.agents.chat.background_voice import infer_background_voice
from app.agents.chat.prompts_profile import format_user_profile_context

View File

@@ -7,6 +7,8 @@ from __future__ import annotations
from typing import Final
from app.features.conversation.constants import chat
# Brand / interviewer name — keep aligned with frontend i18n `conversation.agentName`,
# OpenAPI title, README, and project metadata. zh = 「岁月知己」en = Life Echo.
AGENT_NAME_ZH: Final[str] = "岁月知己"
@@ -18,7 +20,7 @@ def agent_name(language: str = "zh") -> str:
return AGENT_NAME_EN if (language or "zh").strip().lower() == "en" else AGENT_NAME_ZH
# 与 settings.chat_interview_persona 及文档保持一致
# 与 chat.interview_persona 及文档保持一致
VALID_INTERVIEW_PERSONAS: Final[frozenset[str]] = frozenset(
{"default", "warm_listener", "curious_guide"}
)

View File

@@ -26,6 +26,9 @@ from app.core.llm_call import allm_json_call
from app.core.llm_gateway import LlmGateway, LlmUseCase
from app.core.logging import get_logger
from app.ports.llm import LLMProvider
from app.core.runtime_constants import agent_log_defaults
from app.features.conversation.constants import chat
from app.features.story.constants import story
logger = get_logger(__name__)
@@ -207,8 +210,8 @@ class ProfileAgent:
if conversation_id:
hw = await get_history_with_window(
conversation_id,
max_pairs=settings.chat_history_max_pairs,
max_chars=settings.chat_history_max_chars,
max_pairs=chat.history_max_pairs,
max_chars=chat.history_max_chars,
)
recent = hw.window[-4:] if len(hw.window) > 4 else hw.window
parts = []
@@ -232,7 +235,7 @@ class ProfileAgent:
ProfileExtractionOutput,
use_case=LlmUseCase(
"ProfileAgent.extract_profile_from_message",
max_tokens=settings.chat_profile_extract_max_tokens,
max_tokens=chat.profile_extract_max_tokens,
),
fallback_factory=lambda: ProfileExtractionOutput(),
)
@@ -285,8 +288,8 @@ class ProfileAgent:
)
hw = await get_history_with_window(
conversation_id,
max_pairs=settings.chat_history_max_pairs,
max_chars=settings.chat_history_max_chars,
max_pairs=chat.history_max_pairs,
max_chars=chat.history_max_chars,
)
messages: List[Any] = [SystemMessage(content=prompt)]
messages.extend(hw.window)
@@ -296,7 +299,7 @@ class ProfileAgent:
"ProfileAgent.followup.prompt",
format_history_string(
messages,
omit_system_body=settings.agent_log_omit_system_message_body,
omit_system_body=agent_log_defaults.omit_system_message_body,
),
)
prompt_chars = _message_contents_char_count(messages)
@@ -309,14 +312,14 @@ class ProfileAgent:
)
response_text = await self._invoke_chat(
messages,
max_tokens=settings.chat_profile_followup_max_tokens,
max_tokens=chat.profile_followup_max_tokens,
conversation_id=conversation_id,
agent_name="ProfileAgent.generate_profile_followup",
)
segments = await self._segments_from_response(
response_text,
max_segments=3,
max_chars_per_segment=settings.chat_interview_max_chars_per_segment,
max_chars_per_segment=chat.interview_max_chars_per_segment,
fallback=_profile_followup_fallback(language),
)
log_agent_summary(
@@ -344,8 +347,8 @@ class ProfileAgent:
)
hw = await get_history_with_window(
conversation_id,
max_pairs=settings.chat_history_max_pairs,
max_chars=settings.chat_history_max_chars,
max_pairs=chat.history_max_pairs,
max_chars=chat.history_max_chars,
)
messages: List[Any] = [SystemMessage(content=prompt)]
messages.extend(hw.window)
@@ -367,7 +370,7 @@ class ProfileAgent:
"ProfileAgent.greeting.prompt",
format_history_string(
messages,
omit_system_body=settings.agent_log_omit_system_message_body,
omit_system_body=agent_log_defaults.omit_system_message_body,
),
)
prompt_chars = _message_contents_char_count(messages)
@@ -380,14 +383,14 @@ class ProfileAgent:
)
response_text = await self._invoke_chat(
messages,
max_tokens=settings.chat_profile_followup_max_tokens,
max_tokens=chat.profile_followup_max_tokens,
conversation_id=conversation_id,
agent_name="ProfileAgent.generate_profile_greeting",
)
segments = await self._segments_from_response(
response_text,
max_segments=2,
max_chars_per_segment=settings.chat_interview_max_chars_per_segment,
max_chars_per_segment=chat.interview_max_chars_per_segment,
fallback=_profile_greeting_fallback(language),
)
log_agent_summary(

View File

@@ -34,6 +34,7 @@ from app.agents.stage_constants import (
)
from app.agents.state_schema import KnownFact, PersonaThread
from app.core.config import settings
from app.features.conversation.constants import chat
# 风格示例的单一事实源已迁至 `app.agents.style_profiles.ChatStyleProfile.reply_style_examples`
# 这里**不再**维护具体字面示例,避免同一模块被当作 few-shot 锚点反复注入,导致风格过拟合。
@@ -292,7 +293,7 @@ def get_opening_prompt(
era_opening_line = ""
if (
settings.chat_era_context_enabled
chat.era_context_enabled
and profile_birth_year is not None
and _compact_era_hint(
current_stage,
@@ -450,7 +451,7 @@ def get_guided_conversation_prompt(
)
era_line = ""
if settings.chat_era_context_enabled:
if chat.era_context_enabled:
era_line = _compact_era_hint(
active_stage,
birth_year=profile_birth_year,
@@ -696,7 +697,7 @@ _STAGE_TOPIC_CHIP_BANK: Dict[str, List[tuple[str, str]]] = {
("support", "家人之间的相互支持"),
("responsibility", "肩上的家庭责任"),
],
"later_life": [
"belief": [
("value", "现在最看重的事"),
("regret", "心里的遗憾"),
("pride", "最骄傲的事"),

View File

@@ -20,6 +20,7 @@ from app.agents.stage_constants import (
from app.core.config import settings
from app.core.llm_call import allm_json_call
from app.core.logging import get_logger
from app.features.conversation.constants import chat
logger = get_logger(__name__)
@@ -59,7 +60,7 @@ async def detect_primary_life_stage(
每轮在启用时调用阶段检测 LLM短句亦由模型判断不用关键词替代
"""
fb = normalize_chat_stage(current_stage, "childhood")
if not settings.chat_stage_detection_enabled:
if not chat.stage_detection_enabled:
return _keyword_fallback_stage(user_message, fb)
if not llm:
@@ -76,7 +77,7 @@ async def detect_primary_life_stage(
llm,
prompt,
StageDetectionOutput,
max_tokens=settings.chat_stage_detection_max_tokens,
max_tokens=chat.stage_detection_max_tokens,
agent="detect_primary_life_stage",
fallback_factory=fallback_factory,
)

View File

@@ -12,6 +12,7 @@ from app.agents.image_prompt.prompt_agent import PromptGenerationAgent
from app.core.config import settings
from app.core.logging import get_logger
from app.features.memoir.memoir_images.settings import MemoirImageSettings
from app.features.memoir.constants import memoir
logger = get_logger(__name__)
@@ -84,7 +85,7 @@ def get_image_prompt_orchestrator() -> ImagePromptOrchestrator:
try:
llm = LlmGateway().langchain_llm_for(LlmUseCase("image_prompt"))
except Exception as e:
if settings.image_prompt_fallback_disabled:
if memoir.image_prompt_fallback_disabled:
raise
logger.warning(
"ImagePromptOrchestrator LLM 初始化失败,使用确定性 fallback: {}",

View File

@@ -15,6 +15,7 @@ from app.core.config import settings
from app.core.llm_call import LLMCallError, llm_json_call
from app.core.logging import get_logger
from app.features.conversation.models import Segment
from app.features.memoir.constants import memoir
logger = get_logger(__name__)
@@ -68,7 +69,7 @@ def run_batch_phase1_prep(
llm,
prompt,
BatchPhase1LLMOutput,
max_tokens=int(settings.memoir_phase1_batch_llm_max_tokens),
max_tokens=int(memoir.phase1_batch_llm_max_tokens),
agent="BatchPhase1Prep.run",
)
except LLMCallError as e:

View File

@@ -26,6 +26,7 @@ from app.core.config import settings
from app.core.json_utils import extract_json_payload
from app.core.llm_call import LLMCallError, llm_json_call
from app.core.logging import get_logger
from app.features.memoir.constants import memoir
logger = get_logger(__name__)
@@ -144,7 +145,7 @@ class ClassificationAgent:
llm,
prompt,
ClassificationOutput,
max_tokens=settings.memoir_classification_max_tokens,
max_tokens=memoir.classification_max_tokens,
agent="ClassificationAgent.classify",
)
category = _normalize_llm_category(out.category)

View File

@@ -14,6 +14,7 @@ from app.agents.stage_constants import normalize_chat_stage
from app.core.config import settings
from app.core.llm_call import LLMCallError, llm_json_call
from app.core.logging import get_logger
from app.features.memoir.constants import memoir
logger = get_logger(__name__)
@@ -64,7 +65,7 @@ class ExtractionAgent:
llm,
prompt,
StateExtractionOutput,
max_tokens=settings.memoir_extraction_max_tokens,
max_tokens=memoir.extraction_max_tokens,
agent="ExtractionAgent.extract",
)
raw_slots = parsed.slots or {}

View File

@@ -13,6 +13,7 @@ from app.agents.memoir.schemas import FidelityOutput
from app.core.config import settings
from app.core.llm_call import LLMCallError, llm_json_call
from app.core.logging import get_logger
from app.features.memoir.constants import memoir
logger = get_logger(__name__)
@@ -46,7 +47,7 @@ class FidelityCheckAgent:
existing_canonical_markdown: str | None = None,
is_append: bool = False,
) -> bool:
if not llm or not settings.memoir_fidelity_check_enabled:
if not llm or not memoir.fidelity_check_enabled:
return True
oral = (oral_text or "").strip()
gen = (narrative_json or "").strip()
@@ -108,7 +109,7 @@ class FidelityCheckAgent:
llm,
prompt,
FidelityOutput,
max_tokens=settings.memoir_fidelity_check_max_tokens,
max_tokens=memoir.fidelity_check_max_tokens,
agent="FidelityCheckAgent.passes",
)
ok = bool(out.pass_)
@@ -120,7 +121,7 @@ class FidelityCheckAgent:
return ok
except LLMCallError as e:
logger.warning("FidelityCheckAgent 解析失败: {}", e)
if is_append or settings.memoir_fidelity_fail_open_on_parse_error:
if is_append or memoir.fidelity_fail_open_on_parse_error:
logger.info("event=fidelity_parse_fail_open is_append={}", is_append)
return True
logger.warning("event=fidelity_parse_fail_closed")

View File

@@ -18,6 +18,7 @@ from app.core.config import settings
from app.core.langchain_llm import invoke_json_object
from app.core.llm_call import llm_json_call
from app.core.logging import get_logger
from app.features.memoir.constants import memoir
logger = get_logger(__name__)
@@ -63,7 +64,7 @@ class NarrativeAgent:
llm,
prompt,
MemoirTitleOutput,
max_tokens=settings.memoir_title_max_tokens,
max_tokens=memoir.title_max_tokens,
agent="NarrativeAgent.generate_title",
fallback_factory=_title_fallback,
)
@@ -118,7 +119,7 @@ class NarrativeAgent:
occupation=occupation,
language=language,
)
max_tokens = int(settings.memoir_narrative_merge_max_tokens)
max_tokens = int(memoir.narrative_merge_max_tokens)
agent_name = "NarrativeAgent.generate_narrative_merge"
else:
prompt = get_narrative_json_prompt(
@@ -132,7 +133,7 @@ class NarrativeAgent:
occupation=occupation,
language=language,
)
max_tokens = int(settings.memoir_narrative_max_tokens)
max_tokens = int(memoir.narrative_max_tokens)
agent_name = "NarrativeAgent.generate_narrative"
return invoke_json_object(
llm,

View File

@@ -29,6 +29,7 @@ from app.core.agent_logging import agent_span, agent_summary_enabled, log_agent_
from app.core.config import settings
from app.core.logging import get_logger
from app.features.conversation.models import Segment
from app.features.memoir.constants import memoir
logger = get_logger(__name__)
@@ -90,7 +91,7 @@ class MemoirOrchestrator:
use_batch = (
bool(segments)
and classify_extract_llm is not None
and settings.memoir_phase1_batch_llm_enabled
and memoir.phase1_batch_llm_enabled
)
if use_batch:
try:
@@ -204,7 +205,7 @@ class MemoirOrchestrator:
segments,
state,
classify_extract_llm,
chunk_size=int(settings.memoir_phase1_batch_llm_chunk_size),
chunk_size=int(memoir.phase1_batch_llm_chunk_size),
on_chunk=on_phase1_chunk,
language=language,
)

View File

@@ -21,6 +21,8 @@ from app.core.config import settings
from app.core.llm_call import LLMCallError, llm_json_call
from app.core.logging import get_logger
from app.features.story.models import Story
from app.features.memoir.constants import memoir
from app.features.story.constants import story
logger = get_logger(__name__)
@@ -47,7 +49,7 @@ def default_append_target_story_id(
ordered = sort_stories_for_route(
candidate_stories,
meta,
summary_min_chars=int(settings.story_route_summary_min_chars),
summary_min_chars=int(story.route_summary_min_chars),
)
if not ordered:
return None
@@ -247,7 +249,7 @@ class StoryRouteAgent:
llm,
prompt,
StoryRouteDecision,
max_tokens=settings.memoir_story_route_max_tokens,
max_tokens=memoir.story_route_max_tokens,
agent="StoryRouteAgent.decide",
fallback_factory=_decide_fallback,
)
@@ -295,7 +297,7 @@ class StoryRouteAgent:
llm,
prompt,
StoryBatchPlan,
max_tokens=settings.memoir_story_batch_plan_max_tokens,
max_tokens=memoir.story_batch_plan_max_tokens,
agent="StoryRouteAgent.plan_batch",
)
except LLMCallError as e:

View File

@@ -15,6 +15,7 @@ if TYPE_CHECKING:
from app.core.config import Settings
from app.features.story.models import Story
from app.features.story.constants import story
_PLAIN_SNIPPET_NOISE = re.compile(r"[`*_#]+")
@@ -213,11 +214,11 @@ def build_route_candidate_rows(
) -> list[dict[str, Any]]:
"""排序 + 完整候选行(尚未做总预算降级)。"""
meta = story_meta or {}
summary_min = int(settings.story_route_summary_min_chars)
summary_min = int(story.route_summary_min_chars)
ordered = sort_stories_for_route(stories, meta, summary_min_chars=summary_min)
body_max = int(settings.story_route_candidate_body_max_chars)
head_c = int(settings.story_route_long_body_head_chars)
tail_c = int(settings.story_route_long_body_tail_chars)
body_max = int(story.route_candidate_body_max_chars)
head_c = int(story.route_long_body_head_chars)
tail_c = int(story.route_long_body_tail_chars)
rows: list[dict[str, Any]] = []
for s in ordered:
rows.append(
@@ -231,8 +232,8 @@ def build_route_candidate_rows(
)
)
by_id = {str(s.id): s for s in ordered}
total_max = int(settings.story_route_candidate_total_max_chars)
index_prev = int(settings.story_route_index_preview_chars)
total_max = int(story.route_candidate_total_max_chars)
index_prev = int(story.route_index_preview_chars)
return apply_total_budget_downgrade(
rows,
stories_by_id=by_id,