seawolf2357 commited on
Commit
cef3bdf
Β·
verified Β·
1 Parent(s): 983f442

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +580 -221
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- AI 기반 μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ - 슀트리밍 κ°•ν™” 버전 + Brave 웹검색
3
  Dataset: https://huggingface.co/datasets/ginipick/market
4
  """
5
  import gradio as gr
@@ -168,34 +168,15 @@ class MarketAnalyzer:
168
  """포괄적인 μΈμ‚¬μ΄νŠΈ 생성"""
169
  insights = []
170
 
171
- # 1. 업쒅별 점포 수 (μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜)
172
  insights.append(self._create_top_categories_chart())
173
-
174
- # 2. λŒ€λΆ„λ₯˜λ³„ 뢄포 (파이 차트)
175
  insights.append(self._create_major_category_pie())
176
-
177
- # 3. 측별 뢄포 상세 뢄석
178
  insights.append(self._create_floor_analysis())
179
-
180
- # 4. 지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜
181
  insights.append(self._create_diversity_index())
182
-
183
- # 5. ν”„λžœμ°¨μ΄μ¦ˆ vs κ°œμΈμ‚¬μ—…μž 뢄석
184
  insights.append(self._create_franchise_analysis())
185
-
186
- # 6. 업쒅별 μΈ΅ μ„ ν˜Έλ„
187
  insights.append(self._create_floor_preference())
188
-
189
- # 7. μ‹œκ΅°κ΅¬λ³„ μƒκΆŒ 밀집도 TOP 20
190
  insights.append(self._create_district_density())
191
-
192
- # 8. μ—…μ’… 상관관계 (같은 지역에 자주 λ‚˜νƒ€λ‚˜λŠ” μ—…μ’…)
193
  insights.append(self._create_category_correlation())
194
-
195
- # 9. μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ (μƒμœ„ 20개)
196
  insights.append(self._create_subcategory_trends())
197
-
198
- # 10. 지역별 νŠΉν™” μ—…μ’…
199
  insights.append(self._create_regional_specialization())
200
 
201
  return insights
@@ -244,7 +225,6 @@ class MarketAnalyzer:
244
  floor_data = self.df['측정보_숫자'].dropna()
245
  floor_counts = floor_data.value_counts().sort_index()
246
 
247
- # μ§€ν•˜, 1μΈ΅, 2μΈ΅ μ΄μƒμœΌλ‘œ κ·Έλ£Ήν™”
248
  underground = floor_counts[floor_counts.index < 0].sum()
249
  first_floor = floor_counts.get(1, 0)
250
  upper_floors = floor_counts[floor_counts.index > 1].sum()
@@ -273,9 +253,8 @@ class MarketAnalyzer:
273
  if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
274
  return None
275
 
276
- # 각 μ‹œκ΅°κ΅¬λ³„ μ—…μ’… λ‹€μ–‘μ„± 계산 (μ—…μ’… 수 / 전체 점포 수)
277
  diversity_data = []
278
- for district in self.df['μ‹œκ΅°κ΅¬λͺ…'].unique()[:20]: # μƒμœ„ 20개
279
  district_df = self.df[self.df['μ‹œκ΅°κ΅¬λͺ…'] == district]
280
  num_categories = district_df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique()
281
  total_stores = len(district_df)
@@ -307,7 +286,6 @@ class MarketAnalyzer:
307
  if 'λΈŒλžœλ“œλͺ…' not in self.df.columns:
308
  return None
309
 
310
- # λΈŒλžœλ“œλͺ…이 있으면 ν”„λžœμ°¨μ΄μ¦ˆλ‘œ κ°„μ£Ό
311
  franchise_count = self.df['λΈŒλžœλ“œλͺ…'].notna().sum()
312
  individual_count = self.df['λΈŒλžœλ“œλͺ…'].isna().sum()
313
 
@@ -333,7 +311,6 @@ class MarketAnalyzer:
333
  if '측정보_숫자' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
334
  return None
335
 
336
- # μƒμœ„ 10개 μ—…μ’… 선택
337
  top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(10).index
338
  floor_pref_data = []
339
 
@@ -390,21 +367,17 @@ class MarketAnalyzer:
390
  return {'type': 'plot', 'data': fig, 'title': 'μ§€μ—­ 밀집도 뢄석'}
391
 
392
  def _create_category_correlation(self) -> Dict:
393
- """μ—…μ’… 상관관계 (같은 지역에 자주 λ‚˜νƒ€λ‚˜λŠ” μ—…μ’…)"""
394
  if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
395
  return None
396
 
397
- # μƒμœ„ 10개 μ—…μ’…λ§Œ 선택
398
  top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(10).index.tolist()
399
-
400
- # 각 μ‹œκ΅°κ΅¬λ³„λ‘œ μ—…μ’… 쑴재 μ—¬λΆ€ 맀트릭슀 생성
401
  districts = self.df['μ‹œκ΅°κ΅¬λͺ…'].unique()
402
  correlation_matrix = np.zeros((len(top_categories), len(top_categories)))
403
 
404
  for i, cat1 in enumerate(top_categories):
405
  for j, cat2 in enumerate(top_categories):
406
  if i != j:
407
- # 두 업쒅이 같은 μ‹œκ΅°κ΅¬μ— μ‘΄μž¬ν•˜λŠ” 횟수
408
  coexist_count = 0
409
  for district in districts:
410
  district_df = self.df[self.df['μ‹œκ΅°κ΅¬λͺ…'] == district]
@@ -456,7 +429,6 @@ class MarketAnalyzer:
456
  if 'μ‹œλ„λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
457
  return None
458
 
459
- # 각 μ‹œλ„λ³„ μƒμœ„ 3개 μ—…μ’…
460
  specialization_data = []
461
  for region in self.df['μ‹œλ„λͺ…'].unique():
462
  region_df = self.df[self.df['μ‹œλ„λͺ…'] == region]
@@ -490,7 +462,6 @@ class MarketAnalyzer:
490
 
491
  m = folium.Map(location=[center_lat, center_lon], zoom_start=11, tiles='OpenStreetMap')
492
 
493
- # 히트맡
494
  heat_data = [[row['μœ„οΏ½οΏ½'], row['경도']] for _, row in df_sample.iterrows()]
495
  HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m)
496
 
@@ -522,7 +493,6 @@ class LLMQueryProcessor:
522
  """Fireworks AI 기반 μžμ—°μ–΄ 처리 (슀트리밍 지원 + 웹검색)"""
523
 
524
  def __init__(self, api_key: str = None):
525
- # ν™˜κ²½λ³€μˆ˜μ—μ„œ API ν‚€ κ°€μ Έμ˜€κΈ°
526
  self.api_key = api_key or os.getenv("FIREWORKS_API_KEY")
527
  self.base_url = "https://api.fireworks.ai/inference/v1/chat/completions"
528
 
@@ -532,7 +502,6 @@ class LLMQueryProcessor:
532
  def process_query_stream(self, query: str, data_context: Dict, chat_history: List = None, web_search_results: str = None):
533
  """μžμ—°μ–΄ 쿼리 처리 (슀트리밍 λͺ¨λ“œ) - 웹검색 κ²°κ³Ό 포함"""
534
 
535
- # 웹검색 κ²°κ³Όκ°€ 있으면 μ»¨ν…μŠ€νŠΈμ— μΆ”κ°€
536
  web_context = ""
537
  if web_search_results and "⚠️" not in web_search_results:
538
  web_context = f"""
@@ -551,7 +520,8 @@ class LLMQueryProcessor:
551
 
552
  ꡬ체적인 μˆ«μžμ™€ λΉ„μœ¨λ‘œ μ •λŸ‰μ  뢄석을 μ œκ³΅ν•˜μ„Έμš”.
553
  μ°½μ—…, 투자, 경쟁 뢄석 κ΄€μ μ—μ„œ μ‹€μš©μ  μΈμ‚¬μ΄νŠΈλ₯Ό μ œκ³΅ν•˜μ„Έμš”.
