praisonaiagents 0.0.108__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.
Files changed (70) hide show
  1. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/PKG-INFO +1 -1
  2. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/__init__.py +6 -0
  3. praisonaiagents-0.0.110/praisonaiagents/agent/__init__.py +6 -0
  4. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/agent/agent.py +79 -3
  5. praisonaiagents-0.0.110/praisonaiagents/agent/handoff.py +317 -0
  6. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/llm/llm.py +217 -68
  7. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/mcp/mcp.py +27 -7
  8. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/memory/memory.py +48 -0
  9. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/duckdb_tools.py +47 -16
  10. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/file_tools.py +52 -10
  11. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/python_tools.py +84 -4
  12. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/shell_tools.py +18 -8
  13. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/spider_tools.py +55 -0
  14. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/PKG-INFO +1 -1
  15. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/SOURCES.txt +2 -0
  16. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/pyproject.toml +1 -1
  17. praisonaiagents-0.0.110/tests/test_handoff_compatibility.py +238 -0
  18. praisonaiagents-0.0.108/praisonaiagents/agent/__init__.py +0 -5
  19. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/README.md +0 -0
  20. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/agent/image_agent.py +0 -0
  21. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/agents/__init__.py +0 -0
  22. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/agents/agents.py +0 -0
  23. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/agents/autoagents.py +0 -0
  24. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/approval.py +0 -0
  25. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/guardrails/__init__.py +0 -0
  26. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/guardrails/guardrail_result.py +0 -0
  27. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/guardrails/llm_guardrail.py +0 -0
  28. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/knowledge/__init__.py +0 -0
  29. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/knowledge/chunking.py +0 -0
  30. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/knowledge/knowledge.py +0 -0
  31. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/llm/__init__.py +0 -0
  32. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/main.py +0 -0
  33. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/mcp/__init__.py +0 -0
  34. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/mcp/mcp_sse.py +0 -0
  35. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/memory/__init__.py +0 -0
  36. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/process/__init__.py +0 -0
  37. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/process/process.py +0 -0
  38. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/session.py +0 -0
  39. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/task/__init__.py +0 -0
  40. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/task/task.py +0 -0
  41. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/telemetry/__init__.py +0 -0
  42. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/telemetry/integration.py +0 -0
  43. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/telemetry/telemetry.py +0 -0
  44. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/README.md +0 -0
  45. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/__init__.py +0 -0
  46. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/arxiv_tools.py +0 -0
  47. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/calculator_tools.py +0 -0
  48. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/csv_tools.py +0 -0
  49. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/duckduckgo_tools.py +0 -0
  50. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/excel_tools.py +0 -0
  51. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/json_tools.py +0 -0
  52. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/newspaper_tools.py +0 -0
  53. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/pandas_tools.py +0 -0
  54. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/searxng_tools.py +0 -0
  55. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/test.py +0 -0
  56. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/tools.py +0 -0
  57. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/train/data/generatecot.py +0 -0
  58. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/wikipedia_tools.py +0 -0
  59. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/xml_tools.py +0 -0
  60. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/yaml_tools.py +0 -0
  61. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents/tools/yfinance_tools.py +0 -0
  62. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/dependency_links.txt +0 -0
  63. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/requires.txt +0 -0
  64. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/praisonaiagents.egg-info/top_level.txt +0 -0
  65. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/setup.cfg +0 -0
  66. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/tests/test-graph-memory.py +0 -0
  67. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/tests/test.py +0 -0
  68. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/tests/test_ollama_async_fix.py +0 -0
  69. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/tests/test_ollama_fix.py +0 -0
  70. {praisonaiagents-0.0.108 → praisonaiagents-0.0.110}/tests/test_posthog_fixed.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praisonaiagents
3
- Version: 0.0.108
3
+ Version: 0.0.110
4
4
  Summary: Praison AI agents for completing complex tasks with Self Reflection Agents
5
5
  Author: Mervin Praison
6
6
  Requires-Python: >=3.10
@@ -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:
@@ -373,7 +374,10 @@ class Agent:
373
374
  user_id: Optional[str] = None,
374
375
  reasoning_steps: bool = False,
