spanforge 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spanforge/__init__.py +815 -0
- spanforge/_ansi.py +93 -0
- spanforge/_batch_exporter.py +409 -0
- spanforge/_cli.py +2094 -0
- spanforge/_cli_audit.py +639 -0
- spanforge/_cli_compliance.py +711 -0
- spanforge/_cli_cost.py +243 -0
- spanforge/_cli_ops.py +791 -0
- spanforge/_cli_phase11.py +356 -0
- spanforge/_hooks.py +337 -0
- spanforge/_server.py +1708 -0
- spanforge/_span.py +1036 -0
- spanforge/_store.py +288 -0
- spanforge/_stream.py +664 -0
- spanforge/_trace.py +335 -0
- spanforge/_tracer.py +254 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +469 -0
- spanforge/auto.py +464 -0
- spanforge/baseline.py +335 -0
- spanforge/cache.py +635 -0
- spanforge/compliance.py +325 -0
- spanforge/config.py +532 -0
- spanforge/consent.py +228 -0
- spanforge/consumer.py +377 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1254 -0
- spanforge/cost.py +600 -0
- spanforge/debug.py +548 -0
- spanforge/deprecations.py +205 -0
- spanforge/drift.py +482 -0
- spanforge/egress.py +58 -0
- spanforge/eval.py +648 -0
- spanforge/event.py +1064 -0
- spanforge/exceptions.py +240 -0
- spanforge/explain.py +178 -0
- spanforge/export/__init__.py +69 -0
- spanforge/export/append_only.py +337 -0
- spanforge/export/cloud.py +357 -0
- spanforge/export/datadog.py +497 -0
- spanforge/export/grafana.py +320 -0
- spanforge/export/jsonl.py +195 -0
- spanforge/export/openinference.py +158 -0
- spanforge/export/otel_bridge.py +294 -0
- spanforge/export/otlp.py +811 -0
- spanforge/export/otlp_bridge.py +233 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/siem_schema.py +98 -0
- spanforge/export/siem_splunk.py +264 -0
- spanforge/export/siem_syslog.py +212 -0
- spanforge/export/webhook.py +299 -0
- spanforge/exporters/__init__.py +30 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/exporters/sqlite.py +142 -0
- spanforge/gate.py +1150 -0
- spanforge/governance.py +181 -0
- spanforge/hitl.py +295 -0
- spanforge/http.py +187 -0
- spanforge/inspect.py +427 -0
- spanforge/integrations/__init__.py +45 -0
- spanforge/integrations/_pricing.py +280 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/azure_openai.py +133 -0
- spanforge/integrations/bedrock.py +292 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +351 -0
- spanforge/integrations/groq.py +442 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/langgraph.py +306 -0
- spanforge/integrations/llamaindex.py +373 -0
- spanforge/integrations/ollama.py +287 -0
- spanforge/integrations/openai.py +368 -0
- spanforge/integrations/together.py +483 -0
- spanforge/io.py +214 -0
- spanforge/lint.py +322 -0
- spanforge/metrics.py +417 -0
- spanforge/metrics_export.py +343 -0
- spanforge/migrate.py +402 -0
- spanforge/model_registry.py +278 -0
- spanforge/models.py +389 -0
- spanforge/namespaces/__init__.py +254 -0
- spanforge/namespaces/audit.py +256 -0
- spanforge/namespaces/cache.py +237 -0
- spanforge/namespaces/chain.py +77 -0
- spanforge/namespaces/confidence.py +72 -0
- spanforge/namespaces/consent.py +92 -0
- spanforge/namespaces/cost.py +179 -0
- spanforge/namespaces/decision.py +143 -0
- spanforge/namespaces/diff.py +157 -0
- spanforge/namespaces/drift.py +80 -0
- spanforge/namespaces/eval_.py +251 -0
- spanforge/namespaces/feedback.py +241 -0
- spanforge/namespaces/fence.py +193 -0
- spanforge/namespaces/guard.py +105 -0
- spanforge/namespaces/hitl.py +91 -0
- spanforge/namespaces/latency.py +72 -0
- spanforge/namespaces/prompt.py +190 -0
- spanforge/namespaces/redact.py +173 -0
- spanforge/namespaces/retrieval.py +379 -0
- spanforge/namespaces/runtime_governance.py +494 -0
- spanforge/namespaces/template.py +208 -0
- spanforge/namespaces/tool_call.py +77 -0
- spanforge/namespaces/trace.py +1029 -0
- spanforge/normalizer.py +171 -0
- spanforge/plugins.py +82 -0
- spanforge/presidio_backend.py +349 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +418 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +914 -0
- spanforge/regression.py +192 -0
- spanforge/runtime_policy.py +159 -0
- spanforge/sampling.py +511 -0
- spanforge/schema.py +183 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/sdk/__init__.py +625 -0
- spanforge/sdk/_base.py +584 -0
- spanforge/sdk/_base.pyi +71 -0
- spanforge/sdk/_exceptions.py +1096 -0
- spanforge/sdk/_types.py +2184 -0
- spanforge/sdk/alert.py +1514 -0
- spanforge/sdk/alert.pyi +56 -0
- spanforge/sdk/audit.py +1196 -0
- spanforge/sdk/audit.pyi +67 -0
- spanforge/sdk/cec.py +1215 -0
- spanforge/sdk/cec.pyi +37 -0
- spanforge/sdk/config.py +641 -0
- spanforge/sdk/config.pyi +55 -0
- spanforge/sdk/enterprise.py +714 -0
- spanforge/sdk/enterprise.pyi +79 -0
- spanforge/sdk/explain.py +170 -0
- spanforge/sdk/fallback.py +432 -0
- spanforge/sdk/feedback.py +351 -0
- spanforge/sdk/gate.py +874 -0
- spanforge/sdk/gate.pyi +51 -0
- spanforge/sdk/identity.py +2114 -0
- spanforge/sdk/identity.pyi +47 -0
- spanforge/sdk/lineage.py +175 -0
- spanforge/sdk/observe.py +1065 -0
- spanforge/sdk/observe.pyi +50 -0
- spanforge/sdk/operator.py +338 -0
- spanforge/sdk/pii.py +1473 -0
- spanforge/sdk/pii.pyi +119 -0
- spanforge/sdk/pipelines.py +458 -0
- spanforge/sdk/pipelines.pyi +39 -0
- spanforge/sdk/policy.py +930 -0
- spanforge/sdk/rag.py +594 -0
- spanforge/sdk/rbac.py +280 -0
- spanforge/sdk/registry.py +430 -0
- spanforge/sdk/registry.pyi +46 -0
- spanforge/sdk/scope.py +279 -0
- spanforge/sdk/secrets.py +293 -0
- spanforge/sdk/secrets.pyi +25 -0
- spanforge/sdk/security.py +560 -0
- spanforge/sdk/security.pyi +57 -0
- spanforge/sdk/trust.py +472 -0
- spanforge/sdk/trust.pyi +41 -0
- spanforge/secrets.py +799 -0
- spanforge/signing.py +1179 -0
- spanforge/stats.py +100 -0
- spanforge/stream.py +560 -0
- spanforge/testing.py +378 -0
- spanforge/testing_mocks.py +1052 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +300 -0
- spanforge/validate.py +379 -0
- spanforge-1.0.0.dist-info/METADATA +1509 -0
- spanforge-1.0.0.dist-info/RECORD +174 -0
- spanforge-1.0.0.dist-info/WHEEL +4 -0
- spanforge-1.0.0.dist-info/entry_points.txt +5 -0
- spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""spanforge.sdk.feedback — SpanForge sf-feedback User Feedback client (Phase 13).
|
|
2
|
+
|
|
3
|
+
Implements FB-001 through FB-006: collecting, storing, and surfacing user
|
|
4
|
+
feedback for LLM responses, with optional T.R.U.S.T. dimension linkage.
|
|
5
|
+
|
|
6
|
+
Architecture
|
|
7
|
+
------------
|
|
8
|
+
* :meth:`submit` records a ``llm.feedback.submitted`` event.
|
|
9
|
+
* :meth:`get_feedback` returns feedback records for a given session.
|
|
10
|
+
* :meth:`get_summary` returns an aggregated :class:`FeedbackSummaryPayload`.
|
|
11
|
+
* :meth:`link_to_trust` links feedback to a T.R.U.S.T. dimension score
|
|
12
|
+
adjustment.
|
|
13
|
+
* :meth:`get_status` returns service health statistics.
|
|
14
|
+
|
|
15
|
+
Raw user text (free-text comments) is **never stored**. Only SHA-256 hashes
|
|
16
|
+
are retained. User identifiers are similarly hashed before storage.
|
|
17
|
+
|
|
18
|
+
Security requirements
|
|
19
|
+
---------------------
|
|
20
|
+
* All plaintext comment and user ID values are hashed with SHA-256 before
|
|
21
|
+
recording.
|
|
22
|
+
* Thread-safety: all in-process state uses locks.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import logging
|
|
29
|
+
import threading
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from spanforge.namespaces.feedback import (
|
|
34
|
+
FeedbackRating,
|
|
35
|
+
FeedbackSubmittedPayload,
|
|
36
|
+
FeedbackSummaryPayload,
|
|
37
|
+
)
|
|
38
|
+
from spanforge.sdk._base import SFClientConfig, SFServiceClient
|
|
39
|
+
|
|
40
|
+
__all__ = ["SFFeedbackClient"]
|
|
41
|
+
|
|
42
|
+
_log = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Status dataclass
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class FeedbackStatusInfo:
|
|
52
|
+
"""sf-feedback service status.
|
|
53
|
+
|
|
54
|
+
Returned by :meth:`SFFeedbackClient.get_status`.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
status: ``"ok"`` or ``"degraded"``.
|
|
58
|
+
total_submitted: Total feedback submissions recorded in this process.
|
|
59
|
+
sessions_tracked: Number of distinct session IDs tracked.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
status: str
|
|
63
|
+
total_submitted: int
|
|
64
|
+
sessions_tracked: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Client
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SFFeedbackClient(SFServiceClient):
|
|
73
|
+
"""SpanForge User Feedback service client.
|
|
74
|
+
|
|
75
|
+
Provides structured user feedback collection for LLM interactions,
|
|
76
|
+
supporting thumbs, star, Likert, and free-text modalities.
|
|
77
|
+
|
|
78
|
+
Example usage::
|
|
79
|
+
|
|
80
|
+
from spanforge.sdk import sf_feedback
|
|
81
|
+
|
|
82
|
+
# Submit thumbs-up feedback
|
|
83
|
+
fb_id = sf_feedback.submit(
|
|
84
|
+
session_id="sess-abc",
|
|
85
|
+
trace_id="trace-xyz",
|
|
86
|
+
rating="thumbs_up",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Submit a free-text comment (text is hashed, never stored)
|
|
90
|
+
fb_id = sf_feedback.submit(
|
|
91
|
+
session_id="sess-abc",
|
|
92
|
+
trace_id="trace-xyz",
|
|
93
|
+
rating="free_text",
|
|
94
|
+
comment="The answer was very helpful.",
|
|
95
|
+
user_id="user-42",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Get session summary
|
|
99
|
+
summary = sf_feedback.get_summary("sess-abc")
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, config: SFClientConfig) -> None:
|
|
103
|
+
super().__init__(config, service_name="feedback")
|
|
104
|
+
self._lock = threading.Lock()
|
|
105
|
+
# session_id → list of FeedbackSubmittedPayload dicts
|
|
106
|
+
self._store: dict[str, list[dict[str, Any]]] = {}
|
|
107
|
+
self._total_submitted: int = 0
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# FB-001: submit
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def submit(
|
|
114
|
+
self,
|
|
115
|
+
session_id: str,
|
|
116
|
+
trace_id: str,
|
|
117
|
+
rating: str | FeedbackRating,
|
|
118
|
+
*,
|
|
119
|
+
comment: str | None = None,
|
|
120
|
+
user_id: str | None = None,
|
|
121
|
+
source: str = "api",
|
|
122
|
+
metadata: dict[str, Any] | None = None,
|
|
123
|
+
linked_trust_dimension: str | None = None,
|
|
124
|
+
) -> str:
|
|
125
|
+
"""Submit user feedback for an LLM response.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
session_id: Session or conversation the feedback applies to.
|
|
129
|
+
trace_id: Trace ID of the specific LLM call being rated.
|
|
130
|
+
rating: A :class:`~spanforge.namespaces.feedback.FeedbackRating`
|
|
131
|
+
value or its string equivalent (e.g. ``"thumbs_up"``).
|
|
132
|
+
comment: Optional free-text comment. The text is
|
|
133
|
+
hashed with SHA-256; raw text is NOT stored.
|
|
134
|
+
user_id: Optional user identifier. Hashed with
|
|
135
|
+
SHA-256 before storage.
|
|
136
|
+
source: Feedback channel label (default ``"api"``).
|
|
137
|
+
metadata: Arbitrary key-value metadata.
|
|
138
|
+
linked_trust_dimension: Optional T.R.U.S.T. dimension to link
|
|
139
|
+
this feedback to (e.g. ``"reliability"``).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The unique ``feedback_id`` (ULID) for the submitted record.
|
|
143
|
+
"""
|
|
144
|
+
from spanforge.ulid import generate as _ulid
|
|
145
|
+
|
|
146
|
+
rating_enum = FeedbackRating(rating) if isinstance(rating, str) else rating
|
|
147
|
+
|
|
148
|
+
comment_hash = ""
|
|
149
|
+
if comment:
|
|
150
|
+
comment_hash = hashlib.sha256(comment.encode("utf-8")).hexdigest()
|
|
151
|
+
|
|
152
|
+
user_id_hash = ""
|
|
153
|
+
if user_id:
|
|
154
|
+
user_id_hash = hashlib.sha256(user_id.encode("utf-8")).hexdigest()
|
|
155
|
+
|
|
156
|
+
feedback_id = _ulid()
|
|
157
|
+
payload = FeedbackSubmittedPayload(
|
|
158
|
+
feedback_id=feedback_id,
|
|
159
|
+
session_id=session_id,
|
|
160
|
+
trace_id=trace_id,
|
|
161
|
+
rating=rating_enum,
|
|
162
|
+
comment_hash=comment_hash,
|
|
163
|
+
user_id_hash=user_id_hash,
|
|
164
|
+
source=source,
|
|
165
|
+
metadata=metadata or {},
|
|
166
|
+
linked_trust_dimension=linked_trust_dimension,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
record = payload.to_dict()
|
|
170
|
+
|
|
171
|
+
with self._lock:
|
|
172
|
+
if session_id not in self._store:
|
|
173
|
+
self._store[session_id] = []
|
|
174
|
+
self._store[session_id].append(record)
|
|
175
|
+
self._total_submitted += 1
|
|
176
|
+
|
|
177
|
+
self._emit_local("llm.feedback.submitted", record, session_id=session_id)
|
|
178
|
+
return feedback_id
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
# FB-002: get_feedback
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def get_feedback(
|
|
185
|
+
self,
|
|
186
|
+
session_id: str,
|
|
187
|
+
*,
|
|
188
|
+
rating_filter: str | FeedbackRating | None = None,
|
|
189
|
+
) -> list[dict[str, Any]]:
|
|
190
|
+
"""Return feedback records for *session_id*.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
session_id: Session to query.
|
|
194
|
+
rating_filter: Optional rating type to filter by.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of feedback record dicts (in submission order).
|
|
198
|
+
"""
|
|
199
|
+
with self._lock:
|
|
200
|
+
records = list(self._store.get(session_id, []))
|
|
201
|
+
|
|
202
|
+
if rating_filter is not None:
|
|
203
|
+
rf = FeedbackRating(rating_filter) if isinstance(rating_filter, str) else rating_filter
|
|
204
|
+
records = [r for r in records if r.get("rating") == rf.value]
|
|
205
|
+
|
|
206
|
+
return records
|
|
207
|
+
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
# FB-003: get_summary
|
|
210
|
+
# ------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def get_summary(self, session_id: str) -> FeedbackSummaryPayload:
|
|
213
|
+
"""Return an aggregated feedback summary for *session_id*.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
session_id: Session to summarise.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
A :class:`~spanforge.namespaces.feedback.FeedbackSummaryPayload`.
|
|
220
|
+
"""
|
|
221
|
+
records = self.get_feedback(session_id)
|
|
222
|
+
|
|
223
|
+
thumbs_up = sum(1 for r in records if r.get("rating") == FeedbackRating.THUMBS_UP.value)
|
|
224
|
+
thumbs_down = sum(1 for r in records if r.get("rating") == FeedbackRating.THUMBS_DOWN.value)
|
|
225
|
+
free_text = sum(1 for r in records if r.get("rating") == FeedbackRating.FREE_TEXT.value)
|
|
226
|
+
|
|
227
|
+
star_values = [
|
|
228
|
+
FeedbackRating(r["rating"]).numeric_value()
|
|
229
|
+
for r in records
|
|
230
|
+
if r.get("rating", "").startswith("star_")
|
|
231
|
+
]
|
|
232
|
+
star_values_clean = [v for v in star_values if v is not None]
|
|
233
|
+
|
|
234
|
+
likert_values = [
|
|
235
|
+
FeedbackRating(r["rating"]).numeric_value()
|
|
236
|
+
for r in records
|
|
237
|
+
if r.get("rating", "").startswith("likert_")
|
|
238
|
+
]
|
|
239
|
+
likert_values_clean = [v for v in likert_values if v is not None]
|
|
240
|
+
|
|
241
|
+
# Numeric values are on [0, 1]. Convert star/likert back to 1–5 scale.
|
|
242
|
+
avg_star = (
|
|
243
|
+
round(sum(star_values_clean) / len(star_values_clean) * 4 + 1, 2)
|
|
244
|
+
if star_values_clean
|
|
245
|
+
else None
|
|
246
|
+
)
|
|
247
|
+
avg_likert = (
|
|
248
|
+
round(sum(likert_values_clean) / len(likert_values_clean) * 4 + 1, 2)
|
|
249
|
+
if likert_values_clean
|
|
250
|
+
else None
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Positive rate: numeric ratings above 0.5 are "positive"
|
|
254
|
+
all_numeric: list[float] = []
|
|
255
|
+
for r in records:
|
|
256
|
+
try:
|
|
257
|
+
v = FeedbackRating(r["rating"]).numeric_value()
|
|
258
|
+
if v is not None:
|
|
259
|
+
all_numeric.append(v)
|
|
260
|
+
except ValueError:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
positive_rate = (
|
|
264
|
+
sum(1 for v in all_numeric if v >= 0.5) / len(all_numeric) if all_numeric else 0.0
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return FeedbackSummaryPayload(
|
|
268
|
+
session_id=session_id,
|
|
269
|
+
total_feedback=len(records),
|
|
270
|
+
thumbs_up_count=thumbs_up,
|
|
271
|
+
thumbs_down_count=thumbs_down,
|
|
272
|
+
avg_star_rating=avg_star,
|
|
273
|
+
avg_likert_score=avg_likert,
|
|
274
|
+
free_text_count=free_text,
|
|
275
|
+
positive_rate=round(positive_rate, 4),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# ------------------------------------------------------------------
|
|
279
|
+
# FB-004: link_to_trust
|
|
280
|
+
# ------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
def link_to_trust(
|
|
283
|
+
self,
|
|
284
|
+
feedback_id: str,
|
|
285
|
+
trust_dimension: str,
|
|
286
|
+
*,
|
|
287
|
+
weight: float = 0.1,
|
|
288
|
+
) -> bool:
|
|
289
|
+
"""Link a feedback record to a T.R.U.S.T. dimension score adjustment.
|
|
290
|
+
|
|
291
|
+
This emits a ``llm.feedback.trust_linked`` event that the T.R.U.S.T.
|
|
292
|
+
service can consume to adjust dimension scores based on explicit user
|
|
293
|
+
signal.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
feedback_id: ULID of the feedback record to link.
|
|
297
|
+
trust_dimension: T.R.U.S.T. dimension to adjust
|
|
298
|
+
(``"transparency"``, ``"reliability"``,
|
|
299
|
+
``"user_trust"``, ``"security"``,
|
|
300
|
+
``"traceability"``).
|
|
301
|
+
weight: Adjustment weight in [0.0, 1.0] (default 0.1).
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
``True`` if the link event was emitted successfully.
|
|
305
|
+
"""
|
|
306
|
+
_VALID_DIMENSIONS = frozenset(
|
|
307
|
+
{"transparency", "reliability", "user_trust", "security", "traceability"}
|
|
308
|
+
)
|
|
309
|
+
if trust_dimension not in _VALID_DIMENSIONS:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"link_to_trust: trust_dimension must be one of {sorted(_VALID_DIMENSIONS)}"
|
|
312
|
+
)
|
|
313
|
+
if not (0.0 <= weight <= 1.0):
|
|
314
|
+
raise ValueError(f"link_to_trust: weight must be in [0, 1]; got {weight}")
|
|
315
|
+
|
|
316
|
+
self._emit_local(
|
|
317
|
+
"llm.feedback.trust_linked",
|
|
318
|
+
{
|
|
319
|
+
"feedback_id": feedback_id,
|
|
320
|
+
"trust_dimension": trust_dimension,
|
|
321
|
+
"weight": weight,
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
return True
|
|
325
|
+
|
|
326
|
+
# ------------------------------------------------------------------
|
|
327
|
+
# FB-005: get_status
|
|
328
|
+
# ------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
def get_status(self) -> FeedbackStatusInfo:
|
|
331
|
+
"""Return service health and submission statistics."""
|
|
332
|
+
with self._lock:
|
|
333
|
+
return FeedbackStatusInfo(
|
|
334
|
+
status="ok",
|
|
335
|
+
total_submitted=self._total_submitted,
|
|
336
|
+
sessions_tracked=len(self._store),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# ------------------------------------------------------------------
|
|
340
|
+
# Internal helpers
|
|
341
|
+
# ------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
def _emit_local(
|
|
344
|
+
self,
|
|
345
|
+
event_type: str,
|
|
346
|
+
payload: dict[str, Any],
|
|
347
|
+
*,
|
|
348
|
+
session_id: str = "",
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Emit a feedback event locally or forward to remote endpoint."""
|
|
351
|
+
_log.debug("sf-feedback emit %s session=%s", event_type, session_id)
|