feat: add seo_meta service for AI-generated SEO metadata with CLI management and database layer
DevPlace CI / test (push) Failing after 2m13s
DevPlace CI / test (push) Failing after 2m13s
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:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .embeddings import embed_texts, local_embed
|
||||
@@ -11,6 +12,8 @@ from .store import Chunk, VectorStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CITATION_MARKER = re.compile(r"\[(\d+)\]")
|
||||
|
||||
CHAT_TOP_K = 8
|
||||
MAX_CONTEXT_CHARS = 9000
|
||||
CHAT_MAX_TOKENS = 900
|
||||
@@ -63,12 +66,22 @@ class DeepsearchChat:
|
||||
|
||||
async def _embed_query(self, question: str) -> list[float]:
|
||||
result = await embed_texts([question], self.api_key)
|
||||
if not result.vectors:
|
||||
if not result.vectors or not result.vectors[0]:
|
||||
result = local_embed([question])
|
||||
return result.vectors[0]
|
||||
|
||||
async def retrieve(self, question: str) -> list[Chunk]:
|
||||
query_vector = await self._embed_query(question)
|
||||
stored_dim = self.store.dims
|
||||
if stored_dim is not None and len(query_vector) != stored_dim:
|
||||
query_vector = local_embed([question]).vectors[0]
|
||||
if len(query_vector) != stored_dim:
|
||||
logger.warning(
|
||||
"deepsearch query embedding dim %d != stored %d",
|
||||
len(query_vector),
|
||||
stored_dim,
|
||||
)
|
||||
return []
|
||||
return self.store.hybrid_search(question, query_vector, top_k=CHAT_TOP_K)
|
||||
|
||||
async def answer(self, question: str, history: list[dict] | None = None) -> ChatAnswer:
|
||||
@@ -104,4 +117,13 @@ class DeepsearchChat:
|
||||
"I could not reach the language model to synthesise an answer, but the "
|
||||
"most relevant sources are listed below."
|
||||
)
|
||||
valid = {citation["index"] for citation in citations}
|
||||
text = self._strip_unmatched_markers(text, valid)
|
||||
return ChatAnswer(text=text, citations=citations)
|
||||
|
||||
def _strip_unmatched_markers(self, text: str, valid: set[int]) -> str:
|
||||
def replace(match: re.Match) -> str:
|
||||
index = int(match.group(1))
|
||||
return match.group(0) if index in valid else ""
|
||||
|
||||
return CITATION_MARKER.sub(replace, text)
|
||||
|
||||
Reference in New Issue
Block a user