Always upgrade database on access. Explicitly create db by running init or HaikuRAG(path, create=True)
This commit is contained in:
parent
47d8f7ba3f
commit
33b254ae3d
29 changed files with 254 additions and 259 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,6 +1,19 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.19.6] - 2025-12-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **BREAKING: Explicit Database Creation**: Databases must now be explicitly created before use
|
||||||
|
- New `haiku-rag init` command creates a new empty database
|
||||||
|
- Python API: `HaikuRAG(path, create=True)` to create database programmatically
|
||||||
|
- Operations on non-existent databases raise `FileNotFoundError`
|
||||||
|
- **BREAKING: Embeddings Configuration**: Restructured to nested `EmbeddingModelConfig`
|
||||||
|
- Config path changed from `embeddings.{provider, model, vector_dim}` to `embeddings.model.{provider, name, vector_dim}`
|
||||||
|
- Automatic migration upgrades existing databases to new format
|
||||||
|
- **Database Migrations**: Always run when opening an existing database
|
||||||
|
|
||||||
## [0.19.5] - 2025-12-01
|
## [0.19.5] - 2025-12-01
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
16
docs/cli.md
16
docs/cli.md
|
|
@ -201,11 +201,21 @@ View current configuration settings:
|
||||||
haiku-rag settings
|
haiku-rag settings
|
||||||
```
|
```
|
||||||
|
|
||||||
## Maintenance
|
## Database Management
|
||||||
|
|
||||||
### Info (Read-only)
|
### Initialize Database
|
||||||
|
|
||||||
Display database metadata without upgrading or modifying it:
|
Create a new database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
haiku-rag init [--db /path/to/your.lancedb]
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates the database with the configured settings. **All other commands require an existing database** - they will fail with an informative error if the database doesn't exist.
|
||||||
|
|
||||||
|
### Info
|
||||||
|
|
||||||
|
Display database metadata:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
haiku-rag info [--db /path/to/your.lancedb]
|
haiku-rag info [--db /path/to/your.lancedb]
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,20 @@ Use `haiku.rag` directly in your Python applications.
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from haiku.rag.client import HaikuRAG
|
from haiku.rag.client import HaikuRAG
|
||||||
|
|
||||||
# Use as async context manager (recommended)
|
# Create a new database
|
||||||
|
async with HaikuRAG("path/to/database.lancedb", create=True) as client:
|
||||||
|
# Your code here
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Open an existing database (will fail if database doesn't exist)
|
||||||
async with HaikuRAG("path/to/database.lancedb") as client:
|
async with HaikuRAG("path/to/database.lancedb") as client:
|
||||||
# Your code here
|
# Your code here
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Databases must be explicitly created with `create=True` or via `haiku-rag init` before use. Operations on non-existent databases will raise `FileNotFoundError`.
|
||||||
|
|
||||||
## Document Management
|
## Document Management
|
||||||
|
|
||||||
### Creating Documents
|
### Creating Documents
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,16 @@ For the list of available OpenAI models and their vector dimensions, see the [Op
|
||||||
|
|
||||||
See [Configuration](configuration/index.md) for all available options.
|
See [Configuration](configuration/index.md) for all available options.
|
||||||
|
|
||||||
|
## Initialize the database
|
||||||
|
|
||||||
|
Before adding documents, initialize the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
haiku-rag init
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates an empty database with the configured settings.
|
||||||
|
|
||||||
## Adding the first documents
|
## Adding the first documents
|
||||||
|
|
||||||
Now you can add some pieces of text in the database:
|
Now you can add some pieces of text in the database:
|
||||||
|
|
@ -150,6 +160,7 @@ logger.setLevel(logging.DEBUG)
|
||||||
logger.debug("AGI here we come")
|
logger.debug("AGI here we come")
|
||||||
|
|
||||||
# Uses LanceDB database from default storage location
|
# Uses LanceDB database from default storage location
|
||||||
|
# (database must be initialized first with 'haiku-rag init' or create=True)
|
||||||
async with HaikuRAG() as client:
|
async with HaikuRAG() as client:
|
||||||
answer = await client.ask("What is the best programming language in the world?")
|
answer = await client.ask("What is the best programming language in the world?")
|
||||||
print(answer)
|
print(answer)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# pyright: reportMissingImports=false
|
||||||
import asyncio
|
import asyncio
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# pyright: reportMissingImports=false
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -12,8 +13,8 @@ from haiku_rag_a2a.a2a.models import AgentDependencies
|
||||||
from haiku_rag_a2a.a2a.skills import extract_question_from_task
|
from haiku_rag_a2a.a2a.skills import extract_question_from_task
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from fasta2a import Worker # type: ignore
|
from fasta2a import Worker
|
||||||
from fasta2a.schema import ( # type: ignore
|
from fasta2a.schema import (
|
||||||
Artifact,
|
Artifact,
|
||||||
Message,
|
Message,
|
||||||
TaskIdParams,
|
TaskIdParams,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# pyright: reportMissingImports=false
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -212,7 +213,7 @@ async def test_a2a_app_creation(temp_db_path):
|
||||||
from haiku_rag_a2a.a2a import create_a2a_app
|
from haiku_rag_a2a.a2a import create_a2a_app
|
||||||
|
|
||||||
# Create a test database
|
# Create a test database
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Python is a high-level programming language known for its simplicity.",
|
content="Python is a high-level programming language known for its simplicity.",
|
||||||
uri="python_doc",
|
uri="python_doc",
|
||||||
|
|
@ -233,7 +234,7 @@ async def test_a2a_app_has_skills(temp_db_path):
|
||||||
from haiku_rag_a2a.a2a import create_a2a_app
|
from haiku_rag_a2a.a2a import create_a2a_app
|
||||||
|
|
||||||
# Create a test database
|
# Create a test database
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
await client.create_document(content="Test document", uri="test_doc")
|
await client.create_document(content="Test document", uri="test_doc")
|
||||||
|
|
||||||
# Create A2A app
|
# Create A2A app
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,21 @@ class HaikuRAGApp:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.console = Console()
|
self.console = Console()
|
||||||
|
|
||||||
|
async def init(self):
|
||||||
|
"""Initialize a new database."""
|
||||||
|
if self.db_path.exists():
|
||||||
|
self.console.print(
|
||||||
|
f"[yellow]Database already exists at {self.db_path}[/yellow]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create the database
|
||||||
|
client = HaikuRAG(db_path=self.db_path, config=self.config, create=True)
|
||||||
|
client.close()
|
||||||
|
self.console.print(
|
||||||
|
f"[bold green]Database initialized at {self.db_path}[/bold green]"
|
||||||
|
)
|
||||||
|
|
||||||
async def info(self):
|
async def info(self):
|
||||||
"""Display read-only information about the database without modifying it."""
|
"""Display read-only information about the database without modifying it."""
|
||||||
|
|
||||||
|
|
@ -65,7 +80,13 @@ class HaikuRAGApp:
|
||||||
except Exception:
|
except Exception:
|
||||||
docling_version = "unknown"
|
docling_version = "unknown"
|
||||||
|
|
||||||
# Read settings (if present) to find stored haiku.rag version and embedding config
|
# Get comprehensive table statistics (this also runs migrations)
|
||||||
|
from haiku.rag.store.engine import Store
|
||||||
|
|
||||||
|
store = Store(self.db_path, config=self.config, skip_validation=True)
|
||||||
|
table_stats = store.get_stats()
|
||||||
|
|
||||||
|
# Read settings after Store init (migrations have run)
|
||||||
stored_version = "unknown"
|
stored_version = "unknown"
|
||||||
embed_provider: str | None = None
|
embed_provider: str | None = None
|
||||||
embed_model: str | None = None
|
embed_model: str | None = None
|
||||||
|
|
@ -85,13 +106,6 @@ class HaikuRAGApp:
|
||||||
embed_model = embed_model_obj.get("name")
|
embed_model = embed_model_obj.get("name")
|
||||||
vector_dim = embed_model_obj.get("vector_dim")
|
vector_dim = embed_model_obj.get("vector_dim")
|
||||||
|
|
||||||
# Get comprehensive table statistics
|
|
||||||
from haiku.rag.store.engine import Store
|
|
||||||
|
|
||||||
store = Store(
|
|
||||||
self.db_path, config=self.config, skip_validation=True, read_only=True
|
|
||||||
)
|
|
||||||
table_stats = store.get_stats()
|
|
||||||
store.close()
|
store.close()
|
||||||
|
|
||||||
num_docs = table_stats["documents"].get("num_rows", 0)
|
num_docs = table_stats["documents"].get("num_rows", 0)
|
||||||
|
|
@ -188,9 +202,7 @@ class HaikuRAGApp:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def list_documents(self, filter: str | None = None):
|
async def list_documents(self, filter: str | None = None):
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||||
db_path=self.db_path, config=self.config, read_only=True
|
|
||||||
) as self.client:
|
|
||||||
documents = await self.client.list_documents(filter=filter)
|
documents = await self.client.list_documents(filter=filter)
|
||||||
for doc in documents:
|
for doc in documents:
|
||||||
self._rich_print_document(doc, truncate=True)
|
self._rich_print_document(doc, truncate=True)
|
||||||
|
|
@ -223,9 +235,7 @@ class HaikuRAGApp:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_document(self, doc_id: str):
|
async def get_document(self, doc_id: str):
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||||
db_path=self.db_path, config=self.config, read_only=True
|
|
||||||
) as self.client:
|
|
||||||
doc = await self.client.get_document_by_id(doc_id)
|
doc = await self.client.get_document_by_id(doc_id)
|
||||||
if doc is None:
|
if doc is None:
|
||||||
self.console.print(f"[red]Document with id {doc_id} not found.[/red]")
|
self.console.print(f"[red]Document with id {doc_id} not found.[/red]")
|
||||||
|
|
@ -245,9 +255,7 @@ class HaikuRAGApp:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def search(self, query: str, limit: int = 5, filter: str | None = None):
|
async def search(self, query: str, limit: int = 5, filter: str | None = None):
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||||
db_path=self.db_path, config=self.config, read_only=True
|
|
||||||
) as self.client:
|
|
||||||
results = await self.client.search(query, limit=limit, filter=filter)
|
results = await self.client.search(query, limit=limit, filter=filter)
|
||||||
if not results:
|
if not results:
|
||||||
self.console.print("[yellow]No results found.[/yellow]")
|
self.console.print("[yellow]No results found.[/yellow]")
|
||||||
|
|
@ -270,9 +278,7 @@ class HaikuRAGApp:
|
||||||
deep: Use deep QA mode (multi-step reasoning)
|
deep: Use deep QA mode (multi-step reasoning)
|
||||||
verbose: Show verbose output
|
verbose: Show verbose output
|
||||||
"""
|
"""
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(db_path=self.db_path, config=self.config) as self.client:
|
||||||
db_path=self.db_path, config=self.config, read_only=True
|
|
||||||
) as self.client:
|
|
||||||
try:
|
try:
|
||||||
if deep:
|
if deep:
|
||||||
from haiku.rag.graph.deep_qa.dependencies import DeepQAContext
|
from haiku.rag.graph.deep_qa.dependencies import DeepQAContext
|
||||||
|
|
@ -317,9 +323,7 @@ class HaikuRAGApp:
|
||||||
question: The research question
|
question: The research question
|
||||||
verbose: Show AG-UI event stream during execution
|
verbose: Show AG-UI event stream during execution
|
||||||
"""
|
"""
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(db_path=self.db_path, config=self.config) as client:
|
||||||
db_path=self.db_path, config=self.config, read_only=True
|
|
||||||
) as client:
|
|
||||||
try:
|
try:
|
||||||
self.console.print("[bold cyan]Starting research[/bold cyan]")
|
self.console.print("[bold cyan]Starting research[/bold cyan]")
|
||||||
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
||||||
|
|
|
||||||
|
|
@ -410,7 +410,19 @@ def create_index(
|
||||||
asyncio.run(app.create_index())
|
asyncio.run(app.create_index())
|
||||||
|
|
||||||
|
|
||||||
@cli.command("info", help="Show read-only database info (no upgrades or writes)")
|
@cli.command("init", help="Initialize a new database")
|
||||||
|
def init_db(
|
||||||
|
db: Path | None = typer.Option(
|
||||||
|
None,
|
||||||
|
"--db",
|
||||||
|
help="Path to the LanceDB database file",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
app = create_app(db)
|
||||||
|
asyncio.run(app.init())
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("info", help="Show database info")
|
||||||
def info(
|
def info(
|
||||||
db: Path | None = typer.Option(
|
db: Path | None = typer.Option(
|
||||||
None,
|
None,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class HaikuRAG:
|
||||||
db_path: Path | None = None,
|
db_path: Path | None = None,
|
||||||
config: AppConfig = Config,
|
config: AppConfig = Config,
|
||||||
skip_validation: bool = False,
|
skip_validation: bool = False,
|
||||||
read_only: bool = False,
|
create: bool = False,
|
||||||
):
|
):
|
||||||
"""Initialize the RAG client with a database path.
|
"""Initialize the RAG client with a database path.
|
||||||
|
|
||||||
|
|
@ -46,8 +46,7 @@ class HaikuRAG:
|
||||||
db_path: Path to the database file. If None, uses config.storage.data_dir.
|
db_path: Path to the database file. If None, uses config.storage.data_dir.
|
||||||
config: Configuration to use. Defaults to global Config.
|
config: Configuration to use. Defaults to global Config.
|
||||||
skip_validation: Whether to skip configuration validation on database load.
|
skip_validation: Whether to skip configuration validation on database load.
|
||||||
read_only: Whether to open in read-only mode. If True, will raise error
|
create: Whether to create the database if it doesn't exist.
|
||||||
if database doesn't exist and will skip upgrades.
|
|
||||||
"""
|
"""
|
||||||
self._config = config
|
self._config = config
|
||||||
if db_path is None:
|
if db_path is None:
|
||||||
|
|
@ -56,7 +55,7 @@ class HaikuRAG:
|
||||||
db_path,
|
db_path,
|
||||||
config=self._config,
|
config=self._config,
|
||||||
skip_validation=skip_validation,
|
skip_validation=skip_validation,
|
||||||
read_only=read_only,
|
create=create,
|
||||||
)
|
)
|
||||||
self.document_repository = DocumentRepository(self.store)
|
self.document_repository = DocumentRepository(self.store)
|
||||||
self.chunk_repository = ChunkRepository(self.store)
|
self.chunk_repository = ChunkRepository(self.store)
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ class InspectorApp(App): # type: ignore[misc]
|
||||||
async def on_mount(self) -> None:
|
async def on_mount(self) -> None:
|
||||||
"""Initialize the app when mounted."""
|
"""Initialize the app when mounted."""
|
||||||
config = get_config()
|
config = get_config()
|
||||||
self.client = HaikuRAG(db_path=self.db_path, config=config, read_only=True)
|
self.client = HaikuRAG(db_path=self.db_path, config=config)
|
||||||
await self.client.__aenter__()
|
await self.client.__aenter__()
|
||||||
|
|
||||||
# Load initial documents
|
# Load initial documents
|
||||||
|
|
|
||||||
|
|
@ -54,35 +54,41 @@ class Store:
|
||||||
db_path: Path,
|
db_path: Path,
|
||||||
config: AppConfig = Config,
|
config: AppConfig = Config,
|
||||||
skip_validation: bool = False,
|
skip_validation: bool = False,
|
||||||
read_only: bool = False,
|
create: bool = False,
|
||||||
):
|
):
|
||||||
self.db_path: Path = db_path
|
self.db_path: Path = db_path
|
||||||
self._config = config
|
self._config = config
|
||||||
self.embedder = get_embedder(config=self._config)
|
self.embedder = get_embedder(config=self._config)
|
||||||
self._vacuum_lock = asyncio.Lock()
|
self._vacuum_lock = asyncio.Lock()
|
||||||
self._read_only = read_only
|
|
||||||
|
|
||||||
# Create the ChunkRecord model with the correct vector dimension
|
# Create the ChunkRecord model with the correct vector dimension
|
||||||
self.ChunkRecord = create_chunk_model(self.embedder._vector_dim)
|
self.ChunkRecord = create_chunk_model(self.embedder._vector_dim)
|
||||||
|
|
||||||
# Local filesystem handling for DB directory
|
# Check if database exists (for local filesystem only)
|
||||||
|
is_new_db = False
|
||||||
if not self._has_cloud_config():
|
if not self._has_cloud_config():
|
||||||
if read_only:
|
if not db_path.exists():
|
||||||
# Read operations should not create the database
|
if not create:
|
||||||
if not db_path.exists():
|
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Database does not exist: {db_path}. Use a write operation (add, add-src) to create it."
|
f"Database does not exist at {db_path}. "
|
||||||
|
"Use 'haiku-rag init' to create a new database."
|
||||||
)
|
)
|
||||||
else:
|
is_new_db = True
|
||||||
# Write operations - ensure parent directories exist
|
# Ensure parent directories exist for new databases
|
||||||
if not db_path.parent.exists():
|
if not db_path.parent.exists():
|
||||||
Path.mkdir(db_path.parent, parents=True)
|
Path.mkdir(db_path.parent, parents=True)
|
||||||
|
|
||||||
# Connect to LanceDB
|
# Connect to LanceDB
|
||||||
self.db = self._connect_to_lancedb(db_path)
|
self.db = self._connect_to_lancedb(db_path)
|
||||||
|
|
||||||
# Initialize tables
|
# Initialize tables (creates them if they don't exist)
|
||||||
self.create_or_update_db()
|
self._init_tables()
|
||||||
|
|
||||||
|
# Run upgrades only on existing databases, set version for new ones
|
||||||
|
if is_new_db:
|
||||||
|
self._set_initial_version()
|
||||||
|
else:
|
||||||
|
self._run_upgrades()
|
||||||
|
|
||||||
# Validate config compatibility after connection is established
|
# Validate config compatibility after connection is established
|
||||||
if not skip_validation:
|
if not skip_validation:
|
||||||
|
|
@ -234,9 +240,8 @@ class Store:
|
||||||
settings_repo = SettingsRepository(self)
|
settings_repo = SettingsRepository(self)
|
||||||
settings_repo.validate_config_compatibility()
|
settings_repo.validate_config_compatibility()
|
||||||
|
|
||||||
def create_or_update_db(self):
|
def _init_tables(self):
|
||||||
"""Create the database tables."""
|
"""Initialize database tables (create if they don't exist)."""
|
||||||
|
|
||||||
# Get list of existing tables
|
# Get list of existing tables
|
||||||
existing_tables = self.db.table_names()
|
existing_tables = self.db.table_names()
|
||||||
|
|
||||||
|
|
@ -271,44 +276,29 @@ class Store:
|
||||||
[SettingsRecord(id="settings", settings=json.dumps(settings_data))]
|
[SettingsRecord(id="settings", settings=json.dumps(settings_data))]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run pending upgrades based on stored version and package version
|
def _set_initial_version(self):
|
||||||
# Skip in read-only mode to avoid modifying the database
|
"""Set the initial version for a new database."""
|
||||||
if not self._read_only:
|
self.set_haiku_version(metadata.version("haiku.rag-slim"))
|
||||||
try:
|
|
||||||
from haiku.rag.store.upgrades import run_pending_upgrades
|
|
||||||
|
|
||||||
current_version = metadata.version("haiku.rag-slim")
|
def _run_upgrades(self):
|
||||||
db_version = self.get_haiku_version()
|
"""Run pending database upgrades."""
|
||||||
|
try:
|
||||||
|
from haiku.rag.store.upgrades import run_pending_upgrades
|
||||||
|
|
||||||
if db_version != "0.0.0":
|
current_version = metadata.version("haiku.rag-slim")
|
||||||
run_pending_upgrades(self, db_version, current_version)
|
db_version = self.get_haiku_version()
|
||||||
|
|
||||||
# After upgrades complete (or if none), set stored version
|
run_pending_upgrades(self, db_version, current_version)
|
||||||
# to the greater of the installed package version and the
|
|
||||||
# highest available upgrade step version in code.
|
|
||||||
try:
|
|
||||||
from packaging.version import parse as _v
|
|
||||||
|
|
||||||
from haiku.rag.store.upgrades import upgrades as _steps
|
self.set_haiku_version(current_version)
|
||||||
|
except Exception as e:
|
||||||
highest_step = max((_v(u.version) for u in _steps), default=None)
|
# Avoid hard failure on initial connection; log and continue so CLI remains usable.
|
||||||
effective_version = (
|
logger.warning(
|
||||||
str(max(_v(current_version), highest_step))
|
"Skipping upgrade due to error (db=%s -> pkg=%s): %s",
|
||||||
if highest_step is not None
|
self.get_haiku_version(),
|
||||||
else current_version
|
metadata.version("haiku.rag-slim"),
|
||||||
)
|
e,
|
||||||
except Exception:
|
)
|
||||||
effective_version = current_version
|
|
||||||
|
|
||||||
self.set_haiku_version(effective_version)
|
|
||||||
except Exception as e:
|
|
||||||
# Avoid hard failure on initial connection; log and continue so CLI remains usable.
|
|
||||||
logger.warning(
|
|
||||||
"Skipping upgrade due to error (db=%s -> pkg=%s): %s",
|
|
||||||
self.get_haiku_version(),
|
|
||||||
metadata.version("haiku.rag-slim"),
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_haiku_version(self) -> str:
|
def get_haiku_version(self) -> str:
|
||||||
"""Returns the user version stored in settings."""
|
"""Returns the user version stored in settings."""
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@ def qa_corpus() -> Dataset:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temp_db_path():
|
def temp_db_path():
|
||||||
"""Create a temporary database path for testing."""
|
"""Create a temporary database path for testing.
|
||||||
|
|
||||||
|
Note: Tests that need a database should use HaikuRAG with create=True.
|
||||||
|
"""
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
yield Path(temp_dir) / "test.lancedb"
|
yield Path(temp_dir) / "test.lancedb"
|
||||||
|
|
||||||
|
|
@ -43,11 +46,13 @@ def temp_yaml_config(tmp_path, monkeypatch):
|
||||||
"vacuum_retention_seconds": 60,
|
"vacuum_retention_seconds": 60,
|
||||||
},
|
},
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
"provider": "ollama",
|
"model": {
|
||||||
"model": "qwen3-embedding:4b",
|
"provider": "ollama",
|
||||||
"vector_dim": 2560,
|
"name": "qwen3-embedding:4b",
|
||||||
|
"vector_dim": 2560,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"qa": {"provider": "ollama", "model": "gpt-oss"},
|
"qa": {"model": {"provider": "ollama", "name": "gpt-oss"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ async def test_deep_qa_graph_end_to_end(monkeypatch, temp_db_path):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use real client but with TestModel for LLM calls
|
# Use real client but with TestModel for LLM calls
|
||||||
client = HaikuRAG(temp_db_path)
|
client = HaikuRAG(temp_db_path, create=True)
|
||||||
deps = DeepQADeps(client=client)
|
deps = DeepQADeps(client=client)
|
||||||
|
|
||||||
result = await graph.run(state=state, deps=deps)
|
result = await graph.run(state=state, deps=deps)
|
||||||
|
|
@ -67,7 +67,7 @@ async def test_deep_qa_with_citations(monkeypatch, temp_db_path):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use real client but with TestModel for LLM calls
|
# Use real client but with TestModel for LLM calls
|
||||||
client = HaikuRAG(temp_db_path)
|
client = HaikuRAG(temp_db_path, create=True)
|
||||||
deps = DeepQADeps(client=client)
|
deps = DeepQADeps(client=client)
|
||||||
|
|
||||||
result = await graph.run(state=state, deps=deps)
|
result = await graph.run(state=state, deps=deps)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ async def test_graph_end_to_end_with_test_model(monkeypatch, temp_db_path):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use real client but with TestModel for LLM calls
|
# Use real client but with TestModel for LLM calls
|
||||||
client = HaikuRAG(temp_db_path)
|
client = HaikuRAG(temp_db_path, create=True)
|
||||||
deps = ResearchDeps(client=client)
|
deps = ResearchDeps(client=client)
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from haiku.rag.store.repositories.document import DocumentRepository
|
||||||
async def test_chunk_repository_operations(qa_corpus: Dataset, temp_db_path):
|
async def test_chunk_repository_operations(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test ChunkRepository operations."""
|
"""Test ChunkRepository operations."""
|
||||||
# Create client
|
# Create client
|
||||||
client = HaikuRAG(db_path=temp_db_path, config=Config)
|
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
|
||||||
|
|
||||||
# Get the first document from the corpus
|
# Get the first document from the corpus
|
||||||
first_doc = qa_corpus[0]
|
first_doc = qa_corpus[0]
|
||||||
|
|
@ -56,7 +56,7 @@ async def test_chunk_repository_operations(qa_corpus: Dataset, temp_db_path):
|
||||||
async def test_create_chunks_for_document(qa_corpus: Dataset, temp_db_path):
|
async def test_create_chunks_for_document(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test creating chunks for a document."""
|
"""Test creating chunks for a document."""
|
||||||
# Create a store and repositories
|
# Create a store and repositories
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
chunk_repo = ChunkRepository(store)
|
chunk_repo = ChunkRepository(store)
|
||||||
doc_repo = DocumentRepository(store)
|
doc_repo = DocumentRepository(store)
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@ async def test_create_chunks_for_document(qa_corpus: Dataset, temp_db_path):
|
||||||
async def test_chunk_repository_crud(temp_db_path):
|
async def test_chunk_repository_crud(temp_db_path):
|
||||||
"""Test basic CRUD operations in ChunkRepository."""
|
"""Test basic CRUD operations in ChunkRepository."""
|
||||||
# Create a store
|
# Create a store
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
chunk_repo = ChunkRepository(store)
|
chunk_repo = ChunkRepository(store)
|
||||||
doc_repo = DocumentRepository(store)
|
doc_repo = DocumentRepository(store)
|
||||||
|
|
||||||
|
|
@ -151,7 +151,7 @@ async def test_chunk_repository_crud(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_adjacent_chunks(temp_db_path):
|
async def test_adjacent_chunks(temp_db_path):
|
||||||
"""Test the get_adjacent_chunks repository method."""
|
"""Test the get_adjacent_chunks repository method."""
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
doc_repo = DocumentRepository(store)
|
doc_repo = DocumentRepository(store)
|
||||||
chunk_repo = ChunkRepository(store)
|
chunk_repo = ChunkRepository(store)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,18 @@ def test_ask_with_deep_and_verbose():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
with patch("haiku.rag.cli.HaikuRAGApp") as mock_app:
|
||||||
|
mock_app_instance = MagicMock()
|
||||||
|
mock_app_instance.init = AsyncMock()
|
||||||
|
mock_app.return_value = mock_app_instance
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["init"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
mock_app_instance.init.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_info():
|
def test_info():
|
||||||
with patch("haiku.rag.cli.HaikuRAGApp") as mock_app:
|
with patch("haiku.rag.cli.HaikuRAGApp") as mock_app:
|
||||||
mock_app_instance = MagicMock()
|
mock_app_instance = MagicMock()
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from haiku.rag.store.models.document import Document
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_document_crud(qa_corpus: Dataset, temp_db_path):
|
async def test_client_document_crud(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test HaikuRAG CRUD operations for documents."""
|
"""Test HaikuRAG CRUD operations for documents."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Get test data
|
# Get test data
|
||||||
first_doc = qa_corpus[0]
|
first_doc = qa_corpus[0]
|
||||||
document_text = first_doc["document_extracted"]
|
document_text = first_doc["document_extracted"]
|
||||||
|
|
@ -81,7 +81,7 @@ async def test_client_document_crud(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_update_document_fields(qa_corpus: Dataset, temp_db_path):
|
async def test_client_update_document_fields(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test updating document with individual parameters."""
|
"""Test updating document with individual parameters."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Get test data
|
# Get test data
|
||||||
first_doc = qa_corpus[0]
|
first_doc = qa_corpus[0]
|
||||||
document_text = first_doc["document_extracted"]
|
document_text = first_doc["document_extracted"]
|
||||||
|
|
@ -152,7 +152,7 @@ async def test_client_update_document_fields(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_from_source(temp_db_path):
|
async def test_client_create_document_from_source(temp_db_path):
|
||||||
"""Test creating a document from a file source."""
|
"""Test creating a document from a file source."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
test_content = "This is test content from a file."
|
test_content = "This is test content from a file."
|
||||||
temp_path = Path(temp_dir) / "test.txt"
|
temp_path = Path(temp_dir) / "test.txt"
|
||||||
|
|
@ -183,7 +183,7 @@ async def test_client_create_document_from_source(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_from_source_with_title(temp_db_path):
|
async def test_client_create_document_from_source_with_title(temp_db_path):
|
||||||
"""Test creating a document from a file source with a title."""
|
"""Test creating a document from a file source with a title."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
test_content = "This is test content from a file."
|
test_content = "This is test content from a file."
|
||||||
temp_path = Path(temp_dir) / "test_title.txt"
|
temp_path = Path(temp_dir) / "test_title.txt"
|
||||||
|
|
@ -200,7 +200,7 @@ async def test_client_create_document_from_source_with_title(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_update_title_noop_behavior(temp_db_path):
|
async def test_client_update_title_noop_behavior(temp_db_path):
|
||||||
"""When content is unchanged, updating title should update document without re-chunking."""
|
"""When content is unchanged, updating title should update document without re-chunking."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
temp_path = Path(temp_dir) / "test_update_title.txt"
|
temp_path = Path(temp_dir) / "test_update_title.txt"
|
||||||
temp_path.write_text("Original content")
|
temp_path.write_text("Original content")
|
||||||
|
|
@ -222,7 +222,7 @@ async def test_client_update_title_noop_behavior(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_from_source_unsupported(temp_db_path):
|
async def test_client_create_document_from_source_unsupported(temp_db_path):
|
||||||
"""Test creating a document from an unsupported file type."""
|
"""Test creating a document from an unsupported file type."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create a temporary file with unsupported extension
|
# Create a temporary file with unsupported extension
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
mode="w", suffix=".unsupported", delete=False
|
mode="w", suffix=".unsupported", delete=False
|
||||||
|
|
@ -238,7 +238,7 @@ async def test_client_create_document_from_source_unsupported(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_from_source_nonexistent(temp_db_path):
|
async def test_client_create_document_from_source_nonexistent(temp_db_path):
|
||||||
"""Test creating a document from a non-existent file."""
|
"""Test creating a document from a non-existent file."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
non_existent_path = Path("/non/existent/file.txt")
|
non_existent_path = Path("/non/existent/file.txt")
|
||||||
|
|
||||||
# Should raise ValueError when file doesn't exist
|
# Should raise ValueError when file doesn't exist
|
||||||
|
|
@ -249,7 +249,7 @@ async def test_client_create_document_from_source_nonexistent(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_from_directory(temp_db_path):
|
async def test_client_create_document_from_directory(temp_db_path):
|
||||||
"""Test creating documents from a directory recursively."""
|
"""Test creating documents from a directory recursively."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
test_dir = Path(temp_dir) / "test_docs"
|
test_dir = Path(temp_dir) / "test_docs"
|
||||||
test_dir.mkdir()
|
test_dir.mkdir()
|
||||||
|
|
@ -294,7 +294,7 @@ async def test_client_create_document_from_directory_with_filters(
|
||||||
"haiku.rag.client.Config.monitor.include_patterns", ["**/include/**/*.txt"]
|
"haiku.rag.client.Config.monitor.include_patterns", ["**/include/**/*.txt"]
|
||||||
)
|
)
|
||||||
|
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
test_dir = Path(temp_dir) / "test_docs"
|
test_dir = Path(temp_dir) / "test_docs"
|
||||||
test_dir.mkdir()
|
test_dir.mkdir()
|
||||||
|
|
@ -333,7 +333,7 @@ async def test_client_create_document_from_directory_with_filters(
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_from_url(temp_db_path):
|
async def test_client_create_document_from_url(temp_db_path):
|
||||||
"""Test creating a document from a URL."""
|
"""Test creating a document from a URL."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Mock the HTTP response
|
# Mock the HTTP response
|
||||||
mock_response = AsyncMock()
|
mock_response = AsyncMock()
|
||||||
mock_response.content = b"<html><body><h1>Test Page</h1><p>This is test content from a webpage.</p></body></html>"
|
mock_response.content = b"<html><body><h1>Test Page</h1><p>This is test content from a webpage.</p></body></html>"
|
||||||
|
|
@ -361,7 +361,7 @@ async def test_client_create_document_from_url_with_different_content_types(
|
||||||
temp_db_path,
|
temp_db_path,
|
||||||
):
|
):
|
||||||
"""Test creating documents from URLs with different content types."""
|
"""Test creating documents from URLs with different content types."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Test JSON content
|
# Test JSON content
|
||||||
mock_json_response = AsyncMock()
|
mock_json_response = AsyncMock()
|
||||||
mock_json_response.content = (
|
mock_json_response.content = (
|
||||||
|
|
@ -406,7 +406,7 @@ async def test_client_create_document_from_url_with_different_content_types(
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_from_url_unsupported_content(temp_db_path):
|
async def test_client_create_document_from_url_unsupported_content(temp_db_path):
|
||||||
"""Test creating a document from URL with unsupported content type."""
|
"""Test creating a document from URL with unsupported content type."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Mock response with unsupported content type
|
# Mock response with unsupported content type
|
||||||
mock_response = AsyncMock()
|
mock_response = AsyncMock()
|
||||||
mock_response.content = b"binary content"
|
mock_response.content = b"binary content"
|
||||||
|
|
@ -423,7 +423,7 @@ async def test_client_create_document_from_url_unsupported_content(temp_db_path)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_from_url_http_error(temp_db_path):
|
async def test_client_create_document_from_url_http_error(temp_db_path):
|
||||||
"""Test handling HTTP errors when creating document from URL."""
|
"""Test handling HTTP errors when creating document from URL."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
with patch("httpx.AsyncClient.get") as mock_get:
|
with patch("httpx.AsyncClient.get") as mock_get:
|
||||||
mock_get.side_effect = httpx.HTTPStatusError(
|
mock_get.side_effect = httpx.HTTPStatusError(
|
||||||
"404 Not Found",
|
"404 Not Found",
|
||||||
|
|
@ -440,7 +440,7 @@ async def test_client_create_document_from_url_http_error(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_extension_from_content_type_or_url(temp_db_path):
|
async def test_get_extension_from_content_type_or_url(temp_db_path):
|
||||||
"""Test the helper method for determining file extensions."""
|
"""Test the helper method for determining file extensions."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Test content type mappings
|
# Test content type mappings
|
||||||
assert (
|
assert (
|
||||||
client._get_extension_from_content_type_or_url("", "text/html") == ".html"
|
client._get_extension_from_content_type_or_url("", "text/html") == ".html"
|
||||||
|
|
@ -487,7 +487,7 @@ async def test_client_metadata_content_type_and_md5(temp_db_path):
|
||||||
"""Test that contentType and md5 metadata are correctly set."""
|
"""Test that contentType and md5 metadata are correctly set."""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create a temporary file with known content
|
# Create a temporary file with known content
|
||||||
test_content = "Test content for MD5 calculation."
|
test_content = "Test content for MD5 calculation."
|
||||||
expected_md5 = hashlib.md5(test_content.encode()).hexdigest()
|
expected_md5 = hashlib.md5(test_content.encode()).hexdigest()
|
||||||
|
|
@ -520,7 +520,7 @@ async def test_client_metadata_content_type_and_md5(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_update_no_op_behavior(temp_db_path):
|
async def test_client_create_update_no_op_behavior(temp_db_path):
|
||||||
"""Test create/update/no-op behavior based on MD5 changes."""
|
"""Test create/update/no-op behavior based on MD5 changes."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create a temporary file
|
# Create a temporary file
|
||||||
test_content = "Original content for testing."
|
test_content = "Original content for testing."
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
|
@ -559,7 +559,7 @@ async def test_client_create_update_no_op_behavior(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_unchanged_file_keeps_timestamp(temp_db_path):
|
async def test_client_unchanged_file_keeps_timestamp(temp_db_path):
|
||||||
"""Test that unchanged files don't update the updated_at timestamp."""
|
"""Test that unchanged files don't update the updated_at timestamp."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create a temporary file
|
# Create a temporary file
|
||||||
test_content = "Test content for timestamp check."
|
test_content = "Test content for timestamp check."
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
|
@ -581,7 +581,7 @@ async def test_client_unchanged_file_keeps_timestamp(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_url_create_update_no_op_behavior(temp_db_path):
|
async def test_client_url_create_update_no_op_behavior(temp_db_path):
|
||||||
"""Test create/update/no-op behavior for URLs based on MD5 changes."""
|
"""Test create/update/no-op behavior for URLs based on MD5 changes."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
url = "https://example.com/test.txt"
|
url = "https://example.com/test.txt"
|
||||||
original_content = b"Original URL content"
|
original_content = b"Original URL content"
|
||||||
updated_content = b"Updated URL content"
|
updated_content = b"Updated URL content"
|
||||||
|
|
@ -620,7 +620,7 @@ async def test_client_url_create_update_no_op_behavior(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_search(temp_db_path):
|
async def test_client_search(temp_db_path):
|
||||||
"""Test HaikuRAG search functionality."""
|
"""Test HaikuRAG search functionality."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Add multiple documents to search from
|
# Add multiple documents to search from
|
||||||
doc1_text = "Python is a high-level programming language known for its simplicity and readability."
|
doc1_text = "Python is a high-level programming language known for its simplicity and readability."
|
||||||
doc2_text = "Machine learning algorithms help computers learn patterns from data without explicit programming."
|
doc2_text = "Machine learning algorithms help computers learn patterns from data without explicit programming."
|
||||||
|
|
@ -665,7 +665,7 @@ async def test_client_async_context_manager(temp_db_path):
|
||||||
"""Test HaikuRAG as async context manager."""
|
"""Test HaikuRAG as async context manager."""
|
||||||
|
|
||||||
# Test that context manager works and auto-closes
|
# Test that context manager works and auto-closes
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create a document to ensure the client works
|
# Create a document to ensure the client works
|
||||||
doc = await client.create_document(
|
doc = await client.create_document(
|
||||||
content="Test content for context manager",
|
content="Test content for context manager",
|
||||||
|
|
@ -688,7 +688,7 @@ async def test_client_async_context_manager(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_create_document_with_custom_chunks(temp_db_path):
|
async def test_client_create_document_with_custom_chunks(temp_db_path):
|
||||||
"""Test creating a document with pre-created chunks."""
|
"""Test creating a document with pre-created chunks."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create some custom chunks with and without embeddings
|
# Create some custom chunks with and without embeddings
|
||||||
chunks = [
|
chunks = [
|
||||||
Chunk(
|
Chunk(
|
||||||
|
|
@ -741,7 +741,7 @@ async def test_client_ask_without_cite(monkeypatch, temp_db_path):
|
||||||
"haiku.rag.utils.get_model", lambda *args, **kwargs: TestModel()
|
"haiku.rag.utils.get_model", lambda *args, **kwargs: TestModel()
|
||||||
)
|
)
|
||||||
|
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create a test document for the agent to search
|
# Create a test document for the agent to search
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Python is a high-level programming language.", uri="test.txt"
|
content="Python is a high-level programming language.", uri="test.txt"
|
||||||
|
|
@ -765,7 +765,7 @@ async def test_client_ask_with_cite(monkeypatch, temp_db_path):
|
||||||
"haiku.rag.utils.get_model", lambda *args, **kwargs: TestModel()
|
"haiku.rag.utils.get_model", lambda *args, **kwargs: TestModel()
|
||||||
)
|
)
|
||||||
|
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create a test document
|
# Create a test document
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Python is a high-level programming language.", uri="test.txt"
|
content="Python is a high-level programming language.", uri="test.txt"
|
||||||
|
|
@ -784,7 +784,7 @@ async def test_client_expand_context(temp_db_path):
|
||||||
"""Test expanding search results with adjacent chunks."""
|
"""Test expanding search results with adjacent chunks."""
|
||||||
# Mock Config to have CONTEXT_CHUNK_RADIUS = 2
|
# Mock Config to have CONTEXT_CHUNK_RADIUS = 2
|
||||||
with patch("haiku.rag.client.Config.processing.context_chunk_radius", 2):
|
with patch("haiku.rag.client.Config.processing.context_chunk_radius", 2):
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create chunks manually with precomputed embeddings to avoid network
|
# Create chunks manually with precomputed embeddings to avoid network
|
||||||
dim = client.chunk_repository.embedder._vector_dim
|
dim = client.chunk_repository.embedder._vector_dim
|
||||||
z = [0.0] * dim
|
z = [0.0] * dim
|
||||||
|
|
@ -836,7 +836,7 @@ async def test_client_expand_context(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_expand_context_radius_zero(temp_db_path):
|
async def test_client_expand_context_radius_zero(temp_db_path):
|
||||||
"""Test expand_context with radius 0 returns original results."""
|
"""Test expand_context with radius 0 returns original results."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create a simple document
|
# Create a simple document
|
||||||
doc = await client.create_document(content="Simple test content")
|
doc = await client.create_document(content="Simple test content")
|
||||||
assert doc.id is not None
|
assert doc.id is not None
|
||||||
|
|
@ -853,7 +853,7 @@ async def test_client_expand_context_radius_zero(temp_db_path):
|
||||||
async def test_client_expand_context_multiple_chunks(temp_db_path):
|
async def test_client_expand_context_multiple_chunks(temp_db_path):
|
||||||
"""Test expand_context with multiple search results."""
|
"""Test expand_context with multiple search results."""
|
||||||
with patch("haiku.rag.client.Config.processing.context_chunk_radius", 1):
|
with patch("haiku.rag.client.Config.processing.context_chunk_radius", 1):
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create first document with manual chunks
|
# Create first document with manual chunks
|
||||||
doc1_chunks = [
|
doc1_chunks = [
|
||||||
Chunk(content="Doc1 Part A", order=0),
|
Chunk(content="Doc1 Part A", order=0),
|
||||||
|
|
@ -906,7 +906,7 @@ async def test_client_expand_context_multiple_chunks(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_expand_context_merges_overlapping_chunks(temp_db_path):
|
async def test_client_expand_context_merges_overlapping_chunks(temp_db_path):
|
||||||
"""Test that overlapping expanded chunks are merged into one."""
|
"""Test that overlapping expanded chunks are merged into one."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create document with 5 chunks
|
# Create document with 5 chunks
|
||||||
manual_chunks = [
|
manual_chunks = [
|
||||||
Chunk(content="Chunk 0", order=0),
|
Chunk(content="Chunk 0", order=0),
|
||||||
|
|
@ -953,7 +953,7 @@ async def test_client_expand_context_merges_overlapping_chunks(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_client_expand_context_keeps_separate_non_overlapping(temp_db_path):
|
async def test_client_expand_context_keeps_separate_non_overlapping(temp_db_path):
|
||||||
"""Test that non-overlapping expanded chunks remain separate."""
|
"""Test that non-overlapping expanded chunks remain separate."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
# Create document with chunks far apart
|
# Create document with chunks far apart
|
||||||
manual_chunks = [
|
manual_chunks = [
|
||||||
Chunk(content="Chunk 0", order=0),
|
Chunk(content="Chunk 0", order=0),
|
||||||
|
|
|
||||||
|
|
@ -7,93 +7,43 @@ from haiku.rag.client import HaikuRAG
|
||||||
from haiku.rag.config import AppConfig
|
from haiku.rag.config import AppConfig
|
||||||
|
|
||||||
|
|
||||||
def test_read_operations_do_not_create_database():
|
def test_database_not_created_without_create_flag():
|
||||||
"""Test that read operations fail if database doesn't exist."""
|
"""Test that database is not created without create=True."""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
db_path = Path(tmpdir) / "test.lancedb"
|
db_path = Path(tmpdir) / "test.lancedb"
|
||||||
|
|
||||||
config = AppConfig()
|
config = AppConfig()
|
||||||
|
|
||||||
# Read operation with read_only=True should fail
|
with pytest.raises(FileNotFoundError, match="Database does not exist"):
|
||||||
with pytest.raises(
|
HaikuRAG(db_path=db_path, config=config)
|
||||||
FileNotFoundError,
|
|
||||||
match="Database does not exist.*Use a write operation",
|
|
||||||
):
|
|
||||||
HaikuRAG(db_path=db_path, config=config, read_only=True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_write_operations_create_database():
|
def test_database_created_with_create_flag():
|
||||||
"""Test that write operations create the database."""
|
"""Test that database is created with create=True."""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
db_path = Path(tmpdir) / "test.lancedb"
|
db_path = Path(tmpdir) / "test.lancedb"
|
||||||
|
|
||||||
config = AppConfig()
|
config = AppConfig()
|
||||||
|
|
||||||
# Write operation with read_only=False (default) should succeed
|
client = HaikuRAG(db_path=db_path, config=config, create=True)
|
||||||
client = HaikuRAG(db_path=db_path, config=config, read_only=False)
|
|
||||||
assert db_path.exists()
|
assert db_path.exists()
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
async def test_add_document_creates_database():
|
@pytest.mark.asyncio
|
||||||
"""Test that add operations create the database."""
|
async def test_operations_work_after_database_created():
|
||||||
|
"""Test that operations work after DB is created."""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
db_path = Path(tmpdir) / "test.lancedb"
|
db_path = Path(tmpdir) / "test.lancedb"
|
||||||
|
|
||||||
config = AppConfig()
|
config = AppConfig()
|
||||||
|
|
||||||
# Create a document (write operation) should work and create DB
|
# First, create DB with create=True and add document
|
||||||
async with HaikuRAG(db_path=db_path, config=config, read_only=False) as client:
|
async with HaikuRAG(db_path=db_path, config=config, create=True) as client:
|
||||||
doc = await client.create_document("Test content")
|
|
||||||
assert doc.id is not None
|
|
||||||
assert doc.content == "Test content"
|
|
||||||
assert db_path.exists()
|
|
||||||
|
|
||||||
|
|
||||||
async def test_search_fails_if_database_does_not_exist():
|
|
||||||
"""Test that search operations fail if DB doesn't exist."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
db_path = Path(tmpdir) / "test.lancedb"
|
|
||||||
|
|
||||||
config = AppConfig()
|
|
||||||
|
|
||||||
# Read operation (search) should fail if DB doesn't exist
|
|
||||||
with pytest.raises(
|
|
||||||
FileNotFoundError,
|
|
||||||
match="Database does not exist.*Use a write operation",
|
|
||||||
):
|
|
||||||
async with HaikuRAG(
|
|
||||||
db_path=db_path, config=config, read_only=True
|
|
||||||
) as client:
|
|
||||||
await client.search("test query")
|
|
||||||
|
|
||||||
|
|
||||||
async def test_read_operations_work_after_database_created():
|
|
||||||
"""Test that read operations work after DB is created via write operation."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
db_path = Path(tmpdir) / "test.lancedb"
|
|
||||||
|
|
||||||
config = AppConfig()
|
|
||||||
|
|
||||||
# First, create DB via write operation
|
|
||||||
async with HaikuRAG(db_path=db_path, config=config, read_only=False) as client:
|
|
||||||
await client.create_document("Test content", uri="test://doc1")
|
await client.create_document("Test content", uri="test://doc1")
|
||||||
|
|
||||||
# Now read operations should work since DB exists
|
# Re-open without create flag and verify we can read the document
|
||||||
async with HaikuRAG(db_path=db_path, config=config, read_only=True) as client:
|
async with HaikuRAG(db_path=db_path, config=config) as client:
|
||||||
docs = await client.list_documents()
|
docs = await client.list_documents()
|
||||||
assert len(docs) == 1
|
assert len(docs) == 1
|
||||||
assert docs[0].content == "Test content"
|
assert docs[0].content == "Test content"
|
||||||
|
|
||||||
|
|
||||||
def test_default_read_only_is_false():
|
|
||||||
"""Test that read_only defaults to False for backward compatibility."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
db_path = Path(tmpdir) / "test.lancedb"
|
|
||||||
|
|
||||||
config = AppConfig()
|
|
||||||
|
|
||||||
# Without specifying read_only, it should default to False (allow creation)
|
|
||||||
client = HaikuRAG(db_path=db_path, config=config)
|
|
||||||
assert db_path.exists()
|
|
||||||
client.close()
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from haiku.rag.store.repositories.document import DocumentRepository
|
||||||
async def test_create_document_with_chunks(qa_corpus: Dataset, temp_db_path):
|
async def test_create_document_with_chunks(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test creating a document with chunks from the qa_corpus using repository."""
|
"""Test creating a document with chunks from the qa_corpus using repository."""
|
||||||
# Create client
|
# Create client
|
||||||
client = HaikuRAG(db_path=temp_db_path, config=Config)
|
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
|
||||||
|
|
||||||
# Get the first document from the corpus
|
# Get the first document from the corpus
|
||||||
first_doc = qa_corpus[0]
|
first_doc = qa_corpus[0]
|
||||||
|
|
@ -44,7 +44,7 @@ async def test_create_document_with_chunks(qa_corpus: Dataset, temp_db_path):
|
||||||
async def test_document_repository_crud(qa_corpus: Dataset, temp_db_path):
|
async def test_document_repository_crud(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test CRUD operations in DocumentRepository."""
|
"""Test CRUD operations in DocumentRepository."""
|
||||||
# Create a store and repository
|
# Create a store and repository
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
doc_repo = DocumentRepository(store)
|
doc_repo = DocumentRepository(store)
|
||||||
|
|
||||||
# Get the first document from the corpus
|
# Get the first document from the corpus
|
||||||
|
|
@ -100,7 +100,7 @@ async def test_document_repository_crud(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_document_list_with_filter(qa_corpus: Dataset, temp_db_path):
|
async def test_document_list_with_filter(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test listing documents with filter clause."""
|
"""Test listing documents with filter clause."""
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
doc_repo = DocumentRepository(store)
|
doc_repo = DocumentRepository(store)
|
||||||
|
|
||||||
first_doc = qa_corpus[0]
|
first_doc = qa_corpus[0]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from haiku.rag.client import HaikuRAG
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_search_with_uri_filter(temp_db_path):
|
async def test_search_with_uri_filter(temp_db_path):
|
||||||
"""Test filtering by document URI."""
|
"""Test filtering by document URI."""
|
||||||
async with HaikuRAG(db_path=temp_db_path) as client:
|
async with HaikuRAG(db_path=temp_db_path, create=True) as client:
|
||||||
# Add multiple test documents
|
# Add multiple test documents
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Python tutorial content",
|
content="Python tutorial content",
|
||||||
|
|
@ -40,7 +40,7 @@ async def test_search_with_uri_filter(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_search_with_title_filter(temp_db_path):
|
async def test_search_with_title_filter(temp_db_path):
|
||||||
"""Test filtering by document title."""
|
"""Test filtering by document title."""
|
||||||
async with HaikuRAG(db_path=temp_db_path) as client:
|
async with HaikuRAG(db_path=temp_db_path, create=True) as client:
|
||||||
# Add test documents
|
# Add test documents
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Programming content",
|
content="Programming content",
|
||||||
|
|
@ -66,7 +66,7 @@ async def test_search_with_title_filter(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_search_with_combined_filters(temp_db_path):
|
async def test_search_with_combined_filters(temp_db_path):
|
||||||
"""Test filtering with AND/OR conditions."""
|
"""Test filtering with AND/OR conditions."""
|
||||||
async with HaikuRAG(db_path=temp_db_path) as client:
|
async with HaikuRAG(db_path=temp_db_path, create=True) as client:
|
||||||
# Add test documents
|
# Add test documents
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Content about AI",
|
content="Content about AI",
|
||||||
|
|
@ -105,7 +105,7 @@ async def test_search_with_combined_filters(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_search_with_no_matching_filter(temp_db_path):
|
async def test_search_with_no_matching_filter(temp_db_path):
|
||||||
"""Test that search returns empty results when filter matches no documents."""
|
"""Test that search returns empty results when filter matches no documents."""
|
||||||
async with HaikuRAG(db_path=temp_db_path) as client:
|
async with HaikuRAG(db_path=temp_db_path, create=True) as client:
|
||||||
# Add a test document
|
# Add a test document
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Test content",
|
content="Test content",
|
||||||
|
|
@ -123,7 +123,7 @@ async def test_search_with_no_matching_filter(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_search_with_invalid_filter(temp_db_path):
|
async def test_search_with_invalid_filter(temp_db_path):
|
||||||
"""Test that invalid filter syntax raises an appropriate error."""
|
"""Test that invalid filter syntax raises an appropriate error."""
|
||||||
async with HaikuRAG(db_path=temp_db_path) as client:
|
async with HaikuRAG(db_path=temp_db_path, create=True) as client:
|
||||||
# Add a test document
|
# Add a test document
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Test content",
|
content="Test content",
|
||||||
|
|
@ -139,7 +139,7 @@ async def test_search_with_invalid_filter(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_search_filter_with_all_search_types(temp_db_path):
|
async def test_search_filter_with_all_search_types(temp_db_path):
|
||||||
"""Test that filtering works with all search types (vector, fts, hybrid)."""
|
"""Test that filtering works with all search types (vector, fts, hybrid)."""
|
||||||
async with HaikuRAG(db_path=temp_db_path) as client:
|
async with HaikuRAG(db_path=temp_db_path, create=True) as client:
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
content="Machine learning is a subset of artificial intelligence",
|
content="Machine learning is a subset of artificial intelligence",
|
||||||
uri="https://ai.example.com/ml.html",
|
uri="https://ai.example.com/ml.html",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from haiku.rag.app import HaikuRAGApp
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_app_info_outputs_and_read_only(temp_db_path, capsys):
|
async def test_app_info_outputs(temp_db_path, capsys):
|
||||||
# Build a minimal LanceDB with settings, documents, and chunks without using Store
|
# Build a minimal LanceDB with settings, documents, and chunks without using Store
|
||||||
import lancedb
|
import lancedb
|
||||||
from lancedb.pydantic import LanceModel, Vector
|
from lancedb.pydantic import LanceModel, Vector
|
||||||
|
|
@ -32,7 +32,7 @@ async def test_app_info_outputs_and_read_only(temp_db_path, capsys):
|
||||||
docs_tbl = db.create_table("documents", schema=DocumentRecord)
|
docs_tbl = db.create_table("documents", schema=DocumentRecord)
|
||||||
chunks_tbl = db.create_table("chunks", schema=ChunkRecord)
|
chunks_tbl = db.create_table("chunks", schema=ChunkRecord)
|
||||||
|
|
||||||
# Insert one of each
|
# Insert one of each - using the new config format
|
||||||
settings_tbl.add(
|
settings_tbl.add(
|
||||||
[
|
[
|
||||||
SettingsRecord(
|
SettingsRecord(
|
||||||
|
|
@ -41,9 +41,11 @@ async def test_app_info_outputs_and_read_only(temp_db_path, capsys):
|
||||||
{
|
{
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"embeddings": {
|
"embeddings": {
|
||||||
"provider": "openai",
|
"model": {
|
||||||
"model": "text-embedding-3-small",
|
"provider": "openai",
|
||||||
"vector_dim": 3,
|
"name": "text-embedding-3-small",
|
||||||
|
"vector_dim": 3,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
@ -55,20 +57,13 @@ async def test_app_info_outputs_and_read_only(temp_db_path, capsys):
|
||||||
[ChunkRecord(id="c1", document_id="doc-1", content="c", vector=[0.1, 0.2, 0.3])]
|
[ChunkRecord(id="c1", document_id="doc-1", content="c", vector=[0.1, 0.2, 0.3])]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Capture versions before
|
|
||||||
before_versions = {
|
|
||||||
"settings": int(settings_tbl.version),
|
|
||||||
"documents": int(docs_tbl.version),
|
|
||||||
"chunks": int(chunks_tbl.version),
|
|
||||||
}
|
|
||||||
|
|
||||||
app = HaikuRAGApp(db_path=temp_db_path)
|
app = HaikuRAGApp(db_path=temp_db_path)
|
||||||
await app.info()
|
await app.info()
|
||||||
|
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
# Validate expected content substrings
|
# Validate expected content substrings
|
||||||
assert f"path: \n{temp_db_path}" in out
|
assert f"path: \n{temp_db_path}" in out
|
||||||
assert "haiku.rag version (db): 1.2.3" in out
|
assert "haiku.rag version (db):" in out
|
||||||
assert "embeddings: openai/text-embedding-3-small (dim: 3)" in out
|
assert "embeddings: openai/text-embedding-3-small (dim: 3)" in out
|
||||||
assert "documents: 1" in out
|
assert "documents: 1" in out
|
||||||
assert "chunks: 1" in out
|
assert "chunks: 1" in out
|
||||||
|
|
@ -85,13 +80,6 @@ async def test_app_info_outputs_and_read_only(temp_db_path, capsys):
|
||||||
assert "lancedb:" in out
|
assert "lancedb:" in out
|
||||||
assert "haiku.rag:" in out
|
assert "haiku.rag:" in out
|
||||||
|
|
||||||
# Verify no versions changed (read-only)
|
|
||||||
# Re-open to ensure fresh view
|
|
||||||
db2 = lancedb.connect(temp_db_path)
|
|
||||||
assert int(db2.open_table("settings").version) == before_versions["settings"]
|
|
||||||
assert int(db2.open_table("documents").version) == before_versions["documents"]
|
|
||||||
assert int(db2.open_table("chunks").version) == before_versions["chunks"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_app_info_with_vector_index(temp_db_path, capsys):
|
async def test_app_info_with_vector_index(temp_db_path, capsys):
|
||||||
|
|
@ -148,13 +136,6 @@ async def test_app_info_with_vector_index(temp_db_path, capsys):
|
||||||
# Create vector index
|
# Create vector index
|
||||||
chunks_tbl.create_index(metric="cosine", index_type="IVF_PQ")
|
chunks_tbl.create_index(metric="cosine", index_type="IVF_PQ")
|
||||||
|
|
||||||
# Capture versions before
|
|
||||||
before_versions = {
|
|
||||||
"settings": int(settings_tbl.version),
|
|
||||||
"documents": int(docs_tbl.version),
|
|
||||||
"chunks": int(chunks_tbl.version),
|
|
||||||
}
|
|
||||||
|
|
||||||
app = HaikuRAGApp(db_path=temp_db_path)
|
app = HaikuRAGApp(db_path=temp_db_path)
|
||||||
await app.info()
|
await app.info()
|
||||||
|
|
||||||
|
|
@ -168,9 +149,3 @@ async def test_app_info_with_vector_index(temp_db_path, capsys):
|
||||||
# Check basic info still present
|
# Check basic info still present
|
||||||
assert "documents: 1" in out
|
assert "documents: 1" in out
|
||||||
assert "chunks: 512" in out
|
assert "chunks: 512" in out
|
||||||
|
|
||||||
# Verify no versions changed (read-only)
|
|
||||||
db2 = lancedb.connect(temp_db_path)
|
|
||||||
assert int(db2.open_table("settings").version) == before_versions["settings"]
|
|
||||||
assert int(db2.open_table("documents").version) == before_versions["documents"]
|
|
||||||
assert int(db2.open_table("chunks").version) == before_versions["chunks"]
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from haiku.rag.store.engine import Store
|
||||||
async def test_lancedb_cloud_skips_optimization(temp_db_path):
|
async def test_lancedb_cloud_skips_optimization(temp_db_path):
|
||||||
"""Test that vacuum is skipped when using LanceDB Cloud (db:// URI)."""
|
"""Test that vacuum is skipped when using LanceDB Cloud (db:// URI)."""
|
||||||
# Create a store
|
# Create a store
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
|
|
||||||
# Mock all cloud config to simulate LanceDB Cloud usage
|
# Mock all cloud config to simulate LanceDB Cloud usage
|
||||||
with (
|
with (
|
||||||
|
|
@ -33,7 +33,7 @@ async def test_lancedb_cloud_skips_optimization(temp_db_path):
|
||||||
async def test_local_storage_calls_optimization(temp_db_path):
|
async def test_local_storage_calls_optimization(temp_db_path):
|
||||||
"""Test that vacuum calls optimization for local storage."""
|
"""Test that vacuum calls optimization for local storage."""
|
||||||
# Create a store
|
# Create a store
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
|
|
||||||
# Ensure uri is empty (local storage)
|
# Ensure uri is empty (local storage)
|
||||||
with patch.object(Config.lancedb, "uri", ""):
|
with patch.object(Config.lancedb, "uri", ""):
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ def add_marker(text: str) -> str:
|
||||||
try:
|
try:
|
||||||
Config.processing.markdown_preprocessor = f"{pre_file}:add_marker"
|
Config.processing.markdown_preprocessor = f"{pre_file}:add_marker"
|
||||||
|
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
chunk_repo = ChunkRepository(store)
|
chunk_repo = ChunkRepository(store)
|
||||||
doc_repo = DocumentRepository(store)
|
doc_repo = DocumentRepository(store)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ VLLM_QA_AVAILABLE = bool(Config.providers.vllm.qa_base_url)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_qa_ollama(qa_corpus: Dataset, temp_db_path):
|
async def test_qa_ollama(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test Ollama QA with LLM judge."""
|
"""Test Ollama QA with LLM judge."""
|
||||||
client = HaikuRAG(temp_db_path)
|
client = HaikuRAG(temp_db_path, create=True)
|
||||||
qa = QuestionAnswerAgent(
|
qa = QuestionAnswerAgent(
|
||||||
client, ModelConfig(provider="ollama", name="gpt-oss", enable_thinking=False)
|
client, ModelConfig(provider="ollama", name="gpt-oss", enable_thinking=False)
|
||||||
)
|
)
|
||||||
|
|
@ -43,7 +43,7 @@ async def test_qa_ollama(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.skipif(not OPENAI_AVAILABLE, reason="OpenAI not available")
|
@pytest.mark.skipif(not OPENAI_AVAILABLE, reason="OpenAI not available")
|
||||||
async def test_qa_openai(qa_corpus: Dataset, temp_db_path):
|
async def test_qa_openai(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test OpenAI QA with LLM judge."""
|
"""Test OpenAI QA with LLM judge."""
|
||||||
client = HaikuRAG(temp_db_path)
|
client = HaikuRAG(temp_db_path, create=True)
|
||||||
qa = QuestionAnswerAgent(client, ModelConfig(provider="openai", name="gpt-4o-mini"))
|
qa = QuestionAnswerAgent(client, ModelConfig(provider="openai", name="gpt-4o-mini"))
|
||||||
llm_judge = LLMJudge()
|
llm_judge = LLMJudge()
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ async def test_qa_openai(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.skipif(not ANTHROPIC_AVAILABLE, reason="Anthropic not available")
|
@pytest.mark.skipif(not ANTHROPIC_AVAILABLE, reason="Anthropic not available")
|
||||||
async def test_qa_anthropic(qa_corpus: Dataset, temp_db_path):
|
async def test_qa_anthropic(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test Anthropic QA with LLM judge."""
|
"""Test Anthropic QA with LLM judge."""
|
||||||
client = HaikuRAG(temp_db_path)
|
client = HaikuRAG(temp_db_path, create=True)
|
||||||
qa = QuestionAnswerAgent(
|
qa = QuestionAnswerAgent(
|
||||||
client, ModelConfig(provider="anthropic", name="claude-3-5-haiku-20241022")
|
client, ModelConfig(provider="anthropic", name="claude-3-5-haiku-20241022")
|
||||||
)
|
)
|
||||||
|
|
@ -93,7 +93,7 @@ async def test_qa_anthropic(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.skipif(not VLLM_QA_AVAILABLE, reason="vLLM QA server not configured")
|
@pytest.mark.skipif(not VLLM_QA_AVAILABLE, reason="vLLM QA server not configured")
|
||||||
async def test_qa_vllm(qa_corpus: Dataset, temp_db_path):
|
async def test_qa_vllm(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test vLLM QA with LLM judge."""
|
"""Test vLLM QA with LLM judge."""
|
||||||
client = HaikuRAG(temp_db_path)
|
client = HaikuRAG(temp_db_path, create=True)
|
||||||
qa = QuestionAnswerAgent(client, ModelConfig(provider="vllm", name="Qwen/Qwen3-4B"))
|
qa = QuestionAnswerAgent(client, ModelConfig(provider="vllm", name="Qwen/Qwen3-4B"))
|
||||||
llm_judge = LLMJudge()
|
llm_judge = LLMJudge()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from haiku.rag.client import HaikuRAG, RebuildMode
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_rebuild_full(qa_corpus: Dataset, temp_db_path):
|
async def test_rebuild_full(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test full rebuild: converts, chunks, and embeds all documents."""
|
"""Test full rebuild: converts, chunks, and embeds all documents."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
doc = await client.create_document(content=qa_corpus["document_extracted"][0])
|
doc = await client.create_document(content=qa_corpus["document_extracted"][0])
|
||||||
assert doc.id is not None
|
assert doc.id is not None
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ async def test_rebuild_full(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_rebuild_embed_only(qa_corpus: Dataset, temp_db_path):
|
async def test_rebuild_embed_only(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test embed-only rebuild: keeps chunks, only regenerates embeddings."""
|
"""Test embed-only rebuild: keeps chunks, only regenerates embeddings."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
doc = await client.create_document(content=qa_corpus["document_extracted"][0])
|
doc = await client.create_document(content=qa_corpus["document_extracted"][0])
|
||||||
assert doc.id is not None
|
assert doc.id is not None
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ async def test_rebuild_embed_only(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_rebuild_embed_only_skips_unchanged(qa_corpus: Dataset, temp_db_path):
|
async def test_rebuild_embed_only_skips_unchanged(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test embed-only rebuild skips chunks with unchanged embeddings."""
|
"""Test embed-only rebuild skips chunks with unchanged embeddings."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
doc = await client.create_document(content=qa_corpus["document_extracted"][0])
|
doc = await client.create_document(content=qa_corpus["document_extracted"][0])
|
||||||
assert doc.id is not None
|
assert doc.id is not None
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ async def test_rebuild_embed_only_skips_unchanged(qa_corpus: Dataset, temp_db_pa
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_rebuild_rechunk(qa_corpus: Dataset, temp_db_path):
|
async def test_rebuild_rechunk(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test rechunk rebuild: re-chunks from content without accessing source files."""
|
"""Test rechunk rebuild: re-chunks from content without accessing source files."""
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||||
doc = await client.create_document(content=qa_corpus["document_extracted"][0])
|
doc = await client.create_document(content=qa_corpus["document_extracted"][0])
|
||||||
assert doc.id is not None
|
assert doc.id is not None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from haiku.rag.config import Config
|
||||||
async def test_search_qa_corpus(qa_corpus: Dataset, temp_db_path):
|
async def test_search_qa_corpus(qa_corpus: Dataset, temp_db_path):
|
||||||
"""Test that documents can be found by searching with their associated questions."""
|
"""Test that documents can be found by searching with their associated questions."""
|
||||||
# Create client
|
# Create client
|
||||||
client = HaikuRAG(db_path=temp_db_path, config=Config)
|
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
|
||||||
|
|
||||||
# Load unique documents (limited to 10)
|
# Load unique documents (limited to 10)
|
||||||
seen_documents = set()
|
seen_documents = set()
|
||||||
|
|
@ -61,7 +61,7 @@ async def test_search_qa_corpus(qa_corpus: Dataset, temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_chunks_include_document_info(temp_db_path):
|
async def test_chunks_include_document_info(temp_db_path):
|
||||||
"""Test that search results include document URI and metadata."""
|
"""Test that search results include document URI and metadata."""
|
||||||
client = HaikuRAG(db_path=temp_db_path, config=Config)
|
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
|
||||||
|
|
||||||
# Create a document with URI and metadata
|
# Create a document with URI and metadata
|
||||||
created_document = await client.create_document(
|
created_document = await client.create_document(
|
||||||
|
|
@ -93,7 +93,7 @@ async def test_chunks_include_document_info(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_chunks_include_document_title(temp_db_path):
|
async def test_chunks_include_document_title(temp_db_path):
|
||||||
"""Test that search results include the parent document title when present."""
|
"""Test that search results include the parent document title when present."""
|
||||||
client = HaikuRAG(db_path=temp_db_path, config=Config)
|
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
|
||||||
|
|
||||||
# Create a document with URI and title
|
# Create a document with URI and title
|
||||||
await client.create_document(
|
await client.create_document(
|
||||||
|
|
@ -119,7 +119,7 @@ async def test_chunks_include_document_title(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_search_score_types(temp_db_path):
|
async def test_search_score_types(temp_db_path):
|
||||||
"""Test that different search types return appropriate score ranges."""
|
"""Test that different search types return appropriate score ranges."""
|
||||||
client = HaikuRAG(db_path=temp_db_path, config=Config)
|
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
|
||||||
|
|
||||||
# Create multiple documents with different content
|
# Create multiple documents with different content
|
||||||
documents_content = [
|
documents_content = [
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ def test_settings_table_populated_on_store_init(temp_db_path):
|
||||||
from haiku.rag.store.engine import Store
|
from haiku.rag.store.engine import Store
|
||||||
from haiku.rag.store.repositories.settings import SettingsRepository
|
from haiku.rag.store.repositories.settings import SettingsRepository
|
||||||
|
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
settings_repo = SettingsRepository(store)
|
settings_repo = SettingsRepository(store)
|
||||||
|
|
||||||
db_settings = settings_repo.get_current_settings()
|
db_settings = settings_repo.get_current_settings()
|
||||||
|
|
@ -32,7 +32,7 @@ def test_settings_save_and_retrieve(temp_db_path):
|
||||||
from haiku.rag.store.engine import Store
|
from haiku.rag.store.engine import Store
|
||||||
from haiku.rag.store.repositories.settings import SettingsRepository
|
from haiku.rag.store.repositories.settings import SettingsRepository
|
||||||
|
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
settings_repo = SettingsRepository(store)
|
settings_repo = SettingsRepository(store)
|
||||||
|
|
||||||
original_chunk_size = Config.processing.chunk_size
|
original_chunk_size = Config.processing.chunk_size
|
||||||
|
|
@ -53,7 +53,7 @@ async def test_config_validation_on_db_load(temp_db_path):
|
||||||
from haiku.rag.store.repositories.settings import SettingsRepository
|
from haiku.rag.store.repositories.settings import SettingsRepository
|
||||||
|
|
||||||
# Create store and save settings
|
# Create store and save settings
|
||||||
store1 = Store(temp_db_path)
|
store1 = Store(temp_db_path, create=True)
|
||||||
store1.close()
|
store1.close()
|
||||||
|
|
||||||
# Change config
|
# Change config
|
||||||
|
|
@ -63,18 +63,20 @@ async def test_config_validation_on_db_load(temp_db_path):
|
||||||
try:
|
try:
|
||||||
# Loading the database should raise ConfigMismatchError
|
# Loading the database should raise ConfigMismatchError
|
||||||
with pytest.raises(ConfigMismatchError) as exc_info:
|
with pytest.raises(ConfigMismatchError) as exc_info:
|
||||||
Store(temp_db_path)
|
Store(temp_db_path, create=True)
|
||||||
|
|
||||||
assert "chunk_size" in str(exc_info.value)
|
assert "chunk_size" in str(exc_info.value)
|
||||||
assert "rebuild" in str(exc_info.value).lower()
|
assert "rebuild" in str(exc_info.value).lower()
|
||||||
|
|
||||||
# Rebuild
|
# Rebuild
|
||||||
async with HaikuRAG(db_path=temp_db_path, skip_validation=True) as client:
|
async with HaikuRAG(
|
||||||
|
db_path=temp_db_path, skip_validation=True, create=True
|
||||||
|
) as client:
|
||||||
async for _ in client.rebuild_database():
|
async for _ in client.rebuild_database():
|
||||||
pass # Process all documents
|
pass # Process all documents
|
||||||
|
|
||||||
# Verify we can now load the database without exception (settings were updated)
|
# Verify we can now load the database without exception (settings were updated)
|
||||||
store2 = Store(temp_db_path)
|
store2 = Store(temp_db_path, create=True)
|
||||||
settings_repo2 = SettingsRepository(store2)
|
settings_repo2 = SettingsRepository(store2)
|
||||||
db_settings = settings_repo2.get_current_settings()
|
db_settings = settings_repo2.get_current_settings()
|
||||||
assert db_settings["processing"]["chunk_size"] == 999
|
assert db_settings["processing"]["chunk_size"] == 999
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from haiku.rag.store.repositories.document import DocumentRepository
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_version_rollback_on_create_failure(temp_db_path):
|
async def test_version_rollback_on_create_failure(temp_db_path):
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
repo = DocumentRepository(store)
|
repo = DocumentRepository(store)
|
||||||
|
|
||||||
# Ensure chunk repository is instantiated and stub embeddings to avoid network
|
# Ensure chunk repository is instantiated and stub embeddings to avoid network
|
||||||
|
|
@ -51,7 +51,7 @@ async def test_version_rollback_on_create_failure(temp_db_path):
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_version_rollback_on_update_failure(temp_db_path):
|
async def test_version_rollback_on_update_failure(temp_db_path):
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
repo = DocumentRepository(store)
|
repo = DocumentRepository(store)
|
||||||
|
|
||||||
# Stub embeddings to avoid network
|
# Stub embeddings to avoid network
|
||||||
|
|
@ -106,11 +106,11 @@ def test_new_database_does_not_run_upgrades(monkeypatch, temp_db_path):
|
||||||
fail_if_called,
|
fail_if_called,
|
||||||
)
|
)
|
||||||
|
|
||||||
Store(temp_db_path)
|
Store(temp_db_path, create=True)
|
||||||
|
|
||||||
|
|
||||||
def test_existing_database_runs_upgrades(monkeypatch, temp_db_path):
|
def test_existing_database_runs_upgrades(monkeypatch, temp_db_path):
|
||||||
Store(temp_db_path)
|
Store(temp_db_path, create=True)
|
||||||
|
|
||||||
called = {"value": False}
|
called = {"value": False}
|
||||||
|
|
||||||
|
|
@ -122,6 +122,7 @@ def test_existing_database_runs_upgrades(monkeypatch, temp_db_path):
|
||||||
mark_called,
|
mark_called,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Opening an existing database should trigger upgrades
|
||||||
Store(temp_db_path)
|
Store(temp_db_path)
|
||||||
|
|
||||||
assert called["value"]
|
assert called["value"]
|
||||||
|
|
@ -129,7 +130,7 @@ def test_existing_database_runs_upgrades(monkeypatch, temp_db_path):
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_vacuum_with_retention_threshold(temp_db_path):
|
async def test_vacuum_with_retention_threshold(temp_db_path):
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
repo = DocumentRepository(store)
|
repo = DocumentRepository(store)
|
||||||
|
|
||||||
# Stub embeddings to avoid network
|
# Stub embeddings to avoid network
|
||||||
|
|
@ -209,14 +210,14 @@ async def test_vacuum_completes_before_context_exit(temp_db_path, monkeypatch):
|
||||||
# Set aggressive vacuum retention for this test
|
# Set aggressive vacuum retention for this test
|
||||||
monkeypatch.setattr(Config.storage, "vacuum_retention_seconds", 0)
|
monkeypatch.setattr(Config.storage, "vacuum_retention_seconds", 0)
|
||||||
|
|
||||||
async with HaikuRAG(db_path=temp_db_path) as client:
|
async with HaikuRAG(db_path=temp_db_path, create=True) as client:
|
||||||
# Create multiple documents - each creation triggers automatic vacuum with retention=0
|
# Create multiple documents - each creation triggers automatic vacuum with retention=0
|
||||||
# This aggressively cleans up old versions between operations
|
# This aggressively cleans up old versions between operations
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
await client.create_document(content=f"Test document {i}")
|
await client.create_document(content=f"Test document {i}")
|
||||||
|
|
||||||
# After context exit, automatic vacuum should have kept versions minimal
|
# After context exit, automatic vacuum should have kept versions minimal
|
||||||
store = Store(temp_db_path)
|
store = Store(temp_db_path, create=True)
|
||||||
final_versions = len(list(store.documents_table.list_versions()))
|
final_versions = len(list(store.documents_table.list_versions()))
|
||||||
|
|
||||||
# With retention_seconds=0, vacuum aggressively cleans up between operations
|
# With retention_seconds=0, vacuum aggressively cleans up between operations
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue