streamlit / src /app.py
hyerong's picture
Upload src/app.py with huggingface_hub
ef12202 verified
import os, json, re, io, requests
import streamlit as st
from openai import OpenAI
from PIL import Image
from datetime import datetime
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 1. ์ดˆ๊ธฐ ์„ค์ • ๋ฐ CSS ๋กœ๋“œ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def local_css(file_name):
current_dir = os.path.dirname(os.path.abspath(__file__))
css_path = os.path.join(current_dir, file_name)
if os.path.exists(css_path):
with open(css_path, encoding="utf-8") as f:
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
st.set_page_config(page_title="MeetingAI | KT Enterprise", page_icon="๐ŸŽ™๏ธ", layout="wide")
local_css("style.css")
# ๊ฒฝ๋กœ ๋ฐ ํด๋ผ์ด์–ธํŠธ ์„ค์ •
current_dir = os.path.dirname(os.path.abspath(__file__))
img_path = os.path.join(current_dir, "kt.png")
client = OpenAI()
# ์„ธ์…˜ ์ƒํƒœ ์ดˆ๊ธฐํ™”
if "analysis_result" not in st.session_state:
st.session_state.analysis_result = None
if "transcript" not in st.session_state:
st.session_state.transcript = ""
if "messages" not in st.session_state: # ์ค‘์š” : AttributeError ํ•ด๊ฒฐ์„ ์œ„ํ•œ ํ•ต์‹ฌ ์ดˆ๊ธฐํ™”
st.session_state.messages = []
# ์„ธ์…˜์ƒํƒœ ์ดˆ๊ธฐํ™” ์ถ”๊ฐ€!!!!!!!!!!!
if "mail_body" not in st.session_state: # ์ด๋ฉ”์ผ ๋‚ด์šฉ ์ถ”๊ฐ€
st.session_state.mail_body = ""
# ์šฐ์„ ์ˆœ์œ„ ๋ผ๋ฒจ ์„ค์ •
PRIORITY_LABEL = {"high": "๐Ÿ”ด ๋†’์Œ", "medium": "๐ŸŸก ๋ณดํ†ต", "low": "๐ŸŸข ๋‚ฎ์Œ"}
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 2. ๋ฐฑ์—”๋“œ ํ•ต์‹ฌ ํ•จ์ˆ˜ (๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€, ๋ถ„์„)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def detect_sensitive(text):
SENSITIVE_PATTERNS = [
(r"๊ธฐ๋ฐ€|๋น„๋ฐ€|๋Œ€์™ธ๋น„|๋‚ด๋ถ€.*๋ณด์•ˆ|๋ณด์•ˆ.*์œ ์ง€|confidential", "๊ธฐ๋ฐ€/๋Œ€์™ธ๋น„ ํ‘œํ˜„"),
(r"๊ธ‰์—ฌ|์—ฐ๋ด‰|์ž„๊ธˆ|salary", "๊ธ‰์—ฌยท์ธ์‚ฌ ์ •๋ณด"),
(r"๊ฐœ์ธ์ •๋ณด|์ฃผ๋ฏผ|์ƒ๋…„์›”์ผ", "๊ฐœ์ธ์ •๋ณด"),
(r"\d{3}-\d{4}-\d{4}", "์ „ํ™”๋ฒˆํ˜ธ ํŒจํ„ด"),
(r"\d{6}-\d{7}", "์ฃผ๋ฏผ๋ฒˆํ˜ธ ํŒจํ„ด"),
]
return list({label for pat, label in SENSITIVE_PATTERNS if re.search(pat, text, re.IGNORECASE)})
sys_role = """
## ์—ญํ• 
๋‹น์‹ ์€ KT Enterprise IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting์˜ AI ํšŒ์˜ ๋น„์„œ์ž…๋‹ˆ๋‹ค.
ํšŒ์˜ ๋‚ด์šฉ์„ ์ •ํ™•ํ•˜๊ณ  ์‹ ์†ํ•˜๊ฒŒ ๋ถ„์„ยท์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์‘๋‹ต์€ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
## ํšŒ์˜๋ก ์ •๋ฆฌ ๊ทœ์น™
1. ํšŒ์˜ ๊ฐœ์š” โ€” ์ œ๋ชฉ / ๋ชฉ์  / ์ฃผ์š” ์•ˆ๊ฑด
2. ๊ฒฐ์ •๋œ ์‚ฌํ•ญ โ€” ํ•ต์‹ฌ ๊ฒฐ์ •๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ
3. ๋‹ด๋‹น ์—…๋ฌด(Action Items) โ€” [๋‹ด๋‹น์ž]:[์—…๋ฌด]/๊ธฐํ•œ/์šฐ์„ ์ˆœ์œ„
4. ์ผ์ • ์š”์•ฝ โ€” ๋งˆ๊ฐ ๊ธฐํ•œ ์‹œ๊ฐ„์ˆœ ์ •๋ฆฌ
"""
sys_role_chat = "๋‹น์‹ ์€ ํšŒ์˜ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋‹ต๋ณ€ํ•˜๋Š” ์นœ์ ˆํ•œ AI ๋น„์„œ '์—์ด๋ธ”'์ž…๋‹ˆ๋‹ค. ์ด์ „ ๋Œ€ํ™”๋ฅผ ๊ธฐ์–ตํ•˜๋ฉฐ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค."
def analyze_meeting(text):
today = datetime.now().strftime("%Y๋…„ %m์›” %d์ผ")
prompt = f"""
๋‹ค์Œ์€ ์˜ค๋Š˜({today}) ์ง„ํ–‰๋œ ํšŒ์˜ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ์•„๋ž˜ JSON ํ˜•์‹์œผ๋กœ ๋ถ„์„ํ•˜์„ธ์š”. ๋ฐ˜๋“œ์‹œ JSON๋งŒ ์ถœ๋ ฅํ•˜์„ธ์š”.
ํšŒ์˜ ๋‚ด์šฉ:
{text}
{{
"meeting_title": "ํšŒ์˜ ์ œ๋ชฉ",
"purpose": "ํšŒ์˜ ๋ชฉ์ ",
"decisions": ["๊ฒฐ์ • ์‚ฌํ•ญ ๋ฆฌ์ŠคํŠธ"],
"tasks": [
{{"person":"๋‹ด๋‹น์ž","task":"์—…๋ฌด ๋‚ด์šฉ","deadline":"๊ธฐํ•œ","priority":"high|medium|low"}}
],
"agenda_items": ["์•ˆ๊ฑด ๋ฆฌ์ŠคํŠธ"],
"summary": "์ „์ฒด ์š”์•ฝ",
"keywords": ["ํ‚ค์›Œ๋“œ1", "ํ‚ค์›Œ๋“œ2"]
}}
"""
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": sys_role}, {"role": "user", "content": prompt}],
temperature=0.2,
)
raw = re.sub(r"^```json\s*|```\s*$", "", resp.choices[0].message.content.strip())
return json.loads(raw)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Email ๋ฐœ์†ก ํ•จ์ˆ˜ ์ถ”๊ฐ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ์ด๋ฉ”์ผ ์ดˆ์•ˆ ์ƒ์„ฑ ํ•จ์ˆ˜ ์ถ”๊ฐ€!!!!!!!!!!!!
def generate_email_body(res, recipient_name):
decisions = "\n".join(f" โ€ข {d}" for d in res.get("decisions", []))
tasks = "\n".join(
f" - [{t['person']}] {t['task']}" + (f" (๊ธฐํ•œ: {t['deadline']})" if t.get("deadline") and t["deadline"] != "null" else "")
for t in res.get("tasks", [])
)
prompt = f"""
๋‹ค์Œ ํšŒ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ {recipient_name}์—๊ฒŒ ๋ณด๋‚ด๋Š” ๊ณต์‹ ๋ฆฌ๋งˆ์ธ๋“œ ์ด๋ฉ”์ผ์„ ์ž‘์„ฑํ•˜์„ธ์š”.
์ •์ค‘ํ•˜๊ณ  ์ „๋ฌธ์ ์ธ ์–ด์กฐ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
๊ทœ์น™:
- ์ด๋ฉ”์ผ ์ฒซ ์ค„ ์ธ์‚ฌ๋Š” ๋ฐ˜๋“œ์‹œ "KT Enterprise IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting AI ๋น„์„œ AIBLE์ž…๋‹ˆ๋‹ค." ๋กœ ์‹œ์ž‘ํ•˜์„ธ์š”.
- ์ด๋ฉ”์ผ ๋งˆ์ง€๋ง‰ ์„œ๋ช…์€ ๋ฐ˜๋“œ์‹œ "AIBLE ๋“œ๋ฆผ" ์œผ๋กœ ๋๋‚ด์„ธ์š”.
- ๊ทธ ์™ธ ์ง์› ์ด๋ฆ„์ด๋‚˜ ๊ฐœ์ธ ์„œ๋ช…์€ ์ ˆ๋Œ€ ํฌํ•จํ•˜์ง€ ๋งˆ์„ธ์š”.
ํšŒ์˜ ์ œ๋ชฉ: {res.get('meeting_title', 'ํšŒ์˜')}
๊ฒฐ์ • ์‚ฌํ•ญ:
{decisions}
๋‹ด๋‹น ์—…๋ฌด:
{tasks}
์ด๋ฉ”์ผ ๋ณธ๋ฌธ๋งŒ ์ถœ๋ ฅํ•˜์„ธ์š” (์ œ๋ชฉ ์ œ์™ธ).
"""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.4,
)
return resp.choices[0].message.content.strip()
# ์ด๋ฉ”์ผ ๋ฐœ์†ก ํ•จ์ˆ˜ (SendGrid REST API)
def send_email(to, subject, body):
api_key = os.getenv("SENDGRID_API_KEY", "")
sender = os.getenv("EMAIL_ADDRESS", "")
resp = requests.post(
"https://api.sendgrid.com/v3/mail/send",
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
json={
"personalizations": [{"to": [{"email": to}], "subject": subject}],
"from": {"email": sender},
"content": [{"type": "text/plain", "value": body}],
},
)
return resp.status_code == 202
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 3. UI ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ (ํ—ค๋” ๋ฐ ์ž…๋ ฅ๋ถ€)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ์ƒ๋‹จ ํ—ค๋” ์˜์—ญ (ํฌ๊ธฐ ํ™•๋Œ€ ๋ฒ„์ „)
st.markdown("""
<div style="display:flex;align-items:center;gap:20px;padding:25px 30px;
background:linear-gradient(135deg,#16213E 0%,#0F3460 100%);
border-bottom:4px solid #E3000B;border-radius:15px;margin-bottom:25px;">
<div style="font-family:'Rajdhani',sans-serif;font-size:3.5rem;font-weight:800;
color:#E3000B;letter-spacing:1px;line-height:1;">KT</div>
<div class="title-container">
<div class="main-title" style="font-size:2.2rem; font-weight:700; color:#FFFFFF;">Meeting AI ํšŒ์˜ ๋ถ„์„ ์–ด์‹œ์Šคํ„ดํŠธ</div>
<div class="sub-title" style="font-size:1.0rem; color:#E0E0E0; margin-top:5px;">IT๊ธฐ์ˆ ํ˜์‹ ํŒ€ DX Consulting ยท Intelligent Meeting Solution</div>
</div>
</div>
""", unsafe_allow_html=True)
col1, col2 = st.columns([1, 2], gap="large")
with col1:
try:
st.image(Image.open(img_path), caption="KT DX Assistant", use_container_width=True)
except:
st.info("์ด๋ฏธ์ง€(kt.png)๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
with col2:
st.subheader("ํšŒ์˜๋ก์„ ์—…๋กœ๋“œํ•˜์‹œ๋ฉด ์š”์•ฝํ•ด๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.")
with st.container(border=True):
uploaded_file = st.file_uploader("๐Ÿ“„ ํšŒ์˜ ๋‚ด์šฉ ํ…์ŠคํŠธ ํŒŒ์ผ ์—…๋กœ๋“œ", type=["txt"])
st.write("**์š”์ฒญ ๋ฐฉ์‹ ์„ ํƒ**")
input_mode = st.radio("์š”์ฒญ ๋ฐฉ์‹", ["๐ŸŽ™๏ธ ์‹ค์‹œ๊ฐ„ ๋…น์Œ", "๐Ÿ“ ์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ", "๐Ÿ“ ํ…์ŠคํŠธ ์ž…๋ ฅ"], horizontal=True, label_visibility="collapsed")
audio_value = None
uploaded_audio_file = None
text_input = ""
if input_mode == "๐ŸŽ™๏ธ ์‹ค์‹œ๊ฐ„ ๋…น์Œ":
audio_value = st.audio_input("์Œ์„ฑ์œผ๋กœ ๋‚ด์šฉ์„ ๋งํ•ด์ฃผ์„ธ์š”.")
elif input_mode == "๐Ÿ“ ์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ":
uploaded_audio_file = st.file_uploader("์Œ์„ฑ ํŒŒ์ผ ์—…๋กœ๋“œ", type=["mp3", "wav", "m4a"])
else:
text_input = st.text_area("๋‚ด์šฉ์„ ์ง์ ‘ ์ž…๋ ฅํ•˜์„ธ์š”.", height=150)
submit = st.button("๐Ÿš€ ํšŒ์˜ ๋ถ„์„ ์‹œ์ž‘", use_container_width=True)
if submit:
meeting_text = ""
if uploaded_file:
meeting_text = uploaded_file.read().decode("utf-8")
with st.spinner("AI๊ฐ€ ๋‚ด์šฉ์„ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค..."):
try:
user_req = ""
if audio_value:
user_req = client.audio.transcriptions.create(model="whisper-1", file=("audio.wav", audio_value, "audio/wav")).text
elif uploaded_audio_file:
user_req = client.audio.transcriptions.create(model="whisper-1", file=(uploaded_audio_file.name, uploaded_audio_file, uploaded_audio_file.type)).text
else:
user_req = text_input
final_content = (meeting_text + "\n" + user_req).strip()
if not final_content:
st.warning("๋ถ„์„ํ•  ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค.")
else:
st.session_state.transcript = final_content
st.session_state.analysis_result = analyze_meeting(final_content)
st.session_state.messages = [] # ์ƒˆ ๋ถ„์„ ์‹œ ๋Œ€ํ™” ์ดˆ๊ธฐํ™”, ์„ธ์…˜ ๋ฆฌ์ŠคํŠธ ํ•œ๋ฒˆ ๋น„์›Œ์ค˜์•ผํ•จ
st.rerun()
except Exception as e:
st.error(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 4. ๊ฒฐ๊ณผ ์ถœ๋ ฅ ์˜์—ญ (Tabs ํ™œ์šฉ) + ๋ฉ€ํ‹ฐํ„ด ๊ธฐ๋Šฅ ์ถ”๊ฐ€
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if st.session_state.analysis_result:
res = st.session_state.analysis_result
hits = detect_sensitive(st.session_state.transcript)
if hits:
st.error(f"๐Ÿ”’ ๋ฏผ๊ฐ์ •๋ณด ๊ฐ์ง€: {', '.join(hits)} ํ•ญ๋ชฉ ์ฃผ์˜")
tab1, tab2, tab3, tab4, tab5 = st.tabs(["๐Ÿ“‹ ํšŒ์˜ ์š”์•ฝ", "โœ… ํ•  ์ผ & ์šฐ์„ ์ˆœ์œ„", "๐Ÿ“„ ์›๋ฌธ ๋ณด๊ธฐ", "โœ‰๏ธ ๋ฉ”์ผ ๋ฐœ์†ก", "๐Ÿค– ์งˆ์˜์‘๋‹ต"])
with tab1:
st.markdown(f"### ๐Ÿ“Œ {res.get('meeting_title', 'ํšŒ์˜ ๊ฒฐ๊ณผ')}")
st.info(f"**ํšŒ์˜ ๋ชฉ์ :** {res.get('purpose', '๋‚ด์šฉ ์—†์Œ')}")
c1, c2 = st.columns(2)
with c1:
st.subheader("โœ… ๊ฒฐ์ • ์‚ฌํ•ญ")
for d in res.get('decisions', []): st.write(f"- {d}")
with c2:
st.subheader("๐Ÿ“Œ ์ฃผ์š” ์•ˆ๊ฑด")
for a in res.get('agenda_items', []): st.write(f"- {a}")
st.divider()
st.subheader("๐Ÿ’ฌ ์ „์ฒด ์š”์•ฝ")
st.write(res.get('summary', '์š”์•ฝ ๋‚ด์šฉ ์—†์Œ'))
with tab2:
tasks = res.get('tasks', [])
if not tasks:
st.write("๋ฐฐ์ •๋œ ํ•  ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค.")
else:
for t in tasks:
p = t.get('priority', 'low').lower()
label = PRIORITY_LABEL.get(p, "๐ŸŸข ๋‚ฎ์Œ")
deadline = f" (๊ธฐํ•œ: {t['deadline']})" if t.get('deadline') and t['deadline'] != "null" else ""
st.markdown(f"**{label} [{t.get('person', '๋ฏธ์ง€์ •')}]** : {t.get('task')}{deadline}")
with tab3:
st.markdown("#### ๐Ÿ”‘ ํ•ต์‹ฌ ํ‚ค์›Œ๋“œ")
st.write(", ".join([f"#{k}" for k in res.get('keywords', [])]))
st.divider()
st.text_area("์ „์‚ฌ ์›๋ฌธ", st.session_state.transcript, height=300)
###################################################### ์ด๋ฉ”์ผ ๋ฐœ์†ก ์ถ”๊ฐ€
with tab4:
st.subheader("โœ‰๏ธ ํšŒ์˜ ๊ฒฐ๊ณผ ๋ฆฌ๋งˆ์ธ๋“œ ๋ฉ”์ผ")
# ์ˆ˜์‹ ์ž ์ •๋ณด๋ฅผ ๋จผ์ € ์ž…๋ ฅ
recipient_name = st.text_input("์ˆ˜์‹ ์ž ์ด๋ฆ„", placeholder="์˜ˆ) ๊น€๋Œ€๋ฆฌ")
recipient_email = st.text_input("์ˆ˜์‹ ์ž ์ด๋ฉ”์ผ ์ฃผ์†Œ", placeholder="์˜ˆ) kim@kt.com")
if st.button("๐Ÿ“ ๋ฉ”์ผ ์ดˆ์•ˆ ์ƒ์„ฑ"):
if not recipient_name.strip():
st.warning("์ˆ˜์‹ ์ž ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
else:
with st.spinner("๋ฉ”์ผ ์ดˆ์•ˆ์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘..."):
st.session_state.mail_body = generate_email_body(res, recipient_name)
if st.session_state.mail_body:
mail_subject = st.text_input(
"๋ฉ”์ผ ์ œ๋ชฉ",
value=f"[ํšŒ์˜ ๊ฒฐ๊ณผ ๊ณต์œ ] {res.get('meeting_title', 'ํšŒ์˜')}",
)
edited_body = st.text_area(
"๋ฉ”์ผ ๋ณธ๋ฌธ (์ˆ˜์ • ๊ฐ€๋Šฅ)",
value=st.session_state.mail_body,
height=300,
)
if st.button("๐Ÿ“ค ๋ฉ”์ผ ๋ฐœ์†ก"):
if not recipient_email.strip():
st.warning("์ˆ˜์‹ ์ž ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
else:
with st.spinner("๋ฉ”์ผ์„ ๋ฐœ์†กํ•˜๋Š” ์ค‘..."):
ok = send_email(recipient_email, mail_subject, edited_body)
if ok:
st.success(f"โœ… {recipient_email} ์œผ๋กœ ๋ฉ”์ผ์„ ๋ฐœ์†กํ–ˆ์Šต๋‹ˆ๋‹ค.")
else:
st.error("๋ฉ”์ผ ๋ฐœ์†ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. SENDGRID_API_KEY์™€ EMAIL_ADDRESS๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
###################################################### ๋ฉ€ํ‹ฐํ„ด ๋Œ€ํ™” ์ƒ์„ฑ, ํžˆ์Šคํ† ๋ฆฌ ์ตœ๋Œ€ 10๊ฐœ ์ •๋„๋กœ
with tab5:
st.markdown("#### ๐Ÿ’ฌ GPT 4o mini์™€ ๋Œ€ํ™”ํ•˜๊ธฐ ")
st.info("AI์—๊ฒŒ ๊ถ๊ธˆํ•œ ์ ์„ ๋ฌผ์–ด๋ณด์„ธ์š”. ์ฃผ์˜: ์ตœ๊ทผ 10๊ฐœ์˜ ๋Œ€ํ™” ๊ธฐ์–ต")
# ๋Œ€ํ™” ๊ธฐ๋ก ํ‘œ์‹œ
chat_container = st.container(height=500)
with chat_container:
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# ์ž…๋ ฅ ์˜์—ญ (์Œ์„ฑ ๋ฐ ํ…์ŠคํŠธ)
user_msg = st.chat_input("์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”...")
if user_msg:
# 1. ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ €์žฅ ๋ฐ ํ‘œ์‹œ
st.session_state.messages.append({"role": "user", "content": user_msg})
with chat_container.chat_message("user"):
st.markdown(user_msg)
# 2. AI ์‘๋‹ต ์ƒ์„ฑ
with chat_container.chat_message("assistant"):
with st.spinner("๋‹ต๋ณ€ ์ƒ์„ฑ ์ค‘..."):
# ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— ํšŒ์˜ ์›๋ฌธ ์ฃผ์ž…
full_messages = [
{"role": "system", "content": f"{sys_role_chat}\n\n[ํšŒ์˜ ์›๋ฌธ]\n{st.session_state.transcript}"}
]
full_messages.extend(st.session_state.messages)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=full_messages
)
answer = response.choices[0].message.content
st.markdown(answer)
st.session_state.messages.append({"role": "assistant", "content": answer})
# 3. ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ (์ตœ๊ทผ 10๊ฐœ ๋ฉ”์‹œ์ง€ ์œ ์ง€)
if len(st.session_state.messages) > 10:
st.session_state.messages = st.session_state.messages[-10:]
st.rerun()
if st.button("๐Ÿ—‘๏ธ ๋Œ€ํ™” ๊ธฐ๋ก ์ดˆ๊ธฐํ™”"):
st.session_state.messages = []
st.rerun()