feat: feedback_api — mask_pii + collect_context
This commit is contained in:
parent
00294e3a5b
commit
ec22cc8a1f
2 changed files with 116 additions and 0 deletions
63
scripts/feedback_api.py
Normal file
63
scripts/feedback_api.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""
|
||||
Feedback API — pure Python backend, no Streamlit imports.
|
||||
Called directly from app/feedback.py now; wrappable in a FastAPI route later.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
_ROOT = Path(__file__).parent.parent
|
||||
_EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}")
|
||||
_PHONE_RE = re.compile(r"(\+?1[\s\-.]?)?\(?\d{3}\)?[\s\-.]?\d{3}[\s\-.]?\d{4}")
|
||||
|
||||
|
||||
def mask_pii(text: str) -> str:
|
||||
"""Redact email addresses and phone numbers from text."""
|
||||
text = _EMAIL_RE.sub("[email redacted]", text)
|
||||
text = _PHONE_RE.sub("[phone redacted]", text)
|
||||
return text
|
||||
|
||||
|
||||
def collect_context(page: str) -> dict:
|
||||
"""Collect app context: page, version, tier, LLM backend, OS, timestamp."""
|
||||
# App version from git
|
||||
try:
|
||||
version = subprocess.check_output(
|
||||
["git", "describe", "--tags", "--always"],
|
||||
cwd=_ROOT, text=True, timeout=5,
|
||||
).strip()
|
||||
except Exception:
|
||||
version = "dev"
|
||||
|
||||
# Tier from user.yaml
|
||||
tier = "unknown"
|
||||
try:
|
||||
user = yaml.safe_load((_ROOT / "config" / "user.yaml").read_text()) or {}
|
||||
tier = user.get("tier", "unknown")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# LLM backend from llm.yaml
|
||||
llm_backend = "unknown"
|
||||
try:
|
||||
llm = yaml.safe_load((_ROOT / "config" / "llm.yaml").read_text()) or {}
|
||||
llm_backend = llm.get("provider", "unknown")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"page": page,
|
||||
"version": version,
|
||||
"tier": tier,
|
||||
"llm_backend": llm_backend,
|
||||
"os": platform.platform(),
|
||||
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
53
tests/test_feedback_api.py
Normal file
53
tests/test_feedback_api.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""Tests for the feedback API backend."""
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ── mask_pii ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_mask_pii_email():
|
||||
from scripts.feedback_api import mask_pii
|
||||
assert mask_pii("contact foo@bar.com please") == "contact [email redacted] please"
|
||||
|
||||
|
||||
def test_mask_pii_phone_dashes():
|
||||
from scripts.feedback_api import mask_pii
|
||||
assert mask_pii("call 555-123-4567 now") == "call [phone redacted] now"
|
||||
|
||||
|
||||
def test_mask_pii_phone_parens():
|
||||
from scripts.feedback_api import mask_pii
|
||||
assert mask_pii("(555) 867-5309") == "[phone redacted]"
|
||||
|
||||
|
||||
def test_mask_pii_clean_text():
|
||||
from scripts.feedback_api import mask_pii
|
||||
assert mask_pii("no sensitive data here") == "no sensitive data here"
|
||||
|
||||
|
||||
def test_mask_pii_multiple_emails():
|
||||
from scripts.feedback_api import mask_pii
|
||||
result = mask_pii("a@b.com and c@d.com")
|
||||
assert result == "[email redacted] and [email redacted]"
|
||||
|
||||
|
||||
# ── collect_context ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_collect_context_required_keys():
|
||||
from scripts.feedback_api import collect_context
|
||||
ctx = collect_context("Home")
|
||||
for key in ("page", "version", "tier", "llm_backend", "os", "timestamp"):
|
||||
assert key in ctx, f"missing key: {key}"
|
||||
|
||||
|
||||
def test_collect_context_page_value():
|
||||
from scripts.feedback_api import collect_context
|
||||
ctx = collect_context("MyPage")
|
||||
assert ctx["page"] == "MyPage"
|
||||
|
||||
|
||||
def test_collect_context_timestamp_is_utc():
|
||||
from scripts.feedback_api import collect_context
|
||||
ctx = collect_context("X")
|
||||
assert ctx["timestamp"].endswith("Z")
|
||||
Loading…
Reference in a new issue