afouda's picture
Update app.py
2b2c2e9 verified
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)