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
- Configuration
- Quickstart
- Resource reference
- Pagination
- Waiting on asynchronous work
- Streaming search events
- Idempotency and retries
- Errors
- Models
- Escape hatch
- CLI
- Development
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:
- inspects every uploaded file (
inspect_bulk_schema) for a signed schema proposal — the server answers with an already-succeeded inspect operation whoseresultis the proposal; - confirms the ingestion (
confirm_bulk_schema) with the newly observed fields the proposal recommends, under a new idempotency key, passingon_conflictandmax_credits; - waits for the operation with
wait(the default), raisingOperationFailedErrorif 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 |
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.createsearch_tools.create,search_tools.deletesearch_tools.documents.add/replace/remove/confirm_schemasearch_tools.create_bulk_ingestion,inspect_bulk_schema,confirm_bulk_schema, and both requestsingest_filesmakes (the confirmation's key is derived from yours)search_tools.evolve_schemasearch_tools.querywithplan=(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.jsonis rendered by deployment tooling and is not source.src/naget/public-defaults.json, wheels, anddist/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.
Related documentation
- Naget API docs: routes, search modes, search tools, events
- TypeScript SDK (
@naget/sdk)
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": ...}onsearch.create, alongsidemax_response_tokensandmax_columns; - document mutations:
documents.add,replaceandconfirm_schema; - file ingestion:
ingest_files,confirm_bulk_schemaandcreate_bulk_ingestion(inspect_bulk_schemaaccepts one too, though an inspection is not charged); - schema evolution:
evolve_schema; - native queries:
search_tools.queryandqueries.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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| naget-0.3.2.tar.gz | 121.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.