Skip to content

Commit 85c6938

Browse files
refactor: complete handoff.py relocation and update all imports
- Created handoff.py in agent/ folder with updated import path - Updated agent.py to import from .handoff instead of ..handoff - Updated main __init__.py to import from .agent.handoff - Added handoff exports to agent/__init__.py for cleaner imports Co-authored-by: Mervin Praison <[email protected]>
1 parent 5609692 commit 85c6938

File tree

4 files changed

+302
-4
lines changed

4 files changed

+302
-4
lines changed

src/praisonai-agents/praisonaiagents/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .session import Session
1515
from .memory.memory import Memory
1616
from .guardrails import GuardrailResult, LLMGuardrail
17-
from .handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
17+
from .agent.handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
1818
from .main import (
1919
TaskOutput,
2020
ReflectionOutput,
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Agent module for AI agents"""
22
from .agent import Agent
33
from .image_agent import ImageAgent
4+
from .handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
45

5-
__all__ = ['Agent', 'ImageAgent']
6+
__all__ = ['Agent', 'ImageAgent', 'Handoff', 'handoff', 'handoff_filters', 'RECOMMENDED_PROMPT_PREFIX', 'prompt_with_handoff_instructions']

src/praisonai-agents/praisonaiagents/agent/agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ def _process_handoffs(self):
711711
return
712712

713713
# Import here to avoid circular imports
714-
from ..handoff import Handoff
714+
from .handoff import Handoff
715715

716716
for handoff_item in self.handoffs:
717717
try:
@@ -721,7 +721,7 @@ def _process_handoffs(self):
721721
self.tools.append(tool_func)
722722
elif hasattr(handoff_item, 'name') and hasattr(handoff_item, 'chat'):
723723
# Direct agent reference - create a simple handoff
724-
from ..handoff import handoff
724+
from .handoff import handoff
725725
handoff_obj = handoff(handoff_item)
726726
tool_func = handoff_obj.to_tool_function(self)
727727
self.tools.append(tool_func)
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
"""
2+
Handoff functionality for agent-to-agent delegation.
3+
4+
This module provides handoff capabilities that allow agents to delegate tasks
5+
to other agents, similar to the OpenAI Agents SDK implementation.
6+
"""
7+
8+
from typing import Optional, Any, Callable, Dict, TYPE_CHECKING
9+
from dataclasses import dataclass, field
10+
import inspect
11+
import logging
12+
13+
if TYPE_CHECKING:
14+
from .agent import Agent
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
@dataclass
20+
class HandoffInputData:
21+
"""Data passed to a handoff target agent."""
22+
messages: list = field(default_factory=list)
23+
context: Dict[str, Any] = field(default_factory=dict)
24+
25+
26+
class Handoff:
27+
"""
28+
Represents a handoff configuration for delegating tasks to another agent.
29+
30+
Handoffs are represented as tools to the LLM, allowing agents to transfer
31+
control to specialized agents for specific tasks.
32+
"""
33+
34+
def __init__(
35+
self,
36+
agent: 'Agent',
37+
tool_name_override: Optional[str] = None,
38+
tool_description_override: Optional[str] = None,
39+
on_handoff: Optional[Callable] = None,
40+
input_type: Optional[type] = None,
41+
input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None
42+
):
43+
"""
44+
Initialize a Handoff configuration.
45+
46+
Args:
47+
agent: The target agent to hand off to
48+
tool_name_override: Custom tool name (defaults to transfer_to_<agent_name>)
49+
tool_description_override: Custom tool description
50+
on_handoff: Callback function executed when handoff is invoked
51+
input_type: Type of input expected by the handoff (for structured data)
52+
input_filter: Function to filter/transform input before passing to target agent
53+
"""
54+
self.agent = agent
55+
self.tool_name_override = tool_name_override
56+
self.tool_description_override = tool_description_override
57+
self.on_handoff = on_handoff
58+
self.input_type = input_type
59+
self.input_filter = input_filter
60+
61+
@property
62+
def tool_name(self) -> str:
63+
"""Get the tool name for this handoff."""
64+
if self.tool_name_override:
65+
return self.tool_name_override
66+
return self.default_tool_name()
67+
68+
@property
69+
def tool_description(self) -> str:
70+
"""Get the tool description for this handoff."""
71+
if self.tool_description_override:
72+
return self.tool_description_override
73+
return self.default_tool_description()
74+
75+
def default_tool_name(self) -> str:
76+
"""Generate default tool name based on agent name."""
77+
# Convert agent name to snake_case for tool name
78+
agent_name = self.agent.name.lower().replace(' ', '_')
79+
return f"transfer_to_{agent_name}"
80+
81+
def default_tool_description(self) -> str:
82+
"""Generate default tool description based on agent role and goal."""
83+
agent_desc = f"Transfer task to {self.agent.name}"
84+
if hasattr(self.agent, 'role') and self.agent.role:
85+
agent_desc += f" ({self.agent.role})"
86+
if hasattr(self.agent, 'goal') and self.agent.goal:
87+
agent_desc += f" - {self.agent.goal}"
88+
return agent_desc
89+
90+
def to_tool_function(self, source_agent: 'Agent') -> Callable:
91+
"""
92+
Convert this handoff to a tool function that can be called by the LLM.
93+
94+
Args:
95+
source_agent: The agent that will be using this handoff
96+
97+
Returns:
98+
A callable function that performs the handoff
99+
"""
100+
def handoff_tool(**kwargs):
101+
"""Execute the handoff to the target agent."""
102+
try:
103+
# Execute on_handoff callback if provided
104+
if self.on_handoff:
105+
sig = inspect.signature(self.on_handoff)
106+
num_params = len(sig.parameters)
107+
108+
if num_params == 0:
109+
self.on_handoff()
110+
elif num_params == 1:
111+
self.on_handoff(source_agent)
112+
elif num_params == 2 and self.input_type:
113+
input_data = self.input_type(**kwargs) if kwargs else self.input_type()
114+
self.on_handoff(source_agent, input_data)
115+
else:
116+
# Fallback for other cases, which may raise a TypeError.
117+
self.on_handoff(source_agent)
118+
119+
# Prepare handoff data
120+
handoff_data = HandoffInputData(
121+
messages=getattr(source_agent, 'chat_history', []),
122+
context={'source_agent': source_agent.name}
123+
)
124+
125+
# Apply input filter if provided
126+
if self.input_filter:
127+
handoff_data = self.input_filter(handoff_data)
128+
129+
# Get the last user message or context to pass to target agent
130+
last_message = None
131+
for msg in reversed(handoff_data.messages):
132+
if isinstance(msg, dict) and msg.get('role') == 'user':
133+
last_message = msg.get('content', '')
134+
break
135+
136+
if not last_message and handoff_data.messages:
137+
# If no user message, use the last message
138+
last_msg = handoff_data.messages[-1]
139+
if isinstance(last_msg, dict):
140+
last_message = last_msg.get('content', '')
141+
else:
142+
last_message = str(last_msg)
143+
144+
# Prepare context information
145+
context_info = f"[Handoff from {source_agent.name}] "
146+
if kwargs and self.input_type:
147+
# Include structured input data in context
148+
context_info += f"Context: {kwargs} "
149+
150+
# Execute the target agent
151+
if last_message:
152+
prompt = context_info + last_message
153+
logger.info(f"Handing off to {self.agent.name} with prompt: {prompt}")
154+
response = self.agent.chat(prompt)
155+
return f"Handoff successful. {self.agent.name} response: {response}"
156+
return f"Handoff to {self.agent.name} completed, but no specific task was provided."
157+
158+
except Exception as e:
159+
logger.error(f"Error during handoff to {self.agent.name}: {str(e)}")
160+
return f"Error during handoff to {self.agent.name}: {str(e)}"
161+
162+
# Set function metadata for tool definition generation
163+
handoff_tool.__name__ = self.tool_name
164+
handoff_tool.__doc__ = self.tool_description
165+
166+
# Add input type annotations if provided
167+
if self.input_type:
168+
sig_params = []
169+
for field_name, field_type in self.input_type.__annotations__.items():
170+
sig_params.append(
171+
inspect.Parameter(
172+
field_name,
173+
inspect.Parameter.KEYWORD_ONLY,
174+
annotation=field_type
175+
)
176+
)
177+
handoff_tool.__signature__ = inspect.Signature(sig_params)
178+
179+
return handoff_tool
180+
181+
182+
def handoff(
183+
agent: 'Agent',
184+
tool_name_override: Optional[str] = None,
185+
tool_description_override: Optional[str] = None,
186+
on_handoff: Optional[Callable] = None,
187+
input_type: Optional[type] = None,
188+
input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None
189+
) -> Handoff:
190+
"""
191+
Create a handoff configuration for delegating tasks to another agent.
192+
193+
This is a convenience function that creates a Handoff instance with the
194+
specified configuration.
195+
196+
Args:
197+
agent: The target agent to hand off to
198+
tool_name_override: Custom tool name (defaults to transfer_to_<agent_name>)
199+
tool_description_override: Custom tool description
200+
on_handoff: Callback function executed when handoff is invoked
201+
input_type: Type of input expected by the handoff (for structured data)
202+
input_filter: Function to filter/transform input before passing to target agent
203+
204+
Returns:
205+
A configured Handoff instance
206+
207+
Example:
208+
```python
209+
from praisonaiagents import Agent, handoff
210+
211+
billing_agent = Agent(name="Billing Agent")
212+
refund_agent = Agent(name="Refund Agent")
213+
214+
triage_agent = Agent(
215+
name="Triage Agent",
216+
handoffs=[billing_agent, handoff(refund_agent)]
217+
)
218+
```
219+
"""
220+
return Handoff(
221+
agent=agent,
222+
tool_name_override=tool_name_override,
223+
tool_description_override=tool_description_override,
224+
on_handoff=on_handoff,
225+
input_type=input_type,
226+
input_filter=input_filter
227+
)
228+
229+
230+
# Handoff filters - common patterns for filtering handoff data
231+
class handoff_filters:
232+
"""Common handoff input filters."""
233+
234+
@staticmethod
235+
def remove_all_tools(data: HandoffInputData) -> HandoffInputData:
236+
"""Remove all tool calls from the message history."""
237+
filtered_messages = []
238+
for msg in data.messages:
239+
if isinstance(msg, dict) and (msg.get('tool_calls') or msg.get('role') == 'tool'):
240+
# Skip messages with tool calls
241+
continue
242+
filtered_messages.append(msg)
243+
244+
data.messages = filtered_messages
245+
return data
246+
247+
@staticmethod
248+
def keep_last_n_messages(n: int) -> Callable[[HandoffInputData], HandoffInputData]:
249+
"""Keep only the last n messages in the history."""
250+
def filter_func(data: HandoffInputData) -> HandoffInputData:
251+
data.messages = data.messages[-n:]
252+
return data
253+
return filter_func
254+
255+
@staticmethod
256+
def remove_system_messages(data: HandoffInputData) -> HandoffInputData:
257+
"""Remove all system messages from the history."""
258+
filtered_messages = []
259+
for msg in data.messages:
260+
if (isinstance(msg, dict) and msg.get('role') != 'system') or not isinstance(msg, dict):
261+
filtered_messages.append(msg)
262+
263+
data.messages = filtered_messages
264+
return data
265+
266+
267+
# Recommended prompt prefix for agents that use handoffs
268+
RECOMMENDED_PROMPT_PREFIX = """You have the ability to transfer tasks to specialized agents when appropriate.
269+
When you determine that a task would be better handled by another agent with specific expertise,
270+
use the transfer tool to hand off the task. The receiving agent will have the full context of
271+
the conversation and will continue helping the user."""
272+
273+
274+
def prompt_with_handoff_instructions(base_prompt: str, agent: 'Agent') -> str:
275+
"""
276+
Add handoff instructions to an agent's prompt.
277+
278+
Args:
279+
base_prompt: The original prompt/instructions
280+
agent: The agent that will use handoffs
281+
282+
Returns:
283+
Updated prompt with handoff instructions
284+
"""
285+
if not hasattr(agent, 'handoffs') or not agent.handoffs:
286+
return base_prompt
287+
288+
handoff_info = "\n\nAvailable handoff agents:\n"
289+
for h in agent.handoffs:
290+
if isinstance(h, Handoff):
291+
handoff_info += f"- {h.agent.name}: {h.tool_description}\n"
292+
else:
293+
# Direct agent reference - create a temporary Handoff to get the default description
294+
temp_handoff = Handoff(agent=h)
295+
handoff_info += f"- {h.name}: {temp_handoff.tool_description}\n"
296+
297+
return RECOMMENDED_PROMPT_PREFIX + handoff_info + "\n\n" + base_prompt

0 commit comments

Comments
 (0)