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.
- docqwise/__init__.py +9 -0
- docqwise/_version.py +1 -0
- docqwise/agent/__init__.py +0 -0
- docqwise/benchmark/__init__.py +0 -0
- docqwise/bridges/__init__.py +0 -0
- docqwise/chunkers/__init__.py +0 -0
- docqwise/classifier/__init__.py +0 -0
- docqwise/cli/__init__.py +0 -0
- docqwise/cli/main.py +79 -0
- docqwise/comparator/__init__.py +0 -0
- docqwise/config.py +130 -0
- docqwise/connectors/__init__.py +0 -0
- docqwise/core/__init__.py +13 -0
- docqwise/core/bases.py +180 -0
- docqwise/core/chunk.py +80 -0
- docqwise/core/document.py +101 -0
- docqwise/core/element.py +163 -0
- docqwise/core/field.py +72 -0
- docqwise/databases/__init__.py +0 -0
- docqwise/embedders/__init__.py +0 -0
- docqwise/engine.py +377 -0
- docqwise/exceptions.py +49 -0
- docqwise/export/__init__.py +0 -0
- docqwise/extractors/__init__.py +0 -0
- docqwise/graph/__init__.py +0 -0
- docqwise/graph/backends/__init__.py +0 -0
- docqwise/incremental/__init__.py +0 -0
- docqwise/layout/__init__.py +0 -0
- docqwise/learning/__init__.py +0 -0
- docqwise/learning/correction.py +94 -0
- docqwise/llm/__init__.py +0 -0
- docqwise/ocr/__init__.py +0 -0
- docqwise/pipeline/__init__.py +0 -0
- docqwise/pipeline/prebuilt/__init__.py +0 -0
- docqwise/readers/__init__.py +3 -0
- docqwise/readers/auto_reader.py +127 -0
- docqwise/readers/base.py +21 -0
- docqwise/readers/csv_reader.py +3 -0
- docqwise/readers/docx_reader.py +3 -0
- docqwise/readers/email_reader.py +3 -0
- docqwise/readers/excel_reader.py +3 -0
- docqwise/readers/html_reader.py +3 -0
- docqwise/readers/image_reader.py +3 -0
- docqwise/readers/json_reader.py +3 -0
- docqwise/readers/parquet_reader.py +3 -0
- docqwise/readers/pdf_reader.py +79 -0
- docqwise/readers/pptx_reader.py +3 -0
- docqwise/readers/text_reader.py +3 -0
- docqwise/retrieval/__init__.py +0 -0
- docqwise/schema/__init__.py +0 -0
- docqwise/security/__init__.py +0 -0
- docqwise/server/__init__.py +0 -0
- docqwise/server/routes/__init__.py +0 -0
- docqwise/speed/__init__.py +0 -0
- docqwise/stores/__init__.py +0 -0
- docqwise/strategy/__init__.py +0 -0
- docqwise/templates/__init__.py +0 -0
- docqwise-0.1.0.dist-info/METADATA +338 -0
- docqwise-0.1.0.dist-info/RECORD +63 -0
- docqwise-0.1.0.dist-info/WHEEL +5 -0
- docqwise-0.1.0.dist-info/entry_points.txt +2 -0
- docqwise-0.1.0.dist-info/licenses/LICENSE +189 -0
- docqwise-0.1.0.dist-info/top_level.txt +1 -0
docqwise/__init__.py
ADDED
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
|
docqwise/cli/__init__.py
ADDED
|
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]
|