""" CV + Portfolio Analyzer — Python/Streamlit Powered by Claude & Gemini · Multi-step Tool Use """ import json import os import streamlit as st import anthropic import google.generativeai as genai from google.generativeai.types import FunctionDeclaration, Tool as GeminiTool # ─── Page Config ─────────────────────────────────────────────────────────────── st.set_page_config( page_title="CV + Portfolio Analyzer", page_icon="📄", layout="wide", ) # ─── Custom CSS ──────────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ─── Tool Definitions ───────────────────────────────────────────────────────── TOOLS = [ { "name": "analyze_cv_sections", "description": "Analyze the CV structure, identify all sections present, rate each section quality, detect critical gaps, and provide improvement recommendations.", "input_schema": { "type": "object", "properties": { "sections_found": {"type": "array", "items": {"type": "string"}, "description": "Sections detected in the CV"}, "missing_sections": {"type": "array", "items": {"type": "string"}, "description": "Important missing sections"}, "section_scores": { "type": "object", "description": "Quality score per section (0-10)", "additionalProperties": {"type": "number"} }, "overall_score": {"type": "number", "description": "CV strength score (0-100)"}, "key_strengths": {"type": "array", "items": {"type": "string"}}, "critical_gaps": {"type": "array", "items": {"type": "string"}}, "recommendations": {"type": "array", "items": {"type": "string"}} }, "required": ["sections_found", "missing_sections", "overall_score", "key_strengths", "critical_gaps", "recommendations"] } }, { "name": "calculate_job_match", "description": "Compare CV against the job description. Identify matched/missing skills, calculate a match percentage, and give actionable tips.", "input_schema": { "type": "object", "properties": { "match_score": {"type": "number", "description": "Match percentage 0-100"}, "matched_skills": {"type": "array", "items": {"type": "string"}}, "missing_skills": {"type": "array", "items": {"type": "string"}}, "experience_match": {"type": "string"}, "highlight_points": {"type": "array", "items": {"type": "string"}, "description": "Strong alignment points to emphasize"}, "improvement_tips": {"type": "array", "items": {"type": "string"}} }, "required": ["match_score", "matched_skills", "missing_skills", "highlight_points", "improvement_tips"] } }, { "name": "write_cover_letter", "description": "Generate a personalized, compelling cover letter tailored to the job description using insights from the CV analysis.", "input_schema": { "type": "object", "properties": { "subject_line": {"type": "string"}, "cover_letter": {"type": "string", "description": "Full formatted cover letter"}, "key_selling_points": {"type": "array", "items": {"type": "string"}} }, "required": ["cover_letter", "key_selling_points"] } } ] # ─── Gemini Tool Definitions (function declarations) ────────────────────────── GEMINI_TOOLS = [GeminiTool(function_declarations=[ FunctionDeclaration( name="analyze_cv_sections", description="Analyze the CV structure, identify all sections present, rate each section quality, detect critical gaps, and provide improvement recommendations.", parameters={ "type": "object", "properties": { "sections_found": {"type": "array", "items": {"type": "string"}, "description": "Sections detected in the CV"}, "missing_sections": {"type": "array", "items": {"type": "string"}, "description": "Important missing sections"}, "overall_score": {"type": "number", "description": "CV strength score (0-100)"}, "key_strengths": {"type": "array", "items": {"type": "string"}}, "critical_gaps": {"type": "array", "items": {"type": "string"}}, "recommendations": {"type": "array", "items": {"type": "string"}}, }, "required": ["sections_found", "missing_sections", "overall_score", "key_strengths", "critical_gaps", "recommendations"], }, ), FunctionDeclaration( name="calculate_job_match", description="Compare CV against the job description. Identify matched/missing skills, calculate a match percentage, and give actionable tips.", parameters={ "type": "object", "properties": { "match_score": {"type": "number", "description": "Match percentage 0-100"}, "matched_skills": {"type": "array", "items": {"type": "string"}}, "missing_skills": {"type": "array", "items": {"type": "string"}}, "experience_match": {"type": "string"}, "highlight_points": {"type": "array", "items": {"type": "string"}}, "improvement_tips": {"type": "array", "items": {"type": "string"}}, }, "required": ["match_score", "matched_skills", "missing_skills", "highlight_points", "improvement_tips"], }, ), FunctionDeclaration( name="write_cover_letter", description="Generate a personalized, compelling cover letter tailored to the job description using insights from the CV analysis.", parameters={ "type": "object", "properties": { "subject_line": {"type": "string"}, "cover_letter": {"type": "string", "description": "Full formatted cover letter"}, "key_selling_points": {"type": "array", "items": {"type": "string"}}, }, "required": ["cover_letter", "key_selling_points"], }, ), ])] STEPS = [ {"id": "analyze_cv_sections", "label": "CV Analizi", "icon": "▣", "desc": "Bölümler, güçlü yanlar ve eksikler tespit ediliyor"}, {"id": "calculate_job_match", "label": "İş Eşleştirme", "icon": "◎", "desc": "Pozisyona uyum oranı hesaplanıyor"}, {"id": "write_cover_letter", "label": "Ön Yazı", "icon": "✦", "desc": "Kişiselleştirilmiş cover letter oluşturuluyor"}, ] # ─── Sample Data ─────────────────────────────────────────────────────────────── SAMPLE_CV = """John Doe john@example.com | +1 (555) 123-4567 | linkedin.com/in/johndoe | github.com/johndoe SUMMARY Full-stack developer with 4 years of experience building scalable web applications. Passionate about clean code, performance optimization, and developer experience. EXPERIENCE Senior Frontend Developer — TechCorp (2022–Present) - Built React/TypeScript dashboard used by 50k daily users - Reduced page load time by 40% through code splitting and lazy loading - Mentored 3 junior developers Frontend Developer — StartupXYZ (2020–2022) - Developed e-commerce platform with Next.js and Node.js - Integrated Stripe payment system and REST APIs EDUCATION B.Sc. Computer Science — State University (2016–2020) SKILLS JavaScript, TypeScript, React, Next.js, Node.js, PostgreSQL, Docker, AWS""" SAMPLE_JD = """Senior Full-Stack Engineer — FinTech Startup We're looking for an experienced full-stack engineer to join our growing team. Requirements: - 5+ years of full-stack development experience - Strong proficiency in React, TypeScript, and Node.js - Experience with microservices architecture - Knowledge of financial systems or payment processing (Stripe, Plaid) - GraphQL API design and implementation - AWS/GCP cloud infrastructure experience - Leadership and mentoring skills - Strong communication and teamwork""" # ─── Helper: colored badge ───────────────────────────────────────────────────── def badge(text: str, color: str = "#f59e0b", bg: str = "rgba(245,158,11,0.12)") -> str: return ( f'{text}' ) def green_badge(t): return badge(t, "#34d399", "rgba(16,185,129,0.12)") def red_badge(t): return badge(t, "#f87171", "rgba(239,68,68,0.12)") def amber_badge(t): return badge(t, "#fbbf24", "rgba(245,158,11,0.12)") def blue_badge(t): return badge(t, "#60a5fa", "rgba(59,130,246,0.12)") def score_circle(score: int, label: str, color: str = "#f59e0b"): pct = max(0, min(100, score)) return f"""
{score}
{label}
{score}/100
""" # ─── Core: run analysis ───────────────────────────────────────────────────────── def run_analysis(cv_text: str, job_desc: str, api_key: str): """Generator that yields status dicts as Claude streams tool results.""" client = anthropic.Anthropic(api_key=api_key) messages = [ { "role": "user", "content": ( "You are an expert career coach and CV analyzer. " "Analyze the following CV and job description thoroughly using all three available tools in sequence.\n\n" f"\n{cv_text}\n\n\n" f"\n{job_desc}\n\n\n" "Please:\n" "1. First call analyze_cv_sections to evaluate the CV quality\n" "2. Then call calculate_job_match to assess alignment with the job\n" "3. Finally call write_cover_letter to create a compelling application letter\n\n" "Use all three tools." ) } ] results = {"cv": None, "match": None, "letter": None} tool_log = [] continue_loop = True while continue_loop: yield {"type": "status", "msg": "Claude API'ye istek gönderiliyor..."} response = client.messages.create( model="claude-opus-4-5", max_tokens=4000, tools=TOOLS, messages=messages, ) messages.append({"role": "assistant", "content": response.content}) tool_use_blocks = [b for b in response.content if b.type == "tool_use"] if not tool_use_blocks: continue_loop = False break tool_results_msg = [] for block in tool_use_blocks: tool_name = block.name tool_input = block.input step_idx = next((i for i, s in enumerate(STEPS) if s["id"] == tool_name), -1) step_label = STEPS[step_idx]["label"] if step_idx >= 0 else tool_name yield {"type": "step", "step_idx": step_idx, "label": step_label} yield {"type": "status", "msg": f"{step_label} çalışıyor..."} # Store results if tool_name == "analyze_cv_sections": results["cv"] = tool_input elif tool_name == "calculate_job_match": results["match"] = tool_input elif tool_name == "write_cover_letter": results["letter"] = tool_input tool_log.append({"name": tool_name, "result": tool_input}) yield {"type": "tool_done", "name": tool_name, "result": tool_input, "results": results, "log": tool_log} tool_results_msg.append({ "type": "tool_result", "tool_use_id": block.id, "content": json.dumps(tool_input), }) messages.append({"role": "user", "content": tool_results_msg}) if response.stop_reason == "end_turn": continue_loop = False yield {"type": "done", "results": results, "log": tool_log} # ─── Core: run analysis with Gemini ──────────────────────────────────────────── def run_analysis_gemini(cv_text: str, job_desc: str, api_key: str): """Generator that yields status dicts as Gemini streams tool results.""" genai.configure(api_key=api_key) model = genai.GenerativeModel( model_name="gemini-2.0-flash", tools=GEMINI_TOOLS, system_instruction=( "You are an expert career coach and CV analyzer. " "Use all three available tools in sequence: " "first analyze_cv_sections, then calculate_job_match, then write_cover_letter." ), ) user_prompt = ( f"Analyze this CV and job description using all three tools in order.\n\n" f"\n{cv_text}\n\n\n" f"\n{job_desc}\n\n\n" "1. Call analyze_cv_sections\n" "2. Call calculate_job_match\n" "3. Call write_cover_letter\n" "Use all three tools." ) results = {"cv": None, "match": None, "letter": None} tool_log = [] history = [] # Ordered list of tools to call tools_to_call = ["analyze_cv_sections", "calculate_job_match", "write_cover_letter"] current_prompt = user_prompt for expected_tool in tools_to_call: yield {"type": "status", "msg": "Gemini API'ye istek gönderiliyor..."} chat = model.start_chat(history=history) response = chat.send_message(current_prompt) # Find function call in response fc = None for part in response.parts: if hasattr(part, "function_call") and part.function_call.name: fc = part.function_call break if fc is None: # Try to extract JSON from text as fallback break tool_name = fc.name # Convert Gemini MapComposite to plain dict tool_input = dict(fc.args) # Recursively convert nested MapComposite / ListValue objects tool_input = json.loads(json.dumps(tool_input, default=str)) # De-nest arrays stored as {"values": [...]} for k, v in tool_input.items(): if isinstance(v, dict) and list(v.keys()) == ["values"]: tool_input[k] = v["values"] step_idx = next((i for i, s in enumerate(STEPS) if s["id"] == tool_name), -1) step_label = STEPS[step_idx]["label"] if step_idx >= 0 else tool_name yield {"type": "step", "step_idx": step_idx, "label": step_label} yield {"type": "status", "msg": f"{step_label} çalışıyor..."} if tool_name == "analyze_cv_sections": results["cv"] = tool_input elif tool_name == "calculate_job_match": results["match"] = tool_input elif tool_name == "write_cover_letter": results["letter"] = tool_input tool_log.append({"name": tool_name, "result": tool_input}) yield {"type": "tool_done", "name": tool_name, "result": tool_input, "results": results, "log": tool_log} # Build history for next turn: assistant called the function, we return the result history = chat.history + [ { "role": "user", "parts": [{"function_response": {"name": tool_name, "response": {"result": str(tool_input)}}}], } ] current_prompt = ( f"Good. Now call the next tool." ) yield {"type": "done", "results": results, "log": tool_log} # ─── Session State Init ───────────────────────────────────────────────────────── for key, default in [ ("stage", "input"), # input | loading | results ("results", {}), ("tool_log", []), ("cv_text", SAMPLE_CV), ("job_desc", SAMPLE_JD), ("provider", "Claude"), ]: if key not in st.session_state: st.session_state[key] = default # ─── Header ──────────────────────────────────────────────────────────────────── st.markdown("""
CV
CV + Portfolio Analyzer
POWERED BY CLAUDE & GEMINI · MULTI-STEP TOOL USE
""" + green_badge("Function Calling") + " " + blue_badge("Context Management") + " " + amber_badge("Multi-step Reasoning") + """
""", unsafe_allow_html=True) # ─── Settings Expander (HF Spaces uyumlu) ──────────────────────────────── with st.expander("⚙️ Ayarlar — API Anahtarı & Model Seçimi", expanded=not st.session_state.get("active_key")): col_radio, col_key, col_info = st.columns([1, 2, 1]) with col_radio: provider = st.radio( "AI Sağlayıcı", options=["Claude", "Gemini"], index=0 if st.session_state.provider == "Claude" else 1, horizontal=False, help="Hangi AI modeli kullanılsın?" ) st.session_state.provider = provider with col_key: if provider == "Claude": api_key = st.text_input( "Anthropic API Anahtarı", value=os.environ.get("ANTHROPIC_API_KEY", ""), type="password", placeholder="sk-ant-...", help="https://console.anthropic.com" ) gemini_key = "" else: api_key = "" gemini_key = st.text_input( "Google Gemini API Anahtarı", value=os.environ.get("GOOGLE_API_KEY", ""), type="password", placeholder="AIza...", help="https://aistudio.google.com/app/apikey" ) with col_info: if provider == "Claude": st.markdown( '
🤖 claude-opus-4-5
', unsafe_allow_html=True ) else: st.markdown( '
🤖 gemini-2.0-flash
', unsafe_allow_html=True ) # ════════════════════════════════════════════════════════════════════════════════ # STAGE: INPUT # ════════════════════════════════════════════════════════════════════════════════ if st.session_state.stage == "input": col1, col2 = st.columns(2) with col1: hdr1, btn1 = st.columns([3, 1]) with hdr1: st.markdown('

CV / Özgeçmiş

', unsafe_allow_html=True) with btn1: if st.button("Örnek", key="sample_cv", help="Örnek CV yükle"): st.session_state.cv_text = SAMPLE_CV st.rerun() cv_input = st.text_area("cv_area", value=st.session_state.cv_text, height=360, label_visibility="collapsed", key="cv_input_area") with col2: hdr2, btn2 = st.columns([3, 1]) with hdr2: st.markdown('

İş İlanı

', unsafe_allow_html=True) with btn2: if st.button("Örnek", key="sample_jd", help="Örnek iş ilanı yükle"): st.session_state.job_desc = SAMPLE_JD st.rerun() jd_input = st.text_area("jd_area", value=st.session_state.job_desc, height=360, label_visibility="collapsed", key="jd_input_area") st.markdown("
", unsafe_allow_html=True) _, center, _ = st.columns([2, 1, 2]) with center: start = st.button("✦ Analizi Başlat", use_container_width=True) if start: active_key = api_key if st.session_state.provider == "Claude" else gemini_key if not active_key: pname = "Anthropic" if st.session_state.provider == "Claude" else "Google Gemini" st.error(f"⚠️ Lütfen sol panelden {pname} API anahtarınızı girin.") elif not cv_input.strip() or not jd_input.strip(): st.error("Lütfen CV ve iş ilanı alanlarını doldurun.") else: st.session_state.cv_text = cv_input st.session_state.job_desc = jd_input st.session_state.active_key = active_key st.session_state.stage = "loading" st.session_state.results = {} st.session_state.tool_log = [] st.rerun() # ════════════════════════════════════════════════════════════════════════════════ # STAGE: LOADING # ════════════════════════════════════════════════════════════════════════════════ elif st.session_state.stage == "loading": # Step progress header step_cols = st.columns(len(STEPS)) step_placeholders = [] for i, step in enumerate(STEPS): with step_cols[i]: ph = st.empty() ph.markdown( f'
' f'
{step["icon"]}
' f'
{step["label"]}
' f'
{step["desc"]}
' f'
', unsafe_allow_html=True ) step_placeholders.append(ph) st.markdown("
", unsafe_allow_html=True) status_ph = st.empty() progress_ph = st.empty() log_ph = st.empty() active_step = 0 partial_results = {} log_entries = [] def render_step(idx, done_steps): for i, step in enumerate(STEPS): if i < done_steps: icon_html = f'
' bg = "linear-gradient(135deg,rgba(16,185,129,0.15),rgba(16,185,129,0.05))" border = "rgba(16,185,129,0.25)" label_color = "#34d399" elif i == done_steps: icon_html = f'
{step["icon"]}
' bg = "rgba(245,158,11,0.08)" border = "#f59e0b" label_color = "#f59e0b" else: icon_html = f'
{step["icon"]}
' bg = "rgba(255,255,255,0.03)" border = "rgba(255,255,255,0.06)" label_color = "#475569" step_placeholders[i].markdown( f'
' f'{icon_html}' f'
{step["label"]}
' f'
{step["desc"]}
' f'
', unsafe_allow_html=True ) def render_log(entries): if not entries: log_ph.markdown( '
' 'Tool call log bekleniyor...' '
', unsafe_allow_html=True ) return html = '
' html += '
TOOL CALL LOG
' for e in entries: preview = json.dumps(e["result"])[:200] + ("..." if len(json.dumps(e["result"])) > 200 else "") html += f'''
TOOL CALL → {e["name"]}
✓ Tamamlandı
{preview}
''' html += '
' log_ph.markdown(html, unsafe_allow_html=True) render_step(0, 0) render_log([]) try: done_count = 0 _key = st.session_state.get("active_key", "") _provider = st.session_state.get("provider", "Claude") _fn = run_analysis if _provider == "Claude" else run_analysis_gemini for event in _fn(st.session_state.cv_text, st.session_state.job_desc, _key): if event["type"] == "status": status_ph.markdown( f'
' f'▸ {event["msg"]}
', unsafe_allow_html=True ) progress_ph.progress(done_count / 3) elif event["type"] == "step": render_step(event["step_idx"], done_count) elif event["type"] == "tool_done": log_entries.append({"name": event["name"], "result": event["result"]}) done_count += 1 partial_results = event["results"] render_step(done_count, done_count) render_log(log_entries) progress_ph.progress(done_count / 3) elif event["type"] == "done": st.session_state.results = event["results"] st.session_state.tool_log = event["log"] progress_ph.progress(1.0) status_ph.markdown( '
' '✓ Analiz tamamlandı!
', unsafe_allow_html=True ) import time; time.sleep(0.5) st.session_state.stage = "results" st.rerun() except Exception as e: st.error(f"Hata: {e}") if st.button("← Geri Dön"): st.session_state.stage = "input" st.rerun() # ════════════════════════════════════════════════════════════════════════════════ # STAGE: RESULTS # ════════════════════════════════════════════════════════════════════════════════ elif st.session_state.stage == "results": results = st.session_state.results cv = results.get("cv") or {} match = results.get("match") or {} letter = results.get("letter") or {} tab_overview, tab_match, tab_letter, tab_log = st.tabs( ["📊 Genel Bakış", "🎯 İş Eşleşmesi", "✉️ Cover Letter", "🔧 Tool Logs"] ) # ── Overview Tab ─────────────────────────────────────────────────────────── with tab_overview: # Score cards m1, m2, m3 = st.columns(3) with m1: st.markdown(score_circle(int(cv.get("overall_score", 0)), "CV Puanı", "#f59e0b"), unsafe_allow_html=True) with m2: st.markdown(score_circle(int(match.get("match_score", 0)), "İş Uyumu", "#10b981"), unsafe_allow_html=True) with m3: sections = cv.get("sections_found", []) badges_html = " ".join(blue_badge(s) for s in sections) st.markdown( f'
' f'
Bulunan Bölümler
' f'{badges_html}' f'
', unsafe_allow_html=True ) st.markdown("
", unsafe_allow_html=True) # Strengths & Gaps c_str, c_gap = st.columns(2) with c_str: items = "".join( f'
{s}
' for s in cv.get("key_strengths", []) ) st.markdown( f'
' f'
✦ Güçlü Yanlar
' f'{items}
', unsafe_allow_html=True ) with c_gap: items = "".join( f'
{g}
' for g in cv.get("critical_gaps", []) ) st.markdown( f'
' f'
⚠ Kritik Eksikler
' f'{items}
', unsafe_allow_html=True ) # Missing sections missing = cv.get("missing_sections", []) if missing: st.markdown("
", unsafe_allow_html=True) badges_html = " ".join(amber_badge(s) for s in missing) st.markdown( f'
' f'
EKSİK BÖLÜMLER
' f'{badges_html}
', unsafe_allow_html=True ) # Recommendations st.markdown("
", unsafe_allow_html=True) recs_html = "".join( f'
' f'{i+1}' f'{r}
' for i, r in enumerate(cv.get("recommendations", [])) ) st.markdown( f'
' f'
▣ Öneriler
' f'{recs_html}
', unsafe_allow_html=True ) # ── Match Tab ────────────────────────────────────────────────────────────── with tab_match: score = int(match.get("match_score", 0)) st.markdown( f'
' f'
{score}% Uyum
' f'
{match.get("experience_match","")}
' f'
', unsafe_allow_html=True ) st.progress(score / 100) c_ok, c_miss = st.columns(2) with c_ok: badges = " ".join(green_badge(s) for s in match.get("matched_skills", [])) st.markdown( f'
' f'
✓ EŞLEŞEn BECERİLER
' f'{badges}
', unsafe_allow_html=True ) with c_miss: badges = " ".join(red_badge(s) for s in match.get("missing_skills", [])) st.markdown( f'
' f'
✗ EKSİK BECERİLER
' f'{badges}
', unsafe_allow_html=True ) st.markdown("
", unsafe_allow_html=True) hp_items = "".join( f'
{h}
' for h in match.get("highlight_points", []) ) st.markdown( f'
' f'
◎ ÖNE ÇIKARILACAK NOKTALAR
' f'{hp_items}
', unsafe_allow_html=True ) tip_items = "".join( f'
{t}
' for t in match.get("improvement_tips", []) ) st.markdown( f'
' f'
⟳ İYİLEŞTİRME ÖNERİLERİ
' f'{tip_items}
', unsafe_allow_html=True ) # ── Cover Letter Tab ─────────────────────────────────────────────────────── with tab_letter: ksp = letter.get("key_selling_points", []) if ksp: badges = " ".join(amber_badge(p) for p in ksp) st.markdown( f'
' f'
✦ ÖNEMLİ SATIŞ NOKTALARI
' f'{badges}
', unsafe_allow_html=True ) if letter.get("subject_line"): st.markdown( f'
' f'KONU:' f'{letter["subject_line"]}' f'
', unsafe_allow_html=True ) cover = letter.get("cover_letter", "") st.markdown( f'
' f'
{cover}
' f'
', unsafe_allow_html=True ) if st.button("⧉ Kopyala", key="copy_letter"): st.code(cover, language=None) # ── Tool Log Tab ─────────────────────────────────────────────────────────── with tab_log: for entry in st.session_state.tool_log: with st.expander(f"🔧 TOOL CALL → {entry['name']}"): st.json(entry["result"]) # New analysis button st.markdown("

", unsafe_allow_html=True) _, center, _ = st.columns([2, 1, 2]) with center: if st.button("← Yeni Analiz", use_container_width=True): st.session_state.stage = "input" st.session_state.results = {} st.session_state.tool_log = [] st.rerun()