jaf-py 2.5.10__py3-none-any.whl → 2.5.11__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.
- jaf/__init__.py +154 -57
- jaf/a2a/__init__.py +42 -21
- jaf/a2a/agent.py +79 -126
- jaf/a2a/agent_card.py +87 -78
- jaf/a2a/client.py +30 -66
- jaf/a2a/examples/client_example.py +12 -12
- jaf/a2a/examples/integration_example.py +38 -47
- jaf/a2a/examples/server_example.py +56 -53
- jaf/a2a/memory/__init__.py +0 -4
- jaf/a2a/memory/cleanup.py +28 -21
- jaf/a2a/memory/factory.py +155 -133
- jaf/a2a/memory/providers/composite.py +21 -26
- jaf/a2a/memory/providers/in_memory.py +89 -83
- jaf/a2a/memory/providers/postgres.py +117 -115
- jaf/a2a/memory/providers/redis.py +128 -121
- jaf/a2a/memory/serialization.py +77 -87
- jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
- jaf/a2a/memory/tests/test_cleanup.py +211 -94
- jaf/a2a/memory/tests/test_serialization.py +73 -68
- jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
- jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
- jaf/a2a/memory/types.py +91 -53
- jaf/a2a/protocol.py +95 -125
- jaf/a2a/server.py +90 -118
- jaf/a2a/standalone_client.py +30 -43
- jaf/a2a/tests/__init__.py +16 -33
- jaf/a2a/tests/run_tests.py +17 -53
- jaf/a2a/tests/test_agent.py +40 -140
- jaf/a2a/tests/test_client.py +54 -117
- jaf/a2a/tests/test_integration.py +28 -82
- jaf/a2a/tests/test_protocol.py +54 -139
- jaf/a2a/tests/test_types.py +50 -136
- jaf/a2a/types.py +58 -34
- jaf/cli.py +21 -41
- jaf/core/__init__.py +7 -1
- jaf/core/agent_tool.py +93 -72
- jaf/core/analytics.py +257 -207
- jaf/core/checkpoint.py +223 -0
- jaf/core/composition.py +249 -235
- jaf/core/engine.py +817 -519
- jaf/core/errors.py +55 -42
- jaf/core/guardrails.py +276 -202
- jaf/core/handoff.py +47 -31
- jaf/core/parallel_agents.py +69 -75
- jaf/core/performance.py +75 -73
- jaf/core/proxy.py +43 -44
- jaf/core/proxy_helpers.py +24 -27
- jaf/core/regeneration.py +220 -129
- jaf/core/state.py +68 -66
- jaf/core/streaming.py +115 -108
- jaf/core/tool_results.py +111 -101
- jaf/core/tools.py +114 -116
- jaf/core/tracing.py +269 -210
- jaf/core/types.py +371 -151
- jaf/core/workflows.py +209 -168
- jaf/exceptions.py +46 -38
- jaf/memory/__init__.py +1 -6
- jaf/memory/approval_storage.py +54 -77
- jaf/memory/factory.py +4 -4
- jaf/memory/providers/in_memory.py +216 -180
- jaf/memory/providers/postgres.py +216 -146
- jaf/memory/providers/redis.py +173 -116
- jaf/memory/types.py +70 -51
- jaf/memory/utils.py +36 -34
- jaf/plugins/__init__.py +12 -12
- jaf/plugins/base.py +105 -96
- jaf/policies/__init__.py +0 -1
- jaf/policies/handoff.py +37 -46
- jaf/policies/validation.py +76 -52
- jaf/providers/__init__.py +6 -3
- jaf/providers/mcp.py +97 -51
- jaf/providers/model.py +360 -279
- jaf/server/__init__.py +1 -1
- jaf/server/main.py +7 -11
- jaf/server/server.py +514 -359
- jaf/server/types.py +208 -52
- jaf/utils/__init__.py +17 -18
- jaf/utils/attachments.py +111 -116
- jaf/utils/document_processor.py +175 -174
- jaf/visualization/__init__.py +1 -1
- jaf/visualization/example.py +111 -110
- jaf/visualization/functional_core.py +46 -71
- jaf/visualization/graphviz.py +154 -189
- jaf/visualization/imperative_shell.py +7 -16
- jaf/visualization/types.py +8 -4
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
- jaf_py-2.5.11.dist-info/RECORD +97 -0
- jaf_py-2.5.10.dist-info/RECORD +0 -96
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
|
@@ -30,16 +30,15 @@ from ..types import (
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
async def create_a2a_redis_task_provider(
|
|
33
|
-
config: A2ARedisTaskConfig,
|
|
34
|
-
redis_client: Any
|
|
33
|
+
config: A2ARedisTaskConfig, redis_client: Any
|
|
35
34
|
) -> A2AResult[A2ATaskProvider]:
|
|
36
35
|
"""
|
|
37
36
|
Create a Redis-based A2A task provider
|
|
38
|
-
|
|
37
|
+
|
|
39
38
|
Args:
|
|
40
39
|
config: Configuration for the Redis provider
|
|
41
40
|
redis_client: Redis client instance
|
|
42
|
-
|
|
41
|
+
|
|
43
42
|
Returns:
|
|
44
43
|
A2AResult containing the task provider or an error
|
|
45
44
|
"""
|
|
@@ -62,39 +61,37 @@ async def create_a2a_redis_task_provider(
|
|
|
62
61
|
def serialized_task_to_hash(serialized: A2ATaskSerialized) -> Dict[str, str]:
|
|
63
62
|
"""Convert serialized task to Redis hash"""
|
|
64
63
|
hash_data = {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
"taskId": serialized.task_id,
|
|
65
|
+
"contextId": serialized.context_id,
|
|
66
|
+
"state": serialized.state,
|
|
67
|
+
"taskData": serialized.task_data,
|
|
68
|
+
"createdAt": serialized.created_at,
|
|
69
|
+
"updatedAt": serialized.updated_at,
|
|
71
70
|
}
|
|
72
71
|
if serialized.status_message:
|
|
73
|
-
hash_data[
|
|
72
|
+
hash_data["statusMessage"] = serialized.status_message
|
|
74
73
|
if serialized.metadata:
|
|
75
|
-
hash_data[
|
|
74
|
+
hash_data["metadata"] = serialized.metadata
|
|
76
75
|
return hash_data
|
|
77
76
|
|
|
78
77
|
def hash_to_serialized_task(hash_data: Dict[str, str]) -> A2ATaskSerialized:
|
|
79
78
|
"""Convert Redis hash to serialized task"""
|
|
80
79
|
return A2ATaskSerialized(
|
|
81
|
-
task_id=hash_data[
|
|
82
|
-
context_id=hash_data[
|
|
83
|
-
state=hash_data[
|
|
84
|
-
task_data=hash_data[
|
|
85
|
-
status_message=hash_data.get(
|
|
86
|
-
created_at=hash_data[
|
|
87
|
-
updated_at=hash_data[
|
|
88
|
-
metadata=hash_data.get(
|
|
80
|
+
task_id=hash_data["taskId"],
|
|
81
|
+
context_id=hash_data["contextId"],
|
|
82
|
+
state=hash_data["state"],
|
|
83
|
+
task_data=hash_data["taskData"],
|
|
84
|
+
status_message=hash_data.get("statusMessage"),
|
|
85
|
+
created_at=hash_data["createdAt"],
|
|
86
|
+
updated_at=hash_data["updatedAt"],
|
|
87
|
+
metadata=hash_data.get("metadata"),
|
|
89
88
|
)
|
|
90
89
|
|
|
91
90
|
class RedisA2ATaskProvider:
|
|
92
91
|
"""Redis implementation of A2ATaskProvider"""
|
|
93
92
|
|
|
94
93
|
async def store_task(
|
|
95
|
-
self,
|
|
96
|
-
task: A2ATask,
|
|
97
|
-
metadata: Optional[Dict[str, Any]] = None
|
|
94
|
+
self, task: A2ATask, metadata: Optional[Dict[str, Any]] = None
|
|
98
95
|
) -> A2AResult[None]:
|
|
99
96
|
"""Store a new A2A task in Redis"""
|
|
100
97
|
try:
|
|
@@ -120,8 +117,8 @@ async def create_a2a_redis_task_provider(
|
|
|
120
117
|
await pipe.hset(task_key, mapping=task_hash)
|
|
121
118
|
|
|
122
119
|
# Set TTL if specified
|
|
123
|
-
if metadata and metadata.get(
|
|
124
|
-
expires_at = metadata[
|
|
120
|
+
if metadata and metadata.get("expires_at"):
|
|
121
|
+
expires_at = metadata["expires_at"]
|
|
125
122
|
if isinstance(expires_at, datetime):
|
|
126
123
|
ttl_seconds = int((expires_at - datetime.now()).total_seconds())
|
|
127
124
|
if ttl_seconds > 0:
|
|
@@ -134,8 +131,8 @@ async def create_a2a_redis_task_provider(
|
|
|
134
131
|
await pipe.sadd(state_index_key, task.id)
|
|
135
132
|
|
|
136
133
|
# Update stats
|
|
137
|
-
await pipe.hincrby(get_stats_key(),
|
|
138
|
-
await pipe.hincrby(get_stats_key(), f
|
|
134
|
+
await pipe.hincrby(get_stats_key(), "totalTasks", 1)
|
|
135
|
+
await pipe.hincrby(get_stats_key(), f"state:{task.status.state.value}", 1)
|
|
139
136
|
|
|
140
137
|
await pipe.execute()
|
|
141
138
|
|
|
@@ -143,7 +140,7 @@ async def create_a2a_redis_task_provider(
|
|
|
143
140
|
|
|
144
141
|
except Exception as error:
|
|
145
142
|
return create_a2a_failure(
|
|
146
|
-
create_a2a_task_storage_error(
|
|
143
|
+
create_a2a_task_storage_error("store", "redis", task.id, error)
|
|
147
144
|
)
|
|
148
145
|
|
|
149
146
|
async def get_task(self, task_id: str) -> A2AResult[Optional[A2ATask]]:
|
|
@@ -156,14 +153,17 @@ async def create_a2a_redis_task_provider(
|
|
|
156
153
|
return create_a2a_success(None)
|
|
157
154
|
|
|
158
155
|
hash_data = await redis_client.hgetall(task_key)
|
|
159
|
-
if not hash_data or
|
|
156
|
+
if not hash_data or "taskData" not in hash_data:
|
|
160
157
|
return create_a2a_success(None)
|
|
161
158
|
|
|
162
159
|
# Convert bytes to strings if needed (depends on Redis client)
|
|
163
160
|
if isinstance(hash_data, dict):
|
|
164
|
-
hash_data = {
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
hash_data = {
|
|
162
|
+
k.decode() if isinstance(k, bytes) else k: v.decode()
|
|
163
|
+
if isinstance(v, bytes)
|
|
164
|
+
else v
|
|
165
|
+
for k, v in hash_data.items()
|
|
166
|
+
}
|
|
167
167
|
|
|
168
168
|
serialized = hash_to_serialized_task(hash_data)
|
|
169
169
|
deserialize_result = deserialize_a2a_task(serialized)
|
|
@@ -175,13 +175,11 @@ async def create_a2a_redis_task_provider(
|
|
|
175
175
|
|
|
176
176
|
except Exception as error:
|
|
177
177
|
return create_a2a_failure(
|
|
178
|
-
create_a2a_task_storage_error(
|
|
178
|
+
create_a2a_task_storage_error("get", "redis", task_id, error)
|
|
179
179
|
)
|
|
180
180
|
|
|
181
181
|
async def update_task(
|
|
182
|
-
self,
|
|
183
|
-
task: A2ATask,
|
|
184
|
-
metadata: Optional[Dict[str, Any]] = None
|
|
182
|
+
self, task: A2ATask, metadata: Optional[Dict[str, Any]] = None
|
|
185
183
|
) -> A2AResult[None]:
|
|
186
184
|
"""Update an existing task in Redis"""
|
|
187
185
|
try:
|
|
@@ -189,18 +187,19 @@ async def create_a2a_redis_task_provider(
|
|
|
189
187
|
exists = await redis_client.exists(task_key)
|
|
190
188
|
|
|
191
189
|
if not exists:
|
|
192
|
-
return create_a2a_failure(
|
|
193
|
-
create_a2a_task_not_found_error(task.id, 'redis')
|
|
194
|
-
)
|
|
190
|
+
return create_a2a_failure(create_a2a_task_not_found_error(task.id, "redis"))
|
|
195
191
|
|
|
196
192
|
# Get existing task to check for state changes
|
|
197
193
|
existing_hash = await redis_client.hgetall(task_key)
|
|
198
194
|
if isinstance(existing_hash, dict):
|
|
199
|
-
existing_hash = {
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
existing_hash = {
|
|
196
|
+
k.decode() if isinstance(k, bytes) else k: v.decode()
|
|
197
|
+
if isinstance(v, bytes)
|
|
198
|
+
else v
|
|
199
|
+
for k, v in existing_hash.items()
|
|
200
|
+
}
|
|
202
201
|
|
|
203
|
-
old_state = existing_hash.get(
|
|
202
|
+
old_state = existing_hash.get("state")
|
|
204
203
|
|
|
205
204
|
# Validate and sanitize task
|
|
206
205
|
sanitize_result = sanitize_task(task)
|
|
@@ -209,9 +208,9 @@ async def create_a2a_redis_task_provider(
|
|
|
209
208
|
|
|
210
209
|
# Merge metadata
|
|
211
210
|
existing_metadata = {}
|
|
212
|
-
if existing_hash.get(
|
|
211
|
+
if existing_hash.get("metadata"):
|
|
213
212
|
try:
|
|
214
|
-
existing_metadata = json.loads(existing_hash[
|
|
213
|
+
existing_metadata = json.loads(existing_hash["metadata"])
|
|
215
214
|
except:
|
|
216
215
|
pass
|
|
217
216
|
merged_metadata = {**existing_metadata, **(metadata or {})}
|
|
@@ -237,8 +236,10 @@ async def create_a2a_redis_task_provider(
|
|
|
237
236
|
await pipe.sadd(new_state_index_key, task.id)
|
|
238
237
|
|
|
239
238
|
# Update stats
|
|
240
|
-
await pipe.hincrby(get_stats_key(), f
|
|
241
|
-
await pipe.hincrby(
|
|
239
|
+
await pipe.hincrby(get_stats_key(), f"state:{old_state}", -1)
|
|
240
|
+
await pipe.hincrby(
|
|
241
|
+
get_stats_key(), f"state:{task.status.state.value}", 1
|
|
242
|
+
)
|
|
242
243
|
|
|
243
244
|
await pipe.execute()
|
|
244
245
|
|
|
@@ -246,7 +247,7 @@ async def create_a2a_redis_task_provider(
|
|
|
246
247
|
|
|
247
248
|
except Exception as error:
|
|
248
249
|
return create_a2a_failure(
|
|
249
|
-
create_a2a_task_storage_error(
|
|
250
|
+
create_a2a_task_storage_error("update", "redis", task.id, error)
|
|
250
251
|
)
|
|
251
252
|
|
|
252
253
|
async def update_task_status(
|
|
@@ -254,7 +255,7 @@ async def create_a2a_redis_task_provider(
|
|
|
254
255
|
task_id: str,
|
|
255
256
|
state: TaskState,
|
|
256
257
|
status_message: Optional[Any] = None,
|
|
257
|
-
timestamp: Optional[str] = None
|
|
258
|
+
timestamp: Optional[str] = None,
|
|
258
259
|
) -> A2AResult[None]:
|
|
259
260
|
"""Update task status only"""
|
|
260
261
|
try:
|
|
@@ -264,9 +265,7 @@ async def create_a2a_redis_task_provider(
|
|
|
264
265
|
return get_result
|
|
265
266
|
|
|
266
267
|
if not get_result.data:
|
|
267
|
-
return create_a2a_failure(
|
|
268
|
-
create_a2a_task_not_found_error(task_id, 'redis')
|
|
269
|
-
)
|
|
268
|
+
return create_a2a_failure(create_a2a_task_not_found_error(task_id, "redis"))
|
|
270
269
|
|
|
271
270
|
task = get_result.data
|
|
272
271
|
|
|
@@ -277,30 +276,30 @@ async def create_a2a_redis_task_provider(
|
|
|
277
276
|
|
|
278
277
|
# Update task status
|
|
279
278
|
from ...types import A2ATaskStatus
|
|
279
|
+
|
|
280
280
|
updated_status = A2ATaskStatus(
|
|
281
281
|
state=state,
|
|
282
282
|
message=status_message or task.status.message,
|
|
283
|
-
timestamp=timestamp or datetime.now().isoformat()
|
|
283
|
+
timestamp=timestamp or datetime.now().isoformat(),
|
|
284
284
|
)
|
|
285
285
|
|
|
286
|
-
updated_task = task.model_copy(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
})
|
|
286
|
+
updated_task = task.model_copy(
|
|
287
|
+
update={"status": updated_status, "history": updated_history}
|
|
288
|
+
)
|
|
290
289
|
|
|
291
290
|
# Use update_task for the actual update
|
|
292
291
|
return await self.update_task(updated_task)
|
|
293
292
|
|
|
294
293
|
except Exception as error:
|
|
295
294
|
return create_a2a_failure(
|
|
296
|
-
create_a2a_task_storage_error(
|
|
295
|
+
create_a2a_task_storage_error("update-status", "redis", task_id, error)
|
|
297
296
|
)
|
|
298
297
|
|
|
299
298
|
async def find_tasks(self, query: A2ATaskQuery) -> A2AResult[List[A2ATask]]:
|
|
300
299
|
"""Search tasks by query parameters"""
|
|
301
300
|
try:
|
|
302
301
|
task_ids: List[str] = []
|
|
303
|
-
|
|
302
|
+
|
|
304
303
|
# Determine which sets to use for filtering
|
|
305
304
|
keys_to_intersect = []
|
|
306
305
|
if query.context_id:
|
|
@@ -318,7 +317,7 @@ async def create_a2a_redis_task_provider(
|
|
|
318
317
|
# Get all task keys if no context or state is provided
|
|
319
318
|
pattern = f"{key_prefix}task:*"
|
|
320
319
|
keys = await redis_client.keys(pattern)
|
|
321
|
-
task_ids = [key.replace(f"{key_prefix}task:",
|
|
320
|
+
task_ids = [key.replace(f"{key_prefix}task:", "") for key in keys]
|
|
322
321
|
|
|
323
322
|
# Convert bytes to strings if needed
|
|
324
323
|
task_ids = [tid.decode() if isinstance(tid, bytes) else tid for tid in task_ids]
|
|
@@ -345,27 +344,34 @@ async def create_a2a_redis_task_provider(
|
|
|
345
344
|
if hash_data:
|
|
346
345
|
# Convert bytes to strings if needed
|
|
347
346
|
if isinstance(hash_data, dict):
|
|
348
|
-
hash_data = {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
347
|
+
hash_data = {
|
|
348
|
+
k.decode() if isinstance(k, bytes) else k: v.decode()
|
|
349
|
+
if isinstance(v, bytes)
|
|
350
|
+
else v
|
|
351
|
+
for k, v in hash_data.items()
|
|
352
|
+
}
|
|
353
|
+
|
|
352
354
|
# Try to get timestamp from metadata first, then created_at
|
|
353
355
|
task_timestamp = None
|
|
354
|
-
if hash_data.get(
|
|
356
|
+
if hash_data.get("metadata"):
|
|
355
357
|
try:
|
|
356
|
-
metadata = json.loads(hash_data[
|
|
357
|
-
if metadata.get(
|
|
358
|
-
task_timestamp = datetime.fromisoformat(
|
|
358
|
+
metadata = json.loads(hash_data["metadata"])
|
|
359
|
+
if metadata.get("created_at"):
|
|
360
|
+
task_timestamp = datetime.fromisoformat(
|
|
361
|
+
metadata["created_at"].replace("Z", "+00:00")
|
|
362
|
+
)
|
|
359
363
|
except:
|
|
360
364
|
pass
|
|
361
|
-
|
|
365
|
+
|
|
362
366
|
# Fall back to createdAt field
|
|
363
|
-
if not task_timestamp and hash_data.get(
|
|
367
|
+
if not task_timestamp and hash_data.get("createdAt"):
|
|
364
368
|
try:
|
|
365
|
-
task_timestamp = datetime.fromisoformat(
|
|
369
|
+
task_timestamp = datetime.fromisoformat(
|
|
370
|
+
hash_data["createdAt"].replace("Z", "+00:00")
|
|
371
|
+
)
|
|
366
372
|
except:
|
|
367
373
|
pass
|
|
368
|
-
|
|
374
|
+
|
|
369
375
|
# Apply time filters
|
|
370
376
|
if task_timestamp:
|
|
371
377
|
if query.since and task_timestamp < query.since:
|
|
@@ -377,26 +383,23 @@ async def create_a2a_redis_task_provider(
|
|
|
377
383
|
|
|
378
384
|
# Sort by timestamp (newest first)
|
|
379
385
|
results.sort(
|
|
380
|
-
key=lambda t: t.status.timestamp or "1970-01-01T00:00:00Z",
|
|
381
|
-
reverse=True
|
|
386
|
+
key=lambda t: t.status.timestamp or "1970-01-01T00:00:00Z", reverse=True
|
|
382
387
|
)
|
|
383
388
|
|
|
384
389
|
# Apply pagination
|
|
385
390
|
offset = query.offset or 0
|
|
386
391
|
limit = query.limit or len(results)
|
|
387
|
-
paginated_results = results[offset:offset + limit]
|
|
392
|
+
paginated_results = results[offset : offset + limit]
|
|
388
393
|
|
|
389
394
|
return create_a2a_success(paginated_results)
|
|
390
395
|
|
|
391
396
|
except Exception as error:
|
|
392
397
|
return create_a2a_failure(
|
|
393
|
-
create_a2a_task_storage_error(
|
|
398
|
+
create_a2a_task_storage_error("find", "redis", None, error)
|
|
394
399
|
)
|
|
395
400
|
|
|
396
401
|
async def get_tasks_by_context(
|
|
397
|
-
self,
|
|
398
|
-
context_id: str,
|
|
399
|
-
limit: Optional[int] = None
|
|
402
|
+
self, context_id: str, limit: Optional[int] = None
|
|
400
403
|
) -> A2AResult[List[A2ATask]]:
|
|
401
404
|
"""Get tasks by context ID"""
|
|
402
405
|
return await self.find_tasks(A2ATaskQuery(context_id=context_id, limit=limit))
|
|
@@ -413,12 +416,15 @@ async def create_a2a_redis_task_provider(
|
|
|
413
416
|
|
|
414
417
|
# Convert bytes to strings if needed
|
|
415
418
|
if isinstance(hash_data, dict):
|
|
416
|
-
hash_data = {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
+
hash_data = {
|
|
420
|
+
k.decode() if isinstance(k, bytes) else k: v.decode()
|
|
421
|
+
if isinstance(v, bytes)
|
|
422
|
+
else v
|
|
423
|
+
for k, v in hash_data.items()
|
|
424
|
+
}
|
|
419
425
|
|
|
420
|
-
context_id = hash_data.get(
|
|
421
|
-
state = hash_data.get(
|
|
426
|
+
context_id = hash_data.get("contextId")
|
|
427
|
+
state = hash_data.get("state")
|
|
422
428
|
|
|
423
429
|
async with redis_client.pipeline() as pipe:
|
|
424
430
|
# Delete task
|
|
@@ -434,9 +440,9 @@ async def create_a2a_redis_task_provider(
|
|
|
434
440
|
await pipe.srem(state_index_key, task_id)
|
|
435
441
|
|
|
436
442
|
# Update stats
|
|
437
|
-
await pipe.hincrby(get_stats_key(),
|
|
443
|
+
await pipe.hincrby(get_stats_key(), "totalTasks", -1)
|
|
438
444
|
if state:
|
|
439
|
-
await pipe.hincrby(get_stats_key(), f
|
|
445
|
+
await pipe.hincrby(get_stats_key(), f"state:{state}", -1)
|
|
440
446
|
|
|
441
447
|
await pipe.execute()
|
|
442
448
|
|
|
@@ -444,7 +450,7 @@ async def create_a2a_redis_task_provider(
|
|
|
444
450
|
|
|
445
451
|
except Exception as error:
|
|
446
452
|
return create_a2a_failure(
|
|
447
|
-
create_a2a_task_storage_error(
|
|
453
|
+
create_a2a_task_storage_error("delete", "redis", task_id, error)
|
|
448
454
|
)
|
|
449
455
|
|
|
450
456
|
async def delete_tasks_by_context(self, context_id: str) -> A2AResult[int]:
|
|
@@ -463,9 +469,13 @@ async def create_a2a_redis_task_provider(
|
|
|
463
469
|
deleted_count = 0
|
|
464
470
|
for task_id in task_ids:
|
|
465
471
|
delete_result = await self.delete_task(task_id)
|
|
466
|
-
if
|
|
472
|
+
if (
|
|
473
|
+
hasattr(delete_result, "data")
|
|
474
|
+
and isinstance(delete_result.data, bool)
|
|
475
|
+
and delete_result.data
|
|
476
|
+
):
|
|
467
477
|
deleted_count += 1
|
|
468
|
-
elif hasattr(delete_result,
|
|
478
|
+
elif hasattr(delete_result, "error"):
|
|
469
479
|
# Log error but continue with other deletions
|
|
470
480
|
continue
|
|
471
481
|
|
|
@@ -473,7 +483,7 @@ async def create_a2a_redis_task_provider(
|
|
|
473
483
|
|
|
474
484
|
except Exception as error:
|
|
475
485
|
return create_a2a_failure(
|
|
476
|
-
create_a2a_task_storage_error(
|
|
486
|
+
create_a2a_task_storage_error("delete-by-context", "redis", None, error)
|
|
477
487
|
)
|
|
478
488
|
|
|
479
489
|
async def cleanup_expired_tasks(self) -> A2AResult[int]:
|
|
@@ -485,12 +495,11 @@ async def create_a2a_redis_task_provider(
|
|
|
485
495
|
|
|
486
496
|
except Exception as error:
|
|
487
497
|
return create_a2a_failure(
|
|
488
|
-
create_a2a_task_storage_error(
|
|
498
|
+
create_a2a_task_storage_error("cleanup", "redis", None, error)
|
|
489
499
|
)
|
|
490
500
|
|
|
491
501
|
async def get_task_stats(
|
|
492
|
-
self,
|
|
493
|
-
context_id: Optional[str] = None
|
|
502
|
+
self, context_id: Optional[str] = None
|
|
494
503
|
) -> A2AResult[Dict[str, Any]]:
|
|
495
504
|
"""Get task statistics"""
|
|
496
505
|
try:
|
|
@@ -507,10 +516,10 @@ async def create_a2a_redis_task_provider(
|
|
|
507
516
|
tasks_by_state[task.status.state.value] += 1
|
|
508
517
|
|
|
509
518
|
stats = {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
519
|
+
"total_tasks": len(tasks),
|
|
520
|
+
"tasks_by_state": tasks_by_state,
|
|
521
|
+
"oldest_task": None,
|
|
522
|
+
"newest_task": None,
|
|
514
523
|
}
|
|
515
524
|
# Also add individual state counts for backwards compatibility
|
|
516
525
|
for state in TaskState:
|
|
@@ -523,25 +532,28 @@ async def create_a2a_redis_task_provider(
|
|
|
523
532
|
stats = await redis_client.hgetall(stats_key)
|
|
524
533
|
|
|
525
534
|
if isinstance(stats, dict):
|
|
526
|
-
stats = {
|
|
527
|
-
|
|
528
|
-
|
|
535
|
+
stats = {
|
|
536
|
+
k.decode() if isinstance(k, bytes) else k: v.decode()
|
|
537
|
+
if isinstance(v, bytes)
|
|
538
|
+
else v
|
|
539
|
+
for k, v in stats.items()
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
total_tasks = int(stats.get("totalTasks", 0))
|
|
529
543
|
|
|
530
|
-
total_tasks = int(stats.get('totalTasks', 0))
|
|
531
|
-
|
|
532
544
|
# Build tasks_by_state dict
|
|
533
545
|
tasks_by_state = {}
|
|
534
546
|
for state in TaskState:
|
|
535
|
-
tasks_by_state[state.value] = int(stats.get(f
|
|
536
|
-
|
|
547
|
+
tasks_by_state[state.value] = int(stats.get(f"state:{state.value}", 0))
|
|
548
|
+
|
|
537
549
|
# Build stats dict with individual state counts
|
|
538
550
|
result_stats = {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
551
|
+
"total_tasks": total_tasks,
|
|
552
|
+
"tasks_by_state": tasks_by_state,
|
|
553
|
+
"oldest_task": None,
|
|
554
|
+
"newest_task": None,
|
|
543
555
|
}
|
|
544
|
-
|
|
556
|
+
|
|
545
557
|
# Also add individual state counts for backwards compatibility
|
|
546
558
|
for state in TaskState:
|
|
547
559
|
result_stats[state.value] = tasks_by_state[state.value]
|
|
@@ -550,7 +562,7 @@ async def create_a2a_redis_task_provider(
|
|
|
550
562
|
|
|
551
563
|
except Exception as error:
|
|
552
564
|
return create_a2a_failure(
|
|
553
|
-
create_a2a_task_storage_error(
|
|
565
|
+
create_a2a_task_storage_error("stats", "redis", None, error)
|
|
554
566
|
)
|
|
555
567
|
|
|
556
568
|
async def health_check(self) -> A2AResult[Dict[str, Any]]:
|
|
@@ -562,17 +574,12 @@ async def create_a2a_redis_task_provider(
|
|
|
562
574
|
await redis_client.ping()
|
|
563
575
|
|
|
564
576
|
latency_ms = (datetime.now() - start_time).total_seconds() * 1000
|
|
565
|
-
return create_a2a_success(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
'latency_ms': latency_ms
|
|
569
|
-
})
|
|
577
|
+
return create_a2a_success(
|
|
578
|
+
{"healthy": True, "provider": "redis", "latency_ms": latency_ms}
|
|
579
|
+
)
|
|
570
580
|
|
|
571
581
|
except Exception as error:
|
|
572
|
-
return create_a2a_success({
|
|
573
|
-
'healthy': False,
|
|
574
|
-
'error': str(error)
|
|
575
|
-
})
|
|
582
|
+
return create_a2a_success({"healthy": False, "error": str(error)})
|
|
576
583
|
|
|
577
584
|
async def close(self) -> A2AResult[None]:
|
|
578
585
|
"""Close/cleanup the provider"""
|
|
@@ -583,12 +590,12 @@ async def create_a2a_redis_task_provider(
|
|
|
583
590
|
|
|
584
591
|
except Exception as error:
|
|
585
592
|
return create_a2a_failure(
|
|
586
|
-
create_a2a_task_storage_error(
|
|
593
|
+
create_a2a_task_storage_error("close", "redis", None, error)
|
|
587
594
|
)
|
|
588
595
|
|
|
589
596
|
return create_a2a_success(RedisA2ATaskProvider())
|
|
590
597
|
|
|
591
598
|
except Exception as error:
|
|
592
599
|
return create_a2a_failure(
|
|
593
|
-
create_a2a_task_storage_error(
|
|
600
|
+
create_a2a_task_storage_error("create-redis-provider", "redis", None, error)
|
|
594
601
|
)
|