Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions python/packages/core/tests/core/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,21 @@ def test_function_approval_serialization_roundtrip():
# The Content union will need to be handled differently when we fully migrate


def test_function_approval_request_function_call_none_guard():
"""Test that accessing function_call attributes is safe when function_call is None."""
# Construct a Content with type "function_approval_request" but no function_call.
# This verifies the None-guard pattern used in samples to prevent AttributeError.
content = Content("function_approval_request", id="req-none")
assert content.function_call is None

# A proper approval request always has function_call set
fc = Content.from_function_call(call_id="call-1", name="do_something", arguments={"a": 1})
req = Content.from_function_approval_request(id="req-1", function_call=fc)
assert req.function_call is not None
assert req.function_call.name == "do_something"
assert req.function_call.arguments == {"a": 1}


def test_function_approval_accepts_mcp_call():
"""Ensure FunctionApprovalRequestContent supports MCP server tool calls."""
mcp_call = Content.from_mcp_server_tool_call(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@

This sample works as follows:
1. A ConcurrentBuilder workflow is created with two agents running in parallel.
2. Both agents have the same tools, including one requiring approval (execute_trade).
2. Both agents have the same tools, including two requiring approval (execute_trade, set_stop_loss).
3. Both agents receive the same task and work concurrently on their respective stocks.
4. When either agent tries to execute a trade, it triggers an approval request.
4. When either agent tries to execute a trade or set a stop-loss, it triggers an approval request.
5. The sample simulates human approval and the workflow completes.
6. Results from both agents are aggregated and output.

Purpose:
Show how tool call approvals work in parallel execution scenarios where multiple
agents may independently trigger approval requests.
agents may independently trigger approval requests for different tools.

Demonstrate:
- Handling multiple approval requests from different agents in concurrent workflows.
- Handling during concurrent agent execution.
- Handling approval requests for different tools during concurrent agent execution.
- Understanding that approval pauses only the agent that triggered it, not all agents.

Prerequisites:
Expand Down Expand Up @@ -88,6 +88,15 @@ def execute_trade(
return f"Trade executed: {action.upper()} {quantity} shares of {symbol.upper()}"


@tool(approval_mode="always_require")
def set_stop_loss(
symbol: Annotated[str, "The stock ticker symbol"],
stop_price: Annotated[float, "The stop-loss price"],
) -> str:
"""Set a stop-loss order for a stock. Requires human approval due to financial impact."""
return f"Stop-loss set for {symbol.upper()} at ${stop_price:.2f}"


@tool(approval_mode="never_require")
def get_portfolio_balance() -> str:
"""Get current portfolio balance and available funds."""
Expand Down Expand Up @@ -117,14 +126,17 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
if event.type == "request_info" and isinstance(event.data, Content):
# We are only expecting tool approval requests in this sample
requests[event.request_id] = event.data
if event.data.type == "function_approval_request" and event.data.function_call is not None:
print(f"\nApproval requested for tool: {event.data.function_call.name}")
print(f"Arguments: {event.data.function_call.arguments}")
elif event.type == "output":
_print_output(event)

responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
print(f"\nSimulating human approval for: {request.function_call.name}") # type: ignore
if request.type == "function_approval_request" and request.function_call is not None:
print(f"\nSimulating human approval for: {request.function_call.name}")
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)

Expand All @@ -143,18 +155,20 @@ async def main() -> None:
name="MicrosoftAgent",
instructions=(
"You are a personal trading assistant focused on Microsoft (MSFT). "
"You manage my portfolio and take actions based on market data."
"You manage my portfolio and take actions based on market data. "
"Use stop-loss orders to manage risk."
),
tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade],
tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade, set_stop_loss],
)

google_agent = client.as_agent(
name="GoogleAgent",
instructions=(
"You are a personal trading assistant focused on Google (GOOGL). "
"You manage my trades and portfolio based on market conditions."
"You manage my trades and portfolio based on market conditions. "
"Use stop-loss orders to manage risk."
),
tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade],
tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade, set_stop_loss],
)

# 4. Build a concurrent workflow with both agents
Expand All @@ -169,7 +183,8 @@ async def main() -> None:
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"Manage my portfolio. Use a max of 5000 dollars to adjust my position using "
"your best judgment based on market sentiment. No need to confirm trades with me.",
"your best judgment based on market sentiment. Set stop-loss orders to manage risk. "
"No need to confirm trades with me.",
stream=True,
)

Expand All @@ -188,22 +203,32 @@ async def main() -> None:
Approval requested for tool: execute_trade
Arguments: {"symbol":"MSFT","action":"buy","quantity":13}

Approval requested for tool: set_stop_loss
Arguments: {"symbol":"MSFT","stop_price":340.0}

Approval requested for tool: execute_trade
Arguments: {"symbol":"GOOGL","action":"buy","quantity":35}

Approval requested for tool: set_stop_loss
Arguments: {"symbol":"GOOGL","stop_price":126.0}

Simulating human approval for: execute_trade

Simulating human approval for: set_stop_loss

Simulating human approval for: execute_trade

Simulating human approval for: set_stop_loss

------------------------------------------------------------
Workflow completed. Aggregated results from both agents:
- user: Manage my portfolio. Use a max of 5000 dollars to adjust my position using your best judgment based on
market sentiment. No need to confirm trades with me.
- MicrosoftAgent: I have successfully executed the trade, purchasing 13 shares of Microsoft (MSFT). This action
was based on the positive market sentiment and available funds within the specified limit.
Your portfolio has been adjusted accordingly.
- GoogleAgent: I have successfully executed the trade, purchasing 35 shares of GOOGL. If you need further
assistance or any adjustments, feel free to ask!
market sentiment. Set stop-loss orders to manage risk. No need to confirm trades with me.
- MicrosoftAgent: I have successfully purchased 13 shares of Microsoft (MSFT) and set a stop-loss at $340.00.
This action was based on the positive market sentiment and available funds within the
specified limit. Your portfolio has been adjusted accordingly.
- GoogleAgent: I have successfully purchased 35 shares of GOOGL and set a stop-loss at $126.00. If you need
further assistance or any adjustments, feel free to ask!
"""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
if request.type == "function_approval_request" and request.function_call is not None:
print("\n[APPROVAL REQUIRED]")
print(f" Tool: {request.function_call.name}") # type: ignore
print(f" Arguments: {request.function_call.arguments}") # type: ignore
print(f"Simulating human approval for: {request.function_call.name}") # type: ignore
print(f" Tool: {request.function_call.name}")
print(f" Arguments: {request.function_call.arguments}")
print(f"Simulating human approval for: {request.function_call.name}")
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
if request.type == "function_approval_request" and request.function_call is not None:
print("\n[APPROVAL REQUIRED]")
print(f" Tool: {request.function_call.name}") # type: ignore
print(f" Arguments: {request.function_call.arguments}") # type: ignore
print(f"Simulating human approval for: {request.function_call.name}") # type: ignore
print(f" Tool: {request.function_call.name}")
print(f" Arguments: {request.function_call.arguments}")
print(f"Simulating human approval for: {request.function_call.name}")
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)

Expand Down
Loading