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
Clientand reuse it for all your requests. It keeps connections open until you close it, either withasync with Client(config) as client:or withawait 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())withawait main(). Start Jupyter from a shell whereENIGMA_API_KEYis 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
datais filled in, so a 206 answer is still usable. - An answer whose
extensionsreport 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
searchreturns a list of entities.__typenamesays what each one is:Brand,LegalEntity,OperatingLocation,PersonorAddress.- A question that names several businesses, people or street addresses gets one search for each, answered under
search,search2andsearch3. A person named at a company, or a street address given for a business, shares that business's search.GenerativeStrategyandAgentStrategyhave an LLM write the query and its keys, so with them read every key ofdata. A template query's variables are numbered like its keys:searchInput,searchInput2andsearchInput3, and likewise any other variable, such aslocConditions2. - In a template query (
ServiceStrategy's default, orTemplateStrategy), 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
registrationsInStatein each of itsregisteredEntities: 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 aslocationsCountandbrandsCount. A count of a brand's locations depends on how the query is written. From a template (ServiceStrategy's default, orTemplateStrategy) it comes aslocationsCounton the brand insearch, one search per brand (search,search2and so on). FromGenerativeStrategy, orServiceStrategy(query_strategy="generative"), it comes aslocationsCountinaggregate, one per brand (aggregate,aggregate2and so on). - A
Brandis a business as its customers know it, such as Chipotle. ALegalEntityis a company as a government registers it, such as Chipotle Mexican Grill, Inc. AnOperatingLocationis 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:
idand__typenameon 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 itsedgescarries one. It also skips an entity selected only through a named fragment at the top ofsearch, as insearch(...) { ...BrandFields }. Selectidand__typenameyourself in a skipped selection to get__url.__urlon each brand, legal entity and operating location that has anidand a__typename: a link to its profile in the Enigma Console. A person's__urllinks 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. UseGenerativeStrategyfor 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:InvokeModelandbedrock:InvokeModelWithResponseStreamon 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 isregion=, elseAWS_REGIONorAWS_DEFAULT_REGION, elseus-east-1. Passmodel=andregion=to change them, and keep the region inside the profile's geography. max_turns(default 8) andmax_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.
GenerativeStrategyis told to write every value into the query text, andAgentStrategyalways does. - Call
generate_queryinside theasync withblock. A local strategy checks its query against the API schema, which theClientfetches with your key. - Called directly, a local strategy's
generate_querywrites forAccessTier.REDISTRIBUTIONunless you pass your tier, as ingenerate_query(prompt, tier=AccessTier.EXTERNAL).client.searchpassesClientConfig.tierfor you.ServiceStrategyalways 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.REDISTRIBUTIONis the narrowest tier.AccessTier.EXTERNALadds 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_datawith the reasoncontractual.enigma_api_client.restricted_fields_for(tier)returns those fields. enigma_api_client.gate_blocked_fields(tier)returns those fields plus, forAccessTier.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 usegate_blocked_fieldsto filter output you pass on.- The SDK fetches the API schema for the
tierof the firstClientyou create in a process, with that client's key, and checks generated queries against it. Settierbefore you create your firstClient. In a notebook, restart the kernel after you change it. - A later
Clientwith a differenttiergets 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=TrueorENIGMA_ADDRESS_SEARCH=on). AgentStrategyraisesPROVIDER_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:GenerativeStrategyorAgentStrategywrote a query that failed the schema check even after repair. It carries.draft,.errorsand.tier, and you can pass it to anotherGenerativeStrategyasgenerate_query(prompt, prior_attempt=exc)to start from that draft. Its message quotes values from the generated query, so log the exception type anderror_coderather thanstr(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()andresponse.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-Afterheader,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
dataset toNone, an error inerrorsand the body inraw_body, sois_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
ValueErrornaming the header, before the API request is sent. - Network failures, from the API or the hosted query service, raise
httpxexceptions such ashttpx.TimeoutExceptionandhttpx.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
cursorset toNone. - Use
hasNextPageto decide whether more pages exist. A full page doesn't mean there are more. - Keep the connection's
firstandconditions(with itsfilterandorderBy) the same on every page, because a cursor is only valid for the arguments it came from. ClientConfig(auto_inject_page_info=True)addspageInfo { hasNextPage endCursor }to every connection that doesn't already selectpageInfo, in every query the client sends, including generated ones.- Generated queries already select page info (for
GenerativeStrategyandAgentStrategy, 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 onBrand.operatingLocationstoo. The legal entity lookup selects it onregistrationsInStatewhen it lists registrations in several states, which share one page. - To read more pages of a generated query, get it with
generate_queryand addafter: $cursorto the connection. Then declare$cursor: Stringwith 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 withquery ($cursor: String) {when it has no name. rolesdoesn'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,stateandzip. - 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
street2in 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.
- It moves a suite, floor or apartment into
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
recordspage, a question for them often returns few or none. - Set
ENIGMA_ARCHIVE_SEARCH=offto 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 asucc_lienorabc_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_dataentry 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
recordspage, 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, whichClientConfig.urldoesn'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. searchreturns a list. Select through inline fragments, as insearch(...) { ... on Brand { ... } }; it has noedges.- A category in
namematches business names.name: "coffee shop"returns businesses with names like "Coffee Shop". To count a category, useaggregate. - A company's officers are under its registrations.
LegalEntity.personsis almost always empty for a company. FollowregisteredEntities, thenregistrations, thenroles, thenlegalEntities. - Connections have no
totalCount. Count with anaggregatequery, or withcount(field: ...)on an entity. - Website content sits under each website. Select
websiteContentsinsidewebsites:websites(first: 1) { edges { node { websiteContents(first: 1) { edges { node { httpStatusCode } } } } } }. - A brand search for a local franchise returns the whole chain. Searching
BRANDfor a single franchised hotel returns the parent brand with all its locations. SearchOPERATING_LOCATIONwith 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.
TemplateStrategyanswers a question that names several businesses, people or street addresses with one search for each in the same query, answered undersearch,search2andsearch3, where it answered one of them or refused the question.GenerativeStrategylikewise searches each brand a question names, keyedsearch,search2andsearch3, where it usually searched the first brand only and otherwise keyed the searches with other aliases such asaandb. Code that reads onlydata["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, andServiceStrategyraisesINVALID_QUERYonce the hosted service adopts the release. GenerativeStrategycounts each brand a how-many question names, underaggregate,aggregate2and so on, where it counted the first brand only.GenerativeStrategyandAgentStrategyrefuse 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
GenerativeStrategyasks 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,
TemplateStrategysearches 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
GenerativeStrategyorAgentStrategyquery that repeats a root field with different arguments, the second one is keyedsearch2(oraggregate2), where it wassearch_2.
Upgrading from 0.10.3
- Address lookups no longer select the five
Addresscount fields (associatedBrandCount,operatingLocationOperatesAtCount,legalEntityReceivesMailAtCount,registrationRecordedCountandassociatedPersonCount). An address result no longer carries those keys, so code that readsaddress["associatedBrandCount"]raisesKeyError; read the operating locations, legal entities and registrations each address lists instead. WithClient'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 anAddresstype and theNodeinterface as one that supports address lookups, besides one whoseAddresstype carriesassociatedBrandCount.- When a question asks how many, other than a headcount, and the query is an address lookup that selects no count,
unavailable_datahas anaddress countsentry (out_of_scope).
Upgrading from 0.10.2
SearchResponse.headersholds the HTTP response headers, so you can readRetry-Afteron a 429. It takes no part in==orrepr;dataclasses.asdict(response)anddataclasses.astuple(response)now include it as anhttpx.Headers, whichjson.dumpscan't serialize.- A response body that isn't a JSON object, such as a gateway's error page, no longer raises
ValueError(orAttributeErrorfor JSON that isn't an object).client.searchreturns it withdataset toNone, an error inerrorsand the body inraw_body, sois_success()is false.ServiceStrategyraisesEnigmaClientExceptionwithINVALID_QUERYfor one, andintrospect_schemaraises 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 theidand__typenameit adds to the first field, and the API rejected the query. EnigmaClientException.status_codeholds 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.headersholds 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
ValueErrornaming the header, before the API request is sent. It replaces the httpx error that could quote the value, which anexcept httpx.HTTPErrorhandler no longer catches, and for a character outside ASCII theUnicodeEncodeError, which anexcept UnicodeEncodeErrorhandler no longer catches.introspect_schema, which sent an API key outside ASCII encoded as UTF-8, now refuses it the same way. - A custom
LlmProvidercan raiseProviderHTTPError, now exported fromenigma_api_client.strategies, with the HTTP status of a failure. 408, 429 and 5xx are reported asTRANSIENT_PROVIDER_ERROR, any other HTTP status asPROVIDER_ERROR, and a value that is not an HTTP status asTRANSIENT_PROVIDER_ERROR.
Upgrading from 0.10.1
Client.searchasks 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. AClientConfig.urlthat setsmax_background_task_waitormax_call_durationkeeps 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'sidin place ofinternalWatchlistEntryId, which the API is retiring. The two identifiers differ, so ids you stored frominternalWatchlistEntryIdwon't match.
Upgrading from 0.10.0
- An
unavailable_dataentry 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,GenerativeStrategyorAgentStrategy, 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 gainsarchiveRecords, plusarchiveBrandRecordsbeside a brand or location search. A question that asks for no records returns the same query as in 0.9.14. unavailable_dataleaves 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, aviolationsentry 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
AgentStrategyreads no longer listsinternalId, which the API is retiring on every type.
Upgrading from 0.9.12
- Generated queries select
pageInfo { hasNextPage endCursor }onBrand.affiliatedBrandsandLegalEntity.legalEntities, so a corporate family longer than one page can be paged withafter:.
Upgrading from 0.9.11
TemplateStrategylookups 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.TemplateStrategycan refuse a question about several businesses or people at once withUNSUPPORTED_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 raisesUNSUPPORTED_DATA_REQUESTEDinstead of being sent as an empty search. TemplateStrategy's brand profiles selectoperatingLocationsin place of thehqAddressalias, so the response key changes, and no longer selectisMarketables.TemplateStrategyanswers 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.searchthen returns aLegalEntitywhose officers are underregisteredEntities,registrationsandroles. Other names, including names ending in Corp, Corporation or Ltd, still search brands.
Upgrading from 0.9.10
restricted_fields_forandgate_blocked_fieldsalso listplaceClasses,siteTypesandtiersfor theEXTERNALandREDISTRIBUTIONtiers, andlegalEntitiesAppearsOnandlegalEntitiesIsFlaggedByforREDISTRIBUTION; the API does not serve them to those tiers.TemplateStrategyanswers 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_forandgate_blocked_fieldsno longer listheadcountsfor theEXTERNALandREDISTRIBUTIONtiers; 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 inunavailable_data. For a query that is not a company or legal entity lookup,TemplateStrategyandGenerativeStrategystill declareemployees(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 oneClientregisters, 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 introspectorClientregisters uses the five-minute interval. - Whatever
address_searchsays, a strategy refuses an address search, with the reasonbare address lookup, while the cached schema for the access tier shows no support for address lookups (in 0.9.8, one that lacksAddress.associatedBrandCount; see Upgrading from 0.10.3). Withaddress_search=Trueit still serves one while no schema is cached. TemplateStrategyoutput and answers from the query shape cache drop any field your tier's schema doesn't define, besides therestricted_fields_forfields, and declare each inunavailable_dataascontractual.
Upgrading from 0.9.7
restricted_fields_forandgate_blocked_fieldslisttins,watchlistEntries,isFlaggedByWatchlistEntriesandappearsOnWatchlistEntriesfor theREDISTRIBUTIONtier; the API no longer serves them to it.GenerativeStrategy,TemplateStrategyandAgentStrategydrop a field the tier is not served from the query they return and declare it inunavailable_data(contractual, keyed by the field's schema name), withrestricted-fieldindeterministic_repair. A query left with nothing else, or whose filter,orderByorfieldargument path still names such a field, raisesUNSUPPORTED_DATA_REQUESTED.ClientConfig.tier(defaultAccessTier.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;ServiceStrategyignores it. Schema validation is process wide and follows the firstClient's tier, so a laterClientat another tier validates without the schema and itsAgentStrategyraisesPROVIDER_ERROR.Client.searchpassestier=togenerate_querywhenClientConfig.tieris not the default, so a customQueryStrategymust accept atierkeyword argument.
Upgrading from 0.9.1
__urllinks 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__urllinks 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
TemplateStrategycounts a brand's locations ("How many Starbucks are in Texas?") with a brand search, so the count moves fromdata.aggregate.locationsCounttodata.search[0].locationsCount,brandsCountis no longer returned, andsearchis 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
AgentStrategyruns on Amazon Bedrock only and takes the keyword argumentsmodel,region,max_turnsandmax_budget_usd;anthropic_api_keyis removed. The calling identity needsbedrock:InvokeModelandbedrock:InvokeModelWithResponseStreamon the model.AgentStrategyraises instead of returning an empty query:UnrepairedQueryErrorwhen its answer fails schema validation or does not parse.UNSUPPORTED_DATA_REQUESTEDfrom the refusal checksGenerativeStrategyapplies.INVALID_QUERYfor 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_ERRORorPROVIDER_ERRORwhen the model call or the CLI fails.
AgentStrategyrecords its token usage inGENERATION_USAGE.- The
anthropicandallextras pin an exactclaude_agent_sdkversion (currently 0.2.121). AgentStrategyserves its answer inline with{}variables, refuses the call before the agent runs when the tier has no schema (PROVIDER_ERROR), and takesaddress_search=likeGenerativeStrategy.
Upgrading from 0.7.x or Earlier
- Custom
LlmProvider:complete()now returnsenigma_api_client.strategies.Completion(text, truncated, input_tokens=..., cache_read_tokens=..., cache_write_tokens=..., output_tokens=...)instead ofstr. - Custom
QueryStrategy:generate_query(prompt, *, tier=DEFAULT_TIER, prior_attempt=None)gainedprior_attempt. Implementations must setGENERATION_USAGEto a freshLlmUsage()on entry. GenerativeStrategyraisesTruncatedOutputErrorandUnrepairedQueryError. Both subclassEnigmaClientExceptionwith codeINVALID_QUERY, so existing handlers still catch them.- LLM provider transport failures now raise
EnigmaClientException:TRANSIENT_PROVIDER_ERRORfor timeouts and connection errors,GENERATE_QUERY_CALLBACK_NOT_CONFIGUREDfor configuration faults. They used to surface as rawhttpxexceptions. restricted_fields_forandgate_blocked_fieldsno longer listisMentionedInMediaContents, and now listheadcounts,corporateFamiliesandcustomsfor theEXTERNALandREDISTRIBUTIONtiers.- Address lookups are new and default to
auto(see Look Up an Address). WithENIGMA_ADDRESS_SEARCH=off, a question that gives only an address raisesUNSUPPORTED_DATA_REQUESTED; 0.7.11'sTemplateStrategysent it as a nameless operating location search that the API rejected with HTTP 400. GenerativeStrategynow refuses some drafts withUNSUPPORTED_DATA_REQUESTEDinstead of serving them: a person search by surname alone, an EIN or filing number placed inname, a wildcard name pattern, a street address placed inname, and a category count offered for a listing request.AgentStrategylogs its transcript at DEBUG onenigma_api_client.strategies.agent, not at INFO on the root logger.TemplateStrategyrefuses a blank prompt withINVALID_QUERYbefore 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.