Functions/GenXdev.AI/Invoke-LLMQuery.ps1

###############################################################################
<#
.SYNOPSIS
Sends queries to an OpenAI compatible Large Language Chat completion API and
processes responses.
 
.DESCRIPTION
This function sends queries to an OpenAI compatible Large Language Chat
completion API and processes responses. It supports text and image inputs,
handles tool function calls, and can operate in various chat modes including
text and audio.
 
The function provides comprehensive support for LLM interaction including:
- Text and image input processing
- Tool function calling and command execution
- Interactive chat modes (text and audio)
- Model initialization and configuration
- Response formatting and processing
- Session management and caching
- Window positioning and display control
 
.PARAMETER Query
The text query to send to the model. Can be empty for chat modes.
 
.PARAMETER Instructions
System instructions to provide context to the model.
 
.PARAMETER Attachments
Array of file paths to attach to the query. Supports images and text files.
 
.PARAMETER ResponseFormat
A JSON schema for the requested output format.
 
.PARAMETER Temperature
Controls response randomness (0.0-1.0). Lower values are more deterministic.
 
.PARAMETER Functions
Array of function definitions that the model can call.
 
.PARAMETER ExposedCmdLets
PowerShell commands to expose as tools to the model.
 
.PARAMETER NoConfirmationToolFunctionNames
Tool functions that don't require user confirmation.
 
.PARAMETER ImageDetail
Detail level for image processing (low/medium/high).
 
.PARAMETER IncludeThoughts
Include model's thought process in output.
 
.PARAMETER DontAddThoughtsToHistory
Exclude thought processes from conversation history.
 
.PARAMETER ContinueLast
Continue from the last conversation context.
 
.PARAMETER Speak
Enable text-to-speech for AI responses.
 
.PARAMETER SpeakThoughts
Enable text-to-speech for AI thought process.
 
.PARAMETER OutputMarkdownBlocksOnly
Only output markup block responses.
 
.PARAMETER MarkupBlocksTypeFilter
Only output markup blocks of the specified types.
 
.PARAMETER ChatMode
Enable interactive chat mode with specified input method.
 
.PARAMETER ChatOnce
Internal parameter to control chat mode invocation.
 
.PARAMETER NoSessionCaching
Don't store session in session cache.
 
.PARAMETER NoLMStudioInitialize
Skip LM-Studio initialization (used when already called by parent function).
 
.PARAMETER LLMQueryType
The type of LLM query to use for AI operations.
 
.PARAMETER Model
The model identifier or pattern to use for AI operations.
 
.PARAMETER HuggingFaceIdentifier
The LM Studio specific model identifier.
 
.PARAMETER MaxToken
The maximum number of tokens to use in AI operations.
 
.PARAMETER Cpu
The number of CPU cores to dedicate to AI operations.
 
.PARAMETER Gpu
How much to offload to the GPU. Values range from -2 (Auto) to 1 (max).
 
.PARAMETER ApiEndpoint
The API endpoint URL for AI operations.
 
.PARAMETER ApiKey
The API key for authenticated AI operations.
 
.PARAMETER TimeoutSeconds
The timeout in seconds for AI operations.
 
.PARAMETER PreferencesDatabasePath
Database path for preference data files.
 
.PARAMETER ShowWindow
Show the LM Studio window during processing.
 
.PARAMETER Force
Force stop LM Studio before initialization.
 
.PARAMETER Unload
Unloads the specified model instead of loading it.
 
.PARAMETER TTLSeconds
Time-to-live in seconds for loaded models.
 
.PARAMETER Monitor
The monitor to use, 0 = default, -1 is discard.
 
.PARAMETER NoBorders
Removes the borders of the window.
 
.PARAMETER Width
The initial width of the window.
 
.PARAMETER Height
The initial height of the window.
 
.PARAMETER X
The initial X position of the window.
 
.PARAMETER Y
The initial Y position of the window.
 
.PARAMETER Left
Place window on the left side of the screen.
 
.PARAMETER Right
Place window on the right side of the screen.
 
.PARAMETER Top
Place window on the top side of the screen.
 
.PARAMETER Bottom
Place window on the bottom side of the screen.
 
.PARAMETER Centered
Place window in the center of the screen.
 
.PARAMETER Fullscreen
Maximize the window.
 
.PARAMETER RestoreFocus
Restore PowerShell window focus.
 
.PARAMETER SideBySide
Will either set the window fullscreen on a different monitor than PowerShell,
or side by side with PowerShell on the same monitor.
 
.PARAMETER FocusWindow
Focus the window after opening.
 
.PARAMETER SetForeground
Set the window to foreground after opening.
 
.PARAMETER Maximize
Maximize the window after positioning.
 
.PARAMETER KeysToSend
Keystrokes to send to the Window, see documentation for cmdlet
GenXdev.Windows\Send-Key.
 
.PARAMETER SendKeyEscape
Escape control characters and modifiers when sending keys.
 
.PARAMETER SendKeyHoldKeyboardFocus
Hold keyboard focus on target window when sending keys.
 
.PARAMETER SendKeyUseShiftEnter
Use Shift+Enter instead of Enter when sending keys.
 
.PARAMETER SendKeyDelayMilliSeconds
Delay between different input strings in milliseconds when sending keys.
 
.PARAMETER SessionOnly
Use alternative settings stored in session for AI preferences.
 
.PARAMETER ClearSession
Clear alternative settings stored in session for AI preferences.
 
.PARAMETER SkipSession
Store settings only in persistent preferences without affecting session.
 
.EXAMPLE
Invoke-LLMQuery -Query "What is 2+2?" -Model "qwen" -Temperature 0.7
 
Sends a simple mathematical query to the qwen model with specified temperature.
 
.EXAMPLE
qllm "What is 2+2?" -Model "qwen"
 
Uses the alias to send a query with default parameters.
 
.EXAMPLE
Invoke-LLMQuery -Query "Analyze this image" -Attachments @("image.jpg") -Model "qwen"
 
Sends a query with an image attachment for analysis.
 
.EXAMPLE
llm "Start a conversation" -ChatMode "textprompt" -Model "qwen"
 
Starts an interactive text chat session with the specified model.
#>

