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 \n Available 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