> ## Documentation Index
> Fetch the complete documentation index at: https://developers.lighton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Narrowing search with Agentic

> Detect content type and attribute filters from a natural-language query before searching, asking, or listing files.

<Tip>
  This tutorial uses [`POST /api/v3/content-types/scope`](/api-reference/facets/resolve-search-scope). The full schema lives in the [API reference](/api-reference/introduction).
</Tip>

Your app receives free-text queries, but your corpus has structured metadata: content types, dates, jurisdictions, statuses. **Agentic** bridges the gap — it infers the right content type and attribute filters from a natural-language query *before* searching, so semantic search runs on a narrowed, relevant subset instead of the entire corpus.

Without Agentic, a query like *"rejected electronics patents filed after 2023"* runs semantic search across all 20,000 documents. With Agentic, the API first infers `content_type: patent:electricity` and `attribute: decision_status:REJECTED, filing_date:>=2023-01-01`, then searches only the 5,409 matching documents.

```mermaid theme={null}
graph LR
    Q[User query] --> S[POST /content-types/scope]
    S --> G[Scored CTs + attributes]
    S --> PC[prompt_context]
    PC -->|Pass model| CM[Completion mode]
    PC -->|No model| PM[Prompt mode]
    CM --> SC[scope_completion]
    PM --> LLM[Your LLM]
    LLM --> F[content_type + attribute]
    SC --> F
    F --> Search[POST /search]
    F --> Ask[POST /ask]
    F --> Files[GET /files]
```

The endpoint supports two modes:

* **Completion mode** — pass a `model` parameter and the API calls the LLM for you. Returns `scope_completion` with parsed, normalized filters ready to pass to search.
* **Prompt mode** — omit `model` and the API returns `prompt_context`, a pre-built text block you feed to your own LLM.

Both paths produce the same output: `content_type` and `attribute` filters that map directly to the search API parameters.

### When you don't need scope inference

If your app already knows the right filters (e.g. the user picks "NDA" from a dropdown), skip the scope endpoint. Pass `content_type` and `attribute` directly to [`POST /api/v3/search`](/api-reference/search/search-document-chunks), [`POST /api/v3/ask`](/api-reference/ask/ask-a-question-over-your-documents), or [`GET /api/v3/files`](/api-reference/files/list-files-accessible-to-the-authenticated-user). See [Filtering documents by metadata](/tutorials/facets/filter) for the full syntax.

## Quickstart: scope then search

The simplest integration — one call to infer scope, one call to search within it.

```python title="scope_completion.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

# Infer content type and attribute filters for a search query.
# The model parameter tells the API to call the LLM and return
# parsed, normalized filters in scope_completion.
response = requests.post(
    "https://api.lighton.ai/api/v3/content-types/scope",
    headers=headers,
    json={
        "query": "rejected electronics patents filed after 2023",
        "model": "mistral-large-latest",  # replace with your model
    },
)
print(response.json())
```

The response includes ranked content types with their attribute definitions and the parsed scope:

```json theme={null}
{
  "has_signal": true,
  "groups": [
    {
      "root": "patent",
      "root_label": "Patent Classification",
      "max_score": 4.2,
      "content_types": [
        {
          "path": "patent:electricity",
          "label": "Electricity",
          "root": "patent",
          "score": 4.2,
          "chunk_count": 5409,
          "doc_count": 5409,
          "attributes": [
            {
              "name": "decision_status",
              "label": "Decision Status",
              "type": "select",
              "required": false,
              "description": "Patent application decision",
              "choices": ["Accepted", "Rejected"]
            },
            {
              "name": "filing_date",
              "label": "Filing Date",
              "type": "date",
              "required": false,
              "description": "Date the patent was filed",
              "choices": []
            }
          ]
        }
      ]
    }
  ],
  "scope_completion": {
    "content_type": "patent:electricity",
    "attribute": ["decision_status:REJECTED", "filing_date:>=2023-01-01"],
    "raw_output": "{\"content_type\":\"patent:electricity\",\"attribute\":[\"decision_status:REJECTED\",\"filing_date:>=2023-01-01\"]}",
    "normalized": false,
    "warnings": []
  },
  "prompt_context": "Content types (by relevance):\n  1. Electricity (patent:electricity) — score: 4.20, 5409 chunks *\n\nRelevant filters:\n  - decision_status \"Decision Status\" (select: Accepted, Rejected)\n  - filing_date \"Filing Date\" (date: >=, <=)\n  ...\n\nRULES:\n  1. Use null content_type when ...\n\nOUTPUT FORMAT:\n  {\"content_type\": \"<path>\" or null, \"attribute\": [...]}",
  "prompt_version": "t:a1b2c3.d:d4e5f6"
}
```

