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

1"""Use Gemini LLM to rate how well scraped jobs match user qualifications.""" 

2 

3from sqlalchemy.orm import Session 

4 

5from app.job_rating.chatgpt import openai_query 

6from app.job_rating.claude import claude_query 

7from app.job_rating.models import AiSystemPrompt, AiJobPromptTemplate 

8 

9 

10# -------------------------------------------------------- V1 --------------------------------------------------------- 

11 

12 

13SYSTEM_PROMPT_V1 = """ 

14 You are a career–job matching agent. 

15  

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. 

18  

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

24  

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 

31  

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 

37  

38 Output: 

39 Return ONLY valid JSON matching this exact schema: 

40  

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

49 

50 

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} 

57 

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

63 

64# -------------------------------------------------------- V2 --------------------------------------------------------- 

65 

66 

67SYSTEM_PROMPT_V2 = """ 

68You are a career–job matching agent. 

69Evaluate how well a candidate matches a specific job across the dimensions below. 

70 

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 

76 

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. 

81 

82Field–dimension mapping: 

83- Experience -> experience_alignment 

84- Education -> educational_match 

85- Skills -> technical_fit 

86- Interests -> interest_match 

87 

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. 

95 

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 

100 

101Output: 

102Return ONLY valid JSON matching this exact schema: 

103 

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

112 

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

120 

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

126 

127 

128def _or_not_provided(value: str | None) -> str: 

129 """Return "Not provided" if value is None or empty, otherwise strip and return.""" 

130 

131 return value.strip() if value and value.strip() else "Not provided" 

132 

133 

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

150 

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("}}", "}") 

159 

160 

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

173 

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 ) 

179 

180 

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

185 

186 system_prompt = AiSystemPrompt(prompt=SYSTEM_PROMPT_V2) 

187 db.add(system_prompt) 

188 

189 job_template = AiJobPromptTemplate(prompt=JOB_ONLY_PROMPT_TEMPLATE_V2) 

190 db.add(job_template) 

191 

192 db.commit() 

193 db.refresh(system_prompt) 

194 db.refresh(job_template) 

195 

196 return system_prompt, job_template 

197 

198 

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