155 lines
5.8 KiB
Python
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},
|
|
]
|