addisai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- addisai-0.1.0/.github/workflows/ci.yml +21 -0
- addisai-0.1.0/.github/workflows/publish.yml +24 -0
- addisai-0.1.0/.github/workflows/smoke.yml +22 -0
- addisai-0.1.0/.gitignore +8 -0
- addisai-0.1.0/CHANGELOG.md +20 -0
- addisai-0.1.0/LICENSE +21 -0
- addisai-0.1.0/PKG-INFO +200 -0
- addisai-0.1.0/README.md +178 -0
- addisai-0.1.0/addisai/__init__.py +51 -0
- addisai-0.1.0/addisai/_client.py +62 -0
- addisai-0.1.0/addisai/_clip.py +88 -0
- addisai-0.1.0/addisai/_env.py +34 -0
- addisai-0.1.0/addisai/_exceptions.py +171 -0
- addisai-0.1.0/addisai/_idempotency.py +27 -0
- addisai-0.1.0/addisai/_streaming.py +172 -0
- addisai-0.1.0/addisai/_transport.py +194 -0
- addisai-0.1.0/addisai/_version.py +1 -0
- addisai-0.1.0/addisai/py.typed +0 -0
- addisai-0.1.0/addisai/resources/__init__.py +8 -0
- addisai-0.1.0/addisai/resources/chat.py +273 -0
- addisai-0.1.0/addisai/resources/legacy.py +81 -0
- addisai-0.1.0/addisai/resources/speech.py +43 -0
- addisai-0.1.0/addisai/resources/translate.py +32 -0
- addisai-0.1.0/addisai/resources/voice.py +142 -0
- addisai-0.1.0/addisai/resources/voices.py +37 -0
- addisai-0.1.0/pyproject.toml +34 -0
- addisai-0.1.0/scripts/smoke.py +88 -0
- addisai-0.1.0/tests/test_sdk.py +308 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: pip install -e ".[dev]"
|
|
21
|
+
- run: pytest -q
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishes `addisai` to PyPI using the PYPI_API_TOKEN repository secret.
|
|
4
|
+
# Create a GitHub Release (tag e.g. v0.1.0) to trigger, or run manually.
|
|
5
|
+
# (Consider migrating to PyPI Trusted Publishing/OIDC later — then drop the token.)
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.11"
|
|
19
|
+
- run: pip install -e ".[dev]" build
|
|
20
|
+
- run: pytest -q
|
|
21
|
+
- run: python -m build
|
|
22
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
23
|
+
with:
|
|
24
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: smoke
|
|
2
|
+
|
|
3
|
+
# Live smoke test against the real API. NOT a PR gate — manual + scheduled only.
|
|
4
|
+
# Requires repo secret ADDIS_SMOKE_API_KEY (a low-balance sandbox key).
|
|
5
|
+
on:
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 6 * * *"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
smoke:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
if: ${{ github.repository_owner == 'Addis-AI-Org' }}
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.11"
|
|
19
|
+
- run: pip install -e ".[dev]"
|
|
20
|
+
- run: python scripts/smoke.py
|
|
21
|
+
env:
|
|
22
|
+
ADDIS_API_KEY: ${{ secrets.ADDIS_SMOKE_API_KEY }}
|
addisai-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — Developer preview
|
|
4
|
+
|
|
5
|
+
Initial release of the official Addis AI SDK for Python.
|
|
6
|
+
|
|
7
|
+
- **Voice (TTS):** `voice.generate`, `voices.list/preview`, `voice.estimate`,
|
|
8
|
+
`voice.usage`, `voice.clips.*`, `VoiceClip` helpers.
|
|
9
|
+
- **Chat / LLM:** OpenAI-compatible `chat.completions.create` with `system`,
|
|
10
|
+
`persona`, tools/function calling, attachments, audio input; `chat.run_tools`
|
|
11
|
+
agent loop; beta SSE streaming via `ChatStream`.
|
|
12
|
+
- **Speech-to-text:** `speech.transcribe`. **Translation:** `translate.create`.
|
|
13
|
+
- **Reliability/security:** automatic retries with backoff, idempotent paid
|
|
14
|
+
calls, normalized exception hierarchy, secret redaction, Cloudflare-only
|
|
15
|
+
transport. Built on `httpx`. Typed (`py.typed`).
|
|
16
|
+
|
|
17
|
+
> Persona, system prompts, and function calling depend on a backend rollout; they
|
|
18
|
+
> are verified and ready in the SDK and activate as the server side ships.
|
|
19
|
+
>
|
|
20
|
+
> An async client (`AsyncAddisAI`) is planned for a follow-up release.
|
addisai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Addis AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
addisai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: addisai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Addis AI SDK for Python — voice (TTS), chat/LLM with system prompts, personas and function calling, speech-to-text, and translation for Amharic and Afan Oromo.
|
|
5
|
+
Project-URL: Homepage, https://addisai.com
|
|
6
|
+
Project-URL: Repository, https://github.com/Addis-AI-Org/addisai-py
|
|
7
|
+
Project-URL: Issues, https://github.com/Addis-AI-Org/addisai-py/issues
|
|
8
|
+
Author: Addis AI
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: addis,addisai,afan-oromo,ai,amharic,llm,speech-to-text,text-to-speech,translation,tts,voice
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Requires-Dist: httpx<1,>=0.24
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy>=1.6; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# addisai (Python)
|
|
24
|
+
|
|
25
|
+
The official [Addis AI](https://addisai.com) SDK for Python — voice (text‑to‑speech), chat/LLM with system prompts, personas and function calling, speech‑to‑text, and translation for **Amharic (`am`)** and **Afan Oromo (`om`)**.
|
|
26
|
+
|
|
27
|
+
Mirrors the [Node SDK](../node) surface, Pythonic and `snake_case`.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install addisai
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Requires Python 3.8+.
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from addisai import AddisAI
|
|
39
|
+
|
|
40
|
+
addis = AddisAI() # reads ADDIS_API_KEY from the environment
|
|
41
|
+
|
|
42
|
+
# Text-to-speech
|
|
43
|
+
clip = addis.voice.generate(voice_id="am-hiwot", text="ሰላም ለዓለም።", language="am")
|
|
44
|
+
clip.to_file("welcome.mp3")
|
|
45
|
+
|
|
46
|
+
# Chat
|
|
47
|
+
res = addis.chat.completions.create(
|
|
48
|
+
language="am",
|
|
49
|
+
system="Answer in concise bullet points.",
|
|
50
|
+
messages=[{"role": "user", "content": "ስለ አዲስ አበባ ንገረኝ"}],
|
|
51
|
+
)
|
|
52
|
+
print(res["choices"][0]["message"]["content"])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
addis = AddisAI(
|
|
59
|
+
api_key="...", # or set ADDIS_API_KEY
|
|
60
|
+
timeout=60.0, # seconds (voice.generate raises this to >= 95s)
|
|
61
|
+
max_retries=2, # backoff on 408/409/425/429/5xx
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The key is read from `api_key=` or `ADDIS_API_KEY`, never logged, and redacted in `repr()`. The SDK only talks to the Cloudflare‑wrapped API and refuses raw `*.supabase.co` hosts.
|
|
66
|
+
|
|
67
|
+
## Voice (text‑to‑speech)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
clip = addis.voice.generate(
|
|
71
|
+
voice_id="am-hiwot",
|
|
72
|
+
text="ሰላም።",
|
|
73
|
+
language="am",
|
|
74
|
+
output_format="mp3_44100", # mp3_44100 | wav_44100 | pcm_16000
|
|
75
|
+
voice_settings={"speed": 50, "stability": 50, "similarity": 50, "style": 0},
|
|
76
|
+
)
|
|
77
|
+
clip.audio_url
|
|
78
|
+
clip.usage # {"credits_used": ..., "currency": "ETB", ...}
|
|
79
|
+
data = clip.content() # bytes
|
|
80
|
+
clip.to_file("out.mp3")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Idempotency keys are generated automatically and reused across retries so a retry is never double‑billed. Pass `client_request_id=` for full control.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
addis.voices.list(language="am", gender="female")
|
|
87
|
+
addis.voice.estimate(voice_id="am-hiwot", text="ሰላም", language="am")
|
|
88
|
+
addis.voice.usage()
|
|
89
|
+
for clip in addis.voice.clips.list(language="am"): # auto-paginates
|
|
90
|
+
print(clip.id)
|
|
91
|
+
addis.voice.clips.delete("clip_123")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Chat / LLM
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
res = addis.chat.completions.create(
|
|
98
|
+
language="am",
|
|
99
|
+
system="Be concise.",
|
|
100
|
+
persona="You are RecipeBot by AcmeCorp.",
|
|
101
|
+
messages=[{"role": "user", "content": "የእንጀራ አሰራር አስተምረኝ"}],
|
|
102
|
+
temperature=0.7,
|
|
103
|
+
max_tokens=1200,
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> The model is selected by Addis AI; responses report the id `addis-1-alef`.
|
|
108
|
+
|
|
109
|
+
### Function calling
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
def get_order_status(args):
|
|
113
|
+
return {"status": "shipped", "eta": "tomorrow"}
|
|
114
|
+
|
|
115
|
+
final = addis.chat.run_tools(
|
|
116
|
+
language="am",
|
|
117
|
+
messages=[{"role": "user", "content": "Check order 123 and summarize it."}],
|
|
118
|
+
tools=[{
|
|
119
|
+
"type": "function",
|
|
120
|
+
"function": {
|
|
121
|
+
"name": "get_order_status",
|
|
122
|
+
"description": "Fetch order status by order ID.",
|
|
123
|
+
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
|
|
124
|
+
"callable": get_order_status,
|
|
125
|
+
},
|
|
126
|
+
}],
|
|
127
|
+
)
|
|
128
|
+
print(final["choices"][0]["message"]["content"])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Streaming (beta)
|
|
132
|
+
|
|
133
|
+
`stream=True` returns a `ChatStream` — iterate for OpenAI‑style chunk dicts, or use the accumulators:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
stream = addis.chat.completions.create(
|
|
137
|
+
language="am", messages=[{"role": "user", "content": "Capital?"}], stream=True
|
|
138
|
+
)
|
|
139
|
+
for chunk in stream:
|
|
140
|
+
print(chunk["choices"][0]["delta"].get("content", ""), end="", flush=True)
|
|
141
|
+
|
|
142
|
+
# or, instead of iterating:
|
|
143
|
+
text = addis.chat.completions.create(language="am", messages=[...], stream=True).final_text()
|
|
144
|
+
completion = addis.chat.completions.create(language="am", messages=[...], stream=True).final_completion()
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Streaming is beta and not available with tools.
|
|
148
|
+
|
|
149
|
+
## Speech‑to‑text & translation
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
with open("call.wav", "rb") as f:
|
|
153
|
+
t = addis.speech.transcribe(audio=f, language="am") # am|om|en|ha|sw
|
|
154
|
+
print(t["text"])
|
|
155
|
+
|
|
156
|
+
out = addis.translate.create(text="Hello", source="en", target="am") # am|om|en
|
|
157
|
+
print(out["text"])
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Legacy audio (deprecated)
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# ⚠️ deprecated — use addis.voice.generate
|
|
164
|
+
out = addis.legacy.audio.generate(text="ሰላም", language="am")
|
|
165
|
+
out.to_file("legacy.wav")
|
|
166
|
+
|
|
167
|
+
# streaming bridge — yields audio bytes, handles both legacy encodings
|
|
168
|
+
audio = addis.legacy.audio.stream(text="ሰላም", language="am")
|
|
169
|
+
audio.to_file("legacy.wav") # or: data = audio.read()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Errors
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from addisai import InsufficientCreditsError, RateLimitError, APIError
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
addis.voice.generate(voice_id="am-hiwot", text="ሰላም", language="am")
|
|
179
|
+
except InsufficientCreditsError as e:
|
|
180
|
+
show_top_up(e.available_balance)
|
|
181
|
+
except RateLimitError as e:
|
|
182
|
+
sleep(e.retry_after or 1)
|
|
183
|
+
except APIError as e:
|
|
184
|
+
print(e.status, e.code, e.details)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Not yet in the Python SDK
|
|
188
|
+
|
|
189
|
+
An async client (`AsyncAddisAI`) is the one planned follow‑up. Everything else — including streaming — is at parity with the Node SDK.
|
|
190
|
+
|
|
191
|
+
## Live smoke test
|
|
192
|
+
|
|
193
|
+
`scripts/smoke.py` exercises the real API end-to-end. It is **not** a PR gate —
|
|
194
|
+
the `smoke` workflow runs on manual dispatch and a daily schedule, only where the
|
|
195
|
+
repo secret `ADDIS_SMOKE_API_KEY` (a low-balance sandbox key) is set. Run locally
|
|
196
|
+
with `ADDIS_API_KEY=<key> python scripts/smoke.py`.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
addisai-0.1.0/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# addisai (Python)
|
|
2
|
+
|
|
3
|
+
The official [Addis AI](https://addisai.com) SDK for Python — voice (text‑to‑speech), chat/LLM with system prompts, personas and function calling, speech‑to‑text, and translation for **Amharic (`am`)** and **Afan Oromo (`om`)**.
|
|
4
|
+
|
|
5
|
+
Mirrors the [Node SDK](../node) surface, Pythonic and `snake_case`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install addisai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.8+.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from addisai import AddisAI
|
|
17
|
+
|
|
18
|
+
addis = AddisAI() # reads ADDIS_API_KEY from the environment
|
|
19
|
+
|
|
20
|
+
# Text-to-speech
|
|
21
|
+
clip = addis.voice.generate(voice_id="am-hiwot", text="ሰላም ለዓለም።", language="am")
|
|
22
|
+
clip.to_file("welcome.mp3")
|
|
23
|
+
|
|
24
|
+
# Chat
|
|
25
|
+
res = addis.chat.completions.create(
|
|
26
|
+
language="am",
|
|
27
|
+
system="Answer in concise bullet points.",
|
|
28
|
+
messages=[{"role": "user", "content": "ስለ አዲስ አበባ ንገረኝ"}],
|
|
29
|
+
)
|
|
30
|
+
print(res["choices"][0]["message"]["content"])
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
addis = AddisAI(
|
|
37
|
+
api_key="...", # or set ADDIS_API_KEY
|
|
38
|
+
timeout=60.0, # seconds (voice.generate raises this to >= 95s)
|
|
39
|
+
max_retries=2, # backoff on 408/409/425/429/5xx
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The key is read from `api_key=` or `ADDIS_API_KEY`, never logged, and redacted in `repr()`. The SDK only talks to the Cloudflare‑wrapped API and refuses raw `*.supabase.co` hosts.
|
|
44
|
+
|
|
45
|
+
## Voice (text‑to‑speech)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
clip = addis.voice.generate(
|
|
49
|
+
voice_id="am-hiwot",
|
|
50
|
+
text="ሰላም።",
|
|
51
|
+
language="am",
|
|
52
|
+
output_format="mp3_44100", # mp3_44100 | wav_44100 | pcm_16000
|
|
53
|
+
voice_settings={"speed": 50, "stability": 50, "similarity": 50, "style": 0},
|
|
54
|
+
)
|
|
55
|
+
clip.audio_url
|
|
56
|
+
clip.usage # {"credits_used": ..., "currency": "ETB", ...}
|
|
57
|
+
data = clip.content() # bytes
|
|
58
|
+
clip.to_file("out.mp3")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Idempotency keys are generated automatically and reused across retries so a retry is never double‑billed. Pass `client_request_id=` for full control.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
addis.voices.list(language="am", gender="female")
|
|
65
|
+
addis.voice.estimate(voice_id="am-hiwot", text="ሰላም", language="am")
|
|
66
|
+
addis.voice.usage()
|
|
67
|
+
for clip in addis.voice.clips.list(language="am"): # auto-paginates
|
|
68
|
+
print(clip.id)
|
|
69
|
+
addis.voice.clips.delete("clip_123")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Chat / LLM
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
res = addis.chat.completions.create(
|
|
76
|
+
language="am",
|
|
77
|
+
system="Be concise.",
|
|
78
|
+
persona="You are RecipeBot by AcmeCorp.",
|
|
79
|
+
messages=[{"role": "user", "content": "የእንጀራ አሰራር አስተምረኝ"}],
|
|
80
|
+
temperature=0.7,
|
|
81
|
+
max_tokens=1200,
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> The model is selected by Addis AI; responses report the id `addis-1-alef`.
|
|
86
|
+
|
|
87
|
+
### Function calling
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
def get_order_status(args):
|
|
91
|
+
return {"status": "shipped", "eta": "tomorrow"}
|
|
92
|
+
|
|
93
|
+
final = addis.chat.run_tools(
|
|
94
|
+
language="am",
|
|
95
|
+
messages=[{"role": "user", "content": "Check order 123 and summarize it."}],
|
|
96
|
+
tools=[{
|
|
97
|
+
"type": "function",
|
|
98
|
+
"function": {
|
|
99
|
+
"name": "get_order_status",
|
|
100
|
+
"description": "Fetch order status by order ID.",
|
|
101
|
+
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
|
|
102
|
+
"callable": get_order_status,
|
|
103
|
+
},
|
|
104
|
+
}],
|
|
105
|
+
)
|
|
106
|
+
print(final["choices"][0]["message"]["content"])
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Streaming (beta)
|
|
110
|
+
|
|
111
|
+
`stream=True` returns a `ChatStream` — iterate for OpenAI‑style chunk dicts, or use the accumulators:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
stream = addis.chat.completions.create(
|
|
115
|
+
language="am", messages=[{"role": "user", "content": "Capital?"}], stream=True
|
|
116
|
+
)
|
|
117
|
+
for chunk in stream:
|
|
118
|
+
print(chunk["choices"][0]["delta"].get("content", ""), end="", flush=True)
|
|
119
|
+
|
|
120
|
+
# or, instead of iterating:
|
|
121
|
+
text = addis.chat.completions.create(language="am", messages=[...], stream=True).final_text()
|
|
122
|
+
completion = addis.chat.completions.create(language="am", messages=[...], stream=True).final_completion()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Streaming is beta and not available with tools.
|
|
126
|
+
|
|
127
|
+
## Speech‑to‑text & translation
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
with open("call.wav", "rb") as f:
|
|
131
|
+
t = addis.speech.transcribe(audio=f, language="am") # am|om|en|ha|sw
|
|
132
|
+
print(t["text"])
|
|
133
|
+
|
|
134
|
+
out = addis.translate.create(text="Hello", source="en", target="am") # am|om|en
|
|
135
|
+
print(out["text"])
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Legacy audio (deprecated)
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# ⚠️ deprecated — use addis.voice.generate
|
|
142
|
+
out = addis.legacy.audio.generate(text="ሰላም", language="am")
|
|
143
|
+
out.to_file("legacy.wav")
|
|
144
|
+
|
|
145
|
+
# streaming bridge — yields audio bytes, handles both legacy encodings
|
|
146
|
+
audio = addis.legacy.audio.stream(text="ሰላም", language="am")
|
|
147
|
+
audio.to_file("legacy.wav") # or: data = audio.read()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Errors
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from addisai import InsufficientCreditsError, RateLimitError, APIError
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
addis.voice.generate(voice_id="am-hiwot", text="ሰላም", language="am")
|
|
157
|
+
except InsufficientCreditsError as e:
|
|
158
|
+
show_top_up(e.available_balance)
|
|
159
|
+
except RateLimitError as e:
|
|
160
|
+
sleep(e.retry_after or 1)
|
|
161
|
+
except APIError as e:
|
|
162
|
+
print(e.status, e.code, e.details)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Not yet in the Python SDK
|
|
166
|
+
|
|
167
|
+
An async client (`AsyncAddisAI`) is the one planned follow‑up. Everything else — including streaming — is at parity with the Node SDK.
|
|
168
|
+
|
|
169
|
+
## Live smoke test
|
|
170
|
+
|
|
171
|
+
`scripts/smoke.py` exercises the real API end-to-end. It is **not** a PR gate —
|
|
172
|
+
the `smoke` workflow runs on manual dispatch and a daily schedule, only where the
|
|
173
|
+
repo secret `ADDIS_SMOKE_API_KEY` (a low-balance sandbox key) is set. Run locally
|
|
174
|
+
with `ADDIS_API_KEY=<key> python scripts/smoke.py`.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Official Addis AI SDK for Python."""
|
|
2
|
+
from ._client import AddisAI
|
|
3
|
+
from ._clip import VoiceClip
|
|
4
|
+
from ._idempotency import ulid
|
|
5
|
+
from ._streaming import AudioStream, ChatStream
|
|
6
|
+
from ._version import __version__
|
|
7
|
+
from .resources import ADDIS_CHAT_MODEL
|
|
8
|
+
from ._exceptions import (
|
|
9
|
+
AddisAIError,
|
|
10
|
+
APIConnectionError,
|
|
11
|
+
APIConnectionTimeoutError,
|
|
12
|
+
APIError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
BadRequestError,
|
|
15
|
+
ConflictError,
|
|
16
|
+
GenerationInProgressError,
|
|
17
|
+
IdempotencyConflictError,
|
|
18
|
+
InsufficientCreditsError,
|
|
19
|
+
InternalServerError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
NotSupportedError,
|
|
22
|
+
PermissionDeniedError,
|
|
23
|
+
RateLimitError,
|
|
24
|
+
UnprocessableEntityError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AddisAI",
|
|
29
|
+
"VoiceClip",
|
|
30
|
+
"ChatStream",
|
|
31
|
+
"AudioStream",
|
|
32
|
+
"ulid",
|
|
33
|
+
"ADDIS_CHAT_MODEL",
|
|
34
|
+
"__version__",
|
|
35
|
+
"AddisAIError",
|
|
36
|
+
"APIError",
|
|
37
|
+
"APIConnectionError",
|
|
38
|
+
"APIConnectionTimeoutError",
|
|
39
|
+
"AuthenticationError",
|
|
40
|
+
"BadRequestError",
|
|
41
|
+
"ConflictError",
|
|
42
|
+
"GenerationInProgressError",
|
|
43
|
+
"IdempotencyConflictError",
|
|
44
|
+
"InsufficientCreditsError",
|
|
45
|
+
"InternalServerError",
|
|
46
|
+
"NotFoundError",
|
|
47
|
+
"NotSupportedError",
|
|
48
|
+
"PermissionDeniedError",
|
|
49
|
+
"RateLimitError",
|
|
50
|
+
"UnprocessableEntityError",
|
|
51
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from ._env import API_KEY_ENV_VAR, redact_api_key, resolve_base_url
|
|
9
|
+
from ._exceptions import AddisAIError
|
|
10
|
+
from ._transport import Transport
|
|
11
|
+
from .resources import Chat, Legacy, Speech, Translate, Voice, Voices
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AddisAI:
|
|
15
|
+
"""Client for the Addis AI API — voice, chat/LLM, speech-to-text, translation."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
api_key: Optional[str] = None,
|
|
21
|
+
base_url: Optional[str] = None,
|
|
22
|
+
timeout: float = 60.0,
|
|
23
|
+
max_retries: int = 3,
|
|
24
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
25
|
+
default_query: Optional[Dict[str, str]] = None,
|
|
26
|
+
http_client: Optional[httpx.Client] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
key = api_key or os.environ.get(API_KEY_ENV_VAR)
|
|
29
|
+
if not key:
|
|
30
|
+
raise AddisAIError(
|
|
31
|
+
f"Missing API key. Pass api_key= or set the {API_KEY_ENV_VAR} environment variable."
|
|
32
|
+
)
|
|
33
|
+
self._api_key = key
|
|
34
|
+
self._transport = Transport(
|
|
35
|
+
api_key=key,
|
|
36
|
+
base_url=resolve_base_url(base_url),
|
|
37
|
+
timeout=timeout,
|
|
38
|
+
max_retries=max_retries,
|
|
39
|
+
default_headers=default_headers or {},
|
|
40
|
+
default_query=default_query or {},
|
|
41
|
+
http_client=http_client,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.chat = Chat(self._transport)
|
|
45
|
+
self.voice = Voice(self._transport)
|
|
46
|
+
self.voices = Voices(self._transport)
|
|
47
|
+
self.speech = Speech(self._transport)
|
|
48
|
+
self.translate = Translate(self._transport)
|
|
49
|
+
#: Deprecated. Use ``voice``.
|
|
50
|
+
self.legacy = Legacy(self._transport)
|
|
51
|
+
|
|
52
|
+
def close(self) -> None:
|
|
53
|
+
self._transport.close()
|
|
54
|
+
|
|
55
|
+
def __enter__(self) -> "AddisAI":
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, *exc: Any) -> None:
|
|
59
|
+
self.close()
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return f'AddisAI(api_key="{redact_api_key(self._api_key)}")'
|