Skip to content

Commit 7fe0da0

Browse files
authored
Merge branch 'main' into codex/add-nest_handoff_history-field-and-functionality
2 parents 6590319 + 05dc79d commit 7fe0da0

29 files changed

+4305
-2145
lines changed

examples/realtime/twilio/twilio_handler.py

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ def get_current_time() -> str:
3434

3535
agent = RealtimeAgent(
3636
name="Twilio Assistant",
37-
instructions="You are a helpful assistant that starts every conversation with a creative greeting. Keep responses concise and friendly since this is a phone conversation.",
37+
instructions=(
38+
"You are a helpful assistant that starts every conversation with a creative greeting. "
39+
"Keep responses concise and friendly since this is a phone conversation."
40+
),
3841
tools=[get_weather, get_current_time],
3942
)
4043

@@ -46,21 +49,39 @@ def __init__(self, twilio_websocket: WebSocket):
4649
self.session: RealtimeSession | None = None
4750
self.playback_tracker = RealtimePlaybackTracker()
4851

49-
# Audio buffering configuration (matching CLI demo)
50-
self.CHUNK_LENGTH_S = 0.05 # 50ms chunks like CLI demo
51-
self.SAMPLE_RATE = 8000 # Twilio uses 8kHz for g711_ulaw
52-
self.BUFFER_SIZE_BYTES = int(self.SAMPLE_RATE * self.CHUNK_LENGTH_S) # 50ms worth of audio
52+
# Audio chunking (matches CLI demo)
53+
self.CHUNK_LENGTH_S = 0.05 # 50ms chunks
54+
self.SAMPLE_RATE = 8000 # Twilio g711_ulaw at 8kHz
55+
self.BUFFER_SIZE_BYTES = int(self.SAMPLE_RATE * self.CHUNK_LENGTH_S) # ~400 bytes per 50ms
5356

5457
self._stream_sid: str | None = None
5558
self._audio_buffer: bytearray = bytearray()
5659
self._last_buffer_send_time = time.time()
5760

58-
# Mark event tracking for playback
61+
# Playback tracking for outbound audio
5962
self._mark_counter = 0
6063
self._mark_data: dict[
6164
str, tuple[str, int, int]
6265
] = {} # mark_id -> (item_id, content_index, byte_count)
6366

67+
# ---- Deterministic startup warm-up (preferred over sleep) ----
68+
# Buffer the first N chunks before sending to OpenAI; then mark warmed.
69+
try:
70+
self.STARTUP_BUFFER_CHUNKS = max(0, int(os.getenv("TWILIO_STARTUP_BUFFER_CHUNKS", "3")))
71+
except Exception:
72+
self.STARTUP_BUFFER_CHUNKS = 3
73+
74+
self._startup_buffer = bytearray()
75+
self._startup_warmed = (
76+
self.STARTUP_BUFFER_CHUNKS == 0
77+
) # if 0, considered warmed immediately
78+
79+
# Optional delay (defaults 0.0 because buffering is preferred)
80+
try:
81+
self.STARTUP_DELAY_S = float(os.getenv("TWILIO_STARTUP_DELAY_S", "0.0"))
82+
except Exception:
83+
self.STARTUP_DELAY_S = 0.0
84+
6485
async def start(self) -> None:
6586
"""Start the session."""
6687
runner = RealtimeRunner(agent)
@@ -89,6 +110,11 @@ async def start(self) -> None:
89110
await self.twilio_websocket.accept()
90111
print("Twilio WebSocket connection accepted")
91112

113+
# Optional tiny delay (kept configurable; default 0.0)
114+
if self.STARTUP_DELAY_S > 0:
115+
await asyncio.sleep(self.STARTUP_DELAY_S)
116+
117+
# Start loops after handshake
92118
self._realtime_session_task = asyncio.create_task(self._realtime_session_loop())
93119
self._message_loop_task = asyncio.create_task(self._twilio_message_loop())
94120
self._buffer_flush_task = asyncio.create_task(self._buffer_flush_loop())
@@ -197,7 +223,7 @@ async def _handle_media_event(self, message: dict[str, Any]) -> None:
197223
# Add original µ-law to buffer for OpenAI (they expect µ-law)
198224
self._audio_buffer.extend(ulaw_bytes)
199225

