diff --git a/src/deeptrust/agents/livekit.py b/src/deeptrust/agents/livekit.py index 91f08c7..d3665ad 100644 --- a/src/deeptrust/agents/livekit.py +++ b/src/deeptrust/agents/livekit.py @@ -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, @@ -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. @@ -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 diff --git a/tests/test_adapters.py b/tests/test_adapters.py index f8fc3dd..261ba59 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -9,6 +9,7 @@ import asyncio import json from dataclasses import dataclass +from types import SimpleNamespace from typing import Any import httpx @@ -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, @@ -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(