Skip to content

Commit 9f491c6

Browse files
feat(agentkit): update Agent builder and session lifecycle for v2.7
Aligns Agent and AgentSession with the generated v2.7 request shape. MLLM sessions no longer require TTS, LLM, or STT, and enabled avatars are rejected when MLLM is configured. AgentSession now enriches generic and RTC avatars with session context, auto-generates avatar tokens, validates TTS sample rates from vendor-specific fields, and adds paginated get_turns/get_all_turns helpers with fail-fast pagination guards.
1 parent 26706d7 commit 9f491c6

2 files changed

Lines changed: 360 additions & 35 deletions

File tree

src/agora_agent/agentkit/agent.py

Lines changed: 144 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@
88
from .agent_session import AgentSession, AsyncAgentSession
99

1010
from ..agents.types.start_agents_request_properties import StartAgentsRequestProperties
11+
from ..agents.types.start_agents_request_properties_asr import StartAgentsRequestPropertiesAsr
12+
from ..agents.types.start_agents_request_properties_asr_vendor import StartAgentsRequestPropertiesAsrVendor
13+
from ..agents.types.start_agents_request_properties_avatar import StartAgentsRequestPropertiesAvatar
14+
from ..agents.types.start_agents_request_properties_avatar_vendor import StartAgentsRequestPropertiesAvatarVendor
15+
from ..agents.types.start_agents_request_properties_llm import StartAgentsRequestPropertiesLlm
16+
from ..agents.types.start_agents_request_properties_llm_style import StartAgentsRequestPropertiesLlmStyle
17+
from ..agents.types.start_agents_request_properties_mllm import StartAgentsRequestPropertiesMllm
18+
from ..agents.types.start_agents_request_properties_mllm_vendor import StartAgentsRequestPropertiesMllmVendor
19+
from ..agents.types.update_agents_request_properties import UpdateAgentsRequestProperties
20+
from ..agents.types.get_agents_response import GetAgentsResponse
21+
from ..agents.types.list_agents_response import ListAgentsResponse
22+
from ..agents.types.list_agents_response_data_list_item import ListAgentsResponseDataListItem
23+
from ..agents.types.get_history_agents_response import GetHistoryAgentsResponse
24+
from ..agents.types.get_history_agents_response_contents_item import GetHistoryAgentsResponseContentsItem
25+
from ..agents.types.get_history_agents_response_contents_item_role import GetHistoryAgentsResponseContentsItemRole
26+
from ..agents.types.get_turns_agents_response import GetTurnsAgentsResponse
27+
from ..agents.types.get_turns_agents_response_turns_item import GetTurnsAgentsResponseTurnsItem
28+
from ..agents.types.speak_agents_request_priority import SpeakAgentsRequestPriority
1129
from ..agents.types.start_agents_request_properties_turn_detection import StartAgentsRequestPropertiesTurnDetection
1230
from ..agents.types.start_agents_request_properties_turn_detection_config import StartAgentsRequestPropertiesTurnDetectionConfig
1331
from ..agents.types.start_agents_request_properties_turn_detection_config_start_of_speech import StartAgentsRequestPropertiesTurnDetectionConfigStartOfSpeech
@@ -46,10 +64,21 @@
4664
from ..agents.types.start_agents_request_properties_filler_words_trigger_fixed_time_config import StartAgentsRequestPropertiesFillerWordsTriggerFixedTimeConfig
4765
from ..agents.types.start_agents_request_properties_filler_words_content import StartAgentsRequestPropertiesFillerWordsContent
4866
from ..agents.types.start_agents_request_properties_filler_words_content_static_config import StartAgentsRequestPropertiesFillerWordsContentStaticConfig
67+
from ..agents.types.start_agents_request_properties_filler_words_content_static_config_selection_rule import StartAgentsRequestPropertiesFillerWordsContentStaticConfigSelectionRule
68+
from ..types.tts import Tts
4969
from .token import generate_convo_ai_token, _validate_expires_in
5070
from .vendors.base import BaseAvatar, BaseLLM, BaseMLLM, BaseSTT, BaseTTS
5171

