Skip to content
Open
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
10 changes: 7 additions & 3 deletions sdk/ai/azure-ai-projects/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Release History

## 2.6.0 (Unreleased)
## 2.6.0 (2026-09-04)
Comment thread
howieleung marked this conversation as resolved.

### Features Added

Expand All @@ -12,11 +12,15 @@
* Added `ShellToolboxTool` and supporting container environment and network policy models, with the new `ToolboxToolType.SHELL` enum member.
* Added `WebIQPreviewTool` and `WebIQPreviewToolboxTool`, with new `ToolType.WEB_IQ_PREVIEW` and `ToolboxToolType.WEB_IQ_PREVIEW` enum members.
* Added the optional `external_web_access` property to `WebSearchTool` and `WebSearchToolboxTool` for disabling live internet access.
* Added preview support for Model Router, which dynamically routes each request to an appropriate model.

### Sample updates

* Added `sample_toolbox_with_shell.py`, demonstrating a Prompt Agent invoking a `ShellToolboxTool`.
* Added `sample_synthetic_multiturn_evaluation.py`, demonstrating simulation seed generation from an agent followed by multi-turn conversation simulation and evaluation.
* Added `sample_toolbox_with_shell.py` under `samples/agents/tools/`, demonstrating a Prompt Agent invoking a `ShellToolboxTool`.
* Added `sample_toolbox_with_shipping_skill.py` under `samples/agents/tools/`, demonstrating a Prompt Agent using a skill through a Toolbox MCP endpoint.
* Added `sample_toolbox_with_shell_and_skill.py` under `samples/agents/tools/`, demonstrating a Prompt Agent using a skill with a `ShellToolboxTool` through a Toolbox MCP endpoint.
* Added `sample_synthetic_multiturn_evaluation.py` under `samples/evaluations/`, demonstrating simulation seed generation from an agent followed by multi-turn conversation simulation and evaluation.
* Added `sample_responses_model_router.py` under `samples/responses/`, demonstrating a Responses API request to a model router deployment and selection of a model by the router.

### Bugs Fixed

Expand Down
256 changes: 256 additions & 0 deletions sdk/ai/azure-ai-projects/GeneratePublicMethods.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

[CmdletBinding()]
param(
[string]$PythonExecutable = "python",
[string]$OutputPath = (Join-Path $PSScriptRoot "docs\public-methods.md")
)

$ErrorActionPreference = "Stop"
$packageRoot = $PSScriptRoot
$temporaryScript = Join-Path ([System.IO.Path]::GetTempPath()) ("generate-public-methods-{0}.py" -f [guid]::NewGuid())

$pythonScript = @'
from __future__ import annotations

import inspect
import os
from pathlib import Path
import sys
from typing import Any


package_root = Path(sys.argv[1]).resolve()
output_path = Path(sys.argv[2]).resolve()
sys.path.insert(0, str(package_root))
os.chdir(package_root)

from azure.core.credentials import AccessToken
from azure.ai.projects import AIProjectClient
from azure.ai.projects.aio import AIProjectClient as AsyncAIProjectClient
import azure.ai.projects as projects_package


class FakeCredential:
def get_token(self, *args: Any, **kwargs: Any) -> AccessToken:
return AccessToken("fake-token", 2**31)


class AsyncFakeCredential:
async def get_token(self, *args: Any, **kwargs: Any) -> AccessToken:
return AccessToken("fake-token", 2**31)


def assert_local_import() -> None:
imported_path = Path(projects_package.__file__).resolve()
if not imported_path.is_relative_to(package_root):
raise RuntimeError(
f"Expected azure.ai.projects from {package_root}, but imported {imported_path}"
)


def unwrap_operation(value: Any) -> Any:
return getattr(value, "_operation", value)


def operation_instances(container: Any, *, exclude: set[str] | None = None) -> dict[str, Any]:
excluded = exclude or set()
operations: dict[str, Any] = {}
for name, value in vars(container).items():
if name.startswith("_") or name in excluded:
continue
operation = unwrap_operation(value)
if type(operation).__name__.endswith("Operations"):
operations[name] = operation
return operations


def is_handwritten_method(cls: type[Any], name: str) -> bool:
owner = next((base for base in cls.__mro__ if name in vars(base)), None)
if owner is None:
raise RuntimeError(f"Unable to find the class that defines {cls.__name__}.{name}")
source_path = inspect.getsourcefile(owner)
return source_path is not None and "_patch" in Path(source_path).name


def public_methods(instance: Any) -> dict[str, bool]:
methods: dict[str, bool] = {}
for name, member in inspect.getmembers(type(instance), predicate=callable):
if name.startswith("_"):
continue
methods[name] = is_handwritten_method(type(instance), name)
return methods


