chainshift-dashboard / core /export_utils.py
GitHub Action
Sync from GitHub
ef78361
Raw
History Blame Contribute Delete
7.42 kB
"""
Export Utilities - ํ†ตํ•ฉ ๋‚ด๋ณด๋‚ด๊ธฐ ์ปดํฌ๋„ŒํŠธ
๊ณตํ†ต ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:
- CSV/Excel ๋ณ€ํ™˜
- ํ•„ํ„ฐ๋ง ์˜ต์…˜
- ์ „์ฒด ๋‹ต๋ณ€ ํฌํ•จ ์˜ต์…˜
"""
import pandas as pd
import streamlit as st
from io import BytesIO
from typing import Callable
from .supabase_client import get_sentiment_data_for_export
from .athena_client import fetch_full_answers_batch
# openpyxl ์„ค์น˜ ์—ฌ๋ถ€ ํ™•์ธ (Excel export์šฉ)
try:
import openpyxl
EXCEL_AVAILABLE = True
except ImportError:
EXCEL_AVAILABLE = False
# LLM ๊ฒ€์ฆ ์ƒํƒœ ๋ผ๋ฒจ (์šฉ์–ด ํ†ต์ผ)
LLM_STATUS_LABELS = {
"all": "์ „์ฒด",
"verified": "๊ฒ€์ฆ์™„๋ฃŒ",
"false_positive": "์˜คํƒ (๋ถ€์ •โ†’๋น„๋ถ€์ •)", # ๋ถ€์ • ์•„๋‹˜
"true_negative": "์ •ํƒ (๋ถ€์ • ํ™•์ •)", # ๋ถ€์ • ํ™•์ •
"unverified": "๋ฏธ๊ฒ€์ฆ",
}
POLARITY_LABELS = {
"all": "์ „์ฒด",
"negative": "๋ถ€์ •",
"positive": "๊ธ์ •",
"neutral": "์ค‘๋ฆฝ",
}
def prepare_dataframe_for_export(
data: list[dict],
include_full_answers: bool = False,
) -> pd.DataFrame:
"""๋ฐ์ดํ„ฐ๋ฅผ DataFrame์œผ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ๋‚ด๋ณด๋‚ด๊ธฐ์šฉ์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
Args:
data: ๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ๋ฆฌ์ŠคํŠธ
include_full_answers: ์ „์ฒด ๋‹ต๋ณ€ ํฌํ•จ ์—ฌ๋ถ€
Returns:
์ •๋ฆฌ๋œ DataFrame
"""
if not data:
return pd.DataFrame()
df = pd.DataFrame(data)
# ๋ฆฌ์ŠคํŠธ ์ปฌ๋Ÿผ์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
list_columns = ['in_house_brands', 'mentioned_brands', 'llm_evidence_spans']
for col in list_columns:
if col in df.columns:
df[col] = df[col].apply(
lambda x: ', '.join(x) if isinstance(x, list) else str(x) if x else ''
)
# ์ปฌ๋Ÿผ ์ˆœ์„œ ์ •๋ฆฌ - answer_full์„ answer_preview ๋‹ค์Œ์— ๋ฐฐ์น˜
if 'answer_full' in df.columns and 'answer_preview' in df.columns:
cols = list(df.columns)
cols.remove('answer_full')
idx = cols.index('answer_preview') + 1
cols.insert(idx, 'answer_full')
df = df[cols]
return df
def export_to_csv(df: pd.DataFrame) -> bytes:
"""DataFrame์„ CSV ๋ฐ”์ดํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
return df.to_csv(index=False).encode('utf-8-sig')
def export_to_excel(df: pd.DataFrame) -> bytes | None:
"""DataFrame์„ Excel ๋ฐ”์ดํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
Returns:
Excel ๋ฐ”์ดํŠธ ๋ฐ์ดํ„ฐ, ๋˜๋Š” openpyxl์ด ์—†์œผ๋ฉด None
"""
if not EXCEL_AVAILABLE:
return None
output = BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name='Data')
return output.getvalue()
def render_export_component(
campaign_id: int,
key_prefix: str,
title: str = "๐Ÿ“ฅ ๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ",
show_polarity_filter: bool = True,
show_llm_filter: bool = True,
default_polarity: str = "negative",
default_llm_status: str = "all",
in_house_only: bool = True,
):
"""ํ†ตํ•ฉ ๋‚ด๋ณด๋‚ด๊ธฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
Args:
campaign_id: ์บ ํŽ˜์ธ ID
key_prefix: Streamlit ์œ„์ ฏ ํ‚ค ์ ‘๋‘์‚ฌ (์ค‘๋ณต ๋ฐฉ์ง€)
title: ์„น์…˜ ์ œ๋ชฉ
show_polarity_filter: ๊ฐ์ • ํ•„ํ„ฐ ํ‘œ์‹œ ์—ฌ๋ถ€
show_llm_filter: LLM ์ƒํƒœ ํ•„ํ„ฐ ํ‘œ์‹œ ์—ฌ๋ถ€
default_polarity: ๊ธฐ๋ณธ ๊ฐ์ • ํ•„ํ„ฐ ๊ฐ’
default_llm_status: ๊ธฐ๋ณธ LLM ์ƒํƒœ ํ•„ํ„ฐ ๊ฐ’
in_house_only: ์ž์‚ฌ ๋ธŒ๋žœ๋“œ๋งŒ ํ•„ํ„ฐ๋ง
"""
with st.expander(title, expanded=False):
# ํ•„ํ„ฐ ์˜ต์…˜
filter_col1, filter_col2 = st.columns(2)
with filter_col1:
if show_polarity_filter:
polarity_options = list(POLARITY_LABELS.keys())
polarity_labels = list(POLARITY_LABELS.values())
default_idx = polarity_options.index(default_polarity) if default_polarity in polarity_options else 0
selected_polarity = st.selectbox(
"๊ฐ์ • ํ•„ํ„ฐ",
options=polarity_options,
format_func=lambda x: POLARITY_LABELS[x],
index=default_idx,
key=f"{key_prefix}_polarity"
)
else:
selected_polarity = default_polarity
with filter_col2:
if show_llm_filter:
llm_options = list(LLM_STATUS_LABELS.keys())
default_idx = llm_options.index(default_llm_status) if default_llm_status in llm_options else 0
selected_llm_status = st.selectbox(
"LLM ๊ฒ€์ฆ ์ƒํƒœ",
options=llm_options,
format_func=lambda x: LLM_STATUS_LABELS[x],
index=default_idx,
key=f"{key_prefix}_llm_status"
)
else:
selected_llm_status = default_llm_status
# ๋‚ด๋ณด๋‚ด๊ธฐ ์˜ต์…˜
opt_col1, opt_col2 = st.columns(2)
with opt_col1:
include_full_answers = st.checkbox(
"์ „์ฒด ๋‹ต๋ณ€ ํฌํ•จ",
value=False,
help="Athena์—์„œ ์ „์ฒด ๋‹ต๋ณ€์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค (ํŒŒ์ผ ํฌ๊ธฐ ์ฆ๊ฐ€)",
key=f"{key_prefix}_full_answers"
)
with opt_col2:
include_evidence = st.checkbox(
"LLM ๊ทผ๊ฑฐ ํฌํ•จ",
value=False,
help="LLM ํŒ๋‹จ ๊ทผ๊ฑฐ(reasoning, evidence_spans)๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค",
key=f"{key_prefix}_evidence"
)
st.markdown("---")
# ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ
btn_col1, btn_col2, btn_col3 = st.columns([1, 1, 2])
# ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
data = get_sentiment_data_for_export(
campaign_id=campaign_id,
polarity=selected_polarity if selected_polarity != "all" else None,
llm_status=selected_llm_status if selected_llm_status != "all" else None,
in_house_only=in_house_only,
include_full_answers=include_full_answers,
include_evidence=include_evidence,
)
if data:
df = prepare_dataframe_for_export(data, include_full_answers)
count = len(df)
with btn_col1:
csv_data = export_to_csv(df)
st.download_button(
label=f"๐Ÿ“ฅ CSV ({count}๊ฑด)",
data=csv_data,
file_name=f"campaign_{campaign_id}_export_{count}๊ฑด.csv",
mime="text/csv",
key=f"{key_prefix}_csv_download"
)
with btn_col2:
if EXCEL_AVAILABLE:
excel_data = export_to_excel(df)
st.download_button(
label=f"๐Ÿ“ฅ Excel ({count}๊ฑด)",
data=excel_data,
file_name=f"campaign_{campaign_id}_export_{count}๊ฑด.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
key=f"{key_prefix}_excel_download"
)
else:
st.caption("Excel: openpyxl ํ•„์š”")
with btn_col3:
st.caption(f"์ด {count}๊ฑด | ํ•„ํ„ฐ: {POLARITY_LABELS.get(selected_polarity, '์ „์ฒด')} / {LLM_STATUS_LABELS.get(selected_llm_status, '์ „์ฒด')}")
else:
st.info("๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")