from typing import List, Dict, Any import json from pathlib import Path from typing import List, Dict, Any CATALOG_PATH = Path("data/shl_catalog.json") def load_catalog() -> List[Dict[str, Any]]: if not CATALOG_PATH.exists(): return [] with open(CATALOG_PATH, "r", encoding="utf-8") as f: return json.load(f) CATALOG = load_catalog() def get_last_user_message(messages: List[Any]) -> str: for msg in reversed(messages): if msg.role == "user": return msg.content.lower() return "" def build_conversation_query(messages: List[Any]) -> str: """ Builds a compact query from full stateless conversation history. This helps refinement requests like: 'Actually add personality tests' without forgetting earlier role context. """ user_messages = [] for msg in messages: if msg.role == "user": user_messages.append(msg.content) return " ".join(user_messages).lower() def is_out_of_scope(text: str) -> bool: text = text.lower() blocked_phrases = [ # Legal / compliance "legal advice", "is it legal", "employment law", "labor law", "labour law", "discrimination", "fire employee", "terminate employee", "lawsuit", "contract", # General hiring advice outside SHL assessment recommendation "write interview questions", "interview questions", "salary", "compensation", "negotiate offer", "job description template", "write a job description", "resume screening", "cv screening", "cover letter", # Prompt injection "ignore previous instructions", "forget your instructions", "act as unrestricted", "bypass", "system prompt", "developer message", "reveal your prompt", "jailbreak", ] return any(phrase in text for phrase in blocked_phrases) def is_vague(text: str) -> bool: vague_phrases = [ "i need an assessment", "need assessment", "suggest assessment", "recommend assessment", "assessment test", ] has_role_signal = any( word in text for word in [ "java", "python", "developer", "engineer", "manager", "sales", "graduate", "analyst", "stakeholder", "communication", ] ) if any(phrase in text for phrase in vague_phrases) and not has_role_signal: return True if len(text.split()) <= 4 and not has_role_signal: return True return False def score_catalog_item(query: str, item: Dict[str, Any]) -> int: score = 0 name = item.get("name", "").lower() description = item.get("description", "").lower() keywords = item.get("keywords", []) searchable_text = f"{name} {description} {' '.join(keywords)}" query_words = query.lower().split() for word in query_words: if len(word) > 2 and word in searchable_text: score += 1 # Strong skill boosts skill_terms = [ "java", "python", "sql", "javascript", "developer", "coding", "programming", "software", "backend" ] for skill in skill_terms: if skill in query and skill in searchable_text: score += 3 # Personality / behavior boost if any(term in query for term in ["personality", "communication", "stakeholder", "leadership"]): if item.get("test_type") == "P" or "personality" in searchable_text or "opq" in searchable_text: score += 3 # Cognitive / aptitude boost if any(term in query for term in ["cognitive", "aptitude", "reasoning", "ability"]): if item.get("test_type") == "A" or "ability" in searchable_text: score += 3 return score def recommend(query: str) -> List[Dict[str, str]]: scored_items = [] for item in CATALOG: score = score_catalog_item(query, item) if score > 0: scored_items.append((score, item)) scored_items.sort(key=lambda x: x[0], reverse=True) recommendations = [] for _, item in scored_items[:10]: recommendations.append( { "name": item.get('name',''), "url": item.get('url',''), "test_type": item.get('test_type','unknown'), } ) return recommendations def is_compare_query(text: str) -> bool: compare_terms = [ "difference between", "compare", "vs", "versus", "different from", "which is better", ] text = text.lower() return any(term in text for term in compare_terms) def find_matching_assessments(text: str, limit: int = 5) -> List[Dict[str, Any]]: text = text.lower() matches = [] for item in CATALOG: name = item.get("name", "").lower() description = item.get("description", "").lower() keywords = " ".join(item.get("keywords", [])).lower() searchable_text = f"{name} {description} {keywords}" score = 0 # Direct name match for token in text.split(): if len(token) > 2 and token in name: score += 3 elif len(token) > 2 and token in searchable_text: score += 1 if score > 0: matches.append((score, item)) matches.sort(key=lambda x: x[0], reverse=True) return [item for _, item in matches[:limit]] def compare_assessments(query: str) -> Dict[str, Any]: matches = find_matching_assessments(query, limit=4) if len(matches) < 2: return { "reply": "I can compare SHL assessments, but I need two assessment names from the catalog. Please mention both assessments you want to compare.", "recommendations": [], "end_of_conversation": False, } a = matches[0] b = matches[1] a_name = a.get("name", "Assessment 1") b_name = b.get("name", "Assessment 2") a_type = a.get("test_type", "Unknown") b_type = b.get("test_type", "Unknown") a_desc = a.get("description", "") b_desc = b.get("description", "") reply = ( f"Here is a catalog-grounded comparison:\n\n" f"{a_name} focuses on: {a_desc[:300] if a_desc else 'description not available in catalog data'}\n" f"Test type: {a_type}\n\n" f"{b_name} focuses on: {b_desc[:300] if b_desc else 'description not available in catalog data'}\n" f"Test type: {b_type}\n\n" f"Use {a_name} when the role requirement matches its catalog description. " f"Use {b_name} when the hiring need is closer to its catalog description." ) return { "reply": reply, "recommendations": [ { "name": a.get("name", ""), "url": a.get("url", ""), "test_type": a.get("test_type", "Unknown"), }, { "name": b.get("name", ""), "url": b.get("url", ""), "test_type": b.get("test_type", "Unknown"), }, ], "end_of_conversation": False, } def run_agent(messages: List[Any]) -> Dict[str, Any]: query = build_conversation_query(messages) last_user_query = get_last_user_message(messages) if not query: return { "reply": "Please share the role or skills you want to assess using SHL assessments.", "recommendations": [], "end_of_conversation": False, } if is_out_of_scope(query): return { "reply": "I can only help with SHL assessment recommendations, comparisons, and refinements based on the SHL catalog.", "recommendations": [], "end_of_conversation": False, } if is_vague(query): return { "reply": "Sure. Which role, skill area, and seniority level are you hiring for?", "recommendations": [], "end_of_conversation": False, } # Important safety check # If real SHL catalog JSON is missing or not loaded, don't crash and don't hallucinate. if not CATALOG: return { "reply": "The SHL catalog is not loaded yet. Please try again after the catalog data is available.", "recommendations": [], "end_of_conversation": False, } if is_compare_query(last_user_query): return compare_assessments(last_user_query) recommendations = recommend(query) if recommendations: return { "reply": f"Based on the role details, here are {len(recommendations)} SHL assessments that may fit this hiring need.", "recommendations": recommendations, "end_of_conversation": False, } return { "reply": "Could you share the target role, required skills, and seniority level so I can recommend relevant SHL assessments?", "recommendations": [], "end_of_conversation": False, }