56 lines
2.2 KiB
Python
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()
|