1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120from __future__ import annotations
import logging
from typing import Any
import httpx
from src.config import settings
logger = logging.getLogger(__name__)
class FeedbackClient:
"""Fire-and-forget client for the Equalify feedback collection service.
Never blocks or fails the main app. Errors are logged and swallowed.
"""
def __init__(self) -> None:
self._enabled = settings.feedback_enabled and bool(settings.feedback_service_url)
self._base_url = settings.feedback_service_url or ""
self._api_key = settings.feedback_service_api_key.get_secret_value() if settings.feedback_service_api_key else ""
async def _post(self, payload: dict[str, Any] | list[dict[str, Any]]) -> None:
if not self._enabled:
return
try:
async with httpx.AsyncClient(timeout=5.0) as client:
if isinstance(payload, list):
body: dict[str, Any] = {"items": payload}
else:
body = payload
await client.post(
f"{self._base_url}/api/v1/feedback",
json=body,
headers={settings.api_key_header_name: self._api_key},
)
except Exception:
logger.warning("Failed to send feedback to collection service", exc_info=True)
async def upload_document(self, content: bytes, filename: str) -> str | None:
"""Upload a PDF to the feedback service for archival.
Returns the document ref on success, None on failure (fire-and-forget).
"""
if not self._enabled:
return None
try:
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(
f"{self._base_url}/api/v1/documents",
files={"file": (filename, content, "application/pdf")},
headers={settings.api_key_header_name: self._api_key},
)
if resp.status_code == 200:
return resp.json().get("ref")
logger.warning("Document upload returned status %d", resp.status_code)
except Exception:
logger.warning("Failed to upload document to feedback service", exc_info=True)
return None
async def track_edit(
self,
*,
document_id: str | None = None,
document_title: str | None = None,
document_ref: str | None = None,
original_text: str,
corrected_text: str,
page: int | None = None,
section: str | None = None,
category: str = "content",
metadata: dict[str, Any] | None = None,
) -> None:
await self._post({
"document_id": document_id,
"document_title": document_title,
"document_ref": document_ref,
"feedback_type": "user_edit",
"category": category,
"original_text": original_text,
"corrected_text": corrected_text,
"page": page,
"section": section,
"metadata": metadata,
})
async def report_issue(
self,
*,
document_id: str | None = None,
document_title: str | None = None,
document_ref: str | None = None,
category: str = "other",
description: str,
page: int | None = None,
section: str | None = None,
metadata: dict[str, Any] | None = None,
) -> None:
await self._post({
"document_id": document_id,
"document_title": document_title,
"document_ref": document_ref,
"feedback_type": "issue_report",
"category": category,
"description": description,
"page": page,
"section": section,
"metadata": metadata,
})
async def track_edits_batch(self, edits: list[dict[str, Any]]) -> None:
if not edits:
return
await self._post(edits)
# Singleton
feedback_client = FeedbackClient()