Source code for compass.extraction.date
"""Ordinance date extraction logic"""
import logging
from datetime import datetime
from compass.utilities.enums import LLMUsageCategory
from compass.utilities.parsing import raw_pages_from_doc
logger = logging.getLogger(__name__)
# Cap the text sent in a single date-extraction call. Enactment dates
# live in the preamble (front) and signature/adoption block (back), so
# keep the first and last pages and drop the middle for long documents.
_MAX_HEAD_PAGES = 20
_MAX_TAIL_PAGES = 10
[docs]
class DateExtractor:
"""Helper class to extract date info from document"""
SYSTEM_MESSAGE = (
"You are a legal scholar that reads ordinance text and extracts "
"a single date. The date you report is the latest year the "
"ordinance was enacted or amended or became effective. If no such "
"date is available return null."
"Return your answer in JSON format like this: "
'{"explanation": TEXT, "year": YY, "month": MM, "day": DD}'
"Where explanation contains a short summary/explanation of "
"the date information you found, including the exact text the "
"date is based on."
)
"""System message for date extraction LLM calls"""
def __init__(self, json_llm_caller, text_splitter=None):
"""
Parameters
----------
json_llm_caller : JSONFromTextLLMCaller
Instance used for structured validation queries.
text_splitter : LCTextSplitter, optional
Optional text splitter (or subclass instance, or any object
that implements a `split_text` method) to attach to doc
(used for splitting out pages in an HTML document).
By default, ``None``.
"""
self.jlc = json_llm_caller
self.text_splitter = text_splitter
[docs]
async def parse(self, doc):
"""Extract date (year, month, day) from doc
The full document text is read in a single LLM call. The
document's ``source`` URL, if any, is passed along as a hint,
but the document text is the source of truth.
Parameters
----------
doc : BaseDocument
Document to parse.
Returns
-------
tuple
3-tuple of (year, month, day). Any element that cannot be
determined is ``None``.
"""
raw_pages = [
page
for page in raw_pages_from_doc(doc, self.text_splitter)
if page
]
raw_pages = _trim_pages(raw_pages)
text = "\n\n".join(raw_pages)
if not text:
return None, None, None
content = "Please extract the enactment date for this ordinance."
url = doc.attrs.get("source")
if url:
content += f"\nThe document was downloaded from this URL: {url}"
content += f"\n\nOrdinance text:\n{text}"
response = await self.jlc.call(
sys_msg=self.SYSTEM_MESSAGE,
content=content,
usage_sub_label=LLMUsageCategory.DATE_EXTRACTION,
)
if response:
logger.debug(
"Date extraction explanation: %s",
response.get("explanation"),
)
date = _parse_date(response)
logger.debug("Parsed date: %s", date)
return date
def _trim_pages(pages):
"""Keep the head and tail pages, dropping the middle if too long"""
if len(pages) <= _MAX_HEAD_PAGES + _MAX_TAIL_PAGES:
return pages
return pages[:_MAX_HEAD_PAGES] + pages[-_MAX_TAIL_PAGES:]
def _parse_date(date_info):
"""Validate and return the (year, month, day) from a response"""
if not date_info:
return None, None, None
year = _validated_element(
date_info, key="year", min_val=1950, max_val=datetime.now().year + 1
)
month = _validated_element(date_info, key="month", min_val=1, max_val=12)
day = _validated_element(date_info, key="day", min_val=1, max_val=31)
return year, month, day
def _validated_element(date_info, key, min_val, max_val):
"""Return a single date element if it falls within the valid range
Acts as a cheap safety net against an out-of-range or malformed
value in the model response.
"""
value = date_info.get(key)
logger.debug("key=%r, value=%r", key, value)
if value is None:
return None
try:
value = int(float(value))
except (TypeError, ValueError):
return None
return value if min_val <= value <= max_val else None