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 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://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 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.deletesearch_tools.documents.add/replace/removesearch.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 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 --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.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. 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.1
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.1.tar.gz | 94.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| naget-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 163.6 kB
Release files / naget-0.3.1.tar.gz
| Download URL | naget-0.3.1.tar.gz |
|---|---|
| Size | 94.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c09574894bf5c628f38bccf53a2cb43b3dec4742f9faf6f660339b879dad5279
|
|
BLAKE2b-256 checksum How to use checksums |
bebee5591e07516f5b739eb626d02708f0b437d0dc7947c73c6e90bfdfb6200d
|
| 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.1-py3-none-any.whl
| Download URL | naget-0.3.1-py3-none-any.whl |
|---|---|
| Size | 69.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d9eb93a1a4607ff97dc64c5228ca51c91997a60f283fd0d143e7dffd230f65bb
|
|
BLAKE2b-256 checksum How to use checksums |
c247fe6f6d3eeedff2b2911af9d1e7064a459c8284cf2abde477cb2875fbf0e3
|
| 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.1
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.