Skip to content

Commit 287c7f8

Browse files
author
Daniele Briggi
committed
feat: refactory, tests and logger
1 parent 65fb654 commit 287c7f8

5 files changed

Lines changed: 194 additions & 35 deletions

File tree

src/sqlite_rag/logger.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from enum import Enum
2+
from typing import Callable, Optional
3+
4+
5+
class LogLevel(Enum):
6+
DEBUG = 1
7+
INFO = 2
8+
WARNING = 3
9+
ERROR = 4
10+
11+
12+
class Logger:
13+
def __init__(
14+
self,
15+
level: LogLevel = LogLevel.INFO,
16+
output_fn: Optional[Callable[[str], None]] = None,
17+
):
18+
self.level = level
19+
self.output_fn = output_fn or print
20+
21+
def debug(self, message: str):
22+
if self.level.value <= LogLevel.DEBUG.value:
23+
self.output_fn(f"DEBUG: {message}")
24+
25+
def info(self, message: str):
26+
if self.level.value <= LogLevel.INFO.value:
27+
self.output_fn(f"{message}")
28+
29+
def warning(self, message: str):
30+
if self.level.value <= LogLevel.WARNING.value:
31+
self.output_fn(f"WARNING: {message}")
32+
33+
def error(self, message: str):
34+
if self.level.value <= LogLevel.ERROR.value:
35+
self.output_fn(f"ERROR: {message}")

src/sqlite_rag/reader.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,27 @@ def parse_file(path: Path) -> str:
5151
return reader.convert(path).text_content
5252
except Exception as exc:
5353
raise ValueError(f"Failed to parse file {path}") from exc
54+
55+
@staticmethod
56+
def collect_files(path: Path, recursive: bool = False) -> list[Path]:
57+
"""Collect files from the path, optionally recursively"""
58+
if not path.exists():
59+
raise FileNotFoundError(f"{path} does not exist.")
60+
61+
if path.is_file() and FileReader.is_supported(path):
62+
return [path]
63+
64+
files_to_process = []
65+
if path.is_dir():
66+
if recursive:
67+
files_to_process = list(path.rglob("*"))
68+
else:
69+
files_to_process = list(path.glob("*"))
70+
71+
files_to_process = [
72+
f
73+
for f in files_to_process
74+
if f.is_file() and FileReader.is_supported(f)
75+
]
76+
77+
return files_to_process

src/sqlite_rag/sqliterag.py

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import glob
2-
from pathlib import Path
1+
import re
32
import sqlite3
3+
from pathlib import Path
44
from typing import Optional
55

6+
from sqlite_rag.logger import Logger
7+
68
from .chunker import Chunker
79
from .database import Database
810
from .engine import Engine
@@ -17,10 +19,12 @@ def __init__(self, settings: Optional[Settings] = None):
1719
if settings is None:
1820
# TODO: load defaults or from the database
1921
settings = Settings(
20-
model_path_or_name="./all-MiniLM-L6-v2.e4ce9877.q8_0.gguf", db_path="sqliterag.db"
22+
model_path_or_name="./all-MiniLM-L6-v2.e4ce9877.q8_0.gguf",
23+
db_path="sqliterag.db",
2124
)
2225

2326
self.settings = settings
27+
self._logger = Logger()
2428

2529
self._conn = sqlite3.connect(settings.db_path)
2630

@@ -37,54 +41,39 @@ def _ensure_initialized(self):
3741

3842
self.ready = True
3943

40-
def add(self, path: str, recursively: bool = False):
44+
def add(self, path: str, recursive: bool = False) -> int:
4145
"""Add the file content into the database"""
4246
self._ensure_initialized()
4347

4448
if not Path(path).exists():
4549
raise FileNotFoundError(f"{path} does not exist.")
4650

