Skip to main content

Enigma API Client

The Enigma Python SDK, enigma-api-client, sends GraphQL queries to the Enigma GraphQL API and turns questions in plain English into those queries. This page is the SDK reference.

Prerequisites

  • An Enigma API key. Find yours in the Enigma Console under Settings > Account > API keys.
  • Python 3.11 or later.
  • For a local strategy, which generates queries on your machine: an Anthropic or OpenRouter API key, or AWS credentials with access to Claude on Amazon Bedrock.

The examples read your Enigma API key from the ENIGMA_API_KEY environment variable:

export ENIGMA_API_KEY="your-api-key"

Install

pip install enigma-api-client

Optional extras add dependencies for specific features:

Extra Installs Needed for
enigma-api-client[bedrock] botocore BedrockProvider, Claude on Amazon Bedrock as the LLM of TemplateStrategy or GenerativeStrategy
enigma-api-client[anthropic] claude_agent_sdk AgentStrategy
enigma-api-client[all] Both of the above BedrockProvider and AgentStrategy

Quick Start

Ask a Question

import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="Tell me about Chipotle")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        brand = response.data["search"][0]
        print(brand["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL
        print(brand["__url"])  # the brand's profile in the Enigma Console


asyncio.run(main())

The SDK turns the question into a GraphQL query, runs it, and returns the result. By default, Enigma's hosted query service writes the query, so your Enigma API key is all you need.

Send a GraphQL Query

import asyncio
import os

from enigma_api_client import Client, ClientConfig

QUERY = """
query Brand($name: String!) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand {
      names(first: 1) { edges { node { name } } }
      websites(first: 1) { edges { node { website } } }
    }
  }
}
"""


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(query=QUERY, variables={"name": "Chipotle"})
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        brand = response.data["search"][0]
        print(brand["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL
        print(brand["websites"]["edges"][0]["node"]["website"])  # https://www.chipotle.com


asyncio.run(main())

A name can match many entities, so conditions: { limit: 1 } keeps only the first. The GraphQL guide and the schema reference describe the queries you can write, and Write GraphQL by Hand below lists the mistakes to avoid.

Later snippets that set config replace the config = ... line in these programs.

Configure the Client

ClientConfig takes these fields:

Field Default Meaning
enigma_api_key Required Your Enigma API key
query_strategy ServiceStrategy The strategy that turns questions into queries (see Choose a Strategy)
tier AccessTier.REDISTRIBUTION Your key's access tier, which local strategies write queries for (see Access Tiers)
url https://api.enigma.com/graphql The GraphQL endpoint
auto_inject_id_and_typename True Adds id and __typename to the inline fragments and connection nodes a query selects (see The Shape of data)
auto_inject_page_info False Adds pageInfo { hasNextPage endCursor } to every connection a query selects that doesn't already select pageInfo (see Paginate)
customize_headers_callback None A function that takes the default request headers and returns headers to add or replace. A header returned with an empty value is removed. Searches use it; the schema fetch and the hosted query service don't.

Client Lifecycle

  • Create one Client and reuse it for all your requests. It keeps connections open until you close it, either with async with Client(config) as client: or with await client.aclose(). Closing the client also closes its strategy.
  • All methods are asynchronous. In a Jupyter notebook, which already runs an event loop, replace asyncio.run(main()) with await main(). Start Jupyter from a shell where ENIGMA_API_KEY is set.
  • Use a client from one event loop. Don't share it across threads that each run their own loop.

Run Searches Concurrently

One client can run many searches at once:

import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        prompts = ["Tell me about Chipotle", "Tell me about Sweetgreen", "Tell me about Shake Shack"]
        responses = await asyncio.gather(*(client.search(prompt=p) for p in prompts), return_exceptions=True)
        for prompt, response in zip(prompts, responses):
            if isinstance(response, Exception):
                print(prompt, "failed:", type(response).__name__)
            else:
                print(prompt, response.status_code)


asyncio.run(main())

return_exceptions=True keeps one failed search from hiding the others' results. Your plan's rate limits cap requests per second, bursts and requests per day. An asyncio.Semaphore caps how many calls run at once, which keeps bursts down but doesn't enforce a rate.

Timeouts

The client waits up to 60 seconds for the API, and ServiceStrategy waits up to 30 seconds for the hosted query service. A timeout raises httpx.TimeoutException.

Read the Response

client.search() returns a SearchResponse:

Field Holds
status_code The HTTP status of the API's answer
data The GraphQL data object, or None
errors The GraphQL errors list, or None
extensions The GraphQL extensions object, or None
unavailable_data For a question: the parts Enigma cannot answer (see Unavailable Data)
raw_body The response body as text
headers The HTTP response headers, looked up case-insensitively, such as Retry-After

response.is_success() is true for a 2xx answer with no errors, with two exceptions:

  • HTTP 206. The API answers 206 when values it computes in the background, such as some counts, are not ready yet, and leaves them null. The rest of data is filled in, so a 206 answer is still usable.
  • An answer whose extensions report a background task that failed, was cancelled or ran out of credits.

The client asks the API to wait up to 25 seconds for background values before it answers, unless ClientConfig.url sets max_background_task_wait or max_call_duration.

The Shape of data

  • search returns a list of entities. __typename says what each one is: Brand, LegalEntity, OperatingLocation, Person or Address.
  • A question that names several businesses, people or street addresses gets one search for each, answered under search, search2 and search3. A person named at a company, or a street address given for a business, shares that business's search. GenerativeStrategy and AgentStrategy have an LLM write the query and its keys, so with them read every key of data. A template query's variables are numbered like its keys: searchInput, searchInput2 and searchInput3, and likewise any other variable, such as locConditions2.
  • In a template query (ServiceStrategy's default, or TemplateStrategy), the places a question lists are read this way:
    • A lookup of one location of a business ("the Starbucks at 1 Main St") or of an address gets one search for each place in the lists it gives (several streets, cities or states, and in an address lookup several units or ZIP codes), its lists paired by position, so each city or state there pairs with the business or street in the same position.
    • Elsewhere, every place the question lists applies to each of several businesses or people, so "Starbucks locations in Boston and Dunkin locations in Chicago" searches each brand in both cities. To give each its own place, say "respectively" ("Starbucks and Dunkin locations in Boston and Chicago, respectively") or ask about them separately. Cities and states are read separately. When the question gives a city for some of them and none for another ("Starbucks in Boston, Dunkin, and Peet's in Chicago"), each keeps its own city and the one with none isn't filtered by city; a list of states works the same way.
    • A brand's locations and counts are filtered by city and state. A legal entity lookup lists each business's registrations in the states it gets, under registrationsInState in each of its registeredEntities: every state the question names for all of them, or each one's own state when the question says "respectively". Brand profiles, person searches, and officer, contact, watchlist, risk and card transaction lookups don't filter by place.
    • When several businesses or people, or the streets of an address lookup, pair with places by position, a list of cities, states, units or ZIP codes longer than theirs leaves a place with none beside it, and the question is refused. Outside a count question, a street left over beside them gets an address lookup of its own while address lookups are on (see Look Up an Address); with them off, the question is refused.
  • A count question gets aggregate, an object of counts such as locationsCount and brandsCount. A count of a brand's locations depends on how the query is written. From a template (ServiceStrategy's default, or TemplateStrategy) it comes as locationsCount on the brand in search, one search per brand (search, search2 and so on). From GenerativeStrategy, or ServiceStrategy(query_strategy="generative"), it comes as locationsCount in aggregate, one per brand (aggregate, aggregate2 and so on).
  • A Brand is a business as its customers know it, such as Chipotle. A LegalEntity is a company as a government registers it, such as Chipotle Mexican Grill, Inc. An OperatingLocation is one place where a brand does business, such as a store. Understanding Enigma Data describes them in full.
  • Related data comes as connections: {"edges": [{"node": {...}}, ...]}. Each connection returns one page (see Paginate).

The client adds fields to what you asked for:

  • id and __typename on the inline fragments (... on Brand { ... }) and connection nodes the query selects. The client skips an inline fragment that carries a directive, and a connection node when the node or its edges carries one. It also skips an entity selected only through a named fragment at the top of search, as in search(...) { ...BrandFields }. Select id and __typename yourself in a skipped selection to get __url.
  • __url on each brand, legal entity and operating location that has an id and a __typename: a link to its profile in the Enigma Console. A person's __url links to the brand it appears under, or to Enigma's website when it appears under none.

To send your query exactly as written, set auto_inject_id_and_typename=False. A brand, legal entity or operating location then gets __url only when your query selects its id and __typename, and a person only when your query selects its __typename. A few connection nodes have no id, such as the MediaContent nodes of isMentionedInMediaContents. A query that selects them fails unless you set auto_inject_id_and_typename=False.

The GraphQL query in the quick start returns:

{
  "search": [
    {
      "id": "825ce859-b742-49cb-9787-b9b32f674f25",
      "__typename": "Brand",
      "names": {"edges": [{"node": {"id": "000000d0-0003-cd02-0000-0a8000000000", "__typename": "BrandName", "name": "CHIPOTLE MEXICAN GRILL"}}]},
      "websites": {"edges": [{"node": {"id": "0000002a-0010-60fa-0000-0a8000000000", "__typename": "Website", "website": "https://www.chipotle.com"}}]},
      "__url": "https://console.enigma.com/profile/brand/825ce859-b742-49cb-9787-b9b32f674f25"
    }
  ]
}

Generate Queries from Questions

A question passed as client.search(prompt=...) must name what it is about. You can ask:

  • About one business or location by name or website, or one person by name: "Tell me about Chipotle", "Who are the officers of Sweetgreen, Inc.?"
  • What is at a street address: "What is at 1600 Pennsylvania Ave NW, Washington DC?" (see Look Up an Address)
  • For an entity's government records: "Show business licenses held by Walgreens" (see Get Government Records)
  • How many businesses of a category are in a place: "How many coffee shops are in Denver?"
  • About up to three businesses or people at once: "Compare Starbucks and Dunkin"

Each business or street address a question names, and each person named apart from a business, gets its own search, all in one query. If any one of them is refused, the whole question is refused. A query runs at most three searches or counts, besides the government-record searches the SDK adds beside them (see Get Government Records), so a question that needs more, such as one naming more than three businesses, is refused. The limit counts searches, not names: a template lookup of one location of a business or of an address gives each place it lists its own search, so "the Starbucks at 1 Main St, 2 Elm St, 3 Oak St and 4 Pine St in Boston" is refused. Listing businesses by category ("List coffee shops in Denver") is not supported. Data Enigma doesn't serve, such as lawsuits, is left out and listed in unavailable_data.

Choose a Strategy

A strategy turns each question into a GraphQL query. The default runs on Enigma's servers. The other three are local strategies: they run on your machine and call an LLM with your own account.

Strategy How it writes the query What you need Speed
ServiceStrategy (default) On Enigma's hosted query service, an LLM picks one of Enigma's tested query templates and fills it in Your Enigma API key Fast
TemplateStrategy An LLM picks one of Enigma's tested query templates and fills in the names and places An LLM API key Fast
GenerativeStrategy An LLM writes the whole query; the SDK checks it against the API schema and repairs it An LLM API key Slower
AgentStrategy A Claude agent on Amazon Bedrock reads Enigma's query reference and writes the query AWS credentials with Bedrock access, and enigma-api-client[anthropic] Slowest, often tens of seconds
  • Start with the default. It needs nothing beyond your Enigma API key.
  • When the default can't answer a question, ServiceStrategy(query_strategy="generative", enigma_api_key=...) has the hosted service write the whole query instead of picking a template.
  • To generate queries with your own LLM account, use TemplateStrategy. Use GenerativeStrategy for questions the templates don't cover, and review the queries it writes.
  • For a pipeline that asks the same kind of question many times, generate the query once, review it, and run it with your own variables (see the section See and Reuse a Generated Query).

To use a strategy other than the default, pass it to ClientConfig:

from enigma_api_client.strategies import TemplateStrategy

config = ClientConfig(
    enigma_api_key=os.environ["ENIGMA_API_KEY"],
    query_strategy=TemplateStrategy(),
)

ServiceStrategy

ServiceStrategy sends your question to Enigma's hosted query service, authenticated with your Enigma API key, and returns the query the service writes. client.search then runs it. The service reads your access tier from the key.

The service picks a template by default, as TemplateStrategy does. To have it write the whole query instead, create the strategy yourself:

from enigma_api_client.strategies import ServiceStrategy

strategy = ServiceStrategy(query_strategy="generative", enigma_api_key=os.environ["ENIGMA_API_KEY"])
config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=strategy)

A ServiceStrategy you create doesn't take the key from ClientConfig: pass enigma_api_key=, or it reads ENIGMA_API_KEY.

TemplateStrategy

An LLM reads the question, picks one of Enigma's query templates, and extracts the names, places and other values the template needs. The SDK then builds the GraphQL from the template, so the query's fields always come from tested templates.

The templates cover brand profiles, brand lookups by website, a brand's locations, a location's profile, person searches, corporate officers, legal entity verification, contacts, watchlist screening, risk assessments, counts by category and place, counts of a brand's locations, and address lookups. With tier=AccessTier.EXTERNAL, they also cover card transactions.

A lookup returns a core set of fields plus only the data the question asks for: industry, website, phone, officers, bankruptcies, watchlist entries, TINs, registration details, locations, affiliates, alternate names, address, reviews or headcount.

  • A compliance question adds officers, TINs, watchlist entries, bankruptcies and registration details. A question counts as one when it mentions KYB, KYC, AML, compliance, due diligence, a background check or vetting, or asks to vet or screen the entity.
  • A question asking for everything about an entity adds all of them.
  • With several businesses or people, the data the question asks for is added to each one's search.
  • TINs and watchlist entries are left out for AccessTier.REDISTRIBUTION, the default tier (see Access Tiers).

A question no template fits raises EnigmaClientException: UNSUPPORTED_DATA_REQUESTED for a category listing, and INVALID_QUERY for a question that is not about a business, location, person or address.

Argument Default Meaning
anthropic_api_key ANTHROPIC_API_KEY The Anthropic API key, when llm_provider is not set
model claude-haiku-4-5-20251001 The Anthropic model, when llm_provider is not set
llm_provider None Another LLM provider (see LLM Providers)
shape_cache On, unless ENIGMA_SHAPE_CACHE_ENABLED turns it off The query shape cache
shape_cache_max_shapes 8192 The most shapes the cache keeps
address_search ENIGMA_ADDRESS_SEARCH, else "auto" Whether address lookups are offered (see Look Up an Address)

GenerativeStrategy

One LLM call writes the whole query, guided by an excerpt of Enigma's GraphQL reference, example query patterns and rules, which tell it to write its values into the query text. The SDK then checks the query against the API schema and the rules the API enforces at execution. It fixes what it can without a model call and otherwise makes one more LLM call to repair the query. This handles questions the templates don't cover. The LLM can still choose fields that don't answer the question, so review the queries it writes before you rely on them.

GenerativeStrategy refuses a query with UNSUPPORTED_DATA_REQUESTED instead of running it when any of its searches is one of the first four of these, or when every search or count in it is the last:

  • A person search by surname alone.
  • An EIN or filing number searched as a name.
  • A wildcard name pattern.
  • A street address searched as a name.
  • A category count written for a question that asks for a list.

It takes the same arguments as TemplateStrategy.

AgentStrategy

A Claude agent, built with the Claude Agent SDK, reads Enigma's GraphQL reference, examples and field notes, and writes the query. The SDK checks the agent's answer against the API schema and applies the same checks as GenerativeStrategy, fixing what it can without a model call. It suits complex questions best and is the slowest strategy.

pip install "enigma-api-client[anthropic]"
from enigma_api_client.strategies import AgentStrategy

config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=AgentStrategy())

The agent runs on Amazon Bedrock only:

  • Credentials come from the standard AWS credential chain.
  • The identity needs bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream on the inference profile and on the foundation model in every region the profile routes to.
  • The default model is the US inference profile us.anthropic.claude-haiku-4-5-20251001-v1:0. The region is region=, else AWS_REGION or AWS_DEFAULT_REGION, else us-east-1. Pass model= and region= to change them, and keep the region inside the profile's geography.
  • max_turns (default 8) and max_budget_usd (default 0.25) bound each call.

The agent runs the Claude Code CLI bundled with claude_agent_sdk as a subprocess:

  • Its nonessential network traffic is turned off.
  • Its only tool reads files inside the query reference bundled with the SDK. It has no shell, file writes, web access or file attachments.
  • Every environment variable it inherits is blanked, except process basics, TLS and proxy settings, and the AWS credential chain.
  • It keeps no session transcript on disk.

The agent writes values into the query itself, so the query it returns has no variables. When its answer fails the schema check, AgentStrategy raises UnrepairedQueryError.

See and Reuse a Generated Query

strategy.generate_query(prompt) returns the query without running it:

import asyncio
import os

from enigma_api_client import Client, ClientConfig
from enigma_api_client.strategies import ServiceStrategy


async def main():
    key = os.environ["ENIGMA_API_KEY"]
    strategy = ServiceStrategy(enigma_api_key=key)
    async with Client(ClientConfig(enigma_api_key=key, query_strategy=strategy)) as client:
        generated = await strategy.generate_query("Tell me about Chipotle")
        print(generated.query)
        print(generated.variables)  # {'searchInput': {'name': 'Chipotle', 'entityType': 'BRAND', 'conditions': {'limit': 1}}}

        variables = {"searchInput": {**generated.variables["searchInput"], "name": "Sweetgreen"}}
        response = await client.search(query=generated.query, variables=variables)
        print(response.data["search"][0]["names"]["edges"][0]["node"]["name"])  # SWEETGREEN


asyncio.run(main())
  • Check a query before you reuse it with other variables. Any strategy can write some values into the query text, where your variables don't replace them. For example, when a question names a state, a legal entity lookup writes that state into the query, and a query that selects government records writes the searched name into its archive searches. GenerativeStrategy is told to write every value into the query text, and AgentStrategy always does.
  • Call generate_query inside the async with block. A local strategy checks its query against the API schema, which the Client fetches with your key.
  • Called directly, a local strategy's generate_query writes for AccessTier.REDISTRIBUTION unless you pass your tier, as in generate_query(prompt, tier=AccessTier.EXTERNAL). client.search passes ClientConfig.tier for you. ServiceStrategy always writes for your key's tier.

generate_query returns a GenerateQueryResponse:

Field Holds
query, variables The GraphQL query and its variables
unavailable_data The parts of the question Enigma cannot answer
deterministic_repair When the SDK fixed the query without a model call, the kinds of fixes, comma separated
cache_rule "shape" when the query shape cache answered without an LLM call; other values report what the cache learned from the call
records For a question asking for government records, from a local strategy: selected when the SDK added record fields, or why it added none (no_schema, no_entity, already_selected, invalid)

Access Tiers

The fields you can read depend on your API key's access tier:

  • AccessTier.REDISTRIBUTION is the narrowest tier.
  • AccessTier.EXTERNAL adds fields such as TINs, watchlist entries and card transaction data.

The hosted service reads the tier from your key. A local strategy writes queries for the tier in ClientConfig.tier, which defaults to AccessTier.REDISTRIBUTION. Set it to your key's tier:

from enigma_api_client import AccessTier
from enigma_api_client.strategies import TemplateStrategy

config = ClientConfig(
    enigma_api_key=os.environ["ENIGMA_API_KEY"],
    query_strategy=TemplateStrategy(),
    tier=AccessTier.EXTERNAL,
)

If you keep the default tier, a local strategy writes queries that leave out fields a broader key could read. If you don't know your key's tier, ask Enigma support.

  • A local strategy removes from the query each field your tier is not served, and lists it in unavailable_data with the reason contractual. enigma_api_client.restricted_fields_for(tier) returns those fields.
  • enigma_api_client.gate_blocked_fields(tier) returns those fields plus, for AccessTier.REDISTRIBUTION, fields the API serves but a redistribution contract forbids. A local strategy doesn't remove those extra fields, and the API can return them to your own queries too, so use gate_blocked_fields to filter output you pass on.
  • The SDK fetches the API schema for the tier of the first Client you create in a process, with that client's key, and checks generated queries against it. Set tier before you create your first Client. In a notebook, restart the kernel after you change it.
  • A later Client with a different tier gets no schema. For a local strategy on that client:
    • Generated queries aren't checked against the schema.
    • Government records are left out.
    • An address-only question is refused unless address lookups are on (address_search=True or ENIGMA_ADDRESS_SEARCH=on).
    • AgentStrategy raises PROVIDER_ERROR.

To serve several tiers in one process, register a function that takes an AccessTier and returns that tier's schema, before you create the first Client. The SDK calls it either on your event loop's thread, blocking every search on that loop until it returns, or from a background thread with no context variables set. So it must be synchronous and thread-safe, return in bounded time and not read context variables:

import os

from enigma_api_client import AccessTier, validation

KEYS = {
    AccessTier.EXTERNAL: os.environ["ENIGMA_EXTERNAL_KEY"],
    AccessTier.REDISTRIBUTION: os.environ["ENIGMA_API_KEY"],
}


def introspector(tier):
    key = KEYS.get(tier)
    if key is None:
        return None
    try:
        return validation.introspect_schema("https://api.enigma.com/graphql", key)
    except Exception:
        return None


validation.set_schema_introspector(introspector)  # before you create the first Client

Each Client then gets the schema for its own tier. Return None when a fetch fails, as above, so a failed fetch doesn't raise from client.search.

The first time a tier's schema is used after it is five minutes old, the SDK fetches it again in the background. Pass refresh_seconds= to set_schema_introspector to change that interval, or refresh_seconds=None to turn the refresh off.

LLM Providers

TemplateStrategy and GenerativeStrategy call Anthropic's API by default, with anthropic_api_key= or the ANTHROPIC_API_KEY environment variable; their model= argument applies only to that default. Pass llm_provider= to use another provider:

from enigma_api_client.strategies import (
    AnthropicProvider, BedrockProvider, GenerativeStrategy, OpenRouterProvider, TemplateStrategy,
)

# Anthropic's API (ANTHROPIC_API_KEY)
TemplateStrategy(llm_provider=AnthropicProvider(model="claude-haiku-4-5-20251001"))

# OpenRouter (OPENROUTER_API_KEY). provider_routing is sent as OpenRouter's "provider"
# object and reasoning as its "reasoning" object; omit either to keep OpenRouter's default.
GenerativeStrategy(llm_provider=OpenRouterProvider(
    model="anthropic/claude-haiku-4.5",
    provider_routing={"order": ["anthropic"], "allow_fallbacks": False},
    reasoning={"enabled": False},
))

# Claude on Amazon Bedrock (pip install "enigma-api-client[bedrock]"), signed with your AWS credentials.
# The default model is a US inference profile; pass model= outside US regions.
TemplateStrategy(llm_provider=BedrockProvider(region="us-east-1"))

To use another LLM, implement the LlmProvider protocol: async def complete(system, user, max_tokens) -> Completion. Return Completion(text, truncated), where truncated is true when the model hit its output limit. For an HTTP failure, raise ProviderHTTPError(status_code, message): 408, 429 and 5xx are reported as TRANSIENT_PROVIDER_ERROR, any other HTTP status as PROVIDER_ERROR, and a value that is not an HTTP status as TRANSIENT_PROVIDER_ERROR. Pass the call's token counts to Completion as input_tokens=, output_tokens=, cache_read_tokens= and cache_write_tokens= so that Token Usage includes them. Leave cache reads and writes out of input_tokens; they have their own fields. LlmProvider, Completion and ProviderHTTPError import from enigma_api_client.strategies. Raise ValueError for a missing key or bad configuration (reported as GENERATE_QUERY_CALLBACK_NOT_CONFIGURED) and RuntimeError for any other provider failure (reported as TRANSIENT_PROVIDER_ERROR). Any other exception propagates unchanged.

Query Shape Cache

TemplateStrategy and GenerativeStrategy keep an in-memory cache of query shapes learned from successful generations. A shape is a generated query with its names and places left as blanks. Once a shape has been confirmed across several different entities, a matching question is answered from the cache without an LLM call. The cache is on by default and is shared by every caller of the same strategy instance at the same tier. Turn it off with shape_cache=False or by setting ENIGMA_SHAPE_CACHE_ENABLED=0.

Token Usage

After client.search(prompt=...) or generate_query returns or raises, enigma_api_client.strategies.GENERATION_USAGE.get() returns that call's LLM token counts as an LlmUsage: completions, input_tokens, cache_read_tokens, cache_write_tokens and output_tokens, plus unmetered_completions, the completions whose provider left out the input or the output token count, so the sums miss their tokens. Read it in the same task that awaited the call. asyncio.gather runs each call in a task of its own, so read the usage inside the coroutine you pass to it. ServiceStrategy reports zero usage, and AgentStrategy reports its run's totals.

Write Your Own Strategy

Subclass QueryStrategy, implement generate_query and aclose, and set GENERATION_USAGE to a fresh LlmUsage() at the start of generate_query. This strategy treats the whole question as a brand name:

import asyncio
import os

from enigma_api_client import DEFAULT_TIER, Client, ClientConfig, QueryStrategy
from enigma_api_client.strategies import GENERATION_USAGE, LlmUsage
from enigma_api_client.strategies.base import GenerateQueryResponse

QUERY = """
query Brand($name: String!) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand { names(first: 1) { edges { node { name } } } }
  }
}
"""


class BrandNameStrategy(QueryStrategy):
    async def generate_query(self, prompt, *, tier=DEFAULT_TIER, prior_attempt=None):
        GENERATION_USAGE.set(LlmUsage())
        return GenerateQueryResponse(QUERY, {"name": prompt})

    async def aclose(self):
        pass


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=BrandNameStrategy())
    async with Client(config) as client:
        response = await client.search(prompt="Chipotle")
        print(response.data["search"][0]["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL


asyncio.run(main())

Handle Errors

import asyncio
import os

from enigma_api_client import Client, ClientConfig, EnigmaClientException


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        try:
            response = await client.search(prompt="List coffee shops in Denver")
        except EnigmaClientException as exc:
            print(exc.error_code, exc)
            return
        if not response.is_success():
            print("HTTP", response.status_code, response.errors)


asyncio.run(main())

The example prints the error message, which is useful while you develop. In production, log the exception type and error_code instead, because the message can quote values from the query.

Generation Errors

When the SDK can't generate a query, client.search(prompt=...) raises EnigmaClientException, whose error_code is an EnigmaClientErrorCode.

With ServiceStrategy, the default, the code is always INVALID_QUERY, including when the hosted service fails with HTTP 401, 429 or 5xx. When the service answers with an error status, exc.status_code holds it, exc.headers holds its response headers, and the message reads Enigma API returned HTTP <status>: followed by the start of its answer. Retry when the status is 429 or 5xx, after the number of seconds in exc.headers.get("Retry-After") when it gives one. When the service answers with a GraphQL error instead, the message starts Enigma API GraphQL error:. For the example above, the message begins Enigma API returned HTTP 400 and explains that listing businesses by category is not supported.

Local strategies use these codes:

Code Meaning
INVALID_QUERY The question or the generated query is unusable
UNSUPPORTED_DATA_REQUESTED Enigma doesn't serve what the question asks for, or the question needs a query the SDK refuses (see Generate Queries from Questions and GenerativeStrategy)
GENERATE_QUERY_CALLBACK_NOT_CONFIGURED An LLM API key is missing, or the provider is misconfigured
TRANSIENT_PROVIDER_ERROR The LLM provider timed out, rate limited the call, or failed with a 5xx error; retry with backoff
PROVIDER_ERROR Any other LLM provider failure, such as HTTP 401 or 403; retrying won't help. AgentStrategy also raises it when the SDK has no API schema for your tier (see Access Tiers); a retry helps only when the schema fetch failed for a moment.

For a provider failure, exc.status_code holds the provider's HTTP status when it reported one.

The SDK never raises RESTRICTED_DATA_REQUESTED or INVALID_VARIABLES; they exist for applications that enforce their own policies.

Two subclasses of EnigmaClientException, both with the code INVALID_QUERY, import from enigma_api_client:

  • TruncatedOutputError: the LLM hit its output limit. It happens again with the same model and question, so don't retry with the same model.
  • UnrepairedQueryError: GenerativeStrategy or AgentStrategy wrote a query that failed the schema check even after repair. It carries .draft, .errors and .tier, and you can pass it to another GenerativeStrategy as generate_query(prompt, prior_attempt=exc) to start from that draft. Its message quotes values from the generated query, so log the exception type and error_code rather than str(exc).

Unavailable Data

When a question asks for data Enigma doesn't serve, the rest of the question is still answered, and response.unavailable_data lists what was left out, one entry per item with field, reason and message keys. "What is Chipotle's annual revenue?" with a redistribution key returns the brand and this entry:

[{"field": "revenue", "reason": "contractual",
  "message": "Revenue and other card-derived financials are not served due to contractual restrictions."}]

reason is contractual when your tier is not served the data, and out_of_scope otherwise, for example when Enigma doesn't have it. message says why. "Tell me about Chipotle, including any lawsuits against it" lists lawsuits as out_of_scope.

API and Network Errors

  • An error status from the API doesn't raise. Check response.is_success() and response.status_code, and read response status codes. A 402 means a billing problem, such as running out of credits.
  • A 429 means you went over a rate limit. Wait at least the number of seconds in its Retry-After header, response.headers.get("Retry-After"), before you retry. Stop after a few retries, because a 429 can also mean you have used your daily limit.
  • A response body that isn't a JSON object, such as a gateway's error page, comes back with data set to None, an error in errors and the body in raw_body, so is_success() is false.
  • An API key or header value an HTTP header can't carry as written, such as one with a newline or a trailing space, raises ValueError naming the header, before the API request is sent.
  • Network failures, from the API or the hosted query service, raise httpx exceptions such as httpx.TimeoutException and httpx.ConnectError.

Paginate

Each connection returns one page. To read every page, select pageInfo { hasNextPage endCursor } on the connection and send the same query again with after: set to the last endCursor:

import asyncio
import os

from enigma_api_client import Client, ClientConfig

QUERY = """
query Locations($name: String!, $cursor: String) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand {
      operatingLocations(first: 100, after: $cursor) {
        edges { node { addresses(first: 1) { edges { node { fullAddress } } } } }
        pageInfo { hasNextPage endCursor }
      }
    }
  }
}
"""


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        cursor, addresses = None, []
        while True:
            response = await client.search(query=QUERY, variables={"name": "Sweetgreen", "cursor": cursor})
            if not response.is_success():
                raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
            locations = response.data["search"][0]["operatingLocations"]
            for edge in locations["edges"]:
                for address in edge["node"]["addresses"]["edges"]:
                    addresses.append(address["node"]["fullAddress"])
            if not locations["pageInfo"]["hasNextPage"]:
                break
            cursor = locations["pageInfo"]["endCursor"]
        print(len(addresses), "addresses")


asyncio.run(main())
  • Start with cursor set to None.
  • Use hasNextPage to decide whether more pages exist. A full page doesn't mean there are more.
  • Keep the connection's first and conditions (with its filter and orderBy) the same on every page, because a cursor is only valid for the arguments it came from.
  • ClientConfig(auto_inject_page_info=True) adds pageInfo { hasNextPage endCursor } to every connection that doesn't already select pageInfo, in every query the client sends, including generated ones.
  • Generated queries already select page info (for GenerativeStrategy and AgentStrategy, once the API schema for your tier is cached) on a brand's affiliated brands (Brand.affiliatedBrands), a legal entity's parent and subsidiary legal entities (LegalEntity.legalEntities) and the government record pages. The template that lists a brand's locations selects it on Brand.operatingLocations too. The legal entity lookup selects it on registrationsInState when it lists registrations in several states, which share one page.
  • To read more pages of a generated query, get it with generate_query and add after: $cursor to the connection. Then declare $cursor: String with the query's other variables, as the example above does. If the query declares no variables yet, add ($cursor: String) after its name, or begin it with query ($cursor: String) { when it has no name.
  • roles doesn't paginate (see Rules the API Enforces at Execution).

Look Up an Address

A question about a street address with a house number and no business name searches addresses:

import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="What is at 1600 Pennsylvania Ave NW, Washington DC?")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        for address in response.data["search"]:
            locations = address["operatingLocations"]["edges"]
            print(f'{address["fullAddress"]}: {len(locations)} operating locations listed')


asyncio.run(main())

The query is an entityType: ADDRESS search that returns up to 10 addresses: the building and units in it. When the question names a unit, that unit usually comes first. Each address carries:

  • Its address fields, such as fullAddress, city, state and zip.
  • The first page of the operating locations (with brand names), legal entities and registrations at the address.

A question that asks how many gets an unavailable_data entry, address counts, saying the query does not count what is at the address.

How questions are read:

  • A business name with an address ("the Starbucks at 123 Main St") is looked up as that business's location instead.
  • A ZIP code alone, a street with no house number, or a business type on a street ("the barbershop on Elm Street") is refused.
  • Before a local strategy returns the query, the SDK cleans the address the LLM extracted:
    • It moves a suite, floor or apartment into street2 in USPS short form (STE 1A, FL 8).
    • It drops a unit designator with no number.
    • It removes surrounding prose and a trailing country.
    • It converts state names to postal codes.

Local strategies take an address_search= argument: True, False or "auto". Without it, they read the ENIGMA_ADDRESS_SEARCH environment variable (on, off or auto), and use auto when neither is set. auto turns address lookups on when the API schema for your key supports them. Each strategy instance decides at its first question for each access tier and keeps that answer. If the schema can't be fetched at that moment, address lookups stay off for that instance and tier. With address lookups off, a question that gives only an address is refused.

No strategy selects the Address counts brandViaLegalEntityCount, brandViaPersonLegalEntityCount and legalEntityViaRegistrationCount, because they take too long to compute. Leave them out of your own queries too.

Get Government Records

Enigma's Gov Archive holds rows of government and public datasets, such as state UCC filings, business licenses and city food inspections. Each row is a record, and Enigma links most records to the businesses, locations, people and addresses they name. Each dataset carries tags, such as ucc_lien, that name the kinds of records it holds. A question that names an entity and asks for its records gets them. You can ask for:

  • UCC and lien filings
  • Business and occupational licenses, including liquor, contractor, tobacco and cannabis, insurance, money transmitter, firearms and childcare licenses
  • Permits and sales tax permits
  • Inspections and health grades
  • Violations and enforcement actions
  • Government contracts, PPP loans and SAM.gov registrations (UEI, CAGE code)
  • Form 5500 benefit plans, H-1B and labor certifications, workers' compensation and WARN notices
  • SEC filings, 13F holdings and Form 990 filings
  • Property records, EPA records and USDOT motor carrier records
  • Campaign finance and lobbying
  • Medicare and Medicaid providers, and minority-, women- and veteran-owned business certifications

A question that asks for "public records", "government records" or the "gov archive" in general gets records of every kind, unless it also names one of these. The names a question searches by don't count, so a question about a business named "Permit Pros" asks for no permits.

import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="Show business licenses held by Walgreens")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        for entity in response.data["search"]:
            print(entity["__typename"], "linked records:", entity.get("recordCount"))
        for key, items in response.data.items():
            if key.startswith(("archiveRecords", "archiveBrandRecords")):
                for item in items or []:
                    if item["__typename"] == "Record":
                        print(key, item["sourceTitle"], item["data"])


asyncio.run(main())

The records come in two places: on each entity the search returns, and in archive searches next to search in data.

Records on Each Entity

Each entity the search returns gains these fields:

Field Holds
recordCount The number of records linked to the entity, over every dataset
recordSourceCounts, recordTagCounts On a returned address: the record count per dataset title and per tag
recordSources On a legal entity the search returns: the first three of its registeredEntities, each with recordSourceCounts and recordTagCounts, with page info
matchingRecordCount For a question that names record types, when the entity gets a records page: the number of its linked records of those types
records The first ten linked records when the search is limited to one entity (conditions: { limit: 1 }), otherwise the first three, with pageInfo { hasNextPage endCursor }; for a question that names record types, only records of those types

Legal entities the query selects under a returned brand or location get recordCount too, and, when the entities get a records page, three records.

A question asking for records in general gets the records page whatever the API's schema. For a question that names record types, the SDK adds the page and matchingRecordCount only when the API's schema shows it can filter an entity's linked records quickly; otherwise the entities get neither, and those records come from the archive searches alone. Read the page with entity.get("records").

Records from Archive Searches

archiveRecords searches the archive by the same name, person (first and last name) or street address as the search it sits beside. A name search with entityType: BRAND, entityType: OPERATING_LOCATION or no entityType gets two archive searches: archiveRecords searches legal entities, and archiveBrandRecords searches brands, which finds records that name a brand or trade name. The two can return the same record. When several searches in the query get archive searches, the keys of the second and later ones end in a number, as in archiveRecords2. The number counts only the searches that get archive searches, so it need not match the search key beside it, and an archive search identical to an earlier one is sent once.

Each archive search returns the entities it matched, with only __typename (and, for a person, __url), then up to 50 records. It returns a record when the record's data names the searched name or address, whether or not the record is linked to an entity. For a question that names record types, each record carries one of their tags. A question for records in general gets records with any tag. Read the items whose __typename is Record.

  • The archive matches names loosely, so a record can belong to another business or person with a similar name. It also ignores a city or state given without a street, so that namesake can be anywhere. Check the name in the record's data.
  • A dataset can carry a record type's tag without belonging to it, so check sourceTitle.
  • Records are not sorted by date.
  • A search with no name, person or street address, such as one by website alone, gets no archive search.
  • The archive searches miss many records of these types: UCC and lien filings, contractor licenses, PPP loans, government contracts, SAM.gov registrations, workers' compensation, lobbying and Form 990 filings. When the entities get no records page, a question for them often returns few or none.
  • Set ENIGMA_ARCHIVE_SEARCH=off to leave the archive searches out of queries a local strategy writes.

Each record, in either place, carries:

  • sourceTitle: the dataset's title.
  • sourceUrl: a link to the dataset, not to the row. It can be null.
  • tags: the dataset's tags, such as ucc_lien or abc_license.
  • firstObservedDate, lastObservedDate: when Enigma first and last saw the row. These are usually not filing dates.
  • data: the row itself, as column names and values. The filing dates are here.

For the example question, the query gains recordCount on each entity it returns, and archive searches next to search. The archiveRecords search, abbreviated:

archiveRecords: search(searchInput: { name: "Walgreens", entityType: LEGAL_ENTITY, dataLayer: UNGROUNDED,
                                      conditions: { limit: 50, filter: { IN: ["tags", ["abc_license", "active_occupational_licenses", ...]] } } }) {
  __typename
  ... on Record { sourceTitle sourceUrl tags firstObservedDate lastObservedDate data }
}

Read More of an Entity's Records

When an entity's records page has pageInfo.hasNextPage set to true, search that entity alone by its id: searchInput: { id: "<its id>", entityType: <its type>, conditions: { limit: 1 } }, with entityType set to BRAND, LEGAL_ENTITY, OPERATING_LOCATION, PERSON or ADDRESS to match its __typename. Select records with the same first and conditions plus after: "<endCursor>", because the cursor is only valid for those arguments. To get a query to adapt, call generate_query. An archive search returns one page, with no cursor.

Record Types and Their Phrases

The installed package lists the record types a question can ask for, with the phrases that ask for each and the tags that select it:

import json
from importlib.resources import files

path = files("enigma_api_client") / "plugins/enigma-api/skills/enigma-graphql/references/record_topics.json"
for topic in json.loads(path.read_text())["record_topics"]["topics"]:
    print(topic["id"], topic["phrases"][:3])

Records and Unavailable Data

  • An unavailable_data entry for something the selected records answer in full, such as "liquor licenses", is removed.
  • When the query selects record counts but none of the records the question asks for, for example on a search by website alone when the entities get no records page, that entry stays and gets the message "This query does not return these records."
  • An entry for something the records answer only in part, such as "tax liens" or "lawsuits and violations", gets the message "The returned records cover this only in part." when the query selects those records.

Records and the API Schema

A local strategy adds records only once it has the API schema for your tier. With address search on auto, the default, a strategy fetches the schema before it answers its first question, because it needs the schema to decide on address lookups. A TemplateStrategy whose address search you set yourself (address_search=True or False, or ENIGMA_ADDRESS_SEARCH set to on or off) fetches the schema in the background instead. Until the schema arrives, and for a while after a failed fetch, its questions can come back without records, and generate_query reports records as no_schema.

Environment Variables

Variable Used by Meaning
ENIGMA_API_KEY ServiceStrategy The key of a ServiceStrategy you create without enigma_api_key= (the default strategy uses the ClientConfig key)
ANTHROPIC_API_KEY AnthropicProvider Anthropic API key
OPENROUTER_API_KEY OpenRouterProvider OpenRouter API key
AWS_REGION, AWS_DEFAULT_REGION BedrockProvider, AgentStrategy Region when region= is not passed (default us-east-1)
ENIGMA_SHAPE_CACHE_ENABLED TemplateStrategy, GenerativeStrategy 0, false, no or off turns off the query shape cache
ENIGMA_ADDRESS_SEARCH TemplateStrategy, GenerativeStrategy, AgentStrategy on, off or auto (1, true and yes also mean on, 0, false and no off; see Look Up an Address); read when the strategy is created
ENIGMA_ARCHIVE_SEARCH TemplateStrategy, GenerativeStrategy, AgentStrategy off, 0, false or no leaves the archive searches out of record queries (see Get Government Records); read on each call

Logging

The SDK logs through Python's logging module, on loggers named enigma_api_client.*:

  • Lines at INFO and above are fixed messages with counts, statuses, rule names, exception class names, schema field names and file paths. They never contain your questions or the LLM's text.
  • DEBUG lines can contain your questions, the LLM's output and the agent's transcript. Don't turn on DEBUG for these loggers where your logs must not hold that data.

What Leaves Your Machine

  • Searches: the query and its variables go to the Enigma API with your Enigma API key.
  • ServiceStrategy: your question goes to Enigma's hosted query service with your Enigma API key. The service writes the query with an LLM. It has its own address, which ClientConfig.url doesn't change.
  • Local strategies: your question and Enigma's query instructions go to the LLM provider you configure (Amazon Bedrock for AgentStrategy). A repair also sends the draft query and its errors. Search results never go to the LLM.
  • Schema checks: the SDK fetches the API schema from the Enigma API with the first Client's key, or with the keys your own introspector uses.

The SDK sends nothing else.

Write GraphQL by Hand

The Enigma schema has hundreds of fields, several of them named differently from what you might guess. A query can also pass schema validation and still be rejected when it runs. This section lists what most often goes wrong.

Common Mistakes

Field you might guess Actual field
status (of a location) operatingStatus
url website
description industryDesc
title jobTitle
averageRating reviewScoreAvg
  • A field that doesn't exist fails the whole request with HTTP 400 and Cannot query field.
  • search returns a list. Select through inline fragments, as in search(...) { ... on Brand { ... } }; it has no edges.
  • A category in name matches business names. name: "coffee shop" returns businesses with names like "Coffee Shop". To count a category, use aggregate.
  • A company's officers are under its registrations. LegalEntity.persons is almost always empty for a company. Follow registeredEntities, then registrations, then roles, then legalEntities.
  • Connections have no totalCount. Count with an aggregate query, or with count(field: ...) on an entity.
  • Website content sits under each website. Select websiteContents inside websites: websites(first: 1) { edges { node { websiteContents(first: 1) { edges { node { httpStatusCode } } } } } }.
  • A brand search for a local franchise returns the whole chain. Searching BRAND for a single franchised hotel returns the parent brand with all its locations. Search OPERATING_LOCATION with an address to find one location.

Rules the API Enforces at Execution

These queries pass schema validation and are rejected when they run. GenerativeStrategy and AgentStrategy check these rules before they return a query, and the templates follow them.

Rule Error
search needs name, website, person, phoneNumber, id or tin, even when it has a filter. An address alone works only in an entityType: ADDRESS search with address.street1 (a street line with a house number) or address.id. Search must include name, website, person, or phone_number
searchInput.prompt can't be combined with a selection set. To have the API answer a question in plain English, send the question in searchInput.prompt with no selection set (see API search). 'prompt' cannot be combined with a selection set
roles takes first and conditions only, although the schema lists last, after and before. Fetch roles as one page. LegalEntity.roles does not currently support last, after or before (the error names the parent type)
A PERSON search finds people by person: { firstName, lastName }, id or tin, not by name. Person search must include an identifier or first, middle or last name
A filter's AND, OR and NOT take a list of clauses, and NOT takes exactly one. Rejected
A filter clause has exactly one operator. Combine several clauses in one AND list. Rejected
IS_NULL and IS_NOT_NULL test a column. To test for related records on a connection path such as registrations, use HAS or NOT: [{ HAS: [...] }]. Rejected
An aggregate filter on brands.industries.industryCode needs { EQ: ["brands.industries.industryType", "naics_2022_code"] } in the same AND. Joins aren't allowed for filter

Upgrade Notes

Each section lists what changed after the version it names. Read every section that names your current version or a later one.

Upgrading from 0.11.0

With Client's default strategy the hosted service builds the query, so the TemplateStrategy and GenerativeStrategy changes below reach you when the service adopts the release, whatever SDK version you run.

  • TemplateStrategy answers a question that names several businesses, people or street addresses with one search for each in the same query, answered under search, search2 and search3, where it answered one of them or refused the question. GenerativeStrategy likewise searches each brand a question names, keyed search, search2 and search3, where it usually searched the first brand only and otherwise keyed the searches with other aliases such as a and b. Code that reads only data["search"] gets the first; read the other keys for the rest.
  • A generated query with more than three root fields, not counting the government-record searches the SDK adds beside them, is refused: a local strategy raises UNSUPPORTED_DATA_REQUESTED, and ServiceStrategy raises INVALID_QUERY once the hosted service adopts the release.
  • GenerativeStrategy counts each brand a how-many question names, under aggregate, aggregate2 and so on, where it counted the first brand only.
  • GenerativeStrategy and AgentStrategy refuse a query when any one of its searches is a shape they refuse (a person search by surname alone, an EIN searched as a name, a wildcard name pattern, or a street address searched as a name), where they served it when another root field beside it was not such a search.
  • Templates return a brand's affiliated brands 50 to a page, up from 10, and GenerativeStrategy asks its LLM for the same page size.
  • A legal entity lookup lists a business's registrations in each state it gets (see The Shape of data), where it listed none when it got several.
  • In a lookup of one location of a business ("the Starbucks at 1 Main St") or of an address, TemplateStrategy searches each place the question lists (several streets, cities or states, and units and ZIP codes in an address lookup), where it sent the list in a single field, dropped it, or refused the question. A question about a brand's locations in several cities still gets one search covering them all. For several businesses or people, every city, and every state, applies to each unless the question says "respectively" or gives one of them no city (or no state), except in a lookup of one location of a business, where each city or state pairs with the business in the same position. When several businesses or people, or the streets of an address lookup, pair with places by position, a list of cities, states, units or ZIP codes longer than theirs leaves a place with none beside it, and the question is refused; outside a count question, a street left over gets an address lookup of its own, or refuses the question while address lookups are off.
  • When the SDK repairs a GenerativeStrategy or AgentStrategy query that repeats a root field with different arguments, the second one is keyed search2 (or aggregate2), where it was search_2.

Upgrading from 0.10.3

  • Address lookups no longer select the five Address count fields (associatedBrandCount, operatingLocationOperatesAtCount, legalEntityReceivesMailAtCount, registrationRecordedCount and associatedPersonCount). An address result no longer carries those keys, so code that reads address["associatedBrandCount"] raises KeyError; read the operating locations, legal entities and registrations each address lists instead. With Client's default strategy the hosted service builds the query, so this happens when the service adopts the release, whatever SDK version you run.
  • address_search="auto", and the check that refuses an address search before it is served, also take a schema that defines an Address type and the Node interface as one that supports address lookups, besides one whose Address type carries associatedBrandCount.
  • When a question asks how many, other than a headcount, and the query is an address lookup that selects no count, unavailable_data has an address counts entry (out_of_scope).

Upgrading from 0.10.2

  • SearchResponse.headers holds the HTTP response headers, so you can read Retry-After on a 429. It takes no part in == or repr; dataclasses.asdict(response) and dataclasses.astuple(response) now include it as an httpx.Headers, which json.dumps can't serialize.
  • A response body that isn't a JSON object, such as a gateway's error page, no longer raises ValueError (or AttributeError for JSON that isn't an object). client.search returns it with data set to None, an error in errors and the body in raw_body, so is_success() is false. ServiceStrategy raises EnigmaClientException with INVALID_QUERY for one, and introspect_schema raises its "no schema data" RuntimeError.
  • A query written with no space after an opening brace, such as node{name}, is sent correctly. The client used to join the id and __typename it adds to the first field, and the API rejected the query.
  • EnigmaClientException.status_code holds the HTTP status of the failed LLM provider or hosted-service call behind an error, when there was one, so retry logic needs no message parsing. For the hosted service, EnigmaClientException.headers holds its response headers. A mapped provider failure keeps the provider's error as __cause__. No error code changes for an HTTP status.
  • An API key or header value an HTTP header can't carry as written, such as one with a newline or a trailing space, raises ValueError naming the header, before the API request is sent. It replaces the httpx error that could quote the value, which an except httpx.HTTPError handler no longer catches, and for a character outside ASCII the UnicodeEncodeError, which an except UnicodeEncodeError handler no longer catches. introspect_schema, which sent an API key outside ASCII encoded as UTF-8, now refuses it the same way.
  • A custom LlmProvider can raise ProviderHTTPError, now exported from enigma_api_client.strategies, with the HTTP status of a failure. 408, 429 and 5xx are reported as TRANSIENT_PROVIDER_ERROR, any other HTTP status as PROVIDER_ERROR, and a value that is not an HTTP status as TRANSIENT_PROVIDER_ERROR.

Upgrading from 0.10.1

  • Client.search asks the API to wait up to 25 seconds for values it computes in the background, such as some counts, before it answers. The API answers a request with no background work at once. A ClientConfig.url that sets max_background_task_wait or max_call_duration keeps its own wait.
  • SearchResponse.is_success() is false for an HTTP 206 answer, whose background values are still null. It is also false for an answer that reports a background task that failed, was cancelled or ran out of credits.
  • TemplateStrategy's brand watchlist template selects each watchlist entry's id in place of internalWatchlistEntryId, which the API is retiring. The two identifiers differ, so ids you stored from internalWatchlistEntryId won't match.

Upgrading from 0.10.0

  • An unavailable_data entry for something the selected government records answer only in part, such as "tax liens", gets the message "The returned records cover this only in part." when the query selects those records.
  • The phrases "work permit", "export permit", "licensing agreement" and "drivers' license" no longer ask for government records.

Upgrading from 0.9.14

  • With TemplateStrategy, GenerativeStrategy or AgentStrategy, a question that asks for an entity's government or public records returns a query that selects them. Each returned entity carries the fields listed under Records on Each Entity. Beside a search by a name, person or street address, the response gains archiveRecords, plus archiveBrandRecords beside a brand or location search. A question that asks for no records returns the same query as in 0.9.14.
  • unavailable_data leaves out an entry the selected records answer in full, such as "liquor licenses" or "violations". When the SDK adds record counts but none of the records the question asks for, such an entry carries the message "This query does not return these records." When the SDK adds no record fields, a violations entry says that violation records are served as the Gov Archive records of a business, location, person or address.

Upgrading from 0.9.13

  • The query reference AgentStrategy reads no longer lists internalId, which the API is retiring on every type.

Upgrading from 0.9.12

  • Generated queries select pageInfo { hasNextPage endCursor } on Brand.affiliatedBrands and LegalEntity.legalEntities, so a corporate family longer than one page can be paged with after:.

Upgrading from 0.9.11

  • TemplateStrategy lookups return a core set of fields plus only the data the question asks for (see TemplateStrategy). A plain "verify" question no longer returns TINs, watchlist entries, bankruptcies or officers; ask for them, or ask a compliance question.
  • TemplateStrategy's legal entity lookup searches by name only, and reads a state in the question as the registrations to list in that state.
  • TemplateStrategy can refuse a question about several businesses or people at once with UNSUPPORTED_DATA_REQUESTED. From 0.12.0 it answers up to three at once (see Upgrading from 0.11.0).
  • With TemplateStrategy, a question with no usable name, website or person name raises UNSUPPORTED_DATA_REQUESTED instead of being sent as an empty search.
  • TemplateStrategy's brand profiles select operatingLocations in place of the hqAddress alias, so the response key changes, and no longer select isMarketables.
  • TemplateStrategy answers an officers question about a name of more than one word that ends in Inc, Incorporated, Co, Company, LLC, LP, LLP, LLLP or PLLC, with no website, by searching legal entities. search then returns a LegalEntity whose officers are under registeredEntities, registrations and roles. Other names, including names ending in Corp, Corporation or Ltd, still search brands.

Upgrading from 0.9.10

  • restricted_fields_for and gate_blocked_fields also list placeClasses, siteTypes and tiers for the EXTERNAL and REDISTRIBUTION tiers, and legalEntitiesAppearsOn and legalEntitiesIsFlaggedBy for REDISTRIBUTION; the API does not serve them to those tiers.
  • TemplateStrategy answers a question about how many businesses of a category are in a place with a count instead of refusing it.

Upgrading from 0.9.9

  • restricted_fields_for and gate_blocked_fields no longer list headcounts for the EXTERNAL and REDISTRIBUTION tiers; the API serves it to every tier. A headcount question on a brand or legal entity now selects each legal entity's headcount band instead of declaring headcount in unavailable_data. For a query that is not a company or legal entity lookup, TemplateStrategy and GenerativeStrategy still declare employees (out_of_scope); TemplateStrategy's entry has a new message.

Upgrading from 0.9.8

  • The SDK also calls a schema introspector registered with enigma_api_client.validation.set_schema_introspector, including the one Client registers, from a background thread in an empty context. It does so when a strategy needs a tier's schema and none is cached yet, and on the first use of a cached schema that is more than five minutes old. The introspector must be thread-safe, return in bounded time and not rely on context variables. set_schema_introspector(introspector, refresh_seconds=None) turns off the refresh of cached schemas; a missing schema is still fetched in the background. The introspector Client registers uses the five-minute interval.
  • Whatever address_search says, a strategy refuses an address search, with the reason bare address lookup, while the cached schema for the access tier shows no support for address lookups (in 0.9.8, one that lacks Address.associatedBrandCount; see Upgrading from 0.10.3). With address_search=True it still serves one while no schema is cached.
  • TemplateStrategy output and answers from the query shape cache drop any field your tier's schema doesn't define, besides the restricted_fields_for fields, and declare each in unavailable_data as contractual.

Upgrading from 0.9.7

  • restricted_fields_for and gate_blocked_fields list tins, watchlistEntries, isFlaggedByWatchlistEntries and appearsOnWatchlistEntries for the REDISTRIBUTION tier; the API no longer serves them to it.
  • GenerativeStrategy, TemplateStrategy and AgentStrategy drop a field the tier is not served from the query they return and declare it in unavailable_data (contractual, keyed by the field's schema name), with restricted-field in deterministic_repair. A query left with nothing else, or whose filter, orderBy or field argument path still names such a field, raises UNSUPPORTED_DATA_REQUESTED.
  • ClientConfig.tier (default AccessTier.REDISTRIBUTION) is the tier a local strategy generates for and validates against. Set it to your key's tier to keep TIN and watchlist selections with an external key; ServiceStrategy ignores it. Schema validation is process wide and follows the first Client's tier, so a later Client at another tier validates without the schema and its AgentStrategy raises PROVIDER_ERROR. Client.search passes tier= to generate_query when ClientConfig.tier is not the default, so a custom QueryStrategy must accept a tier keyword argument.

Upgrading from 0.9.1

  • __url links point at the Enigma Console's profile pages: https://console.enigma.com/profile/brand/<id>, /profile/legal-entity/<id> and /profile/operating-location/<id>. Old links still redirect. A person's __url links to the brand it appears under, or to Enigma's website when it appears under none, instead of a person profile.

Upgrading from 0.9.0

  • TemplateStrategy counts a brand's locations ("How many Starbucks are in Texas?") with a brand search, so the count moves from data.aggregate.locationsCount to data.search[0].locationsCount, brandsCount is no longer returned, and search is empty when no brand matches. The count covers only that brand's open locations in the state and city the question names; before, it counted every open location, whatever the brand.

Upgrading from 0.8.x

  • AgentStrategy runs on Amazon Bedrock only and takes the keyword arguments model, region, max_turns and max_budget_usd; anthropic_api_key is removed. The calling identity needs bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream on the model.
  • AgentStrategy raises instead of returning an empty query:
    • UnrepairedQueryError when its answer fails schema validation or does not parse.
    • UNSUPPORTED_DATA_REQUESTED from the refusal checks GenerativeStrategy applies.
    • INVALID_QUERY for an empty prompt or answer (the "not applicable" refusal), a run that ends without a query, an answer that doesn't match the agent's answer format, a mutation or subscription, an answer whose variables do not fit their declared types, one nested too deeply, one a check could not read, or one over 10,000 characters.
    • TRANSIENT_PROVIDER_ERROR or PROVIDER_ERROR when the model call or the CLI fails.
  • AgentStrategy records its token usage in GENERATION_USAGE.
  • The anthropic and all extras pin an exact claude_agent_sdk version (currently 0.2.121).
  • AgentStrategy serves its answer inline with {} variables, refuses the call before the agent runs when the tier has no schema (PROVIDER_ERROR), and takes address_search= like GenerativeStrategy.

Upgrading from 0.7.x or Earlier

  • Custom LlmProvider: complete() now returns enigma_api_client.strategies.Completion(text, truncated, input_tokens=..., cache_read_tokens=..., cache_write_tokens=..., output_tokens=...) instead of str.
  • Custom QueryStrategy: generate_query(prompt, *, tier=DEFAULT_TIER, prior_attempt=None) gained prior_attempt. Implementations must set GENERATION_USAGE to a fresh LlmUsage() on entry.
  • GenerativeStrategy raises TruncatedOutputError and UnrepairedQueryError. Both subclass EnigmaClientException with code INVALID_QUERY, so existing handlers still catch them.
  • LLM provider transport failures now raise EnigmaClientException: TRANSIENT_PROVIDER_ERROR for timeouts and connection errors, GENERATE_QUERY_CALLBACK_NOT_CONFIGURED for configuration faults. They used to surface as raw httpx exceptions.
  • restricted_fields_for and gate_blocked_fields no longer list isMentionedInMediaContents, and now list headcounts, corporateFamilies and customs for the EXTERNAL and REDISTRIBUTION tiers.
  • Address lookups are new and default to auto (see Look Up an Address). With ENIGMA_ADDRESS_SEARCH=off, a question that gives only an address raises UNSUPPORTED_DATA_REQUESTED; 0.7.11's TemplateStrategy sent it as a nameless operating location search that the API rejected with HTTP 400.
  • GenerativeStrategy now refuses some drafts with UNSUPPORTED_DATA_REQUESTED instead of serving them: a person search by surname alone, an EIN or filing number placed in name, a wildcard name pattern, a street address placed in name, and a category count offered for a listing request.
  • AgentStrategy logs its transcript at DEBUG on enigma_api_client.strategies.agent, not at INFO on the root logger.
  • TemplateStrategy refuses a blank prompt with INVALID_QUERY before any LLM call.

Metadata

Release files for enigma-api-client 0.12.0

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

Built distribution (wheel)

Table of built distributions (wheels) for enigma-api-client 0.12.0
File Interpreter ABI Platform Actions
enigma_api_client-0.12.0-py3-none-any.whl Python 3 none any Details for enigma_api_client-0.12.0-py3-none-any.whl

Release files / enigma_api_client-0.12.0-py3-none-any.whl

Download URL enigma_api_client-0.12.0-py3-none-any.whl
Size 336.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
330a2062055170c7bf9fb8754e37a73515f4c90d6f7c0bf5efb5b8ef8b758ba7
BLAKE2b-256 checksum
How to use checksums
c98459a5f3d5e48d06f9d83f492edbdcf76d131114fa47edaadebf24a4042e1c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.13

Security for enigma-api-client 0.12.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