Spaces:
Runtime error
Runtime error
File size: 43,434 Bytes
a9a9fa8 bca2990 bc7377c 958899e bca2990 78a5097 db05ab0 0601ab7 bca2990 bc7377c 0601ab7 bca2990 0f76b09 bca2990 78a5097 7095aef bca2990 78a5097 bca2990 398643a bca2990 78a5097 bca2990 78a5097 bca2990 398643a bca2990 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 0601ab7 bca2990 398643a bca2990 7095aef bca2990 7095aef 78a5097 20d800b bca2990 e01009a 78a5097 bca2990 18a171e 78a5097 bca2990 78a5097 bca2990 78a5097 bca2990 e466c11 78a5097 7095aef 78a5097 7095aef bca2990 78a5097 bca2990 78a5097 bca2990 78a5097 bca2990 78a5097 bca2990 78a5097 bca2990 78a5097 bca2990 78a5097 398643a bca2990 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 bca2990 78a5097 bca2990 0601ab7 bca2990 7095aef bca2990 7095aef bca2990 78a5097 bca2990 78a5097 398643a 78a5097 398643a 7095aef bca2990 398643a 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef 78a5097 bc7377c 7095aef bca2990 7095aef 78a5097 7095aef bca2990 78a5097 7095aef 78a5097 7095aef 78a5097 7095aef bc7377c bca2990 78a5097 bc7377c 78a5097 bc7377c 7095aef bca2990 7095aef 78a5097 7095aef bca2990 7095aef bca2990 78a5097 7095aef 78a5097 bca2990 bc7377c bca2990 78a5097 bca2990 7095aef 78a5097 bc7377c 7095aef bca2990 e01009a bca2990 e01009a bca2990 7095aef bca2990 7095aef bca2990 7095aef bca2990 e01009a 8d433bb bca2990 0601ab7 8d433bb 2b2c2e9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 | 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) |