Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -13,6 +13,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 13 |
from pydantic import BaseModel
|
| 14 |
from dotenv import load_dotenv
|
| 15 |
from datetime import datetime
|
|
|
|
|
|
|
| 16 |
from services.kb_creation import ACTION_SYNONYMS, MODULE_VOCAB
|
| 17 |
from services.kb_creation import (
|
| 18 |
collection,
|
|
@@ -144,6 +146,20 @@ ERROR_FAMILY_SYNS = {
|
|
| 144 |
),
|
| 145 |
}
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
def _detect_error_families(msg: str) -> list:
|
| 149 |
"""Return matching error family names found in the message (generic across SOPs)."""
|
|
@@ -936,23 +952,24 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 936 |
"pre-requirements", "requirements"
|
| 937 |
)
|
| 938 |
if detected_intent == "neutral" and any(h in sec_title for h in PREREQ_HEADINGS):
|
| 939 |
-
detected_intent = "prereqs"
|
| 940 |
-
|
|
|
|
| 941 |
STEPS_TERMS = ("how to", "procedure", "perform", "steps", "do", "navigate")
|
| 942 |
-
ACTIONS_PRESENT = any(s in msg_low for syns in
|
| 943 |
mod_tags = ((top_meta or {}).get("module_tags") or "").lower()
|
| 944 |
-
|
| 945 |
-
# Flatten vocabulary terms for module detection (appointments, picking, shipping, inventory...)
|
| 946 |
MODULE_TOKENS = tuple({term for syns in MODULE_VOCAB.values() for term in syns})
|
| 947 |
looks_like_module = (
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
)
|
| 952 |
looks_like_steps_query = any(t in msg_low for t in STEPS_TERMS) or ACTIONS_PRESENT
|
| 953 |
|
| 954 |
if detected_intent in ("neutral", "prereqs") and looks_like_steps_query and looks_like_module:
|
| 955 |
-
|
|
|
|
| 956 |
# --- Meaning-aware SOP gating ---
|
| 957 |
def _contains_any(s: str, keywords: tuple) -> bool:
|
| 958 |
low = (s or "").lower()
|
|
@@ -984,7 +1001,6 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 984 |
weak_domain_only = (mentions_domain and not has_any_action_or_error)
|
| 985 |
low_context_hit = (matched_count < 2 and filter_mode in ("concise", "exact"))
|
| 986 |
|
| 987 |
-
# Bypass gate when strong steps signals are present for Receiving module
|
| 988 |
strong_steps_bypass = looks_like_steps_query and looks_like_module
|
| 989 |
strong_error_signal = len(_detect_error_families(msg_low)) > 0
|
| 990 |
if (weak_domain_only or (low_context_hit and not combined_ok)) \
|
|
@@ -1023,6 +1039,26 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 1023 |
next_step_applied = False
|
| 1024 |
next_step_info: Dict[str, Any] = {}
|
| 1025 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1026 |
if best_doc:
|
| 1027 |
if detected_intent == "steps":
|
| 1028 |
full_steps = get_best_steps_section_text(best_doc)
|
|
@@ -1032,6 +1068,10 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 1032 |
full_steps = get_section_text(best_doc, sec)
|
| 1033 |
|
| 1034 |
if full_steps:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
# Use numbered form only for matching; keep raw for full output
|
| 1036 |
numbered_full = _ensure_numbering(full_steps)
|
| 1037 |
next_only = _resolve_next_steps(input_data.user_message, numbered_full, max_next=6, min_score=0.35)
|
|
@@ -1049,8 +1089,7 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 1049 |
next_step_info = {"count": len(next_only)}
|
| 1050 |
context_preformatted = True
|
| 1051 |
else:
|
| 1052 |
-
# Normal mode: return the full SOP section (raw),
|
| 1053 |
-
# and we'll number it below once.
|
| 1054 |
context = full_steps
|
| 1055 |
context_preformatted = False
|
| 1056 |
|
|
@@ -1075,23 +1114,32 @@ async def chat_with_ai(input_data: ChatInput):
|
|
| 1075 |
)
|
| 1076 |
escalation_line = _extract_escalation_line(full_errors)
|
| 1077 |
else:
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
context = full_steps
|
| 1082 |
detected_intent = "steps"
|
| 1083 |
context_preformatted = False
|
|
|
|
| 1084 |
elif detected_intent == "prereqs":
|
| 1085 |
full_prereqs = _find_prereq_section_text(best_doc)
|
| 1086 |
if full_prereqs:
|
| 1087 |
context = full_prereqs.strip()
|
| 1088 |
else:
|
| 1089 |
-
|
| 1090 |
full_steps = get_best_steps_section_text(best_doc) or get_section_text(best_doc, sec_title or "")
|
| 1091 |
if full_steps:
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
language_hint = _detect_language_hint(input_data.user_message)
|
| 1096 |
lang_line = f"Respond in {language_hint}." if language_hint else "Respond in a clear, polite tone."
|
| 1097 |
use_gemini = (detected_intent == "errors")
|
|
|
|
| 13 |
from pydantic import BaseModel
|
| 14 |
from dotenv import load_dotenv
|
| 15 |
from datetime import datetime
|
| 16 |
+
|
| 17 |
+
# Import shared vocab from KB services
|
| 18 |
from services.kb_creation import ACTION_SYNONYMS, MODULE_VOCAB
|
| 19 |
from services.kb_creation import (
|
| 20 |
collection,
|
|
|
|
| 146 |
),
|
| 147 |
}
|
| 148 |
|
| 149 |
+
# ----- local extension so runtime filtering is precise even without re-ingest -----
|
| 150 |
+
# (Does NOT override your KB synonyms—just augments them at runtime.)
|
| 151 |
+
ACTION_SYNONYMS_EXT: Dict[str, List[str]] = {}
|
| 152 |
+
for k, v in ACTION_SYNONYMS.items():
|
| 153 |
+
ACTION_SYNONYMS_EXT[k] = list(v) # copy
|
| 154 |
+
|
| 155 |
+
# Extend with SOP phrasing (appointments often say 'updation', 'deletion', 'reschedule')
|
| 156 |
+
ACTION_SYNONYMS_EXT.setdefault("create", []).extend(["appointment creation", "create appointment"])
|
| 157 |
+
ACTION_SYNONYMS_EXT.setdefault("update", []).extend([
|
| 158 |
+
"updation", "reschedule", "change time", "change date", "change slot",
|
| 159 |
+
"update time", "update date", "update slot", "update appointment", "edit appointment"
|
| 160 |
+
])
|
| 161 |
+
ACTION_SYNONYMS_EXT.setdefault("delete", []).extend(["deletion", "delete appointment", "cancel appointment"])
|
| 162 |
+
|
| 163 |
|
| 164 |
def _detect_error_families(msg: str) -> list:
|
| 165 |
"""Return matching error family names found in the message (generic across SOPs)."""
|
|
|
|
| 952 |
"pre-requirements", "requirements"
|
| 953 |
)
|
| 954 |
if detected_intent == "neutral" and any(h in sec_title for h in PREREQ_HEADINGS):
|
| 955 |
+
detected_intent = "prereqs"
|
| 956 |
+
|
| 957 |
+
# --- Module-aware steps nudge (appointments, picking, shipping, etc.) ---
|
| 958 |
STEPS_TERMS = ("how to", "procedure", "perform", "steps", "do", "navigate")
|
| 959 |
+
ACTIONS_PRESENT = any(s in msg_low for syns in ACTION_SYNONYMS_EXT.values() for s in syns)
|
| 960 |
mod_tags = ((top_meta or {}).get("module_tags") or "").lower()
|
| 961 |
+
|
|
|
|
| 962 |
MODULE_TOKENS = tuple({term for syns in MODULE_VOCAB.values() for term in syns})
|
| 963 |
looks_like_module = (
|
| 964 |
+
any(t in msg_low for t in MODULE_TOKENS)
|
| 965 |
+
or any(m in mod_tags for m in ["appointments", "receiving", "picking", "putaway", "shipping", "inventory", "replenishment"])
|
| 966 |
+
or any(m in sec_title for m in ["appointment", "appointments", "schedule", "dock", "door", "picking", "putaway", "shipping", "inventory"])
|
| 967 |
+
)
|
| 968 |
looks_like_steps_query = any(t in msg_low for t in STEPS_TERMS) or ACTIONS_PRESENT
|
| 969 |
|
| 970 |
if detected_intent in ("neutral", "prereqs") and looks_like_steps_query and looks_like_module:
|
| 971 |
+
detected_intent = "steps"
|
| 972 |
+
|
| 973 |
# --- Meaning-aware SOP gating ---
|
| 974 |
def _contains_any(s: str, keywords: tuple) -> bool:
|
| 975 |
low = (s or "").lower()
|
|
|
|
| 1001 |
weak_domain_only = (mentions_domain and not has_any_action_or_error)
|
| 1002 |
low_context_hit = (matched_count < 2 and filter_mode in ("concise", "exact"))
|
| 1003 |
|
|
|
|
| 1004 |
strong_steps_bypass = looks_like_steps_query and looks_like_module
|
| 1005 |
strong_error_signal = len(_detect_error_families(msg_low)) > 0
|
| 1006 |
if (weak_domain_only or (low_context_hit and not combined_ok)) \
|
|
|
|
| 1039 |
next_step_applied = False
|
| 1040 |
next_step_info: Dict[str, Any] = {}
|
| 1041 |
|
| 1042 |
+
# Helper: detect asked action from query using extended synonyms
|
| 1043 |
+
def _detect_action_from_query(q: str) -> Optional[str]:
|
| 1044 |
+
qlow = (q or "").lower()
|
| 1045 |
+
for act, syns in ACTION_SYNONYMS_EXT.items():
|
| 1046 |
+
if any(s in qlow for s in syns):
|
| 1047 |
+
return act
|
| 1048 |
+
return None
|
| 1049 |
+
|
| 1050 |
+
# Helper: filter lines in a steps context to keep only the asked action
|
| 1051 |
+
def _filter_steps_by_action(raw_context: str, asked_act: Optional[str]) -> str:
|
| 1052 |
+
if not asked_act or not raw_context.strip():
|
| 1053 |
+
return raw_context
|
| 1054 |
+
other_terms: List[str] = []
|
| 1055 |
+
for act, syns in ACTION_SYNONYMS_EXT.items():
|
| 1056 |
+
if act != asked_act:
|
| 1057 |
+
other_terms.extend(syns)
|
| 1058 |
+
lines = _normalize_lines(raw_context)
|
| 1059 |
+
kept = [ln for ln in lines if not any(t in ln.lower() for t in other_terms)]
|
| 1060 |
+
return "\n".join(kept).strip() if kept else raw_context
|
| 1061 |
+
|
| 1062 |
if best_doc:
|
| 1063 |
if detected_intent == "steps":
|
| 1064 |
full_steps = get_best_steps_section_text(best_doc)
|
|
|
|
| 1068 |
full_steps = get_section_text(best_doc, sec)
|
| 1069 |
|
| 1070 |
if full_steps:
|
| 1071 |
+
# Apply action-focused filtering FIRST (to avoid mixing create/update/delete)
|
| 1072 |
+
asked_action = _detect_action_from_query(input_data.user_message)
|
| 1073 |
+
full_steps = _filter_steps_by_action(full_steps, asked_action)
|
| 1074 |
+
|
| 1075 |
# Use numbered form only for matching; keep raw for full output
|
| 1076 |
numbered_full = _ensure_numbering(full_steps)
|
| 1077 |
next_only = _resolve_next_steps(input_data.user_message, numbered_full, max_next=6, min_score=0.35)
|
|
|
|
| 1089 |
next_step_info = {"count": len(next_only)}
|
| 1090 |
context_preformatted = True
|
| 1091 |
else:
|
| 1092 |
+
# Normal mode: return the full SOP section (raw), and we'll number it below once.
|
|
|
|
| 1093 |
context = full_steps
|
| 1094 |
context_preformatted = False
|
| 1095 |
|
|
|
|
| 1114 |
)
|
| 1115 |
escalation_line = _extract_escalation_line(full_errors)
|
| 1116 |
else:
|
| 1117 |
+
# Fallback when Errors section is missing: show steps
|
| 1118 |
+
full_steps = get_best_steps_section_text(best_doc) or get_section_text(best_doc, sec_title or "")
|
| 1119 |
+
if full_steps:
|
| 1120 |
+
# Apply action filter to keep responses tight
|
| 1121 |
+
asked_action = _detect_action_from_query(input_data.user_message)
|
| 1122 |
+
full_steps = _filter_steps_by_action(full_steps, asked_action)
|
| 1123 |
+
|
| 1124 |
context = full_steps
|
| 1125 |
detected_intent = "steps"
|
| 1126 |
context_preformatted = False
|
| 1127 |
+
|
| 1128 |
elif detected_intent == "prereqs":
|
| 1129 |
full_prereqs = _find_prereq_section_text(best_doc)
|
| 1130 |
if full_prereqs:
|
| 1131 |
context = full_prereqs.strip()
|
| 1132 |
else:
|
| 1133 |
+
# Fallback when Prereqs section is missing: show steps
|
| 1134 |
full_steps = get_best_steps_section_text(best_doc) or get_section_text(best_doc, sec_title or "")
|
| 1135 |
if full_steps:
|
| 1136 |
+
asked_action = _detect_action_from_query(input_data.user_message)
|
| 1137 |
+
full_steps = _filter_steps_by_action(full_steps, asked_action)
|
| 1138 |
+
|
| 1139 |
+
context = full_steps
|
| 1140 |
+
detected_intent = "steps"
|
| 1141 |
+
context_preformatted = False
|
| 1142 |
+
|
| 1143 |
language_hint = _detect_language_hint(input_data.user_message)
|
| 1144 |
lang_line = f"Respond in {language_hint}." if language_hint else "Respond in a clear, polite tone."
|
| 1145 |
use_gemini = (detected_intent == "errors")
|