Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 293 additions & 0 deletions docs/pipecat-ai-integration-plan.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions py/noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,22 @@ def test_livekit_agents(session, version):
_run_tests(session, f"{INTEGRATION_DIR}/livekit_agents/test_livekit_agents.py", version=version, env=env)


PIPECAT_VERSIONS = _get_matrix_versions("pipecat-ai")


@nox.session()
@nox.parametrize("version", PIPECAT_VERSIONS, ids=PIPECAT_VERSIONS)
def test_pipecat(session, version):
if sys.version_info < (3, 11):
session.skip("Pipecat AI 1.x requires Python 3.11+")
if sys.version_info >= (3, 14):
session.skip("Pipecat AI's onnxruntime dependency does not ship Python 3.14 wheels")
_install_test_deps(session)
_install_group_locked(session, "test-pipecat")
_install_matrix_dep(session, "pipecat-ai", version)
_run_tests(session, f"{INTEGRATION_DIR}/pipecat/test_pipecat.py", version=version)


STRANDS_VERSIONS = _get_matrix_versions("strands-agents")


Expand Down
12 changes: 12 additions & 0 deletions py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ test-livekit-agents = [
"opentelemetry-sdk<1.39",
]

test-pipecat = [
{include-group = "test"},
# pipecat-ai 1.3.0 imports websockets but does not install it transitively.
"websockets==15.0.1",
]

