Files
gh-christianlouis-docuelevate/AGENTIC_CODING.md
T
2026-02-13 23:48:04 +00:00

19 KiB

Agentic Coding Guide for DocuElevate

Version: 1.0 Last Updated: 2026-02-06

This guide helps AI coding agents work effectively with the DocuElevate codebase. It provides context, conventions, and best practices for autonomous code contributions.


🎯 Project Overview

What is DocuElevate?

DocuElevate is an intelligent document processing system that:

  • Ingests documents from multiple sources (email, web upload, API)
  • Processes documents (OCR, PDF conversion, metadata extraction)
  • Stores documents in various cloud storage providers
  • Uses AI (OpenAI, Azure) for intelligent document classification and metadata extraction

Tech Stack

Backend:  FastAPI, SQLAlchemy, Celery, Redis
Frontend: Jinja2 templates, Tailwind CSS
AI/ML:    OpenAI API, Azure Document Intelligence
Storage:  Dropbox, Google Drive, OneDrive, S3, Nextcloud, Paperless-NGX
Auth:     Authentik (OAuth2), Basic Auth
Infra:    Docker, Docker Compose, Alembic (migrations)

Key Directories

DocuElevate/
├── app/
│   ├── api/          # REST API endpoints
│   ├── tasks/        # Celery background tasks
│   ├── routes/       # Deprecated - being migrated to api/
│   ├── views/        # UI routes and templates
│   ├── utils/        # Utility functions
│   ├── config.py     # Configuration (Pydantic Settings)
│   ├── database.py   # SQLAlchemy setup
│   ├── models.py     # Database models
│   ├── main.py       # FastAPI app initialization
│   └── auth.py       # Authentication logic
├── frontend/
│   ├── static/       # CSS, JS, images
│   └── templates/    # Jinja2 HTML templates
├── tests/           # Pytest test suite
├── docs/            # User documentation
├── migrations/      # Alembic database migrations
└── docker/          # Docker configuration

🤖 Agent Guidelines

Before Making Changes

  1. Understand the Context

    • Read relevant documentation in docs/
    • Check TODO.md for current priorities
    • Review SECURITY_AUDIT.md for security considerations
    • Check ROADMAP.md for feature direction
  2. Check Existing Patterns

    • Look at similar existing code first
    • Follow the established patterns in the codebase
    • Don't introduce new patterns without good reason
  3. Identify Dependencies

    • Check if your change affects multiple modules
    • Ensure you understand the Celery task flow
    • Consider impact on database schema

Code Conventions

Python Style

# Use Ruff formatting (line length: 120)
# Use type hints
def process_document(file_path: str, metadata: Dict[str, Any]) -> DocumentMetadata:
    """
    Process a document and extract metadata.

    Args:
        file_path: Absolute path to the document file
        metadata: Additional metadata to include

    Returns:
        DocumentMetadata object with extracted information

    Raises:
        FileNotFoundError: If file doesn't exist
        ProcessingError: If processing fails
    """
    pass

# Use descriptive variable names
user_document_path = Path("/workdir/documents/invoice.pdf")
ocr_result = extract_text_from_pdf(user_document_path)

# Prefer explicit over implicit
if storage_provider == "dropbox":
    upload_to_dropbox(file_path, metadata)
elif storage_provider == "google_drive":
    upload_to_google_drive(file_path, metadata)
else:
    raise ValueError(f"Unknown storage provider: {storage_provider}")

Configuration

# Always use settings from config.py
from app.config import settings

# Good
api_key = settings.openai_api_key

# Bad - never hardcode
api_key = "sk-abc123..."

# Check if optional services are configured
if settings.dropbox_app_key:
    # Dropbox is configured
    upload_to_dropbox()

Error Handling

# Use appropriate exception types
from fastapi import HTTPException, status

# API endpoints should return HTTP errors
@router.get("/files/{file_id}")
async def get_file(file_id: int):
    file = get_file_from_db(file_id)
    if not file:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"File with ID {file_id} not found"
        )
    return file

# Tasks should log and handle errors gracefully
@celery_app.task(bind=True, max_retries=3)
def process_document_task(self, file_path: str):
    try:
        result = process_document(file_path)
        return result
    except TemporaryError as e:
        logger.warning(f"Temporary error processing {file_path}: {e}")
        raise self.retry(exc=e, countdown=60)
    except PermanentError as e:
        logger.error(f"Permanent error processing {file_path}: {e}")
        # Don't retry permanent errors
        return {"error": str(e)}

