feat: cleanup endpoint, auto-extraction on capture, daily cron script
- POST /admin/backup/cleanup — retention cleanup via API (dry-run by default) - record_interaction() accepts extract=True to auto-extract candidate memories from response text using the Phase 9C rule-based extractor - POST /interactions accepts extract field to enable extraction on capture - deploy/dalidou/cron-backup.sh — daily backup + cleanup for cron Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
56
deploy/dalidou/cron-backup.sh
Normal file
56
deploy/dalidou/cron-backup.sh
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# deploy/dalidou/cron-backup.sh
|
||||||
|
# ------------------------------
|
||||||
|
# Daily backup + retention cleanup via the AtoCore API.
|
||||||
|
#
|
||||||
|
# Intended to run from cron on Dalidou:
|
||||||
|
#
|
||||||
|
# # Daily at 03:00 UTC
|
||||||
|
# 0 3 * * * /srv/storage/atocore/app/deploy/dalidou/cron-backup.sh >> /var/log/atocore-backup.log 2>&1
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Creates a runtime backup (db + registry, no chroma by default)
|
||||||
|
# 2. Runs retention cleanup with --confirm to delete old snapshots
|
||||||
|
# 3. Logs results to stdout (captured by cron into the log file)
|
||||||
|
#
|
||||||
|
# Fail-open: exits 0 even on API errors so cron doesn't send noise
|
||||||
|
# emails. Check /var/log/atocore-backup.log for diagnostics.
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# ATOCORE_URL default http://127.0.0.1:8100
|
||||||
|
# ATOCORE_BACKUP_CHROMA default false (set to "true" for cold chroma copy)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ATOCORE_URL="${ATOCORE_URL:-http://127.0.0.1:8100}"
|
||||||
|
INCLUDE_CHROMA="${ATOCORE_BACKUP_CHROMA:-false}"
|
||||||
|
TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
|
||||||
|
log() { printf '[%s] %s\n' "$TIMESTAMP" "$*"; }
|
||||||
|
|
||||||
|
log "=== AtoCore daily backup starting ==="
|
||||||
|
|
||||||
|
# Step 1: Create backup
|
||||||
|
log "Step 1: creating backup (chroma=$INCLUDE_CHROMA)"
|
||||||
|
BACKUP_RESULT=$(curl -sf -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"include_chroma\": $INCLUDE_CHROMA}" \
|
||||||
|
"$ATOCORE_URL/admin/backup" 2>&1) || {
|
||||||
|
log "ERROR: backup creation failed: $BACKUP_RESULT"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
log "Backup created: $BACKUP_RESULT"
|
||||||
|
|
||||||
|
# Step 2: Retention cleanup (confirm=true to actually delete)
|
||||||
|
log "Step 2: running retention cleanup"
|
||||||
|
CLEANUP_RESULT=$(curl -sf -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"confirm": true}' \
|
||||||
|
"$ATOCORE_URL/admin/backup/cleanup" 2>&1) || {
|
||||||
|
log "ERROR: cleanup failed: $CLEANUP_RESULT"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
log "Cleanup result: $CLEANUP_RESULT"
|
||||||
|
|
||||||
|
log "=== AtoCore daily backup complete ==="
|
||||||
@@ -49,6 +49,7 @@ from atocore.memory.service import (
|
|||||||
)
|
)
|
||||||
from atocore.observability.logger import get_logger
|
from atocore.observability.logger import get_logger
|
||||||
from atocore.ops.backup import (
|
from atocore.ops.backup import (
|
||||||
|
cleanup_old_backups,
|
||||||
create_runtime_backup,
|
create_runtime_backup,
|
||||||
list_runtime_backups,
|
list_runtime_backups,
|
||||||
validate_backup,
|
validate_backup,
|
||||||
@@ -511,6 +512,7 @@ class InteractionRecordRequest(BaseModel):
|
|||||||
chunks_used: list[str] = []
|
chunks_used: list[str] = []
|
||||||
context_pack: dict | None = None
|
context_pack: dict | None = None
|
||||||
reinforce: bool = True
|
reinforce: bool = True
|
||||||
|
extract: bool = False
|
||||||
|
|
||||||
|
|
||||||
@router.post("/interactions")
|
@router.post("/interactions")
|
||||||
@@ -536,6 +538,7 @@ def api_record_interaction(req: InteractionRecordRequest) -> dict:
|
|||||||
chunks_used=req.chunks_used,
|
chunks_used=req.chunks_used,
|
||||||
context_pack=req.context_pack,
|
context_pack=req.context_pack,
|
||||||
reinforce=req.reinforce,
|
reinforce=req.reinforce,
|
||||||
|
extract=req.extract,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
@@ -731,6 +734,25 @@ def api_list_backups() -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BackupCleanupRequest(BaseModel):
|
||||||
|
confirm: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/admin/backup/cleanup")
|
||||||
|
def api_cleanup_backups(req: BackupCleanupRequest | None = None) -> dict:
|
||||||
|
"""Apply retention policy to old backup snapshots.
|
||||||
|
|
||||||
|
Dry-run by default. Pass ``confirm: true`` to actually delete.
|
||||||
|
Retention: last 7 daily, last 4 weekly (Sundays), last 6 monthly (1st).
|
||||||
|
"""
|
||||||
|
payload = req or BackupCleanupRequest()
|
||||||
|
try:
|
||||||
|
return cleanup_old_backups(confirm=payload.confirm)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("admin_cleanup_failed", error=str(e))
|
||||||
|
raise HTTPException(status_code=500, detail=f"Cleanup failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin/backup/{stamp}/validate")
|
@router.get("/admin/backup/{stamp}/validate")
|
||||||
def api_validate_backup(stamp: str) -> dict:
|
def api_validate_backup(stamp: str) -> dict:
|
||||||
"""Validate that a previously created backup is structurally usable."""
|
"""Validate that a previously created backup is structurally usable."""
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ def record_interaction(
|
|||||||
chunks_used: list[str] | None = None,
|
chunks_used: list[str] | None = None,
|
||||||
context_pack: dict | None = None,
|
context_pack: dict | None = None,
|
||||||
reinforce: bool = True,
|
reinforce: bool = True,
|
||||||
|
extract: bool = False,
|
||||||
) -> Interaction:
|
) -> Interaction:
|
||||||
"""Persist a single interaction to the audit trail.
|
"""Persist a single interaction to the audit trail.
|
||||||
|
|
||||||
@@ -163,6 +164,30 @@ def record_interaction(
|
|||||||
error=str(exc),
|
error=str(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if extract and (response or response_summary):
|
||||||
|
try:
|
||||||
|
from atocore.memory.extractor import extract_candidates_from_interaction
|
||||||
|
from atocore.memory.service import create_memory
|
||||||
|
|
||||||
|
candidates = extract_candidates_from_interaction(interaction)
|
||||||
|
for candidate in candidates:
|
||||||
|
try:
|
||||||
|
create_memory(
|
||||||
|
memory_type=candidate.memory_type,
|
||||||
|
content=candidate.content,
|
||||||
|
project=candidate.project,
|
||||||
|
confidence=candidate.confidence,
|
||||||
|
status="candidate",
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass # duplicate or validation error — skip silently
|
||||||
|
except Exception as exc: # pragma: no cover - extraction must never block capture
|
||||||
|
log.error(
|
||||||
|
"extraction_failed_on_capture",
|
||||||
|
interaction_id=interaction_id,
|
||||||
|
error=str(exc),
|
||||||
|
)
|
||||||
|
|
||||||
return interaction
|
return interaction
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user