openfree commited on
Commit
710469d
Β·
verified Β·
1 Parent(s): 7c4c04f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +21 -1083
app.py CHANGED
@@ -1,1097 +1,35 @@
1
- """
2
- K-Listed Company Analysis Agent - Enhanced Async with Multi-Agent Collaboration
3
- Using Qwen3-235B model with Investigator/Auditor/Supervisor team
4
- """
5
-
6
  import os
7
  import sys
8
- import json
9
- import time
10
- import traceback
11
- import sqlite3
12
- import pickle
13
- import asyncio
14
- import aiohttp
15
- from typing import List, Dict, Optional, Tuple, Any
16
- from datetime import datetime, timedelta
17
- from dataclasses import dataclass, field, asdict
18
- from concurrent.futures import ThreadPoolExecutor, as_completed
19
- import re
20
- import hashlib
21
-
22
  import streamlit as st
23
- import pandas as pd
24
- import plotly.graph_objects as go
25
- import plotly.express as px
26
- import requests
27
- import yfinance as yf
28
-
29
- # Set page config at the very beginning
30
- st.set_page_config(
31
- page_title="πŸ‡°πŸ‡· K-Listed Company Analysis Agent",
32
- page_icon="πŸ‡°πŸ‡·",
33
- layout="wide",
34
- initial_sidebar_state="expanded"
35
- )
36
-
37
- # =============================================================================
38
- # Configuration
39
- # =============================================================================
40
- APP_TITLE = "πŸ‡°πŸ‡· K-Listed Company Analysis Agent (KOSPI/KOSDAQ)"
41
- APP_DESC = """
42
- ν•œκ΅­ 상μž₯사 μ „λ¬Έ AI 뢄석 μ‹œμŠ€ν…œ - Multi-Agent Collaboration
43
- - 🀝 3단계 ν˜‘μ—…: μ‘°μ‚¬μž β†’ κ°μ‚¬μž β†’ κ°λ…μž
44
- - πŸš€ Qwen3-235B λͺ¨λΈ (12,000 토큰)
45
- - ⚑ 비동기 병렬 처리
46
- - πŸ“Š 2,600+ μ „ 상μž₯사 지원
47
- """
48
-
49
- # Fireworks Configuration - QWEN MODEL
50
- FIREWORKS_MODEL = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
51
- FIREWORKS_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
52
- BRAVE_URL = "https://api.search.brave.com/res/v1/web/search"
53
-
54
- # Token allocation for multi-agent system
55
- TOKEN_ALLOCATION = {
56
- "investigator": 4096,
57
- "auditor": 2048,
58
- "supervisor": 4096,
59
- "total": 12000
60
- }
61
-
62
- ALLOWED_OFFICIAL_DOMAINS = [
63
- "dart.fss.or.kr",
64
- "kind.krx.co.kr",
65
- "krx.co.kr",
66
- "www.krx.co.kr",
67
- "finance.naver.com",
68
- "comp.fnguide.com",
69
- ]
70
-
71
- # Database
72
- DB_PATH = "korean_stocks.db"
73
- CACHE_EXPIRY_HOURS = 24
74
-
75
- # Sample Korean stocks data
76
- KOREAN_STOCKS_DATA = [
77
- # KOSPI
78
- ("005930", "μ‚Όμ„±μ „μž", "KOSPI", "μ „κΈ°μ „μž", "λ°˜λ„μ²΄"),
79
- ("000660", "SKν•˜μ΄λ‹‰μŠ€", "KOSPI", "μ „κΈ°μ „μž", "λ°˜λ„μ²΄"),
80
- ("373220", "LGμ—λ„ˆμ§€μ†”λ£¨μ…˜", "KOSPI", "μ „κΈ°μ „μž", "배터리"),
81
- ("207940", "μ‚Όμ„±λ°”μ΄μ˜€λ‘œμ§μŠ€", "KOSPI", "μ˜μ•½ν’ˆ", "λ°”μ΄μ˜€"),
82
- ("005380", "ν˜„λŒ€μ°¨", "KOSPI", "운수μž₯λΉ„", "μžλ™μ°¨"),
83
- ("000270", "κΈ°μ•„", "KOSPI", "운수μž₯λΉ„", "μžλ™μ°¨"),
84
- ("068270", "μ…€νŠΈλ¦¬μ˜¨", "KOSPI", "μ˜μ•½ν’ˆ", "λ°”μ΄μ˜€"),
85
- ("035720", "카카였", "KOSPI", "μ„œλΉ„μŠ€μ—…", "인터넷"),
86
- ("035420", "넀이버", "KOSPI", "μ„œλΉ„μŠ€μ—…", "인터넷"),
87
- ("006400", "μ‚Όμ„±SDI", "KOSPI", "μ „κΈ°μ „μž", "배터리"),
88
- ("051910", "LGν™”ν•™", "KOSPI", "ν™”ν•™", "μ„μœ ν™”ν•™"),
89
- ("005490", "ν¬μŠ€μ½”ν™€λ”©μŠ€", "KOSPI", "μ² κ°•κΈˆμ†", "μ² κ°•"),
90
- ("012330", "ν˜„λŒ€λͺ¨λΉ„μŠ€", "KOSPI", "운수μž₯λΉ„", "μžλ™μ°¨λΆ€ν’ˆ"),
91
- ("105560", "KB금육", "KOSPI", "κΈˆμœ΅μ—…", "은행"),
92
- ("055550", "μ‹ ν•œμ§€μ£Ό", "KOSPI", "κΈˆμœ΅μ—…", "은행"),
93
- # KOSDAQ
94
- ("247540", "μ—μ½”ν”„λ‘œλΉ„μ— ", "KOSDAQ", "μ „κΈ°μ „μž", "λ°°ν„°λ¦¬μ†Œμž¬"),
95
- ("086520", "μ—μ½”ν”„λ‘œ", "KOSDAQ", "μ „κΈ°μ „μž", "ν™˜κ²½"),
96
- ("328130", "루닛", "KOSDAQ", "의료", "AI의료"),
97
- ("196170", "μ•Œν…Œμ˜€μ  ", "KOSDAQ", "μ˜μ•½ν’ˆ", "λ°”μ΄μ˜€"),
98
- ("326030", "SKλ°”μ΄μ˜€νŒœ", "KOSDAQ", "μ˜μ•½ν’ˆ", "μ‹ μ•½"),
99
- ]
100
-
101
- # =============================================================================
102
- # Data Classes
103
- # =============================================================================
104
- @dataclass
105
- class SearchResult:
106
- url: str
107
- title: str
108
- description: str
109
- hostname: str
110
-
111
- @dataclass
112
- class AnalysisReport:
113
- company: str
114
- ticker: str = ""
115
- market: str = ""
116
- report_text: str = ""
117
- sources: List[SearchResult] = field(default_factory=list)
118
- timestamp: str = ""
119
- error: str = ""
120
- stock_data: Optional[pd.DataFrame] = None
121
- financial_info: Optional[Dict] = None
122
- from_cache: bool = False
123
- processing_time: float = 0.0
124
- agent_outputs: Dict[str, str] = field(default_factory=dict)
125
-
126
- # =============================================================================
127
- # Utility Functions
128
- # =============================================================================
129
- def _now_kst() -> str:
130
- """Get current KST timestamp"""
131
- kst = datetime.utcnow() + timedelta(hours=9)
132
- return kst.strftime("%Y-%m-%d %H:%M:%S KST")
133
-
134
- def ensure_env() -> List[str]:
135
- """Check required environment variables"""
136
- missing = []
137
- if not os.getenv("BRAVE_API_KEY"):
138
- missing.append("BRAVE_API_KEY")
139
- if not os.getenv("FIREWORKS_API_KEY"):
140
- missing.append("FIREWORKS_API_KEY")
141
- return missing
142
-
143
- def safe_get(dct: Dict, path: str, default=None):
144
- """Safely get nested dictionary values"""
145
- cur = dct
146
- for p in path.split("."):
147
- if not isinstance(cur, dict) or p not in cur:
148
- return default
149
- cur = cur[p]
150
- return cur
151
-
152
- # =============================================================================
153
- # Database Functions
154
- # =============================================================================
155
- @st.cache_resource
156
- def init_database():
157
- """Initialize SQLite database with Korean stocks"""
158
- conn = sqlite3.connect(DB_PATH, check_same_thread=False)
159
- cursor = conn.cursor()
160
-
161
- cursor.execute('''
162
- CREATE TABLE IF NOT EXISTS stocks (
163
- ticker TEXT PRIMARY KEY,
164
- name TEXT NOT NULL,
165
- market TEXT NOT NULL,
166
- sector TEXT,
167
- industry TEXT,
168
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
169
- )
170
- ''')
171
-
172
- cursor.execute('''
173
- CREATE TABLE IF NOT EXISTS analysis_cache (
174
- cache_key TEXT PRIMARY KEY,
175
- company TEXT NOT NULL,
176
- report_data BLOB,
177
- sources_data BLOB,
178
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
179
- expires_at TIMESTAMP
180
- )
181
- ''')
182
-
183
- # Load stocks data
184
- cursor.executemany(
185
- "INSERT OR REPLACE INTO stocks (ticker, name, market, sector, industry) VALUES (?, ?, ?, ?, ?)",
186
- KOREAN_STOCKS_DATA
187
- )
188
-
189
- conn.commit()
190
- return conn
191
-
192
- @st.cache_data(ttl=3600)
193
- def search_stock_by_name(name: str) -> Optional[Tuple[str, str, str]]:
194
- """Search stock by company name"""
195
- conn = init_database()
196
- cursor = conn.cursor()
197
-
198
- cursor.execute(
199
- "SELECT ticker, name, market FROM stocks WHERE name LIKE ? OR ticker = ?",
200
- (f"%{name}%", name)
201
- )
202
- result = cursor.fetchone()
203
- return result
204
-
205
- def save_to_cache(company: str, report: Dict, sources: List, ttl_hours: int = 24):
206
- """Save analysis to cache"""
207
- conn = init_database()
208
- cursor = conn.cursor()
209
-
210
- cache_key = hashlib.md5(f"{company}_{datetime.now().date()}".encode()).hexdigest()
211
- expires_at = datetime.now() + timedelta(hours=ttl_hours)
212
-
213
- cursor.execute('''
214
- INSERT OR REPLACE INTO analysis_cache
215
- (cache_key, company, report_data, sources_data, expires_at)
216
- VALUES (?, ?, ?, ?, ?)
217
- ''', (cache_key, company, pickle.dumps(report), pickle.dumps(sources), expires_at))
218
-
219
- conn.commit()
220
-
221
- def get_from_cache(company: str) -> Optional[Tuple[Dict, List]]:
222
- """Get cached analysis if available"""
223
- conn = init_database()
224
- cursor = conn.cursor()
225
-
226
- cursor.execute('''
227
- SELECT report_data, sources_data
228
- FROM analysis_cache
229
- WHERE company = ? AND expires_at > ?
230
- ORDER BY created_at DESC LIMIT 1
231
- ''', (company, datetime.now()))
232
-
233
- result = cursor.fetchone()
234
- if result:
235
- return pickle.loads(result[0]), pickle.loads(result[1])
236
- return None
237
-
238
- # =============================================================================
239
- # Async Fireworks Client with Qwen Model
240
- # =============================================================================
241
- class AsyncFireworksClient:
242
- """Async client for Fireworks API with Qwen3-235B model"""
243
-
244
- def __init__(self):
245
- self.api_key = os.getenv("FIREWORKS_API_KEY")
246
- self.model = FIREWORKS_MODEL
247
- self.url = FIREWORKS_URL
248
- self.session = None
249
-
250
- async def __aenter__(self):
251
- self.session = aiohttp.ClientSession()
252
- return self
253
-
254
- async def __aexit__(self, exc_type, exc_val, exc_tb):
255
- if self.session:
256
- await self.session.close()
257
-
258
- async def chat_async(self, messages: List[Dict], max_tokens: int = 4096,
259
- temperature: float = 0.6, timeout: int = 60) -> Optional[str]:
260
- """Async chat with Qwen model"""
261
- if not self.api_key:
262
- st.error("Fireworks API key not set")
263
- return None
264
-
265
- if not self.session:
266
- self.session = aiohttp.ClientSession()
267
-
268
- headers = {
269
- "Accept": "application/json",
270
- "Content-Type": "application/json",
271
- "Authorization": f"Bearer {self.api_key}"
272
- }
273
-
274
- payload = {
275
- "model": self.model,
276
- "messages": messages,
277
- "max_tokens": max_tokens,
278
- "top_p": 1,
279
- "top_k": 40,
280
- "presence_penalty": 0,
281
- "frequency_penalty": 0,
282
- "temperature": temperature
283
- }
284
-
285
- try:
286
- async with self.session.post(
287
- self.url,
288
- headers=headers,
289
- json=payload,
290
- timeout=aiohttp.ClientTimeout(total=timeout)
291
- ) as response:
292
- if response.status == 200:
293
- data = await response.json()
294
- content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
295
- return content
296
- else:
297
- error_text = await response.text()
298
- st.error(f"Fireworks API error ({response.status}): {error_text[:200]}")
299
- return None
300
-
301
- except asyncio.TimeoutError:
302
- st.error(f"Timeout after {timeout} seconds")
303
- return None
304
- except Exception as e:
305
- st.error(f"API call error: {str(e)}")
306
- return None
307
-
308
- async def multi_agent_process(self, company: str, sources: List[SearchResult],
309
- sections: List[str], tone: str) -> Dict[str, str]:
310
- """Process through Investigator -> Auditor -> Supervisor agents"""
311
- results = {}
312
-
313
- # 1. INVESTIGATOR AGENT
314
- st.info("πŸ•΅οΈ μ‘°μ‚¬μž μ—μ΄μ „νŠΈ μž‘λ™ 쀑...")
315
- investigator_result = await self.run_investigator(company, sources)
316
- results['investigator'] = investigator_result
317
-
318
- if not investigator_result:
319
- st.error("μ‘°μ‚¬μž μ—μ΄μ „νŠΈ μ‹€νŒ¨")
320
- return results
321
-
322
- # 2. AUDITOR AGENT
323
- st.info("βœ… κ°μ‚¬μž μ—μ΄μ „νŠΈ 검증 쀑...")
324
- auditor_result = await self.run_auditor(company, investigator_result)
325
- results['auditor'] = auditor_result
326
-
327
- # 3. SUPERVISOR AGENT
328
- st.info("πŸ“ κ°λ…μž μ—μ΄μ „νŠΈ μ΅œμ’… λ³΄κ³ μ„œ μž‘μ„± 쀑...")
329
- supervisor_result = await self.run_supervisor(
330
- company, investigator_result, auditor_result, sections, tone
331
- )
332
- results['supervisor'] = supervisor_result
333
-
334
- return results
335
-
336
- async def run_investigator(self, company: str, sources: List[SearchResult]) -> str:
337
- """μ‘°μ‚¬μž μ—μ΄μ „νŠΈ: 정보 μˆ˜μ§‘ 및 초기 뢄석"""
338
- source_text = "\n\n".join([
339
- f"{i+1}. {s.title}\n{s.description}\n좜처: {s.hostname}\nURL: {s.url}"
340
- for i, s in enumerate(sources[:20])
341
- ])
342
-
343
- system_prompt = """당신은 ν•œκ΅­ 상μž₯사 μ „λ¬Έ 쑰사 μ• λ„λ¦¬μŠ€νŠΈμž…λ‹ˆλ‹€.
344
- λͺ©ν‘œ: KOSPI/KOSDAQ 상μž₯μ‚¬μ˜ 사싀 기반 정보λ₯Ό μ²΄κ³„μ μœΌλ‘œ μˆ˜μ§‘ν•˜κ³  μ •λ¦¬ν•©λ‹ˆλ‹€.
345
- 원칙:
346
- 1) 곡식 좜처(DART, KRX) μ΅œμš°μ„ 
347
- 2) ꡬ체적 μˆ˜μΉ˜μ™€ λ‚ μ§œ λͺ…μ‹œ
348
- 3) μΆ”μ •κ³Ό ν™•μ • ꡬ뢄
349
- 4) 각 λ¬Έμž₯ 좜처 ν‘œκΈ° [n]"""
350
-
351
- user_prompt = f"""
352
- 뢄석 λŒ€μƒ: {company}
353
-
354
- 검색 κ²°κ³Ό:
355
- {source_text}
356
-
357
- 상세 쑰사 μš”κ΅¬μ‚¬ν•­:
358
- 1. νšŒμ‚¬ κ°œμš” (사업 μ˜μ—­, μ‹œμž₯ μ§€μœ„, 핡심 경쟁λ ₯)
359
- 2. 졜근 재무 ν•˜μ΄λΌμ΄νŠΈ (맀좜, μ˜μ—…μ΄μ΅, 순이읡 λ“± ꡬ체적 수치)
360
- 3. 졜근 60일 μ£Όμš” 이벀트 (κ³΅μ‹œ, λ‰΄μŠ€, κ²½μ˜μ§„ λ°œν‘œ)
361
- 4. 핡심 리슀크 μš”μΈ (3-5개)
362
- 5. μ„±μž₯ μΉ΄νƒˆλ¦¬μŠ€νŠΈ (3-5개)
363
- 6. 경쟁 ν™˜κ²½ 및 μ‹œμž₯ ν¬μ§€μ…˜
364
- 7. λ°Έλ₯˜μ—μ΄μ…˜ ν˜„ν™© (PER, PBR λ“±)
365
-
366
- 각 μ„Ήμ…˜λ³„λ‘œ ꡬ체적 μˆ˜μΉ˜μ™€ λ‚ μ§œλ₯Ό ν¬ν•¨ν•˜κ³ , λͺ¨λ“  μ£Όμž₯에 [n] 좜처 ν‘œκΈ°.
367
- """
368
-
369
- messages = [
370
- {"role": "system", "content": system_prompt},
371
- {"role": "user", "content": user_prompt}
372
- ]
373
-
374
- return await self.chat_async(messages, max_tokens=TOKEN_ALLOCATION['investigator'], temperature=0.4)
375
-
376
- async def run_auditor(self, company: str, investigation: str) -> str:
377
- """κ°μ‚¬μž μ—μ΄μ „νŠΈ: 사싀 검증 및 ν’ˆμ§ˆ 관리"""
378
- system_prompt = """당신은 투자 λ¦¬μ„œμΉ˜ ν’ˆμ§ˆκ΄€λ¦¬ μ±…μž„μžμž…λ‹ˆλ‹€.
379
- μ—­ν• : 쑰사 λ‚΄μš©μ˜ μ •ν™•μ„±, μ™„μ „μ„±, 객관성을 κ²€μ¦ν•©λ‹ˆλ‹€.
380
- 검증 κΈ°μ€€:
381
- 1) ν•œκ΅­ 상μž₯사 μ—¬λΆ€ 확인
382
- 2) DART/KRX 좜처 포함 μ—¬λΆ€
383
- 3) μˆ«μžμ™€ λ‚ μ§œμ˜ μ •ν™•μ„±
384
- 4) κ³Όλ„ν•œ μΆ”μ •μ΄λ‚˜ 주관적 ν‘œν˜„
385
- 5) 논리적 일관성
386
- 6) μ€‘μš” 정보 λˆ„λ½"""
387
-
388
- user_prompt = f"""
389
- νšŒμ‚¬: {company}
390
 
391
- κ²€ν†  λŒ€μƒ λ³΄κ³ μ„œ:
392
- {investigation}
393
-
394
- 점검 체크리슀트:
395
- β–‘ ν•œκ΅­ 상μž₯사(KOSPI/KOSDAQ) 확인됨
396
- β–‘ DART/KRX 곡식 좜처 μ΅œμ†Œ 2개 이상
397
- β–‘ 졜근 60일 이내 정보 포함
398
- β–‘ 재무 수치 일관성 및 λ‹¨μœ„ μ •ν™•μ„±
399
- β–‘ μΆ”μ •/ν™•μ • ꡬ뢄 λͺ…ν™•
400
- β–‘ λ¦¬μŠ€ν¬μ™€ 기회 κ· ν˜•μ  μ„œμˆ 
401
- β–‘ κ²½μŸμ‚¬ μ–ΈκΈ‰ 및 비ꡐ
402
- β–‘ 인용 인덱슀 μ™„μ „μ„±
403
-
404
- [검증 κ²°κ³Ό]
405
- - μš°μˆ˜ν•œ 점:
406
- - 문제점:
407
- - ν•„μˆ˜ 보완 사항:
408
- - μΆ”κ°€ ꢌμž₯ 사항:
409
- """
410
-
411
- messages = [
412
- {"role": "system", "content": system_prompt},
413
- {"role": "user", "content": user_prompt}
414
- ]
415
-
416
- return await self.chat_async(messages, max_tokens=TOKEN_ALLOCATION['auditor'], temperature=0.2)
417
-
418
- async def run_supervisor(self, company: str, investigation: str, audit: str,
419
- sections: List[str], tone: str) -> str:
420
- """κ°λ…μž μ—μ΄μ „νŠΈ: μ΅œμ’… λ³΄κ³ μ„œ μž‘μ„±"""
421
- system_prompt = """당신은 ν•œκ΅­ 졜고의 νˆ¬μžμ „λž΅ μ±…μž„μžμž…λ‹ˆλ‹€.
422
- μž„λ¬΄: 쑰사 λ‚΄μš©κ³Ό 감사 ν”Όλ“œλ°±μ„ μ’…ν•©ν•˜μ—¬ 전문적인 투자 뢄석 λ³΄κ³ μ„œλ₯Ό μž‘μ„±ν•©λ‹ˆλ‹€.
423
- 원칙:
424
- 1) 투자 μ˜μ‚¬κ²°μ •μ— ν•„μš”ν•œ 핡심 정보 μš°μ„ 
425
- 2) 데이터 기반 객관적 뢄석
426
- 3) λͺ…οΏ½οΏ½ν•œ 투자 포인트 μ œμ‹œ
427
- 4) 리슀크-리턴 밸런슀 κ³ λ €
428
- 5) DART/KRX 좜처 μš°μ„  인용"""
429
-
430
- section_map = {
431
- "투자 포인트": "## πŸ“Œ 핡심 투자 포인트",
432
- "νšŒμ‚¬ κ°œμš”": "## 🏒 νšŒμ‚¬ κ°œμš” 및 사업 ꡬ쑰",
433
- "재무 뢄석": "## πŸ’° 재무 μ„±κ³Ό 및 건전성",
434
- "졜근 이슈": "## πŸ“° 졜근 μ£Όμš” 이벀트 및 κ³΅μ‹œ",
435
- "μ‚°μ—… 뢄석": "## 🏭 μ‚°μ—… ν™˜κ²½ 및 경쟁 μ§€μœ„",
436
- "리슀크 μš”μΈ": "## ⚠️ 투자 리슀크 뢄석",
437
- "μ„±μž₯ 전망": "## πŸš€ μ„±μž₯ 동λ ₯ 및 μΉ΄νƒˆλ¦¬μŠ€νŠΈ",
438
- "λ°Έλ₯˜μ—μ΄μ…˜": "## πŸ“Š λ°Έλ₯˜μ—μ΄μ…˜ 및 λͺ©ν‘œμ£Όκ°€",
439
- }
440
-
441
- requested_sections = "\n".join([section_map.get(s, f"## {s}") for s in sections])
442
-
443
- user_prompt = f"""
444
- νšŒμ‚¬: {company}
445
- μž‘μ„± μŠ€νƒ€μΌ: {tone}
446
-
447
- [μ‘°μ‚¬μž λ³΄κ³ μ„œ]
448
- {investigation}
449
-
450
- [κ°μ‚¬μž ν”Όλ“œλ°±]
451
- {audit}
452
-
453
- [μ΅œμ’… λ³΄κ³ μ„œ μž‘μ„± μ§€μΉ¨]
454
- 포함 μ„Ήμ…˜:
455
- {requested_sections}
456
-
457
- ν˜•μ‹ κ·œμΉ™:
458
- 1. 각 μ„Ήμ…˜μ€ λ§ˆν¬λ‹€μš΄ 헀더(##)둜 ꡬ뢄
459
- 2. 핡심 μˆ˜μΉ˜λŠ” **ꡡ게** ν‘œμ‹œ
460
- 3. μ€‘μš” ν¬μΈνŠΈλŠ” > 인용ꡬ둜 κ°•μ‘°
461
- 4. ν‘œλ₯Ό ν™œμš©ν•œ 데이터 정리
462
- 5. 각 μ£Όμž₯에 [n] 인용 μœ μ§€
463
- 6. μ„Ήμ…˜λ³„ 핡심 μš”μ•½ λ°•μŠ€
464
- 7. 투자자 μ•‘μ…˜ μ•„μ΄ν…œ μ œμ‹œ
465
- 8. λ§ˆμ§€λ§‰μ— 좜처 λͺ©λ‘κ³Ό λ©΄μ±…μ‘°ν•­
466
-
467
- 투자 μ˜μ‚¬κ²°μ •μ— μ¦‰μ‹œ ν™œμš© κ°€λŠ₯ν•œ μˆ˜μ€€μœΌλ‘œ μž‘μ„±ν•˜μ„Έμš”.
468
- """
469
-
470
- messages = [
471
- {"role": "system", "content": system_prompt},
472
- {"role": "user", "content": user_prompt}
473
- ]
474
-
475
- return await self.chat_async(messages, max_tokens=TOKEN_ALLOCATION['supervisor'], temperature=0.5)
476
-
477
- # =============================================================================
478
- # Sync wrapper for async operations
479
- # =============================================================================
480
- class FireworksMultiAgentClient:
481
- """Synchronous wrapper for async multi-agent system"""
482
-
483
- def __init__(self):
484
- self.async_client = AsyncFireworksClient()
485
-
486
- def process_analysis(self, company: str, sources: List[SearchResult],
487
- sections: List[str], tone: str) -> Dict[str, str]:
488
- """Run multi-agent analysis synchronously"""
489
- loop = None
490
- try:
491
- loop = asyncio.get_event_loop()
492
- except RuntimeError:
493
- loop = asyncio.new_event_loop()
494
- asyncio.set_event_loop(loop)
495
 
