jaf-py 2.5.10__py3-none-any.whl → 2.5.12__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 +310 -210
  54. jaf/core/types.py +403 -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 +475 -283
  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.10.dist-info → jaf_py-2.5.12.dist-info}/METADATA +2 -2
  87. jaf_py-2.5.12.dist-info/RECORD +97 -0
  88. jaf_py-2.5.10.dist-info/RECORD +0 -96
  89. {jaf_py-2.5.10.dist-info → jaf_py-2.5.12.dist-info}/WHEEL +0 -0
  90. {jaf_py-2.5.10.dist-info → jaf_py-2.5.12.dist-info}/entry_points.txt +0 -0
  91. {jaf_py-2.5.10.dist-info → jaf_py-2.5.12.dist-info}/licenses/LICENSE +0 -0
  92. {jaf_py-2.5.10.dist-info → jaf_py-2.5.12.dist-info}/top_level.txt +0 -0
jaf/utils/attachments.py CHANGED
@@ -15,7 +15,7 @@ from ..core.types import Attachment
15
15
 
16
16
  class AttachmentValidationError(Exception):
17
17
  """Exception raised when attachment validation fails."""
18
-
18
+
19
19
  def __init__(self, message: str, field: Optional[str] = None):
20
20
  super().__init__(message)
21
21
  self.field = field
@@ -28,14 +28,22 @@ BASE64_SIZE_RATIO = 0.75 # Base64 decoded size is approximately 3/4 of the enco
28
28
  MAX_FORMAT_LENGTH = 10
29
29
 
30
30
  ALLOWED_IMAGE_MIME_TYPES = [
31
- 'image/jpeg', 'image/jpg', 'image/png', 'image/gif',
32
- 'image/webp', 'image/bmp', 'image/svg+xml'
31
+ "image/jpeg",
32
+ "image/jpg",
33
+ "image/png",
34
+ "image/gif",
35
+ "image/webp",
36
+ "image/bmp",
37
+ "image/svg+xml",
33
38
  ]
34
39
 
35
40
  ALLOWED_DOCUMENT_MIME_TYPES = [
36
- 'application/pdf', 'text/plain', 'text/csv', 'application/json',
37
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
38
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
41
+ "application/pdf",
42
+ "text/plain",
43
+ "text/csv",
44
+ "application/json",
45
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
46
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
39
47
  ]
40
48
 
41
49
 
@@ -43,18 +51,18 @@ def _validate_base64(data: str) -> bool:
43
51
  """Validate base64 string format."""
44
52
  try:
45
53
  # Basic base64 pattern check
46
- base64_pattern = re.compile(r'^[A-Za-z0-9+/]*={0,2}$')
54
+ base64_pattern = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
47
55
  if not base64_pattern.match(data):
48
56
  return False
49
-
57
+
50
58
  # Try to decode to verify it's valid base64
51
59
  decoded = base64.b64decode(data)
52
- reencoded = base64.b64encode(decoded).decode('ascii')
53
-
60
+ reencoded = base64.b64encode(decoded).decode("ascii")
61
+
54
62
  # Account for padding differences
55
- normalized_input = data.rstrip('=')
56
- normalized_reencoded = reencoded.rstrip('=')
57
-
63
+ normalized_input = data.rstrip("=")
64
+ normalized_reencoded = reencoded.rstrip("=")
65
+
58
66
  return normalized_input == normalized_reencoded
59
67
  except Exception:
60
68
  return False
@@ -65,17 +73,17 @@ def _validate_attachment_size(data: Optional[str]) -> None:
65
73
  if data:
66
74
  # Calculate exact decoded size for base64 data
67
75
  # Remove padding to get accurate count
68
- data_without_padding = data.rstrip('=')
76
+ data_without_padding = data.rstrip("=")
69
77
  # Each 4 base64 chars encode 3 bytes, with the last group potentially having padding
70
78
  exact_groups = len(data_without_padding) // 4
71
79
  remaining_chars = len(data_without_padding) % 4
72
-
80
+
73
81
  decoded_size = exact_groups * 3
