Metadata-Version: 2.4
Name: manifest-api
Version: 0.4.0
Summary: Python SDK for the Manifest API — structured action manifests for AI agents
Project-URL: Homepage, https://omfang.io/docs
Project-URL: Repository, https://github.com/omfang/manifest-api
Project-URL: Bug Tracker, https://github.com/omfang/manifest-api/issues
Author-email: Omfang AB <max@omfang.io>
License: MIT
Keywords: agents,ai,automation,browser,manifest,web
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Provides-Extra: langchain
Requires-Dist: langchain-core>=1.0; extra == 'langchain'
Description-Content-Type: text/markdown

# manifest-api

Stop guessing selectors. Manifest tells your agent what's clickable, fillable, and submittable on any page.

Python SDK for the [Manifest API](https://omfang.io/manifest-docs) — extracts structured action manifests from web pages so AI agents know *what they can do*, not just what's on screen.

| | Raw browser access | Content extraction | Manifest |
|---|---|---|---|
| Gives agents a browser | ✓ | ✗ | ✓ |
| Returns page content | ✓ | ✓ | ✓ |
| Returns available actions | ✗ | ✗ | ✓ |
| Required fields, input types | ✗ | ✗ | ✓ |
| Survives UI redesigns | ✗ | ✓ | ✓ |

## Install

```bash
pip install manifest-api
```

## Quickstart

### Sync

```python
from manifest_api import ManifestClient

client = ManifestClient(api_key="your-key")  # or set MANIFEST_API_KEY env var
manifest = client.get("https://example.com")

print(manifest.current_page_state)
print(manifest.actions)

# Convenience helpers
action = manifest.action("submit-form")
inputs = manifest.actions_of_type("input")
required = manifest.required_actions

# Cheap structural-change check (no LLM call, never cached)
fp = client.fingerprint("https://example.com")
print(fp.fingerprint)
```

### Async

```python
import asyncio
from manifest_api import AsyncManifestClient

async def main():
    async with AsyncManifestClient(api_key="your-key") as client:
        manifest = await client.get("https://example.com")
        print(manifest.current_page_state)

asyncio.run(main())
```

### LangChain

```bash
pip install "manifest-api[langchain]"
```

```python
from manifest_api.integrations.langchain import create_manifest_tool

tool = create_manifest_tool()  # reads MANIFEST_API_KEY from env
# or: create_manifest_tool(api_key="...")

# use with an agent, e.g.:
from langchain.agents import create_agent
agent = create_agent(model=..., tools=[tool])
```

`create_manifest_tool` wraps `AsyncManifestClient` as a `get_manifest(url)` `StructuredTool` — the API key is bound at creation time, so the LLM never sees it.

## All methods

```python
# Both ManifestClient and AsyncManifestClient expose:
manifest = client.get("https://example.com")          # POST /manifest → Manifest
manifest = client.get("https://example.com", country="US")  # POST /manifest → Manifest, Accept-Language: en-US,en;q=0.9
manifest = client.get("https://example.com", storage_state="storage_state.json")  # authenticated fetch, see below
fp       = client.fingerprint("https://example.com")  # POST /fingerprint → Fingerprint
health   = client.health()                             # GET  /health → dict
valid    = client.session_valid()                      # GET  /session-status → bool
```

## Manifest helpers

```python
manifest.action("id")              # → Action | None
manifest.actions_of_type("input")  # → list[Action]
manifest.required_actions          # → list[Action]
manifest.blocked_actions([...])    # → list[Action], see "Action dependencies" below
manifest.fingerprint               # → str | None, same hash `fingerprint()` returns
```

## Fingerprint

`fingerprint(url)` runs the same page-render + extraction pipeline as
`get(url)` but skips the LLM translation step, returning just a stable hash
of the page's interactive surface (`Fingerprint.fingerprint`). Useful for
cheaply polling whether a page's action surface has changed since your last
`get()` call, without paying for another manifest generation. Every `Manifest`
also carries this same hash on `manifest.fingerprint`, so you can compare it
against a later `fingerprint()` call directly.

```python
fp = client.fingerprint("https://example.com")
print(fp.url, fp.fingerprint)
```

## Geo/locale

```python
manifest = client.get("https://example.com", country="US")  # ISO 3166-1 alpha-2
print(manifest.requested_country, manifest.served_locale, manifest.locale_mismatch)
```

`country` sets the scan's `Accept-Language` header to steer server-side
geo/locale redirects (e.g. a store redirecting to a different country's
pricing). This is a first attempt, not a guarantee — some sites geo-route on
IP and ignore `Accept-Language` entirely, no proxy routing yet. Check
`manifest.locale_mismatch`: `True` means a `country` was requested and the
page actually captured (`manifest.served_locale`, best-effort from the URL)
didn't match — an honest flag, not corrected pricing/currency data.

## Authenticated pages

If your agent already holds a logged-in session for the target site, pass its
Playwright `storage_state` so the scan perceives the authenticated view instead
of the logged-out wall:

```python
# a dict, or a path to a storage_state JSON file
manifest = client.get("https://app.example.com/dashboard", storage_state="storage_state.json")
manifest = client.get("https://app.example.com/dashboard", storage_state={"cookies": [...], "origins": [...]})
```

Capture `storage_state` from your own browser automation once it's logged in:

```python
context.storage_state(path="storage_state.json")   # Playwright
```

What changes for an authenticated request:

- **`manifest.cache_status` is `"bypass"`** — authenticated responses are never
  read from or written to the shared cache, so one caller's logged-in manifest
  can't be served to another. (`fresh` is implied; you don't need to pass it.)
