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.
@@ -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
+ }