ginipick commited on
Commit
c6beee2
·
verified ·
1 Parent(s): a1e836a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -28
app.py CHANGED
@@ -3,7 +3,7 @@ import requests
3
  import json
4
  import os
5
  from datetime import datetime, timedelta
6
- from concurrent.futures import ThreadPoolExecutor
7
  from functools import lru_cache
8
  from requests.adapters import HTTPAdapter
9
  from requests.packages.urllib3.util.retry import Retry
@@ -45,6 +45,9 @@ def convert_to_seoul_time(timestamp_str):
45
  return timestamp_str
46
 
47
  def analyze_sentiment_batch(articles, client):
 
 
 
48
  try:
49
  # 모든 기사의 제목과 내용을 하나의 텍스트로 결합
50
  combined_text = "\n\n".join([
@@ -52,7 +55,6 @@ def analyze_sentiment_batch(articles, client):
52
  for article in articles
53
  ])
54
 
55
- # f""" ... """ 형태로 여러 줄 문자열을 정확히 사용
56
  prompt = f"""다음 뉴스 모음에 대해 전반적인 감성 분석을 수행하세요:
57
 
58
  뉴스 내용:
@@ -96,6 +98,9 @@ def init_db():
96
  conn.close()
97
 
98
  def save_to_db(keyword, country, results):
 
 
 
99
  conn = sqlite3.connect("search_results.db")
100
  c = conn.cursor()
101
  seoul_tz = pytz.timezone('Asia/Seoul')
@@ -110,6 +115,9 @@ def save_to_db(keyword, country, results):
110
  conn.close()
111
 
112
  def load_from_db(keyword, country):
 
 
 
113
  conn = sqlite3.connect("search_results.db")
114
  c = conn.cursor()
115
  c.execute("SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
@@ -121,6 +129,9 @@ def load_from_db(keyword, country):
121
  return None, None
122
 
123
  def display_results(articles):
 
 
 
124
  output = ""
125
  for idx, article in enumerate(articles, 1):
126
  output += f"### {idx}. {article['title']}\n"
@@ -131,6 +142,9 @@ def display_results(articles):
131
  return output
132
 
133
  def search_company(company):
 
 
 
134
  error_message, articles = serphouse_search(company, "United States")
135
  if not error_message and articles:
136
  save_to_db(company, "United States", articles)
@@ -138,17 +152,29 @@ def search_company(company):
138
  return f"{company}에 대한 검색 결과가 없습니다."
139
 
140
  def load_company(company):
 
 
 
141
  results, timestamp = load_from_db(company, "United States")
142
  if results:
143
  return f"### {company} 검색 결과\n저장 시간: {timestamp}\n\n" + display_results(results)
144
  return f"{company}에 대한 저장된 결과가 없습니다."
145
 
146
  def show_stats():
 
 
 
 
 
 
 
147
  conn = sqlite3.connect("search_results.db")
148
  c = conn.cursor()
149
 
150
  output = "## 한국 기업 뉴스 분석 리포트\n\n"
151
 
 
 
152
  for company in KOREAN_COMPANIES:
153
  c.execute("""
154
  SELECT results, timestamp
@@ -158,36 +184,70 @@ def show_stats():
158
  LIMIT 1
