Public/Invoke-ChatCompletion.ps1

<#
.SYNOPSIS
Invokes a completion request to a specified AI model provider.
 
.DESCRIPTION
The Invoke-ChatCompletion function sends a prompt to a specified AI model provider and returns the completion response.
The model provider and model name must be specified in the 'provider:model' format. The function dynamically constructs
the provider-specific function name and invokes it to get the response. It can also accept context via the pipeline.
 
.PARAMETER Messages
The messages to be sent to the AI model for completion. This parameter is mandatory and can accept either:
- A string, which will be wrapped in a user message automatically
- A hashtable array containing properly formatted chat messages
 
.PARAMETER Context
Context provided via the pipeline. This will be prepended to the Messages as a user message.
 
.PARAMETER Model
The model to be used for the completion request, specified in 'provider:model' format. Defaults to "openai:gpt-4o-mini".
 
.PARAMETER TextOnly
A switch parameter that, if specified, returns only the response text. If not specified, a custom object containing
the response text, model information, and timestamp is returned.
 
.PARAMETER IncludeElapsedTime
A switch parameter that, if specified, measures and includes the elapsed time of the API request in the output.
 
.EXAMPLE
$Message = New-ChatMessage -Prompt "Hello, world!"
Invoke-ChatCompletion -Messages $Message -Model "openai:gpt-4o-mini"
 
Sends the prompt "Hello, world!" to the OpenAI GPT-4o-mini model and returns the completion response.
 
.EXAMPLE
Invoke-ChatCompletion -Messages "Hello, world!" -Model "openai:gpt-4o-mini" -TextOnly
 
Sends the string "Hello, world!" as a user message to the OpenAI GPT-4o-mini model and returns only the completion response text.
 
.EXAMPLE
$Message = New-ChatMessage -Prompt "Hello, world!"
Invoke-ChatCompletion -Messages $Message -Model "openai:gpt-4o-mini" -IncludeElapsedTime
 
Sends the prompt and returns the completion response with the elapsed time information.
 
.EXAMPLE
Get-Content .\\README.md | Invoke-ChatCompletion -Messages "Summarize this document." -Model "openai:gpt-4o-mini"
 
Sends the content of README.md as context along with the prompt "Summarize this document." to the model.
 
.EXAMPLE
"This is context from the pipeline" | Invoke-ChatCompletion -Messages "Explain the context." -Model "openai:gpt-4o-mini"
 
Sends the pipeline string as context along with the prompt "Explain the context." to the model.
 
.NOTES
The function dynamically constructs the provider-specific function name based on the provider specified in the Model
parameter. If the provider function does not exist, an error is thrown.
Pipeline input is treated as context and prepended to the messages.
 
#>

function Invoke-ChatCompletion {
    [CmdletBinding()]
    param(
        [object]$Messages,

        [string]$Model = "openai:gpt-4o-mini",
        
        [Parameter(ValueFromPipeline)] # Accepts pipeline input, not positional
        [object]$Context, # Changed from [string] to [object]
    
        [switch]$TextOnly,
        [switch]$IncludeElapsedTime
    )

    Begin {
        # Initialize a list to collect pipeline input
        $pipedContext = [System.Collections.Generic.List[string]]::new()
    }

    Process {
        # Collect pipeline input
        if ($PSBoundParameters.ContainsKey('Context')) {
            # Convert the pipeline object to string before adding
            $pipedContext.Add(($Context | Out-String).Trim())
        }
    }

    End {
        if ($env:PSAISUITE_DEFAULT_MODEL) {
            Write-Verbose "Using default model from environment variable `$env:PSAISUITE_DEFAULT_MODEL: $env:PSAISUITE_DEFAULT_MODEL"
            $Model = $env:PSAISUITE_DEFAULT_MODEL
        }

        # Process the main Messages parameter
        $processedMessages = @()
        if ($Messages -is [string]) {
            $processedMessages += @{
                'role'    = 'user'
                'content' = $Messages
            }
        }
        elseif ($Messages -is [array]) {
            $processedMessages = $Messages
        }
        else {
            # Assuming $Messages is a single hashtable message
            $processedMessages += $Messages
        }

        # Prepend piped context if it exists
        if ($pipedContext.Count -gt 0) {
            $contextString = $pipedContext -join "`n"
            $contextMessage = @{
                'role'    = 'user'
                'content' = "Context:\n$contextString"
            }
            # Prepend context message, then the original prompt message
            $finalMessages = @($contextMessage) + $processedMessages
        }
        else {
            $finalMessages = $processedMessages
        }

        # Parse provider and model from the Model parameter
        if ($Model -match "^([^:]+):(.+)$") {
            $provider = $Matches[1].ToLower()
            $modelName = $Matches[2]
        }
        else {
            throw "Model must be specified in 'provider:model' format."
        }

        # Dynamically construct provider function name
        $providerFunction = "Invoke-${provider}Provider"

        # Check if the provider function exists
        if (-not (Get-Command $providerFunction -ErrorAction SilentlyContinue)) {
            throw "Unsupported provider: $provider. No function named $providerFunction found."
        }

        # Start measuring execution time if requested
        if ($IncludeElapsedTime) {
            $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        }

        # Invoke the provider-specific implementation
        $functionParams = @{
            ModelName = $modelName
            Messages  = $finalMessages # Use the potentially modified messages array
        }

        if ($SystemRole) {
            $functionParams.SystemRole = $SystemRole
        }

        $responseText = & $providerFunction @functionParams

        # Stop measuring execution time if requested
        if ($IncludeElapsedTime) {
            $stopwatch.Stop()
            $elapsedTime = $stopwatch.Elapsed
        }

        # Return the result based on the TextOnly switch
        if ($TextOnly -or [System.Convert]::ToBoolean($env:PSAISUITE_TEXT_ONLY)) {
            # Format the text output to include the elapsed time if requested
            if ($IncludeElapsedTime) {
                return "$responseText`n`nElapsed Time: $($elapsedTime.ToString('hh\:mm\:ss\.fff'))"
            }
            else {
                return $responseText
            }
        }
        else {
            # Create the base response object
            $responseObject = [PSCustomObject]@{
                Messages  = ($finalMessages | ConvertTo-Json -Compress) # Use finalMessages
                Response  = $responseText
                Model     = $Model
                Provider  = $provider
                ModelName = $modelName
                Timestamp = Get-Date
            }

            # Add elapsed time if requested
            if ($IncludeElapsedTime) {
                $responseObject | Add-Member -MemberType NoteProperty -Name 'ElapsedTime' -Value $elapsedTime
            }

            if ($SystemRole) {
                $responseObject | Add-Member -MemberType NoteProperty -Name 'SystemRole' -Value $SystemRole
            }

            return $responseObject
        }
    }
}