Metadata-Version: 2.4
Name: rawintent
Version: 5.2.2
Summary: Build simple APIs and train tiny educational language models
Author: RawIntent contributors
License-Expression: MIT
Keywords: api,http,language-model,transformer,machine-learning,fastapi
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
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 :: HTTP Servers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi<1,>=0.115
Requires-Dist: uvicorn<1,>=0.30
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.7
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: build>=1; extra == "dev"
Requires-Dist: twine>=6; extra == "dev"
Provides-Extra: ml
Requires-Dist: torch>=2.2; extra == "ml"
Provides-Extra: onion-search
Requires-Dist: httpx[socks]<1,>=0.27; extra == "onion-search"
Requires-Dist: beautifulsoup4<5,>=4.12; extra == "onion-search"
Requires-Dist: rpgp-py<0.21,>=0.20; extra == "onion-search"
Dynamic: license-file

# RawIntent 5.2.2

Make a small API with a few lines of Python.

## Start here

Install this local version with `python -m pip install .` from the project folder.

## Train a tiny language model

Install the optional machine-learning dependency with `python -m pip install '.[ml]'`.
This educational character model starts with random weights and practices on your
own UTF-8 text. It is small and does not have the knowledge or quality of a large
pretrained assistant. A longer text file and more practice rounds take more time.

Save this as `tiny_llm.py` next to `corpus.txt`:

```python
from rawintent import AIModel

AIModel = AIModel()

@AIModel.TRAIN(TRAINFILE="corpus.txt", MODELFILE="my_model.pt", STEPS=1000)
def train_model():
    print("Training finished")

@AIModel.PROMPT(SYSTEM_PROMPT="You are a helpful assistant.")
def ask(prompt):
    pass

train_model()
print(ask("Hello", tokens=80))
```

`STEPS` means practice rounds: each round shows the model short pieces of your
text and asks it to guess the next character. `CONTEXT` controls how many
characters it sees at once. Training creates a checkpoint file; the prompt
decorator can load it again on a later run.

### 1. Make something people can read

Save this as `app.py`:

```python
from rawintent import API

api = API()
api.reply("hello", "Hello, world!", public=True)
api.start()
```

Run `python app.py`. Open **http://127.0.0.1:8000/hello** in your browser.
You will see `"Hello, world!"`. Keep the program running while you use the API.

- `API()` makes your API.
- `reply("hello", ...)` puts an answer at `/hello`.
- `public=True` means people can read it without a key.
- `start()` starts the server.

### 2. Make something people can ask to do

Replace `app.py` with:

```python
from rawintent import API

api = API()

@api.action(public=True)
def add(a=0, b=0):
    return a + b

api.start()
```

Stop the previous server with Ctrl+C, then run `python app.py` again.
The function name `add` becomes the address `/add`.
`a=0` and `b=0` mean these inputs are whole numbers and default to zero.
Use `0.0` for decimals, `""` for text, or `False` for yes/no values.
Explicit type annotations still work when you want them.

In another terminal, run a second Python file:

```python
from rawintent import send

answer = send("http://127.0.0.1:8000/add", a=2, b=3)
print(answer)  # 5
```

That's it: give `send()` an address and the values to send. It handles the JSON
request and closes its connection for you. Use `read(address)` for fixed replies
and other GET endpoints. Supply query values as `read(address, name="Alice")`.
The address must include `http://` or `https://`; pass inputs as named values,
not as a query string in the address.

### 3. Add a key when you want private access

Change `@api.action(public=True)` to just **`@api.action`**, then restart the server.
It now requires a key. In a separate file, run this once from the same folder
where you started your server:

```python
from rawintent import make_key

print(make_key())
```

Copy the printed key. Then send it with your request:

```python
from rawintent import send

answer = send("http://127.0.0.1:8000/add", key="PASTE_YOUR_KEY_HERE", a=2, b=3)
print(answer)
```

Keep your real key private; an environment variable is a good place to keep it
when sharing your code. `examples/easy_client.py` shows that approach. Create a
key once and reuse it; each call to `make_key()` creates a different key.
If you set `API(key_store=...)`, use the same path in `make_key(store=...)`.

### Run and try your API in one file

For a quick demo or a test, use `api.running()`. It starts a real local server,
waits until it is ready, and stops it when you leave the block:

```python
from rawintent import API

api = API(key_store=":memory:")

@api.action
def multiply(a=0, b=0):
    return a * b

key = api.create_key("example")

with api.running(key=key) as server:
    print(server.send("multiply", a=6, b=7))  # 42
```

There is no need to write socket, thread, waiting, or shutdown code.
`server.send("name", ...)` calls an action. `server.read("name", ...)` reads an
endpoint. `server.url` gives the address if you want to use a different client.
A free port is selected automatically. Set `port=8000` if you need a fixed port.

The key is optional for public endpoints. Passing a key does not bypass
permissions, expiration, or revocation. This example uses an in-memory key store
so its demo key disappears when the program ends; normal apps use the default
persistent store. Each running block starts a new server; a finished context
cannot be re-entered.

