Remove the implicit jpmschweitzer default tenant (DEFAULT_USER) from src/core/multi_tenancy.py and every endpoint and request model that inherited it (~40 endpoints across /query, /wiki, /vector, /graph, /ingest, /volatile, /documents, /stats, /rag). - Add validate_required_user() + RequiredUser pydantic type in multi_tenancy and a shared require_user FastAPI dependency (RequiredUserQuery) that rejects missing, empty, and whitespace-only users with 422, following the /maintenance/* pattern. - Wiki page create / smart-create / dossier request models now require user (no fallback in wiki_service). - /maintenance/cleanup/test-data derives the tenant from the page path instead of using the production tenant collection. - Wiki.js change listener skips changes when no tenant user can be derived from the notification email instead of defaulting to the production tenant. - Consolidation service internal helpers no longer default to the production tenant. - Tool catalog marks user as required with honest descriptions. - OpenAPI descriptions updated honestly; CHANGELOG notes that callers (tatlock, Scheduler ingest tasks) must now send explicit user. - Offline tests: 422 coverage for query/body endpoints, required-user validator tests; updated legacy tests that assumed a default tenant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89 lines
2.3 KiB
Python
89 lines
2.3 KiB
Python
"""
|
|
RAG search models for Library Desk.
|
|
|
|
Pydantic models for web/news/image search requests and responses.
|
|
"""
|
|
|
|
from enum import Enum
|
|
from typing import Optional, List
|
|
from pydantic import BaseModel, Field
|
|
|
|
from src.core.multi_tenancy import RequiredUser
|
|
|
|
|
|
class SearchType(str, Enum):
|
|
"""Supported search types."""
|
|
WEB = "web"
|
|
NEWS = "news"
|
|
IMAGES = "images"
|
|
|
|
|
|
class RAGSearchRequest(BaseModel):
|
|
"""Request for RAG search endpoint."""
|
|
|
|
query: str = Field(
|
|
...,
|
|
min_length=1,
|
|
max_length=500,
|
|
description="The search query"
|
|
)
|
|
search_type: SearchType = Field(
|
|
default=SearchType.WEB,
|
|
description="Type of search: web, news, or images"
|
|
)
|
|
limit: int = Field(
|
|
default=10,
|
|
ge=1,
|
|
le=20,
|
|
description="Maximum number of results (1-20)"
|
|
)
|
|
user: RequiredUser = Field(
|
|
...,
|
|
description="User identifier (tenant). Required — used for rate limiting/personalization."
|
|
)
|
|
|
|
|
|
class RAGSearchResult(BaseModel):
|
|
"""A single search result with extracted content."""
|
|
|
|
title: str = Field(..., description="Title of the result")
|
|
url: str = Field(..., description="URL of the source")
|
|
content: str = Field(
|
|
"",
|
|
description="Full extracted text via Trafilatura (max ~2000 chars)"
|
|
)
|
|
snippet: str = Field(
|
|
"",
|
|
description="Original search engine snippet (150-300 chars)"
|
|
)
|
|
source: str = Field(..., description="Domain name of the source")
|
|
published_date: Optional[str] = Field(
|
|
None,
|
|
description="Publication date in ISO format if available"
|
|
)
|
|
|
|
|
|
class RAGSearchResponse(BaseModel):
|
|
"""Response from RAG search endpoint."""
|
|
|
|
query: str = Field(..., description="Echo of the original query")
|
|
search_type: SearchType = Field(..., description="Type of search performed")
|
|
results: List[RAGSearchResult] = Field(
|
|
default_factory=list,
|
|
description="List of search results with extracted content"
|
|
)
|
|
total_results: int = Field(
|
|
...,
|
|
ge=0,
|
|
description="Number of results returned"
|
|
)
|
|
search_time_ms: int = Field(
|
|
...,
|
|
ge=0,
|
|
description="Total time for search and content extraction"
|
|
)
|
|
sources_summary: str = Field(
|
|
"",
|
|
description="Markdown-formatted list of all source URLs"
|
|
)
|