Merge branch 'main' into main
This commit is contained in:
commit
f8655c3b44
14 changed files with 116 additions and 10 deletions
18
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal file
18
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
<!-- Please give a short summary of the change and the problem this solves. -->
|
||||||
|
|
||||||
|
### Test plan
|
||||||
|
|
||||||
|
<!-- Please explain how this was tested -->
|
||||||
|
|
||||||
|
### Issue number
|
||||||
|
|
||||||
|
<!-- For example: "Closes #1234" -->
|
||||||
|
|
||||||
|
### Checks
|
||||||
|
|
||||||
|
- [ ] I've added new tests (if relevant)
|
||||||
|
- [ ] I've added/updated the relevant documentation
|
||||||
|
- [ ] I've run `make lint` and `make format`
|
||||||
|
- [ ] I've made sure tests pass
|
||||||
|
|
@ -140,7 +140,7 @@ The Agents SDK is designed to be highly flexible, allowing you to model a wide r
|
||||||
|
|
||||||
## Tracing
|
## Tracing
|
||||||
|
|
||||||
The Agents SDK automatically traces your agent runs, making it easy to track and debug the behavior of your agents. Tracing is extensible by design, supporting custom spans and a wide variety of external destinations, including [Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents), [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk), [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk), and [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration). For more details about how to customize or disable tracing, see [Tracing](http://openai.github.io/openai-agents-python/tracing).
|
The Agents SDK automatically traces your agent runs, making it easy to track and debug the behavior of your agents. Tracing is extensible by design, supporting custom spans and a wide variety of external destinations, including [Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents), [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk), [Braintrust](https://braintrust.dev/docs/guides/traces/integrations#openai-agents-sdk), [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration), and [Keywords AI](https://docs.keywordsai.co/integration/development-frameworks/openai-agent). For more details about how to customize or disable tracing, see [Tracing](http://openai.github.io/openai-agents-python/tracing).
|
||||||
|
|
||||||
## Development (only needed if you need to edit the SDK/examples)
|
## Development (only needed if you need to edit the SDK/examples)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ The most common properties of an agent you'll configure are:
|
||||||
```python
|
```python
|
||||||
from agents import Agent, ModelSettings, function_tool
|
from agents import Agent, ModelSettings, function_tool
|
||||||
|
|
||||||
|
@function_tool
|
||||||
def get_weather(city: str) -> str:
|
def get_weather(city: str) -> str:
|
||||||
return f"The weather in {city} is sunny"
|
return f"The weather in {city} is sunny"
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ agent = Agent(
|
||||||
name="Haiku agent",
|
name="Haiku agent",
|
||||||
instructions="Always respond in haiku form",
|
instructions="Always respond in haiku form",
|
||||||
model="o3-mini",
|
model="o3-mini",
|
||||||
tools=[function_tool(get_weather)],
|
tools=[get_weather],
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class UserInfo: # (1)!
|
||||||
name: str
|
name: str
|
||||||
uid: int
|
uid: int
|
||||||
|
|
||||||
|
@function_tool
|
||||||
async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str: # (2)!
|
async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str: # (2)!
|
||||||
return f"User {wrapper.context.name} is 47 years old"
|
return f"User {wrapper.context.name} is 47 years old"
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ async def main():
|
||||||
|
|
||||||
agent = Agent[UserInfo]( # (4)!
|
agent = Agent[UserInfo]( # (4)!
|
||||||
name="Assistant",
|
name="Assistant",
|
||||||
tools=[function_tool(fetch_user_age)],
|
tools=[fetch_user_age],
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await Runner.run(
|
result = await Runner.run(
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ async def main():
|
||||||
# San Francisco
|
# San Francisco
|
||||||
|
|
||||||
# Second turn
|
# Second turn
|
||||||
new_input = output.to_input_list() + [{"role": "user", "content": "What state is it in?"}]
|
new_input = result.to_input_list() + [{"role": "user", "content": "What state is it in?"}]
|
||||||
result = await Runner.run(agent, new_input)
|
result = await Runner.run(agent, new_input)
|
||||||
print(result.final_output)
|
print(result.final_output)
|
||||||
# California
|
# California
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ async def main():
|
||||||
|
|
||||||
with trace("Joke workflow"): # (1)!
|
with trace("Joke workflow"): # (1)!
|
||||||
first_result = await Runner.run(agent, "Tell me a joke")
|
first_result = await Runner.run(agent, "Tell me a joke")
|
||||||
second_result = await Runner.run(agent, f"Rate this joke: {first_output.final_output}")
|
second_result = await Runner.run(agent, f"Rate this joke: {first_result.final_output}")
|
||||||
print(f"Joke: {first_result.final_output}")
|
print(f"Joke: {first_result.final_output}")
|
||||||
print(f"Rating: {second_result.final_output}")
|
print(f"Rating: {second_result.final_output}")
|
||||||
```
|
```
|
||||||
|
|
@ -94,3 +94,4 @@ External trace processors include:
|
||||||
- [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents)
|
- [Pydantic Logfire](https://logfire.pydantic.dev/docs/integrations/llms/openai/#openai-agents)
|
||||||
- [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk)
|
- [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk)
|
||||||
- [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration))
|
- [Scorecard](https://docs.scorecard.io/docs/documentation/features/tracing#openai-agents-sdk-integration))
|
||||||
|
- [Keywords AI](https://docs.keywordsai.co/integration/development-frameworks/openai-agent)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ async def math_guardrail(
|
||||||
|
|
||||||
return GuardrailFunctionOutput(
|
return GuardrailFunctionOutput(
|
||||||
output_info=final_output,
|
output_info=final_output,
|
||||||
tripwire_triggered=not final_output.is_math_homework,
|
tripwire_triggered=final_output.is_math_homework,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ class InputGuardrail(Generic[TContext]):
|
||||||
[RunContextWrapper[TContext], Agent[Any], str | list[TResponseInputItem]],
|
[RunContextWrapper[TContext], Agent[Any], str | list[TResponseInputItem]],
|
||||||
MaybeAwaitable[GuardrailFunctionOutput],
|
MaybeAwaitable[GuardrailFunctionOutput],
|
||||||
]
|
]
|
||||||
"""A function that receives the the agent input and the context, and returns a
|
"""A function that receives the agent input and the context, and returns a
|
||||||
`GuardrailResult`. The result marks whether the tripwire was triggered, and can optionally
|
`GuardrailResult`. The result marks whether the tripwire was triggered, and can optionally
|
||||||
include information about the guardrail's output.
|
include information about the guardrail's output.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,34 @@ class ModelSettings:
|
||||||
|
|
||||||
This class holds optional model configuration parameters (e.g. temperature,
|
This class holds optional model configuration parameters (e.g. temperature,
|
||||||
top_p, penalties, truncation, etc.).
|
top_p, penalties, truncation, etc.).
|
||||||
|
|
||||||
|
Not all models/providers support all of these parameters, so please check the API documentation
|
||||||
|
for the specific model and provider you are using.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
temperature: float | None = None
|
temperature: float | None = None
|
||||||
|
"""The temperature to use when calling the model."""
|
||||||
|
|
||||||
top_p: float | None = None
|
top_p: float | None = None
|
||||||
|
"""The top_p to use when calling the model."""
|
||||||
|
|
||||||
frequency_penalty: float | None = None
|
frequency_penalty: float | None = None
|
||||||
|
"""The frequency penalty to use when calling the model."""
|
||||||
|
|
||||||
presence_penalty: float | None = None
|
presence_penalty: float | None = None
|
||||||
|
"""The presence penalty to use when calling the model."""
|
||||||
|
|
||||||
tool_choice: Literal["auto", "required", "none"] | str | None = None
|
tool_choice: Literal["auto", "required", "none"] | str | None = None
|
||||||
|
"""The tool choice to use when calling the model."""
|
||||||
|
|
||||||
parallel_tool_calls: bool | None = False
|
parallel_tool_calls: bool | None = False
|
||||||
|
"""Whether to use parallel tool calls when calling the model."""
|
||||||
|
|
||||||
truncation: Literal["auto", "disabled"] | None = None
|
truncation: Literal["auto", "disabled"] | None = None
|
||||||
|
"""The truncation strategy to use when calling the model."""
|
||||||
|
|
||||||
|
max_tokens: int | None = None
|
||||||
|
"""The maximum number of output tokens to generate."""
|
||||||
|
|
||||||
def resolve(self, override: ModelSettings | None) -> ModelSettings:
|
def resolve(self, override: ModelSettings | None) -> ModelSettings:
|
||||||
"""Produce a new ModelSettings by overlaying any non-None values from the
|
"""Produce a new ModelSettings by overlaying any non-None values from the
|
||||||
|
|
@ -33,4 +52,5 @@ class ModelSettings:
|
||||||
tool_choice=override.tool_choice or self.tool_choice,
|
tool_choice=override.tool_choice or self.tool_choice,
|
||||||
parallel_tool_calls=override.parallel_tool_calls or self.parallel_tool_calls,
|
parallel_tool_calls=override.parallel_tool_calls or self.parallel_tool_calls,
|
||||||
truncation=override.truncation or self.truncation,
|
truncation=override.truncation or self.truncation,
|
||||||
|
max_tokens=override.max_tokens or self.max_tokens,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,10 @@ from openai.types.responses import (
|
||||||
ResponseOutputText,
|
ResponseOutputText,
|
||||||
ResponseRefusalDeltaEvent,
|
ResponseRefusalDeltaEvent,
|
||||||
ResponseTextDeltaEvent,
|
ResponseTextDeltaEvent,
|
||||||
|
ResponseUsage,
|
||||||
)
|
)
|
||||||
from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message
|
from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message
|
||||||
|
from openai.types.responses.response_usage import OutputTokensDetails
|
||||||
|
|
||||||
from .. import _debug
|
from .. import _debug
|
||||||
from ..agent_output import AgentOutputSchema
|
from ..agent_output import AgentOutputSchema
|
||||||
|
|
@ -405,7 +407,23 @@ class OpenAIChatCompletionsModel(Model):
|
||||||
for function_call in state.function_calls.values():
|
for function_call in state.function_calls.values():
|
||||||
outputs.append(function_call)
|
outputs.append(function_call)
|
||||||
|
|
||||||
final_response = response.model_copy(update={"output": outputs, "usage": usage})
|
final_response = response.model_copy()
|
||||||
|
final_response.output = outputs
|
||||||
|
final_response.usage = (
|
||||||
|
ResponseUsage(
|
||||||
|
input_tokens=usage.prompt_tokens,
|
||||||
|
output_tokens=usage.completion_tokens,
|
||||||
|
total_tokens=usage.total_tokens,
|
||||||
|
output_tokens_details=OutputTokensDetails(
|
||||||
|
reasoning_tokens=usage.completion_tokens_details.reasoning_tokens
|
||||||
|
if usage.completion_tokens_details
|
||||||
|
and usage.completion_tokens_details.reasoning_tokens
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if usage
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
yield ResponseCompletedEvent(
|
yield ResponseCompletedEvent(
|
||||||
response=final_response,
|
response=final_response,
|
||||||
|
|
@ -503,6 +521,7 @@ class OpenAIChatCompletionsModel(Model):
|
||||||
top_p=self._non_null_or_not_given(model_settings.top_p),
|
top_p=self._non_null_or_not_given(model_settings.top_p),
|
||||||
frequency_penalty=self._non_null_or_not_given(model_settings.frequency_penalty),
|
frequency_penalty=self._non_null_or_not_given(model_settings.frequency_penalty),
|
||||||
presence_penalty=self._non_null_or_not_given(model_settings.presence_penalty),
|
presence_penalty=self._non_null_or_not_given(model_settings.presence_penalty),
|
||||||
|
max_tokens=self._non_null_or_not_given(model_settings.max_tokens),
|
||||||
tool_choice=tool_choice,
|
tool_choice=tool_choice,
|
||||||
response_format=response_format,
|
response_format=response_format,
|
||||||
parallel_tool_calls=parallel_tool_calls,
|
parallel_tool_calls=parallel_tool_calls,
|
||||||
|
|
@ -808,6 +827,13 @@ class _Converter:
|
||||||
"content": cls.extract_text_content(content),
|
"content": cls.extract_text_content(content),
|
||||||
}
|
}
|
||||||
result.append(msg_developer)
|
result.append(msg_developer)
|
||||||
|
elif role == "assistant":
|
||||||
|
flush_assistant_message()
|
||||||
|
msg_assistant: ChatCompletionAssistantMessageParam = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": cls.extract_text_content(content),
|
||||||
|
}
|
||||||
|
result.append(msg_assistant)
|
||||||
else:
|
else:
|
||||||
raise UserError(f"Unexpected role in easy_input_message: {role}")
|
raise UserError(f"Unexpected role in easy_input_message: {role}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@ class OpenAIResponsesModel(Model):
|
||||||
temperature=self._non_null_or_not_given(model_settings.temperature),
|
temperature=self._non_null_or_not_given(model_settings.temperature),
|
||||||
top_p=self._non_null_or_not_given(model_settings.top_p),
|
top_p=self._non_null_or_not_given(model_settings.top_p),
|
||||||
truncation=self._non_null_or_not_given(model_settings.truncation),
|
truncation=self._non_null_or_not_given(model_settings.truncation),
|
||||||
|
max_output_tokens=self._non_null_or_not_given(model_settings.max_tokens),
|
||||||
tool_choice=tool_choice,
|
tool_choice=tool_choice,
|
||||||
parallel_tool_calls=parallel_tool_calls,
|
parallel_tool_calls=parallel_tool_calls,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
|
|
|
||||||
|
|
@ -216,5 +216,3 @@ class RunResultStreaming(RunResultBase):
|
||||||
|
|
||||||
if self._output_guardrails_task and not self._output_guardrails_task.done():
|
if self._output_guardrails_task and not self._output_guardrails_task.done():
|
||||||
self._output_guardrails_task.cancel()
|
self._output_guardrails_task.cancel()
|
||||||
self._output_guardrails_task.cancel()
|
|
||||||
self._output_guardrails_task.cancel()
|
|
||||||
|
|
|
||||||
|
|
@ -393,3 +393,38 @@ def test_unknown_object_errors():
|
||||||
with pytest.raises(UserError, match="Unhandled item type or structure"):
|
with pytest.raises(UserError, match="Unhandled item type or structure"):
|
||||||
# Purposely ignore the type error
|
# Purposely ignore the type error
|
||||||
_Converter.items_to_messages([TestObject()]) # type: ignore
|
_Converter.items_to_messages([TestObject()]) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_assistant_messages_in_history():
|
||||||
|
"""
|
||||||
|
Test that assistant messages are added to the history.
|
||||||
|
"""
|
||||||
|
messages = _Converter.items_to_messages(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Hello?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "What was my Name?",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert messages == [
|
||||||
|
{"role": "user", "content": "Hello"},
|
||||||
|
{"role": "assistant", "content": "Hello?"},
|
||||||
|
{"role": "user", "content": "What was my Name?"},
|
||||||
|
]
|
||||||
|
assert len(messages) == 3
|
||||||
|
assert messages[0]["role"] == "user"
|
||||||
|
assert messages[0]["content"] == "Hello"
|
||||||
|
assert messages[1]["role"] == "assistant"
|
||||||
|
assert messages[1]["content"] == "Hello?"
|
||||||
|
assert messages[2]["role"] == "user"
|
||||||
|
assert messages[2]["content"] == "What was my Name?"
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,11 @@ async def test_stream_response_yields_events_for_text_content(monkeypatch) -> No
|
||||||
assert isinstance(completed_resp.output[0].content[0], ResponseOutputText)
|
assert isinstance(completed_resp.output[0].content[0], ResponseOutputText)
|
||||||
assert completed_resp.output[0].content[0].text == "Hello"
|
assert completed_resp.output[0].content[0].text == "Hello"
|
||||||
|
|
||||||
|
assert completed_resp.usage, "usage should not be None"
|
||||||
|
assert completed_resp.usage.input_tokens == 7
|
||||||
|
assert completed_resp.usage.output_tokens == 5
|
||||||
|
assert completed_resp.usage.total_tokens == 12
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.allow_call_model_methods
|
@pytest.mark.allow_call_model_methods
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue