Skip to content

Commit cd5a222

Browse files
authored
Merge pull request #54 from AgoraIO/vendor-config-ide-hints
Collapse vendor configs into pydantic models for IDE autocomplete
2 parents ea717b7 + be39ea4 commit cd5a222

17 files changed

Lines changed: 1690 additions & 1249 deletions

File tree

.fernignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Specify files that shouldn't be modified by Fern
22
src/agora_agent/pool_client.py
33
src/agora_agent/__init__.py
4+
src/agora_agent/cn.py
45
src/agora_agent/core/domain.py
56
changelog.md
67

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
__pycache__/
44
dist/
55
poetry.toml
6+
.venv/
7+
docs/superpowers/

docs/guides/avatars.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ agent = (
132132
`SpatiusAvatar` is available for `Area.CN` sessions. Provide `spatius_api_key`, `spatius_app_id`, `spatius_avatar_id`, and `agora_uid` when constructing the avatar. `agora_token` is optional and is generated at session start when omitted, like SenseTime and Generic avatars.
133133

134134
```python
135-
from agora_agent import Agora, Area, CNAgent, GenericTTS, SpatiusAvatar, TencentSTT
135+
from agora_agent import Agora, Area, CNAgent, GenericTTS
136+
from agora_agent.cn import SpatiusAvatar, TencentSTT
136137

137138
client = Agora(
138139
area=Area.CN,

src/agora_agent/__init__.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,57 @@
2020
AgentSessionOptions,
2121
CNAgent,
2222
GlobalAgent,
23-
GenericAvatar,
24-
SpatiusAvatar,
2523
RegionalAgent,
26-
XaiGrok,
2724
generate_rtc_token,
2825
GenerateTokenOptions,
26+
# Global (non-CN) vendor classes — re-exported from agentkit for static typing so
27+
# `from agora_agent import DeepgramSTT` autocompletes. Runtime resolution and
28+
# `__all__` membership come from the `__getattr__` fallback + the `__all__` union
29+
# below, so these need no `_dynamic_imports` / `_ROOT_ALL` entries.
30+
# CN vendors are intentionally not here — import them from `agora_agent.cn`.
31+
AkoolAvatar,
32+
AmazonBedrock,
33+
AmazonSTT,
34+
AmazonTTS,
35+
AnamAvatar,
36+
Anthropic,
37+
AresSTT,
38+
AssemblyAISTT,
39+
AzureOpenAI,
40+
CartesiaTTS,
41+
CustomLLM,
42+
DeepgramSTT,
43+
DeepgramTTS,
44+
Dify,
45+
ElevenLabsTTS,
46+
FishAudioTTS,
47+
Gemini,
48+
GeminiLive,
49+
GenericAvatar,
50+
GenericTTS,
51+
GoogleSTT,
52+
GoogleTTS,
53+
Groq,
54+
HeyGenAvatar,
55+
HumeAITTS,
56+
LiveAvatarAvatar,
57+
MicrosoftSTT,
58+
MicrosoftTTS,
59+
MiniMaxTTS,
60+
MurfTTS,
61+
OpenAI,
62+
OpenAIRealtime,
63+
OpenAISTT,
64+
OpenAITTS,
65+
RimeTTS,
66+
SarvamSTT,
67+
SarvamTTS,
68+
SpeechmaticsSTT,
69+
VertexAI,
70+
VertexAILLM,
71+
XaiGrok,
72+
XaiSTT,
73+
XaiTTS,
2974
)
3075
from .agentkit.agent_session import AsyncAgentSession
3176

@@ -39,9 +84,6 @@
3984
"AsyncAgentSession": ".agentkit.agent_session",
4085
"AsyncAgora": ".pool_client",
4186
"AsyncAgentClient": ".pool_client",
42-
"GenericAvatar": ".agentkit",
43-
"SpatiusAvatar": ".agentkit",
44-
"XaiGrok": ".agentkit",
4587
"GenerateTokenOptions": ".agentkit",
4688
"__version__": ".version",
4789
"agentkit": ".agentkit",
@@ -63,9 +105,6 @@
63105
"AsyncAgentSession",
64106
"AsyncAgora",
65107
"AsyncAgentClient",
66-
"GenericAvatar",
67-
"SpatiusAvatar",
68-
"XaiGrok",
69108
"GenerateTokenOptions",
70109
"Pool",
71110
"__version__",

src/agora_agent/agentkit/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ def with_llm(self, vendor: BaseLLM) -> "Agent":
473473
return new_agent
474474

475475
def with_tts(self, vendor: BaseTTS) -> "Agent":
476-
sample_rate = vendor.sample_rate
476+
sample_rate = vendor.resolved_sample_rate
477477
if (
478478
self._avatar_required_sample_rate not in (None, 0)
479479
and sample_rate is not None
Lines changed: 83 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import warnings
22
from typing import Any, Dict, Optional
33

4-
from pydantic import BaseModel, ConfigDict, Field, field_validator
4+
from pydantic import ConfigDict, Field, field_validator
55

66
from .base import BaseAvatar
77

@@ -10,7 +10,7 @@
1010
AKOOL_SAMPLE_RATE = 16000
1111

1212

13-
class LiveAvatarAvatarOptions(BaseModel):
13+
class LiveAvatarAvatar(BaseAvatar):
1414
model_config = ConfigDict(extra="forbid")
1515

1616
api_key: str = Field(..., description="LiveAvatar API key")
@@ -31,110 +31,117 @@ def validate_quality(cls, v: str) -> str:
3131
raise ValueError(f"Invalid quality '{v}'. Must be one of: {', '.join(valid)}")
3232
return v
3333

34-
35-
class LiveAvatarAvatar(BaseAvatar):
36-
def __init__(self, **kwargs: Any):
37-
self.options = LiveAvatarAvatarOptions(**kwargs)
38-
3934
@property
4035
def required_sample_rate(self) -> int:
4136
return LIVEAVATAR_SAMPLE_RATE
4237

4338
def to_config(self) -> Dict[str, Any]:
4439
params: Dict[str, Any] = {
45-
"api_key": self.options.api_key,
46-
"quality": self.options.quality,
47-
"agora_uid": self.options.agora_uid,
40+
"api_key": self.api_key,
41+
"quality": self.quality,
42+
"agora_uid": self.agora_uid,
4843
}
4944

50-
if self.options.agora_token is not None:
51-
params["agora_token"] = self.options.agora_token
52-
if self.options.avatar_id is not None:
53-
params["avatar_id"] = self.options.avatar_id
54-
if self.options.disable_idle_timeout is not None:
55-
params["disable_idle_timeout"] = self.options.disable_idle_timeout
56-
if self.options.activity_idle_timeout is not None:
57-
params["activity_idle_timeout"] = self.options.activity_idle_timeout
58-
if self.options.additional_params is not None:
59-
params = {**self.options.additional_params, **params}
60-
61-
enable = self.options.enable if self.options.enable is not None else True
45+
if self.agora_token is not None:
46+
params["agora_token"] = self.agora_token
47+
if self.avatar_id is not None:
48+
params["avatar_id"] = self.avatar_id
49+
if self.disable_idle_timeout is not None:
50+
params["disable_idle_timeout"] = self.disable_idle_timeout
51+
if self.activity_idle_timeout is not None:
52+
params["activity_idle_timeout"] = self.activity_idle_timeout
53+
if self.additional_params is not None:
54+
params = {**self.additional_params, **params}
55+
56+
enable = self.enable if self.enable is not None else True
6257
return {"enable": enable, "vendor": "liveavatar", "params": params}
6358

6459

65-
class HeyGenAvatarOptions(LiveAvatarAvatarOptions):
66-
"""Deprecated: use :class:`LiveAvatarAvatarOptions` instead."""
67-
68-
6960
class HeyGenAvatar(BaseAvatar):
7061
"""Deprecated: HeyGen has been renamed to LiveAvatar. Use LiveAvatarAvatar instead."""
7162

72-
def __init__(self, **kwargs: Any):
63+
model_config = ConfigDict(extra="forbid")
64+
65+
api_key: str = Field(..., description="LiveAvatar API key")
66+
quality: str = Field(..., description="Avatar quality: low, medium, or high")
67+
agora_uid: str = Field(..., description="Agora UID for the avatar stream")
68+
agora_token: Optional[str] = Field(default=None, description="RTC token for avatar authentication")
69+
avatar_id: Optional[str] = Field(default=None, description="Avatar ID")
70+
enable: Optional[bool] = Field(default=None, description="Enable avatar (default: true)")
71+
disable_idle_timeout: Optional[bool] = Field(default=None, description="Whether to disable idle timeout")
72+
activity_idle_timeout: Optional[int] = Field(default=None, description="Idle timeout in seconds")
73+
additional_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional vendor-specific parameters")
74+
75+
@field_validator("quality")
76+
@classmethod
77+
def validate_quality(cls, v: str) -> str:
78+
valid = ("low", "medium", "high")
79+
if v not in valid:
80+
raise ValueError(f"Invalid quality '{v}'. Must be one of: {', '.join(valid)}")
81+
return v
82+
83+
def model_post_init(self, __context: Any) -> None:
84+
# stacklevel=3: warn() <- model_post_init <- pydantic __init__ <- user code,
85+
# so the warning points at the user's construction site, not pydantic internals.
7386
warnings.warn(
7487
"HeyGenAvatar is deprecated; use LiveAvatarAvatar instead.",
7588
DeprecationWarning,
76-
stacklevel=2,
89+
stacklevel=3,
7790
)
78-
self.options = HeyGenAvatarOptions(**kwargs)
7991

8092
@property
8193
def required_sample_rate(self) -> int:
8294
return HEYGEN_SAMPLE_RATE
8395

8496
def to_config(self) -> Dict[str, Any]:
8597
params: Dict[str, Any] = {
86-
"api_key": self.options.api_key,
87-
"quality": self.options.quality,
88-
"agora_uid": self.options.agora_uid,
98+
"api_key": self.api_key,
99+
"quality": self.quality,
100+
"agora_uid": self.agora_uid,
89101
}
90102

91-
if self.options.agora_token is not None:
92-
params["agora_token"] = self.options.agora_token
93-
if self.options.avatar_id is not None:
94-
params["avatar_id"] = self.options.avatar_id
95-
if self.options.disable_idle_timeout is not None:
96-
params["disable_idle_timeout"] = self.options.disable_idle_timeout
97-
if self.options.activity_idle_timeout is not None:
98-
params["activity_idle_timeout"] = self.options.activity_idle_timeout
99-
if self.options.additional_params is not None:
100-
params = {**self.options.additional_params, **params}
101-
102-
enable = self.options.enable if self.options.enable is not None else True
103+
if self.agora_token is not None:
104+
params["agora_token"] = self.agora_token
105+
if self.avatar_id is not None:
106+
params["avatar_id"] = self.avatar_id
107+
if self.disable_idle_timeout is not None:
108+
params["disable_idle_timeout"] = self.disable_idle_timeout
109+
if self.activity_idle_timeout is not None:
110+
params["activity_idle_timeout"] = self.activity_idle_timeout
111+
if self.additional_params is not None:
112+
params = {**self.additional_params, **params}
113+
114+
enable = self.enable if self.enable is not None else True
103115
return {"enable": enable, "vendor": "heygen", "params": params}
104116

105117

106-
class AkoolAvatarOptions(BaseModel):
118+
class AkoolAvatar(BaseAvatar):
107119
model_config = ConfigDict(extra="forbid")
108120

109121
api_key: str = Field(..., description="Akool API key")
110122
avatar_id: Optional[str] = Field(default=None, description="Avatar ID")
111123
enable: Optional[bool] = Field(default=None, description="Enable avatar (default: true)")
112124
additional_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional vendor-specific parameters")
113125

114-
115-
class AkoolAvatar(BaseAvatar):
116-
def __init__(self, **kwargs: Any):
117-
self.options = AkoolAvatarOptions(**kwargs)
118-
119126
@property
120127
def required_sample_rate(self) -> int:
121128
return AKOOL_SAMPLE_RATE
122129

123130
def to_config(self) -> Dict[str, Any]:
124131
params: Dict[str, Any] = {
125-
"api_key": self.options.api_key,
132+
"api_key": self.api_key,
126133
}
127134

128-
if self.options.avatar_id is not None:
129-
params["avatar_id"] = self.options.avatar_id
130-
if self.options.additional_params is not None:
131-
params = {**self.options.additional_params, **params}
135+
if self.avatar_id is not None:
136+
params["avatar_id"] = self.avatar_id
137+
if self.additional_params is not None:
138+
params = {**self.additional_params, **params}
132139

133-
enable = self.options.enable if self.options.enable is not None else True
140+
enable = self.enable if self.enable is not None else True
134141
return {"enable": enable, "vendor": "akool", "params": params}
135142

136143

137-
class GenericAvatarOptions(BaseModel):
144+
class GenericAvatar(BaseAvatar):
138145
model_config = ConfigDict(extra="forbid")
139146

140147
api_key: str = Field(..., description="Generic avatar provider API key")
@@ -147,61 +154,51 @@ class GenericAvatarOptions(BaseModel):
147154
enable: Optional[bool] = Field(default=None, description="Enable avatar (default: true)")
148155
additional_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional vendor-specific parameters")
149156

150-
151-
class GenericAvatar(BaseAvatar):
152-
def __init__(self, **kwargs: Any):
153-
self.options = GenericAvatarOptions(**kwargs)
154-
155157
@property
156158
def required_sample_rate(self) -> int:
157159
return 0
158160

159161
def to_config(self) -> Dict[str, Any]:
160162
params: Dict[str, Any] = {
161-
"api_key": self.options.api_key,
162-
"api_base_url": self.options.api_base_url,
163-
"avatar_id": self.options.avatar_id,
164-
"agora_uid": self.options.agora_uid,
163+
"api_key": self.api_key,
164+
"api_base_url": self.api_base_url,
165+
"avatar_id": self.avatar_id,
166+
"agora_uid": self.agora_uid,
165167
}
166168

167-
if self.options.agora_appid is not None:
168-
params["agora_appid"] = self.options.agora_appid
169-
if self.options.agora_token is not None:
170-
params["agora_token"] = self.options.agora_token
171-
if self.options.agora_channel is not None:
172-
params["agora_channel"] = self.options.agora_channel
173-
if self.options.additional_params is not None:
174-
params = {**self.options.additional_params, **params}
169+
if self.agora_appid is not None:
170+
params["agora_appid"] = self.agora_appid
171+
if self.agora_token is not None:
172+
params["agora_token"] = self.agora_token
173+
if self.agora_channel is not None:
174+
params["agora_channel"] = self.agora_channel
175+
if self.additional_params is not None:
176+
params = {**self.additional_params, **params}
175177

176-
enable = self.options.enable if self.options.enable is not None else True
178+
enable = self.enable if self.enable is not None else True
177179
return {"enable": enable, "vendor": "generic", "params": params}
178180

179181

180-
class AnamAvatarOptions(BaseModel):
182+
class AnamAvatar(BaseAvatar):
181183
model_config = ConfigDict(extra="forbid")
182184

183185
api_key: str = Field(..., description="Anam API key")
184186
avatar_id: str = Field(..., description="Anam avatar ID")
185187
enable: Optional[bool] = Field(default=None, description="Enable avatar (default: true)")
186188
additional_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional vendor-specific parameters")
187189

188-
189-
class AnamAvatar(BaseAvatar):
190-
def __init__(self, **kwargs: Any):
191-
self.options = AnamAvatarOptions(**kwargs)
192-
193190
@property
194191
def required_sample_rate(self) -> int:
195192
return 0
196193

197194
def to_config(self) -> Dict[str, Any]:
198195
params: Dict[str, Any] = {
199-
"api_key": self.options.api_key,
200-
"avatar_id": self.options.avatar_id,
196+
"api_key": self.api_key,
197+
"avatar_id": self.avatar_id,
201198
}
202199

203-
if self.options.additional_params is not None:
204-
params = {**self.options.additional_params, **params}
200+
if self.additional_params is not None:
201+
params = {**self.additional_params, **params}
205202

206-
enable = self.options.enable if self.options.enable is not None else True
203+
enable = self.enable if self.enable is not None else True
207204
return {"enable": enable, "vendor": "anam", "params": params}

0 commit comments

Comments
 (0)