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.
- {jaf_py-2.1.2/jaf_py.egg-info → jaf_py-2.2.1}/PKG-INFO +11 -2
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/__init__.py +14 -0
- jaf_py-2.2.1/jaf/core/agent_tool.py +315 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/engine.py +158 -41
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/streaming.py +4 -3
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/tools.py +15 -16
- jaf_py-2.2.1/jaf/core/tracing.py +583 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/types.py +99 -5
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/providers/mcp.py +10 -8
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/providers/model.py +16 -1
- {jaf_py-2.1.2 → jaf_py-2.2.1/jaf_py.egg-info}/PKG-INFO +11 -2
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/SOURCES.txt +3 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/requires.txt +11 -1
- {jaf_py-2.1.2 → jaf_py-2.2.1}/pyproject.toml +12 -2
- jaf_py-2.2.1/tests/test_conversation_id_fix.py +190 -0
- jaf_py-2.2.1/tests/test_session_continuity.py +254 -0
- jaf_py-2.1.2/jaf/core/tracing.py +0 -265
- {jaf_py-2.1.2 → jaf_py-2.2.1}/LICENSE +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/README.md +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/agent.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/agent_card.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/client.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/client_example.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/integration_example.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/rag_demo/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/server_demo/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/examples/server_example.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/cleanup.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/factory.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/composite.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/in_memory.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/postgres.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/providers/redis.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/serialization.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/run_comprehensive_tests.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/test_cleanup.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/test_serialization.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/test_stress_concurrency.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/tests/test_task_lifecycle.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/memory/types.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/protocol.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/server.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/standalone_client.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/run_tests.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_agent.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_client.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_integration.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_protocol.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/tests/test_types.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/a2a/types.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/cli.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/analytics.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/composition.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/errors.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/performance.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/tool_results.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/core/workflows.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/exceptions.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/factory.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/providers/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/providers/in_memory.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/providers/postgres.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/providers/redis.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/types.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/memory/utils.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/plugins/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/plugins/base.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/policies/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/policies/handoff.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/policies/validation.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/providers/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/server/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/server/main.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/server/server.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/server/types.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/__init__.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/example.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/functional_core.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/graphviz.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/imperative_shell.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf/visualization/types.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/dependency_links.txt +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/entry_points.txt +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/jaf_py.egg-info/top_level.txt +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/setup.cfg +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/setup.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_a2a_deep.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_a2a_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_api_reference_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_callback_system_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_coffee_tool.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_deployment_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_docs_code_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_engine.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_engine_manual.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_error_handling_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_getting_started_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_math_tool.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_mcp_comprehensive.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_mcp_docs.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_mcp_real_functionality.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_mcp_transports.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_memory_system_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_model_providers_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_property_based.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_redis_fixes.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_redis_memory.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_server_api_examples.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_streamable_http_mcp_example.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_timeout_functionality.py +0 -0
- {jaf_py-2.1.2 → jaf_py-2.2.1}/tests/test_tool_integration.py +0 -0
- {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
|
|
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
|