youngryong's picture
deploy: IntentFinder API (HF Docker Space)
21bdc64
Raw
History Blame Contribute Delete
5.11 kB
# ํ‚ค์›Œ๋“œ๋ฅผ ๋ฐ›์•„ ๋„ค์ด๋ฒ„ ์‹ค์‹œ๊ฐ„ ์กฐํšŒ + ์บ์‹ฑ ํ›„ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” FastAPI ์„œ๋ฒ„
import os
import time
from collections import defaultdict
from fastapi import FastAPI, HTTPException, Request, Header
from fastapi.middleware.cors import CORSMiddleware
import cache
from naver_collector import fetch_keywords
from analyzer import build_overview
from clusterer import cluster_keywords, filter_by_relevance
from intent_classifier import classify_clusters
from search_path import build_search_path
from autocomplete import build_autocomplete_path
from datalab import demographics
import supabase_store
app = FastAPI(title="IntentFinder API")
# โ”€โ”€ ๋ฐฐํฌ ๋ณดํ˜ธ ์„ค์ • (์ „๋ถ€ env ๊ธฐ๋ฐ˜ ยท ๋ฏธ์„ค์ • ์‹œ ๋กœ์ปฌ ๊ฐœ๋ฐœ์šฉ ๊ธฐ๋ณธ๊ฐ’) โ”€โ”€
# ALLOWED_ORIGINS: ํ—ˆ์šฉ ์ถœ์ฒ˜(์‰ผํ‘œ๊ตฌ๋ถ„). ๋ฐฐํฌ ์‹œ ๋Œ€์‹œ๋ณด๋“œ ๋„๋ฉ”์ธ์œผ๋กœ ์ œํ•œ. ๊ธฐ๋ณธ "*"(๋กœ์ปฌ).
# DT_API_KEY: ์„ค์ •๋˜๋ฉด X-DT-Key ํ—ค๋”๊ฐ€ ์ผ์น˜ํ•ด์•ผ ํ˜ธ์ถœ ํ—ˆ์šฉ(๊ณผ๊ธˆ ๋‚จ์šฉ ๋ฐฉ์ง€). ๋ฏธ์„ค์ • ์‹œ ๋ฌด์ธ์ฆ(๋กœ์ปฌ).
# RATE_MAX_PER_MIN: IP๋‹น ๋ถ„๋‹น ์ตœ๋Œ€ ํ˜ธ์ถœ ์ˆ˜.
ALLOWED_ORIGINS = [o.strip() for o in os.getenv("ALLOWED_ORIGINS", "*").split(",") if o.strip()] or ["*"]
DT_API_KEY = os.getenv("DT_API_KEY", "")
RATE_MAX_PER_MIN = int(os.getenv("RATE_MAX_PER_MIN", "20"))
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_methods=["*"],
allow_headers=["*"],
)
cache.init()
_hits: dict[str, list[float]] = defaultdict(list)
def _check_rate(ip: str):
now = time.time()
recent = [t for t in _hits[ip] if now - t < 60]
if len(recent) >= RATE_MAX_PER_MIN:
raise HTTPException(status_code=429, detail="์š”์ฒญ์ด ๋งŽ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.")
recent.append(now)
_hits[ip] = recent
@app.get("/health")
def health():
return {"ok": True}
@app.get("/analyze")
def analyze(keyword: str, request: Request, x_dt_key: str | None = Header(default=None)):
# ๊ณต์œ  ์‹œํฌ๋ฆฟ ๊ฒ€์ฆ(์„ค์ •๋œ ๊ฒฝ์šฐ) + IP ๋ ˆ์ดํŠธ๋ฆฌ๋ฐ‹
if DT_API_KEY and x_dt_key != DT_API_KEY:
raise HTTPException(status_code=401, detail="์ธ์ฆ๋˜์ง€ ์•Š์€ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.")
_check_rate(request.client.host if request.client else "unknown")
# ํ‚ค์›Œ๋“œ ๋ถ„์„: ์บ์‹œ ํžˆํŠธ๋ฉด ์ฆ‰์‹œ, ์•„๋‹ˆ๋ฉด ๋„ค์ด๋ฒ„ ํ˜ธ์ถœ ํ›„ ์บ์‹ฑ
keyword = keyword.strip()
if not keyword:
raise HTTPException(status_code=400, detail="ํ‚ค์›Œ๋“œ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.")
hit = cache.get(keyword)
if hit:
return {**hit, "cached": True}
try:
df_raw = fetch_keywords(keyword)
except Exception as e:
raise HTTPException(status_code=502, detail=f"๋„ค์ด๋ฒ„ API ํ˜ธ์ถœ ์‹คํŒจ: {e}")
# ์‹œ๋“œ ๋ฌด๊ด€ ๋ฒ”์šฉ์–ด(๊ณ„์‚ฐ๊ธฐ ๋“ฑ) ์ œ๊ฑฐ ํ›„ ์ง‘๊ณ„ยท๊ตฐ์ง‘ํ™”
df, emb = filter_by_relevance(df_raw, keyword)
result = build_overview(keyword, df)
result["raw_keyword_count"] = int(len(df_raw)) # ํ•„ํ„ฐ ์ „ ์›๋ณธ ์ˆ˜
result["filtered_out"] = int(len(df_raw) - len(df)) # ๋ฌด๊ด€์–ด ์ œ๊ฑฐ ์ˆ˜
clusters = cluster_keywords(df, embeddings=emb)
result["clusters"] = clusters
result["cluster_count"] = len(clusters) # ๊ทธ๋ฃน ์ˆ˜ ์นด๋“œ
result["clustered_keyword_count"] = sum(c["keyword_count"] for c in clusters)
# ๊ฒ€์ƒ‰ ๊ฒฝ๋กœ: ๋„ค์ด๋ฒ„ ์ž๋™์™„์„ฑ(์‹ค์ œ ๋™์‹œ๊ฒ€์ƒ‰) ์šฐ์„ , ๋นˆ์•ฝํ•˜๋ฉด ์ž„๋ฒ ๋”ฉ ์ถ”์ •์œผ๋กœ ํด๋ฐฑ
vol_lookup = dict(zip(
df["keyword"],
(df["search_volume_pc"].fillna(0) + df["search_volume_mobile"].fillna(0)).astype(int),
))
try:
sp = build_autocomplete_path(keyword, vol_lookup)
sp["source"] = "autocomplete"
if len(sp["nodes"]) < 5: # ์ž๋™์™„์„ฑ์ด ๋นˆ์•ฝํ•˜๋ฉด ์ž„๋ฒ ๋”ฉ ๊ฒฝ๋กœ๋กœ ๋Œ€์ฒด
sp = build_search_path(keyword, df, emb)
sp["source"] = "embedding"
except Exception:
sp = build_search_path(keyword, df, emb)
sp["source"] = "embedding"
result["search_path"] = sp
# ํด๋Ÿฌ์Šคํ„ฐ ๋‹จ์œ„ ์˜๋„ ๋ถ„๋ฅ˜ (๋„๋„› ์ฐจํŠธ). Claude ํ˜ธ์ถœ ์‹คํŒจํ•ด๋„ ๋‚˜๋จธ์ง€๋Š” ๋ฐ˜ํ™˜
try:
intent = classify_clusters(clusters)
result["intent_breakdown"] = intent["intent_breakdown"]
result["marketing_suggestion"] = intent["marketing_suggestion"]
result["intent_usage"] = intent["usage"]
except Exception as e:
result["intent_breakdown"] = {}
result["intent_error"] = str(e)
# ์„ฑ๋ณ„ร—์—ฐ๋ น ์ถ”์ • ๋น„์ค‘ (๋ฐ์ดํ„ฐ๋žฉ). ํ‚ค ์—†๊ฑฐ๋‚˜ ์‹คํŒจํ•ด๋„ ๋‚˜๋จธ์ง€๋Š” ๋ฐ˜ํ™˜
try:
result["demographics"] = demographics(keyword)
except Exception as e:
result["demographics"] = None
result["demographics_error"] = str(e)
# ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ(Supabase)์— ์ ์žฌ โ€” ์ž๊ฒฉ์ฆ๋ช… ์—†๊ฑฐ๋‚˜ ์‹คํŒจํ•ด๋„ ์‘๋‹ต์—” ์˜ํ–ฅ ์—†์Œ
if supabase_store.enabled():
try:
supabase_store.persist(result)
result["supabase_persisted"] = True
except Exception as e:
result["supabase_error"] = str(e)
cache.put(keyword, result)
return {**result, "cached": False}