496
- async def _process():
497
- async with self.async_client as client:
498
- return await client.multi_agent_process(company, sources, sections, tone)
499
 
500
- return loop.run_until_complete(_process())
501
-
502
- # =============================================================================
503
- # Brave Search Client (Parallel)
504
- # =============================================================================
505
- class BraveSearchClient:
506
- """Brave Search with parallel query execution"""
507
-
508
- def __init__(self):
509
- self.api_key = os.getenv("BRAVE_API_KEY")
510
- self.executor = ThreadPoolExecutor(max_workers=6)
511
-
512
- def _search_single(self, query: str, count: int = 10) -> List[SearchResult]:
513
- """Execute single search query"""
514
- if not self.api_key:
515
- return []
516
 
517
- headers = {
518
- "Accept": "application/json",
519
- "X-Subscription-Token": self.api_key
520
- }
521
- params = {
522
- "q": query,
523
- "count": count,
524
- "country": "kr",
525
- "lang": "ko-KR",
526
- "safesearch": "off"
527
- }
528
 
 
529
  try:
530
- response = requests.get(BRAVE_URL, headers=headers, params=params, timeout=15)
531
- if response.status_code == 200:
532
- data = response.json()
533
- results = []
534
- for item in data.get("web", {}).get("results", []):
535
- results.append(SearchResult(
536
- url=item.get("url", ""),
537
- title=item.get("title", ""),
538
- description=item.get("description", ""),
539
- hostname=item.get("meta_url", {}).get("hostname", "")
540
- ))
541
- return results
542
- except Exception as e:
543
- st.warning(f"Search failed for '{query}': {str(e)}")
544
- return []
545
-
546
- return []
547
-
548
- def parallel_search(self, company: str, extra_keywords: str = "") -> List[SearchResult]:
549
- """Execute multiple searches in parallel"""
550
- queries = [
551
- f'{company} site:dart.fss.or.kr μ‚¬μ—…λ³΄κ³ μ„œ',
552
- f'{company} site:kind.krx.co.kr κ³΅μ‹œ',
553
- f'{company} 졜근 싀적 λ°œν‘œ 맀좜 μ˜μ—…μ΄μ΅',
554
- f'{company} μ£Όκ°€ 전망 λͺ©ν‘œμ£Όκ°€ μ• λ„λ¦¬μŠ€νŠΈ',
555
- f'{company} κ²½μŸμ‚¬ 비ꡐ μ‹œμž₯점유율',
556
- f'{company} λ‰΄μŠ€ μ΅œμ‹  2024',
557
- f'{company} 신사업 투자 κ³„νš',
558
- f'{company} ESG 지속가λŠ₯경영',
559
- ]
560
-
561
- if extra_keywords:
562
- for keyword in extra_keywords.split(','):
563
- queries.append(f'{company} {keyword.strip()}')
564
-
565
- all_results = []
566
- seen_urls = set()
567
-
568
- # Execute searches in parallel
569
- futures = {
570
- self.executor.submit(self._search_single, query, 10): query
571
- for query in queries
572
- }
573
-
574
- progress_bar = st.progress(0)
575
- completed = 0
576
-
577
- for future in as_completed(futures):
578
- query = futures[future]
579
- results = future.result()
580
 
