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"
|
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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user