554
- μ›Ή 검색 κ²°κ³Όκ°€ 제곡된 경우 μ΅œμ‹  νŠΈλ Œλ“œμ™€ ν•¨κ»˜ λΆ„μ„ν•˜μ„Έμš”."""
 
555
 
556
  messages = [{"role": "system", "content": system_prompt}]
557
  if chat_history:
@@ -563,7 +533,7 @@ class LLMQueryProcessor:
563
  "max_tokens": 4800,
564
  "temperature": 0.7,
565
  "messages": messages,
566
- "stream": True # πŸ”₯ 슀트리밍 ν™œμ„±ν™”!
567
  }
568
 
569
  headers = {
@@ -577,16 +547,15 @@ class LLMQueryProcessor:
577
  headers=headers,
578
  json=payload,
579
  timeout=60,
580
- stream=True # 슀트리밍 λͺ¨λ“œ
581
  )
582
 
583
  if response.status_code == 200:
584
- # 슀트리밍 응닡 처리
585
  for line in response.iter_lines():
586
  if line:
587
  line_text = line.decode('utf-8')
588
  if line_text.startswith('data: '):
589
- data_str = line_text[6:] # 'data: ' 제거
590
  if data_str.strip() == '[DONE]':
591
  break
592
  try:
@@ -639,15 +608,16 @@ def load_data(regions):
639
 
640
  app_state.analyzer = MarketAnalyzer(df)
641
 
642
- # κΈ°λ³Έ 톡계
643
  stats = f"""
644
  βœ… **데이터 λ‘œλ“œ μ™„λ£Œ!**
645
-
646
- πŸ“Š **톡계**
647
- - 총 점포: {len(df):,}개
648
- - 뢄석 μ§€μ—­: {', '.join(regions)}
649
- - μ—…μ’… 수: {df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique()}개
650
- - λŒ€λΆ„λ₯˜: {df['μƒκΆŒμ—…μ’…λŒ€λΆ„λ₯˜λͺ…'].nunique()}개
 
 
651
  """
652
 
653
  return stats, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
@@ -670,7 +640,6 @@ def generate_insights():
670
  else:
671
  result.append(None)
672
 
673
- # λΆ€μ‘±ν•œ μ°¨νŠΈλŠ” None으둜 μ±„μš°κΈ°
674
  while len(result) < 11:
675
  result.append(None)
676
 
@@ -678,7 +647,7 @@ def generate_insights():
678
 
679
 
680
  def chat_respond(message, history):
681
- """챗봇 응닡 (슀트리밍 λͺ¨λ“œ + 웹검색) - Generator ν•¨μˆ˜"""
682
  if app_state.analyzer is None:
683
  yield history + [[message, "❌ λ¨Όμ € 데이터λ₯Ό λ‘œλ“œν•΄μ£Όμ„Έμš”!"]]
684
  return
@@ -686,39 +655,31 @@ def chat_respond(message, history):
686
  data_context = app_state.analyzer.analyze_for_llm()
687
 
688
  try:
689
- # LLM ν”„λ‘œμ„Έμ„œ μ΄ˆκΈ°ν™”
690
  if app_state.llm_processor is None:
691
  app_state.llm_processor = LLMQueryProcessor()
692
 
693
- # Brave Search ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™”
694
  if app_state.brave_client is None:
695
  try:
696
  app_state.brave_client = BraveSearchClient()
697
  except:
698
  app_state.brave_client = None
699
 
700
- # πŸ” μžλ™ 웹검색 μˆ˜ν–‰
701
  web_results = None
702
  if app_state.brave_client and app_state.brave_client.api_key:
703
- # 검색 쿼리 생성 (μ‚¬μš©μž μ§ˆλ¬Έμ—μ„œ 핡심 ν‚€μ›Œλ“œ μΆ”μΆœ)
704
  search_query = f"ν•œκ΅­ μƒκΆŒ μ°½μ—… νŠΈλ Œλ“œ {message}"
705
  web_results = app_state.brave_client.search(search_query, count=3)
706
 
707
- # λŒ€ν™” νžˆμŠ€ν† λ¦¬ ꡬ성
708
  chat_hist = []
709
  for user_msg, bot_msg in history:
710
  chat_hist.append({"role": "user", "content": user_msg})
711
  chat_hist.append({"role": "assistant", "content": bot_msg})
712
 
713
- # μƒˆ λ©”μ‹œμ§€ μΆ”κ°€
714
  history = history + [[message, ""]]
715
 
716
- # 웹검색 μƒνƒœ ν‘œμ‹œ
717
  if web_results and "⚠️" not in web_results:
718
  history[-1][1] = "πŸ” μ›Ή 검색 쀑...\n\n"
719
  yield history
720
 
721
- # 슀트리밍 응닡 (웹검색 κ²°κ³Ό 포함)
722
  full_response = ""
723
  for chunk in app_state.llm_processor.process_query_stream(message, data_context, chat_hist, web_results):
724
  full_response += chunk
@@ -726,7 +687,6 @@ def chat_respond(message, history):
726
  yield history
727
 
728
  except ValueError as e:
729
- # API ν‚€κ°€ μ—†λŠ” 경우 κΈ°λ³Έ 톡계 제곡
730
  response = f"""πŸ“Š **κΈ°λ³Έ 데이터 뢄석 κ²°κ³Ό**
731
 
732
  **전체 ν˜„ν™©**
@@ -738,119 +698,520 @@ def chat_respond(message, history):
738
  ν™˜κ²½λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”:
739
  ```bash
740
  export FIREWORKS_API_KEY="your_api_key_here"
741
- export BRAVE_API_KEY="your_brave_api_key_here" # μ›Ήκ²€μƒ‰μš© (선택)
742
- ```
743
-
744
- λ˜λŠ” Hugging Face Spaceμ—μ„œλŠ” Settings > Variables μ—μ„œ μ„€μ •ν•˜μ„Έμš”."""
745
 
746
  history = history + [[message, response]]
747
  yield history
748
 
749
 
750
  # ============================================================================
751
- # Gradio UI
752
  # ============================================================================
753
 
754
- with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro", theme=gr.themes.Soft()) as demo:
755
- gr.Markdown("""
756
- # πŸͺ AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro (슀트리밍 + 웹검색 πŸ”)
757
- *μ „κ΅­ 상가(μƒκΆŒ) 데이터 기반 μ‹€μ‹œκ°„ 뢄석 | Powered by Fireworks AI + Brave Search*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
 
759
- **✨ 10κ°€μ§€ 심측 뢄석 제곡**: μ—…μ’… νŠΈλ Œλ“œ, 경쟁 강도, μž…μ§€ 뢄석, ν”„λžœμ°¨μ΄μ¦ˆ λΉ„μœ¨, μ§€μ—­ νŠΉν™”, 측별 μ„ ν˜Έλ„ λ“±
760
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
 
762
- # μ›Ή λ°°μ§€ μΆ”κ°€
 
 
 
 
 
 
 
763
  gr.HTML("""
764
- <style>
765
- .badges-container {
766
- display: flex;
767
- justify-content: center;
768
- align-items: center;
769
- gap: 15px;
770
- flex-wrap: wrap;
771
- margin: 20px 0;
772
- }
773
-
774
- .badge {
775
- display: inline-flex;
776
- align-items: center;
777
- gap: 8px;
778
- padding: 10px 20px;
779
- border-radius: 25px;
780
- text-decoration: none;
781
- font-weight: 600;
782
- font-size: 0.95em;
783
- transition: all 0.3s ease;
784
- box-shadow: 0 4px 15px rgba(0,0,0,0.2);
785
- position: relative;
786
- overflow: hidden;
787
- }
788
-
789
- .badge::before {
790
- content: '';
791
- position: absolute;
792
- top: 0;
793
- left: -100%;
794
- width: 100%;
795
- height: 100%;
796
- background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
797
- transition: left 0.5s;
798
- }
799
-
800
- .badge:hover::before {
801
- left: 100%;
802
- }
803
-
804
- .badge:hover {
805
- transform: translateY(-3px);
806
- box-shadow: 0 6px 25px rgba(0,0,0,0.3);
807
- }
808
-
809
- .badge-kakao {
810
- background: linear-gradient(135deg, #FEE500 0%, #FFEB3B 100%);
811
- color: #3C1E1E;
812
- }
813
-
814
- .badge-kakao:hover {
815
- background: linear-gradient(135deg, #FFD700 0%, #FFC107 100%);
816
- }
817
-
818
- .badge-ginigen {
819
- background: linear-gradient(135deg, #00D9FF 0%, #0099FF 100%);
820
- color: white;
821
- }
822
-
823
- .badge-ginigen:hover {
824
- background: linear-gradient(135deg, #00C4E6 0%, #0080E6 100%);
825
- }
826
-
827
- .badge-icon {
828
- font-size: 1.2em;
829
- }
830
- </style>
831
 
832
- <div class="badges-container">
833
- <a href="https://open.kakao.com/o/peIe8KWh" target="_blank" class="badge badge-kakao">
834
- <span class="badge-icon">πŸ’¬</span>
 
 
 
 
 
 
 
 
835
  <span>μ˜€ν”ˆμ±„νŒ… λ°”λ‘œκ°€κΈ°</span>
836
  </a>
837
- <a href="https://ginigen.ai" target="_blank" class="badge badge-ginigen">
838
- <span class="badge-icon">🍌</span>
839
  <span>λ‚˜λ…Έ λ°”λ‚˜λ‚˜ μ• λ“œμ˜¨ 무료 μ„œλΉ„μŠ€</span>
840
  </a>
841
  </div>
842
  """)
