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"", 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("""
KT
Meeting AI 회의 분석 어시스턴트
IT기술혁신팀 DX Consulting · Intelligent Meeting Solution
""", 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()