jaf-py 2.5.9__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.
Files changed (92) hide show
  1. jaf/__init__.py +154 -57
  2. jaf/a2a/__init__.py +42 -21
  3. jaf/a2a/agent.py +79 -126
  4. jaf/a2a/agent_card.py +87 -78
  5. jaf/a2a/client.py +30 -66
  6. jaf/a2a/examples/client_example.py +12 -12
  7. jaf/a2a/examples/integration_example.py +38 -47
  8. jaf/a2a/examples/server_example.py +56 -53
  9. jaf/a2a/memory/__init__.py +0 -4
  10. jaf/a2a/memory/cleanup.py +28 -21
  11. jaf/a2a/memory/factory.py +155 -133
  12. jaf/a2a/memory/providers/composite.py +21 -26
  13. jaf/a2a/memory/providers/in_memory.py +89 -83
  14. jaf/a2a/memory/providers/postgres.py +117 -115
  15. jaf/a2a/memory/providers/redis.py +128 -121
  16. jaf/a2a/memory/serialization.py +77 -87
  17. jaf/a2a/memory/tests/run_comprehensive_tests.py +112 -83
  18. jaf/a2a/memory/tests/test_cleanup.py +211 -94
  19. jaf/a2a/memory/tests/test_serialization.py +73 -68
  20. jaf/a2a/memory/tests/test_stress_concurrency.py +186 -133
  21. jaf/a2a/memory/tests/test_task_lifecycle.py +138 -120
  22. jaf/a2a/memory/types.py +91 -53
  23. jaf/a2a/protocol.py +95 -125
  24. jaf/a2a/server.py +90 -118
  25. jaf/a2a/standalone_client.py +30 -43
  26. jaf/a2a/tests/__init__.py +16 -33
  27. jaf/a2a/tests/run_tests.py +17 -53
  28. jaf/a2a/tests/test_agent.py +40 -140
  29. jaf/a2a/tests/test_client.py +54 -117
  30. jaf/a2a/tests/test_integration.py +28 -82
  31. jaf/a2a/tests/test_protocol.py +54 -139
  32. jaf/a2a/tests/test_types.py +50 -136
  33. jaf/a2a/types.py +58 -34
  34. jaf/cli.py +21 -41
  35. jaf/core/__init__.py +7 -1
  36. jaf/core/agent_tool.py +93 -72
  37. jaf/core/analytics.py +257 -207
  38. jaf/core/checkpoint.py +223 -0
  39. jaf/core/composition.py +249 -235
  40. jaf/core/engine.py +817 -519
  41. jaf/core/errors.py +55 -42
  42. jaf/core/guardrails.py +276 -202
  43. jaf/core/handoff.py +47 -31
  44. jaf/core/parallel_agents.py +69 -75
  45. jaf/core/performance.py +75 -73
  46. jaf/core/proxy.py +43 -44
  47. jaf/core/proxy_helpers.py +24 -27
  48. jaf/core/regeneration.py +220 -129
  49. jaf/core/state.py +68 -66
  50. jaf/core/streaming.py +115 -108
  51. jaf/core/tool_results.py +111 -101
  52. jaf/core/tools.py +114 -116
  53. jaf/core/tracing.py +269 -210
  54. jaf/core/types.py +371 -151
  55. jaf/core/workflows.py +209 -168
  56. jaf/exceptions.py +46 -38
  57. jaf/memory/__init__.py +1 -6
  58. jaf/memory/approval_storage.py +54 -77
  59. jaf/memory/factory.py +4 -4
  60. jaf/memory/providers/in_memory.py +216 -180
  61. jaf/memory/providers/postgres.py +216 -146
  62. jaf/memory/providers/redis.py +173 -116
  63. jaf/memory/types.py +70 -51
  64. jaf/memory/utils.py +36 -34
  65. jaf/plugins/__init__.py +12 -12
  66. jaf/plugins/base.py +105 -96
  67. jaf/policies/__init__.py +0 -1
  68. jaf/policies/handoff.py +37 -46
  69. jaf/policies/validation.py +76 -52
  70. jaf/providers/__init__.py +6 -3
  71. jaf/providers/mcp.py +97 -51
  72. jaf/providers/model.py +361 -280
  73. jaf/server/__init__.py +1 -1
  74. jaf/server/main.py +7 -11
  75. jaf/server/server.py +514 -359
  76. jaf/server/types.py +208 -52
  77. jaf/utils/__init__.py +17 -18
  78. jaf/utils/attachments.py +111 -116
  79. jaf/utils/document_processor.py +175 -174
  80. jaf/visualization/__init__.py +1 -1
  81. jaf/visualization/example.py +111 -110
  82. jaf/visualization/functional_core.py +46 -71
  83. jaf/visualization/graphviz.py +154 -189
  84. jaf/visualization/imperative_shell.py +7 -16
  85. jaf/visualization/types.py +8 -4
  86. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/METADATA +2 -2
  87. jaf_py-2.5.11.dist-info/RECORD +97 -0
  88. jaf_py-2.5.9.dist-info/RECORD +0 -96
  89. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/WHEEL +0 -0
  90. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
  91. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
  92. {jaf_py-2.5.9.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
@@ -30,7 +30,7 @@ from ..types import (
30
30
 
31
31
  # SQL queries for A2A task operations
32
32
  SQL_QUERIES = {
33
- 'CREATE_TABLE': '''
33
+ "CREATE_TABLE": """
34
34
  CREATE TABLE IF NOT EXISTS {table_name} (
35
35
  task_id VARCHAR(255) PRIMARY KEY,
36
36
  context_id VARCHAR(255) NOT NULL,
@@ -47,150 +47,158 @@ SQL_QUERIES = {
47
47
  CREATE INDEX IF NOT EXISTS idx_{table_name}_state ON {table_name} (state);
48
48
  CREATE INDEX IF NOT EXISTS idx_{table_name}_created_at ON {table_name} (created_at);
49
49
  CREATE INDEX IF NOT EXISTS idx_{table_name}_expires_at ON {table_name} (expires_at) WHERE expires_at IS NOT NULL;
50
- ''',
51
- 'INSERT_TASK': '''
50
+ """,
51
+ "INSERT_TASK": """
52
52
  INSERT INTO {table_name} (
53
53
  task_id, context_id, state, task_data, status_message,
54
54
  created_at, updated_at, expires_at, metadata
55
55
  ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
56
- ''',
57
- 'SELECT_TASK': '''
56
+ """,
57
+ "SELECT_TASK": """
58
58
  SELECT task_id, context_id, state, task_data, status_message,
59
59
  created_at, updated_at, expires_at, metadata
60
60
  FROM {table_name}
61
61
  WHERE task_id = $1
62
62
  AND (expires_at IS NULL OR expires_at > NOW())
63
- ''',
64
- 'UPDATE_TASK': '''
63
+ """,
64
+ "UPDATE_TASK": """
65
65
  UPDATE {table_name}
66
66
  SET state = $2, task_data = $3, status_message = $4,
67
67
  updated_at = $5, metadata = $6
68
68
  WHERE task_id = $1
69
69
  AND (expires_at IS NULL OR expires_at > NOW())
70
- ''',
71
- 'DELETE_TASK': '''
70
+ """,
71
+ "DELETE_TASK": """
72
72
  DELETE FROM {table_name}
73
73
  WHERE task_id = $1
74
- ''',
75
- 'DELETE_TASKS_BY_CONTEXT': '''
74
+ """,
75
+ "DELETE_TASKS_BY_CONTEXT": """
76
76
  DELETE FROM {table_name}
77
77
  WHERE context_id = $1
78
- ''',
79
- 'CLEANUP_EXPIRED': '''
78
+ """,
79
+ "CLEANUP_EXPIRED": """
80
80
  DELETE FROM {table_name}
81
81
  WHERE expires_at IS NOT NULL AND expires_at <= NOW()
82
- ''',
83
- 'COUNT_TASKS': '''
82
+ """,
83
+ "COUNT_TASKS": """
84
84
  SELECT COUNT(*) as total
85
85
  FROM {table_name}
86
86
  WHERE (expires_at IS NULL OR expires_at > NOW())
87
- ''',
88
- 'STATS_BY_STATE': '''
87
+ """,
88
+ "STATS_BY_STATE": """
89
89
  SELECT state, COUNT(*) as count
90
90
  FROM {table_name}
91
91
  WHERE (expires_at IS NULL OR expires_at > NOW())
92
92
  AND ($1::text IS NULL OR context_id = $1)
93
93
  GROUP BY state
94
- ''',
95
- 'DATE_RANGE': '''
94
+ """,
95
+ "DATE_RANGE": """
96
96
  SELECT MIN(created_at) as oldest, MAX(created_at) as newest
97
97
  FROM {table_name}
98
98
  WHERE (expires_at IS NULL OR expires_at > NOW())
99
99
  AND ($1::text IS NULL OR context_id = $1)
100
- '''
100
+ """,
101
101
  }
102
102
 
103
+
103
104
  async def create_a2a_postgres_task_provider(
104
- config: A2APostgresTaskConfig,
105
- postgres_client: Any
105
+ config: A2APostgresTaskConfig, postgres_client: Any
106
106
  ) -> A2AResult[A2ATaskProvider]:
107
107
  """
108
108
  Create a PostgreSQL-based A2A task provider
109
-
109
+
110
110
  Args:
111
111
  config: Configuration for the PostgreSQL provider
112
112
  postgres_client: PostgreSQL client instance (asyncpg connection/pool)
113
-
113
+
114
114
  Returns:
115
115
  A2AResult containing the task provider or an error
116
116
  """
117
117
  try:
118
- table_name = config.table_name or 'a2a_tasks'
118
+ table_name = config.table_name or "a2a_tasks"
119
119
 
120
120
  # Initialize database schema
121
- create_table_sql = SQL_QUERIES['CREATE_TABLE'].format(table_name=table_name)
121
+ create_table_sql = SQL_QUERIES["CREATE_TABLE"].format(table_name=table_name)
122
122
  await postgres_client.execute(create_table_sql)
123
123
 
124
124
  def row_to_serialized_task(row: Any) -> A2ATaskSerialized:
125
125
  """Convert database row to serialized task"""
126
126
  return A2ATaskSerialized(
127
- task_id=row['task_id'],
128
- context_id=row['context_id'],
129
- state=row['state'],
130
- task_data=row['task_data'] if isinstance(row['task_data'], str) else json.dumps(row['task_data']),
131
- status_message=row['status_message'] if isinstance(row['status_message'], str) else json.dumps(row['status_message']) if row['status_message'] else None,
132
- created_at=row['created_at'].isoformat(),
133
- updated_at=row['updated_at'].isoformat(),
134
- metadata=row['metadata'] if isinstance(row['metadata'], str) else json.dumps(row['metadata']) if row['metadata'] else None
127
+ task_id=row["task_id"],
128
+ context_id=row["context_id"],
129
+ state=row["state"],
130
+ task_data=row["task_data"]
131
+ if isinstance(row["task_data"], str)
132
+ else json.dumps(row["task_data"]),
133
+ status_message=row["status_message"]
134
+ if isinstance(row["status_message"], str)
135
+ else json.dumps(row["status_message"])
136
+ if row["status_message"]
137
+ else None,
138
+ created_at=row["created_at"].isoformat(),
139
+ updated_at=row["updated_at"].isoformat(),
140
+ metadata=row["metadata"]
141
+ if isinstance(row["metadata"], str)
142
+ else json.dumps(row["metadata"])
143
+ if row["metadata"]
144
+ else None,
135
145
  )
136
146
 
137
147
  def build_where_clause(query: A2ATaskQuery) -> tuple[str, list]:
138
148
  """Build WHERE clause for queries"""
139
- conditions = ['(expires_at IS NULL OR expires_at > NOW())']
149
+ conditions = ["(expires_at IS NULL OR expires_at > NOW())"]
140
150
  params = []
141
151
  param_index = 1
142
152
 
143
153
  if query.task_id:
144
- conditions.append(f'task_id = ${param_index}')
154
+ conditions.append(f"task_id = ${param_index}")
145
155
  params.append(query.task_id)
146
156
  param_index += 1
147
157
 
148
158
  if query.context_id:
149
- conditions.append(f'context_id = ${param_index}')
159
+ conditions.append(f"context_id = ${param_index}")
150
160
  params.append(query.context_id)
151
161
  param_index += 1
152
162
 
153
163
  if query.state:
154
- conditions.append(f'state = ${param_index}')
164
+ conditions.append(f"state = ${param_index}")
155
165
  params.append(query.state.value)
156
166
  param_index += 1
157
167
 
158
168
  if query.since:
159
169
  # Use metadata created_at if available, otherwise use database created_at
160
- conditions.append(f'''(
170
+ conditions.append(f"""(
161
171
  CASE
162
172
  WHEN metadata ? 'created_at' THEN
163
173
  (metadata->>'created_at')::timestamp with time zone >= ${param_index}
164
174
  ELSE
165
175
  created_at >= ${param_index}
166
176
  END
167
- )''')
177
+ )""")
168
178
  params.append(query.since)
169
179
  param_index += 1
170
180
 
171
181
  if query.until:
172
182
  # Use metadata created_at if available, otherwise use database created_at
173
- conditions.append(f'''(
183
+ conditions.append(f"""(
174
184
  CASE
175
185
  WHEN metadata ? 'created_at' THEN
176
186
  (metadata->>'created_at')::timestamp with time zone <= ${param_index}
177
187
  ELSE
178
188
  created_at <= ${param_index}
179
189
  END
180
- )''')
190
+ )""")
181
191
  params.append(query.until)
182
192
  param_index += 1
183
193
 
184
- where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ''
194
+ where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""
185
195
  return where_clause, params
186
196
 
187
197
  class PostgresA2ATaskProvider:
188
198
  """PostgreSQL implementation of A2ATaskProvider"""
189
199
 
190
200
  async def store_task(
191
- self,
192
- task: A2ATask,
193
- metadata: Optional[Dict[str, Any]] = None
201
+ self, task: A2ATask, metadata: Optional[Dict[str, Any]] = None
194
202
  ) -> A2AResult[None]:
195
203
  """Store a new A2A task in PostgreSQL"""
196
204
  try:
@@ -205,7 +213,7 @@ async def create_a2a_postgres_task_provider(
205
213
  return serialize_result
206
214
 
207
215
  serialized = serialize_result.data
208
- query = SQL_QUERIES['INSERT_TASK'].format(table_name=table_name)
216
+ query = SQL_QUERIES["INSERT_TASK"].format(table_name=table_name)
209
217
 
210
218
  await postgres_client.execute(
211
219
  query,
@@ -216,21 +224,21 @@ async def create_a2a_postgres_task_provider(
216
224
  serialized.status_message,
217
225
  datetime.fromisoformat(serialized.created_at),
218
226
  datetime.fromisoformat(serialized.updated_at),
219
- metadata.get('expires_at') if metadata else None,
220
- json.dumps(metadata) if metadata else None
227
+ metadata.get("expires_at") if metadata else None,
228
+ json.dumps(metadata) if metadata else None,
221
229
  )
222
230
 
223
231
  return create_a2a_success(None)
224
232
 
225
233
  except Exception as error:
226
234
  return create_a2a_failure(
227
- create_a2a_task_storage_error('store', 'postgres', task.id, error)
235
+ create_a2a_task_storage_error("store", "postgres", task.id, error)
228
236
  )
229
237
 
230
238
  async def get_task(self, task_id: str) -> A2AResult[Optional[A2ATask]]:
231
239
  """Retrieve a task by ID from PostgreSQL"""
232
240
  try:
233
- query = SQL_QUERIES['SELECT_TASK'].format(table_name=table_name)
241
+ query = SQL_QUERIES["SELECT_TASK"].format(table_name=table_name)
234
242
  row = await postgres_client.fetchrow(query, task_id)
235
243
 
236
244
  if not row:
@@ -246,13 +254,11 @@ async def create_a2a_postgres_task_provider(
246
254
 
247
255
  except Exception as error:
248
256
  return create_a2a_failure(
249
- create_a2a_task_storage_error('get', 'postgres', task_id, error)
257
+ create_a2a_task_storage_error("get", "postgres", task_id, error)
250
258
  )
251
259
 
252
260
  async def update_task(
253
- self,
254
- task: A2ATask,
255
- metadata: Optional[Dict[str, Any]] = None
261
+ self, task: A2ATask, metadata: Optional[Dict[str, Any]] = None
256
262
  ) -> A2AResult[None]:
257
263
  """Update an existing task in PostgreSQL"""
258
264
  try:
@@ -263,7 +269,7 @@ async def create_a2a_postgres_task_provider(
263
269
 
264
270
  if not existing_result.data:
265
271
  return create_a2a_failure(
266
- create_a2a_task_not_found_error(task.id, 'postgres')
272
+ create_a2a_task_not_found_error(task.id, "postgres")
267
273
  )
268
274
 
269
275
  # Validate and sanitize task
@@ -272,9 +278,13 @@ async def create_a2a_postgres_task_provider(
272
278
  return sanitize_result
273
279
 
274
280
  # Get existing metadata
275
- existing_query = SQL_QUERIES['SELECT_TASK'].format(table_name=table_name)
281
+ existing_query = SQL_QUERIES["SELECT_TASK"].format(table_name=table_name)
276
282
  existing_data = await postgres_client.fetchrow(existing_query, task.id)
277
- existing_metadata = existing_data['metadata'] if existing_data and existing_data['metadata'] else {}
283
+ existing_metadata = (
284
+ existing_data["metadata"]
285
+ if existing_data and existing_data["metadata"]
286
+ else {}
287
+ )
278
288
  if isinstance(existing_metadata, str):
279
289
  existing_metadata = json.loads(existing_metadata)
280
290
 
@@ -286,7 +296,7 @@ async def create_a2a_postgres_task_provider(
286
296
  return serialize_result
287
297
 
288
298
  serialized = serialize_result.data
289
- query = SQL_QUERIES['UPDATE_TASK'].format(table_name=table_name)
299
+ query = SQL_QUERIES["UPDATE_TASK"].format(table_name=table_name)
290
300
 
291
301
  result = await postgres_client.execute(
292
302
  query,
@@ -295,20 +305,20 @@ async def create_a2a_postgres_task_provider(
295
305
  serialized.task_data,
296
306
  serialized.status_message,
297
307
  datetime.fromisoformat(serialized.updated_at),
298
- json.dumps(merged_metadata)
308
+ json.dumps(merged_metadata),
299
309
  )
300
310
 
301
311
  # Check if any rows were affected
302
- if result == 'UPDATE 0':
312
+ if result == "UPDATE 0":
303
313
  return create_a2a_failure(
304
- create_a2a_task_not_found_error(task.id, 'postgres')
314
+ create_a2a_task_not_found_error(task.id, "postgres")
305
315
  )
306
316
 
307
317
  return create_a2a_success(None)
308
318
 
309
319
  except Exception as error:
310
320
  return create_a2a_failure(
311
- create_a2a_task_storage_error('update', 'postgres', task.id, error)
321
+ create_a2a_task_storage_error("update", "postgres", task.id, error)
312
322
  )
313
323
 
314
324
  async def update_task_status(
@@ -316,7 +326,7 @@ async def create_a2a_postgres_task_provider(
316
326
  task_id: str,
317
327
  state: TaskState,
318
328
  status_message: Optional[Any] = None,
319
- timestamp: Optional[str] = None
329
+ timestamp: Optional[str] = None,
320
330
  ) -> A2AResult[None]:
321
331
  """Update task status only"""
322
332
  try:
@@ -327,7 +337,7 @@ async def create_a2a_postgres_task_provider(
327
337
 
328
338
  if not existing_result.data:
329
339
  return create_a2a_failure(
330
- create_a2a_task_not_found_error(task_id, 'postgres')
340
+ create_a2a_task_not_found_error(task_id, "postgres")
331
341
  )
332
342
 
333
343
  task = existing_result.data
@@ -339,22 +349,22 @@ async def create_a2a_postgres_task_provider(
339
349
 
340
350
  # Update task status
341
351
  from ...types import A2ATaskStatus
352
+
342
353
  updated_status = A2ATaskStatus(
343
354
  state=state,
344
355
  message=status_message or task.status.message,
345
- timestamp=timestamp or datetime.now().isoformat()
356
+ timestamp=timestamp or datetime.now().isoformat(),
346
357
  )
347
358
 
348
- updated_task = task.model_copy(update={
349
- 'status': updated_status,
350
- 'history': updated_history
351
- })
359
+ updated_task = task.model_copy(
360
+ update={"status": updated_status, "history": updated_history}
361
+ )
352
362
 
353
363
  return await self.update_task(updated_task)
354
364
 
355
365
  except Exception as error:
356
366
  return create_a2a_failure(
357
- create_a2a_task_storage_error('update-status', 'postgres', task_id, error)
367
+ create_a2a_task_storage_error("update-status", "postgres", task_id, error)
358
368
  )
359
369
 
360
370
  async def find_tasks(self, query: A2ATaskQuery) -> A2AResult[List[A2ATask]]:
@@ -362,19 +372,19 @@ async def create_a2a_postgres_task_provider(
362
372
  try:
363
373
  where_clause, params = build_where_clause(query)
364
374
 
365
- sql = f'''
375
+ sql = f"""
366
376
  SELECT task_id, context_id, state, task_data, status_message,
367
377
  created_at, updated_at, expires_at, metadata
368
378
  FROM {table_name}
369
379
  {where_clause}
370
380
  ORDER BY created_at DESC
371
- '''
381
+ """
372
382
 
373
383
  # Add pagination
374
384
  if query.limit:
375
- sql += f' LIMIT {query.limit}'
385
+ sql += f" LIMIT {query.limit}"
376
386
  if query.offset:
377
- sql += f' OFFSET {query.offset}'
387
+ sql += f" OFFSET {query.offset}"
378
388
 
379
389
  rows = await postgres_client.fetch(sql, *params)
380
390
  tasks: List[A2ATask] = []
@@ -390,13 +400,11 @@ async def create_a2a_postgres_task_provider(
390
400
 
391
401
  except Exception as error:
392
402
  return create_a2a_failure(
393
- create_a2a_task_storage_error('find', 'postgres', None, error)
403
+ create_a2a_task_storage_error("find", "postgres", None, error)
394
404
  )
395
405
 
396
406
  async def get_tasks_by_context(
397
- self,
398
- context_id: str,
399
- limit: Optional[int] = None
407
+ self, context_id: str, limit: Optional[int] = None
400
408
  ) -> A2AResult[List[A2ATask]]:
401
409
  """Get tasks by context ID"""
402
410
  return await self.find_tasks(A2ATaskQuery(context_id=context_id, limit=limit))
@@ -404,87 +412,86 @@ async def create_a2a_postgres_task_provider(
404
412
  async def delete_task(self, task_id: str) -> A2AResult[bool]:
405
413
  """Delete a task and return True if it existed"""
406
414
  try:
407
- query = SQL_QUERIES['DELETE_TASK'].format(table_name=table_name)
415
+ query = SQL_QUERIES["DELETE_TASK"].format(table_name=table_name)
408
416
  result = await postgres_client.execute(query, task_id)
409
417
 
410
418
  # Check if any rows were affected
411
- rows_affected = int(result.split()[-1]) if 'DELETE' in result else 0
419
+ rows_affected = int(result.split()[-1]) if "DELETE" in result else 0
412
420
  return create_a2a_success(rows_affected > 0)
413
421
 
414
422
  except Exception as error:
415
423
  return create_a2a_failure(
416
- create_a2a_task_storage_error('delete', 'postgres', task_id, error)
424
+ create_a2a_task_storage_error("delete", "postgres", task_id, error)
417
425
  )
418
426
 
419
427
  async def delete_tasks_by_context(self, context_id: str) -> A2AResult[int]:
420
428
  """Delete tasks by context ID and return count deleted"""
421
429
  try:
422
- query = SQL_QUERIES['DELETE_TASKS_BY_CONTEXT'].format(table_name=table_name)
430
+ query = SQL_QUERIES["DELETE_TASKS_BY_CONTEXT"].format(table_name=table_name)
423
431
  result = await postgres_client.execute(query, context_id)
424
432
 
425
433
  # Extract count from result
426
- rows_affected = int(result.split()[-1]) if 'DELETE' in result else 0
434
+ rows_affected = int(result.split()[-1]) if "DELETE" in result else 0
427
435
  return create_a2a_success(rows_affected)
428
436
 
429
437
  except Exception as error:
430
438
  return create_a2a_failure(
431
- create_a2a_task_storage_error('delete-by-context', 'postgres', None, error)
439
+ create_a2a_task_storage_error("delete-by-context", "postgres", None, error)
432
440
  )
433
441
 
434
442
  async def cleanup_expired_tasks(self) -> A2AResult[int]:
435
443
  """Clean up expired tasks and return count deleted"""
436
444
  try:
437
- query = SQL_QUERIES['CLEANUP_EXPIRED'].format(table_name=table_name)
445
+ query = SQL_QUERIES["CLEANUP_EXPIRED"].format(table_name=table_name)
438
446
  result = await postgres_client.execute(query)
439
447
 
440
448
  # Extract count from result
441
- rows_affected = int(result.split()[-1]) if 'DELETE' in result else 0
449
+ rows_affected = int(result.split()[-1]) if "DELETE" in result else 0
442
450
  return create_a2a_success(rows_affected)
443
451
 
444
452
  except Exception as error:
445
453
  return create_a2a_failure(
446
- create_a2a_task_storage_error('cleanup', 'postgres', None, error)
454
+ create_a2a_task_storage_error("cleanup", "postgres", None, error)
447
455
  )
448
456
 
449
457
  async def get_task_stats(
450
- self,
451
- context_id: Optional[str] = None
458
+ self, context_id: Optional[str] = None
452
459
  ) -> A2AResult[Dict[str, Any]]:
453
460
  """Get task statistics"""
454
461
  try:
455
462
  tasks_by_state = {state.value: 0 for state in TaskState}
456
463
 
457
464
  # Get state counts
458
- state_query = SQL_QUERIES['STATS_BY_STATE'].format(table_name=table_name)
465
+ state_query = SQL_QUERIES["STATS_BY_STATE"].format(table_name=table_name)
459
466
  state_rows = await postgres_client.fetch(state_query, context_id)
460
467
 
461
468
  total_tasks = 0
462
469
  for row in state_rows:
463
- state = row['state']
464
- count = int(row['count'])
470
+ state = row["state"]
471
+ count = int(row["count"])
465
472
  if state in tasks_by_state:
466
473
  tasks_by_state[state] = count
467
474
  total_tasks += count
468
475
 
469
476
  # Get date range
470
- date_query = SQL_QUERIES['DATE_RANGE'].format(table_name=table_name)
477
+ date_query = SQL_QUERIES["DATE_RANGE"].format(table_name=table_name)
471
478
  date_row = await postgres_client.fetchrow(date_query, context_id)
472
479
 
473
480
  oldest_task = None
474
481
  newest_task = None
475
482
 
476
- if date_row and date_row['oldest']:
477
- oldest_task = date_row['oldest']
478
- newest_task = date_row['newest']
483
+ if date_row and date_row["oldest"]:
484
+ oldest_task = date_row["oldest"]
485
+ newest_task = date_row["newest"]
479
486
 
480
487
  # Build stats dict with individual state counts as top-level keys
481
488
  stats = {
482
- 'total_tasks': total_tasks,
483
- 'tasks_by_state': tasks_by_state,
484
- 'oldest_task': oldest_task,
485
- 'newest_task': newest_task
489
+ "total_tasks": total_tasks,
490
+ "tasks_by_state": tasks_by_state,
491
+ "oldest_task": oldest_task,
492
+ "newest_task": newest_task,
486
493
  }
487
-
494
+
488
495
  # Also add individual state counts for backwards compatibility
489
496
  for state in TaskState:
490
497
  stats[state.value] = tasks_by_state[state.value]
@@ -493,7 +500,7 @@ async def create_a2a_postgres_task_provider(
493
500
 
494
501
  except Exception as error:
495
502
  return create_a2a_failure(
496
- create_a2a_task_storage_error('stats', 'postgres', None, error)
503
+ create_a2a_task_storage_error("stats", "postgres", None, error)
497
504
  )
498
505
 
499
506
  async def health_check(self) -> A2AResult[Dict[str, Any]]:
@@ -502,20 +509,15 @@ async def create_a2a_postgres_task_provider(
502
509
  start_time = datetime.now()
503
510
 
504
511
  # Simple query to check database connectivity
505
- await postgres_client.fetchval('SELECT 1')
512
+ await postgres_client.fetchval("SELECT 1")
506
513
 
507
514
  latency_ms = (datetime.now() - start_time).total_seconds() * 1000
508
- return create_a2a_success({
509
- 'healthy': True,
510
- 'provider': 'postgres',
511
- 'latency_ms': latency_ms
512
- })
515
+ return create_a2a_success(
516
+ {"healthy": True, "provider": "postgres", "latency_ms": latency_ms}
517
+ )
513
518
 
514
519
  except Exception as error:
515
- return create_a2a_success({
516
- 'healthy': False,
517
- 'error': str(error)
518
- })
520
+ return create_a2a_success({"healthy": False, "error": str(error)})
519
521
 
520
522
  async def close(self) -> A2AResult[None]:
521
523
  """Close/cleanup the provider"""
@@ -526,12 +528,12 @@ async def create_a2a_postgres_task_provider(
526
528
 
527
529
  except Exception as error:
528
530
  return create_a2a_failure(
529
- create_a2a_task_storage_error('close', 'postgres', None, error)
531
+ create_a2a_task_storage_error("close", "postgres", None, error)
530
532
  )
531
533
 
532
534
  return create_a2a_success(PostgresA2ATaskProvider())
533
535
 
534
536
  except Exception as error:
535
537
  return create_a2a_failure(
536
- create_a2a_task_storage_error('create-postgres-provider', 'postgres', None, error)
538
+ create_a2a_task_storage_error("create-postgres-provider", "postgres", None, error)
537
539
  )