feat: update all OpenAI/ChatGPT refs + settings dropdown for fixed-value fields

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-23 20:06:57 +00:00
parent dcdef44303
commit 044da0ff47
17 changed files with 383 additions and 113 deletions
+33 -5
View File
@@ -119,16 +119,44 @@ AUTHENTIK_CONFIG_URL=<ConfigUrlOfYourApp, e.g. https://authentik.example.com/app
OAUTH_PROVIDER_NAME="Authentik SSO" OAUTH_PROVIDER_NAME="Authentik SSO"
# **AI/ML Services** # **AI/ML Services**
# OpenAI # Select your AI provider: openai | azure | anthropic | gemini | ollama | openrouter | portkey | litellm
AI_PROVIDER=openai
# Model override (optional falls back to OPENAI_MODEL when not set)
# AI_MODEL=gpt-4o-mini
# --- OpenAI (AI_PROVIDER=openai) ---
OPENAI_API_KEY="<OPENAI_API_KEY>" OPENAI_API_KEY="<OPENAI_API_KEY>"
OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o-mini OPENAI_MODEL=gpt-4o-mini
# Azure AI # --- Anthropic Claude (AI_PROVIDER=anthropic) ---
AZURE_REGION="eastus" # ANTHROPIC_API_KEY=sk-ant-...
AZURE_ENDPOINT="https://<yourendpoint>.cognitiveservices.azure.com/" # AI_MODEL=claude-3-5-sonnet-20241022
AZURE_AI_KEY=<AZURE_AI_KEY>
# --- Google Gemini (AI_PROVIDER=gemini) ---
# GEMINI_API_KEY=AIza...
# AI_MODEL=gemini-1.5-pro
# --- Ollama local LLMs (AI_PROVIDER=ollama) ---
# OLLAMA_BASE_URL=http://localhost:11434
# AI_MODEL=llama3.2
# --- OpenRouter (AI_PROVIDER=openrouter) ---
# OPENROUTER_API_KEY=sk-or-...
# AI_MODEL=anthropic/claude-3.5-sonnet
# --- Portkey AI Gateway (AI_PROVIDER=portkey) ---
# PORTKEY_API_KEY=pk-...
# PORTKEY_VIRTUAL_KEY=vk-... # optional routes to provider credentials in Portkey vault
# PORTKEY_CONFIG=pc-... # optional saved Config ID for fallbacks / load balancing
# --- Azure OpenAI (AI_PROVIDER=azure) ---
# OPENAI_API_KEY=<azure-key>
# OPENAI_BASE_URL=https://my-resource.openai.azure.com
# AZURE_OPENAI_API_VERSION=2024-02-01
# AI_MODEL=gpt-4o # deployment name in Azure
# Azure Document Intelligence (OCR separate from AI provider above)
# **Email Settings** # **Email Settings**
EMAIL_HOST=smtp.example.com EMAIL_HOST=smtp.example.com
EMAIL_PORT=587 EMAIL_PORT=587
+6 -5
View File
@@ -31,7 +31,7 @@
DocuElevate automates the handling, extraction, and processing of documents using a variety of services, including: DocuElevate automates the handling, extraction, and processing of documents using a variety of services, including:
- **OpenAI** for metadata extraction and text refinement. - **AI Provider** (pluggable OpenAI, Anthropic, Gemini, Ollama, OpenRouter, Portkey, and more) for metadata extraction and text refinement.
- **Dropbox**, **Nextcloud**, and **Google Drive** for file storage and uploads. - **Dropbox**, **Nextcloud**, and **Google Drive** for file storage and uploads.
- **Paperless NGX** for document indexing and management. - **Paperless NGX** for document indexing and management.
- **Azure Document Intelligence** for OCR on PDFs. - **Azure Document Intelligence** for OCR on PDFs.
@@ -86,7 +86,7 @@ Documents enter DocuElevate through four possible channels:
Every document goes through the following steps: Every document goes through the following steps:
1. **PDF Conversion**: Non-PDF files are converted to PDF format using Gotenberg 1. **PDF Conversion**: Non-PDF files are converted to PDF format using Gotenberg
2. **OCR Processing**: Azure Document Intelligence extracts text from images/scans 2. **OCR Processing**: Azure Document Intelligence extracts text from images/scans
3. **Metadata Extraction**: OpenAI analyzes document content to identify: 3. **Metadata Extraction**: The configured AI provider analyzes document content to identify:
- Document type (invoice, receipt, contract, etc.) - Document type (invoice, receipt, contract, etc.)
- Key entities (dates, names, amounts, account numbers) - Key entities (dates, names, amounts, account numbers)
- Important data points specific to the document type - Important data points specific to the document type
@@ -116,8 +116,8 @@ Users can choose to send documents to any combination of these destinations thro
- Manual uploads (via API or UI) to Dropbox, Nextcloud, Google Drive, or Paperless - Manual uploads (via API or UI) to Dropbox, Nextcloud, Google Drive, or Paperless
- **OCR Processing (Azure)**: - **OCR Processing (Azure)**:
- Extract text from scanned PDFs using Azure Document Intelligence - Extract text from scanned PDFs using Azure Document Intelligence
- **Metadata Extraction (OpenAI)**: - **Metadata Extraction (AI Provider)**:
- Use GPT to classify, label, or otherwise enrich the text with structured metadata - Use any supported AI provider (OpenAI, Anthropic, Gemini, Ollama, etc.) to classify, label, or otherwise enrich the text with structured metadata
- **PDF Conversion (Gotenberg)**: - **PDF Conversion (Gotenberg)**:
- Convert non-PDF attachments (e.g., Word docs, images) into PDFs - Convert non-PDF attachments (e.g., Word docs, images) into PDFs
- **Document Management (Paperless NGX)**: - **Document Management (Paperless NGX)**:
@@ -214,7 +214,8 @@ The following is a summary of the licenses used by our direct dependencies:
| Uvicorn | BSD | | Uvicorn | BSD |
| SQLAlchemy | MIT | | SQLAlchemy | MIT |
| Pydantic | MIT | | Pydantic | MIT |
| OpenAI | MIT | | openai | MIT |
| litellm | MIT |
| pypdf | BSD | | pypdf | BSD |
| Requests | Apache 2.0 | | Requests | Apache 2.0 |
| puremagic | MIT | | puremagic | MIT |
+57 -1
View File
@@ -1,5 +1,9 @@
""" """
OpenAI API endpoints AI provider and OpenAI API endpoints.
Exposes two endpoints:
- GET /api/ai/test tests the currently configured AI provider (generic, provider-agnostic)
- GET /api/openai/test backward-compatible alias that tests the OpenAI API specifically
""" """
import logging import logging
@@ -146,3 +150,55 @@ async def test_openai_connection(request: Request):
except Exception as e: except Exception as e:
logger.exception("Unexpected error testing OpenAI connection") logger.exception("Unexpected error testing OpenAI connection")
return {"status": "error", "message": f"Unexpected error: {str(e)}"} return {"status": "error", "message": f"Unexpected error: {str(e)}"}
@router.get("/ai/test")
@require_login
async def test_ai_provider_connection(request: Request):
"""
Test the currently configured AI provider connection.
Uses ``get_ai_provider()`` to instantiate the active provider and sends a
minimal chat completion to verify that the credentials and endpoint are
reachable. Works for all supported providers (OpenAI, Azure, Anthropic,
Gemini, Ollama, OpenRouter, Portkey, LiteLLM).
"""
from app.utils.ai_provider import get_ai_provider
provider_name = settings.ai_provider
model = settings.ai_model or settings.openai_model
logger.info(f"Testing AI provider connection: provider={provider_name}, model={model}")
try:
provider = get_ai_provider()
response = provider.chat_completion(
messages=[{"role": "user", "content": "Reply with the single word: ok"}],
model=model,
temperature=0,
max_tokens=5,
)
logger.info(f"AI provider test successful: provider={provider_name}")
return {
"status": "success",
"message": f"AI provider '{provider_name}' is reachable and responding",
"provider": provider_name,
"model": model,
"response_preview": (response or "")[:50],
}
except ValueError as e:
# Configuration errors (missing keys, unknown provider)
logger.warning(f"AI provider configuration error: {e}")
return {
"status": "error",
"message": str(e),
"provider": provider_name,
}
except Exception as e:
detail = _get_exception_chain_detail(e)
logger.error(f"AI provider test failed for '{provider_name}': {detail}", exc_info=True)
return {
"status": "error",
"message": f"Connection failed: {detail}",
"provider": provider_name,
}
+15 -6
View File
@@ -11,7 +11,7 @@ from app.api.google_drive import test_google_drive_token
from app.api.onedrive import test_onedrive_token from app.api.onedrive import test_onedrive_token
# Import the test functions from API routes # Import the test functions from API routes
from app.api.openai import test_openai_connection from app.api.openai import test_ai_provider_connection, test_openai_connection
from app.celery_app import celery from app.celery_app import celery
from app.config import settings from app.config import settings
@@ -75,8 +75,17 @@ def unwrap_decorated_function(func):
# Create synchronous versions of the test functions that bypass authentication # Create synchronous versions of the test functions that bypass authentication
def sync_test_ai_provider_connection():
"""Synchronous wrapper for the AI provider test function that bypasses auth."""
inner_func = unwrap_decorated_function(test_ai_provider_connection)
request = MockRequest()
if inspect.iscoroutinefunction(inner_func):
return asyncio.run(inner_func(request))
return inner_func(request)
def sync_test_openai_connection(): def sync_test_openai_connection():
"""Synchronous wrapper for the OpenAI test function that bypasses auth""" """Synchronous wrapper for the OpenAI test function that bypasses auth."""
# Get the original function without the @require_login decorator # Get the original function without the @require_login decorator
inner_func = unwrap_decorated_function(test_openai_connection) inner_func = unwrap_decorated_function(test_openai_connection)
request = MockRequest() request = MockRequest()
@@ -139,10 +148,10 @@ def check_credentials():
# Define services with their test functions and configuration status # Define services with their test functions and configuration status
services = [ services = [
{ {
"name": "OpenAI", "name": "AI Provider",
"check_func": sync_test_openai_connection, "check_func": sync_test_ai_provider_connection,
"configured": provider_status.get("OpenAI", {}).get("configured", False), "configured": provider_status.get("AI Provider", {}).get("configured", False),
"config_issues": [], # OpenAI isn't in storage_configs "config_issues": [],
}, },
{ {
"name": "Azure Document Intelligence", "name": "Azure Document Intelligence",
+62 -13
View File
@@ -57,20 +57,69 @@ def get_provider_status() -> dict[str, dict[str, object]]:
"test_endpoint": "/api/diagnostic/test-notification", "test_endpoint": "/api/diagnostic/test-notification",
} }
# Add AI services first # Add AI provider (dynamic reflects whichever provider is configured)
providers["OpenAI"] = { ai_provider_name = getattr(settings, "ai_provider", "openai").lower()
"name": "OpenAI", model = getattr(settings, "ai_model", None) or getattr(settings, "openai_model", "gpt-4o-mini")
"icon": "fa-brands fa-openai",
"configured": bool( # Determine whether the active provider has its required credentials set
getattr(settings, "openai_api_key", None) and str(getattr(settings, "openai_api_key", "")).startswith("sk-") def _ai_configured() -> bool:
), if ai_provider_name in ("openai", "azure", "litellm"):
return bool(getattr(settings, "openai_api_key", None))
elif ai_provider_name == "anthropic":
return bool(getattr(settings, "anthropic_api_key", None))
elif ai_provider_name == "gemini":
return bool(getattr(settings, "gemini_api_key", None))
elif ai_provider_name == "ollama":
return bool(getattr(settings, "ollama_base_url", None))
elif ai_provider_name == "openrouter":
return bool(getattr(settings, "openrouter_api_key", None))
elif ai_provider_name == "portkey":
return bool(getattr(settings, "portkey_api_key", None))
return False
# Build provider-specific detail rows
def _ai_details() -> dict:
base = {"provider": ai_provider_name, "model": model}
if ai_provider_name in ("openai", "azure", "litellm"):
base["api_key"] = mask_sensitive_value(getattr(settings, "openai_api_key", None))
base["base_url"] = getattr(settings, "openai_base_url", "https://api.openai.com/v1")
elif ai_provider_name == "anthropic":
base["api_key"] = mask_sensitive_value(getattr(settings, "anthropic_api_key", None))
elif ai_provider_name == "gemini":
base["api_key"] = mask_sensitive_value(getattr(settings, "gemini_api_key", None))
elif ai_provider_name == "ollama":
base["base_url"] = getattr(settings, "ollama_base_url", "http://localhost:11434")
elif ai_provider_name == "openrouter":
base["api_key"] = mask_sensitive_value(getattr(settings, "openrouter_api_key", None))
base["base_url"] = getattr(settings, "openrouter_base_url", "https://openrouter.ai/api/v1")
elif ai_provider_name == "portkey":
base["api_key"] = mask_sensitive_value(getattr(settings, "portkey_api_key", None))
if getattr(settings, "portkey_virtual_key", None):
base["virtual_key"] = mask_sensitive_value(settings.portkey_virtual_key)
if getattr(settings, "portkey_config", None):
base["config"] = settings.portkey_config
return base
_PROVIDER_ICONS = {
"openai": "fa-brands fa-openai",
"azure": "fa-brands fa-microsoft",
"anthropic": "fa-solid fa-robot",
"gemini": "fa-brands fa-google",
"ollama": "fa-solid fa-server",
"openrouter": "fa-solid fa-route",
"portkey": "fa-solid fa-key",
"litellm": "fa-solid fa-layer-group",
}
providers["AI Provider"] = {
"name": "AI Provider",
"icon": _PROVIDER_ICONS.get(ai_provider_name, "fa-solid fa-microchip"),
"configured": _ai_configured(),
"enabled": True, "enabled": True,
"description": "AI-powered document analysis and metadata extraction", "description": f"AI-powered metadata extraction and OCR refinement ({ai_provider_name})",
"details": { "details": _ai_details(),
"api_key": mask_sensitive_value(getattr(settings, "openai_api_key", None)), "testable": True,
"base_url": getattr(settings, "openai_base_url", "https://api.openai.com/v1"), "test_endpoint": "/api/ai/test",
"model": getattr(settings, "openai_model", "gpt-4"),
},
} }
providers["Azure AI"] = { providers["Azure AI"] = {
@@ -170,9 +170,21 @@ def get_settings_for_display(show_values: bool = False) -> dict[str, list[dict[s
"s3_acl", "s3_acl",
], ],
"AI Services": [ "AI Services": [
"ai_provider",
"ai_model",
"openai_api_key", "openai_api_key",
"openai_base_url", "openai_base_url",
"openai_model", "openai_model",
"anthropic_api_key",
"gemini_api_key",
"ollama_base_url",
"openrouter_api_key",
"openrouter_base_url",
"portkey_api_key",
"portkey_virtual_key",
"portkey_config",
"portkey_base_url",
"azure_openai_api_version",
"azure_ai_key", "azure_ai_key",
"azure_endpoint", "azure_endpoint",
"azure_region", "azure_region",
+109 -11
View File
@@ -136,15 +136,15 @@ SETTING_METADATA = {
# AI Services # AI Services
"openai_api_key": { "openai_api_key": {
"category": "AI Services", "category": "AI Services",
"description": "OpenAI API key for metadata extraction", "description": "API key for the AI provider (required for OpenAI, Azure, LiteLLM; unused for Ollama)",
"type": "string", "type": "string",
"sensitive": True, "sensitive": True,
"required": True, "required": False,
"restart_required": False, "restart_required": False,
}, },
"openai_base_url": { "openai_base_url": {
"category": "AI Services", "category": "AI Services",
"description": "OpenAI API base URL (default: https://api.openai.com/v1)", "description": "API base URL override for Azure endpoints, local proxies, or LiteLLM (default: https://api.openai.com/v1)",
"type": "string", "type": "string",
"sensitive": False, "sensitive": False,
"required": False, "required": False,
@@ -152,34 +152,132 @@ SETTING_METADATA = {
}, },
"openai_model": { "openai_model": {
"category": "AI Services", "category": "AI Services",
"description": "OpenAI model to use (e.g., gpt-4o-mini)", "description": "Fallback model name used when AI_MODEL is not set (e.g. gpt-4o-mini)",
"type": "string", "type": "string",
"sensitive": False, "sensitive": False,
"required": False, "required": False,
"restart_required": False, "restart_required": False,
}, },
"azure_ai_key": { "ai_provider": {
"category": "AI Services", "category": "AI Services",
"description": "Azure AI key for document intelligence", "description": "Active AI provider for metadata extraction and OCR refinement",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
"options": ["openai", "azure", "anthropic", "gemini", "ollama", "openrouter", "portkey", "litellm"],
},
"ai_model": {
"category": "AI Services",
"description": "Model name for the selected provider (overrides OPENAI_MODEL). E.g. gpt-4o, claude-3-5-sonnet-20241022, gemini-1.5-pro, llama3.2",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
"anthropic_api_key": {
"category": "AI Services",
"description": "Anthropic API key (required when AI_PROVIDER=anthropic)",
"type": "string", "type": "string",
"sensitive": True, "sensitive": True,
"required": True, "required": False,
"restart_required": False,
},
"gemini_api_key": {
"category": "AI Services",
"description": "Google AI Studio API key (required when AI_PROVIDER=gemini)",
"type": "string",
"sensitive": True,
"required": False,
"restart_required": False,
},
"ollama_base_url": {
"category": "AI Services",
"description": "Ollama server URL for local LLM inference (used when AI_PROVIDER=ollama)",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
"openrouter_api_key": {
"category": "AI Services",
"description": "OpenRouter API key (required when AI_PROVIDER=openrouter)",
"type": "string",
"sensitive": True,
"required": False,
"restart_required": False,
},
"openrouter_base_url": {
"category": "AI Services",
"description": "OpenRouter gateway URL (default: https://openrouter.ai/api/v1)",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
"portkey_api_key": {
"category": "AI Services",
"description": "Portkey account API key (required when AI_PROVIDER=portkey)",
"type": "string",
"sensitive": True,
"required": False,
"restart_required": False,
},
"portkey_virtual_key": {
"category": "AI Services",
"description": "Portkey Virtual Key routes to provider credentials stored in the Portkey vault (optional)",
"type": "string",
"sensitive": True,
"required": False,
"restart_required": False,
},
"portkey_config": {
"category": "AI Services",
"description": "Portkey Config ID for advanced routing, fallbacks, and load balancing (optional, e.g. pc-my-config-abc123)",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
"portkey_base_url": {
"category": "AI Services",
"description": "Portkey gateway URL (default: https://api.portkey.ai/v1; override for self-hosted deployments)",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
"azure_openai_api_version": {
"category": "AI Services",
"description": "Azure OpenAI API version string (used when AI_PROVIDER=azure, default: 2024-02-01)",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
# Azure Document Intelligence (OCR) separate from the AI provider above
"azure_ai_key": {
"category": "AI Services",
"description": "Azure Document Intelligence API key for OCR processing",
"type": "string",
"sensitive": True,
"required": False,
"restart_required": False, "restart_required": False,
}, },
"azure_region": { "azure_region": {
"category": "AI Services", "category": "AI Services",
"description": "Azure region for AI services", "description": "Azure region for Document Intelligence services (e.g., eastus)",
"type": "string", "type": "string",
"sensitive": False, "sensitive": False,
"required": True, "required": False,
"restart_required": False, "restart_required": False,
}, },
"azure_endpoint": { "azure_endpoint": {
"category": "AI Services", "category": "AI Services",
"description": "Azure AI endpoint URL", "description": "Azure Document Intelligence endpoint URL",
"type": "string", "type": "string",
"sensitive": False, "sensitive": False,
"required": True, "required": False,
"restart_required": False, "restart_required": False,
}, },
# Storage Providers - Dropbox # Storage Providers - Dropbox
+17 -28
View File
@@ -90,10 +90,21 @@ def get_required_settings() -> List[Dict[str, Any]]:
"wizard_step": 2, "wizard_step": 2,
"wizard_category": "Security", "wizard_category": "Security",
}, },
{
"key": "ai_provider",
"label": "AI Provider",
"description": "AI provider for metadata extraction and OCR refinement (openai, azure, anthropic, gemini, ollama, openrouter, portkey, litellm)",
"type": "string",
"sensitive": False,
"default": "openai",
"options": ["openai", "azure", "anthropic", "gemini", "ollama", "openrouter", "portkey", "litellm"],
"wizard_step": 3,
"wizard_category": "AI Services",
},
{ {
"key": "openai_api_key", "key": "openai_api_key",
"label": "OpenAI API Key", "label": "API Key (OpenAI / Azure / LiteLLM)",
"description": "API key for OpenAI services (metadata extraction)", "description": "API key for OpenAI, Azure OpenAI, or LiteLLM (not required for Ollama)",
"type": "string", "type": "string",
"sensitive": True, "sensitive": True,
"default": None, "default": None,
@@ -101,32 +112,12 @@ def get_required_settings() -> List[Dict[str, Any]]:
"wizard_category": "AI Services", "wizard_category": "AI Services",
}, },
{ {
"key": "azure_ai_key", "key": "openai_model",
"label": "Azure AI Key", "label": "Default Model",
"description": "Azure AI key for document intelligence (OCR)", "description": "Model name used when AI_MODEL is not set (e.g. gpt-4o-mini, claude-3-5-sonnet-20241022, llama3.2)",
"type": "string",
"sensitive": True,
"default": None,
"wizard_step": 3,
"wizard_category": "AI Services",
},
{
"key": "azure_region",
"label": "Azure Region",
"description": "Azure region for AI services (e.g., eastus)",
"type": "string", "type": "string",
"sensitive": False, "sensitive": False,
"default": "eastus", "default": "gpt-4o-mini",
"wizard_step": 3,
"wizard_category": "AI Services",
},
{
"key": "azure_endpoint",
"label": "Azure Endpoint",
"description": "Azure AI endpoint URL",
"type": "string",
"sensitive": False,
"default": None,
"wizard_step": 3, "wizard_step": 3,
"wizard_category": "AI Services", "wizard_category": "AI Services",
}, },
@@ -147,8 +138,6 @@ def is_setup_required() -> bool:
critical_settings = [ critical_settings = [
("session_secret", ["INSECURE_DEFAULT_FOR_DEVELOPMENT_ONLY_DO_NOT_USE_IN_PRODUCTION_MINIMUM_32_CHARS"]), ("session_secret", ["INSECURE_DEFAULT_FOR_DEVELOPMENT_ONLY_DO_NOT_USE_IN_PRODUCTION_MINIMUM_32_CHARS"]),
("admin_password", [None, "", "your_secure_password", "changeme", "admin"]), ("admin_password", [None, "", "your_secure_password", "changeme", "admin"]),
("openai_api_key", [None, "", "<OPENAI_API_KEY>", "test-key"]),
("azure_ai_key", [None, "", "<AZURE_AI_KEY>", "test-key"]),
] ]
for setting_key, invalid_values in critical_settings: for setting_key, invalid_values in critical_settings:
+1 -1
View File
@@ -6,7 +6,7 @@ This guide provides instructions for deploying DocuElevate in various environmen
- Docker and Docker Compose - Docker and Docker Compose
- Access to required external services (if configured): - Access to required external services (if configured):
- OpenAI API - AI provider API key (OpenAI, Anthropic, Gemini, or other configured provider)
- Azure Document Intelligence - Azure Document Intelligence
- Dropbox API - Dropbox API
- Nextcloud instance - Nextcloud instance
+3 -2
View File
@@ -26,7 +26,7 @@ Settings are organized into logical categories for easy navigation:
- **Core**: Database, Redis, working directory, external hostname, debug mode - **Core**: Database, Redis, working directory, external hostname, debug mode
- **Authentication**: Login settings, session secrets, OAuth configuration - **Authentication**: Login settings, session secrets, OAuth configuration
- **AI Services**: OpenAI and Azure AI configuration - **AI Services**: AI provider selection and credentials (OpenAI, Azure, Anthropic, Gemini, Ollama, OpenRouter, Portkey, LiteLLM)
- **Storage Providers**: Dropbox, Google Drive, OneDrive, S3, FTP, SFTP, WebDAV, Nextcloud, Paperless - **Storage Providers**: Dropbox, Google Drive, OneDrive, S3, FTP, SFTP, WebDAV, Nextcloud, Paperless
- **Email**: SMTP configuration for sending emails - **Email**: SMTP configuration for sending emails
- **IMAP**: Email ingestion configuration (supports multiple accounts) - **IMAP**: Email ingestion configuration (supports multiple accounts)
@@ -152,7 +152,8 @@ Removes a setting from the database (reverts to environment variable or default)
POST /api/settings/bulk-update POST /api/settings/bulk-update
[ [
{"key": "debug", "value": "true"}, {"key": "debug", "value": "true"},
{"key": "openai_model", "value": "gpt-4"} {"key": "ai_provider", "value": "anthropic"},
{"key": "openai_model", "value": "claude-3-5-sonnet-20241022"}
] ]
``` ```
+1 -1
View File
@@ -150,7 +150,7 @@ View the complete processing history with a timeline showing:
- Timestamps for each operation - Timestamps for each operation
**Retry Processing**: If a file's processing has failed, you can use the "Retry Processing" button to reprocess the entire file. This is useful when: **Retry Processing**: If a file's processing has failed, you can use the "Retry Processing" button to reprocess the entire file. This is useful when:
- External API services (like OpenAI) had temporary issues - External API services (like the configured AI provider) had temporary issues
- Network connectivity was lost during processing - Network connectivity was lost during processing
- Configuration has been updated and you want to reprocess with new settings - Configuration has been updated and you want to reprocess with new settings
+3 -2
View File
@@ -18,7 +18,8 @@
for everyone, whether you're a small startup or a large enterprise. for everyone, whether you're a small startup or a large enterprise.
</p> </p>
<p class="text-gray-600"> <p class="text-gray-600">
We harness the power of OpenAI for metadata extraction and text refinement, integrate seamlessly We harness the power of pluggable AI providers (OpenAI, Anthropic Claude, Google Gemini, Ollama,
OpenRouter, Portkey, and more) for metadata extraction and text refinement, integrate seamlessly
with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document
Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions. Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.
</p> </p>
@@ -33,7 +34,7 @@
<ul class="list-disc list-inside text-gray-600 ml-2"> <ul class="list-disc list-inside text-gray-600 ml-2">
<li>Simple and secure file uploads with drag & drop support</li> <li>Simple and secure file uploads with drag & drop support</li>
<li>OCR powered by Azure Document Intelligence</li> <li>OCR powered by Azure Document Intelligence</li>
<li>Automated metadata extraction using OpenAI</li> <li>Automated metadata extraction using a pluggable AI provider</li>
<li>PDF conversion for various file formats via Gotenberg</li> <li>PDF conversion for various file formats via Gotenberg</li>
<li>Intelligent document classification and date extraction</li> <li>Intelligent document classification and date extraction</li>
</ul> </ul>
+14 -1
View File
@@ -139,8 +139,21 @@
Enable {{ setting.key.replace('_', ' ').title() }} Enable {{ setting.key.replace('_', ' ').title() }}
</label> </label>
</div> </div>
{% elif setting.metadata.options %}
<!-- Dropdown Select for fields with a fixed list of values -->
<select
id="{{ setting.key }}"
name="{{ setting.key }}"
x-model="formData['{{ setting.key }}']"
class="setting-input w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option value="">— select —</option>
{% for opt in setting.metadata.options %}
<option value="{{ opt }}">{{ opt }}</option>
{% endfor %}
</select>
{% else %} {% else %}
<!-- Text Input --> <!-- Text / Password Input -->
<div class="relative"> <div class="relative">
{% if setting.metadata.sensitive %} {% if setting.metadata.sensitive %}
<!-- Sensitive Field with Show/Hide Toggle --> <!-- Sensitive Field with Show/Hide Toggle -->
+15 -3
View File
@@ -92,7 +92,7 @@
<div class="border-b border-gray-200 pb-6 last:border-b-0"> <div class="border-b border-gray-200 pb-6 last:border-b-0">
<label for="{{ setting.key }}" class="block text-sm font-medium text-gray-900 mb-1"> <label for="{{ setting.key }}" class="block text-sm font-medium text-gray-900 mb-1">
{{ setting.label }} {{ setting.label }}
{% if setting.default is none or setting.key in ['admin_password', 'openai_api_key', 'azure_ai_key', 'azure_endpoint'] %} {% if setting.default is none or setting.key in ['admin_password'] %}
<span class="text-red-600">*</span> <span class="text-red-600">*</span>
{% endif %} {% endif %}
</label> </label>
@@ -129,7 +129,18 @@
/> />
</div> </div>
{% else %} {% else %}
<!-- Regular input --> <!-- Regular input or select -->
{% if setting.options %}
<select
id="{{ setting.key }}"
name="{{ setting.key }}"
class="wizard-input w-full px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
>
{% for opt in setting.options %}
<option value="{{ opt }}" {% if setting.current_value == opt %}selected{% elif not setting.current_value and opt == setting.default %}selected{% endif %}>{{ opt }}</option>
{% endfor %}
</select>
{% else %}
<input <input
type="{% if setting.sensitive %}password{% else %}text{% endif %}" type="{% if setting.sensitive %}password{% else %}text{% endif %}"
id="{{ setting.key }}" id="{{ setting.key }}"
@@ -137,9 +148,10 @@
value="{{ setting.current_value if setting.current_value else '' }}" value="{{ setting.current_value if setting.current_value else '' }}"
class="wizard-input w-full px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" class="wizard-input w-full px-4 py-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="{{ setting.description }}" placeholder="{{ setting.description }}"
{% if setting.default is none or setting.key in ['admin_password', 'openai_api_key', 'azure_ai_key', 'azure_endpoint'] %}required{% endif %} {% if setting.default is none or setting.key in ['admin_password'] %}required{% endif %}
/> />
{% endif %} {% endif %}
{% endif %}
{% if setting.value_source == 'db' %} {% if setting.value_source == 'db' %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 mt-1">DB</span> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 mt-1">DB</span>
+4 -4
View File
@@ -174,11 +174,11 @@
Configure Now Configure Now
</a> </a>
{% endif %} {% endif %}
{% elif name == "OpenAI" %} {% elif name == "AI Provider" %}
{% if provider.configured %} {% if provider.configured %}
<button <button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="openai"> data-provider="ai_provider">
Test Connection Test Connection
</button> </button>
{% endif %} {% endif %}
@@ -521,8 +521,8 @@ document.addEventListener('DOMContentLoaded', function() {
endpoint = '/api/onedrive/test-token'; endpoint = '/api/onedrive/test-token';
} else if (provider === 'google_drive') { } else if (provider === 'google_drive') {
endpoint = '/api/google-drive/test-token'; endpoint = '/api/google-drive/test-token';
} else if (provider === 'openai') { } else if (provider === 'ai_provider') {
endpoint = '/api/openai/test'; endpoint = '/api/ai/test';
} else if (provider === 'azure') { } else if (provider === 'azure') {
endpoint = '/api/azure/test'; endpoint = '/api/azure/test';
} }
+30 -29
View File
@@ -12,6 +12,7 @@ from app.tasks.check_credentials import (
get_failure_state, get_failure_state,
save_failure_state, save_failure_state,
sync_test_azure_connection, sync_test_azure_connection,
sync_test_ai_provider_connection,
sync_test_openai_connection, sync_test_openai_connection,
unwrap_decorated_function, unwrap_decorated_function,
) )
@@ -205,7 +206,7 @@ class TestCheckCredentialsTask:
@patch("app.tasks.check_credentials.get_failure_state") @patch("app.tasks.check_credentials.get_failure_state")
@patch("app.tasks.check_credentials.get_provider_status") @patch("app.tasks.check_credentials.get_provider_status")
@patch("app.tasks.check_credentials.validate_storage_configs") @patch("app.tasks.check_credentials.validate_storage_configs")
@patch("app.tasks.check_credentials.sync_test_openai_connection") @patch("app.tasks.check_credentials.sync_test_ai_provider_connection")
@patch("app.tasks.check_credentials.sync_test_azure_connection") @patch("app.tasks.check_credentials.sync_test_azure_connection")
@patch("app.tasks.check_credentials.sync_test_dropbox_token") @patch("app.tasks.check_credentials.sync_test_dropbox_token")
@patch("app.tasks.check_credentials.sync_test_google_drive_token") @patch("app.tasks.check_credentials.sync_test_google_drive_token")
@@ -216,7 +217,7 @@ class TestCheckCredentialsTask:
mock_gdrive, mock_gdrive,
mock_dropbox, mock_dropbox,
mock_azure, mock_azure,
mock_openai, mock_ai_provider,
mock_storage_configs, mock_storage_configs,
mock_provider_status, mock_provider_status,
mock_get_state, mock_get_state,
@@ -225,7 +226,7 @@ class TestCheckCredentialsTask:
"""Test checks all configured services.""" """Test checks all configured services."""
mock_get_state.return_value = {} mock_get_state.return_value = {}
mock_provider_status.return_value = { mock_provider_status.return_value = {
"OpenAI": {"configured": True}, "AI Provider": {"configured": True},
"Azure AI": {"configured": True}, "Azure AI": {"configured": True},
"Dropbox": {"configured": True}, "Dropbox": {"configured": True},
"Google Drive": {"configured": True}, "Google Drive": {"configured": True},
@@ -234,7 +235,7 @@ class TestCheckCredentialsTask:
mock_storage_configs.return_value = {"dropbox": [], "google_drive": [], "onedrive": []} mock_storage_configs.return_value = {"dropbox": [], "google_drive": [], "onedrive": []}
# All tests succeed # All tests succeed
mock_openai.return_value = {"status": "success"} mock_ai_provider.return_value = {"status": "success"}
mock_azure.return_value = {"status": "success"} mock_azure.return_value = {"status": "success"}
mock_dropbox.return_value = {"status": "success"} mock_dropbox.return_value = {"status": "success"}
mock_gdrive.return_value = {"status": "success"} mock_gdrive.return_value = {"status": "success"}
@@ -249,14 +250,14 @@ class TestCheckCredentialsTask:
@patch("app.tasks.check_credentials.get_failure_state") @patch("app.tasks.check_credentials.get_failure_state")
@patch("app.tasks.check_credentials.get_provider_status") @patch("app.tasks.check_credentials.get_provider_status")
@patch("app.tasks.check_credentials.validate_storage_configs") @patch("app.tasks.check_credentials.validate_storage_configs")
@patch("app.tasks.check_credentials.sync_test_openai_connection") @patch("app.tasks.check_credentials.sync_test_ai_provider_connection")
def test_tracks_failures( def test_tracks_failures(
self, mock_openai, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state self, mock_ai_provider, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state
): ):
"""Test tracks credential failures.""" """Test tracks credential failures."""
mock_get_state.return_value = {} mock_get_state.return_value = {}
mock_provider_status.return_value = { mock_provider_status.return_value = {
"OpenAI": {"configured": True}, "AI Provider": {"configured": True},
"Azure AI": {"configured": False}, "Azure AI": {"configured": False},
"Dropbox": {"configured": False}, "Dropbox": {"configured": False},
"Google Drive": {"configured": False}, "Google Drive": {"configured": False},
@@ -264,7 +265,7 @@ class TestCheckCredentialsTask:
} }
mock_storage_configs.return_value = {} mock_storage_configs.return_value = {}
mock_openai.return_value = {"status": "error", "message": "Invalid API key"} mock_ai_provider.return_value = {"status": "error", "message": "Invalid API key"}
result = check_credentials() result = check_credentials()
@@ -281,7 +282,7 @@ class TestCheckCredentialsTask:
"""Test skips unconfigured services.""" """Test skips unconfigured services."""
mock_get_state.return_value = {} mock_get_state.return_value = {}
mock_provider_status.return_value = { mock_provider_status.return_value = {
"OpenAI": {"configured": False}, "AI Provider": {"configured": False},
"Azure AI": {"configured": False}, "Azure AI": {"configured": False},
"Dropbox": {"configured": False}, "Dropbox": {"configured": False},
"Google Drive": {"configured": False}, "Google Drive": {"configured": False},
@@ -298,15 +299,15 @@ class TestCheckCredentialsTask:
@patch("app.tasks.check_credentials.get_failure_state") @patch("app.tasks.check_credentials.get_failure_state")
@patch("app.tasks.check_credentials.get_provider_status") @patch("app.tasks.check_credentials.get_provider_status")
@patch("app.tasks.check_credentials.validate_storage_configs") @patch("app.tasks.check_credentials.validate_storage_configs")
@patch("app.tasks.check_credentials.sync_test_openai_connection") @patch("app.tasks.check_credentials.sync_test_ai_provider_connection")
@patch("app.tasks.check_credentials.notify_credential_failure") @patch("app.tasks.check_credentials.notify_credential_failure")
def test_sends_notifications_on_failure( def test_sends_notifications_on_failure(
self, mock_notify, mock_openai, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state self, mock_notify, mock_ai_provider, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state
): ):
"""Test sends notifications on credential failure.""" """Test sends notifications on credential failure."""
mock_get_state.return_value = {} mock_get_state.return_value = {}
mock_provider_status.return_value = { mock_provider_status.return_value = {
"OpenAI": {"configured": True}, "AI Provider": {"configured": True},
"Azure AI": {"configured": False}, "Azure AI": {"configured": False},
"Dropbox": {"configured": False}, "Dropbox": {"configured": False},
"Google Drive": {"configured": False}, "Google Drive": {"configured": False},
@@ -314,7 +315,7 @@ class TestCheckCredentialsTask:
} }
mock_storage_configs.return_value = {} mock_storage_configs.return_value = {}
mock_openai.return_value = {"status": "error", "message": "Invalid API key"} mock_ai_provider.return_value = {"status": "error", "message": "Invalid API key"}
check_credentials() check_credentials()
@@ -324,16 +325,16 @@ class TestCheckCredentialsTask:
@patch("app.tasks.check_credentials.get_failure_state") @patch("app.tasks.check_credentials.get_failure_state")
@patch("app.tasks.check_credentials.get_provider_status") @patch("app.tasks.check_credentials.get_provider_status")
@patch("app.tasks.check_credentials.validate_storage_configs") @patch("app.tasks.check_credentials.validate_storage_configs")
@patch("app.tasks.check_credentials.sync_test_openai_connection") @patch("app.tasks.check_credentials.sync_test_ai_provider_connection")
@patch("app.tasks.check_credentials.notify_credential_failure") @patch("app.tasks.check_credentials.notify_credential_failure")
def test_suppresses_notifications_after_threshold( def test_suppresses_notifications_after_threshold(
self, mock_notify, mock_openai, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state self, mock_notify, mock_ai_provider, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state
): ):
"""Test suppresses notifications after failure threshold.""" """Test suppresses notifications after failure threshold."""
# Existing state with 4 failures # Existing state with 4 failures
mock_get_state.return_value = {"OpenAI": {"count": 4, "last_notified": 12345}} mock_get_state.return_value = {"AI Provider": {"count": 4, "last_notified": 12345}}
mock_provider_status.return_value = { mock_provider_status.return_value = {
"OpenAI": {"configured": True}, "AI Provider": {"configured": True},
"Azure AI": {"configured": False}, "Azure AI": {"configured": False},
"Dropbox": {"configured": False}, "Dropbox": {"configured": False},
"Google Drive": {"configured": False}, "Google Drive": {"configured": False},
@@ -341,7 +342,7 @@ class TestCheckCredentialsTask:
} }
mock_storage_configs.return_value = {} mock_storage_configs.return_value = {}
mock_openai.return_value = {"status": "error", "message": "Invalid API key"} mock_ai_provider.return_value = {"status": "error", "message": "Invalid API key"}
check_credentials() check_credentials()
@@ -352,15 +353,15 @@ class TestCheckCredentialsTask:
@patch("app.tasks.check_credentials.get_failure_state") @patch("app.tasks.check_credentials.get_failure_state")
@patch("app.tasks.check_credentials.get_provider_status") @patch("app.tasks.check_credentials.get_provider_status")
@patch("app.tasks.check_credentials.validate_storage_configs") @patch("app.tasks.check_credentials.validate_storage_configs")
@patch("app.tasks.check_credentials.sync_test_openai_connection") @patch("app.tasks.check_credentials.sync_test_ai_provider_connection")
def test_tracks_recovery( def test_tracks_recovery(
self, mock_openai, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state self, mock_ai_provider, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state
): ):
"""Test tracks service recovery.""" """Test tracks service recovery."""
# Existing state with failures # Existing state with failures
mock_get_state.return_value = {"OpenAI": {"count": 2, "last_notified": 12345}} mock_get_state.return_value = {"AI Provider": {"count": 2, "last_notified": 12345}}
mock_provider_status.return_value = { mock_provider_status.return_value = {
"OpenAI": {"configured": True}, "AI Provider": {"configured": True},
"Azure AI": {"configured": False}, "Azure AI": {"configured": False},
"Dropbox": {"configured": False}, "Dropbox": {"configured": False},
"Google Drive": {"configured": False}, "Google Drive": {"configured": False},
@@ -369,7 +370,7 @@ class TestCheckCredentialsTask:
mock_storage_configs.return_value = {} mock_storage_configs.return_value = {}
# Service is now valid # Service is now valid
mock_openai.return_value = {"status": "success"} mock_ai_provider.return_value = {"status": "success"}
result = check_credentials() result = check_credentials()
@@ -379,14 +380,14 @@ class TestCheckCredentialsTask:
@patch("app.tasks.check_credentials.get_failure_state") @patch("app.tasks.check_credentials.get_failure_state")
@patch("app.tasks.check_credentials.get_provider_status") @patch("app.tasks.check_credentials.get_provider_status")
@patch("app.tasks.check_credentials.validate_storage_configs") @patch("app.tasks.check_credentials.validate_storage_configs")
@patch("app.tasks.check_credentials.sync_test_openai_connection") @patch("app.tasks.check_credentials.sync_test_ai_provider_connection")
def test_handles_exception_during_check( def test_handles_exception_during_check(
self, mock_openai, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state self, mock_ai_provider, mock_storage_configs, mock_provider_status, mock_get_state, mock_save_state
): ):
"""Test handles exception during credential check.""" """Test handles exception during credential check."""
mock_get_state.return_value = {} mock_get_state.return_value = {}
mock_provider_status.return_value = { mock_provider_status.return_value = {
"OpenAI": {"configured": True}, "AI Provider": {"configured": True},
"Azure AI": {"configured": False}, "Azure AI": {"configured": False},
"Dropbox": {"configured": False}, "Dropbox": {"configured": False},
"Google Drive": {"configured": False}, "Google Drive": {"configured": False},
@@ -394,11 +395,11 @@ class TestCheckCredentialsTask:
} }
mock_storage_configs.return_value = {} mock_storage_configs.return_value = {}
mock_openai.side_effect = Exception("Network error") mock_ai_provider.side_effect = Exception("Network error")
result = check_credentials() result = check_credentials()
# Should still complete and record the error # Should still complete and record the error
assert result["failures"] == 1 assert result["failures"] == 1
assert "OpenAI" in result["results"] assert "AI Provider" in result["results"]
assert result["results"]["OpenAI"]["status"] == "error" assert result["results"]["AI Provider"]["status"] == "error"
+1 -1
View File
@@ -35,7 +35,7 @@ class TestStatusDashboard:
mock_exists.return_value = False mock_exists.return_value = False
mock_providers.return_value = { mock_providers.return_value = {
"OpenAI": {"configured": True, "status": "success"}, "AI Provider": {"configured": True, "status": "success"},
"Azure AI": {"configured": False}, "Azure AI": {"configured": False},
} }
mock_settings.version = "1.0.0" mock_settings.version = "1.0.0"