haiku.rag/tests/test_client.py
2025-10-14 13:27:20 +03:00

815 lines
32 KiB
Python

import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, patch
import httpx
import pytest
from datasets import Dataset
from haiku.rag.client import HaikuRAG
from haiku.rag.config import Config
from haiku.rag.store.models.chunk import Chunk
@pytest.mark.asyncio
async def test_client_document_crud(qa_corpus: Dataset, temp_db_path):
"""Test HaikuRAG CRUD operations for documents."""
async with HaikuRAG(temp_db_path) as client:
# Get test data
first_doc = qa_corpus[0]
document_text = first_doc["document_extracted"]
test_uri = "file:///path/to/test.txt"
test_metadata = {"source": "test", "topic": "testing"}
# Test create_document
created_doc = await client.create_document(
content=document_text, uri=test_uri, metadata=test_metadata
)
assert created_doc.id is not None
assert created_doc.content == document_text
assert created_doc.uri == test_uri
assert created_doc.metadata == test_metadata
# Test get_document_by_id
retrieved_doc = await client.get_document_by_id(created_doc.id)
assert retrieved_doc is not None
assert retrieved_doc.id == created_doc.id
assert retrieved_doc.content == document_text
assert retrieved_doc.uri == test_uri
# Test get_document_by_uri
retrieved_by_uri = await client.get_document_by_uri(test_uri)
assert retrieved_by_uri is not None
assert retrieved_by_uri.id == created_doc.id
assert retrieved_by_uri.content == document_text
# Test get_document_by_uri with non-existent URI
non_existent = await client.get_document_by_uri("file:///non/existent.txt")
assert non_existent is None
# Test update_document
retrieved_doc.content = "Updated content"
retrieved_doc.uri = "file:///updated/path.txt"
updated_doc = await client.update_document(retrieved_doc)
assert updated_doc.content == "Updated content"
assert updated_doc.uri == "file:///updated/path.txt"
# Test list_documents
all_docs = await client.list_documents()
assert len(all_docs) == 1
assert all_docs[0].id == created_doc.id
# Test list_documents with pagination
limited_docs = await client.list_documents(limit=10, offset=0)
assert len(limited_docs) == 1
# Test delete_document
deleted = await client.delete_document(created_doc.id)
assert deleted is True
# Verify document is gone
retrieved_doc = await client.get_document_by_id(created_doc.id)
assert retrieved_doc is None
# Test delete non-existent document
deleted_again = await client.delete_document(created_doc.id)
assert deleted_again is False
@pytest.mark.asyncio
async def test_client_create_document_from_source(temp_db_path):
"""Test creating a document from a file source."""
async with HaikuRAG(temp_db_path) as client:
with tempfile.TemporaryDirectory() as temp_dir:
test_content = "This is test content from a file."
temp_path = Path(temp_dir) / "test.txt"
temp_path.write_text(test_content)
# Test create_document_from_source with Path
doc = await client.create_document_from_source(source=temp_path)
assert doc.id is not None
assert doc.content == test_content
assert doc.uri == temp_path.as_uri()
assert "contentType" in doc.metadata
assert "md5" in doc.metadata
assert doc.metadata["contentType"] == "text/plain"
# Test create_document_from_source with string path
doc2 = await client.create_document_from_source(source=str(temp_path))
assert doc2.id is not None
assert doc2.content == test_content
assert doc2.uri == temp_path.as_uri()
assert "contentType" in doc2.metadata
assert "md5" in doc2.metadata
@pytest.mark.asyncio
async def test_client_create_document_from_source_with_title(temp_db_path):
"""Test creating a document from a file source with a title."""
async with HaikuRAG(temp_db_path) as client:
with tempfile.TemporaryDirectory() as temp_dir:
test_content = "This is test content from a file."
temp_path = Path(temp_dir) / "test_title.txt"
temp_path.write_text(test_content)
doc = await client.create_document_from_source(
source=temp_path, title="My Doc"
)
assert doc.id is not None
assert doc.title == "My Doc"
@pytest.mark.asyncio
async def test_client_update_title_noop_behavior(temp_db_path):
"""When content is unchanged, updating title should update document without re-chunking."""
async with HaikuRAG(temp_db_path) as client:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir) / "test_update_title.txt"
temp_path.write_text("Original content")
doc1 = await client.create_document_from_source(temp_path, title="Title A")
assert doc1.id is not None
# Re-add with same content but new title
doc2 = await client.create_document_from_source(temp_path, title="Title B")
assert doc2.id == doc1.id
# Fetch and verify title updated
got = await client.get_document_by_id(doc1.id)
assert got is not None
assert got.title == "Title B"
@pytest.mark.asyncio
async def test_client_create_document_from_source_unsupported(temp_db_path):
"""Test creating a document from an unsupported file type."""
async with HaikuRAG(temp_db_path) as client:
# Create a temporary file with unsupported extension
with tempfile.NamedTemporaryFile(
mode="w", suffix=".unsupported", delete=False
) as f:
f.write("content")
temp_path = Path(f.name)
# Should raise ValueError for unsupported extension
with pytest.raises(ValueError, match="Unsupported file extension"):
await client.create_document_from_source(temp_path)
@pytest.mark.asyncio
async def test_client_create_document_from_source_nonexistent(temp_db_path):
"""Test creating a document from a non-existent file."""
async with HaikuRAG(temp_db_path) as client:
non_existent_path = Path("/non/existent/file.txt")
# Should raise ValueError when file doesn't exist
with pytest.raises(ValueError, match="File does not exist"):
await client.create_document_from_source(non_existent_path)
@pytest.mark.asyncio
async def test_client_create_document_from_url(temp_db_path):
"""Test creating a document from a URL."""
async with HaikuRAG(temp_db_path) as client:
# Mock the HTTP response
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.headers = {"content-type": "text/html"}
mock_response.raise_for_status = AsyncMock()
with patch("httpx.AsyncClient.get", return_value=mock_response):
doc = await client.create_document_from_source(
source="https://example.com/test.html", metadata={"source_type": "web"}
)
assert doc.id is not None
assert "Test Page" in doc.content
assert "test content" in doc.content
assert doc.uri == "https://example.com/test.html"
assert doc.metadata["source_type"] == "web"
assert "contentType" in doc.metadata
assert "md5" in doc.metadata
assert doc.metadata["contentType"] == "text/html"
@pytest.mark.asyncio
async def test_client_create_document_from_url_with_different_content_types(
temp_db_path,
):
"""Test creating documents from URLs with different content types."""
async with HaikuRAG(temp_db_path) as client:
# Test JSON content
mock_json_response = AsyncMock()
mock_json_response.content = (
b'{"title": "Test JSON", "content": "This is JSON content"}'
)
mock_json_response.headers = {"content-type": "application/json"}
mock_json_response.raise_for_status = AsyncMock()
with patch("httpx.AsyncClient.get", return_value=mock_json_response):
doc = await client.create_document_from_source(
"https://api.example.com/data.json"
)
assert doc.id is not None
assert "Test JSON" in doc.content
assert doc.uri == "https://api.example.com/data.json"
assert "contentType" in doc.metadata
assert "md5" in doc.metadata
assert doc.metadata["contentType"] == "application/json"
# Test plain text content
mock_text_response = AsyncMock()
mock_text_response.content = b"This is plain text content from a URL."
mock_text_response.headers = {"content-type": "text/plain"}
mock_text_response.raise_for_status = AsyncMock()
with patch("httpx.AsyncClient.get", return_value=mock_text_response):
doc = await client.create_document_from_source(
"https://example.com/readme.txt"
)
assert doc.id is not None
assert doc.content == "This is plain text content from a URL."
assert doc.uri == "https://example.com/readme.txt"
assert "contentType" in doc.metadata
assert "md5" in doc.metadata
assert doc.metadata["contentType"] == "text/plain"
@pytest.mark.asyncio
async def test_client_create_document_from_url_unsupported_content(temp_db_path):
"""Test creating a document from URL with unsupported content type."""
async with HaikuRAG(temp_db_path) as client:
# Mock response with unsupported content type
mock_response = AsyncMock()
mock_response.content = b"binary content"
mock_response.headers = {"content-type": "application/octet-stream"}
mock_response.raise_for_status = AsyncMock()
with patch("httpx.AsyncClient.get", return_value=mock_response):
with pytest.raises(ValueError, match="Unsupported content type"):
await client.create_document_from_source(
"https://example.com/binary.bin"
)
@pytest.mark.asyncio
async def test_client_create_document_from_url_http_error(temp_db_path):
"""Test handling HTTP errors when creating document from URL."""
async with HaikuRAG(temp_db_path) as client:
with patch("httpx.AsyncClient.get") as mock_get:
mock_get.side_effect = httpx.HTTPStatusError(
"404 Not Found",
request=httpx.Request("GET", "https://example.com/notfound.html"),
response=httpx.Response(404),
)
with pytest.raises(httpx.HTTPStatusError):
await client.create_document_from_source(
"https://example.com/notfound.html"
)
@pytest.mark.asyncio
async def test_get_extension_from_content_type_or_url(temp_db_path):
"""Test the helper method for determining file extensions."""
async with HaikuRAG(temp_db_path) as client:
# Test content type mappings
assert (
client._get_extension_from_content_type_or_url("", "text/html") == ".html"
)
assert (
client._get_extension_from_content_type_or_url("", "application/pdf")
== ".pdf"
)
assert (
client._get_extension_from_content_type_or_url("", "text/plain") == ".txt"
)
# Test URL extension detection
assert (
client._get_extension_from_content_type_or_url(
"https://example.com/doc.pdf", ""
)
== ".pdf"
)
assert (
client._get_extension_from_content_type_or_url(
"https://example.com/data.json", ""
)
== ".json"
)
# Test default fallback
assert (
client._get_extension_from_content_type_or_url("https://example.com/", "")
== ".html"
)
# Test content type priority over URL extension
assert (
client._get_extension_from_content_type_or_url(
"https://example.com/file.txt", "application/pdf"
)
== ".pdf"
)
@pytest.mark.asyncio
async def test_client_metadata_content_type_and_md5(temp_db_path):
"""Test that contentType and md5 metadata are correctly set."""
import hashlib
async with HaikuRAG(temp_db_path) as client:
# Create a temporary file with known content
test_content = "Test content for MD5 calculation."
expected_md5 = hashlib.md5(test_content.encode()).hexdigest()
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir) / "test.txt"
temp_path.write_text(test_content)
doc = await client.create_document_from_source(temp_path)
assert doc.metadata["contentType"] == "text/plain"
assert doc.metadata["md5"] == expected_md5
mock_response = AsyncMock()
mock_response.content = test_content.encode()
mock_response.headers = {"content-type": "text/plain"}
mock_response.raise_for_status = AsyncMock()
with patch("httpx.AsyncClient.get", return_value=mock_response):
url_doc = await client.create_document_from_source(
"https://example.com/test.txt"
)
assert url_doc.metadata["contentType"] == "text/plain"
assert url_doc.metadata["md5"] == expected_md5
@pytest.mark.asyncio
async def test_client_create_update_no_op_behavior(temp_db_path):
"""Test create/update/no-op behavior based on MD5 changes."""
async with HaikuRAG(temp_db_path) as client:
# Create a temporary file
test_content = "Original content for testing."
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir) / "test.txt"
temp_path.write_text(test_content)
# First call - should create new document
doc1 = await client.create_document_from_source(temp_path)
assert doc1.id is not None
assert doc1.content == test_content
original_id = doc1.id
# Second call with same content - should return existing document (no-op)
doc2 = await client.create_document_from_source(temp_path)
assert doc2.id == original_id # Same document
assert doc2.content == test_content
# Modify file content
updated_content = "Updated content for testing."
temp_path.write_text(updated_content)
# Third call with changed content - should update existing document
doc3 = await client.create_document_from_source(temp_path)
assert doc3.id == original_id # Same document ID
assert doc3.content == updated_content # Updated content
# Verify the document was actually updated in database
retrieved_doc = await client.get_document_by_id(original_id)
assert retrieved_doc is not None
assert retrieved_doc.content == updated_content
@pytest.mark.asyncio
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."""
async with HaikuRAG(temp_db_path) as client:
url = "https://example.com/test.txt"
original_content = b"Original URL content"
updated_content = b"Updated URL content"
# Mock first response
mock_response1 = AsyncMock()
mock_response1.content = original_content
mock_response1.headers = {"content-type": "text/plain"}
mock_response1.raise_for_status = AsyncMock()
with patch("httpx.AsyncClient.get", return_value=mock_response1):
# First call - should create new document
doc1 = await client.create_document_from_source(url)
assert doc1.id is not None
original_id = doc1.id
# Second call with same content - should return existing document (no-op)
doc2 = await client.create_document_from_source(url)
assert doc2.id == original_id # Same document
mock_response2 = AsyncMock()
mock_response2.content = updated_content
mock_response2.headers = {"content-type": "text/plain"}
mock_response2.raise_for_status = AsyncMock()
with patch("httpx.AsyncClient.get", return_value=mock_response2):
# Third call with changed content - should update existing document
doc3 = await client.create_document_from_source(url)
assert doc3.id == original_id # Same document ID
assert doc3.content == updated_content.decode() # Updated content
@pytest.mark.asyncio
async def test_client_search(temp_db_path):
"""Test HaikuRAG search functionality."""
async with HaikuRAG(temp_db_path) as client:
# Add multiple documents to search from
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."
doc3_text = "Data science combines statistics, programming, and domain expertise to extract insights."
# Create documents
doc1 = await client.create_document(
content=doc1_text, uri="doc1.txt", metadata={"topic": "python"}
)
doc2 = await client.create_document(
content=doc2_text, uri="doc2.txt", metadata={"topic": "ml"}
)
await client.create_document(
content=doc3_text, uri="doc3.txt", metadata={"topic": "data_science"}
)
# Test search with keyword that should match doc1
results = await client.search("Python programming", limit=3)
assert len(results) > 0
assert all(len(result) == 2 for result in results)
# Verify first result is from the Python document (doc1)
first_chunk, _ = results[0]
assert first_chunk.document_id == doc1.id
# Test search with different query
ml_results = await client.search("machine learning data", limit=2)
assert len(ml_results) > 0
# Verify first result is from the machine learning document (doc2)
first_ml_chunk, _ = ml_results[0]
assert first_ml_chunk.document_id == doc2.id
# Test search with limit parameter
limited_results = await client.search("programming", limit=1)
assert len(limited_results) <= 1
@pytest.mark.asyncio
async def test_client_async_context_manager(temp_db_path):
"""Test HaikuRAG as async context manager."""
# Test that context manager works and auto-closes
async with HaikuRAG(temp_db_path) as client:
# Create a document to ensure the client works
doc = await client.create_document(
content="Test content for context manager",
uri="test://context",
metadata={"test": "context_manager"},
)
assert doc.id is not None
assert doc.content == "Test content for context manager"
# Test search works within context
results = await client.search("Test content", limit=1)
assert len(results) > 0
# Context manager should have automatically closed the connection
# We can't easily test that the connection is closed without accessing internals,
# but the test passing means the context manager methods work correctly
@pytest.mark.asyncio
async def test_client_create_document_with_custom_chunks(temp_db_path):
"""Test creating a document with pre-created chunks."""
async with HaikuRAG(temp_db_path) as client:
# Create some custom chunks with and without embeddings
chunks = [
Chunk(
content="This is the first chunk",
metadata={"custom": "metadata1"},
order=0,
),
Chunk(
content="This is the second chunk",
metadata={"custom": "metadata2"},
embedding=[0.1] * Config.EMBEDDINGS_VECTOR_DIM,
order=1,
), # With embedding
Chunk(
content="This is the third chunk",
metadata={"custom": "metadata3"},
order=2,
),
]
# Create document with custom chunks
document = await client.create_document(
content="Full document content", chunks=chunks
)
assert document.id is not None
assert document.content == "Full document content"
# Verify the chunks were created correctly
doc_chunks = await client.chunk_repository.get_by_document_id(document.id)
assert len(doc_chunks) == 3
# Check chunks have correct content, document_id, and order from list position
for i, chunk in enumerate(doc_chunks):
assert chunk.document_id == document.id
assert chunk.content == chunks[i].content
assert chunk.order == i # Order should be set from list position
assert (
chunk.metadata["custom"] == f"metadata{i + 1}"
) # Original metadata preserved
@pytest.mark.asyncio
async def test_client_ask_without_cite(monkeypatch, temp_db_path):
"""Test asking questions without citations."""
from pydantic_ai.models.test import TestModel
# Mock OpenAIChatModel to return TestModel
monkeypatch.setattr(
"haiku.rag.qa.agent.OpenAIChatModel", lambda **kwargs: TestModel()
)
async with HaikuRAG(temp_db_path) as client:
# Create a test document for the agent to search
await client.create_document(
content="Python is a high-level programming language.", uri="test.txt"
)
# Use real QA agent with TestModel
answer = await client.ask("What is Python?")
# TestModel will generate a valid string response
assert answer is not None
assert isinstance(answer, str)
@pytest.mark.asyncio
async def test_client_ask_with_cite(monkeypatch, temp_db_path):
"""Test asking questions with citations."""
from pydantic_ai.models.test import TestModel
# Mock OpenAIChatModel to return TestModel
monkeypatch.setattr(
"haiku.rag.qa.agent.OpenAIChatModel", lambda **kwargs: TestModel()
)
async with HaikuRAG(temp_db_path) as client:
# Create a test document
await client.create_document(
content="Python is a high-level programming language.", uri="test.txt"
)
# Use real QA agent with TestModel
answer = await client.ask("What is Python?", cite=True)
# TestModel will generate a valid string response
assert answer is not None
assert isinstance(answer, str)
@pytest.mark.asyncio
async def test_client_expand_context(temp_db_path):
"""Test expanding search results with adjacent chunks."""
# Mock Config to have CONTEXT_CHUNK_RADIUS = 2
with patch("haiku.rag.client.Config.CONTEXT_CHUNK_RADIUS", 2):
async with HaikuRAG(temp_db_path) as client:
# Create chunks manually with precomputed embeddings to avoid network
dim = client.chunk_repository.embedder._vector_dim
z = [0.0] * dim
manual_chunks = [
Chunk(content="Chunk 0 content", order=0, embedding=z),
Chunk(content="Chunk 1 content", order=1, embedding=z),
Chunk(content="Chunk 2 content", order=2, embedding=z),
Chunk(content="Chunk 3 content", order=3, embedding=z),
Chunk(content="Chunk 4 content", order=4, embedding=z),
]
doc = await client.create_document(
content="Full document content",
uri="test_doc.txt",
title="test_doc_title",
chunks=manual_chunks,
)
# Get all chunks for the document
assert doc.id is not None
chunks = await client.chunk_repository.get_by_document_id(doc.id)
assert len(chunks) == 5
# Find the middle chunk (order=2)
middle_chunk = next(c for c in chunks if c.order == 2)
search_results = [(middle_chunk, 0.8)]
# Test expand_context with radius=2 and document title preserved
expanded_results = await client.expand_context(search_results, radius=2)
assert len(expanded_results) == 1
expanded_chunk, score = expanded_results[0]
# Check that the expanded chunk has combined content and preserves title/uri
assert expanded_chunk.id == middle_chunk.id
assert score == 0.8
assert "Chunk 2 content" in expanded_chunk.content
assert expanded_chunk.document_title == "test_doc_title"
assert expanded_chunk.document_uri == "test_doc.txt"
# Should include all chunks (radius=2 from chunk 2 = chunks 0,1,2,3,4)
assert "Chunk 0 content" in expanded_chunk.content
assert "Chunk 1 content" in expanded_chunk.content
assert "Chunk 2 content" in expanded_chunk.content
assert "Chunk 3 content" in expanded_chunk.content
assert "Chunk 4 content" in expanded_chunk.content
@pytest.mark.asyncio
async def test_client_expand_context_radius_zero(temp_db_path):
"""Test expand_context with radius 0 returns original results."""
async with HaikuRAG(temp_db_path) as client:
# Create a simple document
doc = await client.create_document(content="Simple test content")
assert doc.id is not None
chunks = await client.chunk_repository.get_by_document_id(doc.id)
search_results = [(chunks[0], 0.9)]
expanded_results = await client.expand_context(search_results, radius=0)
# Should return exactly the same results
assert expanded_results == search_results
@pytest.mark.asyncio
async def test_client_expand_context_multiple_chunks(temp_db_path):
"""Test expand_context with multiple search results."""
with patch("haiku.rag.client.Config.CONTEXT_CHUNK_RADIUS", 1):
async with HaikuRAG(temp_db_path) as client:
# Create first document with manual chunks
doc1_chunks = [
Chunk(content="Doc1 Part A", order=0),
Chunk(content="Doc1 Part B", order=1),
Chunk(content="Doc1 Part C", order=2),
]
doc1 = await client.create_document(
content="Doc1 content", uri="doc1.txt", chunks=doc1_chunks
)
# Create second document with manual chunks
doc2_chunks = [
Chunk(content="Doc2 Section X", order=0),
Chunk(content="Doc2 Section Y", order=1),
]
doc2 = await client.create_document(
content="Doc2 content", uri="doc2.txt", chunks=doc2_chunks
)
assert doc1.id is not None
assert doc2.id is not None
chunks1 = await client.chunk_repository.get_by_document_id(doc1.id)
chunks2 = await client.chunk_repository.get_by_document_id(doc2.id)
# Get middle chunk from doc1 (order=1) and first chunk from doc2 (order=0)
chunk1 = next(c for c in chunks1 if c.order == 1)
chunk2 = next(c for c in chunks2 if c.order == 0)
search_results = [(chunk1, 0.8), (chunk2, 0.7)]
expanded_results = await client.expand_context(search_results, radius=1)
assert len(expanded_results) == 2
# Check first expanded result (should include chunks 0,1,2 from doc1)
expanded1, score1 = expanded_results[0]
assert expanded1.id == chunk1.id
assert score1 == 0.8
assert "Doc1 Part A" in expanded1.content
assert "Doc1 Part B" in expanded1.content
assert "Doc1 Part C" in expanded1.content
# Check second expanded result (should include chunks 0,1 from doc2)
expanded2, score2 = expanded_results[1]
assert expanded2.id == chunk2.id
assert score2 == 0.7
assert "Doc2 Section X" in expanded2.content
assert "Doc2 Section Y" in expanded2.content
@pytest.mark.asyncio
async def test_client_expand_context_merges_overlapping_chunks(temp_db_path):
"""Test that overlapping expanded chunks are merged into one."""
async with HaikuRAG(temp_db_path) as client:
# Create document with 5 chunks
manual_chunks = [
Chunk(content="Chunk 0", order=0),
Chunk(content="Chunk 1", order=1),
Chunk(content="Chunk 2", order=2),
Chunk(content="Chunk 3", order=3),
Chunk(content="Chunk 4", order=4),
]
doc = await client.create_document(
content="Full document content", chunks=manual_chunks
)
assert doc.id is not None
chunks = await client.chunk_repository.get_by_document_id(doc.id)
# Get adjacent chunks (orders 1 and 2) - these will overlap when expanded
chunk1 = next(c for c in chunks if c.order == 1)
chunk2 = next(c for c in chunks if c.order == 2)
# With radius=1:
# chunk1 expanded would be [0,1,2]
# chunk2 expanded would be [1,2,3]
# These should merge into one chunk containing [0,1,2,3]
search_results = [(chunk1, 0.8), (chunk2, 0.7)]
expanded_results = await client.expand_context(search_results, radius=1)
# Should have only 1 merged result instead of 2 overlapping ones
assert len(expanded_results) == 1
merged_chunk, score = expanded_results[0]
# Should contain all chunks from 0 to 3
assert "Chunk 0" in merged_chunk.content
assert "Chunk 1" in merged_chunk.content
assert "Chunk 2" in merged_chunk.content
assert "Chunk 3" in merged_chunk.content
assert "Chunk 4" not in merged_chunk.content # Should not include chunk 4
# Should use the higher score (0.8)
assert score == 0.8
@pytest.mark.asyncio
async def test_client_expand_context_keeps_separate_non_overlapping(temp_db_path):
"""Test that non-overlapping expanded chunks remain separate."""
async with HaikuRAG(temp_db_path) as client:
# Create document with chunks far apart
manual_chunks = [
Chunk(content="Chunk 0", order=0),
Chunk(content="Chunk 1", order=1),
Chunk(content="Chunk 2", order=2),
Chunk(content="Chunk 5", order=5), # Gap here
Chunk(content="Chunk 6", order=6),
Chunk(content="Chunk 7", order=7),
]
doc = await client.create_document(
content="Full document content", chunks=manual_chunks
)
assert doc.id is not None
chunks = await client.chunk_repository.get_by_document_id(doc.id)
# Get chunks by index - they will have sequential orders 0,1,2,3,4,5
# So get chunk with order=0 and chunk with order=5 (far enough apart)
chunk0 = next(c for c in chunks if c.order == 0) # Content: "Chunk 0"
chunk5 = next(
c for c in chunks if c.order == 5
) # Content: "Chunk 7" but now at order 5
# chunk0 expanded: [0,1] with radius=1 (orders 0,1)
# chunk5 expanded: [4,5] with radius=1 (orders 4,5)
search_results = [(chunk0, 0.8), (chunk5, 0.7)]
expanded_results = await client.expand_context(search_results, radius=1)
# Should have 2 separate results
assert len(expanded_results) == 2
# Sort by score to ensure predictable order
expanded_results.sort(key=lambda x: x[1], reverse=True)
chunk0_expanded, score1 = expanded_results[0]
chunk5_expanded, score2 = expanded_results[1]
# First chunk (order=0) expanded should contain orders [0,1]
# Content should be "Chunk 0" + "Chunk 1"
assert "Chunk 0" in chunk0_expanded.content
assert "Chunk 1" in chunk0_expanded.content
assert (
"Chunk 5" not in chunk0_expanded.content
) # Should not have chunk 7 content
assert score1 == 0.8
# Second chunk (order=5) expanded should contain orders [4,5]
# Content should be "Chunk 6" (order 4) + "Chunk 7" (order 5)
assert "Chunk 6" in chunk5_expanded.content
assert "Chunk 7" in chunk5_expanded.content
assert "Chunk 0" not in chunk5_expanded.content
assert score2 == 0.7