refactor: Phase 8 — backend deletion (chat / voice / push / journal / curator)

Mega-commit. Strips all server-side LLM machinery now that Phase 7 has
removed the corresponding UI surfaces and the MCP HTTP endpoint is the
sole assistant interface.

Deleted (services/):
  chat, generation_buffer, generation_log, generation_task, llm, tools/
  (entire package), stt, tts, voice_config, voice_library, push,
  journal_closeout, journal_pipeline, journal_prep, journal_scheduler,
  journal_search, curator, curator_scheduler, consolidation,
  tag_suggestions, research, weather, article_fetcher, pending_actions,
  moments, assist, wikipedia.

Deleted (routes/):
  chat, voice, push, journal, quick_capture, fable_mcp_dist.

Deleted (models/):
  conversation, generation_tool_log, push_subscription,
  pending_curator_action, moment, weather_cache.

Deleted (tests/):
  test_generation_log, test_journal_*, test_consolidation, test_lookup_tool,
  test_notes_consolidation_trigger, test_record_moment_guards,
  test_research_pipeline, test_tools_*, test_tool_use_fixes,
  test_voice_library, test_weather_service, test_calendar_tool_tz,
  test_wikipedia.

Deleted (top-level):
  fable-mcp/ (legacy standalone stdio package — wheel-build pipeline
  also removed from Dockerfile).

app.py:
  - blueprint registrations for the 6 deleted routes
  - startup hook trimmed: no more Ollama warmup, KV-cache priming,
    journal/curator schedulers, voice model loading
  - shutdown hook simplified
  - httpx import dropped (was for Ollama calls)

pyproject.toml:
  - removed deps: pywebpush, feedparser, html2text, trafilatura
  - removed [voice] extras entirely
  - description updated for the MCP-first architecture

Dockerfile:
  - removed faster-whisper / piper-tts install steps
  - removed bundled piper voice download stage
  - removed fable-mcp wheel build stage

Surviving-file edits:
  - services/auth.py: drop Conversation table claim on first-user setup
  - services/backup.py: drop conversation / push-subscription export+restore;
    v1/v2 restore now silently skip pre-pivot conversation data
  - services/notes.py: drop maybe_consolidate trigger on task done/cancelled;
    drop _maybe_trigger_project_summary (LLM auto-summary)
  - services/projects.py: drop generate_project_summary + backfill_project_summaries
    (both LLM-driven)
  - services/user_profile.py: drop append_observations / consolidate /
    clear_learned_data (curator-tied) and build_profile_context
    (was LLM system-prompt builder)
  - services/notifications.py: stub out _fire_push_notif (was send_push_notification)
  - services/event_scheduler.py: drop event-reminder push + chat-retention
    cleanup job; keep CalDAV pull-sync + reminders job (in-app)
  - services/diagnostics.py: _curator_busy() always False
  - routes/notes.py: drop /assist, /assist/stream, /suggest-tags endpoints
  - routes/tasks.py: drop /<id>/consolidate endpoint
  - routes/settings.py: drop /models, KV-cache-prime-on-save, journal-schedule
    timezone hook, and the SearXNG search-test endpoint; inline _is_private_url
    (was in services/llm.py)
  - routes/admin.py: drop /voice, /voice/reload endpoints
  - routes/profile.py: drop /consolidate, /observations (GET, DELETE)
  - models/__init__.py: drop the 6 dead model imports

Frontend cascade:
  - stores/push.ts: deleted entirely (no callers after Phase 7)
  - stores/settings.ts: drop checkVoiceStatus + voice-status state
  - views/SettingsView.vue: drop Locations section + journalConfig state
    (was tied to /api/journal/config); drop JournalConfig + journal/voice
    api/client imports
  - frontend/api/client.ts: orphaned voice/journal/profile-observation/
    fable-mcp-dist exports are left as dead but harmless (call them and
    they 404; type-check is clean).

