Functions/GenXdev.Helpers/ConvertTo-LLMOpenAIApiFunctionDefinition.ps1

<###############################################################################
.SYNOPSIS
Converts PowerShell functions to LLM OpenAI API function definitions.

.DESCRIPTION
Takes exposed cmdlet definitions and generates LLM OpenAI API compatible function definitions
including parameter information and callback handlers.

.LICENSE
Copyright (C) 2026 René Vaessen / GenXdev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

.PARAMETER ExposedCmdLets
Array of custom objects containing function definitions and their allowed parameters to convert to LLM OpenAI API definitions.

.EXAMPLE
Get-Command Get-Process | ConvertTo-LLMOpenAIApiFunctionDefinition

#>

function ConvertTo-LLMOpenAIApiFunctionDefinition {

    [CmdletBinding()]
    [OutputType([System.Collections.Generic.List[hashtable]])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
    param(
        #######################################################################
        # Array of custom objects containing function definitions and their allowed parameters
        [Parameter(
            Mandatory = $false,
            Position = 0,
            ValueFromPipeline = $true,
            HelpMessage = 'PowerShell commands to convert to tool functions'
        )]
        [GenXdev.Helpers.ExposedCmdletDefinition[]] $ExposedCmdLets = @()
    )

    begin {

        # Initialize collection to store the converted function definitions
        [System.Collections.Generic.List[hashtable]] $result = Microsoft.PowerShell.Utility\New-Object 'System.Collections.Generic.List[System.Collections.Hashtable]'

        Microsoft.PowerShell.Utility\Write-Verbose 'Starting conversion of PowerShell functions to LLM OpenAI API format'
    }


    process {

        if ($ExposedCmdLets) {

            foreach ($currentCommand in $ExposedCmdLets) {

                # Retrieve command info for runtime callback (needed to
                # invoke the command at runtime) and for runtime type
                # checks (enum, array, list, switch detection).
                $commandInfo = Microsoft.PowerShell.Core\Get-Command `
                    -Name ($currentCommand.Name) `
                    -ErrorAction SilentlyContinue |
                    Microsoft.PowerShell.Utility\Select-Object -First 1

                if ($null -eq $commandInfo) {
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "Command $($currentCommand.Name) not found. Skipping."
                    continue
                }

                # Leverage Get-CmdletMetaData for all parameter metadata,
                # help messages, output types, and function description.
                $metaData = GenXdev\Get-CmdletMetaData `
                    -Name $currentCommand.Name `
                    -ErrorAction SilentlyContinue

                if ($null -eq $metaData) {
                    Microsoft.PowerShell.Utility\Write-Verbose `
                        "Could not get metadata for $($currentCommand.Name). Skipping."
                    continue
                }

                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Processing command: $($currentCommand.Name)"

                # Extract allowed parameters with optional type overrides
                $allowedParams = @($currentCommand.AllowedParams)

                # Collections to track parameter metadata
                [System.Collections.Generic.List[string]]$requiredParams = @()
                $propertiesTable = @{}

                # Process each parameter from Get-CmdletMetaData
                foreach ($param in $metaData.Parameters) {

                    # Check if parameter is in allowed list and extract
                    # optional type override (e.g. "Name=string").
                    # Match against parameter name AND aliases since
                    # ExposedCmdletDefinitions often use alias names
                    # (e.g. "SearchMask" for the "Name" parameter).
                    $found = $false
                    $typeStr = ''
                    foreach ($allowedParam in $allowedParams) {
                        $parts = "$allowedParam".Split('=')
                        $name = $parts[0].Trim()

                        if ($param.Name -like $name) {
                            $found = $true
                            if ($parts.Length -gt 1) {
                                $typeStr = $parts[1].Trim()
                            }
                            break
                        }

                        # Also check aliases (AllowedParams often
                        # reference parameters by their alias names)
                        if ($param.Aliases -and
                            $param.Aliases.Count -gt 0) {
                            foreach ($alias in $param.Aliases) {
                                if ($alias -like $name) {
                                    $found = $true
                                    if ($parts.Length -gt 1) {
                                        $typeStr = $parts[1].Trim()
                                    }
                                    break
                                }
                            }
                            if ($found) { break }
                        }
                    }

                    if (-not $found) { continue }

                    # Track mandatory parameters
                    if ($param.Mandatory -and
                        $requiredParams.Contains($param.Name) -eq $false) {
                        $null = $requiredParams.Add($param.Name)
                    }

                    # Determine LLM type: use type override if specified,
                    # otherwise convert the .NET type
                    $llmType = if ([string]::IsNullOrWhiteSpace($typeStr)) {
                        GenXdev\Convert-DotNetTypeToLLMType `
                            -DotNetType $param.ParameterType
                    }
                    else { $typeStr }

                    # Build parameter property dictionary
                    $propDef = @{
                        type            = $llmType
                        powershell_type = $param.ParameterType
                    }

                    if (-not [string]::IsNullOrWhiteSpace(
                            $param.HelpMessage)) {
                        $propDef.description = $param.HelpMessage
                    }

                    # Runtime type checks: enum, switch, array, list.
                    # Get-CmdletMetaData returns type names as strings,
                    # so we cross-reference with $commandInfo.Parameters
                    # for .NET reflection (IsEnum, IsArray, etc.).
                    $rtParam = $commandInfo.Parameters[$param.Name]
                    if ($rtParam) {
                        $rtType = $rtParam.ParameterType

                        # Enum → string + allowed values
                        if ($rtType.IsEnum) {
                            $propDef.type = 'string'
                            $propDef.enum = @($rtType.GetEnumNames())
                        }

                        # SwitchParameter → boolean
                        if ($rtType.FullName -eq
                            'System.Management.Automation.SwitchParameter') {
                            if ([string]::IsNullOrWhiteSpace($typeStr)) {
                                $propDef.type = 'boolean'
                            }
                            Microsoft.PowerShell.Utility\Write-Verbose `
                                "Switch parameter '$($param.Name)' set to type '$($propDef.type)'"
                        }

                        # Array types → array + items
                        if ($rtType.IsArray) {
                            $elementType = $rtType.GetElementType().FullName
                            $itemType = GenXdev\Convert-DotNetTypeToLLMType `
                                -DotNetType $elementType
                            $propDef.type = 'array'
                            $propDef.items = @{ type = $itemType }
                        }
                        elseif ($rtType.IsGenericType -and
                            $rtType.Name -like 'List`1*') {
                            $elementType =
                                $rtType.GetGenericArguments()[0].FullName
                            $itemType = GenXdev\Convert-DotNetTypeToLLMType `
                                -DotNetType $elementType
                            $propDef.type = 'array'
                            $propDef.items = @{ type = $itemType }
                        }
                    }

                    $propertiesTable[$param.Name] = $propDef
                }

                # Function-level description: prefer the explicit
                # Description from ExposedCmdletDefinition, then fall
                # back to Get-CmdletMetaData's Description/Synopsis.
                $functionHelpMessage = $currentCommand.Description
                if ([string]::IsNullOrWhiteSpace($functionHelpMessage)) {
                    $functionHelpMessage = $metaData.Description
                }
                if ([string]::IsNullOrWhiteSpace($functionHelpMessage)) {
                    $functionHelpMessage = $metaData.Synopsis
                }
                if ([string]::IsNullOrWhiteSpace($functionHelpMessage)) {
                    $functionHelpMessage = 'No description available.'
                }

                # Output type information from Get-CmdletMetaData
                $returnType = ''
                $powershell_returnType = ''
                if ($metaData.OutputType -and
                    $metaData.OutputType.Count -gt 0 -and
                    -not [string]::IsNullOrWhiteSpace(
                        $metaData.OutputType[0])) {
                    $powershell_returnType = $metaData.OutputType[0]
                    $returnType = GenXdev\Convert-DotNetTypeToLLMType `
                        -DotNetType $powershell_returnType
                }

                # Check if function requires confirmation based on
                # ExposedCmdletDefinition configuration
                $name = $commandInfo.Name
                $found = $false
                $allCmdletNames = @($name.ToLowerInvariant())
                $NoConfirmationToolFunctionNames = @(
                    $ExposedCmdLets |
                        Microsoft.PowerShell.Core\Where-Object `
                            -Property Confirm -EQ $false |
                        Microsoft.PowerShell.Utility\Select-Object `
                            -ExpandProperty Name)

                foreach ($AllowedCmdLet in $NoConfirmationToolFunctionNames) {
                    if ($AllowedCmdLet.ToLowerInvariant() -in
                        $allCmdletNames) {
                        $found = $true
                        break
                    }
                }

                # Construct the final function definition object
                $newFunctionDefinition = @{
                    type     = 'function'
                    function = @{
                        name        = "$name"
                        description = "$functionHelpMessage"
                        parameters  = @{
                            type       = 'object'
                            properties = $propertiesTable
                            required   = $requiredParams
                        }
                        callback    = $commandInfo
                    }
                }

                # Add return type information if available
                if (-not [string]::IsNullOrWhiteSpace(
                        $powershell_returnType)) {
                    $newFunctionDefinition.function.powershell_returnType =
                        $powershell_returnType
                }

                if (-not [string]::IsNullOrWhiteSpace($returnType)) {
                    $newFunctionDefinition.function.returnType = $returnType
                }

                # Add the completed function definition to results
                $null = $result.Add($newFunctionDefinition)
            }
        }
    }

    end {

        # Return the collection of converted function definitions
        Microsoft.PowerShell.Utility\Write-Verbose "Completed conversion with $($result.Count) function definitions"
        Microsoft.PowerShell.Utility\Write-Output $result
    }
}