159
  """, (company,))
160
 
161
- result = c.fetchone()
162
- if result:
163
- results_json, timestamp = result
164
  articles = json.loads(results_json)
165
  seoul_time = convert_to_seoul_time(timestamp)
166
-
167
- output += f"### {company}\n"
168
- output += f"- 마지막 업데이트: {seoul_time}\n"
169
- output += f"- 저장된 기사 수: {len(articles)}건\n\n"
170
-
171
- if articles:
172
- sentiment_analysis = analyze_sentiment_batch(articles, client)
173
- output += "#### 뉴스 감성 분석\n"
174
- output += f"{sentiment_analysis}\n\n"
175
-
176
- output += "---\n\n"
177
 
178
  conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  return output
180
 
 
 
181
  def search_all_companies():
 
 
 
 
182
  overall_result = "# [전체 검색 결과]\n\n"
183
- for comp in KOREAN_COMPANIES:
184
- overall_result += f"## {comp}\n"
185
- overall_result += search_company(comp)
186
- overall_result += "\n"
 
 
 
 
 
 
 
187
  return overall_result
188
 
189
  def load_all_companies():
 
 
 
 
190
  overall_result = "# [전체 출력 결과]\n\n"
 
191
  for comp in KOREAN_COMPANIES:
192
  overall_result += f"## {comp}\n"
193
  overall_result += load_company(comp)
@@ -195,17 +255,23 @@ def load_all_companies():
195
  return overall_result
196
 
197
  def full_summary_report():
198
- # 1) 전체 검색
 
 
 
 
199
  search_result_text = search_all_companies()
200
- # 2) 전체 출력
 
201
  load_result_text = load_all_companies()
202
- # 3) 전체 통계
 
203
  stats_text = show_stats()
204
 
205
  combined_report = (
206
  "# 전체 분석 보고 요약\n\n"
207
  "아래 순서로 실행되었습니다:\n"
208
- "1. 모든 종목 검색 → 2. 모든 종목 DB 결과 출력 → 3. 전체 감성 분석 통계\n\n"
209
  f"{search_result_text}\n\n"
210
  f"{load_result_text}\n\n"
211
  "## [전체 감성 분석 통계]\n\n"
@@ -213,6 +279,47 @@ def full_summary_report():
213
  )
214
  return combined_report
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  ACCESS_TOKEN = os.getenv("HF_TOKEN")
217
  if not ACCESS_TOKEN:
218
  raise ValueError("HF_TOKEN environment variable is not set")
@@ -224,6 +331,8 @@ client = OpenAI(
224
 
225
  API_KEY = os.getenv("SERPHOUSE_API_KEY")
226
 
 
 
227
  COUNTRY_LANGUAGES = {
228
  "United States": "en",
229
  "KOREA": "ko",
@@ -366,8 +475,12 @@ COUNTRY_LOCATIONS = {
366
  "Iceland": "Iceland"
367
  }
368
 
 
369
  @lru_cache(maxsize=100)
370
  def translate_query(query, country):
 
 
 
371
  try:
372
  if is_english(query):
373
  return query
@@ -404,6 +517,10 @@ def is_english(text):
404
  return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
405
 
406
  def search_serphouse(query, country, page=1, num_result=10):
 
 
 
 
407
  url = "https://api.serphouse.com/serp/live"
408
 
409
  now = datetime.utcnow()
@@ -420,7 +537,7 @@ def search_serphouse(query, country, page=1, num_result=10):
420
  "lang": COUNTRY_LANGUAGES.get(country, "en"),
421
  "device": "desktop",
422
  "serp_type": "news",
423
- "page": "1",
424
  "num": "100",
425
  "date_range": date_range,
426
  "sort_by": "date"
@@ -474,6 +591,10 @@ def search_serphouse(query, country, page=1, num_result=10):
474
  }
475
 
476
  def format_results_from_raw(response_data):
 
 
 
 
477
  if "error" in response_data:
478
  return "Error: " + response_data["error"], []
479
 
@@ -481,10 +602,12 @@ def format_results_from_raw(response_data):
481
  results = response_data["results"]
482
  translated_query = response_data["translated_query"]
483
 
 
484
  news_results = results.get('results', {}).get('results', {}).get('news', [])
485
  if not news_results:
486
  return "검색 결과가 없습니다.", []
487
 
 
488
  korean_domains = [
489
  '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
490
  'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
@@ -502,9 +625,10 @@ def format_results_from_raw(response_data):
502
 
503
  is_korean_content = (
504
  any(domain in url or domain in channel for domain in korean_domains) or
505
- any(keyword in title.lower() for keyword in korean_keywords)
506
  )
507
 
 
508
  if not is_korean_content:
509
  filtered_articles.append({
510
  "index": idx,
@@ -522,9 +646,14 @@ def format_results_from_raw(response_data):
522
  return f"결과 처리 중 오류 발생: {str(e)}", []
523
 
524
  def serphouse_search(query, country):
 
 
 
525
  response_data = search_serphouse(query, country)
526
  return format_results_from_raw(response_data)
527
 
 
 
528
  css = """