Testing

# Mark tests appropriately
@pytest.mark.unit
def test_hash_file():
    """Unit test for file hashing utility."""
    pass

@pytest.mark.integration
def test_upload_api_endpoint(client):
    """Integration test for upload API."""
    pass

@pytest.mark.requires_external
@pytest.mark.skip(reason="Requires OpenAI API key")
def test_openai_metadata_extraction():
    """Test actual OpenAI integration."""
    pass

# Use fixtures for common setup
def test_document_processing(sample_pdf_path, db_session):
    """Test uses fixtures from conftest.py"""
    pass

📝 Common Tasks

Adding a New API Endpoint

  1. Create endpoint in app/api/:
# app/api/my_feature.py
from fastapi import APIRouter, HTTPException
from app.database import get_db
from app.models import MyModel

router = APIRouter(prefix="/api/my-feature", tags=["my-feature"])

@router.get("/")
async def list_items(db=Depends(get_db)):
    """List all items."""
    items = db.query(MyModel).all()
    return items
  1. Register router in app/api/__init__.py:
from app.api import my_feature

router.include_router(my_feature.router)
  1. Add tests in tests/test_api_my_feature.py

Adding a New Celery Task

  1. Create task in app/tasks/:
# app/tasks/my_task.py
from app.celery_app import celery_app
import logging

logger = logging.getLogger(__name__)

@celery_app.task(bind=True, max_retries=3)
def my_background_task(self, param: str):
    """
    Description of what this task does.

    Args:
        param: Description of parameter
    """
    try:
        logger.info(f"Processing task with param: {param}")
        # Task logic here
        return {"status": "success"}
    except Exception as e:
        logger.error(f"Task failed: {e}")
        raise self.retry(exc=e, countdown=60)
  1. Import in app/tasks/__init__.py
  2. Add tests in tests/test_tasks.py

Adding a Database Model

  1. Define model in app/models.py:
class MyModel(Base):
    __tablename__ = "my_table"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
  1. Create migration:
cd /path/to/DocuElevate
alembic revision --autogenerate -m "Add MyModel table"
alembic upgrade head
  1. Add model to tests fixtures

Adding a Storage Provider

  1. Create provider module in app/tasks/storage/:
# app/tasks/storage/my_provider.py
from app.config import settings
import logging

logger = logging.getLogger(__name__)

def upload_to_my_provider(file_path: str, metadata: dict) -> str:
    """
    Upload file to My Provider.

    Args:
        file_path: Local path to file
        metadata: Document metadata

    Returns:
        URL or ID of uploaded file

    Raises:
        ProviderError: If upload fails
    """
    if not settings.my_provider_api_key:
        raise ValueError("MY_PROVIDER_API_KEY not configured")

    # Implementation
    pass
  1. Add configuration to app/config.py:
class Settings(BaseSettings):
    # ... existing settings ...
    my_provider_api_key: Optional[str] = None
    my_provider_endpoint: Optional[str] = None
  1. Add to .env.demo:
# My Provider
MY_PROVIDER_API_KEY=your_api_key_here
MY_PROVIDER_ENDPOINT=https://api.myprovider.com
  1. Add validator in app/utils/config_validator/
  2. Add tests with mocked API calls

🔒 Security Best Practices

What to NEVER Do

  • Hardcode API keys, passwords, or secrets
  • Log sensitive data (passwords, tokens, API keys)
  • Accept unsanitized user input for file paths
  • Disable security features without documentation
  • Commit .env files or credentials

What to ALWAYS Do

  • Use settings from app/config.py for all configuration
  • Validate and sanitize all user inputs
  • Use parameterized database queries (SQLAlchemy handles this)
  • Check file paths for directory traversal (Path.resolve())
  • Use appropriate HTTP status codes (401, 403, 404, etc.)
  • Log security-relevant events
  • Add rate limiting for sensitive endpoints
  • Use HTTPS in production (documented in deployment guide)

Input Validation Example

from pathlib import Path
from fastapi import HTTPException, status

def validate_file_path(file_path: str, base_dir: str = "/workdir") -> Path:
    """Validate file path is within allowed directory."""
    try:
        path = Path(file_path).resolve()
        base = Path(base_dir).resolve()

        # Ensure path is within base directory
        if not path.is_relative_to(base):
            raise ValueError("Path outside allowed directory")

        return path
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid file path: {e}"
        )

🧪 Testing Strategy

Test Coverage Goals

  • Target: 80% overall coverage
  • Critical modules: 90%+ (auth, config, database)
  • Tasks: 70%+ (complex to test with external services)
  • API endpoints: 85%+

Test Types

# Unit tests - fast, isolated, no external dependencies
@pytest.mark.unit
def test_hash_file_empty(tmp_path):
    """Test hashing an empty file."""
    file = tmp_path / "empty.txt"
    file.write_text("")
    assert hash_file(str(file)) == "expected_hash"

# Integration tests - test multiple components together
@pytest.mark.integration
def test_upload_and_process(client, sample_pdf):
    """Test full upload and processing flow."""
    response = client.post("/api/upload", files={"file": sample_pdf})
    assert response.status_code == 200

# External service tests - skipped by default
@pytest.mark.requires_external
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="No API key")
def test_real_openai_extraction():
    """Test actual OpenAI API (skipped in CI)."""
    pass

Running Tests

# All tests
pytest

# Specific category
pytest -m unit
pytest -m integration

# With coverage
pytest --cov=app --cov-report=html

# Specific file
pytest tests/test_api.py -v

# Skip external services
pytest -m "not requires_external"

🚀 Performance Considerations

Async/Await

  • FastAPI endpoints are async by default
  • Use async def for I/O-bound operations
  • Use regular def for CPU-bound operations
# Good - async for I/O
@router.get("/files")
async def list_files(db: Session = Depends(get_db)):
    files = db.query(FileRecord).all()
    return files

# Also good - sync for CPU-heavy
@router.post("/hash")
def hash_large_file(file: UploadFile):
    return compute_hash(file.file.read())

Database Queries

# Good - single query with join
files = db.query(FileRecord).options(
    joinedload(FileRecord.metadata)
).filter(FileRecord.user_id == user_id).all()

# Bad - N+1 queries
files = db.query(FileRecord).filter(FileRecord.user_id == user_id).all()
for file in files:
    metadata = file.metadata  # Triggers separate query each time

Celery Tasks

# Long-running tasks should update progress
@celery_app.task(bind=True)
def process_large_batch(self, file_ids: List[int]):
    total = len(file_ids)
    for i, file_id in enumerate(file_ids):
        process_file(file_id)
        self.update_state(
            state='PROGRESS',
            meta={'current': i + 1, 'total': total}
        )

📚 Documentation Requirements

Code Documentation

def complex_function(param1: str, param2: int = 10) -> Dict[str, Any]:
    """
    One-line summary of what the function does.

    More detailed explanation if needed. Can span multiple
    lines and include examples.

    Args:
        param1: Description of param1
        param2: Description of param2, defaults to 10

    Returns:
        Dictionary containing:
            - key1: Description
            - key2: Description

    Raises:
        ValueError: If param1 is empty
        FileNotFoundError: If file doesn't exist

    Examples:
        >>> result = complex_function("test", 5)
        >>> print(result['key1'])
        'value'
    """
    pass

API Documentation

  • Use FastAPI's automatic OpenAPI generation
  • Add descriptions to endpoints
  • Document request/response models
  • Include example requests/responses
@router.post(
    "/upload",
    response_model=UploadResponse,
    status_code=status.HTTP_201_CREATED,
    summary="Upload a document",
    description="Upload a document for processing. Supports PDF, images, and Office documents.",
    responses={
        201: {"description": "Document uploaded successfully"},
        400: {"description": "Invalid file format"},
        413: {"description": "File too large"},
    }
)
async def upload_document(
    file: UploadFile = File(..., description="Document file to upload"),
    tags: List[str] = Query([], description="Optional tags for the document"),
):
    """Upload endpoint implementation."""
    pass

🐛 Debugging

Logging

import logging

logger = logging.getLogger(__name__)

# Use appropriate log levels
logger.debug("Detailed information for debugging")
logger.info("General information about operation")
logger.warning("Warning about potential issue")
logger.error("Error that needs attention")
logger.critical("Critical error that needs immediate attention")

# Include context in logs
logger.info(f"Processing document: {file_id}, user: {user_id}")

# Don't log sensitive data
logger.info(f"User authenticated")  # Good
logger.info(f"Password: {password}")  # BAD!

