shl-recommender / app /Planner.py
devpatel1012's picture
Initial commit of SHL Assessment Recommender
ca20ec1
Raw
History Blame Contribute Delete
4.6 kB
"""Stage 2 of the pipeline: pure Python decision logic. No LLM call happens here.
This is the core fix for the 'rigid' / hallucinated behavior you were seeing: the
old single prompt asked a 3B model to *decide* β€” in the same breath as writing
prose β€” whether it had enough info, whether the user was confirming, whether a
question was a comparison, etc. Small models are unreliable at that kind of
in-context judgment call. Here, the judgment call is a plain if/else over facts
the NLU stage already extracted, so it's 100% consistent and free.
"""
from typing import List, Dict, Any
class Action:
def __init__(self, kind: str, **kwargs):
self.kind = kind
self.__dict__.update(kwargs)
def _to_dict(rec) -> dict:
if hasattr(rec, "model_dump"):
return rec.model_dump()
if hasattr(rec, "dict"):
return rec.dict()
return rec
def find_last_recommendations(conversation) -> List[Dict[str, Any]]:
"""Scans the raw conversation (newest first) for the last Agent turn that
carried a non-empty recommendations list. Requires the frontend to echo
`recommendations` back on Agent turns (see schemas.Turn / main.py) β€” this is
what lets a 'confirmation' turn reuse the REAL prior shortlist instead of the
model having to recall or reconstruct it from plain text."""
for turn in reversed(conversation):
if turn.role.lower() == "agent" and getattr(turn, "recommendations", None):
return [_to_dict(r) for r in turn.recommendations]
return []
def _last_agent_message(conversation):
for turn in reversed(conversation):
if turn.role.lower() == "agent":
return turn.content.strip()
return None
def decide(state: dict, conversation) -> Action:
if not state.get("in_scope", True):
return Action("off_topic")
intent = state.get("intent", "new_request")
prior_recs = find_last_recommendations(conversation)
if intent == "off_topic":
return Action("off_topic")
if intent == "comparison_question":
return Action("compare", compared_tests=state.get("compared_tests", []),
topic_keywords=state.get("compared_tests", []) or state.get("topic_keywords", []))
if intent == "confirmation":
if prior_recs:
return Action("close", reuse_recommendations=prior_recs)
# Nothing to confirm yet (e.g. "thanks" mid-clarification) β€” keep going.
intent = "new_request"
if intent == "pushback_feedback":
return Action(
"redirect",
role_summary=state.get("role_summary"),
purpose=state.get("purpose"),
topic_keywords=state.get("topic_keywords", []),
prior_recommendations=prior_recs,
)
# --- Smart Update Detection ---
# If we already have prior recommendations, any new requirement is an update to
# the existing list, even if the NLU missed the strict 'addition_removal' intent.
updating_flag = False
if intent == "addition_removal":
updating_flag = True
elif intent in ["new_request", "clarifying_answer"] and len(prior_recs) > 0:
updating_flag = True
# --- THE AMNESIA & LOOP FIX ---
# If we already have a shortlist from the conversation history, we know we are ready to recommend.
# This overrides the NLU if it "forgets" the role summary mid-conversation.
if updating_flag and prior_recs:
ready = True
elif intent == "clarifying_answer":
# If the user just gave us more info, stop asking and just recommend!
ready = True
else:
ready = bool(state.get("ready_to_recommend")) and bool(state.get("role_summary"))
candidate_question = (
state.get("missing_info_question")
or "Could you tell me more about the role and the purpose of this assessment?"
)
# LOOP-BREAKER: if the question we're about to ask is the same one already
# asked last turn, the NLU stage isn't making progress on it.
# Proceed with whatever is known instead of asking a third time.
if not ready:
last_agent_msg = _last_agent_message(conversation)
if last_agent_msg and candidate_question.strip().lower() == last_agent_msg.strip().lower():
ready = bool(state.get("role_summary")) # proceed if we at least know who this is for
if ready:
return Action("recommend", updating=updating_flag, prior_recommendations=prior_recs,
topic_keywords=state.get("topic_keywords", []))
return Action("ask_question", question=candidate_question)