Spaces:
Sleeping
Sleeping
File size: 17,807 Bytes
210d88d | 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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | # slm_inference.py
import re, requests
from typing import List, Dict, Any, Optional
API_URL = 'https://vishwasmsme-timetable-slm-api.hf.space'
DAY_WORDS = {
'monday':'MON','tuesday':'TUE','wednesday':'WED',
'thursday':'THU','friday':'FRI','saturday':'SAT',
'mon':'MON','tue':'TUE','wed':'WED',
'thu':'THU','fri':'FRI','sat':'SAT',
}
def get_constraint(instruction):
try:
r = requests.post(f'{API_URL}/constraint',json={'instruction':instruction},timeout=60)
r.raise_for_status(); return r.json()
except Exception as e:
return {'success':False,'error':str(e),'constraints':[]}
def get_constraints_batch(instructions):
try:
r = requests.post(f'{API_URL}/constraints/batch',json={'instructions':instructions},timeout=120)
r.raise_for_status(); return r.json().get('all_constraints',[])
except Exception as e:
print(f'API Error: {e}'); return []
def check_api_health():
try:
return requests.get(f'{API_URL}/health',timeout=10).json().get('model_loaded',False)
except: return False
def smart_parse(prompt, faculties_data, subjects_data=None, sections_data=None):
p = prompt.lower().strip()
fac_map = _build_fac_map(faculties_data)
sub_map = _build_sub_map(subjects_data or [])
days = _extract_all_days(p)
period = _extract_period(p)
# NO FREE PERIOD β "first hour should not be free", "period 1 must have a class"
no_free_kw = ['should not be free', 'must not be free', 'cannot be free',
'should always be occupied', 'always be occupied', 'always occupied',
'no free slot in', 'free slot in period', 'no free in',
'must have a class', 'should have a class', 'always occupied',
'no free period', 'no free slot', 'must be filled',
'first hour should not', 'last period should not']
if any(kw in p for kw in no_free_kw):
# Detect which period(s) are required
periods = []
if any(w in p for w in ['first hour', 'period 1', 'p1', 'first period', 'slot 1']):
periods = [0] # 0-indexed: P1
elif any(w in p for w in ['last hour', 'last period', 'last slot']):
periods = [-1] # will resolve to last period dynamically
elif any(w in p for w in ['second period', 'period 2', 'p2']):
periods = [1]
elif any(w in p for w in ['third period', 'period 3', 'p3']):
periods = [2]
else:
# Try to extract any period number mentioned
nums = re.findall(r'period\s*(\d+)|p\s*(\d+)|slot\s*(\d+)', p)
for g in nums:
n = next((int(x) for x in g if x), None)
if n: periods.append(n - 1)
if not periods:
periods = [0] # default: first period
return {
'type': 'NO_FREE_PERIOD',
'periods': periods,
'section_id': _extract_section(p, sections_data),
}
# FACULTY SUBSTITUTION
if any(kw in p for kw in ['replace','substitute','take over','will take','on leave',
'covering','cover for','instead of','will cover','deputed']):
facs = _find_two_faculties(p, fac_map)
if facs:
return {'type':'FACULTY_SUBSTITUTION','from_faculty_id':facs[0],
'to_faculty_id':facs[1],'days':days,
'subject_codes':_extract_subjects(p,sub_map)}
# NO SUBJECT ON DAY β "No TOC on Thursday", "No NLP class on Friday for 5A"
no_subj_kw = ['no toc', 'no nlp', 'no ml', 'no cn', 'no sepm', 'no nosql',
'no rmipr', 'no iks', 'no evs', 'no dvlab', 'no cnlab',
'no nlplab', 'no mllab', 'no miniproj', 'no major', 'no genai',
'no devops', 'no hcai', 'no cc ', 'no forum', 'no softskill']
has_no_subj = any(kw in p for kw in no_subj_kw)
# Also catch "no [subject_name] class/on/for"
has_no_class = bool(re.search(r'no\s+\w+\s+(class|on|for)', p))
if has_no_subj or has_no_class:
subs = _extract_subjects(p, sub_map)
if subs and days:
return {
'type': 'CANCEL_CLASS',
'subject_code': subs[0],
'day': days[0],
'section_id': _extract_section(p, sections_data),
'faculty_id': None,
'period': None,
}
# CANCEL / HOLIDAY
holiday_kws = ['holiday','off day','public holiday','is a holiday','college is closed',
'closed on','college closed','no classes on','no classes for all',
'is holiday']
cancel_kws2 = ['cancel','no class','no lecture','cancelled','remove','delete','drop']
is_holiday = any(kw in p for kw in holiday_kws)
is_cancel = any(kw in p for kw in cancel_kws2)
# Whole-day clear: holiday OR (cancel/no classes with NO subject mentioned)
subs_found = _extract_subjects(p, sub_map)
fid_found = _extract_one_faculty(p, fac_map)
whole_day = is_holiday or (is_cancel and not subs_found and not fid_found and not period)
if (is_holiday or is_cancel) and days:
if whole_day:
return {'type':'MARK_HOLIDAY', 'days':days,
'section_id':_extract_section(p, sections_data)}
return {'type':'CANCEL_CLASS', 'day':days[0], 'period':period,
'subject_code': subs_found[0] if subs_found else None,
'section_id': _extract_section(p, sections_data),
'faculty_id': fid_found}
# CHANGE ROOM β detect "to Lab 2", "to R3", "shift all labs to L3"
# Must run BEFORE move/reschedule to prevent "Move NLPLAB to Lab 2" β RESCHEDULE_LAB
room_match = re.search(r'\b(lab\s*\d+|l\d+|r\d+|room\s*\d+)\b', p)
room_val = room_match.group(1).replace(' ','').upper() if room_match else None
room_dest_kws = ['to lab ', 'to l', 'to r', 'to room', 'change room',
'assign room', 'move to lab', 'move to room', 'labs to']
has_room_dest = any(kw in p for kw in room_dest_kws) and room_val
if has_room_dest:
subs_r = _extract_subjects(p, sub_map)
return {'type': 'CHANGE_ROOM',
'new_room_id': room_val,
'room_id': room_val,
'subject_code': subs_r[0] if subs_r else None,
'section_id': _extract_section(p, sections_data),
'day': days[0] if days else None,
'all_labs': 'all' in p and 'lab' in p}
# MOVE / RESCHEDULE β only if NOT a cancel/remove keyword
move_kws = ['move','shift','reschedule','transfer','relocate']
cancel_kws = ['cancel','remove','delete','drop','no class','holiday']
is_move = any(kw in p for kw in move_kws)
is_cancel = any(kw in p for kw in cancel_kws)
if is_move and not is_cancel:
subs = _extract_subjects(p, sub_map)
is_lab = ('lab' in p or (subs and any(s.lower().endswith('lab') for s in subs)))
if is_lab and subs:
# "from X to Y" β days[0]=from, days[1]=to
# "to Y" only β days[0]=to, from_day=None
from_day = days[0] if len(days) >= 2 else None
to_day = days[1] if len(days) >= 2 else (days[0] if days else None)
return {'type':'RESCHEDULE_LAB',
'subject_code': subs[0],
'section_id': _extract_section(p, sections_data),
'from_day': from_day,
'to_day': to_day,
'to_period': period or 1}
if len(days) >= 2:
subs = _extract_subjects(p, sub_map)
return {'type':'MOVE_CLASS',
'from_day': days[0],
'to_day': days[1],
'subject_code': subs[0] if subs else None,
'section_id': _extract_section(p, sections_data),
'faculty_id': _extract_one_faculty(p, fac_map)}
# CHANGE ROOM
if any(kw in p for kw in ['change room','move to room','to lab','to room','assign room']):
rm = re.search(r'\b([rl]\d+|lab\s*\d*|room\s*\d+)\b', p)
return {'type':'CHANGE_ROOM',
'room_id':rm.group(1).replace(' ','') if rm else None,
'subject_code':(_extract_subjects(p,sub_map) or [None])[0],
'section_id':_extract_section(p,sections_data),
'day':days[0] if days else None}
# ADD EXTRA / MAKEUP
# Check for "schedule [subject] session/class" pattern like "Schedule extra ML session"
import re as _re
has_schedule_class = bool(_re.search(
r'(schedule|add)\s+(extra|additional|makeup|compensatory)?\s*\w+\s*(class|session)', p))
extra_keywords = ['extra class','makeup','additional class','make up',
'compensatory','add class','schedule extra','extra session',
'additional session','schedule.*class','add.*session']
if has_schedule_class or any(kw in p for kw in extra_keywords):
subs = _extract_subjects(p, sub_map)
return {'type':'ADD_EXTRA_CLASS',
'subject_code':subs[0] if subs else None,
'section_id':_extract_section(p,sections_data),
'faculty_id':_extract_one_faculty(p,fac_map),
'day':days[0] if days else None,
'period': period or (5 if 'afternoon' in p else
1 if 'morning' in p else None)}
# CHANGE FACULTY (permanent)
perm_kws = ['permanently','all classes','change faculty','hand over',
'assign all','transfer all']
if any(kw in p for kw in perm_kws):
facs = _find_two_faculties(p, fac_map) or []
scodes = _extract_subjects(p, sub_map)
# "Assign all X to Y" / "Transfer A's classes to B" β to_fac = LAST mentioned
# "Change faculty from A to B" β from=A (first), to=B (second)
if len(facs) >= 2:
# "from A to B" order β from=facs[0], to=facs[1]
if 'from' in p and 'to' in p:
from_fac, to_fac = facs[0], facs[1]
else:
# "assign all ... to Syed" β to_fac is the one after "to"
to_idx = p.rfind(' to ')
if to_idx > 0:
after_to = p[to_idx+4:]
to_fac = next((fi for n,fi in fac_map.items() if n and n in after_to), facs[-1])
from_fac = next((fi for fi in facs if fi != to_fac), facs[0])
else:
from_fac, to_fac = facs[0], facs[1]
elif len(facs) == 0:
# Try single faculty extraction
single = _extract_one_faculty(p, fac_map)
from_fac, to_fac = None, single
elif len(facs) == 1:
# Only one faculty mentioned β they're the target (to_fac)
from_fac, to_fac = None, facs[0]
else:
from_fac, to_fac = None, None
if to_fac or (scodes and not facs):
# If no faculty found but subject found, still create constraint
# (partial β will match by subject code only)
return {'type':'CHANGE_FACULTY',
'from_faculty_id': from_fac,
'to_faculty_id': to_fac,
'subject_codes': scodes,
'subject_code': scodes[0] if scodes else None,
'section_id':_extract_section(p,sections_data)}
# SWAP
if any(kw in p for kw in ['swap','exchange']):
import re as _re
periods = _re.findall(r'p(?:eriod)?\s*(\d+)', p)
subs = _extract_subjects(p, sub_map)
# Extract faculty ids in order of appearance in prompt
fac_found = sorted(
[(name, fid, p.index(name)) for name, fid in fac_map.items()
if name and name in p],
key=lambda x: x[2]
)
# Deduplicate by fid keeping first occurrence
seen, fac_ids = set(), []
for _, fid, _ in fac_found:
if fid not in seen:
fac_ids.append(fid); seen.add(fid)
return {'type': 'SWAP_CLASSES',
'day1': days[0] if len(days) > 0 else None,
'day2': days[1] if len(days) > 1 else None,
'period1': int(periods[0]) if len(periods) > 0 else None,
'period2': int(periods[1]) if len(periods) > 1 else None,
'subject_code1': subs[0] if len(subs) > 0 else None,
'subject_code2': subs[1] if len(subs) > 1 else None,
'subject_codes': subs,
'faculty_id1': fac_ids[0] if len(fac_ids) > 0 else None,
'faculty_id2': fac_ids[1] if len(fac_ids) > 1 else None,
'section_id': _extract_section(p, sections_data)}
# FREEZE
if any(kw in p for kw in ['freeze','lock slot','fix this slot','keep this',
'lock ','don\'t move','do not move','should not change',
'it should not change','fix mllab','fix nlplab',
'fix cnlab','fix dvlab']):
subs = _extract_subjects(p, sub_map)
return {'type':'FREEZE_SLOT','day':days[0] if days else None,'period':period,
'subject_code':subs[0] if subs else None,
'section_id':_extract_section(p,sections_data)}
return None
def _build_fac_map(fds):
m = {}
for f in fds:
m[f['name'].lower()] = f['id']
short = re.sub(r'(prof\.|dr\.|mr\.|ms\.)\s*','',f['name'],flags=re.I).strip()
m[short.lower()] = f['id']
m[f['id'].lower()] = f['id']
return m
def _build_sub_map(sds):
m = {}
for s in sds:
m[s['name'].lower()] = s['code']
m[s['code'].lower()] = s['code']
return m
def _extract_all_days(text):
found, seen = [], set()
for w, d in DAY_WORDS.items():
idx = text.find(w)
if idx >= 0 and d not in seen:
found.append((idx,d)); seen.add(d)
found.sort(); return [d for _,d in found]
def _extract_period(text):
m = re.search(r'p(?:eriod)?\s*(\d+)', text)
return int(m.group(1)) if m else None
def _extract_subjects(text, sub_map):
# Sort by name length descending so "nlplab" matches before "nlp"
sorted_items = sorted(sub_map.items(), key=lambda x: -len(x[0]))
seen_codes, seen_names, results = set(), set(), []
for name, code in sorted_items:
if not name or name not in text: continue
if code in seen_codes: continue
# Skip if a longer name already matched that contains this name
# e.g. skip "nlp" if "nlplab" already matched
already_covered = any(matched for matched in seen_names if name in matched)
if already_covered: continue
results.append(code)
seen_codes.add(code)
seen_names.add(name)
return results
def _extract_one_faculty(text, fac_map):
for name,fid in fac_map.items():
if name and name in text: return fid
return None
def _find_two_faculties(text, fac_map):
found = []
for name,fid in sorted(fac_map.items(), key=lambda x:-len(x[0])):
if name and name in text and fid not in found:
found.append(fid)
if len(found)==2: break
return found if len(found)==2 else None
def _extract_section(text, sections_data):
if not sections_data: return None
# Sort by length descending so "6a-e1" matches before "6a"
for s in sorted(sections_data, key=lambda x: -len(x['id'])):
if s['id'].lower() in text:
return s['id']
# Match shorthand like "6a" -> return prefix as parent filter
# e.g. "6a" matches 6a-E1 AND 6a-E2 -> return "6A" (prefix)
# caller uses this to filter all sub-sections of 6A
prefixes = {}
for s in sections_data:
prefix = s['id'].split('-')[0].lower()
prefixes.setdefault(prefix, []).append(s['id'])
for prefix, ids in sorted(prefixes.items(), key=lambda x: -len(x[0])):
pattern = r'(?<![a-z0-9])' + re.escape(prefix) + r'(?![a-z0-9])'
if re.search(pattern, text):
if len(ids) == 1:
return ids[0] # unique β return full id
else:
return prefix.upper() # ambiguous β return parent "6A", "5A" etc.
return None
# Sort by length descending so "6a-e1" matches before "6a"
for s in sorted(sections_data, key=lambda x: -len(x['id'])):
if s['id'].lower() in text:
return s['id']
# Match shorthand like "6a" -> returns None if multiple sections share prefix
# e.g. "6a" matches 6a-E1 AND 6a-E2, so return None (move all of them)
prefixes = {}
for s in sections_data:
prefix = s['id'].split('-')[0].lower()
prefixes.setdefault(prefix, []).append(s['id'])
for prefix, ids in prefixes.items():
if re.search(r'' + prefix + r'', text):
if len(ids) == 1:
return ids[0] # unique match
# multiple sections with same prefix β return None (affects all)
return None
def parse_substitution_prompt(prompt, faculties_data):
result = smart_parse(prompt, faculties_data)
return result if result and result.get('type')=='FACULTY_SUBSTITUTION' else {} |