Skip to main content

Naget Python SDK

The naget package provides a synchronous client (Naget), an asynchronous client (AsyncNaget), the generated wire models, and the naget command-line interface for the Naget /v1 API. It targets Python 3.11 or newer.

Routes, search modes, and worked examples in every language are in the Naget API docs. This page is the Python-specific reference; the TypeScript SDK behaves the same way.

Contents

Install

pip install naget            # client and CLI
pipx install naget           # or: only the `naget` CLI, in its own environment
pip install 'naget[keyring]' # store CLI API keys in the OS keyring

Create an API key under API Keys in the developer section of naget.com and export it as NAGET_API_KEY.

Optional extras:

Extra Adds Use for
keyring keyring storing CLI API keys in the OS keyring
test pytest, pytest-asyncio running sdk/python/tests
codegen pinned datamodel-code-generator regenerating src/naget/generated/

Runtime dependencies are httpx, pydantic v2, typer, and rich.

Configuration

from naget import Naget

client = Naget(
    api_key="sk-...",              # else NAGET_API_KEY
    base_url="https://naget.com/v1",  # else NAGET_BASE_URL, else https://naget.com/v1
    timeout=30,                    # seconds; else NAGET_TIMEOUT, else 30
    max_retries=2,                 # retry budget for retryable requests
    http_client=None,              # inject a preconfigured httpx.Client
)

Resolution order, highest priority first:

Setting Order
API key api_key argument → NAGET_API_KEY → ConfigurationError
Base URL base_url argument → NAGET_BASE_URL → https://naget.com/v1
Timeout timeout argument → NAGET_TIMEOUT → 30

The base URL must be an absolute HTTP(S) URL with no credentials, query, or fragment; a trailing slash is stripped. The installed package defaults to https://naget.com/v1, read from the packaged naget/public-defaults.json; a source checkout has no default and raises ConfigurationError until one is configured.

Both clients are context managers, and both own an httpx client unless you pass your own:

with Naget() as client:
    ...

async with AsyncNaget() as client:
    ...

client.base_url reports the resolved origin.

Quickstart

Run a single-step search and read its results:

from naget import Naget

with Naget() as client:
    run = client.search.create(input="Find current sources", mode="search")
    result = client.search.wait(run, timeout=600)
    for row in client.search.iter_results(result.id):
        print(row["title"], row["url"], row.get("date"))

mode="search" is the single-step path. It ranks without an LLM call: it runs one retrieval and reranks it with a cross-encoder (only options["parse_query"] and the filtered_snippet result field add a model call). Without an agent it searches the Web Index, so each row is title, url and snippet — the opening of the page's indexed text — plus the date its date filter reads and date_source (published, crawled or indexed). options={"web_index": {...}} narrows it by tier, category, primary source, site, date and language like any scope, and a time_filter dates it too. The top live web results join the index's before reranking when the scope keeps to no tier, category, primary source or language, and has no dates or a range that runs to today and starts at most a year back. Site and homepage filters keep the live web results but restrict them to those sites (and homepages). A live web row's snippet is the search engine's own description, and it has no date.

mode="agentic" (its default documents output format) is the multi-step path, researching across many queries. Its rows have the same shape: it filters and ranks results by relevance and writes nothing about them, so snippet is the head of the context-filtered content.

To run an agent, pass agent_id instead of mode: the agent's saved defaults pick the mode, and passing both raises ValueError (the API answers 400 conflicting_parameters). An agent saved for single-step search searches each of its attached sources once and merges the results, reranked together: the scope filters its Web Index (and which live web results may join), while its own search tools get every word of the input and no filters. A row from one of the agent's own search tools carries source (that search tool's name) and fields (the record's own fields, in order); its url is a dataset:// id unless the record names a web address. The default Web Agent (web) is what a search with mode and no agent runs, so pass mode rather than agent_id="web".

Add result_fields=["filtered_snippet"] for the full query-relevant extract the context-filtering model kept from each page:

with Naget() as client:
    run = client.search.create(
        input="Find current sources",
        mode="agentic",
        options={"result_fields": ["filtered_snippet"]},
    )
    for row in client.search.iter_results(client.search.wait(run).id):
        print(row["title"], row["url"], row["filtered_snippet"])

output_format="structured" plans the columns to research and fills them row by row. Rows are named entities, or the comparable units of a question's answer (findings, options, steps, aspects); the plan's row_grain says what one row represents. Row keys follow the planned columns, which the results page lists under columns; a question-shaped run also carries results["summary"], a short answer over the ranked rows that references them inline as [N] (skip it with options={"table_summary": False}):

with Naget() as client:
    run = client.search.create(
        input="AI search startups with their funding stage",
        mode="agentic",
        output_format="structured",
    )
    result = client.search.wait(run, timeout=600)
    print(result.results["columns"])  # planned from the question
    for row in client.search.iter_results(result.id):
        print(row)

tier= picks the search tier of a structured run: one of naget.SEARCH_TIERS — "light" (the default), "pro" or "max" — sent as options["tier"]. The tiers run the same research engine with progressively stronger models and more research; max also wants every row backed by two different pages. An unknown name raises ValueError before anything is sent; a tier your account may not run raises PermissionDeniedError (403 tier_not_available). Follow-ups keep the tier, and other modes ignore it:

from naget import Naget, PermissionDeniedError

with Naget() as client:
    try:
        run = client.search.create(
            input="Battery recycling plants in Europe with capacity",
            mode="agentic",
            output_format="structured",
            tier="pro",
        )
    except PermissionDeniedError as error:
        print(error.code)  # tier_not_available

