안전 이벤트 대시보드 보강

This commit is contained in:
Yun Chan 2026-07-01 12:34:46 +09:00
parent fe2796f05a
commit 51053af536
15 changed files with 1081 additions and 423 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import time
import uuid
import hashlib
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Iterable, Protocol
@ -197,6 +198,27 @@ def _model_payload(value: Any) -> dict[str, Any]:
return {}
def _json_object_payload(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return dict(value)
if not isinstance(value, str):
return {}
text = value.strip()
for _ in range(2):
if not text:
return {}
try:
parsed = json.loads(text)
except json.JSONDecodeError:
return {}
if isinstance(parsed, dict):
return parsed
if not isinstance(parsed, str):
return {}
text = parsed.strip()
return {}
def _masked_excerpt(value: str | None, *, limit: int = 220) -> str | None:
text = (value or "").strip()
if not text:
@ -1023,6 +1045,49 @@ async def ensure_review_tables() -> None:
)
"""
)
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS app.safety_events (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
session_id UUID REFERENCES app.sessions(id) ON DELETE CASCADE,
turn_id UUID REFERENCES app.turns(id) ON DELETE SET NULL,
trigger_type TEXT NOT NULL,
ko_risk_level SMALLINT,
escalated BOOLEAN NOT NULL DEFAULT FALSE,
latency_ms INT,
detail JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_safety_session
ON app.safety_events(session_id);
ALTER TABLE app.safety_events ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_safety_events_select ON app.safety_events;
DROP POLICY IF EXISTS p_safety_events_insert ON app.safety_events;
CREATE POLICY p_safety_events_select
ON app.safety_events FOR SELECT USING (
app.is_ai_context()
OR app.current_role_name() IN ('admin','instructor')
OR EXISTS (
SELECT 1 FROM app.sessions s
WHERE s.id = app.safety_events.session_id
AND s.learner_id = app.current_uid()
)
);
CREATE POLICY p_safety_events_insert
ON app.safety_events FOR INSERT WITH CHECK (
app.is_ai_context()
OR app.current_role_name() IN ('admin','instructor')
OR EXISTS (
SELECT 1 FROM app.sessions s
WHERE s.id = app.safety_events.session_id
AND s.learner_id = app.current_uid()
)
)
"""
)
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS app.session_review_status (
@ -2557,7 +2622,7 @@ async def list_safety_alerts(
"trigger_type": str(row["trigger_type"] or "crisis"),
"ko_risk_level": int(row["ko_risk_level"] or 0),
"escalated": bool(row["escalated"]),
"detail": dict(row["detail"] or {}),
"detail": _json_object_payload(row["detail"]),
"created_at": _iso_dt(row["created_at"]),
}
for row in rows