bimpeai 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.
- bimpeai-0.1.0/.gitignore +20 -0
- bimpeai-0.1.0/PKG-INFO +301 -0
- bimpeai-0.1.0/README.md +282 -0
- bimpeai-0.1.0/examples/01_list_agents.py +8 -0
- bimpeai-0.1.0/examples/02_create_agent.py +9 -0
- bimpeai-0.1.0/examples/03_paginate_conversations.py +15 -0
- bimpeai-0.1.0/examples/04_handle_rate_limit.py +9 -0
- bimpeai-0.1.0/examples/05_send_message.py +11 -0
- bimpeai-0.1.0/examples/06_stream_messages.py +11 -0
- bimpeai-0.1.0/examples/07_async_list_agents.py +14 -0
- bimpeai-0.1.0/pyproject.toml +59 -0
- bimpeai-0.1.0/src/bimpeai/__init__.py +82 -0
- bimpeai-0.1.0/src/bimpeai/_async_client.py +151 -0
- bimpeai-0.1.0/src/bimpeai/_base_client.py +94 -0
- bimpeai-0.1.0/src/bimpeai/_client.py +151 -0
- bimpeai-0.1.0/src/bimpeai/_exceptions.py +195 -0
- bimpeai-0.1.0/src/bimpeai/_idempotency.py +11 -0
- bimpeai-0.1.0/src/bimpeai/_models.py +46 -0
- bimpeai-0.1.0/src/bimpeai/_request.py +31 -0
- bimpeai-0.1.0/src/bimpeai/_request_id.py +5 -0
- bimpeai-0.1.0/src/bimpeai/_retries.py +46 -0
- bimpeai-0.1.0/src/bimpeai/_sse.py +83 -0
- bimpeai-0.1.0/src/bimpeai/_version.py +6 -0
- bimpeai-0.1.0/src/bimpeai/pagination.py +84 -0
- bimpeai-0.1.0/src/bimpeai/py.typed +0 -0
- bimpeai-0.1.0/src/bimpeai/resources/__init__.py +0 -0
- bimpeai-0.1.0/src/bimpeai/resources/_specs.py +58 -0
- bimpeai-0.1.0/src/bimpeai/resources/agents.py +290 -0
- bimpeai-0.1.0/src/bimpeai/resources/calls.py +27 -0
- bimpeai-0.1.0/src/bimpeai/resources/conversations.py +361 -0
- bimpeai-0.1.0/src/bimpeai/resources/workflows.py +151 -0
- bimpeai-0.1.0/src/bimpeai/types/__init__.py +0 -0
- bimpeai-0.1.0/src/bimpeai/types/agents.py +176 -0
- bimpeai-0.1.0/src/bimpeai/types/calls.py +11 -0
- bimpeai-0.1.0/src/bimpeai/types/conversations.py +73 -0
- bimpeai-0.1.0/src/bimpeai/types/workflows.py +56 -0
- bimpeai-0.1.0/tests/integration/__init__.py +0 -0
- bimpeai-0.1.0/tests/integration/test_end_to_end.py +114 -0
- bimpeai-0.1.0/tests/integration/test_end_to_end_async.py +45 -0
- bimpeai-0.1.0/tests/integration/test_streaming_integration.py +55 -0
- bimpeai-0.1.0/tests/unit/test_agents.py +110 -0
- bimpeai-0.1.0/tests/unit/test_async_client_transport.py +114 -0
- bimpeai-0.1.0/tests/unit/test_base_client.py +65 -0
- bimpeai-0.1.0/tests/unit/test_calls.py +31 -0
- bimpeai-0.1.0/tests/unit/test_client_transport.py +141 -0
- bimpeai-0.1.0/tests/unit/test_conversations.py +66 -0
- bimpeai-0.1.0/tests/unit/test_exceptions.py +60 -0
- bimpeai-0.1.0/tests/unit/test_idempotency.py +19 -0
- bimpeai-0.1.0/tests/unit/test_messages.py +54 -0
- bimpeai-0.1.0/tests/unit/test_models.py +57 -0
- bimpeai-0.1.0/tests/unit/test_pagination.py +57 -0
- bimpeai-0.1.0/tests/unit/test_public_surface.py +46 -0
- bimpeai-0.1.0/tests/unit/test_request_id.py +13 -0
- bimpeai-0.1.0/tests/unit/test_retries.py +44 -0
- bimpeai-0.1.0/tests/unit/test_sse.py +42 -0
- bimpeai-0.1.0/tests/unit/test_streaming.py +89 -0
- bimpeai-0.1.0/tests/unit/test_types_agents.py +35 -0
- bimpeai-0.1.0/tests/unit/test_workflows.py +89 -0
- bimpeai-0.1.0/uv.lock +479 -0
bimpeai-0.1.0/.gitignore
ADDED
bimpeai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bimpeai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the BimpeAI Agent Console API
|
|
5
|
+
Project-URL: Repository, https://github.com/BimpeAI/bimpe-sdk
|
|
6
|
+
Author: BimpeAI
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: httpx>=0.28
|
|
16
|
+
Requires-Dist: pydantic>=2.9
|
|
17
|
+
Requires-Dist: typing-extensions>=4.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# bimpeai
|
|
21
|
+
|
|
22
|
+
Official Python SDK for the BimpeAI Agent Console API. It ships a synchronous client and an asynchronous one that share the same surface, with request and response types modelled in Pydantic and HTTP handled by httpx. Requires Python 3.10 or newer.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install bimpeai
|
|
28
|
+
# or: uv add bimpeai
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The only runtime dependencies are httpx, pydantic v2, and typing-extensions.
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from bimpeai import BimpeAI
|
|
37
|
+
|
|
38
|
+
client = BimpeAI(api_key="sk_...")
|
|
39
|
+
for agent in client.agents.list(limit=50):
|
|
40
|
+
print(agent.id, agent.name)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The async client mirrors it. Construct `AsyncBimpeAI`, await each call, and use `async for` to walk a list.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
|
|
48
|
+
from bimpeai import AsyncBimpeAI
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def main() -> None:
|
|
52
|
+
async with AsyncBimpeAI(api_key="sk_...") as client:
|
|
53
|
+
page = await client.agents.list(limit=50)
|
|
54
|
+
async for agent in page:
|
|
55
|
+
print(agent.id, agent.name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Authentication
|
|
62
|
+
|
|
63
|
+
Pass your team API key when you construct the client. The SDK sends it as `Authorization: Bearer <key>`; keys are prefixed `sk_`. The key is required, and constructing a client with an empty key raises `UserError` before any request goes out. The SDK does not read the key from the environment, so if you keep it in a variable like `BIMPEAI_API_KEY`, read it yourself and pass it in.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import os
|
|
67
|
+
|
|
68
|
+
client = BimpeAI(api_key=os.environ["BIMPEAI_API_KEY"])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A scope-restricted key works the same way. A call that falls outside the key's scope comes back as `PermissionDeniedError`.
|
|
72
|
+
|
|
73
|
+
## Clients and lifecycle
|
|
74
|
+
|
|
75
|
+
Both clients open an httpx client and own it for their lifetime. Use them as context managers so the connection pool is closed when you are done.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
with BimpeAI(api_key="sk_...") as client:
|
|
79
|
+
client.agents.list()
|
|
80
|
+
|
|
81
|
+
async with AsyncBimpeAI(api_key="sk_...") as client:
|
|
82
|
+
await client.agents.list()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If you would rather manage the lifetime yourself, call `client.close()` on the sync client or `await client.aclose()` on the async one. You can also hand in your own httpx client through the `http_client` argument, in which case the SDK uses it and leaves closing it to you.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import httpx
|
|
89
|
+
|
|
90
|
+
with httpx.Client(proxy="http://localhost:8080") as http:
|
|
91
|
+
client = BimpeAI(api_key="sk_...", http_client=http)
|
|
92
|
+
client.agents.list()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Every client exposes four resources: `agents`, `workflows`, `conversations`, and `calls`.
|
|
96
|
+
|
|
97
|
+
## Agents
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
agents = client.agents.list(page=2, limit=50, search="support", sort="-created_at")
|
|
101
|
+
agent = client.agents.create(name="Support bot", description="Tier 1 support", idempotency_key="op-1")
|
|
102
|
+
detail = client.agents.retrieve(agent.id)
|
|
103
|
+
client.agents.update(agent.id, description="Now tier 2 as well")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`list` returns a `Page[Agent]`. `create` and `update` take the agent fields as keyword arguments (`name`, `description`, `system_prompt`, `language`, `persona`, `agent_workflow_id`, `rules`, `timezone`, `logo`, the `business_*` fields, and `escalation_email`); only `name` is required on create, and every field is optional on update. `create` and `update` return an `Agent`, while `retrieve` returns an `AgentDetail`, which is an `Agent` plus the agent's integrations, channels, conversation flows, actions, and knowledge bases inlined.
|
|
107
|
+
|
|
108
|
+
The read-only sub-resources each return a plain list.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
client.agents.integrations.list(agent_id)
|
|
112
|
+
client.agents.channels.list(agent_id)
|
|
113
|
+
client.agents.conversation_flows.list(agent_id)
|
|
114
|
+
client.agents.actions.list(agent_id)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Knowledge bases support full CRUD. The create body is a text source or a URL source, distinguished by its `type`, and is passed as a single dict so the union stays well typed.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
client.agents.knowledge_bases.list(agent_id)
|
|
121
|
+
client.agents.knowledge_bases.create(agent_id, {"type": "text", "name": "FAQ", "content": "..."})
|
|
122
|
+
client.agents.knowledge_bases.create(agent_id, {"type": "url", "name": "Docs", "url": "https://..."})
|
|
123
|
+
client.agents.knowledge_bases.update(agent_id, kb_id, description="Updated")
|
|
124
|
+
client.agents.knowledge_bases.delete(agent_id, kb_id)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Workflows
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
workflows = client.workflows.list(scope="public", search="triage", sort="-created_at")
|
|
131
|
+
workflow = client.workflows.create(name="Triage", idempotency_key="op-2")
|
|
132
|
+
client.workflows.retrieve(workflow.id)
|
|
133
|
+
client.workflows.update(workflow.id, tags=["v2"])
|
|
134
|
+
client.workflows.delete(workflow.id)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`scope` is either `owned` or `public`. `list` returns a `Page[WorkflowSummary]`; `retrieve`, `create`, and `update` return a `Workflow`, which is the summary plus `system_prompt`, `rules`, `flows`, `tags`, and `prompt_config`. As with agents, create and update take the fields as keyword arguments and only `name` is required on create.
|
|
138
|
+
|
|
139
|
+
## Conversations and messages
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
conversations = client.conversations.list(agent_id, channel="whatsapp", search="invoice")
|
|
143
|
+
conversation = client.conversations.retrieve(agent_id, conversation_id)
|
|
144
|
+
|
|
145
|
+
messages = client.conversations.messages.list(agent_id, conversation_id)
|
|
146
|
+
sent = client.conversations.messages.send(agent_id, conversation_id, message="Hello")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`channel` accepts `whatsapp`, `messenger`, `instagram`, `webchat`, and the `test_*` variants of each. `conversations.list` returns a `Page[Conversation]` and `messages.list` returns a `Page[Message]`. `send` takes `message` and optional `attachments` (each `{"type": ..., "url": ...}`) and returns the created `Message`.
|
|
150
|
+
|
|
151
|
+
## Streaming messages
|
|
152
|
+
|
|
153
|
+
New messages in a conversation can be streamed in real time over Server-Sent Events. The flow has two steps. First the SDK asks the `stream-ticket` endpoint for a single-use, short-lived ticket. Then it opens a `GET` to the message-stream endpoint carrying that ticket as a query parameter, with `Accept: text/event-stream`. The stream is authenticated by the ticket, not the bearer key, so the API key never travels on the long-lived connection. `stream` runs both steps and yields messages as they arrive.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
for message in client.conversations.messages.stream(agent_id, conversation_id):
|
|
157
|
+
print(message.role, message.message)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Each value is a `StreamMessageEvent` with `id`, `conversation_id`, `role`, `message`, `message_type`, and `created_at`. The server also sends periodic heartbeat events to keep the connection open; the SDK consumes those itself and never yields them, so the loop only sees real messages.
|
|
161
|
+
|
|
162
|
+
If the connection drops, the SDK reconnects on its own. It remembers the id of the last message it gave you and resumes from there, so you neither miss a message nor see one twice. The retry budget counts consecutive failures and resets every time a message is delivered, so a stream that runs for hours before a blip still has its full set of retries. Set `reconnect=False` to stop instead of reconnecting when the server closes the stream, `max_retries` to change the reconnect budget (default 5), `after` to replay messages created after a given chat id or ISO-8601 timestamp, and `timeout` to bound the read. Stop a stream by breaking out of the loop.
|
|
163
|
+
|
|
164
|
+
The async client returns an async iterator with the same options.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
async for message in client.conversations.messages.stream(agent_id, conversation_id):
|
|
168
|
+
print(message.role, message.message)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The ticket step is available on its own if you want to open the stream yourself. The ticket is single-use and expires after `expires_in` seconds.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
ticket = client.conversations.messages.stream_ticket(agent_id, conversation_id)
|
|
175
|
+
print(ticket.ticket, ticket.expires_in)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Calls
|
|
179
|
+
|
|
180
|
+
`calls.list()` is wired up, but the API answers with 501 today, so it raises `APINotImplementedError`. The call site will keep working and start returning data once the endpoint ships, without an SDK change.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from bimpeai import APINotImplementedError
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
client.calls.list()
|
|
187
|
+
except APINotImplementedError:
|
|
188
|
+
... # not available yet
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Pagination
|
|
192
|
+
|
|
193
|
+
Every `list` returns a `Page` (or `AsyncPage` on the async client). A page carries the items for the current page in `data`, the `meta` block, and the `request_id` of the response that produced it. The `meta` is a `PaginationMeta` with `total_count`, `page_count`, `current_page`, `limit`, `has_next_page`, and `has_previous_page`.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
page = client.agents.list(limit=50)
|
|
197
|
+
page.data # list[Agent] for this page
|
|
198
|
+
page.meta.total_count if page.meta else 0
|
|
199
|
+
page.request_id # str | None
|
|
200
|
+
page.has_next_page # bool
|
|
201
|
+
next_page = page.get_next_page() # Page[Agent] | None
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Iterating the page walks every item across every page, fetching the next page only when the current one runs out.
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
for agent in client.agents.list(limit=50):
|
|
208
|
+
print(agent.id)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
If you want the page objects rather than the items, iterate `pages()`.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
for page in client.agents.list().pages():
|
|
215
|
+
print(page.meta.current_page if page.meta else None)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
On the async client these become `async for` and `await page.get_next_page()`.
|
|
219
|
+
|
|
220
|
+
## Errors
|
|
221
|
+
|
|
222
|
+
Every error raised by the SDK subclasses `BimpeAIError`. A `UserError` means the SDK rejected something before sending it, such as an empty API key. A connection that never produced a response raises `APIConnectionError`, and a timeout raises `APITimeoutError`, which is a subclass of it. Everything the server returned as an error subclasses `APIError`.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from bimpeai import RateLimitError, ValidationError
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
client.agents.create(name="")
|
|
229
|
+
except ValidationError as err:
|
|
230
|
+
for field in err.field_errors:
|
|
231
|
+
print(field["path"], field["message"])
|
|
232
|
+
except RateLimitError as err:
|
|
233
|
+
print("retry after", err.retry_after, "seconds")
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The hierarchy:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
BimpeAIError
|
|
240
|
+
├── UserError
|
|
241
|
+
├── APIConnectionError
|
|
242
|
+
│ └── APITimeoutError
|
|
243
|
+
└── APIError
|
|
244
|
+
├── BadRequestError
|
|
245
|
+
│ └── ValidationError
|
|
246
|
+
├── AuthenticationError
|
|
247
|
+
├── PermissionDeniedError
|
|
248
|
+
├── NotFoundError
|
|
249
|
+
├── ConflictError
|
|
250
|
+
├── RateLimitError
|
|
251
|
+
├── InternalServerError
|
|
252
|
+
└── APINotImplementedError
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Every `APIError` carries `status`, `code`, `request_id`, `headers`, and the raw `body`. `code` is one of the `ErrorCode` values (`validation_error`, `bad_request`, `unauthorized`, `api_key_missing`, `api_key_invalid`, `api_key_expired`, `insufficient_scope`, `forbidden`, `not_found`, `conflict`, `rate_limited`, `too_many_requests`, `not_implemented`, `agent_limit_reached`, `internal_error`). `ValidationError` adds `field_errors`, a list of `{"path", "message"}` dicts. `RateLimitError` adds `retry_after`, `limit`, `remaining`, and `reset_at`, read from the `Retry-After` and `X-RateLimit-*` response headers.
|
|
256
|
+
|
|
257
|
+
## Retries and idempotency
|
|
258
|
+
|
|
259
|
+
By default the SDK retries up to twice. It retries connection errors and timeouts, and the status codes 408, 429, and any 5xx other than 501; it never retries 409 or 501. Backoff is exponential with full jitter, and a 429 honours the `Retry-After` header. Change the budget per client or per call.
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
client = BimpeAI(api_key="sk_...", max_retries=3)
|
|
263
|
+
client.agents.create(name="A", max_retries=0) # this call only
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Write requests accept an `idempotency_key`. When retries are on and you do not supply one, the SDK generates a key once per call and reuses it across attempts, so a retried write cannot create a duplicate. The key is sent as the `Idempotency-Key` header.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
client.agents.create(name="A", idempotency_key="create-A-2026-06-14")
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Per-call options
|
|
273
|
+
|
|
274
|
+
The write methods (`agents.create`, `agents.update`, `agents.knowledge_bases.create`, `agents.knowledge_bases.update`, `workflows.create`, `workflows.update`, `conversations.messages.send`, and `conversations.messages.stream_ticket`) accept `idempotency_key`, `timeout`, `max_retries`, and `headers` as keyword arguments alongside the body. Each overrides the client-level setting for that one call. `headers` is merged into the request headers, which is the seam for sending a request id you control through `X-Request-Id`.
|
|
275
|
+
|
|
276
|
+
## Configuration
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
BimpeAI(
|
|
280
|
+
api_key="sk_...", # required
|
|
281
|
+
base_url="https://api.bimpe.ai", # default
|
|
282
|
+
timeout=30.0, # seconds, per request
|
|
283
|
+
max_retries=2,
|
|
284
|
+
default_headers=None, # sent on every request
|
|
285
|
+
http_client=None, # inject an httpx.Client / AsyncClient
|
|
286
|
+
)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`AsyncBimpeAI` takes the same arguments; only `http_client` differs, expecting an `httpx.AsyncClient`. The SDK targets the `/api/v1/console` paths under `base_url`, and identifies itself with a User-Agent like `bimpeai-python/<version> (Python/<py>; <os>)`.
|
|
290
|
+
|
|
291
|
+
## Types
|
|
292
|
+
|
|
293
|
+
Response models are Pydantic models that are frozen and tolerant of unknown fields, so a new field added server-side will not break deserialization and is reachable as an attribute. Request bodies are TypedDicts, and the create and update methods accept them as typed keyword arguments via `Unpack`, so a type checker flags an unknown or mistyped field at the call site.
|
|
294
|
+
|
|
295
|
+
## Requirements
|
|
296
|
+
|
|
297
|
+
Python 3.10, 3.11, 3.12, 3.13, and 3.14 are supported and tested.
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
MIT
|
bimpeai-0.1.0/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# bimpeai
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the BimpeAI Agent Console API. It ships a synchronous client and an asynchronous one that share the same surface, with request and response types modelled in Pydantic and HTTP handled by httpx. Requires Python 3.10 or newer.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install bimpeai
|
|
9
|
+
# or: uv add bimpeai
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The only runtime dependencies are httpx, pydantic v2, and typing-extensions.
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from bimpeai import BimpeAI
|
|
18
|
+
|
|
19
|
+
client = BimpeAI(api_key="sk_...")
|
|
20
|
+
for agent in client.agents.list(limit=50):
|
|
21
|
+
print(agent.id, agent.name)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The async client mirrors it. Construct `AsyncBimpeAI`, await each call, and use `async for` to walk a list.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
|
|
29
|
+
from bimpeai import AsyncBimpeAI
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def main() -> None:
|
|
33
|
+
async with AsyncBimpeAI(api_key="sk_...") as client:
|
|
34
|
+
page = await client.agents.list(limit=50)
|
|
35
|
+
async for agent in page:
|
|
36
|
+
print(agent.id, agent.name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Authentication
|
|
43
|
+
|
|
44
|
+
Pass your team API key when you construct the client. The SDK sends it as `Authorization: Bearer <key>`; keys are prefixed `sk_`. The key is required, and constructing a client with an empty key raises `UserError` before any request goes out. The SDK does not read the key from the environment, so if you keep it in a variable like `BIMPEAI_API_KEY`, read it yourself and pass it in.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import os
|
|
48
|
+
|
|
49
|
+
client = BimpeAI(api_key=os.environ["BIMPEAI_API_KEY"])
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A scope-restricted key works the same way. A call that falls outside the key's scope comes back as `PermissionDeniedError`.
|
|
53
|
+
|
|
54
|
+
## Clients and lifecycle
|
|
55
|
+
|
|
56
|
+
Both clients open an httpx client and own it for their lifetime. Use them as context managers so the connection pool is closed when you are done.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
with BimpeAI(api_key="sk_...") as client:
|
|
60
|
+
client.agents.list()
|
|
61
|
+
|
|
62
|
+
async with AsyncBimpeAI(api_key="sk_...") as client:
|
|
63
|
+
await client.agents.list()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If you would rather manage the lifetime yourself, call `client.close()` on the sync client or `await client.aclose()` on the async one. You can also hand in your own httpx client through the `http_client` argument, in which case the SDK uses it and leaves closing it to you.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import httpx
|
|
70
|
+
|
|
71
|
+
with httpx.Client(proxy="http://localhost:8080") as http:
|
|
72
|
+
client = BimpeAI(api_key="sk_...", http_client=http)
|
|
73
|
+
client.agents.list()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Every client exposes four resources: `agents`, `workflows`, `conversations`, and `calls`.
|
|
77
|
+
|
|
78
|
+
## Agents
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
agents = client.agents.list(page=2, limit=50, search="support", sort="-created_at")
|
|
82
|
+
agent = client.agents.create(name="Support bot", description="Tier 1 support", idempotency_key="op-1")
|
|
83
|
+
detail = client.agents.retrieve(agent.id)
|
|
84
|
+
client.agents.update(agent.id, description="Now tier 2 as well")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`list` returns a `Page[Agent]`. `create` and `update` take the agent fields as keyword arguments (`name`, `description`, `system_prompt`, `language`, `persona`, `agent_workflow_id`, `rules`, `timezone`, `logo`, the `business_*` fields, and `escalation_email`); only `name` is required on create, and every field is optional on update. `create` and `update` return an `Agent`, while `retrieve` returns an `AgentDetail`, which is an `Agent` plus the agent's integrations, channels, conversation flows, actions, and knowledge bases inlined.
|
|
88
|
+
|
|
89
|
+
The read-only sub-resources each return a plain list.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client.agents.integrations.list(agent_id)
|
|
93
|
+
client.agents.channels.list(agent_id)
|
|
94
|
+
client.agents.conversation_flows.list(agent_id)
|
|
95
|
+
client.agents.actions.list(agent_id)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Knowledge bases support full CRUD. The create body is a text source or a URL source, distinguished by its `type`, and is passed as a single dict so the union stays well typed.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
client.agents.knowledge_bases.list(agent_id)
|
|
102
|
+
client.agents.knowledge_bases.create(agent_id, {"type": "text", "name": "FAQ", "content": "..."})
|
|
103
|
+
client.agents.knowledge_bases.create(agent_id, {"type": "url", "name": "Docs", "url": "https://..."})
|
|
104
|
+
client.agents.knowledge_bases.update(agent_id, kb_id, description="Updated")
|
|
105
|
+
client.agents.knowledge_bases.delete(agent_id, kb_id)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Workflows
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
workflows = client.workflows.list(scope="public", search="triage", sort="-created_at")
|
|
112
|
+
workflow = client.workflows.create(name="Triage", idempotency_key="op-2")
|
|
113
|
+
client.workflows.retrieve(workflow.id)
|
|
114
|
+
client.workflows.update(workflow.id, tags=["v2"])
|
|
115
|
+
client.workflows.delete(workflow.id)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`scope` is either `owned` or `public`. `list` returns a `Page[WorkflowSummary]`; `retrieve`, `create`, and `update` return a `Workflow`, which is the summary plus `system_prompt`, `rules`, `flows`, `tags`, and `prompt_config`. As with agents, create and update take the fields as keyword arguments and only `name` is required on create.
|
|
119
|
+
|
|
120
|
+
## Conversations and messages
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
conversations = client.conversations.list(agent_id, channel="whatsapp", search="invoice")
|
|
124
|
+
conversation = client.conversations.retrieve(agent_id, conversation_id)
|
|
125
|
+
|
|
126
|
+
messages = client.conversations.messages.list(agent_id, conversation_id)
|
|
127
|
+
sent = client.conversations.messages.send(agent_id, conversation_id, message="Hello")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`channel` accepts `whatsapp`, `messenger`, `instagram`, `webchat`, and the `test_*` variants of each. `conversations.list` returns a `Page[Conversation]` and `messages.list` returns a `Page[Message]`. `send` takes `message` and optional `attachments` (each `{"type": ..., "url": ...}`) and returns the created `Message`.
|
|
131
|
+
|
|
132
|
+
## Streaming messages
|
|
133
|
+
|
|
134
|
+
New messages in a conversation can be streamed in real time over Server-Sent Events. The flow has two steps. First the SDK asks the `stream-ticket` endpoint for a single-use, short-lived ticket. Then it opens a `GET` to the message-stream endpoint carrying that ticket as a query parameter, with `Accept: text/event-stream`. The stream is authenticated by the ticket, not the bearer key, so the API key never travels on the long-lived connection. `stream` runs both steps and yields messages as they arrive.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
for message in client.conversations.messages.stream(agent_id, conversation_id):
|
|
138
|
+
print(message.role, message.message)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Each value is a `StreamMessageEvent` with `id`, `conversation_id`, `role`, `message`, `message_type`, and `created_at`. The server also sends periodic heartbeat events to keep the connection open; the SDK consumes those itself and never yields them, so the loop only sees real messages.
|
|
142
|
+
|
|
143
|
+
If the connection drops, the SDK reconnects on its own. It remembers the id of the last message it gave you and resumes from there, so you neither miss a message nor see one twice. The retry budget counts consecutive failures and resets every time a message is delivered, so a stream that runs for hours before a blip still has its full set of retries. Set `reconnect=False` to stop instead of reconnecting when the server closes the stream, `max_retries` to change the reconnect budget (default 5), `after` to replay messages created after a given chat id or ISO-8601 timestamp, and `timeout` to bound the read. Stop a stream by breaking out of the loop.
|
|
144
|
+
|
|
145
|
+
The async client returns an async iterator with the same options.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
async for message in client.conversations.messages.stream(agent_id, conversation_id):
|
|
149
|
+
print(message.role, message.message)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The ticket step is available on its own if you want to open the stream yourself. The ticket is single-use and expires after `expires_in` seconds.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
ticket = client.conversations.messages.stream_ticket(agent_id, conversation_id)
|
|
156
|
+
print(ticket.ticket, ticket.expires_in)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Calls
|
|
160
|
+
|
|
161
|
+
`calls.list()` is wired up, but the API answers with 501 today, so it raises `APINotImplementedError`. The call site will keep working and start returning data once the endpoint ships, without an SDK change.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from bimpeai import APINotImplementedError
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
client.calls.list()
|
|
168
|
+
except APINotImplementedError:
|
|
169
|
+
... # not available yet
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Pagination
|
|
173
|
+
|
|
174
|
+
Every `list` returns a `Page` (or `AsyncPage` on the async client). A page carries the items for the current page in `data`, the `meta` block, and the `request_id` of the response that produced it. The `meta` is a `PaginationMeta` with `total_count`, `page_count`, `current_page`, `limit`, `has_next_page`, and `has_previous_page`.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
page = client.agents.list(limit=50)
|
|
178
|
+
page.data # list[Agent] for this page
|
|
179
|
+
page.meta.total_count if page.meta else 0
|
|
180
|
+
page.request_id # str | None
|
|
181
|
+
page.has_next_page # bool
|
|
182
|
+
next_page = page.get_next_page() # Page[Agent] | None
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Iterating the page walks every item across every page, fetching the next page only when the current one runs out.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
for agent in client.agents.list(limit=50):
|
|
189
|
+
print(agent.id)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If you want the page objects rather than the items, iterate `pages()`.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
for page in client.agents.list().pages():
|
|
196
|
+
print(page.meta.current_page if page.meta else None)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
On the async client these become `async for` and `await page.get_next_page()`.
|
|
200
|
+
|
|
201
|
+
## Errors
|
|
202
|
+
|
|
203
|
+
Every error raised by the SDK subclasses `BimpeAIError`. A `UserError` means the SDK rejected something before sending it, such as an empty API key. A connection that never produced a response raises `APIConnectionError`, and a timeout raises `APITimeoutError`, which is a subclass of it. Everything the server returned as an error subclasses `APIError`.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from bimpeai import RateLimitError, ValidationError
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
client.agents.create(name="")
|
|
210
|
+
except ValidationError as err:
|
|
211
|
+
for field in err.field_errors:
|
|
212
|
+
print(field["path"], field["message"])
|
|
213
|
+
except RateLimitError as err:
|
|
214
|
+
print("retry after", err.retry_after, "seconds")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The hierarchy:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
BimpeAIError
|
|
221
|
+
├── UserError
|
|
222
|
+
├── APIConnectionError
|
|
223
|
+
│ └── APITimeoutError
|
|
224
|
+
└── APIError
|
|
225
|
+
├── BadRequestError
|
|
226
|
+
│ └── ValidationError
|
|
227
|
+
├── AuthenticationError
|
|
228
|
+
├── PermissionDeniedError
|
|
229
|
+
├── NotFoundError
|
|
230
|
+
├── ConflictError
|
|
231
|
+
├── RateLimitError
|
|
232
|
+
├── InternalServerError
|
|
233
|
+
└── APINotImplementedError
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Every `APIError` carries `status`, `code`, `request_id`, `headers`, and the raw `body`. `code` is one of the `ErrorCode` values (`validation_error`, `bad_request`, `unauthorized`, `api_key_missing`, `api_key_invalid`, `api_key_expired`, `insufficient_scope`, `forbidden`, `not_found`, `conflict`, `rate_limited`, `too_many_requests`, `not_implemented`, `agent_limit_reached`, `internal_error`). `ValidationError` adds `field_errors`, a list of `{"path", "message"}` dicts. `RateLimitError` adds `retry_after`, `limit`, `remaining`, and `reset_at`, read from the `Retry-After` and `X-RateLimit-*` response headers.
|
|
237
|
+
|
|
238
|
+
## Retries and idempotency
|
|
239
|
+
|
|
240
|
+
By default the SDK retries up to twice. It retries connection errors and timeouts, and the status codes 408, 429, and any 5xx other than 501; it never retries 409 or 501. Backoff is exponential with full jitter, and a 429 honours the `Retry-After` header. Change the budget per client or per call.
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
client = BimpeAI(api_key="sk_...", max_retries=3)
|
|
244
|
+
client.agents.create(name="A", max_retries=0) # this call only
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Write requests accept an `idempotency_key`. When retries are on and you do not supply one, the SDK generates a key once per call and reuses it across attempts, so a retried write cannot create a duplicate. The key is sent as the `Idempotency-Key` header.
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
client.agents.create(name="A", idempotency_key="create-A-2026-06-14")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Per-call options
|
|
254
|
+
|
|
255
|
+
The write methods (`agents.create`, `agents.update`, `agents.knowledge_bases.create`, `agents.knowledge_bases.update`, `workflows.create`, `workflows.update`, `conversations.messages.send`, and `conversations.messages.stream_ticket`) accept `idempotency_key`, `timeout`, `max_retries`, and `headers` as keyword arguments alongside the body. Each overrides the client-level setting for that one call. `headers` is merged into the request headers, which is the seam for sending a request id you control through `X-Request-Id`.
|
|
256
|
+
|
|
257
|
+
## Configuration
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
BimpeAI(
|
|
261
|
+
api_key="sk_...", # required
|
|
262
|
+
base_url="https://api.bimpe.ai", # default
|
|
263
|
+
timeout=30.0, # seconds, per request
|
|
264
|
+
max_retries=2,
|
|
265
|
+
default_headers=None, # sent on every request
|
|
266
|
+
http_client=None, # inject an httpx.Client / AsyncClient
|
|
267
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`AsyncBimpeAI` takes the same arguments; only `http_client` differs, expecting an `httpx.AsyncClient`. The SDK targets the `/api/v1/console` paths under `base_url`, and identifies itself with a User-Agent like `bimpeai-python/<version> (Python/<py>; <os>)`.
|
|
271
|
+
|
|
272
|
+
## Types
|
|
273
|
+
|
|
274
|
+
Response models are Pydantic models that are frozen and tolerant of unknown fields, so a new field added server-side will not break deserialization and is reachable as an attribute. Request bodies are TypedDicts, and the create and update methods accept them as typed keyword arguments via `Unpack`, so a type checker flags an unknown or mistyped field at the call site.
|
|
275
|
+
|
|
276
|
+
## Requirements
|
|
277
|
+
|
|
278
|
+
Python 3.10, 3.11, 3.12, 3.13, and 3.14 are supported and tested.
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from bimpeai import BimpeAI
|
|
4
|
+
|
|
5
|
+
client = BimpeAI(api_key=os.environ.get("BIMPEAI_API_KEY", ""))
|
|
6
|
+
agent = client.agents.create(
|
|
7
|
+
name="Support bot", description="Tier 1 support", idempotency_key="create-support-bot-v1"
|
|
8
|
+
)
|
|
9
|
+
print("created", agent.id)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from bimpeai import BimpeAI
|
|
5
|
+
|
|
6
|
+
if len(sys.argv) < 2:
|
|
7
|
+
raise SystemExit("usage: 03_paginate_conversations.py <agent_id>")
|
|
8
|
+
|
|
9
|
+
client = BimpeAI(api_key=os.environ.get("BIMPEAI_API_KEY", ""))
|
|
10
|
+
count = 0
|
|
11
|
+
for conversation in client.conversations.list(sys.argv[1], channel="whatsapp", limit=100):
|
|
12
|
+
count += 1
|
|
13
|
+
if count % 100 == 0:
|
|
14
|
+
print("seen", count, conversation.id)
|
|
15
|
+
print("total whatsapp conversations:", count)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from bimpeai import BimpeAI, RateLimitError
|
|
4
|
+
|
|
5
|
+
client = BimpeAI(api_key=os.environ.get("BIMPEAI_API_KEY", ""), max_retries=0)
|
|
6
|
+
try:
|
|
7
|
+
client.agents.list()
|
|
8
|
+
except RateLimitError as err:
|
|
9
|
+
print(f"rate limited; retry in {err.retry_after}s, remaining {err.remaining}")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from bimpeai import BimpeAI
|
|
5
|
+
|
|
6
|
+
if len(sys.argv) < 4:
|
|
7
|
+
raise SystemExit("usage: 05_send_message.py <agent_id> <conversation_id> <message>")
|
|
8
|
+
|
|
9
|
+
client = BimpeAI(api_key=os.environ.get("BIMPEAI_API_KEY", ""))
|
|
10
|
+
sent = client.conversations.messages.send(sys.argv[1], sys.argv[2], message=" ".join(sys.argv[3:]))
|
|
11
|
+
print("sent", sent.id)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from bimpeai import BimpeAI
|
|
5
|
+
|
|
6
|
+
if len(sys.argv) < 3:
|
|
7
|
+
raise SystemExit("usage: 06_stream_messages.py <agent_id> <conversation_id>")
|
|
8
|
+
|
|
9
|
+
client = BimpeAI(api_key=os.environ.get("BIMPEAI_API_KEY", ""))
|
|
10
|
+
for message in client.conversations.messages.stream(sys.argv[1], sys.argv[2]):
|
|
11
|
+
print(f"[{message.role}] {message.message or ''}")
|