74
82
  if remaining_chars == 2:
75
83
  decoded_size += 1 # 2 chars = 1 byte
76
84
  elif remaining_chars == 3:
77
85
  decoded_size += 2 # 3 chars = 2 bytes
78
-
86
+
79
87
  if decoded_size > MAX_ATTACHMENT_SIZE:
80
88
  size_mb = round(decoded_size / 1024 / 1024, 2)
81
89
  max_mb = MAX_ATTACHMENT_SIZE // 1024 // 1024
@@ -88,23 +96,23 @@ def _validate_filename(name: Optional[str]) -> None:
88
96
  """Validate filename for security and length constraints."""
89
97
  if not name:
90
98
  return
91
-
99
+
92
100
  if len(name) > MAX_FILENAME_LENGTH:
93
101
  raise AttachmentValidationError(
94
102
  f"Filename length ({len(name)}) exceeds maximum allowed length ({MAX_FILENAME_LENGTH})"
95
103
  )
96
-
104
+
97
105
  # Check for dangerous characters and control characters
98
106
  dangerous_chars = re.compile(r'[<>:"|?*]')
99
- control_chars = re.compile(r'[\x00-\x1f]')
100
-
107
+ control_chars = re.compile(r"[\x00-\x1f]")
108
+
101
109
  if dangerous_chars.search(name) or control_chars.search(name):
102
- raise AttachmentValidationError('Filename contains invalid characters')
103
-
110
+ raise AttachmentValidationError("Filename contains invalid characters")
111
+
104
112
  # Check for path traversal attempts
105
- if '..' in name or '/' in name or '\\' in name:
113
+ if ".." in name or "/" in name or "\\" in name:
106
114
  raise AttachmentValidationError(
107
- 'Filename cannot contain path separators or traversal sequences'
115
+ "Filename cannot contain path separators or traversal sequences"
108
116
  )
109
117
 
110
118
 