843
-
844
 
845
- with gr.Row():
846
- with gr.Column(scale=1):
847
- gr.Markdown("### βš™οΈ μ„€μ •")
 
 
 
 
 
848
 
849
- # ν™˜κ²½λ³€μˆ˜ μƒνƒœ ν‘œμ‹œ
850
- api_status = "βœ… API ν‚€ 섀정됨" if os.getenv("FIREWORKS_API_KEY") else "⚠️ API ν‚€ λ―Έμ„€μ • (κΈ°λ³Έ ν†΅κ³„λ§Œ 제곡)"
851
- brave_status = "βœ… 웹검색 ν™œμ„±ν™”" if os.getenv("BRAVE_API_KEY") else "⚠️ 웹검색 λΉ„ν™œμ„±ν™”"
852
- gr.Markdown(f"**πŸ”‘ Fireworks AI**: {api_status}")
853
- gr.Markdown(f"**πŸ” Brave Search**: {brave_status}")
854
 
855
  region_select = gr.CheckboxGroup(
856
  choices=list(MarketDataLoader.REGIONS.keys()),
@@ -858,12 +1219,22 @@ with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro", theme=gr.themes.Soft()) a
858
  label="πŸ“ 뢄석 μ§€μ—­ 선택 (μ΅œλŒ€ 5개 ꢌμž₯)"
859
  )
860
 
861
- load_btn = gr.Button("πŸ“Š 데이터 λ‘œλ“œ", variant="primary", size="lg")
 
 
 
 
862
 
863
- status_box = gr.Markdown("πŸ‘ˆ 지역을 μ„ νƒν•˜κ³  데이터λ₯Ό λ‘œλ“œν•˜μ„Έμš”!")
864
-
865
- with gr.Column(scale=3):
 
 
 
 
 
866
  with gr.Tabs() as tabs:
 
867
  with gr.Tab("πŸ“Š μΈμ‚¬μ΄νŠΈ λŒ€μ‹œλ³΄λ“œ", id=0) as tab1:
868
  insights_content = gr.Column(visible=False)
869
 
@@ -875,38 +1246,42 @@ with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro", theme=gr.themes.Soft()) a
875
  gr.Markdown("### πŸ“ˆ 10κ°€μ§€ 심측 μƒκΆŒ μΈμ‚¬μ΄νŠΈ")
876
 
877
  with gr.Row():
878
- chart1 = gr.Plot(label="업쒅별 점포 수")
879
- chart2 = gr.Plot(label="λŒ€λΆ„λ₯˜ 뢄포")
880
 
881
  with gr.Row():
882
- chart3 = gr.Plot(label="측별 뢄포")
883
- chart4 = gr.Plot(label="μ—…μ’… λ‹€μ–‘μ„±")
884
 
885
  with gr.Row():
886
- chart5 = gr.Plot(label="ν”„λžœμ°¨μ΄μ¦ˆ 뢄석")
887
- chart6 = gr.Plot(label="μΈ΅ μ„ ν˜Έλ„")
888
 
889
  with gr.Row():
890
- chart7 = gr.Plot(label="μ§€μ—­ 밀집도")
891
- chart8 = gr.Plot(label="μ—…μ’… 상관관계")
892
 
893
  with gr.Row():
894
- chart9 = gr.Plot(label="μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ")
895
- chart10 = gr.Plot(label="μ§€μ—­ νŠΉν™”")
896
 
897
- with gr.Tab("πŸ€– AI 뢄석 챗봇 (μ‹€μ‹œκ°„ 슀트리밍 + 웹검색 βš‘πŸ”)", id=1) as tab2:
 
898
  chat_content = gr.Column(visible=False)
899
 
900
  with chat_content:
901
  gr.Markdown("""
902
- ### πŸ’‘ μƒ˜ν”Œ 질문
903
- κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…? | μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­? | 1측이 μœ λ¦¬ν•œ μ—…μ’…? | ν”„λžœμ°¨μ΄μ¦ˆ 점유율?
904
-
905
- ⚑ **슀트리밍 λͺ¨λ“œ**: AI 응닡이 μ‹€μ‹œκ°„μœΌλ‘œ ν‘œμ‹œλ©λ‹ˆλ‹€!
906
- πŸ” **웹검색 μžλ™**: μ΅œμ‹  μƒκΆŒ νŠΈλ Œλ“œλ₯Ό μžλ™μœΌλ‘œ κ²€μƒ‰ν•˜μ—¬ λ°˜μ˜ν•©λ‹ˆλ‹€!
907
  """)
908
 
909
- chatbot = gr.Chatbot(height=400, label="AI μƒκΆŒ 뢄석 μ–΄μ‹œμŠ€ν„΄νŠΈ")
 
 
 
910
 
911
  with gr.Row():
912
  msg_input = gr.Textbox(
@@ -914,15 +1289,37 @@ with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro", theme=gr.themes.Soft()) a
914
  show_label=False,
915
  scale=4
916
  )
917
- submit_btn = gr.Button("전솑", variant="primary", scale=1)
918
 
919
- # μƒ˜ν”Œ λ²„νŠΌλ“€
920
  with gr.Row():
921
- sample_btn1 = gr.Button("κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…?", size="sm")
922
- sample_btn2 = gr.Button("μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­?", size="sm")
923
- sample_btn3 = gr.Button("1측이 μœ λ¦¬ν•œ μ—…μ’…?", size="sm")
924
- sample_btn4 = gr.Button("ν”„λžœμ°¨μ΄μ¦ˆ 점유율?", size="sm")
925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  # 이벀트 ν•Έλ“€λŸ¬
927
  load_btn.click(
928
  fn=load_data,
@@ -933,7 +1330,7 @@ with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro", theme=gr.themes.Soft()) a
933
  outputs=[map_output, chart1, chart2, chart3, chart4, chart5, chart6, chart7, chart8, chart9, chart10]
934
  )
935
 
