111 lines
5.1 KiB
Python
111 lines
5.1 KiB
Python
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]
|