Documentation updates: - AGENTS.md: Added wakeup.sh usage and local testing instructions - LIBRARIAN_INTEGRATION.md: Complete maintenance scheduler docs - Scheduled task configuration for reconcile-index - Endpoint specifications and response formats - docs/MEMORY_SYSTEM_PLAN.md: Three-tier memory architecture - Volatile (Redis TTL) for ephemeral context - Documents (TBD) for git mirrors, PDFs, images - Knowledge (Wiki + Neo4j) for permanent research 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
622 lines
17 KiB
Markdown
622 lines
17 KiB
Markdown
# 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**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"source": "github",
|
|
"repository": "anthropics/anthropic-cookbook",
|
|
"documents": [
|
|
{
|
|
"path": "skills/citation/guide.md",
|
|
"content": "...",
|
|
"metadata": {...}
|
|
},
|
|
{
|
|
"path": "skills/summarization/techniques.md",
|
|
"content": "...",
|
|
"metadata": {...}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"documents": [
|
|
{
|
|
"path": "docs-mirror/anthropic-cookbook/skills/citation/guide.md",
|
|
"content_hash": "sha256:abc123...",
|
|
"updated_at": "2025-12-08T10:30:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"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**
|
|
```json
|
|
{
|
|
"document_id": "doc_abc123xyz",
|
|
"similarity_threshold": 0.85
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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)
|
|
```python
|
|
# 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)
|
|
```python
|
|
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
|
|
```python
|
|
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`**
|
|
|
|
```yaml
|
|
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
|
|
|
|
```python
|
|
# 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)
|
|
|
|
1. **03:00** - Scheduler runs `docs_mirror` task
|
|
- Pulls latest from all configured repos
|
|
- Writes to `/docs-mirror/*`
|
|
|
|
2. **03:30** - Scheduler runs `library_sync` task
|
|
- Scans `/docs-mirror/` for changes
|
|
- Calls `POST /ingest/check-updates` with file hashes
|
|
- Gets list of updated/new documents
|
|
|
|
3. **03:31-03:40** - Batch ingestion
|
|
- Groups documents by repo
|
|
- Calls `POST /ingest/batch` for each repo
|
|
- Monitors `GET /ingest/status/{batch_id}`
|
|
|
|
4. **03:41-03:44** - Post-processing
|
|
- Calls `POST /deduplicate/check` for new docs
|
|
- Reviews duplicate suggestions
|
|
- Logs statistics to Scheduler database
|
|
|
|
5. **03:45** - Completion
|
|
- Scheduler marks task complete
|
|
- Sends summary to logs
|
|
- Updates next run time
|
|
|
|
## Error Handling
|
|
|
|
### Retry Strategy
|
|
```python
|
|
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:
|
|
```bash
|
|
# Library Integration
|
|
LIBRARY_DESK_URL=http://library-desk:8089
|
|
LIBRARY_API_KEY=${LIBRARY_API_KEY}
|
|
LIBRARY_BATCH_SIZE=50
|
|
LIBRARY_SYNC_ENABLED=true
|
|
```
|
|
|
|
## Maintenance Tasks
|
|
|
|
### Index Reconciliation (Daily)
|
|
|
|
The `reconcile-index` endpoint performs full index maintenance:
|
|
|
|
1. **Cleanup Phase**: Remove orphaned data
|
|
- Vector chunks without wiki source
|
|
- Graph nodes without vectors (bidirectional)
|
|
- Vectors without graph nodes (bidirectional)
|
|
- Orphan entities (no MENTIONS relationships)
|
|
- Broken relationships
|
|
|
|
2. **Reindex Phase**: Index missing pages
|
|
- Wiki pages without vector embeddings
|
|
- Wiki pages without graph Document nodes
|
|
|
|
**Scheduler Task: `library_reconcile_index`**
|
|
|
|
```yaml
|
|
Task Name: library_reconcile_index
|
|
Description: Daily index reconciliation - cleanup orphans + reindex missing pages
|
|
Schedule: Daily at 04:00 (after library_sync at 03:30)
|
|
Priority: 10 (system maintenance)
|
|
Service: library
|
|
Executor: POST /maintenance/reconcile-index
|
|
Configuration:
|
|
- LIBRARY_DESK_URL: http://library-desk:8089
|
|
- LIBRARY_API_KEY: ${LIBRARY_API_KEY}
|
|
Parameters:
|
|
- user: jpmschweitzer
|
|
- dry_run: false
|
|
Outputs:
|
|
- Vector orphans purged
|
|
- Entity orphans purged
|
|
- Missing pages reindexed
|
|
```
|
|
|
|
### Maintenance Endpoints
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/maintenance/reconcile-index` | POST | **Recommended**: Full cleanup + reindex missing |
|
|
| `/maintenance/cleanup/all` | POST | Cleanup only (orphan removal) |
|
|
| `/maintenance/cleanup/vectors` | POST | Clean orphan vector chunks only |
|
|
| `/maintenance/cleanup/graph` | POST | Clean orphan entities & stale docs only |
|
|
| `/maintenance/health` | GET | Lightweight health check (for uptime monitoring) |
|
|
| `/maintenance/health?detailed=true` | GET | Full analysis with orphan counts |
|
|
| `/maintenance/reindex/{page_id}` | POST | Force re-index a specific page |
|
|
|
|
### Health Check Modes
|
|
|
|
**Lightweight (default)** - Use for frequent uptime checks (every 30s):
|
|
```bash
|
|
curl "http://library-desk:8089/maintenance/health?user=jpmschweitzer" \
|
|
-H "Authorization: Bearer ${LIBRARY_API_KEY}"
|
|
```
|
|
|
|
Returns only last cleanup timestamp and basic status (no database queries).
|
|
|
|
**Detailed** - Use for dashboards or before reconciliation:
|
|
```bash
|
|
curl "http://library-desk:8089/maintenance/health?user=jpmschweitzer&detailed=true" \
|
|
-H "Authorization: Bearer ${LIBRARY_API_KEY}"
|
|
```
|
|
|
|
Returns full orphan analysis (runs database queries).
|
|
|
|
### Example Reconcile Request
|
|
|
|
```bash
|
|
curl -X POST "http://library-desk:8089/maintenance/reconcile-index?user=jpmschweitzer" \
|
|
-H "Authorization: Bearer ${LIBRARY_API_KEY}"
|
|
```
|
|
|
|
### Example Response
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"cleanup": {
|
|
"success": true,
|
|
"vector_cleanup": {
|
|
"wiki_chunks": {"orphans_found": 5, "orphans_purged": 5},
|
|
"document_chunks": {"orphans_found": 0, "orphans_purged": 0},
|
|
"chunks_without_graph": {"orphans_found": 2, "orphans_purged": 2},
|
|
"total_chunks_scanned": 1250,
|
|
"total_orphans_purged": 7
|
|
},
|
|
"graph_cleanup": {
|
|
"orphan_entities": {"orphans_found": 3, "orphans_purged": 3},
|
|
"stale_wiki_documents": {"orphans_found": 1, "orphans_purged": 1},
|
|
"stale_store_documents": {"orphans_found": 0, "orphans_purged": 0},
|
|
"docs_without_vectors": {"orphans_found": 0, "orphans_purged": 0},
|
|
"broken_relationships_cleaned": 0
|
|
},
|
|
"total_duration_ms": 1523.5
|
|
},
|
|
"reindex_missing": {
|
|
"pages_without_vectors": 2,
|
|
"pages_without_graph": 1,
|
|
"pages_reindexed": 2,
|
|
"pages_failed": 0,
|
|
"failed_page_ids": [],
|
|
"duration_ms": 3421.2
|
|
},
|
|
"total_duration_ms": 4944.7
|
|
}
|
|
```
|
|
|
|
### Scheduler Integration Code
|
|
|
|
```python
|
|
# scheduler/src/tasks/library_maintenance.py
|
|
|
|
async def library_reconcile_index_task(user: str = "jpmschweitzer"):
|
|
"""Run daily Library Desk index reconciliation."""
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
# Run reconcile-index (cleanup + reindex missing)
|
|
result = await client.post(
|
|
f"{LIBRARY_DESK_URL}/maintenance/reconcile-index",
|
|
params={"user": user, "dry_run": False},
|
|
headers={"Authorization": f"Bearer {LIBRARY_API_KEY}"},
|
|
timeout=600.0 # 10 minutes for large indexes
|
|
)
|
|
|
|
data = result.json()
|
|
|
|
# Log summary
|
|
cleanup = data["cleanup"]
|
|
reindex = data["reindex_missing"]
|
|
|
|
logger.info(
|
|
f"Reconcile complete: "
|
|
f"{cleanup['vector_cleanup']['total_orphans_purged']} vector orphans, "
|
|
f"{cleanup['graph_cleanup']['orphan_entities']['orphans_purged']} entity orphans, "
|
|
f"{reindex['pages_reindexed']} pages reindexed"
|
|
)
|
|
|
|
if reindex["pages_failed"] > 0:
|
|
logger.warning(f"Failed to reindex pages: {reindex['failed_page_ids']}")
|
|
|
|
return data
|
|
```
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. Implement ingestion endpoints in Library Desk
|
|
2. Add LibraryDeskClient to Scheduler
|
|
3. Create `library_sync` task in Scheduler
|
|
4. Test with small batch of documents
|
|
5. Monitor and tune performance
|
|
6. 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
|