openwright-langfuse 0.3.0__tar.gz

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.
@@ -0,0 +1,20 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .venv/
7
+ venv/
8
+ dist/
9
+ build/
10
+
11
+ # generated evidence / demo artifacts
12
+ *.pem
13
+ *.pem.pub
14
+ openwright-out/
15
+ *-ledger/
16
+ out/
17
+
18
+ # node / OS
19
+ node_modules/
20
+ .DS_Store
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: openwright-langfuse
3
+ Version: 0.3.0
4
+ Summary: Langfuse forwarder for OpenWright — co-ingest + openwright:* verdict back-link + evals-as-evidence.
5
+ License: Apache-2.0
6
+ Keywords: evidence,forwarder,langfuse,observability,openwright
7
+ Requires-Python: <3.15,>=3.10
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: openwright-core<0.7,>=0.6
10
+ Provides-Extra: test
11
+ Requires-Dist: openwright-conformance; extra == 'test'
12
+ Requires-Dist: pytest>=8; extra == 'test'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # openwright-langfuse
16
+
17
+ A **Langfuse forwarder** for [OpenWright](https://github.com/allthingsN/openwright):
18
+ keep your traces in Langfuse *and* attach the OpenWright verdicts back to them.
19
+
20
+ - **forward** — co-ingest a telemetry batch to Langfuse unchanged (Basic auth +
21
+ `x-langfuse-ingestion-version: 4`).
22
+ - **backlink** — push `openwright:<control>` scores (the verdicts) onto the trace,
23
+ with the reason as a comment; returns a deep-link.
24
+ - **pull** — read Langfuse scores/evals back as `ComplianceEvent`s (evals-as-evidence).
25
+
26
+ ```python
27
+ from openwright.connectors import load
28
+ fwd = load("openwright.forwarders", "langfuse")
29
+ cfg = {"host": "https://cloud.langfuse.com", "public_key": "pk-…", "secret_key": "sk-…", "trace_id": trace_id}
30
+ fwd.backlink(report, config=cfg) # openwright:* verdicts now on the Langfuse trace
31
+ ```
32
+
33
+ No partnership or endorsement implied; built on Langfuse's public HTTP API. No
34
+ crypto is reimplemented here.
@@ -0,0 +1,20 @@
1
+ # openwright-langfuse
2
+
3
+ A **Langfuse forwarder** for [OpenWright](https://github.com/allthingsN/openwright):
4
+ keep your traces in Langfuse *and* attach the OpenWright verdicts back to them.
5
+
6
+ - **forward** — co-ingest a telemetry batch to Langfuse unchanged (Basic auth +
7
+ `x-langfuse-ingestion-version: 4`).
8
+ - **backlink** — push `openwright:<control>` scores (the verdicts) onto the trace,
9
+ with the reason as a comment; returns a deep-link.
10
+ - **pull** — read Langfuse scores/evals back as `ComplianceEvent`s (evals-as-evidence).
11
+
12
+ ```python
13
+ from openwright.connectors import load
14
+ fwd = load("openwright.forwarders", "langfuse")
15
+ cfg = {"host": "https://cloud.langfuse.com", "public_key": "pk-…", "secret_key": "sk-…", "trace_id": trace_id}
16
+ fwd.backlink(report, config=cfg) # openwright:* verdicts now on the Langfuse trace
17
+ ```
18
+
19
+ No partnership or endorsement implied; built on Langfuse's public HTTP API. No
20
+ crypto is reimplemented here.
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "openwright-langfuse"
7
+ version = "0.3.0"
8
+ description = "Langfuse forwarder for OpenWright — co-ingest + openwright:* verdict back-link + evals-as-evidence."
9
+ requires-python = ">=3.10,<3.15"
10
+ license = { text = "Apache-2.0" }
11
+ readme = "README.md"
12
+ keywords = ["openwright", "langfuse", "observability", "forwarder", "evidence"]
13
+ dependencies = ["openwright-core>=0.6,<0.7", "httpx>=0.27"]
14
+
15
+ [project.optional-dependencies]
16
+ test = ["pytest>=8", "openwright-conformance"]
17
+
18
+ [project.entry-points."openwright.forwarders"]
19
+ langfuse = "openwright_langfuse:forwarder"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/openwright_langfuse"]
@@ -0,0 +1,98 @@
1
+ """Langfuse forwarder for OpenWright (CONN-2).
2
+
3
+ The downstream-observability pattern as a :class:`Forwarder`:
4
+
5
+ * ``forward`` — co-ingest a telemetry batch to Langfuse **unchanged**, injecting
6
+ Basic auth + ``x-langfuse-ingestion-version: 4`` (Langfuse is HTTP-only).
7
+ * ``backlink`` — push ``openwright:<control>`` **scores** (the verdicts) onto the
8
+ Langfuse trace via the public API, with the reason as a comment, and return a
9
+ deep-link to the trace.
10
+ * ``pull`` — read Langfuse scores/evals and yield them as ``ComplianceEvent``s
11
+ (evals-as-evidence).
12
+
13
+ No crypto here; payloads are hashed by the source connector before they become
14
+ events. ``config`` carries ``host``, ``public_key``, ``secret_key`` and per-call
15
+ fields (``trace_id`` for ``backlink``). Pass ``_transport`` (an httpx transport)
16
+ to talk to a stub in tests.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import base64
22
+ from typing import Any, Iterable
23
+
24
+ from openwright.connectors import CONTRACT_VERSION, ExportResult
25
+
26
+ __all__ = ["LangfuseForwarder", "forwarder", "CONTRACT_VERSION"]
27
+
28
+
29
+ class LangfuseForwarder:
30
+ name = "langfuse"
31
+ CONTRACT_VERSION = CONTRACT_VERSION
32
+
33
+ def _client(self, config: dict):
34
+ import httpx
35
+
36
+ host = str(config["host"]).rstrip("/")
37
+ token = base64.b64encode(
38
+ f"{config['public_key']}:{config['secret_key']}".encode("utf-8")
39
+ ).decode("ascii")
40
+ headers = {
41
+ "Authorization": f"Basic {token}",
42
+ "x-langfuse-ingestion-version": "4",
43
+ }
44
+ return httpx.Client(
45
+ base_url=host, headers=headers, timeout=config.get("timeout", 10),
46
+ transport=config.get("_transport"),
47
+ )
48
+
49
+ def forward(self, payload: Any, *, config: dict) -> ExportResult:
50
+ path = config.get("ingest_path", "/api/public/otel/v1/traces")
51
+ with self._client(config) as c:
52
+ if isinstance(payload, (bytes, bytearray)):
53
+ r = c.post(path, content=bytes(payload),
54
+ headers={"Content-Type": "application/x-protobuf"})
55
+ else:
56
+ r = c.post(path, json=payload)
57
+ return ExportResult(ok=r.is_success, detail=f"forward -> HTTP {r.status_code}")
58
+
59
+ def backlink(self, report: dict, *, config: dict) -> ExportResult:
60
+ trace_id = config.get("trace_id")
61
+ oks: list[bool] = []
62
+ with self._client(config) as c:
63
+ for ctrl in report.get("controls", []):
64
+ body = {
65
+ "traceId": trace_id,
66
+ "name": f"openwright:{ctrl['control_id']}",
67
+ "value": ctrl["status"],
68
+ "dataType": "CATEGORICAL",
69
+ "comment": (ctrl.get("reason") or "")[:500],
70
+ }
71
+ oks.append(c.post("/api/public/scores", json=body).is_success)
72
+ url = f"{str(config['host']).rstrip('/')}/trace/{trace_id}" if trace_id else None
73
+ ok = all(oks) if oks else True
74
+ return ExportResult(ok=ok, detail=f"back-linked {sum(oks)}/{len(oks)} verdict(s)", url=url)
75
+
76
+ def pull(self, *, config: dict) -> Iterable[Any]:
77
+ from openwright.canonical import to_rfc3339
78
+ from openwright.events import ComplianceEvent, EventKind
79
+
80
+ with self._client(config) as c:
81
+ r = c.get("/api/public/scores", params={"limit": config.get("limit", 50)})
82
+ r.raise_for_status()
83
+ for sc in r.json().get("data", []):
84
+ yield ComplianceEvent(
85
+ timestamp=to_rfc3339(sc.get("timestamp") or "1970-01-01T00:00:00.000000000Z"),
86
+ kind=EventKind.CONFORMANCE_FINDING,
87
+ actor={"agent_id": "langfuse"},
88
+ source={"format": "langfuse"},
89
+ attributes={
90
+ "score_name": sc.get("name"),
91
+ "value": str(sc.get("value")),
92
+ "trace_id": sc.get("traceId"),
93
+ },
94
+ )
95
+
96
+
97
+ #: The object the ``openwright.forwarders:langfuse`` entry point loads to.
98
+ forwarder = LangfuseForwarder()
@@ -0,0 +1,67 @@
1
+ """Conformance + request-shape behavior (httpx mock transport) for openwright-langfuse."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+
8
+ import httpx
9
+
10
+ from openwright_conformance import run_conformance
11
+ from openwright_langfuse import forwarder
12
+
13
+ CONFIG = {"host": "https://cloud.langfuse.test", "public_key": "pk", "secret_key": "sk"}
14
+
15
+
16
+ def test_conforms():
17
+ run_conformance(forwarder, group="openwright.forwarders", name="langfuse", modules=["openwright_langfuse"])
18
+
19
+
20
+ def test_forward_injects_auth_and_ingestion_version():
21
+ seen = {}
22
+
23
+ def handler(request: httpx.Request) -> httpx.Response:
24
+ seen["url"] = str(request.url)
25
+ seen["auth"] = request.headers.get("Authorization")
26
+ seen["iv"] = request.headers.get("x-langfuse-ingestion-version")
27
+ return httpx.Response(207)
28
+
29
+ cfg = {**CONFIG, "_transport": httpx.MockTransport(handler)}
30
+ res = forwarder.forward(b"\x00otlp-bytes", config=cfg)
31
+ assert res.ok
32
+ assert seen["url"].endswith("/api/public/otel/v1/traces")
33
+ assert seen["auth"] == "Basic " + base64.b64encode(b"pk:sk").decode()
34
+ assert seen["iv"] == "4"
35
+
36
+
37
+ def test_backlink_posts_openwright_verdict_scores():
38
+ posted = []
39
+
40
+ def handler(request: httpx.Request) -> httpx.Response:
41
+ assert request.url.path == "/api/public/scores"
42
+ posted.append(json.loads(request.content))
43
+ return httpx.Response(201)
44
+
45
+ cfg = {**CONFIG, "trace_id": "tr_123", "_transport": httpx.MockTransport(handler)}
46
+ report = {"controls": [
47
+ {"control_id": "art-14-human-oversight", "status": "satisfied", "reason": "approved"},
48
+ {"control_id": "art-27-fria", "status": "not_satisfied", "reason": "no FRIA"},
49
+ ]}
50
+ res = forwarder.backlink(report, config=cfg)
51
+ assert res.ok and res.url == "https://cloud.langfuse.test/trace/tr_123"
52
+ names = {p["name"]: p["value"] for p in posted}
53
+ assert names == {"openwright:art-14-human-oversight": "satisfied", "openwright:art-27-fria": "not_satisfied"}
54
+ assert all(p["traceId"] == "tr_123" for p in posted)
55
+
56
+
57
+ def test_pull_yields_events_from_scores():
58
+ def handler(request: httpx.Request) -> httpx.Response:
59
+ return httpx.Response(200, json={"data": [
60
+ {"name": "hallucination", "value": 0.1, "traceId": "tr_1", "timestamp": "2026-05-28T00:00:00Z"},
61
+ ]})
62
+
63
+ cfg = {**CONFIG, "_transport": httpx.MockTransport(handler)}
64
+ events = list(forwarder.pull(config=cfg))
65
+ assert len(events) == 1
66
+ assert events[0].kind == "conformance_finding"
67
+ assert events[0].attributes["score_name"] == "hallucination"