open-notebook/api/routers/settings.py
Luis Novo ef5bd2ce7b
feat(sources): add Crawl4AI URL engine + honor persisted engine settings (#432) (#1118)
* feat(sources): add Crawl4AI URL engine and honor persisted engine settings (#432)

- Add "crawl4ai" as a selectable URL processing engine (domain Literal,
  settings API validation, SettingsForm select, and label across all 14
  locales; urlHelp updated in en-US to describe the new fallback chain).
- The source graph now loads the persisted ContentSettings and passes the
  document/URL engine choices to ContentCoreConfig. Previously it built a
  hard-coded ContentSettings with "auto" engines, so a user's selection in
  Settings never took effect. Falls back to defaults if settings can't load.
- Crawl4AI Docker mode is driven by content-core's native CRAWL4AI_API_URL
  env var (documented separately under #1105).

Part of #939.

* fix(432): bundle Crawl4AI runtime + address review

- Bundle the Crawl4AI runtime so its local, no-API-key mode works out of
  the box: depend on content-core[crawl4ai] and install the Chromium
  browser via playwright in the Docker runtime-base (both image variants).
  Footprint is modest (no torch/transformers/CUDA); image grows ~300 MB
  from Chromium + system libs.
- Preserve the server-side traceback when persisted content settings fail
  to load (logger.opt(exception=True)) instead of only the message.
- Reset the ContentSettings singleton between domain tests (clear_instance)
  so a non-default value can't leak into neighboring tests.

Addresses review on #432.

* i18n(432): translate urlHelp Crawl4AI description across all 13 non-en locales

The Crawl4AI engine label was already localized; this brings the URL-engine
'help me choose' text in line with en-US in every locale — describing
Crawl4AI (local JS rendering, no API key) and its place in the auto
fallback chain (Firecrawl -> Jina -> Crawl4AI -> simple).
2026-07-12 20:05:03 -03:00

97 lines
3.8 KiB
Python

from fastapi import APIRouter, HTTPException
from loguru import logger
from api.models import SettingsResponse, SettingsUpdate
from open_notebook.domain.content_settings import ContentSettings
from open_notebook.exceptions import (
InvalidInputError,
OpenNotebookError,
)
router = APIRouter()
@router.get("/settings", response_model=SettingsResponse)
async def get_settings():
"""Get all application settings."""
try:
settings: ContentSettings = await ContentSettings.get_instance() # type: ignore[assignment]
return SettingsResponse(
default_content_processing_engine_doc=settings.default_content_processing_engine_doc,
default_content_processing_engine_url=settings.default_content_processing_engine_url,
default_embedding_option=settings.default_embedding_option,
auto_delete_files=settings.auto_delete_files,
youtube_preferred_languages=settings.youtube_preferred_languages,
)
except HTTPException:
raise
except OpenNotebookError:
raise
except Exception as e:
logger.error(f"Error fetching settings: {str(e)}")
raise HTTPException(
status_code=500, detail="Error fetching settings"
)
@router.put("/settings", response_model=SettingsResponse)
async def update_settings(settings_update: SettingsUpdate):
"""Update application settings."""
try:
settings: ContentSettings = await ContentSettings.get_instance() # type: ignore[assignment]
# Update only provided fields
if settings_update.default_content_processing_engine_doc is not None:
# Cast to proper literal type
from typing import Literal, cast
settings.default_content_processing_engine_doc = cast(
Literal["auto", "docling", "simple"],
settings_update.default_content_processing_engine_doc,
)
if settings_update.default_content_processing_engine_url is not None:
from typing import Literal, cast
settings.default_content_processing_engine_url = cast(
Literal["auto", "firecrawl", "jina", "crawl4ai", "simple"],
settings_update.default_content_processing_engine_url,
)
if settings_update.default_embedding_option is not None:
from typing import Literal, cast
settings.default_embedding_option = cast(
Literal["ask", "always", "never"],
settings_update.default_embedding_option,
)
if settings_update.auto_delete_files is not None:
from typing import Literal, cast
settings.auto_delete_files = cast(
Literal["yes", "no"], settings_update.auto_delete_files
)
if settings_update.youtube_preferred_languages is not None:
settings.youtube_preferred_languages = (
settings_update.youtube_preferred_languages
)
await settings.update()
return SettingsResponse(
default_content_processing_engine_doc=settings.default_content_processing_engine_doc,
default_content_processing_engine_url=settings.default_content_processing_engine_url,
default_embedding_option=settings.default_embedding_option,
auto_delete_files=settings.auto_delete_files,
youtube_preferred_languages=settings.youtube_preferred_languages,
)
except HTTPException:
raise
except InvalidInputError as e:
raise HTTPException(status_code=400, detail=str(e))
except OpenNotebookError:
raise
except Exception as e:
logger.error(f"Error updating settings: {str(e)}")
raise HTTPException(
status_code=500, detail="Error updating settings"
)