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.
@@ -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,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,7 @@
1
+ """Provedex binding for Pipecat voice agent pipelines."""
2
+
3
+ from .config import ProvedexConfig
4
+ from .processor import ProvedexFrameProcessor
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["ProvedexConfig", "ProvedexFrameProcessor"]
@@ -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