langchain-ledgerproof 1.0.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.
- langchain_ledgerproof-1.0.0/.gitignore +9 -0
- langchain_ledgerproof-1.0.0/LICENSE +20 -0
- langchain_ledgerproof-1.0.0/PKG-INFO +74 -0
- langchain_ledgerproof-1.0.0/README.md +35 -0
- langchain_ledgerproof-1.0.0/pyproject.toml +31 -0
- langchain_ledgerproof-1.0.0/src/langchain_ledgerproof/__init__.py +26 -0
- langchain_ledgerproof-1.0.0/src/langchain_ledgerproof/_version.py +1 -0
- langchain_ledgerproof-1.0.0/src/langchain_ledgerproof/handler.py +173 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Veronica S. Dawkins / LedgerProof Foundation
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
|
|
19
|
+
For the full text of the Apache License 2.0, see:
|
|
20
|
+
https://www.apache.org/licenses/LICENSE-2.0.txt
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-ledgerproof
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LangChain callback that auto-issues LPR Article 50 receipts for every LLM call.
|
|
5
|
+
Project-URL: Homepage, https://ledgerproofhq.io
|
|
6
|
+
Project-URL: Documentation, https://docs.ledgerproofhq.io/integrations/langchain
|
|
7
|
+
Project-URL: Repository, https://github.com/vsdawkins-creator/ledgerproof-python
|
|
8
|
+
Author-email: "Veronica S. Dawkins" <veronica@ledgerproofhq.io>
|
|
9
|
+
License: Apache License
|
|
10
|
+
Version 2.0, January 2004
|
|
11
|
+
http://www.apache.org/licenses/
|
|
12
|
+
|
|
13
|
+
Copyright 2026 Veronica S. Dawkins / LedgerProof Foundation
|
|
14
|
+
|
|
15
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
16
|
+
you may not use this file except in compliance with the License.
|
|
17
|
+
You may obtain a copy of the License at
|
|
18
|
+
|
|
19
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
20
|
+
|
|
21
|
+
Unless required by applicable law or agreed to in writing, software
|
|
22
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
23
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
24
|
+
See the License for the specific language governing permissions and
|
|
25
|
+
limitations under the License.
|
|
26
|
+
|
|
27
|
+
For the full text of the Apache License 2.0, see:
|
|
28
|
+
https://www.apache.org/licenses/LICENSE-2.0.txt
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: article-50,compliance,eu-ai-act,langchain,ledgerproof
|
|
31
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
32
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
35
|
+
Requires-Python: >=3.9
|
|
36
|
+
Requires-Dist: langchain-core>=0.1
|
|
37
|
+
Requires-Dist: ledgerproof>=1.0
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# langchain-ledgerproof
|
|
41
|
+
|
|
42
|
+
LangChain callback that auto-issues EU AI Act Article 50 receipts for every LLM call.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install langchain-ledgerproof
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from langchain_anthropic import ChatAnthropic
|
|
50
|
+
from langchain_ledgerproof import LedgerProofCallbackHandler
|
|
51
|
+
|
|
52
|
+
callback = LedgerProofCallbackHandler(
|
|
53
|
+
publisher_id="LEI:5493001KJTIIGC8Y1R12",
|
|
54
|
+
deployer_country="DE",
|
|
55
|
+
deployer_name="Acme Corp",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
llm = ChatAnthropic(model="claude-sonnet-4-6", callbacks=[callback])
|
|
59
|
+
response = llm.invoke("Write a haiku about Bitcoin.")
|
|
60
|
+
# Receipt issued automatically. Your code is now Article 50 compliant.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Works with any LangChain LLM (Chat or Completion). Each invocation in a chain
|
|
64
|
+
or agent gets its own receipt. Background issuance — your chain isn't blocked.
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
Reads `LEDGERPROOF_API_KEY` from the environment if not passed.
|
|
69
|
+
|
|
70
|
+
See the [main ledgerproof package](../python) for the full Article 50 protocol.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
Apache-2.0.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# langchain-ledgerproof
|
|
2
|
+
|
|
3
|
+
LangChain callback that auto-issues EU AI Act Article 50 receipts for every LLM call.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install langchain-ledgerproof
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from langchain_anthropic import ChatAnthropic
|
|
11
|
+
from langchain_ledgerproof import LedgerProofCallbackHandler
|
|
12
|
+
|
|
13
|
+
callback = LedgerProofCallbackHandler(
|
|
14
|
+
publisher_id="LEI:5493001KJTIIGC8Y1R12",
|
|
15
|
+
deployer_country="DE",
|
|
16
|
+
deployer_name="Acme Corp",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
llm = ChatAnthropic(model="claude-sonnet-4-6", callbacks=[callback])
|
|
20
|
+
response = llm.invoke("Write a haiku about Bitcoin.")
|
|
21
|
+
# Receipt issued automatically. Your code is now Article 50 compliant.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Works with any LangChain LLM (Chat or Completion). Each invocation in a chain
|
|
25
|
+
or agent gets its own receipt. Background issuance — your chain isn't blocked.
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
Reads `LEDGERPROOF_API_KEY` from the environment if not passed.
|
|
30
|
+
|
|
31
|
+
See the [main ledgerproof package](../python) for the full Article 50 protocol.
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
Apache-2.0.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-ledgerproof"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "LangChain callback that auto-issues LPR Article 50 receipts for every LLM call."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Veronica S. Dawkins", email = "veronica@ledgerproofhq.io" }]
|
|
13
|
+
keywords = ["langchain", "ledgerproof", "eu-ai-act", "article-50", "compliance"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"ledgerproof>=1.0",
|
|
22
|
+
"langchain-core>=0.1",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://ledgerproofhq.io"
|
|
27
|
+
Documentation = "https://docs.ledgerproofhq.io/integrations/langchain"
|
|
28
|
+
Repository = "https://github.com/vsdawkins-creator/ledgerproof-python"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/langchain_ledgerproof"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""LangChain × LedgerProof — auto-issue Article 50 receipts for every LLM call.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from langchain_anthropic import ChatAnthropic
|
|
6
|
+
from langchain_ledgerproof import LedgerProofCallbackHandler
|
|
7
|
+
|
|
8
|
+
callback = LedgerProofCallbackHandler(
|
|
9
|
+
publisher_id="LEI:5493001KJTIIGC8Y1R12",
|
|
10
|
+
deployer_country="DE",
|
|
11
|
+
deployer_name="Acme Corp",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
llm = ChatAnthropic(model="claude-sonnet-4-6", callbacks=[callback])
|
|
15
|
+
response = llm.invoke("Write a haiku.")
|
|
16
|
+
# The callback issued an LPR receipt for the completion automatically.
|
|
17
|
+
|
|
18
|
+
Works with any LangChain LLM that emits ``on_llm_end`` events.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from ._version import __version__
|
|
24
|
+
from .handler import LedgerProofCallbackHandler
|
|
25
|
+
|
|
26
|
+
__all__ = ["LedgerProofCallbackHandler", "__version__"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""LedgerProofCallbackHandler — a LangChain BaseCallbackHandler.
|
|
2
|
+
|
|
3
|
+
Hooks into the LangChain callbacks system to issue an LPR receipt every
|
|
4
|
+
time an LLM completes. Works for ``ChatModel.invoke``, ``LLM.predict``,
|
|
5
|
+
streaming, and agentic chains (every LLM step gets its own receipt).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
|
|
15
|
+
from langchain_core.callbacks.base import BaseCallbackHandler
|
|
16
|
+
from ledgerproof import LedgerProof
|
|
17
|
+
from ledgerproof.errors import LedgerProofError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from langchain_core.outputs import LLMResult
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("langchain_ledgerproof")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LedgerProofCallbackHandler(BaseCallbackHandler):
|
|
26
|
+
"""LangChain callback that issues an LPR Article 50 receipt on every LLM completion.
|
|
27
|
+
|
|
28
|
+
Drop into any LangChain LLM or chain::
|
|
29
|
+
|
|
30
|
+
llm = ChatAnthropic(callbacks=[LedgerProofCallbackHandler(...)])
|
|
31
|
+
|
|
32
|
+
All receipts are issued asynchronously in a background thread. Your chain
|
|
33
|
+
is not blocked. Failures fail open — a warning is logged, your chain
|
|
34
|
+
continues normally.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
publisher_id: str,
|
|
41
|
+
deployer_country: str,
|
|
42
|
+
deployer_name: str,
|
|
43
|
+
ai_system_id: str | None = None,
|
|
44
|
+
api_key: str | None = None,
|
|
45
|
+
api_base: str | None = None,
|
|
46
|
+
is_public_interest: bool | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize the callback.
|
|
49
|
+
|
|
50
|
+
:param publisher_id: Your legal-entity identifier (LEI/EUID/VAT/DID).
|
|
51
|
+
:param deployer_country: ISO 3166-1 alpha-2 country code.
|
|
52
|
+
:param deployer_name: Human-readable organization name.
|
|
53
|
+
:param ai_system_id: Override the AI system identifier in the receipt.
|
|
54
|
+
Default derived from the LangChain LLM metadata.
|
|
55
|
+
:param api_key: LedgerProof API key. Falls back to ``LEDGERPROOF_API_KEY``.
|
|
56
|
+
:param api_base: LedgerProof endpoint. Defaults to api-eu.ledgerproofhq.io.
|
|
57
|
+
:param is_public_interest: Tag every receipt with this assertion.
|
|
58
|
+
"""
|
|
59
|
+
self._lp = LedgerProof(
|
|
60
|
+
publisher_id=publisher_id,
|
|
61
|
+
deployer_country=deployer_country,
|
|
62
|
+
api_key=api_key,
|
|
63
|
+
api_base=api_base,
|
|
64
|
+
)
|
|
65
|
+
self._deployer_name = deployer_name
|
|
66
|
+
self._ai_system_id_override = ai_system_id
|
|
67
|
+
self._is_public_interest = is_public_interest
|
|
68
|
+
self._executor = ThreadPoolExecutor(
|
|
69
|
+
max_workers=4, thread_name_prefix="ledgerproof-langchain"
|
|
70
|
+
)
|
|
71
|
+
# Map run_id → invocation context for picking up the right metadata at end.
|
|
72
|
+
self._runs: dict[UUID, dict[str, Any]] = {}
|
|
73
|
+
|
|
74
|
+
# ── LangChain callback hooks ────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
def on_llm_start(
|
|
77
|
+
self,
|
|
78
|
+
serialized: dict[str, Any],
|
|
79
|
+
prompts: list[str],
|
|
80
|
+
*,
|
|
81
|
+
run_id: UUID,
|
|
82
|
+
**kwargs: Any,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Stash the LLM identity so we can use it at end-time."""
|
|
85
|
+
model_name = (
|
|
86
|
+
serialized.get("kwargs", {}).get("model")
|
|
87
|
+
or serialized.get("kwargs", {}).get("model_name")
|
|
88
|
+
or serialized.get("name")
|
|
89
|
+
or "langchain/unknown"
|
|
90
|
+
)
|
|
91
|
+
# Infer provider from the serialized.id chain (e.g., ["langchain", "chat_models", "anthropic", "ChatAnthropic"]).
|
|
92
|
+
ids = serialized.get("id", [])
|
|
93
|
+
if isinstance(ids, list) and len(ids) >= 3:
|
|
94
|
+
provider = ids[2]
|
|
95
|
+
else:
|
|
96
|
+
provider = "langchain"
|
|
97
|
+
self._runs[run_id] = {
|
|
98
|
+
"ai_system_id": self._ai_system_id_override or f"{provider}/{model_name}",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def on_chat_model_start(
|
|
102
|
+
self,
|
|
103
|
+
serialized: dict[str, Any],
|
|
104
|
+
messages: list[list[Any]],
|
|
105
|
+
*,
|
|
106
|
+
run_id: UUID,
|
|
107
|
+
**kwargs: Any,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Chat models use this instead of on_llm_start."""
|
|
110
|
+
self.on_llm_start(serialized, [], run_id=run_id, **kwargs)
|
|
111
|
+
|
|
112
|
+
def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> None:
|
|
113
|
+
"""Extract the generation text and issue a receipt."""
|
|
114
|
+
context = self._runs.pop(run_id, {})
|
|
115
|
+
ai_system_id = context.get("ai_system_id", "langchain/unknown")
|
|
116
|
+
|
|
117
|
+
# Concatenate all generations' text. For chat models the text is in
|
|
118
|
+
# ``generation.message.content``; for completion LLMs it's in
|
|
119
|
+
# ``generation.text``.
|
|
120
|
+
text = self._extract_text(response)
|
|
121
|
+
if not text:
|
|
122
|
+
return
|
|
123
|
+
self._executor.submit(self._issue_safe, text, ai_system_id)
|
|
124
|
+
|
|
125
|
+
def on_llm_error(self, error: BaseException, *, run_id: UUID, **kwargs: Any) -> None:
|
|
126
|
+
"""LLM failed — drop the context, no receipt to issue."""
|
|
127
|
+
self._runs.pop(run_id, None)
|
|
128
|
+
|
|
129
|
+
# ── Internals ───────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def _extract_text(response: Any) -> str:
|
|
133
|
+
"""Pull all generated text out of a LangChain LLMResult."""
|
|
134
|
+
try:
|
|
135
|
+
parts: list[str] = []
|
|
136
|
+
for generation_list in response.generations:
|
|
137
|
+
for gen in generation_list:
|
|
138
|
+
text = getattr(gen, "text", "")
|
|
139
|
+
if text:
|
|
140
|
+
parts.append(text)
|
|
141
|
+
else:
|
|
142
|
+
message = getattr(gen, "message", None)
|
|
143
|
+
if message is not None:
|
|
144
|
+
content = getattr(message, "content", "")
|
|
145
|
+
if isinstance(content, str):
|
|
146
|
+
parts.append(content)
|
|
147
|
+
return "\n".join(parts)
|
|
148
|
+
except (AttributeError, TypeError):
|
|
149
|
+
return ""
|
|
150
|
+
|
|
151
|
+
def _issue_safe(self, text: str, ai_system_id: str) -> Any:
|
|
152
|
+
try:
|
|
153
|
+
return self._lp.publish_ai_article_50(
|
|
154
|
+
artifact=text,
|
|
155
|
+
artifact_content_type="text/plain",
|
|
156
|
+
ai_system_id=ai_system_id,
|
|
157
|
+
deployer_name=self._deployer_name,
|
|
158
|
+
content_category="SYNTHETIC_TEXT",
|
|
159
|
+
generation_type="FULLY_GENERATED",
|
|
160
|
+
is_public_interest=self._is_public_interest,
|
|
161
|
+
)
|
|
162
|
+
except LedgerProofError as exc:
|
|
163
|
+
logger.warning("LedgerProof receipt issuance failed (fail-open): %s", exc)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def __del__(self) -> None:
|
|
167
|
+
try:
|
|
168
|
+
self._executor.shutdown(wait=False)
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = ["LedgerProofCallbackHandler"]
|