Skip to content

Commit 5839585

Browse files
yulin-liYulin LiCopilot
authored andcommitted
Add C# Voice Live hello-world invocations_ws sample; update Python sample (#381)
* Add C# Voice Live hello-world invocations_ws sample; update Python sample New C# sample at samples/csharp/hosted-agents/bring-your-own/invocations_ws/ HelloWorld/ — minimal realtime voice agent that bridges browser PCM16 ↔ Azure Voice Live, parity with the existing Python sample: * HelloWorld/ production WebSocket handler (Invocations SDK + Azure.AI.VoiceLive 1.1.0-beta.4) * E2ELocal/ CLI E2E client (sends test audio, asserts session_started + response_done) * chat_client/Proxy/ dev-only ASP.NET proxy that serves index.html on :8765 and forwards the browser WS to the agent, signing each upstream connection with a Bearer token from DefaultAzureCredential Python sample (samples/python/.../invocations_ws/hello-world/): * azure-ai-voicelive bumped to >=1.2.0 (latest SDK) * .env.example: added commented APPLICATIONINSIGHTS_CONNECTION_STRING * agent.manifest.yaml: memory 1Gi → 2Gi (matches a valid Foundry resource tier; (1 CPU, 1Gi) is not accepted by the deployment API) Documentation updates in both READMEs: * Added a "Hosted WebSocket URL" subsection documenting the wss://<account>.services.ai.azure.com/api/projects/agents/endpoint/ protocols/invocations_ws URL template (parameter table + Bearer auth note) * Clarified that the deployed agent's managed identity needs Foundry User at AI Services account scope (not project scope). Voice Live's realtime API is exposed at the account host, so a project-scope assignment returns 403 and the upstream WebSocket closes immediately after the client connects. Includes a concrete az role assignment recipe and an RBAC propagation note. * Added a matching troubleshooting bullet that points back to the deploy section when the upstream WS closes on connect. Validation: * Both samples PASS local E2E (>120KB audio, response_done, no errors) against the same Voice Live endpoint * The C# sample also PASSES hosted E2E against a deployed agent on a real Foundry project once the account-scope role assignment propagates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix (1 CPU, 1Gi) leftover in agent.yaml + README files table Address PR review: agent.yaml still pinned the invalid (1 CPU, 1Gi) Foundry resource tier even though agent.manifest.yaml was bumped to 2Gi. Bumping both yaml files and the README files table to keep the documented runtime config consistent with the manifest. Applied the same fix to the Python sample for parity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updates --------- Co-authored-by: Yulin Li <yulili@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 30c957a commit 5839585

30 files changed

Lines changed: 2789 additions & 16 deletions

