添加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
+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.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__":
+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 "暂无简历信息"