Spaces:
Sleeping
Sleeping
| """ | |
| NurseLex — Lex API Client | |
| Wraps the i.AI Lex API for nursing-focused UK legislation search. | |
| """ | |
| import httpx | |
| import logging | |
| from typing import Optional | |
| logger = logging.getLogger(__name__) | |
| LEX_API_BASE = "https://lex.lab.i.ai.gov.uk" | |
| LEX_TIMEOUT = 60.0 # Lex API can be slow for semantic search | |
| # Key legislation IDs for mental health & learning disability nursing | |
| NURSING_LEGISLATION = { | |
| "Mental Health Act 1983": "ukpga/1983/20", | |
| "Mental Capacity Act 2005": "ukpga/2005/9", | |
| "Care Act 2014": "ukpga/2014/23", | |
| "Human Rights Act 1998": "ukpga/1998/42", | |
| "Equality Act 2010": "ukpga/2010/15", | |
| "Health and Social Care Act 2012": "ukpga/2012/7", | |
| "Children Act 1989": "ukpga/1989/41", | |
| "Children Act 2004": "ukpga/2004/31", | |
| "Safeguarding Vulnerable Groups Act 2006": "ukpga/2006/47", | |
| "Mental Health Units (Use of Force) Act 2018": "ukpga/2018/27", | |
| "Health and Care Act 2022": "ukpga/2022/31", | |
| "Autism Act 2009": "ukpga/2009/15", | |
| } | |
| async def _post(endpoint: str, payload: dict) -> dict | list: | |
| """Make a POST request to the Lex API with retry logic.""" | |
| url = f"{LEX_API_BASE}{endpoint}" | |
| for attempt in range(3): | |
| try: | |
| async with httpx.AsyncClient(timeout=LEX_TIMEOUT) as client: | |
| resp = await client.post(url, json=payload) | |
| resp.raise_for_status() | |
| return resp.json() | |
| except httpx.TimeoutException: | |
| logger.warning(f"Lex API timeout (attempt {attempt + 1}/3): {endpoint}") | |
| if attempt == 2: | |
| raise | |
| except httpx.HTTPStatusError as e: | |
| logger.error(f"Lex API error {e.response.status_code}: {endpoint}") | |
| raise | |
| return [] | |
| async def search_legislation_sections( | |
| query: str, | |
| legislation_id: Optional[str] = None, | |
| size: int = 5, | |
| ) -> list[dict]: | |
| """Semantic search across legislation sections.""" | |
| payload = { | |
| "query": query, | |
| "size": size, | |
| "include_text": True, | |
| } | |
| if legislation_id: | |
| payload["legislation_id"] = legislation_id | |
| try: | |
| return await _post("/legislation/section/search", payload) | |
| except Exception as e: | |
| logger.error(f"Section search failed: {e}") | |
| return [] | |
| async def search_legislation_acts( | |
| query: str, | |
| limit: int = 5, | |
| ) -> dict: | |
| """Search for Acts and Statutory Instruments.""" | |
| payload = { | |
| "query": query, | |
| "limit": limit, | |
| "include_text": True, | |
| } | |
| try: | |
| return await _post("/legislation/search", payload) | |
| except Exception as e: | |
| logger.error(f"Act search failed: {e}") | |
| return {"results": [], "total": 0, "offset": 0, "limit": limit} | |
| async def lookup_legislation(legislation_id: str) -> dict: | |
| """Look up a specific Act by its ID (e.g., 'ukpga/1983/20').""" | |
| parts = legislation_id.split("/") | |
| payload = { | |
| "legislation_type": parts[0], | |
| "year": int(parts[1]), | |
| "number": int(parts[2]), | |
| } | |
| return await _post("/legislation/lookup", payload) | |
| async def get_legislation_full_text( | |
| legislation_id: str, | |
| include_schedules: bool = False, | |
| ) -> dict: | |
| """Get the full text of a piece of legislation.""" | |
| payload = { | |
| "legislation_id": legislation_id, | |
| "include_schedules": include_schedules, | |
| } | |
| return await _post("/legislation/text", payload) | |
| async def get_sections_for_legislation( | |
| legislation_id: str, | |
| limit: int = 200, | |
| ) -> list[dict]: | |
| """Get all sections for a specific piece of legislation.""" | |
| payload = { | |
| "legislation_id": legislation_id, | |
| "limit": limit, | |
| } | |
| try: | |
| return await _post("/legislation/section/lookup", payload) | |
| except Exception as e: | |
| logger.error(f"Section lookup failed: {e}") | |
| return [] | |
| async def search_explanatory_notes( | |
| query: str, | |
| legislation_id: Optional[str] = None, | |
| size: int = 5, | |
| ) -> list[dict]: | |
| """Search explanatory notes for legislation.""" | |
| payload = { | |
| "query": query, | |
| "size": size, | |
| } | |
| if legislation_id: | |
| payload["legislation_id"] = legislation_id | |
| try: | |
| return await _post("/explanatory_note/section/search", payload) | |
| except Exception as e: | |
| logger.error(f"Explanatory note search failed: {e}") | |
| return [] | |
| async def search_amendments( | |
| legislation_id: str, | |
| search_amended: bool = True, | |
| size: int = 20, | |
| ) -> list[dict]: | |
| """Search for amendments to or by a piece of legislation.""" | |
| payload = { | |
| "legislation_id": legislation_id, | |
| "search_amended": search_amended, | |
| "size": size, | |
| } | |
| try: | |
| return await _post("/amendment/search", payload) | |
| except Exception as e: | |
| logger.error(f"Amendment search failed: {e}") | |
| return [] | |
| def format_sections_for_context(sections: list[dict], max_chars: int = 6000) -> str: | |
| """Format legislation sections into a readable context string for the LLM.""" | |
| context_parts = [] | |
| total_chars = 0 | |
| for section in sections: | |
| title = section.get("title", "Untitled") | |
| text = section.get("text", "") | |
| leg_id = section.get("legislation_id", "") | |
| section_num = section.get("number", "") | |
| entry = f"### {title}\n" | |
| entry += f"**Source:** {leg_id}, Section {section_num}\n\n" | |
| entry += f"{text}\n\n---\n\n" | |
| if total_chars + len(entry) > max_chars: | |
| break | |
| context_parts.append(entry) | |
| total_chars += len(entry) | |
| return "".join(context_parts) if context_parts else "No relevant legislation sections found." | |
| ======= | |
| """ | |
| NurseLex — Lex API Client | |
| Wraps the i.AI Lex API for nursing-focused UK legislation search. | |
| """ | |
| import httpx | |
| import logging | |
| from typing import Optional | |
| logger = logging.getLogger(__name__) | |
| LEX_API_BASE = "https://lex.lab.i.ai.gov.uk" | |
| LEX_TIMEOUT = 60.0 # Lex API can be slow for semantic search | |
| # Key legislation IDs for mental health & learning disability nursing | |
| NURSING_LEGISLATION = { | |
| "Mental Health Act 1983": "ukpga/1983/20", | |
| "Mental Capacity Act 2005": "ukpga/2005/9", | |
| "Care Act 2014": "ukpga/2014/23", | |
| "Human Rights Act 1998": "ukpga/1998/42", | |
| "Equality Act 2010": "ukpga/2010/15", | |
| "Health and Social Care Act 2012": "ukpga/2012/7", | |
| "Children Act 1989": "ukpga/1989/41", | |
| "Children Act 2004": "ukpga/2004/31", | |
| "Safeguarding Vulnerable Groups Act 2006": "ukpga/2006/47", | |
| "Mental Health Units (Use of Force) Act 2018": "ukpga/2018/27", | |
| "Health and Care Act 2022": "ukpga/2022/31", | |
| "Autism Act 2009": "ukpga/2009/15", | |
| } | |
| async def _post(endpoint: str, payload: dict) -> dict | list: | |
| """Make a POST request to the Lex API with retry logic.""" | |
| url = f"{LEX_API_BASE}{endpoint}" | |
| for attempt in range(3): | |
| try: | |
| async with httpx.AsyncClient(timeout=LEX_TIMEOUT) as client: | |
| resp = await client.post(url, json=payload) | |
| resp.raise_for_status() | |
| return resp.json() | |
| except httpx.TimeoutException: | |
| logger.warning(f"Lex API timeout (attempt {attempt + 1}/3): {endpoint}") | |
| if attempt == 2: | |
| raise | |
| except httpx.HTTPStatusError as e: | |
| logger.error(f"Lex API error {e.response.status_code}: {endpoint}") | |
| raise | |
| return [] | |
| async def search_legislation_sections( | |
| query: str, | |
| legislation_id: Optional[str] = None, | |
| size: int = 5, | |
| ) -> list[dict]: | |
| """Semantic search across legislation sections.""" | |
| payload = { | |
| "query": query, | |
| "size": size, | |
| "include_text": True, | |
| } | |
| if legislation_id: | |
| payload["legislation_id"] = legislation_id | |
| try: | |
| return await _post("/legislation/section/search", payload) | |
| except Exception as e: | |
| logger.error(f"Section search failed: {e}") | |
| return [] | |
| async def search_legislation_acts( | |
| query: str, | |
| limit: int = 5, | |
| ) -> dict: | |
| """Search for Acts and Statutory Instruments.""" | |
| payload = { | |
| "query": query, | |
| "limit": limit, | |
| "include_text": True, | |
| } | |
| try: | |
| return await _post("/legislation/search", payload) | |
| except Exception as e: | |
| logger.error(f"Act search failed: {e}") | |
| return {"results": [], "total": 0, "offset": 0, "limit": limit} | |
| async def lookup_legislation(legislation_id: str) -> dict: | |
| """Look up a specific Act by its ID (e.g., 'ukpga/1983/20').""" | |
| parts = legislation_id.split("/") | |
| payload = { | |
| "legislation_type": parts[0], | |
| "year": int(parts[1]), | |
| "number": int(parts[2]), | |
| } | |
| return await _post("/legislation/lookup", payload) | |
| async def get_legislation_full_text( | |
| legislation_id: str, | |
| include_schedules: bool = False, | |
| ) -> dict: | |
| """Get the full text of a piece of legislation.""" | |
| payload = { | |
| "legislation_id": legislation_id, | |
| "include_schedules": include_schedules, | |
| } | |
| return await _post("/legislation/text", payload) | |
| async def get_sections_for_legislation( | |
| legislation_id: str, | |
| limit: int = 200, | |
| ) -> list[dict]: | |
| """Get all sections for a specific piece of legislation.""" | |
| payload = { | |
| "legislation_id": legislation_id, | |
| "limit": limit, | |
| } | |
| try: | |
| return await _post("/legislation/section/lookup", payload) | |
| except Exception as e: | |
| logger.error(f"Section lookup failed: {e}") | |
| return [] | |
| async def search_explanatory_notes( | |
| query: str, | |
| legislation_id: Optional[str] = None, | |
| size: int = 5, | |
| ) -> list[dict]: | |
| """Search explanatory notes for legislation.""" | |
| payload = { | |
| "query": query, | |
| "size": size, | |
| } | |
| if legislation_id: | |
| payload["legislation_id"] = legislation_id | |
| try: | |
| return await _post("/explanatory_note/section/search", payload) | |
| except Exception as e: | |
| logger.error(f"Explanatory note search failed: {e}") | |
| return [] | |
| async def search_amendments( | |
| legislation_id: str, | |
| search_amended: bool = True, | |
| size: int = 20, | |
| ) -> list[dict]: | |
| """Search for amendments to or by a piece of legislation.""" | |
| payload = { | |
| "legislation_id": legislation_id, | |
| "search_amended": search_amended, | |
| "size": size, | |
| } | |
| try: | |
| return await _post("/amendment/search", payload) | |
| except Exception as e: | |
| logger.error(f"Amendment search failed: {e}") | |
| return [] | |
| def format_sections_for_context(sections: list[dict], max_chars: int = 6000) -> str: | |
| """Format legislation sections into a readable context string for the LLM.""" | |
| context_parts = [] | |
| total_chars = 0 | |
| for section in sections: | |
| title = section.get("title", "Untitled") | |
| text = section.get("text", "") | |
| leg_id = section.get("legislation_id", "") | |
| section_num = section.get("number", "") | |
| entry = f"### {title}\n" | |
| entry += f"**Source:** {leg_id}, Section {section_num}\n\n" | |
| entry += f"{text}\n\n---\n\n" | |
| if total_chars + len(entry) > max_chars: | |
| break | |
| context_parts.append(entry) | |
| total_chars += len(entry) | |
| return "".join(context_parts) if context_parts else "No relevant legislation sections found." | |
| >>>>>>> a4e257b16d56f80612b7c9ac6d2e7c198fef5bb6 | |