feat: lower memory dedupe threshold for more aggressive cleaning
This commit is contained in:
+152
-152
@@ -1,152 +1,152 @@
|
||||
"""
|
||||
Mission Control 2.0 — dünner FastAPI-Einstieg.
|
||||
|
||||
Hängt die Router ein, liefert (in Prod) das gebaute React-Frontend aus und
|
||||
setzt eine no-cache-Middleware. Im Dev läuft das Frontend über den Vite-Dev-
|
||||
Server (proxyt /api hierher), daher CORS für localhost offen.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncGenerator, Awaitable, Callable
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.requests import Request
|
||||
|
||||
from config import FRONTEND_DIST, VERSION
|
||||
from routers import (
|
||||
agent,
|
||||
connect,
|
||||
console,
|
||||
gateway_proxy,
|
||||
health,
|
||||
hermes_ui,
|
||||
maintenance,
|
||||
memory,
|
||||
models,
|
||||
routing,
|
||||
system,
|
||||
voice,
|
||||
)
|
||||
from routers import reminders as reminders_router
|
||||
from services import memory as memory_svc
|
||||
from services import reminders, sentry, warmer
|
||||
|
||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||
# für alle Module (logging.getLogger(__name__)).
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
|
||||
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn
|
||||
+ Health-Wächter (meldet Ausfälle/Erholung in den Lucy-Briefkasten und auf Telegram)."""
|
||||
tasks: list[asyncio.Task[Any]] = []
|
||||
if warmer.ENABLED:
|
||||
tasks.append(asyncio.create_task(warmer.rewarm_loop()))
|
||||
log.info(
|
||||
"Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)",
|
||||
warmer.INTERVAL,
|
||||
)
|
||||
if sentry.ENABLED:
|
||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
||||
log.info(
|
||||
"Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
||||
memory_svc.AUTO_DEDUPE_INTERVAL,
|
||||
memory_svc.AUTO_DEDUPE_THRESHOLD,
|
||||
)
|
||||
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
|
||||
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
|
||||
app.state.gw_client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0),
|
||||
limits=httpx.Limits(max_keepalive_connections=100, max_connections=200),
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for task in tasks:
|
||||
_ = task.cancel()
|
||||
await app.state.gw_client.aclose()
|
||||
|
||||
|
||||
app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan)
|
||||
|
||||
# Dev: Vite-Dev-Server (5173) ruft das Backend per /api auf.
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def no_cache(
|
||||
request: Request, call_next: Callable[[Request], Awaitable[httpx.Response]]
|
||||
) -> httpx.Response:
|
||||
resp = await call_next(request)
|
||||
if request.url.path.startswith("/api"):
|
||||
resp.headers["Cache-Control"] = "no-cache"
|
||||
return resp
|
||||
|
||||
|
||||
app.include_router(health.router)
|
||||
app.include_router(models.router)
|
||||
app.include_router(routing.router)
|
||||
app.include_router(system.router)
|
||||
app.include_router(connect.router)
|
||||
app.include_router(memory.router)
|
||||
app.include_router(agent.router)
|
||||
app.include_router(
|
||||
voice.router
|
||||
) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
|
||||
app.include_router(
|
||||
reminders_router.router
|
||||
) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
|
||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||
app.include_router(maintenance.router)
|
||||
app.include_router(
|
||||
console.router
|
||||
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
||||
app.include_router(
|
||||
hermes_ui.router
|
||||
) # Eingebaute Hermes-Web-GUI (hermes serve) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all
|
||||
|
||||
|
||||
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
|
||||
if FRONTEND_DIST.exists():
|
||||
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
|
||||
|
||||
_DIST_ROOT = FRONTEND_DIST.resolve()
|
||||
|
||||
@app.get("/{full_path:path}")
|
||||
def spa(full_path: str):
|
||||
# Datei direkt aus FRONTEND_DIST ausliefern (manifest.webmanifest, favicon.ico, …) — aber
|
||||
# NUR innerhalb des dist-Ordners: Pfad auflösen + Traversal (../, absolute Pfade) hart raus.
|
||||
try:
|
||||
target = (FRONTEND_DIST / full_path).resolve()
|
||||
if target.is_relative_to(_DIST_ROOT) and target.is_file():
|
||||
return FileResponse(target)
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
index = FRONTEND_DIST / "index.html"
|
||||
if index.exists():
|
||||
# index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle.
|
||||
return FileResponse(
|
||||
index, headers={"Cache-Control": "no-cache, must-revalidate"}
|
||||
)
|
||||
return {"detail": "frontend not built"}
|
||||
"""
|
||||
Mission Control 2.0 — dünner FastAPI-Einstieg.
|
||||
|
||||
Hängt die Router ein, liefert (in Prod) das gebaute React-Frontend aus und
|
||||
setzt eine no-cache-Middleware. Im Dev läuft das Frontend über den Vite-Dev-
|
||||
Server (proxyt /api hierher), daher CORS für localhost offen.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncGenerator, Awaitable, Callable
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.requests import Request
|
||||
|
||||
from config import FRONTEND_DIST, VERSION
|
||||
from routers import (
|
||||
agent,
|
||||
connect,
|
||||
console,
|
||||
gateway_proxy,
|
||||
health,
|
||||
hermes_ui,
|
||||
maintenance,
|
||||
memory,
|
||||
models,
|
||||
routing,
|
||||
system,
|
||||
voice,
|
||||
)
|
||||
from routers import reminders as reminders_router
|
||||
from services import memory as memory_svc
|
||||
from services import reminders, sentry, warmer
|
||||
|
||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||
# für alle Module (logging.getLogger(__name__)).
|
||||
logging.basicConfig(
|
||||
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
|
||||
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn
|
||||
+ Health-Wächter (meldet Ausfälle/Erholung in den Lucy-Briefkasten und auf Telegram)."""
|
||||
tasks: list[asyncio.Task[Any]] = []
|
||||
if warmer.ENABLED:
|
||||
tasks.append(asyncio.create_task(warmer.rewarm_loop()))
|
||||
log.info(
|
||||
"Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)",
|
||||
warmer.INTERVAL,
|
||||
)
|
||||
if sentry.ENABLED:
|
||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
||||
log.info(
|
||||
"Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
||||
memory_svc.AUTO_DEDUPE_INTERVAL,
|
||||
memory_svc.AUTO_DEDUPE_THRESHOLD,
|
||||
)
|
||||
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
|
||||
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
|
||||
app.state.gw_client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0),
|
||||
limits=httpx.Limits(max_keepalive_connections=100, max_connections=200),
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for task in tasks:
|
||||
_ = task.cancel()
|
||||
await app.state.gw_client.aclose()
|
||||
|
||||
|
||||
app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan)
|
||||
|
||||
# Dev: Vite-Dev-Server (5173) ruft das Backend per /api auf.
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def no_cache(
|
||||
request: Request, call_next: Callable[[Request], Awaitable[httpx.Response]]
|
||||
) -> httpx.Response:
|
||||
resp = await call_next(request)
|
||||
if request.url.path.startswith("/api"):
|
||||
resp.headers["Cache-Control"] = "no-cache"
|
||||
return resp
|
||||
|
||||
|
||||
app.include_router(health.router)
|
||||
app.include_router(models.router)
|
||||
app.include_router(routing.router)
|
||||
app.include_router(system.router)
|
||||
app.include_router(connect.router)
|
||||
app.include_router(memory.router)
|
||||
app.include_router(agent.router)
|
||||
app.include_router(
|
||||
voice.router
|
||||
) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
|
||||
app.include_router(
|
||||
reminders_router.router
|
||||
) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
|
||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||
app.include_router(maintenance.router)
|
||||
app.include_router(
|
||||
console.router
|
||||
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
||||
app.include_router(
|
||||
hermes_ui.router
|
||||
) # Eingebaute Hermes-Web-GUI (hermes serve) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all
|
||||
|
||||
|
||||
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
|
||||
if FRONTEND_DIST.exists():
|
||||
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
|
||||
|
||||
_DIST_ROOT = FRONTEND_DIST.resolve()
|
||||
|
||||
@app.get("/{full_path:path}")
|
||||
def spa(full_path: str):
|
||||
# Datei direkt aus FRONTEND_DIST ausliefern (manifest.webmanifest, favicon.ico, …) — aber
|
||||
# NUR innerhalb des dist-Ordners: Pfad auflösen + Traversal (../, absolute Pfade) hart raus.
|
||||
try:
|
||||
target = (FRONTEND_DIST / full_path).resolve()
|
||||
if target.is_relative_to(_DIST_ROOT) and target.is_file():
|
||||
return FileResponse(target)
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
index = FRONTEND_DIST / "index.html"
|
||||
if index.exists():
|
||||
# index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle.
|
||||
return FileResponse(
|
||||
index, headers={"Cache-Control": "no-cache, must-revalidate"}
|
||||
)
|
||||
return {"detail": "frontend not built"}
|
||||
|
||||
Reference in New Issue
Block a user