""" Vision + Yield landing generation via internal LLM gateway. Outputs MUST be strict JSON to support caching + UI rendering. """ from __future__ import annotations import json from typing import Any from pydantic import BaseModel, Field, ValidationError, conint from app.config import get_settings from app.services.llm_gateway import chat_completions settings = get_settings() VISION_PROMPT_VERSION = "v1" YIELD_LANDING_PROMPT_VERSION = "v1" class VisionResult(BaseModel): business_concept: str = Field(..., min_length=10, max_length=240) industry_vertical: str = Field(..., min_length=2, max_length=60) buyer_persona: str = Field(..., min_length=5, max_length=120) cold_email_subject: str = Field(..., min_length=5, max_length=120) cold_email_body: str = Field(..., min_length=20, max_length=800) monetization_idea: str = Field(..., min_length=10, max_length=240) radio_test_score: conint(ge=1, le=10) # type: ignore[valid-type] reasoning: str = Field(..., min_length=20, max_length=800) class YieldLandingConfig(BaseModel): template: str = Field(..., min_length=2, max_length=50) # e.g. "nature", "commerce", "tech" headline: str = Field(..., min_length=10, max_length=180) seo_intro: str = Field(..., min_length=80, max_length=800) cta_label: str = Field(..., min_length=4, max_length=60) niche: str = Field(..., min_length=2, max_length=60) color_scheme: str = Field(..., min_length=2, max_length=30) def _extract_first_json_object(text: str) -> str: """ Extract the first {...} JSON object from text. We do NOT generate fallback content; if parsing fails, caller must raise. """ s = (text or "").strip() if not s: raise ValueError("Empty LLM response") if s.startswith("{") and s.endswith("}"): return s start = s.find("{") end = s.rfind("}") if start == -1 or end == -1 or end <= start: raise ValueError("LLM response is not JSON") return s[start : end + 1] async def generate_vision(domain: str) -> tuple[VisionResult, str]: """ Returns (VisionResult, model_used). """ model = settings.llm_default_model system = ( "You are the Pounce AI, a domain intelligence engine.\n" "You must respond with STRICT JSON only. No markdown. No commentary.\n" "Language: English.\n" "If a field is unknown, make a best-effort realistic assumption.\n" ) user = ( f"Analyze domain '{domain}'.\n" "Act as a VC + domain broker.\n" "Create a realistic business concept and a buyer/outreach angle.\n" "Output STRICT JSON with exactly these keys:\n" "{\n" ' "business_concept": "...",\n' ' "industry_vertical": "...",\n' ' "buyer_persona": "...",\n' ' "cold_email_subject": "...",\n' ' "cold_email_body": "...",\n' ' "monetization_idea": "...",\n' ' "radio_test_score": 1,\n' ' "reasoning": "..."\n' "}\n" ) payload: dict[str, Any] = { "model": model, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], "temperature": 0.6, "stream": False, } res = await chat_completions(payload) content = ( res.get("choices", [{}])[0] .get("message", {}) .get("content", "") ) json_str = _extract_first_json_object(str(content)) try: data = json.loads(json_str) except Exception as e: raise ValueError(f"Failed to parse LLM JSON: {e}") from e try: return VisionResult.model_validate(data), model except ValidationError as e: raise ValueError(f"LLM JSON schema mismatch: {e}") from e async def generate_yield_landing(domain: str) -> tuple[YieldLandingConfig, str]: """ Returns (YieldLandingConfig, model_used). """ model = settings.llm_default_model system = ( "You are the Pounce AI, a domain monetization engine.\n" "You must respond with STRICT JSON only. No markdown. No commentary.\n" "Language: English.\n" "Write helpful, non-spammy copy. Avoid medical/legal claims.\n" ) user = ( f"Analyze domain '{domain}'.\n" "Goal: create a minimal SEO-friendly landing page plan that can route visitors to an affiliate offer.\n" "Output STRICT JSON with exactly these keys:\n" "{\n" ' "template": "tech|commerce|finance|nature|local|generic",\n' ' "headline": "...",\n' ' "seo_intro": "...",\n' ' "cta_label": "...",\n' ' "niche": "...",\n' ' "color_scheme": "..." \n' "}\n" "Keep seo_intro 120-220 words.\n" ) payload: dict[str, Any] = { "model": model, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], "temperature": 0.5, "stream": False, } res = await chat_completions(payload) content = ( res.get("choices", [{}])[0] .get("message", {}) .get("content", "") ) json_str = _extract_first_json_object(str(content)) try: data = json.loads(json_str) except Exception as e: raise ValueError(f"Failed to parse LLM JSON: {e}") from e try: return YieldLandingConfig.model_validate(data), model except ValidationError as e: raise ValueError(f"LLM JSON schema mismatch: {e}") from e