generated from kgod/ai-review-template
81 lines
3.9 KiB
Python
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 |