Spaces:
Running
Running
| # app.py | |
| # Naver News Search API -> Gradio chat-like UI (HF Spaces friendly) | |
| # | |
| # HF Spaces Secrets ์ค์ : | |
| # NAVER_CLIENT_ID = ๋ฐ๊ธ๋ฐ์ Client ID | |
| # NAVER_CLIENT_SECRET = ๋ฐ๊ธ๋ฐ์ Client Secret | |
| import os | |
| import html | |
| import re | |
| from datetime import datetime | |
| from typing import Dict, Any, List, Tuple | |
| import requests | |
| import gradio as gr | |
| NAVER_NEWS_ENDPOINT = "https://openapi.naver.com/v1/search/news.json" | |
| def _get_env(name: str) -> str: | |
| v = os.getenv(name, "").strip() | |
| return v | |
| def _strip_tags(text: str) -> str: | |
| # ๋ค์ด๋ฒ ๋ด์ค API ๊ฒฐ๊ณผ์ title/description์ <b>...</b>๊ฐ ํฌํจ๋ ์ ์์ด ์ ๊ฑฐ | |
| # (HTML ํ๊ทธ ์ ๊ฑฐ + ์ํฐํฐ ์ฒ๋ฆฌ) | |
| if not text: | |
| return "" | |
| text = re.sub(r"<[^>]+>", "", text) | |
| return html.unescape(text).strip() | |
| def _format_pubdate(pub_date: str) -> str: | |
| # ์: "Mon, 19 Jan 2026 09:30:00 +0900" | |
| # ํ์ฑ ์คํจ ์ ์๋ฌธ ๋ฐํ | |
| if not pub_date: | |
| return "" | |
| try: | |
| dt = datetime.strptime(pub_date, "%a, %d %b %Y %H:%M:%S %z") | |
| # ํ๊ตญ ํ๊ฒฝ์์ ๋ณด๊ธฐ ์ข์ ํฌ๋งท | |
| return dt.strftime("%Y-%m-%d %H:%M:%S %z") | |
| except Exception: | |
| return pub_date | |
| def naver_news_search( | |
| query: str, | |
| display: int = 10, | |
| sort: str = "date", | |
| start: int = 1, | |
| timeout: int = 10, | |
| ) -> Dict[str, Any]: | |
| client_id = _get_env("NAVER_CLIENT_ID") | |
| client_secret = _get_env("NAVER_CLIENT_SECRET") | |
| if not client_id or not client_secret: | |
| raise RuntimeError( | |
| "ํ๊ฒฝ๋ณ์ NAVER_CLIENT_ID / NAVER_CLIENT_SECRET ์ด ์ค์ ๋์ด ์์ง ์์ต๋๋ค. " | |
| "Hugging Face Spaces์ Secrets์ ์ถ๊ฐํด ์ฃผ์ธ์." | |
| ) | |
| headers = { | |
| "X-Naver-Client-Id": client_id, | |
| "X-Naver-Client-Secret": client_secret, | |
| } | |
| params = { | |
| "query": query, | |
| "display": int(display), | |
| "start": int(start), | |
| "sort": sort, | |
| } | |
| r = requests.get( | |
| NAVER_NEWS_ENDPOINT, | |
| headers=headers, | |
| params=params, | |
| timeout=timeout, | |
| ) | |
| # ๋ค์ด๋ฒ API๊ฐ ์๋ฌ๋ฅผ ๋ฐํํ๋ ๊ฒฝ์ฐ, body์ ์์ธ๊ฐ ์์ ์ ์์ด ๊ทธ๋๋ก ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ์ฒ๋ฆฌ | |
| if r.status_code != 200: | |
| try: | |
| detail = r.json() | |
| except Exception: | |
| detail = {"text": r.text} | |
| raise RuntimeError(f"๋ค์ด๋ฒ API ํธ์ถ ์คํจ (HTTP {r.status_code}): {detail}") | |
| return r.json() | |
| def render_results(data: Dict[str, Any], max_items: int = 10) -> str: | |
| items = data.get("items", [])[:max_items] | |
| total = data.get("total", None) | |
| lines: List[str] = [] | |
| if total is not None: | |
| lines.append(f"- ์ด ๊ฒ์๊ฒฐ๊ณผ: {total:,}๊ฑด") | |
| lines.append(f"- ๋ฐํ ๊ฐ์: {len(items)}๊ฑด") | |
| lines.append("") | |
| for i, it in enumerate(items, start=1): | |
| title = _strip_tags(it.get("title", "")) | |
| desc = _strip_tags(it.get("description", "")) | |
| link = it.get("link", "") | |
| origin = it.get("originallink", "") | |
| pub = _format_pubdate(it.get("pubDate", "")) | |
| lines.append(f"{i}. **{title}**") | |
| # ํ์ ํญ๋ชฉ 4์นธ ๋ค์ฌ์ฐ๊ธฐ | |
| if pub: | |
| lines.append(f" - ๋ฐํ: {pub}") | |
| if origin: | |
| lines.append(f" - ์๋ฌธ: {origin}") | |
| if link: | |
| lines.append(f" - ๋งํฌ: {link}") | |
| if desc: | |
| lines.append(f" - ์์ฝ: {desc}") | |
| lines.append("") | |
| lines.append("") | |
| return "\n".join(lines).strip() | |
| def dedup_items(all_items: List[Dict[str, Any]]) -> List[Dict[str, Any]]: | |
| """ | |
| ๊ฒฐ๊ณผ ์ค๋ณต ์ ๊ฑฐ: | |
| - originallink ์ฐ์ , ์์ผ๋ฉด link, ์์ผ๋ฉด title+pubDate ํด์ | |
| """ | |
| seen = set() | |
| out = [] | |
| for it in all_items: | |
| origin = (it.get("originallink") or "").strip() | |
| link = (it.get("link") or "").strip() | |
| title = _strip_tags(it.get("title", "")) | |
| pub = it.get("pubDate", "") | |
| key = origin or link or f"{title}|{pub}" | |
| if not key: | |
| continue | |
| if key in seen: | |
| continue | |
| seen.add(key) | |
| out.append(it) | |
| return out | |
| def aggregate_search( | |
| sentence: str, | |
| display: int, | |
| sort: str, | |
| ) -> Tuple[List[str], List[Dict[str, Any]], int]: | |
| """ | |
| ์ฌ์ฉ์ ์ ๋ ฅ ๋ฌธ์ฅ์ ๊ทธ๋๋ก query๋ก ์ฌ์ฉํ์ฌ API ํธ์ถ | |
| ๋ฐํ: (์ฌ์ฉ๋ ์ฟผ๋ฆฌ ๋ชฉ๋ก, ์ต์ข ์์ดํ ๋ชฉ๋ก, total) | |
| """ | |
| queries = [sentence] | |
| all_items: List[Dict[str, Any]] = [] | |
| total: int = 0 | |
| data = naver_news_search(query=sentence, display=int(display), sort=sort, start=1) | |
| total = int(data.get("total", 0) or 0) | |
| all_items.extend(data.get("items", [])) | |
| merged = dedup_items(all_items) | |
| final_items = merged[:display] | |
| return queries, final_items, total | |
| def render_results_from_items(items: List[Dict[str, Any]]) -> str: | |
| """ | |
| items ๋ฆฌ์คํธ๋ฅผ ๋์ผ ์คํ์ผ๋ก ์ถ๋ ฅ | |
| """ | |
| lines: List[str] = [] | |
| # lines.append(f"- ์ต์ข ๋ฐํ ๊ฐ์: {len(items)}๊ฑด") | |
| lines.append("") # (์์ฒญ: ์ํ์ ์ ์๋จ ๋ฌธ๊ตฌ ๋ค์ ์ค์๋ง ์ถ๋ ฅ) | |
| for i, it in enumerate(items, start=1): | |
| title = _strip_tags(it.get("title", "")) | |
| desc = _strip_tags(it.get("description", "")) | |
| link = it.get("link", "") | |
| origin = it.get("originallink", "") | |
| pub = _format_pubdate(it.get("pubDate", "")) | |
| lines.append(f"{i}. **{title}**") | |
| if pub: | |
| lines.append(f" - ๋ฐํ: {pub}") | |
| if origin: | |
| lines.append(f" - ์๋ฌธ: {origin}") | |
| if link: | |
| lines.append(f" - ๋งํฌ: {link}") | |
| if desc: | |
| lines.append(f" - ์์ฝ: {desc}") | |
| lines.append("") | |
| return "\n".join(lines).strip() | |
| def handle_search( | |
| user_query: str, | |
| chat_history: List[Dict[str, str]], | |
| display: int, | |
| sort: str, | |
| ) -> Tuple[List[Dict[str, str]], str]: | |
| q = (user_query or "").strip() | |
| if not q: | |
| return chat_history, "" | |
| chat_history = chat_history + [{"role": "user", "content": q}] | |
| try: | |
| _, items, total = aggregate_search(sentence=q, display=int(display), sort=sort) | |
| total_to_show = total if total > 0 else len(items) | |
| lines: List[str] = [] | |
| # lines.append( | |
| # f"\"{q}\"์ ๋ํ ๊ฒ์ ๊ฒฐ๊ณผ๋ **{total_to_show:,}๊ฑด** ์ด๋ฉฐ, ์ต์ข ์๋ต ๊ฐ์๋ **{len(items)}๊ฑด** ์ ๋๋ค." | |
| # ) | |
| # lines.append("---") | |
| # lines.append("") | |
| # lines.append("---") # โ ์ํ์ ์ โ... ์ ๋๋คโ ๋ค์ ์ค์๋ง | |
| lines.append("") | |
| lines.append(render_results_from_items(items)) # โ ๊ฒฐ๊ณผ๋ 1๋ฒ๋ง ์ถ๋ ฅ | |
| lines.append("") | |
| assistant_text = "\n".join(lines).strip() | |
| except Exception as e: | |
| assistant_text = f"์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.\n\n- {e}" | |
| chat_history = chat_history + [{"role": "assistant", "content": assistant_text}] | |
| return chat_history, "" | |
| with gr.Blocks(title="Naver News Search (Chat UI)") as demo: | |
| with gr.Accordion("๊ฒ์ ์ต์ ฅ - ๋ด์ค ๊ฐ์์ ์ ๋ ฌ ๋ฐฉ์์ ์ ํํ์ธ์.", open=False): | |
| with gr.Row(): | |
| display = gr.Slider( | |
| minimum=1, maximum=100, value=30, step=1, label="๋ด์ค ๊ฐ์" | |
| ) | |
| sort = gr.Dropdown( | |
| choices=[("์ต์ ์", "date"), ("์ ํ๋์(์ฐ๊ด๋์)", "sim")], | |
| value="date", | |
| label="์ ๋ ฌ ๋ฐฉ์", | |
| ) | |
| chatbot = gr.Chatbot( | |
| value=[], | |
| label="NewsChat_v0.1", | |
| type="messages", | |
| height=600, | |
| ) | |
| with gr.Row(): | |
| query_in = gr.Textbox( | |
| label="๊ฒ์์ด๋ฅผ ์ ๋ ฅํ์ธ์.", | |
| placeholder="(์: ํ์๋ถ์ ์ธ๊ณต์ง๋ฅ ์ฌ์ )", | |
| scale=8, | |
| ) | |
| with gr.Column(scale=2): | |
| search_btn = gr.Button("๊ฒ์", variant="primary") | |
| clear_btn = gr.Button("๋ํ ์ง์ฐ๊ธฐ", variant="secondary") | |
| search_btn.click( | |
| fn=handle_search, | |
| inputs=[query_in, chatbot, display, sort], | |
| outputs=[chatbot, query_in], | |
| api_name="search", | |
| ) | |
| query_in.submit( | |
| fn=handle_search, | |
| inputs=[query_in, chatbot, display, sort], | |
| outputs=[chatbot, query_in], | |
| api_name="search_submit", | |
| ) | |
| def clear_chat(): | |
| return [] | |
| clear_btn.click(fn=clear_chat, inputs=[], outputs=[chatbot], api_name="clear") | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |