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:
+4
-190
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user