Skip to content

Commit 5def693

Browse files
committed
Introduce RealtimeAgent
1 parent 815d206 commit 5def693

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed

src/agents/realtime/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .agent import RealtimeAgent, RealtimeAgentHooks, RealtimeRunHooks
2+
3+
__all__ = ["RealtimeAgent", "RealtimeAgentHooks", "RealtimeRunHooks"]

src/agents/realtime/agent.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import dataclasses
5+
import inspect
6+
from collections.abc import Awaitable
7+
from dataclasses import dataclass
8+
from typing import Any, Callable, Generic, cast
9+
10+
from ..agent import AgentBase
11+
from ..lifecycle import AgentHooksBase, RunHooksBase
12+
from ..logger import logger
13+
from ..mcp import MCPUtil
14+
from ..run_context import RunContextWrapper, TContext
15+
from ..tool import FunctionTool, Tool
16+
from ..util._types import MaybeAwaitable
17+
18+
RealtimeAgentHooks = AgentHooksBase[TContext, "RealtimeAgent[TContext]"]
19+
"""Agent hooks for `RealtimeAgent`s."""
20+
21+
RealtimeRunHooks = RunHooksBase[TContext, "RealtimeAgent[TContext]"]
22+
"""Run hooks for `RealtimeAgent`s."""
23+
24+
25+
@dataclass
26+
class RealtimeAgent(AgentBase, Generic[TContext]):
27+
"""A specialized agent instance that is meant to be used within a `RealtimeSession` to build
28+
voice agents. Due to the nature of this agent, some configuration options are not supported
29+
that are supported by regular `Agent` instances. For example:
30+
- `model` choice is not supported, as all RealtimeAgents will be handled by the same model
31+
within a `RealtimeSession`.
32+
- `modelSettings` is not supported, as all RealtimeAgents will be handled by the same model
33+
within a `RealtimeSession`.
34+
- `outputType` is not supported, as RealtimeAgents do not support structured outputs.
35+
- `toolUseBehavior` is not supported, as all RealtimeAgents will be handled by the same model
36+
within a `RealtimeSession`.
37+
- `voice` can be configured on an `Agent` level; however, it cannot be changed after the first
38+
agent within a `RealtimeSession` has spoken.
39+
40+
See `AgentBase` for base parameters that are shared with `Agent`s.
41+
"""
42+
43+
instructions: (
44+
str
45+
| Callable[
46+
[RunContextWrapper[TContext], RealtimeAgent[TContext]],
47+
MaybeAwaitable[str],
48+
]
49+
| None
50+
) = None
51+
"""The instructions for the agent. Will be used as the "system prompt" when this agent is
52+
invoked. Describes what the agent should do, and how it responds.
53+
54+
Can either be a string, or a function that dynamically generates instructions for the agent. If
55+
you provide a function, it will be called with the context and the agent instance. It must
56+
return a string.
57+
"""
58+
59+
hooks: RealtimeAgentHooks | None = None
60+
"""A class that receives callbacks on various lifecycle events for this agent.
61+
"""
62+
63+
def clone(self, **kwargs: Any) -> RealtimeAgent[TContext]:
64+
"""Make a copy of the agent, with the given arguments changed. For example, you could do:
65+
```
66+
new_agent = agent.clone(instructions="New instructions")
67+
```
68+
"""
69+
return dataclasses.replace(self, **kwargs)
70+
71+
async def get_system_prompt(self, run_context: RunContextWrapper[TContext]) -> str | None:
72+
"""Get the system prompt for the agent."""
73+
if isinstance(self.instructions, str):
74+
return self.instructions
75+
elif callable(self.instructions):
76+
if inspect.iscoroutinefunction(self.instructions):
77+
return await cast(Awaitable[str], self.instructions(run_context, self))
78+
else:
79+
return cast(str, self.instructions(run_context, self))
80+
elif self.instructions is not None:
81+
logger.error(f"Instructions must be a string or a function, got {self.instructions}")
82+
83+
return None
84+
85+
async def get_mcp_tools(self, run_context: RunContextWrapper[TContext]) -> list[Tool]:
86+
"""Fetches the available tools from the MCP servers."""
87+
convert_schemas_to_strict = self.mcp_config.get("convert_schemas_to_strict", False)
88+
return await MCPUtil.get_all_function_tools(
89+
self.mcp_servers, convert_schemas_to_strict, run_context, self
90+
)
91+
92+
async def get_all_tools(self, run_context: RunContextWrapper[Any]) -> list[Tool]:
93+
"""All agent tools, including MCP tools and function tools."""
94+
mcp_tools = await self.get_mcp_tools(run_context)
95+
96+
async def _check_tool_enabled(tool: Tool) -> bool:
97+
if not isinstance(tool, FunctionTool):
98+
return True
99+
100+
attr = tool.is_enabled
101+
if isinstance(attr, bool):
102+
return attr
103+
res = attr(run_context, self)
104+
if inspect.isawaitable(res):
105+
return bool(await res)
106+
return bool(res)
107+
108+
results = await asyncio.gather(*(_check_tool_enabled(t) for t in self.tools))
109+
enabled: list[Tool] = [t for t, ok in zip(self.tools, results) if ok]
110+
return [*mcp_tools, *enabled]

tests/realtime/__init__.py

Whitespace-only changes.

tests/realtime/test_agent.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
3+
from agents import RunContextWrapper
4+
from agents.realtime import RealtimeAgent
5+
6+
7+
def test_can_initialize_realtime_agent():
8+
agent = RealtimeAgent(name="test", instructions="Hello")
9+
assert agent.name == "test"
10+
assert agent.instructions == "Hello"
11+
12+
13+
@pytest.mark.asyncio
14+
async def test_dynamic_instructions():
15+
agent = RealtimeAgent(name="test")
16+
assert agent.instructions is None
17+
18+
def _instructions(ctx, agt) -> str:
19+
assert ctx.context is None
20+
assert agt == agent
21+
return "Dynamic"
22+
23+
agent = RealtimeAgent(name="test", instructions=_instructions)
24+
instructions = await agent.get_system_prompt(RunContextWrapper(context=None))
25+
assert instructions == "Dynamic"

0 commit comments

Comments
 (0)