class _LevelSplitItemSchema(BaseModel): level: Literal["project", "signal"] content: str = Field(min_length=1, max_length=4000) class _LevelClassificationSchema(BaseModel): build_intent_score: float = Field(ge=0.0, le=1.0) scope_score: float = Field(ge=0.0, le=1.0) timeline_score: float = Field(ge=0.0, le=1.0) owner_score: float = Field(ge=0.0, le=1.0) project_score: float = Field(ge=0.0, le=1.0) level: Literal["project", "signal"] level_confidence: float = Field(ge=0.0, le=1.0) level_reasoning: str = Field(min_length=1, max_length=600) should_split: bool = False split_items: list[_LevelSplitItemSchema] = Field(default_factory=list, max_length=2) class _SignalExtractionSchema(BaseModel): title: str = Field(min_length=1, max_length=200) type: Literal["竞品动态", "用户反馈", "项目风险", "新需求"] priority: Literal["P0", "P1", "P2", "P3"] matched_project_id: Optional[int] = None summary: str = Field(min_length=1, max_length=3000) reasoning: list[str] = Field(min_length=1, max_length=3) confidence: float = Field(ge=0.0, le=1.0) recommended_action: Literal["enter_only", "create_charter", "trigger_alert"] class _ProjectPrefillSchema(BaseModel): suggested_name: str = Field(min_length=1, max_length=255) suggested_description: str = Field(min_length=1, max_length=4000) suggested_owner: str = Field(min_length=1, max_length=120) suggested_timeline: str = Field(min_length=1, max_length=120) confidence: float = Field(ge=0.0, le=1.0) reasoning: list[str] = Field(min_length=1, max_length=3) def _normalize_reasoning(value: Any, max_items: int = 3) -> list[str]: if isinstance(value, list): rows = [str(x).strip() for x in value if str(x).strip()] return rows[:max_items] if isinstance(value, str): parts = [p.strip(" -•\t") for p in value.splitlines() if p.strip()] return parts[:max_items] return [] def _safe_json_object(value: str) -> dict[str, Any]: raw = (value or "").strip() if raw.startswith("```"): chunks = raw.split("```") if len(chunks) >= 2: raw = chunks[1].strip() if raw.startswith("json"): raw = raw[4:].strip() try: parsed = json.loads(raw) return parsed if isinstance(parsed, dict) else {} except Exception: return {} def _validate_schema( schema: type[BaseModel], payload: dict[str, Any], *, context: str, ) -> dict[str, Any]: if not isinstance(payload, dict): raise ValueError(f"{context}_schema_invalid: payload_not_object") try: if hasattr(schema, "model_validate"): model = schema.model_validate(payload) # type: ignore[attr-defined] else: model = schema.parse_obj(payload) # type: ignore[attr-defined] except ValidationError as exc: raise ValueError(f"{context}_schema_invalid: {str(exc)[:400]}") from exc if hasattr(model, "model_dump"): return model.model_dump() # type: ignore[no-any-return] return model.dict() # type: ignore[no-any-return] def _safe_float(value: Any, default: float = 0.0) -> float: try: return float(value) except (TypeError, ValueError): return float(default) def _safe_bool(value: Any, default: bool = False) -> bool: if isinstance(value, bool): return value if value is None: return default text = str(value).strip().lower() if text in {"1", "true", "yes", "y", "on", "是", "真"}: return True if text in {"0", "false", "no", "n", "off", "否", "假"}: return False return default def _safe_int(value: Any) -> Optional[int]: if isinstance(value, bool): return None try: return int(str(value).strip()) except (TypeError, ValueError): return None def _normalize_level_value(value: Any, default: str = "signal") -> str: text = str(value or "").strip().lower() if not text: return default if text in {"project", "project-level", "project_level", "new_project"}: return "project" if text in {"signal", "signal-level", "signal_level"}: return "signal" if text in {"项目", "项目级", "立项", "新项目"}: return "project" if text in {"信号", "信号级", "需求", "需求级", "事件"}: return "signal" if "project" in text or "项目" in text or "立项" in text: return "project" if "signal" in text or "信号" in text or "需求" in text: return "signal" return default def _first_non_empty(data: dict[str, Any], keys: tuple[str, ...]) -> Any: for key in keys: if key not in data: continue value = data.get(key) if value is None: continue if isinstance(value, str) and not value.strip(): continue return value return None def _utc_now() -> datetime: return now_cn_aware() def _compact_text(value: Any, max_len: int = 160) -> str: text = str(value or "").strip() if len(text) <= max_len: return text return f"{text[: max_len - 3]}..." def _safe_dict(value: Any) -> dict[str, Any]: if isinstance(value, dict): return value if isinstance(value, str): return _safe_json_object(value) return {} def _build_undo_deadline(value: Optional[datetime], *, minutes: int = 10) -> Optional[datetime]: if value is None: return None if value.tzinfo is not None: value = value.astimezone(CN_TZ).replace(tzinfo=None) return value + timedelta(minutes=minutes)