target_results sizes the run, not the table. Only the top target_results plus a small overflow band are filtered, ranked and column-filled; rows beyond that are still published after them, unranked, marked _extra: True, with the cells extraction produced. Row keys starting with an underscore are metadata (_citations, _categories, _requirements_met, _extra), not columns:

rows = list(client.search.iter_results(result.id))
regular = [row for row in rows if not row.get("_extra")]
extra = [row for row in rows if row.get("_extra")]

Plans can be reviewed before execution: confirm_plan: False pauses the run after planning (a plan_review event carries the proposal), and search.confirm applies your edits and starts execution. A structured plan needs its columns and at least one of them in dedup_columns; other proposal fields (single_entity, row_grain, column_enrich, table_summary) pass through as keyword arguments. The proposal leaves out the agent's own search queries; omit queries to run them, or pass your own to replace them:

with Naget() as client:
    run = client.search.create(
        input="AI search startups with their funding stage",
        mode="agentic",
        output_format="structured",
        options={"confirm_plan": False},
    )
    plan = next(
        event.data for event in client.search.stream(run)
        if event.event == "plan_review")
    client.search.confirm(
        run,
        columns=plan["columns"] + ["Headquarters"],
        dedup_columns=plan["dedup_columns"],
    )
    result = client.search.wait(run, timeout=600)

A research run started with options={"interactive": True} may stop to ask a clarifying question (a research_question event). Answer it, or skip it, with search.input; Search.research summarizes the research's progress:

with Naget() as client:
    run = client.search.create(
        input="Compare grid-scale storage options for our site",
        mode="agentic",
        output_format="text",
        options={"interactive": True},
    )
    for event in client.search.stream(run):
        if event.event == "research_question":
            client.search.input(run, question_id=event.data["id"], answer="Northern Germany")
        elif event.event in {"complete", "error"}:
            break

output_format="text" writes a cited answer. The terminal search carries results["answer"] and results["sources"]; the /results page lists the sources with the answer text alongside:

with Naget() as client:
    run = client.search.create(
        input="What is agentic retrieval?",
        mode="agentic",
        output_format="text",
        options={"answer_profile": "concise_factual"},
    )
    result = client.search.wait(run)
    print(result.results["answer"])

When the question names one entity ("Anthropic"), the structured planner resolves to a single-entity profile: the search reports single_entity=True and returns exactly one row whose columns are the profile fields (the name, Type, Summary, Images, and type-appropriate attributes):

with Naget() as client:
    run = client.search.create(
        input="Anthropic", mode="agentic", output_format="structured")
    result = client.search.wait(run)
    assert result.single_entity
    profile = next(client.search.iter_results(result.id))
    print(profile["Name"], "-", profile["Summary"])
    for column in result.results["columns"]:
        print(column, ":", profile.get(column))

Build and query a document search tool — it is queryable as soon as the ingest operation succeeds, and its revision publishes automatically (visibility alone controls who can use it):

with Naget() as client:
    search_tool = client.search_tools.create(
        name="Product docs",
        kind="document",
        document={
            "schema": {
                "fields": [
                    {"name": "id", "type": "keyword"},
                    {"name": "title", "type": "text"},
                    {"name": "text", "type": "text"},
                    {"name": "published_year", "type": "int64"},
                ],
                "primary_key": "id",
            },
        },
        idempotency_key="create-product-docs-v1",
    )

    mutation = client.search_tools.documents.add(
        search_tool.id,
        documents=[{
            "id": "intro",
            "attributes": {"title": "Intro", "text": "..."},
        }],
        idempotency_key="add-intro-v1",
    )
    if mutation.kind == "schema_proposal":
        mutation = client.search_tools.documents.confirm_schema(
            mutation,
            documents=[{
                "id": "intro",
                "attributes": {"title": "Intro", "text": "..."},
            }],
            selected_fields=[
                field.name for field in mutation.new_fields if field.recommended
            ],
            idempotency_key="confirm-add-intro-v1",
        )
    operation = mutation
    client.operations.wait(operation)
    hits = client.search_tools.query(search_tool.id, plan={
        "version": 2,
        "root": {
            "type": "search",
            "source": search_tool.id,
            "query": "introduction",
            "method": "hybrid",
            "fields": [{"field": "title", "boost": 2}, "text"],
            "filter": {
                "op": "gte", "field": "published_year", "value": 2024,
            },
            "candidates": 100,
            "limit": 5,
        },
    })

A mutation using only declared fields returns an Operation after durable admission, not after the build completes. Undeclared fields return a SchemaProposal instead; review its locked existing fields and suggested new fields, then resubmit the same documents with confirm_schema. Always wait on the accepted operation before querying. An accepted add, replace or remove operation can also carry revision and revision_token; with the token, wait_for_revision holds one request (up to 30 s) until queries see that revision:

with Naget() as client:
    visibility = client.search_tools.wait_for_revision(
        search_tool.id, operation.revision_token, wait_ms=10_000)
    print(visibility.status)  # visible, pending (ask again) or failed

SearchTool.building is true while files are still being read and made searchable; the lifecycle can already be ready meanwhile, because queries keep serving the current generation. get() also reports maintenance while a compaction or schema rebuild runs: operations submitted meanwhile are accepted and delayed behind it.

Upload files of any format and ingest them as one durable operation:

with Naget() as client:
    operation = client.search_tools.ingest_files(
        search_tool.id,
        ["catalog.csv", "prices.parquet"],
        description="Product catalog with list prices",
        max_credits="50",
    )
    print(operation.status)  # succeeded

Query a connector by running it once with an input:

with Naget() as client:
    rows = client.search_tools.query("ds_456", input="solar inverters", limit=5)
    for row in rows["data"]:
        print(row)