`api.running(timeout=10)` bounds startup waiting, request timeouts, and graceful
shutdown. Async startup can be cancelled on timeout; blocking user code cannot
be forcibly terminated in a Python thread. A shutdown that cannot finish raises
a timeout error. This helper listens on local loopback and serves HTTP endpoints;
use `api.start()` or the ASGI server commands for normal hosting or WebSockets.

### Handy shortcuts

```python
api.reply("status", {"running": True}, public=True)

@api.action(name="say-hello", public=True)
def greet(name="friend"):
    return f"Hello, {name}!"
```

Use `send("http://127.0.0.1:8000/say-hello", name="Sam")` to call it.
Visit **http://127.0.0.1:8000/docs** to try your API in a browser.

For several requests, reuse a client:

```python
from rawintent import Client

with Client("http://127.0.0.1:8000", api_key="YOUR_KEY") as client:
    print(client.call("add", a=2, b=3))
```

`AsyncClient` also supports `await client.call(...)`.
`send()` and `read()` reserve the argument `key` for authentication; use the
existing `Client.post(..., json={...})` or `Client.get(..., params={...})` interface
if your endpoint itself has an input named `key`.

These helpers are additions: the 5.0 endpoint decorators, clients, and key
management still work. The full API reference follows.

## Optional onion search

Install onion search with `python -m pip install 'rawintent[onion-search]'`.
On the first search, RawIntent checks for Tor on ports 9050 and 9150. If it
does not find a local proxy, it downloads the Tor Project's Expert Bundle,
verifies its OpenPGP signature against Tor Browser Developers' pinned key,
caches the bundle for your user, and starts a private Tor process for the
search. The process is stopped when the searcher closes. This does not install
Tor Browser.

```python
from rawintent import OnionSearcher

with OnionSearcher() as searcher:
    for result in searcher.search("example topic", limit=5):
        print(result.title, result.url, result.description)
```

Pass `proxy="socks5h://127.0.0.1:9150"` to use a specific Tor Browser proxy,
or `proxy="socks5h://127.0.0.1:9050"` for a Tor service. RawIntent starts its
managed Tor process only when the selected local port is not already in use.
Set `auto_start_tor=False` to require an existing proxy. The `socks5h` scheme
resolves onion hostnames through Tor. You can also run
`rawintent-onion-search "example topic" --limit 5`; use `--json` for JSON output.
The default engine is configurable with `engine_url`, and must be a v3 `.onion`
URL. The library returns search-engine results only; it does not visit result
sites. Onion services and search engines may be unavailable or return unsafe,
misleading, or outdated content. Use Tor only in ways allowed by local law.

---

Build your own API: define endpoints, create API keys, and send requests.

Version 5 replaces the English-language engine and neural compiler completely.
It is a breaking redesign, not a compatible update to RawIntent 4. Pin
`rawintent==4.1.0` if you still need the old language engine.

## Install

```sh
pip install rawintent
```

For an unpublished local checkout, use `python -m pip install .`.
Python 3.10 or later is required.

## Create a server

Save this as `app.py`:

```python
from rawintent import API

api = API("My API")

@api.get("/hello")
def hello(name: str = "world"):
    return {"message": f"Hello, {name}!"}

@api.post("/add")
def add(a: float, b: float):
    return {"result": a + b}

@api.get("/health", public=True)
def health():
    return {"status": "ok"}

if __name__ == "__main__":
    api.run()
```

Run `python app.py`. It listens at `http://127.0.0.1:8000`.
Open `http://127.0.0.1:8000/docs` to try endpoints in your browser.

You can also generate the starter and launch it with:

```sh
rawintent init app.py
rawintent serve app:api --reload
```

`python -m rawintent` works anywhere the console command is unavailable.
`init` will not overwrite an existing file.

## Create a key and send requests

Run this in another terminal from the same working directory as your server:

```sh
rawintent keys create --name my-client
```

Copy the returned `ri_...` token. It is displayed only at creation and cannot be
recovered from the database. Put it in an environment variable or a secret store.
In PowerShell you can capture the key directly:

```powershell
$env:RAWINTENT_API_KEY = python -m rawintent keys create --name my-client
python examples/client.py
```

In your client script:

```python
import os
from rawintent import Client

with Client("http://127.0.0.1:8000", api_key=os.environ["RAWINTENT_API_KEY"]) as client:
    print(client.get("/hello", params={"name": "Vincent"}))
    print(client.post("/add", json={"a": 2, "b": 3}))
```

Output:

```text
{'message': 'Hello, Vincent!'}
{'result': 5.0}
```

Any HTTP client can call your API. Send the key in the `X-API-Key` header.
The interactive docs have an **Authorize** button for the same key.
Keys belong to the server/database that issued them; they are not PyPI tokens
or credentials for a hosted RawIntent service.

## Endpoint rules

