diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,7 +1,8 @@ +# filename: app_openai_updated.py import gradio as gr import pandas as pd import numpy as np -import matplotlib.pyplot as plt +# import matplotlib.pyplot as plt # Not directly used for plotting import plotly.graph_objects as go import plotly.express as px from datetime import datetime, timedelta @@ -13,36 +14,50 @@ import requests from typing import List, Dict, Any, Optional import logging from dotenv import load_dotenv -import pytz +# import pytz # Not used import uuid import re -import base64 -from io import BytesIO -from PIL import Image +# import base64 # Not used +# from io import BytesIO # Not used +# from PIL import Image # Not used -# Import the updated Google GenAI SDK -from google import genai -from google.genai import types +# --- Use OpenAI library --- +import openai -# Load environment variables +# --- Load environment variables --- load_dotenv() -# Set up logging -logging.basicConfig(level=logging.INFO, +# --- Set up logging --- +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) -# Configure API keys -GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "your-api-key") -SERPER_API_KEY = os.getenv("SERPER_API_KEY", "your-serper-api-key") +# --- Configure API keys --- +# Make sure you have OPENAI_API_KEY and SERPER_API_KEY in your .env file or environment +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +SERPER_API_KEY = os.getenv("SERPER_API_KEY") -# Initialize the client with the API key - FIXED -client = genai.Client(api_key=GOOGLE_API_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.") -# Model configuration - using Gemini latest model -MODEL_ID = "gemini-2.0-flash-001" +# --- 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 -# Constants for global app +# --- 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"] USER_DB_PATH = "user_database.json" @@ -53,137 +68,179 @@ PORTFOLIO_FOLDER = "user_portfolios" os.makedirs(RESUME_FOLDER, exist_ok=True) os.makedirs(PORTFOLIO_FOLDER, exist_ok=True) -# Function declarations for tools -get_job_opportunities = types.FunctionDeclaration( - name="get_job_opportunities", - description="Get relevant job opportunities based on location and career goals", - parameters={ - "type": "OBJECT", - "properties": { - "location": { - "type": "STRING", - "description": "The city or country where the user is located", - }, - "career_goal": { - "type": "STRING", - "description": "The user's career goal or job interest", - }, - "max_results": { - "type": "NUMBER", - "description": "Maximum number of job opportunities to return", +# --- Tool Definitions for OpenAI --- +# Define functions that the AI can call. +# These will be implemented as Python functions below. + +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"], }, - }, - "required": ["location", "career_goal"], + } }, -) - -generate_document = types.FunctionDeclaration( - name="generate_document_template", - description="Generate a document template for job applications", - parameters={ - "type": "OBJECT", - "properties": { - "document_type": { - "type": "STRING", - "description": "Type of document to generate (Resume, Cover Letter, Self-introduction)", - }, - "career_field": { - "type": "STRING", - "description": "The career field or industry the document is for", + { + "type": "function", + "function": { + "name": "generate_document_template", + "description": "Generate a document template (like a resume or cover letter) based on type, career field, and experience level.", + "parameters": { + "type": "object", + "properties": { + "document_type": { + "type": "string", + "description": "Type of document (e.g., Resume, Cover Letter, Self-introduction).", + }, + "career_field": { + "type": "string", + "description": "The career field or industry.", + }, + "experience_level": { + "type": "string", + "description": "User's experience level (e.g., Entry, Mid, Senior).", + }, + }, + "required": ["document_type"], }, - "experience_level": { - "type": "STRING", - "description": "User's experience level (Entry, Mid, Senior)", - }, - }, - "required": ["document_type"], + } }, -) - -create_routine = types.FunctionDeclaration( - name="create_personalized_routine", - description="Create a personalized career development routine", - parameters={ - "type": "OBJECT", - "properties": { - "emotion": { - "type": "STRING", - "description": "User's current emotional state", - }, - "goal": { - "type": "STRING", - "description": "User's career goal", + { + "type": "function", + "function": { + "name": "create_personalized_routine", + "description": "Create a personalized daily or weekly career development routine based on the user's current emotion, goals, and available time.", + "parameters": { + "type": "object", + "properties": { + "emotion": { + "type": "string", + "description": "User's current primary emotional state (e.g., Unmotivated, Anxious).", + }, + "goal": { + "type": "string", + "description": "User's specific career goal for this routine.", + }, + "available_time_minutes": { + "type": "integer", + "description": "Available time in minutes per day (default 60).", + }, + "routine_length_days": { + "type": "integer", + "description": "Length of the routine in days (default 7).", + }, + }, + "required": ["emotion", "goal"], }, - "available_time_minutes": { - "type": "NUMBER", - "description": "Available time in minutes per day", - }, - "routine_length_days": { - "type": "NUMBER", - "description": "Length of routine in days", - }, - }, - "required": ["emotion", "goal"], + } }, -) - -analyze_resume = types.FunctionDeclaration( - name="analyze_resume", - description="Analyze a user's resume and provide feedback", - parameters={ - "type": "OBJECT", - "properties": { - "resume_text": { - "type": "STRING", - "description": "The full text of the user's resume", + { + "type": "function", + "function": { + "name": "analyze_resume", + "description": "Analyze the provided resume text and provide feedback, comparing it against the user's stated career goal.", + "parameters": { + "type": "object", + "properties": { + "resume_text": { + "type": "string", + "description": "The full text of the user's resume.", + }, + "career_goal": { + "type": "string", + "description": "The user's career goal or target job/industry to analyze against.", + }, + }, + "required": ["resume_text", "career_goal"], }, - "career_goal": { - "type": "STRING", - "description": "The user's career goal or job interest", - }, - }, - "required": ["resume_text"], + } }, -) - -analyze_portfolio = types.FunctionDeclaration( - name="analyze_portfolio", - description="Analyze a user's portfolio and provide feedback", - parameters={ - "type": "OBJECT", - "properties": { - "portfolio_url": { - "type": "STRING", - "description": "URL to the user's portfolio", - }, - "portfolio_description": { - "type": "STRING", - "description": "Description of the portfolio content", + { + "type": "function", + "function": { + "name": "analyze_portfolio", + "description": "Analyze a user's portfolio based on a URL (if provided) and a description, offering feedback relative to their career goal.", + "parameters": { + "type": "object", + "properties": { + "portfolio_url": { + "type": "string", + "description": "URL to the user's online portfolio (optional).", + }, + "portfolio_description": { + "type": "string", + "description": "Detailed description of the portfolio's content, purpose, and structure.", + }, + "career_goal": { + "type": "string", + "description": "The user's career goal or target job/industry to analyze against.", + }, + }, + "required": ["portfolio_description", "career_goal"], }, - "career_goal": { - "type": "STRING", - "description": "The user's career goal or job interest", - }, - }, - "required": ["portfolio_description"], + } }, -) + { + "type": "function", + "function": { + "name": "extract_and_rate_skills_from_resume", + "description": "Extracts key skills from resume text and rates them on a scale of 1-10 based on apparent proficiency shown in the resume.", + "parameters": { + "type": "object", + "properties": { + "resume_text": { + "type": "string", + "description": "The full text of the user's resume.", + }, + "max_skills": { + "type": "integer", + "description": "Maximum number of skills to extract (default 8).", + }, + }, + "required": ["resume_text"], + }, + } + } +] -# Combine tools -job_tool = types.Tool(function_declarations=[get_job_opportunities]) -document_tool = types.Tool(function_declarations=[generate_document]) -routine_tool = types.Tool(function_declarations=[create_routine]) -resume_tool = types.Tool(function_declarations=[analyze_resume]) -portfolio_tool = types.Tool(function_declarations=[analyze_portfolio]) +# --- 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 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: - return json.load(file) + db = json.load(file) + # Ensure chat history uses 'content' key for OpenAI compatibility + for user_id in db.get('users', {}): + if 'chat_history' not in db['users'][user_id]: + db['users'][user_id]['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') + return db except (FileNotFoundError, json.JSONDecodeError): - # Initialize empty database db = {'users': {}} save_user_database(db) return db @@ -211,10 +268,21 @@ def get_user_profile(user_id): "resume_path": "", "portfolio_path": "", "recommendations": [], - "chat_history": [], + "chat_history": [], # Initialize chat history "joined_date": datetime.now().strftime("%Y-%m-%d") } 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] def update_user_profile(user_id, updates): @@ -232,13 +300,13 @@ def add_task_to_user(user_id, task): if user_id in db['users']: if 'completed_tasks' not in db['users'][user_id]: db['users'][user_id]['completed_tasks'] = [] - + task_with_date = { "task": task, "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } db['users'][user_id]['completed_tasks'].append(task_with_date) - db['users'][user_id]['progress_points'] += random.randint(10, 25) + db['users'][user_id]['progress_points'] += random.randint(10, 25) # Keep random points for now save_user_database(db) return db['users'][user_id] @@ -248,13 +316,13 @@ def add_emotion_record(user_id, emotion): if user_id in db['users']: if 'daily_emotions' not in db['users'][user_id]: db['users'][user_id]['daily_emotions'] = [] - + emotion_record = { "emotion": emotion, "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } db['users'][user_id]['daily_emotions'].append(emotion_record) - db['users'][user_id]['current_emotion'] = emotion + db['users'][user_id]['current_emotion'] = emotion # Update current emotion save_user_database(db) return db['users'][user_id] @@ -264,1141 +332,1403 @@ def add_routine_to_user(user_id, routine): if user_id in db['users']: if 'routine_history' not in db['users'][user_id]: db['users'][user_id]['routine_history'] = [] - + routine_with_date = { - "routine": routine, + "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 + "completion": 0 # Start completion at 0 } - db['users'][user_id]['routine_history'].append(routine_with_date) + # Prepend to make the latest routine first (optional) + db['users'][user_id]['routine_history'].insert(0, routine_with_date) save_user_database(db) return db['users'][user_id] + def save_user_resume(user_id, resume_text): - """Save user's resume to file and update profile""" - # Create filename + """Save user's resume text to file and update profile path.""" + if not resume_text: return None filename = f"{user_id}_resume.txt" filepath = os.path.join(RESUME_FOLDER, filename) - - # Save resume text to file - with open(filepath, 'w') as file: - file.write(resume_text) - - # Update user profile - update_user_profile(user_id, {"resume_path": filepath}) - - return filepath - -def save_user_portfolio(user_id, portfolio_content): - """Save user's portfolio info to file and update profile""" - # Create filename + try: + 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 + except Exception as e: + logger.error(f"Error saving resume for user {user_id}: {e}") + return None + +def save_user_portfolio(user_id, portfolio_url, portfolio_description): + """Save user's portfolio info (URL and description) to file.""" + if not portfolio_description: return None filename = f"{user_id}_portfolio.json" filepath = os.path.join(PORTFOLIO_FOLDER, filename) - - # Save portfolio content to file - with open(filepath, 'w') as file: - json.dump(portfolio_content, file, indent=4) - - # Update user profile - update_user_profile(user_id, {"portfolio_path": filepath}) - - return filepath + 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) + update_user_profile(user_id, {"portfolio_path": filepath}) + logger.info(f"Portfolio info saved for user {user_id} at {filepath}") + return filepath + except Exception as e: + 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 to user's recommendations list""" + """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, + "recommendation": recommendation, # The AI generated recommendation object "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "status": "pending" # pending, completed, dismissed } - db['users'][user_id]['recommendations'].append(recommendation_with_date) + # 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] + save_user_database(db) return db['users'][user_id] -def add_chat_message(user_id, role, message): - """Add a message to the user's chat history""" +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 + 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, # user or assistant - "message": message, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "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:] + save_user_database(db) return db['users'][user_id] -# API Helper Functions -def search_jobs_with_serper(query, location, max_results=5): - """Search for job opportunities using Serper API""" +# --- 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."}) + try: headers = { 'X-API-KEY': SERPER_API_KEY, 'Content-Type': 'application/json' } - params = { 'q': f"{query} jobs in {location}", - 'num': max_results + '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', - headers=headers, - params=params + 'https://serper.dev/search', # Use the correct Serper endpoint + headers=headers, + params=params, + timeout=10 # Add a timeout ) - - if response.status_code == 200: - data = response.json() - # Extract job listings from search results - job_results = [] - - # Process organic results - if 'organic' in data: - for item in data['organic']: - if 'title' in item and 'link' in item and 'snippet' in item: - # Check if it looks like a job listing - if any(keyword in item['title'].lower() for keyword in ['job', 'career', 'position', 'hiring', 'work']): - job_results.append({ - 'title': item['title'], - 'company': extract_company_from_title(item['title']), - 'description': item['snippet'], - 'link': item['link'], - 'location': location, - 'date_posted': 'Recent' # Serper doesn't provide this directly - }) - - return job_results - else: - logger.error(f"Error from Serper API: {response.status_code} - {response.text}") - return [] + 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 search_jobs_with_serper: {str(e)}") - return [] + 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): - """Extract company name from job title if possible""" - # This is a simple heuristic and can be improved - if ' at ' in title: - return title.split(' at ')[1].strip() - if ' - ' in title: - return title.split(' - ')[1].strip() + """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" -def get_ai_response(user_id, user_input, context=None, generate_recommendations=True): - """Get AI response using Google GenAI""" + +# --- 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" + + 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" + ) + 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]" + ) + else: + template += "[Structure for this document type needs to be defined.]" + + 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.""" + 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 + +def analyze_resume(resume_text: str, career_goal: str) -> str: + """Provides a basic analysis structure for the resume.""" + 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 + +def analyze_portfolio(portfolio_description: str, career_goal: str, portfolio_url: str = "") -> str: + """Provides a basic analysis structure for the portfolio.""" + 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 + + +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. + """ + 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"] + found_skills = [] + resume_lower = resume_text.lower() + for skill in possible_skills: + if skill.lower() in resume_lower: + # Assign a random score for demonstration + 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)}, + ] + + + logger.info(f"Extracted skills (placeholder): {[s['name'] for s in found_skills]}") + return json.dumps({"skills": found_skills[:max_skills]}) # Return JSON string + + +# --- 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. + """ + 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) - - system_instruction = """ - You are Aishura, an emotionally intelligent AI career assistant. Your goal is to empathize with the user's emotions - and provide realistic information and actionable suggestions. Follow this structure: - 1. Recognize and acknowledge the user's emotion - 2. Respond with high-empathy message - 3. Suggest specific action based on their input - 4. Offer document support, job opportunities, or personalized routine - - Remember to be proactive and preemptive - suggest actions before the user asks. Your goal is to provide - end-to-end support for the user's career journey, from emotional support to concrete action. - - If the user has shared a resume or portfolio, refer to insights from those documents to provide - personalized guidance. - """ - - # Build conversation context - contents = [] - - # Add user profile information as context - profile_context = f""" - User Profile Information: - - Name: {user_profile.get('name', '')} - - Current emotion: {user_profile.get('current_emotion', '')} - - Career goal: {user_profile.get('career_goal', '')} - - Location: {user_profile.get('location', '')} + + # --- System Prompt --- + 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. + 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. """ - - # Add resume context if available - if user_profile.get('resume_path') and os.path.exists(user_profile.get('resume_path')): - try: - with open(user_profile.get('resume_path'), 'r') as file: - resume_text = file.read() - profile_context += f"\nUser Resume Summary: The user has shared their resume. They have experience in {resume_text[:100]}..." - except Exception as e: - logger.error(f"Error reading resume: {str(e)}") - - # Add portfolio context if available - if user_profile.get('portfolio_path') and os.path.exists(user_profile.get('portfolio_path')): - try: - with open(user_profile.get('portfolio_path'), 'r') as file: - portfolio_data = json.load(file) - profile_context += f"\nUser Portfolio: The user has shared their portfolio with URL: {portfolio_data.get('url', 'Not provided')}." - except Exception as e: - logger.error(f"Error reading portfolio: {str(e)}") - - # Start with context - user_context = types.Content( - role="user", - parts=[types.Part.from_text(profile_context)] - ) - contents.append(user_context) - - # Add previous context if provided - if context: - for msg in context: - if msg["role"] == "user": - contents.append(types.Content( - role="user", - parts=[types.Part.from_text(msg["message"])] - )) - else: - contents.append(types.Content( - role="model", - parts=[types.Part.from_text(msg["message"])] - )) - + + # --- 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 - contents.append(types.Content( - role="user", - parts=[types.Part.from_text(user_input)] - )) - - # Configure tools - tools = [job_tool, document_tool, routine_tool, resume_tool, portfolio_tool] - - # Get response - response = client.models.generate_content( + messages.append({"role": "user", "content": user_input}) + + # --- Initial API Call --- + logger.info(f"Sending {len(messages)} messages to OpenAI model {MODEL_ID}.") + response = client.chat.completions.create( model=MODEL_ID, - contents=contents, - system_instruction=system_instruction, - tools=tools, - generation_config=types.GenerationConfig( + messages=messages, + tools=tools_list, + tool_choice="auto", # Let the model decide whether to use tools + temperature=0.7, + max_tokens=1024 # Adjust as needed + ) + + response_message = response.choices[0].message + logger.info("Received initial response from OpenAI.") + + # --- Tool Call Handling --- + tool_calls = response_message.tool_calls + 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 + 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, + "analyze_portfolio": analyze_portfolio, + "extract_and_rate_skills_from_resume": extract_and_rate_skills_from_resume, + } + + 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 + 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."}) + }) + + + # --- 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_output_tokens=2048, - top_p=0.95, - top_k=40 + max_tokens=1024 + # No tool_choice here, we expect a natural language response ) - ) - - ai_response_text = response.text - - # Log the message in chat history + final_response_content = second_response.choices[0].message.content + logger.info("Received final response from OpenAI after tool calls.") + + else: + # No tool calls were made, use the first response + final_response_content = response_message.content + logger.info("No tool calls requested by AI.") + + + # --- Post-processing and Saving --- + 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) - add_chat_message(user_id, "assistant", ai_response_text) - - # Generate recommendations if enabled + # 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) + + + # Generate recommendations (consider doing this asynchronously) if generate_recommendations: - gen_recommendations(user_id, user_input, ai_response_text) - - return ai_response_text + # 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 + + return final_response_content + + 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." + 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." except Exception as e: - logger.error(f"Error in get_ai_response: {str(e)}") - return "I apologize, but I'm having trouble processing your request right now. Please try again later." + # 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." + + +# --- Recommendation Generation (Placeholder - Adapt for OpenAI) --- +def gen_recommendations_openai(user_id, user_input, ai_response): + """Generate recommendations using OpenAI (Adapt prompt and parsing).""" + logger.info(f"Generating recommendations for user {user_id}") + if not client: + logger.warning("OpenAI client not available for generating recommendations.") + return [] -def gen_recommendations(user_id, user_input, ai_response): - """Generate recommendations based on conversation""" try: user_profile = get_user_profile(user_id) - + prompt = f""" - Based on the following conversation between a user and Aishura (an AI career assistant), - generate 1-3 specific, actionable recommendations for the user's next steps in their career journey. - + 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. + User Profile: - - Current emotion: {user_profile.get('current_emotion', '')} - - Career goal: {user_profile.get('career_goal', '')} - - Location: {user_profile.get('location', '')} - - Recent Conversation: + - 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. + + Most Recent Interaction: User: {user_input} - - Aishura: {ai_response} - - Generate specific, actionable recommendations in JSON format: + Aishura (AI Assistant): {ai_response} + + Generate recommendations in this JSON format only: ```json [ {{ - "title": "Brief recommendation title", - "description": "Detailed recommendation description", - "action_type": "job_search|skill_building|networking|resume|portfolio|interview_prep|other", - "priority": "high|medium|low" + "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" }} ] ``` - - Focus on immediate, practical next steps that align with the user's goals and emotional state. + Provide only the JSON array, no introductory text. """ - - response = client.models.generate_content( - model=MODEL_ID, - contents=prompt + + 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 ) - - recommendation_text = response.text - - # Extract JSON from response + + recommendation_json_str = response.choices[0].message.content + logger.info(f"Raw recommendations JSON string: {recommendation_json_str}") + + + # Attempt to parse the JSON try: - # Find JSON content between ```json and ``` if present - if "```json" in recommendation_text and "```" in recommendation_text.split("```json")[1]: - json_str = recommendation_text.split("```json")[1].split("```")[0].strip() + # 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: - # Otherwise try to find anything that looks like JSON array - import re - json_match = re.search(r'(\[.*\])', recommendation_text, re.DOTALL) - if json_match: - json_str = json_match.group(1) - else: - json_str = recommendation_text - - recommendations = json.loads(json_str) - - # Add recommendations to user profile + logger.error(f"Unexpected JSON structure for recommendations: {type(recommendations_data)}") + return [] + + + # Add valid recommendations to user profile + valid_recs_added = 0 for rec in recommendations: - add_recommendation_to_user(user_id, rec) - - return recommendations - except json.JSONDecodeError: - logger.error(f"Failed to parse JSON from AI response: {recommendation_text}") - return [] - except Exception as e: - logger.error(f"Error in gen_recommendations: {str(e)}") - return [] + # 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}") -def create_personalized_routine_with_ai(user_id, emotion, goal, available_time=60, days=7): - """Create a personalized routine using AI""" - try: - user_profile = get_user_profile(user_id) - - prompt = f""" - Create a personalized {days}-day career development routine for a user who is feeling {emotion} and has a goal to {goal}. - They have about {available_time} minutes per day to dedicate to this routine. - - For each day, suggest 1-3 specific tasks that will help them make progress toward their goal while considering their emotional state. - - For each task provide: - 1. Task name - 2. Duration in minutes - 3. Points value (between 10-50) - 4. A brief description of why this task is valuable - - Format the routine as a JSON object with this structure: - ```json - {{ - "name": "Routine name", - "description": "Brief description of the routine", - "days": {days}, - "daily_tasks": [ - {{ - "day": 1, - "tasks": [ - {{ - "name": "Task name", - "points": 20, - "duration": 30, - "description": "Why this task is valuable" - }} - ] - }} - ] - }} - ``` - """ - - # Use resume and portfolio info if available - if user_profile.get('resume_path') and os.path.exists(user_profile.get('resume_path')): - try: - with open(user_profile.get('resume_path'), 'r') as file: - resume_text = file.read() - prompt += f"\n\nTailor the routine based on the user's resume. Here's a summary: {resume_text[:500]}..." - except Exception as e: - logger.error(f"Error reading resume: {str(e)}") - - if user_profile.get('portfolio_path') and os.path.exists(user_profile.get('portfolio_path')): - try: - with open(user_profile.get('portfolio_path'), 'r') as file: - portfolio_data = json.load(file) - prompt += f"\n\nConsider the user's portfolio when creating the routine. Portfolio URL: {portfolio_data.get('url', 'Not provided')}" - except Exception as e: - logger.error(f"Error reading portfolio: {str(e)}") - - response = client.models.generate_content( - model=MODEL_ID, - contents=prompt - ) - - routine_text = response.text - - # Extract JSON portion from the response - try: - # Find JSON content between ```json and ``` if present - if "```json" in routine_text and "```" in routine_text.split("```json")[1]: - json_str = routine_text.split("```json")[1].split("```")[0].strip() - else: - # Otherwise try to find anything that looks like JSON - import re - json_match = re.search(r'(\{.*\})', routine_text, re.DOTALL) - if json_match: - json_str = json_match.group(1) - else: - json_str = routine_text - - routine = json.loads(json_str) - - # Add to user's routines - user_profile = add_routine_to_user(user_id, routine) - return routine - except json.JSONDecodeError: - logger.error(f"Failed to parse JSON from AI response: {routine_text}") - # Fallback to a basic routine - return generate_basic_routine(emotion, goal, available_time, days) - except Exception as e: - logger.error(f"Error in create_personalized_routine_with_ai: {str(e)}") - # Fallback to a basic routine - return generate_basic_routine(emotion, goal, available_time, days) - -def generate_basic_routine(emotion, goal, available_time=60, days=7): - """Generate a basic routine as fallback""" - routine_types = { - "job_search": [ - {"name": "Research target companies", "points": 10, "duration": 20, "description": "Identify potential employers that align with your career goals"}, - {"name": "Update LinkedIn profile", "points": 15, "duration": 30, "description": "Keep your professional presence current and compelling"}, - {"name": "Practice interview questions", "points": 20, "duration": 45, "description": "Build confidence and prepare for upcoming opportunities"}, - {"name": "Reach out to a contact", "points": 25, "duration": 15, "description": "Grow your network and gather industry insights"} - ], - "skill_building": [ - {"name": "Complete one tutorial", "points": 20, "duration": 60, "description": "Develop practical skills in your field"}, - {"name": "Read industry article", "points": 10, "duration": 15, "description": "Stay current with trends and developments"}, - {"name": "Work on portfolio project", "points": 30, "duration": 90, "description": "Create tangible evidence of your abilities"}, - {"name": "Watch expert talk", "points": 15, "duration": 30, "description": "Learn from leaders in your field"} - ], - "motivation": [ - {"name": "Write in gratitude journal", "points": 10, "duration": 10, "description": "Cultivate a positive mindset to enhance motivation"}, - {"name": "Set 3 goals for the day", "points": 15, "duration": 15, "description": "Focus your energy on achievable tasks"}, - {"name": "Exercise break", "points": 20, "duration": 20, "description": "Boost energy and mood with physical activity"}, - {"name": "Reflect on progress", "points": 15, "duration": 15, "description": "Acknowledge achievements and identify next steps"} - ] - } - - # Select routine type based on goal - if "job" in goal.lower() or "company" in goal.lower(): - routine_type = "job_search" - elif "skill" in goal.lower() or "learn" in goal.lower(): - routine_type = "skill_building" - else: - # Default to motivation if feeling negative emotions - if emotion.lower() in ["unmotivated", "anxious", "confused", "overwhelmed", "discouraged"]: - routine_type = "motivation" - else: - routine_type = random.choice(list(routine_types.keys())) - - # Create daily plan - daily_tasks = [] - for day in range(1, days + 1): - # Randomly select 1-3 tasks for the day that fit within available time - available_tasks = routine_types[routine_type].copy() - random.shuffle(available_tasks) - day_tasks = [] - remaining_time = available_time - - for task in available_tasks: - if task["duration"] <= remaining_time and len(day_tasks) < 3: - day_tasks.append(task) - remaining_time -= task["duration"] - - if remaining_time < 10 or len(day_tasks) >= 3: - break - - daily_tasks.append({ - "day": day, - "tasks": day_tasks - }) - - routine = { - "name": f"{days}-Day {routine_type.replace('_', ' ').title()} Plan", - "description": f"A personalized routine to help you {goal} while managing feelings of {emotion}.", - "days": days, - "daily_tasks": daily_tasks - } - - return routine + logger.info(f"Added {valid_recs_added} recommendations for user {user_id}") + return recommendations # Return the raw list parsed -def generate_document_template_with_ai(document_type, career_field="", experience_level=""): - """Generate document templates using AI""" - try: - prompt = f""" - Create a detailed template for a {document_type} for someone in the {career_field} field - with {experience_level} experience level. - - The template should include all necessary sections and sample content that can be replaced. - Format it in markdown. - """ - - response = client.models.generate_content( - model=MODEL_ID, - contents=prompt - ) - - return response.text - except Exception as e: - logger.error(f"Error in generate_document_template_with_ai: {str(e)}") - return f"Error generating {document_type} template. Please try again later." + 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 [] -def analyze_resume_with_ai(user_id, resume_text): - """Analyze resume with AI and provide feedback""" - try: - user_profile = get_user_profile(user_id) - - prompt = f""" - Analyze the following resume for a user who has the career goal of: {user_profile.get('career_goal', 'improving their career')} - - Resume Text: - {resume_text} - - Provide detailed feedback on: - 1. Overall strengths and weaknesses - 2. Format and organization - 3. Content effectiveness for their career goal - 4. Specific improvement suggestions - 5. Keywords and skills that should be highlighted - - Format your analysis with markdown headings and bullet points. - """ - - response = client.models.generate_content( - model=MODEL_ID, - contents=prompt - ) - - # Save resume - save_user_resume(user_id, resume_text) - - return response.text except Exception as e: - logger.error(f"Error in analyze_resume_with_ai: {str(e)}") - return "I apologize, but I'm having trouble analyzing your resume right now. Please try again later." + logger.exception(f"Error in gen_recommendations_openai: {e}") + return [] -def analyze_portfolio_with_ai(user_id, portfolio_url, portfolio_description): - """Analyze portfolio with AI and provide feedback""" - try: - user_profile = get_user_profile(user_id) - - prompt = f""" - Analyze the following portfolio for a user who has the career goal of: {user_profile.get('career_goal', 'improving their career')} - - Portfolio URL: {portfolio_url} - Portfolio Description: {portfolio_description} - - Based on the description provided, analyze: - 1. How well the portfolio aligns with their career goal - 2. Strengths of the portfolio - 3. Areas for improvement - 4. Specific suggestions to enhance the portfolio - 5. How to better showcase skills relevant to their goal - - Format your analysis with markdown headings and bullet points. - """ - - response = client.models.generate_content( - model=MODEL_ID, - contents=prompt - ) - - # Save portfolio info - portfolio_content = { - "url": portfolio_url, - "description": portfolio_description - } - save_user_portfolio(user_id, portfolio_content) - - return response.text - except Exception as e: - logger.error(f"Error in analyze_portfolio_with_ai: {str(e)}") - return "I apologize, but I'm having trouble analyzing your portfolio right now. Please try again later." -# Chart and visualization functions +# --- 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. + 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: - # Return empty chart if no data - fig = px.line(title="Emotion Tracking: No data available yet") + fig = go.Figure() + fig.add_annotation(text="No emotion data tracked yet.", align='center', showarrow=False) + fig.update_layout(title="Emotion Tracking") return fig - - # Prepare data + emotion_values = { - "Unmotivated": 1, - "Anxious": 2, - "Confused": 3, - "Discouraged": 4, - "Overwhelmed": 5, - "Excited": 6 + "Unmotivated": 1, "Anxious": 2, "Confused": 3, + "Discouraged": 4, "Overwhelmed": 5, "Excited": 6 } - - dates = [] - emotion_scores = [] - emotion_names = [] - - for record in emotion_records: - dates.append(datetime.strptime(record['date'], "%Y-%m-%d %H:%M:%S")) - emotion = record['emotion'] - emotion_names.append(emotion) - emotion_scores.append(emotion_values.get(emotion, 3)) - - df = pd.DataFrame({ - 'Date': dates, - 'Emotion Score': emotion_scores, - 'Emotion': emotion_names - }) - - # Create chart - fig = px.line(df, x='Date', y='Emotion Score', markers=True, - labels={"Emotion Score": "Emotional State"}, - title="Your Emotional Journey") - - # Add emotion names as hover text - fig.update_traces(hovertemplate='%{x}
Feeling: %{text}', text=df['Emotion']) - - # Customize y-axis to show emotion names instead of numbers - fig.update_yaxes( - tickvals=list(emotion_values.values()), - ticktext=list(emotion_values.keys()) - ) - + 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 + 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") + 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 def create_progress_chart(user_id): - """Create a chart showing user's progress over time""" + """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: - # Return empty chart if no data - fig = px.line(title="Progress Tracking: No data available yet") + fig = go.Figure() + fig.add_annotation(text="No tasks completed yet.", align='center', showarrow=False) + fig.update_layout(title="Progress Tracking") return fig - - # Prepare data + + # 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 = [] - points = [] 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")) + for task in tasks: - dates.append(datetime.strptime(task['date'], "%Y-%m-%d %H:%M:%S")) - # Increment points (assuming each task has inherent points) - cumulative_points += 20 - points.append(cumulative_points) + 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 + points_timeline.append(cumulative_points) task_labels.append(task['task']) - - df = pd.DataFrame({ - 'Date': dates, - 'Points': points, - 'Task': task_labels - }) - - # Create chart - fig = px.line(df, x='Date', y='Points', markers=True, - title="Your Career Journey Progress") - - # Add task names as hover text - fig.update_traces(hovertemplate='%{x}
Points: %{y}
Task: %{text}', text=df['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 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: - # Return empty chart if no data - fig = go.Figure() - fig.add_annotation(text="No active routines yet", showarrow=False) + 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 - latest_routine = routines[-1] + + # Get the most recent routine (assuming prepend logic) + latest_routine = routines[0] completion = latest_routine.get('completion', 0) - - # Create gauge chart + 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': "Current Routine Completion"}, + title = {'text': f"{routine_name} Completion"}, gauge = { - 'axis': {'range': [None, 100]}, - 'bar': {'color': "darkblue"}, + 'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': "darkblue"}, + 'bar': {'color': "cornflowerblue"}, + 'bgcolor': "white", + 'borderwidth': 2, + 'bordercolor': "gray", 'steps': [ - {'range': [0, 30], 'color': "lightgray"}, - {'range': [30, 70], 'color': "gray"}, - {'range': [70, 100], 'color': "darkgray"} - ], + {'range': [0, 50], 'color': 'whitesmoke'}, + {'range': [50, 80], 'color': 'lightgray'}], 'threshold': { - 'line': {'color': "red", 'width': 4}, - 'thickness': 0.75, - 'value': 90 - } - } - )) - + 'line': {'color': "green", 'width': 4}, + 'thickness': 0.75, 'value': 90}})) # Threshold at 90% return fig + def create_skill_radar_chart(user_id): - """Create a radar chart of user's skills based on resume analysis""" + """ + Creates a radar chart of user's skills. + Requires skills data, potentially extracted by `extract_and_rate_skills_from_resume` tool. + """ + logger.info(f"Creating skill radar chart for user {user_id}") user_profile = get_user_profile(user_id) - - # If no resume, return empty chart - if not user_profile.get('resume_path') or not os.path.exists(user_profile.get('resume_path')): - fig = go.Figure() - fig.add_annotation(text="No resume data available yet", showarrow=False) - return fig - - # Read resume - try: - with open(user_profile.get('resume_path'), 'r') as file: - resume_text = file.read() - except Exception as e: - logger.error(f"Error reading resume: {str(e)}") + 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="Error reading resume data", showarrow=False) + fig.add_annotation(text="Upload & Analyze Resume for Skill Chart", showarrow=False) + fig.update_layout(title="Skill Assessment") return fig - - # Use AI to extract and score skills - prompt = f""" - Based on the following resume, identify 5-8 key skills and rate them on a scale of 1-10. - - Resume: - {resume_text[:2000]}... - - Return the results as a JSON object with this structure: - ```json - {{ - "skills": [ - {{"name": "Skill Name", "score": 7}}, - {{"name": "Another Skill", "score": 9}} - ] - }} - ``` - """ - + try: - response = client.models.generate_content( - model=MODEL_ID, - contents=prompt - ) - - skill_text = response.text - - # Extract JSON - if "```json" in skill_text and "```" in skill_text.split("```json")[1]: - json_str = skill_text.split("```json")[1].split("```")[0].strip() - else: - import re - json_match = re.search(r'(\{.*\})', skill_text, re.DOTALL) - if json_match: - json_str = json_match.group(1) - else: - json_str = skill_text - - skill_data = json.loads(json_str) - - # Create radar chart + 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'] - - # Prepare data for radar chart + # Limit to max 8 skills for readability + skills = skills[:8] + categories = [skill['name'] for skill in skills] values = [skill['score'] for skill in skills] - - # Add the first point at the end to close the loop - categories.append(categories[0]) - values.append(values[0]) - + + # Ensure the loop closes + if len(categories) > 2: + categories.append(categories[0]) + values.append(values[0]) + 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] - ) - ), + polar=dict(radialaxis=dict(visible=True, range=[0, 10])), showlegend=False, - title="Skill Assessment Based on Resume" + 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 - + except Exception as e: - logger.error(f"Error creating skill radar chart: {str(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 -# Gradio interface components +# --- Gradio Interface Components --- def create_interface(): - """Create the Gradio interface for Aishura MVP""" - - # Generate a unique user ID for this session + """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()) - - # Welcome page + 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 --- + def welcome(name, location, emotion, goal): - if not name or not location or not emotion or not goal: - return ("Please fill out all fields to continue.", - gr.update(visible=True), - gr.update(visible=False)) - - # Update user profile + """Handles the initial welcome screen submission.""" + logger.info(f"Welcome action for user {session_user_id}: 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 + "name": name, "location": location, "career_goal": goal }) - - # Record emotion - add_emotion_record(session_user_id, emotion) - - # Generate initial AI response - response = get_ai_response( - session_user_id, - f"I'm {name} from {location}. I'm feeling {emotion} and my career goal is to {goal}." - ) - - return (response, - gr.update(visible=False), - gr.update(visible=True)) - - # Chat function - def chat(message, history): - # Get user profile - user_profile = get_user_profile(session_user_id) - - # Convert history to the format expected by get_ai_response - context = [] - for h in history: - context.append({"role": "user", "message": h[0]}) - context.append({"role": "assistant", "message": h[1]}) - - # Get AI response - response = get_ai_response(session_user_id, message, context) - - # Return updated history and empty message - history.append((message, response)) - return history, "" - - # Function to search for jobs - def search_jobs_interface(query, location, max_results=5): - jobs = search_jobs_with_serper(query, location, int(max_results)) - - if not jobs: - return "No job opportunities found. Try adjusting your search terms." - - result = "## Job Opportunities Found\n\n" - for i, job in enumerate(jobs, 1): - result += f"### {i}. {job['title']}\n" - result += f"**Company:** {job['company']}\n" - result += f"**Location:** {job['location']}\n" - result += f"**Description:** {job['description']}\n" - result += f"**Link:** [Apply Here]({job['link']})\n\n" - - return result - - # Function to generate document templates - def generate_template(document_type, career_field, experience_level): - template = generate_document_template_with_ai(document_type, career_field, experience_level) - return template - - # Function to create personal routine - def create_personal_routine(emotion, goal, available_time, days): - routine = create_personalized_routine_with_ai( - session_user_id, emotion, goal, int(available_time), int(days) - ) - - # Format routine for display - result = f"# Your {routine['name']}\n\n" - result += f"{routine['description']}\n\n" - - for day_plan in routine['daily_tasks']: - result += f"## Day {day_plan['day']}\n\n" - for task in day_plan['tasks']: - result += f"- **{task['name']}** ({task['duration']} mins, {task['points']} points)\n" - result += f" *{task['description']}*\n\n" - - return result - - # Function to analyze resume - def analyze_resume_interface(resume_text): + 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?" + ai_response = get_ai_response(session_user_id, initial_input, generate_recommendations=True) + + # Initial chat history + initial_chat = [(initial_input, ai_response)] + + # 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 + + # 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, 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 + + ai_response = get_ai_response(session_user_id, message, generate_recommendations=True) + history.append((message, ai_response)) + + # Update recommendations display after chat + 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. + + # --- 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}" + + + 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}" + + + 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)) + 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" + 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" + 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" + 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() + + + 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: - return "Please enter your resume text." - - analysis = analyze_resume_with_ai(session_user_id, resume_text) - - # Update skill chart - skill_fig = create_skill_radar_chart(session_user_id) - - return analysis, skill_fig - - # Function to analyze portfolio - def analyze_portfolio_interface(portfolio_url, portfolio_description): + # Clear previous results if input is empty + return "Please paste your resume text above.", gr.update(figure=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) + + 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() + + + 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 enter a description of your portfolio." - - analysis = analyze_portfolio_with_ai(session_user_id, portfolio_url, portfolio_description) - return analysis - - # Function to mark a task as complete - def complete_task(task_name): + return "Please provide a description of your portfolio." + + 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) + + 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', [])]) + + 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}" + + # --- 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 a 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) - - # Update completion percentage of current routine - if user_profile.get('routine_history'): - latest_routine = user_profile['routine_history'][-1] - # Simple approach: increase completion by random amount between 5-15% - new_completion = min(100, latest_routine.get('completion', 0) + random.randint(5, 15)) - latest_routine['completion'] = new_completion - update_user_profile(session_user_id, {"routine_history": user_profile['routine_history']}) - - # Create updated charts + points_earned = 20 # Use a fixed value or get from task data if available + + # 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) + new_completion = min(100, latest_routine_entry.get('completion', 0) + increment) + latest_routine_entry['completion'] = new_completion + save_user_database(db) # Save updated 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"Task '{task_name}' completed! You earned {random.randint(10, 25)} points.", - "", - emotion_fig, - progress_fig, - gauge_fig - ) - - # Function to update emotion - def update_emotion(emotion): + + 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)) + + + 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 + add_emotion_record(session_user_id, emotion) - - # Create updated emotion chart + + # Refresh emotion chart emotion_fig = create_emotion_chart(session_user_id) - - return ( - f"Your emotional state has been updated to: {emotion}", - emotion_fig - ) - - # Function to display recommendations - def display_recommendations(): - user_profile = get_user_profile(session_user_id) + + return f"Your current emotion has been updated to '{emotion}'.", gr.update(figure=emotion_fig) + + + def display_recommendations(current_user_id): + """Fetches and formats recommendations for display.""" + 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. Continue chatting with Aishura to receive personalized suggestions." - - # Show the most recent 5 recommendations - recent_recs = recommendations[-5:] - - result = "# Your Personalized Recommendations\n\n" - - for i, rec in enumerate(recent_recs, 1): - recommendation = rec['recommendation'] - result += f"## {i}. {recommendation['title']}\n\n" - result += f"{recommendation['description']}\n\n" - result += f"**Priority:** {recommendation['priority'].title()}\n" - result += f"**Type:** {recommendation['action_type'].replace('_', ' ').title()}\n\n" - result += "---\n\n" - - return result - - # Create the interface - with gr.Blocks(theme=gr.themes.Soft()) as app: + 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] + + output_md = "# Your Latest Recommendations\n\n" + if not recent_recs: + output_md += "No recommendations yet." + return output_md + + 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" + + return output_md + + # --- Build Gradio Interface --- + with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky")) as app: gr.Markdown("# Aishura - Your AI Career Assistant") - - # Welcome page + + # --- Welcome Screen --- with gr.Group(visible=True) as welcome_group: - gr.Markdown("## Welcome to Aishura") - gr.Markdown("Let's start by getting to know you a little better.") - - name_input = gr.Textbox(label="Your Name") - location_input = gr.Textbox(label="Your Location (City/Country)") - emotion_dropdown = gr.Dropdown(choices=EMOTIONS, label="How are you feeling today?") - goal_dropdown = gr.Dropdown(choices=GOAL_TYPES, label="What's your career goal?") - - welcome_button = gr.Button("Get Started") - welcome_output = gr.Markdown() - - # Main interface + gr.Markdown("## Welcome to Aishura!") + gr.Markdown("Let's get acquainted. Tell me a bit about yourself.") + with gr.Row(): + with gr.Column(): + 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(): + 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?") + welcome_button = gr.Button("Start My Journey") + welcome_output = gr.Markdown() # For validation messages + + # --- Main App Interface (Initially Hidden) --- with gr.Group(visible=False) as main_interface: with gr.Tabs() as tabs: - # Chat tab - with gr.TabItem("Chat with Aishura"): + + # --- Chat Tab --- + with gr.TabItem("πŸ’¬ Chat"): with gr.Row(): - with gr.Column(scale=2): - chatbot = gr.Chatbot(height=500, avatar_images=["πŸ‘€", "πŸ€–"]) - msg = gr.Textbox(show_label=False, placeholder="Type your message here...", container=False) - - with gr.Column(scale=1): - gr.Markdown("## Your Recommendations") - recommendation_output = gr.Markdown() - refresh_recs_button = gr.Button("Refresh Recommendations") - - msg.submit(chat, [msg, chatbot], [chatbot, msg]) - refresh_recs_button.click(display_recommendations, [], recommendation_output) - - # Profile and Career Analysis tab - with gr.TabItem("Profile & Analysis"): - with gr.Tabs() as analysis_tabs: - # Resume Analysis - with gr.TabItem("Resume Analysis"): - gr.Markdown("## Resume Analysis") - resume_text = gr.Textbox(label="Paste your resume here", lines=10, placeholder="Copy and paste your entire resume here for analysis...") - analyze_resume_button = gr.Button("Analyze Resume") - resume_output = gr.Markdown() - skill_chart = gr.Plot(label="Skill Assessment") - - analyze_resume_button.click( - analyze_resume_interface, - [resume_text], - [resume_output, skill_chart] + with gr.Column(scale=3): + 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, + 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 ) - - # Portfolio Analysis - with gr.TabItem("Portfolio Analysis"): - gr.Markdown("## Portfolio Analysis") - portfolio_url = gr.Textbox(label="Portfolio URL", placeholder="https://your-portfolio-website.com") - portfolio_description = gr.Textbox(label="Describe your portfolio", lines=5, placeholder="Describe the content, structure, and purpose of your portfolio...") - analyze_portfolio_button = gr.Button("Analyze Portfolio") - portfolio_output = gr.Markdown() - - analyze_portfolio_button.click( - analyze_portfolio_interface, - [portfolio_url, portfolio_description], - portfolio_output - ) - - # Job Search tab - with gr.TabItem("Find Opportunities"): - gr.Markdown("## Search for Job Opportunities") - job_query = gr.Textbox(label="What kind of job are you looking for?") - job_location = gr.Textbox(label="Location") - job_results = gr.Slider(minimum=5, maximum=20, value=10, step=5, label="Number of Results") - - search_button = gr.Button("Search") - job_output = gr.Markdown() - - search_button.click(search_jobs_interface, [job_query, job_location, job_results], job_output) - - # Document Templates tab - with gr.TabItem("Document Templates"): - gr.Markdown("## Generate Document Templates") - doc_type = gr.Dropdown( - choices=["Resume", "Cover Letter", "Self-Introduction", "LinkedIn Profile", "Portfolio", "Interview Preparation"], - label="Document Type" - ) - career_field = gr.Textbox(label="Career Field/Industry") - experience = gr.Dropdown( - choices=["Entry Level", "Mid-Career", "Senior"], - label="Experience Level" - ) - - template_button = gr.Button("Generate Template") - template_output = gr.Markdown() - - template_button.click(generate_template, [doc_type, career_field, experience], template_output) - - # Personal Routine tab - with gr.TabItem("Personal Routine"): - gr.Markdown("## Create Your Personal Development Routine") - routine_emotion = gr.Dropdown(choices=EMOTIONS, label="Current Emotional State") - routine_goal = gr.Textbox(label="What specific goal are you working toward?") - time_available = gr.Slider(minimum=15, maximum=120, value=60, step=15, label="Minutes Available Per Day") - routine_days = gr.Slider(minimum=3, maximum=30, value=7, step=1, label="Length of Routine (Days)") - - routine_button = gr.Button("Create Routine") - routine_output = gr.Markdown() - - routine_button.click(create_personal_routine, - [routine_emotion, routine_goal, time_available, routine_days], - routine_output) - - # Progress Tracking tab - with gr.TabItem("Track Progress"): + with gr.Column(scale=1): + gr.Markdown("### ✨ Recommendations") + recommendation_output = gr.Markdown(value="Chat with Aishura to get 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...") + 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...") + 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") + + + # --- Tools Tab --- + 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() + with gr.TabItem("πŸ“ Templates"): + gr.Markdown("### Generate Document Templates") + gr.Markdown("Get started with common 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_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") + 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(): - gr.Markdown("## Mark Tasks as Complete") - task_input = gr.Textbox(label="Enter Task Name") - complete_button = gr.Button("Mark as Complete") + 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() - - with gr.Column(): - gr.Markdown("## Update Your Emotional State") - new_emotion = gr.Dropdown(choices=EMOTIONS, label="How are you feeling now?") - emotion_button = gr.Button("Update") + 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.Row(): - with gr.Column(): - emotion_chart = gr.Plot(label="Emotional Journey") - - with gr.Column(): - progress_chart = gr.Plot(label="Progress Journey") - - with gr.Row(): - gauge_chart = gr.Plot(label="Routine Completion") - - complete_button.click( - complete_task, - [task_input], - [task_output, task_input, emotion_chart, progress_chart, gauge_chart] - ) - - emotion_button.click( - update_emotion, - [new_emotion], - [emotion_output, emotion_chart] - ) - - # Welcome button action + 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 + + + # --- Event Wiring --- + + # Welcome screen action welcome_button.click( - welcome, - [name_input, location_input, emotion_dropdown, goal_dropdown], - [welcome_output, welcome_group, main_interface] + 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 + ) + + # Chat submission + msg_textbox.submit( + fn=chat_submit, + inputs=[msg_textbox, chatbot], + outputs=[chatbot, msg_textbox, recommendation_output] # Update chatbot, clear input, refresh recs ) - - # Load initial recommendations - app.load( - display_recommendations, - [], - recommendation_output + + # 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] ) - - return app -# Main function to launch the app -def main(): - app = create_interface() - app.launch(share=True) + # 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. + + + return app + +# --- Main Execution --- if __name__ == "__main__": - main() \ No newline at end of file + 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("*****************************************************") + # 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) + logger.info("Aishura Gradio application stopped.") \ No newline at end of file