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.
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 +360 -279
  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.11.dist-info}/METADATA +2 -2
  87. jaf_py-2.5.11.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.11.dist-info}/WHEEL +0 -0
  90. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/entry_points.txt +0 -0
  91. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/licenses/LICENSE +0 -0
  92. {jaf_py-2.5.10.dist-info → jaf_py-2.5.11.dist-info}/top_level.txt +0 -0
jaf/core/tools.py CHANGED
@@ -30,28 +30,28 @@ from .tool_results import ToolResult
30
30
  def create_function_tool(config: FunctionToolConfig) -> Tool:
31
31
  """
32
32
  Create a function-based tool using object configuration.
33
-
33
+
34
34
  This is the new, recommended API for creating tools that provides better
35
35
  type safety, extensibility, and self-documentation.
36
-
36
+
37
37
  Args:
38
38
  config: Tool configuration object with name, description, execute function,
39
39
  parameters, and optional metadata and source.
40
-
40
+
41
41
  Returns:
42
42
  A Tool implementation that can be used with agents.
43
-
43
+
44
44
  Example:
45
45
  ```python
46
46
  from pydantic import BaseModel
47
47
  from jaf import create_function_tool, ToolSource
48
-
48
+
49
49
  class GreetArgs(BaseModel):
50
50
  name: str
51
-
51
+
52
52
  async def greet_execute(args: GreetArgs, context) -> str:
53
53
  return f"Hello, {args.name}!"
54
-
54
+
55
55
  greet_tool = create_function_tool({
56
56
  'name': 'greet',
57
57
  'description': 'Greets a user by name',
@@ -63,83 +63,85 @@ def create_function_tool(config: FunctionToolConfig) -> Tool:
63
63
  ```
64
64
  """
65
65
  # Get the function from config
66
- original_func = config['execute']
67
-
66
+ original_func = config["execute"]
67
+
68
68
  # Validate tool configuration
69
69
  logger = logging.getLogger(__name__)
70
-
71
- tool_name = config['name']
72
- parameters = config['parameters']
73
-
70
+
71
+ tool_name = config["name"]
72
+ parameters = config["parameters"]
73
+
74
74
  logger.info(f"Creating tool: {tool_name}")
75
-
75
+
76
76
  # Validate parameters schema
77
77
  if parameters is None:
78
78
  logger.error(f"Tool {tool_name}: parameters is None - LLM will receive no schema!")
79
79
  raise ValueError(f"Tool '{tool_name}' has None parameters. Provide a Pydantic model class.")
80
-
80
+
81
81
  # Check if it's a Pydantic model class
82
82
  if BaseModel is None:
83
83
  logger.warning(f"Pydantic not available for tool {tool_name} validation")
84
84
  else:
85
85
  if not (isinstance(parameters, type) and issubclass(parameters, BaseModel)):
86
- logger.error(f"Tool {tool_name}: parameters must be a Pydantic BaseModel class, got {type(parameters)}")
87
- raise ValueError(f"Tool '{tool_name}' parameters must be a Pydantic BaseModel class, got {type(parameters)}")
88
-
86
+ logger.error(
87
+ f"Tool {tool_name}: parameters must be a Pydantic BaseModel class, got {type(parameters)}"
88
+ )
89
+ raise ValueError(
90
+ f"Tool '{tool_name}' parameters must be a Pydantic BaseModel class, got {type(parameters)}"
91
+ )
92
+
89
93
  # Validate schema generation (cached for performance)
90
- if not hasattr(parameters, '_schema_validated'):
94
+ if not hasattr(parameters, "_schema_validated"):
91
95
  try:
92
96
  # Generate schema once to validate the model is well-formed.
93
97
  # Allow empty object schemas (no parameters) for tools that take no args.
94
- if hasattr(parameters, 'model_json_schema'):
98
+ if hasattr(parameters, "model_json_schema"):
95
99
  _ = parameters.model_json_schema()
