fleet-python 0.2.81__py3-none-any.whl → 0.2.83__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.
- fleet/__init__.py +126 -4
- fleet/_async/__init__.py +2 -2
- fleet/_async/base.py +1 -1
- fleet/_async/client.py +549 -0
- fleet/agent/gemini_cua/agent.py +80 -42
- fleet/agent/gemini_cua/mcp_server.py +41 -319
- fleet/agent/orchestrator.py +82 -8
- fleet/agent/types.py +1 -1
- fleet/base.py +11 -1
- fleet/cli.py +107 -32
- fleet/client.py +518 -0
- fleet/eval/__init__.py +15 -0
- fleet/eval/uploader.py +231 -0
- fleet/exceptions.py +8 -0
- fleet/models.py +117 -0
- fleet/proxy/__init__.py +25 -0
- fleet/proxy/proxy.py +453 -0
- fleet/proxy/whitelist.py +244 -0
- fleet/utils/__init__.py +7 -0
- fleet/utils/http_logging.py +178 -0
- fleet/utils/logging.py +13 -0
- fleet/utils/playwright.py +440 -0
- {fleet_python-0.2.81.dist-info → fleet_python-0.2.83.dist-info}/METADATA +5 -1
- {fleet_python-0.2.81.dist-info → fleet_python-0.2.83.dist-info}/RECORD +28 -19
- {fleet_python-0.2.81.dist-info → fleet_python-0.2.83.dist-info}/WHEEL +0 -0
- {fleet_python-0.2.81.dist-info → fleet_python-0.2.83.dist-info}/entry_points.txt +0 -0
- {fleet_python-0.2.81.dist-info → fleet_python-0.2.83.dist-info}/licenses/LICENSE +0 -0
- {fleet_python-0.2.81.dist-info → fleet_python-0.2.83.dist-info}/top_level.txt +0 -0
fleet/_async/client.py
CHANGED
|
@@ -17,11 +17,17 @@
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import base64
|
|
19
19
|
import cloudpickle
|
|
20
|
+
import dataclasses
|
|
20
21
|
import httpx
|
|
21
22
|
import json
|
|
22
23
|
import logging
|
|
23
24
|
import os
|
|
25
|
+
from datetime import date, datetime
|
|
26
|
+
from decimal import Decimal
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from pathlib import Path
|
|
24
29
|
from typing import List, Optional, Dict, Any, TYPE_CHECKING, Union
|
|
30
|
+
from uuid import UUID
|
|
25
31
|
|
|
26
32
|
from .base import EnvironmentBase, AsyncWrapper
|
|
27
33
|
from ..models import (
|
|
@@ -37,12 +43,112 @@ from ..models import (
|
|
|
37
43
|
TaskUpdateRequest,
|
|
38
44
|
Run,
|
|
39
45
|
HeartbeatResponse,
|
|
46
|
+
SessionIngestRequest,
|
|
47
|
+
SessionIngestMessage,
|
|
48
|
+
SessionIngestResponse,
|
|
49
|
+
SessionStatus,
|
|
50
|
+
JobSessionsResponse,
|
|
51
|
+
SessionTranscriptResponse,
|
|
40
52
|
)
|
|
41
53
|
from .tasks import Task
|
|
42
54
|
|
|
43
55
|
if TYPE_CHECKING:
|
|
44
56
|
from .verifiers import AsyncVerifierFunction
|
|
45
57
|
|
|
58
|
+
|
|
59
|
+
def _json_default(x: Any) -> Any:
|
|
60
|
+
"""Default JSON serializer for non-native types."""
|
|
61
|
+
if isinstance(x, (datetime, date)):
|
|
62
|
+
return x.isoformat()
|
|
63
|
+
if isinstance(x, (UUID, Path)):
|
|
64
|
+
return str(x)
|
|
65
|
+
if isinstance(x, Decimal):
|
|
66
|
+
return float(x)
|
|
67
|
+
if isinstance(x, Enum):
|
|
68
|
+
return x.value
|
|
69
|
+
if isinstance(x, bytes):
|
|
70
|
+
return base64.b64encode(x).decode("utf-8")
|
|
71
|
+
if isinstance(x, set):
|
|
72
|
+
return list(x)
|
|
73
|
+
if dataclasses.is_dataclass(x) and not isinstance(x, type):
|
|
74
|
+
return dataclasses.asdict(x)
|
|
75
|
+
# Handle objects with __dict__ (generic objects)
|
|
76
|
+
if hasattr(x, "__dict__"):
|
|
77
|
+
return x.__dict__
|
|
78
|
+
raise TypeError(f"Not JSON serializable: {type(x)}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _to_dict(obj: Any) -> Any:
|
|
82
|
+
"""Convert any object to a JSON-serializable dict/value.
|
|
83
|
+
|
|
84
|
+
Handles:
|
|
85
|
+
- Pydantic v2 models (model_dump)
|
|
86
|
+
- Pydantic v1 models (.dict())
|
|
87
|
+
- dataclasses (asdict)
|
|
88
|
+
- TypedDict (just dict at runtime)
|
|
89
|
+
- Objects with __dict__
|
|
90
|
+
- Primitives pass through
|
|
91
|
+
"""
|
|
92
|
+
if obj is None:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Pydantic v2
|
|
96
|
+
if hasattr(obj, "model_dump"):
|
|
97
|
+
return obj.model_dump()
|
|
98
|
+
|
|
99
|
+
# Pydantic v1
|
|
100
|
+
if hasattr(obj, "dict") and callable(obj.dict):
|
|
101
|
+
return obj.dict()
|
|
102
|
+
|
|
103
|
+
# dataclass
|
|
104
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
105
|
+
return dataclasses.asdict(obj)
|
|
106
|
+
|
|
107
|
+
# Already a dict or list - recursively convert
|
|
108
|
+
if isinstance(obj, dict):
|
|
109
|
+
return {k: _to_dict(v) for k, v in obj.items()}
|
|
110
|
+
if isinstance(obj, list):
|
|
111
|
+
return [_to_dict(v) for v in obj]
|
|
112
|
+
|
|
113
|
+
# Primitives
|
|
114
|
+
if isinstance(obj, (str, int, float, bool, type(None))):
|
|
115
|
+
return obj
|
|
116
|
+
|
|
117
|
+
# bytes -> base64
|
|
118
|
+
if isinstance(obj, bytes):
|
|
119
|
+
return base64.b64encode(obj).decode("utf-8")
|
|
120
|
+
|
|
121
|
+
# datetime/date
|
|
122
|
+
if isinstance(obj, (datetime, date)):
|
|
123
|
+
return obj.isoformat()
|
|
124
|
+
|
|
125
|
+
# UUID, Path
|
|
126
|
+
if isinstance(obj, (UUID, Path)):
|
|
127
|
+
return str(obj)
|
|
128
|
+
|
|
129
|
+
# Enum
|
|
130
|
+
if isinstance(obj, Enum):
|
|
131
|
+
return obj.value
|
|
132
|
+
|
|
133
|
+
# Decimal
|
|
134
|
+
if isinstance(obj, Decimal):
|
|
135
|
+
return float(obj)
|
|
136
|
+
|
|
137
|
+
# set
|
|
138
|
+
if isinstance(obj, set):
|
|
139
|
+
return list(obj)
|
|
140
|
+
|
|
141
|
+
# Generic object with __dict__
|
|
142
|
+
if hasattr(obj, "__dict__"):
|
|
143
|
+
return {k: _to_dict(v) for k, v in obj.__dict__.items() if not k.startswith("_")}
|
|
144
|
+
|
|
145
|
+
# Fallback - try to convert, or return string representation
|
|
146
|
+
try:
|
|
147
|
+
json.dumps(obj)
|
|
148
|
+
return obj
|
|
149
|
+
except (TypeError, ValueError):
|
|
150
|
+
return str(obj)
|
|
151
|
+
|
|
46
152
|
from .instance import (
|
|
47
153
|
AsyncInstanceClient,
|
|
48
154
|
ResetRequest,
|
|
@@ -70,6 +176,166 @@ from .resources.mcp import AsyncMCPResource
|
|
|
70
176
|
logger = logging.getLogger(__name__)
|
|
71
177
|
|
|
72
178
|
|
|
179
|
+
class AsyncSession:
|
|
180
|
+
"""A session for logging agent interactions to Fleet.
|
|
181
|
+
|
|
182
|
+
This provides a simple interface for streaming messages during an agent run.
|
|
183
|
+
Messages are sent one-by-one as they happen.
|
|
184
|
+
|
|
185
|
+
Usage:
|
|
186
|
+
session = await fleet.session_async(
|
|
187
|
+
model="anthropic/claude-sonnet-4",
|
|
188
|
+
task_key="my_task",
|
|
189
|
+
instance_id=env.instance_id,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Log messages as they happen
|
|
193
|
+
await session.log(history, response)
|
|
194
|
+
|
|
195
|
+
# Complete when done
|
|
196
|
+
await session.complete() # or session.fail()
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
client: "AsyncFleet",
|
|
202
|
+
session_id: Optional[str] = None,
|
|
203
|
+
job_id: Optional[str] = None,
|
|
204
|
+
config: Optional[Any] = None,
|
|
205
|
+
model: Optional[str] = None,
|
|
206
|
+
task_key: Optional[str] = None,
|
|
207
|
+
instance_id: Optional[str] = None,
|
|
208
|
+
):
|
|
209
|
+
self.session_id = session_id
|
|
210
|
+
self.job_id = job_id
|
|
211
|
+
self.config = config
|
|
212
|
+
self.model = model
|
|
213
|
+
self.task_key = task_key
|
|
214
|
+
self.instance_id = instance_id
|
|
215
|
+
self._client = client
|
|
216
|
+
self._message_count = 0
|
|
217
|
+
self._logged_count = 0 # Track how many messages from history have been logged
|
|
218
|
+
self._config_sent = False # Only send config/model/task_key/instance_id on first log
|
|
219
|
+
|
|
220
|
+
async def log(self, history: List[Any], response: Any) -> SessionIngestResponse:
|
|
221
|
+
"""Log an LLM call to the session.
|
|
222
|
+
|
|
223
|
+
Pass the input history and the model response. The session tracks what's
|
|
224
|
+
already been logged and only sends new messages. Objects are automatically
|
|
225
|
+
serialized to JSON (supports Pydantic, dataclasses, TypedDict, etc.).
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
response = model.generate(history)
|
|
229
|
+
await session.log(history, response.content)
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
history: The input messages sent to the model
|
|
233
|
+
response: The model's response (any serializable object)
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
SessionIngestResponse with updated message count
|
|
237
|
+
"""
|
|
238
|
+
# Collect new history messages since last call
|
|
239
|
+
new_history = history[self._logged_count:]
|
|
240
|
+
|
|
241
|
+
# Update tracked count to include the response we're about to send
|
|
242
|
+
# This prevents the response from being sent again as "new history" in the next call
|
|
243
|
+
self._logged_count = len(history) + (1 if response is not None else 0)
|
|
244
|
+
|
|
245
|
+
# Build the payload - serialize history + response to JSON
|
|
246
|
+
payload: Dict[str, Any] = {
|
|
247
|
+
"history": [_to_dict(msg) for msg in new_history],
|
|
248
|
+
"response": _to_dict(response),
|
|
249
|
+
}
|
|
250
|
+
if self.session_id:
|
|
251
|
+
payload["session_id"] = self.session_id
|
|
252
|
+
if self.job_id:
|
|
253
|
+
payload["job_id"] = self.job_id
|
|
254
|
+
# Include config, model, task_key, instance_id on first log only
|
|
255
|
+
if not self._config_sent:
|
|
256
|
+
if self.config is not None:
|
|
257
|
+
payload["config"] = _to_dict(self.config)
|
|
258
|
+
if self.model is not None:
|
|
259
|
+
payload["model"] = self.model
|
|
260
|
+
if self.task_key is not None:
|
|
261
|
+
payload["task_key"] = self.task_key
|
|
262
|
+
if self.instance_id is not None:
|
|
263
|
+
payload["instance_id"] = self.instance_id
|
|
264
|
+
self._config_sent = True
|
|
265
|
+
|
|
266
|
+
if not new_history and response is None:
|
|
267
|
+
return SessionIngestResponse(
|
|
268
|
+
success=True,
|
|
269
|
+
session_id=self.session_id or "",
|
|
270
|
+
message_count=self._message_count,
|
|
271
|
+
created_new_session=False,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
result = await self._client._ingest_raw(payload=payload)
|
|
275
|
+
self._message_count = result.message_count
|
|
276
|
+
# Update session_id if this was the first log (new session created)
|
|
277
|
+
if not self.session_id and result.session_id:
|
|
278
|
+
self.session_id = result.session_id
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
async def complete(
|
|
282
|
+
self,
|
|
283
|
+
verifier_execution_id: Optional[str] = None,
|
|
284
|
+
) -> SessionIngestResponse:
|
|
285
|
+
"""Mark the session as completed successfully.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
verifier_execution_id: Optional ID of the verifier execution record
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
SessionIngestResponse with final state
|
|
292
|
+
"""
|
|
293
|
+
from datetime import datetime
|
|
294
|
+
|
|
295
|
+
payload: Dict[str, Any] = {
|
|
296
|
+
"session_id": self.session_id,
|
|
297
|
+
"status": "completed",
|
|
298
|
+
"ended_at": datetime.now().isoformat(),
|
|
299
|
+
}
|
|
300
|
+
if verifier_execution_id:
|
|
301
|
+
payload["verifier_execution_id"] = verifier_execution_id
|
|
302
|
+
|
|
303
|
+
response = await self._client._ingest_raw(payload)
|
|
304
|
+
self._message_count = response.message_count
|
|
305
|
+
return response
|
|
306
|
+
|
|
307
|
+
async def fail(
|
|
308
|
+
self,
|
|
309
|
+
verifier_execution_id: Optional[str] = None,
|
|
310
|
+
) -> SessionIngestResponse:
|
|
311
|
+
"""Mark the session as failed.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
verifier_execution_id: Optional ID of the verifier execution record
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
SessionIngestResponse with final state
|
|
318
|
+
"""
|
|
319
|
+
from datetime import datetime
|
|
320
|
+
|
|
321
|
+
payload: Dict[str, Any] = {
|
|
322
|
+
"session_id": self.session_id,
|
|
323
|
+
"status": "failed",
|
|
324
|
+
"ended_at": datetime.now().isoformat(),
|
|
325
|
+
}
|
|
326
|
+
if verifier_execution_id:
|
|
327
|
+
payload["verifier_execution_id"] = verifier_execution_id
|
|
328
|
+
|
|
329
|
+
response = await self._client._ingest_raw(payload)
|
|
330
|
+
self._message_count = response.message_count
|
|
331
|
+
return response
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def message_count(self) -> int:
|
|
335
|
+
"""Get the current message count."""
|
|
336
|
+
return self._message_count
|
|
337
|
+
|
|
338
|
+
|
|
73
339
|
class AsyncEnv(EnvironmentBase):
|
|
74
340
|
def __init__(self, client: Optional[AsyncWrapper], **kwargs):
|
|
75
341
|
super().__init__(**kwargs)
|
|
@@ -1025,6 +1291,289 @@ class AsyncFleet:
|
|
|
1025
1291
|
)
|
|
1026
1292
|
return TaskResponse(**response.json())
|
|
1027
1293
|
|
|
1294
|
+
# Sessions API methods
|
|
1295
|
+
|
|
1296
|
+
async def list_job_sessions(self, job_id: str) -> JobSessionsResponse:
|
|
1297
|
+
"""List all sessions for a job, grouped by task.
|
|
1298
|
+
|
|
1299
|
+
Args:
|
|
1300
|
+
job_id: The job ID
|
|
1301
|
+
|
|
1302
|
+
Returns:
|
|
1303
|
+
JobSessionsResponse containing sessions grouped by task with statistics
|
|
1304
|
+
"""
|
|
1305
|
+
response = await self.client.request("GET", f"/v1/sessions/job/{job_id}")
|
|
1306
|
+
return JobSessionsResponse(**response.json())
|
|
1307
|
+
|
|
1308
|
+
async def get_session_transcript(self, session_id: str) -> SessionTranscriptResponse:
|
|
1309
|
+
"""Get the transcript for a specific session.
|
|
1310
|
+
|
|
1311
|
+
Args:
|
|
1312
|
+
session_id: The session ID
|
|
1313
|
+
|
|
1314
|
+
Returns:
|
|
1315
|
+
SessionTranscriptResponse containing task, instance, verifier result, and messages
|
|
1316
|
+
"""
|
|
1317
|
+
response = await self.client.request(
|
|
1318
|
+
"GET", f"/v1/sessions/{session_id}/transcript"
|
|
1319
|
+
)
|
|
1320
|
+
return SessionTranscriptResponse(**response.json())
|
|
1321
|
+
|
|
1322
|
+
async def _ingest(
|
|
1323
|
+
self,
|
|
1324
|
+
messages: List[Dict[str, Any]],
|
|
1325
|
+
session_id: Optional[str] = None,
|
|
1326
|
+
model: Optional[str] = None,
|
|
1327
|
+
task_key: Optional[str] = None,
|
|
1328
|
+
job_id: Optional[str] = None,
|
|
1329
|
+
instance_id: Optional[str] = None,
|
|
1330
|
+
status: Optional[str] = None,
|
|
1331
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1332
|
+
started_at: Optional[str] = None,
|
|
1333
|
+
ended_at: Optional[str] = None,
|
|
1334
|
+
verifier_execution_id: Optional[str] = None,
|
|
1335
|
+
) -> SessionIngestResponse:
|
|
1336
|
+
"""Internal method to ingest session data."""
|
|
1337
|
+
message_objects = [SessionIngestMessage(**msg) for msg in messages]
|
|
1338
|
+
request = SessionIngestRequest(
|
|
1339
|
+
messages=message_objects,
|
|
1340
|
+
session_id=session_id,
|
|
1341
|
+
model=model,
|
|
1342
|
+
task_key=task_key,
|
|
1343
|
+
job_id=job_id,
|
|
1344
|
+
instance_id=instance_id,
|
|
1345
|
+
status=SessionStatus(status) if status else None,
|
|
1346
|
+
metadata=metadata,
|
|
1347
|
+
started_at=started_at,
|
|
1348
|
+
ended_at=ended_at,
|
|
1349
|
+
verifier_execution_id=verifier_execution_id,
|
|
1350
|
+
)
|
|
1351
|
+
response = await self.client.request(
|
|
1352
|
+
"POST",
|
|
1353
|
+
"/v1/sessions/ingest",
|
|
1354
|
+
json=request.model_dump(exclude_none=True),
|
|
1355
|
+
)
|
|
1356
|
+
return SessionIngestResponse(**response.json())
|
|
1357
|
+
|
|
1358
|
+
async def _ingest_raw(
|
|
1359
|
+
self,
|
|
1360
|
+
payload: Dict[str, Any],
|
|
1361
|
+
) -> SessionIngestResponse:
|
|
1362
|
+
"""Internal method to ingest raw session data as JSON.
|
|
1363
|
+
|
|
1364
|
+
This sends the history and response as-is to the backend,
|
|
1365
|
+
letting the backend handle format normalization.
|
|
1366
|
+
"""
|
|
1367
|
+
# Pre-serialize with our custom handler to ensure all types are JSON-safe
|
|
1368
|
+
json_str = json.dumps(payload, default=_json_default)
|
|
1369
|
+
clean_payload = json.loads(json_str)
|
|
1370
|
+
|
|
1371
|
+
response = await self.client.request(
|
|
1372
|
+
"POST",
|
|
1373
|
+
"/v1/traces/logs",
|
|
1374
|
+
json=clean_payload,
|
|
1375
|
+
)
|
|
1376
|
+
return SessionIngestResponse(**response.json())
|
|
1377
|
+
|
|
1378
|
+
def start_session(
|
|
1379
|
+
self,
|
|
1380
|
+
session_id: Optional[str] = None,
|
|
1381
|
+
job_id: Optional[str] = None,
|
|
1382
|
+
config: Optional[Any] = None,
|
|
1383
|
+
model: Optional[str] = None,
|
|
1384
|
+
task_key: Optional[str] = None,
|
|
1385
|
+
instance_id: Optional[str] = None,
|
|
1386
|
+
) -> AsyncSession:
|
|
1387
|
+
"""Start a new session for logging agent interactions.
|
|
1388
|
+
|
|
1389
|
+
This returns a Session object. The session is created on the backend
|
|
1390
|
+
when you call log() for the first time.
|
|
1391
|
+
|
|
1392
|
+
Args:
|
|
1393
|
+
session_id: Optional existing session ID to resume
|
|
1394
|
+
job_id: Optional job ID to associate with the session
|
|
1395
|
+
config: Optional config object (e.g., GenerateContentConfig) to log
|
|
1396
|
+
model: Optional model name to log
|
|
1397
|
+
task_key: Optional Fleet task key
|
|
1398
|
+
instance_id: Optional Fleet instance ID
|
|
1399
|
+
|
|
1400
|
+
Returns:
|
|
1401
|
+
AsyncSession object with log(), complete(), and fail() methods
|
|
1402
|
+
|
|
1403
|
+
Example:
|
|
1404
|
+
session = fleet_client.start_session(config=config, model="gpt-4", task_key="task_123")
|
|
1405
|
+
|
|
1406
|
+
# Log LLM calls during agent run
|
|
1407
|
+
await session.log(history, response)
|
|
1408
|
+
|
|
1409
|
+
# Complete when done
|
|
1410
|
+
await session.complete()
|
|
1411
|
+
"""
|
|
1412
|
+
return AsyncSession(
|
|
1413
|
+
client=self,
|
|
1414
|
+
session_id=session_id,
|
|
1415
|
+
job_id=job_id,
|
|
1416
|
+
config=config,
|
|
1417
|
+
model=model,
|
|
1418
|
+
task_key=task_key,
|
|
1419
|
+
instance_id=instance_id,
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
async def trace_job(self, name: str) -> str:
|
|
1423
|
+
"""Create a new trace job.
|
|
1424
|
+
|
|
1425
|
+
Args:
|
|
1426
|
+
name: Name of the job
|
|
1427
|
+
|
|
1428
|
+
Returns:
|
|
1429
|
+
The job_id string
|
|
1430
|
+
"""
|
|
1431
|
+
from fleet.models import TraceJobRequest, TraceJobResponse
|
|
1432
|
+
|
|
1433
|
+
request = TraceJobRequest(name=name)
|
|
1434
|
+
response = await self.client.request(
|
|
1435
|
+
"POST",
|
|
1436
|
+
"/v1/traces/jobs",
|
|
1437
|
+
json=request.model_dump(),
|
|
1438
|
+
)
|
|
1439
|
+
result = TraceJobResponse(**response.json())
|
|
1440
|
+
return result.job_id
|
|
1441
|
+
|
|
1442
|
+
async def create_session(
|
|
1443
|
+
self,
|
|
1444
|
+
model: Optional[str] = None,
|
|
1445
|
+
task_key: Optional[str] = None,
|
|
1446
|
+
job_id: Optional[str] = None,
|
|
1447
|
+
instance_id: Optional[str] = None,
|
|
1448
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1449
|
+
started_at: Optional[str] = None,
|
|
1450
|
+
initial_message: Optional[Dict[str, Any]] = None,
|
|
1451
|
+
) -> SessionIngestResponse:
|
|
1452
|
+
"""Create a new session, optionally with an initial message.
|
|
1453
|
+
|
|
1454
|
+
This is useful for streaming scenarios where you want to create
|
|
1455
|
+
a session first and then append messages one by one.
|
|
1456
|
+
|
|
1457
|
+
Args:
|
|
1458
|
+
model: Model identifier (e.g., "anthropic/claude-sonnet-4")
|
|
1459
|
+
task_key: Task key to associate with the session
|
|
1460
|
+
job_id: Job ID to associate with the session
|
|
1461
|
+
instance_id: Instance ID to associate with the session
|
|
1462
|
+
metadata: Additional metadata for the session
|
|
1463
|
+
started_at: ISO timestamp when session started
|
|
1464
|
+
initial_message: Optional first message dict with 'role' and 'content'
|
|
1465
|
+
|
|
1466
|
+
Returns:
|
|
1467
|
+
SessionIngestResponse containing session_id
|
|
1468
|
+
|
|
1469
|
+
Example:
|
|
1470
|
+
# Create session and get ID
|
|
1471
|
+
session = await fleet.create_session(
|
|
1472
|
+
model="anthropic/claude-sonnet-4",
|
|
1473
|
+
task_key="my_task",
|
|
1474
|
+
started_at=datetime.now().isoformat()
|
|
1475
|
+
)
|
|
1476
|
+
|
|
1477
|
+
# Append messages as they happen
|
|
1478
|
+
await fleet.append_message(session.session_id, {"role": "user", "content": "Hello"})
|
|
1479
|
+
await fleet.append_message(session.session_id, {"role": "assistant", "content": "Hi!"})
|
|
1480
|
+
"""
|
|
1481
|
+
# Use a placeholder message if none provided
|
|
1482
|
+
if initial_message:
|
|
1483
|
+
messages = [initial_message]
|
|
1484
|
+
else:
|
|
1485
|
+
messages = [{"role": "system", "content": "[session created]"}]
|
|
1486
|
+
|
|
1487
|
+
return await self._ingest(
|
|
1488
|
+
messages=messages,
|
|
1489
|
+
model=model,
|
|
1490
|
+
task_key=task_key,
|
|
1491
|
+
job_id=job_id,
|
|
1492
|
+
instance_id=instance_id,
|
|
1493
|
+
status="running",
|
|
1494
|
+
metadata=metadata,
|
|
1495
|
+
started_at=started_at,
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
async def append_message(
|
|
1499
|
+
self,
|
|
1500
|
+
session_id: str,
|
|
1501
|
+
message: Dict[str, Any],
|
|
1502
|
+
status: Optional[str] = None,
|
|
1503
|
+
ended_at: Optional[str] = None,
|
|
1504
|
+
) -> SessionIngestResponse:
|
|
1505
|
+
"""Append a single message to an existing session.
|
|
1506
|
+
|
|
1507
|
+
This is useful for streaming scenarios where you want to send
|
|
1508
|
+
messages one by one as they happen.
|
|
1509
|
+
|
|
1510
|
+
Args:
|
|
1511
|
+
session_id: The session ID to append to
|
|
1512
|
+
message: Message dict with 'role' and 'content' keys.
|
|
1513
|
+
Optional keys: 'tool_calls', 'tool_call_id', 'timestamp', 'tokens', 'metadata'
|
|
1514
|
+
status: Optional status update ("running", "completed", "failed")
|
|
1515
|
+
ended_at: ISO timestamp when session ended (set when completing)
|
|
1516
|
+
|
|
1517
|
+
Returns:
|
|
1518
|
+
SessionIngestResponse with updated message count
|
|
1519
|
+
|
|
1520
|
+
Example:
|
|
1521
|
+
# Append user message
|
|
1522
|
+
await fleet.append_message(session_id, {"role": "user", "content": "What's 2+2?"})
|
|
1523
|
+
|
|
1524
|
+
# Append assistant response
|
|
1525
|
+
await fleet.append_message(session_id, {"role": "assistant", "content": "4"})
|
|
1526
|
+
|
|
1527
|
+
# Complete the session
|
|
1528
|
+
await fleet.append_message(
|
|
1529
|
+
session_id,
|
|
1530
|
+
{"role": "assistant", "content": "Done!"},
|
|
1531
|
+
status="completed",
|
|
1532
|
+
ended_at=datetime.now().isoformat()
|
|
1533
|
+
)
|
|
1534
|
+
"""
|
|
1535
|
+
return await self._ingest(
|
|
1536
|
+
messages=[message],
|
|
1537
|
+
session_id=session_id,
|
|
1538
|
+
status=status,
|
|
1539
|
+
ended_at=ended_at,
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
async def complete_session(
|
|
1543
|
+
self,
|
|
1544
|
+
session_id: str,
|
|
1545
|
+
status: str = "completed",
|
|
1546
|
+
ended_at: Optional[str] = None,
|
|
1547
|
+
final_message: Optional[Dict[str, Any]] = None,
|
|
1548
|
+
) -> SessionIngestResponse:
|
|
1549
|
+
"""Mark a session as complete.
|
|
1550
|
+
|
|
1551
|
+
Args:
|
|
1552
|
+
session_id: The session ID to complete
|
|
1553
|
+
status: Final status ("completed", "failed", "cancelled")
|
|
1554
|
+
ended_at: ISO timestamp when session ended (defaults to now)
|
|
1555
|
+
final_message: Optional final message to append
|
|
1556
|
+
|
|
1557
|
+
Returns:
|
|
1558
|
+
SessionIngestResponse with final state
|
|
1559
|
+
"""
|
|
1560
|
+
from datetime import datetime as dt
|
|
1561
|
+
|
|
1562
|
+
if ended_at is None:
|
|
1563
|
+
ended_at = dt.now().isoformat()
|
|
1564
|
+
|
|
1565
|
+
if final_message:
|
|
1566
|
+
messages = [final_message]
|
|
1567
|
+
else:
|
|
1568
|
+
messages = [{"role": "system", "content": f"[session {status}]"}]
|
|
1569
|
+
|
|
1570
|
+
return await self._ingest(
|
|
1571
|
+
messages=messages,
|
|
1572
|
+
session_id=session_id,
|
|
1573
|
+
status=status,
|
|
1574
|
+
ended_at=ended_at,
|
|
1575
|
+
)
|
|
1576
|
+
|
|
1028
1577
|
async def _create_verifier_from_data(
|
|
1029
1578
|
self, verifier_id: str, verifier_key: str, verifier_code: str, verifier_sha: str, verifier_runtime_version: Optional[str] = None
|
|
1030
1579
|
) -> "AsyncVerifierFunction":
|