github-actions[bot] commited on
Commit
93ec99f
Β·
1 Parent(s): 645a5e6

sync from f0b3ebb

Browse files
Files changed (2) hide show
  1. app.py +123 -34
  2. requirements.txt +2 -0
app.py CHANGED
@@ -3,6 +3,7 @@
3
  from __future__ import annotations
4
 
5
  import gc
 
6
  import os
7
  import re
8
  from collections import OrderedDict
@@ -194,35 +195,131 @@ def _resolveCode(query: str) -> tuple[str | None, str | None]:
194
  return None, f"검색 μ‹€νŒ¨: {e}"
195
 
196
 
197
- def _buildColumnConfig(df: pd.DataFrame) -> dict:
198
- """숫자 μ»¬λŸΌμ— μ²œλ‹¨μœ„ κ΅¬λΆ„μž 포맷 적용."""
199
- config = {}
200
  if df is None or df.empty:
201
- return config
202
- for col in df.columns:
203
- if pd.api.types.is_integer_dtype(df[col]):
204
- config[col] = st.column_config.NumberColumn(
205
- col, format="%d",
 
206
  )
207
- elif pd.api.types.is_float_dtype(df[col]):
208
- config[col] = st.column_config.NumberColumn(
209
- col, format="%.2f",
210
- )
211
- return config
 
 
 
212
 
213
 
214
- def _showDataFrame(df: pd.DataFrame, key: str = ""):
215
- """DataFrame을 ν¬λ§·νŒ…ν•΄μ„œ ν‘œμ‹œ."""
216
  if df is None or df.empty:
217
  st.markdown('<p class="dl-empty">데이터 μ—†μŒ</p>', unsafe_allow_html=True)
218
  return
 