- **`manifest.session_domains`** lists the domains/origins your `storage_state`
  is scoped to (names only, never cookie values) — a quick check that you
  didn't hand over a broader session than the task needs.
- A **stale or invalid session** raises `APIError` (HTTP 503) with a message
  that says the *supplied* session failed — it never silently falls back to an
  anonymous fetch and hands you a logged-out manifest you can't distinguish
  from a real one.

The caller supplies an already-valid session — Manifest does no login
automation, credential capture, 2FA/challenge handling, or session refresh, and
stores no credential profiles.

**Privacy note.** An authenticated page's content — which may include PII — is
sent to Anthropic for the extraction step, the same path every `get()` call
uses. Manifest does not persist page content.

**Security note.** A supplied `storage_state` is held in memory for that one
request only. It is never written to disk, a database, a cache, or a queue; it
is scrubbed from request logs, error responses, and traces; and it is dropped
when the request's browser context closes.

> Authenticated fetch is available in this Python SDK only. The JavaScript SDK
> has no `storageState` parameter yet.

## Action dependencies

Some actions are disabled until others are completed (e.g. a submit button
gated on required fields). `Action.requires` lists the ids of actions that
must be completed first; `blocked_actions()` filters a manifest down to
actions not yet unblocked by a given set of completed ids:

```python
completed = ["email-input"]
still_blocked = manifest.blocked_actions(completed)  # actions still waiting on something
```

`requires` is inferred by the LLM translation step from DOM signals (disabled
attributes, `aria-disabled`, form field proximity) — it's best-effort, not a
guaranteed-accurate dependency graph, and won't capture custom JS validation
logic.

## Action types

`button` · `input` · `textarea` · `select` · `checkbox` · `radio` · `other`

## Locators

Each action may carry a `locator` with `css`, `role`, and `name` — enough to
find and act on the underlying element without guessing a selector yourself.
It's best-effort: `locator` is `None` if no element on the page plausibly
matched the action.

Prefer `role`/`name` over `css` where possible — they hold up better across
redesigns, since `css` can be a brittle positional fallback when the element
has no `id` or `name` attribute.

```python
action = manifest.action("continue")
if action.locator and action.locator.css:
    page.click(action.locator.css)
```

## Error handling

```python
from manifest_api import AuthenticationError, RateLimitError, APIError

try:
    manifest = client.get("https://example.com")
except AuthenticationError:
    print("Check your API key")
except RateLimitError:
    print("Slow down — rate limit hit")
except APIError as e:
    print(f"Server error {e.status_code}")
```

## Docs

[https://omfang.io/manifest-docs](https://omfang.io/manifest-docs)
