"""Author RAG Chatbot SaaS — Token-Aware Context Builder. Assembles retrieved chunks into a formatted context string that fits within the configured token budget. RULE: Hard limit — never exceed RAG_MAX_CONTEXT_TOKENS. RULE: Include book title for each chunk to help model cross-book navigation. """ import structlog from app.config import get_settings from app.services.vector_store import RetrievedChunk from app.utils.token_counter import count_tokens logger = structlog.get_logger(__name__) cfg = get_settings() def build_context( chunks: list[RetrievedChunk], max_tokens: int | None = None, ) -> tuple[str, int]: """Build a formatted context string from retrieved chunks. Includes as many chunks as fit within the token budget (best first). Each chunk is formatted with a book title header for clarity. Args: chunks: Re-ranked list of RetrievedChunk objects (best first). max_tokens: Max tokens for the context block. Returns: Tuple of (context_string, total_tokens_used). """ max_tokens = max_tokens or cfg.RAG_MAX_CONTEXT_TOKENS included_chunks: list[str] = [] total_tokens = 0 for chunk in chunks: formatted = _format_chunk(chunk) chunk_tokens = count_tokens(formatted) if total_tokens + chunk_tokens > max_tokens: logger.debug( "Context token budget reached", included=len(included_chunks), excluded_remaining=len(chunks) - len(included_chunks), ) break included_chunks.append(formatted) total_tokens += chunk_tokens if not included_chunks: return "", 0 context = "\n\n---\n\n".join(included_chunks) logger.debug("Context built", chunks=len(included_chunks), tokens=total_tokens) return context, total_tokens def _format_chunk(chunk: RetrievedChunk) -> str: """Format a single chunk with its book title header. Args: chunk: RetrievedChunk to format. Returns: Formatted string with book title and chunk text. """ return f"[From: {chunk.book_title}]\n{chunk.text.strip()}"