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()