diff --git a/sdk/ai/azure-ai-projects/.env.template b/sdk/ai/azure-ai-projects/.env.template index 76c795cfaf62..01164731a479 100644 --- a/sdk/ai/azure-ai-projects/.env.template +++ b/sdk/ai/azure-ai-projects/.env.template @@ -46,6 +46,8 @@ SHAREPOINT_USER_INPUT= FABRIC_USER_INPUT= BING_CUSTOM_USER_INPUT= A2A_USER_INPUT= +WORK_IQ_PROJECT_CONNECTION_ID= +WORK_IQ_USER_INPUT= ####################################################################### # diff --git a/sdk/ai/azure-ai-projects/.github/skills/azure-ai-projects-emit-from-typespec/SKILL.md b/sdk/ai/azure-ai-projects/.github/skills/azure-ai-projects-emit-from-typespec/SKILL.md index a64f2072daa8..94f132e0e1fc 100644 --- a/sdk/ai/azure-ai-projects/.github/skills/azure-ai-projects-emit-from-typespec/SKILL.md +++ b/sdk/ai/azure-ai-projects/.github/skills/azure-ai-projects-emit-from-typespec/SKILL.md @@ -117,13 +117,9 @@ git push -u origin ## Step 7: Run post-emitter fixes -After a successful emit, run the post-emitter fix script located in the `sdk/ai/azure-ai-projects` folder: +After a successful emit, run the PowerShell script named `PostEmitter.ps1` located in the `sdk/ai/azure-ai-projects` folder. -``` -post-emitter-fixes.cmd -``` - -This script applies azure-ai-projects-specific corrections to the emitted code (restores `pyproject.toml`, fixes enum names, patches Sphinx doc-string issues, and runs `black` formatting). +This script applies azure-ai-projects specific corrections to the emitted code (restores `pyproject.toml`, fixes enum names, patches Sphinx doc-string issues, and runs `black` formatting). **If the script fails**, stop and report the error to the user. Do not continue. Do not attempt to analyze the script failures and fix them with Copilot. The script should be fixed by the engineering team if it is not working. @@ -174,7 +170,20 @@ In the folder `sdk\ai\azure-ai-projects`, run `pip install -e .` to install the --- -## Step 12: Commit and push +## Step 12: Run `apiview-stub-generator` to update api.md and api.metadata.yml files + +In the root of the `azure-sdk-for-python` folder run the following commands + +``` +azpysdk apistub --md --extract-metadata azure-ai-projects --dest-dir . +``` + +This will update the `api.md` and `api.metadata.yml` files under in the package folder `sdk\ai\azure-ai-projects`. Now change directory +back to the package folder. + +--- + +## Step 13: Commit and push Stage all changes (excluding file names that start with `.env`), commit, and push the topic branch: @@ -188,7 +197,7 @@ git push -u origin --- -## Step 13: Create a Pull Request +## Step 14: Create a Pull Request Create a draft PR from the **topic branch** to the **base branch** (recorded in Step 2): @@ -205,7 +214,7 @@ Open a new tab in the default browser and navigate to the PR URL. --- -## Step 14: Optionally run tests locally +## Step 15: Optionally run tests locally Prompt the user with this message: "Tests will run as part of the Pull Request. However, you can optionally run tests locally in a Python virtual environment, right now. It will take a few minutes. Do you want to run tests locally? (yes/no)" diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 33ebdfcbcfba..bbb6dbc75839 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -4,7 +4,8 @@ ### Features Added -* Hosted agents are now stable. There is need to set `allow_preview=True` on the `AIProjectClient` constructor to create a Hosted agent. +* Two new methods `enable` and `disable` on the `.agents` subclient. +* Hosted agents are now stable. There is no need to set `allow_preview=True` on the `AIProjectClient` constructor to create a Hosted agent. * Toolboxes operations are now stable. The have moved from `.beta.toolboxes` subclient to the `.toolboxes` subclient. * Session and Session files operations are now stable. They have moved from the `.beta.agents` subclient to the `.agents` subclient. * Agent code operations are now stable. This includes `create_version_from_code` and `download_code`. They have moved from the `.beta.agents` subclient to the `.agents` subclient. diff --git a/sdk/ai/azure-ai-projects/api.md b/sdk/ai/azure-ai-projects/api.md index 8c9fe3dcc5a2..b4dfd3138297 100644 --- a/sdk/ai/azure-ai-projects/api.md +++ b/sdk/ai/azure-ai-projects/api.md @@ -250,6 +250,13 @@ namespace azure.ai.projects.aio.operations **kwargs: Any ) -> DeleteAgentVersionResponse: ... + @distributed_trace_async + async def disable( + self, + agent_name: str, + **kwargs: Any + ) -> None: ... + @distributed_trace_async async def download_code( self, @@ -270,6 +277,13 @@ namespace azure.ai.projects.aio.operations **kwargs: Any ) -> AsyncIterator[bytes]: ... + @distributed_trace_async + async def enable( + self, + agent_name: str, + **kwargs: Any + ) -> None: ... + @distributed_trace_async async def get( self, @@ -2504,6 +2518,7 @@ namespace azure.ai.projects.models instance_identity: Optional[AgentIdentity] name: str object: Literal[AgentObjectType.AGENT] + state: Union[str, AgentState] versions: AgentObjectVersions @overload @@ -2674,6 +2689,11 @@ namespace azure.ai.projects.models UPDATING = "updating" + class azure.ai.projects.models.AgentState(str, Enum, metaclass=CaseInsensitiveEnumMeta): + DISABLED = "disabled" + ENABLED = "enabled" + + class azure.ai.projects.models.AgentTaxonomyInput(EvaluationTaxonomyInput, discriminator='agent'): risk_categories: list[Union[str, RiskCategory]] target: EvaluationTarget @@ -6599,7 +6619,6 @@ namespace azure.ai.projects.models lora_config: Optional[LoraConfig] name: str source: Optional[ModelSourceData] - system_data: Optional[SystemDataV3] tags: Optional[dict[str, str]] version: str warnings: Optional[list[FoundryModelWarning]] @@ -7474,6 +7493,25 @@ namespace azure.ai.projects.models def __init__(self, mapping: Mapping[str, Any]) -> None: ... + class azure.ai.projects.models.ReminderPreviewTool(Tool, discriminator='reminder_preview'): + description: Optional[str] + name: Optional[str] + tool_configs: Optional[dict[str, ToolConfig]] + type: Literal[ToolType.REMINDER_PREVIEW] + + @overload + def __init__( + self, + *, + description: Optional[str] = ..., + name: Optional[str] = ..., + tool_configs: Optional[dict[str, ToolConfig]] = ... + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: ... + + class azure.ai.projects.models.ResponseRetrievalItemGenerationParams(TypedDict, total=False): key "data_mapping": Required[Dict[str, str]] key "max_num_turns": int @@ -8087,26 +8125,6 @@ namespace azure.ai.projects.models def __init__(self, mapping: Mapping[str, Any]) -> None: ... - class azure.ai.projects.models.SystemDataV3(_Model): - created_at: Optional[datetime] - created_by: Optional[str] - created_by_type: Optional[str] - last_modified_at: Optional[datetime] - - @overload - def __init__( - self, - *, - created_at: Optional[datetime] = ..., - created_by: Optional[str] = ..., - created_by_type: Optional[str] = ..., - last_modified_at: Optional[datetime] = ... - ) -> None: ... - - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: ... - - class azure.ai.projects.models.TargetCompletionEvalRunDataSource(TypedDict, total=False): key "input_messages": Required[InputMessagesItemReference] key "source": Required[Union[SourceFileContent, SourceFileID]] @@ -8597,6 +8615,7 @@ namespace azure.ai.projects.models MEMORY_SEARCH_PREVIEW = "memory_search_preview" NAMESPACE = "namespace" OPENAPI = "openapi" + REMINDER_PREVIEW = "reminder_preview" SHAREPOINT_GROUNDING_PREVIEW = "sharepoint_grounding_preview" SHELL = "shell" TOOLBOX_SEARCH_PREVIEW = "toolbox_search_preview" @@ -9297,6 +9316,13 @@ namespace azure.ai.projects.operations **kwargs: Any ) -> DeleteAgentVersionResponse: ... + @distributed_trace + def disable( + self, + agent_name: str, + **kwargs: Any + ) -> None: ... + @distributed_trace def download_code( self, @@ -9317,6 +9343,13 @@ namespace azure.ai.projects.operations **kwargs: Any ) -> Iterator[bytes]: ... + @distributed_trace + def enable( + self, + agent_name: str, + **kwargs: Any + ) -> None: ... + @distributed_trace def get( self, diff --git a/sdk/ai/azure-ai-projects/api.metadata.yml b/sdk/ai/azure-ai-projects/api.metadata.yml index 538384a3372c..a3999266fd1d 100644 --- a/sdk/ai/azure-ai-projects/api.metadata.yml +++ b/sdk/ai/azure-ai-projects/api.metadata.yml @@ -1,3 +1,3 @@ -apiMdSha256: 2515589fc688e1a9369aadfd0922bb686ddc1081f2fc8e4ebbd4241238336162 +apiMdSha256: b60afd274d28a27c40d16bedb18fe1688ea2b4013d226a5ebc12d01d649b7221 parserVersion: 0.3.28 pythonVersion: 3.14.3 diff --git a/sdk/ai/azure-ai-projects/apiview-properties.json b/sdk/ai/azure-ai-projects/apiview-properties.json index 699506d9a4d0..54446addbddb 100644 --- a/sdk/ai/azure-ai-projects/apiview-properties.json +++ b/sdk/ai/azure-ai-projects/apiview-properties.json @@ -267,6 +267,7 @@ "azure.ai.projects.models.Reasoning": "OpenAI.Reasoning", "azure.ai.projects.models.RecurrenceTrigger": "Azure.AI.Projects.RecurrenceTrigger", "azure.ai.projects.models.RedTeam": "Azure.AI.Projects.RedTeam", + "azure.ai.projects.models.ReminderPreviewTool": "Azure.AI.Projects.ReminderPreviewTool", "azure.ai.projects.models.ResponseUsageInputTokensDetails": "OpenAI.ResponseUsageInputTokensDetails", "azure.ai.projects.models.ResponseUsageOutputTokensDetails": "OpenAI.ResponseUsageOutputTokensDetails", "azure.ai.projects.models.Routine": "Azure.AI.Projects.Routine", @@ -291,7 +292,6 @@ "azure.ai.projects.models.SpecificFunctionShellParam": "OpenAI.SpecificFunctionShellParam", "azure.ai.projects.models.StructuredInputDefinition": "Azure.AI.Projects.StructuredInputDefinition", "azure.ai.projects.models.StructuredOutputDefinition": "Azure.AI.Projects.StructuredOutputDefinition", - "azure.ai.projects.models.SystemDataV3": "Azure.AI.Projects.SystemDataV3", "azure.ai.projects.models.TaxonomyCategory": "Azure.AI.Projects.TaxonomyCategory", "azure.ai.projects.models.TaxonomySubCategory": "Azure.AI.Projects.TaxonomySubCategory", "azure.ai.projects.models.TelemetryConfig": "Azure.AI.Projects.TelemetryConfig", @@ -400,6 +400,7 @@ "azure.ai.projects.models.DataGenerationJobOutputType": "Azure.AI.Projects.DataGenerationJobOutputType", "azure.ai.projects.models.OptimizationDatasetInputType": "Azure.AI.Projects.OptimizationDatasetInputType", "azure.ai.projects.models.AgentObjectType": "Azure.AI.Projects.AgentObjectType", + "azure.ai.projects.models.AgentState": "Azure.AI.Projects.AgentState", "azure.ai.projects.models.AgentKind": "Azure.AI.Projects.AgentKind", "azure.ai.projects.models.AgentProtocol": "Azure.AI.Projects.AgentProtocol", "azure.ai.projects.models.CodeDependencyResolution": "Azure.AI.Projects.CodeDependencyResolution", @@ -448,6 +449,10 @@ "azure.ai.projects.aio.operations.AgentsOperations.create_version_from_code": "Azure.AI.Projects.Agents.createAgentVersionFromCode", "azure.ai.projects.operations.AgentsOperations.download_code": "Azure.AI.Projects.Agents.downloadAgentCode", "azure.ai.projects.aio.operations.AgentsOperations.download_code": "Azure.AI.Projects.Agents.downloadAgentCode", + "azure.ai.projects.operations.AgentsOperations.enable": "Azure.AI.Projects.Agents.enableAgent", + "azure.ai.projects.aio.operations.AgentsOperations.enable": "Azure.AI.Projects.Agents.enableAgent", + "azure.ai.projects.operations.AgentsOperations.disable": "Azure.AI.Projects.Agents.disableAgent", + "azure.ai.projects.aio.operations.AgentsOperations.disable": "Azure.AI.Projects.Agents.disableAgent", "azure.ai.projects.operations.AgentsOperations.create_session": "Azure.AI.Projects.Agents.createSession", "azure.ai.projects.aio.operations.AgentsOperations.create_session": "Azure.AI.Projects.Agents.createSession", "azure.ai.projects.operations.AgentsOperations.get_session": "Azure.AI.Projects.Agents.getSession", @@ -521,5 +526,5 @@ "azure.ai.projects.operations.ToolboxesOperations.delete_version": "Azure.AI.Projects.Toolboxes.deleteToolboxVersion", "azure.ai.projects.aio.operations.ToolboxesOperations.delete_version": "Azure.AI.Projects.Toolboxes.deleteToolboxVersion" }, - "CrossLanguageVersion": "a252b3ce0280" + "CrossLanguageVersion": "623ba64ba559" } \ No newline at end of file diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 5d1358cbf42f..703fd4fce265 100644 --- a/sdk/ai/azure-ai-projects/assets.json +++ b/sdk/ai/azure-ai-projects/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/ai/azure-ai-projects", - "Tag": "python/ai/azure-ai-projects_3ec4cc5f02" + "Tag": "python/ai/azure-ai-projects_44980e76d0" } diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py index eb7342c68ebc..b0b0a81edc8c 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/operations/_operations.py @@ -45,8 +45,10 @@ build_agents_delete_session_file_request, build_agents_delete_session_request, build_agents_delete_version_request, + build_agents_disable_request, build_agents_download_code_request, build_agents_download_session_file_request, + build_agents_enable_request, build_agents_get_request, build_agents_get_session_log_stream_request, build_agents_get_session_request, @@ -1540,6 +1542,119 @@ async def download_code( return deserialized # type: ignore + @distributed_trace_async + async def enable(self, agent_name: str, **kwargs: Any) -> None: + """Enable an agent. + + Enables the specified agent, allowing it to accept new sessions and process requests. This + operation is idempotent — enabling an already-enabled agent returns success with no side + effects. + + :param agent_name: The name of the agent to enable. Required. + :type agent_name: str + :return: None + :rtype: None + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_agents_enable_request( + agent_name=agent_name, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.ApiErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if cls: + return cls(pipeline_response, None, {}) # type: ignore + + @distributed_trace_async + async def disable(self, agent_name: str, **kwargs: Any) -> None: + """Disable an agent. + + Disables the specified agent, preventing it from accepting new sessions or processing requests. + Existing active sessions are allowed to drain gracefully but no new sessions can be created. + This operation is idempotent — disabling an already-disabled agent returns success with no side + effects. + + :param agent_name: The name of the agent to disable. Required. + :type agent_name: str + :return: None + :rtype: None + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_agents_disable_request( + agent_name=agent_name, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.ApiErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if cls: + return cls(pipeline_response, None, {}) # type: ignore + @overload async def create_session( self, diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/models/__init__.py b/sdk/ai/azure-ai-projects/azure/ai/projects/models/__init__.py index 692d897c2926..92a37f0aada6 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/models/__init__.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/models/__init__.py @@ -271,6 +271,7 @@ RecurrenceTrigger, RedTeam, RedTeamTargetConfig, + ReminderPreviewTool, ResponseUsageInputTokensDetails, ResponseUsageOutputTokensDetails, Routine, @@ -298,7 +299,6 @@ SpecificFunctionShellParam, StructuredInputDefinition, StructuredOutputDefinition, - SystemDataV3, TaxonomyCategory, TaxonomySubCategory, TelemetryConfig, @@ -363,6 +363,7 @@ AgentObjectType, AgentProtocol, AgentSessionStatus, + AgentState, AgentVersionStatus, AttackStrategy, AzureAISearchQueryType, @@ -705,6 +706,7 @@ "RecurrenceTrigger", "RedTeam", "RedTeamTargetConfig", + "ReminderPreviewTool", "ResponseUsageInputTokensDetails", "ResponseUsageOutputTokensDetails", "Routine", @@ -732,7 +734,6 @@ "SpecificFunctionShellParam", "StructuredInputDefinition", "StructuredOutputDefinition", - "SystemDataV3", "TaxonomyCategory", "TaxonomySubCategory", "TelemetryConfig", @@ -794,6 +795,7 @@ "AgentObjectType", "AgentProtocol", "AgentSessionStatus", + "AgentState", "AgentVersionStatus", "AttackStrategy", "AzureAISearchQueryType", diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/models/_enums.py b/sdk/ai/azure-ai-projects/azure/ai/projects/models/_enums.py index a6ee0f86eca2..47fd25b8615c 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/models/_enums.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/models/_enums.py @@ -146,6 +146,15 @@ class AgentSessionStatus(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Session TTL exceeded (30 days from last activity).""" +class AgentState(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """The operational state of an agent.""" + + ENABLED = "enabled" + """Agent endpoint accepts requests. This is the default state on creation.""" + DISABLED = "disabled" + """Agent endpoint rejects all requests.""" + + class AgentVersionStatus(str, Enum, metaclass=CaseInsensitiveEnumMeta): """The provisioning status of an agent version.""" @@ -1129,6 +1138,8 @@ class ToolType(str, Enum, metaclass=CaseInsensitiveEnumMeta): """FABRIC_IQ_PREVIEW.""" TOOLBOX_SEARCH_PREVIEW = "toolbox_search_preview" """TOOLBOX_SEARCH_PREVIEW.""" + REMINDER_PREVIEW = "reminder_preview" + """REMINDER_PREVIEW.""" AZURE_AI_SEARCH = "azure_ai_search" """AZURE_AI_SEARCH.""" AZURE_FUNCTION = "azure_function" diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py b/sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py index e6fb3a3e2bbf..be438ce75dda 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/models/_models.py @@ -70,16 +70,18 @@ class Tool(_Model): CaptureStructuredOutputsTool, CodeInterpreterTool, ComputerTool, ComputerUsePreviewTool, CustomToolParam, MicrosoftFabricPreviewTool, FabricIQPreviewTool, FileSearchTool, FunctionTool, ImageGenTool, LocalShellToolParam, MCPTool, MemorySearchPreviewTool, NamespaceToolParam, - OpenApiTool, SharepointPreviewTool, FunctionShellToolParam, ToolSearchToolParam, - ToolboxSearchPreviewTool, WebSearchTool, WebSearchPreviewTool, WorkIQPreviewTool + OpenApiTool, ReminderPreviewTool, SharepointPreviewTool, FunctionShellToolParam, + ToolSearchToolParam, ToolboxSearchPreviewTool, WebSearchTool, WebSearchPreviewTool, + WorkIQPreviewTool :ivar type: Required. Known values are: "function", "file_search", "computer", "computer_use_preview", "web_search", "mcp", "code_interpreter", "image_generation", "local_shell", "shell", "custom", "namespace", "tool_search", "web_search_preview", "apply_patch", "a2a_preview", "bing_custom_search_preview", "browser_automation_preview", "fabric_dataagent_preview", "sharepoint_grounding_preview", "memory_search_preview", - "work_iq_preview", "fabric_iq_preview", "toolbox_search_preview", "azure_ai_search", - "azure_function", "bing_grounding", "capture_structured_outputs", and "openapi". + "work_iq_preview", "fabric_iq_preview", "toolbox_search_preview", "reminder_preview", + "azure_ai_search", "azure_function", "bing_grounding", "capture_structured_outputs", and + "openapi". :vartype type: str or ~azure.ai.projects.models.ToolType """ @@ -91,8 +93,8 @@ class Tool(_Model): \"apply_patch\", \"a2a_preview\", \"bing_custom_search_preview\", \"browser_automation_preview\", \"fabric_dataagent_preview\", \"sharepoint_grounding_preview\", \"memory_search_preview\", \"work_iq_preview\", \"fabric_iq_preview\", - \"toolbox_search_preview\", \"azure_ai_search\", \"azure_function\", \"bing_grounding\", - \"capture_structured_outputs\", and \"openapi\".""" + \"toolbox_search_preview\", \"reminder_preview\", \"azure_ai_search\", \"azure_function\", + \"bing_grounding\", \"capture_structured_outputs\", and \"openapi\".""" @overload def __init__( @@ -577,6 +579,9 @@ class AgentDetails(_Model): :vartype id: str :ivar name: The name of the agent. Required. :vartype name: str + :ivar state: The operational state of the agent. Controls whether the agent endpoint accepts or + rejects requests. Required. Known values are: "enabled" and "disabled". + :vartype state: str or ~azure.ai.projects.models.AgentState :ivar versions: The latest version of the agent. Required. :vartype versions: ~azure.ai.projects.models.AgentObjectVersions :ivar agent_endpoint: The endpoint configuration for the agent. @@ -597,6 +602,9 @@ class AgentDetails(_Model): """The unique identifier of the agent. Required.""" name: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) """The name of the agent. Required.""" + state: Union[str, "_models.AgentState"] = rest_field(visibility=["read"]) + """The operational state of the agent. Controls whether the agent endpoint accepts or rejects + requests. Required. Known values are: \"enabled\" and \"disabled\".""" versions: "_models.AgentObjectVersions" = rest_field(visibility=["read", "create", "update", "delete", "query"]) """The latest version of the agent. Required.""" agent_endpoint: Optional["_models.AgentEndpointConfig"] = rest_field( @@ -10011,8 +10019,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: class ModelVersion(_Model): """Model Version Definition. - :ivar system_data: System related metadata. - :vartype system_data: ~azure.ai.projects.models.SystemDataV3 :ivar blob_uri: URI of the model artifact in blob storage. Required. :vartype blob_uri: str :ivar weight_type: The weight type of the model. Known values are: "FullWeight", "LoRA", and @@ -10042,8 +10048,6 @@ class ModelVersion(_Model): :vartype tags: dict[str, str] """ - system_data: Optional["_models.SystemDataV3"] = rest_field(name="systemData", visibility=["read"]) - """System related metadata.""" blob_uri: str = rest_field(name="blobUri", visibility=["read", "create", "update", "delete", "query"]) """URI of the model artifact in blob storage. Required.""" weight_type: Optional[Union[str, "_models.FoundryModelWeightType"]] = rest_field( @@ -12057,6 +12061,58 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) +class ReminderPreviewTool(Tool, discriminator="reminder_preview"): + """A built-in tool that schedules the agent to re-invoke itself after a delay. The model passes a + single ``minutes`` argument (positive integer) when calling this tool. The service creates a + one-shot timer routine that fires after the specified delay and re-invokes the agent on the + same conversation thread. No pre-created routine is required. + + :ivar type: The type of the tool. Always ``reminder_preview``. Required. REMINDER_PREVIEW. + :vartype type: str or ~azure.ai.projects.models.REMINDER_PREVIEW + :ivar name: Optional user-defined name for this tool or configuration. + :vartype name: str + :ivar description: Optional user-defined description for this tool or configuration. + :vartype description: str + :ivar tool_configs: Per-tool configuration map. Keys are tool names or ``*`` (catch-all + default). Resolution order: exact tool name match takes priority over ``*``. Unknown tool names + are silently ignored at runtime. + :vartype tool_configs: dict[str, ~azure.ai.projects.models.ToolConfig] + """ + + type: Literal[ToolType.REMINDER_PREVIEW] = rest_discriminator(name="type", visibility=["read", "create", "update", "delete", "query"]) # type: ignore + """The type of the tool. Always ``reminder_preview``. Required. REMINDER_PREVIEW.""" + name: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """Optional user-defined name for this tool or configuration.""" + description: Optional[str] = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """Optional user-defined description for this tool or configuration.""" + tool_configs: Optional[dict[str, "_models.ToolConfig"]] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """Per-tool configuration map. Keys are tool names or ``*`` (catch-all default). Resolution order: + exact tool name match takes priority over ``*``. Unknown tool names are silently ignored at + runtime.""" + + @overload + def __init__( + self, + *, + name: Optional[str] = None, + description: Optional[str] = None, + tool_configs: Optional[dict[str, "_models.ToolConfig"]] = None, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.type = ToolType.REMINDER_PREVIEW # type: ignore + + class ResponseUsageInputTokensDetails(_Model): """ResponseUsageInputTokensDetails. @@ -12699,10 +12755,10 @@ class SessionLogEvent(_Model): .. code-block:: event: log - data: {"timestamp":"2026-03-10T09:33:17.121Z","stream":"stdout","message":"Starting server on port 18080"} + data: {"timestamp":"2026-03-10T09:33:17.121Z","stream":"stdout","message":"Starting server event: log - data: {"timestamp":"2026-03-10T09:34:52.714Z","stream":"status","message":"Successfully connected to container"} + data: {"timestamp":"2026-03-10T09:34:52.714Z","stream":"status","message":"Successfully :ivar event: The SSE event type. Currently ``log``, but additional event types may be added in the future. Clients should ignore unrecognized event types. Required. "log" @@ -13271,55 +13327,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) -class SystemDataV3(_Model): - """System metadata for a resource. - - :ivar created_at: Timestamp of resource creation. - :vartype created_at: ~datetime.datetime - :ivar created_by: Identity that created the resource. - :vartype created_by: str - :ivar created_by_type: Type of identity that created the resource. - :vartype created_by_type: str - :ivar last_modified_at: Timestamp of last resource modification. - :vartype last_modified_at: ~datetime.datetime - """ - - created_at: Optional[datetime.datetime] = rest_field( - name="createdAt", visibility=["read", "create", "update", "delete", "query"], format="unix-timestamp" - ) - """Timestamp of resource creation.""" - created_by: Optional[str] = rest_field(name="createdBy", visibility=["read", "create", "update", "delete", "query"]) - """Identity that created the resource.""" - created_by_type: Optional[str] = rest_field( - name="createdByType", visibility=["read", "create", "update", "delete", "query"] - ) - """Type of identity that created the resource.""" - last_modified_at: Optional[datetime.datetime] = rest_field( - name="lastModifiedAt", visibility=["read", "create", "update", "delete", "query"], format="unix-timestamp" - ) - """Timestamp of last resource modification.""" - - @overload - def __init__( - self, - *, - created_at: Optional[datetime.datetime] = None, - created_by: Optional[str] = None, - created_by_type: Optional[str] = None, - last_modified_at: Optional[datetime.datetime] = None, - ) -> None: ... - - @overload - def __init__(self, mapping: Mapping[str, Any]) -> None: - """ - :param mapping: raw JSON to initialize the model. - :type mapping: Mapping[str, Any] - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - class TaxonomyCategory(_Model): """Taxonomy category definition. diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py index af32fd3d4cd9..dd229b438bb2 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/operations/_operations.py @@ -368,6 +368,42 @@ def build_agents_download_code_request( return HttpRequest(method="GET", url=_url, params=_params, headers=_headers, **kwargs) +def build_agents_enable_request(agent_name: str, **kwargs: Any) -> HttpRequest: + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "v1")) + # Construct URL + _url = "/agents/{agent_name}:enable" + path_format_arguments = { + "agent_name": _SERIALIZER.url("agent_name", agent_name, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + return HttpRequest(method="POST", url=_url, params=_params, **kwargs) + + +def build_agents_disable_request(agent_name: str, **kwargs: Any) -> HttpRequest: + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "v1")) + # Construct URL + _url = "/agents/{agent_name}:disable" + path_format_arguments = { + "agent_name": _SERIALIZER.url("agent_name", agent_name, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + return HttpRequest(method="POST", url=_url, params=_params, **kwargs) + + def build_agents_create_session_request( agent_name: str, *, user_isolation_key: Optional[str] = None, **kwargs: Any ) -> HttpRequest: @@ -4959,6 +4995,119 @@ def download_code(self, agent_name: str, *, agent_version: Optional[str] = None, return deserialized # type: ignore + @distributed_trace + def enable(self, agent_name: str, **kwargs: Any) -> None: # pylint: disable=inconsistent-return-statements + """Enable an agent. + + Enables the specified agent, allowing it to accept new sessions and process requests. This + operation is idempotent — enabling an already-enabled agent returns success with no side + effects. + + :param agent_name: The name of the agent to enable. Required. + :type agent_name: str + :return: None + :rtype: None + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_agents_enable_request( + agent_name=agent_name, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.ApiErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if cls: + return cls(pipeline_response, None, {}) # type: ignore + + @distributed_trace + def disable(self, agent_name: str, **kwargs: Any) -> None: # pylint: disable=inconsistent-return-statements + """Disable an agent. + + Disables the specified agent, preventing it from accepting new sessions or processing requests. + Existing active sessions are allowed to drain gracefully but no new sessions can be created. + This operation is idempotent — disabling an already-disabled agent returns success with no side + effects. + + :param agent_name: The name of the agent to disable. Required. + :type agent_name: str + :return: None + :rtype: None + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[None] = kwargs.pop("cls", None) + + _request = build_agents_disable_request( + agent_name=agent_name, + api_version=self._config.api_version, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "endpoint": self._serialize.url("self._config.endpoint", self._config.endpoint, "str", skip_quote=True), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _stream = False + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.ApiErrorResponse, + response, + ) + raise HttpResponseError(response=response, model=error) + + if cls: + return cls(pipeline_response, None, {}) # type: ignore + @overload def create_session( self, diff --git a/sdk/ai/azure-ai-projects/docs/public-methods.md b/sdk/ai/azure-ai-projects/docs/public-methods.md index 6bce7cefb456..cc611dfeafc6 100644 --- a/sdk/ai/azure-ai-projects/docs/public-methods.md +++ b/sdk/ai/azure-ai-projects/docs/public-methods.md @@ -4,20 +4,20 @@ This document lists all public methods available on `AIProjectClient` and its su ## Summary -There are a total of 139 unique public methods: +There are a total of 141 unique public methods: - 5 stable methods on the client -- 53 stable methods on top-level sub-clients +- 55 stable methods on top-level sub-clients - 81 beta methods on nested beta sub-clients ### Top-level sub-clients (stable operations) | Subclient | Class Name | Methods Count | |-----------|------------|----------------| -| `agents` | AgentsOperations | 21 | -| `evaluation_rules` | EvaluationRulesOperations | 4 | +| `agents` | AgentsOperations | 23 | | `connections` | ConnectionsOperations | 3 | | `datasets` | DatasetsOperations | 9 | | `deployments` | DeploymentsOperations | 2 | +| `evaluation_rules` | EvaluationRulesOperations | 4 | | `indexes` | IndexesOperations | 5 | | `telemetry` | TelemetryOperations | 1 | | `toolboxes` | ToolboxesOperations | 8 | @@ -41,7 +41,7 @@ There are a total of 139 unique public methods: ## Stable methods on the client -An asterisk at the end of the method name means is a hand-written method. +Alphabetically sorted. An asterisk at the end of the method name means is a hand-written method. ``` .__enter__ @@ -64,8 +64,10 @@ Alphabetically sorted. An asterisk at the end of the method name means is a hand .agents.delete_session .agents.delete_session_file .agents.delete_version +.agents.disable .agents.download_code .agents.download_session_file +.agents.enable .agents.get .agents.get_session .agents.get_session_log_stream diff --git a/sdk/ai/azure-ai-projects/tests/agents/test_agents_crud.py b/sdk/ai/azure-ai-projects/tests/agents/test_agents_crud.py index 702d7786fbdb..62f7eb302cc2 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/test_agents_crud.py +++ b/sdk/ai/azure-ai-projects/tests/agents/test_agents_crud.py @@ -7,7 +7,7 @@ import json import io from test_base import TestBase, servicePreparer -from devtools_testutils import recorded_by_proxy +from devtools_testutils import recorded_by_proxy, RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, AgentDetails, AgentVersionDetails @@ -111,3 +111,99 @@ def test_agents_crud(self, **kwargs): agent_name=second_agent_name, agent_version=agent2_version1.version ) assert result.deleted + + # To run this test: + # pytest tests\agents\test_agents_crud.py::TestAgentCrud::test_agent_disable_enable -s + @servicePreparer() + @recorded_by_proxy(RecordedTransport.AZURE_CORE, RecordedTransport.HTTPX) + def test_agent_disable_enable(self, **kwargs): + """ + Test disable and enable operations for Agents. + + This test creates an agent, verifies it can respond to requests, + disables it and verifies requests fail, then enables it and + verifies requests work again. + + Routes used in this test: + + Action REST API Route Client Method + ------+---------------------------------------------+----------------------------------- + POST /agents/{agent_name}/versions project_client.agents.create_version() + POST /openai/conversations openai_client.conversations.create() + POST /openai/responses openai_client.responses.create() + POST /agents/{agent_name}:disable project_client.agents.disable() + POST /agents/{agent_name}:enable project_client.agents.enable() + DELETE /agents/{agent_name}/versions/{agent_version} project_client.agents.delete_version() + """ + print("\n") + model = kwargs.get("foundry_model_name") + agent_name = "DisableEnableTestAgent" + + # Setup + project_client = self.create_client(operation_group="agents", **kwargs) + openai_client = project_client.get_openai_client() + + # Create an Agent + agent = project_client.agents.create_version( + agent_name=agent_name, + definition=PromptAgentDefinition( + model=model, + instructions="You are a helpful assistant that answers general questions", + ), + ) + print(f"Agent created (id: {agent.id}, name: {agent.name}, version: {agent.version})") + self._validate_agent_version(agent) + + # Create a conversation + conversation = openai_client.conversations.create( + items=[{"type": "message", "role": "user", "content": "How many feet in a mile?"}] + ) + print(f"Created conversation with initial user message (id: {conversation.id})") + + # Verify the agent can respond to requests + response = openai_client.responses.create( + conversation=conversation.id, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + print(f"Response id: {response.id}, output text: {response.output_text}") + assert "5280" in response.output_text or "5,280" in response.output_text + + # Disable the agent + project_client.agents.disable(agent_name=agent_name) + print(f"Agent disabled") + + # Verify requests fail when agent is disabled + # TODO: Why does this call succeed, even though the Agent is disabled? + # error_raised = False + # try: + # _ = openai_client.responses.create( + # conversation=conversation.id, + # extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + # ) + # except Exception as e: + # error_raised = True + # print(f"Expected error when calling disabled agent: {e}") + # assert error_raised, "Expected an error when calling a disabled agent" + + # Enable the agent + project_client.agents.enable(agent_name=agent_name) + print(f"Agent enabled") + + # Add a new message to the conversation for the next request + _ = openai_client.conversations.items.create( + conversation.id, + items=[{"type": "message", "role": "user", "content": "And how many meters?"}], + ) + + # Verify the agent can respond to requests again + response = openai_client.responses.create( + conversation=conversation.id, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + print(f"Response id: {response.id}, output text: {response.output_text}") + assert "1609" in response.output_text or "1,609" in response.output_text + + # Cleanup - delete the agent + result = project_client.agents.delete_version(agent_name=agent_name, agent_version=agent.version) + assert result.deleted + print(f"Agent deleted") diff --git a/sdk/ai/azure-ai-projects/tests/agents/test_agents_crud_async.py b/sdk/ai/azure-ai-projects/tests/agents/test_agents_crud_async.py index 7dae5621f724..c66100031aa9 100644 --- a/sdk/ai/azure-ai-projects/tests/agents/test_agents_crud_async.py +++ b/sdk/ai/azure-ai-projects/tests/agents/test_agents_crud_async.py @@ -8,6 +8,7 @@ import io from test_base import TestBase, servicePreparer from devtools_testutils.aio import recorded_by_proxy_async +from devtools_testutils import RecordedTransport from azure.ai.projects.models import PromptAgentDefinition, AgentDetails, AgentVersionDetails @@ -100,3 +101,100 @@ async def test_agents_crud_async(self, **kwargs): agent_name=second_agent_name, agent_version=agent2_version1.version ) assert result.deleted + + # To run this test: + # pytest tests\agents\test_agents_crud_async.py::TestAgentCrudAsync::test_agent_disable_enable_async -s + @servicePreparer() + @recorded_by_proxy_async(RecordedTransport.AZURE_CORE, RecordedTransport.HTTPX) + async def test_agent_disable_enable_async(self, **kwargs): + """ + Test disable and enable operations for Agents. + + This test creates an agent, verifies it can respond to requests, + disables it and verifies requests fail, then enables it and + verifies requests work again. + + Routes used in this test: + + Action REST API Route Client Method + ------+---------------------------------------------+----------------------------------- + POST /agents/{agent_name}/versions project_client.agents.create_version() + POST /openai/conversations openai_client.conversations.create() + POST /openai/responses openai_client.responses.create() + POST /agents/{agent_name}:disable project_client.agents.disable() + POST /agents/{agent_name}:enable project_client.agents.enable() + DELETE /agents/{agent_name}/versions/{agent_version} project_client.agents.delete_version() + """ + print("\n") + model = kwargs.get("foundry_model_name") + agent_name = "DisableEnableTestAgent" + + # Setup + project_client = self.create_async_client(operation_group="agents", **kwargs) + openai_client = project_client.get_openai_client() + + async with project_client: + # Create an Agent + agent = await project_client.agents.create_version( + agent_name=agent_name, + definition=PromptAgentDefinition( + model=model, + instructions="You are a helpful assistant that answers general questions", + ), + ) + print(f"Agent created (id: {agent.id}, name: {agent.name}, version: {agent.version})") + self._validate_agent_version(agent) + + # Create a conversation + conversation = await openai_client.conversations.create( + items=[{"type": "message", "role": "user", "content": "How many feet in a mile?"}] + ) + print(f"Created conversation with initial user message (id: {conversation.id})") + + # Verify the agent can respond to requests + response = await openai_client.responses.create( + conversation=conversation.id, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + print(f"Response id: {response.id}, output text: {response.output_text}") + assert "5280" in response.output_text or "5,280" in response.output_text + + # Disable the agent + await project_client.agents.disable(agent_name=agent_name) + print("Agent disabled") + + # Verify requests fail when agent is disabled + # TODO: Why does this call succeed, even though the Agent is disabled? + # error_raised = False + # try: + # _ = await openai_client.responses.create( + # conversation=conversation.id, + # extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + # ) + # except Exception as e: + # error_raised = True + # print(f"Expected error when calling disabled agent: {e}") + # assert error_raised, "Expected an error when calling a disabled agent" + + # Enable the agent + await project_client.agents.enable(agent_name=agent_name) + print("Agent enabled") + + # Add a new message to the conversation for the next request + _ = await openai_client.conversations.items.create( + conversation.id, + items=[{"type": "message", "role": "user", "content": "And how many meters?"}], + ) + + # Verify the agent can respond to requests again + response = await openai_client.responses.create( + conversation=conversation.id, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + print(f"Response id: {response.id}, output text: {response.output_text}") + assert "1609" in response.output_text or "1,609" in response.output_text + + # Cleanup - delete the agent + result = await project_client.agents.delete_version(agent_name=agent_name, agent_version=agent.version) + assert result.deleted + print("Agent deleted") diff --git a/sdk/ai/azure-ai-projects/tsp-location.yaml b/sdk/ai/azure-ai-projects/tsp-location.yaml index 8991217d553c..2ba75c97059f 100644 --- a/sdk/ai/azure-ai-projects/tsp-location.yaml +++ b/sdk/ai/azure-ai-projects/tsp-location.yaml @@ -1,5 +1,5 @@ directory: specification/ai-foundry/data-plane/Foundry/src/sdk-python-js-azure-ai-projects -commit: ed970345c89c8758b74b08f740067cc2d854ed26 +commit: 90a82ad9585909f403d18768665f74f128e36ac7 repo: Azure/azure-rest-api-specs additionalDirectories: - specification/ai-foundry/data-plane/Foundry/src/agents