PMFlow/backend/app/services/ai_pmo_kernel_mixins_closur...

155 lines
5.8 KiB
Python

"""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},
]