Skip to content

Commit 617ee13

Browse files
feat(agentkit): support agent-level pipeline_id
1 parent 583eccc commit 617ee13

6 files changed

Lines changed: 202 additions & 9 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,38 @@ def start_conversation() -> str:
9696

9797
`Agora` generates the required ConvoAI REST auth and RTC join tokens automatically when you provide `app_id` and `app_certificate`. For supported Agora-managed models, leave vendor API keys unset; provide keys when you want BYOK.
9898

99+
## AI Studio pipeline IDs
100+
101+
Use `pipeline_id` when you want a published AI Studio pipeline to provide the base agent configuration:
102+
103+
```python
104+
agent = Agent(
105+
name="support",
106+
pipeline_id="studio-pipeline-id",
107+
)
108+
109+
session = agent.create_session(
110+
client,
111+
channel="support-room",
112+
agent_uid="1",
113+
remote_uids=["100"],
114+
)
115+
```
116+
117+
You can override it per session:
118+
119+
```python
120+
session = agent.create_session(
121+
client,
122+
channel="support-room",
123+
agent_uid="1",
124+
remote_uids=["100"],
125+
pipeline_id="session-pipeline-id",
126+
)
127+
```
128+
129+
AgentKit sends the resolved value as the top-level `/join` field `pipeline_id`, not inside `properties`. Explicit Agent config such as `with_llm()`, `with_tts()`, `with_stt()`, `with_mllm()`, and `advanced_features` may send `properties` fields that override the saved pipeline settings.
130+
99131
### BYOK version
100132

101133
Use the same `Agent` builder shape, but provide credentials explicitly when you want vendor-managed billing and routing instead of Agora-managed models.

docs/reference/agent.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ Agent(
2727
labels: Optional[Dict[str, str]] = None,
2828
rtc: Optional[RtcConfig] = None,
2929
filler_words: Optional[FillerWordsConfig] = None,
30+
pipeline_id: Optional[str] = None,
3031
)
3132
```
3233

3334
| Parameter | Type | Default | Description |
3435
|---|---|---|---|
3536
| `name` | `Optional[str]` | `None` | Agent name, used as default session name |
37+
| `pipeline_id` | `Optional[str]` | `None` | Published AI Studio pipeline ID used as this agent's base configuration |
3638
| `instructions` | `Optional[str]` | `None` | Deprecated. Use LLM vendor `system_messages` instead. |
3739
| `turn_detection` | `Optional[TurnDetectionConfig]` | `None` | Interaction language and turn detection configuration |
3840
| `interruption` | `Optional[InterruptionConfig]` | `None` | Unified interruption control configuration |
@@ -47,6 +49,8 @@ Agent(
4749
| `rtc` | `Optional[RtcConfig]` | `None` | RTC media encryption |
4850
| `filler_words` | `Optional[FillerWordsConfig]` | `None` | Filler words while waiting for LLM |
4951

52+
`pipeline_id` is an AI Studio base configuration. Explicit Agent config such as `with_llm()`, `with_tts()`, `with_stt()`, `with_mllm()`, `advanced_features`, and other builder options may send fields in `properties` that override the saved pipeline settings. Session-level `pipeline_id` overrides the agent-level value.
53+
5054
The Agent-level `instructions`, `greeting`, `failure_message`, `max_history`, and `greeting_configs` fields are compatibility shims. New code should configure those values on the LLM or MLLM vendor because that matches the core request schema.
5155

5256
## Builder Methods
@@ -202,6 +206,8 @@ create_session(
202206
token: Optional[str] = None,
203207
idle_timeout: Optional[int] = None,
204208
enable_string_uid: Optional[bool] = None,
209+
preset: Optional[Union[str, Sequence[str]]] = None,
210+
pipeline_id: Optional[str] = None,
205211
expires_in: Optional[int] = None,
206212
) -> AgentSession
207213
```
@@ -219,6 +225,10 @@ Creates an `AgentSession` bound to the given client and channel.
219225
| `expires_in` | `Optional[int]` | No | Token lifetime in seconds (default: `86400` = 24 h, Agora max). Only applies when the token is auto-generated. Use `expires_in_hours()` or `expires_in_minutes()` for clarity. Valid range: 1–86400. |
220226
| `idle_timeout` | `Optional[int]` | No | Idle timeout in seconds |
221227
| `enable_string_uid` | `Optional[bool]` | No | Enable string UIDs |
228+
| `preset` | `Optional[Union[str, Sequence[str]]]` | No | Advanced preset value for project-specific routing |
229+
| `pipeline_id` | `Optional[str]` | No | Published AI Studio pipeline ID for this session. Overrides `agent.pipeline_id`. |
230+
231+
`pipeline_id` is sent as the top-level `/join` field `pipeline_id`, not inside `properties`.
222232