###############################################################################
function Invoke-LLMQuery {

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
    [Alias('qllm', 'llm', 'Invoke-LMStudioQuery', 'qlms')]

    param(
        ###################################################################
        [Parameter(
            Position = 0,
            Mandatory = $false,
            HelpMessage = 'Query text to send to the model'
        )]
        [AllowEmptyString()]
        [string] $Query = '',
        ###################################################################
        [Parameter(
            Position = 1,
            Mandatory = $false,
            HelpMessage = 'System instructions for the model'
        )]
        [string] $Instructions,
        ###################################################################
        [Parameter(
            Position = 2,
            Mandatory = $false,
            HelpMessage = 'Array of file paths to attach'
        )]
        [string[]] $Attachments = @(),
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'A JSON schema for the requested output format'
        )]
        [string] $ResponseFormat,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Temperature for response randomness (0.0-1.0)'
        )]
        [ValidateRange(0.0, 1.0)]
        [double] $Temperature = 0.2,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Array of function definitions'
        )]
        [hashtable[]] $Functions = @(),
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Array of PowerShell command definitions to use ' +
                'as tools')
        )]
        [GenXdev.Helpers.ExposedCmdletDefinition[]] $ExposedCmdLets,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("Tool functions that don't require user " +
                'confirmation')
        )]
        [Alias('NoConfirmationFor')]
        [string[]] $NoConfirmationToolFunctionNames = @(),
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Image detail level'
        )]
        [ValidateSet('low', 'medium', 'high')]
        [string] $ImageDetail = 'low',
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The type of LLM query'
        )]
        [ValidateSet(
            'SimpleIntelligence',
            'Knowledge',
            'Pictures',
            'TextTranslation',
            'Coding',
            'ToolUse'
        )]
        [string] $LLMQueryType = 'SimpleIntelligence',
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('The model identifier or pattern to use for AI ' +
                'operations')
        )]
        [string] $Model,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The LM Studio specific model identifier'
        )]
        [Alias('ModelLMSGetIdentifier')]
        [string] $HuggingFaceIdentifier,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('The maximum number of tokens to use in AI ' +
                'operations')
        )]
        [int] $MaxToken,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('The number of CPU cores to dedicate to AI ' +
                'operations')
        )]
        [int] $Cpu,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ("How much to offload to the GPU. If 'off', GPU " +
                "offloading is disabled. If 'max', all layers are " +
                'offloaded to GPU. If a number between 0 and 1, ' +
                'that fraction of layers will be offloaded to the ' +
                'GPU. -1 = LM Studio will decide how much to ' +
                'offload to the GPU. -2 = Auto')
        )]
        [ValidateRange(-2, 1)]
        [int] $Gpu,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The API endpoint URL for AI operations'
        )]
        [string] $ApiEndpoint,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The API key for authenticated AI operations'
        )]
        [string] $ApiKey,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The timeout in seconds for AI operations'
        )]
        [int] $TimeoutSeconds,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Database path for preference data files'
        )]
        [Alias('DatabasePath')]
        [string] $PreferencesDatabasePath,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The time-to-live in seconds for cached AI responses'
        )]
        [int] $TTLSeconds,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The monitor to use, 0 = default, -1 is discard'
        )]
        [Alias('m', 'mon')]
        [int] $Monitor,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The initial width of the window'
        )]
        [int] $Width,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The initial height of the window'
        )]
        [int] $Height,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The initial X position of the window'
        )]
        [int] $X,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The initial Y position of the window'
        )]
        [int] $Y,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Temperature for audio generation randomness'
        )]
        [double] $AudioTemperature,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Temperature for response randomness (audio chat)'
        )]
        [double] $TemperatureResponse,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Language code or name for audio chat'
        )]
        [string] $Language,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Number of CPU threads to use for audio chat'
        )]
        [int] $CpuThreads,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Regular expression to suppress certain outputs in audio chat'
        )]
        [string] $SuppressRegex,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Audio context size for audio chat'
        )]
        [int] $AudioContextSize,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Silence threshold for audio chat'
        )]
        [double] $SilenceThreshold,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Length penalty for audio chat responses'
        )]
        [double] $LengthPenalty,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Entropy threshold for audio chat'
        )]
        [double] $EntropyThreshold,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Log probability threshold for audio chat'
        )]
        [double] $LogProbThreshold,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'No speech threshold for audio chat'
        )]
        [double] $NoSpeechThreshold,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Do not speak audio responses'
        )]
        [switch] $DontSpeak,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Do not speak audio thoughts'
        )]
        [switch] $DontSpeakThoughts,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Disable VOX (voice activation) for audio chat'
        )]
        [switch] $NoVOX,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use desktop audio capture for audio chat'
        )]
        [switch] $UseDesktopAudioCapture,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Disable context for audio chat'
        )]
        [switch] $NoContext,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use beam search sampling strategy for audio chat'
        )]
        [switch] $WithBeamSearchSamplingStrategy,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Return only responses (no intermediate output)'
        )]
        [switch] $OnlyResponses,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Keystrokes to send to the Window, see ' +
                'documentation for cmdlet GenXdev.Windows\Send-Key')
        )]
        [string[]] $KeysToSend,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Delay between different input strings in ' +
                'milliseconds when sending keys')
        )]
        [Alias('DelayMilliSeconds')]
        [int] $SendKeyDelayMilliSeconds,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Include model's thoughts in output"
        )]
        [switch] $IncludeThoughts,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Exclude thought processes from conversation ' +
                'history')
        )]
        [switch] $DontAddThoughtsToHistory,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Continue from last conversation'
        )]
        [switch] $ContinueLast,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Enable text-to-speech for AI responses'
        )]
        [switch] $Speak,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Enable text-to-speech for AI thought responses'
        )]
        [switch] $SpeakThoughts,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Will only output markup block responses'
        )]
        [switch] $OutputMarkdownBlocksOnly,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Will only output markup blocks of the ' +
                'specified types')
        )]
        [ValidateNotNull()]
        [string[]] $MarkupBlocksTypeFilter = @('json', 'powershell', 'C#',
            'python', 'javascript', 'typescript', 'html', 'css', 'yaml',
            'xml', 'bash'),
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Enable chat mode'
        )]
        [Alias('chat')]
        [ValidateSet('none', 'textprompt', 'default audioinput device',
            'desktop audio')]
        [string] $ChatMode,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Used internally, to only invoke chat mode once ' +
                'after the llm invocation')
        )]
        [switch] $ChatOnce,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Don't store session in session cache"
        )]
        [switch] $NoSessionCaching,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Skip LM-Studio initialization (used when ' +
                'already called by parent function)')
        )]
        [switch] $NoLMStudioInitialize,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Show the LM Studio window'
        )]
        [switch] $ShowWindow,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Force stop LM Studio before initialization'
        )]
        [switch] $Force,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Unloads the specified model instead of loading it'
        )]
        [switch] $Unload,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Removes the borders of the window'
        )]
        [Alias('nb')]
        [switch] $NoBorders,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place window on the left side of the screen'
        )]
        [switch] $Left,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place window on the right side of the screen'
        )]
        [switch] $Right,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place window on the top side of the screen'
        )]
        [switch] $Top,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place window on the bottom side of the screen'
        )]
        [switch] $Bottom,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Place window in the center of the screen'
        )]
        [switch] $Centered,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Sends F11 to the window'
        )]
        [Alias('fs')]
        [switch]$FullScreen,

        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Restore PowerShell window focus'
        )]
        [Alias('rf', 'bg')]
        [switch] $RestoreFocus,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Will either set the window fullscreen on a ' +
                'different monitor than PowerShell, or side by ' +
                'side with PowerShell on the same monitor')
        )]
        [Alias('sbs')]
        [switch]$SideBySide,

        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Focus the window after opening'
        )]
        [Alias('fw','focus')]
        [switch] $FocusWindow,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Set the window to foreground after opening'
        )]
        [Alias('fg')]
        [switch] $SetForeground,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Maximize the window after positioning'
        )]
        [switch] $Maximize,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Escape control characters and modifiers when ' +
                'sending keys')
        )]
        [Alias('Escape')]
        [switch] $SendKeyEscape,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Hold keyboard focus on target window when ' +
                'sending keys')
        )]
        [Alias('HoldKeyboardFocus')]
        [switch] $SendKeyHoldKeyboardFocus,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use Shift+Enter instead of Enter when sending keys'
        )]
        [Alias('UseShiftEnter')]
        [switch] $SendKeyUseShiftEnter,

        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Use alternative settings stored in session for ' +
                'AI preferences')
        )]
        [switch] $SessionOnly,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Clear alternative settings stored in session ' +
                'for AI preferences')
        )]
        [switch] $ClearSession,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Store settings only in persistent preferences ' +
                'without affecting session')
        )]
        [Alias('FromPreferences')]
        [switch] $SkipSession,
        ###################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Maximum length of tool callback output in characters. Output exceeding this length will be trimmed with a warning message. Default is 100000 characters.'
        )]
        [int] $MaxToolcallBackLength = 100000
        ###################################################################
    )

    begin {

        # store PSBoundParameters to avoid nested function issues
        $myPSBoundParameters = $PSBoundParameters

        # copy identical parameter values for llm configuration
        $llmConfigParams = GenXdev.Helpers\Copy-IdenticalParamValues `
            -BoundParameters $myPSBoundParameters `
            -FunctionName 'GenXdev.AI\Get-AILLMSettings' `
            -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                -Scope Local -ErrorAction SilentlyContinue)

        # get the llm settings configuration
        $llmConfig = GenXdev.AI\Get-AILLMSettings @llmConfigParams

        # apply configuration settings to local variables
        foreach ($param in $llmConfig.Keys) {

            # check if variable exists in local scope
            if (($null -ne $llmConfig[$param]) -and (
                    Microsoft.PowerShell.Utility\Get-Variable -Name $param `
                        -Scope Local -ErrorAction SilentlyContinue)) {

                # set the variable value from configuration
                Microsoft.PowerShell.Utility\Set-Variable -Name $param `
                    -Value $llmConfig[$param] -Scope Local -Force
            }
        }

        # output verbose information about starting llm interaction
        Microsoft.PowerShell.Utility\Write-Verbose 'Starting LLM interaction...'

        # convert markup block types to lowercase for case-insensitive comparison
        $markupBlocksTypeFilter = $MarkupBlocksTypeFilter |
            Microsoft.PowerShell.Core\ForEach-Object { $_.ToLowerInvariant() }

        # initialize lm studio if using localhost
        if ((-not $NoLMStudioInitialize) -and `
            ([string]::IsNullOrWhiteSpace($ApiEndpoint) -or `
                    -not $ApiEndpoint.Contains('localhost'))) {

            # copy identical parameter values to initialize the model
            $initParams = GenXdev.Helpers\Copy-IdenticalParamValues `
                -BoundParameters $myPSBoundParameters `
                -FunctionName 'GenXdev.AI\Initialize-LMStudioModel' `
                -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                    -Scope Local -Name * -ErrorAction SilentlyContinue)

            # set flag to prevent re-initialization
            $noLMStudioInitialize = $true

            # handle force parameter separately to avoid conflicts
            if ($myPSBoundParameters.ContainsKey('Force')) {

                # remove force parameter from bound parameters
                $null = $myPSBoundParameters.Remove('Force')

                # reset force flag
                $force = $false
            }

            # initialize the model and get model information
            $modelInfo = Initialize-LMStudioModel @initParams

            # set the model identifier from initialization result
            $Model = $modelInfo.modelKey
            $FaceHuggingIdentifier = $modelInfo.path
        }
        else {

            $Model = $llmConfig.Model
            $HuggingFaceIdentifier = $llmConfig.HuggingFaceIdentifier
        }

        # remove show window parameter after initialization
        if ($myPSBoundParameters.ContainsKey('ShowWindow')) {

            # remove show window parameter from bound parameters
            $null = $myPSBoundParameters.Remove('ShowWindow')

            # reset show window flag
            $showWindow = $false
        }

        # handle chat mode parameter
        if ($myPSBoundParameters.ContainsKey('ChatMode')) {

            # remove chat mode parameter from bound parameters
            $null = $myPSBoundParameters.Remove('ChatMode')

            # return early if chat mode is not none or chat once is not set
            if (($ChatMode -ne 'none' -or $ChatOnce)) {
                return;
            }
        }

        # convert tool functions if needed or use cached ones for continue last conversation
        if ($ContinueLast -and (-not ($ExposedCmdLets -and `
                        $ExposedCmdLets.Count -gt 0)) -and `
                $Global:LMStudioGlobalExposedCmdlets -and `
            ($Global:LMStudioGlobalExposedCmdlets.Count -gt 0)) {

            # take exposed cmdlets from global cache
            $ExposedCmdLets = $Global:LMStudioGlobalExposedCmdlets
        }

        # check if user has provided exposed cmdlet definitions
        if ($ExposedCmdLets -and $ExposedCmdLets.Count -gt 0) {

            # set global cache if session caching is enabled
            if (-not $NoSessionCaching) {

                # store exposed cmdlets in global cache
                $Global:LMStudioGlobalExposedCmdlets = $ExposedCmdLets
            }

            # output verbose information about converting tool functions
            Microsoft.PowerShell.Utility\Write-Verbose `
                'Converting tool functions to LM Studio format'

            # convert exposed cmdlets to function definitions
            $functions = GenXdev.AI\ConvertTo-LMStudioFunctionDefinition `
                -ExposedCmdLets $ExposedCmdLets
        }

        # create messages list for conversation context
        $messages = [System.Collections.Generic.List[PSCustomObject]] (

            # check if global chat history exists and user wants to continue last conversation
            (($null -ne $Global:LMStudioChatHistory) -and ($ContinueLast)) ?

            # take messages from global cache
            $Global:LMStudioChatHistory :

            # otherwise create new empty list
            @()
        )

        # update global chat history if session caching is enabled
        if (-not $NoSessionCaching) {

            # store messages in global chat history
            $Global:LMStudioChatHistory = $messages
        }

        # create system instruction message
        $newMessage = @{
            role    = 'system'
            content = $Instructions
        }

        # add system message if not already present to avoid duplicates
        $newMessageJson = $newMessage |
            Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 -Compress `
                -WarningAction SilentlyContinue -ErrorAction SilentlyContinue

        # initialize duplicate check flag
        $isDuplicate = $false

        # check for duplicate messages in existing history
        foreach ($msg in $messages) {

            # convert message to json for comparison
            if (($msg | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 `
                        -Compress -WarningAction SilentlyContinue `
                        -ErrorAction SilentlyContinue) -eq $newMessageJson) {

                # mark as duplicate
                $isDuplicate = $true
                break
            }
        }

        # add system message if not duplicate
        if (-not $isDuplicate) {

            # output verbose information about system instructions
            Microsoft.PowerShell.Utility\Write-Verbose `
                "System Instructions: $Instructions"

            # add system message to messages list
            $null = $messages.Add($newMessage)
        }

        # prepare api endpoint and headers
        $apiUrl = 'http://localhost:1234/v1/chat/completions'

        # use custom api endpoint if provided
        if (-not [string]::IsNullOrWhiteSpace($ApiEndpoint)) {

            # set api url to custom endpoint
            $apiUrl = $ApiEndpoint
        }

        # set up http headers including authorization if api key provided
        $headers = @{ 'Content-Type' = 'application/json' }

        # add authorization header if api key is provided
        if (-not [string]::IsNullOrWhiteSpace($ApiKey)) {

            # set bearer token authorization header
            $headers.'Authorization' = "Bearer $ApiKey"
        }

        # output verbose information about conversation initialization
        Microsoft.PowerShell.Utility\Write-Verbose `
            'Initialized conversation with system instructions'
    }

    process {

        # handle chat once mode for internal parameter control
        if ($ChatOnce) {

            # copy identical parameter values for text chat invocation
            $invocationArgs = GenXdev.Helpers\Copy-IdenticalParamValues `
                -BoundParameters $myPSBoundParameters `
                -FunctionName 'GenXdev.AI\New-LLMTextChat' `
                -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                    -Scope Local -ErrorAction SilentlyContinue)

            # invoke text chat and return result
            return (GenXdev.AI\New-LLMTextChat @invocationArgs)
        }

        # output verbose information about request parameters
        Microsoft.PowerShell.Utility\Write-Verbose 'Sending request to LLM with:'
        Microsoft.PowerShell.Utility\Write-Verbose "Model: $Model"
        Microsoft.PowerShell.Utility\Write-Verbose "Query: $Query"
        Microsoft.PowerShell.Utility\Write-Verbose "Temperature: $Temperature"

        # handle different chat modes
        switch ($ChatMode) {

            'textprompt' {

                # copy identical parameter values for text chat invocation
                $invocationArgs = GenXdev.Helpers\Copy-IdenticalParamValues `
                    -BoundParameters $myPSBoundParameters `
                    -FunctionName 'GenXdev.AI\New-LLMTextChat' `
                    -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                        -Scope Local -ErrorAction SilentlyContinue)

                # invoke text chat and return result
                return (GenXdev.AI\New-LLMTextChat @invocationArgs)
            }
            'default audioinput device' {

                # copy identical parameter values for audio chat invocation
                $invocationArgs = GenXdev.Helpers\Copy-IdenticalParamValues `
                    -BoundParameters $myPSBoundParameters `
                    -FunctionName 'GenXdev.AI\New-LLMAudioChat' `
                    -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                        -Scope Local -ErrorAction SilentlyContinue)

                # invoke audio chat and return result
                return (GenXdev.AI\New-LLMAudioChat @invocationArgs)
            }
            'desktop audio' {

                # enable desktop audio for audio chat
                $desktopAudio = $true

                # copy identical parameter values for audio chat invocation
                $invocationArgs = GenXdev.Helpers\Copy-IdenticalParamValues `
                    -BoundParameters $myPSBoundParameters `
                    -FunctionName 'GenXdev.AI\New-LLMAudioChat' `
                    -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                        -Scope Local -ErrorAction SilentlyContinue)

                # invoke audio chat and return result
                return (GenXdev.AI\New-LLMAudioChat @invocationArgs)
            }
        }

        # process attachments if provided
        foreach ($attachment in $Attachments) {

            # expand the file path to handle relative paths
            $filePath = GenXdev.FileSystem\Expand-Path $attachment

            # get file extension for mime type determination
            $fileExtension = [System.IO.Path]::GetExtension($filePath).ToLowerInvariant()

            # initialize mime type and text flag
            $mimeType = 'application/octet-stream'
            $isText = $false

            # determine mime type and text flag based on file extension
            switch ($fileExtension) {
                '.jpg' {
                    $mimeType = 'image/jpeg'
                    $isText = $false
                    break
                }
                '.jpeg' {
                    $mimeType = 'image/jpeg'
                    $isText = $false
                    break
                }
                '.png' {
                    $mimeType = 'image/png'
                    $isText = $false
                    break
                }
                '.gif' {
                    $mimeType = 'image/gif'
                    $isText = $false
                    break
                }
                '.bmp' {
                    $mimeType = 'image/bmp'
                    $isText = $false
                    break
                }
                '.tiff' {
                    $mimeType = 'image/tiff'
                    $isText = $false
                    break
                }
                '.mp4' {
                    $mimeType = 'video/mp4'
                    $isText = $false
                    break
                }
                '.avi' {
                    $mimeType = 'video/avi'
                    $isText = $false
                    break;
                }
                '.mov' {
                    $mimeType = 'video/quicktime'
                    $isText = $false
                    break;
                }
                '.webm' {
                    $mimeType = 'video/webm'
                    $isText = $false
                    break;
                }
                '.mkv' {
                    $mimeType = 'video/x-matroska'
                    $isText = $false
                    break;
                }
                '.flv' {
                    $mimeType = 'video/x-flv'
                    $isText = $false
                    break;
                }
                '.wmv' {
                    $mimeType = 'video/x-ms-wmv'
                    $isText = $false
                    break;
                }
                '.mpg' {
                    $mimeType = 'video/mpeg'
                    $isText = $false
                    break;
                }
                '.mpeg' {
                    $mimeType = 'video/mpeg'
                    $isText = $false
                    break;
                }
                '.3gp' {
                    $mimeType = 'video/3gpp'
                    $isText = $false
                    break;
                }
                '.3g2' {
                    $mimeType = 'video/3gpp2'
                    $isText = $false
                    break;
                }
                '.m4v' {
                    $mimeType = 'video/x-m4v'
                    $isText = $false
                    break;
                }
                '.webp' {
                    $mimeType = 'image/webp'
                    $isText = $false
                    break;
                }
                '.heic' {
                    $mimeType = 'image/heic'
                    $isText = $false
                    break;
                }
                '.heif' {
                    $mimeType = 'image/heif'
                    $isText = $false
                    break;
                }
                '.avif' {
                    $mimeType = 'image/avif'
                    $isText = $false
                    break;
                }
                '.jxl' {
                    $mimeType = 'image/jxl'
                    $isText = $false
                    break;
                }
                '.ps1' {
                    $mimeType = 'text/x-powershell'
                    $isText = $true
                    break;
                }
                '.psm1' {
                    $mimeType = 'text/x-powershell'
                    $isText = $true
                    break;
                }
                '.psd1' {
                    $mimeType = 'text/x-powershell'
                    $isText = $true
                    break;
                }
                '.sh' {
                    $mimeType = 'application/x-sh'
                    $isText = $true
                    break;
                }
                '.bat' {
                    $mimeType = 'application/x-msdos-program'
                    $isText = $true
                    break;
                }
                '.cmd' {
                    $mimeType = 'application/x-msdos-program'
                    $isText = $true
                    break;
                }
                '.py' {
                    $mimeType = 'text/x-python'
                    $isText = $true
                    break;
                }
                '.rb' {
                    $mimeType = 'application/x-ruby'
                    $isText = $true
                    break;
                }
                '.txt' {
                    $mimeType = 'text/plain'
                    $isText = $true
                    break;
                }
                '.pl' {
                    $mimeType = 'text/x-perl'
                    $isText = $true
                    break;
                }
                '.php' {
                    $mimeType = 'application/x-httpd-php'
                    $isText = $true
                    break;
                }
                '.pdf' {
                    $mimeType = 'application/pdf'
                    $isText = $false
                    break;
                }
                '.js' {
                    $mimeType = 'application/javascript'
                    $isText = $true
                    break;
                }
                '.ts' {
                    $mimeType = 'application/typescript'
                    $isText = $true
                    break;
                }
                '.java' {
                    $mimeType = 'text/x-java-source'
                    $isText = $true
                    break;
                }
                '.c' {
                    $mimeType = 'text/x-c'
                    $isText = $true
                    break;
                }
                '.cpp' {
                    $mimeType = 'text/x-c++src'
                    $isText = $true
                    break;
                }
                '.cs' {
                    $mimeType = 'text/x-csharp'
                    $isText = $true
                    break;
                }
                '.go' {
                    $mimeType = 'text/x-go'
                    $isText = $true
                    break;
                }
                '.rs' {
                    $mimeType = 'text/x-rustsrc'
                    $isText = $true
                    break;
                }
                '.swift' {
                    $mimeType = 'text/x-swift'
                    $isText = $true
                    break;
                }
                '.kt' {
                    $mimeType = 'text/x-kotlin'
                    $isText = $true
                    break;
                }
                '.scala' {
                    $mimeType = 'text/x-scala'
                    $isText = $true
                    break;
                }
                '.r' {
                    $mimeType = 'text/x-r'
                    $isText = $true
                    break;
                }
                '.sql' {
                    $mimeType = 'application/sql'
                    $isText = $true
                    break;
                }
                '.html' {
                    $mimeType = 'text/html'
                    $isText = $true
                    break;
                }
                '.css' {
                    $mimeType = 'text/css'
                    $isText = $true
                    break;
                }
                '.xml' {
                    $mimeType = 'application/xml'
                    $isText = $true
                    break;
                }
                '.json' {
                    $mimeType = 'application/json'
                    $isText = $true
                    break;
                }
                '.yaml' {
                    $mimeType = 'application/x-yaml'
                    $isText = $true
                    break;
                }
                '.md' {
                    $mimeType = 'text/markdown'
                    $isText = $true
                    break;
                }
                default {
                    $mimeType = 'image/jpeg'
                    $isText = $false
                    break;
                }
            }

            # internal function to get base64 encoded image data with optional scaling
            function getImageBase64Data($filePath, $ImageDetail) {

                # try to load image using system drawing
                $image = $null
                try {
                    $image = [System.Drawing.Image]::FromFile($filePath)
                }
                catch {
                    $image = $null
                }

                # if image loading failed, return raw file bytes as base64
                if ($null -eq $image) {
                    return [System.Convert]::ToBase64String([IO.File]::ReadAllBytes($filePath))
                }

                # get maximum dimension of the image
                $maxImageDimension = [Math]::Max($image.Width, $image.Height);
                $maxDimension = $maxImageDimension;

                # determine target dimension based on image detail level
                switch ($ImageDetail) {
                    'low' {
                        $maxDimension = 800;
                        break;
                    }
                    'medium' {
                        $maxDimension = 1600;
                        break;
                    }
                }

                # scale image if it exceeds the maximum dimension
                try {
                    if ($maxDimension -lt $maxImageDimension) {

                        # calculate new dimensions maintaining aspect ratio
                        $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))
                        }

                        # create scaled bitmap and draw resized image
                        $scaledImage = Microsoft.PowerShell.Utility\New-Object System.Drawing.Bitmap $newWidth, $newHeight
                        $graphics = [System.Drawing.Graphics]::FromImage($scaledImage)
                        $graphics.DrawImage($image, 0, 0, $newWidth, $newHeight)
                        $graphics.Dispose();
                    }
                }
                catch {
                    # ignore scaling errors and use original image
                }

                # save image to memory stream and convert to base64
                $memoryStream = Microsoft.PowerShell.Utility\New-Object System.IO.MemoryStream
                $image.Save($memoryStream, $image.RawFormat)
                $imageData = $memoryStream.ToArray()
                $memoryStream.Close()
                $image.Dispose()
                return [System.Convert]::ToBase64String($imageData)
            }

            # get base64 encoded data for the attachment
            [string] $base64Data = getImageBase64Data $filePath $ImageDetail

            # handle text files differently than binary files
            if ($isText) {

                $newMessage = @{
                    role    = 'user'
                    content = $Query
                    file    = @{
                        name         = [IO.Path]::GetFileName($filePath)
                        content_type = $mimeType
                        bytes        = "data:$mimeType;base64,$base64Data"
                    }
                }
                $newMessageJson = $newMessage | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 -Compress -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
                $isDuplicate = $false
                foreach ($msg in $messages) {
                    if (($msg | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 -Compress) -eq $newMessageJson) {
                        $isDuplicate = $true
                        break
                    }
                }
                if (-not $isDuplicate) {

                    $null = $messages.Add($newMessage)
                }
            }
            else {

                $newMessage = @{
                    role    = 'user'
                    content = @(
                        @{
                            type      = 'image_url'
                            image_url = @{
                                url    = "data:$mimeType;base64,$base64Data"
                                detail = "$ImageDetail"
                            }
                        }
                    )
                }
                $newMessageJson = $newMessage | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 -Compress -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
                $isDuplicate = $false
                foreach ($msg in $messages) {
                    if (($msg | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 -Compress -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) -eq $newMessageJson) {
                        $isDuplicate = $true
                        break
                    }
                }
                if (-not $isDuplicate) {

                    $null = $messages.Add($newMessage)
                }
            }
        }

        # prepare api payload

        $payload = @{
            stream      = $false
            messages    = $messages
            temperature = $Temperature
        }

        if (-not [string]::IsNullOrWhiteSpace($ResponseFormat)) {
            try {
                $payload.response_format = $ResponseFormat | Microsoft.PowerShell.Utility\ConvertFrom-Json
            }
            catch {
                Microsoft.PowerShell.Utility\Write-Verbose 'Invalid response format schema. Ignoring.'
            }
        }

        if (-not [string]::IsNullOrWhiteSpace($Model)) {

            $payload.model = $Model
        }

        if ($MaxToken -gt 0) {

            $payload.max_tokens = $MaxToken
        }

        if ($Functions -and $Functions.Count -gt 0) {

            # maintain array structure, create new array with required properties
            $functionsWithoutCallbacks = @(
                $Functions | Microsoft.PowerShell.Core\ForEach-Object {
                    [PSCustomObject] @{
                        type     = $_.type
                        function = [PSCustomObject] @{
                            name        = $_.function.name
                            description = $_.function.description
                            parameters  = @{
                                type       = 'object'
                                properties = [PSCustomObject] $_.function.parameters.properties
                                required   = $_.function.parameters.required
                            }
                        }
                    }
                }
            )

            $payload.tools = $functionsWithoutCallbacks
            $payload.function_call = 'auto'
        }

        if (-not [string]::IsNullOrWhiteSpace($Query)) {

            # add main query message
            $newMsg = @{
                role    = 'user'
                content = $Query;
            }

            $null = $messages.Add($newMsg)
        }

        # convert payload to json
        $json = $payload | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 60 -Compress -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
        $bytes = [System.Text.Encoding]::UTF8.GetBytes($json)

        Microsoft.PowerShell.Utility\Write-Verbose "Querying LM-Studio model '$Model' with parameters:"
        Microsoft.PowerShell.Utility\Write-Verbose $($payload | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 7 -WarningAction SilentlyContinue -ErrorAction SilentlyContinue)

        # send request with long timeouts
        $response = Microsoft.PowerShell.Utility\Invoke-RestMethod -Uri $apiUrl `
            -Method Post `
            -Body $bytes `
            -Headers $headers `
            -OperationTimeoutSeconds $TimeoutSeconds `
            -ConnectionTimeoutSeconds $TimeoutSeconds

        # First handle tool calls if present
        if ($response.choices[0].message.tool_calls) {

            # Add assistant's tool calls to history
            $newMsg = $response.choices[0].message
            $newMsgJson = $newMsg | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 -Compress -WarningAction SilentlyContinue -ErrorAction SilentlyContinue

            # Only add if it's not a duplicate of the last message
            if ($messages.Count -eq 0 -or
                ($messages[-1] | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 -Compress -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) -ne $newMsgJson) {
                $messages.Add($newMsg) | Microsoft.PowerShell.Core\Out-Null
            }

            # Process all tool calls sequentially
            foreach ($toolCallCO in $response.choices[0].message.tool_calls) {

                $toolCall = $toolCallCO | GenXdev.Helpers\ConvertTo-HashTable

                Microsoft.PowerShell.Utility\Write-Verbose "Tool call detected: $($toolCall.function.name)"

                # Format parameters as PowerShell command line style
                $foundArguments = ($toolCall.function.arguments | Microsoft.PowerShell.Utility\ConvertFrom-Json)
                $paramLine = $toolCall.function.arguments | Microsoft.PowerShell.Utility\ConvertFrom-Json |
                    Microsoft.PowerShell.Utility\Get-Member -MemberType NoteProperty |
                    Microsoft.PowerShell.Core\ForEach-Object {
                        $name = $_.Name
                        $value = $foundArguments.$name
                        "-$name $($value | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 3 -WarningAction SilentlyContinue)"
                    } | Microsoft.PowerShell.Utility\Join-String -Separator ' '

                Microsoft.PowerShell.Utility\Write-Verbose "PS> $($toolCall.function.name) $paramLine"
                if (-not ($Verbose -or $VerbosePreference -eq 'Continue')) {

                    Microsoft.PowerShell.Utility\Write-Host "PS> $($toolCall.function.name) $paramLine" -ForegroundColor Cyan
                }

                [GenXdev.Helpers.ExposedToolCallInvocationResult] $invocationResult = GenXdev.AI\Invoke-CommandFromToolCall `
                    -ToolCall:$toolCall `
                    -Functions:$Functions `
                    -ExposedCmdLets:$ExposedCmdLets `
                    -NoConfirmationToolFunctionNames:$NoConfirmationToolFunctionNames | Microsoft.PowerShell.Utility\Select-Object -First 1

                if (-not ($Verbose -or $VerbosePreference -eq 'Continue')) {

                    Microsoft.PowerShell.Utility\Write-Host "$($invocationResult.Output | Microsoft.PowerShell.Core\ForEach-Object { if ($_ -is [string]) { $_ } else { $_ | Microsoft.PowerShell.Utility\Out-String } })" -ForegroundColor Green
                }

                Microsoft.PowerShell.Utility\Write-Verbose "Tool function result: $($invocationResult | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 3 -Compress)"

                if (-not $invocationResult.CommandExposed) {

                    # Add tool response to history
                    $null = $messages.Add(@{
                            role         = 'tool'
                            name         = $toolCall.function.name
                            content      = $invocationResult.Error ? $invocationResult.Error : $invocationResult.Reason
                            tool_call_id = $toolCall.id
                            id           = $toolCall.id
                            arguments    = $toolCall.function.arguments | Microsoft.PowerShell.Utility\ConvertFrom-Json
                        })
                }
                else {
                    # Check if the cmdlet is configured to return text only
                    $isTextOnlyOutput = $invocationResult.ExposedCmdLet -and $invocationResult.ExposedCmdLet.OutputText -eq $true

                    if ($isTextOnlyOutput) {
                        # For text-only output, convert everything to string first using Out-String
                        $outputText = "$(($invocationResult.Output | Microsoft.PowerShell.Utility\Out-String))".Trim()

                        if ($outputText.Length -gt $MaxToolcallBackLength) {
                            $originalLength = $outputText.Length
                            $trimMessage = "TRIMMED OUTPUT (check parameter use!) invalid json on purpose, AI Agent: don't retry same function without check parameters! >>"
                            $maxContentLength = $MaxToolcallBackLength - $trimMessage.Length
                            $outputText = $trimMessage + $outputText.Substring(0, $maxContentLength)
                            Microsoft.PowerShell.Utility\Write-Verbose "Tool '$($toolCall.function.name)' output was trimmed from $originalLength to $MaxToolcallBackLength characters"
                        }

                        # Add tool response to history
                        $null = $messages.Add(@{
                                role         = 'tool'
                                name         = $toolCall.function.name
                                content      = $outputText
                                content_type = $invocationResult.OutputType
                                tool_call_id = $toolCall.id
                                id           = $toolCall.id
                                arguments    = $toolCall.function.arguments | Microsoft.PowerShell.Utility\ConvertFrom-Json
                            })
                    } else {
                        # For structured output, serialize with smart depth reduction
                        try {
                            # Start with the specified depth and progressively reduce if too long
                            $targetDepth = $invocationResult.ExposedCmdLet.JsonDepth ?? 10
                            $parsedOutput = $null
                            $finalDepth = $targetDepth

                            # Try progressively smaller depths until it fits or we reach minimum depth of 2
                            $foundValidDepth = $false
                            while ($finalDepth -ge 2) {
                                $parsedOutput = $invocationResult.Output | Microsoft.PowerShell.Utility\ConvertTo-Json -Depth $finalDepth -Compress -ErrorAction SilentlyContinue

                                if ($parsedOutput.Length -le $MaxToolcallBackLength) {
                                    # Found a depth that works
                                    $foundValidDepth = $true
                                    if ($finalDepth -lt $targetDepth) {
                                        Microsoft.PowerShell.Utility\Write-Verbose "Tool '$($toolCall.function.name)' JSON output depth reduced from $targetDepth to $finalDepth to fit within $MaxToolcallBackLength characters"
                                    }
                                    break
                                }
                                $finalDepth--
                            }

                            # If we found a depth that works, use it
                            if ($foundValidDepth) {
                                $content = $parsedOutput
                            } else {
                                # If even depth 2 is too long, trim the output
                                $originalLength = $parsedOutput.Length
                                $trimMessage = "TRIMMED JSON OUTPUT (check parameter use!) incomplete json data, AI Agent: don't retry same function without checking parameters! >>"
                                $maxContentLength = $MaxToolcallBackLength - $trimMessage.Length
                                $content = $trimMessage + $parsedOutput.Substring(0, $maxContentLength)
                                Microsoft.PowerShell.Utility\Write-Verbose "Tool '$($toolCall.function.name)' JSON output was trimmed from $originalLength to $MaxToolcallBackLength characters (even at minimum depth 2)"
                            }
                        } catch {
                            # If JSON conversion fails, fall back to text with trimming
                            $outputText = "$($invocationResult.Output)".Trim()
                            if ($outputText.Length -gt $MaxToolcallBackLength) {
                                $originalLength = $outputText.Length
                                $trimMessage = "TRIMMED OUTPUT (check parameter use!) invalid json on purpose, AI Agent: don't retry same function without check parameters! >>"
                                $maxContentLength = $MaxToolcallBackLength - $trimMessage.Length
                                $outputText = $trimMessage + $outputText.Substring(0, $maxContentLength)
                                Microsoft.PowerShell.Utility\Write-Verbose "Tool '$($toolCall.function.name)' fallback output was trimmed from $originalLength to $MaxToolcallBackLength characters"
                            }
                            $content = $outputText
                        }

                        # Add tool response to history
                        $null = $messages.Add(@{
                                role         = 'tool'
                                name         = $toolCall.function.name
                                content      = $content
                                content_type = $invocationResult.OutputType
                                tool_call_id = $toolCall.id
                                id           = $toolCall.id
                                arguments    = $toolCall.function.arguments | Microsoft.PowerShell.Utility\ConvertFrom-Json
                            })
                    }
                }
            }

            Microsoft.PowerShell.Utility\Write-Verbose 'Continuing conversation after tool responses'

            if (-not $myPSBoundParameters.ContainsKey('ContinueLast')) {

                $myPSBoundParameters.Add('ContinueLast', $true)
            }
            else {

                $myPSBoundParameters['ContinueLast'] = $true
            }

            if (-not $myPSBoundParameters.ContainsKey('Query')) {

                $myPSBoundParameters.Add('Query', '')
            }
            else {

                $myPSBoundParameters['Query'] = ''
            }

            GenXdev.AI\Invoke-LLMQuery @myPSBoundParameters

            return;
        }

        # Handle regular message content if no tool calls
        [System.Collections.Generic.List[object]] $finalOutput = @()

        foreach ($msg in $response.choices.message) {

            $content = $msg.content

            # Extract and process embedded tool calls
            # Try multiple formats that LLMs might use for tool function calls

            # Format 1: <tool_call>{...}</tool_call>
            while ($content -match '<tool_call>\s*({[^}]+})\s*</tool_call>') {
                $toolCallJson = $matches[1]
                try {
                    # parse the json into a tool call object
                    $toolCall = $toolCallJson |
                        Microsoft.PowerShell.Utility\ConvertFrom-Json -ErrorAction SilentlyContinue -WarningAction SilentlyContinue |
                        GenXdev.Helpers\ConvertTo-HashTable

                    # verify this has the expected properties for a function call
                    if ($toolCall.function -and $toolCall.function.name) {
                        Microsoft.PowerShell.Utility\Write-Verbose "Tool call detected (Format 1): $($toolCall.function.name)"

                        # invoke the command from the tool call
                        [GenXdev.Helpers.ExposedToolCallInvocationResult] $invocationResult = GenXdev.AI\Invoke-CommandFromToolCall `
                            -ToolCall:$toolCall `
                            -Functions:$Functions `
                            -ExposedCmdLets:$ExposedCmdLets `
                            -NoConfirmationToolFunctionNames:$NoConfirmationToolFunctionNames |
                            Microsoft.PowerShell.Utility\Select-Object -First 1

                        # create replacement text with the function result
                        $replacement = "**Function Call Result:** $($invocationResult.Output)"

                        # replace the original tool call with the result
                        $content = $content.Replace($matches[0], $replacement)
                    }
                }
                catch {
                    # if we can't process it, replace with error message
                    $content = $content.Replace($matches[0], "Error processing tool call: $($_.Exception.Message)")
                }
            }

            # Format 2: [FUNCTION_CALL]{...}[/FUNCTION_CALL]
            while ($content -match '\[FUNCTION_CALL\]\s*({[^}]+})\s*\[/FUNCTION_CALL\]') {
                $toolCallJson = $matches[1]
                try {
                    # parse the json into a tool call object
                    $toolCall = $toolCallJson |
                        Microsoft.PowerShell.Utility\ConvertFrom-Json -ErrorAction SilentlyContinue -WarningAction SilentlyContinue |
                        GenXdev.Helpers\ConvertTo-HashTable

                    # verify this has the expected properties for a function call
                    if ($toolCall.function -and $toolCall.function.name) {
                        Microsoft.PowerShell.Utility\Write-Verbose "Tool call detected (Format 2): $($toolCall.function.name)"

                        # invoke the command from the tool call
                        [GenXdev.Helpers.ExposedToolCallInvocationResult] $invocationResult = GenXdev.AI\Invoke-CommandFromToolCall `
                            -ToolCall:$toolCall `
                            -Functions:$Functions `
                            -ExposedCmdLets:$ExposedCmdLets `
                            -NoConfirmationToolFunctionNames:$NoConfirmationToolFunctionNames |
                            Microsoft.PowerShell.Utility\Select-Object -First 1

                        # create replacement text with the function result
                        $replacement = "**Function Call Result:** $($invocationResult.Output)"

                        # replace the original tool call with the result
                        $content = $content.Replace($matches[0], $replacement)
                    }
                }
                catch {
                    # if we can't process it, replace with error message
                    $content = $content.Replace($matches[0], "Error processing tool call: $($_.Exception.Message)")
                }
            }

            # Format 3: <function>{...}</function>
            while ($content -match '<function>\s*({[^}]+})\s*</function>') {
                $toolCallJson = $matches[1]
                try {
                    # parse the json into a tool call object
                    $toolCall = $toolCallJson |
                        Microsoft.PowerShell.Utility\ConvertFrom-Json -ErrorAction SilentlyContinue -WarningAction SilentlyContinue |
                        GenXdev.Helpers\ConvertTo-HashTable

                    # verify this has the expected properties for a function call
                    if ($toolCall.function -and $toolCall.function.name) {
                        Microsoft.PowerShell.Utility\Write-Verbose "Tool call detected (Format 3): $($toolCall.function.name)"

                        # invoke the command from the tool call
                        [GenXdev.Helpers.ExposedToolCallInvocationResult] $invocationResult = GenXdev.AI\Invoke-CommandFromToolCall `
                            -ToolCall:$toolCall `
                            -Functions:$Functions `
                            -ExposedCmdLets:$ExposedCmdLets `
                            -NoConfirmationToolFunctionNames:$NoConfirmationToolFunctionNames |
                            Microsoft.PowerShell.Utility\Select-Object -First 1

                        # create replacement text with the function result
                        $replacement = "**Function Call Result:** $($invocationResult.Output)"

                        # replace the original tool call with the result
                        $content = $content.Replace($matches[0], $replacement)
                    }
                }
                catch {
                    # if we can't process it, replace with error message
                    $content = $content.Replace($matches[0], "Error processing tool call: $($_.Exception.Message)")
                }
            }

            # Format 4: Check for code blocks with function calls
            while ($content -match '```(?:json)?\s*({[\s\S]*?"function"[\s\S]*?})\s*```') {
                $potentialJson = $matches[1].Trim()
                try {
                    # Try to parse as a function call
                    $toolCall = $potentialJson |
                        Microsoft.PowerShell.Utility\ConvertFrom-Json -ErrorAction SilentlyContinue -WarningAction SilentlyContinue |
                        GenXdev.Helpers\ConvertTo-HashTable

                    # Verify this is actually a function call with the expected properties
                    if ($toolCall.function -and $toolCall.function.name) {
                        Microsoft.PowerShell.Utility\Write-Verbose (
                            "Tool call detected (Format 4): $($toolCall.function.name)")

                        # invoke the command from the tool call
                        [GenXdev.Helpers.ExposedToolCallInvocationResult] $invocationResult = GenXdev.AI\Invoke-CommandFromToolCall `
                            -ToolCall:$toolCall `
                            -Functions:$Functions `
                            -ExposedCmdLets:$ExposedCmdLets `
                            -NoConfirmationToolFunctionNames:$NoConfirmationToolFunctionNames |
                            Microsoft.PowerShell.Utility\Select-Object -First 1

                        # create replacement text with the function result
                        $replacement = "**Function Call Result:** $($invocationResult.Output)"

                        # replace the original tool call with the result
                        $content = $content.Replace($matches[0], $replacement)
                    }
                }
                catch {
                    # not a valid function call, leave it as is
                    # only replace if we're sure it's a function call
                }
            }

            # update chat history with assistant's response
            # convert message to json and back to create a copy
            $messageForHistory = $msg |
                Microsoft.PowerShell.Utility\ConvertTo-Json -Depth 10 -WarningAction SilentlyContinue |
                Microsoft.PowerShell.Utility\ConvertFrom-Json -WarningAction SilentlyContinue

            # decide whether to include thoughts in history based on parameter
            $messageForHistory.content = $DontAddThoughtsToHistory ?
            [regex]::Replace($content, '<think>.*?</think>', '') :
            $content

            # add the message to conversation history
            $null = $messages.Add($messageForHistory)

            # process content if not empty
            if (-not [string]::IsNullOrWhiteSpace($content)) {

                # if including thoughts, add raw content to output
                if ($IncludeThoughts) {
                    $null = $finalOutput.Add($content)
                }

                # extract and process thought content between <think> tags
                $i = $content.IndexOf('<think>')
                if ($i -ge 0) {
                    # skip the opening tag
                    $i += 7
                    $i2 = $content.IndexOf('</think>')
                    if ($i2 -ge 0) {
                        # extract thought content between tags
                        $thoughts = $content.Substring($i, $i2 - $i)
                        Microsoft.PowerShell.Utility\Write-Verbose "LLM Thoughts: $thoughts"

                        # display thoughts if not including them in output
                        if (-not $IncludeThoughts) {
                            Microsoft.PowerShell.Utility\Write-Host $thoughts -ForegroundColor Yellow
                        }

                        # speak thoughts if enabled
                        if ($SpeakThoughts) {
                            $null = GenXdev.Console\Start-TextToSpeech $thoughts
                        }
                    }
                }

                # Remove <think> patterns
                $cleaned = [regex]::Replace($content, '<think>.*?</think>', '')
                Microsoft.PowerShell.Utility\Write-Verbose "LLM Response: $cleaned"

                if ($OutputMarkdownBlocksOnly) {

                    $null = $finalOutput.RemoveAt($finalOutput.Count - 1);

                    $cleaned = "`n$cleaned`n"
                    $i = $cleaned.IndexOf("`n``````");
                    while ($i -ge 0) {

                        $i += 4;
                        $i2 = $cleaned.IndexOf("`n", $i);
                        $name = $cleaned.Substring($i, $i2 - $i).Trim().ToLowerInvariant();

                        $i = $i2 + 1;
                        $i2 = $cleaned.IndexOf("`n``````", $i);
                        if ($i2 -ge 0) {

                            $codeBlock = $cleaned.Substring($i, $i2 - $i);
                            $codeBlock = $json.Trim();
                            if ($name -in $MarkupBlocksTypeFilter) {

                                $null = $finalOutput.Add($codeBlock);
                            }
                        }

                        $i = $cleaned.IndexOf("`n``````", $i2 + 4);
                    }
                }
                else {

                    $null = $finalOutput.Add($cleaned)

                    if ($Speak) {

                        $null = GenXdev.Console\Start-TextToSpeech $cleaned
                    }
                }
            }
        }
        # output all collected results
        $finalOutput | Microsoft.PowerShell.Core\ForEach-Object {

            # write each output object to the pipeline
            Microsoft.PowerShell.Utility\Write-Output $_
        }

        # output verbose information about conversation history update
        Microsoft.PowerShell.Utility\Write-Verbose 'Conversation history updated'
    }

    end {
    }
}
###############################################################################