jaf-py 2.5.1__tar.gz → 2.5.3__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.5.1/jaf_py.egg-info → jaf_py-2.5.3}/PKG-INFO +2 -2
- {jaf_py-2.5.1 → jaf_py-2.5.3}/README.md +1 -1
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/__init__.py +1 -1
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/__init__.py +6 -0
- jaf_py-2.5.3/jaf/core/handoff.py +191 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/state.py +117 -6
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/tracing.py +371 -73
- {jaf_py-2.5.1 → jaf_py-2.5.3/jaf_py.egg-info}/PKG-INFO +2 -2
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/SOURCES.txt +1 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/pyproject.toml +1 -1
- {jaf_py-2.5.1 → jaf_py-2.5.3}/LICENSE +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/agent.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/agent_card.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/client.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/client_example.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/integration_example.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/rag_demo/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/server_demo/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/examples/server_example.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/cleanup.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/factory.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/composite.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/in_memory.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/postgres.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/providers/redis.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/serialization.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/run_comprehensive_tests.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/test_cleanup.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/test_serialization.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/test_stress_concurrency.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/tests/test_task_lifecycle.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/memory/types.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/protocol.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/server.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/standalone_client.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/run_tests.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_agent.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_client.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_integration.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_protocol.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/tests/test_types.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/a2a/types.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/cli.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/agent_tool.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/analytics.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/composition.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/engine.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/errors.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/guardrails.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/parallel_agents.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/performance.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/proxy.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/proxy_helpers.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/streaming.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/tool_results.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/tools.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/types.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/core/workflows.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/exceptions.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/approval_storage.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/factory.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/providers/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/providers/in_memory.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/providers/postgres.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/providers/redis.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/types.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/memory/utils.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/plugins/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/plugins/base.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/policies/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/policies/handoff.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/policies/validation.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/providers/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/providers/mcp.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/providers/model.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/server/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/server/main.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/server/server.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/server/types.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/utils/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/utils/attachments.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/utils/document_processor.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/__init__.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/example.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/functional_core.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/graphviz.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/imperative_shell.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf/visualization/types.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/dependency_links.txt +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/entry_points.txt +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/requires.txt +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/jaf_py.egg-info/top_level.txt +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/setup.cfg +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/setup.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_a2a_deep.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_a2a_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_api_reference_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_attachments.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_callback_system_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_coffee_tool.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_conversation_id_fix.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_deployment_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_docs_code_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_engine.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_engine_manual.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_error_handling_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_getting_started_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_manual.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_math_tool.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_mcp_comprehensive.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_mcp_docs.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_mcp_real_functionality.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_mcp_transports.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_memory_system_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_model_providers_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_property_based.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_proxy_simple.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_redis_fixes.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_redis_memory.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_server_api_examples.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_session_continuity.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_streamable_http_mcp_example.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_timeout_functionality.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_tool_integration.py +0 -0
- {jaf_py-2.5.1 → jaf_py-2.5.3}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jaf-py
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.3
|
|
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
|
|
@@ -82,7 +82,7 @@ Dynamic: license-file
|
|
|
82
82
|
|
|
83
83
|
<!--  -->
|
|
84
84
|
|
|
85
|
-
[](https://github.com/xynehq/jaf-py)
|
|
86
86
|
[](https://www.python.org/)
|
|
87
87
|
[](https://xynehq.github.io/jaf-py/)
|
|
88
88
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<!--  -->
|
|
4
4
|
|
|
5
|
-
[](https://github.com/xynehq/jaf-py)
|
|
6
6
|
[](https://www.python.org/)
|
|
7
7
|
[](https://xynehq.github.io/jaf-py/)
|
|
8
8
|
|
|
@@ -191,7 +191,7 @@ def generate_run_id() -> RunId:
|
|
|
191
191
|
"""Generate a new run ID."""
|
|
192
192
|
return create_run_id(str(uuid.uuid4()))
|
|
193
193
|
|
|
194
|
-
__version__ = "2.
|
|
194
|
+
__version__ = "2.5.3"
|
|
195
195
|
__all__ = [
|
|
196
196
|
# Core types and functions
|
|
197
197
|
"TraceId", "RunId", "ValidationResult", "Message", "ModelConfig",
|
|
@@ -22,6 +22,7 @@ from .parallel_agents import (
|
|
|
22
22
|
create_domain_experts_tool,
|
|
23
23
|
)
|
|
24
24
|
from .proxy import ProxyConfig, ProxyAuth, create_proxy_config, get_default_proxy_config
|
|
25
|
+
from .handoff import handoff_tool, handoff, create_handoff_tool, is_handoff_request, extract_handoff_target
|
|
25
26
|
|
|
26
27
|
__all__ = [
|
|
27
28
|
"Agent",
|
|
@@ -52,6 +53,7 @@ __all__ = [
|
|
|
52
53
|
"create_conditional_enabler",
|
|
53
54
|
"create_default_output_extractor",
|
|
54
55
|
"create_domain_experts_tool",
|
|
56
|
+
"create_handoff_tool",
|
|
55
57
|
"create_json_output_extractor",
|
|
56
58
|
"create_language_specialists_tool",
|
|
57
59
|
"create_parallel_agents_tool",
|
|
@@ -59,8 +61,12 @@ __all__ = [
|
|
|
59
61
|
"create_run_id",
|
|
60
62
|
"create_simple_parallel_tool",
|
|
61
63
|
"create_trace_id",
|
|
64
|
+
"extract_handoff_target",
|
|
62
65
|
"get_current_run_config",
|
|
63
66
|
"get_default_proxy_config",
|
|
67
|
+
"handoff",
|
|
68
|
+
"handoff_tool",
|
|
69
|
+
"is_handoff_request",
|
|
64
70
|
"require_permissions",
|
|
65
71
|
"run",
|
|
66
72
|
"set_current_run_config",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Handoff system for JAF framework.
|
|
3
|
+
|
|
4
|
+
This module provides a simple, elegant handoff mechanism that allows agents
|
|
5
|
+
to seamlessly transfer control to other agents with clean state management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Optional, TypeVar
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from .types import Tool, ToolSchema, ToolSource
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
except ImportError:
|
|
17
|
+
BaseModel = None
|
|
18
|
+
Field = None
|
|
19
|
+
|
|
20
|
+
Ctx = TypeVar('Ctx')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _create_handoff_json(agent_name: str, message: str = "") -> str:
|
|
24
|
+
"""Create the JSON structure for handoff requests."""
|
|
25
|
+
return json.dumps({
|
|
26
|
+
"handoff_to": agent_name,
|
|
27
|
+
"message": message or f"Handing off to {agent_name}",
|
|
28
|
+
"type": "handoff"
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if BaseModel is not None and Field is not None:
|
|
33
|
+
class _HandoffInput(BaseModel):
|
|
34
|
+
"""Input parameters for handoff tool (Pydantic model)."""
|
|
35
|
+
agent_name: str = Field(description="Name of the agent to hand off to")
|
|
36
|
+
message: str = Field(description="Message or context to pass to the target agent")
|
|
37
|
+
else:
|
|
38
|
+
class _HandoffInput(object):
|
|
39
|
+
"""Plain-Python fallback for handoff input when Pydantic is unavailable.
|
|
40
|
+
|
|
41
|
+
This class intentionally does not call Field() so it is safe to import
|
|
42
|
+
when Pydantic is not installed.
|
|
43
|
+
"""
|
|
44
|
+
agent_name: str
|
|
45
|
+
message: str
|
|
46
|
+
|
|
47
|
+
def __init__(self, agent_name: str, message: str = ""):
|
|
48
|
+
self.agent_name = agent_name
|
|
49
|
+
self.message = message
|
|
50
|
+
|
|
51
|
+
HandoffInput = _HandoffInput
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class HandoffResult:
|
|
56
|
+
"""Result of a handoff operation."""
|
|
57
|
+
target_agent: str
|
|
58
|
+
message: str
|
|
59
|
+
success: bool = True
|
|
60
|
+
error: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HandoffTool:
|
|
64
|
+
"""A tool that enables agents to hand off to other agents."""
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
# Create schema
|
|
68
|
+
if BaseModel:
|
|
69
|
+
parameters_model = HandoffInput
|
|
70
|
+
else:
|
|
71
|
+
# Fallback schema when Pydantic is not available
|
|
72
|
+
parameters_model = {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"agent_name": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Name of the agent to hand off to"
|
|
78
|
+
},
|
|
79
|
+
"message": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Message or context to pass to the target agent"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"required": ["agent_name", "message"]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
self.schema = ToolSchema(
|
|
88
|
+
name="handoff",
|
|
89
|
+
description="Hand off the conversation to another agent",
|
|
90
|
+
parameters=parameters_model
|
|
91
|
+
)
|
|
92
|
+
self.source = ToolSource.NATIVE
|
|
93
|
+
self.metadata = {"type": "handoff", "system": True}
|
|
94
|
+
|
|
95
|
+
async def execute(self, args: HandoffInput, context: Any) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Execute the handoff.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
args (HandoffInput): The handoff input arguments.
|
|
101
|
+
context (Any): Context containing current agent and run state information.
|
|
102
|
+
"""
|
|
103
|
+
# Extract arguments
|
|
104
|
+
if hasattr(args, 'agent_name'):
|
|
105
|
+
agent_name = args.agent_name
|
|
106
|
+
message = args.message
|
|
107
|
+
elif isinstance(args, dict):
|
|
108
|
+
agent_name = args.get('agent_name', '')
|
|
109
|
+
message = args.get('message', '')
|
|
110
|
+
else:
|
|
111
|
+
return json.dumps({
|
|
112
|
+
"error": "invalid_handoff_args",
|
|
113
|
+
"message": "Invalid handoff arguments provided",
|
|
114
|
+
"usage": "handoff(agent_name='target_agent', message='optional context')"
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if not agent_name:
|
|
118
|
+
return json.dumps({
|
|
119
|
+
"error": "missing_agent_name",
|
|
120
|
+
"message": "Agent name is required for handoff",
|
|
121
|
+
"usage": "handoff(agent_name='target_agent', message='optional context')"
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
# Add agent validation if we have access to current agent info
|
|
125
|
+
if context and hasattr(context, 'current_agent'):
|
|
126
|
+
current_agent = context.current_agent
|
|
127
|
+
if current_agent.handoffs and agent_name not in current_agent.handoffs:
|
|
128
|
+
return json.dumps({
|
|
129
|
+
"error": "handoff_not_allowed",
|
|
130
|
+
"message": f"Agent {current_agent.name} cannot handoff to {agent_name}",
|
|
131
|
+
"allowed_handoffs": current_agent.handoffs
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
# Return the special handoff JSON that the engine recognizes
|
|
135
|
+
return _create_handoff_json(agent_name, message)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def create_handoff_tool() -> Tool:
|
|
139
|
+
"""Create a handoff tool that can be added to any agent."""
|
|
140
|
+
return HandoffTool()
|
|
141
|
+
|
|
142
|
+
handoff_tool = create_handoff_tool()
|
|
143
|
+
|
|
144
|
+
def handoff(agent_name: str, message: str = "") -> str:
|
|
145
|
+
"""
|
|
146
|
+
Simple function to perform a handoff (for use in agent tools).
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
agent_name: Name of the agent to hand off to
|
|
150
|
+
message: Optional message to pass to the target agent
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
JSON string that triggers a handoff
|
|
154
|
+
"""
|
|
155
|
+
return _create_handoff_json(agent_name, message)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def is_handoff_request(result: str) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Check if a tool result is a handoff request.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
result: Tool execution result
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if the result is a handoff request
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
parsed = json.loads(result)
|
|
170
|
+
return isinstance(parsed, dict) and "handoff_to" in parsed
|
|
171
|
+
except (json.JSONDecodeError, TypeError):
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def extract_handoff_target(result: str) -> Optional[str]:
|
|
176
|
+
"""
|
|
177
|
+
Extract the target agent name from a handoff result.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
result: Tool execution result
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Target agent name if it's a handoff, None otherwise
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
parsed = json.loads(result)
|
|
187
|
+
if isinstance(parsed, dict) and "handoff_to" in parsed:
|
|
188
|
+
return parsed["handoff_to"]
|
|
189
|
+
except (json.JSONDecodeError, TypeError):
|
|
190
|
+
pass
|
|
191
|
+
return None
|
|
@@ -5,10 +5,113 @@ This module provides functions to manage approval state transitions
|
|
|
5
5
|
and integrate with approval storage systems.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Dict, Any, Optional
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
9
|
from dataclasses import replace
|
|
10
10
|
|
|
11
|
-
from .types import RunState, RunConfig, Interruption, ApprovalValue
|
|
11
|
+
from .types import RunState, RunConfig, Interruption, ApprovalValue, Message, ContentRole, Attachment
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _extract_attachments_from_messages(messages: List[Dict[str, Any]]) -> List[Attachment]:
|
|
15
|
+
"""Extract attachment objects from message data."""
|
|
16
|
+
attachments = []
|
|
17
|
+
|
|
18
|
+
for msg in messages:
|
|
19
|
+
msg_attachments = msg.get('attachments', [])
|
|
20
|
+
for att in msg_attachments:
|
|
21
|
+
try:
|
|
22
|
+
# Convert dict to Attachment object
|
|
23
|
+
attachment = Attachment(
|
|
24
|
+
kind=att.get('kind', 'image'),
|
|
25
|
+
mime_type=att.get('mime_type'),
|
|
26
|
+
name=att.get('name'),
|
|
27
|
+
url=att.get('url'),
|
|
28
|
+
data=att.get('data'),
|
|
29
|
+
format=att.get('format'),
|
|
30
|
+
use_litellm_format=att.get('use_litellm_format')
|
|
31
|
+
)
|
|
32
|
+
attachments.append(attachment)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"[JAF:APPROVAL] Failed to process attachment: {e}")
|
|
35
|
+
|
|
36
|
+
return attachments
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _process_additional_context_images(additional_context: Optional[Dict[str, Any]]) -> List[Attachment]:
|
|
40
|
+
"""Process additional context and extract any image attachments."""
|
|
41
|
+
if not additional_context:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
attachments = []
|
|
45
|
+
|
|
46
|
+
# Handle messages with attachments
|
|
47
|
+
messages = additional_context.get('messages', [])
|
|
48
|
+
if messages:
|
|
49
|
+
attachments.extend(_extract_attachments_from_messages(messages))
|
|
50
|
+
|
|
51
|
+
# Handle legacy image_context format
|
|
52
|
+
image_context = additional_context.get('image_context')
|
|
53
|
+
if image_context and image_context.get('type') == 'image_url':
|
|
54
|
+
try:
|
|
55
|
+
image_url = image_context.get('image_url', {})
|
|
56
|
+
url = image_url.get('url', '')
|
|
57
|
+
|
|
58
|
+
if url.startswith('data:'):
|
|
59
|
+
# Parse data URL: data:image/png;base64,iVBORw0KGgo...
|
|
60
|
+
header, data = url.split(',', 1)
|
|
61
|
+
mime_type = header.split(':')[1].split(';')[0]
|
|
62
|
+
|
|
63
|
+
attachment = Attachment(
|
|
64
|
+
kind='image',
|
|
65
|
+
mime_type=mime_type,
|
|
66
|
+
data=data,
|
|
67
|
+
name=f"approval_image.{mime_type.split('/')[-1]}"
|
|
68
|
+
)
|
|
69
|
+
attachments.append(attachment)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"[JAF:APPROVAL] Failed to process image_context: {e}")
|
|
72
|
+
|
|
73
|
+
return attachments
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _add_approval_context_to_conversation(
|
|
77
|
+
state: RunState[Any],
|
|
78
|
+
additional_context: Optional[Dict[str, Any]]
|
|
79
|
+
) -> RunState[Any]:
|
|
80
|
+
"""Add approval context including images to the conversation."""
|
|
81
|
+
if not additional_context:
|
|
82
|
+
return state
|
|
83
|
+
|
|
84
|
+
# Extract image attachments
|
|
85
|
+
attachments = _process_additional_context_images(additional_context)
|
|
86
|
+
|
|
87
|
+
if not attachments:
|
|
88
|
+
return state
|
|
89
|
+
|
|
90
|
+
# Create approval context message
|
|
91
|
+
approval_message = "Additional context provided during approval process."
|
|
92
|
+
|
|
93
|
+
# Check if there are text messages to include
|
|
94
|
+
messages = additional_context.get('messages', [])
|
|
95
|
+
if messages:
|
|
96
|
+
text_content = []
|
|
97
|
+
for msg in messages:
|
|
98
|
+
content = msg.get('content', '')
|
|
99
|
+
if content:
|
|
100
|
+
text_content.append(content)
|
|
101
|
+
|
|
102
|
+
if text_content:
|
|
103
|
+
approval_message = f"User provided additional context: {' '.join(text_content)}"
|
|
104
|
+
|
|
105
|
+
# Create user message with attachments (using USER role for better compatibility)
|
|
106
|
+
context_message = Message(
|
|
107
|
+
role=ContentRole.USER,
|
|
108
|
+
content=approval_message,
|
|
109
|
+
attachments=attachments
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Add to conversation
|
|
113
|
+
new_messages = state.messages + [context_message]
|
|
114
|
+
return replace(state, messages=new_messages)
|
|
12
115
|
|
|
13
116
|
|
|
14
117
|
async def approve(
|
|
@@ -60,8 +163,12 @@ async def approve(
|
|
|
60
163
|
# Update in-memory state
|
|
61
164
|
new_approvals = {**state.approvals}
|
|
62
165
|
new_approvals[interruption.tool_call.id] = approval_value
|
|
63
|
-
|
|
64
|
-
|
|
166
|
+
|
|
167
|
+
# Process any image context and add to conversation
|
|
168
|
+
updated_state = replace(state, approvals=new_approvals)
|
|
169
|
+
updated_state = _add_approval_context_to_conversation(updated_state, additional_context)
|
|
170
|
+
|
|
171
|
+
return updated_state
|
|
65
172
|
|
|
66
173
|
return state
|
|
67
174
|
|
|
@@ -115,8 +222,12 @@ async def reject(
|
|
|
115
222
|
# Update in-memory state
|
|
116
223
|
new_approvals = {**state.approvals}
|
|
117
224
|
new_approvals[interruption.tool_call.id] = approval_value
|
|
118
|
-
|
|
119
|
-
|
|
225
|
+
|
|
226
|
+
# Process any image context and add to conversation
|
|
227
|
+
updated_state = replace(state, approvals=new_approvals)
|
|
228
|
+
updated_state = _add_approval_context_to_conversation(updated_state, additional_context)
|
|
229
|
+
|
|
230
|
+
return updated_state
|
|
120
231
|
|
|
121
232
|
return state
|
|
122
233
|
|