.github/CODEOWNERS

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# See for instructions on this file https://help.github.com/articles/about-codeowners/
2+
3+
#### Team-owned sample areas ############################################################################
4+
# Each contributing team owns their sample paths for review routing. See CONTRIBUTING.md for setup guide.
5+
/samples/python/hosted-agents/ @microsoft-foundry/hosted-agents
6+
/samples/csharp/hosted-agents/ @microsoft-foundry/hosted-agents
7+
8+
#### files referenced in docs (DO NOT EDIT, except for Docs team!!!) ##########################################
9+
/infrastructure/infrastructure-setup-bicep/01-connections/connection-key-vault.bicep @microsoft-foundry/AI-Platform-Docs
10+
/infrastructure/infrastructure-setup-bicep/05-custom-policy-definitions/deny-disallowed-connections.json @microsoft-foundry/AI-Platform-Docs
11+
/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/main.tf @microsoft-foundry/AI-Platform-Docs
12+
/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/providers.tf @microsoft-foundry/AI-Platform-Docs
13+
/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/variables.tf @microsoft-foundry/AI-Platform-Docs
14+
/infrastructure/infrastructure-setup-terraform/00-basic/code/main.tf @microsoft-foundry/AI-Platform-Docs
15+
/infrastructure/infrastructure-setup-terraform/00-basic/code/providers.tf @microsoft-foundry/AI-Platform-Docs
16+
/infrastructure/infrastructure-setup-terraform/00-basic/code/variables.tf @microsoft-foundry/AI-Platform-Docs
17+
/samples-classic/REST/quickstart/quickstart.sh @microsoft-foundry/AI-Platform-Docs
18+
/samples-classic/csharp/quickstart/Samples/.env.example @microsoft-foundry/AI-Platform-Docs
19+
/samples-classic/csharp/quickstart/Samples/AgentFileSearch.cs @microsoft-foundry/AI-Platform-Docs
20+
/samples-classic/csharp/quickstart/Samples/AgentService.cs @microsoft-foundry/AI-Platform-Docs
21+
/samples-classic/csharp/quickstart/Samples/SimpleInference.cs @microsoft-foundry/AI-Platform-Docs
22+
/samples-classic/java/quickstart/src/main/java/com/azure/ai/foundry/samples/AgentSample.java @microsoft-foundry/AI-Platform-Docs
23+
/samples-classic/java/quickstart/src/main/java/com/azure/ai/foundry/samples/ChatCompletionSample.java @microsoft-foundry/AI-Platform-Docs
24+
/samples-classic/java/quickstart/src/main/java/com/azure/ai/foundry/samples/FileSearchAgentSample.java @microsoft-foundry/AI-Platform-Docs
25+
/samples-classic/python/quickstart/create_project.py @microsoft-foundry/AI-Platform-Docs
26+
/samples-classic/python/quickstart/quickstart.py @microsoft-foundry/AI-Platform-Docs
27+
/samples-classic/typescript/quickstart/.env.template @microsoft-foundry/AI-Platform-Docs
28+
/samples-classic/typescript/quickstart/src/quickstart.ts @microsoft-foundry/AI-Platform-Docs
29+
/samples/REST/quickstart/quickstart-chat-with-agent.sh @microsoft-foundry/AI-Platform-Docs
30+
/samples/REST/quickstart/quickstart-create-agent.sh @microsoft-foundry/AI-Platform-Docs
31+
/samples/REST/quickstart/quickstart-responses.sh @microsoft-foundry/AI-Platform-Docs
32+
/samples/csharp/enterprise-agent-tutorial/1-idea-to-prototype/Evaluate/Program.cs @microsoft-foundry/AI-Platform-Docs
33+
/samples/csharp/enterprise-agent-tutorial/1-idea-to-prototype/ModernWorkplaceAssistant/Program.cs @microsoft-foundry/AI-Platform-Docs
34+
/samples/csharp/quickstart/chat-with-agent/quickstart-chat-with-agent.cs @microsoft-foundry/AI-Platform-Docs
35+
/samples/csharp/quickstart/create-agent/quickstart-create-agent.cs @microsoft-foundry/AI-Platform-Docs
36+
/samples/csharp/quickstart/responses/quickstart-responses.cs @microsoft-foundry/AI-Platform-Docs
37+
/samples/java/quickstart/chat-with-agent/src/main/java/com/azure/ai/agents/ChatWithAgent.java @microsoft-foundry/AI-Platform-Docs
38+
/samples/java/quickstart/create-agent/src/main/java/com/azure/ai/agents/CreateAgent.java @microsoft-foundry/AI-Platform-Docs
39+
/samples/python/enterprise-agent-tutorial/1-idea-to-prototype/evaluate.py @microsoft-foundry/AI-Platform-Docs
40+
/samples/python/enterprise-agent-tutorial/1-idea-to-prototype/main.py @microsoft-foundry/AI-Platform-Docs
41+
/samples/python/foundry-models/model-router/model-router-chat-completions.py @microsoft-foundry/AI-Platform-Docs
42+
/samples/python/foundry-models/model-router/model-router-foundry-responses.py @microsoft-foundry/AI-Platform-Docs
43+
/samples/python/quickstart/chat-with-agent/quickstart-chat-with-agent.py @microsoft-foundry/AI-Platform-Docs
44+
/samples/python/quickstart/create-agent/quickstart-create-agent.py @microsoft-foundry/AI-Platform-Docs
45+
/samples/python/quickstart/responses/quickstart-responses.py @microsoft-foundry/AI-Platform-Docs
46+
/samples/typescript/quickstart/chat-with-agent/src/quickstart-chat-with-agent.ts @microsoft-foundry/AI-Platform-Docs
47+
/samples/typescript/quickstart/create-agent/src/quickstart-create-agent.ts @microsoft-foundry/AI-Platform-Docs
48+
/samples/typescript/quickstart/responses/src/quickstart-responses.ts @microsoft-foundry/AI-Platform-Docs
49+

