NurseLex / lex_client.py
NurseCitizenDeveloper's picture
fix: resolve remaining merge conflicts across all affected files
27c849b
"""
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