47-
files_to_process: list[Path] = []
48-
path_obj = Path(path)
49-
50-
if path_obj.is_file():
51-
files_to_process.append(path_obj)
52-
elif path_obj.is_dir():
53-
if recursively:
54-
files_to_process = list(path_obj.rglob("*"))
55-
else:
56-
files_to_process = list(path_obj.glob("*"))
57-
58-
files_to_process = [
59-
f
60-
for f in files_to_process
61-
if f.is_file() and FileReader.is_supported(f)
62-
]
51+
files_to_process = FileReader.collect_files(Path(path), recursive=recursive)
6352

53+
self._logger.info(f"Processing {len(files_to_process)} files...")
6454
for file_path in files_to_process:
65-
# TODO: check the file extension
66-
content = FileReader.parse_file(file_path)
6755
# TODO: include metadata extraction and mdx options (see our docsearch)
68-
document = Document(content=content, uri=str(file_path))
69-
chunks = self._chunker.chunk(document.content)
70-
chunks = self._engine.generate_embedding(chunks)
71-
document.chunks = chunks
56+
content = FileReader.parse_file(file_path)
57+
document = self._engine.process(
58+
Document(content=content, uri=str(file_path))
59+
)
7260

7361
self._repository.add_document(document)
62+
self._logger.info(str(file_path))
7463

7564
if self.settings.quantize_scan:
7665
self._engine.quantize()
7766

67+
return len(files_to_process)
68+
7869
def add_text(
7970
self, text: str, uri: Optional[str] = None, metadata: dict = {}
8071
) -> None:
8172
"""Add a text content into the database"""
8273
self._ensure_initialized()
8374

8475
document = Document(content=text, uri=uri, metadata=metadata)
85-
chunks = self._chunker.chunk(document.content)
86-
chunks = self._engine.generate_embedding(chunks)
87-
document.chunks = chunks
76+
document = self._engine.process(document)
8877

8978
self._repository.add_document(document)
9079

@@ -105,16 +94,16 @@ def find_document(self, identifier: str) -> Document | None:
10594
def remove_document(self, identifier: str) -> bool:
10695
"""Remove document by ID or URI"""
10796
self._ensure_initialized()
108-
97+
10998
# First find the document to get its ID
11099
document = self._repository.find_document_by_id_or_uri(identifier)
111100
if not document:
112101
return False
113-
102+
114103
return self._repository.remove_document(document.id or "")
115104

116105
# def search(
117106
# self, query: str, top_k: int = 10
118107
# ) -> list[Document]:
119108
# """Search for documents matching the query"""
120-
# self._ensure_initialized()
109+
# self._ensure_initialized()

tests/test_reader.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import tempfile
2+
from pathlib import Path
3+
4+
from sqlite_rag.reader import FileReader
5+
6+
7+
class TestFileReader:
8+
def test_collect_files_empty_directory(self):
9+
with tempfile.TemporaryDirectory() as temp_dir:
10+
files = FileReader.collect_files(Path(temp_dir), recursive=False)
11+
assert len(files) == 0
12+
13+
def test_collect_files_single_file(self):
14+
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
15+
f.write(b"This is a test document.")
16+
temp_file_path = f.name
17+
18+
files = FileReader.collect_files(Path(temp_file_path), recursive=False)
19+
assert len(files) == 1
20+
assert Path(temp_file_path) in files
21+
22+
def test_collect_files(self):
23+
with tempfile.TemporaryDirectory() as temp_dir:
24+
temp_dir = Path(temp_dir)
25+
file1 = temp_dir / "test1.txt"
26+
file2 = temp_dir / "test2.txt"
27+
file1.write_text("This is the first test document.")
28+
file2.write_text("This is the second test document.")
29+
30+
files = FileReader.collect_files(temp_dir, recursive=False)
31+
assert len(files) == 2
32+
assert file1 in files
33+
assert file2 in files
34+
35+
def test_collect_ignore_unsupported_files(self):
36+
with tempfile.TemporaryDirectory() as temp_dir:
37+
sub_dir = Path(temp_dir) / "subdir"
38+
sub_dir.mkdir()
39+
file1 = Path(temp_dir) / "test1.txt"
40+
file2 = sub_dir / "test2.unsupported"
41+
file1.write_text("This is a test document.")
42+
file2.write_text("This is an unsupported file type.")
43+
44+
files = FileReader.collect_files(Path(temp_dir), recursive=True)
45+
assert len(files) == 1
46+
assert file1 in files
47+
assert file2 not in files
48+
49+
def test_collect_files_recursive_directory(self):
50+
with tempfile.TemporaryDirectory() as temp_dir:
51+
sub_dir = Path(temp_dir) / "subdir"
52+
sub_dir.mkdir()
53+
file1 = Path(temp_dir) / "file1.txt"
54+
file2 = sub_dir / "file2.txt"
55+
56+
file1.write_text("This is the first test document.")
57+
file2.write_text("This is the second test document.")
58+
59+
files = FileReader.collect_files(Path(temp_dir), recursive=True)
60+
assert len(files) == 2
61+
assert file1 in files
62+
assert file2 in files
63+
64+
def test_is_supported(self):
65+
unsupported_extensions = [".exe", ".bin", ".jpg", ".png"]
66+
67+
for ext in FileReader.extensions:
68+
assert FileReader.is_supported(Path(f"test{ext}"))
69+
70+
for ext in unsupported_extensions:
71+
assert not FileReader.is_supported(Path(f"test{ext}"))
72+
73+
def test_parse_html_into_markdown(self):
74+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
75+
f.write(b"<html><body><h1>This is a test markdown file.</h1></body></html>")
76+
77+
content = FileReader.parse_file(Path(f.name))
78+
assert "# This is a test markdown file." in content

