Skip to content

Commit 0ec6519

Browse files
Add AgentKit parity and vendor regression tests
1 parent bd7691a commit 0ec6519

8 files changed

Lines changed: 896 additions & 0 deletions

File tree

tests/agentkit/helpers.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from __future__ import annotations
2+
3+
from types import SimpleNamespace
4+
from typing import Any, Dict, List, Optional
5+
6+
7+
def dump_model(value: Any) -> Any:
8+
if hasattr(value, "model_dump"):
9+
return value.model_dump(exclude_none=True)
10+
if isinstance(value, dict):
11+
return {k: dump_model(v) for k, v in value.items()}
12+
if isinstance(value, list):
13+
return [dump_model(v) for v in value]
14+
return value
15+
16+
17+
class DummyAgents:
18+
def __init__(self) -> None:
19+
self.start_calls: List[Any] = []
20+
self.stop_calls: List[Any] = []
21+
self.speak_calls: List[Any] = []
22+
self.interrupt_calls: List[Any] = []
23+
self.update_calls: List[Any] = []
24+
self.history_calls: List[Any] = []
25+
self.turn_calls: List[Any] = []
26+
self.get_calls: List[Any] = []
27+
28+
self.start_result: Any = SimpleNamespace(agent_id="agent-1")
29+
self.start_error: Optional[Exception] = None
30+
self.stop_error: Optional[Exception] = None
31+
32+
def start(self, app_id, **kwargs):
33+
self.start_calls.append((app_id, kwargs))
34+
if self.start_error is not None:
35+
raise self.start_error
36+
return self.start_result
37+
38+
def stop(self, app_id, agent_id, request_options=None):
39+
self.stop_calls.append((app_id, agent_id, request_options))
40+
if self.stop_error is not None:
41+
raise self.stop_error
42+
return None
43+
44+
def speak(self, app_id, agent_id, request_options=None, **kwargs):
45+
self.speak_calls.append((app_id, agent_id, request_options, kwargs))
46+
return None
47+
48+
def interrupt(self, app_id, agent_id, request_options=None):
49+
self.interrupt_calls.append((app_id, agent_id, request_options))
50+
return None
51+
52+
def update(self, app_id, agent_id, properties=None, request_options=None):
53+
self.update_calls.append((app_id, agent_id, properties, request_options))
54+
return None
55+
56+
def get_history(self, app_id, agent_id, request_options=None):
57+
self.history_calls.append((app_id, agent_id, request_options))
58+
return {"contents": []}
59+
60+
def get_turns(self, app_id, agent_id, request_options=None):
61+
self.turn_calls.append((app_id, agent_id, request_options))
62+
return {"turns": [{"agent_id": agent_id}]}
63+
64+
def get(self, app_id, agent_id, request_options=None):
65+
self.get_calls.append((app_id, agent_id, request_options))
66+
return {"agent_id": agent_id}
67+
68+
69+
class DummyAsyncAgents(DummyAgents):
70+
async def start(self, app_id, **kwargs):
71+
return super().start(app_id, **kwargs)
72+
73+
async def stop(self, app_id, agent_id, request_options=None):
74+
return super().stop(app_id, agent_id, request_options)
75+
76+
async def speak(self, app_id, agent_id, request_options=None, **kwargs):
77+
return super().speak(app_id, agent_id, request_options, **kwargs)
78+
79+
async def interrupt(self, app_id, agent_id, request_options=None):
80+
return super().interrupt(app_id, agent_id, request_options)
81+
82+
async def update(self, app_id, agent_id, properties=None, request_options=None):
83+
return super().update(app_id, agent_id, properties, request_options)
84+
85+
async def get_history(self, app_id, agent_id, request_options=None):
86+
return super().get_history(app_id, agent_id, request_options)
87+
88+
async def get_turns(self, app_id, agent_id, request_options=None):
89+
return super().get_turns(app_id, agent_id, request_options)
90+
91+
async def get(self, app_id, agent_id, request_options=None):
92+
return super().get(app_id, agent_id, request_options)
93+
94+
95+
class DummyClient:
96+
def __init__(
97+
self,
98+
*,
99+
auth_mode: str = "basic",
100+
app_id: str = "app-id",
101+
app_certificate: Optional[str] = "app-cert",
102+
) -> None:
103+
self.app_id = app_id
104+
self.app_certificate = app_certificate
105+
self.auth_mode = auth_mode
106+
self.agents = DummyAgents()
107+
108+
109+
class DummyAsyncClient(DummyClient):
110+
def __init__(self, **kwargs: Any) -> None:
111+
super().__init__(**kwargs)
112+
self.agents = DummyAsyncAgents()

tests/agentkit/test_agent.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from unittest import mock
2+
3+
import pytest
4+
5+
from agora_agent.agentkit import Agent
6+
from agora_agent.agentkit.vendors import (
7+
DeepgramSTT,
8+
ElevenLabsTTS,
9+
OpenAI,
10+
OpenAIRealtime,
11+
)
12+
from tests.agentkit.helpers import DummyClient, dump_model
13+
14+
15+
def test_builder_methods_are_immutable_and_reflected_in_config_and_getters():
16+
agent = Agent(name="base", instructions="helpful")
17+
llm = OpenAI(api_key="key", model="gpt-4o-mini")
18+
tts = ElevenLabsTTS(key="tts", model_id="model", voice_id="voice", sample_rate=24000)
19+
stt = DeepgramSTT(api_key="dg", model="nova-3")
20+
21+
configured = agent.with_llm(llm).with_tts(tts).with_stt(stt).with_greeting("hi").with_max_history(10)
22+
23+
assert agent.llm is None
24+
assert configured.llm == llm.to_config()
25+
assert configured.tts == tts.to_config()
26+
assert configured.stt == stt.to_config()
27+
assert configured.greeting == "hi"
28+
assert configured.max_history == 10
29+
assert configured.config["name"] == "base"
30+
31+
32+
def test_create_session_resolves_name_from_option_agent_or_timestamp():
33+
client = DummyClient()
34+
named = Agent(name="from-agent")
35+
explicit = named.create_session(client, channel="c", agent_uid="1", remote_uids=["2"], name="explicit")
36+
assert explicit.agent.name == "from-agent"
37+
assert explicit._name == "explicit"
38+
39+
defaulted = named.create_session(client, channel="c", agent_uid="1", remote_uids=["2"])
40+
assert defaulted._name == "from-agent"
41+
42+
with mock.patch("agora_agent.agentkit.agent.time.time", return_value=123456):
43+
generated = Agent().create_session(client, channel="c", agent_uid="1", remote_uids=["2"])
44+
assert generated._name == "agent-123456"
45+
46+
47+
def test_to_properties_throws_when_llm_or_tts_missing_outside_preset_or_pipeline_flow():
48+
with pytest.raises(ValueError, match="TTS configuration is required"):
49+
Agent().with_llm(OpenAI(api_key="key", model="gpt-4o-mini")).to_properties(
50+
channel="room",
51+
agent_uid="1",
52+
remote_uids=["2"],
53+
token="token",
54+
)
55+
56+
with pytest.raises(ValueError, match="LLM configuration is required"):
57+
Agent().with_tts(ElevenLabsTTS(key="tts", model_id="model", voice_id="voice", sample_rate=24000)).to_properties(
58+
channel="room",
59+
agent_uid="1",
60+
remote_uids=["2"],
61+
token="token",
62+
)
63+
64+
65+
def test_to_properties_applies_defaults_and_overrides_for_standard_pipeline():
66+
agent = (
67+
Agent(instructions="top-level instructions", greeting="hello", failure_message="retry", max_history=7)
68+
.with_llm(
69+
OpenAI(
70+
api_key="key",
71+
model="gpt-4o-mini",
72+
greeting_message="vendor greeting",
73+
failure_message="vendor failure",
74+
)
75+
)
76+
.with_tts(ElevenLabsTTS(key="tts", model_id="model", voice_id="voice", sample_rate=24000))
77+
.with_stt(DeepgramSTT(api_key="dg", model="nova-3"))
78+
)
79+
80+
props = dump_model(
81+
agent.to_properties(channel="room", agent_uid="1", remote_uids=["2"], token="token")
82+
)
83+
84+
assert props["llm"]["system_messages"] == [{"role": "system", "content": "top-level instructions"}]
85+
assert props["llm"]["greeting_message"] == "vendor greeting"
86+
assert props["llm"]["failure_message"] == "vendor failure"
87+
assert props["llm"]["max_history"] == 7
88+
assert props["tts"]["vendor"] == "elevenlabs"
89+
assert props["asr"]["vendor"] == "deepgram"
90+
91+
92+
def test_to_properties_supports_preset_or_pipeline_backed_sessions_without_llm_or_tts():
93+
props = dump_model(
94+
Agent(instructions="preset-backed").to_properties(
95+
channel="room",
96+
agent_uid="1",
97+
remote_uids=["2"],
98+
token="token",
99+
skip_vendor_validation=True,
100+
)
101+
)
102+
assert props["channel"] == "room"
103+
assert "llm" not in props
104+
assert "tts" not in props
105+
106+
107+
def test_to_properties_generates_token_and_respects_mllm_vendor_precedence():
108+
agent = Agent(greeting="top hello", failure_message="top fail", max_history=9).with_mllm(
109+
OpenAIRealtime(
110+
api_key="key",
111+
greeting_message="vendor hello",
112+
)
113+
).with_advanced_features({"enable_mllm": True})
114+
115+
props = dump_model(
116+
agent.to_properties(
117+
channel="room",
118+
agent_uid="1",
119+
remote_uids=["2"],
120+
app_id="app-id",
121+
app_certificate="app-cert",
122+
)
123+
)
124+
125+
assert props["mllm"]["greeting_message"] == "vendor hello"
126+
assert "failure_message" not in props["mllm"]
127+
assert "max_history" not in props["mllm"]
128+
assert isinstance(props["token"], str) and props["token"]

0 commit comments

Comments
 (0)