def client_methods(client: Any) -> dict[str, bool]:
included_special_methods = {"__enter__", "__exit__"}
methods: dict[str, bool] = {}
for name, member in inspect.getmembers(type(client), predicate=callable):
if name.startswith("_") and name not in included_special_methods:
continue
methods[name] = is_handwritten_method(type(client), name)
return methods


def method_label(prefix: str, name: str, handwritten: bool) -> str:
return f".{prefix}{name}{'*' if handwritten else ''}"


def validate_async_parity(
sync_operations: dict[str, Any],
async_operations: dict[str, Any],
group_name: str,
) -> None:
if sync_operations.keys() != async_operations.keys():
sync_only = sorted(sync_operations.keys() - async_operations.keys())
async_only = sorted(async_operations.keys() - sync_operations.keys())
raise RuntimeError(
f"{group_name} sub-client mismatch; sync-only={sync_only}, async-only={async_only}"
)

for name in sorted(sync_operations):
sync_methods = set(public_methods(sync_operations[name]))
async_methods = set(public_methods(async_operations[name]))
if sync_methods != async_methods:
raise RuntimeError(
f"{group_name}.{name} method mismatch; "
f"sync-only={sorted(sync_methods - async_methods)}, "
f"async-only={sorted(async_methods - sync_methods)}"
)


def table(lines: list[str], rows: list[tuple[str, str, int]]) -> None:
lines.extend(
[
"| Subclient | Class Name | Methods Count |",
"| --- | --- | --- |",
]
)
lines.extend(f"| `{name}` | {class_name} | {count} |" for name, class_name, count in rows)


assert_local_import()
endpoint = "https://example.services.ai.azure.com/api/projects/example"
sync_client = AIProjectClient(endpoint=endpoint, credential=FakeCredential(), allow_preview=True)
async_client = AsyncAIProjectClient(endpoint=endpoint, credential=AsyncFakeCredential(), allow_preview=True)

try:
sync_stable = operation_instances(sync_client, exclude={"beta"})
async_stable = operation_instances(async_client, exclude={"beta"})
sync_beta = operation_instances(sync_client.beta)
async_beta = operation_instances(async_client.beta)

validate_async_parity(sync_stable, async_stable, "stable")
validate_async_parity(sync_beta, async_beta, "beta")

stable_methods = {name: public_methods(instance) for name, instance in sync_stable.items()}
beta_methods = {name: public_methods(instance) for name, instance in sync_beta.items()}
direct_methods = client_methods(sync_client)

stable_count = sum(len(methods) for methods in stable_methods.values())
beta_count = sum(len(methods) for methods in beta_methods.values())
total_count = len(direct_methods) + stable_count + beta_count

lines = [
"# Public AIProjectClient methods",
"",
"<!-- Generated by GeneratePublicMethods.ps1. Do not edit manually. -->",
"",
"This document lists all public methods available on `AIProjectClient` and its sub-clients. "
"Overload methods are not counted. Only synchronous methods are counted (but each one has an "
"equivalent asynchronous method).",
"",
"## Summary",
"",
f"There are a total of {total_count} unique public methods:",
"",
f"- {len(direct_methods)} stable methods on the client",
f"- {stable_count} stable methods on top-level sub-clients",
f"- {beta_count} beta methods on nested beta sub-clients",
"",
"### Top-level sub-clients (stable operations)",
"",
]

stable_rows = [
(name, type(sync_stable[name]).__name__, len(stable_methods[name]))
for name in sorted(sync_stable)
]
table(lines, stable_rows)
lines.extend(["", "### Nested sub-clients (beta operations)", ""])
beta_rows = [
(f"beta.{name}", type(sync_beta[name]).__name__, len(beta_methods[name]))
for name in sorted(sync_beta)
]
table(lines, beta_rows)

lines.extend(
[
"",
"## Stable methods on the client",
"",
"Alphabetically sorted. An asterisk at the end of the method name means it is a hand-written method.",
"",
"```text",
]
)
lines.extend(method_label("", name, direct_methods[name]) for name in sorted(direct_methods))
lines.extend(
[
"```",
"",
"## Stable methods on top-level sub clients",
"",
"Alphabetically sorted. An asterisk at the end of the method name means it is a hand-written method.",
"",
"```text",
]
)
for index, subclient_name in enumerate(sorted(stable_methods)):
if index:
lines.append("")
methods = stable_methods[subclient_name]
lines.extend(
method_label(f"{subclient_name}.", name, methods[name]) for name in sorted(methods)
)
lines.extend(
[
"```",
"",
"## Beta methods on nested sub-clients",
"",
"Alphabetically sorted. An asterisk at the end of the method name means it is a hand-written method.",
"",
"```text",
]
)
for index, subclient_name in enumerate(sorted(beta_methods)):
if index:
lines.append("")
methods = beta_methods[subclient_name]
lines.extend(
method_label(f"beta.{subclient_name}.", name, methods[name]) for name in sorted(methods)
)
lines.extend(["```", ""])

