feat(librarian): send explicit non-empty user on every library-desk request
Library-desk is removing its server-side default user, so a request without an explicit tenant will 422 after its next deploy: - New client-level _resolve_user() resolves the tenant (explicit arg or request context) and raises ValueError on an empty/whitespace value BEFORE any bytes hit the wire; all 15 tenant-scoped methods use it - extract_content / extract_content_batch now accept and send the user (query param), matching the rest of the API surface - search_web no longer falls back to a phantom "tatlock-librarian" tenant; it sends the resolved user - health_check stays user-less (public, not tenant-scoped) Tests: parametrized sweep pins the wire contract (user present in params or payload) for every tenant-scoped method, for both context and explicit users; empty-tenant calls are asserted to fail without any HTTP call; the recorded-fixture hybrid contract test now pins user as an explicit query param. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -263,6 +263,23 @@ class LibraryDeskClient:
|
||||
)
|
||||
return self._client
|
||||
|
||||
def _resolve_user(self, user: str | None) -> str:
|
||||
"""
|
||||
Resolve the effective tenant for a request and require it non-empty.
|
||||
|
||||
Library-desk is removing its server-side default user, so every
|
||||
request must carry an explicit tenant (a missing user will 422).
|
||||
An empty tenant is a programming or configuration error - fail
|
||||
loudly here, before any bytes hit the wire.
|
||||
"""
|
||||
effective = user if user is not None else get_user()
|
||||
if not effective or not effective.strip():
|
||||
raise ValueError(
|
||||
"library-desk request requires a non-empty user (tenant); "
|
||||
"got an empty value from the caller or request context"
|
||||
)
|
||||
return effective
|
||||
|
||||
async def _request_with_retry(
|
||||
self,
|
||||
send: Callable[[], Awaitable[httpx.Response]],
|
||||
@@ -337,7 +354,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
HybridRAGResponse with ranked results and context
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
# The live service requires all limits >= 1 (422 otherwise);
|
||||
@@ -446,7 +463,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
List of matching wiki pages
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
logger.debug("library_desk_wiki_search", query=query, user=user)
|
||||
@@ -478,7 +495,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
WikiPage with full content
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
response = await self._request_with_retry(
|
||||
@@ -509,7 +526,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
List of wiki pages
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
params: dict[str, Any] = {"user": user, "limit": limit}
|
||||
@@ -548,7 +565,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
Created WikiPage
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
payload = {
|
||||
@@ -593,7 +610,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
Updated WikiPage
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
# Build update payload with only provided fields
|
||||
@@ -651,7 +668,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
SmartCreateResponse with page and research metadata
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
@@ -702,7 +719,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
List of dossiers with page counts
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
response = await self._request_with_retry(
|
||||
@@ -740,7 +757,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
List of matching document chunks with scores
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
payload = {
|
||||
@@ -781,7 +798,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
List of result records
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
payload = {
|
||||
@@ -814,7 +831,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
List of graph nodes
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
params: dict[str, Any] = {"user": user, "limit": limit}
|
||||
@@ -845,7 +862,7 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
Node with relationships and connected nodes
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
response = await self._request_with_retry(
|
||||
@@ -907,14 +924,14 @@ class LibraryDeskClient:
|
||||
Returns:
|
||||
WebSearchResponse with results and pre-formatted sources
|
||||
"""
|
||||
user = user or get_user()
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
payload = {
|
||||
"query": query,
|
||||
"search_type": search_type,
|
||||
"limit": limit,
|
||||
"user": user or "tatlock-librarian",
|
||||
"user": user,
|
||||
}
|
||||
|
||||
logger.info("library_desk_web_search", query=query, limit=limit)
|
||||
@@ -955,6 +972,7 @@ class LibraryDeskClient:
|
||||
async def extract_content(
|
||||
self,
|
||||
url: str,
|
||||
user: str | None = None,
|
||||
include_metadata: bool = True,
|
||||
max_length: int = 5000,
|
||||
) -> ContentExtractionResult:
|
||||
@@ -968,12 +986,14 @@ class LibraryDeskClient:
|
||||
|
||||
Args:
|
||||
url: URL to extract content from
|
||||
user: User identifier (defaults to request context)
|
||||
include_metadata: Whether to extract author, date, etc.
|
||||
max_length: Maximum content length
|
||||
|
||||
Returns:
|
||||
ContentExtractionResult (check .success and .error fields)
|
||||
"""
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
payload = {
|
||||
@@ -984,7 +1004,11 @@ class LibraryDeskClient:
|
||||
|
||||
logger.debug("library_desk_extract_content", url=url)
|
||||
|
||||
response = await client.post("/content/extract", json=payload)
|
||||
response = await client.post(
|
||||
"/content/extract",
|
||||
json=payload,
|
||||
params={"user": user},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
@@ -1004,6 +1028,7 @@ class LibraryDeskClient:
|
||||
async def extract_content_batch(
|
||||
self,
|
||||
urls: list[str],
|
||||
user: str | None = None,
|
||||
include_metadata: bool = True,
|
||||
max_length: int = 2000,
|
||||
) -> BatchExtractionResponse:
|
||||
@@ -1017,12 +1042,14 @@ class LibraryDeskClient:
|
||||
|
||||
Args:
|
||||
urls: List of URLs to extract (max 20)
|
||||
user: User identifier (defaults to request context)
|
||||
include_metadata: Whether to extract author, date, etc.
|
||||
max_length: Maximum content length per URL
|
||||
|
||||
Returns:
|
||||
BatchExtractionResponse with results and stats
|
||||
"""
|
||||
user = self._resolve_user(user)
|
||||
client = self._ensure_client()
|
||||
|
||||
payload = {
|
||||
@@ -1036,6 +1063,7 @@ class LibraryDeskClient:
|
||||
response = await client.post(
|
||||
"/content/extract/batch",
|
||||
json=payload,
|
||||
params={"user": user},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user