Merge branch 'main' into feat/read-only-db
This commit is contained in:
commit
8c0adb47e1
11 changed files with 187 additions and 18 deletions
|
|
@ -9,6 +9,15 @@
|
||||||
- Excludes write tools (`add_document_*`, `delete_document`) from MCP server
|
- Excludes write tools (`add_document_*`, `delete_document`) from MCP server
|
||||||
- Disables file monitor with warning when `--read-only` is used with `serve --monitor`
|
- Disables file monitor with warning when `--read-only` is used with `serve --monitor`
|
||||||
|
|
||||||
|
### 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
|
## [0.21.0] - 2025-12-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -44,19 +44,35 @@ Mount your config file and data directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 8001:8001 \
|
docker run -p 8001:8001 \
|
||||||
-v $(pwd)/haiku.rag.yaml:/app/haiku.rag.yaml \
|
-v /path/to/haiku.rag.yaml:/app/haiku.rag.yaml \
|
||||||
-v $(pwd)/data:/data \
|
-v /path/to/data:/data \
|
||||||
haiku-rag
|
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:
|
For API keys (OpenAI, Anthropic, etc.), pass them as environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 8001:8001 \
|
docker run -p 8001:8001 \
|
||||||
-v $(pwd)/haiku.rag.yaml:/app/haiku.rag.yaml \
|
-v /path/to/haiku.rag.yaml:/app/haiku.rag.yaml \
|
||||||
-v $(pwd)/data:/data \
|
-v /path/to/data:/data \
|
||||||
-e OPENAI_API_KEY=your-key-here \
|
-e OPENAI_API_KEY=your-key-here \
|
||||||
haiku-rag
|
haiku-rag
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,10 @@ Build locally to include all features and document processing without docling-se
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f docker/Dockerfile -t haiku-rag .
|
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.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic_ai import Agent
|
from pydantic_ai import Agent
|
||||||
|
|
||||||
from haiku.rag.config import Config
|
from haiku.rag.config.models import AppConfig, ModelConfig
|
||||||
from haiku.rag.config.models import ModelConfig
|
|
||||||
from haiku.rag.utils import get_model
|
from haiku.rag.utils import get_model
|
||||||
|
|
||||||
ANSWER_EQUIVALENCE_RUBRIC = """You are evaluating whether two answers to the same question are semantically equivalent.
|
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:
|
class LLMJudge:
|
||||||
"""LLM-as-judge for evaluating answer equivalence using Pydantic AI."""
|
"""LLM-as-judge for evaluating answer equivalence using Pydantic AI."""
|
||||||
|
|
||||||
def __init__(self, model: str = "gpt-oss"):
|
def __init__(self, model: str = "gpt-oss", config: AppConfig | None = None):
|
||||||
# Create model using get_model with thinking disabled
|
|
||||||
model_config = ModelConfig(provider="ollama", name=model, enable_thinking=False)
|
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
|
# Create Pydantic AI agent
|
||||||
self._agent = Agent(
|
self._agent = Agent(
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,36 @@ This setup showcases the minimal haiku.rag-slim image combined with external doc
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Create required directories
|
||||||
mkdir -p data docs
|
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
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Place documents in `docs/` for automatic indexing.
|
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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -18,57 +18,129 @@ class TextFileHandler:
|
||||||
# Plain text extensions that we'll read directly
|
# Plain text extensions that we'll read directly
|
||||||
text_extensions: ClassVar[list[str]] = [
|
text_extensions: ClassVar[list[str]] = [
|
||||||
".astro",
|
".astro",
|
||||||
|
".bash",
|
||||||
".c",
|
".c",
|
||||||
|
".clj",
|
||||||
|
".cljs",
|
||||||
".cpp",
|
".cpp",
|
||||||
|
".cs",
|
||||||
".css",
|
".css",
|
||||||
|
".dart",
|
||||||
|
".elm",
|
||||||
|
".ex",
|
||||||
|
".exs",
|
||||||
|
".fs",
|
||||||
|
".fsx",
|
||||||
".go",
|
".go",
|
||||||
|
".gql",
|
||||||
|
".graphql",
|
||||||
|
".groovy",
|
||||||
".h",
|
".h",
|
||||||
|
".hcl",
|
||||||
".hpp",
|
".hpp",
|
||||||
|
".hs",
|
||||||
".java",
|
".java",
|
||||||
|
".jl",
|
||||||
".js",
|
".js",
|
||||||
".json",
|
".json",
|
||||||
".kt",
|
".kt",
|
||||||
|
".less",
|
||||||
|
".lua",
|
||||||
".mdx",
|
".mdx",
|
||||||
".mjs",
|
".mjs",
|
||||||
|
".ml",
|
||||||
|
".mli",
|
||||||
|
".nim",
|
||||||
|
".nix",
|
||||||
".php",
|
".php",
|
||||||
|
".pl",
|
||||||
|
".pm",
|
||||||
|
".proto",
|
||||||
|
".ps1",
|
||||||
".py",
|
".py",
|
||||||
|
".r",
|
||||||
".rb",
|
".rb",
|
||||||
".rs",
|
".rs",
|
||||||
|
".sass",
|
||||||
|
".scala",
|
||||||
|
".scss",
|
||||||
|
".sh",
|
||||||
|
".sql",
|
||||||
".svelte",
|
".svelte",
|
||||||
".swift",
|
".swift",
|
||||||
|
".tf",
|
||||||
|
".toml",
|
||||||
".ts",
|
".ts",
|
||||||
".tsx",
|
".tsx",
|
||||||
".txt",
|
".txt",
|
||||||
".vue",
|
".vue",
|
||||||
|
".xml",
|
||||||
".yaml",
|
".yaml",
|
||||||
".yml",
|
".yml",
|
||||||
|
".zig",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Code file extensions with their markdown language identifiers
|
# Code file extensions with their markdown language identifiers
|
||||||
code_markdown_identifier: ClassVar[dict[str, str]] = {
|
code_markdown_identifier: ClassVar[dict[str, str]] = {
|
||||||
".astro": "astro",
|
".astro": "astro",
|
||||||
|
".bash": "bash",
|
||||||
".c": "c",
|
".c": "c",
|
||||||
|
".clj": "clojure",
|
||||||
|
".cljs": "clojure",
|
||||||
".cpp": "cpp",
|
".cpp": "cpp",
|
||||||
|
".cs": "csharp",
|
||||||
".css": "css",
|
".css": "css",
|
||||||
|
".dart": "dart",
|
||||||
|
".elm": "elm",
|
||||||
|
".ex": "elixir",
|
||||||
|
".exs": "elixir",
|
||||||
|
".fs": "fsharp",
|
||||||
|
".fsx": "fsharp",
|
||||||
".go": "go",
|
".go": "go",
|
||||||
|
".gql": "graphql",
|
||||||
|
".graphql": "graphql",
|
||||||
|
".groovy": "groovy",
|
||||||
".h": "c",
|
".h": "c",
|
||||||
|
".hcl": "hcl",
|
||||||
".hpp": "cpp",
|
".hpp": "cpp",
|
||||||
|
".hs": "haskell",
|
||||||
".java": "java",
|
".java": "java",
|
||||||
|
".jl": "julia",
|
||||||
".js": "javascript",
|
".js": "javascript",
|
||||||
".json": "json",
|
".json": "json",
|
||||||
".kt": "kotlin",
|
".kt": "kotlin",
|
||||||
|
".less": "less",
|
||||||
|
".lua": "lua",
|
||||||
".mjs": "javascript",
|
".mjs": "javascript",
|
||||||
|
".ml": "ocaml",
|
||||||
|
".mli": "ocaml",
|
||||||
|
".nim": "nim",
|
||||||
|
".nix": "nix",
|
||||||
".php": "php",
|
".php": "php",
|
||||||
|
".pl": "perl",
|
||||||
|
".pm": "perl",
|
||||||
|
".proto": "protobuf",
|
||||||
|
".ps1": "powershell",
|
||||||
".py": "python",
|
".py": "python",
|
||||||
|
".r": "r",
|
||||||
".rb": "ruby",
|
".rb": "ruby",
|
||||||
".rs": "rust",
|
".rs": "rust",
|
||||||
|
".sass": "sass",
|
||||||
|
".scala": "scala",
|
||||||
|
".scss": "scss",
|
||||||
|
".sh": "bash",
|
||||||
|
".sql": "sql",
|
||||||
".svelte": "svelte",
|
".svelte": "svelte",
|
||||||
".swift": "swift",
|
".swift": "swift",
|
||||||
|
".tf": "hcl",
|
||||||
|
".toml": "toml",
|
||||||
".ts": "typescript",
|
".ts": "typescript",
|
||||||
".tsx": "tsx",
|
".tsx": "tsx",
|
||||||
".vue": "vue",
|
".vue": "vue",
|
||||||
|
".xml": "xml",
|
||||||
".yaml": "yaml",
|
".yaml": "yaml",
|
||||||
".yml": "yaml",
|
".yml": "yaml",
|
||||||
|
".zig": "zig",
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,18 @@ class FileWatcher:
|
||||||
self.supported_extensions = get_converter(config).supported_extensions
|
self.supported_extensions = get_converter(config).supported_extensions
|
||||||
|
|
||||||
async def observe(self):
|
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}")
|
logger.info(f"Watching files in {self.paths}")
|
||||||
filter = FileFilter(
|
filter = FileFilter(
|
||||||
ignore_patterns=self.ignore_patterns,
|
ignore_patterns=self.ignore_patterns,
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,6 @@ def get_qa_agent(
|
||||||
return QuestionAnswerAgent(
|
return QuestionAnswerAgent(
|
||||||
client=client,
|
client=client,
|
||||||
model_config=config.qa.model,
|
model_config=config.qa.model,
|
||||||
|
config=config,
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ from pydantic_ai import Agent, RunContext
|
||||||
from pydantic_ai.output import ToolOutput
|
from pydantic_ai.output import ToolOutput
|
||||||
|
|
||||||
from haiku.rag.client import HaikuRAG
|
from haiku.rag.client import HaikuRAG
|
||||||
from haiku.rag.config import Config
|
from haiku.rag.config.models import AppConfig, ModelConfig
|
||||||
from haiku.rag.config.models import ModelConfig
|
|
||||||
from haiku.rag.graph.research.models import Citation, RawSearchAnswer, resolve_citations
|
from haiku.rag.graph.research.models import Citation, RawSearchAnswer, resolve_citations
|
||||||
from haiku.rag.qa.prompts import QA_SYSTEM_PROMPT
|
from haiku.rag.qa.prompts import QA_SYSTEM_PROMPT
|
||||||
from haiku.rag.store.models import SearchResult
|
from haiku.rag.store.models import SearchResult
|
||||||
|
|
@ -23,10 +22,11 @@ class QuestionAnswerAgent:
|
||||||
self,
|
self,
|
||||||
client: HaikuRAG,
|
client: HaikuRAG,
|
||||||
model_config: ModelConfig,
|
model_config: ModelConfig,
|
||||||
|
config: AppConfig | None = None,
|
||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
):
|
):
|
||||||
self._client = client
|
self._client = client
|
||||||
model_obj = get_model(model_config, Config)
|
model_obj = get_model(model_config, config)
|
||||||
|
|
||||||
self._agent = Agent(
|
self._agent = Agent(
|
||||||
model=model_obj,
|
model=model_obj,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from packaging.version import Version, parse
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
|
|
||||||
|
from haiku.rag.config.models import AppConfig, ModelConfig
|
||||||
from haiku.rag.graph.research.models import Citation
|
from haiku.rag.graph.research.models import Citation
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -44,8 +45,8 @@ def apply_common_settings(
|
||||||
|
|
||||||
|
|
||||||
def get_model(
|
def get_model(
|
||||||
model_config: Any,
|
model_config: "ModelConfig",
|
||||||
app_config: Any | None = None,
|
app_config: "AppConfig | None" = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Get a model instance for the specified configuration.
|
Get a model instance for the specified configuration.
|
||||||
|
|
|
||||||
|
|
@ -440,3 +440,37 @@ async def test_file_watcher_orphan_handles_spaces_in_filenames():
|
||||||
|
|
||||||
# Should NOT delete the document since file exists
|
# Should NOT delete the document since file exists
|
||||||
mock_client.delete_document.assert_not_called()
|
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()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue