Files
life-echo/api/app/features/memoir/ingest_scheduler.py

78 lines
2.1 KiB
Python
Raw Normal View History

"""Memoir ingest scheduling boundary.
The real batching logic still lives in ``BackgroundTaskRunner``. This adapter
keeps conversation code from depending on that implementation directly.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal, Sequence
from app.features.memoir.background_runner import BackgroundTaskRunner
MemoirTrigger = Literal[
"turn",
"conversation_end",
"manual_flush",
"evaluation_replay",
]
@dataclass(frozen=True)
class MemoirPhasePlan:
"""A visible plan for submitting segments into the memoir pipeline."""
user_id: str
segment_ids: tuple[str, ...]
trigger: MemoirTrigger
class MemoirIngestScheduler:
"""Small facade over debounce batching and Phase2 flush dispatch."""
def __init__(self, runner: BackgroundTaskRunner | None = None) -> None:
self._runner = runner or BackgroundTaskRunner()
@property
def runner(self) -> BackgroundTaskRunner:
"""Compatibility escape hatch for existing tests and eval utilities."""
return self._runner
async def queue_segment(
self,
user_id: str,
segment_id: str,
*,
text_char_count: int = 0,
trigger: MemoirTrigger = "turn",
) -> MemoirPhasePlan:
await self._runner.queue_message(
user_id,
segment_id,
text_char_count=text_char_count,
)
return MemoirPhasePlan(
user_id=user_id,
segment_ids=(segment_id,),
trigger=trigger,
)
async def flush_pending(
self,
user_id: str,
*,
extra_segment_ids: Sequence[str] | None = None,
trigger: MemoirTrigger = "manual_flush",
) -> tuple[MemoirPhasePlan, str | None]:
ids = tuple(str(x) for x in (extra_segment_ids or ()) if str(x).strip())
task_id = await self._runner.flush_pending(
user_id,
extra_segment_ids=list(ids),
)
return MemoirPhasePlan(user_id=user_id, segment_ids=ids, trigger=trigger), task_id
__all__ = ["MemoirIngestScheduler", "MemoirPhasePlan", "MemoirTrigger"]