Initial commit
This commit is contained in:
232
backend/app/api/routes_settings.py
Normal file
232
backend/app/api/routes_settings.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""API routes for managing persistent single-user application settings."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.schemas.settings import (
|
||||
AppSettingsUpdateRequest,
|
||||
AppSettingsResponse,
|
||||
DisplaySettingsResponse,
|
||||
HandwritingSettingsResponse,
|
||||
HandwritingStyleSettingsResponse,
|
||||
HandwritingSettingsUpdateRequest,
|
||||
OcrTaskSettingsResponse,
|
||||
ProviderSettingsResponse,
|
||||
RoutingTaskSettingsResponse,
|
||||
SummaryTaskSettingsResponse,
|
||||
TaskSettingsResponse,
|
||||
UploadDefaultsResponse,
|
||||
)
|
||||
from app.services.app_settings import (
|
||||
TASK_OCR_HANDWRITING,
|
||||
TASK_ROUTING_CLASSIFICATION,
|
||||
TASK_SUMMARY_GENERATION,
|
||||
read_app_settings,
|
||||
reset_app_settings,
|
||||
update_app_settings,
|
||||
update_handwriting_settings,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _build_response(payload: dict) -> AppSettingsResponse:
|
||||
"""Converts internal settings dictionaries into API response models."""
|
||||
|
||||
upload_defaults_payload = payload.get("upload_defaults", {})
|
||||
display_payload = payload.get("display", {})
|
||||
providers_payload = payload.get("providers", [])
|
||||
tasks_payload = payload.get("tasks", {})
|
||||
handwriting_style_payload = payload.get("handwriting_style_clustering", {})
|
||||
ocr_payload = tasks_payload.get(TASK_OCR_HANDWRITING, {})
|
||||
summary_payload = tasks_payload.get(TASK_SUMMARY_GENERATION, {})
|
||||
routing_payload = tasks_payload.get(TASK_ROUTING_CLASSIFICATION, {})
|
||||
|
||||
return AppSettingsResponse(
|
||||
upload_defaults=UploadDefaultsResponse(
|
||||
logical_path=str(upload_defaults_payload.get("logical_path", "Inbox")),
|
||||
tags=[
|
||||
str(tag).strip()
|
||||
for tag in upload_defaults_payload.get("tags", [])
|
||||
if isinstance(tag, str) and tag.strip()
|
||||
],
|
||||
),
|
||||
display=DisplaySettingsResponse(
|
||||
cards_per_page=int(display_payload.get("cards_per_page", 12)),
|
||||
log_typing_animation_enabled=bool(display_payload.get("log_typing_animation_enabled", True)),
|
||||
),
|
||||
handwriting_style_clustering=HandwritingStyleSettingsResponse(
|
||||
enabled=bool(handwriting_style_payload.get("enabled", True)),
|
||||
embed_model=str(handwriting_style_payload.get("embed_model", "ts/clip-vit-b-p32")),
|
||||
neighbor_limit=int(handwriting_style_payload.get("neighbor_limit", 8)),
|
||||
match_min_similarity=float(handwriting_style_payload.get("match_min_similarity", 0.86)),
|
||||
bootstrap_match_min_similarity=float(
|
||||
handwriting_style_payload.get("bootstrap_match_min_similarity", 0.89)
|
||||
),
|
||||
bootstrap_sample_size=int(handwriting_style_payload.get("bootstrap_sample_size", 3)),
|
||||
image_max_side=int(handwriting_style_payload.get("image_max_side", 1024)),
|
||||
),
|
||||
predefined_paths=[
|
||||
{
|
||||
"value": str(item.get("value", "")).strip(),
|
||||
"global_shared": bool(item.get("global_shared", False)),
|
||||
}
|
||||
for item in payload.get("predefined_paths", [])
|
||||
if isinstance(item, dict) and str(item.get("value", "")).strip()
|
||||
],
|
||||
predefined_tags=[
|
||||
{
|
||||
"value": str(item.get("value", "")).strip(),
|
||||
"global_shared": bool(item.get("global_shared", False)),
|
||||
}
|
||||
for item in payload.get("predefined_tags", [])
|
||||
if isinstance(item, dict) and str(item.get("value", "")).strip()
|
||||
],
|
||||
providers=[
|
||||
ProviderSettingsResponse(
|
||||
id=str(provider.get("id", "")),
|
||||
label=str(provider.get("label", "")),
|
||||
provider_type=str(provider.get("provider_type", "openai_compatible")),
|
||||
base_url=str(provider.get("base_url", "https://api.openai.com/v1")),
|
||||
timeout_seconds=int(provider.get("timeout_seconds", 45)),
|
||||
api_key_set=bool(provider.get("api_key_set", False)),
|
||||
api_key_masked=str(provider.get("api_key_masked", "")),
|
||||
)
|
||||
for provider in providers_payload
|
||||
],
|
||||
tasks=TaskSettingsResponse(
|
||||
ocr_handwriting=OcrTaskSettingsResponse(
|
||||
enabled=bool(ocr_payload.get("enabled", True)),
|
||||
provider_id=str(ocr_payload.get("provider_id", "openai-default")),
|
||||
model=str(ocr_payload.get("model", "gpt-4.1-mini")),
|
||||
prompt=str(ocr_payload.get("prompt", "")),
|
||||
),
|
||||
summary_generation=SummaryTaskSettingsResponse(
|
||||
enabled=bool(summary_payload.get("enabled", True)),
|
||||
provider_id=str(summary_payload.get("provider_id", "openai-default")),
|
||||
model=str(summary_payload.get("model", "gpt-4.1-mini")),
|
||||
prompt=str(summary_payload.get("prompt", "")),
|
||||
max_input_tokens=int(summary_payload.get("max_input_tokens", 8000)),
|
||||
),
|
||||
routing_classification=RoutingTaskSettingsResponse(
|
||||
enabled=bool(routing_payload.get("enabled", True)),
|
||||
provider_id=str(routing_payload.get("provider_id", "openai-default")),
|
||||
model=str(routing_payload.get("model", "gpt-4.1-mini")),
|
||||
prompt=str(routing_payload.get("prompt", "")),
|
||||
neighbor_count=int(routing_payload.get("neighbor_count", 8)),
|
||||
neighbor_min_similarity=float(routing_payload.get("neighbor_min_similarity", 0.84)),
|
||||
auto_apply_confidence_threshold=float(routing_payload.get("auto_apply_confidence_threshold", 0.78)),
|
||||
auto_apply_neighbor_similarity_threshold=float(
|
||||
routing_payload.get("auto_apply_neighbor_similarity_threshold", 0.55)
|
||||
),
|
||||
neighbor_path_override_enabled=bool(routing_payload.get("neighbor_path_override_enabled", True)),
|
||||
neighbor_path_override_min_similarity=float(
|
||||
routing_payload.get("neighbor_path_override_min_similarity", 0.86)
|
||||
),
|
||||
neighbor_path_override_min_gap=float(routing_payload.get("neighbor_path_override_min_gap", 0.04)),
|
||||
neighbor_path_override_max_confidence=float(
|
||||
routing_payload.get("neighbor_path_override_max_confidence", 0.9)
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=AppSettingsResponse)
|
||||
def get_app_settings() -> AppSettingsResponse:
|
||||
"""Returns persisted provider and per-task settings configuration."""
|
||||
|
||||
return _build_response(read_app_settings())
|
||||
|
||||
|
||||
@router.patch("", response_model=AppSettingsResponse)
|
||||
def set_app_settings(payload: AppSettingsUpdateRequest) -> AppSettingsResponse:
|
||||
"""Updates providers and task settings and returns resulting persisted configuration."""
|
||||
|
||||
providers_payload = None
|
||||
if payload.providers is not None:
|
||||
providers_payload = [provider.model_dump() for provider in payload.providers]
|
||||
|
||||
tasks_payload = None
|
||||
if payload.tasks is not None:
|
||||
tasks_payload = payload.tasks.model_dump(exclude_none=True)
|
||||
|
||||
upload_defaults_payload = None
|
||||
if payload.upload_defaults is not None:
|
||||
upload_defaults_payload = payload.upload_defaults.model_dump(exclude_none=True)
|
||||
|
||||
display_payload = None
|
||||
if payload.display is not None:
|
||||
display_payload = payload.display.model_dump(exclude_none=True)
|
||||
|
||||
handwriting_style_payload = None
|
||||
if payload.handwriting_style_clustering is not None:
|
||||
handwriting_style_payload = payload.handwriting_style_clustering.model_dump(exclude_none=True)
|
||||
predefined_paths_payload = None
|
||||
if payload.predefined_paths is not None:
|
||||
predefined_paths_payload = [item.model_dump(exclude_none=True) for item in payload.predefined_paths]
|
||||
predefined_tags_payload = None
|
||||
if payload.predefined_tags is not None:
|
||||
predefined_tags_payload = [item.model_dump(exclude_none=True) for item in payload.predefined_tags]
|
||||
|
||||
updated = update_app_settings(
|
||||
providers=providers_payload,
|
||||
tasks=tasks_payload,
|
||||
upload_defaults=upload_defaults_payload,
|
||||
display=display_payload,
|
||||
handwriting_style=handwriting_style_payload,
|
||||
predefined_paths=predefined_paths_payload,
|
||||
predefined_tags=predefined_tags_payload,
|
||||
)
|
||||
return _build_response(updated)
|
||||
|
||||
|
||||
@router.post("/reset", response_model=AppSettingsResponse)
|
||||
def reset_settings_to_defaults() -> AppSettingsResponse:
|
||||
"""Resets all persisted settings to default providers and task bindings."""
|
||||
|
||||
return _build_response(reset_app_settings())
|
||||
|
||||
|
||||
@router.patch("/handwriting", response_model=AppSettingsResponse)
|
||||
def set_handwriting_settings(payload: HandwritingSettingsUpdateRequest) -> AppSettingsResponse:
|
||||
"""Updates handwriting transcription settings and returns the resulting configuration."""
|
||||
|
||||
updated = update_handwriting_settings(
|
||||
enabled=payload.enabled,
|
||||
openai_base_url=payload.openai_base_url,
|
||||
openai_model=payload.openai_model,
|
||||
openai_timeout_seconds=payload.openai_timeout_seconds,
|
||||
openai_api_key=payload.openai_api_key,
|
||||
clear_openai_api_key=payload.clear_openai_api_key,
|
||||
)
|
||||
return _build_response(updated)
|
||||
|
||||
|
||||
@router.get("/handwriting", response_model=HandwritingSettingsResponse)
|
||||
def get_handwriting_settings() -> HandwritingSettingsResponse:
|
||||
"""Returns legacy handwriting response shape for compatibility with older clients."""
|
||||
|
||||
payload = _build_response(read_app_settings())
|
||||
fallback_provider = ProviderSettingsResponse(
|
||||
id="openai-default",
|
||||
label="OpenAI Default",
|
||||
provider_type="openai_compatible",
|
||||
base_url="https://api.openai.com/v1",
|
||||
timeout_seconds=45,
|
||||
api_key_set=False,
|
||||
api_key_masked="",
|
||||
)
|
||||
ocr = payload.tasks.ocr_handwriting
|
||||
provider = next((item for item in payload.providers if item.id == ocr.provider_id), None)
|
||||
if provider is None:
|
||||
provider = payload.providers[0] if payload.providers else fallback_provider
|
||||
return HandwritingSettingsResponse(
|
||||
provider=provider.provider_type,
|
||||
enabled=ocr.enabled,
|
||||
openai_base_url=provider.base_url,
|
||||
openai_model=ocr.model,
|
||||
openai_timeout_seconds=provider.timeout_seconds,
|
||||
openai_api_key_set=provider.api_key_set,
|
||||
openai_api_key_masked=provider.api_key_masked,
|
||||
)
|
||||
Reference in New Issue
Block a user