"""지역명으로 맛집 후보 이름을 찾는다. ★ services/external/perplexity.py(채널 발견: 단일 업소 → URL)와는 다른 용도다 — 여기는 "이 지역에 뭐가 있나"를 묻는 지역 목록 검색이다. 원칙은 같다: Perplexity 응답을 사실로 쓰지 않는다. 이름만 후보로 받고, 실제 값은 이후 네이버 크롤링(NaverPlaceAdapter)이 확정한다. """ import json from common.logger import LOG from services.llm.perplexity import DEFAULT_MAX_TOKENS, DEFAULT_MODEL, PerplexityError, call from services.prompts.restaurant_search import RESPONSE_SCHEMA, SYSTEM_PROMPT, build_prompt MAX_RESULTS = 10 def _parse_names(payload: dict) -> list[str]: choices = payload.get("choices") or [] if not choices or not isinstance(choices[0], dict): return [] content = ((choices[0].get("message") or {}).get("content")) or "" try: doc = json.loads(content) except (json.JSONDecodeError, TypeError): LOG.w("[restaurant_discovery] 구조화 출력 파싱 실패") return [] names = doc.get("restaurants") if not isinstance(names, list): return [] return [str(n).strip() for n in names if str(n or "").strip()][:MAX_RESULTS] async def search_region_restaurants( region_label: str, *, model: str = DEFAULT_MODEL, client=None, ) -> list[str]: """지역명으로 맛집 상위 10곳의 이름만 받는다. 실패(미설정·타임아웃·5xx)하면 빈 목록 — 호출측이 TourAPI 결과만으로 계속 진행한다. """ if not (region_label or "").strip(): return [] body = { "model": model, "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_prompt(region_label)}, ], "max_tokens": DEFAULT_MAX_TOKENS, "temperature": 0, "response_format": RESPONSE_SCHEMA, } try: payload = await call(body, client=client) except PerplexityError as ex: LOG.w(f"[restaurant_discovery] '{region_label}' 검색 실패: {ex}") return [] return _parse_names(payload)