Metadata-Version: 2.5
Name: krenzo
Version: 0.1.0
Summary: Search and extract API for LLM agents — clean, token-efficient web context.
Project-URL: Homepage, https://krenzo.in
Project-URL: Documentation, https://krenzo.in/docs
License: MIT
License-File: LICENSE
Keywords: agents,llm,rag,scraping,search,web-extraction
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# krenzo

Search and extract API for LLM agents. One endpoint for live web search, one
for turning any URL into clean markdown — returned as token-efficient context
rather than raw HTML.

No runtime dependencies.

```bash
pip install krenzo
```

## Quickstart

Get a key at [krenzo.in/dashboard/keys](https://krenzo.in/dashboard/keys), then:

```bash
export KRENZO_API_KEY=krz_live_...
```

```python
from krenzo import Krenzo

client = Krenzo()

for hit in client.search("nvidia q3 earnings", max_results=5):
    print(hit.source, "—", hit.title)
```

## Search

```python
results = client.search(
    "western digital analyst price target",
    depth="deep",              # also returns page contents for the top results
    max_results=10,
    include_domains=["stockanalysis.com"],
    freshness="week",
)

for r in results:
    print(r.title, r.url)
    if r.content:              # populated when depth="deep"
        print(r.content[:500])
```

`depth="deep"` fetches and extracts the top results in the same call, which
saves a round trip per result when you were going to read them anyway.

## Extract

```python
pages = client.extract([
    "https://example.com/a",
    "https://example.com/b",
])

for p in pages:
    if p.ok:
        print(p.title, p.word_count)
        print(p.content)
    else:
        print("failed:", p.url, p.error_type, p.error_message)
```

Each URL carries its own `ok`/`error`, so one dead link does not discard the
rest of the batch. Pass `format="text"` for plain text instead of markdown, and
`render=True` for pages that block plain clients or build their content in the
browser.

### When an empty page is not an empty page

A site that blocks you and a site with nothing on it look identical downstream
— and only one of them is a bug in your parser. `extract_one` makes the
difference explicit:

```python
from krenzo import Krenzo, BlockedError

client = Krenzo()

try:
    text = client.extract_one("https://www.tipranks.com/stocks/wdc/forecast")
except BlockedError as e:
    print("needs a browser or was refused:", e)
```

Batch `extract()` reports the same thing without raising: check
`p.needs_rendering` and `p.error_type`.

## Answer

A grounded answer in one call, with every quote checked against the page it
came from.

```python
res = client.answer("What did the RBI decide on the repo rate?", max_sources=4)

print(res.answer)
for c in res.citations:
    print(f"  — {c.quote}\n    {c.url}")
```

The model proposes quotes; the service verifies each one against the fetched
source and drops anything it cannot find. A fabricated citation therefore
cannot reach you. Two fields are worth reading every time:

- `res.unverified` — quotes that were dropped. Empty means every claim was
  grounded; non-empty means the answer drifted from its sources.
- `res.skipped` — pages found but not readable, with a reason. An answer built
  on one of four sources is weaker than one built on four.

If the sources do not contain the answer, it says so and returns no citations
rather than guessing. Pass `render=True` to retry unreadable sources through a
browser; it is billed at the rendered rate, so it is off by default.

## Errors

| Exception | When |
|---|---|
| `AuthenticationError` | missing, malformed, or revoked key |
| `InsufficientCredits` | balance cannot cover the call — top up |
| `RateLimited` | too many requests; carries `retry_after` |
| `UpstreamError` | search index or origin failed; usually transient |
| `BlockedError` | page refused, or needs a browser (`extract_one` only) |
| `KrenzoError` | base class for all of the above |

Rate limits and upstream failures are retried automatically with backoff.
Authentication and credit errors are not — they fail identically on a retry.

## Usage and billing

Every call records what it cost:

```python
client.search("anything")
print(client.last_usage.balance_usd, client.last_usage.free_calls_remaining)
```

Calls are billed per call; `extract` is billed per URL, so a 20-URL batch is 20
calls. Failed requests are not billed, and a call that fails upstream is
refunded.

## Configuration

| | |
|---|---|
| `KRENZO_API_KEY` | your API key (or pass `api_key=`) |
| `KRENZO_API_URL` | override the base URL, for testing against a local server |

```python
client = Krenzo(api_key="krz_live_...", timeout=30, max_retries=3)
```

**Never hard-code a key in published source.** A key committed to a repository
is a key anyone can spend, and keys cannot be un-published. Read it from the
environment or a secret manager.

## License

MIT
