Skip to content

Commit e5ec085

Browse files
yulin-liYulin LiCopilot
authored andcommitted
Convert BYO invocations_ws samples to path-based WebSocket routing (#563)
The hosted-agent WebSocket data-plane route now carries the project and agent as URL path segments (mirroring the HTTP /invocations route) instead of query parameters: before: /api/projects/agents/endpoint/protocols/invocations_ws?project_name=P&agent_name=A after: /api/projects/P/agents/A/endpoint/protocols/invocations_ws - Python (hello-world, duplex-live-agent, pipecat-ws-server, pipecat-webrtc, livekit-server): move project/agent into the URL path (urllib quote), drop project_name/agent_name query params; keep api-version & agent_session_id. - C# (HelloWorld proxy + e2e): same, using Uri.EscapeDataString. - READMEs updated to document the path-based route. - Local container /invocations_ws routes are unchanged. Co-authored-by: Yulin Li <yulili@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c2c7a6c commit e5ec085

13 files changed

Lines changed: 65 additions & 54 deletions

File tree

samples/csharp/hosted-agents/bring-your-own/invocations_ws/HelloWorld/E2ELocal/Program.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,11 @@ static string BuildFoundryUrl(string projectEndpoint, string agent, string sessi
254254
var parts = new Uri(projectEndpoint);
255255
var project = parts.AbsolutePath.TrimEnd('/').Split('/')[^1];
256256
var qs = HttpUtility.ParseQueryString(string.Empty);
257-
qs["project_name"] = project;
258-
qs["agent_name"] = agent;
259257
qs["api-version"] = apiVersion;
260258
qs["agent_session_id"] = sessionId;
261259
var scheme = parts.Scheme is "https" or "wss" ? "wss" : "ws";
262-
return $"{scheme}://{parts.Host}/api/projects/agents/endpoint/protocols/invocations_ws?{qs}";
260+
var path = $"/api/projects/{Uri.EscapeDataString(project)}/agents/{Uri.EscapeDataString(agent)}/endpoint/protocols/invocations_ws";
261+
return $"{scheme}://{parts.Host}{path}?{qs}";
263262
}
264263

265264
static async Task<string> GetEntraTokenAsync(string resource = "https://ai.azure.com")

samples/csharp/hosted-agents/bring-your-own/invocations_ws/HelloWorld/README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,8 @@ data-plane** WebSocket URL (the proxy and `E2ELocal` build this for
205205
you from `--foundry` + `--agent`):
206206

207207
```
208-
wss://<account>.services.ai.azure.com/api/projects/agents/endpoint/protocols/invocations_ws
208+
wss://<account>.services.ai.azure.com/api/projects/<project>/agents/<agent>/endpoint/protocols/invocations_ws
209209
?api-version=v1
210-
&project_name=<project>
211-
&agent_name=<agent>
212210
&agent_session_id=<unique-session-id>
213211
```
214212

@@ -217,10 +215,10 @@ Where the segments come from:
217215
| Part | Value |
218216
|------|-------|
219217
| `<account>` | AI Services account host — the same host as your Foundry project endpoint (`https://<account>.services.ai.azure.com/api/projects/<project>`). |
220-
| `/api/projects/agents/endpoint/protocols/invocations_ws` | Fixed data-plane route`agents` and `endpoint` are literal path segments, not your agent name. The actual agent is picked via the `agent_name` query parameter. |
218+
| `/api/projects/<project>/agents/<agent>/endpoint/protocols/invocations_ws` | Data-plane route; project and agent are URL-encoded path segments. |
221219
| `api-version=v1` | Foundry data-plane API version. |
222-
| `project_name=<project>` | The last segment of your project endpoint path. |
223-
| `agent_name=<agent>` | Matches the agent `name` in [`agent.manifest.yaml`](agent.manifest.yaml)`hello-world-dotnet-invocations-ws`. |
220+
| `<project>` | The last segment of your project endpoint path. |
221+
| `<agent>` | Matches the agent `name` in [`agent.manifest.yaml`](agent.manifest.yaml)`hello-world-dotnet-invocations-ws`. |
224222
| `agent_session_id=<unique-session-id>` | A caller-generated string that identifies the conversation. Reuse the same id to resume; use a fresh one (e.g. a GUID) to start a new session. |
225223

226224
Every request must also include `Authorization: Bearer <Entra token>`

samples/csharp/hosted-agents/bring-your-own/invocations_ws/HelloWorld/chat_client/Proxy/Program.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,11 @@ static string BuildFoundryUrl(string projectEndpoint, string agent, string sessi
207207
var parts = new Uri(projectEndpoint);
208208
var project = parts.AbsolutePath.TrimEnd('/').Split('/')[^1];
209209
var qs = HttpUtility.ParseQueryString(string.Empty);
210-
qs["project_name"] = project;
211-
qs["agent_name"] = agent;
212210
qs["api-version"] = apiVersion;
213211
qs["agent_session_id"] = sessionId;
214212
var scheme = parts.Scheme is "https" or "wss" ? "wss" : "ws";
215-
return $"{scheme}://{parts.Host}/api/projects/agents/endpoint/protocols/invocations_ws?{qs}";
213+
var path = $"/api/projects/{Uri.EscapeDataString(project)}/agents/{Uri.EscapeDataString(agent)}/endpoint/protocols/invocations_ws";
214+
return $"{scheme}://{parts.Host}{path}?{qs}";
216215
}
217216

218217
static async Task<string> GetEntraTokenAsync(string resource)

samples/python/hosted-agents/bring-your-own/invocations_ws/duplex-live-agent/chat_client/proxy.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
import sys
5353
import uuid
5454
from http import HTTPStatus
55-
from urllib.parse import urlencode, urlsplit, urlunsplit
55+
from urllib.parse import quote, urlencode, urlsplit, urlunsplit
5656

5757
import websockets
5858
from websockets.asyncio.server import serve
@@ -66,15 +66,18 @@ def _foundry_url(project_endpoint: str, agent: str, session_id: str, api_version
6666
parts = urlsplit(project_endpoint)
6767
project = parts.path.rstrip("/").rsplit("/", 1)[-1]
6868
qs = urlencode({
69-
"project_name": project,
70-
"agent_name": agent,
7169
"api-version": api_version,
7270
"agent_session_id": session_id,
7371
})
72+
path = (
73+
f"/api/projects/{quote(project, safe='')}"
74+
f"/agents/{quote(agent, safe='')}"
75+
"/endpoint/protocols/invocations_ws"
76+
)
7477
return urlunsplit((
7578
"wss" if parts.scheme in ("https", "wss") else "ws",
7679
parts.netloc,
77-
"/api/projects/agents/endpoint/protocols/invocations_ws",
80+
path,
7881
qs, "",
7982
))
8083

samples/python/hosted-agents/bring-your-own/invocations_ws/duplex-live-agent/e2e_local.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import subprocess
3131
import sys
3232
import time
33-
from urllib.parse import urlencode, urlsplit, urlunsplit
33+
from urllib.parse import quote, urlencode, urlsplit, urlunsplit
3434

3535
import websockets
3636

@@ -39,15 +39,18 @@ def _foundry_url(project_endpoint: str, agent: str, session_id: str, api_version
3939
parts = urlsplit(project_endpoint)
4040
project = parts.path.rstrip("/").rsplit("/", 1)[-1]
4141
qs = urlencode({
42-
"project_name": project,
43-
"agent_name": agent,
4442
"api-version": api_version,
4543
"agent_session_id": session_id,
4644
})
45+
path = (
46+
f"/api/projects/{quote(project, safe='')}"
47+
f"/agents/{quote(agent, safe='')}"
48+
"/endpoint/protocols/invocations_ws"
49+
)
4750
return urlunsplit((
4851
"wss" if parts.scheme in ("https", "wss") else "ws",
4952
parts.netloc,
50-
"/api/projects/agents/endpoint/protocols/invocations_ws",
53+
path,
5154
qs, "",
5255
))
5356

samples/python/hosted-agents/bring-your-own/invocations_ws/hello-world/README.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,8 @@ data-plane** WebSocket URL (`e2e_local.py` builds this for you from
197197
`--foundry` + `--agent`):
198198

199199
```
200-
wss://<account>.services.ai.azure.com/api/projects/agents/endpoint/protocols/invocations_ws
200+
wss://<account>.services.ai.azure.com/api/projects/<project>/agents/<agent>/endpoint/protocols/invocations_ws
201201
?api-version=v1
202-
&project_name=<project>
203-
&agent_name=<agent>
204202
&agent_session_id=<unique-session-id>
205203
```
206204

@@ -209,10 +207,10 @@ Where the segments come from:
209207
| Part | Value |
210208
|------|-------|
211209
| `<account>` | AI Services account host — the same host as your Foundry project endpoint (`https://<account>.services.ai.azure.com/api/projects/<project>`). |
212-
| `/api/projects/agents/endpoint/protocols/invocations_ws` | Fixed data-plane route`agents` and `endpoint` are literal path segments, not your agent name. The actual agent is picked via the `agent_name` query parameter. |
210+
| `/api/projects/<project>/agents/<agent>/endpoint/protocols/invocations_ws` | Data-plane route; project and agent are URL-encoded path segments. |
213211
| `api-version=v1` | Foundry data-plane API version. |
214-
| `project_name=<project>` | The last segment of your project endpoint path. |
215-
| `agent_name=<agent>` | Matches the agent `name` in [`agent.manifest.yaml`](agent.manifest.yaml)`hello-world`. |
212+
| `<project>` | The last segment of your project endpoint path. |
213+
| `<agent>` | Matches the agent `name` in [`agent.manifest.yaml`](agent.manifest.yaml)`hello-world`. |
216214
| `agent_session_id=<unique-session-id>` | A caller-generated string that identifies the conversation. Reuse the same id to resume; use a fresh one (e.g. a UUID) to start a new session. |
217215

218216
Every request must also include `Authorization: Bearer <Entra token>`

samples/python/hosted-agents/bring-your-own/invocations_ws/hello-world/chat_client/proxy.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
import sys
5353
import uuid
5454
from http import HTTPStatus
55-
from urllib.parse import urlencode, urlsplit, urlunsplit
55+
from urllib.parse import quote, urlencode, urlsplit, urlunsplit
5656

5757
import websockets
5858
from websockets.asyncio.server import serve
@@ -66,15 +66,18 @@ def _foundry_url(project_endpoint: str, agent: str, session_id: str, api_version
6666
parts = urlsplit(project_endpoint)
6767
project = parts.path.rstrip("/").rsplit("/", 1)[-1]
6868
qs = urlencode({
69-
"project_name": project,
70-
"agent_name": agent,
7169
"api-version": api_version,
7270
"agent_session_id": session_id,
7371
})
72+
path = (
73+
f"/api/projects/{quote(project, safe='')}"
74+
f"/agents/{quote(agent, safe='')}"
75+
"/endpoint/protocols/invocations_ws"
76+
)
7477
return urlunsplit((
7578
"wss" if parts.scheme in ("https", "wss") else "ws",
7679
parts.netloc,
77-
"/api/projects/agents/endpoint/protocols/invocations_ws",
80+
path,
7881
qs, "",
7982
))
8083

samples/python/hosted-agents/bring-your-own/invocations_ws/hello-world/e2e_local.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import subprocess
3131
import sys
3232
import time
33-
from urllib.parse import urlencode, urlsplit, urlunsplit
33+
from urllib.parse import quote, urlencode, urlsplit, urlunsplit
3434

3535
import websockets
3636

@@ -39,15 +39,18 @@ def _foundry_url(project_endpoint: str, agent: str, session_id: str, api_version
3939
parts = urlsplit(project_endpoint)
4040
project = parts.path.rstrip("/").rsplit("/", 1)[-1]
4141
qs = urlencode({
42-
"project_name": project,
43-
"agent_name": agent,
4442
"api-version": api_version,
4543
"agent_session_id": session_id,
4644
})
45+
path = (
46+
f"/api/projects/{quote(project, safe='')}"
47+
f"/agents/{quote(agent, safe='')}"
48+
"/endpoint/protocols/invocations_ws"
49+
)
4750
return urlunsplit((
4851
"wss" if parts.scheme in ("https", "wss") else "ws",
4952
parts.netloc,
50-
"/api/projects/agents/endpoint/protocols/invocations_ws",
53+
path,
5154
qs, "",
5255
))
5356

samples/python/hosted-agents/bring-your-own/invocations_ws/livekit-server/chat_client/upstream.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import os
1414
import subprocess
1515
import time
16-
from urllib.parse import urlencode, urlsplit, urlunsplit
16+
from urllib.parse import quote, urlencode, urlsplit, urlunsplit
1717

1818

1919
logger = logging.getLogger("chat-client.upstream")
@@ -41,15 +41,18 @@ def _build_url(agent_name: str, session_id: str) -> str:
4141
scheme = "wss" if parts.scheme in ("https", "wss") else "ws"
4242
project = parts.path.rstrip("/").rsplit("/", 1)[-1]
4343
qs = urlencode({
44-
"project_name": project,
45-
"agent_name": agent_name,
4644
"api-version": _api_version(),
4745
"agent_session_id": session_id,
4846
})
47+
path = (
48+
f"/api/projects/{quote(project, safe='')}"
49+
f"/agents/{quote(agent_name, safe='')}"
50+
"/endpoint/protocols/invocations_ws"
51+
)
4952
return urlunsplit((
5053
scheme,
5154
parts.netloc,
52-
"/api/projects/agents/endpoint/protocols/invocations_ws",
55+
path,
5356
qs,
5457
"",
5558
))

samples/python/hosted-agents/bring-your-own/invocations_ws/pipecat-webrtc/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,8 @@ The portal builds the upstream URL as:
146146

147147
```
148148
wss://{account}.services.ai.azure.com
149-
/api/projects/agents/endpoint/protocols/invocations_ws
150-
?project_name={project}
151-
&agent_name={PIPECAT_WEBRTC_AGENT_NAME}
152-
&api-version={API_VERSION}
149+
/api/projects/{project}/agents/{PIPECAT_WEBRTC_AGENT_NAME}/endpoint/protocols/invocations_ws
150+
?api-version={API_VERSION}
153151
&agent_session_id={generated-per-connection}
154152
```
155153

0 commit comments

Comments
 (0)