Skip to content

Commit cf4b6a0

Browse files
authored
Merge pull request #705 from Kiln-AI/main
Update docs for v0.22.1
2 parents e9fb939 + 377a474 commit cf4b6a0

File tree

30 files changed

+2342
-125
lines changed

30 files changed

+2342
-125
lines changed

app/desktop/studio_server/tool_api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
ToolServerType,
1313
)
1414
from kiln_ai.datamodel.tool_id import (
15-
KILN_TASK_TOOL_ID_PREFIX,
1615
MCP_LOCAL_TOOL_ID_PREFIX,
1716
MCP_REMOTE_TOOL_ID_PREFIX,
18-
RAG_TOOL_ID_PREFIX,
1917
KilnBuiltInToolId,
2018
ToolId,
19+
build_kiln_task_tool_id,
20+
build_rag_tool_id,
2121
)
2222
from kiln_ai.tools.kiln_task_tool import KilnTaskTool
2323
from kiln_ai.tools.mcp_session_manager import MCPSessionManager
@@ -227,7 +227,7 @@ async def get_available_tools(
227227
if rag_configs:
228228
tools = [
229229
ToolApiDescription(
230-
id=f"{RAG_TOOL_ID_PREFIX}{rag_config.id}",
230+
id=build_rag_tool_id(rag_config.id),
231231
name=rag_config.tool_name,
232232
description=f"{rag_config.name}: {rag_config.tool_description}",
233233
)
@@ -259,7 +259,7 @@ async def get_available_tools(
259259
if not server.properties.get("is_archived", False):
260260
task_tools.append(
261261
ToolApiDescription(
262-
id=f"{KILN_TASK_TOOL_ID_PREFIX}{server.id}",
262+
id=build_kiln_task_tool_id(server.id),
263263
name=server.properties.get("name") or "",
264264
description=server.properties.get("description") or "",
265265
)

app/web_ui/src/routes/(app)/prompts/[project_id]/[task_id]/create/+page.svelte

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { createKilnError, KilnError } from "$lib/utils/error_handlers"
99
import { goto } from "$app/navigation"
1010
import posthog from "posthog-js"
11+
import { onMount } from "svelte"
1112
1213
$: project_id = $page.params.project_id
1314
$: task_id = $page.params.task_id
@@ -21,6 +22,12 @@
2122
"Think step by step, explaining your reasoning."
2223
let create_error: KilnError | null = null
2324
let create_loading = false
25+
let warn_before_unload = false
26+
let mounted = false
27+
28+
onMount(() => {
29+
mounted = true
30+
})
2431
2532
async function create_prompt() {
2633
try {
@@ -64,6 +71,20 @@
6471
create_loading = false
6572
}
6673
}
74+
75+
// Warn before unload if there's any user input
76+
$: prompt_name,
77+
prompt_description,
78+
prompt,
79+
is_chain_of_thought,
80+
chain_of_thought_instructions,
81+
user_input_detected()
82+
83+
function user_input_detected() {
84+
if (mounted) {
85+
warn_before_unload = true
86+
}
87+
}
6788
</script>
6889

6990
<div class="max-w-[1400px]">
@@ -83,6 +104,7 @@
83104
on:submit={create_prompt}
84105
bind:error={create_error}
85106
bind:submitting={create_loading}
107+
{warn_before_unload}
86108
>
87109
<FormElement
88110
label="Prompt Name"

conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,11 @@ def mock_attachment_factory(mock_file_factory):
207207

208208
def create_attachment(
209209
mime_type: MockFileFactoryMimeType,
210+
text: str | None = None,
210211
) -> KilnAttachmentModel:
212+
if text is not None:
213+
return KilnAttachmentModel.from_data(text, mime_type)
214+
211215
path = mock_file_factory(mime_type)
212216
return KilnAttachmentModel.from_file(path)
213217

libs/core/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ The library has a [comprehensive set of docs](https://kiln-ai.github.io/Kiln/kil
4343
- [Building and Running a Kiln Task from Code](#building-and-running-a-kiln-task-from-code)
4444
- [Tagging Task Runs Programmatically](#tagging-task-runs-programmatically)
4545
- [Adding Custom Model or AI Provider from Code](#adding-custom-model-or-ai-provider-from-code)
46+
- [Taking Kiln RAG to production](#taking-kiln-rag-to-production)
47+
- [Load a LlamaIndex Vector Store](#load-a-llamaindex-vector-store)
48+
- [Example: LanceDB Cloud](#example-lancedb-cloud)
49+
- [Deploy RAG without LlamaIndex](#deploy-rag-without-llamaindex)
4650
- [Full API Reference](#full-api-reference)
4751

4852
## Installation
@@ -310,6 +314,78 @@ custom_model_ids.append(new_model)
310314
Config.shared().custom_models = custom_model_ids
311315
```
312316

317+
## Taking Kiln RAG to production
318+
319+
When you're ready to deploy your RAG system, you can export your processed documents to any vector store supported by LlamaIndex. This allows you to use your Kiln-configured chunking and embedding settings in production.
320+
321+
### Load a LlamaIndex Vector Store
322+
323+
Kiln provides a `VectorStoreLoader` that yields your processed document chunks as LlamaIndex `TextNode` objects. These nodes contain the same metadata, chunking and embedding data as your Kiln Search Tool configuration.
324+
325+
```py
326+
from kiln_ai.datamodel import Project
327+
from kiln_ai.datamodel.rag import RagConfig
328+
from kiln_ai.adapters.vector_store_loaders import VectorStoreLoader
329+
330+
# Load your project and RAG configuration
331+
project = Project.load_from_file("path/to/your/project.kiln")
332+
rag_config = RagConfig.from_id_and_parent_path("rag-config-id", project.path)
333+
334+
# Create the loader
335+
loader = VectorStoreLoader(project=project, rag_config=rag_config)
336+
337+
# Export chunks to any LlamaIndex vector store
338+
async for batch in loader.iter_llama_index_nodes(batch_size=10):
339+
# Insert into your chosen vector store
340+
# Examples: LanceDB, Pinecone, Chroma, Qdrant, etc.
341+
pass
342+
```
343+
344+
**Supported Vector Stores:** LlamaIndex supports 20+ vector stores including LanceDB, Pinecone, Weaviate, Chroma, Qdrant, and more. See the [full list](https://developers.llamaindex.ai/python/framework/module_guides/storing/vector_stores/).
345+
346+
### Example: LanceDB Cloud
347+
348+
Internally Kiln uses LanceDB. By using LanceDB cloud you'll get the same indexing behaviour as in app.
349+
350+
Here's a complete example using LanceDB Cloud:
351+
352+
```py
353+
from kiln_ai.datamodel import Project
354+
from kiln_ai.datamodel.rag import RagConfig
355+
from kiln_ai.datamodel.vector_store import VectorStoreConfig
356+
from kiln_ai.adapters.vector_store_loaders import VectorStoreLoader
357+
from kiln_ai.adapters.vector_store.lancedb_adapter import lancedb_construct_from_config
358+
359+
# Load configurations
360+
project = Project.load_from_file("path/to/your/project.kiln")
361+
rag_config = RagConfig.from_id_and_parent_path("rag-config-id", project.path)
362+
vector_store_config = VectorStoreConfig.from_id_and_parent_path(
363+
rag_config.vector_store_config_id, project.path,
364+
)
365+
366+
# Create LanceDB vector store
367+
lancedb_store = lancedb_construct_from_config(
368+
vector_store_config=vector_store_config,
369+
uri="db://my-project",
370+
api_key="sk_...",
371+
region="us-east-1",
372+
table_name="my-documents", # Created automatically
373+
)
374+
375+
# Export and insert your documents
376+
loader = VectorStoreLoader(project=project, rag_config=rag_config)
377+
async for batch in loader.iter_llama_index_nodes(batch_size=100):
378+
await lancedb_store.async_add(batch)
379+
380+
print("Documents successfully exported to LanceDB!")
381+
```
382+
383+
After export, query your data using [LlamaIndex](https://developers.llamaindex.ai/python/framework-api-reference/storage/vector_store/lancedb/) or the [LanceDB client](https://lancedb.github.io/lancedb/).
384+
385+
### Deploy RAG without LlamaIndex
386+
387+
While Kiln is designed for deploying to LlamaIndex, you don't need to use it. The `iter_llama_index_nodes` returns a `TextNode` object which includes all the data you need to build a RAG index in any stack: embedding, text, document name, chunk ID, etc.
388+
313389
## Full API Reference
314390

315391
The library can do a lot more than the examples we've shown here.

libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
)
3232
from kiln_ai.adapters.model_adapters.litellm_config import LiteLlmConfig
3333
from kiln_ai.datamodel.json_schema import validate_schema_with_value_error
34-
from kiln_ai.tools.base_tool import KilnToolInterface, ToolCallContext
34+
from kiln_ai.tools.base_tool import (
35+
KilnToolInterface,
36+
ToolCallContext,
37+
ToolCallDefinition,
38+
)
3539
from kiln_ai.tools.kiln_task_tool import KilnTaskToolResult
3640
from kiln_ai.utils.exhaustive_error import raise_exhaustive_enum_error
3741
from kiln_ai.utils.litellm import get_litellm_provider_info
@@ -560,7 +564,7 @@ async def cached_available_tools(self) -> list[KilnToolInterface]:
560564
self._cached_available_tools = await self.available_tools()
561565
return self._cached_available_tools
562566

563-
async def litellm_tools(self) -> list[Dict]:
567+
async def litellm_tools(self) -> list[ToolCallDefinition]:
564568
available_tools = await self.cached_available_tools()
565569

566570
# LiteLLM takes the standard OpenAI-compatible tool call format

libs/core/kiln_ai/adapters/vector_store/lancedb_adapter.py

Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@
55
from typing import Any, Dict, List, Literal, Optional, Set, TypedDict
66

77
from llama_index.core import StorageContext, VectorStoreIndex
8-
from llama_index.core.schema import (
9-
BaseNode,
10-
NodeRelationship,
11-
RelatedNodeInfo,
12-
TextNode,
13-
)
8+
from llama_index.core.schema import BaseNode, TextNode
149
from llama_index.core.vector_stores.types import (
1510
VectorStoreQuery as LlamaIndexVectorStoreQuery,
1611
)
@@ -24,15 +19,19 @@
2419
SearchResult,
2520
VectorStoreQuery,
2621
)
22+
from kiln_ai.adapters.vector_store.lancedb_helpers import (
23+
convert_to_llama_index_node,
24+
deterministic_chunk_id,
25+
lancedb_construct_from_config,
26+
store_type_to_lancedb_query_type,
27+
)
2728
from kiln_ai.datamodel.rag import RagConfig
2829
from kiln_ai.datamodel.vector_store import (
2930
VectorStoreConfig,
30-
VectorStoreType,
3131
raise_exhaustive_enum_error,
3232
)
3333
from kiln_ai.utils.config import Config
3434
from kiln_ai.utils.env import temporary_env
35-
from kiln_ai.utils.uuid import string_to_uuid
3635

3736
logger = logging.getLogger(__name__)
3837

@@ -48,6 +47,7 @@ def __init__(
4847
self,
4948
rag_config: RagConfig,
5049
vector_store_config: VectorStoreConfig,
50+
lancedb_vector_store: LanceDBVectorStore | None = None,
5151
):
5252
super().__init__(rag_config, vector_store_config)
5353
self.config_properties = self.vector_store_config.lancedb_properties
@@ -56,17 +56,15 @@ def __init__(
5656
if vector_store_config.lancedb_properties.nprobes is not None:
5757
kwargs["nprobes"] = vector_store_config.lancedb_properties.nprobes
5858

59-
self.lancedb_vector_store = LanceDBVectorStore(
60-
mode="create",
61-
uri=LanceDBAdapter.lancedb_path_for_config(rag_config),
62-
query_type=self.query_type,
63-
overfetch_factor=vector_store_config.lancedb_properties.overfetch_factor,
64-
vector_column_name=vector_store_config.lancedb_properties.vector_column_name,
65-
text_key=vector_store_config.lancedb_properties.text_key,
66-
doc_id_key=vector_store_config.lancedb_properties.doc_id_key,
67-
**kwargs,
59+
# allow overriding the vector store with a custom one, useful for user loading into an arbitrary
60+
# deployment
61+
self.lancedb_vector_store = (
62+
lancedb_vector_store
63+
or lancedb_construct_from_config(
64+
vector_store_config,
65+
uri=LanceDBAdapter.lancedb_path_for_config(rag_config),
66+
)
6867
)
69-
7068
self._index = None
7169

7270
@property
@@ -149,7 +147,7 @@ async def add_chunks_with_embeddings(
149147

150148
chunk_count_for_document = len(chunks)
151149
deterministic_chunk_ids = [
152-
self.compute_deterministic_chunk_id(document_id, chunk_idx)
150+
deterministic_chunk_id(document_id, chunk_idx)
153151
for chunk_idx in range(chunk_count_for_document)
154152
]
155153

@@ -176,42 +174,12 @@ async def add_chunks_with_embeddings(
176174
zip(chunks_text, embeddings)
177175
):
178176
node_batch.append(
179-
TextNode(
180-
id_=deterministic_chunk_ids[chunk_idx],
177+
convert_to_llama_index_node(
178+
document_id=document_id,
179+
chunk_idx=chunk_idx,
180+
node_id=deterministic_chunk_id(document_id, chunk_idx),
181181
text=chunk_text,
182-
embedding=embedding.vector,
183-
metadata={
184-
# metadata is populated by some internal llama_index logic
185-
# that uses for example the source_node relationship
186-
"kiln_doc_id": document_id,
187-
"kiln_chunk_idx": chunk_idx,
188-
#
189-
# llama_index lancedb vector store automatically sets these metadata:
190-
# "doc_id": "UUID node_id of the Source Node relationship",
191-
# "document_id": "UUID node_id of the Source Node relationship",
192-
# "ref_doc_id": "UUID node_id of the Source Node relationship"
193-
#
194-
# llama_index file loaders set these metadata, which would be useful to also support:
195-
# "creation_date": "2025-09-03",
196-
# "file_name": "file.pdf",
197-
# "file_path": "/absolute/path/to/the/file.pdf",
198-
# "file_size": 395154,
199-
# "file_type": "application\/pdf",
200-
# "last_modified_date": "2025-09-03",
201-
# "page_label": "1",
202-
},
203-
relationships={
204-
# when using the llama_index loaders, llama_index groups Nodes under Documents
205-
# and relationships point to the Document (which is also a Node), which confusingly
206-
# enough does not map to an actual file (for a PDF, a Document is a page of the PDF)
207-
# the Document structure is not something that is persisted, so it is fine here
208-
# if we have a relationship to a node_id that does not exist in the db
209-
NodeRelationship.SOURCE: RelatedNodeInfo(
210-
node_id=document_id,
211-
node_type="1",
212-
metadata={},
213-
),
214-
},
182+
vector=embedding.vector,
215183
)
216184
)
217185

@@ -330,10 +298,6 @@ async def search(self, query: VectorStoreQuery) -> List[SearchResult]:
330298
return []
331299
raise
332300

333-
def compute_deterministic_chunk_id(self, document_id: str, chunk_idx: int) -> str:
334-
# the id_ of the Node must be a UUID string, otherwise llama_index / LanceDB fails downstream
335-
return str(string_to_uuid(f"{document_id}::{chunk_idx}"))
336-
337301
async def count_records(self) -> int:
338302
try:
339303
table = self.lancedb_vector_store.table
@@ -346,15 +310,7 @@ async def count_records(self) -> int:
346310

347311
@property
348312
def query_type(self) -> Literal["fts", "hybrid", "vector"]:
349-
match self.vector_store_config.store_type:
350-
case VectorStoreType.LANCE_DB_FTS:
351-
return "fts"
352-
case VectorStoreType.LANCE_DB_HYBRID:
353-
return "hybrid"
354-
case VectorStoreType.LANCE_DB_VECTOR:
355-
return "vector"
356-
case _:
357-
raise_exhaustive_enum_error(self.vector_store_config.store_type)
313+
return store_type_to_lancedb_query_type(self.vector_store_config.store_type)
358314

359315
@staticmethod
360316
def lancedb_path_for_config(rag_config: RagConfig) -> str:
@@ -380,9 +336,7 @@ async def delete_nodes_not_in_set(self, document_ids: Set[str]) -> None:
380336
kiln_doc_id = row["metadata"]["kiln_doc_id"]
381337
if kiln_doc_id not in document_ids:
382338
kiln_chunk_idx = row["metadata"]["kiln_chunk_idx"]
383-
record_id = self.compute_deterministic_chunk_id(
384-
kiln_doc_id, kiln_chunk_idx
385-
)
339+
record_id = deterministic_chunk_id(kiln_doc_id, kiln_chunk_idx)
386340
rows_to_delete.append(record_id)
387341

388342
if rows_to_delete:

0 commit comments

Comments
 (0)