tests/test_sqlite_rag.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ def test_add_simple_text_file(self, db_settings):
2626
chunk_count = cursor.fetchone()[0]
2727
assert chunk_count > 0
2828

29+
def test_add_unsupported_file_type(self, db_settings):
30+
# Create a temporary file with an unsupported extension
31+
with tempfile.NamedTemporaryFile(mode="w", suffix=".unsupported", delete=False) as f:
32+
f.write("This is a test document with unsupported file type.")
33+
34+
rag = SQLiteRag(db_settings)
35+
36+
# Attempt to add the unsupported file
37+
processed = rag.add(f.name)
38+
39+
assert processed == 0
40+
2941
def test_add_directory(self, db_settings):
3042
with tempfile.TemporaryDirectory() as temp_dir:
3143
file1 = Path(temp_dir) / "file1.txt"
@@ -34,9 +46,6 @@ def test_add_directory(self, db_settings):
3446
file1.write_text("This is the first test document.")
3547
file2.write_text("This is the second test document.")
3648

37-
db_settings.chunk_size = 100
38-
db_settings.chunk_overlap = 10
39-
4049
rag = SQLiteRag(db_settings)
4150

4251
rag.add(temp_dir)
@@ -50,6 +59,30 @@ def test_add_directory(self, db_settings):
5059
chunk_count = cursor.fetchone()[0]
5160
assert chunk_count > 0
5261

62+
def test_add_directory_recursively(self, db_settings):
63+
with tempfile.TemporaryDirectory() as temp_dir:
64+
sub_dir = Path(temp_dir) / "subdir"
65+
sub_dir.mkdir()
66+
67+
file1 = Path(temp_dir) / "file1.txt"
68+
file2 = sub_dir / "file2.txt"
69+
70+
file1.write_text("This is the first test document.")
71+
file2.write_text("This is the second test document in a subdirectory.")
72+
73+
rag = SQLiteRag(db_settings)
74+
75+
rag.add(temp_dir, recursive=True)
76+
77+
conn = rag._conn
78+
cursor = conn.execute("SELECT COUNT(*) FROM documents")
79+
doc_count = cursor.fetchone()[0]
80+
assert doc_count == 2
81+
82+
cursor = conn.execute("SELECT COUNT(*) FROM chunks")
83+
chunk_count = cursor.fetchone()[0]
84+
assert chunk_count > 0
85+
5386
def test_add_text(self, db_settings):
5487
rag = SQLiteRag(db_settings)
5588

0 commit comments

Comments
 (0)