529
  /* 전역 스타일 */
530
  footer {visibility: hidden;}
@@ -700,15 +829,44 @@ footer {visibility: hidden;}
700
  }
701
  """
702
 
 
 
703
  with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
704
  init_db()
705
 
706
  with gr.Tabs():
707
- # 첫 번째 탭 (DB 검색)
708
  with gr.Tab("Earnbot"):
709
  gr.Markdown("## EarnBot: 글로벌 빅테크 기업 및 투자 전망 AI 자동 분석")
710
- gr.Markdown(" '전체 분석 보고 요약' 클릭시 전체 자동 보고 생성, 개별은 '검색(DB 자동 저장)'과 '출력(DB자동 호출)' 사용")
711
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  with gr.Row():
713
  full_report_btn = gr.Button("전체 분석 보고 요약", variant="primary")
714
  full_report_display = gr.Markdown()
@@ -718,6 +876,7 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비
718
  outputs=full_report_display
719
  )
720
 
 
721
  with gr.Column():
722
  for i in range(0, len(KOREAN_COMPANIES), 2):
723
  with gr.Row():
 
3
  import json
4
  import os
5
  from datetime import datetime, timedelta
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
  from functools import lru_cache
8
  from requests.adapters import HTTPAdapter
9
  from requests.packages.urllib3.util.retry import Retry
 
45
  return timestamp_str
46
 
47
  def analyze_sentiment_batch(articles, client):
48
+ """
49
+ OpenAI API를 통해 뉴스 기사들의 종합 감성 분석을 수행
50
+ """
51
  try:
52
  # 모든 기사의 제목과 내용을 하나의 텍스트로 결합
53
  combined_text = "\n\n".join([
 
55
  for article in articles
56
  ])
57
 
 
58
  prompt = f"""다음 뉴스 모음에 대해 전반적인 감성 분석을 수행하세요:
59
 
60
  뉴스 내용:
 
98
  conn.close()
99
 
100
  def save_to_db(keyword, country, results):
101
+ """
102
+ 특정 (keyword, country) 조합에 대한 검색 결과를 DB에 저장
103
+ """
104
  conn = sqlite3.connect("search_results.db")
105
  c = conn.cursor()
106
  seoul_tz = pytz.timezone('Asia/Seoul')
 
115
  conn.close()
116
 
117
  def load_from_db(keyword, country):
118
+ """
119
+ 특정 (keyword, country) 조합에 대한 가장 최근 검색 결과를 DB에서 불러오기
120
+ """
121
  conn = sqlite3.connect("search_results.db")
122
  c = conn.cursor()
