provedex-pipecat 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.
- provedex_pipecat-0.1.0/.gitignore +10 -0
- provedex_pipecat-0.1.0/PKG-INFO +185 -0
- provedex_pipecat-0.1.0/README.md +157 -0
- provedex_pipecat-0.1.0/RELEASING.md +36 -0
- provedex_pipecat-0.1.0/examples/voice_agent_basic.py +41 -0
- provedex_pipecat-0.1.0/pyproject.toml +68 -0
- provedex_pipecat-0.1.0/src/provedex_pipecat/__init__.py +7 -0
- provedex_pipecat-0.1.0/src/provedex_pipecat/_client.py +37 -0
- provedex_pipecat-0.1.0/src/provedex_pipecat/_state.py +41 -0
- provedex_pipecat-0.1.0/src/provedex_pipecat/config.py +40 -0
- provedex_pipecat-0.1.0/src/provedex_pipecat/mapping.py +149 -0
- provedex_pipecat-0.1.0/src/provedex_pipecat/processor.py +158 -0
- provedex_pipecat-0.1.0/tests/__init__.py +0 -0
- provedex_pipecat-0.1.0/tests/conftest.py +74 -0
- provedex_pipecat-0.1.0/tests/test_async_smoke.py +71 -0
- provedex_pipecat-0.1.0/tests/test_client.py +55 -0
- provedex_pipecat-0.1.0/tests/test_config.py +40 -0
- provedex_pipecat-0.1.0/tests/test_integration.py +78 -0
- provedex_pipecat-0.1.0/tests/test_mapping.py +152 -0
- provedex_pipecat-0.1.0/tests/test_processor.py +71 -0
- provedex_pipecat-0.1.0/tests/test_state.py +37 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: provedex-pipecat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pipecat FrameProcessor that signs every frame via the Provedex sidecar.
|
|
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,pipecat,provedex,signing,voice
|
|
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: pipecat-ai<0.1.0,>=0.0.40
|
|
20
|
+
Requires-Dist: pydantic>=2.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# provedex-pipecat
|
|
30
|
+
|
|
31
|
+
provedex-pipecat is a Pipecat FrameProcessor that signs every frame in your
|
|
32
|
+
voice agent pipeline using the Provedex sidecar. One line of integration code.
|
|
33
|
+
Hash-chained, Ed25519-signed audit ledger as output. Built for regulated voice
|
|
34
|
+
agents: healthcare scribes, financial voice bots, claims handlers.
|
|
35
|
+
|
|
36
|
+
The binding translates Pipecat frames into `AgentEvent` shapes and POSTs them
|
|
37
|
+
over loopback HTTP to the Provedex sidecar. The sidecar holds the signing key
|
|
38
|
+
and ledger. Your pipeline code never touches a key.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install provedex-pipecat
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from provedex_pipecat import ProvedexConfig, ProvedexFrameProcessor
|
|
49
|
+
|
|
50
|
+
processor = ProvedexFrameProcessor(config=ProvedexConfig())
|
|
51
|
+
# Add `processor` anywhere in your Pipecat pipeline.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Assumes `provedex-agent` is running on `127.0.0.1:8765` (the default). To
|
|
55
|
+
start the agent:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
provedex-agent
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Override the URL via the `PROVEDEX_AGENT_URL` environment variable or the
|
|
62
|
+
`agent_url` constructor argument.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## Frame mapping
|
|
66
|
+
|
|
67
|
+
| Pipecat Frame | AgentEvent variant | Fields populated |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `StartFrame` | `SessionStarted` | `agent_id`, `model_id` (both from config), `session_id` (config or uuid) |
|
|
70
|
+
| `EndFrame` | `SessionEnded` | `reason = "pipeline_end"`, `summary_sha256 = sha256("")` |
|
|
71
|
+
| `TranscriptionFrame` (final) | `UtteranceCaptured` | `audio_sha256 = sha256(transcript bytes)`, `transcript`, `lang`, `duration_ms = 0` if unknown |
|
|
72
|
+
| `LLMMessagesFrame` + `LLMFullResponseEndFrame` (paired) | `ModelInvoked` | `model_id` (from config or inferred), `prompt_sha256 = sha256(canonical_json(messages))`, `response_sha256 = sha256(end_frame.text)`, `prompt_tokens = 0` if unknown, `response_tokens = 0` if unknown |
|
|
73
|
+
| `TextFrame` (final, post-LLM, no end-frame pairing) | `UtteranceSpoken` | `text_sha256 = sha256(text)`, `text`, `audio_sha256 = sha256(b"")` |
|
|
74
|
+
| `FunctionCallInProgressFrame` | `ToolCalled` | `tool_name`, `args_sha256 = sha256(canonical_json(arguments))`, `args_redacted = arguments` |
|
|
75
|
+
| `FunctionCallResultFrame` | `ToolReturned` | `tool_name`, `result_sha256 = sha256(canonical_json(result))`, `latency_ms` (measured if start-frame timestamp captured), `success` |
|
|
76
|
+
|
|
77
|
+
**Skipped frames** (not signed):
|
|
78
|
+
|
|
79
|
+
- `AudioRawFrame` - too high frequency; hashing every audio chunk would
|
|
80
|
+
saturate the ledger with noise.
|
|
81
|
+
- `InterimTranscriptionFrame` - not final; only committed transcripts are
|
|
82
|
+
auditable.
|
|
83
|
+
- `MetricsFrame` - telemetry, not a decision event.
|
|
84
|
+
- `SystemFrame` subclasses - control flow, not agent output.
|
|
85
|
+
- `LLMFullResponseStartFrame` - used internally for pairing only.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Configuration reference
|
|
89
|
+
|
|
90
|
+
| Field | Type | Default | Description |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| `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. |
|
|
93
|
+
| `session_id` | `str` | `uuid4()` | Identifier for this call session. Passed as-is into `SessionStarted`. Override to tie the ledger entry to your own session ID. |
|
|
94
|
+
| `agent_id` | `str` | `"pipecat-agent"` | Logical name of your agent. Appears in every signed event for that session. |
|
|
95
|
+
| `model_id` | `str` | `"unknown"` | LLM model identifier. Used in `ModelInvoked` events. |
|
|
96
|
+
| `include_frames` | `list[type] \| None` | `None` (use default list) | Override the set of frame types to sign. `None` uses the mapping table above. |
|
|
97
|
+
| `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 and kills the pipeline - useful in test environments. `silent` increments counters only. |
|
|
98
|
+
| `queue_size` | `int` | `1000` | Capacity of the internal deque. When full, the oldest queued event is dropped. |
|
|
99
|
+
| `request_timeout_seconds` | `float` | `2.0` | HTTP timeout for each POST to the agent. |
|
|
100
|
+
| `shutdown_drain_seconds` | `float` | `5.0` | How long to wait for the queue to drain after `EndFrame` before forwarding it downstream. |
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## Latency budget
|
|
104
|
+
|
|
105
|
+
Test rig: 1000-frame burst with a 1 ms simulated agent response time
|
|
106
|
+
(`tests/test_async_smoke.py`).
|
|
107
|
+
|
|
108
|
+
| Percentile | Producer block time |
|
|
109
|
+
|---|---|
|
|
110
|
+
| p50 | 1.1 microseconds |
|
|
111
|
+
| p99 | 2.2 microseconds |
|
|
112
|
+
|
|
113
|
+
The producer just enqueues onto a deque; the background worker does the HTTP
|
|
114
|
+
POST off the audio hot path. The signing round-trip never touches the frame's
|
|
115
|
+
pass-through latency.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## Failure modes
|
|
119
|
+
|
|
120
|
+
| Failure | Behaviour | Counter |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| Agent unreachable (ConnectionRefused) | warn + drop | `dropped_total` |
|
|
123
|
+
| Agent slow (timeout) | warn + drop | `dropped_total` |
|
|
124
|
+
| Agent 4xx | log error + apply `on_sign_failure` | `dropped_total` |
|
|
125
|
+
| Agent 5xx | warn + drop | `dropped_total` |
|
|
126
|
+
| Queue overflow | drop oldest, rate-limited warning | `overflow_total` |
|
|
127
|
+
| Frame mapping failure | log warning, drop event | n/a |
|
|
128
|
+
|
|
129
|
+
Counters are readable as attributes on the processor instance:
|
|
130
|
+
`processor.signed_total`, `processor.dropped_total`, `processor.overflow_total`.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## Architecture
|
|
134
|
+
|
|
135
|
+
This binding does not contain the signing primitive. The primitive is the Rust
|
|
136
|
+
agent at https://github.com/provedex/provedex. The binding translates Pipecat
|
|
137
|
+
frames into `AgentEvent` shapes per `docs/spec/event-schema-v1.md` and POSTs
|
|
138
|
+
them to the agent over loopback HTTP. No key material passes through Python.
|
|
139
|
+
|
|
140
|
+
The agent signs each event with the operator's Ed25519 key and chains it via
|
|
141
|
+
SHA-256 parent hashes into a local NDJSON ledger. Anyone with the public key
|
|
142
|
+
can verify the ledger offline without contacting any external service.
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## Verifying the ledger
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
provedex verify
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
provedex verify --ledger ~/.provedex/ledger.ndjson
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
provedex verify --ledger /path/to/sandboxed/ledger.ndjson
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`provedex verify` walks the chain, checks each Ed25519 signature, recomputes
|
|
160
|
+
each SHA-256 parent hash, and exits 0 on success or 1 with a diagnostic on the
|
|
161
|
+
first broken link.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Regulatory context
|
|
165
|
+
|
|
166
|
+
Tamper-evident audit logs are a direct requirement across several frameworks
|
|
167
|
+
currently in force or taking effect in 2026. The EU AI Act Article 12 requires
|
|
168
|
+
high-risk AI deployments to produce audit logs that are tamper-evident and
|
|
169
|
+
retained for at least six months; enforcement applies from August 2, 2026.
|
|
170
|
+
The Colorado AI Act (effective February 1, 2026) requires deployers of
|
|
171
|
+
high-risk AI systems to maintain records sufficient to demonstrate compliance
|
|
172
|
+
with consumer protection obligations. HIPAA's audit-control safeguard
|
|
173
|
+
(45 CFR 164.312(b)) requires clinical voice agents to record and examine
|
|
174
|
+
system activity, which for AI scribes means a verifiable transcript of every
|
|
175
|
+
utterance processed. FINRA's 2026 examination priorities identify AI agent
|
|
176
|
+
auditability as a focus area for broker-dealer supervision. A hash-chained,
|
|
177
|
+
Ed25519-signed ledger satisfies the tamper-evident requirement across all four
|
|
178
|
+
frameworks with a single integration point.
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
License: Apache-2.0
|
|
184
|
+
|
|
185
|
+
Main repo: https://github.com/provedex/provedex
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# provedex-pipecat
|
|
2
|
+
|
|
3
|
+
provedex-pipecat is a Pipecat FrameProcessor that signs every frame in your
|
|
4
|
+
voice agent pipeline using the Provedex sidecar. One line of integration code.
|
|
5
|
+
Hash-chained, Ed25519-signed audit ledger as output. Built for regulated voice
|
|
6
|
+
agents: healthcare scribes, financial voice bots, claims handlers.
|
|
7
|
+
|
|
8
|
+
The binding translates Pipecat frames into `AgentEvent` shapes and POSTs them
|
|
9
|
+
over loopback HTTP to the Provedex sidecar. The sidecar holds the signing key
|
|
10
|
+
and ledger. Your pipeline code never touches a key.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pip install provedex-pipecat
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from provedex_pipecat import ProvedexConfig, ProvedexFrameProcessor
|
|
21
|
+
|
|
22
|
+
processor = ProvedexFrameProcessor(config=ProvedexConfig())
|
|
23
|
+
# Add `processor` anywhere in your Pipecat pipeline.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Assumes `provedex-agent` is running on `127.0.0.1:8765` (the default). To
|
|
27
|
+
start the agent:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
provedex-agent
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Override the URL via the `PROVEDEX_AGENT_URL` environment variable or the
|
|
34
|
+
`agent_url` constructor argument.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Frame mapping
|
|
38
|
+
|
|
39
|
+
| Pipecat Frame | AgentEvent variant | Fields populated |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `StartFrame` | `SessionStarted` | `agent_id`, `model_id` (both from config), `session_id` (config or uuid) |
|
|
42
|
+
| `EndFrame` | `SessionEnded` | `reason = "pipeline_end"`, `summary_sha256 = sha256("")` |
|
|
43
|
+
| `TranscriptionFrame` (final) | `UtteranceCaptured` | `audio_sha256 = sha256(transcript bytes)`, `transcript`, `lang`, `duration_ms = 0` if unknown |
|
|
44
|
+
| `LLMMessagesFrame` + `LLMFullResponseEndFrame` (paired) | `ModelInvoked` | `model_id` (from config or inferred), `prompt_sha256 = sha256(canonical_json(messages))`, `response_sha256 = sha256(end_frame.text)`, `prompt_tokens = 0` if unknown, `response_tokens = 0` if unknown |
|
|
45
|
+
| `TextFrame` (final, post-LLM, no end-frame pairing) | `UtteranceSpoken` | `text_sha256 = sha256(text)`, `text`, `audio_sha256 = sha256(b"")` |
|
|
46
|
+
| `FunctionCallInProgressFrame` | `ToolCalled` | `tool_name`, `args_sha256 = sha256(canonical_json(arguments))`, `args_redacted = arguments` |
|
|
47
|
+
| `FunctionCallResultFrame` | `ToolReturned` | `tool_name`, `result_sha256 = sha256(canonical_json(result))`, `latency_ms` (measured if start-frame timestamp captured), `success` |
|
|
48
|
+
|
|
49
|
+
**Skipped frames** (not signed):
|
|
50
|
+
|
|
51
|
+
- `AudioRawFrame` - too high frequency; hashing every audio chunk would
|
|
52
|
+
saturate the ledger with noise.
|
|
53
|
+
- `InterimTranscriptionFrame` - not final; only committed transcripts are
|
|
54
|
+
auditable.
|
|
55
|
+
- `MetricsFrame` - telemetry, not a decision event.
|
|
56
|
+
- `SystemFrame` subclasses - control flow, not agent output.
|
|
57
|
+
- `LLMFullResponseStartFrame` - used internally for pairing only.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## Configuration reference
|
|
61
|
+
|
|
62
|
+
| Field | Type | Default | Description |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `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. |
|
|
65
|
+
| `session_id` | `str` | `uuid4()` | Identifier for this call session. Passed as-is into `SessionStarted`. Override to tie the ledger entry to your own session ID. |
|
|
66
|
+
| `agent_id` | `str` | `"pipecat-agent"` | Logical name of your agent. Appears in every signed event for that session. |
|
|
67
|
+
| `model_id` | `str` | `"unknown"` | LLM model identifier. Used in `ModelInvoked` events. |
|
|
68
|
+
| `include_frames` | `list[type] \| None` | `None` (use default list) | Override the set of frame types to sign. `None` uses the mapping table above. |
|
|
69
|
+
| `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 and kills the pipeline - useful in test environments. `silent` increments counters only. |
|
|
70
|
+
| `queue_size` | `int` | `1000` | Capacity of the internal deque. When full, the oldest queued event is dropped. |
|
|
71
|
+
| `request_timeout_seconds` | `float` | `2.0` | HTTP timeout for each POST to the agent. |
|
|
72
|
+
| `shutdown_drain_seconds` | `float` | `5.0` | How long to wait for the queue to drain after `EndFrame` before forwarding it downstream. |
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Latency budget
|
|
76
|
+
|
|
77
|
+
Test rig: 1000-frame burst with a 1 ms simulated agent response time
|
|
78
|
+
(`tests/test_async_smoke.py`).
|
|
79
|
+
|
|
80
|
+
| Percentile | Producer block time |
|
|
81
|
+
|---|---|
|
|
82
|
+
| p50 | 1.1 microseconds |
|
|
83
|
+
| p99 | 2.2 microseconds |
|
|
84
|
+
|
|
85
|
+
The producer just enqueues onto a deque; the background worker does the HTTP
|
|
86
|
+
POST off the audio hot path. The signing round-trip never touches the frame's
|
|
87
|
+
pass-through latency.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## Failure modes
|
|
91
|
+
|
|
92
|
+
| Failure | Behaviour | Counter |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| Agent unreachable (ConnectionRefused) | warn + drop | `dropped_total` |
|
|
95
|
+
| Agent slow (timeout) | warn + drop | `dropped_total` |
|
|
96
|
+
| Agent 4xx | log error + apply `on_sign_failure` | `dropped_total` |
|
|
97
|
+
| Agent 5xx | warn + drop | `dropped_total` |
|
|
98
|
+
| Queue overflow | drop oldest, rate-limited warning | `overflow_total` |
|
|
99
|
+
| Frame mapping failure | log warning, drop event | n/a |
|
|
100
|
+
|
|
101
|
+
Counters are readable as attributes on the processor instance:
|
|
102
|
+
`processor.signed_total`, `processor.dropped_total`, `processor.overflow_total`.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## Architecture
|
|
106
|
+
|
|
107
|
+
This binding does not contain the signing primitive. The primitive is the Rust
|
|
108
|
+
agent at https://github.com/provedex/provedex. The binding translates Pipecat
|
|
109
|
+
frames into `AgentEvent` shapes per `docs/spec/event-schema-v1.md` and POSTs
|
|
110
|
+
them to the agent over loopback HTTP. No key material passes through Python.
|
|
111
|
+
|
|
112
|
+
The agent signs each event with the operator's Ed25519 key and chains it via
|
|
113
|
+
SHA-256 parent hashes into a local NDJSON ledger. Anyone with the public key
|
|
114
|
+
can verify the ledger offline without contacting any external service.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
## Verifying the ledger
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
provedex verify
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
provedex verify --ledger ~/.provedex/ledger.ndjson
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
provedex verify --ledger /path/to/sandboxed/ledger.ndjson
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`provedex verify` walks the chain, checks each Ed25519 signature, recomputes
|
|
132
|
+
each SHA-256 parent hash, and exits 0 on success or 1 with a diagnostic on the
|
|
133
|
+
first broken link.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
## Regulatory context
|
|
137
|
+
|
|
138
|
+
Tamper-evident audit logs are a direct requirement across several frameworks
|
|
139
|
+
currently in force or taking effect in 2026. The EU AI Act Article 12 requires
|
|
140
|
+
high-risk AI deployments to produce audit logs that are tamper-evident and
|
|
141
|
+
retained for at least six months; enforcement applies from August 2, 2026.
|
|
142
|
+
The Colorado AI Act (effective February 1, 2026) requires deployers of
|
|
143
|
+
high-risk AI systems to maintain records sufficient to demonstrate compliance
|
|
144
|
+
with consumer protection obligations. HIPAA's audit-control safeguard
|
|
145
|
+
(45 CFR 164.312(b)) requires clinical voice agents to record and examine
|
|
146
|
+
system activity, which for AI scribes means a verifiable transcript of every
|
|
147
|
+
utterance processed. FINRA's 2026 examination priorities identify AI agent
|
|
148
|
+
auditability as a focus area for broker-dealer supervision. A hash-chained,
|
|
149
|
+
Ed25519-signed ledger satisfies the tamper-evident requirement across all four
|
|
150
|
+
frameworks with a single integration point.
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
License: Apache-2.0
|
|
156
|
+
|
|
157
|
+
Main repo: https://github.com/provedex/provedex
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Release process for provedex-pipecat
|
|
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 pipecat-vX.Y.Z` (use a binding-scoped tag 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-pipecat
|
|
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-pipecat` from a clean venv pulls the new version.
|
|
22
|
+
2. Confirm the README on PyPI renders correctly (long_description comes from `README.md`).
|
|
23
|
+
3. Bump the `provedex-pipecat` row in the root `README.md` Components table if anything material changed.
|
|
24
|
+
|
|
25
|
+
Yank policy:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
python -m twine yank provedex-pipecat==X.Y.Z
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
A yank does not delete the version; it stops new dependents from picking it up. Existing lockfiles keep their pin. Use yank when a published version has a hard bug; publish a fixed `X.Y.Z+1` and document the yank reason in the next release notes.
|
|
32
|
+
|
|
33
|
+
Out of scope here:
|
|
34
|
+
|
|
35
|
+
- The Rust agent + CLI publish process lives in the root `RELEASING.md`.
|
|
36
|
+
- PyPI account ownership and 2FA recovery live in 1Password under the `provedex-pipecat-pypi` entry. Not in this repo.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Minimal Pipecat pipeline with Provedex signing.
|
|
2
|
+
|
|
3
|
+
This is an illustrative skeleton. Replace the placeholder transport, STT,
|
|
4
|
+
LLM, and TTS classes with the real Pipecat services from your stack
|
|
5
|
+
(twilio_transport.TwilioTransport, deepgram.DeepgramSTTService, etc.).
|
|
6
|
+
|
|
7
|
+
Run a local provedex-agent before starting this script:
|
|
8
|
+
provedex-agent --rate-limit-off &
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from pipecat.frames.frames import EndFrame, StartFrame, TranscriptionFrame, TextFrame
|
|
15
|
+
from provedex_pipecat import ProvedexConfig, ProvedexFrameProcessor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def main() -> None:
|
|
19
|
+
cfg = ProvedexConfig(
|
|
20
|
+
agent_url=os.getenv("PROVEDEX_AGENT_URL", "http://127.0.0.1:8765"),
|
|
21
|
+
agent_id="example-voice-agent",
|
|
22
|
+
model_id="llama3.2:3b",
|
|
23
|
+
session_id="example-session-001",
|
|
24
|
+
)
|
|
25
|
+
processor = ProvedexFrameProcessor(config=cfg)
|
|
26
|
+
await processor.start()
|
|
27
|
+
|
|
28
|
+
# Simulated pipeline events. Replace with real Pipecat pipeline composition.
|
|
29
|
+
await processor.handle_frame(StartFrame())
|
|
30
|
+
await processor.handle_frame(
|
|
31
|
+
TranscriptionFrame(text="hello", user_id="caller", timestamp="t", language="en-US")
|
|
32
|
+
)
|
|
33
|
+
await processor.handle_frame(TextFrame(text="hello back"))
|
|
34
|
+
await processor.handle_frame(EndFrame())
|
|
35
|
+
|
|
36
|
+
await processor.stop()
|
|
37
|
+
print(f"signed={processor.signed_total} dropped={processor.dropped_total}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "provedex-pipecat"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pipecat FrameProcessor that signs every frame via the Provedex sidecar."
|
|
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 = ["pipecat", "voice", "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
|
+
"pipecat-ai>=0.0.40,<0.1.0",
|
|
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
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/provedex/provedex"
|
|
41
|
+
Repository = "https://github.com/provedex/provedex"
|
|
42
|
+
Issues = "https://github.com/provedex/provedex/issues"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/provedex_pipecat"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = "py311"
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
select = ["E", "F", "I", "B", "UP", "ASYNC"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint.per-file-ignores]
|
|
55
|
+
# Tests legitimately invoke cargo + provedex CLI via blocking subprocess.
|
|
56
|
+
"tests/*" = ["ASYNC221"]
|
|
57
|
+
|
|
58
|
+
[tool.mypy]
|
|
59
|
+
python_version = "3.11"
|
|
60
|
+
strict = true
|
|
61
|
+
ignore_missing_imports = true
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
asyncio_mode = "auto"
|
|
65
|
+
markers = [
|
|
66
|
+
"integration: requires real provedex-agent binary",
|
|
67
|
+
"slow: takes > 1s",
|
|
68
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
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 processor 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(
|
|
33
|
+
f"agent returned {resp.status_code}: {resp.text[:200]}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def aclose(self) -> None:
|
|
37
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Per-processor correlation buffer for paired LLM frames + frame dedup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class CorrelationState:
|
|
11
|
+
"""Track in-flight LLM exchanges and seen frame IDs."""
|
|
12
|
+
|
|
13
|
+
last_messages: list[dict[str, Any]] | None = None
|
|
14
|
+
pending_response_text: str = ""
|
|
15
|
+
response_in_progress: bool = False
|
|
16
|
+
seen_frame_ids: set[int] = field(default_factory=set)
|
|
17
|
+
|
|
18
|
+
def buffer_messages(self, messages: list[dict[str, Any]]) -> None:
|
|
19
|
+
self.last_messages = messages
|
|
20
|
+
|
|
21
|
+
def buffer_response_text(self, text: str) -> None:
|
|
22
|
+
self.pending_response_text += text
|
|
23
|
+
|
|
24
|
+
def take_paired_invocation(self) -> tuple[list[dict[str, Any]] | None, str]:
|
|
25
|
+
"""Return (messages, response_text) and clear the buffers."""
|
|
26
|
+
messages = self.last_messages
|
|
27
|
+
text = self.pending_response_text
|
|
28
|
+
self.last_messages = None
|
|
29
|
+
self.pending_response_text = ""
|
|
30
|
+
self.response_in_progress = False
|
|
31
|
+
return messages, text
|
|
32
|
+
|
|
33
|
+
def mark_response_start(self) -> None:
|
|
34
|
+
self.response_in_progress = True
|
|
35
|
+
self.pending_response_text = ""
|
|
36
|
+
|
|
37
|
+
def already_seen(self, frame_id: int) -> bool:
|
|
38
|
+
if frame_id in self.seen_frame_ids:
|
|
39
|
+
return True
|
|
40
|
+
self.seen_frame_ids.add(frame_id)
|
|
41
|
+
return False
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Configuration for the Provedex Pipecat binding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field, field_validator
|
|
10
|
+
|
|
11
|
+
OnSignFailure = Literal["warn", "raise", "silent"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProvedexConfig(BaseModel):
|
|
15
|
+
"""Configuration for ProvedexFrameProcessor.
|
|
16
|
+
|
|
17
|
+
Env-first with constructor overrides. PROVEDEX_AGENT_URL is the only
|
|
18
|
+
runtime-discovered field; everything else is set explicitly by the operator.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
agent_url: str = Field(
|
|
22
|
+
default_factory=lambda: os.getenv("PROVEDEX_AGENT_URL", "http://127.0.0.1:8765")
|
|
23
|
+
)
|
|
24
|
+
session_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
25
|
+
agent_id: str = "pipecat-agent"
|
|
26
|
+
model_id: str = "unknown"
|
|
27
|
+
include_frames: list[type] | None = None
|
|
28
|
+
on_sign_failure: OnSignFailure = "warn"
|
|
29
|
+
queue_size: int = Field(default=1000, ge=1)
|
|
30
|
+
request_timeout_seconds: float = Field(default=2.0, gt=0)
|
|
31
|
+
shutdown_drain_seconds: float = Field(default=5.0, ge=0)
|
|
32
|
+
|
|
33
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
34
|
+
|
|
35
|
+
@field_validator("agent_url")
|
|
36
|
+
@classmethod
|
|
37
|
+
def url_must_be_http(cls, v: str) -> str:
|
|
38
|
+
if not v.startswith(("http://", "https://")):
|
|
39
|
+
raise ValueError(f"agent_url must start with http:// or https://, got {v!r}")
|
|
40
|
+
return v
|