Andrew Ridgway bec1eaac87
Some checks failed
Test / test (push) Has been cancelled
first pass at the newspaper builder
2026-09-14 11:57:22 +10:00

56 lines
2.2 KiB
Python

"""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()