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
jaf/core/agent_tool.py CHANGED
@@ -33,11 +33,13 @@ from .types import (
33
33
  generate_trace_id,
34
34
  )
35
35
 
36
- Ctx = TypeVar('Ctx')
37
- Out = TypeVar('Out')
36
+ Ctx = TypeVar("Ctx")
37
+ Out = TypeVar("Out")
38
38
 
39
39
  # Context variable to store the current RunConfig for agent tools
40
- _current_run_config: contextvars.ContextVar[Optional[RunConfig]] = contextvars.ContextVar('current_run_config', default=None)
40
+ _current_run_config: contextvars.ContextVar[Optional[RunConfig]] = contextvars.ContextVar(
41
+ "current_run_config", default=None
42
+ )
41
43
 
42
44
 
43
45
  def set_current_run_config(config: RunConfig) -> None:
@@ -52,9 +54,11 @@ def get_current_run_config() -> Optional[RunConfig]:
52
54
 
53
55
  class AgentToolInput(BaseModel if BaseModel else object):
54
56
  """Input parameters for agent tools."""
57
+
55
58
  input: str
56
-
59
+
57
60
  if not BaseModel:
61
+
58
62
  def __init__(self, input: str):
59
63
  self.input = input
60
64
 
@@ -64,15 +68,21 @@ def create_agent_tool(
64
68
  tool_name: Optional[str] = None,
65
69
  tool_description: Optional[str] = None,
66
70
  max_turns: Optional[int] = None,
67
- custom_output_extractor: Optional[Callable[[RunResult[Out]], Union[str, Awaitable[str]]]] = None,
68
- is_enabled: Union[bool, Callable[[Any, Agent[Ctx, Out]], bool], Callable[[Any, Agent[Ctx, Out]], Awaitable[bool]]] = True,
71
+ custom_output_extractor: Optional[
72
+ Callable[[RunResult[Out]], Union[str, Awaitable[str]]]
73
+ ] = None,
74
+ is_enabled: Union[
75
+ bool,
76
+ Callable[[Any, Agent[Ctx, Out]], bool],
77
+ Callable[[Any, Agent[Ctx, Out]], Awaitable[bool]],
78
+ ] = True,
69
79
  metadata: Optional[Dict[str, Any]] = None,
70
80
  timeout: Optional[float] = None,
71
- preserve_session: bool = False
81
+ preserve_session: bool = False,
72
82
  ) -> Tool[AgentToolInput, Ctx]:
73
83
  """
74
84
  Create a tool from an agent.
75
-
85
+
76
86
  Args:
77
87
  agent: The agent to convert into a tool
78
88
  tool_name: Optional custom name for the tool
@@ -82,15 +92,17 @@ def create_agent_tool(
82
92
  is_enabled: Whether the tool is enabled (bool, sync function, or async function)
83
93
  metadata: Optional metadata for the tool
84
94
  timeout: Optional timeout for tool execution
85
-
95
+
86
96
  Returns:
87
97
  A Tool that wraps the agent execution
88
98
  """
89
-
99
+
90
100
  # Default names and descriptions
91
101
  final_tool_name = tool_name or f"run_{agent.name.lower().replace(' ', '_')}"
92
- final_tool_description = tool_description or f"Execute the {agent.name} agent with the given input"
93
-
102
+ final_tool_description = (
103
+ tool_description or f"Execute the {agent.name} agent with the given input"
104
+ )
105
+
94
106
  # Create the tool schema
95
107
  if BaseModel and create_model:
96
108
  # Use Pydantic if available
@@ -102,67 +114,68 @@ def create_agent_tool(
102
114
  "properties": {
103
115
  "input": {"type": "string", "description": "The input message to send to the agent"}
104
116
  },
105
- "required": ["input"]
117
+ "required": ["input"],
106
118
  }
107
-
119
+
108
120
  tool_schema = ToolSchema(
109
121
  name=final_tool_name,
110
122
  description=final_tool_description,
111
123
  parameters=parameters_model,
112
- timeout=timeout
124
+ timeout=timeout,
113
125
  )
114
-
126
+
115
127
  async def _check_if_enabled(context: Ctx) -> bool:
116
128
  """Check if the tool is enabled based on the is_enabled parameter."""
117
129
  if isinstance(is_enabled, bool):
118
130
  return is_enabled
119
131
  elif callable(is_enabled):
120
132
  result = is_enabled(context, agent)
121
- if hasattr(result, '__await__'):
133
+ if hasattr(result, "__await__"):
122
134
  return await result
