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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Simple correction store — exact overrides, no ML."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sqlite3
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CorrectionStore:
|
|
13
|
+
"""Stores user corrections as exact overrides. No RL. No weights. Deterministic."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, store_path: str = "./docqwise_db"):
|
|
16
|
+
os.makedirs(store_path, exist_ok=True)
|
|
17
|
+
self._db_path = os.path.join(store_path, "corrections.db")
|
|
18
|
+
self._init_db()
|
|
19
|
+
|
|
20
|
+
def _init_db(self) -> None:
|
|
21
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
22
|
+
conn.execute("""
|
|
23
|
+
CREATE TABLE IF NOT EXISTS corrections (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
doc_id TEXT,
|
|
26
|
+
source_path TEXT,
|
|
27
|
+
field_name TEXT NOT NULL,
|
|
28
|
+
wrong_value TEXT,
|
|
29
|
+
correct_value TEXT NOT NULL,
|
|
30
|
+
scope TEXT DEFAULT 'global',
|
|
31
|
+
vendor TEXT,
|
|
32
|
+
doc_type TEXT,
|
|
33
|
+
created_at TEXT NOT NULL
|
|
34
|
+
)
|
|
35
|
+
""")
|
|
36
|
+
conn.execute("""
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_corrections_field
|
|
38
|
+
ON corrections(field_name, scope, vendor)
|
|
39
|
+
""")
|
|
40
|
+
|
|
41
|
+
def save(self, doc_id: str, source_path: str,
|
|
42
|
+
corrections: dict[str, Any], scope: str = "global") -> None:
|
|
43
|
+
"""Save corrections as exact overrides."""
|
|
44
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
45
|
+
for field_name, correct_value in corrections.items():
|
|
46
|
+
conn.execute(
|
|
47
|
+
"""INSERT INTO corrections
|
|
48
|
+
(doc_id, source_path, field_name, correct_value, scope, created_at)
|
|
49
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
50
|
+
(doc_id, source_path, field_name,
|
|
51
|
+
json.dumps(correct_value, default=str),
|
|
52
|
+
scope, datetime.now().isoformat()),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def apply(self, doc_type: str, fields: dict[str, Any],
|
|
56
|
+
vendor: Optional[str] = None) -> dict[str, Any]:
|
|
57
|
+
"""Apply stored corrections as exact overrides. No scoring. No probability."""
|
|
58
|
+
corrected = dict(fields)
|
|
59
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
60
|
+
for field_name in fields:
|
|
61
|
+
rows = conn.execute(
|
|
62
|
+
"""SELECT correct_value FROM corrections
|
|
63
|
+
WHERE field_name = ? AND (scope = 'global' OR vendor = ?)
|
|
64
|
+
ORDER BY created_at DESC LIMIT 1""",
|
|
65
|
+
(field_name, vendor or ""),
|
|
66
|
+
).fetchall()
|
|
67
|
+
if rows:
|
|
68
|
+
corrected[field_name] = json.loads(rows[0][0])
|
|
69
|
+
return corrected
|
|
70
|
+
|
|
71
|
+
def list_corrections(self, doc_type: Optional[str] = None) -> list[dict]:
|
|
72
|
+
"""List all stored corrections."""
|
|
73
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
74
|
+
conn.row_factory = sqlite3.Row
|
|
75
|
+
if doc_type:
|
|
76
|
+
rows = conn.execute(
|
|
77
|
+
"SELECT * FROM corrections WHERE doc_type = ? ORDER BY created_at DESC",
|
|
78
|
+
(doc_type,),
|
|
79
|
+
).fetchall()
|
|
80
|
+
else:
|
|
81
|
+
rows = conn.execute(
|
|
82
|
+
"SELECT * FROM corrections ORDER BY created_at DESC"
|
|
83
|
+
).fetchall()
|
|
84
|
+
return [dict(row) for row in rows]
|
|
85
|
+
|
|
86
|
+
def delete_correction(self, correction_id: int) -> None:
|
|
87
|
+
"""Remove a correction."""
|
|
88
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
89
|
+
conn.execute("DELETE FROM corrections WHERE id = ?", (correction_id,))
|
|
90
|
+
|
|
91
|
+
def count(self) -> int:
|
|
92
|
+
"""Count total corrections."""
|
|
93
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
94
|
+
return conn.execute("SELECT COUNT(*) FROM corrections").fetchone()[0]
|
docqwise/llm/__init__.py
ADDED
|
File without changes
|
docqwise/ocr/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Auto-detect document format and dispatch to appropriate reader."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from docqwise.core.document import DocqwiseDocument, DocumentType, SourceType
|
|
9
|
+
from docqwise.readers.base import BaseReader
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
FORMAT_MAP = {
|
|
13
|
+
".pdf": DocumentType.PDF,
|
|
14
|
+
".doc": DocumentType.DOCX,
|
|
15
|
+
".docx": DocumentType.DOCX,
|
|
16
|
+
".rtf": DocumentType.RTF,
|
|
17
|
+
".txt": DocumentType.TEXT,
|
|
18
|
+
".md": DocumentType.MARKDOWN,
|
|
19
|
+
".rst": DocumentType.TEXT,
|
|
20
|
+
".log": DocumentType.TEXT,
|
|
21
|
+
".html": DocumentType.HTML,
|
|
22
|
+
".htm": DocumentType.HTML,
|
|
23
|
+
".xhtml": DocumentType.HTML,
|
|
24
|
+
".jpg": DocumentType.IMAGE,
|
|
25
|
+
".jpeg": DocumentType.IMAGE,
|
|
26
|
+
".png": DocumentType.IMAGE,
|
|
27
|
+
".tiff": DocumentType.IMAGE,
|
|
28
|
+
".tif": DocumentType.IMAGE,
|
|
29
|
+
".bmp": DocumentType.IMAGE,
|
|
30
|
+
".webp": DocumentType.IMAGE,
|
|
31
|
+
".heic": DocumentType.IMAGE,
|
|
32
|
+
".xlsx": DocumentType.EXCEL,
|
|
33
|
+
".xls": DocumentType.EXCEL,
|
|
34
|
+
".xlsm": DocumentType.EXCEL,
|
|
35
|
+
".ods": DocumentType.EXCEL,
|
|
36
|
+
".csv": DocumentType.CSV,
|
|
37
|
+
".tsv": DocumentType.CSV,
|
|
38
|
+
".json": DocumentType.JSON,
|
|
39
|
+
".jsonl": DocumentType.JSON,
|
|
40
|
+
".xml": DocumentType.XML,
|
|
41
|
+
".yaml": DocumentType.YAML,
|
|
42
|
+
".yml": DocumentType.YAML,
|
|
43
|
+
".toml": DocumentType.YAML,
|
|
44
|
+
".parquet": DocumentType.PARQUET,
|
|
45
|
+
".pptx": DocumentType.PPTX,
|
|
46
|
+
".ppt": DocumentType.PPTX,
|
|
47
|
+
".eml": DocumentType.EMAIL,
|
|
48
|
+
".msg": DocumentType.EMAIL,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AutoReader(BaseReader):
|
|
53
|
+
"""Auto-detects document format and dispatches to the correct reader."""
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self._readers: dict[DocumentType, BaseReader] = {}
|
|
57
|
+
|
|
58
|
+
def can_read(self, path: str) -> bool:
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def supported_formats(self) -> list[str]:
|
|
62
|
+
return list(FORMAT_MAP.keys())
|
|
63
|
+
|
|
64
|
+
def detect_type(self, path: str) -> DocumentType:
|
|
65
|
+
"""Detect document type from file extension and magic bytes."""
|
|
66
|
+
if path.startswith(("postgresql://", "mysql://", "sqlite:///",
|
|
67
|
+
"mongodb://", "mongodb+srv://")):
|
|
68
|
+
return DocumentType.DATABASE
|
|
69
|
+
|
|
70
|
+
ext = os.path.splitext(path)[1].lower()
|
|
71
|
+
return FORMAT_MAP.get(ext, DocumentType.UNKNOWN)
|
|
72
|
+
|
|
73
|
+
def detect_source_type(self, path: str) -> SourceType:
|
|
74
|
+
"""Detect source type from path."""
|
|
75
|
+
if path.startswith(("http://", "https://")):
|
|
76
|
+
return SourceType.URL
|
|
77
|
+
if path.startswith(("postgresql://", "mysql://", "sqlite:///", "mongodb://")):
|
|
78
|
+
return SourceType.DATABASE
|
|
79
|
+
if path.startswith(("s3://", "gs://", "az://")):
|
|
80
|
+
return SourceType.CLOUD_STORAGE
|
|
81
|
+
if path.startswith(("imap://", "pop3://")):
|
|
82
|
+
return SourceType.EMAIL
|
|
83
|
+
return SourceType.FILE
|
|
84
|
+
|
|
85
|
+
def _get_reader(self, doc_type: DocumentType) -> BaseReader:
|
|
86
|
+
"""Get or lazy-load the reader for a document type."""
|
|
87
|
+
if doc_type in self._readers:
|
|
88
|
+
return self._readers[doc_type]
|
|
89
|
+
|
|
90
|
+
reader: Optional[BaseReader] = None
|
|
91
|
+
|
|
92
|
+
if doc_type == DocumentType.PDF:
|
|
93
|
+
from docqwise.readers.pdf_reader import PDFReader
|
|
94
|
+
reader = PDFReader()
|
|
95
|
+
elif doc_type == DocumentType.DOCX:
|
|
96
|
+
from docqwise.readers.docx_reader import DOCXReader
|
|
97
|
+
reader = DOCXReader()
|
|
98
|
+
elif doc_type == DocumentType.TEXT or doc_type == DocumentType.MARKDOWN:
|
|
99
|
+
from docqwise.readers.text_reader import TextReader
|
|
100
|
+
reader = TextReader()
|
|
101
|
+
elif doc_type == DocumentType.IMAGE:
|
|
102
|
+
from docqwise.readers.image_reader import ImageReader
|
|
103
|
+
reader = ImageReader()
|
|
104
|
+
elif doc_type in (DocumentType.CSV, DocumentType.EXCEL):
|
|
105
|
+
from docqwise.readers.csv_reader import CSVReader
|
|
106
|
+
reader = CSVReader()
|
|
107
|
+
elif doc_type in (DocumentType.JSON, DocumentType.XML, DocumentType.YAML):
|
|
108
|
+
from docqwise.readers.json_reader import JSONReader
|
|
109
|
+
reader = JSONReader()
|
|
110
|
+
|
|
111
|
+
if reader:
|
|
112
|
+
self._readers[doc_type] = reader
|
|
113
|
+
|
|
114
|
+
return reader
|
|
115
|
+
|
|
116
|
+
def read(self, path: str, **kwargs) -> DocqwiseDocument:
|
|
117
|
+
"""Read a document by auto-detecting its format."""
|
|
118
|
+
doc_type = self.detect_type(path)
|
|
119
|
+
reader = self._get_reader(doc_type)
|
|
120
|
+
|
|
121
|
+
if reader is None:
|
|
122
|
+
from docqwise.exceptions import UnsupportedFormatError
|
|
123
|
+
raise UnsupportedFormatError(
|
|
124
|
+
f"No reader available for {doc_type.value} format: {path}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return reader.read(path, **kwargs)
|
docqwise/readers/base.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Base reader abstract class."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from docqwise.core.document import DocqwiseDocument
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseReader(ABC):
|
|
9
|
+
"""Abstract base class for document readers. Implement to add new format support."""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def can_read(self, path: str) -> bool:
|
|
13
|
+
"""Return True if this reader can handle the given path or format."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def read(self, path: str, **kwargs) -> DocqwiseDocument:
|
|
17
|
+
"""Read a document and return a DocqwiseDocument."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def supported_formats(self) -> list[str]:
|
|
21
|
+
"""Return list of supported file extensions (e.g. ['.pdf', '.PDF'])."""
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""PDF reader using PyMuPDF."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import fitz # PyMuPDF
|
|
9
|
+
|
|
10
|
+
from docqwise.core.document import (
|
|
11
|
+
DocqwiseDocument, DocumentMetadata, DocumentType,
|
|
12
|
+
PageContent, SourceType,
|
|
13
|
+
)
|
|
14
|
+
from docqwise.readers.base import BaseReader
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PDFReader(BaseReader):
|
|
18
|
+
"""Read PDF documents using PyMuPDF (fitz)."""
|
|
19
|
+
|
|
20
|
+
def can_read(self, path: str) -> bool:
|
|
21
|
+
return path.lower().endswith((".pdf",))
|
|
22
|
+
|
|
23
|
+
def supported_formats(self) -> list[str]:
|
|
24
|
+
return [".pdf"]
|
|
25
|
+
|
|
26
|
+
def read(self, path: str, **kwargs) -> DocqwiseDocument:
|
|
27
|
+
"""Read a PDF and return DocqwiseDocument with text and metadata."""
|
|
28
|
+
doc = fitz.open(path)
|
|
29
|
+
file_size = os.path.getsize(path)
|
|
30
|
+
|
|
31
|
+
with open(path, "rb") as f:
|
|
32
|
+
file_hash = hashlib.sha256(f.read()).hexdigest()[:16]
|
|
33
|
+
|
|
34
|
+
pages = []
|
|
35
|
+
full_text_parts = []
|
|
36
|
+
total_words = 0
|
|
37
|
+
has_text = False
|
|
38
|
+
|
|
39
|
+
for page_num in range(len(doc)):
|
|
40
|
+
page = doc[page_num]
|
|
41
|
+
text = page.get_text("text")
|
|
42
|
+
if text.strip():
|
|
43
|
+
has_text = True
|
|
44
|
+
|
|
45
|
+
words = text.split()
|
|
46
|
+
total_words += len(words)
|
|
47
|
+
full_text_parts.append(text)
|
|
48
|
+
|
|
49
|
+
pages.append(PageContent(
|
|
50
|
+
page_num=page_num + 1,
|
|
51
|
+
text=text,
|
|
52
|
+
width=page.rect.width,
|
|
53
|
+
height=page.rect.height,
|
|
54
|
+
is_scanned=not bool(text.strip()),
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
metadata = DocumentMetadata(
|
|
58
|
+
title=doc.metadata.get("title", ""),
|
|
59
|
+
author=doc.metadata.get("author", ""),
|
|
60
|
+
page_count=len(doc),
|
|
61
|
+
word_count=total_words,
|
|
62
|
+
file_size_bytes=file_size,
|
|
63
|
+
file_hash=file_hash,
|
|
64
|
+
is_scanned=not has_text,
|
|
65
|
+
has_selectable_text=has_text,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
doc_id = DocqwiseDocument.generate_id(path, file_hash)
|
|
69
|
+
doc.close()
|
|
70
|
+
|
|
71
|
+
return DocqwiseDocument(
|
|
72
|
+
doc_id=doc_id,
|
|
73
|
+
source_path=path,
|
|
74
|
+
source_type=SourceType.FILE,
|
|
75
|
+
doc_type=DocumentType.PDF,
|
|
76
|
+
metadata=metadata,
|
|
77
|
+
text="\n".join(full_text_parts),
|
|
78
|
+
pages=pages,
|
|
79
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|