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:
@@ -119,16 +119,44 @@ AUTHENTIK_CONFIG_URL=<ConfigUrlOfYourApp, e.g. https://authentik.example.com/app
|
||||
OAUTH_PROVIDER_NAME="Authentik SSO"
|
||||
|
||||
# **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_BASE_URL=https://api.openai.com/v1
|
||||
OPENAI_MODEL=gpt-4o-mini
|
||||
|
||||
# Azure AI
|
||||
AZURE_REGION="eastus"
|
||||
AZURE_ENDPOINT="https://<yourendpoint>.cognitiveservices.azure.com/"
|
||||
AZURE_AI_KEY=<AZURE_AI_KEY>
|
||||
# --- Anthropic Claude (AI_PROVIDER=anthropic) ---
|
||||
# ANTHROPIC_API_KEY=sk-ant-...
|
||||
# AI_MODEL=claude-3-5-sonnet-20241022
|
||||
|
||||
# --- 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_HOST=smtp.example.com
|
||||
EMAIL_PORT=587
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
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.
|
||||
- **Paperless NGX** for document indexing and management.
|
||||
- **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:
|
||||
1. **PDF Conversion**: Non-PDF files are converted to PDF format using Gotenberg
|
||||
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.)
|
||||
- Key entities (dates, names, amounts, account numbers)
|
||||
- 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
|
||||
- **OCR Processing (Azure)**:
|
||||
- Extract text from scanned PDFs using Azure Document Intelligence
|
||||
- **Metadata Extraction (OpenAI)**:
|
||||
- Use GPT to classify, label, or otherwise enrich the text with structured metadata
|
||||
- **Metadata Extraction (AI Provider)**:
|
||||
- Use any supported AI provider (OpenAI, Anthropic, Gemini, Ollama, etc.) to classify, label, or otherwise enrich the text with structured metadata
|
||||
- **PDF Conversion (Gotenberg)**:
|
||||
- Convert non-PDF attachments (e.g., Word docs, images) into PDFs
|
||||
- **Document Management (Paperless NGX)**:
|
||||
@@ -214,7 +214,8 @@ The following is a summary of the licenses used by our direct dependencies:
|
||||
| Uvicorn | BSD |
|
||||
| SQLAlchemy | MIT |
|
||||
| Pydantic | MIT |
|
||||
| OpenAI | MIT |
|
||||
| openai | MIT |
|
||||
| litellm | MIT |
|
||||
| pypdf | BSD |
|
||||
| Requests | Apache 2.0 |
|
||||
| puremagic | MIT |
|
||||
|
||||
+57
-1
@@ -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
|
||||
@@ -146,3 +150,55 @@ async def test_openai_connection(request: Request):
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error testing OpenAI connection")
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.api.google_drive import test_google_drive_token
|
||||
from app.api.onedrive import test_onedrive_token
|
||||
|
||||
# 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.config import settings
|
||||
|
||||
@@ -75,8 +75,17 @@ def unwrap_decorated_function(func):
|
||||
|
||||
|
||||
# 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():
|
||||
"""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
|
||||
inner_func = unwrap_decorated_function(test_openai_connection)
|
||||
request = MockRequest()
|
||||
@@ -139,10 +148,10 @@ def check_credentials():
|
||||
# Define services with their test functions and configuration status
|
||||
services = [
|
||||
{
|
||||
"name": "OpenAI",
|
||||
"check_func": sync_test_openai_connection,
|
||||
"configured": provider_status.get("OpenAI", {}).get("configured", False),
|
||||
"config_issues": [], # OpenAI isn't in storage_configs
|
||||
"name": "AI Provider",
|
||||
"check_func": sync_test_ai_provider_connection,
|
||||
"configured": provider_status.get("AI Provider", {}).get("configured", False),
|
||||
"config_issues": [],
|
||||
},
|
||||
{
|
||||
"name": "Azure Document Intelligence",
|
||||
|
||||
@@ -57,20 +57,69 @@ def get_provider_status() -> dict[str, dict[str, object]]:
|
||||
"test_endpoint": "/api/diagnostic/test-notification",
|
||||
}
|
||||
|
||||
# Add AI services first
|
||||
providers["OpenAI"] = {
|
||||
"name": "OpenAI",
|
||||
"icon": "fa-brands fa-openai",
|
||||
"configured": bool(
|
||||
getattr(settings, "openai_api_key", None) and str(getattr(settings, "openai_api_key", "")).startswith("sk-")
|
||||
),
|
||||
# Add AI provider (dynamic – reflects whichever provider is configured)
|
||||
ai_provider_name = getattr(settings, "ai_provider", "openai").lower()
|
||||
model = getattr(settings, "ai_model", None) or getattr(settings, "openai_model", "gpt-4o-mini")
|
||||
|
||||
# Determine whether the active provider has its required credentials set
|
||||
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,
|
||||
"description": "AI-powered document analysis and metadata extraction",
|
||||
"details": {
|
||||
"api_key": mask_sensitive_value(getattr(settings, "openai_api_key", None)),
|
||||
"base_url": getattr(settings, "openai_base_url", "https://api.openai.com/v1"),
|
||||
"model": getattr(settings, "openai_model", "gpt-4"),
|
||||
},
|
||||
"description": f"AI-powered metadata extraction and OCR refinement ({ai_provider_name})",
|
||||
"details": _ai_details(),
|
||||
"testable": True,
|
||||
"test_endpoint": "/api/ai/test",
|
||||
}
|
||||
|
||||
providers["Azure AI"] = {
|
||||
|
||||
@@ -170,9 +170,21 @@ def get_settings_for_display(show_values: bool = False) -> dict[str, list[dict[s
|
||||
"s3_acl",
|
||||
],
|
||||
"AI Services": [
|
||||
"ai_provider",
|
||||
"ai_model",
|
||||
"openai_api_key",
|
||||
"openai_base_url",
|
||||
"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_endpoint",
|
||||
"azure_region",
|
||||
|
||||
+109
-11
@@ -136,15 +136,15 @@ SETTING_METADATA = {
|
||||
# AI Services
|
||||
"openai_api_key": {
|
||||
"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",
|
||||
"sensitive": True,
|
||||
"required": True,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"openai_base_url": {
|
||||
"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",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
@@ -152,34 +152,132 @@ SETTING_METADATA = {
|
||||
},
|
||||
"openai_model": {
|
||||
"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",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"azure_ai_key": {
|
||||
"ai_provider": {
|
||||
"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",
|
||||
"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,
|
||||
},
|
||||
"azure_region": {
|
||||
"category": "AI Services",
|
||||
"description": "Azure region for AI services",
|
||||
"description": "Azure region for Document Intelligence services (e.g., eastus)",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": True,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"azure_endpoint": {
|
||||
"category": "AI Services",
|
||||
"description": "Azure AI endpoint URL",
|
||||
"description": "Azure Document Intelligence endpoint URL",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": True,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Storage Providers - Dropbox
|
||||
|
||||
+17
-28
@@ -90,10 +90,21 @@ def get_required_settings() -> List[Dict[str, Any]]:
|
||||
"wizard_step": 2,
|
||||
"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",
|
||||
"label": "OpenAI API Key",
|
||||
"description": "API key for OpenAI services (metadata extraction)",
|
||||
"label": "API Key (OpenAI / Azure / LiteLLM)",
|
||||
"description": "API key for OpenAI, Azure OpenAI, or LiteLLM (not required for Ollama)",
|
||||
"type": "string",
|
||||
"sensitive": True,
|
||||
"default": None,
|
||||
@@ -101,32 +112,12 @@ def get_required_settings() -> List[Dict[str, Any]]:
|
||||
"wizard_category": "AI Services",
|
||||
},
|
||||
{
|
||||
"key": "azure_ai_key",
|
||||
"label": "Azure AI Key",
|
||||
"description": "Azure AI key for document intelligence (OCR)",
|
||||
"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)",
|
||||
"key": "openai_model",
|
||||
"label": "Default Model",
|
||||
"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": False,
|
||||
"default": "eastus",
|
||||
"wizard_step": 3,
|
||||
"wizard_category": "AI Services",
|
||||
},
|
||||
{
|
||||
"key": "azure_endpoint",
|
||||
"label": "Azure Endpoint",
|
||||
"description": "Azure AI endpoint URL",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"default": None,
|
||||
"default": "gpt-4o-mini",
|
||||
"wizard_step": 3,
|
||||
"wizard_category": "AI Services",
|
||||
},
|
||||
@@ -147,8 +138,6 @@ def is_setup_required() -> bool:
|
||||
critical_settings = [
|
||||
("session_secret", ["INSECURE_DEFAULT_FOR_DEVELOPMENT_ONLY_DO_NOT_USE_IN_PRODUCTION_MINIMUM_32_CHARS"]),
|
||||
("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:
|
||||
|
||||
@@ -6,7 +6,7 @@ This guide provides instructions for deploying DocuElevate in various environmen
|
||||
|
||||
- Docker and Docker Compose
|
||||
- Access to required external services (if configured):
|
||||
- OpenAI API
|
||||
- AI provider API key (OpenAI, Anthropic, Gemini, or other configured provider)
|
||||
- Azure Document Intelligence
|
||||
- Dropbox API
|
||||
- Nextcloud instance
|
||||
|
||||
@@ -26,7 +26,7 @@ Settings are organized into logical categories for easy navigation:
|
||||
|
||||
- **Core**: Database, Redis, working directory, external hostname, debug mode
|
||||
- **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
|
||||
- **Email**: SMTP configuration for sending emails
|
||||
- **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
|
||||
[
|
||||
{"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
@@ -150,7 +150,7 @@ View the complete processing history with a timeline showing:
|
||||
- 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:
|
||||
- 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
|
||||
- Configuration has been updated and you want to reprocess with new settings
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
for everyone, whether you're a small startup or a large enterprise.
|
||||
</p>
|
||||
<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
|
||||
Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.
|
||||
</p>
|
||||
@@ -33,7 +34,7 @@
|
||||
<ul class="list-disc list-inside text-gray-600 ml-2">
|
||||
<li>Simple and secure file uploads with drag & drop support</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>Intelligent document classification and date extraction</li>
|
||||
</ul>
|
||||
|
||||
@@ -139,8 +139,21 @@
|
||||
Enable {{ setting.key.replace('_', ' ').title() }}
|
||||
</label>
|
||||
</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 %}
|
||||
<!-- Text Input -->
|
||||
<!-- Text / Password Input -->
|
||||
<div class="relative">
|
||||
{% if setting.metadata.sensitive %}
|
||||
<!-- Sensitive Field with Show/Hide Toggle -->
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
<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">
|
||||
{{ 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>
|
||||
{% endif %}
|
||||
</label>
|
||||
@@ -129,7 +129,18 @@
|
||||
/>
|
||||
</div>
|
||||
{% 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
|
||||
type="{% if setting.sensitive %}password{% else %}text{% endif %}"
|
||||
id="{{ setting.key }}"
|
||||
@@ -137,9 +148,10 @@
|
||||
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"
|
||||
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 %}
|
||||
|
||||
{% 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>
|
||||
|
||||
@@ -174,11 +174,11 @@
|
||||
Configure Now
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif name == "OpenAI" %}
|
||||
{% elif name == "AI Provider" %}
|
||||
{% if provider.configured %}
|
||||
<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"
|
||||
data-provider="openai">
|
||||
data-provider="ai_provider">
|
||||
Test Connection
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -521,8 +521,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
endpoint = '/api/onedrive/test-token';
|
||||
} else if (provider === 'google_drive') {
|
||||
endpoint = '/api/google-drive/test-token';
|
||||
} else if (provider === 'openai') {
|
||||
endpoint = '/api/openai/test';
|
||||
} else if (provider === 'ai_provider') {
|
||||
endpoint = '/api/ai/test';
|
||||
} else if (provider === 'azure') {
|
||||
endpoint = '/api/azure/test';
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from app.tasks.check_credentials import (
|
||||
get_failure_state,
|
||||
save_failure_state,
|
||||
sync_test_azure_connection,
|
||||
sync_test_ai_provider_connection,
|
||||
sync_test_openai_connection,
|
||||
unwrap_decorated_function,
|
||||
)
|
||||
@@ -205,7 +206,7 @@ class TestCheckCredentialsTask:
|
||||
@patch("app.tasks.check_credentials.get_failure_state")
|
||||
@patch("app.tasks.check_credentials.get_provider_status")
|
||||
@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_dropbox_token")
|
||||
@patch("app.tasks.check_credentials.sync_test_google_drive_token")
|
||||
@@ -216,7 +217,7 @@ class TestCheckCredentialsTask:
|
||||
mock_gdrive,
|
||||
mock_dropbox,
|
||||
mock_azure,
|
||||
mock_openai,
|
||||
mock_ai_provider,
|
||||
mock_storage_configs,
|
||||
mock_provider_status,
|
||||
mock_get_state,
|
||||
@@ -225,7 +226,7 @@ class TestCheckCredentialsTask:
|
||||
"""Test checks all configured services."""
|
||||
mock_get_state.return_value = {}
|
||||
mock_provider_status.return_value = {
|
||||
"OpenAI": {"configured": True},
|
||||
"AI Provider": {"configured": True},
|
||||
"Azure AI": {"configured": True},
|
||||
"Dropbox": {"configured": True},
|
||||
"Google Drive": {"configured": True},
|
||||
@@ -234,7 +235,7 @@ class TestCheckCredentialsTask:
|
||||
mock_storage_configs.return_value = {"dropbox": [], "google_drive": [], "onedrive": []}
|
||||
|
||||
# All tests succeed
|
||||
mock_openai.return_value = {"status": "success"}
|
||||
mock_ai_provider.return_value = {"status": "success"}
|
||||
mock_azure.return_value = {"status": "success"}
|
||||
mock_dropbox.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_provider_status")
|
||||
@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(
|
||||
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."""
|
||||
mock_get_state.return_value = {}
|
||||
mock_provider_status.return_value = {
|
||||
"OpenAI": {"configured": True},
|
||||
"AI Provider": {"configured": True},
|
||||
"Azure AI": {"configured": False},
|
||||
"Dropbox": {"configured": False},
|
||||
"Google Drive": {"configured": False},
|
||||
@@ -264,7 +265,7 @@ class TestCheckCredentialsTask:
|
||||
}
|
||||
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()
|
||||
|
||||
@@ -281,7 +282,7 @@ class TestCheckCredentialsTask:
|
||||
"""Test skips unconfigured services."""
|
||||
mock_get_state.return_value = {}
|
||||
mock_provider_status.return_value = {
|
||||
"OpenAI": {"configured": False},
|
||||
"AI Provider": {"configured": False},
|
||||
"Azure AI": {"configured": False},
|
||||
"Dropbox": {"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_provider_status")
|
||||
@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")
|
||||
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."""
|
||||
mock_get_state.return_value = {}
|
||||
mock_provider_status.return_value = {
|
||||
"OpenAI": {"configured": True},
|
||||
"AI Provider": {"configured": True},
|
||||
"Azure AI": {"configured": False},
|
||||
"Dropbox": {"configured": False},
|
||||
"Google Drive": {"configured": False},
|
||||
@@ -314,7 +315,7 @@ class TestCheckCredentialsTask:
|
||||
}
|
||||
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()
|
||||
|
||||
@@ -324,16 +325,16 @@ class TestCheckCredentialsTask:
|
||||
@patch("app.tasks.check_credentials.get_failure_state")
|
||||
@patch("app.tasks.check_credentials.get_provider_status")
|
||||
@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")
|
||||
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."""
|
||||
# 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 = {
|
||||
"OpenAI": {"configured": True},
|
||||
"AI Provider": {"configured": True},
|
||||
"Azure AI": {"configured": False},
|
||||
"Dropbox": {"configured": False},
|
||||
"Google Drive": {"configured": False},
|
||||
@@ -341,7 +342,7 @@ class TestCheckCredentialsTask:
|
||||
}
|
||||
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()
|
||||
|
||||
@@ -352,15 +353,15 @@ class TestCheckCredentialsTask:
|
||||
@patch("app.tasks.check_credentials.get_failure_state")
|
||||
@patch("app.tasks.check_credentials.get_provider_status")
|
||||
@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(
|
||||
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."""
|
||||
# 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 = {
|
||||
"OpenAI": {"configured": True},
|
||||
"AI Provider": {"configured": True},
|
||||
"Azure AI": {"configured": False},
|
||||
"Dropbox": {"configured": False},
|
||||
"Google Drive": {"configured": False},
|
||||
@@ -369,7 +370,7 @@ class TestCheckCredentialsTask:
|
||||
mock_storage_configs.return_value = {}
|
||||
|
||||
# Service is now valid
|
||||
mock_openai.return_value = {"status": "success"}
|
||||
mock_ai_provider.return_value = {"status": "success"}
|
||||
|
||||
result = check_credentials()
|
||||
|
||||
@@ -379,14 +380,14 @@ class TestCheckCredentialsTask:
|
||||
@patch("app.tasks.check_credentials.get_failure_state")
|
||||
@patch("app.tasks.check_credentials.get_provider_status")
|
||||
@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(
|
||||
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."""
|
||||
mock_get_state.return_value = {}
|
||||
mock_provider_status.return_value = {
|
||||
"OpenAI": {"configured": True},
|
||||
"AI Provider": {"configured": True},
|
||||
"Azure AI": {"configured": False},
|
||||
"Dropbox": {"configured": False},
|
||||
"Google Drive": {"configured": False},
|
||||
@@ -394,11 +395,11 @@ class TestCheckCredentialsTask:
|
||||
}
|
||||
mock_storage_configs.return_value = {}
|
||||
|
||||
mock_openai.side_effect = Exception("Network error")
|
||||
mock_ai_provider.side_effect = Exception("Network error")
|
||||
|
||||
result = check_credentials()
|
||||
|
||||
# Should still complete and record the error
|
||||
assert result["failures"] == 1
|
||||
assert "OpenAI" in result["results"]
|
||||
assert result["results"]["OpenAI"]["status"] == "error"
|
||||
assert "AI Provider" in result["results"]
|
||||
assert result["results"]["AI Provider"]["status"] == "error"
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestStatusDashboard:
|
||||
|
||||
mock_exists.return_value = False
|
||||
mock_providers.return_value = {
|
||||
"OpenAI": {"configured": True, "status": "success"},
|
||||
"AI Provider": {"configured": True, "status": "success"},
|
||||
"Azure AI": {"configured": False},
|
||||
}
|
||||
mock_settings.version = "1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user