Metro.AI.psm1
#region Helper Functions class MetroAIContext { [string]$Endpoint [ValidateSet('Agent', 'Assistant')] [string]$ApiType [bool]$UseNewApi [string]$ApiVersion MetroAIContext([string]$endpoint, [string]$apiType, [string]$apiVersion = "") { # keep your original logic for simple endpoint+apiType $this.Endpoint = $endpoint.TrimEnd('/') $this.ApiType = $apiType $this.ApiVersion = $apiVersion # auto-detect new vs old based on hostname # new AI endpoints live under *.ai.azure.com $this.UseNewApi = $this.Endpoint -match '\.ai\.azure\.com' } MetroAIContext([string]$connectionString, [string]$apiType, [switch]$fromConnectionString) { # exactly your original split/format $parts = $connectionString -split ';' if ($parts.Count -ne 4) { throw "Invalid connection string format." } $this.Endpoint = ('https://{0}/agents/v1.0/subscriptions/{1}/resourceGroups/{2}/providers/Microsoft.MachineLearningServices/workspaces/{3}' -f $parts) $this.ApiType = $apiType $this.ApiVersion = "" } [string] ResolveUri( [string]$Service, [string]$Operation, [string]$Path = "", [switch]$UseOpenPrefix ) { if ($this.UseNewApi) { # new surface: endpoint already includes /api/projects/{…} $base = "$($this.Endpoint)/$Service" if ($Path) { $base += "/$Path" } $ver = if ($this.ApiVersion) { $this.ApiVersion } else { '2025-05-15-preview' } return "$base`?api-version=$ver" } # old-style behavior $prefix = ($this.ApiType -eq 'Assistant' -and $UseOpenPrefix) ? "openai/" : "" $baseUri = "$($this.Endpoint)/$prefix$Service" if ($Path) { $baseUri += "/$Path" } $ver = if ($this.ApiVersion) { $this.ApiVersion } else { Get-MetroApiVersion -Operation $Operation -ApiType $this.ApiType } return "$baseUri`?api-version=$ver" } } function Set-MetroAIContext { [CmdletBinding(DefaultParameterSetName = 'Endpoint')] param ( [Parameter(Mandatory, ParameterSetName = 'Endpoint')] [string]$Endpoint, [Parameter(Mandatory)] [ValidateSet('Agent', 'Assistant')] [string]$ApiType, [Parameter(Mandatory, ParameterSetName = 'ConnectionString')] [string]$ConnectionString, [string]$ApiVersion ) if ($PSCmdlet.ParameterSetName -eq 'ConnectionString') { Write-Verbose "Setting context from connection string" $script:MetroContext = [MetroAIContext]::new($ConnectionString, $ApiType, $true) } else { Write-Verbose "Setting context for endpoint $Endpoint" $script:MetroContext = [MetroAIContext]::new($Endpoint, $ApiType, $ApiVersion) } } function Get-MetroAIContext { if ($script:MetroContext) { return $script:MetroContext } else { Write-Error "No Metro AI context set. Use Set-MetroAIContext to set it." } } function Get-MetroAuthHeader { <# .SYNOPSIS Returns a header hashtable with an authorization token for the specified API type, automatically choosing the right Azure resource URL for old vs new endpoints. #> param ( [Parameter(Mandatory = $true)] [ValidateSet('Agent', 'Assistant')] [string]$ApiType ) try { # make sure our context is set if (-not $script:MetroContext) { throw "No Metro AI context set. Use Set-MetroAIContext first." } # decide which resource to ask a token for if ($script:MetroContext.UseNewApi) { # unified new AI surface $resourceUrl = "https://ai.azure.com/" } elseif ($ApiType -eq 'Agent') { # old Agent endpoint $resourceUrl = "https://ml.azure.com/" } else { # old Assistant endpoint $resourceUrl = "https://cognitiveservices.azure.com/" } # grab the token $token = (Get-AzAccessToken -ResourceUrl $resourceUrl -AsSecureString).Token ` | ConvertFrom-SecureString -AsPlainText if (-not $token) { throw "Token retrieval failed for resource $resourceUrl" } # return the bare auth header; # x-ms-enable-preview goes in Invoke-MetroAIApiCall so it’s applied on every request uniformly return @{ Authorization = "Bearer $token" } } catch { Write-Error "Get-MetroAuthHeader error for '$ApiType': $_" } } function Get-MetroApiVersion { <# .SYNOPSIS Returns the API version for a given operation. .PARAMETER Operation The operation name. .PARAMETER ApiType The API type: Agent or Assistant. #> param ( [Parameter(Mandatory = $true)] [string]$Operation, [Parameter(Mandatory = $true)] [ValidateSet('Agent', 'Assistant')] [string]$ApiType ) switch ($Operation) { 'upload' { return '2024-05-01-preview' } 'create' { return '2024-07-01-preview' } 'get' { return '2024-02-15-preview' } 'thread' { return '2024-03-01-preview' } 'threadStatus' { return '2024-05-01-preview' } 'messages' { return '2024-05-01-preview' } 'openapi' { return '2024-12-01-preview' } default { return '2024-05-01-preview' } } } function Invoke-MetroAIApiCall { <# .SYNOPSIS Generalized API caller for Metro AI endpoints. .DESCRIPTION Constructs the full API URI, obtains the authorization header, merges additional headers, and invokes the REST method with error handling. .PARAMETER Service The service segment. .PARAMETER Operation The operation name used to determine the API version. .PARAMETER Path Optional additional path appended to the URI. .PARAMETER Method The HTTP method (e.g. Get, Post, Delete). Defaults to "Get". .PARAMETER Body Optional body content for POST/PUT requests. .PARAMETER ContentType Optional content type (e.g. "application/json"). .PARAMETER AdditionalHeaders Optional extra headers to merge with the authorization header. .PARAMETER TimeoutSeconds Optional REST call timeout (default 100 seconds). .PARAMETER UseOpenPrefix Switch to use the "openai/" prefix for Assistant API calls. .PARAMETER Form Optional parameter for multipart/form-data form data. .EXAMPLE Invoke-MetroAIApiCall -Endpoint "https://aoai-policyassistant.openai.azure.com" -ApiType Assistant ` -Service 'threads' -Operation 'thread' -Method Post -ContentType "application/json" ` -Body @{ some = "data" } -UseOpenPrefix #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Service, [Parameter(Mandatory = $true)] [string]$Operation, [Parameter(Mandatory = $false)] [string]$Path, [Parameter(Mandatory = $false)] [string]$Method = "Get", [Parameter(Mandatory = $false)] [object]$Body, [Parameter(Mandatory = $false)] [string]$ContentType, [Parameter(Mandatory = $false)] [hashtable]$AdditionalHeaders, [Parameter(Mandatory = $false)] [int]$TimeoutSeconds = 100, [Parameter(Mandatory = $false)] [switch]$UseOpenPrefix, [Parameter(Mandatory = $false)] [object]$Form ) if (-not $script:MetroContext) { throw "MetroAI context not set. Use Set-MetroAIContext before invoking calls." } try { $authHeader = Get-MetroAuthHeader -ApiType $script:MetroContext.ApiType if ($AdditionalHeaders) { $authHeader += $AdditionalHeaders } $uri = $script:MetroContext.ResolveUri($Service, $Operation, $Path, $UseOpenPrefix) Write-Verbose "Calling API at URI: $uri with method $Method" $invokeParams = @{ Uri = $uri Method = $Method Headers = $authHeader TimeoutSec = $TimeoutSeconds } if ($ContentType) { $invokeParams.ContentType = $ContentType } if ($Form) { $invokeParams.Form = $Form } elseif ($Body) { if ($ContentType -eq "application/json") { $invokeParams.Body = $Body | ConvertTo-Json -Depth 100 } else { $invokeParams.Body = $Body } } return Invoke-RestMethod @invokeParams } catch { Write-Error "Invoke-MetroAIApiCall error: $_" } } #endregion #region File Upload & Output Files function Invoke-MetroAIUploadFile { <# .SYNOPSIS Uploads a file to the API endpoint. .DESCRIPTION Reads a local file and uploads it via a multipart/form-data request. .PARAMETER FilePath The local path to the file. .PARAMETER Purpose The purpose of the file upload. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$FilePath, [string]$Purpose = "assistants" ) try { $fileItem = Get-Item -Path $FilePath -ErrorAction Stop $body = @{ purpose = $Purpose; file = $fileItem } Invoke-MetroAIApiCall -Service 'files' -Operation 'upload' -Method Post -Form $body -ContentType "multipart/form-data" } catch { Write-Error "Invoke-MetroAIUploadFile error: $_" } } function Get-MetroAIOutputFiles { <# .SYNOPSIS Retrieves output files for an assistant. .DESCRIPTION Downloads output files (with purpose "assistants_output") from an assistant endpoint. .PARAMETER FileId Optional file ID. .PARAMETER LocalFilePath Optional path to save the file. #> [CmdletBinding(DefaultParameterSetName = 'NoFileId')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'FileId')] [string]$FileId, [Parameter(Mandatory = $false, ParameterSetName = 'FileId')] [string]$LocalFilePath ) try { if ($PSBoundParameters['LocalFilePath'] -and -not $PSBoundParameters['FileId']) { Write-Error "LocalFilePath can only be used with FileId." break } $files = Invoke-MetroAIApiCall -Service 'files' -Operation 'upload' -Method Get if (-not [string]::IsNullOrWhiteSpace($FileId)) { $item = $files.data | Where-Object { $_.id -eq $FileId -and $_.purpose -eq "assistants_output" } if ($item) { $content = Invoke-MetroAIApiCall -Service 'files' -Operation 'upload' -Path ("{0}/content" -f $FileId) -Method Get if ($LocalFilePath) { $content | Out-File -FilePath $LocalFilePath -Force -Verbose } else { return $content } } else { Write-Error "File $FileId not found or wrong purpose." } } else { $outputFiles = $files.data | Where-Object { $_.purpose -eq "assistants_output" } if ($outputFiles.Count -gt 0) { return $outputFiles } else { Write-Output "No output files found." } } } catch { Write-Error "Get-MetroAIOutputFiles error: $_" } } function Remove-MetroAIFiles { <# .SYNOPSIS Deletes files from an endpoint. .DESCRIPTION Removes the specified file (or all files if FileId is not provided). .PARAMETER FileId Optional specific file ID. #> [CmdletBinding()] param ( [string]$FileId ) try { $files = Invoke-MetroAIApiCall -Service 'files' -Operation 'upload' -Method Get if ($FileId) { $item = $files.data | Where-Object { $_.id -eq $FileId } if ($item) { Invoke-MetroAIApiCall -Service 'files' -Operation 'upload' -Path $FileId -Method Delete Write-Output "File $FileId deleted." } else { Write-Error "File $FileId not found." } } else { foreach ($file in $files.data) { try { Invoke-MetroAIApiCall -Service 'files' -Operation 'upload' -Path $file.id -Method Delete } catch { Write-Error "Error deleting file $($file.id): $_" } } } } catch { Write-Error "Remove-MetroAIFiles error: $_" } } #endregion #region Resource Management function New-MetroAIResource { <# .SYNOPSIS Creates a new Metro AI agent or assistant resource with comprehensive tool support. .DESCRIPTION Creates an Azure AI agent or assistant with various tools including connected agents, code interpreter, file search, Azure AI Search, custom functions, and OpenAPI integrations. Follows Azure best practices for parameter validation, error handling, and resource management. Note: Bing grounding must be added after creation using Set-MetroAIResource. .PARAMETER Model The model identifier (e.g., 'gpt-4', 'gpt-4-turbo', 'gpt-35-turbo'). .PARAMETER Name The name of the agent/assistant resource. Must be unique within the workspace. .PARAMETER Description Optional description for the resource (max 512 characters). .PARAMETER Instructions System instructions that guide the agent's behavior (max 256,000 characters). .PARAMETER Metadata Optional metadata as key-value pairs (max 16 pairs, keys/values max 64 chars each). .PARAMETER ResponseFormat Response format specification. Use 'text' or structured format definitions. .PARAMETER Temperature Sampling temperature between 0.0 (deterministic) and 2.0 (most random). Default is 1.0. .PARAMETER TopP Nucleus sampling parameter between 0.0 and 1.0. Alternative to temperature. .PARAMETER EnableConnectedAgent Enable a single connected agent tool. .PARAMETER ConnectedAgentId ID of the connected agent. Required when EnableConnectedAgent is used. .PARAMETER ConnectedAgentName Display name for the connected agent. Required when EnableConnectedAgent is used. .PARAMETER ConnectedAgentDescription Description of the connected agent's capabilities. Required when EnableConnectedAgent is used. .PARAMETER ConnectedAgentsDefinition Array of connected agent definitions. Each must have 'id', 'name', and 'description' properties. .PARAMETER EnableCodeInterpreter Enable Python code execution capabilities. .PARAMETER CodeInterpreterFileIds Array of file IDs to make available to the code interpreter. .PARAMETER EnableFileSearch Enable file search capabilities across vector stores. .PARAMETER FileSearchVectorStoreIds Array of vector store IDs for file search operations. .PARAMETER EnableAzureAiSearch Enable Azure AI Search integration. .PARAMETER AzureAiSearchIndexes Array of Azure AI Search index configurations. .PARAMETER EnableFunctionTool Enable custom function calling. .PARAMETER FunctionName Name of the custom function. .PARAMETER FunctionDescription Description of what the function does. .PARAMETER FunctionParameters JSON Schema defining the function's parameters. .PARAMETER EnableOpenApi Enable OpenAPI/REST API integration. .PARAMETER OpenApiDefinitionFile Path to the OpenAPI specification file (JSON format only). .PARAMETER OpenApiName Friendly name for the OpenAPI integration. .PARAMETER OpenApiDescription Description of the OpenAPI service. .PARAMETER OpenApiAuthType Authentication method: 'Anonymous', 'Connection', or 'ManagedIdentity'. .PARAMETER OpenApiConnectionId Connection ID for API authentication when using 'Connection' auth type. .PARAMETER OpenApiManagedAudience Target audience URI when using 'ManagedIdentity' auth type. .EXAMPLE New-MetroAIResource -Model 'gpt-4' -Name 'MyAssistant' -Description 'General purpose assistant' .EXAMPLE New-MetroAIResource -Model 'gpt-4' -Name 'CodeHelper' -EnableCodeInterpreter -CodeInterpreterFileIds @('file-123') .EXAMPLE # Create assistant, then add Bing grounding $assistant = New-MetroAIResource -Model 'gpt-4' -Name 'SearchBot' Set-MetroAIResource -AssistantId $assistant.id -EnableBingGrounding -BingConnectionId 'bing-search-connection' .EXAMPLE # Multi-tool agent with various capabilities New-MetroAIResource -Model 'gpt-4' -Name 'MultiToolAgent' ` -EnableCodeInterpreter -CodeInterpreterFileIds @('file-123') ` -EnableAzureAiSearch -AzureAiSearchIndexes @(@{ index_connection_id='search-conn'; index_name='docs'; query_type='semantic'; top_k=5 }) .NOTES Requires Set-MetroAIContext to be called first. Follow Azure AI responsible AI guidelines. To add Bing grounding, use Set-MetroAIResource with -EnableBingGrounding after creation. #> [Alias("New-MetroAIAgent")] [Alias("New-MetroAIAssistant")] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateLength(1, 256)] [string]$Model, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateLength(1, 256)] [Alias('ResourceName')] [string]$Name, [ValidateLength(0, 512)] [string]$Description, [ValidateLength(0, 256000)] [string]$Instructions, [ValidateScript({ if ($_.Count -gt 16) { throw "Maximum 16 metadata entries allowed" } foreach ($key in $_.Keys) { if ($key.Length -gt 64) { throw "Metadata key '$key' exceeds 64 character limit" } if ($_[$key].ToString().Length -gt 512) { throw "Metadata value for '$key' exceeds 512 character limit" } } return $true })] [hashtable]$Metadata, [string]$ResponseFormat, [ValidateRange(0.0, 2.0)] [double]$Temperature, [ValidateRange(0.0, 1.0)] [double]$TopP, # Connected Agent Parameters [switch]$EnableConnectedAgent, [ValidateNotNullOrEmpty()] [string]$ConnectedAgentId, [ValidateLength(1, 256)] [string]$ConnectedAgentName, [ValidateLength(1, 512)] [string]$ConnectedAgentDescription, [ValidateScript({ foreach ($agent in $_) { if (-not ($agent.id -and $agent.name -and $agent.description)) { throw "Each ConnectedAgentsDefinition entry must include 'id', 'name', and 'description' properties" } if ($agent.name.Length -gt 256) { throw "Connected agent name exceeds 256 characters" } if ($agent.description.Length -gt 512) { throw "Connected agent description exceeds 512 characters" } } return $true })] [object[]]$ConnectedAgentsDefinition, # Code Interpreter Parameters [switch]$EnableCodeInterpreter, [ValidateCount(0, 20)] [string[]]$CodeInterpreterFileIds, # File Search Parameters [switch]$EnableFileSearch, [ValidateCount(1, 1)] [string[]]$FileSearchVectorStoreIds, # Azure AI Search Parameters [switch]$EnableAzureAiSearch, [ValidateScript({ foreach ($index in $_) { $requiredProps = @('index_connection_id', 'index_name') foreach ($prop in $requiredProps) { if (-not $index.$prop) { throw "Azure AI Search index missing required property: $prop" } } } return $true })] [object[]]$AzureAiSearchIndexes, # Function Tool Parameters [switch]$EnableFunctionTool, [ValidatePattern('^[a-zA-Z0-9_-]+$')] [ValidateLength(1, 64)] [string]$FunctionName, [ValidateLength(1, 1024)] [string]$FunctionDescription, [ValidateScript({ if ($_.type -ne 'object') { throw "Function parameters must have type 'object'" } if (-not $_.properties) { throw "Function parameters must include 'properties'" } return $true })] [hashtable]$FunctionParameters, # OpenAPI Parameters [switch]$EnableOpenApi, [ValidateScript({ if (-not (Test-Path $_ -PathType Leaf)) { throw "OpenAPI definition file not found: $_" } $extension = [System.IO.Path]::GetExtension($_).ToLower() if ($extension -ne '.json') { throw "OpenAPI file must be JSON format (.json). YAML is not supported." } return $true })] [string]$OpenApiDefinitionFile, [ValidateLength(1, 64)] [string]$OpenApiName, [ValidateLength(1, 1024)] [string]$OpenApiDescription, [ValidateSet('Anonymous', 'Connection', 'ManagedIdentity')] [string]$OpenApiAuthType, [ValidateNotNullOrEmpty()] [string]$OpenApiConnectionId, [ValidatePattern('^https?://')] [string]$OpenApiManagedAudience ) begin { Write-Verbose "Starting New-MetroAIResource with Model: $Model, Name: $Name" # Ensure context is set if (-not $script:MetroContext) { throw "Metro AI context not set. Use Set-MetroAIContext first." } Write-Verbose "Using $($script:MetroContext.ApiType) API at $($script:MetroContext.Endpoint)" } process { try { # Enhanced parameter validation with detailed error messages if ($EnableConnectedAgent -and $ConnectedAgentsDefinition) { throw "Cannot use both -EnableConnectedAgent and -ConnectedAgentsDefinition simultaneously. Choose one approach." } if ($EnableConnectedAgent) { $requiredParams = @('ConnectedAgentId', 'ConnectedAgentName', 'ConnectedAgentDescription') foreach ($param in $requiredParams) { if (-not $PSBoundParameters[$param]) { throw "Parameter '$param' is required when using -EnableConnectedAgent." } } # Validate connected agent exists with detailed error handling Write-Verbose "Validating connected agent: $ConnectedAgentId" try { $connectedAgent = Get-MetroAIResource -AssistantId $ConnectedAgentId -ErrorAction Stop Write-Verbose "Connected agent '$($connectedAgent.name)' validated successfully" } catch { throw "Connected agent '$ConnectedAgentId' not found or inaccessible. Verify the ID exists and you have access: $($_.Exception.Message)" } } if ($ConnectedAgentsDefinition) { Write-Verbose "Validating $($ConnectedAgentsDefinition.Count) connected agents" foreach ($agent in $ConnectedAgentsDefinition) { try { $connectedAgent = Get-MetroAIResource -AssistantId $agent.id -ErrorAction Stop Write-Verbose "Connected agent '$($agent.name)' (ID: $($agent.id)) validated successfully" } catch { throw "Connected agent '$($agent.id)' not found or inaccessible. Verify the ID exists and you have access: $($_.Exception.Message)" } } } # Tool-specific validation with helpful error messages if ($EnableCodeInterpreter -and -not $CodeInterpreterFileIds) { throw "CodeInterpreterFileIds parameter is required when EnableCodeInterpreter is specified. Provide an array of file IDs or use an empty array @() if no files are needed initially." } if ($EnableFileSearch -and -not $FileSearchVectorStoreIds) { throw "FileSearchVectorStoreIds parameter is required when EnableFileSearch is specified." } if ($EnableAzureAiSearch -and -not $AzureAiSearchIndexes) { throw "CodeInterpreterFileIds parameter is required when EnableCodeInterpreter is specified. Provide an array of file IDs or use an empty array @() if no files are needed initially." } if ($EnableFileSearch -and -not $FileSearchVectorStoreIds) { throw "FileSearchVectorStoreIds parameter is required when EnableFileSearch is specified." } if ($EnableAzureAiSearch -and -not $AzureAiSearchIndexes) { throw "AzureAiSearchIndexes parameter is required when EnableAzureAiSearch is specified." } if ($EnableFunctionTool) { $requiredFuncParams = @('FunctionName', 'FunctionDescription', 'FunctionParameters') foreach ($param in $requiredFuncParams) { if (-not $PSBoundParameters[$param]) { throw "Parameter '$param' is required when using -EnableFunctionTool" } } } if ($EnableOpenApi) { $requiredApiParams = @('OpenApiDefinitionFile', 'OpenApiName', 'OpenApiDescription', 'OpenApiAuthType') foreach ($param in $requiredApiParams) { if (-not $PSBoundParameters[$param]) { throw "Parameter '$param' is required when using -EnableOpenApi" } } if ($OpenApiAuthType -eq 'Connection' -and -not $OpenApiConnectionId) { throw "OpenApiConnectionId is required when OpenApiAuthType is 'Connection'" } if ($OpenApiAuthType -eq 'ManagedIdentity' -and -not $OpenApiManagedAudience) { throw "OpenApiManagedAudience is required when OpenApiAuthType is 'ManagedIdentity'" } } if ($EnableBingGrounding -and -not $BingConnectionId) { throw "BingConnectionId is required when EnableBingGrounding is specified. Only connection-based authentication is supported for Bing grounding." } # Temperature and TopP mutual exclusivity check if ($PSBoundParameters.ContainsKey('Temperature') -and $PSBoundParameters.ContainsKey('TopP')) { Write-Warning "Both Temperature and TopP specified. The API may use only one. Consider using only Temperature or only TopP for predictable behavior." } # Build request body with proper null checking $body = @{ model = $Model name = $Name } # Add optional core properties only if they have values if ($Description) { $body.description = $Description } if ($Instructions) { $body.instructions = $Instructions } if ($Metadata -and $Metadata.Count -gt 0) { $body.metadata = $Metadata } if ($ResponseFormat) { $body.response_format = $ResponseFormat } if ($PSBoundParameters.ContainsKey('Temperature')) { $body.temperature = $Temperature } if ($PSBoundParameters.ContainsKey('TopP')) { $body.top_p = $TopP } # Assemble tools and resources using Lists for better performance $tools = [System.Collections.Generic.List[hashtable]]::new() $toolResources = @{} # Connected Agent Tools if ($EnableConnectedAgent) { $tools.Add(@{ type = 'connected_agent' connected_agent = @{ id = $ConnectedAgentId name = $ConnectedAgentName description = $ConnectedAgentDescription } }) Write-Verbose "Added connected agent tool: $ConnectedAgentName" } if ($ConnectedAgentsDefinition) { foreach ($agent in $ConnectedAgentsDefinition) { $tools.Add(@{ type = 'connected_agent' connected_agent = @{ id = $agent.id name = $agent.name description = $agent.description } }) Write-Verbose "Added connected agent tool: $($agent.name)" } } # Code Interpreter Tool if ($EnableCodeInterpreter) { $tools.Add(@{ type = 'code_interpreter' }) if ($CodeInterpreterFileIds -and $CodeInterpreterFileIds.Count -gt 0) { $toolResources.code_interpreter = @{ file_ids = $CodeInterpreterFileIds } Write-Verbose "Added code interpreter tool with $($CodeInterpreterFileIds.Count) files" } else { Write-Verbose "Added code interpreter tool with no initial files" } } # File Search Tool if ($EnableFileSearch) { $tools.Add(@{ type = 'file_search' }) $toolResources.file_search = @{ vector_store_ids = $FileSearchVectorStoreIds } Write-Verbose "Added file search tool with $($FileSearchVectorStoreIds.Count) vector stores" } # Azure AI Search Tool if ($EnableAzureAiSearch) { $tools.Add(@{ type = 'azure_ai_search' }) $toolResources.azure_ai_search = @{ indexes = $AzureAiSearchIndexes } Write-Verbose "Added Azure AI Search tool with $($AzureAiSearchIndexes.Count) indexes" } # Function Tool if ($EnableFunctionTool) { $tools.Add(@{ type = 'function' function = @{ name = $FunctionName description = $FunctionDescription parameters = $FunctionParameters } }) Write-Verbose "Added function tool: $FunctionName" } # OpenAPI Tool if ($EnableOpenApi) { Write-Verbose "Loading OpenAPI specification from: $OpenApiDefinitionFile" try { $specContent = Get-Content -Path $OpenApiDefinitionFile -Raw -ErrorAction Stop $spec = $specContent | ConvertFrom-Json -ErrorAction Stop $auth = switch ($OpenApiAuthType) { 'Anonymous' { @{ type = 'anonymous' } } 'Connection' { @{ type = 'connection'; connection_id = $OpenApiConnectionId } } 'ManagedIdentity' { @{ type = 'managed_identity'; security_scheme = @{ audience = $OpenApiManagedAudience } } } } $tools.Add(@{ type = 'openapi' openapi = @{ name = $OpenApiName description = $OpenApiDescription spec = $spec auth = $auth } }) Write-Verbose "Added OpenAPI tool: $OpenApiName ($OpenApiAuthType auth)" } catch { throw "Failed to process OpenAPI definition file '$OpenApiDefinitionFile': $($_.Exception.Message)" } } # Bing Grounding Tool - Connection-based only with correct payload structure if ($EnableBingGrounding) { Write-Verbose "Configuring Bing grounding tool with connection: $BingConnectionId" $tools.Add(@{ type = 'bing_grounding' bing_grounding = @{ connections = @( @{ connection_id = $BingConnectionId } ) } }) Write-Verbose "Added Bing grounding tool with connection ID: $BingConnectionId" } # Add tools and resources to body if any exist if ($tools.Count -gt 0) { $body.tools = $tools.ToArray() Write-Verbose "Total tools configured: $($tools.Count)" } if ($toolResources.Count -gt 0) { $body.tool_resources = $toolResources Write-Verbose "Tool resources configured for: $($toolResources.Keys -join ', ')" } # Create resource with confirmation $resourceType = $script:MetroContext.ApiType $confirmMessage = "Create new $resourceType '$Name' with model '$Model'" if ($tools.Count -gt 0) { $toolsList = ($tools | ForEach-Object { $_.type }) -join ', ' $confirmMessage += " and tools: $toolsList" } if ($PSCmdlet.ShouldProcess($confirmMessage, "New-MetroAIResource")) { Write-Verbose "Creating $resourceType resource..." Write-Verbose "Request payload: $($body | ConvertTo-Json -Depth 10 -Compress)" $invokeParams = @{ Service = 'assistants' Operation = 'create' Method = 'Post' ContentType = 'application/json' Body = $body } $result = Invoke-MetroAIApiCall @invokeParams if ($result -and $result.id) { Write-Information "Successfully created $resourceType '$Name' with ID: $($result.id)" -InformationAction Continue # Add some useful output for the user if ($tools.Count -gt 0) { Write-Information "Configured tools: $($tools | ForEach-Object { $_.type })" -InformationAction Continue } Write-Verbose "Resource creation completed successfully" return $result } else { throw "Resource creation appeared to succeed but no ID was returned in the response. This may indicate a service issue." } } } catch { $errorMessage = "Failed to create Metro AI resource '$Name': $($_.Exception.Message)" Write-Error $errorMessage -ErrorAction Stop } } } function Set-MetroAIResource { <# .SYNOPSIS Updates an existing Metro AI agent or assistant resource with comprehensive tool support. .DESCRIPTION Updates an Azure AI agent or assistant with various tools including connected agents, code interpreter, file search, Azure AI Search, custom functions, OpenAPI integrations, and Bing grounding. Can update from JSON file or specify individual parameters. .PARAMETER AssistantId The ID of the agent/assistant resource to update. .PARAMETER InputFile Path to a JSON file containing the complete resource definition to update. .PARAMETER Model The model identifier (e.g., 'gpt-4', 'gpt-4-turbo', 'gpt-35-turbo'). .PARAMETER Name The name of the agent/assistant resource. .PARAMETER Description Optional description for the resource (max 512 characters). .PARAMETER Instructions System instructions that guide the agent's behavior (max 256,000 characters). .PARAMETER Metadata Optional metadata as key-value pairs (max 16 pairs, keys/values max 64 chars each). .PARAMETER ResponseFormat Response format specification. Use 'text' or structured format definitions. .PARAMETER Temperature Sampling temperature between 0.0 (deterministic) and 2.0 (most random). .PARAMETER TopP Nucleus sampling parameter between 0.0 and 1.0. Alternative to temperature. .PARAMETER EnableBingGrounding Enable Bing search grounding for real-time information retrieval. .PARAMETER BingConnectionId Connection ID for Bing Search API. Required when EnableBingGrounding is used. .PARAMETER AddBingGrounding Switch to add Bing grounding to existing tools without replacing them. .PARAMETER RemoveBingGrounding Switch to remove Bing grounding from existing tools. .PARAMETER ClearAllTools Switch to remove all existing tools before applying new configuration. .EXAMPLE Set-MetroAIResource -AssistantId 'asst-123' -InputFile './updated-assistant.json' .EXAMPLE Set-MetroAIResource -AssistantId 'asst-123' -EnableBingGrounding -BingConnectionId 'bing-conn-1' .EXAMPLE Set-MetroAIResource -AssistantId 'asst-123' -AddBingGrounding -BingConnectionId 'bing-conn-1' .EXAMPLE Set-MetroAIResource -AssistantId 'asst-123' -RemoveBingGrounding .NOTES When using InputFile, individual parameters are ignored. Use AddBingGrounding to preserve existing tools. #> [Alias("Set-MetroAIAgent")] [Alias("Set-MetroAIAssistant")] [CmdletBinding(DefaultParameterSetName = 'Parameters', SupportsShouldProcess, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$AssistantId, [Parameter(Mandatory = $true, ParameterSetName = 'Json')] [ValidateScript({ if (-not (Test-Path $_ -PathType Leaf)) { throw "Input file not found: $_" } $extension = [System.IO.Path]::GetExtension($_).ToLower() if ($extension -ne '.json') { throw "Input file must be JSON format (.json)" } return $true })] [string]$InputFile, [Parameter(ParameterSetName = 'Parameters')] [ValidateNotNullOrEmpty()] [string]$Model, [Parameter(ParameterSetName = 'Parameters')] [ValidateLength(1, 256)] [string]$Name, [Parameter(ParameterSetName = 'Parameters')] [ValidateLength(0, 512)] [string]$Description, [Parameter(ParameterSetName = 'Parameters')] [ValidateLength(0, 256000)] [string]$Instructions, [Parameter(ParameterSetName = 'Parameters')] [ValidateScript({ if ($_.Count -gt 16) { throw "Maximum 16 metadata entries allowed" } foreach ($key in $_.Keys) { if ($key.Length -gt 64) { throw "Metadata key '$key' exceeds 64 character limit" } if ($_[$key].ToString().Length -gt 512) { throw "Metadata value for '$key' exceeds 512 character limit" } } return $true })] [hashtable]$Metadata, [Parameter(ParameterSetName = 'Parameters')] [string]$ResponseFormat, [Parameter(ParameterSetName = 'Parameters')] [ValidateRange(0.0, 2.0)] [double]$Temperature, [Parameter(ParameterSetName = 'Parameters')] [ValidateRange(0.0, 1.0)] [double]$TopP, # Bing Grounding Parameters [Parameter(ParameterSetName = 'Parameters')] [switch]$EnableBingGrounding, [Parameter(ParameterSetName = 'Parameters')] [ValidateNotNullOrEmpty()] [string]$BingConnectionId, [Parameter(ParameterSetName = 'Parameters')] [switch]$AddBingGrounding, [Parameter(ParameterSetName = 'Parameters')] [switch]$RemoveBingGrounding, [Parameter(ParameterSetName = 'Parameters')] [switch]$ClearAllTools ) begin { Write-Verbose "Starting Set-MetroAIResource for Assistant ID: $AssistantId" # Ensure context is set if (-not $script:MetroContext) { throw "Metro AI context not set. Use Set-MetroAIContext first." } } process { try { # Validate mutually exclusive Bing grounding options $bingOptions = @($EnableBingGrounding, $AddBingGrounding, $RemoveBingGrounding) | Where-Object { $_ } if ($bingOptions.Count -gt 1) { throw "Cannot use multiple Bing grounding options simultaneously. Choose one: -EnableBingGrounding, -AddBingGrounding, or -RemoveBingGrounding." } # Validate Bing connection ID is provided when needed if (($EnableBingGrounding -or $AddBingGrounding) -and -not $BingConnectionId) { throw "BingConnectionId is required when using -EnableBingGrounding or -AddBingGrounding." } if ($PSCmdlet.ParameterSetName -eq 'Json') { # Handle JSON file input Write-Verbose "Processing input file: $InputFile" try { $requestBody = Get-Content -Path $InputFile -Raw -ErrorAction Stop | ConvertFrom-Json -Depth 100 -ErrorAction Stop Write-Verbose "Successfully parsed JSON input file" } catch { throw "Failed to parse JSON input file '$InputFile': $($_.Exception.Message)" } # Extract assistant ID from JSON if present, otherwise use parameter $targetAssistantId = if ($requestBody.id) { $requestBody.id } else { $AssistantId } # Remove system-generated properties that shouldn't be updated $requestBody = $requestBody | Select-Object -ExcludeProperty id, object, created_at $confirmMessage = "Update assistant '$targetAssistantId' from file '$InputFile'" } else { # Handle parameter-based updates Write-Verbose "Processing parameter-based update" # Get current resource to preserve existing configuration try { $currentResource = Get-MetroAIResource -AssistantId $AssistantId -ErrorAction Stop Write-Verbose "Retrieved current resource configuration" } catch { throw "Failed to retrieve current resource '$AssistantId': $($_.Exception.Message). Verify the ID exists and you have access." } $targetAssistantId = $AssistantId $requestBody = @{} # Preserve existing values and update only specified parameters if ($Model) { $requestBody.model = $Model } else { $requestBody.model = $currentResource.model } if ($Name) { $requestBody.name = $Name } else { $requestBody.name = $currentResource.name } if ($PSBoundParameters.ContainsKey('Description')) { $requestBody.description = $Description } elseif ($currentResource.description) { $requestBody.description = $currentResource.description } if ($PSBoundParameters.ContainsKey('Instructions')) { $requestBody.instructions = $Instructions } elseif ($currentResource.instructions) { $requestBody.instructions = $currentResource.instructions } if ($PSBoundParameters.ContainsKey('Metadata')) { $requestBody.metadata = $Metadata } elseif ($currentResource.metadata) { $requestBody.metadata = $currentResource.metadata } if ($PSBoundParameters.ContainsKey('ResponseFormat')) { $requestBody.response_format = $ResponseFormat } elseif ($currentResource.response_format) { $requestBody.response_format = $currentResource.response_format } if ($PSBoundParameters.ContainsKey('Temperature')) { $requestBody.temperature = $Temperature } elseif ($null -ne $currentResource.temperature) { $requestBody.temperature = $currentResource.temperature } if ($PSBoundParameters.ContainsKey('TopP')) { $requestBody.top_p = $TopP } elseif ($null -ne $currentResource.top_p) { $requestBody.top_p = $currentResource.top_p } # Handle tools configuration $currentTools = if ($currentResource.tools) { $currentResource.tools } else { @() } $newTools = [System.Collections.Generic.List[object]]::new() if ($ClearAllTools) { Write-Verbose "Clearing all existing tools" # Start with empty tools array } else { # Preserve existing tools unless specifically modifying Bing grounding foreach ($tool in $currentTools) { if ($tool.type -eq 'bing_grounding' -and ($EnableBingGrounding -or $AddBingGrounding -or $RemoveBingGrounding)) { # Skip existing Bing grounding tools when we're modifying them Write-Verbose "Removing existing Bing grounding tool for reconfiguration" continue } $newTools.Add($tool) } } # Add Bing grounding if requested if ($EnableBingGrounding -or $AddBingGrounding) { Write-Verbose "Adding Bing grounding tool with connection: $BingConnectionId" $bingTool = @{ type = 'bing_grounding' bing_grounding = @{ connections = @( @{ connection_id = $BingConnectionId } ) } } $newTools.Add($bingTool) Write-Verbose "Added Bing grounding tool" } # Set tools in request body $requestBody.tools = $newTools.ToArray() # Preserve tool_resources if they exist if ($currentResource.tool_resources) { $requestBody.tool_resources = $currentResource.tool_resources } # Build confirmation message $confirmMessage = "Update assistant '$AssistantId'" $changes = @() if ($Model -and $Model -ne $currentResource.model) { $changes += "model: $($currentResource.model) → $Model" } if ($Name -and $Name -ne $currentResource.name) { $changes += "name: $($currentResource.name) → $Name" } if ($EnableBingGrounding -or $AddBingGrounding) { $changes += "add Bing grounding" } if ($RemoveBingGrounding) { $changes += "remove Bing grounding" } if ($ClearAllTools) { $changes += "clear all tools" } if ($changes.Count -gt 0) { $confirmMessage += " with changes: $($changes -join ', ')" } } # Execute the update with confirmation if ($PSCmdlet.ShouldProcess($confirmMessage, "Set-MetroAIResource")) { Write-Verbose "Updating resource..." Write-Verbose "Request payload: $($requestBody | ConvertTo-Json -Depth 10 -Compress)" $invokeParams = @{ Service = 'assistants' Operation = 'create' Path = $targetAssistantId Method = 'Post' ContentType = 'application/json' Body = $requestBody } $result = Invoke-MetroAIApiCall @invokeParams if ($result -and $result.id) { Write-Information "Successfully updated assistant '$($result.id)'" -InformationAction Continue # Provide feedback about tools configuration if ($result.tools -and $result.tools.Count -gt 0) { $toolTypes = ($result.tools | ForEach-Object { $_.type }) -join ', ' Write-Information "Current tools: $toolTypes" -InformationAction Continue } else { Write-Information "No tools configured" -InformationAction Continue } Write-Verbose "Resource update completed successfully" return $result } else { throw "Resource update appeared to succeed but no ID was returned in the response. This may indicate a service issue." } } } catch { $errorMessage = "Failed to update Metro AI resource '$AssistantId': $($_.Exception.Message)" Write-Error $errorMessage -ErrorAction Stop } } } function Get-MetroAIResource { <# .SYNOPSIS Retrieves details of Metro AI resources (Agent or Assistant). .DESCRIPTION This function queries the specified Metro AI service endpoint to retrieve resource details. If an AssistantId is provided, it returns details for that specific resource; otherwise, it returns a collection of all available resources based on the ApiType. .PARAMETER AssistantId (Optional) The unique identifier of a specific assistant resource to retrieve. If not provided, the function returns all available resources. .EXAMPLE Get-MetroAIResource -AssistantId "resource-123" -Endpoint "https://example.azure.com" -ApiType Agent .EXAMPLE Get-MetroAIResource -Endpoint "https://example.azure.com" -ApiType Assistant .NOTES When an AssistantId is provided, the function returns the detailed resource object; otherwise, it returns an array of resource summaries. #> [Alias("Get-MetroAIAgent")] [Alias("Get-MetroAIAssistant")] [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string]$AssistantId ) try { $path = $AssistantId $result = Invoke-MetroAIApiCall -Service 'assistants' -Operation 'get' -Path $path -Method Get if ($PSBoundParameters['AssistantId']) { return $result } else { return $result.data } } catch { Write-Error "Get-MetroAIResource error: $_" } } function Remove-MetroAIResource { <# .SYNOPSIS Removes one or more Metro AI resources (Agent or Assistant). .DESCRIPTION This function deletes Metro AI resources from the specified endpoint. When an AssistantId is provided, it deletes that specific resource. Otherwise, it retrieves all resources for the specified ApiType and attempts to delete each one. Use caution, as this action is irreversible. .PARAMETER All (Optional) Switch parameter to delete all resources. When used, the function will delete every resource matching the specified ApiType. .PARAMETER AssistantId (Optional) The unique identifier of a specific assistant resource to delete. If provided, only that resource is deleted. .EXAMPLE Remove-MetroAIResource -Endpoint "https://example.azure.com" -ApiType Agent -AssistantId "resource-123" .EXAMPLE Remove-MetroAIResource -Endpoint "https://example.azure.com" -ApiType Assistant -All .NOTES This function permanently deletes resources. Confirm that the resources are no longer needed before executing this command. #> [Alias("Remove-MetroAIAgent")] [Alias("Remove-MetroAIAssistant")] [CmdletBinding(DefaultParameterSetName = 'All')] param ( [Parameter( ParameterSetName = 'All', Mandatory = $false)] [switch]$All, [Alias('id')] [Parameter( ParameterSetName = 'ById', Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] [string]$AssistantId ) begin { $idsToDelete = @() } process { if ($PSCmdlet.ParameterSetName -eq 'ById') { $idsToDelete += $AssistantId } } end { if ($PSCmdlet.ParameterSetName -eq 'All') { $resources = Get-MetroAIResource $idsToDelete = $resources.id } if ($idsToDelete.Count -eq 0) { Write-Error "No resources to delete." return } foreach ($id in $idsToDelete) { try { Invoke-MetroAIApiCall -Service 'assistants' -Operation 'create' -Path $id -Method Delete } catch { Write-Error "Failed to delete resource with ID: $id. Error: $_" } } } } #endregion #region Function Registration function New-MetroAIFunction { <# .SYNOPSIS Registers a custom function for an agent or assistant. .DESCRIPTION Adds a new tool definition to an existing agent or assistant. .PARAMETER Name The name of the function. .PARAMETER Description A description of the function. .PARAMETER RequiredPropertyName The required parameter name. .PARAMETER PropertyDescription A description for the required parameter. .PARAMETER AssistantId The target agent or assistant ID. .PARAMETER Instructions The instructions for the function. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$Description, [Parameter(Mandatory = $true)] [string]$RequiredPropertyName, [Parameter(Mandatory = $true)] [string]$PropertyDescription, [Parameter(Mandatory = $true)] [string]$AssistantId, [Parameter(Mandatory = $true)] [string]$Instructions ) try { $resource = Get-MetroAIResource -AssistantId $AssistantId -Endpoint $Endpoint -ApiType $ApiType $model = $resource.model $reqProps = @{ $RequiredPropertyName = @{ type = "string" description = $PropertyDescription } } $body = @{ instructions = $Instructions tools = @( @{ type = "function" function = @{ name = $Name description = $Description parameters = @{ type = "object" properties = $reqProps required = @($RequiredPropertyName) } } } ) id = $AssistantId model = $model } Invoke-MetroAIApiCall -Service 'assistants' -Operation 'get' -Method Post -ContentType "application/json" -Body $body } catch { Write-Error "New-MetroAIFunction error: $_" } } #endregion #region Threads and Messaging function New-MetroAIThread { <# .SYNOPSIS Creates a new thread. .DESCRIPTION Initiates a new thread for an agent or assistant. #> [CmdletBinding()] param ( ) try { Invoke-MetroAIApiCall -Service 'threads' -Operation 'thread' -Method Post -ContentType "application/json" } catch { Write-Error "New-MetroAIThread error: $_" } } function Get-MetroAIThread { <# .SYNOPSIS Retrieves thread details. .DESCRIPTION Returns details of a specified thread. .PARAMETER ThreadID The thread ID. #> [CmdletBinding()] param ( [string]$ThreadID ) try { $result = Invoke-MetroAIApiCall -Service 'threads' -Operation 'thread' -Path $ThreadID -Method Get if ($PSBoundParameters['ThreadID']) { return $result } else { return $result.data } } catch { Write-Error "Get-MetroAIThread error: $_" } } function Invoke-MetroAIMessage { <# .SYNOPSIS Sends a message to a thread. .DESCRIPTION Sends a message payload to the specified thread. .PARAMETER ThreadID The thread ID. .PARAMETER Message The message content. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ThreadID, [Parameter(Mandatory = $true)] [string]$Message ) try { $body = @(@{ role = "user"; content = $Message }) Invoke-MetroAIApiCall -Service 'threads' -Operation 'thread' -Path ("{0}/messages" -f $ThreadID) -Method Post -ContentType "application/json" -Body $body } catch { Write-Error "Invoke-MetroAIMessage error: $_" } } function Start-MetroAIThreadRun { <# .SYNOPSIS Initiates a run on a thread. .DESCRIPTION Starts a run on the specified thread and waits for completion unless Async is specified. .PARAMETER AssistantId The agent or assistant ID. .PARAMETER ThreadID The thread ID. .PARAMETER Async Run asynchronously. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$AssistantId, [Parameter(Mandatory = $true)] [string]$ThreadID, [switch]$Async ) try { $body = @{ assistant_id = $AssistantId } $runResponse = Invoke-MetroAIApiCall -Service 'threads' ` -Operation 'threadStatus' -Path ("{0}/runs" -f $ThreadID) -Method Post ` -ContentType "application/json" -Body $body if (-not $Async) { $i = 0 do { Start-Sleep -Seconds 10 $runResult = Invoke-MetroAIApiCall -Service 'threads' -Operation 'threadStatus' -Path ("{0}/runs/{1}" -f $ThreadID, $runResponse.id) -Method Get $i++ } while ($runResult.status -ne "completed" -and $i -lt 100) if ($runResult.status -eq "completed") { $result = Invoke-MetroAIApiCall -Service 'threads' -Operation 'messages' -Path ("{0}/messages" -f $ThreadID) -Method Get return $result.data | ForEach-Object { $_.content.text } } else { Write-Error "Run did not complete in time." } } else { Write-Output "Run started asynchronously. Use Get-MetroAIThreadStatus to check." } return $runResponse } catch { Write-Error "Start-MetroAIThreadRun error: $_" } } function Get-MetroAIThreadStatus { <# .SYNOPSIS Retrieves the status of a thread run. .DESCRIPTION Returns status details of a run. .PARAMETER ThreadID The thread ID. .PARAMETER RunID The run ID. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ThreadID, [Parameter(Mandatory = $true)] [string]$RunID ) try { Invoke-MetroAIApiCall -Service 'threads' -Operation 'threadStatus' -Path ("{0}/runs/{1}" -f $ThreadID, $RunID) -Method Get } catch { Write-Error "Get-MetroAIThreadStatus error: $_" } } function Get-MetroAIMessages { <# .SYNOPSIS Retrieves messages from a thread. .DESCRIPTION Returns the messages for the specified thread. .PARAMETER ThreadID The thread ID. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ThreadID ) try { Invoke-MetroAIApiCall -Service 'threads' -Operation 'messages' -Path ("{0}/messages" -f $ThreadID) -Method Get | Select-Object -ExpandProperty data } catch { Write-Error "Get-MetroAIMessages error: $_" } } function Start-MetroAIThreadWithMessages { <# .SYNOPSIS Creates a new thread with an initial message. .DESCRIPTION Initiates a thread and sends an initial message. .PARAMETER Endpoint The base API URL. .PARAMETER MessageContent The initial message. .PARAMETER Async Run asynchronously. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$AssistantId, [Parameter(Mandatory = $true)] [string]$MessageContent, [switch]$Async ) try { $body = @{ assistant_id = $AssistantId; thread = @{ messages = @(@{ role = "user"; content = $MessageContent }) } } $response = Invoke-MetroAIApiCall -Service 'threads' -Operation 'thread' -Path "runs" -Method Post -ContentType "application/json" -Body $body if (-not $Async) { $i = 0 do { Start-Sleep -Seconds 10 Write-Verbose "Checking thread status..." $runResult = Invoke-MetroAIApiCall -Service 'threads' -Operation 'threadStatus' -Path ("{0}/runs/{1}" -f $response.thread_id, $response.id) -Method Get $i++ } while ($runResult.status -ne "completed" -and $i -lt 100) if ($runResult.status -eq "completed") { $result = Invoke-MetroAIApiCall -Service 'threads' -Operation 'messages' -Path ("{0}/messages" -f $response.thread_id) -Method Get return $result.data | ForEach-Object { $_.content.text } } else { Write-Error "Thread run did not complete in time." } } else { Write-Output "Run started asynchronously. Use Get-MetroAIThreadStatus to check." } return @{ ThreadID = $response.thread_id; RunID = $response.id } } catch { Write-Error "Start-MetroAIThreadWithMessages error: $_" } } #endregion #region OpenAPI Definition (Agent Only) function Add-MetroAIAgentOpenAPIDefinition { <# .SYNOPSIS Adds an OpenAPI definition to an agent. .DESCRIPTION Reads an OpenAPI JSON file and adds it as a tool to the specified agent. .PARAMETER AgentId The agent ID. .PARAMETER DefinitionFile The path to the OpenAPI JSON file. .PARAMETER Name Optional name for the OpenAPI definition. .PARAMETER Description Optional description. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$AgentId, [Parameter(Mandatory = $true)] [string]$DefinitionFile, [string]$Name = "", [string]$Description = "" ) try { if ($ApiType -ne 'Agent') { throw "Only Agent API type is supported." } $openAPISpec = Get-Content -Path $DefinitionFile -Raw | ConvertFrom-Json $body = @{ tools = @( @{ type = "openapi" openapi = @{ name = $Name description = $Description auth = @{ type = "managed_identity" security_scheme = @{ audience = "https://cognitiveservices.azure.com/" } } spec = $openAPISpec } } ) } Invoke-MetroAIApiCall -Service 'assistants' -Operation 'openapi' -Path $AgentId -Method Post -ContentType "application/json" -Body $body } catch { Write-Error "Add-MetroAIAgentOpenAPIDefinition error: $_" } } #endregion # Export module members with the Metro prefix. Export-ModuleMember -Function * -Alias * |