Spaces:
Sleeping
Sleeping
| """ | |
| n8n 워크플로우 자동 심사 시스템 - Streamlit 웹 앱 | |
| Hugging Face Spaces용 | |
| 기능: | |
| 1. 기본 심사: 기존 심사 기준으로 평가 | |
| 2. 커스텀 심사: 사용자 정의 심사 기준으로 평가 | |
| """ | |
| import os | |
| import streamlit as st | |
| import tempfile | |
| import shutil | |
| import json | |
| from pathlib import Path | |
| from io import BytesIO | |
| import pandas as pd | |
| from main_evaluator import WorkflowEvaluator, CustomWorkflowEvaluator, MultiCriteriaWorkflowEvaluator | |
| from custom_criteria_generator import ( | |
| CustomCriteriaGenerator, | |
| DEFAULT_CRITERIA_TEMPLATES, | |
| FIXED_CRITERIA_TEMPLATES, | |
| get_default_template, | |
| get_fixed_template_with_points, | |
| list_default_templates, | |
| list_fixed_templates | |
| ) | |
| def validate_file_upload(csv_file, zip_file): | |
| """파일 업로드 검증""" | |
| errors = [] | |
| if csv_file is None: | |
| errors.append("❌ CSV 파일을 업로드해주세요.") | |
| elif not csv_file.name.endswith('.csv'): | |
| errors.append("❌ CSV 파일만 업로드 가능합니다.") | |
| if zip_file is None: | |
| errors.append("❌ ZIP 파일을 업로드해주세요.") | |
| elif not zip_file.name.endswith('.zip'): | |
| errors.append("❌ ZIP 파일만 업로드 가능합니다.") | |
| return errors | |
| def render_default_evaluation_page(): | |
| """기본 심사 페이지 렌더링""" | |
| st.header("📊 기본 심사") | |
| st.markdown("기존 심사 기준(총 90점)으로 워크플로우를 평가합니다.") | |
| # 설명 | |
| with st.expander("ℹ️ 사용 방법", expanded=False): | |
| st.markdown(""" | |
| ### 📝 평가 항목 | |
| 1. **기술적 완성도** (15점): 워크플로우의 구조, 데이터 처리, 예외 처리 | |
| 2. **업스테이지 제품 활용도** (15점): API 사용 적합성, 프롬프트 설계, 기능 조합 | |
| 3. **실용성** (30점): 업무 적용 가능성, 재사용성·확장성, 사용자 편의성 | |
| 4. **문제 해결 접근법** (30점): 문제 정의의 독창성, 솔루션 참신성 | |
| **총점: 90점** | |
| ### 📂 파일 요구사항 | |
| - **CSV 파일**: 제출된 프로젝트 정보가 포함된 CSV 파일 (.csv) | |
| - **ZIP 파일**: 워크플로우 JSON 파일들이 포함된 압축 파일 (.zip) | |
| ### ⚠️ 주의사항 | |
| - 각 팀은 정확히 1개의 JSON 워크플로우 파일만 제출해야 합니다. | |
| """) | |
| st.markdown("") | |
| # 파일 업로드 | |
| st.subheader("📁 파일 업로드") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| csv_file = st.file_uploader( | |
| "CSV 파일 업로드", | |
| type=['csv'], | |
| help="프로젝트 제출 정보가 담긴 CSV 파일", | |
| key="default_csv" | |
| ) | |
| with col2: | |
| zip_file = st.file_uploader( | |
| "ZIP 파일 업로드", | |
| type=['zip'], | |
| help="워크플로우 JSON 파일들이 포함된 ZIP 파일", | |
| key="default_zip" | |
| ) | |
| # 파일 검증 | |
| if csv_file or zip_file: | |
| errors = validate_file_upload(csv_file, zip_file) | |
| if errors: | |
| for error in errors: | |
| st.error(error) | |
| return | |
| # 평가 실행 버튼 | |
| if st.button("🚀 평가 시작", type="primary", disabled=(csv_file is None or zip_file is None), key="default_eval"): | |
| run_default_evaluation(csv_file, zip_file) | |
| def run_default_evaluation(csv_file, zip_file): | |
| """기본 심사 실행""" | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| try: | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| # 파일 저장 | |
| status_text.text("📥 파일 저장 중...") | |
| progress_bar.progress(10) | |
| csv_path = os.path.join(temp_dir, csv_file.name) | |
| zip_path = os.path.join(temp_dir, zip_file.name) | |
| output_dir = os.path.join(temp_dir, "results") | |
| with open(csv_path, "wb") as f: | |
| f.write(csv_file.getbuffer()) | |
| with open(zip_path, "wb") as f: | |
| f.write(zip_file.getbuffer()) | |
| progress_bar.progress(20) | |
| # 평가 시스템 초기화 | |
| status_text.text("🔧 평가 시스템 초기화 중...") | |
| evaluator = WorkflowEvaluator(csv_path, zip_path, output_dir) | |
| progress_bar.progress(30) | |
| # 평가 실행 | |
| status_text.text("🔍 워크플로우 평가 진행 중... (시간이 걸릴 수 있습니다)") | |
| with st.spinner("평가 진행 중..."): | |
| df_result = evaluator.evaluate_all() | |
| progress_bar.progress(80) | |
| status_text.text("💾 결과 저장 중...") | |
| output_path = evaluator.save_results() | |
| progress_bar.progress(90) | |
| progress_bar.progress(100) | |
| status_text.text("✅ 평가 완료!") | |
| st.success("🎉 평가가 완료되었습니다!") | |
| # 결과 다운로드 | |
| display_results(output_path) | |
| except Exception as e: | |
| st.error(f"❌ 오류 발생: {str(e)}") | |
| import traceback | |
| with st.expander("🔍 상세 오류 정보"): | |
| st.code(traceback.format_exc()) | |
| def render_custom_criteria_page(): | |
| """커스텀 심사 기준 설정 페이지""" | |
| st.header("⚙️ 커스텀 심사 기준 설정") | |
| # 안내 문구 | |
| st.info(""" | |
| ℹ️ **커스텀 심사 구조** | |
| 모든 심사 기준의 배점을 자유롭게 설정할 수 있습니다! | |
| 1. **기술적 완성도** (템플릿 고정, 배점 자유) | |
| 2. **업스테이지 활용도** (템플릿 고정, 배점 자유) | |
| 3. **추가 심사 기준** (실용성/문제해결/직접입력, 여러 개 추가 가능) | |
| **총점 제한 없음 - 자유롭게 설정하세요!** | |
| """) | |
| # 탭 구성 | |
| tab1, tab2, tab3 = st.tabs(["📝 심사 기준 & 배점 설정", "🤖 프롬프트 생성", "🚀 커스텀 심사 실행"]) | |
| with tab1: | |
| render_multi_criteria_editor() | |
| with tab2: | |
| render_multi_prompt_generator() | |
| with tab3: | |
| render_multi_custom_evaluation() | |
| def render_multi_criteria_editor(): | |
| """여러 심사 기준 설정""" | |
| # 세션 상태 초기화 | |
| if 'custom_criteria_list' not in st.session_state: | |
| st.session_state['custom_criteria_list'] = [] | |
| if 'technical_points' not in st.session_state: | |
| st.session_state['technical_points'] = 15 | |
| if 'upstage_points' not in st.session_state: | |
| st.session_state['upstage_points'] = 15 | |
| # 1. 고정 템플릿 배점 설정 | |
| st.subheader("📌 고정 템플릿 배점 설정") | |
| st.markdown("**템플릿 내용은 고정되어 있으며, 총 배점만 조정할 수 있습니다.**") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.session_state['technical_points'] = st.number_input( | |
| "🔧 기술적 완성도 배점", | |
| min_value=1, | |
| max_value=30, | |
| value=min(st.session_state['technical_points'], 30), | |
| help="워크플로우의 구조, 데이터 처리, 예외 처리 평가" | |
| ) | |
| with st.expander("기술적 완성도 템플릿 미리보기"): | |
| st.markdown(""" | |
| - 구조 완결성 & 모듈화 (40%) | |
| - 데이터·스키마 일관성 (40%) | |
| - 분기/예외 처리 (20%) | |
| """) | |
| with col2: | |
| st.session_state['upstage_points'] = st.number_input( | |
| "⭐ 업스테이지 활용도 배점", | |
| min_value=1, | |
| max_value=30, | |
| value=min(st.session_state['upstage_points'], 30), | |
| help="업스테이지 API 사용, 프롬프트 설계, 기능 조합 평가" | |
| ) | |
| with st.expander("업스테이지 활용도 템플릿 미리보기"): | |
| st.markdown(""" | |
| - API 사용 적합성 (33%) | |
| - Prompt/System 설계 (33%) | |
| - 기능 조합·오케스트레이션 (34%) | |
| """) | |
| fixed_total = st.session_state['technical_points'] + st.session_state['upstage_points'] | |
| st.markdown("---") | |
| # 2. 추가 심사 기준 설정 | |
| st.subheader("➕ 추가 심사 기준 설정") | |
| st.markdown("실용성, 문제해결 등 추가 평가 기준을 설정할 수 있습니다.") | |
| # 추가 기준 추가 버튼 | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| template_to_add = st.selectbox( | |
| "템플릿 선택", | |
| ["직접 입력"] + list_default_templates() | |
| ) | |
| with col2: | |
| if st.button("➕ 기준 추가"): | |
| if template_to_add == "직접 입력": | |
| new_criteria = { | |
| "name": f"커스텀 기준 {len(st.session_state['custom_criteria_list']) + 1}", | |
| "description": "", | |
| "total_points": 30, | |
| "evaluation_target": "project_description", | |
| "sub_criteria": [] | |
| } | |
| else: | |
| new_criteria = get_default_template(template_to_add).copy() | |
| st.session_state['custom_criteria_list'].append(new_criteria) | |
| st.rerun() | |
| # 추가된 기준 표시 및 편집 | |
| custom_criteria_list = st.session_state['custom_criteria_list'] | |
| if not custom_criteria_list: | |
| st.info("➕ 위의 버튼을 눌러 추가 심사 기준을 추가하세요.") | |
| else: | |
| for idx, criteria in enumerate(custom_criteria_list): | |
| with st.expander(f"📋 {criteria.get('name', f'기준 {idx+1}')} ({criteria.get('total_points', 0)}점)", expanded=False): | |
| render_single_criteria_editor(idx, criteria) | |
| # 총점 계산 | |
| st.markdown("---") | |
| additional_total = sum(c.get('total_points', 0) for c in custom_criteria_list) | |
| total_points = fixed_total + additional_total | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("고정 템플릿", f"{fixed_total}점") | |
| with col2: | |
| st.metric("추가 기준", f"{additional_total}점") | |
| with col3: | |
| st.metric("총점", f"{total_points}점") | |
| def render_single_criteria_editor(idx: int, criteria: dict): | |
| """단일 심사 기준 편집기 (최적화 버전)""" | |
| col1, col2, col3 = st.columns([2, 1, 1]) | |
| with col1: | |
| new_name = st.text_input( | |
| "기준 이름", | |
| value=criteria.get('name', ''), | |
| key=f"crit_name_{idx}", | |
| placeholder="예: 실용성" | |
| ) | |
| if new_name != criteria.get('name', ''): | |
| criteria['name'] = new_name | |
| with col2: | |
| points_value = criteria.get('total_points', 30) | |
| if points_value < 1: | |
| points_value = 30 | |
| new_points = st.number_input( | |
| "총점", | |
| min_value=1, | |
| max_value=100, | |
| value=min(points_value, 100), | |
| key=f"crit_points_{idx}" | |
| ) | |
| if new_points != criteria.get('total_points', 30): | |
| criteria['total_points'] = new_points | |
| with col3: | |
| if st.button("🗑️ 삭제", key=f"del_crit_{idx}"): | |
| st.session_state['custom_criteria_list'].pop(idx) | |
| st.rerun() | |
| new_desc = st.text_area( | |
| "설명", | |
| value=criteria.get('description', ''), | |
| key=f"crit_desc_{idx}", | |
| height=60 | |
| ) | |
| if new_desc != criteria.get('description', ''): | |
| criteria['description'] = new_desc | |
| new_target = st.radio( | |
| "평가 대상", | |
| ["project_description", "workflow"], | |
| format_func=lambda x: "프로젝트 설명서" if x == "project_description" else "워크플로우 JSON", | |
| horizontal=True, | |
| index=0 if criteria.get('evaluation_target', 'project_description') == 'project_description' else 1, | |
| key=f"crit_target_{idx}" | |
| ) | |
| if new_target != criteria.get('evaluation_target', 'project_description'): | |
| criteria['evaluation_target'] = new_target | |
| # 세부 기준 - 간소화된 버전 | |
| sub_criteria = criteria.get('sub_criteria', []) | |
| col_a, col_b = st.columns([4, 1]) | |
| with col_a: | |
| st.markdown(f"**세부 기준:** ({len(sub_criteria)}개)") | |
| with col_b: | |
| if st.button(f"➕ 추가", key=f"add_sub_{idx}"): | |
| sub_criteria.append({ | |
| "name": f"세부 기준 {len(sub_criteria) + 1}", | |
| "description": "", | |
| "points": 10, | |
| "details": [] | |
| }) | |
| criteria['sub_criteria'] = sub_criteria | |
| st.rerun() | |
| # 세부 기준은 간단히 표시 | |
| if sub_criteria: | |
| for sub_idx, sub in enumerate(sub_criteria): | |
| col1, col2, col3 = st.columns([3, 1, 1]) | |
| with col1: | |
| new_sub_name = st.text_input( | |
| "이름", | |
| value=sub.get('name', ''), | |
| key=f"sub_name_{idx}_{sub_idx}", | |
| label_visibility="collapsed" | |
| ) | |
| if new_sub_name != sub.get('name', ''): | |
| sub['name'] = new_sub_name | |
| with col2: | |
| sub_points_value = max(sub.get('points', 10), 1) | |
| new_sub_points = st.number_input( | |
| "점수", | |
| min_value=1, | |
| max_value=100, | |
| value=sub_points_value, | |
| key=f"sub_points_{idx}_{sub_idx}", | |
| label_visibility="collapsed" | |
| ) | |
| if new_sub_points != sub.get('points', 10): | |
| sub['points'] = new_sub_points | |
| with col3: | |
| if st.button("❌", key=f"del_sub_{idx}_{sub_idx}"): | |
| sub_criteria.pop(sub_idx) | |
| criteria['sub_criteria'] = sub_criteria | |
| st.rerun() | |
| def render_criteria_editor(): | |
| """심사 기준 편집기 (기존 - 사용 안 함)""" | |
| st.subheader("심사 기준 편집") | |
| # 템플릿 선택 | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| template_option = st.selectbox( | |
| "템플릿 선택", | |
| ["직접 입력"] + list_default_templates(), | |
| help="기본 템플릿을 선택하거나 직접 입력할 수 있습니다." | |
| ) | |
| with col2: | |
| if template_option != "직접 입력": | |
| if st.button("템플릿 불러오기"): | |
| template = get_default_template(template_option) | |
| if template: | |
| st.session_state['custom_criteria'] = template | |
| st.success(f"'{template_option}' 템플릿을 불러왔습니다!") | |
| st.rerun() | |
| st.markdown("---") | |
| # 기존 기준이 있으면 불러오기 | |
| if 'custom_criteria' not in st.session_state: | |
| st.session_state['custom_criteria'] = { | |
| "name": "", | |
| "description": "", | |
| "total_points": 30, | |
| "evaluation_target": "project_description", | |
| "sub_criteria": [] | |
| } | |
| criteria = st.session_state['custom_criteria'] | |
| # total_points가 0 이하이면 기본값으로 설정 | |
| if criteria.get('total_points', 0) < 1: | |
| criteria['total_points'] = 30 | |
| # total_points가 60 초과이면 60으로 제한 | |
| if criteria.get('total_points', 0) > 60: | |
| criteria['total_points'] = 60 | |
| # 기본 정보 입력 | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| criteria['name'] = st.text_input( | |
| "심사 기준 이름", | |
| value=criteria.get('name', ''), | |
| placeholder="예: 기술적 완성도" | |
| ) | |
| with col2: | |
| criteria['total_points'] = st.number_input( | |
| "총점 (커스텀 평가 배점, 고정 30점 제외)", | |
| min_value=1, | |
| max_value=60, | |
| value=min(max(criteria.get('total_points', 30), 1), 60), | |
| help="기술적 완성도(15점) + 업스테이지 활용도(15점) = 30점은 자동 평가됩니다." | |
| ) | |
| criteria['description'] = st.text_area( | |
| "심사 기준 설명", | |
| value=criteria.get('description', ''), | |
| placeholder="이 심사 기준이 무엇을 평가하는지 설명해주세요...", | |
| height=100 | |
| ) | |
| criteria['evaluation_target'] = st.radio( | |
| "평가 대상", | |
| ["project_description", "workflow"], | |
| format_func=lambda x: "프로젝트 설명서" if x == "project_description" else "워크플로우 JSON", | |
| horizontal=True, | |
| index=0 if criteria.get('evaluation_target', 'project_description') == 'project_description' else 1, | |
| help="일반적으로 실용성/문제해결은 프로젝트 설명서, 기술적 평가는 워크플로우 JSON을 평가합니다." | |
| ) | |
| st.markdown("---") | |
| st.subheader("세부 기준 설정") | |
| # 세부 기준 편집 | |
| sub_criteria = criteria.get('sub_criteria', []) | |
| # 세부 기준 추가 버튼 | |
| if st.button("➕ 세부 기준 추가"): | |
| sub_criteria.append({ | |
| "name": f"세부 기준 {len(sub_criteria) + 1}", | |
| "description": "", | |
| "points": 10, | |
| "details": [] | |
| }) | |
| criteria['sub_criteria'] = sub_criteria | |
| st.rerun() | |
| # 각 세부 기준 편집 | |
| for idx, sub in enumerate(sub_criteria): | |
| with st.expander(f"📌 {sub.get('name', f'세부 기준 {idx+1}')} ({sub.get('points', 0)}점)", expanded=True): | |
| col1, col2, col3 = st.columns([3, 1, 1]) | |
| with col1: | |
| sub['name'] = st.text_input( | |
| "기준 이름", | |
| value=sub.get('name', ''), | |
| key=f"sub_name_{idx}" | |
| ) | |
| with col2: | |
| # points가 0 이하이면 기본값으로 설정 | |
| sub_points_value = sub.get('points', 10) | |
| if sub_points_value < 1: | |
| sub_points_value = 10 | |
| sub['points'] = 10 | |
| sub['points'] = st.number_input( | |
| "배점", | |
| min_value=1, | |
| max_value=50, | |
| value=sub_points_value, | |
| key=f"sub_points_{idx}" | |
| ) | |
| with col3: | |
| if st.button("🗑️ 삭제", key=f"del_sub_{idx}"): | |
| sub_criteria.pop(idx) | |
| criteria['sub_criteria'] = sub_criteria | |
| st.rerun() | |
| sub['description'] = st.text_area( | |
| "설명", | |
| value=sub.get('description', ''), | |
| key=f"sub_desc_{idx}", | |
| height=80 | |
| ) | |
| # 세부 항목 (details) | |
| st.markdown("**세부 평가 항목:**") | |
| details = sub.get('details', []) | |
| if st.button(f"➕ 세부 항목 추가", key=f"add_detail_{idx}"): | |
| details.append({ | |
| "name": f"항목 {len(details) + 1}", | |
| "points": 3, | |
| "description": "" | |
| }) | |
| sub['details'] = details | |
| st.rerun() | |
| for d_idx, detail in enumerate(details): | |
| col1, col2, col3, col4 = st.columns([2, 1, 3, 1]) | |
| with col1: | |
| detail['name'] = st.text_input( | |
| "항목명", | |
| value=detail.get('name', ''), | |
| key=f"detail_name_{idx}_{d_idx}", | |
| label_visibility="collapsed" | |
| ) | |
| with col2: | |
| # detail points가 0 이하이면 기본값으로 설정 | |
| detail_points_value = detail.get('points', 3) | |
| if detail_points_value < 1: | |
| detail_points_value = 3 | |
| detail['points'] = 3 | |
| detail['points'] = st.number_input( | |
| "점수", | |
| min_value=1, | |
| max_value=20, | |
| value=detail_points_value, | |
| key=f"detail_points_{idx}_{d_idx}", | |
| label_visibility="collapsed" | |
| ) | |
| with col3: | |
| detail['description'] = st.text_input( | |
| "설명", | |
| value=detail.get('description', ''), | |
| key=f"detail_desc_{idx}_{d_idx}", | |
| label_visibility="collapsed" | |
| ) | |
| with col4: | |
| if st.button("❌", key=f"del_detail_{idx}_{d_idx}"): | |
| details.pop(d_idx) | |
| sub['details'] = details | |
| st.rerun() | |
| # 저장 | |
| st.markdown("---") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("💾 기준 저장", type="primary"): | |
| st.session_state['custom_criteria'] = criteria | |
| st.success("✅ 심사 기준이 저장되었습니다!") | |
| with col2: | |
| # JSON으로 내보내기 | |
| if criteria.get('name'): | |
| criteria_json = json.dumps(criteria, ensure_ascii=False, indent=2) | |
| st.download_button( | |
| label="📥 JSON으로 내보내기", | |
| data=criteria_json, | |
| file_name=f"criteria_{criteria['name']}.json", | |
| mime="application/json" | |
| ) | |
| # 점수 합계 검증 | |
| total_from_sub = sum(s.get('points', 0) for s in sub_criteria) | |
| if total_from_sub != criteria.get('total_points', 0): | |
| st.warning(f"⚠️ 세부 기준 점수 합계({total_from_sub})가 총점({criteria.get('total_points', 0)})과 일치하지 않습니다.") | |
| def render_multi_prompt_generator(): | |
| """여러 심사 기준에 대한 프롬프트 생성""" | |
| st.subheader("🤖 LLM 평가 프롬프트 생성") | |
| # 필수 데이터 확인 | |
| if 'technical_points' not in st.session_state: | |
| st.warning("⚠️ 먼저 '심사 기준 & 배점 설정' 탭에서 기준을 설정해주세요.") | |
| return | |
| # 생성할 프롬프트 준비 | |
| prompts_to_generate = [] | |
| # 1. 기술적 완성도 (고정 템플릿) | |
| tech_template = get_fixed_template_with_points("기술적_완성도", st.session_state['technical_points']) | |
| prompts_to_generate.append(("기술적 완성도", tech_template)) | |
| # 2. 업스테이지 활용도 (고정 템플릿) | |
| upstage_template = get_fixed_template_with_points("업스테이지_활용도", st.session_state['upstage_points']) | |
| prompts_to_generate.append(("업스테이지 활용도", upstage_template)) | |
| # 3. 추가 기준들 | |
| custom_criteria_list = st.session_state.get('custom_criteria_list', []) | |
| for criteria in custom_criteria_list: | |
| if criteria.get('name'): | |
| prompts_to_generate.append((criteria['name'], criteria)) | |
| if not prompts_to_generate: | |
| st.info("설정된 심사 기준이 없습니다.") | |
| return | |
| st.markdown(f"**총 {len(prompts_to_generate)}개의 심사 기준에 대한 프롬프트를 생성합니다.**") | |
| # 프롬프트 생성 방식 선택 | |
| generation_method = st.radio( | |
| "프롬프트 생성 방식", | |
| ["자동 생성 (규칙 기반)", "LLM 활용 생성 (더 정교함)"], | |
| horizontal=True | |
| ) | |
| if st.button("🔮 모든 프롬프트 생성", type="primary"): | |
| with st.spinner("프롬프트 생성 중..."): | |
| try: | |
| generator = CustomCriteriaGenerator() | |
| generated_prompts = {} | |
| progress_bar = st.progress(0) | |
| for idx, (name, template) in enumerate(prompts_to_generate): | |
| st.text(f"생성 중: {name}") | |
| if generation_method == "자동 생성 (규칙 기반)": | |
| prompt = generator.generate_evaluation_prompt( | |
| criteria_name=template['name'], | |
| criteria_description=template['description'], | |
| sub_criteria=template['sub_criteria'], | |
| total_points=template['total_points'], | |
| evaluation_target=template['evaluation_target'] | |
| ) | |
| else: | |
| prompt = generator.generate_prompt_with_llm( | |
| criteria_name=template['name'], | |
| criteria_description=template['description'], | |
| sub_criteria=template['sub_criteria'], | |
| total_points=template['total_points'], | |
| evaluation_target=template['evaluation_target'] | |
| ) | |
| generated_prompts[name] = { | |
| "prompt": prompt, | |
| "template": template | |
| } | |
| progress_bar.progress((idx + 1) / len(prompts_to_generate)) | |
| st.session_state['generated_prompts'] = generated_prompts | |
| st.success("✅ 모든 프롬프트가 생성되었습니다!") | |
| except Exception as e: | |
| st.error(f"❌ 프롬프트 생성 실패: {str(e)}") | |
| # 생성된 프롬프트 표시 | |
| if 'generated_prompts' in st.session_state: | |
| st.markdown("---") | |
| st.subheader("📄 생성된 프롬프트") | |
| generated_prompts = st.session_state['generated_prompts'] | |
| for name, data in generated_prompts.items(): | |
| with st.expander(f"📋 {name} ({data['template']['total_points']}점)", expanded=False): | |
| edited_prompt = st.text_area( | |
| "프롬프트 (편집 가능)", | |
| value=data['prompt'], | |
| height=300, | |
| key=f"prompt_{name}" | |
| ) | |
| # 수정된 프롬프트 저장 | |
| data['prompt'] = edited_prompt | |
| st.download_button( | |
| label="📥 다운로드", | |
| data=edited_prompt, | |
| file_name=f"prompt_{name}.txt", | |
| mime="text/plain", | |
| key=f"download_{name}" | |
| ) | |
| def render_prompt_generator(): | |
| """프롬프트 생성 페이지 (기존 - 사용 안 함)""" | |
| st.subheader("🤖 LLM 평가 프롬프트 생성") | |
| if 'custom_criteria' not in st.session_state or not st.session_state['custom_criteria'].get('name'): | |
| st.warning("⚠️ 먼저 '기준 설정' 탭에서 심사 기준을 설정해주세요.") | |
| return | |
| criteria = st.session_state['custom_criteria'] | |
| st.markdown(f"**현재 설정된 기준:** {criteria['name']} ({criteria['total_points']}점)") | |
| # 프롬프트 생성 방식 선택 | |
| generation_method = st.radio( | |
| "프롬프트 생성 방식", | |
| ["자동 생성 (규칙 기반)", "LLM 활용 생성 (더 정교함)"], | |
| horizontal=True | |
| ) | |
| additional_context = st.text_area( | |
| "추가 컨텍스트 (선택)", | |
| placeholder="평가 시 특별히 고려해야 할 사항이 있다면 입력하세요...", | |
| height=100 | |
| ) | |
| if st.button("🔮 프롬프트 생성", type="primary"): | |
| with st.spinner("프롬프트 생성 중..."): | |
| try: | |
| generator = CustomCriteriaGenerator() | |
| if generation_method == "자동 생성 (규칙 기반)": | |
| prompt = generator.generate_evaluation_prompt( | |
| criteria_name=criteria['name'], | |
| criteria_description=criteria['description'], | |
| sub_criteria=criteria['sub_criteria'], | |
| total_points=criteria['total_points'], | |
| evaluation_target=criteria['evaluation_target'] | |
| ) | |
| else: | |
| prompt = generator.generate_prompt_with_llm( | |
| criteria_name=criteria['name'], | |
| criteria_description=criteria['description'], | |
| sub_criteria=criteria['sub_criteria'], | |
| total_points=criteria['total_points'], | |
| evaluation_target=criteria['evaluation_target'], | |
| additional_context=additional_context | |
| ) | |
| st.session_state['generated_prompt'] = prompt | |
| st.success("✅ 프롬프트가 생성되었습니다!") | |
| except Exception as e: | |
| st.error(f"❌ 프롬프트 생성 실패: {str(e)}") | |
| # 생성된 프롬프트 표시 | |
| if 'generated_prompt' in st.session_state: | |
| st.markdown("---") | |
| st.subheader("📄 생성된 프롬프트") | |
| # 편집 가능한 텍스트 영역 | |
| edited_prompt = st.text_area( | |
| "프롬프트 (편집 가능)", | |
| value=st.session_state['generated_prompt'], | |
| height=400 | |
| ) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("💾 프롬프트 저장"): | |
| st.session_state['generated_prompt'] = edited_prompt | |
| st.success("✅ 프롬프트가 저장되었습니다!") | |
| with col2: | |
| st.download_button( | |
| label="📥 프롬프트 다운로드", | |
| data=edited_prompt, | |
| file_name=f"prompt_{criteria['name']}.txt", | |
| mime="text/plain" | |
| ) | |
| def render_multi_custom_evaluation(): | |
| """여러 심사 기준으로 평가 실행""" | |
| st.subheader("🚀 커스텀 심사 실행") | |
| # 필수 조건 확인 | |
| if 'generated_prompts' not in st.session_state: | |
| st.warning("⚠️ 먼저 '프롬프트 생성' 탭에서 평가 프롬프트를 생성해주세요.") | |
| return | |
| generated_prompts = st.session_state['generated_prompts'] | |
| if not generated_prompts: | |
| st.warning("⚠️ 생성된 프롬프트가 없습니다.") | |
| return | |
| # 심사 기준 요약 | |
| st.success(f""" | |
| ✅ 심사 준비 완료 | |
| **총 {len(generated_prompts)}개의 심사 기준:** | |
| """) | |
| total_points = 0 | |
| for name, data in generated_prompts.items(): | |
| points = data['template']['total_points'] | |
| total_points += points | |
| st.write(f"- {name}: {points}점") | |
| st.write(f"\n**총 {total_points}점**") | |
| st.markdown("---") | |
| # 파일 업로드 | |
| st.markdown("### 📁 파일 업로드") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| csv_file = st.file_uploader( | |
| "CSV 파일 업로드", | |
| type=['csv'], | |
| help="프로젝트 제출 정보가 담긴 CSV 파일", | |
| key="multi_custom_csv" | |
| ) | |
| with col2: | |
| zip_file = st.file_uploader( | |
| "ZIP 파일 업로드", | |
| type=['zip'], | |
| help="워크플로우 JSON 파일들이 포함된 ZIP 파일", | |
| key="multi_custom_zip" | |
| ) | |
| # 파일 검증 | |
| if csv_file or zip_file: | |
| errors = validate_file_upload(csv_file, zip_file) | |
| if errors: | |
| for error in errors: | |
| st.error(error) | |
| return | |
| # 평가 실행 | |
| if st.button("🚀 커스텀 심사 시작", type="primary", disabled=(csv_file is None or zip_file is None)): | |
| run_multi_custom_evaluation(csv_file, zip_file, generated_prompts) | |
| def render_custom_evaluation(): | |
| """커스텀 기준으로 심사 실행 (기존 - 사용 안 함)""" | |
| st.subheader("🚀 커스텀 심사 실행") | |
| # 필수 조건 확인 | |
| has_criteria = 'custom_criteria' in st.session_state and st.session_state['custom_criteria'].get('name') | |
| has_prompt = 'generated_prompt' in st.session_state | |
| if not has_criteria: | |
| st.warning("⚠️ 먼저 '기준 설정' 탭에서 심사 기준을 설정해주세요.") | |
| return | |
| if not has_prompt: | |
| st.warning("⚠️ 먼저 '프롬프트 생성' 탭에서 평가 프롬프트를 생성해주세요.") | |
| return | |
| criteria = st.session_state['custom_criteria'] | |
| prompt = st.session_state['generated_prompt'] | |
| total_with_fixed = 30 + criteria['total_points'] | |
| st.success(f""" | |
| ✅ 심사 준비 완료 | |
| - 고정 평가: 기술적 완성도(15점) + 업스테이지 활용도(15점) = **30점** | |
| - 커스텀 평가: **{criteria['name']}** ({criteria['total_points']}점) | |
| - **총 {total_with_fixed}점** | |
| """) | |
| st.markdown("---") | |
| # 파일 업로드 | |
| st.markdown("### 📁 파일 업로드") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| csv_file = st.file_uploader( | |
| "CSV 파일 업로드", | |
| type=['csv'], | |
| help="프로젝트 제출 정보가 담긴 CSV 파일", | |
| key="custom_csv" | |
| ) | |
| with col2: | |
| zip_file = st.file_uploader( | |
| "ZIP 파일 업로드", | |
| type=['zip'], | |
| help="워크플로우 JSON 파일들이 포함된 ZIP 파일", | |
| key="custom_zip" | |
| ) | |
| # 파일 검증 | |
| if csv_file or zip_file: | |
| errors = validate_file_upload(csv_file, zip_file) | |
| if errors: | |
| for error in errors: | |
| st.error(error) | |
| return | |
| # 평가 실행 | |
| if st.button("🚀 커스텀 심사 시작", type="primary", disabled=(csv_file is None or zip_file is None)): | |
| run_custom_evaluation(csv_file, zip_file, criteria, prompt) | |
| def run_multi_custom_evaluation(csv_file, zip_file, generated_prompts): | |
| """여러 심사 기준으로 평가 실행""" | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| try: | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| # 파일 저장 | |
| status_text.text("📥 파일 저장 중...") | |
| progress_bar.progress(10) | |
| csv_path = os.path.join(temp_dir, csv_file.name) | |
| zip_path = os.path.join(temp_dir, zip_file.name) | |
| output_dir = os.path.join(temp_dir, "results") | |
| with open(csv_path, "wb") as f: | |
| f.write(csv_file.getbuffer()) | |
| with open(zip_path, "wb") as f: | |
| f.write(zip_file.getbuffer()) | |
| progress_bar.progress(20) | |
| # 평가 기준 리스트 구성 | |
| criteria_list = [] | |
| for name, data in generated_prompts.items(): | |
| criteria_list.append({ | |
| "name": name, | |
| "prompt": data['prompt'], | |
| "total_points": data['template']['total_points'], | |
| "evaluation_target": data['template']['evaluation_target'] | |
| }) | |
| # 멀티 기준 평가 시스템 초기화 | |
| status_text.text("🔧 멀티 기준 평가 시스템 초기화 중...") | |
| evaluator = MultiCriteriaWorkflowEvaluator( | |
| csv_path=csv_path, | |
| zip_path=zip_path, | |
| output_dir=output_dir, | |
| criteria_list=criteria_list | |
| ) | |
| progress_bar.progress(30) | |
| # 평가 실행 | |
| status_text.text("🔍 멀티 기준으로 평가 진행 중... (시간이 걸릴 수 있습니다)") | |
| with st.spinner("멀티 기준 평가 진행 중..."): | |
| df_result = evaluator.evaluate_all() | |
| progress_bar.progress(80) | |
| status_text.text("💾 결과 저장 중...") | |
| output_path = evaluator.save_results() | |
| progress_bar.progress(90) | |
| progress_bar.progress(100) | |
| status_text.text("✅ 멀티 기준 평가 완료!") | |
| st.success("🎉 멀티 기준 평가가 완료되었습니다!") | |
| # 결과 다운로드 | |
| display_results(output_path) | |
| except Exception as e: | |
| st.error(f"❌ 오류 발생: {str(e)}") | |
| import traceback | |
| with st.expander("🔍 상세 오류 정보"): | |
| st.code(traceback.format_exc()) | |
| def run_custom_evaluation(csv_file, zip_file, criteria, prompt): | |
| """커스텀 심사 실행 (기존 - 사용 안 함)""" | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| try: | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| # 파일 저장 | |
| status_text.text("📥 파일 저장 중...") | |
| progress_bar.progress(10) | |
| csv_path = os.path.join(temp_dir, csv_file.name) | |
| zip_path = os.path.join(temp_dir, zip_file.name) | |
| output_dir = os.path.join(temp_dir, "results") | |
| with open(csv_path, "wb") as f: | |
| f.write(csv_file.getbuffer()) | |
| with open(zip_path, "wb") as f: | |
| f.write(zip_file.getbuffer()) | |
| progress_bar.progress(20) | |
| # 커스텀 평가 시스템 초기화 | |
| status_text.text("🔧 커스텀 평가 시스템 초기화 중...") | |
| evaluator = CustomWorkflowEvaluator( | |
| csv_path=csv_path, | |
| zip_path=zip_path, | |
| output_dir=output_dir, | |
| custom_criteria={ | |
| "name": criteria['name'], | |
| "prompt": prompt, | |
| "total_points": criteria['total_points'], | |
| "evaluation_target": criteria['evaluation_target'] | |
| } | |
| ) | |
| progress_bar.progress(30) | |
| # 평가 실행 | |
| status_text.text("🔍 커스텀 기준으로 평가 진행 중... (시간이 걸릴 수 있습니다)") | |
| with st.spinner("커스텀 평가 진행 중..."): | |
| df_result = evaluator.evaluate_all() | |
| progress_bar.progress(80) | |
| status_text.text("💾 결과 저장 중...") | |
| output_path = evaluator.save_results() | |
| progress_bar.progress(90) | |
| progress_bar.progress(100) | |
| status_text.text("✅ 커스텀 평가 완료!") | |
| st.success("🎉 커스텀 평가가 완료되었습니다!") | |
| # 결과 다운로드 | |
| display_results(output_path) | |
| except Exception as e: | |
| st.error(f"❌ 오류 발생: {str(e)}") | |
| import traceback | |
| with st.expander("🔍 상세 오류 정보"): | |
| st.code(traceback.format_exc()) | |
| def display_results(output_path): | |
| """평가 결과 표시 및 다운로드""" | |
| st.markdown("---") | |
| st.header("📊 평가 결과") | |
| # 결과 파일 읽기 | |
| df_result = pd.read_excel(output_path, engine='openpyxl') | |
| # 결과 다운로드 | |
| st.subheader("💾 결과 다운로드") | |
| # XLSX 다운로드 | |
| output = BytesIO() | |
| with pd.ExcelWriter(output, engine='openpyxl') as writer: | |
| df_result.to_excel(writer, index=False, sheet_name='평가결과') | |
| excel_data = output.getvalue() | |
| st.download_button( | |
| label="📥 XLSX 다운로드", | |
| data=excel_data, | |
| file_name="평가_결과_최종.xlsx", | |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | |
| ) | |
| def main(): | |
| st.set_page_config( | |
| page_title="n8n 워크플로우 자동 심사", | |
| page_icon="📊", | |
| layout="wide" | |
| ) | |
| st.title("🤖 n8n 워크플로우 자동 심사 시스템") | |
| st.markdown("---") | |
| # API Key 확인 (환경변수에서 자동 로드) | |
| api_key = os.environ.get("OPENAI_API_KEY") | |
| if not api_key: | |
| st.error("❌ OpenAI API Key가 설정되지 않았습니다. 환경변수를 확인하세요.") | |
| return | |
| st.success("✅ OpenAI API Key 로드 완료") | |
| # 사이드바 네비게이션 | |
| st.sidebar.title("🧭 네비게이션") | |
| page = st.sidebar.radio( | |
| "페이지 선택", | |
| ["📊 기본 심사", "⚙️ 커스텀 심사 기준"], | |
| label_visibility="collapsed" | |
| ) | |
| st.sidebar.markdown("---") | |
| st.sidebar.markdown(""" | |
| ### 📌 사용 가이드 | |
| **기본 심사** | |
| - 기존 90점 만점 기준으로 평가 | |
| - CSV + ZIP 파일 업로드 후 즉시 평가 | |
| **커스텀 심사 기준** | |
| 1. 심사 기준 설정 | |
| 2. LLM 프롬프트 자동 생성 | |
| 3. 커스텀 기준으로 평가 | |
| """) | |
| # 페이지 렌더링 | |
| if page == "📊 기본 심사": | |
| render_default_evaluation_page() | |
| else: | |
| render_custom_criteria_page() | |
| # 푸터 | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style='text-align: center; color: gray;'> | |
| <p>n8n 워크플로우 자동 심사 시스템 | Powered by GPT-4o & Streamlit</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if __name__ == "__main__": | |
| main() | |