""" NurseLex — Pre-cached legislation for offline lookup. Loads full section text from nursing_sections.json (1,000+ sections). Source: legislation.gov.uk (Crown Copyright, OGL v3.0) via i.AI Lex API. """ import os import json import logging logger = logging.getLogger(__name__) # --- Configuration --- JSON_PATH = os.path.join(os.path.dirname(__file__), "nursing_sections.json") # --- Load Sections --- CACHED_SECTIONS = [] try: if os.path.exists(JSON_PATH): with open(JSON_PATH, "r", encoding="utf-8") as f: CACHED_SECTIONS = json.load(f) logger.info(f"Loaded {len(CACHED_SECTIONS)} sections from {JSON_PATH}") else: logger.warning(f"Metadata file {JSON_PATH} not found.") except Exception as e: logger.error(f"Error loading {JSON_PATH}: {e}") # --- Keyword Map for Natural Language Shortcuts --- # Direct section links for common nursing search terms KEYWORD_MAP = { "nurse holding power": ("ukpga/1983/20", "5(4)"), "doctor holding power": ("ukpga/1983/20", "5(2)"), "section 2": ("ukpga/1983/20", "2"), "section 3": ("ukpga/1983/20", "3"), "section 4": ("ukpga/1983/20", "4"), "aftercare": ("ukpga/1983/20", "117"), "section 117": ("ukpga/1983/20", "117"), "leave of absence": ("ukpga/1983/20", "17"), "section 17": ("ukpga/1983/20", "17"), "place of safety": ("ukpga/1983/20", "136"), "section 136": ("ukpga/1983/20", "136"), "section 135": ("ukpga/1983/20", "135"), "best interests": ("ukpga/2005/9", "4"), "capacity test": ("ukpga/2005/9", "3"), "functional test": ("ukpga/2005/9", "3"), "mca principles": ("ukpga/2005/9", "1"), "safeguarding": ("ukpga/2014/23", "42"), "section 42": ("ukpga/2014/23", "42"), "advocacy": ("ukpga/2014/23", "67"), } def search_cached(query: str, max_results: int = 5) -> list: """ Search local sections by keyword, title, or legislation ID. Returns a list of section dictionaries. """ if not query: return [] query = query.lower().strip() results = [] # 1. Check Keyword Map First (High Precision) for kw, (leg_id, sec_num) in KEYWORD_MAP.items(): if kw in query: # Find the specific section in our list for s in CACHED_SECTIONS: if s.get("legislation_id") == leg_id and str(s.get("number")) == sec_num: if s not in results: results.append(s) # Find closest matches if exact number not found (e.g. 5(4) vs 5) if not results: for s in CACHED_SECTIONS: if s.get("legislation_id") == leg_id and str(s.get("number")).startswith(sec_num.split('(')[0]): if s not in results: results.append(s) # 2. Text-based Search # Sort sections by relevance (title match > text match) scored_results = [] for s in CACHED_SECTIONS: score = 0 title = s.get("title", "").lower() text = s.get("text", "").lower() leg_id = s.get("legislation_id", "").lower() num = str(s.get("number", "")).lower() # Exact section reference (e.g. "Section 5") if f"section {num}" in query or f"s.{num}" in query or f"s {num}" in query: score += 100 # Title matches if query in title: score += 50 # Word matches in title for word in query.split(): if len(word) > 3 and word in title: score += 10 # Content matches if query in text: score += 5 if score > 0: scored_results.append((score, s)) # Sort and add to results scored_results.sort(key=lambda x: x[0], reverse=True) for _, s in scored_results: if s not in results: results.append(s) if len(results) >= max_results: break return results[:max_results]