diff --git a/docs/_static/llama-stack-spec.html b/docs/_static/llama-stack-spec.html
index affc426d6..801e8dc33 100644
--- a/docs/_static/llama-stack-spec.html
+++ b/docs/_static/llama-stack-spec.html
@@ -11190,6 +11190,115 @@
],
"title": "InsertRequest"
},
+ "Chunk": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "$ref": "#/components/schemas/InterleavedContent",
+ "description": "The content of the chunk, which can be interleaved text, images, or other types."
+ },
+ "metadata": {
+ "type": "object",
+ "additionalProperties": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "array"
+ },
+ {
+ "type": "object"
+ }
+ ]
+ },
+ "description": "Metadata associated with the chunk that will be used in the model context during inference."
+ },
+ "embedding": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ },
+ "description": "Optional embedding for the chunk. If not provided, it will be computed later."
+ },
+ "stored_chunk_id": {
+ "type": "string",
+ "description": "The chunk ID that is stored in the vector database. Used for backend functionality."
+ },
+ "chunk_metadata": {
+ "$ref": "#/components/schemas/ChunkMetadata",
+ "description": "Metadata for the chunk that will NOT be used in the context during inference. The `chunk_metadata` is required backend functionality."
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "content",
+ "metadata"
+ ],
+ "title": "Chunk",
+ "description": "A chunk of content that can be inserted into a vector database."
+ },
+ "ChunkMetadata": {
+ "type": "object",
+ "properties": {
+ "chunk_id": {
+ "type": "string",
+ "description": "The ID of the chunk. If not set, it will be generated based on the document ID and content."
+ },
+ "document_id": {
+ "type": "string",
+ "description": "The ID of the document this chunk belongs to."
+ },
+ "source": {
+ "type": "string",
+ "description": "The source of the content, such as a URL, file path, or other identifier."
+ },
+ "created_timestamp": {
+ "type": "integer",
+ "description": "An optional timestamp indicating when the chunk was created."
+ },
+ "updated_timestamp": {
+ "type": "integer",
+ "description": "An optional timestamp indicating when the chunk was last updated."
+ },
+ "chunk_window": {
+ "type": "string",
+ "description": "The window of the chunk, which can be used to group related chunks together."
+ },
+ "chunk_tokenizer": {
+ "type": "string",
+ "description": "The tokenizer used to create the chunk. Default is Tiktoken."
+ },
+ "chunk_embedding_model": {
+ "type": "string",
+ "description": "The embedding model used to create the chunk's embedding."
+ },
+ "chunk_embedding_dimension": {
+ "type": "integer",
+ "description": "The dimension of the embedding vector for the chunk."
+ },
+ "content_token_count": {
+ "type": "integer",
+ "description": "The number of tokens in the content of the chunk."
+ },
+ "metadata_token_count": {
+ "type": "integer",
+ "description": "The number of tokens in the metadata of the chunk."
+ }
+ },
+ "additionalProperties": false,
+ "title": "ChunkMetadata",
+ "description": "`ChunkMetadata` is backend metadata for a `Chunk` that is used to store additional information about the chunk that will not be used in the context during inference, but is required for backend functionality. The `ChunkMetadata` is set during chunk creation in `MemoryToolRuntimeImpl().insert()`and is not expected to change after. Use `Chunk.metadata` for metadata that will be used in the context during inference."
+ },
"InsertChunksRequest": {
"type": "object",
"properties": {
@@ -11200,53 +11309,7 @@
"chunks": {
"type": "array",
"items": {
- "type": "object",
- "properties": {
- "content": {
- "$ref": "#/components/schemas/InterleavedContent",
- "description": "The content of the chunk, which can be interleaved text, images, or other types."
- },
- "metadata": {
- "type": "object",
- "additionalProperties": {
- "oneOf": [
- {
- "type": "null"
- },
- {
- "type": "boolean"
- },
- {
- "type": "number"
- },
- {
- "type": "string"
- },
- {
- "type": "array"
- },
- {
- "type": "object"
- }
- ]
- },
- "description": "Metadata associated with the chunk, such as document ID, source, or other relevant information."
- },
- "embedding": {
- "type": "array",
- "items": {
- "type": "number"
- },
- "description": "Optional embedding for the chunk. If not provided, it will be computed later."
- }
- },
- "additionalProperties": false,
- "required": [
- "content",
- "metadata"
- ],
- "title": "Chunk",
- "description": "A chunk of content that can be inserted into a vector database."
+ "$ref": "#/components/schemas/Chunk"
},
"description": "The chunks to insert. Each `Chunk` should contain content which can be interleaved text, images, or other types. `metadata`: `dict[str, Any]` and `embedding`: `List[float]` are optional. If `metadata` is provided, you configure how Llama Stack formats the chunk during generation. If `embedding` is not provided, it will be computed later."
},
@@ -14671,53 +14734,7 @@
"chunks": {
"type": "array",
"items": {
- "type": "object",
- "properties": {
- "content": {
- "$ref": "#/components/schemas/InterleavedContent",
- "description": "The content of the chunk, which can be interleaved text, images, or other types."
- },
- "metadata": {
- "type": "object",
- "additionalProperties": {
- "oneOf": [
- {
- "type": "null"
- },
- {
- "type": "boolean"
- },
- {
- "type": "number"
- },
- {
- "type": "string"
- },
- {
- "type": "array"
- },
- {
- "type": "object"
- }
- ]
- },
- "description": "Metadata associated with the chunk, such as document ID, source, or other relevant information."
- },
- "embedding": {
- "type": "array",
- "items": {
- "type": "number"
- },
- "description": "Optional embedding for the chunk. If not provided, it will be computed later."
- }
- },
- "additionalProperties": false,
- "required": [
- "content",
- "metadata"
- ],
- "title": "Chunk",
- "description": "A chunk of content that can be inserted into a vector database."
+ "$ref": "#/components/schemas/Chunk"
}
},
"scores": {
diff --git a/docs/_static/llama-stack-spec.yaml b/docs/_static/llama-stack-spec.yaml
index 1e1293dc2..b736cd904 100644
--- a/docs/_static/llama-stack-spec.yaml
+++ b/docs/_static/llama-stack-spec.yaml
@@ -7867,6 +7867,107 @@ components:
- vector_db_id
- chunk_size_in_tokens
title: InsertRequest
+ Chunk:
+ type: object
+ properties:
+ content:
+ $ref: '#/components/schemas/InterleavedContent'
+ description: >-
+ The content of the chunk, which can be interleaved text, images, or other
+ types.
+ metadata:
+ type: object
+ additionalProperties:
+ oneOf:
+ - type: 'null'
+ - type: boolean
+ - type: number
+ - type: string
+ - type: array
+ - type: object
+ description: >-
+ Metadata associated with the chunk that will be used in the model context
+ during inference.
+ embedding:
+ type: array
+ items:
+ type: number
+ description: >-
+ Optional embedding for the chunk. If not provided, it will be computed
+ later.
+ stored_chunk_id:
+ type: string
+ description: >-
+ The chunk ID that is stored in the vector database. Used for backend functionality.
+ chunk_metadata:
+ $ref: '#/components/schemas/ChunkMetadata'
+ description: >-
+ Metadata for the chunk that will NOT be used in the context during inference.
+ The `chunk_metadata` is required backend functionality.
+ additionalProperties: false
+ required:
+ - content
+ - metadata
+ title: Chunk
+ description: >-
+ A chunk of content that can be inserted into a vector database.
+ ChunkMetadata:
+ type: object
+ properties:
+ chunk_id:
+ type: string
+ description: >-
+ The ID of the chunk. If not set, it will be generated based on the document
+ ID and content.
+ document_id:
+ type: string
+ description: >-
+ The ID of the document this chunk belongs to.
+ source:
+ type: string
+ description: >-
+ The source of the content, such as a URL, file path, or other identifier.
+ created_timestamp:
+ type: integer
+ description: >-
+ An optional timestamp indicating when the chunk was created.
+ updated_timestamp:
+ type: integer
+ description: >-
+ An optional timestamp indicating when the chunk was last updated.
+ chunk_window:
+ type: string
+ description: >-
+ The window of the chunk, which can be used to group related chunks together.
+ chunk_tokenizer:
+ type: string
+ description: >-
+ The tokenizer used to create the chunk. Default is Tiktoken.
+ chunk_embedding_model:
+ type: string
+ description: >-
+ The embedding model used to create the chunk's embedding.
+ chunk_embedding_dimension:
+ type: integer
+ description: >-
+ The dimension of the embedding vector for the chunk.
+ content_token_count:
+ type: integer
+ description: >-
+ The number of tokens in the content of the chunk.
+ metadata_token_count:
+ type: integer
+ description: >-
+ The number of tokens in the metadata of the chunk.
+ additionalProperties: false
+ title: ChunkMetadata
+ description: >-
+ `ChunkMetadata` is backend metadata for a `Chunk` that is used to store additional
+ information about the chunk that will not be used in the context during
+ inference, but is required for backend functionality. The `ChunkMetadata` is
+ set during chunk creation in `MemoryToolRuntimeImpl().insert()`and is not
+ expected to change after. Use `Chunk.metadata` for metadata that will
+ be used in the context during inference.
InsertChunksRequest:
type: object
properties:
@@ -7877,40 +7978,7 @@ components:
chunks:
type: array
items:
- type: object
- properties:
- content:
- $ref: '#/components/schemas/InterleavedContent'
- description: >-
- The content of the chunk, which can be interleaved text, images,
- or other types.
- metadata:
- type: object
- additionalProperties:
- oneOf:
- - type: 'null'
- - type: boolean
- - type: number
- - type: string
- - type: array
- - type: object
- description: >-
- Metadata associated with the chunk, such as document ID, source,
- or other relevant information.
- embedding:
- type: array
- items:
- type: number
- description: >-
- Optional embedding for the chunk. If not provided, it will be computed
- later.
- additionalProperties: false
- required:
- - content
- - metadata
- title: Chunk
- description: >-
- A chunk of content that can be inserted into a vector database.
+ $ref: '#/components/schemas/Chunk'
description: >-
The chunks to insert. Each `Chunk` should contain content which can be
interleaved text, images, or other types. `metadata`: `dict[str, Any]`
@@ -10231,40 +10299,7 @@ components:
chunks:
type: array
items:
- type: object
- properties:
- content:
- $ref: '#/components/schemas/InterleavedContent'
- description: >-
- The content of the chunk, which can be interleaved text, images,
- or other types.
- metadata:
- type: object
- additionalProperties:
- oneOf:
- - type: 'null'
- - type: boolean
- - type: number
- - type: string
- - type: array
- - type: object
- description: >-
- Metadata associated with the chunk, such as document ID, source,
- or other relevant information.
- embedding:
- type: array
- items:
- type: number
- description: >-
- Optional embedding for the chunk. If not provided, it will be computed
- later.
- additionalProperties: false
- required:
- - content
- - metadata
- title: Chunk
- description: >-
- A chunk of content that can be inserted into a vector database.
+ $ref: '#/components/schemas/Chunk'
scores:
type: array
items:
diff --git a/llama_stack/apis/vector_io/vector_io.py b/llama_stack/apis/vector_io/vector_io.py
index d6de0108c..2d4131315 100644
--- a/llama_stack/apis/vector_io/vector_io.py
+++ b/llama_stack/apis/vector_io/vector_io.py
@@ -8,6 +8,7 @@
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
+import uuid
from typing import Annotated, Any, Literal, Protocol, runtime_checkable
from pydantic import BaseModel, Field
@@ -15,21 +16,80 @@ from pydantic import BaseModel, Field
from llama_stack.apis.inference import InterleavedContent
from llama_stack.apis.vector_dbs import VectorDB
from llama_stack.providers.utils.telemetry.trace_protocol import trace_protocol
+from llama_stack.providers.utils.vector_io.chunk_utils import generate_chunk_id
from llama_stack.schema_utils import json_schema_type, webmethod
from llama_stack.strong_typing.schema import register_schema
+@json_schema_type
+class ChunkMetadata(BaseModel):
+ """
+ `ChunkMetadata` is backend metadata for a `Chunk` that is used to store additional information about the chunk that
+ will not be used in the context during inference, but is required for backend functionality. The `ChunkMetadata`
+ is set during chunk creation in `MemoryToolRuntimeImpl().insert()`and is not expected to change after.
+ Use `Chunk.metadata` for metadata that will be used in the context during inference.
+ :param chunk_id: The ID of the chunk. If not set, it will be generated based on the document ID and content.
+ :param document_id: The ID of the document this chunk belongs to.
+ :param source: The source of the content, such as a URL, file path, or other identifier.
+ :param created_timestamp: An optional timestamp indicating when the chunk was created.
+ :param updated_timestamp: An optional timestamp indicating when the chunk was last updated.
+ :param chunk_window: The window of the chunk, which can be used to group related chunks together.
+ :param chunk_tokenizer: The tokenizer used to create the chunk. Default is Tiktoken.
+ :param chunk_embedding_model: The embedding model used to create the chunk's embedding.
+ :param chunk_embedding_dimension: The dimension of the embedding vector for the chunk.
+ :param content_token_count: The number of tokens in the content of the chunk.
+ :param metadata_token_count: The number of tokens in the metadata of the chunk.
+ """
+
+ chunk_id: str | None = None
+ document_id: str | None = None
+ source: str | None = None
+ created_timestamp: int | None = None
+ updated_timestamp: int | None = None
+ chunk_window: str | None = None
+ chunk_tokenizer: str | None = None
+ chunk_embedding_model: str | None = None
+ chunk_embedding_dimension: int | None = None
+ content_token_count: int | None = None
+ metadata_token_count: int | None = None
+
+
+@json_schema_type
class Chunk(BaseModel):
"""
A chunk of content that can be inserted into a vector database.
:param content: The content of the chunk, which can be interleaved text, images, or other types.
:param embedding: Optional embedding for the chunk. If not provided, it will be computed later.
- :param metadata: Metadata associated with the chunk, such as document ID, source, or other relevant information.
+ :param metadata: Metadata associated with the chunk that will be used in the model context during inference.
+ :param stored_chunk_id: The chunk ID that is stored in the vector database. Used for backend functionality.
+ :param chunk_metadata: Metadata for the chunk that will NOT be used in the context during inference.
+ The `chunk_metadata` is required backend functionality.
"""
content: InterleavedContent
metadata: dict[str, Any] = Field(default_factory=dict)
embedding: list[float] | None = None
+ # The alias parameter serializes the field as "chunk_id" in JSON but keeps the internal name as "stored_chunk_id"
+ stored_chunk_id: str | None = Field(default=None, alias="chunk_id")
+ chunk_metadata: ChunkMetadata | None = None
+
+ model_config = {"populate_by_name": True}
+
+ def model_post_init(self, __context):
+ # Extract chunk_id from metadata if present
+ if self.metadata and "chunk_id" in self.metadata:
+ self.stored_chunk_id = self.metadata.pop("chunk_id")
+
+ @property
+ def chunk_id(self) -> str:
+ """Returns the chunk ID, which is either an input `chunk_id` or a generated one if not set."""
+ if self.stored_chunk_id:
+ return self.stored_chunk_id
+
+ if "document_id" in self.metadata:
+ return generate_chunk_id(self.metadata["document_id"], str(self.content))
+
+ return generate_chunk_id(str(uuid.uuid4()), str(self.content))
@json_schema_type
diff --git a/llama_stack/providers/inline/tool_runtime/rag/memory.py b/llama_stack/providers/inline/tool_runtime/rag/memory.py
index 7f4fe5dbd..6a7c7885c 100644
--- a/llama_stack/providers/inline/tool_runtime/rag/memory.py
+++ b/llama_stack/providers/inline/tool_runtime/rag/memory.py
@@ -81,6 +81,7 @@ class MemoryToolRuntimeImpl(ToolGroupsProtocolPrivate, ToolRuntime, RAGToolRunti
chunks = []
for doc in documents:
content = await content_from_doc(doc)
+ # TODO: we should add enrichment here as URLs won't be added to the metadata by default
chunks.extend(
make_overlapped_chunks(
doc.document_id,
@@ -157,8 +158,24 @@ class MemoryToolRuntimeImpl(ToolGroupsProtocolPrivate, ToolRuntime, RAGToolRunti
)
break
- metadata_subset = {k: v for k, v in metadata.items() if k not in ["token_count", "metadata_token_count"]}
- text_content = query_config.chunk_template.format(index=i + 1, chunk=chunk, metadata=metadata_subset)
+ # Add useful keys from chunk_metadata to metadata and remove some from metadata
+ chunk_metadata_keys_to_include_from_context = [
+ "chunk_id",
+ "document_id",
+ "source",
+ ]
+ metadata_keys_to_exclude_from_context = [
+ "token_count",
+ "metadata_token_count",
+ ]
+ metadata_for_context = {}
+ for k in chunk_metadata_keys_to_include_from_context:
+ metadata_for_context[k] = getattr(chunk.chunk_metadata, k)
+ for k in metadata:
+ if k not in metadata_keys_to_exclude_from_context:
+ metadata_for_context[k] = metadata[k]
+
+ text_content = query_config.chunk_template.format(index=i + 1, chunk=chunk, metadata=metadata_for_context)
picked.append(TextContentItem(text=text_content))
picked.append(TextContentItem(text="END of knowledge_search tool results.\n"))
diff --git a/llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py b/llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py
index d832e56f5..3b3c5f486 100644
--- a/llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py
+++ b/llama_stack/providers/inline/vector_io/sqlite_vec/sqlite_vec.py
@@ -5,12 +5,10 @@
# the root directory of this source tree.
import asyncio
-import hashlib
import json
import logging
import sqlite3
import struct
-import uuid
from typing import Any
import numpy as np
@@ -201,10 +199,7 @@ class SQLiteVecIndex(EmbeddingIndex):
batch_embeddings = embeddings[i : i + batch_size]
# Insert metadata
- metadata_data = [
- (generate_chunk_id(chunk.metadata["document_id"], chunk.content), chunk.model_dump_json())
- for chunk in batch_chunks
- ]
+ metadata_data = [(chunk.chunk_id, chunk.model_dump_json()) for chunk in batch_chunks]
cur.executemany(
f"""
INSERT INTO {self.metadata_table} (id, chunk)
@@ -218,7 +213,7 @@ class SQLiteVecIndex(EmbeddingIndex):
embedding_data = [
(
(
- generate_chunk_id(chunk.metadata["document_id"], chunk.content),
+ chunk.chunk_id,
serialize_vector(emb.tolist()),
)
)
@@ -230,10 +225,7 @@ class SQLiteVecIndex(EmbeddingIndex):
)
# Insert FTS content
- fts_data = [
- (generate_chunk_id(chunk.metadata["document_id"], chunk.content), chunk.content)
- for chunk in batch_chunks
- ]
+ fts_data = [(chunk.chunk_id, chunk.content) for chunk in batch_chunks]
# DELETE existing entries with same IDs (FTS5 doesn't support ON CONFLICT)
cur.executemany(
f"DELETE FROM {self.fts_table} WHERE id = ?;",
@@ -381,13 +373,12 @@ class SQLiteVecIndex(EmbeddingIndex):
vector_response = await self.query_vector(embedding, k, score_threshold)
keyword_response = await self.query_keyword(query_string, k, score_threshold)
- # Convert responses to score dictionaries using generate_chunk_id
+ # Convert responses to score dictionaries using chunk_id
vector_scores = {
- generate_chunk_id(chunk.metadata["document_id"], str(chunk.content)): score
- for chunk, score in zip(vector_response.chunks, vector_response.scores, strict=False)
+ chunk.chunk_id: score for chunk, score in zip(vector_response.chunks, vector_response.scores, strict=False)
}
keyword_scores = {
- generate_chunk_id(chunk.metadata["document_id"], str(chunk.content)): score
+ chunk.chunk_id: score
for chunk, score in zip(keyword_response.chunks, keyword_response.scores, strict=False)
}
@@ -408,13 +399,7 @@ class SQLiteVecIndex(EmbeddingIndex):
filtered_items = [(doc_id, score) for doc_id, score in top_k_items if score >= score_threshold]
# Create a map of chunk_id to chunk for both responses
- chunk_map = {}
- for c in vector_response.chunks:
- chunk_id = generate_chunk_id(c.metadata["document_id"], str(c.content))
- chunk_map[chunk_id] = c
- for c in keyword_response.chunks:
- chunk_id = generate_chunk_id(c.metadata["document_id"], str(c.content))
- chunk_map[chunk_id] = c
+ chunk_map = {c.chunk_id: c for c in vector_response.chunks + keyword_response.chunks}
# Use the map to look up chunks by their IDs
chunks = []
@@ -757,9 +742,3 @@ class SQLiteVecVectorIOAdapter(OpenAIVectorStoreMixin, VectorIO, VectorDBsProtoc
if vector_db_id not in self.cache:
raise ValueError(f"Vector DB {vector_db_id} not found")
return await self.cache[vector_db_id].query_chunks(query, params)
-
-
-def generate_chunk_id(document_id: str, chunk_text: str) -> str:
- """Generate a unique chunk ID using a hash of document ID and chunk text."""
- hash_input = f"{document_id}:{chunk_text}".encode()
- return str(uuid.UUID(hashlib.md5(hash_input).hexdigest()))
diff --git a/llama_stack/providers/remote/vector_io/qdrant/qdrant.py b/llama_stack/providers/remote/vector_io/qdrant/qdrant.py
index e9d6eec22..09ea08fa0 100644
--- a/llama_stack/providers/remote/vector_io/qdrant/qdrant.py
+++ b/llama_stack/providers/remote/vector_io/qdrant/qdrant.py
@@ -70,8 +70,8 @@ class QdrantIndex(EmbeddingIndex):
)
points = []
- for i, (chunk, embedding) in enumerate(zip(chunks, embeddings, strict=False)):
- chunk_id = f"{chunk.metadata['document_id']}:chunk-{i}"
+ for _i, (chunk, embedding) in enumerate(zip(chunks, embeddings, strict=False)):
+ chunk_id = chunk.chunk_id
points.append(
PointStruct(
id=convert_id(chunk_id),
diff --git a/llama_stack/providers/utils/memory/vector_store.py b/llama_stack/providers/utils/memory/vector_store.py
index a6e420feb..ab204a75a 100644
--- a/llama_stack/providers/utils/memory/vector_store.py
+++ b/llama_stack/providers/utils/memory/vector_store.py
@@ -7,6 +7,7 @@ import base64
import io
import logging
import re
+import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
@@ -23,12 +24,13 @@ from llama_stack.apis.common.content_types import (
)
from llama_stack.apis.tools import RAGDocument
from llama_stack.apis.vector_dbs import VectorDB
-from llama_stack.apis.vector_io import Chunk, QueryChunksResponse
+from llama_stack.apis.vector_io import Chunk, ChunkMetadata, QueryChunksResponse
from llama_stack.models.llama.llama3.tokenizer import Tokenizer
from llama_stack.providers.datatypes import Api
from llama_stack.providers.utils.inference.prompt_adapter import (
interleaved_content_as_str,
)
+from llama_stack.providers.utils.vector_io.chunk_utils import generate_chunk_id
log = logging.getLogger(__name__)
@@ -148,6 +150,7 @@ async def content_from_doc(doc: RAGDocument) -> str:
def make_overlapped_chunks(
document_id: str, text: str, window_len: int, overlap_len: int, metadata: dict[str, Any]
) -> list[Chunk]:
+ default_tokenizer = "DEFAULT_TIKTOKEN_TOKENIZER"
tokenizer = Tokenizer.get_instance()
tokens = tokenizer.encode(text, bos=False, eos=False)
try:
@@ -161,16 +164,32 @@ def make_overlapped_chunks(
for i in range(0, len(tokens), window_len - overlap_len):
toks = tokens[i : i + window_len]
chunk = tokenizer.decode(toks)
+ chunk_id = generate_chunk_id(chunk, text)
chunk_metadata = metadata.copy()
+ chunk_metadata["chunk_id"] = chunk_id
chunk_metadata["document_id"] = document_id
chunk_metadata["token_count"] = len(toks)
chunk_metadata["metadata_token_count"] = len(metadata_tokens)
+ backend_chunk_metadata = ChunkMetadata(
+ chunk_id=chunk_id,
+ document_id=document_id,
+ source=metadata.get("source", None),
+ created_timestamp=metadata.get("created_timestamp", int(time.time())),
+ updated_timestamp=int(time.time()),
+ chunk_window=f"{i}-{i + len(toks)}",
+ chunk_tokenizer=default_tokenizer,
+ chunk_embedding_model=None, # This will be set in `VectorDBWithIndex.insert_chunks`
+ content_token_count=len(toks),
+ metadata_token_count=len(metadata_tokens),
+ )
+
# chunk is a string
chunks.append(
Chunk(
content=chunk,
metadata=chunk_metadata,
+ chunk_metadata=backend_chunk_metadata,
)
)
@@ -237,6 +256,9 @@ class VectorDBWithIndex:
for i, c in enumerate(chunks):
if c.embedding is None:
chunks_to_embed.append(c)
+ if c.chunk_metadata:
+ c.chunk_metadata.chunk_embedding_model = self.vector_db.embedding_model
+ c.chunk_metadata.chunk_embedding_dimension = self.vector_db.embedding_dimension
else:
_validate_embedding(c.embedding, i, self.vector_db.embedding_dimension)
diff --git a/llama_stack/providers/utils/vector_io/__init__.py b/llama_stack/providers/utils/vector_io/__init__.py
new file mode 100644
index 000000000..756f351d8
--- /dev/null
+++ b/llama_stack/providers/utils/vector_io/__init__.py
@@ -0,0 +1,5 @@
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+# All rights reserved.
+#
+# This source code is licensed under the terms described in the LICENSE file in
+# the root directory of this source tree.
diff --git a/llama_stack/providers/utils/vector_io/chunk_utils.py b/llama_stack/providers/utils/vector_io/chunk_utils.py
new file mode 100644
index 000000000..68cf11cad
--- /dev/null
+++ b/llama_stack/providers/utils/vector_io/chunk_utils.py
@@ -0,0 +1,14 @@
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+# All rights reserved.
+#
+# This source code is licensed under the terms described in the LICENSE file in
+# the root directory of this source tree.
+
+import hashlib
+import uuid
+
+
+def generate_chunk_id(document_id: str, chunk_text: str) -> str:
+ """Generate a unique chunk ID using a hash of document ID and chunk text."""
+ hash_input = f"{document_id}:{chunk_text}".encode()
+ return str(uuid.UUID(hashlib.md5(hash_input).hexdigest()))
diff --git a/tests/unit/providers/vector_io/conftest.py b/tests/unit/providers/vector_io/conftest.py
index 3bcd0613f..5eaca8a25 100644
--- a/tests/unit/providers/vector_io/conftest.py
+++ b/tests/unit/providers/vector_io/conftest.py
@@ -9,7 +9,7 @@ import random
import numpy as np
import pytest
-from llama_stack.apis.vector_io import Chunk
+from llama_stack.apis.vector_io import Chunk, ChunkMetadata
EMBEDDING_DIMENSION = 384
@@ -33,6 +33,20 @@ def sample_chunks():
for j in range(k)
for i in range(n)
]
+ sample.extend(
+ [
+ Chunk(
+ content=f"Sentence {i} from document {j + k}",
+ chunk_metadata=ChunkMetadata(
+ document_id=f"document-{j + k}",
+ chunk_id=f"document-{j}-chunk-{i}",
+ source=f"example source-{j + k}-{i}",
+ ),
+ )
+ for j in range(k)
+ for i in range(n)
+ ]
+ )
return sample
diff --git a/tests/unit/providers/vector_io/test_chunk_utils.py b/tests/unit/providers/vector_io/test_chunk_utils.py
new file mode 100644
index 000000000..941928b6d
--- /dev/null
+++ b/tests/unit/providers/vector_io/test_chunk_utils.py
@@ -0,0 +1,66 @@
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+# All rights reserved.
+#
+# This source code is licensed under the terms described in the LICENSE file in
+# the root directory of this source tree.
+
+from llama_stack.apis.vector_io import Chunk, ChunkMetadata
+from llama_stack.providers.utils.vector_io.chunk_utils import generate_chunk_id
+
+# This test is a unit test for the chunk_utils.py helpers. This should only contain
+# tests which are specific to this file. More general (API-level) tests should be placed in
+# tests/integration/vector_io/
+#
+# How to run this test:
+#
+# pytest tests/unit/providers/vector_io/test_chunk_utils.py \
+# -v -s --tb=short --disable-warnings --asyncio-mode=auto
+
+
+def test_generate_chunk_id():
+ chunks = [
+ Chunk(content="test", metadata={"document_id": "doc-1"}),
+ Chunk(content="test ", metadata={"document_id": "doc-1"}),
+ Chunk(content="test 3", metadata={"document_id": "doc-1"}),
+ ]
+
+ chunk_ids = sorted([chunk.chunk_id for chunk in chunks])
+ assert chunk_ids == [
+ "177a1368-f6a8-0c50-6e92-18677f2c3de3",
+ "bc744db3-1b25-0a9c-cdff-b6ba3df73c36",
+ "f68df25d-d9aa-ab4d-5684-64a233add20d",
+ ]
+
+
+def test_chunk_id():
+ # Test with existing chunk ID
+ chunk_with_id = Chunk(content="test", metadata={"document_id": "existing-id"})
+ assert chunk_with_id.chunk_id == "84ededcc-b80b-a83e-1a20-ca6515a11350"
+
+ # Test with document ID in metadata
+ chunk_with_doc_id = Chunk(content="test", metadata={"document_id": "doc-1"})
+ assert chunk_with_doc_id.chunk_id == generate_chunk_id("doc-1", "test")
+
+ # Test chunks with ChunkMetadata
+ chunk_with_metadata = Chunk(
+ content="test",
+ metadata={"document_id": "existing-id", "chunk_id": "chunk-id-1"},
+ chunk_metadata=ChunkMetadata(document_id="document_1"),
+ )
+ assert chunk_with_metadata.chunk_id == "chunk-id-1"
+
+ # Test with no ID or document ID
+ chunk_without_id = Chunk(content="test")
+ generated_id = chunk_without_id.chunk_id
+ assert isinstance(generated_id, str) and len(generated_id) == 36 # Should be a valid UUID
+
+
+def test_stored_chunk_id_alias():
+ # Test with existing chunk ID alias
+ chunk_with_alias = Chunk(content="test", metadata={"document_id": "existing-id", "chunk_id": "chunk-id-1"})
+ assert chunk_with_alias.chunk_id == "chunk-id-1"
+ serialized_chunk = chunk_with_alias.model_dump()
+ assert serialized_chunk["stored_chunk_id"] == "chunk-id-1"
+ # showing chunk_id is not serialized (i.e., a computed field)
+ assert "chunk_id" not in serialized_chunk
+ assert chunk_with_alias.stored_chunk_id == "chunk-id-1"
diff --git a/tests/unit/providers/vector_io/test_qdrant.py b/tests/unit/providers/vector_io/test_qdrant.py
index 607eccb24..6902c8850 100644
--- a/tests/unit/providers/vector_io/test_qdrant.py
+++ b/tests/unit/providers/vector_io/test_qdrant.py
@@ -81,7 +81,7 @@ __QUERY = "Sample query"
@pytest.mark.asyncio
-@pytest.mark.parametrize("max_query_chunks, expected_chunks", [(2, 2), (100, 30)])
+@pytest.mark.parametrize("max_query_chunks, expected_chunks", [(2, 2), (100, 60)])
async def test_qdrant_adapter_returns_expected_chunks(
qdrant_adapter: QdrantVectorIOAdapter,
vector_db_id,
diff --git a/tests/unit/providers/vector_io/test_sqlite_vec.py b/tests/unit/providers/vector_io/test_sqlite_vec.py
index 6424b9e86..bbac717c7 100644
--- a/tests/unit/providers/vector_io/test_sqlite_vec.py
+++ b/tests/unit/providers/vector_io/test_sqlite_vec.py
@@ -15,7 +15,6 @@ from llama_stack.providers.inline.vector_io.sqlite_vec.sqlite_vec import (
SQLiteVecIndex,
SQLiteVecVectorIOAdapter,
_create_sqlite_connection,
- generate_chunk_id,
)
# This test is a unit test for the SQLiteVecVectorIOAdapter class. This should only contain
@@ -65,6 +64,14 @@ async def test_query_chunks_vector(sqlite_vec_index, sample_chunks, sample_embed
assert len(response.chunks) == 2
+@pytest.mark.xfail(reason="Chunk Metadata not yet supported for SQLite-vec", strict=True)
+async def test_query_chunk_metadata(sqlite_vec_index, sample_chunks, sample_embeddings):
+ await sqlite_vec_index.add_chunks(sample_chunks, sample_embeddings)
+ query_embedding = sample_embeddings[0]
+ response = await sqlite_vec_index.query_vector(query_embedding, k=2, score_threshold=0.0)
+ assert response.chunks[-1].chunk_metadata == sample_chunks[-1].chunk_metadata
+
+
@pytest.mark.asyncio
async def test_query_chunks_full_text_search(sqlite_vec_index, sample_chunks, sample_embeddings):
await sqlite_vec_index.add_chunks(sample_chunks, sample_embeddings)
@@ -150,21 +157,6 @@ async def sqlite_vec_adapter(sqlite_connection):
await adapter.shutdown()
-def test_generate_chunk_id():
- chunks = [
- Chunk(content="test", metadata={"document_id": "doc-1"}),
- Chunk(content="test ", metadata={"document_id": "doc-1"}),
- Chunk(content="test 3", metadata={"document_id": "doc-1"}),
- ]
-
- chunk_ids = sorted([generate_chunk_id(chunk.metadata["document_id"], chunk.content) for chunk in chunks])
- assert chunk_ids == [
- "177a1368-f6a8-0c50-6e92-18677f2c3de3",
- "bc744db3-1b25-0a9c-cdff-b6ba3df73c36",
- "f68df25d-d9aa-ab4d-5684-64a233add20d",
- ]
-
-
@pytest.mark.asyncio
async def test_query_chunks_hybrid_no_keyword_matches(sqlite_vec_index, sample_chunks, sample_embeddings):
"""Test hybrid search when keyword search returns no matches - should still return vector results."""
@@ -339,7 +331,7 @@ async def test_query_chunks_hybrid_mixed_results(sqlite_vec_index, sample_chunks
# Verify scores are in descending order
assert all(response.scores[i] >= response.scores[i + 1] for i in range(len(response.scores) - 1))
# Verify we get results from both the vector-similar document and keyword-matched document
- doc_ids = {chunk.metadata["document_id"] for chunk in response.chunks}
+ doc_ids = {chunk.metadata.get("document_id") or chunk.chunk_metadata.document_id for chunk in response.chunks}
assert "document-0" in doc_ids # From vector search
assert "document-2" in doc_ids # From keyword search
@@ -364,7 +356,11 @@ async def test_query_chunks_hybrid_weighted_reranker_parametrization(
reranker_params={"alpha": 1.0},
)
assert len(response.chunks) > 0 # Should get at least one result
- assert any("document-0" in chunk.metadata["document_id"] for chunk in response.chunks)
+ assert any(
+ "document-0"
+ in (chunk.metadata.get("document_id") or (chunk.chunk_metadata.document_id if chunk.chunk_metadata else ""))
+ for chunk in response.chunks
+ )
# alpha=0.0 (should behave like pure vector)
response = await sqlite_vec_index.query_hybrid(
@@ -389,7 +385,11 @@ async def test_query_chunks_hybrid_weighted_reranker_parametrization(
reranker_params={"alpha": 0.7},
)
assert len(response.chunks) > 0 # Should get at least one result
- assert any("document-0" in chunk.metadata["document_id"] for chunk in response.chunks)
+ assert any(
+ "document-0"
+ in (chunk.metadata.get("document_id") or (chunk.chunk_metadata.document_id if chunk.chunk_metadata else ""))
+ for chunk in response.chunks
+ )
@pytest.mark.asyncio
diff --git a/tests/unit/rag/test_rag_query.py b/tests/unit/rag/test_rag_query.py
index b9fd8cca4..d2dd1783b 100644
--- a/tests/unit/rag/test_rag_query.py
+++ b/tests/unit/rag/test_rag_query.py
@@ -4,10 +4,15 @@
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
-from unittest.mock import MagicMock
+from unittest.mock import AsyncMock, MagicMock
import pytest
+from llama_stack.apis.vector_io import (
+ Chunk,
+ ChunkMetadata,
+ QueryChunksResponse,
+)
from llama_stack.providers.inline.tool_runtime.rag.memory import MemoryToolRuntimeImpl
@@ -17,3 +22,41 @@ class TestRagQuery:
rag_tool = MemoryToolRuntimeImpl(config=MagicMock(), vector_io_api=MagicMock(), inference_api=MagicMock())
with pytest.raises(ValueError):
await rag_tool.query(content=MagicMock(), vector_db_ids=[])
+
+ @pytest.mark.asyncio
+ async def test_query_chunk_metadata_handling(self):
+ rag_tool = MemoryToolRuntimeImpl(config=MagicMock(), vector_io_api=MagicMock(), inference_api=MagicMock())
+ content = "test query content"
+ vector_db_ids = ["db1"]
+
+ chunk_metadata = ChunkMetadata(
+ document_id="doc1",
+ chunk_id="chunk1",
+ source="test_source",
+ metadata_token_count=5,
+ )
+ interleaved_content = MagicMock()
+ chunk = Chunk(
+ content=interleaved_content,
+ metadata={
+ "key1": "value1",
+ "token_count": 10,
+ "metadata_token_count": 5,
+ # Note this is inserted into `metadata` during MemoryToolRuntimeImpl().insert()
+ "document_id": "doc1",
+ },
+ stored_chunk_id="chunk1",
+ chunk_metadata=chunk_metadata,
+ )
+
+ query_response = QueryChunksResponse(chunks=[chunk], scores=[1.0])
+
+ rag_tool.vector_io_api.query_chunks = AsyncMock(return_value=query_response)
+ result = await rag_tool.query(content=content, vector_db_ids=vector_db_ids)
+
+ assert result is not None
+ expected_metadata_string = (
+ "Metadata: {'chunk_id': 'chunk1', 'document_id': 'doc1', 'source': 'test_source', 'key1': 'value1'}"
+ )
+ assert expected_metadata_string in result.content[1].text
+ assert result.content is not None