"""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