"""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), ) )