Skip to content

Commit fecdc77

Browse files
Fix AgentKit request validation and provider wire-key coverage
1 parent 87585c5 commit fecdc77

8 files changed

Lines changed: 420 additions & 42 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Python AgentKit Snake Case API Audit
2+
3+
Scope: `agora-agents-python` public AgentKit wrappers, docs, and tests.
4+
5+
Search terms:
6+
7+
```bash
8+
rg -n "apiKey|baseUrl|modelId|voiceId|groupId|keyTerm|turnDetection|inputAudioTranscription|greetingMessage|failureMessage|projectId|adcCredentialsString|sampleRate|targetLanguageCode|resourceName|deploymentName" agora-agents-python
9+
```
10+
11+
## Result
12+
13+
No shipped camelCase public Python constructor kwargs were found in source or docs examples. No deprecated alias helper is required for this pass.
14+
15+
| File | Class / symbol | Public arg or example | Current spelling | Desired Python spelling | `to_config()` key | Wire key | Action | Compatibility needed | Test coverage |
16+
|---|---|---|---|---|---|---|---|---|---|
17+
| `src/agora_agent/agentkit/vendors/tts.py` | `GoogleTTS` | constructor arg | `voice_name` | `voice_name` | `params.VoiceSelectionParams` | `params.VoiceSelectionParams` | keep | no | `tests/custom/test_tts_vendors.py` |
18+
| `src/agora_agent/agentkit/vendors/tts.py` | `RimeTTS` | constructor arg | `model_id` | `model_id` | `params.modelId` | `params.modelId` | keep | no | `tests/custom/test_tts_vendors.py` |
19+
| `src/agora_agent/agentkit/vendors/tts.py` | `MurfTTS` | constructor arg | `voice_id` | `voice_id` | `params.voiceId` | `params.voiceId` | keep | no | `tests/custom/test_tts_vendors.py`, `tests/custom/test_request_body.py` |
20+
| `src/agora_agent/types/rime_tts_params.py` | generated model | generated alias | `modelId` | n/a | `model_id` | `modelId` | keep | no | `tests/custom/test_tts_vendors.py` |
21+
| `src/agora_agent/types/murf_tts_params.py` | generated model | generated alias | `voiceId` | n/a | `voice_id` | `voiceId` | keep | no | `tests/custom/test_tts_vendors.py` |
22+
| `tests/custom/test_request_body.py` | wire assertion | payload key | `voiceId` | n/a | `params.voiceId` | `params.voiceId` | keep | no | request-body test |
23+
| `tests/custom/test_tts_vendors.py` | wire assertion | payload key | `modelId`, `voiceId`, `VoiceSelectionParams` | n/a | generated model fields | wire aliases | keep | no | wire serialization test |
24+
25+
## Guardrail Added
26+
27+
`tests/custom/test_docs_snake_case.py` scans Python markdown code fences and fails on common camelCase kwargs such as `apiKey`, `baseUrl`, `modelId`, `voiceId`, `projectId`, and `greetingMessage`. JSON, TypeScript, Go, shell, and YAML examples are skipped so wire payload examples can retain required non-Python keys.

src/agora_agent/agentkit/agent.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
from ..agent_management.types.agent_think_agent_management_response import (
7777
AgentThinkAgentManagementResponse,
7878
)
79+
from ..core.pydantic_utilities import parse_obj_as
7980
from .vendors.base import BaseAvatar, BaseLLM, BaseMLLM, BaseSTT, BaseTTS
8081

8182
# Top-level aliases
@@ -188,6 +189,13 @@ class SessionOptions(typing_extensions.TypedDict, total=False):
188189
debug: bool
189190
warn: typing.Callable[[str], None]
190191

