diff --git a/sdk/ai/azure-ai-projects/.env.template b/sdk/ai/azure-ai-projects/.env.template index 71237c4c0e44..a89bd766f143 100644 --- a/sdk/ai/azure-ai-projects/.env.template +++ b/sdk/ai/azure-ai-projects/.env.template @@ -46,9 +46,14 @@ SHAREPOINT_USER_INPUT= FABRIC_USER_INPUT= BING_CUSTOM_USER_INPUT= A2A_USER_INPUT= +A2A_ENDPOINT= EVALUATOR_NAME= DATASET_NAME= DATASET_VERSION= +DATASET_VERSION_1= +DATASET_VERSION_2= +DATA_FOLDER= +DOWNLOAD_FOLDER= POLL_INTERVAL_SECONDS= EVAL_MODEL= OPTIMIZATION_MODEL= @@ -57,6 +62,44 @@ WORK_IQ_PROJECT_CONNECTION_ID= WORK_IQ_USER_INPUT= FABRIC_IQ_PROJECT_CONNECTION_ID= FABRIC_IQ_USER_INPUT= +AI_SEARCH_CONNECTION_NAME= +INDEX_NAME= +INDEX_VERSION= +FILE_PATH= +AGENT_ID= +APPINSIGHTS_RESOURCE_ID= +TRACE_LOOKBACK_HOURS= +FOUNDRY_AGENT_VERSION= +FOUNDRY_CONVERSATION_IDS= +FOUNDRY_TRACE_IDS= +FOUNDRY_INSTANT_MODEL_NAME= +FOUNDRY_REFERENCE_DATASET_NAME= +FOUNDRY_REFERENCE_DATASET_VERSION= +FOUNDRY_TRACES_WINDOW_DAYS= +AZURE_AI_PROJECT_ENDPOINT= +AZURE_AI_MODEL_DEPLOYMENT_NAME= +AZURE_AI_MODEL_DEPLOYMENT_NAME_FOR_AUDIO= +ENDPOINT_URL= +ENDPOINT_API_KEY= +ENDPOINT_APP_ID= +AZURE_RESOURCE_GROUP_NAME= +MODEL_PUBLISHER= +MODEL_VERSION= +MODEL_ENDPOINT= +MODEL_API_KEY= +COMPUTER_USE_MODEL_DEPLOYMENT_NAME= +STORAGE_INPUT_QUEUE_NAME= +STORAGE_OUTPUT_QUEUE_NAME= +STORAGE_QUEUE_SERVICE_ENDPOINT= +DELEGATED_USER_IDENTITY= +DELEGATED_USER_IDENTITY_2= +FOUNDRY_HOSTED_AGENT_REMOTE_BUILD= +GITHUB_CONNECTION_NAME= +GITHUB_PAT_TOKEN= +GITHUB_REPOSITORY_NAME= +GITHUB_USERNAME= +TEAMS_CONNECTION_NAME= +TEAMS_CHANNEL_URL= ####################################################################### # diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 22ae9928800a..8bc071660c72 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -1,5 +1,23 @@ # Release History +## 2.5.0 (Unreleased) + +### Features Added + +Placeholder + +### Breaking Changes + +Placeholder + +### Bugs Fixed + +Placeholder + +### Sample updates + +Placeholder + ## 2.4.0 (2026-07-24) ### Features Added diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_console.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_console.py new file mode 100644 index 000000000000..6d5656fb2cee --- /dev/null +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_console.py @@ -0,0 +1,28 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +"""Handwritten console helpers used by SDK customization code.""" + +from __future__ import annotations + +from typing import IO + + +def console_print( + *values: object, + sep: str | None = " ", + end: str | None = "\n", + file: IO[str] | None = None, + flush: bool = False, +) -> None: + """Mirror ``print`` behind a named helper for SDK console capture. + + The SDK patch modules alias this helper to ``print`` so contributors can + keep writing plain ``print(...)`` calls while tests still patch one module- + local symbol to distinguish SDK console output from sample-authored prints. + Keeping this helper in a handwritten module avoids placing custom behavior + in generated files that may be overwritten by the next emit. + """ + print(*values, sep=sep, end=end, file=file, flush=flush) diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py index 9796a5679697..6d655921c9e7 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_patch.py @@ -17,6 +17,7 @@ from azure.core.tracing.decorator import distributed_trace from azure.core.credentials import TokenCredential from azure.identity import get_bearer_token_provider +from ._console import console_print as print from ._client import AIProjectClient as AIProjectClientGenerated from .operations import TelemetryOperations from .models._patch import _BETA_OPERATION_FEATURE_HEADERS, _FOUNDRY_FEATURES_HEADER_NAME, _has_header_case_insensitive @@ -24,13 +25,6 @@ logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- -# Shared helpers used by both the sync and async AIProjectClient.get_openai_client() -# implementations. Defined at module level so the async client can import and reuse -# them without duplicating the logic. -# --------------------------------------------------------------------------- - - def _resolve_openai_base_url(config: Any, agent_name: Optional[str], kwargs: dict) -> str: """Resolve the base URL for the (Async)OpenAI client. diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_version.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_version.py index 3fa6b6d4831f..602c3a5f5b94 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_version.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "2.4.0" +VERSION = "2.5.0" diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py index ce80d545efa5..b821a17ea9bd 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/aio/_patch.py @@ -16,6 +16,7 @@ from azure.core.tracing.decorator import distributed_trace from azure.core.credentials_async import AsyncTokenCredential from azure.identity.aio import get_bearer_token_provider +from .._console import console_print as print from .._patch import ( _AuthSecretsFilter, _build_openai_user_agent, diff --git a/sdk/ai/azure-ai-projects/docs/tool-classes-removed-properties.md b/sdk/ai/azure-ai-projects/docs/tool-classes-removed-properties.md new file mode 100644 index 000000000000..7a0f4e3ebd2d --- /dev/null +++ b/sdk/ai/azure-ai-projects/docs/tool-classes-removed-properties.md @@ -0,0 +1,43 @@ +# Tool Classes: Removed Properties (v2.2.0 → v2.3.0) + +The following Tool-derived classes had properties **removed** in v2.3.0 compared to v2.2.0. These properties (`name`, `description`, `tool_configs`) now only exist on the corresponding `ToolboxTool` subclasses. + +## General Availability Tools + +| Class Name | Removed Properties | +|------------|-------------------| +| `AzureAISearchTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `AzureFunctionTool` | `tool_configs: Optional[dict[str, ToolConfig]]` | +| `BingGroundingTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `CaptureStructuredOutputsTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `CodeInterpreterTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `FileSearchTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `FunctionShellToolParam` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `FunctionTool` | *(no changes)* | +| `ImageGenTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `LocalShellToolParam` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `MCPTool` | `tool_configs: Optional[dict[str, ToolConfig]]` | +| `OpenApiTool` | `tool_configs: Optional[dict[str, ToolConfig]]` | +| `WebSearchTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +v +## Preview Tools + +| Class Name | Removed Properties | +|------------|-------------------| +| `A2APreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `BingCustomSearchPreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `BrowserAutomationPreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `ComputerUsePreviewTool` | *(no changes)* | +| `FabricIQPreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `MemorySearchPreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `MicrosoftFabricPreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `SharepointPreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `ToolboxSearchPreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | +| `WebSearchPreviewTool` | *(no changes)* | +| `WorkIQPreviewTool` | `name: Optional[str]`, `description: Optional[str]`, `tool_configs: Optional[dict[str, ToolConfig]]` | + +## Summary + +- **Total Tool classes analyzed:** 24 +- **Classes with removed properties:** 19 +- **Common pattern:** `name`, `description`, and `tool_configs` were moved exclusively to `ToolboxTool` subclasses diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic.py index ed5a094a4fb0..ce6fca327e74 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_basic.py @@ -45,7 +45,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" with ( DefaultAzureCredential() as credential, diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_retrieve_basic.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_retrieve_basic.py index 98fb61604346..0d221d8b177c 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_retrieve_basic.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_retrieve_basic.py @@ -40,7 +40,7 @@ endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] model = os.environ["FOUNDRY_MODEL_NAME"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" with ( DefaultAzureCredential() as credential, AIProjectClient(endpoint=endpoint, credential=credential) as project_client, diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_retrieve_basic_async.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_retrieve_basic_async.py index 94cafecfbb91..135ea9ed217f 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_retrieve_basic_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_retrieve_basic_async.py @@ -47,7 +47,7 @@ endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] model = os.environ["FOUNDRY_MODEL_NAME"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" async def main(): diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_stream_events.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_stream_events.py index f62263413ad6..1e8918f93e2c 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_stream_events.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_stream_events.py @@ -38,7 +38,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" with ( DefaultAzureCredential() as credential, diff --git a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_structured_output.py b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_structured_output.py index ed7f5cb01917..ac3520b3cefa 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/sample_agent_structured_output.py +++ b/sdk/ai/azure-ai-projects/samples/agents/sample_agent_structured_output.py @@ -54,7 +54,7 @@ class CalendarEvent(BaseModel): endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" with ( DefaultAzureCredential() as credential, diff --git a/sdk/ai/azure-ai-projects/samples/agents/telemetry/sample_agent_basic_with_azure_monitor_tracing.py b/sdk/ai/azure-ai-projects/samples/agents/telemetry/sample_agent_basic_with_azure_monitor_tracing.py index 251bd4a8fe0e..984fd972c116 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/telemetry/sample_agent_basic_with_azure_monitor_tracing.py +++ b/sdk/ai/azure-ai-projects/samples/agents/telemetry/sample_agent_basic_with_azure_monitor_tracing.py @@ -43,7 +43,7 @@ load_dotenv() agent = None -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" with ( DefaultAzureCredential() as credential, diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_ai_search.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_ai_search.py index 6eb6cbc49694..77c672838cbe 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_ai_search.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_ai_search.py @@ -44,7 +44,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = AzureAISearchTool( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_azure_function.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_azure_function.py index 5a7729dbf78f..d3ed1f546a49 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_azure_function.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_azure_function.py @@ -47,7 +47,7 @@ agent = None endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = AzureFunctionTool( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_bing_custom_search.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_bing_custom_search.py index 1ba8ddba0f8f..7fd09361524e 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_bing_custom_search.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_bing_custom_search.py @@ -53,7 +53,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = BingCustomSearchPreviewTool( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_bing_grounding.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_bing_grounding.py index 8fbf96112bfc..61399fa276e8 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_bing_grounding.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_bing_grounding.py @@ -58,7 +58,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = BingGroundingTool( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_browser_automation.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_browser_automation.py index 57645de43e4e..25585081f81b 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_browser_automation.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_browser_automation.py @@ -42,7 +42,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = BrowserAutomationPreviewTool( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter.py index 90de218037b1..3703d2a8a8e7 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter.py @@ -34,7 +34,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" with ( DefaultAzureCredential() as credential, diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter_async.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter_async.py index 0c54d31371fd..7b002282df3b 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter_async.py @@ -35,7 +35,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" async def main() -> None: diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter_with_files_async.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter_with_files_async.py index 49a7f1112856..fcd5b87c7c6a 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter_with_files_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_code_interpreter_with_files_async.py @@ -58,7 +58,7 @@ async def main() -> None: # Create agent with code interpreter tool agent = await project_client.agents.create_version( - agent_name=os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent"), + agent_name=os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent", definition=PromptAgentDefinition( model=os.environ["FOUNDRY_MODEL_NAME"], instructions="You are a helpful assistant.", diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_computer_use.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_computer_use.py index 27e46a98d574..0f2a733df0dc 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_computer_use.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_computer_use.py @@ -48,7 +48,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" # Initialize state machine current_state = SearchState.INITIAL diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_computer_use_async.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_computer_use_async.py index f4fcbd193e76..79960c8162c2 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_computer_use_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_computer_use_async.py @@ -48,7 +48,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" async def main(): diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_fabric.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_fabric.py index dc192ccfe614..bebed4e9a33d 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_fabric.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_fabric.py @@ -42,7 +42,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = MicrosoftFabricPreviewTool( fabric_dataagent_preview=FabricDataAgentToolParameters( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_fabric_iq.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_fabric_iq.py index 98eb521f9968..7c2d134925dd 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_fabric_iq.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_fabric_iq.py @@ -36,7 +36,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool_payload = FabricIQPreviewTool( project_connection_id=os.environ["FABRIC_IQ_PROJECT_CONNECTION_ID"], diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_file_search_structured_inputs.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_file_search_structured_inputs.py index c2e04b85efa5..6e24325aa01f 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_file_search_structured_inputs.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_file_search_structured_inputs.py @@ -84,7 +84,7 @@ # Create agent with file search tool agent = project_client.agents.create_version( - agent_name=os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent"), + agent_name=os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent", definition=agent_definition, description="File search agent for product information queries.", ) diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_function_tool.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_function_tool.py index 10ab7d0a258e..e6e9a9d417bf 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_function_tool.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_function_tool.py @@ -36,7 +36,7 @@ load_dotenv() -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" def get_horoscope(sign: str) -> str: diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_image_generation.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_image_generation.py index 408f13a33150..7dbf7ff139c4 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_image_generation.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_image_generation.py @@ -57,7 +57,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" image_generation_model = os.environ["IMAGE_GENERATION_MODEL_DEPLOYMENT_NAME"] tool = ImageGenTool( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_image_generation_async.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_image_generation_async.py index 2912dd74ee1a..5a7a034cf02f 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_image_generation_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_image_generation_async.py @@ -57,7 +57,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" image_generation_model = os.environ["IMAGE_GENERATION_MODEL_DEPLOYMENT_NAME"] diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp.py index 03ba643cf794..47844b570f30 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp.py @@ -35,7 +35,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" mcp_tool = MCPTool( server_label="api-specs", diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp_async.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp_async.py index 83eb878eb648..a3f12af3b7cd 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp_async.py @@ -36,7 +36,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" async def main(): diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp_with_project_connection.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp_with_project_connection.py index 69816267de85..5af6fb5756b6 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp_with_project_connection.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_mcp_with_project_connection.py @@ -37,7 +37,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = MCPTool( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_memory_search.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_memory_search.py index da339d10d3bd..a0dbfaec6ddf 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_memory_search.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_memory_search.py @@ -50,7 +50,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -AGENT_NAME = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +AGENT_NAME = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" credential = DefaultAzureCredential() project_client = AIProjectClient(endpoint=endpoint, credential=credential) diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_memory_search_async.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_memory_search_async.py index 7f151e983e4c..102be3034bb1 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_memory_search_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_memory_search_async.py @@ -49,7 +49,7 @@ load_dotenv() -AGENT_NAME = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +AGENT_NAME = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" async def main() -> None: diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_openapi.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_openapi.py index 068205c2c0e9..4003b68019e0 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_openapi.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_openapi.py @@ -42,7 +42,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" weather_asset_file_path = Path(__file__).resolve().parent / "../assets/weather_openapi.json" openapi_weather = cast(dict[str, Any], jsonref.loads(weather_asset_file_path.read_text(encoding="utf-8"))) diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_openapi_with_project_connection.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_openapi_with_project_connection.py index 03e3cb47836e..e48d5d83760e 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_openapi_with_project_connection.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_openapi_with_project_connection.py @@ -46,7 +46,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tripadvisor_asset_file_path = Path(__file__).resolve().parent / "../assets/tripadvisor_openapi.json" openapi_tripadvisor = cast(dict[str, Any], jsonref.loads(tripadvisor_asset_file_path.read_text(encoding="utf-8"))) diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_sharepoint.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_sharepoint.py index 77027e023598..d89890e835cb 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_sharepoint.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_sharepoint.py @@ -42,7 +42,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = SharepointPreviewTool( sharepoint_grounding_preview=SharepointGroundingToolParameters( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_to_agent.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_to_agent.py index e1287eb17f74..e7505acb3e7d 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_to_agent.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_to_agent.py @@ -44,7 +44,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = A2APreviewTool( project_connection_id=os.environ["A2A_PROJECT_CONNECTION_ID"], diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search.py index 91ff1fa42dbc..595fa2bf2dad 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search.py @@ -49,7 +49,7 @@ endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = WebSearchTool(user_location=WebSearchApproximateLocation(country="GB", city="London", region="London")) with ( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search_preview.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search_preview.py index 9ad225b182b7..819847e6379e 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search_preview.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search_preview.py @@ -45,7 +45,7 @@ endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = WebSearchPreviewTool(user_location=ApproximateLocation(country="GB", city="London", region="London")) with ( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search_with_custom_search.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search_with_custom_search.py index 04d94babb8c4..48a94b24d6fc 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search_with_custom_search.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_web_search_with_custom_search.py @@ -54,7 +54,7 @@ endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool = WebSearchTool( custom_search_configuration=WebSearchConfiguration( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_work_iq.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_work_iq.py index c5b48b80932f..2540a5b04d67 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_work_iq.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_agent_work_iq.py @@ -36,7 +36,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" tool_payload = WorkIQPreviewTool( project_connection_id=os.environ["WORK_IQ_PROJECT_CONNECTION_ID"], diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search.py index cc0bc3269510..49afda0725b7 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search.py @@ -56,7 +56,7 @@ INNER_MCP_LABEL = "github" INNER_MCP_URL = "https://api.githubcopilot.com/mcp" TOOLBOX_MCP_LABEL = "search-tool" -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" with ( diff --git a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_async.py b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_async.py index c5694a779f7d..575d0faf52b8 100644 --- a/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_async.py +++ b/sdk/ai/azure-ai-projects/samples/agents/tools/sample_toolboxes_with_search_async.py @@ -57,7 +57,7 @@ INNER_MCP_LABEL = "github" INNER_MCP_URL = "https://api.githubcopilot.com/mcp" TOOLBOX_MCP_LABEL = "search-tool" -agent_name = os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent") +agent_name = os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent" async def main() -> None: diff --git a/sdk/ai/azure-ai-projects/samples/evaluations/sample_agent_evaluation.py b/sdk/ai/azure-ai-projects/samples/evaluations/sample_agent_evaluation.py index 9c9f48b5f6a4..0a8562aa44bc 100644 --- a/sdk/ai/azure-ai-projects/samples/evaluations/sample_agent_evaluation.py +++ b/sdk/ai/azure-ai-projects/samples/evaluations/sample_agent_evaluation.py @@ -54,7 +54,7 @@ project_client.get_openai_client() as openai_client, ): agent = project_client.agents.create_version( - agent_name=os.environ.get("FOUNDRY_AGENT_NAME", "MyAgent"), + agent_name=os.environ.get("FOUNDRY_AGENT_NAME") or "MyAgent", definition=PromptAgentDefinition( model=model_deployment_name, instructions="You are a helpful assistant that answers general questions", diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/github_routine_util.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/github_routine_util.py new file mode 100644 index 000000000000..56c9e5f6b95d --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/github_routine_util.py @@ -0,0 +1,57 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +import json +import threading +import time +import urllib.error +import urllib.request +from urllib.parse import urlparse + +from azure.core.exceptions import ResourceNotFoundError + +from azure.ai.projects import AIProjectClient + + +def create_github_issue(owner: str, repository: str, token: str, *, title: str, assignee: str) -> None: + """Create and assign a GitHub issue using the REST API.""" + request = urllib.request.Request( + url=f"https://api.github.com/repos/{owner}/{repository}/issues", + data=json.dumps({"title": title, "assignees": [assignee]}).encode("utf-8"), + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "User-Agent": "azure-ai-projects-sample", + "X-GitHub-Api-Version": "2022-11-28", + }, + method="POST", + ) + + with urllib.request.urlopen(request, timeout=30) as response: + issue = json.load(response) + + print(f"Created GitHub issue #{issue['number']}: {issue['html_url']}") + + +def start_issue_creation_thread( + owner: str, repository: str, token: str | None, assignee: str, *, title: str = "Testing routine" +) -> threading.Thread | None: + """Create the trigger issue in parallel so polling can begin immediately.""" + if not token: + print("GITHUB_PAT_TOKEN is not set; skipping automatic issue creation.") + return None + + def worker() -> None: + try: + create_github_issue(owner, repository, token, title=title, assignee=assignee) + except urllib.error.HTTPError as exc: + details = exc.read().decode("utf-8", errors="replace") + print(f"Failed to create GitHub issue: {exc.code} {exc.reason} {details}") + except Exception as exc: # pylint: disable=broad-except + print(f"Failed to create GitHub issue: {exc}") + + thread = threading.Thread(target=worker, name="github-issue-trigger", daemon=True) + thread.start() + return thread diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/hosted_agents_util.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/hosted_agents_util.py index 36391144a2e9..7c64763a23e2 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/hosted_agents_util.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/hosted_agents_util.py @@ -1,4 +1,5 @@ import asyncio +import os import sys import time from contextlib import asynccontextmanager, contextmanager diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code.py index c1720fe35eb8..811db9151027 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code.py @@ -64,7 +64,7 @@ endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] model_name = os.environ["FOUNDRY_MODEL_NAME"] use_remote_build = os.environ.get("FOUNDRY_HOSTED_AGENT_REMOTE_BUILD", "true").strip().lower() == "true" -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" dependency_resolution, code_zip_stream = select_basic_agent_code_zip(use_remote_build) original_agent_endpoint = None diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_dispatch.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_dispatch.py index 408ffe971161..a875e1ee3c90 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_dispatch.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_dispatch.py @@ -67,7 +67,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" model_name = os.environ["FOUNDRY_MODEL_NAME"] dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py index d7a836f83bb3..a8e00cd2e5ea 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py @@ -14,9 +14,11 @@ version, routes the configured hosted agent name to that version, and then creates a routine configured with a `GitHubIssueRoutineTrigger`. The trigger uses a GitHub-compatible Foundry RemoteTool connection supplied through - `GITHUB_CONNECTION_NAME`. After creating the routine, open an issue in the - configured repository to fire it. The sample polls the routine run history - for a short period and then deletes the routine and hosted-agent version. + `GITHUB_CONNECTION_NAME`. If `GITHUB_PAT_TOKEN` is set, the sample also + creates a GitHub issue titled `Testing routine` and assigns it to the + configured owner to trigger the routine automatically; otherwise you can + open an issue manually. The sample polls the routine run history for a + short period and then deletes the routine and hosted-agent version. Routines are currently a preview feature. In the Python SDK, you access these operations via `project_client.beta.routines`. @@ -38,9 +40,12 @@ 4) GITHUB_CONNECTION_NAME - The Foundry GitHub RemoteTool connection name. The connection must be GitHub-compatible and use PAT or OAuth2 credentials. 5) GITHUB_USERNAME - The GitHub owner or organization name. - 6) GITHUB_REPOSITORY - The GitHub repository name in the format of https://github.com/xxx/xxx.git. + 6) GITHUB_REPOSITORY_NAME - The GitHub repository name from `https://github.com//`. 7) POLL_INTERVAL_SECONDS - Optional. Seconds to sleep between run-history polls. - Defaults to 10. + Defaults to 10. + 8) GITHUB_PAT_TOKEN - Optional. GitHub personal access token with permission + to create and assign issues. If set, the sample creates a `Testing routine` + issue and assigns it to `GITHUB_USERNAME` automatically. """ import json @@ -63,22 +68,25 @@ RoutineRun, ) +from github_routine_util import start_issue_creation_thread from hosted_agents_util import create_version_from_code, select_basic_agent_code_zip load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" model_name = os.environ["FOUNDRY_MODEL_NAME"] github_connection_name = os.environ["GITHUB_CONNECTION_NAME"] poll_interval_seconds = int(os.environ.get("POLL_INTERVAL_SECONDS", "10")) +github_pat_token = os.environ.get("GITHUB_PAT_TOKEN") github_owner = os.environ["GITHUB_USERNAME"] -github_repository = os.environ["GITHUB_REPOSITORY"] +github_repository_name = os.environ["GITHUB_REPOSITORY_NAME"] def main() -> None: dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) + issue_thread = None with ( code_zip_stream as code_stream, @@ -107,7 +115,7 @@ def main() -> None: ): routine_name = "sample-routine-github-issue" - print(f"Preparing routine `{routine_name}` for {github_repository}.") + print(f"Preparing routine `{routine_name}` for {github_repository_name}.") try: print(f"Deleting any existing routine `{routine_name}`.") project_client.beta.routines.delete(routine_name) @@ -124,19 +132,27 @@ def main() -> None: "on-issue": GitHubIssueRoutineTrigger( connection_id=github_connection_name, # Currently accepts a connection name. owner=github_owner, - repository=github_repository, + repository=github_repository_name, issue_event=GitHubIssueEvent.OPENED, ), }, action=InvokeAgentResponsesApiRoutineAction(agent_name=agent_name), ) + time.sleep(5) print( f"Created routine: {created.name} enabled={created.enabled} " - f"repo={github_owner}/{github_repository} event={GitHubIssueEvent.OPENED}" + f"repo={github_owner}/{github_repository_name} event={GitHubIssueEvent.OPENED}" ) - print(f"Open a GitHub issue in {github_repository} to fire the routine.") - print("Waiting for a routine run for up to 10 minutes...") + issue_thread = start_issue_creation_thread( + github_owner, + github_repository_name, + github_pat_token, + github_owner, + ) + print(f"Open a GitHub issue in {github_owner}/{github_repository_name} to fire the routine.") + + print("Waiting for a routine run for up to 10 minutes...") try: seen_phases: dict[str, str] = {} final_run: RoutineRun | None = None @@ -174,6 +190,8 @@ def main() -> None: except KeyboardInterrupt: print("Interrupted by user; cleaning up routine before exiting.") finally: + if issue_thread is not None: + issue_thread.join(timeout=5) try: project_client.beta.routines.delete(routine_name) print("Routine deleted") diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_schedule_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_schedule_trigger.py index 533581ebe8eb..ccb08ff99229 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_schedule_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_schedule_trigger.py @@ -71,7 +71,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" model_name = os.environ["FOUNDRY_MODEL_NAME"] poll_interval_seconds = int(os.environ.get("POLL_INTERVAL_SECONDS", "15")) dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_teams_message_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_teams_message_trigger.py index bf7daba193a9..41f84c043a56 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_teams_message_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_teams_message_trigger.py @@ -86,7 +86,7 @@ def parse_teams_channel_url(channel_url: str) -> tuple[str | None, str | None]: load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" model_name = os.environ["FOUNDRY_MODEL_NAME"] teams_connection_name = os.environ.get("TEAMS_CONNECTION_NAME", "teams-conn") teams_channel_url = os.environ["TEAMS_CHANNEL_URL"] diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py index b9b8256b4d4d..28a21e1b88da 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py @@ -63,7 +63,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" model_name = os.environ["FOUNDRY_MODEL_NAME"] dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream.py index ead42d994d88..b86587c910b7 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream.py @@ -44,7 +44,7 @@ load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" model_name = os.environ["FOUNDRY_MODEL_NAME"] hosted_agent_source_dir = Path(__file__).parent / "assets" / "basic-agent" diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download_async.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download_async.py index e8df03ad07d0..f2d484081c53 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download_async.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download_async.py @@ -49,7 +49,7 @@ async def main(): endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] - agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") + agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" model_name = os.environ["FOUNDRY_MODEL_NAME"] hosted_agent_source_dir = Path(__file__).parent / "assets" / "basic-agent" diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_toolbox_with_skill.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_toolbox_with_skill.py index 430f8fd0e46a..4b4101c90762 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_toolbox_with_skill.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_toolbox_with_skill.py @@ -64,7 +64,7 @@ endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] model_name = os.environ["FOUNDRY_MODEL_NAME"] subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"] -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME") or "MyHostedAgent" _HOSTED_AGENT_SOURCE_DIR = Path(__file__).parent / "assets" / "toolbox-agent" diff --git a/sdk/ai/azure-ai-projects/tests/samples/README.md b/sdk/ai/azure-ai-projects/tests/samples/README.md index a8c2ba7916cd..57991f9a8901 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/README.md +++ b/sdk/ai/azure-ai-projects/tests/samples/README.md @@ -11,22 +11,14 @@ Use recorded tests to validate samples with `SyncSampleExecutor` and `AsyncSampl ## Sample test logging -Optionally enable logging to capture sample execution results in log files (useful for monitoring and alerting): +In live runs, sample executors automatically create artifacts in the system temp directory: -```bash -# In .env - uncomment to enable logging -SAMPLE_TEST_ERROR_LOG=_errors_.log -SAMPLE_TEST_FAILED_LOG=_failed_.log -SAMPLE_TEST_PASSED_LOG=_success_.log -``` - -Log types: - -- **`SAMPLE_TEST_ERROR_LOG`**: Sample crashed with an exception during execution -- **`SAMPLE_TEST_FAILED_LOG`**: Sample ran successfully but LLM validation failed (incorrect output) -- **`SAMPLE_TEST_PASSED_LOG`**: Sample ran successfully and LLM validation passed (correct output) +- **`_errors_.log`**: Sample crashed with an exception during execution +- **`_failed_.log`**: Sample ran successfully but LLM validation failed +- **`_success_.log`**: Sample ran successfully and LLM validation passed +- **`_output_.txt`**: Captured sample `print()` output -Logs are written to the system's temp directory with the specified filename format. Each log includes the sample path, status/error details, exception traceback (for errors), and all captured print statements. +The `.log` files include the sample path, status/error details, exception traceback for error cases, and all captured `print()` and SDK `_print()` statements. The `.txt` file contains only the captured sample `print()` output. ## Sync example @@ -231,7 +223,7 @@ executor = SyncSampleExecutor( Behavior: -- **Samples in the allowlist:** Pass the test even when LLM validation fails. A warning message is printed to the console, and a failed report is still generated (if `SAMPLE_TEST_FAILED_LOG` is configured in `.env`). +- **Samples in the allowlist:** Pass the test even when LLM validation fails. A warning message is printed to the console, and a failed report is still generated. - **Samples not in the allowlist:** Fail the test when LLM validation fails (existing behavior). - **All samples:** Execution errors (exceptions) always fail the test, regardless of the allowlist. diff --git a/sdk/ai/azure-ai-projects/tests/samples/llm-analyze.py b/sdk/ai/azure-ai-projects/tests/samples/llm-analyze.py index aee1c9b7c86b..9fc71bada70d 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/llm-analyze.py +++ b/sdk/ai/azure-ai-projects/tests/samples/llm-analyze.py @@ -1,3 +1,4 @@ +# pylint: disable=line-too-long,invalid-name,import-error,no-name-in-module,wrong-import-position,wrong-import-order,missing-function-docstring,too-few-public-methods,too-many-locals # ------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -9,34 +10,50 @@ python .\\tests\\samples\\llm-analyze.py --sample-path="samples\\agents\\tools\\sample_agent_file_search.py" \ --foundry_project_endpoint="https://foundy6maq.services.ai.azure.com/api/projects/project6maq" \ --foundry_model_name="gpt-5" \ - --llm_endpoint="https://foundy6maq.services.ai.azure.com/api/projects/project6maq", \ + --llm_validation_project_endpoint="https://foundy6maq.services.ai.azure.com/api/projects/project6maq" \ --llm_model_name="gpt-5" +Example using environment variables: + python .\\tests\\samples\\llm-analyze.py --sample-path="samples\\agents\\tools\\sample_agent_file_search.py" + This uses `.env` or existing environment variables for values such as + `FOUNDRY_PROJECT_ENDPOINT`, `FOUNDRY_MODEL_NAME`, + `LLM_VALIDATION_PROJECT_ENDPOINT`, and `LLM_MODEL_NAME`. + If `LLM_MODEL_NAME` is not set, validation defaults to `gpt-5.2`. + Example JSON output: { "correct": true, "llm_comment": "Execution completed successfully with substantive output.", "log_file": "C:\\Users\\\\AppData\\Local\\Temp\\sample_agent_file_search_success_.log", + "captured_prints_file": "C:\\Users\\\\AppData\\Local\\Temp\\sample_agent_file_search_output_.txt", "duration": 117.912 } Notes: + - Values are loaded from `.env` first. Explicit CLI arguments override environment variables. + - `--sample-path` is the only dedicated CLI option. All other `--lower_case_name=value` arguments are mapped to uppercase environment variables and override `.env` values. + - Validation uses `LLM_VALIDATION_PROJECT_ENDPOINT` and `LLM_MODEL_NAME` only. + If `LLM_MODEL_NAME` is unset, the default validation model is `gpt-5.2`. - Extra lower-case CLI arguments are mapped to upper-case environment variables for the sample. - - Pass sample environment variables as extra CLI args using lower-case names. Examples: - --foundry_project_endpoint="https://.../api/projects/..." - --foundry_model_name="gpt-5" - --ai_search_index_name="index_sample" - These become: - FOUNDRY_PROJECT_ENDPOINT - FOUNDRY_MODEL_NAME - AI_SEARCH_INDEX_NAME - - The final output is JSON only: correctness, LLM comment, temp log path, and duration. + Pass sample environment variables as extra CLI args using lower-case names. Examples: + --foundry_project_endpoint="https://.../api/projects/..." + --foundry_model_name="gpt-5" + --llm_validation_project_endpoint="https://.../api/projects/..." + --llm_model_name="gpt-5" + --ai_search_index_name="index_sample" + These become: + FOUNDRY_PROJECT_ENDPOINT + FOUNDRY_MODEL_NAME + LLM_VALIDATION_PROJECT_ENDPOINT + LLM_MODEL_NAME + AI_SEARCH_INDEX_NAME + - The final output is JSON only: correctness, LLM comment, temp log path, captured prints path, and duration. """ from __future__ import annotations -import asyncio import argparse +import asyncio import json import os import sys @@ -44,28 +61,38 @@ from contextlib import contextmanager, redirect_stderr, redirect_stdout from io import BytesIO from pathlib import Path +from typing import cast -from azure.ai.projects import AIProjectClient -from azure.ai.projects.aio import AIProjectClient as AsyncAIProjectClient -from azure.core.credentials import TokenCredential -from azure.core.credentials_async import AsyncTokenCredential -from azure.identity import DefaultAzureCredential -from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential +from dotenv import load_dotenv SAMPLES_ROOT = Path(__file__).resolve().parent TESTS_ROOT = SAMPLES_ROOT.parent PROJECT_ROOT = TESTS_ROOT.parent + +load_dotenv(PROJECT_ROOT / ".env") + +sys.path.insert(0, str(PROJECT_ROOT)) +try: + from azure.ai.projects import AIProjectClient + from azure.ai.projects.aio import AIProjectClient as AsyncAIProjectClient +finally: + sys.path.pop(0) + sys.path.insert(0, str(TESTS_ROOT)) -from sample_executor import AsyncSampleExecutor, SyncSampleExecutor # pylint: disable=wrong-import-position +from azure.core.credentials import TokenCredential +from azure.core.credentials_async import AsyncTokenCredential +from azure.identity import DefaultAzureCredential +from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential + +from sample_executor import ( # pylint: disable=wrong-import-position + LIVE_LLM_VALIDATION_MODEL, + AsyncSampleExecutor, + SyncSampleExecutor, +) from test_base import patched_open_crlf_to_lf # pylint: disable=wrong-import-position -LOG_FILE_PATTERNS = { - "AZURE_TEST_RUN_LIVE": "true", - "SAMPLE_TEST_PASSED_LOG": "_success_.log", - "SAMPLE_TEST_FAILED_LOG": "_failed_.log", - "SAMPLE_TEST_ERROR_LOG": "_errors_.log", -} +LIVE_MODE_ENV = {"AZURE_TEST_RUN_LIVE": "true"} class _CredentialProvider: @@ -89,9 +116,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log_file_path: str | None = None - def _capture_print(self, *args, **_kwargs): - self.print_calls.append(" ".join(str(arg) for arg in args)) - def _write_error_log(self, reason: str, exception_info: str) -> str | None: self.log_file_path = super()._write_error_log(reason, exception_info) return self.log_file_path @@ -104,14 +128,15 @@ def _write_passed_log(self, reason: str = "Validation passed") -> str | None: self.log_file_path = super()._write_passed_log(reason) return self.log_file_path - def validate_print_calls_by_llm(self, *, endpoint: str, model: str, instructions: str | None = None) -> dict: + def validate_print_calls_by_llm_cli(self, *, endpoint: str, model: str, instructions: str | None = None) -> dict: instructions = self._resolve_validation_instructions(instructions) response = None uploaded_file_ids: list[str] = [] + credential = cast(TokenCredential, self.tokenCredential) with ( AIProjectClient( endpoint=endpoint, - credential=self.tokenCredential, + credential=credential, logging_enable=True, ) as project_client, project_client.get_openai_client() as openai_client, @@ -160,9 +185,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log_file_path: str | None = None - def _capture_print(self, *args, **_kwargs): - self.print_calls.append(" ".join(str(arg) for arg in args)) - def _write_error_log(self, reason: str, exception_info: str) -> str | None: self.log_file_path = super()._write_error_log(reason, exception_info) return self.log_file_path @@ -175,16 +197,17 @@ def _write_passed_log(self, reason: str = "Validation passed") -> str | None: self.log_file_path = super()._write_passed_log(reason) return self.log_file_path - async def validate_print_calls_by_llm_async( + async def validate_print_calls_by_llm_async_cli( self, *, endpoint: str, model: str, instructions: str | None = None ) -> dict: instructions = self._resolve_validation_instructions(instructions) response = None uploaded_file_ids: list[str] = [] + credential = cast(AsyncTokenCredential, self.tokenCredential) async with ( AsyncAIProjectClient( endpoint=endpoint, - credential=self.tokenCredential, + credential=credential, logging_enable=True, ) as project_client, project_client.get_openai_client() as openai_client, @@ -251,9 +274,7 @@ def _suppress_terminal_output(): def _parse_args() -> tuple[argparse.Namespace, dict[str, str]]: parser = argparse.ArgumentParser() - parser.add_argument("--sample-path", "--sample_path", dest="sample_path", required=True) - parser.add_argument("--llm-endpoint", "--llm_endpoint", dest="llm_endpoint", required=True) - parser.add_argument("--llm-model-name", "--llm_model_name", dest="llm_model_name", required=True) + parser.add_argument("--sample-path", "--sample_path", dest="sample_path") args, unknown = parser.parse_known_args() def _clean_cli_value(value: str) -> str: @@ -264,11 +285,10 @@ def _clean_cli_value(value: str) -> str: cleaned = cleaned[1:].lstrip() return cleaned - args.sample_path = _clean_cli_value(args.sample_path) - args.llm_endpoint = _clean_cli_value(args.llm_endpoint) - args.llm_model_name = _clean_cli_value(args.llm_model_name) + if args.sample_path: + args.sample_path = _clean_cli_value(args.sample_path) - env_vars: dict[str, str] = {} + arg_env_vars: dict[str, str] = {} i = 0 while i < len(unknown): item = unknown[i] @@ -282,56 +302,78 @@ def _clean_cli_value(value: str) -> str: if i >= len(unknown): raise SystemExit(f"Missing value for argument: --{name}") value = unknown[i] - env_vars[name.replace("-", "_").upper()] = _clean_cli_value(value) + arg_env_vars[name.replace("-", "_").upper()] = _clean_cli_value(value) i += 1 - return args, env_vars + args.sample_path = args.sample_path or os.environ.get("SAMPLE_PATH") + args.llm_endpoint = arg_env_vars.get("LLM_VALIDATION_PROJECT_ENDPOINT") or os.environ.get( + "LLM_VALIDATION_PROJECT_ENDPOINT" + ) + args.llm_model_name = ( + arg_env_vars.get("LLM_MODEL_NAME") or os.environ.get("LLM_MODEL_NAME") or LIVE_LLM_VALIDATION_MODEL + ) + + if not args.sample_path: + raise SystemExit("Missing sample path. Provide --sample-path or set SAMPLE_PATH.") + if not args.llm_endpoint: + raise SystemExit( + "Missing LLM endpoint. Set LLM_VALIDATION_PROJECT_ENDPOINT or pass --llm_validation_project_endpoint." + ) + return args, arg_env_vars -def _build_result(report: dict, *, log_file: str | None, start_time: float) -> dict: +def _build_result(report: dict, *, log_file: str | None, duration: float) -> dict: + captured_prints_file = report.get("captured_prints_file") return { "correct": report.get("correct", False), "llm_comment": report.get("reason"), "log_file": log_file, - "duration": round(time.perf_counter() - start_time, 3), + "captured_prints_file": captured_prints_file, + "duration": round(duration, 3), } -def _run_sync_sample(sample_path: str, args: argparse.Namespace, env_vars: dict[str, str], start_time: float) -> dict: +def _run_sync_sample(sample_path: str, args: argparse.Namespace, env_vars: dict[str, str]) -> dict: with DefaultAzureCredential() as credential: executor = _CliSampleExecutor( _CredentialProvider(credential), sample_path, - env_vars={**LOG_FILE_PATTERNS, **env_vars}, + env_vars=env_vars, ) try: with _suppress_terminal_output(): + sample_start_time = time.perf_counter() executor.execute(patched_open_fn=patched_open_crlf_to_lf) - report = executor.validate_print_calls_by_llm(endpoint=args.llm_endpoint, model=args.llm_model_name) + sample_duration = time.perf_counter() - sample_start_time + report = executor.validate_print_calls_by_llm_cli(endpoint=args.llm_endpoint, model=args.llm_model_name) except Exception as ex: # pylint: disable=broad-exception-caught report = {"correct": False, "reason": f"Sample execution failed: {type(ex).__name__}: {ex}"} - return _build_result(report, log_file=executor.log_file_path, start_time=start_time) + sample_duration = 0.0 + report["captured_prints_file"] = executor.output_file_path + return _build_result(report, log_file=executor.log_file_path, duration=sample_duration) -async def _run_async_sample( - sample_path: str, args: argparse.Namespace, env_vars: dict[str, str], start_time: float -) -> dict: +async def _run_async_sample(sample_path: str, args: argparse.Namespace, env_vars: dict[str, str]) -> dict: async with AsyncDefaultAzureCredential() as credential: executor = _CliAsyncSampleExecutor( _AsyncCredentialProvider(credential), sample_path, - env_vars={**LOG_FILE_PATTERNS, **env_vars}, + env_vars=env_vars, ) try: with _suppress_terminal_output(): + sample_start_time = time.perf_counter() await executor.execute_async(patched_open_fn=patched_open_crlf_to_lf) - report = await executor.validate_print_calls_by_llm_async( + sample_duration = time.perf_counter() - sample_start_time + report = await executor.validate_print_calls_by_llm_async_cli( endpoint=args.llm_endpoint, model=args.llm_model_name, ) except Exception as ex: # pylint: disable=broad-exception-caught report = {"correct": False, "reason": f"Sample execution failed: {type(ex).__name__}: {ex}"} - return _build_result(report, log_file=executor.log_file_path, start_time=start_time) + sample_duration = 0.0 + report["captured_prints_file"] = executor.output_file_path + return _build_result(report, log_file=executor.log_file_path, duration=sample_duration) def main() -> int: @@ -339,12 +381,11 @@ def main() -> int: sample_path = ( str((PROJECT_ROOT / args.sample_path).resolve()) if not os.path.isabs(args.sample_path) else args.sample_path ) - start_time = time.perf_counter() - with _temporary_env(LOG_FILE_PATTERNS): + with _temporary_env(LIVE_MODE_ENV): result = ( - asyncio.run(_run_async_sample(sample_path, args, env_vars, start_time)) + asyncio.run(_run_async_sample(sample_path, args, env_vars)) if sample_path.endswith("_async.py") - else _run_sync_sample(sample_path, args, env_vars, start_time) + else _run_sync_sample(sample_path, args, env_vars) ) print(json.dumps(result, indent=2)) diff --git a/sdk/ai/azure-ai-projects/tests/samples/sample_executor.py b/sdk/ai/azure-ai-projects/tests/samples/sample_executor.py index ccc291443da7..72197a62ccd8 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/sample_executor.py +++ b/sdk/ai/azure-ai-projects/tests/samples/sample_executor.py @@ -29,6 +29,8 @@ import unittest.mock as mock from typing import cast from io import BytesIO +from azure.ai.projects import _patch as projects_patch +from azure.ai.projects.aio import _patch as async_projects_patch from azure.core.credentials import TokenCredential from azure.core.credentials_async import AsyncTokenCredential from devtools_testutils.fake_credentials import FakeTokenCredential @@ -47,6 +49,7 @@ PLAYBACK_LLM_VALIDATION_PROJECT_ENDPOINT = ( "https://sanitized-account-name.services.ai.azure.com/api/projects/sanitized-project-name" ) +LIVE_LLM_VALIDATION_MODEL = "gpt-5.2" PLAYBACK_LLM_VALIDATION_MODEL = "sanitized-model-deployment-name" @@ -197,6 +200,11 @@ def __init__( self.test_instance = test_instance self.sample_path = sample_path self.print_calls: list[str] = [] + self.console_entries: list[str] = [] + self.validation_entries: list[str] = [] + self.output_file_path: Optional[str] = None + self._artifact_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + self._sample_filename_stem = os.path.basename(self.sample_path).replace(".py", "") self._original_print = print self.allowed_llm_validation_failures = allowed_llm_validation_failures or set() self._validation_text_preprocessor = validation_text_preprocessor @@ -235,6 +243,15 @@ def _capture_print(self, *args, **kwargs): """Capture print calls while still outputting to console.""" text = " ".join(str(arg) for arg in args) self.print_calls.append(text) + self.console_entries.append(text) + self.validation_entries.append(text) + self._original_print(*args, **kwargs) + + def _capture_sdk_print(self, *args, **kwargs): + """Capture SDK _print calls without routing them back through patched builtins.print.""" + text = " ".join(str(arg) for arg in args) + self.console_entries.append(text) + self.validation_entries.append(text) self._original_print(*args, **kwargs) @contextmanager @@ -307,7 +324,7 @@ def _always_true(_level, _logger=module_logger): except Exception: # pylint: disable=broad-exception-caught continue - capture_handler = _PrintCaptureLogHandler(self.print_calls) + capture_handler = _PrintCaptureLogHandler(self.validation_entries) capture_handler.setFormatter(logging.Formatter("[%(name)s] %(message)s")) root_logger.setLevel(logging.DEBUG) @@ -325,36 +342,51 @@ def _always_true(_level, _logger=module_logger): for module_logger, original_is_enabled_for in patched_is_enabled_for: module_logger.isEnabledFor = original_is_enabled_for - def _get_log_file_path(self, log_env_var: str) -> Optional[str]: - """Get and prepare log file path based on environment variable. + def _get_log_file_path(self, suffix: str) -> Optional[str]: + """Get and prepare a log file path in the temp directory for live runs.""" + if not _is_live_mode(): + return None - Args: - log_env_var: Environment variable name to check for log format + log_file = os.path.join( + tempfile.gettempdir(), + f"{self._sample_filename_stem}_{suffix}_{self._artifact_timestamp}.log", + ) - Returns: - Path to the log file (cleaned up and ready to write), or None if logging is disabled - """ - # Only create logs in live mode + if os.path.exists(log_file): + os.remove(log_file) + + return log_file + + def _get_output_file_path(self) -> Optional[str]: + """Get and prepare an output file path in the temp directory for live runs.""" if not _is_live_mode(): return None - # Only log if environment variable is set - log_format = os.environ.get(log_env_var) - if not log_format: - return None + output_file = os.path.join( + tempfile.gettempdir(), + f"{self._sample_filename_stem}_output_{self._artifact_timestamp}.txt", + ) - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - sample_filename = os.path.basename(self.sample_path).replace(".py", "") + if os.path.exists(output_file): + os.remove(output_file) - # Replace placeholders in the format template - log_filename = log_format.replace("", sample_filename).replace("", timestamp) - log_file = os.path.join(tempfile.gettempdir(), log_filename) + return output_file - # Remove existing file if present to ensure clean overwrite - if os.path.exists(log_file): - os.remove(log_file) + def _write_output_file(self) -> Optional[str]: + """Write captured sample print statements to the temp output file.""" + output_file = self._get_output_file_path() + if not output_file: + return None - return log_file + with open(output_file, "w", encoding="utf-8") as f: + if self.print_calls: + f.write("\n".join(self.print_calls)) + f.write("\n") + else: + f.write("No captured print statements.\n") + + self.output_file_path = output_file + return output_file def _write_error_log(self, reason: str, exception_info: str) -> Optional[str]: """Write captured print statements to a log file for execution errors. @@ -366,7 +398,7 @@ def _write_error_log(self, reason: str, exception_info: str) -> Optional[str]: Returns: Path to the created log file, or None if logging is disabled """ - log_file = self._get_log_file_path("SAMPLE_TEST_ERROR_LOG") + log_file = self._get_log_file_path("errors") if not log_file: return None @@ -379,7 +411,7 @@ def _write_error_log(self, reason: str, exception_info: str) -> Optional[str]: f.write("\n" + "=" * 80 + "\n\n") f.write("Print Statements:\n") f.write("=" * 80 + "\n") - for i, print_call in enumerate(self.print_calls, 1): + for i, print_call in enumerate(self.console_entries, 1): f.write(f"{i}. {print_call}\n") return log_file @@ -392,7 +424,7 @@ def _write_failed_log(self, reason: str) -> Optional[str]: Returns: Path to the created log file, or None if logging is disabled """ - log_file = self._get_log_file_path("SAMPLE_TEST_FAILED_LOG") + log_file = self._get_log_file_path("failed") if not log_file: return None @@ -401,7 +433,7 @@ def _write_failed_log(self, reason: str) -> Optional[str]: f.write(f"Validation Failed: {reason}\n\n") f.write("Print Statements:\n") f.write("=" * 80 + "\n") - for i, print_call in enumerate(self.print_calls, 1): + for i, print_call in enumerate(self.console_entries, 1): f.write(f"{i}. {print_call}\n") return log_file @@ -414,7 +446,7 @@ def _write_passed_log(self, reason: str = "Validation passed") -> Optional[str]: Returns: Path to the created log file, or None if logging is disabled """ - log_file = self._get_log_file_path("SAMPLE_TEST_PASSED_LOG") + log_file = self._get_log_file_path("success") if not log_file: return None @@ -423,7 +455,7 @@ def _write_passed_log(self, reason: str = "Validation passed") -> Optional[str]: f.write(f"Validation Passed: {reason}\n\n") f.write("Print Statements:\n") f.write("=" * 80 + "\n") - for i, print_call in enumerate(self.print_calls, 1): + for i, print_call in enumerate(self.console_entries, 1): f.write(f"{i}. {print_call}\n") return log_file @@ -463,8 +495,16 @@ def _build_validation_text(self) -> str: and text transformations. """ if self._validation_text_preprocessor: - return self._validation_text_preprocessor(self.print_calls) - return "\n".join(self.print_calls) + return self._validation_text_preprocessor(self.validation_entries) + return "\n".join(self.validation_entries) + + @contextmanager + def _capture_sdk_console_prints(self): + with ( + mock.patch.object(projects_patch, "print", side_effect=self._capture_sdk_print), + mock.patch.object(async_projects_patch, "print", side_effect=self._capture_sdk_print), + ): + yield def _build_validation_txt_bytes(self, validation_log_text: str) -> bytes: """Build UTF-8 TXT content containing captured print/log output.""" @@ -679,6 +719,7 @@ def execute(self, patched_open_fn=None): with ( self._capture_debug_logs(), + self._capture_sdk_console_prints(), mock.patch("builtins.print", side_effect=self._capture_print), mock.patch("builtins.open", side_effect=patched_open_fn), ): @@ -694,9 +735,11 @@ def execute(self, patched_open_fn=None): # Call main() if it exists (samples wrap their code in main()) if hasattr(self.module, "main") and callable(self.module.main): self.module.main() + self._write_output_file() except Exception as e: # Log print statements with exception details before re-raising exception_info = traceback.format_exc() + self._write_output_file() log_file = self._write_error_log( reason=f"{type(e).__name__}: {str(e)}", exception_info=exception_info ) @@ -709,7 +752,7 @@ def validate_print_calls_by_llm(self, *, instructions: Optional[str] = None): instructions = self._resolve_validation_instructions(instructions) if is_live(): endpoint = os.environ["LLM_VALIDATION_PROJECT_ENDPOINT"] - model = "gpt-5.2" + model = LIVE_LLM_VALIDATION_MODEL else: endpoint = PLAYBACK_LLM_VALIDATION_PROJECT_ENDPOINT model = PLAYBACK_LLM_VALIDATION_MODEL @@ -875,6 +918,7 @@ async def execute_async(self, patched_open_fn=None): MonkeyPatch.context() as mp, self._get_mock_credential(), self._capture_debug_logs(), + self._capture_sdk_console_prints(), mock.patch("builtins.print", side_effect=self._capture_print), mock.patch("builtins.open", side_effect=patched_open_fn), ): @@ -895,9 +939,11 @@ async def execute_async(self, patched_open_fn=None): # Call main() if it exists (samples wrap their code in main()) if hasattr(self.module, "main") and callable(self.module.main): await self.module.main() # type: ignore[misc] + self._write_output_file() except Exception as e: # Log print statements with exception details before re-raising exception_info = traceback.format_exc() + self._write_output_file() log_file = self._write_error_log(reason=f"{type(e).__name__}: {str(e)}", exception_info=exception_info) if log_file: print(f"\nSample execution failed! Print statements logged to: {log_file}") @@ -912,7 +958,7 @@ async def validate_print_calls_by_llm_async( instructions = self._resolve_validation_instructions(instructions) if is_live(): endpoint = os.environ["LLM_VALIDATION_PROJECT_ENDPOINT"] - model = "gpt-5.2" + model = LIVE_LLM_VALIDATION_MODEL else: endpoint = PLAYBACK_LLM_VALIDATION_PROJECT_ENDPOINT model = PLAYBACK_LLM_VALIDATION_MODEL