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
1 change: 1 addition & 0 deletions .fernignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Specify files that shouldn't be modified by Fern
src/agora_agent/pool_client.py
src/agora_agent/__init__.py
src/agora_agent/cn.py
src/agora_agent/core/domain.py
changelog.md

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
__pycache__/
dist/
poetry.toml
.venv/
docs/superpowers/
3 changes: 2 additions & 1 deletion docs/guides/avatars.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ agent = (
`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.

```python
from agora_agent import Agora, Area, CNAgent, GenericTTS, SpatiusAvatar, TencentSTT
from agora_agent import Agora, Area, CNAgent, GenericTTS
from agora_agent.cn import SpatiusAvatar, TencentSTT

client = Agora(
area=Area.CN,
Expand Down
57 changes: 48 additions & 9 deletions src/agora_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,57 @@
AgentSessionOptions,
CNAgent,
GlobalAgent,
GenericAvatar,
SpatiusAvatar,
RegionalAgent,
XaiGrok,
generate_rtc_token,
GenerateTokenOptions,
# Global (non-CN) vendor classes — re-exported from agentkit for static typing so
# `from agora_agent import DeepgramSTT` autocompletes. Runtime resolution and
# `__all__` membership come from the `__getattr__` fallback + the `__all__` union
# below, so these need no `_dynamic_imports` / `_ROOT_ALL` entries.
# CN vendors are intentionally not here — import them from `agora_agent.cn`.
AkoolAvatar,
AmazonBedrock,
AmazonSTT,
AmazonTTS,
AnamAvatar,
Anthropic,
AresSTT,
AssemblyAISTT,
AzureOpenAI,
CartesiaTTS,
CustomLLM,
DeepgramSTT,
DeepgramTTS,
Dify,
ElevenLabsTTS,
FishAudioTTS,
Gemini,
GeminiLive,
GenericAvatar,
GenericTTS,
GoogleSTT,
GoogleTTS,
Groq,
HeyGenAvatar,
HumeAITTS,
LiveAvatarAvatar,
MicrosoftSTT,
MicrosoftTTS,
MiniMaxTTS,
MurfTTS,
OpenAI,
OpenAIRealtime,
OpenAISTT,
OpenAITTS,
RimeTTS,
SarvamSTT,
SarvamTTS,
SpeechmaticsSTT,
VertexAI,
VertexAILLM,
XaiGrok,
XaiSTT,
XaiTTS,
)
from .agentkit.agent_session import AsyncAgentSession

Expand All @@ -39,9 +84,6 @@
"AsyncAgentSession": ".agentkit.agent_session",
"AsyncAgora": ".pool_client",
"AsyncAgentClient": ".pool_client",
"GenericAvatar": ".agentkit",
"SpatiusAvatar": ".agentkit",
"XaiGrok": ".agentkit",
"GenerateTokenOptions": ".agentkit",
"__version__": ".version",
"agentkit": ".agentkit",
Expand All @@ -63,9 +105,6 @@
"AsyncAgentSession",
"AsyncAgora",
"AsyncAgentClient",
"GenericAvatar",
"SpatiusAvatar",
"XaiGrok",
"GenerateTokenOptions",
"Pool",
"__version__",
Expand Down
2 changes: 1 addition & 1 deletion src/agora_agent/agentkit/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ def with_llm(self, vendor: BaseLLM) -> "Agent":
return new_agent

def with_tts(self, vendor: BaseTTS) -> "Agent":
sample_rate = vendor.sample_rate
sample_rate = vendor.resolved_sample_rate
if (
self._avatar_required_sample_rate not in (None, 0)
and sample_rate is not None
Expand Down
169 changes: 83 additions & 86 deletions src/agora_agent/agentkit/vendors/avatar.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import warnings
from typing import Any, Dict, Optional

from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import ConfigDict, Field, field_validator

from .base import BaseAvatar

Expand All @@ -10,7 +10,7 @@
AKOOL_SAMPLE_RATE = 16000


class LiveAvatarAvatarOptions(BaseModel):
class LiveAvatarAvatar(BaseAvatar):
model_config = ConfigDict(extra="forbid")

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


class LiveAvatarAvatar(BaseAvatar):
def __init__(self, **kwargs: Any):
self.options = LiveAvatarAvatarOptions(**kwargs)

@property
def required_sample_rate(self) -> int:
return LIVEAVATAR_SAMPLE_RATE

def to_config(self) -> Dict[str, Any]:
params: Dict[str, Any] = {
"api_key": self.options.api_key,
"quality": self.options.quality,
"agora_uid": self.options.agora_uid,
"api_key": self.api_key,
"quality": self.quality,
"agora_uid": self.agora_uid,
}

if self.options.agora_token is not None:
params["agora_token"] = self.options.agora_token
if self.options.avatar_id is not None:
params["avatar_id"] = self.options.avatar_id
if self.options.disable_idle_timeout is not None:
params["disable_idle_timeout"] = self.options.disable_idle_timeout
if self.options.activity_idle_timeout is not None:
params["activity_idle_timeout"] = self.options.activity_idle_timeout
if self.options.additional_params is not None:
params = {**self.options.additional_params, **params}

enable = self.options.enable if self.options.enable is not None else True
if self.agora_token is not None:
params["agora_token"] = self.agora_token
if self.avatar_id is not None:
params["avatar_id"] = self.avatar_id
if self.disable_idle_timeout is not None:
params["disable_idle_timeout"] = self.disable_idle_timeout
if self.activity_idle_timeout is not None:
params["activity_idle_timeout"] = self.activity_idle_timeout
if self.additional_params is not None:
params = {**self.additional_params, **params}

enable = self.enable if self.enable is not None else True
return {"enable": enable, "vendor": "liveavatar", "params": params}


class HeyGenAvatarOptions(LiveAvatarAvatarOptions):
"""Deprecated: use :class:`LiveAvatarAvatarOptions` instead."""


class HeyGenAvatar(BaseAvatar):
"""Deprecated: HeyGen has been renamed to LiveAvatar. Use LiveAvatarAvatar instead."""

def __init__(self, **kwargs: Any):
model_config = ConfigDict(extra="forbid")

api_key: str = Field(..., description="LiveAvatar API key")
quality: str = Field(..., description="Avatar quality: low, medium, or high")
agora_uid: str = Field(..., description="Agora UID for the avatar stream")
agora_token: Optional[str] = Field(default=None, description="RTC token for avatar authentication")
avatar_id: Optional[str] = Field(default=None, description="Avatar ID")
enable: Optional[bool] = Field(default=None, description="Enable avatar (default: true)")
disable_idle_timeout: Optional[bool] = Field(default=None, description="Whether to disable idle timeout")
activity_idle_timeout: Optional[int] = Field(default=None, description="Idle timeout in seconds")
additional_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional vendor-specific parameters")

@field_validator("quality")
@classmethod
def validate_quality(cls, v: str) -> str:
valid = ("low", "medium", "high")
if v not in valid:
raise ValueError(f"Invalid quality '{v}'. Must be one of: {', '.join(valid)}")
return v

def model_post_init(self, __context: Any) -> None:
# stacklevel=3: warn() <- model_post_init <- pydantic __init__ <- user code,
# so the warning points at the user's construction site, not pydantic internals.
warnings.warn(
"HeyGenAvatar is deprecated; use LiveAvatarAvatar instead.",
DeprecationWarning,
stacklevel=2,
stacklevel=3,
)
self.options = HeyGenAvatarOptions(**kwargs)

@property
def required_sample_rate(self) -> int:
return HEYGEN_SAMPLE_RATE

def to_config(self) -> Dict[str, Any]:
params: Dict[str, Any] = {
"api_key": self.options.api_key,
"quality": self.options.quality,
"agora_uid": self.options.agora_uid,
"api_key": self.api_key,
"quality": self.quality,
"agora_uid": self.agora_uid,
}

if self.options.agora_token is not None:
params["agora_token"] = self.options.agora_token
if self.options.avatar_id is not None:
params["avatar_id"] = self.options.avatar_id
if self.options.disable_idle_timeout is not None:
params["disable_idle_timeout"] = self.options.disable_idle_timeout
if self.options.activity_idle_timeout is not None:
params["activity_idle_timeout"] = self.options.activity_idle_timeout
if self.options.additional_params is not None:
params = {**self.options.additional_params, **params}

enable = self.options.enable if self.options.enable is not None else True
if self.agora_token is not None:
params["agora_token"] = self.agora_token
if self.avatar_id is not None:
params["avatar_id"] = self.avatar_id
if self.disable_idle_timeout is not None:
params["disable_idle_timeout"] = self.disable_idle_timeout
if self.activity_idle_timeout is not None:
params["activity_idle_timeout"] = self.activity_idle_timeout
if self.additional_params is not None:
params = {**self.additional_params, **params}

enable = self.enable if self.enable is not None else True
return {"enable": enable, "vendor": "heygen", "params": params}


class AkoolAvatarOptions(BaseModel):
class AkoolAvatar(BaseAvatar):
model_config = ConfigDict(extra="forbid")

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


class AkoolAvatar(BaseAvatar):
def __init__(self, **kwargs: Any):
self.options = AkoolAvatarOptions(**kwargs)

@property
def required_sample_rate(self) -> int:
return AKOOL_SAMPLE_RATE

def to_config(self) -> Dict[str, Any]:
params: Dict[str, Any] = {
"api_key": self.options.api_key,
"api_key": self.api_key,
}

if self.options.avatar_id is not None:
params["avatar_id"] = self.options.avatar_id
if self.options.additional_params is not None:
params = {**self.options.additional_params, **params}
if self.avatar_id is not None:
params["avatar_id"] = self.avatar_id
if self.additional_params is not None:
params = {**self.additional_params, **params}

enable = self.options.enable if self.options.enable is not None else True
enable = self.enable if self.enable is not None else True
return {"enable": enable, "vendor": "akool", "params": params}


class GenericAvatarOptions(BaseModel):
class GenericAvatar(BaseAvatar):
model_config = ConfigDict(extra="forbid")

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


class GenericAvatar(BaseAvatar):
def __init__(self, **kwargs: Any):
self.options = GenericAvatarOptions(**kwargs)

@property
def required_sample_rate(self) -> int:
return 0

def to_config(self) -> Dict[str, Any]:
params: Dict[str, Any] = {
"api_key": self.options.api_key,
"api_base_url": self.options.api_base_url,
"avatar_id": self.options.avatar_id,
"agora_uid": self.options.agora_uid,
"api_key": self.api_key,
"api_base_url": self.api_base_url,
"avatar_id": self.avatar_id,
"agora_uid": self.agora_uid,
}

if self.options.agora_appid is not None:
params["agora_appid"] = self.options.agora_appid
if self.options.agora_token is not None:
params["agora_token"] = self.options.agora_token
if self.options.agora_channel is not None:
params["agora_channel"] = self.options.agora_channel
if self.options.additional_params is not None:
params = {**self.options.additional_params, **params}
if self.agora_appid is not None:
params["agora_appid"] = self.agora_appid
if self.agora_token is not None:
params["agora_token"] = self.agora_token
if self.agora_channel is not None:
params["agora_channel"] = self.agora_channel
if self.additional_params is not None:
params = {**self.additional_params, **params}

enable = self.options.enable if self.options.enable is not None else True
enable = self.enable if self.enable is not None else True
return {"enable": enable, "vendor": "generic", "params": params}


class AnamAvatarOptions(BaseModel):
class AnamAvatar(BaseAvatar):
model_config = ConfigDict(extra="forbid")

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


class AnamAvatar(BaseAvatar):
def __init__(self, **kwargs: Any):
self.options = AnamAvatarOptions(**kwargs)

@property
def required_sample_rate(self) -> int:
return 0

def to_config(self) -> Dict[str, Any]:
params: Dict[str, Any] = {
"api_key": self.options.api_key,
"avatar_id": self.options.avatar_id,
"api_key": self.api_key,
"avatar_id": self.avatar_id,
}

if self.options.additional_params is not None:
params = {**self.options.additional_params, **params}
if self.additional_params is not None:
params = {**self.additional_params, **params}

enable = self.options.enable if self.options.enable is not None else True
enable = self.enable if self.enable is not None else True
return {"enable": enable, "vendor": "anam", "params": params}
Loading
Loading