Implements comprehensive service client layer for Library Desk API to support
Librarian AI agent with multi-tenant knowledge management across Neo4j, Qdrant,
Wiki.js, SearXNG, and Ollama.
## Service Clients (src/clients/)
- Neo4j async client with connection pooling and user-scoped labels
- Qdrant vector store with collection-per-user multi-tenancy
- Wiki.js GraphQL API client for page/dossier management
- SearXNG client for web search integration
- Ollama client for text embeddings (nomic-embed-text)
## Core Infrastructure (src/core/)
- Multi-tenancy helpers for user namespace management
- Wiki.js: path-based namespaces (/users/{user})
- Neo4j: user-specific labels (User_{User}_Document)
- Qdrant: collection per user (library_desk_{user})
- Dependency injection with FastAPI Depends and @lru_cache singletons
- Lifecycle management (startup/shutdown) for all service connections
## Background Jobs (src/jobs/)
- Redis-based job manager for long-running operations
- Job status tracking with 24-hour TTL
- Support for queued, processing, completed, failed states
## Configuration
- Updated config.py with Redis DB 4 for library-desk jobs
- Updated docker-compose.yml: REDIS_DB from 2 to 4
- Added pytest and pytest-asyncio to requirements.txt
## Testing
- Unit tests: 25/25 passed (multi-tenancy helpers)
- Integration tests: 12/12 passed (all services verified)
- Neo4j connection and CRUD operations
- Qdrant vector operations with 768-dim embeddings
- Wiki.js GraphQL queries
- SearXNG web search
- Job Manager with Redis
- Dependency injection lifecycle
- pytest.ini configuration with asyncio support
## Health Monitoring
- Real-time service health checks via /health endpoint
- Connection status for all 5 external services
- Graceful degradation for partial service availability
## Architecture
- Follows async/await pattern throughout
- Connection pooling for Neo4j (singleton driver)
- HTTP client lifecycle management (httpx)
- Multi-tenancy enforced at client layer
- Default user: jpmschweitzer
Files changed: 26 files
- 5 new service clients (~1500 lines)
- 2 core modules (~500 lines)
- 1 job manager (~350 lines)
- 3 test files with 37 test cases
- Updated main.py with lifecycle hooks
All services tested and operational. Ready for Phase 2 (routers/services).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
12 KiB
Librarian Integration Guide
How The Scheduler (Librarian) interacts with Library Desk for documentation management
Overview
The Scheduler's documentation mirroring tasks feed content into The Library system via Library Desk API. This creates a knowledge graph and vector index of all documentation for semantic search and relationship discovery.
Architecture Flow
┌─────────────────┐
│ The Scheduler │ (The Librarian)
│ (scheduler) │
└────────┬────────┘
│ 1. Mirror docs from sources
│ (GitHub, Gitea, etc.)
↓
┌─────────────────┐
│ Gitea Repo │
│ docs-mirror/* │
└────────┬────────┘
│ 2. Ingest to Library
↓
┌─────────────────┐
│ Library Desk │ (Coordination API)
│ (library-desk) │
└────────┬────────┘
│ 3. Process & Index
├──→ Neo4j (relationships)
├──→ Qdrant (embeddings)
└──→ Wiki.js (dossiers)
Required Endpoints
1. Document Ingestion
POST /ingest/document
{
"source": "github",
"repository": "anthropics/anthropic-cookbook",
"path": "skills/citation/guide.md",
"content": "# Citation Guide\n...",
"metadata": {
"commit_sha": "abc123",
"author": "Anthropic",
"updated_at": "2025-12-08T10:30:00Z",
"gitea_mirror_path": "docs-mirror/anthropic-cookbook/skills/citation/guide.md",
"language": "markdown",
"tags": ["skills", "citation", "prompting"]
}
}
Response:
{
"document_id": "doc_abc123xyz",
"status": "processing",
"operations": {
"chunking": "pending",
"embedding": "pending",
"entity_extraction": "pending",
"graph_indexing": "pending"
},
"estimated_completion": "2025-12-08T10:30:15Z"
}
2. Batch Ingestion
POST /ingest/batch
{
"source": "github",
"repository": "anthropics/anthropic-cookbook",
"documents": [
{
"path": "skills/citation/guide.md",
"content": "...",
"metadata": {...}
},
{
"path": "skills/summarization/techniques.md",
"content": "...",
"metadata": {...}
}
]
}
Response:
{
"batch_id": "batch_xyz789",
"total_documents": 2,
"status": "processing",
"documents": [
{"document_id": "doc_1", "status": "queued"},
{"document_id": "doc_2", "status": "queued"}
]
}
3. Document Status Check
GET /ingest/status/{document_id}
Response:
{
"document_id": "doc_abc123xyz",
"status": "completed",
"operations": {
"chunking": "completed",
"embedding": "completed",
"entity_extraction": "completed",
"graph_indexing": "completed"
},
"results": {
"chunks_created": 12,
"vectors_indexed": 12,
"entities_extracted": 8,
"relationships_created": 15
},
"completed_at": "2025-12-08T10:30:14Z"
}
4. Content Update Detection
POST /ingest/check-updates
{
"documents": [
{
"path": "docs-mirror/anthropic-cookbook/skills/citation/guide.md",
"content_hash": "sha256:abc123...",
"updated_at": "2025-12-08T10:30:00Z"
}
]
}
Response:
{
"updates_needed": [
{
"path": "docs-mirror/anthropic-cookbook/skills/citation/guide.md",
"reason": "content_changed",
"last_indexed": "2025-12-07T10:30:00Z",
"action": "re-index"
}
],
"up_to_date": [],
"new_documents": []
}
5. Deduplication Check
POST /deduplicate/check
{
"document_id": "doc_abc123xyz",
"similarity_threshold": 0.85
}
Response:
{
"duplicates": [
{
"document_id": "doc_def456",
"similarity": 0.92,
"path": "docs-mirror/claude-docs/citation-best-practices.md",
"overlap_summary": "Both documents cover citation formatting"
}
],
"suggestions": {
"action": "merge_or_cross_reference",
"confidence": 0.88
}
}
6. Repository Sync Status
GET /ingest/repo-status/{repository_name}
Response:
{
"repository": "anthropic-cookbook",
"total_documents": 156,
"indexed_documents": 156,
"pending_updates": 0,
"failed_documents": 0,
"last_sync": "2025-12-08T03:00:00Z",
"next_scheduled_sync": "2025-12-09T03:00:00Z"
}
Scheduler Integration Workflow
Phase 1: Mirror Documentation (Current)
# This already exists in the Scheduler
async def mirror_documentation():
"""Mirror docs from external sources to Gitea"""
repos = [
"anthropics/anthropic-cookbook",
"anthropics/prompt-eng-interactive-tutorial",
# ... etc
]
for repo in repos:
# Clone/pull to /docs-mirror/{repo-name}
await git_sync(repo, f"/docs-mirror/{repo}")
Phase 2: Index to Library (New)
async def index_to_library():
"""Send mirrored docs to Library Desk for indexing"""
# Get list of documents in docs-mirror
docs_path = Path("/docs-mirror")
for repo_dir in docs_path.iterdir():
if not repo_dir.is_dir():
continue
# Find all markdown files
md_files = list(repo_dir.rglob("*.md"))
# Check what needs updating
update_check = await check_library_updates(md_files)
if update_check["updates_needed"]:
# Batch ingest updated documents
await batch_ingest_documents(
repository=repo_dir.name,
documents=update_check["updates_needed"]
)
# Wait for processing to complete
await wait_for_batch_completion(batch_id)
# Check for duplicates
await check_and_resolve_duplicates(repo_dir.name)
Phase 3: Monitor & Maintain
async def maintain_library_index():
"""Periodic maintenance of Library index"""
# Check for orphaned entries (deleted from source)
await cleanup_orphaned_documents()
# Update embeddings if model changed
await refresh_embeddings_if_needed()
# Generate relationship maps for new content
await discover_document_relationships()
Scheduler Task Definition
New Task: library_sync
Task Name: library_sync
Description: Sync mirrored documentation to Library for indexing and search
Schedule: Daily at 03:30 (after doc mirroring at 03:00)
Priority: 15 (user maintenance)
Service: library
Executor: scheduler.tasks.library_tasks.sync_library_index
Dependencies:
- docs_mirror (must complete first)
Configuration:
- LIBRARY_DESK_URL: http://library-desk:8089
- LIBRARY_API_KEY: ${LIBRARY_API_KEY}
- BATCH_SIZE: 50
- CHECK_UPDATES_ONLY: true
Outputs:
- Documents indexed
- Duplicates found
- Relationships created
API Client Example
# scheduler/src/clients/library_desk.py
import httpx
from typing import List, Dict, Any
from pathlib import Path
class LibraryDeskClient:
def __init__(self, base_url: str, api_key: str):
self.base_url = base_url
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
async def ingest_document(
self,
source: str,
repository: str,
path: str,
content: str,
metadata: Dict[str, Any]
) -> Dict[str, Any]:
"""Ingest a single document"""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/ingest/document",
headers=self.headers,
json={
"source": source,
"repository": repository,
"path": path,
"content": content,
"metadata": metadata
}
)
response.raise_for_status()
return response.json()
async def batch_ingest(
self,
source: str,
repository: str,
documents: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""Ingest multiple documents"""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/ingest/batch",
headers=self.headers,
json={
"source": source,
"repository": repository,
"documents": documents
},
timeout=300.0 # 5 minutes for large batches
)
response.raise_for_status()
return response.json()
async def check_updates(
self,
documents: List[Dict[str, str]]
) -> Dict[str, Any]:
"""Check which documents need updating"""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/ingest/check-updates",
headers=self.headers,
json={"documents": documents}
)
response.raise_for_status()
return response.json()
async def get_repo_status(self, repository: str) -> Dict[str, Any]:
"""Get indexing status for a repository"""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/ingest/repo-status/{repository}",
headers=self.headers
)
response.raise_for_status()
return response.json()
Typical Workflow Sequence
Daily Documentation Sync (03:00-03:45)
-
03:00 - Scheduler runs
docs_mirrortask- Pulls latest from all configured repos
- Writes to
/docs-mirror/*
-
03:30 - Scheduler runs
library_synctask- Scans
/docs-mirror/for changes - Calls
POST /ingest/check-updateswith file hashes - Gets list of updated/new documents
- Scans
-
03:31-03:40 - Batch ingestion
- Groups documents by repo
- Calls
POST /ingest/batchfor each repo - Monitors
GET /ingest/status/{batch_id}
-
03:41-03:44 - Post-processing
- Calls
POST /deduplicate/checkfor new docs - Reviews duplicate suggestions
- Logs statistics to Scheduler database
- Calls
-
03:45 - Completion
- Scheduler marks task complete
- Sends summary to logs
- Updates next run time
Error Handling
Retry Strategy
async def ingest_with_retry(document: Dict, max_retries: int = 3):
"""Ingest with exponential backoff"""
for attempt in range(max_retries):
try:
result = await library_client.ingest_document(**document)
return result
except httpx.TimeoutException:
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt) # 1s, 2s, 4s
else:
# Log failure and continue
logger.error(f"Failed to ingest {document['path']} after {max_retries} attempts")
return None
Graceful Degradation
- If Library Desk is down, queue documents for later ingestion
- Store failed ingestions in Scheduler database
- Retry failed ingestions on next run
Metrics to Track
The Scheduler should track:
- Documents mirrored vs. documents indexed
- Average ingestion time per document
- Deduplication rate
- Failed ingestions
- Library Desk response times
These can be displayed in the Scheduler UI dashboard.
Environment Variables
Add to Scheduler's environment:
# Library Integration
LIBRARY_DESK_URL=http://library-desk:8089
LIBRARY_API_KEY=${LIBRARY_API_KEY}
LIBRARY_BATCH_SIZE=50
LIBRARY_SYNC_ENABLED=true
Next Steps
- Implement ingestion endpoints in Library Desk
- Add LibraryDeskClient to Scheduler
- Create
library_synctask in Scheduler - Test with small batch of documents
- Monitor and tune performance
- Expand to full documentation corpus
Benefits
- Automatic Knowledge Base: All mirrored docs automatically indexed
- Semantic Search: Find docs by meaning, not just keywords
- Relationship Discovery: Understand connections between docs
- Deduplication: Identify overlapping content across repos
- HybridRAG Ready: Knowledge graph + vectors enable advanced AI queries