agent-player 1.0.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.
- agent_player-1.0.0.dist-info/METADATA +346 -0
- agent_player-1.0.0.dist-info/RECORD +20 -0
- agent_player-1.0.0.dist-info/WHEEL +5 -0
- agent_player-1.0.0.dist-info/licenses/LICENSE +21 -0
- agent_player-1.0.0.dist-info/top_level.txt +1 -0
- whitesands/__init__.py +1 -0
- whitesands/agent_player/__init__.py +34 -0
- whitesands/agent_player/agent_server.py +412 -0
- whitesands/agent_player/errors.py +35 -0
- whitesands/agent_player/events.py +22 -0
- whitesands/agent_player/logger.py +22 -0
- whitesands/agent_player/mcp.py +474 -0
- whitesands/agent_player/memory.py +101 -0
- whitesands/agent_player/platform_connection.py +429 -0
- whitesands/agent_player/protocol.py +98 -0
- whitesands/agent_player/py.typed +0 -0
- whitesands/agent_player/reflect.py +312 -0
- whitesands/agent_player/session_loop.py +332 -0
- whitesands/agent_player/testing.py +66 -0
- whitesands/agent_player/turn_gate.py +144 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""One in-process MCP server and one outbound connection to the platform.
|
|
2
|
+
|
|
3
|
+
Every start() is its own session. The game calls turn() to decide whose turn
|
|
4
|
+
it is. The platform chooses the action.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from whitesands.agent_player.errors import ConnectionLostError, TurnFailedError
|
|
15
|
+
from whitesands.agent_player.events import AgentEvent
|
|
16
|
+
from whitesands.agent_player.logger import log
|
|
17
|
+
from whitesands.agent_player.mcp import InMemoryTransport, SessionActionLog, SessionMcpServer
|
|
18
|
+
from whitesands.agent_player.memory import SessionMemory, default_memory_path
|
|
19
|
+
from whitesands.agent_player.platform_connection import PlatformConnection
|
|
20
|
+
from whitesands.agent_player.protocol import (
|
|
21
|
+
DEFAULT_AGENT_INSTRUCTIONS,
|
|
22
|
+
DEFAULT_PLATFORM_URL,
|
|
23
|
+
TokenUsage,
|
|
24
|
+
TurnReport,
|
|
25
|
+
turn_report_from_wire,
|
|
26
|
+
)
|
|
27
|
+
from whitesands.agent_player.reflect import reflect_player
|
|
28
|
+
from whitesands.agent_player.turn_gate import TurnGate
|
|
29
|
+
|
|
30
|
+
DEFAULT_TURN_TIMEOUT_MS = 60_000
|
|
31
|
+
|
|
32
|
+
EventListener = Callable[[AgentEvent], None]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class TurnResult:
|
|
37
|
+
"""What the agent did on one granted turn."""
|
|
38
|
+
|
|
39
|
+
action: str
|
|
40
|
+
args: dict[str, Any]
|
|
41
|
+
result: Any
|
|
42
|
+
attempts: int
|
|
43
|
+
usage: TokenUsage
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class StartSession:
|
|
48
|
+
"""What an inline platform receives for one player."""
|
|
49
|
+
|
|
50
|
+
session_id: str
|
|
51
|
+
session_token: str
|
|
52
|
+
name: str
|
|
53
|
+
tools: list[dict[str, Any]]
|
|
54
|
+
instructions: str
|
|
55
|
+
transport: Any
|
|
56
|
+
on_turn_result: Callable[[TurnReport], None]
|
|
57
|
+
goal: str | None = None
|
|
58
|
+
logger: Any = None
|
|
59
|
+
on_event: Callable[[AgentEvent], None] | None = None
|
|
60
|
+
on_usage: Callable[[int], Any] | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AgentHandle:
|
|
64
|
+
"""One started player. turn() grants that character one chance to act."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
session_id: str,
|
|
69
|
+
turn: Callable[..., Any],
|
|
70
|
+
stop: Callable[[], Any],
|
|
71
|
+
on: Callable[[EventListener], Callable[[], None]],
|
|
72
|
+
) -> None:
|
|
73
|
+
self.session_id = session_id
|
|
74
|
+
self._turn = turn
|
|
75
|
+
self._stop = stop
|
|
76
|
+
self._on = on
|
|
77
|
+
|
|
78
|
+
async def turn(self, *, timeout_ms: int | None = None, signal: Any = None) -> TurnResult:
|
|
79
|
+
"""Grant this session one turn.
|
|
80
|
+
|
|
81
|
+
Resolves with what the agent did, or raises after a second failed
|
|
82
|
+
action, a timeout, or a lost connection.
|
|
83
|
+
"""
|
|
84
|
+
return await self._turn(timeout_ms=timeout_ms, signal=signal)
|
|
85
|
+
|
|
86
|
+
async def stop(self) -> None:
|
|
87
|
+
"""Stop the remote agent for this session. The AgentServer stays up."""
|
|
88
|
+
await self._stop()
|
|
89
|
+
|
|
90
|
+
def on(self, listener: EventListener) -> Callable[[], None]:
|
|
91
|
+
"""Subscribe to this session's action, retry, and failure events."""
|
|
92
|
+
return self._on(listener)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _Session:
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
session_id: str,
|
|
99
|
+
token: str,
|
|
100
|
+
name: str,
|
|
101
|
+
gate: TurnGate,
|
|
102
|
+
server: SessionMcpServer,
|
|
103
|
+
) -> None:
|
|
104
|
+
self.id = session_id
|
|
105
|
+
self.token = token
|
|
106
|
+
self.name = name
|
|
107
|
+
self.gate = gate
|
|
108
|
+
self.server = server
|
|
109
|
+
self.listeners: set[EventListener] = set()
|
|
110
|
+
self.mux: Any = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AgentServer:
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
platform_key: str | None = None,
|
|
118
|
+
platform_url: str | None = None,
|
|
119
|
+
inline: Any = None,
|
|
120
|
+
instructions: str | None = None,
|
|
121
|
+
turn_timeout_ms: int | None = None,
|
|
122
|
+
on_event: EventListener | None = None,
|
|
123
|
+
logger: Any = None,
|
|
124
|
+
memory_path: str | None | bool = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
resolved_key = platform_key if platform_key is not None else os.environ.get("WSI_PLATFORM_KEY")
|
|
127
|
+
if inline is None and not resolved_key:
|
|
128
|
+
raise RuntimeError(
|
|
129
|
+
"Set WSI_PLATFORM_KEY or pass platform_key, unless an inline platform is provided"
|
|
130
|
+
)
|
|
131
|
+
self._platform_key = resolved_key or "inline"
|
|
132
|
+
self._platform_url = platform_url or os.environ.get("WSI_PLATFORM_URL") or DEFAULT_PLATFORM_URL
|
|
133
|
+
self._inline = inline
|
|
134
|
+
self._server_instructions = instructions
|
|
135
|
+
self._turn_timeout_ms = turn_timeout_ms
|
|
136
|
+
self._logger = logger
|
|
137
|
+
self._memory_path = memory_path
|
|
138
|
+
self._connection: PlatformConnection | None = None
|
|
139
|
+
self._sessions: dict[str, _Session] = {}
|
|
140
|
+
self._listeners: set[EventListener] = set()
|
|
141
|
+
self._memory: SessionMemory | None = None
|
|
142
|
+
self._started = False
|
|
143
|
+
self._closing = False
|
|
144
|
+
if on_event is not None:
|
|
145
|
+
self._listeners.add(on_event)
|
|
146
|
+
|
|
147
|
+
async def __aenter__(self) -> AgentServer:
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
async def __aexit__(self, *_exc: object) -> None:
|
|
151
|
+
await self.close()
|
|
152
|
+
|
|
153
|
+
async def connect(self) -> None:
|
|
154
|
+
"""Open the outbound platform connection. No-op for an inline platform."""
|
|
155
|
+
if self._inline is not None or self._started:
|
|
156
|
+
self._started = True
|
|
157
|
+
return
|
|
158
|
+
self._connection = PlatformConnection(
|
|
159
|
+
self._platform_url,
|
|
160
|
+
self._platform_key,
|
|
161
|
+
on_envelope=self._handle_envelope,
|
|
162
|
+
on_disconnect=lambda: self._emit(AgentEvent(type="disconnected")),
|
|
163
|
+
on_reconnect=lambda: self._emit(AgentEvent(type="reconnected")),
|
|
164
|
+
on_error=self._on_connection_error,
|
|
165
|
+
sessions=lambda: [
|
|
166
|
+
{"session_id": session.id, "session_token": session.token}
|
|
167
|
+
for session in self._sessions.values()
|
|
168
|
+
],
|
|
169
|
+
logger=self._logger,
|
|
170
|
+
)
|
|
171
|
+
await self._connection.connect()
|
|
172
|
+
self._started = True
|
|
173
|
+
|
|
174
|
+
async def start(
|
|
175
|
+
self,
|
|
176
|
+
instance: object,
|
|
177
|
+
*,
|
|
178
|
+
name: str | None = None,
|
|
179
|
+
goal: str | None = None,
|
|
180
|
+
instructions: str | None = None,
|
|
181
|
+
) -> AgentHandle:
|
|
182
|
+
"""Reflect the player into tools, register a session, and return a handle."""
|
|
183
|
+
await self.connect()
|
|
184
|
+
reflected = reflect_player(instance)
|
|
185
|
+
session_id = str(uuid.uuid4())
|
|
186
|
+
session_token = str(uuid.uuid4())
|
|
187
|
+
gate = TurnGate()
|
|
188
|
+
player_name = name or type(instance).__name__ or "player"
|
|
189
|
+
memory = self._action_memory()
|
|
190
|
+
actions = SessionActionLog(memory, session_id, player_name) if memory is not None else None
|
|
191
|
+
server = SessionMcpServer(
|
|
192
|
+
reflected,
|
|
193
|
+
gate,
|
|
194
|
+
name=f"agent-player:{player_name}",
|
|
195
|
+
actions=actions,
|
|
196
|
+
)
|
|
197
|
+
prompt = _join_instructions(self._server_instructions, instructions)
|
|
198
|
+
session = _Session(session_id, session_token, player_name, gate, server)
|
|
199
|
+
|
|
200
|
+
if self._inline is not None:
|
|
201
|
+
server_transport, client_transport = InMemoryTransport.create_linked_pair()
|
|
202
|
+
await server.connect(server_transport)
|
|
203
|
+
self._sessions[session_id] = session
|
|
204
|
+
await self._inline.start_session(
|
|
205
|
+
StartSession(
|
|
206
|
+
session_id=session_id,
|
|
207
|
+
session_token=session_token,
|
|
208
|
+
name=player_name,
|
|
209
|
+
tools=reflected.tools,
|
|
210
|
+
instructions=prompt,
|
|
211
|
+
transport=client_transport,
|
|
212
|
+
on_turn_result=gate.complete_turn,
|
|
213
|
+
goal=goal,
|
|
214
|
+
logger=self._logger,
|
|
215
|
+
on_event=self._emit,
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
connection = self._connection
|
|
220
|
+
if connection is None:
|
|
221
|
+
raise RuntimeError("Not connected")
|
|
222
|
+
mux = connection.create_transport(session_id)
|
|
223
|
+
await server.connect(mux)
|
|
224
|
+
session.mux = mux
|
|
225
|
+
self._sessions[session_id] = session
|
|
226
|
+
started = connection.begin_wait(session_id, "start")
|
|
227
|
+
body: dict[str, Any] = {
|
|
228
|
+
"type": "start_session",
|
|
229
|
+
"sessionId": session_id,
|
|
230
|
+
"sessionToken": session_token,
|
|
231
|
+
"name": player_name,
|
|
232
|
+
"tools": reflected.tools,
|
|
233
|
+
"instructions": prompt,
|
|
234
|
+
}
|
|
235
|
+
if goal:
|
|
236
|
+
body["goal"] = goal
|
|
237
|
+
connection.send(body)
|
|
238
|
+
await started
|
|
239
|
+
|
|
240
|
+
return AgentHandle(
|
|
241
|
+
session_id,
|
|
242
|
+
lambda **kwargs: self._turn(session_id, **kwargs),
|
|
243
|
+
lambda: self._stop_session(session_id),
|
|
244
|
+
lambda listener: self._on_session(session_id, listener),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def on(self, listener: EventListener) -> Callable[[], None]:
|
|
248
|
+
"""Subscribe to every session, plus connection events."""
|
|
249
|
+
self._listeners.add(listener)
|
|
250
|
+
|
|
251
|
+
def unsubscribe() -> None:
|
|
252
|
+
self._listeners.discard(listener)
|
|
253
|
+
|
|
254
|
+
return unsubscribe
|
|
255
|
+
|
|
256
|
+
async def close(self) -> None:
|
|
257
|
+
"""Stop every session and close the platform connection."""
|
|
258
|
+
if self._closing:
|
|
259
|
+
return
|
|
260
|
+
self._closing = True
|
|
261
|
+
try:
|
|
262
|
+
for session_id in list(self._sessions):
|
|
263
|
+
await self._stop_session(session_id)
|
|
264
|
+
if self._inline is not None and hasattr(self._inline, "close"):
|
|
265
|
+
await self._inline.close()
|
|
266
|
+
if self._connection is not None:
|
|
267
|
+
await self._connection.close()
|
|
268
|
+
self._connection = None
|
|
269
|
+
if self._memory is not None:
|
|
270
|
+
self._memory.close()
|
|
271
|
+
self._memory = None
|
|
272
|
+
self._started = False
|
|
273
|
+
finally:
|
|
274
|
+
self._closing = False
|
|
275
|
+
|
|
276
|
+
async def _turn(self, session_id: str, *, timeout_ms: int | None = None, signal: Any = None) -> TurnResult:
|
|
277
|
+
session = self._sessions.get(session_id)
|
|
278
|
+
if session is None:
|
|
279
|
+
raise RuntimeError(f"Unknown session {session_id}")
|
|
280
|
+
if timeout_ms is None:
|
|
281
|
+
timeout_ms = self._turn_timeout_ms if self._turn_timeout_ms is not None else DEFAULT_TURN_TIMEOUT_MS
|
|
282
|
+
outcome = await session.gate.run_turn(timeout_ms=timeout_ms, signal=signal)
|
|
283
|
+
if not outcome.ok:
|
|
284
|
+
if outcome.connection_lost:
|
|
285
|
+
raise ConnectionLostError(outcome.error or "Connection to the platform was lost")
|
|
286
|
+
raise TurnFailedError(outcome.error or "turn failed", outcome.attempt_details)
|
|
287
|
+
return TurnResult(
|
|
288
|
+
action=outcome.action or "unknown",
|
|
289
|
+
args=dict(outcome.args or {}),
|
|
290
|
+
result=outcome.result,
|
|
291
|
+
attempts=outcome.attempts or 1,
|
|
292
|
+
usage=outcome.usage or TokenUsage(),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def _on_session(self, session_id: str, listener: EventListener) -> Callable[[], None]:
|
|
296
|
+
session = self._sessions.get(session_id)
|
|
297
|
+
if session is None:
|
|
298
|
+
return lambda: None
|
|
299
|
+
session.listeners.add(listener)
|
|
300
|
+
|
|
301
|
+
def unsubscribe() -> None:
|
|
302
|
+
session.listeners.discard(listener)
|
|
303
|
+
|
|
304
|
+
return unsubscribe
|
|
305
|
+
|
|
306
|
+
def _emit(self, event: AgentEvent) -> None:
|
|
307
|
+
for listener in list(self._listeners):
|
|
308
|
+
listener(event)
|
|
309
|
+
if event.type in ("disconnected", "reconnected") or not event.session_id:
|
|
310
|
+
return
|
|
311
|
+
session = self._sessions.get(event.session_id)
|
|
312
|
+
if session is None:
|
|
313
|
+
return
|
|
314
|
+
for listener in list(session.listeners):
|
|
315
|
+
listener(event)
|
|
316
|
+
|
|
317
|
+
def _on_connection_error(self, error: BaseException) -> None:
|
|
318
|
+
log(self._logger, "error", str(error))
|
|
319
|
+
if isinstance(error, ConnectionLostError):
|
|
320
|
+
for session in self._sessions.values():
|
|
321
|
+
session.gate.fail_turn(error)
|
|
322
|
+
|
|
323
|
+
def _handle_envelope(self, envelope: dict[str, Any]) -> None:
|
|
324
|
+
kind = envelope.get("type")
|
|
325
|
+
session_id = envelope.get("sessionId")
|
|
326
|
+
session = self._sessions.get(session_id) if isinstance(session_id, str) else None
|
|
327
|
+
if kind == "mcp":
|
|
328
|
+
if session is not None and session.mux is not None:
|
|
329
|
+
session.mux.receive(envelope.get("message"))
|
|
330
|
+
return
|
|
331
|
+
if kind == "turn_result":
|
|
332
|
+
if session is not None:
|
|
333
|
+
session.gate.complete_turn(turn_report_from_wire(envelope))
|
|
334
|
+
return
|
|
335
|
+
if kind == "session_event":
|
|
336
|
+
self._emit_wire_event(envelope)
|
|
337
|
+
return
|
|
338
|
+
if kind == "error":
|
|
339
|
+
log(self._logger, "error", str(envelope.get("message") or ""))
|
|
340
|
+
|
|
341
|
+
def _emit_wire_event(self, envelope: dict[str, Any]) -> None:
|
|
342
|
+
session_id = str(envelope.get("sessionId") or "")
|
|
343
|
+
name = envelope.get("event")
|
|
344
|
+
action = envelope.get("action")
|
|
345
|
+
args = envelope.get("args") if isinstance(envelope.get("args"), dict) else None
|
|
346
|
+
if name == "action":
|
|
347
|
+
self._emit(
|
|
348
|
+
AgentEvent(
|
|
349
|
+
type="action",
|
|
350
|
+
session_id=session_id,
|
|
351
|
+
action=str(action or "unknown"),
|
|
352
|
+
args=dict(args or {}),
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
return
|
|
356
|
+
if name == "retry":
|
|
357
|
+
self._emit(
|
|
358
|
+
AgentEvent(
|
|
359
|
+
type="retry",
|
|
360
|
+
session_id=session_id,
|
|
361
|
+
action=str(action or "unknown"),
|
|
362
|
+
args=dict(args or {}),
|
|
363
|
+
error=str(envelope.get("error") or "action failed"),
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
return
|
|
367
|
+
self._emit(
|
|
368
|
+
AgentEvent(
|
|
369
|
+
type="turnFailed",
|
|
370
|
+
session_id=session_id,
|
|
371
|
+
error=str(envelope.get("error") or "turn failed"),
|
|
372
|
+
action=str(action) if action else None,
|
|
373
|
+
args=dict(args) if args else None,
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
async def _stop_session(self, session_id: str) -> None:
|
|
378
|
+
session = self._sessions.pop(session_id, None)
|
|
379
|
+
if session is None:
|
|
380
|
+
return
|
|
381
|
+
session.gate.reset()
|
|
382
|
+
if self._inline is not None:
|
|
383
|
+
await self._inline.stop_session(session_id)
|
|
384
|
+
elif self._connection is not None:
|
|
385
|
+
connection = self._connection
|
|
386
|
+
waiter = connection.begin_wait(session_id, "stop")
|
|
387
|
+
try:
|
|
388
|
+
connection.send({"type": "stop_session", "sessionId": session_id})
|
|
389
|
+
except Exception:
|
|
390
|
+
connection.cancel_wait(session_id, "stop")
|
|
391
|
+
try:
|
|
392
|
+
await waiter
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
395
|
+
try:
|
|
396
|
+
await session.server.close()
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
|
|
400
|
+
def _action_memory(self) -> SessionMemory | None:
|
|
401
|
+
if self._memory_path is False:
|
|
402
|
+
return None
|
|
403
|
+
if self._memory is None:
|
|
404
|
+
path = self._memory_path if isinstance(self._memory_path, str) else default_memory_path()
|
|
405
|
+
self._memory = SessionMemory(path)
|
|
406
|
+
return self._memory
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _join_instructions(server_instructions: str | None, start_instructions: str | None) -> str:
|
|
410
|
+
base = DEFAULT_AGENT_INSTRUCTIONS if server_instructions is None else server_instructions
|
|
411
|
+
parts = [part for part in (base, start_instructions) if part]
|
|
412
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Typed failures raised by AgentServer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from whitesands.agent_player.protocol import Attempt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlatformAuthError(Exception):
|
|
12
|
+
"""The platform rejected the key."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProtocolVersionError(Exception):
|
|
16
|
+
"""This SDK and the platform disagree on the protocol."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TurnFailedError(Exception):
|
|
20
|
+
"""The action and the retry both failed."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, message: str, attempts: list[Attempt] | None = None) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.attempts = list(attempts or [])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TurnTimeoutError(Exception):
|
|
28
|
+
"""The platform did not finish within the turn timeout."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConnectionLostError(Exception):
|
|
32
|
+
"""The socket dropped and the session could not be resumed."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str = "Connection to the platform was lost") -> None:
|
|
35
|
+
super().__init__(message)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Events emitted while a session acts, or while the platform connection changes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class AgentEvent:
|
|
11
|
+
"""One action, retry, failure, or connection change.
|
|
12
|
+
|
|
13
|
+
``disconnected`` and ``reconnected`` have no session id. The other events
|
|
14
|
+
do. ``type`` is ``action``, ``retry``, ``turnFailed``, ``disconnected``,
|
|
15
|
+
or ``reconnected``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
type: str
|
|
19
|
+
session_id: str | None = None
|
|
20
|
+
action: str | None = None
|
|
21
|
+
args: dict[str, Any] | None = None
|
|
22
|
+
error: str | None = None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Optional logger. Omit it and the SDK writes nothing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def log(logger: Any, level: str, message: str, extra: Any = None) -> None:
|
|
9
|
+
if logger is None:
|
|
10
|
+
return
|
|
11
|
+
fn = getattr(logger, level, None)
|
|
12
|
+
if fn is None and level == "warn":
|
|
13
|
+
fn = getattr(logger, "warning", None)
|
|
14
|
+
if not callable(fn):
|
|
15
|
+
return
|
|
16
|
+
try:
|
|
17
|
+
if extra is None:
|
|
18
|
+
fn(message)
|
|
19
|
+
else:
|
|
20
|
+
fn(message, extra)
|
|
21
|
+
except TypeError:
|
|
22
|
+
fn(message)
|