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,7 +30,7 @@ from ..types import (
|
|
|
30
30
|
|
|
31
31
|
# SQL queries for A2A task operations
|
|
32
32
|
SQL_QUERIES = {
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
""",
|
|
71
|
+
"DELETE_TASK": """
|
|
72
72
|
DELETE FROM {table_name}
|
|
73
73
|
WHERE task_id = $1
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
""",
|
|
75
|
+
"DELETE_TASKS_BY_CONTEXT": """
|
|
76
76
|
DELETE FROM {table_name}
|
|
77
77
|
WHERE context_id = $1
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
""",
|
|
79
|
+
"CLEANUP_EXPIRED": """
|
|
80
80
|
DELETE FROM {table_name}
|
|
81
81
|
WHERE expires_at IS NOT NULL AND expires_at <= NOW()
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
118
|
+
table_name = config.table_name or "a2a_tasks"
|
|
119
119
|
|
|
120
120
|
# Initialize database schema
|
|
121
|
-
create_table_sql = SQL_QUERIES[
|
|
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[
|
|
128
|
-
context_id=row[
|
|
129
|
-
state=row[
|
|
130
|
-
task_data=row[
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 = [
|
|
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
|
|
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
|
|
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
|
|
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[
|
|
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(
|
|
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(
|
|
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[
|
|
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(
|
|
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,
|
|
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[
|
|
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 =
|
|
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[
|
|
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 ==
|
|
312
|
+
if result == "UPDATE 0":
|
|
303
313
|
return create_a2a_failure(
|
|
304
|
-
create_a2a_task_not_found_error(task.id,
|
|
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(
|
|
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,
|
|
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(
|
|
349
|
-
|
|
350
|
-
|
|
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(
|
|
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
|
|
385
|
+
sql += f" LIMIT {query.limit}"
|
|
376
386
|
if query.offset:
|
|
377
|
-
sql += f
|
|
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(
|
|
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[
|
|
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
|
|
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(
|
|
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[
|
|
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
|
|
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(
|
|
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[
|
|
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
|
|
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(
|
|
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[
|
|
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[
|
|
464
|
-
count = int(row[
|
|
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[
|
|
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[
|
|
477
|
-
oldest_task = date_row[
|
|
478
|
-
newest_task = date_row[
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
510
|
-
|
|
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(
|
|
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(
|
|
538
|
+
create_a2a_task_storage_error("create-postgres-provider", "postgres", None, error)
|
|
537
539
|
)
|