Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import google.generativeai as genai | |
| from pypdf import PdfReader | |
| import pandas as pd | |
| import requests | |
| from io import BytesIO | |
| import os | |
| import time | |
| import re | |
| from html import unescape | |
| import json | |
| from docx import Document | |
| from datetime import datetime | |
| from analyze import dashboard | |
| from bs4 import BeautifulSoup | |
| from config import new_schema | |
| # Function definitions | |
| def is_valid_url(url): | |
| pattern = r'https://hiring\.base\.vn/opening/candidates/(\d+)\?stage=(\d+)$' | |
| return re.match(pattern, url) is not None | |
| def load_job_descriptions(csv_file): | |
| df = pd.read_csv(csv_file) | |
| return dict(zip(df['Position'], df['Job_Description'])) | |
| def get_pdf_text_from_url(url): | |
| try: | |
| response = requests.get(url) | |
| response.raise_for_status() | |
| with BytesIO(response.content) as f: | |
| reader = PdfReader(f) | |
| text = "" | |
| for page in reader.pages: | |
| text += page.extract_text() + " " | |
| return ' '.join(text.split()) # Xóa khoảng trắng thừa nếu có | |
| except Exception as e: | |
| st.error(f"Lỗi khi tải hoặc trích xuất văn bản từ URL {url}: {str(e)}") | |
| return None | |
| def get_docx_text_from_url(url): | |
| try: | |
| response = requests.get(url) | |
| response.raise_for_status() | |
| with BytesIO(response.content) as f: | |
| document = Document(f) | |
| text = "" | |
| for para in document.paragraphs: | |
| text += para.text + "\n" | |
| return ' '.join(text.split()) # Xóa khoảng trắng thừa nếu có | |
| except Exception as e: | |
| print(f"Lỗi khi tải hoặc trích xuất văn bản từ URL {url}: {str(e)}") | |
| return None | |
| # Hàm kiểm tra định dạng file và chọn hàm tương ứng | |
| def get_cv_text_from_url(cv_url): | |
| if not isinstance(cv_url, str): | |
| print(f"Invalid URL format: {cv_url}. Expected string, got {type(cv_url)}.") | |
| return None | |
| cv_url = cv_url.strip() # Remove any leading/trailing whitespace | |
| if not cv_url: # Check if the URL is empty after stripping | |
| print("Empty URL provided.") | |
| return None | |
| if cv_url.lower().endswith('.pdf'): | |
| return get_pdf_text_from_url(cv_url) | |
| elif cv_url.lower().endswith('.docx'): | |
| return get_docx_text_from_url(cv_url) | |
| else: | |
| print(f"Unsupported file format for URL: {cv_url}") | |
| return None | |
| def get_gemini_response(prompt, content): | |
| model = genai.GenerativeModel('models/gemini-1.5-flash-latest', | |
| generation_config={ | |
| "response_mime_type": "application/json", | |
| "response_schema": new_schema | |
| }) | |
| response = model.generate_content(prompt + content) | |
| response_json = json.loads(response.text) | |
| time.sleep(3) | |
| return response_json | |
| def extract_ids_from_url(url): | |
| match = re.search(r'candidates/(\d+)\?stage=(\d+)', url) | |
| if match: | |
| return match.group(1), match.group(2) | |
| return None, None | |
| # Updated function to fetch candidates using stage_ids | |
| def fetch_candidates_by_stages(opening_id, access_token): | |
| """ | |
| Fetch candidates for specific stages of a job opening | |
| Parameters: | |
| opening_id (str): The ID of the opening | |
| access_token (str): The API access token | |
| stage_ids (list): List of stage IDs to filter candidates | |
| Returns: | |
| dict: API response with candidate data | |
| """ | |
| api_url = "https://hiring.base.vn/publicapi/v2/candidate/list" | |
| # Prepare the payload | |
| payload = { | |
| 'access_token': access_token, | |
| 'opening_id': opening_id, | |
| 'num_per_page': '10000', | |
| } | |
| headers = {'Content-Type': 'application/x-www-form-urlencoded'} | |
| response = requests.post(api_url, headers=headers, data=payload) | |
| return response.json() | |
| def fetch_jd(opening_id, access_token): | |
| api_url = "https://hiring.base.vn/publicapi/v2/opening/get" | |
| payload = { | |
| 'access_token': access_token, | |
| 'id': opening_id, | |
| } | |
| headers = {'Content-Type': 'application/x-www-form-urlencoded'} | |
| response = requests.post(api_url, headers=headers, data=payload) | |
| # Parse the JSON response | |
| json_response = response.json() | |
| # Get the 'content' field | |
| html_content = json_response.get('opening', {}).get('content', '') | |
| # Use BeautifulSoup to convert HTML content to plain text | |
| soup = BeautifulSoup(html_content, "html.parser") | |
| plain_text = soup.get_text() | |
| return plain_text | |
| # New function to get active stages for an opening | |
| def get_opening_stages(opening_id, access_token): | |
| """Get active stages for a specific job opening""" | |
| api_url = "https://hiring.base.vn/publicapi/v2/opening/get" | |
| payload = { | |
| 'access_token': access_token, | |
| 'id': opening_id, | |
| } | |
| headers = {'Content-Type': 'application/x-www-form-urlencoded'} | |
| response = requests.post(api_url, headers=headers, data=payload) | |
| if response.status_code == 200: | |
| data = response.json() | |
| active_stages = [ | |
| {"id": stage["id"], "name": stage["name"]} | |
| for stage in data.get("opening", {}).get("stats", {}).get("stages", []) | |
| if stage.get("state") == "active" | |
| ] | |
| return pd.DataFrame(active_stages) | |
| else: | |
| st.error(f"Lỗi khi lấy danh sách giai đoạn: {response.status_code} - {response.text}") | |
| return pd.DataFrame() | |
| def extract_message(evaluations): | |
| """Extract text content from HTML evaluations""" | |
| if isinstance(evaluations, list) and len(evaluations) > 0: | |
| raw_html = evaluations[0].get('content', '') | |
| soup = BeautifulSoup(raw_html, "html.parser") | |
| return " ".join(soup.stripped_strings) | |
| return None | |
| def process_data(data): | |
| if 'candidates' not in data: | |
| st.error("Không tìm thấy ứng viên trong phản hồi.") | |
| return None | |
| df = pd.DataFrame(data['candidates']) | |
| # Convert cvs to first element if it exists | |
| if 'cvs' in df.columns: | |
| df['cvs'] = df['cvs'].apply(lambda x: x[0] if isinstance(x, list) and len(x) > 0 else None) | |
| # Clean title if it exists | |
| if 'title' in df.columns: | |
| df['title'] = df['title'].apply(lambda x: re.sub(r'<.*?>', '', x) if isinstance(x, str) else x) | |
| # Unescape name if it exists | |
| if 'name' in df.columns: | |
| df['name'] = df['name'].apply(lambda x: unescape(x) if isinstance(x, str) else x) | |
| # Drop DOB columns if they exist | |
| dob_columns = ['dob_day', 'dob_month', 'dob_year'] | |
| df_columns = df.columns.tolist() | |
| for col in dob_columns: | |
| if col in df_columns: | |
| df = df.drop(columns=[col]) | |
| # Extract review from evaluations if it exists | |
| if 'evaluations' in df.columns: | |
| df['review'] = df['evaluations'].apply(extract_message) | |
| # Filter out rows without CVs if cvs column exists | |
| if 'cvs' in df.columns: | |
| df = df[df['cvs'].notnull()] | |
| # Drop columns with all null values | |
| df = df.dropna(axis=1, how='all') | |
| # List of columns to select (if they exist in the DataFrame) | |
| columns_to_select = ['id', 'name', 'gender', 'cvs', 'email', 'phone', 'form', 'stage_id'] | |
| if 'review' in df.columns: | |
| columns_to_select.append('review') | |
| # Select only columns that exist in the DataFrame | |
| available_columns = [col for col in columns_to_select if col in df.columns] | |
| df = df[available_columns] | |
| # Process form data if it exists | |
| if 'form' in df.columns: | |
| form_data_list = df['form'] | |
| # Convert each row in 'form' column to a dictionary | |
| form_df_list = [] | |
| for form_data in form_data_list: | |
| if isinstance(form_data, list): | |
| data_dict = {item['id']: item['value'] for item in form_data} | |
| form_df_list.append(data_dict) | |
| else: | |
| form_df_list.append({}) | |
| # Create new DataFrame from list of dictionaries | |
| form_df_transformed = pd.DataFrame(form_df_list) | |
| # Merge the original DataFrame (without 'form' column) with the transformed form data | |
| df_merged = pd.concat([df.drop(columns=['form']), form_df_transformed], axis=1) | |
| selected_df = df_merged | |
| else: | |
| selected_df = df | |
| df_cleaned = selected_df.dropna(axis=1, how='all') # Xóa cột chứa toàn bộ giá trị None | |
| # Nếu muốn xóa cả cột chứa toàn bộ chuỗi rỗng | |
| df_cleaned = df_cleaned.loc[:, ~(df_cleaned == '').all()] | |
| return df_cleaned | |
| # New function to get active job openings | |
| def get_base_openings(access_token): | |
| """Retrieve active job openings from Base API""" | |
| url = "https://hiring.base.vn/publicapi/v2/opening/list" | |
| payload = {'access_token': access_token} | |
| response = requests.post(url, data=payload) | |
| if response.status_code == 200: | |
| data = response.json() | |
| openings = data.get('openings', []) | |
| filtered_openings = [ | |
| {"id": opening['id'], "name": opening['name']} | |
| for opening in openings if opening.get('status') == '10' | |
| ] | |
| return pd.DataFrame(filtered_openings) | |
| else: | |
| st.error(f"Lỗi khi lấy danh sách vị trí tuyển dụng: {response.status_code} - {response.text}") | |
| return pd.DataFrame() | |
| # Function to convert empty strings to None | |
| def convert_empty_to_none(df): | |
| """Convert empty strings to None in a pandas DataFrame""" | |
| # Replace empty strings with None in the entire DataFrame | |
| return df.replace('', None) | |
| # Main application | |
| st.set_page_config(page_title="Công Cụ Đánh Giá CV và Lấy Dữ Liệu Công Việc", layout="wide") | |
| st.title("🚀 Công Cụ Đánh Giá CV và Lấy Dữ Liệu Công Việc") | |
| # Configure Google API | |
| api_key = os.getenv('GOOGLE_API_KEY') | |
| if api_key: | |
| genai.configure(api_key=api_key) | |
| else: | |
| st.error("Không tìm thấy GOOGLE_API_KEY trong biến môi trường. Vui lòng cấu hình trước khi sử dụng ứng dụng.") | |
| st.stop() | |
| # Get BASE API Key | |
| access_token = os.getenv('BASE_API_KEY') | |
| if not access_token: | |
| st.error("Không tìm thấy BASE_API_KEY trong biến môi trường. Vui lòng cấu hình trước khi sử dụng ứng dụng.") | |
| st.stop() | |
| st.sidebar.header("📚 Hướng dẫn sử dụng") | |
| st.sidebar.markdown(""" | |
| ### 👋 Chào mừng bạn đến với Công cụ Đánh giá CV! | |
| Công cụ này giúp bạn: | |
| 1. **🔍 Lấy thông tin ứng viên tự động từ Base.vn** | |
| 2. **📊 Đánh giá CV theo tiêu chí công việc** | |
| 3. **📈 Phân tích dữ liệu tuyển dụng qua Dashboard** | |
| """) | |
| with st.sidebar.expander("🔍 Cách lấy thông tin và đánh giá CV", expanded=False): | |
| st.markdown(""" | |
| ### Bước 1: Chọn nguồn dữ liệu | |
| - Chuyển đến tab "Lấy Dữ Liệu Ứng Viên và Đánh giá CV" | |
| - Chọn vị trí tuyển dụng từ danh sách dropdown | |
| ### Bước 2: Chọn giai đoạn tuyển dụng | |
| - Chọn một hoặc nhiều giai đoạn tuyển dụng để lọc ứng viên | |
| ### Bước 3: Tải tài liệu bổ sung (tùy chọn) | |
| - Nếu có, tải lên file PDF/DOCX chứa thông tin bổ sung | |
| ### Bước 4: Xử lý dữ liệu | |
| - Nhấp "Lấy Thông Tin Ứng Viên" để bắt đầu | |
| - Hệ thống sẽ tự động đánh giá CV dựa trên JD | |
| ### Bước 5: Xem và lưu kết quả | |
| - Kết quả đánh giá hiển thị ở dạng bảng | |
| - Tải xuống file CSV để lưu trữ và phân tích | |
| """) | |
| with st.sidebar.expander("📈 Cách sử dụng Dashboard phân tích", expanded=False): | |
| st.markdown(""" | |
| ### Bước 1: Mở tab Dashboard | |
| - Chuyển đến tab "Dashboard" | |
| ### Bước 2: Tải lên dữ liệu | |
| - Tải lên file CSV đã lưu từ chức năng đánh giá CV | |
| ### Bước 3: Xem báo cáo và biểu đồ | |
| - Xem thống kê tổng quan về ứng viên | |
| - Phân tích điểm số theo từng tiêu chí | |
| - So sánh hiệu quả giữa các nguồn tuyển dụng | |
| """) | |
| st.sidebar.warning(""" | |
| **⚠️ Lưu ý quan trọng:** | |
| - Đảm bảo bạn đã đăng nhập vào hệ thống Base.vn | |
| - Công cụ này hỗ trợ quyết định, không thay thế đánh giá chuyên môn | |
| - Kết quả đánh giá dựa trên AI có thể cần kiểm tra lại | |
| - Bảo mật thông tin ứng viên theo quy định PDPA | |
| """) | |
| st.sidebar.info(""" | |
| **💡 Mẹo sử dụng:** | |
| - Tải lên tài liệu bổ sung để có đánh giá chính xác hơn | |
| - Chọn các giai đoạn tuyển dụng phù hợp để lọc ứng viên | |
| - Tải xuống CSV sau mỗi lần đánh giá để lưu trữ | |
| """) | |
| st.sidebar.success("✨ Chúc bạn tuyển dụng hiệu quả!") | |
| # Tabs for different functionalities | |
| tab1, tab2 = st.tabs(["🔍 Lấy Dữ Liệu Ứng Viên và 📊 Đánh giá CV", "📈 Dashboard"]) | |
| with tab1: | |
| st.header("🔍 Lấy Dữ Liệu Ứng Viên") | |
| # Add file upload feature | |
| st.subheader("📁 Tải lên tài liệu bổ sung (tùy chọn)") | |
| uploaded_file = st.file_uploader("Tải lên file PDF hoặc DOCX để cung cấp thông tin bổ sung", type=["pdf", "docx"]) | |
| additional_info = "" | |
| if uploaded_file is not None: | |
| with st.spinner("⏳ Đang xử lý file tải lên..."): | |
| # Create BytesIO object from uploaded file | |
| file_bytes = BytesIO(uploaded_file.getvalue()) | |
| # Process the file based on type | |
| if uploaded_file.name.lower().endswith('.pdf'): | |
| try: | |
| reader = PdfReader(file_bytes) | |
| text = "" | |
| for page in reader.pages: | |
| text += page.extract_text() + " " | |
| additional_info = ' '.join(text.split()) # Remove extra whitespace | |
| st.success(f"✅ Đã xử lý file PDF: {uploaded_file.name}") | |
| st.text_area("Nội dung file PDF: ",text, disabled=True) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi xử lý file PDF: {str(e)}") | |
| elif uploaded_file.name.lower().endswith('.docx'): | |
| try: | |
| document = Document(file_bytes) | |
| text = "" | |
| for para in document.paragraphs: | |
| text += para.text + "\n" | |
| additional_info = ' '.join(text.split()) # Remove extra whitespace | |
| st.success(f"✅ Đã xử lý file DOCX: {uploaded_file.name}") | |
| st.text_area("Nội dung file WORD: ",text, disabled=True) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi xử lý file DOCX: {str(e)}") | |
| # Get job openings for dropdown | |
| job_openings = get_base_openings(access_token) | |
| if not job_openings.empty: | |
| selected_job = st.selectbox( | |
| "🏢 Chọn vị trí tuyển dụng:", | |
| options=job_openings['name'].tolist(), | |
| key="job_selection" | |
| ) | |
| # Get the opening_id for the selected job | |
| selected_opening_id = job_openings[job_openings['name'] == selected_job]['id'].iloc[0] | |
| # Get stages for the selected opening | |
| stages_df = get_opening_stages(selected_opening_id, access_token) | |
| if not stages_df.empty: | |
| # Multi-select for stages | |
| selected_stages = st.multiselect( | |
| "📋 Chọn giai đoạn tuyển dụng:", | |
| options=stages_df['name'].tolist(), | |
| default=stages_df['name'].tolist()[:1], # Chỉ chọn mục đầu tiên mặc định | |
| key="stages_selection" | |
| ) | |
| # Get the stage_ids for the selected stages | |
| selected_stage_ids = stages_df[stages_df['name'].isin(selected_stages)]['id'].tolist() | |
| else: | |
| st.warning("Không thể tải danh sách giai đoạn tuyển dụng") | |
| selected_stage_ids = [] | |
| else: | |
| st.warning("Không thể tải danh sách vị trí tuyển dụng") | |
| selected_job = None | |
| selected_opening_id = None | |
| selected_stage_ids = [] | |
| # Process button | |
| if st.button("🔎 Lấy Thông Tin Ứng Viên"): | |
| if selected_opening_id and selected_stage_ids: | |
| # Fetch candidates by stages | |
| with st.spinner("⏳ Đang lấy thông tin ứng viên từ Base.vn..."): | |
| raw_data = fetch_candidates_by_stages(selected_opening_id, access_token) | |
| data = process_data(raw_data) | |
| if selected_stage_ids: | |
| data = data[data['stage_id'].isin(selected_stage_ids)] | |
| jd = fetch_jd(selected_opening_id, access_token) | |
| elif selected_opening_id: | |
| # If no stages selected, fetch all candidates for the opening | |
| with st.spinner("⏳ Đang lấy thông tin tất cả ứng viên từ vị trí này..."): | |
| raw_data = fetch_candidates_by_stages(selected_opening_id, access_token) | |
| data = process_data(raw_data) | |
| jd = fetch_jd(selected_opening_id, access_token) | |
| else: | |
| st.error("Vui lòng chọn vị trí tuyển dụng") | |
| st.stop() | |
| # Convert empty strings to None | |
| data = convert_empty_to_none(data) | |
| if data is not None and not data.empty: | |
| # Get stage names and create a stage mapping dictionary | |
| stages_mapping = dict(zip(stages_df['id'].astype(str), stages_df['name'])) | |
| # Create a preview dataframe with key information | |
| preview_df = data[['name', 'email', 'phone', 'stage_id']].copy() | |
| # Add stage name column | |
| preview_df['stage_name'] = preview_df['stage_id'].astype(str).map(stages_mapping) | |
| # Rename columns for display | |
| preview_df.rename(columns={ | |
| 'name': 'Tên ứng viên', | |
| 'email': 'Email', | |
| 'phone': 'Số điện thoại', | |
| 'stage_id': 'Mã giai đoạn', | |
| 'stage_name': 'Giai đoạn' | |
| }, inplace=True) | |
| # Display the preview dataframe | |
| st.subheader(f"📋 Danh sách {len(preview_df)} ứng viên sẽ được đánh giá") | |
| st.dataframe(preview_df) | |
| st.success(f"✅ Đã lấy thông tin {len(data)} ứng viên thành công!") | |
| st.header("📊 Đánh giá và Lọc CV") | |
| # Continue with the existing evaluation code | |
| results = [] | |
| progress_bar = st.progress(0) | |
| # Inside the loop where each CV is processed | |
| for i, (_, row) in enumerate(data.iterrows()): | |
| name = row['name'] | |
| cv_url = row['cvs'] | |
| cv_text = get_cv_text_from_url(cv_url) | |
| # Initialize empty strings for review and form data | |
| review_text = "" | |
| form_data_text = "" | |
| # Add review data if available | |
| if 'review' in row and pd.notna(row['review']) and row['review'] != "": | |
| review_text = f"ĐÁNH GIÁ CỦA NHÀ TUYỂN DỤNG: {row['review'].strip()}" | |
| # Add form data if available | |
| form_data_fields = [] | |
| for column in row.index: | |
| # Skip non-form data columns | |
| if column not in ['id', 'name', 'gender', 'cvs', 'email', 'phone', 'review', 'stage_id']: | |
| # Explicitly check for None, empty string, and NaN | |
| if pd.notna(row[column]) and row[column] is not None and row[column] != "": | |
| form_data_fields.append(f"{column}: {row[column]}") | |
| if form_data_fields: | |
| form_data_text = "THÔNG TIN TỪ FORM ỨNG TUYỂN: " + " ".join(form_data_fields) | |
| if cv_text: | |
| # Add the additional information from uploaded file | |
| additional_info_text = "" | |
| if additional_info and additional_info.strip(): | |
| additional_info_text = f"THÔNG TIN BỔ SUNG TỪ TÀI LIỆU ĐÃ TẢI LÊN: {additional_info.strip()}" | |
| # Build prompt components and filter out empty ones | |
| prompt_components = [ | |
| "Bạn là chuyên gia nhân sự với 15 năm kinh nghiệm đánh giá CV. Nhiệm vụ của bạn là đánh giá chi tiết và khách quan CV dưới đây dựa trên mô tả công việc và đặc thù tuyển dụng tại A Plus.", | |
| "### ĐẶC THÙ TUYỂN DỤNG TẠI A PLUS:", | |
| "- Công ty đang trong quá trình chuyển đổi mạnh mẽ.", | |
| "- Tìm kiếm ứng viên có khả năng **thích ứng với công nghệ, AI và sự thay đổi**.", | |
| "- Ưu tiên **tiềm năng phát triển hơn kinh nghiệm**, đề cao tinh thần khởi nghiệp.", | |
| "- Không phù hợp với ứng viên **chỉ tìm kiếm sự ổn định hoặc phúc lợi cao ngay từ đầu**.", | |
| "- Tiêu chí đánh giá: 40% năng lực chuyên môn, 30% phù hợp văn hóa, 20% khả năng học hỏi & đổi mới, 10% điểm cộng & điểm trừ đặc biệt.", | |
| "### MÔ TẢ CÔNG VIỆC:", | |
| f"{jd.strip()}" if jd and jd.strip() else None, | |
| "### CÁC YẾU TỐ QUAN TRỌNG KHI ĐÁNH GIÁ CV:", | |
| "- **Năng lực chuyên môn:** Kinh nghiệm làm việc với nhà cung cấp, xử lý chứng từ, theo dõi tiến độ mua hàng, làm việc với đơn vị logistics.", | |
| "- **Phù hợp văn hóa A Plus:** Cẩn thận & minh bạch, tư duy linh hoạt & chủ động, phù hợp mô hình cung ứng tinh gọn.", | |
| "- **Khả năng học hỏi & đổi mới:** Chủ động học hỏi, thích ứng với thay đổi, tư duy cải tiến & tối ưu công việc.", | |
| "- **Điểm cộng & điểm trừ:** Các điểm mạnh nổi bật (ngoại ngữ, phân tích dữ liệu, ERP...) và rủi ro tiềm ẩn/điểm yếu lớn.", | |
| "### HƯỚNG DẪN CHẤM ĐIỂM:", | |
| "1. **Năng lực chuyên môn (40%)**", | |
| " - 5 – Xuất sắc: Đã có kinh nghiệm thực tế, làm việc hiệu quả, chủ động xử lý vấn đề.", | |
| " - 3-4 – Có kinh nghiệm: Đã làm trong lĩnh vực liên quan nhưng cần rèn luyện thêm.", | |
| " - 1-2 – Chưa có kinh nghiệm: Thiếu kiến thức hoặc chưa từng làm công việc tương tự.", | |
| "2. **Phù hợp văn hóa (30%)**", | |
| " - 5 – Xuất sắc: Tinh thần trách nhiệm cao, linh hoạt, chủ động, phù hợp hoàn toàn với mô hình A Plus.", | |
| " - 3-4 – Phù hợp tương đối: Có tinh thần làm việc tốt nhưng cần kiểm chứng thêm.", | |
| " - 1-2 – Chưa phù hợp: Thiếu sự chủ động, khó thích nghi với văn hóa công ty.", | |
| "3. **Khả năng học hỏi & đổi mới (20%)**", | |
| " - 5 – Rất tốt: Luôn chủ động học hỏi, tiếp thu nhanh công nghệ và quy trình mới.", | |
| " - 3-4 – Ổn: Có khả năng học hỏi nhưng cần thời gian thích nghi.", | |
| " - 1-2 – Yếu: Ít chủ động học tập, khó tiếp thu thay đổi.", | |
| "4. **Điểm cộng & điểm trừ (10%)**", | |
| " - Điểm cộng (Thêm điểm nếu có kỹ năng đặc biệt như ngoại ngữ, ERP, phân tích dữ liệu, v.v.).", | |
| " - Điểm trừ (Trừ điểm nếu có hạn chế lớn như không linh hoạt, thiếu kinh nghiệm thực tế, v.v.).", | |
| "### HƯỚNG DẪN ĐÁNH GIÁ:", | |
| "1. Phân tích kỹ CV và so sánh với từng yêu cầu trong JD.", | |
| "2. Đối với mỗi tiêu chí, đưa ra điểm số từ 1-5 dựa trên mức độ phù hợp.", | |
| "3. Đưa ra nhận xét khách quan, nêu rõ điểm mạnh và điểm cần cải thiện.", | |
| "4. Xem xét các tiêu chí như **độ nhảy việc, tốc độ thăng tiến, khả năng làm việc với nhiều đối tác**.", | |
| "5. Áp dụng **điểm cộng & điểm trừ** khi phân tích CV.", | |
| "6. Nếu có thông tin từ form ứng tuyển hoặc nhận xét của nhà tuyển dụng, hãy kết hợp vào phân tích.", | |
| "7. Vui lòng trả về kết quả đánh giá **CHÍNH XÁC theo định dạng JSON**.", | |
| f"### NỘI DUNG CV CỦA ỨNG VIÊN {name}: {cv_text.strip()}" if cv_text and cv_text.strip() else None, | |
| review_text if review_text.strip() else None, | |
| form_data_text if form_data_text.strip() else None, | |
| additional_info_text if additional_info_text.strip() else None, | |
| ] | |
| # Filter out None values | |
| prompt = " ".join([component for component in prompt_components if component is not None]) | |
| try: | |
| response = get_gemini_response(prompt, cv_text) | |
| # Thay đổi công thức tính điểm tổng thể | |
| main_CV_score = round((response["nang_luc_chuyen_mon"]*0.4 + | |
| response["phu_hop_van_hoa"]*0.3 + | |
| response["kha_nang_hoc_hoi"]*0.2 + | |
| (response["diem_cong"] - response["diem_tru"])*0.1), 2) | |
| # Đảm bảo điểm không bị âm | |
| main_CV_score = max(0, main_CV_score) | |
| uv = { | |
| 'Tên ứng viên': name, | |
| 'Năng lực chuyên môn': response["nang_luc_chuyen_mon"], | |
| 'Phù hợp văn hóa': response["phu_hop_van_hoa"], | |
| 'Khả năng học hỏi': response["kha_nang_hoc_hoi"], | |
| 'Điểm cộng': response["diem_cong"], | |
| 'Điểm trừ': response["diem_tru"], | |
| 'Điểm tổng quát': main_CV_score, | |
| 'Tóm tắt': response["tom_tat"] | |
| } | |
| results.append(uv) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi xử lý CV từ {cv_url}: {str(e)}") | |
| progress_bar.progress(min(1.0, (i + 1) / len(data))) | |
| if results: | |
| st.subheader("📊 Kết quả đánh giá CV") | |
| # Create a DataFrame from the evaluation results | |
| df_results = pd.DataFrame(results) | |
| # Merge the original DataFrame with evaluation results on the 'name' column | |
| final_df = pd.merge(data, df_results, left_on='name', right_on='Tên ứng viên', how='inner') | |
| # Drop the duplicate 'Tên ứng viên (gốc)' column | |
| final_df.drop(columns=['Tên ứng viên'], inplace=True) | |
| # Get stage names and create a stage mapping dictionary | |
| stages_mapping = dict(zip(stages_df['id'].astype(str), stages_df['name'])) | |
| # Add stage name column if stage_id exists | |
| if 'stage_id' in final_df.columns: | |
| final_df['Giai đoạn'] = final_df['stage_id'].astype(str).map(stages_mapping) | |
| # Rename the columns to Vietnamese, keeping only one 'Tên ứng viên' column | |
| final_df.rename(columns={ | |
| 'id': 'Mã ứng viên', | |
| 'name': "Tên ứng viên", | |
| 'email': 'Email', | |
| 'status': 'Trạng thái', | |
| 'cvs': 'Link CV', | |
| 'stage_id': 'Mã giai đoạn' | |
| }, inplace=True) | |
| opening_id = selected_opening_id | |
| # Add link to candidate profile | |
| final_df['Link ứng viên'] = final_df['Mã ứng viên'].apply( | |
| lambda x: f"https://hiring.base.vn/opening/{opening_id}?candidate={x}" | |
| ) | |
| # Loại bỏ các cột không có bất kỳ dữ liệu nào hoặc chỉ chứa giá trị rỗng | |
| final_df = final_df.dropna(axis=1, how='all') # Xóa cột nếu tất cả giá trị đều là NaN | |
| final_df = final_df.loc[:, (final_df != "").any(axis=0)] # Xóa cột nếu tất cả giá trị đều là "" | |
| st.header("📋 Dữ liệu chi tiết") | |
| st.dataframe(final_df) | |
| # Add download button for the results | |
| csv = final_df.to_csv(index=False).encode('utf-8-sig') | |
| st.download_button( | |
| label="📥 Tải xuống kết quả đánh giá CSV", | |
| data=csv, | |
| file_name="ket_qua_danh_gia_cv.csv", | |
| mime="text/csv", | |
| ) | |
| else: | |
| st.warning("⚠️ Không có kết quả nào được tạo. Vui lòng kiểm tra API key và thử lại.") | |
| else: | |
| st.error("Không thể lấy dữ liệu hoặc không tìm thấy ứng viên nào.") | |
| with tab2: | |
| dashboard() | |
| st.markdown("---") | |
| st.markdown("🚀 Powered by Streamlit | 💼 Created for HR professionals") | |