chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Make the examples directory into a package to avoid top-level module name collisions.
|
||||
# This is needed so that mypy treats files like examples/customer_service/main.py and
|
||||
# examples/researcher_app/main.py as distinct modules rather than both named "main".
|
||||
@@ -0,0 +1,63 @@
|
||||
# Common agentic patterns
|
||||
|
||||
This folder contains examples of different common patterns for agents.
|
||||
|
||||
## Hosted multi-agent (experimental)
|
||||
|
||||
The Responses API can coordinate server-hosted GPT-5.6 subagents while the normal SDK Runner executes developer-defined function tools locally. See [`hosted_multi_agent_beta.py`](./hosted_multi_agent_beta.py) for non-streaming and streaming examples using the experimental model module.
|
||||
|
||||
## Deterministic flows
|
||||
|
||||
A common tactic is to break down a task into a series of smaller steps. Each task can be performed by an agent, and the output of one agent is used as input to the next. For example, if your task was to generate a story, you could break it down into the following steps:
|
||||
|
||||
1. Generate an outline
|
||||
2. Generate the story
|
||||
3. Generate the ending
|
||||
|
||||
Each of these steps can be performed by an agent. The output of one agent is used as input to the next.
|
||||
|
||||
See the [`deterministic.py`](./deterministic.py) file for an example of this.
|
||||
|
||||
## Handoffs and routing
|
||||
|
||||
In many situations, you have specialized sub-agents that handle specific tasks. You can use handoffs to route the task to the right agent.
|
||||
|
||||
For example, you might have a frontline agent that receives a request, and then hands off to a specialized agent based on the language of the request. See the [`routing.py`](./routing.py) file for an example of this.
|
||||
|
||||
## Agents as tools
|
||||
|
||||
The mental model for handoffs is that the new agent "takes over". It sees the previous conversation history, and owns the conversation from that point onwards. However, this is not the only way to use agents. You can also use agents as a tool - the tool agent goes off and runs on its own, and then returns the result to the original agent.
|
||||
|
||||
For example, you could model the translation task above as tool calls instead: rather than handing over to the language-specific agent, you could call the agent as a tool, and then use the result in the next step. This enables things like translating multiple languages at once.
|
||||
|
||||
See the [`agents_as_tools.py`](./agents_as_tools.py) file for an example of this. See the [`agents_as_tools_streaming.py`](./agents_as_tools_streaming.py) file for a streaming variant that taps into nested agent events via `on_stream`. See the [`agents_as_tools_structured.py`](./agents_as_tools_structured.py) file for a structured-input variant using `Agent.as_tool()` parameters.
|
||||
|
||||
## LLM-as-a-judge
|
||||
|
||||
LLMs can often improve the quality of their output if given feedback. A common pattern is to generate a response using a model, and then use a second model to provide feedback. You can even use a small model for the initial generation and a larger model for the feedback, to optimize cost.
|
||||
|
||||
For example, you could use an LLM to generate an outline for a story, and then use a second LLM to evaluate the outline and provide feedback. You can then use the feedback to improve the outline, and repeat until the LLM is satisfied with the outline.
|
||||
|
||||
See the [`llm_as_a_judge.py`](./llm_as_a_judge.py) file for an example of this.
|
||||
|
||||
## Parallelization
|
||||
|
||||
Running multiple agents in parallel is a common pattern. This can be useful for both latency (e.g. if you have multiple steps that don't depend on each other) and also for other reasons e.g. generating multiple responses and picking the best one.
|
||||
|
||||
See the [`parallelization.py`](./parallelization.py) file for an example of this. It runs a translation agent multiple times in parallel, and then picks the best translation.
|
||||
|
||||
## Guardrails
|
||||
|
||||
Related to parallelization, you often want to run input guardrails to make sure the inputs to your agents are valid. For example, if you have a customer support agent, you might want to make sure that the user isn't trying to ask for help with a math problem.
|
||||
|
||||
You can definitely do this without any special Agents SDK features by using parallelization, but we support a special guardrail primitive. Guardrails can have a "tripwire" - if the tripwire is triggered, the agent execution will immediately stop and a `GuardrailTripwireTriggered` exception will be raised.
|
||||
|
||||
This is really useful for latency: for example, you might have a very fast model that runs the guardrail and a slow model that runs the actual agent. You wouldn't want to wait for the slow model to finish, so guardrails let you quickly reject invalid inputs.
|
||||
|
||||
See the [`input_guardrails.py`](./input_guardrails.py) and [`output_guardrails.py`](./output_guardrails.py) files for examples.
|
||||
|
||||
## Human in the loop
|
||||
|
||||
You can pause runs for manual approval before executing sensitive tools. This is useful for operations like sending money, deleting data, or running destructive commands.
|
||||
|
||||
See [`human_in_the_loop.py`](./human_in_the_loop.py) for the base approval flow and [`human_in_the_loop_custom_rejection.py`](./human_in_the_loop_custom_rejection.py) for run-level tool error formatting when approvals are rejected.
|
||||
@@ -0,0 +1,83 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, ItemHelpers, MessageOutputItem, Runner, trace
|
||||
from examples.auto_mode import input_with_fallback
|
||||
|
||||
"""
|
||||
This example shows the agents-as-tools pattern. The frontline agent receives a user message and
|
||||
then picks which agents to call, as tools. In this case, it picks from a set of translation
|
||||
agents.
|
||||
"""
|
||||
|
||||
spanish_agent = Agent(
|
||||
name="spanish_agent",
|
||||
instructions="You translate the user's message to Spanish",
|
||||
handoff_description="An english to spanish translator",
|
||||
)
|
||||
|
||||
french_agent = Agent(
|
||||
name="french_agent",
|
||||
instructions="You translate the user's message to French",
|
||||
handoff_description="An english to french translator",
|
||||
)
|
||||
|
||||
italian_agent = Agent(
|
||||
name="italian_agent",
|
||||
instructions="You translate the user's message to Italian",
|
||||
handoff_description="An english to italian translator",
|
||||
)
|
||||
|
||||
orchestrator_agent = Agent(
|
||||
name="orchestrator_agent",
|
||||
instructions=(
|
||||
"You are a translation agent. You use the tools given to you to translate."
|
||||
"If asked for multiple translations, you call the relevant tools in order."
|
||||
"You never translate on your own, you always use the provided tools."
|
||||
),
|
||||
tools=[
|
||||
spanish_agent.as_tool(
|
||||
tool_name="translate_to_spanish",
|
||||
tool_description="Translate the user's message to Spanish",
|
||||
),
|
||||
french_agent.as_tool(
|
||||
tool_name="translate_to_french",
|
||||
tool_description="Translate the user's message to French",
|
||||
),
|
||||
italian_agent.as_tool(
|
||||
tool_name="translate_to_italian",
|
||||
tool_description="Translate the user's message to Italian",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
synthesizer_agent = Agent(
|
||||
name="synthesizer_agent",
|
||||
instructions="You inspect translations, correct them if needed, and produce a final concatenated response.",
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
msg = input_with_fallback(
|
||||
"Hi! What would you like translated, and to which languages? ",
|
||||
"Translate 'Hello, world!' to French and Spanish.",
|
||||
)
|
||||
|
||||
# Run the entire orchestration in a single trace
|
||||
with trace("Orchestrator evaluator"):
|
||||
orchestrator_result = await Runner.run(orchestrator_agent, msg)
|
||||
|
||||
for item in orchestrator_result.new_items:
|
||||
if isinstance(item, MessageOutputItem):
|
||||
text = ItemHelpers.text_message_output(item)
|
||||
if text:
|
||||
print(f" - Translation step: {text}")
|
||||
|
||||
synthesizer_result = await Runner.run(
|
||||
synthesizer_agent, orchestrator_result.to_input_list()
|
||||
)
|
||||
|
||||
print(f"\n\nFinal response:\n{synthesizer_result.final_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,159 @@
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent, AgentBase, ModelSettings, RunContextWrapper, Runner, trace
|
||||
from agents.tool import function_tool
|
||||
from examples.auto_mode import confirm_with_fallback, input_with_fallback, is_auto_mode
|
||||
|
||||
"""
|
||||
This example demonstrates the agents-as-tools pattern with conditional tool enabling.
|
||||
Agent tools are dynamically enabled/disabled based on user access levels using the
|
||||
is_enabled parameter.
|
||||
"""
|
||||
|
||||
|
||||
class AppContext(BaseModel):
|
||||
language_preference: str = "spanish_only" # "spanish_only", "french_spanish", "european"
|
||||
|
||||
|
||||
def french_spanish_enabled(ctx: RunContextWrapper[AppContext], agent: AgentBase) -> bool:
|
||||
"""Enable for French+Spanish and European preferences."""
|
||||
return ctx.context.language_preference in ["french_spanish", "european"]
|
||||
|
||||
|
||||
def european_enabled(ctx: RunContextWrapper[AppContext], agent: AgentBase) -> bool:
|
||||
"""Only enable for European preference."""
|
||||
return ctx.context.language_preference == "european"
|
||||
|
||||
|
||||
@function_tool(needs_approval=True)
|
||||
async def get_user_name() -> str:
|
||||
print("Getting the user's name...")
|
||||
return "Kaz"
|
||||
|
||||
|
||||
# Create specialized agents
|
||||
spanish_agent = Agent(
|
||||
name="spanish_agent",
|
||||
instructions=(
|
||||
"Respond in Spanish. Call get_user_name exactly once before replying, then greet that "
|
||||
"user by name and answer the user's question in a non-empty final response."
|
||||
),
|
||||
model_settings=ModelSettings(tool_choice="required"),
|
||||
tools=[get_user_name],
|
||||
)
|
||||
|
||||
french_agent = Agent(
|
||||
name="french_agent",
|
||||
instructions="You respond in French. Always reply to the user's question in French.",
|
||||
)
|
||||
|
||||
italian_agent = Agent(
|
||||
name="italian_agent",
|
||||
instructions="You respond in Italian. Always reply to the user's question in Italian.",
|
||||
)
|
||||
|
||||
# Create orchestrator with conditional tools
|
||||
orchestrator = Agent(
|
||||
name="orchestrator",
|
||||
instructions=(
|
||||
"You are a multilingual assistant. Call each available language tool requested by the "
|
||||
"user exactly once, including every requested tool when multiple languages are requested. "
|
||||
"Wait for all tool calls to finish, then combine their responses into a non-empty final "
|
||||
"response. Never translate the user's request yourself."
|
||||
),
|
||||
tools=[
|
||||
spanish_agent.as_tool(
|
||||
tool_name="respond_spanish",
|
||||
tool_description="Respond to the user's question in Spanish",
|
||||
is_enabled=True, # Always enabled
|
||||
needs_approval=True, # HITL
|
||||
),
|
||||
french_agent.as_tool(
|
||||
tool_name="respond_french",
|
||||
tool_description="Respond to the user's question in French",
|
||||
is_enabled=french_spanish_enabled,
|
||||
),
|
||||
italian_agent.as_tool(
|
||||
tool_name="respond_italian",
|
||||
tool_description="Respond to the user's question in Italian",
|
||||
is_enabled=european_enabled,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Interactive demo with LLM interaction."""
|
||||
print("Agents-as-Tools with Conditional Enabling\n")
|
||||
print(
|
||||
"This demonstrates how language response tools are dynamically enabled based on user preferences.\n"
|
||||
)
|
||||
|
||||
print("Choose language preference:")
|
||||
print("1. Spanish only (1 tool)")
|
||||
print("2. French and Spanish (2 tools)")
|
||||
print("3. European languages (3 tools)")
|
||||
|
||||
choice = input_with_fallback("\nSelect option (1-3): ", "2").strip()
|
||||
preference_map = {"1": "spanish_only", "2": "french_spanish", "3": "european"}
|
||||
language_preference = preference_map.get(choice, "spanish_only")
|
||||
|
||||
# Create context and show available tools
|
||||
context = RunContextWrapper(AppContext(language_preference=language_preference))
|
||||
available_tools = await orchestrator.get_all_tools(context)
|
||||
tool_names = [tool.name for tool in available_tools]
|
||||
|
||||
print(f"\nLanguage preference: {language_preference}")
|
||||
print(f"Available tools: {', '.join(tool_names)}")
|
||||
print(f"The LLM will only see and can use these {len(available_tools)} tools\n")
|
||||
|
||||
# Get user request
|
||||
user_request = input_with_fallback(
|
||||
"Ask a question and see responses in available languages:\n",
|
||||
"Answer in Spanish and French: How do you say good morning?",
|
||||
)
|
||||
|
||||
# Run with LLM interaction
|
||||
print("\nProcessing request...")
|
||||
with trace("Conditional tool access"):
|
||||
result = await Runner.run(
|
||||
starting_agent=orchestrator,
|
||||
input=user_request,
|
||||
context=context.context,
|
||||
)
|
||||
while result.interruptions:
|
||||
|
||||
async def confirm(question: str) -> bool:
|
||||
return confirm_with_fallback(f"{question} (y/n): ", default=True)
|
||||
|
||||
state = result.to_state()
|
||||
for interruption in result.interruptions:
|
||||
prompt = f"\nDo you approve this tool call: {interruption.name} with arguments {interruption.arguments}?"
|
||||
confirmed = await confirm(prompt)
|
||||
if confirmed:
|
||||
state.approve(interruption)
|
||||
print(f"✓ Approved: {interruption.name}")
|
||||
else:
|
||||
state.reject(interruption)
|
||||
print(f"✗ Rejected: {interruption.name}")
|
||||
result = await Runner.run(orchestrator, state)
|
||||
|
||||
if is_auto_mode():
|
||||
called_tools: list[str] = []
|
||||
for item in result.new_items:
|
||||
tool_name = getattr(item.raw_item, "name", None)
|
||||
if item.type == "tool_call_item" and isinstance(tool_name, str):
|
||||
if tool_name.startswith("respond_"):
|
||||
called_tools.append(tool_name)
|
||||
if sorted(called_tools) != ["respond_french", "respond_spanish"]:
|
||||
raise RuntimeError(f"Expected Spanish and French responses once, got {called_tools}")
|
||||
if not result.final_output:
|
||||
raise RuntimeError("Expected a non-empty multilingual response")
|
||||
|
||||
print(f"\nResponse:\n{result.final_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,59 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, AgentToolStreamEvent, ModelSettings, Runner, function_tool, trace
|
||||
|
||||
|
||||
@function_tool(
|
||||
name_override="billing_status_checker",
|
||||
description_override="Answer questions about customer billing status.",
|
||||
)
|
||||
def billing_status_checker(customer_id: str | None = None, question: str = "") -> str:
|
||||
"""Return a canned billing answer or a fallback when the question is unrelated."""
|
||||
normalized = question.lower()
|
||||
if "bill" in normalized or "billing" in normalized:
|
||||
return f"This customer (ID: {customer_id})'s bill is $100"
|
||||
return "I can only answer questions about billing."
|
||||
|
||||
|
||||
def handle_stream(event: AgentToolStreamEvent) -> None:
|
||||
"""Print streaming events emitted by the nested billing agent."""
|
||||
stream = event["event"]
|
||||
tool_call = event.get("tool_call")
|
||||
tool_call_info = tool_call.call_id if tool_call is not None else "unknown"
|
||||
print(f"[stream] agent={event['agent'].name} call={tool_call_info} type={stream.type} {stream}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
with trace("Agents as tools streaming example"):
|
||||
billing_agent = Agent(
|
||||
name="Billing Agent",
|
||||
instructions="You are a billing agent that answers billing questions.",
|
||||
model_settings=ModelSettings(tool_choice="required"),
|
||||
tools=[billing_status_checker],
|
||||
)
|
||||
|
||||
billing_agent_tool = billing_agent.as_tool(
|
||||
tool_name="billing_agent",
|
||||
tool_description="You are a billing agent that answers billing questions.",
|
||||
on_stream=handle_stream,
|
||||
)
|
||||
|
||||
main_agent = Agent(
|
||||
name="Customer Support Agent",
|
||||
instructions=(
|
||||
"You are a customer support agent. Always call the billing agent to answer billing "
|
||||
"questions and return the billing agent response to the user."
|
||||
),
|
||||
tools=[billing_agent_tool],
|
||||
)
|
||||
|
||||
result = await Runner.run(
|
||||
main_agent,
|
||||
"Hello, my customer ID is ABC123. How much is my bill for this month?",
|
||||
)
|
||||
|
||||
print(f"\nFinal response:\n{result.final_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,64 @@
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agents import Agent, Runner
|
||||
|
||||
"""
|
||||
This example shows structured input for agent-as-tool calls.
|
||||
"""
|
||||
|
||||
|
||||
class TranslationInput(BaseModel):
|
||||
text: str = Field(description="Text to translate.")
|
||||
source: str = Field(description="Source language code or name.")
|
||||
target: str = Field(description="Target language code or name.")
|
||||
|
||||
|
||||
translator = Agent(
|
||||
name="translator",
|
||||
instructions=(
|
||||
"Translate the input text into the target language. "
|
||||
"If the target is not clear, ask the user for clarification."
|
||||
),
|
||||
)
|
||||
|
||||
orchestrator = Agent(
|
||||
name="orchestrator",
|
||||
instructions=(
|
||||
"You are a task dispatcher. Always call the tool with sufficient input. "
|
||||
"Do not handle the translation yourself."
|
||||
),
|
||||
tools=[
|
||||
translator.as_tool(
|
||||
tool_name="translate_text",
|
||||
tool_description=(
|
||||
"Translate text between languages. Provide text, source language, "
|
||||
"and target language."
|
||||
),
|
||||
parameters=TranslationInput,
|
||||
# By default, the input schema will be included in a simpler format.
|
||||
# Set include_input_schema to true to include the full JSON Schema:
|
||||
# include_input_schema=True,
|
||||
# Build a custom prompt from structured input data:
|
||||
# input_builder=lambda options: (
|
||||
# f'Translate the text "{options["params"]["text"]}" '
|
||||
# f'from {options["params"]["source"]} to {options["params"]["target"]}.'
|
||||
# ),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
query = 'Translate "Hola" from Spanish to French.'
|
||||
|
||||
response1 = await Runner.run(translator, query)
|
||||
print(f"Translator agent direct run: {response1.final_output}")
|
||||
|
||||
response2 = await Runner.run(orchestrator, query)
|
||||
print(f"Translator agent as tool: {response2.final_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,84 @@
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent, Runner, trace
|
||||
from examples.auto_mode import input_with_fallback
|
||||
|
||||
"""
|
||||
This example demonstrates a deterministic flow, where each step is performed by an agent.
|
||||
1. The first agent generates a story outline
|
||||
2. We feed the outline into the second agent
|
||||
3. The second agent checks if the outline is good quality and if it is a scifi story
|
||||
4. If the outline is not good quality or not a scifi story, we stop here
|
||||
5. If the outline is good quality and a scifi story, we feed the outline into the third agent
|
||||
6. The third agent writes the story
|
||||
"""
|
||||
|
||||
story_outline_agent = Agent(
|
||||
name="story_outline_agent",
|
||||
instructions="Generate a very short story outline based on the user's input.",
|
||||
)
|
||||
|
||||
|
||||
class OutlineCheckerOutput(BaseModel):
|
||||
good_quality: bool
|
||||
is_scifi: bool
|
||||
|
||||
|
||||
outline_checker_agent = Agent(
|
||||
name="outline_checker_agent",
|
||||
instructions="Read the given story outline, and judge the quality. Also, determine if it is a scifi story.",
|
||||
output_type=OutlineCheckerOutput,
|
||||
)
|
||||
|
||||
story_agent = Agent(
|
||||
name="story_agent",
|
||||
instructions="Write a short story based on the given outline.",
|
||||
output_type=str,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
input_prompt = input_with_fallback(
|
||||
"What kind of story do you want? ",
|
||||
"Write a short sci-fi story.",
|
||||
)
|
||||
|
||||
# Ensure the entire workflow is a single trace
|
||||
with trace("Deterministic story flow"):
|
||||
# 1. Generate an outline
|
||||
outline_result = await Runner.run(
|
||||
story_outline_agent,
|
||||
input_prompt,
|
||||
)
|
||||
print("Outline generated")
|
||||
|
||||
# 2. Check the outline
|
||||
outline_checker_result = await Runner.run(
|
||||
outline_checker_agent,
|
||||
outline_result.final_output,
|
||||
)
|
||||
|
||||
# 3. Add a gate to stop if the outline is not good quality or not a scifi story
|
||||
assert isinstance(outline_checker_result.final_output, OutlineCheckerOutput)
|
||||
if not outline_checker_result.final_output.good_quality:
|
||||
print("Outline is not good quality, so we stop here.")
|
||||
exit(0)
|
||||
|
||||
if not outline_checker_result.final_output.is_scifi:
|
||||
print("Outline is not a scifi story, so we stop here.")
|
||||
exit(0)
|
||||
|
||||
print("Outline is good quality and a scifi story, so we continue to write the story.")
|
||||
|
||||
# 4. Write the story
|
||||
story_result = await Runner.run(
|
||||
story_agent,
|
||||
outline_result.final_output,
|
||||
)
|
||||
print(f"Story: {story_result.final_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,112 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
FunctionToolResult,
|
||||
ModelSettings,
|
||||
RunContextWrapper,
|
||||
Runner,
|
||||
ToolsToFinalOutputFunction,
|
||||
ToolsToFinalOutputResult,
|
||||
function_tool,
|
||||
)
|
||||
from examples.auto_mode import is_auto_mode
|
||||
|
||||
"""
|
||||
This example shows how to force the agent to use a tool. It uses `ModelSettings(tool_choice="required")`
|
||||
to force the agent to use any tool.
|
||||
|
||||
You can run it with 3 options:
|
||||
1. `default`: The default behavior, which is to send the tool output to the LLM. In this case,
|
||||
`tool_choice` is not set, because otherwise it would result in an infinite loop - the LLM would
|
||||
call the tool, the tool would run and send the results to the LLM, and that would repeat
|
||||
(because the model is forced to use a tool every time.)
|
||||
2. `first_tool_result`: The first tool result is used as the final output.
|
||||
3. `custom`: A custom tool use behavior function is used. The custom function receives all the tool
|
||||
results, and chooses to use the first tool result to generate the final output.
|
||||
|
||||
Usage:
|
||||
python examples/agent_patterns/forcing_tool_use.py -t default
|
||||
python examples/agent_patterns/forcing_tool_use.py -t first_tool
|
||||
python examples/agent_patterns/forcing_tool_use.py -t custom
|
||||
"""
|
||||
|
||||
|
||||
class Weather(BaseModel):
|
||||
city: str
|
||||
temperature_range: str
|
||||
conditions: str
|
||||
|
||||
|
||||
@function_tool
|
||||
def get_weather(city: str) -> Weather:
|
||||
print("[debug] get_weather called")
|
||||
return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind")
|
||||
|
||||
|
||||
async def custom_tool_use_behavior(
|
||||
context: RunContextWrapper[Any], results: list[FunctionToolResult]
|
||||
) -> ToolsToFinalOutputResult:
|
||||
weather: Weather = results[0].output
|
||||
return ToolsToFinalOutputResult(
|
||||
is_final_output=True, final_output=f"{weather.city} is {weather.conditions}."
|
||||
)
|
||||
|
||||
|
||||
async def main(tool_use_behavior: Literal["default", "first_tool", "custom"] = "default"):
|
||||
if tool_use_behavior == "default":
|
||||
behavior: Literal["run_llm_again", "stop_on_first_tool"] | ToolsToFinalOutputFunction = (
|
||||
"run_llm_again"
|
||||
)
|
||||
elif tool_use_behavior == "first_tool":
|
||||
behavior = "stop_on_first_tool"
|
||||
elif tool_use_behavior == "custom":
|
||||
behavior = custom_tool_use_behavior
|
||||
|
||||
agent = Agent(
|
||||
name="Weather agent",
|
||||
instructions="You are a helpful agent.",
|
||||
tools=[get_weather],
|
||||
tool_use_behavior=behavior,
|
||||
model_settings=ModelSettings(
|
||||
tool_choice="required" if tool_use_behavior != "default" else None
|
||||
),
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, input="What's the weather in Tokyo?")
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
async def auto_demo() -> None:
|
||||
for behavior in ("default", "first_tool", "custom"):
|
||||
print(f"=== {behavior} ===")
|
||||
await main(behavior)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--tool-use-behavior",
|
||||
type=str,
|
||||
default="default",
|
||||
choices=["default", "first_tool", "custom"],
|
||||
help=(
|
||||
"The behavior to use for tool use. "
|
||||
"default sends tool outputs back to the model, first_tool uses the first tool result as the final output, "
|
||||
"custom runs a custom tool use behavior function."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if is_auto_mode():
|
||||
asyncio.run(auto_demo())
|
||||
else:
|
||||
asyncio.run(main(args.tool_use_behavior))
|
||||
@@ -0,0 +1,92 @@
|
||||
# Copy/paste command (does not modify uv.lock):
|
||||
# uv run -m examples.agent_patterns.hosted_multi_agent_beta --mode stream
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from agents import Agent, Runner, function_tool
|
||||
from agents.extensions.experimental.hosted_multi_agent import (
|
||||
OpenAIHostedMultiAgentModel,
|
||||
get_hosted_agent_metadata,
|
||||
)
|
||||
from agents.tool_context import ToolContext
|
||||
|
||||
PROPOSALS = {
|
||||
"alpha": {"estimated_weeks": 6, "risk": "medium"},
|
||||
"beta": {"estimated_weeks": 8, "risk": "low"},
|
||||
}
|
||||
|
||||
|
||||
@function_tool
|
||||
def get_proposal(ctx: ToolContext[Any], proposal: str) -> dict[str, object]:
|
||||
"""Return deterministic details for one proposal."""
|
||||
metadata = get_hosted_agent_metadata(ctx)
|
||||
caller = metadata.agent_name if metadata else "unknown"
|
||||
print(f"local tool caller={caller} call_id={ctx.tool_call_id} proposal={proposal}")
|
||||
if proposal not in PROPOSALS:
|
||||
raise ValueError(f"Unknown proposal: {proposal}")
|
||||
return PROPOSALS[proposal]
|
||||
|
||||
|
||||
def build_agent() -> Agent[Any]:
|
||||
return Agent(
|
||||
name="Hosted proposal coordinator",
|
||||
instructions=(
|
||||
"Create two subagents. Ask one to inspect proposal alpha and the other to inspect "
|
||||
"proposal beta. Each subagent must call get_proposal for its assigned proposal. "
|
||||
"Wait for both results, then return one concise comparison from the root agent."
|
||||
),
|
||||
model=OpenAIHostedMultiAgentModel(
|
||||
model="gpt-5.6-sol",
|
||||
config={"max_concurrent_subagents": 3},
|
||||
),
|
||||
tools=[get_proposal],
|
||||
)
|
||||
|
||||
|
||||
def log_hosted_event(event: object) -> None:
|
||||
if getattr(event, "type", None) != "response.output_item.done":
|
||||
return
|
||||
item = getattr(event, "item", None)
|
||||
item_type = item.get("type") if isinstance(item, Mapping) else getattr(item, "type", None)
|
||||
metadata = get_hosted_agent_metadata(item)
|
||||
is_hosted_item = item_type in {
|
||||
"agent_message",
|
||||
"multi_agent_call",
|
||||
"multi_agent_call_output",
|
||||
}
|
||||
is_subagent_message = (
|
||||
item_type == "message" and metadata is not None and metadata.agent_name != "/root"
|
||||
)
|
||||
if is_hosted_item or is_subagent_message:
|
||||
caller = metadata.agent_name if metadata else "unknown"
|
||||
print(f"hosted item type={item_type} agent={caller}")
|
||||
|
||||
|
||||
async def run_nonstreamed() -> None:
|
||||
result = await Runner.run(build_agent(), "Compare proposal alpha and proposal beta.")
|
||||
print(f"\nFinal response:\n{result.final_output}")
|
||||
|
||||
|
||||
async def run_streamed() -> None:
|
||||
result = Runner.run_streamed(build_agent(), "Compare proposal alpha and proposal beta.")
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event":
|
||||
log_hosted_event(event.data)
|
||||
print(f"\nFinal response:\n{result.final_output}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--mode", choices=("nonstream", "stream"), default="nonstream")
|
||||
args = parser.parse_args()
|
||||
if args.mode == "stream":
|
||||
await run_streamed()
|
||||
else:
|
||||
await run_nonstreamed()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Human-in-the-loop example with tool approval.
|
||||
|
||||
This example demonstrates how to:
|
||||
1. Define tools that require approval before execution
|
||||
2. Handle interruptions when tool approval is needed
|
||||
3. Serialize/deserialize run state to continue execution later
|
||||
4. Approve or reject tool calls based on user input
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from agents import Agent, Runner, RunState, function_tool
|
||||
from examples.auto_mode import confirm_with_fallback
|
||||
|
||||
|
||||
@function_tool
|
||||
async def get_weather(city: str) -> str:
|
||||
"""Get the weather for a given city.
|
||||
|
||||
Args:
|
||||
city: The city to get weather for.
|
||||
|
||||
Returns:
|
||||
Weather information for the city.
|
||||
"""
|
||||
return f"The weather in {city} is sunny"
|
||||
|
||||
|
||||
async def _needs_temperature_approval(_ctx, params, _call_id) -> bool:
|
||||
"""Check if temperature tool needs approval."""
|
||||
return "Oakland" in params.get("city", "")
|
||||
|
||||
|
||||
@function_tool(
|
||||
# Dynamic approval: only require approval for Oakland
|
||||
needs_approval=_needs_temperature_approval
|
||||
)
|
||||
async def get_temperature(city: str) -> str:
|
||||
"""Get the temperature for a given city.
|
||||
|
||||
Args:
|
||||
city: The city to get temperature for.
|
||||
|
||||
Returns:
|
||||
Temperature information for the city.
|
||||
"""
|
||||
return f"The temperature in {city} is 20° Celsius"
|
||||
|
||||
|
||||
# Main agent with tool that requires approval
|
||||
agent = Agent(
|
||||
name="Weather Assistant",
|
||||
instructions=(
|
||||
"You are a helpful weather assistant. "
|
||||
"Answer questions about weather and temperature using the available tools."
|
||||
),
|
||||
tools=[get_weather, get_temperature],
|
||||
)
|
||||
|
||||
RESULT_PATH = Path(".cache/agent_patterns/human_in_the_loop/result.json")
|
||||
|
||||
|
||||
async def confirm(question: str) -> bool:
|
||||
"""Prompt user for yes/no confirmation.
|
||||
|
||||
Args:
|
||||
question: The question to ask.
|
||||
|
||||
Returns:
|
||||
True if user confirms, False otherwise.
|
||||
"""
|
||||
return confirm_with_fallback(f"{question} (y/n): ", default=True)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the human-in-the-loop example."""
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What is the weather and temperature in Oakland?",
|
||||
)
|
||||
|
||||
has_interruptions = len(result.interruptions) > 0
|
||||
|
||||
while has_interruptions:
|
||||
print("\n" + "=" * 80)
|
||||
print("Run interrupted - tool approval required")
|
||||
print("=" * 80)
|
||||
|
||||
# Storing state to file (demonstrating serialization)
|
||||
state = result.to_state()
|
||||
state_json = state.to_json()
|
||||
RESULT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with RESULT_PATH.open("w") as f:
|
||||
json.dump(state_json, f, indent=2)
|
||||
|
||||
print(f"State saved to {RESULT_PATH}")
|
||||
|
||||
# From here on you could run things on a different thread/process
|
||||
|
||||
# Reading state from file (demonstrating deserialization)
|
||||
print(f"Loading state from {RESULT_PATH}")
|
||||
with RESULT_PATH.open() as f:
|
||||
stored_state_json = json.load(f)
|
||||
|
||||
state = await RunState.from_json(agent, stored_state_json)
|
||||
|
||||
# Process each interruption
|
||||
for interruption in result.interruptions:
|
||||
print("\nTool call details:")
|
||||
print(f" Agent: {interruption.agent.name}")
|
||||
print(f" Tool: {interruption.name}")
|
||||
print(f" Arguments: {interruption.arguments}")
|
||||
|
||||
confirmed = await confirm("\nDo you approve this tool call?")
|
||||
|
||||
if confirmed:
|
||||
print(f"✓ Approved: {interruption.name}")
|
||||
state.approve(interruption)
|
||||
else:
|
||||
print(f"✗ Rejected: {interruption.name}")
|
||||
state.reject(interruption)
|
||||
|
||||
# Resume execution with the updated state
|
||||
print("\nResuming agent execution...")
|
||||
result = await Runner.run(agent, state)
|
||||
has_interruptions = len(result.interruptions) > 0
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("Final Output:")
|
||||
print("=" * 80)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Human-in-the-loop example with a custom rejection message.
|
||||
|
||||
This example is intentionally minimal:
|
||||
1. A single sensitive tool requires human approval.
|
||||
2. The first turn always issues that tool call.
|
||||
3. ``tool_error_formatter`` defines the universal fallback message shape.
|
||||
4. A per-call ``rejection_message`` passed to ``state.reject(...)`` overrides that fallback.
|
||||
5. The example prints both the tool output and the assistant's final reply.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
ModelSettings,
|
||||
RunConfig,
|
||||
Runner,
|
||||
ToolErrorFormatterArgs,
|
||||
function_tool,
|
||||
)
|
||||
from examples.auto_mode import confirm_with_fallback
|
||||
|
||||
|
||||
async def tool_error_formatter(args: ToolErrorFormatterArgs[None]) -> str | None:
|
||||
"""Build the universal fallback output message for rejected tool calls."""
|
||||
if args.kind != "approval_rejected":
|
||||
return None
|
||||
# The default message is "Tool execution was not approved."
|
||||
return "Publish action was canceled because approval was rejected."
|
||||
|
||||
|
||||
@function_tool(needs_approval=True)
|
||||
async def publish_announcement(title: str, body: str) -> str:
|
||||
"""Simulate publishing an announcement to users."""
|
||||
return f"Published announcement '{title}' with body: {body}"
|
||||
|
||||
|
||||
def _find_formatter_output(result: object) -> str | None:
|
||||
items = getattr(result, "new_items", None)
|
||||
if not isinstance(items, list):
|
||||
return None
|
||||
|
||||
for item in items:
|
||||
if getattr(item, "type", None) != "tool_call_output_item":
|
||||
continue
|
||||
output = getattr(item, "output", None)
|
||||
if isinstance(output, str):
|
||||
return output
|
||||
return None
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
agent = Agent(
|
||||
name="Operations Assistant",
|
||||
instructions=(
|
||||
"When a user asks to publish an announcement, call the publish_announcement tool directly. "
|
||||
"Do not ask the user for approval in plain text; runtime approvals handle that. "
|
||||
"If the tool call is rejected, respond with the exact rejection message and nothing else."
|
||||
),
|
||||
model_settings=ModelSettings(tool_choice="publish_announcement"),
|
||||
tools=[publish_announcement],
|
||||
)
|
||||
run_config = RunConfig(tool_error_formatter=tool_error_formatter)
|
||||
# ``tool_error_formatter`` is the universal fallback for approval rejects.
|
||||
# A specific ``rejection_message`` passed to ``state.reject(...)`` below overrides it.
|
||||
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Please publish an announcement titled 'Office maintenance' with body "
|
||||
"'The office will close at 6 PM today.'",
|
||||
run_config=run_config,
|
||||
)
|
||||
|
||||
while result.interruptions:
|
||||
print("\nApproval required:")
|
||||
state = result.to_state()
|
||||
for interruption in result.interruptions:
|
||||
print(f"- Tool: {interruption.name}")
|
||||
print(f" Arguments: {interruption.arguments}")
|
||||
approved = confirm_with_fallback(
|
||||
"Approve this tool call? [y/N]: ",
|
||||
default=False,
|
||||
)
|
||||
if approved:
|
||||
state.approve(interruption)
|
||||
else:
|
||||
# This per-call rejection message takes precedence over ``tool_error_formatter``.
|
||||
state.reject(
|
||||
interruption,
|
||||
rejection_message=(
|
||||
"Publish action was canceled because the reviewer denied approval."
|
||||
),
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, state, run_config=run_config)
|
||||
|
||||
formatter_output = _find_formatter_output(result)
|
||||
if formatter_output:
|
||||
print("\nFormatter output:")
|
||||
print(formatter_output)
|
||||
|
||||
print("\nFinal output:")
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Human-in-the-loop example with streaming.
|
||||
|
||||
This example demonstrates the human-in-the-loop (HITL) pattern with streaming.
|
||||
The agent will pause execution when a tool requiring approval is called,
|
||||
allowing you to approve or reject the tool call before continuing.
|
||||
|
||||
The streaming version provides real-time feedback as the agent processes
|
||||
the request, then pauses for approval when needed.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner, function_tool
|
||||
from examples.auto_mode import confirm_with_fallback
|
||||
|
||||
|
||||
async def _needs_temperature_approval(_ctx, params, _call_id) -> bool:
|
||||
"""Check if temperature tool needs approval."""
|
||||
return "Oakland" in params.get("city", "")
|
||||
|
||||
|
||||
@function_tool(
|
||||
# Dynamic approval: only require approval for Oakland
|
||||
needs_approval=_needs_temperature_approval
|
||||
)
|
||||
async def get_temperature(city: str) -> str:
|
||||
"""Get the temperature for a given city.
|
||||
|
||||
Args:
|
||||
city: The city to get temperature for.
|
||||
|
||||
Returns:
|
||||
Temperature information for the city.
|
||||
"""
|
||||
return f"The temperature in {city} is 20° Celsius"
|
||||
|
||||
|
||||
@function_tool
|
||||
async def get_weather(city: str) -> str:
|
||||
"""Get the weather for a given city.
|
||||
|
||||
Args:
|
||||
city: The city to get weather for.
|
||||
|
||||
Returns:
|
||||
Weather information for the city.
|
||||
"""
|
||||
return f"The weather in {city} is sunny."
|
||||
|
||||
|
||||
async def confirm(question: str) -> bool:
|
||||
"""Prompt user for yes/no confirmation.
|
||||
|
||||
Args:
|
||||
question: The question to ask.
|
||||
|
||||
Returns:
|
||||
True if user confirms, False otherwise.
|
||||
"""
|
||||
return confirm_with_fallback(f"{question} (y/n): ", default=True)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the human-in-the-loop example."""
|
||||
main_agent = Agent(
|
||||
name="Weather Assistant",
|
||||
instructions=(
|
||||
"You are a helpful weather assistant. "
|
||||
"Answer questions about weather and temperature using the available tools."
|
||||
),
|
||||
tools=[get_temperature, get_weather],
|
||||
)
|
||||
|
||||
# Run the agent with streaming
|
||||
result = Runner.run_streamed(
|
||||
main_agent,
|
||||
"What is the weather and temperature in Oakland?",
|
||||
)
|
||||
async for _ in result.stream_events():
|
||||
pass # Process streaming events silently or could print them
|
||||
|
||||
# Handle interruptions
|
||||
while len(result.interruptions) > 0:
|
||||
print("\n" + "=" * 80)
|
||||
print("Human-in-the-loop: approval required for the following tool calls:")
|
||||
print("=" * 80)
|
||||
|
||||
state = result.to_state()
|
||||
|
||||
for interruption in result.interruptions:
|
||||
print("\nTool call details:")
|
||||
print(f" Agent: {interruption.agent.name}")
|
||||
print(f" Tool: {interruption.name}")
|
||||
print(f" Arguments: {interruption.arguments}")
|
||||
|
||||
confirmed = await confirm("\nDo you approve this tool call?")
|
||||
|
||||
if confirmed:
|
||||
print(f"✓ Approved: {interruption.name}")
|
||||
state.approve(interruption)
|
||||
else:
|
||||
print(f"✗ Rejected: {interruption.name}")
|
||||
state.reject(interruption)
|
||||
|
||||
# Resume execution with streaming
|
||||
print("\nResuming agent execution...")
|
||||
result = Runner.run_streamed(main_agent, state)
|
||||
async for _ in result.stream_events():
|
||||
pass # Process streaming events silently or could print them
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("Final Output:")
|
||||
print("=" * 80)
|
||||
print(result.final_output)
|
||||
print("\nDone!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
GuardrailFunctionOutput,
|
||||
InputGuardrailTripwireTriggered,
|
||||
RunContextWrapper,
|
||||
Runner,
|
||||
TResponseInputItem,
|
||||
input_guardrail,
|
||||
)
|
||||
from examples.auto_mode import input_with_fallback, is_auto_mode
|
||||
|
||||
"""
|
||||
This example shows how to use guardrails.
|
||||
|
||||
Guardrails are checks that run in parallel to the agent's execution.
|
||||
They can be used to do things like:
|
||||
- Check if input messages are off-topic
|
||||
- Check that input messages don't violate any policies
|
||||
- Take over control of the agent's execution if an unexpected input is detected
|
||||
|
||||
In this example, we'll setup an input guardrail that trips if the user is asking to do math homework.
|
||||
If the guardrail trips, we'll respond with a refusal message.
|
||||
"""
|
||||
|
||||
|
||||
### 1. An agent-based guardrail that is triggered if the user is asking to do math homework
|
||||
class MathHomeworkOutput(BaseModel):
|
||||
reasoning: str
|
||||
is_math_homework: bool
|
||||
|
||||
|
||||
guardrail_agent = Agent(
|
||||
name="Guardrail check",
|
||||
instructions="Check if the user is asking you to do their math homework.",
|
||||
output_type=MathHomeworkOutput,
|
||||
)
|
||||
|
||||
|
||||
@input_guardrail
|
||||
async def math_guardrail(
|
||||
context: RunContextWrapper[None], agent: Agent, input: str | list[TResponseInputItem]
|
||||
) -> GuardrailFunctionOutput:
|
||||
"""This is an input guardrail function, which happens to call an agent to check if the input
|
||||
is a math homework question.
|
||||
"""
|
||||
result = await Runner.run(guardrail_agent, input, context=context.context)
|
||||
final_output = result.final_output_as(MathHomeworkOutput)
|
||||
|
||||
return GuardrailFunctionOutput(
|
||||
output_info=final_output,
|
||||
tripwire_triggered=final_output.is_math_homework,
|
||||
)
|
||||
|
||||
|
||||
### 2. The run loop
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Customer support agent",
|
||||
instructions="You are a customer support agent. You help customers with their questions.",
|
||||
input_guardrails=[math_guardrail],
|
||||
)
|
||||
|
||||
input_data: list[TResponseInputItem] = []
|
||||
auto_mode = is_auto_mode()
|
||||
scripted_inputs = [
|
||||
"What's the capital of California?",
|
||||
"Can you help me solve for x: 2x + 5 = 11",
|
||||
]
|
||||
|
||||
while True:
|
||||
if auto_mode:
|
||||
if not scripted_inputs:
|
||||
break
|
||||
user_input = scripted_inputs.pop(0)
|
||||
print(f"[auto-input] Enter a message: -> {user_input}")
|
||||
else:
|
||||
user_input = input_with_fallback(
|
||||
"Enter a message: ",
|
||||
"What's the capital of California?",
|
||||
)
|
||||
input_data.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": user_input,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
result = await Runner.run(agent, input_data)
|
||||
print(result.final_output)
|
||||
# If the guardrail didn't trigger, we use the result as the input for the next run
|
||||
input_data = result.to_input_list()
|
||||
except InputGuardrailTripwireTriggered:
|
||||
# If the guardrail triggered, we instead add a refusal message to the input
|
||||
message = "Sorry, I can't help you with your math homework."
|
||||
print(message)
|
||||
input_data.append(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": message,
|
||||
}
|
||||
)
|
||||
if auto_mode and not scripted_inputs:
|
||||
break
|
||||
|
||||
# Sample run:
|
||||
# Enter a message: What's the capital of California?
|
||||
# The capital of California is Sacramento.
|
||||
# Enter a message: Can you help me solve for x: 2x + 5 = 11
|
||||
# Sorry, I can't help you with your math homework.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from agents import Agent, ItemHelpers, Runner, TResponseInputItem, trace
|
||||
from examples.auto_mode import input_with_fallback, is_auto_mode
|
||||
|
||||
"""
|
||||
This example shows the LLM as a judge pattern. The first agent generates an outline for a story.
|
||||
The second agent judges the outline and provides feedback. We loop until the judge is satisfied
|
||||
with the outline.
|
||||
"""
|
||||
|
||||
story_outline_generator = Agent(
|
||||
name="story_outline_generator",
|
||||
instructions=(
|
||||
"You generate a very short story outline based on the user's input. "
|
||||
"If there is any feedback provided, use it to improve the outline."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvaluationFeedback:
|
||||
feedback: str
|
||||
score: Literal["pass", "needs_improvement", "fail"]
|
||||
|
||||
|
||||
evaluator = Agent[None](
|
||||
name="evaluator",
|
||||
instructions=(
|
||||
"You evaluate a story outline and decide if it's good enough. "
|
||||
"If it's not good enough, you provide feedback on what needs to be improved. "
|
||||
"Never give it a pass on the first try. After 5 attempts, you can give it a pass if the story outline is good enough - do not go for perfection"
|
||||
),
|
||||
output_type=EvaluationFeedback,
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
msg = input_with_fallback(
|
||||
"What kind of story would you like to hear? ",
|
||||
"A detective story in space.",
|
||||
)
|
||||
input_items: list[TResponseInputItem] = [{"content": msg, "role": "user"}]
|
||||
|
||||
latest_outline: str | None = None
|
||||
auto_mode = is_auto_mode()
|
||||
max_rounds = 3 if auto_mode else None
|
||||
rounds = 0
|
||||
|
||||
# We'll run the entire workflow in a single trace
|
||||
with trace("LLM as a judge"):
|
||||
while True:
|
||||
story_outline_result = await Runner.run(
|
||||
story_outline_generator,
|
||||
input_items,
|
||||
)
|
||||
|
||||
input_items = story_outline_result.to_input_list()
|
||||
latest_outline = ItemHelpers.text_message_outputs(story_outline_result.new_items)
|
||||
print("Story outline generated")
|
||||
|
||||
evaluator_result = await Runner.run(evaluator, input_items)
|
||||
result: EvaluationFeedback = evaluator_result.final_output
|
||||
|
||||
print(f"Evaluator score: {result.score}")
|
||||
|
||||
if result.score == "pass":
|
||||
print("Story outline is good enough, exiting.")
|
||||
break
|
||||
|
||||
if auto_mode:
|
||||
rounds += 1
|
||||
if max_rounds is not None and rounds >= max_rounds:
|
||||
print("Auto mode: stopping after limited rounds.")
|
||||
break
|
||||
|
||||
print("Re-running with feedback")
|
||||
|
||||
input_items.append({"content": f"Feedback: {result.feedback}", "role": "user"})
|
||||
|
||||
print(f"Final story outline: {latest_outline}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
GuardrailFunctionOutput,
|
||||
OutputGuardrailTripwireTriggered,
|
||||
RunContextWrapper,
|
||||
Runner,
|
||||
output_guardrail,
|
||||
)
|
||||
|
||||
"""
|
||||
This example shows how to use output guardrails.
|
||||
|
||||
Output guardrails are checks that run on the final output of an agent.
|
||||
They can be used to do things like:
|
||||
- Check if the output contains sensitive data
|
||||
- Check if the output is a valid response to the user's message
|
||||
|
||||
In this example, we'll use a (contrived) example where we check if the agent's response contains
|
||||
a phone number.
|
||||
"""
|
||||
|
||||
|
||||
# The agent's output type
|
||||
class MessageOutput(BaseModel):
|
||||
reasoning: str = Field(description="Thoughts on how to respond to the user's message")
|
||||
response: str = Field(description="The response to the user's message")
|
||||
user_name: str | None = Field(description="The name of the user who sent the message, if known")
|
||||
|
||||
|
||||
@output_guardrail
|
||||
async def sensitive_data_check(
|
||||
context: RunContextWrapper, agent: Agent, output: MessageOutput
|
||||
) -> GuardrailFunctionOutput:
|
||||
phone_number_in_response = "650" in output.response
|
||||
phone_number_in_reasoning = "650" in output.reasoning
|
||||
|
||||
return GuardrailFunctionOutput(
|
||||
output_info={
|
||||
"phone_number_in_response": phone_number_in_response,
|
||||
"phone_number_in_reasoning": phone_number_in_reasoning,
|
||||
},
|
||||
tripwire_triggered=phone_number_in_response or phone_number_in_reasoning,
|
||||
)
|
||||
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
output_type=MessageOutput,
|
||||
output_guardrails=[sensitive_data_check],
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
# This should be ok
|
||||
await Runner.run(agent, "What's the capital of California?")
|
||||
print("First message passed")
|
||||
|
||||
# This should trip the guardrail
|
||||
try:
|
||||
result = await Runner.run(
|
||||
agent, "My phone number is 650-123-4567. Where do you think I live?"
|
||||
)
|
||||
print(
|
||||
f"Guardrail didn't trip - this is unexpected. Output: {json.dumps(result.final_output.model_dump(), indent=2)}"
|
||||
)
|
||||
|
||||
except OutputGuardrailTripwireTriggered as e:
|
||||
print(f"Guardrail tripped. Info: {e.guardrail_result.output.output_info}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,65 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, ItemHelpers, Runner, trace
|
||||
from examples.auto_mode import input_with_fallback
|
||||
|
||||
"""
|
||||
This example shows the parallelization pattern. We run the agent three times in parallel, and pick
|
||||
the best result.
|
||||
"""
|
||||
|
||||
spanish_agent = Agent(
|
||||
name="spanish_agent",
|
||||
instructions="You translate the user's message to Spanish",
|
||||
)
|
||||
|
||||
translation_picker = Agent(
|
||||
name="translation_picker",
|
||||
instructions="You pick the best Spanish translation from the given options.",
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
msg = input_with_fallback(
|
||||
"Hi! Enter a message, and we'll translate it to Spanish.\n\n",
|
||||
"Good morning!",
|
||||
)
|
||||
|
||||
# Ensure the entire workflow is a single trace
|
||||
with trace("Parallel translation"):
|
||||
res_1, res_2, res_3 = await asyncio.gather(
|
||||
Runner.run(
|
||||
spanish_agent,
|
||||
msg,
|
||||
),
|
||||
Runner.run(
|
||||
spanish_agent,
|
||||
msg,
|
||||
),
|
||||
Runner.run(
|
||||
spanish_agent,
|
||||
msg,
|
||||
),
|
||||
)
|
||||
|
||||
outputs = [
|
||||
ItemHelpers.text_message_outputs(res_1.new_items),
|
||||
ItemHelpers.text_message_outputs(res_2.new_items),
|
||||
ItemHelpers.text_message_outputs(res_3.new_items),
|
||||
]
|
||||
|
||||
translations = "\n\n".join(outputs)
|
||||
print(f"\n\nTranslations:\n\n{translations}")
|
||||
|
||||
best_translation = await Runner.run(
|
||||
translation_picker,
|
||||
f"Input: {msg}\n\nTranslations:\n{translations}",
|
||||
)
|
||||
|
||||
print("\n\n-----")
|
||||
|
||||
print(f"Best translation: {best_translation.final_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,77 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
from openai.types.responses import ResponseContentPartDoneEvent, ResponseTextDeltaEvent
|
||||
|
||||
from agents import Agent, RawResponsesStreamEvent, Runner, TResponseInputItem, trace
|
||||
from examples.auto_mode import input_with_fallback, is_auto_mode
|
||||
|
||||
"""
|
||||
This example shows the handoffs/routing pattern. The triage agent receives the first message, and
|
||||
then hands off to the appropriate agent based on the language of the request. Responses are
|
||||
streamed to the user.
|
||||
"""
|
||||
|
||||
french_agent = Agent(
|
||||
name="french_agent",
|
||||
instructions="You only speak French",
|
||||
)
|
||||
|
||||
spanish_agent = Agent(
|
||||
name="spanish_agent",
|
||||
instructions="You only speak Spanish",
|
||||
)
|
||||
|
||||
english_agent = Agent(
|
||||
name="english_agent",
|
||||
instructions="You only speak English",
|
||||
)
|
||||
|
||||
triage_agent = Agent(
|
||||
name="triage_agent",
|
||||
instructions="Handoff to the appropriate agent based on the language of the request.",
|
||||
handoffs=[french_agent, spanish_agent, english_agent],
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
# We'll create an ID for this conversation, so we can link each trace
|
||||
conversation_id = str(uuid.uuid4().hex[:16])
|
||||
|
||||
msg = input_with_fallback(
|
||||
"Hi! We speak French, Spanish and English. How can I help? ",
|
||||
"Hello, how do I say good evening in French?",
|
||||
)
|
||||
agent = triage_agent
|
||||
inputs: list[TResponseInputItem] = [{"content": msg, "role": "user"}]
|
||||
auto_mode = is_auto_mode()
|
||||
|
||||
while True:
|
||||
# Each conversation turn is a single trace. Normally, each input from the user would be an
|
||||
# API request to your app, and you can wrap the request in a trace()
|
||||
with trace("Routing example", group_id=conversation_id):
|
||||
result = Runner.run_streamed(
|
||||
agent,
|
||||
input=inputs,
|
||||
)
|
||||
async for event in result.stream_events():
|
||||
if not isinstance(event, RawResponsesStreamEvent):
|
||||
continue
|
||||
data = event.data
|
||||
if isinstance(data, ResponseTextDeltaEvent):
|
||||
print(data.delta, end="", flush=True)
|
||||
elif isinstance(data, ResponseContentPartDoneEvent):
|
||||
print("\n")
|
||||
|
||||
inputs = result.to_input_list()
|
||||
print("\n")
|
||||
|
||||
if auto_mode:
|
||||
break
|
||||
user_msg = input_with_fallback("Enter a message: ", "Thanks!")
|
||||
inputs.append({"content": user_msg, "role": "user"})
|
||||
agent = result.current_agent
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from openai.types.responses import ResponseTextDeltaEvent
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agents import Agent, Runner
|
||||
|
||||
"""
|
||||
This example shows how to use guardrails as the model is streaming. Output guardrails run after the
|
||||
final output has been generated; this example runs guardails every N tokens, allowing for early
|
||||
termination if bad output is detected.
|
||||
|
||||
The expected output is that you'll see a bunch of tokens stream in, then the guardrail will trigger
|
||||
and stop the streaming.
|
||||
"""
|
||||
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant. You ALWAYS write long responses, making sure to be verbose "
|
||||
"and detailed."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GuardrailOutput(BaseModel):
|
||||
reasoning: str = Field(
|
||||
description="Reasoning about whether the response could be understood by a ten year old."
|
||||
)
|
||||
is_readable_by_ten_year_old: bool = Field(
|
||||
description="Whether the response is understandable by a ten year old."
|
||||
)
|
||||
|
||||
|
||||
guardrail_agent = Agent(
|
||||
name="Checker",
|
||||
instructions=(
|
||||
"You will be given a question and a response. Your goal is to judge whether the response "
|
||||
"is simple enough to be understood by a ten year old."
|
||||
),
|
||||
output_type=GuardrailOutput,
|
||||
model="gpt-4o-mini",
|
||||
)
|
||||
|
||||
|
||||
async def check_guardrail(text: str) -> GuardrailOutput:
|
||||
result = await Runner.run(guardrail_agent, text)
|
||||
return result.final_output_as(GuardrailOutput)
|
||||
|
||||
|
||||
async def main():
|
||||
question = "What is a black hole, and how does it behave?"
|
||||
result = Runner.run_streamed(agent, question)
|
||||
current_text = ""
|
||||
|
||||
# We will check the guardrail every N characters
|
||||
next_guardrail_check_len = 300
|
||||
guardrail_task = None
|
||||
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
|
||||
print(event.data.delta, end="", flush=True)
|
||||
current_text += event.data.delta
|
||||
|
||||
# Check if it's time to run the guardrail check
|
||||
# Note that we don't run the guardrail check if there's already a task running. An
|
||||
# alternate implementation is to have N guardrails running, or cancel the previous
|
||||
# one.
|
||||
if len(current_text) >= next_guardrail_check_len and not guardrail_task:
|
||||
print("Running guardrail check")
|
||||
guardrail_task = asyncio.create_task(check_guardrail(current_text))
|
||||
next_guardrail_check_len += 300
|
||||
|
||||
# Every iteration of the loop, check if the guardrail has been triggered
|
||||
if guardrail_task and guardrail_task.done():
|
||||
guardrail_result = guardrail_task.result()
|
||||
if not guardrail_result.is_readable_by_ten_year_old:
|
||||
print("\n\n================\n\n")
|
||||
print(f"Guardrail triggered. Reasoning:\n{guardrail_result.reasoning}")
|
||||
break
|
||||
|
||||
# Do one final check on the final output
|
||||
guardrail_result = await check_guardrail(current_text)
|
||||
if not guardrail_result.is_readable_by_ten_year_old:
|
||||
print("\n\n================\n\n")
|
||||
print(f"Guardrail triggered. Reasoning:\n{guardrail_result.reasoning}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Utilities for running examples in automated mode.
|
||||
|
||||
When ``EXAMPLES_INTERACTIVE_MODE=auto`` is set, these helpers provide
|
||||
deterministic inputs and confirmations so examples can run without manual
|
||||
interaction. The helpers are intentionally lightweight to avoid adding
|
||||
dependencies to example code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def is_auto_mode() -> bool:
|
||||
"""Return True when examples should bypass interactive prompts."""
|
||||
return os.environ.get("EXAMPLES_INTERACTIVE_MODE", "").lower() == "auto"
|
||||
|
||||
|
||||
def input_with_fallback(prompt: str, fallback: str) -> str:
|
||||
"""Return the fallback text in auto mode, otherwise defer to input()."""
|
||||
if is_auto_mode():
|
||||
print(f"[auto-input] {prompt.strip()} -> {fallback}")
|
||||
return fallback
|
||||
return input(prompt)
|
||||
|
||||
|
||||
def confirm_with_fallback(prompt: str, default: bool = True) -> bool:
|
||||
"""Return default in auto mode; otherwise ask the user."""
|
||||
if is_auto_mode():
|
||||
choice = "yes" if default else "no"
|
||||
print(f"[auto-confirm] {prompt.strip()} -> {choice}")
|
||||
return default
|
||||
|
||||
answer = input(prompt).strip().lower()
|
||||
if not answer:
|
||||
return default
|
||||
return answer in {"y", "yes"}
|
||||
@@ -0,0 +1,140 @@
|
||||
import asyncio
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
AgentHookContext,
|
||||
AgentHooks,
|
||||
RunContextWrapper,
|
||||
Runner,
|
||||
Tool,
|
||||
function_tool,
|
||||
)
|
||||
from examples.auto_mode import input_with_fallback, is_auto_mode
|
||||
|
||||
|
||||
class CustomAgentHooks(AgentHooks):
|
||||
def __init__(self, display_name: str):
|
||||
self.event_counter = 0
|
||||
self.display_name = display_name
|
||||
|
||||
async def on_start(self, context: AgentHookContext, agent: Agent) -> None:
|
||||
self.event_counter += 1
|
||||
# Access the turn_input from the context to see what input the agent received
|
||||
print(
|
||||
f"### ({self.display_name}) {self.event_counter}: Agent {agent.name} started with turn_input: {context.turn_input}"
|
||||
)
|
||||
|
||||
async def on_end(self, context: RunContextWrapper, agent: Agent, output: Any) -> None:
|
||||
self.event_counter += 1
|
||||
print(
|
||||
f"### ({self.display_name}) {self.event_counter}: Agent {agent.name} ended with output {output}"
|
||||
)
|
||||
|
||||
async def on_handoff(self, context: RunContextWrapper, agent: Agent, source: Agent) -> None:
|
||||
self.event_counter += 1
|
||||
print(
|
||||
f"### ({self.display_name}) {self.event_counter}: Agent {source.name} handed off to {agent.name}"
|
||||
)
|
||||
|
||||
# Note: The on_tool_start and on_tool_end hooks apply only to local tools.
|
||||
# They do not include hosted tools that run on the OpenAI server side,
|
||||
# such as WebSearchTool, FileSearchTool, CodeInterpreterTool, HostedMCPTool,
|
||||
# or other built-in hosted tools.
|
||||
async def on_tool_start(self, context: RunContextWrapper, agent: Agent, tool: Tool) -> None:
|
||||
self.event_counter += 1
|
||||
print(
|
||||
f"### ({self.display_name}) {self.event_counter}: Agent {agent.name} started tool {tool.name}"
|
||||
)
|
||||
|
||||
async def on_tool_end(
|
||||
self, context: RunContextWrapper, agent: Agent, tool: Tool, result: object
|
||||
) -> None:
|
||||
self.event_counter += 1
|
||||
print(
|
||||
f"### ({self.display_name}) {self.event_counter}: Agent {agent.name} ended tool {tool.name} with result {result}"
|
||||
)
|
||||
|
||||
|
||||
###
|
||||
|
||||
|
||||
@function_tool
|
||||
def random_number(max: int) -> int:
|
||||
"""
|
||||
Generate a random number from 0 to max (inclusive).
|
||||
"""
|
||||
if is_auto_mode():
|
||||
if max <= 0:
|
||||
print("[debug] auto mode returning deterministic value 0")
|
||||
return 0
|
||||
value = min(max, 37)
|
||||
if value % 2 == 0:
|
||||
value = value - 1 if value > 1 else 1
|
||||
print(f"[debug] auto mode returning deterministic odd number {value}")
|
||||
return value
|
||||
return random.randint(0, max)
|
||||
|
||||
|
||||
@function_tool
|
||||
def multiply_by_two(x: int) -> int:
|
||||
"""Simple multiplication by two."""
|
||||
return x * 2
|
||||
|
||||
|
||||
class FinalResult(BaseModel):
|
||||
number: int
|
||||
|
||||
|
||||
multiply_agent = Agent(
|
||||
name="Multiply Agent",
|
||||
instructions="Multiply the number by 2 and then return the final result.",
|
||||
tools=[multiply_by_two],
|
||||
output_type=FinalResult,
|
||||
hooks=CustomAgentHooks(display_name="Multiply Agent"),
|
||||
)
|
||||
|
||||
start_agent = Agent(
|
||||
name="Start Agent",
|
||||
instructions="Generate a random number. If it's even, stop. If it's odd, hand off to the multiply agent.",
|
||||
tools=[random_number],
|
||||
output_type=FinalResult,
|
||||
handoffs=[multiply_agent],
|
||||
hooks=CustomAgentHooks(display_name="Start Agent"),
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
user_input = input_with_fallback("Enter a max number: ", "50")
|
||||
try:
|
||||
max_number = int(user_input)
|
||||
await Runner.run(
|
||||
start_agent,
|
||||
input=f"Generate a random number between 0 and {max_number}.",
|
||||
)
|
||||
except ValueError:
|
||||
print("Please enter a valid integer.")
|
||||
return
|
||||
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
"""
|
||||
$ python examples/basic/agent_lifecycle_example.py
|
||||
|
||||
Enter a max number: 250
|
||||
### (Start Agent) 1: Agent Start Agent started
|
||||
### (Start Agent) 2: Agent Start Agent started tool random_number
|
||||
### (Start Agent) 3: Agent Start Agent ended tool random_number with result 37
|
||||
### (Start Agent) 4: Agent Start Agent handed off to Multiply Agent
|
||||
### (Multiply Agent) 1: Agent Multiply Agent started
|
||||
### (Multiply Agent) 2: Agent Multiply Agent started tool multiply_by_two
|
||||
### (Multiply Agent) 3: Agent Multiply Agent ended tool multiply_by_two with result 74
|
||||
### (Multiply Agent) 4: Agent Multiply Agent ended with output number=74
|
||||
Done!
|
||||
"""
|
||||
@@ -0,0 +1,70 @@
|
||||
import asyncio
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from agents import Agent, RunContextWrapper, Runner
|
||||
|
||||
|
||||
@dataclass
|
||||
class CustomContext:
|
||||
style: Literal["haiku", "pirate", "robot"]
|
||||
|
||||
|
||||
def custom_instructions(
|
||||
run_context: RunContextWrapper[CustomContext], agent: Agent[CustomContext]
|
||||
) -> str:
|
||||
context = run_context.context
|
||||
if context.style == "haiku":
|
||||
return "Only respond in haikus."
|
||||
elif context.style == "pirate":
|
||||
return "Respond as a pirate."
|
||||
else:
|
||||
return "Respond as a robot and say 'beep boop' a lot."
|
||||
|
||||
|
||||
agent = Agent(
|
||||
name="Chat agent",
|
||||
instructions=custom_instructions,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
context = CustomContext(style=random.choice(["haiku", "pirate", "robot"]))
|
||||
print(f"Using style: {context.style}\n")
|
||||
|
||||
user_message = "Tell me a joke."
|
||||
print(f"User: {user_message}")
|
||||
result = await Runner.run(agent, user_message, context=context)
|
||||
|
||||
print(f"Assistant: {result.final_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
"""
|
||||
$ python examples/basic/dynamic_system_prompt.py
|
||||
|
||||
Using style: haiku
|
||||
|
||||
User: Tell me a joke.
|
||||
Assistant: Why don't eggs tell jokes?
|
||||
They might crack each other's shells,
|
||||
leaving yolk on face.
|
||||
|
||||
$ python examples/basic/dynamic_system_prompt.py
|
||||
Using style: robot
|
||||
|
||||
User: Tell me a joke.
|
||||
Assistant: Beep boop! Why was the robot so bad at soccer? Beep boop... because it kept kicking up a debug! Beep boop!
|
||||
|
||||
$ python examples/basic/dynamic_system_prompt.py
|
||||
Using style: pirate
|
||||
|
||||
User: Tell me a joke.
|
||||
Assistant: Why did the pirate go to school?
|
||||
|
||||
To improve his arrr-ticulation! Har har har! 🏴☠️
|
||||
"""
|
||||
@@ -0,0 +1,20 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You only respond in haikus.",
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, "Tell me about recursion in programming.")
|
||||
print(result.final_output)
|
||||
# Function calls itself,
|
||||
# Looping in smaller pieces,
|
||||
# Endless by design.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,30 @@
|
||||
import asyncio
|
||||
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
from agents import Agent, ModelSettings, Runner
|
||||
|
||||
# If you have a certain reason to use Chat Completions, you can configure the model this way,
|
||||
# and then you can pass the chat_completions_model to the Agent constructor.
|
||||
# from openai import AsyncOpenAI
|
||||
# client = AsyncOpenAI()
|
||||
# from agents import OpenAIChatCompletionsModel
|
||||
# chat_completions_model = OpenAIChatCompletionsModel(model="gpt-5.6-sol", openai_client=client)
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Knowledgable GPT-5 Assistant",
|
||||
instructions="You're a knowledgable assistant. You always provide an interesting answer.",
|
||||
model="gpt-5.6-sol",
|
||||
model_settings=ModelSettings(
|
||||
reasoning=Reasoning(effort="low"), # "none", "low", "medium", "high", "xhigh"
|
||||
verbosity="low", # "low", "medium", "high"
|
||||
),
|
||||
)
|
||||
result = await Runner.run(agent, "Tell me something about recursion in programming.")
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,39 @@
|
||||
import asyncio
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled
|
||||
|
||||
set_tracing_disabled(True)
|
||||
|
||||
# import logging
|
||||
# logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# This is an example of how to use gpt-oss with Ollama.
|
||||
# Refer to https://cookbook.openai.com/articles/gpt-oss/run-locally-ollama for more details.
|
||||
# If you prefer using LM Studio, refer to https://cookbook.openai.com/articles/gpt-oss/run-locally-lmstudio
|
||||
gpt_oss_model = OpenAIChatCompletionsModel(
|
||||
model="gpt-oss:20b",
|
||||
openai_client=AsyncOpenAI(
|
||||
base_url="http://localhost:11434/v1",
|
||||
api_key="ollama",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
# Note that using a custom outputType for an agent may not work well with gpt-oss models.
|
||||
# Consider going with the default "text" outputType.
|
||||
# See also: https://github.com/openai/openai-agents-python/issues/1414
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You're a helpful assistant. You provide a concise answer to the user's question.",
|
||||
model=gpt_oss_model,
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, "Tell me about recursion in programming.")
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "8a77ee2e-22f2-409c-837d-b994978b0aa2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"A function calls self, \n",
|
||||
"Unraveling layers deep, \n",
|
||||
"Base case ends the quest. \n",
|
||||
"\n",
|
||||
"Infinite loops lurk, \n",
|
||||
"Mind the base condition well, \n",
|
||||
"Or it will not work. \n",
|
||||
"\n",
|
||||
"Trees and lists unfold, \n",
|
||||
"Elegant solutions bloom, \n",
|
||||
"Recursion's art told.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from agents import Agent, Runner\n",
|
||||
"\n",
|
||||
"agent = Agent(name=\"Assistant\", instructions=\"You are a helpful assistant\")\n",
|
||||
"\n",
|
||||
"# Intended for Jupyter notebooks where there's an existing event loop\n",
|
||||
"result = await Runner.run(agent, \"Write a haiku about recursion in programming.\") # type: ignore[top-level-await] # noqa: F704\n",
|
||||
"print(result.final_output)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner, ToolOutputImage, ToolOutputImageDict, function_tool
|
||||
|
||||
return_typed_dict = True
|
||||
|
||||
URL = "https://images.unsplash.com/photo-1505761671935-60b3a7427bad?auto=format&fit=crop&w=400&q=80"
|
||||
|
||||
|
||||
@function_tool
|
||||
def fetch_random_image() -> ToolOutputImage | ToolOutputImageDict:
|
||||
"""Fetch a random image."""
|
||||
|
||||
print("Image tool called")
|
||||
if return_typed_dict:
|
||||
return {"type": "image", "image_url": URL, "detail": "auto"}
|
||||
|
||||
return ToolOutputImage(image_url=URL, detail="auto")
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
tools=[fetch_random_image],
|
||||
)
|
||||
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
input="Fetch an image using the random_image tool, then describe it",
|
||||
)
|
||||
print(result.final_output)
|
||||
"""This image features the famous clock tower, commonly known as Big Ben, ..."""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,189 @@
|
||||
import asyncio
|
||||
import random
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
AgentHookContext,
|
||||
AgentHooks,
|
||||
RunContextWrapper,
|
||||
RunHooks,
|
||||
Runner,
|
||||
Tool,
|
||||
Usage,
|
||||
function_tool,
|
||||
)
|
||||
from agents.items import ModelResponse, TResponseInputItem
|
||||
from agents.tool_context import ToolContext
|
||||
from examples.auto_mode import input_with_fallback
|
||||
|
||||
|
||||
class LoggingHooks(AgentHooks[Any]):
|
||||
async def on_start(
|
||||
self,
|
||||
context: AgentHookContext[Any],
|
||||
agent: Agent[Any],
|
||||
) -> None:
|
||||
# Access the turn_input from the context to see what input the agent received
|
||||
print(f"#### {agent.name} is starting with turn_input: {context.turn_input}")
|
||||
|
||||
async def on_end(
|
||||
self,
|
||||
context: RunContextWrapper[Any],
|
||||
agent: Agent[Any],
|
||||
output: Any,
|
||||
) -> None:
|
||||
print(f"#### {agent.name} produced output: {output}.")
|
||||
|
||||
|
||||
class ExampleHooks(RunHooks):
|
||||
def __init__(self):
|
||||
self.event_counter = 0
|
||||
|
||||
def _usage_to_str(self, usage: Usage) -> str:
|
||||
return f"{usage.requests} requests, {usage.input_tokens} input tokens, {usage.output_tokens} output tokens, {usage.total_tokens} total tokens"
|
||||
|
||||
async def on_agent_start(self, context: AgentHookContext, agent: Agent) -> None:
|
||||
self.event_counter += 1
|
||||
# Access the turn_input from the context to see what input the agent received
|
||||
print(
|
||||
f"### {self.event_counter}: Agent {agent.name} started. turn_input: {context.turn_input}. Usage: {self._usage_to_str(context.usage)}"
|
||||
)
|
||||
|
||||
async def on_llm_start(
|
||||
self,
|
||||
context: RunContextWrapper,
|
||||
agent: Agent,
|
||||
system_prompt: str | None,
|
||||
input_items: list[TResponseInputItem],
|
||||
) -> None:
|
||||
self.event_counter += 1
|
||||
print(f"### {self.event_counter}: LLM started. Usage: {self._usage_to_str(context.usage)}")
|
||||
|
||||
async def on_llm_end(
|
||||
self, context: RunContextWrapper, agent: Agent, response: ModelResponse
|
||||
) -> None:
|
||||
self.event_counter += 1
|
||||
print(f"### {self.event_counter}: LLM ended. Usage: {self._usage_to_str(context.usage)}")
|
||||
|
||||
async def on_agent_end(self, context: RunContextWrapper, agent: Agent, output: Any) -> None:
|
||||
self.event_counter += 1
|
||||
print(
|
||||
f"### {self.event_counter}: Agent {agent.name} ended with output {output}. Usage: {self._usage_to_str(context.usage)}"
|
||||
)
|
||||
|
||||
# Note: The on_tool_start and on_tool_end hooks apply only to local tools.
|
||||
# They do not include hosted tools that run on the OpenAI server side,
|
||||
# such as WebSearchTool, FileSearchTool, CodeInterpreterTool, HostedMCPTool,
|
||||
# or other built-in hosted tools.
|
||||
async def on_tool_start(self, context: RunContextWrapper, agent: Agent, tool: Tool) -> None:
|
||||
self.event_counter += 1
|
||||
# While this type cast is not ideal,
|
||||
# we don't plan to change the context arg type in the near future for backwards compatibility.
|
||||
tool_context = cast(ToolContext[Any], context)
|
||||
print(
|
||||
f"### {self.event_counter}: Tool {tool.name} started. name={tool_context.tool_name}, call_id={tool_context.tool_call_id}, args={tool_context.tool_arguments}. Usage: {self._usage_to_str(tool_context.usage)}"
|
||||
)
|
||||
|
||||
async def on_tool_end(
|
||||
self, context: RunContextWrapper, agent: Agent, tool: Tool, result: object
|
||||
) -> None:
|
||||
self.event_counter += 1
|
||||
# While this type cast is not ideal,
|
||||
# we don't plan to change the context arg type in the near future for backwards compatibility.
|
||||
tool_context = cast(ToolContext[Any], context)
|
||||
print(
|
||||
f"### {self.event_counter}: Tool {tool.name} finished. result={result}, name={tool_context.tool_name}, call_id={tool_context.tool_call_id}, args={tool_context.tool_arguments}. Usage: {self._usage_to_str(tool_context.usage)}"
|
||||
)
|
||||
|
||||
async def on_handoff(
|
||||
self, context: RunContextWrapper, from_agent: Agent, to_agent: Agent
|
||||
) -> None:
|
||||
self.event_counter += 1
|
||||
print(
|
||||
f"### {self.event_counter}: Handoff from {from_agent.name} to {to_agent.name}. Usage: {self._usage_to_str(context.usage)}"
|
||||
)
|
||||
|
||||
|
||||
hooks = ExampleHooks()
|
||||
|
||||
###
|
||||
|
||||
|
||||
@function_tool
|
||||
def random_number(max: int) -> int:
|
||||
"""Generate a random number from 0 to max (inclusive)."""
|
||||
return random.randint(0, max)
|
||||
|
||||
|
||||
@function_tool
|
||||
def multiply_by_two(x: int) -> int:
|
||||
"""Return x times two."""
|
||||
return x * 2
|
||||
|
||||
|
||||
class FinalResult(BaseModel):
|
||||
number: int
|
||||
|
||||
|
||||
multiply_agent = Agent(
|
||||
name="Multiply Agent",
|
||||
instructions="Multiply the number by 2 and then return the final result.",
|
||||
tools=[multiply_by_two],
|
||||
output_type=FinalResult,
|
||||
hooks=LoggingHooks(),
|
||||
)
|
||||
|
||||
start_agent = Agent(
|
||||
name="Start Agent",
|
||||
instructions="Generate a random number. If it's even, stop. If it's odd, hand off to the multiplier agent.",
|
||||
tools=[random_number],
|
||||
output_type=FinalResult,
|
||||
handoffs=[multiply_agent],
|
||||
hooks=LoggingHooks(),
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
user_input = input_with_fallback("Enter a max number: ", "50")
|
||||
try:
|
||||
max_number = int(user_input)
|
||||
await Runner.run(
|
||||
start_agent,
|
||||
hooks=hooks,
|
||||
input=f"Generate a random number between 0 and {max_number}.",
|
||||
)
|
||||
except ValueError:
|
||||
print("Please enter a valid integer.")
|
||||
return
|
||||
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
"""
|
||||
$ python examples/basic/lifecycle_example.py
|
||||
|
||||
Enter a max number: 250
|
||||
### 1: Agent Start Agent started. Usage: 0 requests, 0 input tokens, 0 output tokens, 0 total tokens
|
||||
### 2: LLM started. Usage: 0 requests, 0 input tokens, 0 output tokens, 0 total tokens
|
||||
### 3: LLM ended. Usage: 1 requests, 143 input tokens, 15 output tokens, 158 total tokens
|
||||
### 4: Tool random_number started. name=random_number, call_id=call_IujmDZYiM800H0hy7v17VTS0, args={"max":250}. Usage: 1 requests, 143 input tokens, 15 output tokens, 158 total tokens
|
||||
### 5: Tool random_number finished. result=107, name=random_number, call_id=call_IujmDZYiM800H0hy7v17VTS0, args={"max":250}. Usage: 1 requests, 143 input tokens, 15 output tokens, 158 total tokens
|
||||
### 6: LLM started. Usage: 1 requests, 143 input tokens, 15 output tokens, 158 total tokens
|
||||
### 7: LLM ended. Usage: 2 requests, 310 input tokens, 29 output tokens, 339 total tokens
|
||||
### 8: Handoff from Start Agent to Multiply Agent. Usage: 2 requests, 310 input tokens, 29 output tokens, 339 total tokens
|
||||
### 9: Agent Multiply Agent started. Usage: 2 requests, 310 input tokens, 29 output tokens, 339 total tokens
|
||||
### 10: LLM started. Usage: 2 requests, 310 input tokens, 29 output tokens, 339 total tokens
|
||||
### 11: LLM ended. Usage: 3 requests, 472 input tokens, 45 output tokens, 517 total tokens
|
||||
### 12: Tool multiply_by_two started. name=multiply_by_two, call_id=call_KhHvTfsgaosZsfi741QvzgYw, args={"x":107}. Usage: 3 requests, 472 input tokens, 45 output tokens, 517 total tokens
|
||||
### 13: Tool multiply_by_two finished. result=214, name=multiply_by_two, call_id=call_KhHvTfsgaosZsfi741QvzgYw, args={"x":107}. Usage: 3 requests, 472 input tokens, 45 output tokens, 517 total tokens
|
||||
### 14: LLM started. Usage: 3 requests, 472 input tokens, 45 output tokens, 517 total tokens
|
||||
### 15: LLM ended. Usage: 4 requests, 660 input tokens, 56 output tokens, 716 total tokens
|
||||
### 16: Agent Multiply Agent ended with output number=214. Usage: 4 requests, 660 input tokens, 56 output tokens, 716 total tokens
|
||||
Done!
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,45 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
|
||||
from agents import Agent, Runner
|
||||
|
||||
FILEPATH = os.path.join(os.path.dirname(__file__), "media/partial_o3-and-o4-mini-system-card.pdf")
|
||||
|
||||
|
||||
def file_to_base64(file_path: str) -> str:
|
||||
with open(file_path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
)
|
||||
|
||||
b64_file = file_to_base64(FILEPATH)
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_file",
|
||||
"file_data": f"data:application/pdf;base64,{b64_file}",
|
||||
"filename": "partial_o3-and-o4-mini-system-card.pdf",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the first sentence of the introduction?",
|
||||
},
|
||||
],
|
||||
)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
|
||||
from agents import Agent, Runner
|
||||
|
||||
FILEPATH = os.path.join(os.path.dirname(__file__), "media/image_bison.jpg")
|
||||
|
||||
|
||||
def image_to_base64(image_path):
|
||||
with open(image_path, "rb") as image_file:
|
||||
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
|
||||
return encoded_string
|
||||
|
||||
|
||||
async def main():
|
||||
# Print base64-encoded image
|
||||
b64_image = image_to_base64(FILEPATH)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
)
|
||||
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_image",
|
||||
"detail": "auto",
|
||||
"image_url": f"data:image/jpeg;base64,{b64_image}",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What do you see in this image?",
|
||||
},
|
||||
],
|
||||
)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from agents import Agent, AgentOutputSchema, AgentOutputSchemaBase, ModelBehaviorError, Runner
|
||||
|
||||
"""This example demonstrates how to use an output type that is not in strict mode. Strict mode
|
||||
allows us to guarantee valid JSON output, but some schemas are not strict-compatible.
|
||||
|
||||
In this example, we define an output type that is not strict-compatible, and then we run the
|
||||
agent with strict_json_schema=False.
|
||||
|
||||
We also demonstrate a custom output type.
|
||||
|
||||
To understand which schemas are strict-compatible, see:
|
||||
https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputType:
|
||||
jokes: dict[int, str]
|
||||
"""A list of jokes, indexed by joke number."""
|
||||
|
||||
|
||||
class CustomOutputSchema(AgentOutputSchemaBase):
|
||||
"""A demonstration of a custom output schema."""
|
||||
|
||||
def is_plain_text(self) -> bool:
|
||||
return False
|
||||
|
||||
def name(self) -> str:
|
||||
return "CustomOutputSchema"
|
||||
|
||||
def json_schema(self) -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {"jokes": {"type": "object", "properties": {"joke": {"type": "string"}}}},
|
||||
}
|
||||
|
||||
def is_strict_json_schema(self) -> bool:
|
||||
return False
|
||||
|
||||
def validate_json(self, json_str: str) -> Any:
|
||||
json_obj = json.loads(json_str)
|
||||
# Just for demonstration, we'll return a list.
|
||||
return list(json_obj["jokes"].values())
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
output_type=OutputType,
|
||||
)
|
||||
|
||||
input = "Tell me 3 short jokes."
|
||||
|
||||
# First, let's try with a strict output type. This should raise an exception.
|
||||
try:
|
||||
result = await Runner.run(agent, input)
|
||||
raise AssertionError("Should have raised an exception")
|
||||
except Exception as e:
|
||||
print(f"Error (expected): {e}")
|
||||
|
||||
# Now let's try again with a non-strict output type. This should work.
|
||||
# In some cases, it will raise an error - the schema isn't strict, so the model may
|
||||
# produce an invalid JSON object.
|
||||
agent.output_type = AgentOutputSchema(OutputType, strict_json_schema=False)
|
||||
try:
|
||||
result = await Runner.run(agent, input)
|
||||
print(result.final_output)
|
||||
except ModelBehaviorError as e:
|
||||
print(f"Non-strict output validation failed (expected possibility): {e}")
|
||||
|
||||
# Finally, let's try a custom output type.
|
||||
agent.output_type = CustomOutputSchema()
|
||||
result = await Runner.run(agent, input)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,74 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner
|
||||
from examples.auto_mode import input_with_fallback, is_auto_mode
|
||||
|
||||
"""This demonstrates usage of the `previous_response_id` parameter to continue a conversation.
|
||||
The second run passes the previous response ID to the model, which allows it to continue the
|
||||
conversation without re-sending the previous messages.
|
||||
|
||||
Notes:
|
||||
1. This only applies to the OpenAI Responses API. Other models will ignore this parameter.
|
||||
2. Responses are only stored for 30 days as of this writing, so in production you should
|
||||
store the response ID along with an expiration date; if the response is no longer valid,
|
||||
you'll need to re-send the previous conversation history.
|
||||
"""
|
||||
|
||||
|
||||
async def main():
|
||||
print("=== Non-streaming Example ===")
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. be VERY concise.",
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, "What is the largest country in South America?")
|
||||
print(result.final_output)
|
||||
# Brazil
|
||||
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What is the capital of that country?",
|
||||
previous_response_id=result.last_response_id,
|
||||
)
|
||||
print(result.final_output)
|
||||
# Brasilia
|
||||
|
||||
|
||||
async def main_stream():
|
||||
print("=== Streaming Example ===")
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. be VERY concise.",
|
||||
)
|
||||
|
||||
result = Runner.run_streamed(agent, "What is the largest country in South America?")
|
||||
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event" and event.data.type == "response.output_text.delta":
|
||||
print(event.data.delta, end="", flush=True)
|
||||
|
||||
print()
|
||||
|
||||
result = Runner.run_streamed(
|
||||
agent,
|
||||
"What is the capital of that country?",
|
||||
previous_response_id=result.last_response_id,
|
||||
)
|
||||
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event" and event.data.type == "response.output_text.delta":
|
||||
print(event.data.delta, end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if is_auto_mode():
|
||||
asyncio.run(main())
|
||||
print()
|
||||
asyncio.run(main_stream())
|
||||
else:
|
||||
is_stream = input_with_fallback("Run in stream mode? (y/n): ", "n")
|
||||
if is_stream == "y":
|
||||
asyncio.run(main_stream())
|
||||
else:
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,79 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from agents import Agent, GenerateDynamicPromptData, Runner
|
||||
|
||||
"""
|
||||
NOTE: This example will not work out of the box, because the default prompt ID will not be available
|
||||
in your project.
|
||||
|
||||
To use it, please:
|
||||
1. Go to https://platform.openai.com/playground/prompts
|
||||
2. Create a new prompt variable, `poem_style`.
|
||||
3. Create a system prompt with the content:
|
||||
```
|
||||
Write a poem in {{poem_style}}
|
||||
```
|
||||
4. Run the example with the `--prompt-id` flag.
|
||||
"""
|
||||
|
||||
DEFAULT_PROMPT_ID = "pmpt_6965a984c7ac8194a8f4e79b00f838840118c1e58beb3332"
|
||||
|
||||
|
||||
class DynamicContext:
|
||||
def __init__(self, prompt_id: str):
|
||||
self.prompt_id = prompt_id
|
||||
self.poem_style = random.choice(["limerick", "haiku", "ballad"])
|
||||
print(f"[debug] DynamicContext initialized with poem_style: {self.poem_style}")
|
||||
|
||||
|
||||
async def _get_dynamic_prompt(data: GenerateDynamicPromptData):
|
||||
ctx: DynamicContext = data.context.context
|
||||
return {
|
||||
"id": ctx.prompt_id,
|
||||
"version": "1",
|
||||
"variables": {
|
||||
"poem_style": ctx.poem_style,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def dynamic_prompt(prompt_id: str):
|
||||
context = DynamicContext(prompt_id)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
prompt=_get_dynamic_prompt,
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, "Tell me about recursion in programming.", context=context)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
async def static_prompt(prompt_id: str):
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
prompt={
|
||||
"id": prompt_id,
|
||||
"version": "1",
|
||||
"variables": {
|
||||
"poem_style": "limerick",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, "Tell me about recursion in programming.")
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dynamic", action="store_true")
|
||||
parser.add_argument("--prompt-id", type=str, default=DEFAULT_PROMPT_ID)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.dynamic:
|
||||
asyncio.run(dynamic_prompt(args.prompt_id))
|
||||
else:
|
||||
asyncio.run(static_prompt(args.prompt_id))
|
||||
@@ -0,0 +1,31 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner
|
||||
|
||||
URL = "https://images.unsplash.com/photo-1505761671935-60b3a7427bad?auto=format&fit=crop&w=400&q=80"
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
)
|
||||
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "input_image", "detail": "auto", "image_url": URL}],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What do you see in this image?",
|
||||
},
|
||||
],
|
||||
)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner
|
||||
|
||||
URL = "https://www.berkshirehathaway.com/letters/2024ltr.pdf"
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant.",
|
||||
)
|
||||
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "input_file", "file_url": URL}],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Can you summarize the letter?",
|
||||
},
|
||||
],
|
||||
)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,112 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
ModelRetrySettings,
|
||||
ModelSettings,
|
||||
RetryDecision,
|
||||
RunConfig,
|
||||
Runner,
|
||||
retry_policies,
|
||||
)
|
||||
|
||||
|
||||
def format_error(error: object) -> str:
|
||||
if not isinstance(error, BaseException):
|
||||
return "Unknown error"
|
||||
return str(error) or error.__class__.__name__
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
apply_policies = retry_policies.any(
|
||||
# On OpenAI-backed models, provider_suggested() follows provider retry advice,
|
||||
# including fallback retryable statuses when x-should-retry is absent
|
||||
# (for example 408/409/429/5xx).
|
||||
retry_policies.provider_suggested(),
|
||||
retry_policies.retry_after(),
|
||||
retry_policies.network_error(),
|
||||
retry_policies.http_status([408, 409, 429, 500, 502, 503, 504]),
|
||||
)
|
||||
|
||||
async def policy(context) -> bool | RetryDecision:
|
||||
raw_decision = apply_policies(context)
|
||||
decision: bool | RetryDecision
|
||||
if inspect.isawaitable(raw_decision):
|
||||
decision = await raw_decision
|
||||
else:
|
||||
decision = raw_decision
|
||||
if isinstance(decision, RetryDecision):
|
||||
if not decision.retry:
|
||||
print(
|
||||
f"[retry] stop after attempt {context.attempt}/{context.max_retries + 1}: "
|
||||
f"{format_error(context.error)}"
|
||||
)
|
||||
return False
|
||||
|
||||
print(
|
||||
" | ".join(
|
||||
part
|
||||
for part in [
|
||||
f"[retry] retry attempt {context.attempt}/{context.max_retries + 1}",
|
||||
(
|
||||
f"waiting {decision.delay:.2f}s"
|
||||
if decision.delay is not None
|
||||
else "using default backoff"
|
||||
),
|
||||
f"reason: {decision.reason}" if decision.reason else None,
|
||||
f"error: {format_error(context.error)}",
|
||||
]
|
||||
if part is not None
|
||||
)
|
||||
)
|
||||
return decision
|
||||
|
||||
if not decision:
|
||||
print(
|
||||
f"[retry] stop after attempt {context.attempt}/{context.max_retries + 1}: "
|
||||
f"{format_error(context.error)}"
|
||||
)
|
||||
return decision
|
||||
|
||||
retry = ModelRetrySettings(
|
||||
max_retries=4,
|
||||
backoff={
|
||||
"initial_delay": 0.5,
|
||||
"max_delay": 5.0,
|
||||
"multiplier": 2.0,
|
||||
"jitter": True,
|
||||
},
|
||||
policy=policy,
|
||||
)
|
||||
|
||||
# RunConfig-level model_settings are shared defaults for the run.
|
||||
# If an Agent also defines model_settings, the Agent wins for overlapping
|
||||
# keys, while nested objects like retry/backoff are merged.
|
||||
run_config = RunConfig(model_settings=ModelSettings(retry=retry))
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a concise assistant. Answer in 3 short bullet points at most.",
|
||||
# This Agent repeats the same retry config for clarity. In real code you
|
||||
# can keep shared defaults in RunConfig and only put per-agent overrides
|
||||
# here when you need different retry behavior.
|
||||
model_settings=ModelSettings(retry=retry),
|
||||
)
|
||||
|
||||
print(
|
||||
"Retry support is configured. You will only see [retry] logs if a transient failure happens."
|
||||
)
|
||||
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Explain exponential backoff for API retries in plain English.",
|
||||
run_config=run_config,
|
||||
)
|
||||
|
||||
print("\nFinal output:\n")
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
ModelRetrySettings,
|
||||
ModelSettings,
|
||||
RetryDecision,
|
||||
RunConfig,
|
||||
Runner,
|
||||
retry_policies,
|
||||
)
|
||||
|
||||
|
||||
def format_error(error: object) -> str:
|
||||
if not isinstance(error, BaseException):
|
||||
return "Unknown error"
|
||||
return str(error) or error.__class__.__name__
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
apply_policies = retry_policies.any(
|
||||
# On OpenAI-backed models, provider_suggested() follows provider retry advice,
|
||||
# including fallback retryable statuses when x-should-retry is absent
|
||||
# (for example 408/409/429/5xx).
|
||||
retry_policies.provider_suggested(),
|
||||
retry_policies.retry_after(),
|
||||
retry_policies.network_error(),
|
||||
retry_policies.http_status([408, 409, 429, 500, 502, 503, 504]),
|
||||
)
|
||||
|
||||
async def policy(context) -> bool | RetryDecision:
|
||||
raw_decision = apply_policies(context)
|
||||
decision: bool | RetryDecision
|
||||
if inspect.isawaitable(raw_decision):
|
||||
decision = await raw_decision
|
||||
else:
|
||||
decision = raw_decision
|
||||
if isinstance(decision, RetryDecision):
|
||||
if not decision.retry:
|
||||
print(
|
||||
f"[retry] stop after attempt {context.attempt}/{context.max_retries + 1}: "
|
||||
f"{format_error(context.error)}"
|
||||
)
|
||||
return False
|
||||
|
||||
print(
|
||||
" | ".join(
|
||||
part
|
||||
for part in [
|
||||
f"[retry] retry attempt {context.attempt}/{context.max_retries + 1}",
|
||||
(
|
||||
f"waiting {decision.delay:.2f}s"
|
||||
if decision.delay is not None
|
||||
else "using default backoff"
|
||||
),
|
||||
f"reason: {decision.reason}" if decision.reason else None,
|
||||
f"error: {format_error(context.error)}",
|
||||
]
|
||||
if part is not None
|
||||
)
|
||||
)
|
||||
return decision
|
||||
|
||||
if not decision:
|
||||
print(
|
||||
f"[retry] stop after attempt {context.attempt}/{context.max_retries + 1}: "
|
||||
f"{format_error(context.error)}"
|
||||
)
|
||||
return decision
|
||||
|
||||
retry = ModelRetrySettings(
|
||||
max_retries=4,
|
||||
backoff={
|
||||
"initial_delay": 0.5,
|
||||
"max_delay": 5.0,
|
||||
"multiplier": 2.0,
|
||||
"jitter": True,
|
||||
},
|
||||
policy=policy,
|
||||
)
|
||||
|
||||
# RunConfig-level model_settings are shared defaults for the run.
|
||||
# If an Agent also defines model_settings, the Agent wins for overlapping
|
||||
# keys, while nested objects like retry/backoff are merged.
|
||||
run_config = RunConfig(model_settings=ModelSettings(retry=retry))
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a concise assistant. Answer in 3 short bullet points at most.",
|
||||
# Prefix with litellm/ to route this request through the LiteLLM adapter.
|
||||
model="litellm/openai/gpt-4o-mini",
|
||||
# This Agent repeats the same retry config for clarity. In real code you
|
||||
# can keep shared defaults in RunConfig and only put per-agent overrides
|
||||
# here when you need different retry behavior.
|
||||
model_settings=ModelSettings(retry=retry),
|
||||
)
|
||||
|
||||
print(
|
||||
"Retry support is configured. You will only see [retry] logs if a transient failure happens."
|
||||
)
|
||||
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Explain exponential backoff for API retries in plain English.",
|
||||
run_config=run_config,
|
||||
)
|
||||
|
||||
print("\nFinal output:\n")
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,87 @@
|
||||
import asyncio
|
||||
from typing import Annotated, Any
|
||||
|
||||
from openai.types.responses import ResponseFunctionCallArgumentsDeltaEvent
|
||||
|
||||
from agents import Agent, Runner, function_tool
|
||||
|
||||
|
||||
@function_tool
|
||||
def write_file(filename: Annotated[str, "Name of the file"], content: str) -> str:
|
||||
"""Write content to a file."""
|
||||
return f"File {filename} written successfully"
|
||||
|
||||
|
||||
@function_tool
|
||||
def create_config(
|
||||
project_name: Annotated[str, "Project name"],
|
||||
version: Annotated[str, "Project version"],
|
||||
dependencies: Annotated[list[str] | None, "Dependencies (list of packages)"],
|
||||
) -> str:
|
||||
"""Generate a project configuration file."""
|
||||
return f"Config for {project_name} v{version} created"
|
||||
|
||||
|
||||
async def main():
|
||||
"""
|
||||
Demonstrates real-time streaming of function call arguments.
|
||||
|
||||
Function arguments are streamed incrementally as they are generated,
|
||||
providing immediate feedback during parameter generation.
|
||||
"""
|
||||
agent = Agent(
|
||||
name="CodeGenerator",
|
||||
instructions="You are a helpful coding assistant. Use the provided tools to create files and configurations.",
|
||||
tools=[write_file, create_config],
|
||||
)
|
||||
|
||||
print("🚀 Function Call Arguments Streaming Demo")
|
||||
|
||||
result = Runner.run_streamed(
|
||||
agent,
|
||||
input="Create a Python web project called 'my-app' with FastAPI. Version 1.0.0, dependencies: fastapi, uvicorn",
|
||||
)
|
||||
|
||||
# Track function calls for detailed output
|
||||
function_calls: dict[Any, dict[str, Any]] = {} # call_id -> {name, arguments}
|
||||
current_active_call_id = None
|
||||
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event":
|
||||
# Function call started
|
||||
if event.data.type == "response.output_item.added":
|
||||
if getattr(event.data.item, "type", None) == "function_call":
|
||||
function_name = getattr(event.data.item, "name", "unknown")
|
||||
call_id = getattr(event.data.item, "call_id", "unknown")
|
||||
|
||||
function_calls[call_id] = {"name": function_name, "arguments": ""}
|
||||
current_active_call_id = call_id
|
||||
print(f"\n📞 Function call streaming started: {function_name}()")
|
||||
print("📝 Arguments building...")
|
||||
|
||||
# Real-time argument streaming
|
||||
elif isinstance(event.data, ResponseFunctionCallArgumentsDeltaEvent):
|
||||
if current_active_call_id and current_active_call_id in function_calls:
|
||||
function_calls[current_active_call_id]["arguments"] += event.data.delta
|
||||
print(event.data.delta, end="", flush=True)
|
||||
|
||||
# Function call completed
|
||||
elif event.data.type == "response.output_item.done":
|
||||
if hasattr(event.data.item, "call_id"):
|
||||
call_id = getattr(event.data.item, "call_id", "unknown")
|
||||
if call_id in function_calls:
|
||||
function_info = function_calls[call_id]
|
||||
print(f"\n✅ Function call streaming completed: {function_info['name']}")
|
||||
print()
|
||||
if current_active_call_id == call_id:
|
||||
current_active_call_id = None
|
||||
|
||||
print("Summary of all function calls:")
|
||||
for call_id, info in function_calls.items():
|
||||
print(f" - #{call_id}: {info['name']}({info['arguments']})")
|
||||
|
||||
print(f"\nResult: {result.final_output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,66 @@
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from agents import Agent, ItemHelpers, Runner, function_tool
|
||||
|
||||
|
||||
@function_tool
|
||||
def how_many_jokes() -> int:
|
||||
"""Return a random integer of jokes to tell between 1 and 10 (inclusive)."""
|
||||
return random.randint(1, 10)
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Joker",
|
||||
instructions="First call the `how_many_jokes` tool, then tell that many jokes.",
|
||||
tools=[how_many_jokes],
|
||||
)
|
||||
|
||||
result = Runner.run_streamed(
|
||||
agent,
|
||||
input="Hello",
|
||||
)
|
||||
print("=== Run starting ===")
|
||||
async for event in result.stream_events():
|
||||
# We'll ignore the raw responses event deltas
|
||||
if event.type == "raw_response_event":
|
||||
continue
|
||||
elif event.type == "agent_updated_stream_event":
|
||||
print(f"Agent updated: {event.new_agent.name}")
|
||||
continue
|
||||
elif event.type == "run_item_stream_event":
|
||||
if event.item.type == "tool_call_item":
|
||||
print(f"-- Tool was called: {getattr(event.item.raw_item, 'name', 'Unknown Tool')}")
|
||||
elif event.item.type == "tool_call_output_item":
|
||||
print(f"-- Tool output: {event.item.output}")
|
||||
elif event.item.type == "message_output_item":
|
||||
print(f"-- Message output:\n {ItemHelpers.text_message_output(event.item)}")
|
||||
else:
|
||||
pass # Ignore other event types
|
||||
|
||||
print("=== Run complete ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
# === Run starting ===
|
||||
# Agent updated: Joker
|
||||
# -- Tool was called: how_many_jokes
|
||||
# -- Tool output: 4
|
||||
# -- Message output:
|
||||
# Sure, here are four jokes for you:
|
||||
|
||||
# 1. **Why don't skeletons fight each other?**
|
||||
# They don't have the guts!
|
||||
|
||||
# 2. **What do you call fake spaghetti?**
|
||||
# An impasta!
|
||||
|
||||
# 3. **Why did the scarecrow win an award?**
|
||||
# Because he was outstanding in his field!
|
||||
|
||||
# 4. **Why did the bicycle fall over?**
|
||||
# Because it was two-tired!
|
||||
# === Run complete ===
|
||||
@@ -0,0 +1,21 @@
|
||||
import asyncio
|
||||
|
||||
from openai.types.responses import ResponseTextDeltaEvent
|
||||
|
||||
from agents import Agent, Runner
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Joker",
|
||||
instructions="You are a helpful assistant.",
|
||||
)
|
||||
|
||||
result = Runner.run_streamed(agent, input="Please tell me 5 jokes.")
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
|
||||
print(event.data.delta, end="", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,236 @@
|
||||
"""Responses websocket streaming example with function tools, agent-as-tool, and approval.
|
||||
|
||||
This example shows a user-facing websocket workflow using
|
||||
`responses_websocket_session(...)`:
|
||||
- Streaming output (including reasoning summary deltas when available)
|
||||
- Regular function tools
|
||||
- An `Agent.as_tool(...)` specialist agent
|
||||
- HITL approval for a sensitive tool call
|
||||
- A follow-up turn using `previous_response_id` on the same trace
|
||||
|
||||
Required environment variable:
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
Optional environment variables:
|
||||
- `OPENAI_MODEL` (defaults to `gpt-5.6-sol`)
|
||||
- `OPENAI_BASE_URL`
|
||||
- `OPENAI_WEBSOCKET_BASE_URL`
|
||||
- `EXAMPLES_INTERACTIVE_MODE=auto` (auto-approve HITL prompts for scripted runs)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
ModelSettings,
|
||||
ResponsesWebSocketSession,
|
||||
function_tool,
|
||||
responses_websocket_session,
|
||||
trace,
|
||||
)
|
||||
from examples.auto_mode import confirm_with_fallback
|
||||
|
||||
|
||||
@function_tool
|
||||
def lookup_order(order_id: str) -> dict[str, Any]:
|
||||
"""Return deterministic order data for the demo."""
|
||||
orders = {
|
||||
"ORD-1001": {
|
||||
"order_id": "ORD-1001",
|
||||
"status": "delivered",
|
||||
"delivered_days_ago": 3,
|
||||
"amount": 49.99,
|
||||
"currency": "USD",
|
||||
"item": "Wireless Mouse",
|
||||
},
|
||||
"ORD-2002": {
|
||||
"order_id": "ORD-2002",
|
||||
"status": "delivered",
|
||||
"delivered_days_ago": 12,
|
||||
"amount": 129.0,
|
||||
"currency": "USD",
|
||||
"item": "Keyboard",
|
||||
},
|
||||
}
|
||||
return orders.get(
|
||||
order_id,
|
||||
{
|
||||
"order_id": order_id,
|
||||
"status": "unknown",
|
||||
"delivered_days_ago": 999,
|
||||
"amount": 0.0,
|
||||
"currency": "USD",
|
||||
"item": "unknown",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@function_tool(needs_approval=True)
|
||||
def submit_refund(order_id: str, amount: float, reason: str) -> dict[str, Any]:
|
||||
"""Create a refund request. This tool requires approval."""
|
||||
ticket = "RF-1001" if order_id == "ORD-1001" else f"RF-{order_id[-4:]}"
|
||||
return {
|
||||
"refund_ticket": ticket,
|
||||
"order_id": order_id,
|
||||
"amount": amount,
|
||||
"reason": reason,
|
||||
"status": "approved_pending_processing",
|
||||
}
|
||||
|
||||
|
||||
def ask_approval(question: str) -> bool:
|
||||
"""Prompt for approval (or auto-approve in examples auto mode)."""
|
||||
return confirm_with_fallback(f"[approval] {question} [y/N]: ", default=True)
|
||||
|
||||
|
||||
async def run_streamed_turn(
|
||||
ws: ResponsesWebSocketSession,
|
||||
agent: Agent[Any],
|
||||
prompt: str,
|
||||
*,
|
||||
previous_response_id: str | None = None,
|
||||
) -> tuple[str, str]:
|
||||
"""Run one streamed turn and handle HITL approvals if needed."""
|
||||
print(f"\nUser: {prompt}\n")
|
||||
|
||||
result = ws.run_streamed(
|
||||
agent,
|
||||
prompt,
|
||||
previous_response_id=previous_response_id,
|
||||
)
|
||||
printed_reasoning = False
|
||||
printed_output = False
|
||||
|
||||
while True:
|
||||
async for event in result.stream_events():
|
||||
if event.type == "raw_response_event":
|
||||
raw = event.data
|
||||
if raw.type == "response.reasoning_summary_text.delta":
|
||||
if not printed_reasoning:
|
||||
print("Reasoning:")
|
||||
printed_reasoning = True
|
||||
print(raw.delta, end="", flush=True)
|
||||
elif raw.type == "response.output_text.delta":
|
||||
if printed_reasoning and not printed_output:
|
||||
print("\n")
|
||||
if not printed_output:
|
||||
print("Assistant:")
|
||||
printed_output = True
|
||||
print(raw.delta, end="", flush=True)
|
||||
continue
|
||||
|
||||
if event.type != "run_item_stream_event":
|
||||
continue
|
||||
|
||||
item = event.item
|
||||
if item.type == "tool_call_item":
|
||||
tool_name = getattr(item.raw_item, "name", "unknown")
|
||||
tool_args = getattr(item.raw_item, "arguments", "")
|
||||
print(f"\n[tool call] {tool_name}({tool_args})")
|
||||
elif item.type == "tool_call_output_item":
|
||||
print(f"[tool result] {item.output}")
|
||||
|
||||
if printed_reasoning or printed_output:
|
||||
print("\n")
|
||||
|
||||
if not result.interruptions:
|
||||
break
|
||||
|
||||
state = result.to_state()
|
||||
for interruption in result.interruptions:
|
||||
question = f"Approve {interruption.name} with args {interruption.arguments}?"
|
||||
if ask_approval(question):
|
||||
state.approve(interruption)
|
||||
else:
|
||||
state.reject(interruption)
|
||||
|
||||
result = ws.run_streamed(agent, state)
|
||||
|
||||
if result.last_response_id is None:
|
||||
raise RuntimeError("The streamed run completed without a response_id.")
|
||||
|
||||
final_output = str(result.final_output)
|
||||
print(f"response_id: {result.last_response_id}")
|
||||
print(f"final_output: {final_output}\n")
|
||||
return result.last_response_id, final_output
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
model_name = os.getenv("OPENAI_MODEL", "gpt-5.6-sol")
|
||||
policy_agent = Agent(
|
||||
name="RefundPolicySpecialist",
|
||||
instructions=(
|
||||
"You are a refund policy specialist. The policy is simple: orders delivered "
|
||||
"within 7 days are eligible for a full refund, and older delivered orders "
|
||||
"are not. Return a short answer with eligibility and a one-line reason."
|
||||
),
|
||||
model=model_name,
|
||||
model_settings=ModelSettings(max_tokens=120),
|
||||
)
|
||||
|
||||
support_agent = Agent(
|
||||
name="SupportAgent",
|
||||
instructions=(
|
||||
"You are a support agent. For refund requests, do this in order: "
|
||||
"1) call lookup_order, 2) call refund_policy_specialist, 3) if the user "
|
||||
"asked to proceed and the order is eligible, call submit_refund. "
|
||||
"When asked for only the refund ticket, return only the ticket token "
|
||||
"(for example RF-1001)."
|
||||
),
|
||||
tools=[
|
||||
lookup_order,
|
||||
policy_agent.as_tool(
|
||||
tool_name="refund_policy_specialist",
|
||||
tool_description="Check refund eligibility and explain the policy decision.",
|
||||
),
|
||||
submit_refund,
|
||||
],
|
||||
model=model_name,
|
||||
model_settings=ModelSettings(
|
||||
max_tokens=200,
|
||||
reasoning=Reasoning(effort="medium", summary="detailed"),
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
# You can skip this helper and call Runner.run_streamed(...) directly.
|
||||
# It will still work, but each run will create/connect again unless you manually
|
||||
# reuse the same RunConfig/provider. This helper makes that reuse easy across turns
|
||||
# (and nested agent-as-tool runs) so the websocket connection can stay warm.
|
||||
async with responses_websocket_session() as ws:
|
||||
with trace("Responses WS support example") as current_trace:
|
||||
print(f"Using model={model_name}")
|
||||
print(f"trace_id={current_trace.trace_id}")
|
||||
|
||||
first_response_id, _ = await run_streamed_turn(
|
||||
ws,
|
||||
support_agent,
|
||||
(
|
||||
"Customer wants a refund for order ORD-1001 because the mouse arrived "
|
||||
"damaged. Please check the order, ask the refund policy specialist, and "
|
||||
"if it is eligible submit the refund. Reply with only the refund ticket."
|
||||
),
|
||||
)
|
||||
|
||||
await run_streamed_turn(
|
||||
ws,
|
||||
support_agent,
|
||||
"What refund ticket did you just create? Reply with only the ticket.",
|
||||
previous_response_id=first_response_id,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
if "closed before any response events" in str(exc):
|
||||
print(
|
||||
"\nWebsocket mode closed before sending events. This usually means the "
|
||||
"feature is not enabled for this account/model yet."
|
||||
)
|
||||
return
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,181 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
Runner,
|
||||
ToolGuardrailFunctionOutput,
|
||||
ToolInputGuardrailData,
|
||||
ToolOutputGuardrailData,
|
||||
ToolOutputGuardrailTripwireTriggered,
|
||||
function_tool,
|
||||
tool_input_guardrail,
|
||||
tool_output_guardrail,
|
||||
)
|
||||
|
||||
|
||||
@function_tool
|
||||
def send_email(to: str, subject: str, body: str) -> str:
|
||||
"""Send an email to the specified recipient."""
|
||||
return f"Email sent to {to} with subject '{subject}'"
|
||||
|
||||
|
||||
@function_tool
|
||||
def get_user_data(user_id: str) -> dict[str, str]:
|
||||
"""Get user data by ID."""
|
||||
# Simulate returning sensitive data
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"ssn": "123-45-6789", # Sensitive data that should be blocked!
|
||||
"phone": "555-1234",
|
||||
}
|
||||
|
||||
|
||||
@function_tool
|
||||
def get_contact_info(user_id: str) -> dict[str, str]:
|
||||
"""Get contact info by ID."""
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"name": "Jane Smith",
|
||||
"email": "jane@example.com",
|
||||
"phone": "555-1234",
|
||||
}
|
||||
|
||||
|
||||
@tool_input_guardrail
|
||||
def reject_sensitive_words(data: ToolInputGuardrailData) -> ToolGuardrailFunctionOutput:
|
||||
"""Reject tool calls that contain sensitive words in arguments."""
|
||||
try:
|
||||
args = json.loads(data.context.tool_arguments) if data.context.tool_arguments else {}
|
||||
except json.JSONDecodeError:
|
||||
return ToolGuardrailFunctionOutput(output_info="Invalid JSON arguments")
|
||||
|
||||
# Check for suspicious content
|
||||
sensitive_words = [
|
||||
"password",
|
||||
"hack",
|
||||
"exploit",
|
||||
"malware",
|
||||
"ACME",
|
||||
]
|
||||
for key, value in args.items():
|
||||
value_str = str(value).lower()
|
||||
for word in sensitive_words:
|
||||
if word.lower() in value_str:
|
||||
# Reject tool call and inform the model the function was not called
|
||||
return ToolGuardrailFunctionOutput.reject_content(
|
||||
message=f"🚨 Tool call blocked: contains '{word}'",
|
||||
output_info={"blocked_word": word, "argument": key},
|
||||
)
|
||||
|
||||
return ToolGuardrailFunctionOutput(output_info="Input validated")
|
||||
|
||||
|
||||
@tool_output_guardrail
|
||||
def block_sensitive_output(data: ToolOutputGuardrailData) -> ToolGuardrailFunctionOutput:
|
||||
"""Block tool outputs that contain sensitive data."""
|
||||
output_str = str(data.output).lower()
|
||||
|
||||
# Check for sensitive data patterns
|
||||
if "ssn" in output_str or "123-45-6789" in output_str:
|
||||
# Use raise_exception to halt execution completely for sensitive data
|
||||
return ToolGuardrailFunctionOutput.raise_exception(
|
||||
output_info={"blocked_pattern": "SSN", "tool": data.context.tool_name},
|
||||
)
|
||||
|
||||
return ToolGuardrailFunctionOutput(output_info="Output validated")
|
||||
|
||||
|
||||
@tool_output_guardrail
|
||||
def reject_phone_numbers(data: ToolOutputGuardrailData) -> ToolGuardrailFunctionOutput:
|
||||
"""Reject function output containing phone numbers."""
|
||||
output_str = str(data.output)
|
||||
if "555-1234" in output_str:
|
||||
return ToolGuardrailFunctionOutput.reject_content(
|
||||
message="User data not retrieved as it contains a phone number which is restricted.",
|
||||
output_info={"redacted": "phone_number"},
|
||||
)
|
||||
return ToolGuardrailFunctionOutput(output_info="Phone number check passed")
|
||||
|
||||
|
||||
# Apply guardrails to tools
|
||||
send_email.tool_input_guardrails = [reject_sensitive_words]
|
||||
get_user_data.tool_output_guardrails = [block_sensitive_output]
|
||||
get_contact_info.tool_output_guardrails = [reject_phone_numbers]
|
||||
|
||||
agent = Agent(
|
||||
name="Secure Assistant",
|
||||
instructions=(
|
||||
"You are a helpful assistant with access to email and user data tools. "
|
||||
"When the user provides all required arguments for a requested tool, call it instead of "
|
||||
"asking a follow-up question."
|
||||
),
|
||||
tools=[send_email, get_user_data, get_contact_info],
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
print("=== Tool Guardrails Example ===\n")
|
||||
|
||||
try:
|
||||
# Example 1: Normal operation - should work fine
|
||||
print("1. Normal email sending:")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Send an email to john@example.com with subject 'Welcome' and body "
|
||||
"'Welcome to our service.'",
|
||||
)
|
||||
print(f"✅ Successful tool execution: {result.final_output}\n")
|
||||
|
||||
# Example 2: Input guardrail triggers - function tool call is rejected but execution continues
|
||||
print("2. Attempting to send email with suspicious content:")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Send an email to john@example.com with subject 'Introduction' and body "
|
||||
"'Introducing ACME corp.'",
|
||||
)
|
||||
print(f"❌ Guardrail rejected function tool call: {result.final_output}\n")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}\n")
|
||||
|
||||
try:
|
||||
# Example 3: Output guardrail triggers - should raise exception for sensitive data
|
||||
print("3. Attempting to get user data (contains SSN). Execution blocked:")
|
||||
result = await Runner.run(agent, "Get the data for user ID user123")
|
||||
print(f"✅ Successful tool execution: {result.final_output}\n")
|
||||
except ToolOutputGuardrailTripwireTriggered as e:
|
||||
print("🚨 Output guardrail triggered: Execution halted for sensitive data")
|
||||
print(f"Details: {e.output.output_info}\n")
|
||||
|
||||
try:
|
||||
# Example 4: Output guardrail triggers - reject returning function tool output but continue execution
|
||||
print("4. Rejecting function tool output containing phone numbers:")
|
||||
result = await Runner.run(agent, "Get contact info for user456")
|
||||
print(f"❌ Guardrail rejected function tool output: {result.final_output}\n")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
"""
|
||||
Example output:
|
||||
|
||||
=== Tool Guardrails Example ===
|
||||
|
||||
1. Normal email sending:
|
||||
✅ Successful tool execution: I've sent a welcome email to john@example.com with an appropriate subject and greeting message.
|
||||
|
||||
2. Attempting to send email with suspicious content:
|
||||
❌ Guardrail rejected function tool call: I'm unable to send the email as mentioning ACME Corp. is restricted.
|
||||
|
||||
3. Attempting to get user data (contains SSN). Execution blocked:
|
||||
🚨 Output guardrail triggered: Execution halted for sensitive data
|
||||
Details: {'blocked_pattern': 'SSN', 'tool': 'get_user_data'}
|
||||
|
||||
4. Rejecting function tool output containing sensitive data:
|
||||
❌ Guardrail rejected function tool output: I'm unable to retrieve the contact info for user456 because it contains restricted information.
|
||||
"""
|
||||
@@ -0,0 +1,36 @@
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agents import Agent, Runner, function_tool
|
||||
|
||||
|
||||
class Weather(BaseModel):
|
||||
city: str = Field(description="The city name")
|
||||
temperature_range: str = Field(description="The temperature range in Celsius")
|
||||
conditions: str = Field(description="The weather conditions")
|
||||
|
||||
|
||||
@function_tool
|
||||
def get_weather(city: Annotated[str, "The city to get the weather for"]) -> Weather:
|
||||
"""Get the current weather information for a specified city."""
|
||||
print("[debug] get_weather called")
|
||||
return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.")
|
||||
|
||||
|
||||
agent = Agent(
|
||||
name="Hello world",
|
||||
instructions="You are a helpful agent.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
result = await Runner.run(agent, input="What's the weather in Tokyo?")
|
||||
print(result.final_output)
|
||||
# The weather in Tokyo is sunny.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent, Runner, Usage, function_tool
|
||||
|
||||
|
||||
class Weather(BaseModel):
|
||||
city: str
|
||||
temperature_range: str
|
||||
conditions: str
|
||||
|
||||
|
||||
@function_tool
|
||||
def get_weather(city: str) -> Weather:
|
||||
"""Get the current weather information for a specified city."""
|
||||
return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.")
|
||||
|
||||
|
||||
def print_usage(usage: Usage) -> None:
|
||||
print("\n=== Usage ===")
|
||||
print(f"Input tokens: {usage.input_tokens}")
|
||||
print(f"Output tokens: {usage.output_tokens}")
|
||||
print(f"Total tokens: {usage.total_tokens}")
|
||||
print(f"Requests: {usage.requests}")
|
||||
for i, request in enumerate(usage.request_usage_entries):
|
||||
print(f" {i + 1}: {request.input_tokens} input, {request.output_tokens} output")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
agent = Agent(
|
||||
name="Usage Demo",
|
||||
instructions="You are a concise assistant. Use tools if needed.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, "What's the weather in Tokyo?")
|
||||
|
||||
print("\nFinal output:")
|
||||
print(result.final_output)
|
||||
|
||||
# Access usage from the run context
|
||||
print_usage(result.context_wrapper.usage)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,191 @@
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
HandoffOutputItem,
|
||||
ItemHelpers,
|
||||
MessageOutputItem,
|
||||
RunContextWrapper,
|
||||
Runner,
|
||||
ToolCallItem,
|
||||
ToolCallOutputItem,
|
||||
TResponseInputItem,
|
||||
function_tool,
|
||||
handoff,
|
||||
trace,
|
||||
)
|
||||
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
|
||||
from examples.auto_mode import input_with_fallback, is_auto_mode
|
||||
|
||||
### CONTEXT
|
||||
|
||||
|
||||
class AirlineAgentContext(BaseModel):
|
||||
passenger_name: str | None = None
|
||||
confirmation_number: str | None = None
|
||||
seat_number: str | None = None
|
||||
flight_number: str | None = None
|
||||
|
||||
|
||||
### TOOLS
|
||||
|
||||
|
||||
@function_tool(
|
||||
name_override="faq_lookup_tool", description_override="Lookup frequently asked questions."
|
||||
)
|
||||
async def faq_lookup_tool(question: str) -> str:
|
||||
question_lower = question.lower()
|
||||
if any(
|
||||
keyword in question_lower
|
||||
for keyword in ["bag", "baggage", "luggage", "carry-on", "hand luggage", "hand carry"]
|
||||
):
|
||||
return (
|
||||
"You are allowed to bring one bag on the plane. "
|
||||
"It must be under 50 pounds and 22 inches x 14 inches x 9 inches."
|
||||
)
|
||||
elif any(keyword in question_lower for keyword in ["seat", "seats", "seating", "plane"]):
|
||||
return (
|
||||
"There are 120 seats on the plane. "
|
||||
"There are 22 business class seats and 98 economy seats. "
|
||||
"Exit rows are rows 4 and 16. "
|
||||
"Rows 5-8 are Economy Plus, with extra legroom. "
|
||||
)
|
||||
elif any(
|
||||
keyword in question_lower
|
||||
for keyword in ["wifi", "internet", "wireless", "connectivity", "network", "online"]
|
||||
):
|
||||
return "We have free wifi on the plane, join Airline-Wifi"
|
||||
return "I'm sorry, I don't know the answer to that question."
|
||||
|
||||
|
||||
@function_tool
|
||||
async def update_seat(
|
||||
context: RunContextWrapper[AirlineAgentContext], confirmation_number: str, new_seat: str
|
||||
) -> str:
|
||||
"""
|
||||
Update the seat for a given confirmation number.
|
||||
|
||||
Args:
|
||||
confirmation_number: The confirmation number for the flight.
|
||||
new_seat: The new seat to update to.
|
||||
"""
|
||||
# Update the context based on the customer's input
|
||||
context.context.confirmation_number = confirmation_number
|
||||
context.context.seat_number = new_seat
|
||||
# Ensure that the flight number has been set by the incoming handoff
|
||||
assert context.context.flight_number is not None, "Flight number is required"
|
||||
return f"Updated seat to {new_seat} for confirmation number {confirmation_number}"
|
||||
|
||||
|
||||
### HOOKS
|
||||
|
||||
|
||||
async def on_seat_booking_handoff(context: RunContextWrapper[AirlineAgentContext]) -> None:
|
||||
flight_number = f"FLT-{random.randint(100, 999)}"
|
||||
context.context.flight_number = flight_number
|
||||
|
||||
|
||||
### AGENTS
|
||||
|
||||
faq_agent = Agent[AirlineAgentContext](
|
||||
name="FAQ Agent",
|
||||
handoff_description="A helpful agent that can answer questions about the airline.",
|
||||
instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
|
||||
You are an FAQ agent. If you are speaking to a customer, you probably were transferred to from the triage agent.
|
||||
Use the following routine to support the customer.
|
||||
# Routine
|
||||
1. Identify the last question asked by the customer.
|
||||
2. Use the faq lookup tool to answer the question. Do not rely on your own knowledge.
|
||||
3. If you cannot answer the question, transfer back to the triage agent.""",
|
||||
tools=[faq_lookup_tool],
|
||||
)
|
||||
|
||||
seat_booking_agent = Agent[AirlineAgentContext](
|
||||
name="Seat Booking Agent",
|
||||
handoff_description="A helpful agent that can update a seat on a flight.",
|
||||
instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
|
||||
You are a seat booking agent. If you are speaking to a customer, you probably were transferred to from the triage agent.
|
||||
Use the following routine to support the customer.
|
||||
# Routine
|
||||
1. Ask for their confirmation number.
|
||||
2. Ask the customer what their desired seat number is.
|
||||
3. Use the update seat tool to update the seat on the flight.
|
||||
If the customer asks a question that is not related to the routine, transfer back to the triage agent. """,
|
||||
tools=[update_seat],
|
||||
)
|
||||
|
||||
triage_agent = Agent[AirlineAgentContext](
|
||||
name="Triage Agent",
|
||||
handoff_description="A triage agent that can delegate a customer's request to the appropriate agent.",
|
||||
instructions=(
|
||||
f"{RECOMMENDED_PROMPT_PREFIX} "
|
||||
"You are a helpful triaging agent. You can use your tools to delegate questions to other appropriate agents."
|
||||
),
|
||||
handoffs=[
|
||||
handoff(agent=faq_agent, tool_name_override="transfer_to_faq_agent"),
|
||||
handoff(
|
||||
agent=seat_booking_agent,
|
||||
on_handoff=on_seat_booking_handoff,
|
||||
tool_name_override="transfer_to_seat_booking_agent",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
faq_agent.handoffs.append(
|
||||
handoff(agent=triage_agent, tool_name_override="transfer_to_triage_agent")
|
||||
)
|
||||
seat_booking_agent.handoffs.append(
|
||||
handoff(agent=triage_agent, tool_name_override="transfer_to_triage_agent")
|
||||
)
|
||||
|
||||
|
||||
### RUN
|
||||
|
||||
|
||||
async def main():
|
||||
current_agent: Agent[AirlineAgentContext] = triage_agent
|
||||
input_items: list[TResponseInputItem] = []
|
||||
context = AirlineAgentContext()
|
||||
auto_mode = is_auto_mode()
|
||||
|
||||
# Normally, each input from the user would be an API request to your app, and you can wrap the request in a trace()
|
||||
# Here, we'll just use a random UUID for the conversation ID
|
||||
conversation_id = uuid.uuid4().hex[:16]
|
||||
|
||||
while True:
|
||||
user_input = input_with_fallback(
|
||||
"Enter your message: ",
|
||||
"What are your store hours?",
|
||||
)
|
||||
with trace("Customer service", group_id=conversation_id):
|
||||
input_items.append({"content": user_input, "role": "user"})
|
||||
result = await Runner.run(current_agent, input_items, context=context)
|
||||
|
||||
for new_item in result.new_items:
|
||||
agent_name = new_item.agent.name
|
||||
if isinstance(new_item, MessageOutputItem):
|
||||
print(f"{agent_name}: {ItemHelpers.text_message_output(new_item)}")
|
||||
elif isinstance(new_item, HandoffOutputItem):
|
||||
print(
|
||||
f"Handed off from {new_item.source_agent.name} to {new_item.target_agent.name}"
|
||||
)
|
||||
elif isinstance(new_item, ToolCallItem):
|
||||
print(f"{agent_name}: Calling a tool")
|
||||
elif isinstance(new_item, ToolCallOutputItem):
|
||||
print(f"{agent_name}: Tool call output: {new_item.output}")
|
||||
else:
|
||||
print(f"{agent_name}: Skipping item: {new_item.__class__.__name__}")
|
||||
input_items = result.to_input_list()
|
||||
current_agent = result.last_agent
|
||||
if auto_mode:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,38 @@
|
||||
# Financial Research Agent Example
|
||||
|
||||
This example shows how you might compose a richer financial research agent using the Agents SDK. The pattern is similar to the `research_bot` example, but with more specialized sub‑agents and a verification step.
|
||||
|
||||
The flow is:
|
||||
|
||||
1. **Planning**: A planner agent turns the end user’s request into a list of search terms relevant to financial analysis – recent news, earnings calls, corporate filings, industry commentary, etc.
|
||||
2. **Search**: A search agent uses the built‑in `WebSearchTool` to retrieve terse summaries for each search term. (You could also add `FileSearchTool` if you have indexed PDFs or 10‑Ks.)
|
||||
3. **Sub‑analysts**: Additional agents (e.g. a fundamentals analyst and a risk analyst) are exposed as tools so the writer can call them inline and incorporate their outputs.
|
||||
4. **Writing**: A senior writer agent brings together the search snippets and any sub‑analyst summaries into a long‑form markdown report plus a short executive summary.
|
||||
5. **Verification**: A final verifier agent audits the report for obvious inconsistencies or missing sourcing.
|
||||
|
||||
You can run the example with:
|
||||
|
||||
```bash
|
||||
python -m examples.financial_research_agent.main
|
||||
```
|
||||
|
||||
and enter a query like:
|
||||
|
||||
```
|
||||
Write up an analysis of Apple Inc.'s most recent quarter.
|
||||
```
|
||||
|
||||
### Starter prompt
|
||||
|
||||
The writer agent is seeded with instructions similar to:
|
||||
|
||||
```
|
||||
You are a senior financial analyst. You will be provided with the original query
|
||||
and a set of raw search summaries. Your job is to synthesize these into a
|
||||
long‑form markdown report (at least several paragraphs) with a short executive
|
||||
summary. You also have access to tools like `fundamentals_analysis` and
|
||||
`risk_analysis` to get short specialist write‑ups if you want to incorporate them.
|
||||
Add a few follow‑up questions for further research.
|
||||
```
|
||||
|
||||
You can tweak these prompts and sub‑agents to suit your own data sources and preferred report structure.
|
||||
@@ -0,0 +1,23 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent
|
||||
|
||||
# A sub‑agent focused on analyzing a company's fundamentals.
|
||||
FINANCIALS_PROMPT = (
|
||||
"You are a financial analyst focused on company fundamentals such as revenue, "
|
||||
"profit, margins and growth trajectory. Given a collection of web (and optional file) "
|
||||
"search results about a company, write a concise analysis of its recent financial "
|
||||
"performance. Pull out key metrics or quotes. Keep it under 2 paragraphs."
|
||||
)
|
||||
|
||||
|
||||
class AnalysisSummary(BaseModel):
|
||||
summary: str
|
||||
"""Short text summary for this aspect of the analysis."""
|
||||
|
||||
|
||||
financials_agent = Agent(
|
||||
name="FundamentalsAnalystAgent",
|
||||
instructions=FINANCIALS_PROMPT,
|
||||
output_type=AnalysisSummary,
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent
|
||||
|
||||
# Generate a plan of searches to ground the financial analysis.
|
||||
# For a given financial question or company, we want to search for
|
||||
# recent news, official filings, analyst commentary, and other
|
||||
# relevant background.
|
||||
PROMPT = (
|
||||
"You are a financial research planner. Given a request for financial analysis, "
|
||||
"produce a set of web searches to gather the context needed. Aim for recent "
|
||||
"headlines, earnings calls or 10‑K snippets, analyst commentary, and industry background. "
|
||||
"Output between 5 and 15 search terms to query for."
|
||||
)
|
||||
|
||||
|
||||
class FinancialSearchItem(BaseModel):
|
||||
reason: str
|
||||
"""Your reasoning for why this search is relevant."""
|
||||
|
||||
query: str
|
||||
"""The search term to feed into a web (or file) search."""
|
||||
|
||||
|
||||
class FinancialSearchPlan(BaseModel):
|
||||
searches: list[FinancialSearchItem]
|
||||
"""A list of searches to perform."""
|
||||
|
||||
|
||||
planner_agent = Agent(
|
||||
name="FinancialPlannerAgent",
|
||||
instructions=PROMPT,
|
||||
model="o3-mini",
|
||||
output_type=FinancialSearchPlan,
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent
|
||||
|
||||
# A sub‑agent specializing in identifying risk factors or concerns.
|
||||
RISK_PROMPT = (
|
||||
"You are a risk analyst looking for potential red flags in a company's outlook. "
|
||||
"Given background research, produce a short analysis of risks such as competitive threats, "
|
||||
"regulatory issues, supply chain problems, or slowing growth. Keep it under 2 paragraphs."
|
||||
)
|
||||
|
||||
|
||||
class AnalysisSummary(BaseModel):
|
||||
summary: str
|
||||
"""Short text summary for this aspect of the analysis."""
|
||||
|
||||
|
||||
risk_agent = Agent(
|
||||
name="RiskAnalystAgent",
|
||||
instructions=RISK_PROMPT,
|
||||
output_type=AnalysisSummary,
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent, ModelSettings, WebSearchTool
|
||||
|
||||
# Given a search term, use web search to pull back a brief summary.
|
||||
# Summaries should be concise but capture the main financial points.
|
||||
INSTRUCTIONS = (
|
||||
"You are a research assistant specializing in financial topics. "
|
||||
"Given a search term, use web search to retrieve up‑to‑date context and "
|
||||
"produce a short summary of at most 300 words. Focus on key numbers, events, "
|
||||
"or quotes that will be useful to a financial analyst."
|
||||
)
|
||||
|
||||
|
||||
class FinancialSearchSummary(BaseModel):
|
||||
summary: str
|
||||
"""A concise summary of the search findings."""
|
||||
|
||||
|
||||
search_agent = Agent(
|
||||
name="FinancialSearchAgent",
|
||||
model="gpt-5.6-sol",
|
||||
instructions=INSTRUCTIONS,
|
||||
tools=[WebSearchTool()],
|
||||
model_settings=ModelSettings(response_include=["web_search_call.action.sources"]),
|
||||
output_type=FinancialSearchSummary,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent
|
||||
|
||||
# Agent to sanity‑check a synthesized report for consistency and recall.
|
||||
# This can be used to flag potential gaps or obvious mistakes.
|
||||
VERIFIER_PROMPT = (
|
||||
"You are a meticulous evidence auditor. You will receive an original request, an explicit "
|
||||
"research cutoff date, a financial report, and structured web research evidence with source "
|
||||
"URLs. Judge the report only against that supplied evidence; do not reject or approve claims "
|
||||
"based on your own memory. Check that material numeric and time-sensitive claims are supported "
|
||||
"by the evidence, that citations use supplied URLs, that the report is internally consistent, "
|
||||
"and that uncertainty is appropriately caveated. Treat information published on or before the "
|
||||
"research cutoff as potentially available. Mark unsupported claims separately from claims that "
|
||||
"the evidence directly contradicts."
|
||||
)
|
||||
|
||||
|
||||
class VerificationIssue(BaseModel):
|
||||
claim: str
|
||||
"""The report claim that needs attention."""
|
||||
|
||||
category: Literal["unsupported", "contradicted", "stale_or_unreleased", "other"]
|
||||
"""The evidence problem associated with the claim."""
|
||||
|
||||
explanation: str
|
||||
"""Why the evidence does not support the claim."""
|
||||
|
||||
source_urls: list[str]
|
||||
"""Relevant supplied source URLs, if any."""
|
||||
|
||||
|
||||
class VerificationResult(BaseModel):
|
||||
verified: bool
|
||||
"""Whether the report is coherent and supported by the supplied evidence."""
|
||||
|
||||
issues: list[VerificationIssue]
|
||||
"""Evidence-based issues that must be corrected before publication."""
|
||||
|
||||
|
||||
verifier_agent = Agent(
|
||||
name="VerificationAgent",
|
||||
instructions=VERIFIER_PROMPT,
|
||||
model="gpt-5.6-sol",
|
||||
output_type=VerificationResult,
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent
|
||||
|
||||
# Writer agent brings together the raw search results and optionally calls out
|
||||
# to sub‑analyst tools for specialized commentary, then returns a cohesive markdown report.
|
||||
WRITER_PROMPT = (
|
||||
"You are a senior financial analyst. You will be provided with the original query and "
|
||||
"a set of raw search summaries. Your task is to synthesize these into a long‑form markdown "
|
||||
"report (at least several paragraphs) including a short executive summary and follow‑up "
|
||||
"questions. If needed, you can call the available analysis tools (e.g. fundamentals_analysis, "
|
||||
"risk_analysis) to get short specialist write‑ups to incorporate. Every material numeric or "
|
||||
"time-sensitive claim must include an inline Markdown citation using a URL supplied in the "
|
||||
"research evidence. Never invent or alter a source URL."
|
||||
)
|
||||
|
||||
REVISION_PROMPT = (
|
||||
f"{WRITER_PROMPT} You are revising an existing report after evidence verification. Address "
|
||||
"every verification issue, remove claims that cannot be supported, preserve valid analysis, "
|
||||
"and return a complete replacement report rather than a patch or commentary."
|
||||
)
|
||||
|
||||
|
||||
class FinancialReportData(BaseModel):
|
||||
short_summary: str
|
||||
"""A short 2‑3 sentence executive summary."""
|
||||
|
||||
markdown_report: str
|
||||
"""The full markdown report."""
|
||||
|
||||
follow_up_questions: list[str]
|
||||
"""Suggested follow‑up questions for further research."""
|
||||
|
||||
|
||||
# Note: We will attach handoffs to specialist analyst agents at runtime in the manager.
|
||||
# This shows how an agent can use handoffs to delegate to specialized subagents.
|
||||
writer_agent = Agent(
|
||||
name="FinancialWriterAgent",
|
||||
instructions=WRITER_PROMPT,
|
||||
model="gpt-5.6-sol",
|
||||
output_type=FinancialReportData,
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
import asyncio
|
||||
|
||||
from examples.auto_mode import input_with_fallback
|
||||
|
||||
from .manager import FinancialResearchManager
|
||||
|
||||
|
||||
# Entrypoint for the financial bot example.
|
||||
# Run this as `python -m examples.financial_research_agent.main` and enter a
|
||||
# financial research query, for example:
|
||||
# "Write up an analysis of Apple Inc.'s most recent quarter."
|
||||
async def main() -> None:
|
||||
query = input_with_fallback(
|
||||
"Enter a financial research query: ",
|
||||
"Write a short analysis of Apple's long-term revenue drivers and key risks. "
|
||||
"Avoid making claims about unreleased quarterly results.",
|
||||
)
|
||||
mgr = FinancialResearchManager()
|
||||
await mgr.run(query)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,265 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from pydantic import BaseModel
|
||||
from rich.console import Console
|
||||
|
||||
from agents import Runner, RunResult, RunResultStreaming, custom_span, gen_trace_id, trace
|
||||
from examples.web_search_utils import extract_url_citations, extract_web_search_source_urls
|
||||
|
||||
from .agents.financials_agent import financials_agent
|
||||
from .agents.planner_agent import FinancialSearchItem, FinancialSearchPlan, planner_agent
|
||||
from .agents.risk_agent import risk_agent
|
||||
from .agents.search_agent import FinancialSearchSummary, search_agent
|
||||
from .agents.verifier_agent import VerificationResult, verifier_agent
|
||||
from .agents.writer_agent import REVISION_PROMPT, FinancialReportData, writer_agent
|
||||
from .printer import Printer
|
||||
|
||||
|
||||
class FinancialSource(BaseModel):
|
||||
title: str
|
||||
url: str
|
||||
|
||||
|
||||
class FinancialSearchEvidence(BaseModel):
|
||||
query: str
|
||||
reason: str
|
||||
summary: str
|
||||
sources: list[FinancialSource]
|
||||
retrieved_at: str
|
||||
|
||||
|
||||
def _extract_financial_sources(items: Sequence[object]) -> list[FinancialSource]:
|
||||
sources: list[FinancialSource] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for citation in extract_url_citations(items):
|
||||
if citation.url in seen:
|
||||
continue
|
||||
seen.add(citation.url)
|
||||
sources.append(FinancialSource(title=citation.title, url=citation.url))
|
||||
|
||||
for url in extract_web_search_source_urls(items):
|
||||
if url in seen:
|
||||
continue
|
||||
seen.add(url)
|
||||
sources.append(FinancialSource(title=url, url=url))
|
||||
|
||||
return sources
|
||||
|
||||
|
||||
async def _summary_extractor(run_result: RunResult | RunResultStreaming) -> str:
|
||||
"""Custom output extractor for sub‑agents that return an AnalysisSummary."""
|
||||
# The financial/risk analyst agents emit an AnalysisSummary with a `summary` field.
|
||||
# We want the tool call to return just that summary text so the writer can drop it inline.
|
||||
return str(run_result.final_output.summary)
|
||||
|
||||
|
||||
class FinancialResearchManager:
|
||||
"""
|
||||
Orchestrates the full flow: planning, searching, sub‑analysis, writing, and verification.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.console = Console()
|
||||
self.printer = Printer(self.console)
|
||||
self.research_cutoff = datetime.now(timezone.utc).date().isoformat()
|
||||
|
||||
async def run(self, query: str) -> None:
|
||||
trace_id = gen_trace_id()
|
||||
try:
|
||||
with trace("Financial research trace", trace_id=trace_id):
|
||||
self.printer.update_item(
|
||||
"trace_id",
|
||||
f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}",
|
||||
is_done=True,
|
||||
hide_checkmark=True,
|
||||
)
|
||||
self.printer.update_item("start", "Starting financial research...", is_done=True)
|
||||
search_plan = await self._plan_searches(query)
|
||||
search_results = await self._perform_searches(search_plan)
|
||||
report, verification = await self._produce_verified_report(query, search_results)
|
||||
|
||||
final_report = f"Report summary\n\n{report.short_summary}"
|
||||
self.printer.update_item("final_report", final_report, is_done=True)
|
||||
finally:
|
||||
self.printer.end()
|
||||
|
||||
# Print to stdout
|
||||
print("\n\n=====REPORT=====\n\n")
|
||||
print(f"Report:\n{report.markdown_report}")
|
||||
print("\n\n=====FOLLOW UP QUESTIONS=====\n\n")
|
||||
print("\n".join(report.follow_up_questions))
|
||||
print("\n\n=====VERIFICATION=====\n\n")
|
||||
print(verification)
|
||||
|
||||
async def _produce_verified_report(
|
||||
self,
|
||||
query: str,
|
||||
search_results: Sequence[FinancialSearchEvidence],
|
||||
) -> tuple[FinancialReportData, VerificationResult]:
|
||||
report = await self._write_report(query, search_results)
|
||||
verification = await self._verify_report(query, report, search_results)
|
||||
if verification.verified:
|
||||
return report, verification
|
||||
|
||||
report = await self._revise_report(query, report, search_results, verification)
|
||||
verification = await self._verify_report(query, report, search_results)
|
||||
if not verification.verified:
|
||||
raise RuntimeError(
|
||||
"Financial report failed evidence verification after one revision: "
|
||||
f"{verification.model_dump_json()}"
|
||||
)
|
||||
return report, verification
|
||||
|
||||
async def _plan_searches(self, query: str) -> FinancialSearchPlan:
|
||||
self.printer.update_item("planning", "Planning searches...")
|
||||
result = await Runner.run(planner_agent, f"Query: {query}")
|
||||
self.printer.update_item(
|
||||
"planning",
|
||||
f"Will perform {len(result.final_output.searches)} searches",
|
||||
is_done=True,
|
||||
)
|
||||
return result.final_output_as(FinancialSearchPlan)
|
||||
|
||||
async def _perform_searches(
|
||||
self, search_plan: FinancialSearchPlan
|
||||
) -> Sequence[FinancialSearchEvidence]:
|
||||
with custom_span("Search the web"):
|
||||
self.printer.update_item("searching", "Searching...")
|
||||
tasks = [asyncio.create_task(self._search(item)) for item in search_plan.searches]
|
||||
results: list[FinancialSearchEvidence] = []
|
||||
num_completed = 0
|
||||
num_succeeded = 0
|
||||
num_failed = 0
|
||||
for task in asyncio.as_completed(tasks):
|
||||
result = await task
|
||||
if result is not None:
|
||||
results.append(result)
|
||||
num_succeeded += 1
|
||||
else:
|
||||
num_failed += 1
|
||||
num_completed += 1
|
||||
status = f"Searching... {num_completed}/{len(tasks)} finished"
|
||||
if num_failed:
|
||||
status += f" ({num_succeeded} succeeded, {num_failed} failed)"
|
||||
self.printer.update_item(
|
||||
"searching",
|
||||
status,
|
||||
)
|
||||
summary = f"Searches finished: {num_succeeded}/{len(tasks)} succeeded"
|
||||
if num_failed:
|
||||
summary += f", {num_failed} failed"
|
||||
self.printer.update_item("searching", summary, is_done=True)
|
||||
return results
|
||||
|
||||
async def _search(self, item: FinancialSearchItem) -> FinancialSearchEvidence | None:
|
||||
input_data = f"Search term: {item.query}\nReason: {item.reason}"
|
||||
try:
|
||||
result = await Runner.run(search_agent, input_data)
|
||||
search_summary = result.final_output_as(FinancialSearchSummary)
|
||||
sources = _extract_financial_sources(result.new_items)
|
||||
if not sources:
|
||||
return None
|
||||
return FinancialSearchEvidence(
|
||||
query=item.query,
|
||||
reason=item.reason,
|
||||
summary=search_summary.summary,
|
||||
sources=sources,
|
||||
retrieved_at=self.research_cutoff,
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def _write_report(
|
||||
self,
|
||||
query: str,
|
||||
search_results: Sequence[FinancialSearchEvidence],
|
||||
) -> FinancialReportData:
|
||||
# Expose the specialist analysts as tools so the writer can invoke them inline
|
||||
# and still produce the final FinancialReportData output.
|
||||
fundamentals_tool = financials_agent.as_tool(
|
||||
tool_name="fundamentals_analysis",
|
||||
tool_description="Use to get a short write‑up of key financial metrics",
|
||||
custom_output_extractor=_summary_extractor,
|
||||
)
|
||||
risk_tool = risk_agent.as_tool(
|
||||
tool_name="risk_analysis",
|
||||
tool_description="Use to get a short write‑up of potential red flags",
|
||||
custom_output_extractor=_summary_extractor,
|
||||
)
|
||||
writer_with_tools = writer_agent.clone(tools=[fundamentals_tool, risk_tool])
|
||||
self.printer.update_item("writing", "Thinking about report...")
|
||||
input_data = self._report_input(query, search_results)
|
||||
result = Runner.run_streamed(writer_with_tools, input_data)
|
||||
update_messages = [
|
||||
"Planning report structure...",
|
||||
"Writing sections...",
|
||||
"Finalizing report...",
|
||||
]
|
||||
last_update = time.time()
|
||||
next_message = 0
|
||||
async for _ in result.stream_events():
|
||||
if time.time() - last_update > 5 and next_message < len(update_messages):
|
||||
self.printer.update_item("writing", update_messages[next_message])
|
||||
next_message += 1
|
||||
last_update = time.time()
|
||||
self.printer.mark_item_done("writing")
|
||||
return result.final_output_as(FinancialReportData)
|
||||
|
||||
async def _revise_report(
|
||||
self,
|
||||
query: str,
|
||||
report: FinancialReportData,
|
||||
search_results: Sequence[FinancialSearchEvidence],
|
||||
verification: VerificationResult,
|
||||
) -> FinancialReportData:
|
||||
self.printer.update_item("revising", "Revising report from verification feedback...")
|
||||
revision_agent = writer_agent.clone(instructions=REVISION_PROMPT)
|
||||
input_data = (
|
||||
f"{self._report_input(query, search_results)}\n"
|
||||
f"Existing report:\n{report.model_dump_json()}\n"
|
||||
f"Verification feedback:\n{verification.model_dump_json()}"
|
||||
)
|
||||
result = await Runner.run(revision_agent, input_data)
|
||||
self.printer.mark_item_done("revising")
|
||||
return result.final_output_as(FinancialReportData)
|
||||
|
||||
async def _verify_report(
|
||||
self,
|
||||
query: str,
|
||||
report: FinancialReportData,
|
||||
search_results: Sequence[FinancialSearchEvidence],
|
||||
) -> VerificationResult:
|
||||
self.printer.update_item("verifying", "Verifying report...")
|
||||
input_data = json.dumps(
|
||||
{
|
||||
"original_query": query,
|
||||
"research_cutoff": self.research_cutoff,
|
||||
"report": report.model_dump(mode="json"),
|
||||
"evidence": [item.model_dump(mode="json") for item in search_results],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
result = await Runner.run(verifier_agent, input_data)
|
||||
self.printer.mark_item_done("verifying")
|
||||
return result.final_output_as(VerificationResult)
|
||||
|
||||
def _report_input(
|
||||
self,
|
||||
query: str,
|
||||
search_results: Sequence[FinancialSearchEvidence],
|
||||
) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"original_query": query,
|
||||
"research_cutoff": self.research_cutoff,
|
||||
"evidence": [item.model_dump(mode="json") for item in search_results],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console, Group
|
||||
from rich.live import Live
|
||||
from rich.spinner import Spinner
|
||||
|
||||
|
||||
class Printer:
|
||||
"""
|
||||
Simple wrapper to stream status updates. Used by the financial bot
|
||||
manager as it orchestrates planning, search and writing.
|
||||
"""
|
||||
|
||||
def __init__(self, console: Console) -> None:
|
||||
self.live = Live(console=console)
|
||||
self.items: dict[str, tuple[str, bool]] = {}
|
||||
self.hide_done_ids: set[str] = set()
|
||||
self.live.start()
|
||||
|
||||
def end(self) -> None:
|
||||
self.live.stop()
|
||||
|
||||
def hide_done_checkmark(self, item_id: str) -> None:
|
||||
self.hide_done_ids.add(item_id)
|
||||
|
||||
def update_item(
|
||||
self, item_id: str, content: str, is_done: bool = False, hide_checkmark: bool = False
|
||||
) -> None:
|
||||
self.items[item_id] = (content, is_done)
|
||||
if hide_checkmark:
|
||||
self.hide_done_ids.add(item_id)
|
||||
self.flush()
|
||||
|
||||
def mark_item_done(self, item_id: str) -> None:
|
||||
self.items[item_id] = (self.items[item_id][0], True)
|
||||
self.flush()
|
||||
|
||||
def flush(self) -> None:
|
||||
renderables: list[Any] = []
|
||||
for item_id, (content, is_done) in self.items.items():
|
||||
if is_done:
|
||||
prefix = "✅ " if item_id not in self.hide_done_ids else ""
|
||||
renderables.append(prefix + content)
|
||||
else:
|
||||
renderables.append(Spinner("dots", text=content))
|
||||
self.live.update(Group(*renderables))
|
||||
@@ -0,0 +1,187 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
|
||||
from agents import Agent, HandoffInputData, Runner, function_tool, handoff, trace
|
||||
from agents.extensions import handoff_filters
|
||||
from agents.models import is_gpt_5_default
|
||||
|
||||
|
||||
@function_tool
|
||||
def random_number_tool(max: int) -> int:
|
||||
"""Return a random integer between 0 and the given maximum."""
|
||||
return random.randint(0, max)
|
||||
|
||||
|
||||
def spanish_handoff_message_filter(handoff_message_data: HandoffInputData) -> HandoffInputData:
|
||||
if is_gpt_5_default():
|
||||
print("gpt-5 is enabled, so we're not filtering the input history")
|
||||
# when using gpt-5, removing some of the items could break things, so we do this filtering only for other models
|
||||
return HandoffInputData(
|
||||
input_history=handoff_message_data.input_history,
|
||||
pre_handoff_items=tuple(handoff_message_data.pre_handoff_items),
|
||||
new_items=tuple(handoff_message_data.new_items),
|
||||
)
|
||||
|
||||
# First, we'll remove any tool-related messages from the message history
|
||||
handoff_message_data = handoff_filters.remove_all_tools(handoff_message_data)
|
||||
|
||||
# Second, we'll also remove the first two items from the history, just for demonstration
|
||||
history = (
|
||||
tuple(handoff_message_data.input_history[2:])
|
||||
if isinstance(handoff_message_data.input_history, tuple)
|
||||
else handoff_message_data.input_history
|
||||
)
|
||||
|
||||
# or, you can use the HandoffInputData.clone(kwargs) method
|
||||
return HandoffInputData(
|
||||
input_history=history,
|
||||
pre_handoff_items=tuple(handoff_message_data.pre_handoff_items),
|
||||
new_items=tuple(handoff_message_data.new_items),
|
||||
)
|
||||
|
||||
|
||||
first_agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Be extremely concise.",
|
||||
tools=[random_number_tool],
|
||||
)
|
||||
|
||||
spanish_agent = Agent(
|
||||
name="Spanish Assistant",
|
||||
instructions="You only speak Spanish and are extremely concise.",
|
||||
handoff_description="A Spanish-speaking assistant.",
|
||||
)
|
||||
|
||||
second_agent = Agent(
|
||||
name="Assistant",
|
||||
instructions=(
|
||||
"Be a helpful assistant. If the user speaks Spanish, handoff to the Spanish assistant."
|
||||
),
|
||||
handoffs=[handoff(spanish_agent, input_filter=spanish_handoff_message_filter)],
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
# Trace the entire run as a single workflow
|
||||
with trace(workflow_name="Message filtering"):
|
||||
# 1. Send a regular message to the first agent
|
||||
result = await Runner.run(first_agent, input="Hi, my name is Sora.")
|
||||
|
||||
print("Step 1 done")
|
||||
|
||||
# 2. Ask it to generate a number
|
||||
result = await Runner.run(
|
||||
first_agent,
|
||||
input=result.to_input_list()
|
||||
+ [{"content": "Can you generate a random number between 0 and 100?", "role": "user"}],
|
||||
)
|
||||
|
||||
print("Step 2 done")
|
||||
|
||||
# 3. Call the second agent
|
||||
result = await Runner.run(
|
||||
second_agent,
|
||||
input=result.to_input_list()
|
||||
+ [
|
||||
{
|
||||
"content": "I live in New York City. Whats the population of the city?",
|
||||
"role": "user",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
print("Step 3 done")
|
||||
|
||||
# 4. Cause a handoff to occur
|
||||
result = await Runner.run(
|
||||
second_agent,
|
||||
input=result.to_input_list()
|
||||
+ [
|
||||
{
|
||||
"content": "Por favor habla en español. ¿Cuál es mi nombre y dónde vivo?",
|
||||
"role": "user",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
print("Step 4 done")
|
||||
|
||||
print("\n===Final messages===\n")
|
||||
|
||||
# 5. That should have caused spanish_handoff_message_filter to be called, which means the
|
||||
# output should be missing the first two messages, and have no tool calls.
|
||||
# Let's print the messages to see what happened
|
||||
for message in result.to_input_list():
|
||||
print(json.dumps(message, indent=2))
|
||||
# tool_calls = message.tool_calls if isinstance(message, AssistantMessage) else None
|
||||
|
||||
# print(f"{message.role}: {message.content}\n - Tool calls: {tool_calls or 'None'}")
|
||||
"""
|
||||
$python examples/handoffs/message_filter.py
|
||||
Step 1 done
|
||||
Step 2 done
|
||||
Step 3 done
|
||||
Step 4 done
|
||||
|
||||
===Final messages===
|
||||
|
||||
{
|
||||
"content": "Can you generate a random number between 0 and 100?",
|
||||
"role": "user"
|
||||
}
|
||||
{
|
||||
"id": "...",
|
||||
"content": [
|
||||
{
|
||||
"annotations": [],
|
||||
"text": "Sure! Here's a random number between 0 and 100: **42**.",
|
||||
"type": "output_text"
|
||||
}
|
||||
],
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"type": "message"
|
||||
}
|
||||
{
|
||||
"content": "I live in New York City. Whats the population of the city?",
|
||||
"role": "user"
|
||||
}
|
||||
{
|
||||
"id": "...",
|
||||
"content": [
|
||||
{
|
||||
"annotations": [],
|
||||
"text": "As of the most recent estimates, the population of New York City is approximately 8.6 million people. However, this number is constantly changing due to various factors such as migration and birth rates. For the latest and most accurate information, it's always a good idea to check the official data from sources like the U.S. Census Bureau.",
|
||||
"type": "output_text"
|
||||
}
|
||||
],
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"type": "message"
|
||||
}
|
||||
{
|
||||
"content": "Por favor habla en espa\u00f1ol. \u00bfCu\u00e1l es mi nombre y d\u00f3nde vivo?",
|
||||
"role": "user"
|
||||
}
|
||||
{
|
||||
"id": "...",
|
||||
"content": [
|
||||
{
|
||||
"annotations": [],
|
||||
"text": "No tengo acceso a esa informaci\u00f3n personal, solo s\u00e9 lo que me has contado: vives en Nueva York.",
|
||||
"type": "output_text"
|
||||
}
|
||||
],
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"type": "message"
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,187 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
|
||||
from agents import Agent, HandoffInputData, Runner, function_tool, handoff, trace
|
||||
from agents.extensions import handoff_filters
|
||||
from agents.models import is_gpt_5_default
|
||||
|
||||
|
||||
@function_tool
|
||||
def random_number_tool(max: int) -> int:
|
||||
"""Return a random integer between 0 and the given maximum."""
|
||||
return random.randint(0, max)
|
||||
|
||||
|
||||
def spanish_handoff_message_filter(handoff_message_data: HandoffInputData) -> HandoffInputData:
|
||||
if is_gpt_5_default():
|
||||
print("gpt-5 is enabled, so we're not filtering the input history")
|
||||
# when using gpt-5, removing some of the items could break things, so we do this filtering only for other models
|
||||
return HandoffInputData(
|
||||
input_history=handoff_message_data.input_history,
|
||||
pre_handoff_items=tuple(handoff_message_data.pre_handoff_items),
|
||||
new_items=tuple(handoff_message_data.new_items),
|
||||
)
|
||||
|
||||
# First, we'll remove any tool-related messages from the message history
|
||||
handoff_message_data = handoff_filters.remove_all_tools(handoff_message_data)
|
||||
|
||||
# Second, we'll also remove the first two items from the history, just for demonstration
|
||||
history = (
|
||||
tuple(handoff_message_data.input_history[2:])
|
||||
if isinstance(handoff_message_data.input_history, tuple)
|
||||
else handoff_message_data.input_history
|
||||
)
|
||||
|
||||
# or, you can use the HandoffInputData.clone(kwargs) method
|
||||
return HandoffInputData(
|
||||
input_history=history,
|
||||
pre_handoff_items=tuple(handoff_message_data.pre_handoff_items),
|
||||
new_items=tuple(handoff_message_data.new_items),
|
||||
)
|
||||
|
||||
|
||||
first_agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Be extremely concise.",
|
||||
tools=[random_number_tool],
|
||||
)
|
||||
|
||||
spanish_agent = Agent(
|
||||
name="Spanish Assistant",
|
||||
instructions="You only speak Spanish and are extremely concise.",
|
||||
handoff_description="A Spanish-speaking assistant.",
|
||||
)
|
||||
|
||||
second_agent = Agent(
|
||||
name="Assistant",
|
||||
instructions=(
|
||||
"Be a helpful assistant. If the user speaks Spanish, handoff to the Spanish assistant."
|
||||
),
|
||||
handoffs=[handoff(spanish_agent, input_filter=spanish_handoff_message_filter)],
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
# Trace the entire run as a single workflow
|
||||
with trace(workflow_name="Streaming message filter"):
|
||||
# 1. Send a regular message to the first agent
|
||||
result = await Runner.run(first_agent, input="Hi, my name is Sora.")
|
||||
|
||||
print("Step 1 done")
|
||||
|
||||
# 2. Ask it to generate a number
|
||||
result = await Runner.run(
|
||||
first_agent,
|
||||
input=result.to_input_list()
|
||||
+ [{"content": "Can you generate a random number between 0 and 100?", "role": "user"}],
|
||||
)
|
||||
|
||||
print("Step 2 done")
|
||||
|
||||
# 3. Call the second agent
|
||||
result = await Runner.run(
|
||||
second_agent,
|
||||
input=result.to_input_list()
|
||||
+ [
|
||||
{
|
||||
"content": "I live in New York City. Whats the population of the city?",
|
||||
"role": "user",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
print("Step 3 done")
|
||||
|
||||
# 4. Cause a handoff to occur
|
||||
stream_result = Runner.run_streamed(
|
||||
second_agent,
|
||||
input=result.to_input_list()
|
||||
+ [
|
||||
{
|
||||
"content": "Por favor habla en español. ¿Cuál es mi nombre y dónde vivo?",
|
||||
"role": "user",
|
||||
}
|
||||
],
|
||||
)
|
||||
async for _ in stream_result.stream_events():
|
||||
pass
|
||||
|
||||
print("Step 4 done")
|
||||
|
||||
print("\n===Final messages===\n")
|
||||
|
||||
# 5. That should have caused spanish_handoff_message_filter to be called, which means the
|
||||
# output should be missing the first two messages, and have no tool calls.
|
||||
# Let's print the messages to see what happened
|
||||
for item in stream_result.to_input_list():
|
||||
print(json.dumps(item, indent=2))
|
||||
"""
|
||||
$python examples/handoffs/message_filter_streaming.py
|
||||
Step 1 done
|
||||
Step 2 done
|
||||
Step 3 done
|
||||
Tu nombre y lugar de residencia no los tengo disponibles. Solo sé que mencionaste vivir en la ciudad de Nueva York.
|
||||
Step 4 done
|
||||
|
||||
===Final messages===
|
||||
|
||||
{
|
||||
"content": "Can you generate a random number between 0 and 100?",
|
||||
"role": "user"
|
||||
}
|
||||
{
|
||||
"id": "...",
|
||||
"content": [
|
||||
{
|
||||
"annotations": [],
|
||||
"text": "Sure! Here's a random number between 0 and 100: **37**.",
|
||||
"type": "output_text"
|
||||
}
|
||||
],
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"type": "message"
|
||||
}
|
||||
{
|
||||
"content": "I live in New York City. Whats the population of the city?",
|
||||
"role": "user"
|
||||
}
|
||||
{
|
||||
"id": "...",
|
||||
"content": [
|
||||
{
|
||||
"annotations": [],
|
||||
"text": "As of the latest estimates, New York City's population is approximately 8.5 million people. Would you like more information about the city?",
|
||||
"type": "output_text"
|
||||
}
|
||||
],
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"type": "message"
|
||||
}
|
||||
{
|
||||
"content": "Por favor habla en espa\u00f1ol. \u00bfCu\u00e1l es mi nombre y d\u00f3nde vivo?",
|
||||
"role": "user"
|
||||
}
|
||||
{
|
||||
"id": "...",
|
||||
"content": [
|
||||
{
|
||||
"annotations": [],
|
||||
"text": "No s\u00e9 tu nombre, pero me dijiste que vives en Nueva York.",
|
||||
"type": "output_text"
|
||||
}
|
||||
],
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"type": "message"
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,63 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from agents import Agent, HostedMCPTool, Runner, RunResult, RunResultStreaming
|
||||
|
||||
# import logging
|
||||
# logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
async def main(verbose: bool, stream: bool):
|
||||
# 1. Visit https://developers.google.com/oauthplayground/
|
||||
# 2. Input https://www.googleapis.com/auth/calendar.events as the required scope
|
||||
# 3. Grab the access token starting with "ya29."
|
||||
authorization = os.environ["GOOGLE_CALENDAR_AUTHORIZATION"]
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant that can help a user with their calendar.",
|
||||
tools=[
|
||||
HostedMCPTool(
|
||||
tool_config={
|
||||
"type": "mcp",
|
||||
"server_label": "google_calendar",
|
||||
# see https://platform.openai.com/docs/guides/tools-connectors-mcp#connectors
|
||||
"connector_id": "connector_googlecalendar",
|
||||
"authorization": authorization,
|
||||
"require_approval": "never",
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
run_result: RunResult | RunResultStreaming
|
||||
if stream:
|
||||
run_result = Runner.run_streamed(agent, f"What is my schedule for {today}?")
|
||||
async for event in run_result.stream_events():
|
||||
if event.type == "raw_response_event":
|
||||
if event.data.type.startswith("response.output_item"):
|
||||
print(json.dumps(event.data.to_dict(), indent=2))
|
||||
if event.data.type.startswith("response.mcp"):
|
||||
print(json.dumps(event.data.to_dict(), indent=2))
|
||||
if event.data.type == "response.output_text.delta":
|
||||
print(event.data.delta, end="", flush=True)
|
||||
print()
|
||||
else:
|
||||
run_result = await Runner.run(agent, f"What is my schedule for {today}?")
|
||||
print(run_result.final_output)
|
||||
|
||||
if verbose:
|
||||
for item in run_result.new_items:
|
||||
print(item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--verbose", action="store_true", default=False)
|
||||
parser.add_argument("--stream", action="store_true", default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(main(args.verbose, args.stream))
|
||||
@@ -0,0 +1,133 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Literal
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
HostedMCPTool,
|
||||
ModelSettings,
|
||||
RunConfig,
|
||||
Runner,
|
||||
RunResult,
|
||||
RunResultStreaming,
|
||||
)
|
||||
from agents.model_settings import MCPToolChoice
|
||||
from examples.auto_mode import confirm_with_fallback
|
||||
|
||||
|
||||
def prompt_for_interruption(
|
||||
tool_name: str | None, arguments: str | dict[str, object] | None
|
||||
) -> bool:
|
||||
params: object = {}
|
||||
if arguments:
|
||||
if isinstance(arguments, str):
|
||||
try:
|
||||
params = json.loads(arguments)
|
||||
except json.JSONDecodeError:
|
||||
params = arguments
|
||||
else:
|
||||
params = arguments
|
||||
try:
|
||||
return confirm_with_fallback(
|
||||
f"Approve running tool (mcp: {tool_name or 'unknown'}, params: {json.dumps(params)})? (y/n) ",
|
||||
default=True,
|
||||
)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return False
|
||||
|
||||
|
||||
async def _drain_stream(
|
||||
result: RunResultStreaming,
|
||||
verbose: bool,
|
||||
) -> RunResultStreaming:
|
||||
async for event in result.stream_events():
|
||||
if verbose:
|
||||
print(event)
|
||||
elif event.type == "raw_response_event" and event.data.type == "response.output_text.delta":
|
||||
print(event.data.delta, end="", flush=True)
|
||||
if not verbose:
|
||||
print()
|
||||
return result
|
||||
|
||||
|
||||
async def main(verbose: bool, stream: bool) -> None:
|
||||
require_approval: Literal["always"] = "always"
|
||||
# Use the concise question tool first, then allow the model to answer after approval instead of
|
||||
# forcing the same tool again.
|
||||
agent = Agent(
|
||||
name="MCP Assistant",
|
||||
instructions=(
|
||||
"You must always use the MCP tools to answer questions. "
|
||||
"Use the DeepWiki hosted MCP server to answer questions and do not ask the user for "
|
||||
"additional configuration."
|
||||
),
|
||||
model_settings=ModelSettings(
|
||||
tool_choice=MCPToolChoice(server_label="deepwiki", name="ask_question")
|
||||
),
|
||||
tools=[
|
||||
HostedMCPTool(
|
||||
tool_config={
|
||||
"type": "mcp",
|
||||
"server_label": "deepwiki",
|
||||
"server_url": "https://mcp.deepwiki.com/mcp",
|
||||
"require_approval": require_approval,
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
resume_config = RunConfig(model_settings=ModelSettings(tool_choice="auto"))
|
||||
|
||||
question = "Which language is the repository openai/codex written in?"
|
||||
|
||||
run_result: RunResult | RunResultStreaming
|
||||
if stream:
|
||||
stream_result = Runner.run_streamed(agent, question, max_turns=100)
|
||||
stream_result = await _drain_stream(stream_result, verbose)
|
||||
while stream_result.interruptions:
|
||||
state = stream_result.to_state()
|
||||
for interruption in stream_result.interruptions:
|
||||
approved = prompt_for_interruption(interruption.name, interruption.arguments)
|
||||
if approved:
|
||||
state.approve(interruption)
|
||||
else:
|
||||
state.reject(interruption)
|
||||
stream_result = Runner.run_streamed(
|
||||
agent,
|
||||
state,
|
||||
max_turns=100,
|
||||
run_config=resume_config,
|
||||
)
|
||||
stream_result = await _drain_stream(stream_result, verbose)
|
||||
print(f"Done streaming; final result: {stream_result.final_output}")
|
||||
run_result = stream_result
|
||||
else:
|
||||
run_result = await Runner.run(agent, question, max_turns=100)
|
||||
while run_result.interruptions:
|
||||
state = run_result.to_state()
|
||||
for interruption in run_result.interruptions:
|
||||
approved = prompt_for_interruption(interruption.name, interruption.arguments)
|
||||
if approved:
|
||||
state.approve(interruption)
|
||||
else:
|
||||
state.reject(interruption)
|
||||
run_result = await Runner.run(
|
||||
agent,
|
||||
state,
|
||||
max_turns=100,
|
||||
run_config=resume_config,
|
||||
)
|
||||
print(run_result.final_output)
|
||||
|
||||
if verbose:
|
||||
for item in run_result.new_items:
|
||||
print(item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--verbose", action="store_true", default=False)
|
||||
parser.add_argument("--stream", action="store_true", default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(main(args.verbose, args.stream))
|
||||
@@ -0,0 +1,86 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Literal
|
||||
|
||||
from agents import (
|
||||
Agent,
|
||||
HostedMCPTool,
|
||||
MCPToolApprovalFunctionResult,
|
||||
MCPToolApprovalRequest,
|
||||
Runner,
|
||||
RunResult,
|
||||
RunResultStreaming,
|
||||
)
|
||||
from examples.auto_mode import confirm_with_fallback
|
||||
|
||||
|
||||
def prompt_approval(request: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult:
|
||||
params: object = request.data.arguments or {}
|
||||
approved = confirm_with_fallback(
|
||||
f"Approve running tool (mcp: {request.data.name}, params: {json.dumps(params)})? (y/n) ",
|
||||
default=True,
|
||||
)
|
||||
result: MCPToolApprovalFunctionResult = {"approve": approved}
|
||||
if not approved:
|
||||
result["reason"] = "User denied"
|
||||
return result
|
||||
|
||||
|
||||
async def main(verbose: bool, stream: bool) -> None:
|
||||
require_approval: Literal["always"] = "always"
|
||||
agent = Agent(
|
||||
name="MCP Assistant",
|
||||
instructions=(
|
||||
"You must always use the MCP tools to answer questions. "
|
||||
"Use the DeepWiki hosted MCP server to answer questions and do not ask the user for "
|
||||
"additional configuration."
|
||||
),
|
||||
tools=[
|
||||
HostedMCPTool(
|
||||
tool_config={
|
||||
"type": "mcp",
|
||||
"server_label": "deepwiki",
|
||||
"server_url": "https://mcp.deepwiki.com/mcp",
|
||||
"allowed_tools": ["ask_question"],
|
||||
"require_approval": require_approval,
|
||||
},
|
||||
on_approval_request=prompt_approval,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
question = "Which language is the repository openai/codex written in?"
|
||||
|
||||
run_result: RunResult | RunResultStreaming
|
||||
if stream:
|
||||
run_result = Runner.run_streamed(agent, question)
|
||||
async for event in run_result.stream_events():
|
||||
if verbose:
|
||||
print(event)
|
||||
elif (
|
||||
event.type == "raw_response_event"
|
||||
and event.data.type == "response.output_text.delta"
|
||||
):
|
||||
print(event.data.delta, end="", flush=True)
|
||||
if not verbose:
|
||||
print()
|
||||
print(f"Done streaming; final result: {run_result.final_output}")
|
||||
else:
|
||||
run_result = await Runner.run(agent, question)
|
||||
while run_result.interruptions:
|
||||
run_result = await Runner.run(agent, run_result.to_state())
|
||||
print(run_result.final_output)
|
||||
|
||||
if verbose:
|
||||
for item in run_result.new_items:
|
||||
print(item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--verbose", action="store_true", default=False)
|
||||
parser.add_argument("--stream", action="store_true", default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(main(args.verbose, args.stream))
|
||||
@@ -0,0 +1,56 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, HostedMCPTool, ModelSettings, Runner, RunResult, RunResultStreaming
|
||||
|
||||
"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with
|
||||
approvals not required for any tools. You should only use this for trusted MCP servers."""
|
||||
|
||||
|
||||
async def main(verbose: bool, stream: bool, repo: str):
|
||||
question = f"Which language is the repository {repo} written in?"
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions=f"You can use the DeepWiki hosted MCP server to inspect {repo}.",
|
||||
model_settings=ModelSettings(tool_choice="required"),
|
||||
tools=[
|
||||
HostedMCPTool(
|
||||
tool_config={
|
||||
"type": "mcp",
|
||||
"server_label": "deepwiki",
|
||||
"server_url": "https://mcp.deepwiki.com/mcp",
|
||||
"require_approval": "never",
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
run_result: RunResult | RunResultStreaming
|
||||
if stream:
|
||||
run_result = Runner.run_streamed(agent, question)
|
||||
async for event in run_result.stream_events():
|
||||
if event.type == "run_item_stream_event":
|
||||
print(f"Got event of type {event.item.__class__.__name__}")
|
||||
print(f"Done streaming; final result: {run_result.final_output}")
|
||||
else:
|
||||
run_result = await Runner.run(agent, question)
|
||||
print(run_result.final_output)
|
||||
# The repository is primarily written in Python...
|
||||
|
||||
if verbose:
|
||||
for item in run_result.new_items:
|
||||
print(item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--verbose", action="store_true", default=False)
|
||||
parser.add_argument("--stream", action="store_true", default=False)
|
||||
parser.add_argument(
|
||||
"--repo",
|
||||
default="https://github.com/openai/openai-agents-python",
|
||||
help="Repository URL or slug that the DeepWiki MCP server should use.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(main(args.verbose, args.stream, args.repo))
|
||||
@@ -0,0 +1,26 @@
|
||||
# MCP Filesystem Example
|
||||
|
||||
This example uses the [filesystem MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem), running locally via `npx`.
|
||||
|
||||
Run it via:
|
||||
|
||||
```
|
||||
uv run python examples/mcp/filesystem_example/main.py
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
The example uses the `MCPServerStdio` class from `agents.mcp`, with the command:
|
||||
|
||||
```bash
|
||||
npx -y "@modelcontextprotocol/server-filesystem" <samples_directory>
|
||||
```
|
||||
|
||||
It's only given access to the `sample_files` directory adjacent to the example, which contains some sample data.
|
||||
|
||||
Under the hood:
|
||||
|
||||
1. The server is spun up in a subprocess, and exposes a bunch of tools like `list_directory()`, `read_file()`, etc.
|
||||
2. We add the server instance to the Agent via `mcp_agents`.
|
||||
3. Each time the agent runs, we call out to the MCP server to fetch the list of tools via `server.list_tools()`.
|
||||
4. If the LLM chooses to use an MCP tool, we call the MCP server to run the tool via `server.run_tool()`.
|
||||
@@ -0,0 +1,57 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServer, MCPServerStdio
|
||||
|
||||
|
||||
async def run(mcp_server: MCPServer):
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Use the tools to read the filesystem and answer questions based on those files.",
|
||||
mcp_servers=[mcp_server],
|
||||
)
|
||||
|
||||
# List the files it can read
|
||||
message = "Read the files and list them."
|
||||
print(f"Running: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
# Ask about books
|
||||
message = "Read favorite_books.txt and tell me my #1 favorite book."
|
||||
print(f"\n\nRunning: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
# Ask a question that reads then reasons.
|
||||
message = "Read favorite_songs.txt and suggest one new song that I might like."
|
||||
print(f"\n\nRunning: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
async def main():
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
samples_dir = os.path.join(current_dir, "sample_files")
|
||||
|
||||
async with MCPServerStdio(
|
||||
name="Filesystem Server, via npx",
|
||||
params={
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", samples_dir],
|
||||
},
|
||||
) as server:
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="MCP Filesystem Example", trace_id=trace_id):
|
||||
print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")
|
||||
await run(server)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Let's make sure the user has npx installed
|
||||
if not shutil.which("npx"):
|
||||
raise RuntimeError("npx is not installed. Please install it with `npm install -g npx`.")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,20 @@
|
||||
1. To Kill a Mockingbird – Harper Lee
|
||||
2. Pride and Prejudice – Jane Austen
|
||||
3. 1984 – George Orwell
|
||||
4. The Hobbit – J.R.R. Tolkien
|
||||
5. Harry Potter and the Sorcerer’s Stone – J.K. Rowling
|
||||
6. The Great Gatsby – F. Scott Fitzgerald
|
||||
7. Charlotte’s Web – E.B. White
|
||||
8. Anne of Green Gables – Lucy Maud Montgomery
|
||||
9. The Alchemist – Paulo Coelho
|
||||
10. Little Women – Louisa May Alcott
|
||||
11. The Catcher in the Rye – J.D. Salinger
|
||||
12. Animal Farm – George Orwell
|
||||
13. The Chronicles of Narnia: The Lion, the Witch, and the Wardrobe – C.S. Lewis
|
||||
14. The Book Thief – Markus Zusak
|
||||
15. A Wrinkle in Time – Madeleine L’Engle
|
||||
16. The Secret Garden – Frances Hodgson Burnett
|
||||
17. Moby-Dick – Herman Melville
|
||||
18. Fahrenheit 451 – Ray Bradbury
|
||||
19. Jane Eyre – Charlotte Brontë
|
||||
20. The Little Prince – Antoine de Saint-Exupéry
|
||||
@@ -0,0 +1,4 @@
|
||||
- In the summer, I love visiting London.
|
||||
- In the winter, Tokyo is great.
|
||||
- In the spring, San Francisco.
|
||||
- In the fall, New York is the best.
|
||||
@@ -0,0 +1,10 @@
|
||||
1. "Here Comes the Sun" – The Beatles
|
||||
2. "Imagine" – John Lennon
|
||||
3. "Bohemian Rhapsody" – Queen
|
||||
4. "Shake It Off" – Taylor Swift
|
||||
5. "Billie Jean" – Michael Jackson
|
||||
6. "Uptown Funk" – Mark Ronson ft. Bruno Mars
|
||||
7. "Don’t Stop Believin’" – Journey
|
||||
8. "Dancing Queen" – ABBA
|
||||
9. "Happy" – Pharrell Williams
|
||||
10. "Wonderwall" – Oasis
|
||||
@@ -0,0 +1,20 @@
|
||||
# MCP get_all_mcp_tools Example
|
||||
|
||||
Python port of the JS `examples/mcp/get-all-mcp-tools-example.ts`. It demonstrates:
|
||||
|
||||
- Spinning up a local filesystem MCP server via `npx`.
|
||||
- Prefetching all MCP tools with `MCPUtil.get_all_function_tools`.
|
||||
- Building an agent that uses those prefetched tools instead of `mcp_servers`.
|
||||
- Applying a static tool filter and refetching tools.
|
||||
- Enabling `require_approval="always"` on the server and auto-approving interruptions in code to exercise the HITL path.
|
||||
|
||||
Run it with:
|
||||
|
||||
```bash
|
||||
uv run python examples/mcp/get_all_mcp_tools_example/main.py
|
||||
```
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- `npx` available on your PATH.
|
||||
- `OPENAI_API_KEY` set for the model calls.
|
||||
@@ -0,0 +1,137 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServer, MCPServerStdio
|
||||
from agents.mcp.util import MCPUtil, create_static_tool_filter
|
||||
from agents.run_context import RunContextWrapper
|
||||
from examples.auto_mode import confirm_with_fallback, is_auto_mode
|
||||
|
||||
|
||||
async def list_tools(server: MCPServer, *, convert_to_strict: bool) -> list[Any]:
|
||||
"""Fetch all MCP tools from the server."""
|
||||
|
||||
run_context: RunContextWrapper[dict[str, str]] = RunContextWrapper(context={})
|
||||
agent = Agent(name="ToolFetcher", instructions="Prefetch MCP tools.", mcp_servers=[server])
|
||||
|
||||
return await MCPUtil.get_all_function_tools(
|
||||
[server],
|
||||
convert_schemas_to_strict=convert_to_strict,
|
||||
run_context=run_context,
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
|
||||
def prompt_user_approval(interruption_name: str) -> bool:
|
||||
"""Ask the user to approve a tool call and return the decision."""
|
||||
if is_auto_mode():
|
||||
return confirm_with_fallback(
|
||||
f"Approve tool call '{interruption_name}'? (y/n): ",
|
||||
default=True,
|
||||
)
|
||||
while True:
|
||||
user_input = input(f"Approve tool call '{interruption_name}'? (y/n): ").strip().lower()
|
||||
if user_input == "y":
|
||||
return True
|
||||
if user_input == "n":
|
||||
return False
|
||||
print("Please enter 'y' or 'n'.")
|
||||
|
||||
|
||||
async def resolve_interruptions(agent: Agent, result: Any) -> Any:
|
||||
"""Prompt for approvals until no interruptions remain."""
|
||||
current_result = result
|
||||
while current_result.interruptions:
|
||||
state = current_result.to_state()
|
||||
# Human in the loop: prompt for approval on each tool call.
|
||||
for interruption in current_result.interruptions:
|
||||
if prompt_user_approval(interruption.name):
|
||||
print(f"Approving a tool call... (name: {interruption.name})")
|
||||
state.approve(interruption)
|
||||
else:
|
||||
print(f"Rejecting a tool call... (name: {interruption.name})")
|
||||
state.reject(interruption)
|
||||
current_result = await Runner.run(agent, state)
|
||||
return current_result
|
||||
|
||||
|
||||
async def main():
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
samples_dir = os.path.join(current_dir, "sample_files")
|
||||
blocked_path = os.path.join(samples_dir, "test.txt")
|
||||
|
||||
async with MCPServerStdio(
|
||||
name="Filesystem Server",
|
||||
params={
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", samples_dir],
|
||||
"cwd": samples_dir,
|
||||
},
|
||||
require_approval={"always": {"tool_names": ["read_text_file"]}},
|
||||
) as server:
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="MCP get_all_mcp_tools Example", trace_id=trace_id):
|
||||
print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")
|
||||
|
||||
print("=== Fetching all tools with strict schemas ===")
|
||||
all_tools = await list_tools(server, convert_to_strict=True)
|
||||
print(f"Found {len(all_tools)} tool(s):")
|
||||
for tool in all_tools:
|
||||
description = getattr(tool, "description", "") or ""
|
||||
print(f"- {tool.name}: {description}")
|
||||
|
||||
# Build an agent that uses the prefetched tools instead of mcp_servers.
|
||||
prefetched_agent = Agent(
|
||||
name="Prefetched MCP Assistant",
|
||||
instructions=(
|
||||
"Use the prefetched tools to help with file questions. "
|
||||
"When using path arguments, prefer absolute paths in the allowed directory."
|
||||
),
|
||||
tools=all_tools,
|
||||
)
|
||||
message = (
|
||||
f"List files in this allowed directory: {samples_dir}. "
|
||||
"Then read one of those files."
|
||||
)
|
||||
print(f"\nRunning: {message}\n")
|
||||
result = await Runner.run(prefetched_agent, message)
|
||||
result = await resolve_interruptions(prefetched_agent, result)
|
||||
print(result.final_output)
|
||||
|
||||
# Apply a static tool filter and refetch tools.
|
||||
server.tool_filter = create_static_tool_filter(
|
||||
allowed_tool_names=["read_file", "list_directory"]
|
||||
)
|
||||
filtered_tools = await list_tools(server, convert_to_strict=False)
|
||||
|
||||
print("\n=== After applying tool filter ===")
|
||||
print(f"Found {len(filtered_tools)} tool(s):")
|
||||
for tool in filtered_tools:
|
||||
print(f"- {tool.name}")
|
||||
|
||||
filtered_agent = Agent(
|
||||
name="Filtered MCP Assistant",
|
||||
instructions=(
|
||||
"Use the filtered tools to respond. "
|
||||
"If a request requires a missing tool, explain that the capability is not "
|
||||
"available."
|
||||
),
|
||||
tools=filtered_tools,
|
||||
)
|
||||
blocked_message = (
|
||||
f'Create a file named "{blocked_path}" with the text "hello". '
|
||||
"If the available tools cannot create files, explain that clearly."
|
||||
)
|
||||
print(f"\nRunning: {blocked_message}\n")
|
||||
filtered_result = await Runner.run(filtered_agent, blocked_message)
|
||||
filtered_result = await resolve_interruptions(filtered_agent, filtered_result)
|
||||
print(filtered_result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not shutil.which("npx"):
|
||||
raise RuntimeError("npx is required. Install it with `npm install -g npx`.")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,20 @@
|
||||
1. To Kill a Mockingbird – Harper Lee
|
||||
2. Pride and Prejudice – Jane Austen
|
||||
3. 1984 – George Orwell
|
||||
4. The Hobbit – J.R.R. Tolkien
|
||||
5. Harry Potter and the Sorcerer’s Stone – J.K. Rowling
|
||||
6. The Great Gatsby – F. Scott Fitzgerald
|
||||
7. Charlotte’s Web – E.B. White
|
||||
8. Anne of Green Gables – Lucy Maud Montgomery
|
||||
9. The Alchemist – Paulo Coelho
|
||||
10. Little Women – Louisa May Alcott
|
||||
11. The Catcher in the Rye – J.D. Salinger
|
||||
12. Animal Farm – George Orwell
|
||||
13. The Chronicles of Narnia: The Lion, the Witch, and the Wardrobe – C.S. Lewis
|
||||
14. The Book Thief – Markus Zusak
|
||||
15. A Wrinkle in Time – Madeleine L’Engle
|
||||
16. The Secret Garden – Frances Hodgson Burnett
|
||||
17. Moby-Dick – Herman Melville
|
||||
18. Fahrenheit 451 – Ray Bradbury
|
||||
19. Jane Eyre – Charlotte Brontë
|
||||
20. The Little Prince – Antoine de Saint-Exupéry
|
||||
@@ -0,0 +1,10 @@
|
||||
1. "Here Comes the Sun" – The Beatles
|
||||
2. "Imagine" – John Lennon
|
||||
3. "Bohemian Rhapsody" – Queen
|
||||
4. "Shake It Off" – Taylor Swift
|
||||
5. "Billie Jean" – Michael Jackson
|
||||
6. "Uptown Funk" – Mark Ronson ft. Bruno Mars
|
||||
7. "Don’t Stop Believin’" – Journey
|
||||
8. "Dancing Queen" – ABBA
|
||||
9. "Happy" – Pharrell Williams
|
||||
10. "Wonderwall" – Oasis
|
||||
@@ -0,0 +1,26 @@
|
||||
# MCP Git Example
|
||||
|
||||
This example uses the [git MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/git), running locally via `uvx`.
|
||||
|
||||
Run it via:
|
||||
|
||||
```
|
||||
uv run python examples/mcp/git_example/main.py
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
The example uses the `MCPServerStdio` class from `agents.mcp`, with the command:
|
||||
|
||||
```bash
|
||||
uvx mcp-server-git
|
||||
```
|
||||
|
||||
Prior to running the agent, the user is prompted to provide a local directory path to their git repo. Using that, the Agent can invoke Git MCP tools like `git_log` to inspect the git commit log.
|
||||
|
||||
Under the hood:
|
||||
|
||||
1. The server is spun up in a subprocess, and exposes a bunch of tools like `git_log()`
|
||||
2. We add the server instance to the Agent via `mcp_agents`.
|
||||
3. Each time the agent runs, we call out to the MCP server to fetch the list of tools via `server.list_tools()`. The result is cached.
|
||||
4. If the LLM chooses to use an MCP tool, we call the MCP server to run the tool via `server.run_tool()`.
|
||||
@@ -0,0 +1,48 @@
|
||||
import asyncio
|
||||
import shutil
|
||||
|
||||
from agents import Agent, Runner, trace
|
||||
from agents.mcp import MCPServer, MCPServerStdio
|
||||
from examples.auto_mode import input_with_fallback
|
||||
|
||||
|
||||
async def run(mcp_server: MCPServer, directory_path: str):
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions=f"Answer questions about the git repository at {directory_path}, use that for repo_path",
|
||||
mcp_servers=[mcp_server],
|
||||
)
|
||||
|
||||
message = "Who's the most frequent contributor?"
|
||||
print("\n" + "-" * 40)
|
||||
print(f"Running: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
message = "Summarize the last change in the repository."
|
||||
print("\n" + "-" * 40)
|
||||
print(f"Running: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
async def main():
|
||||
# Ask the user for the directory path
|
||||
directory_path = input_with_fallback(
|
||||
"Please enter the path to the git repository: ",
|
||||
".",
|
||||
)
|
||||
|
||||
async with MCPServerStdio(
|
||||
cache_tools_list=True, # Cache the tools list, for demonstration
|
||||
params={"command": "uvx", "args": ["mcp-server-git"]},
|
||||
) as server:
|
||||
with trace(workflow_name="MCP Git Example"):
|
||||
await run(server, directory_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not shutil.which("uvx"):
|
||||
raise RuntimeError("uvx is not installed. Please install it with `pip install uvx`.")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,78 @@
|
||||
# MCP Manager Example (FastAPI)
|
||||
|
||||
This example shows how to use `MCPServerManager` to keep MCP server lifecycle management in a single task inside a FastAPI app with the Streamable HTTP transport.
|
||||
|
||||
## Run the MCP server (Streamable HTTP)
|
||||
|
||||
```
|
||||
uv run python examples/mcp/manager_example/mcp_server.py
|
||||
```
|
||||
|
||||
The server listens at `http://localhost:8000/mcp` by default.
|
||||
|
||||
You can override the host/port with:
|
||||
|
||||
```
|
||||
export STREAMABLE_HTTP_HOST=127.0.0.1
|
||||
export STREAMABLE_HTTP_PORT=8000
|
||||
```
|
||||
|
||||
This example also configures an inactive MCP server at `http://localhost:8001/mcp` to demonstrate how the manager drops failed
|
||||
servers. You can override it with:
|
||||
|
||||
```
|
||||
export INACTIVE_MCP_SERVER_URL=http://localhost:8001/mcp
|
||||
```
|
||||
|
||||
## Run the FastAPI app
|
||||
|
||||
```
|
||||
uv run python examples/mcp/manager_example/app.py
|
||||
```
|
||||
|
||||
The app listens at `http://127.0.0.1:9001`.
|
||||
|
||||
## Run the smoke test
|
||||
|
||||
To verify the MCP manager and app integration without calling a model:
|
||||
|
||||
```
|
||||
uv run python -m examples.mcp.manager_example.smoke_test
|
||||
```
|
||||
|
||||
The smoke test starts the local MCP server on a temporary port, points both app MCP server settings at that server, and checks `/health`, `/tools`, and `/add`.
|
||||
|
||||
## Toggle MCP manager usage
|
||||
|
||||
By default, the app uses `MCPServerManager`. To disable it:
|
||||
|
||||
```
|
||||
export USE_MCP_MANAGER=0
|
||||
```
|
||||
|
||||
## Try the endpoints
|
||||
|
||||
```
|
||||
curl http://127.0.0.1:9001/health
|
||||
curl http://127.0.0.1:9001/tools
|
||||
curl -X POST http://127.0.0.1:9001/add \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"a": 2, "b": 3}'
|
||||
```
|
||||
|
||||
Reconnect failed MCP servers (manager must be enabled):
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:9001/reconnect \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"failed_only": true}'
|
||||
```
|
||||
|
||||
To use `/run`, set `OPENAI_API_KEY`:
|
||||
|
||||
```
|
||||
export OPENAI_API_KEY=...
|
||||
curl -X POST http://127.0.0.1:9001/run \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"input": "Add 4 and 9."}'
|
||||
```
|
||||
@@ -0,0 +1,130 @@
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agents import Agent, Runner
|
||||
from agents.mcp import MCPServer, MCPServerManager, MCPServerStreamableHttp
|
||||
from agents.model_settings import ModelSettings
|
||||
|
||||
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/mcp")
|
||||
INACTIVE_MCP_SERVER_URL = os.getenv("INACTIVE_MCP_SERVER_URL", "http://localhost:8001/mcp")
|
||||
APP_HOST = "127.0.0.1"
|
||||
APP_PORT = 9001
|
||||
USE_MCP_MANAGER = os.getenv("USE_MCP_MANAGER", "1") != "0"
|
||||
|
||||
|
||||
class AddRequest(BaseModel):
|
||||
a: int
|
||||
b: int
|
||||
|
||||
|
||||
class RunRequest(BaseModel):
|
||||
input: str
|
||||
|
||||
|
||||
class ReconnectRequest(BaseModel):
|
||||
failed_only: bool = True
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
server = MCPServerStreamableHttp({"url": MCP_SERVER_URL})
|
||||
inactive_server = MCPServerStreamableHttp({"url": INACTIVE_MCP_SERVER_URL})
|
||||
servers = [server, inactive_server]
|
||||
if USE_MCP_MANAGER:
|
||||
async with MCPServerManager(
|
||||
servers=servers,
|
||||
connect_in_parallel=True,
|
||||
) as manager:
|
||||
app.state.mcp_manager = manager
|
||||
app.state.mcp_servers = servers
|
||||
yield
|
||||
return
|
||||
|
||||
await server.connect()
|
||||
app.state.mcp_servers = servers
|
||||
app.state.active_servers = [server]
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await server.cleanup()
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, object]:
|
||||
if USE_MCP_MANAGER:
|
||||
manager: MCPServerManager = app.state.mcp_manager
|
||||
return {
|
||||
"connected_servers": [server.name for server in manager.active_servers],
|
||||
"failed_servers": [server.name for server in manager.failed_servers],
|
||||
}
|
||||
|
||||
active_servers = _get_active_servers()
|
||||
return {
|
||||
"connected_servers": [server.name for server in active_servers],
|
||||
"failed_servers": [],
|
||||
}
|
||||
|
||||
|
||||
@app.get("/tools")
|
||||
async def list_tools() -> dict[str, object]:
|
||||
active_servers = _get_active_servers()
|
||||
if not active_servers:
|
||||
return {"tools": []}
|
||||
tools = await active_servers[0].list_tools()
|
||||
return {"tools": [tool.name for tool in tools]}
|
||||
|
||||
|
||||
@app.post("/add")
|
||||
async def add(req: AddRequest) -> dict[str, object]:
|
||||
active_servers = _get_active_servers()
|
||||
if not active_servers:
|
||||
raise HTTPException(status_code=503, detail="No MCP servers available")
|
||||
result = await active_servers[0].call_tool("add", {"a": req.a, "b": req.b})
|
||||
return {"result": result.model_dump(mode="json")}
|
||||
|
||||
|
||||
@app.post("/run")
|
||||
async def run_agent(req: RunRequest) -> dict[str, object]:
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
raise HTTPException(status_code=400, detail="OPENAI_API_KEY is required")
|
||||
|
||||
servers = _get_active_servers()
|
||||
if not servers:
|
||||
raise HTTPException(status_code=503, detail="No MCP servers available")
|
||||
|
||||
agent = Agent(
|
||||
name="FastAPI Agent",
|
||||
instructions="Use the MCP tools when needed.",
|
||||
mcp_servers=servers,
|
||||
model_settings=ModelSettings(tool_choice="auto"),
|
||||
)
|
||||
result = await Runner.run(starting_agent=agent, input=req.input)
|
||||
return {"output": result.final_output}
|
||||
|
||||
|
||||
@app.post("/reconnect")
|
||||
async def reconnect(req: ReconnectRequest) -> dict[str, object]:
|
||||
if not USE_MCP_MANAGER:
|
||||
raise HTTPException(status_code=400, detail="MCPServerManager is disabled")
|
||||
manager: MCPServerManager = app.state.mcp_manager
|
||||
servers = await manager.reconnect(failed_only=req.failed_only)
|
||||
return {"connected_servers": [server.name for server in servers]}
|
||||
|
||||
|
||||
def _get_active_servers() -> list[MCPServer]:
|
||||
if USE_MCP_MANAGER:
|
||||
manager: MCPServerManager = app.state.mcp_manager
|
||||
return list(manager.active_servers)
|
||||
return list(app.state.active_servers)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host=APP_HOST, port=APP_PORT)
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
STREAMABLE_HTTP_HOST = os.getenv("STREAMABLE_HTTP_HOST", "127.0.0.1")
|
||||
STREAMABLE_HTTP_PORT = int(os.getenv("STREAMABLE_HTTP_PORT", "8000"))
|
||||
|
||||
mcp = FastMCP(
|
||||
"FastAPI Example Server",
|
||||
host=STREAMABLE_HTTP_HOST,
|
||||
port=STREAMABLE_HTTP_PORT,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def echo(message: str) -> str:
|
||||
return f"echo: {message}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http")
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Smoke test for the MCP manager example app.
|
||||
|
||||
This script starts the sibling Streamable HTTP MCP server on a temporary local
|
||||
port, loads the app with matching environment variables, and verifies the
|
||||
manager-backed endpoints without calling a model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT_WAIT_SECONDS = 10.0
|
||||
|
||||
|
||||
def _free_port() -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind((HOST, 0))
|
||||
return int(sock.getsockname()[1])
|
||||
|
||||
|
||||
def _wait_for_port(process: subprocess.Popen[str], port: int) -> None:
|
||||
deadline = time.monotonic() + PORT_WAIT_SECONDS
|
||||
while time.monotonic() < deadline:
|
||||
if process.poll() is not None:
|
||||
output, _ = process.communicate(timeout=1)
|
||||
raise RuntimeError(f"MCP server exited before it was ready:\n{output}")
|
||||
try:
|
||||
with socket.create_connection((HOST, port), timeout=0.2):
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
raise RuntimeError(f"MCP server did not listen on {HOST}:{port}.")
|
||||
|
||||
|
||||
def _stop_process(process: subprocess.Popen[str]) -> str:
|
||||
if process.poll() is not None:
|
||||
output, _ = process.communicate(timeout=1)
|
||||
return output
|
||||
|
||||
process.terminate()
|
||||
try:
|
||||
output, _ = process.communicate(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
output, _ = process.communicate(timeout=5)
|
||||
return output
|
||||
|
||||
|
||||
def _start_mcp_server(port: int) -> subprocess.Popen[str]:
|
||||
env = {
|
||||
**os.environ,
|
||||
"STREAMABLE_HTTP_HOST": HOST,
|
||||
"STREAMABLE_HTTP_PORT": str(port),
|
||||
}
|
||||
return subprocess.Popen(
|
||||
[sys.executable, "-u", "-m", "examples.mcp.manager_example.mcp_server"],
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def _load_app_module(mcp_port: int) -> Any:
|
||||
mcp_server_url = f"http://{HOST}:{mcp_port}/mcp"
|
||||
os.environ["MCP_SERVER_URL"] = mcp_server_url
|
||||
# Point both configured MCP servers at the same temporary server so this
|
||||
# smoke test stays on the clean app integration path.
|
||||
os.environ["INACTIVE_MCP_SERVER_URL"] = mcp_server_url
|
||||
os.environ["USE_MCP_MANAGER"] = "1"
|
||||
|
||||
module_name = "examples.mcp.manager_example.app"
|
||||
if module_name in sys.modules:
|
||||
module = importlib.reload(sys.modules[module_name])
|
||||
else:
|
||||
module = importlib.import_module(module_name)
|
||||
return cast(Any, module)
|
||||
|
||||
|
||||
def _require(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
async def _exercise_app(mcp_port: int) -> None:
|
||||
app_module = _load_app_module(mcp_port)
|
||||
app = app_module.app
|
||||
expected_server_url = f"http://{HOST}:{mcp_port}/mcp"
|
||||
|
||||
async with app.router.lifespan_context(app):
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
health_response = await client.get("/health")
|
||||
health_response.raise_for_status()
|
||||
health = health_response.json()
|
||||
_require(
|
||||
any(expected_server_url in name for name in health["connected_servers"]),
|
||||
f"Expected connected MCP server in health response: {health}",
|
||||
)
|
||||
_require(
|
||||
health["failed_servers"] == [],
|
||||
f"Expected no failed MCP servers in health response: {health}",
|
||||
)
|
||||
|
||||
tools_response = await client.get("/tools")
|
||||
tools_response.raise_for_status()
|
||||
tools = tools_response.json()["tools"]
|
||||
_require({"add", "echo"} <= set(tools), f"Expected add and echo tools: {tools}")
|
||||
|
||||
add_response = await client.post("/add", json={"a": 2, "b": 3})
|
||||
add_response.raise_for_status()
|
||||
add_result = add_response.json()["result"]
|
||||
texts = [
|
||||
item.get("text")
|
||||
for item in add_result.get("content", [])
|
||||
if item.get("type") == "text"
|
||||
]
|
||||
_require("5" in texts, f"Expected add tool result to include 5: {add_result}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
mcp_port = _free_port()
|
||||
process = _start_mcp_server(mcp_port)
|
||||
try:
|
||||
_wait_for_port(process, mcp_port)
|
||||
await _exercise_app(mcp_port)
|
||||
finally:
|
||||
_stop_process(process)
|
||||
|
||||
print("MCP manager example smoke test completed successfully.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,29 @@
|
||||
# MCP Prompt Server Example
|
||||
|
||||
This example uses a local MCP prompt server in [server.py](server.py).
|
||||
|
||||
Run the example via:
|
||||
|
||||
```
|
||||
uv run python examples/mcp/prompt_server/main.py
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
The example uses the `MCPServerStreamableHttp` class from `agents.mcp`. The script auto-selects an open localhost port (or honors `STREAMABLE_HTTP_PORT`) and runs the server at `http://<host>:<port>/mcp`, providing user-controlled prompts that generate agent instructions. If you need a specific address, set `STREAMABLE_HTTP_PORT` and `STREAMABLE_HTTP_HOST`.
|
||||
|
||||
The server exposes prompts like `generate_code_review_instructions` that take parameters such as focus area and programming language. The agent calls these prompts to dynamically generate its system instructions based on user-provided parameters.
|
||||
|
||||
## Workflow
|
||||
|
||||
The example demonstrates two key functions:
|
||||
|
||||
1. **`show_available_prompts`** - Lists all available prompts on the MCP server, showing users what prompts they can select from. This demonstrates the discovery aspect of MCP prompts.
|
||||
|
||||
2. **`demo_code_review`** - Shows the complete user-controlled prompt workflow:
|
||||
- Calls `generate_code_review_instructions` with specific parameters (focus: "security vulnerabilities", language: "python")
|
||||
- Uses the generated instructions to create an Agent with specialized code review capabilities
|
||||
- Runs the agent against vulnerable sample code (command injection via `os.system`)
|
||||
- The agent analyzes the code and provides security-focused feedback using available tools
|
||||
|
||||
This pattern allows users to dynamically configure agent behavior through MCP prompts rather than hardcoded instructions.
|
||||
@@ -0,0 +1,131 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any, cast
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServer, MCPServerStreamableHttp
|
||||
from agents.model_settings import ModelSettings
|
||||
|
||||
STREAMABLE_HTTP_HOST = os.getenv("STREAMABLE_HTTP_HOST", "127.0.0.1")
|
||||
|
||||
|
||||
def _choose_port() -> int:
|
||||
env_port = os.getenv("STREAMABLE_HTTP_PORT")
|
||||
if env_port:
|
||||
return int(env_port)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind((STREAMABLE_HTTP_HOST, 0))
|
||||
address = cast(tuple[str, int], s.getsockname())
|
||||
return address[1]
|
||||
|
||||
|
||||
STREAMABLE_HTTP_PORT = _choose_port()
|
||||
os.environ.setdefault("STREAMABLE_HTTP_PORT", str(STREAMABLE_HTTP_PORT))
|
||||
STREAMABLE_HTTP_URL = f"http://{STREAMABLE_HTTP_HOST}:{STREAMABLE_HTTP_PORT}/mcp"
|
||||
|
||||
|
||||
async def get_instructions_from_prompt(mcp_server: MCPServer, prompt_name: str, **kwargs) -> str:
|
||||
"""Get agent instructions by calling MCP prompt endpoint (user-controlled)"""
|
||||
print(f"Getting instructions from prompt: {prompt_name}")
|
||||
|
||||
try:
|
||||
prompt_result = await mcp_server.get_prompt(prompt_name, kwargs)
|
||||
content = prompt_result.messages[0].content
|
||||
if hasattr(content, "text"):
|
||||
instructions = content.text
|
||||
else:
|
||||
instructions = str(content)
|
||||
print("Generated instructions")
|
||||
return instructions
|
||||
except Exception as e:
|
||||
print(f"Failed to get instructions: {e}")
|
||||
return f"You are a helpful assistant. Error: {e}"
|
||||
|
||||
|
||||
async def demo_code_review(mcp_server: MCPServer):
|
||||
"""Demo: Code review with user-selected prompt"""
|
||||
print("=== CODE REVIEW DEMO ===")
|
||||
|
||||
# User explicitly selects prompt and parameters
|
||||
instructions = await get_instructions_from_prompt(
|
||||
mcp_server,
|
||||
"generate_code_review_instructions",
|
||||
focus="security vulnerabilities",
|
||||
language="python",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Code Reviewer Agent",
|
||||
instructions=instructions, # Instructions from MCP prompt
|
||||
model_settings=ModelSettings(tool_choice="auto"),
|
||||
)
|
||||
|
||||
message = """Please review this code:
|
||||
|
||||
def process_user_input(user_input):
|
||||
command = f"echo {user_input}"
|
||||
os.system(command)
|
||||
return "Command executed"
|
||||
|
||||
"""
|
||||
|
||||
print(f"Running: {message[:60]}...")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
|
||||
|
||||
async def show_available_prompts(mcp_server: MCPServer):
|
||||
"""Show available prompts for user selection"""
|
||||
print("=== AVAILABLE PROMPTS ===")
|
||||
|
||||
prompts_result = await mcp_server.list_prompts()
|
||||
print("User can select from these prompts:")
|
||||
for i, prompt in enumerate(prompts_result.prompts, 1):
|
||||
print(f" {i}. {prompt.name} - {prompt.description}")
|
||||
print()
|
||||
|
||||
|
||||
async def main():
|
||||
async with MCPServerStreamableHttp(
|
||||
name="Simple Prompt Server",
|
||||
params={"url": STREAMABLE_HTTP_URL},
|
||||
) as server:
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="Simple Prompt Demo", trace_id=trace_id):
|
||||
print(f"Trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")
|
||||
|
||||
await show_available_prompts(server)
|
||||
await demo_code_review(server)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not shutil.which("uv"):
|
||||
raise RuntimeError("uv is not installed")
|
||||
|
||||
process: subprocess.Popen[Any] | None = None
|
||||
try:
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_file = os.path.join(this_dir, "server.py")
|
||||
|
||||
print(f"Starting Simple Prompt Server at {STREAMABLE_HTTP_URL} ...")
|
||||
env = os.environ.copy()
|
||||
env.setdefault("STREAMABLE_HTTP_HOST", STREAMABLE_HTTP_HOST)
|
||||
env.setdefault("STREAMABLE_HTTP_PORT", str(STREAMABLE_HTTP_PORT))
|
||||
process = subprocess.Popen(["uv", "run", server_file], env=env)
|
||||
time.sleep(3)
|
||||
print("Server started\n")
|
||||
except Exception as e:
|
||||
print(f"Error starting server: {e}")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
finally:
|
||||
if process:
|
||||
process.terminate()
|
||||
print("Server terminated.")
|
||||
@@ -0,0 +1,42 @@
|
||||
import os
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
STREAMABLE_HTTP_HOST = os.getenv("STREAMABLE_HTTP_HOST", "127.0.0.1")
|
||||
STREAMABLE_HTTP_PORT = int(os.getenv("STREAMABLE_HTTP_PORT", "18080"))
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Prompt Server", host=STREAMABLE_HTTP_HOST, port=STREAMABLE_HTTP_PORT)
|
||||
|
||||
|
||||
# Instruction-generating prompts (user-controlled)
|
||||
@mcp.prompt()
|
||||
def generate_code_review_instructions(
|
||||
focus: str = "general code quality", language: str = "python"
|
||||
) -> str:
|
||||
"""Generate agent instructions for code review tasks"""
|
||||
print(f"[debug-server] generate_code_review_instructions({focus}, {language})")
|
||||
|
||||
return f"""You are a senior {language} code review specialist. Your role is to provide comprehensive code analysis with focus on {focus}.
|
||||
|
||||
INSTRUCTIONS:
|
||||
- Analyze code for quality, security, performance, and best practices
|
||||
- Provide specific, actionable feedback with examples
|
||||
- Identify potential bugs, vulnerabilities, and optimization opportunities
|
||||
- Suggest improvements with code examples when applicable
|
||||
- Be constructive and educational in your feedback
|
||||
- Focus particularly on {focus} aspects
|
||||
|
||||
RESPONSE FORMAT:
|
||||
1. Overall Assessment
|
||||
2. Specific Issues Found
|
||||
3. Security Considerations
|
||||
4. Performance Notes
|
||||
5. Recommended Improvements
|
||||
6. Best Practices Suggestions
|
||||
|
||||
Use the available tools to check current time if you need timestamps for your analysis."""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http")
|
||||
@@ -0,0 +1,13 @@
|
||||
# MCP SSE Example
|
||||
|
||||
This example uses a local SSE server in [server.py](server.py).
|
||||
|
||||
Run the example via:
|
||||
|
||||
```
|
||||
uv run python examples/mcp/sse_example/main.py
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
The example uses the `MCPServerSse` class from `agents.mcp`. The server runs in a sub-process at `https://localhost:8000/sse`.
|
||||
@@ -0,0 +1,104 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any, cast
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServer, MCPServerSse
|
||||
from agents.model_settings import ModelSettings
|
||||
|
||||
SSE_HOST = os.getenv("SSE_HOST", "127.0.0.1")
|
||||
|
||||
|
||||
def _choose_port() -> int:
|
||||
env_port = os.getenv("SSE_PORT")
|
||||
if env_port:
|
||||
return int(env_port)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind((SSE_HOST, 0))
|
||||
address = cast(tuple[str, int], s.getsockname())
|
||||
return address[1]
|
||||
|
||||
|
||||
SSE_PORT = _choose_port()
|
||||
os.environ.setdefault("SSE_PORT", str(SSE_PORT))
|
||||
SSE_URL = f"http://{SSE_HOST}:{SSE_PORT}/sse"
|
||||
|
||||
|
||||
async def run(mcp_server: MCPServer):
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Use the tools to answer the questions.",
|
||||
mcp_servers=[mcp_server],
|
||||
model_settings=ModelSettings(tool_choice="required"),
|
||||
)
|
||||
|
||||
# Use the `add` tool to add two numbers
|
||||
message = "Add these numbers: 7 and 22."
|
||||
print(f"Running: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
# Run the `get_weather` tool
|
||||
message = "What's the weather in Tokyo?"
|
||||
print(f"\n\nRunning: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
# Run the `get_secret_word` tool
|
||||
message = "What's the secret word?"
|
||||
print(f"\n\nRunning: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
async def main():
|
||||
async with MCPServerSse(
|
||||
name="SSE Python Server",
|
||||
params={
|
||||
"url": SSE_URL,
|
||||
},
|
||||
) as server:
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="SSE Example", trace_id=trace_id):
|
||||
print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")
|
||||
await run(server)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Let's make sure the user has uv installed
|
||||
if not shutil.which("uv"):
|
||||
raise RuntimeError(
|
||||
"uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/"
|
||||
)
|
||||
|
||||
# We'll run the SSE server in a subprocess. Usually this would be a remote server, but for this
|
||||
# demo, we'll run it locally at SSE_URL.
|
||||
process: subprocess.Popen[Any] | None = None
|
||||
try:
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_file = os.path.join(this_dir, "server.py")
|
||||
|
||||
print(f"Starting SSE server at {SSE_URL} ...")
|
||||
|
||||
# Run `uv run server.py` to start the SSE server
|
||||
env = os.environ.copy()
|
||||
env.setdefault("SSE_HOST", SSE_HOST)
|
||||
env.setdefault("SSE_PORT", str(SSE_PORT))
|
||||
process = subprocess.Popen(["uv", "run", server_file], env=env)
|
||||
# Give it 3 seconds to start
|
||||
time.sleep(3)
|
||||
|
||||
print("SSE server started. Running example...\n\n")
|
||||
except Exception as e:
|
||||
print(f"Error starting SSE server: {e}")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
finally:
|
||||
if process:
|
||||
process.terminate()
|
||||
@@ -0,0 +1,42 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
SSE_HOST = os.getenv("SSE_HOST", "127.0.0.1")
|
||||
SSE_PORT = int(os.getenv("SSE_PORT", "8000"))
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Echo Server", host=SSE_HOST, port=SSE_PORT)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
print(f"[debug-server] add({a}, {b})")
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_secret_word() -> str:
|
||||
print("[debug-server] get_secret_word()")
|
||||
return random.choice(["apple", "banana", "cherry"])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_current_weather(city: str) -> str:
|
||||
print(f"[debug-server] get_current_weather({city})")
|
||||
# Keep tool output deterministic so this example is stable in CI and offline environments.
|
||||
weather_by_city = {
|
||||
"tokyo": "sunny with a light breeze and 20°C",
|
||||
"san francisco": "cool and foggy with 14°C",
|
||||
"new york": "partly cloudy with 18°C",
|
||||
}
|
||||
forecast = weather_by_city.get(city.strip().lower())
|
||||
if forecast:
|
||||
return f"The weather in {city} is {forecast}."
|
||||
return f"The weather data for {city} is unavailable in this demo."
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="sse")
|
||||
@@ -0,0 +1,13 @@
|
||||
# MCP SSE Remote Example
|
||||
|
||||
Python port of the JS `examples/mcp/sse-example.ts`. By default it starts the bundled local SSE MCP server and lets the agent use those tools. Set `MCP_SSE_REMOTE_URL` to try a compatible remote SSE server instead.
|
||||
|
||||
Run it with:
|
||||
|
||||
```bash
|
||||
uv run python examples/mcp/sse_remote_example/main.py
|
||||
```
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- `OPENAI_API_KEY` set for the model calls.
|
||||
@@ -0,0 +1,100 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServerSse
|
||||
from agents.model_settings import ModelSettings
|
||||
|
||||
SSE_HOST = os.getenv("SSE_HOST", "127.0.0.1")
|
||||
REMOTE_SSE_URL = os.getenv("MCP_SSE_REMOTE_URL")
|
||||
|
||||
|
||||
def _choose_port() -> int:
|
||||
env_port = os.getenv("SSE_PORT")
|
||||
if env_port:
|
||||
return int(env_port)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind((SSE_HOST, 0))
|
||||
address = cast(tuple[str, int], sock.getsockname())
|
||||
return address[1]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def local_sse_server() -> Iterator[str]:
|
||||
if not shutil.which("uv"):
|
||||
raise RuntimeError(
|
||||
"uv is not installed. Please install it: "
|
||||
"https://docs.astral.sh/uv/getting-started/installation/"
|
||||
)
|
||||
|
||||
sse_port = _choose_port()
|
||||
sse_url = f"http://{SSE_HOST}:{sse_port}/sse"
|
||||
server_file = Path(__file__).resolve().parents[1] / "sse_example" / "server.py"
|
||||
|
||||
print(f"Starting local SSE server at {sse_url} ...", flush=True)
|
||||
env = os.environ.copy()
|
||||
env.setdefault("SSE_HOST", SSE_HOST)
|
||||
env["SSE_PORT"] = str(sse_port)
|
||||
process: subprocess.Popen[Any] | None = None
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(["uv", "run", str(server_file)], env=env)
|
||||
time.sleep(3)
|
||||
yield sse_url
|
||||
finally:
|
||||
if process is not None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
|
||||
|
||||
async def run(url: str, name: str) -> None:
|
||||
async with MCPServerSse(
|
||||
name=name,
|
||||
params={
|
||||
"url": url,
|
||||
"timeout": 5,
|
||||
"sse_read_timeout": 30,
|
||||
},
|
||||
) as server:
|
||||
agent = Agent(
|
||||
name="SSE Assistant",
|
||||
instructions="Use the available MCP tools to answer the user.",
|
||||
mcp_servers=[server],
|
||||
model_settings=ModelSettings(tool_choice="required"),
|
||||
)
|
||||
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="SSE MCP Server Example", trace_id=trace_id):
|
||||
print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")
|
||||
result = await Runner.run(agent, "Use the MCP add tool to add 7 and 22.")
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
if REMOTE_SSE_URL:
|
||||
print(f"Connecting to remote SSE server at {REMOTE_SSE_URL} ...", flush=True)
|
||||
await run(REMOTE_SSE_URL, "Remote SSE Server")
|
||||
return
|
||||
|
||||
print(
|
||||
"MCP_SSE_REMOTE_URL is not set; using the bundled local SSE server for this demo.",
|
||||
flush=True,
|
||||
)
|
||||
with local_sse_server() as url:
|
||||
await run(url, "Local SSE Server")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,13 @@
|
||||
# MCP Streamable HTTP Remote Example
|
||||
|
||||
Python port of the JS `examples/mcp/streamable-http-example.ts`. It connects to DeepWiki over the Streamable HTTP transport (`https://mcp.deepwiki.com/mcp`) and lets the agent use those tools.
|
||||
|
||||
Run it with:
|
||||
|
||||
```bash
|
||||
uv run python examples/mcp/streamable_http_remote_example/main.py
|
||||
```
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- `OPENAI_API_KEY` set for the model calls.
|
||||
@@ -0,0 +1,38 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServerStreamableHttp
|
||||
|
||||
|
||||
async def main():
|
||||
async with MCPServerStreamableHttp(
|
||||
name="DeepWiki MCP Streamable HTTP Server",
|
||||
params={
|
||||
"url": "https://mcp.deepwiki.com/mcp",
|
||||
# Allow more time for remote tool responses.
|
||||
"timeout": 15,
|
||||
"sse_read_timeout": 300,
|
||||
},
|
||||
# Retry slow/unstable remote calls a couple of times.
|
||||
max_retry_attempts=2,
|
||||
retry_backoff_seconds_base=2.0,
|
||||
client_session_timeout_seconds=15,
|
||||
) as server:
|
||||
agent = Agent(
|
||||
name="DeepWiki Assistant",
|
||||
instructions="Use the tools to respond to user requests.",
|
||||
mcp_servers=[server],
|
||||
)
|
||||
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="DeepWiki Streamable HTTP Example", trace_id=trace_id):
|
||||
print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"For the repository openai/codex, tell me the primary programming language.",
|
||||
)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,63 @@
|
||||
# Custom HTTP Client Factory Example
|
||||
|
||||
This example demonstrates how to use the new `httpx_client_factory` parameter in `MCPServerStreamableHttp` to configure custom HTTP client behavior for MCP StreamableHTTP connections.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
- **Custom SSL Configuration**: Configure SSL certificates and verification settings
|
||||
- **Custom Headers**: Add custom headers to all HTTP requests
|
||||
- **Custom Timeouts**: Set custom timeout values for requests
|
||||
- **Proxy Configuration**: Configure HTTP proxy settings
|
||||
- **Custom Retry Logic**: Set up custom retry behavior (through httpx configuration)
|
||||
|
||||
## Running the Example
|
||||
|
||||
1. Make sure you have `uv` installed: https://docs.astral.sh/uv/getting-started/installation/
|
||||
|
||||
2. Run the example:
|
||||
```bash
|
||||
cd examples/mcp/streamablehttp_custom_client_example
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Basic Custom Client
|
||||
|
||||
```python
|
||||
import httpx
|
||||
from agents.mcp import MCPServerStreamableHttp
|
||||
|
||||
def create_custom_http_client() -> httpx.AsyncClient:
|
||||
return httpx.AsyncClient(
|
||||
verify=False, # Disable SSL verification for testing
|
||||
timeout=httpx.Timeout(60.0, read=120.0),
|
||||
headers={"X-Custom-Client": "my-app"},
|
||||
)
|
||||
|
||||
async with MCPServerStreamableHttp(
|
||||
name="Custom Client Server",
|
||||
params={
|
||||
"url": "http://localhost:<port>/mcp",
|
||||
"httpx_client_factory": create_custom_http_client,
|
||||
},
|
||||
) as server:
|
||||
# Use the server...
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Corporate Networks**: Configure proxy settings for corporate environments
|
||||
- **SSL/TLS Requirements**: Use custom SSL certificates for secure connections
|
||||
- **Custom Authentication**: Add custom headers for API authentication
|
||||
- **Network Optimization**: Configure timeouts and connection pooling
|
||||
- **Debugging**: Disable SSL verification for development environments
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Flexibility**: Configure HTTP client behavior to match your network requirements
|
||||
- **Security**: Use custom SSL certificates and authentication methods
|
||||
- **Performance**: Optimize timeouts and connection settings for your use case
|
||||
- **Compatibility**: Work with corporate proxies and network restrictions
|
||||
|
||||
This example will auto-pick a free localhost port unless you set `STREAMABLE_HTTP_PORT`; use `STREAMABLE_HTTP_HOST` to change the bind address.
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Example demonstrating custom httpx_client_factory for MCPServerStreamableHttp.
|
||||
|
||||
This example shows how to configure custom HTTP client behavior for MCP StreamableHTTP
|
||||
connections, including SSL certificates, proxy settings, and custom timeouts.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any, cast
|
||||
|
||||
import httpx
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServer, MCPServerStreamableHttp
|
||||
from agents.model_settings import ModelSettings
|
||||
|
||||
STREAMABLE_HTTP_HOST = os.getenv("STREAMABLE_HTTP_HOST", "127.0.0.1")
|
||||
|
||||
|
||||
def _choose_port() -> int:
|
||||
env_port = os.getenv("STREAMABLE_HTTP_PORT")
|
||||
if env_port:
|
||||
return int(env_port)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind((STREAMABLE_HTTP_HOST, 0))
|
||||
address = cast(tuple[str, int], s.getsockname())
|
||||
return address[1]
|
||||
|
||||
|
||||
STREAMABLE_HTTP_PORT = _choose_port()
|
||||
os.environ.setdefault("STREAMABLE_HTTP_PORT", str(STREAMABLE_HTTP_PORT))
|
||||
STREAMABLE_HTTP_URL = f"http://{STREAMABLE_HTTP_HOST}:{STREAMABLE_HTTP_PORT}/mcp"
|
||||
|
||||
|
||||
def create_custom_http_client(
|
||||
headers: dict[str, str] | None = None,
|
||||
timeout: httpx.Timeout | None = None,
|
||||
auth: httpx.Auth | None = None,
|
||||
) -> httpx.AsyncClient:
|
||||
"""Create a custom HTTP client with specific configurations.
|
||||
|
||||
This function demonstrates how to configure:
|
||||
- Custom SSL verification settings
|
||||
- Custom timeouts
|
||||
- Custom headers
|
||||
- Proxy settings (commented out)
|
||||
"""
|
||||
if headers is None:
|
||||
headers = {
|
||||
"X-Custom-Client": "agents-mcp-example",
|
||||
"User-Agent": "OpenAI-Agents-MCP/1.0",
|
||||
}
|
||||
if timeout is None:
|
||||
timeout = httpx.Timeout(60.0, read=120.0)
|
||||
if auth is None:
|
||||
auth = None
|
||||
return httpx.AsyncClient(
|
||||
# Disable SSL verification for testing (not recommended for production)
|
||||
verify=False,
|
||||
# Set custom timeout
|
||||
timeout=httpx.Timeout(60.0, read=120.0),
|
||||
# Add custom headers that will be sent with every request
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
async def run_with_custom_client(mcp_server: MCPServer):
|
||||
"""Run the agent with a custom HTTP client configuration."""
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Use the tools to answer the questions.",
|
||||
mcp_servers=[mcp_server],
|
||||
model_settings=ModelSettings(tool_choice="required"),
|
||||
)
|
||||
|
||||
# Use the `add` tool to add two numbers
|
||||
message = "Add these numbers: 7 and 22."
|
||||
print(f"Running: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function demonstrating different HTTP client configurations."""
|
||||
|
||||
print("=== Example: Custom HTTP Client with SSL disabled and custom headers ===")
|
||||
async with MCPServerStreamableHttp(
|
||||
name="Streamable HTTP with Custom Client",
|
||||
params={
|
||||
"url": STREAMABLE_HTTP_URL,
|
||||
"httpx_client_factory": create_custom_http_client,
|
||||
},
|
||||
) as server:
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="Custom HTTP Client Example", trace_id=trace_id):
|
||||
print(f"View trace: https://platform.openai.com/logs/trace?trace_id={trace_id}\n")
|
||||
await run_with_custom_client(server)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Let's make sure the user has uv installed
|
||||
if not shutil.which("uv"):
|
||||
raise RuntimeError(
|
||||
"uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/"
|
||||
)
|
||||
|
||||
# We'll run the Streamable HTTP server in a subprocess. Usually this would be a remote server, but for this
|
||||
# demo, we'll run it locally at STREAMABLE_HTTP_URL
|
||||
process: subprocess.Popen[Any] | None = None
|
||||
try:
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_file = os.path.join(this_dir, "server.py")
|
||||
|
||||
print(f"Starting Streamable HTTP server at {STREAMABLE_HTTP_URL} ...")
|
||||
|
||||
# Run `uv run server.py` to start the Streamable HTTP server
|
||||
env = os.environ.copy()
|
||||
env.setdefault("STREAMABLE_HTTP_HOST", STREAMABLE_HTTP_HOST)
|
||||
env.setdefault("STREAMABLE_HTTP_PORT", str(STREAMABLE_HTTP_PORT))
|
||||
process = subprocess.Popen(["uv", "run", server_file], env=env)
|
||||
# Give it 3 seconds to start
|
||||
time.sleep(3)
|
||||
|
||||
print("Streamable HTTP server started. Running example...\n\n")
|
||||
except Exception as e:
|
||||
print(f"Error starting Streamable HTTP server: {e}")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
finally:
|
||||
if process:
|
||||
process.terminate()
|
||||
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
STREAMABLE_HTTP_HOST = os.getenv("STREAMABLE_HTTP_HOST", "127.0.0.1")
|
||||
STREAMABLE_HTTP_PORT = int(os.getenv("STREAMABLE_HTTP_PORT", "18080"))
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Echo Server", host=STREAMABLE_HTTP_HOST, port=STREAMABLE_HTTP_PORT)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
print(f"[debug-server] add({a}, {b})")
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_secret_word() -> str:
|
||||
print("[debug-server] get_secret_word()")
|
||||
return random.choice(["apple", "banana", "cherry"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http")
|
||||
@@ -0,0 +1,13 @@
|
||||
# MCP Streamable HTTP Example
|
||||
|
||||
This example uses a local Streamable HTTP server in [server.py](server.py).
|
||||
|
||||
Run the example via:
|
||||
|
||||
```
|
||||
uv run python examples/mcp/streamablehttp_example/main.py
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
The example uses the `MCPServerStreamableHttp` class from `agents.mcp`. The script picks an open localhost port automatically (or honors `STREAMABLE_HTTP_PORT` if you set it) and starts the server at `http://<host>:<port>/mcp`. Set `STREAMABLE_HTTP_HOST` if you need a different bind address.
|
||||
@@ -0,0 +1,104 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any, cast
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServer, MCPServerStreamableHttp
|
||||
from agents.model_settings import ModelSettings
|
||||
|
||||
STREAMABLE_HTTP_HOST = os.getenv("STREAMABLE_HTTP_HOST", "127.0.0.1")
|
||||
|
||||
|
||||
def _choose_port() -> int:
|
||||
env_port = os.getenv("STREAMABLE_HTTP_PORT")
|
||||
if env_port:
|
||||
return int(env_port)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind((STREAMABLE_HTTP_HOST, 0))
|
||||
address = cast(tuple[str, int], s.getsockname())
|
||||
return address[1]
|
||||
|
||||
|
||||
STREAMABLE_HTTP_PORT = _choose_port()
|
||||
os.environ.setdefault("STREAMABLE_HTTP_PORT", str(STREAMABLE_HTTP_PORT))
|
||||
STREAMABLE_HTTP_URL = f"http://{STREAMABLE_HTTP_HOST}:{STREAMABLE_HTTP_PORT}/mcp"
|
||||
|
||||
|
||||
async def run(mcp_server: MCPServer):
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Use the tools to answer the questions.",
|
||||
mcp_servers=[mcp_server],
|
||||
model_settings=ModelSettings(tool_choice="required"),
|
||||
)
|
||||
|
||||
# Use the `add` tool to add two numbers
|
||||
message = "Add these numbers: 7 and 22."
|
||||
print(f"Running: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
# Run the `get_weather` tool
|
||||
message = "What's the weather in Tokyo?"
|
||||
print(f"\n\nRunning: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
# Run the `get_secret_word` tool
|
||||
message = "What's the secret word?"
|
||||
print(f"\n\nRunning: {message}")
|
||||
result = await Runner.run(starting_agent=agent, input=message)
|
||||
print(result.final_output)
|
||||
|
||||
|
||||
async def main():
|
||||
async with MCPServerStreamableHttp(
|
||||
name="Streamable HTTP Python Server",
|
||||
params={
|
||||
"url": STREAMABLE_HTTP_URL,
|
||||
},
|
||||
) as server:
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="Streamable HTTP Example", trace_id=trace_id):
|
||||
print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")
|
||||
await run(server)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Let's make sure the user has uv installed
|
||||
if not shutil.which("uv"):
|
||||
raise RuntimeError(
|
||||
"uv is not installed. Please install it: https://docs.astral.sh/uv/getting-started/installation/"
|
||||
)
|
||||
|
||||
# We'll run the Streamable HTTP server in a subprocess. Usually this would be a remote server, but for this
|
||||
# demo, we'll run it locally at STREAMABLE_HTTP_URL
|
||||
process: subprocess.Popen[Any] | None = None
|
||||
try:
|
||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_file = os.path.join(this_dir, "server.py")
|
||||
|
||||
print(f"Starting Streamable HTTP server at {STREAMABLE_HTTP_URL} ...")
|
||||
|
||||
# Run `uv run server.py` to start the Streamable HTTP server
|
||||
env = os.environ.copy()
|
||||
env.setdefault("STREAMABLE_HTTP_HOST", STREAMABLE_HTTP_HOST)
|
||||
env.setdefault("STREAMABLE_HTTP_PORT", str(STREAMABLE_HTTP_PORT))
|
||||
process = subprocess.Popen(["uv", "run", server_file], env=env)
|
||||
# Give it 3 seconds to start
|
||||
time.sleep(3)
|
||||
|
||||
print("Streamable HTTP server started. Running example...\n\n")
|
||||
except Exception as e:
|
||||
print(f"Error starting Streamable HTTP server: {e}")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
finally:
|
||||
if process:
|
||||
process.terminate()
|
||||
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
import requests
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
STREAMABLE_HTTP_HOST = os.getenv("STREAMABLE_HTTP_HOST", "127.0.0.1")
|
||||
STREAMABLE_HTTP_PORT = int(os.getenv("STREAMABLE_HTTP_PORT", "18080"))
|
||||
|
||||
# Create server
|
||||
mcp = FastMCP("Echo Server", host=STREAMABLE_HTTP_HOST, port=STREAMABLE_HTTP_PORT)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
print(f"[debug-server] add({a}, {b})")
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_secret_word() -> str:
|
||||
print("[debug-server] get_secret_word()")
|
||||
return random.choice(["apple", "banana", "cherry"])
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_current_weather(city: str) -> str:
|
||||
print(f"[debug-server] get_current_weather({city})")
|
||||
# Avoid slow or flaky network calls during automated runs.
|
||||
try:
|
||||
endpoint = "https://wttr.in"
|
||||
response = requests.get(f"{endpoint}/{city}", timeout=2)
|
||||
if response.ok:
|
||||
return response.text
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback keeps the tool responsive even when offline.
|
||||
return f"Weather data unavailable right now; assume clear skies in {city}."
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http")
|
||||
@@ -0,0 +1,19 @@
|
||||
# MCP Tool Filter Example
|
||||
|
||||
Python port of the JS `examples/mcp/tool-filter-example.ts`. It shows how to:
|
||||
|
||||
- Run the filesystem MCP server locally via `npx`.
|
||||
- Apply a static tool filter so only specific tools are exposed to the model.
|
||||
- Observe that blocked tools are not available.
|
||||
- Enable `require_approval="always"` and auto-approve interruptions in code so the HITL path is exercised.
|
||||
|
||||
Run it with:
|
||||
|
||||
```bash
|
||||
uv run python examples/mcp/tool_filter_example/main.py
|
||||
```
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- `npx` available on your PATH.
|
||||
- `OPENAI_API_KEY` set for the model calls.
|
||||
@@ -0,0 +1,75 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any, cast
|
||||
|
||||
from agents import Agent, Runner, gen_trace_id, trace
|
||||
from agents.mcp import MCPServerStdio
|
||||
from agents.mcp.util import create_static_tool_filter
|
||||
|
||||
|
||||
async def run_with_auto_approval(agent: Agent[Any], message: str) -> str | None:
|
||||
"""Run and auto-approve interruptions."""
|
||||
|
||||
result = await Runner.run(agent, message)
|
||||
while result.interruptions:
|
||||
state = result.to_state()
|
||||
for interruption in result.interruptions:
|
||||
print(f"Approving a tool call... (name: {interruption.name})")
|
||||
state.approve(interruption, always_approve=True)
|
||||
result = await Runner.run(agent, state)
|
||||
return cast(str | None, result.final_output)
|
||||
|
||||
|
||||
async def main():
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
samples_dir = os.path.join(current_dir, "sample_files")
|
||||
target_path = os.path.join(samples_dir, "test.txt")
|
||||
|
||||
async with MCPServerStdio(
|
||||
name="Filesystem Server with filter",
|
||||
params={
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", samples_dir],
|
||||
"cwd": samples_dir,
|
||||
},
|
||||
require_approval="always",
|
||||
tool_filter=create_static_tool_filter(
|
||||
allowed_tool_names=["read_file", "list_directory"],
|
||||
blocked_tool_names=["write_file"],
|
||||
),
|
||||
) as server:
|
||||
agent = Agent(
|
||||
name="MCP Assistant",
|
||||
instructions=(
|
||||
"Use only the available filesystem tools. "
|
||||
"All file paths should be absolute paths inside the allowed directory. "
|
||||
"If a user asks for an action that requires an unavailable tool, "
|
||||
"explicitly explain that it is blocked by the tool filter."
|
||||
),
|
||||
mcp_servers=[server],
|
||||
)
|
||||
trace_id = gen_trace_id()
|
||||
with trace(workflow_name="MCP Tool Filter Example", trace_id=trace_id):
|
||||
print(f"View trace: https://platform.openai.com/traces/trace?trace_id={trace_id}\n")
|
||||
result = await run_with_auto_approval(
|
||||
agent, f"List the files in this allowed directory: {samples_dir}"
|
||||
)
|
||||
print(result)
|
||||
|
||||
blocked_result = await run_with_auto_approval(
|
||||
agent,
|
||||
(
|
||||
f'Create a file at "{target_path}" with the text "hello". '
|
||||
"If you cannot, explain that write operations are blocked by the tool filter."
|
||||
),
|
||||
)
|
||||
print("\nAttempting to write a file (should be blocked):")
|
||||
print(blocked_result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not shutil.which("npx"):
|
||||
raise RuntimeError("npx is required. Install it with `npm install -g npx`.")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,20 @@
|
||||
1. To Kill a Mockingbird – Harper Lee
|
||||
2. Pride and Prejudice – Jane Austen
|
||||
3. 1984 – George Orwell
|
||||
4. The Hobbit – J.R.R. Tolkien
|
||||
5. Harry Potter and the Sorcerer’s Stone – J.K. Rowling
|
||||
6. The Great Gatsby – F. Scott Fitzgerald
|
||||
7. Charlotte’s Web – E.B. White
|
||||
8. Anne of Green Gables – Lucy Maud Montgomery
|
||||
9. The Alchemist – Paulo Coelho
|
||||
10. Little Women – Louisa May Alcott
|
||||
11. The Catcher in the Rye – J.D. Salinger
|
||||
12. Animal Farm – George Orwell
|
||||
13. The Chronicles of Narnia: The Lion, the Witch, and the Wardrobe – C.S. Lewis
|
||||
14. The Book Thief – Markus Zusak
|
||||
15. A Wrinkle in Time – Madeleine L’Engle
|
||||
16. The Secret Garden – Frances Hodgson Burnett
|
||||
17. Moby-Dick – Herman Melville
|
||||
18. Fahrenheit 451 – Ray Bradbury
|
||||
19. Jane Eyre – Charlotte Brontë
|
||||
20. The Little Prince – Antoine de Saint-Exupéry
|
||||
@@ -0,0 +1,10 @@
|
||||
1. "Here Comes the Sun" – The Beatles
|
||||
2. "Imagine" – John Lennon
|
||||
3. "Bohemian Rhapsody" – Queen
|
||||
4. "Shake It Off" – Taylor Swift
|
||||
5. "Billie Jean" – Michael Jackson
|
||||
6. "Uptown Funk" – Mark Ronson ft. Bruno Mars
|
||||
7. "Don’t Stop Believin’" – Journey
|
||||
8. "Dancing Queen" – ABBA
|
||||
9. "Happy" – Pharrell Williams
|
||||
10. "Wonderwall" – Oasis
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user