Find search tools semantically — q ranks everything you can see (owned, installed, public) by name and description and returns one ranked page:

with Naget() as client:
    page = client.search_tools.list(q="customer support knowledge", limit=10)
    for search_tool in page.data:
        print(search_tool.id, search_tool.name)

The asynchronous client mirrors the same surface; await each call and use async for where the synchronous client yields an iterator:

from naget import AsyncNaget

async with AsyncNaget() as client:
    run = await client.search.create(input="Find current sources", mode="search")
    result = await client.search.wait(run)
    async for row in client.search.iter_results(result.id):
        print(row)

Resource reference

Every method below exists on both Naget and AsyncNaget with the same name and arguments. Keyword-only arguments are shown with *.

client.search_tools

Method Returns
list(*, cursor=None, limit=20, kind=None, q=None) Page[SearchTool] (q = semantic search, single ranked page)
iter(*, limit=20, kind=None) iterator of SearchTool
create(*, name, kind, document=None, connector=None, visibility="private", description="", idempotency_key=None) SearchTool
draft_connector(description, *, example_queries=None) draft payload (connector + summary, or clarifying_question)
draft_document_schema(documents, *, description=None) suggested document configuration, plus per-field keep/searchable decisions
get(search_tool_id) SearchTool (with building, and maintenance while a rebuild runs)
upload_files(search_tool_id, paths, *, canonical=False, concurrency=4, on_progress=None) list[SearchToolBulkUploadReference]
ingest_files(search_tool_id, paths, *, description=None, canonical=False, on_conflict="reject", max_credits=None, wait=True, timeout=14400, poll_interval=2, concurrency=4, on_progress=None, idempotency_key=None) Operation (terminal with wait)
initiate_uploads(search_tool_id, *, files) presigned SearchToolUploadInitiateResponse
complete_upload(search_tool_id, upload_id) sealed SearchToolUploadCompletion
inspect_bulk_schema(search_tool_id, *, uploads, expected_schema_digest, input_format="canonical-jsonl", max_credits=None, idempotency_key=None, timeout=3600, poll_interval=1) full-file SearchToolBulkSchemaProposal
confirm_bulk_schema(proposal, *, uploads, selected_fields, on_conflict="reject", input_format="canonical-jsonl", max_credits=None, idempotency_key=None) durable Operation
create_bulk_ingestion(search_tool_id, *, uploads, expected_schema_digest, schema_update=None, on_conflict="reject", idempotency_key=None, input_format="canonical-jsonl", max_credits=None) inspect Operation without schema_update, durable Operation with one
update(search_tool_id, **payload) SearchTool
inspect_schema_candidates(search_tool_id, *, documents, cursor=None, limit=100) SchemaCandidatePage
list_schema_candidates(search_tool_id, *, statuses=None, cursor=None, limit=100) PersistedSchemaCandidatePage
evolve_schema(search_tool_id, *, expected_schema_digest, new_fields=None, new_compound_indexes=None, new_relationships=None, idempotency_key=None, max_credits=None) Operation | SchemaEvolutionUnchanged
delete(search_tool_id, *, idempotency_key=None) Operation or None on 204
validate(search_tool_id) validation payload
publish(search_tool_id) SearchTool
install(search_tool_id) / uninstall(search_tool_id) SearchTool / None
query(search_tool_id, *, plan=None, input=None, limit=None, retrieval_mode=None, cursor=None, max_credits=None, idempotency_key=None) typed rows from one QueryPlan, or a connector's {"data", "usage"}
wait_for_revision(search_tool_id, revision_token, *, wait_ms=None) RevisionVisibility
ground_values(search_tool_id, *, field, query, limit=20) snapshot-bound exact-value candidates

create validates locally before sending: kind must be document or connector, and exactly the matching configuration argument must be supplied (kind="document" requires document=...).

draft_document_schema reads a handful of representative documents and names the fields worth keeping, the ones worth searching, and the field that identifies a document — returned as the document configuration create takes. Describing the data is optional context, not a requirement, and the suggestion is advisory: edit it before creating.

inspect_schema_candidates infers bounded typed declarations from caller samples without changing the active schema. list_schema_candidates pages the authoritative observations persisted during mutation admission with a protected cursor and optional status filters. Pass selected full declarations to evolve_schema. Empty changes are rejected locally; a non-empty field, compound-index, or relationship declaration that is already active may return the explicit unchanged result. max_credits is sent only when given.

query takes exactly one of plan or input, and raises ValueError otherwise. plan runs one canonical QueryPlan against a document search tool and pages with its protected cursor. input runs a published connector once and returns {"data": [...], "usage": {...}}; limit caps its results (default 10, at most 50). retrieval_mode is accepted for compatibility and ignored: a connector chooses its own retrieval. A connector query is not replayed by key, so it sends no Idempotency-Key, is never retried, and rejects cursor and idempotency_key.

wait_for_revision takes the revision_token of an accepted add, replace or remove operation. With wait_ms (0 to 30000) the server holds the request until queries see the revision, the mutation fails, or the time is up; the returned status is visible, failed (with error) or still pending. The SDK lengthens that request's read timeout to outlast the hold.

Uploading and ingesting files

ingest_files uploads local files of any size and format and ingests them as one durable operation. Raw files (CSV, JSON, JSONL, Parquet, XLSX, ...) are read by the search tool's parser, generated from samples of every uploaded file when the search tool has none yet; with canonical=True the files are canonical {id, attributes} JSONL and no parser runs. It then:

  1. inspects every uploaded file (inspect_bulk_schema) for a signed schema proposal — the server answers with an already-succeeded inspect operation whose result is the proposal;
  2. confirms the ingestion (confirm_bulk_schema) with the newly observed fields the proposal recommends, under a new idempotency key, passing on_conflict and max_credits;
  3. waits for the operation with wait (the default), raising OperationFailedError if it fails.

