Python: Add Python feature lifecycle decorators for released APIs (#4975)

* Add Python feature lifecycle decorators

Introduce reusable experimental and release-candidate decorators for released packages, migrate the Skills APIs to the new staged metadata and warning system, and add lifecycle guidance plus samples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Python CI follow-ups

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Preserve protocol runtime checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-03-31 21:40:08 +02:00
committed by GitHub
Unverified
parent 9c9d81d8b6
commit 55b6e7a9f4
19 changed files with 1220 additions and 60 deletions
@@ -0,0 +1,83 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Any
import agent_framework
"""Feature stage introspection.
This sample demonstrates how to inspect feature lifecycle metadata on Agent
Framework APIs.
The recommended minimal setup for consumers is:
1. Read `__feature_stage__` with `getattr(...)` to see whether an API is staged
2. Read `__feature_id__` with `getattr(...)` only when that metadata is present
3. Treat missing metadata as "no explicit feature-stage annotation"
4. Do not rely on `ExperimentalFeature` or `ReleaseCandidateFeature` membership
over time, since staged features may move or be removed as they advance
This sample loops through the symbols exported from the root `agent_framework`
module and reports the ones that currently carry feature-stage metadata.
"""
def describe_api(name: str, api: Any) -> None:
"""Print optional feature-stage metadata for one API."""
feature_stage = getattr(api, "__feature_stage__", "released")
feature_id = getattr(api, "__feature_id__", None)
print(f"{name}:")
print(f" feature_stage = {feature_stage!r}")
print(f" feature_id = {feature_id!r}")
def iter_staged_root_exports() -> list[tuple[str, Any]]:
"""Return root exports that currently carry feature-stage metadata."""
staged_root_symbols: list[tuple[str, Any]] = []
for symbol_name in sorted(agent_framework.__all__):
symbol = getattr(agent_framework, symbol_name)
feature_stage = getattr(symbol, "__feature_stage__", None)
feature_id = getattr(symbol, "__feature_id__", None)
if feature_stage is None and feature_id is None:
continue
staged_root_symbols.append((symbol_name, symbol))
return staged_root_symbols
async def main() -> None:
"""Run the feature-stage introspection sample."""
print("Feature stage introspection")
print("-" * 60)
# 1. Loop through everything exported from the root module.
staged_root_symbols = iter_staged_root_exports()
# 2. Show the root exports that currently carry feature-stage metadata.
if not staged_root_symbols:
print("No root exports currently carry feature-stage metadata.")
return
print("Root exports with feature-stage metadata:")
for name, api in staged_root_symbols:
describe_api(name, api)
print()
print("Root exports without metadata currently have no explicit feature-stage metadata.")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
Feature stage introspection
------------------------------------------------------------
Root exports with feature-stage metadata:
<export name>:
feature_stage = 'experimental'
feature_id = '<feature id>'
Root exports without metadata currently have no explicit feature-stage metadata.
"""
@@ -53,3 +53,9 @@ All samples require:
- An [Azure AI Foundry](https://ai.azure.com/) project with a deployed model (e.g. `gpt-4o-mini`)
- Azure CLI authentication (`az login`)
- Environment variables set in a `.env` file (see `python/.env.example`)
## Suppressing the experimental warning
The Agent Skills APIs in these samples are still experimental. Each sample includes
a short commented `warnings.filterwarnings(...)` snippet near the imports. Uncomment
it if you want to suppress the Skills warning before using the experimental APIs.
@@ -3,6 +3,11 @@
import asyncio
import json
import os
# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings # isort: skip
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from textwrap import dedent
from typing import Any
@@ -89,7 +94,7 @@ def conversion_policy(**kwargs: Any) -> Any:
Args:
**kwargs: Runtime keyword arguments from ``agent.run()``.
For example, ``agent.run(..., precision=2)``
For example, ``agent.run(..., function_invocation_kwargs={"precision": 2})``
makes ``kwargs["precision"]`` available here.
"""
precision = kwargs.get("precision", 4)
@@ -137,15 +142,11 @@ async def main() -> None:
credential=AzureCliCredential(),
)
# Create the skills provider with the code-defined skill
skills_provider = SkillsProvider(
skills=[unit_converter_skill],
)
# Create the skills provider with the code-defined skill and pass it to the agent
async with Agent(
client=client,
instructions="You are a helpful assistant that can convert units.",
context_providers=[skills_provider],
context_providers=[SkillsProvider(skills=[unit_converter_skill])],
) as agent:
print("Converting units")
print("-" * 60)
@@ -3,6 +3,11 @@
import asyncio
import os
import sys
# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from pathlib import Path
from agent_framework import Agent, SkillsProvider
@@ -4,6 +4,11 @@ import asyncio
import json
import os
import sys
# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from pathlib import Path
from textwrap import dedent
from typing import Any
@@ -2,6 +2,11 @@
import asyncio
import os
# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from textwrap import dedent
from agent_framework import Agent, Skill, SkillsProvider
@@ -9,6 +9,11 @@ from __future__ import annotations
import subprocess
import sys
# Uncomment this filter to suppress the experimental Skills warning before
# using the sample's Skills APIs.
# import warnings
# warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning)
from pathlib import Path
from typing import Any