o2o-site-AEO/solution/backend/services/llm/perplexity.py
Mina Choi 11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00

86 lines
2.8 KiB
Python

"""Perplexity 호출 — 이 프로젝트에서 Perplexity 로 나가는 **유일한 통로**."""
import json
from dataclasses import dataclass
import httpx
from config.server_configs import external_api_config
API_URL = "https://api.perplexity.ai/chat/completions"
DEFAULT_MODEL = "sonar"
# 첫 구조화 출력 요청은 스키마 준비에 10~30초가 걸린다 — 그 시간을 포함한 값이다.
DEFAULT_TIMEOUT = httpx.Timeout(90.0, connect=10.0)
DEFAULT_MAX_TOKENS = 2048
class PerplexityError(RuntimeError):
"""Perplexity 호출 실패."""
class PerplexityNotConfigured(PerplexityError):
"""PERPLEXITY_API_KEY 미설정 — 이 어댑터만 비활성."""
def is_configured() -> bool:
"""키가 설정돼 있는지."""
return bool((external_api_config.perplexity_api_key or "").strip())
@dataclass
class Usage:
"""호출 1회의 실제 사용량·비용."""
input_tokens: int = 0
output_tokens: int = 0
num_search_queries: int = 0
cost: float = 0.0
def read_usage(payload: dict) -> Usage:
"""응답의 usage 를 읽는다."""
u = payload.get("usage") or {}
cost_field = u.get("cost")
if isinstance(cost_field, dict):
cost = cost_field.get("total_cost")
else:
cost = cost_field
return Usage(
input_tokens=int(u.get("prompt_tokens") or 0),
output_tokens=int(u.get("completion_tokens") or 0),
num_search_queries=int(u.get("num_search_queries") or 0),
cost=float(cost) if isinstance(cost, (int, float)) else 0.0,
)
async def call(body: dict, *, client: httpx.AsyncClient | None = None) -> dict:
"""LLM 이 실제로 불리는 지점."""
api_key = (external_api_config.perplexity_api_key or "").strip()
if not api_key:
raise PerplexityNotConfigured("PERPLEXITY_API_KEY 미설정 — 채널 발견을 건너뛴다")
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
owns_client = client is None
http = client or httpx.AsyncClient(timeout=DEFAULT_TIMEOUT)
try:
resp = await http.post(API_URL, json=body, headers=headers)
except httpx.TimeoutException as ex:
raise PerplexityError(f"Perplexity 타임아웃: {ex}") from ex
except httpx.HTTPError as ex:
raise PerplexityError(f"Perplexity 호출 실패: {type(ex).__name__}: {ex}") from ex
finally:
if owns_client:
await http.aclose()
if resp.status_code != 200:
raise PerplexityError(f"Perplexity 응답 오류: status={resp.status_code} body={resp.text[:200]}")
try:
payload = resp.json()
except (json.JSONDecodeError, ValueError) as ex:
raise PerplexityError(f"Perplexity 응답 JSON 파싱 실패: {ex}") from ex
if not isinstance(payload, dict):
raise PerplexityError("Perplexity 응답이 객체가 아니다")
return payload