""" Article Generation Module Claude API를 사용한 기사 생성 로직 """ from typing import Dict, Any, List, Optional from datetime import datetime import json import uuid import logging from anthropic import AsyncAnthropic from pydantic import BaseModel, Field logger = logging.getLogger(__name__) # Data Models class NewsSource(BaseModel): """뉴스 소스 정보""" title: str url: str published_date: Optional[str] = None source_site: str = "Unknown" class EventInfo(BaseModel): """이벤트 정보""" name: str date: Optional[str] = None location: Optional[str] = None class Entities(BaseModel): """추출된 엔티티""" people: List[str] = Field(default_factory=list) organizations: List[str] = Field(default_factory=list) groups: List[str] = Field(default_factory=list) countries: List[str] = Field(default_factory=list) events: List[EventInfo] = Field(default_factory=list) keywords: List[str] = Field(default_factory=list) class SubTopic(BaseModel): """기사 소주제""" title: str content: List[str] class GeneratedArticle(BaseModel): """생성된 기사""" news_id: str = Field(default_factory=lambda: str(uuid.uuid4())) title: str summary: str subtopics: List[SubTopic] categories: List[str] entities: Entities sources: List[NewsSource] = Field(default_factory=list) created_at: datetime = Field(default_factory=datetime.now) generation_metadata: Dict[str, Any] = Field(default_factory=dict) async def generate_article_with_claude( news_data: Dict[str, Any], style: str = "professional", claude_api_key: str = None ) -> GeneratedArticle: """Claude API를 사용하여 기사 생성""" if not claude_api_key: import os claude_api_key = os.getenv("CLAUDE_API_KEY") # Initialize Claude client claude_client = AsyncAnthropic(api_key=claude_api_key) # Collect source information sources_info = [] # Prepare the prompt system_prompt = """당신은 전문적인 한국 언론사의 수석 기자입니다. 제공된 데이터를 기반으로 깊이 있고 통찰력 있는 기사를 작성해야 합니다. 기사는 다음 요구사항을 충족해야 합니다: 1. 소주제는 최소 2개, 최대 6개로 구성해야 합니다 2. 각 소주제는 최소 1개, 최대 10개의 문단으로 구성해야 합니다 3. 전문적이고 객관적인 어조를 유지해야 합니다 4. 사실에 기반한 분석과 통찰을 제공해야 합니다 5. 한국 독자를 대상으로 작성되어야 합니다 6. 이벤트 정보는 가능한 일시와 장소를 포함해야 합니다 7. 핵심 키워드를 최대 10개까지 추출해야 합니다 반드시 다음 JSON 형식으로 응답하세요: { "title": "기사 제목", "summary": "한 줄 요약 (100자 이내)", "subtopics": [ { "title": "소주제 제목", "content": ["문단1", "문단2", ...] // 1-10개 문단 } ], // 2-6개 소주제 "categories": ["카테고리1", "카테고리2"], "entities": { "people": ["인물1", "인물2"], "organizations": ["기관1", "기관2"], "groups": ["단체1", "단체2"], "countries": ["나라1", "나라2"], "events": [ { "name": "이벤트명", "date": "2025년 1월 15일", // 선택사항 "location": "서울 코엑스" // 선택사항 } ], "keywords": ["키워드1", "키워드2", ...] // 최대 10개 } }""" # Prepare news content for Claude and collect sources news_content = [] for item in news_data.get("news_items", []): # Add RSS source info rss_title = item.get('rss_title', '') rss_link = item.get('rss_link', '') rss_published = item.get('rss_published', '') if rss_title and rss_link: sources_info.append(NewsSource( title=rss_title, url=rss_link, published_date=rss_published, source_site="RSS Feed" )) item_text = f"제목: {rss_title}\n" for result in item.get("google_results", []): # Add Google search result sources if "title" in result and "link" in result: sources_info.append(NewsSource( title=result.get('title', ''), url=result.get('link', ''), published_date=None, source_site="Google Search" )) if "full_content" in result and result["full_content"]: content = result["full_content"] if isinstance(content, dict): item_text += f"출처: {content.get('url', '')}\n" item_text += f"내용: {content.get('content', '')[:1000]}...\n\n" else: item_text += f"내용: {str(content)[:1000]}...\n\n" news_content.append(item_text) combined_content = "\n".join(news_content[:10]) # Limit to prevent token overflow user_prompt = f"""다음 뉴스 데이터를 기반으로 종합적인 기사를 작성하세요: 키워드: {news_data.get('keyword', '')} 수집된 뉴스 수: {len(news_data.get('news_items', []))} 뉴스 내용: {combined_content} 스타일: {style} - professional: 전통적인 뉴스 기사 스타일 - analytical: 분석적이고 심층적인 스타일 - investigative: 탐사보도 스타일 위의 데이터를 종합하여 통찰력 있는 기사를 JSON 형식으로 작성해주세요.""" try: # Call Claude API response = await claude_client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=4000, temperature=0.7, system=system_prompt, messages=[ {"role": "user", "content": user_prompt} ] ) # Parse response content = response.content[0].text # Extract JSON from response json_start = content.find('{') json_end = content.rfind('}') + 1 if json_start != -1 and json_end > json_start: json_str = content[json_start:json_end] article_data = json.loads(json_str) else: raise ValueError("No valid JSON found in response") # Create article object article = GeneratedArticle( title=article_data.get("title", ""), summary=article_data.get("summary", ""), subtopics=[ SubTopic( title=st.get("title", ""), content=st.get("content", []) ) for st in article_data.get("subtopics", []) ], categories=article_data.get("categories", []), entities=Entities(**article_data.get("entities", {})), sources=sources_info, generation_metadata={ "style": style, "keyword": news_data.get('keyword', ''), "model": "claude-3-5-sonnet-20241022", "timestamp": datetime.now().isoformat() } ) logger.info(f"Successfully generated article: {article.title}") return article except json.JSONDecodeError as e: logger.error(f"Failed to parse Claude response as JSON: {e}") raise except Exception as e: logger.error(f"Error generating article with Claude: {e}") raise