Pre-existing v1 backups that contained conversations/messages still
restore — those tables are silently dropped from the import path.
Anyone pulling the new image with a populated database will need the
Phase 9 migration to drop the dead tables (coming next).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 17:47:18 -04:00
parent 8bec68abc0
commit 91bafb641f
123 changed files with 161 additions and 19312 deletions
+4 -190
View File
@@ -3,25 +3,18 @@ import time
import traceback as tb_module
from pathlib import Path
import httpx
from quart import Quart, g, jsonify, make_response, request, send_from_directory
from fabledassistant.config import Config
from fabledassistant.routes.admin import admin_bp
from fabledassistant.routes.api import api
from fabledassistant.routes.auth import auth_bp
from fabledassistant.routes.chat import chat_bp
from fabledassistant.routes.journal import journal_bp
from fabledassistant.routes.export import export_bp
from fabledassistant.routes.notes import notes_bp
from fabledassistant.routes.images import images_bp
from fabledassistant.routes.milestones import milestones_bp
from fabledassistant.routes.task_logs import task_logs_bp
from fabledassistant.routes.projects import projects_bp
from fabledassistant.routes.push import push_bp
from fabledassistant.routes.fable_mcp_dist import fable_mcp_dist_bp
from fabledassistant.routes.quick_capture import quick_capture_bp
from fabledassistant.routes.settings import settings_bp
from fabledassistant.routes.tasks import tasks_bp
from fabledassistant.routes.groups import groups_bp
@@ -31,7 +24,6 @@ from fabledassistant.routes.users import users_bp
from fabledassistant.routes.api_keys import api_keys_bp
from fabledassistant.routes.events import events_bp
from fabledassistant.routes.search import search_bp
from fabledassistant.routes.voice import voice_bp
from fabledassistant.routes.profile import profile_bp
from fabledassistant.routes.knowledge import knowledge_bp
from fabledassistant.mcp import mount_mcp
@@ -76,16 +68,11 @@ def create_app() -> Quart:
app.register_blueprint(admin_bp)
app.register_blueprint(api)
app.register_blueprint(auth_bp)
app.register_blueprint(chat_bp)
app.register_blueprint(journal_bp)
app.register_blueprint(export_bp)
app.register_blueprint(images_bp)
app.register_blueprint(milestones_bp)
app.register_blueprint(notes_bp)
app.register_blueprint(fable_mcp_dist_bp)
app.register_blueprint(projects_bp)
app.register_blueprint(push_bp)
app.register_blueprint(quick_capture_bp)
app.register_blueprint(settings_bp)
app.register_blueprint(task_logs_bp)
app.register_blueprint(tasks_bp)
@@ -96,7 +83,6 @@ def create_app() -> Quart:
app.register_blueprint(api_keys_bp)
app.register_blueprint(events_bp)
app.register_blueprint(search_bp)
app.register_blueprint(voice_bp)
app.register_blueprint(profile_bp)
app.register_blueprint(knowledge_bp)
@@ -161,218 +147,46 @@ def create_app() -> Quart:
import asyncio
from fabledassistant.services.embeddings import backfill_note_embeddings
from fabledassistant.services.generation_buffer import start_cleanup_loop
from fabledassistant.services.llm import ensure_model
from fabledassistant.services.logging import start_log_retention_loop
from fabledassistant.services.notifications import start_notification_loop
from fabledassistant.services.push import ensure_vapid_keys
start_cleanup_loop()
start_log_retention_loop()
start_notification_loop()
ensure_vapid_keys()
async def _warm_model(model: str) -> None:
"""Warm an already-installed model into VRAM (no pull)."""
from fabledassistant.services.llm import keep_alive_for
try:
async with httpx.AsyncClient(timeout=300.0) as client:
await client.post(
f"{Config.OLLAMA_URL}/api/generate",
json={"model": model, "prompt": "", "keep_alive": keep_alive_for(model)},
)
logger.info("Warmed model '%s' into VRAM", model)
except Exception:
logger.warning("Failed to warm model '%s'", model, exc_info=True)
async def _prime_kv_cache(user_id: int, model: str) -> None:
"""Send a minimal chat request to prime Ollama's KV cache with the user's system prompt.
This ensures the next real user message only needs to process its own tokens
rather than the full ~4,650-token system prompt, cutting TTFT from ~25s to <1s.
The num_ctx must match what real requests will use so Ollama doesn't reload.
"""
try:
from fabledassistant.services.llm import build_context, keep_alive_for, pick_num_ctx
from fabledassistant.services.tools import get_tools_for_user
messages, _ = await build_context(
user_id=user_id,
history=[],
current_note_id=None,
user_message=" ",
)
# Include tool schemas so num_ctx matches real chat requests.
tools = await get_tools_for_user(user_id)
num_ctx = pick_num_ctx(messages, tools=tools)
async with httpx.AsyncClient(timeout=120.0) as client:
await client.post(
f"{Config.OLLAMA_URL}/api/chat",
json={
"model": model,
"messages": messages,
"stream": False,
"options": {"num_predict": 1, "num_ctx": num_ctx},
"keep_alive": keep_alive_for(model),
},
)
logger.info("Primed KV cache for user %d with model '%s' (num_ctx=%d)", user_id, model, num_ctx)
except Exception:
logger.warning("Failed to prime KV cache for user %d", user_id, exc_info=True)
async def _warm_user_models() -> None:
"""
Pull any user-configured models that are missing from Ollama, then warm
them and prime the KV cache with each user's system prompt.
Handles both default_model (chat) and background_model user overrides.
Falls back silently if no user preferences exist or Ollama is unreachable.
"""
from sqlalchemy import select as sa_select
from fabledassistant.models import async_session
from fabledassistant.models.setting import Setting
# 1. Collect all user model preferences (both chat and background).
try:
async with async_session() as session:
rows = await session.execute(
sa_select(Setting.user_id, Setting.key, Setting.value).where(
Setting.key.in_(["default_model", "background_model"]),
Setting.value.isnot(None),
Setting.value != "",
)
)
settings_rows: list[tuple[int, str, str]] = list(rows)
except Exception:
logger.debug("Could not read user model preferences from DB", exc_info=True)
return
if not settings_rows:
logger.debug("No user model preferences found; skipping warm-up")
return
# 2. Build the set of unique models to ensure, and the list of
# (user_id, chat_model) pairs for KV-cache priming.
all_models: set[str] = set()
user_chat_models: list[tuple[int, str]] = []
for user_id_val, key, model in settings_rows:
all_models.add(model)
if key == "default_model":
user_chat_models.append((user_id_val, model))
# 3. Ask Ollama which models are currently installed.
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(f"{Config.OLLAMA_URL}/api/tags")
resp.raise_for_status()
raw_installed: set[str] = {m["name"] for m in resp.json().get("models", [])}
installed: set[str] = raw_installed | {
n.removesuffix(":latest") for n in raw_installed if n.endswith(":latest")
}
except Exception:
logger.debug("Could not reach Ollama to check installed models", exc_info=True)
return
# 4. Pull any user-configured models that are missing.
for model in all_models:
if model not in installed:
logger.info("User-configured model '%s' not installed; pulling...", model)
await _pull_model(model)
installed.add(model)
# 5. Warm each unique chat model, then prime KV cache per user.
warmed: set[str] = set()
for user_id_val, model in user_chat_models:
if model in installed:
if model not in warmed:
await _warm_model(model)
warmed.add(model)
await _prime_kv_cache(user_id_val, model)
async def _pull_model(model: str, warm: bool = False) -> None:
try:
await ensure_model(model)
except Exception:
logger.warning(
"Failed to ensure model '%s'",
model,
exc_info=True,
)
return
if warm:
await _warm_model(model)
# Pull supporting models without warming them — embedding model and
# the configured background model load on demand without competing
# for VRAM. The chat model is warmed by _warm_user_models() which
# uses each user's *actual* default_model setting; warming
# Config.OLLAMA_MODEL unconditionally (the OLD behaviour) blasted
# the system default into VRAM ahead of the user's real preference,
# which on a single-GPU setup pushed the user's chat model out
# before they ever sent a message.
asyncio.create_task(_pull_model(Config.OLLAMA_MODEL, warm=False))
asyncio.create_task(_pull_model(Config.EMBEDDING_MODEL, warm=False))
asyncio.create_task(_pull_model(Config.OLLAMA_BACKGROUND_MODEL, warm=False))
asyncio.create_task(_warm_user_models())
# After models are pulled, backfill embeddings for existing notes.
# Runs in the background so it never blocks the server from accepting requests.
# Backfill embeddings for any notes that don't have one. Runs in the
# background so it never blocks the server from accepting requests.
async def _delayed_backfill() -> None:
await asyncio.sleep(30) # Give Ollama time to load the embedding model
try:
await backfill_note_embeddings()
except Exception:
logger.warning("Embedding backfill failed", exc_info=True)
try:
from fabledassistant.services.projects import backfill_project_summaries
await backfill_project_summaries()
except Exception:
logger.warning("Project summary backfill failed", exc_info=True)
asyncio.create_task(_delayed_backfill())
# Start journal scheduler (per-user daily prep generation)
from fabledassistant.services.journal_scheduler import start_journal_scheduler
start_journal_scheduler(asyncio.get_running_loop())
# Start event scheduler (reminders + CalDAV pull sync)
# Event scheduler (reminders + CalDAV pull sync)
from fabledassistant.services.event_scheduler import start_event_scheduler
start_event_scheduler(asyncio.get_running_loop())
# Start version-pinning scheduler (daily auto-pin scan at 03:00 UTC)
# Version-pinning scheduler (daily auto-pin scan at 03:00 UTC)
from fabledassistant.services.version_pinning_scheduler import (
start_version_pinning_scheduler,
)
start_version_pinning_scheduler(asyncio.get_running_loop())
# Start curator scheduler (15-min sweep of journal conversations)
from fabledassistant.services.curator_scheduler import start_curator_scheduler
start_curator_scheduler(asyncio.get_running_loop())
# Diagnostic instrumentation — heartbeat, signal handlers, asyncio
# exception hook. Cheap (~1 log line/min), high diagnostic value when
# the app crashes mysteriously. See services/diagnostics.py.
from fabledassistant.services.diagnostics import start_diagnostics
start_diagnostics(asyncio.get_running_loop())
# Voice model loading (enabled via Admin → Config in the UI, or VOICE_ENABLED env var)
from fabledassistant.services.stt import load_stt_model
from fabledassistant.services.tts import load_tts_model
asyncio.create_task(load_stt_model())
asyncio.create_task(load_tts_model())
@app.after_serving
async def shutdown():
from fabledassistant.services.journal_scheduler import stop_journal_scheduler
stop_journal_scheduler()
from fabledassistant.services.event_scheduler import stop_event_scheduler
stop_event_scheduler()
from fabledassistant.services.version_pinning_scheduler import (
stop_version_pinning_scheduler,
)
stop_version_pinning_scheduler()
from fabledassistant.services.curator_scheduler import stop_curator_scheduler
stop_curator_scheduler()
from fabledassistant.services.diagnostics import stop_diagnostics
stop_diagnostics()