openai-agents 0.2.8__py3-none-any.whl → 0.6.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. agents/__init__.py +105 -4
  2. agents/_debug.py +15 -4
  3. agents/_run_impl.py +1203 -96
  4. agents/agent.py +164 -19
  5. agents/apply_diff.py +329 -0
  6. agents/editor.py +47 -0
  7. agents/exceptions.py +35 -0
  8. agents/extensions/experimental/__init__.py +6 -0
  9. agents/extensions/experimental/codex/__init__.py +92 -0
  10. agents/extensions/experimental/codex/codex.py +89 -0
  11. agents/extensions/experimental/codex/codex_options.py +35 -0
  12. agents/extensions/experimental/codex/codex_tool.py +1142 -0
  13. agents/extensions/experimental/codex/events.py +162 -0
  14. agents/extensions/experimental/codex/exec.py +263 -0
  15. agents/extensions/experimental/codex/items.py +245 -0
  16. agents/extensions/experimental/codex/output_schema_file.py +50 -0
  17. agents/extensions/experimental/codex/payloads.py +31 -0
  18. agents/extensions/experimental/codex/thread.py +214 -0
  19. agents/extensions/experimental/codex/thread_options.py +54 -0
  20. agents/extensions/experimental/codex/turn_options.py +36 -0
  21. agents/extensions/handoff_filters.py +13 -1
  22. agents/extensions/memory/__init__.py +120 -0
  23. agents/extensions/memory/advanced_sqlite_session.py +1285 -0
  24. agents/extensions/memory/async_sqlite_session.py +239 -0
  25. agents/extensions/memory/dapr_session.py +423 -0
  26. agents/extensions/memory/encrypt_session.py +185 -0
  27. agents/extensions/memory/redis_session.py +261 -0
  28. agents/extensions/memory/sqlalchemy_session.py +334 -0
  29. agents/extensions/models/litellm_model.py +449 -36
  30. agents/extensions/models/litellm_provider.py +3 -1
  31. agents/function_schema.py +47 -5
  32. agents/guardrail.py +16 -2
  33. agents/{handoffs.py → handoffs/__init__.py} +89 -47
  34. agents/handoffs/history.py +268 -0
  35. agents/items.py +237 -11
  36. agents/lifecycle.py +75 -14
  37. agents/mcp/server.py +280 -37
  38. agents/mcp/util.py +24 -3
  39. agents/memory/__init__.py +22 -2
  40. agents/memory/openai_conversations_session.py +91 -0
  41. agents/memory/openai_responses_compaction_session.py +249 -0
  42. agents/memory/session.py +19 -261
  43. agents/memory/sqlite_session.py +275 -0
  44. agents/memory/util.py +20 -0
  45. agents/model_settings.py +14 -3
  46. agents/models/__init__.py +13 -0
  47. agents/models/chatcmpl_converter.py +303 -50
  48. agents/models/chatcmpl_helpers.py +63 -0
  49. agents/models/chatcmpl_stream_handler.py +290 -68
  50. agents/models/default_models.py +58 -0
  51. agents/models/interface.py +4 -0
  52. agents/models/openai_chatcompletions.py +103 -49
  53. agents/models/openai_provider.py +10 -4
  54. agents/models/openai_responses.py +162 -46
  55. agents/realtime/__init__.py +4 -0
  56. agents/realtime/_util.py +14 -3
  57. agents/realtime/agent.py +7 -0
  58. agents/realtime/audio_formats.py +53 -0
  59. agents/realtime/config.py +78 -10
  60. agents/realtime/events.py +18 -0
  61. agents/realtime/handoffs.py +2 -2
  62. agents/realtime/items.py +17 -1
  63. agents/realtime/model.py +13 -0
  64. agents/realtime/model_events.py +12 -0
  65. agents/realtime/model_inputs.py +18 -1
  66. agents/realtime/openai_realtime.py +696 -150
  67. agents/realtime/session.py +243 -23
  68. agents/repl.py +7 -3
  69. agents/result.py +197 -38
  70. agents/run.py +949 -168
  71. agents/run_context.py +13 -2
  72. agents/stream_events.py +1 -0
  73. agents/strict_schema.py +14 -0
  74. agents/tool.py +413 -15
  75. agents/tool_context.py +22 -1
  76. agents/tool_guardrails.py +279 -0
  77. agents/tracing/__init__.py +2 -0
  78. agents/tracing/config.py +9 -0
  79. agents/tracing/create.py +4 -0
  80. agents/tracing/processor_interface.py +84 -11
  81. agents/tracing/processors.py +65 -54
  82. agents/tracing/provider.py +64 -7
  83. agents/tracing/spans.py +105 -0
  84. agents/tracing/traces.py +116 -16
  85. agents/usage.py +134 -12
  86. agents/util/_json.py +19 -1
  87. agents/util/_transforms.py +12 -2
  88. agents/voice/input.py +5 -4
  89. agents/voice/models/openai_stt.py +17 -9
  90. agents/voice/pipeline.py +2 -0
  91. agents/voice/pipeline_config.py +4 -0
  92. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
  93. openai_agents-0.6.8.dist-info/RECORD +134 -0
  94. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
  95. openai_agents-0.2.8.dist-info/RECORD +0 -103
  96. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,185 @@
