Files
resume-agent/backend/app/target_position_suggester.py

81 lines
3.9 KiB
Python

"""Grounded target-position recommendations for users who are still exploring."""
from __future__ import annotations
from typing import Any, Protocol
from pydantic import Field
from .llm_services import OpenAICompatibleStructuredClient, StrictSchema
from .settings import Settings
class PositionSuggestion(StrictSchema):
title: str = Field(min_length=1, max_length=32)
reason: str = Field(min_length=1, max_length=100)
class TargetPositionSuggestionOutput(StrictSchema):
positions: list[PositionSuggestion] = Field(min_length=3, max_length=5)
class TargetPositionSuggester(Protocol):
def suggest(self, *, major: str, job_type: str | None, interests: str | None) -> list[dict[str, str]]: ...
class RuleBasedTargetPositionSuggester:
def suggest(self, *, major: str, job_type: str | None, interests: str | None) -> list[dict[str, str]]:
text = f"{major} {interests or ''}".casefold()
if any(token in text for token in ("计算机", "软件", "网络", "data", "人工智能", "ai")):
titles = ["后端工程师", "前端工程师", "测试开发工程师", "数据分析师", "产品经理"]
elif any(token in text for token in ("设计", "视觉", "艺术", "media")):
titles = ["UI/UX 设计师", "视觉设计师", "产品经理", "新媒体运营", "品牌营销专员"]
elif any(token in text for token in ("财务", "会计", "金融", "经济")):
titles = ["财务分析师", "审计助理", "数据分析师", "商业分析师", "产品运营"]
else:
titles = ["产品运营", "项目助理", "数据分析师", "市场专员", "客户成功专员"]
suffix = "实习岗位" if job_type == "internship" else "校招/社招岗位"
return [
{"title": title, "reason": f"结合{major}及已填写方向的{suffix}建议"}
for title in titles
]
class OpenAITargetPositionSuggester:
def __init__(self, completion: OpenAICompatibleStructuredClient) -> None:
self.completion = completion
def suggest(self, *, major: str, job_type: str | None, interests: str | None) -> list[dict[str, str]]:
output = self.completion.complete(
schema=TargetPositionSuggestionOutput,
schema_name="target_position_suggestions",
system_prompt=(
"Recommend 3 to 5 realistic Chinese job titles from the user's major and optional interests. "
"These are exploratory suggestions, not facts about the user. "
"When interests explicitly name a role or domain, put that exact role/domain first and prioritize its direct adjacent roles; "
"do not replace an explicit technical interest such as 后端开发 with unrelated general roles. "
"Do not claim skills, experience, qualifications, or hiring outcomes."
),
payload={"major": major, "job_type": job_type, "interests": interests},
)
return [item.model_dump() for item in output.positions]
class FallbackTargetPositionSuggester:
def __init__(self, primary: TargetPositionSuggester, fallback: TargetPositionSuggester) -> None:
self.primary = primary
self.fallback = fallback
def suggest(self, *, major: str, job_type: str | None, interests: str | None) -> list[dict[str, str]]:
try:
return self.primary.suggest(major=major, job_type=job_type, interests=interests)
except Exception:
return self.fallback.suggest(major=major, job_type=job_type, interests=interests)
def build_target_position_suggester(settings: Settings, client: Any | None = None) -> TargetPositionSuggester:
rules = RuleBasedTargetPositionSuggester()
if not settings.use_openai:
return rules
primary = OpenAITargetPositionSuggester(OpenAICompatibleStructuredClient(settings, client))
return FallbackTargetPositionSuggester(primary, rules) if settings.fallback_to_rules else primary