Extracting Structured Data from Documents¶
Last Updated: 2026-01-09
Overview¶
OpenContracts transforms any collection of documents into a spreadsheet-like data grid. You define what to extract via a Fieldset, and the system:
- Fans out work across documents and columns using Celery
- Uses our structured extraction API powered by PydanticAI agents
- Enforces constraints through intelligent prompting
- Parses results into Python primitives or Pydantic models with guaranteed type safety
Everything is orchestrated by two Celery tasks: - run_extract – creates individual cells and queues work - doc_extract_query_task – performs the actual extraction using our agent framework
LLM agents can also dispatch extracts. The
list_fieldsets/start_extractagent tools wrap the samerun_extractpipeline — letting a chat agent or an agent-basedCorpusActionpick from configured Fieldsets and queue an extract under the standard approval gate. The tools always pick from existing Fieldsets; they never invent new column schemas.
Data Models¶
All models are defined in opencontractserver/extracts/models.py.
Fieldset¶
Groups related columns together. Each Fieldset represents a specific configuration of data fields to extract.
Key features: - Defines the schema for extraction - Can be linked to a corpus as its metadata schema - Supports permissions for access control
Column¶
Defines individual data fields to extract. Each column can be configured for either LLM-based extraction or manual entry.
Extraction configuration: - query or match_text: The extraction prompt (one required for extraction columns) - output_type: Python type as string (e.g., "str", "int", "list[str]") - extract_is_list: Wraps the type in List[] - must_contain_text: Only extract from sections containing this text - limit_to_label: Only extract from annotations with this label - instructions: Additional context for extraction
Manual entry configuration: - is_manual_entry: When True, column is for manual metadata entry (no LLM extraction) - data_type: Structured data type (STRING, INTEGER, DATE, BOOLEAN, CHOICE, etc.) - validation_config: JSON configuration for field validation rules - default_value: Default value for manual entry fields - help_text: Help text displayed to users - display_order: Order in which to display manual entry fields
Extract¶
Represents an extraction job, containing metadata about the process.
Usage: - Groups documents to process with the fieldset defining what to extract - Tracks extraction progress and completion status - Stores error information if extraction fails
Datacell¶
Stores the result of extracting a specific column from a specific document.
Features: - Stores extracted data in JSON format - Links to source annotations (when available) - Tracks processing status and errors - Supports approval workflow for human review - Captures LLM call history for debugging
Extraction Pipeline¶
Orchestration (run_extract)¶
The main orchestrator task that creates and manages extraction jobs:
@shared_task
def run_extract(extract_id: Optional[str | int], user_id: str | int):
# Creates Datacells for each document × column pair
# Queues doc_extract_query_task for each cell
# Uses chord to wait for completion
Key operations: 1. Creates one Datacell per document × column combination 2. Looks up the Celery task from column.task_name 3. Uses chord(group(*tasks)) to wait for all cells 4. Calls mark_extract_complete when finished
Per-Cell Extraction (doc_extract_query_task)¶
The async task that performs actual extraction using our agent framework:
@celery_task_with_async_to_sync()
async def doc_extract_query_task(
cell_id: int,
similarity_top_k: int = 10,
max_token_length: int = 64000
) -> None:
"""Agent-based data extraction pipeline using PydanticAI."""
Extraction steps:
- Setup: Fetch Datacell, mark as started, validate corpus membership
- Type parsing: Convert
column.output_typestring to Python type - Prompt construction: Build extraction prompt from query or match_text
- System prompt: Add constraints from must_contain_text and limit_to_label
- Extract: Call
agents.get_structured_response_from_document() - Save results: Convert response to appropriate format and mark complete
Async Task Decorator Pattern¶
The extraction task uses our custom decorator to handle async functions in Celery:
@celery_task_with_async_to_sync()
async def doc_extract_query_task(...) -> None:
# Async implementation
This decorator: - Converts async functions to sync using asgiref.sync.async_to_sync - Properly handles database connections - Works seamlessly in test and production environments - Avoids complex event loop management
Testing async tasks:
from django.test import TransactionTestCase
class ExtractionTestCase(TransactionTestCase):
def test_extraction(self):
# Create datacell...
doc_extract_query_task.si(datacell.id).apply()
# Assert results...
Sequence Diagram¶
```mermaid sequenceDiagram participant U as User participant G as GraphQL/Admin participant R as run_extract participant Q as doc_extract_query_task participant A as Agent Framework participant LLM as Language Model
U->>G: Start extraction
G->>R: Call run_extract(extract_id)
R->>R: Create Datacells
R->>Q: Queue task for each cell
Q->>A: get_structured_response()
A->>LLM: Vector search + extraction
LLM-->>A: Typed response
A-->>Q: Parsed result
Q-->>Q: Save to Datacell.data
Q-->>R: Task complete
R-->>G: Extract finished
G-->>U: Results ready
```
Supported Output Types¶
The system supports extraction to various Python types:
Primitive Types¶
str- Text stringsint- Integersfloat- Floating point numbersbool- Boolean values
Collection Types¶
list[str]- List of stringslist[int]- List of integers- Use
extract_is_list=Trueto wrap any type in a list
Complex Types¶
- JSON objects via
dicttype - Custom Pydantic models (planned)
Constraints and Filtering¶
Document Section Filtering¶
Use must_contain_text to limit extraction to specific sections:
column.must_contain_text = "CONFIDENTIALITY"
# Only extracts from sections containing this text
Annotation Label Filtering¶
Use limit_to_label to extract only from specific annotation types:
column.limit_to_label = "contract-term"
# Only processes annotations with this label
Additional Instructions¶
Provide extra context via instructions:
column.instructions = "Extract as ISO 8601 date format"
Error Handling¶
The extraction pipeline includes comprehensive error tracking:
- Cell-level errors: Stored in
Datacell.stacktrace - Extract-level errors: Stored in
Extract.error - Automatic retry: Failed cells can be retried
- Partial completion: Successful cells are saved even if others fail
Manual Metadata Entry¶
Columns can be configured for manual entry instead of LLM-based extraction by setting is_manual_entry=True. This enables users to enter structured metadata directly.
Supported Data Types¶
The data_type field supports: STRING, TEXT, BOOLEAN, INTEGER, FLOAT, DATE, DATETIME, URL, EMAIL, CHOICE, MULTI_CHOICE, and JSON. See METADATA_DATA_TYPES in the models file.
Validation¶
Manual entry fields support validation via validation_config:
- Numeric fields:
min_value,max_value - String fields:
min_length,max_length,regex_pattern - Choice fields:
choices(list of valid options) - Required fields:
required: true
Validation is enforced in Datacell._validate_manual_entry().
Constraints¶
Manual metadata has a unique constraint ensuring one datacell per document-column combination when extract is null.
Datacell Approval Workflow¶
Datacells support a human review workflow for validating extracted or manually entered data.
Approval Fields¶
approved_by: User who approved the datacell valuerejected_by: User who rejected the datacell valuecorrected_data: Stores user-corrected data when the original extraction was incorrect
Workflow States¶
- Pending review: Both
approved_byandrejected_byare null - Approved:
approved_byis set, value accepted as-is - Rejected with correction:
rejected_byis set,corrected_datacontains the fix
LLM Call Logging¶
For debugging extraction issues, datacells capture the LLM conversation history.
llm_call_log: Text field storing the complete message history from the extraction agent- Useful for diagnosing unexpected extraction results
- Captured during
doc_extract_query_taskexecution
Performance Optimization¶
Parallel Processing¶
- Extraction tasks run in parallel across Celery workers
- Each document × column combination is independent
- Scales horizontally with additional workers
Vector Search Efficiency¶
- Uses pgvector for fast similarity search
- Caches embeddings for reuse
- Limits token context to
max_token_length
Database Optimization¶
- Batch creates Datacells
- Uses select_related/prefetch_related
- Minimizes database round trips
Configuration¶
Framework Selection¶
Set the agent framework in settings:
LLMS_DEFAULT_AGENT_FRAMEWORK = "pydantic_ai"
Custom Task Registration¶
Register custom extraction tasks:
# In your app's tasks.py
@shared_task
def custom_extract_task(cell_id: int):
# Custom extraction logic
pass
# In Column configuration
column.task_name = "myapp.tasks.custom_extract_task"
Next Steps¶
- Complex types: Expand output_type to support JSON schemas
- Multi-step extraction: Leverage conversation history
- Cross-document aggregation: Use corpus agents for analysis
- Custom models: Allow registration of Pydantic models