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
4 changes: 2 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).

### Added

- **Inline REST LLM tools** — Global and CN LLM vendors now accept dictionary-based `tools` definitions and serialize them to `llm.tools`. Added public `LlmToolConfig`, `LlmToolExecutionConfig`, `LlmToolFunctionConfig`, `LlmToolFunctionParametersConfig`, and `LlmToolServerConfig` aliases. Tool execution must be enabled explicitly with `Agent.with_tools()`.
- **Inline REST LLM tools** — Global and CN LLM vendors now accept dictionary-based definitions or the exported typed `LlmToolConfig` models and serialize them to `llm.tools`. Added public `LlmToolConfig`, `LlmToolExecutionConfig`, `LlmToolFunctionConfig`, `LlmToolFunctionParametersConfig`, and `LlmToolServerConfig` aliases. Tool execution must be enabled explicitly with `Agent.with_tools()`.

### Changed

- **Generated filler words configuration** — Generated filler word settings are now optional. The service can use default generator settings when `generated_config` is omitted, and `llm_provider`, `prompt`, and `fallback_strategy` may be omitted individually.
- **ASR hotwords** — `keywords` on `AresSTT` and `FengmingSTT` now serialize as top-level `asr.keywords`, matching the current OpenAPI schema. Vendor-specific `additional_params` remain under `asr.params`; nested `additional_params["keywords"]` is rejected to prevent ambiguous requests.

## [v2.7.2] — 2026-08-26

Expand Down Expand Up @@ -65,7 +66,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
- **Qwen Omni Realtime MLLM (CN)** — `QwenOmni` (`mllm.vendor`: `qwen_omni`), exported from `agora_agent.cn`.
- **MLLM support in AgentKit vendor namespaces** — Global and CN vendor catalogs now expose an `mllm` namespace, MLLM vendors are validated against the client area, and `CNAgent`/`GlobalAgent` expose typed `with_mllm()`.
- **Typecast TTS** — `TypecastTTS` (`tts.vendor`: `typecast`).
- **ASR hotwords** — `keywords` on `AresSTT` and `FengmingSTT`, serialized as top-level `asr.keywords`. Vendor-specific `additional_params` remain under `asr.params`.
- **Configurable API base URL** — Setting `AGORA_AGENTS_API_BASE_URL` overrides regional domain selection; when set, the domain pool skips DNS-based domain selection and region cycling and always uses the configured base URL.

### Changed
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ When `enable_rtm=True`, AgentKit defaults `parameters.data_channel` to `"rtm"` u

### `with_tools(enabled: bool = True) -> Agent`

Enable or disable MCP tool invocation by setting `advanced_features.enable_tools`.
Enable or disable MCP and inline REST tool invocation by setting `advanced_features.enable_tools`.

### `with_parameters(parameters: SessionParams) -> Agent`

Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ Repository = 'https://github.com/AgoraIO-Conversational-AI/agent-server-sdk-pyth
[tool.poetry.dependencies]
python = "^3.8"
httpx = ">=0.21.2"
pydantic = ">= 1.9.2"
pydantic-core = ">=2.18.2"
pydantic = ">=2.7.1,<3"
typing_extensions = ">= 4.0.0"

[tool.poetry.group.dev.dependencies]
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 @@ -628,7 +628,7 @@ def with_advanced_features(self, features: AdvancedFeatures) -> "Agent":
return new_agent

def with_tools(self, enabled: bool = True) -> "Agent":
"""Returns a new Agent with MCP tool invocation enabled or disabled."""
"""Returns a new Agent with MCP and inline tool invocation enabled or disabled."""
new_agent = self._clone()
if new_agent._advanced_features is None:
new_agent._advanced_features = StartAgentsRequestPropertiesAdvancedFeatures(enable_tools=enabled)
Expand Down
17 changes: 13 additions & 4 deletions src/agora_agent/agentkit/vendors/cn.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .llm import (
_OPENAI_MANAGED_MODELS,
LlmGreetingConfigs,
LlmToolInput,
_dump_optional_model,
_ensure_mcp_transport,
)
Expand Down Expand Up @@ -48,6 +49,14 @@ class FengmingSTTOptions(BaseModel):
keywords: Optional[List[str]] = Field(default=None, description="Hotwords that improve ASR accuracy")
additional_params: Optional[Dict[str, Any]] = Field(default=None)

@model_validator(mode="after")
def _reject_nested_keywords(self) -> "FengmingSTTOptions":
if self.additional_params is not None and "keywords" in self.additional_params:
raise ValueError(
"FengmingSTT additional_params must not contain keywords; use the top-level keywords field"
)
return self


class FengmingSTT(FengmingSTTOptions, _BaseSTTCompat):
def to_config(self) -> Dict[str, Any]:
Expand Down Expand Up @@ -515,7 +524,7 @@ class AliyunLLMOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default="aliyun")
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")

@model_validator(mode="after")
Expand Down Expand Up @@ -602,7 +611,7 @@ class BytedanceLLMOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default="bytedance")
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")

@model_validator(mode="after")
Expand Down Expand Up @@ -689,7 +698,7 @@ class DeepSeekLLMOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default="deepseek")
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")

@model_validator(mode="after")
Expand Down Expand Up @@ -776,7 +785,7 @@ class TencentLLMOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default="tencent")
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")

@model_validator(mode="after")
Expand Down
26 changes: 16 additions & 10 deletions src/agora_agent/agentkit/vendors/llm.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union

from ...types.llm_tool import LlmTool
from .base import BaseLLM
from pydantic import BaseModel, ConfigDict, Field, model_validator

