diff --git a/app/ai/browser_plug/__init__.py b/app/ai/browser_plug/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/ai/browser_plug/__init__.py @@ -0,0 +1 @@ + diff --git a/app/ai/browser_plug/form_filler.py b/app/ai/browser_plug/form_filler.py new file mode 100644 index 0000000..2a456be --- /dev/null +++ b/app/ai/browser_plug/form_filler.py @@ -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生成回答失败,请稍后重试") diff --git a/app/ai/browser_plug/prompts.py b/app/ai/browser_plug/prompts.py new file mode 100644 index 0000000..2e96f76 --- /dev/null +++ b/app/ai/browser_plug/prompts.py @@ -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. 直接输出填写内容,不要任何解释、前缀或格式包裹""" diff --git a/app/api/browser_plug.py b/app/api/browser_plug.py new file mode 100644 index 0000000..a37c913 --- /dev/null +++ b/app/api/browser_plug.py @@ -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) diff --git a/app/main.py b/app/main.py index fd2f7e8..f728857 100644 --- a/app/main.py +++ b/app/main.py @@ -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__": diff --git a/app/schemas/browser_plug.py b/app/schemas/browser_plug.py new file mode 100644 index 0000000..021c9cc --- /dev/null +++ b/app/schemas/browser_plug.py @@ -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生成的填写内容") diff --git a/app/services/browser_plug_service.py b/app/services/browser_plug_service.py new file mode 100644 index 0000000..7fab37a --- /dev/null +++ b/app/services/browser_plug_service.py @@ -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 "暂无简历信息"