Spaces:
Paused
Paused
| """ | |
| Report Generator — Markdown 求职策略报告导出模块 | |
| 一键生成包含岗位排序、简历优化建议、7 天投递策略、面试准备的完整报告。 | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from datetime import date | |
| from pathlib import Path | |
| from typing import Optional | |
| # --------------------------------------------------------------------------- | |
| # 报告生成主函数 | |
| # --------------------------------------------------------------------------- | |
| def generate_report( | |
| profile: dict, | |
| ranked_jobs: list[dict], | |
| strategy: dict, | |
| gaps: Optional[list[str]] = None, | |
| rewrites: Optional[list[dict]] = None, | |
| top_n: int = 5, | |
| ) -> str: | |
| """ | |
| 生成完整 Markdown 求职策略报告。 | |
| 参数: | |
| - profile: 学生画像(来自 Resume Parser Agent) | |
| - ranked_jobs: 已排序岗位列表(来自 Application Ranker Agent) | |
| - strategy: 策略包(来自 Strategy Planner Agent) | |
| - gaps: 能力缺口列表(可选,来自 Gap Diagnosis Agent) | |
| - rewrites: 简历改写建议(可选,来自 Resume Conversion Agent) | |
| - top_n: 报告中展示的岗位数量(默认 5) | |
| 返回: | |
| - Markdown 格式字符串 | |
| """ | |
| parts = [] | |
| # ---- 标题 + 基本信息 ---- | |
| parts.append(_build_header(profile)) | |
| parts.append("") | |
| # ---- 一、岗位投递优先级榜单 ---- | |
| parts.append("## 一、岗位投递优先级榜单") | |
| parts.append("") | |
| parts.append(_build_job_table(ranked_jobs[:top_n])) | |
| parts.append("") | |
| # ---- 二、今日优先投递 Top3 ---- | |
| parts.append("## 二、今日优先投递建议(Top 3)") | |
| parts.append("") | |
| parts.append(_build_priority_top3(strategy.get("priority_top3", []))) | |
| parts.append("") | |
| # ---- 三、稳妥 / 平衡 / 冲刺岗位组合 ---- | |
| parts.append("## 三、岗位组合建议") | |
| parts.append("") | |
| parts.append(_build_job_combo(strategy.get("job_combo", {}))) | |
| parts.append("") | |
| # ---- 四、能力缺口诊断 ---- | |
| parts.append("## 四、能力缺口诊断") | |
| parts.append("") | |
| parts.append(_build_gaps(ranked_jobs[:top_n], gaps)) | |
| parts.append("") | |
| # ---- 五、简历改写建议 ---- | |
| parts.append("## 五、简历改写建议") | |
| parts.append("") | |
| parts.append(_build_rewrites(ranked_jobs[:top_n], rewrites)) | |
| parts.append("") | |
| # ---- 六、7 天投递计划 ---- | |
| parts.append("## 六、7 天投递执行计划") | |
| parts.append("") | |
| parts.append(_build_apply_plan(strategy.get("apply_plan_7day", []))) | |
| parts.append("") | |
| # ---- 七、面试准备计划 ---- | |
| parts.append("## 七、7 天面试准备计划") | |
| parts.append("") | |
| parts.append(_build_interview_plan(strategy.get("interview_plan_7day", []))) | |
| parts.append("") | |
| # ---- 八、需要先改简历再投的岗位 ---- | |
| resume_advice = strategy.get("resume_advice", []) | |
| if resume_advice: | |
| parts.append("") | |
| parts.append("## 八、需优先优化简历的岗位") | |
| parts.append("") | |
| parts.append(_build_resume_advice(resume_advice)) | |
| parts.append("") | |
| # ---- 附录:学生画像摘要 ---- | |
| parts.append("") | |
| parts.append("## 附录:学生画像摘要") | |
| parts.append("") | |
| parts.append(_build_profile_summary(profile)) | |
| parts.append("") | |
| parts.append("---") | |
| parts.append(f"*报告生成时间:{date.today().strftime('%Y-%m-%d')} | 由 Offer 捕手 Agent 决策驾驶舱自动生成*") | |
| return "\n".join(parts) | |
| # --------------------------------------------------------------------------- | |
| # 各小节构建函数 | |
| # --------------------------------------------------------------------------- | |
| def _build_header(profile: dict) -> str: | |
| skills = "、".join(profile.get("skills", [])[:8]) or "待补充" | |
| return f"""# 📊 Offer 捕手 · 求职策略报告 | |
| **技能标签:** {skills} | |
| """ | |
| def _build_job_table(ranked_jobs: list[dict]) -> str: | |
| if not ranked_jobs: | |
| return "_暂无匹配岗位,请先运行匹配。_" | |
| lines = [] | |
| lines.append("| 排名 | 岗位 | 公司 | 城市 | Match | Pass | Risk | Growth | 优先级 | 推荐动作 |") | |
| lines.append("|------|------|------|------|-------|------|------|---------|--------|----------|") | |
| action_map = {} | |
| for job in ranked_jobs: | |
| pass_s = job.get("pass_score", 50) | |
| risk_s = job.get("risk_score", 50) | |
| if pass_s >= 70 and risk_s <= 30: | |
| action = "✅ 立即投递" | |
| elif pass_s >= 55: | |
| action = "✏️ 先优化再投" | |
| elif job.get("growth_score", 50) >= 65: | |
| action = "🚀 冲刺岗位" | |
| else: | |
| action = "⏳ 暂缓" | |
| action_map[id(job)] = action | |
| for i, job in enumerate(ranked_jobs, 1): | |
| title = job.get("title", "未知岗位") | |
| company = job.get("company", "") | |
| city = job.get("city", "") | |
| match = job.get("match_score", job.get("score", 0)) | |
| pass_s = job.get("pass_score", "-") | |
| risk_s = job.get("risk_score", "-") | |
| growth = job.get("growth_score", "-") | |
| priority = job.get("apply_priority", "-") | |
| action = action_map.get(id(job), "-") | |
| lines.append(f"| Top{i} | {title} | {company} | {city} | {match} | {pass_s} | {risk_s} | {growth} | {priority} | {action} |") | |
| return "\n".join(lines) | |
| def _build_priority_top3(top3: list[dict]) -> str: | |
| if not top3: | |
| return "_请先运行匹配获取岗位推荐。_" | |
| lines = [] | |
| for item in top3: | |
| lines.append(f"**Top {item['rank']}:{item['title']}({item['company']})**") | |
| lines.append(f"- 推荐动作:{item['apply_action']}") | |
| lines.append(f"- 理由:{item['reason']}") | |
| lines.append(f"- ApplyPriority = {item.get('apply_priority', '-')},PassScore = {item.get('pass_score', '-')},RiskScore = {item.get('risk_score', '-')}") | |
| lines.append("") | |
| return "\n".join(lines) | |
| def _build_job_combo(combo: dict) -> str: | |
| if not combo: | |
| return "_暂无组合建议。_" | |
| lines = [] | |
| emoji_map = {"稳妥岗(高通过率)": "🛡️", "平衡岗(综合推荐)": "⚖️", "冲刺岗(高成长价值)": "🚀"} | |
| for category, jobs in combo.items(): | |
| emoji = emoji_map.get(category, "📌") | |
| lines.append(f"**{emoji} {category}**") | |
| if not jobs: | |
| lines.append(" (暂无)") | |
| for j in jobs: | |
| lines.append(f" - {j.get('title', '')}({j.get('company', '')})| Match={j.get('match_score', '-')} Pass={j.get('pass_score', '-')} Risk={j.get('risk_score', '-')}") | |
| lines.append("") | |
| return "\n".join(lines) | |
| def _build_gaps(ranked_jobs: list[dict], gaps: Optional[list[str]]) -> str: | |
| lines = [] | |
| if gaps: | |
| for g in gaps[:6]: | |
| lines.append(f"- ⚠️ {g}") | |
| elif ranked_jobs: | |
| # 从第一个岗位的 gaps 字段取 | |
| first_gaps = ranked_jobs[0].get("gaps", []) | |
| if first_gaps: | |
| for g in first_gaps: | |
| lines.append(f"- ⚠️ {g}") | |
| else: | |
| lines.append("_岗位能力缺口已纳入评分,暂无高风险项。_") | |
| else: | |
| lines.append("_请先运行匹配。_") | |
| return "\n".join(lines) if lines else "_暂无数据。_" | |
| def _build_rewrites(ranked_jobs: list[dict], rewrites: Optional[list[dict]]) -> str: | |
| lines = [] | |
| # 优先用传入的 rewrites,否则从 Top1 岗位取 | |
| items = rewrites or (ranked_jobs[0].get("rewrites", []) if ranked_jobs else []) | |
| if not items: | |
| return "_暂无改写建议,简历与岗位匹配度较好。_" | |
| for item in items: | |
| before = item.get("before", "") | |
| after = item.get("after", "") | |
| lines.append(f"**原表达:**") | |
| lines.append(f"> {before}") | |
| lines.append(f"**建议改写:**") | |
| lines.append(f"> {after}") | |
| lines.append("") | |
| return "\n".join(lines) | |
| def _build_apply_plan(plan: list[str]) -> str: | |
| if not plan: | |
| return "_暂无投递计划。_" | |
| return "\n".join(f"- {p}" for p in plan) | |
| def _build_interview_plan(plan: list[str]) -> str: | |
| if not plan: | |
| return "_暂无面试准备计划。_" | |
| return "\n".join(f"- {p}" for p in plan) | |
| def _build_resume_advice(advice: list[dict]) -> str: | |
| lines = [] | |
| for item in advice[:5]: | |
| lines.append(f"**{item['title']}({item['company']})** — 紧急度:{item.get('urgency', '中')}") | |
| for tip in item.get("advice", []): | |
| lines.append(f" - {tip}") | |
| lines.append("") | |
| return "\n".join(lines) if lines else "_所有岗位简历匹配度较好,可直接投递。_" | |
| def _build_profile_summary(profile: dict) -> str: | |
| lines = [] | |
| skills = "、".join(profile.get("skills", [])) or "未检测到明显技能词" | |
| project_sigs = "、".join(profile.get("project_signals", profile.get("project_signals", []))) or "未检测到项目信号" | |
| lines.append(f"- **技能:** {skills}") | |
| lines.append(f"- **项目信号:** {project_sigs}") | |
| lines.append(f"- **量化指标:** {'有 ✅' if profile.get('has_metrics') else '缺少 ⚠️'}(建议在简历中补充 NDCG/HitRate 等具体数字)") | |
| lines.append(f"- **LLM 项目信号:** {'有 ✅' if profile.get('has_llm_project') else '无 ⚠️'}(大模型方向建议补充 RAG/Agent 项目描述)") | |
| lines.append(f"- **推荐项目信号:** {'有 ✅' if profile.get('has_rec_project') else '无 ⚠️'}(推荐方向建议补充召回/排序项目描述)") | |
| return "\n".join(lines) | |
| # --------------------------------------------------------------------------- | |
| # 保存报告到文件 | |
| # --------------------------------------------------------------------------- | |
| def save_report(md_text: str, output_path: str | Path) -> Path: | |
| """ | |
| 将 Markdown 报告保存到指定路径,返回实际保存的 Path 对象。 | |
| """ | |
| path = Path(output_path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(md_text, encoding="utf-8") | |
| return path | |
| # --------------------------------------------------------------------------- | |
| # CLI 入口(方便本地测试) | |
| # --------------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| # 构造一个假数据结构用于测试报告格式 | |
| dummy_profile = { | |
| "skills": ["Python", "PyTorch", "Transformer", "RAG", "Agent", "Embedding"], | |
| "project_signals": ["RAG", "Agent", "检索", "推荐", "Semantic ID"], | |
| "has_metrics": True, | |
| "has_llm_project": True, | |
| "has_rec_project": True, | |
| } | |
| dummy_jobs = [ | |
| { | |
| "title": "大模型应用算法实习生", | |
| "company": "腾讯云智能", | |
| "city": "深圳", | |
| "direction": "大模型应用算法", | |
| "score": 92, | |
| "match_score": 92, | |
| "pass_score": 78, | |
| "risk_score": 22, | |
| "growth_score": 85, | |
| "apply_priority": 88, | |
| "gaps": ["建议补充 Agent 工具调用失败兜底方案", "简历缺少多轮对话评估指标"], | |
| "rewrites": [ | |
| {"before": "做过 RAG Demo。", "after": "基于 bge embedding + FAISS 构建企业知识库问答,Top5 命中率提升 18%,并设计 Agent 工具调用链路。"} | |
| ], | |
| }, | |
| { | |
| "title": "LLM 推荐算法实习生", | |
| "company": "内容平台事业群", | |
| "city": "北京", | |
| "direction": "推荐算法", | |
| "score": 85, | |
| "match_score": 85, | |
| "pass_score": 65, | |
| "risk_score": 40, | |
| "growth_score": 90, | |
| "apply_priority": 76, | |
| "gaps": [], | |
| "rewrites": [], | |
| }, | |
| ] | |
| dummy_strategy = { | |
| "priority_top3": [ | |
| {"rank": 1, "title": "大模型应用算法实习生", "company": "腾讯云智能", "apply_action": "先优化再投", "reason": "PassScore=78,建议补齐 Agent 项目证据再投。", "apply_priority": 88, "pass_score": 78, "risk_score": 22}, | |
| ], | |
| "job_combo": { | |
| "稳妥岗(高通过率)": [{"title": "大模型应用算法实习生", "company": "腾讯云智能", "match_score": 92, "pass_score": 78, "risk_score": 22, "growth_score": 85}], | |
| "平衡岗(综合推荐)": [{"title": "LLM 推荐算法实习生", "company": "内容平台事业群", "match_score": 85, "pass_score": 65, "risk_score": 40, "growth_score": 90}], | |
| "冲刺岗(高成长价值)": [], | |
| }, | |
| "apply_plan_7day": [f"第 {i} 天:投递执行 + 简历优化。" for i in range(1, 8)], | |
| "interview_plan_7day": [f"第 {i} 天:面试准备。" for i in range(1, 8)], | |
| "resume_advice": [], | |
| } | |
| md = generate_report(dummy_profile, dummy_jobs, dummy_strategy) | |
| out = Path(__file__).resolve().parent.parent / "reports" | |
| saved = save_report(md, out / "求职策略报告_测试.md") | |
| print(f"测试报告已保存到:{saved}") | |