Functions/GenXdev.AI/Invoke-LMStudioQuery.ps1

################################################################################
<#
.SYNOPSIS
Queries the LM-Studio API with given parameters and returns the response.
 
.DESCRIPTION
Sends a query to the LM-Studio API and returns the response. Can handle text and
image inputs, manages model loading, and supports various query parameters.
 
.PARAMETER Query
The query string to send to the LLM.
 
.PARAMETER Attachments
File paths of attachments to send with the query.
 
.PARAMETER Instructions
System instructions for the LLM.
 
.PARAMETER Model
The LM-Studio model to use.
 
.PARAMETER Temperature
Controls response randomness (0.0-1.0).
 
.PARAMETER Max_token
Maximum tokens to generate in response.
 
.PARAMETER ImageDetail
Detail level for image attachments (low/medium/high).
 
.PARAMETER ShowLMStudioWindow
Shows the LM-Studio window when set.
 
.PARAMETER IncludeThoughts
Include <think></think> patterns in output.
 
.EXAMPLE
Invoke-LMStudioQuery -Query "What is PowerShell?" -Temperature 0.7
 
.EXAMPLE
qlms "Analyze this code" -Attachments ".\script.ps1" -Instructions "Be thorough"
#>

function Invoke-LMStudioQuery {

    [CmdletBinding()]
    [Alias("qlms")]
    param (
        ########################################################################
        [Parameter(
            Position = 0,
            Mandatory = $true,
            HelpMessage = "Query string for the LLM"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Query,

        ########################################################################
        [Parameter(
            Position = 1,
            Mandatory = $false,
            HelpMessage = "File paths of attachments"
        )]
        [string[]] $Attachments = @(),

        ########################################################################
        [Parameter(
            Position = 2,
            Mandatory = $false,
            HelpMessage = "System instructions for LLM"
        )]
        [string] $Instructions = "Your an AI assistent that never tells a lie " +
            "and always answers truthfully, first comprehensive then consice.",

        ################################################################################
        [Parameter(
            Position = 3,
            Mandatory = $false,
            HelpMessage = "The LM-Studio model to use for generating the response.")]
        [PSDefaultValue(Value = "llama")]
        [string]$Model = "llama",

        ################################################################################
        [Parameter(
            Mandatory = $false,
            Position = 4,
            HelpMessage = "The temperature parameter for controlling the randomness of the response."
        )]
        [ValidateRange(0.0, 1.0)]
        [double] $Temperature = 0.01,

        ################################################################################
        [Parameter(
            Mandatory = $false,
            Position = 5,
            HelpMessage = "The maximum number of tokens to generate in the response."
        )]
        [int] $Max_token = -1,

        ################################################################################
        [Parameter(
            Mandatory = $false,
            Position = 6,
            HelpMessage = "The image detail to use for the attachments."
        )]
        [ValidateSet("low", "medium", "high")]
        [string] $ImageDetail = "low",

        [Parameter(
            Mandatory = $false,
            HelpMessage = "Show the LM-Studio window."
        )]
        [Switch] $ShowLMStudioWindow,
        ################################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Includes <think></think> patterns in output."
        )]
        [Switch] $IncludeThoughts
    )

    begin {
        # get full path expansions for lm studio executables
        $lmStudioPath = Get-ChildItem `
            "${env:LOCALAPPDATA}\LM-Studio\lm studio.exe", `
            "${env:LOCALAPPDATA}\Programs\LM-Studio\lm studio.exe" `
            -File -rec -ErrorAction SilentlyContinue |
            Select-Object -First 1 |
            ForEach-Object FullName

        $lmsPath = Get-ChildItem `
            "${env:LOCALAPPDATA}\LM-Studio\lms.exe", `
            "${env:LOCALAPPDATA}\Programs\LM-Studio\lms.exe" `
            -File -rec -ErrorAction SilentlyContinue |
            Select-Object -First 1 |
            ForEach-Object FullName
    }

    process {
        function IsLMStudioInstalled {
            return Test-Path -Path $lmsPath -ErrorAction SilentlyContinue
        }

        # Function to check if LMStudio is running
        function IsLMStudioRunning {
            $process = Get-Process -Name "LM Studio" -ErrorAction SilentlyContinue | Where-Object -Property MainWindowHandle -NE 0
            if ($null -ne $process) {
                $process.PriorityClass = "Idle"
                $w = [GenXdev.Helpers.WindowObj]::new($process.MainWindowHandle, "LM Studio");
                if ($null -ne $w) {
                    if ($ShowLMStudioWindow) {
                        $w.Maximize();
                        $w.Show();
                        (Get-PowershellMainWindow).Focus();
                        wp -Left -Process (Get-PowershellMainWindowProcess)
                        wp -Right -Process $process
                    }
                }
            }
            return $null -ne $process
        }

        function IsWinGetInstalled {
            Import-Module "Microsoft.WinGet.Client" -ErrorAction SilentlyContinue
            $module = Get-Module "Microsoft.WinGet.Client" -ErrorAction SilentlyContinue
            if ($null -eq $module) {
                return $false
            }
            return $true
        }

        function InstallWinGet {
            Write-Verbose "Installing WinGet PowerShell client.."
            Install-Module "Microsoft.WinGet.Client" -Force -AllowClobber
            Import-Module "Microsoft.WinGet.Client"
        }

        function InstallLMStudio {
            if (-not (IsWinGetInstalled)) {
                InstallWinGet
            }
            $lmStudio = "ElementLabs.LMStudio"
            $lmStudioPackage = Get-WinGetPackage -Id $lmStudio
            if ($null -eq $lmStudioPackage) {
                Write-Verbose "Installing LM-Studio.."
                try {
                    Install-WinGetPackage -Id $lmStudio -Force
                }
                catch {
                    winget install $lmStudio
                }
                # get full path expansions for lm studio executables
                $lmStudioPath = Get-ChildItem `
                    "${env:LOCALAPPDATA}\LM-Studio\lm studio.exe", `
                    "${env:LOCALAPPDATA}\Programs\LM-Studio\lm studio.exe" `
                    -File -rec -ErrorAction SilentlyContinue |
                Select-Object -First 1 |
                ForEach-Object FullName

                $lmsPath = Get-ChildItem `
                    "${env:LOCALAPPDATA}\LM-Studio\lms.exe", `
                    "${env:LOCALAPPDATA}\Programs\LM-Studio\lms.exe" `
                    -File -rec -ErrorAction SilentlyContinue |
                Select-Object -First 1 |
                ForEach-Object FullName
            }
        }

        # Function to start LMStudio if it's not running
        function Start-LMStudio {
            if (-not (IsLMStudioInstalled)) {
                InstallLMStudio
            }
            if (-not (IsLMStudioRunning)) {
                Write-Verbose "Starting LM-Studio..";
                # get full path expansions for lm studio executables
                $lmStudioPath = Get-ChildItem `
                    "${env:LOCALAPPDATA}\LM-Studio\lm studio.exe", `
                    "${env:LOCALAPPDATA}\Programs\LM-Studio\lm studio.exe" `
                    -File -rec -ErrorAction SilentlyContinue |
                Select-Object -First 1 |
                ForEach-Object FullName

                $lmsPath = Get-ChildItem `
                    "${env:LOCALAPPDATA}\LM-Studio\lms.exe", `
                    "${env:LOCALAPPDATA}\Programs\LM-Studio\lms.exe" `
                    -File -rec -ErrorAction SilentlyContinue |
                Select-Object -First 1 |
                ForEach-Object FullName

                Start-Job { param($lmStudioPath) Start-Process -FilePath $lmStudioPath -WindowStyle Minimized } -ArgumentList @($lmStudioPath) | Out-Null
                Start-Sleep -Seconds 10
                IsLMStudioRunning | Out-Null
            }
        }

        # Function to get the list of models
        function Get-ModelList {
            Write-Verbose "Getting installed model list.."
            $ModelList = & "$lmsPath" ls --json | ConvertFrom-Json
            return $ModelList
        }
        function Get-LoadedModelList {
            Write-Verbose "Getting loaded model list.."
            $ModelList = & "$lmsPath" ps --json | ConvertFrom-Json
            return $ModelList
        }

        # Function to load the LLava model
        function LoadLMStudioModel {
            $ModelList = Get-ModelList
            $foundModel = $ModelList | Where-Object { $PSItem.path -like "*$Model*" } | Select-Object -First 1
            if (-not $foundModel) {
                $preferredModelList = @("llama", "vicuna", "alpaca", "gpt", "falcon", "mpt", "koala", "wizard", "guanaco", "bloom", "rwkv", "camel", "pythia", "baichuan")
                foreach ($preferredModel in $preferredModelList) {
                    $foundModel = $ModelList | Where-Object { $PSItem.path -like "*$preferredModel*" } | Select-Object -First 1
                    if ($foundModel) {
                        break
                    }
                }
            }
            if (-not $foundModel) {
                $foundModel = $ModelList | Select-Object -First 1
            }
            if (-not $foundModel) {
                $ShowLMStudioWindow = $true
                IsLMStudioRunning | Out-Null
                throw "Model with path: '*$Model*', not found. Please install and configure startup-parameters manually in LM-Studio first."
            }
            Write-Output $foundModel
            $foundModelLoaded = (Get-LoadedModelList) | Where-Object {
                $PSItem.path -eq $foundModel.path
            } | Select-Object -First 1
            if ($null -eq $foundModelLoaded) {
                $success = $true;
                try {
                    Write-Verbose "Loading model.."
                    [System.Console]::Write("`r`n");
                    if (-not (Get-HasCapableGpu)) {
                        & "$lmsPath" load "$($foundModel.path)" --gpu off --exact
                    }
                    else {
                        & "$lmsPath" load "$($foundModel.path)" --exact
                    }
                    if ($LASTEXITCODE -ne 0) {
                        $success = $false;
                    }
                    else {
                        # ansi for cursor up and clear line
                        [System.Console]::Write("`e[1A`e[2K")
                        # ansi for cursor up and clear line
                        [System.Console]::Write("`e[1A`e[2K")
                        # ansi for cursor up and clear line
                        [System.Console]::Write("`e[1A`e[2K")
                        # ansi for cursor up and clear line
                        [System.Console]::Write("`e[1A`e[2K")
                        # ansi for cursor up and clear line
                        [System.Console]::Write("`e[1A`e[2K")
                        # ansi for cursor up and clear line
                        [System.Console]::Write("`e[1A`e[2K")
                    }
                }
                catch {
                    $success = $false;
                }
                if (-not $success) {
                    & "$lmsPath" unload --all
                    $success = $true;
                    try {
                        Write-Verbose "Loading model.."
                        [System.Console]::Write("`r`n");
                        if (-not (Get-HasCapableGpu)) {
                            & "$lmsPath" load "$($foundModel.path)" --gpu off --exact
                        }
                        else {
                            & "$lmsPath" load "$($foundModel.path)" --exact
                        }
                        if ($LASTEXITCODE -ne 0) {
                            $success = $false;
                        }
                        else {
                            # ansi for cursor up and clear line
                            [System.Console]::Write("`e[1A`e[2K")
                            # ansi for cursor up and clear line
                            [System.Console]::Write("`e[1A`e[2K")
                            # ansi for cursor up and clear line
                            [System.Console]::Write("`e[1A`e[2K")
                            # ansi for cursor up and clear line
                            [System.Console]::Write("`e[1A`e[2K")
                            # ansi for cursor up and clear line
                            [System.Console]::Write("`e[1A`e[2K")
                            # ansi for cursor up and clear line
                            [System.Console]::Write("`e[1A`e[2K")
                        }
                    }
                    catch {
                        $success = $false;
                    }
                }
                if (-not $success) {
                    $ShowLMStudioWindow = $true
                    IsLMStudioRunning | Out-Null
                    throw "Model with path: '*$Model*', not found. Please install and configure startup-parameters manually in LM-Studio first."
                }
            }
            $foundModelLoaded = (Get-LoadedModelList) | Where-Object {
                $PSItem.path -eq $foundModel.path
            } | Select-Object -First 1
            if ($null -eq $foundModelLoaded) {
                $ShowLMStudioWindow = $true
                IsLMStudioRunning | Out-Null
                throw "Model with path: '*$Model*', not found. Please install and configure startup-parameters manually in LM-Studio first."
            }
            $foundModelLoaded
        }

        # Function to upload image and query to LM-Studio local server
        function QueryLMStudio {
            param (
                $foundModelLoaded,
                [string]$Instructions,
                [string]$Query,
                [string[]]$Attachments,
                [double]$Temperature,
                [int]$Max_token = -1
            )
            $messages = [System.Collections.ArrayList]@()
            $messages.Add(
                @{
                    role    = "system"
                    content = "$Instructions"
                }
            ) | Out-Null;
            $Attachments | ForEach-Object {
                $filePath = Expand-Path $PSItem;
                $fileExtension = [IO.Path]::GetExtension($filePath).ToLowerInvariant();
                $mimeType = "application/octet-stream";
                $isText = $false;
                switch ($fileExtension) {
                    ".jpg" {
                        $mimeType = "image/jpeg"
                        $isText = $false
                    }
                    ".jpeg" {
                        $mimeType = "image/jpeg"
                        $isText = $false
                    }
                    ".png" {
                        $mimeType = "image/png"
                        $isText = $false
                    }
                    ".gif" {
                        $mimeType = "image/gif"
                        $isText = $false
                    }
                    ".bmp" {
                        $mimeType = "image/bmp"
                        $isText = $false
                    }
                    ".tiff" {
                        $mimeType = "image/tiff"
                        $isText = $false
                    }
                    ".mp4" {
                        $mimeType = "video/mp4"
                        $isText = $false
                    }
                    ".avi" {
                        $mimeType = "video/avi"
                        $isText = $false
                    }
                    ".mov" {
                        $mimeType = "video/quicktime"
                        $isText = $false
                    }
                    ".webm" {
                        $mimeType = "video/webm"
                        $isText = $false
                    }
                    ".mkv" {
                        $mimeType = "video/x-matroska"
                        $isText = $false
                    }
                    ".flv" {
                        $mimeType = "video/x-flv"
                        $isText = $false
                    }
                    ".wmv" {
                        $mimeType = "video/x-ms-wmv"
                        $isText = $false
                    }
                    ".mpg" {
                        $mimeType = "video/mpeg"
                        $isText = $false
                    }
                    ".mpeg" {
                        $mimeType = "video/mpeg"
                        $isText = $false
                    }
                    ".3gp" {
                        $mimeType = "video/3gpp"
                        $isText = $false
                    }
                    ".3g2" {
                        $mimeType = "video/3gpp2"
                        $isText = $false
                    }
                    ".m4v" {
                        $mimeType = "video/x-m4v"
                        $isText = $false
                    }
                    ".webp" {
                        $mimeType = "image/webp"
                        $isText = $false
                    }
                    ".heic" {
                        $mimeType = "image/heic"
                        $isText = $false
                    }
                    ".heif" {
                        $mimeType = "image/heif"
                        $isText = $false
                    }
                    ".avif" {
                        $mimeType = "image/avif"
                        $isText = $false
                    }
                    ".jxl" {
                        $mimeType = "image/jxl"
                        $isText = $false
                    }
                    ".ps1" {
                        $mimeType = "text/x-powershell"
                        $isText = $true
                    }
                    ".psm1" {
                        $mimeType = "text/x-powershell"
                        $isText = $true
                    }
                    ".psd1" {
                        $mimeType = "text/x-powershell"
                        $isText = $true
                    }
                    ".sh" {
                        $mimeType = "application/x-sh"
                        $isText = $true
                    }
                    ".bat" {
                        $mimeType = "application/x-msdos-program"
                        $isText = $true
                    }
                    ".cmd" {
                        $mimeType = "application/x-msdos-program"
                        $isText = $true
                    }
                    ".py" {
                        $mimeType = "text/x-python"
                        $isText = $true
                    }
                    ".rb" {
                        $mimeType = "application/x-ruby"
                        $isText = $true
                    }
                    ".txt" {
                        $mimeType = "text/plain"
                        $isText = $true
                    }
                    ".pl" {
                        $mimeType = "text/x-perl"
                        $isText = $true
                    }
                    ".php" {
                        $mimeType = "application/x-httpd-php"
                        $isText = $true
                    }
                    ".js" {
                        $mimeType = "application/javascript"
                        $isText = $true
                    }
                    ".ts" {
                        $mimeType = "application/typescript"
                        $isText = $true
                    }
                    ".java" {
                        $mimeType = "text/x-java-source"
                        $isText = $true
                    }
                    ".c" {
                        $mimeType = "text/x-c"
                        $isText = $true
                    }
                    ".cpp" {
                        $mimeType = "text/x-c++src"
                        $isText = $true
                    }
                    ".cs" {
                        $mimeType = "text/x-csharp"
                        $isText = $true
                    }
                    ".go" {
                        $mimeType = "text/x-go"
                        $isText = $true
                    }
                    ".rs" {
                        $mimeType = "text/x-rustsrc"
                        $isText = $true
                    }
                    ".swift" {
                        $mimeType = "text/x-swift"
                        $isText = $true
                    }
                    ".kt" {
                        $mimeType = "text/x-kotlin"
                        $isText = $true
                    }
                    ".scala" {
                        $mimeType = "text/x-scala"
                        $isText = $true
                    }
                    ".r" {
                        $mimeType = "text/x-r"
                        $isText = $true
                    }
                    ".sql" {
                        $mimeType = "application/sql"
                        $isText = $true
                    }
                    ".html" {
                        $mimeType = "text/html"
                        $isText = $true
                    }
                    ".css" {
                        $mimeType = "text/css"
                        $isText = $true
                    }
                    ".xml" {
                        $mimeType = "application/xml"
                        $isText = $true
                    }
                    ".json" {
                        $mimeType = "application/json"
                        $isText = $true
                    }
                    ".yaml" {
                        $mimeType = "application/x-yaml"
                        $isText = $true
                    }
                    ".md" {
                        $mimeType = "text/markdown"
                        $isText = $true
                    }
                    default {
                        $mimeType = "image/jpeg"
                        $isText = $false
                    }
                }
                function getImageBase64Data($filePath, $ImageDetail) {
                    $image = $null
                    try {
                        $image = [System.Drawing.Image]::FromFile($filePath)
                    }
                    catch {
                        $image = $null
                    }
                    if ($null -eq $image) {
                        return [System.Convert]::ToBase64String([IO.File]::ReadAllBytes($filePath));
                    }
                    $maxImageDimension = [Math]::Max($image.Width, $image.Height);
                    $maxDimension = $maxImageDimension;
                    switch ($ImageDetail) {
                        "low" {
                            $maxDimension = 800;
                        }
                        "medium" {
                            $maxDimension = 1600;
                        }
                    }
                    try {
                        if ($maxDimension -lt $maxImageDimension) {
                            $newWidth = $image.Width;
                            $newHeight = $image.Height;
                            if ($image.Width -gt $image.Height) {
                                $newWidth = $maxDimension
                                $newHeight = [math]::Round($image.Height * ($maxDimension / $image.Width))
                            }
                            else {
                                $newHeight = $maxDimension
                                $newWidth = [math]::Round($image.Width * ($maxDimension / $image.Height))
                            }
                            $scaledImage = New-Object System.Drawing.Bitmap $newWidth, $newHeight
                            $graphics = [System.Drawing.Graphics]::FromImage($scaledImage)
                            $graphics.DrawImage($image, 0, 0, $newWidth, $newHeight)
                            $graphics.Dispose();
                        }
                    }
                    catch {
                    }
                    $memoryStream = New-Object System.IO.MemoryStream
                    $image.Save($memoryStream, $image.RawFormat)
                    $imageData = $memoryStream.ToArray()
                    $memoryStream.Close()
                    $image.Dispose()
                    $base64Image = [System.Convert]::ToBase64String($imageData);
                    return $base64Image;
                }
                if ($isText) {
                    $base64Image = [System.Convert]::ToBase64String([IO.File]::ReadAllBytes($filePath));
                    $messages.Add(
                        @{
                            role    = "user"
                            content = $Query
                            file    = @{
                                name         = [IO.Path]::GetFileName($filePath)
                                content_type = $mimeType
                                bytes        = "data:$mimeType;base64,$base64Image"
                            }
                        }
                    ) | Out-Null;
                }
                else {
                    $base64Image = getImageBase64Data $filePath $ImageDetail
                    $messages.Add(
                        @{
                            role    = "user"
                            content = @(
                                @{
                                    type      = "image_url"
                                    image_url = @{
                                        url    = "data:$mimeType;base64,$base64Image"
                                        detail = "$ImageDetail"
                                    }
                                }
                            )
                        }
                    ) | Out-Null;
                }
            }
            $messages.Add(
                @{
                    role    = "user"
                    content = @(
                        @{
                            type = "text"
                            text = $Query
                        }
                    )
                }
            ) | Out-Null;
            $json = @{
                "stream"      = $false
                "model"       = "$($foundModelLoaded.identifier)".trim()
                "messages"    = $messages
                "temperature" = $Temperature
                "max_tokens"  = $Max_token
            } | ConvertTo-Json -Depth 60 -Compress;
            $apiUrl = "http://localhost:1234/v1/chat/completions"
            $bytes = [System.Text.Encoding]::UTF8.GetBytes($json);
            $headers = @{
                "Content-Type" = "application/json"
            }
            Write-Verbose "Quering LM-Studio model '$Model'.."
            # ansi for cursor up and clear line
            [System.Console]::WriteLine("Quering LM-Studio model '$Model'..");
            $response = Invoke-RestMethod -Uri $apiUrl -Method Post -Body $bytes -Headers $headers -OperationTimeoutSeconds (3600 * 24) -ConnectionTimeoutSeconds (3600 * 24)
            [System.Console]::Write("`e[1A`e[2K");
            $response.choices.message | ForEach-Object content | ForEach-Object {

                if (-not $IncludeThoughts) {

                    $i = $PSItem.IndexOf("<think>")
                    if ($i -ge 0) {

                        $i2 = $PSItem.IndexOf("</think>")
                        if ($i2 -ge 0) {

                            $thoughts = $PSItem.Substring($i + 7, $i2 - $i - 7)
                            $message = $PSItem.Substring(0, $i) + $PSItem.Substring($i2 + 8)

                            Write-Information $thoughts
                            $message
                            return;
                        }
                    }
                }

                $PSItem
            }
        }

        # Main script execution
        Start-LMStudio
        $foundModelLoaded = LoadLMStudioModel
        if ($null -eq $foundModelLoaded) { return }
        QueryLMStudio -foundModelLoaded $foundModelLoaded -instructions $Instructions -query $Query -attachments $Attachments -temperature $Temperature -max_token $Max_token
    }

    end {
        # clean up resources or finalize logic if needed
    }
}