diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,4 +1,4 @@
-# filename: app_openai_updated.py
+# filename: app_openai_no_serper.py
import gradio as gr
import pandas as pd
import numpy as np
@@ -10,7 +10,7 @@ import random
import json
import os
import time
-import requests
+import requests # Keep for potential future internal API calls if needed, but not for Serper
from typing import List, Dict, Any, Optional
import logging
from dotenv import load_dotenv
@@ -33,33 +33,36 @@ logging.basicConfig(level=logging.INFO,
logger = logging.getLogger(__name__)
# --- Configure API keys ---
-# Make sure you have OPENAI_API_KEY and SERPER_API_KEY in your .env file or environment
+# Make sure you have OPENAI_API_KEY in your .env file or environment
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
-SERPER_API_KEY = os.getenv("SERPER_API_KEY")
+# SERPER_API_KEY = os.getenv("SERPER_API_KEY") # Removed Serper key
if not OPENAI_API_KEY:
logger.warning("OPENAI_API_KEY not found. AI features will not work.")
- # You might want to raise an error or handle this case gracefully
-if not SERPER_API_KEY:
- logger.warning("SERPER_API_KEY not found. Web search features will not work.")
+# if not SERPER_API_KEY: # Removed Serper check
+# logger.warning("SERPER_API_KEY not found. Web search features will not work.")
# --- Initialize the OpenAI client ---
try:
client = openai.OpenAI(api_key=OPENAI_API_KEY)
- # Test connection (optional, uncomment to test during startup)
- # client.models.list()
logger.info("OpenAI client initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize OpenAI client: {e}")
- # Handle error appropriately, maybe exit or set client to None
client = None
# --- Model configuration ---
MODEL_ID = "gpt-4o" # Use OpenAI GPT-4o model
# --- Constants ---
-EMOTIONS = ["Unmotivated", "Anxious", "Confused", "Excited", "Overwhelmed", "Discouraged"]
-GOAL_TYPES = ["Get a job at a big company", "Find an internship", "Change careers", "Improve skills", "Network better"]
+EMOTIONS = ["Unmotivated π©", "Anxious π₯", "Confused π€", "Excited π", "Overwhelmed π€―", "Discouraged π"]
+# Added Emojis and changed label text
+GOAL_TYPES = [
+ "Get a job at a big company π’",
+ "Find an internship π",
+ "Change careers π",
+ "Improve skills π‘",
+ "Network better π€"
+]
USER_DB_PATH = "user_database.json"
RESUME_FOLDER = "user_resumes"
PORTFOLIO_FOLDER = "user_portfolios"
@@ -68,36 +71,12 @@ PORTFOLIO_FOLDER = "user_portfolios"
os.makedirs(RESUME_FOLDER, exist_ok=True)
os.makedirs(PORTFOLIO_FOLDER, exist_ok=True)
-# --- Tool Definitions for OpenAI ---
-# Define functions that the AI can call.
-# These will be implemented as Python functions below.
-
+# --- Tool Definitions for OpenAI (Removed Job Search Tool) ---
tools_list = [
- {
- "type": "function",
- "function": {
- "name": "get_job_opportunities",
- "description": "Search for relevant job opportunities based on query, location, and career goals using web search.",
- "parameters": {
- "type": "object",
- "properties": {
- "query": {
- "type": "string",
- "description": "The specific job title, keyword, or role the user is searching for.",
- },
- "location": {
- "type": "string",
- "description": "The city, region, or country where the user wants to search for jobs.",
- },
- "max_results": {
- "type": "integer",
- "description": "Maximum number of job opportunities to return (default 5).",
- },
- },
- "required": ["query", "location"],
- },
- }
- },
+ # { # Removed get_job_opportunities tool definition
+ # "type": "function",
+ # "function": { ... }
+ # },
{
"type": "function",
"function": {
@@ -133,7 +112,7 @@ tools_list = [
"properties": {
"emotion": {
"type": "string",
- "description": "User's current primary emotional state (e.g., Unmotivated, Anxious).",
+ "description": "User's current primary emotional state (e.g., Unmotivated, Anxious). Needs to be one of the predefined emotions.",
},
"goal": {
"type": "string",
@@ -221,45 +200,77 @@ tools_list = [
}
]
-# --- User Database Functions (Unchanged, adapted for history format if needed) ---
-# [Previous database functions load_user_database, save_user_database, get_user_profile, update_user_profile, etc. remain largely the same]
-# Ensure chat history format matches OpenAI's expected {role: 'user'/'assistant', content: 'message'}
-
+# --- User Database Functions ---
+# (Keep load_user_database, save_user_database, get_user_profile,
+# update_user_profile, add_task_to_user, add_emotion_record,
+# add_routine_to_user, save_user_resume, save_user_portfolio,
+# add_recommendation_to_user, add_chat_message - unchanged from previous corrected version)
def load_user_database():
"""Load user database from JSON file or create if it doesn't exist"""
try:
- with open(USER_DB_PATH, 'r') as file:
+ # Ensure correct encoding for wider compatibility
+ with open(USER_DB_PATH, 'r', encoding='utf-8') as file:
db = json.load(file)
- # Ensure chat history uses 'content' key for OpenAI compatibility
+ # Validate and fix chat history structure
for user_id in db.get('users', {}):
- if 'chat_history' not in db['users'][user_id]:
- db['users'][user_id]['chat_history'] = []
+ profile = db['users'][user_id]
+ if 'chat_history' not in profile or not isinstance(profile['chat_history'], list):
+ profile['chat_history'] = []
else:
- # Convert old format if necessary
- for msg in db['users'][user_id]['chat_history']:
- if 'message' in msg and 'content' not in msg:
- msg['content'] = msg.pop('message')
+ fixed_history = []
+ for msg in profile['chat_history']:
+ if isinstance(msg, dict) and 'role' in msg and 'content' in msg:
+ # Ensure content is string for user/assistant if not None
+ if msg['role'] in ['user', 'assistant'] and msg['content'] is not None and not isinstance(msg['content'], str):
+ msg['content'] = str(msg['content'])
+ fixed_history.append(msg)
+ elif isinstance(msg, dict) and msg.get('role') == 'tool' and all(k in msg for k in ['tool_call_id', 'name', 'content']):
+ # Ensure tool content is string
+ if not isinstance(msg['content'], str):
+ msg['content'] = json.dumps(msg['content']) if msg['content'] is not None else ""
+ fixed_history.append(msg)
+ else:
+ # Attempt to fix older formats or log invalid message
+ if isinstance(msg, dict) and 'message' in msg and 'role' in msg:
+ msg['content'] = str(msg.pop('message'))
+ fixed_history.append(msg)
+ else:
+ logger.warning(f"Skipping invalid chat message structure for user {user_id}: {msg}")
+ profile['chat_history'] = fixed_history
+
+ # Ensure recommendations is a list
+ if 'recommendations' not in profile or not isinstance(profile['recommendations'], list):
+ profile['recommendations'] = []
+
return db
except (FileNotFoundError, json.JSONDecodeError):
+ logger.info(f"Database file '{USER_DB_PATH}' not found or invalid. Creating new one.")
db = {'users': {}}
save_user_database(db)
return db
+ except Exception as e:
+ logger.error(f"Error loading user database from {USER_DB_PATH}: {e}")
+ return {'users': {}} # Return empty DB on critical error
def save_user_database(db):
"""Save user database to JSON file"""
- with open(USER_DB_PATH, 'w') as file:
- json.dump(db, file, indent=4)
+ try:
+ with open(USER_DB_PATH, 'w', encoding='utf-8') as file:
+ json.dump(db, file, indent=4, ensure_ascii=False)
+ except Exception as e:
+ logger.error(f"Error saving user database to {USER_DB_PATH}: {e}")
def get_user_profile(user_id):
"""Get user profile from database or create new one"""
db = load_user_database()
- if user_id not in db['users']:
+ if user_id not in db.get('users', {}):
+ db['users'] = db.get('users', {}) # Ensure 'users' key exists
db['users'][user_id] = {
"user_id": user_id,
"name": "",
"location": "",
"current_emotion": "",
- "career_goal": "",
+ "career_goal": "", # Renamed from career_goal in UI, but keep key consistent internally for now
"progress_points": 0,
"completed_tasks": [],
"upcoming_events": [],
@@ -269,81 +280,97 @@ def get_user_profile(user_id):
"portfolio_path": "",
"recommendations": [],
"chat_history": [], # Initialize chat history
- "joined_date": datetime.now().strftime("%Y-%m-%d")
+ "joined_date": datetime.now().isoformat()
}
save_user_database(db)
- # Ensure chat history uses 'content' key
- elif 'chat_history' not in db['users'][user_id] or \
- (db['users'][user_id]['chat_history'] and 'content' not in db['users'][user_id]['chat_history'][0]):
- if 'chat_history' not in db['users'][user_id]:
- db['users'][user_id]['chat_history'] = []
- else:
- for msg in db['users'][user_id]['chat_history']:
- if 'message' in msg and 'content' not in msg:
- msg['content'] = msg.pop('message')
- save_user_database(db)
-
- return db['users'][user_id]
+ # Validate essential lists exist
+ profile = db.get('users', {}).get(user_id, {})
+ if 'chat_history' not in profile or not isinstance(profile.get('chat_history'), list):
+ profile['chat_history'] = []
+ # save_user_database(db) # Avoid saving just for this check unless needed
+ if 'recommendations' not in profile or not isinstance(profile.get('recommendations'), list):
+ profile['recommendations'] = []
+ if 'daily_emotions' not in profile or not isinstance(profile.get('daily_emotions'), list):
+ profile['daily_emotions'] = []
+ if 'completed_tasks' not in profile or not isinstance(profile.get('completed_tasks'), list):
+ profile['completed_tasks'] = []
+ if 'routine_history' not in profile or not isinstance(profile.get('routine_history'), list):
+ profile['routine_history'] = []
+
+
+ return profile
def update_user_profile(user_id, updates):
"""Update user profile with new information"""
db = load_user_database()
- if user_id in db['users']:
+ if user_id in db.get('users', {}):
+ profile = db['users'][user_id]
for key, value in updates.items():
- db['users'][user_id][key] = value
+ profile[key] = value
save_user_database(db)
- return db['users'][user_id]
+ return profile
+ else:
+ logger.warning(f"Attempted to update non-existent user profile: {user_id}")
+ return None
def add_task_to_user(user_id, task):
"""Add a new task to user's completed tasks"""
db = load_user_database()
- if user_id in db['users']:
- if 'completed_tasks' not in db['users'][user_id]:
- db['users'][user_id]['completed_tasks'] = []
+ profile = db.get('users', {}).get(user_id)
+ if profile:
+ if 'completed_tasks' not in profile or not isinstance(profile['completed_tasks'], list):
+ profile['completed_tasks'] = []
task_with_date = {
"task": task,
- "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ "date": datetime.now().isoformat() # Use ISO format
}
- db['users'][user_id]['completed_tasks'].append(task_with_date)
- db['users'][user_id]['progress_points'] += random.randint(10, 25) # Keep random points for now
+ profile['completed_tasks'].append(task_with_date)
+ profile['progress_points'] = profile.get('progress_points', 0) + random.randint(10, 25)
save_user_database(db)
- return db['users'][user_id]
+ return profile
+ return None
def add_emotion_record(user_id, emotion):
"""Add a new emotion record to user's daily emotions"""
+ cleaned_emotion = emotion.split(" ")[0] if " " in emotion else emotion
db = load_user_database()
- if user_id in db['users']:
- if 'daily_emotions' not in db['users'][user_id]:
- db['users'][user_id]['daily_emotions'] = []
+ profile = db.get('users', {}).get(user_id)
+ if profile:
+ if 'daily_emotions' not in profile or not isinstance(profile['daily_emotions'], list):
+ profile['daily_emotions'] = []
emotion_record = {
- "emotion": emotion,
- "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ "emotion": cleaned_emotion, # Store cleaned emotion
+ "date": datetime.now().isoformat() # Use ISO format
}
- db['users'][user_id]['daily_emotions'].append(emotion_record)
- db['users'][user_id]['current_emotion'] = emotion # Update current emotion
+ profile['daily_emotions'].append(emotion_record)
+ profile['current_emotion'] = cleaned_emotion
save_user_database(db)
- return db['users'][user_id]
+ return profile
+ return None
def add_routine_to_user(user_id, routine):
"""Add a new routine to user's routine history"""
db = load_user_database()
- if user_id in db['users']:
- if 'routine_history' not in db['users'][user_id]:
- db['users'][user_id]['routine_history'] = []
-
+ profile = db.get('users', {}).get(user_id)
+ if profile:
+ if 'routine_history' not in profile or not isinstance(profile['routine_history'], list):
+ profile['routine_history'] = []
+ try:
+ days_delta = int(routine.get('days', 7))
+ except (ValueError, TypeError): days_delta = 7
+ end_date = (datetime.now() + timedelta(days=days_delta)).isoformat()
routine_with_date = {
- "routine": routine, # The AI generated routine JSON
- "start_date": datetime.now().strftime("%Y-%m-%d"),
- "end_date": (datetime.now() + timedelta(days=routine.get('days', 7))).strftime("%Y-%m-%d"),
- "completion": 0 # Start completion at 0
+ "routine": routine,
+ "start_date": datetime.now().isoformat(),
+ "end_date": end_date, "completion": 0
}
- # Prepend to make the latest routine first (optional)
- db['users'][user_id]['routine_history'].insert(0, routine_with_date)
+ profile['routine_history'].insert(0, routine_with_date)
+ profile['routine_history'] = profile['routine_history'][:10]
save_user_database(db)
- return db['users'][user_id]
-
+ return profile
+ return None
def save_user_resume(user_id, resume_text):
"""Save user's resume text to file and update profile path."""
@@ -351,8 +378,7 @@ def save_user_resume(user_id, resume_text):
filename = f"{user_id}_resume.txt"
filepath = os.path.join(RESUME_FOLDER, filename)
try:
- with open(filepath, 'w', encoding='utf-8') as file:
- file.write(resume_text)
+ with open(filepath, 'w', encoding='utf-8') as file: file.write(resume_text)
update_user_profile(user_id, {"resume_path": filepath})
logger.info(f"Resume saved for user {user_id} at {filepath}")
return filepath
@@ -365,14 +391,9 @@ def save_user_portfolio(user_id, portfolio_url, portfolio_description):
if not portfolio_description: return None
filename = f"{user_id}_portfolio.json"
filepath = os.path.join(PORTFOLIO_FOLDER, filename)
- portfolio_content = {
- "url": portfolio_url,
- "description": portfolio_description,
- "saved_date": datetime.now().isoformat()
- }
+ portfolio_content = {"url": portfolio_url, "description": portfolio_description, "saved_date": datetime.now().isoformat()}
try:
- with open(filepath, 'w', encoding='utf-8') as file:
- json.dump(portfolio_content, file, indent=4)
+ with open(filepath, 'w', encoding='utf-8') as file: json.dump(portfolio_content, file, indent=4, ensure_ascii=False)
update_user_profile(user_id, {"portfolio_path": filepath})
logger.info(f"Portfolio info saved for user {user_id} at {filepath}")
return filepath
@@ -380,326 +401,226 @@ def save_user_portfolio(user_id, portfolio_url, portfolio_description):
logger.error(f"Error saving portfolio info for user {user_id}: {e}")
return None
-
def add_recommendation_to_user(user_id, recommendation):
"""Add a new recommendation object to user's list"""
db = load_user_database()
- if user_id in db['users']:
- if 'recommendations' not in db['users'][user_id]:
- db['users'][user_id]['recommendations'] = []
-
- recommendation_with_date = {
- "recommendation": recommendation, # The AI generated recommendation object
- "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- "status": "pending" # pending, completed, dismissed
- }
- # Add to the beginning of the list
- db['users'][user_id]['recommendations'].insert(0, recommendation_with_date)
- # Optional: Limit the number of stored recommendations
- max_recs = 20
- if len(db['users'][user_id]['recommendations']) > max_recs:
- db['users'][user_id]['recommendations'] = db['users'][user_id]['recommendations'][:max_recs]
-
+ profile = db.get('users', {}).get(user_id)
+ if profile:
+ if 'recommendations' not in profile or not isinstance(profile['recommendations'], list): profile['recommendations'] = []
+ recommendation_with_date = {"recommendation": recommendation, "date": datetime.now().isoformat(), "status": "pending"}
+ profile['recommendations'].insert(0, recommendation_with_date)
+ profile['recommendations'] = profile['recommendations'][:20]
save_user_database(db)
- return db['users'][user_id]
+ return profile
+ return None
def add_chat_message(user_id, role, content):
"""Add a message to the user's chat history using OpenAI format."""
db = load_user_database()
- if user_id in db['users']:
- if 'chat_history' not in db['users'][user_id]:
- db['users'][user_id]['chat_history'] = []
-
- # Basic validation
+ profile = db.get('users', {}).get(user_id)
+ if profile:
+ if 'chat_history' not in profile or not isinstance(profile['chat_history'], list): profile['chat_history'] = []
if role not in ['user', 'assistant', 'system', 'tool']:
- logger.warning(f"Invalid role '{role}' provided for chat message.")
- return db['users'][user_id]
- if not content and role != 'tool': # Tool messages can have null content initially
- logger.warning(f"Empty content provided for chat role '{role}'.")
- # return db['users'][user_id] # Allow empty content for now?
-
- chat_message = {
- "role": role,
- "content": content, # Use 'content' key
- "timestamp": datetime.now().isoformat() # Use ISO format
- }
- db['users'][user_id]['chat_history'].append(chat_message)
-
- # Optional: Limit chat history length
- max_history = 50 # Keep last 50 messages (user + assistant)
- if len(db['users'][user_id]['chat_history']) > max_history:
- # Keep system prompt + last N messages
- system_msgs = [m for m in db['users'][user_id]['chat_history'] if m['role'] == 'system']
- other_msgs = [m for m in db['users'][user_id]['chat_history'] if m['role'] != 'system']
- db['users'][user_id]['chat_history'] = system_msgs + other_msgs[-max_history:]
-
+ logger.warning(f"Invalid role '{role}' provided for chat message."); return profile
+ # Allow None content for assistant (tool calls) and stringify tool content if needed
+ if role == 'tool' and content is not None and not isinstance(content, str):
+ content = json.dumps(content)
+ elif role == 'assistant' and content is None:
+ content = "" # Store empty string instead of None for assistant text content
+ elif not content and role == 'user':
+ logger.warning(f"Empty content provided for chat role 'user'. Skipping save."); #return profile # Skip saving empty user messages
+
+ chat_message = {"role": role, "content": content, "timestamp": datetime.now().isoformat()}
+ profile['chat_history'].append(chat_message)
+ # Limit history
+ max_history = 50
+ if len(profile['chat_history']) > max_history:
+ system_msgs = [m for m in profile['chat_history'] if m['role'] == 'system']
+ other_msgs = [m for m in profile['chat_history'] if m['role'] != 'system']
+ profile['chat_history'] = system_msgs + other_msgs[-max_history:]
save_user_database(db)
- return db['users'][user_id]
+ return profile
+ return None
+
+
+# --- Basic Routine Fallback Function ---
+def generate_basic_routine(emotion, goal, available_time=60, days=7):
+ """Generate a basic routine as fallback."""
+ logger.info(f"Generating basic fallback routine for emotion={emotion}, goal={goal}")
+ routine_types = {
+ "job_search": [
+ {"name": "Research Target Companies", "points": 15, "duration": 20, "description": "Identify 3 potential employers aligned with your goal."},
+ {"name": "Update LinkedIn Section", "points": 15, "duration": 25, "description": "Refine one section of your LinkedIn profile (e.g., summary, experience)."},
+ {"name": "Practice STAR Method", "points": 20, "duration": 15, "description": "Outline one experience using the STAR method for interviews."},
+ {"name": "Find Networking Event", "points": 10, "duration": 10, "description": "Look for one relevant online or local networking event."}
+ ],
+ "skill_building": [
+ {"name": "Online Tutorial (1 Module)", "points": 25, "duration": 45, "description": "Complete one module of a relevant online course/tutorial."},
+ {"name": "Read Industry Blog/Article", "points": 10, "duration": 15, "description": "Read and summarize one article about trends in your field."},
+ {"name": "Small Project Task", "points": 30, "duration": 60, "description": "Dedicate time to a specific task within a personal project."},
+ {"name": "Review Skill Documentation", "points": 15, "duration": 30, "description": "Read documentation or examples for a skill you're learning."}
+ ],
+ "motivation_wellbeing": [
+ {"name": "Mindful Reflection", "points": 10, "duration": 10, "description": "Spend 10 minutes reflecting on progress and challenges without judgment."},
+ {"name": "Set 1-3 Daily Intentions", "points": 10, "duration": 5, "description": "Define small, achievable goals for the day."},
+ {"name": "Short Break/Walk", "points": 15, "duration": 15, "description": "Take a brief break away from screens, preferably with light movement."},
+ {"name": "Connect with Support", "points": 20, "duration": 20, "description": "Briefly chat with a friend, mentor, or peer about your journey."}
+ ]
+ }
+ cleaned_emotion = emotion.split(" ")[0].lower() if " " in emotion else emotion.lower()
+ negative_emotions = ["unmotivated", "anxious", "confused", "overwhelmed", "discouraged"]
+ if "job" in goal.lower() or "internship" in goal.lower() or "company" in goal.lower(): base_type = "job_search"
+ elif "skill" in goal.lower() or "learn" in goal.lower(): base_type = "skill_building"
+ elif "network" in goal.lower(): base_type = "job_search"
+ else: base_type = "skill_building"
+ include_wellbeing = cleaned_emotion in negative_emotions
+ daily_tasks_list = []
+ for day in range(1, days + 1):
+ day_tasks, remaining_time, tasks_added_count = [], available_time, 0
+ possible_tasks = routine_types[base_type].copy()
+ if include_wellbeing: possible_tasks.extend(routine_types["motivation_wellbeing"])
+ random.shuffle(possible_tasks)
+ for task in possible_tasks:
+ if task["duration"] <= remaining_time and tasks_added_count < 3:
+ day_tasks.append(task); remaining_time -= task["duration"]; tasks_added_count += 1
+ if remaining_time < 10 or tasks_added_count >= 3: break
+ daily_tasks_list.append({"day": day, "tasks": day_tasks})
+ routine = {"name": f"{days}-Day Focus Plan", "description": f"A basic {days}-day plan focusing on '{goal}' while acknowledging feeling {cleaned_emotion}.", "days": days, "daily_tasks": daily_tasks_list}
+ return routine
# --- Tool Implementation Functions ---
-# These functions are called when the AI decides to use a tool.
-
-def get_job_opportunities(query: str, location: str, max_results: int = 5) -> str:
- """
- Searches for job opportunities using the Serper API based on a query and location.
- Returns a JSON string of the search results or an error message.
- """
- logger.info(f"Executing tool: get_job_opportunities(query='{query}', location='{location}', max_results={max_results})")
- if not SERPER_API_KEY:
- return json.dumps({"error": "Serper API key is not configured."})
+# (Keep generate_document_template, create_personalized_routine,
+# analyze_resume, analyze_portfolio, extract_and_rate_skills_from_resume - unchanged from previous corrected version)
+# Note: get_job_opportunities function is now removed.
- try:
- headers = {
- 'X-API-KEY': SERPER_API_KEY,
- 'Content-Type': 'application/json'
- }
- params = {
- 'q': f"{query} jobs in {location}",
- 'num': max_results,
- 'location': location # Add location parameter explicitly if API supports it
- }
- logger.info(f"Calling Serper API with params: {params}")
- response = requests.get(
- 'https://serper.dev/search', # Use the correct Serper endpoint
- headers=headers,
- params=params,
- timeout=10 # Add a timeout
- )
- response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
-
- data = response.json()
- logger.info(f"Serper API response received (keys: {data.keys()})")
-
- # Extract relevant job listings (adapt based on Serper's actual output structure)
- job_results = []
- # Check 'jobs' key first, as it's common in job search results
- if 'jobs' in data and isinstance(data['jobs'], list):
- for item in data['jobs']:
- job_results.append({
- 'title': item.get('title', 'N/A'),
- 'company': item.get('company_name', item.get('source', 'Unknown Company')), # Try different fields
- 'description': item.get('description', item.get('snippet', 'No description provided.')),
- 'link': item.get('link', '#'),
- 'location': item.get('location', location), # Use provided location if not in result
- 'date_posted': item.get('detected_extensions', {}).get('posted_at', 'N/A') # Example nested field
- })
- # Fallback to organic results if 'jobs' key is not present or empty
- elif 'organic' in data and not job_results:
- logger.info("Parsing 'organic' results for jobs.")
- for item in data['organic']:
- # Heuristic check if it looks like a job listing
- title = item.get('title', '')
- snippet = item.get('snippet', '')
- if any(keyword in title.lower() for keyword in ['job', 'career', 'hiring', 'position', 'vacancy']) or \
- any(keyword in snippet.lower() for keyword in ['apply', 'responsibilities', 'qualifications']):
- job_results.append({
- 'title': title,
- 'company': item.get('source', extract_company_from_title(title)), # Use source or extract
- 'description': snippet,
- 'link': item.get('link', '#'),
- 'location': location, # Serper organic results might not specify location clearly
- 'date_posted': 'Recent' # Often not available in organic results
- })
-
- if not job_results:
- logger.warning(f"No job results extracted from Serper response for query '{query}' in '{location}'.")
- return json.dumps({"message": "No job opportunities found for your query.", "results": []})
-
- logger.info(f"Extracted {len(job_results)} job results.")
- # Return results as a JSON string for the AI
- return json.dumps({"message": f"Found {len(job_results)} potential job opportunities.", "results": job_results})
-
- except requests.exceptions.RequestException as e:
- logger.error(f"Error calling Serper API: {e}")
- return json.dumps({"error": f"Could not connect to job search service: {e}"})
- except Exception as e:
- logger.error(f"Exception in get_job_opportunities tool: {e}")
- return json.dumps({"error": f"An unexpected error occurred during job search: {e}"})
-
-def extract_company_from_title(title):
- """Simple helper to guess company name from job title string."""
- # Improved heuristic
- delimiters = [' at ', ' - ', ' | ', ' hiring ', ' for ']
- for delim in delimiters:
- if delim in title:
- parts = title.split(delim)
- # Take the part after the delimiter, unless it looks like a job title itself
- potential_company = parts[-1].strip()
- if len(potential_company) > 1 and not any(kw in potential_company.lower() for kw in ['developer', 'manager', 'engineer', 'analyst']):
- return potential_company
- # If no delimiter found or extraction failed, return default
- return "Unknown Company"
-
-
-# --- Implement other tool functions ---
def generate_document_template(document_type: str, career_field: str = "", experience_level: str = "") -> str:
"""Generates a basic markdown template for the specified document type."""
logger.info(f"Executing tool: generate_document_template(document_type='{document_type}', career_field='{career_field}', experience_level='{experience_level}')")
- # This function *could* call the AI again for a more detailed template,
- # but for simplicity, we'll return a predefined basic structure here.
- # A real implementation would likely use the AI.
template = f"## Basic Template: {document_type}\n\n"
template += f"**Target Field:** {career_field or 'Not specified'}\n"
- template += f"**Experience Level:** {experience_level or 'Not specified'}\n\n"
-
+ template += f"**Experience Level:** {experience_level or 'Not specified'}\n\n---\n\n"
if "resume" in document_type.lower():
- template += (
- "### Contact Information\n"
- "- Name:\n- Phone:\n- Email:\n- LinkedIn:\n- Portfolio (Optional):\n\n"
- "### Summary/Objective\n"
- "- [Write 2-3 sentences summarizing your key skills and career goals relevant to the target field/job]\n\n"
- "### Experience\n"
- "- **Company Name** | Location | Job Title | Start Date - End Date\n"
- " - [Quantifiable achievement 1 using action verbs]\n"
- " - [Quantifiable achievement 2 using action verbs]\n\n"
- "### Education\n"
- "- University Name | Degree | Graduation Date\n\n"
- "### Skills\n"
- "- Technical Skills: [List relevant software, tools, languages]\n"
- "- Soft Skills: [List relevant interpersonal skills]\n"
- )
+ template += ("### Contact Information\n- Name:\n- Phone:\n- Email:\n- LinkedIn URL:\n- Portfolio URL (Optional):\n\n"
+ "### Summary/Objective\n_[ 2-3 sentences summarizing your key skills, experience, and career goals, tailored to the job/field. ]_\n\n"
+ "### Experience\n**Company Name** | Location | Job Title | _Start Date β End Date_\n- Accomplishment 1 (Use action verbs and quantify results, e.g., 'Increased sales by 15%...')\n- Accomplishment 2\n\n_[ Repeat for other relevant positions ]_\n\n"
+ "### Education\n**University/Institution Name** | Degree | _Graduation Date (or Expected)_\n- Relevant coursework, honors, activities (Optional)\n\n"
+ "### Skills\n- **Technical Skills:** [ e.g., Python, Java, SQL, MS Excel, Google Analytics ]\n- **Languages:** [ e.g., English (Native), Spanish (Fluent) ]\n- **Other:** [ Certifications, relevant tools ]\n")
elif "cover letter" in document_type.lower():
- template += (
- "[Your Name]\n[Your Address]\n[Your Phone]\n[Your Email]\n\n"
- "[Date]\n\n"
- "[Hiring Manager Name (if known), or Title]\n[Company Name]\n[Company Address]\n\n"
- "Dear [Mr./Ms./Mx. Hiring Manager Last Name or Hiring Team],\n\n"
- "**Introduction:** [State the position you're applying for and where you saw it. Briefly mention your key qualification or enthusiasm.]\n\n"
- "**Body Paragraph(s):** [Connect your skills and experience directly to the job requirements. Provide specific examples. Explain why you are interested in this company and role.]\n\n"
- "**Conclusion:** [Reiterate your interest and key qualification. State your call to action (e.g., looking forward to discussing). Thank the reader.]\n\n"
- "Sincerely,\n[Your Name]"
- )
+ template += ("[Your Name]\n[Your Address]\n[Your Phone]\n[Your Email]\n\n"
+ "[Date]\n\n"
+ "[Hiring Manager Name (if known), or 'Hiring Team']\n[Hiring Manager Title (if known)]\n[Company Name]\n[Company Address]\n\n"
+ "**Subject: Application for [Job Title] Position - [Your Name]**\n\n"
+ "Dear [Mr./Ms./Mx. Last Name or Hiring Team],\n\n"
+ "**Introduction:** State the position you are applying for and where you saw the advertisement. Briefly express your enthusiasm for the role and the company. Mention 1-2 key qualifications that make you a strong fit.\n_[ Example: I am writing to express my strong interest in the [Job Title] position advertised on [Platform]. With my background in [Relevant Field] and proven ability to [Key Skill], I am confident I possess the skills and experience necessary to excel in this role and contribute significantly to [Company Name]. ]_\n\n"
+ "**Body Paragraph(s):** Elaborate on your qualifications and experiences, directly addressing the requirements listed in the job description. Provide specific examples (using the STAR method implicitly can be effective). Explain why you are interested in *this specific* company and role. Show you've done your research.\n_[ Example: In my previous role at [Previous Company], I was responsible for [Responsibility relevant to new job]. I successfully [Quantifiable achievement relevant to new job], demonstrating my ability to [Skill required by new job]. I am particularly drawn to [Company Name]'s work in [Specific area company works in], as described in [Source, e.g., recent news, company website], and I believe my [Relevant skill/experience] would be a valuable asset to your team. ]_\n\n"
+ "**Conclusion:** Reiterate your strong interest and suitability for the role. Briefly summarize your key strengths. State your call to action (e.g., "I am eager to discuss my qualifications further..."). Thank the reader for their time and consideration.\n_[ Example: Thank you for considering my application. My resume provides further detail on my qualifications. I am excited about the opportunity to contribute to [Company Name] and look forward to hearing from you soon. ]_\n\n"
+ "Sincerely,\n\n[Your Typed Name]")
+ elif "linkedin summary" in document_type.lower():
+ template += ("### LinkedIn Summary/About Section Template\n\n"
+ "**Headline:** [ A concise, keyword-rich description of your professional identity, e.g., 'Software Engineer specializing in AI | Python | Cloud Computing | Seeking Innovative Opportunities' ]\n\n"
+ "**About Section:**\n"
+ "_[ Paragraph 1: Hook & Overview. Start with a compelling statement about your passion, expertise, or career mission. Briefly introduce who you are professionally and your main areas of focus. Use keywords relevant to your field and desired roles. ]_\n\n"
+ "_[ Paragraph 2: Key Skills & Experience Highlights. Detail your core competencies and technical/soft skills. Mention key experiences or types of projects you've worked on. Quantify achievements where possible. Tailor this to the audience you want to attract (recruiters, clients, peers). ]_\n\n"
+ "_[ Paragraph 3: Career Goals & What You're Seeking (Optional but recommended). Briefly state your career aspirations or the types of opportunities, connections, or collaborations you are looking for. ]_\n\n"
+ "_[ Paragraph 4: Call to Action / Personality (Optional). You might end with an invitation to connect, mention personal interests related to your field, or add a touch of personality. ]_\n\n"
+ "**Specialties/Keywords:** [ List 5-10 key terms related to your skills and industry, e.g., Project Management, Data Analysis, Agile Methodologies, Content Strategy, Java, Cloud Security ]")
else:
- template += "[Structure for this document type needs to be defined.]"
+ template += "_[ Basic structure for this document type will be provided here. ]_"
+
+ # Return as JSON string, even though AI might generate markdown directly
+ return json.dumps({"template_markdown": template})
- return json.dumps({"template_markdown": template}) # Return as JSON string
def create_personalized_routine(emotion: str, goal: str, available_time_minutes: int = 60, routine_length_days: int = 7) -> str:
- """Creates a basic personalized routine structure."""
+ """Creates a personalized routine, falling back to basic generation if needed."""
logger.info(f"Executing tool: create_personalized_routine(emotion='{emotion}', goal='{goal}', time={available_time_minutes}, days={routine_length_days})")
- # Similar to template generation, this could call the AI for a detailed plan.
- # Here, we generate a basic fallback structure.
- # A real implementation should use the AI for better personalization.
- routine = generate_basic_routine(emotion, goal, available_time_minutes, routine_length_days) # Use the existing fallback
- logger.info(f"Generated basic routine: {routine['name']}")
- # Add routine to user profile
- # user_profile = add_routine_to_user(session_user_id, routine) # Need user_id here! Pass it if possible.
- # For now, just return the routine structure. The main chat logic should handle saving it.
- return json.dumps(routine) # Return JSON string
+ try:
+ logger.warning("create_personalized_routine tool is using the basic fallback generation.")
+ routine = generate_basic_routine(emotion, goal, available_time_minutes, routine_length_days)
+ if not routine: raise ValueError("Basic routine generation failed.")
+ logger.info(f"Generated routine: {routine.get('name', 'Unnamed Routine')}")
+ return json.dumps(routine)
+ except Exception as e:
+ logger.error(f"Error in create_personalized_routine tool: {e}")
+ try: # Attempt fallback again
+ routine = generate_basic_routine(emotion, goal, available_time_minutes, routine_length_days)
+ return json.dumps(routine) if routine else json.dumps({"error": "Failed to generate routine."})
+ except Exception as fallback_e:
+ logger.error(f"Fallback routine generation also failed: {fallback_e}")
+ return json.dumps({"error": f"Failed to generate routine: {e}"})
+
def analyze_resume(resume_text: str, career_goal: str) -> str:
- """Provides a basic analysis structure for the resume."""
+ """Provides analysis of the resume using AI (Simulated)."""
logger.info(f"Executing tool: analyze_resume(career_goal='{career_goal}', resume_length={len(resume_text)})")
- # This should ideally call the AI for actual analysis.
- # Returning a placeholder structure for now.
- analysis = {
- "strengths": ["Identified strength 1 based on AI analysis (placeholder).", "Identified strength 2 (placeholder)."],
- "areas_for_improvement": ["Suggestion 1 for improvement (placeholder).", "Suggestion 2 based on goal alignment (placeholder)."],
- "format_feedback": "General feedback on format (placeholder).",
- "content_feedback": f"Feedback on content relevance to '{career_goal}' (placeholder).",
- "next_steps": ["Recommended action 1 (placeholder).", "Recommended action 2 (placeholder)."]
- }
- # Save the resume text (need user_id)
- # save_user_resume(session_user_id, resume_text) # Pass user_id if available
- return json.dumps({"analysis": analysis}) # Return JSON string
+ logger.warning("analyze_resume tool is using placeholder analysis.")
+ analysis = { "analysis": { "strengths": ["Placeholder: Clear objective/summary.", "Placeholder: Good use of action verbs."], "areas_for_improvement": ["Placeholder: Quantify achievements more.", f"Placeholder: Tailor skills section better for '{career_goal}'."], "format_feedback": "Placeholder: Overall format is clean, but consider standardizing date formats.", "content_feedback": f"Placeholder: Experience seems partially relevant to '{career_goal}', but highlight transferable skills.", "keyword_suggestions": ["Placeholder: Add keywords like 'Keyword1', 'Keyword2' relevant to goal."], "next_steps": ["Placeholder: Refine descriptions for last 2 roles.", "Placeholder: Add a project section if applicable."] } }
+ return json.dumps(analysis)
def analyze_portfolio(portfolio_description: str, career_goal: str, portfolio_url: str = "") -> str:
- """Provides a basic analysis structure for the portfolio."""
+ """Provides analysis of the portfolio using AI (Simulated)."""
logger.info(f"Executing tool: analyze_portfolio(career_goal='{career_goal}', url='{portfolio_url}', desc_length={len(portfolio_description)})")
- # Placeholder analysis
- analysis = {
- "alignment_with_goal": f"Assessment of alignment with '{career_goal}' (placeholder).",
- "strengths": ["Portfolio strength 1 (placeholder).", "Portfolio strength 2 (placeholder)."],
- "areas_for_improvement": ["Suggestion 1 for portfolio enhancement (placeholder)."],
- "presentation_feedback": "Feedback on presentation/UX (placeholder).",
- "next_steps": ["Recommended action for portfolio (placeholder)."]
- }
- # Save portfolio info (need user_id)
- # save_user_portfolio(session_user_id, portfolio_url, portfolio_description) # Pass user_id if available
- return json.dumps({"analysis": analysis}) # Return JSON string
-
+ logger.warning("analyze_portfolio tool is using placeholder analysis.")
+ analysis = { "analysis": { "alignment_with_goal": f"Placeholder: Portfolio description suggests moderate alignment with '{career_goal}'.", "strengths": ["Placeholder: Variety of projects mentioned.", "Placeholder: Clear description provided."], "areas_for_improvement": ["Placeholder: Ensure project descriptions explicitly link to skills needed for the goal.", "Placeholder: Consider adding testimonials or case study depth."], "presentation_feedback": f"Placeholder: If URL ({portfolio_url}) provided, check for mobile responsiveness and clear navigation (visual analysis needed). Based on description, sounds organized.", "next_steps": ["Placeholder: Select 2-3 best projects strongly related to the goal and feature them prominently.", "Placeholder: Get feedback from peers in the target field."] } }
+ return json.dumps(analysis)
def extract_and_rate_skills_from_resume(resume_text: str, max_skills: int = 8) -> str:
- """
- Placeholder function to simulate skill extraction and rating.
- In a real scenario, this would involve more sophisticated NLP or another AI call.
- """
+ """Extracts and rates skills from resume text (Simulated)."""
logger.info(f"Executing tool: extract_and_rate_skills_from_resume(resume_length={len(resume_text)}, max_skills={max_skills})")
-
- # Simple keyword spotting for demonstration
- possible_skills = ["Python", "Java", "Project Management", "Communication", "Data Analysis", "Teamwork", "Leadership", "SQL", "React", "Customer Service", "Problem Solving", "Microsoft Office"]
+ logger.warning("extract_and_rate_skills_from_resume tool is using placeholder extraction.")
+ possible_skills = ["Python", "Java", "JavaScript", "Project Management", "Communication", "Data Analysis", "Teamwork", "Leadership", "SQL", "React", "Customer Service", "Problem Solving", "Cloud Computing (AWS/Azure/GCP)", "Agile Methodologies", "Machine Learning"]
found_skills = []
resume_lower = resume_text.lower()
for skill in possible_skills:
- if skill.lower() in resume_lower:
- # Assign a random score for demonstration
+ if re.search(r'\b' + re.escape(skill.lower()) + r'\b', resume_lower):
found_skills.append({"name": skill, "score": random.randint(4, 9)})
- if len(found_skills) >= max_skills:
- break
-
- # Ensure we return *some* skills if none automatically found
- if not found_skills:
- found_skills = [
- {"name": "Communication", "score": random.randint(5,8)},
- {"name": "Teamwork", "score": random.randint(5,8)},
- {"name": "Problem Solving", "score": random.randint(5,8)},
- ]
-
-
+ if len(found_skills) >= max_skills: break
+ if not found_skills and len(resume_text) > 50:
+ found_skills = [ {"name": "Communication", "score": random.randint(5,8)}, {"name": "Teamwork", "score": random.randint(5,8)}, {"name": "Problem Solving", "score": random.randint(5,8)}, ]
logger.info(f"Extracted skills (placeholder): {[s['name'] for s in found_skills]}")
- return json.dumps({"skills": found_skills[:max_skills]}) # Return JSON string
+ return json.dumps({"skills": found_skills[:max_skills]})
# --- AI Interaction Logic (Using OpenAI) ---
-
def get_ai_response(user_id: str, user_input: str, generate_recommendations: bool = True) -> str:
- """
- Gets a response from the OpenAI API, handling context, system prompt, and tool calls.
- """
+ """Gets response from OpenAI, handling context, system prompt, and tool calls."""
logger.info(f"Getting AI response for user {user_id}. Input: '{user_input[:100]}...'")
if not client:
return "I apologize, the AI service is currently unavailable. Please check the configuration."
try:
user_profile = get_user_profile(user_id)
+ if not user_profile:
+ logger.error(f"Failed to retrieve profile for user {user_id}.")
+ return "Sorry, I couldn't access your profile information right now."
- # --- System Prompt ---
+ current_emotion_display = user_profile.get('current_emotion', 'Not specified')
+ # --- System Prompt Updated ---
system_prompt = f"""
- You are Aishura, an emotionally intelligent AI career assistant. Your primary goal is to provide empathetic,
- realistic, and actionable career guidance. Always follow these steps:
- 1. Acknowledge the user's message and, if applicable, their expressed emotion (from their profile: '{user_profile.get('current_emotion', 'Not specified')}' or message). Use empathetic language.
+ You are Aishura, an emotionally intelligent AI career assistant. Your primary goal is to provide empathetic, realistic, and actionable career guidance. Always follow these steps:
+ 1. Acknowledge the user's message and, if applicable, their expressed emotion (e.g., "I understand you're feeling {current_emotion_display}..."). Use empathetic language.
2. Directly address the user's query or statement.
- 3. Proactively offer relevant support using your tools: suggest searching for jobs (`get_job_opportunities`), generating document templates (`generate_document_template`), creating a personalized routine (`create_personalized_routine`), analyzing their resume (`analyze_resume`) or portfolio (`analyze_portfolio`) if they've provided them or mention doing so.
- 4. Tailor your response based on the user's profile:
- - Name: {user_profile.get('name', 'User')}
- - Location: {user_profile.get('location', 'Not specified')}
- - Stated Career Goal: {user_profile.get('career_goal', 'Not specified')}
- - Recent Emotion: {user_profile.get('current_emotion', 'Not specified')}
- 5. If the user has uploaded a resume or portfolio (check profile paths: resume='{user_profile.get('resume_path', '')}', portfolio='{user_profile.get('portfolio_path', '')}'), mention you can analyze them or use insights from previous analysis if available.
- 6. Keep responses concise, friendly, and focused on next steps. Avoid overly long paragraphs.
- 7. Use markdown for formatting (bolding, lists) where appropriate.
+ 3. Proactively offer relevant support using your available tools: suggest generating document templates (`generate_document_template`), creating a personalized routine (`create_personalized_routine`), analyzing their resume (`analyze_resume`) or portfolio (`analyze_portfolio`) if appropriate or if they mention them.
+ 4. **Job Suggestions:** If the user asks for job opportunities or related help, **do not use a tool**. Instead, generate 2-3 plausible job titles/roles based on their stated main goal ('{user_profile.get('career_goal', 'Not specified')}') and location ('{user_profile.get('location', 'Not specified')}'). If they have provided a resume (path: '{user_profile.get('resume_path', '')}'), mention how their skills might align with these roles. Keep suggestions general and indicate they are examples, not live listings.
+ 5. Tailor your response based on the user's profile: Name: {user_profile.get('name', 'User')}, Location: {user_profile.get('location', 'Not specified')}, Goal: {user_profile.get('career_goal', 'Not specified')}.
+ 6. If the user has uploaded a resume or portfolio (check paths above), mention you can analyze them or reference previous analysis if relevant.
+ 7. Keep responses concise, friendly, and focused on next steps. Use markdown for formatting.
+ 8. If a tool call fails, inform the user gracefully (e.g., "I couldn't generate the template right now...") and suggest alternatives. Do not show raw error messages.
"""
# --- Build Message History ---
messages = [{"role": "system", "content": system_prompt}]
-
- # Add recent chat history (ensure it's in OpenAI format)
chat_history = user_profile.get('chat_history', [])
- # Append only user/assistant messages with 'content' key
for msg in chat_history:
- if msg.get('role') in ['user', 'assistant'] and 'content' in msg:
- messages.append({"role": msg['role'], "content": msg['content']})
- elif msg.get('role') == 'tool' and 'tool_call_id' in msg and 'name' in msg and 'content' in msg:
- # Reconstruct tool call response message correctly
- messages.append({
- "role": "tool",
- "tool_call_id": msg['tool_call_id'],
- "name": msg['name'],
- "content": msg['content'] # Content should be the JSON string result from the tool function
- })
-
-
- # Add current user input
+ if isinstance(msg, dict) and 'role' in msg and 'content' in msg:
+ # Handle potential None content for assistant messages (tool calls)
+ content = msg['content'] if msg['content'] is not None else ""
+ messages.append({"role": msg['role'], "content": content})
+ elif isinstance(msg, dict) and msg.get('role') == 'tool' and all(k in msg for k in ['tool_call_id', 'name', 'content']):
+ # Content for tool role must be a string
+ tool_content = msg['content'] if isinstance(msg['content'], str) else json.dumps(msg['content'])
+ messages.append({ "role": "tool", "tool_call_id": msg['tool_call_id'], "name": msg['name'], "content": tool_content })
+
messages.append({"role": "user", "content": user_input})
# --- Initial API Call ---
@@ -707,25 +628,34 @@ def get_ai_response(user_id: str, user_input: str, generate_recommendations: boo
response = client.chat.completions.create(
model=MODEL_ID,
messages=messages,
- tools=tools_list,
- tool_choice="auto", # Let the model decide whether to use tools
+ tools=tools_list, # Provide available tools (excluding job search)
+ tool_choice="auto",
temperature=0.7,
- max_tokens=1024 # Adjust as needed
+ max_tokens=1500
)
-
response_message = response.choices[0].message
- logger.info("Received initial response from OpenAI.")
- # --- Tool Call Handling ---
+ # --- Log Assistant's Turn (Potentially with Tool Calls) ---
+ # Store the assistant's message regardless of whether it contains text or tool calls.
+ # The 'content' might be None/empty if only tool calls are made.
+ assistant_response_for_db = {
+ "role": "assistant",
+ "content": response_message.content, # Store text content (can be None)
+ # Add tool calls if they exist, for accurate history reconstruction
+ "tool_calls": [tc.model_dump() for tc in response_message.tool_calls] if response_message.tool_calls else None
+ }
+ # Don't save yet, save *after* potential second call
+
+ final_response_content = response_message.content # Initial text response
tool_calls = response_message.tool_calls
+
+ # --- Tool Call Handling ---
if tool_calls:
logger.info(f"AI requested {len(tool_calls)} tool call(s): {[tc.function.name for tc in tool_calls]}")
- # Append the assistant's response message that contains the tool calls
+ # Append the assistant's response message that contains the tool calls to the *local* messages list for the next API call
messages.append(response_message)
- # --- Execute Tools and Get Results ---
available_functions = {
- "get_job_opportunities": get_job_opportunities,
"generate_document_template": generate_document_template,
"create_personalized_routine": create_personalized_routine,
"analyze_resume": analyze_resume,
@@ -733,280 +663,167 @@ def get_ai_response(user_id: str, user_input: str, generate_recommendations: boo
"extract_and_rate_skills_from_resume": extract_and_rate_skills_from_resume,
}
+ tool_results_for_api = [] # Collect results to append for the next API call
+ tool_results_for_db = [] # Collect results for database storage
+
for tool_call in tool_calls:
function_name = tool_call.function.name
function_to_call = available_functions.get(function_name)
- function_args = json.loads(tool_call.function.arguments) # Arguments are provided as a JSON string
-
- if function_to_call:
- try:
- # Special handling for functions needing user_id or profile info
- if function_name in ["analyze_resume", "analyze_portfolio", "create_personalized_routine"]:
- # Add user_id or necessary profile elements to args if needed by the function
- # e.g., function_args['user_id'] = user_id
- # Pass career goal from profile if not in direct args for analysis functions
- if function_name == "analyze_resume" and 'career_goal' not in function_args:
- function_args['career_goal'] = user_profile.get('career_goal', 'Not specified')
- if function_name == "analyze_portfolio" and 'career_goal' not in function_args:
- function_args['career_goal'] = user_profile.get('career_goal', 'Not specified')
-
- # Save files when analysis tools are called
- if function_name == "analyze_resume":
- save_user_resume(user_id, function_args.get('resume_text', ''))
- if function_name == "analyze_portfolio":
- save_user_portfolio(user_id, function_args.get('portfolio_url', ''), function_args.get('portfolio_description', ''))
-
-
- # Call the function with unpacked arguments
+ try:
+ function_args = json.loads(tool_call.function.arguments)
+ if function_to_call:
+ # --- Special Handling & File Saving ---
+ if function_name == "analyze_resume":
+ if 'career_goal' not in function_args: function_args['career_goal'] = user_profile.get('career_goal', 'Not specified')
+ save_user_resume(user_id, function_args.get('resume_text', ''))
+ if function_name == "analyze_portfolio":
+ if 'career_goal' not in function_args: function_args['career_goal'] = user_profile.get('career_goal', 'Not specified')
+ save_user_portfolio(user_id, function_args.get('portfolio_url', ''), function_args.get('portfolio_description', ''))
+ # --- Call Function ---
logger.info(f"Calling function '{function_name}' with args: {function_args}")
- function_response = function_to_call(**function_args)
- logger.info(f"Function '{function_name}' returned (type: {type(function_response)}): {str(function_response)[:200]}...")
-
- # Append tool response to messages
- messages.append(
- {
- "tool_call_id": tool_call.id,
- "role": "tool",
- "name": function_name,
- "content": function_response, # Must be a string (JSON string in our case)
- }
- )
- # Also add tool call result to chat history DB
- add_chat_message(user_id, "tool", {
- "tool_call_id": tool_call.id,
- "name": function_name,
- "content": function_response # Save the JSON string result
- })
-
-
- except Exception as e:
- logger.error(f"Error executing function {function_name}: {e}")
- messages.append(
- {
- "tool_call_id": tool_call.id,
- "role": "tool",
- "name": function_name,
- "content": json.dumps({"error": f"Failed to execute tool {function_name}: {e}"}),
- }
- )
- # Also add error to chat history DB
- add_chat_message(user_id, "tool", {
- "tool_call_id": tool_call.id,
- "name": function_name,
- "content": json.dumps({"error": f"Failed to execute tool {function_name}: {e}"})
- })
-
-
- else:
- logger.warning(f"Function {function_name} requested by AI but not found.")
- # Append a message indicating the function wasn't found
- messages.append(
- {
- "tool_call_id": tool_call.id,
- "role": "tool",
- "name": function_name,
- "content": json.dumps({"error": f"Tool '{function_name}' is not available."})
- }
- )
- add_chat_message(user_id, "tool", {
- "tool_call_id": tool_call.id,
- "name": function_name,
- "content": json.dumps({"error": f"Tool '{function_name}' is not available."})
- })
-
+ function_response = function_to_call(**function_args) # Response should be JSON string
+ logger.info(f"Function '{function_name}' returned: {function_response[:200]}...")
+ tool_result = { "tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": function_response }
+ else:
+ logger.warning(f"Function {function_name} requested by AI but not implemented.")
+ tool_result = { "tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": json.dumps({"error": f"Tool '{function_name}' is not available."}) }
+ except json.JSONDecodeError as e:
+ logger.error(f"Error decoding arguments for {function_name}: {tool_call.function.arguments} - {e}")
+ tool_result = { "tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": json.dumps({"error": f"Invalid arguments provided for tool {function_name}."}) }
+ except Exception as e:
+ logger.exception(f"Error executing function {function_name}: {e}")
+ tool_result = { "tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": json.dumps({"error": f"Failed to execute tool {function_name}."}) }
+
+ # Append result for both next API call and DB storage
+ messages.append(tool_result) # Append full dict to local messages for API
+ tool_results_for_db.append(tool_result) # Store for DB
# --- Second API Call (after tool execution) ---
logger.info(f"Sending {len(messages)} messages to OpenAI (including tool results).")
second_response = client.chat.completions.create(
- model=MODEL_ID,
- messages=messages,
- temperature=0.7,
- max_tokens=1024
- # No tool_choice here, we expect a natural language response
+ model=MODEL_ID, messages=messages, temperature=0.7, max_tokens=1500
)
final_response_content = second_response.choices[0].message.content
logger.info("Received final response from OpenAI after tool calls.")
+ # --- Store User Input, Assistant (Tool Call) Message, Tool Results, and Final Assistant Response ---
+ add_chat_message(user_id, "user", user_input)
+ # Store the *first* assistant message (which contained the tool call request)
+ add_chat_message(user_id, "assistant", assistant_response_for_db)
+ # Store the tool results
+ for res in tool_results_for_db: add_chat_message(user_id, "tool", res)
+ # Store the *final* assistant text response
+ add_chat_message(user_id, "assistant", {"role": "assistant", "content": final_response_content})
+
else:
- # No tool calls were made, use the first response
- final_response_content = response_message.content
+ # --- No Tool Calls ---
logger.info("No tool calls requested by AI.")
+ # Store User Input and Assistant Response
+ add_chat_message(user_id, "user", user_input)
+ # Ensure the response content is stored correctly (might be None initially)
+ add_chat_message(user_id, "assistant", {"role": "assistant", "content": final_response_content if final_response_content else ""})
- # --- Post-processing and Saving ---
+ # --- Post-processing and Return ---
if not final_response_content:
- final_response_content = "I received that, but I don't have a specific response right now. Could you try rephrasing?"
- logger.warning("AI returned empty content.")
-
-
- # Save user message and final AI response to DB
- add_chat_message(user_id, "user", user_input)
- # Check if the last message added was the assistant's message with tool calls
- if messages[-1]['role'] == 'assistant' and messages[-1].tool_calls:
- # Don't add the tool call message itself to the history again,
- # just add the final text response
- pass
- elif messages[-1]['role'] == 'tool':
- # If the last message was a tool response, the final content comes from the second call
- pass
- else:
- # If no tools were called, the first response message needs saving
- add_chat_message(user_id, "assistant", final_response_content)
-
+ final_response_content = "I've processed that. Is there anything else I can help you with?"
+ logger.warning("AI returned empty content after processing.")
- # Generate recommendations (consider doing this asynchronously)
+ # Optional: Generate recommendations based on the final interaction (can be slow)
if generate_recommendations:
- # This could be a separate AI call based on the final interaction
- # For simplicity, we'll skip detailed recommendation generation here
- # but you would call a function like `gen_recommendations_openai`
- # gen_recommendations_openai(user_id, user_input, final_response_content)
- pass # Placeholder for recommendation generation logic
+ try:
+ # Consider making this async or optional via UI button
+ # gen_recommendations_openai(user_id, user_input, final_response_content if final_response_content else "Tool action completed.")
+ pass # Skipping inline recommendation generation for performance
+ except Exception as rec_e:
+ logger.error(f"Error during recommendation generation: {rec_e}")
- return final_response_content
+
+ return final_response_content if final_response_content else "Action completed."
except openai.APIError as e:
- logger.error(f"OpenAI API returned an API Error: {e}")
- return f"I'm sorry, there was an error communicating with the AI service (API Error: {e.status_code}). Please try again later."
+ logger.error(f"OpenAI API Error: {e.status_code} - {e.response}")
+ return f"I'm sorry, there was an issue communicating with the AI service (Code: {e.status_code}). Please try again."
+ except openai.APITimeoutError:
+ logger.error("OpenAI API request timed out.")
+ return "I'm sorry, the request to the AI service timed out. Please try again."
except openai.APIConnectionError as e:
- logger.error(f"Failed to connect to OpenAI API: {e}")
- return "I'm sorry, I couldn't connect to the AI service. Please check your connection and try again."
- except openai.RateLimitError as e:
- logger.error(f"OpenAI API request exceeded rate limit: {e}")
- return "I'm currently experiencing high demand. Please try again in a few moments."
+ logger.error(f"OpenAI Connection Error: {e}")
+ return "I couldn't connect to the AI service. Please check your network connection."
+ except openai.RateLimitError:
+ logger.error("OpenAI Rate Limit Exceeded.")
+ return "I'm experiencing high demand right now. Please try again in a moment."
except Exception as e:
- # Log the full traceback for debugging
logger.exception(f"Unexpected error in get_ai_response for user {user_id}: {e}")
- return "I apologize, but an unexpected error occurred while processing your request. Please try again."
+ return "I apologize, but an unexpected error occurred. Please try restarting the conversation or try again later."
# --- Recommendation Generation (Placeholder - Adapt for OpenAI) ---
def gen_recommendations_openai(user_id, user_input, ai_response):
- """Generate recommendations using OpenAI (Adapt prompt and parsing)."""
+ """Generate recommendations using OpenAI."""
logger.info(f"Generating recommendations for user {user_id}")
- if not client:
- logger.warning("OpenAI client not available for generating recommendations.")
- return []
-
+ if not client: return []
try:
user_profile = get_user_profile(user_id)
-
prompt = f"""
- Based on the following user profile and recent conversation, generate 1-3 specific, actionable recommendations
- for the user's next steps in their career journey. Focus on practical actions they can take soon.
+ Based on the user profile and recent conversation, generate 1-3 specific, actionable recommendations for their next steps. Focus on practical actions.
- User Profile:
- - Current emotion: {user_profile.get('current_emotion', 'Not specified')}
- - Career goal: {user_profile.get('career_goal', 'Not specified')}
- - Location: {user_profile.get('location', 'Not specified')}
- - Recent chat history is available to the main assistant.
+ User Profile: Emotion: {user_profile.get('current_emotion', 'N/A')}, Goal: {user_profile.get('career_goal', 'N/A')}, Location: {user_profile.get('location', 'N/A')}
+ Recent Interaction: User: {user_input} | AI: {ai_response}
- Most Recent Interaction:
- User: {user_input}
- Aishura (AI Assistant): {ai_response}
-
- Generate recommendations in this JSON format only:
+ Generate recommendations in this JSON format ONLY (a list of objects):
```json
[
- {{
- "title": "Concise recommendation title (e.g., 'Refine Resume Keywords')",
- "description": "Detailed explanation of the recommendation and why it's relevant (2-3 sentences).",
- "action_type": "job_search | skill_building | networking | resume_update | portfolio_review | interview_prep | mindset_shift | other",
- "priority": "high | medium | low"
- }}
+ {{"title": "Concise title", "description": "Detailed explanation (2-3 sentences).", "action_type": "skill_building | networking | resume_update | portfolio_review | interview_prep | mindset_shift | other", "priority": "high | medium | low"}}
]
```
- Provide only the JSON array, no introductory text.
"""
-
response = client.chat.completions.create(
- model=MODEL_ID, # Or a faster/cheaper model if preferred for this task
- messages=[
- {"role": "system", "content": "You are an expert career advisor generating concise, actionable recommendations in JSON format."},
- {"role": "user", "content": prompt}
- ],
- temperature=0.5,
- max_tokens=512,
- response_format={"type": "json_object"} # Request JSON output if model supports it
+ model=MODEL_ID,
+ messages=[ {"role": "system", "content": "You generate career recommendations in JSON list format."}, {"role": "user", "content": prompt} ],
+ temperature=0.5, max_tokens=512,
+ # Attempting JSON mode, but ensure the prompt clearly asks for the list format
+ # response_format={"type": "json_object"} # This might force an outer object, adjust parsing if used.
)
-
- recommendation_json_str = response.choices[0].message.content
- logger.info(f"Raw recommendations JSON string: {recommendation_json_str}")
-
-
- # Attempt to parse the JSON
+ json_str = response.choices[0].message.content
+ logger.info(f"Raw recommendations JSON string: {json_str}")
try:
- # The response_format parameter should ensure it's valid JSON, but double-check
- # Clean potential markdown fences if response_format didn't work
- if recommendation_json_str.startswith("```json"):
- recommendation_json_str = recommendation_json_str.split("```json")[1].split("```")[0].strip()
-
- # The prompt asks for a list, but response_format might enforce an object. Adjust parsing.
- recommendations_data = json.loads(recommendation_json_str)
-
- # If the root is an object with a key like "recommendations", extract the list
- if isinstance(recommendations_data, dict) and "recommendations" in recommendations_data and isinstance(recommendations_data["recommendations"], list):
- recommendations = recommendations_data["recommendations"]
- elif isinstance(recommendations_data, list):
- recommendations = recommendations_data # It's already a list
- else:
- logger.error(f"Unexpected JSON structure for recommendations: {type(recommendations_data)}")
- return []
-
+ # Clean potential markdown fences
+ if json_str.startswith("```json"): json_str = json_str.split("```json")[1].split("```")[0].strip()
+ recommendations = json.loads(json_str)
+ if not isinstance(recommendations, list): # Handle if AI wraps in an object
+ if isinstance(recommendations, dict) and len(recommendations) == 1:
+ key = list(recommendations.keys())[0]
+ if isinstance(recommendations[key], list):
+ recommendations = recommendations[key]
+ else: raise ValueError("JSON is not a list or expected object wrapper.")
+ else: raise ValueError("JSON is not a list.")
- # Add valid recommendations to user profile
valid_recs_added = 0
for rec in recommendations:
- # Basic validation of recommendation structure
if isinstance(rec, dict) and all(k in rec for k in ['title', 'description', 'action_type', 'priority']):
- add_recommendation_to_user(user_id, rec)
- valid_recs_added += 1
- else:
- logger.warning(f"Skipping invalid recommendation format: {rec}")
+ add_recommendation_to_user(user_id, rec); valid_recs_added += 1
+ else: logger.warning(f"Skipping invalid recommendation format: {rec}")
+ logger.info(f"Added {valid_recs_added} recommendations.")
+ return recommendations
+ except (json.JSONDecodeError, ValueError) as e:
+ logger.error(f"Failed to parse JSON recommendations: {e}\nResponse: {json_str}"); return []
+ except Exception as e: logger.exception(f"Error generating recommendations: {e}"); return []
- logger.info(f"Added {valid_recs_added} recommendations for user {user_id}")
- return recommendations # Return the raw list parsed
-
- except json.JSONDecodeError as e:
- logger.error(f"Failed to parse JSON recommendations from AI response: {e}\nResponse: {recommendation_json_str}")
- return []
- except Exception as e:
- logger.exception(f"Error processing recommendations: {e}")
- return []
-
- except Exception as e:
- logger.exception(f"Error in gen_recommendations_openai: {e}")
- return []
-
-
-# --- Chart and Visualization Functions (Unchanged, but depend on data format) ---
-# [Keep create_emotion_chart, create_progress_chart, create_routine_completion_gauge]
-# Ensure they handle the data structures saved by the updated functions correctly.
+# --- Chart and Visualization Functions ---
def create_emotion_chart(user_id):
"""Create a chart of user's emotions over time"""
user_profile = get_user_profile(user_id)
emotion_records = user_profile.get('daily_emotions', [])
-
if not emotion_records:
- fig = go.Figure()
- fig.add_annotation(text="No emotion data tracked yet.", align='center', showarrow=False)
- fig.update_layout(title="Emotion Tracking")
- return fig
-
- emotion_values = {
- "Unmotivated": 1, "Anxious": 2, "Confused": 3,
- "Discouraged": 4, "Overwhelmed": 5, "Excited": 6
- }
- dates = [datetime.fromisoformat(record['date']) if isinstance(record['date'], str) else datetime.strptime(record['date'], "%Y-%m-%d %H:%M:%S") for record in emotion_records] # Handle ISO or older format
+ fig = go.Figure(); fig.add_annotation(text="No emotion data tracked yet.", showarrow=False); fig.update_layout(title="Emotion Tracking"); return fig
+ emotion_values = {"Unmotivated": 1, "Anxious": 2, "Confused": 3, "Discouraged": 4, "Overwhelmed": 5, "Excited": 6}
+ dates = [datetime.fromisoformat(record['date']) for record in emotion_records]
emotion_scores = [emotion_values.get(record['emotion'], 3) for record in emotion_records]
emotion_names = [record['emotion'] for record in emotion_records]
-
- df = pd.DataFrame({'Date': dates, 'Emotion Score': emotion_scores, 'Emotion': emotion_names})
- df = df.sort_values('Date') # Ensure chronological order
-
- fig = px.line(df, x='Date', y='Emotion Score', markers=True,
- labels={"Emotion Score": "Emotional State"},
- title="Your Emotional Journey")
+ df = pd.DataFrame({'Date': dates, 'Emotion Score': emotion_scores, 'Emotion': emotion_names}).sort_values('Date')
+ fig = px.line(df, x='Date', y='Emotion Score', markers=True, labels={"Emotion Score": "Emotional State"}, title="Your Emotional Journey")
fig.update_traces(hovertemplate='%{x|%Y-%m-%d %H:%M}
Feeling: %{text}', text=df['Emotion'])
fig.update_yaxes(tickvals=list(emotion_values.values()), ticktext=list(emotion_values.keys()))
return fig
@@ -1015,36 +832,19 @@ def create_progress_chart(user_id):
"""Create a chart showing user's progress points over time"""
user_profile = get_user_profile(user_id)
tasks = user_profile.get('completed_tasks', [])
-
if not tasks:
- fig = go.Figure()
- fig.add_annotation(text="No tasks completed yet.", align='center', showarrow=False)
- fig.update_layout(title="Progress Tracking")
- return fig
-
- # Ensure tasks have points (might need adjustment based on how points are awarded)
- points_per_task = 20 # Example: Assign fixed points if not stored with task
- dates = []
- cumulative_points = 0
- points_timeline = []
- task_labels = []
-
- # Sort tasks by date
- tasks.sort(key=lambda x: datetime.fromisoformat(x['date']) if isinstance(x['date'], str) else datetime.strptime(x['date'], "%Y-%m-%d %H:%M:%S"))
-
+ fig = go.Figure(); fig.add_annotation(text="No tasks completed yet.", showarrow=False); fig.update_layout(title="Progress Tracking"); return fig
+ tasks.sort(key=lambda x: datetime.fromisoformat(x['date']))
+ dates, points_timeline, task_labels, cumulative_points = [], [], [], 0
+ points_per_task = 20 # Default points if not stored
for task in tasks:
- task_date = datetime.fromisoformat(task['date']) if isinstance(task['date'], str) else datetime.strptime(task['date'], "%Y-%m-%d %H:%M:%S")
- dates.append(task_date)
- # Use points from profile if calculated there, otherwise estimate
- # We are using the cumulative points stored in the profile directly now
- # For simplicity, let's recalculate cumulative points for the chart
- cumulative_points += task.get('points', points_per_task) # Use stored points if available
+ dates.append(datetime.fromisoformat(task['date']))
+ # Use actual profile points for timeline if available and reliable
+ # For simplicity, recalculate cumulative based on tasks for chart
+ cumulative_points += task.get('points', points_per_task) # Use points stored with task if they existed
points_timeline.append(cumulative_points)
task_labels.append(task['task'])
-
-
df = pd.DataFrame({'Date': dates, 'Points': points_timeline, 'Task': task_labels})
-
fig = px.line(df, x='Date', y='Points', markers=True, title="Your Career Journey Progress")
fig.update_traces(hovertemplate='%{x|%Y-%m-%d %H:%M}
Points: %{y}
Completed: %{text}', text=df['Task'])
return fig
@@ -1053,459 +853,215 @@ def create_routine_completion_gauge(user_id):
"""Create a gauge chart showing routine completion percentage"""
user_profile = get_user_profile(user_id)
routines = user_profile.get('routine_history', [])
-
if not routines:
fig = go.Figure(go.Indicator(mode="gauge", value=0, title={'text': "Routine Completion"}))
- fig.add_annotation(text="No active routine.", showarrow=False)
- return fig
-
- # Get the most recent routine (assuming prepend logic)
- latest_routine = routines[0]
+ fig.add_annotation(text="No active routine.", showarrow=False); return fig
+ latest_routine = routines[0] # Assuming latest is first
completion = latest_routine.get('completion', 0)
routine_name = latest_routine.get('routine', {}).get('name', 'Current Routine')
-
fig = go.Figure(go.Indicator(
- mode = "gauge+number",
- value = completion,
- domain = {'x': [0, 1], 'y': [0, 1]},
- title = {'text': f"{routine_name} Completion"},
- gauge = {
- 'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': "darkblue"},
- 'bar': {'color': "cornflowerblue"},
- 'bgcolor': "white",
- 'borderwidth': 2,
- 'bordercolor': "gray",
- 'steps': [
- {'range': [0, 50], 'color': 'whitesmoke'},
- {'range': [50, 80], 'color': 'lightgray'}],
- 'threshold': {
- 'line': {'color': "green", 'width': 4},
- 'thickness': 0.75, 'value': 90}})) # Threshold at 90%
+ mode = "gauge+number", value = completion, domain = {'x': [0, 1], 'y': [0, 1]},
+ title = {'text': f"{routine_name} Completion (%)"},
+ gauge = {'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': "darkblue"},
+ 'bar': {'color': "cornflowerblue"}, 'bgcolor': "white", 'borderwidth': 2, 'bordercolor': "gray",
+ 'steps': [{'range': [0, 50], 'color': 'whitesmoke'}, {'range': [50, 80], 'color': 'lightgray'}],
+ 'threshold': {'line': {'color': "green", 'width': 4}, 'thickness': 0.75, 'value': 90}}))
return fig
-
def create_skill_radar_chart(user_id):
- """
- Creates a radar chart of user's skills.
- Requires skills data, potentially extracted by `extract_and_rate_skills_from_resume` tool.
- """
+ """Creates a radar chart of user's skills based on resume analysis."""
logger.info(f"Creating skill radar chart for user {user_id}")
user_profile = get_user_profile(user_id)
resume_path = user_profile.get('resume_path')
-
if not resume_path or not os.path.exists(resume_path):
logger.warning("No resume path found or file missing for skill chart.")
- fig = go.Figure()
- fig.add_annotation(text="Upload & Analyze Resume for Skill Chart", showarrow=False)
- fig.update_layout(title="Skill Assessment")
- return fig
-
+ fig = go.Figure(); fig.add_annotation(text="Upload & Analyze Resume for Skill Chart", showarrow=False); fig.update_layout(title="Skill Assessment"); return fig
try:
- with open(resume_path, 'r', encoding='utf-8') as f:
- resume_text = f.read()
-
+ with open(resume_path, 'r', encoding='utf-8') as f: resume_text = f.read()
# Use the tool function to extract skills (simulated call here)
- # In a real app, this might be triggered explicitly or data stored after analysis
skills_json_str = extract_and_rate_skills_from_resume(resume_text=resume_text)
skill_data = json.loads(skills_json_str)
-
if 'skills' in skill_data and skill_data['skills']:
- skills = skill_data['skills']
- # Limit to max 8 skills for readability
- skills = skills[:8]
-
+ skills = skill_data['skills'][:8] # Limit skills
categories = [skill['name'] for skill in skills]
values = [skill['score'] for skill in skills]
-
- # Ensure the loop closes
- if len(categories) > 2:
- categories.append(categories[0])
- values.append(values[0])
-
+ if len(categories) > 2: categories.append(categories[0]); values.append(values[0]) # Close loop
fig = go.Figure()
- fig.add_trace(go.Scatterpolar(
- r=values,
- theta=categories,
- fill='toself',
- name='Skills'
- ))
- fig.update_layout(
- polar=dict(radialaxis=dict(visible=True, range=[0, 10])),
- showlegend=False,
- title="Skill Assessment (Based on Resume)"
- )
+ fig.add_trace(go.Scatterpolar(r=values, theta=categories, fill='toself', name='Skills'))
+ fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 10])), showlegend=False, title="Skill Assessment (Based on Resume)")
logger.info(f"Successfully created radar chart with {len(skills)} skills.")
return fig
else:
logger.warning("Could not extract skills from resume for chart.")
- fig = go.Figure()
- fig.add_annotation(text="Could not extract skills from resume", showarrow=False)
- fig.update_layout(title="Skill Assessment")
- return fig
-
+ fig = go.Figure(); fig.add_annotation(text="Could not extract skills from resume", showarrow=False); fig.update_layout(title="Skill Assessment"); return fig
except Exception as e:
logger.exception(f"Error creating skill radar chart: {e}")
- fig = go.Figure()
- fig.add_annotation(text="Error analyzing skills", showarrow=False)
- fig.update_layout(title="Skill Assessment")
- return fig
+ fig = go.Figure(); fig.add_annotation(text="Error analyzing skills", showarrow=False); fig.update_layout(title="Skill Assessment"); return fig
# --- Gradio Interface Components ---
def create_interface():
"""Create the Gradio interface for Aishura"""
-
- # Generate a unique user ID for this session (can be replaced with login later)
- # This state needs careful handling in Gradio for multi-user scenarios.
- # Using a simple global or closure for demo purposes.
- # A better approach involves Gradio's State management or user handling.
session_user_id = str(uuid.uuid4())
logger.info(f"Initializing Gradio interface for session user ID: {session_user_id}")
- # Initialize profile for session user
- get_user_profile(session_user_id)
-
-
- # --- Event Handlers for Gradio Components ---
+ get_user_profile(session_user_id) # Initialize profile
+ # --- Event Handlers ---
def welcome(name, location, emotion, goal):
- """Handles the initial welcome screen submission."""
- logger.info(f"Welcome action for user {session_user_id}: name='{name}', loc='{location}', emo='{emotion}', goal='{goal}'")
+ """Handles welcome screen submission."""
+ logger.info(f"Welcome action: name='{name}', loc='{location}', emo='{emotion}', goal='{goal}'")
if not all([name, location, emotion, goal]):
- return ("Please fill out all fields to get started.",
- gr.update(visible=True), # Keep welcome visible
- gr.update(visible=False)) # Keep main hidden
-
- # Update profile
- update_user_profile(session_user_id, {
- "name": name, "location": location, "career_goal": goal
- })
- add_emotion_record(session_user_id, emotion) # Record initial emotion
-
- # Generate initial AI message based on input
- initial_input = f"Hi Aishura! I'm {name} from {location}. I'm currently feeling {emotion}, and my main goal is to {goal}. Can you help me get started?"
+ return ("Please fill out all fields.", gr.update(visible=True), gr.update(visible=False)) # Keep welcome visible
+ # Clean goal string if it includes emoji
+ cleaned_goal = goal.rsplit(" ", 1)[0] if goal[-1].isnumeric() == False and goal[-2] == " " else goal # Basic emoji removal
+ update_user_profile(session_user_id, {"name": name, "location": location, "career_goal": cleaned_goal}) # Store cleaned goal
+ add_emotion_record(session_user_id, emotion)
+ initial_input = f"Hi Aishura! I'm {name} from {location}. I'm feeling {emotion}, and my main goal is '{cleaned_goal}'. Can you help me get started?"
ai_response = get_ai_response(session_user_id, initial_input, generate_recommendations=True)
-
- # Initial chat history
- initial_chat = [(initial_input, ai_response)]
-
- # Initial charts
+ initial_chat = [{"role":"user", "content": initial_input}, {"role":"assistant", "content": ai_response}] # Use messages format
+ # Fetch initial charts
emotion_fig = create_emotion_chart(session_user_id)
progress_fig = create_progress_chart(session_user_id)
routine_fig = create_routine_completion_gauge(session_user_id)
- skill_fig = create_skill_radar_chart(session_user_id) # Will be empty initially
+ skill_fig = create_skill_radar_chart(session_user_id)
+ # Return updates: chatbot history, visibility, and chart values
+ return (gr.update(value=initial_chat), # Chatbot expects list of dicts
+ gr.update(visible=False), gr.update(visible=True), # Show/hide groups
+ gr.update(value=emotion_fig), gr.update(value=progress_fig), # Update plots with value=figure
+ gr.update(value=routine_fig), gr.update(value=skill_fig))
- # Output: Hide welcome, show main, populate initial chat and charts
- return (gr.update(value=initial_chat), # Update chatbot
- gr.update(visible=False), # Hide welcome group
- gr.update(visible=True), # Show main interface
- gr.update(figure=emotion_fig),
- gr.update(figure=progress_fig),
- gr.update(figure=routine_fig),
- gr.update(figure=skill_fig)
- )
+ def chat_submit(message_text, history_list_dicts):
+ """Handles sending a message in the chatbot (using messages format)."""
+ logger.info(f"Chat submit: '{message_text[:50]}...'")
+ if not message_text: return history_list_dicts, "", gr.update() # Return current history if empty input
+ # Append user message to the history list
+ history_list_dicts.append({"role": "user", "content": message_text})
- def chat_submit(message, history):
- """Handles sending a message in the chatbot."""
- logger.info(f"Chat submit for user {session_user_id}: '{message[:50]}...'")
- if not message:
- return history, "" # Do nothing if message is empty
+ # Get AI response (which also saves interaction to DB)
+ ai_response_text = get_ai_response(session_user_id, message_text, generate_recommendations=True)
- ai_response = get_ai_response(session_user_id, message, generate_recommendations=True)
- history.append((message, ai_response))
+ # Append AI response to the history list
+ history_list_dicts.append({"role": "assistant", "content": ai_response_text})
- # Update recommendations display after chat
+ # Update recommendations display
recommendations_md = display_recommendations(session_user_id)
- return history, "", gr.update(value=recommendations_md) # Return updated history, clear input, update recs
-
-
- # --- Simulation for Emotion Messages ---
- pause_message = "Take your time, weβre here when you're ready."
- retype_message = "It doesnβt have to be perfect. Letβs just begin."
-
- # JS for basic simulation (might need refinement based on Gradio version/behavior)
- # This is illustrative; direct JS injection can be tricky/fragile in Gradio.
- # We'll use Gradio events for a simpler simulation.
-
- def show_pause_message():
- # Simulate showing pause message (e.g., make a Markdown visible)
- # In a real app, this needs proper timing logic (JS setTimeout)
- # logger.info("Simulating 'pause' message visibility.")
- return gr.update(value=pause_message, visible=True)
-
- def show_retype_message():
- # Simulate showing retype message
- # logger.info("Simulating 'retype' message visibility.")
- return gr.update(value=retype_message, visible=True)
-
- def hide_emotion_message():
- # logger.info("Hiding emotion message.")
- return gr.update(value="", visible=False)
-
-
- def handle_chat_focus():
- """Called when chat input gains focus."""
- # logger.info("Chat input focused.")
- # Decide whether to show a message, e.g., maybe the retype one briefly?
- # Or just hide any existing message.
- return hide_emotion_message() # Hide message on focus for now
-
-
- # Placeholder: More complex logic would be needed for actual pause/retype detection
- # Using .change() with debounce might approximate it, but Gradio support varies.
+ # Return updated history list, clear input box, update recommendations display
+ return history_list_dicts, "", gr.update(value=recommendations_md)
# --- Tool Interface Handlers ---
-
- def search_jobs_interface_handler(query, location, max_results):
- """Handles the Job Search button click."""
- logger.info(f"Manual Job Search UI: query='{query}', loc='{location}', num={max_results}")
- # Call the underlying tool function directly for the UI button
- results_json_str = get_job_opportunities(query, location, int(max_results))
- try:
- results_data = json.loads(results_json_str)
- if "error" in results_data:
- return f"Error: {results_data['error']}"
- if not results_data.get("results"):
- return "No job opportunities found matching your criteria."
-
- output_md = f"## Job Opportunities Found ({len(results_data['results'])})\n\n"
- for i, job in enumerate(results_data['results'], 1):
- output_md += f"### {i}. {job.get('title', 'N/A')}\n"
- output_md += f"**Company:** {job.get('company', 'N/A')}\n"
- output_md += f"**Location:** {job.get('location', location)}\n" # Use search location as fallback
- output_md += f"**Description:** {job.get('description', 'N/A')}\n"
- output_md += f"**Posted:** {job.get('date_posted', 'N/A')}\n"
- link = job.get('link', '#')
- output_md += f"**Link:** [{link}]({link})\n\n"
- return output_md
- except json.JSONDecodeError:
- logger.error(f"Failed to parse job search results: {results_json_str}")
- return "Error displaying job search results."
- except Exception as e:
- logger.exception("Error in search_jobs_interface_handler")
- return f"An unexpected error occurred: {e}"
-
-
+ # Removed search_jobs_interface_handler
def generate_template_interface_handler(doc_type, career_field, experience):
- """Handles Generate Template button click."""
logger.info(f"Manual Template UI: type='{doc_type}', field='{career_field}', exp='{experience}'")
template_json_str = generate_document_template(doc_type, career_field, experience)
try:
- template_data = json.loads(template_json_str)
- if "error" in template_data:
- return f"Error: {template_data['error']}"
- return template_data.get('template_markdown', "Could not generate template.")
- except json.JSONDecodeError:
- logger.error(f"Failed to parse template results: {template_json_str}")
- return "Error displaying template."
- except Exception as e:
- logger.exception("Error in generate_template_interface_handler")
- return f"An unexpected error occurred: {e}"
-
+ template_data = json.loads(template_json_str); return template_data.get('template_markdown', "Error.")
+ except: return "Error displaying template."
def create_routine_interface_handler(emotion, goal, time_available, days):
- """Handles Create Routine button click."""
logger.info(f"Manual Routine UI: emo='{emotion}', goal='{goal}', time='{time_available}', days='{days}'")
- routine_json_str = create_personalized_routine(emotion, goal, int(time_available), int(days))
+ # Clean emotion string
+ cleaned_emotion = emotion.split(" ")[0] if " " in emotion else emotion
+ routine_json_str = create_personalized_routine(cleaned_emotion, goal, int(time_available), int(days))
try:
routine_data = json.loads(routine_json_str)
- if "error" in routine_data:
- return f"Error: {routine_data['error']}"
-
- # Save the generated routine to the user profile
- add_routine_to_user(session_user_id, routine_data)
-
- # Format for display
- output_md = f"# Your {routine_data.get('name', 'Personalized Routine')}\n\n"
- output_md += f"{routine_data.get('description', '')}\n\n"
+ if "error" in routine_data: return f"Error: {routine_data['error']}", gr.update()
+ add_routine_to_user(session_user_id, routine_data) # Save routine
+ output_md = f"# Your {routine_data.get('name', 'Personalized Routine')}\n\n{routine_data.get('description', '')}\n\n"
for day_plan in routine_data.get('daily_tasks', []):
output_md += f"## Day {day_plan.get('day', '?')}\n"
- if not day_plan.get('tasks'):
- output_md += "- Rest day or free choice.\n"
+ tasks = day_plan.get('tasks', [])
+ if not tasks: output_md += "- Rest day or free choice.\n"
else:
- for task in day_plan.get('tasks', []):
- output_md += f"- **{task.get('name', 'Task')}** "
- output_md += f"({task.get('duration', '?')} mins"
- if 'points' in task: # Only show points if available
- output_md += f", {task.get('points', '?')} points"
- output_md += ")\n"
- output_md += f" *Why: {task.get('description', '...') }*\n"
+ for task in tasks:
+ output_md += f"- **{task.get('name', 'Task')}** ({task.get('duration', '?')} mins)\n *Why: {task.get('description', '...') }*\n" # Simplified display
output_md += "\n"
-
- # Update the gauge chart as well
gauge_fig = create_routine_completion_gauge(session_user_id)
-
- return output_md, gr.update(figure=gauge_fig) # Return markdown and updated gauge
-
- except json.JSONDecodeError:
- logger.error(f"Failed to parse routine results: {routine_json_str}")
- return "Error displaying routine.", gr.update() # Return update for gauge too
- except Exception as e:
- logger.exception("Error in create_routine_interface_handler")
- return f"An unexpected error occurred: {e}", gr.update()
-
+ return output_md, gr.update(value=gauge_fig)
+ except: return "Error displaying routine.", gr.update()
def analyze_resume_interface_handler(resume_text):
- """Handles Analyze Resume button click."""
logger.info(f"Manual Resume Analysis UI: length={len(resume_text)}")
- if not resume_text:
- # Clear previous results if input is empty
- return "Please paste your resume text above.", gr.update(figure=None)
-
+ if not resume_text: return "Please paste your resume text.", gr.update(value=None)
user_profile = get_user_profile(session_user_id)
- career_goal = user_profile.get('career_goal', 'Not specified') # Get goal from profile
-
- # Save resume first
- save_user_resume(session_user_id, resume_text)
-
- # Call analysis tool (placeholder version for now)
- analysis_json_str = analyze_resume(resume_text, career_goal)
-
+ career_goal = user_profile.get('career_goal', 'Not specified')
+ save_user_resume(session_user_id, resume_text) # Save first
+ analysis_json_str = analyze_resume(resume_text, career_goal) # Call tool (placeholder analysis)
try:
- analysis_data = json.loads(analysis_json_str)
- if "error" in analysis_data:
- return f"Error: {analysis_data['error']}", gr.update() # Update for chart
-
- # Format analysis for display (adapt based on actual tool output)
- analysis = analysis_data.get('analysis', {})
- output_md = "## Resume Analysis Results\n\n"
- output_md += f"**Analysis against goal:** '{career_goal}'\n\n"
- output_md += "**Strengths:**\n" + "\n".join([f"- {s}" for s in analysis.get('strengths', [])]) + "\n\n"
- output_md += "**Areas for Improvement:**\n" + "\n".join([f"- {s}" for s in analysis.get('areas_for_improvement', [])]) + "\n\n"
- output_md += f"**Format Feedback:** {analysis.get('format_feedback', 'N/A')}\n\n"
- output_md += f"**Content Feedback:** {analysis.get('content_feedback', 'N/A')}\n\n"
- output_md += "**Suggested Next Steps:**\n" + "\n".join([f"- {s}" for s in analysis.get('next_steps', [])])
-
- # Update skill chart after analysis
- skill_fig = create_skill_radar_chart(session_user_id)
-
- return output_md, gr.update(figure=skill_fig)
-
- except json.JSONDecodeError:
- logger.error(f"Failed to parse resume analysis results: {analysis_json_str}")
- return "Error displaying resume analysis.", gr.update()
- except Exception as e:
- logger.exception("Error in analyze_resume_interface_handler")
- return f"An unexpected error occurred: {e}", gr.update()
-
+ analysis_data = json.loads(analysis_json_str).get('analysis', {})
+ output_md = "## Resume Analysis Results (Simulated)\n\n" # Indicate simulation
+ output_md += f"**Analysis vs Goal:** '{career_goal}'\n\n"
+ output_md += "**Strengths:**\n" + "\n".join([f"- {s}" for s in analysis_data.get('strengths', [])]) + "\n\n"
+ output_md += "**Areas for Improvement:**\n" + "\n".join([f"- {s}" for s in analysis_data.get('areas_for_improvement', [])]) + "\n\n"
+ output_md += f"**Format Feedback:** {analysis_data.get('format_feedback', 'N/A')}\n"
+ output_md += f"**Content Feedback:** {analysis_data.get('content_feedback', 'N/A')}\n"
+ output_md += f"**Keyword Suggestions:** {', '.join(analysis_data.get('keyword_suggestions', []))}\n\n"
+ output_md += "**Next Steps:**\n" + "\n".join([f"- {s}" for s in analysis_data.get('next_steps', [])])
+ skill_fig = create_skill_radar_chart(session_user_id) # Update skill chart
+ return output_md, gr.update(value=skill_fig)
+ except: return "Error displaying analysis.", gr.update(value=None)
def analyze_portfolio_interface_handler(portfolio_url, portfolio_description):
- """Handles Analyze Portfolio button click."""
logger.info(f"Manual Portfolio Analysis UI: url='{portfolio_url}', desc_len={len(portfolio_description)}")
- if not portfolio_description:
- return "Please provide a description of your portfolio."
-
+ if not portfolio_description: return "Please provide a description."
user_profile = get_user_profile(session_user_id)
- career_goal = user_profile.get('career_goal', 'Not specified') # Get goal from profile
-
- # Save portfolio info first
- save_user_portfolio(session_user_id, portfolio_url, portfolio_description)
-
- # Call analysis tool (placeholder)
- analysis_json_str = analyze_portfolio(portfolio_description, career_goal, portfolio_url)
-
+ career_goal = user_profile.get('career_goal', 'Not specified')
+ save_user_portfolio(session_user_id, portfolio_url, portfolio_description) # Save first
+ analysis_json_str = analyze_portfolio(portfolio_description, career_goal, portfolio_url) # Call tool (placeholder analysis)
try:
- analysis_data = json.loads(analysis_json_str)
- if "error" in analysis_data:
- return f"Error: {analysis_data['error']}"
-
- # Format analysis for display
- analysis = analysis_data.get('analysis', {})
- output_md = "## Portfolio Analysis Results\n\n"
- output_md += f"**Analysis against goal:** '{career_goal}'\n"
- if portfolio_url:
- output_md += f"**Portfolio URL:** {portfolio_url}\n\n"
- output_md += f"**Alignment with Goal:**\n{analysis.get('alignment_with_goal', 'N/A')}\n\n"
- output_md += "**Strengths:**\n" + "\n".join([f"- {s}" for s in analysis.get('strengths', [])]) + "\n\n"
- output_md += "**Areas for Improvement:**\n" + "\n".join([f"- {s}" for s in analysis.get('areas_for_improvement', [])]) + "\n\n"
- output_md += f"**Presentation Feedback:** {analysis.get('presentation_feedback', 'N/A')}\n\n"
- output_md += "**Suggested Next Steps:**\n" + "\n".join([f"- {s}" for s in analysis.get('next_steps', [])])
-
+ analysis_data = json.loads(analysis_json_str).get('analysis', {})
+ output_md = "## Portfolio Analysis Results (Simulated)\n\n" # Indicate simulation
+ output_md += f"**Analysis vs Goal:** '{career_goal}'\n"
+ if portfolio_url: output_md += f"**URL:** {portfolio_url}\n\n"
+ output_md += f"**Alignment:** {analysis_data.get('alignment_with_goal', 'N/A')}\n\n"
+ output_md += "**Strengths:**\n" + "\n".join([f"- {s}" for s in analysis_data.get('strengths', [])]) + "\n\n"
+ output_md += "**Areas for Improvement:**\n" + "\n".join([f"- {s}" for s in analysis_data.get('areas_for_improvement', [])]) + "\n\n"
+ output_md += f"**Presentation Feedback:** {analysis_data.get('presentation_feedback', 'N/A')}\n\n"
+ output_md += "**Next Steps:**\n" + "\n".join([f"- {s}" for s in analysis_data.get('next_steps', [])])
return output_md
-
- except json.JSONDecodeError:
- logger.error(f"Failed to parse portfolio analysis results: {analysis_json_str}")
- return "Error displaying portfolio analysis."
- except Exception as e:
- logger.exception("Error in analyze_portfolio_interface_handler")
- return f"An unexpected error occurred: {e}"
+ except: return "Error displaying analysis."
# --- Progress Tracking Handlers ---
-
def complete_task_handler(task_name):
- """Handles marking a task as complete."""
- logger.info(f"Complete Task UI: task='{task_name}' for user {session_user_id}")
- if not task_name:
- return ("Please enter the name of the task you completed.", "",
- gr.update(), gr.update(), gr.update()) # No chart updates if no task
-
- # Add task and update points
- user_profile = add_task_to_user(session_user_id, task_name)
- points_earned = 20 # Use a fixed value or get from task data if available
-
+ logger.info(f"Complete Task UI: task='{task_name}'")
+ if not task_name: return ("Enter task name.", "", gr.update(), gr.update(), gr.update())
+ add_task_to_user(session_user_id, task_name)
# Update completion % of latest routine
- db = load_user_database()
- if session_user_id in db['users'] and db['users'][session_user_id].get('routine_history'):
- latest_routine_entry = db['users'][session_user_id]['routine_history'][0] # Get latest
- # Simple: increment completion by a fixed amount per task (e.g., 5-15%)
- # More complex: calculate based on routine definition and completed tasks
- increment = random.randint(5, 15)
+ db = load_user_database(); profile = db.get('users', {}).get(session_user_id)
+ if profile and profile.get('routine_history'):
+ latest_routine_entry = profile['routine_history'][0]
+ increment = random.randint(5, 15) # Simple increment
new_completion = min(100, latest_routine_entry.get('completion', 0) + increment)
- latest_routine_entry['completion'] = new_completion
- save_user_database(db) # Save updated DB
-
+ latest_routine_entry['completion'] = new_completion; save_user_database(db)
# Refresh charts
emotion_fig = create_emotion_chart(session_user_id)
progress_fig = create_progress_chart(session_user_id)
gauge_fig = create_routine_completion_gauge(session_user_id)
-
- return (f"Great job completing '{task_name}'! You've earned progress points.",
- "", # Clear task input
- gr.update(figure=emotion_fig),
- gr.update(figure=progress_fig),
- gr.update(figure=gauge_fig))
-
+ return (f"Great job on '{task_name}'!", "", gr.update(value=emotion_fig), gr.update(value=progress_fig), gr.update(value=gauge_fig))
def update_emotion_handler(emotion):
- """Handles updating the user's current emotion."""
- logger.info(f"Update Emotion UI: emotion='{emotion}' for user {session_user_id}")
- if not emotion:
- return "Please select an emotion.", gr.update() # No chart update
-
+ logger.info(f"Update Emotion UI: emotion='{emotion}'")
+ if not emotion: return "Please select an emotion.", gr.update()
add_emotion_record(session_user_id, emotion)
-
- # Refresh emotion chart
emotion_fig = create_emotion_chart(session_user_id)
-
- return f"Your current emotion has been updated to '{emotion}'.", gr.update(figure=emotion_fig)
-
+ # Clean emotion for display message
+ cleaned_emotion_display = emotion.split(" ")[0] if " " in emotion else emotion
+ return f"Emotion updated to '{cleaned_emotion_display}'.", gr.update(value=emotion_fig)
def display_recommendations(current_user_id):
- """Fetches and formats recommendations for display."""
+ """Fetches and formats recommendations."""
logger.info(f"Displaying recommendations for user {current_user_id}")
user_profile = get_user_profile(current_user_id)
recommendations = user_profile.get('recommendations', [])
-
- if not recommendations:
- return "No recommendations available yet. Chat with Aishura to get personalized suggestions!"
-
- # Show the most recent 5 recommendations (they are prepended)
- recent_recs = recommendations[:5]
-
+ if not recommendations: return "Chat with Aishura to get recommendations!"
+ recent_recs = recommendations[:5] # Latest 5
output_md = "# Your Latest Recommendations\n\n"
- if not recent_recs:
- output_md += "No recommendations yet."
- return output_md
-
+ if not recent_recs: return output_md + "No recommendations yet."
for i, rec_entry in enumerate(recent_recs, 1):
- rec = rec_entry.get('recommendation', {}) # Get the actual recommendation object
- output_md += f"### {i}. {rec.get('title', 'Recommendation')}\n"
- output_md += f"{rec.get('description', 'No details.')}\n"
- output_md += f"**Priority:** {rec.get('priority', 'N/A').title()} | "
- output_md += f"**Type:** {rec.get('action_type', 'N/A').replace('_', ' ').title()}\n"
- # output_md += f"*Generated: {rec_entry.get('date', 'N/A')}*\n" # Optional: show date
- output_md += "---\n"
-
+ rec = rec_entry.get('recommendation', {})
+ output_md += f"### {i}. {rec.get('title', 'N/A')}\n"
+ output_md += f"{rec.get('description', 'N/A')}\n"
+ output_md += f"**Priority:** {rec.get('priority', 'N/A').title()} | **Type:** {rec.get('action_type', 'N/A').replace('_', ' ').title()}\n---\n"
return output_md
# --- Build Gradio Interface ---
@@ -1521,214 +1077,120 @@ def create_interface():
name_input = gr.Textbox(label="Your Name", placeholder="e.g., Alex Chen")
location_input = gr.Textbox(label="Your Location", placeholder="e.g., London, UK")
with gr.Column():
+ # Updated label and choices
emotion_dropdown = gr.Dropdown(choices=EMOTIONS, label="How are you feeling today?")
- goal_dropdown = gr.Dropdown(choices=GOAL_TYPES, label="What's your main career goal?")
+ goal_dropdown = gr.Dropdown(choices=GOAL_TYPES, label="What's your main goal?") # Changed Label
welcome_button = gr.Button("Start My Journey")
- welcome_output = gr.Markdown() # For validation messages
+ welcome_output = gr.Markdown()
- # --- Main App Interface (Initially Hidden) ---
+ # --- Main App Interface ---
with gr.Group(visible=False) as main_interface:
with gr.Tabs() as tabs:
-
# --- Chat Tab ---
with gr.TabItem("π¬ Chat"):
with gr.Row():
with gr.Column(scale=3):
+ # Corrected Chatbot initialization
chatbot = gr.Chatbot(
- label="Aishura Assistant",
- height=550,
- avatar_images=("./user_avatar.png", "./aishura_avatar.png"), # Provide paths to avatar images if available
- bubble_full_width=False,
+ label="Aishura Assistant", height=550, type="messages", # Added type='messages'
+ avatar_images=("./user_avatar.png", "./aishura_avatar.png"),
show_copy_button=True
- )
- # --- Simulated Emotion Message Area ---
- emotion_message_area = gr.Markdown("", visible=False, elem_classes="subtle-message") # Hidden initially
- # --- Chat Input ---
- msg_textbox = gr.Textbox(
- show_label=False,
- placeholder="Type your message here and press Enter...",
- container=False,
- scale=1 # Take full width below chatbot
+ # Removed bubble_full_width
)
+ emotion_message_area = gr.Markdown("", visible=False, elem_classes="subtle-message")
+ msg_textbox = gr.Textbox(show_label=False, placeholder="Type your message...", container=False, scale=1)
with gr.Column(scale=1):
gr.Markdown("### β¨ Recommendations")
- recommendation_output = gr.Markdown(value="Chat with Aishura to get recommendations.")
+ recommendation_output = gr.Markdown(value="Chat for recommendations.")
refresh_recs_button = gr.Button("π Refresh Recommendations")
-
# --- Analysis Tab ---
with gr.TabItem("π Analysis"):
with gr.Tabs() as analysis_subtabs:
with gr.TabItem("π Resume"):
gr.Markdown("### Resume Analysis")
- gr.Markdown("Paste your full resume below. Aishura can analyze it against your career goals and help identify strengths and areas for improvement.")
- resume_text_input = gr.Textbox(label="Paste Resume Text Here", lines=15, placeholder="Your resume content...")
+ gr.Markdown("Paste resume below for analysis against your goals.")
+ resume_text_input = gr.Textbox(label="Paste Resume Text Here", lines=15)
analyze_resume_button = gr.Button("Analyze My Resume")
resume_analysis_output = gr.Markdown()
with gr.TabItem("π¨ Portfolio"):
gr.Markdown("### Portfolio Analysis")
- gr.Markdown("Provide a link and/or description of your portfolio (e.g., website, GitHub, Behance).")
- portfolio_url_input = gr.Textbox(label="Portfolio URL (Optional)", placeholder="[https://your-portfolio.com](https://your-portfolio.com)")
- portfolio_desc_input = gr.Textbox(label="Portfolio Description", lines=5, placeholder="Describe your portfolio's purpose, key projects, and target audience...")
+ gr.Markdown("Provide link/description.")
+ portfolio_url_input = gr.Textbox(label="Portfolio URL (Optional)")
+ portfolio_desc_input = gr.Textbox(label="Portfolio Description", lines=5)
analyze_portfolio_button = gr.Button("Analyze My Portfolio")
portfolio_analysis_output = gr.Markdown()
with gr.TabItem("π‘ Skills"):
gr.Markdown("### Skill Assessment")
- gr.Markdown("This chart visualizes skills identified from your latest resume analysis.")
- skill_radar_chart_output = gr.Plot(label="Skill Radar Chart")
-
+ gr.Markdown("Visualize skills from resume analysis.")
+ skill_radar_chart_output = gr.Plot(label="Skill Radar Chart") # Corrected: Plot init
- # --- Tools Tab ---
+ # --- Tools Tab (Removed Job Search) ---
with gr.TabItem("π οΈ Tools"):
with gr.Tabs() as tools_subtabs:
- with gr.TabItem("π Job Search"):
- gr.Markdown("### Find Job Opportunities")
- gr.Markdown("Use this tool to search for jobs based on keywords and location.")
- job_query_input = gr.Textbox(label="Job Title/Keyword", placeholder="e.g., Software Engineer, Marketing Manager")
- job_location_input = gr.Textbox(label="Location", placeholder="e.g., New York, Remote")
- job_results_slider = gr.Slider(minimum=5, maximum=20, value=10, step=1, label="Number of Results")
- search_jobs_button = gr.Button("Search for Jobs")
- job_search_output = gr.Markdown()
+ # Removed Job Search TabItem
with gr.TabItem("π Templates"):
gr.Markdown("### Generate Document Templates")
- gr.Markdown("Get started with common career documents.")
+ gr.Markdown("Get started with career documents.")
doc_type_dropdown = gr.Dropdown(choices=["Resume", "Cover Letter", "LinkedIn Summary", "Networking Email"], label="Select Document Type")
- doc_field_input = gr.Textbox(label="Career Field (Optional)", placeholder="e.g., Healthcare, Technology")
+ doc_field_input = gr.Textbox(label="Career Field (Optional)")
doc_exp_dropdown = gr.Dropdown(choices=["Entry-Level", "Mid-Career", "Senior-Level", "Student/Intern"], label="Experience Level")
generate_template_button = gr.Button("Generate Template")
template_output_md = gr.Markdown()
with gr.TabItem("π
Routine"):
gr.Markdown("### Create a Personalized Routine")
- gr.Markdown("Develop a daily or weekly plan to work towards your goals, tailored to how you feel.")
- routine_emotion_dropdown = gr.Dropdown(choices=EMOTIONS, label="How are you feeling about this goal?")
- routine_goal_input = gr.Textbox(label="Specific Goal for this Routine", placeholder="e.g., Apply to 5 jobs, Learn basic Python")
- routine_time_slider = gr.Slider(minimum=15, maximum=120, value=45, step=15, label="Minutes Available Per Day")
+ gr.Markdown("Develop a plan tailored to your goals and feelings.")
+ routine_emotion_dropdown = gr.Dropdown(choices=EMOTIONS, label="How are you feeling?")
+ routine_goal_input = gr.Textbox(label="Specific Goal", placeholder="e.g., Apply to 5 jobs")
+ routine_time_slider = gr.Slider(minimum=15, maximum=120, value=45, step=15, label="Minutes/Day")
routine_days_slider = gr.Slider(minimum=3, maximum=21, value=7, step=1, label="Routine Length (Days)")
create_routine_button = gr.Button("Create My Routine")
routine_output_md = gr.Markdown()
-
# --- Progress Tab ---
with gr.TabItem("π Progress"):
gr.Markdown("## Track Your Journey")
with gr.Row():
with gr.Column(scale=1):
- gr.Markdown("### Mark Task Complete")
- task_input = gr.Textbox(label="Task Name", placeholder="e.g., Updated LinkedIn Profile")
- complete_button = gr.Button("Complete Task")
- task_output = gr.Markdown()
- gr.Markdown("---")
- gr.Markdown("### Update Emotion")
- new_emotion_dropdown = gr.Dropdown(choices=EMOTIONS, label="How are you feeling now?")
- emotion_button = gr.Button("Update Feeling")
- emotion_output = gr.Markdown()
+ gr.Markdown("### Mark Task Complete"); task_input = gr.Textbox(label="Task Name"); complete_button = gr.Button("Complete Task"); task_output = gr.Markdown()
+ gr.Markdown("---"); gr.Markdown("### Update Emotion"); new_emotion_dropdown = gr.Dropdown(choices=EMOTIONS, label="How are you feeling now?"); emotion_button = gr.Button("Update Feeling"); emotion_output = gr.Markdown()
with gr.Column(scale=2):
gr.Markdown("### Visualizations")
- with gr.Row():
- emotion_chart_output = gr.Plot(label="Emotional Journey")
- progress_chart_output = gr.Plot(label="Progress Points")
- with gr.Row():
- routine_gauge_output = gr.Plot(label="Routine Completion")
- # Maybe add skill chart here too? Or keep in Analysis.
- gr.Markdown("") # Spacer
-
+ with gr.Row(): emotion_chart_output = gr.Plot(label="Emotional Journey") # Corrected: Plot init
+ with gr.Row(): progress_chart_output = gr.Plot(label="Progress Points") # Corrected: Plot init
+ with gr.Row(): routine_gauge_output = gr.Plot(label="Routine Completion") # Corrected: Plot init
# --- Event Wiring ---
-
- # Welcome screen action
welcome_button.click(
- fn=welcome,
- inputs=[name_input, location_input, emotion_dropdown, goal_dropdown],
- outputs=[chatbot, welcome_group, main_interface, # Show/hide groups
- emotion_chart_output, progress_chart_output, routine_gauge_output, skill_radar_chart_output] # Populate initial charts
+ fn=welcome, inputs=[name_input, location_input, emotion_dropdown, goal_dropdown],
+ outputs=[chatbot, welcome_group, main_interface, emotion_chart_output, progress_chart_output, routine_gauge_output, skill_radar_chart_output]
)
-
- # Chat submission
- msg_textbox.submit(
- fn=chat_submit,
- inputs=[msg_textbox, chatbot],
- outputs=[chatbot, msg_textbox, recommendation_output] # Update chatbot, clear input, refresh recs
- )
-
- # Recommendation refresh button
- refresh_recs_button.click(
- fn=lambda: display_recommendations(session_user_id), # Use lambda to pass user_id
- inputs=[],
- outputs=[recommendation_output]
- )
-
- # --- Simulated Emotion Message Wiring ---
- # Simple simulation: Show/hide message on focus/blur (or change)
- # msg_textbox.focus(fn=handle_chat_focus, outputs=[emotion_message_area])
- # msg_textbox.blur(fn=hide_emotion_message, outputs=[emotion_message_area])
- # Example: Show retype message briefly on change, then hide
- # msg_textbox.change(fn=show_retype_message, outputs=emotion_message_area).then(
- # fn=hide_emotion_message, outputs=emotion_message_area, js="() => { return new Promise(resolve => setTimeout(() => { resolve('') }, 2000)) }")
-
-
- # Analysis Tab Wiring
- analyze_resume_button.click(
- fn=analyze_resume_interface_handler,
- inputs=[resume_text_input],
- outputs=[resume_analysis_output, skill_radar_chart_output] # Update analysis text and skill chart
- )
- analyze_portfolio_button.click(
- fn=analyze_portfolio_interface_handler,
- inputs=[portfolio_url_input, portfolio_desc_input],
- outputs=[portfolio_analysis_output]
- )
-
- # Tools Tab Wiring
- search_jobs_button.click(
- fn=search_jobs_interface_handler,
- inputs=[job_query_input, job_location_input, job_results_slider],
- outputs=[job_search_output]
- )
- generate_template_button.click(
- fn=generate_template_interface_handler,
- inputs=[doc_type_dropdown, doc_field_input, doc_exp_dropdown],
- outputs=[template_output_md]
- )
- create_routine_button.click(
- fn=create_routine_interface_handler,
- inputs=[routine_emotion_dropdown, routine_goal_input, routine_time_slider, routine_days_slider],
- outputs=[routine_output_md, routine_gauge_output] # Update routine text and gauge chart
- )
-
- # Progress Tab Wiring
- complete_button.click(
- fn=complete_task_handler,
- inputs=[task_input],
- outputs=[task_output, task_input, # Update message, clear input
- emotion_chart_output, progress_chart_output, routine_gauge_output] # Update all charts
- )
- emotion_button.click(
- fn=update_emotion_handler,
- inputs=[new_emotion_dropdown],
- outputs=[emotion_output, emotion_chart_output] # Update message and emotion chart
- )
-
- # Load initial state for elements that need it (e.g., charts if resuming session)
- # app.load(...) could be used here if state management was more robust.
-
+ msg_textbox.submit( fn=chat_submit, inputs=[msg_textbox, chatbot], outputs=[chatbot, msg_textbox, recommendation_output] )
+ refresh_recs_button.click( fn=lambda: display_recommendations(session_user_id), outputs=[recommendation_output] )
+ # Analysis
+ analyze_resume_button.click( fn=analyze_resume_interface_handler, inputs=[resume_text_input], outputs=[resume_analysis_output, skill_radar_chart_output] )
+ analyze_portfolio_button.click( fn=analyze_portfolio_interface_handler, inputs=[portfolio_url_input, portfolio_desc_input], outputs=[portfolio_analysis_output] )
+ # Tools
+ generate_template_button.click( fn=generate_template_interface_handler, inputs=[doc_type_dropdown, doc_field_input, doc_exp_dropdown], outputs=[template_output_md] )
+ create_routine_button.click( fn=create_routine_interface_handler, inputs=[routine_emotion_dropdown, routine_goal_input, routine_time_slider, routine_days_slider], outputs=[routine_output_md, routine_gauge_output] )
+ # Progress
+ complete_button.click( fn=complete_task_handler, inputs=[task_input], outputs=[task_output, task_input, emotion_chart_output, progress_chart_output, routine_gauge_output] )
+ emotion_button.click( fn=update_emotion_handler, inputs=[new_emotion_dropdown], outputs=[emotion_output, emotion_chart_output] )
return app
# --- Main Execution ---
if __name__ == "__main__":
- if not OPENAI_API_KEY or not SERPER_API_KEY:
- print("*****************************************************")
- print("Warning: API keys for OpenAI or Serper not found.")
- print("Please set OPENAI_API_KEY and SERPER_API_KEY environment variables.")
- print("You can create a .env file in the same directory:")
- print("OPENAI_API_KEY=your_openai_key")
- print("SERPER_API_KEY=your_serper_key")
- print("*****************************************************")
+ if not OPENAI_API_KEY:
+ print("\n" + "*"*60)
+ print(" Warning: OPENAI_API_KEY environment variable not found. ")
+ print(" AI features require a valid OpenAI API key. ")
+ print(" Create a '.env' file with: OPENAI_API_KEY=your_openai_key ")
+ print("*"*60 + "\n")
# Decide whether to exit or continue with limited functionality
# exit(1)
logger.info("Starting Aishura Gradio application...")
aishura_app = create_interface()
- # Consider adding share=False for local testing, share=True for public link
- aishura_app.launch(share=False)
+ aishura_app.launch(share=False) # Set share=True for public link if needed
logger.info("Aishura Gradio application stopped.")
\ No newline at end of file