96
- elif hasattr(parameters, 'schema'):
100
+ elif hasattr(parameters, "schema"):
97
101
  _ = parameters.schema()
98
102
  parameters._schema_validated = True
99
103
  except Exception as e:
100
104
  logger.error(f"Tool {tool_name} schema generation failed: {e}")
101
105
  raise ValueError(f"Tool '{tool_name}' schema generation failed: {e}")
102
-
106
+
103
107
  # Create schema
104
108
  tool_schema = ToolSchema(
105
- name=config['name'],
106
- description=config['description'],
107
- parameters=config['parameters'],
108
- timeout=config.get('timeout')
109
+ name=config["name"],
110
+ description=config["description"],
111
+ parameters=config["parameters"],
112
+ timeout=config.get("timeout"),
109
113
  )
110
-
114
+
111
115
  # Create a new wrapper function for this tool to avoid conflicts when multiple tools use the same base function
112
116
  async def tool_wrapper(args: Any, context: Any) -> Union[str, ToolResult]:
113
117
  """Execute the tool with given arguments and context."""
114
118
  result = original_func(args, context)
115
-
119
+
116
120
  # Handle both sync and async execute functions
117
- if hasattr(result, '__await__'):
121
+ if hasattr(result, "__await__"):
118
122
  return await result
119
123
  return result
120
-
124
+
121
125
  # Add tool properties and methods to the wrapper function
122
126
  tool_wrapper.schema = tool_schema
123
- tool_wrapper.metadata = config.get('metadata', {})
124
- tool_wrapper.source = config.get('source', ToolSource.NATIVE)
125
-
127
+ tool_wrapper.metadata = config.get("metadata", {})
128
+ tool_wrapper.source = config.get("source", ToolSource.NATIVE)
129
+
126
130
  # Add execute method that calls the wrapper function
127
131
  async def execute(args: Any, context: Any) -> Union[str, ToolResult]:
128
132
  """Execute the tool with given arguments and context."""
129
133
  return await tool_wrapper(args, context)
130
-
134
+
131
135
  tool_wrapper.execute = execute
132
-
136
+
133
137
  # Add __call__ method for direct execution
134
138
  async def call_method(args: Any, context: Any) -> Union[str, ToolResult]:
135
139
  """Execute the tool with given arguments and context."""
136
140
  return await tool_wrapper.execute(args, context)
137
-
138
- tool_wrapper.__call__ = call_method
139
-
140
- return tool_wrapper
141
141
 
142
+ tool_wrapper.__call__ = call_method
142
143
 
144
+ return tool_wrapper
143
145
 
144
146
 