200-
# Send buffered audio if we have enough data
226+
# Send buffered audio if we have enough data for one chunk
201227
if len(self._audio_buffer) >= self.BUFFER_SIZE_BYTES:
202228
await self._flush_audio_buffer()
203229

@@ -210,47 +236,55 @@ async def _handle_mark_event(self, message: dict[str, Any]) -> None:
210236
mark_data = message.get("mark", {})
211237
mark_id = mark_data.get("name", "")
212238

213-
# Look up stored data for this mark ID
214239
if mark_id in self._mark_data:
215240
item_id, item_content_index, byte_count = self._mark_data[mark_id]
216-
217-
# Convert byte count back to bytes for playback tracker
218-
audio_bytes = b"\x00" * byte_count # Placeholder bytes
219-
220-
# Update playback tracker
241+
audio_bytes = b"\x00" * byte_count # Placeholder bytes for tracker
221242
self.playback_tracker.on_play_bytes(item_id, item_content_index, audio_bytes)
222243
print(
223244
f"Playback tracker updated: {item_id}, index {item_content_index}, {byte_count} bytes"
224245
)
225-
226-
# Clean up the stored data
227246
del self._mark_data[mark_id]
228247

229248
except Exception as e:
230249
print(f"Error handling mark event: {e}")
231250

232251
async def _flush_audio_buffer(self) -> None:
233-
"""Send buffered audio to OpenAI."""
252+
"""Send buffered audio to OpenAI with deterministic startup warm-up."""
234253
if not self._audio_buffer or not self.session:
235254
return
236255

237256
try:
238-
# Send the buffered audio
239257
buffer_data = bytes(self._audio_buffer)
240-
await self.session.send_audio(buffer_data)
241-
242-
# Clear the buffer
243258
self._audio_buffer.clear()
244259
self._last_buffer_send_time = time.time()
245260

261+
# During startup, accumulate first N chunks before sending anything
262+
if not self._startup_warmed:
263+
self._startup_buffer.extend(buffer_data)
264+
265+
# target bytes = N chunks * bytes-per-chunk
266+
target_bytes = self.BUFFER_SIZE_BYTES * max(0, self.STARTUP_BUFFER_CHUNKS)
267+
268+
if len(self._startup_buffer) >= target_bytes:
269+
# Warm-up complete: flush all buffered data in order
270+
await self.session.send_audio(bytes(self._startup_buffer))
271+
self._startup_buffer.clear()
272+
self._startup_warmed = True
273+
else:
274+
# Not enough yet; keep buffering and return
275+
return
276+
else:
277+
# Already warmed: send immediately
278+
await self.session.send_audio(buffer_data)
279+
246280
except Exception as e:
247281
print(f"Error sending buffered audio to OpenAI: {e}")
248282

249283
async def _buffer_flush_loop(self) -> None:
250284
"""Periodically flush audio buffer to prevent stale data."""
251285
try:
252286
while True:
253-
await asyncio.sleep(self.CHUNK_LENGTH_S) # Check every 50ms
287+
await asyncio.sleep(self.CHUNK_LENGTH_S) # check every 50ms
254288

255289
# If buffer has data and it's been too long since last send, flush it
256290
current_time = time.time()

