Skip to content

Commit f7c107d

Browse files
munkhorgilclaude
andcommitted
feat(observer): add optional roark_integration_id for call attribution
Adds a keyword-only `roark_integration_id` to `RoarkObserver`, defaulting from the `ROARK_INTEGRATION_ID` env var (self-hosted) / `pcc secrets` (Pipecat Cloud); the constructor arg wins when both are present. When set it ships as `roarkIntegrationId` on both the `call-started` and `call-ended` webhook bodies so app-roark-analytics can attribute each call to a specific Roark Pipecat integration. Fully backward compatible — when unset the field is omitted and the server falls back to the project's self-hosted integration. The observer stays transport-agnostic; the id is the only new field. Bumps version to 0.1.5 and documents ROARK_INTEGRATION_ID in the README setup / running-modes sections, .env.example, and the example bot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a6a97e5 commit f7c107d

9 files changed

Lines changed: 120 additions & 7 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
# This is the only Roark setting you need; the observer knows its own endpoints.
77
ROARK_API_KEY=rk_live_replace_me
88

9+
# Roark integration id — copy it from the Roark dashboard when you create a
10+
# Pipecat integration. Optional; omit to fall back to the project's self-hosted integration.
11+
ROARK_INTEGRATION_ID=
12+
913
# ---------------------------------------------------------------------------
1014
# Services — only needed to run examples/bot.py (not by the library itself).
1115
# Swap providers freely in your own pipeline; RoarkObserver is provider-agnostic.

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- **Optional `roark_integration_id`** to attribute each call to a specific Roark
12+
Pipecat integration. Pass it to `RoarkObserver(roark_integration_id=...)` or
13+
set the `ROARK_INTEGRATION_ID` environment variable (the constructor arg wins);
14+
copy the id from the Roark dashboard when you create a Pipecat integration. When
15+
set, it ships as `roarkIntegrationId` on both the `call-started` and
16+
`call-ended` webhook bodies. Fully backward compatible — when unset the field is
17+
omitted and Roark falls back to the project's self-hosted integration.
18+
919
## [0.1.4] - 2026-05-28
1020

1121
### Added

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,17 @@ Set one env var:
4545

4646
```bash
4747
ROARK_API_KEY=rk_live_...
48+
49+
# Optional — copy from the Roark dashboard when you create a Pipecat integration.
50+
ROARK_INTEGRATION_ID=...
4851
```
4952

5053
> The Roark API key is all you configure — the observer knows its own service
5154
> endpoints. `ROARK_API_KEY` can also be passed as `api_key=` to `RoarkObserver`.
55+
>
56+
> `ROARK_INTEGRATION_ID` is optional: set it to attribute calls to a specific
57+
> Roark Pipecat integration (also accepted as `roark_integration_id=`). Omit it
58+
> and Roark falls back to the project's self-hosted integration.
5259
5360
### 3. Wire the observer
5461

