offer-catcher-agent / src /report_generator.py
hungryb's picture
Deploy Offer Catcher Agent demo
3161850 verified
Raw
History Blame Contribute Delete
13.1 kB
"""
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}")