PsChat.psm1

using module ".\Private\OutHelper.psm1"
using module ".\Classes\Options.psm1"
using module ".\Classes\OpenAiChat.psm1"
using module ".\Classes\PsChatUi.psm1"

$ErrorActionPreference = "Stop"

function Get-PsChatAnswer {
    <#
    .SYNOPSIS
    Request an answer from OpenAI Chat Completion.
 
    .DESCRIPTION
    This function is a wrapper around OpenAI Chat Completion. It takes a question and returns an answer.
 
    Please note $ENV:OPENAI_AUTH_TOKEN must be set with a valid OpenAI API key.
 
    .PARAMETER InputObject
    The question (which may include message history) to ask. Must be either:
    1) A string or an array of strings, eg. "hello" or @("hello", "whats your name?")
    2) A hashtable/object, eg. @{ "role"="user"; "content"="hello" } or
       @( @{ "role"="user"; "content"="hello" }, @{ "role"="assistant"; "content"="hello" } )
 
    .PARAMETER NoEnumerate
    If set, the InputObject is not enumerated. This is useful if you want to pass an array of hashtables/objects, eg.:
    @(
        @{ "role"="user"; "content"="hello" }
        @{ "role"="assistant"; "content"="hello" }
        @{ "role"="user"; "content"="whats your name?" }
    )
 
    .PARAMETER NumberOfAnswers
    The number of answers to return. Default is 1.
 
    .PARAMETER OpenAiAuthToken
    The OpenAI API key. If not specified, the value of $ENV:OPENAI_AUTH_TOKEN is used.
 
    .PARAMETER Temperature
    The temperature of the model. Higher values means the model will take more risks. Default is 0.9.
 
    .PARAMETER Top_P
    The cumulative probability for top-p sampling. Default is 1.
 
    .PARAMETER NumberOfAnswers
    The number of answers/choices to return. Default is 1.
     
    .EXAMPLE
    Get-PsChatAnswer "What is your name?" # Asks OpenAI Chat for its name.
 
    .EXAMPLE
    "Hello OpenAI" | Get-PsChatAnswer # Says hello to OpenAI using pipes.
 
    .EXAMPLE
    $dialog = @(
        @{ "role"="user"; "content"="Hello OpenAI. Can we talk Powershell?" },
        @{ "role"="assistant"; "content"="Hello! Of course, we can talk about PowerShell. What would you like to know or discuss?" },
        @{ "role"="user"; "content"="How does piping work?" }
        )
    Get-PsChatAnswer -InputObject $dialog -NoEnumerate # Asks OpenAI a question, based on previous messages.
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [PSObject[]]$InputObject,
        [Switch]$NoEnumerate,
        [int]$NumberOfAnswers = 1,
        [string]$OpenAiAuthToken,
        [string]$Model,
        [decimal]$Temperature,
        [decimal]$Top_P,
        [ResultType]$ResultType = [ResultType]::LastAnswerAsText
    )

    Begin {
        # Initialize any variables or resources needed for the function
        $authToken = if($OpenAiAuthToken) { $OpenAiAuthToken } else { $ENV:OPENAI_AUTH_TOKEN }
        $chatApi = [OpenAiChat]::new($authToken)
        $chatApi.Stream = $false
        if($Temperature) { $chatApi.Temperature = $Temperature }
        if($Top_P) { $chatApi.Top_p = $Top_P }
        if($NumberOfAnswers -ne 1) { $chatApi.N = $NumberOfAnswers }
        if($Model) { $chatApi.Model = $Model }

        if($NumberOfAnswers -gt 1 -and $ResultType -eq [ResultType]::LastAnswerAsText) {
            Write-Warning ("NumberOfAnswers is greater than 1, but ResultType is set to LastAnswerAsText. "+ `
                "This will only return the last answer. ResultType should be set to Objects to return all answers")
        }
    }

    Process {
        function Write-Answer($answer) {
            switch($ResultType) {
                None { return }
                Objects { return $answer | Select-Object -Property Role, Content, AltChoices }
                LastAnswerAsText {
                    return $answer.Content
                }
            }                
            Write-Output -InputObject $answer
        }

        # handle array of hashtable/object, eg. @( @{ "role"="user"; "content"="hello" } )
        if($NoEnumerate -and $InputObject -is [array]) {
            Write-Answer $chatApi.GetAnswer($InputObject)
        } else {
            # iterate over each item in the pipeline
            foreach ($item in $InputObject) {
                $messages = @()

                $answer = $null

                # handle string, eg. "hello"
                if($item -is [string]) {
                    $messages += [OpenAiChatMessage]::FromUser($item)
                    $answer = $chatApi.GetAnswer($messages)
                }

                # handle hashtable/object, eg. @{ "role"="user"; "content"="hello" }
                if($item -is [Hashtable]) {
                    $messages += [OpenAiChatMessage]::Parse($item)
                    $answer = $chatApi.GetAnswer($messages)
                }

                if($null -ne $answer) {
                    Write-Answer $answer
                }
            }
        }
    }

    End {
    }
}

function Invoke-PsChat {
    <#
    .SYNOPSIS
    Create an interactive chat session with OpenAI Chat Completion in Powershell.
 
    .DESCRIPTION
    This function creates an interactive chat session with OpenAI Chat Completion in Powershell.
 
    You can press 'h' in the chat to get help.
 
    Please note $ENV:OPENAI_AUTH_TOKEN must be set with a valid OpenAI API key.
 
    .PARAMETER Question
    The initial question to ask the OpenAI Chat. This parameter is optional.
 
    .PARAMETER Single
    Specifies that the execution will end after the response to the initial question.
    This parameter is optional.
 
    .PARAMETER PreLoad_Path
    Specifies the path to a JSON-file containing chat messages (useful for providing context).
    This parameter is optional.
 
    .PARAMETER PreLoad_Lock
    The preloaded messages will always be prefixed to the dialog, keeping the context.
    This parameter is optional.
 
    .PARAMETER AutoSave_Enabled
    Specifies whether the chat messages should be autosaved or not.
    This parameter is optional, and takes a Switch datatype.
 
    .PARAMETER AutoSave_Path
    Specifies the path (file name) to where autosaved chat messages should be stored.
    This parameter is optional.
 
    .PARAMETER WordCountWarning_Threshold
    Specifies the maximum number of words before a warning should be issued.
    This parameter is optional, and takes an Integer datatype. Its default value is 300 to minimize cost.
    You can you the 'z' command to compress the dialog into a single message.
 
    .PARAMETER OpenAiAuthToken
    The OpenAI API key. If not specified, the value of $ENV:OPENAI_AUTH_TOKEN is used.
 
    .PARAMETER Api_Model
    The model to use. Default is "gpt-3".
     
    .PARAMETER Api_Temperature
    The temperature of the model. Higher values means the model will take more risks. Default is 0.9.
 
    .PARAMETER Api_Top_P
    The cumulative probability for top-p sampling. Default is 1.
 
    .EXAMPLE
    Invoke-PsChat "What is your name?" # Start a chat by asking OpenAI Chat for its name.
 
    .EXAMPLE
    Invoke-PsChat "What is your name?" -Single # Asks the question and quits.
    #>

    param(
        # Initial invocation parameters
        [Parameter(Position=0)][string]$Question,
        [Parameter(Position=1)][Switch]$Single,
        [Parameter(Position=2)][Switch]$NonInteractive,
        [Parameter(Position=3)][Switch]$SkipQuestion,
        [ResultType]$ResultType = [ResultType]::None,
        [string]$OpenAiAuthToken,
        [Parameter(ValueFromRemainingArguments=$true)]
        [object[]]$AdditionalArguments
        )

    $options = [Options]::new()
    $options.AdditionalArguments = $AdditionalArguments
    $options.InitialQuestion = $Question
    $options.SingleQuestion = $Single
    $options.NonInteractive = $NonInteractive
    $options.SkipQuestion = $SkipQuestion

    # disable all output if non-interactive
    [OutHelper]::HostOutput = -not($NonInteractive)

    # initialize the api
    $authToken = if($OpenAiAuthToken) { $OpenAiAuthToken } else { $ENV:OPENAI_AUTH_TOKEN }
    $chat = [PsChatUi]::new($authToken, $options)
    $dlg = $chat.Start()

    # output answer if specified
    switch($ResultType) {
        None { return }
        Objects { return [DialogMessage]::AsObjects($dlg.Messages) }
        LastAnswerAsText {
            return $dlg.Messages[-1].Content 
        }
    }
}

function New-OpenAiChat {
    <#
    .SYNOPSIS
    Instantiates the OpenAiChat API wrapper for external use cases.
 
    .DESCRIPTION
    This function creates an instance of OpenAiChat, the internal wrapper class for the OpenAI Chat Completion API.
 
    .PARAMETER AuthToken
    The OpenAI API key to use for authentication.
 
    .EXAMPLE
    #>

    param(
        [string]$OpenAiAuthToken
    )

    # initialize the api
    $authToken = if($OpenAiAuthToken) { $OpenAiAuthToken } else { $ENV:OPENAI_AUTH_TOKEN }
    return [OpenAiChat]::new($authToken)
}