conversational-agent-client 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: conversational-agent-client
3
+ Version: 0.1.0
4
+ Summary: Unofficial Python client for the Dust (dust.tt) conversational AI agent platform API
5
+ Author: Egor Nesterov
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Zaymerstone/dust-python-sdk
8
+ Project-URL: Repository, https://github.com/Zaymerstone/dust-python-sdk
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: requests
13
+ Dynamic: license-file
14
+
15
+ # dust-sdk (unofficial)
16
+
17
+ An unofficial Python client for the [Dust](https://dust.tt) API.
18
+
19
+ Dust ships an official [JavaScript/TypeScript SDK](https://docs.dust.tt/reference/javascript-sdk),
20
+ but has no official Python client - despite Python being the dominant
21
+ language for the data science, ML engineering, and automation teams
22
+ that make up a large part of Dust's target audience (their own
23
+ marketing highlights Data & Analytics as a core use case).
24
+
25
+ This project closes that gap.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install dust-sdk
31
+ ```
32
+
33
+ _(not yet published to PyPI — see [Status](#status) below)_
34
+
35
+ ## Quickstart
36
+
37
+ ```python
38
+ from dust_sdk.client import DustClient
39
+
40
+ client = DustClient(
41
+ api_key="your-dust-api-key",
42
+ workspace_id="your-workspace-id",
43
+ base_url="https://eu.dust.tt", # or https://dust.tt — see note below
44
+ )
45
+
46
+ # List agents available in your workspace
47
+ agents = client.list_agents()
48
+ for agent in agents:
49
+ print(agent["sId"], "-", agent["name"])
50
+
51
+ # Talk to an agent
52
+ conversation = client.create_conversation(
53
+ message_content="What can you help me with?",
54
+ agent_sid="dust",
55
+ )
56
+ answer = client.get_last_agent_message_text(conversation)
57
+ print(answer)
58
+ ```
59
+
60
+ ### ⚠️ `base_url` is required, no default
61
+
62
+ Dust hosts separate regional infrastructure (`https://dust.tt` for US,
63
+ `https://eu.dust.tt` for EU). Using the wrong one doesn't 404 — it
64
+ returns a misleading `invalid_api_key_error`, making it look like your
65
+ key is wrong when it's actually a region mismatch. Check which region
66
+ your workspace lives in (visible in your workspace URL) before making
67
+ your first call.
68
+
69
+ ## What's implemented
70
+
71
+ | Method | Operation | Verified against |
72
+ | --------------------------------- | --------------------- | ------------------------ |
73
+ | `list_agents()` | GET agent list | ✅ Live API call |
74
+ | `get_agent(sid)` | GET single agent | ✅ Live API call |
75
+ | `list_spaces()` | GET spaces | ✅ Live API call |
76
+ | `list_data_sources(space_id)` | GET data sources | ✅ Live API call |
77
+ | `list_documents(space_id, ds_id)` | GET documents | 📄 Official OpenAPI spec |
78
+ | `get_tables(space_id, ds_id)` | GET tables | 📄 Official OpenAPI spec |
79
+ | `create_conversation(...)` | POST new conversation | ✅ Live API call |
80
+ | `get_conversation(cid)` | GET conversation | ✅ Live API call |
81
+ | `import_agent(...)` | POST create agent | ✅ Live API call |
82
+ | `archive_agent(sid)` | DELETE (soft) agent | ✅ Live API call |
83
+
84
+ _"Live API call" means the response schema was confirmed against a
85
+ real request during development, not just documentation. "Official
86
+ OpenAPI spec" means it's based on Dust's published spec but hasn't
87
+ been round-tripped against a live response yet (usually because
88
+ testing it live requires resources — like a connected data source —
89
+ that weren't available in the development workspace)._
90
+
91
+ ## Known limitations
92
+
93
+ - **Message-sending is gated on Dust's Free plan.** Any endpoint that
94
+ invokes a model (`create_conversation` with an agent mention) returns
95
+ `429 rate_limit_error` on workspaces without a paid seat —
96
+ `Programmatic usage` is entirely disabled (`No access`) on Free,
97
+ independent of the regular in-app usage credits shown in the UI.
98
+ Write operations that _don't_ invoke a model (`import_agent`,
99
+ `archive_agent`) work fine on Free.
100
+ - **Some `Private` API endpoints aren't accessible via API key at all**,
101
+ regardless of plan — e.g. `POST /spaces` (creating a space) returns
102
+ `401 not_authenticated` even with a valid Bearer token, because it's
103
+ a session-only, web-app-internal endpoint despite appearing in the
104
+ public API reference.
105
+ - **The documentation contains at least one broken example URL.**
106
+ `GET /spaces` is shown at `https://dust.tt/api/w/{wId}/spaces`
107
+ (missing `/v1/`) — using that exact path returns a misleading
108
+ `401 not_authenticated` instead of a 404, making it look like an
109
+ auth problem. The correct path is `/api/v1/w/{wId}/spaces`.
110
+ - **Response shapes aren't consistent across endpoints.** Most list
111
+ endpoints wrap results in an object (e.g. `{"data_sources": [...]}`),
112
+ but `GET .../tables` returns a bare JSON array. This SDK normalizes
113
+ both into consistent Python return types, but it's worth knowing if
114
+ you're calling the raw API directly.
115
+ - **Dust's official OpenAPI spec has several inaccuracies**, found
116
+ through live testing:
117
+ - `agent.avatar_url` is required in practice, marked optional in the spec
118
+ - `editors` must be an array of email strings, not objects as the spec shows
119
+ - `generation_settings.reasoning_effort` is required but easy to miss
120
+ - Message `type` example values in the spec show `"human"`, but the
121
+ real API returns `"user_message"` / `"agent_message"`
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ git clone https://github.com/Zaymerstone/dust-python-sdk.git
127
+ cd dust-python-sdk
128
+ python -m venv venv
129
+ venv\Scripts\Activate.ps1 # Windows
130
+ pip install -e .
131
+ pip install pytest requests-mock
132
+ pytest -v
133
+ ```
134
+
135
+ Tests run entirely against recorded fixtures (`tests/fixtures/`) —
136
+ no live API calls or credits are required to run the test suite.
137
+
138
+ ## Status
139
+
140
+ This is an early-stage, unofficial project built to explore a gap in
141
+ Dust's SDK coverage. 10 methods are implemented and tested; the full
142
+ Dust API surface is 40+ endpoints. Contributions and feedback welcome.
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,7 @@
1
+ conversational_agent_client-0.1.0.dist-info/licenses/LICENSE,sha256=4AUZ_FM--_3PAwlpt6iqDwQC78rPgIboopKIUf_TLPM,1089
2
+ dust_sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ dust_sdk/client.py,sha256=7qaQ_YUb80WbJwtGqKmgL4XdYBIOpekwVOHwp3gM7GY,9167
4
+ conversational_agent_client-0.1.0.dist-info/METADATA,sha256=Fq3zeYe0wxLtdeG0nh2ciJTTgRBVFl_cWEhrX60gDj8,6191
5
+ conversational_agent_client-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
6
+ conversational_agent_client-0.1.0.dist-info/top_level.txt,sha256=0yubgCL7SL2-40eSRdvFCvPlLOePngrL_FTgSMKOFXY,9
7
+ conversational_agent_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zaymerstone
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.
dust_sdk/__init__.py ADDED
File without changes
dust_sdk/client.py ADDED
@@ -0,0 +1,226 @@
1
+ import requests
2
+
3
+
4
+ class DustAPIError(Exception):
5
+ """Base exception for all Dust API errors."""
6
+ pass
7
+
8
+
9
+ class DustClient:
10
+ def __init__(self, api_key: str, workspace_id: str, base_url: str):
11
+ """
12
+ base_url is required with no default on purpose: Dust hosts
13
+ separate regional infrastructure (e.g. https://eu.dust.tt
14
+ and https://dust.tt), and passing the wrong region produces
15
+ a confusing 'invalid_api_key_error' rather than a
16
+ 'wrong region' error. So we force the SDK user to specify
17
+ it explicitly.
18
+ """
19
+ self.api_key = api_key
20
+ self.workspace_id = workspace_id
21
+ self.base_url = base_url.rstrip("/")
22
+
23
+ def _headers(self) -> dict:
24
+ return {
25
+ "Authorization": f"Bearer {self.api_key}",
26
+ "Content-Type": "application/json",
27
+ }
28
+
29
+ def _handle_response(self, response: requests.Response) -> dict:
30
+ """
31
+ Shared response handling for all methods: status check and
32
+ JSON parsing. Extracted here after this same check started
33
+ being duplicated across 4 methods in a row (list_agents,
34
+ create_conversation, list_spaces, list_data_sources).
35
+ """
36
+ if response.status_code != 200:
37
+ raise DustAPIError(
38
+ f"Dust API returned {response.status_code}: {response.text}"
39
+ )
40
+ return response.json()
41
+
42
+ def list_agents(self) -> list[dict]:
43
+ """Returns the list of agent configurations in the workspace."""
44
+ url = f"{self.base_url}/api/v1/w/{self.workspace_id}/assistant/agent_configurations"
45
+ response = requests.get(url, headers=self._headers())
46
+ return self._handle_response(response)["agentConfigurations"]
47
+
48
+ def create_conversation(
49
+ self,
50
+ message_content: str,
51
+ agent_sid: str,
52
+ username: str = "sdk-user",
53
+ timezone: str = "UTC",
54
+ blocking: bool = True,
55
+ ) -> dict:
56
+ """
57
+ Creates a conversation and sends the first message to an agent.
58
+ Response schema confirmed against live data on 2026-07-09
59
+ (see NOTES.md).
60
+ """
61
+ url = f"{self.base_url}/api/v1/w/{self.workspace_id}/assistant/conversations"
62
+
63
+ payload = {
64
+ "message": {
65
+ "content": message_content,
66
+ "mentions": [{"configurationId": agent_sid}],
67
+ "context": {
68
+ "timezone": timezone,
69
+ "username": username,
70
+ },
71
+ },
72
+ "blocking": blocking,
73
+ }
74
+
75
+ response = requests.post(url, headers=self._headers(), json=payload)
76
+ return self._handle_response(response)["conversation"]
77
+
78
+ @staticmethod
79
+ def get_last_agent_message_text(conversation: dict) -> str | None:
80
+ """
81
+ Extracts the text of the last agent message.
82
+ conversation['content'] is a 2D array content[rank][version]:
83
+ rank = the message's position in the conversation, version =
84
+ edit/revision at that position. We take the latest version at
85
+ each rank and look for the last message of type agent_message.
86
+ """
87
+ for rank_group in reversed(conversation["content"]):
88
+ latest_version = rank_group[-1]
89
+ if latest_version.get("type") == "agent_message":
90
+ return latest_version.get("content")
91
+ return None
92
+
93
+ def list_spaces(self) -> list[dict]:
94
+ """Returns the list of spaces in the workspace."""
95
+ url = f"{self.base_url}/api/v1/w/{self.workspace_id}/spaces"
96
+ response = requests.get(url, headers=self._headers())
97
+ return self._handle_response(response)["spaces"]
98
+
99
+ def list_data_sources(self, space_id: str) -> list[dict]:
100
+ """Returns the list of data sources in the given space."""
101
+ url = f"{self.base_url}/api/v1/w/{self.workspace_id}/spaces/{space_id}/data_sources"
102
+ response = requests.get(url, headers=self._headers())
103
+ return self._handle_response(response)["data_sources"]
104
+
105
+ def get_agent(self, agent_sid: str) -> dict:
106
+ """Returns a single agent's configuration by its sId."""
107
+ url = f"{self.base_url}/api/v1/w/{self.workspace_id}/assistant/agent_configurations/{agent_sid}"
108
+ response = requests.get(url, headers=self._headers())
109
+ return self._handle_response(response)["agentConfiguration"]
110
+
111
+ def get_tables(self, space_id: str, data_source_id: str) -> list[dict]:
112
+ """
113
+ Returns the list of tables in the given data source.
114
+
115
+ Note: unlike the other methods, this endpoint returns a bare
116
+ JSON array ([...]) rather than a wrapper object (e.g.
117
+ {"data_sources": [...]}). Because of that, _handle_response()
118
+ can't be used as-is here — it assumes response.json() is a
119
+ dict, but here it's a list.
120
+ """
121
+ url = (
122
+ f"{self.base_url}/api/v1/w/{self.workspace_id}"
123
+ f"/spaces/{space_id}/data_sources/{data_source_id}/tables"
124
+ )
125
+ response = requests.get(url, headers=self._headers())
126
+
127
+ if response.status_code != 200:
128
+ raise DustAPIError(
129
+ f"Dust API returned {response.status_code}: {response.text}"
130
+ )
131
+
132
+ return response.json()
133
+
134
+ def list_documents(self, space_id: str, data_source_id: str) -> list[dict]:
135
+ """Returns the list of documents in the given data source."""
136
+ url = (
137
+ f"{self.base_url}/api/v1/w/{self.workspace_id}"
138
+ f"/spaces/{space_id}/data_sources/{data_source_id}/documents"
139
+ )
140
+ response = requests.get(url, headers=self._headers())
141
+ return self._handle_response(response)["documents"]
142
+
143
+ def get_conversation(self, conversation_id: str) -> dict:
144
+ """Returns a conversation by its id, including its full message history."""
145
+ url = (
146
+ f"{self.base_url}/api/v1/w/{self.workspace_id}"
147
+ f"/assistant/conversations/{conversation_id}"
148
+ )
149
+ response = requests.get(url, headers=self._headers())
150
+ return self._handle_response(response)["conversation"]
151
+
152
+ def import_agent(
153
+ self,
154
+ handle: str,
155
+ description: str,
156
+ instructions: str,
157
+ editors: list[str],
158
+ avatar_url: str,
159
+ model_id: str = "claude-sonnet-5",
160
+ provider_id: str = "anthropic",
161
+ temperature: float = 0.7,
162
+ reasoning_effort: str = "medium",
163
+ max_steps_per_run: int = 5,
164
+ scope: str = "hidden",
165
+ visualization_enabled: bool = False,
166
+ ) -> dict:
167
+ """
168
+ Creates a new agent in the workspace.
169
+
170
+ Field requirements were discovered empirically on 2026-07-09
171
+ through a series of live 400 errors (see NOTES.md) — the
172
+ official OpenAPI spec is inaccurate in several places:
173
+ - avatar_url is required, though marked optional in the spec
174
+ - editors is a list of strings (emails), not objects as shown
175
+ in the spec
176
+ - editors requires at least 1 element
177
+ - generation_settings.reasoning_effort is required
178
+
179
+ Note: unlike create_conversation, this method does NOT invoke
180
+ a model — it's a pure write operation, so it works even on
181
+ the Free plan despite "Programmatic access: No access".
182
+ """
183
+ url = (
184
+ f"{self.base_url}/api/v1/w/{self.workspace_id}"
185
+ f"/assistant/agent_configurations/import"
186
+ )
187
+
188
+ payload = {
189
+ "agent": {
190
+ "handle": handle,
191
+ "description": description,
192
+ "scope": scope,
193
+ "avatar_url": avatar_url,
194
+ "max_steps_per_run": max_steps_per_run,
195
+ "visualization_enabled": visualization_enabled,
196
+ },
197
+ "instructions": instructions,
198
+ "generation_settings": {
199
+ "model_id": model_id,
200
+ "provider_id": provider_id,
201
+ "temperature": temperature,
202
+ "reasoning_effort": reasoning_effort,
203
+ },
204
+ "tags": [],
205
+ "editors": editors,
206
+ "toolset": [],
207
+ }
208
+
209
+ response = requests.post(url, headers=self._headers(), json=payload)
210
+ return self._handle_response(response)["agentConfiguration"]
211
+
212
+ def archive_agent(self, agent_sid: str) -> dict:
213
+ """
214
+ Archives (soft-deletes) an agent by its sId.
215
+
216
+ Like import_agent, this operation does not invoke a model, so
217
+ it works on the Free plan despite "Programmatic access: No
218
+ access". Confirmed with a live call on 2026-07-09 — it worked
219
+ on the first try, with no validation errors.
220
+ """
221
+ url = (
222
+ f"{self.base_url}/api/v1/w/{self.workspace_id}"
223
+ f"/assistant/agent_configurations/{agent_sid}"
224
+ )
225
+ response = requests.delete(url, headers=self._headers())
226
+ return self._handle_response(response)