145
147
  def create_function_tool_legacy(
@@ -148,14 +150,14 @@ def create_function_tool_legacy(
148
150
  execute: ToolExecuteFunction,
149
151
  parameters: Any,
150
152
  metadata: Optional[Dict[str, Any]] = None,
151
- source: Optional[ToolSource] = None
153
+ source: Optional[ToolSource] = None,
152
154
  ) -> Tool:
153
155
  """
154
156
  Create a function-based tool using legacy positional arguments.
155
-
157
+
156
158
  **DEPRECATED**: This function is deprecated. Use `create_function_tool` with
157
159
  an object-based configuration instead for better type safety and extensibility.
158
-
160
+
159
161
  Args:
160
162
  name: The name of the tool
161
163
  description: A description of what the tool does
@@ -163,22 +165,22 @@ def create_function_tool_legacy(
163
165
  parameters: Pydantic model or similar for parameter validation
164
166
  metadata: Optional metadata for the tool
165
167
  source: Optional source tracking for the tool
166
-
168
+
167
169
  Returns:
168
170
  A Tool implementation that can be used with agents.
169
171
  """
170
172
  warnings.warn(
171
173
  "create_function_tool_legacy is deprecated. Use create_function_tool with object configuration instead.",
172
174
  DeprecationWarning,
173
- stacklevel=2
175
+ stacklevel=2,
174
176
  )
175
177
  config: FunctionToolConfig = {
176
- 'name': name,
177
- 'description': description,
178
- 'execute': execute,
179
- 'parameters': parameters,
180
- 'metadata': metadata,
181
- 'source': source or ToolSource.NATIVE
178
+ "name": name,
179
+ "description": description,
180
+ "execute": execute,
181
+ "parameters": parameters,
182
+ "metadata": metadata,
183
+ "source": source or ToolSource.NATIVE,
182
184
  }
183
185
  return create_function_tool(config)
184
186
 
@@ -186,13 +188,13 @@ def create_function_tool_legacy(
186
188
  def create_async_function_tool(config: FunctionToolConfig) -> Tool:
187
189
  """
188
190
  Create an async function-based tool using object configuration.
189
-
191
+
190
192
  This is a convenience function that's identical to create_function_tool
191
193
  but with a name that makes it clear the execute function should be async.
192
-
194
+
193
195
  Args:
194
196
  config: Tool configuration object with async execute function.
195
-
197
+
196
198
  Returns:
197
199
  A Tool implementation that can be used with agents.
198
200
  """
@@ -205,18 +207,18 @@ def create_async_function_tool_legacy(
205
207
  execute: ToolExecuteFunction,
206
208
  parameters: Any,
207
209
  metadata: Optional[Dict[str, Any]] = None,
208
- source: Optional[ToolSource] = None
210
+ source: Optional[ToolSource] = None,
209
211
  ) -> Tool:
210
212
  """
211
213
  Create an async function-based tool using legacy positional arguments.
212
-
214
+
213
215
  **DEPRECATED**: This function is deprecated. Use `create_function_tool` with
214
216
  an object-based configuration instead for better type safety and extensibility.
215
217
  """
216
218
  warnings.warn(
217
219
  "create_async_function_tool_legacy is deprecated. Use create_function_tool with object configuration instead.",
218
220
  DeprecationWarning,
219
- stacklevel=2
221
+ stacklevel=2,
220
222
  )
221
223
  return create_function_tool_legacy(name, description, execute, parameters, metadata, source)
222
224
 
@@ -225,32 +227,34 @@ def _extract_docstring_info(func):
225
227
  """Extract description and parameter info from function docstring."""
226
228
  doc = inspect.getdoc(func)
227
229
  if not doc:
228
- return func.__name__.replace('_', ' ').title(), {}
229
-
230
- lines = doc.strip().split('\n')
230
+ return func.__name__.replace("_", " ").title(), {}
231
+
232
+ lines = doc.strip().split("\n")
231
233
  if not lines:
232
- return func.__name__.replace('_', ' ').title(), {}
233
-
234
+ return func.__name__.replace("_", " ").title(), {}
235
+
234
236
  # First non-empty line is the description
235
237
  description = lines[0].strip()
236
-
238
+
237
239
  # Look for Args section to extract parameter descriptions
238
240
  param_descriptions = {}
239
241
  in_args_section = False
240
-
242
+
241
243
  for line in lines[1:]:
242
244
  line = line.strip()
243
- if line.lower().startswith('args:'):
245
+ if line.lower().startswith("args:"):
244
246
  in_args_section = True
245
247
  continue
246
- elif line.lower().startswith(('returns:', 'return:', 'raises:', 'raise:', 'examples:', 'example:')):
248
+ elif line.lower().startswith(
249
+ ("returns:", "return:", "raises:", "raise:", "examples:", "example:")
250
+ ):
247
251
  in_args_section = False
248
252
  continue
249
- elif in_args_section and line and ':' in line:
253
+ elif in_args_section and line and ":" in line:
250
254
  # Parse parameter description like "location: The location to fetch the weather for."
251
- param_name, param_desc = line.split(':', 1)
255
+ param_name, param_desc = line.split(":", 1)
252
256
  param_descriptions[param_name.strip()] = param_desc.strip()
253
-
257
+
254
258
  return description, param_descriptions
255
259
 
256
260
 
@@ -259,45 +263,45 @@ def _create_parameter_schema_from_signature(func):
259
263
  try:
260
264
  # Try to use Pydantic if available
261
265
  from pydantic import BaseModel, create_model
262
-
266
+
263
267
  signature = inspect.signature(func)
264
268
  type_hints = get_type_hints(func)
265
-
269
+
266
270
  # Extract parameter info, excluding 'context' parameter
267
271
  fields = {}
268
272
  for param_name, param in signature.parameters.items():
269
- if param_name == 'context':
273
+ if param_name == "context":
270
274
  continue
271
-
275
+
272
276
  param_type = type_hints.get(param_name, str)
273
-
277
+
274
278
  # Handle default values
275
279
  if param.default != inspect.Parameter.empty:
276
280
  fields[param_name] = (param_type, param.default)
277
281
  else:
278
282
  fields[param_name] = (param_type, ...)
279
-
283
+
280
284
  # Create dynamic Pydantic model
281
285
  if fields:
282
286
  return create_model(f"{func.__name__}Args", **fields)
283
287
  else:
284
288
  # Return a simple BaseModel if no parameters
285
289
  return create_model(f"{func.__name__}Args")
286
-
290
+
287
291
  except ImportError:
288
292
  # Fallback to simple dict-based schema if Pydantic not available
289
293
  signature = inspect.signature(func)
290
294
  type_hints = get_type_hints(func)
291
-
295
+
292
296
  properties = {}
293
297
  required = []
294
-
298
+
295
299
  for param_name, param in signature.parameters.items():
296
- if param_name == 'context':
300
+ if param_name == "context":
297
301
  continue
298
-
302
+
299
303
  param_type = type_hints.get(param_name, str)
300
-
304
+
301
305
  # Convert Python types to JSON schema types
302
306
  if param_type == str:
303
307
  properties[param_name] = {"type": "string"}
@@ -309,16 +313,12 @@ def _create_parameter_schema_from_signature(func):
309
313
  properties[param_name] = {"type": "boolean"}
310
314
  else:
311
315
  properties[param_name] = {"type": "string"} # Default fallback
312
-
316
+
313
317
  # Check if parameter is required
314
318
  if param.default == inspect.Parameter.empty:
315
319
  required.append(param_name)
316
-
317
- return {
318
- "type": "object",
319
- "properties": properties,
320
- "required": required
321
- }
320
+
321
+ return {"type": "object", "properties": properties, "required": required}
322
322
 
323
323
 
324
324
  def function_tool(
@@ -328,19 +328,19 @@ def function_tool(
328
328
  description: Optional[str] = None,
329
329
  metadata: Optional[Dict[str, Any]] = None,
330
330
  source: Optional[ToolSource] = None,
331
- timeout: Optional[float] = None
331
+ timeout: Optional[float] = None,
332
332
  ):
333
333
  """
334
334
  Decorator to automatically create a tool from a function.
335
-
335
+
336
336
  This decorator extracts type information from function annotations and
337
337
  docstrings to automatically create a properly configured tool by adding
338
338
  tool properties and methods directly to the function.
339
-
339
+
340
340
  Can be used with or without parameters:
341
341
  - @function_tool
342
342
  - @function_tool(name="custom", description="Custom tool")
343
-
343
+
344
344
  Args:
345
345
  func_or_name: When used as @function_tool, this is the function being decorated.
346
346
  When used as @function_tool(...), this should be None.
@@ -348,18 +348,18 @@ def function_tool(
348
348
  description: Optional custom description (defaults to docstring)
349
349
  metadata: Optional metadata for the tool
350
350
  source: Optional source tracking for the tool
351
-
351
+
352
352
  Returns:
353
353
  A Tool implementation that can be used with agents.
354
-
354
+
355
355
  Example:
356
356
  ```python
357
357
  from jaf import function_tool
358
-
358
+
359
359
  @function_tool
360
360
  async def fetch_weather(location: str, context) -> str:
361
361
  '''Fetch the weather for a given location.
362
-
362
+
363
363
  Args:
364
364
  location: The location to fetch the weather for.
365
365
  '''
@@ -367,55 +367,53 @@ def function_tool(
367
367
  return "sunny"
368
368
  ```
369
369
  """
370
+
370
371
  def create_tool_from_func(func):
371
372
  # Extract function information
372
373
  func_name = name or func.__name__
373
374
  func_description, param_descriptions = _extract_docstring_info(func)
374
375
  if description:
375
376
  func_description = description
376
-
377
+
377
378
  # Create parameter schema
378
379
  parameters = _create_parameter_schema_from_signature(func)
379
-
380
+
380
381
  # Store the original function
381
382
  original_func = func
382
-
383
+
383
384
  # Create schema
384
385
  tool_schema = ToolSchema(
385
- name=func_name,
386
- description=func_description,
387
- parameters=parameters,
388
- timeout=timeout
386
+ name=func_name, description=func_description, parameters=parameters, timeout=timeout
389
387
  )
390
-
388
+
391
389
  # Add tool properties and methods to the function
392
390
  func.schema = tool_schema
393
391
  func.metadata = metadata or {}
394
392
  func.source = source or ToolSource.NATIVE
395
-
393
+
396
394
  # Add execute method that calls the original function
397
395
  async def execute(args: Any, context: Any) -> Union[str, ToolResult]:
398
396
  """Execute the tool with given arguments and context."""
399
397
  # Check if args is a Pydantic model (from JAF engine) or individual parameters (manual call)
400
- if hasattr(args, 'model_dump'): # Pydantic v2
398
+ if hasattr(args, "model_dump"): # Pydantic v2
401
399
  # Unpack Pydantic model to individual parameters
402
400
  kwargs = args.model_dump()
403
401
  result = original_func(**kwargs, context=context)
404
- elif hasattr(args, 'dict'): # Pydantic v1
402
+ elif hasattr(args, "dict"): # Pydantic v1
405
403
  # Unpack Pydantic model to individual parameters
406
404
  kwargs = args.dict()
407
405
  result = original_func(**kwargs, context=context)
408
406
  else:
409
407
  # Assume it's already unpacked parameters (backward compatibility)
410
408
  result = original_func(args, context)
411
-
409
+
412
410
  # Handle both sync and async execute functions
413
- if hasattr(result, '__await__'):
411
+ if hasattr(result, "__await__"):
414
412
  return await result
415
413
  return result
416
-
414
+
417
415
  func.execute = execute
418
-
416
+
419
417
  # Add __call__ method that provides helpful error message
420
418
  def call_method(*args, **kwargs):
421
419
  """Provide helpful error for incorrect tool usage."""
@@ -424,23 +422,23 @@ def function_tool(
424
422
  f"from JAF engine, or directly as 'await {func_name}(param1, param2, ..., context)' "
425
423
  f"for manual execution. Direct tool object calls are not supported."
426
424
  )
427
-
425
+
428
426
  func.__call__ = call_method
429
-
427
+
430
428
  return func
431
-
429
+
432
430
  # If func_or_name is a callable, this means the decorator was used without parentheses: @function_tool
433
431
  if callable(func_or_name):
434
432
  return create_tool_from_func(func_or_name)
435
-
433
+
436
434
  # Otherwise, this means the decorator was used with parentheses: @function_tool(...)
437
435
  # In this case, func_or_name might be None or the name parameter
438
436
  if func_or_name is not None and name is None:
439
437
  # Handle the case where the first parameter was meant to be the name
440
438
  name = func_or_name
441
-
439
+
442
440
  # Return the decorator function
443
441
  def decorator(func):
444
442
  return create_tool_from_func(func)
445
-
443
+
446
444
  return decorator