936
- # 챗봇 이벀트 (슀트리밍 λͺ¨λ“œ)
937
  submit_btn.click(
938
  fn=chat_respond,
939
  inputs=[msg_input, chatbot],
@@ -959,49 +1356,11 @@ with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ Pro", theme=gr.themes.Soft()) a
959
  yield result
960
  return handler
961
 
962
- sample_btn1.click(fn=create_sample_click("κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…?"), inputs=[chatbot], outputs=[chatbot])
963
- sample_btn2.click(fn=create_sample_click("μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­?"), inputs=[chatbot], outputs=[chatbot])
964
- sample_btn3.click(fn=create_sample_click("1측이 μœ λ¦¬ν•œ μ—…μ’…?"), inputs=[chatbot], outputs=[chatbot])
965
- sample_btn4.click(fn=create_sample_click("ν”„λžœμ°¨μ΄μ¦ˆ 점유율?"), inputs=[chatbot], outputs=[chatbot])
966
-
967
- gr.Markdown("""
968
- ---
969
- ### πŸ“– μ‚¬μš© κ°€μ΄λ“œ
970
- 1. μ§€μ—­ 선택 β†’ 2. 데이터 λ‘œλ“œ β†’ 3. 10κ°€μ§€ μΈμ‚¬μ΄νŠΈ 확인 λ˜λŠ” AIμ—κ²Œ 질문
971
-
972
- ### πŸ”‘ AI 챗봇 + 웹검색 ν™œμ„±ν™” 방법
973
- ν™˜κ²½λ³€μˆ˜ μ„€μ •:
974
- ```bash
975
- export FIREWORKS_API_KEY="your_api_key_here"
976
- export BRAVE_API_KEY="your_brave_api_key_here" # 웹검색 κΈ°λŠ₯용 (선택)
977
- ```
978
-
979
- Hugging Face Spaceμ—μ„œλŠ”:
980
- 1. Settings 메뉴 클릭
981
- 2. Variables νƒ­ 선택
982
- 3. New variable μΆ”κ°€:
983
- - `FIREWORKS_API_KEY` (ν•„μˆ˜ - AI λΆ„μ„μš©)
984
- - `BRAVE_API_KEY` (선택 - 웹검색 ν™œμ„±ν™”μš©)
985
-
986
- 🌐 **웹검색 κΈ°λŠ₯**: BRAVE_API_KEY μ„€μ • μ‹œ μžλ™μœΌλ‘œ μ΅œμ‹  μƒκΆŒ νŠΈλ Œλ“œλ₯Ό κ²€μƒ‰ν•˜μ—¬ 닡변에 λ°˜μ˜ν•©λ‹ˆλ‹€!
987
-
988
- ### πŸ“Š μ œκ³΅λ˜λŠ” 10κ°€μ§€ 뢄석
989
- 1. **업쒅별 점포 수**: κ°€μž₯ λ§Žμ€ μ—…μ’… TOP 15
990
- 2. **λŒ€λΆ„λ₯˜ 뢄포**: μ†Œλ§€/μŒμ‹/μ„œλΉ„μŠ€ λ“± λŒ€λΆ„λ₯˜ λΉ„μœ¨
991
- 3. **측별 뢄포**: μ§€ν•˜/1μΈ΅/상측 μž…μ§€ 뢄석
992
- 4. **μ—…μ’… λ‹€μ–‘μ„±**: 지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜
993
- 5. **ν”„λžœμ°¨μ΄μ¦ˆ 뢄석**: 개인 vs ν”„λžœμ°¨μ΄μ¦ˆ λΉ„μœ¨
994
- 6. **μΈ΅ μ„ ν˜Έλ„**: 업쒅별 μ„ ν˜Έ 측수
995
- 7. **μ§€μ—­ 밀집도**: 점포 수 μƒμœ„ μ§€μ—­
996
- 8. **μ—…μ’… 상관관계**: 같이 λ‚˜νƒ€λ‚˜λŠ” μ—…μ’… νŒ¨ν„΄
997
- 9. **μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ**: μ„ΈλΆ€ μ—…μ’… 뢄포
998
- 10. **μ§€μ—­ νŠΉν™”**: 각 μ§€μ—­μ˜ νŠΉν™” μ—…μ’…
999
-
1000
- πŸ’‘ **Tip**: API ν‚€ 없이도 10κ°€μ§€ μ‹œκ°ν™” 뢄석과 κΈ°λ³Έ 톡계λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€!
1001
-
1002
- ⚑ **NEW!** 챗봇이 이제 μ‹€μ‹œκ°„ 슀트리밍으둜 μ‘λ‹΅ν•©λ‹ˆλ‹€!
1003
- πŸ” **NEW!** Brave Search μ›Ήκ²€μƒ‰μœΌλ‘œ μ΅œμ‹  μƒκΆŒ νŠΈλ Œλ“œλ₯Ό μžλ™ λ°˜μ˜ν•©λ‹ˆλ‹€!
1004
- """)
1005
 
1006
  # μ‹€ν–‰
1007
  if __name__ == "__main__":
 
1
  """
2
+ AI 기반 μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ - Comic Classic Theme 버전
3
  Dataset: https://huggingface.co/datasets/ginipick/market
4
  """
5
  import gradio as gr
 
168
  """포괄적인 μΈμ‚¬μ΄νŠΈ 생성"""
169
  insights = []
170
 
 
171
  insights.append(self._create_top_categories_chart())
 
 
172
  insights.append(self._create_major_category_pie())
 
 
173
  insights.append(self._create_floor_analysis())
 
 
174
  insights.append(self._create_diversity_index())
 
 
175
  insights.append(self._create_franchise_analysis())
 
 
176
  insights.append(self._create_floor_preference())
 
 
177
  insights.append(self._create_district_density())
 
 
178
  insights.append(self._create_category_correlation())
 
 
179
  insights.append(self._create_subcategory_trends())
 
 
180
  insights.append(self._create_regional_specialization())
181
 
182
  return insights
 
225
  floor_data = self.df['측정보_숫자'].dropna()
226
  floor_counts = floor_data.value_counts().sort_index()
227
 
 
228
  underground = floor_counts[floor_counts.index < 0].sum()
229
  first_floor = floor_counts.get(1, 0)
230
  upper_floors = floor_counts[floor_counts.index > 1].sum()
 
253
  if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
254
  return None
255
 
 
256
  diversity_data = []
257
+ for district in self.df['μ‹œκ΅°κ΅¬λͺ…'].unique()[:20]:
258
  district_df = self.df[self.df['μ‹œκ΅°κ΅¬λͺ…'] == district]
259
  num_categories = district_df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique()
260
  total_stores = len(district_df)
 
286
  if 'λΈŒλžœλ“œλͺ…' not in self.df.columns:
287
  return None
288
 
 
289
  franchise_count = self.df['λΈŒλžœλ“œλͺ…'].notna().sum()
290
  individual_count = self.df['λΈŒλžœλ“œλͺ…'].isna().sum()
291
 
 
311
  if '측정보_숫자' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
312
  return None
313
 
 
314
  top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(10).index
315
  floor_pref_data = []
316
 
 
367
  return {'type': 'plot', 'data': fig, 'title': 'μ§€μ—­ 밀집도 뢄석'}
368
 
369
  def _create_category_correlation(self) -> Dict:
370
+ """μ—…μ’… 상관관계"""
371
  if 'μ‹œκ΅°κ΅¬λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
372
  return None
373
 
 
374
  top_categories = self.df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].value_counts().head(10).index.tolist()
 
 
375
  districts = self.df['μ‹œκ΅°κ΅¬λͺ…'].unique()
376
  correlation_matrix = np.zeros((len(top_categories), len(top_categories)))
377
 
378
  for i, cat1 in enumerate(top_categories):
379
  for j, cat2 in enumerate(top_categories):
380
  if i != j:
 
381
  coexist_count = 0
382
  for district in districts:
383
  district_df = self.df[self.df['μ‹œκ΅°κ΅¬λͺ…'] == district]
 
429
  if 'μ‹œλ„λͺ…' not in self.df.columns or 'μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…' not in self.df.columns:
430
  return None
431
 
 
432
  specialization_data = []
433
  for region in self.df['μ‹œλ„λͺ…'].unique():
434
  region_df = self.df[self.df['μ‹œλ„λͺ…'] == region]
 
462
 
463
  m = folium.Map(location=[center_lat, center_lon], zoom_start=11, tiles='OpenStreetMap')
464
 
 
465
  heat_data = [[row['μœ„οΏ½οΏ½'], row['경도']] for _, row in df_sample.iterrows()]
466
  HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m)
467
 
 
493
  """Fireworks AI 기반 μžμ—°μ–΄ 처리 (슀트리밍 지원 + 웹검색)"""
494
 
495
  def __init__(self, api_key: str = None):
 
496
  self.api_key = api_key or os.getenv("FIREWORKS_API_KEY")
497
  self.base_url = "https://api.fireworks.ai/inference/v1/chat/completions"
498
 
 
502
  def process_query_stream(self, query: str, data_context: Dict, chat_history: List = None, web_search_results: str = None):
503
  """μžμ—°μ–΄ 쿼리 처리 (슀트리밍 λͺ¨λ“œ) - 웹검색 κ²°κ³Ό 포함"""
504
 
 
505
  web_context = ""
506
  if web_search_results and "⚠️" not in web_search_results:
507
  web_context = f"""
 
520
 
521
  ꡬ체적인 μˆ«μžμ™€ λΉ„μœ¨λ‘œ μ •λŸ‰μ  뢄석을 μ œκ³΅ν•˜μ„Έμš”.
522
  μ°½μ—…, 투자, 경쟁 뢄석 κ΄€μ μ—μ„œ μ‹€μš©μ  μΈμ‚¬μ΄νŠΈλ₯Ό μ œκ³΅ν•˜μ„Έμš”.
523
+ μ›Ή 검색 κ²°κ³Όκ°€ 제곡된 경우 μ΅œμ‹  νŠΈλ Œλ“œμ™€ ν•¨κ»˜ λΆ„μ„ν•˜μ„Έμš”.
524
+ λ°˜λ“œμ‹œ ν•œκ΅­μ–΄λ‘œ λ‹΅λ³€ν•˜μ„Έμš”."""
525
 
526
  messages = [{"role": "system", "content": system_prompt}]
527
  if chat_history:
 
533
  "max_tokens": 4800,
534
  "temperature": 0.7,
535
  "messages": messages,
536
+ "stream": True
537
  }
538
 
