dimo-python-sdk 1.6.0__tar.gz → 1.7.1__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.
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/PKG-INFO +24 -2
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/README.md +23 -1
- dimo_python_sdk-1.7.1/dimo/api/conversations.py +343 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/dimo.py +2 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/environments.py +2 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo_python_sdk.egg-info/PKG-INFO +24 -2
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo_python_sdk.egg-info/SOURCES.txt +2 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/pyproject.toml +1 -1
- dimo_python_sdk-1.7.1/tests/test_conversations.py +693 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/LICENSE +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/__init__.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/api/__init__.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/api/attestation.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/api/auth.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/api/device_definitions.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/api/token_exchange.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/api/trips.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/api/valuations.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/api/vehicle_triggers.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/constants.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/errors.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/eth_signer.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/graphql/__init__.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/graphql/identity.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/graphql/telemetry.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/permission_decoder.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo/request.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo_python_sdk.egg-info/dependency_links.txt +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo_python_sdk.egg-info/requires.txt +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo_python_sdk.egg-info/top_level.txt +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/setup.cfg +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/tests/test_dimo.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/tests/test_errors.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/tests/test_permission_decoder.py +0 -0
- {dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/tests/test_request.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dimo-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.1
|
|
4
4
|
Summary: DIMO SDK in Python
|
|
5
5
|
Author-email: Barrett Kowalsky <barrettkowalsky@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/DIMO-Network/dimo-python-sdk
|
|
@@ -28,7 +28,29 @@ pip install dimo-python-sdk
|
|
|
28
28
|
|
|
29
29
|
## Unit Testing
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
The SDK includes comprehensive unit tests to ensure reliability and correctness. To run the tests:
|
|
32
|
+
|
|
33
|
+
1. **Install dependencies:**
|
|
34
|
+
```bash
|
|
35
|
+
pip install -r requirements.txt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. **Run all tests:**
|
|
39
|
+
```bash
|
|
40
|
+
pytest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
3. **Run tests with verbose output:**
|
|
44
|
+
```bash
|
|
45
|
+
pytest -v
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
4. **Run specific test files:**
|
|
49
|
+
```bash
|
|
50
|
+
pytest tests/test_conversations.py -v
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The test suite uses `pytest` and includes tests for all major SDK functionality including authentication, API endpoints, GraphQL queries, and error handling
|
|
32
54
|
|
|
33
55
|
## API Documentation
|
|
34
56
|
|
|
@@ -10,7 +10,29 @@ pip install dimo-python-sdk
|
|
|
10
10
|
|
|
11
11
|
## Unit Testing
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
The SDK includes comprehensive unit tests to ensure reliability and correctness. To run the tests:
|
|
14
|
+
|
|
15
|
+
1. **Install dependencies:**
|
|
16
|
+
```bash
|
|
17
|
+
pip install -r requirements.txt
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. **Run all tests:**
|
|
21
|
+
```bash
|
|
22
|
+
pytest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. **Run tests with verbose output:**
|
|
26
|
+
```bash
|
|
27
|
+
pytest -v
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
4. **Run specific test files:**
|
|
31
|
+
```bash
|
|
32
|
+
pytest tests/test_conversations.py -v
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The test suite uses `pytest` and includes tests for all major SDK functionality including authentication, API endpoints, GraphQL queries, and error handling
|
|
14
36
|
|
|
15
37
|
## API Documentation
|
|
16
38
|
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from dimo.errors import check_type, check_optional_type, HTTPError
|
|
2
|
+
from typing import Dict, List, Optional, Any, Generator
|
|
3
|
+
from requests import Session, RequestException
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Conversations:
|
|
8
|
+
"""
|
|
9
|
+
Client for the DIMO Conversations API.
|
|
10
|
+
|
|
11
|
+
This API enables developers to create conversational AI agents that can query
|
|
12
|
+
vehicle data, telemetry data, and perform web searches on behalf of users.
|
|
13
|
+
|
|
14
|
+
Key Features:
|
|
15
|
+
- Create AI agents with access to specific vehicles
|
|
16
|
+
- Query vehicle identity (make, model, owner) via GraphQL
|
|
17
|
+
- Query real-time telemetry (speed, fuel, location) via GraphQL
|
|
18
|
+
- Perform location-based web searches
|
|
19
|
+
- Stream responses in real-time using Server-Sent Events (SSE)
|
|
20
|
+
- Multi-agent delegation architecture with specialized subagents
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, request_method, get_auth_headers, get_full_path, session: Session):
|
|
24
|
+
self._request = request_method
|
|
25
|
+
self._get_auth_headers = get_auth_headers
|
|
26
|
+
self._get_full_path = get_full_path
|
|
27
|
+
self._session = session
|
|
28
|
+
|
|
29
|
+
def health_check(self) -> Dict:
|
|
30
|
+
"""
|
|
31
|
+
Check the service status and configuration.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict: Service health information including status, version, proxy, and default_model
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> dimo = DIMO("Production")
|
|
38
|
+
>>> health = dimo.conversations.health_check()
|
|
39
|
+
>>> print(health['status'])
|
|
40
|
+
"""
|
|
41
|
+
response = self._request("GET", "Conversations", "/")
|
|
42
|
+
return response
|
|
43
|
+
|
|
44
|
+
def create_agent(
|
|
45
|
+
self,
|
|
46
|
+
developer_jwt: str,
|
|
47
|
+
api_key: str,
|
|
48
|
+
user_wallet: str,
|
|
49
|
+
agent_type: str,
|
|
50
|
+
vehicle_ids: Optional[str] = None,
|
|
51
|
+
personality: str = "uncle_mechanic",
|
|
52
|
+
) -> Dict:
|
|
53
|
+
"""
|
|
54
|
+
Create a new conversational agent with the specified configuration.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
developer_jwt (str): Developer JWT token for authentication
|
|
58
|
+
api_key (str): DIMO API key for the agent to access vehicle data
|
|
59
|
+
user_wallet (str): User's wallet address (e.g., "0x2345...")
|
|
60
|
+
agent_type (str): The type of agent to create (e.g., "driver_agent_v1")
|
|
61
|
+
vehicle_ids (str, optional): JSON array string of vehicle token IDs (e.g., "[1, 2, 3]").
|
|
62
|
+
If not provided, agent will have access to all vehicles owned by the user.
|
|
63
|
+
personality (str, optional): Personality preset for the agent. Defaults to "uncle_mechanic"
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
dict: Agent information including agentId and configuration details
|
|
67
|
+
|
|
68
|
+
Behavior:
|
|
69
|
+
- Creates a new agent with the specified type and configuration
|
|
70
|
+
- Validates configuration and mode detection
|
|
71
|
+
- Creates/reuses shared identity subagent
|
|
72
|
+
- Creates per-vehicle telemetry subagents with token exchange
|
|
73
|
+
- Creates shared websearch subagent if enabled
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> dimo = DIMO("Production")
|
|
77
|
+
>>> dev_jwt = "your_developer_jwt"
|
|
78
|
+
>>> agent = dimo.conversations.create_agent(
|
|
79
|
+
... developer_jwt=dev_jwt,
|
|
80
|
+
... api_key="0x1234567890abcdef...",
|
|
81
|
+
... user_wallet="0x86b04f6d1D9E79aD7eB31cDEAF37442B00d64605",
|
|
82
|
+
... agent_type="driver_agent_v1",
|
|
83
|
+
... vehicle_ids="[1, 2, 3]",
|
|
84
|
+
... )
|
|
85
|
+
>>> print(agent['agentId'])
|
|
86
|
+
"""
|
|
87
|
+
check_type("developer_jwt", developer_jwt, str)
|
|
88
|
+
check_type("api_key", api_key, str)
|
|
89
|
+
check_type("user_wallet", user_wallet, str)
|
|
90
|
+
check_optional_type("vehicle_ids", vehicle_ids, str)
|
|
91
|
+
check_type("agent_type", agent_type, str)
|
|
92
|
+
check_type("personality", personality, str)
|
|
93
|
+
|
|
94
|
+
# Build variables dict
|
|
95
|
+
variables = {"USER_WALLET": user_wallet}
|
|
96
|
+
if vehicle_ids is not None:
|
|
97
|
+
variables["VEHICLE_IDS"] = vehicle_ids
|
|
98
|
+
|
|
99
|
+
# Build request body
|
|
100
|
+
body = {
|
|
101
|
+
"personality": personality,
|
|
102
|
+
"secrets": {"DIMO_API_KEY": api_key},
|
|
103
|
+
"type": agent_type,
|
|
104
|
+
"variables": variables,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
response = self._request(
|
|
108
|
+
"POST",
|
|
109
|
+
"Conversations",
|
|
110
|
+
"/agents",
|
|
111
|
+
headers=self._get_auth_headers(developer_jwt),
|
|
112
|
+
data=body,
|
|
113
|
+
)
|
|
114
|
+
return response
|
|
115
|
+
|
|
116
|
+
def delete_agent(self, developer_jwt: str, agent_id: str) -> Dict:
|
|
117
|
+
"""
|
|
118
|
+
Delete an agent and all associated resources.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
developer_jwt (str): Developer JWT token for authentication
|
|
122
|
+
agent_id (str): The agent ID to delete
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
dict: Confirmation message
|
|
126
|
+
|
|
127
|
+
Behavior:
|
|
128
|
+
- Deletes Letta agent from server
|
|
129
|
+
- Removes metadata from AgentManager
|
|
130
|
+
- Cleanup errors are logged but don't fail the request
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
>>> dimo = DIMO("Production")
|
|
134
|
+
>>> dev_jwt = "your_developer_jwt"
|
|
135
|
+
>>> result = dimo.conversations.delete_agent(
|
|
136
|
+
... developer_jwt=dev_jwt,
|
|
137
|
+
... agent_id="agent-abc123"
|
|
138
|
+
... )
|
|
139
|
+
>>> print(result['message'])
|
|
140
|
+
"""
|
|
141
|
+
check_type("developer_jwt", developer_jwt, str)
|
|
142
|
+
check_type("agent_id", agent_id, str)
|
|
143
|
+
|
|
144
|
+
response = self._request(
|
|
145
|
+
"DELETE",
|
|
146
|
+
"Conversations",
|
|
147
|
+
f"/agents/{agent_id}",
|
|
148
|
+
headers=self._get_auth_headers(developer_jwt),
|
|
149
|
+
)
|
|
150
|
+
return response
|
|
151
|
+
|
|
152
|
+
def send_message(
|
|
153
|
+
self,
|
|
154
|
+
developer_jwt: str,
|
|
155
|
+
agent_id: str,
|
|
156
|
+
message: str,
|
|
157
|
+
vehicle_ids: Optional[List[int]] = None,
|
|
158
|
+
user: Optional[str] = None,
|
|
159
|
+
) -> Dict:
|
|
160
|
+
"""
|
|
161
|
+
Send a message to an agent and receive the complete response (synchronous).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
developer_jwt (str): Developer JWT token for authentication
|
|
165
|
+
agent_id (str): The agent ID to send the message to
|
|
166
|
+
message (str): The message to send to the agent
|
|
167
|
+
vehicle_ids (list[int], optional): Optional vehicle IDs override
|
|
168
|
+
user (str, optional): Optional user override
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
dict: Response including agentId, message, response, vehiclesQueried, and timestamp
|
|
172
|
+
|
|
173
|
+
Behavior:
|
|
174
|
+
- Synchronous request/response
|
|
175
|
+
- Agent delegates to subagents as needed
|
|
176
|
+
- Returns full response after agent completes reasoning
|
|
177
|
+
- Timeout: 120 seconds for complex queries
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> dimo = DIMO("Production")
|
|
181
|
+
>>> dev_jwt = "your_developer_jwt"
|
|
182
|
+
>>> response = dimo.conversations.send_message(
|
|
183
|
+
... developer_jwt=dev_jwt,
|
|
184
|
+
... agent_id="agent-abc123",
|
|
185
|
+
... message="What's the make and model of my vehicle?"
|
|
186
|
+
... )
|
|
187
|
+
>>> print(response['response'])
|
|
188
|
+
"""
|
|
189
|
+
check_type("developer_jwt", developer_jwt, str)
|
|
190
|
+
check_type("agent_id", agent_id, str)
|
|
191
|
+
check_type("message", message, str)
|
|
192
|
+
check_optional_type("vehicle_ids", vehicle_ids, list)
|
|
193
|
+
check_optional_type("user", user, str)
|
|
194
|
+
|
|
195
|
+
body = {"message": message}
|
|
196
|
+
if vehicle_ids is not None:
|
|
197
|
+
body["vehicleIds"] = vehicle_ids
|
|
198
|
+
if user is not None:
|
|
199
|
+
body["user"] = user
|
|
200
|
+
|
|
201
|
+
response = self._request(
|
|
202
|
+
"POST",
|
|
203
|
+
"Conversations",
|
|
204
|
+
f"/agents/{agent_id}/message",
|
|
205
|
+
headers=self._get_auth_headers(developer_jwt),
|
|
206
|
+
data=body,
|
|
207
|
+
)
|
|
208
|
+
return response
|
|
209
|
+
|
|
210
|
+
def stream_message(
|
|
211
|
+
self,
|
|
212
|
+
developer_jwt: str,
|
|
213
|
+
agent_id: str,
|
|
214
|
+
message: str,
|
|
215
|
+
vehicle_ids: Optional[List[int]] = None,
|
|
216
|
+
user: Optional[str] = None,
|
|
217
|
+
) -> Generator[Dict[str, Any], None, None]:
|
|
218
|
+
"""
|
|
219
|
+
Send a message and receive real-time token-by-token streaming response via SSE.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
developer_jwt (str): Developer JWT token for authentication
|
|
223
|
+
agent_id (str): The agent ID to send the message to
|
|
224
|
+
message (str): The message to send to the agent
|
|
225
|
+
vehicle_ids (list[int], optional): Optional vehicle IDs override
|
|
226
|
+
user (str, optional): Optional user override
|
|
227
|
+
|
|
228
|
+
Yields:
|
|
229
|
+
dict: SSE events with either {"content": "token"} or {"done": true, ...metadata}
|
|
230
|
+
|
|
231
|
+
Behavior:
|
|
232
|
+
- Real-time streaming for better UX
|
|
233
|
+
- Token-by-token generation from LLM
|
|
234
|
+
- Final message includes metadata (agentId, vehiclesQueried)
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
>>> dimo = DIMO("Production")
|
|
238
|
+
>>> dev_jwt = "your_developer_jwt"
|
|
239
|
+
>>> for chunk in dimo.conversations.stream_message(
|
|
240
|
+
... developer_jwt=dev_jwt,
|
|
241
|
+
... agent_id="agent-abc123",
|
|
242
|
+
... message="What's my current speed?"
|
|
243
|
+
... ):
|
|
244
|
+
... if "content" in chunk:
|
|
245
|
+
... print(chunk["content"], end="", flush=True)
|
|
246
|
+
... elif "done" in chunk:
|
|
247
|
+
... print(f"\\nVehicles queried: {chunk['vehiclesQueried']}")
|
|
248
|
+
"""
|
|
249
|
+
check_type("developer_jwt", developer_jwt, str)
|
|
250
|
+
check_type("agent_id", agent_id, str)
|
|
251
|
+
check_type("message", message, str)
|
|
252
|
+
check_optional_type("vehicle_ids", vehicle_ids, list)
|
|
253
|
+
check_optional_type("user", user, str)
|
|
254
|
+
|
|
255
|
+
body = {"message": message}
|
|
256
|
+
if vehicle_ids is not None:
|
|
257
|
+
body["vehicleIds"] = vehicle_ids
|
|
258
|
+
if user is not None:
|
|
259
|
+
body["user"] = user
|
|
260
|
+
|
|
261
|
+
headers = self._get_auth_headers(developer_jwt)
|
|
262
|
+
headers["Accept"] = "text/event-stream"
|
|
263
|
+
headers["Content-Type"] = "application/json"
|
|
264
|
+
|
|
265
|
+
# Build full URL
|
|
266
|
+
url = self._get_full_path("Conversations", f"/agents/{agent_id}/stream")
|
|
267
|
+
|
|
268
|
+
# Make streaming request directly with session
|
|
269
|
+
try:
|
|
270
|
+
response = self._session.request(
|
|
271
|
+
method="POST",
|
|
272
|
+
url=url,
|
|
273
|
+
headers=headers,
|
|
274
|
+
data=json.dumps(body),
|
|
275
|
+
stream=True,
|
|
276
|
+
)
|
|
277
|
+
response.raise_for_status()
|
|
278
|
+
except RequestException as exc:
|
|
279
|
+
status = getattr(exc.response, "status_code", None)
|
|
280
|
+
body_error = None
|
|
281
|
+
try:
|
|
282
|
+
body_error = exc.response.json()
|
|
283
|
+
except Exception:
|
|
284
|
+
body_error = exc.response.text if exc.response else None
|
|
285
|
+
raise HTTPError(status=status or -1, message=str(exc), body=body_error)
|
|
286
|
+
|
|
287
|
+
# Parse SSE stream
|
|
288
|
+
for line in response.iter_lines():
|
|
289
|
+
if line:
|
|
290
|
+
line = line.decode("utf-8")
|
|
291
|
+
if line.startswith("data: "):
|
|
292
|
+
data = line[6:] # Remove "data: " prefix
|
|
293
|
+
try:
|
|
294
|
+
yield json.loads(data)
|
|
295
|
+
except json.JSONDecodeError:
|
|
296
|
+
# Skip malformed JSON
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
def get_history(
|
|
300
|
+
self,
|
|
301
|
+
developer_jwt: str,
|
|
302
|
+
agent_id: str,
|
|
303
|
+
limit: int = 100,
|
|
304
|
+
) -> Dict:
|
|
305
|
+
"""
|
|
306
|
+
Retrieve all messages in a conversation.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
developer_jwt (str): Developer JWT token for authentication
|
|
310
|
+
agent_id (str): The agent ID to get history for
|
|
311
|
+
limit (int): Maximum number of messages to return (default: 100)
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
dict: Conversation history including agentId, messages array, and total count
|
|
315
|
+
|
|
316
|
+
Behavior:
|
|
317
|
+
- Retrieves from Letta server
|
|
318
|
+
- Includes all message roles (user, agent, system)
|
|
319
|
+
- Reverse chronological order (newest first)
|
|
320
|
+
|
|
321
|
+
Example:
|
|
322
|
+
>>> dimo = DIMO("Production")
|
|
323
|
+
>>> dev_jwt = "your_developer_jwt"
|
|
324
|
+
>>> history = dimo.conversations.get_history(
|
|
325
|
+
... developer_jwt=dev_jwt,
|
|
326
|
+
... agent_id="agent-abc123",
|
|
327
|
+
... limit=50
|
|
328
|
+
... )
|
|
329
|
+
>>> for msg in history['messages']:
|
|
330
|
+
... print(f"{msg['role']}: {msg['content']}")
|
|
331
|
+
"""
|
|
332
|
+
check_type("developer_jwt", developer_jwt, str)
|
|
333
|
+
check_type("agent_id", agent_id, str)
|
|
334
|
+
check_type("limit", limit, int)
|
|
335
|
+
|
|
336
|
+
response = self._request(
|
|
337
|
+
"GET",
|
|
338
|
+
"Conversations",
|
|
339
|
+
f"/agents/{agent_id}/history",
|
|
340
|
+
headers=self._get_auth_headers(developer_jwt),
|
|
341
|
+
params={"limit": limit},
|
|
342
|
+
)
|
|
343
|
+
return response
|
|
@@ -2,6 +2,7 @@ from requests import Session
|
|
|
2
2
|
|
|
3
3
|
from .api.attestation import Attestation
|
|
4
4
|
from .api.auth import Auth
|
|
5
|
+
from .api.conversations import Conversations
|
|
5
6
|
from .api.device_definitions import DeviceDefinitions
|
|
6
7
|
from .api.token_exchange import TokenExchange
|
|
7
8
|
from .api.trips import Trips
|
|
@@ -75,6 +76,7 @@ class DIMO:
|
|
|
75
76
|
mapping = {
|
|
76
77
|
"attestation": (Attestation, ("request", "_get_auth_headers")),
|
|
77
78
|
"auth": (Auth, ("request", "_get_auth_headers", "env", "self")),
|
|
79
|
+
"conversations": (Conversations, ("request", "_get_auth_headers", "_get_full_path", "session")),
|
|
78
80
|
"device_definitions": (DeviceDefinitions, ("request", "_get_auth_headers")),
|
|
79
81
|
"token_exchange": (
|
|
80
82
|
TokenExchange,
|
|
@@ -2,6 +2,7 @@ dimo_environment = {
|
|
|
2
2
|
"Production": {
|
|
3
3
|
"Attestation": "https://attestation-api.dimo.zone",
|
|
4
4
|
"Auth": "https://auth.dimo.zone",
|
|
5
|
+
"Conversations": "https://conversations-api.dimo.zone",
|
|
5
6
|
"Identity": "https://identity-api.dimo.zone/query",
|
|
6
7
|
"DeviceDefinitions": "https://device-definitions-api.dimo.zone",
|
|
7
8
|
"Telemetry": "https://telemetry-api.dimo.zone/query",
|
|
@@ -14,6 +15,7 @@ dimo_environment = {
|
|
|
14
15
|
"Dev": {
|
|
15
16
|
"Attestation": "https://attestation-api.dev.dimo.zone",
|
|
16
17
|
"Auth": "https://auth.dev.dimo.zone",
|
|
18
|
+
"Conversations": "https://conversations-api.dev.dimo.zone",
|
|
17
19
|
"Identity": "https://identity-api.dev.dimo.zone/query",
|
|
18
20
|
"DeviceDefinitions": "https://device-definitions-api.dev.dimo.zone",
|
|
19
21
|
"Telemetry": "https://telemetry-api.dev.dimo.zone/query",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dimo-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.1
|
|
4
4
|
Summary: DIMO SDK in Python
|
|
5
5
|
Author-email: Barrett Kowalsky <barrettkowalsky@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/DIMO-Network/dimo-python-sdk
|
|
@@ -28,7 +28,29 @@ pip install dimo-python-sdk
|
|
|
28
28
|
|
|
29
29
|
## Unit Testing
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
The SDK includes comprehensive unit tests to ensure reliability and correctness. To run the tests:
|
|
32
|
+
|
|
33
|
+
1. **Install dependencies:**
|
|
34
|
+
```bash
|
|
35
|
+
pip install -r requirements.txt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. **Run all tests:**
|
|
39
|
+
```bash
|
|
40
|
+
pytest
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
3. **Run tests with verbose output:**
|
|
44
|
+
```bash
|
|
45
|
+
pytest -v
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
4. **Run specific test files:**
|
|
49
|
+
```bash
|
|
50
|
+
pytest tests/test_conversations.py -v
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The test suite uses `pytest` and includes tests for all major SDK functionality including authentication, API endpoints, GraphQL queries, and error handling
|
|
32
54
|
|
|
33
55
|
## API Documentation
|
|
34
56
|
|
|
@@ -12,6 +12,7 @@ dimo/request.py
|
|
|
12
12
|
dimo/api/__init__.py
|
|
13
13
|
dimo/api/attestation.py
|
|
14
14
|
dimo/api/auth.py
|
|
15
|
+
dimo/api/conversations.py
|
|
15
16
|
dimo/api/device_definitions.py
|
|
16
17
|
dimo/api/token_exchange.py
|
|
17
18
|
dimo/api/trips.py
|
|
@@ -25,6 +26,7 @@ dimo_python_sdk.egg-info/SOURCES.txt
|
|
|
25
26
|
dimo_python_sdk.egg-info/dependency_links.txt
|
|
26
27
|
dimo_python_sdk.egg-info/requires.txt
|
|
27
28
|
dimo_python_sdk.egg-info/top_level.txt
|
|
29
|
+
tests/test_conversations.py
|
|
28
30
|
tests/test_dimo.py
|
|
29
31
|
tests/test_errors.py
|
|
30
32
|
tests/test_permission_decoder.py
|
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the DIMO Conversations API.
|
|
3
|
+
|
|
4
|
+
These tests verify the functionality of the Conversations client including:
|
|
5
|
+
- Agent creation and deletion
|
|
6
|
+
- Synchronous and streaming message sending
|
|
7
|
+
- Conversation history retrieval
|
|
8
|
+
- Health checks
|
|
9
|
+
- Error handling
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
14
|
+
import pytest
|
|
15
|
+
from dimo.dimo import DIMO
|
|
16
|
+
from dimo.errors import HTTPError, DimoTypeError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestConversationsHealthCheck:
|
|
20
|
+
"""Test the health_check endpoint."""
|
|
21
|
+
|
|
22
|
+
def test_health_check_success(self, monkeypatch):
|
|
23
|
+
"""Test successful health check returns service status."""
|
|
24
|
+
client = DIMO(env="Dev")
|
|
25
|
+
|
|
26
|
+
# Mock the request method to return health data
|
|
27
|
+
fake_request = MagicMock(return_value={
|
|
28
|
+
"status": "healthy",
|
|
29
|
+
"version": "1.0.0",
|
|
30
|
+
"proxy": "active",
|
|
31
|
+
"default_model": "gpt-4"
|
|
32
|
+
})
|
|
33
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
34
|
+
|
|
35
|
+
result = client.conversations.health_check()
|
|
36
|
+
|
|
37
|
+
# Verify the request was called correctly
|
|
38
|
+
fake_request.assert_called_once_with("GET", "Conversations", "/")
|
|
39
|
+
|
|
40
|
+
# Verify the response
|
|
41
|
+
assert result["status"] == "healthy"
|
|
42
|
+
assert result["version"] == "1.0.0"
|
|
43
|
+
assert "default_model" in result
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestConversationsCreateAgent:
|
|
47
|
+
"""Test the create_agent endpoint."""
|
|
48
|
+
|
|
49
|
+
def test_create_agent_minimal(self, monkeypatch):
|
|
50
|
+
"""Test creating an agent with minimal required parameters (no vehicle_ids)."""
|
|
51
|
+
client = DIMO(env="Dev")
|
|
52
|
+
|
|
53
|
+
# Mock the request method
|
|
54
|
+
fake_request = MagicMock(return_value={
|
|
55
|
+
"agentId": "agent-abc123",
|
|
56
|
+
"type": "driver_agent_v1",
|
|
57
|
+
"personality": "uncle_mechanic",
|
|
58
|
+
"createdAt": "2024-01-01T00:00:00Z"
|
|
59
|
+
})
|
|
60
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
61
|
+
|
|
62
|
+
dev_jwt = "test_developer_jwt"
|
|
63
|
+
api_key = "0x1234567890abcdef"
|
|
64
|
+
user_wallet = "0x86b04f6d1D9E79aD7eB31cDEAF37442B00d64605"
|
|
65
|
+
|
|
66
|
+
result = client.conversations.create_agent(
|
|
67
|
+
developer_jwt=dev_jwt,
|
|
68
|
+
api_key=api_key,
|
|
69
|
+
user_wallet=user_wallet,
|
|
70
|
+
agent_type="driver_agent_v1"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Verify the request was called correctly
|
|
74
|
+
fake_request.assert_called_once()
|
|
75
|
+
args, kwargs = fake_request.call_args
|
|
76
|
+
|
|
77
|
+
assert args[0] == "POST"
|
|
78
|
+
assert args[1] == "Conversations"
|
|
79
|
+
assert args[2] == "/agents"
|
|
80
|
+
assert kwargs["data"]["type"] == "driver_agent_v1"
|
|
81
|
+
assert kwargs["data"]["personality"] == "uncle_mechanic"
|
|
82
|
+
assert kwargs["data"]["secrets"]["DIMO_API_KEY"] == api_key
|
|
83
|
+
assert kwargs["data"]["variables"]["USER_WALLET"] == user_wallet
|
|
84
|
+
assert "VEHICLE_IDS" not in kwargs["data"]["variables"]
|
|
85
|
+
|
|
86
|
+
# Verify the response
|
|
87
|
+
assert result["agentId"] == "agent-abc123"
|
|
88
|
+
assert result["type"] == "driver_agent_v1"
|
|
89
|
+
|
|
90
|
+
def test_create_agent_with_vehicle_ids(self, monkeypatch):
|
|
91
|
+
"""Test creating an agent with specific vehicle IDs."""
|
|
92
|
+
client = DIMO(env="Dev")
|
|
93
|
+
|
|
94
|
+
fake_request = MagicMock(return_value={
|
|
95
|
+
"agentId": "agent-def456",
|
|
96
|
+
"type": "driver_agent_v1",
|
|
97
|
+
"createdAt": "2024-01-01T00:00:00Z"
|
|
98
|
+
})
|
|
99
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
100
|
+
|
|
101
|
+
dev_jwt = "test_developer_jwt"
|
|
102
|
+
api_key = "0xabcdef123456"
|
|
103
|
+
user_wallet = "0x86b04f6d1D9E79aD7eB31cDEAF37442B00d64605"
|
|
104
|
+
vehicle_ids = "[872, 1234]"
|
|
105
|
+
|
|
106
|
+
result = client.conversations.create_agent(
|
|
107
|
+
developer_jwt=dev_jwt,
|
|
108
|
+
api_key=api_key,
|
|
109
|
+
user_wallet=user_wallet,
|
|
110
|
+
agent_type="driver_agent_v1",
|
|
111
|
+
vehicle_ids=vehicle_ids
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Verify the request
|
|
115
|
+
args, kwargs = fake_request.call_args
|
|
116
|
+
assert kwargs["data"]["secrets"]["DIMO_API_KEY"] == api_key
|
|
117
|
+
assert kwargs["data"]["variables"]["USER_WALLET"] == user_wallet
|
|
118
|
+
assert kwargs["data"]["variables"]["VEHICLE_IDS"] == vehicle_ids
|
|
119
|
+
|
|
120
|
+
# Verify the response
|
|
121
|
+
assert result["agentId"] == "agent-def456"
|
|
122
|
+
|
|
123
|
+
def test_create_agent_with_custom_personality(self, monkeypatch):
|
|
124
|
+
"""Test creating an agent with custom personality preset."""
|
|
125
|
+
client = DIMO(env="Dev")
|
|
126
|
+
|
|
127
|
+
fake_request = MagicMock(return_value={
|
|
128
|
+
"agentId": "agent-ghi789",
|
|
129
|
+
"type": "driver_agent_v1",
|
|
130
|
+
"personality": "helpful_assistant",
|
|
131
|
+
"createdAt": "2024-01-01T00:00:00Z"
|
|
132
|
+
})
|
|
133
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
134
|
+
|
|
135
|
+
result = client.conversations.create_agent(
|
|
136
|
+
developer_jwt="test_jwt",
|
|
137
|
+
api_key="0xapikey",
|
|
138
|
+
user_wallet="0xwallet",
|
|
139
|
+
agent_type="driver_agent_v1",
|
|
140
|
+
personality="helpful_assistant"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Verify the request
|
|
144
|
+
args, kwargs = fake_request.call_args
|
|
145
|
+
assert kwargs["data"]["personality"] == "helpful_assistant"
|
|
146
|
+
|
|
147
|
+
# Verify the response
|
|
148
|
+
assert result["personality"] == "helpful_assistant"
|
|
149
|
+
|
|
150
|
+
def test_create_agent_full_config(self, monkeypatch):
|
|
151
|
+
"""Test creating an agent with all configuration options."""
|
|
152
|
+
client = DIMO(env="Dev")
|
|
153
|
+
|
|
154
|
+
fake_request = MagicMock(return_value={
|
|
155
|
+
"agentId": "agent-full123",
|
|
156
|
+
"type": "driver_agent_v1",
|
|
157
|
+
"personality": "uncle_mechanic",
|
|
158
|
+
"createdAt": "2024-01-01T00:00:00Z"
|
|
159
|
+
})
|
|
160
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
161
|
+
|
|
162
|
+
result = client.conversations.create_agent(
|
|
163
|
+
developer_jwt="test_jwt",
|
|
164
|
+
api_key="0x1234567890abcdef",
|
|
165
|
+
user_wallet="0x86b04f6d1D9E79aD7eB31cDEAF37442B00d64605",
|
|
166
|
+
vehicle_ids="[1, 2, 3]",
|
|
167
|
+
agent_type="driver_agent_v1",
|
|
168
|
+
personality="uncle_mechanic"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Verify all fields are in request
|
|
172
|
+
args, kwargs = fake_request.call_args
|
|
173
|
+
assert kwargs["data"]["type"] == "driver_agent_v1"
|
|
174
|
+
assert kwargs["data"]["personality"] == "uncle_mechanic"
|
|
175
|
+
assert kwargs["data"]["secrets"]["DIMO_API_KEY"] == "0x1234567890abcdef"
|
|
176
|
+
assert kwargs["data"]["variables"]["USER_WALLET"] == "0x86b04f6d1D9E79aD7eB31cDEAF37442B00d64605"
|
|
177
|
+
assert kwargs["data"]["variables"]["VEHICLE_IDS"] == "[1, 2, 3]"
|
|
178
|
+
|
|
179
|
+
def test_create_agent_invalid_types(self):
|
|
180
|
+
"""Test that type checking is enforced for parameters."""
|
|
181
|
+
client = DIMO(env="Dev")
|
|
182
|
+
|
|
183
|
+
# Test invalid developer_jwt type
|
|
184
|
+
with pytest.raises(DimoTypeError):
|
|
185
|
+
client.conversations.create_agent(
|
|
186
|
+
developer_jwt=123, # Should be string
|
|
187
|
+
api_key="0xapikey",
|
|
188
|
+
user_wallet="0xwallet",
|
|
189
|
+
agent_type="driver_agent_v1"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Test invalid api_key type
|
|
193
|
+
with pytest.raises(DimoTypeError):
|
|
194
|
+
client.conversations.create_agent(
|
|
195
|
+
developer_jwt="test_jwt",
|
|
196
|
+
api_key=123, # Should be string
|
|
197
|
+
user_wallet="0xwallet",
|
|
198
|
+
agent_type="driver_agent_v1"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Test invalid user_wallet type
|
|
202
|
+
with pytest.raises(DimoTypeError):
|
|
203
|
+
client.conversations.create_agent(
|
|
204
|
+
developer_jwt="test_jwt",
|
|
205
|
+
api_key="0xapikey",
|
|
206
|
+
user_wallet=123, # Should be string
|
|
207
|
+
agent_type="driver_agent_v1"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Test invalid agent_type type
|
|
211
|
+
with pytest.raises(DimoTypeError):
|
|
212
|
+
client.conversations.create_agent(
|
|
213
|
+
developer_jwt="test_jwt",
|
|
214
|
+
api_key="0xapikey",
|
|
215
|
+
user_wallet="0xwallet",
|
|
216
|
+
agent_type=123 # Should be string
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Test invalid vehicle_ids type
|
|
220
|
+
with pytest.raises(DimoTypeError):
|
|
221
|
+
client.conversations.create_agent(
|
|
222
|
+
developer_jwt="test_jwt",
|
|
223
|
+
api_key="0xapikey",
|
|
224
|
+
user_wallet="0xwallet",
|
|
225
|
+
agent_type="driver_agent_v1",
|
|
226
|
+
vehicle_ids=123 # Should be string or None
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Test invalid personality type
|
|
230
|
+
with pytest.raises(DimoTypeError):
|
|
231
|
+
client.conversations.create_agent(
|
|
232
|
+
developer_jwt="test_jwt",
|
|
233
|
+
api_key="0xapikey",
|
|
234
|
+
user_wallet="0xwallet",
|
|
235
|
+
agent_type="driver_agent_v1",
|
|
236
|
+
personality=123 # Should be string
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TestConversationsDeleteAgent:
|
|
241
|
+
"""Test the delete_agent endpoint."""
|
|
242
|
+
|
|
243
|
+
def test_delete_agent_success(self, monkeypatch):
|
|
244
|
+
"""Test successful agent deletion."""
|
|
245
|
+
client = DIMO(env="Dev")
|
|
246
|
+
|
|
247
|
+
fake_request = MagicMock(return_value={
|
|
248
|
+
"message": "Agent deleted successfully",
|
|
249
|
+
"agentId": "agent-abc123"
|
|
250
|
+
})
|
|
251
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
252
|
+
|
|
253
|
+
dev_jwt = "test_developer_jwt"
|
|
254
|
+
agent_id = "agent-abc123"
|
|
255
|
+
|
|
256
|
+
result = client.conversations.delete_agent(
|
|
257
|
+
developer_jwt=dev_jwt,
|
|
258
|
+
agent_id=agent_id
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Verify the request
|
|
262
|
+
fake_request.assert_called_once()
|
|
263
|
+
args, kwargs = fake_request.call_args
|
|
264
|
+
|
|
265
|
+
assert args[0] == "DELETE"
|
|
266
|
+
assert args[1] == "Conversations"
|
|
267
|
+
assert args[2] == "/agents/agent-abc123"
|
|
268
|
+
|
|
269
|
+
# Verify the response
|
|
270
|
+
assert result["message"] == "Agent deleted successfully"
|
|
271
|
+
assert result["agentId"] == agent_id
|
|
272
|
+
|
|
273
|
+
def test_delete_agent_invalid_types(self):
|
|
274
|
+
"""Test that type checking is enforced."""
|
|
275
|
+
client = DIMO(env="Dev")
|
|
276
|
+
|
|
277
|
+
with pytest.raises(DimoTypeError):
|
|
278
|
+
client.conversations.delete_agent(
|
|
279
|
+
developer_jwt=123, # Should be string
|
|
280
|
+
agent_id="agent-abc123"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
with pytest.raises(DimoTypeError):
|
|
284
|
+
client.conversations.delete_agent(
|
|
285
|
+
developer_jwt="test_jwt",
|
|
286
|
+
agent_id=123 # Should be string
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class TestConversationsSendMessage:
|
|
291
|
+
"""Test the send_message endpoint (synchronous)."""
|
|
292
|
+
|
|
293
|
+
def test_send_message_basic(self, monkeypatch):
|
|
294
|
+
"""Test sending a basic message and receiving a response."""
|
|
295
|
+
client = DIMO(env="Dev")
|
|
296
|
+
|
|
297
|
+
fake_request = MagicMock(return_value={
|
|
298
|
+
"agentId": "agent-abc123",
|
|
299
|
+
"message": "What's my car's make and model?",
|
|
300
|
+
"response": "Your vehicle is a 2020 Tesla Model 3.",
|
|
301
|
+
"vehiclesQueried": [872],
|
|
302
|
+
"timestamp": "2024-01-01T00:00:00Z"
|
|
303
|
+
})
|
|
304
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
305
|
+
|
|
306
|
+
dev_jwt = "test_developer_jwt"
|
|
307
|
+
agent_id = "agent-abc123"
|
|
308
|
+
message = "What's my car's make and model?"
|
|
309
|
+
|
|
310
|
+
result = client.conversations.send_message(
|
|
311
|
+
developer_jwt=dev_jwt,
|
|
312
|
+
agent_id=agent_id,
|
|
313
|
+
message=message
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Verify the request
|
|
317
|
+
args, kwargs = fake_request.call_args
|
|
318
|
+
|
|
319
|
+
assert args[0] == "POST"
|
|
320
|
+
assert args[1] == "Conversations"
|
|
321
|
+
assert args[2] == "/agents/agent-abc123/message"
|
|
322
|
+
assert kwargs["data"]["message"] == message
|
|
323
|
+
|
|
324
|
+
# Verify the response
|
|
325
|
+
assert result["agentId"] == agent_id
|
|
326
|
+
assert result["response"] == "Your vehicle is a 2020 Tesla Model 3."
|
|
327
|
+
assert result["vehiclesQueried"] == [872]
|
|
328
|
+
|
|
329
|
+
def test_send_message_with_vehicle_ids_override(self, monkeypatch):
|
|
330
|
+
"""Test sending a message with vehicle IDs override."""
|
|
331
|
+
client = DIMO(env="Dev")
|
|
332
|
+
|
|
333
|
+
fake_request = MagicMock(return_value={
|
|
334
|
+
"agentId": "agent-abc123",
|
|
335
|
+
"message": "What's the speed?",
|
|
336
|
+
"response": "Current speed is 65 mph.",
|
|
337
|
+
"vehiclesQueried": [1234],
|
|
338
|
+
"timestamp": "2024-01-01T00:00:00Z"
|
|
339
|
+
})
|
|
340
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
341
|
+
|
|
342
|
+
result = client.conversations.send_message(
|
|
343
|
+
developer_jwt="test_jwt",
|
|
344
|
+
agent_id="agent-abc123",
|
|
345
|
+
message="What's the speed?",
|
|
346
|
+
vehicle_ids=[1234]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Verify vehicle_ids was included in request body
|
|
350
|
+
args, kwargs = fake_request.call_args
|
|
351
|
+
assert kwargs["data"]["vehicleIds"] == [1234]
|
|
352
|
+
assert result["vehiclesQueried"] == [1234]
|
|
353
|
+
|
|
354
|
+
def test_send_message_with_user_override(self, monkeypatch):
|
|
355
|
+
"""Test sending a message with user override."""
|
|
356
|
+
client = DIMO(env="Dev")
|
|
357
|
+
|
|
358
|
+
fake_request = MagicMock(return_value={
|
|
359
|
+
"agentId": "agent-abc123",
|
|
360
|
+
"message": "Hello",
|
|
361
|
+
"response": "Hi there!",
|
|
362
|
+
"vehiclesQueried": [],
|
|
363
|
+
"timestamp": "2024-01-01T00:00:00Z"
|
|
364
|
+
})
|
|
365
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
366
|
+
|
|
367
|
+
result = client.conversations.send_message(
|
|
368
|
+
developer_jwt="test_jwt",
|
|
369
|
+
agent_id="agent-abc123",
|
|
370
|
+
message="Hello",
|
|
371
|
+
user="0xnewuser"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Verify user was included in request body
|
|
375
|
+
args, kwargs = fake_request.call_args
|
|
376
|
+
assert kwargs["data"]["user"] == "0xnewuser"
|
|
377
|
+
|
|
378
|
+
def test_send_message_invalid_types(self):
|
|
379
|
+
"""Test that type checking is enforced."""
|
|
380
|
+
client = DIMO(env="Dev")
|
|
381
|
+
|
|
382
|
+
with pytest.raises(DimoTypeError):
|
|
383
|
+
client.conversations.send_message(
|
|
384
|
+
developer_jwt=123, # Should be string
|
|
385
|
+
agent_id="agent-abc123",
|
|
386
|
+
message="Hello"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
with pytest.raises(DimoTypeError):
|
|
390
|
+
client.conversations.send_message(
|
|
391
|
+
developer_jwt="test_jwt",
|
|
392
|
+
agent_id="agent-abc123",
|
|
393
|
+
message=123 # Should be string
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class TestConversationsStreamMessage:
|
|
398
|
+
"""Test the stream_message endpoint (SSE streaming)."""
|
|
399
|
+
|
|
400
|
+
def test_stream_message_success(self, monkeypatch):
|
|
401
|
+
"""Test streaming a message and receiving token-by-token response."""
|
|
402
|
+
client = DIMO(env="Dev")
|
|
403
|
+
|
|
404
|
+
# Mock SSE response data
|
|
405
|
+
sse_lines = [
|
|
406
|
+
b"data: {\"content\": \"Your\"}",
|
|
407
|
+
b"data: {\"content\": \" vehicle\"}",
|
|
408
|
+
b"data: {\"content\": \" is\"}",
|
|
409
|
+
b"data: {\"content\": \" a\"}",
|
|
410
|
+
b"data: {\"content\": \" Tesla.\"}",
|
|
411
|
+
b"data: {\"done\": true, \"agentId\": \"agent-abc123\", \"vehiclesQueried\": [872]}"
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
# Mock response object
|
|
415
|
+
mock_response = Mock()
|
|
416
|
+
mock_response.status_code = 200
|
|
417
|
+
mock_response.iter_lines = Mock(return_value=sse_lines)
|
|
418
|
+
mock_response.raise_for_status = Mock()
|
|
419
|
+
|
|
420
|
+
# Mock session.request
|
|
421
|
+
mock_session = Mock()
|
|
422
|
+
mock_session.request = Mock(return_value=mock_response)
|
|
423
|
+
monkeypatch.setattr(client.conversations, "_session", mock_session)
|
|
424
|
+
|
|
425
|
+
dev_jwt = "test_developer_jwt"
|
|
426
|
+
agent_id = "agent-abc123"
|
|
427
|
+
message = "What's my car?"
|
|
428
|
+
|
|
429
|
+
# Collect streamed chunks
|
|
430
|
+
chunks = list(client.conversations.stream_message(
|
|
431
|
+
developer_jwt=dev_jwt,
|
|
432
|
+
agent_id=agent_id,
|
|
433
|
+
message=message
|
|
434
|
+
))
|
|
435
|
+
|
|
436
|
+
# Verify we got all chunks
|
|
437
|
+
assert len(chunks) == 6
|
|
438
|
+
assert chunks[0] == {"content": "Your"}
|
|
439
|
+
assert chunks[1] == {"content": " vehicle"}
|
|
440
|
+
assert chunks[-1]["done"] is True
|
|
441
|
+
assert chunks[-1]["agentId"] == "agent-abc123"
|
|
442
|
+
assert chunks[-1]["vehiclesQueried"] == [872]
|
|
443
|
+
|
|
444
|
+
# Verify the request was made correctly
|
|
445
|
+
mock_session.request.assert_called_once()
|
|
446
|
+
call_args = mock_session.request.call_args
|
|
447
|
+
assert call_args[1]["method"] == "POST"
|
|
448
|
+
assert "/agents/agent-abc123/stream" in call_args[1]["url"]
|
|
449
|
+
assert call_args[1]["stream"] is True
|
|
450
|
+
assert call_args[1]["headers"]["Accept"] == "text/event-stream"
|
|
451
|
+
|
|
452
|
+
def test_stream_message_with_overrides(self, monkeypatch):
|
|
453
|
+
"""Test streaming with vehicle_ids and user overrides."""
|
|
454
|
+
client = DIMO(env="Dev")
|
|
455
|
+
|
|
456
|
+
# Mock minimal SSE response
|
|
457
|
+
sse_lines = [
|
|
458
|
+
b"data: {\"content\": \"Hello\"}",
|
|
459
|
+
b"data: {\"done\": true, \"agentId\": \"agent-abc123\", \"vehiclesQueried\": [1234]}"
|
|
460
|
+
]
|
|
461
|
+
|
|
462
|
+
mock_response = Mock()
|
|
463
|
+
mock_response.status_code = 200
|
|
464
|
+
mock_response.iter_lines = Mock(return_value=sse_lines)
|
|
465
|
+
mock_response.raise_for_status = Mock()
|
|
466
|
+
|
|
467
|
+
mock_session = Mock()
|
|
468
|
+
mock_session.request = Mock(return_value=mock_response)
|
|
469
|
+
monkeypatch.setattr(client.conversations, "_session", mock_session)
|
|
470
|
+
|
|
471
|
+
# Call with overrides
|
|
472
|
+
chunks = list(client.conversations.stream_message(
|
|
473
|
+
developer_jwt="test_jwt",
|
|
474
|
+
agent_id="agent-abc123",
|
|
475
|
+
message="Hello",
|
|
476
|
+
vehicle_ids=[1234],
|
|
477
|
+
user="0xnewuser"
|
|
478
|
+
))
|
|
479
|
+
|
|
480
|
+
# Verify the request included overrides in body
|
|
481
|
+
call_args = mock_session.request.call_args
|
|
482
|
+
body = json.loads(call_args[1]["data"])
|
|
483
|
+
assert body["message"] == "Hello"
|
|
484
|
+
assert body["vehicleIds"] == [1234]
|
|
485
|
+
assert body["user"] == "0xnewuser"
|
|
486
|
+
|
|
487
|
+
def test_stream_message_handles_malformed_json(self, monkeypatch):
|
|
488
|
+
"""Test that malformed JSON is skipped gracefully."""
|
|
489
|
+
client = DIMO(env="Dev")
|
|
490
|
+
|
|
491
|
+
# Mock SSE with malformed data
|
|
492
|
+
sse_lines = [
|
|
493
|
+
b"data: {\"content\": \"Good\"}",
|
|
494
|
+
b"data: {malformed json here", # This should be skipped
|
|
495
|
+
b"data: {\"content\": \"data\"}",
|
|
496
|
+
b"data: {\"done\": true, \"agentId\": \"agent-abc123\", \"vehiclesQueried\": []}"
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
mock_response = Mock()
|
|
500
|
+
mock_response.status_code = 200
|
|
501
|
+
mock_response.iter_lines = Mock(return_value=sse_lines)
|
|
502
|
+
mock_response.raise_for_status = Mock()
|
|
503
|
+
|
|
504
|
+
mock_session = Mock()
|
|
505
|
+
mock_session.request = Mock(return_value=mock_response)
|
|
506
|
+
monkeypatch.setattr(client.conversations, "_session", mock_session)
|
|
507
|
+
|
|
508
|
+
# Collect chunks - malformed one should be skipped
|
|
509
|
+
chunks = list(client.conversations.stream_message(
|
|
510
|
+
developer_jwt="test_jwt",
|
|
511
|
+
agent_id="agent-abc123",
|
|
512
|
+
message="Test"
|
|
513
|
+
))
|
|
514
|
+
|
|
515
|
+
# Should have 3 valid chunks (malformed one skipped)
|
|
516
|
+
assert len(chunks) == 3
|
|
517
|
+
assert chunks[0] == {"content": "Good"}
|
|
518
|
+
assert chunks[1] == {"content": "data"}
|
|
519
|
+
assert chunks[2]["done"] is True
|
|
520
|
+
|
|
521
|
+
def test_stream_message_http_error(self, monkeypatch):
|
|
522
|
+
"""Test that HTTP errors are properly raised."""
|
|
523
|
+
from requests import RequestException
|
|
524
|
+
|
|
525
|
+
client = DIMO(env="Dev")
|
|
526
|
+
|
|
527
|
+
# Mock a failed request
|
|
528
|
+
mock_response = Mock()
|
|
529
|
+
mock_response.status_code = 404
|
|
530
|
+
mock_response.json = Mock(return_value={"error": "Agent not found"})
|
|
531
|
+
|
|
532
|
+
mock_exception = RequestException("Not found")
|
|
533
|
+
mock_exception.response = mock_response
|
|
534
|
+
|
|
535
|
+
mock_session = Mock()
|
|
536
|
+
mock_session.request = Mock(side_effect=mock_exception)
|
|
537
|
+
monkeypatch.setattr(client.conversations, "_session", mock_session)
|
|
538
|
+
|
|
539
|
+
# Verify HTTPError is raised
|
|
540
|
+
with pytest.raises(HTTPError) as exc_info:
|
|
541
|
+
list(client.conversations.stream_message(
|
|
542
|
+
developer_jwt="test_jwt",
|
|
543
|
+
agent_id="bad-agent-id",
|
|
544
|
+
message="Test"
|
|
545
|
+
))
|
|
546
|
+
|
|
547
|
+
assert exc_info.value.status == 404
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
class TestConversationsGetHistory:
|
|
551
|
+
"""Test the get_history endpoint."""
|
|
552
|
+
|
|
553
|
+
def test_get_history_default_limit(self, monkeypatch):
|
|
554
|
+
"""Test retrieving conversation history with default limit."""
|
|
555
|
+
client = DIMO(env="Dev")
|
|
556
|
+
|
|
557
|
+
fake_request = MagicMock(return_value={
|
|
558
|
+
"agentId": "agent-abc123",
|
|
559
|
+
"messages": [
|
|
560
|
+
{"role": "user", "content": "Hello", "timestamp": "2024-01-01T00:00:00Z"},
|
|
561
|
+
{"role": "agent", "content": "Hi there!", "timestamp": "2024-01-01T00:00:01Z"}
|
|
562
|
+
],
|
|
563
|
+
"total": 2
|
|
564
|
+
})
|
|
565
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
566
|
+
|
|
567
|
+
dev_jwt = "test_developer_jwt"
|
|
568
|
+
agent_id = "agent-abc123"
|
|
569
|
+
|
|
570
|
+
result = client.conversations.get_history(
|
|
571
|
+
developer_jwt=dev_jwt,
|
|
572
|
+
agent_id=agent_id
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Verify the request
|
|
576
|
+
args, kwargs = fake_request.call_args
|
|
577
|
+
|
|
578
|
+
assert args[0] == "GET"
|
|
579
|
+
assert args[1] == "Conversations"
|
|
580
|
+
assert args[2] == "/agents/agent-abc123/history"
|
|
581
|
+
assert kwargs["params"]["limit"] == 100 # Default limit
|
|
582
|
+
|
|
583
|
+
# Verify the response
|
|
584
|
+
assert result["agentId"] == agent_id
|
|
585
|
+
assert len(result["messages"]) == 2
|
|
586
|
+
assert result["total"] == 2
|
|
587
|
+
|
|
588
|
+
def test_get_history_custom_limit(self, monkeypatch):
|
|
589
|
+
"""Test retrieving conversation history with custom limit."""
|
|
590
|
+
client = DIMO(env="Dev")
|
|
591
|
+
|
|
592
|
+
fake_request = MagicMock(return_value={
|
|
593
|
+
"agentId": "agent-abc123",
|
|
594
|
+
"messages": [
|
|
595
|
+
{"role": "user", "content": "Test", "timestamp": "2024-01-01T00:00:00Z"}
|
|
596
|
+
],
|
|
597
|
+
"total": 1
|
|
598
|
+
})
|
|
599
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
600
|
+
|
|
601
|
+
result = client.conversations.get_history(
|
|
602
|
+
developer_jwt="test_jwt",
|
|
603
|
+
agent_id="agent-abc123",
|
|
604
|
+
limit=50
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# Verify custom limit was used
|
|
608
|
+
args, kwargs = fake_request.call_args
|
|
609
|
+
assert kwargs["params"]["limit"] == 50
|
|
610
|
+
|
|
611
|
+
def test_get_history_invalid_types(self):
|
|
612
|
+
"""Test that type checking is enforced."""
|
|
613
|
+
client = DIMO(env="Dev")
|
|
614
|
+
|
|
615
|
+
with pytest.raises(DimoTypeError):
|
|
616
|
+
client.conversations.get_history(
|
|
617
|
+
developer_jwt=123, # Should be string
|
|
618
|
+
agent_id="agent-abc123"
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
with pytest.raises(DimoTypeError):
|
|
622
|
+
client.conversations.get_history(
|
|
623
|
+
developer_jwt="test_jwt",
|
|
624
|
+
agent_id=123 # Should be string
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
with pytest.raises(DimoTypeError):
|
|
628
|
+
client.conversations.get_history(
|
|
629
|
+
developer_jwt="test_jwt",
|
|
630
|
+
agent_id="agent-abc123",
|
|
631
|
+
limit="not_an_int" # Should be int
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class TestConversationsIntegration:
|
|
636
|
+
"""Integration tests demonstrating complete workflows."""
|
|
637
|
+
|
|
638
|
+
def test_full_agent_lifecycle(self, monkeypatch):
|
|
639
|
+
"""Test creating an agent, sending messages, and deleting it."""
|
|
640
|
+
client = DIMO(env="Dev")
|
|
641
|
+
|
|
642
|
+
# Track which endpoints are called
|
|
643
|
+
calls_made = []
|
|
644
|
+
|
|
645
|
+
def fake_request(*args, **kwargs):
|
|
646
|
+
calls_made.append((args[0], args[2]))
|
|
647
|
+
if args[0] == "POST" and args[2] == "/agents":
|
|
648
|
+
return {
|
|
649
|
+
"agentId": "agent-test123",
|
|
650
|
+
"type": "driver_agent_v1",
|
|
651
|
+
"personality": "uncle_mechanic",
|
|
652
|
+
"createdAt": "2024-01-01T00:00:00Z"
|
|
653
|
+
}
|
|
654
|
+
elif args[0] == "POST" and "/message" in args[2]:
|
|
655
|
+
return {
|
|
656
|
+
"agentId": "agent-test123",
|
|
657
|
+
"response": "Your vehicle is a Tesla.",
|
|
658
|
+
"vehiclesQueried": [872]
|
|
659
|
+
}
|
|
660
|
+
elif args[0] == "DELETE":
|
|
661
|
+
return {"message": "Agent deleted successfully"}
|
|
662
|
+
return {}
|
|
663
|
+
|
|
664
|
+
monkeypatch.setattr(client, "request", fake_request)
|
|
665
|
+
|
|
666
|
+
# 1. Create agent
|
|
667
|
+
agent = client.conversations.create_agent(
|
|
668
|
+
developer_jwt="test_jwt",
|
|
669
|
+
api_key="0x1234567890abcdef",
|
|
670
|
+
user_wallet="0x86b04f6d1D9E79aD7eB31cDEAF37442B00d64605",
|
|
671
|
+
agent_type="driver_agent_v1",
|
|
672
|
+
vehicle_ids="[872]"
|
|
673
|
+
)
|
|
674
|
+
assert agent["agentId"] == "agent-test123"
|
|
675
|
+
assert ("POST", "/agents") in calls_made
|
|
676
|
+
|
|
677
|
+
# 2. Send message
|
|
678
|
+
response = client.conversations.send_message(
|
|
679
|
+
developer_jwt="test_jwt",
|
|
680
|
+
agent_id=agent["agentId"],
|
|
681
|
+
message="What's my vehicle?"
|
|
682
|
+
)
|
|
683
|
+
assert response["agentId"] == "agent-test123"
|
|
684
|
+
assert "Tesla" in response["response"]
|
|
685
|
+
|
|
686
|
+
# 3. Delete agent
|
|
687
|
+
delete_result = client.conversations.delete_agent(
|
|
688
|
+
developer_jwt="test_jwt",
|
|
689
|
+
agent_id=agent["agentId"]
|
|
690
|
+
)
|
|
691
|
+
assert "deleted" in delete_result["message"].lower()
|
|
692
|
+
assert len(calls_made) == 3 # Verify all 3 operations were called
|
|
693
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dimo_python_sdk-1.6.0 → dimo_python_sdk-1.7.1}/dimo_python_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|