123
135
  return result
124
136
  return True
125
-
137
+
126
138
  async def _execute_agent_tool(args: AgentToolInput, context: Ctx) -> str:
127
139
  """Execute the agent and return the result."""
128
140
  # Check if tool is enabled
129
141
  if not await _check_if_enabled(context):
130
- return json.dumps({
131
- "error": "tool_disabled",
132
- "message": f"Tool {final_tool_name} is currently disabled"
133
- })
134
-
142
+ return json.dumps(
143
+ {
144
+ "error": "tool_disabled",
145
+ "message": f"Tool {final_tool_name} is currently disabled",
146
+ }
147
+ )
148
+
135
149
  # Extract input from args
136
- if hasattr(args, 'input'):
150
+ if hasattr(args, "input"):
137
151
  user_input = args.input
138
152
  elif isinstance(args, dict):
139
- user_input = args.get('input', '')
153
+ user_input = args.get("input", "")
140
154
  else:
141
155
  user_input = str(args)
142
-
156
+
143
157
  # Create initial state for the agent
144
- initial_messages = [Message(
145
- role=ContentRole.USER,
146
- content=user_input
147
- )]
148
-
158
+ initial_messages = [Message(role=ContentRole.USER, content=user_input)]
159
+
149
160
  initial_state = RunState(
150
161
  run_id=generate_run_id(),
151
162
  trace_id=generate_trace_id(),
152
163
  messages=initial_messages,
153
164
  current_agent_name=agent.name,
154
165
  context=context,
155
- turn_count=0
166
+ turn_count=0,
156
167
  )
157
-
168
+
158
169
  # Get the current RunConfig from context variable
159
170
  parent_config = _current_run_config.get()
160
171
  if parent_config is None:
161
172
  # If no parent config available, we can't execute the agent
162
- return json.dumps({
163
- "error": "no_parent_config",
164
- "message": f"Agent tool {final_tool_name} requires a parent RunConfig to execute. Please ensure the agent tool is called from within a JAF run context."
165
- })
173
+ return json.dumps(
174
+ {
175
+ "error": "no_parent_config",
176
+ "message": f"Agent tool {final_tool_name} requires a parent RunConfig to execute. Please ensure the agent tool is called from within a JAF run context.",
177
+ }
178
+ )
166
179
 
167
180
  # Create a sub-config that inherits from parent but uses this agent
168
181
  # Session inheritance is configurable via preserve_session.
@@ -179,13 +192,14 @@ def create_agent_tool(
179
192
  memory=parent_config.memory if preserve_session else None,
180
193
  conversation_id=parent_config.conversation_id if preserve_session else None,
181
194
  default_tool_timeout=parent_config.default_tool_timeout,
182
- prefer_streaming=parent_config.prefer_streaming
195
+ prefer_streaming=parent_config.prefer_streaming,
183
196
  )
184
197
 
185
198
  token = _current_run_config.set(sub_config)
186
199
  try:
187
200
  # Import here to avoid circular imports
188
201
  from . import engine
202
+
189
203
  # Execute the agent
190
204
  result = await engine.run(initial_state, sub_config)
191
205
  finally:
