diff --git a/.kiro/steering/项目结构说明.md b/.kiro/steering/项目结构说明.md index e2eb737..11c5525 100644 --- a/.kiro/steering/项目结构说明.md +++ b/.kiro/steering/项目结构说明.md @@ -41,10 +41,13 @@ offerpie_python_ai/ │ ├─ skill_gap_analyzer/ # 技能差距分析 + 定制简历 AI 模块 │ │ ├─ prompts.py # 差距分析 + 简历优化 + Agent 规划(原子化操作)/ 单条记录修改 / 新增记录 Prompt 模板 + MODULE_SCHEMAS │ │ └─ analyzer.py # AI 调用逻辑(差距分析 + summary优化 + 经历优化 + Agent规划 + 单条记录修改 + 新增记录) - │ └─ job_agent/ # 求职助手 Agent AI 模块 - │ ├─ prompts.py # 对话 System Prompt + 岗位简历优化 Prompt 模板 - │ ├─ chat.py # AI 对话引擎(构造 prompt + 拼 messages + 调 LLM + 解析返回) - │ └─ resume_optimizer.py # 岗位简历优化 AI 引擎(summary优化 + 经历优化,独立 chain) + │ ├─ job_agent/ # 求职助手 Agent AI 模块 + │ │ ├─ prompts.py # 对话 System Prompt + 岗位简历优化 Prompt 模板 + │ │ ├─ chat.py # AI 对话引擎(构造 prompt + 拼 messages + 调 LLM + 解析返回) + │ │ └─ resume_optimizer.py # 岗位简历优化 AI 引擎(summary优化 + 经历优化,独立 chain) + │ └─ nova_chat/ # Nova 对话助手 AI 模块 + │ ├─ prompts.py # Nova 对话 System Prompt(岗位匹配评估 + 简历优化建议 + 通用求职对话) + │ └─ chat.py # Nova 对话 AI 引擎(拼 prompt + 调 LLM,返回纯文本) │ ├─ api/ # **路由层**(REST API 接口) │ ├─ health.py # 健康检查接口 GET /health/ @@ -52,7 +55,8 @@ offerpie_python_ai/ │ ├─ resume_diagnose.py # 简历诊断接口(POST 触发诊断 / GET 查询报告 / PUT 标记处理+用户评价 / POST 润色优化) │ ├─ skill_gap.py # 技能差距分析接口(差距分析 / 生成定制简历 / AI对话编辑) │ ├─ customize_resume.py # 定制简历接口(查询 / 修改 / 回滚) - │ └─ job_agent_chat.py # 求职助手接口(POST /job-agent/chat 对话、POST /job-agent/optimize-resume 岗位简历优化) + │ ├─ job_agent_chat.py # 求职助手接口(POST /job-agent/chat 对话、POST /job-agent/optimize-resume 岗位简历优化) + │ └─ nova_chat.py # Nova 对话助手接口(POST /nova-chat/chat 纯对话,支持可选岗位上下文) │ ├─ models/ # **ORM 模型层**(SQLAlchemy 声明式映射) │ ├─ func_permission.py # 功能权限定义表(bg_func_permission) @@ -77,7 +81,8 @@ offerpie_python_ai/ ├─ schemas/ # **Schema 层**(Pydantic 请求/响应/缓存模型) │ ├─ skill_gap.py # 技能差距分析 Schema(SkillGapParam、CustomizeResumeParam、AiEditParam) │ ├─ customize_resume.py # 定制简历 Schema(CustomizeResume、ResumeProfile、Education、Work、Internship、Project、Competition、Paragraph) - │ └─ job_agent_chat.py # 求职助手对话 Schema(JobAgentChatParam、JobAgentChatDto、ToolParams) + │ ├─ job_agent_chat.py # 求职助手对话 Schema(JobAgentChatParam、JobAgentChatDto、ToolParams) + │ └─ nova_chat.py # Nova 对话助手 Schema(NovaChatParam、NovaChatDto) │ └─ services/ # **业务逻辑层** ├─ func_permission_service.py # 功能权限服务(校验+扣减+回退,逻辑与Java端一致) @@ -86,7 +91,8 @@ offerpie_python_ai/ ├─ skill_gap_service.py # 技能差距分析服务(差距分析→定制简历生成→AI对话编辑) ├─ resume_loader.py # 简历统一查询模块(按ID查/自动选默认+5张子表,返回 ResumeDetail dataclass) ├─ customize_resume_store.py # 定制简历 Redis 存取模块(保存自动回滚备份、查询、回滚) - └─ job_agent_chat_service.py # 求职助手对话服务(查简历→序列化→调AI模块完成对话) + ├─ job_agent_chat_service.py # 求职助手对话服务(查简历→序列化→调AI模块完成对话) + └─ nova_chat_service.py # Nova 对话助手服务(查简历+查岗位(可选)→调AI,纯对话不持久化) ``` ## 2️⃣ 各层模块职责 @@ -94,11 +100,11 @@ offerpie_python_ai/ |------|----------|-------------| | **config** | 统一配置管理,基于 Pydantic Settings,支持 .env 文件加载 | `Settings`(数据库、Redis、LLM供应商、JWT、CORS、日志等全部配置项) | | **core** | 核心基础设施:数据库连接、Redis连接、鉴权、日志、中间件、异常处理、统一响应 | `database.py`、`redis.py`、`auth.py`、`middleware.py`、`exceptions.py`、`logger.py`、`StandardResponse` | -| **ai** | AI 模型管理 + 业务 AI 能力 | `LLM` 枚举、`resume_extractor/`(简历并行提取)、`resume_diagnoser/`(简历诊断)、`skill_gap_analyzer/`(技能差距分析 + 定制简历优化 + Agent 原子化规划 + 单条记录修改/新增)、`job_agent/`(求职助手对话 + 岗位简历优化) | -| **api** | REST API 路由定义 | `health.py`(健康检查)、`resume.py`(简历上传解析)、`resume_diagnose.py`(简历诊断)、`skill_gap.py`(技能差距分析 + 生成定制简历 + AI对话编辑)、`customize_resume.py`(定制简历查询/修改/回滚)、`job_agent_chat.py`(求职助手对话 + 岗位简历优化) | +| **ai** | AI 模型管理 + 业务 AI 能力 | `LLM` 枚举、`resume_extractor/`(简历并行提取)、`resume_diagnoser/`(简历诊断)、`skill_gap_analyzer/`(技能差距分析 + 定制简历优化 + Agent 原子化规划 + 单条记录修改/新增)、`job_agent/`(求职助手对话 + 岗位简历优化)、`nova_chat/`(Nova 对话助手,纯对话) | +| **api** | REST API 路由定义 | `health.py`(健康检查)、`resume.py`(简历上传解析)、`resume_diagnose.py`(简历诊断)、`skill_gap.py`(技能差距分析 + 生成定制简历 + AI对话编辑)、`customize_resume.py`(定制简历查询/修改/回滚)、`job_agent_chat.py`(求职助手对话 + 岗位简历优化)、`nova_chat.py`(Nova 对话助手) | | **models** | SQLAlchemy ORM 模型,与 Java 端共享同一数据库 | `FuncPermission`、`UserFuncPermissionStock`、`UserFuncUsageLog`、`UserResume`、`UserResumeEducation`/`Work`/`Internship`/`Project`/`Competition`、`ResumeDiagnosisReport`、`ResumeDiagnosisIssue`、`Job`(只读)、`JobAgentConfig` | | **tool** | 无状态通用工具,不依赖数据库/Redis/用户上下文 | `file_parser.py`(PDF/Word/TXT 文件解析为纯文本)、`json_helper.py`(AI 输出 JSON 解析,去 markdown 代码块 + json_repair 容错)、`snowflake.py`(雪花ID生成) | -| **services** | 业务逻辑实现 | `FuncPermissionService`(功能权限校验、扣减、回退)、`ResumeParseService`(简历文件解析→AI结构化→入库)、`ResumeDiagnoseService`(简历诊断→AI并行分析→评级→入库)、`SkillGapService`(技能差距分析→定制简历生成→AI对话编辑)、`resume_loader`(简历统一查询,返回ResumeDetail)、`customize_resume_store`(定制简历Redis存取+数据构建,自动回滚备份)、`JobAgentChatService`(求职助手对话+岗位简历优化) | +| **services** | 业务逻辑实现 | `FuncPermissionService`(功能权限校验、扣减、回退)、`ResumeParseService`(简历文件解析→AI结构化→入库)、`ResumeDiagnoseService`(简历诊断→AI并行分析→评级→入库)、`SkillGapService`(技能差距分析→定制简历生成→AI对话编辑)、`resume_loader`(简历统一查询,返回ResumeDetail)、`customize_resume_store`(定制简历Redis存取+数据构建,自动回滚备份)、`JobAgentChatService`(求职助手对话+岗位简历优化)、`NovaChatService`(Nova对话助手,查简历+查岗位→调AI) | ## 3️⃣ 技术栈 | 类别 | 技术选型 | 说明 | diff --git a/app/ai/nova_chat/__init__.py b/app/ai/nova_chat/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/ai/nova_chat/__init__.py @@ -0,0 +1 @@ + diff --git a/app/ai/nova_chat/chat.py b/app/ai/nova_chat/chat.py new file mode 100644 index 0000000..a43ea02 --- /dev/null +++ b/app/ai/nova_chat/chat.py @@ -0,0 +1,32 @@ +"""Nova Chat AI 引擎 + +构造 prompt + 调 LLM,直接返回 Markdown 文本。 +依赖:LLM 枚举、nova_chat/prompts +""" + +from app.ai.models import LLM +from app.ai.nova_chat.prompts import SYSTEM_PROMPT +from app.core.logger import log + + +async def nova_chat(resume_text: str, message: str, history: list[dict], + job_context: str) -> str: + """Nova 对话:拼 prompt → 拼历史 → 调 LLM → 返回 Markdown 文本""" + + system_content = SYSTEM_PROMPT.format( + resume_text=resume_text, + job_context=job_context, + ) + + messages = [("system", system_content)] + for msg in history: + messages.append((msg["role"], msg["content"])) + messages.append(("human", message)) + + try: + llm = LLM.JIAYU_CLAUDE_SONNET_4_5.create(temperature=0.7) + result = await llm.ainvoke(messages) + return result.content.strip() + except Exception as e: + log.error(f"Nova Chat AI 调用失败: {e}") + return "抱歉,我暂时无法回复,请稍后再试。" diff --git a/app/ai/nova_chat/prompts.py b/app/ai/nova_chat/prompts.py new file mode 100644 index 0000000..f3987b8 --- /dev/null +++ b/app/ai/nova_chat/prompts.py @@ -0,0 +1,48 @@ +"""Nova Chat Prompt 模板 + +Nova 求职对话助手的 system prompt,根据用户意图自行选择回答策略。 +""" + +SYSTEM_PROMPT = """你是 Nova,OfferPie 的 AI 求职助手。 +你现在是一个兼具客观、犀利的资深技术招聘专家。你的任务是帮候选人进行冷酷的岗位差距分析,而不是一味鼓励。 + +【候选人简历】 +{resume_text} + +【当前浏览岗位】 +{job_context} + +【回答策略 — 根据用户意图选择】 + +策略1:岗位匹配评估 +当用户问"这个工作适合我吗"、"告诉我这个工作为什么适合我"等意图时: +- 必须包含以下四个固定维度,使用加粗标题: + **相关经验**、**资历级别**、**教育背景**、**核心技能** +- 每个维度的结论用 Emoji 标识: + ✅ 完全匹配或基本满足要求 + ❌ 存在明显差距,不满足核心要求 +- 基于候选人简历中的过往经历,与JD中的核心岗位职责进行具体对比分析。必须结合具体业务场景,如:虽然具备金融分析经验,但缺乏JD中明确要求的MVP构建经验。 +- 基于候选人的工作年限、当前职级或应届生身份,与JD中要求的资历(如Entry-level/Senior)进行对比评估。 +- 基于候选人的最高学历及专业,说明其如何为该岗位的核心工作(如产品开发、战略规划)提供基础支撑,或指出不符之处。 +- ❌ 缺失的核心技能:[技能词1]、[技能词2]、[技能词3](*注:如果全部匹配则不显示此行*) +- ✅ ❌ 具体说明候选人现有的技能池(如Python、数据分析)与JD技能树的重合度,并详细说明差距所在(如缺乏产品思维、数据驱动开发经验)。 +- 禁止使用套话,必须提取JD中的具体名词和用户简历中的具体名词 +- 高度个性化论述 + +策略2:简历优化建议 +当用户问"怎么优化简历"、"我想投递这个岗位,怎么优化简历"等意图时: +- 必须结合当前岗位JD和用户简历,给出针对性的修改建议 +- 指出简历中可以突出的亮点、需要补充的内容 +- 建议具体到哪个模块(教育/工作/项目经历等)怎么改 +- 将候选人现有的经历用目标岗位(JD)的行话重新包装,绝不能凭空捏造候选人没有做过的事情,而是改变描述的侧重点 + +策略3:通用求职对话 +其他求职相关问题(面试技巧、行业分析、薪资谈判等): +- 简洁专业地回答 +- 不超过200字 + +【交互规则】 +1. 保持简洁:每个维度1-2句话点到为止,不展开论述,不重复信息,拒绝废话 +2. 只聊求职相关话题,其他话题礼貌拒绝 +3. 禁止使用套话,必须提取JD中的具体名词和用户简历中的具体名词 +4. 如果没有岗位上下文,不要主动提及岗位匹配评估相关内容""" diff --git a/app/api/nova_chat.py b/app/api/nova_chat.py new file mode 100644 index 0000000..302f837 --- /dev/null +++ b/app/api/nova_chat.py @@ -0,0 +1,25 @@ +"""Nova Chat 对话接口""" + +from fastapi import APIRouter + +from app.core.context import RequestContext +from app.core.database import get_db +from app.schemas.nova_chat import NovaChatParam, NovaChatDto + +router = APIRouter(prefix="/nova-chat", tags=["Nova对话助手"]) + + +@router.post("/chat", summary="Nova对话", response_model=NovaChatDto) +async def chat(param: NovaChatParam): + """Nova 求职对话,根据用户简历和岗位上下文(可选)提供求职分析与建议""" + from app.services.nova_chat_service import NovaChatService + + user_id = RequestContext.user_id.get() + async for session in get_db(): + service = NovaChatService(session) + result = await service.chat( + user_id, param.resume_id, param.message, + [msg.model_dump() for msg in param.history], + param.job_id, + ) + return NovaChatDto(message=result) diff --git a/app/main.py b/app/main.py index e9e012f..fd2f7e8 100644 --- a/app/main.py +++ b/app/main.py @@ -36,6 +36,7 @@ from app.api.resume_diagnose import router as resume_diagnose_router from app.api.skill_gap import router as skill_gap_router from app.api.customize_resume import router as customize_resume_router from app.api.job_agent_chat import router as job_agent_chat_router +from app.api.nova_chat import router as nova_chat_router app.include_router(health_router) app.include_router(resume_router) @@ -43,6 +44,7 @@ app.include_router(resume_diagnose_router) app.include_router(skill_gap_router) app.include_router(customize_resume_router) app.include_router(job_agent_chat_router) +app.include_router(nova_chat_router) # ============================== if __name__ == "__main__": diff --git a/app/schemas/nova_chat.py b/app/schemas/nova_chat.py new file mode 100644 index 0000000..0e71a5e --- /dev/null +++ b/app/schemas/nova_chat.py @@ -0,0 +1,27 @@ +"""Nova Chat Schema + +请求参数 Param、响应 Dto。 +字段命名使用 camelCase alias,与前端 JSON 对齐。 +""" + +from typing import Literal, Optional + +from pydantic import BaseModel, Field + +ChatRole = Literal["user", "assistant"] + + +class ChatMessage(BaseModel): + role: ChatRole + content: str + + +class NovaChatParam(BaseModel): + message: str = Field(..., description="用户输入的消息") + resume_id: int = Field(..., alias="resumeId", description="简历ID") + job_id: Optional[int] = Field(default=None, alias="jobId", description="当前浏览岗位ID,不传则无岗位上下文") + history: list[ChatMessage] = Field(default_factory=list, description="历史对话,前端维护") + + +class NovaChatDto(BaseModel): + message: str = Field(..., description="AI回复,Markdown格式") diff --git a/app/services/nova_chat_service.py b/app/services/nova_chat_service.py new file mode 100644 index 0000000..dcad208 --- /dev/null +++ b/app/services/nova_chat_service.py @@ -0,0 +1,83 @@ +"""Nova Chat Service + +主要功能:查询简历数据 + 查询岗位(可选),调用 AI 模块完成对话。 +依赖:resume_loader(简历统一查询)、nova_chat AI 模块 +使用表:bg_user_resume + 5张子表(通过 resume_loader 查询)、bg_job(查岗位,可选) +""" + +from typing import Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.ai.nova_chat.chat import nova_chat +from app.models.job import Job +from app.services.resume_loader import ResumeDetail, load_resume_detail + + +class NovaChatService: + + def __init__(self, session: AsyncSession): + self.session = session + + async def chat(self, user_id: int, resume_id: int, message: str, + history: list[dict], job_id: Optional[int] = None) -> str: + """Nova 对话:查简历 → 查岗位(可选) → 序列化 → 调 AI""" + detail = await load_resume_detail(self.session, resume_id, user_id) + resume_text = self._build_resume_text(detail) + job_context = await self._build_job_context(job_id) if job_id else "用户当前未浏览具体岗位" + return await nova_chat(resume_text, message, history, job_context) + + async def _build_job_context(self, job_id: int) -> str: + """查岗位并序列化为文本""" + result = await self.session.execute(select(Job).where(Job.id == job_id)) + job = result.scalar_one_or_none() + if not job: + return "用户当前未浏览具体岗位" + parts = [] + if job.title: + parts.append(f"岗位名称:{job.title}") + if job.description: + parts.append(f"岗位职责:{job.description}") + if job.requirement: + parts.append(f"任职要求:{job.requirement}") + if job.skill_tags: + parts.append(f"技能标签:{'、'.join(job.skill_tags)}") + return "\n".join(parts) if parts else "用户当前未浏览具体岗位" + + @staticmethod + def _build_resume_text(detail: ResumeDetail) -> str: + """将简历数据序列化为文本供 AI 使用""" + resume = detail.resume + parts = [] + if resume.name: + parts.append(f"姓名:{resume.name}") + if resume.target_position: + parts.append(f"目标岗位:{resume.target_position}") + if resume.skills: + parts.append(f"技能:{'、'.join(resume.skills)}") + if resume.certificates: + parts.append(f"证书:{'、'.join(resume.certificates)}") + if resume.summary: + parts.append(f"个人概述:{resume.summary}") + if detail.education: + parts.append("教育经历:") + for r in detail.education: + parts.append(f" - {r.school or ''} {r.major or ''} {r.degree or ''}") + if detail.work: + parts.append("工作经历:") + for r in detail.work: + parts.append(f" - {r.company_name or ''} {r.position or ''}") + if detail.internship: + parts.append("实习经历:") + for r in detail.internship: + parts.append(f" - {r.company_name or ''} {r.position or ''}") + if detail.project: + parts.append("项目经历:") + for r in detail.project: + parts.append(f" - {r.project_name or ''} {r.role or ''}") + if detail.competition: + parts.append("竞赛经历:") + for r in detail.competition: + parts.append(f" - {r.competition_name or ''} {r.award or ''}") + return "\n".join(parts) if parts else "暂无简历信息"