| 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 advice",
|
| "is it legal",
|
| "employment law",
|
| "labor law",
|
| "labour law",
|
| "discrimination",
|
| "fire employee",
|
| "terminate employee",
|
| "lawsuit",
|
| "contract",
|
|
|
|
|
| "write interview questions",
|
| "interview questions",
|
| "salary",
|
| "compensation",
|
| "negotiate offer",
|
| "job description template",
|
| "write a job description",
|
| "resume screening",
|
| "cv screening",
|
| "cover letter",
|
|
|
|
|
| "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
|
|
|
|
|
| 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
|
|
|
|
|
| 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
|
|
|
|
|
| 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
|
|
|
|
|
| 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,
|
| }
|
|
|
|
|
|
|
| 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,
|
| } |