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
+35
View File
@@ -42,6 +42,7 @@ class VectorStore:
self.collection_name = collection_name
self._client = None
self._collection = None
self._dims: int | None = None
def _ensure(self):
if self._collection is not None:
@@ -55,9 +56,43 @@ class VectorStore:
)
return self._collection
@property
def dims(self) -> int | None:
if self._dims is not None:
return self._dims
try:
collection = self._ensure()
data = collection.get(include=["embeddings"], limit=1)
rows = data.get("embeddings") or []
if rows and rows[0]:
self._dims = len(rows[0])
except Exception:
return self._dims
return self._dims
def add(self, chunks: list[Chunk], vectors: list[list[float]]) -> None:
if not chunks:
return
keep_chunks: list[Chunk] = []
keep_vectors: list[list[float]] = []
for chunk, vector in zip(chunks, vectors):
if not vector:
continue
if self._dims is None:
self._dims = len(vector)
if len(vector) != self._dims:
logger.warning(
"deepsearch dropping chunk with mismatched embedding dim %d != %d",
len(vector),
self._dims,
)
continue
keep_chunks.append(chunk)
keep_vectors.append(vector)
if not keep_chunks:
return
chunks = keep_chunks
vectors = keep_vectors
collection = self._ensure()
collection.add(
ids=[chunk.uid for chunk in chunks],