.github/copilot-instructions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Files owned by the AI Platform Docs team
2+
3+
If a file is listed in the CODEOWNERS file with @azure-ai-foundry/ai-platform-docs as the owner, it is owned by the AI Platform Docs team. For these files:
4+
5+
- Do not change the filename or move the file.
6+
- Do not remove any comments which contain <some_text> or </some_text> (for any text in between the tags)
7+
- Do not remove any cell in a notebook if it contains metadata with "name:" in it.
8+
9+
In a code review, if any of the above rules are broken, please add the following text to your review:
10+
🛑STOP! This PR contains changes that may break documentation. Please post a message on [ai-platform-docs](https://teams.microsoft.com/l/team/19%3AHhf4F_YfPn3kYGdmWvePNwlbF5-RR8wciQEUwwrcggw1%40thread.tacv2/conversations?groupId=fdaf4412-8993-4ea6-a7d4-aeaded7fc854&tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47) to request help.
11+
12+
Only files owned by the AI Platform Docs team are subject to these rules.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import subprocess
4+
import sys
5+
from dataclasses import dataclass
6+
from itertools import chain
7+
from pathlib import Path
8+
from typing import Dict, Iterable, List, Literal, Optional, Tuple
9+
10+
11+
@dataclass
12+
class GitDiffTreeRecord:
13+
"""Represents a line of output from 'git diff-tree'"""
14+
15+
src_mode: str
16+
src_hash: str
17+
dst_mode: str
18+
dst_hash: str
19+
src_path: Path
20+
dst_path: Optional[Path]
21+
status: Literal["A", "C", "D", "M", "R", "T", "U", "X"]
22+
score: Optional[int]
23+
24+
25+
@dataclass
26+
class GitChange:
27+
diff_record: GitDiffTreeRecord
28+
bytes_changed: int
29+
30+
31+
def parse_git_diff_tree_output(output: str) -> List[GitDiffTreeRecord]:
32+
"""Parses the output of `git diff-tree` as described in the "Raw Output" section
33+
of the man page
34+
"""
35+
36+
def make_record(line: str) -> GitDiffTreeRecord:
37+
src_mode, dst_mode, src_hash, dst_hash, rest = line[1:].split(" ", maxsplit=4)
38+
status_score_and_paths = rest.split("\t")
39+
return GitDiffTreeRecord(
40+
src_mode=src_mode,
41+
src_hash=src_hash,
42+
dst_mode=dst_mode,
43+
dst_hash=dst_hash,
44+
status=status_score_and_paths[0][0],
45+
score=int(status_score_and_paths[0][1:]) if len(status_score_and_paths[0]) > 1 else None,
46+
src_path=Path(status_score_and_paths[1]),
47+
dst_path=Path(status_score_and_paths[2]) if len(status_score_and_paths) >= 3 else None,
48+
)
49+
50+
return [make_record(line) for line in output.splitlines(keepends=False)]
51+
52+
53+
def get_blob_sizes(hashes: Iterable[str]) -> Dict[str, Optional[int]]:
54+
"""Fetches the sizes, in bytes, of git blobs
55+
56+
:param hashes: A iterable of git blob hashes
57+
:type hashes: Iterable[str]
58+
59+
:return: A dictionary that mapping blob hashes to their size if the blob exists,
60+
or None otherwise
61+
:rtype: Dict[str, Optional[int]]
62+
"""
63+
input = "\n".join(set(hashes))
64+
cat_file_output = subprocess.run(
65+
["git", "cat-file", "--batch-check"],
66+
input=input,
67+
check=True,
68+
text=True,
69+
capture_output=True,
70+
).stdout
71+
72+
def make_object_size_tuple(line: str) -> Tuple[str, Optional[int]]:
73+
hash, *_, size = line.split()
74+
return (hash, int(size) if size != "missing" else None)
75+
76+
return dict(make_object_size_tuple(line) for line in cat_file_output.splitlines(keepends=False))
77+
78+
79+
def get_file_size_differences(commit_range: str) -> Dict[Path, GitChange]:
80+
"""Computes the size difference, in bytes, of files changed between two commits
81+
82+
:param commit_range: A git commit range (e.g. HEAD~3..HEAD)
83+
:type commit_range: str
84+
85+
:return: A dictionary mapping paths (relative to repository root) to size
86+
differences.
87+
:rtype: dict[Path, GitChange]
88+
"""
89+
changed_records = parse_git_diff_tree_output(
90+
subprocess.run(
91+
["git", "diff-tree", "-r", commit_range],
92+
capture_output=True,
93+
text=True,
94+
check=True,
95+
).stdout
96+
)
97+
98+
sizes = get_blob_sizes(chain.from_iterable((idx.src_hash, idx.dst_hash) for idx in changed_records))
99+
100+
assert {"A", "D", "M"}.issuperset(idx.status for idx in changed_records)
101+
102+
def as_int(maybe_num: Optional[int]) -> int:
103+
return maybe_num or 0
104+
105+
return {
106+
x.src_path: GitChange(
107+
diff_record=x,
108+
bytes_changed=as_int(sizes[x.dst_hash]) - as_int(sizes[x.src_hash]),
109+
)
110+
for x in changed_records
111+
}
112+
113+
114+
def main(
115+
commit_range: str,
116+
quiet: bool = False,
117+
limit: Optional[int] = None,
118+
show_n_largest_files: int = 30,
119+
) -> Literal[0, 1]:
120+
size_differences = get_file_size_differences(commit_range)
121+
cumulative_size_difference = sum(x.bytes_changed for x in size_differences.values())
122+
exceeds_limit = limit is not None and cumulative_size_difference > limit
123+
124+
def bytes_diff(num: int) -> str:
125+
return ("+" if num >= 0 else "") + human_friendly_bytes(num)
126+
127+
if not quiet:
128+
print(f"Total file size difference for commit range '{commit_range}': ")
129+
print(f"\t{bytes_diff(cumulative_size_difference)}", end="")
130+
print(f" (Exceeds set limit of {bytes_diff(limit)})" if exceeds_limit else "")
131+
132+
largest_n_sizes = sorted(size_differences.items(), key=lambda x: x[1].bytes_changed, reverse=True)[
133+
:show_n_largest_files
134+
]
135+
136+
if largest_n_sizes:
137+
print("")
138+
print(f"Largest {len(largest_n_sizes)} filesize differences:")
139+
140+
for path, val in largest_n_sizes:
141+
print(f"\t{bytes_diff(val.bytes_changed)}\t{path}")
142+
143+
return 1 if exceeds_limit else 0
144+
145+
146+
def num_bytes(arg: str) -> int:
147+
"""Converts a string to a number of bytes"""
148+
error = argparse.ArgumentTypeError(f"'{arg}' cannot be parsed into a number of bytes")
149+
try:
150+
return int(arg)
151+
except ValueError:
152+
pass
153+
154+
if len(arg) < 3:
155+
raise error
156+
157+
num, suffix = (arg[:-2], arg[-2:])
158+
shift_values = {
159+
"KB": 1,
160+
"MB": 2,
161+
"GB": 3,
162+
"TB": 4,
163+
"PB": 5,
164+
"EB": 6,
165+
"ZB": 7,
166+
"YB": 8,
167+
}
168+
169+
shift = shift_values.get(suffix)
170+
171+
if shift is None:
172+
raise error
173+
try:
174+
return int(num) << (shift * 10)
175+
except ValueError as e:
176+
raise error from e
177+
178+
179+
def human_friendly_bytes(num: int) -> str:
180+
"""Prints a number of bytes as a human friendly string"""
181+
for prefix in ["", "K", "M", "G", "T", "P", "E", "Z"]:
182+
if abs(num) < 1024.0:
183+
return f"{num:.1f}{prefix}B"
184+
num /= 1024.0
185+
return f"{num:.1f}YB"
186+
187+
188+
if __name__ == "__main__":
189+
parser = argparse.ArgumentParser(
190+
description="A program that summarizes the file size " + "differences between two git commits."
191+
)
192+
parser.add_argument(
193+
"commit_range",
194+
default="HEAD^..HEAD",
195+
type=str,
196+
help="Commit range to commit the diff for (e.g. HEAD~3..HEAD)",
197+
)
198+
parser.add_argument("--quiet", action="store_true", help="Silence all output")
199+
parser.add_argument(
200+
"--limit",
201+
type=num_bytes,
202+
help="Exit non-zero if total changes exceeds this value. "
203+
+ "Can be a raw number of bytes (e.g. 65536) or a suffixed value (e.g 2MB)",
204+
)
205+
parser.add_argument(
206+
"--show-n-largest-files",
207+
type=int,
208+
help="Show this many of the largest files in diff",
209+
default=30,
210+
)
211+
sys.exit(main(**vars(parser.parse_args())))
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Create ADO user story from GitHub issue
2+
run-name: GitHub Issue #${{ github.event.issue.number }}
3+
4+
on:
5+
issues:
6+
types: [opened]
7+
8+
jobs:
9+
create-ado-story:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Build payload and call Azure DevOps
14+
env:
15+
ADO_ORG: ${{ secrets.ADO_ORG }}
16+
ADO_PROJECT: ${{ secrets.ADO_PROJECT }}
17+
ADO_PAT: ${{ secrets.ADO_PAT }}
18+
DRI_EMAIL: ${{ secrets.DRI_EMAIL }}
19+
ADO_AREA_PATH: ${{ secrets.ADO_AREA_PATH }}
20+
ADO_ITERATION_PATH: ${{ secrets.ADO_ITERATION_PATH }}
21+
ADO_TAG: ${{ secrets.ADO_TAG }}
22+
ISSUE_TITLE: ${{ github.event.issue.title }}
23+
ISSUE_BODY: ${{ github.event.issue.body }}
24+
ISSUE_URL: ${{ github.event.issue.html_url }}
25+
run: |
26+
DESCRIPTION="<div>${ISSUE_BODY//$'\n'/<br/>}<br/><br/><a href=\"$ISSUE_URL\">$ISSUE_URL</a></div>"
27+
28+
jq -n \
29+
--arg title "$ISSUE_TITLE" \
30+
--arg desc "$DESCRIPTION" \
31+
--arg area "$ADO_AREA_PATH" \
32+
--arg iter "$ADO_ITERATION_PATH" \
33+
--arg assn "$DRI_EMAIL" \
34+
--arg tags "$ADO_TAG" \
35+
'[
36+
{ "op":"add", "path":"/fields/System.Title", "value":$title },
37+
{ "op":"add", "path":"/fields/System.Description", "value":$desc },
38+
{ "op":"add", "path":"/fields/System.AreaPath", "value":$area },
39+
{ "op":"add", "path":"/fields/System.IterationPath", "value":$iter },
40+
{ "op":"add", "path":"/fields/System.AssignedTo", "value":$assn },
41+
{ "op":"add", "path":"/fields/System.Tags", "value":$tags }
42+
]' > /tmp/payload.json
43+
44+
AUTH=$(printf ":$ADO_PAT" | base64 | tr -d '\n')
45+
WORK_ITEM_URL="https://dev.azure.com/${ADO_ORG}/${ADO_PROJECT}/_apis/wit/workitems/\$User%20Story?api-version=7.1-preview.3"
46+
47+
curl --fail-with-body -sS \
48+
-H "Content-Type: application/json-patch+json" \
49+
-H "Authorization: Basic $AUTH" \
50+
-X POST \
51+
--data-binary @/tmp/payload.json \
52+
"$WORK_ITEM_URL"