192+
193+
def _start_properties_from_mapping(
194+
properties: typing.Mapping[str, typing.Any],
195+
) -> StartAgentsRequestProperties:
196+
return parse_obj_as(StartAgentsRequestProperties, dict(properties))
197+
198+
191199
# LLM sub-type aliases
192200
LlmGreetingConfigs = typing.Dict[str, typing.Any]
193201
LlmGreetingConfigsMode = typing.Any
@@ -896,7 +904,7 @@ def to_properties(
896904
if self._failure_message is not None:
897905
mllm_config.setdefault("failure_message", self._failure_message)
898906
base_kwargs["mllm"] = mllm_config
899-
return StartAgentsRequestProperties(**base_kwargs)
907+
return _start_properties_from_mapping(base_kwargs)
900908

901909
if skip_vendor_validation:
902910
warnings.warn(
@@ -925,7 +933,7 @@ def to_properties(
925933
base_kwargs["turn_detection"] = turn_detection_config
926934

927935
if skip_vendor_validation:
928-
return StartAgentsRequestProperties(**base_kwargs)
936+
return _start_properties_from_mapping(base_kwargs)
929937

930938
if self._tts is None and not (skip_tts_validation or allow_missing_tts):
931939
raise ValueError("TTS configuration is required. Use with_tts() to set it.")
@@ -938,7 +946,7 @@ def to_properties(
938946
if self._tts is not None and not skip_tts_validation:
939947
base_kwargs["tts"] = self._tts
940948

941-
return StartAgentsRequestProperties(**base_kwargs)
949+
return _start_properties_from_mapping(base_kwargs)
942950

943951
def _resolve_llm_config(self) -> typing.Dict[str, typing.Any]:
944952
llm_config = dict(self._llm or {})

src/agora_agent/agentkit/agent_session.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
AgentThinkAgentManagementResponse as AgentThinkResponse,
1616
)
1717
from ..agents.types.get_turns_agents_response import GetTurnsAgentsResponse
18-
from ..agents.types.start_agents_request_properties import StartAgentsRequestProperties
19-
from .agent import Agent, GetTurnsOptions, SayOptions, ThinkOptions
18+
from .agent import Agent, GetTurnsOptions, SayOptions, ThinkOptions, _start_properties_from_mapping
2019
from .avatar_types import (
2120
is_akool_avatar,
2221
is_anam_avatar,
@@ -350,6 +349,44 @@ def _build_start_properties(
350349

351350
return properties
352351

352+
@staticmethod
353+
def _request_properties_for_start(
354+
resolved_properties: typing.Dict[str, typing.Any],
355+
*,
356+
resolved_preset: typing.Optional[str],
357+
pipeline_id: typing.Optional[str],
358+
) -> typing.Any:
359+
try:
360+
return _start_properties_from_mapping(resolved_properties)
361+
except Exception as exc:
362+
if pipeline_id:
363+
return resolved_properties
364+
if resolved_preset:
365+
preset_categories = {
366+
category
367+
for item in normalize_preset_input(resolved_preset).split(",")
368+
for category in [get_preset_category(item)]
369+
if category is not None
370+
}
371+
error_categories = _AgentSessionBase._validation_error_categories(exc)
372+
if error_categories and error_categories.issubset(preset_categories):
373+
return resolved_properties
374+
raise
375+
376+
@staticmethod
377+
def _validation_error_categories(exc: Exception) -> typing.Set[str]:
378+
errors = getattr(exc, "errors", None)
379+
if not callable(errors):
380+
return set()
381+
categories: typing.Set[str] = set()
382+
for error in errors():
383+
loc = error.get("loc") if isinstance(error, dict) else None
384+
if isinstance(loc, tuple) and loc:
385+
field = loc[0]
386+
if field in {"asr", "llm", "tts"}:
387+
categories.add(typing.cast(str, field))
388+
return categories
389+
353390
def _vendor_validation_categories(
354391
self,
355392
pipeline_id: typing.Optional[str],
@@ -514,10 +551,11 @@ def start(self) -> str:
514551
"properties": resolved_properties,
515552
})
516553

517-
try:
518-
request_properties: typing.Any = StartAgentsRequestProperties(**resolved_properties)
519-
except Exception:
520-
request_properties = resolved_properties
554+
request_properties = self._request_properties_for_start(
555+
resolved_properties,
556+
resolved_preset=resolved_preset,
557+
pipeline_id=pipeline_id,
558+
)
521559

522560
response = self._client.agents.start(
523561
self._app_id,
@@ -841,10 +879,11 @@ async def start(self) -> str:
841879
"properties": resolved_properties,
842880
})
843881

844-
try:
845-
request_properties: typing.Any = StartAgentsRequestProperties(**resolved_properties)
846-
except Exception:
847-
request_properties = resolved_properties
882+
request_properties = self._request_properties_for_start(
883+
resolved_properties,
884+
resolved_preset=resolved_preset,
885+
pipeline_id=pipeline_id,
886+
)
848887

849888
response = await self._client.agents.start(
850889
self._app_id,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
6+
7+
ROOT = Path(__file__).resolve().parents[2]
8+
9+
SCANNED_MARKDOWN = [
10+
ROOT / "README.md",
11+
*sorted((ROOT / "docs").rglob("*.md")),
12+
]
13+
14+
SKIP_LANGS = {
15+
"bash",
16+
"console",
17+
"go",
18+
"javascript",
19+
"js",
20+
"json",
21+
"shell",
22+
"sh",
23+
"text",
24+
"ts",
25+
"typescript",
26+
"yaml",
27+
"yml",
28+
}
29+
30+
PYTHON_HINTS = (
31+
"from agora_agent",
32+
"import agora_agent",
33+
"Agent(",
34+
"OpenAI(",
35+
"OpenAITTS(",
36+
"OpenAISTT(",
37+
"MiniMaxTTS(",
38+
"DeepgramSTT(",
39+
"GoogleTTS(",
40+
"RimeTTS(",
41+
"VertexAI(",
42+
"VertexAILLM(",
43+
)
44+
45+
BLOCKED_TERMS = {
46+
"apiKey": "api_key",
47+
"baseUrl": "base_url",
48+
"modelId": "model_id",
49+
"voiceId": "voice_id",
50+
"groupId": "group_id",
51+
"projectId": "project_id",
52+
"resourceName": "resource_name",
53+
"deploymentName": "deployment_name",
54+
"inputAudioTranscription": "input_audio_transcription",
55+
"greetingMessage": "greeting_message",
56+
"failureMessage": "failure_message",
57+
"turnDetection": "turn_detection",
58+
"adcCredentialsString": "adc_credentials_string",
59+
"sampleRate": "sample_rate",
60+
"targetLanguageCode": "target_language_code",
61+
}
62+
63+
FENCE_RE = re.compile(r"^```(?P<lang>[^\n`]*)\n(?P<body>.*?)(?:^```)", re.MULTILINE | re.DOTALL)
64+
65+
66+
def _should_scan(lang: str, body: str) -> bool:
67+
lang_parts = lang.strip().split(maxsplit=1)
68+
normalized = lang_parts[0].lower() if lang_parts else ""
69+
if normalized in {"python", "py"}:
70+
return True
71+
if normalized in SKIP_LANGS:
72+
return False
73+
if normalized:
74+
return False
75+
return any(hint in body for hint in PYTHON_HINTS)
76+
77+
78+
def test_python_docs_examples_use_snake_case_kwargs() -> None:
79+
failures: list[str] = []
80+
81+
for path in SCANNED_MARKDOWN:
82+
text = path.read_text()
83+
for match in FENCE_RE.finditer(text):
84+
body = match.group("body")
85+
if not _should_scan(match.group("lang"), body):
86+
continue
87+
88+
line_offset = text[: match.start("body")].count("\n")
89+
for term, replacement in BLOCKED_TERMS.items():
90+
for term_match in re.finditer(rf"\b{re.escape(term)}\b", body):
91+
line = line_offset + body[: term_match.start()].count("\n") + 1
92+
failures.append(f"{path.relative_to(ROOT)}:{line}: use {replacement} instead of {term}")
93+
94+
assert not failures, "CamelCase kwargs found in Python docs examples:\n" + "\n".join(failures)

tests/custom/test_llm_vendors.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ def test_vertex_ai_llm_includes_project_routing() -> None:
6363
assert config["api_key"] == "vertex-token"
6464
assert config["style"] == "gemini"
6565
assert config["params"]["model"] == "gemini-2.0-flash"
66-
assert config["params"]["project_id"] == "project"
67-
assert config["params"]["location"] == "us-central1"
66+
assert "project" in config["url"]
67+
assert "us-central1" in config["url"]
68+
assert "project_id" not in config.get("params", {})
69+
assert "location" not in config.get("params", {})
6870

6971

7072
def test_amazon_bedrock_serializes_as_bedrock_style() -> None:

0 commit comments

Comments
 (0)