Spaces:
Running
Running
deploy: phase 3 BYOK backend (Dockerfile.hf, FastAPI on 7860)
Browse files- interfaces/api.py +39 -0
- utils/audit.py +37 -0
interfaces/api.py
CHANGED
|
@@ -779,6 +779,45 @@ if _FASTAPI_AVAILABLE:
|
|
| 779 |
"items": [e.model_dump(mode="json") for e in entries],
|
| 780 |
}
|
| 781 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
# ββ BYOK upload endpoints ββββββββββββββββββββββββββββββββββββββββ
|
| 783 |
from fastapi import File, UploadFile
|
| 784 |
from qdrant_client.models import FieldCondition, Filter, MatchValue
|
|
|
|
| 779 |
"items": [e.model_dump(mode="json") for e in entries],
|
| 780 |
}
|
| 781 |
|
| 782 |
+
# ββ BYOK answer feedback (π/π β hash-chained audit row) βββββββββ
|
| 783 |
+
class _ByokFeedbackBody(_ByokBaseModel):
|
| 784 |
+
"""Public-demo feedback payload β a rating on the last answer."""
|
| 785 |
+
|
| 786 |
+
rating: str
|
| 787 |
+
query: str = ""
|
| 788 |
+
answer_summary: str = ""
|
| 789 |
+
|
| 790 |
+
@app.post("/byok/feedback", tags=["byok"])
|
| 791 |
+
async def byok_feedback_endpoint(
|
| 792 |
+
request: _FastApiRequest,
|
| 793 |
+
body: _ByokFeedbackBody,
|
| 794 |
+
creds: Annotated[ByokCreds, Depends(extract_byok)],
|
| 795 |
+
) -> dict:
|
| 796 |
+
"""Record a thumbs-up/down on an answer as a session-scoped audit row.
|
| 797 |
+
|
| 798 |
+
The rating lands on the same SHA-256 hash chain as every query, so it
|
| 799 |
+
is itself tamper-evident and shows up in ``/byok/audit``. No LLM call,
|
| 800 |
+
no throttle β it is a cheap write.
|
| 801 |
+
"""
|
| 802 |
+
rating = (body.rating or "").strip().lower()
|
| 803 |
+
if rating not in ("up", "down"):
|
| 804 |
+
raise HTTPException(
|
| 805 |
+
status.HTTP_400_BAD_REQUEST,
|
| 806 |
+
detail={"reason": "rating must be 'up' or 'down'"},
|
| 807 |
+
)
|
| 808 |
+
try:
|
| 809 |
+
audit_logger.log_feedback(
|
| 810 |
+
user_id=f"demo-{creds.session_id}",
|
| 811 |
+
org_id=_DEMO_ORG_ID,
|
| 812 |
+
rating=rating,
|
| 813 |
+
query=body.query,
|
| 814 |
+
answer_summary=(body.answer_summary or "")[:200],
|
| 815 |
+
persona=creds.demo_persona or "anonymous",
|
| 816 |
+
)
|
| 817 |
+
except Exception as exc: # pragma: no cover -- defensive
|
| 818 |
+
logger.warning("byok_feedback_persist_failed", error=str(exc))
|
| 819 |
+
return {"session_id": creds.session_id, "rating": rating, "recorded": True}
|
| 820 |
+
|
| 821 |
# ββ BYOK upload endpoints ββββββββββββββββββββββββββββββββββββββββ
|
| 822 |
from fastapi import File, UploadFile
|
| 823 |
from qdrant_client.models import FieldCondition, Filter, MatchValue
|
utils/audit.py
CHANGED
|
@@ -261,6 +261,43 @@ class AuditLogger:
|
|
| 261 |
)
|
| 262 |
return entry
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
def log_security_event(
|
| 265 |
self,
|
| 266 |
*,
|
|
|
|
| 261 |
)
|
| 262 |
return entry
|
| 263 |
|
| 264 |
+
def log_feedback(
|
| 265 |
+
self,
|
| 266 |
+
*,
|
| 267 |
+
user_id: str,
|
| 268 |
+
org_id: str = "",
|
| 269 |
+
rating: str,
|
| 270 |
+
query: str = "",
|
| 271 |
+
**kwargs: Any,
|
| 272 |
+
) -> AuditEntry:
|
| 273 |
+
"""Record a user thumbs-up/down on an answer.
|
| 274 |
+
|
| 275 |
+
Lands on the same SHA-256 hash chain as every other event (PII-redacted
|
| 276 |
+
before persistence), so feedback is itself tamper-evident and exportable
|
| 277 |
+
via ``/byok/audit``. ``rating`` is normalised to ``up`` / ``down``.
|
| 278 |
+
|
| 279 |
+
Args:
|
| 280 |
+
user_id: Identifier of the user giving feedback.
|
| 281 |
+
org_id: Organization identifier.
|
| 282 |
+
rating: ``up`` or ``down``.
|
| 283 |
+
query: The question the rated answer responded to.
|
| 284 |
+
**kwargs: Extra metadata (e.g. answer_summary).
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
The persisted AuditEntry.
|
| 288 |
+
"""
|
| 289 |
+
entry = AuditEntry(
|
| 290 |
+
action="feedback",
|
| 291 |
+
user_id=user_id,
|
| 292 |
+
org_id=org_id,
|
| 293 |
+
details={"rating": rating, "query": query},
|
| 294 |
+
status="success",
|
| 295 |
+
metadata=kwargs,
|
| 296 |
+
)
|
| 297 |
+
self._persist(entry)
|
| 298 |
+
_audit_log.info("feedback_recorded", user_id=user_id, rating=rating)
|
| 299 |
+
return entry
|
| 300 |
+
|
| 301 |
def log_security_event(
|
| 302 |
self,
|
| 303 |
*,
|