From 1223fd214974825c372d5bf388babbcfe2454048 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 20 Mar 2025 21:48:59 +0300 Subject: [PATCH 01/26] win-setup --- setup.ps1 | 782 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 782 insertions(+) create mode 100644 setup.ps1 diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 00000000..8572484f --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,782 @@ +# DocsGPT Setup PowerShell Script for Windows +# PowerShell -ExecutionPolicy Bypass -File .\setup.ps1 + +# Script execution policy - uncomment if needed +# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + +# Set error action preference +$ErrorActionPreference = "Stop" + +# Get current script directory +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Definition +$COMPOSE_FILE = Join-Path -Path $SCRIPT_DIR -ChildPath "deployment\docker-compose.yaml" +$ENV_FILE = Join-Path -Path $SCRIPT_DIR -ChildPath ".env" + +# Function to write colored text +function Write-ColorText { + param ( + [Parameter(Mandatory=$true)][string]$Text, + [Parameter()][string]$ForegroundColor = "White", + [Parameter()][switch]$Bold + ) + + $params = @{ + ForegroundColor = $ForegroundColor + NoNewline = $false + } + + if ($Bold) { + # PowerShell doesn't have bold + Write-Host $Text @params + } else { + Write-Host $Text @params + } +} + +# Animation function (Windows PowerShell version of animate_dino) +function Animate-Dino { + [Console]::CursorVisible = $false + + # Clear screen + Clear-Host + + # Static DocsGPT text + $static_text = @( + " ____ ____ ____ _____ " + " | _ \ ___ ___ ___ / ___| _ \_ _|" + " | | | |/ _ \ / __/ __| | _| |_) || | " + " | |_| | (_) | (__\__ \ |_| | __/ | | " + " |____/ \___/ \___|___/\____|_| |_| " + " " + ) + + # Print static text + foreach ($line in $static_text) { + Write-Host $line + } + + # Dino ASCII art + $dino_lines = @( + " ######### " + " ############# " + " ##################" + " ####################" + " ######################" + " ####################### ######" + " ############################### " + " ################################## " + " ################ ############ " + " ################## ########## " + " ##################### ######## " + " ###################### ###### ### " + " ############ ########## #### ## " + " ############# ######### ##### " + " ############## ######### " + " ############## ########## " + "############ ####### " + " ###### ###### #### " + " ################ " + " ################# " + ) + + # Save cursor position + $cursorPos = $Host.UI.RawUI.CursorPosition + + # Build-up animation + for ($i = 0; $i -lt $dino_lines.Count; $i++) { + # Restore cursor position + $Host.UI.RawUI.CursorPosition = $cursorPos + + # Display lines up to current index + for ($j = 0; $j -le $i; $j++) { + Write-Host $dino_lines[$j] + } + + # Slow down animation + Start-Sleep -Milliseconds 50 + } + + # Pause at end of animation + Start-Sleep -Milliseconds 500 + + # Clear the animation + $Host.UI.RawUI.CursorPosition = $cursorPos + + # Clear from cursor to end of screen + for ($i = 0; $i -lt $dino_lines.Count; $i++) { + Write-Host (" " * $dino_lines[0].Length) + } + + # Restore cursor position for next output + $Host.UI.RawUI.CursorPosition = $cursorPos + + # Show cursor again + [Console]::CursorVisible = $true +} + +# Check and start Docker function +function Check-AndStartDocker { + # Check if Docker is running + try { + $dockerRunning = $false + + # First try with 'docker info' which should work if Docker is fully operational + try { + $dockerInfo = docker info 2>&1 + # If we get here without an exception, Docker is running + Write-ColorText "Docker is already running." -ForegroundColor "Green" + return $true + } catch { + # Docker info command failed + } + + # Check if Docker process is running + $dockerProcess = Get-Process "Docker Desktop" -ErrorAction SilentlyContinue + if ($dockerProcess) { + # Docker Desktop is running, but might not be fully initialized + Write-ColorText "Docker Desktop is starting up. Waiting for it to be ready..." -ForegroundColor "Yellow" + + # Wait for Docker to become operational + $attempts = 0 + $maxAttempts = 30 + + while ($attempts -lt $maxAttempts) { + try { + $null = docker ps 2>&1 + Write-ColorText "Docker is now operational." -ForegroundColor "Green" + return $true + } catch { + Write-Host "." -NoNewline + Start-Sleep -Seconds 2 + $attempts++ + } + } + + Write-ColorText "`nDocker Desktop is running but not responding to commands. Please check Docker status." -ForegroundColor "Red" + return $false + } + + # Docker is not running, attempt to start it + Write-ColorText "Docker is not running. Attempting to start Docker Desktop..." -ForegroundColor "Yellow" + + # Docker Desktop locations to check + $dockerPaths = @( + "${env:ProgramFiles}\Docker\Docker\Docker Desktop.exe", + "${env:ProgramFiles(x86)}\Docker\Docker\Docker Desktop.exe", + "$env:LOCALAPPDATA\Docker\Docker\Docker Desktop.exe" + ) + + $dockerPath = $null + foreach ($path in $dockerPaths) { + if (Test-Path $path) { + $dockerPath = $path + break + } + } + + if ($null -eq $dockerPath) { + Write-ColorText "Docker Desktop not found. Please install Docker Desktop or start it manually." -ForegroundColor "Red" + return $false + } + + # Start Docker Desktop + try { + Start-Process $dockerPath + Write-Host -NoNewline "Waiting for Docker to start" + + # Wait for Docker to be ready + $attempts = 0 + $maxAttempts = 60 # 60 x 2 seconds = maximum 2 minutes wait + + while ($attempts -lt $maxAttempts) { + try { + $null = docker ps 2>&1 + Write-Host "`nDocker has started successfully!" + return $true + } catch { + # Show waiting animation + Write-Host -NoNewline "." + Start-Sleep -Seconds 2 + $attempts++ + + if ($attempts % 3 -eq 0) { + Write-Host "`r" -NoNewline + Write-Host "Waiting for Docker to start " -NoNewline + } + } + } + + Write-ColorText "`nDocker did not start within the expected time. Please start Docker Desktop manually." -ForegroundColor "Red" + return $false + } catch { + Write-ColorText "Failed to start Docker Desktop. Please start it manually." -ForegroundColor "Red" + return $false + } + } catch { + Write-ColorText "Error checking Docker status: $_" -ForegroundColor "Red" + return $false + } +} + +# Function to prompt the user for the main menu choice +function Prompt-MainMenu { + Write-Host "" + Write-ColorText "Welcome to DocsGPT Setup!" -ForegroundColor "White" -Bold + Write-ColorText "How would you like to proceed?" -ForegroundColor "White" + Write-ColorText "1) Use DocsGPT Public API Endpoint (simple and free)" -ForegroundColor "Yellow" + Write-ColorText "2) Serve Local (with Ollama)" -ForegroundColor "Yellow" + Write-ColorText "3) Connect Local Inference Engine" -ForegroundColor "Yellow" + Write-ColorText "4) Connect Cloud API Provider" -ForegroundColor "Yellow" + Write-Host "" + $script:main_choice = Read-Host "Choose option (1-4)" +} + +# Function to prompt for Local Inference Engine options +function Prompt-LocalInferenceEngineOptions { + Clear-Host + Write-Host "" + Write-ColorText "Connect Local Inference Engine" -ForegroundColor "White" -Bold + Write-ColorText "Choose your local inference engine:" -ForegroundColor "White" + Write-ColorText "1) LLaMa.cpp" -ForegroundColor "Yellow" + Write-ColorText "2) Ollama" -ForegroundColor "Yellow" + Write-ColorText "3) Text Generation Inference (TGI)" -ForegroundColor "Yellow" + Write-ColorText "4) SGLang" -ForegroundColor "Yellow" + Write-ColorText "5) vLLM" -ForegroundColor "Yellow" + Write-ColorText "6) Aphrodite" -ForegroundColor "Yellow" + Write-ColorText "7) FriendliAI" -ForegroundColor "Yellow" + Write-ColorText "8) LMDeploy" -ForegroundColor "Yellow" + Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow" + Write-Host "" + $script:engine_choice = Read-Host "Choose option (1-8, or b)" +} + +# Function to prompt for Cloud API Provider options +function Prompt-CloudAPIProviderOptions { + Clear-Host + Write-Host "" + Write-ColorText "Connect Cloud API Provider" -ForegroundColor "White" -Bold + Write-ColorText "Choose your Cloud API Provider:" -ForegroundColor "White" + Write-ColorText "1) OpenAI" -ForegroundColor "Yellow" + Write-ColorText "2) Google (Vertex AI, Gemini)" -ForegroundColor "Yellow" + Write-ColorText "3) Anthropic (Claude)" -ForegroundColor "Yellow" + Write-ColorText "4) Groq" -ForegroundColor "Yellow" + Write-ColorText "5) HuggingFace Inference API" -ForegroundColor "Yellow" + Write-ColorText "6) Azure OpenAI" -ForegroundColor "Yellow" + Write-ColorText "7) Novita" -ForegroundColor "Yellow" + Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow" + Write-Host "" + $script:provider_choice = Read-Host "Choose option (1-7, or b)" +} + +# Function to prompt for Ollama CPU/GPU options +function Prompt-OllamaOptions { + Clear-Host + Write-Host "" + Write-ColorText "Serve Local with Ollama" -ForegroundColor "White" -Bold + Write-ColorText "Choose how to serve Ollama:" -ForegroundColor "White" + Write-ColorText "1) CPU" -ForegroundColor "Yellow" + Write-ColorText "2) GPU" -ForegroundColor "Yellow" + Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow" + Write-Host "" + $script:ollama_choice = Read-Host "Choose option (1-2, or b)" +} + +# 1) Use DocsGPT Public API Endpoint (simple and free) +function Use-DocsPublicAPIEndpoint { + Write-Host "" + Write-ColorText "Setting up DocsGPT Public API Endpoint..." -ForegroundColor "White" + + # Create .env file + "LLM_NAME=docsgpt" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force + "VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8 + + Write-ColorText ".env file configured for DocsGPT Public API." -ForegroundColor "Green" + + # Start Docker if needed + $dockerRunning = Check-AndStartDocker + if (-not $dockerRunning) { + Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red" + return + } + + Write-Host "" + Write-ColorText "Starting Docker Compose..." -ForegroundColor "White" + + # Run Docker compose commands + try { + & docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build + if ($LASTEXITCODE -ne 0) { + throw "Docker compose build failed with exit code $LASTEXITCODE" + } + + & docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d + if ($LASTEXITCODE -ne 0) { + throw "Docker compose up failed with exit code $LASTEXITCODE" + } + + Write-Host "" + Write-ColorText "DocsGPT is now running on http://localhost:5173" -ForegroundColor "Green" + Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow" + } + catch { + Write-Host "" + Write-ColorText "Error starting Docker Compose: $_" -ForegroundColor "Red" + Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red" + Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red" + exit 1 # Exit script with error + } +} + +# 2) Serve Local (with Ollama) +function Serve-LocalOllama { + $script:model_name = "" + $default_model = "llama3.2:1b" + $docker_compose_file_suffix = "" + + function Get-ModelNameOllama { + $model_name_input = Read-Host "Enter Ollama Model Name (press Enter for default: $default_model (1.3GB))" + if ([string]::IsNullOrEmpty($model_name_input)) { + $script:model_name = $default_model + } else { + $script:model_name = $model_name_input + } + } + + while ($true) { + Clear-Host + Prompt-OllamaOptions + + switch ($ollama_choice) { + "1" { # CPU + $docker_compose_file_suffix = "cpu" + Get-ModelNameOllama + break + } + "2" { # GPU + Write-Host "" + Write-ColorText "For this option to work correctly you need to have a supported GPU and configure Docker to utilize it." -ForegroundColor "Yellow" + Write-ColorText "Refer to: https://hub.docker.com/r/ollama/ollama for more information." -ForegroundColor "Yellow" + $confirm_gpu = Read-Host "Continue with GPU setup? (y/b)" + + if ($confirm_gpu -eq "y" -or $confirm_gpu -eq "Y") { + $docker_compose_file_suffix = "gpu" + Get-ModelNameOllama + break + } + elseif ($confirm_gpu -eq "b" -or $confirm_gpu -eq "B") { + Clear-Host + return + } + else { + Write-Host "" + Write-ColorText "Invalid choice. Please choose y or b." -ForegroundColor "Red" + Start-Sleep -Seconds 1 + } + } + "b" { Clear-Host; return } + "B" { Clear-Host; return } + default { + Write-Host "" + Write-ColorText "Invalid choice. Please choose 1-2, or b." -ForegroundColor "Red" + Start-Sleep -Seconds 1 + } + } + + if (-not [string]::IsNullOrEmpty($docker_compose_file_suffix)) { + break + } + } + + Write-Host "" + Write-ColorText "Configuring for Ollama ($($docker_compose_file_suffix.ToUpper()))..." -ForegroundColor "White" + + # Create .env file + "API_KEY=xxxx" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force + "LLM_NAME=openai" | Add-Content -Path $ENV_FILE -Encoding utf8 + "MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8 + "VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8 + "OPENAI_BASE_URL=http://host.docker.internal:11434/v1" | Add-Content -Path $ENV_FILE -Encoding utf8 + "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" | Add-Content -Path $ENV_FILE -Encoding utf8 + + Write-ColorText ".env file configured for Ollama ($($docker_compose_file_suffix.ToUpper()))." -ForegroundColor "Green" + Write-ColorText "Note: MODEL_NAME is set to '$model_name'. You can change it later in the .env file." -ForegroundColor "Yellow" + + # Start Docker if needed + $dockerRunning = Check-AndStartDocker + if (-not $dockerRunning) { + Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red" + return + } + + # Setup compose file paths + $optional_compose = Join-Path -Path (Split-Path -Parent $COMPOSE_FILE) -ChildPath "optional\docker-compose.optional.ollama-$docker_compose_file_suffix.yaml" + + try { + Write-Host "" + Write-ColorText "Starting Docker Compose with Ollama ($docker_compose_file_suffix)..." -ForegroundColor "White" + + # Build the containers + & docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" build + if ($LASTEXITCODE -ne 0) { + throw "Docker compose build failed with exit code $LASTEXITCODE" + } + + # Start the containers + & docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" up -d + if ($LASTEXITCODE -ne 0) { + throw "Docker compose up failed with exit code $LASTEXITCODE" + } + + # Wait for Ollama container to be ready + Write-ColorText "Waiting for Ollama container to be ready..." -ForegroundColor "White" + $ollamaReady = $false + $maxAttempts = 30 # Maximum number of attempts (30 x 5 seconds = 2.5 minutes) + $attempts = 0 + + while (-not $ollamaReady -and $attempts -lt $maxAttempts) { + $containerStatus = & docker compose -f "$COMPOSE_FILE" -f "$optional_compose" ps --services --filter "status=running" --format "{{.Service}}" + + if ($containerStatus -like "*ollama*") { + $ollamaReady = $true + Write-ColorText "Ollama container is running." -ForegroundColor "Green" + } else { + Write-Host "Ollama container not yet ready, waiting... (Attempt $($attempts+1)/$maxAttempts)" + Start-Sleep -Seconds 5 + $attempts++ + } + } + + if (-not $ollamaReady) { + Write-ColorText "Ollama container did not start within the expected time. Please check Docker logs for errors." -ForegroundColor "Red" + return + } + + # Pull the Ollama model + Write-ColorText "Pulling $model_name model for Ollama..." -ForegroundColor "White" + & docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" exec -it ollama ollama pull "$model_name" + + Write-Host "" + Write-ColorText "DocsGPT is now running with Ollama ($docker_compose_file_suffix) on http://localhost:5173" -ForegroundColor "Green" + Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" -f `"$optional_compose`" down" -ForegroundColor "Yellow" + } + catch { + Write-Host "" + Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red" + Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red" + Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red" + exit 1 + } +} + +# 3) Connect Local Inference Engine +function Connect-LocalInferenceEngine { + $script:engine_name = "" + $script:openai_base_url = "" + $script:model_name = "" + + function Get-ModelName { + $model_name_input = Read-Host "Enter Model Name (press Enter for None)" + if ([string]::IsNullOrEmpty($model_name_input)) { + $script:model_name = "None" + } else { + $script:model_name = $model_name_input + } + } + + while ($true) { + Clear-Host + Prompt-LocalInferenceEngineOptions + + switch ($engine_choice) { + "1" { # LLaMa.cpp + $script:engine_name = "LLaMa.cpp" + $script:openai_base_url = "http://localhost:8000/v1" + Get-ModelName + break + } + "2" { # Ollama + $script:engine_name = "Ollama" + $script:openai_base_url = "http://localhost:11434/v1" + Get-ModelName + break + } + "3" { # TGI + $script:engine_name = "TGI" + $script:openai_base_url = "http://localhost:8080/v1" + Get-ModelName + break + } + "4" { # SGLang + $script:engine_name = "SGLang" + $script:openai_base_url = "http://localhost:30000/v1" + Get-ModelName + break + } + "5" { # vLLM + $script:engine_name = "vLLM" + $script:openai_base_url = "http://localhost:8000/v1" + Get-ModelName + break + } + "6" { # Aphrodite + $script:engine_name = "Aphrodite" + $script:openai_base_url = "http://localhost:2242/v1" + Get-ModelName + break + } + "7" { # FriendliAI + $script:engine_name = "FriendliAI" + $script:openai_base_url = "http://localhost:8997/v1" + Get-ModelName + break + } + "8" { # LMDeploy + $script:engine_name = "LMDeploy" + $script:openai_base_url = "http://localhost:23333/v1" + Get-ModelName + break + } + "b" { Clear-Host; return } + "B" { Clear-Host; return } + default { + Write-Host "" + Write-ColorText "Invalid choice. Please choose 1-8, or b." -ForegroundColor "Red" + Start-Sleep -Seconds 1 + } + } + + if (-not [string]::IsNullOrEmpty($script:engine_name)) { + break + } + } + + Write-Host "" + Write-ColorText "Configuring for Local Inference Engine: $engine_name..." -ForegroundColor "White" + + # Create .env file + "API_KEY=None" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force + "LLM_NAME=openai" | Add-Content -Path $ENV_FILE -Encoding utf8 + "MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8 + "VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8 + "OPENAI_BASE_URL=$openai_base_url" | Add-Content -Path $ENV_FILE -Encoding utf8 + "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" | Add-Content -Path $ENV_FILE -Encoding utf8 + + Write-ColorText ".env file configured for $engine_name with OpenAI API format." -ForegroundColor "Green" + Write-ColorText "Note: MODEL_NAME is set to '$model_name'. You can change it later in the .env file." -ForegroundColor "Yellow" + + # Start Docker if needed + $dockerRunning = Check-AndStartDocker + if (-not $dockerRunning) { + Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red" + return + } + + try { + Write-Host "" + Write-ColorText "Starting Docker Compose..." -ForegroundColor "White" + + # Build the containers + & docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build + if ($LASTEXITCODE -ne 0) { + throw "Docker compose build failed with exit code $LASTEXITCODE" + } + + # Start the containers + & docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d + if ($LASTEXITCODE -ne 0) { + throw "Docker compose up failed with exit code $LASTEXITCODE" + } + + Write-Host "" + Write-ColorText "DocsGPT is now configured to connect to $engine_name at $openai_base_url" -ForegroundColor "Green" + Write-ColorText "Ensure your $engine_name inference server is running at that address" -ForegroundColor "Yellow" + Write-Host "" + Write-ColorText "DocsGPT is running at http://localhost:5173" -ForegroundColor "Green" + Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow" + } + catch { + Write-Host "" + Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red" + Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red" + Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red" + exit 1 + } +} + +# 4) Connect Cloud API Provider +function Connect-CloudAPIProvider { + $script:provider_name = "" + $script:llm_name = "" + $script:model_name = "" + $script:api_key = "" + + function Get-APIKey { + Write-ColorText "Your API key will be stored locally in the .env file and will not be sent anywhere else" -ForegroundColor "Yellow" + $script:api_key = Read-Host "Please enter your API key" + } + + while ($true) { + Clear-Host + Prompt-CloudAPIProviderOptions + + switch ($provider_choice) { + "1" { # OpenAI + $script:provider_name = "OpenAI" + $script:llm_name = "openai" + $script:model_name = "gpt-4o" + Get-APIKey + break + } + "2" { # Google + $script:provider_name = "Google (Vertex AI, Gemini)" + $script:llm_name = "google" + $script:model_name = "gemini-2.0-flash" + Get-APIKey + break + } + "3" { # Anthropic + $script:provider_name = "Anthropic (Claude)" + $script:llm_name = "anthropic" + $script:model_name = "claude-3-5-sonnet-latest" + Get-APIKey + break + } + "4" { # Groq + $script:provider_name = "Groq" + $script:llm_name = "groq" + $script:model_name = "llama-3.1-8b-instant" + Get-APIKey + break + } + "5" { # HuggingFace Inference API + $script:provider_name = "HuggingFace Inference API" + $script:llm_name = "huggingface" + $script:model_name = "meta-llama/Llama-3.1-8B-Instruct" + Get-APIKey + break + } + "6" { # Azure OpenAI + $script:provider_name = "Azure OpenAI" + $script:llm_name = "azure_openai" + $script:model_name = "gpt-4o" + Get-APIKey + break + } + "7" { # Novita + $script:provider_name = "Novita" + $script:llm_name = "novita" + $script:model_name = "deepseek/deepseek-r1" + Get-APIKey + break + } + "b" { Clear-Host; return } + "B" { Clear-Host; return } + default { + Write-Host "" + Write-ColorText "Invalid choice. Please choose 1-7, or b." -ForegroundColor "Red" + Start-Sleep -Seconds 1 + } + } + + if (-not [string]::IsNullOrEmpty($script:provider_name)) { + break + } + } + + Write-Host "" + Write-ColorText "Configuring for Cloud API Provider: $provider_name..." -ForegroundColor "White" + + # Create .env file + "API_KEY=$api_key" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force + "LLM_NAME=$llm_name" | Add-Content -Path $ENV_FILE -Encoding utf8 + "MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8 + "VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8 + + Write-ColorText ".env file configured for $provider_name." -ForegroundColor "Green" + + # Start Docker if needed + $dockerRunning = Check-AndStartDocker + if (-not $dockerRunning) { + Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red" + return + } + + try { + Write-Host "" + Write-ColorText "Starting Docker Compose..." -ForegroundColor "White" + + # Run Docker compose commands + & docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d --build + if ($LASTEXITCODE -ne 0) { + throw "Docker compose build or up failed with exit code $LASTEXITCODE" + } + + Write-Host "" + Write-ColorText "DocsGPT is now configured to use $provider_name on http://localhost:5173" -ForegroundColor "Green" + Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow" + } + catch { + Write-Host "" + Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red" + Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red" + Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red" + exit 1 + } +} + +# Main script execution +Animate-Dino + +while ($true) { + Clear-Host + Prompt-MainMenu + + $exitLoop = $false # Add this flag + + switch ($main_choice) { + "1" { + Use-DocsPublicAPIEndpoint + $exitLoop = $true # Set flag to true on completion + break + } + "2" { + Serve-LocalOllama + # Only exit the loop if user didn't press "b" to go back + if ($ollama_choice -ne "b" -and $ollama_choice -ne "B") { + $exitLoop = $true + } + break + } + "3" { + Connect-LocalInferenceEngine + # Only exit the loop if user didn't press "b" to go back + if ($engine_choice -ne "b" -and $engine_choice -ne "B") { + $exitLoop = $true + } + break + } + "4" { + Connect-CloudAPIProvider + # Only exit the loop if user didn't press "b" to go back + if ($provider_choice -ne "b" -and $provider_choice -ne "B") { + $exitLoop = $true + } + break + } + default { + Write-Host "" + Write-ColorText "Invalid choice. Please choose 1-4." -ForegroundColor "Red" + Start-Sleep -Seconds 1 + } + } + + # Only break out of the loop if a function completed successfully + if ($exitLoop) { + break + } +} + +Write-Host "" +Write-ColorText "DocsGPT Setup Complete." -ForegroundColor "Green" + +exit 0 \ No newline at end of file From c70be12bfdf4f9c86608fb44ed3a028a15f5c61b Mon Sep 17 00:00:00 2001 From: asminkarki012 Date: Fri, 28 Mar 2025 22:46:11 +0545 Subject: [PATCH 02/26] fix[csv_parser]:missing header --- application/parser/file/tabular_parser.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/application/parser/file/tabular_parser.py b/application/parser/file/tabular_parser.py index b2dbd193..079fb475 100644 --- a/application/parser/file/tabular_parser.py +++ b/application/parser/file/tabular_parser.py @@ -104,9 +104,13 @@ class PandasCSVParser(BaseParser): raise ValueError("pandas module is required to read CSV files.") df = pd.read_csv(file, **self._pandas_config) + headers = df.columns.tolist() text_list = df.apply( - lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1 + lambda row: self._col_joiner.join( + [f"{headers[i]}: {str(val)}" for i, val in enumerate(row)] + ), + axis=1, ).tolist() if self._concat_rows: @@ -169,12 +173,16 @@ class ExcelParser(BaseParser): raise ValueError("pandas module is required to read Excel files.") df = pd.read_excel(file, **self._pandas_config) + headers = df.columns.tolist() text_list = df.apply( - lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1 + lambda row: self._col_joiner.join( + [f"{headers[i]}: {str(val)}" for i, val in enumerate(row)] + ), + axis=1, ).tolist() if self._concat_rows: return (self._row_joiner).join(text_list) else: - return text_list \ No newline at end of file + return text_list From 57a6fb31b2f252e0414defe464f0ccc5f8ea4864 Mon Sep 17 00:00:00 2001 From: Pavel Date: Mon, 31 Mar 2025 22:28:04 +0400 Subject: [PATCH 03/26] periodic header injection --- application/parser/file/tabular_parser.py | 79 ++++++++++++++++------- 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/application/parser/file/tabular_parser.py b/application/parser/file/tabular_parser.py index 079fb475..40971b3c 100644 --- a/application/parser/file/tabular_parser.py +++ b/application/parser/file/tabular_parser.py @@ -73,7 +73,13 @@ class PandasCSVParser(BaseParser): for more information. Set to empty dict by default, this means pandas will try to figure out the separators, table head, etc. on its own. - + + header_period (int): Controls how headers are included in output: + - 0: Headers only at the beginning + - 1: Headers in every row + - N > 1: Headers every N rows + + header_prefix (str): Prefix for header rows. Default is "HEADERS: ". """ def __init__( @@ -83,6 +89,8 @@ class PandasCSVParser(BaseParser): col_joiner: str = ", ", row_joiner: str = "\n", pandas_config: dict = {}, + header_period: int = 20, + header_prefix: str = "HEADERS: ", **kwargs: Any ) -> None: """Init params.""" @@ -91,6 +99,8 @@ class PandasCSVParser(BaseParser): self._col_joiner = col_joiner self._row_joiner = row_joiner self._pandas_config = pandas_config + self._header_period = header_period + self._header_prefix = header_prefix def _init_parser(self) -> Dict: """Init parser.""" @@ -105,18 +115,25 @@ class PandasCSVParser(BaseParser): df = pd.read_csv(file, **self._pandas_config) headers = df.columns.tolist() + header_row = f"{self._header_prefix}{self._col_joiner.join(headers)}" - text_list = df.apply( - lambda row: self._col_joiner.join( - [f"{headers[i]}: {str(val)}" for i, val in enumerate(row)] - ), - axis=1, - ).tolist() + if not self._concat_rows: + return df.apply( + lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1 + ).tolist() + + text_list = [] + if self._header_period != 1: + text_list.append(header_row) + + for i, row in df.iterrows(): + if (self._header_period > 1 and i > 0 and i % self._header_period == 0): + text_list.append(header_row) + text_list.append(self._col_joiner.join(row.astype(str).tolist())) + if self._header_period == 1 and i < len(df) - 1: + text_list.append(header_row) - if self._concat_rows: - return (self._row_joiner).join(text_list) - else: - return text_list + return self._row_joiner.join(text_list) class ExcelParser(BaseParser): @@ -142,7 +159,13 @@ class ExcelParser(BaseParser): for more information. Set to empty dict by default, this means pandas will try to figure out the table structure on its own. - + + header_period (int): Controls how headers are included in output: + - 0: Headers only at the beginning (default) + - 1: Headers in every row + - N > 1: Headers every N rows + + header_prefix (str): Prefix for header rows. Default is "HEADERS: ". """ def __init__( @@ -152,6 +175,8 @@ class ExcelParser(BaseParser): col_joiner: str = ", ", row_joiner: str = "\n", pandas_config: dict = {}, + header_period: int = 20, + header_prefix: str = "HEADERS: ", **kwargs: Any ) -> None: """Init params.""" @@ -160,6 +185,8 @@ class ExcelParser(BaseParser): self._col_joiner = col_joiner self._row_joiner = row_joiner self._pandas_config = pandas_config + self._header_period = header_period + self._header_prefix = header_prefix def _init_parser(self) -> Dict: """Init parser.""" @@ -174,15 +201,21 @@ class ExcelParser(BaseParser): df = pd.read_excel(file, **self._pandas_config) headers = df.columns.tolist() + header_row = f"{self._header_prefix}{self._col_joiner.join(headers)}" + + if not self._concat_rows: + return df.apply( + lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1 + ).tolist() + + text_list = [] + if self._header_period != 1: + text_list.append(header_row) - text_list = df.apply( - lambda row: self._col_joiner.join( - [f"{headers[i]}: {str(val)}" for i, val in enumerate(row)] - ), - axis=1, - ).tolist() - - if self._concat_rows: - return (self._row_joiner).join(text_list) - else: - return text_list + for i, row in df.iterrows(): + if (self._header_period > 1 and i > 0 and i % self._header_period == 0): + text_list.append(header_row) + text_list.append(self._col_joiner.join(row.astype(str).tolist())) + if self._header_period == 1 and i < len(df) - 1: + text_list.append(header_row) + return self._row_joiner.join(text_list) \ No newline at end of file From 8bb263a2ecfb73766df16f498dd238696c1a847c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:24:09 +0000 Subject: [PATCH 04/26] build(deps): bump multidict from 6.1.0 to 6.3.2 in /application Bumps [multidict](https://github.com/aio-libs/multidict) from 6.1.0 to 6.3.2. - [Release notes](https://github.com/aio-libs/multidict/releases) - [Changelog](https://github.com/aio-libs/multidict/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/multidict/compare/v6.1.0...v6.3.2) --- updated-dependencies: - dependency-name: multidict dependency-version: 6.3.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index 3a7f6051..760078ae 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -41,7 +41,7 @@ lxml==5.3.1 markupsafe==3.0.2 marshmallow==3.26.1 mpmath==1.3.0 -multidict==6.1.0 +multidict==6.3.2 mypy-extensions==1.0.0 networkx==3.4.2 numpy==2.2.1 From 5923781484a02c8b84390a83eaa18503cad0237a Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 4 Apr 2025 03:29:01 +0530 Subject: [PATCH 05/26] (feat:attach) warning for error, progress, removal --- frontend/src/components/MessageInput.tsx | 173 +++++++++++------- .../src/conversation/conversationSlice.ts | 4 + 2 files changed, 108 insertions(+), 69 deletions(-) diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 1e3576ee..62e6dea0 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -9,13 +9,16 @@ import SourceIcon from '../assets/source.svg'; import ToolIcon from '../assets/tool.svg'; import SpinnerDark from '../assets/spinner-dark.svg'; import Spinner from '../assets/spinner.svg'; +import ExitIcon from '../assets/exit.svg'; +import AlertIcon from '../assets/alert.svg'; import SourcesPopup from './SourcesPopup'; import ToolsPopup from './ToolsPopup'; import { selectSelectedDocs, selectToken } from '../preferences/preferenceSlice'; import { ActiveState } from '../models/misc'; import Upload from '../upload/Upload'; import ClipIcon from '../assets/clip.svg'; -import { setAttachments } from '../conversation/conversationSlice'; +import { setAttachments, removeAttachment } from '../conversation/conversationSlice'; + interface MessageInputProps { value: string; @@ -71,50 +74,59 @@ export default function MessageInput({ status: 'uploading' }; - setUploads(prev => [...prev, uploadState]); - const uploadIndex = uploads.length; - - xhr.upload.addEventListener('progress', (event) => { - if (event.lengthComputable) { - const progress = Math.round((event.loaded / event.total) * 100); - setUploads(prev => prev.map((upload, index) => - index === uploadIndex - ? { ...upload, progress } - : upload - )); - } - }); - - xhr.onload = () => { - if (xhr.status === 200) { - const response = JSON.parse(xhr.responseText); - console.log('File uploaded successfully:', response); - - if (response.task_id) { - setUploads(prev => prev.map((upload, index) => - index === uploadIndex - ? { ...upload, taskId: response.task_id, status: 'processing' } + setUploads(prev => { + const newUploads = [...prev, uploadState]; + const uploadIndex = newUploads.length - 1; + + xhr.upload.addEventListener('progress', (event) => { + if (event.lengthComputable) { + const progress = Math.round((event.loaded / event.total) * 100); + setUploads(current => current.map((upload, idx) => + idx === uploadIndex + ? { ...upload, progress } : upload )); } - } else { - setUploads(prev => prev.map((upload, index) => - index === uploadIndex + }); + + xhr.onload = () => { + if (xhr.status === 200) { + const response = JSON.parse(xhr.responseText); + console.log('File uploaded successfully:', response); + + if (response.task_id) { + setUploads(current => current.map((upload, idx) => + idx === uploadIndex + ? { + ...upload, + taskId: response.task_id, + status: 'processing', + progress: 10 + } + : upload + )); + } + } else { + setUploads(current => current.map((upload, idx) => + idx === uploadIndex + ? { ...upload, status: 'failed' } + : upload + )); + console.error('Error uploading file:', xhr.responseText); + } + }; + + xhr.onerror = () => { + setUploads(current => current.map((upload, idx) => + idx === uploadIndex ? { ...upload, status: 'failed' } : upload )); - console.error('Error uploading file:', xhr.responseText); - } - }; - - xhr.onerror = () => { - setUploads(prev => prev.map((upload, index) => - index === uploadIndex - ? { ...upload, status: 'failed' } - : upload - )); - console.error('Network error during file upload'); - }; + console.error('Network error during file upload'); + }; + + return newUploads; + }); xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`); xhr.setRequestHeader('Authorization', `Bearer ${token}`); @@ -233,45 +245,68 @@ export default function MessageInput({ {uploads.map((upload, index) => (
{upload.fileName} {upload.status === 'completed' && ( - + )} {upload.status === 'failed' && ( - + Upload failed )} - {(upload.status === 'uploading' || upload.status === 'processing') && ( -
- - - - -
- )} +{(upload.status === 'uploading' || upload.status === 'processing') && ( +
+ + {/* Background circle */} + + + +
+)}
))} diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index d38ba21b..fd07c707 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -289,6 +289,9 @@ export const conversationSlice = createSlice({ setAttachments: (state, action: PayloadAction<{ fileName: string; id: string }[]>) => { state.attachments = action.payload; }, + removeAttachment(state, action: PayloadAction) { + state.attachments = state.attachments?.filter(att => att.id !== action.payload); + }, }, extraReducers(builder) { builder @@ -323,5 +326,6 @@ export const { updateToolCalls, setConversation, setAttachments, + removeAttachment, } = conversationSlice.actions; export default conversationSlice.reducer; From 493dc8689ceba40bb0b4c79c5a0fd6b8d21291e7 Mon Sep 17 00:00:00 2001 From: Pavel Date: Sat, 5 Apr 2025 17:49:56 +0400 Subject: [PATCH 06/26] guide-updates --- README.md | 12 +++++++----- docs/pages/quickstart.mdx | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4c0953a3..caf0b367 100644 --- a/README.md +++ b/README.md @@ -92,13 +92,15 @@ A more detailed [Quickstart](https://docs.docsgpt.cloud/quickstart) is available ./setup.sh ``` -This interactive script will guide you through setting up DocsGPT. It offers four options: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. The script will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option. - **For Windows:** -2. **Follow the Docker Deployment Guide:** +2. **Run the PowerShell setup script:** - Please refer to the [Docker Deployment documentation](https://docs.docsgpt.cloud/Deploying/Docker-Deploying) for detailed step-by-step instructions on setting up DocsGPT using Docker. + ```powershell + PowerShell -ExecutionPolicy Bypass -File .\setup.ps1 + ``` + +Either script will guide you through setting up DocsGPT. Four options available: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. Scripts will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option. **Navigate to http://localhost:5173/** @@ -107,7 +109,7 @@ To stop DocsGPT, open a terminal in the `DocsGPT` directory and run: ```bash docker compose -f deployment/docker-compose.yaml down ``` -(or use the specific `docker compose down` command shown after running `setup.sh`). +(or use the specific `docker compose down` command shown after running the setup script). > [!Note] > For development environment setup instructions, please refer to the [Development Environment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment). diff --git a/docs/pages/quickstart.mdx b/docs/pages/quickstart.mdx index cef1cd68..11a17895 100644 --- a/docs/pages/quickstart.mdx +++ b/docs/pages/quickstart.mdx @@ -73,9 +73,44 @@ The easiest way to launch DocsGPT is using the provided `setup.sh` script. This ## Launching DocsGPT (Windows) -For Windows users, we recommend following the Docker deployment guide for detailed instructions. Please refer to the [Docker Deployment documentation](/Deploying/Docker-Deploying) for step-by-step instructions on setting up DocsGPT on Windows using Docker. +For Windows users, we provide a PowerShell script that offers the same functionality as the macOS/Linux setup script. -**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding. +**Steps:** + +1. **Download the DocsGPT Repository:** + + First, you need to download the DocsGPT repository to your local machine. You can do this using Git: + + ```powershell + git clone https://github.com/arc53/DocsGPT.git + cd DocsGPT + ``` + +2. **Run the `setup.ps1` script:** + + Execute the PowerShell setup script: + + ```powershell + PowerShell -ExecutionPolicy Bypass -File .\setup.ps1 + ``` + +3. **Follow the interactive setup:** + + Just like the Linux/macOS script, the PowerShell script will guide you through setting DocsGPT. + The script will handle environment configuration and start DocsGPT based on your selections. + +4. **Access DocsGPT in your browser:** + + Once the setup is complete and Docker containers are running, navigate to [http://localhost:5173/](http://localhost:5173/) in your web browser to access the DocsGPT web application. + +5. **Stopping DocsGPT:** + + To stop DocsGPT run the Docker Compose down command displayed at the end of the setup script's execution. + +**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding. The script will attempt to start Docker if it's not running, but you may need to start it manually if there are issues. + +**Alternative Method:** +If you prefer a more manual approach, you can follow our [Docker Deployment documentation](/Deploying/Docker-Deploying) for detailed instructions on setting up DocsGPT on Windows using Docker commands directly. ## Advanced Configuration From e4945b41e9d90b062f15729e5768738b7670c0dd Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 6 Apr 2025 15:57:18 +0530 Subject: [PATCH 07/26] (feat:files) link attachment to openai_api --- application/llm/openai.py | 134 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/application/llm/openai.py b/application/llm/openai.py index 5e11a072..9da09c15 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -35,6 +35,15 @@ class OpenAILLM(BaseLLM): cleaned_messages.append( {"role": role, "content": item["text"]} ) + elif isinstance(item, dict): + content_parts = [] + if "text" in item: + content_parts.append({"type": "text", "text": item["text"]}) + elif "type" in item and item["type"] == "text" and "text" in item: + content_parts.append(item) + elif "type" in item and item["type"] == "file" and "file" in item: + content_parts.append(item) + cleaned_messages.append({"role": role, "content": content_parts}) elif "function_call" in item: tool_call = { "id": item["function_call"]["call_id"], @@ -133,6 +142,131 @@ class OpenAILLM(BaseLLM): def _supports_tools(self): return True + def prepare_messages_with_attachments(self, messages, attachments=None): + """ + Process attachments using OpenAI's file API for more efficient handling. + + Args: + messages (list): List of message dictionaries. + attachments (list): List of attachment dictionaries with content and metadata. + + Returns: + list: Messages formatted with file references for OpenAI API. + """ + if not attachments: + return messages + + prepared_messages = messages.copy() + + # Find the user message to attach file_id to the last one + user_message_index = None + for i in range(len(prepared_messages) - 1, -1, -1): + if prepared_messages[i].get("role") == "user": + user_message_index = i + break + + if user_message_index is None: + user_message = {"role": "user", "content": []} + prepared_messages.append(user_message) + user_message_index = len(prepared_messages) - 1 + + if isinstance(prepared_messages[user_message_index].get("content"), str): + text_content = prepared_messages[user_message_index]["content"] + prepared_messages[user_message_index]["content"] = [ + {"type": "text", "text": text_content} + ] + elif not isinstance(prepared_messages[user_message_index].get("content"), list): + prepared_messages[user_message_index]["content"] = [] + + for attachment in attachments: + # Upload the file to OpenAI + try: + file_id = self._upload_file_to_openai(attachment) + + prepared_messages[user_message_index]["content"].append({ + "type": "file", + "file": {"file_id": file_id} + }) + except Exception as e: + import logging + logging.error(f"Error uploading attachment to OpenAI: {e}") + if 'content' in attachment: + prepared_messages[user_message_index]["content"].append({ + "type": "text", + "text": f"File content:\n\n{attachment['content']}" + }) + + return prepared_messages + + def _upload_file_to_openai(self, attachment): + """ + Upload a file to OpenAI and return the file_id. + + Args: + attachment (dict): Attachment dictionary with path and metadata. + Expected keys: + - path: Path to the file + - id: Optional MongoDB ID for caching + + Returns: + str: OpenAI file_id for the uploaded file. + """ + import os + import mimetypes + + # Check if we already have the file_id cached + if 'openai_file_id' in attachment: + return attachment['openai_file_id'] + + file_path = attachment.get('path') + if not file_path: + raise ValueError("No file path provided in attachment") + + # Make path absolute if it's relative + if not os.path.isabs(file_path): + current_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + file_path = os.path.join(current_dir,"application", file_path) + + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + mime_type = attachment.get('mime_type') + if not mime_type: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + supported_mime_types = ['application/pdf', 'image/png', 'image/jpeg', 'image/gif'] + if mime_type not in supported_mime_types: + import logging + logging.warning(f"MIME type {mime_type} not supported by OpenAI for file uploads. Falling back to text.") + raise ValueError(f"Unsupported MIME type: {mime_type}") + + try: + with open(file_path, 'rb') as file: + response = self.client.files.create( + file=file, + purpose="assistants" + ) + + file_id = response.id + + from application.core.mongo_db import MongoDB + mongo = MongoDB.get_client() + db = mongo["docsgpt"] + attachments_collection = db["attachments"] + if '_id' in attachment: + attachments_collection.update_one( + {"_id": attachment['_id']}, + {"$set": {"openai_file_id": file_id}} + ) + + return file_id + except Exception as e: + import logging + logging.error(f"Error uploading file to OpenAI: {e}") + raise + class AzureOpenAILLM(OpenAILLM): From a37bd7695038685b8a81036652488267d8ac4fd4 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 6 Apr 2025 16:01:57 +0530 Subject: [PATCH 08/26] (feat:storeAttach) store in inputs, raise errors from worker --- application/api/user/routes.py | 25 ++++++++++++--------- application/worker.py | 41 +++++++++++++++------------------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 8f374aa7..91b028d5 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -2506,23 +2506,26 @@ class StoreAttachment(Resource): user = secure_filename(decoded_token.get("sub")) try: + attachment_id = ObjectId() original_filename = secure_filename(file.filename) - folder_name = original_filename - save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, "attachments",folder_name) + + save_dir = os.path.join( + current_dir, + settings.UPLOAD_FOLDER, + user, + "attachments", + str(attachment_id) + ) os.makedirs(save_dir, exist_ok=True) - # Create directory structure: user/attachments/filename/ + file_path = os.path.join(save_dir, original_filename) - # Handle filename conflicts - if os.path.exists(file_path): - name_parts = os.path.splitext(original_filename) - timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - new_filename = f"{name_parts[0]}_{timestamp}{name_parts[1]}" - file_path = os.path.join(save_dir, new_filename) - original_filename = new_filename file.save(file_path) - file_info = {"folder": folder_name, "filename": original_filename} + file_info = { + "filename": original_filename, + "attachment_id": str(attachment_id) + } current_app.logger.info(f"Saved file: {file_path}") # Start async task to process single file diff --git a/application/worker.py b/application/worker.py index 23ff0422..0437c38a 100755 --- a/application/worker.py +++ b/application/worker.py @@ -334,28 +334,23 @@ def attachment_worker(self, directory, file_info, user): db = mongo["docsgpt"] attachments_collection = db["attachments"] - job_name = file_info["folder"] - logging.info(f"Processing attachment: {job_name}", extra={"user": user, "job": job_name}) + filename = file_info["filename"] + attachment_id = file_info["attachment_id"] + + logging.info(f"Processing attachment: {attachment_id}/{filename}", extra={"user": user}) self.update_state(state="PROGRESS", meta={"current": 10}) - folder_name = file_info["folder"] - filename = file_info["filename"] - file_path = os.path.join(directory, filename) - - logging.info(f"Processing file: {file_path}", extra={"user": user, "job": job_name}) - if not os.path.exists(file_path): - logging.warning(f"File not found: {file_path}", extra={"user": user, "job": job_name}) - return {"error": "File not found"} + logging.warning(f"File not found: {file_path}", extra={"user": user}) + raise FileNotFoundError(f"File not found: {file_path}") try: reader = SimpleDirectoryReader( input_files=[file_path] ) - documents = reader.load_data() self.update_state(state="PROGRESS", meta={"current": 50}) @@ -364,33 +359,33 @@ def attachment_worker(self, directory, file_info, user): content = documents[0].text token_count = num_tokens_from_string(content) - file_path_relative = f"{user}/attachments/{folder_name}/{filename}" + file_path_relative = f"{settings.UPLOAD_FOLDER}/{user}/attachments/{attachment_id}/{filename}" - attachment_id = attachments_collection.insert_one({ + doc_id = ObjectId(attachment_id) + attachments_collection.insert_one({ + "_id": doc_id, "user": user, "path": file_path_relative, "content": content, "token_count": token_count, "date": datetime.datetime.now(), - }).inserted_id + }) logging.info(f"Stored attachment with ID: {attachment_id}", - extra={"user": user, "job": job_name}) + extra={"user": user}) self.update_state(state="PROGRESS", meta={"current": 100}) return { - "attachment_id": str(attachment_id), "filename": filename, - "folder": folder_name, "path": file_path_relative, - "token_count": token_count + "token_count": token_count, + "attachment_id": attachment_id } else: logging.warning("No content was extracted from the file", - extra={"user": user, "job": job_name}) - return {"error": "No content was extracted from the file"} + extra={"user": user}) + raise ValueError("No content was extracted from the file") except Exception as e: - logging.error(f"Error processing file {filename}: {e}", - extra={"user": user, "job": job_name}, exc_info=True) - return {"error": f"Error processing file: {str(e)}"} + logging.error(f"Error processing file {filename}: {e}", extra={"user": user}, exc_info=True) + raise \ No newline at end of file From 244c9b96a2dbafda7751de3f6d8c67515fc08ca6 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sun, 6 Apr 2025 16:02:30 +0530 Subject: [PATCH 09/26] (fix:attach) pass attachment docs as it is --- application/api/answer/routes.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 7f61880d..97fce28c 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -831,12 +831,7 @@ def get_attachments_content(attachment_ids, user): }) if attachment_doc: - attachments.append({ - "id": str(attachment_doc["_id"]), - "content": attachment_doc["content"], - "token_count": attachment_doc.get("token_count", 0), - "path": attachment_doc.get("path", "") - }) + attachments.append(attachment_doc) except Exception as e: logger.error(f"Error retrieving attachment {attachment_id}: {e}") From f6c88da81b34c0124068969f5d3c678af82e267e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Apr 2025 11:03:49 +0100 Subject: [PATCH 10/26] fix: thought param in /api/answer --- application/api/answer/routes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 7f61880d..2f158b5b 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -611,6 +611,7 @@ class Answer(Resource): source_log_docs = [] tool_calls = [] stream_ended = False + thought = "" for line in complete_stream( question=question, @@ -633,6 +634,8 @@ class Answer(Resource): source_log_docs = event["source"] elif event["type"] == "tool_calls": tool_calls = event["tool_calls"] + elif event["type"] == "thought": + thought = event["thought"] elif event["type"] == "error": logger.error(f"Error from stream: {event['error']}") return bad_request(500, event["error"]) @@ -664,6 +667,7 @@ class Answer(Resource): conversation_id, question, response_full, + thought, source_log_docs, tool_calls, llm, From 1f3d1cc73ea366a4f10a397328401941df35a254 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 7 Apr 2025 20:15:11 +0530 Subject: [PATCH 11/26] (feat:attach) handle unsupported attachments --- application/agents/llm_handler.py | 52 ++++++++++++++++++++++++++----- application/llm/base.py | 9 ++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/application/agents/llm_handler.py b/application/agents/llm_handler.py index 21d972d9..80acc2bb 100644 --- a/application/agents/llm_handler.py +++ b/application/agents/llm_handler.py @@ -15,7 +15,7 @@ class LLMHandler(ABC): @abstractmethod def handle_response(self, agent, resp, tools_dict, messages, attachments=None, **kwargs): pass - + def prepare_messages_with_attachments(self, agent, messages, attachments=None): """ Prepare messages with attachment content if available. @@ -33,15 +33,53 @@ class LLMHandler(ABC): logger.info(f"Preparing messages with {len(attachments)} attachments") - # Check if the LLM has its own custom attachment handling implementation - if hasattr(agent.llm, "prepare_messages_with_attachments") and agent.llm.__class__.__name__ != "BaseLLM": - logger.info(f"Using {agent.llm.__class__.__name__}'s own prepare_messages_with_attachments method") - return agent.llm.prepare_messages_with_attachments(messages, attachments) + supported_types = agent.llm.get_supported_attachment_types() - # Otherwise, append attachment content to the system prompt + supported_attachments = [] + unsupported_attachments = [] + + for attachment in attachments: + mime_type = attachment.get('mime_type') + if not mime_type: + import mimetypes + file_path = attachment.get('path') + if file_path: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + else: + unsupported_attachments.append(attachment) + continue + + if mime_type in supported_types: + supported_attachments.append(attachment) + else: + unsupported_attachments.append(attachment) + + # Process supported attachments with the LLM's custom method + prepared_messages = messages + if supported_attachments: + logger.info(f"Processing {len(supported_attachments)} supported attachments with {agent.llm.__class__.__name__}'s method") + prepared_messages = agent.llm.prepare_messages_with_attachments(messages, supported_attachments) + + # Process unsupported attachments with the default method + if unsupported_attachments: + logger.info(f"Processing {len(unsupported_attachments)} unsupported attachments with default method") + prepared_messages = self._append_attachment_content_to_system(prepared_messages, unsupported_attachments) + + return prepared_messages + + def _append_attachment_content_to_system(self, messages, attachments): + """ + Default method to append attachment content to the system prompt. + + Args: + messages (list): List of message dictionaries. + attachments (list): List of attachment dictionaries with content. + + Returns: + list: Messages with attachment context added to the system prompt. + """ prepared_messages = messages.copy() - # Build attachment content string attachment_texts = [] for attachment in attachments: logger.info(f"Adding attachment {attachment.get('id')} to context") diff --git a/application/llm/base.py b/application/llm/base.py index 0fce208c..0607159d 100644 --- a/application/llm/base.py +++ b/application/llm/base.py @@ -55,3 +55,12 @@ class BaseLLM(ABC): def _supports_tools(self): raise NotImplementedError("Subclass must implement _supports_tools method") + + def get_supported_attachment_types(self): + """ + Return a list of MIME types supported by this LLM for file uploads. + + Returns: + list: List of supported MIME types + """ + return [] # Default: no attachments supported From 0c1138179b811a2a5e117e5aa24546c66b43622b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Mon, 7 Apr 2025 20:16:03 +0530 Subject: [PATCH 12/26] (feat:attch) store file mime type --- application/worker.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/application/worker.py b/application/worker.py index 0437c38a..bbd422ac 100755 --- a/application/worker.py +++ b/application/worker.py @@ -328,6 +328,7 @@ def attachment_worker(self, directory, file_info, user): """ import datetime import os + import mimetypes from application.utils import num_tokens_from_string mongo = MongoDB.get_client() @@ -361,6 +362,8 @@ def attachment_worker(self, directory, file_info, user): file_path_relative = f"{settings.UPLOAD_FOLDER}/{user}/attachments/{attachment_id}/{filename}" + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + doc_id = ObjectId(attachment_id) attachments_collection.insert_one({ "_id": doc_id, @@ -368,6 +371,7 @@ def attachment_worker(self, directory, file_info, user): "path": file_path_relative, "content": content, "token_count": token_count, + "mime_type": mime_type, "date": datetime.datetime.now(), }) @@ -380,7 +384,8 @@ def attachment_worker(self, directory, file_info, user): "filename": filename, "path": file_path_relative, "token_count": token_count, - "attachment_id": attachment_id + "attachment_id": attachment_id, + "mime_type": mime_type } else: logging.warning("No content was extracted from the file", @@ -388,4 +393,4 @@ def attachment_worker(self, directory, file_info, user): raise ValueError("No content was extracted from the file") except Exception as e: logging.error(f"Error processing file {filename}: {e}", extra={"user": user}, exc_info=True) - raise \ No newline at end of file + raise From 5421bc13861cfbe8f7a7c66a79183d2fec8cc0dd Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 8 Apr 2025 15:51:37 +0530 Subject: [PATCH 13/26] (feat:attach) extend support for imgs --- application/llm/openai.py | 114 ++++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 29 deletions(-) diff --git a/application/llm/openai.py b/application/llm/openai.py index 9da09c15..ad0a0d56 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -1,4 +1,8 @@ import json +import base64 +import os +import mimetypes +import logging from application.core.settings import settings from application.llm.base import BaseLLM @@ -142,6 +146,22 @@ class OpenAILLM(BaseLLM): def _supports_tools(self): return True + def get_supported_attachment_types(self): + """ + Return a list of MIME types supported by OpenAI for file uploads. + + Returns: + list: List of supported MIME types + """ + return [ + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/webp', + 'image/gif' + ] + def prepare_messages_with_attachments(self, messages, attachments=None): """ Process attachments using OpenAI's file API for more efficient handling. @@ -179,26 +199,74 @@ class OpenAILLM(BaseLLM): prepared_messages[user_message_index]["content"] = [] for attachment in attachments: - # Upload the file to OpenAI - try: - file_id = self._upload_file_to_openai(attachment) - - prepared_messages[user_message_index]["content"].append({ - "type": "file", - "file": {"file_id": file_id} - }) - except Exception as e: - import logging - logging.error(f"Error uploading attachment to OpenAI: {e}") - if 'content' in attachment: + mime_type = attachment.get('mime_type') + if not mime_type: + file_path = attachment.get('path') + if file_path: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + if mime_type and mime_type.startswith('image/'): + try: + base64_image = self._get_base64_image(attachment) prepared_messages[user_message_index]["content"].append({ - "type": "text", - "text": f"File content:\n\n{attachment['content']}" + "type": "image_url", + "image_url": { + "url": f"data:{mime_type};base64,{base64_image}" + } }) + except Exception as e: + logging.error(f"Error processing image attachment: {e}") + if 'content' in attachment: + prepared_messages[user_message_index]["content"].append({ + "type": "text", + "text": f"[Image could not be processed: {attachment.get('path', 'unknown')}]" + }) + # Handle PDFs using the file API + elif mime_type == 'application/pdf': + try: + file_id = self._upload_file_to_openai(attachment) + + prepared_messages[user_message_index]["content"].append({ + "type": "file", + "file": {"file_id": file_id} + }) + except Exception as e: + logging.error(f"Error uploading PDF to OpenAI: {e}") + if 'content' in attachment: + prepared_messages[user_message_index]["content"].append({ + "type": "text", + "text": f"File content:\n\n{attachment['content']}" + }) return prepared_messages - def _upload_file_to_openai(self, attachment): + def _get_base64_image(self, attachment): + """ + Convert an image file to base64 encoding. + + Args: + attachment (dict): Attachment dictionary with path and metadata. + + Returns: + str: Base64-encoded image data. + """ + file_path = attachment.get('path') + if not file_path: + raise ValueError("No file path provided in attachment") + + if not os.path.isabs(file_path): + current_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + file_path = os.path.join(current_dir, "application", file_path) + + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + with open(file_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode('utf-8') + + def _upload_file_to_openai(self, attachment): ##pdfs """ Upload a file to OpenAI and return the file_id. @@ -212,9 +280,8 @@ class OpenAILLM(BaseLLM): str: OpenAI file_id for the uploaded file. """ import os - import mimetypes + import logging - # Check if we already have the file_id cached if 'openai_file_id' in attachment: return attachment['openai_file_id'] @@ -222,7 +289,6 @@ class OpenAILLM(BaseLLM): if not file_path: raise ValueError("No file path provided in attachment") - # Make path absolute if it's relative if not os.path.isabs(file_path): current_dir = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -231,16 +297,7 @@ class OpenAILLM(BaseLLM): if not os.path.exists(file_path): raise FileNotFoundError(f"File not found: {file_path}") - - mime_type = attachment.get('mime_type') - if not mime_type: - mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' - - supported_mime_types = ['application/pdf', 'image/png', 'image/jpeg', 'image/gif'] - if mime_type not in supported_mime_types: - import logging - logging.warning(f"MIME type {mime_type} not supported by OpenAI for file uploads. Falling back to text.") - raise ValueError(f"Unsupported MIME type: {mime_type}") + try: with open(file_path, 'rb') as file: @@ -263,7 +320,6 @@ class OpenAILLM(BaseLLM): return file_id except Exception as e: - import logging logging.error(f"Error uploading file to OpenAI: {e}") raise From cd7bbb45c3bd012d7b7c59657164a5242fda8bd9 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Tue, 8 Apr 2025 17:51:18 +0530 Subject: [PATCH 14/26] (fix:popups) minor hover --- frontend/src/components/SourcesPopup.tsx | 2 +- frontend/src/components/ToolsPopup.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/SourcesPopup.tsx b/frontend/src/components/SourcesPopup.tsx index 9f8ce8ce..088b3eb1 100644 --- a/frontend/src/components/SourcesPopup.tsx +++ b/frontend/src/components/SourcesPopup.tsx @@ -207,7 +207,7 @@ export default function SourcesPopup({ )} -
+ ); -} \ No newline at end of file +} From 02f8132f3aa9af371154bdc0a345a92aa9998fd6 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Apr 2025 15:56:34 +0300 Subject: [PATCH 15/26] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e7d7fbd1..598cfa1d 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,11 @@ - [x] Manually updating chunks in the app UI (Feb 2025) - [x] Devcontainer for easy development (Feb 2025) - [x] ReACT agent (March 2025) -- [ ] Anthropic Tool compatibility -- [ ] New input box in the conversation menu -- [ ] Add triggerable actions / tools (webhook) +- [ ] Chatbots menu re-design to handle tools, agent types, and more (April 2025) +- [ ] New input box in the conversation menu (April 2025) +- [ ] Anthropic Tool compatibility (April 2025) +- [ ] Add triggerable actions / tools (webhook) (April 2025) - [ ] Add OAuth 2.0 authentication for tools and sources -- [ ] Chatbots menu re-design to handle tools, agent types, and more - [ ] Agent scheduling You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT! From dd9ea46e58c2de59ebf8587040a04d3205df7f4d Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 10 Apr 2025 01:29:01 +0530 Subject: [PATCH 16/26] (feat:attach) strategy specific to google genai --- application/llm/google_ai.py | 148 ++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/application/llm/google_ai.py b/application/llm/google_ai.py index 5e33550c..180ed43b 100644 --- a/application/llm/google_ai.py +++ b/application/llm/google_ai.py @@ -1,5 +1,8 @@ from google import genai from google.genai import types +import os +import logging +import mimetypes from application.llm.base import BaseLLM @@ -9,6 +12,141 @@ class GoogleLLM(BaseLLM): super().__init__(*args, **kwargs) self.api_key = api_key self.user_api_key = user_api_key + self.client = genai.Client(api_key=self.api_key) + + def get_supported_attachment_types(self): + """ + Return a list of MIME types supported by Google Gemini for file uploads. + + Returns: + list: List of supported MIME types + """ + return [ + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/webp', + 'image/gif' + ] + + def prepare_messages_with_attachments(self, messages, attachments=None): + """ + Process attachments using Google AI's file API for more efficient handling. + + Args: + messages (list): List of message dictionaries. + attachments (list): List of attachment dictionaries with content and metadata. + + Returns: + list: Messages formatted with file references for Google AI API. + """ + + if not attachments: + return messages + + prepared_messages = messages.copy() + + # Find the user message to attach files to the last one + user_message_index = None + for i in range(len(prepared_messages) - 1, -1, -1): + if prepared_messages[i].get("role") == "user": + user_message_index = i + break + + + if user_message_index is None: + user_message = {"role": "user", "content": []} + prepared_messages.append(user_message) + user_message_index = len(prepared_messages) - 1 + + if isinstance(prepared_messages[user_message_index].get("content"), str): + text_content = prepared_messages[user_message_index]["content"] + prepared_messages[user_message_index]["content"] = [ + {"type": "text", "text": text_content} + ] + elif not isinstance(prepared_messages[user_message_index].get("content"), list): + prepared_messages[user_message_index]["content"] = [] + + file_uris = [] + for attachment in attachments: + mime_type = attachment.get('mime_type') + if not mime_type: + file_path = attachment.get('path') + if file_path: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + if mime_type in self.get_supported_attachment_types(): + try: + file_uri = self._upload_file_to_google(attachment) + logging.info(f"GoogleLLM: Successfully uploaded file, got URI: {file_uri}") + file_uris.append((file_uri, mime_type)) + except Exception as e: + logging.error(f"GoogleLLM: Error uploading file: {e}") + if 'content' in attachment: + prepared_messages[user_message_index]["content"].append({ + "type": "text", + "text": f"[File could not be processed: {attachment.get('path', 'unknown')}]" + }) + + if file_uris: + logging.info(f"GoogleLLM: Adding {len(file_uris)} file URIs to message") + prepared_messages[user_message_index]["content"].append({ + "type": "file_uris", + "file_uris": file_uris + }) + + return prepared_messages + + def _upload_file_to_google(self, attachment): + """ + Upload a file to Google AI and return the file URI. + + Args: + attachment (dict): Attachment dictionary with path and metadata. + + Returns: + str: Google AI file URI for the uploaded file. + """ + if 'google_file_uri' in attachment: + return attachment['google_file_uri'] + + file_path = attachment.get('path') + if not file_path: + raise ValueError("No file path provided in attachment") + + if not os.path.isabs(file_path): + current_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + file_path = os.path.join(current_dir, "application", file_path) + + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + mime_type = attachment.get('mime_type') + if not mime_type: + mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' + + try: + response = self.client.files.upload(file=file_path) + + file_uri = response.uri + + from application.core.mongo_db import MongoDB + mongo = MongoDB.get_client() + db = mongo["docsgpt"] + attachments_collection = db["attachments"] + if '_id' in attachment: + attachments_collection.update_one( + {"_id": attachment['_id']}, + {"$set": {"google_file_uri": file_uri}} + ) + + return file_uri + except Exception as e: + logging.error(f"Error uploading file to Google AI: {e}") + raise def _clean_messages_google(self, messages): cleaned_messages = [] @@ -26,7 +164,7 @@ class GoogleLLM(BaseLLM): elif isinstance(content, list): for item in content: if "text" in item: - parts.append(types.Part.from_text(item["text"])) + parts.append(types.Part.from_text(text=item["text"])) elif "function_call" in item: parts.append( types.Part.from_function_call( @@ -41,6 +179,14 @@ class GoogleLLM(BaseLLM): response=item["function_response"]["response"], ) ) + elif "type" in item and item["type"] == "file_uris": + for file_uri, mime_type in item["file_uris"]: + parts.append( + types.Part.from_uri( + file_uri=file_uri, + mime_type=mime_type + ) + ) else: raise ValueError( f"Unexpected content dictionary format:{item}" From 6cb4577e1b79522dce99dd85b392acb09f26ae7a Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Thu, 10 Apr 2025 01:43:46 +0530 Subject: [PATCH 17/26] (feat:input) hotkey for sources open --- frontend/src/components/MessageInput.tsx | 30 ++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 62e6dea0..612cbaf2 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -4,6 +4,7 @@ import { useDarkTheme } from '../hooks'; import { useSelector, useDispatch } from 'react-redux'; import userService from '../api/services/userService'; import endpoints from '../api/endpoints'; +import { getOS, isTouchDevice } from '../utils/browserUtils'; import PaperPlane from '../assets/paper_plane.svg'; import SourceIcon from '../assets/source.svg'; import ToolIcon from '../assets/tool.svg'; @@ -57,6 +58,26 @@ export default function MessageInput({ const dispatch = useDispatch(); + const browserOS = getOS(); + const isTouch = isTouchDevice(); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + ((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') || + (browserOS === 'mac' && event.metaKey && event.key === 'k') + ) { + event.preventDefault(); + setIsSourcesPopupOpen(!isSourcesPopupOpen); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [browserOS]); + const handleFileAttachment = (e: React.ChangeEvent) => { if (!e.target.files || e.target.files.length === 0) return; @@ -333,15 +354,20 @@ export default function MessageInput({
)} - {upload.status === 'failed' && ( + {attachment.status === 'failed' && ( Upload failed )} -{(upload.status === 'uploading' || upload.status === 'processing') && ( -
- - {/* Background circle */} - - - -
-)} + {(attachment.status === 'uploading' || attachment.status === 'processing') && ( +
+ + {/* Background circle */} + + + +
+ )}
))}
diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index ec244087..a241b2d3 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -57,7 +57,6 @@ const ConversationBubble = forwardRef< updated?: boolean, index?: number, ) => void; - attachments?: { fileName: string; id: string }[]; } >(function ConversationBubble( { @@ -72,7 +71,6 @@ const ConversationBubble = forwardRef< retryBtn, questionNumber, handleUpdatedQuestionSubmission, - attachments, }, ref, ) { @@ -99,36 +97,6 @@ const ConversationBubble = forwardRef< handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber); }; let bubble; - const renderAttachments = () => { - if (!attachments || attachments.length === 0) return null; - - return ( -
- {attachments.map((attachment, index) => ( -
- - - - {attachment.fileName} -
- ))} -
- ); - }; if (type === 'QUESTION') { bubble = (
{message}
- {renderAttachments()} )} - + {attachment.status === 'failed' && ( )} - + {(attachment.status === 'uploading' || attachment.status === 'processing') && (
@@ -328,6 +329,7 @@ export default function MessageInput({ ref={sourceButtonRef} className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[130px] sm:max-w-[150px]" onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)} + title={selectedDocs ? selectedDocs.name : t('conversation.sources.title')} > Sources From 02934452d693c1cf412f5a13e3a80a77bd9e84ae Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Apr 2025 23:48:17 +0100 Subject: [PATCH 25/26] fix: remove comment --- application/agents/llm_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/application/agents/llm_handler.py b/application/agents/llm_handler.py index b9e7ba37..7fe794f8 100644 --- a/application/agents/llm_handler.py +++ b/application/agents/llm_handler.py @@ -164,7 +164,6 @@ class OpenAILLMHandler(LLMHandler): while True: tool_calls = {} for chunk in resp: - logger.info(f"Chunk: {chunk}") if isinstance(chunk, str) and len(chunk) > 0: yield chunk continue From ad610d2f90e682ad9c8bdefdd955b6270f92c135 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 14 Apr 2025 23:49:40 +0100 Subject: [PATCH 26/26] fix: lint ruff --- application/llm/google_ai.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/application/llm/google_ai.py b/application/llm/google_ai.py index 8ca5a4a0..c049eaa2 100644 --- a/application/llm/google_ai.py +++ b/application/llm/google_ai.py @@ -308,8 +308,6 @@ class GoogleLLM(BaseLLM): config=config, ) - # Track if we've seen any function calls - function_call_seen = False for chunk in response: if hasattr(chunk, "candidates") and chunk.candidates: @@ -317,7 +315,6 @@ class GoogleLLM(BaseLLM): if candidate.content and candidate.content.parts: for part in candidate.content.parts: if part.function_call: - function_call_seen = True yield part elif part.text: yield part.text