Skip to content
Merged
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
34 changes: 33 additions & 1 deletion src/deeptrust/agents/livekit.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,30 @@ def listen(
return stop


# The API's metadata limits, checked here so a bad config fails once, not every request.
MAX_CONFIG_KEYS = 32
MAX_CONFIG_VALUE = 1024
MAX_PROMPT = 65536


def _config_attributes(config: dict[str, str | None]) -> dict[str, str]:
attributes = {f"deeptrust_{k}": str(v) for k, v in config.items() if v}
if len(attributes) > MAX_CONFIG_KEYS:
raise ValueError(f"config has more than {MAX_CONFIG_KEYS} keys")
for key, value in attributes.items():
limit = MAX_PROMPT if key == "deeptrust_prompt" else MAX_CONFIG_VALUE
if len(value) > limit:
raise ValueError(f"config {key} is longer than {limit} characters")
return attributes


async def tag_call(room: Any, **config: str | None) -> None:
"""Tag the call with what it ran on (test_name, prompt_id, llm, ...). Call after
``ctx.connect()``; every participant sees it, so send ``prompt_id``, not
``prompt``, with browser callers."""
await room.local_participant.set_attributes(_config_attributes(config))


def attach(
agent_session: Any,
dt: DeepTrust,
Expand All @@ -200,6 +224,7 @@ def attach(
user: User | None = None,
interrupt: bool = True,
on_analysis: Callable[[Any], None] | None = None,
config: dict[str, str | None] | None = None,
) -> Any:
"""Wire a LiveKit AgentSession to DeepTrust. Returns the DeepTrust session.

Expand All @@ -215,10 +240,17 @@ def attach(
When the session closes, the DeepTrust call is ended so post-call
processing starts at once.

Pass ``config`` to record what the call ran on (see ``tag_call``).

Returns the DeepTrust session, so the transcript and findings remain
reachable.
"""
call = dt.session(external_id=external_id, user=user, platform="livekit")
call = dt.session(
external_id=external_id,
user=user,
platform="livekit",
metadata=dict(_config_attributes(config or {})),
)
spawn = _spawner()
delivery = _Delivery(agent_session, agent, interrupt)
stop = delivery.listen(room, spawn) if room is not None else None
Expand Down
48 changes: 47 additions & 1 deletion tests/test_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import asyncio
import json
from dataclasses import dataclass
from types import SimpleNamespace
from typing import Any

import httpx
Expand All @@ -17,7 +18,7 @@

from deeptrust.agents import DeepTrust
from deeptrust.agents.elevenlabs import Monitor, _read_turn, contextual_update_command
from deeptrust.agents.livekit import attach, listen
from deeptrust.agents.livekit import attach, listen, tag_call
from deeptrust.agents.vapi import (
SECRET_HEADER,
Bridge,
Expand Down Expand Up @@ -176,6 +177,51 @@ async def settle() -> None:
await asyncio.sleep(0.01)


async def test_livekit_tag_call_sets_deeptrust_attributes() -> None:
room = FakeRoom()
set_to: dict[str, str] = {}

async def set_attributes(attributes: dict[str, str]) -> None:
set_to.update(attributes)

room.local_participant = SimpleNamespace(set_attributes=set_attributes)
await tag_call(
room, test_name="gpt-4o-vs-gpt-4.1", llm="openai/gpt-4.1", stt=None, tts=""
)

assert set_to == {
"deeptrust_test_name": "gpt-4o-vs-gpt-4.1",
"deeptrust_llm": "openai/gpt-4.1",
}


def test_livekit_config_over_the_api_limits_fails_up_front() -> None:
dt = DeepTrust(api_key="dt_test", base_url=BASE)
with pytest.raises(ValueError):
attach(FakeSession(), dt, external_id="r", config={"llm": "x" * 1025})
with pytest.raises(ValueError):
attach(FakeSession(), dt, external_id="r", config={"prompt": "x" * 65537})
attach(FakeSession(), dt, external_id="r", config={"prompt": "x" * 5000})


@respx.mock
async def test_livekit_attach_sends_the_config_as_metadata() -> None:
route = respx.post(f"{BASE}/agents/analyze").mock(
return_value=httpx.Response(
200, json={"session_id": "sess_1", "job_id": "job_1", "findings": []}
)
)
lk = FakeSession()
dt = DeepTrust(api_key="dt_test", base_url=BASE)
attach(lk, dt, external_id="room-1", config={"test_name": "t1", "llm": None})

await lk.say("user", "hello")
await settle()

sent = json.loads(route.calls.last.request.content)
assert sent["metadata"] == {"deeptrust_test_name": "t1"}


@respx.mock
async def test_livekit_analyses_caller_turns_and_delivers_once() -> None:
route = respx.post(f"{BASE}/agents/analyze").mock(
Expand Down
Loading