.github/workflows/pre-commit.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Pre-Commit
2+
3+
4+
on:
5+
# push:
6+
# branches:
7+
# - main
8+
# pull_request:
9+
# branches:
10+
# - main
11+
workflow_dispatch:
12+
13+
jobs:
14+
pre-commit:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v3
18+
- uses: actions/setup-python@v4
19+
with:
20+
python-version: "3.9"
21+
- run: pip install -r dev-requirements.txt
22+
- name: Run Pre-Commit
23+
run: pre-commit run --all-files
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Pull Request Checks
2+
3+
on:
4+
# push:
5+
# branches:
6+
# - main
7+
# pull_request:
8+
# branches:
9+
# - main
10+
workflow_dispatch:
11+
12+
jobs:
13+
pull_request_size:
14+
if: github.event_name == 'pull_request'
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v3
18+
- uses: actions/setup-python@v4
19+
with:
20+
python-version: "3.9"
21+
- name: Check Pull Request Size
22+
run: |
23+
git fetch origin ${{ github.event.pull_request.base.ref }} --quiet # Need to manually fetch base branch in CI
24+
python ./.github/scripts/commit-filesize-diff-summary.py --limit 1MB origin/${{ github.event.pull_request.base.ref }}..HEAD

0 commit comments

Comments
 (0)