from __future__ import annotations from argparse import ArgumentParser from pathlib import Path import json import sys from sqlalchemy import select ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from services.shared.core import new_id, utc_now_iso from services.shared.db import get_session from services.shared.kb_localization import normalize_kb_language, resolve_article_group_id from services.shared.sql_models import KBArticleRow, KBCategoryRow def _parser() -> ArgumentParser: parser = ArgumentParser(description="Seed KB article localizations without creating duplicates.") parser.add_argument( "--seed-file", default=str(ROOT / "scripts" / "kb_voice_basic_kz_localizations.json"), help="Path to the localization seed JSON file.", ) parser.add_argument( "--apply", action="store_true", help="Persist changes. Without this flag the script runs in dry-run mode.", ) parser.add_argument( "--update-existing", action="store_true", help="Update existing localized articles instead of leaving them untouched.", ) return parser def _load_seed(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) def _find_category(session, category_name: str) -> KBCategoryRow: row = session.execute( select(KBCategoryRow).where(KBCategoryRow.name == category_name) ).scalar_one_or_none() if row is None: raise RuntimeError(f"KB category not found: {category_name}") return row def _find_source_article( session, *, category_id: str, source_title: str, source_language: str, ) -> KBArticleRow: rows = session.execute( select(KBArticleRow) .where(KBArticleRow.category_id == category_id) .where(KBArticleRow.title == source_title) .where(KBArticleRow.language == source_language) .order_by(KBArticleRow.id.asc()) ).scalars().all() if not rows: raise RuntimeError(f"Source article not found: {source_title}") if len(rows) > 1: raise RuntimeError(f"Multiple source articles found for title: {source_title}") return rows[0] def _find_localized_article( session, *, article_group_id: str, target_language: str, ) -> KBArticleRow | None: return session.execute( select(KBArticleRow) .where(KBArticleRow.article_group_id == article_group_id) .where(KBArticleRow.language == target_language) ).scalar_one_or_none() def main() -> None: args = _parser().parse_args() seed_file = Path(args.seed_file).resolve() seed = _load_seed(seed_file) source_language = normalize_kb_language(seed.get("source_language")) target_language = normalize_kb_language(seed.get("target_language")) category_name = str(seed.get("category_name") or "").strip() if not category_name: raise RuntimeError("Seed file must define category_name") session = get_session() try: category = _find_category(session, category_name) summary = { "category_id": category.category_id, "category_name": category.name, "seed_file": str(seed_file), "source_language": source_language, "target_language": target_language, "apply": bool(args.apply), "update_existing": bool(args.update_existing), "created": [], "updated": [], "skipped": [], } for item in seed.get("localizations", []): source_title = str(item.get("source_title") or "").strip() title = str(item.get("title") or "").strip() body = str(item.get("body") or "").strip() tags = [str(tag).strip() for tag in item.get("tags", []) if str(tag).strip()] if not source_title or not title or not body: raise RuntimeError(f"Incomplete localization entry: {item!r}") source_article = _find_source_article( session, category_id=category.category_id, source_title=source_title, source_language=source_language, ) article_group_id = resolve_article_group_id( source_article.article_id, source_article.article_group_id, ) existing = _find_localized_article( session, article_group_id=article_group_id, target_language=target_language, ) if existing is None: row = KBArticleRow( article_id=new_id("kba"), category_id=category.category_id, article_group_id=article_group_id, language=target_language, title=title, body=body, tags_json=json.dumps(tags, ensure_ascii=False), created_at=utc_now_iso(), updated_at=utc_now_iso(), ) session.add(row) summary["created"].append( { "source_title": source_title, "article_group_id": article_group_id, "target_title": title, } ) continue if args.update_existing: existing.title = title existing.body = body existing.tags_json = json.dumps(tags, ensure_ascii=False) existing.updated_at = utc_now_iso() summary["updated"].append( { "source_title": source_title, "article_id": existing.article_id, "article_group_id": article_group_id, "target_title": title, } ) else: summary["skipped"].append( { "source_title": source_title, "article_id": existing.article_id, "article_group_id": article_group_id, "target_title": existing.title, } ) if args.apply: session.commit() else: session.rollback() print(json.dumps(summary, ensure_ascii=True, indent=2)) finally: session.close() if __name__ == "__main__": main()