o2o-negosium-original/lps/worker_main.py
민헌 b0a86b9720 feat(lps): AI 유사도 판정 — '같은 상품' 매칭으로 액세서리 오염 해결
파이프라인의 비워둔 슬롯(이상치 뒤·top-N 앞)에 OpenAI 유사도 판정을 결합.
"스탠리 텀블러" 검색 시 빨대마개·커버 등 호환 액세서리가 최저가로 올라오던
문제를 해결한다(기계적 최저가 → 같은 상품 최저가).

- ai/similarity: SimilarityJudge(OpenAI structured output). 액세서리/부품/다른규격 불일치 판별
- pipeline/core: apply_filters + rank_result 로 분리(AI 를 그 사이에 끼움), run_price_pipeline 동작 불변
- handler: judge 주입 시 ai_match STAGE 추가(필터 후 후보만 판정 → 토큰 절약), 미주입 시 생략
- worker_main: OPENAI_API_KEY 있으면 판정 ON
- requirements: openai / tests: fake judge 필터링 검증 → 전체 32/32
- 라이브: '스탠리 퀜처 887ml' → ai_match(60→15) → 실제 텀블러 top-6(액세서리 제거)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:44:57 +09:00

59 lines
2.2 KiB
Python

# LPS 워커 프로세스 진입점 (API 와 분리 실행 — 코드베이스 공유, 독립 스케일).
# python worker_main.py
# WORKER_CONCURRENCY=2 python worker_main.py
#
# 쿠팡 검색은 브라우저(Chrome)라 무겁고 컨텍스트당 직렬이므로 기본 동시성은 1.
# 여러 브라우저로 늘리려면 CoupangAdapter 인스턴스를 워커마다 따로 준다.
import asyncio
import os
from common.logger import LOG
from config.server_configs import web_server_config
from crud.job_crud import JobQueue
from services.search.coupang.adapter import CoupangAdapter
from services.search.naver.adapter import NaverAdapter
from services.ai.similarity import SimilarityJudge
from worker.handlers import build_search_handler
from worker.notify import JobListener
from worker.runner import Worker, run_reaper
LOG.SetPrefix(f"{web_server_config.server_name}-worker")
async def main(concurrency: int = 1):
queue = JobQueue()
# 쿠팡(브라우저, 무거움) + 네이버(오픈API, 가벼움) 동시 검색 → 병합 최저가
adapters = {"coupang": CoupangAdapter(headless=False), "naver": NaverAdapter()}
# OPENAI_API_KEY 있으면 '같은 상품' AI 판정 활성화(없으면 기계적 최저가만)
judge = SimilarityJudge() if os.environ.get("OPENAI_API_KEY") else None
LOG.i(f"AI 유사도 판정: {'ON' if judge else 'OFF(키 없음)'}")
handler = build_search_handler(adapters, judge=judge)
stop = asyncio.Event()
listeners: list[JobListener] = []
tasks: list[asyncio.Task] = []
for i in range(concurrency):
listener = JobListener()
await listener.start()
listeners.append(listener)
worker = Worker(f"worker-{i}", queue, handler)
tasks.append(asyncio.create_task(worker.run(listener, stop)))
tasks.append(asyncio.create_task(run_reaper(queue, stop)))
LOG.i(f"LPS 워커 {concurrency}개 + reaper 기동")
try:
await asyncio.gather(*tasks)
finally:
stop.set()
for listener in listeners:
await listener.close()
for adapter in adapters.values():
await adapter.close()
if __name__ == "__main__":
asyncio.run(main(int(os.environ.get("WORKER_CONCURRENCY", "1"))))