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.
@@ -0,0 +1,11 @@
1
+ # Local dev event log
2
+ .proofledger/
3
+
4
+ # Python build / cache artifacts
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.egg-info/
8
+ build/
9
+ dist/
10
+ .pytest_cache/
11
+ .venv/
@@ -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
+ }