5272
# Top-level aliases
73+
LlmConfig = StartAgentsRequestPropertiesLlm
74+
LlmStyle = StartAgentsRequestPropertiesLlmStyle
75+
SttConfig = StartAgentsRequestPropertiesAsr
76+
SttVendor = StartAgentsRequestPropertiesAsrVendor
77+
TtsConfig = Tts
78+
MllmConfig = StartAgentsRequestPropertiesMllm
79+
MllmVendor = StartAgentsRequestPropertiesMllmVendor
80+
AvatarConfig = StartAgentsRequestPropertiesAvatar
81+
AvatarVendor = StartAgentsRequestPropertiesAvatarVendor
5382
TurnDetectionConfig = StartAgentsRequestPropertiesTurnDetection
5483
SalConfig = StartAgentsRequestPropertiesSal
5584
SalMode = StartAgentsRequestPropertiesSalSalMode
@@ -93,6 +122,18 @@
93122
InterruptionMode = StartAgentsRequestPropertiesInterruptionMode
94123
MllmTurnDetectionConfig = StartAgentsRequestPropertiesMllmTurnDetection
95124
MllmTurnDetectionMode = StartAgentsRequestPropertiesMllmTurnDetectionMode
125+
AgentConfig = StartAgentsRequestProperties
126+
AgentConfigUpdate = UpdateAgentsRequestProperties
127+
SessionInfo = GetAgentsResponse
128+
SessionListResponse = ListAgentsResponse
129+
SessionSummary = ListAgentsResponseDataListItem
130+
ConversationHistory = GetHistoryAgentsResponse
131+
ConversationTurn = GetHistoryAgentsResponseContentsItem
132+
ConversationRole = GetHistoryAgentsResponseContentsItemRole
133+
ConversationTurns = GetTurnsAgentsResponse
134+
ConversationSessionTurn = GetTurnsAgentsResponseTurnsItem
135+
SpeakPriority = SpeakAgentsRequestPriority
136+
Labels = typing.Dict[str, str]
96137

97138

98139
class SessionParamsInput(typing_extensions.TypedDict, total=False):
@@ -116,6 +157,7 @@ class SessionParamsInput(typing_extensions.TypedDict, total=False):
116157
FillerWordsTriggerFixedTimeConfig = StartAgentsRequestPropertiesFillerWordsTriggerFixedTimeConfig
117158
FillerWordsContent = StartAgentsRequestPropertiesFillerWordsContent
118159
FillerWordsContentStaticConfig = StartAgentsRequestPropertiesFillerWordsContentStaticConfig
160+
FillerWordsContentSelectionRule = StartAgentsRequestPropertiesFillerWordsContentStaticConfigSelectionRule
119161

120162

121163
class Agent:
@@ -183,9 +225,20 @@ def with_llm(self, vendor: BaseLLM) -> "Agent":
183225
return new_agent
184226

185227
def with_tts(self, vendor: BaseTTS) -> "Agent":
228+
sample_rate = vendor.sample_rate
229+
if (
230+
self._avatar_required_sample_rate not in (None, 0)
231+
and sample_rate is not None
232+
and sample_rate != self._avatar_required_sample_rate
233+
):
234+
raise ValueError(
235+
f"Avatar requires TTS sample rate of {self._avatar_required_sample_rate} Hz, "
236+
f"but TTS is configured with {sample_rate} Hz. "
237+
f"Please update your TTS sample_rate to {self._avatar_required_sample_rate}."
238+
)
186239
new_agent = self._clone()
187240
new_agent._tts = vendor.to_config()
188-
new_agent._tts_sample_rate = vendor.sample_rate
241+
new_agent._tts_sample_rate = sample_rate
189242
return new_agent
190243

191244
def with_stt(self, vendor: BaseSTT) -> "Agent":
@@ -194,6 +247,9 @@ def with_stt(self, vendor: BaseSTT) -> "Agent":
194247
return new_agent
195248

