138 lines
4.5 KiB
Python
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),
|
|
)
|
|
)
|