from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING from densefeed.llm_types import ( # noqa: F401 — re-exported for backward compat EpisodeOutline, MovementPlan, RankedItem, ScoredItem, Verdict, ) if TYPE_CHECKING: from densefeed.config import DenseFeedConfig from densefeed.models import ScriptLine class InferenceBackend(ABC): @abstractmethod async def filter_items(self, items: list[dict]) -> list[Verdict]: """Binary KEEP/DROP filter.""" pass @abstractmethod async def score_items(self, items: list[dict]) -> list[ScoredItem]: """Graded 0-10 scoring.""" pass @abstractmethod async def generate_outline(self, date: str, stories: list[dict]) -> EpisodeOutline: """Generate a structured episode outline.""" pass @abstractmethod async def generate_script_segment( self, date: str, story_title: str, content_chunk: str, previous_context: str | None, segment_type: str, outline_context: str | None, ) -> list[ScriptLine]: """Generate a script segment.""" pass async def filter_score_items(self, items: list[dict]) -> list[dict]: """Collapsed filter+score in a single call. Default implementation delegates to filter_items then score_items. Backends with a native collapsed call (e.g. ZeroGPU) should override. """ verdicts = await self.filter_items(items) keep_ids = {v.i for v in verdicts if v.v == "KEEP"} kept = [item for item in items if item.get("i") in keep_ids] scored = await self.score_items(kept) return [{"i": s.i, "score": s.s, "reason": s.r} for s in scored] def create_backend(config: DenseFeedConfig) -> InferenceBackend: backend_type = config.llm.backend.lower() if backend_type == "job": from .job_backend import JobBackend return JobBackend(config) if backend_type == "baml": from .baml_backend import BAMLBackend return BAMLBackend(config) if backend_type == "zerogpu": from .zerogpu_backend import ZeroGPUBackend return ZeroGPUBackend(config) raise ValueError(f"Unknown LLM backend: {config.llm.backend}")