A stable idempotency_key keys the inspection, and the confirmation's key is derived from it, so re-running the call with the same key replays both requests instead of ingesting twice.

The steps are also available one by one. upload_files streams each file from disk to a presigned URL (up to concurrency at once, calling on_progress(filename, sent_bytes, total_bytes)) and seals it, returning the upload references; initiate_uploads and complete_upload are the two calls it makes per batch. inspect_bulk_schema scans every sealed byte through bounded range reads and returns signed field suggestions; it publishes nothing. Pass the same upload references, the same input_format ("parsed" for raw files, "canonical-jsonl" otherwise) and the selected field names to confirm_bulk_schema. The confirmation is one idempotent durable operation and publishes the selected schema additions and documents atomically; an empty selection keeps newly observed attributes raw-only. Poll the returned operation with client.operations.wait, and cancel it with client.operations.cancel.

with Naget() as client:
    uploads = client.search_tools.upload_files(search_tool.id, ["papers.jsonl"], canonical=True)
    proposal = client.search_tools.inspect_bulk_schema(
        search_tool.id,
        uploads=uploads,
        expected_schema_digest=search_tool.configuration["schema"]["digest"],
    )
    operation = client.search_tools.confirm_bulk_schema(
        proposal,
        uploads=uploads,
        selected_fields=[field.name for field in proposal.new_fields if field.recommended],
        max_credits="20",
    )
    client.operations.wait(operation, timeout=3600)

Document search tools are manifest only. Their schema declares typed fields and optional capabilities, compound indexes, and relationships. Text fields default to stored BM25 and semantic indexes; numeric/date/timestamp fields default to stored exact and range indexes; primary keys always gain an exact index. A plan can select fields, combine structured predicates with BM25, semantic, or hybrid retrieval, and page with its protected cursor. Plan keys use the canonical snake_case wire spelling.

draft_connector writes a connector from a description of the API — the code, the hosts it may reach, the secret slots it reads, and the input/output schemas — and returns it in the shape create(kind="connector", connector=...) takes. Nothing is persisted and the generated source is not executed until the connector runs in the sandbox, so review it first; when the description leaves out something required (the endpoint, the authentication scheme) the reply is a clarifying_question instead of a draft.

client.search_tools.parser

The parse(file_path) function the parsed input format runs. Callers read it but never set it: generate and refine are the only ways it changes.

