provedex-langchain 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,10 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ .coverage
9
+ htmlcov/
10
+ *.pyc
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: provedex-langchain
3
+ Version: 0.1.0
4
+ Summary: LangChain BaseCallbackHandler that signs every LLM and tool callback via the Provedex sidecar. Covers LangGraph by inheritance.
5
+ Project-URL: Homepage, https://github.com/provedex/provedex
6
+ Project-URL: Repository, https://github.com/provedex/provedex
7
+ Project-URL: Issues, https://github.com/provedex/provedex/issues
8
+ Author-email: Aditya Suresh <adi@provedex.io>
9
+ License: Apache-2.0
10
+ Keywords: audit,compliance,ed25519,langchain,langgraph,provedex,signing
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: httpx>=0.27
19
+ Requires-Dist: langchain-core<0.4,>=0.3
20
+ Requires-Dist: pydantic>=2.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: langchain-openai>=0.2; extra == 'dev'
23
+ Requires-Dist: langchain<0.4,>=0.3; extra == 'dev'
24
+ Requires-Dist: langgraph>=0.2; extra == 'dev'
25
+ Requires-Dist: mypy>=1.10; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: respx>=0.21; extra == 'dev'
29
+ Requires-Dist: ruff>=0.5; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # provedex-langchain
33
+
34
+ `provedex-langchain` is a LangChain `BaseCallbackHandler` that signs every LLM
35
+ call, tool call, and operator session boundary via the Provedex sidecar. Each
36
+ event gets an Ed25519 signature and a SHA-256 parent hash, written to a local
37
+ NDJSON ledger that anyone with the operator's public key can verify offline.
38
+ The primary buyers are regulated-AI shops: healthcare scribes processing clinical
39
+ notes, financial bots executing trades or claims, and customer-service agents
40
+ subject to FINRA or state AI-act supervision. One `ProvedexCallbackHandler`
41
+ instance covers both LangChain LCEL pipelines and LangGraph state machines,
42
+ because LangGraph propagates LangChain callbacks for every LLM and tool step.
43
+
44
+ ## Quickstart
45
+
46
+ ```
47
+ pip install provedex-langchain
48
+ ```
49
+
50
+ Start the sidecar first (default `127.0.0.1:8765`):
51
+
52
+ ```
53
+ provedex-agent --rate-limit-off &
54
+ ```
55
+
56
+ ```python
57
+ from provedex_langchain import ProvedexCallbackHandler, ProvedexConfig
58
+
59
+ handler = ProvedexCallbackHandler(config=ProvedexConfig())
60
+ # pass to your chain or graph:
61
+ chain.invoke({"q": "hi"}, config={"callbacks": [handler]})
62
+ ```
63
+
64
+ ## Callback mapping
65
+
66
+ | LangChain callback(s) | AgentEvent variant | Fields populated |
67
+ |---|---|---|
68
+ | `start_session()` (operator call) | `SessionStarted` | `agent_id`, `model_id`, `session_id` from config |
69
+ | `end_session(reason)` (operator call) | `SessionEnded` | `reason`, `summary_sha256 = sha256("")` |
70
+ | `on_llm_start` / `aon_llm_start` | none (buffered by `run_id`) | model id from `serialized.get("id")`, joined prompts, start timestamp |
71
+ | `on_chat_model_start` / `aon_chat_model_start` | none (buffered) | model id, flattened message list, start timestamp |
72
+ | `on_llm_end` / `aon_llm_end` (paired with start by `run_id`) | `ModelInvoked` | `model_id`, `prompt_sha256`, `response_sha256`, `prompt_tokens` / `response_tokens` from `llm_output.get("token_usage", {})` if present (else 0) |
73
+ | `on_llm_error` (paired) | `ModelInvoked` | `response_sha256 = sha256(f"{type(error).__name__}: {error}")` |
74
+ | `on_tool_start` / `aon_tool_start` | `ToolCalled` | `tool_name`, `args_sha256` of canonical-JSON of args, `args_redacted` |
75
+ | `on_tool_end` / `aon_tool_end` | `ToolReturned` | `tool_name`, `result_sha256`, `latency_ms`, `success = True` |
76
+ | `on_tool_error` | `ToolReturned` | `tool_name`, `result_sha256` of error description, `latency_ms`, `success = False` |
77
+
78
+ **Skipped** (not signed in v0.1): `on_llm_new_token` (per-token noise),
79
+ `on_chain_start` / `on_chain_end` (LCEL composition makes chain boundaries
80
+ ambiguous), `on_agent_action` / `on_agent_finish` (covered by tool events),
81
+ `on_retriever_start` / `on_retriever_end` (no v1 event variant), `on_text`
82
+ (no defined semantics).
83
+
84
+ ## Configuration reference
85
+
86
+ | Field | Type | Default | Description |
87
+ |---|---|---|---|
88
+ | `agent_url` | `str` | `$PROVEDEX_AGENT_URL` or `http://127.0.0.1:8765` | URL of the running `provedex-agent`. Override via env var `PROVEDEX_AGENT_URL` or constructor argument. |
89
+ | `session_id` | `str` | `uuid4()` | Identifier for this call session. Override to tie the ledger entry to your own session ID. |
90
+ | `agent_id` | `str` | `"langchain-agent"` | Logical name of your agent. Appears in every signed event for that session. |
91
+ | `model_id` | `str` | `"unknown"` | LLM model identifier. Used in `ModelInvoked` events when the callback args do not supply one. |
92
+ | `on_sign_failure` | `"warn" \| "raise" \| "silent"` | `"warn"` | What to do when the agent returns 4xx. `warn` logs a warning and continues. `raise` propagates the exception out of the background worker - useful in test environments. `silent` increments counters only. |
93
+ | `queue_size` | `int` | `1000` | Capacity of the internal deque. When full, the oldest queued event is dropped. |
94
+ | `request_timeout_seconds` | `float` | `2.0` | HTTP timeout for each POST to the agent. |
95
+ | `shutdown_drain_seconds` | `float` | `5.0` | How long to wait for the queue to drain after `handler.stop()` before returning. |
96
+
97
+ ## Session lifecycle
98
+
99
+ A session groups a set of LLM and tool events under a single `SessionStarted` /
100
+ `SessionEnded` pair. The operator controls session boundaries - the handler does
101
+ not infer them from chain hierarchy.
102
+
103
+ Explicit form:
104
+
105
+ ```python
106
+ handler.start_session()
107
+ chain.invoke({"q": "hi"}, config={"callbacks": [handler]})
108
+ handler.end_session(reason="request_complete")
109
+ ```
110
+
111
+ Context manager (sync):
112
+
113
+ ```python
114
+ with handler.session("user-12345-request"):
115
+ chain.invoke({"q": "hi"}, config={"callbacks": [handler]})
116
+ ```
117
+
118
+ Context manager (async):
119
+
120
+ ```python
121
+ async with handler.session("user-12345-request"):
122
+ await chain.ainvoke({"q": "hi"}, config={"callbacks": [handler]})
123
+ ```
124
+
125
+ On exception, the context manager calls `end_session` with `reason` set to the
126
+ exception class name, so the ledger always has a closed session boundary.
127
+
128
+ ## Latency budget
129
+
130
+ The handler's hot path is a single `deque.appendleft()` call. The background
131
+ worker thread drains the deque and performs the HTTP POST off the LLM call
132
+ thread.
133
+
134
+ Measured against a 1ms-latency mock agent with a 1000-callback burst
135
+ (one `on_llm_start` + `on_llm_end` pair per iteration):
136
+
137
+ - p50 producer overhead: 2.5 microseconds
138
+ - p99 producer overhead: 5 microseconds
139
+
140
+ The LLM call thread is not blocked by network I/O.
141
+
142
+ ## Failure modes
143
+
144
+ | Failure | Behaviour | Counter |
145
+ |---|---|---|
146
+ | Agent unreachable (ConnectionRefused) | warn + drop | `dropped_total` |
147
+ | Agent slow (timeout) | warn + drop | `dropped_total` |
148
+ | Agent 4xx | log error + apply `on_sign_failure` | `dropped_total` |
149
+ | Agent 5xx | warn + drop | `dropped_total` |
150
+ | Queue overflow | drop oldest, rate-limited warning | `overflow_total` |
151
+ | Callback with missing fields | log warning, skip enqueue | n/a |
152
+ | `run_id` missing on `on_llm_end` (no paired start) | log warning, skip emission | n/a |
153
+
154
+ Counters are readable as attributes on the handler instance:
155
+ `handler.signed_total`, `handler.dropped_total`, `handler.overflow_total`.
156
+
157
+ ## LangGraph
158
+
159
+ LangGraph fires LangChain callbacks for every LLM and tool step inside a graph.
160
+ No additional integration is required. The operator wraps the graph invocation
161
+ inside a session context:
162
+
163
+ ```python
164
+ async with handler.session("graph-run"):
165
+ await graph.invoke(state, config={"callbacks": [handler]})
166
+ ```
167
+
168
+ Graph-specific events (CheckpointSaved, node enter / exit, edge transitions) are
169
+ NOT signed in v0.1. They are documented as a follow-up item once a customer
170
+ surfaces a concrete audit requirement for checkpoint-level granularity.
171
+
172
+ ## Architecture
173
+
174
+ This binding does not contain the signing primitive. The primitive is the Rust
175
+ sidecar at https://github.com/provedex/provedex. The binding translates
176
+ LangChain callback arguments into `AgentEvent` shapes per
177
+ `docs/spec/event-schema-v1.md` and POSTs them to the sidecar over loopback
178
+ HTTP. The translation is pure Python with no C extensions required.
179
+
180
+ The sidecar signs each event with the operator's Ed25519 private key and chains
181
+ it via SHA-256 parent hashes into a local NDJSON ledger. Each event record
182
+ contains the signature, the parent hash, and the payload hash. Anyone with the
183
+ operator's public key can run `provedex verify` against the ledger file offline,
184
+ without network access and without trusting a third party.
185
+
186
+ ## Verifying the ledger
187
+
188
+ ```
189
+ provedex verify
190
+ provedex verify --ledger ~/.provedex/ledger.ndjson
191
+ provedex verify --ledger /path/to/sandboxed/ledger.ndjson
192
+ ```
193
+
194
+ The command reads the NDJSON file, checks the Ed25519 signature on every record,
195
+ and verifies the SHA-256 hash chain is unbroken. Exit code 0 means the ledger is
196
+ intact.
197
+
198
+ ## Regulatory context
199
+
200
+ Tamper-evident audit logs are a direct requirement across several frameworks
201
+ currently in force or taking effect in 2026. The EU AI Act Article 12 requires
202
+ high-risk AI deployments to produce audit logs that are tamper-evident and
203
+ retained for at least six months; enforcement applies from August 2, 2026.
204
+ The Colorado AI Act (effective February 1, 2026) requires deployers of
205
+ high-risk AI systems to maintain records sufficient to demonstrate compliance
206
+ with consumer protection obligations. HIPAA's audit-control safeguard
207
+ (45 CFR 164.312(b)) requires clinical voice agents to record and examine
208
+ system activity, which for AI scribes means a verifiable transcript of every
209
+ utterance processed. FINRA's 2026 examination priorities identify AI agent
210
+ auditability as a focus area for broker-dealer supervision. A hash-chained,
211
+ Ed25519-signed ledger satisfies the tamper-evident requirement across all four
212
+ frameworks with a single integration point.
213
+
214
+ ---
215
+
216
+ License: Apache-2.0. Main repo: https://github.com/provedex/provedex
@@ -0,0 +1,185 @@
1
+ # provedex-langchain
2
+
3
+ `provedex-langchain` is a LangChain `BaseCallbackHandler` that signs every LLM
4
+ call, tool call, and operator session boundary via the Provedex sidecar. Each
5
+ event gets an Ed25519 signature and a SHA-256 parent hash, written to a local
6
+ NDJSON ledger that anyone with the operator's public key can verify offline.
7
+ The primary buyers are regulated-AI shops: healthcare scribes processing clinical
8
+ notes, financial bots executing trades or claims, and customer-service agents
9
+ subject to FINRA or state AI-act supervision. One `ProvedexCallbackHandler`
10
+ instance covers both LangChain LCEL pipelines and LangGraph state machines,
11
+ because LangGraph propagates LangChain callbacks for every LLM and tool step.
12
+
13
+ ## Quickstart
14
+
15
+ ```
16
+ pip install provedex-langchain
17
+ ```
18
+
19
+ Start the sidecar first (default `127.0.0.1:8765`):
20
+
21
+ ```
22
+ provedex-agent --rate-limit-off &
23
+ ```
24
+
25
+ ```python
26
+ from provedex_langchain import ProvedexCallbackHandler, ProvedexConfig
27
+
28
+ handler = ProvedexCallbackHandler(config=ProvedexConfig())
29
+ # pass to your chain or graph:
30
+ chain.invoke({"q": "hi"}, config={"callbacks": [handler]})
31
+ ```
32
+
33
+ ## Callback mapping
34
+
35
+ | LangChain callback(s) | AgentEvent variant | Fields populated |
36
+ |---|---|---|
37
+ | `start_session()` (operator call) | `SessionStarted` | `agent_id`, `model_id`, `session_id` from config |
38
+ | `end_session(reason)` (operator call) | `SessionEnded` | `reason`, `summary_sha256 = sha256("")` |
39
+ | `on_llm_start` / `aon_llm_start` | none (buffered by `run_id`) | model id from `serialized.get("id")`, joined prompts, start timestamp |
40
+ | `on_chat_model_start` / `aon_chat_model_start` | none (buffered) | model id, flattened message list, start timestamp |
41
+ | `on_llm_end` / `aon_llm_end` (paired with start by `run_id`) | `ModelInvoked` | `model_id`, `prompt_sha256`, `response_sha256`, `prompt_tokens` / `response_tokens` from `llm_output.get("token_usage", {})` if present (else 0) |
42
+ | `on_llm_error` (paired) | `ModelInvoked` | `response_sha256 = sha256(f"{type(error).__name__}: {error}")` |
43
+ | `on_tool_start` / `aon_tool_start` | `ToolCalled` | `tool_name`, `args_sha256` of canonical-JSON of args, `args_redacted` |
44
+ | `on_tool_end` / `aon_tool_end` | `ToolReturned` | `tool_name`, `result_sha256`, `latency_ms`, `success = True` |
45
+ | `on_tool_error` | `ToolReturned` | `tool_name`, `result_sha256` of error description, `latency_ms`, `success = False` |
46
+
47
+ **Skipped** (not signed in v0.1): `on_llm_new_token` (per-token noise),
48
+ `on_chain_start` / `on_chain_end` (LCEL composition makes chain boundaries
49
+ ambiguous), `on_agent_action` / `on_agent_finish` (covered by tool events),
50
+ `on_retriever_start` / `on_retriever_end` (no v1 event variant), `on_text`
51
+ (no defined semantics).
52
+
53
+ ## Configuration reference
54
+
55
+ | Field | Type | Default | Description |
56
+ |---|---|---|---|
57
+ | `agent_url` | `str` | `$PROVEDEX_AGENT_URL` or `http://127.0.0.1:8765` | URL of the running `provedex-agent`. Override via env var `PROVEDEX_AGENT_URL` or constructor argument. |
58
+ | `session_id` | `str` | `uuid4()` | Identifier for this call session. Override to tie the ledger entry to your own session ID. |
59
+ | `agent_id` | `str` | `"langchain-agent"` | Logical name of your agent. Appears in every signed event for that session. |
60
+ | `model_id` | `str` | `"unknown"` | LLM model identifier. Used in `ModelInvoked` events when the callback args do not supply one. |
61
+ | `on_sign_failure` | `"warn" \| "raise" \| "silent"` | `"warn"` | What to do when the agent returns 4xx. `warn` logs a warning and continues. `raise` propagates the exception out of the background worker - useful in test environments. `silent` increments counters only. |
62
+ | `queue_size` | `int` | `1000` | Capacity of the internal deque. When full, the oldest queued event is dropped. |
63
+ | `request_timeout_seconds` | `float` | `2.0` | HTTP timeout for each POST to the agent. |
64
+ | `shutdown_drain_seconds` | `float` | `5.0` | How long to wait for the queue to drain after `handler.stop()` before returning. |
65
+
66
+ ## Session lifecycle
67
+
68
+ A session groups a set of LLM and tool events under a single `SessionStarted` /
69
+ `SessionEnded` pair. The operator controls session boundaries - the handler does
70
+ not infer them from chain hierarchy.
71
+
72
+ Explicit form:
73
+
74
+ ```python
75
+ handler.start_session()
76
+ chain.invoke({"q": "hi"}, config={"callbacks": [handler]})
77
+ handler.end_session(reason="request_complete")
78
+ ```
79
+
80
+ Context manager (sync):
81
+
82
+ ```python
83
+ with handler.session("user-12345-request"):
84
+ chain.invoke({"q": "hi"}, config={"callbacks": [handler]})
85
+ ```
86
+
87
+ Context manager (async):
88
+
89
+ ```python
90
+ async with handler.session("user-12345-request"):
91
+ await chain.ainvoke({"q": "hi"}, config={"callbacks": [handler]})
92
+ ```
93
+
94
+ On exception, the context manager calls `end_session` with `reason` set to the
95
+ exception class name, so the ledger always has a closed session boundary.
96
+
97
+ ## Latency budget
98
+
99
+ The handler's hot path is a single `deque.appendleft()` call. The background
100
+ worker thread drains the deque and performs the HTTP POST off the LLM call
101
+ thread.
102
+
103
+ Measured against a 1ms-latency mock agent with a 1000-callback burst
104
+ (one `on_llm_start` + `on_llm_end` pair per iteration):
105
+
106
+ - p50 producer overhead: 2.5 microseconds
107
+ - p99 producer overhead: 5 microseconds
108
+
109
+ The LLM call thread is not blocked by network I/O.
110
+
111
+ ## Failure modes
112
+
113
+ | Failure | Behaviour | Counter |
114
+ |---|---|---|
115
+ | Agent unreachable (ConnectionRefused) | warn + drop | `dropped_total` |
116
+ | Agent slow (timeout) | warn + drop | `dropped_total` |
117
+ | Agent 4xx | log error + apply `on_sign_failure` | `dropped_total` |
118
+ | Agent 5xx | warn + drop | `dropped_total` |
119
+ | Queue overflow | drop oldest, rate-limited warning | `overflow_total` |
120
+ | Callback with missing fields | log warning, skip enqueue | n/a |
121
+ | `run_id` missing on `on_llm_end` (no paired start) | log warning, skip emission | n/a |
122
+
123
+ Counters are readable as attributes on the handler instance:
124
+ `handler.signed_total`, `handler.dropped_total`, `handler.overflow_total`.
125
+
126
+ ## LangGraph
127
+
128
+ LangGraph fires LangChain callbacks for every LLM and tool step inside a graph.
129
+ No additional integration is required. The operator wraps the graph invocation
130
+ inside a session context:
131
+
132
+ ```python
133
+ async with handler.session("graph-run"):
134
+ await graph.invoke(state, config={"callbacks": [handler]})
135
+ ```
136
+
137
+ Graph-specific events (CheckpointSaved, node enter / exit, edge transitions) are
138
+ NOT signed in v0.1. They are documented as a follow-up item once a customer
139
+ surfaces a concrete audit requirement for checkpoint-level granularity.
140
+
141
+ ## Architecture
142
+
143
+ This binding does not contain the signing primitive. The primitive is the Rust
144
+ sidecar at https://github.com/provedex/provedex. The binding translates
145
+ LangChain callback arguments into `AgentEvent` shapes per
146
+ `docs/spec/event-schema-v1.md` and POSTs them to the sidecar over loopback
147
+ HTTP. The translation is pure Python with no C extensions required.
148
+
149
+ The sidecar signs each event with the operator's Ed25519 private key and chains
150
+ it via SHA-256 parent hashes into a local NDJSON ledger. Each event record
151
+ contains the signature, the parent hash, and the payload hash. Anyone with the
152
+ operator's public key can run `provedex verify` against the ledger file offline,
153
+ without network access and without trusting a third party.
154
+
155
+ ## Verifying the ledger
156
+
157
+ ```
158
+ provedex verify
159
+ provedex verify --ledger ~/.provedex/ledger.ndjson
160
+ provedex verify --ledger /path/to/sandboxed/ledger.ndjson
161
+ ```
162
+
163
+ The command reads the NDJSON file, checks the Ed25519 signature on every record,
164
+ and verifies the SHA-256 hash chain is unbroken. Exit code 0 means the ledger is
165
+ intact.
166
+
167
+ ## Regulatory context
168
+
169
+ Tamper-evident audit logs are a direct requirement across several frameworks
170
+ currently in force or taking effect in 2026. The EU AI Act Article 12 requires
171
+ high-risk AI deployments to produce audit logs that are tamper-evident and
172
+ retained for at least six months; enforcement applies from August 2, 2026.
173
+ The Colorado AI Act (effective February 1, 2026) requires deployers of
174
+ high-risk AI systems to maintain records sufficient to demonstrate compliance
175
+ with consumer protection obligations. HIPAA's audit-control safeguard
176
+ (45 CFR 164.312(b)) requires clinical voice agents to record and examine
177
+ system activity, which for AI scribes means a verifiable transcript of every
178
+ utterance processed. FINRA's 2026 examination priorities identify AI agent
179
+ auditability as a focus area for broker-dealer supervision. A hash-chained,
180
+ Ed25519-signed ledger satisfies the tamper-evident requirement across all four
181
+ frameworks with a single integration point.
182
+
183
+ ---
184
+
185
+ License: Apache-2.0. Main repo: https://github.com/provedex/provedex
@@ -0,0 +1,27 @@
1
+ # Release process for provedex-langchain
2
+
3
+ Pre-release checklist:
4
+
5
+ 1. All tests pass locally and in CI.
6
+ 2. `pyproject.toml` version bumped if shipping a new version.
7
+ 3. Tag the binding release: `git tag langchain-vX.Y.Z` (binding-scoped prefix so it does not collide with the agent's `vX.Y.Z` tags).
8
+
9
+ Publish to PyPI:
10
+
11
+ ```
12
+ cd bindings/python/provedex-langchain
13
+ python -m pip install --upgrade build twine
14
+ python -m build
15
+ python -m twine check dist/*
16
+ python -m twine upload dist/*
17
+ ```
18
+
19
+ After publish:
20
+
21
+ 1. Verify `pip install provedex-langchain` from a clean venv pulls the new version.
22
+ 2. Confirm the README on PyPI renders correctly (long_description from `README.md`).
23
+ 3. Update the `provedex-langchain` row in the root `README.md` Components table if anything material changed.
24
+
25
+ Yank policy: same as `provedex-pipecat`; see `bindings/python/provedex-pipecat/RELEASING.md` for the procedure.
26
+
27
+ Out of scope here: the Rust agent + CLI publish process lives in the root `RELEASING.md`.
@@ -0,0 +1,38 @@
1
+ """Minimal LangChain LCEL pipeline with Provedex signing.
2
+
3
+ Run a local provedex-agent before starting:
4
+ provedex-agent --rate-limit-off &
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+
10
+ from langchain_core.language_models.fake_chat_models import FakeListChatModel
11
+ from langchain_core.prompts import ChatPromptTemplate
12
+
13
+ from provedex_langchain import ProvedexCallbackHandler, ProvedexConfig
14
+
15
+
16
+ async def main() -> None:
17
+ cfg = ProvedexConfig(
18
+ agent_url=os.getenv("PROVEDEX_AGENT_URL", "http://127.0.0.1:8765"),
19
+ agent_id="example-langchain-agent",
20
+ model_id="fake-list",
21
+ session_id="example-session-001",
22
+ )
23
+ handler = ProvedexCallbackHandler(config=cfg)
24
+
25
+ # Replace FakeListChatModel with ChatOpenAI(model="gpt-4o") or any real LLM.
26
+ llm = FakeListChatModel(responses=["Hello back."])
27
+ prompt = ChatPromptTemplate.from_template("Say hi to {name}.")
28
+ chain = prompt | llm
29
+
30
+ async with handler.session("example-request"):
31
+ await chain.ainvoke({"name": "world"}, config={"callbacks": [handler]})
32
+
33
+ await handler.stop()
34
+ print(f"signed={handler.signed_total} dropped={handler.dropped_total}")
35
+
36
+
37
+ if __name__ == "__main__":
38
+ asyncio.run(main())
@@ -0,0 +1,52 @@
1
+ """Minimal LangGraph pipeline with Provedex signing.
2
+
3
+ Demonstrates that LangGraph users get audit coverage automatically because
4
+ LangGraph propagates LangChain callbacks for every LLM and tool call.
5
+
6
+ Run a local provedex-agent before starting:
7
+ provedex-agent --rate-limit-off &
8
+ """
9
+
10
+ import asyncio
11
+ import os
12
+ from typing import TypedDict
13
+
14
+ from langchain_core.language_models.fake_chat_models import FakeListChatModel
15
+ from langgraph.graph import END, START, StateGraph
16
+
17
+ from provedex_langchain import ProvedexCallbackHandler, ProvedexConfig
18
+
19
+
20
+ class State(TypedDict):
21
+ answer: str
22
+
23
+
24
+ async def main() -> None:
25
+ cfg = ProvedexConfig(
26
+ agent_url=os.getenv("PROVEDEX_AGENT_URL", "http://127.0.0.1:8765"),
27
+ agent_id="example-langgraph-agent",
28
+ model_id="fake-list",
29
+ )
30
+ handler = ProvedexCallbackHandler(config=cfg)
31
+
32
+ llm = FakeListChatModel(responses=["Hello from the graph."])
33
+
34
+ async def respond(state: State, config) -> State:
35
+ resp = await llm.ainvoke("greet user", config=config)
36
+ return {"answer": resp.content}
37
+
38
+ graph_builder = StateGraph(State)
39
+ graph_builder.add_node("respond", respond)
40
+ graph_builder.add_edge(START, "respond")
41
+ graph_builder.add_edge("respond", END)
42
+ graph = graph_builder.compile()
43
+
44
+ async with handler.session("graph-example"):
45
+ await graph.ainvoke({"answer": ""}, config={"callbacks": [handler]})
46
+
47
+ await handler.stop()
48
+ print(f"signed={handler.signed_total} dropped={handler.dropped_total}")
49
+
50
+
51
+ if __name__ == "__main__":
52
+ asyncio.run(main())
@@ -0,0 +1,71 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "provedex-langchain"
7
+ version = "0.1.0"
8
+ description = "LangChain BaseCallbackHandler that signs every LLM and tool callback via the Provedex sidecar. Covers LangGraph by inheritance."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [
13
+ { name = "Aditya Suresh", email = "adi@provedex.io" },
14
+ ]
15
+ keywords = ["langchain", "langgraph", "audit", "signing", "compliance", "ed25519", "provedex"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Security :: Cryptography",
23
+ ]
24
+ dependencies = [
25
+ "langchain-core>=0.3,<0.4",
26
+ "httpx>=0.27",
27
+ "pydantic>=2.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=8.0",
33
+ "pytest-asyncio>=0.23",
34
+ "respx>=0.21",
35
+ "ruff>=0.5",
36
+ "mypy>=1.10",
37
+ "langchain>=0.3,<0.4",
38
+ "langchain-openai>=0.2",
39
+ "langgraph>=0.2",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/provedex/provedex"
44
+ Repository = "https://github.com/provedex/provedex"
45
+ Issues = "https://github.com/provedex/provedex/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/provedex_langchain"]
49
+
50
+ [tool.ruff]
51
+ line-length = 100
52
+ target-version = "py311"
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "B", "UP", "ASYNC"]
56
+
57
+ [tool.ruff.lint.per-file-ignores]
58
+ # Tests legitimately invoke cargo + provedex CLI via blocking subprocess.
59
+ "tests/*" = ["ASYNC221"]
60
+
61
+ [tool.mypy]
62
+ python_version = "3.11"
63
+ strict = true
64
+ ignore_missing_imports = true
65
+
66
+ [tool.pytest.ini_options]
67
+ asyncio_mode = "auto"
68
+ markers = [
69
+ "integration: requires real provedex-agent binary",
70
+ "slow: takes > 1s",
71
+ ]
@@ -0,0 +1,7 @@
1
+ """Provedex binding for LangChain (and LangGraph by inheritance)."""
2
+
3
+ from .config import ProvedexConfig
4
+ from .handler import ProvedexCallbackHandler
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["ProvedexCallbackHandler", "ProvedexConfig"]
@@ -0,0 +1,35 @@
1
+ """Private async HTTP client for the provedex-agent /v1/sign endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+
10
+ class SignError(Exception):
11
+ """Raised when a sign attempt fails (network, timeout, or non-2xx)."""
12
+
13
+
14
+ class AgentClient:
15
+ """Thin httpx wrapper. One per handler instance; reuses the connection."""
16
+
17
+ def __init__(self, base_url: str, timeout: float) -> None:
18
+ self._base_url = base_url.rstrip("/")
19
+ self._client = httpx.AsyncClient(
20
+ base_url=self._base_url,
21
+ timeout=httpx.Timeout(timeout, connect=timeout),
22
+ headers={"content-type": "application/json"},
23
+ )
24
+
25
+ async def sign(self, event: dict[str, Any]) -> None:
26
+ """POST {event: ...} to /v1/sign. Raises SignError on any failure."""
27
+ try:
28
+ resp = await self._client.post("/v1/sign", json={"event": event})
29
+ except httpx.HTTPError as e:
30
+ raise SignError(f"agent unreachable: {e}") from e
31
+ if resp.status_code >= 400:
32
+ raise SignError(f"agent returned {resp.status_code}: {resp.text[:200]}")
33
+
34
+ async def aclose(self) -> None:
35
+ await self._client.aclose()