Merge pull request #21 from ggozad/feat/external-chunks
create_document now optionally accepts pre-computed chunks
This commit is contained in:
commit
5214432f71
6 changed files with 106 additions and 11 deletions
|
|
@ -27,6 +27,31 @@ doc = await client.create_document(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
With custom externally generated chunks:
|
||||||
|
```python
|
||||||
|
from haiku.rag.store.models.chunk import Chunk
|
||||||
|
|
||||||
|
# Create custom chunks with optional embeddings
|
||||||
|
chunks = [
|
||||||
|
Chunk(
|
||||||
|
content="This is the first chunk",
|
||||||
|
metadata={"section": "intro"}
|
||||||
|
),
|
||||||
|
Chunk(
|
||||||
|
content="This is the second chunk",
|
||||||
|
metadata={"section": "body"},
|
||||||
|
embedding=[0.1] * 1024 # Optional pre-computed embedding
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
doc = await client.create_document(
|
||||||
|
content="Full document content",
|
||||||
|
uri="doc://custom",
|
||||||
|
metadata={"source": "manual"},
|
||||||
|
chunks=chunks # Use provided chunks instead of auto-generating
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
From file:
|
From file:
|
||||||
```python
|
```python
|
||||||
doc = await client.create_document_from_source("path/to/document.pdf")
|
doc = await client.create_document_from_source("path/to/document.pdf")
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,11 @@ class HaikuRAG:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def create_document(
|
async def create_document(
|
||||||
self, content: str, uri: str | None = None, metadata: dict | None = None
|
self,
|
||||||
|
content: str,
|
||||||
|
uri: str | None = None,
|
||||||
|
metadata: dict | None = None,
|
||||||
|
chunks: list[Chunk] | None = None,
|
||||||
) -> Document:
|
) -> Document:
|
||||||
"""Create a new document with optional URI and metadata.
|
"""Create a new document with optional URI and metadata.
|
||||||
|
|
||||||
|
|
@ -58,6 +62,7 @@ class HaikuRAG:
|
||||||
content: The text content of the document.
|
content: The text content of the document.
|
||||||
uri: Optional URI identifier for the document.
|
uri: Optional URI identifier for the document.
|
||||||
metadata: Optional metadata dictionary.
|
metadata: Optional metadata dictionary.
|
||||||
|
chunks: Optional list of pre-created chunks to use instead of generating new ones.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The created Document instance.
|
The created Document instance.
|
||||||
|
|
@ -67,7 +72,7 @@ class HaikuRAG:
|
||||||
uri=uri,
|
uri=uri,
|
||||||
metadata=metadata or {},
|
metadata=metadata or {},
|
||||||
)
|
)
|
||||||
return await self.document_repository.create(document)
|
return await self.document_repository.create(document, chunks)
|
||||||
|
|
||||||
async def create_document_from_source(
|
async def create_document_from_source(
|
||||||
self, source: str | Path, metadata: dict = {}
|
self, source: str | Path, metadata: dict = {}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ class Chunk(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int | None = None
|
id: int | None = None
|
||||||
document_id: int
|
document_id: int | None = None
|
||||||
content: str
|
content: str
|
||||||
metadata: dict = {}
|
metadata: dict = {}
|
||||||
document_uri: str | None = None
|
document_uri: str | None = None
|
||||||
document_meta: dict = {}
|
document_meta: dict = {}
|
||||||
|
embedding: list[float] | None = None
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ class ChunkRepository(BaseRepository[Chunk]):
|
||||||
"""Create a chunk in the database."""
|
"""Create a chunk in the database."""
|
||||||
if self.store._connection is None:
|
if self.store._connection is None:
|
||||||
raise ValueError("Store connection is not available")
|
raise ValueError("Store connection is not available")
|
||||||
|
if entity.document_id is None:
|
||||||
|
raise ValueError("Chunk must have a document_id to be created")
|
||||||
|
|
||||||
cursor = self.store._connection.cursor()
|
cursor = self.store._connection.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
|
|
@ -34,9 +36,15 @@ class ChunkRepository(BaseRepository[Chunk]):
|
||||||
|
|
||||||
entity.id = cursor.lastrowid
|
entity.id = cursor.lastrowid
|
||||||
|
|
||||||
# Generate and store embedding
|
# Generate and store embedding - use existing one if provided
|
||||||
embedding = await self.embedder.embed(entity.content)
|
if entity.embedding is not None:
|
||||||
serialized_embedding = self.store.serialize_embedding(embedding)
|
# Use the provided embedding
|
||||||
|
serialized_embedding = self.store.serialize_embedding(entity.embedding)
|
||||||
|
else:
|
||||||
|
# Generate embedding from content
|
||||||
|
embedding = await self.embedder.embed(entity.content)
|
||||||
|
serialized_embedding = self.store.serialize_embedding(embedding)
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO chunk_embeddings (chunk_id, embedding)
|
INSERT INTO chunk_embeddings (chunk_id, embedding)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import json
|
import json
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from haiku.rag.store.models.document import Document
|
from haiku.rag.store.models.document import Document
|
||||||
from haiku.rag.store.repositories.base import BaseRepository
|
from haiku.rag.store.repositories.base import BaseRepository
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from haiku.rag.store.models.chunk import Chunk
|
||||||
|
|
||||||
|
|
||||||
class DocumentRepository(BaseRepository[Document]):
|
class DocumentRepository(BaseRepository[Document]):
|
||||||
"""Repository for Document database operations."""
|
"""Repository for Document database operations."""
|
||||||
|
|
@ -16,7 +20,9 @@ class DocumentRepository(BaseRepository[Document]):
|
||||||
chunk_repository = ChunkRepository(store)
|
chunk_repository = ChunkRepository(store)
|
||||||
self.chunk_repository = chunk_repository
|
self.chunk_repository = chunk_repository
|
||||||
|
|
||||||
async def create(self, entity: Document) -> Document:
|
async def create(
|
||||||
|
self, entity: Document, chunks: list["Chunk"] | None = None
|
||||||
|
) -> Document:
|
||||||
"""Create a document with its chunks and embeddings."""
|
"""Create a document with its chunks and embeddings."""
|
||||||
if self.store._connection is None:
|
if self.store._connection is None:
|
||||||
raise ValueError("Store connection is not available")
|
raise ValueError("Store connection is not available")
|
||||||
|
|
@ -46,10 +52,20 @@ class DocumentRepository(BaseRepository[Document]):
|
||||||
assert document_id is not None, "Failed to create document in database"
|
assert document_id is not None, "Failed to create document in database"
|
||||||
entity.id = document_id
|
entity.id = document_id
|
||||||
|
|
||||||
# Create chunks and embeddings using ChunkRepository
|
# Create chunks - either use provided chunks or generate from content
|
||||||
await self.chunk_repository.create_chunks_for_document(
|
if chunks is not None:
|
||||||
document_id, entity.content, commit=False
|
# Use provided chunks, but update their document_id and set order from list position
|
||||||
)
|
for order, chunk in enumerate(chunks):
|
||||||
|
chunk.document_id = document_id
|
||||||
|
# Ensure order is set from list position
|
||||||
|
chunk.metadata = chunk.metadata.copy() if chunk.metadata else {}
|
||||||
|
chunk.metadata["order"] = order
|
||||||
|
await self.chunk_repository.create(chunk, commit=False)
|
||||||
|
else:
|
||||||
|
# Create chunks and embeddings using ChunkRepository
|
||||||
|
await self.chunk_repository.create_chunks_for_document(
|
||||||
|
document_id, entity.content, commit=False
|
||||||
|
)
|
||||||
|
|
||||||
cursor.execute("COMMIT")
|
cursor.execute("COMMIT")
|
||||||
return entity
|
return entity
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import pytest
|
||||||
from datasets import Dataset
|
from datasets import Dataset
|
||||||
|
|
||||||
from haiku.rag.client import HaikuRAG
|
from haiku.rag.client import HaikuRAG
|
||||||
|
from haiku.rag.store.models.chunk import Chunk
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -449,3 +450,42 @@ async def test_client_async_context_manager():
|
||||||
# Context manager should have automatically closed the connection
|
# Context manager should have automatically closed the connection
|
||||||
# We can't easily test that the connection is closed without accessing internals,
|
# We can't easily test that the connection is closed without accessing internals,
|
||||||
# but the test passing means the context manager methods work correctly
|
# but the test passing means the context manager methods work correctly
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_create_document_with_custom_chunks():
|
||||||
|
"""Test creating a document with pre-created chunks."""
|
||||||
|
async with HaikuRAG(":memory:") as client:
|
||||||
|
# Create some custom chunks with and without embeddings
|
||||||
|
chunks = [
|
||||||
|
Chunk(content="This is the first chunk", metadata={"custom": "metadata1"}),
|
||||||
|
Chunk(
|
||||||
|
content="This is the second chunk",
|
||||||
|
metadata={"custom": "metadata2"},
|
||||||
|
embedding=[0.1] * 1024,
|
||||||
|
), # With embedding
|
||||||
|
Chunk(content="This is the third chunk", metadata={"custom": "metadata3"}),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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.metadata["order"] == i
|
||||||
|
) # Order should be set from list position
|
||||||
|
assert (
|
||||||
|
chunk.metadata["custom"] == f"metadata{i + 1}"
|
||||||
|
) # Original metadata preserved
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue