from __future__ import annotations from .models import * class IntentResolver: """Resolve raw text to canonical intent.""" OPERATION_VERBS = ( "新增", "增加", "添加", "加", "加一个", "加个", "新建", "创建", "建立", "立项", "接入", "修复", "优化", "改成", "改为", "更新", "分配", "指派", "归档", "执行", ) @staticmethod def _looks_like_negated_write_statement(*, text: str) -> bool: normalized = re.sub(r"\s+", "", str(text or "")).lower() if not normalized: return False negation_tokens = ("别", "别再", "别乱", "不要", "不要再", "先别", "先不要", "禁止", "不能", "无需", "不用") if not any(token in normalized for token in negation_tokens): return False write_tokens = ("创建", "新建", "新增", "增加", "添加", "加", "接入", "执行", "分配", "指派", "立项") object_tokens = ("信号", "需求", "任务", "项目", "事项", "工单", "流程", "信号源") if any(w in normalized for w in write_tokens) and any(o in normalized for o in object_tokens): return True return bool( re.search( r"(?:别|不要|先别|先不要|禁止|不能).{0,8}(?:创建|新建|新增|增加|添加|加|接入|执行|分配|指派|立项)", normalized, re.IGNORECASE, ) ) @staticmethod def resolve(*, text: str, project_id: Optional[int]) -> CanonicalIntent: normalized = re.sub(r"\s+", "", str(text or "")).lower() pretty = re.sub(r"\s+", " ", str(text or "")).strip() if not pretty: return CanonicalIntent(INTENT_IGNORE, 1.0, project_id, "", "", {"reason": "empty"}) if IntentResolver._is_smalltalk_message(pretty=pretty, normalized=normalized): return CanonicalIntent(INTENT_IGNORE, 0.95, project_id, "", "", {"reason": "smalltalk"}) if IntentResolver._looks_like_negated_write_statement(text=pretty): return CanonicalIntent( INTENT_QUERY, 0.9, project_id, "", pretty[:300], {"reason": "negated_write_statement"}, ) explicit_followup_target = FOLLOWUP_WORK_ITEM_ID_RE.search(pretty) if explicit_followup_target is not None: return CanonicalIntent( INTENT_FOLLOWUP_CONTINUE, 0.92, project_id, "", "", { "followup_by_work_item_id": True, "target_work_item_id": _safe_int(explicit_followup_target.group(1)), }, ) if any(token in pretty for token in FOLLOWUP_CONFIRM_HINTS): return CanonicalIntent(INTENT_FOLLOWUP_CONFIRM, 0.82, project_id, "", "", {}) if any(token in pretty for token in FOLLOWUP_CONTINUE_HINTS): return CanonicalIntent(INTENT_FOLLOWUP_CONTINUE, 0.82, project_id, "", "", {}) if SignalSourceOrchestrationService.looks_like_signal_source_request(pretty): return CanonicalIntent( INTENT_SIGNAL_SOURCE_ONBOARD, 0.92, project_id, IntentResolver._build_signal_title(pretty, default="新增信号源接入请求"), pretty[:300], {"source_request": True}, ) if ( IntentResolver._looks_like_project_create_request(text=pretty) or extract_project_create_title(pretty) ): return CanonicalIntent( INTENT_PROJECT_CREATE, 0.9, project_id, IntentResolver._build_signal_title(pretty, default="新项目创建请求"), pretty[:300], {}, ) if IntentResolver._looks_like_signal_create_request(text=pretty): return CanonicalIntent( INTENT_SIGNAL_CREATE, 0.78, project_id, IntentResolver._build_signal_title(pretty, default="新信号"), pretty[:300], {}, ) if IntentResolver._looks_like_query(text=pretty): return CanonicalIntent( INTENT_QUERY, 0.88, project_id, "", pretty[:300], {}, ) return CanonicalIntent( INTENT_QUERY, 0.6, project_id, "", pretty[:300], {"fallback": True, "reason": "default_query_fallback"}, ) @staticmethod def _looks_like_signal_create_request(*, text: str) -> bool: normalized = re.sub(r"\s+", "", str(text or "")).lower() if not normalized: return False if IntentResolver._looks_like_negated_write_statement(text=normalized): return False candidate_discover_hints = ("候选", "换一批", "刷新", "再来一批", "先分析") if ( any(token in normalized for token in candidate_discover_hints) and any(token in normalized for token in ("确认", "入库", "提交")) and any(token in normalized for token in ("项目", "project")) ): return True if re.search(r"(?:抓|找|给).{0,8}(?:\d+|[一二两三四五])(?:条|个)?候选", normalized, re.IGNORECASE): return True if any(token in normalized for token in SIGNAL_CREATE_HINTS): return True signal_object_hints = ("信号", "需求", "任务", "事项", "问题", "改造", "优化") has_operation = any(token in normalized for token in IntentResolver.OPERATION_VERBS) has_signal_object = any(token in normalized for token in signal_object_hints) if has_operation and has_signal_object: return True return bool( re.search( r"(?:给|帮|请).{0,20}(?:新增|增加|添加|补充|加).{0,20}(?:信号|需求|任务|事项)", normalized, re.IGNORECASE, ) ) @staticmethod def _is_smalltalk_message(*, pretty: str, normalized: str) -> bool: compact = re.sub(r"[\\s\\??!!,,。~~、]+", "", str(pretty or "")).strip().lower() if not compact: return True if any(keyword in compact for keyword in SMALLTALK_STRUCTURED_HINTS): return False if compact in SMALLTALK_EXACT: return True if len(compact) <= 8 and any(keyword in compact for keyword in SMALLTALK_SHORT_HINTS): return True return False @staticmethod def _looks_like_query(*, text: str) -> bool: normalized = re.sub(r"\s+", "", str(text or "")).lower() if not normalized: return False if IntentResolver._looks_like_signal_create_request(text=normalized): return False if any(token in normalized for token in QUESTION_HINTS): return True has_question_structure = ( "?" in normalized or "?" in normalized or any(token in normalized for token in ("吗", "呢", "吧", "是否", "是不是")) ) if not has_question_structure: return False has_operation = any(token in normalized for token in IntentResolver.OPERATION_VERBS) return not has_operation @staticmethod def _looks_like_project_create_request(*, text: str) -> bool: normalized = re.sub(r"\s+", "", str(text or "")).lower() if not normalized: return False if any(token in normalized for token in PROJECT_CREATE_HINTS): return True return bool( re.search( r"(?:新增|增加|添加|新建|创建|建立|开个|开一个|建个|建一个|做个|做一个|加个|加一个|立项)(?:一个|个)?项目", normalized, re.IGNORECASE, ) ) @staticmethod def _build_signal_title(text: str, *, default: str) -> str: raw = str(text or "").strip() return raw or default