"""AI PMO kernel closure data-source mixin.""" from __future__ import annotations from . import ai_pmo_kernel_common as _common for _name, _value in vars(_common).items(): if not _name.startswith("__"): globals()[_name] = _value class KernelClosureDataMixin: async def _load_closure_turns_from_processing_logs( self, *, source_message_id: Optional[str], message_text: str, ) -> list[dict[str, str]]: message_id = str(source_message_id or "").strip() if not message_id: return [] rows = ( await self.db.execute( select(AIProcessingLog) .where( and_( AIProcessingLog.tenant_id == self.tenant_id, AIProcessingLog.feishu_message_id == message_id, ) ) .order_by(AIProcessingLog.id.asc()) .limit(12) ) ).scalars().all() if not rows: return [] normalized_message = self._normalize_dialogue_text(message_text, max_len=600) turns: list[dict[str, str]] = [] for row in rows: user_text = self._normalize_feishu_user_text( row.normalized_text or row.raw_message, max_len=260, ) if user_text and (not normalized_message or user_text != normalized_message): turns = self._merge_closure_turns(turns, [{"role": "user", "text": user_text}]) assistant_text = self._extract_assistant_text_from_log_payload(row.operation_result) if assistant_text: turns = self._merge_closure_turns(turns, [{"role": "assistant", "text": assistant_text}]) return turns[-4:] @classmethod def _score_qa_row_for_closure( cls, *, row: QARecord, normalized_message: str, reference_at: Optional[datetime], ) -> float: question = cls._normalize_dialogue_text(row.question, max_len=500) answer = cls._normalize_dialogue_text(row.answer, max_len=900) if not question or not answer: return -1.0 score = 0.0 merged = f"{question} {answer}" if "信号源" in merged: score += 3.0 if any(token in merged for token in ("模块", "后端", "前端", "流程", "SSTC", "任务", "执行", "接口")): score += 2.0 if any(token in answer for token in ("暂时无法生成可靠回答", "模型服务异常", "请稍后重试")): score -= 5.0 if normalized_message and cls._is_generic_signal_source_message(normalized_message): if "信号源" in question: score += 1.5 if "项目" in answer: score += 1.0 created_at = cls._parse_datetime_like(getattr(row, "created_at", None)) if reference_at is not None and created_at is not None: gap_seconds = abs((reference_at - created_at).total_seconds()) if gap_seconds > 7 * 24 * 3600: score -= 4.0 elif gap_seconds <= 6 * 3600: score += 1.5 elif gap_seconds <= 24 * 3600: score += 0.8 return score async def _load_closure_turns_from_qa_records( self, *, owner_user_id: Optional[int], project_id: Optional[int], captured_at: Optional[datetime], message_text: str, ) -> list[dict[str, str]]: uid = _safe_int(owner_user_id) pid = _safe_int(project_id) if uid is None and pid is None: return [] normalized_message = self._normalize_dialogue_text(message_text, max_len=600) reference_at = self._parse_datetime_like(captured_at) candidates: list[QARecord] = [] conditions: list[Any] = [] if uid is not None and pid is not None: conditions.append(and_(QARecord.asked_by == int(uid), QARecord.project_id == int(pid))) if pid is not None: conditions.append(QARecord.project_id == int(pid)) if uid is not None: conditions.append(QARecord.asked_by == int(uid)) for condition in conditions: stmt = select(QARecord).where( and_( QARecord.tenant_id == self.tenant_id, QARecord.asked_via == "feishu", condition, ) ) if reference_at is not None: stmt = stmt.where(QARecord.created_at <= reference_at) rows = ( await self.db.execute( stmt.order_by(QARecord.created_at.desc(), QARecord.id.desc()).limit(12) ) ).scalars().all() candidates.extend(rows) if rows: break if not candidates: return [] best_row: Optional[QARecord] = None best_score = -999.0 seen_ids: set[int] = set() for row in candidates: row_id = int(getattr(row, "id", 0) or 0) if row_id > 0 and row_id in seen_ids: continue seen_ids.add(row_id) score = self._score_qa_row_for_closure( row=row, normalized_message=normalized_message, reference_at=reference_at, ) if score > best_score: best_score = score best_row = row if best_row is None or best_score < 0.5: return [] question = self._normalize_dialogue_text(best_row.question, max_len=260) answer = self._normalize_dialogue_text(best_row.answer, max_len=420) if not question or not answer: return [] return [ {"role": "user", "text": question}, {"role": "assistant", "text": answer}, ]