@@ -130,15 +137,15 @@ and the same file runs in both modes — see `examples/bot.py`.
130137
|---|---|---|
131138
| Entry point | `python bot.py``pipecat.runner.run.main()` dispatches to `bot()` | Platform invokes `bot(runner_args)` per session |
132139
| Room/token | You provision (Daily REST, `pipecat.runner.daily.configure`, …) | Injected via `DailyRunnerArguments` |
133-
| Env vars | `.env` / your secrets manager | `pcc secrets set <name> KEY=value …` |
140+
| Env vars | `ROARK_API_KEY` (+ optional `ROARK_INTEGRATION_ID`) in `.env` / your secrets manager | same vars via `pcc secrets set <name> KEY=value …` |
134141
| Teardown | `EndFrame` is reliable | Sessions can vanish — wire [`aflush()` on disconnect](#handling-webrtc-disconnects) |
135142
| Observer wiring | ← identical → | ← identical → |
136143

137144
### Self-hosted
138145

139146
```bash
140147
cp .env.example .env
141-
# fill in ROARK_API_KEY
148+
# fill in ROARK_API_KEY (and optionally ROARK_INTEGRATION_ID)
142149
uv sync --all-extras
143150
uv run python examples/bot.py --transport daily # or: --transport webrtc
144151
```
@@ -149,7 +156,8 @@ Set the same vars as deployment secrets, then deploy:
149156

150157
```bash
151158
pcc secrets set roark-secrets \
152-
ROARK_API_KEY=rk_live_...
159+
ROARK_API_KEY=rk_live_... \
160+
ROARK_INTEGRATION_ID=... # optional — from the Roark dashboard
153161

154162
pcc deploy
155163
pcc agent start <agent-name>

examples/bot.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments) -> Non
107107
# Tie the Roark record to the platform-provided session id so you can
108108
# correlate Roark calls with Pipecat Cloud / Daily session logs.
109109
pipecat_call_id=getattr(runner_args, "session_id", None),
110+
# Which Roark Pipecat integration this deployment belongs to (from the
111+
# Roark dashboard). Optional — omit to fall back to the project's
112+
# self-hosted integration.
113+
roark_integration_id=os.environ.get("ROARK_INTEGRATION_ID"),
110114
)
111115

112116
pipeline = Pipeline(

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "pipecat-roark"
7-
version = "0.1.4"
7+
version = "0.1.5"
88
description = "Roark analytics observer for Pipecat — capture call lifecycle, transcripts, tool calls, and recordings from any Pipecat pipeline."
99
readme = "README.md"
1010
license = { file = "LICENSE" }

src/pipecat_roark/_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class CallStartedPayload(TypedDict, total=False):
5656
agentId: str
5757
agentName: str
5858
agentPrompt: str
59+
roarkIntegrationId: str # Roark integration id; omitted falls back to self-hosted
5960

6061

6162
class CallEndedPayload(TypedDict, total=False):
@@ -78,6 +79,7 @@ class CallEndedPayload(TypedDict, total=False):
7879
recordingNumChannels: int
7980
transcript: list[TranscriptMessage]
8081
toolCalls: list[ToolCallMessage | ToolResultMessage]
82+
roarkIntegrationId: str # Roark integration id; omitted falls back to self-hosted
8183

8284

8385
class ChunkUploadUrlResponse(TypedDict, total=False):

src/pipecat_roark/observer.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import asyncio
6666
import json
6767
import logging
68+
import os
6869
import uuid
6970
from datetime import datetime, timezone
7071
from typing import TYPE_CHECKING, Literal, cast
@@ -194,6 +195,7 @@ def __init__(
194195
agent_prompt: str | None = None,
195196
audio_buffer_processor: AudioBufferProcessor | None = None,
196197
pipecat_call_id: str | None = None,
198+
roark_integration_id: str | None = None,
197199
) -> None:
198200
"""Construct a ``RoarkObserver`` for a single Pipecat call.
199201
@@ -217,6 +219,12 @@ def __init__(
217219
the same value to ``PipelineTask(conversation_id=...)`` when
218220
OpenTelemetry tracing is enabled so each Roark call can be
219221
looked up by ``conversation.id`` in your tracing backend.
222+
roark_integration_id: The Roark integration this deployment
223+
belongs to, copied from the Roark dashboard when you create a
224+
Pipecat integration. Optional — defaults to the
225+
``ROARK_INTEGRATION_ID`` environment variable, and when neither
226+
is set nothing is sent and Roark falls back to the project's
227+
self-hosted integration.
220228
"""
221229
super().__init__()
222230

@@ -226,6 +234,7 @@ def __init__(
226234
self._agent_name = agent_name
227235
self._agent_prompt = agent_prompt
228236
self._pipecat_call_id = pipecat_call_id or str(uuid.uuid4())
237+
self._roark_integration_id = roark_integration_id or os.environ.get("ROARK_INTEGRATION_ID")
229238

230239
self._transcript: list[TranscriptMessage] = []
231240
self._tool_calls: list[ToolCallMessage | ToolResultMessage] = []
@@ -518,8 +527,15 @@ async def _post_call_started(self) -> None:
518527
payload["agentName"] = self._agent_name
519528
if self._agent_prompt:
520529
payload["agentPrompt"] = self._agent_prompt
530+
if self._roark_integration_id:
531+
payload["roarkIntegrationId"] = self._roark_integration_id
521532

522-
log.info("call-started: pipecatCallId=%s agentId=%s", self._pipecat_call_id, self._agent_id)
533+
log.info(
534+
"call-started: pipecatCallId=%s agentId=%s roarkIntegrationId=%s",
535+
self._pipecat_call_id,
536+
self._agent_id,
537+
self._roark_integration_id,
538+
)
523539
await self._client.post_call_started(payload)
524540

525541
# ------------------------------------------------------------------ timing
@@ -631,14 +647,18 @@ async def _flush_call_ended(self, *, reason: str) -> None:
631647
)
632648
if self._tool_calls:
633649
payload["toolCalls"] = list(self._tool_calls)
650+
if self._roark_integration_id:
651+
payload["roarkIntegrationId"] = self._roark_integration_id
634652

635653
log.info(
636-
"call-ended: pipecatCallId=%s reason=%s transcript=%d toolCalls=%d chunks=%d",
654+
"call-ended: pipecatCallId=%s reason=%s transcript=%d toolCalls=%d chunks=%d "
655+
"roarkIntegrationId=%s",
637656
self._pipecat_call_id,
638657
reason,
639658
len(self._transcript),
640659
len(self._tool_calls),
641660
self._chunk_index,
661+
self._roark_integration_id,
642662
)
643663
await self._client.post_call_ended(payload)
644664
await self._client.aclose()

tests/test_observer.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,71 @@ async def test_on_pipeline_started_posts_call_started_with_required_fields() ->
152152
assert "eventTimestamp" in payload
153153

154154

155+
@pytest.mark.asyncio
156+
async def test_roark_integration_id_from_constructor_on_both_payloads() -> None:
157+
obs = RoarkObserver(
158+
api_key="rk_test", agent_id="agent-1", roark_integration_id="int_abc123"
159+
)
160+
fake = _FakeClient()
161+
obs._client = fake # type: ignore[assignment]
162+
163+
await obs.on_pipeline_started()
164+
await obs.on_push_frame(_push(EndFrame()))
165+
166+
assert fake.started[0]["roarkIntegrationId"] == "int_abc123"
167+
assert fake.ended[0]["roarkIntegrationId"] == "int_abc123"
168+
169+
170+
@pytest.mark.asyncio
171+
async def test_roark_integration_id_from_env_on_both_payloads(
172+
monkeypatch: pytest.MonkeyPatch,
173+
) -> None:
174+
monkeypatch.setenv("ROARK_INTEGRATION_ID", "int_from_env")
175+
obs = RoarkObserver(api_key="rk_test", agent_id="agent-1")
176+
fake = _FakeClient()
177+
obs._client = fake # type: ignore[assignment]
178+
179+
await obs.on_pipeline_started()
180+
await obs.on_push_frame(_push(EndFrame()))
181+
182+
assert fake.started[0]["roarkIntegrationId"] == "int_from_env"
183+
assert fake.ended[0]["roarkIntegrationId"] == "int_from_env"
184+
185+
186+
@pytest.mark.asyncio
187+
async def test_constructor_roark_integration_id_overrides_env(
188+
monkeypatch: pytest.MonkeyPatch,
189+
) -> None:
190+
monkeypatch.setenv("ROARK_INTEGRATION_ID", "int_from_env")
191+
obs = RoarkObserver(
192+
api_key="rk_test", agent_id="agent-1", roark_integration_id="int_explicit"
193+
)
194+
fake = _FakeClient()
195+
obs._client = fake # type: ignore[assignment]
196+
197+
await obs.on_pipeline_started()
198+
await obs.on_push_frame(_push(EndFrame()))
199+
200+
assert fake.started[0]["roarkIntegrationId"] == "int_explicit"
201+
assert fake.ended[0]["roarkIntegrationId"] == "int_explicit"
202+
203+
204+
@pytest.mark.asyncio
205+
async def test_roark_integration_id_absent_when_unset(
206+
monkeypatch: pytest.MonkeyPatch,
207+
) -> None:
208+
monkeypatch.delenv("ROARK_INTEGRATION_ID", raising=False)
209+
obs = RoarkObserver(api_key="rk_test", agent_id="agent-1")
210+
fake = _FakeClient()
211+
obs._client = fake # type: ignore[assignment]
212+
213+
await obs.on_pipeline_started()
214+
await obs.on_push_frame(_push(EndFrame()))
215+
216+
assert "roarkIntegrationId" not in fake.started[0]
217+
assert "roarkIntegrationId" not in fake.ended[0]
218+
219+
155220
@pytest.mark.asyncio
156221
async def test_multiple_on_pipeline_started_calls_only_post_once() -> None:
157222
obs = RoarkObserver(api_key="rk_test", agent_id="agent-1")

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)