diff --git a/python/.github/skills/python-feature-lifecycle/SKILL.md b/python/.github/skills/python-feature-lifecycle/SKILL.md new file mode 100644 index 0000000000..d9b654a9da --- /dev/null +++ b/python/.github/skills/python-feature-lifecycle/SKILL.md @@ -0,0 +1,238 @@ +# Copyright (c) Microsoft. All rights reserved. + +--- +name: python-feature-lifecycle +description: > + Guidance for package and feature lifecycle in the Agent Framework Python + codebase, including stage meanings, feature-stage decorators, feature enums, + and how to move APIs from one stage to the next. +--- + +# Python Feature Lifecycle + +## Two lifecycle levels + +Agent Framework uses lifecycle at two different levels: + +1. **Package lifecycle** — the maturity of the package as a whole +2. **Feature lifecycle** — the maturity of a specific API or feature inside that package + +These are related, but they are **not the same thing**. + +- The **package stage is the default** for everything in the package. +- **Feature-stage decorators are only for exceptions** when a feature is behind the package's default stage. +- Do **not** decorate every class or function just because the package is experimental or release candidate. + +### Important default + +If a package is still in **beta / experimental preview**, all public APIs in that package are experimental by default. + +- Do **not** add `@experimental(...)` everywhere in that package. +- The package stage already communicates that default. + +Once a package moves forward, you can keep individual features behind: + +- If a package moves to **release candidate**, a feature may remain **experimental** +- If a package moves to **released / GA**, a feature may remain **experimental** or **release candidate** + +That is the main use case for feature-stage decorators. + +## The four stages + +### 1. Experimental + +Use for features that are still unstable and may change or be removed without notice. + +Feature-level code pattern: + +```python +from ._feature_stage import ExperimentalFeature, experimental + + +@experimental(feature_id=ExperimentalFeature.MY_FEATURE) +class MyFeature: + ... +``` + +Behavior: + +- Adds an experimental warning block to the docstring +- Records feature metadata on the decorated object +- Emits a runtime warning the first time the feature is used (once per feature by default) + +Enum setup: + +- Add an all-caps member to `ExperimentalFeature` +- Reuse the same feature ID across all APIs that belong to the same conceptual feature + +### 2. Release candidate + +Use for features that are nearly stable but may still receive small refinements before GA. + +Feature-level code pattern: + +```python +from ._feature_stage import ReleaseCandidateFeature, release_candidate + + +@release_candidate(feature_id=ReleaseCandidateFeature.MY_FEATURE) +class MyFeature: + ... +``` + +Behavior: + +- Adds a release-candidate note to the docstring +- Records feature metadata on the decorated object +- Does **not** emit the experimental warning + +Enum setup: + +- Add an all-caps member to `ReleaseCandidateFeature` + +### 3. Released + +Use for stable GA APIs. + +Code pattern: + +- **No feature-stage decorator** +- **No entry** in `ExperimentalFeature` +- **No entry** in `ReleaseCandidateFeature` + +If a feature is fully released, remove any stage-specific feature annotation. + +### 4. Deprecated + +Use for APIs that still exist but should not be used for new code. + +Code pattern: + +```python +import sys + +if sys.version_info >= (3, 13): + from warnings import deprecated # type: ignore # pragma: no cover +else: + from typing_extensions import deprecated # type: ignore # pragma: no cover + + +@deprecated("MyOldFeature is deprecated. Use MyNewFeature instead.") +class MyOldFeature: + ... +``` + +Behavior: + +- Uses the repository's version-conditional deprecation import pattern +- Should describe what to use instead + +Deprecated APIs should not also carry feature-stage decorators. + +## Expected decorators by stage + +| Feature stage | Expected annotation | +| --- | --- | +| Experimental | `@experimental(feature_id=ExperimentalFeature.X)` | +| Release candidate | `@release_candidate(feature_id=ReleaseCandidateFeature.X)` | +| Released | No feature-stage decorator | +| Deprecated | `@deprecated("...")` | + +## Feature enums + +The feature enums are the inventory of currently staged features: + +- `ExperimentalFeature` +- `ReleaseCandidateFeature` + +Guidance: + +- Use one enum member per conceptual feature, not per class +- Ideally, an ADR already defines the overall feature boundary and therefore the feature ID that staged APIs for that feature should reuse +- Keep feature IDs all caps +- Reuse the same member across related APIs for the same feature +- Remove enum members when the feature no longer belongs to that stage +- Treat these enums as **current-stage inventories**, not as a stable consumer introspection API + +Minimal consumer guidance: + +- Treat `__feature_stage__` and `__feature_id__` as optional staged metadata, not as stable contracts +- Use `getattr(obj, "__feature_stage__", None)` and `getattr(obj, "__feature_id__", None)` rather than direct attribute access +- Treat missing metadata as "no explicit feature-stage annotation" +- For warning filters while a feature is staged, match the literal feature ID string +- Do **not** rely on `ExperimentalFeature.X`, `ReleaseCandidateFeature.X`, or the continued presence of `__feature_id__` after a feature moves stages or is released + +For consumers, the enums are also re-exported from `agent_framework`. + +For internal implementation code inside `agent_framework`, continue to import the enums and decorators from `._feature_stage`. + +## Package stage vs feature stage + +Use the following rules: + +### Package is experimental / beta + +- All public APIs are experimental by default +- Do **not** add feature-stage decorators just to restate that +- Only introduce feature-level annotations later if the package advances first + +### Package is release candidate + +- All public APIs are RC by default +- Do **not** decorate everything +- Add `@experimental(...)` only for features that are intentionally still behind the package + +### Package is released / GA + +- All public APIs are released by default +- Add `@experimental(...)` or `@release_candidate(...)` only for features still being held back + +## Moving a feature from one stage to the next + +### Experimental -> Release candidate + +1. Move the feature ID from `ExperimentalFeature` to `ReleaseCandidateFeature` +2. Replace `@experimental(...)` with `@release_candidate(...)` +3. Update any tests or docs that mention the old stage + +### Experimental -> Released + +1. Remove `@experimental(...)` +2. Remove the feature from `ExperimentalFeature` +3. Do not add a replacement feature-stage decorator + +### Release candidate -> Released + +1. Remove `@release_candidate(...)` +2. Remove the feature from `ReleaseCandidateFeature` +3. Leave the API undecorated + +### Any stage -> Deprecated + +1. Remove any feature-stage decorator +2. Remove the feature from the stage enum +3. Add `@deprecated("...")` +4. Update docs/tests to reflect the replacement path + +## Promotion guidance + +Features do **not** have to pass through every stage. + +- It is usually a good idea to move features in order when that reflects reality +- But it is completely acceptable to go **experimental -> released** +- Do **not** force a feature through release candidate if there is no real RC period + +Likewise, when a package advances, do not automatically move every feature with it. + +- Promote features based on actual readiness +- Keep lagging features explicitly marked only when they are behind the package default + +## Practical rules of thumb + +- **Package default first, feature exceptions second** +- **Do not decorate everything in preview packages** +- **Do not double-annotate members of an already-staged class** +- **Use enums only for currently staged features** +- **Do not treat stage enums as a compatibility contract** +- **Treat `__feature_stage__` and `__feature_id__` as optional metadata; use `getattr`** +- **Remove stage annotations once a feature is released or deprecated** diff --git a/python/AGENTS.md b/python/AGENTS.md index 7ec268dcd1..eeab275a4e 100644 --- a/python/AGENTS.md +++ b/python/AGENTS.md @@ -11,6 +11,7 @@ Instructions for AI coding agents working in the Python codebase. - `python-development` — coding standards, type annotations, docstrings, logging, performance - `python-testing` — test structure, fixtures, async mode, running tests - `python-code-quality` — linting, formatting, type checking, prek hooks, CI workflow +- `python-feature-lifecycle` — package vs feature lifecycle stages, decorators, enums, and promotion guidance - `python-package-management` — monorepo structure, lazy loading, versioning, new packages - `python-samples` — sample file structure, PEP 723, documentation guidelines diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index d54db07fab..2a74f6f74d 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -76,8 +76,8 @@ def mock_project_client() -> MagicMock: mock_client.telemetry = MagicMock() mock_client.telemetry.get_application_insights_connection_string = AsyncMock() - # Mock get_openai_client method - mock_client.get_openai_client = MagicMock() + # AIProjectClient.get_openai_client() is a sync accessor, even on the aio client. + mock_client.get_openai_client = MagicMock(return_value=MagicMock()) # Mock close method mock_client.close = AsyncMock() diff --git a/python/packages/azure-ai/tests/test_provider.py b/python/packages/azure-ai/tests/test_provider.py index 912b8625e7..09eb8feeee 100644 --- a/python/packages/azure-ai/tests/test_provider.py +++ b/python/packages/azure-ai/tests/test_provider.py @@ -33,8 +33,8 @@ def mock_project_client() -> MagicMock: mock_client.telemetry = MagicMock() mock_client.telemetry.get_application_insights_connection_string = AsyncMock() - # Mock get_openai_client method - mock_client.get_openai_client = MagicMock() + # AIProjectClient.get_openai_client() is a sync accessor, even on the aio client. + mock_client.get_openai_client = MagicMock(return_value=MagicMock()) # Mock close method mock_client.close = AsyncMock() diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index a9e4245e77..d30d5159ea 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -78,6 +78,7 @@ from ._evaluation import ( tool_called_check, tool_calls_present, ) +from ._feature_stage import ExperimentalFeature, ReleaseCandidateFeature from ._mcp import MCPStdioTool, MCPStreamableHTTPTool, MCPWebsocketTool from ._middleware import ( AgentContext, @@ -314,6 +315,7 @@ __all__ = [ "Evaluator", "Executor", "ExpectedToolCall", + "ExperimentalFeature", "FanInEdgeGroup", "FanOutEdgeGroup", "FileCheckpointStorage", @@ -344,6 +346,7 @@ __all__ = [ "OuterFinalT", "OuterUpdateT", "RawAgent", + "ReleaseCandidateFeature", "ResponseStream", "Role", "RoleLiteral", diff --git a/python/packages/core/agent_framework/_docstrings.py b/python/packages/core/agent_framework/_docstrings.py index 44dd7c50a3..93854b4627 100644 --- a/python/packages/core/agent_framework/_docstrings.py +++ b/python/packages/core/agent_framework/_docstrings.py @@ -3,12 +3,14 @@ from __future__ import annotations import inspect +import textwrap from collections.abc import Callable, Mapping from typing import Any _GOOGLE_SECTION_HEADERS = ( "Args:", "Keyword Args:", + "Attributes:", "Returns:", "Raises:", "Examples:", @@ -45,6 +47,29 @@ def _format_keyword_arg_lines(extra_keyword_args: Mapping[str, str]) -> list[str return formatted_lines +def insert_docstring_block(docstring: str | None, *, block: str) -> str | None: + """Insert a preformatted block before the first Google-style section.""" + cleaned_block = textwrap.dedent(block).strip() + if not cleaned_block: + return docstring + if not docstring: + return cleaned_block + + lines = inspect.cleandoc(docstring).splitlines() + block_lines = cleaned_block.splitlines() + insert_index = _find_next_section_index(lines, 0) + + insertion: list[str] = [] + if insert_index > 0 and lines[insert_index - 1] != "": + insertion.append("") + insertion.extend(block_lines) + if insert_index < len(lines) and insertion[-1] != "": + insertion.append("") + + lines[insert_index:insert_index] = insertion + return "\n".join(lines).rstrip() + + def build_layered_docstring( source: Callable[..., Any], *, diff --git a/python/packages/core/agent_framework/_feature_stage.py b/python/packages/core/agent_framework/_feature_stage.py new file mode 100644 index 0000000000..9d5d50499a --- /dev/null +++ b/python/packages/core/agent_framework/_feature_stage.py @@ -0,0 +1,278 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import asyncio.coroutines +import functools +import inspect +import sys +import warnings +from collections.abc import Callable +from enum import Enum +from types import MethodType +from typing import Any, Literal, TypeVar, cast + +from ._docstrings import insert_docstring_block + +FeatureStageT = TypeVar("FeatureStageT", bound=Callable[..., Any]) + +FeatureStageName = Literal["experimental", "release_candidate"] + +# Optional feature-stage metadata for warnings and best-effort introspection. +_FEATURE_ID_ATTR = "__feature_id__" +_FEATURE_STAGE_ATTR = "__feature_stage__" +_WARNED_FEATURES: set[tuple[type[Warning], str]] = set() +_EXPERIMENTAL_DOCSTRING = """\ +.. warning:: Experimental + + This API is experimental and subject to change or removal + in future versions without notice. +""" +_RELEASE_CANDIDATE_DOCSTRING = """\ +.. note:: Release candidate + + This API is in release-candidate stage and may receive + minor refinements before it is considered generally available. +""" + + +class ExperimentalFeature(str, Enum): + """Current experimental feature IDs. + + This enum is a stage-scoped inventory, not a stable introspection surface. + Members may move or be removed as features advance. The `__feature_id__` + attribute is also optional stage metadata and may disappear when a feature + is released, so consumer code should use `getattr(...)` rather than relying + on enum membership or attribute presence over time. + """ + + SKILLS = "SKILLS" + + +class ReleaseCandidateFeature(str, Enum): + """Current release-candidate feature IDs. + + This enum is a stage-scoped inventory, not a stable introspection surface. + Members may move or be removed as features advance. The `__feature_id__` + attribute is also optional stage metadata and may disappear when a feature + is released, so consumer code should use `getattr(...)` rather than relying + on enum membership or attribute presence over time. + """ + + +class FeatureStageWarning(FutureWarning): + """Base warning category for staged APIs.""" + + +class ExperimentalWarning(FeatureStageWarning): + """Warning emitted when an experimental API is used.""" + + +def _normalize_feature_id(feature_id: str | Enum) -> str: + return str(feature_id.value if isinstance(feature_id, Enum) else feature_id) + + +def _get_object_name(obj: Any) -> str: + return str(getattr(obj, "__qualname__", getattr(obj, "__name__", type(obj).__name__))) + + +def _get_descriptor_callable(obj: Any) -> Callable[..., Any]: + return cast(Callable[..., Any], obj.__func__) + + +def _is_protocol_class(obj: Any) -> bool: + return isinstance(obj, type) and bool(getattr(obj, "_is_protocol", False)) + + +def _build_stage_warning_message(*, stage: FeatureStageName, feature_id: str, object_name: str) -> str: + if stage == "experimental": + return ( + f"[{feature_id}] {object_name} is experimental and may change or be removed in future versions " + "without notice." + ) + + return ( + f"[{feature_id}] {object_name} is in release-candidate stage and may receive minor refinements before it is " + "considered generally available." + ) + + +def _set_feature_stage_metadata(obj: Any, *, stage: FeatureStageName, feature_id: str) -> None: + setattr(obj, _FEATURE_STAGE_ATTR, stage) + setattr(obj, _FEATURE_ID_ATTR, feature_id) + + +def _warn_on_feature_use( + *, + stage: FeatureStageName, + feature_id: str, + object_name: str, + category: type[Warning], + stacklevel: int, +) -> None: + warning_key = (category, feature_id) + if warning_key in _WARNED_FEATURES: + return + + warnings.warn( + _build_stage_warning_message(stage=stage, feature_id=feature_id, object_name=object_name), + category=category, + stacklevel=stacklevel, + ) + _WARNED_FEATURES.add(warning_key) + + +def _add_runtime_warning( + obj: FeatureStageT, + *, + stage: FeatureStageName, + feature_id: str, + category: type[Warning], +) -> FeatureStageT: + object_name = _get_object_name(obj) + + if isinstance(obj, type): + experimental_class = cast(type[Any], obj) + original_new: Any = experimental_class.__new__ + + @functools.wraps(original_new) + def __new__(cls: type[Any], /, *args: Any, **kwargs: Any) -> Any: + if cls is experimental_class: + _warn_on_feature_use( + stage=stage, + feature_id=feature_id, + object_name=object_name, + category=category, + stacklevel=3, + ) + if original_new is not object.__new__: + return original_new(cls, *args, **kwargs) + if cls.__init__ is object.__init__ and (args or kwargs): + raise TypeError(f"{cls.__name__}() takes no arguments") + return original_new(cls) + + experimental_class.__new__ = staticmethod(__new__) # type: ignore[assignment] + + original_init_subclass: Any = experimental_class.__init_subclass__ + if isinstance(original_init_subclass, MethodType): + original_init_subclass_func = original_init_subclass.__func__ + + @functools.wraps(original_init_subclass_func) + def bound_init_subclass_wrapper(*args: Any, **kwargs: Any) -> Any: + _warn_on_feature_use( + stage=stage, + feature_id=feature_id, + object_name=object_name, + category=category, + stacklevel=3, + ) + return original_init_subclass_func(*args, **kwargs) + + experimental_class.__init_subclass__ = classmethod(bound_init_subclass_wrapper) # type: ignore[assignment] + else: + + @functools.wraps(original_init_subclass) + def init_subclass_wrapper(*args: Any, **kwargs: Any) -> Any: + _warn_on_feature_use( + stage=stage, + feature_id=feature_id, + object_name=object_name, + category=category, + stacklevel=3, + ) + return original_init_subclass(*args, **kwargs) + + experimental_class.__init_subclass__ = init_subclass_wrapper # type: ignore[assignment] + + return cast(FeatureStageT, experimental_class) + + @functools.wraps(obj) + def wrapper(*args: Any, **kwargs: Any) -> Any: + _warn_on_feature_use( + stage=stage, + feature_id=feature_id, + object_name=object_name, + category=category, + stacklevel=3, + ) + return obj(*args, **kwargs) + + if inspect.iscoroutinefunction(obj): + if sys.version_info >= (3, 12): + wrapper = inspect.markcoroutinefunction(wrapper) + else: + wrapper._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore[attr-defined] + + return cast(FeatureStageT, wrapper) + + +def _feature_stage( + *, + stage: FeatureStageName, + feature_id: str | Enum, + docstring_block: str, + warning_category: type[Warning] | None, +) -> Callable[[FeatureStageT], FeatureStageT]: + normalized_feature_id = _normalize_feature_id(feature_id) + + def decorator(obj: FeatureStageT) -> FeatureStageT: + descriptor_wrapper: Callable[[Any], Any] | None = None + target: Any = obj + + if isinstance(obj, staticmethod): + descriptor_wrapper = staticmethod + target = _get_descriptor_callable(obj) + elif isinstance(obj, classmethod): + descriptor_wrapper = classmethod + target = _get_descriptor_callable(obj) + + if not callable(target): + raise TypeError(f"{stage} decorator can only be applied to classes and callables, not {obj!r}.") + + is_protocol_class = _is_protocol_class(target) + decorated: Any = target + if warning_category is not None and not is_protocol_class: + decorated = _add_runtime_warning( + target, + stage=stage, + feature_id=normalized_feature_id, + category=warning_category, + ) + + updated_docstring = insert_docstring_block(decorated.__doc__, block=docstring_block) + if updated_docstring is not None: + decorated.__doc__ = updated_docstring + + # runtime_checkable Protocol classes treat added class attributes as protocol members + # on older Python versions, which breaks isinstance/issubclass checks. + if not is_protocol_class: + _set_feature_stage_metadata(decorated, stage=stage, feature_id=normalized_feature_id) + if descriptor_wrapper is not None: + return cast(FeatureStageT, descriptor_wrapper(decorated)) + + return cast(FeatureStageT, decorated) + + return decorator + + +def experimental(*, feature_id: ExperimentalFeature) -> Callable[[FeatureStageT], FeatureStageT]: + """Mark a class or callable as experimental.""" + return _feature_stage( + stage="experimental", + feature_id=feature_id, + docstring_block=_EXPERIMENTAL_DOCSTRING, + warning_category=ExperimentalWarning, + ) + + +def release_candidate( + *, + feature_id: ReleaseCandidateFeature, +) -> Callable[[FeatureStageT], FeatureStageT]: + """Mark a class or callable as release-candidate.""" + return _feature_stage( + stage="release_candidate", + feature_id=feature_id, + docstring_block=_RELEASE_CANDIDATE_DOCSTRING, + warning_category=None, + ) diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index c95fc46aa2..dc111750af 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -35,6 +35,7 @@ from html import escape as xml_escape from pathlib import Path, PurePosixPath from typing import TYPE_CHECKING, Any, ClassVar, Final, Protocol, runtime_checkable +from ._feature_stage import ExperimentalFeature, experimental from ._sessions import BaseContextProvider from ._tools import FunctionTool @@ -47,14 +48,10 @@ logger = logging.getLogger(__name__) # region Models +@experimental(feature_id=ExperimentalFeature.SKILLS) class SkillResource: """A named piece of supplementary content attached to a skill. - .. warning:: Experimental - - This API is experimental and subject to change or removal - in future versions without notice. - A resource provides data that an agent can retrieve on demand. It holds either a static ``content`` string or a ``function`` that produces content dynamically (sync or async). Exactly one must be provided. @@ -117,14 +114,10 @@ class SkillResource: self._accepts_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()) +@experimental(feature_id=ExperimentalFeature.SKILLS) class SkillScript: """An executable script attached to a skill. - .. warning:: Experimental - - This API is experimental and subject to change or removal - in future versions without notice. - A script represents executable code that an agent can run. It holds either an inline ``function`` callable (code-defined scripts) or a ``path`` to a script file on disk (file-based scripts). @@ -202,11 +195,6 @@ class SkillScript: def parameters_schema(self) -> dict[str, Any] | None: """JSON Schema describing the script's parameters. - .. warning:: Experimental - - This API is experimental and subject to change or removal - in future versions without notice. - Lazily generated from the callable's signature on first access. Returns ``None`` for file-based scripts or functions with no introspectable parameters. @@ -219,14 +207,10 @@ class SkillScript: return self._parameters_schema +@experimental(feature_id=ExperimentalFeature.SKILLS) class Skill: """A skill definition with optional resources. - .. warning:: Experimental - - This API is experimental and subject to change or removal - in future versions without notice. - A skill bundles a set of instructions (``content``) with metadata and zero or more :class:`SkillResource` and :class:`SkillScript` instances. Resources and scripts can be supplied at construction time or added later @@ -432,14 +416,10 @@ class Skill: @runtime_checkable +@experimental(feature_id=ExperimentalFeature.SKILLS) class SkillScriptRunner(Protocol): """Protocol for skill script runners. - .. warning:: Experimental - - This API is experimental and subject to change or removal - in future versions without notice. - A script runner determines how **file-based** skill scripts are run. Implementations decide the execution strategy (e.g., local subprocess, hosted code execution environment, @@ -538,14 +518,10 @@ SCRIPT_RUNNER_INSTRUCTIONS: Final[str] = ( # region SkillsProvider +@experimental(feature_id=ExperimentalFeature.SKILLS) class SkillsProvider(BaseContextProvider): """Context provider that advertises skills and exposes skill tools. - .. warning:: Experimental - - This API is experimental and subject to change or removal - in future versions without notice. - Supports both **file-based** skills (discovered from ``SKILL.md`` files) and **code-defined** skills (passed as :class:`Skill` instances). diff --git a/python/packages/core/tests/core/conftest.py b/python/packages/core/tests/core/conftest.py index 57c0cf5217..a331dca20d 100644 --- a/python/packages/core/tests/core/conftest.py +++ b/python/packages/core/tests/core/conftest.py @@ -3,6 +3,7 @@ import asyncio import logging import sys +import warnings from collections.abc import AsyncIterable, Awaitable, MutableSequence, Sequence from typing import Any, Generic from unittest.mock import patch @@ -10,7 +11,13 @@ from uuid import uuid4 from pytest import fixture -from agent_framework import ( +warnings.filterwarnings( + "ignore", + message=r"\[SKILLS\].*", + category=FutureWarning, +) + +from agent_framework import ( # noqa: E402 AgentResponse, AgentResponseUpdate, AgentSession, @@ -26,8 +33,8 @@ from agent_framework import ( SupportsAgentRun, tool, ) -from agent_framework._clients import OptionsCoT -from agent_framework.observability import ChatTelemetryLayer +from agent_framework._clients import OptionsCoT # noqa: E402 +from agent_framework.observability import ChatTelemetryLayer # noqa: E402 if sys.version_info >= (3, 12): from typing import override # type: ignore diff --git a/python/packages/core/tests/core/test_docstrings.py b/python/packages/core/tests/core/test_docstrings.py index ab4b116422..8ec3b2ee7c 100644 --- a/python/packages/core/tests/core/test_docstrings.py +++ b/python/packages/core/tests/core/test_docstrings.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework._docstrings import apply_layered_docstring, build_layered_docstring +from agent_framework._docstrings import apply_layered_docstring, build_layered_docstring, insert_docstring_block # -- Helpers: stub functions with various docstring shapes -- @@ -36,6 +36,14 @@ def _source_no_sections() -> None: """A plain summary with no Google-style sections.""" +def _source_with_attributes() -> None: + """A documented object. + + Attributes: + value: A documented attribute. + """ + + def _source_no_docstring() -> None: pass @@ -141,6 +149,67 @@ def test_build_preserves_multiple_extra_kwargs_order() -> None: assert alpha_idx < beta_idx < gamma_idx +# -- insert_docstring_block tests -- + + +def test_insert_docstring_block_before_args_section() -> None: + result = insert_docstring_block( + _source_with_args_only.__doc__, + block="""\ + .. warning:: Experimental + + This API is experimental. + """, + ) + assert result is not None + lines = result.splitlines() + warning_index = next(i for i, line in enumerate(lines) if line == ".. warning:: Experimental") + args_index = next(i for i, line in enumerate(lines) if line == "Args:") + assert warning_index < args_index + + +def test_insert_docstring_block_before_attributes_section() -> None: + result = insert_docstring_block( + _source_with_attributes.__doc__, + block="""\ + .. warning:: Experimental + + This API is experimental. + """, + ) + assert result is not None + lines = result.splitlines() + warning_index = next(i for i, line in enumerate(lines) if line == ".. warning:: Experimental") + attributes_index = next(i for i, line in enumerate(lines) if line == "Attributes:") + assert warning_index < attributes_index + + +def test_insert_docstring_block_appends_when_no_sections() -> None: + result = insert_docstring_block( + _source_no_sections.__doc__, + block="""\ + .. note:: Release candidate + + This API is nearly final. + """, + ) + assert result is not None + assert result.endswith("This API is nearly final.") + assert ".. note:: Release candidate" in result + + +def test_insert_docstring_block_returns_block_for_missing_docstring() -> None: + result = insert_docstring_block( + _source_no_docstring.__doc__, + block="""\ + .. warning:: Experimental + + This API is experimental. + """, + ) + assert result == ".. warning:: Experimental\n\n This API is experimental." + + # -- apply_layered_docstring tests -- diff --git a/python/packages/core/tests/core/test_feature_stage.py b/python/packages/core/tests/core/test_feature_stage.py new file mode 100644 index 0000000000..3b5f495e33 --- /dev/null +++ b/python/packages/core/tests/core/test_feature_stage.py @@ -0,0 +1,427 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import inspect +import warnings +from enum import Enum +from typing import Protocol, runtime_checkable + +import pytest + +from agent_framework import ExperimentalFeature as PublicExperimentalFeature +from agent_framework import ReleaseCandidateFeature as PublicReleaseCandidateFeature +from agent_framework._feature_stage import ( + _WARNED_FEATURES, + ExperimentalWarning, + _feature_stage, + experimental, + release_candidate, +) +from agent_framework._feature_stage import ( + ExperimentalFeature as InternalExperimentalFeature, +) +from agent_framework._feature_stage import ( + ReleaseCandidateFeature as InternalReleaseCandidateFeature, +) + + +class AlternateExperimentalFeature(str, Enum): + EXPERIMENTAL_FEATURE = "EXPERIMENTAL_FEATURE" + SHARED_FEATURE = "SHARED_EXPERIMENTAL_FEATURE" + ALTERNATE_FEATURE = "ALTERNATE_EXPERIMENTAL_FEATURE" + + +class InvalidStageFeature(str, Enum): + LOWERCASE = "skills" + + +class NonStringFeature(Enum): + INTEGER = 1 + + +class HelperReleaseCandidateFeature(str, Enum): + RC_FEATURE = "RC_FEATURE" + + +@pytest.fixture(autouse=True) +def clear_feature_warning_state() -> None: + _WARNED_FEATURES.clear() + yield + _WARNED_FEATURES.clear() + + +def test_feature_enums_are_exposed_from_root() -> None: + assert PublicExperimentalFeature is InternalExperimentalFeature + assert PublicReleaseCandidateFeature is InternalReleaseCandidateFeature + + +def test_experimental_decorator_accepts_feature_enum() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @experimental(feature_id=AlternateExperimentalFeature.EXPERIMENTAL_FEATURE) # type: ignore[arg-type] + def skill_function() -> None: + pass + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + skill_function() + + assert len(caught) == 1 + assert f"[{AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value}]" in str(caught[0].message) + assert "skill_function" in str(caught[0].message) + assert skill_function.__feature_id__ == AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value + + +def test_experimental_function_warns_on_call_and_not_on_definition() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @experimental(feature_id=AlternateExperimentalFeature.EXPERIMENTAL_FEATURE) # type: ignore[arg-type] + def my_function(value: int) -> int: + """Double the input. + + Args: + value: Value to double. + + Returns: + The doubled value. + """ + return value * 2 + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + assert my_function(3) == 6 + assert my_function(4) == 8 + + assert len(caught) == 1 + assert f"[{AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value}]" in str(caught[0].message) + assert "my_function" in str(caught[0].message) + assert my_function.__feature_stage__ == "experimental" + assert my_function.__feature_id__ == AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value + assert my_function.__doc__ is not None + lines = my_function.__doc__.splitlines() + warning_index = next(i for i, line in enumerate(lines) if line == ".. warning:: Experimental") + args_index = next(i for i, line in enumerate(lines) if line == "Args:") + assert warning_index < args_index + + +def test_experimental_class_warns_on_instantiation_and_not_on_definition() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @experimental(feature_id=AlternateExperimentalFeature.EXPERIMENTAL_FEATURE) # type: ignore[arg-type] + class ExperimentalClass: + """An experimental class. + + Args: + value: Value to store. + """ + + def __init__(self, value: int) -> None: + self.value = value + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + instantiation_line = inspect.currentframe().f_lineno + 1 + instance = ExperimentalClass(4) + second_instance = ExperimentalClass(5) + + assert len(caught) == 1 + assert f"[{AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value}]" in str(caught[0].message) + assert "ExperimentalClass" in str(caught[0].message) + assert caught[0].filename == __file__ + assert caught[0].lineno == instantiation_line + assert instance.value == 4 + assert second_instance.value == 5 + assert ExperimentalClass.__feature_stage__ == "experimental" + assert ExperimentalClass.__feature_id__ == AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value + + +def test_experimental_runtime_checkable_protocol_keeps_protocol_runtime_checks() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @runtime_checkable + @experimental(feature_id=AlternateExperimentalFeature.EXPERIMENTAL_FEATURE) # type: ignore[arg-type] + class ExampleProtocol(Protocol): + """A protocol used for runtime checks. + + Returns: + Nothing. + """ + + def __call__(self, value: int) -> int: ... + + assert not caught + + def implementation(value: int) -> int: + return value + + assert isinstance(implementation, ExampleProtocol) + assert ExampleProtocol.__doc__ is not None + assert ".. warning:: Experimental" in ExampleProtocol.__doc__ + assert getattr(ExampleProtocol, "__feature_stage__", None) is None + assert getattr(ExampleProtocol, "__feature_id__", None) is None + + +def test_experimental_warning_is_emitted_once_per_feature() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @experimental(feature_id=AlternateExperimentalFeature.SHARED_FEATURE) # type: ignore[arg-type] + def first() -> None: + pass + + @experimental(feature_id=AlternateExperimentalFeature.SHARED_FEATURE) # type: ignore[arg-type] + class Second: + pass + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + first() + Second() + + assert first is not None + assert Second is not None + assert len(caught) == 1 + assert f"[{AlternateExperimentalFeature.SHARED_FEATURE.value}]" in str(caught[0].message) + assert "first" in str(caught[0].message) + + +def test_release_candidate_internal_helper_adds_metadata_without_runtime_warning() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @_feature_stage( + stage="release_candidate", + feature_id=HelperReleaseCandidateFeature.RC_FEATURE, + docstring_block="""\ +.. note:: Release candidate + + This API is in release-candidate stage and may receive + minor refinements before it is considered generally available. +""", + warning_category=None, + ) + class ReleaseCandidateClass: + """A release-candidate class. + + Args: + value: Value to store. + """ + + def __init__(self, value: int) -> None: + self.value = value + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + instance = ReleaseCandidateClass(5) + + assert instance.value == 5 + assert not caught + assert ReleaseCandidateClass.__feature_stage__ == "release_candidate" + assert ReleaseCandidateClass.__feature_id__ == HelperReleaseCandidateFeature.RC_FEATURE.value + assert ReleaseCandidateClass.__doc__ is not None + assert ".. note:: Release candidate" in ReleaseCandidateClass.__doc__ + + +def test_experimental_property_warns_on_access_and_not_on_definition() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + class Example: + @property + @experimental(feature_id=AlternateExperimentalFeature.EXPERIMENTAL_FEATURE) # type: ignore[arg-type] + def value(self) -> int: + """Return the value. + + Returns: + The stored value. + """ + return 1 + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + assert Example().value == 1 + assert Example().value == 1 + + assert len(caught) == 1 + assert f"[{AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value}]" in str(caught[0].message) + assert "Example.value" in str(caught[0].message) + assert Example.value.__doc__ is not None + lines = Example.value.__doc__.splitlines() + warning_index = next(i for i, line in enumerate(lines) if line == ".. warning:: Experimental") + returns_index = next(i for i, line in enumerate(lines) if line == "Returns:") + assert warning_index < returns_index + + +def test_experimental_staticmethod_warns_when_decorator_wraps_descriptor() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + class Example: + @experimental(feature_id=AlternateExperimentalFeature.EXPERIMENTAL_FEATURE) # type: ignore[arg-type] + @staticmethod + def value() -> int: + """Return the value. + + Returns: + The stored value. + """ + return 1 + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + assert Example.value() == 1 + assert Example.value() == 1 + + assert len(caught) == 1 + assert f"[{AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value}]" in str(caught[0].message) + assert "Example.value" in str(caught[0].message) + assert Example.value.__feature_id__ == AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value + assert Example.value.__doc__ is not None + lines = Example.value.__doc__.splitlines() + warning_index = next(i for i, line in enumerate(lines) if line == ".. warning:: Experimental") + returns_index = next(i for i, line in enumerate(lines) if line == "Returns:") + assert warning_index < returns_index + + +def test_experimental_classmethod_warns_when_decorator_wraps_descriptor() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + class Example: + @experimental(feature_id=AlternateExperimentalFeature.EXPERIMENTAL_FEATURE) # type: ignore[arg-type] + @classmethod + def value(cls) -> int: + """Return the value. + + Returns: + The stored value. + """ + return 1 + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + assert Example.value() == 1 + assert Example.value() == 1 + + assert len(caught) == 1 + assert f"[{AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value}]" in str(caught[0].message) + assert "Example.value" in str(caught[0].message) + assert Example.value.__func__.__feature_id__ == AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value + assert Example.value.__doc__ is not None + lines = Example.value.__doc__.splitlines() + warning_index = next(i for i, line in enumerate(lines) if line == ".. warning:: Experimental") + returns_index = next(i for i, line in enumerate(lines) if line == "Returns:") + assert warning_index < returns_index + + +def test_feature_id_allows_lowercase_values() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @_feature_stage( + stage="experimental", + feature_id=InvalidStageFeature.LOWERCASE, + docstring_block=".. warning:: Experimental", + warning_category=ExperimentalWarning, + ) + def lowercase_feature() -> None: + pass + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + lowercase_feature() + + assert len(caught) == 1 + assert "[skills]" in str(caught[0].message) + assert "lowercase_feature" in str(caught[0].message) + assert lowercase_feature.__feature_id__ == "skills" + + +def test_experimental_decorator_allows_string_feature_id_at_runtime() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @experimental(feature_id="STRING_FEATURE") # type: ignore[arg-type] + def skill_function() -> None: + pass + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + skill_function() + + assert len(caught) == 1 + assert "[STRING_FEATURE]" in str(caught[0].message) + assert "skill_function" in str(caught[0].message) + assert skill_function.__feature_id__ == "STRING_FEATURE" + + +def test_experimental_decorator_allows_other_enum_values_at_runtime() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @experimental(feature_id=AlternateExperimentalFeature.ALTERNATE_FEATURE) # type: ignore[arg-type] + def my_function() -> None: + pass + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + my_function() + + assert len(caught) == 1 + assert f"[{AlternateExperimentalFeature.ALTERNATE_FEATURE.value}]" in str(caught[0].message) + assert "my_function" in str(caught[0].message) + assert my_function.__feature_id__ == AlternateExperimentalFeature.ALTERNATE_FEATURE.value + + +def test_release_candidate_decorator_allows_string_feature_id_at_runtime() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @release_candidate(feature_id="RC_FEATURE") # type: ignore[arg-type] + class ReleaseCandidateClass: + """A release-candidate class.""" + + assert not caught + assert ReleaseCandidateClass.__feature_stage__ == "release_candidate" + assert ReleaseCandidateClass.__feature_id__ == "RC_FEATURE" + + +def test_feature_id_stringifies_non_string_enum_values() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + + @_feature_stage( + stage="experimental", + feature_id=NonStringFeature.INTEGER, + docstring_block=".. warning:: Experimental", + warning_category=ExperimentalWarning, + ) + def numeric_feature() -> None: + pass + + assert not caught + + with warnings.catch_warnings(record=True) as caught: + numeric_feature() + + assert len(caught) == 1 + assert "[1]" in str(caught[0].message) + assert "numeric_feature" in str(caught[0].message) + assert numeric_feature.__feature_id__ == "1" diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index 134c4219cd..16cae57dcd 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -11,7 +11,7 @@ from unittest.mock import AsyncMock import pytest -from agent_framework import SessionContext, Skill, SkillResource, SkillsProvider +from agent_framework import SessionContext, Skill, SkillResource, SkillScript, SkillScriptRunner, SkillsProvider from agent_framework._skills import ( DEFAULT_RESOURCE_EXTENSIONS, DEFAULT_SCRIPT_EXTENSIONS, @@ -32,6 +32,8 @@ from agent_framework._skills import ( _validate_skill_metadata, ) +pytestmark = pytest.mark.filterwarnings(r"ignore:\[SKILLS\].*:FutureWarning") + async def _noop_script_runner(skill: Any, script: Any, args: Any = None) -> None: """No-op script runner for tests that need a SkillScriptRunner.""" @@ -778,6 +780,44 @@ class TestSymlinkDetection: # --------------------------------------------------------------------------- +class TestSkillsExperimentalStage: + """Tests for the experimental stage annotations applied to skills APIs.""" + + def test_docstrings_include_experimental_warning(self) -> None: + assert SkillResource.__doc__ is not None + assert SkillScript.__doc__ is not None + assert Skill.__doc__ is not None + assert SkillScriptRunner.__doc__ is not None + assert SkillsProvider.__doc__ is not None + assert SkillScript.parameters_schema.__doc__ is not None + + assert ".. warning:: Experimental" in SkillResource.__doc__ + assert ".. warning:: Experimental" in SkillScript.__doc__ + assert ".. warning:: Experimental" in Skill.__doc__ + assert ".. warning:: Experimental" in SkillScriptRunner.__doc__ + assert ".. warning:: Experimental" in SkillsProvider.__doc__ + assert ".. warning:: Experimental" not in SkillScript.parameters_schema.__doc__ + + def test_feature_metadata_is_set(self) -> None: + assert SkillResource.__feature_stage__ == "experimental" + assert SkillScript.__feature_stage__ == "experimental" + assert Skill.__feature_stage__ == "experimental" + assert SkillsProvider.__feature_stage__ == "experimental" + feature_ids = [ + SkillResource.__feature_id__, + SkillScript.__feature_id__, + Skill.__feature_id__, + SkillsProvider.__feature_id__, + ] + assert all(isinstance(feature_id, str) and feature_id for feature_id in feature_ids) + assert len(set(feature_ids)) == 1 + assert getattr(SkillScriptRunner, "__feature_stage__", None) is None + assert getattr(SkillScriptRunner, "__feature_id__", None) is None + assert SkillScript.parameters_schema.fget is not None + assert not hasattr(SkillScript.parameters_schema.fget, "__feature_stage__") + assert not hasattr(SkillScript.parameters_schema.fget, "__feature_id__") + + class TestSkillResource: """Tests for SkillResource dataclass.""" @@ -1839,40 +1879,28 @@ class TestSkillScript: """Tests for the SkillScript data model.""" def test_empty_name_raises(self) -> None: - from agent_framework import SkillScript - with pytest.raises(ValueError, match="Script name cannot be empty"): SkillScript(name="") def test_whitespace_name_raises(self) -> None: - from agent_framework import SkillScript - with pytest.raises(ValueError, match="Script name cannot be empty"): SkillScript(name=" ") def test_path_default_none(self) -> None: - from agent_framework import SkillScript - script = SkillScript(name="test", function=lambda: None) assert script.path is None def test_path_set_explicitly(self) -> None: - from agent_framework import SkillScript - script = SkillScript(name="gen.py", path="/skills/my-skill/scripts/gen.py") assert script.path == "/skills/my-skill/scripts/gen.py" def test_create_with_function(self) -> None: - from agent_framework import SkillScript - script = SkillScript(name="analyze", description="Run analysis", function=lambda: "result") assert script.name == "analyze" assert script.description == "Run analysis" assert script.function is not None def test_accepts_kwargs_true_for_kwargs_function(self) -> None: - from agent_framework import SkillScript - def func_with_kwargs(**kwargs: Any) -> str: return "result" @@ -1880,8 +1908,6 @@ class TestSkillScript: assert script._accepts_kwargs is True def test_accepts_kwargs_false_for_regular_function(self) -> None: - from agent_framework import SkillScript - def func_no_kwargs(x: int = 0) -> str: return "result" diff --git a/python/samples/02-agents/feature_stage_introspection.py b/python/samples/02-agents/feature_stage_introspection.py new file mode 100644 index 0000000000..b6ccfff43c --- /dev/null +++ b/python/samples/02-agents/feature_stage_introspection.py @@ -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: + +: + feature_stage = 'experimental' + feature_id = '' + +Root exports without metadata currently have no explicit feature-stage metadata. +""" diff --git a/python/samples/02-agents/skills/README.md b/python/samples/02-agents/skills/README.md index 29f6a85e31..b401278577 100644 --- a/python/samples/02-agents/skills/README.md +++ b/python/samples/02-agents/skills/README.md @@ -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. diff --git a/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py b/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py index 27c9d3a090..a68b09540b 100644 --- a/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py +++ b/python/samples/02-agents/skills/code_defined_skill/code_defined_skill.py @@ -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) diff --git a/python/samples/02-agents/skills/file_based_skill/file_based_skill.py b/python/samples/02-agents/skills/file_based_skill/file_based_skill.py index bcb6bc35ff..809c1cdd63 100644 --- a/python/samples/02-agents/skills/file_based_skill/file_based_skill.py +++ b/python/samples/02-agents/skills/file_based_skill/file_based_skill.py @@ -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 diff --git a/python/samples/02-agents/skills/mixed_skills/mixed_skills.py b/python/samples/02-agents/skills/mixed_skills/mixed_skills.py index 14d015f125..5843dcdd67 100644 --- a/python/samples/02-agents/skills/mixed_skills/mixed_skills.py +++ b/python/samples/02-agents/skills/mixed_skills/mixed_skills.py @@ -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 diff --git a/python/samples/02-agents/skills/script_approval/script_approval.py b/python/samples/02-agents/skills/script_approval/script_approval.py index 25c6c07baf..770e003d39 100644 --- a/python/samples/02-agents/skills/script_approval/script_approval.py +++ b/python/samples/02-agents/skills/script_approval/script_approval.py @@ -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 diff --git a/python/samples/02-agents/skills/subprocess_script_runner.py b/python/samples/02-agents/skills/subprocess_script_runner.py index e24724a0e0..52bac380e8 100644 --- a/python/samples/02-agents/skills/subprocess_script_runner.py +++ b/python/samples/02-agents/skills/subprocess_script_runner.py @@ -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