143 lines
6.2 KiB
Python
143 lines
6.2 KiB
Python
#!/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))
|