"""Claude Haiku 기반 종목 뉴스 호재/악재 분석.""" from __future__ import annotations import json import logging import os from typing import Any, Dict, List log = logging.getLogger(__name__) DEFAULT_MODEL = os.getenv("AI_NEWS_MODEL", "claude-haiku-4-5-20251001") PROMPT_TEMPLATE = """다음은 종목 {name}({ticker})에 대한 최근 뉴스 {n}개의 헤드라인입니다. {news_block} 이 뉴스들이 종목에 호재인지 악재인지 평가하세요. score: -10(매우 강한 악재) ~ +10(매우 강한 호재) 사이의 실수. 0은 중립. reason: 30자 이내 한 줄 근거. JSON으로만 응답하세요. 다른 텍스트 금지: {{"score": , "reason": ""}}""" def _clamp(x: float, lo: float = -10.0, hi: float = 10.0) -> float: return max(lo, min(hi, x)) async def score_sentiment( llm, ticker: str, news: List[Dict[str, Any]], *, name: str | None = None, model: str = DEFAULT_MODEL, ) -> Dict[str, Any]: """Returns {ticker, score_raw, reason, news_count, tokens_input, tokens_output, model}.""" news_block = "\n".join(f"- {n['title']}" for n in news) prompt = PROMPT_TEMPLATE.format( name=name or ticker, ticker=ticker, n=len(news), news_block=news_block, ) resp = await llm.messages.create( model=model, max_tokens=200, temperature=0, system="너는 한국 주식 뉴스 감성 분석가다. JSON 객체 하나만 반환한다.", messages=[ {"role": "user", "content": prompt}, # Assistant prefill — 첫 토큰을 강제로 '{' 로 시작해 JSON 응답을 보장 {"role": "assistant", "content": "{"}, ], ) raw = resp.content[0].text if resp.content else "" # prefill '{' 이 응답에 포함되지 않으므로 다시 붙임 text = "{" + raw if not raw.lstrip().startswith("{") else raw in_tokens = int(getattr(resp.usage, "input_tokens", 0) or 0) out_tokens = int(getattr(resp.usage, "output_tokens", 0) or 0) try: data = json.loads(text) score = _clamp(float(data["score"])) reason = str(data["reason"])[:200] return { "ticker": ticker, "score_raw": score, "reason": reason, "news_count": len(news), "tokens_input": in_tokens, "tokens_output": out_tokens, "model": model, } except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e: log.warning("ai_news parse fail for %s: %s (raw=%r)", ticker, e, text[:100]) return { "ticker": ticker, "score_raw": 0.0, "reason": f"parse fail: {e!s}"[:200], "news_count": len(news), "tokens_input": in_tokens, "tokens_output": out_tokens, "model": model, }