Spaces:
Sleeping
Sleeping
| """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) |