eddmpython commited on
Commit
a572e7a
·
1 Parent(s): a481d8e

Docker SDK → Gradio SDK 전환

Browse files
Files changed (6) hide show
  1. .dockerignore +0 -17
  2. Dockerfile +0 -24
  3. README.md +5 -4
  4. app.py +305 -0
  5. requirements.txt +2 -0
  6. scripts/hfPreload.py +0 -31
.dockerignore DELETED
@@ -1,17 +0,0 @@
1
- .git
2
- .github
3
- .venv
4
- .venv-wsl
5
- __pycache__
6
- *.pyc
7
- .pytest_cache
8
- .ruff_cache
9
- experiments
10
- notebooks
11
- tests
12
- docs
13
- blog
14
- landing
15
- data
16
- .claude
17
- *.egg-info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile DELETED
@@ -1,24 +0,0 @@
1
- # HuggingFace Spaces — DartLab 데모
2
- # CPU free tier (16GB RAM), port 7860
3
-
4
- FROM python:3.12-slim
5
-
6
- WORKDIR /app
7
-
8
- # 시스템 의존성
9
- RUN apt-get update && apt-get install -y --no-install-recommends \
10
- build-essential \
11
- && rm -rf /var/lib/apt/lists/*
12
-
13
- # dartlab 설치
14
- RUN pip install --no-cache-dir dartlab
15
-
16
- # 샘플 데이터 프리로드 스크립트
17
- COPY scripts/hfPreload.py scripts/hfPreload.py
18
- RUN python scripts/hfPreload.py
19
-
20
- # HF Spaces 기본 포트
21
- EXPOSE 7860
22
-
23
- # 서버 실행
24
- CMD ["python", "-m", "dartlab.server"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,11 +1,12 @@
1
  ---
2
  title: DartLab
3
- emoji: 🗺️
4
  colorFrom: red
5
  colorTo: yellow
6
- sdk: docker
7
- app_port: 7860
 
8
  pinned: true
9
  license: mit
10
- short_description: "DART + EDGAR disclosure analysis — try without install"
11
  ---
 
1
  ---
2
  title: DartLab
3
+ emoji: 📊
4
  colorFrom: red
5
  colorTo: yellow
6
+ sdk: gradio
7
+ sdk_version: "5.0"
8
+ app_file: app.py
9
  pinned: true
10
  license: mit
11
+ short_description: DART + EDGAR disclosure analysis — try without install
12
  ---
app.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DartLab Gradio Demo — DART/EDGAR 공시 분석."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gc
6
+ import os
7
+ import threading
8
+ from collections import OrderedDict
9
+
10
+ import gradio as gr
11
+
12
+ import dartlab
13
+
14
+ # ── 설정 ──────────────────────────────────────────────
15
+
16
+ _MAX_CACHE = 2
17
+ _companyCache: OrderedDict = OrderedDict()
18
+ _HAS_AI = bool(os.environ.get("OPENAI_API_KEY"))
19
+
20
+ if _HAS_AI:
21
+ dartlab.llm.configure(provider="openai", api_key=os.environ["OPENAI_API_KEY"])
22
+
23
+
24
+ # ── 유틸 ──────────────────────────────────────────────
25
+
26
+
27
+ def _toPandas(df):
28
+ """Polars/pandas DataFrame → pandas 변환."""
29
+ if df is None:
30
+ return None
31
+ if hasattr(df, "to_pandas"):
32
+ return df.to_pandas()
33
+ return df
34
+
35
+
36
+ def _getCompany(code: str):
37
+ """캐시된 Company 반환, 최대 2개 유지."""
38
+ code = code.strip()
39
+ if code in _companyCache:
40
+ _companyCache.move_to_end(code)
41
+ return _companyCache[code]
42
+ while len(_companyCache) >= _MAX_CACHE:
43
+ _companyCache.popitem(last=False)
44
+ gc.collect()
45
+ c = dartlab.Company(code)
46
+ _companyCache[code] = c
47
+ return c
48
+
49
+
50
+ # ── 프리로드 ──────────────────────────────────────────
51
+
52
+
53
+ def _warmup():
54
+ """백그라운드 listing 캐시 워밍업."""
55
+ try:
56
+ dartlab.search("삼성전자")
57
+ except Exception:
58
+ pass
59
+
60
+
61
+ threading.Thread(target=_warmup, daemon=True).start()
62
+
63
+
64
+ # ── 핸들러: 검색 ─────────────────────────────────────
65
+
66
+
67
+ def handleSearch(keyword: str):
68
+ """종목 검색."""
69
+ if not keyword or not keyword.strip():
70
+ return None
71
+ df = dartlab.search(keyword.strip())
72
+ return _toPandas(df)
73
+
74
+
75
+ # ── 핸들러: 기업 개요 + 재무 ─────────────────────────
76
+
77
+
78
+ def handleLoadCompany(code: str):
79
+ """기업 개요 로드 → (기본정보 markdown, topics 리스트, 재무 DataFrame)."""
80
+ if not code or not code.strip():
81
+ return "종목코드를 입력하세요.", gr.update(choices=[]), None
82
+ try:
83
+ c = _getCompany(code)
84
+ except Exception as e:
85
+ return f"로드 실패: {e}", gr.update(choices=[]), None
86
+
87
+ # 기본정보
88
+ info = f"## {c.corpName} ({c.stockCode})\n"
89
+ info += f"- 시장: {c.market}\n"
90
+ if hasattr(c, "currency") and c.currency:
91
+ info += f"- 통화: {c.currency}\n"
92
+
93
+ # topics
94
+ topics = []
95
+ try:
96
+ topics = list(c.topics) if c.topics else []
97
+ except Exception:
98
+ pass
99
+
100
+ # IS 기본 로드
101
+ finance = None
102
+ try:
103
+ finance = _toPandas(c.IS)
104
+ except Exception:
105
+ pass
106
+
107
+ return info, gr.update(choices=topics, value=topics[0] if topics else None), finance
108
+
109
+
110
+ def handleFinance(code: str, sheet: str):
111
+ """재무제표 시트 전환."""
112
+ if not code or not code.strip():
113
+ return None
114
+ try:
115
+ c = _getCompany(code)
116
+ except Exception:
117
+ return None
118
+
119
+ lookup = {"BS": "BS", "IS": "IS", "CF": "CF", "ratios": "ratios"}
120
+ attr = lookup.get(sheet, "IS")
121
+ try:
122
+ result = getattr(c, attr, None)
123
+ return _toPandas(result)
124
+ except Exception:
125
+ return None
126
+
127
+
128
+ # ── 핸들러: Sections ──────────────────────────────────
129
+
130
+
131
+ def handleShow(code: str, topic: str):
132
+ """sections show → DataFrame 또는 Markdown."""
133
+ if not code or not topic:
134
+ return None, ""
135
+ try:
136
+ c = _getCompany(code)
137
+ result = c.show(topic)
138
+ except Exception as e:
139
+ return None, f"조회 실패: {e}"
140
+
141
+ if result is None:
142
+ return None, "데이터 없음"
143
+ if hasattr(result, "to_pandas"):
144
+ return _toPandas(result), ""
145
+ return None, str(result)
146
+
147
+
148
+ # ── 핸들러: AI Chat ───────────────────────────────────
149
+
150
+
151
+ def handleChat(message: str, history: list, code: str):
152
+ """AI 분석 대화."""
153
+ if not _HAS_AI:
154
+ history = history + [
155
+ {"role": "user", "content": message},
156
+ {"role": "assistant", "content": "OPENAI_API_KEY가 설정되지 않았습니다.\n\nHuggingFace Spaces Settings → Variables and secrets에서 설정하세요."},
157
+ ]
158
+ return history, ""
159
+
160
+ stockCode = code.strip() if code else None
161
+ if not stockCode:
162
+ history = history + [
163
+ {"role": "user", "content": message},
164
+ {"role": "assistant", "content": "먼저 Company 탭에서 종목을 로드하세요."},
165
+ ]
166
+ return history, ""
167
+
168
+ # ask() 호출 — raw=True로 Generator 받아서 스트리밍
169
+ try:
170
+ if stockCode:
171
+ query = f"{stockCode} {message}"
172
+ else:
173
+ query = message
174
+ answer = dartlab.ask(query, stream=False, raw=False)
175
+ except Exception as e:
176
+ answer = f"분석 실패: {e}"
177
+
178
+ history = history + [
179
+ {"role": "user", "content": message},
180
+ {"role": "assistant", "content": answer if answer else "응답 없음"},
181
+ ]
182
+ return history, ""
183
+
184
+
185
+ # ── UI ────────────────────────────────────────────────
186
+
187
+ _CSS = """
188
+ .main-title { text-align: center; margin-bottom: 0.5em; }
189
+ .subtitle { text-align: center; color: #666; margin-top: 0; }
190
+ """
191
+
192
+ with gr.Blocks(
193
+ title="DartLab — DART/EDGAR 공시 분석",
194
+ theme=gr.themes.Soft(),
195
+ css=_CSS,
196
+ ) as demo:
197
+ gr.Markdown("# DartLab", elem_classes="main-title")
198
+ gr.Markdown(
199
+ "DART 전자공시 + EDGAR — 종목코드 하나로 기업 분석",
200
+ elem_classes="subtitle",
201
+ )
202
+
203
+ # 공유 state
204
+ codeState = gr.State("")
205
+
206
+ with gr.Tabs():
207
+ # ── Tab 1: 검색 ──
208
+ with gr.Tab("Search"):
209
+ with gr.Row():
210
+ searchInput = gr.Textbox(
211
+ label="종목 검색",
212
+ placeholder="삼성전자, AAPL, 005930 ...",
213
+ scale=4,
214
+ )
215
+ searchBtn = gr.Button("검색", scale=1, variant="primary")
216
+ searchResult = gr.DataFrame(label="검색 결과", interactive=False)
217
+
218
+ with gr.Row():
219
+ selectedCode = gr.Textbox(
220
+ label="종목코드 입력 후 분석",
221
+ placeholder="005930",
222
+ scale=4,
223
+ )
224
+ loadBtn = gr.Button("분석하기", scale=1, variant="primary")
225
+
226
+ searchBtn.click(handleSearch, inputs=searchInput, outputs=searchResult)
227
+ searchInput.submit(handleSearch, inputs=searchInput, outputs=searchResult)
228
+
229
+ # ── Tab 2: 기업 + 재무 ──
230
+ with gr.Tab("Company"):
231
+ companyInfo = gr.Markdown("종목코드를 입력하고 '분석하기'를 클릭하세요.")
232
+
233
+ with gr.Row():
234
+ sheetSelect = gr.Dropdown(
235
+ choices=["IS", "BS", "CF", "ratios"],
236
+ value="IS",
237
+ label="재무제표",
238
+ scale=1,
239
+ )
240
+ financeTable = gr.DataFrame(label="재무제표", interactive=False)
241
+
242
+ sheetSelect.change(
243
+ handleFinance,
244
+ inputs=[codeState, sheetSelect],
245
+ outputs=financeTable,
246
+ )
247
+
248
+ # ── Tab 3: Sections ──
249
+ with gr.Tab("Sections"):
250
+ gr.Markdown("topic × period 수평화 — 기업의 전체 지도")
251
+ topicSelect = gr.Dropdown(
252
+ choices=[],
253
+ label="Topic",
254
+ interactive=True,
255
+ )
256
+ sectionTable = gr.DataFrame(label="Section 데이터", interactive=False)
257
+ sectionText = gr.Markdown("")
258
+
259
+ topicSelect.change(
260
+ handleShow,
261
+ inputs=[codeState, topicSelect],
262
+ outputs=[sectionTable, sectionText],
263
+ )
264
+
265
+ # ── Tab 4: AI Chat ──
266
+ with gr.Tab("AI Chat"):
267
+ if not _HAS_AI:
268
+ gr.Markdown(
269
+ "**AI 분석을 사용하려면** HuggingFace Spaces Settings → "
270
+ "Variables and secrets에서 `OPENAI_API_KEY`를 설정하세요."
271
+ )
272
+ chatbot = gr.Chatbot(label="AI 분석", type="messages", height=500)
273
+ with gr.Row():
274
+ chatInput = gr.Textbox(
275
+ label="질문",
276
+ placeholder="재무건전성 분석해줘, 배당 이상 징후 찾아줘 ...",
277
+ scale=5,
278
+ )
279
+ chatBtn = gr.Button("전송", scale=1, variant="primary")
280
+
281
+ chatBtn.click(
282
+ handleChat,
283
+ inputs=[chatInput, chatbot, codeState],
284
+ outputs=[chatbot, chatInput],
285
+ )
286
+ chatInput.submit(
287
+ handleChat,
288
+ inputs=[chatInput, chatbot, codeState],
289
+ outputs=[chatbot, chatInput],
290
+ )
291
+
292
+ # ── 분석하기 버튼 → Company 탭 로드 ──
293
+ loadBtn.click(
294
+ lambda code: code.strip(),
295
+ inputs=selectedCode,
296
+ outputs=codeState,
297
+ ).then(
298
+ handleLoadCompany,
299
+ inputs=selectedCode,
300
+ outputs=[companyInfo, topicSelect, financeTable],
301
+ )
302
+
303
+
304
+ if __name__ == "__main__":
305
+ demo.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ dartlab>=0.7.8
2
+ gradio>=5.0,<6
scripts/hfPreload.py DELETED
@@ -1,31 +0,0 @@
1
- """HuggingFace Spaces Docker 빌드 시 샘플 데이터 프리로드.
2
-
3
- 삼성전자(005930)와 AAPL 데이터를 미리 다운로드하여
4
- cold start 시간을 줄인다.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import os
10
- import sys
11
-
12
- # Docker 빌드 환경에서는 dartlab이 이미 pip install된 상태
13
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
14
-
15
- SAMPLES = ["005930", "AAPL"]
16
-
17
-
18
- def main() -> None:
19
- from dartlab.core.dataLoader import download
20
-
21
- for code in SAMPLES:
22
- print(f" preloading {code}...")
23
- try:
24
- download(code)
25
- print(f" {code} done")
26
- except Exception as e: # noqa: BLE001
27
- print(f" {code} skipped: {e}")
28
-
29
-
30
- if __name__ == "__main__":
31
- main()