Extract `content_type` and `attribute` from `scope_completion` and pass them to your search call:

```python title="scope_then_search.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

query = "rejected electronics patents filed after 2023"

# Step 1: Infer scope from the query (completion mode)
scope_response = requests.post(
    "https://api.lighton.ai/api/v3/content-types/scope",
    headers=headers,
    json={
        "query": query,
        "model": "mistral-large-latest",  # replace with your model
    },
)

# Step 2: Build a scoped search from the inferred filters
scope = scope_response.json().get("scope_completion", {})

search_body = {"query": query}
if scope.get("content_type"):
    search_body["content_type"] = [scope["content_type"]]
if scope.get("attribute"):
    search_body["attribute"] = scope["attribute"]

# Step 3: Search within the inferred scope
response = requests.post(
    "https://api.lighton.ai/api/v3/search",
    headers=headers,
    json=search_body,
)
print(response.json())
```

`scope_completion.content_type` and `scope_completion.attribute` map directly to the search API parameters — no transformation needed. If the LLM call fails, `scope_completion` is still returned with a `warnings` array explaining the failure, and the rest of the response (scores, groups, attributes) remains usable.

For the full attribute filter syntax (operators, OR logic, date shortcuts), see [Filtering documents by metadata](/tutorials/facets/filter).

## Prompt mode: bring your own LLM

Completion mode handles everything in one call, but it uses the auto-generated prompt as-is. If you need more control, use **prompt mode**: omit the `model` parameter and the API returns `prompt_context` instead of calling an LLM.

<Note>
  Without `model`, the scope endpoint does **not** return `scope_completion`. You get `prompt_context`, `groups`, and scores, but you need your own LLM call to produce the `content_type` and `attribute` filters.
</Note>

Why choose prompt mode:

* **Add domain-specific rules** that the auto-generated prompt doesn't know about (e.g. *"'Project Aurora' is our internal name for renewable energy patents"*)
* **Add few-shot examples** that teach the LLM patterns specific to your data
* **Post-process the LLM output** with validation or normalization before searching
* **Use a model not available** through the completion mode

### Step 1: Get the prompt context

````python title="scope_prompt.py" theme={null}
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['LIGHTON_API_KEY']}"}

