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
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ The most common properties of an agent you'll configure are:
|
|||
```python
|
||||
from agents import Agent, ModelSettings, function_tool
|
||||
|
||||
@function_tool
|
||||
def get_weather(city: str) -> str:
|
||||
return f"The weather in {city} is sunny"
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ agent = Agent(
|
|||
name="Haiku agent",
|
||||
instructions="Always respond in haiku form",
|
||||
model="o3-mini",
|
||||
tools=[function_tool(get_weather)],
|
||||
tools=[get_weather],
|
||||
)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class UserInfo: # (1)!
|
|||
name: str
|
||||
uid: int
|
||||
|
||||
@function_tool
|
||||
async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str: # (2)!
|
||||
return f"User {wrapper.context.name} is 47 years old"
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ async def main():
|
|||
|
||||
agent = Agent[UserInfo]( # (4)!
|
||||
name="Assistant",
|
||||
tools=[function_tool(fetch_user_age)],
|
||||
tools=[fetch_user_age],
|
||||
)
|
||||
|
||||
result = await Runner.run(
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ async def main():
|
|||
# San Francisco
|
||||
|
||||
# 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)
|
||||
print(result.final_output)
|
||||
# California
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ async def main():
|
|||
|
||||
with trace("Joke workflow"): # (1)!
|
||||
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"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)
|
||||
- [AgentOps](https://docs.agentops.ai/v1/integrations/agentssdk)
|
||||
- [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(
|
||||
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]],
|
||||
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
|
||||
include information about the guardrail's output.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -10,15 +10,34 @@ class ModelSettings:
|
|||
|
||||
This class holds optional model configuration parameters (e.g. temperature,
|
||||
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
|
||||
"""The temperature to use when calling the model."""
|
||||
|
||||
top_p: float | None = None
|
||||
"""The top_p to use when calling the model."""
|
||||
|
||||
frequency_penalty: float | None = None
|
||||
"""The frequency penalty to use when calling the model."""
|
||||
|
||||
presence_penalty: float | None = None
|
||||
"""The presence penalty to use when calling the model."""
|
||||
|
||||
tool_choice: Literal["auto", "required", "none"] | str | None = None
|
||||
"""The tool choice to use when calling the model."""
|
||||
|
||||
parallel_tool_calls: bool | None = False
|
||||
"""Whether to use parallel tool calls when calling the model."""
|
||||
|
||||
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:
|
||||
"""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,
|
||||
parallel_tool_calls=override.parallel_tool_calls or self.parallel_tool_calls,
|
||||
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,
|
||||
ResponseRefusalDeltaEvent,
|
||||
ResponseTextDeltaEvent,
|
||||
ResponseUsage,
|
||||
)
|
||||
from openai.types.responses.response_input_param import FunctionCallOutput, ItemReference, Message
|
||||
from openai.types.responses.response_usage import OutputTokensDetails
|
||||
|
||||
from .. import _debug
|
||||
from ..agent_output import AgentOutputSchema
|
||||
|
|
@ -405,7 +407,23 @@ class OpenAIChatCompletionsModel(Model):
|
|||
for function_call in state.function_calls.values():
|
||||
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(
|
||||
response=final_response,
|
||||
|
|
@ -503,6 +521,7 @@ class OpenAIChatCompletionsModel(Model):
|
|||
top_p=self._non_null_or_not_given(model_settings.top_p),
|
||||
frequency_penalty=self._non_null_or_not_given(model_settings.frequency_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,
|
||||
response_format=response_format,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
|
|
@ -808,6 +827,13 @@ class _Converter:
|
|||
"content": cls.extract_text_content(content),
|
||||
}
|
||||
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:
|
||||
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),
|
||||
top_p=self._non_null_or_not_given(model_settings.top_p),
|
||||
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,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
stream=stream,
|
||||
|
|
|
|||
|
|
@ -216,5 +216,3 @@ class RunResultStreaming(RunResultBase):
|
|||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -393,3 +393,38 @@ def test_unknown_object_errors():
|
|||
with pytest.raises(UserError, match="Unhandled item type or structure"):
|
||||
# Purposely ignore the type error
|
||||
_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 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.asyncio
|
||||
|
|
|
|||
Loading…
Reference in a new issue