196249
def with_mllm(self, vendor: BaseMLLM) -> "Agent":
250+
# Note: avatars are not supported with MLLM. The combination is rejected
251+
# at ``to_properties`` / ``AgentSession.start`` so callers can still
252+
# configure both for tests, debugging, or disabled-avatar use cases.
197253
new_agent = self._clone()
198254
new_agent._mllm = vendor.to_config()
199255
if isinstance(new_agent._mllm, dict):
@@ -202,7 +258,10 @@ def with_mllm(self, vendor: BaseMLLM) -> "Agent":
202258
advanced_features = {key: value for key, value in new_agent._advanced_features.items() if key != "enable_mllm"}
203259
new_agent._advanced_features = typing.cast(AdvancedFeatures, advanced_features) if advanced_features else None
204260
elif isinstance(new_agent._advanced_features, StartAgentsRequestPropertiesAdvancedFeatures):
205-
advanced_features_model = new_agent._advanced_features.model_copy(update={"enable_mllm": None})
261+
advanced_features_model = self._copy_model_update(
262+
new_agent._advanced_features,
263+
{"enable_mllm": None},
264+
)
206265
if (
207266
advanced_features_model.enable_rtm is None
208267
and advanced_features_model.enable_sal is None
@@ -214,6 +273,10 @@ def with_mllm(self, vendor: BaseMLLM) -> "Agent":
214273
return new_agent
215274

216275
def with_avatar(self, vendor: BaseAvatar) -> "Agent":
276+
# Note: avatars are not supported with MLLM. The combination is rejected
277+
# at ``to_properties`` / ``AgentSession.start`` (only when the avatar is
278+
# enabled) so callers may still combine the two for testing or for the
279+
# disabled-avatar pattern.
217280
required_sample_rate = vendor.required_sample_rate
218281
if (
219282
required_sample_rate not in (None, 0)
@@ -282,7 +345,10 @@ def with_tools(self, enabled: bool = True) -> "Agent":
282345
{**new_agent._advanced_features, "enable_tools": enabled},
283346
)
284347
else:
285-
new_agent._advanced_features = new_agent._advanced_features.model_copy(update={"enable_tools": enabled})
348+
new_agent._advanced_features = self._copy_model_update(
349+
new_agent._advanced_features,
350+
{"enable_tools": enabled},
351+
)
286352
return new_agent
287353

288354
def with_parameters(self, parameters: typing.Union[SessionParams, SessionParamsInput]) -> "Agent":
@@ -294,6 +360,23 @@ def with_parameters(self, parameters: typing.Union[SessionParams, SessionParamsI
294360
new_agent._parameters = parameters
295361
return new_agent
296362

363+
def with_audio_scenario(self, audio_scenario: ParametersAudioScenario) -> "Agent":
364+
"""Returns a new Agent with the specified RTC audio scenario."""
365+
new_agent = self._clone()
366+
if new_agent._parameters is None:
367+
new_agent._parameters = StartAgentsRequestPropertiesParameters(audio_scenario=audio_scenario)
368+
elif isinstance(new_agent._parameters, dict):
369+
new_agent._parameters = typing.cast(
370+
SessionParamsInput,
371+
{**new_agent._parameters, "audio_scenario": audio_scenario},
372+
)
373+
else:
374+
new_agent._parameters = self._copy_model_update(
375+
new_agent._parameters,
376+
{"audio_scenario": audio_scenario},
377+
)
378+
return new_agent
379+
297380
def with_failure_message(self, message: str) -> "Agent":
298381
"""Returns a new Agent with the specified failure message.
299382
@@ -342,6 +425,33 @@ def with_filler_words(self, filler_words: FillerWordsConfig) -> "Agent":
342425
new_agent._filler_words = filler_words
343426
return new_agent
344427

428+
@staticmethod
429+
def _field_value(value: typing.Any, field: str) -> typing.Any:
430+
if value is None:
431+
return None
432+
if isinstance(value, dict):
433+
return value.get(field)
434+
return getattr(value, field, None)
435+
436+
@staticmethod
437+
def _copy_model_update(value: typing.Any, update: typing.Dict[str, typing.Any]) -> typing.Any:
438+
if hasattr(value, "model_copy"):
439+
return value.model_copy(update=update)
440+
if hasattr(value, "copy"):
441+
return value.copy(update=update)
442+
raise TypeError(f"Object of type {type(value).__name__} does not support model copying")
443+
444+
def _resolved_parameters(self) -> typing.Optional[typing.Union[SessionParams, SessionParamsInput]]:
445+
enable_rtm = self._field_value(self._advanced_features, "enable_rtm") is True
446+
data_channel = self._field_value(self._parameters, "data_channel")
447+
if not enable_rtm or data_channel is not None:
448+
return self._parameters
449+
if self._parameters is None:
450+
return StartAgentsRequestPropertiesParameters(data_channel="rtm")
451+
if isinstance(self._parameters, dict):
452+
return typing.cast(SessionParamsInput, {**self._parameters, "data_channel": "rtm"})
453+
return self._copy_model_update(self._parameters, {"data_channel": "rtm"})
454+
345455
@property
346456
def name(self) -> typing.Optional[str]:
347457
return self._name
@@ -354,6 +464,10 @@ def llm(self) -> typing.Optional[typing.Dict[str, typing.Any]]:
354464
def tts(self) -> typing.Optional[typing.Dict[str, typing.Any]]:
355465
return self._tts
356466

467+
@property
468+
def tts_sample_rate(self) -> typing.Optional[int]:
469+
return self._tts_sample_rate
470+
357471
@property
358472
def stt(self) -> typing.Optional[typing.Dict[str, typing.Any]]:
359473
return self._stt
@@ -536,6 +650,20 @@ def to_properties(
536650
expires_in: typing.Optional[int] = None,
537651
skip_vendor_validation: bool = False,
538652
) -> StartAgentsRequestProperties:
653+
# Validate the MLLM + enabled-avatar combination BEFORE generating the
654+
# RTC token so callers get a clear, actionable error first (matches the
655+
# TypeScript and Go SDKs' fail-fast contract).
656+
mllm_flag = isinstance(self._mllm, dict) and self._mllm.get("enable") is True
657+
is_mllm_mode = bool(mllm_flag or self._mllm is not None)
658+
avatar_enabled = (
659+
isinstance(self._avatar, dict) and self._avatar.get("enable") is not False
660+
)
661+
if is_mllm_mode and avatar_enabled:
662+
raise ValueError(
663+
"Avatars are only supported with the cascading ASR + LLM + TTS pipeline. "
664+
"Remove the avatar configuration when using MLLM, or switch to a cascading session."
665+
)
666+
539667
if token is None:
540668
if app_id is None or app_certificate is None:
541669
raise ValueError("Either token or app_id+app_certificate must be provided")
@@ -553,9 +681,6 @@ def to_properties(
553681
**token_kwargs,
554682
)
555683

556-
mllm_flag = isinstance(self._mllm, dict) and self._mllm.get("enable") is True
557-
is_mllm_mode = bool(mllm_flag or self._mllm is not None)
558-
559684
base_kwargs: typing.Dict[str, typing.Any] = {
560685
"channel": channel,
561686
"token": token,
@@ -579,11 +704,12 @@ def to_properties(
579704
base_kwargs["avatar"] = self._avatar
580705
if self._advanced_features is not None:
581706
base_kwargs["advanced_features"] = self._advanced_features
582-
if self._parameters is not None:
583-
if isinstance(self._parameters, dict):
584-
base_kwargs["parameters"] = StartAgentsRequestPropertiesParameters(**self._parameters)
707+
parameters = self._resolved_parameters()
708+
if parameters is not None:
709+
if isinstance(parameters, dict):
710+
base_kwargs["parameters"] = StartAgentsRequestPropertiesParameters(**parameters)
585711
else:
586-
base_kwargs["parameters"] = self._parameters
712+
base_kwargs["parameters"] = parameters
587713
if self._geofence is not None:
588714
base_kwargs["geofence"] = self._geofence
589715
if self._labels is not None:
@@ -596,12 +722,10 @@ def to_properties(
596722
if is_mllm_mode:
597723
if self._mllm is not None:
598724
mllm_config = dict(self._mllm)
599-
if self._greeting:
725+
if self._greeting is not None:
600726
mllm_config.setdefault("greeting_message", self._greeting)
601-
if self._failure_message:
727+
if self._failure_message is not None:
602728
mllm_config.setdefault("failure_message", self._failure_message)
603-
if self._max_history is not None:
604-
mllm_config.setdefault("max_history", self._max_history)
605729
base_kwargs["mllm"] = mllm_config
606730
return StartAgentsRequestProperties(**base_kwargs)
607731

@@ -617,14 +741,14 @@ def to_properties(
617741
llm_config = dict(self._llm)
618742
# Agent-level fields take priority over the vendor's defaults.
619743
# This matches the TS SDK where agent-level values override vendor config.
620-
if self._instructions:
744+
if self._instructions is not None:
621745
llm_config["system_messages"] = [{"role": "system", "content": self._instructions}]
622-
if self._greeting:
623-
llm_config.setdefault("greeting_message", self._greeting)
624-
if self._failure_message:
625-
llm_config.setdefault("failure_message", self._failure_message)
746+
if self._greeting is not None:
747+
llm_config["greeting_message"] = self._greeting
748+
if self._failure_message is not None:
749+
llm_config["failure_message"] = self._failure_message
626750
if self._max_history is not None:
627-
llm_config.setdefault("max_history", self._max_history)
751+
llm_config["max_history"] = self._max_history
628752

629753
base_kwargs["llm"] = llm_config
630754
base_kwargs["tts"] = self._tts

0 commit comments

Comments
 (0)