Akshay Chame
π Add LinkedIn Profile Enhancer Streamlit app with all agents and dependencies
035c4af
import streamlit as st | |
import json | |
import pandas as pd | |
from agents.orchestrator import ProfileOrchestrator | |
from agents.scraper_agent import ScraperAgent | |
from agents.content_agent import ContentAgent | |
import plotly.express as px | |
import plotly.graph_objects as go | |
from datetime import datetime | |
# Configure Streamlit page | |
st.set_page_config( | |
page_title="π LinkedIn Profile Enhancer", | |
page_icon="π", | |
layout="wide", | |
initial_sidebar_state="expanded" | |
) | |
# Custom CSS for better styling | |
st.markdown(""" | |
<style> | |
.main-header { | |
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); | |
padding: 2rem; | |
border-radius: 10px; | |
color: white; | |
text-align: center; | |
margin-bottom: 2rem; | |
} | |
.metric-card { | |
background: #f8f9fa; | |
padding: 1rem; | |
border-radius: 8px; | |
border-left: 4px solid #667eea; | |
margin: 0.5rem 0; | |
} | |
.success-card { | |
background: #d4edda; | |
padding: 1rem; | |
border-radius: 8px; | |
border-left: 4px solid #28a745; | |
margin: 0.5rem 0; | |
} | |
.warning-card { | |
background: #fff3cd; | |
padding: 1rem; | |
border-radius: 8px; | |
border-left: 4px solid #ffc107; | |
margin: 0.5rem 0; | |
} | |
.info-card { | |
background: #e7f3ff; | |
padding: 1rem; | |
border-radius: 8px; | |
border-left: 4px solid #17a2b8; | |
margin: 0.5rem 0; | |
} | |
.stTabs > div > div > div > div { | |
padding: 1rem; | |
} | |
.profile-section { | |
background: white; | |
padding: 1.5rem; | |
border-radius: 10px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
margin: 1rem 0; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
def initialize_session_state(): | |
"""Initialize session state variables""" | |
if 'orchestrator' not in st.session_state: | |
st.session_state.orchestrator = ProfileOrchestrator() | |
if 'analysis_results' not in st.session_state: | |
st.session_state.analysis_results = None | |
if 'profile_data' not in st.session_state: | |
st.session_state.profile_data = None | |
if 'suggestions' not in st.session_state: | |
st.session_state.suggestions = None | |
if 'current_url' not in st.session_state: | |
st.session_state.current_url = None | |
def clear_results_if_url_changed(linkedin_url): | |
"""Clear cached results if URL has changed""" | |
if st.session_state.current_url != linkedin_url: | |
st.session_state.analysis_results = None | |
st.session_state.profile_data = None | |
st.session_state.suggestions = None | |
st.session_state.current_url = linkedin_url | |
st.cache_data.clear() # Clear any Streamlit cache | |
print(f"π URL changed to: {linkedin_url} - Clearing cached data") | |
def create_header(): | |
"""Create the main header""" | |
st.markdown(""" | |
<div class="main-header"> | |
<h1>π LinkedIn Profile Enhancer</h1> | |
<p style="font-size: 1.2em; margin: 1rem 0;">AI-powered LinkedIn profile analysis and enhancement suggestions</p> | |
<div style="display: flex; justify-content: center; gap: 2rem; margin-top: 1rem;"> | |
<div style="text-align: center;"> | |
<div style="font-size: 2em;">π</div> | |
<div>Real Scraping</div> | |
</div> | |
<div style="text-align: center;"> | |
<div style="font-size: 2em;">π€</div> | |
<div>AI Analysis</div> | |
</div> | |
<div style="text-align: center;"> | |
<div style="font-size: 2em;">π―</div> | |
<div>Smart Suggestions</div> | |
</div> | |
<div style="text-align: center;"> | |
<div style="font-size: 2em;">π</div> | |
<div>Data Insights</div> | |
</div> | |
</div> | |
</div> | |
""", unsafe_allow_html=True) | |
def create_sidebar(): | |
"""Create the sidebar with input controls""" | |
with st.sidebar: | |
st.header("π Configuration") | |
# LinkedIn URL input | |
linkedin_url = st.text_input( | |
"π LinkedIn Profile URL", | |
placeholder="https://linkedin.com/in/your-profile", | |
help="Enter the full LinkedIn profile URL to analyze" | |
) | |
# Job description input | |
job_description = st.text_area( | |
"π― Target Job Description (Optional)", | |
placeholder="Paste the job description here for tailored suggestions...", | |
height=150, | |
help="Include job description for personalized optimization" | |
) | |
# API Status | |
st.subheader("π API Status") | |
# Test API connections | |
if st.button("π Test Connections"): | |
with st.spinner("Testing API connections..."): | |
# Test Apify | |
try: | |
scraper = ScraperAgent() | |
apify_status = scraper.test_apify_connection() | |
if apify_status: | |
st.success("β Apify: Connected") | |
else: | |
st.error("β Apify: Failed") | |
except Exception as e: | |
st.error(f"β Apify: Error - {str(e)}") | |
# Test OpenAI | |
try: | |
content_agent = ContentAgent() | |
openai_status = content_agent.test_openai_connection() | |
if openai_status: | |
st.success("β OpenAI: Connected") | |
else: | |
st.error("β OpenAI: Failed") | |
except Exception as e: | |
st.error(f"β OpenAI: Error - {str(e)}") | |
# Examples | |
st.subheader("π‘ Example URLs") | |
example_urls = [ | |
"https://linkedin.com/in/example-profile", | |
"https://www.linkedin.com/in/sample-user" | |
] | |
for url in example_urls: | |
if st.button(f"π {url.split('/')[-1]}", key=url): | |
st.session_state.example_url = url | |
return linkedin_url, job_description | |
def create_metrics_display(analysis): | |
"""Create metrics display""" | |
col1, col2, col3, col4 = st.columns(4) | |
with col1: | |
st.metric( | |
"π Completeness Score", | |
f"{analysis.get('completeness_score', 0):.1f}%", | |
delta=None | |
) | |
with col2: | |
rating = analysis.get('overall_rating', 'Unknown') | |
st.metric( | |
"β Overall Rating", | |
rating, | |
delta=None | |
) | |
with col3: | |
st.metric( | |
"π― Job Match Score", | |
f"{analysis.get('job_match_score', 0):.1f}%", | |
delta=None | |
) | |
with col4: | |
keywords = analysis.get('keyword_analysis', {}) | |
found_count = len(keywords.get('found_keywords', [])) | |
st.metric( | |
"π Keywords Found", | |
found_count, | |
delta=None | |
) | |
def create_analysis_charts(analysis): | |
"""Create analysis charts""" | |
col1, col2 = st.columns(2) | |
with col1: | |
# Completeness breakdown | |
scores = { | |
'Profile Info': 20, | |
'About Section': 25, | |
'Experience': 25, | |
'Skills': 15, | |
'Education': 15 | |
} | |
fig_pie = px.pie( | |
values=list(scores.values()), | |
names=list(scores.keys()), | |
title="Profile Section Weights", | |
color_discrete_sequence=px.colors.qualitative.Set3 | |
) | |
fig_pie.update_layout(height=400) | |
st.plotly_chart(fig_pie, use_container_width=True) | |
with col2: | |
# Score comparison | |
current_score = analysis.get('completeness_score', 0) | |
target_score = 90 | |
fig_gauge = go.Figure(go.Indicator( | |
mode = "gauge+number+delta", | |
value = current_score, | |
domain = {'x': [0, 1], 'y': [0, 1]}, | |
title = {'text': "Profile Completeness"}, | |
delta = {'reference': target_score, 'increasing': {'color': "green"}}, | |
gauge = { | |
'axis': {'range': [None, 100]}, | |
'bar': {'color': "darkblue"}, | |
'steps': [ | |
{'range': [0, 50], 'color': "lightgray"}, | |
{'range': [50, 80], 'color': "gray"}, | |
{'range': [80, 100], 'color': "lightgreen"} | |
], | |
'threshold': { | |
'line': {'color': "red", 'width': 4}, | |
'thickness': 0.75, | |
'value': 90 | |
} | |
} | |
)) | |
fig_gauge.update_layout(height=400) | |
st.plotly_chart(fig_gauge, use_container_width=True) | |
def display_profile_data(profile_data): | |
"""Display scraped profile data in a structured format""" | |
if not profile_data: | |
st.warning("No profile data available") | |
return | |
# Profile Header with Image | |
st.subheader("π€ Profile Overview") | |
# Create columns for profile image and basic info | |
col1, col2, col3 = st.columns([1, 2, 2]) | |
with col1: | |
# Display profile image | |
profile_image = profile_data.get('profile_image_hq') or profile_data.get('profile_image') | |
if profile_image: | |
st.image(profile_image, width=150, caption="Profile Picture") | |
else: | |
st.markdown(""" | |
<div style="width: 150px; height: 150px; background-color: #f0f0f0; border-radius: 50%; | |
display: flex; align-items: center; justify-content: center; font-size: 48px;"> | |
π€ | |
</div> | |
""", unsafe_allow_html=True) | |
with col2: | |
st.markdown(f""" | |
<div class="info-card"> | |
<strong>Name:</strong> {profile_data.get('name', 'N/A')}<br> | |
<strong>Headline:</strong> {profile_data.get('headline', 'N/A')}<br> | |
<strong>Location:</strong> {profile_data.get('location', 'N/A')}<br> | |
<strong>Connections:</strong> {profile_data.get('connections', 'N/A')}<br> | |
<strong>Followers:</strong> {profile_data.get('followers', 'N/A')} | |
</div> | |
""", unsafe_allow_html=True) | |
with col3: | |
st.markdown(f""" | |
<div class="info-card"> | |
<strong>Current Job:</strong> {profile_data.get('job_title', 'N/A')}<br> | |
<strong>Company:</strong> {profile_data.get('company_name', 'N/A')}<br> | |
<strong>Industry:</strong> {profile_data.get('company_industry', 'N/A')}<br> | |
<strong>Email:</strong> {profile_data.get('email', 'N/A')}<br> | |
<strong>Profile URL:</strong> <a href="{profile_data.get('url', '#')}" target="_blank">View Profile</a> | |
</div> | |
""", unsafe_allow_html=True) | |
# About Section | |
if profile_data.get('about'): | |
st.subheader("π About Section") | |
st.markdown(f""" | |
<div class="profile-section"> | |
{profile_data.get('about', 'No about section available')} | |
</div> | |
""", unsafe_allow_html=True) | |
# Experience | |
if profile_data.get('experience'): | |
st.subheader("πΌ Experience") | |
for i, exp in enumerate(profile_data.get('experience', [])): | |
with st.expander(f"{exp.get('title', 'Position')} at {exp.get('company', 'Company')}", expanded=i==0): | |
col1, col2 = st.columns([2, 1]) | |
with col1: | |
st.write(f"**Duration:** {exp.get('duration', 'N/A')}") | |
st.write(f"**Location:** {exp.get('location', 'N/A')}") | |
if exp.get('description'): | |
st.write("**Description:**") | |
st.write(exp.get('description')) | |
with col2: | |
st.write(f"**Current Role:** {'Yes' if exp.get('is_current') else 'No'}") | |
# Skills | |
if profile_data.get('skills'): | |
st.subheader("π οΈ Skills") | |
skills = profile_data.get('skills', []) | |
if skills: | |
# Create a DataFrame for better display | |
skills_df = pd.DataFrame({'Skills': skills}) | |
st.dataframe(skills_df, use_container_width=True) | |
# Education | |
if profile_data.get('education'): | |
st.subheader("π Education") | |
for edu in profile_data.get('education', []): | |
st.markdown(f""" | |
<div class="info-card"> | |
<strong>{edu.get('degree', 'Degree')}</strong><br> | |
{edu.get('school', 'School')} | {edu.get('field', 'Field')}<br> | |
<em>{edu.get('year', 'Year')}</em> | |
</div> | |
""", unsafe_allow_html=True) | |
# Raw Data (collapsible) | |
with st.expander("π Raw JSON Data"): | |
st.json(profile_data) | |
def display_analysis_results(analysis): | |
"""Display analysis results""" | |
if not analysis: | |
st.warning("No analysis results available") | |
return | |
# Metrics | |
create_metrics_display(analysis) | |
# Charts | |
st.subheader("π Analysis Visualization") | |
create_analysis_charts(analysis) | |
# Strengths and Weaknesses | |
col1, col2 = st.columns(2) | |
with col1: | |
st.subheader("π Profile Strengths") | |
strengths = analysis.get('strengths', []) | |
if strengths: | |
for strength in strengths: | |
st.markdown(f""" | |
<div class="success-card"> | |
β {strength} | |
</div> | |
""", unsafe_allow_html=True) | |
else: | |
st.info("No specific strengths identified") | |
with col2: | |
st.subheader("π§ Areas for Improvement") | |
weaknesses = analysis.get('weaknesses', []) | |
if weaknesses: | |
for weakness in weaknesses: | |
st.markdown(f""" | |
<div class="warning-card"> | |
πΈ {weakness} | |
</div> | |
""", unsafe_allow_html=True) | |
else: | |
st.success("No major areas for improvement identified") | |
# Keyword Analysis | |
keyword_analysis = analysis.get('keyword_analysis', {}) | |
if keyword_analysis: | |
st.subheader("π Keyword Analysis") | |
col1, col2 = st.columns(2) | |
with col1: | |
found_keywords = keyword_analysis.get('found_keywords', []) | |
if found_keywords: | |
st.write("**Keywords Found:**") | |
st.write(", ".join(found_keywords[:10])) | |
with col2: | |
missing_keywords = keyword_analysis.get('missing_keywords', []) | |
if missing_keywords: | |
st.write("**Missing Keywords:**") | |
st.write(", ".join(missing_keywords[:5])) | |
def generate_suggestions_markdown(suggestions, profile_data=None): | |
"""Generate markdown content from suggestions""" | |
if not suggestions: | |
return "# LinkedIn Profile Enhancement Suggestions\n\nNo suggestions available." | |
# Get profile name for personalization | |
profile_name = profile_data.get('name', 'Your Profile') if profile_data else 'Your Profile' | |
current_date = datetime.now().strftime("%B %d, %Y") | |
markdown_content = f"""# LinkedIn Profile Enhancement Suggestions | |
**Profile:** {profile_name} | |
**Generated on:** {current_date} | |
**Powered by:** LinkedIn Profile Enhancer AI | |
--- | |
## π Table of Contents | |
""" | |
# Add table of contents | |
toc_items = [] | |
for category in suggestions.keys(): | |
if category == 'ai_generated_content': | |
toc_items.append("- [π€ AI-Generated Content Suggestions](#ai-generated-content-suggestions)") | |
else: | |
category_name = category.replace('_', ' ').title() | |
toc_items.append(f"- [π {category_name}](#{category.replace('_', '-').lower()})") | |
markdown_content += "\n".join(toc_items) + "\n\n---\n\n" | |
# Add suggestions content | |
for category, items in suggestions.items(): | |
if category == 'ai_generated_content': | |
markdown_content += "## π€ AI-Generated Content Suggestions\n\n" | |
ai_content = items if isinstance(items, dict) else {} | |
# Headlines | |
if 'ai_headlines' in ai_content and ai_content['ai_headlines']: | |
markdown_content += "### β¨ Professional Headlines\n\n" | |
for i, headline in enumerate(ai_content['ai_headlines'], 1): | |
cleaned_headline = headline.strip('"').replace('\\"', '"') | |
if cleaned_headline.startswith(('1.', '2.', '3.', '4.', '5.')): | |
cleaned_headline = cleaned_headline[2:].strip() | |
markdown_content += f"{i}. {cleaned_headline}\n" | |
markdown_content += "\n" | |
# About Section | |
if 'ai_about_section' in ai_content and ai_content['ai_about_section']: | |
markdown_content += "### π Enhanced About Section\n\n" | |
markdown_content += f"```\n{ai_content['ai_about_section']}\n```\n\n" | |
# Experience Descriptions | |
if 'ai_experience_descriptions' in ai_content and ai_content['ai_experience_descriptions']: | |
markdown_content += "### πΌ Experience Description Ideas\n\n" | |
for desc in ai_content['ai_experience_descriptions']: | |
markdown_content += f"- {desc}\n" | |
markdown_content += "\n" | |
else: | |
# Standard categories | |
category_name = category.replace('_', ' ').title() | |
markdown_content += f"## π {category_name}\n\n" | |
if isinstance(items, list): | |
for item in items: | |
markdown_content += f"- {item}\n" | |
else: | |
markdown_content += f"- {items}\n" | |
markdown_content += "\n" | |
# Add footer | |
markdown_content += """--- | |
## π Implementation Tips | |
### Getting Started | |
1. **Prioritize High-Impact Changes**: Start with headline and about section improvements | |
2. **Use Keywords Strategically**: Incorporate industry-relevant keywords naturally | |
3. **Maintain Authenticity**: Ensure all changes reflect your genuine experience and personality | |
4. **Regular Updates**: Keep your profile fresh with recent achievements and experiences | |
### Best Practices | |
- **Professional Photo**: Use a high-quality, professional headshot | |
- **Active Engagement**: Regularly share industry insights and engage with your network | |
- **Skills Endorsements**: Ask colleagues to endorse your key skills | |
- **Recommendations**: Request recommendations from supervisors and colleagues | |
- **Content Strategy**: Share articles, insights, and achievements regularly | |
### Measuring Success | |
- Monitor profile views and connection requests | |
- Track engagement on your posts and content | |
- Observe changes in recruiter outreach | |
- Measure network growth and quality | |
--- | |
*This report was generated by LinkedIn Profile Enhancer AI. For best results, implement changes gradually and monitor their impact on your profile performance.* | |
**Need Help?** Contact support or revisit the LinkedIn Profile Enhancer tool for updated suggestions. | |
""" | |
return markdown_content | |
def display_suggestions(suggestions): | |
"""Display enhancement suggestions with download option""" | |
if not suggestions: | |
st.warning("No suggestions available") | |
return | |
# Add download button at the top | |
col1, col2 = st.columns([1, 4]) | |
with col1: | |
# Generate markdown content | |
markdown_content = generate_suggestions_markdown( | |
suggestions, | |
st.session_state.get('profile_data') | |
) | |
# Create filename with timestamp | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
profile_name = "" | |
if st.session_state.get('profile_data'): | |
name = st.session_state.profile_data.get('name', '') | |
if name: | |
# Clean name for filename | |
profile_name = "".join(c for c in name if c.isalnum() or c in (' ', '_')).rstrip() | |
profile_name = profile_name.replace(' ', '_') + "_" | |
filename = f"linkedin_suggestions_{profile_name}{timestamp}.md" | |
st.download_button( | |
label="π₯ Download Suggestions", | |
data=markdown_content, | |
file_name=filename, | |
mime="text/markdown", | |
help="Download all suggestions as a markdown file", | |
use_container_width=True | |
) | |
with col2: | |
st.markdown("*π‘ Click the download button to save all suggestions as a markdown file for easy reference and implementation.*") | |
st.markdown("---") | |
# Display suggestions as before | |
for category, items in suggestions.items(): | |
if category == 'ai_generated_content': | |
st.subheader("π€ AI-Generated Content Suggestions") | |
ai_content = items if isinstance(items, dict) else {} | |
# Headlines | |
if 'ai_headlines' in ai_content and ai_content['ai_headlines']: | |
st.write("**β¨ Professional Headlines:**") | |
for i, headline in enumerate(ai_content['ai_headlines'], 1): | |
cleaned_headline = headline.strip('"').replace('\\"', '"') | |
if cleaned_headline.startswith(('1.', '2.', '3.', '4.', '5.')): | |
cleaned_headline = cleaned_headline[2:].strip() | |
st.write(f"{i}. {cleaned_headline}") | |
st.write("") | |
# About Section | |
if 'ai_about_section' in ai_content and ai_content['ai_about_section']: | |
st.write("**π Enhanced About Section:**") | |
st.code(ai_content['ai_about_section'], language='text') | |
st.write("") | |
# Experience Descriptions | |
if 'ai_experience_descriptions' in ai_content and ai_content['ai_experience_descriptions']: | |
st.write("**πΌ Experience Description Ideas:**") | |
for desc in ai_content['ai_experience_descriptions']: | |
st.write(f"β’ {desc}") | |
st.write("") | |
else: | |
# Standard categories | |
category_name = category.replace('_', ' ').title() | |
st.subheader(f"π {category_name}") | |
if isinstance(items, list): | |
for item in items: | |
st.write(f"β’ {item}") | |
else: | |
st.write(f"β’ {items}") | |
st.write("") | |
def main(): | |
"""Main Streamlit application""" | |
initialize_session_state() | |
create_header() | |
# Sidebar | |
linkedin_url, job_description = create_sidebar() | |
# Main content | |
if st.button("π Enhance Profile", type="primary", use_container_width=True): | |
if not linkedin_url.strip(): | |
st.error("Please enter a LinkedIn profile URL") | |
elif not any(pattern in linkedin_url.lower() for pattern in ['linkedin.com/in/', 'www.linkedin.com/in/']): | |
st.error("Please enter a valid LinkedIn profile URL") | |
else: | |
# Clear cached data if URL has changed | |
clear_results_if_url_changed(linkedin_url) | |
with st.spinner("π Analyzing LinkedIn profile..."): | |
try: | |
st.info(f"π Extracting data from: {linkedin_url}") | |
# Get profile data and analysis (force fresh extraction) | |
profile_data = st.session_state.orchestrator.scraper.extract_profile_data(linkedin_url) | |
st.info(f"β Profile data extracted for: {profile_data.get('name', 'Unknown')}") | |
analysis = st.session_state.orchestrator.analyzer.analyze_profile(profile_data, job_description) | |
suggestions = st.session_state.orchestrator.content_generator.generate_suggestions(analysis, job_description) | |
# Store in session state | |
st.session_state.profile_data = profile_data | |
st.session_state.analysis_results = analysis | |
st.session_state.suggestions = suggestions | |
st.success("β Profile analysis completed!") | |
except Exception as e: | |
st.error(f"β Error analyzing profile: {str(e)}") | |
# Display results if available | |
if st.session_state.profile_data or st.session_state.analysis_results: | |
st.markdown("---") | |
# Create tabs for different views | |
tab1, tab2, tab3, tab4 = st.tabs(["π Analysis", "π Scraped Data", "π― Suggestions", "π Implementation"]) | |
with tab1: | |
st.header("π Profile Analysis") | |
if st.session_state.analysis_results: | |
display_analysis_results(st.session_state.analysis_results) | |
else: | |
st.info("No analysis results available yet") | |
with tab2: | |
st.header("π Scraped Profile Data") | |
if st.session_state.profile_data: | |
display_profile_data(st.session_state.profile_data) | |
else: | |
st.info("No profile data available yet") | |
with tab3: | |
st.header("π― Enhancement Suggestions") | |
if st.session_state.suggestions: | |
display_suggestions(st.session_state.suggestions) | |
else: | |
st.info("No suggestions available yet") | |
with tab4: | |
st.header("π Implementation Roadmap") | |
if st.session_state.analysis_results: | |
recommendations = st.session_state.analysis_results.get('recommendations', []) | |
if recommendations: | |
st.subheader("π― Priority Actions") | |
for i, rec in enumerate(recommendations[:5], 1): | |
st.markdown(f""" | |
<div class="metric-card"> | |
<strong>{i}.</strong> {rec} | |
</div> | |
""", unsafe_allow_html=True) | |
st.subheader("π General Best Practices") | |
best_practices = [ | |
"Update your profile regularly with new achievements", | |
"Use professional keywords relevant to your industry", | |
"Engage with your network by sharing valuable content", | |
"Ask for recommendations from colleagues and clients", | |
"Monitor profile views and connection requests" | |
] | |
for practice in best_practices: | |
st.markdown(f""" | |
<div class="info-card"> | |
πΈ {practice} | |
</div> | |
""", unsafe_allow_html=True) | |
else: | |
st.info("Complete the analysis first to see implementation suggestions") | |
# Footer | |
st.markdown("---") | |
st.markdown(""" | |
<div style="text-align: center; color: #666; margin-top: 2rem;"> | |
<p>π <strong>LinkedIn Profile Enhancer</strong> | Powered by AI | Data scraped with respect to LinkedIn's ToS</p> | |
<p>Built with β€οΈ using Streamlit, OpenAI GPT-4o-mini, and Apify</p> | |
</div> | |
""", unsafe_allow_html=True) | |
if __name__ == "__main__": | |
main() |