Common Issues

  1. Import Errors

    • Check if module is in __init__.py
    • Verify Python path includes project root
    • Look for circular imports
  2. Database Issues

    • Check if migrations are up to date: alembic upgrade head
    • Verify DATABASE_URL is set correctly
    • Check if tables exist: sqlite3 app/database.db .schema
  3. Celery Issues

    • Verify Redis is running: redis-cli ping
    • Check Celery worker logs
    • Ensure tasks are imported in celery_worker.py
  4. Test Failures

    • Check if test database is clean (use fixtures)
    • Verify environment variables are set in conftest.py
    • Run single test to isolate issue: pytest tests/test_file.py::test_name -v

🔄 Git Workflow & Versioning

Branch Names

  • feature/description - New features
  • bugfix/description - Bug fixes
  • hotfix/description - Urgent production fixes
  • refactor/description - Code refactoring
  • docs/description - Documentation updates

Conventional Commits (REQUIRED)

All commit messages MUST follow the Conventional Commits specification for automated versioning.

Format

<type>(<scope>): <subject>

<body>

<footer>

Commit Types and Version Bumps

  • feat: New feature → minor version bump (0.5.0 → 0.6.0)
  • fix: Bug fix → patch version bump (0.5.0 → 0.5.1)
  • perf: Performance improvement → patch version bump
  • docs: Documentation only → no version bump
  • style: Code style/formatting → no version bump
  • refactor: Code refactoring → no version bump
  • test: Test changes → no version bump
  • build: Build system changes → no version bump
  • ci: CI/CD changes → no version bump
  • chore: Other changes → no version bump

Breaking Changes

Add ! after type/scope or include BREAKING CHANGE: in footer for major version bump:

feat(api)!: redesign authentication endpoints

BREAKING CHANGE: OAuth2 tokens now required instead of API keys

Result: 0.5.0 → 1.0.0

Scope Examples

  • api - REST API changes
  • ui - Frontend/UI changes
  • auth - Authentication
  • storage - Storage providers
  • ocr - OCR processing
  • tasks - Celery tasks
  • config - Configuration

Good Commit Examples

feat(storage): add Amazon S3 storage provider

Implements S3StorageProvider with upload, download, delete operations.
Includes configuration for bucket, region, and credentials.

Closes #123
fix(ocr): handle PDFs without text layer

Previously failed silently. Now properly processes through Azure.

Fixes #456
docs: update deployment guide with Docker Compose

Added step-by-step instructions for Docker Compose deployment.

Semantic Release Automation

DocuElevate uses python-semantic-release for automated version management.

How It Works

  1. PR merges to main with conventional commits
  2. semantic-release analyzes commit messages
  3. Automatic updates:
    • Bumps VERSION file
    • Updates CHANGELOG.md
    • Creates Git tag (e.g., v0.6.0)
    • Creates GitHub Release
    • Triggers Docker builds

Agent Rules

  • DO: Write conventional commit messages
  • DO: Use correct commit types
  • DO: Include BREAKING CHANGE: when applicable
  • DON'T: Manually edit VERSION file
  • DON'T: Manually edit CHANGELOG.md
  • DON'T: Create version tags or releases manually

Pull Requests

  1. Create PR with descriptive title (conventional format if single change)
  2. Fill out PR template
  3. Link related issues
  4. Ensure CI passes
  5. Request reviews
  6. Address feedback
  7. Merge when approved (commits retain conventional format)

Pre-commit Checklist

Before submitting code:

  • Code follows style guide (Ruff formatted)
  • Commit messages use conventional commit format
  • All tests pass (pytest)
  • New code has tests
  • Coverage doesn't decrease
  • Documentation updated if needed
  • No secrets or credentials in code
  • Linting passes (ruff check)
  • Type hints added (mypy clean)
  • No manual edits to VERSION or CHANGELOG.md
  • Security scan passed (included in ruff check)

Run full check:

pytest --cov=app
ruff check app/ tests/
ruff format --check app/ tests/
mypy app/

Note: This project uses Ruff, which replaces Black, Flake8, isort, and Bandit with a single, faster tool.


🤝 Agent Collaboration

When to Ask for Help

  • Breaking changes needed
  • Unsure about architecture decision
  • Security implications unclear
  • Performance impact unknown
  • Tests consistently failing

How to Document Changes

  1. Update relevant documentation
  2. Add comments for complex logic
  3. Update TODO.md if introducing tech debt
  4. Note breaking changes in commit message
  5. Update API documentation if endpoints changed

📞 Resources


This guide is a living document. Improvements welcome via PR!