Realtime: make sure we use the initial model settings (#1198)
Made a mistake here - we were ignoring the settings passed into the Runner init., and only usig the override settings passed to run()
This commit is contained in:
@@ -17,125 +17,19 @@ The OpenAI Agents SDK is a lightweight yet powerful framework for building multi
|
||||
|
||||
Explore the [examples](examples) directory to see the SDK in action, and read our [documentation](https://openai.github.io/openai-agents-python/) for more details.
|
||||
|
||||
## Sessions
|
||||
|
||||
The Agents SDK provides built-in session memory to automatically maintain conversation history across multiple agent runs, eliminating the need to manually handle `.to_input_list()` between turns.
|
||||
|
||||
### Quick start
|
||||
|
||||
```python
|
||||
from agents import Agent, Runner, SQLiteSession
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
# Create a session instance
|
||||
session = SQLiteSession("conversation_123")
|
||||
|
||||
# First turn
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session
|
||||
)
|
||||
print(result.final_output) # "San Francisco"
|
||||
|
||||
# Second turn - agent automatically remembers previous context
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What state is it in?",
|
||||
session=session
|
||||
)
|
||||
print(result.final_output) # "California"
|
||||
|
||||
# Also works with synchronous runner
|
||||
result = Runner.run_sync(
|
||||
agent,
|
||||
"What's the population?",
|
||||
session=session
|
||||
)
|
||||
print(result.final_output) # "Approximately 39 million"
|
||||
```
|
||||
|
||||
### Session options
|
||||
|
||||
- **No memory** (default): No session memory when session parameter is omitted
|
||||
- **`session: Session = DatabaseSession(...)`**: Use a Session instance to manage conversation history
|
||||
|
||||
```python
|
||||
from agents import Agent, Runner, SQLiteSession
|
||||
|
||||
# Custom SQLite database file
|
||||
session = SQLiteSession("user_123", "conversations.db")
|
||||
agent = Agent(name="Assistant")
|
||||
|
||||
# Different session IDs maintain separate conversation histories
|
||||
result1 = await Runner.run(
|
||||
agent,
|
||||
"Hello",
|
||||
session=session
|
||||
)
|
||||
result2 = await Runner.run(
|
||||
agent,
|
||||
"Hello",
|
||||
session=SQLiteSession("user_456", "conversations.db")
|
||||
)
|
||||
```
|
||||
|
||||
### Custom session implementations
|
||||
|
||||
You can implement your own session memory by creating a class that follows the `Session` protocol:
|
||||
|
||||
```python
|
||||
from agents.memory import Session
|
||||
from typing import List
|
||||
|
||||
class MyCustomSession:
|
||||
"""Custom session implementation following the Session protocol."""
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
self.session_id = session_id
|
||||
# Your initialization here
|
||||
|
||||
async def get_items(self, limit: int | None = None) -> List[dict]:
|
||||
# Retrieve conversation history for the session
|
||||
pass
|
||||
|
||||
async def add_items(self, items: List[dict]) -> None:
|
||||
# Store new items for the session
|
||||
pass
|
||||
|
||||
async def pop_item(self) -> dict | None:
|
||||
# Remove and return the most recent item from the session
|
||||
pass
|
||||
|
||||
async def clear_session(self) -> None:
|
||||
# Clear all items for the session
|
||||
pass
|
||||
|
||||
# Use your custom session
|
||||
agent = Agent(name="Assistant")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Hello",
|
||||
session=MyCustomSession("my_session")
|
||||
)
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
||||
1. Set up your Python environment
|
||||
|
||||
- Option A: Using venv (traditional method)
|
||||
- Option A: Using venv (traditional method)
|
||||
|
||||
```bash
|
||||
python -m venv env
|
||||
source env/bin/activate # On Windows: env\Scripts\activate
|
||||
```
|
||||
|
||||
- Option B: Using uv (recommended)
|
||||
- Option B: Using uv (recommended)
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
@@ -263,6 +157,114 @@ The Agents SDK is designed to be highly flexible, allowing you to model a wide r
|
||||
|
||||
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), which also includes a larger list of [external tracing processors](http://openai.github.io/openai-agents-python/tracing/#external-tracing-processors-list).
|
||||
|
||||
## Sessions
|
||||
|
||||
The Agents SDK provides built-in session memory to automatically maintain conversation history across multiple agent runs, eliminating the need to manually handle `.to_input_list()` between turns.
|
||||
|
||||
### Quick start
|
||||
|
||||
```python
|
||||
from agents import Agent, Runner, SQLiteSession
|
||||
|
||||
# Create agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
# Create a session instance
|
||||
session = SQLiteSession("conversation_123")
|
||||
|
||||
# First turn
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session
|
||||
)
|
||||
print(result.final_output) # "San Francisco"
|
||||
|
||||
# Second turn - agent automatically remembers previous context
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What state is it in?",
|
||||
session=session
|
||||
)
|
||||
print(result.final_output) # "California"
|
||||
|
||||
# Also works with synchronous runner
|
||||
result = Runner.run_sync(
|
||||
agent,
|
||||
"What's the population?",
|
||||
session=session
|
||||
)
|
||||
print(result.final_output) # "Approximately 39 million"
|
||||
```
|
||||
|
||||
### Session options
|
||||
|
||||
- **No memory** (default): No session memory when session parameter is omitted
|
||||
- **`session: Session = DatabaseSession(...)`**: Use a Session instance to manage conversation history
|
||||
|
||||
```python
|
||||
from agents import Agent, Runner, SQLiteSession
|
||||
|
||||
# Custom SQLite database file
|
||||
session = SQLiteSession("user_123", "conversations.db")
|
||||
agent = Agent(name="Assistant")
|
||||
|
||||
# Different session IDs maintain separate conversation histories
|
||||
result1 = await Runner.run(
|
||||
agent,
|
||||
"Hello",
|
||||
session=session
|
||||
)
|
||||
result2 = await Runner.run(
|
||||
agent,
|
||||
"Hello",
|
||||
session=SQLiteSession("user_456", "conversations.db")
|
||||
)
|
||||
```
|
||||
|
||||
### Custom session implementations
|
||||
|
||||
You can implement your own session memory by creating a class that follows the `Session` protocol:
|
||||
|
||||
```python
|
||||
from agents.memory import Session
|
||||
from typing import List
|
||||
|
||||
class MyCustomSession:
|
||||
"""Custom session implementation following the Session protocol."""
|
||||
|
||||
def __init__(self, session_id: str):
|
||||
self.session_id = session_id
|
||||
# Your initialization here
|
||||
|
||||
async def get_items(self, limit: int | None = None) -> List[dict]:
|
||||
# Retrieve conversation history for the session
|
||||
pass
|
||||
|
||||
async def add_items(self, items: List[dict]) -> None:
|
||||
# Store new items for the session
|
||||
pass
|
||||
|
||||
async def pop_item(self) -> dict | None:
|
||||
# Remove and return the most recent item from the session
|
||||
pass
|
||||
|
||||
async def clear_session(self) -> None:
|
||||
# Clear all items for the session
|
||||
pass
|
||||
|
||||
# Use your custom session
|
||||
agent = Agent(name="Assistant")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Hello",
|
||||
session=MyCustomSession("my_session")
|
||||
)
|
||||
```
|
||||
|
||||
## Development (only needed if you need to edit the SDK/examples)
|
||||
|
||||
0. Ensure you have [`uv`](https://docs.astral.sh/uv/) installed.
|
||||
@@ -284,6 +286,7 @@ make check # run tests linter and typechecker
|
||||
```
|
||||
|
||||
Or to run them individually:
|
||||
|
||||
```
|
||||
make tests # run tests
|
||||
make mypy # run typechecker
|
||||
|
||||
@@ -121,7 +121,8 @@ class RealtimeSession(RealtimeModelListener):
|
||||
|
||||
model_config = self._model_config.copy()
|
||||
model_config["initial_model_settings"] = await self._get_updated_model_settings_from_agent(
|
||||
self._current_agent
|
||||
starting_settings=self._model_config.get("initial_model_settings", None),
|
||||
agent=self._current_agent,
|
||||
)
|
||||
|
||||
# Connect to the model
|
||||
@@ -330,7 +331,8 @@ class RealtimeSession(RealtimeModelListener):
|
||||
|
||||
# Get updated model settings from new agent
|
||||
updated_settings = await self._get_updated_model_settings_from_agent(
|
||||
self._current_agent
|
||||
starting_settings=None,
|
||||
agent=self._current_agent,
|
||||
)
|
||||
|
||||
# Send handoff event
|
||||
@@ -509,9 +511,16 @@ class RealtimeSession(RealtimeModelListener):
|
||||
|
||||
async def _get_updated_model_settings_from_agent(
|
||||
self,
|
||||
starting_settings: RealtimeSessionModelSettings | None,
|
||||
agent: RealtimeAgent,
|
||||
) -> RealtimeSessionModelSettings:
|
||||
updated_settings: RealtimeSessionModelSettings = {}
|
||||
# Start with run config model settings as base
|
||||
run_config_settings = self._run_config.get("model_settings", {})
|
||||
updated_settings: RealtimeSessionModelSettings = run_config_settings.copy()
|
||||
# Apply starting settings (from model config) next
|
||||
if starting_settings:
|
||||
updated_settings.update(starting_settings)
|
||||
|
||||
instructions, tools, handoffs = await asyncio.gather(
|
||||
agent.get_system_prompt(self._context_wrapper),
|
||||
agent.get_all_tools(self._context_wrapper),
|
||||
@@ -521,10 +530,6 @@ class RealtimeSession(RealtimeModelListener):
|
||||
updated_settings["tools"] = tools or []
|
||||
updated_settings["handoffs"] = handoffs or []
|
||||
|
||||
# Override with initial settings
|
||||
initial_settings = self._model_config.get("initial_model_settings", {})
|
||||
updated_settings.update(initial_settings)
|
||||
|
||||
disable_tracing = self._run_config.get("tracing_disabled", False)
|
||||
if disable_tracing:
|
||||
updated_settings["tracing"] = None
|
||||
|
||||
+176
-10
@@ -6,7 +6,7 @@ import pytest
|
||||
from agents.guardrail import GuardrailFunctionOutput, OutputGuardrail
|
||||
from agents.handoffs import Handoff
|
||||
from agents.realtime.agent import RealtimeAgent
|
||||
from agents.realtime.config import RealtimeRunConfig
|
||||
from agents.realtime.config import RealtimeRunConfig, RealtimeSessionModelSettings
|
||||
from agents.realtime.events import (
|
||||
RealtimeAgentEndEvent,
|
||||
RealtimeAgentStartEvent,
|
||||
@@ -1242,8 +1242,9 @@ class TestModelSettingsIntegration:
|
||||
await session.__aexit__(None, None, None)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_config_overrides_agent_settings(self):
|
||||
"""Test that initial_model_settings from model_config override agent settings."""
|
||||
async def test_model_config_overrides_model_settings_not_agent(self):
|
||||
"""Test that initial_model_settings from model_config override model settings
|
||||
but not agent-derived settings."""
|
||||
mock_model = Mock(spec=RealtimeModel)
|
||||
mock_model.connect = AsyncMock()
|
||||
mock_model.add_listener = Mock()
|
||||
@@ -1253,10 +1254,9 @@ class TestModelSettingsIntegration:
|
||||
agent.get_all_tools = AsyncMock(return_value=[{"type": "function", "name": "agent_tool"}])
|
||||
agent.handoffs = []
|
||||
|
||||
# Provide model config with overrides
|
||||
# Provide model config with settings
|
||||
model_config: RealtimeModelConfig = {
|
||||
"initial_model_settings": {
|
||||
"instructions": "Override instructions",
|
||||
"voice": "nova",
|
||||
"model_name": "gpt-4o-realtime",
|
||||
}
|
||||
@@ -1266,16 +1266,16 @@ class TestModelSettingsIntegration:
|
||||
|
||||
await session.__aenter__()
|
||||
|
||||
# Verify overrides were applied
|
||||
# Verify model config settings were applied
|
||||
connect_config = mock_model.connect.call_args[0][0]
|
||||
initial_settings = connect_config["initial_model_settings"]
|
||||
|
||||
# Should have override values
|
||||
assert initial_settings["instructions"] == "Override instructions"
|
||||
# Agent-derived settings should come from agent
|
||||
assert initial_settings["instructions"] == "Agent instructions"
|
||||
assert initial_settings["tools"] == [{"type": "function", "name": "agent_tool"}]
|
||||
# Model config settings should be applied
|
||||
assert initial_settings["voice"] == "nova"
|
||||
assert initial_settings["model_name"] == "gpt-4o-realtime"
|
||||
# Should still have agent tools since not overridden
|
||||
assert initial_settings["tools"] == [{"type": "function", "name": "agent_tool"}]
|
||||
|
||||
await session.__aexit__(None, None, None)
|
||||
|
||||
@@ -1320,3 +1320,169 @@ class TestModelSettingsIntegration:
|
||||
assert initial_settings["handoffs"] == [mock_handoff]
|
||||
|
||||
await session.__aexit__(None, None, None)
|
||||
|
||||
|
||||
# Test: Model settings precedence
|
||||
class TestModelSettingsPrecedence:
|
||||
"""Test suite for model settings precedence in RealtimeSession"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_settings_precedence_order(self):
|
||||
"""Test that model settings follow correct precedence:
|
||||
run_config -> agent -> model_config"""
|
||||
|
||||
# Create a test agent
|
||||
agent = RealtimeAgent(name="test_agent", instructions="agent_instructions")
|
||||
agent.handoffs = []
|
||||
|
||||
# Mock the agent methods to return known values
|
||||
agent.get_system_prompt = AsyncMock(return_value="agent_system_prompt") # type: ignore
|
||||
agent.get_all_tools = AsyncMock(return_value=[]) # type: ignore
|
||||
|
||||
# Mock model
|
||||
mock_model = Mock(spec=RealtimeModel)
|
||||
mock_model.connect = AsyncMock()
|
||||
|
||||
# Define settings at each level with different values
|
||||
run_config_settings: RealtimeSessionModelSettings = {
|
||||
"voice": "run_config_voice",
|
||||
"modalities": ["text"],
|
||||
}
|
||||
|
||||
model_config_initial_settings: RealtimeSessionModelSettings = {
|
||||
"voice": "model_config_voice", # Should override run_config
|
||||
"tool_choice": "auto", # New setting not in run_config
|
||||
}
|
||||
|
||||
run_config: RealtimeRunConfig = {"model_settings": run_config_settings}
|
||||
|
||||
model_config: RealtimeModelConfig = {
|
||||
"initial_model_settings": model_config_initial_settings
|
||||
}
|
||||
|
||||
# Create session with both configs
|
||||
session = RealtimeSession(
|
||||
model=mock_model,
|
||||
agent=agent,
|
||||
context=None,
|
||||
model_config=model_config,
|
||||
run_config=run_config,
|
||||
)
|
||||
|
||||
# Mock the _get_handoffs method
|
||||
async def mock_get_handoffs(cls, agent, context_wrapper):
|
||||
return []
|
||||
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr("agents.realtime.session.RealtimeSession._get_handoffs", mock_get_handoffs)
|
||||
|
||||
# Test the method directly
|
||||
model_settings = await session._get_updated_model_settings_from_agent(
|
||||
starting_settings=model_config_initial_settings, agent=agent
|
||||
)
|
||||
|
||||
# Verify precedence order:
|
||||
# 1. Agent settings should always be set (highest precedence for these)
|
||||
assert model_settings["instructions"] == "agent_system_prompt"
|
||||
assert model_settings["tools"] == []
|
||||
assert model_settings["handoffs"] == []
|
||||
|
||||
# 2. model_config settings should override run_config settings
|
||||
assert model_settings["voice"] == "model_config_voice" # model_config wins
|
||||
|
||||
# 3. run_config settings should be preserved when not overridden
|
||||
assert model_settings["modalities"] == ["text"] # only in run_config
|
||||
|
||||
# 4. model_config-only settings should be present
|
||||
assert model_settings["tool_choice"] == "auto" # only in model_config
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_settings_with_run_config_only(self):
|
||||
"""Test that run_config model_settings are used when no model_config provided"""
|
||||
|
||||
agent = RealtimeAgent(name="test_agent", instructions="test")
|
||||
agent.handoffs = []
|
||||
agent.get_system_prompt = AsyncMock(return_value="test_prompt") # type: ignore
|
||||
agent.get_all_tools = AsyncMock(return_value=[]) # type: ignore
|
||||
|
||||
mock_model = Mock(spec=RealtimeModel)
|
||||
|
||||
run_config_settings: RealtimeSessionModelSettings = {
|
||||
"voice": "run_config_only_voice",
|
||||
"modalities": ["text", "audio"],
|
||||
"input_audio_format": "pcm16",
|
||||
}
|
||||
|
||||
session = RealtimeSession(
|
||||
model=mock_model,
|
||||
agent=agent,
|
||||
context=None,
|
||||
model_config=None, # No model config
|
||||
run_config={"model_settings": run_config_settings},
|
||||
)
|
||||
|
||||
async def mock_get_handoffs(cls, agent, context_wrapper):
|
||||
return []
|
||||
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr("agents.realtime.session.RealtimeSession._get_handoffs", mock_get_handoffs)
|
||||
|
||||
model_settings = await session._get_updated_model_settings_from_agent(
|
||||
starting_settings=None, # No initial settings
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
# Agent settings should be present
|
||||
assert model_settings["instructions"] == "test_prompt"
|
||||
assert model_settings["tools"] == []
|
||||
assert model_settings["handoffs"] == []
|
||||
|
||||
# All run_config settings should be preserved (no overrides)
|
||||
assert model_settings["voice"] == "run_config_only_voice"
|
||||
assert model_settings["modalities"] == ["text", "audio"]
|
||||
assert model_settings["input_audio_format"] == "pcm16"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_settings_with_model_config_only(self):
|
||||
"""Test that model_config settings are used when no run_config model_settings"""
|
||||
|
||||
agent = RealtimeAgent(name="test_agent", instructions="test")
|
||||
agent.handoffs = []
|
||||
agent.get_system_prompt = AsyncMock(return_value="test_prompt") # type: ignore
|
||||
agent.get_all_tools = AsyncMock(return_value=[]) # type: ignore
|
||||
|
||||
mock_model = Mock(spec=RealtimeModel)
|
||||
|
||||
model_config_settings: RealtimeSessionModelSettings = {
|
||||
"voice": "model_config_only_voice",
|
||||
"tool_choice": "required",
|
||||
"output_audio_format": "g711_ulaw",
|
||||
}
|
||||
|
||||
session = RealtimeSession(
|
||||
model=mock_model,
|
||||
agent=agent,
|
||||
context=None,
|
||||
model_config={"initial_model_settings": model_config_settings},
|
||||
run_config={}, # No model_settings in run_config
|
||||
)
|
||||
|
||||
async def mock_get_handoffs(cls, agent, context_wrapper):
|
||||
return []
|
||||
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr("agents.realtime.session.RealtimeSession._get_handoffs", mock_get_handoffs)
|
||||
|
||||
model_settings = await session._get_updated_model_settings_from_agent(
|
||||
starting_settings=model_config_settings, agent=agent
|
||||
)
|
||||
|
||||
# Agent settings should be present
|
||||
assert model_settings["instructions"] == "test_prompt"
|
||||
assert model_settings["tools"] == []
|
||||
assert model_settings["handoffs"] == []
|
||||
|
||||
# All model_config settings should be preserved
|
||||
assert model_settings["voice"] == "model_config_only_voice"
|
||||
assert model_settings["tool_choice"] == "required"
|
||||
assert model_settings["output_audio_format"] == "g711_ulaw"
|
||||
|
||||
@@ -239,7 +239,9 @@ class TestRealtimeTracingIntegration:
|
||||
)
|
||||
|
||||
# Test the _get_updated_model_settings_from_agent method directly
|
||||
model_settings = await session._get_updated_model_settings_from_agent(agent)
|
||||
model_settings = await session._get_updated_model_settings_from_agent(
|
||||
starting_settings=None, agent=agent
|
||||
)
|
||||
|
||||
# When tracing is disabled, model settings should have tracing=None
|
||||
assert model_settings["tracing"] is None
|
||||
|
||||
Reference in New Issue
Block a user