안전 이벤트 대시보드 보강
This commit is contained in:
parent
fe2796f05a
commit
51053af536
15 changed files with 1081 additions and 423 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue