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/providers/mcp.py CHANGED
@@ -23,7 +23,8 @@ from fastmcp.client.transports import (
23
23
  from ..core.tool_results import ToolErrorCodes, ToolResult, ToolResultStatus
24
24
  from ..core.types import ToolSchema
25
25
 
26
- Ctx = TypeVar('Ctx')
26
+ Ctx = TypeVar("Ctx")
27
+
27
28
 
28
29
  def _json_schema_to_python_type(schema: Dict[str, Any]) -> type:
29
30
  """Maps JSON schema types to Python types for Pydantic model creation."""
@@ -65,14 +66,24 @@ def _json_schema_to_python_type(schema: Dict[str, Any]) -> type:
65
66
  # Fallback
66
67
  return Any
67
68
 
69
+
68
70
  class MCPToolArgs(BaseModel):
69
71
  """Base class for MCP tool arguments."""
72
+
70
73
  pass
71
74
 
75
+
72
76
  class FastMCPTool:
73
77
  """A tool that proxies to a FastMCP server, managing its own session."""
74
78
 
75
- def __init__(self, transport: Union[StdioTransport, SSETransport, StreamableHttpTransport], tool_info: mcp.types.Tool, args_model: type[BaseModel], client_info: mcp.types.Implementation, timeout: Optional[float] = None):
79
+ def __init__(
80
+ self,
81
+ transport: Union[StdioTransport, SSETransport, StreamableHttpTransport],
82
+ tool_info: mcp.types.Tool,
83
+ args_model: type[BaseModel],
84
+ client_info: mcp.types.Implementation,
85
+ timeout: Optional[float] = None,
86
+ ):
76
87
  self.transport = transport
77
88
  self.tool_name = tool_info.name
78
89
  self.args_model = args_model
@@ -82,7 +93,7 @@ class FastMCPTool:
82
93
  name=tool_info.name,
83
94
  description=tool_info.description or f"MCP tool: {tool_info.name}",
84
95
  parameters=args_model,
85
- timeout=timeout
96
+ timeout=timeout,
86
97
  )
87
98
 
88
99
  @property
@@ -92,15 +103,15 @@ class FastMCPTool:
92
103
  def _convert_simple_filters_to_flat_filter(self, simple_filters: dict) -> dict:
93
104
  """
94
105
  Converts a "simple filter" dictionary to the "FlatFilter" format required by some tools.
95
-
106
+
96
107
  Simple filters are dictionaries mapping field names to values, e.g.:
97
108
  {"status": "active", "category": ["A", "B"]}
98
109
  Each key is a field name, and each value is either a single value or a list of values to match.
99
-
110
+
100
111
  Flat filters are a more structured format with two keys:
101
112
  - "clauses": a list of filter conditions, each specifying a field, a condition (e.g., "In"), and a list of values.
102
113
  - "logic": a string expressing how to combine the clauses (e.g., "0 AND 1").
103
-
114
+
104
115
  Example output:
105
116
  {
106
117
  "clauses": [
@@ -109,49 +120,47 @@ class FastMCPTool:
109
120
  ],
110
121
  "logic": "0 AND 1"
111
122
  }
112
-
123
+
113
124
  This conversion is needed for tools that expect filters in FlatFilter format rather than as simple key-value pairs.
114
-
125
+
115
126
  Args:
116
127
  simple_filters (dict): A dictionary of field names to values (simple filter).
117
-
128
+
118
129
  Returns:
119
130
  dict: The filter in FlatFilter format.
120
131
  """
121
132
  if not simple_filters:
122
133
  return simple_filters
123
-
134
+
124
135
  clauses = []
125
136
  for i, (field, value) in enumerate(simple_filters.items()):
126
137
  # Convert single values to lists for "In" condition
127
138
  if not isinstance(value, list):
128
139
  value = [value]
129
-
130
- clauses.append({
131
- "field": field,
132
- "condition": "In",
133
- "val": value
134
- })
135
-
140
+
141
+ clauses.append({"field": field, "condition": "In", "val": value})
142
+
136
143
  # Create logic string: "0 AND 1 AND 2..." for all clauses
137
144
  logic = " AND ".join(str(i) for i in range(len(clauses)))
138
-
139
- return {
140
- "clauses": clauses,
141
- "logic": logic
142
- }
145
+
146
+ return {"clauses": clauses, "logic": logic}
143
147
 
144
148
  def _transform_arguments_for_tool(self, args_dict: dict) -> dict:
145
149
  """Transform arguments based on tool-specific requirements."""
146
150
  # Handle flatFilters transformation for tools that expect FlatFilter schema
