dooers-workers 0.2.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.
- dooers/__init__.py +73 -0
- dooers/broadcast.py +180 -0
- dooers/config.py +22 -0
- dooers/features/__init__.py +0 -0
- dooers/features/analytics/__init__.py +12 -0
- dooers/features/analytics/collector.py +219 -0
- dooers/features/analytics/models.py +50 -0
- dooers/features/analytics/worker_analytics.py +100 -0
- dooers/features/settings/__init__.py +12 -0
- dooers/features/settings/broadcaster.py +97 -0
- dooers/features/settings/models.py +72 -0
- dooers/features/settings/worker_settings.py +85 -0
- dooers/handlers/__init__.py +16 -0
- dooers/handlers/memory.py +105 -0
- dooers/handlers/request.py +12 -0
- dooers/handlers/response.py +66 -0
- dooers/handlers/router.py +957 -0
- dooers/migrations/__init__.py +3 -0
- dooers/migrations/schemas.py +126 -0
- dooers/persistence/__init__.py +9 -0
- dooers/persistence/base.py +42 -0
- dooers/persistence/postgres.py +459 -0
- dooers/persistence/sqlite.py +433 -0
- dooers/protocol/__init__.py +108 -0
- dooers/protocol/frames.py +298 -0
- dooers/protocol/models.py +72 -0
- dooers/protocol/parser.py +19 -0
- dooers/registry.py +101 -0
- dooers/server.py +162 -0
- dooers/settings.py +3 -0
- dooers_workers-0.2.0.dist-info/METADATA +228 -0
- dooers_workers-0.2.0.dist-info/RECORD +33 -0
- dooers_workers-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from collections.abc import AsyncGenerator, Callable
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import TYPE_CHECKING, Protocol
|
|
7
|
+
|
|
8
|
+
from dooers.features.analytics.models import AnalyticsEvent
|
|
9
|
+
from dooers.features.analytics.worker_analytics import WorkerAnalytics
|
|
10
|
+
from dooers.features.settings.worker_settings import WorkerSettings
|
|
11
|
+
from dooers.handlers.memory import WorkerMemory
|
|
12
|
+
from dooers.handlers.request import WorkerRequest
|
|
13
|
+
from dooers.handlers.response import WorkerEvent, WorkerResponse
|
|
14
|
+
from dooers.persistence.base import Persistence
|
|
15
|
+
from dooers.protocol.frames import (
|
|
16
|
+
AckPayload,
|
|
17
|
+
C2S_AnalyticsSubscribe,
|
|
18
|
+
C2S_AnalyticsUnsubscribe,
|
|
19
|
+
C2S_Connect,
|
|
20
|
+
C2S_EventCreate,
|
|
21
|
+
C2S_Feedback,
|
|
22
|
+
C2S_SettingsPatch,
|
|
23
|
+
C2S_SettingsSubscribe,
|
|
24
|
+
C2S_SettingsUnsubscribe,
|
|
25
|
+
C2S_ThreadList,
|
|
26
|
+
C2S_ThreadSubscribe,
|
|
27
|
+
C2S_ThreadUnsubscribe,
|
|
28
|
+
ClientToServer,
|
|
29
|
+
EventAppendPayload,
|
|
30
|
+
FeedbackAckPayload,
|
|
31
|
+
RunUpsertPayload,
|
|
32
|
+
S2C_Ack,
|
|
33
|
+
S2C_EventAppend,
|
|
34
|
+
S2C_FeedbackAck,
|
|
35
|
+
S2C_RunUpsert,
|
|
36
|
+
S2C_ThreadListResult,
|
|
37
|
+
S2C_ThreadSnapshot,
|
|
38
|
+
S2C_ThreadUpsert,
|
|
39
|
+
ServerToClient,
|
|
40
|
+
ThreadListResultPayload,
|
|
41
|
+
ThreadSnapshotPayload,
|
|
42
|
+
ThreadUpsertPayload,
|
|
43
|
+
)
|
|
44
|
+
from dooers.protocol.models import DocumentPart, ImagePart, Run, TextPart, Thread, ThreadEvent
|
|
45
|
+
from dooers.protocol.parser import serialize_frame
|
|
46
|
+
from dooers.registry import ConnectionRegistry
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from dooers.features.analytics.collector import AnalyticsCollector
|
|
50
|
+
from dooers.features.settings.broadcaster import SettingsBroadcaster
|
|
51
|
+
from dooers.features.settings.models import SettingsSchema
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class WebSocketProtocol(Protocol):
|
|
55
|
+
async def receive_text(self) -> str: ...
|
|
56
|
+
async def send_text(self, data: str) -> None: ...
|
|
57
|
+
async def close(self, code: int = 1000) -> None: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
Handler = Callable[
|
|
61
|
+
[WorkerRequest, WorkerResponse, WorkerMemory, WorkerAnalytics, WorkerSettings],
|
|
62
|
+
AsyncGenerator[WorkerEvent, None],
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _generate_id() -> str:
|
|
67
|
+
return str(uuid.uuid4())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _now() -> datetime:
|
|
71
|
+
return datetime.now(UTC)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Router:
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
persistence: Persistence,
|
|
78
|
+
handler: Handler,
|
|
79
|
+
registry: ConnectionRegistry,
|
|
80
|
+
subscriptions: dict[str, set[str]],
|
|
81
|
+
analytics_collector: AnalyticsCollector | None = None,
|
|
82
|
+
settings_broadcaster: SettingsBroadcaster | None = None,
|
|
83
|
+
settings_schema: SettingsSchema | None = None,
|
|
84
|
+
analytics_subscriptions: dict[str, set[str]] | None = None,
|
|
85
|
+
settings_subscriptions: dict[str, set[str]] | None = None,
|
|
86
|
+
):
|
|
87
|
+
self._persistence = persistence
|
|
88
|
+
self._handler = handler
|
|
89
|
+
self._registry = registry
|
|
90
|
+
self._subscriptions = subscriptions # ws_id -> set of subscribed thread_ids
|
|
91
|
+
|
|
92
|
+
# Analytics and settings
|
|
93
|
+
self._analytics_collector = analytics_collector
|
|
94
|
+
self._settings_broadcaster = settings_broadcaster
|
|
95
|
+
self._settings_schema = settings_schema
|
|
96
|
+
self._analytics_subscriptions = analytics_subscriptions or {}
|
|
97
|
+
self._settings_subscriptions = settings_subscriptions or {}
|
|
98
|
+
|
|
99
|
+
# Connection state
|
|
100
|
+
self._ws: WebSocketProtocol | None = None
|
|
101
|
+
self._ws_id: str = _generate_id() # Unique ID for this connection
|
|
102
|
+
self._worker_id: str | None = None
|
|
103
|
+
self._user_id: str | None = None
|
|
104
|
+
self._user_name: str | None = None
|
|
105
|
+
self._user_email: str | None = None
|
|
106
|
+
self._subscribed_threads: set[str] = set()
|
|
107
|
+
|
|
108
|
+
async def _send(self, ws: WebSocketProtocol, frame: ServerToClient) -> None:
|
|
109
|
+
await ws.send_text(serialize_frame(frame))
|
|
110
|
+
|
|
111
|
+
async def _send_ack(
|
|
112
|
+
self,
|
|
113
|
+
ws: WebSocketProtocol,
|
|
114
|
+
ack_id: str,
|
|
115
|
+
ok: bool = True,
|
|
116
|
+
error: dict | None = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
frame = S2C_Ack(
|
|
119
|
+
id=_generate_id(),
|
|
120
|
+
payload=AckPayload(ack_id=ack_id, ok=ok, error=error),
|
|
121
|
+
)
|
|
122
|
+
await self._send(ws, frame)
|
|
123
|
+
|
|
124
|
+
async def _broadcast_to_worker(self, frame: ServerToClient) -> None:
|
|
125
|
+
"""Broadcast a frame to all connections for the current worker."""
|
|
126
|
+
if not self._worker_id:
|
|
127
|
+
return
|
|
128
|
+
message = serialize_frame(frame)
|
|
129
|
+
await self._registry.broadcast(self._worker_id, message)
|
|
130
|
+
|
|
131
|
+
async def _broadcast_to_worker_except_self(self, ws: WebSocketProtocol, frame: ServerToClient) -> None:
|
|
132
|
+
"""Broadcast a frame to all connections for the current worker except this one."""
|
|
133
|
+
if not self._worker_id:
|
|
134
|
+
return
|
|
135
|
+
message = serialize_frame(frame)
|
|
136
|
+
await self._registry.broadcast_except(self._worker_id, ws, message)
|
|
137
|
+
|
|
138
|
+
async def route(self, ws: WebSocketProtocol, frame: ClientToServer) -> None:
|
|
139
|
+
self._ws = ws
|
|
140
|
+
match frame:
|
|
141
|
+
case C2S_Connect():
|
|
142
|
+
await self._handle_connect(ws, frame)
|
|
143
|
+
case C2S_ThreadList():
|
|
144
|
+
await self._handle_thread_list(ws, frame)
|
|
145
|
+
case C2S_ThreadSubscribe():
|
|
146
|
+
await self._handle_thread_subscribe(ws, frame)
|
|
147
|
+
case C2S_ThreadUnsubscribe():
|
|
148
|
+
await self._handle_thread_unsubscribe(ws, frame)
|
|
149
|
+
case C2S_EventCreate():
|
|
150
|
+
await self._handle_event_create(ws, frame)
|
|
151
|
+
# Analytics frames
|
|
152
|
+
case C2S_AnalyticsSubscribe():
|
|
153
|
+
await self._handle_analytics_subscribe(ws, frame)
|
|
154
|
+
case C2S_AnalyticsUnsubscribe():
|
|
155
|
+
await self._handle_analytics_unsubscribe(ws, frame)
|
|
156
|
+
case C2S_Feedback():
|
|
157
|
+
await self._handle_feedback(ws, frame)
|
|
158
|
+
# Settings frames
|
|
159
|
+
case C2S_SettingsSubscribe():
|
|
160
|
+
await self._handle_settings_subscribe(ws, frame)
|
|
161
|
+
case C2S_SettingsUnsubscribe():
|
|
162
|
+
await self._handle_settings_unsubscribe(ws, frame)
|
|
163
|
+
case C2S_SettingsPatch():
|
|
164
|
+
await self._handle_settings_patch(ws, frame)
|
|
165
|
+
|
|
166
|
+
async def cleanup(self) -> None:
|
|
167
|
+
"""Clean up connection resources. Call this when the connection closes."""
|
|
168
|
+
if self._worker_id:
|
|
169
|
+
await self._registry.unregister(self._worker_id, self._ws)
|
|
170
|
+
|
|
171
|
+
# Clean up analytics subscriptions
|
|
172
|
+
if self._worker_id in self._analytics_subscriptions:
|
|
173
|
+
self._analytics_subscriptions[self._worker_id].discard(self._ws_id)
|
|
174
|
+
if not self._analytics_subscriptions[self._worker_id]:
|
|
175
|
+
del self._analytics_subscriptions[self._worker_id]
|
|
176
|
+
|
|
177
|
+
# Clean up settings subscriptions
|
|
178
|
+
if self._worker_id in self._settings_subscriptions:
|
|
179
|
+
self._settings_subscriptions[self._worker_id].discard(self._ws_id)
|
|
180
|
+
if not self._settings_subscriptions[self._worker_id]:
|
|
181
|
+
del self._settings_subscriptions[self._worker_id]
|
|
182
|
+
|
|
183
|
+
# Clean up thread subscriptions tracking
|
|
184
|
+
if self._ws_id in self._subscriptions:
|
|
185
|
+
del self._subscriptions[self._ws_id]
|
|
186
|
+
|
|
187
|
+
async def _handle_connect(self, ws: WebSocketProtocol, frame: C2S_Connect) -> None:
|
|
188
|
+
# Extract identity from payload
|
|
189
|
+
self._worker_id = frame.payload.worker_id
|
|
190
|
+
self._user_id = frame.payload.user_id
|
|
191
|
+
self._user_name = frame.payload.user_name
|
|
192
|
+
self._user_email = frame.payload.user_email
|
|
193
|
+
self._ws = ws
|
|
194
|
+
|
|
195
|
+
# Register connection in registry
|
|
196
|
+
await self._registry.register(self._worker_id, ws)
|
|
197
|
+
|
|
198
|
+
# Initialize subscriptions for this connection
|
|
199
|
+
self._subscriptions[self._ws_id] = set()
|
|
200
|
+
|
|
201
|
+
await self._send_ack(ws, frame.id)
|
|
202
|
+
|
|
203
|
+
async def _handle_thread_list(self, ws: WebSocketProtocol, frame: C2S_ThreadList) -> None:
|
|
204
|
+
if not self._worker_id:
|
|
205
|
+
await self._send_ack(
|
|
206
|
+
ws,
|
|
207
|
+
frame.id,
|
|
208
|
+
ok=False,
|
|
209
|
+
error={"code": "NOT_CONNECTED", "message": "Must connect first"},
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Filter by worker_id first, then optionally by user_id
|
|
214
|
+
threads = await self._persistence.list_threads(
|
|
215
|
+
worker_id=self._worker_id,
|
|
216
|
+
user_id=None, # Show all threads for this worker (team collaboration)
|
|
217
|
+
cursor=frame.payload.cursor,
|
|
218
|
+
limit=frame.payload.limit or 30,
|
|
219
|
+
)
|
|
220
|
+
result = S2C_ThreadListResult(
|
|
221
|
+
id=_generate_id(),
|
|
222
|
+
payload=ThreadListResultPayload(threads=threads, cursor=None),
|
|
223
|
+
)
|
|
224
|
+
await self._send(ws, result)
|
|
225
|
+
|
|
226
|
+
async def _handle_thread_subscribe(
|
|
227
|
+
self,
|
|
228
|
+
ws: WebSocketProtocol,
|
|
229
|
+
frame: C2S_ThreadSubscribe,
|
|
230
|
+
) -> None:
|
|
231
|
+
if not self._worker_id:
|
|
232
|
+
await self._send_ack(
|
|
233
|
+
ws,
|
|
234
|
+
frame.id,
|
|
235
|
+
ok=False,
|
|
236
|
+
error={"code": "NOT_CONNECTED", "message": "Must connect first"},
|
|
237
|
+
)
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
thread_id = frame.payload.thread_id
|
|
241
|
+
thread = await self._persistence.get_thread(thread_id)
|
|
242
|
+
|
|
243
|
+
if not thread:
|
|
244
|
+
await self._send_ack(
|
|
245
|
+
ws,
|
|
246
|
+
frame.id,
|
|
247
|
+
ok=False,
|
|
248
|
+
error={"code": "NOT_FOUND", "message": "Thread not found"},
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
# Verify thread belongs to this worker
|
|
253
|
+
if thread.worker_id != self._worker_id:
|
|
254
|
+
await self._send_ack(
|
|
255
|
+
ws,
|
|
256
|
+
frame.id,
|
|
257
|
+
ok=False,
|
|
258
|
+
error={"code": "FORBIDDEN", "message": "Thread belongs to different worker"},
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
events = await self._persistence.get_events(
|
|
263
|
+
thread_id=thread_id,
|
|
264
|
+
after_event_id=frame.payload.after_event_id,
|
|
265
|
+
limit=100,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
self._subscribed_threads.add(thread_id)
|
|
269
|
+
self._subscriptions[self._ws_id].add(thread_id)
|
|
270
|
+
|
|
271
|
+
snapshot = S2C_ThreadSnapshot(
|
|
272
|
+
id=_generate_id(),
|
|
273
|
+
payload=ThreadSnapshotPayload(thread=thread, events=events),
|
|
274
|
+
)
|
|
275
|
+
await self._send(ws, snapshot)
|
|
276
|
+
|
|
277
|
+
async def _handle_thread_unsubscribe(
|
|
278
|
+
self,
|
|
279
|
+
ws: WebSocketProtocol,
|
|
280
|
+
frame: C2S_ThreadUnsubscribe,
|
|
281
|
+
) -> None:
|
|
282
|
+
thread_id = frame.payload.thread_id
|
|
283
|
+
self._subscribed_threads.discard(thread_id)
|
|
284
|
+
if self._ws_id in self._subscriptions:
|
|
285
|
+
self._subscriptions[self._ws_id].discard(thread_id)
|
|
286
|
+
await self._send_ack(ws, frame.id)
|
|
287
|
+
|
|
288
|
+
async def _handle_event_create(self, ws: WebSocketProtocol, frame: C2S_EventCreate) -> None:
|
|
289
|
+
if not self._worker_id:
|
|
290
|
+
await self._send_ack(
|
|
291
|
+
ws,
|
|
292
|
+
frame.id,
|
|
293
|
+
ok=False,
|
|
294
|
+
error={"code": "NOT_CONNECTED", "message": "Must connect first"},
|
|
295
|
+
)
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
now = _now()
|
|
299
|
+
thread_id = frame.payload.thread_id
|
|
300
|
+
|
|
301
|
+
if not thread_id:
|
|
302
|
+
# Create new thread for this worker
|
|
303
|
+
thread_id = _generate_id()
|
|
304
|
+
thread = Thread(
|
|
305
|
+
id=thread_id,
|
|
306
|
+
worker_id=self._worker_id,
|
|
307
|
+
user_id=self._user_id,
|
|
308
|
+
title=None,
|
|
309
|
+
created_at=now,
|
|
310
|
+
updated_at=now,
|
|
311
|
+
last_event_at=now,
|
|
312
|
+
)
|
|
313
|
+
await self._persistence.create_thread(thread)
|
|
314
|
+
self._subscribed_threads.add(thread_id)
|
|
315
|
+
self._subscriptions[self._ws_id].add(thread_id)
|
|
316
|
+
|
|
317
|
+
# Broadcast new thread to ALL worker connections
|
|
318
|
+
thread_upsert = S2C_ThreadUpsert(
|
|
319
|
+
id=_generate_id(),
|
|
320
|
+
payload=ThreadUpsertPayload(thread=thread),
|
|
321
|
+
)
|
|
322
|
+
await self._broadcast_to_worker(thread_upsert)
|
|
323
|
+
|
|
324
|
+
# Track thread.created
|
|
325
|
+
await self._track_event(
|
|
326
|
+
AnalyticsEvent.THREAD_CREATED.value,
|
|
327
|
+
thread_id=thread_id,
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
# Verify thread belongs to this worker
|
|
331
|
+
thread = await self._persistence.get_thread(thread_id)
|
|
332
|
+
if not thread:
|
|
333
|
+
await self._send_ack(
|
|
334
|
+
ws,
|
|
335
|
+
frame.id,
|
|
336
|
+
ok=False,
|
|
337
|
+
error={"code": "NOT_FOUND", "message": "Thread not found"},
|
|
338
|
+
)
|
|
339
|
+
return
|
|
340
|
+
if thread.worker_id != self._worker_id:
|
|
341
|
+
await self._send_ack(
|
|
342
|
+
ws,
|
|
343
|
+
frame.id,
|
|
344
|
+
ok=False,
|
|
345
|
+
error={"code": "FORBIDDEN", "message": "Thread belongs to different worker"},
|
|
346
|
+
)
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
# Create user event with identity
|
|
350
|
+
user_event_id = _generate_id()
|
|
351
|
+
content_parts = [self._convert_content_part(part) for part in frame.payload.event.content]
|
|
352
|
+
user_event = ThreadEvent(
|
|
353
|
+
id=user_event_id,
|
|
354
|
+
thread_id=thread_id,
|
|
355
|
+
run_id=None,
|
|
356
|
+
type="message",
|
|
357
|
+
actor="user",
|
|
358
|
+
user_id=self._user_id,
|
|
359
|
+
user_name=self._user_name,
|
|
360
|
+
user_email=self._user_email,
|
|
361
|
+
content=content_parts,
|
|
362
|
+
data=frame.payload.event.data,
|
|
363
|
+
created_at=now,
|
|
364
|
+
)
|
|
365
|
+
await self._persistence.create_event(user_event)
|
|
366
|
+
|
|
367
|
+
# Broadcast user event to ALL worker connections
|
|
368
|
+
event_append = S2C_EventAppend(
|
|
369
|
+
id=_generate_id(),
|
|
370
|
+
payload=EventAppendPayload(thread_id=thread_id, events=[user_event]),
|
|
371
|
+
)
|
|
372
|
+
await self._broadcast_to_worker(event_append)
|
|
373
|
+
|
|
374
|
+
# Track message.c2s
|
|
375
|
+
await self._track_event(
|
|
376
|
+
AnalyticsEvent.MESSAGE_C2S.value,
|
|
377
|
+
thread_id=thread_id,
|
|
378
|
+
event_id=user_event_id,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
await self._send_ack(ws, frame.id)
|
|
382
|
+
|
|
383
|
+
# Process with handler
|
|
384
|
+
message = self._extract_message(content_parts)
|
|
385
|
+
request = WorkerRequest(
|
|
386
|
+
message=message,
|
|
387
|
+
content=content_parts,
|
|
388
|
+
thread_id=thread_id,
|
|
389
|
+
event_id=user_event_id,
|
|
390
|
+
user_id=self._user_id,
|
|
391
|
+
)
|
|
392
|
+
response = WorkerResponse()
|
|
393
|
+
memory = WorkerMemory(thread_id=thread_id, persistence=self._persistence)
|
|
394
|
+
|
|
395
|
+
# Create analytics instance for handler
|
|
396
|
+
analytics = (
|
|
397
|
+
WorkerAnalytics(
|
|
398
|
+
worker_id=self._worker_id,
|
|
399
|
+
thread_id=thread_id,
|
|
400
|
+
user_id=self._user_id,
|
|
401
|
+
run_id=None, # Will be set when run starts
|
|
402
|
+
collector=self._analytics_collector,
|
|
403
|
+
)
|
|
404
|
+
if self._analytics_collector
|
|
405
|
+
else self._create_noop_analytics()
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Create settings instance for handler
|
|
409
|
+
settings = (
|
|
410
|
+
WorkerSettings(
|
|
411
|
+
worker_id=self._worker_id,
|
|
412
|
+
schema=self._settings_schema,
|
|
413
|
+
persistence=self._persistence,
|
|
414
|
+
broadcaster=self._settings_broadcaster,
|
|
415
|
+
)
|
|
416
|
+
if self._settings_schema and self._settings_broadcaster
|
|
417
|
+
else self._create_noop_settings()
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
current_run_id: str | None = None
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
async for event in self._handler(request, response, memory, analytics, settings):
|
|
424
|
+
event_now = _now()
|
|
425
|
+
|
|
426
|
+
if event.response_type == "run_start":
|
|
427
|
+
current_run_id = _generate_id()
|
|
428
|
+
run = Run(
|
|
429
|
+
id=current_run_id,
|
|
430
|
+
thread_id=thread_id,
|
|
431
|
+
agent_id=event.data.get("agent_id"),
|
|
432
|
+
status="running",
|
|
433
|
+
started_at=event_now,
|
|
434
|
+
)
|
|
435
|
+
await self._persistence.create_run(run)
|
|
436
|
+
|
|
437
|
+
run_upsert = S2C_RunUpsert(
|
|
438
|
+
id=_generate_id(),
|
|
439
|
+
payload=RunUpsertPayload(run=run),
|
|
440
|
+
)
|
|
441
|
+
await self._broadcast_to_worker(run_upsert)
|
|
442
|
+
|
|
443
|
+
elif event.response_type == "run_end":
|
|
444
|
+
if current_run_id:
|
|
445
|
+
run = Run(
|
|
446
|
+
id=current_run_id,
|
|
447
|
+
thread_id=thread_id,
|
|
448
|
+
status=event.data.get("status", "succeeded"),
|
|
449
|
+
started_at=event_now,
|
|
450
|
+
ended_at=event_now,
|
|
451
|
+
error=event.data.get("error"),
|
|
452
|
+
)
|
|
453
|
+
await self._persistence.update_run(run)
|
|
454
|
+
|
|
455
|
+
run_upsert = S2C_RunUpsert(
|
|
456
|
+
id=_generate_id(),
|
|
457
|
+
payload=RunUpsertPayload(run=run),
|
|
458
|
+
)
|
|
459
|
+
await self._broadcast_to_worker(run_upsert)
|
|
460
|
+
current_run_id = None
|
|
461
|
+
|
|
462
|
+
elif event.response_type == "text":
|
|
463
|
+
event_id = _generate_id()
|
|
464
|
+
thread_event = ThreadEvent(
|
|
465
|
+
id=event_id,
|
|
466
|
+
thread_id=thread_id,
|
|
467
|
+
run_id=current_run_id,
|
|
468
|
+
type="message",
|
|
469
|
+
actor="assistant",
|
|
470
|
+
user_id=None,
|
|
471
|
+
user_name=None,
|
|
472
|
+
user_email=None,
|
|
473
|
+
content=[TextPart(text=event.data["text"])],
|
|
474
|
+
created_at=event_now,
|
|
475
|
+
)
|
|
476
|
+
await self._persistence.create_event(thread_event)
|
|
477
|
+
|
|
478
|
+
append = S2C_EventAppend(
|
|
479
|
+
id=_generate_id(),
|
|
480
|
+
payload=EventAppendPayload(thread_id=thread_id, events=[thread_event]),
|
|
481
|
+
)
|
|
482
|
+
await self._broadcast_to_worker(append)
|
|
483
|
+
|
|
484
|
+
# Track message.s2c
|
|
485
|
+
await self._track_event(
|
|
486
|
+
AnalyticsEvent.MESSAGE_S2C.value,
|
|
487
|
+
thread_id=thread_id,
|
|
488
|
+
run_id=current_run_id,
|
|
489
|
+
event_id=event_id,
|
|
490
|
+
data={"type": "text"},
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
elif event.response_type == "image":
|
|
494
|
+
event_id = _generate_id()
|
|
495
|
+
thread_event = ThreadEvent(
|
|
496
|
+
id=event_id,
|
|
497
|
+
thread_id=thread_id,
|
|
498
|
+
run_id=current_run_id,
|
|
499
|
+
type="message",
|
|
500
|
+
actor="assistant",
|
|
501
|
+
user_id=None,
|
|
502
|
+
user_name=None,
|
|
503
|
+
user_email=None,
|
|
504
|
+
content=[
|
|
505
|
+
ImagePart(
|
|
506
|
+
url=event.data["url"],
|
|
507
|
+
mime_type=event.data.get("mime_type"),
|
|
508
|
+
alt=event.data.get("alt"),
|
|
509
|
+
)
|
|
510
|
+
],
|
|
511
|
+
created_at=event_now,
|
|
512
|
+
)
|
|
513
|
+
await self._persistence.create_event(thread_event)
|
|
514
|
+
|
|
515
|
+
append = S2C_EventAppend(
|
|
516
|
+
id=_generate_id(),
|
|
517
|
+
payload=EventAppendPayload(thread_id=thread_id, events=[thread_event]),
|
|
518
|
+
)
|
|
519
|
+
await self._broadcast_to_worker(append)
|
|
520
|
+
|
|
521
|
+
# Track message.s2c
|
|
522
|
+
await self._track_event(
|
|
523
|
+
AnalyticsEvent.MESSAGE_S2C.value,
|
|
524
|
+
thread_id=thread_id,
|
|
525
|
+
run_id=current_run_id,
|
|
526
|
+
event_id=event_id,
|
|
527
|
+
data={"type": "image"},
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
elif event.response_type == "document":
|
|
531
|
+
event_id = _generate_id()
|
|
532
|
+
thread_event = ThreadEvent(
|
|
533
|
+
id=event_id,
|
|
534
|
+
thread_id=thread_id,
|
|
535
|
+
run_id=current_run_id,
|
|
536
|
+
type="message",
|
|
537
|
+
actor="assistant",
|
|
538
|
+
user_id=None,
|
|
539
|
+
user_name=None,
|
|
540
|
+
user_email=None,
|
|
541
|
+
content=[
|
|
542
|
+
DocumentPart(
|
|
543
|
+
url=event.data["url"],
|
|
544
|
+
filename=event.data["filename"],
|
|
545
|
+
mime_type=event.data["mime_type"],
|
|
546
|
+
)
|
|
547
|
+
],
|
|
548
|
+
created_at=event_now,
|
|
549
|
+
)
|
|
550
|
+
await self._persistence.create_event(thread_event)
|
|
551
|
+
|
|
552
|
+
append = S2C_EventAppend(
|
|
553
|
+
id=_generate_id(),
|
|
554
|
+
payload=EventAppendPayload(thread_id=thread_id, events=[thread_event]),
|
|
555
|
+
)
|
|
556
|
+
await self._broadcast_to_worker(append)
|
|
557
|
+
|
|
558
|
+
# Track message.s2c
|
|
559
|
+
await self._track_event(
|
|
560
|
+
AnalyticsEvent.MESSAGE_S2C.value,
|
|
561
|
+
thread_id=thread_id,
|
|
562
|
+
run_id=current_run_id,
|
|
563
|
+
event_id=event_id,
|
|
564
|
+
data={"type": "document"},
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
elif event.response_type == "tool_call":
|
|
568
|
+
event_id = _generate_id()
|
|
569
|
+
thread_event = ThreadEvent(
|
|
570
|
+
id=event_id,
|
|
571
|
+
thread_id=thread_id,
|
|
572
|
+
run_id=current_run_id,
|
|
573
|
+
type="tool.call",
|
|
574
|
+
actor="assistant",
|
|
575
|
+
user_id=None,
|
|
576
|
+
user_name=None,
|
|
577
|
+
user_email=None,
|
|
578
|
+
data={
|
|
579
|
+
"name": event.data["name"],
|
|
580
|
+
"args": event.data["args"],
|
|
581
|
+
},
|
|
582
|
+
created_at=event_now,
|
|
583
|
+
)
|
|
584
|
+
await self._persistence.create_event(thread_event)
|
|
585
|
+
|
|
586
|
+
append = S2C_EventAppend(
|
|
587
|
+
id=_generate_id(),
|
|
588
|
+
payload=EventAppendPayload(thread_id=thread_id, events=[thread_event]),
|
|
589
|
+
)
|
|
590
|
+
await self._broadcast_to_worker(append)
|
|
591
|
+
|
|
592
|
+
# Track tool.called
|
|
593
|
+
await self._track_event(
|
|
594
|
+
AnalyticsEvent.TOOL_CALLED.value,
|
|
595
|
+
thread_id=thread_id,
|
|
596
|
+
run_id=current_run_id,
|
|
597
|
+
event_id=event_id,
|
|
598
|
+
data={"name": event.data["name"]},
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
elif event.response_type == "tool_result":
|
|
602
|
+
thread_event = ThreadEvent(
|
|
603
|
+
id=_generate_id(),
|
|
604
|
+
thread_id=thread_id,
|
|
605
|
+
run_id=current_run_id,
|
|
606
|
+
type="tool.result",
|
|
607
|
+
actor="tool",
|
|
608
|
+
user_id=None,
|
|
609
|
+
user_name=None,
|
|
610
|
+
user_email=None,
|
|
611
|
+
data={
|
|
612
|
+
"name": event.data["name"],
|
|
613
|
+
"result": event.data["result"],
|
|
614
|
+
},
|
|
615
|
+
created_at=event_now,
|
|
616
|
+
)
|
|
617
|
+
await self._persistence.create_event(thread_event)
|
|
618
|
+
|
|
619
|
+
append = S2C_EventAppend(
|
|
620
|
+
id=_generate_id(),
|
|
621
|
+
payload=EventAppendPayload(thread_id=thread_id, events=[thread_event]),
|
|
622
|
+
)
|
|
623
|
+
await self._broadcast_to_worker(append)
|
|
624
|
+
|
|
625
|
+
await self._update_thread_last_event(thread_id, event_now)
|
|
626
|
+
|
|
627
|
+
except Exception as e:
|
|
628
|
+
# Track error.occurred
|
|
629
|
+
await self._track_event(
|
|
630
|
+
AnalyticsEvent.ERROR_OCCURRED.value,
|
|
631
|
+
thread_id=thread_id,
|
|
632
|
+
run_id=current_run_id,
|
|
633
|
+
data={"error": str(e), "type": type(e).__name__},
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
if current_run_id:
|
|
637
|
+
error_now = _now()
|
|
638
|
+
run = Run(
|
|
639
|
+
id=current_run_id,
|
|
640
|
+
thread_id=thread_id,
|
|
641
|
+
status="failed",
|
|
642
|
+
started_at=error_now,
|
|
643
|
+
ended_at=error_now,
|
|
644
|
+
error=str(e),
|
|
645
|
+
)
|
|
646
|
+
await self._persistence.update_run(run)
|
|
647
|
+
|
|
648
|
+
run_upsert = S2C_RunUpsert(
|
|
649
|
+
id=_generate_id(),
|
|
650
|
+
payload=RunUpsertPayload(run=run),
|
|
651
|
+
)
|
|
652
|
+
await self._broadcast_to_worker(run_upsert)
|
|
653
|
+
|
|
654
|
+
async def _update_thread_last_event(self, thread_id: str, timestamp: datetime) -> None:
|
|
655
|
+
thread = await self._persistence.get_thread(thread_id)
|
|
656
|
+
if thread:
|
|
657
|
+
thread.last_event_at = timestamp
|
|
658
|
+
thread.updated_at = timestamp
|
|
659
|
+
await self._persistence.update_thread(thread)
|
|
660
|
+
|
|
661
|
+
def _convert_content_part(self, part):
|
|
662
|
+
if hasattr(part, "model_dump"):
|
|
663
|
+
data = part.model_dump()
|
|
664
|
+
else:
|
|
665
|
+
data = dict(part) if hasattr(part, "__iter__") else part
|
|
666
|
+
|
|
667
|
+
part_type = data.get("type")
|
|
668
|
+
if part_type == "text":
|
|
669
|
+
return TextPart(**data)
|
|
670
|
+
elif part_type == "image":
|
|
671
|
+
return ImagePart(**data)
|
|
672
|
+
elif part_type == "document":
|
|
673
|
+
return DocumentPart(**data)
|
|
674
|
+
return part
|
|
675
|
+
|
|
676
|
+
def _extract_message(self, content: list) -> str:
|
|
677
|
+
texts = []
|
|
678
|
+
for part in content:
|
|
679
|
+
if isinstance(part, TextPart):
|
|
680
|
+
texts.append(part.text)
|
|
681
|
+
return " ".join(texts)
|
|
682
|
+
|
|
683
|
+
# Analytics frame handlers
|
|
684
|
+
|
|
685
|
+
async def _handle_analytics_subscribe(
|
|
686
|
+
self,
|
|
687
|
+
ws: WebSocketProtocol,
|
|
688
|
+
frame: C2S_AnalyticsSubscribe,
|
|
689
|
+
) -> None:
|
|
690
|
+
if not self._worker_id:
|
|
691
|
+
await self._send_ack(
|
|
692
|
+
ws,
|
|
693
|
+
frame.id,
|
|
694
|
+
ok=False,
|
|
695
|
+
error={"code": "NOT_CONNECTED", "message": "Must connect first"},
|
|
696
|
+
)
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
worker_id = frame.payload.worker_id
|
|
700
|
+
if worker_id != self._worker_id:
|
|
701
|
+
await self._send_ack(
|
|
702
|
+
ws,
|
|
703
|
+
frame.id,
|
|
704
|
+
ok=False,
|
|
705
|
+
error={"code": "FORBIDDEN", "message": "Cannot subscribe to other worker's analytics"},
|
|
706
|
+
)
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
# Add to analytics subscriptions
|
|
710
|
+
if worker_id not in self._analytics_subscriptions:
|
|
711
|
+
self._analytics_subscriptions[worker_id] = set()
|
|
712
|
+
self._analytics_subscriptions[worker_id].add(self._ws_id)
|
|
713
|
+
|
|
714
|
+
await self._send_ack(ws, frame.id)
|
|
715
|
+
|
|
716
|
+
async def _handle_analytics_unsubscribe(
|
|
717
|
+
self,
|
|
718
|
+
ws: WebSocketProtocol,
|
|
719
|
+
frame: C2S_AnalyticsUnsubscribe,
|
|
720
|
+
) -> None:
|
|
721
|
+
worker_id = frame.payload.worker_id
|
|
722
|
+
if worker_id in self._analytics_subscriptions:
|
|
723
|
+
self._analytics_subscriptions[worker_id].discard(self._ws_id)
|
|
724
|
+
if not self._analytics_subscriptions[worker_id]:
|
|
725
|
+
del self._analytics_subscriptions[worker_id]
|
|
726
|
+
|
|
727
|
+
await self._send_ack(ws, frame.id)
|
|
728
|
+
|
|
729
|
+
async def _handle_feedback(
|
|
730
|
+
self,
|
|
731
|
+
ws: WebSocketProtocol,
|
|
732
|
+
frame: C2S_Feedback,
|
|
733
|
+
) -> None:
|
|
734
|
+
if not self._worker_id:
|
|
735
|
+
await self._send_ack(
|
|
736
|
+
ws,
|
|
737
|
+
frame.id,
|
|
738
|
+
ok=False,
|
|
739
|
+
error={"code": "NOT_CONNECTED", "message": "Must connect first"},
|
|
740
|
+
)
|
|
741
|
+
return
|
|
742
|
+
|
|
743
|
+
if self._analytics_collector:
|
|
744
|
+
await self._analytics_collector.feedback(
|
|
745
|
+
feedback_type=frame.payload.feedback,
|
|
746
|
+
target_type=frame.payload.target_type,
|
|
747
|
+
target_id=frame.payload.target_id,
|
|
748
|
+
worker_id=self._worker_id,
|
|
749
|
+
user_id=self._user_id,
|
|
750
|
+
reason=frame.payload.reason,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
# Send feedback acknowledgment
|
|
754
|
+
ack = S2C_FeedbackAck(
|
|
755
|
+
id=_generate_id(),
|
|
756
|
+
payload=FeedbackAckPayload(
|
|
757
|
+
target_type=frame.payload.target_type,
|
|
758
|
+
target_id=frame.payload.target_id,
|
|
759
|
+
feedback=frame.payload.feedback,
|
|
760
|
+
ok=True,
|
|
761
|
+
),
|
|
762
|
+
)
|
|
763
|
+
await self._send(ws, ack)
|
|
764
|
+
|
|
765
|
+
# Settings frame handlers
|
|
766
|
+
|
|
767
|
+
async def _handle_settings_subscribe(
|
|
768
|
+
self,
|
|
769
|
+
ws: WebSocketProtocol,
|
|
770
|
+
frame: C2S_SettingsSubscribe,
|
|
771
|
+
) -> None:
|
|
772
|
+
if not self._worker_id:
|
|
773
|
+
await self._send_ack(
|
|
774
|
+
ws,
|
|
775
|
+
frame.id,
|
|
776
|
+
ok=False,
|
|
777
|
+
error={"code": "NOT_CONNECTED", "message": "Must connect first"},
|
|
778
|
+
)
|
|
779
|
+
return
|
|
780
|
+
|
|
781
|
+
worker_id = frame.payload.worker_id
|
|
782
|
+
if worker_id != self._worker_id:
|
|
783
|
+
await self._send_ack(
|
|
784
|
+
ws,
|
|
785
|
+
frame.id,
|
|
786
|
+
ok=False,
|
|
787
|
+
error={"code": "FORBIDDEN", "message": "Cannot subscribe to other worker's settings"},
|
|
788
|
+
)
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
if not self._settings_schema:
|
|
792
|
+
await self._send_ack(
|
|
793
|
+
ws,
|
|
794
|
+
frame.id,
|
|
795
|
+
ok=False,
|
|
796
|
+
error={"code": "NOT_CONFIGURED", "message": "No settings schema configured"},
|
|
797
|
+
)
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
# Add to settings subscriptions
|
|
801
|
+
if worker_id not in self._settings_subscriptions:
|
|
802
|
+
self._settings_subscriptions[worker_id] = set()
|
|
803
|
+
self._settings_subscriptions[worker_id].add(self._ws_id)
|
|
804
|
+
|
|
805
|
+
# Send settings snapshot directly to this connection
|
|
806
|
+
if self._settings_broadcaster:
|
|
807
|
+
values = await self._persistence.get_settings(worker_id)
|
|
808
|
+
await self._settings_broadcaster.broadcast_snapshot_to_ws(
|
|
809
|
+
worker_id=worker_id,
|
|
810
|
+
ws=ws,
|
|
811
|
+
schema=self._settings_schema,
|
|
812
|
+
values=values,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
await self._send_ack(ws, frame.id)
|
|
816
|
+
|
|
817
|
+
async def _handle_settings_unsubscribe(
|
|
818
|
+
self,
|
|
819
|
+
ws: WebSocketProtocol,
|
|
820
|
+
frame: C2S_SettingsUnsubscribe,
|
|
821
|
+
) -> None:
|
|
822
|
+
worker_id = frame.payload.worker_id
|
|
823
|
+
if worker_id in self._settings_subscriptions:
|
|
824
|
+
self._settings_subscriptions[worker_id].discard(self._ws_id)
|
|
825
|
+
if not self._settings_subscriptions[worker_id]:
|
|
826
|
+
del self._settings_subscriptions[worker_id]
|
|
827
|
+
|
|
828
|
+
await self._send_ack(ws, frame.id)
|
|
829
|
+
|
|
830
|
+
async def _handle_settings_patch(
|
|
831
|
+
self,
|
|
832
|
+
ws: WebSocketProtocol,
|
|
833
|
+
frame: C2S_SettingsPatch,
|
|
834
|
+
) -> None:
|
|
835
|
+
if not self._worker_id:
|
|
836
|
+
await self._send_ack(
|
|
837
|
+
ws,
|
|
838
|
+
frame.id,
|
|
839
|
+
ok=False,
|
|
840
|
+
error={"code": "NOT_CONNECTED", "message": "Must connect first"},
|
|
841
|
+
)
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
if not self._settings_schema:
|
|
845
|
+
await self._send_ack(
|
|
846
|
+
ws,
|
|
847
|
+
frame.id,
|
|
848
|
+
ok=False,
|
|
849
|
+
error={"code": "NOT_CONFIGURED", "message": "No settings schema configured"},
|
|
850
|
+
)
|
|
851
|
+
return
|
|
852
|
+
|
|
853
|
+
field_id = frame.payload.field_id
|
|
854
|
+
value = frame.payload.value
|
|
855
|
+
|
|
856
|
+
# Validate field exists and is not readonly
|
|
857
|
+
field = self._settings_schema.get_field(field_id)
|
|
858
|
+
if not field:
|
|
859
|
+
await self._send_ack(
|
|
860
|
+
ws,
|
|
861
|
+
frame.id,
|
|
862
|
+
ok=False,
|
|
863
|
+
error={"code": "NOT_FOUND", "message": f"Unknown field: {field_id}"},
|
|
864
|
+
)
|
|
865
|
+
return
|
|
866
|
+
|
|
867
|
+
if field.readonly:
|
|
868
|
+
await self._send_ack(
|
|
869
|
+
ws,
|
|
870
|
+
frame.id,
|
|
871
|
+
ok=False,
|
|
872
|
+
error={"code": "READONLY", "message": f"Field '{field_id}' is readonly"},
|
|
873
|
+
)
|
|
874
|
+
return
|
|
875
|
+
|
|
876
|
+
# Update setting
|
|
877
|
+
await self._persistence.update_setting(self._worker_id, field_id, value)
|
|
878
|
+
|
|
879
|
+
# Broadcast to other subscribers (exclude sender)
|
|
880
|
+
if self._settings_broadcaster:
|
|
881
|
+
await self._settings_broadcaster.broadcast_patch(
|
|
882
|
+
worker_id=self._worker_id,
|
|
883
|
+
field_id=field_id,
|
|
884
|
+
value=value,
|
|
885
|
+
exclude_ws=ws,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
await self._send_ack(ws, frame.id)
|
|
889
|
+
|
|
890
|
+
# Analytics tracking helpers
|
|
891
|
+
|
|
892
|
+
async def _track_event(
|
|
893
|
+
self,
|
|
894
|
+
event: str,
|
|
895
|
+
thread_id: str | None = None,
|
|
896
|
+
run_id: str | None = None,
|
|
897
|
+
event_id: str | None = None,
|
|
898
|
+
data: dict | None = None,
|
|
899
|
+
) -> None:
|
|
900
|
+
"""Track an analytics event if collector is enabled."""
|
|
901
|
+
if self._analytics_collector and self._worker_id:
|
|
902
|
+
await self._analytics_collector.track(
|
|
903
|
+
event=event,
|
|
904
|
+
worker_id=self._worker_id,
|
|
905
|
+
thread_id=thread_id,
|
|
906
|
+
user_id=self._user_id,
|
|
907
|
+
run_id=run_id,
|
|
908
|
+
event_id=event_id,
|
|
909
|
+
data=data,
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
def _create_noop_analytics(self) -> WorkerAnalytics:
|
|
913
|
+
"""Create a no-op analytics instance when collector is disabled."""
|
|
914
|
+
|
|
915
|
+
# Create a minimal collector that does nothing
|
|
916
|
+
class NoopCollector:
|
|
917
|
+
async def track(self, **kwargs) -> None:
|
|
918
|
+
pass
|
|
919
|
+
|
|
920
|
+
async def feedback(self, **kwargs) -> None:
|
|
921
|
+
pass
|
|
922
|
+
|
|
923
|
+
return WorkerAnalytics(
|
|
924
|
+
worker_id=self._worker_id or "",
|
|
925
|
+
thread_id="",
|
|
926
|
+
user_id=None,
|
|
927
|
+
run_id=None,
|
|
928
|
+
collector=NoopCollector(), # type: ignore
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
def _create_noop_settings(self) -> WorkerSettings:
|
|
932
|
+
"""Create a no-op settings instance when settings are not configured."""
|
|
933
|
+
from dooers.features.settings.models import SettingsSchema
|
|
934
|
+
|
|
935
|
+
class NoopBroadcaster:
|
|
936
|
+
async def broadcast_snapshot(self, **kwargs) -> None:
|
|
937
|
+
pass
|
|
938
|
+
|
|
939
|
+
async def broadcast_patch(self, **kwargs) -> None:
|
|
940
|
+
pass
|
|
941
|
+
|
|
942
|
+
class NoopPersistence:
|
|
943
|
+
async def get_settings(self, worker_id: str) -> dict:
|
|
944
|
+
return {}
|
|
945
|
+
|
|
946
|
+
async def update_setting(self, worker_id: str, field_id: str, value) -> None:
|
|
947
|
+
pass
|
|
948
|
+
|
|
949
|
+
async def set_settings(self, worker_id: str, values: dict) -> None:
|
|
950
|
+
pass
|
|
951
|
+
|
|
952
|
+
return WorkerSettings(
|
|
953
|
+
worker_id=self._worker_id or "",
|
|
954
|
+
schema=SettingsSchema(fields=[]),
|
|
955
|
+
persistence=NoopPersistence(), # type: ignore
|
|
956
|
+
broadcaster=NoopBroadcaster(), # type: ignore
|
|
957
|
+
)
|