Skip to content

Tasks and pipelines

A task takes a LegalDoc and returns it with results added under doc.extracted[<key>]. TaskPipeline runs tasks in order. Each task declares requires_llm, which marks the deterministic/probabilistic boundary. The pipeline uses it to refuse model-calling tasks on blocked documents.

Built-in tasks

task_id Calls a model Output key How it works When the output is poor
pii_detection no pii_spans Presidio, or the regex fallback; also sets doc.privacy_profile Missed PII stays in the text. See Limitations.
citation no citations Regular expressions for US case citations, U.S.C., C.F.R., EU regulations and directives, and acronyms such as GDPR Other citation formats are silently absent.
classification no cuad_labels Keyword matching against CUAD clause types A label means an indicative phrase occurs, not that the clause exists.
ner no ner_entities spaCy (contractex[spacy] plus a spaCy model) As good as the spaCy model.
summarization yes summary One completion over the first 12,000 characters Returned as-is; nothing is validated. Longer documents are truncated.
timeline yes timeline One structured call over the first 12,000 characters Output that does not fit the schema raises an error; nothing is repaired.
obligations yes obligations One structured call over the first 12,000 characters As for timeline.
comparison yes comparison One structured call over the first 8,000 characters of each document (doc_b= keyword) As for timeline. Both documents' profiles are enforced.
contract_extraction yes contract ContractExtractor: document details, then clauses and financial terms per chunk, then risks Items below confidence_threshold get a warning in contract.metadata.warnings.
risk_analysis yes, plus rules risks Keyword rules over extracted clauses, plus one model call over the first 20,000 characters If the model call fails, only the rule results are returned (logged).

Model-calling tasks need a provider: an LLMProvider instance, or a model name (see LLM providers). There is no default, and a task without one raises ValueError.

Building a pipeline

from contractex import LegalDoc
from contractex.core.legal_document import DocType
from contractex.tasks import TaskRegistry

text = open("examples/data/sample_nda.txt", encoding="utf-8").read()
doc = LegalDoc(doc_type=DocType.CONTRACT, full_text=text)

pipeline = TaskRegistry.default().build_pipeline(["pii_detection", "classification"])
result = pipeline.run(doc)

print(result.privacy_profile.sensitivity, result.privacy_profile.pii_entities_found)
print(result.extracted["cuad_labels"])
confidential ['EMAIL_ADDRESS', 'PHONE_NUMBER']
['confidentiality', 'governing_law']

When pii_detection finds PII in a public document, it raises the document's sensitivity to confidential. That way the model-calling tasks later in the pipeline redact their prompts. To configure a task, pass task_kwargs={"<task_id>": {...}} to build_pipeline.

pipeline.run_async(doc) runs the same tasks without blocking the event loop.

Writing a task

Subclass LegalTask. If the task calls a model, set requires_llm = True and get the provider from self.llm_for(doc), which enforces the document's privacy profile on every call. Pass every document whose text goes into the prompt.

from contractex.llm import LocalProvider
from contractex.tasks import LegalTask, TaskPipeline


class DefinedTermsTask(LegalTask):
    """Deterministic: list defined terms with the structure parser."""

    task_id = "defined_terms"
    requires_llm = False

    def run(self, doc, **kwargs):
        from contractex.structure import parse_structure

        doc.extracted["defined_terms"] = sorted(parse_structure(doc.full_text).defined_terms)
        return doc


class PlainEnglishTask(LegalTask):
    """Calls a model through the privacy router."""

    task_id = "plain_english"
    requires_llm = True

    def __init__(self, llm_provider):
        self._llm_provider = llm_provider

    def run(self, doc, **kwargs):
        prompt = f"Explain this agreement in two sentences:\n\n{doc.full_text[:4000]}"
        doc.extracted["plain_english"] = self.llm_for(doc).complete(prompt)
        return doc


pipeline = TaskPipeline([DefinedTermsTask(), PlainEnglishTask(LocalProvider(model="llama3.1:8b"))])
print(pipeline.run(doc).extracted["defined_terms"])
['Agreement', 'Confidential Information', 'Harbourline', 'Kestrel', 'Purpose', 'Representatives']

To make a task available by name, register it with TaskRegistry.default().register(MyTask).