examples/tools/apply_patch.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import argparse
2+
import asyncio
3+
import hashlib
4+
import os
5+
import tempfile
6+
from pathlib import Path
7+
8+
from agents import Agent, ApplyPatchTool, ModelSettings, Runner, apply_diff, trace
9+
from agents.editor import ApplyPatchOperation, ApplyPatchResult
10+
11+
12+
class ApprovalTracker:
13+
def __init__(self) -> None:
14+
self._approved: set[str] = set()
15+
16+
def fingerprint(self, operation: ApplyPatchOperation, relative_path: str) -> str:
17+
hasher = hashlib.sha256()
18+
hasher.update(operation.type.encode("utf-8"))
19+
hasher.update(b"\0")
20+
hasher.update(relative_path.encode("utf-8"))
21+
hasher.update(b"\0")
22+
hasher.update((operation.diff or "").encode("utf-8"))
23+
return hasher.hexdigest()
24+
25+
def remember(self, fingerprint: str) -> None:
26+
self._approved.add(fingerprint)
27+
28+
def is_approved(self, fingerprint: str) -> bool:
29+
return fingerprint in self._approved
30+
31+
32+
class WorkspaceEditor:
33+
def __init__(self, root: Path, approvals: ApprovalTracker, auto_approve: bool) -> None:
34+
self._root = root.resolve()
35+
self._approvals = approvals
36+
self._auto_approve = auto_approve or os.environ.get("APPLY_PATCH_AUTO_APPROVE") == "1"
37+
38+
def create_file(self, operation: ApplyPatchOperation) -> ApplyPatchResult:
39+
relative = self._relative_path(operation.path)
40+
self._require_approval(operation, relative)
41+
target = self._resolve(operation.path, ensure_parent=True)
42+
diff = operation.diff or ""
43+
content = apply_diff("", diff, mode="create")
44+
target.write_text(content, encoding="utf-8")
45+
return ApplyPatchResult(output=f"Created {relative}")
46+
47+
def update_file(self, operation: ApplyPatchOperation) -> ApplyPatchResult:
48+
relative = self._relative_path(operation.path)
49+
self._require_approval(operation, relative)
50+
target = self._resolve(operation.path)
51+
original = target.read_text(encoding="utf-8")
52+
diff = operation.diff or ""
53+
patched = apply_diff(original, diff)
54+
target.write_text(patched, encoding="utf-8")
55+
return ApplyPatchResult(output=f"Updated {relative}")
56+
57+
def delete_file(self, operation: ApplyPatchOperation) -> ApplyPatchResult:
58+
relative = self._relative_path(operation.path)
59+
self._require_approval(operation, relative)
60+
target = self._resolve(operation.path)
61+
target.unlink(missing_ok=True)
62+
return ApplyPatchResult(output=f"Deleted {relative}")
63+
64+
def _relative_path(self, value: str) -> str:
65+
resolved = self._resolve(value)
66+
return resolved.relative_to(self._root).as_posix()
67+
68+
def _resolve(self, relative: str, ensure_parent: bool = False) -> Path:
69+
candidate = Path(relative)
70+
target = candidate if candidate.is_absolute() else (self._root / candidate)
71+
target = target.resolve()
72+
try:
73+
target.relative_to(self._root)
74+
except ValueError:
75+
raise RuntimeError(f"Operation outside workspace: {relative}") from None
76+
if ensure_parent:
77+
target.parent.mkdir(parents=True, exist_ok=True)
78+
return target
79+
80+
def _require_approval(self, operation: ApplyPatchOperation, display_path: str) -> None:
81+
fingerprint = self._approvals.fingerprint(operation, display_path)
82+
if self._auto_approve or self._approvals.is_approved(fingerprint):
83+
self._approvals.remember(fingerprint)
84+
return
85+
86+
print("\n[apply_patch] approval required")
87+
print(f"- type: {operation.type}")
88+
print(f"- path: {display_path}")
89+
if operation.diff:
90+
preview = operation.diff if len(operation.diff) < 400 else f"{operation.diff[:400]}…"
91+
print("- diff preview:\n", preview)
92+
answer = input("Proceed? [y/N] ").strip().lower()
93+
if answer not in {"y", "yes"}:
94+
raise RuntimeError("Apply patch operation rejected by user.")
95+
self._approvals.remember(fingerprint)
96+
97+
98+
async def main(auto_approve: bool, model: str) -> None:
99+
with trace("apply_patch_example"):
100+
with tempfile.TemporaryDirectory(prefix="apply-patch-example-") as workspace:
101+
workspace_path = Path(workspace).resolve()
102+
approvals = ApprovalTracker()
103+
editor = WorkspaceEditor(workspace_path, approvals, auto_approve)
104+
tool = ApplyPatchTool(editor=editor)
105+
previous_response_id: str | None = None
106+
107+
agent = Agent(
108+
name="Patch Assistant",
109+
model=model,
110+
instructions=(
111+
f"You can edit files inside {workspace_path} using the apply_patch tool. "
112+
"When modifying an existing file, include the file contents between "
113+
"<BEGIN_FILES> and <END_FILES> in your prompt."
114+
),
115+
tools=[tool],
116+
model_settings=ModelSettings(tool_choice="required"),
117+
)
118+
119+
print(f"[info] Workspace root: {workspace_path}")
120+
print(f"[info] Using model: {model}")
121+
print("[run] Creating tasks.md")
122+
result = await Runner.run(
123+
agent,
124+
"Create tasks.md with a shopping checklist of 5 entries.",
125+
previous_response_id=previous_response_id,
126+
)
127+
previous_response_id = result.last_response_id
128+
print(f"[run] Final response #1:\n{result.final_output}\n")
129+
notes_path = workspace_path / "tasks.md"
130+
if not notes_path.exists():
131+
raise RuntimeError(f"{notes_path} was not created by the apply_patch tool.")
132+
updated_notes = notes_path.read_text(encoding="utf-8")
133+
print("[file] tasks.md after creation:\n")
134+
print(updated_notes)
135+
136+
prompt = (
137+
"<BEGIN_FILES>\n"
138+
f"===== tasks.md\n{updated_notes}\n"
139+
"<END_FILES>\n"
140+
"Check off the last two items from the file."
141+
)
142+
print("\n[run] Updating tasks.md")
143+
result2 = await Runner.run(
144+
agent,
145+
prompt,
146+
previous_response_id=previous_response_id,
147+
)
148+
print(f"[run] Final response #2:\n{result2.final_output}\n")
149+
if not notes_path.exists():
150+
raise RuntimeError("tasks.md vanished unexpectedly before the second read.")
151+
print("[file] Final tasks.md:\n")
152+
print(notes_path.read_text(encoding="utf-8"))
153+
154+
155+
if __name__ == "__main__":
156+
parser = argparse.ArgumentParser()
157+
parser.add_argument(
158+
"--auto-approve",
159+
action="store_true",
160+
default=False,
161+
help="Skip manual confirmations for apply_patch operations.",
162+
)
163+
parser.add_argument(
164+
"--model",
165+
default="gpt-5.1",
166+
help="Model ID to use for the agent.",
167+
)
168+
args = parser.parse_args()
169+
asyncio.run(main(args.auto_approve, args.model))

