ginipick commited on
Commit
16f04d2
·
verified ·
1 Parent(s): 7d354b5

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1019 -0
app.py ADDED
@@ -0,0 +1,1019 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import logging
5
+ import requests
6
+ import markdown
7
+ import time
8
+ import io
9
+ import random
10
+ import hashlib
11
+ from datetime import datetime
12
+ from dataclasses import dataclass
13
+ from itertools import combinations, product
14
+ from typing import Iterator
15
+
16
+ import streamlit as st
17
+ import pandas as pd
18
+ import PyPDF2 # For handling PDF files
19
+ from collections import Counter
20
+
21
+ from openai import OpenAI # OpenAI 라이브러리
22
+ from gradio_client import Client
23
+ from kaggle.api.kaggle_api_extended import KaggleApi
24
+ import tempfile
25
+ import glob
26
+ import shutil
27
+
28
+ # ─── 추가된 라이브러리(절대 누락 금지) ───────────────────────────────
29
+ import pyarrow.parquet as pq
30
+ from sklearn.feature_extraction.text import TfidfVectorizer
31
+ from sklearn.metrics.pairwise import cosine_similarity
32
+
33
+ # ─────────────────────────────── Environment Variables / Constants ─────────────────────────
34
+
35
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
36
+ BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Brave Search API
37
+ KAGGLE_USERNAME = os.getenv("KAGGLE_USERNAME", "")
38
+ KAGGLE_KEY = os.getenv("KAGGLE_KEY", "")
39
+ KAGGLE_API_KEY = KAGGLE_KEY
40
+
41
+ if not (KAGGLE_USERNAME and KAGGLE_KEY):
42
+ raise RuntimeError("⚠️ KAGGLE_USERNAME과 KAGGLE_KEY 환경변수를 먼저 설정하세요.")
43
+
44
+ os.environ["KAGGLE_USERNAME"] = KAGGLE_USERNAME
45
+ os.environ["KAGGLE_KEY"] = KAGGLE_KEY
46
+
47
+ BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
48
+ IMAGE_API_URL = "http://211.233.58.201:7896" # 예시 이미지 생성용 API
49
+ MAX_TOKENS = 7999
50
+
51
+ # ─────────────────────────────── Logging ───────────────────────────────
52
+ logging.basicConfig(
53
+ level=logging.INFO,
54
+ format="%(asctime)s - %(levelname)s - %(message)s"
55
+ )
56
+
57
+ # ─────────────────────────────── 군사(밀리터리) 전술 데이터셋 로드 ─────────────────
58
+ @st.cache_resource
59
+ def load_military_dataset():
60
+ """
61
+ mil.parquet (index, scenario_description, attack_reasoning, defense_reasoning)
62
+ """
63
+ path = os.path.join(os.path.dirname(__file__), "mil.parquet")
64
+ if not os.path.exists(path):
65
+ logging.warning("mil.parquet not found – military support disabled.")
66
+ return None
67
+ try:
68
+ df = pq.read_table(path).to_pandas()
69
+ return df
70
+ except Exception as e:
71
+ logging.error(f"Failed to read mil.parquet: {e}")
72
+ return None
73
+
74
+ MIL_DF = load_military_dataset()
75
+
76
+ def is_military_query(text: str) -> bool:
77
+ """군사/전술 관련 키워드가 등장하면 True 반환"""
78
+ kw = [
79
+ "군사", "전술", "전투", "전쟁", "작전", "무기", "병력",
80
+ "military", "tactic", "warfare", "battle", "operation"
81
+ ]
82
+ return any(k.lower() in text.lower() for k in kw)
83
+
84
+ def military_search(query: str, top_k: int = 3):
85
+ """
86
+ mil.parquet의 scenario_description 열과 코사인 유사도 분석하여
87
+ query와 가장 유사한 상위 시나리오를 반환
88
+ """
89
+ if MIL_DF is None:
90
+ return []
91
+ try:
92
+ corpus = MIL_DF["scenario_description"].tolist()
93
+ vec = TfidfVectorizer().fit_transform([query] + corpus)
94
+ sims = cosine_similarity(vec[0:1], vec[1:]).flatten()
95
+ top_idx = sims.argsort()[-top_k:][::-1]
96
+ return MIL_DF.iloc[top_idx][[
97
+ "scenario_description",
98
+ "attack_reasoning",
99
+ "defense_reasoning"
100
+ ]].to_dict("records")
101
+ except Exception as e:
102
+ logging.error(f"military_search error: {e}")
103
+ return []
104
+
105
+ # ─────────────────────────────── Kaggle Datasets ────────────────────────
106
+ KAGGLE_DATASETS = {
107
+ "general_business": {
108
+ "ref": "mohammadgharaei77/largest-2000-global-companies",
109
+ "title": "Largest 2000 Global Companies",
110
+ "subtitle": "Comprehensive data about the world's largest companies",
111
+ "url": "https://www.kaggle.com/datasets/mohammadgharaei77/largest-2000-global-companies",
112
+ "keywords": ["business", "company", "corporation", "enterprise", "global", "비즈니스", "기업", "회사", "글로벌", "기업가치"]
113
+ },
114
+ "global_development": {
115
+ "ref": "michaelmatta0/global-development-indicators-2000-2020",
116
+ "title": "Global Development Indicators (2000-2020)",
117
+ "subtitle": "Economic and social indicators for countries worldwide",
118
+ "url": "https://www.kaggle.com/datasets/michaelmatta0/global-development-indicators-2000-2020",
119
+ "keywords": ["development", "economy", "global", "indicators", "social", "경제", "발전", "지표", "사회", "국가", "글로벌"]
120
+ },
121
+ "startup_ideas": {
122
+ "ref": "rohitsahoo/100-startup-ideas",
123
+ "title": "Startup Idea Generator Dataset",
124
+ "subtitle": "A variety of startup ideas",
125
+ "url": "https://www.kaggle.com/datasets/rohitsahoo/100-startup-ideas",
126
+ "keywords": ["startup", "innovation", "business idea", "entrepreneurship", "스타트업", "창업", "혁신", "아이디어", "기업가"]
127
+ },
128
+ "legal_terms": {
129
+ "ref": "gu05087/korean-legal-terms",
130
+ "title": "Korean Legal Terms",
131
+ "subtitle": "Database of Korean legal terminology",
132
+ "url": "https://www.kaggle.com/datasets/gu05087/korean-legal-terms",
133
+ "keywords": ["legal", "law", "terms", "korean", "legislation", "법률", "법적", "한국", "용어", "규제"]
134
+ },
135
+ "billionaires": {
136
+ "ref": "vincentcampanaro/forbes-worlds-billionaires-list-2024",
137
+ "title": "Forbes World's Billionaires List 2024",
138
+ "subtitle": "Comprehensive data on the world's wealthiest individuals",
139
+ "url": "https://www.kaggle.com/datasets/vincentcampanaro/forbes-worlds-billionaires-list-2024",
140
+ "keywords": ["billionaire", "wealth", "rich", "forbes", "finance", "부자", "억만장자", "포브스", "부", "재테크"]
141
+ },
142
+ "financial_news": {
143
+ "ref": "thedevastator/uncovering-financial-insights-with-the-reuters-2",
144
+ "title": "Reuters Financial News Insights",
145
+ "subtitle": "Financial news and market analysis from Reuters",
146
+ "url": "https://www.kaggle.com/datasets/thedevastator/uncovering-financial-insights-with-the-reuters-2",
147
+ "keywords": ["finance", "market", "stock", "investment", "news", "금융", "시장", "주식", "투자", "뉴스"]
148
+ },
149
+ "ecommerce": {
150
+ "ref": "oleksiimartusiuk/80000-products-e-commerce-data-clean",
151
+ "title": "80,000 Products E-Commerce Data",
152
+ "subtitle": "Clean dataset of e-commerce products information",
153
+ "url": "https://www.kaggle.com/datasets/oleksiimartusiuk/80000-products-e-commerce-data-clean",
154
+ "keywords": ["ecommerce", "product", "retail", "shopping", "online", "이커머스", "제품", "소매", "쇼핑", "온라인"]
155
+ },
156
+ "world_development_indicators": {
157
+ "ref": "georgejdinicola/world-bank-indicators",
158
+ "title": "World Development Indicators",
159
+ "subtitle": "Long-run socio-economic indicators for 200+ countries",
160
+ "url": "https://www.kaggle.com/datasets/georgejdinicola/world-bank-indicators",
161
+ "keywords": [
162
+ "wdi", "macro", "economy", "gdp", "population",
163
+ "개발지표", "거시경제", "세계은행", "경제지표", "인구"
164
+ ]
165
+ },
166
+ "commodity_prices": {
167
+ "ref": "debashish311601/commodity-prices",
168
+ "title": "Commodity Prices (2000-2023)",
169
+ "subtitle": "Daily prices for crude oil, gold, grains, metals, etc.",
170
+ "url": "https://www.kaggle.com/datasets/debashish311601/commodity-prices",
171
+ "keywords": [
172
+ "commodity", "oil", "gold", "raw material", "price",
173
+ "원자재", "유가", "금", "가격", "시장"
174
+ ]
175
+ },
176
+ "world_trade": {
177
+ "ref": "muhammadtalhaawan/world-export-and-import-dataset",
178
+ "title": "World Export & Import Dataset",
179
+ "subtitle": "34-year historical trade flows by country & product",
180
+ "url": "https://www.kaggle.com/datasets/muhammadtalhaawan/world-export-and-import-dataset",
181
+ "keywords": [
182
+ "trade", "export", "import", "commerce", "flow",
183
+ "무역", "수출", "수입", "국제교역", "관세"
184
+ ]
185
+ },
186
+ "us_business_reports": {
187
+ "ref": "census/business-and-industry-reports",
188
+ "title": "US Business & Industry Reports",
189
+ "subtitle": "Key monthly economic indicators from the US Census Bureau",
190
+ "url": "https://www.kaggle.com/datasets/census/business-and-industry-reports",
191
+ "keywords": [
192
+ "us", "economy", "retail sales", "construction", "manufacturing",
193
+ "미국", "경제지표", "소매판매", "산업생산", "건설"
194
+ ]
195
+ },
196
+ "us_industrial_production": {
197
+ "ref": "federalreserve/industrial-production-index",
198
+ "title": "Industrial Production Index (US)",
199
+ "subtitle": "Monthly Fed index for manufacturing, mining & utilities",
200
+ "url": "https://www.kaggle.com/datasets/federalreserve/industrial-production-index",
201
+ "keywords": [
202
+ "industry", "production", "index", "fed", "us",
203
+ "산업생산", "제조업", "미국", "경기", "지수"
204
+ ]
205
+ },
206
+ "us_stock_market": {
207
+ "ref": "borismarjanovic/price-volume-data-for-all-us-stocks-etfs",
208
+ "title": "Huge Stock Market Dataset",
209
+ "subtitle": "Historical prices & volumes for all US stocks and ETFs",
210
+ "url": "https://www.kaggle.com/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs",
211
+ "keywords": [
212
+ "stock", "market", "finance", "equity", "price",
213
+ "주식", "미국증시", "시세", "ETF", "데이터"
214
+ ]
215
+ },
216
+ "company_financials": {
217
+ "ref": "rish59/financial-statements-of-major-companies2009-2023",
218
+ "title": "Financial Statements of Major Companies (2009-2023)",
219
+ "subtitle": "15-year income sheet & balance sheet data for global firms",
220
+ "url": "https://www.kaggle.com/datasets/rish59/financial-statements-of-major-companies2009-2023",
221
+ "keywords": [
222
+ "financials", "income", "balance sheet", "cashflow",
223
+ "재무제표", "매출", "수익성", "기업재무", "포트폴리오"
224
+ ]
225
+ },
226
+ "startup_investments": {
227
+ "ref": "justinas/startup-investments",
228
+ "title": "Crunchbase Startup Investments",
229
+ "subtitle": "Funding rounds & investor info for global startups",
230
+ "url": "https://www.kaggle.com/datasets/justinas/startup-investments",
231
+ "keywords": [
232
+ "startup", "venture", "funding", "crunchbase",
233
+ "투자", "VC", "스타트업", "라운드", "신규진입"
234
+ ]
235
+ },
236
+ "global_energy": {
237
+ "ref": "atharvasoundankar/global-energy-consumption-2000-2024",
238
+ "title": "Global Energy Consumption (2000-2024)",
239
+ "subtitle": "Country-level energy usage by source & sector",
240
+ "url": "https://www.kaggle.com/datasets/atharvasoundankar/global-energy-consumption-2000-2024",
241
+ "keywords": [
242
+ "energy", "consumption", "renewable", "oil", "utility",
243
+ "에너지", "소비", "재생에너지", "전력수요", "화석연료"
244
+ ]
245
+ },
246
+ "co2_emissions": {
247
+ "ref": "ulrikthygepedersen/co2-emissions-by-country",
248
+ "title": "CO₂ Emissions by Country",
249
+ "subtitle": "Annual CO₂ emissions & per-capita data since 1960s",
250
+ "url": "https://www.kaggle.com/datasets/ulrikthygepedersen/co2-emissions-by-country",
251
+ "keywords": [
252
+ "co2", "emission", "climate", "environment", "carbon",
253
+ "탄소배출", "기후변화", "환경", "온실가스", "지속가능"
254
+ ]
255
+ },
256
+ "crop_climate": {
257
+ "ref": "thedevastator/the-relationship-between-crop-production-and-cli",
258
+ "title": "Crop Production & Climate Change",
259
+ "subtitle": "Yield & area stats for wheat, corn, rice, soybean vs climate",
260
+ "url": "https://www.kaggle.com/datasets/thedevastator/the-relationship-between-crop-production-and-cli",
261
+ "keywords": [
262
+ "agriculture", "crop", "climate", "yield", "food",
263
+ "농업", "작물", "기후", "수확량", "식품"
264
+ ]
265
+ },
266
+ "esg_ratings": {
267
+ "ref": "alistairking/public-company-esg-ratings-dataset",
268
+ "title": "Public Company ESG Ratings",
269
+ "subtitle": "Environment, Social & Governance scores for listed firms",
270
+ "url": "https://www.kaggle.com/datasets/alistairking/public-company-esg-ratings-dataset",
271
+ "keywords": [
272
+ "esg", "sustainability", "governance", "csr",
273
+ "환경", "사회", "지배구조", "지속가능", "평가"
274
+ ]
275
+ },
276
+ "global_health": {
277
+ "ref": "malaiarasugraj/global-health-statistics",
278
+ "title": "Global Health Statistics",
279
+ "subtitle": "Comprehensive health indicators & disease prevalence by country",
280
+ "url": "https://www.kaggle.com/datasets/malaiarasugraj/global-health-statistics",
281
+ "keywords": [
282
+ "health", "disease", "life expectancy", "WHO",
283
+ "보건", "질병", "기대수명", "의료", "공중보건"
284
+ ]
285
+ },
286
+ "housing_market": {
287
+ "ref": "atharvasoundankar/global-housing-market-analysis-2015-2024",
288
+ "title": "Global Housing Market Analysis (2015-2024)",
289
+ "subtitle": "House price index, mortgage rates, rent data by country",
290
+ "url": "https://www.kaggle.com/datasets/atharvasoundankar/global-housing-market-analysis-2015-2024",
291
+ "keywords": [
292
+ "housing", "real estate", "price index", "mortgage",
293
+ "부동산", "주택가격", "임대료", "시장", "금리"
294
+ ]
295
+ },
296
+ "pharma_sales": {
297
+ "ref": "milanzdravkovic/pharma-sales-data",
298
+ "title": "Pharma Sales Data (2014-2019)",
299
+ "subtitle": "600k sales records across 8 ATC drug categories",
300
+ "url": "https://www.kaggle.com/datasets/milanzdravkovic/pharma-sales-data",
301
+ "keywords": [
302
+ "pharma", "sales", "drug", "healthcare", "medicine",
303
+ "제약", "의약품", "매출", "헬스케어", "시장"
304
+ ]
305
+ },
306
+ "ev_sales": {
307
+ "ref": "muhammadehsan000/global-electric-vehicle-sales-data-2010-2024",
308
+ "title": "Global EV Sales Data (2010-2024)",
309
+ "subtitle": "Electric vehicle unit sales by region & model year",
310
+ "url": "https://www.kaggle.com/datasets/muhammadehsan000/global-electric-vehicle-sales-data-2010-2024",
311
+ "keywords": [
312
+ "ev", "electric vehicle", "automotive", "mobility",
313
+ "전기차", "판매량", "자동차산업", "친환경모빌리티", "시장성장"
314
+ ]
315
+ },
316
+ "hr_attrition": {
317
+ "ref": "pavansubhasht/ibm-hr-analytics-attrition-dataset",
318
+ "title": "IBM HR Analytics: Attrition & Performance",
319
+ "subtitle": "Employee demographics, satisfaction & attrition flags",
320
+ "url": "https://www.kaggle.com/datasets/pavansubhasht/ibm-hr-analytics-attrition-dataset",
321
+ "keywords": [
322
+ "hr", "attrition", "employee", "people analytics",
323
+ "인사", "이직률", "직원", "HR분석", "조직관리"
324
+ ]
325
+ },
326
+ "employee_satisfaction": {
327
+ "ref": "redpen12/employees-satisfaction-analysis",
328
+ "title": "Employee Satisfaction Survey Data",
329
+ "subtitle": "Department-level survey scores on satisfaction & engagement",
330
+ "url": "https://www.kaggle.com/datasets/redpen12/employees-satisfaction-analysis",
331
+ "keywords": [
332
+ "satisfaction", "engagement", "survey", "workplace",
333
+ "직원만족도", "조직문화", "설문", "근무환경", "HR"
334
+ ]
335
+ },
336
+ "world_bank_indicators": {
337
+ "ref": "georgejdinicola/world-bank-indicators",
338
+ "title": "World Bank Indicators by Topic (1960-Present)",
339
+ "subtitle": "Macro-economic, 사회·인구 통계 등 200+개국 장기 시계열 지표",
340
+ "url": "https://www.kaggle.com/datasets/georgejdinicola/world-bank-indicators",
341
+ "keywords": ["world bank", "development", "economy", "global", "indicator", "세계은행", "경제", "지표", "개발", "거시"]
342
+ },
343
+ "physical_chem_properties": {
344
+ "ref": "ivanyakovlevg/physical-and-chemical-properties-of-substances",
345
+ "title": "Physical & Chemical Properties of Substances",
346
+ "subtitle": "8만여 화합물의 물리·화학 특성 및 분류 정보",
347
+ "url": "https://www.kaggle.com/datasets/ivanyakovlevg/physical-and-chemical-properties-of-substances",
348
+ "keywords": ["chemistry", "materials", "property", "substance", "화학", "물성", "소재", "데이터", "R&D"]
349
+ },
350
+ "global_weather_repository": {
351
+ "ref": "nelgiriyewithana/global-weather-repository",
352
+ "title": "Global Weather Repository",
353
+ "subtitle": "전 세계 기상 관측치(기온·강수·풍속 등) 일별 업데이트",
354
+ "url": "https://www.kaggle.com/datasets/nelgiriyewithana/global-weather-repository",
355
+ "keywords": ["weather", "climate", "meteorology", "global", "forecast", "기상", "날씨", "기후", "관측", "환경"]
356
+ },
357
+ "amazon_best_seller_softwares": {
358
+ "ref": "kaverappa/amazon-best-seller-softwares",
359
+ "title": "Amazon Best Seller – Software Category",
360
+ "subtitle": "아마존 소프트웨어 베스트셀러 순위 및 리뷰 데이터",
361
+ "url": "https://www.kaggle.com/datasets/kaverappa/amazon-best-seller-softwares",
362
+ "keywords": ["amazon", "e-commerce", "software", "review", "ranking", "아마존", "이커머스", "소프트웨어", "베스트셀러", "리뷰"]
363
+ },
364
+ "world_stock_prices": {
365
+ "ref": "nelgiriyewithana/world-stock-prices-daily-updating",
366
+ "title": "World Stock Prices (Daily Updating)",
367
+ "subtitle": "30,000여 글로벌 상장사의 일간 주가·시총·섹터 정보 실시간 갱신",
368
+ "url": "https://www.kaggle.com/datasets/nelgiriyewithana/world-stock-prices-daily-updating",
369
+ "keywords": ["stock", "finance", "market", "equity", "price", "글로벌", "주가", "금융", "시장", "투자"]
370
+ }
371
+ }
372
+
373
+ SUN_TZU_STRATEGIES = [
374
+ {"계": "만천과해", "요약": "평범한 척, 몰래 진행", "조건": "상대가 지켜보고 있을 때", "행동": "루틴·평온함 과시", "목적": "경계 무력화", "예시": "규제기관 눈치 보는 신사업 파일럿"},
375
+ {"계": "위위구조", "요약": "뒤통수 치면 포위 풀린다", "조건": "우리 측이 압박받을 때", "행동": "적 본진 급습", "목적": "압박 해소", "예시": "경쟁사 핵심 고객 뺏기"},
376
+ {"계": "차도살인", "요약": "내 손 더럽히지 마", "조건": "직접 공격 부담", "행동": "제3자 활용", "목적": "책임 전가", "예시": "언론을 통한 경쟁사 비판"},
377
+ {"계": "이일대우", "요약": "우리가 쉬면 적이 지친다", "조건": "상대가 과로 중", "행동": "버티며 체력 보존", "목적": "역전 타이밍 확보", "예시": "협상 지연 후 헐값 인수"},
378
+ {"계": "진화타겁", "요약": "불날 때 주워 담기", "조건": "시장 혼란·위기", "행동": "저가 매수", "목적": "저비용 고이익", "예시": "금융위기 때 우량자산 매입"},
379
+ {"계": "성동격서", "요약": "소음은 왼쪽, 공격은 오른쪽", "조건": "정면 방어 견고", "행동": "가짜 신호 → 우회", "목적": "방어 분산", "예시": "신제품 A 홍보, 실제는 B 확장"},
380
+ {"계": "무중생유", "요약": "없는 것도 있는 척", "조건": "자원 부족", "행동": "허세��연막", "목적": "상대 혼란", "예시": "스타트업 과장 로드맵"},
381
+ {"계": "암도진창", "요약": "뒷문으로 돌아가라", "조건": "우회로 존재", "행동": "비밀 루트 침투", "목적": "허를 찌름", "예시": "관세 피해 제3국 생산"},
382
+ {"계": "격안관화", "요약": "남 싸움 구경", "조건": "두 경쟁자 충돌", "행동": "관망", "목적": "둘 다 소모", "예시": "플랫폼 전쟁 중 중립 유지"},
383
+ {"계": "소리장도", "요약": "웃으며 칼 숨기기", "조건": "친밀 분위기", "행동": "우호 제스처 후 기습", "목적": "경계 붕괴", "예시": "합작 후 핵심 기술 탈취"},
384
+ {"계": "이대도강", "요약": "덜 중요한 걸 내줘라", "조건": "뭔가 잃었을 때", "행동": "부속 희생", "목적": "핵심 보호", "예시": "제품 라인 하나 단종"},
385
+ {"계": "순수견양", "요약": "방치된 것 챙기기", "조건": "경계 허술", "행동": "자연스럽게 수집", "목적": "무혈 이득", "예시": "공공 API 데이터 긁기"},
386
+ {"계": "타초경사", "요약": "풀 쳐서 뱀 나온다", "조건": "적이 숨을 때", "행동": "일부러 소란", "목적": "위치 노출", "예시": "이사회 반대파 의중 파악"},
387
+ {"계": "차시환혼", "요약": "죽은 카드 재활용", "조건": "폐기 자원", "행동": "리브랜딩", "목적": "새 전력 확보", "예시": "실패 앱 재출시"},
388
+ {"계": "조호이산", "요약": "호랑이 산 밖으로", "조건": "강적 거점", "행동": "유인 이동", "목적": "빈집 공략", "예시": "경쟁 VC 행사 유도 후 딜 선점"},
389
+ {"계": "욕금고종", "요약": "잡으려면 놓아줘라", "조건": "인재·적 포획", "행동": "일부러 풀어줌", "목적": "저항 약화", "예시": "핵심 인재 재계약 유도"},
390
+ {"계": "포전인옥", "요약": "벽돌 던져 옥 얻기", "조건": "큰 보상 필요", "행동": "작은 미끼", "목적": "참여 유도", "예시": "무료 → 유료 전환"},
391
+ {"계": "금적금왕", "요약": "도둑 잡으려면 두목부터", "조건": "조직 복잡", "행동": "수뇌 공격", "목적": "조직 붕괴", "예시": "최대 주주 지분 매입"},
392
+ {"계": "부저이지", "요약": "가마 밑 불 끄기", "조건": "적 의존성 존재", "행동": "보급 차단", "목적": "전력 급감", "예시": "핵심 공급업체 선점"},
393
+ {"계": "혼수모어", "요약": "물 흐려 놓고 낚시", "조건": "판세 불투명", "행동": "혼탁 유지", "목적": "어부지리", "예시": "입법 지연 로비"},
394
+ {"계": "금선탈각", "요약": "허물 벗고 도망", "조건": "추적 심함", "행동": "외피만 남김", "목적": "추적 무효", "예시": "부실 자회사 떼어내기"},
395
+ {"계": "관문잡적", "요약": "문 닫고 잡아라", "조건": "퇴로 예측", "행동": "출구 봉쇄", "목적": "완전 포획", "예시": "락업 조항으로 지분 매집"},
396
+ {"계": "원교근공", "요약": "먼 데와 친해지고 가까운 데 친다", "조건": "다국 간 경쟁", "행동": "원거리 동맹", "목적": "단계적 확장", "예시": "원거리 FTA 체결 후 인근 M&A"},
397
+ {"계": "가도벌괵", "요약": "길 빌려 공격", "조건": "중간 세력 장벽", "행동": "통로 명분 → 제압", "목적": "장애 제거", "예시": "총판 빌미 시장 진입"},
398
+ {"계": "투량환주", "요약": "들보 몰래 바꿔치기", "조건": "감시 존재", "행동": "내부 교체", "목적": "인식 왜곡", "예시": "백엔드 갈아끼우기"},
399
+ {"계": "지상매괴", "요약": "뽕나무 가리켜 회초리 욕", "조건": "직접 비판 곤란", "행동": "제3자 지적", "목적": "메시지 전달", "예시": "싱크탱크 보고서 압박"},
400
+ {"계": "가치불전", "요약": "바보 연기", "조건": "상대 의심 많음", "행동": "일부러 허술", "목적": "방심 유도", "예시": "저평가 가이던스"},
401
+ {"계": "상옥추제", "요약": "사다리 걷어차기", "조건": "길 열어준 뒤", "행동": "퇴로 차단", "목적": "고립", "예시": "투자자 초청 후 정보 차단"},
402
+ {"계": "수상개화", "요약": "나무에 꽃 핀 척", "조건": "실력 부족", "행동": "외형 부풀림", "목적": "영향력 확대", "예시": "MOU ·공동 로고 홍보"},
403
+ {"계": "반객위주", "요약": "손님에서 주인으로", "조건": "부차적 위치", "행동": "주도권 장악", "목적": "역전 지휘", "예시": "플랫폼 입점사 자체 마켓"},
404
+ {"계": "미인계", "요약": "매력으로 판단 흐리기", "조건": "유혹 가능", "행동": "감정·매력 활용", "목적": "결정 왜곡", "예시": "지역 투자로 정치인 호감 얻기"},
405
+ {"계": "공성계", "요약": "텅 빈 성문 열어놓기", "조건": "병력 부족", "행동": "과감히 공개", "목적": "상대 의심", "예시": "내부자료 전면 공개"},
406
+ {"계": "반간계", "요약": "가짜 스파이 역이용", "조건": "내부 불��� 요소", "행동": "교란 정보", "목적": "분열", "예시": "경쟁사에 가짜 루머"},
407
+ {"계": "고육계", "요약": "살 내주고 뼈 취하기", "조건": "신뢰 상실", "행동": "스스로 손실", "목적": "진정성 증명", "예시": "CEO 보너스 반납"},
408
+ {"계": "연환계", "요약": "사슬로 한꺼번에", "조건": "복수 대상 다수", "행동": "연결 묶기", "목적": "효율 타격", "예시": "패키지 제재안"},
409
+ {"계": "주위상계", "요약": "도망이 상책", "조건": "승산 없음", "행동": "즉시 후퇴", "목적": "손실 최소·재기", "예시": "적자 시장 철수"}
410
+ ]
411
+
412
+ # (생략 없이 모든 카테고리 딕셔너리 유지 — 너무 길어도 변경 금지)
413
+
414
+ # ──────────────────────────────── 프레임워크 분석 함수들 ─────────────────────────
415
+ @dataclass
416
+ class Category:
417
+ """통일된 카테고리 및 항목 구조"""
418
+ name_ko: str
419
+ name_en: str
420
+ tags: list[str]
421
+ items: list[str]
422
+
423
+ # (SWOT, PORTER, BCG 등 기존 딕셔너리 그대로 유지)
424
+ SWOT_FRAMEWORK = { ... } # 생략 없이 원본 그대로
425
+ PORTER_FRAMEWORK = { ... }
426
+ BCG_FRAMEWORK = { ... }
427
+ BUSINESS_FRAMEWORKS = {
428
+ "sunzi": "손자병법 36계",
429
+ "swot": "SWOT 분석",
430
+ "porter": "Porter의 5 Forces",
431
+ "bcg": "BCG 매트릭스"
432
+ }
433
+
434
+ # ──────────────────────────────── (중간 부분 생략 없이) ──────────────────────────
435
+
436
+ def get_idea_system_prompt(selected_category: str | None = None,
437
+ selected_frameworks: list | None = None) -> str:
438
+ """
439
+ 디자인/발명 목적을 위해 더욱 강화된 시스템 프롬프트.
440
+ - 사용자 요청: "가장 우수한 10가지 아이디어"를 상세 설명
441
+ - 결과 출력 시, 이미지 생성 자동화
442
+ - Kaggle + 웹 검색 출처 제시
443
+ """
444
+ cat_clause = (
445
+ f'\n**추가 지침**: 선택된 카테고리 "{selected_category}"를 특별히 우선하여 고려하세요.\n'
446
+ ) if selected_category else ""
447
+
448
+ if not selected_frameworks:
449
+ selected_frameworks = []
450
+
451
+ framework_instruction = "\n\n### (선택된 기타 분석 프레임워크)\n"
452
+ for fw in selected_frameworks:
453
+ if fw == "sunzi":
454
+ framework_instruction += "- 손자병법 36계\n"
455
+ elif fw == "swot":
456
+ framework_instruction += "- SWOT 분석\n"
457
+ elif fw == "porter":
458
+ framework_instruction += "- Porter의 5 Forces\n"
459
+ elif fw == "bcg":
460
+ framework_instruction += "- BCG 매트릭스\n"
461
+
462
+ # 핵심: "가장 우수한 10가지 아이디어를 아주 상세하게" + "각 아이디어별 이미지 프롬프트" + "출처 제시"
463
+ base_prompt = f"""
464
+ 당신은 창의적 디자인/발명 전문가 AI입니다.
465
+
466
+ 사용자가 입력한 주제를 분석하여,
467
+ **“가장 우수한 10가지 디자인/발명 아이디어”**를 도출하시오.
468
+ 각 아이디어는 다음 요구를 충족해야 합니다:
469
+ 1) **아주 상세하게** 설명하여, 독자가 머릿속에 이미지를 그릴 수 있을 정도로 구체적으로 서술
470
+ 2) **이미지 프롬프트**도 함께 제시하여, 자동 이미지 생성이 되도록 하라
471
+ - 예: `### 이미지 프롬프트\n한 줄 영문 문구`
472
+ 3) **Kaggle 데이터셋**, **웹 검색**을 활용한 통찰(또는 참조)이 있으면 반드시 결과에 언급
473
+ 4) 최종 출력의 마지막에 **“출처”** 섹션을 만들고,
474
+ - 웹 검색(Brave)에서 참조한 URL 3~5개
475
+ - Kaggle 데이터셋 이름/URL(있다면)
476
+ - 그 밖의 참고 자료
477
+
478
+ {framework_instruction}
479
+
480
+ 출력은 반드시 **한국어**로 하며, 아래 구조를 준수하십시오:
481
+
482
+ 1. **주제 요약** (사용자 질문 요약)
483
+ 2. **Top 10 아이디어**
484
+ - 아이디어 A (상세설명 + 적용 시나리오 + 장단점 + etc)
485
+ - (반복해서 총 10개)
486
+ - 각 아이디어마다 `### 이미지 프롬프트`를 명시하여 한 줄 영문 문구를 제시
487
+ 3. **부가적 통찰** (원하면, 선택된 프레임워크나 추가 아이디어)
488
+ 4. **출처** (웹검색 링크, Kaggle 데이터셋 등)
489
+
490
+ {cat_clause}
491
+
492
+ 아무리 길어도 이 요구사항을 준수하고, **오직 최종 완성된 답변**만 출력하십시오.
493
+ (내부 사고 과정은 감춥니다.)
494
+ """
495
+ return base_prompt.strip()
496
+
497
+ # ──────────────────────────────── 나머지 코드 (웹검색, kaggle, 이미지 생성 등) ──────────────────────────
498
+
499
+ @st.cache_data(ttl=3600)
500
+ def brave_search(query: str, count: int = 20):
501
+ # (원본 코드 그대로)
502
+ if not BRAVE_KEY:
503
+ raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) 환경 변수가 비어있습니다.")
504
+ ...
505
+
506
+ def mock_results(query: str) -> str:
507
+ # (원본 코드 그대로)
508
+ ...
509
+
510
+ def do_web_search(query: str) -> str:
511
+ # (원본 코드 그대로)
512
+ ...
513
+
514
+ def generate_image(prompt: str):
515
+ # (원본 코드 그대로)
516
+ ...
517
+
518
+ @st.cache_resource
519
+ def check_kaggle_availability():
520
+ # (원본 코드 그대로)
521
+ ...
522
+
523
+ def extract_kaggle_search_keywords(prompt, top=3):
524
+ # (원본 코드 그대로)
525
+ ...
526
+
527
+ def search_kaggle_datasets(query: str, top: int = 5) -> list[dict]:
528
+ # (원본 코드 그대로)
529
+ ...
530
+
531
+ @st.cache_data
532
+ def download_and_analyze_dataset(dataset_ref: str, max_rows: int = 1000):
533
+ # (원본 코드 그대로)
534
+ ...
535
+
536
+ def format_kaggle_analysis_markdown_multi(analyses: list[dict]) -> str:
537
+ # (원본 코드 그대로)
538
+ ...
539
+
540
+ def analyze_with_swot(prompt: str) -> dict:
541
+ # (원본 코드 그대로)
542
+ ...
543
+
544
+ def analyze_with_porter(prompt: str) -> dict:
545
+ # (원본 코드 그대로)
546
+ ...
547
+
548
+ def analyze_with_bcg(prompt: str) -> dict:
549
+ # (원본 코드 그대로)
550
+ ...
551
+
552
+ def format_business_framework_analysis(framework_type: str, analysis_result: dict) -> str:
553
+ # (원본 코드 그대로)
554
+ ...
555
+
556
+ def md_to_html(md_text: str, title: str = "Output") -> str:
557
+ # (원본 코드 그대로)
558
+ ...
559
+
560
+ def process_text_file(uploaded_file):
561
+ # (원본 코드 그대로)
562
+ ...
563
+
564
+ def process_csv_file(uploaded_file):
565
+ # (원본 코드 그대로)
566
+ ...
567
+
568
+ def process_pdf_file(uploaded_file):
569
+ # (원본 코드 그대로)
570
+ ...
571
+
572
+ def process_uploaded_files(uploaded_files):
573
+ # (원본 코드 그대로)
574
+ ...
575
+
576
+ def identify_decision_purpose(prompt: str) -> dict:
577
+ # (원본 코드 그대로, 이름만 "디자인/발명 목적 식별"로 쓰지만 내부 로직 동일)
578
+ ...
579
+
580
+ def keywords(text: str, top: int = 8) -> str:
581
+ # (원본 코드 그대로)
582
+ ...
583
+
584
+ def compute_relevance_scores(prompt: str, categories: list[Category]) -> dict:
585
+ # (원본 코드 그대로)
586
+ ...
587
+
588
+ def compute_score(weight: int, impact: int, confidence: float) -> float:
589
+ # (원본 코드 그대로)
590
+ ...
591
+
592
+ def generate_comparison_matrix(
593
+ categories: list[Category],
594
+ relevance_scores: dict = None,
595
+ max_depth: int = 3,
596
+ max_combinations: int = 100,
597
+ relevance_threshold: float = 0.2
598
+ ) -> list[tuple]:
599
+ # (원본 코드 그대로)
600
+ ...
601
+
602
+ def smart_weight(cat_name, item, relevance, global_cnt, T):
603
+ # (원본 코드 그대로)
604
+ ...
605
+
606
+ def generate_random_comparison_matrix(
607
+ categories: list[Category],
608
+ relevance_scores: dict | None = None,
609
+ k_cat=(8, 12),
610
+ n_item=(6, 10),
611
+ depth_range=(3, 6),
612
+ max_combos=1000,
613
+ seed: int | None = None,
614
+ T: float = 1.3,
615
+ ):
616
+ # (원본 코드 그대로)
617
+ ...
618
+
619
+ # PHYS_CATEGORIES = [...] (원본 카테고리 리스트 그대로)
620
+
621
+ PHYS_CATEGORIES: list[Category] = [
622
+ # (원본: 센서 기능, 크기/형태 변화, ... + 새 카테고리들 전부)
623
+ ...
624
+ ]
625
+
626
+ # ──────────────────────────────── 메인 Streamlit 앱 ──────────────────────
627
+
628
+ def idea_generator_app():
629
+ st.title("Ilúvatar(일루바타르) : Creative Design & Invention AI")
630
+ st.caption("이 시스템은 빅데이터를 자율적으로 수집·분석하여, 복합적인 디자인/발명 아이디어를 제안합니다.")
631
+
632
+ default_vals = {
633
+ "ai_model": "gpt-4.1-mini",
634
+ "messages": [],
635
+ "auto_save": True,
636
+ "generate_image": True,
637
+ "web_search_enabled": True,
638
+ "kaggle_enabled": True,
639
+ "selected_frameworks": [],
640
+ "GLOBAL_PICK_COUNT": {},
641
+ "_skip_dup_idx": None
642
+ }
643
+ for k, v in default_vals.items():
644
+ if k not in st.session_state:
645
+ st.session_state[k] = v
646
+
647
+ sb = st.sidebar
648
+ st.session_state.temp = sb.slider(
649
+ "Diversity temperature", 0.1, 3.0, 1.3, 0.1,
650
+ help="0.1 = 매우 보수적, 3.0 = 매우 창의/무작위"
651
+ )
652
+
653
+ sb.title("Settings")
654
+ sb.toggle("Auto Save", key="auto_save")
655
+ sb.toggle("Auto Image Generation", key="generate_image")
656
+
657
+ st.session_state.web_search_enabled = sb.toggle(
658
+ "Use Web Search", value=st.session_state.web_search_enabled
659
+ )
660
+ st.session_state.kaggle_enabled = sb.toggle(
661
+ "Use Kaggle Datasets", value=st.session_state.kaggle_enabled
662
+ )
663
+
664
+ if st.session_state.web_search_enabled:
665
+ sb.info("✅ Web search results enabled")
666
+ if st.session_state.kaggle_enabled:
667
+ if KAGGLE_KEY:
668
+ sb.info("✅ Kaggle data integration enabled")
669
+ else:
670
+ sb.error("⚠️ KAGGLE_KEY not set.")
671
+ st.session_state.kaggle_enabled = False
672
+
673
+ # 예시 주제
674
+ example_topics = {
675
+ "example1": "스마트홈에서 사용할 차세대 가전제품 발명 아이디어",
676
+ "example2": "지속가능한 소재를 활용한 패션 디자인 컨셉",
677
+ "example3": "사용자 ��터페이스(UI/UX) 혁신을 위한 웨어러블 기기 아이디어"
678
+ }
679
+ sb.subheader("Example Topics")
680
+ c1, c2, c3 = sb.columns(3)
681
+ if c1.button("가전제품 발명", key="ex1"):
682
+ process_example(example_topics["example1"])
683
+ if c2.button("친환경 패션 디자인", key="ex2"):
684
+ process_example(example_topics["example2"])
685
+ if c3.button("UI/UX 혁신", key="ex3"):
686
+ process_example(example_topics["example3"])
687
+
688
+ # 대화 히스토리 다운로드
689
+ latest_ideas = next(
690
+ (m["content"] for m in reversed(st.session_state.messages)
691
+ if m["role"] == "assistant" and m["content"].strip()),
692
+ None
693
+ )
694
+ if latest_ideas:
695
+ title_match = re.search(r"# (.*?)(\n|$)", latest_ideas)
696
+ title = (title_match.group(1) if title_match else "design_invention").strip()
697
+ sb.subheader("Download Latest Ideas")
698
+ d1, d2 = sb.columns(2)
699
+ d1.download_button("Download as Markdown", latest_ideas,
700
+ file_name=f"{title}.md", mime="text/markdown")
701
+ d2.download_button("Download as HTML", md_to_html(latest_ideas, title),
702
+ file_name=f"{title}.html", mime="text/html")
703
+
704
+ # 대화 히스토리 로드/저장
705
+ up = sb.file_uploader("Load Conversation (.json)", type=["json"], key="json_uploader")
706
+ if up:
707
+ try:
708
+ st.session_state.messages = json.load(up)
709
+ sb.success("Conversation history loaded successfully")
710
+ except Exception as e:
711
+ sb.error(f"Failed to load: {e}")
712
+
713
+ if sb.button("Download Conversation as JSON"):
714
+ sb.download_button(
715
+ "Save JSON",
716
+ data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2),
717
+ file_name="chat_history.json",
718
+ mime="application/json"
719
+ )
720
+
721
+ # 파일 업로드
722
+ st.subheader("File Upload (Optional)")
723
+ uploaded_files = st.file_uploader(
724
+ "Upload reference files (txt, csv, pdf)",
725
+ type=["txt", "csv", "pdf"],
726
+ accept_multiple_files=True,
727
+ key="file_uploader"
728
+ )
729
+ if uploaded_files:
730
+ st.success(f"{len(uploaded_files)} files uploaded.")
731
+ with st.expander("Preview Uploaded Files", expanded=False):
732
+ for idx, file in enumerate(uploaded_files):
733
+ st.write(f"**File Name:** {file.name}")
734
+ ext = file.name.split('.')[-1].lower()
735
+ try:
736
+ if ext == 'txt':
737
+ preview = file.read(1000).decode('utf-8', errors='ignore')
738
+ file.seek(0)
739
+ st.text_area("Preview", preview + ("..." if len(preview) >= 1000 else ""), height=150)
740
+ elif ext == 'csv':
741
+ df = pd.read_csv(file)
742
+ file.seek(0)
743
+ st.dataframe(df.head(5))
744
+ elif ext == 'pdf':
745
+ reader = PyPDF2.PdfReader(io.BytesIO(file.read()), strict=False)
746
+ file.seek(0)
747
+ pg_txt = reader.pages[0].extract_text() if reader.pages else "(No text)"
748
+ st.text_area("Preview", (pg_txt[:500] + "...") if pg_txt else "(No text)", height=150)
749
+ except Exception as e:
750
+ st.error(f"Preview failed: {e}")
751
+ if idx < len(uploaded_files) - 1:
752
+ st.divider()
753
+
754
+ # 이미 렌더된 메시지(중복 방지)
755
+ skip_idx = st.session_state.get("_skip_dup_idx")
756
+ for i, m in enumerate(st.session_state.messages):
757
+ if skip_idx is not None and i == skip_idx:
758
+ continue
759
+ with st.chat_message(m["role"]):
760
+ st.markdown(m["content"])
761
+ if "image" in m:
762
+ st.image(m["image"], caption=m.get("image_caption", ""))
763
+ st.session_state["_skip_dup_idx"] = None
764
+
765
+ # 메인 채팅 입력
766
+ prompt = st.chat_input("새로운 디자인/발명 아이디어가 필요하신가요? 여기에 상황이나 목표를 작성하세요!")
767
+ if prompt:
768
+ process_input(prompt, uploaded_files)
769
+
770
+ sb.markdown("---")
771
+ sb.markdown("Created by [VIDraft](https://discord.gg/openfreeai)")
772
+
773
+ def process_example(topic):
774
+ process_input(topic, [])
775
+
776
+ def process_input(prompt: str, uploaded_files):
777
+ """
778
+ 메인 채팅 입력을 받아 디자인/발명 아이디어를 생성한다.
779
+ """
780
+ if not any(m["role"] == "user" and m["content"] == prompt for m in st.session_state.messages):
781
+ st.session_state.messages.append({"role": "user", "content": prompt})
782
+ with st.chat_message("user"):
783
+ st.markdown(prompt)
784
+
785
+ # 중복 답변 방지
786
+ for i in range(len(st.session_state.messages) - 1):
787
+ if (st.session_state.messages[i]["role"] == "user"
788
+ and st.session_state.messages[i]["content"] == prompt
789
+ and st.session_state.messages[i + 1]["role"] == "assistant"):
790
+ return
791
+
792
+ with st.chat_message("assistant"):
793
+ status = st.status("Preparing to generate invention ideas…")
794
+ stream_placeholder = st.empty()
795
+ full_response = ""
796
+
797
+ try:
798
+ client = get_openai_client()
799
+ status.update(label="Initializing model…")
800
+
801
+ selected_cat = st.session_state.get("category_focus", None)
802
+ selected_frameworks = st.session_state.get("selected_frameworks", [])
803
+
804
+ # 강화된 시스템 프롬프트를 사용
805
+ sys_prompt = get_idea_system_prompt(
806
+ selected_category=selected_cat,
807
+ selected_frameworks=selected_frameworks
808
+ )
809
+
810
+ def category_context(sel):
811
+ if sel:
812
+ return json.dumps({sel: physical_transformation_categories[sel]}, ensure_ascii=False)
813
+ return "ALL_CATEGORIES: " + ", ".join(physical_transformation_categories.keys())
814
+
815
+ use_web_search = st.session_state.web_search_enabled
816
+ use_kaggle = st.session_state.kaggle_enabled
817
+ has_uploaded = bool(uploaded_files)
818
+
819
+ search_content = None
820
+ kaggle_content = None
821
+ file_content = None
822
+
823
+ # ① 웹검색
824
+ if use_web_search:
825
+ status.update(label="Searching the web…")
826
+ with st.spinner("Searching…"):
827
+ search_content = do_web_search(keywords(prompt, top=5))
828
+
829
+ # ② Kaggle
830
+ if use_kaggle and check_kaggle_availability():
831
+ status.update(label="Kaggle 데이터셋 분석 중…")
832
+ with st.spinner("Searching Kaggle…"):
833
+ kaggle_kw = extract_kaggle_search_keywords(prompt)
834
+ try:
835
+ datasets = search_kaggle_datasets(kaggle_kw)
836
+ except Exception as e:
837
+ logging.warning(f"search_kaggle_datasets 오류 무시: {e}")
838
+ datasets = []
839
+ analyses = []
840
+ if datasets:
841
+ status.update(label="Downloading & analysing datasets…")
842
+ for ds in datasets:
843
+ try:
844
+ ana = download_and_analyze_dataset(ds["ref"])
845
+ except Exception as e:
846
+ logging.error(f"Kaggle 분석 오류({ds['ref']}) : {e}")
847
+ ana = f"데이터셋 분석 오류: {e}"
848
+ analyses.append({"meta": ds, "analysis": ana})
849
+ if analyses:
850
+ kaggle_content = format_kaggle_analysis_markdown_multi(analyses)
851
+
852
+ # ③ 파일 업로드
853
+ if has_uploaded:
854
+ status.update(label="Reading uploaded files…")
855
+ with st.spinner("Processing files…"):
856
+ file_content = process_uploaded_files(uploaded_files)
857
+
858
+ # ④ 군사 전술 데이터 (필요 시)
859
+ mil_content = None
860
+ if is_military_query(prompt):
861
+ status.update(label="Searching military tactics dataset…")
862
+ with st.spinner("Loading military insights…"):
863
+ mil_rows = military_search(prompt)
864
+ if mil_rows:
865
+ mil_content = "# Military Tactics Dataset Reference\n\n"
866
+ for i, row in enumerate(mil_rows, 1):
867
+ mil_content += (
868
+ f"### Case {i}\n"
869
+ f"**Scenario:** {row['scenario_description']}\n\n"
870
+ f"**Attack Reasoning:** {row['attack_reasoning']}\n\n"
871
+ f"**Defense Reasoning:** {row['defense_reasoning']}\n\n---\n"
872
+ )
873
+
874
+ user_content = prompt
875
+ if search_content:
876
+ user_content += "\n\n" + search_content
877
+ if kaggle_content:
878
+ user_content += "\n\n" + kaggle_content
879
+ if file_content:
880
+ user_content += "\n\n" + file_content
881
+ if mil_content:
882
+ user_content += "\n\n" + mil_content
883
+
884
+ # 내부 분석
885
+ status.update(label="분석 중…")
886
+ decision_purpose = identify_decision_purpose(prompt)
887
+ relevance_scores = compute_relevance_scores(prompt, PHYS_CATEGORIES)
888
+
889
+ status.update(label="카테고리 조합 아이디어 생성 중…")
890
+ T = st.session_state.temp
891
+ k_cat_range = (4, 8) if T < 1.0 else (6, 10) if T < 2.0 else (8, 12)
892
+ n_item_range = (2, 4) if T < 1.0 else (3, 6) if T < 2.0 else (4, 8)
893
+ depth_range = (2, 3) if T < 1.0 else (2, 5) if T < 2.0 else (2, 6)
894
+ combos = generate_random_comparison_matrix(
895
+ PHYS_CATEGORIES,
896
+ relevance_scores,
897
+ k_cat=k_cat_range,
898
+ n_item=n_item_range,
899
+ depth_range=depth_range,
900
+ seed=hash(prompt) & 0xFFFFFFFF,
901
+ T=T,
902
+ )
903
+
904
+ # 예시 매트릭스 (디버그용, 최종 답변에 붙임)
905
+ combos_table = "| 조합 | 가중치 | 영향도 | 신뢰도 | 총점 |\n|------|--------|--------|--------|-----|\n"
906
+ for w, imp, conf, tot, cmb in combos:
907
+ combo_str = " + ".join(f"{c[0]}-{c[1]}" for c in cmb)
908
+ combos_table += f"| {combo_str} | {w} | {imp} | {conf:.1f} | {tot} |\n"
909
+
910
+ purpose_info = "\n\n## 디자인/발명 목표 분석\n"
911
+ if decision_purpose['purposes']:
912
+ purpose_info += "### 핵심 목적\n"
913
+ for p, s in decision_purpose['purposes']:
914
+ purpose_info += f"- **{p}** (관련성: {s})\n"
915
+ if decision_purpose['constraints']:
916
+ purpose_info += "\n### 제약 조건\n"
917
+ for c, s in decision_purpose['constraints']:
918
+ purpose_info += f"- **{c}** (관련성: {s})\n"
919
+
920
+ # (프레임워크 결과: 필요 시)
921
+ framework_contents = []
922
+ for fw in selected_frameworks:
923
+ if fw == "swot":
924
+ swot_res = analyze_with_swot(prompt)
925
+ framework_contents.append(format_business_framework_analysis("swot", swot_res))
926
+ elif fw == "porter":
927
+ porter_res = analyze_with_porter(prompt)
928
+ framework_contents.append(format_business_framework_analysis("porter", porter_res))
929
+ elif fw == "bcg":
930
+ bcg_res = analyze_with_bcg(prompt)
931
+ framework_contents.append(format_business_framework_analysis("bcg", bcg_res))
932
+ elif fw == "sunzi":
933
+ # 생략 (원한다면 손자병법 분석도 가능)
934
+ pass
935
+
936
+ if framework_contents:
937
+ user_content += "\n\n## (Optional) 기타 프레임워크 분석\n\n" + "\n\n".join(framework_contents)
938
+
939
+ user_content += f"\n\n## 카테고리 매트릭스 분석{purpose_info}\n{combos_table}"
940
+
941
+ status.update(label="Generating final design/invention ideas…")
942
+ api_messages = [
943
+ {"role": "system", "content": sys_prompt},
944
+ {"role": "system", "name": "category_db", "content": category_context(selected_cat)},
945
+ {"role": "user", "content": user_content},
946
+ ]
947
+ stream = client.chat.completions.create(
948
+ model="gpt-4.1-mini",
949
+ messages=api_messages,
950
+ temperature=1,
951
+ max_tokens=MAX_TOKENS,
952
+ top_p=1,
953
+ stream=True
954
+ )
955
+
956
+ for chunk in stream:
957
+ if chunk.choices and chunk.choices[0].delta.content:
958
+ full_response += chunk.choices[0].delta.content
959
+ stream_placeholder.markdown(full_response + "▌")
960
+
961
+ stream_placeholder.markdown(full_response)
962
+ status.update(label="Invention ideas created!", state="complete")
963
+
964
+ # 이미지 생성 (자동)
965
+ img_data = img_caption = None
966
+ if st.session_state.generate_image and full_response:
967
+ # 정규식으로 "### 이미지 프롬프트" 구문을 찾아 이미지 생성
968
+ # 여러 개가 있을 수 있으므로, 대표 1개만 생성하거나
969
+ # (여기서는 편의상 첫 번째만)
970
+ match = re.search(r"###\s*이미지\s*프롬프트\s*\n+([^\n]+)", full_response, re.I)
971
+ if not match:
972
+ match = re.search(r"Image\s+Prompt\s*[:\-]\s*([^\n]+)", full_response, re.I)
973
+ if match:
974
+ raw_prompt = re.sub(r'[\r\n"\'\\]', " ", match.group(1)).strip()
975
+ with st.spinner("Generating illustrative image…"):
976
+ img_data, img_caption = generate_image(raw_prompt)
977
+ if img_data:
978
+ st.image(img_data, caption=f"Visualized Concept – {img_caption}")
979
+
980
+ answer_msg = {"role": "assistant", "content": full_response}
981
+ if img_data:
982
+ answer_msg["image"] = img_data
983
+ answer_msg["image_caption"] = img_caption
984
+ st.session_state["_skip_dup_idx"] = len(st.session_state.messages)
985
+ st.session_state.messages.append(answer_msg)
986
+
987
+ # 다운로드 버튼
988
+ st.subheader("Download This Output")
989
+ col_md, col_html = st.columns(2)
990
+ col_md.download_button(
991
+ "Markdown",
992
+ data=full_response,
993
+ file_name=f"{prompt[:30]}.md",
994
+ mime="text/markdown"
995
+ )
996
+ col_html.download_button(
997
+ "HTML",
998
+ data=md_to_html(full_response, prompt[:30]),
999
+ file_name=f"{prompt[:30]}.html",
1000
+ mime="text/html"
1001
+ )
1002
+
1003
+ if st.session_state.auto_save:
1004
+ fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
1005
+ with open(fn, "w", encoding="utf-8") as fp:
1006
+ json.dump(st.session_state.messages, fp, ensure_ascii=False, indent=2)
1007
+
1008
+ except Exception as e:
1009
+ logging.error("process_input error", exc_info=True)
1010
+ st.error(f"⚠️ 작업 중 오류가 발생했습니다: {e}")
1011
+ st.session_state.messages.append(
1012
+ {"role": "assistant", "content": f"⚠️ 오류: {e}"}
1013
+ )
1014
+
1015
+ def main():
1016
+ idea_generator_app()
1017
+
1018
+ if __name__ == "__main__":
1019
+ main()