Spaces:
Paused
Paused
| """ | |
| Strategy Planner Agent — 投递策略规划模块 | |
| 生成:今日优先投递 Top3、稳妥/平衡/冲刺组合、7 天投递计划、 | |
| 7 天面试准备计划、需要先改简历再投的岗位列表。 | |
| """ | |
| from __future__ import annotations | |
| from typing import Optional | |
| # --------------------------------------------------------------------------- | |
| # 工具函数 | |
| # --------------------------------------------------------------------------- | |
| def _int(v, default: int = 0) -> int: | |
| """安全转换为 int。""" | |
| try: | |
| return int(v) | |
| except (TypeError, ValueError): | |
| return default | |
| def _pick_top(ranked_jobs: list[dict], n: int = 3) -> list[dict]: | |
| """取 ApplyPriority 最高(或 score/apply_priority 字段)的前 n 个。""" | |
| key = _priority_key(ranked_jobs) | |
| sorted_jobs = sorted(ranked_jobs, key=lambda x: x.get(key, 0), reverse=True) | |
| return sorted_jobs[:n] | |
| def _priority_key(ranked_jobs: list[dict]) -> str: | |
| """自动探测优先分数字段名。""" | |
| if not ranked_jobs: | |
| return "score" | |
| sample = ranked_jobs[0] | |
| if "apply_priority" in sample: | |
| return "apply_priority" | |
| if "score" in sample: | |
| return "score" | |
| return "score" | |
| # --------------------------------------------------------------------------- | |
| # 1. 今日优先投递 Top3 | |
| # --------------------------------------------------------------------------- | |
| def gen_priority_top3(ranked_jobs: list[dict], profile: Optional[dict] = None) -> list[dict]: | |
| """ | |
| 返回每个 Top3 岗位 + 推荐动作说明: | |
| - apply_action: 立即投递 / 先优化再投 / 冲刺岗位 / 暂缓 | |
| 策略一致性保证: | |
| - 如果 Top1 是"立即投递",则 Top2/3 不允许是"暂缓" | |
| - 如果 Top1 是"暂缓",则 Top2/3 可以是"先优化再投"或"冲刺岗位" | |
| """ | |
| top3 = _pick_top(ranked_jobs, 3) | |
| result = [] | |
| # 第一遍:计算所有 action | |
| actions = [] | |
| for job in top3: | |
| action = _infer_action(job, profile) | |
| actions.append((job, action)) | |
| # 第二遍:策略一致性调整。Top3 是一个投递组合,不能同时出现 | |
| # "立即投递"和"暂缓"这种互相打架的建议。 | |
| if any(action == "立即投递" for _, action in actions) and any(action == "暂缓" for _, action in actions): | |
| actions = [ | |
| (job, "先优化再投" if action == "暂缓" else action) | |
| for job, action in actions | |
| ] | |
| # 如果 Top1 本身都需要暂缓,说明整体匹配风险较高,后续岗位不应更激进。 | |
| if actions and actions[0][1] == "暂缓": | |
| actions = [ | |
| (job, "先优化再投" if action in {"立即投递", "冲刺岗位"} else action) | |
| for job, action in actions | |
| ] | |
| # 生成结果 | |
| for job, action in actions: | |
| result.append({ | |
| "rank": len(result) + 1, | |
| "title": job.get("title", ""), | |
| "company": job.get("company", ""), | |
| "apply_priority": job.get("apply_priority", job.get("score", 0)), | |
| "pass_score": job.get("pass_score", 0), | |
| "risk_score": job.get("risk_score", 0), | |
| "apply_action": action, | |
| "reason": _action_reason(job, action), | |
| }) | |
| return result | |
| def _infer_action(job: dict, profile: Optional[dict] = None) -> str: | |
| """ | |
| 基于 pass/risk/growth/missing_skills 推断投递动作。 | |
| 通用规则,不针对特定 case 硬编码。 | |
| """ | |
| pass_s = _int(job.get("pass_score"), 50) | |
| risk_s = _int(job.get("risk_score"), 50) | |
| growth_s = _int(job.get("growth_score"), 50) | |
| missing = job.get("missing_skills", []) | |
| missing_count = len(missing) if isinstance(missing, list) else 0 | |
| if profile is None: | |
| profile = {} | |
| target_city = profile.get("_city", "") | |
| target_stage = profile.get("_stage", "") | |
| has_llm_project = bool(profile.get("has_llm_project", False)) | |
| has_metrics = bool(profile.get("has_metrics", False)) | |
| has_rec_project = bool(profile.get("has_rec_project", False)) | |
| target_role = profile.get("_target_role", "") | |
| job_direction = job.get("direction") or "" | |
| job_stage = job.get("stage") or "" | |
| # 方向是否一致 | |
| if target_role and job_direction: | |
| direction_match = (target_role in job_direction or job_direction in target_role) | |
| else: | |
| direction_match = False | |
| # 城市/阶段错位判断 | |
| city_mismatch = bool( | |
| target_city and target_city != "不限" and job.get("city") != target_city | |
| ) | |
| stage_mismatch = bool( | |
| target_stage and job_stage and job_stage != "不限" and job_stage != target_stage | |
| ) | |
| # 已毕业判断 | |
| is_graduated = any(kw in str(target_stage) for kw in ["已毕业", "校招", "2025"]) | |
| # 已毕业 + 投实习 = 阶段错位 | |
| if is_graduated and job_stage == "实习": | |
| stage_mismatch = True | |
| has_project_signal = has_llm_project or has_rec_project | |
| low_risk = risk_s <= 15 | |
| medium_risk = risk_s <= 35 | |
| strong_fit = pass_s >= 70 and low_risk and missing_count <= 1 | |
| # ========== 1. 硬伤过滤:城市/阶段错位 ========== | |
| if city_mismatch or stage_mismatch: | |
| if strong_fit: | |
| return "冲刺岗位" | |
| if is_graduated and has_project_signal and has_metrics and pass_s >= 55 and risk_s <= 30 and missing_count <= 3: | |
| return "冲刺岗位" | |
| if pass_s >= 50 and medium_risk and missing_count <= 3: | |
| return "先优化再投" | |
| return "暂缓" | |
| # ========== 2. 方向不一致:先判断是否还有可迁移价值 ========== | |
| if target_role and not direction_match: | |
| if pass_s >= 50 and risk_s <= 25 and missing_count <= 2: | |
| return "先优化再投" | |
| if pass_s >= 35 and risk_s <= 45 and ("算法" in job_direction or "NLP" in job_direction or "计算机视觉" in job_direction): | |
| return "先优化再投" | |
| return "暂缓" | |
| # ========== 3. 立即投递:要求低风险且证据足 ========== | |
| if direction_match: | |
| if missing_count == 0 and pass_s >= 55 and risk_s <= 25: | |
| return "立即投递" | |
| if strong_fit: | |
| return "立即投递" | |
| # NLP/相关方向迁移到大模型岗位,若风险极低且有量化证据,可直接投。 | |
| if has_metrics and risk_s <= 10 and pass_s >= 55 and missing_count <= 3: | |
| return "立即投递" | |
| # ========== 4. 有项目但证据不完整:优先先优化再投 ========== | |
| if not has_metrics and missing_count > 0: | |
| return "先优化再投" | |
| if missing_count >= 3 and pass_s < 70: | |
| return "先优化再投" | |
| # ========== 5. 冲刺岗位:成长高但仍有明显缺口 ========== | |
| if direction_match and has_project_signal and has_metrics and growth_s >= 70 and pass_s >= 55 and risk_s <= 35: | |
| return "冲刺岗位" | |
| if direction_match and has_llm_project and growth_s >= 75 and pass_s >= 45 and risk_s <= 45: | |
| return "冲刺岗位" | |
| # ========== 6. 兜底 ========== | |
| if pass_s < 30 or risk_s > 70: | |
| return "暂缓" | |
| if pass_s >= 40: | |
| return "先优化再投" | |
| return "暂缓" | |
| def _action_reason(job: dict, action: str) -> str: | |
| if action == "立即投递": | |
| return f"PassScore={job.get('pass_score', '?')},初筛通过潜力高,建议今日投递。" | |
| if action == "先优化再投": | |
| return f"RiskScore={job.get('risk_score', '?')},建议先补齐 {', '.join(job.get('missing_skills', [])[:3])} 再投。" | |
| if action == "冲刺岗位": | |
| return f"GrowthScore={job.get('growth_score', '?')},虽有一定风险,但成长价值高,可冲刺。" | |
| return "多项评分偏低,建议优先投递其他岗位后再考虑。" | |
| # --------------------------------------------------------------------------- | |
| # 2. 稳妥 / 平衡 / 冲刺 岗位组合 | |
| # --------------------------------------------------------------------------- | |
| def gen_job_combo(ranked_jobs: list[dict]) -> dict: | |
| """ | |
| 按 ApplyPriority 将岗位分为三类(各取最高分 2-3 个): | |
| - stable:PassScore ≥ 70 且 RiskScore ≤ 30 | |
| - balanced:PassScore ≥ 55 且 RiskScore ≤ 55 | |
| - challenge:GrowthScore ≥ 65(允许 PassScore 较低) | |
| """ | |
| stable, balanced, challenge = [], [], [] | |
| for job in ranked_jobs: | |
| pass_s = job.get("pass_score", 0) | |
| risk_s = job.get("risk_score", 100) | |
| growth_s = job.get("growth_score", 0) | |
| if pass_s >= 70 and risk_s <= 30 and len(stable) < 3: | |
| stable.append(_summarize(job)) | |
| elif pass_s >= 55 and risk_s <= 55 and len(balanced) < 3: | |
| balanced.append(_summarize(job)) | |
| elif growth_s >= 65 and len(challenge) < 3: | |
| challenge.append(_summarize(job)) | |
| # 保底:如果某类为空,从剩余岗位里补 | |
| all_summary = [_summarize(j) for j in ranked_jobs] | |
| if not stable: | |
| stable = all_summary[:2] | |
| if not balanced: | |
| balanced = all_summary[2:4] if len(all_summary) >= 4 else all_summary[:2] | |
| if not challenge: | |
| challenge = all_summary[:2] | |
| return { | |
| "稳妥岗(高通过率)": stable, | |
| "平衡岗(综合推荐)": balanced, | |
| "冲刺岗(高成长价值)": challenge, | |
| } | |
| def _summarize(job: dict) -> dict: | |
| return { | |
| "title": job.get("title", ""), | |
| "company": job.get("company", ""), | |
| "match_score": job.get("match_score", job.get("score", 0)), | |
| "pass_score": job.get("pass_score", 0), | |
| "risk_score": job.get("risk_score", 0), | |
| "growth_score": job.get("growth_score", 0), | |
| "apply_priority": job.get("apply_priority", 0), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # 3. 7 天投递计划 | |
| # --------------------------------------------------------------------------- | |
| def gen_7day_apply_plan(ranked_jobs: list[dict], profile: Optional[dict] = None) -> list[str]: | |
| """ | |
| 生成 7 天投递节奏计划,每天 1-2 条可操作步骤。 | |
| """ | |
| top5 = _pick_top(ranked_jobs, 5) | |
| names = [f"{j.get('title', '岗位')}({j.get('company', '')})" for j in top5] | |
| plan = [ | |
| f"第 1 天:锁定今日优先投递 Top{len(top5)},完善简历中 {', '.join(top5[0].get('matched_skills', ['相关技能'])[:3])} 的项目证据。", | |
| f"第 2 天:投递第 1 个岗位 {names[0] if names else '目标岗位'},并针对第 2 个岗位优化简历关键词。", | |
| f"第 3 天:投递第 2 个岗位,同步准备 {top5[0].get('interview_themes', ['技术问题'])[0] if top5 else '技术问题'} 相关面试题。", | |
| f"第 4 天:根据前两天投递反馈(若有),调整第 3 个岗位简历版本,补充量化指标。", | |
| f"第 5 天:投递第 3 个岗位,开始准备行为面试(自我介绍、项目深挖)。", | |
| f"第 6 天:投递第 4-5 个岗位(或冲刺岗),整理所有投递记录到表格。", | |
| f"第 7 天:复盘本周投递效果,更新简历版本,规划下周投递策略。", | |
| ] | |
| return plan | |
| # --------------------------------------------------------------------------- | |
| # 4. 7 天面试准备计划 | |
| # --------------------------------------------------------------------------- | |
| def gen_7day_interview_plan(ranked_jobs: list[dict], profile: Optional[dict] = None) -> list[str]: | |
| """ | |
| 基于 Top1 岗位的 interview_themes 生成 7 天面试准备计划。 | |
| """ | |
| if not ranked_jobs: | |
| return [f"第 {i} 天:请先运行匹配获取岗位信息。" for i in range(1, 8)] | |
| top = ranked_jobs[0] | |
| themes = top.get("interview_themes", ["算法基础", "项目深挖", "系统设计", "业务理解"]) | |
| plan = [ | |
| f"第 1 天:复盘 {top.get('direction', '目标方向')} 岗位 JD,整理关键词和项目证据清单。", | |
| f"第 2 天:准备 {themes[0] if len(themes) > 0 else '算法基础'},讲清输入、模型、输出和可量化指标。", | |
| f"第 3 天:准备 {themes[1] if len(themes) > 1 else '项目深挖'},覆盖数据、模型、评估、落地四个维度。", | |
| f"第 4 天:准备 {themes[2] if len(themes) > 2 else '系统设计'},重点讲失败 case 和优化方案。", | |
| f"第 5 天:模拟 {top.get('company', '目标公司')} 算法题 3 道,限时 60 分钟。", | |
| "第 6 天:做一次 20 分钟项目深挖 mock,覆盖数据、模型、评估、落地。", | |
| "第 7 天:压缩成 1 分钟自我介绍、3 分钟项目介绍和 5 个高频追问答案。", | |
| ] | |
| return plan | |
| # --------------------------------------------------------------------------- | |
| # 5. 哪些岗位需要先改简历再投 | |
| # --------------------------------------------------------------------------- | |
| def gen_resume_advice(ranked_jobs: list[dict]) -> list[dict]: | |
| """ | |
| 返回需要先改简历再投的岗位列表,附带具体改写建议。 | |
| """ | |
| advice = [] | |
| for job in ranked_jobs: | |
| pass_s = job.get("pass_score", 50) | |
| risk_s = job.get("risk_score", 50) | |
| missing = job.get("missing_skills", []) | |
| if pass_s < 65 or risk_s >= 50 or missing: | |
| advice.append({ | |
| "title": job.get("title", ""), | |
| "company": job.get("company", ""), | |
| "pass_score": pass_s, | |
| "risk_score": risk_s, | |
| "missing_skills": missing[:4], | |
| "advice": _resume_fix_tips(job), | |
| "urgency": "高" if pass_s < 50 else ("中" if pass_s < 65 else "低"), | |
| }) | |
| # 按紧急程度排序:高 > 中 > 低 | |
| urgency_order = {"高": 0, "中": 1, "低": 2} | |
| advice.sort(key=lambda x: urgency_order.get(x["urgency"], 9)) | |
| return advice | |
| def _resume_fix_tips(job: dict) -> list[str]: | |
| tips = [] | |
| missing = job.get("missing_skills", []) | |
| if missing: | |
| tips.append(f"补充 {missing[0]} 的真实项目经历或课程项目,不要只写「了解」。") | |
| if job.get("risk_score", 0) >= 50: | |
| tips.append("简历中项目描述缺少量化指标,建议补充 NDCG/HitRate/TopK 等具体数字。") | |
| if job.get("keyword_coverage", 1.0) < 0.4: | |
| tips.append(f"JD 关键词覆盖率偏低,建议在项目描述中自然嵌入:{', '.join(job.get('skills', [])[:4])}。") | |
| if not tips: | |
| tips.append("简历与岗位匹配度尚可,建议再压缩表达、突出亮点。") | |
| return tips | |
| # --------------------------------------------------------------------------- | |
| # 统一入口:生成完整策略包 | |
| # --------------------------------------------------------------------------- | |
| def gen_strategy_package(ranked_jobs: list[dict], profile: Optional[dict] = None) -> dict: | |
| """一次性返回所有策略内容,供 report_generator 和 app.py 使用。""" | |
| return { | |
| "priority_top3": gen_priority_top3(ranked_jobs, profile), | |
| "job_combo": gen_job_combo(ranked_jobs), | |
| "apply_plan_7day": gen_7day_apply_plan(ranked_jobs, profile), | |
| "interview_plan_7day": gen_7day_interview_plan(ranked_jobs, profile), | |
| "resume_advice": gen_resume_advice(ranked_jobs), | |
| } | |