Metadata-Version: 2.4
Name: askfaro
Version: 0.3.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Requires-Dist: httpx>=0.28.0
Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24.0 ; extra == 'dev'
Requires-Dist: respx>=0.22.0 ; extra == 'dev'
Provides-Extra: dev
License-File: LICENSE
Summary: Faro SDK: run free Faro tools on-device via the bundled Rust core, fall back to the API for the rest. Local and remote results share the identical envelope.
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://askfaro.com
Project-URL: Repository, https://github.com/poolside-ventures/faro

# askfaro

[![PyPI](https://img.shields.io/pypi/v/askfaro)](https://pypi.org/project/askfaro/)
[![CI](https://github.com/poolside-ventures/askfaro/actions/workflows/ci.yml/badge.svg)](https://github.com/poolside-ventures/askfaro/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

The Faro Python SDK. **Local-first**: tools the bundled Rust core can run execute
on-device — no API key, no network, no credits. Everything else falls back to the
Faro backend. Local and remote results share the identical canonical envelope, so
the same code path works whether a tool ran on your machine or in the cloud.

```bash
pip install askfaro
```

```python
from askfaro import Faro

faro = Faro()                                    # no key needed for on-device tools
r = faro.invoke("calc/evaluate", {"expression": "2 + 2 * 3"})
assert r.ok and r.local and r.data["result"] == 8

# Anything beyond the on-device core is a *skill*: the skill agent picks the
# tools, runs them, enforces your budget, and bills your account.
faro = Faro(api_key="faro_...")
faro.run("image", {"prompt": "a red bicycle"})
```

The Rust core is compiled into this package (`askfaro._core`), so a single
`pip install askfaro` is all you need — there is no separate core package to
install.

## invoke() vs run()

Two execution methods:

- **`invoke("namespace/tool")`** calls one tool. Use it for the **on-device
  core** (calc, units, phone, …), which runs locally with no key and no network.
  Raw *remote* tools are not directly callable (the API answers "use the skill
  layer"), so for anything vendor-backed, use `run()`.
- **`run("skill", intent)`** runs a **skill**: the skill agent selects the
  operations, calls the underlying tools, enforces your budget, and bills your
  account. This is the path for every capability that isn't an on-device tool.
  It needs an API key and runs on Faro's hosted skill agent (`skill.askfaro.com`).

`invoke()` routing (`mode=`, per-call override on `invoke(..., mode=...)`):

| mode | behavior |
|------|----------|
| `auto` (default) | run on-device when the core can; otherwise call the API |
| `local` | on-device only; raise `LocalUnavailableError` if the core can't run it |
| `remote` | always call the API (succeeds only for tools the API still exposes directly) |

What runs on-device is the bundled core's capability list
(`Faro.local_namespaces()`), not a pricing flag; it grows as more tools are
ported into the core.

## Async

For server-side consumers on an event loop (e.g. an async FastAPI backend),
`AsyncFaro` mirrors `Faro` with awaitable network methods, so you don't wrap calls
in `asyncio.to_thread`:

```python
from askfaro import AsyncFaro

async with AsyncFaro(api_key="faro_...") as faro:
    hits = await faro.search("transcribe an audio file")
    r = await faro.run("image", {"prompt": "a red bicycle"})
    assert r.ok
```

Same constructor, routing, and result types as `Faro`. Only the network methods
(`search`, `describe`, `browse`, remote `invoke`, `run`) are coroutines; on-device
`invoke()` runs in the synchronous in-process core (sub-millisecond), so there is
no blocking I/O to offload.

## Discovery

Two ways to reach a capability: invoke one you already know, or find one from
intent. Discovery needs no API key.

```python
faro = Faro()

# Describe what you want; get ranked, ready-to-invoke skills/tools:
for hit in faro.search("transcribe an audio file"):
    print(hit.id, hit.short_description, hit.pricing)

# Each hit's .id is exactly what invoke() takes:
best = faro.search("transcribe an audio file")[0]
# faro.invoke(best.id, {"url": "https://.../clip.mp3"}, mode="remote")   # paid -> needs a key

# Full input schema + pricing for one candidate:
faro.describe("audio-intelligence/transcribe")

# A skill hit's .id runs via the skill agent; a tool hit's .id is for invoke():
hit = faro.search("generate an image")[0]
if hit.kind == "skill":
    faro.run(hit.id, {"prompt": "a red bicycle"})

# Browse instead of search: a progressive-context (pcx) map you expand one branch
# at a time, sized for small / on-device context windows:
manifest = faro.browse(budget="4k")    # navigate via its self-describing `usage` field
```

`search()` is hybrid lexical + semantic over the public catalog; `browse()`
returns the [progressive-context](https://github.com/poolside-ventures/askfaro-progressive-context)
manifest. Both work with no account. `invoke()` on a paid tool still needs a key
and credits.

## What's bundled

`askfaro._core` is the MIT open-source free-tool slice of the Faro core (the
`faro-core-free` Rust crate): calc, units, phone, astronomy, encoding, datetime,
timezone, random, and timer, plus the canonical envelope builders. The proprietary
parts of Faro (selection gate, signed continuations, cloud client, billing) are NOT
in this package; vendor-backed tools run server-side via the API.

## Development

This is a [maturin](https://www.maturin.rs) mixed Rust/Python project, fully
self-contained:

- `core-free/` — the `faro-core-free` Rust crate (the free-tool implementations +
  the canonical envelope)
- `src/lib.rs` — the PyO3 binding that builds the `askfaro._core` extension from it
- `python/askfaro/` — the pure-Python SDK (routing, client, result types)
- `examples/quickstart.py` — a runnable tour

```bash
cargo test -p faro-core-free   # Rust tests
uv venv && uv pip install ".[dev]"   # builds askfaro._core via maturin
.venv/bin/pytest               # Python tests, no network
python examples/quickstart.py
```

Published wheels are prebuilt (manylinux x86_64/aarch64 + macOS universal2), so
end users install with no Rust toolchain.

