MAAS / app /ads /headings_service.py
Hammad712's picture
Added Tone and updated the banner prompt
7b6df86
raw
history blame
6.4 kB
# app/services/headings_service.py
import os
import json
import logging
import time
from typing import List
import google.generativeai as genai
from app.ads.schemas import HeadingsRequest, Persona
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
API_KEY = os.getenv("GEMINI_API_KEY")
if not API_KEY:
logger.error("GEMINI_API_KEY not set; headings generation will fail if called without configuration.")
else:
try:
genai.configure(api_key=API_KEY)
logger.debug("Configured google.generativeai in headings service")
except Exception as e:
logger.exception("Failed to configure google.generativeai in headings service: %s", e)
def _extract_json_array(raw: str) -> str:
start = raw.find('[')
end = raw.rfind(']')
if start != -1 and end != -1 and end > start:
return raw[start:end + 1]
return raw
def _build_headings_prompt(req: HeadingsRequest) -> str:
personas_json = json.dumps([p.dict() for p in req.selected_personas], indent=2)
main_goal_value = req.main_goal.value
main_goal_desc = getattr(req.main_goal, "description", "")
tone_enum = getattr(req, "tone", None)
tone_value = tone_enum.value if tone_enum is not None else "Professional"
prompt = f"""
You are an expert copywriter specialized in short, high-converting ad headlines for digital ads.
Tone: Use a "{tone_value}" tone for all headings. If tone is "Professional", keep headlines formal and trustworthy. If "Casual / Friendly", keep them approachable. If "Bold / Persuasive", use urgency and commanding language. If "Inspiring / Visionary", use aspirational language.
Task:
Produce exactly {req.num_headings} short, punchy ad headings (strings) for a paid ad campaign that target the selected personas and align with the business goal. RETURN ONLY a JSON array of strings (e.g. ["Heading 1", "Heading 2", ...]) and nothing else.
Requirements:
- Each heading should be concise (max ~60 characters), benefit-focused, and tailored to the provided personas and business goal.
- Use active language and mention the key value when appropriate (e.g., "scale", "AI", "launch", "MVP", "secure funding", "reduce time-to-market").
- Avoid punctuation-only headlines, and do not include numbering in text.
- If the main goal is "{main_goal_value}", use that intention as a primary framing. Goal description: {main_goal_desc}
Business Inputs:
- Business name: {req.business_name}
- Business category: {req.business_category}
- Business description: {req.business_description}
- Promotion type: {req.promotion_type}
- Offer description: {req.offer_description}
- Value proposition: {req.value}
- Main goal: {main_goal_value}{main_goal_desc}
- Serving clients info: {req.serving_clients_info}
- Serving clients location: {req.serving_clients_location}
Selected persona(s) (use these to shape headings):
{personas_json}
Now generate exactly {req.num_headings} unique ad headings as a JSON array of strings. No explanation, no extra text.
"""
logger.debug("Built headings prompt (len=%d) for business '%s' (tone=%s)", len(prompt), req.business_name, tone_value)
return prompt.strip()
def generate_headings(req: HeadingsRequest) -> List[str]:
prompt = _build_headings_prompt(req)
model_name = "gemini-2.5-pro"
logger.info("Generating %d headings for business '%s' using model %s",
req.num_headings, req.business_name, model_name)
try:
model = genai.GenerativeModel(model_name)
except Exception as e:
logger.exception("Failed to create GenerativeModel: %s", e)
raise RuntimeError(f"Gemini model init failed: {e}")
try:
start = time.perf_counter()
response = model.generate_content(prompt)
duration = time.perf_counter() - start
logger.info("Gemini generate_content (headings) completed in %.2fs", duration)
except Exception as e:
logger.exception("Gemini generate_content failed for headings")
raise RuntimeError(f"Gemini request failed: {e}")
raw = None
try:
if response and hasattr(response, "text") and response.text:
raw = response.text
logger.debug("Received response.text (len=%d) for headings", len(raw))
elif response and getattr(response, "candidates", None):
first = response.candidates[0]
if getattr(first, "finish_reason", "").upper() == "SAFETY":
msg = "Gemini headings generation blocked by safety filter"
logger.error(msg)
raise RuntimeError(msg)
raw = getattr(first, "content", None) or getattr(first, "text", None) or str(response)
logger.debug("Received candidate response for headings (len=%d)", len(raw) if raw else 0)
else:
raw = str(response)
logger.debug("Converted headings response to string (len=%d)", len(raw))
except Exception as e:
logger.exception("Failed to extract raw text from Gemini headings response")
raise RuntimeError(f"Failed to extract Gemini response text: {e}")
if not raw:
logger.error("Empty response from Gemini when generating headings")
raise RuntimeError("Empty response from Gemini")
snippet = _extract_json_array(raw)
try:
parsed = json.loads(snippet)
except json.JSONDecodeError:
logger.exception("Failed to parse JSON from headings response. Raw response: %s", raw)
raise RuntimeError(f"Failed to parse Gemini response as JSON array of strings.\nRaw: {raw}")
if not isinstance(parsed, list) or not all(isinstance(i, str) for i in parsed):
logger.error("Parsed headings JSON is not a list of strings. Parsed type: %s", type(parsed))
raise RuntimeError("Gemini did not return a JSON array of strings as expected.")
headings = parsed
if len(headings) < req.num_headings:
logger.warning("Gemini returned %d headings; expected %d. Returning what we have.",
len(headings), req.num_headings)
elif len(headings) > req.num_headings:
headings = headings[: req.num_headings]
logger.debug("Trimmed headings to requested num_headings=%d", req.num_headings)
headings = [h.strip() for h in headings]
logger.info("Generated %d headings for business '%s'", len(headings), req.business_name)
return headings