LlmGreetingConfigs = Dict[str, Any]
LlmToolInput = Union[Dict[str, Any], LlmTool]
_OPENAI_MANAGED_MODELS = {"gpt-4o-mini", "gpt-4.1-mini", "gpt-5-nano", "gpt-5-mini"}


Expand All @@ -23,6 +25,10 @@ def _dump_optional_model(value: Any) -> Any:
return value.model_dump(exclude_none=True)
if hasattr(value, "dict"):
return value.dict(exclude_none=True)
if isinstance(value, list):
return [_dump_optional_model(item) for item in value]
if isinstance(value, dict):
return {key: _dump_optional_model(item) for key, item in value.items()}
return value


Expand All @@ -47,7 +53,7 @@ class OpenAIOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")

@model_validator(mode="after")
Expand Down Expand Up @@ -139,7 +145,7 @@ class AzureOpenAIOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")


Expand Down Expand Up @@ -216,7 +222,7 @@ class AnthropicOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")


Expand Down Expand Up @@ -288,7 +294,7 @@ class GeminiOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")


Expand Down Expand Up @@ -364,7 +370,7 @@ class GroqOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")

@model_validator(mode="after")
Expand Down Expand Up @@ -443,7 +449,7 @@ class CustomLLMOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")

@model_validator(mode="after")
Expand Down Expand Up @@ -526,7 +532,7 @@ class VertexAILLMOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")


Expand Down Expand Up @@ -608,7 +614,7 @@ class AmazonBedrockOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0, description="Maximum number of conversation history messages to cache")


Expand Down Expand Up @@ -679,7 +685,7 @@ class DifyOptions(BaseModel):
template_variables: Optional[Dict[str, str]] = Field(default=None)
vendor: Optional[str] = Field(default=None)
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[Dict[str, Any]]] = Field(default=None)
tools: Optional[List[LlmToolInput]] = Field(default=None)
max_history: Optional[int] = Field(default=None, gt=0)


Expand Down
6 changes: 6 additions & 0 deletions src/agora_agent/agentkit/vendors/stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ class AresSTTOptions(BaseModel):
keywords: Optional[List[str]] = Field(default=None, description="Hotwords that improve ASR accuracy")
additional_params: Optional[Dict[str, Any]] = Field(default=None)

@model_validator(mode="after")
def _reject_nested_keywords(self) -> "AresSTTOptions":
if self.additional_params is not None and "keywords" in self.additional_params:
raise ValueError("AresSTT additional_params must not contain keywords; use the top-level keywords field")
return self


class AresSTT(AresSTTOptions, BaseSTT):
def to_config(self) -> Dict[str, Any]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class StartAgentsRequestPropertiesAdvancedFeatures(UncheckedBaseModel):

enable_tools: typing.Optional[bool] = pydantic.Field(default=None)
"""
Enable tool invocation. When enabled, the agent can invoke tools provided by the MCP server to implement advanced functionality.
Enable invocation for MCP servers and inline REST tools.
"""

if IS_PYDANTIC_V2:
Expand Down
9 changes: 9 additions & 0 deletions tests/custom/test_asr_keywords.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import pytest
from pydantic import ValidationError

from agora_agent import AresSTT, FengmingSTT


Expand All @@ -21,3 +24,9 @@ def test_fengming_keywords_are_top_level() -> None:
"keywords": ["Agora"],
"params": {"custom": True},
}


@pytest.mark.parametrize("vendor", [AresSTT, FengmingSTT])
def test_keywords_cannot_be_duplicated_in_vendor_params(vendor: type) -> None:
with pytest.raises(ValidationError, match="use the top-level keywords field"):
vendor(additional_params={"keywords": ["Agora"]})
40 changes: 39 additions & 1 deletion tests/custom/test_llm_tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from test_helpers import test_client

from agora_agent import AdvancedFeatures, Agent, OpenAI
from agora_agent import (
AdvancedFeatures,
Agent,
LlmToolConfig,
LlmToolFunctionConfig,
LlmToolFunctionParametersConfig,
LlmToolServerConfig,
OpenAI,
)
from agora_agent.agentkit.vendors.cn import AliyunLLM


Expand All @@ -15,6 +23,16 @@ def _tool() -> dict:
}


def _typed_tool() -> LlmToolConfig:
return LlmToolConfig(
function=LlmToolFunctionConfig(
name="lookup_order",
parameters=LlmToolFunctionParametersConfig(properties={}),
),
server=LlmToolServerConfig(method="GET", url="https://example.com/orders"),
)


def test_global_llm_tools_use_dict_shape_and_require_explicit_enablement() -> None:
tool = _tool()
vendor = OpenAI(
Expand Down Expand Up @@ -47,6 +65,26 @@ def test_cn_llm_uses_the_same_tools_shape_as_global_llm() -> None:
assert config["tools"] == [tool]


def test_global_and_cn_llms_accept_exported_typed_tools() -> None:
expected = [_tool()]

global_config = OpenAI(
api_key="openai-key",
base_url="https://api.openai.com/v1/chat/completions",
model="gpt-4o-mini",
tools=[_typed_tool()],
).to_config()
cn_config = AliyunLLM(
api_key="aliyun-key",
base_url="https://example.com/v1/chat/completions",
model="qwen-plus",
tools=[_typed_tool()],
).to_config()

assert global_config["tools"] == expected
assert cn_config["tools"] == expected


def test_with_tools_preserves_other_advanced_features() -> None:
agent = Agent(
test_client(), advanced_features=AdvancedFeatures(enable_sal=True)
Expand Down
Loading