Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
167 lines
5.4 KiB
Python
167 lines
5.4 KiB
Python
"""
|
|
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
|
|
|