This document describes the third type of corpus action: agent-based actions that invoke an AI agent with a prompt and pre-authorized tools. This extends the existing corpus action system (which supports fieldsets and analyzers) to enable intelligent, automated document processing.
Status: Implemented in v3.0.0 Last Updated: 2026-01-09
- Auto-update Document Summary: When a document is added, analyze it and create/update the summary
- Auto-annotate Key Clauses: Find and annotate specific clause types (indemnification, termination, etc.)
- Auto-tag Documents: Analyze content and add appropriate tags/labels
- Extract Metadata: Pull out key dates, parties, values into custom_meta
- Quality Checks: Verify document completeness or flag issues
Critical: All agent operations must respect the Document versioning architecture (see docs/architecture/document_versioning.md).
| Tool/Operation | Target | Why Safe |
|---|---|---|
update_document_summary |
DocumentSummaryRevision | Separate append-only model |
Update description |
Document.description | Metadata field, no hash change |
Update title |
Document.title | Metadata field, no hash change |
Update custom_meta |
Document.custom_meta | JSON field, no hash change |
| Create annotations | Annotation model | Separate model |
| Create notes | Note model | Separate model |
- Uploading/replacing PDF file (changes
pdf_file_hash) - Any operation that modifies document content
The CorpusAction model has been extended to support agent-based actions.
Source: opencontractserver/corpuses/models.py (lines 830-942)
Key fields added:
agent_config- ForeignKey toAgentConfigurationtask_instructions- Task-specific prompt for the agentpre_authorized_tools- JSON list of tools pre-authorized to run without approval
Database constraint ensures exactly ONE of fieldset, analyzer, or agent_config is set.
Stores results from agent-based corpus actions.
Source: opencontractserver/agents/models.py (lines 223-379)
Key fields:
corpus_action- ForeignKey to the triggering CorpusActiondocument- ForeignKey to the processed document (nullable for thread-based actions)triggering_conversation/triggering_message- For thread/message triggersstatus- Execution status (PENDING, RUNNING, COMPLETED, FAILED)agent_response- Final response content from the agentexecution_metadata- JSON with model used, token counts, etc.
Source: opencontractserver/tasks/agent_tasks.py
- Celery task that executes agent-based corpus actions on documents
- Uses
asyncio.run()to invoke async agent API - Creates/updates
AgentActionResultrecords for tracking - Implements idempotency (skips already-completed or running tasks)
- Handles race conditions with
select_for_update() - Links to
CorpusActionExecutionfor unified tracking
- Handles
NEW_THREADandNEW_MESSAGEtriggers - Builds context from thread/message content
- Automatically includes moderation tools
Source: opencontractserver/tasks/corpus_tasks.py (lines 146-354)
The process_corpus_action task dispatches to the appropriate handler:
action.fieldset→ Fieldset extraction tasksaction.analyzer→ Analyzer tasksaction.agent_config→run_agent_corpus_actionorrun_agent_thread_action
The agent factory (opencontractserver/llms/api.py) supports skip_approval_gate parameter for automated corpus actions. When enabled, all tools run without requiring user approval.
Key Integration Points:
agents.for_document()- Passskip_approval_gate=Truefor automated executionagents.for_corpus()- Same parameter for corpus-level agents
The agent task implementation sets skip_approval_gate=True when invoking agents for corpus actions to enable fully automated execution.
Source: config/graphql/mutations.py
Extended to support agent-based actions:
agent_config_id- ID of the agent configuration to usetask_instructions- Task prompt for the agentpre_authorized_tools- List of tool names pre-authorized for executioncreate_agent_inline- Create a new corpus-scoped agent inline (for thread/message triggers)
Supports updating all agent-specific fields including trigger type, agent config, and prompt.
Source: config/graphql/graphene_types.py
Exposes agent-related fields:
agent_config- The linked AgentConfigurationtask_instructions- Task promptpre_authorized_tools- List of pre-authorized tool names
Exposes execution results:
tools_executed- List of tools executed with resultsexecution_metadata- Model, tokens, timing infoduration_seconds- Computed execution duration
Source: config/graphql/queries.py (lines 2477-2523)
agent_action_results - Query agent action results with filters:
corpus_action_id- Filter by corpus actiondocument_id- Filter by documentstatus- Filter by execution status
Uses AgentActionResult.objects.visible_to_user() for permission filtering.
mutation CreateAutoSummaryAction {
create_corpus_action(
corpusId: "Q29ycHVzVHlwZTox"
trigger: "add_document"
name: "Auto-Generate Summary"
agentConfigId: "QWdlbnRDb25maWd1cmF0aW9uVHlwZTox"
taskInstructions: """
Analyze this document and create a comprehensive summary.
1. Use load_document_text to read the full content
2. Identify the document type, key parties, and main topics
3. Use update_document_summary to save a 3-5 sentence summary
Focus on: document purpose, key terms, important dates, and parties involved.
"""
preAuthorizedTools: ["load_document_text", "load_document_summary", "update_document_summary"]
) {
ok
message
obj {
id
name
taskInstructions
preAuthorizedTools
}
}
}query GetActionResults($corpusActionId: ID!) {
agent_action_results(corpusActionId: $corpusActionId) {
edges {
node {
id
document {
id
title
}
status
agentResponse
toolsExecuted
startedAt
completedAt
errorMessage
}
}
}
}| Phase | Component | Status |
|---|---|---|
| 1 | CorpusAction model extension | ✅ Complete |
| 1 | AgentActionResult model | ✅ Complete |
| 1 | Database migrations | ✅ Complete |
| 2 | skip_approval_gate in agent factory |
✅ Complete |
| 2 | run_agent_corpus_action task |
✅ Complete |
| 2 | run_agent_thread_action task |
✅ Complete |
| 2 | Integration with process_corpus_action |
✅ Complete |
| 2 | Deferred action architecture | ✅ Complete |
| 3 | CreateCorpusAction mutation extended | ✅ Complete |
| 3 | UpdateCorpusAction mutation | ✅ Complete |
| 3 | AgentActionResultType | ✅ Complete |
| 3 | GraphQL queries for results | ✅ Complete |
| 4 | CorpusSettings UI for agent actions | ✅ Complete |
| 4 | Corpus agent management UI | ✅ Complete |
| 4 | Pre-authorized tools selector | ✅ Complete |
| 5 | Unit tests for models | ✅ Complete |
| 5 | Integration tests for task execution | ✅ Complete |
| 5 | Corpus document action tests | ✅ Complete |
| Category | File |
|---|---|
| Models | opencontractserver/corpuses/models.py - CorpusAction, CorpusActionExecution |
| Models | opencontractserver/agents/models.py - AgentActionResult |
| Tasks | opencontractserver/tasks/agent_tasks.py - Agent execution tasks |
| Tasks | opencontractserver/tasks/corpus_tasks.py - Action dispatch |
| Tasks | opencontractserver/tasks/doc_tasks.py - Document unlock triggers |
| GraphQL | config/graphql/graphene_types.py - CorpusActionType, AgentActionResultType |
| GraphQL | config/graphql/mutations.py - CreateCorpusAction, UpdateCorpusAction |
| GraphQL | config/graphql/queries.py - agent_action_results query |
| Frontend | frontend/src/components/corpuses/CorpusAgentManagement.tsx |
| Tests | opencontractserver/tests/test_agent_corpus_action_task.py |
| Tests | opencontractserver/tests/test_corpus_document_actions.py |
- Tool Pre-Authorization: Only corpus owners can create actions with pre-authorized tools
- Permission Inheritance: Actions inherit corpus permissions
- Audit Trail: All executions are logged via AgentActionResult model
- Tool Filtering: Public corpuses cannot use approval-gated tools (even if pre-authorized)
- Rate Limiting: Celery task queue provides natural rate limiting; additional limits can be configured
Corpus actions (including agent-based ones) need access to fully processed document content. When a document is uploaded, it goes through a processing pipeline:
- Thumbnail extraction - Generate preview image
- Document ingestion - Parse PDF, extract text, create PAWLs layers
- Unlock document - Set
backend_lock=False,processing_finished=now()
If corpus actions fire immediately when a document is added to a corpus,
agent tools like load_document_text may fail because the document isn't fully parsed yet.
We use direct invocation from the document lifecycle methods, with DocumentPath as the
source of truth for corpus membership (not the M2M relationship):
┌─────────────────────────────────────────────────────────────────────────────┐
│ DOCUMENT UPLOAD FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Document Created │
│ │ │
│ ▼ │
│ backend_lock = True ──────────────────┐ │
│ │ │ │
│ ▼ │ │
│ add_document() / import_document() │ │
│ │ │ │
│ ▼ │ │
│ Create DocumentPath │ │
│ │ │ │
│ ▼ │ │
│ Check backend_lock │ │
│ │ │ │
│ ┌────┴────┐ │ │
│ │ │ │ │
│ ▼ ▼ │ │
│ TRUE FALSE │ Processing Pipeline │
│ │ │ │ (thumbnail, parse, embed) │
│ │ ▼ │ │ │
│ │ process_corpus_action │ │ │
│ │ triggered directly │ ▼ │
│ │ └──► set_doc_lock_state(False) │
│ │ │ │
│ │ ▼ │
│ │ Query DocumentPath for corpuses │
│ │ │ │
│ │ ▼ │
│ └──────────────────────────────────────► process_corpus_action │
│ for each corpus │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The document versioning architecture (Issue #654) introduced DocumentPath as the source
of truth for corpus-document relationships. The M2M relationship (Corpus.documents) has
been completely removed (Issue #835, migration 0039_remove_corpus_documents_m2m).
Using DocumentPath ensures:
import_document()works correctly (it creates DocumentPath but not M2M)- Soft-deleted documents are properly excluded (
is_deleted=False) - Only current paths are considered (
is_current=True)
Source: opencontractserver/corpuses/models.py (lines 443-624)
The Corpus.add_document() method triggers corpus actions directly if the document is ready (backend_lock=False). If the document is still processing, actions are deferred to set_doc_lock_state().
Source: opencontractserver/documents/versioning.py
Same pattern as add_document() - triggers actions directly if document is ready.
Source: opencontractserver/tasks/doc_tasks.py (lines 60-118)
When a document is unlocked (locked=False), this task:
- Updates the document's
backend_lockandprocessing_finishedfields - Queries
DocumentPathto find all corpuses the document belongs to - Triggers
process_corpus_actionfor each corpus - Creates
DOCUMENT_PROCESSEDnotifications for the document creator and corpus owners
| Scenario | add_document/import_document | set_doc_lock_state |
|---|---|---|
| New doc uploaded to corpus | Skipped (locked) | Triggers actions via DocumentPath |
| Existing processed doc added | Triggers immediately | N/A (already unlocked) |
| Doc in multiple corpuses | N/A | Triggers for ALL corpuses |
| Doc not in any corpus | N/A | No action (no DocumentPath records) |
| Soft-deleted doc | N/A | Ignored (is_deleted=True) |
Important: Corpus actions SHOULD be designed to be idempotent. Due to the deferred execution architecture, the same action may be triggered multiple times for the same document in edge cases:
- Document added to corpus while still processing → triggers via
set_doc_lock_state - Document later re-added or corpus action re-run → may trigger again
Most built-in corpus actions are idempotent by design:
- Fieldset extractions: Use
Extract.objects.get_or_create() - Analyzers: Create Analysis objects (tracked via CorpusAction)
- Agent actions: Use
AgentActionResult.objects.get_or_create()- skips if already completed
Recommendations for custom agent prompts:
- Check before creating: Use tools like
load_document_summaryto check if work already exists - Update rather than duplicate: Use
update_document_summarywhich overwrites, not appends - Idempotent annotations: Agent should check for existing annotations before creating new ones
If stricter duplicate prevention is needed, actions can check for existing results before execution.
Source: opencontractserver/tests/test_corpus_document_actions.py
Tests covering the deferred action architecture:
test_add_document_triggers_actions_for_ready_doc- Ready docs trigger immediatelytest_add_document_skips_actions_for_locked_doc- Locked docs deferred to set_doc_lock_statetest_set_doc_lock_state_triggers_actions_via_document_path- DocumentPath used as source of truthtest_set_doc_lock_state_no_corpus_no_action- Orphan docs ignoredtest_set_doc_lock_state_triggers_for_multiple_corpuses- Multi-corpus supporttest_set_doc_lock_state_ignores_deleted_paths- Soft-deleted paths excluded
Source: opencontractserver/tests/test_agent_corpus_action_task.py
Tests covering agent task execution:
test_successful_execution_creates_result- Result creation on successtest_skip_already_completed_result- Idempotency for completed resultstest_skip_already_running_result- Race condition preventiontest_retry_failed_result- Failed results can be retriedtest_agent_failure_marks_result_failed- Error handlingtest_long_error_message_truncated- Error message truncationtest_execution_tracking_on_success- CorpusActionExecution integration