添加 插件表单ai 能力
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""表单填写 AI 生成模块
|
||||
|
||||
根据用户简历、岗位信息和表单字段,调 LLM 生成填写内容。
|
||||
依赖:LLM 枚举、browser_plug/prompts
|
||||
"""
|
||||
|
||||
from app.ai.browser_plug.prompts import FORM_FILL_SYSTEM_PROMPT
|
||||
from app.ai.models import LLM
|
||||
from app.core.logger import log
|
||||
|
||||
|
||||
async def generate_form_answer(resume_text: str, job_text: str, agent_config_text: str,
|
||||
label: str, reference: str | None, field_type: str) -> str:
|
||||
"""生成表单字段的填写内容"""
|
||||
system_content = FORM_FILL_SYSTEM_PROMPT.format(
|
||||
resume_text=resume_text,
|
||||
job_text=job_text,
|
||||
agent_config_text=agent_config_text,
|
||||
)
|
||||
|
||||
# 构造用户消息
|
||||
user_parts = [f"表单字段:{label}", f"字段类型:{field_type}"]
|
||||
if reference:
|
||||
user_parts.append(f"参考信息:{reference}")
|
||||
user_message = "\n".join(user_parts)
|
||||
|
||||
messages = [("system", system_content), ("human", user_message)]
|
||||
|
||||
try:
|
||||
llm = LLM.DOUBAO_PRO_32K.create(temperature=0.3)
|
||||
result = await llm.ainvoke(messages)
|
||||
return result.content.strip()
|
||||
except Exception as e:
|
||||
log.error(f"表单填写AI调用失败: {e}")
|
||||
raise ValueError("AI生成回答失败,请稍后重试")
|
||||
@@ -0,0 +1,20 @@
|
||||
"""浏览器插件 AI Prompt 模板"""
|
||||
|
||||
FORM_FILL_SYSTEM_PROMPT = """你是一个求职表单填写助手。根据用户的简历信息和岗位信息,为招聘网站的表单字段生成合适的填写内容。
|
||||
|
||||
【用户简历】
|
||||
{resume_text}
|
||||
|
||||
【岗位信息】
|
||||
{job_text}
|
||||
|
||||
【网申预设配置】
|
||||
{agent_config_text}
|
||||
|
||||
【规则】
|
||||
1. 根据表单字段的标签和类型,生成最合适的填写内容
|
||||
2. 如果是选择题(select/radio/checkbox),必须从提供的选项中选择,返回选项原文
|
||||
3. 如果是文本题(input/textarea),根据简历内容生成简洁、真实的回答
|
||||
4. 不要编造简历中没有的信息
|
||||
5. 回答要专业、得体,符合求职场景
|
||||
6. 直接输出填写内容,不要任何解释、前缀或格式包裹"""
|
||||
@@ -0,0 +1,21 @@
|
||||
"""浏览器插件接口"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.core.context import RequestContext
|
||||
from app.core.database import get_db
|
||||
from app.schemas.browser_plug import FormFillAnswerParam, FormFillAnswerDto
|
||||
from app.services.browser_plug_service import BrowserPlugService
|
||||
|
||||
router = APIRouter(prefix="/browser-plug", tags=["浏览器插件"])
|
||||
|
||||
|
||||
@router.post("/form-fill-answer", summary="表单填写AI生成答案", response_model=FormFillAnswerDto)
|
||||
async def form_fill_answer(param: FormFillAnswerParam):
|
||||
"""插件端规则匹配不上的表单字段,调AI根据用户简历和岗位信息生成填写内容"""
|
||||
user_id = RequestContext.user_id.get()
|
||||
async for session in get_db():
|
||||
service = BrowserPlugService(session)
|
||||
value = await service.form_fill_answer(
|
||||
user_id, param.job_id, param.label, param.reference, param.type)
|
||||
return FormFillAnswerDto(value=value)
|
||||
@@ -37,6 +37,7 @@ 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
|
||||
from app.api.browser_plug import router as browser_plug_router
|
||||
|
||||
app.include_router(health_router)
|
||||
app.include_router(resume_router)
|
||||
@@ -45,6 +46,7 @@ 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)
|
||||
app.include_router(browser_plug_router)
|
||||
# ==============================
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""浏览器插件相关 Schema
|
||||
|
||||
请求参数 Param、响应 Dto。
|
||||
字段命名使用 camelCase alias,与前端 JSON 对齐。
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FormFillAnswerParam(BaseModel):
|
||||
"""表单填写AI生成答案 请求参数"""
|
||||
job_id: int = Field(default=0, alias="jobId", description="岗位ID,不传默认0")
|
||||
label: str = Field(..., description="表单字段标签文本")
|
||||
reference: Optional[str] = Field(default=None, description="回答参考(选项列表、字数限制等)")
|
||||
type: str = Field(..., description="表单类型:input/textarea/select/radio/checkbox")
|
||||
|
||||
|
||||
class FormFillAnswerDto(BaseModel):
|
||||
"""表单填写AI生成答案 响应"""
|
||||
value: str = Field(..., description="AI生成的填写内容")
|
||||
@@ -0,0 +1,111 @@
|
||||
"""浏览器插件 Service
|
||||
|
||||
表单填写AI生成:查简历 + 岗位 + agent_config → 调 AI 生成答案。
|
||||
"""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.ai.browser_plug.form_filler import generate_form_answer
|
||||
from app.models.job import Job
|
||||
from app.models.job_agent_config import JobAgentConfig
|
||||
from app.services import customize_resume_store
|
||||
|
||||
|
||||
class BrowserPlugService:
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def form_fill_answer(self, user_id: int, job_id: int, label: str, reference: str | None, field_type: str) -> str:
|
||||
"""生成表单字段的填写内容"""
|
||||
# 1. 获取简历数据(有定制简历用定制,没有自动走默认)
|
||||
resume_data = await customize_resume_store.get(user_id, job_id)
|
||||
resume_text = self._serialize_resume(resume_data)
|
||||
|
||||
# 2. 获取岗位信息
|
||||
job_text = await self._get_job_text(job_id)
|
||||
|
||||
# 3. 获取 agent_config
|
||||
agent_config_text = await self._get_agent_config_text(user_id)
|
||||
|
||||
# 4. 调 AI 生成
|
||||
return await generate_form_answer(resume_text, job_text, agent_config_text, label, reference, field_type)
|
||||
|
||||
async def _get_job_text(self, job_id: int) -> str:
|
||||
"""获取岗位信息文本"""
|
||||
if job_id == 0:
|
||||
return "未指定岗位"
|
||||
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}")
|
||||
return "\n".join(parts) if parts else "未指定岗位"
|
||||
|
||||
async def _get_agent_config_text(self, user_id: int) -> str:
|
||||
"""获取 agent_config 预设答案文本"""
|
||||
result = await self.session.execute(
|
||||
select(JobAgentConfig).where(JobAgentConfig.user_id == user_id))
|
||||
config = result.scalar_one_or_none()
|
||||
if not config:
|
||||
return "无预设配置"
|
||||
parts = []
|
||||
if config.accept_dept_transfer:
|
||||
parts.append(f"是否接受部门调剂:{config.accept_dept_transfer}")
|
||||
if config.accept_location_transfer:
|
||||
parts.append(f"是否接受地点调剂:{config.accept_location_transfer}")
|
||||
if config.interview_type:
|
||||
parts.append(f"可参加面试方式:{'、'.join(config.interview_type)}")
|
||||
if config.languages:
|
||||
lang_strs = [f"{l.get('language', '')}({l.get('proficiency', '')})" for l in config.languages]
|
||||
parts.append(f"语言能力:{'、'.join(lang_strs)}")
|
||||
if config.available_date:
|
||||
parts.append(f"预计到岗时间:{config.available_date}")
|
||||
if config.intern_days_per_week:
|
||||
parts.append(f"每周实习天数:{config.intern_days_per_week}")
|
||||
if config.intern_duration:
|
||||
parts.append(f"预计实习时长:{config.intern_duration}")
|
||||
return "\n".join(parts) if parts else "无预设配置"
|
||||
|
||||
@staticmethod
|
||||
def _serialize_resume(data: dict) -> str:
|
||||
"""将简历 dict 序列化为文本"""
|
||||
if not data:
|
||||
return "暂无简历信息"
|
||||
parts = []
|
||||
resume = data.get("resume", {})
|
||||
if resume.get("name"):
|
||||
parts.append(f"姓名:{resume['name']}")
|
||||
if resume.get("email"):
|
||||
parts.append(f"邮箱:{resume['email']}")
|
||||
if resume.get("mobileNumber"):
|
||||
parts.append(f"手机:{resume['mobileNumber']}")
|
||||
if resume.get("city"):
|
||||
parts.append(f"城市:{resume['city']}")
|
||||
if resume.get("skills"):
|
||||
parts.append(f"技能:{'、'.join(resume['skills'])}")
|
||||
if resume.get("certificates"):
|
||||
parts.append(f"证书:{'、'.join(resume['certificates'])}")
|
||||
if resume.get("summary"):
|
||||
parts.append(f"个人概述:{resume['summary']}")
|
||||
for section, title in [("education", "教育经历"), ("work", "工作经历"), ("internship", "实习经历"), ("project", "项目经历"), ("competition", "竞赛经历")]:
|
||||
items = data.get(section, [])
|
||||
if items:
|
||||
parts.append(f"{title}:")
|
||||
for item in items:
|
||||
if section == "education":
|
||||
parts.append(f" - {item.get('school', '')} {item.get('major', '')} {item.get('degree', '')}")
|
||||
elif section in ("work", "internship"):
|
||||
parts.append(f" - {item.get('companyName', '')} {item.get('position', '')}")
|
||||
elif section == "project":
|
||||
parts.append(f" - {item.get('projectName', '')} {item.get('role', '')}")
|
||||
elif section == "competition":
|
||||
parts.append(f" - {item.get('competitionName', '')} {item.get('award', '')}")
|
||||
return "\n".join(parts) if parts else "暂无简历信息"
|
||||
Reference in New Issue
Block a user