Spaces:
Runtime error
Runtime error
| import os | |
| import weaviate | |
| from weaviate.auth import Auth | |
| from openai import OpenAI | |
| import json | |
| import gradio as gr | |
| import atexit | |
| import datetime | |
| import re | |
| import uuid | |
| # --- New libraries for file processing --- | |
| import pypdf | |
| import docx | |
| # --- 1. CONFIGURATION --- | |
| MODEL_NAME = "openai/gpt-oss-120b" | |
| EMBEDDING_MODEL_NAME = "Qwen/Qwen3-Embedding-8B" | |
| DEEPINFRA_API_KEY = "KwZiFcFHhOPUE6Rrc6wY4ng0mqPfwsVN" | |
| BASE_URL = "https://api.deepinfra.com/v1/openai" | |
| WEAVIATE_URL = "maf5cvz1saelnti3k34a.c0.europe-west3.gcp.weaviate.cloud" | |
| WEAVIATE_API_KEY = "cHFZK1JOaEg3K2p6K3JnQl9ZM1FEQ2NhMVU1SnBRVUpYWCtCVHlVU0J2Qmx1Mk9SaktpT09UQTNiU1hRPV92MjAw" | |
| # --- SIMULATED USER FOR TESTING --- | |
| # In a real application, this would come from a login system. | |
| CURRENT_USER_ID = "recruiter_001" | |
| # To test student features, change to "student_007" | |
| # --- Helper function to create schemas --- | |
| def create_application_schema(client: weaviate.WeaviateClient): | |
| # ... (code unchanged) | |
| collection_name = "Application" | |
| if not client.collections.exists(collection_name): | |
| print(f"Creating collection: {collection_name}") | |
| client.collections.create( | |
| name=collection_name, | |
| properties=[ | |
| weaviate.classes.config.Property(name="job_id", data_type=weaviate.classes.config.DataType.TEXT), | |
| weaviate.classes.config.Property(name="user_id", data_type=weaviate.classes.config.DataType.TEXT), | |
| weaviate.classes.config.Property(name="cv_content", data_type=weaviate.classes.config.DataType.TEXT), | |
| weaviate.classes.config.Property(name="cover_letter_content", data_type=weaviate.classes.config.DataType.TEXT), | |
| weaviate.classes.config.Property(name="submission_date", data_type=weaviate.classes.config.DataType.DATE), | |
| weaviate.classes.config.Property(name="status", data_type=weaviate.classes.config.DataType.TEXT), | |
| ] | |
| ) | |
| print(f"✅ Collection '{collection_name}' created successfully.") | |
| else: | |
| print(f"✅ Collection '{collection_name}' already exists.") | |
| def create_project_schema(client: weaviate.WeaviateClient): | |
| # ... (code unchanged) | |
| collection_name = "Project" | |
| if not client.collections.exists(collection_name): | |
| print(f"Creating collection: {collection_name}") | |
| client.collections.create( | |
| name=collection_name, | |
| properties=[ | |
| weaviate.classes.config.Property(name="project_name", data_type=weaviate.classes.config.DataType.TEXT), | |
| weaviate.classes.config.Property(name="description", data_type=weaviate.classes.config.DataType.TEXT), | |
| weaviate.classes.config.Property(name="required_skills", data_type=weaviate.classes.config.DataType.TEXT_ARRAY), | |
| weaviate.classes.config.Property(name="team_members", data_type=weaviate.classes.config.DataType.TEXT_ARRAY), | |
| weaviate.classes.config.Property(name="pending_members", data_type=weaviate.classes.config.DataType.TEXT_ARRAY), | |
| weaviate.classes.config.Property(name="max_team_size", data_type=weaviate.classes.config.DataType.NUMBER), | |
| weaviate.classes.config.Property(name="creator_id", data_type=weaviate.classes.config.DataType.TEXT), | |
| weaviate.classes.config.Property(name="is_recruiting", data_type=weaviate.classes.config.DataType.BOOL), | |
| ] | |
| ) | |
| print(f"✅ Collection '{collection_name}' created successfully.") | |
| else: | |
| print(f"✅ Collection '{collection_name}' already exists.") | |
| def create_user_schema(client: weaviate.WeaviateClient): | |
| # ... (code unchanged) | |
| collection_name = "User" | |
| if not client.collections.exists(collection_name): | |
| print(f"Creating collection: {collection_name}") | |
| client.collections.create( | |
| name=collection_name, | |
| properties=[ | |
| weaviate.classes.config.Property(name="user_id", data_type=weaviate.classes.config.DataType.TEXT), | |
| weaviate.classes.config.Property(name="cv_content", data_type=weaviate.classes.config.DataType.TEXT), | |
| ] | |
| ) | |
| print(f"✅ Collection '{collection_name}' created successfully.") | |
| else: | |
| print(f"✅ Collection '{collection_name}' already exists.") | |
| # --- 2. CHATBOT CLASS --- | |
| class WeaviateChatbot: | |
| def __init__(self, weaviate_url, weaviate_api_key, llm_api_key, llm_base_url): | |
| print("Connecting to clients...") | |
| self.weaviate_client = weaviate.connect_to_weaviate_cloud( | |
| cluster_url=weaviate_url, | |
| auth_credentials=Auth.api_key(weaviate_api_key), | |
| skip_init_checks=True | |
| ) | |
| self.weaviate_client.connect() | |
| print("✅ Successfully connected to Weaviate.") | |
| create_application_schema(self.weaviate_client) | |
| create_project_schema(self.weaviate_client) | |
| create_user_schema(self.weaviate_client) | |
| self.llm_client = OpenAI(api_key=llm_api_key, base_url=llm_base_url) | |
| print("✅ Successfully connected to LLM client (DeepInfra).") | |
| self.collection_names = ["Job", "Opportunities", "Project", "User"] | |
| # --- Core Methods --- | |
| def _embed_text(self, text: str) -> list[float]: | |
| resp = self.llm_client.embeddings.create(model=EMBEDDING_MODEL_NAME, input=text, encoding_format="float") | |
| return resp.data[0].embedding | |
| def _search_database(self, query_vector: list[float], limit: int = 5, collection_name: str = None) -> str: | |
| # ... (code unchanged) | |
| all_results = [] | |
| collections_to_search = [collection_name] if collection_name else self.collection_names | |
| for name in collections_to_search: | |
| try: | |
| collection = self.weaviate_client.collections.get(name) | |
| response = collection.query.near_vector(near_vector=query_vector, limit=limit) | |
| for item in response.objects: | |
| all_results.append(f"Type: {name}\nContent: {json.dumps(item.properties, indent=2, default=str)}\n") | |
| except Exception as e: | |
| print(f"Could not query collection '{name}'. Error: {e}") | |
| return "\n---\n".join(all_results) if all_results else "No relevant information found in the database." | |
| def _generate_response(self, query: str, context: str) -> str: | |
| prompt = f""" | |
| You are *EduNatives Assistant*. | |
| Your primary goal is to help users discover opportunities (e.g., jobs, internships, projects) and take actions such as applying, creating projects, or analyzing CVs. | |
| ### Guidelines: | |
| 1. **Language Consistency**: Always respond in the same language as the user's query. | |
| 2. **Job Listings**: | |
| - By default, list jobs in a **numbered list** with unique identifiers like `(job_001)`. | |
| - If the user explicitly asks for a "table", format results as a clean **markdown table** with the following columns: | |
| `Identifier | Title | Company | Location | Description`. | |
| 3. **Special Intents**: | |
| - If the user wants to apply for a job → respond ONLY with: | |
| `STARTING_APPLICATION_PROCESS:job_id` | |
| - If the user wants to create a project → respond ONLY with: | |
| `STARTING_PROJECT_CREATION` | |
| - If the user wants to analyze their CV → respond ONLY with: | |
| `INTENT_ANALYZE_CV` | |
| - If the user wants to rerank or evaluate CVs → respond ONLY with: | |
| `INTENT_START_RERANK` | |
| 4. **Answer Style**: Keep responses concise, clear, and directly helpful. | |
| 5. **Priority**: Always prioritize opportunities and context provided in the database. | |
| --- CONTEXT FROM DATABASE START --- | |
| {context} | |
| --- CONTEXT FROM DATABASE END --- | |
| User Question: {query} | |
| Answer: | |
| """ | |
| response = self.llm_client.chat.completions.create(model=MODEL_NAME, messages=[{"role": "user", "content": prompt}], max_tokens=4096) | |
| return response.choices[0].message.content.strip() | |
| def _get_query_intent(self, query: str) -> dict: | |
| # ... (code unchanged) | |
| prompt = f""" | |
| Analyze the user's query to understand their intent and extract key entities. Your goal is to route the query to the correct function. | |
| Respond with a JSON object containing "intent", "entity_type", and "entity_id". | |
| - 'intent' can be one of: ["get_details", "get_applicants", "general_query", "other_action"]. | |
| - 'entity_type' can be one of: ["job", "project", "user", "unknown"]. | |
| - 'entity_id' should be the specific identifier mentioned (e.g., "job_022", "user_010", "PROJ-007", "Blockchain-Based Academic Credential System"). | |
| If the query is a command to the chatbot (like "analyze my CV" or "join a project"), set intent to "other_action". | |
| If the query is a general question without a specific ID, set intent to "general_query". | |
| Examples: | |
| - Query: "Who applied for job_022?" -> {{"intent": "get_applicants", "entity_type": "job", "entity_id": "job_022"}} | |
| - Query: "Show me details about project_003" -> {{"intent": "get_details", "entity_type": "project", "entity_id": "project_003"}} | |
| - Query: "tell me about user_010" -> {{"intent": "get_details", "entity_type": "user", "entity_id": "user_010"}} | |
| - Query: "Show me details about PROJ-0007" -> {{"intent": "get_details", "entity_type": "project", "entity_id": "PROJ-0007"}} | |
| - Query: "find me jobs in marketing" -> {{"intent": "general_query", "entity_type": "job", "entity_id": null}} | |
| - Query: "join the AI project" -> {{"intent": "other_action", "entity_type": "project", "entity_id": null}} | |
| User Query: "{query}" | |
| JSON Response: | |
| """ | |
| try: | |
| response = self.llm_client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=256, | |
| response_format={"type": "json_object"} # Use JSON mode | |
| ) | |
| result = json.loads(response.choices[0].message.content.strip()) | |
| print(f"DEBUG: Intent recognized -> {result}") | |
| return result | |
| except Exception as e: | |
| print(f"❌ Error in intent recognition: {e}") | |
| return {"intent": "general_query", "entity_type": "unknown", "entity_id": None} | |
| def _get_details_by_id(self, collection_name: str, property_name: str, entity_id: str): | |
| # ... (code unchanged) | |
| try: | |
| collection = self.weaviate_client.collections.get(collection_name) | |
| response = collection.query.fetch_objects( | |
| limit=1, | |
| filters=weaviate.classes.query.Filter.by_property(property_name).equal(entity_id) | |
| ) | |
| if response.objects: | |
| return f"Details for {collection_name} '{entity_id}':\n\n{json.dumps(response.objects[0].properties, indent=2, default=str)}" | |
| else: | |
| return f"🔍 Sorry, I couldn't find any details for {collection_name} with the ID '{entity_id}'." | |
| except Exception as e: | |
| print(f"Error fetching details for {entity_id}: {e}") | |
| return f"❌ An error occurred while searching for '{entity_id}'." | |
| def _get_applicants_by_job_id(self, job_id: str): | |
| # ... (code unchanged) | |
| try: | |
| applications = self.weaviate_client.collections.get("Application") | |
| response = applications.query.fetch_objects( | |
| filters=weaviate.classes.query.Filter.by_property("job_id").equal(job_id) | |
| ) | |
| if response.objects: | |
| user_ids = [obj.properties.get("user_id", "Unknown User") for obj in response.objects] | |
| return f"Applicants for job '{job_id}':\n- " + "\n- ".join(user_ids) | |
| else: | |
| return f"I couldn't find any applicants for job '{job_id}'." | |
| except Exception as e: | |
| print(f"Error fetching applicants for {job_id}: {e}") | |
| return f"❌ An error occurred while searching for applicants for job '{job_id}'." | |
| def ask(self, query: str): | |
| # ... (code unchanged) | |
| print(f"\nProcessing query: '{query}'") | |
| intent_data = self._get_query_intent(query) | |
| intent = intent_data.get("intent") | |
| entity_type = intent_data.get("entity_type") | |
| entity_id = intent_data.get("entity_id") | |
| if intent == "get_details" and entity_id: | |
| entity_type_lower = entity_type.lower() | |
| if entity_type_lower == "job": | |
| return self._get_details_by_id("Job", "job_id", entity_id) | |
| elif entity_type_lower == "user": | |
| return self._get_details_by_id("User", "user_id", entity_id) | |
| elif entity_type_lower == "project": | |
| project_obj = self._get_project_by_name(entity_id) # Use smart search for project names | |
| if project_obj: | |
| return f"Details for project '{project_obj.properties.get('project_name')}':\n\n{json.dumps(project_obj.properties, indent=2, default=str)}" | |
| else: | |
| return f"🔍 Sorry, I couldn't find any details for a project named '{entity_id}'." | |
| elif intent == "get_applicants" and entity_id and entity_type.lower() == "job": | |
| return self._get_applicants_by_job_id(entity_id) | |
| query_vector = self._embed_text(query) | |
| context = self._search_database(query_vector) | |
| return self._generate_response(query, context) | |
| def save_application(self, application_data: dict, user_id: str): | |
| # ... (code unchanged) | |
| print("Saving application to Weaviate...") | |
| try: | |
| applications = self.weaviate_client.collections.get("Application") | |
| app_uuid = applications.data.insert({ | |
| "job_id": application_data.get("job_id"), | |
| "user_id": user_id, | |
| "cv_content": application_data.get("cv_content"), | |
| "cover_letter_content": application_data.get("cover_letter_content"), | |
| "submission_date": datetime.datetime.now(datetime.timezone.utc), | |
| "status": "Submitted" | |
| }) | |
| print(f"✅ Application saved with UUID: {app_uuid}") | |
| return True | |
| except Exception as e: | |
| print(f"❌ Failed to save application: {e}") | |
| return False | |
| def close_connections(self): | |
| if self.weaviate_client.is_connected(): | |
| self.weaviate_client.close() | |
| print("\nWeaviate connection closed.") | |
| def _get_job_details(self, job_id: str) -> dict: | |
| # ... (code unchanged) | |
| try: | |
| jobs = self.weaviate_client.collections.get("Job") | |
| response = jobs.query.fetch_objects( | |
| limit=1, | |
| filters=weaviate.classes.query.Filter.by_property("job_id").equal(job_id) | |
| ) | |
| if response.objects: | |
| return response.objects[0].properties | |
| except Exception as e: | |
| print(f"Error fetching job details for {job_id}: {e}") | |
| return None | |
| def generate_cover_letter(self, cv_content: str, job_id: str) -> str: | |
| # ... (code unchanged) | |
| print(f"Generating Cover Letter for job: {job_id}") | |
| job_details = self._get_job_details(job_id) | |
| if not job_details: | |
| print(f"⚠️ Job details for '{job_id}' not found. Generating a generic cover letter based on CV.") | |
| prompt = f""" | |
| You are an expert career coach. A user has provided their CV but the specific job details for job '{job_id}' could not be found. | |
| **Goal:** Write a strong, general-purpose cover letter based ONLY on the user's CV. | |
| **Instructions:** | |
| 1. Analyze the User's CV to identify their key skills, main role, and accomplishments. | |
| 2. Write a cover letter that showcases these strengths for a typical role in their field. | |
| 3. Start with "Dear Hiring Manager,". Maintain a professional and enthusiastic tone. | |
| 4. **Important:** Add a note at the end: "[This is a general cover letter as the specific job details for '{job_id}' were not found.]" | |
| --- USER CV CONTENT START --- | |
| {cv_content} | |
| --- USER CV CONTENT END --- | |
| Now, write the general-purpose cover letter. | |
| """ | |
| else: | |
| prompt = f""" | |
| You are an expert career coach specializing in crafting impactful cover letters. | |
| **Goal:** Write a professional, personalized cover letter that bridges a candidate's CV and a job's requirements. | |
| **Instructions:** | |
| 1. Analyze the Job Description for key responsibilities and skills. | |
| 2. Analyze the User's CV for relevant experiences. | |
| 3. Explicitly connect the user's qualifications to the job requirements. | |
| 4. Start with "Dear Hiring Manager,". Maintain a professional tone. | |
| --- JOB DESCRIPTION START --- | |
| {json.dumps(job_details, indent=2)} | |
| --- JOB DESCRIPTION END --- | |
| --- USER CV CONTENT START --- | |
| {cv_content} | |
| --- USER CV CONTENT END --- | |
| Now, write the cover letter. | |
| """ | |
| response = self.llm_client.chat.completions.create(model=MODEL_NAME, messages=[{"role": "user", "content": prompt}], max_tokens=2048) | |
| return response.choices[0].message.content.strip() | |
| # --- Project Management Methods --- | |
| def create_project(self, project_data: dict, creator_id: str): | |
| # ... (code unchanged) | |
| print("Saving new project to Weaviate...") | |
| try: | |
| projects = self.weaviate_client.collections.get("Project") | |
| project_uuid = projects.data.insert({ | |
| "project_name": project_data.get("project_name"), | |
| "description": project_data.get("description"), | |
| "required_skills": project_data.get("required_skills"), | |
| "max_team_size": project_data.get("max_team_size"), | |
| "creator_id": creator_id, | |
| "is_recruiting": True, | |
| "team_members": [creator_id], # Creator is the first member | |
| "pending_members": [] | |
| }) | |
| print(f"✅ Project saved with UUID: {project_uuid}") | |
| return True, "Project created successfully!" | |
| except Exception as e: | |
| print(f"❌ Failed to save project: {e}") | |
| return False, "Sorry, there was an error creating your project. Please try again later." | |
| def _get_project_by_name(self, project_name: str): | |
| # ... (code unchanged) | |
| try: | |
| projects = self.weaviate_client.collections.get("Project") | |
| response = projects.query.hybrid( | |
| query=project_name, | |
| limit=1, | |
| query_properties=["project_name", "description"] | |
| ) | |
| return response.objects[0] if response.objects else None | |
| except Exception as e: | |
| print(f"Error fetching project '{project_name}': {e}") | |
| return None | |
| def request_to_join_project(self, project_name: str, user_id: str): | |
| # ... (code unchanged) | |
| project = self._get_project_by_name(project_name) | |
| if not project: | |
| return False, f"🔍 Sorry, I couldn't find a project named '{project_name}'. Please check the name and try again." | |
| props = project.properties | |
| actual_project_name = props.get('project_name') | |
| if user_id in props.get("team_members", []): | |
| return False, f"You are already a member of the '{actual_project_name}' project." | |
| if user_id in props.get("pending_members", []): | |
| return False, f"You have already sent a request to join '{actual_project_name}'." | |
| try: | |
| projects = self.weaviate_client.collections.get("Project") | |
| pending_list = props.get("pending_members", []) + [user_id] | |
| projects.data.update(uuid=project.uuid, properties={"pending_members": pending_list}) | |
| return True, f"✅ Your request to join '{actual_project_name}' has been sent!" | |
| except Exception as e: | |
| print(f"❌ Failed to update project join requests: {e}") | |
| return False, "Sorry, there was an error sending your request." | |
| def get_project_requests(self, project_name: str, user_id: str): | |
| # ... (code unchanged) | |
| project = self._get_project_by_name(project_name) | |
| if not project: | |
| return f"🔍 Sorry, I couldn't find a project named '{project_name}'." | |
| if project.properties.get("creator_id") != user_id: | |
| return "You are not the creator of this project, so you cannot view its requests." | |
| pending = project.properties.get("pending_members", []) | |
| if not pending: | |
| return f"There are currently no pending requests for '{project_name}'." | |
| return f"Pending requests for '{project_name}':\n- " + "\n- ".join(pending) | |
| def accept_project_member(self, project_name: str, member_id: str, user_id: str): | |
| # ... (code unchanged) | |
| project = self._get_project_by_name(project_name) | |
| if not project: | |
| return f"🔍 Sorry, I couldn't find a project named '{project_name}'." | |
| if project.properties.get("creator_id") != user_id: | |
| return "You are not the creator of this project." | |
| props = project.properties | |
| pending_list = props.get("pending_members", []) | |
| if member_id not in pending_list: | |
| return f"User '{member_id}' has not requested to join this project." | |
| try: | |
| projects = self.weaviate_client.collections.get("Project") | |
| pending_list.remove(member_id) | |
| team_list = props.get("team_members", []) + [member_id] | |
| projects.data.update(uuid=project.uuid, properties={ | |
| "pending_members": pending_list, | |
| "team_members": team_list | |
| }) | |
| return f"✅ User '{member_id}' has been added to the '{project_name}' team!" | |
| except Exception as e: | |
| print(f"❌ Failed to accept member: {e}") | |
| return "Sorry, there was an error accepting this member." | |
| # --- Smart CV & Job Matching Methods --- | |
| def analyze_cv(self, cv_content: str, user_id: str): | |
| # ... (code unchanged) | |
| print(f"Analyzing CV for user: {user_id}") | |
| try: | |
| users = self.weaviate_client.collections.get("User") | |
| response = users.query.fetch_objects(limit=1, filters=weaviate.classes.query.Filter.by_property("user_id").equal(user_id)) | |
| if response.objects: | |
| user_uuid = response.objects[0].uuid | |
| users.data.update(uuid=user_uuid, properties={"cv_content": cv_content}) | |
| else: | |
| users.data.insert({"user_id": user_id, "cv_content": cv_content}) | |
| except Exception as e: | |
| print(f"❌ Could not save CV for user {user_id}: {e}") | |
| prompt = f""" | |
| You are an expert career coach and CV reviewer. Analyze the following CV and provide constructive feedback. | |
| Focus on: | |
| 1. **Clarity and Conciseness:** Is the language clear? Are the sentences too long? | |
| 2. **Impactful Language:** Suggest stronger action verbs (e.g., instead of "worked on," suggest "developed," "engineered," "managed"). | |
| 3. **Keywords:** Are there relevant industry keywords missing? Suggest some based on the content. | |
| 4. **Structure and Formatting:** Comment on the overall layout and readability. | |
| Provide the feedback in a structured format with clear headings. Respond in the same language as the CV content. | |
| --- CV CONTENT START --- | |
| {cv_content} | |
| --- CV CONTENT END --- | |
| """ | |
| response = self.llm_client.chat.completions.create(model=MODEL_NAME, messages=[{"role": "user", "content": prompt}], max_tokens=2048) | |
| return response.choices[0].message.content.strip() | |
| def match_jobs_to_cv(self, cv_content: str): | |
| # ... (code unchanged) | |
| print("Matching jobs to CV content...") | |
| prompt = f"Extract a list of key technical and soft skills from this CV. Return them as a single, comma-separated string. CV: {cv_content}" | |
| response = self.llm_client.chat.completions.create(model=MODEL_NAME, messages=[{"role": "user", "content": prompt}], max_tokens=512) | |
| skills_text = response.choices[0].message.content.strip() | |
| if not skills_text: | |
| return "Could not extract skills from the CV to match jobs." | |
| print(f"Extracted skills: {skills_text}") | |
| skills_vector = self._embed_text(skills_text) | |
| search_results = self._search_database(skills_vector, limit=3, collection_name="Job") | |
| if "No relevant information" in search_results: | |
| return "🔍 I couldn't find any jobs that closely match the skills in your CV right now." | |
| else: | |
| return f"Here are the top 3 jobs that match the skills in your CV:\n\n{search_results}" | |
| # --- NEW: CV Reranking Engine --- | |
| def rerank_cvs(self, requirements: str, cv_files: list): | |
| print(f"Starting CV reranking process for requirements: {requirements}") | |
| cv_contents_str = "" | |
| for i, cv in enumerate(cv_files): | |
| cv_contents_str += f"\n--- CV FILENAME: {cv['name']} ---\n{cv['content']}\n" | |
| prompt = f""" | |
| You are an expert AI-powered HR Recruiter. Your task is to analyze and rank multiple CVs based on a specific set of job requirements. | |
| Provide a score from 1 to 100 for each CV, where 100 is a perfect match. Also, provide a brief, crisp justification for your score. | |
| **Job Requirements:** | |
| {requirements} | |
| **CVs to Analyze:** | |
| {cv_contents_str} | |
| **Instructions:** | |
| 1. Carefully read each CV and compare it against the job requirements. | |
| 2. Assign a score based on skills, experience, and overall fit. | |
| 3. Write a short justification explaining the score. | |
| 4. Return the final result as a single JSON array of objects. Each object must have three keys: "cv_name", "score", and "justification". | |
| 5. **Important**: The JSON array should be sorted with the highest score first. | |
| JSON Response: | |
| """ | |
| try: | |
| response = self.llm_client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=4096, # Allow for longer responses with multiple CVs | |
| response_format={"type": "json_object"} | |
| ) | |
| # The model might return a JSON object with a key like "results" | |
| result_data = json.loads(response.choices[0].message.content.strip()) | |
| # Handle both list and dict responses | |
| ranked_list = result_data if isinstance(result_data, list) else result_data.get("results", []) | |
| if not ranked_list: | |
| return "I couldn't generate a ranking for the provided CVs. Please try again." | |
| # Format the output for the user | |
| output = "### CV Reranking Results\nHere are the CVs ranked by suitability:\n\n" | |
| for i, item in enumerate(ranked_list): | |
| output += f"**{i+1}. {item.get('cv_name')}**\n" | |
| output += f" - **Score:** {item.get('score')}/100\n" | |
| output += f" - **Justification:** {item.get('justification')}\n\n" | |
| return output | |
| except Exception as e: | |
| print(f"❌ Error during CV reranking: {e}") | |
| return "❌ An error occurred while trying to rerank the CVs. Please check the file formats and try again." | |
| # --- Helper to extract text from uploaded files --- | |
| def _extract_text_from_file(file_path): | |
| # ... (code unchanged) | |
| print(f"Extracting text from: {file_path}") | |
| if file_path.endswith('.pdf'): | |
| try: | |
| reader = pypdf.PdfReader(file_path) | |
| text = "".join(page.extract_text() for page in reader.pages) | |
| return text | |
| except Exception as e: | |
| return f"Error reading PDF: {e}" | |
| elif file_path.endswith('.docx'): | |
| try: | |
| doc = docx.Document(file_path) | |
| return "\n".join([para.text for para in doc.paragraphs]) | |
| except Exception as e: | |
| return f"Error reading DOCX: {e}" | |
| elif file_path.endswith('.txt'): | |
| try: | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| return f.read() | |
| except Exception as e: | |
| return f"Error reading TXT: {e}" | |
| return "Unsupported file type." | |
| # --- 3. INITIALIZE CHATBOT --- | |
| chatbot_instance = WeaviateChatbot(WEAVIATE_URL, WEAVIATE_API_KEY, DEEPINFRA_API_KEY, BASE_URL) | |
| atexit.register(chatbot_instance.close_connections) | |
| # --- 4. GRADIO INTERFACE LOGIC --- | |
| def chat_interface_func(message: str, history: list, app_state: dict, file_obj: object): | |
| history = history or [] | |
| current_mode = app_state.get("mode", "GENERAL") | |
| hide_examples = gr.update(visible=False) | |
| # --- Part 1: Handle File Uploads --- | |
| if file_obj is not None: | |
| files = file_obj if isinstance(file_obj, list) else [file_obj] | |
| # Standard single file uploads | |
| if len(files) == 1: | |
| file_path = files[0].name | |
| text = _extract_text_from_file(file_path) | |
| if current_mode == "APPLYING_CV": | |
| app_state["cv_content"] = text | |
| bot_message = (f"📄 CV '{os.path.basename(file_path)}' uploaded. " | |
| f"Would you like me to help you write a cover letter for job **{app_state.get('job_id')}**, " | |
| "or would you prefer to upload your own?") | |
| history.append((None, bot_message)) | |
| app_state["mode"] = "APPLYING_COVER_LETTER_CHOICE" | |
| return history, app_state, gr.update(visible=True, value=None, file_count="single"), hide_examples | |
| elif current_mode == "APPLYING_COVER_LETTER_UPLOAD": | |
| app_state["cover_letter_content"] = text | |
| history.append((f"📄 Cover Letter '{os.path.basename(file_path)}' uploaded.", "Thank you! Submitting your application now...")) | |
| success = chatbot_instance.save_application(app_state, CURRENT_USER_ID) | |
| final_message = f"✅ Your application for job **{app_state.get('job_id')}** has been submitted successfully!" if success else "❌ Sorry, there was an error submitting your application." | |
| history.append((None, final_message)) | |
| app_state = {"mode": "GENERAL"} | |
| return history, app_state, gr.update(visible=False, value=None, file_count="single"), hide_examples | |
| elif current_mode == "AWAITING_CV_FOR_ANALYSIS": | |
| history.append((f"📄 CV '{os.path.basename(file_path)}' received. Analyzing now...", None)) | |
| feedback = chatbot_instance.analyze_cv(text, CURRENT_USER_ID) | |
| job_matches = chatbot_instance.match_jobs_to_cv(text) | |
| full_response = f"### CV Analysis & Feedback\n\n{feedback}\n\n---\n\n### Top Job Matches For You\n\n{job_matches}" | |
| history.append((None, full_response)) | |
| app_state["mode"] = "GENERAL" | |
| return history, app_state, gr.update(visible=False, value=None, file_count="single"), hide_examples | |
| # NEW: Multi-file upload for Reranking | |
| if current_mode == "AWAITING_CVs_FOR_RERANK": | |
| history.append((f"📄 Received {len(files)} CVs. Starting the reranking process now...", None)) | |
| cv_files_data = [] | |
| for file in files: | |
| cv_files_data.append({ | |
| "name": os.path.basename(file.name), | |
| "content": _extract_text_from_file(file.name) | |
| }) | |
| requirements = app_state.get("rerank_requirements") | |
| ranked_results = chatbot_instance.rerank_cvs(requirements, cv_files_data) | |
| history.append((None, ranked_results)) | |
| app_state["mode"] = "GENERAL" | |
| return history, app_state, gr.update(visible=False, value=None, file_count="single"), hide_examples | |
| # --- Part 2: Handle Text Messages --- | |
| if message: | |
| history.append((message, None)) | |
| # --- Multi-step Conversation Flows --- | |
| if current_mode == "AWAITING_REQUIREMENTS_FOR_RERANK": | |
| app_state["rerank_requirements"] = message | |
| app_state["mode"] = "AWAITING_CVs_FOR_RERANK" | |
| bot_message = "Great. Now, please upload all the CVs you want me to rank based on these requirements." | |
| history.append((None, bot_message)) | |
| return history, app_state, gr.update(visible=True, file_count="multiple"), hide_examples | |
| if current_mode == "CREATING_PROJECT_NAME": | |
| app_state["project_name"] = message | |
| app_state["mode"] = "CREATING_PROJECT_DESC" | |
| history.append((None, "Great! Now, please provide a short description for your project.")) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| # ... (rest of multi-step flows are similar) | |
| elif current_mode == "CREATING_PROJECT_DESC": | |
| app_state["description"] = message | |
| app_state["mode"] = "CREATING_PROJECT_SKILLS" | |
| history.append((None, "What skills are required? (e.g., Python, UI/UX, Marketing)")) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| elif current_mode == "CREATING_PROJECT_SKILLS": | |
| app_state["required_skills"] = [skill.strip() for skill in message.split(',')] | |
| app_state["mode"] = "CREATING_PROJECT_SIZE" | |
| history.append((None, "Perfect. How many members are you looking for in the team?")) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| elif current_mode == "CREATING_PROJECT_SIZE": | |
| try: | |
| app_state["max_team_size"] = int(message) | |
| success, bot_message = chatbot_instance.create_project(app_state, CURRENT_USER_ID) | |
| history.append((None, bot_message)) | |
| app_state = {"mode": "GENERAL"} | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| except ValueError: | |
| history.append((None, "Please enter a valid number for the team size.")) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| elif current_mode == "AWAITING_PROJECT_TO_JOIN": | |
| project_name = message | |
| success, bot_message = chatbot_instance.request_to_join_project(project_name, CURRENT_USER_ID) | |
| history.append((None, bot_message)) | |
| if success: | |
| app_state["mode"] = "GENERAL" | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| elif current_mode == "AWAITING_PROJECT_TO_VIEW": | |
| project_name = message | |
| bot_message = chatbot_instance.get_project_requests(project_name, CURRENT_USER_ID) | |
| history.append((None, bot_message)) | |
| app_state["mode"] = "GENERAL" | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| elif current_mode == "AWAITING_MEMBER_TO_ACCEPT": | |
| app_state["member_to_accept"] = message | |
| app_state["mode"] = "AWAITING_PROJECT_FOR_ACCEPT" | |
| history.append((None, f"Which project do you want to accept '{message}' for?")) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| elif current_mode == "AWAITING_PROJECT_FOR_ACCEPT": | |
| project_name = message | |
| member_id = app_state.get("member_to_accept") | |
| bot_message = chatbot_instance.accept_project_member(project_name, member_id, CURRENT_USER_ID) | |
| history.append((None, bot_message)) | |
| app_state = {"mode": "GENERAL"} | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| if current_mode == "APPLYING_COVER_LETTER_CHOICE": | |
| positive_keywords = ["help", "generate", "write", "yes", "ok", "sure", "please"] | |
| if any(keyword in message.lower() for keyword in positive_keywords) and "upload" not in message.lower(): | |
| history.append((None, "Of course! I'm generating a draft for you now... This might take a moment.")) | |
| cover_letter = chatbot_instance.generate_cover_letter(app_state["cv_content"], app_state["job_id"]) | |
| history.append((None, f"Here is a draft for your cover letter:\n\n---\n{cover_letter}\n\n---\n\nIf you are happy with this, please type 'submit' to send the application.")) | |
| app_state["cover_letter_content"] = cover_letter | |
| app_state["mode"] = "CONFIRM_SUBMISSION" | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| elif "upload" in message.lower(): | |
| history.append((None, "Okay, please upload your cover letter file.")) | |
| app_state["mode"] = "APPLYING_COVER_LETTER_UPLOAD" | |
| return history, app_state, gr.update(visible=True, file_count="single"), hide_examples | |
| else: | |
| history.append((None, "I'm sorry, I didn't quite understand. Do you want me to **write** a letter for you, or would you prefer to **upload** your own?")) | |
| return history, app_state, gr.update(visible=True, file_count="single"), hide_examples | |
| if current_mode == "CONFIRM_SUBMISSION": | |
| if "submit" in message.lower(): | |
| history.append((None, "Thank you! Submitting your application now...")) | |
| success = chatbot_instance.save_application(app_state, CURRENT_USER_ID) | |
| final_message = f"✅ Your application for job **{app_state.get('job_id')}** has been submitted successfully!" if success else "❌ Sorry, there was an error submitting your application." | |
| history.append((None, final_message)) | |
| app_state = {"mode": "GENERAL"} | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| else: | |
| history.append((None, "Please type 'submit' to confirm and send your application.")) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| # --- General Chat & Starting New Flows --- | |
| response = chatbot_instance.ask(message) | |
| app_match = re.search(r"STARTING_APPLICATION_PROCESS:([\w-]+)", response) | |
| if app_match: | |
| job_id = app_match.group(1) | |
| app_state["mode"] = "APPLYING_CV" | |
| app_state["job_id"] = job_id | |
| bot_message = f"Starting application for job **{job_id}**. Please upload your CV." | |
| history.append((None, bot_message)) | |
| return history, app_state, gr.update(visible=True, file_count="single"), hide_examples | |
| elif "STARTING_PROJECT_CREATION" in response: | |
| app_state["mode"] = "CREATING_PROJECT_NAME" | |
| bot_message = "Awesome! Let's create a new project. What would you like to name it?" | |
| history.append((None, bot_message)) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| elif "INTENT_ANALYZE_CV" in response: | |
| app_state["mode"] = "AWAITING_CV_FOR_ANALYSIS" | |
| bot_message = "Of course! I can help with that. Please upload your CV, and I'll provide feedback and match you with the best jobs." | |
| history.append((None, bot_message)) | |
| return history, app_state, gr.update(visible=True, file_count="single"), hide_examples | |
| elif "INTENT_START_RERANK" in response: | |
| app_state["mode"] = "AWAITING_REQUIREMENTS_FOR_RERANK" | |
| bot_message = "I can definitely help with that. Please provide the job requirements or the key skills you are looking for." | |
| history.append((None, bot_message)) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| else: | |
| history.append((None, response)) | |
| return history, app_state, gr.update(visible=False, file_count="single"), hide_examples | |
| # Default return | |
| return history, app_state, gr.update(visible=False, file_count="single"), gr.update() | |
| # --- 5. BUILD GRADIO UI --- | |
| with gr.Blocks(theme=gr.themes.Soft(), title="EduNatives Assistant") as demo: | |
| initial_state = { | |
| "mode": "GENERAL", "job_id": None, "cv_content": None, "cover_letter_content": None, | |
| "project_name": None, "description": None, "required_skills": None, "max_team_size": None, | |
| "rerank_requirements": None | |
| } | |
| application_state = gr.State(initial_state) | |
| gr.Markdown( | |
| """ | |
| # 🤖 EduNatives Assistant | |
| Ask me anything about jobs, projects, or student availability. I can also help you navigate the EduNatives app. | |
| """ | |
| ) | |
| chatbot_window = gr.Chatbot(height=450, label="Chat Window", bubble_full_width=False) | |
| with gr.Column() as examples_container: | |
| examples_list = [ | |
| "Analyze my CV", | |
| "Rerank CVs for a job", | |
| "I want to create a new project", | |
| "Who applied for job_022?", | |
| "Show me details about user_010" | |
| ] | |
| with gr.Row(): | |
| btn1 = gr.Button(examples_list[0], variant='secondary') | |
| btn2 = gr.Button(examples_list[1], variant='secondary') | |
| btn3 = gr.Button(examples_list[2], variant='secondary') | |
| with gr.Row(): | |
| btn4 = gr.Button(examples_list[3], variant='secondary') | |
| btn5 = gr.Button(examples_list[4], variant='secondary') | |
| example_buttons = [btn1, btn2, btn3, btn4, btn5] | |
| with gr.Row() as main_input_row: | |
| text_input = gr.Textbox(placeholder="Ask your question or try an example from above...", container=False, scale=7) | |
| submit_btn = gr.Button("Send", variant="primary", scale=1) | |
| file_uploader = gr.File(label="Upload Document(s)", file_types=['.pdf', '.docx', '.txt'], visible=False) | |
| outputs_list = [chatbot_window, application_state, file_uploader, examples_container] | |
| submit_btn.click( | |
| fn=chat_interface_func, | |
| inputs=[text_input, chatbot_window, application_state, file_uploader], | |
| outputs=outputs_list | |
| ) | |
| text_input.submit( | |
| fn=chat_interface_func, | |
| inputs=[text_input, chatbot_window, application_state, file_uploader], | |
| outputs=outputs_list | |
| ) | |
| for btn in example_buttons: | |
| def trigger_example(value): | |
| return value, [] | |
| btn.click( | |
| fn=trigger_example, | |
| inputs=btn, | |
| outputs=[text_input, chatbot_window] | |
| ).then( | |
| fn=chat_interface_func, | |
| inputs=[text_input, chatbot_window, application_state, file_uploader], | |
| outputs=outputs_list | |
| ) | |
| file_uploader.upload( | |
| fn=chat_interface_func, | |
| inputs=[gr.Textbox(value="", visible=False), chatbot_window, application_state, file_uploader], | |
| outputs=outputs_list | |
| ) | |
| submit_btn.click(lambda: "", outputs=text_input) | |
| text_input.submit(lambda: "", outputs=text_input) | |
| # --- 6. LAUNCH APP --- | |
| if __name__ == "__main__": | |
| demo.launch(debug=True) |