"""Pydantic models for TOML-backed application configuration (non-secret SSOT).""" from pydantic import BaseModel, ConfigDict, Field class DeployConfig(BaseModel): model_config = ConfigDict(extra="forbid") alembic_startup_fail_fast: bool = False access_token_expire_minutes: int = 120 refresh_token_expire_days: int = 30 refresh_token_reuse_grace_seconds: int = Field(default=30, ge=0, le=300) mock_sms_login_enabled: bool = False tencent_sms_sdk_app_id: str = "" tencent_sms_sign_name: str = "" tencent_sms_template_id: str = "" tencent_cos_bucket: str = "" tencent_cos_base_url: str = "" enable_tts: bool = True memoir_image_enabled: bool = False enable_docs: bool = True wechat_pay_app_id: str = "" wechat_pay_mch_id: str = "" wechat_pay_private_key_path: str = "certs/apiclient_key.pem" wechat_pay_cert_serial_no: str = "" wechat_pay_notify_url: str = "" wechat_pay_platform_public_key_path: str = "" wechat_pay_platform_public_key_id: str = "" alipay_app_id: str = "" alipay_notify_url: str = "" liblib_template_uuid: str = "" log_level: str = "INFO" otel_enabled: bool = False otel_exporter_otlp_endpoint: str = "http://localhost:48317" api_cors_origins: str = "" class ChatConfig(BaseModel): model_config = ConfigDict(extra="forbid") interview_max_tokens: int = 512 interview_max_segments: int = 2 interview_max_chars_per_segment: int = 380 opening_max_tokens: int = 380 profile_followup_max_tokens: int = 280 history_max_pairs: int = 15 history_max_chars: int = 6000 era_context_enabled: bool = True stage_detection_enabled: bool = True stage_detection_max_tokens: int = 128 interview_persona: str = "default" interview_temperature: float = 0.93 memory_retrieval_enabled: bool = True memory_top_k: int = 8 memory_evidence_max_chars: int = 4096 memory_safe_evidence_format_enabled: bool = True reply_planner_llm_enabled: bool = False reply_planner_max_tokens: int = 256 reply_planner_temperature: float = 0.2 re_greeting_enabled: bool = True re_greeting_idle_hours: float = 6.0 topic_chips_enabled: bool = True topic_chips_max: int = 4 input_normalize_enabled: bool = True input_normalize_mode: str = "rules" input_normalize_llm_max_tokens: int = 512 input_normalize_llm_max_input_chars: int = 8000 input_normalize_llm_voice_only: bool = True profile_max_turns: int = 8 profile_extract_max_tokens: int = 512 class MemoirConfig(BaseModel): model_config = ConfigDict(extra="forbid") fidelity_check_enabled: bool = True fidelity_check_max_tokens: int = 512 oral_normalize_enabled: bool = True oral_normalize_mode: str = "rules" oral_normalize_llm_max_tokens: int = 512 oral_normalize_llm_max_input_chars: int = 8000 phase1_batch_llm_enabled: bool = True phase1_batch_llm_max_tokens: int = 4096 phase1_batch_llm_chunk_size: int = 24 pipeline_run_ttl_seconds: int = 172_800 extraction_max_tokens: int = 1024 classification_max_tokens: int = 256 narrative_max_tokens: int = 4096 narrative_merge_max_tokens: int = 8192 title_max_tokens: int = 256 story_route_max_tokens: int = 1024 story_batch_plan_max_tokens: int = 4096 segment_batch_min_chars: int = 50 segment_batch_max_wait_seconds: float = 60.0 narrative_immediate_char_threshold: int = 50 narrative_batch_min_segments: int = 3 narrative_batch_min_chars: int = 80 narrative_batch_max_wait_seconds: float = 120.0 extraction_updates_current_stage: bool = False fidelity_fail_open_on_parse_error: bool = False narrative_evidence_overlap_min_chars: int = 14 evidence_scene_anchor_check_enabled: bool = True title_slots_require_body_or_oral_match: bool = True title_hay_grounding_strict_phrases_enabled: bool = True recompose_retry_on_lock_contention: bool = True phase2_singleflight_immediate: bool = True route_defer_enabled: bool = True route_defer_seconds: float = 120.0 route_defer_max_attempts: int = 3 quality_pass_enabled: bool = True quality_pass_delay_seconds: int = 5 story_route_append_guardrail_oral_chars: int = 1800 min_inline_images_for_chapter_cover: int = 1 image_poll_interval: int = 3 image_max_attempts: int = 20 image_provider: str = "liblib" image_style_default: str = "watercolor" image_size_default: str = "1280x720" image_download_hosts: str = "" image_prompt_fallback_disabled: bool = False class MemoryConfig(BaseModel): model_config = ConfigDict(extra="forbid") enrichment_enabled: bool = True enrichment_max_chars: int = 12000 compaction_enabled: bool = True compaction_debounce_seconds: int = 105 compaction_lock_ttl_seconds: int = 600 compaction_chunk_similarity_threshold: float = 0.92 compaction_min_layers_for_exclude: int = 2 compaction_max_chunks_per_run: int = 200 compaction_max_excludes_per_run: int = 50 compaction_max_neighbors_per_chunk: int = 25 compaction_text_jaccard_min: float = 0.55 compaction_metadata_event_year_window: int = 1 compaction_sweep_recent_hours: int = 24 class StoryConfig(BaseModel): model_config = ConfigDict(extra="forbid") image_min_body_chars: int = 400 image_enqueue_dedup_ttl: int = 300 recompose_chapter_delay_seconds: int = 8 chapter_pipeline_lock_ttl_seconds: int = 360 append_max_canonical_chars: int = 12000 append_max_versions: int = 20 route_candidate_body_max_chars: int = 2200 route_candidate_total_max_chars: int = 20_000 route_long_body_head_chars: int = 700 route_long_body_tail_chars: int = 700 route_summary_min_chars: int = 30 route_index_preview_chars: int = 140 title_min_body_chars: int = 60 evidence_top_k_default: int = 10 evidence_top_k_large_batch: int = 5 evidence_large_batch_threshold: int = 3 class EvalConfig(BaseModel): model_config = ConfigDict(extra="forbid") judge_base_url: str = "https://open.bigmodel.cn/api/paas/v4" judge_model: str = "glm-5" judge_temperature: float = 0.3 judge_deepseek_model: str = "deepseek-v4-flash" judge_deepseek_thinking_enabled: bool = False judge_deepseek_context_window_tokens: int = 64_000 judge_context_window_tokens: int = 200_000 judge_completion_reserve_tokens: int = 4096 judge_prompt_budget_safety_tokens: int = 2048 judge_approx_tokens_per_char: float = 1.0 judge_max_transcript_chars: int = 0 judge_max_compare_transcript_chars_each: int = 0 judge_compare_prompt_overhead_chars: int = 10_000 judge_memoir_chapter_concurrency: int = 4 judge_memoir_body_max_chars: int = 36_000 judge_memoir_evidence_max_chars: int = 32_000 judge_memoir_completion_max_tokens: int = 3072 candidate_temperature: float = 0.7 gate_protected_regression_threshold: float = 2.0 execution_enabled: bool = True internal_enable_docs: bool = False internal_cors_origins: str = "" class LlmConfig(BaseModel): model_config = ConfigDict(extra="forbid") deepseek_base_url: str = "https://api.deepseek.com" deepseek_model: str = "deepseek-v4-flash" deepseek_thinking_enabled: bool = False temperature: float = 0.7 fast_model: str = "" embedding_base_url: str = "https://open.bigmodel.cn/api/paas/v4" embedding_model: str = "embedding-3" class AsrConfig(BaseModel): model_config = ConfigDict(extra="forbid") provider: str = "whisper" model_size: str = "small" device: str = "auto" compute_type: str = "auto" model_cache_dir: str = "" class TtsConfig(BaseModel): model_config = ConfigDict(extra="forbid") provider: str = "tencent" voice_type: int = 501004 voice_type_en: int = 501004 codec: str = "mp3" class RedisConfig(BaseModel): model_config = ConfigDict(extra="forbid") socket_timeout_seconds: float = 5.0 socket_connect_timeout_seconds: float = 2.0 health_check_interval_seconds: int = 30 task_tracker_ttl_seconds: int = 86400 class CeleryConfig(BaseModel): model_config = ConfigDict(extra="forbid") memory_enrichment_queue: str = "memory_idle" broker_pool_limit: int = 10 broker_connection_retry_on_startup: bool = True memoir_soft_time_limit: int = 1800 memoir_hard_time_limit: int = 2400 image_soft_time_limit: int = 600 image_hard_time_limit: int = 900 compaction_sweep_soft_time_limit: int = 300 compaction_sweep_hard_time_limit: int = 600 enrichment_soft_time_limit: int = 660 enrichment_hard_time_limit: int = 960 class AlembicConfig(BaseModel): model_config = ConfigDict(extra="forbid") run_on_startup: bool = True max_retries: int = 3 retry_base_seconds: float = 1.0 class AgentLogConfig(BaseModel): model_config = ConfigDict(extra="forbid") agent_verbose: bool = False max_chars: int = 4096 omit_system_message_body: bool = True json_prompt_prefix_chars: int = 0 json_prompt_prefix_only_if_len_gt: int = 4000 prompt_mode: str = "preview" prompt_dedup: bool = False celery_log_level: str = "" httpx_log_level: str = "" log_json_file: str = "" class OtelConfig(BaseModel): model_config = ConfigDict(extra="forbid") exporter_insecure: bool = True service_name: str = "life-echo-api" metric_export_interval_ms: int = 10_000 def traces_sampler(self, app_environment: str) -> str: env = (app_environment or "").strip().lower() if env in ("production", "staging"): return "parentbased_traceidratio" return "always_on" def traces_sampler_arg(self, app_environment: str) -> float | None: env = (app_environment or "").strip().lower() if env in ("production", "staging"): return 0.1 return None class MiscConfig(BaseModel): model_config = ConfigDict(extra="forbid") algorithm: str = "HS256" redis_session_ttl: int = 86400 tencent_sms_template_param_count: int = 2 tencent_cos_region: str = "ap-shanghai" liblib_base_url: str = "https://openapi.liblibai.cloud" alipay_sign_type: str = "RSA2" alipay_under_development: str = "true" class AppConfig(BaseModel): model_config = ConfigDict(extra="forbid") deploy: DeployConfig = Field(default_factory=DeployConfig) chat: ChatConfig = Field(default_factory=ChatConfig) memoir: MemoirConfig = Field(default_factory=MemoirConfig) memory: MemoryConfig = Field(default_factory=MemoryConfig) story: StoryConfig = Field(default_factory=StoryConfig) eval: EvalConfig = Field(default_factory=EvalConfig) llm: LlmConfig = Field(default_factory=LlmConfig) asr: AsrConfig = Field(default_factory=AsrConfig) tts: TtsConfig = Field(default_factory=TtsConfig) celery: CeleryConfig = Field(default_factory=CeleryConfig) redis: RedisConfig = Field(default_factory=RedisConfig) alembic: AlembicConfig = Field(default_factory=AlembicConfig) agent_log: AgentLogConfig = Field(default_factory=AgentLogConfig) otel: OtelConfig = Field(default_factory=OtelConfig) misc: MiscConfig = Field(default_factory=MiscConfig)