123
  c.execute("SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
 
129
  return None, None
130
 
131
  def display_results(articles):
132
+ """
133
+ 뉴스 기사 목록을 Markdown 문자열로 변환하여 반환
134
+ """
135
  output = ""
136
  for idx, article in enumerate(articles, 1):
137
  output += f"### {idx}. {article['title']}\n"
 
142
  return output
143
 
144
  def search_company(company):
145
+ """
146
+ 단일 기업(또는 키워드)에 대해 미국 뉴스 검색, DB 저장 후 결과 Markdown 반환
147
+ """
148
  error_message, articles = serphouse_search(company, "United States")
149
  if not error_message and articles:
150
  save_to_db(company, "United States", articles)
 
152
  return f"{company}에 대한 검색 결과가 없습니다."
153
 
154
  def load_company(company):
155
+ """
156
+ DB에서 단일 기업(또는 키워드)의 미국 뉴스 검색 결과를 불러와 Markdown 반환
157
+ """
158
  results, timestamp = load_from_db(company, "United States")
159
  if results:
160
  return f"### {company} 검색 결과\n저장 시간: {timestamp}\n\n" + display_results(results)
161
  return f"{company}에 대한 저장된 결과가 없습니다."
162
 
163
  def show_stats():
164
+ """
165
+ KOREAN_COMPANIES 목록 내 모든 기업에 대해:
166
+ - 가장 최근 DB 저장 일자
167
+ - 기사 수
168
+ - 감성 분석 결과
169
+ 를 순차(또는 병렬)로 조회하여 보고서 형태로 반환
170
+ """
171
  conn = sqlite3.connect("search_results.db")
172
  c = conn.cursor()
173
 
174
  output = "## 한국 기업 뉴스 분석 리포트\n\n"
175
 
176
+ # 모든 기업에 대해 DB에서 읽어올 (company, timestamp, articles) 목록 수집
177
+ data_list = []
178
  for company in KOREAN_COMPANIES:
179
  c.execute("""
180
  SELECT results, timestamp
 
184
  LIMIT 1
185
  """, (company,))
186
 
187
+ row = c.fetchone()
188
+ if row:
189
+ results_json, timestamp = row
190
  articles = json.loads(results_json)
191
  seoul_time = convert_to_seoul_time(timestamp)
192
+ data_list.append((company, seoul_time, articles))
 
 
 
 
 
 
 
 
 
 
193
 
194
  conn.close()
195
+
196
+ # (옵션) 각 기업 감성 분석을 병렬 처리
197
+ def analyze_data(item):
198
+ comp, tstamp, arts = item
199
+ sentiment = ""
200
+ if arts:
201
+ sentiment = analyze_sentiment_batch(arts, client)
202
+ return (comp, tstamp, len(arts), sentiment)
203
+
204
+ # ThreadPoolExecutor로 병렬 감성 분석
205
+ results_list = []
206
+ with ThreadPoolExecutor(max_workers=5) as executor:
207
+ futures = [executor.submit(analyze_data, dl) for dl in data_list]
208
+ for future in as_completed(futures):
209
+ results_list.append(future.result())
210
+
211
+ # 결과 정렬(원하는 순서대로) - 여기서는 기업명 기준 or 그냥 순서 없음
212
+ for comp, tstamp, count, sentiment in results_list:
213
+ output += f"### {comp}\n"
214
+ output += f"- 마지막 업데이트: {tstamp}\n"
215
+ output += f"- 저장된 기사 수: {count}건\n\n"
216
+ if sentiment:
217
+ output += "#### 뉴스 감성 분석\n"
218
+ output += f"{sentiment}\n\n"
219
+ output += "---\n\n"
220
+
221
  return output
222
 
223
+
224
+ ### (1) 전체 검색: 멀티스레드 적용
225
  def search_all_companies():
226
+ """
227
+ KOREAN_COMPANIES 리스트 내 모든 기업에 대해,
228
+ 검색을 병렬(쓰레드)로 수행 후 결과를 합쳐 Markdown 형태로 반환
229
+ """
230
  overall_result = "# [전체 검색 결과]\n\n"
231
+
232
+ def do_search(comp):
233
+ return comp, search_company(comp)
234
+
235
+ with ThreadPoolExecutor(max_workers=5) as executor:
236
+ futures = [executor.submit(do_search, c) for c in KOREAN_COMPANIES]
237
+ for future in as_completed(futures):
238
+ comp, res_text = future.result()
239
+ overall_result += f"## {comp}\n"
240
+ overall_result += res_text + "\n\n"
241
+
242
  return overall_result
243
 
244
  def load_all_companies():
245
+ """
246
+ KOREAN_COMPANIES 리스트 내 모든 기업에 대해,
247
+ DB에서 불러온 결과를 순차(또는 병렬)로 합쳐서 Markdown 형태로 반환
248
+ """
249
  overall_result = "# [전체 출력 결과]\n\n"
250
+
251
  for comp in KOREAN_COMPANIES:
252
  overall_result += f"## {comp}\n"
253
  overall_result += load_company(comp)
 
255
  return overall_result
256
 
257
  def full_summary_report():
258
+ """
259
+ (1) 모든 기업 검색 -> (2) DB에서 모든 기업 불러오기 -> (3) 감성 분석 통계
260
+ 순서대로 실행하여, 전체 리포트를 합쳐 반환
261
+ """
262
+ # 1) 전체 검색(병렬)
263
  search_result_text = search_all_companies()
264
+
265
+ # 2) 전체 출력(순차)
266
  load_result_text = load_all_companies()
267
+
268
+ # 3) 전체 통계(감성 분석)
269
  stats_text = show_stats()
270
 
271
  combined_report = (
272
  "# 전체 분석 보고 요약\n\n"
273
  "아래 순서로 실행되었습니다:\n"
274
+ "1. 모든 종목 검색(병렬) → 2. 모든 종목 DB 결과 출력 → 3. 전체 감성 분석 통계\n\n"
275
  f"{search_result_text}\n\n"
276
  f"{load_result_text}\n\n"
277
  "## [전체 감성 분석 통계]\n\n"
 
279
  )
280
  return combined_report
281
 
282
+
283
+ ### (2) 사용자 임의 검색 + 국가 선택 기능
284
+ def search_custom(query, country):
285
+ """
286
+ 사용자가 입력한 (query, country)를 대상으로
287
+ - 검색 (API 요청)
288
+ - DB 저장
289
+ - DB 로드 후 감성 분석
290
+ - 최종 결과를 Markdown 형태로 반환
291
+ """
292
+ # 1) 검색
293
+ error_message, articles = serphouse_search(query, country)
294
+ if error_message:
295
+ return f"오류 발생: {error_message}"
296
+ if not articles:
297
+ return "검색 결과가 없습니다."
298
+
299
+ # 2) DB 저장
300
+ save_to_db(query, country, articles)
301
+
302
+ # 3) DB에서 다시 불러오기
303
+ results, timestamp = load_from_db(query, country)
304
+ if not results:
305
+ return f"DB 로드 실패: 저장된 결과가 없습니다."
306
+
307
+ # 4) 감성 분석
308
+ sentiment_analysis = analyze_sentiment_batch(results, client)
309
+
310
+ # 5) 최종 리포트(기사 목록 + 감성 분석)
311
+ output = f"## [사용자 임의 검색 결과]\n\n"
312
+ output += f"**키워드**: {query}\n\n"
313
+ output += f"**국가**: {country}\n\n"
314
+ output += f"**저장 시간**: {timestamp}\n\n"
315
+ output += display_results(results)
316
+
317
+ output += "### 뉴스 감성 분석\n"
318
+ output += f"{sentiment_analysis}\n"
319
+ return output
320
+
321
+
322
+ ### (필수) API 인증
323
  ACCESS_TOKEN = os.getenv("HF_TOKEN")
324
  if not ACCESS_TOKEN:
325
  raise ValueError("HF_TOKEN environment variable is not set")
 
331
 
332
  API_KEY = os.getenv("SERPHOUSE_API_KEY")
333
 
334
+
335
+ ### 국가별 설정
336
  COUNTRY_LANGUAGES = {
337
  "United States": "en",
338
  "KOREA": "ko",
 
475
  "Iceland": "Iceland"
476
  }
477
 
478
+
479
  @lru_cache(maxsize=100)
480
  def translate_query(query, country):
481
+ """
482
+ Google Translation API(비공식) 사용하여 검색어를 해당 국가 언어로 번역
483
+ """
484
  try:
485
  if is_english(query):
486
  return query
 
517
  return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
518
 
519
  def search_serphouse(query, country, page=1, num_result=10):
520
+ """
521
+ SerpHouse API에 실시간 검색 요청을 보내어,
522
+ '뉴스' 탭 (sort_by=date)에서 해당 query에 대한 기사 목록을 가져온다.
523
+ """
524
  url = "https://api.serphouse.com/serp/live"
525
 
526
  now = datetime.utcnow()
 
537
  "lang": COUNTRY_LANGUAGES.get(country, "en"),
538
  "device": "desktop",
539
  "serp_type": "news",
540
+ "page": str(page),
541
  "num": "100",
542
  "date_range": date_range,
543
  "sort_by": "date"
 
591
  }
