Metadata-Version: 2.4
Name: cq-sdk
Version: 0.15.1
Summary: Python SDK for cq — shared agent knowledge commons
License: Apache-2.0
Project-URL: Homepage, https://github.com/mozilla-ai/cq
Project-URL: Repository, https://github.com/mozilla-ai/cq
Project-URL: Issues, https://github.com/mozilla-ai/cq/issues
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: cq-schema>=0.0.0.dev0
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2.12.5

# cq-sdk

Python SDK for [cq](https://github.com/mozilla-ai/cq) — the shared agent knowledge commons.

Lets any Python application query, propose, confirm, and flag knowledge units against a remote cq API, or store locally when no remote is configured.

## Installation

```bash
uv add cq-sdk
```

Or with pip:

```bash
pip install cq-sdk
```

## Quick Start

```python
from cq import Client, FlagReason

cq = Client()  # Auto-discovers config; falls back to local-only.

# Query.
results = cq.query(domains=["api", "stripe"], language="python")

# Propose.
ku = cq.propose(
    summary="Stripe 402 means card_declined",
    detail="Check error.code, not error.type.",
    action="Handle card_declined explicitly.",
    domains=["api", "stripe"],
)

# Confirm / flag.
cq.confirm(ku.id)
cq.flag(ku.id, reason=FlagReason.STALE)

# Get the canonical agent prompts.
from cq import prompts

skill_prompt = prompts.skill()
reflect_prompt = prompts.reflect()
```

## Configuration

The client reads configuration from environment variables:

| Variable              | Description                                                       | Default                       |
|-----------------------|-------------------------------------------------------------------|-------------------------------|
| `CQ_ADDR`             | Remote cq API address                                             | None (local-only)             |
| `CQ_API_KEY`          | API key for the remote API                                        | None                          |
| `CQ_LOCAL_DATABASE_URL` | Local store connection URL (e.g. `sqlite:///abs/path/local.db`) | None (falls back to `CQ_LOCAL_DB_PATH`) |
| `CQ_LOCAL_DB_PATH`    | Local SQLite file path                                            | `$XDG_DATA_HOME/cq/local.db` |
| `XDG_DATA_HOME`       | Base directory for the default database path ([XDG spec](https://specifications.freedesktop.org/basedir/latest/)) | `~/.local/share` |

Or pass directly:

```python
cq = Client(
    addr="http://localhost:3000",
    local_db_path=Path("~/.local/share/cq/local.db").expanduser(),
)
```

### Store protocol

The local store is pluggable. The SDK defines a `Store` runtime-checkable Protocol that the `Client` depends on; the default `SqliteStore` satisfies it, and you can supply any implementation.

#### Selecting a store

The client resolves the local store in this precedence order:

1. **`store=` argument** — inject any `cq.Store` directly.
2. **`CQ_LOCAL_DATABASE_URL`** — a connection-string URL resolved by `cq.create_store`. Accepted schemes: `sqlite:///abs/path` or `sqlite:path`. A `postgresql://` URL raises `NotImplementedError` naming the planned `cq-sdk[postgres]` extra.
3. **`local_db_path=` argument / `CQ_LOCAL_DB_PATH` env var** — path to a SQLite file.
4. **XDG default** — `$XDG_DATA_HOME/cq/local.db` (typically `~/.local/share/cq/local.db`).

#### Interface

The `cq.Store` Protocol requires eight methods. Implementations must be safe for use across `asyncio.to_thread` executor threads.

| Method   | Signature                                           | Semantics |
|----------|-----------------------------------------------------|-----------|
| `get`    | `(unit_id: str) -> KnowledgeUnit \| None`           | Retrieve by ID, or `None` if absent. |
| `all`    | `() -> list[KnowledgeUnit]`                         | Return every unit in the store. |
| `insert` | `(unit: KnowledgeUnit) -> None`                     | Insert a unit. Raise `DuplicateUnitError` on an existing ID; raise `ValueError` if domains are empty after normalization. |
| `update` | `(unit: KnowledgeUnit) -> None`                     | Replace an existing unit. Raise `KeyError` when the ID is absent; raise `ValueError` if domains are empty. |
| `delete` | `(unit_id: str) -> None`                            | Remove by ID. Raise `KeyError` when absent. |
| `query`  | `(params: QueryParams) -> StoreQueryResult`         | Return units matching the query, ranked most-relevant first. |
| `stats`  | `(*, recent_limit: int = 5) -> StoreStats`          | Return aggregated store statistics. |
| `close`  | `() -> None`                                        | Release resources. Must be safe to call more than once. |

#### Built-in implementations

- **`SqliteStore`** — the default. Opens a SQLite file with FTS5 full-text search, WAL journaling, and domain-tag indexing.
- **`InMemoryStore`** — map-backed, no persistence. Useful for tests and as a worked example for custom stores (domain-tag matching only, no full-text).

```python
from cq import Client, InMemoryStore

client = Client(store=InMemoryStore())
```

#### Bring your own

Implement the `cq.Store` protocol and inject it via `store=`. Reuse the shared ranker `cq.rank_candidates` from your `query` implementation so ranking stays consistent across backends. Verify the implementation against the conformance suite in `tests/conformance.py`:

```python
from tests.conformance import run_store_conformance

run_store_conformance(lambda: MyCustomStore())
```

## Knowledge tiers

Every knowledge unit has a tier: `local` (on-disk SQLite, never leaves the machine), `private` (stored on the remote API at `CQ_ADDR`, visible to every client pointing at the same remote), or `public` (open commons; not yet available).

With a remote configured, `cq.propose(...)` sends the unit to the remote and returns it tagged `private`; with no remote, or if the remote is unreachable, it writes the unit locally as `local`.

See the [top-level README](../../README.md) for the full description.

## Dev Setup

```bash
uv sync --group dev
```

## Testing

```bash
make test
```

## Linting

```bash
make lint
```

## License

[Apache License 2.0](../../LICENSE)
