添加nova chat 接口

This commit is contained in:
zk
2026-04-27 15:29:34 +08:00
parent de8030f3de
commit dbbc97a836
8 changed files with 234 additions and 10 deletions
+16 -10
View File
@@ -41,10 +41,13 @@ offerpie_python_ai/
│ ├─ skill_gap_analyzer/ # 技能差距分析 + 定制简历 AI 模块 │ ├─ skill_gap_analyzer/ # 技能差距分析 + 定制简历 AI 模块
│ │ ├─ prompts.py # 差距分析 + 简历优化 + Agent 规划(原子化操作)/ 单条记录修改 / 新增记录 Prompt 模板 + MODULE_SCHEMAS │ │ ├─ prompts.py # 差距分析 + 简历优化 + Agent 规划(原子化操作)/ 单条记录修改 / 新增记录 Prompt 模板 + MODULE_SCHEMAS
│ │ └─ analyzer.py # AI 调用逻辑(差距分析 + summary优化 + 经历优化 + Agent规划 + 单条记录修改 + 新增记录) │ │ └─ analyzer.py # AI 调用逻辑(差距分析 + summary优化 + 经历优化 + Agent规划 + 单条记录修改 + 新增记录)
─ job_agent/ # 求职助手 Agent AI 模块 ─ job_agent/ # 求职助手 Agent AI 模块
├─ prompts.py # 对话 System Prompt + 岗位简历优化 Prompt 模板 ├─ prompts.py # 对话 System Prompt + 岗位简历优化 Prompt 模板
├─ chat.py # AI 对话引擎(构造 prompt + 拼 messages + 调 LLM + 解析返回) ├─ chat.py # AI 对话引擎(构造 prompt + 拼 messages + 调 LLM + 解析返回)
└─ resume_optimizer.py # 岗位简历优化 AI 引擎(summary优化 + 经历优化,独立 chain) └─ resume_optimizer.py # 岗位简历优化 AI 引擎(summary优化 + 经历优化,独立 chain)
│ └─ nova_chat/ # Nova 对话助手 AI 模块
│ ├─ prompts.py # Nova 对话 System Prompt(岗位匹配评估 + 简历优化建议 + 通用求职对话)
│ └─ chat.py # Nova 对话 AI 引擎(拼 prompt + 调 LLM,返回纯文本)
├─ api/ # **路由层**REST API 接口) ├─ api/ # **路由层**REST API 接口)
│ ├─ health.py # 健康检查接口 GET /health/ │ ├─ health.py # 健康检查接口 GET /health/
@@ -52,7 +55,8 @@ offerpie_python_ai/
│ ├─ resume_diagnose.py # 简历诊断接口(POST 触发诊断 / GET 查询报告 / PUT 标记处理+用户评价 / POST 润色优化) │ ├─ resume_diagnose.py # 简历诊断接口(POST 触发诊断 / GET 查询报告 / PUT 标记处理+用户评价 / POST 润色优化)
│ ├─ skill_gap.py # 技能差距分析接口(差距分析 / 生成定制简历 / AI对话编辑) │ ├─ skill_gap.py # 技能差距分析接口(差距分析 / 生成定制简历 / AI对话编辑)
│ ├─ customize_resume.py # 定制简历接口(查询 / 修改 / 回滚) │ ├─ 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 声明式映射) ├─ models/ # **ORM 模型层**SQLAlchemy 声明式映射)
│ ├─ func_permission.py # 功能权限定义表(bg_func_permission │ ├─ func_permission.py # 功能权限定义表(bg_func_permission
@@ -77,7 +81,8 @@ offerpie_python_ai/
├─ schemas/ # **Schema 层**Pydantic 请求/响应/缓存模型) ├─ schemas/ # **Schema 层**Pydantic 请求/响应/缓存模型)
│ ├─ skill_gap.py # 技能差距分析 SchemaSkillGapParam、CustomizeResumeParam、AiEditParam │ ├─ skill_gap.py # 技能差距分析 SchemaSkillGapParam、CustomizeResumeParam、AiEditParam
│ ├─ customize_resume.py # 定制简历 SchemaCustomizeResume、ResumeProfile、Education、Work、Internship、Project、Competition、Paragraph │ ├─ customize_resume.py # 定制简历 SchemaCustomizeResume、ResumeProfile、Education、Work、Internship、Project、Competition、Paragraph
─ job_agent_chat.py # 求职助手对话 SchemaJobAgentChatParam、JobAgentChatDto、ToolParams ─ job_agent_chat.py # 求职助手对话 SchemaJobAgentChatParam、JobAgentChatDto、ToolParams
│ └─ nova_chat.py # Nova 对话助手 SchemaNovaChatParam、NovaChatDto
└─ services/ # **业务逻辑层** └─ services/ # **业务逻辑层**
├─ func_permission_service.py # 功能权限服务(校验+扣减+回退,逻辑与Java端一致) ├─ func_permission_service.py # 功能权限服务(校验+扣减+回退,逻辑与Java端一致)
@@ -86,7 +91,8 @@ offerpie_python_ai/
├─ skill_gap_service.py # 技能差距分析服务(差距分析→定制简历生成→AI对话编辑) ├─ skill_gap_service.py # 技能差距分析服务(差距分析→定制简历生成→AI对话编辑)
├─ resume_loader.py # 简历统一查询模块(按ID查/自动选默认+5张子表,返回 ResumeDetail dataclass ├─ resume_loader.py # 简历统一查询模块(按ID查/自动选默认+5张子表,返回 ResumeDetail dataclass
├─ customize_resume_store.py # 定制简历 Redis 存取模块(保存自动回滚备份、查询、回滚) ├─ customize_resume_store.py # 定制简历 Redis 存取模块(保存自动回滚备份、查询、回滚)
─ job_agent_chat_service.py # 求职助手对话服务(查简历→序列化→调AI模块完成对话) ─ job_agent_chat_service.py # 求职助手对话服务(查简历→序列化→调AI模块完成对话)
└─ nova_chat_service.py # Nova 对话助手服务(查简历+查岗位(可选)→调AI,纯对话不持久化)
``` ```
## 2️⃣ 各层模块职责 ## 2️⃣ 各层模块职责
@@ -94,11 +100,11 @@ offerpie_python_ai/
|------|----------|-------------| |------|----------|-------------|
| **config** | 统一配置管理,基于 Pydantic Settings,支持 .env 文件加载 | `Settings`(数据库、Redis、LLM供应商、JWT、CORS、日志等全部配置项) | | **config** | 统一配置管理,基于 Pydantic Settings,支持 .env 文件加载 | `Settings`(数据库、Redis、LLM供应商、JWT、CORS、日志等全部配置项) |
| **core** | 核心基础设施:数据库连接、Redis连接、鉴权、日志、中间件、异常处理、统一响应 | `database.py``redis.py``auth.py``middleware.py``exceptions.py``logger.py``StandardResponse` | | **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/`(求职助手对话 + 岗位简历优化) | | **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`(求职助手对话 + 岗位简历优化) | | **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` | | **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生成) | | **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️⃣ 技术栈 ## 3️⃣ 技术栈
| 类别 | 技术选型 | 说明 | | 类别 | 技术选型 | 说明 |
+1
View File
@@ -0,0 +1 @@
+32
View File
@@ -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 "抱歉,我暂时无法回复,请稍后再试。"
+48
View File
@@ -0,0 +1,48 @@
"""Nova Chat Prompt 模板
Nova 求职对话助手的 system prompt,根据用户意图自行选择回答策略。
"""
SYSTEM_PROMPT = """你是 NovaOfferPie 的 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. 如果没有岗位上下文,不要主动提及岗位匹配评估相关内容"""
+25
View File
@@ -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)
+2
View File
@@ -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.skill_gap import router as skill_gap_router
from app.api.customize_resume import router as customize_resume_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.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(health_router)
app.include_router(resume_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(skill_gap_router)
app.include_router(customize_resume_router) app.include_router(customize_resume_router)
app.include_router(job_agent_chat_router) app.include_router(job_agent_chat_router)
app.include_router(nova_chat_router)
# ============================== # ==============================
if __name__ == "__main__": if __name__ == "__main__":
+27
View File
@@ -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格式")
+83
View File
@@ -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 "暂无简历信息"