Metadata-Version: 2.4
Name: enigma-api-client
Version: 0.4.1
Summary: Enigma's Public API SDK
Author-email: Enigma Technologies <info@enigma.com>
License-Expression: Apache-2.0
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx==0.28.1
Requires-Dist: pyyaml==6.0.3
Requires-Dist: pydantic<3.0,>=2.0
Provides-Extra: anthropic
Requires-Dist: claude_agent_sdk>=0.1.44; extra == "anthropic"
Dynamic: license-file

# Enigma API Client

Python SDK for the Enigma GraphQL business intelligence API. Query Enigma's database of 250M+ businesses using raw GraphQL or natural language prompts.

## Installation

```bash
# Core client (raw GraphQL only)
pip install enigma-api-client

# With Claude agent (skill-based generation, requires Anthropic API key)
pip install enigma-api-client[anthropic]
```

## Quick Start
The client supports both raw GraphQL queries and natural language queries.
* Below example showcases the raw GraphQL approach.
* Subseqent examples show how to use the natural language strategies.

### Using GraphQL Queries

```python
import asyncio
from enigma_api_client import Client, ClientConfig

client = Client(ClientConfig(enigma_api_key="your-api-key"))

async def main():
    # Example, sending off a handwritten GraphQL query
    response = await client.search(
        query="""query {
          search(searchInput: { name: "Starbucks", entityType: BRAND }) {
            ... on Brand {
              id
              names(first: 1) { edges { node { name } } }
            }
          }
        }"""
    )
    print(response.data)

asyncio.run(main())
```

### Natural Language Query strategies

The client supports three strategies for converting natural language prompts into GraphQL queries. Pass a strategy via `ClientConfig.query_strategy` to enable `client.search(prompt="...")`.

All strategies require the `ANTHROPIC_API_KEY` environment variable. The AgentStrategy additionally requires the `claude_agent_sdk` package (`pip install enigma-api-client[anthropic]`).

#### Service Strategy (default)
Will execute a http request to perform query generation using any of the strategies below on the server side.

```python
import asyncio
from enigma_api_client import Client, ClientConfig

client = Client(ClientConfig(
    enigma_api_key="your-api-key",
    # will use ServiceStrategy by default
))

async def main():
    # Brand lookup
    response = await client.search(prompt="Tell me about Chipotle")
    print(response.data)

    # Location search
    response = await client.search(prompt="Find Starbucks locations in California")
    print(response.data)

    # Business discovery
    response = await client.search(prompt="Find coffee shops in Denver")
    print(response.data)

    # Person search
    response = await client.search(prompt="Find companies owned by Elon Musk")
    print(response.data)

    # KYB verification
    response = await client.search(prompt="Verify legal entity for Enigma Technologies in NY")
    print(response.data)

asyncio.run(main())
```

#### TemplateStrategy — LLM-classified, template-built

Uses an LLM (via Anthropic Messages API) to classify the user's prompt into one of 13 pre-defined query templates and extract parameters, then assembles the GraphQL query deterministically from the matching template. Fast and reliable — no hallucinated field names since the GraphQL is built from verified templates.

```python
import asyncio
from enigma_api_client import Client, ClientConfig
from enigma_api_client.strategies import TemplateStrategy

client = Client(ClientConfig(
    enigma_api_key="your-api-key",
    query_strategy=TemplateStrategy(),
))

async def main():
    # Brand lookup
    response = await client.search(prompt="Tell me about Chipotle")
    print(response.data)

    # Location search
    response = await client.search(prompt="Find Starbucks locations in California")
    print(response.data)

    # Business discovery
    response = await client.search(prompt="Find coffee shops in Denver")
    print(response.data)

    # Person search
    response = await client.search(prompt="Find companies owned by Elon Musk")
    print(response.data)

    # KYB verification
    response = await client.search(prompt="Verify legal entity for Enigma Technologies in NY")
    print(response.data)

asyncio.run(main())
```

The template strategy covers query templates: brand profiles, brand lookups by website, locations, business discovery, person search, corporate officers, legal entity verification, contacts, watchlist screening, operating location profiles, aggregate counts, and risk assessment. Prompts that don't map to any template raise an `EnigmaClientException`.

#### GenerativeStrategy — end-to-end LLM query generation

Uses a single LLM call (via Anthropic Messages API) to generate the full GraphQL query and variables directly. The LLM receives the complete Enigma GraphQL API skill reference as context. More flexible than TemplateStrategy for complex or unusual queries that don't fit neatly into pre-defined templates, but slower and carries some risk of hallucinated field names.

```python
import asyncio
from enigma_api_client import Client, ClientConfig
from enigma_api_client.strategies import GenerativeStrategy

client = Client(ClientConfig(
    enigma_api_key="your-api-key",
    query_strategy=GenerativeStrategy(),
))

async def main():
    response = await client.search(
        prompt="Find all open Dunkin' Donuts locations in Massachusetts with phone numbers"
    )
    print(response.data)

asyncio.run(main())
```

#### AgentStrategy — Claude Agent SDK with skill

