proofledger 0.1.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.
- proofledger/__init__.py +193 -0
- proofledger/adapters.py +157 -0
- proofledger/client.py +544 -0
- proofledger/hashing.py +208 -0
- proofledger/ids.py +32 -0
- proofledger/signing.py +77 -0
- proofledger/transport.py +225 -0
- proofledger-0.1.0.dist-info/METADATA +137 -0
- proofledger-0.1.0.dist-info/RECORD +11 -0
- proofledger-0.1.0.dist-info/WHEEL +4 -0
- proofledger-0.1.0.dist-info/licenses/LICENSE +21 -0
proofledger/__init__.py
ADDED
|
@@ -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
|
+
)
|
proofledger/adapters.py
ADDED
|
@@ -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
|
+
}
|