Files
C.E.L_Slide_test2/scripts/build_block_index.py
kyeongmin a01f7a7f8a Phase G: Kei API 통신 정상화 — streaming 전환 + Sonnet fallback 제거
G-1: httpx non-streaming → streaming 전환 (3개 파일)
  - client.post() → client.stream("POST") + response.aiter_lines()
  - SSE 토큰을 실시간 수신 (30분+ 무응답 해소)

G-2: Sonnet fallback 완전 제거
  - kei_client.py: classify_content()에서 _call_anthropic_direct() 호출 제거
  - content_editor.py: fill_content()에서 Sonnet fallback 분기 제거
  - Kei API만 사용. 실패 시 manual_classify() 또는 _apply_defaults() 안전망

G-3: _parse_json() 마크다운 제거 3파일 동기화
  - content_editor.py, design_director.py에 kei_client.py와 동일한 전처리 추가

G-4: FAISS를 CPU로 전환 (GPU 메모리 경쟁 해소)
  - block_search.py + build_block_index.py: device="cpu"

G-5: streaming 파서에 event:error 처리
  - persona_agent 에러 시 무한 대기 방지. 즉시 중단.

G-6: content_editor.py None 가드
  - Kei API 실패 시 _parse_json(None) TypeError 방지

G-7: "mode" → "mode_hint" 필드명 수정 (3개 파일)
  - persona_agent의 실제 필드명에 맞춤

persona_agent 수정: 0건

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:26:03 +09:00

163 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""블록 라이브러리 FAISS 인덱스 빌드 스크립트.
catalog.yaml의 46개 블록을 임베딩하여 FAISS 인덱스를 생성한다.
블록 추가/수정 시 이 스크립트를 다시 실행하면 인덱스가 갱신된다.
사용법:
python scripts/build_block_index.py
산출물:
data/block_index.faiss — FAISS 벡터 인덱스
data/block_metadata.json — 인덱스 순서 → 블록 매핑
"""
from __future__ import annotations
import json
import logging
import sys
from pathlib import Path
import faiss
import numpy as np
import yaml
from sentence_transformers import SentenceTransformer
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
# Kei persona와 동일 모델 (1024차원, 한국어 최적화)
EMBEDDING_MODEL = "BAAI/bge-m3"
PROJECT_ROOT = Path(__file__).parent.parent
CATALOG_PATH = PROJECT_ROOT / "templates" / "catalog.yaml"
INDEX_PATH = PROJECT_ROOT / "data" / "block_index.faiss"
META_PATH = PROJECT_ROOT / "data" / "block_metadata.json"
def load_catalog() -> list[dict]:
"""catalog.yaml에서 블록 목록을 로드한다."""
if not CATALOG_PATH.exists():
logger.error(f"catalog.yaml 없음: {CATALOG_PATH}")
sys.exit(1)
with open(CATALOG_PATH, encoding="utf-8") as f:
data = yaml.safe_load(f)
blocks = data.get("blocks", [])
if not blocks:
logger.error("catalog.yaml에 블록이 없습니다.")
sys.exit(1)
logger.info(f"catalog 로드: {len(blocks)}개 블록")
return blocks
def build_search_texts(blocks: list[dict]) -> list[str]:
"""각 블록의 검색용 텍스트를 생성한다.
name + visual + when을 조합하여 검색 쿼리와 매칭되도록 한다.
not_for는 네거티브이므로 검색 텍스트에 포함하지 않는다.
"""
texts = []
for block in blocks:
parts = [
block.get("name", ""),
block.get("visual", ""),
block.get("when", ""),
]
text = ". ".join(p.strip() for p in parts if p.strip())
texts.append(text)
return texts
def build_index(texts: list[str]) -> tuple[faiss.IndexFlatIP, np.ndarray]:
"""텍스트를 임베딩하고 FAISS 인덱스를 생성한다."""
logger.info(f"임베딩 모델 로딩: {EMBEDDING_MODEL}")
model = SentenceTransformer(EMBEDDING_MODEL, device="cpu")
logger.info(f"{len(texts)}개 텍스트 임베딩 중...")
embeddings = model.encode(
texts,
normalize_embeddings=True, # 코사인 유사도를 위해 정규화
show_progress_bar=True,
)
embeddings = np.array(embeddings, dtype=np.float32)
dim = embeddings.shape[1]
logger.info(f"임베딩 완료: {embeddings.shape[0]}× {dim}차원")
# Inner Product = 정규화된 벡터에서 코사인 유사도
index = faiss.IndexFlatIP(dim)
index.add(embeddings)
logger.info(f"FAISS 인덱스 생성: {index.ntotal}개 벡터, {dim}차원")
return index, embeddings
def save_metadata(blocks: list[dict]) -> None:
"""블록 메타데이터를 인덱스 순서대로 저장한다."""
metadata = []
for block in blocks:
metadata.append({
"id": block["id"],
"name": block.get("name", ""),
"template": block.get("template", ""),
"category": block.get("template", "").split("/")[1] if "/" in block.get("template", "") else "",
"height_cost": block.get("height_cost", "medium"),
"visual": block.get("visual", ""),
"when": block.get("when", ""),
"not_for": block.get("not_for", ""),
})
META_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(META_PATH, "w", encoding="utf-8") as f:
json.dump(metadata, f, ensure_ascii=False, indent=2)
logger.info(f"메타데이터 저장: {META_PATH} ({len(metadata)}개)")
def main():
# 1. catalog 로드
blocks = load_catalog()
# 2. 검색용 텍스트 생성
texts = build_search_texts(blocks)
# 3. 임베딩 + FAISS 인덱스
index, embeddings = build_index(texts)
# 4. 저장
INDEX_PATH.parent.mkdir(parents=True, exist_ok=True)
faiss.write_index(index, str(INDEX_PATH))
logger.info(f"FAISS 인덱스 저장: {INDEX_PATH}")
save_metadata(blocks)
# 5. 검증
logger.info("--- 검증 ---")
test_index = faiss.read_index(str(INDEX_PATH))
with open(META_PATH, encoding="utf-8") as f:
test_meta = json.load(f)
assert test_index.ntotal == len(blocks), f"벡터 수 불일치: {test_index.ntotal} vs {len(blocks)}"
assert len(test_meta) == len(blocks), f"메타데이터 수 불일치: {len(test_meta)} vs {len(blocks)}"
logger.info(f"✅ 검증 통과: {test_index.ntotal}개 벡터, {len(test_meta)}개 메타데이터")
# 6. 테스트 검색
model = SentenceTransformer(EMBEDDING_MODEL, device="cpu")
test_queries = [
"A vs B 두 개념 비교",
"연도별 정책 로드맵",
"핵심 수치 KPI 통계",
]
for query in test_queries:
q_emb = model.encode([query], normalize_embeddings=True)
scores, indices = test_index.search(np.array(q_emb, dtype=np.float32), 3)
results = [test_meta[i]["id"] for i in indices[0]]
logger.info(f" 검색 '{query}'{results}")
if __name__ == "__main__":
main()