"""Thin, minimal-surface model client for ai-draft. Per NFR design (minimal-surface): this is the ONE narrow caller to the granted model. It builds a minimal prompt from the DraftBrief (content context + tone + per-type max-length note), calls the cloud model, and parses the response into a DraftArticle. It is injectable so tests can substitute a stub (no real network in CI). """ from __future__ import annotations from typing import Protocol from .model import DraftArticle class ModelClient(Protocol): """Interface for the single sanctioned external call.""" def generate(self, brief_text: str, article_type: str, article_id: str) -> DraftArticle: ... class HttpModelClient: """Default client calling the granted cloud model endpoint. Endpoint/alias is supplied by the user via a local config (infrastructure design Q3=A, never committed / hard-coded). Implementation detail for code-generation: a thin HTTP POST of `brief_text` to the configured endpoint, response parsed locally into a DraftArticle. The concrete HTTP transport is wired in code-generation; this class documents the contract. """ def __init__(self, model: str = "deepseek-v4-flash:cloud", endpoint: str = "") -> None: self.model = model self.endpoint = endpoint def generate(self, brief_text: str, article_type: str, article_id: str) -> DraftArticle: # Placeholder transport — the real HTTP call is implemented in # code-generation. Kept as an interface-level stub per the design-stage # constraint (≤15 lines, no complete HTTP implementation here). raise NotImplementedError( "HttpModelClient transport is wired in code-generation; tests use the stub." ) def build_model_prompt(brief, article_type: str, max_length: int) -> str: """Assemble the minimal prompt (Q4=A, BR2.2/BR3.1/BR3.2): content context + tone + per-type max-length note. No secrets, only the brief.""" prompt = ( f"Write a {article_type} article.\n" f"Context: {brief.content_context}\n" f"Tone: {brief.tone_instruction}\n" f"Max length: {max_length} characters." ) return prompt.strip()