-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: add typed payload contract to handoff example to prevent silent data violations (closes #1810) #1889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: add typed payload contract to handoff example to prevent silent data violations (closes #1810) #1889
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,43 +4,70 @@ | |||||||||||||||||||||
| This example shows how agents can hand off tasks to specialized agents. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from pydantic import BaseModel | ||||||||||||||||||||||
| from typing import TypedDict, Optional | ||||||||||||||||||||||
| from praisonaiagents import Agent | ||||||||||||||||||||||
|
Comment on lines
+7
to
9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Create specialized agents | ||||||||||||||||||||||
| # Define typed payload contracts for each handoff | ||||||||||||||||||||||
| class BillingPayload(BaseModel): | ||||||||||||||||||||||
| account_id: str | ||||||||||||||||||||||
| invoice_amount: Optional[float] = None | ||||||||||||||||||||||
| issue_type: str = "billing" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class RefundPayload(TypedDict): | ||||||||||||||||||||||
| transaction_id: str | ||||||||||||||||||||||
| amount: float | ||||||||||||||||||||||
| reason: str | ||||||||||||||||||||||
|
Comment on lines
+17
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The PR description explicitly states the fix "enabling runtime validation", but Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class TechnicalPayload(BaseModel): | ||||||||||||||||||||||
| error_code: Optional[str] = None | ||||||||||||||||||||||
| device_info: str | ||||||||||||||||||||||
| user_agent: str | ||||||||||||||||||||||
|
Comment on lines
+12
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Required payload fields are stricter than the sample inputs can satisfy. Line 13, Lines 18-20, and Lines 24-25 define mandatory fields that are absent from the plain-text sample requests on Lines 79-82. In a validated handoff flow, this either fails routing or pushes agents to invent data. Proposed example-safe schema adjustment class BillingPayload(BaseModel):
- account_id: str
+ account_id: Optional[str] = None
invoice_amount: Optional[float] = None
issue_type: str = "billing"
-class RefundPayload(TypedDict):
+class RefundPayload(TypedDict, total=False):
transaction_id: str
amount: float
reason: str
class TechnicalPayload(BaseModel):
error_code: Optional[str] = None
- device_info: str
- user_agent: str
+ device_info: Optional[str] = None
+ user_agent: Optional[str] = None- When handing off, populate the appropriate payload with extracted data.""",
+ When handing off, populate only fields explicitly present in the request. Do not invent missing values.""",Also applies to: 78-82 🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Create specialized agents that expect typed payloads from the triage | ||||||||||||||||||||||
| # (In a real scenario, the agents would validate and use the payload.) | ||||||||||||||||||||||
| billing_agent = Agent( | ||||||||||||||||||||||
| name="Billing Agent", | ||||||||||||||||||||||
| role="Billing Specialist", | ||||||||||||||||||||||
| goal="Handle all billing-related inquiries and tasks", | ||||||||||||||||||||||
| backstory="I am an expert in billing systems, payment processing, and invoice management." | ||||||||||||||||||||||
| goal="Handle all billing-related inquiries and tasks using structured data", | ||||||||||||||||||||||
| backstory="I am an expert in billing systems, payment processing, and invoice management.", | ||||||||||||||||||||||
| input_payload_schema=BillingPayload # hypothetical but consistent with design | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
Comment on lines
+34
to
35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Locate Agent class and constructor signatures =="
rg -n --type=py -C3 '\bclass\s+Agent\b|def __init__\('
echo "== Find input_payload_schema declaration and consumption =="
rg -n --type=py -C4 '\binput_payload_schema\b'
echo "== Find handoff parsing logic and tuple/schema handling =="
rg -n --type=py -C4 '\bhandoffs\b|HandoffValidationError|model_validate|TypeAdapter|TypedDict|isinstance\([^)]*tuple'Repository: MervinPraison/PraisonAI Length of output: 50380 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Search for input_payload_schema usage =="
rg -n --type=py --hidden --glob '!**/node_modules/**' --glob '!**/.venv/**' "input_payload_schema" . || true
echo
echo "== Search for handoffs usage =="
rg -n --type=py --hidden --glob '!**/node_modules/**' --glob '!**/.venv/**' "\bhandoffs\b" . || true
echo
echo "== Locate the real Agent class implementation =="
rg -n --type=py --hidden --glob '!**/node_modules/**' --glob '!**/.venv/**' "class\s+Agent\b" src examples || true
echo
echo "== Locate Agent.__init__ in implementation files =="
# Try to find __init__ near the Agent class definition lines we found above
rg -n --type=py "class\s+Agent\b" -S src examples | cut -d: -f1 | sort -u | while read -r f; do
echo "--- $f : __init__ signatures near Agent ---"
rg -n --type=py "def __init__\(" "$f" -n || true
doneRepository: MervinPraison/PraisonAI Length of output: 16284 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Show handoff_basic example around Lines 25-90 =="
nl -ba examples/python/handoff/handoff_basic.py | sed -n '1,120p'
echo
echo "== Search for input_payload_schema in implementation =="
rg -n --type=py "input_payload_schema" src/praisonai-agents src/praisonai examples || true
echo
echo "== Inspect Agent.__init__ signature and handoff-related params =="
nl -ba src/praisonai-agents/praisonaiagents/agent/agent.py | sed -n '480,860p'
echo
echo "== Inspect Agent handoff processing code =="
nl -ba src/praisonai-agents/praisonaiagents/agent/agent.py | sed -n '1800,1925p'
nl -ba src/praisonai-agents/praisonaiagents/agent/agent.py | sed -n '4520,4660p'
echo
echo "== Inspect Handoff implementation for schema/tuple handling =="
nl -ba src/praisonai-agents/praisonaiagents/agent/handoff.py | sed -n '1,220p'
nl -ba src/praisonai-agents/praisonaiagents/agent/handoff.py | sed -n '640,880p'
echo
echo "== Search for tuple parsing or (agent, schema) patterns =="
rg -n --type=py "isinstance\([^)]*tuple|Tuple\[|\(.*\,.*\)" src/praisonai-agents/praisonaiagents/agent/handoff.py src/praisonai-agents/praisonaiagents/agent/agent.py || trueRepository: MervinPraison/PraisonAI Length of output: 180 🏁 Script executed: #!/bin/bash
set -euo pipefail
py_print_range () {
local path="$1"; local start="$2"; local end="$3";
python3 - "$path" "$start" "$end" <<'PY'
import sys
path,start,end=sys.argv[1],int(sys.argv[2]),int(sys.argv[3])
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f, start=1):
if start <= i <= end:
print(f"{i:5d} {line.rstrip()}")
elif i > end:
break
PY
}
echo "== handoff_basic.py (lines ~20-90) =="
py_print_range "examples/python/handoff/handoff_basic.py" 20 95
echo
echo "== Agent implementation: signature area (agent.py ~480-860) =="
py_print_range "src/praisonai-agents/praisonaiagents/agent/agent.py" 480 860
echo
echo "== Agent handoff processing (agent.py ~1800-1925) =="
py_print_range "src/praisonai-agents/praisonaiagents/agent/agent.py" 1820 1925
echo
echo "== Agent 'Process handoffs' function (agent.py ~4520-4660) =="
py_print_range "src/praisonai-agents/praisonaiagents/agent/agent.py" 4510 4665
echo
echo "== Handoff implementation: top (handoff.py ~1-220) =="
py_print_range "src/praisonai-agents/praisonaiagents/agent/handoff.py" 1 220
echo
echo "== Handoff implementation: config/schema bits (~640-880) =="
py_print_range "src/praisonai-agents/praisonaiagents/agent/handoff.py" 640 880
echo
echo "== Search for tuple parsing/typing in handoff.py =="
rg -n --type=py "tuple|Tuple\[|isinstance\([^)]*tuple|schema|input_payload_schema" src/praisonai-agents/praisonaiagents/agent/handoff.py || true
echo
echo "== Search for input_payload_schema in Agent+handoff code =="
rg -n --type=py "input_payload_schema" src/praisonai-agents/praisonaiagents/agent/agent.py src/praisonai-agents/praisonaiagents/agent/handoff.py || trueRepository: MervinPraison/PraisonAI Length of output: 50379 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Search for input_payload_schema across codebase =="
rg -n --type=py "input_payload_schema" . || true
echo
echo "== Search for helper function handoff(...) in handoff.py =="
rg -n --type=py "def\s+handoff\s*\(" src/praisonai-agents/praisonaiagents/agent/handoff.py || true
echo
echo "== Show handoff(...) function definition (if present) =="
python3 - <<'PY'
import re, itertools
path="src/praisonai-agents/praisonaiagents/agent/handoff.py"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
for i,l in enumerate(lines, start=1):
if re.search(r"def\s+handoff\s*\(", l):
start=max(1,i-20); end=min(len(lines), i+120)
for j in range(start, end+1):
print(f"{j:5d} {lines[j-1].rstrip()}")
break
PY
echo
echo "== Search for input_type usage in handoff.py =="
rg -n --type=py "input_type|input_filter|__annotations__" src/praisonai-agents/praisonaiagents/agent/handoff.py || true
echo
echo "== Show the part where handoff tool params are built (input_type -> tool schema) =="
python3 - <<'PY'
import re
path="src/praisonai-agents/praisonaiagents/agent/handoff.py"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
# find first occurrence of "input_type" near where tool definition generation happens
hits=[]
for i,l in enumerate(lines, start=1):
if "input_type" in l and ("if self.input_type" in l or "Add input type annotations" in l):
hits.append(i)
if not hits:
# fallback: show around "Add input type annotations"
for i,l in enumerate(lines, start=1):
if "Add input type annotations" in l:
hits.append(i)
if hits:
i=hits[0]
start=max(1,i-60); end=min(len(lines), i+220)
for j in range(start, end+1):
print(f"{j:5d} {lines[j-1].rstrip()}")
else:
print("No matching blocks found.")
PY
echo
echo "== Agent._process_handoffs tuple handling evidence =="
python3 - <<'PY'
path="src/praisonai-agents/praisonaiagents/agent/agent.py"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
# locate def _process_handoffs
import re
idx=None
for i,l in enumerate(lines, start=1):
if re.search(r"def\s+_process_handoffs\s*\(", l):
idx=i; break
if idx:
start=max(1,idx-30); end=min(len(lines), idx+80)
for j in range(start, end+1):
print(f"{j:5d} {lines[j-1].rstrip()}")
else:
print("Couldn't find _process_handoffs")
PY
echo
echo "== Verify Agent __init__ wrapper in src/praisonai/praisonai/agent.py accepts/forwards unknown kwargs =="
python3 - <<'PY'
path="src/praisonai/praisonai/agent.py"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
for i,l in enumerate(lines, start=1):
if "class Agent" in l:
# print some range that includes __init__
pass
# find __init__
import re
for i,l in enumerate(lines, start=1):
if re.search(r"def\s+__init__\s*\(", l) and "cli_backend" in l:
start=max(1,i-20); end=min(len(lines), i+80)
for j in range(start,end+1):
print(f"{j:5d} {lines[j-1].rstrip()}")
break
PYRepository: MervinPraison/PraisonAI Length of output: 29734 Fix typed handoff API usage in
File: 🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| refund_agent = Agent( | ||||||||||||||||||||||
| name="Refund Agent", | ||||||||||||||||||||||
| role="Refund Specialist", | ||||||||||||||||||||||
| goal="Process refund requests and handle refund-related issues", | ||||||||||||||||||||||
| backstory="I specialize in processing refunds, evaluating refund eligibility, and ensuring customer satisfaction." | ||||||||||||||||||||||
| goal="Process refund requests and handle refund-related issues with validated data", | ||||||||||||||||||||||
| backstory="I specialize in processing refunds, evaluating refund eligibility, and ensuring customer satisfaction.", | ||||||||||||||||||||||
| input_payload_schema=RefundPayload | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| technical_support_agent = Agent( | ||||||||||||||||||||||
| name="Technical Support", | ||||||||||||||||||||||
| role="Technical Support Specialist", | ||||||||||||||||||||||
| goal="Resolve technical issues and provide technical assistance", | ||||||||||||||||||||||
| backstory="I am skilled in troubleshooting technical problems and providing solutions." | ||||||||||||||||||||||
| goal="Resolve technical issues and provide technical assistance with structured context", | ||||||||||||||||||||||
| backstory="I am skilled in troubleshooting technical problems and providing solutions.", | ||||||||||||||||||||||
| input_payload_schema=TechnicalPayload | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
Comment on lines
29
to
51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The billing_agent = Agent(
name="Billing Agent",
role="Billing Specialist",
goal="Handle all billing-related inquiries and tasks using structured data",
backstory="I am an expert in billing systems, payment processing, and invoice management."
)
refund_agent = Agent(
name="Refund Agent",
role="Refund Specialist",
goal="Process refund requests and handle refund-related issues with validated data",
backstory="I specialize in processing refunds, evaluating refund eligibility, and ensuring customer satisfaction."
)
technical_support_agent = Agent(
name="Technical Support",
role="Technical Support Specialist",
goal="Resolve technical issues and provide technical assistance with structured context",
backstory="I am skilled in troubleshooting technical problems and providing solutions."
)
Comment on lines
29
to
51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Create a triage agent with handoffs to specialized agents | ||||||||||||||||||||||
| # Create a triage agent with typed handoffs | ||||||||||||||||||||||
| triage_agent = Agent( | ||||||||||||||||||||||
| name="Triage Agent", | ||||||||||||||||||||||
| role="Customer Service Triage", | ||||||||||||||||||||||
| goal="Understand customer needs and route them to the appropriate specialist", | ||||||||||||||||||||||
| goal="Understand customer needs and route them to the appropriate specialist with a validated payload", | ||||||||||||||||||||||
| backstory="I analyze customer requests and direct them to the most suitable specialist for efficient resolution.", | ||||||||||||||||||||||
| instructions="""Analyze the customer's request and determine which specialist can best help: | ||||||||||||||||||||||
| - For billing questions, payment issues, or invoices, transfer to the Billing Agent | ||||||||||||||||||||||
| - For refund requests or refund status inquiries, transfer to the Refund Agent | ||||||||||||||||||||||
| - For technical problems or product issues, transfer to Technical Support | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Always explain why you're transferring the customer before doing so.""", | ||||||||||||||||||||||
| handoffs=[billing_agent, refund_agent, technical_support_agent] | ||||||||||||||||||||||
| Always explain why you're transferring the customer before doing so. | ||||||||||||||||||||||
| When handing off, populate the appropriate payload with extracted data.""", | ||||||||||||||||||||||
| handoffs=[ | ||||||||||||||||||||||
| (billing_agent, BillingPayload), | ||||||||||||||||||||||
| (refund_agent, RefundPayload), | ||||||||||||||||||||||
| (technical_support_agent, TechnicalPayload) | ||||||||||||||||||||||
| ] | ||||||||||||||||||||||
|
Comment on lines
+66
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
Comment on lines
+66
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Example usage | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import the
handoffhelper function frompraisonaiagentsto configure the typed handoffs.