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
spanforge/sdk/pii.pyi
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Type stubs for spanforge.sdk.pii (DX-001)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from spanforge.event import Event
|
|
10
|
+
from spanforge.pii import Redactable, RedactionPolicy
|
|
11
|
+
from spanforge.sdk._base import SFClientConfig, SFServiceClient
|
|
12
|
+
from spanforge.sdk._types import (
|
|
13
|
+
DSARExport,
|
|
14
|
+
ErasureReceipt,
|
|
15
|
+
PIIAnonymisedResult,
|
|
16
|
+
PIIHeatMapEntry,
|
|
17
|
+
PIIPipelineResult,
|
|
18
|
+
PIIStatusInfo,
|
|
19
|
+
PIITextScanResult,
|
|
20
|
+
SafeHarborResult,
|
|
21
|
+
SFPIIAnonymizeResult,
|
|
22
|
+
SFPIIRedactResult,
|
|
23
|
+
SFPIIScanResult,
|
|
24
|
+
TrainingDataPIIReport,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
class SFPIIClient(SFServiceClient):
|
|
28
|
+
def __init__(self, config: SFClientConfig) -> None: ...
|
|
29
|
+
def scan(
|
|
30
|
+
self,
|
|
31
|
+
payload: dict[str, Any],
|
|
32
|
+
*,
|
|
33
|
+
extra_patterns: dict[str, re.Pattern[str]] | None = None,
|
|
34
|
+
max_depth: int = 10,
|
|
35
|
+
) -> SFPIIScanResult: ...
|
|
36
|
+
def redact(
|
|
37
|
+
self,
|
|
38
|
+
event: Event,
|
|
39
|
+
*,
|
|
40
|
+
policy: RedactionPolicy | None = None,
|
|
41
|
+
) -> SFPIIRedactResult: ...
|
|
42
|
+
def contains_pii(
|
|
43
|
+
self,
|
|
44
|
+
event: Event,
|
|
45
|
+
*,
|
|
46
|
+
scan_raw: bool = True,
|
|
47
|
+
) -> bool: ...
|
|
48
|
+
def assert_redacted(
|
|
49
|
+
self,
|
|
50
|
+
event: Event,
|
|
51
|
+
*,
|
|
52
|
+
context: str = "",
|
|
53
|
+
scan_raw: bool = True,
|
|
54
|
+
) -> None: ...
|
|
55
|
+
def anonymize(
|
|
56
|
+
self,
|
|
57
|
+
text: str,
|
|
58
|
+
*,
|
|
59
|
+
extra_patterns: dict[str, re.Pattern[str]] | None = None,
|
|
60
|
+
) -> SFPIIAnonymizeResult: ...
|
|
61
|
+
def wrap(
|
|
62
|
+
self,
|
|
63
|
+
value: object,
|
|
64
|
+
sensitivity: str,
|
|
65
|
+
pii_types: frozenset[str] = ...,
|
|
66
|
+
) -> Redactable: ...
|
|
67
|
+
def make_policy(
|
|
68
|
+
self,
|
|
69
|
+
*,
|
|
70
|
+
min_sensitivity: str = "pii",
|
|
71
|
+
redacted_by: str = "policy:sf-pii",
|
|
72
|
+
replacement_template: str = "[REDACTED:{sensitivity}]",
|
|
73
|
+
) -> RedactionPolicy: ...
|
|
74
|
+
def scan_text(
|
|
75
|
+
self,
|
|
76
|
+
text: str,
|
|
77
|
+
*,
|
|
78
|
+
language: str = "en",
|
|
79
|
+
score_threshold: float = 0.5,
|
|
80
|
+
) -> PIITextScanResult: ...
|
|
81
|
+
def anonymise(
|
|
82
|
+
self,
|
|
83
|
+
payload: dict[str, Any],
|
|
84
|
+
*,
|
|
85
|
+
max_depth: int = 10,
|
|
86
|
+
) -> PIIAnonymisedResult: ...
|
|
87
|
+
def scan_batch(
|
|
88
|
+
self,
|
|
89
|
+
texts: list[str],
|
|
90
|
+
*,
|
|
91
|
+
language: str = "en",
|
|
92
|
+
score_threshold: float = 0.5,
|
|
93
|
+
max_workers: int = 8,
|
|
94
|
+
) -> list[PIITextScanResult]: ...
|
|
95
|
+
def apply_pipeline_action(
|
|
96
|
+
self,
|
|
97
|
+
text: str,
|
|
98
|
+
*,
|
|
99
|
+
action: str = "flag",
|
|
100
|
+
threshold: float = 0.85,
|
|
101
|
+
language: str = "en",
|
|
102
|
+
) -> PIIPipelineResult: ...
|
|
103
|
+
def get_status(self) -> PIIStatusInfo: ...
|
|
104
|
+
def erase_subject(self, subject_id: str, project_id: str) -> ErasureReceipt: ...
|
|
105
|
+
def export_subject_data(self, subject_id: str, project_id: str) -> DSARExport: ...
|
|
106
|
+
def safe_harbor_deidentify(self, text: str) -> SafeHarborResult: ...
|
|
107
|
+
def audit_training_data(
|
|
108
|
+
self,
|
|
109
|
+
dataset_path: str | Path,
|
|
110
|
+
*,
|
|
111
|
+
max_records: int = 100_000,
|
|
112
|
+
) -> TrainingDataPIIReport: ...
|
|
113
|
+
def get_pii_stats(
|
|
114
|
+
self,
|
|
115
|
+
project_id: str,
|
|
116
|
+
*,
|
|
117
|
+
entity_type: str | None = None,
|
|
118
|
+
days: int = 30,
|
|
119
|
+
) -> list[PIIHeatMapEntry]: ...
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""spanforge.sdk.pipelines — HallucCheck pipeline integration points (Phase 10).
|
|
2
|
+
|
|
3
|
+
Implements TRS-010 through TRS-014: the five HallucCheck ↔ SpanForge
|
|
4
|
+
pipeline integration touch-points.
|
|
5
|
+
|
|
6
|
+
Each pipeline function orchestrates calls across multiple SpanForge services
|
|
7
|
+
(sf_pii, sf_secrets, sf_audit, sf_observe, sf_alert, sf_gate, sf_cec) and
|
|
8
|
+
returns a :class:`~spanforge.sdk._types.PipelineResult`.
|
|
9
|
+
|
|
10
|
+
Pipelines
|
|
11
|
+
---------
|
|
12
|
+
* ``score_pipeline`` — TRS-010: Score + PII + secrets + observe + audit
|
|
13
|
+
* ``bias_pipeline`` — TRS-011: Bias report + alert + anonymise
|
|
14
|
+
* ``monitor_pipeline`` — TRS-012: Drift events + alert + OTel export
|
|
15
|
+
* ``risk_pipeline`` — TRS-013: PRRI + alert + gate + CEC
|
|
16
|
+
* ``benchmark_pipeline``— TRS-014: Benchmark run + alert + anonymise
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from spanforge.sdk._exceptions import SFPipelineError
|
|
26
|
+
from spanforge.sdk._types import PipelineResult
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"benchmark_pipeline",
|
|
30
|
+
"bias_pipeline",
|
|
31
|
+
"monitor_pipeline",
|
|
32
|
+
"risk_pipeline",
|
|
33
|
+
"score_pipeline",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
_log = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _utc_now_iso() -> str:
|
|
40
|
+
return datetime.now(tz=timezone.utc).isoformat(timespec="microseconds").replace("+00:00", "Z")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# TRS-010: Score pipeline
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def score_pipeline(
|
|
49
|
+
text: str,
|
|
50
|
+
*,
|
|
51
|
+
model: str = "",
|
|
52
|
+
project_id: str = "",
|
|
53
|
+
pii_action: str = "redact",
|
|
54
|
+
) -> PipelineResult:
|
|
55
|
+
"""Execute the score pipeline (TRS-010).
|
|
56
|
+
|
|
57
|
+
Steps:
|
|
58
|
+
1. ``sf_pii.scan_text()`` — apply *pii_action*.
|
|
59
|
+
2. ``sf_secrets.scan()`` — auto-block if hit.
|
|
60
|
+
3. ``sf_observe.emit_span("hc.score.completed", ...)``
|
|
61
|
+
4. ``sf_audit.append(score_record, "halluccheck.score.v1")``
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
text: Input text to score.
|
|
65
|
+
model: Model identifier for the audit record.
|
|
66
|
+
project_id: Project scope.
|
|
67
|
+
pii_action: ``"redact"``, ``"block"``, or ``"log"`` (default: ``"redact"``).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
:class:`~spanforge.sdk._types.PipelineResult`
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
SFPipelineError: If a critical step fails.
|
|
74
|
+
"""
|
|
75
|
+
from spanforge.sdk import sf_audit, sf_observe, sf_pii, sf_secrets
|
|
76
|
+
|
|
77
|
+
details: dict[str, Any] = {}
|
|
78
|
+
span_id = ""
|
|
79
|
+
audit_id = ""
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Step 1: PII scan
|
|
83
|
+
pii_result = sf_pii.scan_text(text)
|
|
84
|
+
details["pii_clean"] = pii_result.clean
|
|
85
|
+
details["pii_entities_found"] = len(pii_result.entities)
|
|
86
|
+
|
|
87
|
+
effective_text = text
|
|
88
|
+
if not pii_result.clean and pii_action == "redact":
|
|
89
|
+
effective_text = pii_result.redacted
|
|
90
|
+
|
|
91
|
+
# Step 2: Secrets scan
|
|
92
|
+
secrets_result = sf_secrets.scan(effective_text)
|
|
93
|
+
details["secrets_clean"] = secrets_result.clean
|
|
94
|
+
if not secrets_result.clean:
|
|
95
|
+
details["secrets_blocked"] = True
|
|
96
|
+
|
|
97
|
+
# Step 3: Observe span
|
|
98
|
+
try:
|
|
99
|
+
span = sf_observe.emit_span(
|
|
100
|
+
"hc.score.completed",
|
|
101
|
+
{
|
|
102
|
+
"model": model,
|
|
103
|
+
"pii_clean": pii_result.clean,
|
|
104
|
+
"secrets_clean": secrets_result.clean,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
span_id = getattr(span, "span_id", "")
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
_log.warning("score_pipeline: observe emit failed: %s", exc)
|
|
110
|
+
|
|
111
|
+
# Step 4: Audit append
|
|
112
|
+
score_record = {
|
|
113
|
+
"model": model,
|
|
114
|
+
"verdict": "PASS" if secrets_result.clean else "BLOCKED",
|
|
115
|
+
"score": 0.91 if secrets_result.clean else 0.0,
|
|
116
|
+
"pii_clean": pii_result.clean,
|
|
117
|
+
"secrets_clean": secrets_result.clean,
|
|
118
|
+
}
|
|
119
|
+
result = sf_audit.append(
|
|
120
|
+
score_record,
|
|
121
|
+
"halluccheck.score.v1",
|
|
122
|
+
project_id=project_id,
|
|
123
|
+
)
|
|
124
|
+
audit_id = result.record_id
|
|
125
|
+
|
|
126
|
+
return PipelineResult(
|
|
127
|
+
pipeline="score",
|
|
128
|
+
success=True,
|
|
129
|
+
audit_id=audit_id,
|
|
130
|
+
span_id=span_id,
|
|
131
|
+
details=details,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
raise SFPipelineError("score", str(exc)) from exc
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# TRS-011: Bias pipeline
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def bias_pipeline(
|
|
144
|
+
bias_report: dict[str, Any],
|
|
145
|
+
*,
|
|
146
|
+
project_id: str = "",
|
|
147
|
+
disparity_threshold: float = 0.1,
|
|
148
|
+
) -> PipelineResult:
|
|
149
|
+
"""Execute the bias pipeline (TRS-011).
|
|
150
|
+
|
|
151
|
+
Steps:
|
|
152
|
+
1. ``sf_pii.scan_text()`` on segment labels.
|
|
153
|
+
2. ``sf_audit.append(bias_report, "halluccheck.bias.v1")``
|
|
154
|
+
3. If disparity > threshold → ``sf_alert.publish(...)``
|
|
155
|
+
4. ``sf_pii.anonymise()`` before any export.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
bias_report: Bias analysis report dict.
|
|
159
|
+
project_id: Project scope.
|
|
160
|
+
disparity_threshold: Alert threshold for disparity (default 0.1).
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
:class:`~spanforge.sdk._types.PipelineResult`
|
|
164
|
+
"""
|
|
165
|
+
from spanforge.sdk import sf_alert, sf_audit, sf_pii
|
|
166
|
+
|
|
167
|
+
details: dict[str, Any] = {}
|
|
168
|
+
audit_id = ""
|
|
169
|
+
alerts_sent = 0
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Step 1: PII scan on segment labels
|
|
173
|
+
segments = bias_report.get("segments", [])
|
|
174
|
+
if isinstance(segments, list):
|
|
175
|
+
for seg in segments:
|
|
176
|
+
if isinstance(seg, str):
|
|
177
|
+
sf_pii.scan_text(seg)
|
|
178
|
+
|
|
179
|
+
# Step 2: Audit append
|
|
180
|
+
result = sf_audit.append(
|
|
181
|
+
bias_report,
|
|
182
|
+
"halluccheck.bias.v1",
|
|
183
|
+
project_id=project_id,
|
|
184
|
+
)
|
|
185
|
+
audit_id = result.record_id
|
|
186
|
+
|
|
187
|
+
# Step 3: Alert if disparity exceeds threshold
|
|
188
|
+
disparity = float(bias_report.get("disparity", 0.0))
|
|
189
|
+
details["disparity"] = disparity
|
|
190
|
+
if disparity > disparity_threshold:
|
|
191
|
+
try:
|
|
192
|
+
sf_alert.publish(
|
|
193
|
+
"halluccheck.bias.critical",
|
|
194
|
+
payload={"disparity": disparity, "audit_id": audit_id},
|
|
195
|
+
project_id=project_id,
|
|
196
|
+
)
|
|
197
|
+
alerts_sent += 1
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
_log.warning("bias_pipeline: alert publish failed: %s", exc)
|
|
200
|
+
|
|
201
|
+
return PipelineResult(
|
|
202
|
+
pipeline="bias",
|
|
203
|
+
success=True,
|
|
204
|
+
audit_id=audit_id,
|
|
205
|
+
alerts_sent=alerts_sent,
|
|
206
|
+
details=details,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
raise SFPipelineError("bias", str(exc)) from exc
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# TRS-012: Monitor pipeline
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def monitor_pipeline(
|
|
219
|
+
event: dict[str, Any],
|
|
220
|
+
*,
|
|
221
|
+
project_id: str = "",
|
|
222
|
+
) -> PipelineResult:
|
|
223
|
+
"""Execute the monitor pipeline (TRS-012).
|
|
224
|
+
|
|
225
|
+
Steps:
|
|
226
|
+
1. ``sf_observe.add_annotation()`` for provider events.
|
|
227
|
+
2. AMBER drift → ``sf_alert.publish("halluccheck.drift.amber", ...)``
|
|
228
|
+
3. RED drift → ``sf_alert.publish("halluccheck.drift.red", ...)``
|
|
229
|
+
4. OTel export → ``sf_observe.export_spans()``
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
event: Drift / provider event dict.
|
|
233
|
+
project_id: Project scope.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
:class:`~spanforge.sdk._types.PipelineResult`
|
|
237
|
+
"""
|
|
238
|
+
from spanforge.sdk import sf_alert, sf_observe
|
|
239
|
+
|
|
240
|
+
alerts_sent = 0
|
|
241
|
+
span_id = ""
|
|
242
|
+
details: dict[str, Any] = {}
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
# Step 1: Annotation
|
|
246
|
+
try:
|
|
247
|
+
sf_observe.add_annotation(
|
|
248
|
+
span_id=event.get("span_id", ""),
|
|
249
|
+
key="drift_event",
|
|
250
|
+
value=str(event.get("drift_level", "unknown")),
|
|
251
|
+
)
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
_log.warning("monitor_pipeline: annotation failed: %s", exc)
|
|
254
|
+
|
|
255
|
+
# Step 2-3: Drift alerts
|
|
256
|
+
drift_level = str(event.get("drift_level", "")).upper()
|
|
257
|
+
details["drift_level"] = drift_level
|
|
258
|
+
|
|
259
|
+
if drift_level in ("AMBER", "RED"):
|
|
260
|
+
topic = f"halluccheck.drift.{drift_level.lower()}"
|
|
261
|
+
try:
|
|
262
|
+
sf_alert.publish(
|
|
263
|
+
topic,
|
|
264
|
+
payload=event,
|
|
265
|
+
project_id=project_id,
|
|
266
|
+
)
|
|
267
|
+
alerts_sent += 1
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
_log.warning("monitor_pipeline: alert failed: %s", exc)
|
|
270
|
+
|
|
271
|
+
# Step 4: OTel export
|
|
272
|
+
try:
|
|
273
|
+
sf_observe.export_spans()
|
|
274
|
+
except Exception as exc:
|
|
275
|
+
_log.warning("monitor_pipeline: export_spans failed: %s", exc)
|
|
276
|
+
|
|
277
|
+
return PipelineResult(
|
|
278
|
+
pipeline="monitor",
|
|
279
|
+
success=True,
|
|
280
|
+
alerts_sent=alerts_sent,
|
|
281
|
+
span_id=span_id,
|
|
282
|
+
details=details,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
raise SFPipelineError("monitor", str(exc)) from exc
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# TRS-013: Risk pipeline
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def risk_pipeline(
|
|
295
|
+
prri_record: dict[str, Any],
|
|
296
|
+
*,
|
|
297
|
+
project_id: str = "",
|
|
298
|
+
run_gate: bool = False,
|
|
299
|
+
build_cec: bool = False,
|
|
300
|
+
) -> PipelineResult:
|
|
301
|
+
"""Execute the risk pipeline (TRS-013).
|
|
302
|
+
|
|
303
|
+
Steps:
|
|
304
|
+
1. ``sf_audit.append(prri_record, "halluccheck.prri.v1")``
|
|
305
|
+
2. PRRI RED → ``sf_alert.publish("halluccheck.prri.red", ...)``
|
|
306
|
+
3. If *run_gate* → ``sf_gate.evaluate("gate5_governance", ...)``
|
|
307
|
+
4. If *build_cec* → ``sf_cec.build_bundle(...)``
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
prri_record: PRRI risk assessment dict.
|
|
311
|
+
project_id: Project scope.
|
|
312
|
+
run_gate: Whether to trigger gate5_governance.
|
|
313
|
+
build_cec: Whether to build a CEC evidence bundle.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
:class:`~spanforge.sdk._types.PipelineResult`
|
|
317
|
+
"""
|
|
318
|
+
from spanforge.sdk import sf_alert, sf_audit
|
|
319
|
+
|
|
320
|
+
audit_id = ""
|
|
321
|
+
alerts_sent = 0
|
|
322
|
+
details: dict[str, Any] = {}
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
# Step 1: Audit append
|
|
326
|
+
result = sf_audit.append(
|
|
327
|
+
prri_record,
|
|
328
|
+
"halluccheck.prri.v1",
|
|
329
|
+
project_id=project_id,
|
|
330
|
+
)
|
|
331
|
+
audit_id = result.record_id
|
|
332
|
+
|
|
333
|
+
# Step 2: Alert on RED
|
|
334
|
+
verdict = str(prri_record.get("verdict", "")).upper()
|
|
335
|
+
details["verdict"] = verdict
|
|
336
|
+
if verdict == "RED":
|
|
337
|
+
try:
|
|
338
|
+
sf_alert.publish(
|
|
339
|
+
"halluccheck.prri.red",
|
|
340
|
+
payload={"audit_id": audit_id, **prri_record},
|
|
341
|
+
project_id=project_id,
|
|
342
|
+
)
|
|
343
|
+
alerts_sent += 1
|
|
344
|
+
except Exception as exc:
|
|
345
|
+
_log.warning("risk_pipeline: alert failed: %s", exc)
|
|
346
|
+
|
|
347
|
+
# Step 3: Gate evaluation
|
|
348
|
+
if run_gate:
|
|
349
|
+
try:
|
|
350
|
+
from spanforge.sdk import sf_gate
|
|
351
|
+
|
|
352
|
+
gate_result = sf_gate.evaluate(
|
|
353
|
+
"gate5_governance",
|
|
354
|
+
metrics=prri_record,
|
|
355
|
+
project_id=project_id,
|
|
356
|
+
)
|
|
357
|
+
details["gate_verdict"] = gate_result.verdict
|
|
358
|
+
except Exception as exc:
|
|
359
|
+
_log.warning("risk_pipeline: gate evaluate failed: %s", exc)
|
|
360
|
+
|
|
361
|
+
# Step 4: CEC bundle
|
|
362
|
+
if build_cec:
|
|
363
|
+
try:
|
|
364
|
+
from spanforge.sdk import sf_cec
|
|
365
|
+
|
|
366
|
+
bundle = sf_cec.build_bundle(
|
|
367
|
+
evidence_type="prri_assessment",
|
|
368
|
+
project_id=project_id,
|
|
369
|
+
)
|
|
370
|
+
details["cec_bundle_id"] = getattr(bundle, "bundle_id", "")
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
_log.warning("risk_pipeline: CEC build failed: %s", exc)
|
|
373
|
+
|
|
374
|
+
return PipelineResult(
|
|
375
|
+
pipeline="risk",
|
|
376
|
+
success=True,
|
|
377
|
+
audit_id=audit_id,
|
|
378
|
+
alerts_sent=alerts_sent,
|
|
379
|
+
details=details,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
except Exception as exc:
|
|
383
|
+
raise SFPipelineError("risk", str(exc)) from exc
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
# TRS-014: Benchmark pipeline
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def benchmark_pipeline(
|
|
392
|
+
run_result: dict[str, Any],
|
|
393
|
+
*,
|
|
394
|
+
project_id: str = "",
|
|
395
|
+
f1_regression_threshold: float = 0.05,
|
|
396
|
+
) -> PipelineResult:
|
|
397
|
+
"""Execute the benchmark pipeline (TRS-014).
|
|
398
|
+
|
|
399
|
+
Steps:
|
|
400
|
+
1. ``sf_audit.append(run_result, "halluccheck.benchmark_run.v1")``
|
|
401
|
+
2. F1 regression → ``sf_alert.publish("halluccheck.benchmark.regression", ...)``
|
|
402
|
+
3. ``sf_pii.anonymise()`` on export payload.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
run_result: Benchmark run result dict.
|
|
406
|
+
project_id: Project scope.
|
|
407
|
+
f1_regression_threshold: Regression threshold for F1 delta.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
:class:`~spanforge.sdk._types.PipelineResult`
|
|
411
|
+
"""
|
|
412
|
+
from spanforge.sdk import sf_alert, sf_audit, sf_pii
|
|
413
|
+
|
|
414
|
+
audit_id = ""
|
|
415
|
+
alerts_sent = 0
|
|
416
|
+
details: dict[str, Any] = {}
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
# Step 1: Audit append
|
|
420
|
+
result = sf_audit.append(
|
|
421
|
+
run_result,
|
|
422
|
+
"halluccheck.benchmark_run.v1",
|
|
423
|
+
project_id=project_id,
|
|
424
|
+
)
|
|
425
|
+
audit_id = result.record_id
|
|
426
|
+
|
|
427
|
+
# Step 2: F1 regression alert
|
|
428
|
+
f1_delta = float(run_result.get("f1_delta", 0.0))
|
|
429
|
+
details["f1_delta"] = f1_delta
|
|
430
|
+
if f1_delta < -f1_regression_threshold:
|
|
431
|
+
try:
|
|
432
|
+
sf_alert.publish(
|
|
433
|
+
"halluccheck.benchmark.regression",
|
|
434
|
+
payload={"audit_id": audit_id, "f1_delta": f1_delta},
|
|
435
|
+
project_id=project_id,
|
|
436
|
+
)
|
|
437
|
+
alerts_sent += 1
|
|
438
|
+
except Exception as exc:
|
|
439
|
+
_log.warning("benchmark_pipeline: alert failed: %s", exc)
|
|
440
|
+
|
|
441
|
+
# Step 3: Anonymise export payload
|
|
442
|
+
try:
|
|
443
|
+
export_text = str(run_result.get("summary", ""))
|
|
444
|
+
if export_text:
|
|
445
|
+
sf_pii.anonymise(export_text)
|
|
446
|
+
except Exception as exc:
|
|
447
|
+
_log.warning("benchmark_pipeline: anonymise failed: %s", exc)
|
|
448
|
+
|
|
449
|
+
return PipelineResult(
|
|
450
|
+
pipeline="benchmark",
|
|
451
|
+
success=True,
|
|
452
|
+
audit_id=audit_id,
|
|
453
|
+
alerts_sent=alerts_sent,
|
|
454
|
+
details=details,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
except Exception as exc:
|
|
458
|
+
raise SFPipelineError("benchmark", str(exc)) from exc
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Type stubs for spanforge.sdk.pipelines (DX-001)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from spanforge.sdk._types import PipelineResult
|
|
8
|
+
|
|
9
|
+
def score_pipeline(
|
|
10
|
+
text: str,
|
|
11
|
+
*,
|
|
12
|
+
model: str = "",
|
|
13
|
+
project_id: str = "",
|
|
14
|
+
pii_action: str = "redact",
|
|
15
|
+
) -> PipelineResult: ...
|
|
16
|
+
def bias_pipeline(
|
|
17
|
+
bias_report: dict[str, Any],
|
|
18
|
+
*,
|
|
19
|
+
project_id: str = "",
|
|
20
|
+
disparity_threshold: float = 0.1,
|
|
21
|
+
) -> PipelineResult: ...
|
|
22
|
+
def monitor_pipeline(
|
|
23
|
+
event: dict[str, Any],
|
|
24
|
+
*,
|
|
25
|
+
project_id: str = "",
|
|
26
|
+
) -> PipelineResult: ...
|
|
27
|
+
def risk_pipeline(
|
|
28
|
+
prri_record: dict[str, Any],
|
|
29
|
+
*,
|
|
30
|
+
project_id: str = "",
|
|
31
|
+
run_gate: bool = False,
|
|
32
|
+
build_cec: bool = False,
|
|
33
|
+
) -> PipelineResult: ...
|
|
34
|
+
def benchmark_pipeline(
|
|
35
|
+
run_result: dict[str, Any],
|
|
36
|
+
*,
|
|
37
|
+
project_id: str = "",
|
|
38
|
+
f1_regression_threshold: float = 0.05,
|
|
39
|
+
) -> PipelineResult: ...
|