asap-protocol 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asap/__init__.py +7 -0
- asap/cli.py +220 -0
- asap/errors.py +150 -0
- asap/examples/README.md +25 -0
- asap/examples/__init__.py +1 -0
- asap/examples/coordinator.py +184 -0
- asap/examples/echo_agent.py +100 -0
- asap/examples/run_demo.py +120 -0
- asap/models/__init__.py +146 -0
- asap/models/base.py +55 -0
- asap/models/constants.py +14 -0
- asap/models/entities.py +410 -0
- asap/models/enums.py +71 -0
- asap/models/envelope.py +94 -0
- asap/models/ids.py +55 -0
- asap/models/parts.py +207 -0
- asap/models/payloads.py +423 -0
- asap/models/types.py +39 -0
- asap/observability/__init__.py +43 -0
- asap/observability/logging.py +216 -0
- asap/observability/metrics.py +399 -0
- asap/schemas.py +203 -0
- asap/state/__init__.py +22 -0
- asap/state/machine.py +86 -0
- asap/state/snapshot.py +265 -0
- asap/transport/__init__.py +84 -0
- asap/transport/client.py +399 -0
- asap/transport/handlers.py +444 -0
- asap/transport/jsonrpc.py +190 -0
- asap/transport/middleware.py +359 -0
- asap/transport/server.py +739 -0
- asap_protocol-0.1.0.dist-info/METADATA +251 -0
- asap_protocol-0.1.0.dist-info/RECORD +36 -0
- asap_protocol-0.1.0.dist-info/WHEEL +4 -0
- asap_protocol-0.1.0.dist-info/entry_points.txt +2 -0
- asap_protocol-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Handler registry for ASAP protocol payload processing.
|
|
2
|
+
|
|
3
|
+
This module provides a handler registry for dispatching ASAP envelopes
|
|
4
|
+
to appropriate handlers based on payload type.
|
|
5
|
+
|
|
6
|
+
The HandlerRegistry allows:
|
|
7
|
+
- Registration of handlers for specific payload types
|
|
8
|
+
- Dispatch of envelopes to registered handlers
|
|
9
|
+
- Discovery of registered payload types
|
|
10
|
+
- Structured logging for observability
|
|
11
|
+
|
|
12
|
+
Thread Safety:
|
|
13
|
+
All operations on HandlerRegistry are thread-safe. The registry uses
|
|
14
|
+
an internal RLock to protect concurrent access to the handler mapping.
|
|
15
|
+
This allows safe usage in multi-threaded environments.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> from asap.transport.handlers import HandlerRegistry, create_echo_handler
|
|
19
|
+
>>> from asap.models.envelope import Envelope
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Create registry and register handler
|
|
22
|
+
>>> registry = HandlerRegistry()
|
|
23
|
+
>>> registry.register("task.request", create_echo_handler())
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Dispatch envelope to handler
|
|
26
|
+
>>> response = registry.dispatch(envelope, manifest)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import inspect
|
|
31
|
+
import time
|
|
32
|
+
from collections.abc import Awaitable
|
|
33
|
+
from threading import RLock
|
|
34
|
+
from typing import Protocol
|
|
35
|
+
|
|
36
|
+
from asap.errors import ASAPError
|
|
37
|
+
from asap.models.entities import Manifest
|
|
38
|
+
from asap.models.enums import TaskStatus
|
|
39
|
+
from asap.models.envelope import Envelope
|
|
40
|
+
from asap.models.ids import generate_id
|
|
41
|
+
from asap.models.payloads import TaskRequest, TaskResponse
|
|
42
|
+
from asap.observability import get_logger
|
|
43
|
+
|
|
44
|
+
# Module logger
|
|
45
|
+
logger = get_logger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SyncHandler(Protocol):
|
|
49
|
+
"""Protocol for synchronous handlers."""
|
|
50
|
+
|
|
51
|
+
def __call__(self, envelope: Envelope, manifest: Manifest) -> Envelope:
|
|
52
|
+
"""Process envelope synchronously.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
envelope: The incoming ASAP envelope to process
|
|
56
|
+
manifest: The server's manifest for context
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Response envelope to send back
|
|
60
|
+
"""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AsyncHandler(Protocol):
|
|
65
|
+
"""Protocol for asynchronous handlers."""
|
|
66
|
+
|
|
67
|
+
def __call__(self, envelope: Envelope, manifest: Manifest) -> Awaitable[Envelope]:
|
|
68
|
+
"""Process envelope asynchronously.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
envelope: The incoming ASAP envelope to process
|
|
72
|
+
manifest: The server's manifest for context
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Awaitable that resolves to response envelope
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Type alias for handler functions (supports both sync and async)
|
|
81
|
+
Handler = SyncHandler | AsyncHandler
|
|
82
|
+
"""Type alias for ASAP message handlers.
|
|
83
|
+
|
|
84
|
+
A handler is a callable that receives an Envelope and a Manifest,
|
|
85
|
+
and returns a response Envelope (sync) or an awaitable that resolves
|
|
86
|
+
to a response Envelope (async).
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
envelope: The incoming ASAP envelope to process
|
|
90
|
+
manifest: The server's manifest for context
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Response envelope to send back (sync) or awaitable (async)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HandlerNotFoundError(ASAPError):
|
|
98
|
+
"""Raised when no handler is registered for a payload type.
|
|
99
|
+
|
|
100
|
+
This error occurs when attempting to dispatch an envelope with
|
|
101
|
+
a payload_type that has no registered handler.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
payload_type: The payload type that has no handler
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> try:
|
|
108
|
+
... raise HandlerNotFoundError("task.request")
|
|
109
|
+
... except HandlerNotFoundError as exc:
|
|
110
|
+
... exc.payload_type
|
|
111
|
+
'task.request'
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self, payload_type: str) -> None:
|
|
115
|
+
"""Initialize handler not found error.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
payload_type: The payload type that has no handler
|
|
119
|
+
"""
|
|
120
|
+
message = f"No handler registered for payload type: {payload_type}"
|
|
121
|
+
super().__init__(
|
|
122
|
+
code="asap:transport/handler_not_found",
|
|
123
|
+
message=message,
|
|
124
|
+
details={"payload_type": payload_type},
|
|
125
|
+
)
|
|
126
|
+
self.payload_type = payload_type
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class HandlerRegistry:
|
|
130
|
+
"""Registry for ASAP payload handlers.
|
|
131
|
+
|
|
132
|
+
HandlerRegistry manages the mapping between payload types and their
|
|
133
|
+
corresponding handlers. It provides methods for registration, dispatch,
|
|
134
|
+
and discovery of handlers.
|
|
135
|
+
|
|
136
|
+
Thread Safety:
|
|
137
|
+
All public methods are thread-safe. The registry uses an internal
|
|
138
|
+
RLock to protect concurrent access to the handler mapping. This
|
|
139
|
+
allows safe concurrent registration and dispatch operations from
|
|
140
|
+
multiple threads.
|
|
141
|
+
|
|
142
|
+
Attributes:
|
|
143
|
+
_handlers: Internal mapping of payload_type to handler function
|
|
144
|
+
_lock: Reentrant lock for thread-safe operations
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
>>> registry = HandlerRegistry()
|
|
148
|
+
>>> registry.register("task.request", my_handler)
|
|
149
|
+
>>> registry.has_handler("task.request")
|
|
150
|
+
True
|
|
151
|
+
>>> response = registry.dispatch(envelope, manifest)
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self) -> None:
|
|
155
|
+
"""Initialize empty handler registry with thread-safe lock."""
|
|
156
|
+
self._handlers: dict[str, Handler] = {}
|
|
157
|
+
self._lock = RLock()
|
|
158
|
+
|
|
159
|
+
def register(self, payload_type: str, handler: Handler) -> None:
|
|
160
|
+
"""Register a handler for a payload type.
|
|
161
|
+
|
|
162
|
+
If a handler is already registered for the payload type,
|
|
163
|
+
it will be replaced with the new handler.
|
|
164
|
+
|
|
165
|
+
This method is thread-safe.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
payload_type: The payload type to handle (e.g., "task.request")
|
|
169
|
+
handler: Callable that processes envelopes of this type
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> registry = HandlerRegistry()
|
|
173
|
+
>>> registry.register("task.request", create_echo_handler())
|
|
174
|
+
"""
|
|
175
|
+
with self._lock:
|
|
176
|
+
is_override = payload_type in self._handlers
|
|
177
|
+
self._handlers[payload_type] = handler
|
|
178
|
+
logger.debug(
|
|
179
|
+
"asap.handler.registered",
|
|
180
|
+
payload_type=payload_type,
|
|
181
|
+
handler_name=handler.__name__ if hasattr(handler, "__name__") else str(handler),
|
|
182
|
+
is_override=is_override,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def has_handler(self, payload_type: str) -> bool:
|
|
186
|
+
"""Check if a handler is registered for a payload type.
|
|
187
|
+
|
|
188
|
+
This method is thread-safe.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
payload_type: The payload type to check
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if a handler is registered, False otherwise
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
>>> registry = HandlerRegistry()
|
|
198
|
+
>>> registry.has_handler("task.request")
|
|
199
|
+
False
|
|
200
|
+
"""
|
|
201
|
+
with self._lock:
|
|
202
|
+
return payload_type in self._handlers
|
|
203
|
+
|
|
204
|
+
def dispatch(self, envelope: Envelope, manifest: Manifest) -> Envelope:
|
|
205
|
+
"""Dispatch an envelope to its registered handler.
|
|
206
|
+
|
|
207
|
+
Looks up the handler for the envelope's payload_type and
|
|
208
|
+
invokes it with the envelope and manifest.
|
|
209
|
+
|
|
210
|
+
This method is thread-safe for handler lookup. The handler
|
|
211
|
+
execution itself is not protected by the lock.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
envelope: The incoming ASAP envelope
|
|
215
|
+
manifest: The server's manifest for context
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Response envelope from the handler
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
HandlerNotFoundError: If no handler is registered for the payload type
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> registry = create_default_registry()
|
|
225
|
+
>>> response = registry.dispatch(envelope, manifest)
|
|
226
|
+
"""
|
|
227
|
+
payload_type = envelope.payload_type
|
|
228
|
+
start_time = time.perf_counter()
|
|
229
|
+
|
|
230
|
+
with self._lock:
|
|
231
|
+
if payload_type not in self._handlers:
|
|
232
|
+
logger.warning(
|
|
233
|
+
"asap.handler.not_found",
|
|
234
|
+
payload_type=payload_type,
|
|
235
|
+
envelope_id=envelope.id,
|
|
236
|
+
)
|
|
237
|
+
raise HandlerNotFoundError(payload_type)
|
|
238
|
+
handler = self._handlers[payload_type]
|
|
239
|
+
|
|
240
|
+
# Log dispatch start
|
|
241
|
+
logger.debug(
|
|
242
|
+
"asap.handler.dispatch",
|
|
243
|
+
payload_type=payload_type,
|
|
244
|
+
envelope_id=envelope.id,
|
|
245
|
+
handler_name=handler.__name__ if hasattr(handler, "__name__") else str(handler),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Execute handler outside the lock to allow concurrent dispatches
|
|
249
|
+
try:
|
|
250
|
+
# Note: dispatch() only works with sync handlers that return Envelope directly
|
|
251
|
+
# For async handlers, use dispatch_async() instead
|
|
252
|
+
# Type narrowing: we expect sync handlers here
|
|
253
|
+
result = handler(envelope, manifest)
|
|
254
|
+
# For sync handlers, result is Envelope directly
|
|
255
|
+
if inspect.isawaitable(result):
|
|
256
|
+
raise TypeError(
|
|
257
|
+
f"Handler {handler} returned awaitable in sync dispatch(). "
|
|
258
|
+
"Use dispatch_async() for async handlers."
|
|
259
|
+
)
|
|
260
|
+
response: Envelope = result
|
|
261
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
262
|
+
logger.debug(
|
|
263
|
+
"asap.handler.completed",
|
|
264
|
+
payload_type=payload_type,
|
|
265
|
+
envelope_id=envelope.id,
|
|
266
|
+
response_id=response.id,
|
|
267
|
+
duration_ms=round(duration_ms, 2),
|
|
268
|
+
)
|
|
269
|
+
return response
|
|
270
|
+
except Exception as e:
|
|
271
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
272
|
+
logger.exception(
|
|
273
|
+
"asap.handler.error",
|
|
274
|
+
payload_type=payload_type,
|
|
275
|
+
envelope_id=envelope.id,
|
|
276
|
+
error=str(e),
|
|
277
|
+
error_type=type(e).__name__,
|
|
278
|
+
duration_ms=round(duration_ms, 2),
|
|
279
|
+
)
|
|
280
|
+
raise
|
|
281
|
+
|
|
282
|
+
async def dispatch_async(self, envelope: Envelope, manifest: Manifest) -> Envelope:
|
|
283
|
+
"""Dispatch an envelope to its registered handler (async version).
|
|
284
|
+
|
|
285
|
+
This method supports both synchronous and asynchronous handlers.
|
|
286
|
+
When called from an async context (e.g., FastAPI endpoint), this
|
|
287
|
+
method will properly await async handlers and run sync handlers
|
|
288
|
+
in a thread pool to avoid blocking the event loop.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
envelope: The ASAP envelope to dispatch
|
|
292
|
+
manifest: The server's manifest for context
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Response envelope from the handler
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
HandlerNotFoundError: If no handler is registered for the payload type
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
>>> registry = create_default_registry()
|
|
302
|
+
>>> response = await registry.dispatch_async(envelope, manifest)
|
|
303
|
+
"""
|
|
304
|
+
payload_type = envelope.payload_type
|
|
305
|
+
start_time = time.perf_counter()
|
|
306
|
+
|
|
307
|
+
with self._lock:
|
|
308
|
+
if payload_type not in self._handlers:
|
|
309
|
+
logger.warning(
|
|
310
|
+
"asap.handler.not_found",
|
|
311
|
+
payload_type=payload_type,
|
|
312
|
+
envelope_id=envelope.id,
|
|
313
|
+
)
|
|
314
|
+
raise HandlerNotFoundError(payload_type)
|
|
315
|
+
handler = self._handlers[payload_type]
|
|
316
|
+
|
|
317
|
+
# Log dispatch start
|
|
318
|
+
logger.debug(
|
|
319
|
+
"asap.handler.dispatch",
|
|
320
|
+
payload_type=payload_type,
|
|
321
|
+
envelope_id=envelope.id,
|
|
322
|
+
handler_name=handler.__name__ if hasattr(handler, "__name__") else str(handler),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Execute handler outside the lock to allow concurrent dispatches
|
|
326
|
+
try:
|
|
327
|
+
# Support both sync and async handlers
|
|
328
|
+
response: Envelope
|
|
329
|
+
if inspect.iscoroutinefunction(handler):
|
|
330
|
+
# Async handler - await it directly
|
|
331
|
+
response = await handler(envelope, manifest)
|
|
332
|
+
else:
|
|
333
|
+
# Sync handler - run in thread pool to avoid blocking event loop
|
|
334
|
+
# Also handle async callable objects that return awaitables
|
|
335
|
+
loop = asyncio.get_event_loop()
|
|
336
|
+
result: object = await loop.run_in_executor(None, handler, envelope, manifest)
|
|
337
|
+
# Check if result is awaitable (handles async __call__ methods)
|
|
338
|
+
if inspect.isawaitable(result):
|
|
339
|
+
response = await result
|
|
340
|
+
else:
|
|
341
|
+
# Type narrowing: result is Envelope for sync handlers
|
|
342
|
+
response = result # type: ignore[assignment]
|
|
343
|
+
|
|
344
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
345
|
+
logger.debug(
|
|
346
|
+
"asap.handler.completed",
|
|
347
|
+
payload_type=payload_type,
|
|
348
|
+
envelope_id=envelope.id,
|
|
349
|
+
response_id=response.id,
|
|
350
|
+
duration_ms=round(duration_ms, 2),
|
|
351
|
+
)
|
|
352
|
+
return response
|
|
353
|
+
except Exception as e:
|
|
354
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
355
|
+
logger.exception(
|
|
356
|
+
"asap.handler.error",
|
|
357
|
+
payload_type=payload_type,
|
|
358
|
+
envelope_id=envelope.id,
|
|
359
|
+
error=str(e),
|
|
360
|
+
error_type=type(e).__name__,
|
|
361
|
+
duration_ms=round(duration_ms, 2),
|
|
362
|
+
)
|
|
363
|
+
raise
|
|
364
|
+
|
|
365
|
+
def list_handlers(self) -> list[str]:
|
|
366
|
+
"""List all registered payload types.
|
|
367
|
+
|
|
368
|
+
This method is thread-safe. Returns a copy of the keys list.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
List of payload type strings that have registered handlers
|
|
372
|
+
|
|
373
|
+
Example:
|
|
374
|
+
>>> registry = create_default_registry()
|
|
375
|
+
>>> registry.list_handlers()
|
|
376
|
+
['task.request']
|
|
377
|
+
"""
|
|
378
|
+
with self._lock:
|
|
379
|
+
return list(self._handlers.keys())
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def create_echo_handler() -> Handler:
|
|
383
|
+
"""Create an echo handler that echoes TaskRequest input.
|
|
384
|
+
|
|
385
|
+
The echo handler is a simple implementation that:
|
|
386
|
+
- Receives a TaskRequest envelope
|
|
387
|
+
- Returns a TaskResponse with the input echoed back
|
|
388
|
+
- Preserves trace_id and sets correlation_id
|
|
389
|
+
|
|
390
|
+
This is useful for testing and as a base for custom handlers.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Handler function that echoes TaskRequest input
|
|
394
|
+
|
|
395
|
+
Example:
|
|
396
|
+
>>> handler = create_echo_handler()
|
|
397
|
+
>>> response = handler(request_envelope, manifest)
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
def echo_handler(envelope: Envelope, manifest: Manifest) -> Envelope:
|
|
401
|
+
"""Echo handler implementation."""
|
|
402
|
+
# Parse the TaskRequest payload
|
|
403
|
+
task_request = TaskRequest(**envelope.payload)
|
|
404
|
+
|
|
405
|
+
# Create response with echoed input
|
|
406
|
+
response_payload = TaskResponse(
|
|
407
|
+
task_id=f"task_{generate_id()}",
|
|
408
|
+
status=TaskStatus.COMPLETED,
|
|
409
|
+
result={"echoed": task_request.input},
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Create response envelope
|
|
413
|
+
return Envelope(
|
|
414
|
+
asap_version=envelope.asap_version,
|
|
415
|
+
sender=manifest.id,
|
|
416
|
+
recipient=envelope.sender,
|
|
417
|
+
payload_type="task.response",
|
|
418
|
+
payload=response_payload.model_dump(),
|
|
419
|
+
correlation_id=envelope.id,
|
|
420
|
+
trace_id=envelope.trace_id,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
return echo_handler
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def create_default_registry() -> HandlerRegistry:
|
|
427
|
+
"""Create a registry with default handlers.
|
|
428
|
+
|
|
429
|
+
Creates a HandlerRegistry pre-configured with standard handlers:
|
|
430
|
+
- task.request: Echo handler (for basic testing)
|
|
431
|
+
|
|
432
|
+
Additional handlers can be registered after creation.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
HandlerRegistry with default handlers registered
|
|
436
|
+
|
|
437
|
+
Example:
|
|
438
|
+
>>> registry = create_default_registry()
|
|
439
|
+
>>> registry.has_handler("task.request")
|
|
440
|
+
True
|
|
441
|
+
"""
|
|
442
|
+
registry = HandlerRegistry()
|
|
443
|
+
registry.register("task.request", create_echo_handler())
|
|
444
|
+
return registry
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""JSON-RPC 2.0 wrapper models for ASAP protocol transport.
|
|
2
|
+
|
|
3
|
+
This module implements JSON-RPC 2.0 specification (https://www.jsonrpc.org/specification)
|
|
4
|
+
to wrap ASAP Envelope messages for HTTP transport.
|
|
5
|
+
|
|
6
|
+
The JSON-RPC layer provides:
|
|
7
|
+
- Standard request/response structure
|
|
8
|
+
- Error handling with standard error codes
|
|
9
|
+
- Request/response correlation via id field
|
|
10
|
+
|
|
11
|
+
Standard JSON-RPC Error Codes:
|
|
12
|
+
-32700: Parse error (invalid JSON)
|
|
13
|
+
-32600: Invalid request (malformed JSON-RPC)
|
|
14
|
+
-32601: Method not found
|
|
15
|
+
-32602: Invalid params
|
|
16
|
+
-32603: Internal error
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> from asap.models import Envelope, TaskRequest
|
|
20
|
+
>>> from asap.transport.jsonrpc import JsonRpcRequest
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Create ASAP envelope
|
|
23
|
+
>>> envelope = Envelope(
|
|
24
|
+
... sender="urn:asap:agent:client",
|
|
25
|
+
... recipient="urn:asap:agent:server",
|
|
26
|
+
... payload_type="task.request",
|
|
27
|
+
... payload=TaskRequest(...)
|
|
28
|
+
... )
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Wrap in JSON-RPC
|
|
31
|
+
>>> rpc_request = JsonRpcRequest(
|
|
32
|
+
... method="asap.send",
|
|
33
|
+
... params={"envelope": envelope.model_dump()},
|
|
34
|
+
... id="req-1"
|
|
35
|
+
... )
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from typing import Any, Literal
|
|
39
|
+
|
|
40
|
+
from pydantic import Field
|
|
41
|
+
|
|
42
|
+
from asap.models.base import ASAPBaseModel
|
|
43
|
+
|
|
44
|
+
# JSON-RPC 2.0 Standard Error Codes
|
|
45
|
+
PARSE_ERROR = -32700
|
|
46
|
+
INVALID_REQUEST = -32600
|
|
47
|
+
METHOD_NOT_FOUND = -32601
|
|
48
|
+
INVALID_PARAMS = -32602
|
|
49
|
+
INTERNAL_ERROR = -32603
|
|
50
|
+
|
|
51
|
+
# ASAP Protocol JSON-RPC Method Name
|
|
52
|
+
ASAP_METHOD = "asap.send"
|
|
53
|
+
|
|
54
|
+
# Error code descriptions
|
|
55
|
+
ERROR_MESSAGES: dict[int, str] = {
|
|
56
|
+
PARSE_ERROR: "Parse error",
|
|
57
|
+
INVALID_REQUEST: "Invalid request",
|
|
58
|
+
METHOD_NOT_FOUND: "Method not found",
|
|
59
|
+
INVALID_PARAMS: "Invalid params",
|
|
60
|
+
INTERNAL_ERROR: "Internal error",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class JsonRpcError(ASAPBaseModel):
|
|
65
|
+
"""JSON-RPC 2.0 error object.
|
|
66
|
+
|
|
67
|
+
Represents an error that occurred during request processing.
|
|
68
|
+
Follows JSON-RPC 2.0 specification for error responses.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
code: Integer error code (standard or application-defined)
|
|
72
|
+
message: Short error description
|
|
73
|
+
data: Optional additional error information
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> error = JsonRpcError(
|
|
77
|
+
... code=-32602,
|
|
78
|
+
... message="Invalid params",
|
|
79
|
+
... data={"missing_field": "task_id"}
|
|
80
|
+
... )
|
|
81
|
+
>>> error.code
|
|
82
|
+
-32602
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
code: int = Field(description="Error code (negative integer)")
|
|
86
|
+
message: str = Field(description="Short error description")
|
|
87
|
+
data: dict[str, Any] | None = Field(
|
|
88
|
+
default=None, description="Optional additional error information"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def from_code(code: int, data: dict[str, Any] | None = None) -> "JsonRpcError":
|
|
93
|
+
"""Create error from standard error code.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
code: Standard JSON-RPC error code
|
|
97
|
+
data: Optional additional error information
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
JsonRpcError instance with standard message
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> error = JsonRpcError.from_code(INVALID_PARAMS, data={"field": "task_id"})
|
|
104
|
+
>>> error.message
|
|
105
|
+
'Invalid params'
|
|
106
|
+
"""
|
|
107
|
+
message = ERROR_MESSAGES.get(code, "Unknown error")
|
|
108
|
+
return JsonRpcError(code=code, message=message, data=data)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class JsonRpcRequest(ASAPBaseModel):
|
|
112
|
+
"""JSON-RPC 2.0 request.
|
|
113
|
+
|
|
114
|
+
Wraps an ASAP Envelope in a JSON-RPC request structure.
|
|
115
|
+
The envelope is passed in the params field.
|
|
116
|
+
|
|
117
|
+
Attributes:
|
|
118
|
+
jsonrpc: Protocol version (always "2.0")
|
|
119
|
+
method: RPC method name (typically "asap.send")
|
|
120
|
+
params: Request parameters (contains ASAP envelope)
|
|
121
|
+
id: Request identifier for correlation
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> request = JsonRpcRequest(
|
|
125
|
+
... method="asap.send",
|
|
126
|
+
... params={"envelope": {...}},
|
|
127
|
+
... id="req-123"
|
|
128
|
+
... )
|
|
129
|
+
>>> request.jsonrpc
|
|
130
|
+
'2.0'
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
jsonrpc: Literal["2.0"] = Field(
|
|
134
|
+
default="2.0", description="JSON-RPC protocol version (always '2.0')"
|
|
135
|
+
)
|
|
136
|
+
method: str = Field(description="RPC method name")
|
|
137
|
+
params: dict[str, Any] = Field(description="Request parameters")
|
|
138
|
+
id: str | int = Field(description="Request identifier for correlation")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class JsonRpcResponse(ASAPBaseModel):
|
|
142
|
+
"""JSON-RPC 2.0 successful response.
|
|
143
|
+
|
|
144
|
+
Wraps an ASAP Envelope response or other result data.
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
jsonrpc: Protocol version (always "2.0")
|
|
148
|
+
result: Response data (ASAP envelope or other result)
|
|
149
|
+
id: Request identifier (matches original request)
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
>>> response = JsonRpcResponse(
|
|
153
|
+
... result={"envelope": {...}},
|
|
154
|
+
... id="req-123"
|
|
155
|
+
... )
|
|
156
|
+
>>> response.jsonrpc
|
|
157
|
+
'2.0'
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
jsonrpc: Literal["2.0"] = Field(
|
|
161
|
+
default="2.0", description="JSON-RPC protocol version (always '2.0')"
|
|
162
|
+
)
|
|
163
|
+
result: dict[str, Any] = Field(description="Response data")
|
|
164
|
+
id: str | int = Field(description="Request identifier (matches request)")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class JsonRpcErrorResponse(ASAPBaseModel):
|
|
168
|
+
"""JSON-RPC 2.0 error response.
|
|
169
|
+
|
|
170
|
+
Returned when a request fails or cannot be processed.
|
|
171
|
+
|
|
172
|
+
Attributes:
|
|
173
|
+
jsonrpc: Protocol version (always "2.0")
|
|
174
|
+
error: Error object with code, message, and optional data
|
|
175
|
+
id: Request identifier (matches request, or null if id unavailable)
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
>>> error_response = JsonRpcErrorResponse(
|
|
179
|
+
... error=JsonRpcError(code=-32602, message="Invalid params"),
|
|
180
|
+
... id="req-123"
|
|
181
|
+
... )
|
|
182
|
+
>>> error_response.error.code
|
|
183
|
+
-32602
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
jsonrpc: Literal["2.0"] = Field(
|
|
187
|
+
default="2.0", description="JSON-RPC protocol version (always '2.0')"
|
|
188
|
+
)
|
|
189
|
+
error: JsonRpcError = Field(description="Error object")
|
|
190
|
+
id: str | int | None = Field(description="Request identifier (or null)")
|