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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
- name: Install dependencies
run: poetry install
- name: Release metadata check
run: poetry run python scripts/check_release_workflow.py
- name: Compile
run: poetry run mypy .
test:
Expand Down
4 changes: 2 additions & 2 deletions compat/agora-agent-server-sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "agora-agent-server-sdk"

[tool.poetry]
name = "agora-agent-server-sdk"
version = "v2.1.0"
version = "v2.1.1"
description = "Compatibility shim for the renamed agora-agents package."
readme = "README.md"
authors = []
Expand Down Expand Up @@ -35,7 +35,7 @@ Repository = 'https://github.com/AgoraIO-Conversational-AI/agent-server-sdk-pyth

[tool.poetry.dependencies]
python = "^3.8"
agora-agents = ">=2.1.0,<3.0.0"
agora-agents = ">=2.1.1,<3.0.0"

[build-system]
requires = ["poetry-core"]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "agora-agents"

[tool.poetry]
name = "agora-agents"
version = "v2.1.0"
version = "v2.1.1"
description = ""
readme = "README.md"
authors = []
Expand Down
54 changes: 54 additions & 0 deletions scripts/check_release_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python3

import re
import sys
from pathlib import Path
from typing import NoReturn


def fail(message: str) -> NoReturn:
print(message, file=sys.stderr)
raise SystemExit(1)


def read_version(path: str) -> str:
text = Path(path).read_text()
match = re.search(r'^version\s*=\s*"v?([^"]+)"', text, re.M)
if not match:
fail(f"version not found in {path}")
return match.group(1)


def read_compat_dependency(path: str) -> str:
text = Path(path).read_text()
match = re.search(r'^agora-agents\s*=\s*"([^"]+)"', text, re.M)
if not match:
fail(f"agora-agents dependency not found in {path}")
return match.group(1)


root_version = read_version("pyproject.toml")
compat_pyproject = "compat/agora-agent-server-sdk/pyproject.toml"
compat_version = read_version(compat_pyproject)
compat_dependency = read_compat_dependency(compat_pyproject)

if compat_version != root_version:
fail(f"Compat package version ({compat_version}) must match root package version ({root_version}).")

expected_dependency = f">={root_version},<3.0.0"
if compat_dependency != expected_dependency:
fail(f"Compat package dependency on agora-agents ({compat_dependency}) must be {expected_dependency}.")

release_workflow = Path(".github/workflows/release.yml").read_text()
required_workflow_markers = [
("contents: write", "release workflow must have contents: write so it can create GitHub releases"),
("gh release create", "release workflow must create a GitHub release when one does not exist"),
("gh release edit", "release workflow must update an existing GitHub release"),
("release_notes.md", "release workflow must generate and use a release notes file"),
]

for marker, message in required_workflow_markers:
if marker not in release_workflow:
fail(message)

print("Release metadata and workflow checks passed.")
47 changes: 38 additions & 9 deletions src/agora_agent/agentkit/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import time
import typing
import typing_extensions
import warnings

