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 at 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://api.example.internal/v1",  # else NAGET_BASE_URL, else generated default
    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 → release-generated default → ConfigurationError
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 release-generated default is read from the packaged naget/public-defaults.json in an installed wheel, or from sdk/public-defaults.json / deployment/local/generated/public-runtime.json when running from a source tree. No production hostname is hardcoded in source, so a bare source checkout with no environment configured fails closed.

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 the built-in web agent 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)

mode="search" is the one-step path over the live web or the agent's search tools, and mode="agentic" (its default documents output format) is the multi-step one, researching across many queries. Both return the same shape: one LLM call filters and ranks the results by relevance and writes nothing about them, so each row is title, url, and snippet — the search engine's own description in search, and the head of the context-filtered content in documents, whose researched pages carry no engine text.

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)

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. 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)

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.

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
initiate_uploads(search_tool_id, *, files) presigned SearchToolUploadInitiateResponse
complete_upload(search_tool_id, upload_id) sealed SearchToolUploadCompletion
create_bulk_ingestion(search_tool_id, *, uploads, expected_schema_digest, schema_update=None, on_conflict="reject", idempotency_key=None) durable Operation (or a schema proposal response)
inspect_bulk_schema(search_tool_id, *, uploads, expected_schema_digest, idempotency_key=None, timeout=3600, poll_interval=1) full-file SearchToolBulkSchemaProposal
confirm_bulk_schema(proposal, *, uploads, selected_fields, on_conflict="reject", idempotency_key=None) durable Operation
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) 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, cursor=None) typed rows from one canonical QueryPlan
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.

Large canonical JSONL imports use resumable, object-backed ingestion. Initiate bounded files, PUT each file to its returned URL, and seal each upload with complete_upload. inspect_bulk_schema scans every sealed byte through bounded range reads and returns signed field suggestions; it does not publish documents. Pass the same upload references and selected field names to confirm_bulk_schema. The confirmation is one idempotent durable operation, and publishes selected schema additions and documents atomically. An empty selection keeps newly observed attributes raw-only. Poll the returned operation with client.operations.wait; cancellation uses the ordinary operation API.

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.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) Operation | SchemaProposal
replace(search_tool_id, *, documents, schema_update=None, idempotency_key=None) Operation | SchemaProposal
confirm_schema(proposal, *, documents, selected_fields, on_conflict="reject", idempotency_key=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) 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), add(agent_id, *, search_tool_id), 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).

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)
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). 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.

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, agent_id=None, options=None, idempotency_key=None) Search
get(search_id) Search
wait(search, *, timeout=600, poll_interval=1) Search
results(search_id, *, cursor=None, limit=20) Page[dict]
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"); every other mode runs through a saved agent. options carries the bounded tuning fields defined in the OpenAPI SearchOptions schema (search_depth, answer_profile, time_filter, web_index, 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. "categories" and "provenance" are retired: still accepted so older callers are not rejected, but they add nothing.

client.operations

list, iter, get(operation_id), and wait(operation, *, timeout=300, poll_interval=1).

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.

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.

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.

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, and gives up after max_reconnects (default 3) reconnects without a terminal event. 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:

  • search_tools.create, search_tools.delete
  • search_tools.documents.add / replace / remove
  • search.create

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. Methods without a key — including agents.create 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, 403
NotFoundError 404
ConflictError 409
ExpiredOperationError 410; subclass of ConflictError
ValidationError 400, 422
RateLimitError 429
APIConnectionError transport failure after retries
APITimeoutError request timeout; subclass of APIConnectionError
OperationFailedError a remote operation or search reached failed

APIError carries status_code, type, code, param, request_id, and the parsed body. OperationFailedError also carries .operation, the terminal model. Quote request_id when reporting a failure:

from naget import APIError, Naget

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

Models

Responses are parsed into pydantic models — SearchTool, Document, Agent, Operation, TypedQuery, Search, Secret, DailyUsage, WebIndexTaxonomy (with WebIndexTier and WebIndexCategory), Event, and Page[T]. 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.

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
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|get|update|delete|validate|publish|install|uninstall|query|import
naget search-tools documents list|get|add|replace|remove
naget search-tools credential-bindings list|set|remove
naget operations list|get|wait
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 release-generated default when one is available.

naget context create staging --base-url https://staging.example.internal
naget context use staging
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, which is useful for verifying a release-generated default.

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 --agent agt_123 --input "Question" --watch
naget search watch SEARCH_ID --last-event-id EVENT_ID

# Agents scoped to the Web Index
naget web-index taxonomy                      # tiers and categories 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 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

# 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

# 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

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.

--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

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 or authorization failure
4 conflict, including an expired operation
5 remote operation failure or another API error
6 connection failure or timeout

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. Search options accept max_credits, max_response_tokens, and max_columns. Document mutations and native query execution accept max_credits; retain idempotency_key when retrying a logical operation. A cap must cover the entire bounded quote. Signed-in accounts receive 2,000 free credits per UTC day, spent before any granted credits; 402 insufficient_credits reports the required and available amounts when neither covers a quote, and 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.0

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.0
File Size Uploaded Provenance
naget-0.3.0.tar.gz 94.3 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 163.9 kB

Release files / naget-0.3.0.tar.gz

Download URL naget-0.3.0.tar.gz
Size 94.3 kB
Tags Source
SHA-256 checksum
How to use checksums
ae87728e2a538ba3cf7d1e54f682a6026bb82a45f983a1ca1a00926ae41d5608
BLAKE2b-256 checksum
How to use checksums
eabd3f3eb67251564e7eb3cdba51024c9701e420915282a49b9eb09d504c1d29
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.0-py3-none-any.whl

Download URL naget-0.3.0-py3-none-any.whl
Size 69.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5a1616a7ff516326d278628e5413b69d37d72fbb1d89caa9f7ca723788de3445
BLAKE2b-256 checksum
How to use checksums
f37165b1ec8a5794127d992ee52859a00d5ecfa96aeb74ccf38f87d9265a5a23
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.0

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