bimpeai 0.1.0.dev3__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.
Files changed (59) hide show
  1. bimpeai-0.1.0.dev3/.gitignore +20 -0
  2. bimpeai-0.1.0.dev3/PKG-INFO +301 -0
  3. bimpeai-0.1.0.dev3/README.md +282 -0
  4. bimpeai-0.1.0.dev3/examples/01_list_agents.py +8 -0
  5. bimpeai-0.1.0.dev3/examples/02_create_agent.py +9 -0
  6. bimpeai-0.1.0.dev3/examples/03_paginate_conversations.py +15 -0
  7. bimpeai-0.1.0.dev3/examples/04_handle_rate_limit.py +9 -0
  8. bimpeai-0.1.0.dev3/examples/05_send_message.py +11 -0
  9. bimpeai-0.1.0.dev3/examples/06_stream_messages.py +11 -0
  10. bimpeai-0.1.0.dev3/examples/07_async_list_agents.py +14 -0
  11. bimpeai-0.1.0.dev3/pyproject.toml +59 -0
  12. bimpeai-0.1.0.dev3/src/bimpeai/__init__.py +82 -0
  13. bimpeai-0.1.0.dev3/src/bimpeai/_async_client.py +151 -0
  14. bimpeai-0.1.0.dev3/src/bimpeai/_base_client.py +94 -0
  15. bimpeai-0.1.0.dev3/src/bimpeai/_client.py +151 -0
  16. bimpeai-0.1.0.dev3/src/bimpeai/_exceptions.py +195 -0
  17. bimpeai-0.1.0.dev3/src/bimpeai/_idempotency.py +11 -0
  18. bimpeai-0.1.0.dev3/src/bimpeai/_models.py +46 -0
  19. bimpeai-0.1.0.dev3/src/bimpeai/_request.py +31 -0
  20. bimpeai-0.1.0.dev3/src/bimpeai/_request_id.py +5 -0
  21. bimpeai-0.1.0.dev3/src/bimpeai/_retries.py +46 -0
  22. bimpeai-0.1.0.dev3/src/bimpeai/_sse.py +83 -0
  23. bimpeai-0.1.0.dev3/src/bimpeai/_version.py +6 -0
  24. bimpeai-0.1.0.dev3/src/bimpeai/pagination.py +84 -0
  25. bimpeai-0.1.0.dev3/src/bimpeai/py.typed +0 -0
  26. bimpeai-0.1.0.dev3/src/bimpeai/resources/__init__.py +0 -0
  27. bimpeai-0.1.0.dev3/src/bimpeai/resources/_specs.py +58 -0
  28. bimpeai-0.1.0.dev3/src/bimpeai/resources/agents.py +290 -0
  29. bimpeai-0.1.0.dev3/src/bimpeai/resources/calls.py +27 -0
  30. bimpeai-0.1.0.dev3/src/bimpeai/resources/conversations.py +361 -0
  31. bimpeai-0.1.0.dev3/src/bimpeai/resources/workflows.py +151 -0
  32. bimpeai-0.1.0.dev3/src/bimpeai/types/__init__.py +0 -0
  33. bimpeai-0.1.0.dev3/src/bimpeai/types/agents.py +176 -0
  34. bimpeai-0.1.0.dev3/src/bimpeai/types/calls.py +11 -0
  35. bimpeai-0.1.0.dev3/src/bimpeai/types/conversations.py +73 -0
  36. bimpeai-0.1.0.dev3/src/bimpeai/types/workflows.py +56 -0
  37. bimpeai-0.1.0.dev3/tests/integration/__init__.py +0 -0
  38. bimpeai-0.1.0.dev3/tests/integration/test_end_to_end.py +114 -0
  39. bimpeai-0.1.0.dev3/tests/integration/test_end_to_end_async.py +45 -0
  40. bimpeai-0.1.0.dev3/tests/integration/test_streaming_integration.py +55 -0
  41. bimpeai-0.1.0.dev3/tests/unit/test_agents.py +110 -0
  42. bimpeai-0.1.0.dev3/tests/unit/test_async_client_transport.py +114 -0
  43. bimpeai-0.1.0.dev3/tests/unit/test_base_client.py +65 -0
  44. bimpeai-0.1.0.dev3/tests/unit/test_calls.py +31 -0
  45. bimpeai-0.1.0.dev3/tests/unit/test_client_transport.py +141 -0
  46. bimpeai-0.1.0.dev3/tests/unit/test_conversations.py +66 -0
  47. bimpeai-0.1.0.dev3/tests/unit/test_exceptions.py +60 -0
  48. bimpeai-0.1.0.dev3/tests/unit/test_idempotency.py +19 -0
  49. bimpeai-0.1.0.dev3/tests/unit/test_messages.py +54 -0
  50. bimpeai-0.1.0.dev3/tests/unit/test_models.py +57 -0
  51. bimpeai-0.1.0.dev3/tests/unit/test_pagination.py +57 -0
  52. bimpeai-0.1.0.dev3/tests/unit/test_public_surface.py +46 -0
  53. bimpeai-0.1.0.dev3/tests/unit/test_request_id.py +13 -0
  54. bimpeai-0.1.0.dev3/tests/unit/test_retries.py +44 -0
  55. bimpeai-0.1.0.dev3/tests/unit/test_sse.py +42 -0
  56. bimpeai-0.1.0.dev3/tests/unit/test_streaming.py +89 -0
  57. bimpeai-0.1.0.dev3/tests/unit/test_types_agents.py +35 -0
  58. bimpeai-0.1.0.dev3/tests/unit/test_workflows.py +89 -0
  59. bimpeai-0.1.0.dev3/uv.lock +479 -0
@@ -0,0 +1,20 @@
1
+ node_modules
2
+ dist
3
+ .turbo
4
+ .cache
5
+ *.tsbuildinfo
6
+ coverage
7
+ .DS_Store
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ .idea
12
+ .vscode
13
+ *.log
14
+
15
+ # python
16
+ .venv
17
+ __pycache__/
18
+ *.py[cod]
19
+ .pytest_cache/
20
+ .ruff_cache/
@@ -0,0 +1,301 @@
1
+ Metadata-Version: 2.4
2
+ Name: bimpeai
3
+ Version: 0.1.0.dev3
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
@@ -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,8 @@
1
+ import os
2
+
3
+ from bimpeai import BimpeAI
4
+
5
+ client = BimpeAI(api_key=os.environ.get("BIMPEAI_API_KEY", ""))
6
+ page = client.agents.list(limit=50, sort="-created_at")
7
+ for agent in page:
8
+ print(agent.id, agent.name)
@@ -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 ''}")