219
  st.dataframe(
220
- df,
221
- column_config=_buildColumnConfig(df),
222
  use_container_width=True,
223
  hide_index=True,
224
  key=key or None,
225
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
 
228
  # ── ν”„λ¦¬λ‘œλ“œ ──────────────────────────────────────────
@@ -328,7 +425,7 @@ if code:
328
  except Exception:
329
  pass
330
 
331
- _showDataFrame(finDf, key="finance")
332
 
333
  # ── Sections ──
334
  topics = []
@@ -361,17 +458,14 @@ if code:
361
  secText = f"쑰회 μ‹€νŒ¨: {e}"
362
 
363
  if secDf is not None:
364
- _showDataFrame(secDf, key="section")
365
  elif secText:
366
  st.markdown(secText)
367
 
368
  # ── AI Chat ──
369
- with st.expander("πŸ€– AI 뢄석 (OpenAI API ν•„μš”)", expanded=False):
370
- hasAi = bool(os.environ.get("OPENAI_API_KEY"))
371
-
372
- if not hasAi:
373
- st.info("AI 뢄석을 μ‚¬μš©ν•˜λ €λ©΄ HuggingFace Spaces Settings β†’ Variables and secretsμ—μ„œ `OPENAI_API_KEY`λ₯Ό μ„€μ •ν•˜μ„Έμš”.")
374
- else:
375
  if "messages" not in st.session_state:
376
  st.session_state.messages = []
377
 
@@ -385,15 +479,10 @@ if code:
385
  st.markdown(prompt)
386
 
387
  with st.chat_message("assistant"):
388
- try:
389
- q = f"{code} {prompt}"
390
- answer = dartlab.ask(q, stream=False, raw=False)
391
- st.markdown(answer or "응닡 μ—†μŒ")
392
- st.session_state.messages.append({"role": "assistant", "content": answer or "응닡 μ—†μŒ"})
393
- except Exception as e:
394
- errMsg = f"뢄석 μ‹€νŒ¨: {e}"
395
- st.markdown(errMsg)
396
- st.session_state.messages.append({"role": "assistant", "content": errMsg})
397
 
398
  else:
399
  # λ―Έμž…λ ₯ μƒνƒœ
 
3
  from __future__ import annotations
4
 
5
  import gc
6
+ import io
7
  import os
8
  import re
9
  from collections import OrderedDict
 
195
  return None, f"검색 μ‹€νŒ¨: {e}"
196
 
197
 
198
+ def _formatDf(df: pd.DataFrame) -> pd.DataFrame:
199
+ """숫자 μ»¬λŸΌμ„ μ²œλ‹¨μœ„ 콀마 λ¬Έμžμ—΄λ‘œ λ³€ν™˜ (μ†Œμˆ˜μ  제거)."""
 
200
  if df is None or df.empty:
201
+ return df
202
+ result = df.copy()
203
+ for col in result.columns:
204
+ if pd.api.types.is_numeric_dtype(result[col]):
205
+ result[col] = result[col].apply(
206
+ lambda x: f"{int(x):,}" if pd.notna(x) and x == x else ""
207
  )
208
+ return result
209
+
210
+
211
+ def _toExcel(df: pd.DataFrame) -> bytes:
212
+ """DataFrame β†’ Excel bytes."""
213
+ buf = io.BytesIO()
214
+ df.to_excel(buf, index=False, engine="openpyxl")
215
+ return buf.getvalue()
216
 
217
 
218
+ def _showDataFrame(df: pd.DataFrame, key: str = "", downloadName: str = ""):
219
+ """DataFrame ν‘œμ‹œ + μ—‘μ…€ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ."""
220
  if df is None or df.empty:
221
  st.markdown('<p class="dl-empty">데이터 μ—†μŒ</p>', unsafe_allow_html=True)
222
  return
223
+ # ν¬λ§·νŒ…λœ λ²„μ „μœΌλ‘œ ν‘œμ‹œ
224
  st.dataframe(
225
+ _formatDf(df),
 
226
  use_container_width=True,
227
  hide_index=True,
228
  key=key or None,
229
  )
230
+ # μ—‘μ…€ λ‹€μš΄λ‘œλ“œ (원본 숫자 μœ μ§€)
231
+ if downloadName:
232
+ st.download_button(
233
+ label="πŸ“₯ Excel λ‹€μš΄λ‘œλ“œ",
234
+ data=_toExcel(df),
235
+ file_name=f"{downloadName}.xlsx",
236
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
237
+ key=f"dl_{key}" if key else None,
238
+ )
239
+
240
+
241
+ # ── AI ────────────────────────────────────────────────
242
+
243
+ _HAS_OPENAI = bool(os.environ.get("OPENAI_API_KEY"))
244
+
245
+ if _HAS_OPENAI:
246
+ dartlab.llm.configure(provider="openai", api_key=os.environ["OPENAI_API_KEY"])
247
+
248
+
249
+ def _askAi(stockCode: str, question: str) -> str:
250
+ """AI 질문 처리. OpenAI μš°μ„ , μ—†μœΌλ©΄ HF 무료 Inference API."""
251
+ # OpenAIκ°€ μ„€μ •λ˜μ–΄ 있으면 dartlab.ask μ‚¬μš©
252
+ if _HAS_OPENAI:
253
+ try:
254
+ q = f"{stockCode} {question}" if stockCode else question
255
+ answer = dartlab.ask(q, stream=False, raw=False)
256
+ return answer or "응닡 μ—†μŒ"
257
+ except Exception as e:
258
+ return f"뢄석 μ‹€νŒ¨: {e}"
259
+
260
+ # HF Inference API (토큰 없이도 무료 호좜 κ°€λŠ₯)
261
+ try:
262
+ from huggingface_hub import InferenceClient
263
+ token = os.environ.get("HF_TOKEN") # 있으면 rate limit 높아짐
264
+ client = InferenceClient(
265
+ model="meta-llama/Llama-3.1-8B-Instruct",
266
+ token=token if token else None,
267
+ )
268
+
269
+ context = _buildAiContext(stockCode)
270
+ systemMsg = (
271
+ "당신은 ν•œκ΅­ κΈ°μ—… 재무 뢄석 μ „λ¬Έκ°€μž…λ‹ˆλ‹€. "
272
+ "μ•„λž˜ 재무 데이터λ₯Ό λ°”νƒ•μœΌλ‘œ μ‚¬μš©μžμ˜ μ§ˆλ¬Έμ— ν•œκ΅­μ–΄λ‘œ λ‹΅λ³€ν•˜μ„Έμš”. "
273
+ "μˆ«μžλŠ” μ²œλ‹¨μœ„ 콀마λ₯Ό μ‚¬μš©ν•˜κ³ , κ·Όκ±°λ₯Ό λͺ…ν™•νžˆ μ œμ‹œν•˜μ„Έμš”.\n\n"
274
+ f"{context}"
275
+ )
276
+
277
+ response = client.chat_completion(
278
+ messages=[
279
+ {"role": "system", "content": systemMsg},
280
+ {"role": "user", "content": question},
281
+ ],
282
+ max_tokens=1024,
283
+ )
284
+ return response.choices[0].message.content or "응닡 μ—†μŒ"
285
+ except Exception as e:
286
+ return f"AI 뢄석 μ‹€νŒ¨: {e}"
287
+
288
+
289
+ def _buildAiContext(stockCode: str) -> str:
290
+ """AI에 전달할 κΈ°μ—… 재무 μ»¨ν…μŠ€νŠΈ ꡬ성."""
291
+ try:
292
+ c = _getCompany(stockCode)
293
+ except Exception:
294
+ return f"μ’…λͺ©μ½”λ“œ: {stockCode}"
295
+
296
+ parts = [f"κΈ°μ—…: {c.corpName} ({c.stockCode}), μ‹œμž₯: {c.market}"]
297
+
298
+ # IS μš”μ•½
299
+ try:
300
+ isDf = _toPandas(c.IS)
301
+ if isDf is not None and not isDf.empty:
302
+ parts.append(f"\n[μ†μ΅κ³„μ‚°μ„œ μš”μ•½]\n{isDf.head(15).to_string()}")
303
+ except Exception:
304
+ pass
305
+
306
+ # BS μš”μ•½
307
+ try:
308
+ bsDf = _toPandas(c.BS)
309
+ if bsDf is not None and not bsDf.empty:
310
+ parts.append(f"\n[μž¬λ¬΄μƒνƒœν‘œ μš”μ•½]\n{bsDf.head(15).to_string()}")
311
+ except Exception:
312
+ pass
313
+
314
+ # ratios μš”μ•½
315
+ try:
316
+ ratDf = _toPandas(c.ratios)
317
+ if ratDf is not None and not ratDf.empty:
318
+ parts.append(f"\n[μž¬λ¬΄λΉ„μœ¨]\n{ratDf.head(15).to_string()}")
319
+ except Exception:
320
+ pass
321
+
322
+ return "\n".join(parts)
323
 
324
 
325
  # ── ν”„λ¦¬λ‘œλ“œ ──────────────────────────────────────────
 
425
  except Exception:
426
  pass
427
 
428
+ _showDataFrame(finDf, key="finance", downloadName=f"{code}_{sheetTab}")
429
 
430
  # ── Sections ──
431
  topics = []
 
458
  secText = f"쑰회 μ‹€νŒ¨: {e}"
459
 
460
  if secDf is not None:
461
+ _showDataFrame(secDf, key="section", downloadName=f"{code}_{selectedTopic}")
462
  elif secText:
463
  st.markdown(secText)
464
 
465
  # ── AI Chat ──
466
+ with st.expander("πŸ€– AI 뢄석 (무료)", expanded=False):
467
+ st.caption("Llama 3.1 8B 기반 무료 AI 뢄석 Β· λ³΅μž‘ν•œ μ§ˆλ¬Έμ€ OpenAI API μ„€μ • μ‹œ 더 μ •ν™•ν•©λ‹ˆλ‹€")
468
+ if True:
 
 
 
469
  if "messages" not in st.session_state:
470
  st.session_state.messages = []
471
 
 
479
  st.markdown(prompt)
480
 
481
  with st.chat_message("assistant"):
482
+ with st.spinner("뢄석 쀑..."):
483
+ answer = _askAi(code, prompt)
484
+ st.markdown(answer)
485
+ st.session_state.messages.append({"role": "assistant", "content": answer})
 
 
 
 
 
486
 
487
  else:
488
  # λ―Έμž…λ ₯ μƒνƒœ
requirements.txt CHANGED
@@ -1,2 +1,4 @@
1
  dartlab>=0.7.8
2
  streamlit>=1.45,<2
 
 
 
1
  dartlab>=0.7.8
2
  streamlit>=1.45,<2
3
+ openpyxl>=3.1
4
+ huggingface_hub>=0.25