Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions extensions/lazerbeam47/numeric-date-consistency-auditor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Numeric & Date Consistency Auditor

## SuperDocs Round 2 — Assigned Build

This project contains the focused **Numeric and Date Consistency Auditor** implementation extracted from the DocuCheck engineering project.

The auditor is designed for financial/legal documents and detects inconsistencies in numbers and dates while preserving source evidence for review.

## What it does

### Numeric consistency

- Revenue / expense / profit arithmetic
- Totals and subtotals
- Percentage and margin consistency
- Repeated numeric values
- Numeric normalization
- Cross-document numeric conflicts
- Table-aware arithmetic where parsed tables are available

### Date consistency

- Invalid or reversed date ranges
- Deadline/date relationships
- Conflicting date values
- Cross-document date inconsistencies

### Evidence

Findings contain explanations and citations pointing back to the parsed source block/page/section where the value was found.

The implementation deliberately uses deterministic code for arithmetic and date comparison rather than asking an LLM to perform basic calculations.

## Architecture

```text
Document
|
v
Parser
|
v
Deterministic Fact Extraction
|
+----------------------+
| |
v v
Numeric Engine Date Engine
| |
+----------+-----------+
|
v
Conflict Detection
|
v
Findings + Evidence
```

## Example

Given:

```text
Revenue: $5,000,000
Expenses: $3,000,000
Net Income: $2,500,000
```

the arithmetic engine calculates:

```text
Expected Net Income = $5,000,000 - $3,000,000
= $2,000,000
```

and produces a finding for the $500,000 difference.

For dates:

```text
Effective Date: 20 August 2026
Expiry Date: 10 August 2026
```

the date engine flags the invalid ordering.

## Demo data

`backend/demo_data/` contains synthetic documents with intentionally planted inconsistencies.

No confidential or personal documents are used.

## Running the focused auditor tests

From the `backend` directory:

```bash
pip install -r requirements.txt
pytest tests/test_auditor_demo.py -q
```

The demo test exercises:

```text
parse
-> deterministic fact extraction
-> arithmetic audit
-> date audit
-> cited findings
```

## Important submission note

This folder represents the **auditor implementation already present in the DocuCheck project**.

The original uploaded repository did not contain a separate SuperDocs-facing adapter/PR package. If the SuperDocs submission requires an API/MCP integration and public `superdocs-builds` PR, that integration still needs to be added around this auditor.

## Design principle

Use deterministic validation for facts that can be mathematically or chronologically verified. Use an LLM only for tasks where semantic reasoning is useful, such as enrichment/explanation.

## License / submission

This code is being prepared as part of the SuperDocs Round 2 engineering task.

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Configuration-driven settings.

Every tunable lives here so no module hardcodes behaviour. Settings are read
once and injected (see :func:`get_settings`) rather than imported ad-hoc, which
keeps modules testable with overridden configuration.
"""

from __future__ import annotations

from functools import lru_cache
from pathlib import Path

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")

app_name: str = "SuperDocs Agentic Document Intelligence"
environment: str = "local"
log_level: str = "INFO"

database_url: str = "postgresql+psycopg://superdocs:superdocs@localhost:5432/superdocs"

# --- LLM ---------------------------------------------------------------
gemini_api_key: str | None = None
llm_model: str = "gemini-2.5-flash"
embedding_model: str = "text-embedding-004"
embedding_dim: int = 768
llm_max_retries: int = 3
llm_timeout_s: float = 60.0

# Deterministic mode: skips every paid API call. Used by CI and as the
# graceful-degradation path when no key is configured.
offline_mode: bool = False

# --- Storage -----------------------------------------------------------
storage_dir: Path = Path("./.storage")
watch_dir: Path = Path("./.inbox")
max_upload_mb: int = 50
allowed_extensions: tuple[str, ...] = (".pdf", ".docx", ".txt", ".md")

# --- Workflow ----------------------------------------------------------
node_max_retries: int = 3
node_backoff_s: float = 1.5
max_concurrent_runs: int = 8

# --- Cost model (USD per 1M tokens), configuration not code ------------
price_input_per_mtok: float = 0.30
price_output_per_mtok: float = 2.50
price_embedding_per_mtok: float = 0.15

# Confidence below which a finding is always routed to human review.
review_confidence_threshold: float = 0.85
# Absolute tolerance for float comparisons in the arithmetic engine.
arithmetic_tolerance: float = 0.01

api_keys: list[str] = Field(default_factory=list)

@property
def llm_enabled(self) -> bool:
return bool(self.gemini_api_key) and not self.offline_mode


@lru_cache
def get_settings() -> Settings:
settings = Settings()
settings.storage_dir.mkdir(parents=True, exist_ok=True)
settings.watch_dir.mkdir(parents=True, exist_ok=True)
return settings
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Typed error hierarchy.

Errors carry a `retryable` flag so the workflow engine can decide between
retrying a node and failing the run — the decision lives with the error, not
scattered across call sites.
"""

from __future__ import annotations


class SuperDocsError(Exception):
"""Base class for every application error."""

status_code: int = 500
retryable: bool = False
code: str = "internal_error"

def __init__(self, message: str, *, details: dict | None = None) -> None:
super().__init__(message)
self.message = message
self.details = details or {}

def to_dict(self) -> dict:
return {"code": self.code, "message": self.message, "details": self.details}


class ValidationError(SuperDocsError):
status_code = 422
code = "validation_error"


class UnsupportedFileType(ValidationError):
code = "unsupported_file_type"


class FileTooLarge(ValidationError):
code = "file_too_large"


class NotFoundError(SuperDocsError):
status_code = 404
code = "not_found"


class ConflictError(SuperDocsError):
status_code = 409
code = "conflict"


class ParsingError(SuperDocsError):
code = "parsing_error"
retryable = False


class LLMError(SuperDocsError):
code = "llm_error"
retryable = True


class LLMUnavailable(LLMError):
"""No key / offline mode. Callers must degrade gracefully, not crash."""

code = "llm_unavailable"
retryable = False


class PromptInjectionDetected(SuperDocsError):
status_code = 400
code = "prompt_injection_detected"
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Structured logging.

JSON logs with a correlation id bound per request/run so a single workflow can
be traced end-to-end across API, graph nodes and engines.
"""

from __future__ import annotations

import logging
import sys
from contextvars import ContextVar

import structlog

_correlation_id: ContextVar[str | None] = ContextVar("correlation_id", default=None)


def bind_correlation_id(value: str | None) -> None:
_correlation_id.set(value)


def _inject_correlation_id(_logger, _name, event_dict): # noqa: ANN001
cid = _correlation_id.get()
if cid:
event_dict.setdefault("correlation_id", cid)
return event_dict


def configure_logging(level: str = "INFO") -> None:
logging.basicConfig(format="%(message)s", stream=sys.stdout, level=level.upper())
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
_inject_correlation_id,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(
logging.getLevelName(level.upper())
),
cache_logger_on_first_use=True,
)


def get_logger(name: str) -> structlog.stdlib.BoundLogger:
return structlog.get_logger(name)
Loading