praisonaiagents 0.0.109__tar.gz → 0.0.110__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.
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/PKG-INFO +1 -1
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/__init__.py +6 -0
- praisonaiagents-0.0.110/praisonaiagents/agent/__init__.py +6 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/agent/agent.py +37 -0
- praisonaiagents-0.0.110/praisonaiagents/agent/handoff.py +317 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/llm/llm.py +217 -68
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/mcp/mcp.py +27 -7
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/memory/memory.py +48 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/duckdb_tools.py +47 -16
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/file_tools.py +52 -10
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/python_tools.py +84 -4
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/shell_tools.py +18 -8
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/spider_tools.py +55 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/PKG-INFO +1 -1
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/SOURCES.txt +2 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/pyproject.toml +1 -1
- praisonaiagents-0.0.110/tests/test_handoff_compatibility.py +238 -0
- praisonaiagents-0.0.109/praisonaiagents/agent/__init__.py +0 -5
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/README.md +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/agent/image_agent.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/agents/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/agents/agents.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/agents/autoagents.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/approval.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/guardrails/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/guardrails/guardrail_result.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/guardrails/llm_guardrail.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/knowledge/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/knowledge/chunking.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/knowledge/knowledge.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/llm/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/main.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/mcp/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/mcp/mcp_sse.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/memory/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/process/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/process/process.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/session.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/task/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/task/task.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/telemetry/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/telemetry/integration.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/telemetry/telemetry.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/README.md +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/__init__.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/arxiv_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/calculator_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/csv_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/duckduckgo_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/excel_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/json_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/newspaper_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/pandas_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/searxng_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/test.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/train/data/generatecot.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/wikipedia_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/xml_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/yaml_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents/tools/yfinance_tools.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/dependency_links.txt +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/requires.txt +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/top_level.txt +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/setup.cfg +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/tests/test-graph-memory.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/tests/test.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/tests/test_ollama_async_fix.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/tests/test_ollama_fix.py +0 -0
- {praisonaiagents-0.0.109 → praisonaiagents-0.0.110}/tests/test_posthog_fixed.py +0 -0
@@ -14,6 +14,7 @@ from .mcp.mcp import MCP
|
|
14
14
|
from .session import Session
|
15
15
|
from .memory.memory import Memory
|
16
16
|
from .guardrails import GuardrailResult, LLMGuardrail
|
17
|
+
from .agent.handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
|
17
18
|
from .main import (
|
18
19
|
TaskOutput,
|
19
20
|
ReflectionOutput,
|
@@ -102,6 +103,11 @@ __all__ = [
|
|
102
103
|
'MCP',
|
103
104
|
'GuardrailResult',
|
104
105
|
'LLMGuardrail',
|
106
|
+
'Handoff',
|
107
|
+
'handoff',
|
108
|
+
'handoff_filters',
|
109
|
+
'RECOMMENDED_PROMPT_PREFIX',
|
110
|
+
'prompt_with_handoff_instructions',
|
105
111
|
'get_telemetry',
|
106
112
|
'enable_telemetry',
|
107
113
|
'disable_telemetry',
|
@@ -0,0 +1,6 @@
|
|
1
|
+
"""Agent module for AI agents"""
|
2
|
+
from .agent import Agent
|
3
|
+
from .image_agent import ImageAgent
|
4
|
+
from .handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
|
5
|
+
|
6
|
+
__all__ = ['Agent', 'ImageAgent', 'Handoff', 'handoff', 'handoff_filters', 'RECOMMENDED_PROMPT_PREFIX', 'prompt_with_handoff_instructions']
|
@@ -33,6 +33,7 @@ _shared_apps = {} # Dict of port -> FastAPI app
|
|
33
33
|
if TYPE_CHECKING:
|
34
34
|
from ..task.task import Task
|
35
35
|
from ..main import TaskOutput
|
36
|
+
from ..handoff import Handoff
|
36
37
|
|
37
38
|
@dataclass
|
38
39
|
class ChatCompletionMessage:
|
@@ -374,6 +375,7 @@ class Agent:
|
|
374
375
|
reasoning_steps: bool = False,
|
375
376
|
guardrail: Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]] = None,
|
376
377
|
max_guardrail_retries: int = 3,
|
378
|
+
handoffs: Optional[List[Union['Agent', 'Handoff']]] = None,
|
377
379
|
base_url: Optional[str] = None,
|
378
380
|
api_key: Optional[str] = None
|
379
381
|
):
|
@@ -459,6 +461,9 @@ class Agent:
|
|
459
461
|
description string for LLM-based validation. Defaults to None.
|
460
462
|
max_guardrail_retries (int, optional): Maximum number of retry attempts when guardrail
|
461
463
|
validation fails before giving up. Defaults to 3.
|
464
|
+
handoffs (Optional[List[Union['Agent', 'Handoff']]], optional): List of agents or
|
465
|
+
handoff configurations that this agent can delegate tasks to. Enables agent-to-agent
|
466
|
+
collaboration and task specialization. Defaults to None.
|
462
467
|
base_url (Optional[str], optional): Base URL for custom LLM endpoints (e.g., Ollama).
|
463
468
|
If provided, automatically creates a custom LLM instance. Defaults to None.
|
464
469
|
api_key (Optional[str], optional): API key for LLM provider. If not provided,
|
@@ -623,6 +628,10 @@ Your Goal: {self.goal}
|
|
623
628
|
self._guardrail_fn = None
|
624
629
|
self._setup_guardrail()
|
625
630
|
|
631
|
+
# Process handoffs and convert them to tools
|
632
|
+
self.handoffs = handoffs if handoffs else []
|
633
|
+
self._process_handoffs()
|
634
|
+
|
626
635
|
# Check if knowledge parameter has any values
|
627
636
|
if not knowledge:
|
628
637
|
self.knowledge = None
|
@@ -696,6 +705,34 @@ Your Goal: {self.goal}
|
|
696
705
|
else:
|
697
706
|
raise ValueError("Agent guardrail must be either a callable or a string description")
|
698
707
|
|
708
|
+
def _process_handoffs(self):
|
709
|
+
"""Process handoffs and convert them to tools that can be used by the agent."""
|
710
|
+
if not self.handoffs:
|
711
|
+
return
|
712
|
+
|
713
|
+
# Import here to avoid circular imports
|
714
|
+
from .handoff import Handoff
|
715
|
+
|
716
|
+
for handoff_item in self.handoffs:
|
717
|
+
try:
|
718
|
+
if isinstance(handoff_item, Handoff):
|
719
|
+
# Convert Handoff object to a tool function
|
720
|
+
tool_func = handoff_item.to_tool_function(self)
|
721
|
+
self.tools.append(tool_func)
|
722
|
+
elif hasattr(handoff_item, 'name') and hasattr(handoff_item, 'chat'):
|
723
|
+
# Direct agent reference - create a simple handoff
|
724
|
+
from .handoff import handoff
|
725
|
+
handoff_obj = handoff(handoff_item)
|
726
|
+
tool_func = handoff_obj.to_tool_function(self)
|
727
|
+
self.tools.append(tool_func)
|
728
|
+
else:
|
729
|
+
logging.warning(
|
730
|
+
f"Invalid handoff item type: {type(handoff_item)}. "
|
731
|
+
"Expected Agent or Handoff instance."
|
732
|
+
)
|
733
|
+
except Exception as e:
|
734
|
+
logging.error(f"Failed to process handoff item {handoff_item}: {e}")
|
735
|
+
|
699
736
|
def _process_guardrail(self, task_output):
|
700
737
|
"""Process the guardrail validation for a task output.
|
701
738
|
|
@@ -0,0 +1,317 @@
|
|
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
|
+
try:
|
106
|
+
sig = inspect.signature(self.on_handoff)
|
107
|
+
# Get parameters excluding those with defaults and varargs/varkwargs
|
108
|
+
required_params = [
|
109
|
+
p for p in sig.parameters.values()
|
110
|
+
if p.default == inspect.Parameter.empty
|
111
|
+
and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
|
112
|
+
]
|
113
|
+
num_required = len(required_params)
|
114
|
+
|
115
|
+
if num_required == 0:
|
116
|
+
self.on_handoff()
|
117
|
+
elif num_required == 1:
|
118
|
+
self.on_handoff(source_agent)
|
119
|
+
elif num_required == 2:
|
120
|
+
if self.input_type and kwargs:
|
121
|
+
try:
|
122
|
+
input_data = self.input_type(**kwargs)
|
123
|
+
self.on_handoff(source_agent, input_data)
|
124
|
+
except TypeError as e:
|
125
|
+
logger.error(f"Failed to create input_type instance: {e}")
|
126
|
+
self.on_handoff(source_agent, kwargs)
|
127
|
+
else:
|
128
|
+
# No input_type or no kwargs: pass raw kwargs or empty dict
|
129
|
+
self.on_handoff(source_agent, kwargs or {})
|
130
|
+
else:
|
131
|
+
raise ValueError(
|
132
|
+
f"Callback {self.on_handoff.__name__} requires {num_required} parameters, "
|
133
|
+
"but only 0-2 are supported"
|
134
|
+
)
|
135
|
+
except Exception as e:
|
136
|
+
logger.error(f"Error invoking callback {self.on_handoff.__name__}: {e}")
|
137
|
+
# Continue with handoff even if callback fails
|
138
|
+
|
139
|
+
# Prepare handoff data
|
140
|
+
handoff_data = HandoffInputData(
|
141
|
+
messages=getattr(source_agent, 'chat_history', []),
|
142
|
+
context={'source_agent': source_agent.name}
|
143
|
+
)
|
144
|
+
|
145
|
+
# Apply input filter if provided
|
146
|
+
if self.input_filter:
|
147
|
+
handoff_data = self.input_filter(handoff_data)
|
148
|
+
|
149
|
+
# Get the last user message or context to pass to target agent
|
150
|
+
last_message = None
|
151
|
+
for msg in reversed(handoff_data.messages):
|
152
|
+
if isinstance(msg, dict) and msg.get('role') == 'user':
|
153
|
+
last_message = msg.get('content', '')
|
154
|
+
break
|
155
|
+
|
156
|
+
if not last_message and handoff_data.messages:
|
157
|
+
# If no user message, use the last message
|
158
|
+
last_msg = handoff_data.messages[-1]
|
159
|
+
if isinstance(last_msg, dict):
|
160
|
+
last_message = last_msg.get('content', '')
|
161
|
+
else:
|
162
|
+
last_message = str(last_msg)
|
163
|
+
|
164
|
+
# Prepare context information
|
165
|
+
context_info = f"[Handoff from {source_agent.name}] "
|
166
|
+
if kwargs and self.input_type:
|
167
|
+
# Include structured input data in context
|
168
|
+
context_info += f"Context: {kwargs} "
|
169
|
+
|
170
|
+
# Execute the target agent
|
171
|
+
if last_message:
|
172
|
+
prompt = context_info + last_message
|
173
|
+
logger.info(f"Handing off to {self.agent.name} with prompt: {prompt}")
|
174
|
+
response = self.agent.chat(prompt)
|
175
|
+
return f"Handoff successful. {self.agent.name} response: {response}"
|
176
|
+
return f"Handoff to {self.agent.name} completed, but no specific task was provided."
|
177
|
+
|
178
|
+
except Exception as e:
|
179
|
+
logger.error(f"Error during handoff to {self.agent.name}: {str(e)}")
|
180
|
+
return f"Error during handoff to {self.agent.name}: {str(e)}"
|
181
|
+
|
182
|
+
# Set function metadata for tool definition generation
|
183
|
+
handoff_tool.__name__ = self.tool_name
|
184
|
+
handoff_tool.__doc__ = self.tool_description
|
185
|
+
|
186
|
+
# Add input type annotations if provided
|
187
|
+
if self.input_type and hasattr(self.input_type, '__annotations__'):
|
188
|
+
sig_params = []
|
189
|
+
for field_name, field_type in self.input_type.__annotations__.items():
|
190
|
+
sig_params.append(
|
191
|
+
inspect.Parameter(
|
192
|
+
field_name,
|
193
|
+
inspect.Parameter.KEYWORD_ONLY,
|
194
|
+
annotation=field_type
|
195
|
+
)
|
196
|
+
)
|
197
|
+
handoff_tool.__signature__ = inspect.Signature(sig_params)
|
198
|
+
|
199
|
+
return handoff_tool
|
200
|
+
|
201
|
+
|
202
|
+
def handoff(
|
203
|
+
agent: 'Agent',
|
204
|
+
tool_name_override: Optional[str] = None,
|
205
|
+
tool_description_override: Optional[str] = None,
|
206
|
+
on_handoff: Optional[Callable] = None,
|
207
|
+
input_type: Optional[type] = None,
|
208
|
+
input_filter: Optional[Callable[[HandoffInputData], HandoffInputData]] = None
|
209
|
+
) -> Handoff:
|
210
|
+
"""
|
211
|
+
Create a handoff configuration for delegating tasks to another agent.
|
212
|
+
|
213
|
+
This is a convenience function that creates a Handoff instance with the
|
214
|
+
specified configuration.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
agent: The target agent to hand off to
|
218
|
+
tool_name_override: Custom tool name (defaults to transfer_to_<agent_name>)
|
219
|
+
tool_description_override: Custom tool description
|
220
|
+
on_handoff: Callback function executed when handoff is invoked
|
221
|
+
input_type: Type of input expected by the handoff (for structured data)
|
222
|
+
input_filter: Function to filter/transform input before passing to target agent
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
A configured Handoff instance
|
226
|
+
|
227
|
+
Example:
|
228
|
+
```python
|
229
|
+
from praisonaiagents import Agent, handoff
|
230
|
+
|
231
|
+
billing_agent = Agent(name="Billing Agent")
|
232
|
+
refund_agent = Agent(name="Refund Agent")
|
233
|
+
|
234
|
+
triage_agent = Agent(
|
235
|
+
name="Triage Agent",
|
236
|
+
handoffs=[billing_agent, handoff(refund_agent)]
|
237
|
+
)
|
238
|
+
```
|
239
|
+
"""
|
240
|
+
return Handoff(
|
241
|
+
agent=agent,
|
242
|
+
tool_name_override=tool_name_override,
|
243
|
+
tool_description_override=tool_description_override,
|
244
|
+
on_handoff=on_handoff,
|
245
|
+
input_type=input_type,
|
246
|
+
input_filter=input_filter
|
247
|
+
)
|
248
|
+
|
249
|
+
|
250
|
+
# Handoff filters - common patterns for filtering handoff data
|
251
|
+
class handoff_filters:
|
252
|
+
"""Common handoff input filters."""
|
253
|
+
|
254
|
+
@staticmethod
|
255
|
+
def remove_all_tools(data: HandoffInputData) -> HandoffInputData:
|
256
|
+
"""Remove all tool calls from the message history."""
|
257
|
+
filtered_messages = []
|
258
|
+
for msg in data.messages:
|
259
|
+
if isinstance(msg, dict) and (msg.get('tool_calls') or msg.get('role') == 'tool'):
|
260
|
+
# Skip messages with tool calls
|
261
|
+
continue
|
262
|
+
filtered_messages.append(msg)
|
263
|
+
|
264
|
+
data.messages = filtered_messages
|
265
|
+
return data
|
266
|
+
|
267
|
+
@staticmethod
|
268
|
+
def keep_last_n_messages(n: int) -> Callable[[HandoffInputData], HandoffInputData]:
|
269
|
+
"""Keep only the last n messages in the history."""
|
270
|
+
def filter_func(data: HandoffInputData) -> HandoffInputData:
|
271
|
+
data.messages = data.messages[-n:]
|
272
|
+
return data
|
273
|
+
return filter_func
|
274
|
+
|
275
|
+
@staticmethod
|
276
|
+
def remove_system_messages(data: HandoffInputData) -> HandoffInputData:
|
277
|
+
"""Remove all system messages from the history."""
|
278
|
+
filtered_messages = []
|
279
|
+
for msg in data.messages:
|
280
|
+
if (isinstance(msg, dict) and msg.get('role') != 'system') or not isinstance(msg, dict):
|
281
|
+
filtered_messages.append(msg)
|
282
|
+
|
283
|
+
data.messages = filtered_messages
|
284
|
+
return data
|
285
|
+
|
286
|
+
|
287
|
+
# Recommended prompt prefix for agents that use handoffs
|
288
|
+
RECOMMENDED_PROMPT_PREFIX = """You have the ability to transfer tasks to specialized agents when appropriate.
|
289
|
+
When you determine that a task would be better handled by another agent with specific expertise,
|
290
|
+
use the transfer tool to hand off the task. The receiving agent will have the full context of
|
291
|
+
the conversation and will continue helping the user."""
|
292
|
+
|
293
|
+
|
294
|
+
def prompt_with_handoff_instructions(base_prompt: str, agent: 'Agent') -> str:
|
295
|
+
"""
|
296
|
+
Add handoff instructions to an agent's prompt.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
base_prompt: The original prompt/instructions
|
300
|
+
agent: The agent that will use handoffs
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
Updated prompt with handoff instructions
|
304
|
+
"""
|
305
|
+
if not hasattr(agent, 'handoffs') or not agent.handoffs:
|
306
|
+
return base_prompt
|
307
|
+
|
308
|
+
handoff_info = "\n\nAvailable handoff agents:\n"
|
309
|
+
for h in agent.handoffs:
|
310
|
+
if isinstance(h, Handoff):
|
311
|
+
handoff_info += f"- {h.agent.name}: {h.tool_description}\n"
|
312
|
+
else:
|
313
|
+
# Direct agent reference - create a temporary Handoff to get the default description
|
314
|
+
temp_handoff = Handoff(agent=h)
|
315
|
+
handoff_info += f"- {h.name}: {temp_handoff.tool_description}\n"
|
316
|
+
|
317
|
+
return RECOMMENDED_PROMPT_PREFIX + handoff_info + "\n\n" + base_prompt
|