# acp-easy

A lightweight, LangChain-style Python wrapper around the [ACP SDK](https://pypi.org/project/acp-sdk/) that makes it easy to talk to ACP-compliant agent servers — with auto agent-discovery, sync and async support, and clear error messages.

## Install

```bash
pip install acp-easy
```

## Quick start

```python
from acp_easy import AcpClient

client = AcpClient(base_url="http://localhost:8000", agent="my-agent")

reply = client.chat(
    messages=[
        {"role": "user", "content": "Hello!"},
    ],
    model="gpt-4o-mini",
)

print(reply)
```

Each message must be a dict with `role` and `content` keys, e.g. `{"role": "user", "content": "..."}`.

## Finding your agent name

If you don't know what agents your ACP server exposes, list them first:

```python
from acp_easy import AcpClient

client = AcpClient(base_url="http://localhost:8000")

agents = client.list_agents()
print(agents)
# [{'name': 'my-agent', 'description': 'No description provided.'}]
```

Each entry gives you the `name` to pass as `agent=...`.

- If you don't pass `agent` at all **and** the server has exactly one agent, `acp-easy` auto-resolves it for you.
- If the server has multiple agents and you don't specify one, you'll get a clear `AcpAgentResolutionError` listing all available agents so you can pick one.

You can set a default agent once at client creation instead of passing it on every call:

```python
client = AcpClient(base_url="http://localhost:8000", agent="my-agent")
```

## Available models

Pass any of these keys as the `model` argument to `chat()`:

| Key | Model |
|---|---|
| `GLM-5.2` | `huggingface/zai-org/GLM-5.2` |
| `gpt-4o-mini` | `openai/gpt-4o-mini` |
| `Qwen3-Coder-Next` | `huggingface/Qwen/Qwen3-Coder-Next` |
| `MiniMAX-M3` | `huggingface/MiniMaxAI/MiniMax-M3` |
| `DeepSeek-V4-Pro` | `huggingface/deepseek-ai/DeepSeek-V4-Pro` |

## Sync vs Async

`acp-easy` gives you both sync and async versions of every network call. Use sync in plain scripts, and async inside code that already runs an event loop (FastAPI, Jupyter, asyncio apps).

| Task | Sync | Async |
|---|---|---|
| List agents | `client.list_agents()` | `await client.list_agents_async()` |
| Chat | `client.chat(...)` | `await client.chat_async(...)` |

```python
# Inside a FastAPI route / Jupyter notebook / any async function:
agents = await client.list_agents_async()
reply = await client.chat_async(
    messages=[{"role": "user", "content": "Hello!"}],
    model="gpt-4o-mini",
)
```

> ⚠️ Calling the sync methods (`chat`, `list_agents`) from inside a running event loop will raise a clear `RuntimeError` telling you to use the `_async` version instead — this avoids the confusing `asyncio.run() cannot be called from a running event loop` crash.

## Multi-turn conversations

Pass the full message history as a list — `acp-easy` doesn't manage conversation memory for you:

```python
reply = client.chat(
    messages=[
        {"role": "user", "content": "My name is Ganaik."},
        {"role": "assistant", "content": "Nice to meet you, Ganaik!"},
        {"role": "user", "content": "What's my name?"},
    ],
    model="gpt-4o-mini",
)
```

## Error handling

`acp-easy` raises `AcpAgentResolutionError` for:
- No agents found on the server
- Multiple agents found with none specified
- Malformed messages (missing `role`/`content`, wrong types)

```python
from acp_easy import AcpClient, AcpAgentResolutionError

client = AcpClient(base_url="http://localhost:8000")

try:
    reply = client.chat(
        messages=[{"role": "user", "content": "Hi"}],
        model="gpt-4o-mini",
    )
except AcpAgentResolutionError as e:
    print(f"Couldn't resolve agent: {e}")
```

## API reference

### `AcpClient(base_url, agent=None, timeout=60.0)`
- `base_url` — URL of your ACP server (e.g. `"http://localhost:8000"`)
- `agent` — optional default agent name; skips auto-discovery if set
- `timeout` — request timeout in seconds (default `60.0`)

### `client.list_agents()` / `await client.list_agents_async()`
Returns a list of `{"name": ..., "description": ...}` dicts for all agents on the server.

### `client.chat(messages, model, agent=None)` / `await client.chat_async(messages, model, agent=None)`
Sends a message history to the resolved agent and returns the agent's text reply.
- `messages` — list of `{"role": ..., "content": ...}` dicts
- `model` — model key from the [Available models](#available-models) table
- `agent` — optional agent name; overrides the client's default for this call only

```