"""Feishu signal pipeline mixin.""" from __future__ import annotations from .shared import * class FeishuPipelineExtractionMixin: async def _extract_project_prefill(self, message: str) -> dict[str, Any]: prompt = PROMPT_FEISHU_PROJECT_PREFILL.format(message=message[:1500]) try: raw = await self.llm.generate( prompt, db=self.db, tenant_id=self.tenant_id, function_name="feishu_project_prefill", ) parsed = _safe_json_object(raw) if not parsed: raise ValueError("project_prefill_empty_json") parsed["reasoning"] = _normalize_reasoning(parsed.get("reasoning")) return _validate_schema(_ProjectPrefillSchema, parsed, context="project_prefill") except Exception as exc: logger.warning(f"feishu_project_prefill_fallback: error={exc}") fallback = self._build_project_prefill_degraded(message=message) return _validate_schema(_ProjectPrefillSchema, fallback, context="project_prefill_fallback") async def _extract_signal( self, message: str, *, projects: list[dict[str, Any]], signal_intake_hint: bool = False, ) -> dict[str, Any]: prompt = PROMPT_FEISHU_SIGNAL_EXTRACTION.format( message=message[:1500], projects_json=json.dumps(projects, ensure_ascii=False), ) try: raw = await self.llm.generate( prompt, db=self.db, tenant_id=self.tenant_id, function_name="feishu_signal_extraction", ) parsed = _safe_json_object(raw) if not parsed: raise ValueError("signal_extraction_empty_json") parsed["reasoning"] = _normalize_reasoning(parsed.get("reasoning")) return _validate_schema(_SignalExtractionSchema, parsed, context="signal_extraction") except Exception as exc: logger.warning(f"feishu_signal_extraction_fallback: error={exc}") fallback = self._build_signal_extraction_degraded( message=message, signal_intake_hint=signal_intake_hint, ) return _validate_schema(_SignalExtractionSchema, fallback, context="signal_extraction_fallback") async def _classify_level( self, *, message: str, projects: list[dict[str, Any]], signal_intake_hint: bool = False, ) -> dict[str, Any]: prompt = PROMPT_FEISHU_LEVEL_CLASSIFICATION.format( message=message[:1500], projects_json=json.dumps(projects, ensure_ascii=False), ) try: raw = await self.llm.generate( prompt, db=self.db, tenant_id=self.tenant_id, function_name="feishu_level_classification", ) parsed = _safe_json_object(raw) if not parsed: raise ValueError("level_classification_empty_json") # 兼容不同模型输出别名,避免只返回总分/中文 level 时被误降级到 signal。 bi_raw = _first_non_empty(parsed, ("build_intent_score", "buildIntentScore", "intent_score", "build_intent")) sc_raw = _first_non_empty(parsed, ("scope_score", "scopeScore", "scope")) tl_raw = _first_non_empty(parsed, ("timeline_score", "timelineScore", "timeline")) ow_raw = _first_non_empty(parsed, ("owner_score", "ownerScore", "owner")) provided_project_score_raw = _first_non_empty(parsed, ("project_score", "projectScore", "score")) bi = _safe_float(bi_raw, 0.0) sc = _safe_float(sc_raw, 0.0) tl = _safe_float(tl_raw, 0.0) ow = _safe_float(ow_raw, 0.0) weighted = round((bi * 0.4) + (sc * 0.25) + (tl * 0.2) + (ow * 0.15), 4) component_count = sum(value is not None for value in (bi_raw, sc_raw, tl_raw, ow_raw)) has_project_score = provided_project_score_raw is not None provided_project_score = _safe_float(provided_project_score_raw, 0.0) final_project_score = weighted if component_count < 4 and has_project_score: final_project_score = provided_project_score elif component_count > 0 and has_project_score: # 当模型给出了总分与维度分,优先采用更保守的较高值,降低误判为 signal 的风险。 final_project_score = max(weighted, provided_project_score) final_project_score = max(0.0, min(1.0, final_project_score)) default_level = "project" if final_project_score >= 0.6 else "signal" normalized_split_items: list[dict[str, str]] = [] raw_split_items = parsed.get("split_items") if isinstance(raw_split_items, list): for item in raw_split_items[:2]: if not isinstance(item, dict): continue item_level = _normalize_level_value(item.get("level"), default=default_level) item_content = str(item.get("content") or "").strip() if not item_content: continue normalized_split_items.append({"level": item_level, "content": item_content}) level_reasoning = str(parsed.get("level_reasoning") or "").strip() or "模型返回缺少理由,已按结构化兜底。" payload: dict[str, Any] = { "build_intent_score": max(0.0, min(1.0, bi)), "scope_score": max(0.0, min(1.0, sc)), "timeline_score": max(0.0, min(1.0, tl)), "owner_score": max(0.0, min(1.0, ow)), "project_score": final_project_score, "level": _normalize_level_value(parsed.get("level"), default=default_level), "level_confidence": max( 0.0, min(1.0, _safe_float(parsed.get("level_confidence"), max(final_project_score, 1 - final_project_score))), ), "level_reasoning": level_reasoning[:600], "should_split": _safe_bool(parsed.get("should_split"), False), "split_items": normalized_split_items, } return _validate_schema(_LevelClassificationSchema, payload, context="level_classification") except Exception as exc: logger.warning(f"feishu_level_classification_fallback: error={exc}") fallback = self._build_level_classification_degraded(signal_intake_hint=signal_intake_hint) return _validate_schema(_LevelClassificationSchema, fallback, context="level_classification_fallback") @staticmethod def _build_level_classification_degraded(*, signal_intake_hint: bool) -> dict[str, Any]: project_score = 0.2 if signal_intake_hint else 0.3 return { "build_intent_score": 0.0, "scope_score": 0.0, "timeline_score": 0.0, "owner_score": 0.0, "project_score": project_score, "level": "signal", "level_confidence": 0.35 if signal_intake_hint else 0.25, "level_reasoning": "结构化分类暂时不可用,当前先按低置信信号待确认处理。", "should_split": False, "split_items": [], } @staticmethod def _build_signal_extraction_degraded(*, message: str, signal_intake_hint: bool) -> dict[str, Any]: text = str(message or "").strip() cleaned = re.sub(r"\s+", " ", text).strip() summary = cleaned[:280] if cleaned else "原始飞书消息暂未解析出稳定信号结构。" title = (cleaned[:48] or "飞书需求信号").strip() or "飞书需求信号" return { "title": title[:200], "type": "新需求", "priority": "P2", "matched_project_id": None, "summary": summary, "reasoning": ["结构化信号提取失败,当前仅保留原始消息,待确认后再绑定正式对象。"], "confidence": 0.35 if signal_intake_hint else 0.3, "recommended_action": "enter_only", } @staticmethod def _build_project_prefill_degraded(*, message: str) -> dict[str, Any]: text = str(message or "").strip() cleaned = re.sub(r"\s+", " ", text).strip() suggested_name = cleaned[:30] if cleaned else "飞书新需求项目" if len(suggested_name) < 4: suggested_name = "飞书新需求项目" suggested_description = ( f"由飞书需求自动生成的项目草案:{cleaned[:200]}" if cleaned else "由飞书需求自动生成的项目草案,待补充目标、范围与里程碑。" ) return { "suggested_name": suggested_name, "suggested_description": suggested_description, "suggested_scope": None, "suggested_owner": None, "suggested_timeline": None, "confidence": 0.35, "reasoning": [ "结构化项目预填失败,当前仅保留原始描述,待补充标题、范围和负责人。", "负责人和时间线仅在消息明确给出时才填写。", ], }