The Text Model
Amazon Nova Lite (amazon.nova-lite-v1:0) runs on Bedrock in us-east-1. It's fast (~15 seconds for a full site) and inexpensive. The payload format follows the Bedrock Converse API style with a messages array.
TEXT_MODEL = "amazon.nova-lite-v1:0"
def _generate_text(prompt: str) -> str:
client = boto3.client(
"bedrock-runtime",
region_name="us-east-1",
aws_access_key_id=config("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=config("AWS_SECRET_ACCESS_KEY"),
)
response = client.invoke_model(
modelId=TEXT_MODEL,
contentType="application/json",
accept="application/json",
body=json.dumps({
"messages": [{"role": "user", "content": [{"text": prompt}]}],
"inferenceConfig": {"max_new_tokens": 2000, "temperature": 0.8},
}),
)
body = json.loads(response["body"].read())
return body["output"]["message"]["content"][0]["text"]
The Prompt Structure
The prompt explicitly requests a raw JSON object with no markdown wrapping. Nova Lite sometimes wraps JSON in triple-backtick blocks anyway — the parser strips those before calling json.loads.
def generate_text_only(site) -> dict:
raw = _generate_text(prompt).strip()
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
content = json.loads(raw.strip())
Handling Array Content
Nova Lite sometimes returns blog post content as an array of paragraph strings instead of a single string. The normalisation step joins them before storing.
for blog_type in content.get("blog_types", []):
for post in blog_type.get("posts", []):
if isinstance(post.get("content"), list):
post["content"] = "\n\n".join(post["content"])
Tip: Always validate the structure of LLM JSON responses before trusting field types. Nova Lite is generally well-behaved but array vs string ambiguity on long text fields is a known edge case.