public/Invoke-DbatoolsAI.ps1

function Invoke-DbatoolsAI {
<#
    .SYNOPSIS
    Executes a natural language query to perform dbatools operations.
 
    .DESCRIPTION
    The Invoke-DbatoolsAI function allows you to execute a natural language query to perform various dbatools operations. It utilizes an AI assistant to generate the corresponding dbatools command and executes it.
 
    Currently, only Copy-DbaDatabase is supported. If you'd like to use Invoke-DbaQuery, check out Invoke-DbaiQuery.
 
    .PARAMETER Message
    The natural language query to execute as a dbatools command.
 
    .PARAMETER AssistantName
    The name of the AI assistant to use for command generation.
 
    .PARAMETER As
    The output format of the result. Supported values are 'PSObject' and 'String'. Default is 'String'.
 
    .EXAMPLE
    PS C:\> Invoke-DbatoolsAI -Message "Copy the SalesDB database from ServerA to ServerB using the network share \\NetworkPath"
 
    This example executes a natural language query to copy the SalesDB database from ServerA to ServerB using the specified network share.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromRemainingArguments, Position = 0)]
        [string[]]$Message,
        [string]$AssistantName = "dbatools",
        [ValidateSet("PSObject", "String")]
        [string]$As = "String"
    )
    begin {
        $PSDefaultParameterValues['Write-Progress:Activity'] = "Getting answer"
        $PSDefaultParameterValues['*:Model'] = "gpt-4o-mini"
        $querykey = $AssistantName

        if (-not $script:threadcache[$querykey]) {
            Write-Verbose "Creating new thread cache object for key $querykey"
            $assistant = Get-Assistant -All | Where-Object Name -eq $AssistantName | Select-Object -First 1
            $cacheobject = [PSCustomObject]@{
                thread    = PSOpenAI\New-Thread
                assistant = $assistant
            }
            $script:threadcache[$querykey] = $cacheobject
        } else {
            $thread = $script:threadcache[$querykey].thread
            $assistant = $script:threadcache[$querykey].assistant
        }

        if (-not $thread) {
            $thread = $script:threadcache[$querykey].thread
        }

        $totalMessages = $Message.Count
        $processedMessages = 0
        $sentence = @()
        $msgs = @()
    }
    process {
        if ($Message -match '^\w+$' -or $Message -match '^\w{ 1 }$') {
            $sentence += "$Message"
        } else {
            $msgs += $Message
        }
    }
    end {
        if ($sentence.Length -gt 0) {
            $msgs += "$sentence"
        }

        foreach ($msg in $msgs) {
            $messages = $rundata = $null
            Write-Progress -Status "Processing message $($processedMessages + 1) of $totalMessages" -PercentComplete ((1 / 10) * 100)

            if (-not $assistant) {
                Write-Progress -Status "Retrieving or creating assistant" -PercentComplete ((2 / 10) * 100)
                $assistant = PSOpenAI\Get-Assistant -All | Where-Object Name -eq $AssistantName | Select-Object -First 1

                if (-not $assistant) {
                    $assistant = New-DbaiAssistant -Name $AssistantName
                }
                $script:threadcache[$querykey].assistant = $assistant
            }

            $null = PSOpenAI\Add-ThreadMessage -ThreadId $thread.id -Role user -Message $msg
            $run = PSOpenAI\Start-ThreadRun -ThreadId $thread.id -Assistant $assistant.Id
            $PSDefaultParameterValues["*:RunId"] = $run.id

            Write-Progress -Status "Waiting for run to complete" -PercentComplete ((3 / 10) * 100)
            $runcount = 0
            while ($null -eq $rundata -and $runcount -lt 25) {
                $rundata = PSOpenAI\Get-ThreadRun -ThreadId $thread.id
                Start-Sleep -Milliseconds 300
                $runcount++
            }

            if ($runcount -ge 25) {
                if ($rundata.status) {
                    throw "Run did not complete in a reasonable amount of time. Failed with status $($rundata.status)"
                } else {
                    throw "Run did not complete in a reasonable amount of time."
                }
            }

            Write-Progress -Status "Current status: $($rundata.status)" -PercentComplete ((4 / 10) * 100)
            $runcount = 0

            while ($rundata.status -notin "requires_action", "completed" -and $runcount -lt 25) {
                Start-Sleep -Milliseconds 300
                $rundata = PSOpenAI\Get-ThreadRun -ThreadId $thread.id
                $runcount++
            }

            if ($runcount -ge 25) {
                if ($rundata.status) {
                    throw "Run did not require action in a reasonable amount of time. Failed with status $($rundata.status)"
                } else {
                    throw "Run did not require action in a reasonable amount of time."
                }
            }

            if ($rundata.status -eq "requires_action") {
                $requiredAction = $rundata.required_action

                if ($requiredAction.type -eq "submit_tool_outputs") {
                    $toolOutputs = $rundata.required_action.submit_tool_outputs.tool_calls
                    $arguments = "$($toolOutputs.function.arguments)".Replace('""', '"_empty"')

                    try {
                        $result = ($arguments | ConvertFrom-Json -ErrorAction Stop)
                    } catch {
                        Write-Warning "Error: $_ Failed to parse arguments: $arguments"
                        continue
                    }

                    $parms = @{
                        Source          = $result.Source
                        Destination     = $result.Destination
                        Database        = $result.Database
                        Force           = $result.Force
                        ErrorVariable   = "err"
                        WarningVariable = "warn"
                        WarningAction   = "SilentlyContinue"
                        ErrorAction     = "SilentlyContinue"
                    }

                    if ($result.WhatIf) {
                        $parms.WhatIf = $true
                    }

                    if ($result.DetachAttach) {
                        $parms.DetachAttach = $true
                    } else {
                        $parms.SharedPath    = $result.SharedPath
                        $parms.BackupRestore = $true
                        $parms.UseLastBackup = $result.UseLastBackup
                    }

                    $parms | ConvertTo-Json | Write-Verbose

                    $output = @(Copy-DbaDatabase @parms)

                    if ($err) {
                        foreach ($e in $err) {
                            # No idea what this even is but it's probably the way I selected stuff
                            if ($e -notmatch "StopUpstreamCommandsException") {
                                $output += "ERROR: $e"
                            }
                        }
                    }

                    if ($warn) {
                        $output += "WARNINGS: $warn"
                    }

                    if (-not $output -and -not $result.WhatIf) {
                        $output = "No data returned."
                    } elseif (-not $output -and $result.WhatIf) {
                        $output = "Ask the user if they'd like to run it for real this time" | ConvertTo-Json
                    } else {
                        $output | ConvertTo-Json | Write-Verbose
                        $output = $output | Out-String | ConvertTo-Json
                    }

                    $innerToolOutputs = @()
                    foreach ($to in $toolOutputs) {
                        if ($to.id -as [string]) {
                            $innerToolOutputs += @{
                                tool_call_id = [string]$to.id
                                output       = $output
                            }
                        }
                    }
                    $innerToolOutputs | ConvertTo-Json -Depth 10 | Write-Verbose
                    try {
                        $null = PSOpenAI\Submit-ToolOutput -Run $rundata -ToolOutput $innerToolOutputs -ErrorAction Stop
                    } catch {
                        Write-Warning $_.Exception.Message
                        return
                    }

                    Write-Progress -Status "Waiting for run to complete" -PercentComplete ((7 / 10) * 100)
                    $runcount = 0
                    while ($null -eq $rundata.usage.completion_tokens -and $runcount -lt 45) {
                        Start-Sleep -Milliseconds 300
                        $rundata = PSOpenAI\Get-ThreadRun -ThreadId $thread.id
                        $runcount++
                    }

                    if ($runcount -ge 45) {
                        throw "Run did not complete in a reasonable amount of time while waiting for completion. Status = $($rundata.status)"
                    }

                    Write-Verbose "Prompt tokens: $($rundata.usage.prompt_tokens)"
                    Write-Verbose "Completion: $($rundata.usage.completion_tokens)"
                    Write-Verbose "Totaltokens: $($rundata.usage.total_tokens)"
                } else {
                    throw "Unsupported required action type: $($requiredAction.type)"
                    break
                }
            }

            Write-Progress -Status "Run completed, waiting for answer" -PercentComplete ((8 / 10) * 100)

            $runcount = 0
            while ($null -eq $messages.content.text.value -and $runcount -lt 25) {
                Start-Sleep -Milliseconds 300
                $messages = PSOpenAI\Get-ThreadMessage -ThreadId $thread.id |
                    Where-Object role -eq assistant |
                    Select-Object -First 1
                $runcount++
            }

            if ($runcount -ge 25) {

                if ($rundata.status) {
                    throw "Run completed, but answer was not received in a reasonable amount of time. Failed with status $($rundata.status)"
                } else {
                    throw "Run completed, but answer was not received in a reasonable amount of time."
                }
            }

            if ($As -eq "String") {
                $messages.content.text.value
            } elseif ($As -eq "PSObject") {
                [PSCustomObject]@{
                    Question     = $msg
                    Answer       = $messages.content.text.value
                    PromptTokens = $rundata.usage.prompt_tokens
                    Completion   = $rundata.usage.completion_tokens
                    TotalTokens  = $rundata.usage.total_tokens
                }
            }
        }
    }
}