抽象简历查询

This commit is contained in:
zk
2026-04-24 19:04:57 +08:00
parent 7efa402e9c
commit f795c2fd5d
3 changed files with 112 additions and 108 deletions
+17 -40
View File
@@ -1,20 +1,14 @@
"""求职助手 Agent 对话 Service """求职助手 Agent 对话 Service
主要功能:查询简历数据,调用 AI 模块完成对话。 主要功能:查询简历数据,调用 AI 模块完成对话。
依赖:UserResume + 5张子表 ORM、job_agent.chat AI 模块 依赖:resume_loader(简历统一查询)、job_agent.chat AI 模块
使用表:bg_user_resume、bg_user_resume_education/work/internship/project/competition(查询简历 使用表:bg_user_resume + 5张子表(通过 resume_loader 查询
""" """
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.job_agent.chat import agent_chat from app.ai.job_agent.chat import agent_chat
from app.models.user_resume import UserResume from app.services.resume_loader import ResumeDetail, load_resume_detail
from app.models.user_resume_education import UserResumeEducation
from app.models.user_resume_work import UserResumeWork
from app.models.user_resume_internship import UserResumeInternship
from app.models.user_resume_project import UserResumeProject
from app.models.user_resume_competition import UserResumeCompetition
class JobAgentChatService: class JobAgentChatService:
@@ -26,31 +20,14 @@ class JobAgentChatService:
history: list[dict], job_categories: list[str], history: list[dict], job_categories: list[str],
regions: list[str], industries: list[str]) -> dict: regions: list[str], industries: list[str]) -> dict:
"""求职助手对话:查简历 → 序列化 → 调 AI 模块""" """求职助手对话:查简历 → 序列化 → 调 AI 模块"""
resume = await self._get_resume(resume_id, user_id) detail = await load_resume_detail(self.session, resume_id, user_id)
edu, work, intern, proj, comp = await self._load_sub_tables(resume_id) resume_text = self._build_resume_text(detail)
resume_text = self._build_resume_text(resume, edu, work, intern, proj, comp)
return await agent_chat(resume_text, message, history, job_categories, regions, industries) return await agent_chat(resume_text, message, history, job_categories, regions, industries)
async def _get_resume(self, resume_id: int, user_id: int) -> UserResume:
"""查指定简历"""
result = await self.session.execute(select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("简历不存在")
return resume
async def _load_sub_tables(self, resume_id: int):
"""查询简历5张子表"""
edu = (await self.session.execute(select(UserResumeEducation).where(UserResumeEducation.resume_id == resume_id))).scalars().all()
work = (await self.session.execute(select(UserResumeWork).where(UserResumeWork.resume_id == resume_id))).scalars().all()
intern = (await self.session.execute(select(UserResumeInternship).where(UserResumeInternship.resume_id == resume_id))).scalars().all()
proj = (await self.session.execute(select(UserResumeProject).where(UserResumeProject.resume_id == resume_id))).scalars().all()
comp = (await self.session.execute(select(UserResumeCompetition).where(UserResumeCompetition.resume_id == resume_id))).scalars().all()
return edu, work, intern, proj, comp
@staticmethod @staticmethod
def _build_resume_text(resume: UserResume, edu_list, work_list, intern_list, proj_list, comp_list) -> str: def _build_resume_text(detail: ResumeDetail) -> str:
"""将简历数据序列化为文本供 AI 使用""" """将简历数据序列化为文本供 AI 使用"""
resume = detail.resume
parts = [] parts = []
if resume.name: if resume.name:
parts.append(f"姓名:{resume.name}") parts.append(f"姓名:{resume.name}")
@@ -62,24 +39,24 @@ class JobAgentChatService:
parts.append(f"证书:{''.join(resume.certificates)}") parts.append(f"证书:{''.join(resume.certificates)}")
if resume.summary: if resume.summary:
parts.append(f"个人概述:{resume.summary}") parts.append(f"个人概述:{resume.summary}")
if edu_list: if detail.education:
parts.append("教育经历:") parts.append("教育经历:")
for r in edu_list: for r in detail.education:
parts.append(f" - {r.school or ''} {r.major or ''} {r.degree or ''}") parts.append(f" - {r.school or ''} {r.major or ''} {r.degree or ''}")
if work_list: if detail.work:
parts.append("工作经历:") parts.append("工作经历:")
for r in work_list: for r in detail.work:
parts.append(f" - {r.company_name or ''} {r.position or ''}") parts.append(f" - {r.company_name or ''} {r.position or ''}")
if intern_list: if detail.internship:
parts.append("实习经历:") parts.append("实习经历:")
for r in intern_list: for r in detail.internship:
parts.append(f" - {r.company_name or ''} {r.position or ''}") parts.append(f" - {r.company_name or ''} {r.position or ''}")
if proj_list: if detail.project:
parts.append("项目经历:") parts.append("项目经历:")
for r in proj_list: for r in detail.project:
parts.append(f" - {r.project_name or ''} {r.role or ''}") parts.append(f" - {r.project_name or ''} {r.role or ''}")
if comp_list: if detail.competition:
parts.append("竞赛经历:") parts.append("竞赛经历:")
for r in comp_list: for r in detail.competition:
parts.append(f" - {r.competition_name or ''} {r.award or ''}") parts.append(f" - {r.competition_name or ''} {r.award or ''}")
return "\n".join(parts) if parts else "暂无简历信息" return "\n".join(parts) if parts else "暂无简历信息"
+65
View File
@@ -0,0 +1,65 @@
"""简历统一查询模块
提供简历主表 + 5张子表的统一查询能力,返回脱离 session 的 ResumeDetail dataclass。
各 Service 统一复用,避免重复查询逻辑。
"""
from dataclasses import dataclass, field
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.user_resume import UserResume
from app.models.user_resume_education import UserResumeEducation
from app.models.user_resume_work import UserResumeWork
from app.models.user_resume_internship import UserResumeInternship
from app.models.user_resume_project import UserResumeProject
from app.models.user_resume_competition import UserResumeCompetition
@dataclass
class ResumeDetail:
"""简历完整数据,主表 + 5张子表"""
resume: UserResume
education: list[UserResumeEducation] = field(default_factory=list)
work: list[UserResumeWork] = field(default_factory=list)
internship: list[UserResumeInternship] = field(default_factory=list)
project: list[UserResumeProject] = field(default_factory=list)
competition: list[UserResumeCompetition] = field(default_factory=list)
async def load_resume_detail(session: AsyncSession, resume_id: int, user_id: int) -> ResumeDetail:
"""按ID查简历主表(校验归属)+ 5张子表,返回 ResumeDetail"""
result = await session.execute(select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("简历不存在")
edu, work, intern, proj, comp = await _load_sub_tables(session, resume_id)
return ResumeDetail(resume=resume, education=edu, work=work, internship=intern, project=proj, competition=comp)
async def load_default_resume_detail(session: AsyncSession, user_id: int) -> ResumeDetail:
"""自动选默认简历(先默认再最新)+ 5张子表,返回 ResumeDetail"""
result = await session.execute(
select(UserResume).where(UserResume.user_id == user_id, UserResume.is_default == 1)
.order_by(desc(UserResume.update_time)).limit(1))
resume = result.scalar_one_or_none()
if not resume:
result = await session.execute(
select(UserResume).where(UserResume.user_id == user_id)
.order_by(desc(UserResume.update_time)).limit(1))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("请先创建简历")
edu, work, intern, proj, comp = await _load_sub_tables(session, resume.id)
return ResumeDetail(resume=resume, education=edu, work=work, internship=intern, project=proj, competition=comp)
async def _load_sub_tables(session: AsyncSession, resume_id: int):
"""查询简历5张子表"""
edu = (await session.execute(select(UserResumeEducation).where(UserResumeEducation.resume_id == resume_id))).scalars().all()
work = (await session.execute(select(UserResumeWork).where(UserResumeWork.resume_id == resume_id))).scalars().all()
intern = (await session.execute(select(UserResumeInternship).where(UserResumeInternship.resume_id == resume_id))).scalars().all()
proj = (await session.execute(select(UserResumeProject).where(UserResumeProject.resume_id == resume_id))).scalars().all()
comp = (await session.execute(select(UserResumeCompetition).where(UserResumeCompetition.resume_id == resume_id))).scalars().all()
return edu, work, intern, proj, comp
+30 -68
View File
@@ -11,7 +11,7 @@ import json
import random import random
import string import string
from sqlalchemy import select, desc from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.skill_gap_analyzer.analyzer import ( from app.ai.skill_gap_analyzer.analyzer import (
@@ -26,11 +26,7 @@ from app.schemas.skill_gap import (
) )
from app.models.job import Job from app.models.job import Job
from app.models.user_resume import UserResume from app.models.user_resume import UserResume
from app.models.user_resume_competition import UserResumeCompetition from app.services.resume_loader import ResumeDetail, load_resume_detail, load_default_resume_detail
from app.models.user_resume_education import UserResumeEducation
from app.models.user_resume_internship import UserResumeInternship
from app.models.user_resume_project import UserResumeProject
from app.models.user_resume_work import UserResumeWork
# Redis 常量 # Redis 常量
CUSTOMIZE_RESUME_KEY_PREFIX = "customize:resume:" CUSTOMIZE_RESUME_KEY_PREFIX = "customize:resume:"
@@ -63,24 +59,25 @@ def _build_paragraphs(description: list[dict] | None) -> list[Paragraph]:
return [Paragraph(id=_rand_id(), text=item.get("text", "")) for item in description if isinstance(item, dict)] return [Paragraph(id=_rand_id(), text=item.get("text", "")) for item in description if isinstance(item, dict)]
def _build_resume_json(resume: UserResume, edu_list, work_list, intern_list, proj_list, comp_list) -> str: def _build_resume_json(detail: ResumeDetail) -> str:
"""拼装简历 JSON 字符串供 AI 使用""" """拼装简历 JSON 字符串供 AI 使用"""
resume = detail.resume
data = { data = {
"skills": resume.skills or [], "skills": resume.skills or [],
"certificates": resume.certificates or [], "certificates": resume.certificates or [],
"summary": resume.summary or "", "summary": resume.summary or "",
"targetPosition": resume.target_position or "", "targetPosition": resume.target_position or "",
} }
if edu_list: if detail.education:
data["education"] = [{"school": r.school, "major": r.major, "degree": r.degree, "description": r.description} for r in edu_list] data["education"] = [{"school": r.school, "major": r.major, "degree": r.degree, "description": r.description} for r in detail.education]
if work_list: if detail.work:
data["work"] = [{"companyName": r.company_name, "position": r.position, "description": r.description} for r in work_list] data["work"] = [{"companyName": r.company_name, "position": r.position, "description": r.description} for r in detail.work]
if intern_list: if detail.internship:
data["internship"] = [{"companyName": r.company_name, "position": r.position, "description": r.description} for r in intern_list] data["internship"] = [{"companyName": r.company_name, "position": r.position, "description": r.description} for r in detail.internship]
if proj_list: if detail.project:
data["project"] = [{"companyName": r.company_name, "projectName": r.project_name, "role": r.role, "description": r.description} for r in proj_list] data["project"] = [{"companyName": r.company_name, "projectName": r.project_name, "role": r.role, "description": r.description} for r in detail.project]
if comp_list: if detail.competition:
data["competition"] = [{"competitionName": r.competition_name, "award": r.award, "description": r.description} for r in comp_list] data["competition"] = [{"competitionName": r.competition_name, "award": r.award, "description": r.description} for r in detail.competition]
return json.dumps(data, ensure_ascii=False) return json.dumps(data, ensure_ascii=False)
@@ -94,21 +91,20 @@ class SkillGapService:
async def analyze_skill_gap(self, user_id: int, job_id: int) -> dict: async def analyze_skill_gap(self, user_id: int, job_id: int) -> dict:
"""差距分析完整流程:查简历 → 查岗位 → AI分析 → 计算匹配分""" """差距分析完整流程:查简历 → 查岗位 → AI分析 → 计算匹配分"""
# 1. 自动选择简历 # 1. 自动选择简历
resume = await self._pick_resume(user_id) detail = await load_default_resume_detail(self.session, user_id)
# 2. 查岗位 # 2. 查岗位
job = await self._get_job(job_id) job = await self._get_job(job_id)
skill_tags: list[str] = job.skill_tags or [] skill_tags: list[str] = job.skill_tags or []
# 3. skill_tags 为空 → 满分 # 3. skill_tags 为空 → 满分
if not skill_tags: if not skill_tags:
return self._gap_result(10.0, job, resume, []) return self._gap_result(10.0, job, detail.resume, [])
# 4. 查子表拼 AI 输入 # 4. 拼 AI 输入
edu, work, intern, proj, comp = await self._load_sub_tables(resume.id) resume_json = _build_resume_json(detail)
resume_json = _build_resume_json(resume, edu, work, intern, proj, comp)
# 5. AI 分析 # 5. AI 分析
missing = await analyze_skill_gap(skill_tags, resume_json) missing = await analyze_skill_gap(skill_tags, resume_json)
# 6. 计算匹配分 # 6. 计算匹配分
score = round((len(skill_tags) - len(missing)) / len(skill_tags) * 10, 1) score = round((len(skill_tags) - len(missing)) / len(skill_tags) * 10, 1)
return self._gap_result(score, job, resume, missing) return self._gap_result(score, job, detail.resume, missing)
@staticmethod @staticmethod
def _gap_result(score: float, job: Job, resume: UserResume, missing: list[str]) -> dict: def _gap_result(score: float, job: Job, resume: UserResume, missing: list[str]) -> dict:
@@ -127,16 +123,15 @@ class SkillGapService:
if not optimize_modules: if not optimize_modules:
raise ValueError("请至少选择一个优化模块") raise ValueError("请至少选择一个优化模块")
# 1. 查简历 + 岗位 # 1. 查简历 + 岗位
resume = await self._get_resume(resume_id, user_id) detail = await load_resume_detail(self.session, resume_id, user_id)
job = await self._get_job(job_id) job = await self._get_job(job_id)
edu_rows, work_rows, intern_rows, proj_rows, comp_rows = await self._load_sub_tables(resume.id)
# 2. 组装基础定制简历 # 2. 组装基础定制简历
cr = self._build_customize_resume(resume, edu_rows, work_rows, intern_rows, proj_rows, comp_rows) cr = self._build_customize_resume(detail)
# 3. 并发 AI 优化 # 3. 并发 AI 优化
tasks = [] tasks = []
job_desc = f"{job.description or ''}\n{job.requirement or ''}" job_desc = f"{job.description or ''}\n{job.requirement or ''}"
if "summary" in optimize_modules: if "summary" in optimize_modules:
tasks.append(("summary", optimize_summary(job.title or "", add_skills, resume.summary or ""))) tasks.append(("summary", optimize_summary(job.title or "", add_skills, detail.resume.summary or "")))
if "experience" in optimize_modules: if "experience" in optimize_modules:
for module_name, rows_json in self._experience_tasks(cr, job.title or "", job_desc): for module_name, rows_json in self._experience_tasks(cr, job.title or "", job_desc):
tasks.append((module_name, optimize_module(job.title or "", job_desc, rows_json))) tasks.append((module_name, optimize_module(job.title or "", job_desc, rows_json)))
@@ -371,30 +366,6 @@ class SkillGapService:
# ===== 内部工具方法 ===== # ===== 内部工具方法 =====
async def _pick_resume(self, user_id: int) -> UserResume:
"""自动选择简历:先查默认,再查最新"""
result = await self.session.execute(
select(UserResume).where(UserResume.user_id == user_id, UserResume.is_default == 1)
.order_by(desc(UserResume.update_time)).limit(1))
resume = result.scalar_one_or_none()
if not resume:
result = await self.session.execute(
select(UserResume).where(UserResume.user_id == user_id)
.order_by(desc(UserResume.update_time)).limit(1))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("请先创建简历")
return resume
async def _get_resume(self, resume_id: int, user_id: int) -> UserResume:
"""查指定简历"""
result = await self.session.execute(
select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
resume = result.scalar_one_or_none()
if not resume:
raise ValueError("简历不存在")
return resume
async def _get_job(self, job_id: int) -> Job: async def _get_job(self, job_id: int) -> Job:
"""查岗位""" """查岗位"""
result = await self.session.execute(select(Job).where(Job.id == job_id)) result = await self.session.execute(select(Job).where(Job.id == job_id))
@@ -403,18 +374,9 @@ class SkillGapService:
raise ValueError("岗位不存在") raise ValueError("岗位不存在")
return job return job
async def _load_sub_tables(self, resume_id: int): def _build_customize_resume(self, detail: ResumeDetail) -> CustomizeResume:
"""查询简历5张子表""" """从 ResumeDetail 组装 CustomizeResume"""
edu = (await self.session.execute(select(UserResumeEducation).where(UserResumeEducation.resume_id == resume_id))).scalars().all() resume = detail.resume
work = (await self.session.execute(select(UserResumeWork).where(UserResumeWork.resume_id == resume_id))).scalars().all()
intern = (await self.session.execute(select(UserResumeInternship).where(UserResumeInternship.resume_id == resume_id))).scalars().all()
proj = (await self.session.execute(select(UserResumeProject).where(UserResumeProject.resume_id == resume_id))).scalars().all()
comp = (await self.session.execute(select(UserResumeCompetition).where(UserResumeCompetition.resume_id == resume_id))).scalars().all()
return edu, work, intern, proj, comp
def _build_customize_resume(self, resume: UserResume, edu_rows, work_rows,
intern_rows, proj_rows, comp_rows) -> CustomizeResume:
"""从数据库记录组装 CustomizeResume"""
profile = ResumeProfile( profile = ResumeProfile(
avatarUrl=resume.avatar_url or "", name=resume.name or "", email=resume.email or "", avatarUrl=resume.avatar_url or "", name=resume.name or "", email=resume.email or "",
mobileNumber=resume.mobile_number or "", city=resume.city or "", mobileNumber=resume.mobile_number or "", city=resume.city or "",
@@ -427,19 +389,19 @@ class SkillGapService:
education=[Education(id=_rand_id(), school=r.school or "", major=r.major or "", education=[Education(id=_rand_id(), school=r.school or "", major=r.major or "",
degree=r.degree or "", studyType=r.study_type or "", degree=r.degree or "", studyType=r.study_type or "",
startDate=r.start_date or "", endDate=r.end_date or "", startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in edu_rows], description=_build_paragraphs(r.description)) for r in detail.education],
work=[Work(id=_rand_id(), companyName=r.company_name or "", position=r.position or "", work=[Work(id=_rand_id(), companyName=r.company_name or "", position=r.position or "",
startDate=r.start_date or "", endDate=r.end_date or "", startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in work_rows], description=_build_paragraphs(r.description)) for r in detail.work],
internship=[Internship(id=_rand_id(), companyName=r.company_name or "", position=r.position or "", internship=[Internship(id=_rand_id(), companyName=r.company_name or "", position=r.position or "",
startDate=r.start_date or "", endDate=r.end_date or "", startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in intern_rows], description=_build_paragraphs(r.description)) for r in detail.internship],
project=[Project(id=_rand_id(), companyName=r.company_name or "", projectName=r.project_name or "", project=[Project(id=_rand_id(), companyName=r.company_name or "", projectName=r.project_name or "",
role=r.role or "", startDate=r.start_date or "", endDate=r.end_date or "", role=r.role or "", startDate=r.start_date or "", endDate=r.end_date or "",
description=_build_paragraphs(r.description)) for r in proj_rows], description=_build_paragraphs(r.description)) for r in detail.project],
competition=[Competition(id=_rand_id(), competitionName=r.competition_name or "", award=r.award or "", competition=[Competition(id=_rand_id(), competitionName=r.competition_name or "", award=r.award or "",
awardDate=r.award_date or "", awardDate=r.award_date or "",
description=_build_paragraphs(r.description)) for r in comp_rows], description=_build_paragraphs(r.description)) for r in detail.competition],
) )
@staticmethod @staticmethod