output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text("\n".join(lines), encoding="utf-8", newline="\n")
print(
f"Generated {output_path} with {total_count} methods "
f"({len(direct_methods)} client, {stable_count} stable, {beta_count} beta)."
)
finally:
sync_client.close()
'@

try {
[System.IO.File]::WriteAllText($temporaryScript, $pythonScript, [System.Text.UTF8Encoding]::new($false))
& $PythonExecutable $temporaryScript $packageRoot $OutputPath
if ($LASTEXITCODE -ne 0) {
throw "Public method generation failed with exit code $LASTEXITCODE."
}
}
finally {
Remove-Item $temporaryScript -Force -ErrorAction SilentlyContinue
}
8 changes: 8 additions & 0 deletions sdk/ai/azure-ai-projects/PostEmitter.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,11 @@ foreach ($f in $files) {
# Finishing by running 'black' tool to format code.
pip install black
black --config ../../../eng/black-pyproject.toml .

# Regenerate API review artifacts and the public method inventory.
azpysdk apistub .
$apiStubExitCode = $LASTEXITCODE
.\GeneratePublicMethods.ps1
if ($apiStubExitCode -ne 0) {
throw "API stub generation failed with exit code $apiStubExitCode."
}
53 changes: 0 additions & 53 deletions sdk/ai/azure-ai-projects/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8616,72 +8616,19 @@ namespace azure.ai.projects.models


class azure.ai.projects.models.RaiConfig(_Model):
invocations_moderation: Optional[RaiInvocationModeration]
rai_policy_name: str
Comment thread
howieleung marked this conversation as resolved.

@overload
def __init__(
self,
*,
invocations_moderation: Optional[RaiInvocationModeration] = ...,
rai_policy_name: str
) -> None: ...

@overload
def __init__(self, mapping: Mapping[str, Any]) -> None: ...


class azure.ai.projects.models.RaiInvocationContentType(str, Enum, metaclass=CaseInsensitiveEnumMeta):
JSON = "json"
TEXT = "text"


class azure.ai.projects.models.RaiInvocationMode(str, Enum, metaclass=CaseInsensitiveEnumMeta):
BOTH = "both"
NON_STREAMING = "non_streaming"
STREAMING = "streaming"


class azure.ai.projects.models.RaiInvocationModeration(_Model):
input_content_type: Optional[Union[str, RaiInvocationContentType]]
input_paths: Optional[list[str]]
output_content_type: Optional[Union[str, RaiInvocationContentType]]
output_paths: Optional[list[str]]
response_mode: Union[str, RaiInvocationMode]
stream_selectors: Optional[list[RaiSseTextSelector]]

@overload
def __init__(
self,
*,
input_content_type: Optional[Union[str, RaiInvocationContentType]] = ...,
input_paths: Optional[list[str]] = ...,
output_content_type: Optional[Union[str, RaiInvocationContentType]] = ...,
output_paths: Optional[list[str]] = ...,
response_mode: Union[str, RaiInvocationMode],
stream_selectors: Optional[list[RaiSseTextSelector]] = ...
) -> None: ...

@overload
def __init__(self, mapping: Mapping[str, Any]) -> None: ...


class azure.ai.projects.models.RaiSseTextSelector(_Model):
event_type: str
text_field: Optional[str]

@overload
def __init__(
self,
*,
event_type: str,
text_field: Optional[str] = ...
) -> None: ...

@overload
def __init__(self, mapping: Mapping[str, Any]) -> None: ...


class azure.ai.projects.models.RankerVersionType(str, Enum, metaclass=CaseInsensitiveEnumMeta):
AUTO = "auto"
DEFAULT_2024_11_15 = "default-2024-11-15"
Expand Down
2 changes: 1 addition & 1 deletion sdk/ai/azure-ai-projects/api.metadata.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
apiMdSha256: 565d431df41079ca560a9b1c4fda23578362e831b819fc37dd1db36cd4ab0b82
apiMdSha256: 5d405fa9c99c19e09c66ec01f262504083883917fbd849199f1a58de4a21c81e
packageVersion: 2.6.0
parserVersion: 0.3.31
pythonVersion: 3.12.10
2 changes: 1 addition & 1 deletion sdk/ai/azure-ai-projects/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/ai/azure-ai-projects",
"Tag": "python/ai/azure-ai-projects_1cf1cb56ef"
"Tag": "python/ai/azure-ai-projects_80e35fe70b"
}
Loading