flyteplugins-agents-deepagents 2.5.15__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,27 @@
1
+ """Deep Agents adapter for Flyte.
2
+
3
+ Bring your own `Deep Agent <https://docs.langchain.com/oss/python/deepagents/overview>`_
4
+ — LangChain's agent harness with built-in planning, a virtual filesystem, and
5
+ subagents — and run it durably on Flyte. The adapter provides:
6
+
7
+ - :func:`tool` — turn a Flyte ``@env.task`` into a LangChain ``StructuredTool``
8
+ (a ``BaseTool``) that executes as a durable child action (own container/GPU,
9
+ retries, caching). Attach it to the main agent or to a subagent.
10
+ - :func:`run_agent` — run the deep agent (a compiled ``create_deep_agent``
11
+ graph) inside your task and return the final answer. Either pass a pre-built
12
+ ``agent`` or let it build one from ``tools`` + ``model`` + ``instructions``
13
+ (Deep-Agents options like ``subagents=`` pass through).
14
+ - :class:`DurableChatModel` — wrap any LangChain chat model so each model turn
15
+ is recorded/replayed via ``flyte.trace``; use it when building your own agent
16
+ with ``create_deep_agent(model=DurableChatModel(inner=...))``.
17
+
18
+ Each tool call runs as a durable Flyte child action, and the run timeline is
19
+ rendered into the Flyte task report. ``memory_key`` persists the conversation
20
+ *and* the agent's virtual filesystem across runs.
21
+ """
22
+
23
+ from ._durable import DurableChatModel
24
+ from ._run import run_agent, run_agent_sync
25
+ from ._tools import tool
26
+
27
+ __all__ = ["DurableChatModel", "run_agent", "run_agent_sync", "tool"]
@@ -0,0 +1,133 @@
1
+ """Durable, replayable model turns for Deep Agents chat models.
2
+
3
+ A deep agent (``create_deep_agent``) is a compiled LangGraph graph whose loop
4
+ calls the chat model once per iteration (a "model turn").
5
+ :class:`DurableChatModel` wraps any LangChain ``BaseChatModel`` so every turn is
6
+ recorded through the shared :func:`~flyteplugins.agents.core.durable_step` (a
7
+ ``flyte.trace`` leaf). Inside a Flyte task this means a crashed/retried run
8
+ replays completed turns from their recorded outputs instead of re-calling (and
9
+ re-billing) the model. Tool calls run as durable child actions (see
10
+ :func:`flyteplugins.agents.deepagents.tool`), so the whole agent run becomes
11
+ crash-resilient when the enclosing task carries ``retries=...``.
12
+
13
+ The turn is recorded as JSON: the generated messages of the model's
14
+ ``ChatResult`` are serialized with ``message_to_dict`` and rebuilt with
15
+ ``messages_from_dict``, which keeps the recorded turn human-readable in the
16
+ Flyte UI.
17
+
18
+ Tool-calling still works because :meth:`DurableChatModel.bind_tools` delegates to
19
+ the inner model to format the tools, then re-binds the resulting kwargs to *this*
20
+ wrapper — so the deep agent's bound runnable still routes generation through the
21
+ durable override. This also covers subagents: pass the wrapped model as a
22
+ ``SubAgent``'s ``model`` and its turns are durable too.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import typing
29
+
30
+ from flyteplugins.agents.core import durable_step, fingerprint
31
+ from langchain_core.language_models.chat_models import BaseChatModel
32
+
33
+ if typing.TYPE_CHECKING:
34
+ from langchain_core.messages import BaseMessage
35
+ from langchain_core.outputs import ChatResult
36
+ from langchain_core.runnables import Runnable
37
+
38
+
39
+ def _dumps_result(result: "ChatResult") -> str:
40
+ """Serialize a ``ChatResult``'s generated messages to JSON (readable in the UI)."""
41
+ from langchain_core.messages import message_to_dict
42
+
43
+ return json.dumps([message_to_dict(gen.message) for gen in result.generations])
44
+
45
+
46
+ def _loads_result(payload: str) -> "ChatResult":
47
+ """Rebuild a ``ChatResult`` from the JSON written by :func:`_dumps_result`."""
48
+ from langchain_core.messages import messages_from_dict
49
+ from langchain_core.outputs import ChatGeneration, ChatResult
50
+
51
+ messages = messages_from_dict(json.loads(payload))
52
+ return ChatResult(generations=[ChatGeneration(message=m) for m in messages])
53
+
54
+
55
+ class DurableChatModel(BaseChatModel):
56
+ """Wrap a ``BaseChatModel`` so each model turn is durable and replayable.
57
+
58
+ ``_agenerate`` (async) delegates to the inner model and records the turn via
59
+ ``durable_step``. Pass an instance as the deep agent's model —
60
+ ``create_deep_agent(model=DurableChatModel(inner=model), ...)`` — or as a
61
+ subagent's ``model``; ``bind_tools`` and other capabilities are delegated to
62
+ the inner model so tool-calling behaves exactly as the inner model does.
63
+
64
+ Durability is best-effort: if anything in the durable path raises, the turn
65
+ falls back to a direct inner call so a run is never broken by it.
66
+ """
67
+
68
+ inner: BaseChatModel
69
+ """The wrapped chat model that actually generates responses."""
70
+
71
+ @property
72
+ def _llm_type(self) -> str:
73
+ return f"durable-{self.inner._llm_type}"
74
+
75
+ def _turn_key(self, messages: typing.Sequence["BaseMessage"], **kwargs: typing.Any) -> str:
76
+ """Deterministic memo key for a model turn — serialized messages + bound tool names."""
77
+ from langchain_core.messages import messages_to_dict
78
+
79
+ tools = kwargs.get("tools") or []
80
+ tool_names = sorted(
81
+ (t.get("function", {}).get("name") or t.get("name") or str(t)) if isinstance(t, dict) else str(t)
82
+ for t in tools
83
+ )
84
+ return fingerprint(
85
+ {
86
+ "type": self._llm_type,
87
+ "messages": messages_to_dict(list(messages)),
88
+ "tools": tool_names,
89
+ "stop": kwargs.get("stop"),
90
+ }
91
+ )
92
+
93
+ async def _agenerate(self, messages, stop=None, run_manager=None, **kwargs) -> "ChatResult": # type: ignore[override]
94
+ async def _call() -> "ChatResult":
95
+ return await self.inner._agenerate(messages, stop=stop, run_manager=run_manager, **kwargs)
96
+
97
+ try:
98
+ key = self._turn_key(messages, stop=stop, **kwargs)
99
+ return await durable_step(
100
+ key,
101
+ _call,
102
+ name="model_turn",
103
+ dumps=_dumps_result,
104
+ loads=_loads_result,
105
+ )
106
+ except Exception: # pragma: no cover - durability must never break a run
107
+ return await _call()
108
+
109
+ def _generate(self, messages, stop=None, run_manager=None, **kwargs) -> "ChatResult": # type: ignore[override]
110
+ # ``durable_step`` is async; the sync path delegates straight through so
111
+ # sync callers keep working (durability applies to the async agent loop,
112
+ # which is the one Flyte tasks drive).
113
+ return self.inner._generate(messages, stop=stop, run_manager=run_manager, **kwargs)
114
+
115
+ def bind_tools(self, tools: typing.Sequence[typing.Any], **kwargs: typing.Any) -> "Runnable":
116
+ """Format tools via the inner model, but bind them to *this* wrapper.
117
+
118
+ The inner model knows how to convert tools into its provider format; we
119
+ reuse that, then re-bind the resulting kwargs to ``self`` so the runnable
120
+ the deep agent invokes still routes generation through the durable
121
+ override (rather than the inner model directly).
122
+ """
123
+ bound = self.inner.bind_tools(tools, **kwargs)
124
+ bound_kwargs = dict(getattr(bound, "kwargs", {}) or {})
125
+ return self.bind(**bound_kwargs)
126
+
127
+ def get_num_tokens(self, text: str) -> int:
128
+ return self.inner.get_num_tokens(text)
129
+
130
+ def get_num_tokens_from_messages(self, messages, tools=None) -> int: # type: ignore[override]
131
+ if tools is not None:
132
+ return self.inner.get_num_tokens_from_messages(messages, tools=tools)
133
+ return self.inner.get_num_tokens_from_messages(messages)
@@ -0,0 +1,93 @@
1
+ """Cross-run Deep Agents memory — a thin bridge over Flyte's keyed ``MemoryStore``.
2
+
3
+ A deep agent is driven with a messages state (``graph.ainvoke({"messages":
4
+ [...]})``) and also carries a virtual filesystem (the ``files`` state its
5
+ built-in filesystem tools read and write). By default neither survives the run.
6
+ This module bridges both: it resolves a keyed :class:`MemoryStore`, loads the
7
+ prior conversation and files from path-addressed JSON slots, and writes them
8
+ back after the run — so a later run with the same ``memory_key`` continues the
9
+ conversation *and* sees the same virtual filesystem.
10
+
11
+ The transcript is stored as ``messages_to_dict(...)`` output (rebuilt with
12
+ ``messages_from_dict``); the files state is stored as its plain
13
+ ``{path: contents}`` dict. All operations are best-effort: any failure leaves
14
+ the run untouched (memory never breaks a run).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import typing
20
+
21
+ from flyte._logging import logger
22
+ from flyteplugins.agents.core import resolve_memory as _resolve_memory
23
+
24
+ if typing.TYPE_CHECKING:
25
+ from langchain_core.messages import BaseMessage
26
+
27
+ # Path-addressed memory slots inside the MemoryStore.
28
+ _HISTORY_PATH = "deepagents/history.json"
29
+ _FILES_PATH = "deepagents/files.json"
30
+
31
+
32
+ async def resolve_memory(memory_key: str | None) -> typing.Any | None:
33
+ """Resolve a keyed MemoryStore for Deep Agents cross-run memory, or ``None``.
34
+
35
+ Best-effort: returns ``None`` when ``memory_key`` is falsy or no durable
36
+ store can be resolved, so memory never breaks a run.
37
+ """
38
+ if not memory_key:
39
+ return None
40
+ return await _resolve_memory(memory_key)
41
+
42
+
43
+ async def load_history(store: typing.Any) -> list["BaseMessage"]:
44
+ """Load and deserialize the prior conversation from ``store``.
45
+
46
+ Returns an empty list when there is no prior history or on any error.
47
+ """
48
+ if store is None:
49
+ return []
50
+ try:
51
+ from langchain_core.messages import messages_from_dict
52
+
53
+ raw = await store.read_json.aio(_HISTORY_PATH, [])
54
+ return messages_from_dict(raw) if raw else []
55
+ except Exception: # pragma: no cover - memory is best-effort, never fatal
56
+ logger.warning("Could not load Deep Agents memory; continuing without prior history.")
57
+ return []
58
+
59
+
60
+ async def load_files(store: typing.Any) -> dict[str, typing.Any]:
61
+ """Load the agent's prior virtual filesystem (``{path: contents}``) from ``store``.
62
+
63
+ Returns an empty dict when there are no prior files or on any error.
64
+ """
65
+ if store is None:
66
+ return {}
67
+ try:
68
+ return dict(await store.read_json.aio(_FILES_PATH, {}) or {})
69
+ except Exception: # pragma: no cover - memory is best-effort, never fatal
70
+ logger.warning("Could not load Deep Agents files; continuing without prior files.")
71
+ return {}
72
+
73
+
74
+ async def save_state(
75
+ store: typing.Any,
76
+ messages: typing.Sequence["BaseMessage"],
77
+ files: typing.Mapping[str, typing.Any] | None = None,
78
+ ) -> None:
79
+ """Persist the conversation transcript and virtual filesystem to ``store``.
80
+
81
+ Best-effort: logs and returns on any error so memory never breaks a run.
82
+ """
83
+ if store is None or not messages:
84
+ return
85
+ try:
86
+ from langchain_core.messages import messages_to_dict
87
+
88
+ await store.write_json.aio(_HISTORY_PATH, messages_to_dict(list(messages)))
89
+ if files:
90
+ await store.write_json.aio(_FILES_PATH, dict(files))
91
+ await store.save.aio()
92
+ except Exception: # pragma: no cover - memory is best-effort, never fatal
93
+ logger.warning("Could not save Deep Agents memory; conversation will not be resumed.")
@@ -0,0 +1,207 @@
1
+ """``run_agent`` — run a LangChain Deep Agent on Flyte.
2
+
3
+ Deep Agents (LangChain's agent harness) owns the loop: ``create_deep_agent``
4
+ returns a compiled LangGraph graph with built-in planning (todos), a virtual
5
+ filesystem, and subagents. ``run_agent`` runs that loop inside your
6
+ ``@env.task``: it builds a deep agent with Flyte-task tools, drives it, and
7
+ returns the final answer. Each tool call runs as a durable Flyte child action
8
+ (its own container/resources, with retries and caching).
9
+
10
+ The graph is driven with a messages state: ``await graph.ainvoke({"messages":
11
+ [{"role": "user", "content": input}]})``, and the final text is
12
+ ``result["messages"][-1].content``. The result state also carries ``files`` —
13
+ the agent's virtual filesystem — which ``memory_key`` persists across runs
14
+ alongside the conversation.
15
+
16
+ Observability: the run timeline is rendered into the Flyte task report.
17
+
18
+ The adapter minimizes delta between native Deep Agents code and Flyte
19
+ integration by exposing tools that are drop-in ``BaseTool`` instances.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import typing
25
+
26
+ from flyteplugins.agents.core import ReportTimeline, flush_report, sync_variant
27
+
28
+ from ._durable import DurableChatModel
29
+ from ._memory import load_files, load_history, resolve_memory, save_state
30
+ from ._tools import _coerce_tool
31
+
32
+ # Module-level alias for test monkeypatching: tests substitute the graph builder
33
+ # without importing a real provider model.
34
+ _create_deep_agent = None
35
+
36
+
37
+ def _resolve_chat_model(model: typing.Any) -> typing.Any:
38
+ """Return a LangChain chat model. A model instance passes through; a
39
+ ``provider:model`` string resolves via ``init_chat_model``. ``None`` is an
40
+ error — the caller must choose a model."""
41
+ if model is None:
42
+ raise ValueError(
43
+ "Provide `model=` when building the agent (or pass a pre-built `agent=`). "
44
+ 'For example: `model="anthropic:claude-sonnet-4-6"` or a chat-model instance.'
45
+ )
46
+ from langchain_core.language_models.chat_models import BaseChatModel
47
+
48
+ if isinstance(model, BaseChatModel):
49
+ return model
50
+ from langchain.chat_models import init_chat_model
51
+
52
+ return init_chat_model(model)
53
+
54
+
55
+ def _wrap_durable(model: typing.Any) -> typing.Any:
56
+ """Wrap a chat model in :class:`DurableChatModel` when possible.
57
+
58
+ Best-effort: only ``BaseChatModel`` instances are wrappable; anything else
59
+ (or any failure) is returned unchanged so durability never breaks a run.
60
+ """
61
+ try:
62
+ from langchain_core.language_models.chat_models import BaseChatModel
63
+
64
+ if isinstance(model, BaseChatModel) and not isinstance(model, DurableChatModel):
65
+ return DurableChatModel(inner=model)
66
+ except Exception: # pragma: no cover - durability is best-effort, never fatal
67
+ pass
68
+ return model
69
+
70
+
71
+ def _final_text(result: typing.Any) -> str:
72
+ """Extract the agent's final text from a compiled-graph result.
73
+
74
+ Deep agent graphs return a messages state ``{"messages": [...], "files":
75
+ {...}}``; the final answer is the content of the last message. Falls back
76
+ gracefully for other shapes.
77
+ """
78
+ if isinstance(result, dict):
79
+ messages = result.get("messages")
80
+ if messages:
81
+ content = getattr(messages[-1], "content", messages[-1])
82
+ return content if isinstance(content, str) else str(content)
83
+ return ""
84
+ return str(result)
85
+
86
+
87
+ def _result_messages(result: typing.Any) -> list[typing.Any]:
88
+ """Extract the message list from a compiled-graph result (empty on other shapes)."""
89
+ if isinstance(result, dict):
90
+ messages = result.get("messages")
91
+ if messages:
92
+ return list(messages)
93
+ return []
94
+
95
+
96
+ async def run_agent(
97
+ input: str,
98
+ *,
99
+ tools: typing.Sequence[typing.Any] = (),
100
+ model: typing.Any = None,
101
+ instructions: str | None = None,
102
+ agent: typing.Any = None,
103
+ name: str = "deep-agent",
104
+ durable: bool = True,
105
+ observability: bool = True,
106
+ memory_key: str | None = None,
107
+ **agent_kwargs: typing.Any,
108
+ ) -> str:
109
+ """Run a Deep Agent with the given tools and prompt; return the final text.
110
+
111
+ Await this from an async task as ``await run_agent(...)``; from a sync task
112
+ use :func:`run_agent_sync` instead.
113
+
114
+ Call this from inside an ``@env.task`` — that task is the durable parent.
115
+ Within it, each tool call runs as a durable Flyte child action. Give the
116
+ enclosing task ``retries=...`` for self-healing and ``report=True`` to see
117
+ the agent timeline.
118
+
119
+ Provide either a pre-built ``agent`` (a compiled graph from
120
+ ``create_deep_agent``) or ``tools`` + ``model`` to have one built for you.
121
+ Deep-Agents-specific options — ``subagents=``, ``skills=``, ``backend=``,
122
+ ``interrupt_on=``, … — pass through ``**agent_kwargs`` on the builder path.
123
+
124
+ Args:
125
+ input: The user prompt.
126
+ tools: ``tool``-wrapped tools or bare ``@env.task`` templates.
127
+ model: A LangChain chat model instance or a ``provider:model`` string
128
+ (e.g. ``"anthropic:claude-sonnet-4-6"``). Required when ``agent``
129
+ is not given.
130
+ instructions: System prompt for the built agent.
131
+ agent: A pre-built deep agent (a compiled ``create_deep_agent`` graph).
132
+ Mutually exclusive with ``tools``. To get durable model turns on this
133
+ path, build it with ``create_deep_agent(model=DurableChatModel(inner=...))``.
134
+ name: Agent name (for debugging/observability).
135
+ durable: Record/replay each model turn via ``flyte.trace``. Applies when
136
+ the agent is being built — the resolved model is wrapped in
137
+ :class:`DurableChatModel`. A fully pre-built compiled ``agent`` cannot
138
+ be rewrapped (wrap its model yourself, see above); its tool calls
139
+ remain durable regardless.
140
+ observability: Render the run timeline into the Flyte task report.
141
+ memory_key: Stable id (e.g. a user/thread id) for cross-run memory.
142
+ When set, the conversation *and* the agent's virtual filesystem are
143
+ persisted to a keyed ``MemoryStore`` and resumed on a later run with
144
+ the same key.
145
+ **agent_kwargs: Additional kwargs forwarded to ``create_deep_agent``
146
+ (``subagents=``, ``skills=``, ``backend=``, ...).
147
+
148
+ Returns:
149
+ The agent's final output as a string.
150
+ """
151
+ timeline = ReportTimeline() if observability else None
152
+ if timeline is not None:
153
+ timeline.heading("Deep agent")
154
+
155
+ if agent is not None and tools:
156
+ raise ValueError("Pass either `agent` (with its own tools) or `tools`, not both.")
157
+
158
+ # Build the agent (a compiled graph) if not provided.
159
+ if agent is None:
160
+ if _create_deep_agent is None:
161
+ from deepagents import create_deep_agent as create_deep_agent_fn
162
+ else:
163
+ create_deep_agent_fn = _create_deep_agent
164
+
165
+ chat_model = _resolve_chat_model(model)
166
+ # Wrap the chat model so each model turn is durable/replayable. We can
167
+ # only do this on the builder path (a fully pre-built compiled ``agent``
168
+ # owns its own model, which we cannot reach to rewrap).
169
+ if durable:
170
+ chat_model = _wrap_durable(chat_model)
171
+
172
+ tool_objs = [_coerce_tool(t) for t in tools]
173
+ system_prompt = instructions or f"You are a helpful assistant named {name}."
174
+ agent = create_deep_agent_fn(
175
+ model=chat_model,
176
+ tools=tool_objs,
177
+ system_prompt=system_prompt,
178
+ **agent_kwargs,
179
+ )
180
+
181
+ # Cross-run memory: load the prior conversation and virtual filesystem and
182
+ # seed the run with both, then persist the updated state back after.
183
+ store = await resolve_memory(memory_key)
184
+ prior = await load_history(store)
185
+ files = await load_files(store)
186
+
187
+ state: dict[str, typing.Any] = {"messages": [*prior, {"role": "user", "content": input}]}
188
+ if files:
189
+ state["files"] = files
190
+
191
+ result = await agent.ainvoke(state)
192
+
193
+ await save_state(
194
+ store,
195
+ _result_messages(result),
196
+ files=result.get("files") if isinstance(result, dict) else None,
197
+ )
198
+
199
+ final = _final_text(result)
200
+
201
+ if observability:
202
+ await flush_report()
203
+
204
+ return final or ""
205
+
206
+
207
+ run_agent_sync = sync_variant(run_agent)
@@ -0,0 +1,176 @@
1
+ """Turn Flyte tasks into Deep Agents tools that execute as durable actions.
2
+
3
+ Deep Agents (LangChain's agent harness) accepts LangChain ``BaseTool`` instances
4
+ as tools — both on the main agent (``create_deep_agent(tools=[...])``) and on
5
+ subagents (``SubAgent(tools=[...])``). :func:`tool` wraps a Flyte ``@env.task``
6
+ as a LangChain ``StructuredTool`` whose async coroutine dispatches to the task
7
+ via ``task.aio()`` — so when the agent calls the tool, it runs as a durable
8
+ Flyte child action (its own container/resources, with retries and caching)
9
+ rather than inline in the agent's process.
10
+
11
+ The returned object is a real ``StructuredTool`` (a ``BaseTool``), so it drops
12
+ straight into ``create_deep_agent(tools=[...])`` or a subagent's tool list. It
13
+ additionally exposes ``__wrapped_task__`` and ``task`` (via direct attribute
14
+ assignment, which ``StructuredTool`` permits) and wires the backing task to
15
+ :class:`~flyteplugins.agents.core.ToolTaskResolver` so it resolves to itself on
16
+ the worker (no recursion).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import inspect
22
+ import json
23
+ import typing
24
+ from functools import partial
25
+
26
+ from flyte._task import AsyncFunctionTaskTemplate
27
+ from flyteplugins.agents.core import attach_tool_resolver, coerce_tool_args
28
+
29
+
30
+ def tool(
31
+ func: AsyncFunctionTaskTemplate | typing.Callable | None = None,
32
+ *,
33
+ name: str | None = None,
34
+ description: str | None = None,
35
+ ) -> typing.Any:
36
+ """Convert a Flyte task (or plain callable) into a LangChain ``StructuredTool``.
37
+
38
+ - For an ``@env.task``: returns a ``StructuredTool`` whose async coroutine runs
39
+ the task as a durable Flyte child action when the agent invokes it. The input
40
+ schema is derived from the task's typed signature. The backing task is wired to
41
+ :class:`~flyteplugins.agents.core.ToolTaskResolver` and exposed via
42
+ ``__wrapped_task__`` so it resolves to itself on the worker (no recursion).
43
+ - For a plain (async) callable: returns a ``StructuredTool`` that runs it inline.
44
+
45
+ Usable bare, parametrized, or as a direct call::
46
+
47
+ @tool
48
+ @env.task
49
+ async def get_weather(city: str) -> str: ...
50
+ """
51
+ if func is None:
52
+ return partial(tool, name=name, description=description)
53
+ if isinstance(func, AsyncFunctionTaskTemplate):
54
+ return _task_to_tool(func, name=name, description=description)
55
+ return _callable_to_tool(func, name=name, description=description)
56
+
57
+
58
+ def _task_to_tool(
59
+ task: AsyncFunctionTaskTemplate,
60
+ *,
61
+ name: str | None = None,
62
+ description: str | None = None,
63
+ ) -> typing.Any:
64
+ """Build a LangChain ``StructuredTool`` from a Flyte task."""
65
+ from langchain_core.tools import StructuredTool
66
+
67
+ tool_name = name or task.func.__name__
68
+ desc = (description or task.func.__doc__ or f"Run {tool_name}").strip()
69
+
70
+ async def _arun(**kwargs: typing.Any) -> str:
71
+ # In a Flyte task context this submits a durable child action; locally it
72
+ # runs inline. ``coerce_tool_args`` relaxes LLM int->float args so Flyte's
73
+ # type engine doesn't reject e.g. ``amount_usd=42`` for a ``float`` param.
74
+ result = await task.aio(**coerce_tool_args(task, kwargs or {}))
75
+ return _as_content(result)
76
+
77
+ # Wire the shared resolver so the task resolves to itself on the worker.
78
+ attach_tool_resolver(task)
79
+
80
+ # Derive an explicit args schema from the task's typed signature. The coroutine
81
+ # above is ``**kwargs``-only, so LangChain's own inference would produce a single
82
+ # ``kwargs`` object param — we build the real pydantic model instead.
83
+ args_schema = _args_schema_from_callable(task.func, tool_name)
84
+
85
+ structured = StructuredTool.from_function(
86
+ func=None,
87
+ coroutine=_arun,
88
+ name=tool_name,
89
+ description=desc,
90
+ args_schema=args_schema,
91
+ )
92
+
93
+ # Expose the real task and a convenient ``task`` alias so callers/tests can reach
94
+ # the backing task. ``StructuredTool`` is a pydantic model whose ``__setattr__``
95
+ # rejects non-field names, so set them through ``object.__setattr__`` (dunders
96
+ # like ``__wrapped_task__`` would bypass it, but keep both paths uniform).
97
+ object.__setattr__(structured, "__wrapped_task__", task)
98
+ object.__setattr__(structured, "task", task)
99
+ return structured
100
+
101
+
102
+ def _callable_to_tool(
103
+ func: typing.Callable,
104
+ *,
105
+ name: str | None = None,
106
+ description: str | None = None,
107
+ ) -> typing.Any:
108
+ """Build a LangChain ``StructuredTool`` from a plain callable."""
109
+ from langchain_core.tools import StructuredTool
110
+
111
+ tool_name = name or getattr(func, "__name__", "tool")
112
+ desc = (description or func.__doc__ or f"Run {tool_name}").strip()
113
+
114
+ async def _arun(**kwargs: typing.Any) -> str:
115
+ out = func(**(kwargs or {}))
116
+ if inspect.isawaitable(out):
117
+ out = await out
118
+ return _as_content(out)
119
+
120
+ args_schema = _args_schema_from_callable(func, tool_name)
121
+
122
+ return StructuredTool.from_function(
123
+ func=None,
124
+ coroutine=_arun,
125
+ name=tool_name,
126
+ description=desc,
127
+ args_schema=args_schema,
128
+ )
129
+
130
+
131
+ def _args_schema_from_callable(func: typing.Callable, tool_name: str) -> typing.Any | None:
132
+ """Build a pydantic ``args_schema`` from a callable's typed signature.
133
+
134
+ Returns ``None`` (letting LangChain infer) if the signature can't be resolved.
135
+ The model's fields mirror the callable's parameters, with annotations and
136
+ defaults preserved so the LLM sees a correct tool schema.
137
+ """
138
+ from pydantic import create_model
139
+
140
+ try:
141
+ hints = typing.get_type_hints(func)
142
+ sig = inspect.signature(func)
143
+ except Exception: # pragma: no cover - unresolved annotations; let LangChain infer
144
+ return None
145
+
146
+ fields: dict[str, typing.Any] = {}
147
+ for pname, param in sig.parameters.items():
148
+ if pname == "self" or param.kind in (
149
+ inspect.Parameter.VAR_POSITIONAL,
150
+ inspect.Parameter.VAR_KEYWORD,
151
+ ):
152
+ continue
153
+ annotation = hints.get(pname, typing.Any)
154
+ default = ... if param.default is inspect.Parameter.empty else param.default
155
+ fields[pname] = (annotation, default)
156
+
157
+ if not fields:
158
+ return None
159
+ return create_model(f"{tool_name}Args", **fields)
160
+
161
+
162
+ def _as_content(result: typing.Any) -> str:
163
+ """Convert a tool result to a string for LangChain's ToolMessage."""
164
+ if isinstance(result, str):
165
+ return result
166
+ try:
167
+ return json.dumps(result, default=str)
168
+ except (TypeError, ValueError):
169
+ return str(result)
170
+
171
+
172
+ def _coerce_tool(t: typing.Any) -> typing.Any:
173
+ """Coerce a bare ``@env.task`` into a LangChain tool; pass everything else through."""
174
+ if isinstance(t, AsyncFunctionTaskTemplate):
175
+ return tool(t)
176
+ return t
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyteplugins-agents-deepagents
3
+ Version: 2.5.15
4
+ Summary: Run LangChain Deep Agents on Flyte.
5
+ Author-email: Niels Bantilan <niels@union.ai>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: flyteplugins-agents-core
9
+ Requires-Dist: deepagents
10
+
11
+ # flyteplugins-agents-deepagents
12
+
13
+ Run [Deep Agents](https://docs.langchain.com/oss/python/deepagents/overview) —
14
+ LangChain's agent harness with built-in planning, a virtual filesystem, and
15
+ subagents — durably on Flyte.
16
+
17
+ ```bash
18
+ pip install flyteplugins-agents-deepagents
19
+ ```
20
+
21
+ You keep writing Deep Agents code; Flyte is the durable runtime underneath:
22
+
23
+ - **Tools are Flyte tasks.** Stack `@tool` on `@env.task` and each tool call the
24
+ agent (or any of its subagents) makes runs as a durable child action — its own
25
+ container/resources, retries, and caching.
26
+ - **Model turns are replayable.** On the builder path (or by wrapping your own
27
+ model in `DurableChatModel`), every model turn is recorded via `flyte.trace`,
28
+ so a crashed/retried run replays completed turns instead of re-calling (and
29
+ re-billing) the model.
30
+ - **Memory spans runs.** `run_agent(..., memory_key=...)` persists the
31
+ conversation *and* the agent's virtual filesystem to a durable keyed store, so
32
+ a later run with the same key picks up both.
33
+
34
+ ```python
35
+ import flyte
36
+ from flyteplugins.agents.deepagents import run_agent, tool
37
+
38
+ env = flyte.TaskEnvironment("deep-agent")
39
+
40
+ @tool
41
+ @env.task(cache="auto", retries=3)
42
+ async def search_web(query: str) -> str:
43
+ """Search the web for a query."""
44
+ ...
45
+
46
+ @env.task(report=True, retries=3)
47
+ async def research_agent(question: str) -> str:
48
+ return await run_agent(
49
+ question,
50
+ tools=[search_web],
51
+ instructions="You are an expert researcher.",
52
+ model="anthropic:claude-sonnet-4-6",
53
+ subagents=[{
54
+ "name": "critic",
55
+ "description": "Critiques draft answers.",
56
+ "system_prompt": "You are a ruthless critic.",
57
+ }],
58
+ )
59
+ ```
60
+
61
+ To bring your own agent, build it with `create_deep_agent` (attaching
62
+ `@tool`-wrapped tasks natively) and pass it as `run_agent(agent=...)`; wrap the
63
+ model in `DurableChatModel(inner=...)` to keep durable model turns on that path.
64
+ See [examples/](examples/) for the full patterns.
@@ -0,0 +1,9 @@
1
+ flyteplugins/agents/deepagents/__init__.py,sha256=oUTjQAuCAcgvFBDkXNuo5cScllc0lgjRYhmyRg14MZA,1406
2
+ flyteplugins/agents/deepagents/_durable.py,sha256=MfliWgoUVBPcj4zF3bZ6DDWxrC5xVhoq3__wlY17gq8,6068
3
+ flyteplugins/agents/deepagents/_memory.py,sha256=403PfaIPl14YvdYcxJMUc2W1BpbZGmCRVPWPX2FjsKs,3636
4
+ flyteplugins/agents/deepagents/_run.py,sha256=rgS_OwdROsDpUxKrl0NCBsB_iCjjp6e_HN52uhNbwtY,8324
5
+ flyteplugins/agents/deepagents/_tools.py,sha256=KSo3m0yOdJB4RjU_aomBJGMI6nQ80oFM-ava07kIcj8,6794
6
+ flyteplugins_agents_deepagents-2.5.15.dist-info/METADATA,sha256=ORAMIZ2NSrgJz8rxD8hID7mydEMdg8WikmlLb2iBCU8,2304
7
+ flyteplugins_agents_deepagents-2.5.15.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ flyteplugins_agents_deepagents-2.5.15.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
9
+ flyteplugins_agents_deepagents-2.5.15.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