jaf-py 2.1.2__tar.gz → 2.2.1__tar.gz

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 (120) hide show
  1. {jaf_py-2.1.2/jaf_py.egg-info → jaf_py-2.2.1}/PKG-INFO +11 -2
  2. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/__init__.py +14 -0
  3. jaf_py-2.2.1/jaf/core/agent_tool.py +315 -0
  4. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/engine.py +158 -41
  5. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/streaming.py +4 -3
  6. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/tools.py +15 -16
  7. jaf_py-2.2.1/jaf/core/tracing.py +583 -0
  8. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/types.py +99 -5
  9. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/providers/mcp.py +10 -8
  10. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/providers/model.py +16 -1
  11. {jaf_py-2.1.2 → jaf_py-2.2.1/jaf_py.egg-info}/PKG-INFO +11 -2
  12. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/SOURCES.txt +3 -0
  13. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/requires.txt +11 -1
  14. {jaf_py-2.1.2 → jaf_py-2.2.1}/pyproject.toml +12 -2
  15. jaf_py-2.2.1/tests/test_conversation_id_fix.py +190 -0
  16. jaf_py-2.2.1/tests/test_session_continuity.py +254 -0
  17. jaf_py-2.1.2/jaf/core/tracing.py +0 -265
  18. {jaf_py-2.1.2 → jaf_py-2.2.1}/LICENSE +0 -0
  19. {jaf_py-2.1.2 → jaf_py-2.2.1}/README.md +0 -0
  20. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/__init__.py +0 -0
  21. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/__init__.py +0 -0
  22. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/agent.py +0 -0
  23. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/agent_card.py +0 -0
  24. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/client.py +0 -0
  25. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/__init__.py +0 -0
  26. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/client_example.py +0 -0
  27. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/integration_example.py +0 -0
  28. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/rag_demo/__init__.py +0 -0
  29. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/server_demo/__init__.py +0 -0
  30. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/server_example.py +0 -0
  31. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/__init__.py +0 -0
  32. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/cleanup.py +0 -0
  33. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/factory.py +0 -0
  34. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/__init__.py +0 -0
  35. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/composite.py +0 -0
  36. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/in_memory.py +0 -0
  37. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/postgres.py +0 -0
  38. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/redis.py +0 -0
  39. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/serialization.py +0 -0
  40. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/__init__.py +0 -0
  41. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/run_comprehensive_tests.py +0 -0
  42. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/test_cleanup.py +0 -0
  43. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/test_serialization.py +0 -0
  44. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/test_stress_concurrency.py +0 -0
  45. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/test_task_lifecycle.py +0 -0
  46. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/types.py +0 -0
  47. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/protocol.py +0 -0
  48. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/server.py +0 -0
  49. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/standalone_client.py +0 -0
  50. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/__init__.py +0 -0
  51. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/run_tests.py +0 -0
  52. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_agent.py +0 -0
  53. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_client.py +0 -0
  54. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_integration.py +0 -0
  55. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_protocol.py +0 -0
  56. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_types.py +0 -0
  57. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/types.py +0 -0
  58. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/cli.py +0 -0
  59. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/analytics.py +0 -0
  60. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/composition.py +0 -0
  61. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/errors.py +0 -0
  62. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/performance.py +0 -0
  63. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/tool_results.py +0 -0
  64. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/workflows.py +0 -0
  65. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/exceptions.py +0 -0
  66. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/__init__.py +0 -0
  67. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/factory.py +0 -0
  68. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/providers/__init__.py +0 -0
  69. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/providers/in_memory.py +0 -0
  70. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/providers/postgres.py +0 -0
  71. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/providers/redis.py +0 -0
  72. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/types.py +0 -0
  73. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/utils.py +0 -0
  74. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/plugins/__init__.py +0 -0
  75. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/plugins/base.py +0 -0
  76. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/policies/__init__.py +0 -0
  77. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/policies/handoff.py +0 -0
  78. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/policies/validation.py +0 -0
  79. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/providers/__init__.py +0 -0
  80. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/server/__init__.py +0 -0
  81. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/server/main.py +0 -0
  82. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/server/server.py +0 -0
  83. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/server/types.py +0 -0
  84. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/__init__.py +0 -0
  85. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/example.py +0 -0
  86. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/functional_core.py +0 -0
  87. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/graphviz.py +0 -0
  88. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/imperative_shell.py +0 -0
  89. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/types.py +0 -0
  90. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/dependency_links.txt +0 -0
  91. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/entry_points.txt +0 -0
  92. {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/top_level.txt +0 -0
  93. {jaf_py-2.1.2 → jaf_py-2.2.1}/setup.cfg +0 -0
  94. {jaf_py-2.1.2 → jaf_py-2.2.1}/setup.py +0 -0
  95. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_a2a_deep.py +0 -0
  96. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_a2a_examples.py +0 -0
  97. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_api_reference_examples.py +0 -0
  98. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_callback_system_examples.py +0 -0
  99. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_coffee_tool.py +0 -0
  100. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_deployment_examples.py +0 -0
  101. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_docs_code_examples.py +0 -0
  102. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_engine.py +0 -0
  103. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_engine_manual.py +0 -0
  104. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_error_handling_examples.py +0 -0
  105. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_getting_started_examples.py +0 -0
  106. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_math_tool.py +0 -0
  107. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_mcp_comprehensive.py +0 -0
  108. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_mcp_docs.py +0 -0
  109. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_mcp_real_functionality.py +0 -0
  110. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_mcp_transports.py +0 -0
  111. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_memory_system_examples.py +0 -0
  112. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_model_providers_examples.py +0 -0
  113. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_property_based.py +0 -0
  114. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_redis_fixes.py +0 -0
  115. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_redis_memory.py +0 -0
  116. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_server_api_examples.py +0 -0
  117. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_streamable_http_mcp_example.py +0 -0
  118. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_timeout_functionality.py +0 -0
  119. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_tool_integration.py +0 -0
  120. {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaf-py
3
- Version: 2.1.2
3
+ Version: 2.2.1
4
4
  Summary: A purely functional agent framework with immutable state and composable tools - Python implementation
5
5
  Author: JAF Contributors
6
6
  Maintainer: JAF Contributors
@@ -39,6 +39,15 @@ Requires-Dist: google-auth>=2.20.0
39
39
  Requires-Dist: python-dotenv>=1.0.0
40
40
  Requires-Dist: psutil>=5.9.0
41
41
  Requires-Dist: fastmcp>=0.1.0
42
+ Requires-Dist: opentelemetry-api>=1.22.0
43
+ Requires-Dist: opentelemetry-sdk>=1.22.0
44
+ Requires-Dist: opentelemetry-exporter-otlp>=1.22.0
45
+ Requires-Dist: langfuse<3.0.0
46
+ Provides-Extra: tracing
47
+ Requires-Dist: opentelemetry-api>=1.22.0; extra == "tracing"
48
+ Requires-Dist: opentelemetry-sdk>=1.22.0; extra == "tracing"
49
+ Requires-Dist: opentelemetry-exporter-otlp>=1.22.0; extra == "tracing"
50
+ Requires-Dist: langfuse<3.0.0; extra == "tracing"
42
51
  Provides-Extra: dev
43
52
  Requires-Dist: pytest>=7.0.0; extra == "dev"
44
53
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -57,7 +66,7 @@ Requires-Dist: psycopg2-binary>=2.9.0; extra == "memory"
57
66
  Provides-Extra: visualization
58
67
  Requires-Dist: graphviz>=0.20.0; extra == "visualization"
59
68
  Provides-Extra: all
60
- Requires-Dist: jaf-py[dev,memory,server,visualization]; extra == "all"
69
+ Requires-Dist: jaf-py[dev,memory,server,tracing,visualization]; extra == "all"
61
70
  Dynamic: license-file
62
71
 
63
72
  # JAF (Juspay Agent Framework) - Python Implementation
@@ -5,6 +5,14 @@ from .errors import JAFError
5
5
  from .tool_results import *
6
6
  from .tracing import ConsoleTraceCollector, TraceCollector
7
7
  from .types import *
8
+ from .agent_tool import (
9
+ create_agent_tool,
10
+ create_default_output_extractor,
11
+ create_json_output_extractor,
12
+ create_conditional_enabler,
13
+ get_current_run_config,
14
+ set_current_run_config,
15
+ )
8
16
 
9
17
  __all__ = [
10
18
  "Agent",
@@ -27,10 +35,16 @@ __all__ = [
27
35
  "TraceEvent",
28
36
  "TraceId",
29
37
  "ValidationResult",
38
+ "create_agent_tool",
39
+ "create_conditional_enabler",
40
+ "create_default_output_extractor",
41
+ "create_json_output_extractor",
30
42
  "create_run_id",
31
43
  "create_trace_id",
44
+ "get_current_run_config",
32
45
  "require_permissions",
33
46
  "run",
47
+ "set_current_run_config",
34
48
  "tool_result_to_string",
35
49
  "with_error_handling",
36
50
  ]
@@ -0,0 +1,315 @@
1
+ """
2
+ Agent-as-tool implementation for JAF framework.
3
+
4
+ This module provides functionality to convert agents into tools that can be used
5
+ by other agents, enabling hierarchical agent orchestration patterns.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import inspect
11
+ import inspect
12
+ import contextvars
13
+ from typing import Any, Callable, Dict, List, Optional, Union, Awaitable, TypeVar, get_type_hints
14
+
15
+ try:
16
+ from pydantic import BaseModel, create_model
17
+ except ImportError:
18
+ BaseModel = None
19
+ create_model = None
20
+
21
+ from .types import (
22
+ Agent,
23
+ Tool,
24
+ ToolSchema,
25
+ ToolSource,
26
+ RunConfig,
27
+ RunState,
28
+ RunResult,
29
+ Message,
30
+ ContentRole,
31
+ generate_run_id,
32
+ generate_trace_id,
33
+ )
34
+
35
+ Ctx = TypeVar('Ctx')
36
+ Out = TypeVar('Out')
37
+
38
+ # Context variable to store the current RunConfig for agent tools
39
+ _current_run_config: contextvars.ContextVar[Optional[RunConfig]] = contextvars.ContextVar('current_run_config', default=None)
40
+
41
+
42
+ def set_current_run_config(config: RunConfig) -> None:
43
+ """Set the current RunConfig in context for agent tools to use."""
44
+ _current_run_config.set(config)
45
+
46
+
47
+ def get_current_run_config() -> Optional[RunConfig]:
48
+ """Get the current RunConfig from context."""
49
+ return _current_run_config.get()
50
+
51
+
52
+ class AgentToolInput(BaseModel if BaseModel else object):
53
+ """Input parameters for agent tools."""
54
+ input: str
55
+
56
+ if not BaseModel:
57
+ def __init__(self, input: str):
58
+ self.input = input
59
+
60
+
61
+ def create_agent_tool(
62
+ agent: Agent[Ctx, Out],
63
+ tool_name: Optional[str] = None,
64
+ tool_description: Optional[str] = None,
65
+ max_turns: Optional[int] = None,
66
+ custom_output_extractor: Optional[Callable[[RunResult[Out]], Union[str, Awaitable[str]]]] = None,
67
+ is_enabled: Union[bool, Callable[[Any, Agent[Ctx, Out]], bool], Callable[[Any, Agent[Ctx, Out]], Awaitable[bool]]] = True,
68
+ metadata: Optional[Dict[str, Any]] = None,
69
+ timeout: Optional[float] = None,
70
+ preserve_session: bool = False
71
+ ) -> Tool[AgentToolInput, Ctx]:
72
+ """
73
+ Create a tool from an agent.
74
+
75
+ Args:
76
+ agent: The agent to convert into a tool
77
+ tool_name: Optional custom name for the tool
78
+ tool_description: Optional custom description for the tool
79
+ max_turns: Maximum turns for agent execution
80
+ custom_output_extractor: Optional function to extract output from RunResult
81
+ is_enabled: Whether the tool is enabled (bool, sync function, or async function)
82
+ metadata: Optional metadata for the tool
83
+ timeout: Optional timeout for tool execution
84
+
85
+ Returns:
86
+ A Tool that wraps the agent execution
87
+ """
88
+
89
+ # Default names and descriptions
90
+ final_tool_name = tool_name or f"run_{agent.name.lower().replace(' ', '_')}"
91
+ final_tool_description = tool_description or f"Execute the {agent.name} agent with the given input"
92
+
93
+ # Create the tool schema
94
+ if BaseModel and create_model:
95
+ # Use Pydantic if available
96
+ parameters_model = AgentToolInput
97
+ else:
98
+ # Fallback schema for when Pydantic is not available
99
+ parameters_model = {
100
+ "type": "object",
101
+ "properties": {
102
+ "input": {"type": "string", "description": "The input message to send to the agent"}
103
+ },
104
+ "required": ["input"]
105
+ }
106
+
107
+ tool_schema = ToolSchema(
108
+ name=final_tool_name,
109
+ description=final_tool_description,
110
+ parameters=parameters_model,
111
+ timeout=timeout
112
+ )
113
+
114
+ async def _check_if_enabled(context: Ctx) -> bool:
115
+ """Check if the tool is enabled based on the is_enabled parameter."""
116
+ if isinstance(is_enabled, bool):
117
+ return is_enabled
118
+ elif callable(is_enabled):
119
+ result = is_enabled(context, agent)
120
+ if hasattr(result, '__await__'):
121
+ return await result
122
+ return result
123
+ return True
124
+
125
+ async def _execute_agent_tool(args: AgentToolInput, context: Ctx) -> str:
126
+ """Execute the agent and return the result."""
127
+ # Check if tool is enabled
128
+ if not await _check_if_enabled(context):
129
+ return json.dumps({
130
+ "error": "tool_disabled",
131
+ "message": f"Tool {final_tool_name} is currently disabled"
132
+ })
133
+
134
+ # Extract input from args
135
+ if hasattr(args, 'input'):
136
+ user_input = args.input
137
+ elif isinstance(args, dict):
138
+ user_input = args.get('input', '')
139
+ else:
140
+ user_input = str(args)
141
+
142
+ # Create initial state for the agent
143
+ initial_messages = [Message(
144
+ role=ContentRole.USER,
145
+ content=user_input
146
+ )]
147
+
148
+ initial_state = RunState(
149
+ run_id=generate_run_id(),
150
+ trace_id=generate_trace_id(),
151
+ messages=initial_messages,
152
+ current_agent_name=agent.name,
153
+ context=context,
154
+ turn_count=0
155
+ )
156
+
157
+ # Get the current RunConfig from context variable
158
+ parent_config = _current_run_config.get()
159
+ if parent_config is None:
160
+ # If no parent config available, we can't execute the agent
161
+ return json.dumps({
162
+ "error": "no_parent_config",
163
+ "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."
164
+ })
165
+
166
+ # Create a sub-config that inherits from parent but uses this agent
167
+ # Session inheritance is configurable via preserve_session.
168
+ # - When True: inherit parent's conversation_id and memory (shared memory/session)
169
+ # - When False: do not inherit (ephemeral, per-invocation sub-agent run)
170
+ sub_config = RunConfig(
171
+ agent_registry={agent.name: agent, **parent_config.agent_registry},
172
+ model_provider=parent_config.model_provider,
173
+ max_turns=max_turns or parent_config.max_turns,
174
+ model_override=parent_config.model_override,
175
+ initial_input_guardrails=parent_config.initial_input_guardrails,
176
+ final_output_guardrails=parent_config.final_output_guardrails,
177
+ on_event=parent_config.on_event,
178
+ memory=parent_config.memory if preserve_session else None,
179
+ conversation_id=parent_config.conversation_id if preserve_session else None,
180
+ default_tool_timeout=parent_config.default_tool_timeout
181
+ )
182
+
183
+ token = _current_run_config.set(sub_config)
184
+ try:
185
+ # Import here to avoid circular imports
186
+ from . import engine
187
+ # Execute the agent
188
+ result = await engine.run(initial_state, sub_config)
189
+ finally:
190
+ _current_run_config.reset(token)
191
+
192
+ # Output extraction and error handling
193
+ try:
194
+ if custom_output_extractor:
195
+ output = custom_output_extractor(result)
196
+ if inspect.isawaitable(output):
197
+ output = await output
198
+ return str(output)
199
+ if result.outcome.status == 'completed':
200
+ if hasattr(result.outcome, 'output') and result.outcome.output is not None:
201
+ return str(result.outcome.output)
202
+ else:
203
+ # Fall back to the last assistant message
204
+ assistant_messages = [
205
+ msg for msg in result.final_state.messages
206
+ if msg.role == ContentRole.ASSISTANT and msg.content
207
+ ]
208
+ if assistant_messages:
209
+ return assistant_messages[-1].content
210
+ return "Agent completed successfully but produced no output"
211
+ else:
212
+ # Error case
213
+ error_detail = getattr(result.outcome.error, 'detail', str(result.outcome.error))
214
+ return json.dumps({
215
+ "error": "agent_execution_failed",
216
+ "message": f"Agent {agent.name} failed: {error_detail}"
217
+ })
218
+ except Exception as e:
219
+ return json.dumps({
220
+ "error": "agent_tool_error",
221
+ "message": f"Error executing agent {agent.name}: {str(e)}"
222
+ })
223
+
224
+ # Create the tool wrapper
225
+ class AgentTool:
226
+ def __init__(self):
227
+ self.schema = tool_schema
228
+ self.metadata = metadata or {"source": "agent", "agent_name": agent.name}
229
+ self.source = ToolSource.NATIVE
230
+
231
+ async def execute(self, args: AgentToolInput, context: Ctx) -> str:
232
+ """Execute the agent tool."""
233
+ return await _execute_agent_tool(args, context)
234
+
235
+ return AgentTool()
236
+
237
+
238
+ def create_default_output_extractor(extract_json: bool = False) -> Callable[[RunResult], str]:
239
+ """
240
+ Create a default output extractor function.
241
+
242
+ Args:
243
+ extract_json: If True, attempts to extract JSON from the output
244
+
245
+ Returns:
246
+ An output extractor function
247
+ """
248
+ def extractor(run_result: RunResult) -> str:
249
+ if run_result.outcome.status == 'completed':
250
+ output = run_result.outcome.output
251
+ if extract_json and isinstance(output, str):
252
+ try:
253
+ # Try to parse as JSON and re-serialize for consistency
254
+ parsed = json.loads(output)
255
+ return json.dumps(parsed)
256
+ except (json.JSONDecodeError, TypeError):
257
+ # If parsing fails, return the original output
258
+ pass
259
+ return str(output) if output is not None else ""
260
+ else:
261
+ # Return error information
262
+ error_detail = getattr(run_result.outcome.error, 'detail', str(run_result.outcome.error))
263
+ return json.dumps({
264
+ "error": True,
265
+ "message": error_detail
266
+ })
267
+
268
+ return extractor
269
+
270
+
271
+ def create_json_output_extractor() -> Callable[[RunResult], str]:
272
+ """
273
+ Create an output extractor that specifically looks for JSON in the agent's output.
274
+
275
+ Returns:
276
+ An output extractor that finds and returns JSON content
277
+ """
278
+ def json_extractor(run_result: RunResult) -> str:
279
+ # Scan the agent's outputs in reverse order until we find a JSON-like message
280
+ for message in reversed(run_result.final_state.messages):
281
+ if message.role == ContentRole.ASSISTANT and message.content:
282
+ content = message.content.strip()
283
+ if content.startswith('{') or content.startswith('['):
284
+ try:
285
+ # Validate it's proper JSON
286
+ json.loads(content)
287
+ return content
288
+ except (json.JSONDecodeError, TypeError):
289
+ continue
290
+
291
+ # Fallback to empty JSON object if nothing was found
292
+ return "{}"
293
+
294
+ return json_extractor
295
+
296
+
297
+ # Convenience function for conditional tool enabling
298
+ def create_conditional_enabler(
299
+ condition_key: str,
300
+ expected_value: Any = True
301
+ ) -> Callable[[Any, Agent], bool]:
302
+ """
303
+ Create a conditional enabler function based on context attributes.
304
+
305
+ Args:
306
+ condition_key: The key to check in the context
307
+ expected_value: The expected value for the condition
308
+
309
+ Returns:
310
+ A function that checks if the tool should be enabled
311
+ """
312
+ def enabler(context: Any, agent: Agent) -> bool:
313
+ return getattr(context, condition_key, None) == expected_value
314
+
315
+ return enabler