Metadata-Version: 2.5
Name: krenzo
Version: 0.1.2
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 build their content in the browser with
JavaScript.

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

A page that refused you and a page with nothing on it look identical
downstream — and only one of them is a bug in your parser. `extract_one` makes
the difference explicit, so you can skip the source rather than treat a refusal
as an empty document. It reports the refusal; it does not work around it.

```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 costs the same flat rate as any other call — it is off by default
because it is slower, not because it is dearer.

## 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)
```

**$0.04 per call**, flat across search, extract and answer — including
rendered fetches. `extract` is billed per URL, so a 20-URL batch is 20 calls
($0.80). Failed requests are not billed, and a call that fails upstream is
refunded automatically.

Billing and settlement are in INR (₹3.52 per call); the USD figure is a
conversion at a fixed reference rate. Treat the price above as indicative and
`last_usage.charged_paise` as authoritative — it is what you were actually
charged, and it stays correct if the published rate changes. Current pricing
is always at [krenzo.in/pricing](https://krenzo.in/pricing).

## 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
