flyteplugins-agents-core 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.
- flyteplugins/agents/core/__init__.py +42 -0
- flyteplugins/agents/core/_durable.py +54 -0
- flyteplugins/agents/core/_fingerprint.py +33 -0
- flyteplugins/agents/core/_memory.py +54 -0
- flyteplugins/agents/core/_observability.py +35 -0
- flyteplugins/agents/core/_tools.py +181 -0
- flyteplugins/agents/core/testing.py +65 -0
- flyteplugins_agents_core-2.5.9.dist-info/METADATA +44 -0
- flyteplugins_agents_core-2.5.9.dist-info/RECORD +11 -0
- flyteplugins_agents_core-2.5.9.dist-info/WHEEL +5 -0
- flyteplugins_agents_core-2.5.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""flyteplugins-agents-core — the shared contract every agent-SDK adapter implements.
|
|
2
|
+
|
|
3
|
+
This package holds the SDK-agnostic machinery that makes Flyte the durable
|
|
4
|
+
runtime under any agent framework, so each ``flyteplugins-agents-<sdk>`` adapter
|
|
5
|
+
stays thin and consistent:
|
|
6
|
+
|
|
7
|
+
- :func:`durable_step` — record a call as a durable, replayable ``flyte.trace``
|
|
8
|
+
leaf (the model-turn durability mechanism), keyed by a fingerprint.
|
|
9
|
+
- :func:`fingerprint` — a deterministic memo key from a request payload.
|
|
10
|
+
- :class:`ToolTaskResolver` / :func:`attach_tool_resolver` — make a tool-backing
|
|
11
|
+
task resolve to itself on the worker.
|
|
12
|
+
- :class:`ReportTimeline` — render agent events into the Flyte task report.
|
|
13
|
+
- :mod:`flyteplugins.agents.core.testing` — :func:`assert_adapter_conforms`, the
|
|
14
|
+
CI-enforced conformance check every adapter runs.
|
|
15
|
+
|
|
16
|
+
The division of labor every adapter follows: the agent run is a Flyte
|
|
17
|
+
``@env.task`` (durable parent), each model turn is a ``flyte.trace`` (a
|
|
18
|
+
memoized, replayable leaf), and each tool is a Flyte task invoked as a
|
|
19
|
+
durable child action.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from ._durable import durable_step
|
|
23
|
+
from ._fingerprint import fingerprint, jsonable
|
|
24
|
+
from ._memory import resolve_memory
|
|
25
|
+
from ._observability import ReportTimeline, abbrev, duration_ms, flush_report
|
|
26
|
+
from ._tools import ToolTaskResolver, attach_tool_resolver, coerce_tool_args, task_json_schema, tool
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ReportTimeline",
|
|
30
|
+
"ToolTaskResolver",
|
|
31
|
+
"abbrev",
|
|
32
|
+
"attach_tool_resolver",
|
|
33
|
+
"coerce_tool_args",
|
|
34
|
+
"durable_step",
|
|
35
|
+
"duration_ms",
|
|
36
|
+
"fingerprint",
|
|
37
|
+
"flush_report",
|
|
38
|
+
"jsonable",
|
|
39
|
+
"resolve_memory",
|
|
40
|
+
"task_json_schema",
|
|
41
|
+
"tool",
|
|
42
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Durable, replayable steps via ``flyte.trace`` — the shared mechanism.
|
|
2
|
+
|
|
3
|
+
A model turn (or any expensive, replay-worthy call) is recorded as a
|
|
4
|
+
``flyte.trace`` leaf so that, inside a Flyte task, a crash/retry replays the
|
|
5
|
+
recorded result instead of re-running it. The real call is usually
|
|
6
|
+
non-serializable (closures, SDK objects), but ``flyte.trace`` keys its memo on
|
|
7
|
+
the decorated function's serializable arguments. :func:`durable_step` resolves
|
|
8
|
+
that by capturing the real call in a closure and feeding the trace only a
|
|
9
|
+
deterministic ``request_key``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import typing
|
|
15
|
+
|
|
16
|
+
import flyte
|
|
17
|
+
|
|
18
|
+
T = typing.TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def durable_step(
|
|
22
|
+
request_key: str,
|
|
23
|
+
run: typing.Callable[[], typing.Awaitable[typing.Any]],
|
|
24
|
+
*,
|
|
25
|
+
name: str = "durable_step",
|
|
26
|
+
dumps: typing.Callable[[typing.Any], str] = lambda v: v,
|
|
27
|
+
loads: typing.Callable[[str], typing.Any] = lambda v: v,
|
|
28
|
+
) -> typing.Any:
|
|
29
|
+
"""Run ``run()`` once as a durable, replayable trace step keyed by ``request_key``.
|
|
30
|
+
|
|
31
|
+
The real (possibly non-serializable) work is captured in the ``run`` closure,
|
|
32
|
+
so the traced function only ever sees the serializable ``request_key``. The
|
|
33
|
+
result is serialized with ``dumps`` for the trace record (a ``str`` is stored
|
|
34
|
+
inline and is human-readable in the Flyte UI) and rebuilt with ``loads`` on
|
|
35
|
+
the way out and on replay.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
request_key: A deterministic fingerprint of the call; the trace memo key.
|
|
39
|
+
run: Zero-arg async callable performing the real work.
|
|
40
|
+
name: Label for the trace action in the Flyte UI.
|
|
41
|
+
dumps: Serialize the result to a ``str`` for the trace record.
|
|
42
|
+
loads: Rebuild the result from the recorded ``str``.
|
|
43
|
+
|
|
44
|
+
Outside a task context ``flyte.trace`` is a transparent pass-through, so this
|
|
45
|
+
also works unchanged for local runs and unit tests.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
async def _step(key: str) -> str:
|
|
49
|
+
return dumps(await run())
|
|
50
|
+
|
|
51
|
+
# Name the closure so the trace action reads meaningfully in the UI.
|
|
52
|
+
_step.__name__ = name
|
|
53
|
+
_step.__qualname__ = name
|
|
54
|
+
return loads(await flyte.trace(_step)(request_key))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Deterministic request fingerprints for keying durable steps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fingerprint(payload: typing.Mapping[str, typing.Any]) -> str:
|
|
11
|
+
"""A deterministic sha256 hex of a request ``payload``.
|
|
12
|
+
|
|
13
|
+
Must be a pure function of the semantic request so replays line up. Pass a
|
|
14
|
+
mapping of the request's identifying fields (e.g. system prompt, input
|
|
15
|
+
items, model, tool names) — not callables or live objects. Anything not
|
|
16
|
+
natively JSON-serializable is coerced with ``str``.
|
|
17
|
+
"""
|
|
18
|
+
blob = json.dumps(payload, sort_keys=True, default=str)
|
|
19
|
+
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def jsonable(obj: typing.Any) -> typing.Any:
|
|
23
|
+
"""Best-effort conversion of an SDK object to something JSON-dumpable."""
|
|
24
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
25
|
+
return obj
|
|
26
|
+
for attr in ("to_json_dict", "model_dump"):
|
|
27
|
+
method = getattr(obj, attr, None)
|
|
28
|
+
if callable(method):
|
|
29
|
+
try:
|
|
30
|
+
return method()
|
|
31
|
+
except Exception: # pragma: no cover - defensive
|
|
32
|
+
pass
|
|
33
|
+
return str(obj)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Cross-run agent memory — a thin handle over Flyte's keyed ``MemoryStore``.
|
|
2
|
+
|
|
3
|
+
"Agent memory" = durable state addressable by a stable ``memory_key`` (a user or
|
|
4
|
+
thread id), shared across runs and workers. We don't reinvent it: Flyte already
|
|
5
|
+
ships a keyed, blob-store-backed store (``flyte.ai.agents.memory.MemoryStore``)
|
|
6
|
+
that resolves a deterministic remote path from the key and the active run context
|
|
7
|
+
— stripping the per-run scratch so two runs with the same key share one store,
|
|
8
|
+
and encapsulating all the storage details.
|
|
9
|
+
|
|
10
|
+
It is decoupled from the agent loop - importing it does not pull in any agent
|
|
11
|
+
harness — so adapters use it purely as a durable store. Each adapter then maps its
|
|
12
|
+
SDK's own conversation state onto the returned store.
|
|
13
|
+
|
|
14
|
+
The store carries two complementary surfaces, so one ``memory_key`` covers both
|
|
15
|
+
kinds of memory:
|
|
16
|
+
|
|
17
|
+
- ``messages`` (``append``/``extend``) — the conversation transcript;
|
|
18
|
+
- path-addressed ``read_json``/``write_json``/``list_paths`` — durable named facts
|
|
19
|
+
(the ``remember`` / ``recall`` substrate), with audit + version history.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import typing
|
|
25
|
+
|
|
26
|
+
from flyte._logging import logger
|
|
27
|
+
|
|
28
|
+
if typing.TYPE_CHECKING:
|
|
29
|
+
from flyte.ai.agents.memory import MemoryStore
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def resolve_memory(memory_key: str | None, *, audit: bool = True) -> "MemoryStore | None":
|
|
33
|
+
"""Open (or create) the keyed agent-memory store, or ``None`` if unavailable.
|
|
34
|
+
|
|
35
|
+
Best-effort: returns ``None`` (and logs) when ``memory_key`` is falsy or no
|
|
36
|
+
durable store can be resolved — e.g. no Flyte context/org, or an invalid key —
|
|
37
|
+
so memory never breaks a run. Call from inside an ``@env.task``; the store's
|
|
38
|
+
remote path is derived from the run context and is stable across runs for the
|
|
39
|
+
same key.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
memory_key: A stable single-segment id (a user/thread id). ``None`` or
|
|
43
|
+
empty disables memory.
|
|
44
|
+
audit: Keep the store's append-only audit log (the ``MemoryStore`` default).
|
|
45
|
+
"""
|
|
46
|
+
if not memory_key:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
from flyte.ai.agents.memory import MemoryStore
|
|
50
|
+
|
|
51
|
+
return await MemoryStore.get_or_create.aio(key=memory_key, audit=audit)
|
|
52
|
+
except Exception: # pragma: no cover - memory is best-effort, never fatal
|
|
53
|
+
logger.warning("Could not open agent memory for key %r; continuing without memory.", memory_key)
|
|
54
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Shared report timeline — render agent events into the Flyte task report.
|
|
2
|
+
|
|
3
|
+
Each adapter maps its own SDK's trace/span/event shape onto :class:`ReportTimeline`
|
|
4
|
+
rows; the row formatting, abbreviation, and best-effort error handling now live in
|
|
5
|
+
``flyte.report`` (:class:`flyte.report.Timeline`), so the native ``flyte.ai.agents``
|
|
6
|
+
loop and every adapter render through one implementation. This module keeps the
|
|
7
|
+
adapter-facing names (``ReportTimeline`` defaulting to the ``Agent`` tab, plus the
|
|
8
|
+
``abbrev``/``duration_ms``/``flush_report`` helpers) stable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import flyte.report
|
|
14
|
+
from flyte.report import Timeline, duration_ms
|
|
15
|
+
from flyte.report import abbreviate as abbrev
|
|
16
|
+
|
|
17
|
+
__all__ = ["ReportTimeline", "abbrev", "duration_ms", "flush_report"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReportTimeline(Timeline):
|
|
21
|
+
"""A :class:`flyte.report.Timeline` that defaults to the ``Agent`` report tab."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, tab_name: str = "Agent"):
|
|
24
|
+
super().__init__(tab_name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def flush_report() -> None:
|
|
28
|
+
"""Flush the active Flyte report — a best-effort no-op when there is none.
|
|
29
|
+
|
|
30
|
+
Adapters call this once after a run so the rendered timeline is published.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
await flyte.report.flush()
|
|
34
|
+
except Exception: # pragma: no cover - no active report / local run
|
|
35
|
+
pass
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Make a tool-backing Flyte task resolve to itself on the worker.
|
|
2
|
+
|
|
3
|
+
Every adapter stacks its ``tool`` on top of ``@env.task``, which rebinds
|
|
4
|
+
the module attribute to the tool and shadows the task. Without a guard, the
|
|
5
|
+
worker's default resolver loads the tool, the task runner calls the tool's
|
|
6
|
+
``execute``, and the task re-dispatches itself — recursing without end.
|
|
7
|
+
|
|
8
|
+
:class:`ToolTaskResolver` recovers the real task via the tool's
|
|
9
|
+
``__wrapped_task__`` hook; :func:`attach_tool_resolver` wires it onto the task.
|
|
10
|
+
Each adapter's tool object only has to expose ``__wrapped_task__`` returning its
|
|
11
|
+
backing :class:`~flyte._task.TaskTemplate`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import functools
|
|
17
|
+
import typing
|
|
18
|
+
from functools import partial
|
|
19
|
+
|
|
20
|
+
from flyte._internal.resolvers.default import DefaultTaskResolver
|
|
21
|
+
from flyte._task import AsyncFunctionTaskTemplate, TaskTemplate
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolTaskResolver(DefaultTaskResolver):
|
|
25
|
+
"""Resolver for a task shadowed at module scope by a tool wrapper.
|
|
26
|
+
|
|
27
|
+
Recovers the underlying task via the wrapper's ``__wrapped_task__`` hook so
|
|
28
|
+
the worker runs the task's own body instead of re-dispatching the tool.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def import_path(self) -> str:
|
|
33
|
+
return "flyteplugins.agents.core._tools.ToolTaskResolver"
|
|
34
|
+
|
|
35
|
+
def load_task(self, loader_args): # type: ignore[override]
|
|
36
|
+
task_def = super().load_task(loader_args)
|
|
37
|
+
if isinstance(task_def, TaskTemplate):
|
|
38
|
+
return task_def
|
|
39
|
+
unwrapped = getattr(task_def, "__wrapped_task__", None)
|
|
40
|
+
if isinstance(unwrapped, TaskTemplate):
|
|
41
|
+
return unwrapped
|
|
42
|
+
return task_def
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def attach_tool_resolver(task: typing.Any) -> None:
|
|
46
|
+
"""Point a tool-backing ``@env.task`` at :class:`ToolTaskResolver`.
|
|
47
|
+
|
|
48
|
+
No-op for anything that isn't an async-function task or that already declares
|
|
49
|
+
a custom resolver, so the default resolver is left untouched elsewhere.
|
|
50
|
+
"""
|
|
51
|
+
if isinstance(task, AsyncFunctionTaskTemplate) and task.task_resolver is None:
|
|
52
|
+
task.task_resolver = ToolTaskResolver()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def coerce_tool_args(task: AsyncFunctionTaskTemplate, kwargs: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
56
|
+
"""Coerce LLM-supplied tool args to the task's declared parameter types.
|
|
57
|
+
|
|
58
|
+
LLMs emit JSON numbers without a fractional part as ``int`` (e.g. ``42``), but
|
|
59
|
+
Flyte's type engine rejects an ``int`` where a ``float`` is declared — so the tool
|
|
60
|
+
call fails during input conversion, before the child action is even submitted (the
|
|
61
|
+
action never appears in the UI, and the task body never runs). This converts ``int``
|
|
62
|
+
-> ``float`` for float-annotated params (leaving ``bool`` alone). Best-effort: returns
|
|
63
|
+
the kwargs unchanged if the task's annotations can't be resolved.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
hints = typing.get_type_hints(task.func)
|
|
67
|
+
except Exception: # pragma: no cover - unresolved annotations; pass through
|
|
68
|
+
return kwargs
|
|
69
|
+
coerced = dict(kwargs)
|
|
70
|
+
for name, value in kwargs.items():
|
|
71
|
+
if hints.get(name) is float and isinstance(value, int) and not isinstance(value, bool):
|
|
72
|
+
coerced[name] = float(value)
|
|
73
|
+
return coerced
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def tool(
|
|
77
|
+
func: AsyncFunctionTaskTemplate | typing.Callable | None = None,
|
|
78
|
+
*,
|
|
79
|
+
name: str | None = None,
|
|
80
|
+
description: str | None = None,
|
|
81
|
+
) -> typing.Callable:
|
|
82
|
+
"""Wrap a Flyte ``@env.task`` as a plain async tool function — the generic default.
|
|
83
|
+
|
|
84
|
+
For SDKs that accept plain Python callables as tools (deriving the schema from the
|
|
85
|
+
signature + docstring), this is the whole adapter ``tool``: the returned
|
|
86
|
+
function carries the task's signature (``functools.wraps``), dispatches to
|
|
87
|
+
``task.aio()`` (so each call is a durable Flyte child action), exposes
|
|
88
|
+
``__wrapped_task__``, and wires the backing task to :class:`ToolTaskResolver`.
|
|
89
|
+
Adapters whose SDK needs a native tool type (e.g. OpenAI's
|
|
90
|
+
``FunctionTool``, Claude's MCP ``SdkMcpTool``) provide their own instead.
|
|
91
|
+
|
|
92
|
+
Also accepts any other callable — a plain function or an instance of a callable
|
|
93
|
+
class defining ``__call__`` — and returns it usable as a tool as-is, since the
|
|
94
|
+
plain-callable SDKs derive the schema by inspecting the callable (a class instance
|
|
95
|
+
is inspected through its ``__call__``). A ``name`` or ``description`` override is
|
|
96
|
+
applied to the callable best-effort.
|
|
97
|
+
|
|
98
|
+
Usable bare, parametrized or as a direct call::
|
|
99
|
+
|
|
100
|
+
@tool
|
|
101
|
+
@env.task
|
|
102
|
+
async def get_weather(city: str) -> str: ...
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
if func is None:
|
|
106
|
+
return partial(tool, name=name, description=description)
|
|
107
|
+
if isinstance(func, AsyncFunctionTaskTemplate):
|
|
108
|
+
return _task_to_tool(func, name=name, description=description)
|
|
109
|
+
if not callable(func):
|
|
110
|
+
raise TypeError(f"tool() expects a Flyte @env.task or a callable, got {type(func).__name__!r}.")
|
|
111
|
+
# A plain function or a callable class instance is already usable as a tool as-is.
|
|
112
|
+
return _relabel_callable(func, name=name, description=description)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _relabel_callable(
|
|
116
|
+
func: typing.Callable,
|
|
117
|
+
*,
|
|
118
|
+
name: str | None = None,
|
|
119
|
+
description: str | None = None,
|
|
120
|
+
) -> typing.Callable:
|
|
121
|
+
"""Apply ``name`` / ``description`` overrides to a callable in place, best-effort.
|
|
122
|
+
|
|
123
|
+
Functions accept ``__name__`` / ``__doc__`` assignment; a callable class instance
|
|
124
|
+
may reject it (e.g. ``__slots__``), in which case the override is silently skipped —
|
|
125
|
+
the callable is still returned and usable as a tool.
|
|
126
|
+
"""
|
|
127
|
+
if name:
|
|
128
|
+
try:
|
|
129
|
+
func.__name__ = name # type: ignore[attr-defined]
|
|
130
|
+
except (
|
|
131
|
+
AttributeError,
|
|
132
|
+
TypeError,
|
|
133
|
+
): # pragma: no cover - slotted/immutable callable
|
|
134
|
+
pass
|
|
135
|
+
if description:
|
|
136
|
+
try:
|
|
137
|
+
func.__doc__ = description
|
|
138
|
+
except (
|
|
139
|
+
AttributeError,
|
|
140
|
+
TypeError,
|
|
141
|
+
): # pragma: no cover - slotted/immutable callable
|
|
142
|
+
pass
|
|
143
|
+
return func
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _task_to_tool(
|
|
147
|
+
task: AsyncFunctionTaskTemplate,
|
|
148
|
+
*,
|
|
149
|
+
name: str | None = None,
|
|
150
|
+
description: str | None = None,
|
|
151
|
+
) -> typing.Callable:
|
|
152
|
+
inner = task.func
|
|
153
|
+
|
|
154
|
+
@functools.wraps(inner)
|
|
155
|
+
async def tool(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
|
156
|
+
# In a Flyte task context this submits a durable child action; locally it runs
|
|
157
|
+
# inline. ``functools.wraps`` keeps ``inner``'s signature so the SDK builds the
|
|
158
|
+
# right tool declaration; ``coerce_tool_args`` relaxes LLM int->float args.
|
|
159
|
+
return await task.aio(*args, **coerce_tool_args(task, kwargs))
|
|
160
|
+
|
|
161
|
+
if name:
|
|
162
|
+
tool.__name__ = name
|
|
163
|
+
if description:
|
|
164
|
+
tool.__doc__ = description
|
|
165
|
+
|
|
166
|
+
# The wrapper shadows the task at module scope; expose the real task and wire the
|
|
167
|
+
# shared resolver so it resolves to itself on the worker (no recursion).
|
|
168
|
+
tool.__wrapped_task__ = task # type: ignore[attr-defined]
|
|
169
|
+
tool.task = task # type: ignore[attr-defined]
|
|
170
|
+
attach_tool_resolver(task)
|
|
171
|
+
return tool
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def task_json_schema(task: AsyncFunctionTaskTemplate) -> dict[str, typing.Any]:
|
|
175
|
+
"""The JSON schema of a Flyte task's inputs, via the Flyte type engine.
|
|
176
|
+
|
|
177
|
+
Useful for adapters whose SDK wants a JSON-schema tool definition and that
|
|
178
|
+
prefer Flyte's type-engine schema (correct ``Literal`` enums, ``File``/``Dir``
|
|
179
|
+
/``DataFrame``, dataclasses) over the SDK's own signature inspection.
|
|
180
|
+
"""
|
|
181
|
+
return task.json_schema
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Conformance harness — enforce the common adapter format.
|
|
2
|
+
|
|
3
|
+
Every ``flyteplugins.agents.<sdk>`` adapter ships a one-line test::
|
|
4
|
+
|
|
5
|
+
from flyteplugins.agents.core.testing import assert_adapter_conforms
|
|
6
|
+
import flyteplugins.agents.openai as adapter
|
|
7
|
+
|
|
8
|
+
def test_conformance():
|
|
9
|
+
assert_adapter_conforms(adapter)
|
|
10
|
+
|
|
11
|
+
CI then fails if an adapter drifts from the shared format.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import inspect
|
|
17
|
+
import typing
|
|
18
|
+
|
|
19
|
+
# The keyword surface every ``run_agent`` must accept, so the call shape is the
|
|
20
|
+
# same across SDKs.
|
|
21
|
+
REQUIRED_RUN_AGENT_PARAMS = ("tools", "model", "instructions", "durable", "observability", "memory_key")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def assert_adapter_conforms(adapter: typing.Any) -> None:
|
|
25
|
+
"""Assert that an adapter module implements the common agent-adapter contract.
|
|
26
|
+
|
|
27
|
+
The contract:
|
|
28
|
+
|
|
29
|
+
1. exports a callable ``tool`` that turns an ``@env.task`` into the
|
|
30
|
+
SDK's tool type, attaching :class:`~flyteplugins.agents.core.ToolTaskResolver`
|
|
31
|
+
and exposing ``__wrapped_task__`` (so the task does not self-recurse on the
|
|
32
|
+
worker);
|
|
33
|
+
2. exports an async ``run_agent`` accepting the standard keyword surface.
|
|
34
|
+
|
|
35
|
+
Raises ``AssertionError`` with a specific message on any deviation.
|
|
36
|
+
"""
|
|
37
|
+
import flyte
|
|
38
|
+
|
|
39
|
+
from flyteplugins.agents.core import ToolTaskResolver
|
|
40
|
+
|
|
41
|
+
name = getattr(adapter, "__name__", repr(adapter))
|
|
42
|
+
|
|
43
|
+
tool = getattr(adapter, "tool", None)
|
|
44
|
+
assert callable(tool), f"{name}: must export a callable `tool`"
|
|
45
|
+
|
|
46
|
+
run_agent = getattr(adapter, "run_agent", None)
|
|
47
|
+
assert inspect.iscoroutinefunction(run_agent), f"{name}: must export an async `run_agent`"
|
|
48
|
+
params = inspect.signature(run_agent).parameters
|
|
49
|
+
for required in REQUIRED_RUN_AGENT_PARAMS:
|
|
50
|
+
assert required in params, f"{name}: `run_agent` must accept `{required}`"
|
|
51
|
+
|
|
52
|
+
# tool on a task must expose the real task and wire the resolver.
|
|
53
|
+
env = flyte.TaskEnvironment("agents-core-conformance")
|
|
54
|
+
|
|
55
|
+
@tool
|
|
56
|
+
@env.task
|
|
57
|
+
def _sample(city: str) -> str:
|
|
58
|
+
"""A sample tool task."""
|
|
59
|
+
return city
|
|
60
|
+
|
|
61
|
+
wrapped = getattr(_sample, "__wrapped_task__", None)
|
|
62
|
+
assert wrapped is not None, f"{name}: `tool` result must expose `__wrapped_task__`"
|
|
63
|
+
assert isinstance(getattr(wrapped, "task_resolver", None), ToolTaskResolver), (
|
|
64
|
+
f"{name}: the tool's backing task must use ToolTaskResolver (or it self-recurses on the worker)"
|
|
65
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flyteplugins-agents-core
|
|
3
|
+
Version: 2.5.9
|
|
4
|
+
Summary: Shared contract for Flyte agent-SDK adapters.
|
|
5
|
+
Author-email: Samhita Alla <samhita@union.ai>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: flyte
|
|
9
|
+
|
|
10
|
+
# flyteplugins-agents-core
|
|
11
|
+
|
|
12
|
+
The shared contract every Flyte agent-SDK adapter is built on, so they all
|
|
13
|
+
follow one common format.
|
|
14
|
+
|
|
15
|
+
It is SDK-agnostic — no agent SDK is a dependency here. Adapters depend on it and
|
|
16
|
+
implement the contract; `assert_adapter_conforms` enforces that in CI.
|
|
17
|
+
|
|
18
|
+
## What's in it
|
|
19
|
+
|
|
20
|
+
- `durable_step(request_key, run, *, name, dumps, loads)` — record a call as a
|
|
21
|
+
durable, replayable `flyte.trace` leaf (the model-turn durability mechanism).
|
|
22
|
+
The real, non-serializable call is captured in the `run` closure, so the trace
|
|
23
|
+
only ever sees the serializable `request_key`.
|
|
24
|
+
- `fingerprint(payload)` — a deterministic memo key from a request payload.
|
|
25
|
+
- `ToolTaskResolver` / `attach_tool_resolver(task)` — make a tool-backing task
|
|
26
|
+
resolve to itself on the worker (via the tool's `__wrapped_task__` hook)
|
|
27
|
+
instead of re-dispatching, which would otherwise recurse indefinitely.
|
|
28
|
+
- `ReportTimeline` — render agent events into a tab of the Flyte task report.
|
|
29
|
+
- `flyteplugins.agents.core.testing.assert_adapter_conforms(module)` — the
|
|
30
|
+
conformance check every adapter runs as a one-line test.
|
|
31
|
+
|
|
32
|
+
## The rule every adapter follows
|
|
33
|
+
|
|
34
|
+
The agent run is a Flyte `@env.task` (the durable parent); each model turn
|
|
35
|
+
is a `flyte.trace` (a memoized, replayable leaf); each tool is a Flyte task
|
|
36
|
+
invoked as a durable child action.
|
|
37
|
+
|
|
38
|
+
## Writing an adapter
|
|
39
|
+
|
|
40
|
+
A `flyteplugins-agents-<sdk>` package depends on this core and exposes, at
|
|
41
|
+
minimum, `tool` and `run_agent` (see the conformance contract). Its tool
|
|
42
|
+
object exposes `__wrapped_task__`; `run_agent` runs the SDK's loop inside the
|
|
43
|
+
calling `@env.task`, wraps the model in a durable provider built on `durable_step`,
|
|
44
|
+
and renders the trace via `ReportTimeline`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
flyteplugins/agents/core/__init__.py,sha256=5ZPSn5d2i4L66u8UjcMeOjwX9WZu95Gg7CYoEMcums4,1654
|
|
2
|
+
flyteplugins/agents/core/_durable.py,sha256=Pxh0S1uju4EhfbO7KwMwZmdr-tsRpF3Uhrj_MfmO-fo,2128
|
|
3
|
+
flyteplugins/agents/core/_fingerprint.py,sha256=Rg3f9YkPLYaaeGeVyRN6-8IsRwkplqsFAjTqxF1ERYA,1169
|
|
4
|
+
flyteplugins/agents/core/_memory.py,sha256=9xSerEnMhFRGZyC8Hu4JYvU9hdyacj6iWxU-lOlighs,2380
|
|
5
|
+
flyteplugins/agents/core/_observability.py,sha256=xFZgdww7RjnnIkclAxASrCfwrd6AVt70CFavj7XlsDg,1320
|
|
6
|
+
flyteplugins/agents/core/_tools.py,sha256=oKN2msr5hGpvUW0Vzy6EI4nbhkgIJyjv8GO4gnc2rRk,7383
|
|
7
|
+
flyteplugins/agents/core/testing.py,sha256=k3cROYEGlS_SmO_jz3EI5kTLOx7ZYsgd2mZkFTR34ic,2398
|
|
8
|
+
flyteplugins_agents_core-2.5.9.dist-info/METADATA,sha256=Z0MEvUwrwFJ1yfmFhNidHeojmi7BU8yWffumjaTD080,1972
|
|
9
|
+
flyteplugins_agents_core-2.5.9.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
10
|
+
flyteplugins_agents_core-2.5.9.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
|
|
11
|
+
flyteplugins_agents_core-2.5.9.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flyteplugins
|