test-crewai = [
{include-group = "test"},
# CrewAI's no-network smoke test forces the LiteLLM fallback path via
Expand Down Expand Up @@ -338,6 +344,10 @@ latest = "litellm==1.90.0"
latest = "livekit-agents==1.6.4"
"1.3.1" = "livekit-agents==1.3.1"

[tool.braintrust.matrix.pipecat-ai]
latest = "pipecat-ai==1.4.0"
"1.3.0" = "pipecat-ai==1.3.0"

[tool.braintrust.matrix.claude-agent-sdk]
latest = "claude-agent-sdk==0.2.110"
"0.1.10" = "claude-agent-sdk==0.1.10"
Expand Down Expand Up @@ -485,6 +495,7 @@ mistral = ["mistralai"]
openai = ["openai"]
openai_agents = ["openai-agents"]
openrouter = ["openrouter"]
pipecat = ["pipecat-ai"]
pydantic_ai = ["pydantic-ai-integration", "pydantic-ai-wrap-openai"]
strands = ["strands-agents"]

Expand All @@ -510,5 +521,6 @@ huggingface-hub = "huggingface_hub"
openai = "openai"
openai-agents = "agents"
openrouter = "openrouter"
pipecat-ai = "pipecat"
strands-agents = "strands"
temporalio = "temporalio"
5 changes: 5 additions & 0 deletions py/src/braintrust/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
OpenAIAgentsIntegration,
OpenAIIntegration,
OpenRouterIntegration,
PipecatIntegration,
PydanticAIIntegration,
StrandsIntegration,
TemporalIntegration,
Expand Down Expand Up @@ -78,6 +79,7 @@ def auto_instrument(
strands: bool = True,
temporal: bool = True,
livekit_agents: bool = True,
pipecat: bool = True,
) -> dict[str, bool]:
"""
Auto-instrument supported AI/ML libraries for Braintrust tracing.
Expand Down Expand Up @@ -113,6 +115,7 @@ def auto_instrument(
strands: Enable Strands Agents instrumentation (default: True)
temporal: Enable Temporal instrumentation (default: True)
livekit_agents: Enable LiveKit Agents instrumentation (default: True)
pipecat: Enable Pipecat AI instrumentation (default: True)

Returns:
Dict mapping integration name to whether it was successfully instrumented.
Expand Down Expand Up @@ -208,6 +211,8 @@ def auto_instrument(
results["temporal"] = _instrument_integration(TemporalIntegration)
if livekit_agents:
results["livekit_agents"] = _instrument_integration(LiveKitAgentsIntegration)
if pipecat:
results["pipecat"] = _instrument_integration(PipecatIntegration)

return results

Expand Down
2 changes: 2 additions & 0 deletions py/src/braintrust/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .openai import OpenAIIntegration
from .openai_agents import OpenAIAgentsIntegration
from .openrouter import OpenRouterIntegration
from .pipecat import PipecatIntegration
from .pydantic_ai import PydanticAIIntegration
from .strands import StrandsIntegration
from .temporal import TemporalIntegration
Expand Down Expand Up @@ -46,6 +47,7 @@
"OpenAIIntegration",
"OpenAIAgentsIntegration",
"OpenRouterIntegration",
"PipecatIntegration",
"PydanticAIIntegration",
"StrandsIntegration",
"TemporalIntegration",
Expand Down
102 changes: 102 additions & 0 deletions py/src/braintrust/integrations/auto_test_scripts/test_auto_pipecat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import asyncio
import importlib
import inspect
import os
import tempfile
from pathlib import Path

from braintrust.auto import auto_instrument
from braintrust.integrations.test_utils import autoinstrument_test_context


def _ensure_nltk_punkt_tab():
data_dir = Path(tempfile.gettempdir()) / "braintrust-pipecat-nltk-data"
punkt_tab = data_dir / "tokenizers" / "punkt_tab"
punkt_tab.mkdir(parents=True, exist_ok=True)
os.environ.setdefault("NLTK_DATA", str(data_dir))


def _import(path):
_ensure_nltk_punkt_tab()
module_name, attr = path.rsplit(".", 1)
return getattr(importlib.import_module(module_name), attr)


def _worker_kwargs(**overrides):
PipelineWorker = _import("pipecat.pipeline.worker.PipelineWorker")
signature = inspect.signature(PipelineWorker)
kwargs = {"idle_timeout_secs": None}
for name, value in {
"enable_turn_tracking": False,
"enable_rtvi": False,
"check_dangling_tasks": False,
}.items():
if name in signature.parameters:
kwargs[name] = value
kwargs.update(overrides)
return kwargs


def _runner_kwargs(**overrides):
WorkerRunner = _import("pipecat.workers.runner.WorkerRunner")
signature = inspect.signature(WorkerRunner)
kwargs = {"handle_sigint": False}
if "check_dangling_tasks" in signature.parameters:
kwargs["check_dangling_tasks"] = False
kwargs.update(overrides)
return kwargs


async def main():
with autoinstrument_test_context("test_auto_pipecat", integration="pipecat") as memory_logger:
_ensure_nltk_punkt_tab()
results = auto_instrument()
assert results.get("pipecat") is True

EndFrame = _import("pipecat.frames.frames.EndFrame")
LLMContextFrame = _import("pipecat.frames.frames.LLMContextFrame")
Pipeline = _import("pipecat.pipeline.pipeline.Pipeline")
PipelineParams = _import("pipecat.pipeline.worker.PipelineParams")
PipelineWorker = _import("pipecat.pipeline.worker.PipelineWorker")
LLMContext = _import("pipecat.processors.aggregators.llm_context.LLMContext")
OpenAILLMService = _import("pipecat.services.openai.llm.OpenAILLMService")
WorkerRunner = _import("pipecat.workers.runner.WorkerRunner")

llm = OpenAILLMService(
api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(
model="gpt-4o-mini",
temperature=0.0,
max_completion_tokens=20,
),
)
worker = PipelineWorker(
Pipeline([llm]),
**_worker_kwargs(
name="bt-auto-pipecat-worker",
params=PipelineParams(enable_metrics=True, enable_usage_metrics=True),
),
)
context = LLMContext(
messages=[
{"role": "developer", "content": "Answer with exactly the requested text and no punctuation."},
{"role": "user", "content": "Say: braintrust auto pipecat"},
]
)

@worker.event_handler("on_pipeline_started")
async def on_pipeline_started(_worker, _frame):
await worker.queue_frames([LLMContextFrame(context), EndFrame()])

runner = WorkerRunner(**_runner_kwargs())
await runner.add_workers(worker)
await asyncio.wait_for(runner.run(), timeout=20)

logs = memory_logger.pop()
names = {log.get("span_attributes", {}).get("name") for log in logs}
assert "pipecat_pipeline" in names
assert "pipecat_llm_response" in names


if __name__ == "__main__":
asyncio.run(main())
38 changes: 38 additions & 0 deletions py/src/braintrust/integrations/pipecat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Braintrust integration for Pipecat AI."""

from braintrust.logger import NOOP_SPAN, current_span, init_logger

from .integration import PipecatIntegration
from .patchers import set_default_observer_options, wrap_pipeline_worker
from .tracing import BraintrustPipecatObserver


__all__ = [
"BraintrustPipecatObserver",
"PipecatIntegration",
"setup_pipecat",
"wrap_pipeline_worker",
]


def setup_pipecat(
api_key: str | None = None,
project_id: str | None = None,
project_name: str | None = None,
*,
capture_audio: bool = False,
trace_turns: bool = True,
) -> bool:
"""Set up Braintrust tracing for Pipecat ``PipelineWorker`` instances.

The setup hook patches ``PipelineWorker.__init__`` so newly constructed
workers receive a ``BraintrustPipecatObserver`` unless one was already
provided explicitly.
"""
set_default_observer_options(capture_audio=capture_audio, trace_turns=trace_turns)

span = current_span()
if span == NOOP_SPAN:
init_logger(project=project_name, api_key=api_key, project_id=project_id)

return PipecatIntegration.setup()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
interactions: []
version: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
interactions:
- request:
body: '{"messages":[{"role":"developer","content":"Answer with exactly the requested
text and no punctuation."},{"role":"user","content":"Say: braintrust auto pipecat"}],"model":"gpt-4o-mini","max_completion_tokens":20,"stream":true,"stream_options":{"include_usage":true},"temperature":0.0}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '284'
content-type:
- application/json
host:
- api.openai.com
user-agent:
- AsyncOpenAI/Python 2.44.0
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 2.44.0
x-stainless-read-timeout:
- '600'
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.12
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
string: 'data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"mBvPhUhAJ"}


data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[{"index":0,"delta":{"content":"brain"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"f6iO50"}


data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[{"index":0,"delta":{"content":"trust"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"1OZQen"}


data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[{"index":0,"delta":{"content":"
auto"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nNikol"}


data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[{"index":0,"delta":{"content":"
pi"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xXSuQcBG"}


data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[{"index":0,"delta":{"content":"pec"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"fFb1am2g"}


data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[{"index":0,"delta":{"content":"at"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Q6pMNbfZz"}


data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"np4gz"}


data: {"id":"chatcmpl-DxeZpXNoUnRnT8t7XsNZZ5V83gre4","object":"chat.completion.chunk","created":1783109701,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_4507413d49","choices":[],"usage":{"prompt_tokens":29,"completion_tokens":6,"total_tokens":35,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"oyfaDmQniIW"}


data: [DONE]


'
headers:
access-control-expose-headers:
- X-Request-ID
- CF-Ray
- CF-Ray
alt-svc:
- h3=":443"; ma=86400
cf-cache-status:
- DYNAMIC
cf-ray:
- a1588f52ad9ea22e-YYZ
connection:
- keep-alive
content-type:
- text/event-stream; charset=utf-8
date:
- Fri, 03 Jul 2026 20:15:02 GMT
openai-organization:
- braintrust-data
openai-processing-ms:
- '706'
openai-project:
- proj_vsCSXafhhByzWOThMrJcZiw9
openai-version:
- '2020-10-01'
server:
- cloudflare
set-cookie:
- __cf_bm=1Q17IKR.MBtQXtkQ1S5ly5x3ajkpcTkBBxgFtfRHkf4-1783109701.5507047-1.0.1.1-.CetNrjD1s17f4gfSC5MYP5bHVCA5SUrLsH5suopqdkp.vCr0k9we7_F2wjwQYKSt4LZ4Ca5n2SCUp_csLqra2jfPYwmFjJv53cWGbRnNI3vg6wHBiwdrRhBNAa9NoAE;
HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com; Expires=Fri,
03 Jul 2026 20:45:02 GMT
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
transfer-encoding:
- chunked
x-content-type-options:
- nosniff
x-openai-proxy-wasm:
- v0.1
x-ratelimit-limit-requests:
- '30000'
x-ratelimit-limit-tokens:
- '150000000'
x-ratelimit-remaining-requests:
- '29999'
x-ratelimit-remaining-tokens:
- '149999975'
x-ratelimit-reset-requests:
- 2ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_d7265fcbe8f444b8968fbebc9eedf37c
status:
code: 200
message: OK
version: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
interactions: []
version: 1
Loading