From e8e67d336ccac6759c3b8c1a49a55f6a1c410a85 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 18 Dec 2025 15:10:31 +0200 Subject: [PATCH 1/3] Fix get_model() uses where default Config is forced --- evaluations/evaluations/evaluators/judge.py | 8 +++----- haiku_rag_slim/haiku/rag/qa/__init__.py | 1 + haiku_rag_slim/haiku/rag/qa/agent.py | 6 +++--- haiku_rag_slim/haiku/rag/utils.py | 5 +++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/evaluations/evaluations/evaluators/judge.py b/evaluations/evaluations/evaluators/judge.py index b7e949d9..e2b020ad 100644 --- a/evaluations/evaluations/evaluators/judge.py +++ b/evaluations/evaluations/evaluators/judge.py @@ -1,8 +1,7 @@ from pydantic import BaseModel from pydantic_ai import Agent -from haiku.rag.config import Config -from haiku.rag.config.models import ModelConfig +from haiku.rag.config.models import AppConfig, ModelConfig from haiku.rag.utils import get_model ANSWER_EQUIVALENCE_RUBRIC = """You are evaluating whether two answers to the same question are semantically equivalent. @@ -36,10 +35,9 @@ class LLMJudgeResponseSchema(BaseModel): class LLMJudge: """LLM-as-judge for evaluating answer equivalence using Pydantic AI.""" - def __init__(self, model: str = "gpt-oss"): - # Create model using get_model with thinking disabled + def __init__(self, model: str = "gpt-oss", config: AppConfig | None = None): model_config = ModelConfig(provider="ollama", name=model, enable_thinking=False) - model_obj = get_model(model_config, Config) + model_obj = get_model(model_config, config) # Create Pydantic AI agent self._agent = Agent( diff --git a/haiku_rag_slim/haiku/rag/qa/__init__.py b/haiku_rag_slim/haiku/rag/qa/__init__.py index 1ec754d6..63ee6419 100644 --- a/haiku_rag_slim/haiku/rag/qa/__init__.py +++ b/haiku_rag_slim/haiku/rag/qa/__init__.py @@ -21,5 +21,6 @@ def get_qa_agent( return QuestionAnswerAgent( client=client, model_config=config.qa.model, + config=config, system_prompt=system_prompt, ) diff --git a/haiku_rag_slim/haiku/rag/qa/agent.py b/haiku_rag_slim/haiku/rag/qa/agent.py index d0ab6600..6ce392ea 100644 --- a/haiku_rag_slim/haiku/rag/qa/agent.py +++ b/haiku_rag_slim/haiku/rag/qa/agent.py @@ -3,8 +3,7 @@ from pydantic_ai import Agent, RunContext from pydantic_ai.output import ToolOutput from haiku.rag.client import HaikuRAG -from haiku.rag.config import Config -from haiku.rag.config.models import ModelConfig +from haiku.rag.config.models import AppConfig, ModelConfig from haiku.rag.graph.research.models import Citation, RawSearchAnswer, resolve_citations from haiku.rag.qa.prompts import QA_SYSTEM_PROMPT from haiku.rag.store.models import SearchResult @@ -23,10 +22,11 @@ class QuestionAnswerAgent: self, client: HaikuRAG, model_config: ModelConfig, + config: AppConfig | None = None, system_prompt: str | None = None, ): self._client = client - model_obj = get_model(model_config, Config) + model_obj = get_model(model_config, config) self._agent = Agent( model=model_obj, diff --git a/haiku_rag_slim/haiku/rag/utils.py b/haiku_rag_slim/haiku/rag/utils.py index 9889b680..496579d1 100644 --- a/haiku_rag_slim/haiku/rag/utils.py +++ b/haiku_rag_slim/haiku/rag/utils.py @@ -8,6 +8,7 @@ from packaging.version import Version, parse if TYPE_CHECKING: from rich.console import RenderableType + from haiku.rag.config.models import AppConfig, ModelConfig from haiku.rag.graph.research.models import Citation @@ -44,8 +45,8 @@ def apply_common_settings( def get_model( - model_config: Any, - app_config: Any | None = None, + model_config: "ModelConfig", + app_config: "AppConfig | None" = None, ) -> Any: """ Get a model instance for the specified configuration. From fb1d3c97b3f0c4deff395c0e83afe257b1d563eb Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 19 Dec 2025 11:09:57 +0200 Subject: [PATCH 2/3] Add additional programming language specifiers --- .../haiku/rag/converters/text_utils.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/haiku_rag_slim/haiku/rag/converters/text_utils.py b/haiku_rag_slim/haiku/rag/converters/text_utils.py index 64994e31..eb521b50 100644 --- a/haiku_rag_slim/haiku/rag/converters/text_utils.py +++ b/haiku_rag_slim/haiku/rag/converters/text_utils.py @@ -18,57 +18,129 @@ class TextFileHandler: # Plain text extensions that we'll read directly text_extensions: ClassVar[list[str]] = [ ".astro", + ".bash", ".c", + ".clj", + ".cljs", ".cpp", + ".cs", ".css", + ".dart", + ".elm", + ".ex", + ".exs", + ".fs", + ".fsx", ".go", + ".gql", + ".graphql", + ".groovy", ".h", + ".hcl", ".hpp", + ".hs", ".java", + ".jl", ".js", ".json", ".kt", + ".less", + ".lua", ".mdx", ".mjs", + ".ml", + ".mli", + ".nim", + ".nix", ".php", + ".pl", + ".pm", + ".proto", + ".ps1", ".py", + ".r", ".rb", ".rs", + ".sass", + ".scala", + ".scss", + ".sh", + ".sql", ".svelte", ".swift", + ".tf", + ".toml", ".ts", ".tsx", ".txt", ".vue", + ".xml", ".yaml", ".yml", + ".zig", ] # Code file extensions with their markdown language identifiers code_markdown_identifier: ClassVar[dict[str, str]] = { ".astro": "astro", + ".bash": "bash", ".c": "c", + ".clj": "clojure", + ".cljs": "clojure", ".cpp": "cpp", + ".cs": "csharp", ".css": "css", + ".dart": "dart", + ".elm": "elm", + ".ex": "elixir", + ".exs": "elixir", + ".fs": "fsharp", + ".fsx": "fsharp", ".go": "go", + ".gql": "graphql", + ".graphql": "graphql", + ".groovy": "groovy", ".h": "c", + ".hcl": "hcl", ".hpp": "cpp", + ".hs": "haskell", ".java": "java", + ".jl": "julia", ".js": "javascript", ".json": "json", ".kt": "kotlin", + ".less": "less", + ".lua": "lua", ".mjs": "javascript", + ".ml": "ocaml", + ".mli": "ocaml", + ".nim": "nim", + ".nix": "nix", ".php": "php", + ".pl": "perl", + ".pm": "perl", + ".proto": "protobuf", + ".ps1": "powershell", ".py": "python", + ".r": "r", ".rb": "ruby", ".rs": "rust", + ".sass": "sass", + ".scala": "scala", + ".scss": "scss", + ".sh": "bash", + ".sql": "sql", ".svelte": "svelte", ".swift": "swift", + ".tf": "hcl", + ".toml": "toml", ".ts": "typescript", ".tsx": "tsx", ".vue": "vue", + ".xml": "xml", ".yaml": "yaml", ".yml": "yaml", + ".zig": "zig", } @staticmethod From 090be293d2cbbc0435dc6a8186301c85f9273015 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 19 Dec 2025 10:11:26 +0200 Subject: [PATCH 3/3] Clarify folder mounts in docker docs, check monitor paths exist in FileWatcher --- CHANGELOG.md | 9 ++++++++ docker/README.md | 26 +++++++++++++++++----- docs/installation.md | 7 ++++-- examples/docker/README.md | 25 ++++++++++++++++++++- haiku_rag_slim/haiku/rag/monitor.py | 12 ++++++++++ tests/test_monitor.py | 34 +++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db40a646..4650c7d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog ## [Unreleased] +### Fixed + +- **File Monitor Path Validation**: Monitor now validates directories exist before watching ([#204](https://github.com/ggozad/haiku.rag/issues/204)) + - Provides clear error message pointing to `haiku.rag.yaml` configuration + - Prevents cryptic `FileNotFoundError: No path was found` from watchfiles +- **Docker Documentation**: Improved Docker setup instructions + - Added volume mount examples for config file and documents directory + - Clarified that `monitor.directories` must use container paths, not host paths + ## [0.21.0] - 2025-12-18 ### Added diff --git a/docker/README.md b/docker/README.md index 70917642..03053f38 100644 --- a/docker/README.md +++ b/docker/README.md @@ -44,19 +44,35 @@ Mount your config file and data directory: ```bash docker run -p 8001:8001 \ - -v $(pwd)/haiku.rag.yaml:/app/haiku.rag.yaml \ - -v $(pwd)/data:/data \ + -v /path/to/haiku.rag.yaml:/app/haiku.rag.yaml \ + -v /path/to/data:/data \ haiku-rag ``` -The container will automatically use the mounted `haiku.rag.yaml` configuration file. +To enable file monitoring, also mount a documents directory: + +```bash +docker run -p 8001:8001 \ + -v /path/to/haiku.rag.yaml:/app/haiku.rag.yaml \ + -v /path/to/data:/data \ + -v /path/to/docs:/docs \ + haiku-rag haiku-rag serve --mcp --monitor +``` + +Your `haiku.rag.yaml` must reference the **container path** for monitoring: + +```yaml +monitor: + directories: + - /docs # Container path, not host path +``` For API keys (OpenAI, Anthropic, etc.), pass them as environment variables: ```bash docker run -p 8001:8001 \ - -v $(pwd)/haiku.rag.yaml:/app/haiku.rag.yaml \ - -v $(pwd)/data:/data \ + -v /path/to/haiku.rag.yaml:/app/haiku.rag.yaml \ + -v /path/to/data:/data \ -e OPENAI_API_KEY=your-key-here \ haiku-rag ``` diff --git a/docs/installation.md b/docs/installation.md index 9d08444a..d665a54c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -93,7 +93,10 @@ Build locally to include all features and document processing without docling-se ```bash docker build -f docker/Dockerfile -t haiku-rag . -docker run -p 8001:8001 -v $(pwd)/data:/data haiku-rag +docker run -p 8001:8001 \ + -v /path/to/haiku.rag.yaml:/app/haiku.rag.yaml \ + -v /path/to/data:/data \ + haiku-rag ``` -See `docker/README.md` for complete build and configuration instructions. +See `docker/README.md` for complete build and configuration instructions, including how to enable file monitoring. diff --git a/examples/docker/README.md b/examples/docker/README.md index 0f31b0f2..74f32661 100644 --- a/examples/docker/README.md +++ b/examples/docker/README.md @@ -14,13 +14,36 @@ This setup showcases the minimal haiku.rag-slim image combined with external doc ## Quick Start ```bash +# Create required directories mkdir -p data docs -cp haiku.rag.yaml.example haiku.rag.yaml # Edit as needed + +# Create config file from example (required) +cp haiku.rag.yaml.example haiku.rag.yaml + +# Start services docker compose up -d ``` Place documents in `docs/` for automatic indexing. +## Volume Mounts + +The docker-compose.yml mounts three volumes: + +| Host Path | Container Path | Purpose | +|-----------|---------------|---------| +| `./data` | `/data` | Persistent LanceDB database | +| `./docs` | `/docs` | Documents to monitor and index | +| `./haiku.rag.yaml` | `/app/haiku.rag.yaml` | Configuration file | + +**Important:** The `haiku.rag.yaml` config file must exist before running `docker compose up`. Copy it from the example: + +```bash +cp haiku.rag.yaml.example haiku.rag.yaml +``` + +The example config sets `monitor.directories: [/docs]` - this is the **container path**, not your host path. Documents placed in `./docs` on your host will appear at `/docs` inside the container. + ## Usage ```bash diff --git a/haiku_rag_slim/haiku/rag/monitor.py b/haiku_rag_slim/haiku/rag/monitor.py index c66949f2..45a1178e 100644 --- a/haiku_rag_slim/haiku/rag/monitor.py +++ b/haiku_rag_slim/haiku/rag/monitor.py @@ -90,6 +90,18 @@ class FileWatcher: self.supported_extensions = get_converter(config).supported_extensions async def observe(self): + if not self.paths: + logger.warning("No directories configured for monitoring") + return + + # Validate all paths exist before attempting to watch + missing_paths = [p for p in self.paths if not Path(p).exists()] + if missing_paths: + raise FileNotFoundError( + f"Monitor directories do not exist: {missing_paths}. " + "Check your haiku.rag.yaml configuration." + ) + logger.info(f"Watching files in {self.paths}") filter = FileFilter( ignore_patterns=self.ignore_patterns, diff --git a/tests/test_monitor.py b/tests/test_monitor.py index c9299b0b..78eeb5f6 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -440,3 +440,37 @@ async def test_file_watcher_orphan_handles_spaces_in_filenames(): # Should NOT delete the document since file exists mock_client.delete_document.assert_not_called() + + +@pytest.mark.asyncio +async def test_file_watcher_observe_raises_on_missing_paths(): + """Test observe() raises FileNotFoundError when directories don't exist.""" + mock_client = AsyncMock(spec=HaikuRAG) + + test_config = AppConfig( + monitor=MonitorConfig( + directories=[Path("/nonexistent/path/that/does/not/exist")] + ) + ) + watcher = FileWatcher(client=mock_client, config=test_config) + + with pytest.raises(FileNotFoundError) as exc_info: + await watcher.observe() + + assert "Monitor directories do not exist" in str(exc_info.value) + assert "haiku.rag.yaml" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_file_watcher_observe_returns_early_when_no_directories(): + """Test observe() returns early when no directories are configured.""" + mock_client = AsyncMock(spec=HaikuRAG) + + test_config = AppConfig(monitor=MonitorConfig(directories=[])) + watcher = FileWatcher(client=mock_client, config=test_config) + + # Should return without error when no directories configured + await watcher.observe() + + # No documents should have been processed + mock_client.create_document_from_source.assert_not_called()