"""The Mode protocol. Every interaction type implements `Mode` and yields `ModeEvent`s; the conversations router converts them 1:1 into SSE. Modes know nothing about HTTP; routers know nothing about mode logic. Capture is NOT a Mode: it is writing into a Document directly (see `app/authoring/`), not a conversation. The only core Mode is query (RAG Q&A); EE registers the insights mode. """ import uuid from collections.abc import AsyncIterator from dataclasses import dataclass, field from typing import Protocol, runtime_checkable from sqlalchemy.ext.asyncio import AsyncSession from app.models import Conversation @dataclass class Token: text: str @dataclass class SourceChunk: document_id: uuid.UUID title: str heading_path: str excerpt: str = "" # Whether this passage was actually passed to the model (grounding the # answer), or only retrieved and then dropped as too weak (the no-answer # path). Drives the "?" context inspector; the cited-source badges show # only `used` chunks. used: bool = True # The document has an unanswered request to check it: readable, but not # settled. Marked on the citation, because trusting an answer means # trusting what it leaned on. review_pending: bool = False @dataclass class Sources: chunks: list[SourceChunk] = field(default_factory=list) @dataclass class StateChanged: """Progress signal for the UI — metadata only. Query mode reports a phase (e.g. "searching" / "no_answer") and a count (documents found). No content ever rides this frame. """ phase: str count: int | None = None @dataclass class Done: """Emitted by the ROUTER after persisting the assistant message. Modes normally end their iterator instead of yielding this.""" message_id: uuid.UUID | None = None @dataclass class Error: """Why the turn failed, as a code the frontend phrases (CLAUDE.md: the backend never renders UI-language strings).""" code: str @dataclass class Degraded: """No model could be reached, so this turn has no generated answer: the accompanying `Sources` are what a plain full-text search found, for the user to read themselves. `code` is the endpoint failure that caused it (`LLMError.code`); the frontend says what it means.""" code: str ModeEvent = Token | Sources | StateChanged | Done | Error | Degraded @runtime_checkable class Mode(Protocol): name: str def handle_turn( self, conversation: Conversation, user_message: str, db: AsyncSession ) -> AsyncIterator[ModeEvent]: ...