@@ -198,14 +212,16 @@ def create_agent_tool(
198
212
  if inspect.isawaitable(output):
199
213
  output = await output
200
214
  return str(output)
201
- if result.outcome.status == 'completed':
202
- if hasattr(result.outcome, 'output') and result.outcome.output is not None:
215
+ if result.outcome.status == "completed":
216
+ if hasattr(result.outcome, "output") and result.outcome.output is not None:
203
217
  return str(result.outcome.output)
204
218
  else:
205
219
  # Fall back to the last assistant message
206
220
  from .types import get_text_content
221
+
207
222
  assistant_messages = [
208
- msg for msg in result.final_state.messages
223
+ msg
224
+ for msg in result.final_state.messages
209
225
  if msg.role == ContentRole.ASSISTANT and get_text_content(msg.content)
210
226
  ]
211
227
  if assistant_messages:
@@ -213,43 +229,48 @@ def create_agent_tool(
213
229
  return "Agent completed successfully but produced no output"
214
230
  else:
215
231
  # Error case
216
- error_detail = getattr(result.outcome.error, 'detail', str(result.outcome.error))
217
- return json.dumps({
218
- "error": "agent_execution_failed",
219
- "message": f"Agent {agent.name} failed: {error_detail}"
220
- })
232
+ error_detail = getattr(result.outcome.error, "detail", str(result.outcome.error))
233
+ return json.dumps(
234
+ {
235
+ "error": "agent_execution_failed",
236
+ "message": f"Agent {agent.name} failed: {error_detail}",
237
+ }
238
+ )
221
239
  except Exception as e:
222
- return json.dumps({
223
- "error": "agent_tool_error",
224
- "message": f"Error executing agent {agent.name}: {str(e)}"
225
- })
226
-
240
+ return json.dumps(
241
+ {
242
+ "error": "agent_tool_error",
243
+ "message": f"Error executing agent {agent.name}: {str(e)}",
244
+ }
245
+ )
246
+
227
247
  # Create the tool wrapper
228
248
  class AgentTool:
229
249
  def __init__(self):
230
250
  self.schema = tool_schema
231
251
  self.metadata = metadata or {"source": "agent", "agent_name": agent.name}
232
252
  self.source = ToolSource.NATIVE
233
-
253
+
234
254
  async def execute(self, args: AgentToolInput, context: Ctx) -> str:
235
255
  """Execute the agent tool."""
236
256
  return await _execute_agent_tool(args, context)
237
-
257
+
238
258
  return AgentTool()
239
259
 
240
260
 
241
261
  def create_default_output_extractor(extract_json: bool = False) -> Callable[[RunResult], str]:
242
262
  """
243
263
  Create a default output extractor function.
244
-
264
+
245
265
  Args:
246
266
  extract_json: If True, attempts to extract JSON from the output
247
-
267
+
248
268
  Returns:
249
269
  An output extractor function
250
270
  """
271
+
251
272
  def extractor(run_result: RunResult) -> str:
252
- if run_result.outcome.status == 'completed':
273
+ if run_result.outcome.status == "completed":
253
274
  output = run_result.outcome.output
254
275
  if extract_json and isinstance(output, str):
255
276
  try:
@@ -262,57 +283,57 @@ def create_default_output_extractor(extract_json: bool = False) -> Callable[[Run
262
283
  return str(output) if output is not None else ""
263
284
  else:
264
285
  # Return error information
265
- error_detail = getattr(run_result.outcome.error, 'detail', str(run_result.outcome.error))
266
- return json.dumps({
267
- "error": True,
268
- "message": error_detail
269
- })
270
-
286
+ error_detail = getattr(
287
+ run_result.outcome.error, "detail", str(run_result.outcome.error)
288
+ )
289
+ return json.dumps({"error": True, "message": error_detail})
290
+
271
291
  return extractor
272
292
 
273
293
 
274
294
  def create_json_output_extractor() -> Callable[[RunResult], str]:
275
295
  """
276
296
  Create an output extractor that specifically looks for JSON in the agent's output.
277
-
297
+
278
298
  Returns:
279
299
  An output extractor that finds and returns JSON content
280
300
  """
301
+
281
302
  def json_extractor(run_result: RunResult) -> str:
282
303
  # Scan the agent's outputs in reverse order until we find a JSON-like message
283
304
  for message in reversed(run_result.final_state.messages):
284
305
  if message.role == ContentRole.ASSISTANT and get_text_content(message.content):
285
306
  content = get_text_content(message.content).strip()
286
- if content.startswith('{') or content.startswith('['):
307
+ if content.startswith("{") or content.startswith("["):
287
308
  try:
288
309
  # Validate it's proper JSON
289
310
  json.loads(content)
290
311
  return content
291
312
  except (json.JSONDecodeError, TypeError):
292
313
  continue
293
-
314
+
294
315
  # Fallback to empty JSON object if nothing was found
295
316
  return "{}"
296
-
317
+
297
318
  return json_extractor
298
319
 
299
320
 
300
321
  # Convenience function for conditional tool enabling
301
322
  def create_conditional_enabler(
302
- condition_key: str,
303
- expected_value: Any = True
323
+ condition_key: str, expected_value: Any = True
304
324
  ) -> Callable[[Any, Agent], bool]:
305
325
  """
306
326
  Create a conditional enabler function based on context attributes.
307
-
327
+
308
328
  Args:
309
329
  condition_key: The key to check in the context
310
330
  expected_value: The expected value for the condition
311
-
331
+
312
332
  Returns:
313
333
  A function that checks if the tool should be enabled
314
334
  """
335
+
315
336
  def enabler(context: Any, agent: Agent) -> bool:
316
337
  return getattr(context, condition_key, None) == expected_value
317
-
318
- return enabler
338
+
339
+ return enabler