File size: 5,324 Bytes
be97847 87149fa be97847 87149fa 0ea997a 87149fa 0ea997a 87149fa be97847 87149fa be97847 87149fa 0ea997a 87149fa 0ea997a 87149fa 0ea997a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | """Semantic router: three answer tiers plus the four skills.
Queries are compared against the centroid of a few reference examples per
route. Routing is deliberately asymmetric: sending a real question to a route
without the profile produces a confident answer about the candidate with no
evidence behind it, while sending a greeting to the full profile merely wastes
tokens. So anything short of a clear, confident match falls back to FULL.
The skill routes use the same rule: a request must clearly ask for a skill to
get one, otherwise it is answered as ordinary chat. The route names equal the
skill keys in skills.py.
"""
import logging
import numpy as np
logger = logging.getLogger(__name__)
TRIVIAL = "trivial"
SPECIFIC = "specific"
FULL = "full"
# Skill routes. Names match the keys in skills.SKILLS.
REVIEW = "review"
ATS = "ats"
COVER_LETTER = "cover_letter"
EMAIL = "email"
SKILL_ROUTES = (REVIEW, ATS, COVER_LETTER, EMAIL)
REFERENCE_QUERIES = {
TRIVIAL: [
"hello",
"hi there",
"hey",
"how are you?",
"thanks!",
"thank you",
"good morning",
"good evening",
"who are you?",
"what can you do?",
"bye",
],
SPECIFIC: [
"do I know Rust?",
"what is my Python experience?",
"have I used Kubernetes?",
"which projects used FAISS?",
"tell me about my volunteer work",
"what certifications do I have?",
"where did I use Docker?",
"what did I do at my last job?",
"which languages do I know?",
],
FULL: [
"am I a fit for this role?",
"what are my weakest areas?",
"how should I position myself?",
"summarise my background",
"what should I emphasise in the interview?",
"what gaps do I have for this job?",
"what do I bring to the table?",
"how do I compare to this job description?",
"what will they push back on?",
],
REVIEW: [
"review my resume",
"critique my resume for this job",
"how can I improve my resume?",
"give me feedback on my resume",
"tear down my resume against this job description",
"which resume bullets are weak?",
"rewrite my weakest resume bullets",
],
ATS: [
"what is my ATS score?",
"run an ATS check",
"which keywords am I missing for this job?",
"will my resume pass the applicant tracking system?",
"compare my resume keywords to the job description",
"how well do my keywords match this posting?",
],
COVER_LETTER: [
"write me a cover letter",
"draft a cover letter for this role",
"I need a cover letter for this job",
"can you write my cover letter?",
"cover letter for this position",
],
EMAIL: [
"write an application email",
"draft an email to the recruiter for this job",
"write a short email applying for this role",
"help me email the hiring manager",
"give me subject lines and an email to apply",
],
}
# Tuned against the reference set: correct routes score 0.38-0.60 with margins
# above 0.25, so these thresholds reject genuinely ambiguous queries only.
MIN_SCORE = 0.35
MIN_MARGIN = 0.10
class SemanticRouter:
def __init__(self, embed_fn, references=None, min_score=MIN_SCORE, min_margin=MIN_MARGIN):
self.min_score = min_score
self.min_margin = min_margin
references = references or REFERENCE_QUERIES
self.routes = list(references)
centroids = np.stack([embed_fn(references[route]).mean(axis=0) for route in self.routes])
norms = np.linalg.norm(centroids, axis=1, keepdims=True)
self.centroids = centroids / np.where(norms == 0, 1, norms)
self.embed_fn = embed_fn
def scores(self, query) -> dict:
vector = self.embed_fn([query])[0]
norm = np.linalg.norm(vector)
if norm:
vector = vector / norm
return dict(zip(self.routes, (self.centroids @ vector).tolist()))
def route(self, query) -> str:
# Defensive: an unroutable query must never crash the turn.
if not isinstance(query, str) or not query.strip():
logger.info("Route %s | unroutable query, not scored", FULL)
return FULL
scores = self.scores(query)
ranked = sorted(scores.items(), key=lambda item: item[1], reverse=True)
(winner, top), (runner_up_route, runner_up) = ranked[0], ranked[1]
margin = top - runner_up
# Leaving the profile behind requires confidence; FULL is the safe default.
if winner == FULL:
chosen, why = FULL, "top match"
elif top < self.min_score:
chosen, why = FULL, f"{winner} below min_score {self.min_score}"
elif margin < self.min_margin:
chosen, why = FULL, f"{winner} over {runner_up_route} below min_margin {self.min_margin}"
else:
chosen, why = winner, "top match"
logger.info(
"Route %s | %s | margin %.3f | %s | query: %.60r",
chosen,
why,
margin,
" ".join(f"{route}={score:.3f}" for route, score in ranked),
query,
)
return chosen
|