if typing.TYPE_CHECKING:
from .agent_session import AgentSession, AsyncAgentSession
Expand Down Expand Up @@ -815,6 +816,8 @@ def to_properties(
app_certificate: typing.Optional[str] = None,
expires_in: typing.Optional[int] = None,
skip_vendor_validation: bool = False,
skip_vendor_validation_categories: typing.Optional[typing.AbstractSet[str]] = None,
allow_missing_vendor_categories: typing.Optional[typing.AbstractSet[str]] = None,
) -> StartAgentsRequestProperties:
# Validate the MLLM + enabled-avatar combination BEFORE generating the
# RTC token so callers get a clear, actionable error first (matches the
Expand Down Expand Up @@ -895,19 +898,49 @@ def to_properties(
base_kwargs["mllm"] = mllm_config
return StartAgentsRequestProperties(**base_kwargs)

base_kwargs["asr"] = self._resolve_asr_config()
if skip_vendor_validation:
warnings.warn(
"skip_vendor_validation is deprecated and will be removed in a future release. "
"Use skip_vendor_validation_categories and allow_missing_vendor_categories instead.",
DeprecationWarning,
stacklevel=2,
)

skip_categories = set(skip_vendor_validation_categories or ())
allow_missing_categories = set(allow_missing_vendor_categories or ())
if skip_vendor_validation:
skip_categories.update({"asr", "llm", "tts"})
allow_missing_categories.update({"asr", "llm", "tts"})

skip_asr_validation = skip_vendor_validation or "asr" in skip_categories
skip_llm_validation = skip_vendor_validation or "llm" in skip_categories
skip_tts_validation = skip_vendor_validation or "tts" in skip_categories
allow_missing_asr = "asr" in allow_missing_categories
allow_missing_llm = "llm" in allow_missing_categories
allow_missing_tts = "tts" in allow_missing_categories

if not skip_asr_validation and (self._stt is not None or not allow_missing_asr):
base_kwargs["asr"] = self._resolve_asr_config()
base_kwargs["turn_detection"] = self._resolve_turn_detection_config()

if skip_vendor_validation:
return StartAgentsRequestProperties(**base_kwargs)

if self._tts is None:
if self._tts is None and not (skip_tts_validation or allow_missing_tts):
raise ValueError("TTS configuration is required. Use with_tts() to set it.")

if self._llm is None:
if self._llm is None and not (skip_llm_validation or allow_missing_llm):
raise ValueError("LLM configuration is required. Use with_llm() to set it.")

llm_config = dict(self._llm)
if self._llm is not None and not skip_llm_validation:
base_kwargs["llm"] = self._resolve_llm_config()
if self._tts is not None and not skip_tts_validation:
base_kwargs["tts"] = self._tts

return StartAgentsRequestProperties(**base_kwargs)

def _resolve_llm_config(self) -> typing.Dict[str, typing.Any]:
llm_config = dict(self._llm or {})
# Agent-level fields take priority over the vendor's defaults.
# This matches the TS SDK where agent-level values override vendor config.
if self._instructions is not None:
Expand All @@ -920,11 +953,7 @@ def to_properties(
llm_config["failure_message"] = self._failure_message
if self._max_history is not None:
llm_config["max_history"] = self._max_history

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

return StartAgentsRequestProperties(**base_kwargs)
return llm_config

def _resolve_asr_config(self) -> typing.Dict[str, typing.Any]:
asr_config = dict(self._stt or {})
Expand Down
52 changes: 47 additions & 5 deletions src/agora_agent/agentkit/agent_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@
validate_avatar_config,
validate_tts_sample_rate,
)
from .presets import resolve_session_presets
from .presets import (
get_preset_category,
infer_asr_preset,
infer_llm_preset,
infer_tts_preset,
normalize_preset_input,
resolve_session_presets,
)
from .token import generate_convo_ai_token, _parse_numeric_uid


Expand Down Expand Up @@ -294,15 +301,17 @@ def _is_mllm_mode(self) -> bool:
def _build_start_properties(
self,
token_opts: typing.Dict[str, typing.Any],
skip_vendor_validation: bool,
skip_vendor_validation_categories: typing.AbstractSet[str],
allow_missing_vendor_categories: typing.AbstractSet[str],
) -> typing.Dict[str, typing.Any]:
base_properties = self._agent.to_properties(
channel=self._channel,
agent_uid=self._agent_uid,
remote_uids=self._remote_uids,
idle_timeout=self._idle_timeout,
enable_string_uid=self._enable_string_uid,
skip_vendor_validation=skip_vendor_validation,
skip_vendor_validation_categories=skip_vendor_validation_categories,
allow_missing_vendor_categories=allow_missing_vendor_categories,
**token_opts,
)
properties = self._dump_model(base_properties)
Expand Down Expand Up @@ -340,6 +349,29 @@ def _build_start_properties(

return properties

def _vendor_validation_categories(
self,
pipeline_id: typing.Optional[str],
) -> typing.Tuple[typing.Set[str], typing.Set[str]]:
skip_categories: typing.Set[str] = set()
allow_missing_categories: typing.Set[str] = {"asr", "llm", "tts"} if pipeline_id else set()

preset = normalize_preset_input(self._preset)
if preset:
for item in preset.split(","):
category = get_preset_category(item)
if category is not None:
skip_categories.add(category)
allow_missing_categories.add(category)

if infer_asr_preset(self._agent.stt):
skip_categories.add("asr")
if infer_llm_preset(self._agent.llm):
skip_categories.add("llm")
if infer_tts_preset(self._agent.tts):
skip_categories.add("tts")
return skip_categories, allow_missing_categories

@staticmethod
def _page_value(pagination: typing.Any, field: str) -> typing.Any:
if pagination is None:
Expand Down Expand Up @@ -460,7 +492,12 @@ def start(self) -> str:
"expires_in": self._expires_in,
}

properties = self._build_start_properties(token_opts, skip_vendor_validation=bool(self._preset or pipeline_id))
skip_categories, allow_missing_categories = self._vendor_validation_categories(pipeline_id)
properties = self._build_start_properties(
token_opts,
skip_vendor_validation_categories=skip_categories,
allow_missing_vendor_categories=allow_missing_categories,
)
resolved_preset, resolved_properties = resolve_session_presets(
self._preset,
properties,
Expand Down Expand Up @@ -782,7 +819,12 @@ async def start(self) -> str:
"expires_in": self._expires_in,
}

properties = self._build_start_properties(token_opts, skip_vendor_validation=bool(self._preset or pipeline_id))
skip_categories, allow_missing_categories = self._vendor_validation_categories(pipeline_id)
properties = self._build_start_properties(
token_opts,
skip_vendor_validation_categories=skip_categories,
allow_missing_vendor_categories=allow_missing_categories,
)
resolved_preset, resolved_properties = resolve_session_presets(
self._preset,
properties,
Expand Down
16 changes: 14 additions & 2 deletions src/agora_agent/agentkit/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ class _AgentPresets:
DeepgramPresetModels = ("nova-2", "nova-3")
OpenAIPresetModels = ("gpt-4o-mini", "gpt-4.1-mini", "gpt-5-nano", "gpt-5-mini")
OpenAITtsPresetModels = ("tts-1",)
MiniMaxPresetModels = ("speech-2.6-turbo", "speech_2_6_turbo", "speech-2.8-turbo", "speech_2_8_turbo")
MiniMaxPresetModels = (
"speech-2.6-turbo",
"speech_2_6_turbo",
"speech-2.8-turbo",
"speech_2_8_turbo",
)

PresetInput = typing.Union[str, typing.Sequence[str]]

Expand All @@ -61,7 +66,10 @@ class _AgentPresets:


def _normalize_model_name(value: typing.Any) -> typing.Optional[str]:
return value.strip().lower() if isinstance(value, str) else None
if not isinstance(value, str):
return None
normalized = value.strip().lower()
return normalized if normalized else None


def _parse_preset_input(preset: typing.Optional[PresetInput]) -> typing.List[str]:
Expand All @@ -87,6 +95,10 @@ def _get_preset_category(preset: str) -> typing.Optional[str]:
return None


def get_preset_category(preset: str) -> typing.Optional[str]:
return _get_preset_category(preset)


def _omit_none(value: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
next_value = {k: v for k, v in value.items() if v is not None}
return next_value or None
Expand Down
4 changes: 2 additions & 2 deletions src/agora_agent/core/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ def __init__(

def get_headers(self) -> typing.Dict[str, str]:
headers: typing.Dict[str, str] = {
"User-Agent": "agora-agents/v2.1.0",
"User-Agent": "agora-agents/v2.1.1",
"X-Fern-Language": "Python",
"X-Fern-SDK-Name": "agora-agents",
"X-Fern-SDK-Version": "v2.1.0",
"X-Fern-SDK-Version": "v2.1.1",
**(self.get_custom_headers() or {}),
}
headers["Authorization"] = httpx.BasicAuth(self._get_username(), self._get_password())._auth_header
Expand Down
Loading
Loading