proofledger 0.1.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.
- proofledger-0.1.0/.gitignore +11 -0
- proofledger-0.1.0/LICENSE +21 -0
- proofledger-0.1.0/PKG-INFO +137 -0
- proofledger-0.1.0/README.md +105 -0
- proofledger-0.1.0/examples/basic.py +75 -0
- proofledger-0.1.0/proofledger/__init__.py +193 -0
- proofledger-0.1.0/proofledger/adapters.py +157 -0
- proofledger-0.1.0/proofledger/client.py +544 -0
- proofledger-0.1.0/proofledger/hashing.py +208 -0
- proofledger-0.1.0/proofledger/ids.py +32 -0
- proofledger-0.1.0/proofledger/signing.py +77 -0
- proofledger-0.1.0/proofledger/transport.py +225 -0
- proofledger-0.1.0/pyproject.toml +44 -0
- proofledger-0.1.0/tests/test_adapters.py +61 -0
- proofledger-0.1.0/tests/test_client.py +101 -0
- proofledger-0.1.0/tests/test_hashing.py +117 -0
- proofledger-0.1.0/tests/test_signing.py +36 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ProofLedger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proofledger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Framework-agnostic, tamper-evident audit layer for AI agents. Hash chains verify byte-for-byte against the ProofLedger TypeScript SDK and dashboard.
|
|
5
|
+
Project-URL: Homepage, https://proofledger.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/jorama/proofledger
|
|
7
|
+
Project-URL: Issues, https://github.com/jorama/proofledger/issues
|
|
8
|
+
Author: ProofLedger
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,audit,hash-chain,llm,observability,proofledger,tamper-evident,tracing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build; extra == 'dev'
|
|
26
|
+
Requires-Dist: cryptography>=41; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
28
|
+
Requires-Dist: twine; extra == 'dev'
|
|
29
|
+
Provides-Extra: signing
|
|
30
|
+
Requires-Dist: cryptography>=41; extra == 'signing'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# ProofLedger Python SDK
|
|
34
|
+
|
|
35
|
+
A framework-agnostic, tamper-evident audit layer for AI agents. ProofLedger sits
|
|
36
|
+
underneath or beside any agent framework (LangGraph, CrewAI, OpenAI Agents SDK,
|
|
37
|
+
AutoGen, or a custom stack) and records every run, event, and tool call into a
|
|
38
|
+
SHA-256 hash chain.
|
|
39
|
+
|
|
40
|
+
The chains this SDK produces are **byte-for-byte compatible** with the
|
|
41
|
+
ProofLedger TypeScript SDK: runs captured from Python verify correctly in the
|
|
42
|
+
ProofLedger dashboard, which verifies using the TS implementation.
|
|
43
|
+
|
|
44
|
+
No third-party runtime dependencies — standard library only (Python >= 3.9).
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install proofledger
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or from this repo:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install packages/sdk-py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from proofledger import enable, track, with_run, verify_run
|
|
62
|
+
|
|
63
|
+
# Cloud mode — sends to your ProofLedger backend.
|
|
64
|
+
enable(
|
|
65
|
+
api_key="tl_live_...",
|
|
66
|
+
base_url="https://proofledger.dev",
|
|
67
|
+
project_id="proj_...",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# One-shot tracking of a complete, verifiable run.
|
|
71
|
+
track(
|
|
72
|
+
agent_id="support-agent",
|
|
73
|
+
input="Hello",
|
|
74
|
+
output="Hi there",
|
|
75
|
+
model="gpt-4.1",
|
|
76
|
+
provider="openai",
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If you call `enable()` without an `api_key` (or pass `local=True`), the SDK runs
|
|
81
|
+
in **local dev mode**: events are kept in memory and best-effort appended to
|
|
82
|
+
`./.proofledger/events.jsonl`, with no server required.
|
|
83
|
+
|
|
84
|
+
### Wrapping a unit of work with `with_run`
|
|
85
|
+
|
|
86
|
+
`with_run` opens a run, runs your function, records the result (or the error),
|
|
87
|
+
and closes the run — all on a verifiable chain. The callback receives a
|
|
88
|
+
`RunHandle` you can use to record tool calls and custom events.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from proofledger import enable, with_run, verify_run
|
|
92
|
+
|
|
93
|
+
enable(local=True) # local dev mode, no api key
|
|
94
|
+
|
|
95
|
+
def do_work(run):
|
|
96
|
+
# Record a tool call — emits tool.called + tool.returned around the record.
|
|
97
|
+
run.record_tool_call(
|
|
98
|
+
tool_name="search_kb",
|
|
99
|
+
input={"query": "refund policy"},
|
|
100
|
+
output={"hits": 3},
|
|
101
|
+
)
|
|
102
|
+
return {"answer": "Refunds within 30 days."}
|
|
103
|
+
|
|
104
|
+
result = with_run({"agent_id": "support-agent", "model": "gpt-4.1"}, do_work)
|
|
105
|
+
|
|
106
|
+
# Verify the run's hash chain.
|
|
107
|
+
report = verify_run(...) # pass the run id; see examples/basic.py
|
|
108
|
+
print(report["valid"]) # True
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
See [`examples/basic.py`](examples/basic.py) for a complete, runnable example:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python examples/basic.py
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Hashing primitives
|
|
118
|
+
|
|
119
|
+
The same primitives the dashboard uses are re-exported:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from proofledger import (
|
|
123
|
+
create_payload_hash,
|
|
124
|
+
create_event_hash,
|
|
125
|
+
verify_event_chain,
|
|
126
|
+
GENESIS_HASH,
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **Canonical JSON**: object keys are sorted recursively and serialized with no
|
|
131
|
+
whitespace and literal Unicode, matching JS `JSON.stringify` with sorted keys.
|
|
132
|
+
- `GENESIS_HASH` is 64 zeros — the `previousHash` of the first event.
|
|
133
|
+
- `create_event_hash` commits to the event id, type, timestamp, payload hash,
|
|
134
|
+
and the previous event's hash, chaining every event to the one before it.
|
|
135
|
+
|
|
136
|
+
Because the canonicalization and digests match the TypeScript SDK exactly, a
|
|
137
|
+
chain captured in Python verifies identically in the ProofLedger dashboard.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# ProofLedger Python SDK
|
|
2
|
+
|
|
3
|
+
A framework-agnostic, tamper-evident audit layer for AI agents. ProofLedger sits
|
|
4
|
+
underneath or beside any agent framework (LangGraph, CrewAI, OpenAI Agents SDK,
|
|
5
|
+
AutoGen, or a custom stack) and records every run, event, and tool call into a
|
|
6
|
+
SHA-256 hash chain.
|
|
7
|
+
|
|
8
|
+
The chains this SDK produces are **byte-for-byte compatible** with the
|
|
9
|
+
ProofLedger TypeScript SDK: runs captured from Python verify correctly in the
|
|
10
|
+
ProofLedger dashboard, which verifies using the TS implementation.
|
|
11
|
+
|
|
12
|
+
No third-party runtime dependencies — standard library only (Python >= 3.9).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install proofledger
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or from this repo:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install packages/sdk-py
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from proofledger import enable, track, with_run, verify_run
|
|
30
|
+
|
|
31
|
+
# Cloud mode — sends to your ProofLedger backend.
|
|
32
|
+
enable(
|
|
33
|
+
api_key="tl_live_...",
|
|
34
|
+
base_url="https://proofledger.dev",
|
|
35
|
+
project_id="proj_...",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# One-shot tracking of a complete, verifiable run.
|
|
39
|
+
track(
|
|
40
|
+
agent_id="support-agent",
|
|
41
|
+
input="Hello",
|
|
42
|
+
output="Hi there",
|
|
43
|
+
model="gpt-4.1",
|
|
44
|
+
provider="openai",
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you call `enable()` without an `api_key` (or pass `local=True`), the SDK runs
|
|
49
|
+
in **local dev mode**: events are kept in memory and best-effort appended to
|
|
50
|
+
`./.proofledger/events.jsonl`, with no server required.
|
|
51
|
+
|
|
52
|
+
### Wrapping a unit of work with `with_run`
|
|
53
|
+
|
|
54
|
+
`with_run` opens a run, runs your function, records the result (or the error),
|
|
55
|
+
and closes the run — all on a verifiable chain. The callback receives a
|
|
56
|
+
`RunHandle` you can use to record tool calls and custom events.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from proofledger import enable, with_run, verify_run
|
|
60
|
+
|
|
61
|
+
enable(local=True) # local dev mode, no api key
|
|
62
|
+
|
|
63
|
+
def do_work(run):
|
|
64
|
+
# Record a tool call — emits tool.called + tool.returned around the record.
|
|
65
|
+
run.record_tool_call(
|
|
66
|
+
tool_name="search_kb",
|
|
67
|
+
input={"query": "refund policy"},
|
|
68
|
+
output={"hits": 3},
|
|
69
|
+
)
|
|
70
|
+
return {"answer": "Refunds within 30 days."}
|
|
71
|
+
|
|
72
|
+
result = with_run({"agent_id": "support-agent", "model": "gpt-4.1"}, do_work)
|
|
73
|
+
|
|
74
|
+
# Verify the run's hash chain.
|
|
75
|
+
report = verify_run(...) # pass the run id; see examples/basic.py
|
|
76
|
+
print(report["valid"]) # True
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See [`examples/basic.py`](examples/basic.py) for a complete, runnable example:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python examples/basic.py
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Hashing primitives
|
|
86
|
+
|
|
87
|
+
The same primitives the dashboard uses are re-exported:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from proofledger import (
|
|
91
|
+
create_payload_hash,
|
|
92
|
+
create_event_hash,
|
|
93
|
+
verify_event_chain,
|
|
94
|
+
GENESIS_HASH,
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- **Canonical JSON**: object keys are sorted recursively and serialized with no
|
|
99
|
+
whitespace and literal Unicode, matching JS `JSON.stringify` with sorted keys.
|
|
100
|
+
- `GENESIS_HASH` is 64 zeros — the `previousHash` of the first event.
|
|
101
|
+
- `create_event_hash` commits to the event id, type, timestamp, payload hash,
|
|
102
|
+
and the previous event's hash, chaining every event to the one before it.
|
|
103
|
+
|
|
104
|
+
Because the canonicalization and digests match the TypeScript SDK exactly, a
|
|
105
|
+
chain captured in Python verifies identically in the ProofLedger dashboard.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Runnable ProofLedger example in LOCAL mode (no API key required).
|
|
2
|
+
|
|
3
|
+
Run it with::
|
|
4
|
+
|
|
5
|
+
python examples/basic.py
|
|
6
|
+
|
|
7
|
+
It performs a one-shot ``track`` and a ``with_run`` that records a tool call,
|
|
8
|
+
then prints the verification result for the ``with_run`` chain.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
# Allow running directly from a checkout without installing the package.
|
|
17
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
18
|
+
|
|
19
|
+
from proofledger import enable, track, verify_run, with_run # noqa: E402
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
# Local dev mode — no api key, events kept in memory + ./.proofledger.
|
|
24
|
+
enable(local=True)
|
|
25
|
+
|
|
26
|
+
# 1. One-shot tracking of a complete run.
|
|
27
|
+
tracked = track(
|
|
28
|
+
agent_id="support-agent",
|
|
29
|
+
input="What is your refund policy?",
|
|
30
|
+
output="Refunds are available within 30 days of purchase.",
|
|
31
|
+
model="gpt-4.1",
|
|
32
|
+
provider="openai",
|
|
33
|
+
usage={"promptTokens": 12, "completionTokens": 18},
|
|
34
|
+
)
|
|
35
|
+
track_report = verify_run(tracked["runId"])
|
|
36
|
+
print(f"track run: {tracked['runId']}")
|
|
37
|
+
print(
|
|
38
|
+
f" valid={track_report['valid']} "
|
|
39
|
+
f"events={track_report['eventCount']} "
|
|
40
|
+
f"issues={len(track_report['issues'])}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# 2. A wrapped unit of work that records a tool call.
|
|
44
|
+
captured = {}
|
|
45
|
+
|
|
46
|
+
def do_work(run):
|
|
47
|
+
captured["run_id"] = run.run_id
|
|
48
|
+
run.record_tool_call(
|
|
49
|
+
tool_name="search_kb",
|
|
50
|
+
input={"query": "refund policy"},
|
|
51
|
+
output={"hits": 3, "top": "Refunds within 30 days."},
|
|
52
|
+
)
|
|
53
|
+
return {"answer": "Refunds are available within 30 days."}
|
|
54
|
+
|
|
55
|
+
with_run(
|
|
56
|
+
{"agent_id": "support-agent", "model": "gpt-4.1", "provider": "openai"},
|
|
57
|
+
do_work,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
run_report = verify_run(captured["run_id"])
|
|
61
|
+
print(f"with_run: {captured['run_id']}")
|
|
62
|
+
print(
|
|
63
|
+
f" valid={run_report['valid']} "
|
|
64
|
+
f"events={run_report['eventCount']} "
|
|
65
|
+
f"issues={len(run_report['issues'])}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
all_valid = track_report["valid"] and run_report["valid"]
|
|
69
|
+
print()
|
|
70
|
+
print(f"All chains valid: {all_valid}")
|
|
71
|
+
sys.exit(0 if all_valid else 1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""ProofLedger Python SDK.
|
|
2
|
+
|
|
3
|
+
A framework-agnostic, tamper-evident audit layer for AI agents. Produces hash
|
|
4
|
+
chains that are byte-for-byte compatible with the ProofLedger TypeScript SDK, so
|
|
5
|
+
runs captured from Python verify correctly in the ProofLedger dashboard.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from proofledger import enable, track, with_run, verify_run
|
|
10
|
+
|
|
11
|
+
enable(api_key="tl_live_...", base_url="https://proofledger.dev",
|
|
12
|
+
project_id="proj_...")
|
|
13
|
+
track(agent_id="support-agent", input="Hello", output="Hi there",
|
|
14
|
+
model="gpt-4.1", provider="openai")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
20
|
+
|
|
21
|
+
from .client import RunHandle, ProofLedgerClient, iso_now
|
|
22
|
+
from .hashing import (
|
|
23
|
+
GENESIS_HASH,
|
|
24
|
+
ChainVerificationResult,
|
|
25
|
+
canonicalize,
|
|
26
|
+
create_event_hash,
|
|
27
|
+
create_payload_hash,
|
|
28
|
+
sha256,
|
|
29
|
+
verify_event_chain,
|
|
30
|
+
)
|
|
31
|
+
from .ids import new_id
|
|
32
|
+
from .transport import HttpTransport, LocalTransport, Transport
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Lifecycle / module-level API
|
|
36
|
+
"enable",
|
|
37
|
+
"create_client",
|
|
38
|
+
"client",
|
|
39
|
+
"track",
|
|
40
|
+
"start_run",
|
|
41
|
+
"end_run",
|
|
42
|
+
"record_event",
|
|
43
|
+
"record_tool_call",
|
|
44
|
+
"with_run",
|
|
45
|
+
"verify_run",
|
|
46
|
+
"request_approval",
|
|
47
|
+
"await_approval",
|
|
48
|
+
"require_approval",
|
|
49
|
+
"register_agent_identity",
|
|
50
|
+
"send_agent_message",
|
|
51
|
+
# Adapters
|
|
52
|
+
"instrument",
|
|
53
|
+
"wrap_openai",
|
|
54
|
+
"create_langchain_handler",
|
|
55
|
+
# Signing (Phase 6)
|
|
56
|
+
"create_agent_identity",
|
|
57
|
+
"sign_message",
|
|
58
|
+
"verify_message",
|
|
59
|
+
"signing_available",
|
|
60
|
+
# Building blocks
|
|
61
|
+
"ProofLedgerClient",
|
|
62
|
+
"RunHandle",
|
|
63
|
+
"Transport",
|
|
64
|
+
"LocalTransport",
|
|
65
|
+
"HttpTransport",
|
|
66
|
+
# Hashing
|
|
67
|
+
"create_payload_hash",
|
|
68
|
+
"create_event_hash",
|
|
69
|
+
"verify_event_chain",
|
|
70
|
+
"canonicalize",
|
|
71
|
+
"sha256",
|
|
72
|
+
"GENESIS_HASH",
|
|
73
|
+
"ChainVerificationResult",
|
|
74
|
+
"new_id",
|
|
75
|
+
"iso_now",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
T = TypeVar("T")
|
|
79
|
+
|
|
80
|
+
#: The lazily-bootstrapped module-level singleton.
|
|
81
|
+
_global_client: Optional[ProofLedgerClient] = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _get_client() -> ProofLedgerClient:
|
|
85
|
+
"""Resolve the active client.
|
|
86
|
+
|
|
87
|
+
If :func:`enable` was never called, lazily bootstrap a silent local-mode
|
|
88
|
+
client so that ``from proofledger import track`` simply works in dev.
|
|
89
|
+
"""
|
|
90
|
+
global _global_client
|
|
91
|
+
if _global_client is None:
|
|
92
|
+
_global_client = ProofLedgerClient(silent=True)
|
|
93
|
+
return _global_client
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def enable(
|
|
97
|
+
api_key: Optional[str] = None,
|
|
98
|
+
project_id: str = "default-project",
|
|
99
|
+
base_url: str = "http://localhost:3000",
|
|
100
|
+
environment: str = "development",
|
|
101
|
+
local: bool = False,
|
|
102
|
+
local_dir: Optional[str] = None,
|
|
103
|
+
silent: bool = False,
|
|
104
|
+
disabled: bool = False,
|
|
105
|
+
) -> ProofLedgerClient:
|
|
106
|
+
"""Initialize the global client and return it for advanced use."""
|
|
107
|
+
global _global_client
|
|
108
|
+
_global_client = ProofLedgerClient(
|
|
109
|
+
api_key=api_key,
|
|
110
|
+
project_id=project_id,
|
|
111
|
+
base_url=base_url,
|
|
112
|
+
environment=environment,
|
|
113
|
+
local=local,
|
|
114
|
+
local_dir=local_dir,
|
|
115
|
+
silent=silent,
|
|
116
|
+
disabled=disabled,
|
|
117
|
+
)
|
|
118
|
+
return _global_client
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def create_client(**options: Any) -> ProofLedgerClient:
|
|
122
|
+
"""Create an isolated client instead of using the global singleton."""
|
|
123
|
+
return ProofLedgerClient(**options)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def client() -> ProofLedgerClient:
|
|
127
|
+
"""The active client (auto-bootstrapped in local mode if needed)."""
|
|
128
|
+
return _get_client()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def track(**kwargs: Any) -> Dict[str, str]:
|
|
132
|
+
"""One-shot tracking. See :meth:`ProofLedgerClient.track`."""
|
|
133
|
+
return _get_client().track(**kwargs)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def start_run(**kwargs: Any) -> RunHandle:
|
|
137
|
+
return _get_client().start_run(**kwargs)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def end_run(run_id: str, result: Optional[Dict[str, Any]] = None) -> None:
|
|
141
|
+
return _get_client().end_run(run_id, result)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def record_event(**kwargs: Any) -> Dict[str, Any]:
|
|
145
|
+
return _get_client().record_event(**kwargs)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def record_tool_call(**kwargs: Any) -> Dict[str, Any]:
|
|
149
|
+
return _get_client().record_tool_call(**kwargs)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def with_run(context: Dict[str, Any], fn: Callable[[RunHandle], T]) -> T:
|
|
153
|
+
return _get_client().with_run(context, fn)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def verify_run(run_id: str) -> ChainVerificationResult:
|
|
157
|
+
return _get_client().verify_run(run_id)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def request_approval(**kwargs: Any) -> Dict[str, Any]:
|
|
161
|
+
"""Create a human-approval request. See :meth:`ProofLedgerClient.request_approval`."""
|
|
162
|
+
return _get_client().request_approval(**kwargs)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def await_approval(approval_id: str, **kwargs: Any) -> str:
|
|
166
|
+
return _get_client().await_approval(approval_id, **kwargs)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def require_approval(**kwargs: Any) -> str:
|
|
170
|
+
"""Request approval and block until decided (raises if denied)."""
|
|
171
|
+
return _get_client().require_approval(**kwargs)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def register_agent_identity(agent_id: str, public_key: str, algorithm: str = "ed25519") -> None:
|
|
175
|
+
"""Register an agent's Ed25519 public key. See ProofLedgerClient.register_agent_identity."""
|
|
176
|
+
return _get_client().register_agent_identity(agent_id, public_key, algorithm)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def send_agent_message(**kwargs: Any) -> Dict[str, Any]:
|
|
180
|
+
"""Sign and send a verified agent-to-agent message."""
|
|
181
|
+
return _get_client().send_agent_message(**kwargs)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Adapters (imported lazily-safe; they resolve the global client at call time).
|
|
185
|
+
from .adapters import instrument, wrap_openai, create_langchain_handler # noqa: E402
|
|
186
|
+
|
|
187
|
+
# Signing (Phase 6) — needs the optional `cryptography` extra.
|
|
188
|
+
from .signing import ( # noqa: E402
|
|
189
|
+
create_agent_identity,
|
|
190
|
+
sign_message,
|
|
191
|
+
verify_message,
|
|
192
|
+
signing_available,
|
|
193
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Framework adapters — one-line instrumentation for popular agent stacks.
|
|
2
|
+
|
|
3
|
+
Duck-typed: nothing is imported from OpenAI/LangChain/etc., so the SDK keeps zero
|
|
4
|
+
runtime dependencies. Adapters use the global ProofLedger client by default.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
__all__ = ["instrument", "wrap_openai", "create_langchain_handler"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _default_client() -> Any:
|
|
16
|
+
# Lazy import to avoid a circular import with the package __init__.
|
|
17
|
+
from . import client as _client
|
|
18
|
+
|
|
19
|
+
return _client()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def instrument(context: dict, fn: Callable[[Any], Any], client: Optional[Any] = None) -> Any:
|
|
23
|
+
"""Wrap any callable as a fully-tracked run.
|
|
24
|
+
|
|
25
|
+
``context`` is a dict of :meth:`ProofLedgerClient.start_run` kwargs.
|
|
26
|
+
"""
|
|
27
|
+
c = client or _default_client()
|
|
28
|
+
return c.with_run(context, fn)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def wrap_openai(openai: Any, agent_id: str = "openai", client: Optional[Any] = None) -> Any:
|
|
32
|
+
"""Instrument an OpenAI client so each ``chat.completions.create(...)`` is
|
|
33
|
+
captured as a ProofLedger run (input, output, model, token usage).
|
|
34
|
+
|
|
35
|
+
openai = wrap_openai(OpenAI(), agent_id="support-agent")
|
|
36
|
+
"""
|
|
37
|
+
c = client or _default_client()
|
|
38
|
+
completions = openai.chat.completions
|
|
39
|
+
original = completions.create
|
|
40
|
+
|
|
41
|
+
def wrapped(**params: Any) -> Any:
|
|
42
|
+
run = c.start_run(
|
|
43
|
+
agent_id=agent_id,
|
|
44
|
+
provider="openai",
|
|
45
|
+
model=params.get("model"),
|
|
46
|
+
input=params.get("messages") or params.get("input"),
|
|
47
|
+
)
|
|
48
|
+
started = time.time() * 1000.0
|
|
49
|
+
try:
|
|
50
|
+
res = original(**params)
|
|
51
|
+
message = _extract_message(res)
|
|
52
|
+
run.record_event("model.responded", {"output": message})
|
|
53
|
+
usage = _extract_usage(res)
|
|
54
|
+
c.end_run(
|
|
55
|
+
run.run_id,
|
|
56
|
+
{
|
|
57
|
+
"status": "success",
|
|
58
|
+
"output": message,
|
|
59
|
+
"latencyMs": time.time() * 1000.0 - started,
|
|
60
|
+
"usage": usage,
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
return res
|
|
64
|
+
except Exception as err: # noqa: BLE001 - re-raised below
|
|
65
|
+
c.end_run(
|
|
66
|
+
run.run_id,
|
|
67
|
+
{
|
|
68
|
+
"status": "failed",
|
|
69
|
+
"error": str(err),
|
|
70
|
+
"latencyMs": time.time() * 1000.0 - started,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
raise
|
|
74
|
+
|
|
75
|
+
completions.create = wrapped # type: ignore[assignment]
|
|
76
|
+
return openai
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _extract_message(res: Any) -> Optional[str]:
|
|
80
|
+
try:
|
|
81
|
+
choice = res.choices[0]
|
|
82
|
+
message = getattr(choice, "message", None)
|
|
83
|
+
if message is not None:
|
|
84
|
+
return getattr(message, "content", None)
|
|
85
|
+
return getattr(choice, "text", None)
|
|
86
|
+
except Exception: # noqa: BLE001
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_langchain_handler(agent_id: str = "langchain", client: Optional[Any] = None) -> Any:
|
|
91
|
+
"""A LangChain callback handler that records each LLM + tool call as a run.
|
|
92
|
+
|
|
93
|
+
Duck-typed against LangChain's ``BaseCallbackHandler`` surface (no import
|
|
94
|
+
needed). Pass it via ``callbacks=[create_langchain_handler(agent_id="...")]``.
|
|
95
|
+
"""
|
|
96
|
+
c = client or _default_client()
|
|
97
|
+
runs: dict = {}
|
|
98
|
+
|
|
99
|
+
class ProofLedgerCallbackHandler:
|
|
100
|
+
ignore_llm = False
|
|
101
|
+
ignore_chain = False
|
|
102
|
+
ignore_agent = False
|
|
103
|
+
ignore_retriever = False
|
|
104
|
+
ignore_chat_model = False
|
|
105
|
+
ignore_custom_event = False
|
|
106
|
+
raise_error = False
|
|
107
|
+
run_inline = False
|
|
108
|
+
|
|
109
|
+
def on_llm_start(self, serialized: Any, prompts: Any, **kwargs: Any) -> None:
|
|
110
|
+
run = c.start_run(
|
|
111
|
+
agent_id=agent_id,
|
|
112
|
+
provider="langchain",
|
|
113
|
+
model=(serialized or {}).get("name") if isinstance(serialized, dict) else None,
|
|
114
|
+
input=prompts,
|
|
115
|
+
)
|
|
116
|
+
runs[str(kwargs.get("run_id"))] = run
|
|
117
|
+
|
|
118
|
+
def on_llm_end(self, response: Any, **kwargs: Any) -> None:
|
|
119
|
+
run = runs.pop(str(kwargs.get("run_id")), None)
|
|
120
|
+
if run is None:
|
|
121
|
+
return
|
|
122
|
+
text = None
|
|
123
|
+
try:
|
|
124
|
+
text = response.generations[0][0].text
|
|
125
|
+
except Exception: # noqa: BLE001
|
|
126
|
+
pass
|
|
127
|
+
run.record_event("model.responded", {"output": text})
|
|
128
|
+
c.end_run(run.run_id, {"status": "success", "output": text})
|
|
129
|
+
|
|
130
|
+
def on_llm_error(self, error: Any, **kwargs: Any) -> None:
|
|
131
|
+
run = runs.pop(str(kwargs.get("run_id")), None)
|
|
132
|
+
if run is not None:
|
|
133
|
+
c.end_run(run.run_id, {"status": "failed", "error": str(error)})
|
|
134
|
+
|
|
135
|
+
def on_tool_start(self, serialized: Any, input_str: Any, **kwargs: Any) -> None:
|
|
136
|
+
parent = runs.get(str(kwargs.get("parent_run_id")))
|
|
137
|
+
if parent is not None:
|
|
138
|
+
name = (serialized or {}).get("name", "tool") if isinstance(serialized, dict) else "tool"
|
|
139
|
+
parent.record_event("tool.called", {"toolName": name, "input": input_str})
|
|
140
|
+
|
|
141
|
+
def on_tool_end(self, output: Any, **kwargs: Any) -> None:
|
|
142
|
+
parent = runs.get(str(kwargs.get("parent_run_id")))
|
|
143
|
+
if parent is not None:
|
|
144
|
+
parent.record_event("tool.returned", {"output": str(output)})
|
|
145
|
+
|
|
146
|
+
return ProofLedgerCallbackHandler()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _extract_usage(res: Any) -> Optional[dict]:
|
|
150
|
+
usage = getattr(res, "usage", None)
|
|
151
|
+
if usage is None:
|
|
152
|
+
return None
|
|
153
|
+
return {
|
|
154
|
+
"promptTokens": getattr(usage, "prompt_tokens", None),
|
|
155
|
+
"completionTokens": getattr(usage, "completion_tokens", None),
|
|
156
|
+
"totalTokens": getattr(usage, "total_tokens", None),
|
|
157
|
+
}
|