Filtering based on pathspec

This commit is contained in:
Yiorgis Gozadinos 2025-10-29 10:27:16 +02:00
parent a15f1d8cf5
commit cabb7748f5
No known key found for this signature in database
3 changed files with 73 additions and 14 deletions

View file

@ -474,9 +474,7 @@ class HaikuRAGApp:
# Start file monitor if enabled # Start file monitor if enabled
if enable_monitor: if enable_monitor:
monitor = FileWatcher( monitor = FileWatcher(client=client)
paths=Config.storage.monitor_directories, client=client
)
monitor_task = asyncio.create_task(monitor.observe()) monitor_task = asyncio.create_task(monitor.observe())
tasks.append(monitor_task) tasks.append(monitor_task)

View file

@ -2,9 +2,12 @@ import logging
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pathspec
from pathspec.patterns.gitwildmatch import GitWildMatchPattern
from watchfiles import Change, DefaultFilter, awatch from watchfiles import Change, DefaultFilter, awatch
from haiku.rag.client import HaikuRAG from haiku.rag.client import HaikuRAG
from haiku.rag.config import AppConfig, Config
from haiku.rag.store.models.document import Document from haiku.rag.store.models.document import Document
if TYPE_CHECKING: if TYPE_CHECKING:
@ -14,25 +17,63 @@ logger = logging.getLogger(__name__)
class FileFilter(DefaultFilter): class FileFilter(DefaultFilter):
def __init__(self, *, ignore_paths: list[Path] | None = None) -> None: def __init__(
self,
*,
ignore_patterns: list[str] | None = None,
include_patterns: list[str] | None = None,
) -> None:
# Lazy import to avoid loading docling # Lazy import to avoid loading docling
from haiku.rag.reader import FileReader from haiku.rag.reader import FileReader
self.extensions = tuple(FileReader.extensions) self.extensions = tuple(FileReader.extensions)
super().__init__(ignore_paths=ignore_paths) self.ignore_spec = (
pathspec.PathSpec.from_lines(GitWildMatchPattern, ignore_patterns)
if ignore_patterns
else None
)
self.include_spec = (
pathspec.PathSpec.from_lines(GitWildMatchPattern, include_patterns)
if include_patterns
else None
)
super().__init__()
def __call__(self, change: Change, path: str) -> bool: def __call__(self, change: Change, path: str) -> bool:
return path.endswith(self.extensions) and super().__call__(change, path) # Check extension filter
if not path.endswith(self.extensions):
return False
# Apply include patterns if specified (whitelist mode)
if self.include_spec:
if not self.include_spec.match_file(path):
return False
# Apply ignore patterns (blacklist mode)
if self.ignore_spec:
if self.ignore_spec.match_file(path):
return False
# Apply default watchfiles filter
return super().__call__(change, path)
class FileWatcher: class FileWatcher:
def __init__(self, paths: list[Path], client: HaikuRAG): def __init__(
self.paths = paths self,
client: HaikuRAG,
config: AppConfig = Config,
):
self.paths = config.storage.monitor_directories
self.client = client self.client = client
self.ignore_patterns = config.storage.monitor_ignore_patterns or None
self.include_patterns = config.storage.monitor_include_patterns or None
async def observe(self): async def observe(self):
logger.info(f"Watching files in {self.paths}") logger.info(f"Watching files in {self.paths}")
filter = FileFilter() filter = FileFilter(
ignore_patterns=self.ignore_patterns, include_patterns=self.include_patterns
)
await self.refresh() await self.refresh()
async for changes in awatch(*self.paths, watch_filter=filter): async for changes in awatch(*self.paths, watch_filter=filter):
@ -49,10 +90,17 @@ class FileWatcher:
# Lazy import to avoid loading docling # Lazy import to avoid loading docling
from haiku.rag.reader import FileReader from haiku.rag.reader import FileReader
# Create filter to apply same logic as observe()
filter = FileFilter(
ignore_patterns=self.ignore_patterns, include_patterns=self.include_patterns
)
for path in self.paths: for path in self.paths:
for f in Path(path).rglob("**/*"): for f in Path(path).rglob("**/*"):
if f.is_file() and f.suffix in FileReader.extensions: if f.is_file() and f.suffix in FileReader.extensions:
await self._upsert_document(f) # Apply pattern filters
if filter(Change.added, str(f)):
await self._upsert_document(f)
async def _upsert_document(self, file: Path) -> Document | None: async def _upsert_document(self, file: Path) -> Document | None:
try: try:

View file

@ -5,6 +5,7 @@ from unittest.mock import AsyncMock
import pytest import pytest
from haiku.rag.client import HaikuRAG from haiku.rag.client import HaikuRAG
from haiku.rag.config import AppConfig, StorageConfig
from haiku.rag.monitor import FileWatcher from haiku.rag.monitor import FileWatcher
from haiku.rag.store.models.document import Document from haiku.rag.store.models.document import Document
@ -22,7 +23,10 @@ async def test_file_watcher_upsert_document():
mock_client.create_document_from_source.return_value = mock_doc mock_client.create_document_from_source.return_value = mock_doc
mock_client.get_document_by_uri.return_value = None # No existing document mock_client.get_document_by_uri.return_value = None # No existing document
watcher = FileWatcher(paths=[temp_path.parent], client=mock_client) test_config = AppConfig(
storage=StorageConfig(monitor_directories=[temp_path.parent])
)
watcher = FileWatcher(client=mock_client, config=test_config)
result = await watcher._upsert_document(temp_path) result = await watcher._upsert_document(temp_path)
@ -49,7 +53,10 @@ async def test_file_watcher_upsert_existing_document():
mock_client.get_document_by_uri.return_value = existing_doc mock_client.get_document_by_uri.return_value = existing_doc
mock_client.create_document_from_source.return_value = updated_doc mock_client.create_document_from_source.return_value = updated_doc
watcher = FileWatcher(paths=[temp_path.parent], client=mock_client) test_config = AppConfig(
storage=StorageConfig(monitor_directories=[temp_path.parent])
)
watcher = FileWatcher(client=mock_client, config=test_config)
result = await watcher._upsert_document(temp_path) result = await watcher._upsert_document(temp_path)
@ -69,7 +76,10 @@ async def test_file_watcher_delete_document():
mock_client.get_document_by_uri.return_value = existing_doc mock_client.get_document_by_uri.return_value = existing_doc
mock_client.delete_document.return_value = True mock_client.delete_document.return_value = True
watcher = FileWatcher(paths=[temp_path.parent], client=mock_client) test_config = AppConfig(
storage=StorageConfig(monitor_directories=[temp_path.parent])
)
watcher = FileWatcher(client=mock_client, config=test_config)
await watcher._delete_document(temp_path) await watcher._delete_document(temp_path)
@ -85,7 +95,10 @@ async def test_file_watcher_delete_nonexistent_document():
mock_client = AsyncMock(spec=HaikuRAG) mock_client = AsyncMock(spec=HaikuRAG)
mock_client.get_document_by_uri.return_value = None mock_client.get_document_by_uri.return_value = None
watcher = FileWatcher(paths=[temp_path.parent], client=mock_client) test_config = AppConfig(
storage=StorageConfig(monitor_directories=[temp_path.parent])
)
watcher = FileWatcher(client=mock_client, config=test_config)
await watcher._delete_document(temp_path) await watcher._delete_document(temp_path)