feat: add seo_meta service for AI-generated SEO metadata with CLI management and database layer

Implement a new `SeoMetaService` subservice that generates clean SEO title/description/keywords for published content items, distinct from the existing SEO diagnostics auditor. Add `seo_metadata` polymorphic table with soft-delete support, batch query methods, and usage tracking. Extend the CLI with `seo-meta prune` and `seo-meta clear` commands for job row lifecycle management. Wire `schedule_seo_meta_for_table` into content creation and editing flows in `content.py`. Document the new service in `AGENTS.md` and `README.md`, including the `extra_head` site setting for custom `<head>` injection.
This commit is contained in:
2026-06-19 20:15:22 +00:00
parent 426d3639c6
commit d10f1af118
51 changed files with 2262 additions and 93 deletions
+39 -15
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
import time
from devplacepy import stealth
from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL
@@ -13,6 +14,36 @@ CHAT_TIMEOUT_SECONDS = 120.0
DEFAULT_MAX_TOKENS = 1200
async def request_completion(
messages: list[dict],
api_key: str,
*,
gateway_url: str = INTERNAL_GATEWAY_URL,
model: str = INTERNAL_MODEL,
max_tokens: int = DEFAULT_MAX_TOKENS,
temperature: float = 0.2,
timeout: float = CHAT_TIMEOUT_SECONDS,
) -> tuple[str, dict, int]:
payload = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
start: float = time.monotonic()
async with stealth.stealth_async_client(timeout=timeout) as client:
response = await client.post(gateway_url, json=payload, headers=headers)
elapsed_ms: int = int((time.monotonic() - start) * 1000)
if response.status_code >= 400:
raise RuntimeError(f"chat gateway returned {response.status_code}")
data: dict = response.json()
return data, data.get("usage") or {}, elapsed_ms
async def complete_chat(
messages: list[dict],
api_key: str,
@@ -22,21 +53,14 @@ async def complete_chat(
max_tokens: int = DEFAULT_MAX_TOKENS,
temperature: float = 0.2,
) -> str:
payload = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": temperature,
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
async with stealth.stealth_async_client(timeout=CHAT_TIMEOUT_SECONDS) as client:
response = await client.post(gateway_url, json=payload, headers=headers)
if response.status_code >= 400:
raise RuntimeError(f"chat gateway returned {response.status_code}")
data = response.json()
data, _usage, _elapsed_ms = await request_completion(
messages,
api_key,
gateway_url=gateway_url,
model=model,
max_tokens=max_tokens,
temperature=temperature,
)
choices = data.get("choices") or []
if not choices:
raise RuntimeError("chat gateway returned no choices")