| import os, json, re, io, requests |
| import streamlit as st |
| from openai import OpenAI |
| from PIL import Image |
| from datetime import datetime |
|
|
| |
| |
| |
| 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: |
| st.session_state.messages = [] |
| |
| if "mail_body" not in st.session_state: |
| st.session_state.mail_body = "" |
|
|
| |
| PRIORITY_LABEL = {"high": "๐ด ๋์", "medium": "๐ก ๋ณดํต", "low": "๐ข ๋ฎ์"} |
|
|
| |
| |
| |
|
|
| 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) |
|
|
|
|
| |
| |
| 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() |
|
|
|
|
| |
| 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 |
|
|
|
|
|
|
| |
| |
| |
|
|
| |
| 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}") |
|
|
| |
| |
| |
| 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๋ฅผ ํ์ธํด์ฃผ์ธ์.") |
| |
| 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: |
| |
| st.session_state.messages.append({"role": "user", "content": user_msg}) |
| with chat_container.chat_message("user"): |
| st.markdown(user_msg) |
|
|
| |
| 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}) |
|
|
| |
| 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() |
|
|