Metadata-Version: 2.4
Name: pyaddisai
Version: 0.2.1
Summary: Python client for the Addis AI API — Amharic & Afan Oromo chat, speech and translation.
Project-URL: Homepage, https://addisassistant.com
Project-URL: Repository, https://github.com/wizkiye/pyaddisai
Project-URL: Issues, https://github.com/wizkiye/pyaddisai/issues
Project-URL: Documentation, https://github.com/wizkiye/pyaddisai#readme
Author: Addis AI Community
License-Expression: MIT
Keywords: addis,ai,amharic,llm,oromo,stt,translation,tts
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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 :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: pydantic<3.0,>=2.5
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# Addis AI — Python Client

Python client for the [Addis AI](https://addisassistant.com) API: chat / LLM completion,
speech-to-text, translation and text-to-speech for **Amharic (አማርኛ)**, **Afan Oromo**
and other languages.

Modular, fully typed, **sync and async**, with [Pydantic v2](https://docs.pydantic.dev/) models.

```
addis_ai/
├── client.py            # Client — composes every sync method mixin
├── async_client.py      # AsyncClient — same surface, every method awaitable
├── connection/          # HTTP transports: auth, retries, SSE, error normalization
│   ├── base.py          #   shared policy (backoff, envelopes, SSE decoding)
│   ├── transport.py     #   httpx.Client
│   └── async_transport.py  # httpx.AsyncClient
├── methods/
│   ├── chat/            # generate_chat, stream_chat        (+ async variants)
│   ├── speech/          # transcribe                        (+ async)
│   ├── translation/     # translate                         (+ async)
│   └── voice/           # generate_speech, get_voices, clips, ...  (+ async)
├── types/               # Pydantic models: ChatResponse, Clip, Voice, ...
├── errors/              # AddisAIError → APIStatusError → RateLimitError, ...
└── enums/               # Language, STTLanguage, OutputFormat, FinishReason, ...
```

## Installation

```bash
pip install pyaddisai
```

Requires Python 3.9+, [httpx](https://www.python-httpx.org/) and
[pydantic v2](https://docs.pydantic.dev/).

## Quick start

```python
from addis_ai import Client

# Reads ADDIS_AI_API_KEY from the environment if api_key is omitted
with Client("YOUR_API_KEY") as app:
    response = app.generate_chat("ሰላም እንዴት ነህ?", target_language="am")
    print(response.text)
```

### Async

`AsyncClient` mirrors the entire `Client` surface — every method is a coroutine, and
`stream_chat` / `get_clips` are async generators:

```python
import asyncio
from addis_ai import AsyncClient

async def main():
    async with AsyncClient("YOUR_API_KEY") as app:
        response = await app.generate_chat("ሰላም እንዴት ነህ?", target_language="am")
        print(response.text)

        async for chunk in app.stream_chat("ስለ ኢትዮጵያ ንገረኝ"):
            if chunk.text:
                print(chunk.text, end="", flush=True)

        async for clip in app.get_clips(limit=10):
            print(clip.id)

asyncio.run(main())
```

## Chat

```python
from addis_ai import Client, types, enums

app = Client("YOUR_API_KEY")

response = app.generate_chat(
    "የዛሬው የአየር ሁኔታ ምን ይመስላል?",
    target_language=enums.Language.AMHARIC,
    system="You are concise and friendly.",
    conversation_history=[
        types.Message("user", "ሰላም"),
        types.Message("assistant", "ሰላም! እንዴት ልረዳህ?"),
    ],
    temperature=0.7,
    max_output_tokens=512,
)

print(response.text)
print(response.usage.total_token_count)
```

### Streaming

```python
for chunk in app.stream_chat("ስለ ኢትዮጵያ ታሪክ ንገረኝ", target_language="am"):
    if chunk.text:
        print(chunk.text, end="", flush=True)
```

### Attachments & voice input

```python
response = app.generate_chat(
    "What is in this picture?",
    target_language="am",
    attachments=["photo.png"],           # paths, bytes, file objects or tuples
)

response = app.generate_chat(
    "",                                   # prompt comes from the audio
    target_language="am",
    audio_input="command.wav",
)
print(response.transcription_clean)       # what the user said
print(response.text)                      # the assistant's answer
```

### Function calling

```python
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
        },
    },
}]

response = app.generate_chat(" አዲስ አበባ ውስጥ ስንት ዲግሪ ነው?", tools=tools)

if response.finish_reason == enums.FinishReason.TOOL_CALLS:
    for call in response.tool_calls:
        result = run_my_tool(call.name, call.arguments)   # execute locally
        # send the result back folded into an assistant message,
        # echoing call.to_request() (preserves addis_tool_state)
```

## Speech-to-text

```python
result = app.transcribe("call.wav", language_code=enums.STTLanguage.AMHARIC)
print(result.text, result.confidence)
```

## Translation

```python
result = app.translate("Good morning", source_language="en", target_language="am")
print(result.text)  # ደህና እደሩ...
```

## Text-to-speech

```python
# Browse the catalog
voices = app.get_voices(language="am", gender="female")
voice = voices[0]

# Optional: check the price first
estimate = app.estimate_speech("ሰላም ለዓለም", voice_id=voice.id, language="am")
assert estimate.can_generate

# Synthesize (idempotent — a ULID client_request_id is generated automatically)
clip = app.generate_speech(
    "ሰላም ለዓለም",
    voice_id=voice.id,
    language="am",
    output_format=enums.OutputFormat.MP3_44100,
    voice_settings=types.VoiceSettings(speed=50, stability=60),
)

clip.download("hello.mp3")                # fetches the signed audio_url
                                          # (await clip.download(...) with AsyncClient)

# Manage clips
for clip in app.get_clips(limit=20):      # cursor pagination handled for you
    print(clip.id, clip.text_preview)

app.delete_clip(clip.id)

# Wallet
usage = app.get_voice_usage()
print(usage.formatted_balance)
```

## Error handling

All errors derive from `addis_ai.errors.AddisAIError`. HTTP errors carry the
normalized `message`, `code`, `details`, `request_id` and `retry_after`:

```python
from addis_ai import errors

try:
    clip = app.generate_speech("ሰላም", voice_id="voice_abc")
except errors.InsufficientCreditsError as e:
    print("Top up your wallet:", e.details)
except errors.RateLimitError as e:
    print("Slow down, retry after", e.retry_after)
except errors.APIStatusError as e:
    print(e.status_code, e.code, e.message, e.request_id)
```

Transient failures (408, 425, 429, 5xx and 409 `GENERATION_IN_PROGRESS`) are retried
automatically up to 3 times with exponential backoff, jitter and `Retry-After` support.
`409 IDEMPOTENCY_CONFLICT` is never retried.

## Typed models

Every response is a Pydantic v2 model — validated, with full IDE completion, and tolerant
of new server fields (`extra="allow"`):

```python
clip = app.get_clip("clip_123")
clip.model_dump()                # plain dict (or clip.to_dict())
clip.model_dump_json(indent=2)   # JSON
types.Clip.model_validate(raw)   # parse your own payloads

types.VoiceSettings(speed=500)   # ValidationError: must be 0-100
```

## Configuration

```python
app = Client(                             # same signature for AsyncClient
    api_key="...",                        # or ADDIS_AI_API_KEY env var; JWTs auto-detected
    base_url="https://api.addisassistant.com",
    timeout=60.0,                         # voice generation enforces a ~95s floor
    max_retries=3,
    http_transport=None,                  # custom httpx transport (e.g. MockTransport in tests)
)
```

## License

MIT

---

*Built against the reverse-engineered HTTP contract from `addisai-js` v0.1.0. Not an official Addis AI product.*