1
+ """Encrypted Session wrapper for secure conversation storage.
2
+
3
+ This module provides transparent encryption for session storage with automatic
4
+ expiration of old data. When TTL expires, expired items are silently skipped.
5
+
6
+ Usage::
7
+
8
+ from agents.extensions.memory import EncryptedSession, SQLAlchemySession
9
+
10
+ # Create underlying session (e.g. SQLAlchemySession)
11
+ underlying_session = SQLAlchemySession.from_url(
12
+ session_id="user-123",
13
+ url="postgresql+asyncpg://app:secret@db.example.com/agents",
14
+ create_tables=True,
15
+ )
16
+
17
+ # Wrap with encryption and TTL-based expiration
18
+ session = EncryptedSession(
19
+ session_id="user-123",
20
+ underlying_session=underlying_session,
21
+ encryption_key="your-encryption-key",
22
+ ttl=600, # 10 minutes
23
+ )
24
+
25
+ await Runner.run(agent, "Hello", session=session)
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import base64
31
+ import json
32
+ from typing import Any, cast
33
+
34
+ from cryptography.fernet import Fernet, InvalidToken
35
+ from cryptography.hazmat.primitives import hashes
36
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
37
+ from typing_extensions import Literal, TypedDict, TypeGuard
38
+
39
+ from ...items import TResponseInputItem
40
+ from ...memory.session import SessionABC
41
+
42
+
43
+ class EncryptedEnvelope(TypedDict):
44
+ """TypedDict for encrypted message envelopes stored in the underlying session."""
45
+
46
+ __enc__: Literal[1]
47
+ v: int
48
+ kid: str
49
+ payload: str
50
+
51
+
52
+ def _ensure_fernet_key_bytes(master_key: str) -> bytes:
53
+ """
54
+ Accept either a Fernet key (urlsafe-b64, 32 bytes after decode) or a raw string.
55
+ Returns raw bytes suitable for HKDF input.
56
+ """
57
+ if not master_key:
58
+ raise ValueError("encryption_key not set; required for EncryptedSession.")
59
+ try:
60
+ key_bytes = base64.urlsafe_b64decode(master_key)
61
+ if len(key_bytes) == 32:
62
+ return key_bytes
63
+ except Exception:
64
+ pass
65
+ return master_key.encode("utf-8")
66
+
67
+
68
+ def _derive_session_fernet_key(master_key_bytes: bytes, session_id: str) -> Fernet:
69
+ hkdf = HKDF(
70
+ algorithm=hashes.SHA256(),
71
+ length=32,
72
+ salt=session_id.encode("utf-8"),
73
+ info=b"agents.session-store.hkdf.v1",
74
+ )
75
+ derived = hkdf.derive(master_key_bytes)
76
+ return Fernet(base64.urlsafe_b64encode(derived))
77
+
78
+
79
+ def _to_json_bytes(obj: Any) -> bytes:
80
+ return json.dumps(obj, ensure_ascii=False, separators=(",", ":"), default=str).encode("utf-8")
81
+
82
+
83
+ def _from_json_bytes(data: bytes) -> Any:
84
+ return json.loads(data.decode("utf-8"))
85
+
86
+
87
+ def _is_encrypted_envelope(item: object) -> TypeGuard[EncryptedEnvelope]:
88
+ """Type guard to check if an item is an encrypted envelope."""
89
+ return (
90
+ isinstance(item, dict)
91
+ and item.get("__enc__") == 1
92
+ and "payload" in item
93
+ and "kid" in item
94
+ and "v" in item
95
+ )
96
+
97
+
98
+ class EncryptedSession(SessionABC):
99
+ """Encrypted wrapper for Session implementations with TTL-based expiration.
100
+
101
+ This class wraps any SessionABC implementation to provide transparent
102
+ encryption/decryption of stored items using Fernet encryption with
103
+ per-session key derivation and automatic expiration of old data.
104
+
105
+ When items expire (exceed TTL), they are silently skipped during retrieval.
106
+
107
+ Note: Expired tokens are rejected based on the system clock of the application server.
108
+ To avoid valid tokens being rejected due to clock drift, ensure all servers in
109
+ your environment are synchronized using NTP.
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ session_id: str,
115
+ underlying_session: SessionABC,
116
+ encryption_key: str,
117
+ ttl: int = 600,
118
+ ):
119
+ """
120
+ Args:
121
+ session_id: ID for this session
122
+ underlying_session: The real session store (e.g. SQLiteSession, SQLAlchemySession)
123
+ encryption_key: Master key (Fernet key or raw secret)
124
+ ttl: Token time-to-live in seconds (default 10 min)
125
+ """
126
+ self.session_id = session_id
127
+ self.underlying_session = underlying_session
128
+ self.ttl = ttl
129
+
130
+ master = _ensure_fernet_key_bytes(encryption_key)
131
+ self.cipher = _derive_session_fernet_key(master, session_id)
132
+ self._kid = "hkdf-v1"
133
+ self._ver = 1
134
+
135
+ def __getattr__(self, name):
136
+ return getattr(self.underlying_session, name)
137
+
138
+ def _wrap(self, item: TResponseInputItem) -> EncryptedEnvelope:
139
+ if isinstance(item, dict):
140
+ payload = item
141
+ elif hasattr(item, "model_dump"):
142
+ payload = item.model_dump()
143
+ elif hasattr(item, "__dict__"):
144
+ payload = item.__dict__
145
+ else:
146
+ payload = dict(item)
147
+
148
+ token = self.cipher.encrypt(_to_json_bytes(payload)).decode("utf-8")
149
+ return {"__enc__": 1, "v": self._ver, "kid": self._kid, "payload": token}
150
+
151
+ def _unwrap(self, item: TResponseInputItem | EncryptedEnvelope) -> TResponseInputItem | None:
152
+ if not _is_encrypted_envelope(item):
153
+ return cast(TResponseInputItem, item)
154
+
155
+ try:
156
+ token = item["payload"].encode("utf-8")
157
+ plaintext = self.cipher.decrypt(token, ttl=self.ttl)
158
+ return cast(TResponseInputItem, _from_json_bytes(plaintext))
159
+ except (InvalidToken, KeyError):
160
+ return None
161
+
162
+ async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]:
163
+ encrypted_items = await self.underlying_session.get_items(limit)
164
+ valid_items: list[TResponseInputItem] = []
165
+ for enc in encrypted_items:
166
+ item = self._unwrap(enc)
167
+ if item is not None:
168
+ valid_items.append(item)
169
+ return valid_items
170
+
171
+ async def add_items(self, items: list[TResponseInputItem]) -> None:
172
+ wrapped: list[EncryptedEnvelope] = [self._wrap(it) for it in items]
173
+ await self.underlying_session.add_items(cast(list[TResponseInputItem], wrapped))
174
+
175
+ async def pop_item(self) -> TResponseInputItem | None:
176
+ while True:
177
+ enc = await self.underlying_session.pop_item()
178
+ if not enc:
179
+ return None
180
+ item = self._unwrap(enc)
181
+ if item is not None:
182
+ return item
183
+
184
+ async def clear_session(self) -> None:
185
+ await self.underlying_session.clear_session()
@@ -0,0 +1,261 @@
1
+ """Redis-powered Session backend.
2
+
3
+ Usage::
4
+
5
+ from agents.extensions.memory import RedisSession
6
+
7
+ # Create from Redis URL
8
+ session = RedisSession.from_url(
9
+ session_id="user-123",
10
+ url="redis://localhost:6379/0",
11
+ )
12
+
13
+ # Or pass an existing Redis client that your application already manages
14
+ session = RedisSession(
15
+ session_id="user-123",
16
+ redis_client=my_redis_client,
17
+ )
18
+
19
+ await Runner.run(agent, "Hello", session=session)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import json
26
+ import time
27
+ from typing import Any
28
+
29
+ try:
30
+ import redis.asyncio as redis
31
+ from redis.asyncio import Redis
32
+ except ImportError as e:
33
+ raise ImportError(
34
+ "RedisSession requires the 'redis' package. Install it with: pip install redis"
35
+ ) from e
36
+
37
+ from ...items import TResponseInputItem
38
+ from ...memory.session import SessionABC
39
+
40
+
41
+ class RedisSession(SessionABC):
42
+ """Redis implementation of :pyclass:`agents.memory.session.Session`."""
43
+
44
+ def __init__(
45
+ self,
46
+ session_id: str,
47
+ *,
48
+ redis_client: Redis,
49
+ key_prefix: str = "agents:session",
50
+ ttl: int | None = None,
51
+ ):
52
+ """Initializes a new RedisSession.
53
+
54
+ Args:
55
+ session_id (str): Unique identifier for the conversation.
56
+ redis_client (Redis[bytes]): A pre-configured Redis async client.
57
+ key_prefix (str, optional): Prefix for Redis keys to avoid collisions.
58
+ Defaults to "agents:session".
59
+ ttl (int | None, optional): Time-to-live in seconds for session data.
60
+ If None, data persists indefinitely. Defaults to None.
61
+ """
62
+ self.session_id = session_id
63
+ self._redis = redis_client
64
+ self._key_prefix = key_prefix
65
+ self._ttl = ttl
66
+ self._lock = asyncio.Lock()
67
+ self._owns_client = False # Track if we own the Redis client
68
+
69
+ # Redis key patterns
70
+ self._session_key = f"{self._key_prefix}:{self.session_id}"
71
+ self._messages_key = f"{self._session_key}:messages"
72
+ self._counter_key = f"{self._session_key}:counter"
73
+
74
+ @classmethod
75
+ def from_url(
76
+ cls,
77
+ session_id: str,
78
+ *,
79
+ url: str,
80
+ redis_kwargs: dict[str, Any] | None = None,
81
+ **kwargs: Any,
82
+ ) -> RedisSession:
83
+ """Create a session from a Redis URL string.
84
+
85
+ Args:
86
+ session_id (str): Conversation ID.
87
+ url (str): Redis URL, e.g. "redis://localhost:6379/0" or "rediss://host:6380".
88
+ redis_kwargs (dict[str, Any] | None): Additional keyword arguments forwarded to
89
+ redis.asyncio.from_url.
90
+ **kwargs: Additional keyword arguments forwarded to the main constructor
91
+ (e.g., key_prefix, ttl, etc.).
92
+
93
+ Returns:
94
+ RedisSession: An instance of RedisSession connected to the specified Redis server.
95
+ """
96
+ redis_kwargs = redis_kwargs or {}
97
+
98
+ redis_client = redis.from_url(url, **redis_kwargs)
99
+ session = cls(session_id, redis_client=redis_client, **kwargs)
100
+ session._owns_client = True # We created the client, so we own it
101
+ return session
102
+
103
+ async def _serialize_item(self, item: TResponseInputItem) -> str:
104
+ """Serialize an item to JSON string. Can be overridden by subclasses."""
105
+ return json.dumps(item, separators=(",", ":"))
106
+
107
+ async def _deserialize_item(self, item: str) -> TResponseInputItem:
108
+ """Deserialize a JSON string to an item. Can be overridden by subclasses."""
109
+ return json.loads(item) # type: ignore[no-any-return] # json.loads returns Any but we know the structure
110
+
111
+ async def _get_next_id(self) -> int:
112
+ """Get the next message ID using Redis INCR for atomic increment."""
113
+ result = await self._redis.incr(self._counter_key)
114
+ return int(result)
115
+
116
+ async def _set_ttl_if_configured(self, *keys: str) -> None:
117
+ """Set TTL on keys if configured."""
118
+ if self._ttl is not None:
119
+ pipe = self._redis.pipeline()
120
+ for key in keys:
121
+ pipe.expire(key, self._ttl)
122
+ await pipe.execute()
123
+
124
+ # ------------------------------------------------------------------
125
+ # Session protocol implementation
126
+ # ------------------------------------------------------------------
127
+
128
+ async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]:
129
+ """Retrieve the conversation history for this session.
130
+
131
+ Args:
132
+ limit: Maximum number of items to retrieve. If None, retrieves all items.
133
+ When specified, returns the latest N items in chronological order.
134
+
135
+ Returns:
136
+ List of input items representing the conversation history
137
+ """
138
+ async with self._lock:
139
+ if limit is None:
140
+ # Get all messages in chronological order
141
+ raw_messages = await self._redis.lrange(self._messages_key, 0, -1) # type: ignore[misc] # Redis library returns Union[Awaitable[T], T] in async context
142
+ else:
143
+ if limit <= 0:
144
+ return []
145
+ # Get the latest N messages (Redis list is ordered chronologically)
146
+ # Use negative indices to get from the end - Redis uses -N to -1 for last N items
147
+ raw_messages = await self._redis.lrange(self._messages_key, -limit, -1) # type: ignore[misc] # Redis library returns Union[Awaitable[T], T] in async context
148
+
149
+ items: list[TResponseInputItem] = []
150
+ for raw_msg in raw_messages:
151
+ try:
152
+ # Handle both bytes (default) and str (decode_responses=True) Redis clients
153
+ if isinstance(raw_msg, bytes):
154
+ msg_str = raw_msg.decode("utf-8")
155
+ else:
156
+ msg_str = raw_msg # Already a string
157
+ item = await self._deserialize_item(msg_str)
158
+ items.append(item)
159
+ except (json.JSONDecodeError, UnicodeDecodeError):
160
+ # Skip corrupted messages
161
+ continue
162
+
163
+ return items
164
+
165
+ async def add_items(self, items: list[TResponseInputItem]) -> None:
166
+ """Add new items to the conversation history.
167
+
168
+ Args:
169
+ items: List of input items to add to the history
170
+ """
171
+ if not items:
172
+ return
173
+
174
+ async with self._lock:
175
+ pipe = self._redis.pipeline()
176
+
177
+ # Set session metadata with current timestamp
178
+ pipe.hset(
179
+ self._session_key,
180
+ mapping={
181
+ "session_id": self.session_id,
182
+ "created_at": str(int(time.time())),
183
+ "updated_at": str(int(time.time())),
184
+ },
185
+ )
186
+
187
+ # Add all items to the messages list
188
+ serialized_items = []
189
+ for item in items:
190
+ serialized = await self._serialize_item(item)
191
+ serialized_items.append(serialized)
192
+
193
+ if serialized_items:
194
+ pipe.rpush(self._messages_key, *serialized_items)
195
+
196
+ # Update the session timestamp
197
+ pipe.hset(self._session_key, "updated_at", str(int(time.time())))
198
+
199
+ # Execute all commands
200
+ await pipe.execute()
201
+
202
+ # Set TTL if configured
203
+ await self._set_ttl_if_configured(
204
+ self._session_key, self._messages_key, self._counter_key
205
+ )
206
+
207
+ async def pop_item(self) -> TResponseInputItem | None:
208
+ """Remove and return the most recent item from the session.
209
+
210
+ Returns:
211
+ The most recent item if it exists, None if the session is empty
212
+ """
213
+ async with self._lock:
214
+ # Use RPOP to atomically remove and return the rightmost (most recent) item
215
+ raw_msg = await self._redis.rpop(self._messages_key) # type: ignore[misc] # Redis library returns Union[Awaitable[T], T] in async context
216
+
217
+ if raw_msg is None:
218
+ return None
219
+
220
+ try:
221
+ # Handle both bytes (default) and str (decode_responses=True) Redis clients
222
+ if isinstance(raw_msg, bytes):
223
+ msg_str = raw_msg.decode("utf-8")
224
+ else:
225
+ msg_str = raw_msg # Already a string
226
+ return await self._deserialize_item(msg_str)
227
+ except (json.JSONDecodeError, UnicodeDecodeError):
228
+ # Return None for corrupted messages (already removed)
229
+ return None
230
+
231
+ async def clear_session(self) -> None:
232
+ """Clear all items for this session."""
233
+ async with self._lock:
234
+ # Delete all keys associated with this session
235
+ await self._redis.delete(
236
+ self._session_key,
237
+ self._messages_key,
238
+ self._counter_key,
239
+ )
240
+
241
+ async def close(self) -> None:
242
+ """Close the Redis connection.
243
+
244
+ Only closes the connection if this session owns the Redis client
245
+ (i.e., created via from_url). If the client was injected externally,
246
+ the caller is responsible for managing its lifecycle.
247
+ """
248
+ if self._owns_client:
249
+ await self._redis.aclose()
250
+
251
+ async def ping(self) -> bool:
252
+ """Test Redis connectivity.
253
+
254
+ Returns:
255
+ True if Redis is reachable, False otherwise.
256
+ """
257
+ try:
258
+ await self._redis.ping() # type: ignore[misc] # Redis library returns Union[Awaitable[T], T] in async context
259
+ return True
260
+ except Exception:
261
+ return False