添加岗位简历诊断
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"""技能差距分析 AI 引擎
|
||||
|
||||
差距分析 + 定制简历优化 + Agent 规划/执行。
|
||||
依赖:LLM 枚举、skill_gap_analyzer/prompts
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from json_repair import repair_json
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
|
||||
from app.ai.models import LLM
|
||||
from app.ai.skill_gap_analyzer.prompts import (
|
||||
SKILL_GAP_PROMPT, SUMMARY_OPTIMIZE_PROMPT, EXPERIENCE_OPTIMIZE_PROMPT,
|
||||
AGENT_PLAN_PROMPT, AGENT_MODULE_EDIT_PROMPT, MODULE_SCHEMAS,
|
||||
)
|
||||
from app.core.logger import log
|
||||
|
||||
|
||||
def _parse_json(text: str):
|
||||
"""解析 AI 输出的 JSON,自动去除 markdown 代码块包裹,容错处理"""
|
||||
cleaned = re.sub(r"^```(?:json)?\s*\n?", "", text.strip())
|
||||
cleaned = re.sub(r"\n?```\s*$", "", cleaned)
|
||||
return repair_json(cleaned, return_objects=True)
|
||||
|
||||
|
||||
# ===== 差距分析 =====
|
||||
|
||||
_skill_gap_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", SKILL_GAP_PROMPT), ("human", "请开始分析。")])
|
||||
| LLM.DOUBAO_PRO_32K.create(temperature=0)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def analyze_skill_gap(skill_tags: list[str], resume_json: str) -> list[str]:
|
||||
"""分析技能差距,返回缺失技能列表"""
|
||||
try:
|
||||
raw = await _skill_gap_chain.ainvoke({"skill_tags": str(skill_tags), "resume_json": resume_json})
|
||||
result = _parse_json(raw)
|
||||
if isinstance(result, list):
|
||||
return [s for s in result if isinstance(s, str) and s in skill_tags]
|
||||
return skill_tags # 解析异常降级:全部标记缺失
|
||||
except Exception as e:
|
||||
log.warning(f"AI技能差距分析失败: {e}")
|
||||
return skill_tags # 降级:全部标记缺失
|
||||
|
||||
|
||||
# ===== 定制简历 - summary 优化 =====
|
||||
|
||||
_summary_optimize_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", SUMMARY_OPTIMIZE_PROMPT), ("human", "请开始优化。")])
|
||||
| LLM.CLAUDE_SONNET_4.create(temperature=0.3)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def optimize_summary(job_title: str, add_skills: list[str], original_summary: str) -> str:
|
||||
"""优化个人概述,融入技能关键词"""
|
||||
try:
|
||||
return await _summary_optimize_chain.ainvoke({
|
||||
"job_title": job_title, "add_skills": "、".join(add_skills) if add_skills else "无",
|
||||
"original_summary": original_summary or "暂无",
|
||||
})
|
||||
except Exception as e:
|
||||
log.warning(f"AI优化summary失败: {e}")
|
||||
return original_summary
|
||||
|
||||
|
||||
# ===== 定制简历 - experience 优化 =====
|
||||
|
||||
_experience_optimize_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", EXPERIENCE_OPTIMIZE_PROMPT), ("human", "请开始优化。")])
|
||||
| LLM.CLAUDE_SONNET_4.create(temperature=0.3)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def optimize_module(job_title: str, job_description: str, module_data: str) -> list | dict | None:
|
||||
"""优化子表模块经历描述,返回修改后的完整模块数据"""
|
||||
try:
|
||||
raw = await _experience_optimize_chain.ainvoke({
|
||||
"job_title": job_title, "job_description": job_description or "",
|
||||
"original_module_data": module_data,
|
||||
})
|
||||
return _parse_json(raw)
|
||||
except Exception as e:
|
||||
log.warning(f"AI优化经历模块失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ===== Agent - 规划 =====
|
||||
|
||||
_plan_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", AGENT_PLAN_PROMPT), ("human", "请分析用户指令。")])
|
||||
| LLM.DOUBAO_PRO_32K.create(temperature=0)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def plan_edit(job_title: str, resume_json: str, chat_history: str, instruction: str) -> dict | None:
|
||||
"""Agent 规划:分析用户指令,返回修改计划或对话回复"""
|
||||
try:
|
||||
raw = await _plan_chain.ainvoke({
|
||||
"job_title": job_title, "resume_json": resume_json,
|
||||
"chat_history": chat_history, "instruction": instruction,
|
||||
})
|
||||
result = _parse_json(raw)
|
||||
return result if isinstance(result, dict) else None
|
||||
except Exception as e:
|
||||
log.warning(f"AI规划失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ===== Agent - 模块修改 =====
|
||||
|
||||
_module_edit_chain = (
|
||||
ChatPromptTemplate.from_messages([("system", AGENT_MODULE_EDIT_PROMPT), ("human", "请执行修改。")])
|
||||
| LLM.CLAUDE_SONNET_4.create(temperature=0.3)
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
|
||||
async def execute_module_edit(job_title: str, module_instruction: str,
|
||||
module_schema: str, module_data: str) -> dict | list | None:
|
||||
"""Agent 模块修改:根据指令修改指定模块数据"""
|
||||
try:
|
||||
raw = await _module_edit_chain.ainvoke({
|
||||
"job_title": job_title, "module_instruction": module_instruction,
|
||||
"module_schema": module_schema, "module_data": module_data,
|
||||
})
|
||||
return _parse_json(raw)
|
||||
except Exception as e:
|
||||
log.warning(f"AI模块修改失败: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,115 @@
|
||||
"""技能差距分析 + 定制简历 Prompt 模板
|
||||
|
||||
注意:prompt 中的 JSON 示例花括号必须用 {{ }} 转义,避免被 ChatPromptTemplate 当作变量。
|
||||
"""
|
||||
|
||||
SKILL_GAP_PROMPT = """你是一个技能匹配助手。给定岗位要求的技能标签列表和用户简历信息,判断用户简历中未覆盖的技能。
|
||||
|
||||
【岗位技能标签】
|
||||
{skill_tags}
|
||||
|
||||
【用户简历】
|
||||
{resume_json}
|
||||
|
||||
规则:
|
||||
1. 逐个判断岗位技能标签,用户简历中是否体现了该技能(包括直接提及、经历中隐含的技能)
|
||||
2. 只输出用户简历未覆盖的技能,必须是岗位技能标签的子集,原文输出不要修改
|
||||
3. 返回 JSON 数组格式,如:["Python", "SQL"]
|
||||
4. 如果全部覆盖,返回空数组 []
|
||||
5. 只返回 JSON 数组,不要其他内容"""
|
||||
|
||||
SUMMARY_OPTIMIZE_PROMPT = """你是一个简历优化助手。根据目标岗位信息,微调用户的个人概述。
|
||||
|
||||
【目标岗位】
|
||||
{job_title}
|
||||
|
||||
【需要融入的技能关键词】
|
||||
{add_skills}
|
||||
|
||||
【原始个人概述】
|
||||
{original_summary}
|
||||
|
||||
规则:
|
||||
1. 保持原文风格和主体内容不变
|
||||
2. 只做轻微润色,让概述更贴合目标岗位方向
|
||||
3. 自然融入需要新增的技能关键词,不要生硬堆砌
|
||||
4. 避免过度优化,改动越少越好
|
||||
5. 直接输出优化后的文本,不要其他内容"""
|
||||
|
||||
EXPERIENCE_OPTIMIZE_PROMPT = """你是一个简历优化助手。根据目标岗位信息,微调用户的经历描述。
|
||||
|
||||
【目标岗位】
|
||||
{job_title}
|
||||
{job_description}
|
||||
|
||||
【原始经历数据】
|
||||
{original_module_data}
|
||||
|
||||
规则:
|
||||
1. 基本保持原文不变,只在可以优化的地方做轻微调整
|
||||
2. 让描述更贴合目标岗位方向,但不要编造内容
|
||||
3. 避免过度优化,改动越少越好
|
||||
4. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接移除
|
||||
5. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)"""
|
||||
|
||||
AGENT_PLAN_PROMPT = """你是一个简历编辑助手。分析用户的指令,决定需要修改简历的哪些模块。
|
||||
|
||||
【目标岗位】
|
||||
{job_title}
|
||||
|
||||
【当前简历】
|
||||
{resume_json}
|
||||
|
||||
【对话历史】
|
||||
{chat_history}
|
||||
|
||||
【用户指令】
|
||||
{instruction}
|
||||
|
||||
如果用户指令明确,返回修改计划 JSON:
|
||||
{{"action": "modify", "modules": [{{"module": "模块名", "instruction": "具体修改要求"}}], "updatedModulesLabel": "中文模块名列表"}}
|
||||
|
||||
如果用户指令不明确或需要澄清,返回对话 JSON:
|
||||
{{"action": "chat", "message": "你的追问内容"}}
|
||||
|
||||
模块名可选:
|
||||
- resume:主表(个人信息,包含 name、email、mobileNumber、city、wechatNumber、portfolioUrl、skills、certificates、summary、avatarUrl)
|
||||
- education:教育经历
|
||||
- work:工作经历
|
||||
- internship:实习经历
|
||||
- project:项目经历
|
||||
- competition:竞赛经历
|
||||
只返回 JSON,不要其他内容。"""
|
||||
|
||||
AGENT_MODULE_EDIT_PROMPT = """你是一个简历编辑助手。根据修改要求,修改简历的指定模块。
|
||||
|
||||
【目标岗位】
|
||||
{job_title}
|
||||
|
||||
【修改要求】
|
||||
{module_instruction}
|
||||
|
||||
【模块数据结构】
|
||||
{module_schema}
|
||||
|
||||
【当前模块数据】
|
||||
{module_data}
|
||||
|
||||
规则:
|
||||
1. 严格按照修改要求操作,可以增删改
|
||||
2. 未要求修改的记录保持不变
|
||||
3. 不要编造用户简历中不存在的内容
|
||||
4. 保持原文格式和结构
|
||||
5. description 字段是 [{{"id": "xxx", "text": "xxx"}}] 格式:修改时保留原 id 只改 text,新增段落生成随机8位字符串作为 id,删除段落直接从数组中移除
|
||||
6. 新增记录时按照模块数据结构生成完整字段,id 使用随机8位字符串
|
||||
7. 返回修改后的完整模块数据(JSON 格式,与输入格式一致)"""
|
||||
|
||||
# 各模块数据结构定义(传入 prompt 的 module_schema)
|
||||
MODULE_SCHEMAS: dict[str, str] = {
|
||||
"resume": '{ "avatarUrl": "string", "name": "string", "email": "string", "mobileNumber": "string", "city": "string", "wechatNumber": "string", "portfolioUrl": "string", "skills": ["string"], "certificates": ["string"], "summary": "string" }',
|
||||
"education": '[{ "id": "string(8位)", "school": "string", "major": "string", "degree": "大专/本科/硕士/博士", "studyType": "全日制/非全日制", "startDate": "2023.09", "endDate": "2024.06", "description": [{"id": "string(8位)", "text": "string"}] }]',
|
||||
"work": '[{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]',
|
||||
"internship": '[{ "id": "string(8位)", "companyName": "string", "position": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]',
|
||||
"project": '[{ "id": "string(8位)", "companyName": "string", "projectName": "string", "role": "string", "startDate": "2023.06", "endDate": "2023.09", "description": [{"id": "string(8位)", "text": "string"}] }]',
|
||||
"competition": '[{ "id": "string(8位)", "competitionName": "string", "award": "string", "awardDate": "2023.07", "description": [{"id": "string(8位)", "text": "string"}] }]',
|
||||
}
|
||||
Reference in New Issue
Block a user