examples/tools/code_interpreter.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import asyncio
2+
from collections.abc import Mapping
3+
from typing import Any
24

35
from agents import Agent, CodeInterpreterTool, Runner, trace
46

57

8+
def _get_field(obj: Any, key: str) -> Any:
9+
if isinstance(obj, Mapping):
10+
return obj.get(key)
11+
return getattr(obj, key, None)
12+
13+
614
async def main():
715
agent = Agent(
816
name="Code interpreter",
@@ -21,14 +29,19 @@ async def main():
2129
print("Solving math problem...")
2230
result = Runner.run_streamed(agent, "What is the square root of273 * 312821 plus 1782?")
2331
async for event in result.stream_events():
24-
if (
25-
event.type == "run_item_stream_event"
26-
and event.item.type == "tool_call_item"
27-
and event.item.raw_item.type == "code_interpreter_call"
28-
):
29-
print(f"Code interpreter code:\n```\n{event.item.raw_item.code}\n```\n")
30-
elif event.type == "run_item_stream_event":
31-
print(f"Other event: {event.item.type}")
32+
if event.type != "run_item_stream_event":
33+
continue
34+
35+
item = event.item
36+
if item.type == "tool_call_item":
37+
raw_call = item.raw_item
38+
if _get_field(raw_call, "type") == "code_interpreter_call":
39+
code = _get_field(raw_call, "code")
40+
if isinstance(code, str):
41+
print(f"Code interpreter code:\n```\n{code}\n```\n")
42+
continue
43+
44+
print(f"Other event: {event.item.type}")
3245

3346
print(f"Final output: {result.final_output}")
3447

0 commit comments

Comments
 (0)