539
  headers = {
 
547
  headers=headers,
548
  json=payload,
549
  timeout=60,
550
+ stream=True
551
  )
552
 
553
  if response.status_code == 200:
 
554
  for line in response.iter_lines():
555
  if line:
556
  line_text = line.decode('utf-8')
557
  if line_text.startswith('data: '):
558
+ data_str = line_text[6:]
559
  if data_str.strip() == '[DONE]':
560
  break
561
  try:
 
608
 
609
  app_state.analyzer = MarketAnalyzer(df)
610
 
 
611
  stats = f"""
612
  βœ… **데이터 λ‘œλ“œ μ™„λ£Œ!**
613
+ {'=' * 40}
614
+ πŸ“Š **뢄석 톡계**
615
+ β€’ 총 점포: {len(df):,}개
616
+ β€’ 뢄석 μ§€μ—­: {', '.join(regions)}
617
+ β€’ μ—…μ’… 수: {df['μƒκΆŒμ—…μ’…μ€‘λΆ„λ₯˜λͺ…'].nunique()}개
618
+ β€’ λŒ€λΆ„λ₯˜: {df['μƒκΆŒμ—…μ’…λŒ€λΆ„λ₯˜λͺ…'].nunique()}개
619
+ {'=' * 40}
620
+ πŸ’‘ 이제 μΈμ‚¬μ΄νŠΈλ₯Ό ν™•μΈν•˜κ±°λ‚˜ AIμ—κ²Œ μ§ˆλ¬Έν•˜μ„Έμš”!
621
  """
622
 
623
  return stats, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True)
 
640
  else:
641
  result.append(None)
642
 
 
643
  while len(result) < 11:
644
  result.append(None)
645
 
 
647
 
648
 
649
  def chat_respond(message, history):
650
+ """챗봇 응닡 (슀트리밍 λͺ¨λ“œ + 웹검색)"""
651
  if app_state.analyzer is None:
652
  yield history + [[message, "❌ λ¨Όμ € 데이터λ₯Ό λ‘œλ“œν•΄μ£Όμ„Έμš”!"]]
653
  return
 
655
  data_context = app_state.analyzer.analyze_for_llm()
656
 
657
  try:
 
658
  if app_state.llm_processor is None:
659
  app_state.llm_processor = LLMQueryProcessor()
660
 
 
661
  if app_state.brave_client is None:
662
  try:
663
  app_state.brave_client = BraveSearchClient()
664
  except:
665
  app_state.brave_client = None
666
 
 
667
  web_results = None
668
  if app_state.brave_client and app_state.brave_client.api_key:
 
669
  search_query = f"ν•œκ΅­ μƒκΆŒ μ°½μ—… νŠΈλ Œλ“œ {message}"
670
  web_results = app_state.brave_client.search(search_query, count=3)
671
 
 
672
  chat_hist = []
673
  for user_msg, bot_msg in history:
674
  chat_hist.append({"role": "user", "content": user_msg})
675
  chat_hist.append({"role": "assistant", "content": bot_msg})
676
 
 
677
  history = history + [[message, ""]]
678
 
 
679
  if web_results and "⚠️" not in web_results:
680
  history[-1][1] = "πŸ” μ›Ή 검색 쀑...\n\n"
681
  yield history
682
 
 
683
  full_response = ""
684
  for chunk in app_state.llm_processor.process_query_stream(message, data_context, chat_hist, web_results):
685
  full_response += chunk
 
687
  yield history
688
 
689
  except ValueError as e:
 
690
  response = f"""πŸ“Š **κΈ°λ³Έ 데이터 뢄석 κ²°κ³Ό**
691
 
692
  **전체 ν˜„ν™©**
 
698
  ν™˜κ²½λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”:
699
  ```bash
700
  export FIREWORKS_API_KEY="your_api_key_here"
701
+ export BRAVE_API_KEY="your_brave_api_key_here"
702
+ ```"""
 
 
703
 
704
  history = history + [[message, response]]
705
  yield history
706
 
707
 
708
  # ============================================================================
709
+ # 🎨 Comic Classic Theme CSS
710
  # ============================================================================
711
 
