Files
ai-agent/backend/core/memo.py
Gabriel Vidal 0ff9e40242 refactor(backend): split main.py into domain packages with app.state injection
main.py (5146 lines, 106 routes) becomes an assembly only: one package per
domain — core, files, settings, dashboards, conversations, diff,
notifications, forms, runs, accounts, workers, models, cron, agents,
projects, services, goals, memories, plans, templates — each exposing an
APIRouter; the flat domain modules move into their package behind a barrel
that keeps the old `import conversations` / `import projects` spellings.

The shared singletons (store, meta_store, hub, indexer, …) are built once by
core.state.build_state() and attached to app.state.ai; routes take them as
the `deps: State` dependency and helpers as an explicit `deps: AppState`.
conversations/pricing.py carries the per-model rates out of the parser.

Verified: route table and OpenAPI byte-identical; 90 read endpoints
golden-diffed against the monolith on a copy of the live data (identical);
write routes smoke-tested; 66 backend tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 23:55:47 +02:00

31 lines
1.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""A build-once-per-version memo shared by the catalog rollups."""
import threading
class _Memo:
"""One expensive whole-catalog build, memoized on a version key.
Every list/detail refetch the SSE bus triggers lands right after a
transcript write (a version bump), so several requests used to miss the
cache together and each run the same full build in parallel — N clients =
N× the CPU on one GIL. Callers with the same key now share a single build:
the first builds, the rest wait on the lock and take its result."""
def __init__(self, build):
self._build = build
self._lock = threading.Lock()
self._hit: tuple | None = None # (key, value), swapped atomically
def get(self, key, *args):
hit = self._hit
if hit and hit[0] == key:
return hit[1]
with self._lock:
hit = self._hit
if hit and hit[0] == key:
return hit[1]
val = self._build(*args)
self._hit = (key, val)
return val