docqwise 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. docqwise/__init__.py +9 -0
  2. docqwise/_version.py +1 -0
  3. docqwise/agent/__init__.py +0 -0
  4. docqwise/benchmark/__init__.py +0 -0
  5. docqwise/bridges/__init__.py +0 -0
  6. docqwise/chunkers/__init__.py +0 -0
  7. docqwise/classifier/__init__.py +0 -0
  8. docqwise/cli/__init__.py +0 -0
  9. docqwise/cli/main.py +79 -0
  10. docqwise/comparator/__init__.py +0 -0
  11. docqwise/config.py +130 -0
  12. docqwise/connectors/__init__.py +0 -0
  13. docqwise/core/__init__.py +13 -0
  14. docqwise/core/bases.py +180 -0
  15. docqwise/core/chunk.py +80 -0
  16. docqwise/core/document.py +101 -0
  17. docqwise/core/element.py +163 -0
  18. docqwise/core/field.py +72 -0
  19. docqwise/databases/__init__.py +0 -0
  20. docqwise/embedders/__init__.py +0 -0
  21. docqwise/engine.py +377 -0
  22. docqwise/exceptions.py +49 -0
  23. docqwise/export/__init__.py +0 -0
  24. docqwise/extractors/__init__.py +0 -0
  25. docqwise/graph/__init__.py +0 -0
  26. docqwise/graph/backends/__init__.py +0 -0
  27. docqwise/incremental/__init__.py +0 -0
  28. docqwise/layout/__init__.py +0 -0
  29. docqwise/learning/__init__.py +0 -0
  30. docqwise/learning/correction.py +94 -0
  31. docqwise/llm/__init__.py +0 -0
  32. docqwise/ocr/__init__.py +0 -0
  33. docqwise/pipeline/__init__.py +0 -0
  34. docqwise/pipeline/prebuilt/__init__.py +0 -0
  35. docqwise/readers/__init__.py +3 -0
  36. docqwise/readers/auto_reader.py +127 -0
  37. docqwise/readers/base.py +21 -0
  38. docqwise/readers/csv_reader.py +3 -0
  39. docqwise/readers/docx_reader.py +3 -0
  40. docqwise/readers/email_reader.py +3 -0
  41. docqwise/readers/excel_reader.py +3 -0
  42. docqwise/readers/html_reader.py +3 -0
  43. docqwise/readers/image_reader.py +3 -0
  44. docqwise/readers/json_reader.py +3 -0
  45. docqwise/readers/parquet_reader.py +3 -0
  46. docqwise/readers/pdf_reader.py +79 -0
  47. docqwise/readers/pptx_reader.py +3 -0
  48. docqwise/readers/text_reader.py +3 -0
  49. docqwise/retrieval/__init__.py +0 -0
  50. docqwise/schema/__init__.py +0 -0
  51. docqwise/security/__init__.py +0 -0
  52. docqwise/server/__init__.py +0 -0
  53. docqwise/server/routes/__init__.py +0 -0
  54. docqwise/speed/__init__.py +0 -0
  55. docqwise/stores/__init__.py +0 -0
  56. docqwise/strategy/__init__.py +0 -0
  57. docqwise/templates/__init__.py +0 -0
  58. docqwise-0.1.0.dist-info/METADATA +338 -0
  59. docqwise-0.1.0.dist-info/RECORD +63 -0
  60. docqwise-0.1.0.dist-info/WHEEL +5 -0
  61. docqwise-0.1.0.dist-info/entry_points.txt +2 -0
  62. docqwise-0.1.0.dist-info/licenses/LICENSE +189 -0
  63. docqwise-0.1.0.dist-info/top_level.txt +1 -0
docqwise/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ DocQWise: Read. Extract. Retrieve.
3
+ Document intelligence that adapts, accelerates, and scales.
4
+ """
5
+
6
+ from docqwise._version import __version__
7
+ from docqwise.engine import Docqwise
8
+
9
+ __all__ = ["Docqwise", "__version__"]
docqwise/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
docqwise/cli/main.py ADDED
@@ -0,0 +1,79 @@
1
+ """DocQWise CLI."""
2
+ import click
3
+ from docqwise._version import __version__
4
+
5
+ @click.group()
6
+ @click.version_option(__version__, prog_name="docqwise")
7
+ def app():
8
+ """DocQWise: Read. Extract. Retrieve."""
9
+
10
+ @app.command()
11
+ def info():
12
+ """Show system info."""
13
+ click.echo(f"DocQWise v{__version__}")
14
+ click.echo("Read. Extract. Retrieve.")
15
+ click.echo("https://github.com/VK-Ant/docqwise")
16
+
17
+ @app.command()
18
+ @click.argument("source")
19
+ @click.option("--workers", default=1, help="Number of CPU workers")
20
+ @click.option("--progress/--no-progress", default=True)
21
+ def ingest(source, workers, progress):
22
+ """Ingest documents from a source."""
23
+ from docqwise import Docqwise
24
+ dq = Docqwise(workers=workers)
25
+ click.echo(f"Ingesting from: {source}")
26
+ dq.ingest(source)
27
+
28
+ @app.command()
29
+ @click.argument("source")
30
+ @click.option("--template", default=None, help="Extraction template name")
31
+ @click.option("--tables", is_flag=True, help="Extract tables only")
32
+ def extract(source, template, tables):
33
+ """Extract fields or tables from a document."""
34
+ from docqwise import Docqwise
35
+ dq = Docqwise()
36
+ if tables:
37
+ result = dq.extract_tables(source)
38
+ else:
39
+ result = dq.extract_fields(source, template=template)
40
+ click.echo(result)
41
+
42
+ @app.command()
43
+ @click.argument("query")
44
+ @click.option("--top-k", default=5)
45
+ def query(query, top_k):
46
+ """Semantic search across ingested documents."""
47
+ from docqwise import Docqwise
48
+ dq = Docqwise()
49
+ results = dq.retrieve(query, top_k=top_k)
50
+ click.echo(results)
51
+
52
+ @app.command()
53
+ @click.argument("question")
54
+ @click.option("--source", default=None)
55
+ def ask(question, source):
56
+ """Ask a question about your documents."""
57
+ from docqwise import Docqwise
58
+ dq = Docqwise()
59
+ answer = dq.ask(question, source=source)
60
+ click.echo(answer)
61
+
62
+ @app.command()
63
+ @click.option("--host", default="0.0.0.0")
64
+ @click.option("--port", default=8000)
65
+ @click.option("--workers", default=4)
66
+ def serve(host, port, workers):
67
+ """Start REST API server."""
68
+ click.echo(f"Starting DocQWise server on {host}:{port}")
69
+ click.echo("REST API server coming in v0.1.0")
70
+
71
+ @app.command()
72
+ @click.option("--port", default=8080)
73
+ def mcp(port):
74
+ """Start MCP server."""
75
+ click.echo(f"Starting DocQWise MCP server on port {port}")
76
+ click.echo("MCP server coming in v0.1.0")
77
+
78
+ if __name__ == "__main__":
79
+ app()
File without changes
docqwise/config.py ADDED
@@ -0,0 +1,130 @@
1
+ """Configuration system for docqwise."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Optional, Union
7
+
8
+
9
+ @dataclass
10
+ class StrategyConfig:
11
+ mode: str = "auto"
12
+ confidence_threshold: float = 0.85
13
+ max_api_cost_per_doc: float = 0.05
14
+
15
+
16
+ @dataclass
17
+ class SpeedConfig:
18
+ workers: Union[int, str] = 1
19
+ gpu_workers: int = 0
20
+ threads: int = 4
21
+ batch_size: int = 32
22
+ prefetch: bool = False
23
+ async_mode: bool = False
24
+ max_memory_gb: float = 0
25
+
26
+
27
+ @dataclass
28
+ class StorageConfig:
29
+ vector_store: str = "sqlite"
30
+ database: Optional[str] = None
31
+ graph_store: Optional[str] = None
32
+ store_path: str = "./docqwise_db"
33
+ cache: bool = True
34
+ cache_dir: str = ".docqwise_cache"
35
+
36
+
37
+ @dataclass
38
+ class ModelConfig:
39
+ ocr: str = "easyocr"
40
+ layout_model: str = "yolov8"
41
+ embedder: str = "all-MiniLM-L6-v2"
42
+ llm: Optional[str] = None
43
+ reranker: Optional[str] = None
44
+
45
+
46
+ @dataclass
47
+ class SearchConfig:
48
+ mode: str = "vector"
49
+ default_top_k: int = 5
50
+ rerank: bool = False
51
+ rerank_top_k: int = 5
52
+
53
+
54
+ @dataclass
55
+ class IncrementalConfig:
56
+ enabled: bool = True
57
+ change_detection: str = "hash"
58
+ checkpoint_interval: int = 100
59
+
60
+
61
+ @dataclass
62
+ class ChunkerConfig:
63
+ strategy: str = "structure"
64
+ max_chunk_tokens: int = 512
65
+ overlap_tokens: int = 50
66
+ preserve_tables: bool = True
67
+ attach_headings: bool = True
68
+ attach_page_context: bool = True
69
+
70
+
71
+ @dataclass
72
+ class DocqwiseConfig:
73
+ """Master configuration."""
74
+
75
+ strategy: StrategyConfig = field(default_factory=StrategyConfig)
76
+ speed: SpeedConfig = field(default_factory=SpeedConfig)
77
+ storage: StorageConfig = field(default_factory=StorageConfig)
78
+ models: ModelConfig = field(default_factory=ModelConfig)
79
+ search: SearchConfig = field(default_factory=SearchConfig)
80
+ incremental: IncrementalConfig = field(default_factory=IncrementalConfig)
81
+ chunker: ChunkerConfig = field(default_factory=ChunkerConfig)
82
+
83
+ @classmethod
84
+ def from_yaml(cls, path: str) -> DocqwiseConfig:
85
+ import yaml
86
+
87
+ with open(path) as f:
88
+ data = yaml.safe_load(f)
89
+ return cls._from_dict(data)
90
+
91
+ @classmethod
92
+ def from_json(cls, path: str) -> DocqwiseConfig:
93
+ import json
94
+
95
+ with open(path) as f:
96
+ data = json.load(f)
97
+ return cls._from_dict(data)
98
+
99
+ @classmethod
100
+ def _from_dict(cls, data: dict) -> DocqwiseConfig:
101
+ config = cls()
102
+ if "strategy" in data:
103
+ config.strategy = StrategyConfig(**data["strategy"])
104
+ if "speed" in data:
105
+ config.speed = SpeedConfig(**data["speed"])
106
+ if "storage" in data:
107
+ config.storage = StorageConfig(**data["storage"])
108
+ if "models" in data:
109
+ config.models = ModelConfig(**data["models"])
110
+ if "search" in data:
111
+ config.search = SearchConfig(**data["search"])
112
+ if "incremental" in data:
113
+ config.incremental = IncrementalConfig(**data["incremental"])
114
+ if "chunker" in data:
115
+ config.chunker = ChunkerConfig(**data["chunker"])
116
+ return config
117
+
118
+ def to_yaml(self, path: str) -> None:
119
+ import yaml
120
+ from dataclasses import asdict
121
+
122
+ with open(path, "w") as f:
123
+ yaml.dump(asdict(self), f, default_flow_style=False)
124
+
125
+ def to_json(self, path: str) -> None:
126
+ import json
127
+ from dataclasses import asdict
128
+
129
+ with open(path, "w") as f:
130
+ json.dump(asdict(self), f, indent=2)
File without changes
@@ -0,0 +1,13 @@
1
+ """Core data models for docqwise."""
2
+
3
+ from docqwise.core.document import DocqwiseDocument, DocumentMetadata, DocumentType, SourceType
4
+ from docqwise.core.chunk import DocqwiseChunk, ChunkMetadata, ElementType, BoundingBox
5
+ from docqwise.core.field import FieldResult, ExtractionResult
6
+ from docqwise.core.element import Table, TableCell, ExtractedImage, Entity, Relation, FormField
7
+
8
+ __all__ = [
9
+ "DocqwiseDocument", "DocumentMetadata", "DocumentType", "SourceType",
10
+ "DocqwiseChunk", "ChunkMetadata", "ElementType", "BoundingBox",
11
+ "FieldResult", "ExtractionResult",
12
+ "Table", "TableCell", "ExtractedImage", "Entity", "Relation", "FormField",
13
+ ]
docqwise/core/bases.py ADDED
@@ -0,0 +1,180 @@
1
+ """All pluggable base classes for docqwise.
2
+
3
+ Every component is a Base* class. Swap anything. Test anything. Deploy anywhere.
4
+ Import from the specific module (e.g. docqwise.ocr.base) for implementation.
5
+ This file serves as a single reference for all interfaces.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any, Optional
10
+
11
+ import numpy as np
12
+
13
+ from docqwise.core.document import DocqwiseDocument
14
+ from docqwise.core.chunk import DocqwiseChunk, BoundingBox
15
+ from docqwise.core.field import ExtractionResult
16
+ from docqwise.core.element import Table, Entity, Relation, FormField
17
+
18
+
19
+ # ── PROCESSING LAYER ──
20
+
21
+
22
+ class BaseReader(ABC):
23
+ @abstractmethod
24
+ def can_read(self, path: str) -> bool: ...
25
+ @abstractmethod
26
+ def read(self, path: str, **kwargs) -> DocqwiseDocument: ...
27
+ @abstractmethod
28
+ def supported_formats(self) -> list[str]: ...
29
+
30
+
31
+ class BaseOCREngine(ABC):
32
+ @abstractmethod
33
+ def ocr_page(self, image: np.ndarray, language: str = "en") -> dict: ...
34
+ @abstractmethod
35
+ def ocr_batch(self, images: list[np.ndarray], **kwargs) -> list[dict]: ...
36
+ @abstractmethod
37
+ def supported_languages(self) -> list[str]: ...
38
+
39
+
40
+ class BaseLayoutModel(ABC):
41
+ @abstractmethod
42
+ def detect_layout(self, image: np.ndarray) -> list[dict]: ...
43
+ @abstractmethod
44
+ def detect_batch(self, images: list[np.ndarray]) -> list[list[dict]]: ...
45
+
46
+
47
+ class BaseExtractor(ABC):
48
+ @abstractmethod
49
+ def extract(self, document: DocqwiseDocument, **kwargs) -> DocqwiseDocument: ...
50
+
51
+
52
+ class BaseTableExtractor(ABC):
53
+ @abstractmethod
54
+ def extract_tables(self, document: DocqwiseDocument, **kwargs) -> list[Table]: ...
55
+
56
+
57
+ class BaseFormExtractor(ABC):
58
+ @abstractmethod
59
+ def extract_form(self, document: DocqwiseDocument) -> dict[str, FormField]: ...
60
+
61
+
62
+ class BaseChunker(ABC):
63
+ @abstractmethod
64
+ def chunk(self, document: DocqwiseDocument, **kwargs) -> list[DocqwiseChunk]: ...
65
+
66
+
67
+ class BaseEmbedder(ABC):
68
+ @abstractmethod
69
+ def embed(self, text: str) -> np.ndarray: ...
70
+ @abstractmethod
71
+ def embed_batch(self, texts: list[str]) -> np.ndarray: ...
72
+ @abstractmethod
73
+ def dimension(self) -> int: ...
74
+
75
+
76
+ # ── STORAGE LAYER ──
77
+
78
+
79
+ class BaseVectorStore(ABC):
80
+ @abstractmethod
81
+ def insert(self, chunks: list[DocqwiseChunk]) -> None: ...
82
+ @abstractmethod
83
+ def search(self, query_embedding: np.ndarray, top_k: int = 5,
84
+ filters: Optional[dict] = None) -> list[dict]: ...
85
+ @abstractmethod
86
+ def delete(self, doc_id: str) -> None: ...
87
+ @abstractmethod
88
+ def update(self, doc_id: str, chunks: list[DocqwiseChunk]) -> None: ...
89
+ @abstractmethod
90
+ def count(self) -> int: ...
91
+
92
+
93
+ class BaseDatabaseStore(ABC):
94
+ @abstractmethod
95
+ def connect(self, connection_string: str, **kwargs) -> None: ...
96
+ @abstractmethod
97
+ def insert(self, data: dict, table: str) -> None: ...
98
+ @abstractmethod
99
+ def query(self, query: str, params: Optional[dict] = None) -> list[dict]: ...
100
+ @abstractmethod
101
+ def schema(self, table: Optional[str] = None) -> dict: ...
102
+ @abstractmethod
103
+ def tables(self) -> list[str]: ...
104
+
105
+
106
+ class BaseGraphStore(ABC):
107
+ @abstractmethod
108
+ def add_node(self, node_id: str, node_type: str, properties: dict) -> None: ...
109
+ @abstractmethod
110
+ def add_edge(self, from_id: str, to_id: str, relation: str,
111
+ properties: Optional[dict] = None) -> None: ...
112
+ @abstractmethod
113
+ def query(self, query: str) -> list[dict]: ...
114
+ @abstractmethod
115
+ def neighbors(self, node_id: str, hops: int = 1) -> list[dict]: ...
116
+ @abstractmethod
117
+ def communities(self) -> list[list[str]]: ...
118
+
119
+
120
+ # ── INTELLIGENCE LAYER ──
121
+
122
+
123
+ class BaseLLM(ABC):
124
+ @abstractmethod
125
+ def generate(self, prompt: str, **kwargs) -> str: ...
126
+ @abstractmethod
127
+ def generate_structured(self, prompt: str, schema: dict) -> dict: ...
128
+
129
+
130
+ class BaseReranker(ABC):
131
+ @abstractmethod
132
+ def rerank(self, query: str, documents: list[str],
133
+ top_k: int = 5) -> list[dict]: ...
134
+
135
+
136
+ class BaseClassifier(ABC):
137
+ @abstractmethod
138
+ def classify(self, document: DocqwiseDocument,
139
+ labels: Optional[list[str]] = None) -> list[dict]: ...
140
+
141
+
142
+ class BaseComparator(ABC):
143
+ @abstractmethod
144
+ def compare(self, doc_a: DocqwiseDocument,
145
+ doc_b: DocqwiseDocument) -> dict: ...
146
+
147
+
148
+ # ── INFRASTRUCTURE LAYER ──
149
+
150
+
151
+ class BaseConnector(ABC):
152
+ @abstractmethod
153
+ def list_files(self, path: str, **kwargs) -> list[str]: ...
154
+ @abstractmethod
155
+ def read_file(self, path: str) -> bytes: ...
156
+ @abstractmethod
157
+ def watch(self, path: str, callback: Any, interval: int = 60) -> None: ...
158
+
159
+
160
+ class BaseCacheLayer(ABC):
161
+ @abstractmethod
162
+ def get(self, key: str) -> Optional[Any]: ...
163
+ @abstractmethod
164
+ def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: ...
165
+ @abstractmethod
166
+ def invalidate(self, key: str) -> None: ...
167
+
168
+
169
+ class BaseExporter(ABC):
170
+ @abstractmethod
171
+ def export(self, documents: list[DocqwiseDocument], path: str, **kwargs) -> None: ...
172
+
173
+
174
+ class BaseTemplate(ABC):
175
+ @abstractmethod
176
+ def schema(self) -> dict: ...
177
+ @abstractmethod
178
+ def extract(self, document: DocqwiseDocument) -> ExtractionResult: ...
179
+ @abstractmethod
180
+ def validate(self, result: ExtractionResult) -> dict: ...
docqwise/core/chunk.py ADDED
@@ -0,0 +1,80 @@
1
+ """Chunk representation for retrieval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any, Optional
8
+
9
+ import numpy as np
10
+
11
+
12
+ class ElementType(str, Enum):
13
+ TEXT = "text"
14
+ TABLE = "table"
15
+ IMAGE = "image"
16
+ HEADING = "heading"
17
+ LIST = "list"
18
+ CAPTION = "caption"
19
+ FORM_FIELD = "form_field"
20
+ CODE = "code"
21
+ EQUATION = "equation"
22
+
23
+
24
+ @dataclass
25
+ class BoundingBox:
26
+ """Bounding box coordinates on a page."""
27
+
28
+ x1: float
29
+ y1: float
30
+ x2: float
31
+ y2: float
32
+ page: int = 0
33
+
34
+ @property
35
+ def width(self) -> float:
36
+ return self.x2 - self.x1
37
+
38
+ @property
39
+ def height(self) -> float:
40
+ return self.y2 - self.y1
41
+
42
+ @property
43
+ def area(self) -> float:
44
+ return self.width * self.height
45
+
46
+ def to_list(self) -> list[float]:
47
+ return [self.x1, self.y1, self.x2, self.y2]
48
+
49
+
50
+ @dataclass
51
+ class ChunkMetadata:
52
+ """Metadata attached to a chunk."""
53
+
54
+ doc_id: str = ""
55
+ source_path: str = ""
56
+ page_num: int = 0
57
+ chunk_index: int = 0
58
+ element_type: ElementType = ElementType.TEXT
59
+ heading_context: str = ""
60
+ section_path: list[str] = field(default_factory=list)
61
+ bbox: Optional[BoundingBox] = None
62
+ confidence: float = 1.0
63
+ custom: dict[str, Any] = field(default_factory=dict)
64
+
65
+
66
+ @dataclass
67
+ class DocqwiseChunk:
68
+ """A retrieval-ready piece of a document."""
69
+
70
+ chunk_id: str
71
+ text: str
72
+ embedding: Optional[np.ndarray] = None
73
+ metadata: ChunkMetadata = field(default_factory=ChunkMetadata)
74
+
75
+ # For table chunks
76
+ table_data: Optional[dict] = None
77
+
78
+ # For image chunks
79
+ image_path: Optional[str] = None
80
+ image_description: str = ""
@@ -0,0 +1,101 @@
1
+ """Core document representation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from typing import Any, Optional
10
+
11
+
12
+ class DocumentType(str, Enum):
13
+ PDF = "pdf"
14
+ DOCX = "docx"
15
+ IMAGE = "image"
16
+ EXCEL = "excel"
17
+ CSV = "csv"
18
+ JSON = "json"
19
+ XML = "xml"
20
+ YAML = "yaml"
21
+ HTML = "html"
22
+ PPTX = "pptx"
23
+ EMAIL = "email"
24
+ TEXT = "text"
25
+ PARQUET = "parquet"
26
+ DATABASE = "database"
27
+ RTF = "rtf"
28
+ MARKDOWN = "markdown"
29
+ UNKNOWN = "unknown"
30
+
31
+
32
+ class SourceType(str, Enum):
33
+ FILE = "file"
34
+ URL = "url"
35
+ DATABASE = "database"
36
+ CLOUD_STORAGE = "cloud_storage"
37
+ EMAIL = "email"
38
+ STREAM = "stream"
39
+
40
+
41
+ @dataclass
42
+ class DocumentMetadata:
43
+ """Metadata extracted from a document."""
44
+
45
+ title: Optional[str] = None
46
+ author: Optional[str] = None
47
+ created_at: Optional[datetime] = None
48
+ modified_at: Optional[datetime] = None
49
+ page_count: int = 0
50
+ word_count: int = 0
51
+ language: Optional[str] = None
52
+ file_size_bytes: int = 0
53
+ file_hash: str = ""
54
+ is_scanned: bool = False
55
+ has_selectable_text: bool = True
56
+ custom: dict[str, Any] = field(default_factory=dict)
57
+
58
+
59
+ @dataclass
60
+ class PageContent:
61
+ """Content of a single page."""
62
+
63
+ page_num: int
64
+ text: str = ""
65
+ width: float = 0.0
66
+ height: float = 0.0
67
+ elements: list[Any] = field(default_factory=list)
68
+ is_scanned: bool = False
69
+
70
+
71
+ @dataclass
72
+ class DocqwiseDocument:
73
+ """Core document representation output of reader and extractors."""
74
+
75
+ doc_id: str
76
+ source_path: str
77
+ source_type: SourceType
78
+ doc_type: DocumentType
79
+ metadata: DocumentMetadata = field(default_factory=DocumentMetadata)
80
+
81
+ # Extracted content
82
+ text: str = ""
83
+ pages: list[PageContent] = field(default_factory=list)
84
+ tables: list[Any] = field(default_factory=list)
85
+ images: list[Any] = field(default_factory=list)
86
+ fields: dict[str, Any] = field(default_factory=dict)
87
+ entities: list[Any] = field(default_factory=list)
88
+ relations: list[Any] = field(default_factory=list)
89
+ form_fields: dict[str, Any] = field(default_factory=dict)
90
+
91
+ # Processing state
92
+ extraction_strategy: str = ""
93
+ extraction_confidence: float = 0.0
94
+ processing_time_ms: float = 0.0
95
+ extracted_at: Optional[datetime] = None
96
+
97
+ @staticmethod
98
+ def generate_id(source_path: str, content_hash: str = "") -> str:
99
+ """Generate a unique document ID."""
100
+ raw = f"{source_path}:{content_hash}"
101
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]