- `@api.get`, `@api.post`, `@api.put`, `@api.patch`, and `@api.delete` register routes.
- Endpoints require a valid key unless `public=True` is explicit.
- GET and DELETE scalar arguments come from query parameters.
- POST, PUT, and PATCH arguments become fields of a JSON object.
- `{item_id}` in a route is a path parameter and takes precedence over the body.
- Python type annotations validate inputs. Missing or invalid input returns HTTP 422.
- Both regular `def` and `async def` handlers work.
- Dictionary and list results become JSON. Return `Response` for other formats.
- `@api.route("/status", methods=["GET", "HEAD"])` supports other HTTP methods.
- Register body-based and query-based methods separately.

```python
@api.put("/items/{item_id}")
def update_item(item_id: int, title: str, count: int = 1):
    return {"id": item_id, "title": title, "count": count}
```

Call with `client.put("/items/42", json={"title": "Notebook", "count": 3})`.

For a raw JSON body or explicit query arguments, use exported FastAPI helpers:

```python
from rawintent import Body, Query

@api.post("/echo")
def echo(data: dict = Body(...), verbose: bool = Query(False)):
    return {"data": data, "verbose": verbose}
```

`Body(...)` uses the entire JSON document. Ordinary parameters instead use named
fields, so `def echo(data: dict)` expects `{"data": {...}}`.
`BaseModel`, `Field`, `Depends`, `Request`, `Response`, and `HTTPException` are
also exported. See [FastAPI parameter documentation](https://fastapi.tiangolo.com/tutorial/body/)
for explicit parameter declarations and nested Pydantic models.

## Key management

The default SQLite registry is `.rawintent/keys.sqlite3`, relative to the process
working directory. Use an absolute path for servers launched from different directories:

```python
api = API("My API", key_store="C:/my-api/keys.sqlite3")
```

Use the same file with CLI commands:

```sh
rawintent keys create --store C:/my-api/keys.sqlite3 --name reader --scope items:read --expires-in 86400
rawintent keys list --store C:/my-api/keys.sqlite3
rawintent keys revoke KEY_ID --store C:/my-api/keys.sqlite3
```

`keys list` shows IDs, names, prefixes, scopes, expiration, and active status;
it never prints secret tokens. Revocation takes effect on the next authentication
check, including across server processes sharing the same SQLite file.

Create keys programmatically with `api.create_key("client-name")`, or use
`KeyStore(path).create(...)`. Create them during provisioning, not each time
an application imports or a development server reloads.

For a local app that should save and reuse one client key automatically, call
`api.load_or_create_key(...)` before starting the server:

```python
api = API(key_store=".rawintent/keys.sqlite3")
key = api.load_or_create_key(".rawintent/client-key.txt", name="local-client")
api.start()
```

The helper creates the key in the API's key store if the file is missing, then
returns the saved key on later runs. The key file contains the secret in plain
text; keep it private and out of source control.

```python
@api.get("/items", scopes=["items:read"])
def list_items():
    return []
```

A key created with `scopes=["items:read"]` can access that endpoint. Missing
required scopes return 403. Keys with `scopes=None` (the default) have all scopes;
`scopes=[]` allows ordinary protected endpoints but no named scopes. Scope names
are exact strings, with no wildcard expansion. Expired or revoked keys return 401.
`request.state.api_key` contains authenticated `KeyInfo` inside a protected handler.

## Async clients and errors

```python
import asyncio
import os
from rawintent import AsyncClient, APIError

async def main():
    async with AsyncClient("http://127.0.0.1:8000", os.environ["RAWINTENT_API_KEY"]) as client:
        try:
            print(await client.post("/add", json={"a": 2, "b": 3}))
        except APIError as error:
            print(error.status_code, error.detail)

asyncio.run(main())
```

Clients parse JSON responses, return text for non-JSON responses, and return `None`
for empty responses. HTTP errors and redirects raise `APIError`. Network errors
and timeouts retain their HTTPX exception types. The default timeout is 30 seconds;
set `timeout=10` when constructing a client to change it. Writes are not retried.
Client requests accept paths on their configured server, not arbitrary external
URLs. Redirects are not followed. A base URL ending in `/v1` retains that prefix
for calls such as `client.get("/items")`.

## Hosting

RawIntent builds on [FastAPI](https://fastapi.tiangolo.com/),
[Uvicorn](https://www.uvicorn.org/), and [HTTPX](https://www.python-httpx.org/).
`api` is an ASGI application; run `rawintent serve app:api --host 0.0.0.0 --port 8000`
or `uvicorn app:api` on your host. Use a TLS reverse proxy for HTTPS outside local
development. Disable interactive docs with `API(docs=False)` if desired.
Docs and the OpenAPI schema are public when enabled, but protected endpoint calls
still require a key. `api.app` exposes the underlying FastAPI app for advanced
middleware and lifespan configuration. Routes added directly to `api.app` use
FastAPI rules and do not automatically get RawIntent key authentication.

The SQLite registry must live on persistent storage. Keep it and your tokens out
of source control. The library does not provide hosting, billing, user accounts,
rate limiting, or a public key-signup portal. Key creation is a local administrative
operation; there is no unauthenticated web endpoint that issues credentials.

## Development

```sh
python -m pip install -e ".[dev]"
python -m pytest -q
python -m build
python -m twine check dist/*
```
