Coverage for backend / app / job_rating / prompts.py: 68%
37 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-17 21:34 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-17 21:34 +0000
1"""Use Gemini LLM to rate how well scraped jobs match user qualifications."""
3from sqlalchemy.orm import Session
5from app.job_rating.chatgpt import openai_query
6from app.job_rating.claude import claude_query
7from app.job_rating.models import AiSystemPrompt, AiJobPromptTemplate
10# -------------------------------------------------------- V1 ---------------------------------------------------------
13SYSTEM_PROMPT_V1 = """
14 You are a career–job matching agent.
16 Evaluate how well a candidate matches a specific job across the dimensions below.
17 Score ONLY when the required data is provided; otherwise return null.
19 Scoring dimensions (0–10):
20 - technical_fit: candidate skills vs job requirements (null if Skills = "Not provided")
21 - experience_alignment: relevance of past roles (null if Experience = "Not provided")
22 - educational_match: degree and academic alignment (null if Education = "Not provided")
23 - interest_match: alignment of interests with role/company (null if Interests = "Not provided")
25 overall_score:
26 - A holistic judgement of candidate–job fit
27 - NOT a mathematical average of the other scores
28 - Should be broadly consistent with the available dimension scores
29 - May weight dimensions unevenly based on job importance
30 - If all dimensions are null, set overall_score to null
32 Rules:
33 - 0 = poor fit, null = insufficient information
34 - Consider must-haves, nice-to-haves, and transferable skills
35 - Be objective and evidence-based
36 - Do not invent or infer missing data
38 Output:
39 Return ONLY valid JSON matching this exact schema:
41 {
42 "overall_score": <integer 0–10 or null>,
43 "technical_fit": <integer 0–10 or null>,
44 "experience_alignment": <integer 0–10 or null>,
45 "educational_match": <integer 0–10 or null>,
46 "interest_match": <integer 0–10 or null>,
47 "explanation": "2–3 concise sentences summarising the assessment and noting any missing data"
48 }"""
51JOB_PROMPT_TEMPLATE_V1 = """### Candidate Profile
52- **Experience**: {user_experience_or_not_provided}
53- **Education**: {user_education_or_not_provided}
54- **Skills**: {user_skills_or_not_provided}
55- **Qualities**: {user_qualities_or_not_provided}
56- **Interests**: {user_interests_or_not_provided}
58### Job Details
59- **Title**: {job_title_or_not_provided}
60- **Company**: {job_company_or_not_provided}
61- **Description**: {job_description_or_not_provided}
62"""
64# -------------------------------------------------------- V2 ---------------------------------------------------------
67SYSTEM_PROMPT_V2 = """
68You are a career–job matching agent.
69Evaluate how well a candidate matches a specific job across the dimensions below.
71Scoring dimensions (0–10):
72- technical_fit: candidate skills vs job requirements
73- experience_alignment: relevance of past roles
74- educational_match: degree and academic alignment
75- interest_match: alignment of interests with role/company
77Null handling (very important, follow exactly):
78- For each dimension:
79 - If the *corresponding candidate field string* is exactly "Not provided", you MUST return null for that dimension.
80 - If the corresponding candidate field string is anything else (non-empty), you MUST return an integer score 0–10 for that dimension. Do NOT return null in that case, even if information is limited.
82Field–dimension mapping:
83- Experience -> experience_alignment
84- Education -> educational_match
85- Skills -> technical_fit
86- Interests -> interest_match
88overall_score:
89- A holistic judgement of candidate–job fit
90- NOT a mathematical average of the other scores
91- Should be broadly consistent with the available dimension scores
92- May weight dimensions unevenly based on job importance
93- If at least one dimension has a non-null integer, you MUST output an integer 0–10 (no null allowed).
94- Only if all four dimensions are null, set overall_score to null.
96Rules:
97- Consider must-haves, nice-to-haves, and transferable skills
98- Be objective and evidence-based
99- Do not invent or infer missing data
101Output:
102Return ONLY valid JSON matching this exact schema:
104{{
105 "overall_score": <integer 0–10 or null>,
106 "technical_fit": <integer 0–10 or null>,
107 "experience_alignment": <integer 0–10 or null>,
108 "educational_match": <integer 0–10 or null>,
109 "interest_match": <integer 0–10 or null>,
110 "explanation": "2–3 concise sentences summarising the assessment and noting any missing data"
111}}
113### Candidate Profile
114- **Experience**: {user_experience_or_not_provided}
115- **Education**: {user_education_or_not_provided}
116- **Skills**: {user_skills_or_not_provided}
117- **Qualities**: {user_qualities_or_not_provided}
118- **Interests**: {user_interests_or_not_provided}
119"""
121JOB_ONLY_PROMPT_TEMPLATE_V2 = """### Job Details
122- **Title**: {job_title_or_not_provided}
123- **Company**: {job_company_or_not_provided}
124- **Description**: {job_description_or_not_provided}
125"""
128def _or_not_provided(value: str | None) -> str:
129 """Return "Not provided" if value is None or empty, otherwise strip and return."""
131 return value.strip() if value and value.strip() else "Not provided"
134def create_system_prompt_with_profile(
135 prompt_template: str,
136 user_experience: str | None,
137 user_education: str | None,
138 user_skills: str | None,
139 user_qualities: str | None,
140 user_interests: str | None,
141) -> str:
142 """Build a system prompt with the candidate profile embedded.
143 :param prompt_template: System prompt template with candidate profile placeholders.
144 :param user_experience: User's experience description
145 :param user_education: User's education description
146 :param user_skills: User's skills description
147 :param user_qualities: User's qualities description
148 :param user_interests: User's interests description
149 :return: System prompt string with candidate profile filled in."""
151 prompt = prompt_template.format(
152 user_experience_or_not_provided=_or_not_provided(user_experience),
153 user_education_or_not_provided=_or_not_provided(user_education),
154 user_skills_or_not_provided=_or_not_provided(user_skills),
155 user_qualities_or_not_provided=_or_not_provided(user_qualities),
156 user_interests_or_not_provided=_or_not_provided(user_interests),
157 )
158 return prompt.replace("{{", "{").replace("}}", "}")
161def create_job_only_prompt(
162 prompt_template: str,
163 job_title: str | None,
164 job_company: str | None,
165 job_description: str | None,
166) -> str:
167 """Build a user message containing only job details.
168 :param prompt_template: Job-only prompt template.
169 :param job_title: Job title
170 :param job_company: Job company
171 :param job_description: Job description
172 :return: Prompt string containing job details only."""
174 return prompt_template.format(
175 job_title_or_not_provided=_or_not_provided(job_title),
176 job_company_or_not_provided=_or_not_provided(job_company),
177 job_description_or_not_provided=_or_not_provided(job_description),
178 )
181def seed_ai_prompts(db: Session) -> tuple[AiSystemPrompt, AiJobPromptTemplate]:
182 """Seed the database with initial AI prompts if they don't exist.
183 :param db: Database session
184 :return: Tuple of (AiSystemPrompt, AiJobPromptTemplate) instances"""
186 system_prompt = AiSystemPrompt(prompt=SYSTEM_PROMPT_V2)
187 db.add(system_prompt)
189 job_template = AiJobPromptTemplate(prompt=JOB_ONLY_PROMPT_TEMPLATE_V2)
190 db.add(job_template)
192 db.commit()
193 db.refresh(system_prompt)
194 db.refresh(job_template)
196 return system_prompt, job_template
199if __name__ == "__main__":
200 title = "Software Engineer"
201 company = "Tech Corp"
202 description = "We are looking for a Software Engineer with experience in Python and web development."
203 experience = "3 years as a backend developer using Python and Django."
204 education = "Bachelor's degree in Computer Science."
205 skills = "Python, Django, REST APIs, SQL"
206 qualities = "Team player, problem solver, quick learner"
207 interests = "Not interested in software engineer roles"
208 user_system_prompt = create_system_prompt_with_profile(
209 SYSTEM_PROMPT_V2,
210 experience,
211 education,
212 skills,
213 qualities,
214 interests,
215 )
216 job_prompt = create_job_only_prompt(JOB_ONLY_PROMPT_TEMPLATE_V2, title, company, description)
217 print(openai_query(SYSTEM_PROMPT_V1, user_system_prompt + "\n" + job_prompt))
218 print(claude_query(user_system_prompt, job_prompt))