223233
**Returns:** `AgentSession`
224234

docs/reference/session.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ AgentSession(
3333
token: Optional[str] = None,
3434
idle_timeout: Optional[int] = None,
3535
enable_string_uid: Optional[bool] = None,
36+
preset: Optional[Union[str, Sequence[str]]] = None,
37+
pipeline_id: Optional[str] = None,
38+
expires_in: Optional[int] = None,
39+
debug: Optional[bool] = None,
40+
warn: Optional[Callable[[str], None]] = None,
3641
)
3742
```
3843

@@ -51,6 +56,13 @@ AgentSession(
5156
| `token` | `Optional[str]` | No | Pre-built RTC token |
5257
| `idle_timeout` | `Optional[int]` | No | Idle timeout in seconds |
5358
| `enable_string_uid` | `Optional[bool]` | No | Enable string UIDs |
59+
| `preset` | `Optional[Union[str, Sequence[str]]]` | No | Advanced preset value for project-specific routing |
60+
| `pipeline_id` | `Optional[str]` | No | Published AI Studio pipeline ID for this session. Overrides `agent.pipeline_id`. |
61+
| `expires_in` | `Optional[int]` | No | Auto-generated token lifetime in seconds |
62+
| `debug` | `Optional[bool]` | No | Enable debug logging of the start request |
63+
| `warn` | `Optional[Callable[[str], None]]` | No | Custom warning sink |
64+
65+
`pipeline_id` is sent as the top-level `/join` field `pipeline_id`, not inside `properties`. If unset, `AgentSession.start()` uses the agent-level value from `Agent(..., pipeline_id=...)`.
5466

5567
## Methods
5668

src/agora_agent/agentkit/agent.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,10 @@ def __init__(
343343
rtc: typing.Optional[RtcConfig] = None,
344344
filler_words: typing.Optional[FillerWordsConfig] = None,
345345
greeting_configs: typing.Optional[LlmGreetingConfigs] = None,
346+
pipeline_id: typing.Optional[str] = None,
346347
):
347348
self._name = name
349+
self._pipeline_id = pipeline_id
348350
self._instructions = instructions
349351
self._greeting = greeting
350352
self._failure_message = failure_message
@@ -609,6 +611,11 @@ def _resolved_parameters(self) -> typing.Optional[typing.Union[SessionParams, Se
609611
def name(self) -> typing.Optional[str]:
610612
return self._name
611613

614+
@property
615+
def pipeline_id(self) -> typing.Optional[str]:
616+
"""Published AI Studio pipeline ID used as this agent's base configuration."""
617+
return self._pipeline_id
618+
612619
@property
613620
def llm(self) -> typing.Optional[typing.Dict[str, typing.Any]]:
614621
return self._llm
@@ -693,6 +700,7 @@ def filler_words(self) -> typing.Optional[FillerWordsConfig]:
693700
def config(self) -> typing.Dict[str, typing.Any]:
694701
return {
695702
"name": self._name,
703+
"pipeline_id": self._pipeline_id,
696704
"instructions": self._instructions,
697705
"greeting": self._greeting,
698706
"failure_message": self._failure_message,
@@ -945,6 +953,7 @@ def _resolve_turn_detection_config(self) -> TurnDetectionConfig:
945953
def _clone(self) -> "Agent":
946954
new_agent = Agent.__new__(Agent)
947955
new_agent._name = self._name
956+
new_agent._pipeline_id = self._pipeline_id
948957
new_agent._llm = self._llm
949958
new_agent._tts = self._tts
950959
new_agent._stt = self._stt

src/agora_agent/agentkit/agent_session.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class AgentSessionOptions(_AgentSessionRequiredOptions, total=False):
5252
5353
Optional fields
5454
---------------
55-
app_certificate, token, idle_timeout, enable_string_uid, expires_in
55+
app_certificate, token, idle_timeout, enable_string_uid, preset,
56+
pipeline_id, expires_in, debug, warn
5657
"""
5758

5859
app_certificate: str
@@ -290,14 +291,18 @@ def _is_mllm_mode(self) -> bool:
290291
return True
291292
return mllm is not None
292293

293-
def _build_start_properties(self, token_opts: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
294+
def _build_start_properties(
295+
self,
296+
token_opts: typing.Dict[str, typing.Any],
297+
skip_vendor_validation: bool,
298+
) -> typing.Dict[str, typing.Any]:
294299
base_properties = self._agent.to_properties(
295300
channel=self._channel,
296301
agent_uid=self._agent_uid,
297302
remote_uids=self._remote_uids,
298303
idle_timeout=self._idle_timeout,
299304
enable_string_uid=self._enable_string_uid,
300-
skip_vendor_validation=True,
305+
skip_vendor_validation=skip_vendor_validation,
301306
**token_opts,
302307
)
303308
properties = self._dump_model(base_properties)
@@ -445,6 +450,7 @@ def start(self) -> str:
445450
self._status = "starting"
446451

447452
try:
453+
pipeline_id = self._pipeline_id if self._pipeline_id is not None else self._agent.pipeline_id
448454
if self._token:
449455
token_opts: typing.Dict[str, typing.Any] = {"token": self._token}
450456
else:
@@ -454,7 +460,7 @@ def start(self) -> str:
454460
"expires_in": self._expires_in,
455461
}
456462

457-
properties = self._build_start_properties(token_opts)
463+
properties = self._build_start_properties(token_opts, skip_vendor_validation=bool(self._preset or pipeline_id))
458464
resolved_preset, resolved_properties = resolve_session_presets(
459465
self._preset,
460466
properties,
@@ -466,7 +472,7 @@ def start(self) -> str:
466472
"appid": self._app_id,
467473
"name": self._name,
468474
"preset": resolved_preset,
469-
"pipeline_id": self._pipeline_id,
475+
"pipeline_id": pipeline_id,
470476
"properties": resolved_properties,
471477
})
472478

@@ -480,7 +486,7 @@ def start(self) -> str:
480486
name=self._name,
481487
properties=request_properties,
482488
preset=resolved_preset,
483-
pipeline_id=self._pipeline_id,
489+
pipeline_id=pipeline_id,
484490
request_options=self._request_options(),
485491
)
486492

@@ -766,6 +772,7 @@ async def start(self) -> str:
766772
self._status = "starting"
767773

768774
try:
775+
pipeline_id = self._pipeline_id if self._pipeline_id is not None else self._agent.pipeline_id
769776
if self._token:
770777
token_opts: typing.Dict[str, typing.Any] = {"token": self._token}
771778
else:
@@ -775,7 +782,7 @@ async def start(self) -> str:
775782
"expires_in": self._expires_in,
776783
}
777784

778-
properties = self._build_start_properties(token_opts)
785+
properties = self._build_start_properties(token_opts, skip_vendor_validation=bool(self._preset or pipeline_id))
779786
resolved_preset, resolved_properties = resolve_session_presets(
780787
self._preset,
781788
properties,
@@ -787,7 +794,7 @@ async def start(self) -> str:
787794
"appid": self._app_id,
788795
"name": self._name,
789796
"preset": resolved_preset,
790-
"pipeline_id": self._pipeline_id,
797+
"pipeline_id": pipeline_id,
791798
"properties": resolved_properties,
792799
})
793800

@@ -801,7 +808,7 @@ async def start(self) -> str:
801808
name=self._name,
802809
properties=request_properties,
803810
preset=resolved_preset,
804-
pipeline_id=self._pipeline_id,
811+
pipeline_id=pipeline_id,
805812
request_options=self._request_options(),
806813
)
807814

tests/custom/test_pipeline_id.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import pytest
2+
3+
from agora_agent import Agent
4+
5+
6+
def dump(value):
7+
if hasattr(value, "model_dump"):
8+
return value.model_dump(exclude_none=True)
9+
if hasattr(value, "dict"):
10+
return value.dict(exclude_none=True)
11+
return value
12+
13+
14+
class StartResponse:
15+
agent_id = "agent-id"
16+
17+
18+
class FakeAgentsClient:
19+
def __init__(self):
20+
self.calls = []
21+
22+
def start(self, appid, **kwargs):
23+
self.calls.append({"appid": appid, **kwargs})
24+
return StartResponse()
25+
26+
27+
class FakeAsyncAgentsClient:
28+
def __init__(self):
29+
self.calls = []
30+
31+
async def start(self, appid, **kwargs):
32+
self.calls.append({"appid": appid, **kwargs})
33+
return StartResponse()
34+
35+
36+
class FakeClient:
37+
app_id = "appid"
38+
app_certificate = None
39+
40+
def __init__(self, agents):
41+
self.agents = agents
42+
43+
44+
def start_agent(agent, **overrides):
45+
agents = FakeAgentsClient()
46+
client = FakeClient(agents)
47+
options = {
48+
"channel": "channel",
49+
"token": "token",
50+
"agent_uid": "1",
51+
"remote_uids": ["100"],
52+
**overrides,
53+
}
54+
55+
agent_id = agent.create_session(client, **options).start()
56+
57+
assert agent_id == "agent-id"
58+
assert len(agents.calls) == 1
59+
return agents.calls[0]
60+
61+
62+
def test_agent_pipeline_id_sends_top_level_pipeline_id() -> None:
63+
call = start_agent(Agent(name="support", pipeline_id="studio-pipeline-id"))
64+
65+
assert call["appid"] == "appid"
66+
assert call["name"] == "support"
67+
assert call["pipeline_id"] == "studio-pipeline-id"
68+
properties = dump(call["properties"])
69+
assert properties["channel"] == "channel"
70+
assert properties["token"] == "token"
71+
assert properties["agent_rtc_uid"] == "1"
72+
assert properties["remote_rtc_uids"] == ["100"]
73+
74+
75+
def test_session_pipeline_id_overrides_agent_pipeline_id() -> None:
76+
call = start_agent(
77+
Agent(name="support", pipeline_id="agent-pipeline"),
78+
pipeline_id="session-pipeline",
79+
)
80+
81+
assert call["pipeline_id"] == "session-pipeline"
82+
83+
84+
def test_agent_pipeline_id_skips_missing_vendor_validation() -> None:
85+
call = start_agent(Agent(name="support", pipeline_id="studio-pipeline-id"))
86+
87+
assert call["pipeline_id"] == "studio-pipeline-id"
88+
89+
90+
def test_pipeline_id_is_not_sent_inside_properties() -> None:
91+
call = start_agent(Agent(name="support", pipeline_id="studio-pipeline-id"))
92+
93+
assert call["pipeline_id"] == "studio-pipeline-id"
94+
assert "pipeline_id" not in dump(call["properties"])
95+
96+
97+
def test_pipeline_id_survives_builder_clone() -> None:
98+
agent = Agent(name="support", pipeline_id="studio-pipeline-id").with_tools(True)
99+
100+
assert agent.pipeline_id == "studio-pipeline-id"
101+
call = start_agent(agent)
102+
103+
assert call["pipeline_id"] == "studio-pipeline-id"
104+
assert dump(call["properties"])["advanced_features"] == {"enable_tools": True}
105+
106+
107+
@pytest.mark.asyncio
108+
async def test_async_session_uses_agent_pipeline_id() -> None:
109+
agents = FakeAsyncAgentsClient()
110+
client = FakeClient(agents)
111+
agent = Agent(name="support", pipeline_id="studio-pipeline-id")
112+
113+
agent_id = await agent.create_async_session(
114+
client,
115+
channel="channel",
116+
token="token",
117+
agent_uid="1",
118+
remote_uids=["100"],
119+
).start()
120+
121+
assert agent_id == "agent-id"
122+
assert agents.calls[0]["pipeline_id"] == "studio-pipeline-id"
123+
assert "pipeline_id" not in dump(agents.calls[0]["properties"])

0 commit comments

Comments
 (0)