여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - 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>
86 lines
2.8 KiB
Python
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
|