career-chatbot-exp / src /storage.py
jonghhhh's picture
Update src/storage.py
8749cfa verified
Raw
History Blame Contribute Delete
6.97 kB
# storage.py (Hugging Face Dataset ๋ฒ„์ „)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๋ฐ์ดํ„ฐ ์ €์žฅ โ€” SQLite๋„ ๊ตฌ๊ธ€์‹œํŠธ๋„ ์“ฐ์ง€ ์•Š๊ณ  '์ด์ค‘ ์•ˆ์ „๋ง'์œผ๋กœ ์ €์žฅํ•œ๋‹ค.
#
# ์•ˆ์ „๋ง 1) HF Dataset ์—…๋กœ๋“œ : ์˜๊ตฌ ์ €์žฅ + ์—ฌ๋Ÿฌ ์ฐธ์—ฌ์ž ํ†ตํ•ฉ (๊ตฌ๊ธ€์‹œํŠธ ๋Œ€์ฒด)
# ์•ˆ์ „๋ง 2) ๋ฉ”๋ชจ๋ฆฌ(session_state): ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ์ฆ‰์‹œ CSV ๋‹ค์šด๋กœ๋“œ (๋ฐฑ์—…)
#
# ์™œ HF Dataset์ธ๊ฐ€:
# - ๊ตฌ๊ธ€ ๋ฐฉ์‹(์„œ๋น„์Šค๊ณ„์ •ยทJSON ํ‚คยท์‹œํŠธ ๊ณต์œ )์ด ์ดˆ๋ณด์ž์—๊ฒŒ ๋„ˆ๋ฌด ๋ณต์žกํ•˜๋‹ค.
# - ์ด๋ฏธ HF Spaces์— ๋ฐฐํฌํ•˜๋ฏ€๋กœ, ๊ฐ™์€ HF ์•ˆ์—์„œ 'ํ† ํฐ 1๊ฐœ'๋งŒ ๋” ๋งŒ๋“ค๋ฉด ๋.
# - Dataset ์ €์žฅ์†Œ๋Š” Space์™€ ๋‹ฌ๋ฆฌ ์žฌ์‹œ์ž‘์— ์˜ํ–ฅ๋ฐ›์ง€ ์•Š๋Š” git ์ €์žฅ์†Œ๋ผ
# ๋ฐ์ดํ„ฐ๊ฐ€ ์˜๊ตฌ ๋ณด์กด๋œ๋‹ค(Space๊ฐ€ ์Šฌ๋ฆฝ/์žฌ๋นŒ๋“œ๋ผ๋„ ์•ˆ์ „).
#
# ์ž‘๋™ ๋ฐฉ์‹:
# - ๋ชจ๋“  ๊ธฐ๋ก์€ ์ผ๋‹จ ๋ฉ”๋ชจ๋ฆฌ ๋ฆฌ์ŠคํŠธ(rows_*)์— ์Œ“์ธ๋‹ค(์•ˆ์ „๋ง 2).
# - '์ฐธ์—ฌ์ž ์™„๋ฃŒ' ์‹œ์ ์—๋งŒ, ๊ทธ๋•Œ๊นŒ์ง€ ๋ชจ์ธ ์ „์ฒด CSV๋ฅผ Dataset์— ๋ฎ์–ด์“ด๋‹ค(์•ˆ์ „๋ง 1).
# โ†’ ๋งค ๋ฉ”์‹œ์ง€๋งˆ๋‹ค ์˜ฌ๋ฆฌ๋ฉด ๋„ˆ๋ฌด ์žฆ์œผ๋ฏ€๋กœ, ์™„๋ฃŒ ์‹œ ํ•œ ๋ฒˆ๋งŒ ํ†ต์งธ๋กœ ์˜ฌ๋ฆฐ๋‹ค.
# - ๋‹จ, ๋ฉ”๋ชจ๋ฆฌ๋Š” 'ํ˜„์žฌ ๋ธŒ๋ผ์šฐ์ € ์„ธ์…˜'๋งŒ ๋‹ด๋Š”๋‹ค. ์—ฌ๋Ÿฌ ์ฐธ์—ฌ์ž๋ฅผ ํ•œ ํŒŒ์ผ๋กœ
# ํ•ฉ์น˜๊ธฐ ์œ„ํ•ด, ์—…๋กœ๋“œ ์ „์— Dataset์˜ ๊ธฐ์กด CSV๋ฅผ ๋‚ด๋ ค๋ฐ›์•„ '๋จธ์ง€(merge)'ํ•œ๋‹ค.
#
# ์„ค์ •(1ํšŒ): README/๊ฐ•์˜ 7์žฅ ์ฐธ์กฐ.
# 1) HF์—์„œ ๋นˆ Dataset ์ƒ์„ฑ (์˜ˆ: yourname/career-chatbot-data, Private ๊ถŒ์žฅ)
# 2) write ๊ถŒํ•œ ํ† ํฐ ๋ฐœ๊ธ‰ โ†’ Space secret์— HF_TOKEN์œผ๋กœ ๋“ฑ๋ก
# 3) ์•„๋ž˜ DATASET_REPO๋ฅผ ๋ณธ์ธ ๊ฒƒ์œผ๋กœ ๊ต์ฒด
# HF_TOKEN์ด ์—†์œผ๋ฉด ์—…๋กœ๋“œ๋Š” ์กฐ์šฉํžˆ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์‚ฌ์ด๋“œ๋ฐ” ๋‹ค์šด๋กœ๋“œ๋งŒ ๋™์ž‘ํ•œ๋‹ค
# (๋กœ์ปฌ ๊ฐœ๋ฐœ ์‹œ ํŽธ๋ฆฌ).
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import os
import io
from datetime import datetime, timezone
import pandas as pd
# huggingface_hub์€ Dataset ์—…๋กœ๋“œ๋ฅผ ์“ธ ๋•Œ๋งŒ ํ•„์š”. ์—†์œผ๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ชจ๋“œ๋กœ๋งŒ ๋™์ž‘.
try:
from huggingface_hub import HfApi, hf_hub_download
_HAS_HF = True
except ImportError:
_HAS_HF = False
# โ–ผโ–ผโ–ผ ๋ณธ์ธ์ด ๋งŒ๋“  Dataset ์ €์žฅ์†Œ ์ด๋ฆ„์œผ๋กœ ๊ต์ฒด โ–ผโ–ผโ–ผ
DATASET_REPO = "jonghhhh/career-chatbot-data"
# โ–ฒโ–ฒโ–ฒ (HF์—์„œ New Dataset์œผ๋กœ ๋งŒ๋“  'yourname/์ €์žฅ์†Œ์ด๋ฆ„') โ–ฒโ–ฒโ–ฒ
HF_TOKEN = os.getenv("HUGGINGFACE_TOKEN_WRITE") # Space secret ๋˜๋Š” .env์— ๋„ฃ์€ write ํ† ํฐ
# Dataset ์•ˆ์— ์ €์žฅ๋  3๊ฐœ CSV ํŒŒ์ผ ์ด๋ฆ„
FILES = {
"participants": "participants.csv",
"messages": "messages.csv",
"surveys": "surveys.csv",
}
def now_iso():
"""UTC ISO 8601 ํƒ€์ž„์Šคํƒฌํ”„ (์˜ˆ: 2026-05-27T12:34:56+00:00)."""
return datetime.now(timezone.utc).isoformat()
# โ”€โ”€ ๋ฉ”๋ชจ๋ฆฌ(์•ˆ์ „๋ง 2): ๋ชจ๋“  ๊ธฐ๋ก์„ ์„ธ์…˜ ๋ฆฌ์ŠคํŠธ์— ๋ˆ„์  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def init_state(state):
"""์„ธ์…˜ ์‹œ์ž‘ ์‹œ ๋ˆ„์  ๋ฆฌ์ŠคํŠธ 3๊ฐœ๋ฅผ ์ค€๋น„(์ด๋ฏธ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ)."""
state.setdefault("rows_participants", [])
state.setdefault("rows_messages", [])
state.setdefault("rows_surveys", [])
def add_participant(state, pid, condition):
"""์ฐธ์—ฌ์ž ๋ฐฐ์ • ๊ธฐ๋ก(๋ฉ”๋ชจ๋ฆฌ์—๋งŒ ์ถ”๊ฐ€. ์—…๋กœ๋“œ๋Š” ์™„๋ฃŒ ์‹œ ์ผ๊ด„)."""
state["rows_participants"].append(
{"participant_id": pid, "condition": condition,
"assigned_at": now_iso(), "completed": 0})
def add_message(state, pid, condition, turn, role, content, latency_ms=""):
"""๋Œ€ํ™” ํ•œ ํ„ด ๊ธฐ๋ก(๋ฉ”๋ชจ๋ฆฌ)."""
state["rows_messages"].append(
{"participant_id": pid, "condition": condition, "turn": turn,
"role": role, "content": content, "latency_ms": latency_ms,
"created_at": now_iso()})
def add_survey(state, pid, condition, phase, question_id, answer):
"""์„ค๋ฌธ ํ•œ ๋ฌธํ•ญ ๊ธฐ๋ก(๋ฉ”๋ชจ๋ฆฌ)."""
state["rows_surveys"].append(
{"participant_id": pid, "condition": condition, "phase": phase,
"question_id": question_id, "answer": str(answer),
"created_at": now_iso()})
# โ”€โ”€ HF Dataset(์•ˆ์ „๋ง 1): ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋จธ์ง€ ํ›„ ์—…๋กœ๋“œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _download_existing(api, filename):
"""Dataset์— ์ด๋ฏธ ์žˆ๋Š” CSV๋ฅผ DataFrame์œผ๋กœ ์ฝ์–ด์˜จ๋‹ค. ์—†์œผ๋ฉด ๋นˆ DF."""
try:
path = hf_hub_download(
repo_id=DATASET_REPO, filename=filename,
repo_type="dataset", token=HF_TOKEN,
)
return pd.read_csv(path)
except Exception:
# ํŒŒ์ผ์ด ์•„์ง ์—†๊ฑฐ๋‚˜(์ฒซ ์—…๋กœ๋“œ) ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ โ†’ ๋นˆ DF๋กœ ์‹œ์ž‘
return pd.DataFrame()
def _upload_csv(api, df, filename):
"""DataFrame์„ CSV ๋ฐ”์ดํŠธ๋กœ ๋งŒ๋“ค์–ด Dataset์— ๋ฎ์–ด์“ฐ๊ธฐ ์—…๋กœ๋“œ."""
buf = io.BytesIO(df.to_csv(index=False).encode("utf-8-sig"))
api.upload_file(
path_or_fileobj=buf, # ํŒŒ์ผ ๊ฒฝ๋กœ ๋Œ€์‹  ๋ฉ”๋ชจ๋ฆฌ ๋ฒ„ํผ๋ฅผ ์ง์ ‘ ์˜ฌ๋ฆผ
path_in_repo=filename,
repo_id=DATASET_REPO,
repo_type="dataset",
token=HF_TOKEN,
commit_message=f"update {filename} @ {now_iso()}",
)
def push_to_dataset(state):
"""ํ˜„์žฌ ์„ธ์…˜์˜ ๋ˆ„์ ๋ถ„์„ Dataset์˜ ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ํ•ฉ์ณ ์—…๋กœ๋“œ.
์‹คํŒจํ•ด๋„ ์ฐธ์—ฌ์ž ํ๋ฆ„์„ ๋ง‰์ง€ ์•Š๋„๋ก ์กฐ์šฉํžˆ ํ†ต๊ณผ(๋ฉ”๋ชจ๋ฆฌ ๋ฐฑ์—…์ด ๋‚จ์•„ ์žˆ์Œ).
๋ฐ˜ํ™˜: (์„ฑ๊ณต์—ฌ๋ถ€, ๋ฉ”์‹œ์ง€)."""
if not _HAS_HF or not HF_TOKEN:
return False, "HF_TOKEN ์—†์Œ โ†’ ์—…๋กœ๋“œ ๊ฑด๋„ˆ๋œ€(๋‹ค์šด๋กœ๋“œ ๋ฐฑ์—…๋งŒ ์‚ฌ์šฉ)"
try:
api = HfApi()
# ์„ธ ์ข…๋ฅ˜(participants/messages/surveys) ๊ฐ๊ฐ: ๊ธฐ์กด + ์ด๋ฒˆ ์„ธ์…˜ โ†’ ํ•ฉ์ณ์„œ ์—…๋กœ๋“œ
for key, fname in FILES.items():
new_df = pd.DataFrame(state[f"rows_{key}"])
if new_df.empty:
continue
old_df = _download_existing(api, fname)
merged = pd.concat([old_df, new_df], ignore_index=True)
# ํ˜น์‹œ ๊ฐ™์€ ์„ธ์…˜์„ ๋‘ ๋ฒˆ ์˜ฌ๋ ค ์ƒ๊ธฐ๋Š” ์™„์ „ ์ค‘๋ณต ํ–‰ ์ œ๊ฑฐ
merged = merged.drop_duplicates()
_upload_csv(api, merged, fname)
return True, "Dataset ์—…๋กœ๋“œ ์™„๋ฃŒ"
except Exception as e:
return False, f"์—…๋กœ๋“œ ์‹คํŒจ(๋ฌด์‹œํ•˜๊ณ  ์ง„ํ–‰): {e}"
# โ”€โ”€ ์‚ฌ์ด๋“œ๋ฐ” ๋‹ค์šด๋กœ๋“œ์šฉ: ๋ˆ„์ ๋ถ„ โ†’ CSV ๋ฐ”์ดํŠธ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def to_csv_bytes(rows):
"""๋ˆ„์  ๋ฆฌ์ŠคํŠธ โ†’ UTF-8-SIG CSV(์—‘์…€์—์„œ ํ•œ๊ธ€ ์•ˆ ๊นจ์ง). ๋น„๋ฉด ๋นˆ bytes."""
if not rows:
return b""
return pd.DataFrame(rows).to_csv(index=False).encode("utf-8-sig")