@@ -113,10 +121,10 @@ def _validate_mime_type(mime_type: Optional[str], allowed_types: List[str], kind
113
121
  if mime_type:
114
122
  # Normalize the input mime_type
115
123
  normalized_mime_type = mime_type.lower().strip()
116
-
124
+
117
125
  # Normalize the allowed types list
118
126
  normalized_allowed_types = {t.lower().strip() for t in allowed_types}
119
-
127
+
120
128
  if normalized_mime_type not in normalized_allowed_types:
121
129
  raise AttachmentValidationError(
122
130
  f"MIME type '{mime_type}' is not allowed for {kind} attachments. "
@@ -128,33 +136,33 @@ def _validate_url(url: Optional[str]) -> None:
128
136
  """Validate URL format and protocol."""
129
137
  if not url:
130
138
  return
131
-
139
+
132
140
  try:
133
141
  parsed = urlparse(url)
134
- allowed_protocols = ['http', 'https', 'data']
135
-
142
+ allowed_protocols = ["http", "https", "data"]
143
+
136
144
  if parsed.scheme not in allowed_protocols:
137
145
  raise AttachmentValidationError(
138
146
  f"URL protocol '{parsed.scheme}' is not allowed. "
139
147
  f"Allowed protocols: {', '.join(allowed_protocols)}"
140
148
  )
141
-
149
+
142
150
  # Additional validation for data URLs
143
- if parsed.scheme == 'data':
151
+ if parsed.scheme == "data":
144
152
  # For data URLs, the "path" component in urlparse contains the mediatype and data
145
153
  # Proper data URL format: mediatype[;charset][;base64],data
146
- data_content_pattern = re.compile(r'^([^;,]+)(;[^;,]+)*(;base64)?,(.+)$')
154
+ data_content_pattern = re.compile(r"^([^;,]+)(;[^;,]+)*(;base64)?,(.+)$")
147
155
  data_content = parsed.path
148
-
156
+
149
157
  # Some URLs might have query components that are part of the data
150
158
  if parsed.query:
151
159
  data_content += "?" + parsed.query
152
-
160
+
153
161
  if not data_content_pattern.match(data_content):
154
162
  raise AttachmentValidationError(
155
- 'Invalid data URL format: must match mediatype[;charset][;base64],data pattern'
163
+ "Invalid data URL format: must match mediatype[;charset][;base64],data pattern"
156
164
  )
157
-
165
+
158
166
  except ValueError as e:
159
167
  raise AttachmentValidationError(f"Invalid URL: {e}")
160
168
 
@@ -163,16 +171,16 @@ def _process_base64_data(data: Union[bytes, str, None]) -> Optional[str]:
163
171
  """Process and validate base64 data."""
164
172
  if not data:
165
173
  return None
166
-
174
+
167
175
  if isinstance(data, bytes):
168
- base64_str = base64.b64encode(data).decode('ascii')
176
+ base64_str = base64.b64encode(data).decode("ascii")
169
177
  else:
170
178
  base64_str = data
171
-
179
+
172
180
  # Validate base64 format if it was provided as string
173
181
  if isinstance(data, str) and not _validate_base64(base64_str):
174
- raise AttachmentValidationError('Invalid base64 data format')
175
-
182
+ raise AttachmentValidationError("Invalid base64 data format")
183
+
176
184
  return base64_str
177
185
 
178
186
 
@@ -180,46 +188,40 @@ def make_image_attachment(
180
188
  data: Union[bytes, str, None] = None,
181
189
  url: Optional[str] = None,
182
190
  mime_type: Optional[str] = None,
183
- name: Optional[str] = None
191
+ name: Optional[str] = None,
184
192
  ) -> Attachment:
185
193
  """
186
194
  Create a validated image attachment.
187
-
195
+
188
196
  Args:
189
197
  data: Raw bytes or base64 string
190
198
  url: Remote or data URL
191
199
  mime_type: MIME type (e.g., 'image/png')
192
200
  name: Optional filename
193
-
201
+
194
202
  Returns:
195
203
  Validated Attachment object
196
-
204
+
197
205
  Raises:
198
206
  AttachmentValidationError: If validation fails
199
207
  """
200
208
  # Validate inputs
201
209
  _validate_filename(name)
202
210
  _validate_url(url)
203
- _validate_mime_type(mime_type, ALLOWED_IMAGE_MIME_TYPES, 'image')
204
-
211
+ _validate_mime_type(mime_type, ALLOWED_IMAGE_MIME_TYPES, "image")
212
+
205
213
  # Process data to base64 first, so we can validate size for both bytes and string inputs
206
214
  base64_data = _process_base64_data(data)
207
-
215
+
208
216
  # Validate size if we have data
209
217
  if base64_data:
210
218
  _validate_attachment_size(base64_data)
211
-
219
+
212
220
  # Ensure at least one content source
213
221
  if not url and not base64_data:
214
- raise AttachmentValidationError('Image attachment must have either url or data')
215
-
216
- return Attachment(
217
- kind='image',
218
- mime_type=mime_type,
219
- name=name,
220
- url=url,
221
- data=base64_data
222
- )
222
+ raise AttachmentValidationError("Image attachment must have either url or data")
223
+
224
+ return Attachment(kind="image", mime_type=mime_type, name=name, url=url, data=base64_data)
223
225
 
224
226
 
225
227
  def make_file_attachment(
@@ -227,50 +229,45 @@ def make_file_attachment(
227
229
  url: Optional[str] = None,
228
230
  mime_type: Optional[str] = None,
229
231
  name: Optional[str] = None,
230
- format: Optional[str] = None
232
+ format: Optional[str] = None,
231
233
  ) -> Attachment:
232
234
  """
233
235
  Create a validated file attachment.
234
-
236
+
235
237
  Args:
236
238
  data: Raw bytes or base64 string
237
239
  url: Remote or data URL
238
240
  mime_type: MIME type
239
241
  name: Optional filename
240
242
  format: Optional format identifier (e.g., 'pdf', 'txt')
241
-
243
+
242
244
  Returns:
243
245
  Validated Attachment object
244
-
246
+
245
247
  Raises:
246
248
  AttachmentValidationError: If validation fails
247
249
  """
248
250
  # Validate inputs
249
251
  _validate_filename(name)
250
252
  _validate_url(url)
251
-
253
+
252
254
  # Process data to base64 first, so we can validate size for both bytes and string inputs
253
255
  base64_data = _process_base64_data(data)
254
-
256
+
255
257
  # Validate size if we have data
256
258
  if base64_data:
257
259
  _validate_attachment_size(base64_data)
258
-
260
+
259
261
  # Ensure at least one content source
260
262
  if not url and not base64_data:
261
- raise AttachmentValidationError('File attachment must have either url or data')
262
-
263
+ raise AttachmentValidationError("File attachment must have either url or data")
264
+
263
265
  # Validate format if provided
264
266
  if format and len(format) > MAX_FORMAT_LENGTH:
265
- raise AttachmentValidationError('File format must be 10 characters or less')
266
-
267
+ raise AttachmentValidationError("File format must be 10 characters or less")
268
+
267
269
  return Attachment(
268
- kind='file',
269
- mime_type=mime_type,
270
- name=name,
271
- url=url,
272
- data=base64_data,
273
- format=format
270
+ kind="file", mime_type=mime_type, name=name, url=url, data=base64_data, format=format
274
271
  )
275
272
 
276
273
 
@@ -280,11 +277,11 @@ def make_document_attachment(
280
277
  mime_type: Optional[str] = None,
281
278
  name: Optional[str] = None,
282
279
  format: Optional[str] = None,
283
- use_litellm_format: Optional[bool] = None
280
+ use_litellm_format: Optional[bool] = None,
284
281
  ) -> Attachment:
285
282
  """
286
283
  Create a validated document attachment.
287
-
284
+
288
285
  Args:
289
286
  data: Raw bytes or base64 string
290
287
  url: Remote or data URL
@@ -292,93 +289,91 @@ def make_document_attachment(
292
289
  name: Optional filename
293
290
  format: Optional format identifier
294
291
  use_litellm_format: Whether to use LiteLLM native format
295
-
292
+
296
293
  Returns:
297
294
  Validated Attachment object
298
-
295
+
299
296
  Raises:
300
297
  AttachmentValidationError: If validation fails
301
298
  """
302
299
  # Additional validation for documents
303
- _validate_mime_type(mime_type, ALLOWED_DOCUMENT_MIME_TYPES, 'document')
304
-
300
+ _validate_mime_type(mime_type, ALLOWED_DOCUMENT_MIME_TYPES, "document")
301
+
305
302
  attachment = make_file_attachment(
306
- data=data,
307
- url=url,
308
- mime_type=mime_type,
309
- name=name,
310
- format=format
303
+ data=data, url=url, mime_type=mime_type, name=name, format=format
311
304
  )
312
-
305
+
313
306
  return Attachment(
314
- kind='document',
307
+ kind="document",
315
308
  mime_type=attachment.mime_type,
316
309
  name=attachment.name,
317
310
  url=attachment.url,
318
311
  data=attachment.data,
319
312
  format=attachment.format,
320
- use_litellm_format=use_litellm_format
313
+ use_litellm_format=use_litellm_format,
321
314
  )
322
315
 
323
316
 
324
317
  def validate_attachment(attachment: Attachment) -> None:
325
318
  """
326
319
  Validate an existing attachment object.
327
-
320
+
328
321
  Args:
329
322
  attachment: Attachment to validate
330
-
323
+
331
324
  Raises:
332
325
  AttachmentValidationError: If validation fails
333
326
  """
334
327
  try:
335
328
  if not attachment.url and not attachment.data:
336
329
  raise AttachmentValidationError(
337
- 'Attachment must have either url or data',
338
- field='url/data'
330
+ "Attachment must have either url or data", field="url/data"
339
331
  )
340
-
332
+
341
333
  if attachment.name:
342
334
  try:
343
335
  _validate_filename(attachment.name)
344
336
  except AttachmentValidationError as e:
345
- raise AttachmentValidationError(f"Invalid filename: {e}", field='name') from e
346
-
337
+ raise AttachmentValidationError(f"Invalid filename: {e}", field="name") from e
338
+
347
339
  if attachment.url:
348
340
  try:
349
341
  _validate_url(attachment.url)
350
342
  except AttachmentValidationError as e:
351
- raise AttachmentValidationError(f"Invalid URL: {e}", field='url') from e
352
-
343
+ raise AttachmentValidationError(f"Invalid URL: {e}", field="url") from e
344
+
353
345
  if attachment.data:
354
346
  try:
355
347
  _validate_attachment_size(attachment.data)
356
348
  except AttachmentValidationError as e:
357
- raise AttachmentValidationError(f"Size validation failed: {e}", field='data') from e
358
-
349
+ raise AttachmentValidationError(f"Size validation failed: {e}", field="data") from e
350
+
359
351
  if not _validate_base64(attachment.data):
360
352
  raise AttachmentValidationError(
361
- 'Invalid base64 data format in attachment',
362
- field='data'
353
+ "Invalid base64 data format in attachment", field="data"
363
354
  )
364
-
355
+
365
356
  # Validate MIME type based on attachment kind
366
- if attachment.kind == 'image':
357
+ if attachment.kind == "image":
367
358
  try:
368
- _validate_mime_type(attachment.mime_type, ALLOWED_IMAGE_MIME_TYPES, 'image')
359
+ _validate_mime_type(attachment.mime_type, ALLOWED_IMAGE_MIME_TYPES, "image")
369
360
  except AttachmentValidationError as e:
370
- raise AttachmentValidationError(f"Image MIME type validation failed: {e}", field='mime_type') from e
371
- elif attachment.kind == 'document':
361
+ raise AttachmentValidationError(
362
+ f"Image MIME type validation failed: {e}", field="mime_type"
363
+ ) from e
364
+ elif attachment.kind == "document":
372
365
  try:
373
- _validate_mime_type(attachment.mime_type, ALLOWED_DOCUMENT_MIME_TYPES, 'document')
366
+ _validate_mime_type(attachment.mime_type, ALLOWED_DOCUMENT_MIME_TYPES, "document")
374
367
  except AttachmentValidationError as e:
375
- raise AttachmentValidationError(f"Document MIME type validation failed: {e}", field='mime_type') from e
376
- elif attachment.kind == 'file':
368
+ raise AttachmentValidationError(
369
+ f"Document MIME type validation failed: {e}", field="mime_type"
370
+ ) from e
371
+ elif attachment.kind == "file":
377
372
  # Files can have any MIME type, but still validate format
378
373
  if attachment.format and len(attachment.format) > MAX_FORMAT_LENGTH:
379
374
  raise AttachmentValidationError(
380
375
  f'File format "{attachment.format}" exceeds maximum length of {MAX_FORMAT_LENGTH} characters',
381
- field='format'
376
+ field="format",
382
377
  )
383
378
  except AttachmentValidationError:
384
379
  raise
@@ -394,8 +389,8 @@ def assert_non_empty_attachment(attachment: Attachment) -> None:
394
389
 
395
390
  # Export validation constants for external use
396
391
  ATTACHMENT_LIMITS = {
397
- 'MAX_SIZE': MAX_ATTACHMENT_SIZE,
398
- 'MAX_FILENAME_LENGTH': MAX_FILENAME_LENGTH,
399
- 'ALLOWED_IMAGE_MIME_TYPES': ALLOWED_IMAGE_MIME_TYPES,
400
- 'ALLOWED_DOCUMENT_MIME_TYPES': ALLOWED_DOCUMENT_MIME_TYPES,
401
- }
392
+ "MAX_SIZE": MAX_ATTACHMENT_SIZE,
393
+ "MAX_FILENAME_LENGTH": MAX_FILENAME_LENGTH,
394
+ "ALLOWED_IMAGE_MIME_TYPES": ALLOWED_IMAGE_MIME_TYPES,
395
+ "ALLOWED_DOCUMENT_MIME_TYPES": ALLOWED_DOCUMENT_MIME_TYPES,
396
+ }