147
- if ('flatFilters' in args_dict and
148
- isinstance(args_dict['flatFilters'], dict) and
149
- 'clauses' not in args_dict['flatFilters']):
150
-
151
- logging.info(f"[JAF MCP] Converting simple flatFilters to FlatFilter format for {self.tool_name}")
152
- args_dict['flatFilters'] = self._convert_simple_filters_to_flat_filter(args_dict['flatFilters'])
151
+ if (
152
+ "flatFilters" in args_dict
153
+ and isinstance(args_dict["flatFilters"], dict)
154
+ and "clauses" not in args_dict["flatFilters"]
155
+ ):
156
+ logging.info(
157
+ f"[JAF MCP] Converting simple flatFilters to FlatFilter format for {self.tool_name}"
158
+ )
159
+ args_dict["flatFilters"] = self._convert_simple_filters_to_flat_filter(
160
+ args_dict["flatFilters"]
161
+ )
153
162
  logging.info(f"[JAF MCP] Converted flatFilters: {args_dict['flatFilters']}")
154
-
163
+
155
164
  return args_dict
156
165
 
157
166
  async def execute(self, args: MCPToolArgs, context: Ctx) -> ToolResult:
@@ -160,14 +169,15 @@ class FastMCPTool:
160
169
  async with client:
161
170
  # Only include fields that were explicitly set, not defaults
162
171
  args_dict = args.model_dump(exclude_none=True, exclude_unset=True)
163
-
172
+
164
173
  # Apply tool-specific argument transformations
165
174
  args_dict = self._transform_arguments_for_tool(args_dict)
166
-
175
+
167
176
  result = await client.call_tool_mcp(self.tool_name, arguments=args_dict)
168
177
 
169
178
  if result.isError:
170
179
  from ..core.tool_results import ToolErrorInfo
180
+
171
181
  error_message = "MCP tool execution failed"
172
182
  if result.content and isinstance(result.content[0], mcp.types.TextContent):
173
183
  error_message = result.content[0].text
@@ -176,34 +186,40 @@ class FastMCPTool:
176
186
  error=ToolErrorInfo(
177
187
  code=ToolErrorCodes.EXECUTION_FAILED,
178
188
  message=error_message,
179
- details={"mcp_error": result.structuredContent}
180
- )
189
+ details={"mcp_error": result.structuredContent},
190
+ ),
181
191
  )
182
-
192
+
183
193
  data = result.structuredContent if result.structuredContent else str(result.content)
184
-
194
+
185
195
  # Create proper ToolMetadata object
186
196
  from ..core.tool_results import ToolMetadata
197
+
187
198
  tool_metadata = ToolMetadata(extra={"mcp_response": str(result)})
188
-
199
+
189
200
  return ToolResult(
190
- status=ToolResultStatus.SUCCESS,
191
- data=str(data),
192
- metadata=tool_metadata
201
+ status=ToolResultStatus.SUCCESS, data=str(data), metadata=tool_metadata
193
202
  )
194
203
 
195
204
  except Exception as e:
196
205
  from ..core.tool_results import ToolErrorInfo
206
+
197
207
  return ToolResult(
198
208
  status=ToolResultStatus.ERROR,
199
209
  error=ToolErrorInfo(
200
210
  code=ToolErrorCodes.EXECUTION_FAILED,
201
211
  message=f"MCP tool execution failed: {e!s}",
202
- details={"error": str(e)}
203
- )
212
+ details={"error": str(e)},
213
+ ),
204
214
  )
205
215
 
206
- async def create_tools_from_transport(transport: Union[StdioTransport, SSETransport, StreamableHttpTransport], client_info: mcp.types.Implementation, extra_fields: Optional[Dict[str, Any]] = None, default_timeout: Optional[float] = None) -> List[FastMCPTool]:
216
+
217
+ async def create_tools_from_transport(
218
+ transport: Union[StdioTransport, SSETransport, StreamableHttpTransport],
219
+ client_info: mcp.types.Implementation,
220
+ extra_fields: Optional[Dict[str, Any]] = None,
221
+ default_timeout: Optional[float] = None,
222
+ ) -> List[FastMCPTool]:
207
223
  """Create JAF tools from an MCP transport."""
208
224
  client = Client(transport, client_info=client_info)
209
225
  tools = []