# Get prompt_context: an LLM-ready text block with ranked content types,
# attribute definitions, filter syntax, and inference rules.
# Feed this to your own LLM alongside the user query.
response = requests.post(
    "https://api.lighton.ai/api/v3/content-types/scope",
    headers=headers,
    json={
        "query": "rejected electronics patents filed after 2023",
    },
)
print(response.json())
```python
prompt_context = response.json()["prompt_context"]
user_query = "rejected electronics patents filed after 2023"

# Add business rules only someone familiar with this corpus would know
domain_rules = """
ADDITIONAL RULES:
- "Project Aurora" is our internal name for the renewable energy initiative: always map it to patent:electricity:h02
- Our fiscal year starts in April: "this year" means >= 2025-04-01, "last year" means >= 2024-04-01 AND <= 2025-03-31
- When users say "pending", they mean decision_status:Accepted (our process labels accepted-but-not-granted as "pending")
"""

# Few-shot examples that correct patterns your LLM gets wrong
examples = """
Examples:
  Query: "Project Aurora patents from last year"
  Answer: {"content_type": "patent:electricity:h02", "attribute": ["filing_date:>=2024-04-01", "filing_date:<=2025-03-31"]}

  Query: "pending applications from the energy team"
  Answer: {"content_type": "patent:electricity", "attribute": ["decision_status:Accepted"]}
"""

prompt = f"""{prompt_context}

{domain_rules}

{examples}

Query: "{user_query}"

Respond with ONLY JSON:
{{"content_type": "path or null", "attribute": ["filter1", "filter2"] or []}}"""
````

### Step 2: Call your LLM and search

Send this prompt to any LLM (temperature 0, max 200 tokens). Parse the JSON response and pass `content_type` and `attribute` to your search call, just like with completion mode:

```python theme={null}
import json

raw = your_llm_client.chat(prompt, temperature=0, max_tokens=200)
scope = json.loads(raw)
# scope = {"content_type": "patent:electricity", "attribute": ["decision_status:REJECTED"]}

search_body = {"query": user_query}
if scope.get("content_type"):
    search_body["content_type"] = [scope["content_type"]]
if scope.get("attribute"):
    search_body["attribute"] = scope["attribute"]