712
+ css = """
713
+ /* ===== 🎨 Google Fonts Import ===== */
714
+ @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&family=Noto+Sans+KR:wght@400;700&display=swap');
715
+
716
+ /* ===== 🎨 Comic Classic λ°°κ²½ - λΉˆν‹°μ§€ 페이퍼 + λ„νŠΈ νŒ¨ν„΄ ===== */
717
+ .gradio-container {
718
+ background-color: #FEF9C3 !important;
719
+ background-image:
720
+ radial-gradient(#1F2937 1px, transparent 1px) !important;
721
+ background-size: 20px 20px !important;
722
+ min-height: 100vh !important;
723
+ font-family: 'Noto Sans KR', 'Comic Neue', cursive, sans-serif !important;
724
+ }
725
+
726
+ /* ===== ν—ˆκΉ…νŽ˜μ΄μŠ€ 상단 μš”μ†Œ μˆ¨κΉ€ ===== */
727
+ .huggingface-space-header,
728
+ #space-header,
729
+ .space-header,
730
+ [class*="space-header"],
731
+ .svelte-1ed2p3z,
732
+ .space-header-badge,
733
+ .header-badge,
734
+ [data-testid="space-header"],
735
+ .svelte-kqij2n,
736
+ .svelte-1ax1toq,
737
+ .embed-container > div:first-child {
738
+ display: none !important;
739
+ visibility: hidden !important;
740
+ height: 0 !important;
741
+ width: 0 !important;
742
+ overflow: hidden !important;
743
+ opacity: 0 !important;
744
+ pointer-events: none !important;
745
+ }
746
+
747
+ /* ===== Footer μ™„μ „ μˆ¨κΉ€ ===== */
748
+ footer,
749
+ .footer,
750
+ .gradio-container footer,
751
+ .built-with,
752
+ [class*="footer"],
753
+ .gradio-footer,
754
+ .main-footer,
755
+ div[class*="footer"],
756
+ .show-api,
757
+ .built-with-gradio,
758
+ a[href*="gradio.app"],
759
+ a[href*="huggingface.co/spaces"] {
760
+ display: none !important;
761
+ visibility: hidden !important;
762
+ height: 0 !important;
763
+ padding: 0 !important;
764
+ margin: 0 !important;
765
+ }
766
+
767
+ /* ===== 메인 μ»¨ν…Œμ΄λ„ˆ ===== */
768
+ #col-container {
769
+ max-width: 1400px;
770
+ margin: 0 auto;
771
+ }
772
+
773
+ /* ===== 🎨 헀더 타이틀 - μ½”λ―Ή μŠ€νƒ€μΌ ===== */
774
+ .header-text h1 {
775
+ font-family: 'Bangers', cursive !important;
776
+ color: #1F2937 !important;
777
+ font-size: 3.2rem !important;
778
+ font-weight: 400 !important;
779
+ text-align: center !important;
780
+ margin-bottom: 0.5rem !important;
781
+ text-shadow:
782
+ 4px 4px 0px #FACC15,
783
+ 6px 6px 0px #1F2937 !important;
784
+ letter-spacing: 3px !important;
785
+ -webkit-text-stroke: 2px #1F2937 !important;
786
+ }
787
+
788
+ /* ===== 🎨 μ„œλΈŒνƒ€μ΄ν‹€ ===== */
789
+ .subtitle {
790
+ text-align: center !important;
791
+ font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
792
+ font-size: 1.1rem !important;
793
+ color: #1F2937 !important;
794
+ margin-bottom: 1.5rem !important;
795
+ font-weight: 700 !important;
796
+ }
797
+
798
+ /* ===== 🎨 μΉ΄λ“œ/νŒ¨λ„ - λ§Œν™” ν”„λ ˆμž„ μŠ€νƒ€μΌ ===== */
799
+ .gr-panel,
800
+ .gr-box,
801
+ .gr-form,
802
+ .block,
803
+ .gr-group {
804
+ background: #FFFFFF !important;
805
+ border: 3px solid #1F2937 !important;
806
+ border-radius: 8px !important;
807
+ box-shadow: 6px 6px 0px #1F2937 !important;
808
+ transition: all 0.2s ease !important;
809
+ }
810
+
811
+ .gr-panel:hover,
812
+ .block:hover {
813
+ transform: translate(-2px, -2px) !important;
814
+ box-shadow: 8px 8px 0px #1F2937 !important;
815
+ }
816
+
817
+ /* ===== 🎨 μž…λ ₯ ν•„λ“œ (Textbox) ===== */
818
+ textarea,
819
+ input[type="text"],
820
+ input[type="number"] {
821
+ background: #FFFFFF !important;
822
+ border: 3px solid #1F2937 !important;
823
+ border-radius: 8px !important;
824
+ color: #1F2937 !important;
825
+ font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
826
+ font-size: 1rem !important;
827
+ font-weight: 700 !important;
828
+ transition: all 0.2s ease !important;
829
+ }
830
+
831
+ textarea:focus,
832
+ input[type="text"]:focus,
833
+ input[type="number"]:focus {
834
+ border-color: #3B82F6 !important;
835
+ box-shadow: 4px 4px 0px #3B82F6 !important;
836
+ outline: none !important;
837
+ }
838
+
839
+ textarea::placeholder {
840
+ color: #9CA3AF !important;
841
+ font-weight: 400 !important;
842
+ }
843
+
844
+ /* ===== 🎨 Primary λ²„νŠΌ - μ½”λ―Ή 블루 ===== */
845
+ .gr-button-primary,
846
+ button.primary,
847
+ .gr-button.primary {
848
+ background: #3B82F6 !important;
849
+ border: 3px solid #1F2937 !important;
850
+ border-radius: 8px !important;
851
+ color: #FFFFFF !important;
852
+ font-family: 'Noto Sans KR', 'Bangers', cursive !important;
853
+ font-weight: 700 !important;
854
+ font-size: 1.2rem !important;
855
+ letter-spacing: 1px !important;
856
+ padding: 14px 28px !important;
857
+ box-shadow: 5px 5px 0px #1F2937 !important;
858
+ transition: all 0.1s ease !important;
859
+ text-shadow: 1px 1px 0px #1F2937 !important;
860
+ }
861
+
862
+ .gr-button-primary:hover,
863
+ button.primary:hover,
864
+ .gr-button.primary:hover {
865
+ background: #2563EB !important;
866
+ transform: translate(-2px, -2px) !important;
867
+ box-shadow: 7px 7px 0px #1F2937 !important;
868
+ }
869
+
870
+ .gr-button-primary:active,
871
+ button.primary:active,
872
+ .gr-button.primary:active {
873
+ transform: translate(3px, 3px) !important;
874
+ box-shadow: 2px 2px 0px #1F2937 !important;
875
+ }
876
+
877
+ /* ===== 🎨 Secondary λ²„νŠΌ - μ½”λ―Ή λ ˆλ“œ ===== */
878
+ .gr-button-secondary,
879
+ button.secondary {
880
+ background: #EF4444 !important;
881
+ border: 3px solid #1F2937 !important;
882
+ border-radius: 8px !important;
883
+ color: #FFFFFF !important;
884
+ font-family: 'Noto Sans KR', 'Bangers', cursive !important;
885
+ font-weight: 700 !important;
886
+ font-size: 1rem !important;
887
+ letter-spacing: 1px !important;
888
+ box-shadow: 4px 4px 0px #1F2937 !important;
889
+ transition: all 0.1s ease !important;
890
+ text-shadow: 1px 1px 0px #1F2937 !important;
891
+ }
892
+
893
+ .gr-button-secondary:hover,
894
+ button.secondary:hover {
895
+ background: #DC2626 !important;
896
+ transform: translate(-2px, -2px) !important;
897
+ box-shadow: 6px 6px 0px #1F2937 !important;
898
+ }
899
+
900
+ /* ===== 🎨 Small λ²„νŠΌ ===== */
901
+ button.sm,
902
+ .gr-button-sm {
903
+ background: #10B981 !important;
904
+ border: 2px solid #1F2937 !important;
905
+ border-radius: 6px !important;
906
+ color: #FFFFFF !important;
907
+ font-family: 'Noto Sans KR', cursive !important;
908
+ font-weight: 700 !important;
909
+ font-size: 0.9rem !important;
910
+ padding: 8px 16px !important;
911
+ box-shadow: 3px 3px 0px #1F2937 !important;
912
+ transition: all 0.1s ease !important;
913
+ }
914
+
915
+ button.sm:hover,
916
+ .gr-button-sm:hover {
917
+ background: #059669 !important;
918
+ transform: translate(-1px, -1px) !important;
919
+ box-shadow: 4px 4px 0px #1F2937 !important;
920
+ }
921
+
922
+ /* ===== 🎨 둜그 좜λ ₯ μ˜μ—­ ===== */
923
+ .info-log textarea {
924
+ background: #1F2937 !important;
925
+ color: #10B981 !important;
926
+ font-family: 'Courier New', monospace !important;
927
+ font-size: 0.9rem !important;
928
+ font-weight: 400 !important;
929
+ border: 3px solid #10B981 !important;
930
+ border-radius: 8px !important;
931
+ box-shadow: 4px 4px 0px #10B981 !important;
932
+ }
933
+
934
+ /* ===== 🎨 μ•„μ½”λ””μ–Έ - 말풍선 μŠ€νƒ€μΌ ===== */
935
+ .gr-accordion {
936
+ background: #FACC15 !important;
937
+ border: 3px solid #1F2937 !important;
938
+ border-radius: 8px !important;
939
+ box-shadow: 4px 4px 0px #1F2937 !important;
940
+ }
941
+
942
+ .gr-accordion-header {
943
+ color: #1F2937 !important;
944
+ font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
945
+ font-weight: 700 !important;
946
+ font-size: 1.1rem !important;
947
+ }
948
+
949
+ /* ===== 🎨 μ²΄ν¬λ°•μŠ€ κ·Έλ£Ή ===== */
950
+ .gr-checkbox-group {
951
+ background: #FFFFFF !important;
952
+ border: 3px solid #1F2937 !important;
953
+ border-radius: 8px !important;
954
+ padding: 10px !important;
955
+ }
956
+
957
+ input[type="checkbox"] {
958
+ accent-color: #3B82F6 !important;
959
+ width: 18px !important;
960
+ height: 18px !important;
961
+ }
962
+
963
+ /* ===== 🎨 νƒ­ μŠ€νƒ€μΌ ===== */
964
+ .gr-tab-nav {
965
+ background: #FACC15 !important;
966
+ border: 3px solid #1F2937 !important;
967
+ border-radius: 8px 8px 0 0 !important;
968
+ box-shadow: 4px 4px 0px #1F2937 !important;
969
+ }
970
+
971
+ .gr-tab-nav button {
972
+ font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
973
+ font-weight: 700 !important;
974
+ color: #1F2937 !important;
975
+ border: none !important;
976
+ padding: 12px 20px !important;
977
+ }
978
+
979
+ .gr-tab-nav button.selected {
980
+ background: #3B82F6 !important;
981
+ color: #FFFFFF !important;
982
+ border-radius: 6px 6px 0 0 !important;
983
+ }
984
+
985
+ /* ===== 🎨 챗봇 μŠ€νƒ€μΌ ===== */
986
+ .gr-chatbot {
987
+ background: #FFFFFF !important;
988
+ border: 3px solid #1F2937 !important;
989
+ border-radius: 8px !important;
990
+ box-shadow: 6px 6px 0px #1F2937 !important;
991
+ }
992
+
993
+ .gr-chatbot .message {
994
+ font-family: 'Noto Sans KR', sans-serif !important;
995
+ }
996
+
997
+ /* ===== 🎨 라벨 μŠ€νƒ€μΌ ===== */
998
+ label,
999
+ .gr-input-label,
1000
+ .gr-block-label {
1001
+ color: #1F2937 !important;
1002
+ font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
1003
+ font-weight: 700 !important;
1004
+ font-size: 1rem !important;
1005
+ }
1006
+
1007
+ /* ===== 🎨 Markdown μŠ€νƒ€μΌ ===== */
1008
+ .gr-markdown {
1009
+ font-family: 'Noto Sans KR', 'Comic Neue', cursive !important;
1010
+ color: #1F2937 !important;
1011
+ }
1012
+
1013
+ .gr-markdown h1,
1014
+ .gr-markdown h2,
1015
+ .gr-markdown h3 {
1016
+ font-family: 'Bangers', 'Noto Sans KR', cursive !important;
1017
+ color: #1F2937 !important;
1018
+ text-shadow: 2px 2px 0px #FACC15 !important;
1019
+ }
1020
+
1021
+ /* ===== 🎨 Plot μ˜μ—­ ===== */
1022
+ .gr-plot {
1023
+ border: 3px solid #1F2937 !important;
1024
+ border-radius: 8px !important;
1025
+ box-shadow: 4px 4px 0px #1F2937 !important;
1026
+ background: #FFFFFF !important;
1027
+ }
1028
+
1029
+ /* ===== 🎨 HTML μ˜μ—­ (지도) ===== */
1030
+ .gr-html {
1031
+ border: 4px solid #1F2937 !important;
1032
+ border-radius: 8px !important;
1033
+ box-shadow: 6px 6px 0px #FACC15 !important;
1034
+ overflow: hidden !important;
1035
+ }
1036
+
1037
+ /* ===== 🎨 μŠ€ν¬λ‘€λ°” - μ½”λ―Ή μŠ€νƒ€μΌ ===== */
1038
+ ::-webkit-scrollbar {
1039
+ width: 12px;
1040
+ height: 12px;
1041
+ }
1042
+
1043
+ ::-webkit-scrollbar-track {
1044
+ background: #FEF9C3;
1045
+ border: 2px solid #1F2937;
1046
+ }
1047
+
1048
+ ::-webkit-scrollbar-thumb {
1049
+ background: #3B82F6;
1050
+ border: 2px solid #1F2937;
1051
+ border-radius: 0px;
1052
+ }
1053
+
1054
+ ::-webkit-scrollbar-thumb:hover {
1055
+ background: #EF4444;
1056
+ }
1057
+
1058
+ /* ===== 🎨 선택 ν•˜μ΄λΌμ΄νŠΈ ===== */
1059
+ ::selection {
1060
+ background: #FACC15;
1061
+ color: #1F2937;
1062
+ }
1063
+
1064
+ /* ===== 🎨 링크 μŠ€νƒ€μΌ ===== */
1065
+ a {
1066
+ color: #3B82F6 !important;
1067
+ text-decoration: none !important;
1068
+ font-weight: 700 !important;
1069
+ }
1070
+
1071
+ a:hover {
1072
+ color: #EF4444 !important;
1073
+ }
1074
+
1075
+ /* ===== 🎨 Row/Column 간격 ===== */
1076
+ .gr-row {
1077
+ gap: 1.5rem !important;
1078
+ }
1079
+
1080
+ .gr-column {
1081
+ gap: 1rem !important;
1082
+ }
1083
+
1084
+ /* ===== 🎨 Badge μŠ€νƒ€μΌ ===== */
1085
+ .badge-container {
1086
+ display: flex;
1087
+ justify-content: center;
1088
+ gap: 15px;
1089
+ flex-wrap: wrap;
1090
+ margin: 20px 0;
1091
+ }
1092
+
1093
+ .comic-badge {
1094
+ display: inline-flex;
1095
+ align-items: center;
1096
+ gap: 8px;
1097
+ padding: 12px 24px;
1098
+ border: 3px solid #1F2937;
1099
+ border-radius: 8px;
1100
+ text-decoration: none;
1101
+ font-weight: 700;
1102
+ font-size: 1em;
1103
+ transition: all 0.2s ease;
1104
+ box-shadow: 4px 4px 0px #1F2937;
1105
+ font-family: 'Noto Sans KR', sans-serif;
1106
+ }
1107
+
1108
+ .comic-badge:hover {
1109
+ transform: translate(-2px, -2px);
1110
+ box-shadow: 6px 6px 0px #1F2937;
1111
+ }
1112
+
1113
+ .comic-badge-yellow {
1114
+ background: #FACC15;
1115
+ color: #1F2937;
1116
+ }
1117
+
1118
+ .comic-badge-blue {
1119
+ background: #3B82F6;
1120
+ color: #FFFFFF;
1121
+ }
1122
+
1123
+ .comic-badge-green {
1124
+ background: #10B981;
1125
+ color: #FFFFFF;
1126
+ }
1127
+
1128
+ /* ===== λ°˜μ‘ν˜• μ‘°μ • ===== */
1129
+ @media (max-width: 768px) {
1130
+ .header-text h1 {
1131
+ font-size: 2rem !important;
1132
+ text-shadow:
1133
+ 3px 3px 0px #FACC15,
1134
+ 4px 4px 0px #1F2937 !important;
1135
+ }
1136
 
1137
+ .gr-button-primary,
1138
+ button.primary {
1139
+ padding: 12px 20px !important;
1140
+ font-size: 1rem !important;
1141
+ }
1142
+
1143
+ .gr-panel,
1144
+ .block {
1145
+ box-shadow: 4px 4px 0px #1F2937 !important;
1146
+ }
1147
+ }
1148
+
1149
+ /* ===== 🎨 닀크λͺ¨λ“œ λΉ„ν™œμ„±ν™” ===== */
1150
+ @media (prefers-color-scheme: dark) {
1151
+ .gradio-container {
1152
+ background-color: #FEF9C3 !important;
1153
+ }
1154
+ }
1155
+ """
1156
 
