flyteplugins-agents-openai 2.5.9__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.
@@ -0,0 +1,35 @@
1
+ """OpenAI Agents SDK adapter for Flyte.
2
+
3
+ Bring your own ``openai-agents`` ``Agent`` (tools, handoffs, guardrails) and run
4
+ it durably on Flyte. The adapter provides three things, each independently
5
+ usable:
6
+
7
+ - :func:`tool` — turn a Flyte ``@env.task`` into an OpenAI Agents tool
8
+ that executes as a durable child action (own container/GPU, retries,
9
+ caching) when the agent calls it.
10
+ - :class:`FlyteModelProvider` — a ``ModelProvider`` wrapper that records each
11
+ model turn through ``flyte.trace`` so a crashed/retried run replays
12
+ completed turns instead of re-calling (and re-billing) the LLM.
13
+ - :class:`FlyteTracingProcessor` — forwards the OpenAI Agents trace (turns, tool
14
+ calls, handoffs, token usage) into the Flyte task report for observability.
15
+
16
+ :func:`run_agent` wires all three together for the common case. For full control,
17
+ use them directly with ``Runner.run`` and a ``RunConfig``.
18
+ """
19
+
20
+ from ._durable import FlyteModel, FlyteModelProvider
21
+ from ._memory import FlyteSession
22
+ from ._observability import FlyteTracingProcessor, install_flyte_tracing
23
+ from ._run import run_agent
24
+ from ._tools import FunctionTool, tool
25
+
26
+ __all__ = [
27
+ "FlyteModel",
28
+ "FlyteModelProvider",
29
+ "FlyteSession",
30
+ "FlyteTracingProcessor",
31
+ "FunctionTool",
32
+ "install_flyte_tracing",
33
+ "run_agent",
34
+ "tool",
35
+ ]
@@ -0,0 +1,111 @@
1
+ """Durable, replayable model turns for the OpenAI Agents SDK.
2
+
3
+ The OpenAI Agents ``Runner`` owns the agent loop. To make that loop durable we
4
+ swap in a :class:`FlyteModelProvider`: it wraps the real model so every
5
+ ``get_response`` (one model turn) is recorded through the shared
6
+ :func:`~flyteplugins.agents.core.durable_step` (a ``flyte.trace`` leaf). Inside a
7
+ Flyte task this means a crashed/retried run replays completed turns from
8
+ their recorded outputs instead of re-calling (and re-billing) the model. Tool
9
+ calls run as durable child actions (see :func:`flyteplugins.agents.openai.tool`),
10
+ so the whole agent run becomes crash-resilient and self-healing when the enclosing task
11
+ carries ``retries=...``.
12
+
13
+ The turn is recorded as JSON (pydantic round-trips the SDK's ``ModelResponse``
14
+ faithfully and stays readable in the Flyte UI). The non-serializable real call is
15
+ captured in a closure passed to ``durable_step``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import typing
21
+ from collections.abc import AsyncIterator
22
+
23
+ from agents.items import ModelResponse
24
+ from agents.models.interface import Model, ModelProvider
25
+ from agents.models.multi_provider import MultiProvider
26
+ from flyteplugins.agents.core import durable_step, fingerprint, jsonable
27
+ from pydantic import TypeAdapter
28
+
29
+ # pydantic round-trips ModelResponse (and its nested OpenAI types) faithfully;
30
+ # storing JSON keeps the recorded turn human-readable in the Flyte UI.
31
+ _RESPONSE_ADAPTER: TypeAdapter[ModelResponse] = TypeAdapter(ModelResponse)
32
+
33
+ # Positional parameters of ``Model.get_response`` used for fingerprinting
34
+ # (``tracing`` and the keyword-only ids are intentionally ignored).
35
+ _GET_RESPONSE_PARAMS = (
36
+ "system_instructions",
37
+ "input",
38
+ "model_settings",
39
+ "tools",
40
+ "output_schema",
41
+ "handoffs",
42
+ )
43
+
44
+
45
+ def _bind(args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any]) -> dict[str, typing.Any]:
46
+ bound = dict(kwargs)
47
+ for name, value in zip(_GET_RESPONSE_PARAMS, args):
48
+ bound.setdefault(name, value)
49
+ return bound
50
+
51
+
52
+ def _request_fingerprint(args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any]) -> str:
53
+ """Deterministic memo key for a model turn — tool/handoff names, not callables."""
54
+ bound = _bind(args, kwargs)
55
+ return fingerprint(
56
+ {
57
+ "system": bound.get("system_instructions"),
58
+ "input": jsonable(bound.get("input")),
59
+ "model_settings": jsonable(bound.get("model_settings")),
60
+ "tools": sorted(getattr(t, "name", str(t)) for t in (bound.get("tools") or [])),
61
+ "handoffs": sorted(
62
+ getattr(h, "agent_name", getattr(h, "tool_name", str(h))) for h in (bound.get("handoffs") or [])
63
+ ),
64
+ "output_schema": getattr(bound.get("output_schema"), "name", None),
65
+ }
66
+ )
67
+
68
+
69
+ class FlyteModel(Model):
70
+ """Wrap a :class:`~agents.models.interface.Model` so each turn is durable.
71
+
72
+ ``get_response`` is recorded/replayed via ``durable_step``. ``stream_response``
73
+ is delegated unchanged: streamed turns are not memoized in this version (tool
74
+ calls remain durable regardless).
75
+ """
76
+
77
+ def __init__(self, inner: Model):
78
+ self._inner = inner
79
+
80
+ async def get_response(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
81
+ return await durable_step(
82
+ _request_fingerprint(args, kwargs),
83
+ lambda: self._inner.get_response(*args, **kwargs),
84
+ name="model_turn",
85
+ dumps=lambda response: _RESPONSE_ADAPTER.dump_json(response).decode("utf-8"),
86
+ loads=_RESPONSE_ADAPTER.validate_json,
87
+ )
88
+
89
+ def stream_response(self, *args: typing.Any, **kwargs: typing.Any) -> AsyncIterator[typing.Any]:
90
+ return self._inner.stream_response(*args, **kwargs)
91
+
92
+ async def close(self) -> None:
93
+ await self._inner.close()
94
+
95
+
96
+ class FlyteModelProvider(ModelProvider):
97
+ """Wrap a ``ModelProvider`` so every model it returns produces durable turns.
98
+
99
+ Pass an explicit ``inner`` provider to keep custom routing (Azure, a gateway,
100
+ a local OpenAI-compatible server); defaults to the SDK's ``MultiProvider``.
101
+ Set it on ``RunConfig.model_provider`` (``run_agent`` does this for you).
102
+ """
103
+
104
+ def __init__(self, inner: ModelProvider | None = None):
105
+ self._inner = inner or MultiProvider()
106
+
107
+ def get_model(self, model_name: str | None) -> Model:
108
+ return FlyteModel(self._inner.get_model(model_name))
109
+
110
+ async def aclose(self) -> None:
111
+ await self._inner.aclose()
@@ -0,0 +1,47 @@
1
+ """OpenAI Agents ``Session`` backed by a keyed Flyte ``MemoryStore``.
2
+
3
+ The OpenAI Agents SDK reads and writes conversation history through a ``Session``
4
+ (``get_items`` / ``add_items`` / ``pop_item`` / ``clear_session``).
5
+ :class:`FlyteSession` implements that protocol over a durable, cross-run
6
+ :class:`~flyte.ai.agents.memory.MemoryStore` (keyed by a ``memory_key``), so
7
+ passing ``session=FlyteSession(store)`` to ``Runner.run`` gives the agent memory
8
+ that survives across runs and workers — backed by object storage rather than the
9
+ SDK's default local SQLite (which doesn't persist on a distributed backend).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import typing
15
+
16
+
17
+ class FlyteSession:
18
+ """An ``agents`` ``Session`` whose items live in a keyed Flyte ``MemoryStore``.
19
+
20
+ The store's ``messages`` transcript is the session item list; ``add_items``
21
+ persists durably (an object-store upload) so the next run for the same key
22
+ resumes the conversation.
23
+ """
24
+
25
+ def __init__(self, store: typing.Any) -> None:
26
+ self._store = store
27
+
28
+ async def get_items(self, limit: int | None = None) -> list[dict[str, typing.Any]]:
29
+ items = list(self._store.messages)
30
+ return items[-limit:] if limit else items
31
+
32
+ async def add_items(self, items: list[dict[str, typing.Any]]) -> None:
33
+ if not items:
34
+ return
35
+ self._store.extend(items)
36
+ await self._store.save.aio()
37
+
38
+ async def pop_item(self) -> dict[str, typing.Any] | None:
39
+ if not self._store.messages:
40
+ return None
41
+ item = self._store.messages.pop()
42
+ await self._store.save.aio()
43
+ return item
44
+
45
+ async def clear_session(self) -> None:
46
+ self._store.messages.clear()
47
+ await self._store.save.aio()
@@ -0,0 +1,106 @@
1
+ """Forward the OpenAI Agents trace into the Flyte task report.
2
+
3
+ The OpenAI Agents SDK emits a structured trace of every run (agent spans, model
4
+ ``generation``/``response`` turns, ``function`` tool calls, ``handoff`` and
5
+ ``guardrail`` spans). :class:`FlyteTracingProcessor` is a ``TracingProcessor``
6
+ that maps those spans onto the shared :class:`~flyteplugins.agents.core.ReportTimeline`,
7
+ rendering an in-run timeline (timings, token usage, tool inputs/outputs) into a tab of
8
+ the enclosing task's Flyte report, alongside the tool tasks that already show up as
9
+ Flyte actions.
10
+
11
+ It is best-effort by contract: a processor must never raise into the agent loop,
12
+ and report writes are skipped silently when there is no active report.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import typing
18
+
19
+ from agents import add_trace_processor, set_trace_processors
20
+ from agents.tracing.processor_interface import TracingProcessor
21
+ from flyte._logging import logger
22
+ from flyteplugins.agents.core import ReportTimeline, abbrev, duration_ms
23
+
24
+ _ICONS = {
25
+ "agent": "🤖",
26
+ "generation": "🧠",
27
+ "response": "🧠",
28
+ "function": "🛠️",
29
+ "handoff": "🔀",
30
+ "guardrail": "🛡️",
31
+ "mcp_tools": "🔌",
32
+ }
33
+
34
+
35
+ def _summarize(kind: str, export: dict[str, typing.Any]) -> str:
36
+ """Render the OpenAI-specific span payload as a compact HTML detail string."""
37
+ if kind == "function":
38
+ return f"<code>{abbrev(export.get('input'), 160)}</code> → <code>{abbrev(export.get('output'), 160)}</code>"
39
+ if kind in ("generation", "response"):
40
+ usage = export.get("usage") or {}
41
+ if usage:
42
+ parts = ", ".join(f"{k}={v}" for k, v in usage.items())
43
+ return f'<span style="opacity:.7">{abbrev(parts)}</span>'
44
+ return ""
45
+ if kind == "handoff":
46
+ return f"{abbrev(export.get('from_agent'))} → {abbrev(export.get('to_agent'))}"
47
+ return ""
48
+
49
+
50
+ class FlyteTracingProcessor(TracingProcessor):
51
+ """Map OpenAI Agents spans onto the shared :class:`ReportTimeline`."""
52
+
53
+ def __init__(self, tab_name: str = "Agent"):
54
+ self._timeline = ReportTimeline(tab_name)
55
+
56
+ def on_trace_start(self, trace: typing.Any) -> None:
57
+ self._timeline.heading("OpenAI agent")
58
+
59
+ def on_trace_end(self, trace: typing.Any) -> None:
60
+ pass
61
+
62
+ def on_span_start(self, span: typing.Any) -> None:
63
+ pass
64
+
65
+ def on_span_end(self, span: typing.Any) -> None:
66
+ try:
67
+ self._render(span)
68
+ except Exception: # pragma: no cover - observability must never break the loop
69
+ logger.debug("FlyteTracingProcessor failed to render a span", exc_info=True)
70
+
71
+ def shutdown(self) -> None:
72
+ pass
73
+
74
+ def force_flush(self) -> None:
75
+ pass
76
+
77
+ def _render(self, span: typing.Any) -> None:
78
+ data = getattr(span, "span_data", None)
79
+ if data is None:
80
+ return
81
+ kind = getattr(data, "type", "custom")
82
+ export = data.export() if hasattr(data, "export") else {}
83
+ duration = duration_ms(getattr(span, "started_at", None), getattr(span, "ended_at", None))
84
+ self._timeline.row(
85
+ icon=_ICONS.get(kind, "•"),
86
+ label=export.get("name") or kind,
87
+ meta=" · ".join(p for p in (kind, duration) if p),
88
+ detail=_summarize(kind, export),
89
+ error=getattr(span, "error", None),
90
+ )
91
+
92
+
93
+ def install_flyte_tracing(*, exclusive: bool = True, tab_name: str = "Agent") -> FlyteTracingProcessor:
94
+ """Install a :class:`FlyteTracingProcessor` as a global trace processor.
95
+
96
+ With ``exclusive=True`` (default) it replaces all processors, so traces are
97
+ rendered only into the Flyte report and nothing is uploaded to OpenAI's
98
+ tracing backend. Set ``exclusive=False`` to keep the SDK's default processors
99
+ (e.g. to also export to the OpenAI dashboard) and add Flyte alongside.
100
+ """
101
+ processor = FlyteTracingProcessor(tab_name=tab_name)
102
+ if exclusive:
103
+ set_trace_processors([processor])
104
+ else:
105
+ add_trace_processor(processor)
106
+ return processor
@@ -0,0 +1,99 @@
1
+ """``run_agent`` — the one-call path to run an OpenAI agent durably on Flyte."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typing
6
+
7
+ from agents import Agent, RunConfig, Runner
8
+ from flyte._task import TaskTemplate
9
+ from flyteplugins.agents.core import flush_report, resolve_memory
10
+
11
+ from ._durable import FlyteModelProvider
12
+ from ._memory import FlyteSession
13
+ from ._observability import install_flyte_tracing
14
+ from ._tools import tool
15
+
16
+
17
+ def _normalize_tools(tools: typing.Sequence[typing.Any]) -> list[typing.Any]:
18
+ """Accept bare ``@env.task`` templates as tools by wrapping them on the fly."""
19
+ normalized: list[typing.Any] = []
20
+ for t in tools or ():
21
+ normalized.append(tool(t) if isinstance(t, TaskTemplate) else t)
22
+ return normalized
23
+
24
+
25
+ async def run_agent(
26
+ input: str | list[typing.Any],
27
+ *,
28
+ agent: Agent | None = None,
29
+ tools: typing.Sequence[typing.Any] = (),
30
+ model: str = "gpt-4.1",
31
+ instructions: str | None = None,
32
+ name: str = "flyte-agent",
33
+ max_turns: int = 10,
34
+ durable: bool = True,
35
+ observability: bool = True,
36
+ run_config: RunConfig | None = None,
37
+ memory_key: str | None = None,
38
+ ) -> str:
39
+ """Run an OpenAI Agents SDK agent with Flyte providing the runtime.
40
+
41
+ Call this from inside an ``@env.task`` — that task is the durable parent.
42
+ Within it, each model turn is recorded via ``flyte.trace`` (replayed on
43
+ retry) and each tool call runs as a durable Flyte child action. Give the
44
+ enclosing task ``retries=...`` for self-healing and ``report=True`` to see
45
+ the agent timeline.
46
+
47
+ Provide either a fully-built ``agent`` (keeping its handoffs/guardrails), or
48
+ ``tools`` + ``instructions`` + ``model`` to have one built for you. ``tools``
49
+ may be :func:`tool`-wrapped tools or bare ``@env.task`` templates
50
+ (wrapped automatically).
51
+
52
+ Args:
53
+ input: The user prompt (or a list of input items).
54
+ agent: A pre-built ``agents.Agent``. Mutually exclusive with ``tools``.
55
+ tools: Tools to expose (when ``agent`` is not given).
56
+ model: Model name (when ``agent`` is not given).
57
+ instructions: System instructions (when ``agent`` is not given).
58
+ name: Agent name (when ``agent`` is not given).
59
+ max_turns: Maximum model to tool turns and vice versa before the SDK raises.
60
+ durable: Record/replay each model turn via ``flyte.trace``.
61
+ observability: Render the run timeline into the Flyte task report.
62
+ run_config: A custom ``RunConfig``; ``model_provider`` is wrapped for
63
+ durability unless ``durable=False``.
64
+ memory_key: Stable id (e.g. a user/thread id) for cross-run memory.
65
+ When set, conversation history is loaded from and saved to a durable,
66
+ keyed ``MemoryStore`` (via the SDK's ``Session``), so a later run with
67
+ the same key continues the conversation. ``None`` disables memory.
68
+
69
+ Returns:
70
+ The agent's final output as a string.
71
+ """
72
+ if agent is not None and tools:
73
+ raise ValueError("Pass either `agent` (with its own tools) or `tools`, not both.")
74
+
75
+ if agent is None:
76
+ agent = Agent(
77
+ name=name,
78
+ instructions=instructions or "You are a helpful assistant.",
79
+ model=model,
80
+ tools=_normalize_tools(tools),
81
+ )
82
+
83
+ config = run_config or RunConfig()
84
+ if durable:
85
+ config.model_provider = FlyteModelProvider(config.model_provider)
86
+ if observability:
87
+ install_flyte_tracing()
88
+
89
+ store = await resolve_memory(memory_key)
90
+ session = FlyteSession(store) if store is not None else None
91
+
92
+ try:
93
+ result = await Runner.run(agent, input, max_turns=max_turns, run_config=config, session=session)
94
+ finally:
95
+ if observability:
96
+ await flush_report()
97
+
98
+ final = result.final_output
99
+ return final if isinstance(final, str) else str(final)
@@ -0,0 +1,124 @@
1
+ """Turn Flyte tasks into OpenAI Agents tools that execute as durable actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ import typing
8
+ from dataclasses import dataclass
9
+ from functools import partial
10
+
11
+ from agents import FunctionTool as OpenAIFunctionTool
12
+ from agents import function_tool as openai_function_tool
13
+ from agents.function_schema import function_schema
14
+ from agents.tool_context import ToolContext
15
+ from flyte._task import AsyncFunctionTaskTemplate, TaskTemplate
16
+ from flyte.models import NativeInterface
17
+ from flyteplugins.agents.core import attach_tool_resolver
18
+
19
+
20
+ @dataclass
21
+ class FunctionTool(OpenAIFunctionTool):
22
+ """An OpenAI Agents ``FunctionTool`` backed by a Flyte task.
23
+
24
+ Behaves exactly like ``agents.FunctionTool`` from the SDK's perspective, but
25
+ when the agent invokes it the call is dispatched to the underlying Flyte task
26
+ — so it runs as a durable child action (its own container/resources, with
27
+ retries and caching) rather than inline in the agent's process.
28
+ """
29
+
30
+ task: TaskTemplate | None = None
31
+ native_interface: NativeInterface | None = None
32
+ report: bool = False
33
+
34
+ async def execute(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
35
+ """Run the wrapped task directly (a durable child action in a task context)."""
36
+ if self.task is None: # pragma: no cover - defensive
37
+ raise RuntimeError("FunctionTool has no backing task to execute.")
38
+ return await self.task.aio(*args, **kwargs)
39
+
40
+ @property
41
+ def __wrapped_task__(self) -> typing.Any:
42
+ """The backing :class:`TaskTemplate` so :class:`ToolTaskResolver` can recover it.
43
+
44
+ Stacking ``@tool`` on ``@env.task`` rebinds the module attribute
45
+ to this tool, hiding the task from the default resolver;
46
+ ``flyteplugins.agents.core.ToolTaskResolver`` uses this hook to recover the
47
+ real task on the worker (so the task runs its own body instead of
48
+ re-dispatching itself, which would otherwise recurse indefinitely).
49
+ """
50
+ return self.task if isinstance(self.task, TaskTemplate) else None
51
+
52
+
53
+ def tool(
54
+ func: AsyncFunctionTaskTemplate | typing.Callable | None = None,
55
+ **kwargs: typing.Any,
56
+ ) -> FunctionTool | OpenAIFunctionTool:
57
+ """Flyte-aware replacement for ``agents.function_tool`` — named ``tool`` for consistency.
58
+
59
+ - For an ``@env.task`` (an ``AsyncFunctionTaskTemplate``): returns a
60
+ :class:`FunctionTool` whose invocation runs the task as a durable Flyte
61
+ action. The tool's JSON schema, name and description are derived by the
62
+ OpenAI Agents SDK from the task's function signature, so strict-mode tool
63
+ calling works unchanged.
64
+ - For a plain callable or a ``@flyte.trace`` helper: forwards to the native
65
+ ``agents.function_tool`` (runs inline; ``@flyte.trace`` helpers are still
66
+ recorded for observability when inside a task).
67
+
68
+ ``**kwargs`` (e.g. ``name_override``, ``description_override``) are forwarded
69
+ to ``agents.function_tool`` in both cases.
70
+
71
+ Usable as a bare decorator, a parametrized decorator, or a direct call::
72
+
73
+ @tool
74
+ @env.task
75
+ async def get_weather(city: str) -> str: ...
76
+
77
+ weather = tool(get_weather, name_override="weather")
78
+ """
79
+ if func is None:
80
+ return partial(tool, **kwargs)
81
+
82
+ if isinstance(func, AsyncFunctionTaskTemplate):
83
+ return _task_to_tool(func, **kwargs)
84
+
85
+ # Plain callables and @flyte.trace-decorated functions use the native path.
86
+ return openai_function_tool(func, **kwargs)
87
+
88
+
89
+ def _task_to_tool(task: AsyncFunctionTaskTemplate, **kwargs: typing.Any) -> FunctionTool:
90
+ """Build a :class:`FunctionTool` from a Flyte task.
91
+
92
+ Only the stable, public fields of ``agents.FunctionTool`` are copied from a
93
+ base tool built by ``agents.function_tool`` — we deliberately do not reflect
94
+ over private fields, so this stays robust across SDK versions.
95
+ """
96
+ base = openai_function_tool(task.func, **kwargs)
97
+
98
+ async def _on_invoke_tool(ctx: ToolContext[typing.Any], input: str) -> typing.Any:
99
+ data: dict[str, typing.Any] = json.loads(input) if input else {}
100
+ schema = function_schema(task.func)
101
+ parsed = schema.params_pydantic_model(**data) if data else schema.params_pydantic_model()
102
+ args, kwargs_ = schema.to_call_args(parsed)
103
+ # In a Flyte task context, calling the task submits a durable child
104
+ # action through the controller; locally it runs inline. Sync tasks
105
+ # return a value directly, async tasks return an awaitable.
106
+ out = task(*args, **kwargs_)
107
+ if inspect.isawaitable(out):
108
+ out = await out
109
+ return out
110
+
111
+ # The returned tool shadows the task at module scope, so point the task at the
112
+ # shared resolver that recovers it on the worker via ``__wrapped_task__``.
113
+ attach_tool_resolver(task)
114
+
115
+ return FunctionTool(
116
+ name=base.name,
117
+ description=base.description,
118
+ params_json_schema=base.params_json_schema,
119
+ on_invoke_tool=_on_invoke_tool,
120
+ strict_json_schema=base.strict_json_schema,
121
+ task=task,
122
+ native_interface=task.native_interface,
123
+ report=task.report,
124
+ )
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyteplugins-agents-openai
3
+ Version: 2.5.9
4
+ Summary: Run OpenAI Agents SDK agents on Flyte.
5
+ Author-email: Samhita Alla <samhita@union.ai>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: flyteplugins-agents-core
9
+ Requires-Dist: openai-agents
10
+
11
+ # flyteplugins-agents-openai
12
+
13
+ Run [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) agents on
14
+ Flyte. You keep writing agents in the SDK's own idioms; Flyte is the durable
15
+ orchestration runtime underneath — replay, automatic retries / self-healing,
16
+ per-tool containerized execution (CPU/GPU, caching), and observability.
17
+
18
+ ```bash
19
+ pip install flyteplugins-agents-openai
20
+ ```
21
+
22
+ ```python
23
+ import flyte
24
+ from flyteplugins.agents.openai import tool, run_agent
25
+
26
+ env = flyte.TaskEnvironment(
27
+ "openai-agent",
28
+ secrets=[flyte.Secret(key="openai_api_key", as_env_var="OPENAI_API_KEY")],
29
+ )
30
+
31
+ # A tool that is also a durable, cached Flyte task.
32
+ @tool
33
+ @env.task(cache="auto", retries=3)
34
+ async def get_weather(city: str) -> str:
35
+ """Get the current weather for a city."""
36
+ return f"The weather in {city} is sunny, 22°C."
37
+
38
+ # The durable parent. retries=3 -> self-healing; report=True -> agent timeline.
39
+ @env.task(report=True, retries=3)
40
+ async def city_agent(question: str) -> str:
41
+ return await run_agent(
42
+ question,
43
+ tools=[get_weather],
44
+ instructions="You are a concise assistant. Use the tools to answer.",
45
+ model="gpt-4.1",
46
+ )
47
+ ```
48
+
49
+ ## How it maps to Flyte
50
+
51
+ - The SDK owns the loop — we don't reimplement it. `run_agent` drives the
52
+ OpenAI Agents `Runner`; the agent run is your `@env.task` (the durable
53
+ parent) and the SDK runs its agent loop inside it.
54
+ - Tools as durable child actions. `tool` wraps an `@env.task` so that
55
+ when the agent calls a tool, the task runs as a durable Flyte child action
56
+ (its own container/resources, retries, caching) — not inline in the agent
57
+ process. The SDK derives the tool's JSON schema, name and description from the
58
+ task signature, so strict tool-calling works unchanged.
59
+ - Durable, replayable model turns. Each model turn is recorded via
60
+ `flyte.trace` by tracing the seam below the loop — a `FlyteModelProvider`
61
+ set on `RunConfig.model_provider`. If the task crashes and Flyte retries it,
62
+ completed turns and tool calls replay from their recorded outputs instead of
63
+ re-calling (and re-billing) the model.
64
+ - Self-healing. `retries=...` on the agent task plus per-turn / per-tool replay
65
+ means transient failures recover automatically without redoing completed work.
66
+ - Observability. The OpenAI Agents trace (turns, tool calls, handoffs, token
67
+ usage) renders into the task report (`report=True`); `install_flyte_tracing()`
68
+ replaces the OpenAI exporter (`exclusive=True`) so nothing is uploaded
69
+ externally.
70
+
71
+ The API key is read from the environment. Wire it as a Flyte secret.
72
+
73
+ ### Bring your own agent
74
+
75
+ Already wrote an `agents.Agent` with handoffs and guardrails? Pass it through:
76
+
77
+ ```python
78
+ from agents import Agent
79
+
80
+ triage = Agent(name="triage", handoffs=[...], input_guardrails=[...])
81
+
82
+ @env.task(report=True, retries=3)
83
+ async def run(goal: str) -> str:
84
+ return await run_agent(goal, agent=triage)
85
+ ```
86
+
87
+ ### Power-user building blocks
88
+
89
+ `run_agent` wires three independently usable pieces; reach for them directly when
90
+ driving `Runner.run` yourself:
91
+
92
+ - `tool` — turn a Flyte task into an OpenAI Agents tool.
93
+ - `FlyteModelProvider` — set on `RunConfig.model_provider` to make model turns
94
+ durable (the seam below the loop).
95
+ - `install_flyte_tracing()` / `FlyteTracingProcessor` — render the trace into the
96
+ report (`exclusive=True` by default replaces the OpenAI exporter so nothing is
97
+ uploaded externally).
98
+
99
+ ## Memory
100
+
101
+ Pass `memory_key` (a user/thread id) for cross-run memory — the agent continues
102
+ the same conversation across separate runs, workers and restarts:
103
+
104
+ ```python
105
+ await run_agent(message, model="gpt-4.1", memory_key="user-alice")
106
+ ```
107
+
108
+ It backs the OpenAI Agents SDK `Session` with a durable, keyed `MemoryStore` (object
109
+ storage), so unlike the SDK's default local-SQLite session, it persists on a
110
+ distributed backend. The same store also holds path-addressed facts for long-term
111
+ `remember` / `recall` memory.
112
+
113
+ ## Examples
114
+
115
+ See [`examples/`](examples/):
116
+
117
+ - [`openai_durable_agent.py`](examples/openai_durable_agent.py) — a single durable
118
+ agent: tools as Flyte tasks, traced model turns, agent timeline in the report.
119
+ - [`openai_multi_agent.py`](examples/openai_multi_agent.py) — multi-agent
120
+ orchestration: a planner agent decomposes a topic, researcher agents fan out
121
+ in parallel, an editor agent synthesizes — each agent its own durable action.
122
+ - [`openai_handoffs.py`](examples/openai_handoffs.py) — handoffs + HITL: a
123
+ triage agent hands off to billing / technical specialists inside one
124
+ `Runner.run`; durability spans the handoff (a mid-chain crash replays both
125
+ agents' turns), a sensitive `issue_refund` tool is gated on a human-approval
126
+ form, and a diagnostic tool runs in a higher-CPU environment.
127
+ - [`openai_crash_resume.py`](examples/openai_crash_resume.py) — crash &
128
+ resume: the task crashes on its first attempt after doing real work; on retry
129
+ the completed model turns replay from their `flyte.trace` records and the tool
130
+ calls are cache hits, so it finishes without re-calling the model. Run on a
131
+ backend to see the replay.
132
+ - [`openai_memory.py`](examples/openai_memory.py) — cross-run memory: two
133
+ separate runs share a `memory_key`; the agent learns a fact in run 1 and recalls
134
+ it in run 2.
135
+
136
+ ## Notes
137
+
138
+ - Streamed runs (`Runner.run_streamed`) are not memoized per-turn in this
139
+ version; tool calls remain durable regardless.
140
+
141
+ ## Conformance
142
+
143
+ This adapter passes the shared `flyteplugins.agents.core.testing.assert_adapter_conforms`
144
+ check, so it follows the common format (`tool` + `run_agent`, tool tasks wired
145
+ to the resolver), shared with the Claude and Mistral adapters.
@@ -0,0 +1,10 @@
1
+ flyteplugins/agents/openai/__init__.py,sha256=bovIL0Nb3f6wB_AjkYmN9tmtSK2vBtbUoUglCmgwgaA,1323
2
+ flyteplugins/agents/openai/_durable.py,sha256=vjQzhHUu8Uauknht99tOY35cDz4cWO1rz7YkR8gqdWI,4503
3
+ flyteplugins/agents/openai/_memory.py,sha256=tTF74A3jVxcfw1NQAQfaXWqPJs6rprC_tjkX8-pCnNw,1762
4
+ flyteplugins/agents/openai/_observability.py,sha256=4Ba7qvvZahmPmxVECvO7IOqa041fQzunW_yLYPlxA-w,4054
5
+ flyteplugins/agents/openai/_run.py,sha256=2xsxk9hrfj9SRIlZ5EiccpAqVa03dzcSMAyC3DoN44M,3897
6
+ flyteplugins/agents/openai/_tools.py,sha256=TVhMKuzi-8ExVNWGQ8GZ71y-ur-Wi2dvFlfWxTiJ6q8,5234
7
+ flyteplugins_agents_openai-2.5.9.dist-info/METADATA,sha256=JCQwCKUs14X9JrP7WhYfjMBZkL9yrHvDbzGJuGQPeVs,5970
8
+ flyteplugins_agents_openai-2.5.9.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
9
+ flyteplugins_agents_openai-2.5.9.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
10
+ flyteplugins_agents_openai-2.5.9.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ flyteplugins