375
376
  guardrail: Optional[Union[Callable[['TaskOutput'], Tuple[bool, Any]], str]] = None,
376
- max_guardrail_retries: int = 3
377
+ max_guardrail_retries: int = 3,
378
+ handoffs: Optional[List[Union['Agent', 'Handoff']]] = None,
379
+ base_url: Optional[str] = None,
380
+ api_key: Optional[str] = None
377
381
  ):
378
382
  """Initialize an Agent instance.
379
383
 
@@ -457,6 +461,13 @@ class Agent:
457
461
  description string for LLM-based validation. Defaults to None.
458
462
  max_guardrail_retries (int, optional): Maximum number of retry attempts when guardrail
459
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.
467
+ base_url (Optional[str], optional): Base URL for custom LLM endpoints (e.g., Ollama).
468
+ If provided, automatically creates a custom LLM instance. Defaults to None.
469
+ api_key (Optional[str], optional): API key for LLM provider. If not provided,
470
+ falls back to environment variables. Defaults to None.
460
471
 
461
472
  Raises:
462
473
  ValueError: If all of name, role, goal, backstory, and instructions are None.
@@ -503,10 +514,40 @@ class Agent:
503
514
  # Check for model name in environment variable if not provided
504
515
  self._using_custom_llm = False
505
516
 
517
+ # If base_url is provided, always create a custom LLM instance
518
+ if base_url:
519
+ try:
520
+ from ..llm.llm import LLM
521
+ # Handle different llm parameter types with base_url
522
+ if isinstance(llm, dict):
523
+ # Merge base_url and api_key into the dict
524
+ llm_config = llm.copy()
525
+ llm_config['base_url'] = base_url
526
+ if api_key:
527
+ llm_config['api_key'] = api_key
528
+ self.llm_instance = LLM(**llm_config)
529
+ else:
530
+ # Create LLM with model string and base_url
531
+ model_name = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
532
+ self.llm_instance = LLM(
533
+ model=model_name,
534
+ base_url=base_url,
535
+ api_key=api_key
536
+ )
537
+ self._using_custom_llm = True
538
+ except ImportError as e:
539
+ raise ImportError(
540
+ "LLM features requested but dependencies not installed. "
541
+ "Please install with: pip install \"praisonaiagents[llm]\""
542
+ ) from e
506
543
  # If the user passes a dictionary (for advanced configuration)
507
- if isinstance(llm, dict) and "model" in llm:
544
+ elif isinstance(llm, dict) and "model" in llm:
508
545
  try:
509
546
  from ..llm.llm import LLM
547
+ # Add api_key if provided and not in dict
548
+ if api_key and 'api_key' not in llm:
549
+ llm = llm.copy()
550
+ llm['api_key'] = api_key
510
551
  self.llm_instance = LLM(**llm) # Pass all dict items as kwargs
511
552
  self._using_custom_llm = True
512
553
  except ImportError as e:
@@ -519,7 +560,10 @@ class Agent:
519
560
  try:
520
561
  from ..llm.llm import LLM
521
562
  # Pass the entire string so LiteLLM can parse provider/model
522
- self.llm_instance = LLM(model=llm)
563
+ llm_params = {'model': llm}
564
+ if api_key:
565
+ llm_params['api_key'] = api_key
566
+ self.llm_instance = LLM(**llm_params)
523
567
  self._using_custom_llm = True
524
568
 
525
569
  # Ensure tools are properly accessible when using custom LLM
@@ -584,6 +628,10 @@ Your Goal: {self.goal}
584
628
  self._guardrail_fn = None
585
629
  self._setup_guardrail()
586
630
 
631
+ # Process handoffs and convert them to tools
632
+ self.handoffs = handoffs if handoffs else []
633
+ self._process_handoffs()
634
+
587
635
  # Check if knowledge parameter has any values
588
636
  if not knowledge:
589
637
  self.knowledge = None
@@ -657,6 +705,34 @@ Your Goal: {self.goal}
657
705
  else:
658
706
  raise ValueError("Agent guardrail must be either a callable or a string description")
659
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
+
660
736
  def _process_guardrail(self, task_output):
661
737
  """Process the guardrail validation for a task output.
662
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