592
 
593
  def format_results_from_raw(response_data):
594
+ """
595
+ SerpHouse API의 응답 데이터를 가공하여,
596
+ (에러메시지, 기사리스트) 형태로 반환.
597
+ """
598
  if "error" in response_data:
599
  return "Error: " + response_data["error"], []
600
 
 
602
  results = response_data["results"]
603
  translated_query = response_data["translated_query"]
604
 
605
+ # 실제 뉴스 결과
606
  news_results = results.get('results', {}).get('results', {}).get('news', [])
607
  if not news_results:
608
  return "검색 결과가 없습니다.", []
609
 
610
+ # 한국 도메인 및 한국 관련 키워드 포함 기사 제외
611
  korean_domains = [
612
  '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
613
  'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
 
625
 
626
  is_korean_content = (
627
  any(domain in url or domain in channel for domain in korean_domains) or
628
+ any(keyword in title for keyword in korean_keywords)
629
  )
630
 
631
+ # 한국어 뉴스(또는 한국 도메인) 제외
632
  if not is_korean_content:
633
  filtered_articles.append({
634
  "index": idx,
 
646
  return f"결과 처리 중 오류 발생: {str(e)}", []
647
 
648
  def serphouse_search(query, country):
649
+ """
650
+ 검색 및 결과 포매팅까지 일괄 처리
651
+ """
652
  response_data = search_serphouse(query, country)
653
  return format_results_from_raw(response_data)
654
 
655
+
656
+ # CSS (UI 커스터마이징)
657
  css = """
658
  /* 전역 스타일 */
659
  footer {visibility: hidden;}
 
829
  }
830
  """
831
 
832
+ import gradio as gr
833
+
834
  with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
835
  init_db()
836
 
837
  with gr.Tabs():
838
+ # 첫 번째 탭
839
  with gr.Tab("Earnbot"):
840
  gr.Markdown("## EarnBot: 글로벌 빅테크 기업 및 투자 전망 AI 자동 분석")
841
+ gr.Markdown(" * '전체 분석 보고 요약' 클릭 전체 자동 보고 생성.\n * 아래 개별 종목의 '검색(DB 자동 저장)'과 '출력(DB 자동 호출)' 가능.\n * 추가로, 원하는 임의 키워드 및 국가로 검색/분석할 수도 있습니다.")
842
 
843
+ # (2) 사용자 임의 검색 섹션
844
+ with gr.Group():
845
+ gr.Markdown("### 사용자 임의 검색")
846
+ with gr.Row():
847
+ with gr.Column():
848
+ user_input = gr.Textbox(
849
+ label="검색어 입력",
850
+ placeholder="예) Apple, Samsung 등 자유롭게"
851
+ )
852
+ with gr.Column():
853
+ country_selection = gr.Dropdown(
854
+ choices=list(COUNTRY_LOCATIONS.keys()),
855
+ value="United States",
856
+ label="국가 선택"
857
+ )
858
+ with gr.Column():
859
+ custom_search_btn = gr.Button("실행", variant="primary")
860
+
861
+ custom_search_output = gr.Markdown()
862
+
863
+ custom_search_btn.click(
864
+ fn=search_custom,
865
+ inputs=[user_input, country_selection],
866
+ outputs=custom_search_output
867
+ )
868
+
869
+ # 전체 분석 보고 요약 버튼
870
  with gr.Row():
871
  full_report_btn = gr.Button("전체 분석 보고 요약", variant="primary")
872
  full_report_display = gr.Markdown()
 
876
  outputs=full_report_display
877
  )
878
 
879
+ # 기존 개별 기업 검색/출력 영역
880
  with gr.Column():
881
  for i in range(0, len(KOREAN_COMPANIES), 2):
882
  with gr.Row():