PMFlow/backend/app/api/ai_logs_api/helpers.py

138 lines
4.5 KiB
Python

"""Shared serializers and query helpers for AI logs APIs."""
from __future__ import annotations
import json
from typing import Any, Optional
from sqlalchemy import and_, func, or_, select, union_all
from ...models import AIProcessingLog, FeishuItem, ProjectSignalLink, ProjectSuggestion, Signal, WorkItem
def normalize_bullets(value: Any) -> list[str]:
if isinstance(value, list):
return [str(x).strip() for x in value if str(x).strip()][:3]
if isinstance(value, str):
raw = value.strip()
if not raw:
return []
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
return [str(x).strip() for x in parsed if str(x).strip()][:3]
except Exception:
pass
return [line.strip(" -•\t") for line in raw.splitlines() if line.strip()][:3]
return []
def safe_json(value: Optional[str]) -> Any:
text = (value or "").strip()
if not text:
return None
try:
return json.loads(text)
except Exception:
return None
def serialize_log_row(row: AIProcessingLog) -> dict[str, Any]:
return {
"id": str(row.id),
"feishu_message_id": row.feishu_message_id,
"feishu_item_id": str(row.feishu_item_id) if row.feishu_item_id is not None else None,
"raw_message": row.raw_message,
"project_score": row.project_score,
"signal_confidence": row.signal_confidence,
"level": row.level,
"routing_decision": row.routing_decision,
"routing_reason": row.routing_reason,
"reasoning_bullets": normalize_bullets(row.reasoning_bullets),
"processing_status": row.processing_status,
"error_message": row.error_message,
"duration_ms": row.duration_ms,
"created_at": row.created_at.isoformat() if row.created_at else None,
}
def serialize_log_detail(row: AIProcessingLog) -> dict[str, Any]:
payload = serialize_log_row(row)
payload["ai_raw_output"] = row.ai_raw_output
payload["ai_raw_output_json"] = safe_json(row.ai_raw_output)
return payload
def apply_project_filter(stmt, *, tenant_id: int, project_id: Optional[int]):
if project_id is None:
return stmt
work_item_message_ids = select(func.distinct(WorkItem.source_message_id)).where(
and_(
WorkItem.tenant_id == tenant_id,
WorkItem.project_id == int(project_id),
WorkItem.source_system == "feishu",
WorkItem.source_message_id.is_not(None),
)
)
direct_items = select(FeishuItem.id.label("item_id")).where(
and_(
FeishuItem.tenant_id == tenant_id,
FeishuItem.matched_project_id == int(project_id),
)
)
signal_matched_items = select(Signal.feishu_item_id.label("item_id")).where(
and_(
Signal.tenant_id == tenant_id,
Signal.matched_project_id == int(project_id),
Signal.feishu_item_id.is_not(None),
)
)
linked_items = (
select(Signal.feishu_item_id.label("item_id"))
.select_from(Signal)
.join(
ProjectSignalLink,
and_(
ProjectSignalLink.tenant_id == tenant_id,
ProjectSignalLink.project_id == int(project_id),
ProjectSignalLink.signal_id == Signal.id,
),
)
.where(
and_(
Signal.tenant_id == tenant_id,
Signal.feishu_item_id.is_not(None),
)
)
)
auto_created_items = select(ProjectSuggestion.feishu_item_id.label("item_id")).where(
and_(
ProjectSuggestion.tenant_id == tenant_id,
ProjectSuggestion.created_project_id == int(project_id),
ProjectSuggestion.feishu_item_id.is_not(None),
)
)
all_item_ids = union_all(
direct_items,
signal_matched_items,
linked_items,
auto_created_items,
).subquery("project_item_ids")
item_ids_stmt = select(func.distinct(all_item_ids.c.item_id)).where(all_item_ids.c.item_id.is_not(None))
message_ids_stmt = select(func.distinct(FeishuItem.feishu_message_id)).where(
and_(
FeishuItem.tenant_id == tenant_id,
FeishuItem.id.in_(item_ids_stmt),
FeishuItem.feishu_message_id.is_not(None),
)
)
return stmt.where(
or_(
AIProcessingLog.feishu_item_id.in_(item_ids_stmt),
AIProcessingLog.feishu_message_id.in_(message_ids_stmt),
AIProcessingLog.feishu_message_id.in_(work_item_message_ids),
)
)