Uses the Claude Agent SDK (`claude_agent_sdk`) to generate GraphQL queries via an interactive agent that has access to the full `enigma-graphql` skill — including the schema, examples, field corrections, and pagination templates. The agent can read skill files, reason about the query, and iteratively refine its output. Produces the highest quality queries for complex scenarios but is the slowest option.

Requires: `pip install enigma-api-client[anthropic]`

```python
import asyncio
from enigma_api_client import Client, ClientConfig
from enigma_api_client.strategies import AgentStrategy

client = Client(ClientConfig(
    enigma_api_key="your-api-key",
    query_strategy=AgentStrategy(),
))

async def main():
    response = await client.search(
        prompt="Find all open Dunkin' Donuts locations in Massachusetts with phone numbers"
    )
    print(response.data)

asyncio.run(main())
```

## Strategy comparison

| Strategy | Approach | Speed | Quality | Extra deps |
|---|---|---|---|---|
| `TemplateStrategy` | LLM classifies → deterministic template build | ~1-3s | Reliable, no hallucination | None |
| `GenerativeStrategy` | LLM generates full GraphQL end-to-end | ~3-10s | Flexible, some hallucination risk | None |
| `AgentStrategy` | Claude agent with skill + tool access | ~10-60s | Highest quality, iterative refinement | `claude_agent_sdk` |

**When to use which:**

- **TemplateStrategy** (default): Best for production use. Covers 13 common query patterns with zero hallucination risk. The LLM only classifies and extracts parameters — the GraphQL is deterministic.
- **GenerativeStrategy**: Best for complex or unusual queries that don't fit the 13 templates. The LLM sees the full API reference and generates custom GraphQL. Good for prototyping and exploratory queries.
- **AgentStrategy**: Best for maximum quality when latency is not a concern. The agent can read examples, check field corrections, and reason about the query. Good for batch processing or one-off complex queries.

## Caveats: LLM-generated GraphQL

The Enigma GraphQL schema has several non-obvious conventions that make LLM-generated queries unreliable without guardrails. Understanding these is important regardless of which strategy you use.

### Why this is hard for LLMs

The Enigma schema has ~50 field names, 4 entity types, 10+ filter operators, and 6 connection patterns. Many field names are unintuitive — LLMs consistently guess wrong:

| LLMs guess | Actual field name |
|---|---|
| `status` | `operatingStatus` |
| `url` | `website` |
| `description` | `industryDesc` |
| `title` | `jobTitle` |
| `amount` | `projectedQuantity` |
| `averageRating` | `reviewScoreAvg` |
| `formationDate` | `formationYear` (Int, not Date) |

These cause HTTP 400 errors ("Cannot query field") that are silent failures if not handled.

### Structural pitfalls

Beyond field names, the graph structure has patterns that LLMs regularly get wrong:

- **`search` returns a flat list, not a connection.** LLMs write `search { edges { node { ... } } }` — the correct form is `search { ... on Brand { ... } }`.
- **`LegalEntity.persons` is almost always empty.** To find officers, you must traverse `registeredEntities → registrations → roles → legalEntities`. LLMs take the direct (empty) path.
- **Searching `OPERATING_LOCATION` by brand name returns nothing.** You must search `BRAND` and filter its `operatingLocations` connection. LLMs don't know this.
- **`name` vs `prompt` on SearchInput are completely different modes.** `name` does entity resolution ("Starbucks"); `prompt` does semantic discovery ("coffee shop"). Using `name: "coffee shop"` returns nothing useful.
- **`totalCount` does not exist on connections.** Requesting it causes HTTP 400. Use `aggregate` queries instead.
- **Cursor pagination has a first-page bug.** The first page must NOT declare `$cursor` as a variable; subsequent pages must. Requires two separate query strings.

### Data quality traps

Even syntactically valid queries can return silently wrong data:

- **Franchise resolution:** Searching `BRAND` for a local franchise (e.g., a single Days Inn) returns the parent chain's data — 1,969 locations rolled up instead of the single franchisee.
- **`websiteContents` is a connection, not a scalar.** Must use `websiteContents(first: 1) { edges { node { httpStatusCode } } }`.

### How the strategies handle this

**TemplateStrategy** avoids all of the above. The LLM only classifies the prompt into a template and extracts parameters (company name, state, city, etc.). The actual GraphQL is assembled from 13 hand-written, tested templates. Zero hallucination risk, but limited to the patterns those templates cover.

**GenerativeStrategy** sends the full API skill reference (~600 lines of documentation, guardrails, and examples) as system prompt context. This covers most pitfalls, but the LLM can still hallucinate field names or miss structural rules — especially for unusual queries. Validate generated queries before using them in production.

**AgentStrategy** has full access to the skill files, examples, field corrections reference, and can iteratively refine queries. Highest quality, but slowest and most expensive. The agent can introspect the schema if it encounters errors.

### Recommendations

- **For production automation**, use `TemplateStrategy` or raw GraphQL. The 13 templates cover the most common query patterns and are guaranteed correct.
- **For prototyping or one-off complex queries**, use `GenerativeStrategy` or `AgentStrategy` but always review the generated query before trusting the results.
- **Always check `response.is_success()`** and inspect `response.errors` before using `response.data`. A plausible-looking query can still fail at the API level.