results = requests.post(
    "https://api.lighton.ai/api/v3/search",
    headers=headers,
    json=search_body,
)
```

<Accordion title="What's inside prompt_context">
  Here's a representative example of what `prompt_context` looks like for a patent corpus:

  ```
  Content types (by relevance):
    1. Electricity (patent:electricity) — score: 1.85, 15 chunks *
    2. Basic Electric Elements (patent:electricity:h01) — score: 0.91, 3 chunks

  Relevant filters:
    - filing_date "Filing Date" (date: >=, <=)
      ↳ query match: temporal

  Other available attributes:
    - decision_status "Decision Status" (select: Accepted, Rejected)
    - abstract "Abstract" (rich-text) — only filter when the query
      explicitly names this field; use *value* wildcard

  RULES:
    1. Use null content_type when the query targets attributes
       (dates, names, codes) without naming a topic area from the tree.
    2. CT DEPTH: prefer section-level paths (e.g., patent:electricity),
       NOT subclass-level.
    3. For date attributes: only filter if the query mentions a specific
       date, period, or 'recent'.
    4. DATE RANGES: for periods, use two attribute entries: one with >=
       for the start and one with <= for the end.
    5. FIELD NAMES: use the exact field name in filters, not the
       human-readable label.
    6. MINIMALITY: only add filters directly stated or clearly implied.

  DATE GUIDE:
    Quarter: Q1=Jan 1-Mar 31, Q2=Apr 1-Jun 30, Q3=Jul 1-Sep 30,
             Q4=Oct 1-Dec 31

  EXAMPLES:
    Query: "recent Electricity"
    Answer: {"content_type": "patent:electricity",
             "attribute": ["filing_date:>=2024-01-01"]}

    Query: "Electricity from Q1 2023"
    Answer: {"content_type": "patent:electricity",
             "attribute": ["filing_date:>=2023-01-01",
                           "filing_date:<=2023-03-31"]}

    Query: "Accepted documents"
    Answer: {"content_type": null,
             "attribute": ["decision_status:Accepted"]}

  OUTPUT FORMAT:
    {"content_type": "<path>" or null,
     "attribute": ["field_name:value", ...] or []}
    - content_type: best-matching CT path from the tree above, or null
    - attribute: filters using exact field names and syntax from above
  ```

  The sections are generated dynamically from your schema:

  * **Content types** — ranked by query relevance. The `*` marks the top match.
  * **Relevant filters** — attributes the API detected as related to the query (e.g., date keywords triggered `filing_date`). Shows filter syntax per type.
  * **Other available attributes** — remaining attributes you can filter on.
  * **Rules** — inference guidelines for the LLM. Adapted to your schema (e.g., date rules only appear if you have date attributes).
  * **Date guide** — period interpretation. Helps the LLM translate "Q1" into concrete dates.
  * **Examples** — few-shot demonstrations generated from your actual content types and attributes.
  * **Output format** — the JSON schema the LLM should respond with.
</Accordion>

## Schema context: pre-load the catalog

Omit `query` to get the full content type catalog — useful for system prompts, tool descriptions, or schema exploration. If you have few content types, this avoids scoring latency.

```python theme={null}
response = requests.post(
    "https://api.lighton.ai/api/v3/content-types/scope",
    headers=headers,
    json={},
)
data = response.json()
# data["groups"] — all CTs with attributes (score=0, chunk_count=0)
# data["prompt_context"] — same catalog as LLM-ready text
```

`groups` contains all your content types with their attributes (`score=0.0` and `chunk_count=0` since there's no query to rank against). `doc_count` reflects corpus size. `prompt_context` contains the same catalog formatted as text for LLM consumption.

## Understanding the response

### Content types and scoring

The `groups` array contains content types grouped by root schema (e.g., "Patent Classification" and "SIC Industry" are separate roots). Each group represents an independent taxonomy.

* **`score`** — relevance to the query. Higher = stronger match. Comparable across requests.
* **`chunk_count`** — how many retrieval chunks matched this content type for this specific query. Measures evidence strength.
* **`doc_count`** — total documents classified under this content type in your corpus. Measures coverage.
* **`max_score`** — highest score in a root group. Compare roots at a glance without iterating content types.

### Attribute definitions

Each content type includes its attribute definitions in `attributes`. Use these to build dynamic filter UIs, validate user input, or understand what `scope_completion.attribute` refers to.

```json theme={null}
{
  "name": "decision_status",
  "label": "Decision Status",
  "type": "select",
  "required": false,
  "description": "Patent application decision",
  "choices": ["Accepted", "Rejected"]
}
```

Attribute types: `date`, `select`, `multi-select`, `text`, `number`, `boolean`, `rich-text`. For `select` and `multi-select`, `choices` lists the valid values. You never need to parse `prompt_context` to know what attributes exist — they're here as structured data.

### Prompt version (evaluation)

`prompt_version` is a fingerprint in the format `t:<hex>.d:<hex>`:

* `t:` hashes the prompt template (rules, patterns, syntax). Changes when the platform updates its inference logic.
* `d:` hashes the per-request data (scored paths, attribute definitions). Changes when your corpus or schema changes.

Same `prompt_version` = same prompt was generated. Useful for A/B testing LLMs on the same prompt, or detecting when a platform update changed the prompt your users receive.

## Scoring and confidence

Scores are the primary signal for deciding whether to scope your search. The top content type's `score` tells you how relevant it is to the query.

`has_signal` is a convenience shortcut — it's `true` when the top score meets a default confidence threshold. For custom logic, compare `groups[].content_types[].score` against your own threshold, or pass the `threshold` request parameter to adjust the cutoff.

When the top score is low, groups and attributes are still returned — you can present them to users as suggestions or apply them conditionally based on your own rules.

## When Agentic adds the most value

| Query type          | Example                            | Without Agentic                                     | With Agentic                            |
| ------------------- | ---------------------------------- | --------------------------------------------------- | --------------------------------------- |
| Structured metadata | "examiner Chaki", "filed Jan 2023" | Semantic search cannot handle dates or person names | Filters by metadata: **critical value** |
| Mixed               | "rejected electronics patents"     | Partial results                                     | CT + attribute filters: **high value**  |
| Pure topic          | "energy conversion methods"        | Works well                                          | CT narrows domain: moderate value       |

Agentic delivers the most value on queries that combine free text with structured constraints: dates, statuses, person names, classification codes. These represent roughly 65% of real-world queries and are exactly where semantic search alone falls short.