@@ -216,11 +232,18 @@ async def create_tools_from_transport(transport: Union[StdioTransport, SSETransp
216
232
  # Support both inputSchema (camelCase) and input_schema (snake_case) for compatibility with different MCP implementations.
217
233
  camel_schema = getattr(tool_info, "inputSchema", None)
218
234
  snake_schema = getattr(tool_info, "input_schema", None)
219
-
235
+
220
236
  # Choose schema with preference for camelCase when both are non-empty dicts
221
237
  if camel_schema and snake_schema:
222
- if isinstance(camel_schema, dict) and isinstance(snake_schema, dict) and camel_schema and snake_schema:
223
- logging.info(f"Both 'inputSchema' and 'input_schema' are present for tool '{tool_info.name}'; preferring 'inputSchema'.")
238
+ if (
239
+ isinstance(camel_schema, dict)
240
+ and isinstance(snake_schema, dict)
241
+ and camel_schema
242
+ and snake_schema
243
+ ):
244
+ logging.info(
245
+ f"Both 'inputSchema' and 'input_schema' are present for tool '{tool_info.name}'; preferring 'inputSchema'."
246
+ )
224
247
  params_schema = camel_schema
225
248
  elif camel_schema:
226
249
  params_schema = camel_schema
@@ -232,7 +255,7 @@ async def create_tools_from_transport(transport: Union[StdioTransport, SSETransp
232
255
  params_schema = snake_schema
233
256
  else:
234
257
  params_schema = {}
235
-
258
+
236
259
  # Ensure params_schema is a dict before accessing .get
237
260
  params_schema = params_schema or {}
238
261
  properties = params_schema.get("properties", {})
@@ -246,7 +269,7 @@ async def create_tools_from_transport(transport: Union[StdioTransport, SSETransp
246
269
  else:
247
270
  # Don't set default values - let them be None so exclude_unset works
248
271
  fields[param_name] = (Optional[param_type], None)
249
-
272
+
250
273
  # Add any extra fields to all tool schemas if not already present
251
274
  if extra_fields:
252
275
  for field_name, field_type in extra_fields.items():
@@ -258,12 +281,21 @@ async def create_tools_from_transport(transport: Union[StdioTransport, SSETransp
258
281
  **fields,
259
282
  __base__=MCPToolArgs,
260
283
  )
261
- tools.append(FastMCPTool(transport, tool_info, ArgsModel, client_info, default_timeout))
284
+ tools.append(
285
+ FastMCPTool(transport, tool_info, ArgsModel, client_info, default_timeout)
286
+ )
262
287
  except Exception as e:
263
288
  logging.error(f"Failed to create MCP tools: {e}")
264
289
  return tools
265
290
 
266
- async def create_mcp_stdio_tools(command: List[str], client_name: str = "JAF", client_version: str = "2.0.0", extra_fields: Optional[Dict[str, Any]] = None, default_timeout: Optional[float] = None) -> List[FastMCPTool]:
291
+
292
+ async def create_mcp_stdio_tools(
293
+ command: List[str],
294
+ client_name: str = "JAF",
295
+ client_version: str = "2.0.0",
296
+ extra_fields: Optional[Dict[str, Any]] = None,
297
+ default_timeout: Optional[float] = None,
298
+ ) -> List[FastMCPTool]:
267
299
  if not command:
268
300
  raise ValueError("Command list must not be empty for MCP stdio transport.")
269
301
  # Add juspay_meta_info by default for backward compatibility
@@ -273,7 +305,14 @@ async def create_mcp_stdio_tools(command: List[str], client_name: str = "JAF", c
273
305
  client_info = mcp.types.Implementation(name=client_name, version=client_version)
274
306
  return await create_tools_from_transport(transport, client_info, extra_fields, default_timeout)
275
307
 
276
- async def create_mcp_sse_tools(uri: str, client_name: str = "JAF", client_version: str = "2.0.0", extra_fields: Optional[Dict[str, Any]] = None, default_timeout: Optional[float] = None) -> List[FastMCPTool]:
308
+
309
+ async def create_mcp_sse_tools(
310
+ uri: str,
311
+ client_name: str = "JAF",
312
+ client_version: str = "2.0.0",
313
+ extra_fields: Optional[Dict[str, Any]] = None,
314
+ default_timeout: Optional[float] = None,
315
+ ) -> List[FastMCPTool]:
277
316
  # Add juspay_meta_info by default for backward compatibility
278
317
  if extra_fields is None:
279
318
  extra_fields = {"juspay_meta_info": Dict[str, Any]}
@@ -281,7 +320,14 @@ async def create_mcp_sse_tools(uri: str, client_name: str = "JAF", client_versio
281
320
  client_info = mcp.types.Implementation(name=client_name, version=client_version)
282
321
  return await create_tools_from_transport(transport, client_info, extra_fields, default_timeout)
283
322
 
284
- async def create_mcp_http_tools(uri: str, client_name: str = "JAF", client_version: str = "2.0.0", extra_fields: Optional[Dict[str, Any]] = None, default_timeout: Optional[float] = None) -> List[FastMCPTool]:
323
+
324
+ async def create_mcp_http_tools(
325
+ uri: str,
326
+ client_name: str = "JAF",
327
+ client_version: str = "2.0.0",
328
+ extra_fields: Optional[Dict[str, Any]] = None,
329
+ default_timeout: Optional[float] = None,
330
+ ) -> List[FastMCPTool]:
285
331
  # Add juspay_meta_info by default for backward compatibility
286
332
  if extra_fields is None:
287
333
  extra_fields = {"juspay_meta_info": Dict[str, Any]}