581
- for result in results:
582
- if result.url not in seen_urls:
583
- seen_urls.add(result.url)
584
- all_results.append(result)
585
-
586
- completed += 1
587
- progress_bar.progress(completed / len(queries))
588
- st.caption(f"검색 μ™„λ£Œ: {query[:50]}...")
589
-
590
- progress_bar.empty()
591
- return all_results[:30]
592
-
593
- # =============================================================================
594
- # Stock Data Functions
595
- # =============================================================================
596
- @st.cache_data(ttl=300)
597
- def get_stock_data(ticker: str) -> Tuple[Optional[pd.DataFrame], Optional[Dict]]:
598
- """Fetch stock data from Yahoo Finance"""
599
- try:
600
- stock = yf.Ticker(ticker)
601
- hist_data = stock.history(period="1y")
602
- info = stock.info
603
-
604
- financial_info = {
605
- "market_cap": info.get("marketCap", 0),
606
- "pe_ratio": info.get("trailingPE", 0),
607
- "pb_ratio": info.get("priceToBook", 0),
608
- "dividend_yield": info.get("dividendYield", 0),
609
- "52w_high": info.get("fiftyTwoWeekHigh", 0),
610
- "52w_low": info.get("fiftyTwoWeekLow", 0),
611
- "current_price": info.get("currentPrice", info.get("regularMarketPrice", 0)),
612
- }
613
-
614
- return hist_data, financial_info
615
- except Exception as e:
616
- st.warning(f"μ£Όκ°€ 데이터 쑰회 μ‹€νŒ¨: {e}")
617
- return None, None
618
-
619
- def create_stock_chart(stock_data: pd.DataFrame, company: str, market: str) -> go.Figure:
620
- """Create interactive stock price chart"""
621
- fig = go.Figure()
622
-
623
- # Candlestick
624
- fig.add_trace(go.Candlestick(
625
- x=stock_data.index,
626
- open=stock_data['Open'],
627
- high=stock_data['High'],
628
- low=stock_data['Low'],
629
- close=stock_data['Close'],
630
- name='μ£Όκ°€',
631
- increasing_line_color='red',
632
- decreasing_line_color='blue'
633
- ))
634
-
635
- # Moving averages
636
- if len(stock_data) > 20:
637
- ma20 = stock_data['Close'].rolling(window=20).mean()
638
- fig.add_trace(go.Scatter(
639
- x=stock_data.index,
640
- y=ma20,
641
- name='20일 이동평균',
642
- line=dict(color='orange', width=1)
643
- ))
644
-
645
- if len(stock_data) > 60:
646
- ma60 = stock_data['Close'].rolling(window=60).mean()
647
- fig.add_trace(go.Scatter(
648
- x=stock_data.index,
649
- y=ma60,
650
- name='60일 이동평균',
651
- line=dict(color='purple', width=1)
652
- ))
653
-
654
- # Volume
655
- fig.add_trace(go.Bar(
656
- x=stock_data.index,
657
- y=stock_data['Volume'],
658
- name='κ±°λž˜λŸ‰',
659
- marker_color='rgba(100,100,100,0.3)',
660
- yaxis='y2'
661
- ))
662
-
663
- fig.update_layout(
664
- title=f'{company} ({market}) μ£Όκ°€ 차트',
665
- yaxis_title='μ£Όκ°€ (원)',
666
- yaxis2=dict(
667
- title='κ±°λž˜λŸ‰',
668
- overlaying='y',
669
- side='right'
670
- ),
671
- xaxis_rangeslider_visible=False,
672
- height=500,
673
- hovermode='x unified',
674
- template='plotly_white'
675
- )
676
-
677
- return fig
678
-
679
- # =============================================================================
680
- # Main Analysis Pipeline
681
- # =============================================================================
682
- def run_analysis_pipeline(company: str, tone: str, sections: List[str],
683
- extra_keywords: str, use_cache: bool = True) -> AnalysisReport:
684
- """Run the complete multi-agent analysis pipeline"""
685
- start_time = time.time()
686
-
687
- report = AnalysisReport(
688
- company=company,
689
- timestamp=_now_kst()
690
- )
691
-
692
- # Check cache first
693
- if use_cache:
694
- cached = get_from_cache(company)
695
- if cached:
696
- report_data, sources = cached
697
- report.report_text = report_data.get('report_text', '')
698
- report.sources = sources
699
- report.from_cache = True
700
- st.success("βœ… μΊμ‹œλœ 뢄석 κ²°κ³Όλ₯Ό λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.")
701
- return report
702
-
703
- # Check environment
704
- missing = ensure_env()
705
- if missing:
706
- report.error = f"ν™˜κ²½λ³€μˆ˜ λˆ„λ½: {', '.join(missing)}"
707
- return report
708
-
709
- # Get stock info from database
710
- stock_info = search_stock_by_name(company)
711
- if stock_info:
712
- ticker, official_name, market = stock_info
713
- report.ticker = ticker
714
- report.market = market
715
- company = official_name # Use official name
716
-
717
- # Get stock data
718
- ticker_yf = f"{ticker}.KS" if market == "KOSPI" else f"{ticker}.KQ"
719
- report.stock_data, report.financial_info = get_stock_data(ticker_yf)
720
-
721
- # Progress tracking
722
- progress = st.progress(0)
723
- status = st.empty()
724
-
725
- # Step 1: Parallel search for evidence collection
726
- status.text("πŸ” 정보 μˆ˜μ§‘ 쀑 (병렬 검색)...")
727
- progress.progress(20)
728
-
729
- search_client = BraveSearchClient()
730
- sources = search_client.parallel_search(company, extra_keywords)
731
- report.sources = sources
732
-
733
- if len(sources) < 5:
734
- report.error = "μΆ©λΆ„ν•œ 정보λ₯Ό μˆ˜μ§‘ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."
735
- return report
736
-
737
- # Step 2: Multi-Agent Processing
738
- status.text("🀝 닀쀑 μ—μ΄μ „νŠΈ ν˜‘μ—… 뢄석 쀑...")
739
- progress.progress(40)
740
-
741
- agent_client = FireworksMultiAgentClient()
742
-
743
- try:
744
- # Run multi-agent analysis
745
- agent_results = agent_client.process_analysis(
746
- company=company,
747
- sources=sources,
748
- sections=sections,
749
- tone=tone
750
- )
751
-
752
- report.agent_outputs = agent_results
753
-
754
- # Get final report from supervisor
755
- final_report = agent_results.get('supervisor', '')
756
-
757
- if not final_report:
758
- # Fallback to investigator report if supervisor failed
759
- final_report = agent_results.get('investigator', '')
760
-
761
- if not final_report:
762
- report.error = "λ³΄κ³ μ„œ 생성 μ‹€νŒ¨"
763
- return report
764
-
765
- # Format final report
766
- header = f"""# πŸ“Š {company} 투자 뢄석 λ³΄κ³ μ„œ
767
-
768
- **μž‘μ„±μΌμ‹œ**: {report.timestamp}
769
- **μ‹œμž₯**: {report.market if report.market else 'N/A'}
770
- **μ’…λͺ©μ½”λ“œ**: {report.ticker if report.ticker else 'N/A'}
771
- **뢄석 μ‹œμŠ€ν…œ**: Multi-Agent Collaboration (Qwen3-235B)
772
-
773
- ---
774
-
775
- """
776
-
777
- # Add sources
778
- sources_text = "\n\n## πŸ“š μ°Έκ³  자료\n\n"
779
- for i, source in enumerate(sources[:15], 1):
780
- sources_text += f"[{i}] [{source.title}]({source.url})\n"
781
-
782
- disclaimer = """
783
-
784
- ---
785
-
786
- ### ⚠️ 투자 μœ μ˜μ‚¬ν•­
787
- λ³Έ λ³΄κ³ μ„œλŠ” AI 기반 μžλ™ λΆ„μ„μœΌλ‘œ μž‘μ„±λ˜μ—ˆμœΌλ©°, 투자 νŒλ‹¨μ˜ μ±…μž„μ€ 투자자 λ³ΈμΈμ—κ²Œ μžˆμŠ΅λ‹ˆλ‹€.
788
- """
789
-
790
- report.report_text = header + final_report + sources_text + disclaimer
791
-
792
- # Save to cache
793
- if use_cache:
794
- save_to_cache(
795
- company,
796
- {'report_text': report.report_text},
797
- sources,
798
- ttl_hours=CACHE_EXPIRY_HOURS
799
- )
800
-
801
  except Exception as e:
802
- report.error = f"뢄석 였λ₯˜: {str(e)}"
803
- st.error(traceback.format_exc())
804
- return report
805
-
806
- # Complete
807
- report.processing_time = time.time() - start_time
808
- status.text(f"βœ… 뢄석 μ™„λ£Œ! (μ²˜λ¦¬μ‹œκ°„: {report.processing_time:.1f}초)")
809
- progress.progress(100)
810
-
811
- time.sleep(1)
812
- progress.empty()
813
- status.empty()
814
-
815
- return report
816
-
817
- # =============================================================================
818
- # Streamlit UI
819
- # =============================================================================
820
- def main():
821
- # Initialize database
822
- init_database()
823
-
824
- # Title and description
825
- st.title(APP_TITLE)
826
- st.markdown(APP_DESC)
827
- st.divider()
828
-
829
- # Environment check
830
- missing_env = ensure_env()
831
- if missing_env:
832
- st.error(f"⚠️ ν•„μˆ˜ ν™˜κ²½λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€: {', '.join(missing_env)}")
833
-
834
- col1, col2 = st.columns(2)
835
- with col1:
836
- st.info(f"""
837
- **ν˜„μž¬ μƒνƒœ:**
838
- - BRAVE_API_KEY: {'βœ…' if os.getenv('BRAVE_API_KEY') else '❌'}
839
- - FIREWORKS_API_KEY: {'βœ…' if os.getenv('FIREWORKS_API_KEY') else '❌'}
840
- """)
841
-
842
- with col2:
843
- st.info(f"""
844
- **λͺ¨λΈ 정보:**
845
- {FIREWORKS_MODEL}
846
- """)
847
-
848
- with st.expander("πŸ”§ ν™˜κ²½λ³€μˆ˜ μ„€μ • κ°€μ΄λ“œ"):
849
- st.markdown("""
850
- ### Hugging Face Spaces:
851
- Settings β†’ Repository secrets
852
- """)
853
- st.code("""
854
- BRAVE_API_KEY=your_brave_api_key
855
- FIREWORKS_API_KEY=your_fireworks_api_key
856
- """)
857
-
858
- if not st.checkbox("데λͺ¨ λͺ¨λ“œλ‘œ 계속"):
859
- st.stop()
860
-
861
- # Sidebar
862
- with st.sidebar:
863
- st.header("βš™οΈ 뢄석 μ„€μ •")
864
-
865
- company = st.text_input(
866
- "νšŒμ‚¬λͺ… λ˜λŠ” μ’…λͺ©μ½”λ“œ *",
867
- placeholder="예: μ‚Όμ„±μ „μž, 005930",
868
- help="ν•œκ΅­ 상μž₯사λͺ… λ˜λŠ” μ’…λͺ©μ½”λ“œ"
869
- )
870
-
871
- # Show stock list
872
- with st.expander("πŸ“‹ μ’…λͺ© 리슀트"):
873
- conn = init_database()
874
- stocks_df = pd.read_sql("SELECT * FROM stocks ORDER BY market, name", conn)
875
- st.dataframe(stocks_df, height=300)
876
-
877
- st.divider()
878
-
879
- tone = st.radio(
880
- "μž‘μ„± μŠ€νƒ€μΌ",
881
- ["κ°„κ²°", "상세", "전문적"],
882
- index=2,
883
- help="전문적: κΈ°κ΄€νˆ¬μžμž μˆ˜μ€€"
884
- )
885
-
886
- sections = st.multiselect(
887
- "포함 μ„Ήμ…˜",
888
- ["투자 포인트", "νšŒμ‚¬ κ°œμš”", "재무 뢄석", "졜근 이슈",
889
- "μ‚°μ—… 뢄석", "리슀크 μš”μΈ", "μ„±μž₯ 전망", "λ°Έλ₯˜μ—μ΄μ…˜"],
890
- default=["투자 포인트", "νšŒμ‚¬ κ°œμš”", "재무 뢄석", "리슀크 μš”μΈ", "μ„±μž₯ 전망"]
891
- )
892
-
893
- extra_keywords = st.text_area(
894
- "μΆ”κ°€ ν‚€μ›Œλ“œ",
895
- placeholder="예: AI, μ „κΈ°μ°¨, 배터리",
896
- help="콀마둜 ꡬ뢄"
897
- )
898
-
899
- st.divider()
900
-
901
- st.subheader("πŸ”§ κ³ κΈ‰ μ˜΅μ…˜")
902
- use_cache = st.checkbox("πŸ’Ύ μΊμ‹œ μ‚¬μš©", value=True, help="24μ‹œκ°„ μΊμ‹œ")
903
- show_agent_outputs = st.checkbox("πŸ€– μ—μ΄μ „νŠΈ 좜λ ₯ 보기", value=False)
904
-
905
- # Token info
906
- st.info(f"""
907
- **Multi-Agent Token ν• λ‹Ή**
908
- - πŸ•΅οΈ μ‘°μ‚¬μž: {TOKEN_ALLOCATION['investigator']:,}
909
- - βœ… κ°μ‚¬μž: {TOKEN_ALLOCATION['auditor']:,}
910
- - πŸ“ κ°λ…μž: {TOKEN_ALLOCATION['supervisor']:,}
911
- - **총합**: {TOKEN_ALLOCATION['total']:,}
912
- """)
913
-
914
- st.divider()
915
-
916
- analyze_button = st.button(
917
- "πŸš€ 뢄석 μ‹œμž‘",
918
- type="primary",
919
- disabled=not company
920
- )
921
-
922
- # Main content
923
- if analyze_button and company:
924
- st.header(f"πŸ“Š {company} 뢄석 κ²°κ³Ό")
925
-
926
- # Create tabs
927
- tabs = st.tabs([
928
- "πŸ“„ 뢄석 λ³΄κ³ μ„œ",
929
- "πŸ“ˆ 차트",
930
- "πŸ€– μ—μ΄μ „νŠΈ 좜λ ₯",
931
- "πŸ”— 좜처",
932
- "πŸ’Ύ λ‹€μš΄λ‘œλ“œ"
933
- ])
934
-
935
- # Run analysis
936
- report = run_analysis_pipeline(
937
- company=company.strip(),
938
- tone=tone,
939
- sections=sections,
940
- extra_keywords=extra_keywords,
941
- use_cache=use_cache
942
- )
943
-
944
- if report.error:
945
- st.error(report.error)
946
- else:
947
- # Tab 1: Analysis Report
948
- with tabs[0]:
949
- if report.from_cache:
950
- st.info("πŸ“¦ μΊμ‹œλœ κ²°κ³Ό (24μ‹œκ°„ 이내)")
951
-
952
- if report.report_text:
953
- st.markdown(report.report_text)
954
-
955
- if report.processing_time:
956
- st.success(f"⏱️ μ²˜λ¦¬μ‹œκ°„: {report.processing_time:.1f}초")
957
- else:
958
- st.warning("λ³΄κ³ μ„œλ₯Ό μƒμ„±ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.")
959
-
960
- # Tab 2: Charts
961
- with tabs[1]:
962
- if report.stock_data is not None and not report.stock_data.empty:
963
- chart = create_stock_chart(report.stock_data, company, report.market)
964
- st.plotly_chart(chart, use_container_width=True)
965
-
966
- if report.financial_info:
967
- cols = st.columns(4)
968
- metrics = [
969
- ("ν˜„μž¬κ°€", "current_price", "원"),
970
- ("PER", "pe_ratio", "λ°°"),
971
- ("PBR", "pb_ratio", "λ°°"),
972
- ("λ°°λ‹Ήμˆ˜μ΅λ₯ ", "dividend_yield", "%")
973
- ]
974
-
975
- for col, (label, key, unit) in zip(cols, metrics):
976
- value = report.financial_info.get(key, 0)
977
- if value > 0:
978
- if unit == "%":
979
- col.metric(label, f"{value*100:.2f}{unit}")
980
- elif unit == "원":
981
- col.metric(label, f"{value:,.0f}{unit}")
982
- else:
983
- col.metric(label, f"{value:.2f}{unit}")
984
- else:
985
- st.info("차트 데이터가 μ—†μŠ΅λ‹ˆλ‹€.")
986
-
987
- # Tab 3: Agent Outputs
988
- with tabs[2]:
989
- if show_agent_outputs and report.agent_outputs:
990
- st.subheader("πŸ€– Multi-Agent ν˜‘μ—… κ³Όμ •")
991
-
992
- if 'investigator' in report.agent_outputs:
993
- with st.expander("πŸ•΅οΈ μ‘°μ‚¬μž μ—μ΄μ „νŠΈ 좜λ ₯"):
994
- st.markdown(report.agent_outputs['investigator'])
995
-
996
- if 'auditor' in report.agent_outputs:
997
- with st.expander("βœ… κ°μ‚¬μž μ—μ΄μ „νŠΈ ν”Όλ“œλ°±"):
998
- st.markdown(report.agent_outputs['auditor'])
999
-
1000
- if 'supervisor' in report.agent_outputs:
1001
- with st.expander("πŸ“ κ°λ…μž μ—μ΄μ „νŠΈ μ΅œμ’…"):
1002
- st.markdown(report.agent_outputs['supervisor'])
1003
- else:
1004
- st.info("μ—μ΄μ „νŠΈ 좜λ ₯을 보렀면 μ˜΅μ…˜μ„ ν™œμ„±ν™”ν•˜μ„Έμš”.")
1005
-
1006
- # Tab 4: Sources
1007
- with tabs[3]:
1008
- st.subheader(f"πŸ”— μ°Έκ³  자료 ({len(report.sources)}개)")
1009
- for i, source in enumerate(report.sources, 1):
1010
- with st.expander(f"[{i}] {source.title[:80]}..."):
1011
- st.write(f"**URL**: {source.url}")
1012
- st.write(f"**μ„€λͺ…**: {source.description}")
1013
- st.write(f"**좜처**: {source.hostname}")
1014
-
1015
- # Tab 5: Download
1016
- with tabs[4]:
1017
- st.subheader("πŸ’Ύ λ³΄κ³ μ„œ λ‹€μš΄λ‘œλ“œ")
1018
-
1019
- # Prepare download content
1020
- download_content = report.report_text
1021
-
1022
- # JSON export
1023
- export_data = {
1024
- 'company': company,
1025
- 'ticker': report.ticker,
1026
- 'market': report.market,
1027
- 'timestamp': report.timestamp,
1028
- 'processing_time': report.processing_time,
1029
- 'report': report.report_text,
1030
- 'sources': [
1031
- {'title': s.title, 'url': s.url}
1032
- for s in report.sources
1033
- ]
1034
- }
1035
-
1036
- col1, col2 = st.columns(2)
1037
-
1038
- with col1:
1039
- st.download_button(
1040
- label="πŸ“„ Markdown λ‹€μš΄λ‘œλ“œ",
1041
- data=download_content,
1042
- file_name=f"{company}_analysis_{datetime.now().strftime('%Y%m%d_%H%M')}.md",
1043
- mime="text/markdown"
1044
- )
1045
-
1046
- with col2:
1047
- st.download_button(
1048
- label="πŸ“Š JSON λ‹€μš΄λ‘œλ“œ",
1049
- data=json.dumps(export_data, ensure_ascii=False, indent=2),
1050
- file_name=f"{company}_data_{datetime.now().strftime('%Y%m%d_%H%M')}.json",
1051
- mime="application/json"
1052
- )
1053
-
1054
- # Help section
1055
- if not analyze_button:
1056
- with st.container():
1057
- st.header("πŸ“– Multi-Agent System μ†Œκ°œ")
1058
-
1059
- cols = st.columns(3)
1060
-
1061
- with cols[0]:
1062
- st.markdown("""
1063
- ### πŸ•΅οΈ μ‘°μ‚¬μž (Investigator)
1064
- - 정보 μˆ˜μ§‘ 및 초기 뢄석
1065
- - DART/KRX κ³΅μ‹œ μš°μ„ 
1066
- - 재무 데이터 μΆ”μΆœ
1067
- - 졜근 이벀트 정리
1068
- """)
1069
-
1070
- with cols[1]:
1071
- st.markdown("""
1072
- ### βœ… κ°μ‚¬μž (Auditor)
1073
- - 사싀 검증
1074
- - 좜처 확인
1075
- - 논리적 일관성 체크
1076
- - λˆ„λ½ 정보 지적
1077
- """)
1078
-
1079
- with cols[2]:
1080
- st.markdown("""
1081
- ### πŸ“ κ°λ…μž (Supervisor)
1082
- - μ΅œμ’… λ³΄κ³ μ„œ μž‘μ„±
1083
- - 투자 관점 정리
1084
- - 리슀크/기회 κ· ν˜•
1085
- - μ•‘μ…˜ μ•„μ΄ν…œ μ œμ‹œ
1086
- """)
1087
-
1088
- # Footer
1089
- st.divider()
1090
- st.caption(f"""
1091
- Last updated: {_now_kst()} |
1092
- Powered by Qwen3-235B & Multi-Agent Collaboration |
1093
- Supporting ALL Korean Listed Companies
1094
- """)
1095
 
1096
  if __name__ == "__main__":
1097
  main()
 
 
 
 
 
 
1
  import os
2
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import streamlit as st
4
+ from tempfile import NamedTemporaryFile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ def main():
7
+ try:
8
+ # Get the code from secrets
9
+ code = os.environ.get("MAIN_CODE")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ if not code:
12
+ st.error("⚠️ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
13
+ return
14
 
15
+ # Create a temporary Python file
16
+ with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
17
+ tmp.write(code)
18
+ tmp_path = tmp.name
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ # Execute the code
21
+ exec(compile(code, tmp_path, 'exec'), globals())
 
 
 
 
 
 
 
 
 
22
 
23
+ # Clean up the temporary file
24
  try:
25
+ os.unlink(tmp_path)
26
+ except:
27
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  except Exception as e:
30
+ st.error(f"⚠️ Error loading or executing the application: {str(e)}")
31
+ import traceback
32
+ st.code(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  if __name__ == "__main__":
35
  main()