from __future__ import annotations from .shared import * class AIStewardTitleUtilsMixin: def __init__(self, db: AsyncSession): self.db = db self.llm = LLMService() @staticmethod def _normalize_project_title_candidate(candidate: str, *, max_len: int = 24) -> str: cleaned = re.sub(r"\s+", " ", str(candidate or "")).strip(" []【】()()::\"'“”") if not cleaned: return "" cleaned = re.sub( r"^(?:(?:项目|需求)(?:名字|名称|名)?(?:叫|是|为|名为|名称为|名字为)?|(?:项目|需求)?(?:名为|名称为|名字为|名称是|名字是|叫|为))\s*", "", cleaned, flags=re.IGNORECASE, ).strip(" []【】()()::\"'“”") if not cleaned: return "" normalized = re.sub(r"\s+", "", cleaned).lower() generic_tokens = { "项目", "一个项目", "新项目", "项目列表", "项目库", "pmflow", "pmflow项目列表", } if normalized in generic_tokens: return "" if any(token in normalized for token in ("创建项目", "新建项目", "新增项目", "不是创建信号")): return "" return compact_title(cleaned, max_len=max_len) @classmethod def _extract_explicit_project_title_from_instruction(cls, text: str) -> str: cleaned = re.sub(r"\s+", " ", str(text or "")).strip() if not cleaned: return "" explicit_patterns = ( r"(?:项目|需求)(?:名字|名称|名)?(?:固定|统一|必须)(?:叫|名为|名称为|名字为|命名为|定为|是|为)[::,,\s]*(.+)", r"(?:项目|需求)(?:名字|名称|名)?就叫[::,,\s]*(.+)", r"(?:这个|该)?(?:项目|需求)(?:先|就)?叫[::,,\s]*(.+)", ) for pattern in explicit_patterns: match = re.search(pattern, cleaned, flags=re.IGNORECASE) if not match: continue candidate = re.sub(r"[,。,.;;!??].*$", "", match.group(1)).strip(" []【】()()::\"'“”") normalized = cls._normalize_project_title_candidate(candidate, max_len=50) if normalized: return normalized return "" @classmethod def _extract_project_title_from_instruction(cls, text: str) -> str: cleaned = re.sub(r"\s+", " ", str(text or "")).strip() if not cleaned: return "" explicit_title = cls._extract_explicit_project_title_from_instruction(cleaned) if explicit_title: return explicit_title # 优先去掉“不是X,是Y”中的前置纠正片段,避免把纠正话术落到标题里。 cleaned = re.sub(r"^.*?不是[^,。,;;]*信号[^,。,;;]*[,,;; ]*是", "", cleaned) quoted_match = re.search(r"(?:项目|新项目)[^\"“”']{0,10}[\"“”']([^\"“”']{2,40})[\"“”']", cleaned) if quoted_match: return cls._normalize_project_title_candidate(quoted_match.group(1)) for pattern in ( r"(?:项目|需求)(?:名字|名称|名)?(?:叫|名为|名称是|名称为|是|为)[::,,\s]*(.+)", r"(?:帮我|请|麻烦|给我)?(?:新增|增加|添加|创建|新建|建立|立项|开个|开一个|建个|建一个|做个|做一个|加个|加一个)(?:一个|个)?项目(?:叫|名为|名称是|名称为|是|为)?[::,,\s]*(.+)", ): match = re.search(pattern, cleaned, flags=re.IGNORECASE) if not match: continue candidate = re.sub(r"[,。,.;;!??].*$", "", match.group(1)).strip(" []【】()()::\"'“”") candidate = re.sub(r"^(?:一个|个|新项目|项目|需求)\s*", "", candidate, flags=re.IGNORECASE) candidate = re.sub( r"^(?:(?:项目|需求)(?:名字|名称|名)?(?:叫|是|为|名为|名称为|名字为)?|(?:项目|需求)?(?:名为|名称为|名字为|名称是|名字是|叫|为))\s*", "", candidate, flags=re.IGNORECASE, ) candidate = re.sub(r"(?:因为|所以|用来|为了|方便).*$", "", candidate).strip() normalized = cls._normalize_project_title_candidate(candidate) if normalized: return normalized return "" @staticmethod def _looks_like_instruction_sentence(text: str) -> bool: normalized = re.sub(r"\s+", "", str(text or "")).lower() if not normalized: return False return "项目" in normalized and any( token in normalized for token in ("创建", "新建", "新增", "增加", "添加", "帮我", "给我", "不是", "信号") ) @classmethod def _compact_project_description(cls, *, input_text: str, title: str, draft_description: str) -> str: raw = str(draft_description or "").strip() if raw and not cls._looks_like_instruction_sentence(raw): return raw[:4000] if title: return f"项目:{title}"[:4000] return str(input_text or "").strip()[:4000]