#!/usr/bin/env python3 """ GUARDiA CMS — 중앙 guardia-rag 색인 훅 (브랜드 톤·용어집·콘텐츠 가이드 → cms 컬렉션). 콘텐츠 초안 생성(/answer hybrid·/agent)의 근거 회수와 용어 일관성을 위해, CMS 의 **브랜드 톤· 용어집·콘텐츠 유형 가이드·SEO/모더레이션 정책** 메타만 중앙 RAG 의 `cms` 컬렉션에 적재한다. 회원 PII·자격증명·실데이터 행은 색인하지 않는다(보안 불변). 선택: 라이브 published 콘텐츠 본문도 색인하려면 --with-content 로 공개 delivery API(GET, 인증불요) 에서 published 만 가져와 색인한다(초안/비공개 제외, PII 필드 제외). 사용: python scripts/rag_ingest_cms.py # 기본 메타 docs (http://127.0.0.1:8020) RAG_URL=http://127.0.0.1:8020 python scripts/rag_ingest_cms.py CMS_URL=http://127.0.0.1:8012 python scripts/rag_ingest_cms.py --with-content 설계 메모: - 외부 API 0. 중앙 guardia-rag(온프레미스)만 호출. - 멱등: doc_id 안정 → 재실행 시 upsert(중앙이 처리). 실패해도 CMS 동작에 영향 없음(보조 스크립트). - PII 미색인: 회원 email/phone/주소·password_enc·작성자 식별정보는 문서에 포함하지 않는다. """ import json import os import sys import urllib.request import urllib.error RAG_URL = os.environ.get("RAG_URL", "http://127.0.0.1:8020").rstrip("/") CMS_URL = os.environ.get("CMS_URL", "http://127.0.0.1:8012").rstrip("/") SOLUTION = "cms" # 브랜드 톤·용어집·콘텐츠 정책 메타 — 콘텐츠 초안 grounding·용어 일관성용. DOCS = [ { "doc_id": "cms_brand_tone", "source": "CMS-BRAND", "text": ( "GUARDiA CMS 브랜드 톤 가이드: 친근하지만 신뢰감 있는 한국어. 과장·허위광고 금지, " "구체적 가치(품질·배송·혜택)를 명료하게. 이모지는 절제. 마케팅 카피는 200자 내외 권장." ), }, { "doc_id": "cms_content_types", "source": "CMS-GUIDE", "text": ( "콘텐츠 유형(content_type): PAGE(정적 페이지), POST(블로그/공지), BLOCK(재사용 블록), " "PRODUCT(상품 상세). 게시 워크플로우: draft→review→approved→published. " "AI 초안은 항상 draft 로만 적재하고 published 직행 금지(사람 승인)." ), }, { "doc_id": "cms_glossary", "source": "CMS-GLOSSARY", "text": ( "도메인 용어집(번역·작성 시 보존): 헤드리스(headless)=콘텐츠와 표현 분리, " "delivery API=published 콘텐츠 제공 API, UGC=사용자생성콘텐츠(리뷰/댓글/문의), " "기획전=프로모션 캠페인. 직역 금지, 용어 일관 유지." ), }, { "doc_id": "cms_seo_policy", "source": "CMS-SEO", "text": ( "SEO 가이드: meta title 60자·description 150자 내외, 키워드 3~8개, " "본문 길이 300~1500자 가독성 우수. 중복 콘텐츠·키워드 스터핑 금지." ), }, { "doc_id": "cms_moderation_policy", "source": "CMS-MODERATION", "text": ( "UGC 모더레이션 정책: 욕설/스팸(외부링크·도박·대출·주식리딩)/금칙어/개인정보 노출은 차단 또는 보류. " "판정 verdict: APPROVE/REJECT/REVIEW. 확신이 낮으면 자동 통과 금지 → REVIEW(사람 검토). " "개인정보(전화/이메일/주소/카드)는 pii_flag 로 표시하고 응답·색인에서 제외." ), }, ] def fetch_published_content(): """공개 delivery API에서 published 콘텐츠 본문 일부를 docs 로 변환(옵션). PII/비공개 제외.""" out = [] try: req = urllib.request.Request(f"{CMS_URL}/api/cms/delivery/content?status=PUBLISHED", headers={"Accept": "application/json"}, method="GET") with urllib.request.urlopen(req, timeout=20) as resp: payload = json.loads(resp.read().decode("utf-8")) items = payload.get("data") if isinstance(payload, dict) else payload if not isinstance(items, list): return out for it in items[:200]: cid = it.get("id") title = (it.get("title") or "").strip() body = (it.get("body") or it.get("content") or "").strip() ctype = it.get("contentType") or it.get("content_type") or "CONTENT" if not title and not body: continue out.append({ "doc_id": f"cms_content_{cid}", "source": f"CMS-{ctype}", "text": f"[{ctype}] {title}\n{body[:2000]}", }) except Exception: # 보조 기능 — 라이브 미가용/형식 상이 시 조용히 스킵 pass return out def ingest(docs): payload = {"solution": SOLUTION, "documents": docs} data = json.dumps(payload).encode("utf-8") req = urllib.request.Request( f"{RAG_URL}/rag/ingest", data=data, headers={"Content-Type": "application/json", "X-Solution-Key": SOLUTION}, method="POST", ) try: with urllib.request.urlopen(req, timeout=120) as resp: body = resp.read().decode("utf-8") print(f"[OK] ingested {len(docs)} docs into '{SOLUTION}' collection") print(f" response: {body[:300]}") return 0 except urllib.error.HTTPError as e: print(f"[WARN] ingest HTTP {e.code} — 중앙 RAG 응답 이상(스크립트는 보조). 본 CMS 동작 영향 없음.") return 1 except urllib.error.URLError as e: print(f"[WARN] 중앙 guardia-rag 미가용({RAG_URL}) — 색인 보류. 라이브 가동 후 재실행. " f"(사유 요약: {str(e.reason)[:80]})") return 1 except Exception: print("[WARN] 색인 중 예기치 못한 오류 — 보류(CMS 동작 영향 없음).") return 1 if __name__ == "__main__": docs = list(DOCS) if "--with-content" in sys.argv: extra = fetch_published_content() print(f"[INFO] published 콘텐츠 {len(extra)}건 추가 색인 대상") docs += extra sys.exit(ingest(docs))