Method Returns
get(search_tool_id) SearchToolParser (parse_code); NotFoundError before one exists
generate(search_tool_id, *, description=None) SearchToolParser written from samples of every uploaded file
refine(search_tool_id, feedback, *, preview=None) SearchToolParser corrected from your feedback (and a preview's output)
preview(search_tool_id, *, limit=20) SearchToolParserPreview: records, fields and errors per file
with Naget() as client:
    preview = client.search_tools.parser.preview(search_tool.id, limit=10)
    if not preview.success:
        client.search_tools.parser.refine(
            search_tool.id, "Dates are day-first; keep prices as numbers", preview=preview)

client.search_tools.documents

Method Returns
list(search_tool_id, *, cursor=None, limit=20) Page[Document]
iter(search_tool_id, *, limit=20) iterator of Document
get(search_tool_id, document_id) Document
add(search_tool_id, *, documents, on_conflict="reject", schema_update=None, idempotency_key=None, max_credits=None) Operation | SchemaProposal
replace(search_tool_id, *, documents, schema_update=None, idempotency_key=None, max_credits=None) Operation | SchemaProposal
confirm_schema(proposal, *, documents, selected_fields, on_conflict="reject", idempotency_key=None, max_credits=None) Operation
remove(search_tool_id, *, document_ids, idempotency_key=None) Operation | SchemaProposal

replace is add with on_conflict="replace". All mutations post to /search-tools/{id}/mutations. schema_update also accepts the explicit {"expected_schema_digest": ..., "new_fields": [...]} branch; passing an empty new_fields or selecting no proposed fields explicitly keeps those attributes raw-only.

client.search_tools.credential_bindings

list(search_tool_id), set(search_tool_id, *, slot, secret_id, header), delete(search_tool_id, slot).

client.queries

Method Returns
execute(*, search_tools, plan=None, sql=None, parameters=None, consistency=None, execution="auto", cursor=None, max_credits=None, idempotency_key=None) typed result or TypedQuery
get(query_id) TypedQuery status
results(query_id, *, cursor=None) snapshot-bound typed result page
cancel(query_id) TypedQuery status
wait(query, *, timeout=300, poll_interval=1) terminal TypedQuery
iter_rows(query_id) iterator over durable result rows

execute accepts exactly one of a canonical QueryPlan or read-only SQL. Each source alias is bound explicitly to an authorized search tool and optional revision. Use execution="async" for durable analytical work:

query = client.queries.execute(
    search_tools=[{"source": "papers", "search_tool_id": search_tool.id}],
    sql="SELECT title FROM papers WHERE published_year >= $year LIMIT 10",
    parameters={"year": {"type": "int64", "value": 2024}},
    execution="async",
)
client.queries.wait(query)
for row in client.queries.iter_rows(query.id):
    print(row["values"])

client.agents

Method Returns
list(*, cursor=None, limit=20) / iter(*, limit=20) Page[Agent] / iterator
list_builtin() list[Agent]
create(**payload) Agent
get(agent_id) / update(agent_id, **payload) / delete(agent_id) Agent / Agent / None
publish(agent_id) / install(agent_id) / uninstall(agent_id) Agent / Agent / None

client.agents.sources provides list(agent_id) (a list of AgentSource), add(agent_id, *, search_tool_id) (the added AgentSource), and remove(agent_id, source_id). search_tool_id may also be a built-in search tool: "web" (live web) or "web_index" (the Web Index). An AgentSource reports building while a document source's files are still being made searchable, and for a connector its required_secret_slots, the missing_secret_slots you have not bound yet, needs_credentials and credential_status. Agent.sources holds the same models; item access (source["id"]) still works.

list_builtin() returns the agents Naget provides. Their ids are plain slugs such as "patents", which no saved agent can take, and Agent.builtin is true; "web" is the default Web Agent. Pass a built-in id as agent_id to client.search.create without installing it; get() accepts built-in ids too. Agent.web_index returns an agent's default Web Index scope as a WebIndexScope, or None.

Scoping an agent to the Web Index

An agent searches the Web Index when the web_index search tool is attached, within the scope in configuration["defaults"]["web_index"]. The scope keeps documents whose tier is in tiers and whose category is in categories (an empty list keeps all), optionally only primary sources. Pass a dict or a naget.WebIndexScope; only the fields you set are sent.

from naget import Naget, WebIndexScope

with Naget() as client:
    taxonomy = client.web_index.taxonomy()
    for category in taxonomy.categories:
        print(category.id, category.group, category.tiers, category.description)

    agent = client.agents.create(
        name="Clinical evidence",
        configuration={
            "instructions": "Answer from peer-reviewed and official sources.",
            "defaults": {"web_index": WebIndexScope(
                tiers=["tier1", "tier2"], categories=["pubmed"], primary_sources_only=True,
            )},
        },
    )
    client.agents.sources.add(agent.id, search_tool_id="web_index")

    # Override the scope for one run with options["web_index"].
    run = client.search.create(
        input="Recent trials of GLP-1 drugs for heart failure",
        agent_id=agent.id,
        options={"web_index": {"categories": ["pubmed", "clinical-trials"]}},
    )

    # Built-in agents need no setup.
    for builtin in client.agents.list_builtin():
        print(builtin.id, builtin.name, builtin.web_index)

The six tiers are in naget.WEB_INDEX_TIERS; the category list follows the live index, so read it from client.web_index.taxonomy(). Unknown tiers or categories raise ValidationError with param set to configuration.defaults.web_index or options.web_index.

client.web_index

Method Returns
taxonomy() WebIndexTaxonomy (tiers, categories, primary_sources, languages)
search(query, *, k=10, web_index=None, include_domains=None, exclude_domains=None, start_date=None, end_date=None, homepages_only=None, languages=None, parse_query=False) WebIndexSearchResponse (query, scope, parsed, data, ...)
parse(query) WebIndexParseResponse (query, web_index, parsed)

A scope also keeps documents from given sites or away from them (include_domains, exclude_domains: a host matches its subdomains, gov every .gov host) and dated within a range (start_date, end_date, UTC days; a document's date is its published date, else its crawl day, else the day it was indexed, reported per result as date and date_source), and may keep only homepages (homepages_only: each site's root page) and documents written in given languages (languages: ISO 639-1 codes, reported per result as language; taxonomy().languages lists the major ones by name). The site, date, homepage and language arguments of search set those scope fields. parse(query) returns the search text and the sites, dates, homepages, languages, categories and tiers the query states, for review; search(..., parse_query=True) does both in one call, the fields you pass winning over parsed ones, and reports the parse as parsed. On client.search.create(mode="search", options={"parse_query": True}) the run reports it as Search.search_params and in a search_params event; an agent's single-step search parses only when every source it searches is the web.

with Naget() as client:
    found = client.web_index.parse("climate reports on nasa.gov from last month")
    print(found.query, found.web_index)
    result = client.web_index.search(found.query, web_index=found.web_index,
                                     exclude_domains=["blogs.nasa.gov"])
    for row in result.data:
        print(row.rank, row.url, row.date, row.date_source)

client.search

Method Returns
create(*, input, mode=None, output_format=None, agent_id=None, options=None, idempotency_key=None, tier=None) Search
get(search_id) Search
wait(search, *, timeout=600, poll_interval=1) Search
confirm(search, *, columns=None, queries=None, dedup_columns=None, **fields) acknowledgement ({"ok": True, ...})
input(search, *, question_id, answer=None, skip=False) acknowledgement ({"ok": True, ...})
results(search_id, *, cursor=None, limit=20) Page[dict] (up to 100 rows per page)
iter_results(search_id, *, limit=20) iterator of dict
stream(search, *, last_event_id=None, max_reconnects=3) iterator of Event

create requires exactly one of a built-in mode= or agent_id=... and raises ValueError otherwise. The callable modes are in naget.SEARCH_MODES ("search", "agentic") and the agentic output formats in naget.SEARCH_OUTPUT_FORMATS ("structured", "text", "documents"); output_format with any other mode raises ValueError. tier (one of naget.SEARCH_TIERS) is the same as options["tier"]; passing both with different values raises ValueError. Every other mode runs through a saved agent, whose defaults choose it. options carries the bounded tuning fields defined in the OpenAPI SearchOptions schema (search_depth, source_read_limit, target_results, search_breadth, answer_profile, time_filter, result_fields, confirm_plan, table_summary, interactive, web_index, parse_query, max_credits, max_response_tokens, max_columns, and so on); server caps apply regardless of what is sent. agent_id also takes a built-in agent id such as "academic".

For search and the documents format, options["result_fields"] selects which per-result enrichments to produce — any of naget.SEARCH_RESULT_FIELDS, which is ("filtered_snippet",). The default selection is empty, and a field that is not requested is absent from the result rows. naget.RankedSearchResult documents the shape, including the date, date_source, source and fields a single-step row can carry. "categories" and "provenance" are retired: still accepted so older callers are not rejected, but they add nothing.

client.operations

Method Returns
list(*, cursor=None, limit=20) / iter(*, limit=20) Page[Operation] / iterator
get(operation_id) Operation
cancel(operation_id) Operation
wait(operation, *, timeout=300, poll_interval=1) terminal Operation

cancel stops an operation that has not published yet, such as a schema evolution or a bulk ingestion; one that cannot be cancelled any more raises ConflictError (409 operation_not_cancellable).

client.secrets

list(), create(*, name, value), rotate(secret_id, *, value), delete(secret_id). Secret values are write-only and never returned.

client.usage

daily(*, from_date=None, to_date=None, cursor=None, limit=100) and iter_daily(...), returning DailyUsage records. from_date/to_date map to the from/to query parameters; arguments left as None are not sent.

client.account

Method Returns
get() credit balance and daily quotas
pricing() the current pricing policy
usage(*, limit=30, offset=0) charge receipts
quote(mode, *, settings=None) the maximum charge a request would be quoted

See Credits and daily quotas.

Pagination

List methods return a Page[T] with data, next_cursor, and has_more. The matching iter* helper follows cursors until exhaustion:

for search_tool in client.search_tools.iter(kind="document", limit=100):
    print(search_tool.id, search_tool.lifecycle)

Cursor values are opaque; do not construct or persist them as identifiers. Query parameters left as None (a first page's cursor, an open date range) are omitted from the request rather than sent empty.

Waiting on asynchronous work

operations.wait, queries.wait, and search.wait accept either an identifier or a returned model. When given a model they follow its returned status/result URL, which is why passing the object through is preferred over re-deriving a path.

Waiter Default timeout Poll interval Failure
operations.wait 300 s 1 s OperationFailedError
queries.wait 300 s 1 s OperationFailedError
search.wait 600 s 1 s OperationFailedError

All three raise the built-in TimeoutError when the deadline passes; the remote work keeps running, so a timeout is a reason to poll again, not to resubmit. A status URL pointing at a different origin is refused with ConfigurationError rather than followed with your credentials. search_tools.wait_for_revision is a single long-poll request instead: it returns pending when its wait_ms runs out, and you call it again.

Streaming search events

search.stream yields parsed SSE Event objects, tracks the last event ID, and reconnects with Last-Event-ID after an interrupted stream:

for event in client.search.stream(run):
    if event.event == "answer_chunk":
        print(event.data, end="", flush=True)
    elif event.event in {"complete", "error"}:
        break

The iterator returns on its own once a complete or error event has been seen. A stream the server closes early, one whose connection drops or times out, and a connect answered 429 or 5xx (such as 503 stream_capacity, retried after its Retry-After, otherwise with the usual backoff) all reconnect from the last event ID, up to max_reconnects (default 3) times. After that a clean close ends the iterator, a dropped connection raises APIConnectionError (APITimeoutError for a timeout), and an error status raises its APIError. Any other error status, such as 401, 403 or 404, raises at once. Event names and payloads are documented in the API docs.

Idempotency and retries

These methods send an Idempotency-Key, generating a UUID when you do not supply one, and are therefore retried:

  • search.create
  • search_tools.create, search_tools.delete
  • search_tools.documents.add / replace / remove / confirm_schema
  • search_tools.create_bulk_ingestion, inspect_bulk_schema, confirm_bulk_schema, and both requests ingest_files makes (the confirmation's key is derived from yours)
  • search_tools.evolve_schema
  • search_tools.query with plan= (a replay returns the original answer without a second charge)
  • queries.execute

Pass a stable idempotency_key when a retry must survive a process restart. Replaying a key with the same body returns the original resource; replaying it with different content is a conflict (ConflictError).

A request is retried only when it is a GET, HEAD, or OPTIONS, or when it carries an Idempotency-Key. Retries fire on connection errors, timeouts, and 429, 500, 502, 503, 504, up to max_retries additional attempts. Backoff honors Retry-After (clamped to 30 s) and otherwise grows exponentially from 0.25 s to a 4 s ceiling. search_tools.query with input= sends no key, because a connector runs once per request and the API does not replay it, so it is never retried. Other methods without a key — including agents.create, search.confirm, search.input, operations.cancel, web_index.search, the upload and parser calls, and every PATCH — are never replayed.

Errors

All exceptions are importable from the package root.

Exception Raised for
ConfigurationError unusable configuration; also a ValueError
APIError base class for HTTP failures
AuthenticationError 401: a missing, invalid or disabled API key
PermissionDeniedError 403, such as tier_not_available or agent_not_installed; subclass of AuthenticationError
InsufficientCreditsError 402 insufficient_credits; details carries required_credits and available_credits
NotFoundError 404
ConflictError 409
ExpiredOperationError 410; subclass of ConflictError
ValidationError 400, 422
RateLimitError 429
APIConnectionError transport failure after retries, or a search stream that kept dropping
APITimeoutError request timeout; subclass of APIConnectionError
OperationFailedError a remote operation or search reached failed

APIError carries status_code, type, code, param, request_id, details (the envelope's error.details, such as the required credits or the issues behind 409 agent_dependencies_unavailable; empty when there are none), and the parsed body. OperationFailedError also carries .operation, the terminal model. Because PermissionDeniedError subclasses AuthenticationError, an existing except AuthenticationError still catches 403. Quote request_id when reporting a failure:

from naget import APIError, InsufficientCreditsError, Naget

try:
    with Naget() as client:
        client.search_tools.get("ds_missing")
except InsufficientCreditsError as error:
    print("needs", error.details.get("required_credits"), "credits")
except APIError as error:
    print(error.status_code, error.code, error.request_id)

Models

Responses are parsed into pydantic models — SearchTool, Document, Agent (with AgentSource), Operation, RevisionVisibility, TypedQuery, Search, Secret, DailyUsage, WebIndexTaxonomy (with WebIndexTier, WebIndexCategory and WebIndexLanguage), Event, and Page[T]. RankedSearchResult documents a search or documents result row, and SearchParams what a parsed single-step search searched. WebIndexScope is also accepted in request payloads. They allow extra fields, so a server that adds a property does not break an older client; unknown values stay reachable through the model instance. Operation, TypedQuery, and Search expose a terminal property.

Fields worth knowing: SearchTool.building, SearchTool.maintenance and SearchTool.owner_id; Operation.revision and Operation.revision_token; Search.search_params and Search.research.

For exhaustive request/response schemas generated straight from the contract, use the wire models:

from naget import wire_models

Escape hatch

client.raw.request(method, path, **kwargs) returns an authenticated httpx.Response while keeping the SDK's URL resolution, retry policy, timeout, and error handling. Use it for a route the typed layer does not cover yet, not to bypass the transport rules.

CLI

The wheel installs a naget entry point.

naget context list|create|use|show|delete
naget defaults public-origin
naget search run|get|watch|results
naget agents list|builtin|create|get|update|delete|publish|install|uninstall
naget agents sources list|add|remove
naget web-index taxonomy|search|parse
naget search-tools list|create|draft-schema|draft-connector|get|update|delete|validate|publish|install|uninstall|query|ingest|import
naget search-tools parser get|generate|refine|preview
naget search-tools documents list|get|add|replace|remove
naget search-tools credential-bindings list|set|remove
naget operations list|get|wait|cancel
naget secrets list|create|rotate|delete
naget usage daily

naget COMMAND --help is the exact option reference.

Global options

Global options come before the command group:

Option Effect
--json stable machine-readable output instead of tables
--quiet, -q suppress the "No results." notice
--context NAME select a named context for this invocation
--base-url URL override the resolved base URL
--timeout SECONDS per-request timeout (default 30)
--api-key-env NAME read the key from a different environment variable
naget --json search-tools list --kind document

Contexts and credentials

Named contexts live in $XDG_CONFIG_HOME/naget/config.toml (by default ~/.config/naget/config.toml), written with 0600 permissions. On first use the file is seeded from the default, https://naget.com/v1.

naget context create naget --base-url https://naget.com/v1
naget context use naget
naget context show

Only the base URL is stored. API keys come from NAGET_API_KEY (or the variable named by --api-key-env), falling back to the OS keyring under service naget with the context name as the entry — install the keyring extra to use it. Keys are never written to the context file.

naget defaults public-origin prints the scheme and host of the resolved base URL (https://naget.com for the installed package).

Common flows

# Search
naget search run --mode search --input "Question" --wait
naget search run --mode agentic --output-format structured --input "Question" --wait
naget search run --mode agentic --output-format structured --tier pro --input "Question" --wait
naget search run --agent agt_123 --input "Question" --watch
naget search watch SEARCH_ID --last-event-id EVENT_ID
naget search results SEARCH_ID --limit 50
naget --json search results SEARCH_ID --all

# Agents scoped to the Web Index
naget web-index taxonomy                      # tiers, categories and languages to choose from
naget web-index search "solar inverter recalls" --domain cpsc.gov --from 2026-01-01
naget web-index search "solar panel makers" --homepages-only
naget web-index search "energiewende" --language de
naget web-index parse "climate reports on nasa.gov from last month"
naget web-index search "climate reports on nasa.gov from last month" --parse
naget agents builtin                          # agents Naget provides
naget search run --agent academic --input "Question" --wait
naget agents create --name "Clinical evidence" --instructions-file prompt.md \
  --tier tier1 --tier tier2 --category pubmed --primary-sources-only
naget agents create --name "Whole index" --instructions-file prompt.md --web-index

# SearchTool lifecycle
naget search-tools list --q "customer support knowledge"
naget search-tools create --kind document --name "Docs" \
  --primary-key id --searchable-field title --searchable-field text
naget search-tools documents add ds_123 --file docs.jsonl --on-conflict reject --wait
naget search-tools validate ds_123
naget search-tools publish ds_123
naget search-tools query ds_123 --plan query-plan.json

# Files of any format: parsed, inspected and ingested as one operation
naget search-tools ingest ds_123 catalog.csv prices.parquet \
  --description "Product catalog with list prices" --max-credits 50
naget search-tools parser preview ds_123 --limit 10
naget search-tools parser refine ds_123 "Dates are day-first"

# Supply the complete typed schema for numeric, geo, array, or compound indexes
naget search-tools create --kind document --name "Papers" --schema schema.json

# Connectors
naget secrets create --name company-token --from-stdin
naget search-tools create --kind connector --name "Company API" \
  --code connector.py --entrypoint search \
  --allow-host api.example.com --secret-slot api_token
naget search-tools query ds_456 --input "solar inverters" --limit 5

# Let the schema be suggested from a sample instead of naming fields by hand
naget search-tools draft-schema --sample records.jsonl
naget search-tools create --kind document --name "Docs" --sample records.jsonl

# ...or describe the API and let the connector be written for you
naget search-tools draft-connector "Search https://api.example.com/search?q=<q>, \
  bearer token in Authorization, results in data[] with title/url/snippet"
naget search-tools create --kind connector --name "Company API" \
  --describe "Search https://api.example.com/search?q=<q>, bearer token in \
  Authorization, results in data[] with title/url/snippet"
naget search-tools credential-bindings set ds_456 \
  --slot api_token --secret sec_123 --header X-API-Key

# Operations
naget operations wait OPERATION_ID --timeout 600
naget operations cancel OPERATION_ID

agents create stores --tier, --category and --primary-sources-only as the agent's defaults.web_index scope and then attaches the built-in web_index search tool, without which the scope has no effect. Any of these options implies --web-index; --web-index alone attaches the tool with no scope, so the agent searches the whole index. Add web with naget agents sources add AGENT_ID --search-tool web to search the live web too. (search run --tier is different: it picks the search tier of a structured run.)

search results prints one page (--limit, default 20, at most 100; --cursor for a later page); --all follows the cursors and prints every row. search-tools query takes --plan for a document search tool or --input (with --limit) for a connector.

--wait on a mutation blocks until the operation is terminal. Destructive commands prompt unless --yes is given. secrets create/rotate read the value from stdin with --from-stdin, otherwise prompt without echo.

Bulk document loading

search-tools ingest uploads files of any format and size (5 GiB each) straight to storage and ingests them as one durable operation, generating the parser first when the search tool has none; --canonical-jsonl skips the parser for {id, attributes} JSONL.

documents add and documents replace read a JSONL file in one request and refuse it above the direct-mutation bounds: 1,000 documents or 10 MiB of canonical JSON.

search-tools import streams a larger JSONL file into a sequence of bounded mutations that respect the same limits. Each batch derives its idempotency key deterministically from the search tool id, the batch number, and the canonical batch bytes, so re-running an interrupted import replays rather than duplicates. It is a compatibility helper, not a resumable bundle uploader.

naget search-tools import ds_123 corpus.jsonl --on-conflict reject --wait

Exit codes

Code Meaning
0 success
2 invalid input or a 400/422 response
3 authentication (401) or permission (403) failure
4 conflict, including an expired operation
5 remote operation failure or another API error
6 connection failure or timeout
7 insufficient credits (402)

Errors print to stderr with the request_id when the response carried one.

Development

From a checkout of the Naget repository:

python -m pip install -e './sdk/python[test,codegen]'
pytest sdk/python/tests -q

Regenerate the wire models after an OpenAPI change, then verify that the committed output matches a fresh generation:

sdk/python/scripts/generate_models.sh
sdk/check-generated.sh

src/naget/generated/ is machine-produced; edit openapi/naget-v1.yaml and regenerate instead of patching it. sdk/fixtures/wire-goldens.json pins the exact request bodies this client must emit and is shared with the TypeScript suite.

Generated versus source files

  • src/naget/ and the OpenAPI contract are source controlled.
  • sdk/public-defaults.json is rendered by deployment tooling and is not source.
  • src/naget/public-defaults.json, wheels, and dist/ are generated release artifacts and must not be committed.

Build a release wheel only after rendering deployment defaults:

sdk/python/scripts/build_release.sh

The script fails closed when sdk/public-defaults.json is absent or does not describe a safe HTTP(S) origin.

Credits and daily quotas

account = client.account.get()
pricing = client.account.pricing()
quote = client.account.quote("text", settings={"max_response_tokens": 2000})
receipts = client.account.usage(limit=30)

Money is returned as decimal strings; $1 equals 1,000 credits. Signed-in accounts receive 2,000 free credits per UTC day, spent before any granted credits. A cap must cover the entire bounded quote: a lower one is refused before any work starts (ValidationError, 400 credit_cap_too_low, with details["required_credits"]). max_credits (a decimal string) caps the charge of:

  • a search: options={"max_credits": ...} on search.create, alongside max_response_tokens and max_columns;
  • document mutations: documents.add, replace and confirm_schema;
  • file ingestion: ingest_files, confirm_bulk_schema and create_bulk_ingestion (inspect_bulk_schema accepts one too, though an inspection is not charged);
  • schema evolution: evolve_schema;
  • native queries: search_tools.query and queries.execute.

Retain idempotency_key when retrying a logical operation. When neither the free allowance nor your balance covers a quote, the call raises InsufficientCreditsError (402 insufficient_credits) with details["required_credits"] and details["available_credits"]; 429 includes Retry-After. client.account.pricing() returns the current rates.

Search budgets are managed internally from the planned answer size and evidence needs. Use search_depth and search_breadth to control candidates per query and parallel queries per step.

Metadata

Release files for naget 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for naget 0.3.2
File Size Uploaded Provenance
naget-0.3.2.tar.gz 121.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for naget 0.3.2
File Interpreter ABI Platform
naget-0.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 202.5 kB

Release files / naget-0.3.2.tar.gz

Download URL naget-0.3.2.tar.gz
Size 121.0 kB
Tags Source
SHA-256 checksum
How to use checksums
84126ea22f21934dadac420103f6242e4b17561910978bc07b6af07948ac02b1
BLAKE2b-256 checksum
How to use checksums
c45a2fbcdeeb9cb60a4f6e9a433fd2bc51244fa058ac69865dc38edccabdc514
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / naget-0.3.2-py3-none-any.whl

Download URL naget-0.3.2-py3-none-any.whl
Size 81.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d137848bb7de21ae44ed063f73035c68768e4dce7e92710280f472e8d88e4f0a
BLAKE2b-256 checksum
How to use checksums
3d7a2f95028612d4d491d05358f737aaa9bc34a03dfca410b5dc088227e0d0c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Security for naget 0.3.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page