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,334 @@
1
+ """SQLAlchemy-powered Session backend.
2
+
3
+ Usage::
4
+
5
+ from agents.extensions.memory import SQLAlchemySession
6
+
7
+ # Create from SQLAlchemy URL (uses asyncpg driver under the hood for Postgres)
8
+ session = SQLAlchemySession.from_url(
9
+ session_id="user-123",
10
+ url="postgresql+asyncpg://app:secret@db.example.com/agents",
11
+ create_tables=True, # If you want to auto-create tables, set to True.
12
+ )
13
+
14
+ # Or pass an existing AsyncEngine that your application already manages
15
+ session = SQLAlchemySession(
16
+ session_id="user-123",
17
+ engine=my_async_engine,
18
+ create_tables=True, # If you want to auto-create tables, set to True.
19
+ )
20
+
21
+ await Runner.run(agent, "Hello", session=session)
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ import json
28
+ from typing import Any
29
+
30
+ from sqlalchemy import (
31
+ TIMESTAMP,
32
+ Column,
33
+ ForeignKey,
34
+ Index,
35
+ Integer,
36
+ MetaData,
37
+ String,
38
+ Table,
39
+ Text,
40
+ delete,
41
+ insert,
42
+ select,
43
+ text as sql_text,
44
+ update,
45
+ )
46
+ from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
47
+
48
+ from ...items import TResponseInputItem
49
+ from ...memory.session import SessionABC
50
+
51
+
52
+ class SQLAlchemySession(SessionABC):
53
+ """SQLAlchemy implementation of :pyclass:`agents.memory.session.Session`."""
54
+
55
+ _metadata: MetaData
56
+ _sessions: Table
57
+ _messages: Table
58
+
59
+ def __init__(
60
+ self,
61
+ session_id: str,
62
+ *,
63
+ engine: AsyncEngine,
64
+ create_tables: bool = False,
65
+ sessions_table: str = "agent_sessions",
66
+ messages_table: str = "agent_messages",
67
+ ):
68
+ """Initializes a new SQLAlchemySession.
69
+
70
+ Args:
71
+ session_id (str): Unique identifier for the conversation.
72
+ engine (AsyncEngine): A pre-configured SQLAlchemy async engine. The engine
73
+ must be created with an async driver (e.g., 'postgresql+asyncpg://',
74
+ 'mysql+aiomysql://', or 'sqlite+aiosqlite://').
75
+ create_tables (bool, optional): Whether to automatically create the required
76
+ tables and indexes. Defaults to False for production use. Set to True for
77
+ development and testing when migrations aren't used.
78
+ sessions_table (str, optional): Override the default table name for sessions if needed.
79
+ messages_table (str, optional): Override the default table name for messages if needed.
80
+ """
81
+ self.session_id = session_id
82
+ self._engine = engine
83
+ self._lock = asyncio.Lock()
84
+
85
+ self._metadata = MetaData()
86
+ self._sessions = Table(
87
+ sessions_table,
88
+ self._metadata,
89
+ Column("session_id", String, primary_key=True),
90
+ Column(
91
+ "created_at",
92
+ TIMESTAMP(timezone=False),
93
+ server_default=sql_text("CURRENT_TIMESTAMP"),
94
+ nullable=False,
95
+ ),
96
+ Column(
97
+ "updated_at",
98
+ TIMESTAMP(timezone=False),
99
+ server_default=sql_text("CURRENT_TIMESTAMP"),
100
+ onupdate=sql_text("CURRENT_TIMESTAMP"),
101
+ nullable=False,
102
+ ),
103
+ )
104
+
105
+ self._messages = Table(
106
+ messages_table,
107
+ self._metadata,
108
+ Column("id", Integer, primary_key=True, autoincrement=True),
109
+ Column(
110
+ "session_id",
111
+ String,
112
+ ForeignKey(f"{sessions_table}.session_id", ondelete="CASCADE"),
113
+ nullable=False,
114
+ ),
115
+ Column("message_data", Text, nullable=False),
116
+ Column(
117
+ "created_at",
118
+ TIMESTAMP(timezone=False),
119
+ server_default=sql_text("CURRENT_TIMESTAMP"),
120
+ nullable=False,
121
+ ),
122
+ Index(
123
+ f"idx_{messages_table}_session_time",
124
+ "session_id",
125
+ "created_at",
126
+ ),
127
+ sqlite_autoincrement=True,
128
+ )
129
+
130
+ # Async session factory
131
+ self._session_factory = async_sessionmaker(self._engine, expire_on_commit=False)
132
+
133
+ self._create_tables = create_tables
134
+
135
+ # ---------------------------------------------------------------------
136
+ # Convenience constructors
137
+ # ---------------------------------------------------------------------
138
+ @classmethod
139
+ def from_url(
140
+ cls,
141
+ session_id: str,
142
+ *,
143
+ url: str,
144
+ engine_kwargs: dict[str, Any] | None = None,
145
+ **kwargs: Any,
146
+ ) -> SQLAlchemySession:
147
+ """Create a session from a database URL string.
148
+
149
+ Args:
150
+ session_id (str): Conversation ID.
151
+ url (str): Any SQLAlchemy async URL, e.g. "postgresql+asyncpg://user:pass@host/db".
152
+ engine_kwargs (dict[str, Any] | None): Additional keyword arguments forwarded to
153
+ sqlalchemy.ext.asyncio.create_async_engine.
154
+ **kwargs: Additional keyword arguments forwarded to the main constructor
155
+ (e.g., create_tables, custom table names, etc.).
156
+
157
+ Returns:
158
+ SQLAlchemySession: An instance of SQLAlchemySession connected to the specified database.
159
+ """
160
+ engine_kwargs = engine_kwargs or {}
161
+ engine = create_async_engine(url, **engine_kwargs)
162
+ return cls(session_id, engine=engine, **kwargs)
163
+
164
+ async def _serialize_item(self, item: TResponseInputItem) -> str:
165
+ """Serialize an item to JSON string. Can be overridden by subclasses."""
166
+ return json.dumps(item, separators=(",", ":"))
167
+
168
+ async def _deserialize_item(self, item: str) -> TResponseInputItem:
169
+ """Deserialize a JSON string to an item. Can be overridden by subclasses."""
170
+ return json.loads(item) # type: ignore[no-any-return]
171
+
172
+ # ------------------------------------------------------------------
173
+ # Session protocol implementation
174
+ # ------------------------------------------------------------------
175
+ async def _ensure_tables(self) -> None:
176
+ """Ensure tables are created before any database operations."""
177
+ if self._create_tables:
178
+ async with self._engine.begin() as conn:
179
+ await conn.run_sync(self._metadata.create_all)
180
+ self._create_tables = False # Only create once
181
+
182
+ async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]:
183
+ """Retrieve the conversation history for this session.
184
+
185
+ Args:
186
+ limit: Maximum number of items to retrieve. If None, retrieves all items.
187
+ When specified, returns the latest N items in chronological order.
188
+
189
+ Returns:
190
+ List of input items representing the conversation history
191
+ """
192
+ await self._ensure_tables()
193
+ async with self._session_factory() as sess:
194
+ if limit is None:
195
+ stmt = (
196
+ select(self._messages.c.message_data)
197
+ .where(self._messages.c.session_id == self.session_id)
198
+ .order_by(
199
+ self._messages.c.created_at.asc(),
200
+ self._messages.c.id.asc(),
201
+ )
202
+ )
203
+ else:
204
+ stmt = (
205
+ select(self._messages.c.message_data)
206
+ .where(self._messages.c.session_id == self.session_id)
207
+ # Use DESC + LIMIT to get the latest N
208
+ # then reverse later for chronological order.
209
+ .order_by(
210
+ self._messages.c.created_at.desc(),
211
+ self._messages.c.id.desc(),
212
+ )
213
+ .limit(limit)
214
+ )
215
+
216
+ result = await sess.execute(stmt)
217
+ rows: list[str] = [row[0] for row in result.all()]
218
+
219
+ if limit is not None:
220
+ rows.reverse()
221
+
222
+ items: list[TResponseInputItem] = []
223
+ for raw in rows:
224
+ try:
225
+ items.append(await self._deserialize_item(raw))
226
+ except json.JSONDecodeError:
227
+ # Skip corrupted rows
228
+ continue
229
+ return items
230
+
231
+ async def add_items(self, items: list[TResponseInputItem]) -> None:
232
+ """Add new items to the conversation history.
233
+
234
+ Args:
235
+ items: List of input items to add to the history
236
+ """
237
+ if not items:
238
+ return
239
+
240
+ await self._ensure_tables()
241
+ payload = [
242
+ {
243
+ "session_id": self.session_id,
244
+ "message_data": await self._serialize_item(item),
245
+ }
246
+ for item in items
247
+ ]
248
+
249
+ async with self._session_factory() as sess:
250
+ async with sess.begin():
251
+ # Ensure the parent session row exists - use merge for cross-DB compatibility
252
+ # Check if session exists
253
+ existing = await sess.execute(
254
+ select(self._sessions.c.session_id).where(
255
+ self._sessions.c.session_id == self.session_id
256
+ )
257
+ )
258
+ if not existing.scalar_one_or_none():
259
+ # Session doesn't exist, create it
260
+ await sess.execute(
261
+ insert(self._sessions).values({"session_id": self.session_id})
262
+ )
263
+
264
+ # Insert messages in bulk
265
+ await sess.execute(insert(self._messages), payload)
266
+
267
+ # Touch updated_at column
268
+ await sess.execute(
269
+ update(self._sessions)
270
+ .where(self._sessions.c.session_id == self.session_id)
271
+ .values(updated_at=sql_text("CURRENT_TIMESTAMP"))
272
+ )
273
+
274
+ async def pop_item(self) -> TResponseInputItem | None:
275
+ """Remove and return the most recent item from the session.
276
+
277
+ Returns:
278
+ The most recent item if it exists, None if the session is empty
279
+ """
280
+ await self._ensure_tables()
281
+ async with self._session_factory() as sess:
282
+ async with sess.begin():
283
+ # Fallback for all dialects - get ID first, then delete
284
+ subq = (
285
+ select(self._messages.c.id)
286
+ .where(self._messages.c.session_id == self.session_id)
287
+ .order_by(
288
+ self._messages.c.created_at.desc(),
289
+ self._messages.c.id.desc(),
290
+ )
291
+ .limit(1)
292
+ )
293
+ res = await sess.execute(subq)
294
+ row_id = res.scalar_one_or_none()
295
+ if row_id is None:
296
+ return None
297
+ # Fetch data before deleting
298
+ res_data = await sess.execute(
299
+ select(self._messages.c.message_data).where(self._messages.c.id == row_id)
300
+ )
301
+ row = res_data.scalar_one_or_none()
302
+ await sess.execute(delete(self._messages).where(self._messages.c.id == row_id))
303
+
304
+ if row is None:
305
+ return None
306
+ try:
307
+ return await self._deserialize_item(row)
308
+ except json.JSONDecodeError:
309
+ return None
310
+
311
+ async def clear_session(self) -> None:
312
+ """Clear all items for this session."""
313
+ await self._ensure_tables()
314
+ async with self._session_factory() as sess:
315
+ async with sess.begin():
316
+ await sess.execute(
317
+ delete(self._messages).where(self._messages.c.session_id == self.session_id)
318
+ )
319
+ await sess.execute(
320
+ delete(self._sessions).where(self._sessions.c.session_id == self.session_id)
321
+ )
322
+
323
+ @property
324
+ def engine(self) -> AsyncEngine:
325
+ """Access the underlying SQLAlchemy AsyncEngine.
326
+
327
+ This property provides direct access to the engine for advanced use cases,
328
+ such as checking connection pool status, configuring engine settings,
329
+ or manually disposing the engine when needed.
330
+
331
+ Returns:
332
+ AsyncEngine: The SQLAlchemy async engine instance.
333
+ """
334
+ return self._engine