Structured lookup

For tabular documents (Excel .xlsx or .csv), semantic search is often the wrong tool. When a file holds structured records — people with a name, phone and address; products with a SKU; accounts with an id — you usually want to look a record up by a specific column, not retrieve semantically-similar text.

A structured lookup exposes a dedicated tool that looks records up in such a document: the agent calls it with a value (for example, a caller's name) and gets back the matching row (or a short list of candidates). Matching is fuzzy and phonetic, so speech-to-text errors like Kaitlyn Meyers still resolve to Caitlin Myers. A lookup can also match on more than one column at once — by phone and city, say — when one column alone doesn't identify a record.

Configuring any lookup makes the document lookup-only: its files are not embedded and are not available through RAG, doc_search or doc_get. Attach it to an agent like any other document — the agent gains the lookup tool(s) automatically, in both text and speech-to-speech modes. A single agent can freely mix lookup documents and ordinary RAG documents.

A Flow can use a lookup document the same way, by attaching it to a conversation node, so the node grounds its responses on the retrieved record. Alternatively, the #lookup directive turns such a node into the equivalent of a Call tool node for the lookup: the result never reaches the model, and the flow branches on the outcome.

Configuring a lookup

Lookups are defined in the document's Advanced configuration, under a lookups array. Each entry becomes one tool:

{
  "lookups": [
    {
      "tool_name": "lookup_customer",
      "tool_description": "Look up a customer's account by name.",
      "search_column": "Full Name",
      "return_columns": ["Full Name", "Account ID", "City"]
    }
  ]
}

Only tool_name and search_column are required. Everything else is optional:

Field Description Default
tool_name Name of the tool exposed to the agent. Required.
search_column Primary column matched against the caller value — a column letter (A, B, …) or, when the file has a header row, a header name. Required.
additional_search_columns Up to 2 further columns to match on as well — see Matching on additional columns. none
file Which file to search, when the document has more than one. first Excel/CSV file
sheet Worksheet name (Excel only). first sheet
header_row 1-based row of the header; rows above it are skipped. Use 0 for a file with no header (columns are then referenced only by letter). 1 (auto)
tool_description Description shown to the agent. auto-generated
param_name Name of the tool argument carrying the search_column value (for example phone). the column name as an identifier (Full Namefull_name)
return_columns Columns to include in the result (letters or names); unknown columns are ignored, and every matched column is always included. all columns
match_mode exact, fuzzy (typo-tolerant), phonetic (sounds-alike), or hybrid (both). hybrid
min_score Minimum match score (0–100) for a row to be a candidate. 75
confident_score A single match at or above this score, clearly ahead of the rest, is returned as a confident match rather than as candidates. 92
max_candidates Maximum candidates returned when the match is ambiguous. 5
debug Include a scores list in the result to help tune the thresholds (see below). Disable in production. false

Note: A param_name that collides with a reserved field (such as name) is renamed internally and falls back to query where needed. Prefer distinctive names like customer_name or phone.

Referencing columns

Columns may be referenced two ways:

If a value matches both a header name and a letter, the header name wins.

Matching on additional columns

One column often isn't enough to identify a record — two customers share a name, or a phone number appears against several accounts. Add up to two more columns under additional_search_columns, and each becomes its own tool argument:

{
  "lookups": [
    {
      "tool_name": "find_customer",
      "search_column": "Phone",
      "param_name": "phone",
      "match_mode": "exact",
      "additional_search_columns": [
        { "column": "City", "param_name": "city", "match_mode": "hybrid" }
      ],
      "return_columns": ["Full Name", "Account ID", "City"]
    }
  ]
}

Each entry takes:

Field Description Default
column The column to match — a letter or a header name, exactly like search_column. Required.
param_name Name of the tool argument carrying this column's value. the column name as an identifier (Home Cityhome_city) — or query_<column> when that would be too short to mean anything (Bquery_b)
param_description Description of the tool argument. auto-generated
match_mode Matching strategy for this column. the lookup's match_mode

A record must match every value the agent supplies. That is what makes an extra column narrow the search rather than widen it: the phone above is matched exactly while the city stays fuzzy, so a caller whose city was mis-heard is still found, but a record with the right phone and the wrong city is not returned at all.

Two consequences worth knowing:

A row is indexed as long as it fills at least one search column. A blank cell in an additional column never matches a supplied value, so such a row is unreachable through that column while staying findable by the ones it does fill.

What the agent receives

The tool returns one of three outcomes:

The tool description always tells the model to pass the value exactly as heard and not to "correct" it, because the fuzzy/phonetic layer handles STT errors better than the model's own guesses.

Tuning the thresholds

Good values for min_score and confident_score depend on your data, and the scores aren't obvious up front. Set debug to true while testing: every result then carries a scores list — the top matching rows with their raw scores, ignoring the thresholds — so you can see exactly how real queries score. Each entry breaks the row's score down per column, so you can tell which column rejected it.

For example, a not_found result then looks like:

{
  "result": "not_found",
  "scores": [
    {"score": 72, "keys": [{"column": "Full Name", "value": "Caitlin Myers", "score": 72}]},
    {"score": 58, "keys": [{"column": "Full Name", "value": "Colin Meyer",   "score": 58}]}
  ]
}

Here the intended record scored 72 but the default min_score of 75 rejected it — so lowering min_score to around 70 would let it through. Once tuned, set debug back to false (or remove it) so the scores aren't returned in production.

Table size

A single lookup indexes at most 10,000 rows. Every match mode except exact compares the caller's value against every indexed row, so this limit is what keeps a lookup fast enough to run inside a conversation turn. Adding more search columns doesn't change it.

Multiple files and lookups

A document may define several lookups — for example a customer file searchable by name and a separate lookup by phone, or two different files. Each lookup targets one file (via file) and produces its own tool. Files in the document that no lookup references are unused; this is reported on the document's status.

Viewing the indexed data

Because a lookup document is not chunked for RAG, the Info dialog shows the indexed rows instead: the row count per file and a preview of the rows (capped for large files). Any configuration problem — a missing search column, a file with no matching column — is reported in the document's status.