Metadata-Version: 2.4
Name: pali-client
Version: 0.1.0
Summary: Typed sync and async Python client plus middleware for the Pali memory API.
Author: Pali Maintainers
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Provides-Extra: all
Requires-Dist: openai>=1.0; extra == 'all'
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.2; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: openai
Requires-Dist: openai>=1.0; extra == 'openai'
Description-Content-Type: text/markdown

# Pali Python Client

Typed sync and async Python SDK plus middleware for the Pali memory API.

Pali is very early in development and should not be treated as a complete memory solution yet.
Right now the product focus is infrastructure correctness and reliability first.

Middleware is available as an early-stage autopilot helper, not a guaranteed memory optimization system.

## Installation

```bash
pip install pali-client
```

Optional extras:

```bash
pip install "pali-client[openai]"
pip install "pali-client[dev]"
```

## Environment Variables

The client supports low-priority environment variable fallbacks:

- `PALI_BASE_URL`
- `PALI_TOKEN`
- `PALI_TIMEOUT`

Constructor arguments always win over environment values.

## Quickstart

```python
from pali import PaliClient

client = PaliClient("http://127.0.0.1:8080")
client.create_tenant("user:42", name="User 42")
client.store("user:42", "Likes jazz", tags=["music"], kind="observation")
results = client.search("user:42", "music preferences", top_k=3)

for item in results.items:
    print(item.content)
```

## Common Patterns

Store a memory:

```python
from pali import PaliClient

client = PaliClient("http://127.0.0.1:8080", token="jwt-token")
stored = client.store(
    "user:42",
    "Moved to Austin in 2024.",
    tier="episodic",
    kind="event",
    tags=["profile"],
    source="chat_message",
    created_by="user",
)
print(stored.id)
```

Search memory:

```python
results = client.search(
    "user:42",
    "where does the user live?",
    top_k=5,
    min_score=0.25,
    tiers=["episodic", "semantic"],
    kinds=["event", "observation"],
)
```

Delete a memory:

```python
client.delete("user:42", "mem_abc123")
```

Async client:

```python
import asyncio

from pali import PaliAsyncClient


async def main() -> None:
    async with PaliAsyncClient("http://127.0.0.1:8080") as client:
        health = await client.health()
        print(health.status)


asyncio.run(main())
```

Middleware wrap (experimental autopilot):

```python
from pali import PaliClient, PaliMiddleware

client = PaliClient("http://127.0.0.1:8080")
middleware = PaliMiddleware(client, "user:42")


def llm(messages):
    return "The user likes jazz."


wrapped = middleware.wrap(llm)
reply = wrapped([{"role": "user", "content": "What music do I like?"}])
print(reply)
```

Auto-mutate memory with explicit opt-in (experimental):

```python
from pali import PaliMiddleware, ReplaceMemoryAction, StoreMemoryRequest


def planner(messages, recalled_memories, result, response_text):
    if recalled_memories and "moved to austin" in response_text.lower():
        return [
            ReplaceMemoryAction(
                memory_id=recalled_memories[0].id,
                request=StoreMemoryRequest(
                    tenant_id="user:42",
                    content="User lives in Austin.",
                    kind="observation",
                    created_by="system",
                ),
            )
        ]
    return []


middleware = PaliMiddleware(
    client,
    "user:42",
    allow_destructive_actions=True,
    action_planner=planner,
)
```

`allow_destructive_actions=False` by default. That means auto-add is on, but delete/replace actions are skipped unless the caller opts in.

Middleware hooks:

```python
from pali import PaliClient, PaliMiddleware


def hook(phase: str, payload: dict[str, object]) -> None:
    print(phase, sorted(payload))


client = PaliClient("http://127.0.0.1:8080")
middleware = PaliMiddleware(
    client,
    "user:42",
    hooks={
        "SEARCH": [hook],
        "INJECT": [hook],
        "CALL": [hook],
        "STORE": [hook],
    },
)
```

Anthropic wrap:

```python
wrapped = middleware.wrap_anthropic(anthropic_client)
response = wrapped.messages.create(
    model="claude-3-7-sonnet-latest",
    max_tokens=256,
    system="Be concise.",
    messages=[{"role": "user", "content": [{"type": "text", "text": "What do I like?"}]}],
)
```

## Configuration Reference

| Argument | Type | Default | Env var | Notes |
|---|---|---|---|---|
| `base_url` | `str` | required | `PALI_BASE_URL` | Constructor argument wins over env. |
| `token` | `str | None` | `None` | `PALI_TOKEN` | Sent as `Authorization: Bearer ...`. |
| `timeout` | `float` | `15.0` | `PALI_TIMEOUT` | Applied per request. |
| `max_retries` | `int` | `3` | none | `1` disables retries. |
| `http_client` | `httpx.Client | httpx.AsyncClient | None` | `None` | none | Custom transport, state remains caller-owned. |

## Current API Coverage

Implemented now:

- `health()`
- `create_tenant()`
- `tenant_stats()`
- `store()`
- `store_batch()`
- `search()`
- `delete()`

Not implemented because the current Pali server does not expose them as of March 7, 2026:

- `GET /v1/memory/:id`
- `PATCH /v1/memory/:id`
- `DELETE /v1/tenants/:id`
- cursor-based pagination for memory search
- streaming search/event feeds

Middleware mutation semantics:

- Default writeback is add-only.
- Middleware can now execute `store`, `delete`, and `replace` actions when a custom `action_planner` is provided.
- `replace` is currently executed as delete-plus-store because the server still has no `PATCH /v1/memory/:id`.
- Destructive actions require `allow_destructive_actions=True`.

## Error Handling

```python
from pali import NotFoundError, PaliError

try:
    client.delete("user:42", "missing")
except NotFoundError as err:
    print(err.request_id)
except PaliError as err:
    print(err)
```

Common API errors are raised as typed subclasses:

- `UnauthorizedError`
- `ForbiddenError`
- `NotFoundError`
- `ConflictError`
- `RateLimitError`
- `APIError`
- `ValidationError`
- `TransportError`

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## Examples

See [examples/basic.py](examples/basic.py), [examples/async_client.py](examples/async_client.py), and [examples/middleware_hooks.py](examples/middleware_hooks.py).