1157
+
1158
+ # ============================================================================
1159
+ # Gradio UI
1160
+ # ============================================================================
1161
+
1162
+ with gr.Blocks(title="AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ", css=css) as demo:
1163
+
1164
+ # HOME Badge
1165
  gr.HTML("""
1166
+ <div style="text-align: center; margin: 20px 0 10px 0;">
1167
+ <a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
1168
+ <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
1169
+ </a>
1170
+ </div>
1171
+ """)
1172
+
1173
+ # Header Title
1174
+ gr.Markdown(
1175
+ """
1176
+ # πŸͺ AI μƒκΆŒ 뢄석 μ‹œμŠ€ν…œ PRO πŸ“Š
1177
+ """,
1178
+ elem_classes="header-text"
1179
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1180
 
1181
+ gr.Markdown(
1182
+ """
1183
+ <p class="subtitle">⚑ μ „κ΅­ 상가(μƒκΆŒ) 데이터 μ‹€μ‹œκ°„ 뢄석 | 슀트리밍 + 웹검색 πŸ” | 10κ°€μ§€ 심측 μΈμ‚¬μ΄νŠΈ πŸš€</p>
1184
+ """,
1185
+ )
1186
+
1187
+ # λ°°μ§€
1188
+ gr.HTML("""
1189
+ <div class="badge-container">
1190
+ <a href="https://open.kakao.com/o/peIe8KWh" target="_blank" class="comic-badge comic-badge-yellow">
1191
+ <span>πŸ’¬</span>
1192
  <span>μ˜€ν”ˆμ±„νŒ… λ°”λ‘œκ°€κΈ°</span>
1193
  </a>
1194
+ <a href="https://ginigen.ai" target="_blank" class="comic-badge comic-badge-blue">
1195
+ <span>🍌</span>
1196
  <span>λ‚˜λ…Έ λ°”λ‚˜λ‚˜ μ• λ“œμ˜¨ 무료 μ„œλΉ„μŠ€</span>
1197
  </a>
1198
  </div>
1199
  """)
 
1200
 
1201
+ # API μƒνƒœ
1202
+ api_status = "βœ… 섀정됨" if os.getenv("FIREWORKS_API_KEY") else "⚠️ λ―Έμ„€μ •"
1203
+ brave_status = "βœ… ν™œμ„±ν™”" if os.getenv("BRAVE_API_KEY") else "⚠️ λΉ„ν™œμ„±ν™”"
1204
+
1205
+ with gr.Row(equal_height=False):
1206
+ # μ™Όμͺ½ 컬럼 - μ„€μ •
1207
+ with gr.Column(scale=1, min_width=300):
1208
+ gr.Markdown("### βš™οΈ 뢄석 μ„€μ •")
1209
 
1210
+ gr.Markdown(f"""
1211
+ **πŸ”‘ API μƒνƒœ**
1212
+ - Fireworks AI: {api_status}
1213
+ - Brave Search: {brave_status}
1214
+ """)
1215
 
1216
  region_select = gr.CheckboxGroup(
1217
  choices=list(MarketDataLoader.REGIONS.keys()),
 
1219
  label="πŸ“ 뢄석 μ§€μ—­ 선택 (μ΅œλŒ€ 5개 ꢌμž₯)"
1220
  )
1221
 
1222
+ load_btn = gr.Button(
1223
+ "πŸ“Š 데이터 λ‘œλ“œν•˜κΈ°!",
1224
+ variant="primary",
1225
+ size="lg"
1226
+ )
1227
 
1228
+ with gr.Accordion("πŸ“œ λ‘œλ“œ μƒνƒœ", open=True):
1229
+ status_box = gr.Markdown(
1230
+ "πŸ‘ˆ 지역을 μ„ νƒν•˜κ³  데이터λ₯Ό λ‘œλ“œν•˜μ„Έμš”!",
1231
+ elem_classes="info-log"
1232
+ )
1233
+
1234
+ # 였λ₯Έμͺ½ 컬럼 - 메인 μ½˜ν…μΈ 
1235
+ with gr.Column(scale=3, min_width=600):
1236
  with gr.Tabs() as tabs:
1237
+ # νƒ­ 1: μΈμ‚¬μ΄νŠΈ λŒ€μ‹œλ³΄λ“œ
1238
  with gr.Tab("πŸ“Š μΈμ‚¬μ΄νŠΈ λŒ€μ‹œλ³΄λ“œ", id=0) as tab1:
1239
  insights_content = gr.Column(visible=False)
1240
 
 
1246
  gr.Markdown("### πŸ“ˆ 10κ°€μ§€ 심측 μƒκΆŒ μΈμ‚¬μ΄νŠΈ")
1247
 
1248
  with gr.Row():
1249
+ chart1 = gr.Plot(label="πŸ† 업쒅별 점포 수")
1250
+ chart2 = gr.Plot(label="πŸ“Š λŒ€λΆ„λ₯˜ 뢄포")
1251
 
1252
  with gr.Row():
1253
+ chart3 = gr.Plot(label="🏒 측별 뢄포")
1254
+ chart4 = gr.Plot(label="🎨 μ—…μ’… λ‹€μ–‘μ„±")
1255
 
1256
  with gr.Row():
1257
+ chart5 = gr.Plot(label="πŸͺ ν”„λžœμ°¨μ΄μ¦ˆ 뢄석")
1258
+ chart6 = gr.Plot(label="πŸ“ μΈ΅ μ„ ν˜Έλ„")
1259
 
1260
  with gr.Row():
1261
+ chart7 = gr.Plot(label="πŸ”₯ μ§€μ—­ 밀집도")
1262
+ chart8 = gr.Plot(label="πŸ”— μ—…μ’… 상관관계")
1263
 
1264
  with gr.Row():
1265
+ chart9 = gr.Plot(label="πŸ” μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ")
1266
+ chart10 = gr.Plot(label="🎯 μ§€μ—­ νŠΉν™”")
1267
 
1268
+ # νƒ­ 2: AI 챗봇
1269
+ with gr.Tab("πŸ€– AI 뢄석 챗봇 βš‘πŸ”", id=1) as tab2:
1270
  chat_content = gr.Column(visible=False)
1271
 
1272
  with chat_content:
1273
  gr.Markdown("""
1274
+ ### πŸ’‘ μ˜ˆμ‹œ 질문
1275
+ κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…? | μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­? | 1측이 μœ λ¦¬ν•œ μ—…μ’…? | ν”„λžœμ°¨μ΄μ¦ˆ 점유율?
1276
+
1277
+ ⚑ **슀트리밍**: AI 응닡이 μ‹€μ‹œκ°„μœΌλ‘œ ν‘œμ‹œλ©λ‹ˆλ‹€!
1278
+ πŸ” **웹검색**: μ΅œμ‹  μƒκΆŒ νŠΈλ Œλ“œλ₯Ό μžλ™ λ°˜μ˜ν•©λ‹ˆλ‹€!
1279
  """)
1280
 
1281
+ chatbot = gr.Chatbot(
1282
+ height=450,
1283
+ label="AI μƒκΆŒ 뢄석 μ–΄μ‹œμŠ€ν„΄νŠΈ"
1284
+ )
1285
 
1286
  with gr.Row():
1287
  msg_input = gr.Textbox(
 
1289
  show_label=False,
1290
  scale=4
1291
  )
1292
+ submit_btn = gr.Button("πŸš€ 전솑", variant="primary", scale=1)
1293
 
 
1294
  with gr.Row():
1295
+ sample_btn1 = gr.Button("β˜• 강남 카페 μ°½μ—…?", size="sm")
1296
+ sample_btn2 = gr.Button("πŸ— μΉ˜ν‚¨μ§‘ 포화 μ§€μ—­?", size="sm")
1297
+ sample_btn3 = gr.Button("🏒 1μΈ΅ μœ λ¦¬ν•œ μ—…μ’…?", size="sm")
1298
+ sample_btn4 = gr.Button("πŸͺ ν”„λžœμ°¨μ΄μ¦ˆ 점유율?", size="sm")
1299
 
1300
+ # μ‚¬μš© κ°€μ΄λ“œ
1301
+ gr.Markdown("""
1302
+ ---
1303
+ ### πŸ“– μ‚¬μš© κ°€μ΄λ“œ
1304
+ 1️⃣ μ§€μ—­ 선택 β†’ 2️⃣ 데이터 λ‘œλ“œ β†’ 3️⃣ 10κ°€μ§€ μΈμ‚¬μ΄νŠΈ 확인 λ˜λŠ” AIμ—κ²Œ 질문!
1305
+
1306
+ ### πŸ“Š μ œκ³΅λ˜λŠ” 10κ°€μ§€ 뢄석
1307
+ | 뢄석 ν•­λͺ© | μ„€λͺ… |
1308
+ |----------|------|
1309
+ | πŸ† 업쒅별 점포 수 | κ°€μž₯ λ§Žμ€ μ—…μ’… TOP 15 |
1310
+ | πŸ“Š λŒ€λΆ„λ₯˜ 뢄포 | μ†Œλ§€/μŒμ‹/μ„œλΉ„μŠ€ λ“± λΉ„μœ¨ |
1311
+ | 🏒 측별 뢄포 | μ§€ν•˜/1μΈ΅/상측 μž…μ§€ 뢄석 |
1312
+ | 🎨 μ—…μ’… λ‹€μ–‘μ„± | 지역별 μ—…μ’… λ‹€μ–‘μ„± μ§€μˆ˜ |
1313
+ | πŸͺ ν”„λžœμ°¨μ΄μ¦ˆ 뢄석 | 개인 vs ν”„λžœμ°¨μ΄μ¦ˆ λΉ„μœ¨ |
1314
+ | πŸ“ μΈ΅ μ„ ν˜Έλ„ | 업쒅별 μ„ ν˜Έ 측수 |
1315
+ | πŸ”₯ μ§€μ—­ 밀집도 | 점포 수 μƒμœ„ μ§€μ—­ |
1316
+ | πŸ”— μ—…μ’… 상관관계 | 같이 λ‚˜νƒ€λ‚˜λŠ” μ—…μ’… νŒ¨ν„΄ |
1317
+ | πŸ” μ†ŒλΆ„λ₯˜ νŠΈλ Œλ“œ | μ„ΈλΆ€ μ—…μ’… 뢄포 |
1318
+ | 🎯 μ§€μ—­ νŠΉν™” | 각 μ§€μ—­μ˜ νŠΉν™” μ—…μ’… |
1319
+
1320
+ πŸ’‘ **Tip**: API ν‚€ 없이도 10κ°€μ§€ μ‹œκ°ν™” 뢄석과 κΈ°λ³Έ ���계λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€!
1321
+ """)
1322
+
1323
  # 이벀트 ν•Έλ“€λŸ¬
1324
  load_btn.click(
1325
  fn=load_data,
 
1330
  outputs=[map_output, chart1, chart2, chart3, chart4, chart5, chart6, chart7, chart8, chart9, chart10]
1331
  )
1332
 
1333
+ # 챗봇 이벀트
1334
  submit_btn.click(
1335
  fn=chat_respond,
1336
  inputs=[msg_input, chatbot],
 
1356
  yield result
1357
  return handler
1358
 
1359
+ sample_btn1.click(fn=create_sample_click("κ°•λ‚¨μ—μ„œ 카페 μ°½μ—…ν•˜λ €λ©΄ μ–΄λ–»κ²Œ ν•΄μ•Ό ν•˜λ‚˜μš”?"), inputs=[chatbot], outputs=[chatbot])
1360
+ sample_btn2.click(fn=create_sample_click("μΉ˜ν‚¨μ§‘μ΄ κ°€μž₯ ν¬ν™”λœ 지역은 μ–΄λ””μΈκ°€μš”?"), inputs=[chatbot], outputs=[chatbot])
1361
+ sample_btn3.click(fn=create_sample_click("1측이 μœ λ¦¬ν•œ 업쒅은 λ¬΄μ—‡μΈκ°€μš”?"), inputs=[chatbot], outputs=[chatbot])
1362
+ sample_btn4.click(fn=create_sample_click("ν”„λžœμ°¨μ΄μ¦ˆ 점유율이 높은 업쒅은?"), inputs=[chatbot], outputs=[chatbot])
1363
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1364
 
1365
  # μ‹€ν–‰
1366
  if __name__ == "__main__":