docs: split Query from Job, and fold the agent tutorial into Agentic Skills

Query and Job were one page because they share the _async convention, but they
are different things: a query returns rows and streams them, a job returns an
outcome you wait for. Two pages now, query first, each carrying a short pointer
to the other rather than the shared framing.

Agentic Skills absorbs the agent plugin tutorial, which was a separate page in
Use Cases describing the same plugin in more detail -- the two had diverged, with
the tutorial carrying the fuller install instructions. It also moves to Getting
Started: installing the plugin is something you do before writing anything, not
a concept to understand.
This commit is contained in:
Jack Ye
2026-09-25 15:40:49 -07:00
parent 70c10e4aa4
commit c9275e077b
11 changed files with 369 additions and 418 deletions
+245 -15
View File
@@ -6,6 +6,12 @@ icon: robot
keywords: ["agents", "plugin", "claude code", "codex", "cursor", "copilot"]
---
import {
PyCamelotSchema,
PyCamelotBatches,
PyCamelotOssIngestion,
} from '/snippets/build_with_ai_agents.mdx';
A coding agent writing LanceDB code from memory writes whatever was in its
training data -- often an older API, sometimes one that never existed. The
LanceDB agent plugin fixes that by supplying the current reference at the point
@@ -13,11 +19,10 @@ the agent needs it.
## The plugin {#plugin}
It ships as the `lancedb` plugin in the
[lancedb-agent-plugins](https://github.com/lancedb/lancedb-agent-plugins)
repository, which doubles as a plugin marketplace. It covers the Python and
TypeScript APIs, which code is portable between a local database and a
deployment, ingestion performance, and branch operations.
It ships as the `lancedb` plugin in
the [lancedb-agent-plugins](https://github.com/lancedb/lancedb-agent-plugins)
repository, which is also a plugin marketplace. Install it with your agent's
plugin manager:
<CodeGroup>
```text Claude Code
@@ -35,27 +40,252 @@ npx plugins add lancedb/lancedb-agent-plugins
```
</CodeGroup>
The cross-tool installer detects which agent CLIs are on your `PATH` -- Cursor,
GitHub Copilot CLI, VS Code, Grok Build, Kimi Code, Claude Code, Codex -- and
installs through each one's native plugin system.
The [`plugins`](https://www.npmjs.com/package/plugins) installer shown under
**Other agents** is a cross-tool option. It detects which agent CLIs are on your
`PATH` — Cursor, GitHub Copilot CLI, VS Code, Grok Build, and Kimi Code, as well
as Claude Code and Codex — and installs through each one's native plugin system.
The plugin supplements the agent's training data with current LanceDB
instructions. To pick up later revisions, refresh the marketplace:
<CodeGroup>
```text Claude Code
/plugin marketplace update lancedb
```
```bash Codex icon="terminal"
codex plugin marketplace upgrade
```
```bash Other agents icon="terminal"
npx plugins add lancedb/lancedb-agent-plugins
```
</CodeGroup>
<Accordion title="Where the plugin gets installed">
Each plugin manager clones the repository, reads its marketplace manifest to find
the `lancedb` plugin, and hands the plugin to the agent's own plugin store rather
than copying files into your project. Claude Code and Codex keep it under
`~/.claude/plugins` and `~/.codex/plugins` respectively, so the plugin is available
in every project on the machine.
The `npx plugins` installer defaults to the same user-wide scope. Pass
`--scope project` to record the plugin in the current repository instead, so that
everyone working in it gets the same plugin, or `-t <target>` to install for a
single agent rather than every one it detects:
```bash
npx plugins add lancedb/lancedb-agent-plugins --scope project -t cursor
```
Not every agent supports project scope; `npx plugins targets` lists what it
found and what each target supports.
</Accordion>
### Get started with the LanceDB agent plugin {#get-started-with-the-lancedb-agent-plugin}
This tutorial uses the Camelot dataset from the [quickstart](/quickstart), with
a portrait added for each character. Each LanceDB row contains validated
metadata and raw JPEG bytes. Text, images, and any embeddings you add later
remain in the same table.
### 1\. Download the multimodal dataset {#1-download-the-multimodal-dataset}
From a new project directory, download the JSON file and portraits:
```bash
mkdir -p data/img
BASE_URL="https://docs.lancedb.com/static/assets/tutorials/build-with-ai-agents/camelot/data"
curl -fsSL "$BASE_URL/camelot.json" -o data/camelot.json
for image in \
arthur.jpg \
guinevere.jpg \
merlin.jpg \
mordred.jpg \
sir_galahad.jpg \
sir_gawain.jpg \
sir_lancelot.jpg \
sir_percival.jpg
do
curl -fsSL "$BASE_URL/img/$image" -o "data/img/$image"
done
```
Each JSON record has this shape:
```json
{
"id": 2,
"name": "Merlin",
"role": "Wizard and Advisor",
"description": "A powerful wizard and prophet who mentors Arthur.",
"stats": {
"strength": 2,
"courage": 4,
"magic": 5,
"wisdom": 5
},
"img": "data/img/merlin.jpg"
}
```
JSON input may have missing fields, unexpected fields, or values of the wrong
type. The LanceDB plugin tells the agent to validate each record with strict
Pydantic models before writing it.
After the agent writes the pipeline, inspect the schema, batching, and write
path rather than assuming it followed the plugin's guidance correctly.
### 2\. Prompt your agent to build the pipeline {#2-prompt-your-agent-to-build-the-pipeline}
Install the Python packages used by the example:
```bash icon="terminal"
uv init
uv add lancedb pyarrow pydantic
```
If you're using LanceDB Enterprise, ask the agent to ingest into an Enterprise table,
provide the relevant environment variables for connecting to your Enterprise deployment
in a local `.env` file, and point the agent to it.
```text .env
LANCEDB_URI=db://your_project_name
LANCEDB_API_KEY=your_api_key_here
LANCEDB_REGION=us-east-1
LANCEDB_HOST_OVERRIDE=https://your-enterprise-endpoint.com
```
If you're using LanceDB OSS, no connection settings are required, as it runs as an
embedded retrieval library. A simple prompt like this should work:
```text Agent prompt
# If using OSS
Use the lancedb plugin to ingest the dataset in `data/` into a LanceDB
OSS table.
# If using enterprise
Use the lancedb plugin to ingest the dataset in `data/` into a LanceDB
Enterprise table using the connection information in `.env`.
```
Because the plugin is registered with the agent's own plugin system, the agent
should pick it up on its own once you restart the session. If it does not,
simply ask it to use the `lancedb` plugin in the prompt, as shown above.
That should be enough! The agent will create `ingest_multimodal.py`, or similar.
The following sections inspect the script to verify that it follows the plugin's guidance.
#### Data validation {#data-validation}
The plugin encourages the agent to validate each record with Pydantic before writing it.
The agent should ideally define a schema for the table and a nested schema for the
`stats` field.
<CodeBlock filename="ingest_multimodal.py" language="Python" icon="python">
{PyCamelotSchema}
</CodeBlock>
In this case, our agent correctly defined `Character` and `Stats` Pydantic models
and validated the JSON before adding it to the table.
#### Batched ingestion {#batched-ingestion}
Naively calling `table.add()` once per row is slow, and is considered an anti-pattern
in LanceDB. The plugin encourages the agent to collect incoming rows into batches and
write them with a single `table.add()` call. When you use the plugin, the agent should
produce something like this:
<CodeBlock filename="ingest_multimodal.py" language="Python" icon="python">
{PyCamelotBatches}
</CodeBlock>
The script calls `Character.model_validate(...)` before adding a record to the
batch. If validation fails, that batch is never written. The function yields up
to `batch_size` rows at a time, providing an iterable of batches for the ingestion step,
shown next.
#### Table maintenance {#table-maintenance}
For LanceDB OSS, the plugin instructs the agent to call
`table.optimize()` after the ingestion loop. This compacts small fragments, cleans up
old versions according to the retention policy, and incorporates new data into indexes.
<CodeBlock filename="ingest_multimodal.py" language="Python" icon="python">
{PyCamelotOssIngestion}
</CodeBlock>
If you're using LanceDB Enterprise, the plugin mentions that this step is not needed
because LanceDB Enterprise handles maintenance automatically.
<Note>
This example dataset has only eight rows, so the default batch size writes it in
one call. Larger inputs should still avoid single-row write commits.
</Note>
### 3\. Run the OSS pipeline {#3-run-the-oss-pipeline}
```bash
uv run python ingest_multimodal.py
```
The table now contains the validated character records and their JPEG bytes.
Here are the first three rows:
| Image | Character | Role |
| --- | --- | --- |
| <img src="/static/assets/tutorials/build-with-ai-agents/camelot/data/img/arthur.jpg" alt="King Arthur" width="128" height="128" /> | King Arthur | King of Camelot |
| <img src="/static/assets/tutorials/build-with-ai-agents/camelot/data/img/merlin.jpg" alt="Merlin" width="128" height="128" /> | Merlin | Wizard and Advisor |
| <img src="/static/assets/tutorials/build-with-ai-agents/camelot/data/img/guinevere.jpg" alt="Queen Guinevere" width="128" height="128" /> | Queen Guinevere | Queen of Camelot |
Once your pipeline works, you can [run experiments on branches](/agent-branch-experiments)
to try new embedding models, parsers, or search settings without touching `main`.
### Takeaways {#takeaways}
The example in this tutorial was small, but similar ideas apply to other workflows, too.
Give the agent the data source, the constraints it must respect, and the
artifacts it should return.
The plugin supplies LanceDB-specific guidance, but it's the user's responsibility to
ensure the output makes sense for the application.
### Try the plugin with your own dataset {#try-the-plugin-with-your-own-dataset}
The plugin shown in this tutorial should generalize reasonably well to other use cases.
If you find any issues, open [an issue](https://github.com/lancedb/lancedb-agent-plugins/issues)
on GitHub, clearly describing the intended behavior.
You can choose OSS or Enterprise based on how the work will run:
Start with [LanceDB OSS](/quickstart) during the early stages of a project
when an agent is helping you prototype,
explore a dataset, or run small workflows on a subset of the data. The application owns
the storage and lifecycle work, so ask the agent to validate inputs, write in
batches, and it will include table maintenance operations such as `optimize()`
where appropriate.
Choose [LanceDB Enterprise](/enterprise) when the resulting table becomes
shared production infrastructure, and the workload needs distributed capacity,
private deployment, or platform-managed operations. The underlying data format and table
API stay the same, so the pipeline does not need to be redesigned. The agent
instead connects to a remote `db://` table and lets the cluster handle
maintenance and background work.
## What agents are good at here {#workflows}
Two things about LanceDB make it unusually agent-friendly, and both have a
worked tutorial.
The walkthrough above is one pattern. The other has its own tutorial.
<Columns cols={2}>
<Card title="Build an ingestion pipeline" icon="robot" href="/build-with-ai-agents">
Install the plugin and have an agent build a multimodal ingestion pipeline
end to end.
</Card>
<Card title="Run experiments on branches" icon="flask" href="/agent-branch-experiments">
Isolate agent-driven experiments on a branch, evaluate them on a fixed test
set, and promote only the winner.
</Card>
</Columns>
The second is the more interesting pattern. [Branches](/management/table#versioning)
It is the more interesting pattern. [Branches](/management/table#versioning)
give an agent somewhere to be wrong safely: it can rewrite a schema, recompute a
column, or try three embedding models without touching `main`, and you keep only
what measured better.
+1 -1
View File
@@ -73,7 +73,7 @@ table = reader.read_all()
`execute_query` returns a blocking Arrow reader once results start arriving; it
does not wait for the whole query. `execute_query_async` returns a handle
instead -- see [Query and job](/basics/jobs#queries).
instead -- see [Queries](/basics/query).
## Choosing between them {#choosing}
+1 -1
View File
@@ -75,4 +75,4 @@ latency, or when the query is cheap.
performance decision -- see [Maintenance](/indexing).
**Jobs** are not stored data at all; they are handles to work the server is
doing. See [Query and job](/basics/jobs).
doing. See [Jobs](/basics/jobs).
+1 -1
View File
@@ -98,7 +98,7 @@ nothing.
**A job id is opaque.** It correlates with server logs and the jobs API.
Parsing it, or storing it to resume the job later, is not supported.
See [Query and job](/basics/jobs).
See [Jobs](/basics/jobs).
## Schema {#schema}
+16 -118
View File
@@ -1,127 +1,25 @@
---
title: "Query and job"
sidebarTitle: "Query & Job"
description: "Sync and async in LanceDB: two different distinctions that use the same word."
title: "Jobs"
sidebarTitle: "Job"
description: "Work that outlives the call that started it: submitting, waiting, inspecting, cancelling."
icon: hourglass-half
keywords: ["job", "async", "sync", "query", "background", "await"]
keywords: ["job", "async", "background", "events", "cancel", "refresh"]
---
"Async" means two unrelated things in LanceDB, and conflating them is the most
common source of confusion about how work gets done.
| | What it is | What it changes |
| --- | --- | --- |
| **Async client** | `connect_async`, `await` | whether *your thread* blocks |
| **Async execution** | `_async` methods returning a handle | whether *the work* outlives the call |
They are independent. A synchronous Python call can start work that runs for an
hour; an `await` can wait for something that finishes instantly.
<Note>
Jobs are listed in SQL with [`SHOW JOBS`](/sql/show#jobs), and their failures with [`SHOW ERRORS`](/sql/show#errors).
</Note>
## Every `_async` method returns a handle {#handles}
An `_async` method submits work and hands back a **handle** to it. Which handle
depends on what you submitted -- `execute_query_async` returns a query,
everything else returns a [job](#jobs) -- but they are the same idea, and they
answer the same four questions:
| | Query handle | Job handle |
| --- | --- | --- |
| Which one is this? | `id` | `id` |
| How is it going? | `describe()` | `status()`, `refresh()`, `events()` |
| Give me the result | `reader()` — streams batches as they arrive | `wait()` — blocks for the terminal result |
| Stop it | `cancel()` | `cancel()` |
The difference is in the third row, and it follows from what the work produces.
A query produces **rows**, so its handle streams them and you can start reading
before the query finishes. A job produces **an outcome** -- an index built, a
column filled -- so its handle has nothing to stream and gives you the result
once, at the end.
Everything else is shared: both identify the work on the server, both report on
it without blocking, and both can be told to stop.
## Async client: whether you block {#async-client}
A concern of your language, not of LanceDB. The work is identical either way.
<CodeGroup>
```python Python icon="python"
db = lancedb.connect("db://my-db", api_key="...") # blocking
tbl = db.open_table("documents")
db = await lancedb.connect_async("db://my-db", api_key="...") # coroutines
tbl = await db.open_table("documents")
```
```typescript TypeScript icon="square-js"
// TypeScript is async throughout; there is no blocking form.
const db = await lancedb.connect("db://my-db", { apiKey: "..." });
const tbl = await db.openTable("documents");
```
</CodeGroup>
Python offers both. `AsyncConnection` exposes the same surface as coroutines, so
choosing between them is about the program you are writing -- a service wants
the async form, a script does not care.
## Queries: reading before the query finishes {#queries}
A query is not a job, but it has the same shape of choice: wait for everything,
or take a handle and start reading.
<CodeGroup>
```python Python icon="python"
reader = db.execute_query("SELECT id, text FROM my_docs LIMIT 1000")
table = reader.read_all()
query = db.execute_query_async("SELECT id, text FROM my_docs")
```
```sql SQL
SELECT id, text FROM my_docs LIMIT 1000
```
</CodeGroup>
`execute_query` returns a blocking Arrow reader **once results start arriving**
-- it does not wait for the whole query to finish. Results stream as record
batches, so a large result set does not have to fit in memory before you see its
first row.
`execute_query_async` returns the query handle instead:
```python
query = db.execute_query_async("SELECT id, text FROM my_docs")
query.id # identifies it on the server
query.describe() # what it is doing, without consuming results
reader = query.reader() # stream the rows when you want them
query.cancel() # stop it
```
`describe_query(id)` reports on one from elsewhere, the way `open_job(id)` does
for a job.
<Note>
SQL requires a deployment. See [SDKs and SQL](/basics/apis).
</Note>
Query-builder searches -- vector, full-text, hybrid -- are ordinary calls that
return results. They are fast enough not to need a handle. See
[Query](/search).
## Jobs: work that outlives the call {#jobs}
Some operations take longer than a request should. Building an index over a
large table, refreshing a computed column, registering a function -- these run
on the server after the call that started them has returned.
Each is submitted the same way and returns the same handle.
### The `_async` convention {#convention}
<Note>
A [query](/basics/query) also has an `_async` form, but its handle streams rows
rather than waiting for an outcome. Both answer the same four questions --
which one is this, how is it going, give me the result, stop it -- and differ
only in the third.
</Note>
## The `_async` convention {#convention}
An operation that can run in the background has two forms. The plain name blocks
until the work is done. The `_async` / `Async` name submits the work and returns
@@ -140,7 +38,7 @@ waits. Reach for the `_async` form when you want to do something else meanwhile,
run several at once, or survive the client going away.
</Note>
### Submitting and waiting {#waiting}
## Submitting and waiting {#waiting}
<CodeGroup>
```python Python icon="python"
@@ -184,7 +82,7 @@ The TypeScript `wait()` resolves to `void`. Read the outcome from `result` after
it resolves.
</Note>
### Inspecting a job {#inspecting}
## Inspecting a job {#inspecting}
Submitting an operation returns only an id, so the detail fields are empty until
the handle fetches the record. `refresh()` does that; `status()` does it as a
@@ -217,7 +115,7 @@ job.failure; // why it failed, when it did
Printing the handle renders every field it knows, which is usually faster than
reading them one at a time.
### Progress, from events {#events}
## Progress, from events {#events}
`result` only tells you anything once the job has finished. Events are written
as it runs, and they outlive the workers that produced them -- so a job that is
@@ -253,7 +151,7 @@ silently lose its tail unless you pass `limit`.
Events are recorded server-side, so this raises for an operation that ran
in-process.
### Finding and cancelling jobs {#managing}
## Finding and cancelling jobs {#managing}
The connection lists and reopens jobs, so a handle lost to a restarted process
is recoverable from its id.
+1 -1
View File
@@ -45,7 +45,7 @@ page most likely to mislead you, because nothing in the result says it was cut.
32 of them. Narrow with `FOR JOB` when you need a specific one rather than a
survey.
See [Query and job](/basics/jobs).
See [Jobs](/basics/jobs).
## Schema and types {#schema}
+94
View File
@@ -0,0 +1,94 @@
---
title: "Queries"
sidebarTitle: "Query"
description: "Running a query, and reading its results before it finishes."
icon: magnifying-glass
keywords: ["query", "async", "streaming", "arrow", "reader", "execute_query"]
---
A query returns rows. How you get them depends on two choices that are easy to
conflate: whether **your thread** blocks, and whether you take the results all
at once or stream them as they arrive.
## Blocking or not is a language concern {#async-client}
A concern of your language, not of LanceDB. The work is identical either way.
<CodeGroup>
```python Python icon="python"
db = lancedb.connect("db://my-db", api_key="...") # blocking
tbl = db.open_table("documents")
db = await lancedb.connect_async("db://my-db", api_key="...") # coroutines
tbl = await db.open_table("documents")
```
```typescript TypeScript icon="square-js"
// TypeScript is async throughout; there is no blocking form.
const db = await lancedb.connect("db://my-db", { apiKey: "..." });
const tbl = await db.openTable("documents");
```
</CodeGroup>
Python offers both. `AsyncConnection` exposes the same surface as coroutines, so
choosing between them is about the program you are writing -- a service wants
the async form, a script does not care.
## Reading before the query finishes {#queries}
A query is not a job, but it has the same shape of choice: wait for everything,
or take a handle and start reading.
<CodeGroup>
```python Python icon="python"
reader = db.execute_query("SELECT id, text FROM my_docs LIMIT 1000")
table = reader.read_all()
query = db.execute_query_async("SELECT id, text FROM my_docs")
```
```sql SQL
SELECT id, text FROM my_docs LIMIT 1000
```
</CodeGroup>
`execute_query` returns a blocking Arrow reader **once results start arriving**
-- it does not wait for the whole query to finish. Results stream as record
batches, so a large result set does not have to fit in memory before you see its
first row.
`execute_query_async` returns the query handle instead:
```python
query = db.execute_query_async("SELECT id, text FROM my_docs")
query.id # identifies it on the server
query.describe() # what it is doing, without consuming results
reader = query.reader() # stream the rows when you want them
query.cancel() # stop it
```
`describe_query(id)` reports on one from elsewhere, the way `open_job(id)` does
for a job.
<Note>
SQL requires a deployment. See [SDKs and SQL](/basics/apis).
</Note>
Query-builder searches -- vector, full-text, hybrid -- are ordinary calls that
return results. They are fast enough not to need a handle. See
[Query](/search).
## Searches are ordinary calls {#searches}
Query-builder searches -- [vector](/search/vector-search),
[full-text](/search/full-text-search), [hybrid](/search/hybrid-search) -- return
results directly. They are fast enough not to need a handle, and they have no
SQL form. See [Semantic Search](/search).
## When the work outlives the call {#jobs}
Some operations take longer than a query should: building an index, refreshing a
computed column, registering a function. Those return a **job** rather than
rows, and a job is a different handle with a different shape. See
[Jobs](/basics/jobs).
-270
View File
@@ -1,270 +0,0 @@
---
title: "Tutorial: Use the LanceDB agent plugin"
sidebarTitle: "Using LanceDB Agent Skill"
description: "Install the LanceDB plugin and use an AI coding agent to quickly build a multimodal ingestion pipeline."
icon: "robot"
keywords: ["AI agents", "agent plugins", "multimodal", "Pydantic", "branches", "embeddings"]
---
import {
PyCamelotSchema,
PyCamelotBatches,
PyCamelotOssIngestion,
} from '/snippets/build_with_ai_agents.mdx';
The LanceDB agent plugin gives coding agents a maintained reference for the
Python and TypeScript APIs. It also covers portable OSS and Enterprise code,
ingestion performance, and branch operations. It ships as the `lancedb` plugin in
the [lancedb-agent-plugins](https://github.com/lancedb/lancedb-agent-plugins)
repository, which is also a plugin marketplace. Install it with your agent's
plugin manager:
<CodeGroup>
```text Claude Code
/plugin marketplace add lancedb/lancedb-agent-plugins
/plugin install lancedb@lancedb
```
```bash Codex icon="terminal"
codex plugin marketplace add lancedb/lancedb-agent-plugins
codex plugin add lancedb@lancedb
```
```bash Other agents icon="terminal"
npx plugins add lancedb/lancedb-agent-plugins
```
</CodeGroup>
The [`plugins`](https://www.npmjs.com/package/plugins) installer shown under
**Other agents** is a cross-tool option. It detects which agent CLIs are on your
`PATH` — Cursor, GitHub Copilot CLI, VS Code, Grok Build, and Kimi Code, as well
as Claude Code and Codex — and installs through each one's native plugin system.
The plugin supplements the agent's training data with current LanceDB
instructions. To pick up later revisions, refresh the marketplace:
<CodeGroup>
```text Claude Code
/plugin marketplace update lancedb
```
```bash Codex icon="terminal"
codex plugin marketplace upgrade
```
```bash Other agents icon="terminal"
npx plugins add lancedb/lancedb-agent-plugins
```
</CodeGroup>
<Accordion title="Where the plugin gets installed">
Each plugin manager clones the repository, reads its marketplace manifest to find
the `lancedb` plugin, and hands the plugin to the agent's own plugin store rather
than copying files into your project. Claude Code and Codex keep it under
`~/.claude/plugins` and `~/.codex/plugins` respectively, so the plugin is available
in every project on the machine.
The `npx plugins` installer defaults to the same user-wide scope. Pass
`--scope project` to record the plugin in the current repository instead, so that
everyone working in it gets the same plugin, or `-t <target>` to install for a
single agent rather than every one it detects:
```bash
npx plugins add lancedb/lancedb-agent-plugins --scope project -t cursor
```
Not every agent supports project scope; `npx plugins targets` lists what it
found and what each target supports.
</Accordion>
## Get started with the LanceDB agent plugin {#get-started-with-the-lancedb-agent-plugin}
This tutorial uses the Camelot dataset from the [quickstart](/quickstart), with
a portrait added for each character. Each LanceDB row contains validated
metadata and raw JPEG bytes. Text, images, and any embeddings you add later
remain in the same table.
### 1\. Download the multimodal dataset {#1-download-the-multimodal-dataset}
From a new project directory, download the JSON file and portraits:
```bash
mkdir -p data/img
BASE_URL="https://docs.lancedb.com/static/assets/tutorials/build-with-ai-agents/camelot/data"
curl -fsSL "$BASE_URL/camelot.json" -o data/camelot.json
for image in \
arthur.jpg \
guinevere.jpg \
merlin.jpg \
mordred.jpg \
sir_galahad.jpg \
sir_gawain.jpg \
sir_lancelot.jpg \
sir_percival.jpg
do
curl -fsSL "$BASE_URL/img/$image" -o "data/img/$image"
done
```
Each JSON record has this shape:
```json
{
"id": 2,
"name": "Merlin",
"role": "Wizard and Advisor",
"description": "A powerful wizard and prophet who mentors Arthur.",
"stats": {
"strength": 2,
"courage": 4,
"magic": 5,
"wisdom": 5
},
"img": "data/img/merlin.jpg"
}
```
JSON input may have missing fields, unexpected fields, or values of the wrong
type. The LanceDB plugin tells the agent to validate each record with strict
Pydantic models before writing it.
After the agent writes the pipeline, inspect the schema, batching, and write
path rather than assuming it followed the plugin's guidance correctly.
### 2\. Prompt your agent to build the pipeline {#2-prompt-your-agent-to-build-the-pipeline}
Install the Python packages used by the example:
```bash icon="terminal"
uv init
uv add lancedb pyarrow pydantic
```
If you're using LanceDB Enterprise, ask the agent to ingest into an Enterprise table,
provide the relevant environment variables for connecting to your Enterprise deployment
in a local `.env` file, and point the agent to it.
```text .env
LANCEDB_URI=db://your_project_name
LANCEDB_API_KEY=your_api_key_here
LANCEDB_REGION=us-east-1
LANCEDB_HOST_OVERRIDE=https://your-enterprise-endpoint.com
```
If you're using LanceDB OSS, no connection settings are required, as it runs as an
embedded retrieval library. A simple prompt like this should work:
```text Agent prompt
# If using OSS
Use the lancedb plugin to ingest the dataset in `data/` into a LanceDB
OSS table.
# If using enterprise
Use the lancedb plugin to ingest the dataset in `data/` into a LanceDB
Enterprise table using the connection information in `.env`.
```
Because the plugin is registered with the agent's own plugin system, the agent
should pick it up on its own once you restart the session. If it does not,
simply ask it to use the `lancedb` plugin in the prompt, as shown above.
That should be enough! The agent will create `ingest_multimodal.py`, or similar.
The following sections inspect the script to verify that it follows the plugin's guidance.
#### Data validation {#data-validation}
The plugin encourages the agent to validate each record with Pydantic before writing it.
The agent should ideally define a schema for the table and a nested schema for the
`stats` field.
<CodeBlock filename="ingest_multimodal.py" language="Python" icon="python">
{PyCamelotSchema}
</CodeBlock>
In this case, our agent correctly defined `Character` and `Stats` Pydantic models
and validated the JSON before adding it to the table.
#### Batched ingestion {#batched-ingestion}
Naively calling `table.add()` once per row is slow, and is considered an anti-pattern
in LanceDB. The plugin encourages the agent to collect incoming rows into batches and
write them with a single `table.add()` call. When you use the plugin, the agent should
produce something like this:
<CodeBlock filename="ingest_multimodal.py" language="Python" icon="python">
{PyCamelotBatches}
</CodeBlock>
The script calls `Character.model_validate(...)` before adding a record to the
batch. If validation fails, that batch is never written. The function yields up
to `batch_size` rows at a time, providing an iterable of batches for the ingestion step,
shown next.
#### Table maintenance {#table-maintenance}
For LanceDB OSS, the plugin instructs the agent to call
`table.optimize()` after the ingestion loop. This compacts small fragments, cleans up
old versions according to the retention policy, and incorporates new data into indexes.
<CodeBlock filename="ingest_multimodal.py" language="Python" icon="python">
{PyCamelotOssIngestion}
</CodeBlock>
If you're using LanceDB Enterprise, the plugin mentions that this step is not needed
because LanceDB Enterprise handles maintenance automatically.
<Note>
This example dataset has only eight rows, so the default batch size writes it in
one call. Larger inputs should still avoid single-row write commits.
</Note>
### 3\. Run the OSS pipeline {#3-run-the-oss-pipeline}
```bash
uv run python ingest_multimodal.py
```
The table now contains the validated character records and their JPEG bytes.
Here are the first three rows:
| Image | Character | Role |
| --- | --- | --- |
| <img src="/static/assets/tutorials/build-with-ai-agents/camelot/data/img/arthur.jpg" alt="King Arthur" width="128" height="128" /> | King Arthur | King of Camelot |
| <img src="/static/assets/tutorials/build-with-ai-agents/camelot/data/img/merlin.jpg" alt="Merlin" width="128" height="128" /> | Merlin | Wizard and Advisor |
| <img src="/static/assets/tutorials/build-with-ai-agents/camelot/data/img/guinevere.jpg" alt="Queen Guinevere" width="128" height="128" /> | Queen Guinevere | Queen of Camelot |
Once your pipeline works, you can [run experiments on branches](/agent-branch-experiments)
to try new embedding models, parsers, or search settings without touching `main`.
## Takeaways {#takeaways}
The example in this tutorial was small, but similar ideas apply to other workflows, too.
Give the agent the data source, the constraints it must respect, and the
artifacts it should return.
The plugin supplies LanceDB-specific guidance, but it's the user's responsibility to
ensure the output makes sense for the application.
### Try the plugin with your own dataset {#try-the-plugin-with-your-own-dataset}
The plugin shown in this tutorial should generalize reasonably well to other use cases.
If you find any issues, open [an issue](https://github.com/lancedb/lancedb-agent-plugins/issues)
on GitHub, clearly describing the intended behavior.
You can choose OSS or Enterprise based on how the work will run:
Start with [LanceDB OSS](/quickstart) during the early stages of a project
when an agent is helping you prototype,
explore a dataset, or run small workflows on a subset of the data. The application owns
the storage and lifecycle work, so ask the agent to validate inputs, write in
batches, and it will include table maintenance operations such as `optimize()`
where appropriate.
Choose [LanceDB Enterprise](/enterprise) when the resulting table becomes
shared production infrastructure, and the workload needs distributed capacity,
private deployment, or platform-managed operations. The underlying data format and table
API stay the same, so the pipeline does not need to be redesigned. The agent
instead connects to a remote `db://` table and lets the cluster handle
maintenance and background work.
+8 -9
View File
@@ -138,6 +138,10 @@
{
"source": "/training",
"destination": "/training/data-loading"
},
{
"source": "/build-with-ai-agents",
"destination": "/basics/agentic-skills"
}
],
"navigation": {
@@ -150,7 +154,8 @@
"pages": [
"index",
"quickstart",
"architecture"
"architecture",
"basics/agentic-skills"
]
},
{
@@ -158,12 +163,12 @@
"pages": [
"basics/lance",
"basics/catalog",
"basics/query",
"basics/jobs",
"tables/consistency",
"basics/guarantees",
"basics/limits",
"basics/apis",
"basics/agentic-skills"
"basics/apis"
]
},
{
@@ -393,12 +398,6 @@
"tab": "Use Cases",
"pages": [
"tutorials/index",
{
"group": "General",
"pages": [
"build-with-ai-agents"
]
},
{
"group": "Search",
"pages": [
+1 -1
View File
@@ -30,7 +30,7 @@ Queries execute on the server and results come back as Arrow record batches --
nothing is converted to pandas or Python objects unless you ask. `execute_query`
hands back a reader as soon as the first batch is ready rather than when the
query finishes, so a large result set does not have to fit in memory before you
start reading it. See [Query and job](/basics/jobs#queries).
start reading it. See [Queries](/basics/query).
Served over an [Arrow Flight SQL](https://arrow.apache.org/docs/format/FlightSql.html)
endpoint, so any Flight SQL client reaches it.
+1 -1
View File
@@ -49,7 +49,7 @@ SHOW JOBS
```
Every job the server knows about — id, type, state, creation time. The SQL
counterpart of `list_jobs`; see [Query and job](/basics/jobs).
counterpart of `list_jobs`; see [Jobs](/basics/jobs).
## SHOW ERRORS {#errors}